Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: algokit-utils-py v3 implementation [WIP DRAFT] #119

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions .github/workflows/check-python.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,13 @@ jobs:
- name: Check types with mypy
run: poetry run mypy

- name: Check docs are up to date
run: |
poetry run poe docs
git diff --quiet --exit-code \
':!docs/html/_sources/apidocs/algokit_utils/algokit_utils.md.txt' \
':!docs/html/apidocs/algokit_utils/algokit_utils.html' \
':!docs/html/searchindex.js' \
':!docs/markdown/apidocs/algokit_utils/algokit_utils.md' \
docs/
# TODO: uncomment after bulk of feature parity with ts is addressed
# - name: Check docs are up to date
# run: |
# poetry run poe docs
# git diff --quiet --exit-code \
# ':!docs/html/_sources/apidocs/algokit_utils/algokit_utils.md.txt' \
# ':!docs/html/apidocs/algokit_utils/algokit_utils.html' \
# ':!docs/html/searchindex.js' \
# ':!docs/markdown/apidocs/algokit_utils/algokit_utils.md' \
# docs/
2 changes: 2 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ repos:
additional_dependencies: []
minimum_pre_commit_version: "0"
files: "^(src|tests)/"
exclude: "^tests/artifacts/"
- id: mypy
name: mypy
description: "`mypy` will check Python types for correctness"
Expand All @@ -33,3 +34,4 @@ repos:
additional_dependencies: []
minimum_pre_commit_version: "2.9.2"
files: "^(src|tests)/"
exclude: "^tests/artifacts/"
16 changes: 16 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Python: Debug Tests",
"type": "debugpy",
"request": "launch",
"program": "${file}",
"purpose": [
"debug-test"
],
"console": "integratedTerminal",
"justMyCode": false
}
]
}
23 changes: 12 additions & 11 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,24 @@
"**/__pycache__": true,
".idea": true
},

// Python
"platformSettings.autoLoad": true,
"python.defaultInterpreterPath": "${workspaceFolder}/.venv",
"python.analysis.extraPaths": ["${workspaceFolder}/src"],
"python.analysis.extraPaths": [
"${workspaceFolder}/src"
],
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff"
},
"python.analysis.exclude": [
"tests/artifacts/**"
],
"python.analysis.typeCheckingMode": "basic",
"ruff.enable": true,
"ruff.lint.run": "onSave",
"ruff.lint.args": ["--config=pyproject.toml"],
"ruff.lint.args": [
"--config=pyproject.toml"
],
"ruff.importStrategy": "fromEnvironment",
"ruff.fixAll": true, //lint and fix all files in workspace
"ruff.organizeImports": true, //organize imports on save
Expand All @@ -37,7 +43,6 @@
"ruff.codeAction.fixViolation": {
"enable": true
},

"mypy.configFile": "pyproject.toml",
// set to empty array to use config from project
"mypy.targets": [],
Expand All @@ -52,11 +57,7 @@
}
]
},

// PowerShell
"[powershell]": {
"editor.defaultFormatter": "ms-vscode.powershell"
},
"powershell.codeFormatting.preset": "Stroustrup",
"python.testing.pytestArgs": ["."]
"python.testing.pytestArgs": [
"."
],
}
69 changes: 35 additions & 34 deletions docs/markdown/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,46 +7,47 @@ The goal of this library is to provide intuitive, productive utility functions t
Largely these functions wrap the underlying Algorand SDK, but provide a higher level interface with sensible defaults and capabilities for common tasks.

#### NOTE

