algokit-utils 2.1.3b1__py3-none-any.whl → 2.2.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.

algokit_utils/__init__.py CHANGED
@@ -1,8 +1,5 @@
1
- from algokit_utils._ensure_funded import (
2
- EnsureBalanceParameters,
3
- EnsureFundedResponse,
4
- ensure_funded,
5
- )
1
+ from algokit_utils._debugging import PersistSourceMapInput, persist_sourcemaps, simulate_and_persist_response
2
+ from algokit_utils._ensure_funded import EnsureBalanceParameters, EnsureFundedResponse, ensure_funded
6
3
  from algokit_utils._transfer import TransferAssetParameters, TransferParameters, transfer, transfer_asset
7
4
  from algokit_utils.account import (
8
5
  create_kmd_wallet_account,
@@ -15,7 +12,6 @@ from algokit_utils.account import (
15
12
  )
16
13
  from algokit_utils.application_client import (
17
14
  ApplicationClient,
18
- Program,
19
15
  execute_atc_with_logic_error,
20
16
  get_next_version,
21
17
  get_sender_from_signer,
@@ -32,6 +28,7 @@ from algokit_utils.application_specification import (
32
28
  OnCompleteActionName,
33
29
  )
34
30
  from algokit_utils.asset import opt_in, opt_out
31
+ from algokit_utils.common import Program
35
32
  from algokit_utils.deploy import (
36
33
  DELETABLE_TEMPLATE_NAME,
37
34
  NOTE_PREFIX,
@@ -181,4 +178,7 @@ __all__ = [
181
178
  "transfer_asset",
182
179
  "opt_in",
183
180
  "opt_out",
181
+ "persist_sourcemaps",
182
+ "PersistSourceMapInput",
183
+ "simulate_and_persist_response",
184
184
  ]
@@ -0,0 +1,280 @@
1
+ import base64
2
+ import json
3
+ import logging
4
+ import typing
5
+ from dataclasses import dataclass, field
6
+ from datetime import datetime, timezone
7
+ from pathlib import Path
8
+
9
+ from algosdk.atomic_transaction_composer import (
10
+ AtomicTransactionComposer,
11
+ EmptySigner,
12
+ SimulateAtomicTransactionResponse,
13
+ )
14
+ from algosdk.encoding import checksum
15
+ from algosdk.v2client.models import SimulateRequest, SimulateRequestTransactionGroup, SimulateTraceConfig
16
+
17
+ from algokit_utils.common import Program
18
+
19
+ if typing.TYPE_CHECKING:
20
+ from algosdk.v2client.algod import AlgodClient
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ ALGOKIT_DIR = ".algokit"
25
+ SOURCES_DIR = "sources"
26
+ SOURCES_FILE = "sources.avm.json"
27
+ TRACES_FILE_EXT = ".trace.avm.json"
28
+ DEBUG_TRACES_DIR = "debug_traces"
29
+ TEAL_FILE_EXT = ".teal"
30
+ TEAL_SOURCEMAP_EXT = ".teal.tok.map"
31
+
32
+
33
+ @dataclass
34
+ class AVMDebuggerSourceMapEntry:
35
+ location: str = field(metadata={"json": "sourcemap-location"})
36
+ program_hash: str = field(metadata={"json": "hash"})
37
+
38
+ def __eq__(self, other: object) -> bool:
39
+ if isinstance(other, AVMDebuggerSourceMapEntry):
40
+ return self.location == other.location and self.program_hash == other.program_hash
41
+ return False
42
+
43
+ def __str__(self) -> str:
44
+ return json.dumps({"sourcemap-location": self.location, "hash": self.program_hash})
45
+
46
+
47
+ @dataclass
48
+ class AVMDebuggerSourceMap:
49
+ txn_group_sources: list[AVMDebuggerSourceMapEntry] = field(metadata={"json": "txn-group-sources"})
50
+
51
+ @classmethod
52
+ def from_dict(cls, data: dict) -> "AVMDebuggerSourceMap":
53
+ return cls(
54
+ txn_group_sources=[
55
+ AVMDebuggerSourceMapEntry(location=item["sourcemap-location"], program_hash=item["hash"])
56
+ for item in data.get("txn-group-sources", [])
57
+ ]
58
+ )
59
+
60
+ def to_dict(self) -> dict:
61
+ return {"txn-group-sources": [json.loads(str(item)) for item in self.txn_group_sources]}
62
+
63
+
64
+ @dataclass
65
+ class PersistSourceMapInput:
66
+ def __init__(
67
+ self, app_name: str, file_name: str, raw_teal: str | None = None, compiled_teal: Program | None = None
68
+ ):
69
+ self.compiled_teal = compiled_teal
70
+ self.app_name = app_name
71
+ self._raw_teal = raw_teal
72
+ self._file_name = self.strip_teal_extension(file_name)
73
+
74
+ @classmethod
75
+ def from_raw_teal(cls, raw_teal: str, app_name: str, file_name: str) -> "PersistSourceMapInput":
76
+ return cls(app_name, file_name, raw_teal=raw_teal)
77
+
78
+ @classmethod
79
+ def from_compiled_teal(cls, compiled_teal: Program, app_name: str, file_name: str) -> "PersistSourceMapInput":
80
+ return cls(app_name, file_name, compiled_teal=compiled_teal)
81
+
82
+ @property
83
+ def raw_teal(self) -> str:
84
+ if self._raw_teal:
85
+ return self._raw_teal
86
+ elif self.compiled_teal:
87
+ return self.compiled_teal.teal
88
+ else:
89
+ raise ValueError("No teal content found")
90
+
91
+ @property
92
+ def file_name(self) -> str:
93
+ return self._file_name
94
+
95
+ @staticmethod
96
+ def strip_teal_extension(file_name: str) -> str:
97
+ if file_name.endswith(".teal"):
98
+ return file_name[:-5]
99
+ return file_name
100
+
101
+
102
+ def _load_or_create_sources(sources_path: Path) -> AVMDebuggerSourceMap:
103
+ if not sources_path.exists():
104
+ return AVMDebuggerSourceMap(txn_group_sources=[])
105
+
106
+ with sources_path.open() as f:
107
+ return AVMDebuggerSourceMap.from_dict(json.load(f))
108
+
109
+
110
+ def _upsert_debug_sourcemaps(sourcemaps: list[AVMDebuggerSourceMapEntry], project_root: Path) -> None:
111
+ """
112
+ This function updates or inserts debug sourcemaps. If path in the sourcemap during iteration leads to non
113
+ existing file, removes it. Otherwise upserts.
114
+
115
+ Args:
116
+ sourcemaps (list[AVMDebuggerSourceMapEntry]): A list of AVMDebuggerSourceMapEntry objects.
117
+ project_root (Path): The root directory of the project.
118
+
119
+ Returns:
120
+ None
121
+ """
122
+
123
+ sources_path = project_root / ALGOKIT_DIR / SOURCES_DIR / SOURCES_FILE
124
+ sources = _load_or_create_sources(sources_path)
125
+
126
+ for sourcemap in sourcemaps:
127
+ source_file_path = Path(sourcemap.location)
128
+ if not source_file_path.exists() and sourcemap in sources.txn_group_sources:
129
+ sources.txn_group_sources.remove(sourcemap)
130
+ elif source_file_path.exists():
131
+ if sourcemap not in sources.txn_group_sources:
132
+ sources.txn_group_sources.append(sourcemap)
133
+ else:
134
+ index = sources.txn_group_sources.index(sourcemap)
135
+ sources.txn_group_sources[index] = sourcemap
136
+
137
+ with sources_path.open("w") as f:
138
+ json.dump(sources.to_dict(), f)
139
+
140
+
141
+ def _write_to_file(path: Path, content: str) -> None:
142
+ path.parent.mkdir(parents=True, exist_ok=True)
143
+ path.write_text(content)
144
+
145
+
146
+ def _build_avm_sourcemap( # noqa: PLR0913
147
+ *,
148
+ app_name: str,
149
+ file_name: str,
150
+ output_path: Path,
151
+ client: "AlgodClient",
152
+ raw_teal: str | None = None,
153
+ compiled_teal: Program | None = None,
154
+ with_sources: bool = True,
155
+ ) -> AVMDebuggerSourceMapEntry:
156
+ if not raw_teal and not compiled_teal:
157
+ raise ValueError("Either raw teal or compiled teal must be provided")
158
+
159
+ result = compiled_teal if compiled_teal else Program(str(raw_teal), client=client)
160
+ program_hash = base64.b64encode(
161
+ checksum(result.raw_binary) # type: ignore[no-untyped-call]
162
+ ).decode()
163
+ source_map = result.source_map.__dict__
164
+ source_map["sources"] = [f"{file_name}{TEAL_FILE_EXT}"] if with_sources else []
165
+
166
+ output_dir_path = output_path / ALGOKIT_DIR / SOURCES_DIR / app_name
167
+ source_map_output_path = output_dir_path / f"{file_name}{TEAL_SOURCEMAP_EXT}"
168
+ teal_output_path = output_dir_path / f"{file_name}{TEAL_FILE_EXT}"
169
+ _write_to_file(source_map_output_path, json.dumps(source_map))
170
+
171
+ if with_sources:
172
+ _write_to_file(teal_output_path, result.teal)
173
+
174
+ return AVMDebuggerSourceMapEntry(str(source_map_output_path), program_hash)
175
+
176
+
177
+ def persist_sourcemaps(
178
+ *, sources: list[PersistSourceMapInput], project_root: Path, client: "AlgodClient", with_sources: bool = True
179
+ ) -> None:
180
+ """
181
+ Persist the sourcemaps for the given sources as an AlgoKit AVM Debugger compliant artifacts.
182
+ Args:
183
+ sources (list[PersistSourceMapInput]): A list of PersistSourceMapInput objects.
184
+ project_root (Path): The root directory of the project.
185
+ client (AlgodClient): An AlgodClient object for interacting with the Algorand blockchain.
186
+ with_sources (bool): If True, it will dump teal source files along with sourcemaps.
187
+ Default is True, as needed by an AlgoKit AVM debugger.
188
+ """
189
+
190
+ sourcemaps = [
191
+ _build_avm_sourcemap(
192
+ raw_teal=source.raw_teal,
193
+ compiled_teal=source.compiled_teal,
194
+ app_name=source.app_name,
195
+ file_name=source.file_name,
196
+ output_path=project_root,
197
+ client=client,
198
+ with_sources=with_sources,
199
+ )
200
+ for source in sources
201
+ ]
202
+
203
+ _upsert_debug_sourcemaps(sourcemaps, project_root)
204
+
205
+
206
+ def simulate_response(atc: AtomicTransactionComposer, algod_client: "AlgodClient") -> SimulateAtomicTransactionResponse:
207
+ """
208
+ Simulate and fetch response for the given AtomicTransactionComposer and AlgodClient.
209
+
210
+ Args:
211
+ atc (AtomicTransactionComposer): An AtomicTransactionComposer object.
212
+ algod_client (AlgodClient): An AlgodClient object for interacting with the Algorand blockchain.
213
+
214
+ Returns:
215
+ SimulateAtomicTransactionResponse: The simulated response.
216
+ """
217
+
218
+ unsigned_txn_groups = atc.build_group()
219
+ empty_signer = EmptySigner()
220
+ txn_list = [txn_group.txn for txn_group in unsigned_txn_groups]
221
+ fake_signed_transactions = empty_signer.sign_transactions(txn_list, [])
222
+ txn_group = [SimulateRequestTransactionGroup(txns=fake_signed_transactions)]
223
+ trace_config = SimulateTraceConfig(enable=True, stack_change=True, scratch_change=True)
224
+
225
+ simulate_request = SimulateRequest(
226
+ txn_groups=txn_group, allow_more_logs=True, allow_empty_signatures=True, exec_trace_config=trace_config
227
+ )
228
+ return atc.simulate(algod_client, simulate_request)
229
+
230
+
231
+ def simulate_and_persist_response(
232
+ atc: AtomicTransactionComposer, project_root: Path, algod_client: "AlgodClient", buffer_size_mb: float = 256
233
+ ) -> SimulateAtomicTransactionResponse:
234
+ """
235
+ Simulates the atomic transactions using the provided `AtomicTransactionComposer` object and `AlgodClient` object,
236
+ and persists the simulation response to an AlgoKit AVM Debugger compliant JSON file.
237
+
238
+ :param atc: An `AtomicTransactionComposer` object representing the atomic transactions to be
239
+ simulated and persisted.
240
+ :param project_root: A `Path` object representing the root directory of the project.
241
+ :param algod_client: An `AlgodClient` object representing the Algorand client.
242
+ :param buffer_size_mb: The size of the trace buffer in megabytes. Defaults to 256mb.
243
+ :return: None
244
+
245
+ Returns:
246
+ SimulateAtomicTransactionResponse: The simulated response after persisting it
247
+ for AlgoKit AVM Debugger consumption.
248
+ """
249
+ atc_to_simulate = atc.clone()
250
+ sp = algod_client.suggested_params()
251
+
252
+ for txn_with_sign in atc_to_simulate.txn_list:
253
+ txn_with_sign.txn.first_valid_round = sp.first
254
+ txn_with_sign.txn.last_valid_round = sp.last
255
+ txn_with_sign.txn.genesis_hash = sp.gh
256
+
257
+ response = simulate_response(atc_to_simulate, algod_client)
258
+ txn_results = response.simulate_response["txn-groups"]
259
+
260
+ txn_types = [txn_result["txn-results"][0]["txn-result"]["txn"]["txn"]["type"] for txn_result in txn_results]
261
+ txn_types_count = {txn_type: txn_types.count(txn_type) for txn_type in set(txn_types)}
262
+ txn_types_str = "_".join([f"{count}#{txn_type}" for txn_type, count in txn_types_count.items()])
263
+
264
+ last_round = response.simulate_response["last-round"]
265
+ timestamp = datetime.now(tz=timezone.utc).strftime("%Y%m%d_%H%M%S")
266
+ output_file = project_root / DEBUG_TRACES_DIR / f"{timestamp}_lr{last_round}_{txn_types_str}{TRACES_FILE_EXT}"
267
+
268
+ output_file.parent.mkdir(parents=True, exist_ok=True)
269
+
270
+ # cleanup old files if buffer size is exceeded
271
+ total_size = sum(f.stat().st_size for f in output_file.parent.glob("*") if f.is_file())
272
+ if total_size > buffer_size_mb * 1024 * 1024:
273
+ sorted_files = sorted(output_file.parent.glob("*"), key=lambda p: p.stat().st_mtime)
274
+ while total_size > buffer_size_mb * 1024 * 1024:
275
+ oldest_file = sorted_files.pop(0)
276
+ total_size -= oldest_file.stat().st_size
277
+ oldest_file.unlink()
278
+
279
+ output_file.write_text(json.dumps(response.simulate_response, indent=2))
280
+ return response
@@ -18,7 +18,6 @@ from algosdk.atomic_transaction_composer import (
18
18
  AccountTransactionSigner,
19
19
  AtomicTransactionComposer,
20
20
  AtomicTransactionResponse,
21
- EmptySigner,
22
21
  LogicSigTransactionSigner,
23
22
  MultisigTransactionSigner,
24
23
  SimulateAtomicTransactionResponse,
@@ -28,10 +27,16 @@ from algosdk.atomic_transaction_composer import (
28
27
  from algosdk.constants import APP_PAGE_MAX_SIZE
29
28
  from algosdk.logic import get_application_address
30
29
  from algosdk.source_map import SourceMap
31
- from algosdk.v2client.models import SimulateRequest, SimulateRequestTransactionGroup, SimulateTraceConfig
32
30
 
33
31
  import algokit_utils.application_specification as au_spec
34
32
  import algokit_utils.deploy as au_deploy
33
+ from algokit_utils._debugging import (
34
+ PersistSourceMapInput,
35
+ persist_sourcemaps,
36
+ simulate_and_persist_response,
37
+ simulate_response,
38
+ )
39
+ from algokit_utils.common import Program
35
40
  from algokit_utils.config import config
36
41
  from algokit_utils.logic_error import LogicError, parse_logic_error
37
42
  from algokit_utils.models import (
@@ -61,7 +66,6 @@ logger = logging.getLogger(__name__)
61
66
 
62
67
  __all__ = [
63
68
  "ApplicationClient",
64
- "Program",
65
69
  "execute_atc_with_logic_error",
66
70
  "get_next_version",
67
71
  "get_sender_from_signer",
@@ -72,21 +76,6 @@ __all__ = [
72
76
  representing an ABI method name or signature"""
73
77
 
74
78
 
75
- class Program:
76
- """A compiled TEAL program"""
77
-
78
- def __init__(self, program: str, client: "AlgodClient"):
79
- """
80
- Fully compile the program source to binary and generate a
81
- source map for matching pc to line number
82
- """
83
- self.teal = program
84
- result: dict = client.compile(au_deploy.strip_comments(self.teal), source_map=True)
85
- self.raw_binary = base64.b64decode(result["result"])
86
- self.binary_hash: str = result["hash"]
87
- self.source_map = SourceMap(result["sourcemap"])
88
-
89
-
90
79
  def num_extra_program_pages(approval: bytes, clear: bytes) -> int:
91
80
  """Calculate minimum number of extra_pages required for provided approval and clear programs"""
92
81
 
@@ -346,6 +335,22 @@ class ApplicationClient:
346
335
  self._approval_program, self._clear_program = substitute_template_and_compile(
347
336
  self.algod_client, self.app_spec, template_values
348
337
  )
338
+
339
+ if config.debug and config.project_root:
340
+ persist_sourcemaps(
341
+ sources=[
342
+ PersistSourceMapInput(
343
+ compiled_teal=self._approval_program, app_name=self.app_name, file_name="approval.teal"
344
+ ),
345
+ PersistSourceMapInput(
346
+ compiled_teal=self._clear_program, app_name=self.app_name, file_name="clear.teal"
347
+ ),
348
+ ],
349
+ project_root=config.project_root,
350
+ client=self.algod_client,
351
+ with_sources=True,
352
+ )
353
+
349
354
  deployer = au_deploy.Deployer(
350
355
  app_client=self,
351
356
  creator=self._creator,
@@ -630,6 +635,11 @@ class ApplicationClient:
630
635
  if method:
631
636
  hints = self._method_hints(method)
632
637
  if hints and hints.read_only:
638
+ if config.debug and config.project_root and config.trace_all:
639
+ simulate_and_persist_response(
640
+ atc, config.project_root, self.algod_client, config.trace_buffer_size_mb
641
+ )
642
+
633
643
  return self._simulate_readonly_call(method, atc)
634
644
 
635
645
  return self._execute_atc_tr(atc)
@@ -865,26 +875,40 @@ class ApplicationClient:
865
875
  self._approval_program, self._clear_program = substitute_template_and_compile(
866
876
  self.algod_client, self.app_spec, self.template_values
867
877
  )
878
+
879
+ if config.debug and config.project_root:
880
+ persist_sourcemaps(
881
+ sources=[
882
+ PersistSourceMapInput(
883
+ compiled_teal=self._approval_program, app_name=self.app_name, file_name="approval.teal"
884
+ ),
885
+ PersistSourceMapInput(
886
+ compiled_teal=self._clear_program, app_name=self.app_name, file_name="clear.teal"
887
+ ),
888
+ ],
889
+ project_root=config.project_root,
890
+ client=self.algod_client,
891
+ with_sources=True,
892
+ )
893
+
868
894
  return self._approval_program, self._clear_program
869
895
 
870
896
  def _simulate_readonly_call(
871
897
  self, method: Method, atc: AtomicTransactionComposer
872
898
  ) -> ABITransactionResponse | TransactionResponse:
873
- simulate_response = _simulate_response(atc, self.algod_client)
899
+ response = simulate_response(atc, self.algod_client)
874
900
  traces = None
875
901
  if config.debug:
876
- traces = _create_simulate_traces(simulate_response)
877
- if simulate_response.failure_message:
902
+ traces = _create_simulate_traces(response)
903
+ if response.failure_message:
878
904
  raise _try_convert_to_logic_error(
879
- simulate_response.failure_message,
905
+ response.failure_message,
880
906
  self.app_spec.approval_program,
881
907
  self._get_approval_source_map,
882
908
  traces,
883
- ) or Exception(
884
- f"Simulate failed for readonly method {method.get_signature()}: {simulate_response.failure_message}"
885
- )
909
+ ) or Exception(f"Simulate failed for readonly method {method.get_signature()}: {response.failure_message}")
886
910
 
887
- return TransactionResponse.from_atr(simulate_response)
911
+ return TransactionResponse.from_atr(response)
888
912
 
889
913
  def _load_reference_and_check_app_id(self) -> None:
890
914
  self._load_app_reference()
@@ -1192,7 +1216,9 @@ def substitute_template_and_compile(
1192
1216
  au_deploy.check_template_variables(app_spec.approval_program, template_values)
1193
1217
  approval = au_deploy.replace_template_variables(app_spec.approval_program, template_values)
1194
1218
 
1195
- return Program(approval, algod_client), Program(clear, algod_client)
1219
+ approval_app, clear_app = Program(approval, algod_client), Program(clear, algod_client)
1220
+
1221
+ return approval_app, clear_app
1196
1222
 
1197
1223
 
1198
1224
  def get_next_version(current_version: str) -> str:
@@ -1263,10 +1289,22 @@ def execute_atc_with_logic_error(
1263
1289
  ```
1264
1290
  """
1265
1291
  try:
1292
+ if config.debug and config.project_root and config.trace_all:
1293
+ simulate_and_persist_response(atc, config.project_root, algod_client, config.trace_buffer_size_mb)
1294
+
1266
1295
  return atc.execute(algod_client, wait_rounds=wait_rounds)
1267
1296
  except Exception as ex:
1268
1297
  if config.debug:
1269
- simulate = _simulate_response(atc, algod_client)
1298
+ simulate = None
1299
+ if config.project_root and not config.trace_all:
1300
+ # if trace_all is enabled, we already have the traces executed above
1301
+ # hence we only need to simulate if trace_all is disabled and
1302
+ # project_root is set
1303
+ simulate = simulate_and_persist_response(
1304
+ atc, config.project_root, algod_client, config.trace_buffer_size_mb
1305
+ )
1306
+ else:
1307
+ simulate = simulate_response(atc, algod_client)
1270
1308
  traces = _create_simulate_traces(simulate)
1271
1309
  else:
1272
1310
  traces = None
@@ -1299,22 +1337,6 @@ def _create_simulate_traces(simulate: SimulateAtomicTransactionResponse) -> list
1299
1337
  return traces
1300
1338
 
1301
1339
 
1302
- def _simulate_response(
1303
- atc: AtomicTransactionComposer, algod_client: "AlgodClient"
1304
- ) -> SimulateAtomicTransactionResponse:
1305
- unsigned_txn_groups = atc.build_group()
1306
- empty_signer = EmptySigner()
1307
- txn_list = [txn_group.txn for txn_group in unsigned_txn_groups]
1308
- fake_signed_transactions = empty_signer.sign_transactions(txn_list, [])
1309
- txn_group = [SimulateRequestTransactionGroup(txns=fake_signed_transactions)]
1310
- trace_config = SimulateTraceConfig(enable=True, stack_change=True, scratch_change=True)
1311
-
1312
- simulate_request = SimulateRequest(
1313
- txn_groups=txn_group, allow_more_logs=True, allow_empty_signatures=True, exec_trace_config=trace_config
1314
- )
1315
- return atc.simulate(algod_client, simulate_request)
1316
-
1317
-
1318
1340
  def _convert_transaction_parameters(
1319
1341
  args: TransactionParameters | TransactionParametersDict | None,
1320
1342
  ) -> CreateCallParameters:
@@ -0,0 +1,28 @@
1
+ """
2
+ This module contains common classes and methods that are reused in more than one file.
3
+ """
4
+
5
+ import base64
6
+ import typing
7
+
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"])
algokit_utils/config.py CHANGED
@@ -1,25 +1,112 @@
1
+ import logging
2
+ import os
1
3
  from collections.abc import Callable
4
+ from pathlib import Path
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ # Environment variable to override the project root
9
+ ALGOKIT_PROJECT_ROOT = os.getenv("ALGOKIT_PROJECT_ROOT")
10
+ ALGOKIT_CONFIG_FILENAME = ".algokit.toml"
2
11
 
3
12
 
4
13
  class UpdatableConfig:
14
+ """Class to manage and update configuration settings for the AlgoKit project.
15
+
16
+ Attributes:
17
+ debug (bool): Indicates whether debug mode is enabled.
18
+ project_root (Path | None): The path to the project root directory.
19
+ trace_all (bool): Indicates whether to trace all operations.
20
+ trace_buffer_size_mb (int): The size of the trace buffer in megabytes.
21
+ max_search_depth (int): The maximum depth to search for a specific file.
22
+ """
23
+
5
24
  def __init__(self) -> None:
6
25
  self._debug: bool = False
26
+ self._project_root: Path | None = None
27
+ self._trace_all: bool = False
28
+ self._trace_buffer_size_mb: int | float = 256 # megabytes
29
+ self._max_search_depth: int = 10
30
+ self._configure_project_root()
31
+
32
+ def _configure_project_root(self) -> None:
33
+ """Configures the project root by searching for a specific file within a depth limit."""
34
+ current_path = Path(__file__).resolve()
35
+ for _ in range(self._max_search_depth):
36
+ logger.info(f"Searching in: {current_path}")
37
+ if (current_path / ALGOKIT_CONFIG_FILENAME).exists():
38
+ self._project_root = current_path
39
+ break
40
+ current_path = current_path.parent
7
41
 
8
42
  @property
9
43
  def debug(self) -> bool:
44
+ """Returns the debug status."""
10
45
  return self._debug
11
46
 
12
- def with_debug(self, lambda_func: Callable[[], None | str]) -> None:
13
- original = self._debug
47
+ @property
48
+ def project_root(self) -> Path | None:
49
+ """Returns the project root path."""
50
+ return self._project_root
51
+
52
+ @property
53
+ def trace_all(self) -> bool:
54
+ """Indicates whether to store simulation traces for all operations."""
55
+ return self._trace_all
56
+
57
+ @property
58
+ def trace_buffer_size_mb(self) -> int | float:
59
+ """Returns the size of the trace buffer in megabytes."""
60
+ return self._trace_buffer_size_mb
61
+
62
+ def with_debug(self, func: Callable[[], str | None]) -> None:
63
+ """Executes a function with debug mode temporarily enabled."""
64
+ original_debug = self._debug
14
65
  try:
15
66
  self._debug = True
16
- lambda_func()
67
+ func()
17
68
  finally:
18
- self._debug = original
69
+ self._debug = original_debug
70
+
71
+ def configure( # noqa: PLR0913
72
+ self,
73
+ *,
74
+ debug: bool,
75
+ project_root: Path | None = None,
76
+ trace_all: bool = False,
77
+ trace_buffer_size_mb: float = 256,
78
+ max_search_depth: int = 10,
79
+ ) -> None:
80
+ """
81
+ 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
98
+ """
99
+
100
+ self._debug = debug
101
+
102
+ if project_root:
103
+ self._project_root = project_root.resolve(strict=True)
104
+ elif debug and ALGOKIT_PROJECT_ROOT:
105
+ self._project_root = Path(ALGOKIT_PROJECT_ROOT).resolve(strict=True)
19
106
 
20
- def configure(self, *, debug: bool) -> None:
21
- if debug is not None:
22
- self._debug = debug
107
+ self._trace_all = trace_all
108
+ self._trace_buffer_size_mb = trace_buffer_size_mb
109
+ self._max_search_depth = max_search_depth
23
110
 
24
111
 
25
112
  config = UpdatableConfig()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: algokit-utils
3
- Version: 2.1.3b1
3
+ Version: 2.2.0
4
4
  Summary: Utilities for Algorand development for use by AlgoKit
5
5
  License: MIT
6
6
  Author: Algorand Foundation
@@ -1,18 +1,20 @@
1
- algokit_utils/__init__.py,sha256=mmNPxx6ZoGVYJDEOmBaZpfGGyrQVGJyutV5yMoCR_Ac,4750
1
+ algokit_utils/__init__.py,sha256=yeufbE_5wjRILZs_10UD5B3_sjdwAfPXskdFKskzXhg,4963
2
+ algokit_utils/_debugging.py,sha256=4UC5NZGqxF32y742TUB34rX9kWaObXCCPOs-lbkQjGQ,10732
2
3
  algokit_utils/_ensure_funded.py,sha256=ZdEdUB43QGIQrg7cSSgNrDmWaLSUhli9x9I6juwKfgo,6786
3
4
  algokit_utils/_transfer.py,sha256=CyXGOR_Zy-2crQhk-78uUbB8Sj_ZeTzxPwOAHU7wwno,5947
4
5
  algokit_utils/account.py,sha256=UIuOQZe28pQxjEP9TzhtYlOU20tUdzzS-nIIZM9Bp6Y,7364
5
- algokit_utils/application_client.py,sha256=qTPiP6sc4X2L8tsfVQwOGDRD7pyc_cpCJob6Qk1enDA,57980
6
+ algokit_utils/application_client.py,sha256=2EjOPLEur8xsz31nJKzsElm_DEUI3vbts5d4OxjKxfs,58870
6
7
  algokit_utils/application_specification.py,sha256=XusOe7VrGPun2UoNspC9Ei202NzPkxRNx5USXiABuXc,7466
7
8
  algokit_utils/asset.py,sha256=jsc7T1dH9HZA3Yve2gRLObwUlK6xLDoQz0NxLLnqaGs,7216
8
- algokit_utils/config.py,sha256=V8010eUkbfcoB0bHtxsGQOymq1cqNRc1lDWnwcumQpM,567
9
+ algokit_utils/common.py,sha256=K6-3_9dv2clDn0WMYb8AWE_N46kWWIXglZIPfHIowDs,812
10
+ algokit_utils/config.py,sha256=Ag6Wu2iZDN2CwdmjYA3mJPQ50GInkfXyaWMqb2ZHd6g,4278
9
11
  algokit_utils/deploy.py,sha256=ydE3QSq1lRkjXQC9zdFclywx8q1UgV9l-l3Mx-shbHg,34668
10
12
  algokit_utils/dispenser_api.py,sha256=BpwEhKDig6qz54wbO-htG8hmLxFIrvdzXpESUb7Y1zw,5584
11
13
  algokit_utils/logic_error.py,sha256=bta0YsZF6ggmrspc7tIs0itBOY2jk-AS62EZrKfiHLI,2632
12
14
  algokit_utils/models.py,sha256=KynZnM2YbOyTgr2NCT8CA-cYrO0eiyK6u48eeAzj82I,8246
13
15
  algokit_utils/network_clients.py,sha256=sj5y_g5uclddWCEyUCptA-KjWuAtLV06hZH4QIGM1yE,5313
14
16
  algokit_utils/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
- algokit_utils-2.1.3b1.dist-info/LICENSE,sha256=J5i7U1Q9Q2c7saUzlvFRmrCCFhQyXb5Juz_LO5omNUw,1076
16
- algokit_utils-2.1.3b1.dist-info/METADATA,sha256=VtsD3LLZ0WzKPWmOYh_2nvblkhx_45B1hblRDcRPJoM,2207
17
- algokit_utils-2.1.3b1.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
18
- algokit_utils-2.1.3b1.dist-info/RECORD,,
17
+ algokit_utils-2.2.0.dist-info/LICENSE,sha256=J5i7U1Q9Q2c7saUzlvFRmrCCFhQyXb5Juz_LO5omNUw,1076
18
+ algokit_utils-2.2.0.dist-info/METADATA,sha256=TeDdZ1Z4Eg2fwYi1sKrAANWoYo0lRpP4TYanIFL45hg,2205
19
+ algokit_utils-2.2.0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
20
+ algokit_utils-2.2.0.dist-info/RECORD,,