Skip to content

Commit ef72612

Browse files
feat: updating ts deployer to use v8 utils; updating py to support v3 utils-py (#94)
* refactor: updating ts deployer to use v8 utils; updating py to support v3 utils-py * refactor: further polishing * refactor: remove black/flake-8 from root setup * chore: addressing pr comments * chore: enable auto import completions in pylance * chore: tweak ts utils dependencies * chore: bump to prod versions --------- Co-authored-by: Neil Campbell <neil.campbell@makerx.com.au>
1 parent e33861d commit ef72612

File tree

95 files changed

+1997
-2646
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

95 files changed

+1997
-2646
lines changed

.github/workflows/check-python.yaml

+1-6
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,10 @@ jobs:
2828
- name: Install dependencies
2929
run: poetry env use 3.12 && poetry install --no-interaction --no-root
3030

31-
- name: Check formatting with Black
32-
run: |
33-
# stop the build if there are files that don't meet formatting requirements
34-
poetry run black --check .
35-
3631
- name: Check linting with Ruff
3732
run: |
3833
# stop the build if there are Python syntax errors or undefined names
39-
poetry run ruff .
34+
poetry run ruff check .
4035
4136
- name: Configure git
4237
shell: bash

.pre-commit-config.yaml

+1-10
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,12 @@
11
repos:
22
- repo: local
33
hooks:
4-
- id: black
5-
name: black
6-
description: "Black: The uncompromising Python code formatter"
7-
entry: poetry run black
8-
language: system
9-
minimum_pre_commit_version: 2.9.2
10-
require_serial: true
11-
types_or: [ python, pyi ]
124
- id: ruff
135
name: ruff
146
description: "Run 'ruff' for extremely fast Python linting"
15-
entry: poetry run ruff
7+
entry: poetry run ruff check --fix
168
language: system
179
'types': [python]
18-
args: [--fix]
1910
require_serial: false
2011
additional_dependencies: []
2112
minimum_pre_commit_version: '0'

examples/generators/production_python_smart_contract_python/.algokit.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ test = { commands = [
3434
], description = 'Run smart contract tests' }
3535
audit = { commands = [
3636
'poetry run pip-audit',
37-
], description = 'Audit with pip-audit' }
37+
], description = 'Audit with pip-audit. NOTE: If used with poetry >v2, make sure to install `poetry-plugin-export` as per https://github.com/python-poetry/poetry-plugin-export#installation.' }
3838
lint = { commands = [
3939
'poetry run black --check --diff .',
4040
'poetry run ruff check .',
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,44 @@
11
import logging
22

33
import algokit_utils
4-
from algosdk.v2client.algod import AlgodClient
5-
from algosdk.v2client.indexer import IndexerClient
64

75
logger = logging.getLogger(__name__)
86

97

108
# define deployment behaviour based on supplied app spec
11-
def deploy(
12-
algod_client: AlgodClient,
13-
indexer_client: IndexerClient,
14-
app_spec: algokit_utils.ApplicationSpecification,
15-
deployer: algokit_utils.Account,
16-
) -> None:
9+
def deploy() -> None:
1710
from smart_contracts.artifacts.{{ contract_name }}.{{ contract_name }}_client import (
18-
{{ contract_name.split('_')|map('capitalize')|join }}Client,
11+
{{ contract_name.split('_')|map('capitalize')|join }}Factory,
12+
HelloArgs,
1913
)
2014

21-
app_client = {{ contract_name.split('_')|map('capitalize')|join }}Client(
22-
algod_client,
23-
creator=deployer,
24-
indexer_client=indexer_client,
15+
algorand = algokit_utils.AlgorandClient.from_environment()
16+
deployer_ = algorand.account.from_environment("DEPLOYER")
17+
18+
factory = algorand.client.get_typed_app_factory(
19+
{{ contract_name.split('_')|map('capitalize')|join }}Factory, default_sender=deployer_.address
2520
)
26-
app_client.deploy(
27-
on_schema_break=algokit_utils.OnSchemaBreak.AppendApp,
21+
22+
app_client, result = factory.deploy(
2823
on_update=algokit_utils.OnUpdate.AppendApp,
24+
on_schema_break=algokit_utils.OnSchemaBreak.AppendApp,
2925
)
26+
27+
if result.operation_performed in [
28+
algokit_utils.OperationPerformed.Create,
29+
algokit_utils.OperationPerformed.Replace,
30+
]:
31+
algorand.send.payment(
32+
algokit_utils.PaymentParams(
33+
amount=algokit_utils.AlgoAmount(algo=1),
34+
sender=deployer_.address,
35+
receiver=app_client.app_address,
36+
)
37+
)
38+
3039
name = "world"
31-
response = app_client.hello(name=name)
40+
response = app_client.send.hello(args=HelloArgs(name=name))
3241
logger.info(
33-
f"Called hello on {app_spec.contract.name} ({app_client.app_id}) "
34-
f"with name={name}, received: {response.return_value}"
42+
f"Called hello on {app_client.app_name} ({app_client.app_id}) "
43+
f"with name={name}, received: {response.abi_return}"
3544
)

examples/generators/production_python_smart_contract_python/.github/workflows/production-python-smart-contract-python-cd.yaml

+3-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ jobs:
1919
uses: actions/checkout@v4
2020

2121
- name: Install poetry
22-
run: pipx install poetry
22+
run: |
23+
pipx install poetry
24+
pipx inject poetry poetry-plugin-export
2325
2426
- name: Set up Python 3.12
2527
uses: actions/setup-python@v5

examples/generators/production_python_smart_contract_python/.github/workflows/production-python-smart-contract-python-ci.yaml

+3-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ jobs:
1212
uses: actions/checkout@v4
1313

1414
- name: Install poetry
15-
run: pipx install poetry
15+
run: |
16+
pipx install poetry
17+
pipx inject poetry poetry-plugin-export
1618
1719
- name: Set up Python 3.12
1820
uses: actions/setup-python@v5

examples/generators/production_python_smart_contract_python/.vscode/settings.json

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
},
1414

1515
// Python
16+
"python.analysis.autoImportCompletions": true,
1617
"python.analysis.extraPaths": ["${workspaceFolder}/smart_contracts"],
1718
"python.analysis.diagnosticSeverityOverrides": {
1819
"reportMissingModuleSource": "none"

examples/generators/production_python_smart_contract_python/pyproject.toml

+7-11
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,16 @@ readme = "README.md"
77

88
[tool.poetry.dependencies]
99
python = "^3.12"
10-
algokit-utils = "^2.4.0"
10+
algokit-utils = "^3.0.0"
1111
python-dotenv = "^1.0.0"
1212
algorand-python = "^2.0.0"
1313
algorand-python-testing = "^0.4.0"
1414

1515
[tool.poetry.group.dev.dependencies]
16-
algokit-client-generator = "^1.1.3"
16+
algokit-client-generator = "^2.0.0"
1717
black = {extras = ["d"], version = "*"}
18-
ruff = "^0.1.6"
19-
mypy = "1.11.0"
18+
ruff = "^0.9.4"
19+
mypy = "^1"
2020
pytest = "*"
2121
pytest-cov = "*"
2222
pip-audit = "*"
@@ -29,14 +29,10 @@ build-backend = "poetry.core.masonry.api"
2929

3030
[tool.ruff]
3131
line-length = 120
32-
select = ["E", "F", "ANN", "UP", "N", "C4", "B", "A", "YTT", "W", "FBT", "Q", "RUF", "I"]
33-
ignore = [
34-
"ANN101", # no type for self
35-
"ANN102", # no type for cls
36-
]
37-
unfixable = ["B", "RUF"]
32+
lint.select = ["E", "F", "ANN", "UP", "N", "C4", "B", "A", "YTT", "W", "FBT", "Q", "RUF", "I"]
33+
lint.unfixable = ["B", "RUF"]
3834

39-
[tool.ruff.flake8-annotations]
35+
[tool.ruff.lint.flake8-annotations]
4036
allow-star-arg-any = true
4137
suppress-none-returning = true
4238

examples/generators/production_python_smart_contract_python/smart_contracts/__main__.py

+158-19
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,173 @@
1+
import dataclasses
2+
import importlib
13
import logging
4+
import subprocess
25
import sys
6+
from collections.abc import Callable
37
from pathlib import Path
8+
from shutil import rmtree
49

10+
from algokit_utils.config import config
511
from dotenv import load_dotenv
612

7-
from smart_contracts._helpers.build import build
8-
from smart_contracts._helpers.config import contracts
9-
from smart_contracts._helpers.deploy import deploy
10-
11-
# Uncomment the following lines to enable auto generation of AVM Debugger compliant sourcemap and simulation trace file.
13+
# Set trace_all to True to capture all transactions, defaults to capturing traces only on failure
1214
# Learn more about using AlgoKit AVM Debugger to debug your TEAL source codes and inspect various kinds of
1315
# Algorand transactions in atomic groups -> https://github.com/algorandfoundation/algokit-avm-vscode-debugger
14-
# from algokit_utils.config import config
15-
# config.configure(debug=True, trace_all=True)
16+
config.configure(debug=True, trace_all=False)
17+
18+
# Set up logging and load environment variables.
1619
logging.basicConfig(
1720
level=logging.DEBUG, format="%(asctime)s %(levelname)-10s: %(message)s"
1821
)
1922
logger = logging.getLogger(__name__)
2023
logger.info("Loading .env")
21-
# For manual script execution (bypassing `algokit project deploy`) with a custom .env,
22-
# modify `load_dotenv()` accordingly. For example, `load_dotenv('.env.localnet')`.
2324
load_dotenv()
25+
26+
# Determine the root path based on this file's location.
2427
root_path = Path(__file__).parent
2528

29+
# ----------------------- Contract Configuration ----------------------- #
30+
31+
32+
@dataclasses.dataclass
33+
class SmartContract:
34+
path: Path
35+
name: str
36+
deploy: Callable[[], None] | None = None
37+
38+
39+
def import_contract(folder: Path) -> Path:
40+
"""Imports the contract from a folder if it exists."""
41+
contract_path = folder / "contract.py"
42+
if contract_path.exists():
43+
return contract_path
44+
else:
45+
raise Exception(f"Contract not found in {folder}")
46+
47+
48+
def import_deploy_if_exists(folder: Path) -> Callable[[], None] | None:
49+
"""Imports the deploy function from a folder if it exists."""
50+
try:
51+
module_name = f"{folder.parent.name}.{folder.name}.deploy_config"
52+
deploy_module = importlib.import_module(module_name)
53+
return deploy_module.deploy # type: ignore[no-any-return, misc]
54+
except ImportError:
55+
return None
56+
57+
58+
def has_contract_file(directory: Path) -> bool:
59+
"""Checks whether the directory contains a contract.py file."""
60+
return (directory / "contract.py").exists()
61+
62+
63+
# Use the current directory (root_path) as the base for contract folders and exclude
64+
# folders that start with '_' (internal helpers).
65+
contracts: list[SmartContract] = [
66+
SmartContract(
67+
path=import_contract(folder),
68+
name=folder.name,
69+
deploy=import_deploy_if_exists(folder),
70+
)
71+
for folder in root_path.iterdir()
72+
if folder.is_dir() and has_contract_file(folder) and not folder.name.startswith("_")
73+
]
74+
75+
# -------------------------- Build Logic -------------------------- #
76+
77+
deployment_extension = "py"
78+
79+
80+
def _get_output_path(output_dir: Path, deployment_extension: str) -> Path:
81+
"""Constructs the output path for the generated client file."""
82+
return output_dir / Path(
83+
"{contract_name}"
84+
+ ("_client" if deployment_extension == "py" else "Client")
85+
+ f".{deployment_extension}"
86+
)
87+
88+
89+
def build(output_dir: Path, contract_path: Path) -> Path:
90+
"""
91+
Builds the contract by exporting (compiling) its source and generating a client.
92+
If the output directory already exists, it is cleared.
93+
"""
94+
output_dir = output_dir.resolve()
95+
if output_dir.exists():
96+
rmtree(output_dir)
97+
output_dir.mkdir(exist_ok=True, parents=True)
98+
logger.info(f"Exporting {contract_path} to {output_dir}")
99+
100+
build_result = subprocess.run(
101+
[
102+
"algokit",
103+
"--no-color",
104+
"compile",
105+
"python",
106+
str(contract_path.resolve()),
107+
f"--out-dir={output_dir}",
108+
"--no-output-arc32",
109+
"--output-arc56",
110+
"--output-source-map",
111+
],
112+
stdout=subprocess.PIPE,
113+
stderr=subprocess.STDOUT,
114+
text=True,
115+
)
116+
if build_result.returncode:
117+
raise Exception(f"Could not build contract:\n{build_result.stdout}")
118+
119+
# Look for arc56.json files and generate the client based on them.
120+
app_spec_file_names: list[str] = [
121+
file.name for file in output_dir.glob("*.arc56.json")
122+
]
123+
124+
client_file: str | None = None
125+
if not app_spec_file_names:
126+
logger.warning(
127+
"No '*.arc56.json' file found (likely a logic signature being compiled). Skipping client generation."
128+
)
129+
else:
130+
for file_name in app_spec_file_names:
131+
client_file = file_name
132+
print(file_name)
133+
generate_result = subprocess.run(
134+
[
135+
"algokit",
136+
"generate",
137+
"client",
138+
str(output_dir),
139+
"--output",
140+
str(_get_output_path(output_dir, deployment_extension)),
141+
],
142+
stdout=subprocess.PIPE,
143+
stderr=subprocess.STDOUT,
144+
text=True,
145+
)
146+
if generate_result.returncode:
147+
if "No such command" in generate_result.stdout:
148+
raise Exception(
149+
"Could not generate typed client, requires AlgoKit 2.0.0 or later. Please update AlgoKit"
150+
)
151+
else:
152+
raise Exception(
153+
f"Could not generate typed client:\n{generate_result.stdout}"
154+
)
155+
if client_file:
156+
return output_dir / client_file
157+
return output_dir
158+
159+
160+
# --------------------------- Main Logic --------------------------- #
161+
26162

27163
def main(action: str, contract_name: str | None = None) -> None:
164+
"""Main entry point to build and/or deploy smart contracts."""
28165
artifact_path = root_path / "artifacts"
29-
30-
# Filter contracts if a specific contract name is provided
166+
# Filter contracts based on an optional specific contract name.
31167
filtered_contracts = [
32-
c for c in contracts if contract_name is None or c.name == contract_name
168+
contract
169+
for contract in contracts
170+
if contract_name is None or contract.name == contract_name
33171
]
34172

35173
match action:
@@ -44,23 +182,24 @@ def main(action: str, contract_name: str | None = None) -> None:
44182
(
45183
file.name
46184
for file in output_dir.iterdir()
47-
if file.is_file() and file.suffixes == [".arc32", ".json"]
185+
if file.is_file() and file.suffixes == [".arc56", ".json"]
48186
),
49187
None,
50188
)
51189
if app_spec_file_name is None:
52-
raise Exception("Could not deploy app, .arc32.json file not found")
53-
app_spec_path = output_dir / app_spec_file_name
190+
raise Exception("Could not deploy app, .arc56.json file not found")
54191
if contract.deploy:
55192
logger.info(f"Deploying app {contract.name}")
56-
deploy(app_spec_path, contract.deploy)
193+
contract.deploy()
57194
case "all":
58195
for contract in filtered_contracts:
59196
logger.info(f"Building app at {contract.path}")
60-
app_spec_path = build(artifact_path / contract.name, contract.path)
197+
build(artifact_path / contract.name, contract.path)
61198
if contract.deploy:
62-
logger.info(f"Deploying {contract.path.name}")
63-
deploy(app_spec_path, contract.deploy)
199+
logger.info(f"Deploying {contract.name}")
200+
contract.deploy()
201+
case _:
202+
logger.error(f"Unknown action: {action}")
64203

65204

66205
if __name__ == "__main__":

examples/generators/production_python_smart_contract_python/smart_contracts/_helpers/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)