If you prefer TypeScript there’s an equivalent [TypeScript utility library](https://github.com/algorandfoundation/algokit-utils-ts).

[Core principles]() | [Installation]() | [Usage]() | [Capabilities]() | [Reference docs]()

# Contents

* [Account management](capabilities/account.md)
* [`Account`](capabilities/account.md#account)
* [Client management](capabilities/client.md)
* [Network configuration](capabilities/client.md#network-configuration)
* [Clients](capabilities/client.md#clients)
* [App client](capabilities/app-client.md)
* [Design](capabilities/app-client.md#design)
* [Creating an application client](capabilities/app-client.md#creating-an-application-client)
* [Calling methods on the app](capabilities/app-client.md#calling-methods-on-the-app)
* [Composing calls](capabilities/app-client.md#composing-calls)
* [Reading state](capabilities/app-client.md#reading-state)
* [Handling logic errors and diagnosing errors](capabilities/app-client.md#handling-logic-errors-and-diagnosing-errors)
* [App deployment](capabilities/app-deploy.md)
* [Design](capabilities/app-deploy.md#design)
* [Finding apps by creator](capabilities/app-deploy.md#finding-apps-by-creator)
* [Deploying an application](capabilities/app-deploy.md#deploying-an-application)
* [Algo transfers](capabilities/transfer.md)
* [Transferring Algos](capabilities/transfer.md#transferring-algos)
* [Ensuring minimum Algos](capabilities/transfer.md#ensuring-minimum-algos)
* [Transfering Assets](capabilities/transfer.md#transfering-assets)
* [Dispenser](capabilities/transfer.md#dispenser)
* [TestNet Dispenser Client](capabilities/dispenser-client.md)
* [Creating a Dispenser Client](capabilities/dispenser-client.md#creating-a-dispenser-client)
* [Funding an Account](capabilities/dispenser-client.md#funding-an-account)
* [Registering a Refund](capabilities/dispenser-client.md#registering-a-refund)
* [Getting Current Limit](capabilities/dispenser-client.md#getting-current-limit)
* [Error Handling](capabilities/dispenser-client.md#error-handling)
* [Debugger](capabilities/debugger.md)
* [Configuration](capabilities/debugger.md#configuration)
* [Debugging Utilities](capabilities/debugger.md#debugging-utilities)
* [`algokit_utils`](apidocs/algokit_utils/algokit_utils.md)
* [Data](apidocs/algokit_utils/algokit_utils.md#data)
* [Classes](apidocs/algokit_utils/algokit_utils.md#classes)
* [Functions](apidocs/algokit_utils/algokit_utils.md#functions)
- [Account management](capabilities/account.md)
- [`Account`](capabilities/account.md#account)
- [Client management](capabilities/client.md)
- [Network configuration](capabilities/client.md#network-configuration)
- [Clients](capabilities/client.md#clients)
- [App client](capabilities/app-client.md)
- [Design](capabilities/app-client.md#design)
- [Creating an application client](capabilities/app-client.md#creating-an-application-client)
- [Calling methods on the app](capabilities/app-client.md#calling-methods-on-the-app)
- [Composing calls](capabilities/app-client.md#composing-calls)
- [Reading state](capabilities/app-client.md#reading-state)
- [Handling logic errors and diagnosing errors](capabilities/app-client.md#handling-logic-errors-and-diagnosing-errors)
- [App deployment](capabilities/app-deploy.md)
- [Design](capabilities/app-deploy.md#design)
- [Finding apps by creator](capabilities/app-deploy.md#finding-apps-by-creator)
- [Deploying an application](capabilities/app-deploy.md#deploying-an-application)
- [Algo transfers](capabilities/transfer.md)
- [Transferring Algos](capabilities/transfer.md#transferring-algos)
- [Ensuring minimum Algos](capabilities/transfer.md#ensuring-minimum-algos)
- [Transfering Assets](capabilities/transfer.md#transfering-assets)
- [Dispenser](capabilities/transfer.md#dispenser)
- [TestNet Dispenser Client](capabilities/dispenser-client.md)
- [Creating a Dispenser Client](capabilities/dispenser-client.md#creating-a-dispenser-client)
- [Funding an Account](capabilities/dispenser-client.md#funding-an-account)
- [Registering a Refund](capabilities/dispenser-client.md#registering-a-refund)
- [Getting Current Limit](capabilities/dispenser-client.md#getting-current-limit)
- [Error Handling](capabilities/dispenser-client.md#error-handling)
- [Debugger](capabilities/debugger.md)
- [Configuration](capabilities/debugger.md#configuration)
- [Debugging Utilities](capabilities/debugger.md#debugging-utilities)
- [`algokit_utils`](apidocs/algokit_utils/algokit_utils.md)
- [Data](apidocs/algokit_utils/algokit_utils.md#data)
- [Classes](apidocs/algokit_utils/algokit_utils.md#classes)
- [Functions](apidocs/algokit_utils/algokit_utils.md#functions)

<a id="core-principles"></a>

Expand Down
Empty file added legacy_v2_tests/__init__.py
Empty file.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
211 changes: 211 additions & 0 deletions legacy_v2_tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import inspect
import math
import random
import subprocess
from pathlib import Path
from typing import TYPE_CHECKING
from uuid import uuid4

import algosdk.transaction
import pytest
from dotenv import load_dotenv

from algokit_utils import (
DELETABLE_TEMPLATE_NAME,
UPDATABLE_TEMPLATE_NAME,
Account,
ApplicationClient,
ApplicationSpecification,
EnsureBalanceParameters,
ensure_funded,
get_account,
get_algod_client,
get_indexer_client,
get_kmd_client_from_algod_client,
replace_template_variables,
)
from legacy_v2_tests import app_client_test

if TYPE_CHECKING:
from algosdk.kmd import KMDClient
from algosdk.v2client.algod import AlgodClient
from algosdk.v2client.indexer import IndexerClient


@pytest.fixture(autouse=True, scope="session")
def _environment_fixture() -> None:
env_path = Path(__file__).parent / ".." / "example.env"
load_dotenv(env_path)


def check_output_stability(logs: str, *, test_name: str | None = None) -> None:
"""Test that the contract output hasn't changed for an Application, using git diff"""
caller_frame = inspect.stack()[1]
caller_path = Path(caller_frame.filename).resolve()
caller_dir = caller_path.parent
test_name = test_name or caller_frame.function
caller_stem = Path(caller_frame.filename).stem
output_dir = caller_dir / f"{caller_stem}.approvals"
output_dir.mkdir(exist_ok=True)
output_file = output_dir / f"{test_name}.approved.txt"
output_file_str = str(output_file)
output_file_did_exist = output_file.exists()
output_file.write_text(logs, encoding="utf-8")

git_diff = subprocess.run(
[
"git",
"diff",
"--exit-code",
"--no-ext-diff",
"--no-color",
output_file_str,
],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
check=False,
)
# first fail if there are any changes to already committed files, you must manually add them in that case
assert git_diff.returncode == 0, git_diff.stdout

# if first time running, fail in case of accidental change to output directory
if not output_file_did_exist:
pytest.fail(
f"New output folder created at {output_file_str} from test {test_name} - "
"if this was intentional, please commit the files to the git repo"
)


def read_spec(
file_name: str,
*,
updatable: bool | None = None,
deletable: bool | None = None,
template_values: dict | None = None,
) -> ApplicationSpecification:
path = Path(__file__).parent / file_name
spec = ApplicationSpecification.from_json(Path(path).read_text(encoding="utf-8"))

template_variables = template_values or {}
if updatable is not None:
template_variables["UPDATABLE"] = int(updatable)

if deletable is not None:
template_variables["DELETABLE"] = int(deletable)

spec.approval_program = (
replace_template_variables(spec.approval_program, template_variables)
.replace(f"// {UPDATABLE_TEMPLATE_NAME}", "// updatable")
.replace(f"// {DELETABLE_TEMPLATE_NAME}", "// deletable")
)
return spec


def get_specs(
updatable: bool | None = None,
deletable: bool | None = None,
) -> tuple[ApplicationSpecification, ApplicationSpecification, ApplicationSpecification]:
return (
read_spec("app_v1.json", updatable=updatable, deletable=deletable),
read_spec("app_v2.json", updatable=updatable, deletable=deletable),
read_spec("app_v3.json", updatable=updatable, deletable=deletable),
)


def get_unique_name() -> str:
name = str(uuid4()).replace("-", "")
assert name.isalnum()
return name


def is_opted_in(client_fixture: ApplicationClient) -> bool:
_, sender = client_fixture.resolve_signer_sender()
account_info = client_fixture.algod_client.account_info(sender)
assert isinstance(account_info, dict)
apps_local_state = account_info["apps-local-state"]
return any(x for x in apps_local_state if x["id"] == client_fixture.app_id)


@pytest.fixture(scope="session")
def algod_client() -> "AlgodClient":
return get_algod_client()


@pytest.fixture(scope="session")
def kmd_client(algod_client: "AlgodClient") -> "KMDClient":
return get_kmd_client_from_algod_client(algod_client)


@pytest.fixture(scope="session")
def indexer_client() -> "IndexerClient":
return get_indexer_client()


@pytest.fixture
def creator(algod_client: "AlgodClient") -> Account:
creator_name = get_unique_name()
return get_account(algod_client, creator_name)


@pytest.fixture(scope="session")
def funded_account(algod_client: "AlgodClient") -> Account:
creator_name = get_unique_name()
return get_account(algod_client, creator_name)


@pytest.fixture(scope="session")
def app_spec() -> ApplicationSpecification:
app_spec = app_client_test.app.build()
path = Path(__file__).parent / "app_client_test.json"
path.write_text(app_spec.to_json())
return read_spec("app_client_test.json", deletable=True, updatable=True, template_values={"VERSION": 1})


def generate_test_asset(algod_client: "AlgodClient", sender: Account, total: int | None) -> int:
if total is None:
total = math.floor(random.random() * 100) + 20

decimals = 0
asset_name = f"ASA ${math.floor(random.random() * 100) + 1}_${math.floor(random.random() * 100) + 1}_${total}"

params = algod_client.suggested_params()

txn = algosdk.transaction.AssetConfigTxn(
sender=sender.address,
sp=params,
total=total * 10**decimals,
decimals=decimals,
default_frozen=False,
unit_name="",
asset_name=asset_name,
manager=sender.address,
reserve=sender.address,
freeze=sender.address,
clawback=sender.address,
url="https://path/to/my/asset/details",
metadata_hash=None,
note=None,
lease=None,
rekey_to=None,
)

signed_transaction = txn.sign(sender.private_key)
algod_client.send_transaction(signed_transaction)
ptx = algod_client.pending_transaction_info(txn.get_txid())

if isinstance(ptx, dict) and "asset-index" in ptx and isinstance(ptx["asset-index"], int):
return ptx["asset-index"]
else:
raise ValueError("Unexpected response from pending_transaction_info")


def assure_funds(algod_client: "AlgodClient", account: Account) -> None:
ensure_funded(
algod_client,
EnsureBalanceParameters(
account_to_fund=account,
min_spending_balance_micro_algos=300000,
min_funding_increment_micro_algos=1,
),
)
Loading