Skip to content

Commit ef8f280

Browse files
docs: add initial documentation for common functionality (#8)
BREAKING CHANGE: 1.0 release
1 parent dbfbf05 commit ef8f280

File tree

5 files changed

+94
-6
lines changed

5 files changed

+94
-6
lines changed

README.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,29 @@
1-
# algokit-utils-py
1+
# AlgoKit Python Utilities
2+
3+
A set of core Algorand utilities written in Python and released via PyPi that make it easier to build solutions on Algorand. This project is part of [AlgoKit](https://github.com/algorandfoundation/algokit-cli).
4+
5+
The goal of this library is to provide intuitive, productive utility functions that make it easier, quicker and safer to build applications on Algorand. Largely these functions wrap the underlying Algorand SDK, but provide a higher level interface with sensible defaults and capabilities for common tasks.
6+
7+
Note: If you prefer Typescript there's an equivalent [Typesript utility library](https://github.com/algorandfoundation/algokit-utils-ts).
8+
9+
## Install
10+
11+
This library can be installed using pip, e.g.:
12+
13+
```
14+
pip install algokit-utils
15+
```
16+
17+
## Guiding principles
18+
19+
This library follows the [Guiding Principles of AlgoKit](https://github.com/algorandfoundation/algokit-cli#guiding-principles).
20+
21+
## Contributing
22+
23+
This is an open source project managed by the Algorand Foundation. See the [AlgoKit contributing page](https://github.com/algorandfoundation/algokit-cli/blob/main/CONTRIBUTING.MD) to learn about making improvements.
24+
25+
To successfully run the tests in this repository you need to be running LocalNet via [AlgoKit](https://github.com/algorandfoundation/algokit-cli):
26+
27+
```
28+
algokit localnet start
29+
```

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ version_toml = "pyproject.toml:tool.poetry.version"
9191
remove_dist = false
9292
build_command = "poetry build --format wheel"
9393
version_source = "tag"
94-
major_on_zero = false
94+
major_on_zero = true
9595
upload_to_repository = false
9696
tag_commit = true
9797
branch = "main"

src/algokit_utils/account.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,32 @@ def get_kmd_wallet_account(
131131
def get_account(
132132
client: AlgodClient, name: str, fund_with_algos: float = 1000, kmd_client: KMDClient | None = None
133133
) -> Account:
134+
"""Returns an Algorand account with private key loaded by convention based on the given name identifier.
135+
136+
## Convention:
137+
138+
**Non-LocalNet:** will load os.environ['{name}_MNEMONIC'] as a mnemonic secret;
139+
Be careful how the mnemonic is handled, never commit it into source control and ideally load it via a
140+
secret storage service rather than the file system.
141+
142+
**LocalNet:** will load the account from a KMD wallet called {name} and if that wallet doesn't exist it will
143+
create it and fund the account for you
144+
145+
This allows you to write code that will work seamlessly in production and local development (LocalNet) without
146+
manual config locally (including when you reset the LocalNet).
147+
148+
@example Default
149+
150+
If you have a mnemonic secret loaded into `os.environ['ACCOUNT_MNEMONIC'] then you can call the following to get
151+
that private key loaded into an account object:
152+
```python
153+
account = await get_account('ACCOUNT', algod)
154+
```
155+
156+
If that code runs against LocalNet then a wallet called 'ACCOUNT' will automatically be created with an account
157+
that is automatically funded with 1000 (default) ALGOs from the default LocalNet dispenser.
158+
"""
159+
134160
mnemonic_key = f"{name.upper()}_MNEMONIC"
135161
mnemonic = os.getenv(mnemonic_key)
136162
if mnemonic:

src/algokit_utils/application_client.py

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import base64
2+
import copy
23
import dataclasses
34
import logging
45
import re
@@ -87,6 +88,8 @@ def num_extra_program_pages(approval: bytes, clear: bytes) -> int:
8788

8889
@dataclasses.dataclass(kw_only=True)
8990
class CommonCallParameters:
91+
"""Common transaction parameters used when making update, delete, opt_in, close_out or clear_state calls"""
92+
9093
signer: TransactionSigner | None = None
9194
sender: str | None = None
9295
suggested_params: transaction.SuggestedParams | None = None
@@ -101,11 +104,15 @@ class CommonCallParameters:
101104

102105
@dataclasses.dataclass(kw_only=True)
103106
class OnCompleteCallParameters(CommonCallParameters):
107+
"""Transaction parameters used when making any call to an Application"""
108+
104109
on_complete: transaction.OnComplete | None = None
105110

106111

107112
@dataclasses.dataclass(kw_only=True)
108113
class CreateCallParameters(OnCompleteCallParameters):
114+
"""Transaction parameters used when making a create call for Application"""
115+
109116
extra_pages: int | None = None
110117

111118

@@ -127,6 +134,8 @@ class CreateCallParametersDict(TypedDict, OnCompleteCallParametersDict, total=Fa
127134

128135
@dataclasses.dataclass(kw_only=True)
129136
class ABICallArgs:
137+
"""Parameters used to update or delete an application when calling deploy()"""
138+
130139
method: ABIMethod | bool | None = None
131140
args: ABIArgsDict = dataclasses.field(default_factory=dict)
132141
suggested_params: transaction.SuggestedParams | None = None
@@ -140,6 +149,8 @@ class ABICallArgs:
140149

141150
@dataclasses.dataclass(kw_only=True)
142151
class ABICreateCallArgs(ABICallArgs):
152+
"""Parameters used to create an application when calling deploy()"""
153+
143154
extra_pages: int | None = None
144155
on_complete: transaction.OnComplete | None = None
145156

@@ -162,6 +173,8 @@ class ABICreateCallArgsDict(TypedDict, ABICallArgsDict, total=False):
162173

163174

164175
class ApplicationClient:
176+
"""A class that wraps an ARC-0032 app spec and provides high productivity methods to deploy and call the app"""
177+
165178
@overload
166179
def __init__(
167180
self,
@@ -280,8 +293,8 @@ def prepare(
280293
app_id: int | None = None,
281294
template_values: au_deploy.TemplateValueDict | None = None,
282295
) -> "ApplicationClient":
283-
import copy
284-
296+
"""Creates a copy of this ApplicationClient, using the new signer, sender and app_id values if provided.
297+
Will also substitute provided template_values into the associated app_spec"""
285298
new_client = copy.copy(self)
286299
new_client._prepare(new_client, signer=signer, sender=sender, app_id=app_id, template_values=template_values)
287300
return new_client
@@ -319,6 +332,15 @@ def deploy(
319332
update_args: ABICallArgs | ABICallArgsDict | None = None,
320333
delete_args: ABICallArgs | ABICallArgsDict | None = None,
321334
) -> au_deploy.DeployResponse:
335+
"""Idempotently deploy (create, update/delete if changed) an app against the given name via the given creator
336+
account, including deploy-time template placeholder substitutions. To understand the architecture decisions
337+
behind this functionality please see
338+
https://github.com/algorandfoundation/algokit-cli/blob/main/docs/architecture-decisions/2023-01-12_smart-contract-deployment.md
339+
Note: if there is a breaking state schema change to an existing app (and `on_schema_break` is set to
340+
'ReplaceApp' the existing app will be deleted and re-created.
341+
Note: if there is an update (different TEAL code) to an existing app (and `on_update` is set to 'ReplaceApp')
342+
the existing app will be deleted and re-created.
343+
"""
322344
before = self._approval_program, self._clear_program, self.sender, self.signer, self.app_id
323345
try:
324346
return self._deploy(
@@ -932,7 +954,7 @@ def clear_state(
932954
return self._execute_atc_tr(atc)
933955

934956
def get_global_state(self, *, raw: bool = False) -> dict[bytes | str, bytes | str | int]:
935-
"""gets the global state info for the app id set"""
957+
"""Gets the global state info associated with app_id"""
936958
global_state = self.algod_client.application_info(self.app_id)
937959
assert isinstance(global_state, dict)
938960
return cast(
@@ -941,7 +963,7 @@ def get_global_state(self, *, raw: bool = False) -> dict[bytes | str, bytes | st
941963
)
942964

943965
def get_local_state(self, account: str | None = None, *, raw: bool = False) -> dict[bytes | str, bytes | str | int]:
944-
"""gets the local state info for the app id set and the account specified"""
966+
"""Gets the local state info for associated app_id and account/sender"""
945967

946968
if account is None:
947969
_, account = self._resolve_signer_sender(self.signer, self.sender)
@@ -954,6 +976,8 @@ def get_local_state(self, account: str | None = None, *, raw: bool = False) -> d
954976
)
955977

956978
def resolve(self, to_resolve: au_spec.DefaultArgumentDict) -> int | str | bytes:
979+
"""Resolves the default value for an ABI method, based on app_spec"""
980+
957981
def _data_check(value: Any) -> int | str | bytes:
958982
if isinstance(value, int | str | bytes):
959983
return value
@@ -1258,6 +1282,7 @@ def substitute_template_and_compile(
12581282
app_spec: au_spec.ApplicationSpecification,
12591283
template_values: au_deploy.TemplateValueDict,
12601284
) -> tuple[Program, Program]:
1285+
"""Substitutes the provided template_values into app_spec and compiles"""
12611286
template_values = dict(template_values or {})
12621287
clear = au_deploy.replace_template_variables(app_spec.clear_program, template_values)
12631288

@@ -1298,6 +1323,8 @@ def execute_atc_with_logic_error(
12981323
approval_program: str | None = None,
12991324
approval_source_map: SourceMap | None = None,
13001325
) -> AtomicTransactionResponse:
1326+
"""Calls execute on provided atc, but will parse any errors into a LogicError,
1327+
if approval_program and approval_source_map are specifed"""
13011328
try:
13021329
return atc.execute(algod_client, wait_rounds=wait_rounds)
13031330
except Exception as ex:

src/algokit_utils/network_clients.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,23 +24,30 @@ class AlgoClientConfig:
2424

2525

2626
def get_algod_client(config: AlgoClientConfig | None = None) -> AlgodClient:
27+
"""Returns an algod SDK client using configuration from environment variables
28+
ALGOD_SERVER, ALGOD_PORT and ALGOD_TOKEN"""
2729
config = config or _get_config_from_environment("ALGOD")
2830
headers = _get_headers(config)
2931
return AlgodClient(config.token, config.server, headers)
3032

3133

3234
def get_indexer_client(config: AlgoClientConfig | None = None) -> IndexerClient:
35+
"""Returns an indexer SDK client using configuration from environment variables
36+
INDEXER_SERVER, INDEXER_PORT and INDEXER_TOKEN"""
3337
config = config or _get_config_from_environment("INDEXER")
3438
headers = _get_headers(config)
3539
return IndexerClient(config.token, config.server, headers) # type: ignore[no-untyped-call]
3640

3741

3842
def is_sandbox(client: AlgodClient) -> bool:
43+
"""Returns True if client genesis is devnet-v1 or sandnet-v1"""
3944
params = client.suggested_params()
4045
return params.gen in ["devnet-v1", "sandnet-v1"]
4146

4247

4348
def get_kmd_client_from_algod_client(client: AlgodClient) -> KMDClient:
49+
"""Returns and SDK KMDClient using the same address as provided AlgodClient, but on port specified by
50+
KMD_PORT environment variable, or 4092 by default"""
4451
# We can only use Kmd on the Sandbox otherwise it's not exposed so this makes some assumptions
4552
# (e.g. same token and server as algod and port 4002 by default)
4653
port = os.getenv("KMD_PORT", "4002")

0 commit comments

Comments
 (0)