algokit-utils 2.4.0b1__py3-none-any.whl → 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of algokit-utils might be problematic. Click here for more details.

Files changed (70) hide show
  1. algokit_utils/__init__.py +23 -181
  2. algokit_utils/_debugging.py +89 -45
  3. algokit_utils/_legacy_v2/__init__.py +177 -0
  4. algokit_utils/{_ensure_funded.py → _legacy_v2/_ensure_funded.py} +21 -24
  5. algokit_utils/{_transfer.py → _legacy_v2/_transfer.py} +26 -23
  6. algokit_utils/_legacy_v2/account.py +203 -0
  7. algokit_utils/_legacy_v2/application_client.py +1472 -0
  8. algokit_utils/_legacy_v2/application_specification.py +21 -0
  9. algokit_utils/_legacy_v2/asset.py +168 -0
  10. algokit_utils/_legacy_v2/common.py +28 -0
  11. algokit_utils/_legacy_v2/deploy.py +822 -0
  12. algokit_utils/_legacy_v2/logic_error.py +14 -0
  13. algokit_utils/{models.py → _legacy_v2/models.py} +16 -45
  14. algokit_utils/_legacy_v2/network_clients.py +144 -0
  15. algokit_utils/account.py +12 -183
  16. algokit_utils/accounts/__init__.py +2 -0
  17. algokit_utils/accounts/account_manager.py +912 -0
  18. algokit_utils/accounts/kmd_account_manager.py +161 -0
  19. algokit_utils/algorand.py +359 -0
  20. algokit_utils/application_client.py +9 -1447
  21. algokit_utils/application_specification.py +39 -197
  22. algokit_utils/applications/__init__.py +7 -0
  23. algokit_utils/applications/abi.py +275 -0
  24. algokit_utils/applications/app_client.py +2108 -0
  25. algokit_utils/applications/app_deployer.py +725 -0
  26. algokit_utils/applications/app_factory.py +1134 -0
  27. algokit_utils/applications/app_manager.py +578 -0
  28. algokit_utils/applications/app_spec/__init__.py +2 -0
  29. algokit_utils/applications/app_spec/arc32.py +207 -0
  30. algokit_utils/applications/app_spec/arc56.py +989 -0
  31. algokit_utils/applications/enums.py +40 -0
  32. algokit_utils/asset.py +32 -168
  33. algokit_utils/assets/__init__.py +1 -0
  34. algokit_utils/assets/asset_manager.py +336 -0
  35. algokit_utils/beta/_utils.py +36 -0
  36. algokit_utils/beta/account_manager.py +4 -195
  37. algokit_utils/beta/algorand_client.py +4 -314
  38. algokit_utils/beta/client_manager.py +5 -74
  39. algokit_utils/beta/composer.py +5 -712
  40. algokit_utils/clients/__init__.py +2 -0
  41. algokit_utils/clients/client_manager.py +738 -0
  42. algokit_utils/clients/dispenser_api_client.py +224 -0
  43. algokit_utils/common.py +8 -26
  44. algokit_utils/config.py +76 -29
  45. algokit_utils/deploy.py +7 -894
  46. algokit_utils/dispenser_api.py +8 -176
  47. algokit_utils/errors/__init__.py +1 -0
  48. algokit_utils/errors/logic_error.py +121 -0
  49. algokit_utils/logic_error.py +7 -82
  50. algokit_utils/models/__init__.py +8 -0
  51. algokit_utils/models/account.py +217 -0
  52. algokit_utils/models/amount.py +200 -0
  53. algokit_utils/models/application.py +91 -0
  54. algokit_utils/models/network.py +29 -0
  55. algokit_utils/models/simulate.py +11 -0
  56. algokit_utils/models/state.py +68 -0
  57. algokit_utils/models/transaction.py +100 -0
  58. algokit_utils/network_clients.py +7 -128
  59. algokit_utils/protocols/__init__.py +2 -0
  60. algokit_utils/protocols/account.py +22 -0
  61. algokit_utils/protocols/typed_clients.py +108 -0
  62. algokit_utils/transactions/__init__.py +3 -0
  63. algokit_utils/transactions/transaction_composer.py +2499 -0
  64. algokit_utils/transactions/transaction_creator.py +688 -0
  65. algokit_utils/transactions/transaction_sender.py +1219 -0
  66. {algokit_utils-2.4.0b1.dist-info → algokit_utils-3.0.0.dist-info}/METADATA +11 -7
  67. algokit_utils-3.0.0.dist-info/RECORD +70 -0
  68. {algokit_utils-2.4.0b1.dist-info → algokit_utils-3.0.0.dist-info}/WHEEL +1 -1
  69. algokit_utils-2.4.0b1.dist-info/RECORD +0 -24
  70. {algokit_utils-2.4.0b1.dist-info → algokit_utils-3.0.0.dist-info}/LICENSE +0 -0
@@ -0,0 +1,224 @@
1
+ import contextlib
2
+ import enum
3
+ import os
4
+ from dataclasses import dataclass
5
+ from typing import overload
6
+
7
+ import httpx
8
+ from typing_extensions import deprecated
9
+
10
+ from algokit_utils.config import config
11
+
12
+ __all__ = [
13
+ "DISPENSER_ACCESS_TOKEN_KEY",
14
+ "DISPENSER_ASSETS",
15
+ "DISPENSER_REQUEST_TIMEOUT",
16
+ "DispenserApiConfig",
17
+ "DispenserAsset",
18
+ "DispenserAssetName",
19
+ "DispenserFundResponse",
20
+ "DispenserLimitResponse",
21
+ "TestNetDispenserApiClient",
22
+ ]
23
+
24
+
25
+ logger = config.logger
26
+
27
+
28
+ class DispenserApiConfig:
29
+ BASE_URL = "https://api.dispenser.algorandfoundation.tools"
30
+
31
+
32
+ class DispenserAssetName(enum.IntEnum):
33
+ ALGO = 0
34
+
35
+
36
+ @dataclass
37
+ class DispenserAsset:
38
+ asset_id: int
39
+ """The ID of the asset"""
40
+ decimals: int
41
+ """The amount of decimal places the asset was created with"""
42
+ description: str
43
+ """The description of the asset"""
44
+
45
+
46
+ @dataclass
47
+ class DispenserFundResponse:
48
+ tx_id: str
49
+ """The transaction ID of the funded transaction"""
50
+ amount: int
51
+ """The amount of Algos funded"""
52
+
53
+
54
+ @dataclass
55
+ class DispenserLimitResponse:
56
+ amount: int
57
+ """The amount of Algos that can be funded"""
58
+
59
+
60
+ DISPENSER_ASSETS = {
61
+ DispenserAssetName.ALGO: DispenserAsset(
62
+ asset_id=0,
63
+ decimals=6,
64
+ description="Algo",
65
+ ),
66
+ }
67
+ DISPENSER_REQUEST_TIMEOUT = 15
68
+ DISPENSER_ACCESS_TOKEN_KEY = "ALGOKIT_DISPENSER_ACCESS_TOKEN"
69
+
70
+
71
+ class TestNetDispenserApiClient:
72
+ """
73
+ Client for interacting with the [AlgoKit TestNet Dispenser API](https://github.com/algorandfoundation/algokit/blob/main/docs/testnet_api.md).
74
+ To get started create a new access token via `algokit dispenser login --ci`
75
+ and pass it to the client constructor as `auth_token`.
76
+ Alternatively set the access token as environment variable `ALGOKIT_DISPENSER_ACCESS_TOKEN`,
77
+ and it will be auto loaded. If both are set, the constructor argument takes precedence.
78
+
79
+ Default request timeout is 15 seconds. Modify by passing `request_timeout` to the constructor.
80
+ """
81
+
82
+ # NOTE: ensures pytest does not think this is a test
83
+ # https://docs.pytest.org/en/stable/example/pythoncollection.html#customizing-test-collection
84
+ __test__ = False
85
+ auth_token: str
86
+ request_timeout = DISPENSER_REQUEST_TIMEOUT
87
+
88
+ def __init__(self, auth_token: str | None = None, request_timeout: int = DISPENSER_REQUEST_TIMEOUT):
89
+ auth_token_from_env = os.getenv(DISPENSER_ACCESS_TOKEN_KEY)
90
+
91
+ if auth_token:
92
+ self.auth_token = auth_token
93
+ elif auth_token_from_env:
94
+ self.auth_token = auth_token_from_env
95
+ else:
96
+ raise Exception(
97
+ f"Can't init AlgoKit TestNet Dispenser API client "
98
+ f"because neither environment variable {DISPENSER_ACCESS_TOKEN_KEY} or "
99
+ "the auth_token were provided."
100
+ )
101
+
102
+ self.request_timeout = request_timeout
103
+
104
+ def _process_dispenser_request(
105
+ self, *, auth_token: str, url_suffix: str, data: dict | None = None, method: str = "POST"
106
+ ) -> httpx.Response:
107
+ """
108
+ Generalized method to process http requests to dispenser API
109
+ """
110
+
111
+ headers = {"Authorization": f"Bearer {(auth_token)}"}
112
+
113
+ # Set request arguments
114
+ request_args = {
115
+ "url": f"{DispenserApiConfig.BASE_URL}/{url_suffix}",
116
+ "headers": headers,
117
+ "timeout": self.request_timeout,
118
+ }
119
+
120
+ if method.upper() != "GET" and data is not None:
121
+ request_args["json"] = data
122
+
123
+ try:
124
+ response: httpx.Response = getattr(httpx, method.lower())(**request_args)
125
+ response.raise_for_status()
126
+ return response
127
+
128
+ except httpx.HTTPStatusError as err:
129
+ error_message = f"Error processing dispenser API request: {err.response.status_code}"
130
+ error_response = None
131
+ with contextlib.suppress(Exception):
132
+ error_response = err.response.json()
133
+
134
+ if error_response and error_response.get("code"):
135
+ error_message = error_response.get("code")
136
+
137
+ elif err.response.status_code == httpx.codes.BAD_REQUEST:
138
+ error_message = err.response.json()["message"]
139
+
140
+ raise Exception(error_message) from err
141
+
142
+ except Exception as err:
143
+ error_message = "Error processing dispenser API request"
144
+ logger.debug(f"{error_message}: {err}", exc_info=True)
145
+ raise err
146
+
147
+ @overload
148
+ def fund(self, address: str, amount: int) -> DispenserFundResponse: ...
149
+
150
+ @deprecated("Asset ID parameter is deprecated. Can now use `fund(address, amount)` instead.")
151
+ @overload
152
+ def fund(self, address: str, amount: int, asset_id: int | None = None) -> DispenserFundResponse: ...
153
+
154
+ def fund(self, address: str, amount: int, asset_id: int | None = None) -> DispenserFundResponse: # noqa: ARG002
155
+ """
156
+ Fund an account with Algos from the dispenser API
157
+
158
+ :param address: The address to fund
159
+ :param amount: The amount of Algos to fund
160
+ :param asset_id: The asset ID to fund (deprecated)
161
+ :return: The transaction ID of the funded transaction
162
+ :raises Exception: If the dispenser API request fails
163
+
164
+ :example:
165
+ >>> dispenser_client = TestNetDispenserApiClient()
166
+ >>> dispenser_client.fund(address="SENDER_ADDRESS", amount=1000000)
167
+ """
168
+
169
+ try:
170
+ response = self._process_dispenser_request(
171
+ auth_token=self.auth_token,
172
+ url_suffix=f"fund/{DISPENSER_ASSETS[DispenserAssetName.ALGO].asset_id}",
173
+ data={
174
+ "receiver": address,
175
+ "amount": amount,
176
+ "assetID": DISPENSER_ASSETS[DispenserAssetName.ALGO].asset_id,
177
+ },
178
+ method="POST",
179
+ )
180
+
181
+ content = response.json()
182
+ return DispenserFundResponse(tx_id=content["txID"], amount=content["amount"])
183
+
184
+ except Exception as err:
185
+ logger.exception(f"Error funding account {address}: {err}")
186
+ raise err
187
+
188
+ def refund(self, refund_txn_id: str) -> None:
189
+ """
190
+ Register a refund for a transaction with the dispenser API
191
+ """
192
+
193
+ try:
194
+ self._process_dispenser_request(
195
+ auth_token=self.auth_token,
196
+ url_suffix="refund",
197
+ data={"refundTransactionID": refund_txn_id},
198
+ method="POST",
199
+ )
200
+
201
+ except Exception as err:
202
+ logger.exception(f"Error issuing refund for txn_id {refund_txn_id}: {err}")
203
+ raise err
204
+
205
+ def get_limit(
206
+ self,
207
+ address: str,
208
+ ) -> DispenserLimitResponse:
209
+ """
210
+ Get current limit for an account with Algos from the dispenser API
211
+ """
212
+
213
+ try:
214
+ response = self._process_dispenser_request(
215
+ auth_token=self.auth_token,
216
+ url_suffix=f"fund/{DISPENSER_ASSETS[DispenserAssetName.ALGO].asset_id}/limit",
217
+ method="GET",
218
+ )
219
+ content = response.json()
220
+
221
+ return DispenserLimitResponse(amount=content["amount"])
222
+ except Exception as err:
223
+ logger.exception(f"Error setting limit for account {address}: {err}")
224
+ raise err
algokit_utils/common.py CHANGED
@@ -1,28 +1,10 @@
1
- """
2
- This module contains common classes and methods that are reused in more than one file.
3
- """
1
+ import warnings
4
2
 
5
- import base64
6
- import typing
3
+ warnings.warn(
4
+ "The legacy v2 common module is deprecated and will be removed in a future version. "
5
+ "Refer to `CompiledTeal` class from `algokit_utils` instead.",
6
+ DeprecationWarning,
7
+ stacklevel=2,
8
+ )
7
9
 
8
- from algosdk.source_map import SourceMap
9
-
10
- from algokit_utils import deploy
11
-
12
- if typing.TYPE_CHECKING:
13
- from algosdk.v2client.algod import AlgodClient
14
-
15
-
16
- class Program:
17
- """A compiled TEAL program"""
18
-
19
- def __init__(self, program: str, client: "AlgodClient"):
20
- """
21
- Fully compile the program source to binary and generate a
22
- source map for matching pc to line number
23
- """
24
- self.teal = program
25
- result: dict = client.compile(deploy.strip_comments(self.teal), source_map=True)
26
- self.raw_binary = base64.b64decode(result["result"])
27
- self.binary_hash: str = result["hash"]
28
- self.source_map = SourceMap(result["sourcemap"])
10
+ from algokit_utils._legacy_v2.common import * # noqa: F403, E402
algokit_utils/config.py CHANGED
@@ -3,42 +3,82 @@ import os
3
3
  from collections.abc import Callable
4
4
  from pathlib import Path
5
5
 
6
- logger = logging.getLogger(__name__)
7
-
8
6
  # Environment variable to override the project root
9
7
  ALGOKIT_PROJECT_ROOT = os.getenv("ALGOKIT_PROJECT_ROOT")
10
8
  ALGOKIT_CONFIG_FILENAME = ".algokit.toml"
11
9
 
12
10
 
11
+ class AlgoKitLogger(logging.Logger):
12
+ def __init__(self, name: str = "algokit-utils-py", level: int = logging.NOTSET):
13
+ super().__init__(name, level)
14
+
15
+ def _log(self, level: int, msg: object, args, exc_info=None, extra=None, stack_info=False, stacklevel=1) -> None: # type: ignore[no-untyped-def] # noqa: FBT002, ANN001
16
+ """
17
+ Overrides the base _log method to allow suppressing individual log calls.
18
+ When a caller passes suppress_log=True in the extra keyword, the log call is ignored.
19
+ """
20
+
21
+ # Check if the 'suppress_log' flag is provided in the extra dictionary.
22
+ if extra and extra.get("suppress_log", False):
23
+ return
24
+ # Call the parent _log
25
+ super()._log(level, msg, args, exc_info, extra, stack_info, stacklevel)
26
+
27
+ @classmethod
28
+ def get_null_logger(cls) -> logging.Logger:
29
+ """Return a logger that does nothing (a null logger)."""
30
+ null_logger = logging.getLogger("null")
31
+ null_logger.handlers.clear()
32
+ null_logger.addHandler(logging.NullHandler())
33
+ null_logger.propagate = False
34
+ return null_logger
35
+
36
+
37
+ # Set our custom logger class as the default.
38
+ logging.setLoggerClass(AlgoKitLogger)
39
+
40
+
13
41
  class UpdatableConfig:
14
- """Class to manage and update configuration settings for the AlgoKit project.
42
+ """
43
+ Class to manage and update configuration settings for the AlgoKit project.
15
44
 
16
45
  Attributes:
17
46
  debug (bool): Indicates whether debug mode is enabled.
18
47
  project_root (Path | None): The path to the project root directory.
19
48
  trace_all (bool): Indicates whether to trace all operations.
20
- trace_buffer_size_mb (int): The size of the trace buffer in megabytes.
49
+ trace_buffer_size_mb (int | float): The size of the trace buffer in megabytes.
21
50
  max_search_depth (int): The maximum depth to search for a specific file.
51
+ populate_app_call_resources (bool): Whether to populate app call resources.
52
+ logger (logging.Logger): The logger instance to use. Defaults to an AlgoKitLogger instance.
22
53
  """
23
54
 
24
55
  def __init__(self) -> None:
56
+ self._logger: logging.Logger = AlgoKitLogger()
25
57
  self._debug: bool = False
26
58
  self._project_root: Path | None = None
27
59
  self._trace_all: bool = False
28
60
  self._trace_buffer_size_mb: int | float = 256 # megabytes
29
61
  self._max_search_depth: int = 10
62
+ self._populate_app_call_resources: bool = False
30
63
  self._configure_project_root()
31
64
 
32
65
  def _configure_project_root(self) -> None:
33
- """Configures the project root by searching for a specific file within a depth limit."""
66
+ """
67
+ Configures the project root by searching for a specific file within a depth limit.
68
+ """
34
69
  current_path = Path(__file__).resolve()
35
70
  for _ in range(self._max_search_depth):
36
- logger.debug(f"Searching in: {current_path}")
71
+ self._logger.debug(f"Searching in: {current_path}")
37
72
  if (current_path / ALGOKIT_CONFIG_FILENAME).exists():
38
73
  self._project_root = current_path
39
74
  break
40
75
  current_path = current_path.parent
41
76
 
77
+ @property
78
+ def logger(self) -> logging.Logger:
79
+ """Returns the logger instance."""
80
+ return self._logger
81
+
42
82
  @property
43
83
  def debug(self) -> bool:
44
84
  """Returns the debug status."""
@@ -51,7 +91,7 @@ class UpdatableConfig:
51
91
 
52
92
  @property
53
93
  def trace_all(self) -> bool:
54
- """Indicates whether to store simulation traces for all operations."""
94
+ """Indicates whether simulation traces for all operations should be stored."""
55
95
  return self._trace_all
56
96
 
57
97
  @property
@@ -59,8 +99,15 @@ class UpdatableConfig:
59
99
  """Returns the size of the trace buffer in megabytes."""
60
100
  return self._trace_buffer_size_mb
61
101
 
102
+ @property
103
+ def populate_app_call_resource(self) -> bool:
104
+ """Indicates whether or not to populate app call resources."""
105
+ return self._populate_app_call_resources
106
+
62
107
  def with_debug(self, func: Callable[[], str | None]) -> None:
63
- """Executes a function with debug mode temporarily enabled."""
108
+ """
109
+ Executes a function with debug mode temporarily enabled.
110
+ """
64
111
  original_debug = self._debug
65
112
  try:
66
113
  self._debug = True
@@ -68,45 +115,45 @@ class UpdatableConfig:
68
115
  finally:
69
116
  self._debug = original_debug
70
117
 
71
- def configure( # noqa: PLR0913
118
+ def configure(
72
119
  self,
73
120
  *,
74
- debug: bool,
121
+ debug: bool | None = None,
75
122
  project_root: Path | None = None,
76
123
  trace_all: bool = False,
77
124
  trace_buffer_size_mb: float = 256,
78
125
  max_search_depth: int = 10,
126
+ populate_app_call_resources: bool = False,
127
+ logger: logging.Logger | None = None,
79
128
  ) -> None:
80
129
  """
81
130
  Configures various settings for the application.
82
- Please note, when `project_root` is not specified, by default config will attempt to find the `algokit.toml` by
83
- scanning the parent directories according to the `max_search_depth` parameter.
84
- Alternatively value can also be set via the `ALGOKIT_PROJECT_ROOT` environment variable.
85
- If you are executing the config from an algokit compliant project, you can simply call
86
- `config.configure(debug=True)`.
87
-
88
- Args:
89
- debug (bool): Indicates whether debug mode is enabled.
90
- project_root (Path | None, optional): The path to the project root directory. Defaults to None.
91
- trace_all (bool, optional): Indicates whether to trace all operations. Defaults to False. Which implies that
92
- only the operations that are failed will be traced by default.
93
- trace_buffer_size_mb (float, optional): The size of the trace buffer in megabytes. Defaults to 512mb.
94
- max_search_depth (int, optional): The maximum depth to search for a specific file. Defaults to 10.
95
-
96
- Returns:
97
- None
131
+
132
+ :param debug: Whether debug mode is enabled.
133
+ :param project_root: The path to the project root directory.
134
+ :param trace_all: Whether to trace all operations. Defaults to False.
135
+ :param trace_buffer_size_mb: The trace buffer size in megabytes. Defaults to 256.
136
+ :param max_search_depth: The maximum depth to search for a specific file. Defaults to 10.
137
+ :param populate_app_call_resources: Whether to populate app call resources. Defaults to False.
138
+ :param logger: A custom logger to use. Defaults to AlgoKitLogger instance.
98
139
  """
140
+ if logger is not None:
141
+ self._logger = logger
99
142
 
100
- self._debug = debug
143
+ if debug is not None:
144
+ self._debug = debug
145
+ # Update logger's level so debug messages are processed only when debug is True.
146
+ self._logger.setLevel(logging.DEBUG)
101
147
 
102
- if project_root:
148
+ if project_root is not None:
103
149
  self._project_root = project_root.resolve(strict=True)
104
- elif debug and ALGOKIT_PROJECT_ROOT:
150
+ elif debug is not None and ALGOKIT_PROJECT_ROOT:
105
151
  self._project_root = Path(ALGOKIT_PROJECT_ROOT).resolve(strict=True)
106
152
 
107
153
  self._trace_all = trace_all
108
154
  self._trace_buffer_size_mb = trace_buffer_size_mb
109
155
  self._max_search_depth = max_search_depth
156
+ self._populate_app_call_resources = populate_app_call_resources
110
157
 
111
158
 
112
159
  config = UpdatableConfig()