algorand-python-testing 0.2.1__py3-none-any.whl → 0.2.2b2__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.
algopy/__init__.py CHANGED
@@ -10,11 +10,14 @@ from algopy_testing.models import (
10
10
  GTxn,
11
11
  ITxn,
12
12
  LogicSig,
13
+ StateTotals,
13
14
  TemplateVar,
14
15
  Txn,
15
16
  logicsig,
17
+ uenumerate,
16
18
  urange,
17
19
  )
20
+ from algopy_testing.models.box import Box, BoxMap, BoxRef
18
21
  from algopy_testing.primitives import BigUInt, Bytes, String, UInt64
19
22
  from algopy_testing.protocols import BytesBacked
20
23
  from algopy_testing.state import GlobalState, LocalState
@@ -55,4 +58,7 @@ __all__ = [
55
58
  "subroutine",
56
59
  "uenumerate",
57
60
  "urange",
61
+ "Box",
62
+ "BoxRef",
63
+ "BoxMap",
58
64
  ]
algopy_testing/context.py CHANGED
@@ -285,7 +285,7 @@ class AlgopyTestContext:
285
285
  self._scratch_spaces: dict[str, list[algopy.Bytes | algopy.UInt64 | bytes | int]] = {}
286
286
  self._template_vars: dict[str, Any] = template_vars or {}
287
287
  self._blocks: dict[int, dict[str, int]] = {}
288
- self._boxes: dict[bytes, algopy.Bytes] = {}
288
+ self._boxes: dict[bytes, bytes] = {}
289
289
  self._lsigs: dict[algopy.LogicSig, Callable[[], algopy.UInt64 | bool]] = {}
290
290
  self._active_lsig_args: Sequence[algopy.Bytes] = []
291
291
 
@@ -1047,20 +1047,23 @@ class AlgopyTestContext:
1047
1047
 
1048
1048
  return new_txn
1049
1049
 
1050
- def get_box(self, name: algopy.Bytes | bytes) -> algopy.Bytes:
1050
+ def does_box_exist(self, name: algopy.Bytes | bytes) -> bool:
1051
+ """return true if the box with the given name exists."""
1052
+ name_bytes = name if isinstance(name, bytes) else name.value
1053
+ return name_bytes in self._boxes
1054
+
1055
+ def get_box(self, name: algopy.Bytes | bytes) -> bytes:
1051
1056
  """Get the content of a box."""
1052
- import algopy
1053
1057
 
1054
1058
  name_bytes = name if isinstance(name, bytes) else name.value
1055
- return self._boxes.get(name_bytes, algopy.Bytes(b""))
1059
+ return self._boxes.get(name_bytes, b"")
1056
1060
 
1057
1061
  def set_box(self, name: algopy.Bytes | bytes, content: algopy.Bytes | bytes) -> None:
1058
1062
  """Set the content of a box."""
1059
- import algopy
1060
1063
 
1061
1064
  name_bytes = name if isinstance(name, bytes) else name.value
1062
1065
  content_bytes = content if isinstance(content, bytes) else content.value
1063
- self._boxes[name_bytes] = algopy.Bytes(content_bytes)
1066
+ self._boxes[name_bytes] = content_bytes
1064
1067
 
1065
1068
  def execute_logicsig(
1066
1069
  self, lsig: algopy.LogicSig, lsig_args: Sequence[algopy.Bytes] | None = None
@@ -1071,12 +1074,14 @@ class AlgopyTestContext:
1071
1074
  self._lsigs[lsig] = lsig.func
1072
1075
  return lsig.func()
1073
1076
 
1074
- def clear_box(self, name: algopy.Bytes | bytes) -> None:
1077
+ def clear_box(self, name: algopy.Bytes | bytes) -> bool:
1075
1078
  """Clear the content of a box."""
1076
1079
 
1077
1080
  name_bytes = name if isinstance(name, bytes) else name.value
1078
1081
  if name_bytes in self._boxes:
1079
1082
  del self._boxes[name_bytes]
1083
+ return True
1084
+ return False
1080
1085
 
1081
1086
  def clear_all_boxes(self) -> None:
1082
1087
  """Clear all boxes."""
@@ -1170,7 +1175,6 @@ class AlgopyTestContext:
1170
1175
  self._app_id = iter(range(1, 2**64))
1171
1176
 
1172
1177
 
1173
- #
1174
1178
  _var: ContextVar[AlgopyTestContext] = ContextVar("_var")
1175
1179
 
1176
1180
 
algopy_testing/enums.py CHANGED
@@ -1,22 +1,42 @@
1
- from enum import Enum, IntEnum, StrEnum
1
+ from __future__ import annotations
2
2
 
3
+ from enum import Enum, StrEnum
3
4
 
4
- class OnCompleteAction(Enum):
5
- NoOp = 0
6
- OptIn = 1
7
- CloseOut = 2
8
- ClearState = 3
9
- UpdateApplication = 4
10
- DeleteApplication = 5
5
+ from algopy_testing.primitives import UInt64
11
6
 
12
7
 
13
- class TransactionType(IntEnum):
14
- Payment = 0
15
- KeyRegistration = 1
16
- AssetConfig = 2
17
- AssetTransfer = 3
18
- AssetFreeze = 4
19
- ApplicationCall = 5
8
+ class OnCompleteAction(UInt64):
9
+ NoOp: OnCompleteAction
10
+ OptIn: OnCompleteAction
11
+ CloseOut: OnCompleteAction
12
+ ClearState: OnCompleteAction
13
+ UpdateApplication: OnCompleteAction
14
+ DeleteApplication: OnCompleteAction
15
+
16
+
17
+ OnCompleteAction.NoOp = OnCompleteAction(0)
18
+ OnCompleteAction.OptIn = OnCompleteAction(1)
19
+ OnCompleteAction.CloseOut = OnCompleteAction(2)
20
+ OnCompleteAction.ClearState = OnCompleteAction(3)
21
+ OnCompleteAction.UpdateApplication = OnCompleteAction(4)
22
+ OnCompleteAction.DeleteApplication = OnCompleteAction(5)
23
+
24
+
25
+ class TransactionType(UInt64):
26
+ Payment: TransactionType
27
+ KeyRegistration: TransactionType
28
+ AssetConfig: TransactionType
29
+ AssetTransfer: TransactionType
30
+ AssetFreeze: TransactionType
31
+ ApplicationCall: TransactionType
32
+
33
+
34
+ TransactionType.Payment = TransactionType(0)
35
+ TransactionType.KeyRegistration = TransactionType(1)
36
+ TransactionType.AssetConfig = TransactionType(2)
37
+ TransactionType.AssetTransfer = TransactionType(3)
38
+ TransactionType.AssetFreeze = TransactionType(4)
39
+ TransactionType.ApplicationCall = TransactionType(5)
20
40
 
21
41
 
22
42
  class ECDSA(Enum):
@@ -1,141 +1,258 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING
3
+ import typing
4
4
 
5
+ from algopy_testing.constants import MAX_BOX_SIZE
5
6
  from algopy_testing.context import get_test_context
7
+ from algopy_testing.utils import as_bytes, as_string
6
8
 
7
- if TYPE_CHECKING:
9
+ _TKey = typing.TypeVar("_TKey")
10
+ _TValue = typing.TypeVar("_TValue")
11
+
12
+ if typing.TYPE_CHECKING:
8
13
  import algopy
9
14
 
10
15
 
11
- class Box:
12
- @staticmethod
13
- def create(a: algopy.Bytes | bytes, b: algopy.UInt64 | int, /) -> bool:
16
+ class Box(typing.Generic[_TValue]):
17
+ """
18
+ Box abstracts the reading and writing of a single value to a single box.
19
+ The box size will be reconfigured dynamically to fit the size of the value being assigned to
20
+ it.
21
+ """
22
+
23
+ def __init__(
24
+ self, type_: type[_TValue], /, *, key: bytes | str | algopy.Bytes | algopy.String = ""
25
+ ) -> None:
14
26
  import algopy
15
27
 
28
+ self._type = type_
29
+
30
+ self._key = (
31
+ algopy.String(as_string(key)).bytes
32
+ if isinstance(key, str | algopy.String)
33
+ else algopy.Bytes(as_bytes(key))
34
+ )
35
+
36
+ def __bool__(self) -> bool:
37
+ """
38
+ Returns True if the box exists, regardless of the truthiness of the contents
39
+ of the box
40
+ """
16
41
  context = get_test_context()
17
- name_bytes = a.value if isinstance(a, algopy.Bytes) else a
18
- size = int(b)
19
- if not name_bytes or size > 32768:
20
- raise ValueError("Invalid box name or size")
21
- if context.get_box(name_bytes):
22
- return False
23
- context.set_box(name_bytes, b"\x00" * size)
24
- return True
42
+ return context.does_box_exist(self.key)
25
43
 
26
- @staticmethod
27
- def delete(a: algopy.Bytes | bytes, /) -> bool:
28
- import algopy
44
+ @property
45
+ def key(self) -> algopy.Bytes:
46
+ """Provides access to the raw storage key"""
47
+ if not self._key:
48
+ raise RuntimeError("Box key is empty")
49
+ return self._key
29
50
 
51
+ @property
52
+ def value(self) -> _TValue:
53
+ """Retrieve the contents of the box. Fails if the box has not been created."""
30
54
  context = get_test_context()
31
- name_bytes = a.value if isinstance(a, algopy.Bytes) else a
32
- if context.get_box(name_bytes):
33
- context.clear_box(name_bytes)
34
- return True
35
- return False
55
+ if not context.does_box_exist(self.key):
56
+ raise RuntimeError("Box has not been created")
57
+ return _cast_to_value_type(self._type, context.get_box(self.key))
36
58
 
37
- @staticmethod
38
- def extract(
39
- a: algopy.Bytes | bytes, b: algopy.UInt64 | int, c: algopy.UInt64 | int, /
40
- ) -> algopy.Bytes:
41
- import algopy
59
+ @value.setter
60
+ def value(self, value: _TValue) -> None:
61
+ """Write _value_ to the box. Creates the box if it does not exist."""
62
+ context = get_test_context()
63
+ bytes_value = _cast_to_bytes(value)
64
+ context.set_box(self.key, bytes_value)
42
65
 
66
+ @value.deleter
67
+ def value(self) -> None:
68
+ """Delete the box"""
43
69
  context = get_test_context()
44
- name_bytes = a.value if isinstance(a, algopy.Bytes) else a
45
- start = int(b)
46
- length = int(c)
47
- box_content = context.get_box(name_bytes)
48
- if not box_content:
49
- raise ValueError("Box does not exist")
50
- return box_content[start : start + length]
70
+ context.clear_box(self.key)
51
71
 
52
- @staticmethod
53
- def get(a: algopy.Bytes | bytes, /) -> tuple[algopy.Bytes, bool]:
54
- import algopy
72
+ def get(self, *, default: _TValue) -> _TValue:
73
+ """
74
+ Retrieve the contents of the box, or return the default value if the box has not been
75
+ created.
76
+
77
+ :arg default: The default value to return if the box has not been created
78
+ """
79
+ box_content, box_exists = self.maybe()
80
+ return default if not box_exists else box_content
55
81
 
82
+ def maybe(self) -> tuple[_TValue, bool]:
83
+ """
84
+ Retrieve the contents of the box if it exists, and return a boolean indicating if the box
85
+ exists.
86
+
87
+ """
56
88
  context = get_test_context()
57
- name_bytes = a.value if isinstance(a, algopy.Bytes) else a
58
- box_content = context.get_box(name_bytes)
59
- return box_content, bool(box_content)
89
+ box_exists = context.does_box_exist(self.key)
90
+ box_content_bytes = context.get_box(self.key)
91
+ box_content = _cast_to_value_type(self._type, box_content_bytes)
92
+ return box_content, box_exists
60
93
 
61
- @staticmethod
62
- def length(a: algopy.Bytes | bytes, /) -> tuple[algopy.UInt64, bool]:
94
+ @property
95
+ def length(self) -> algopy.UInt64:
96
+ """
97
+ Get the length of this Box. Fails if the box does not exist
98
+ """
63
99
  import algopy
64
100
 
65
101
  context = get_test_context()
66
- name_bytes = a.value if isinstance(a, algopy.Bytes) else a
67
- box_content = context.get_box(name_bytes)
68
- return algopy.UInt64(len(box_content)), bool(box_content)
102
+ if not context.does_box_exist(self.key):
103
+ raise RuntimeError("Box has not been created")
104
+ return algopy.UInt64(len(context.get_box(self.key)))
105
+
69
106
 
70
- @staticmethod
71
- def put(a: algopy.Bytes | bytes, b: algopy.Bytes | bytes, /) -> None:
107
+ class BoxRef:
108
+ """
109
+ BoxRef abstracts the reading and writing of boxes containing raw binary data. The size is
110
+ configured manually, and can be set to values larger than what the AVM can handle in a single
111
+ value.
112
+ """
113
+
114
+ def __init__(self, /, *, key: bytes | str | algopy.Bytes | algopy.String = "") -> None:
72
115
  import algopy
73
116
 
117
+ self._key = (
118
+ algopy.String(as_string(key)).bytes
119
+ if isinstance(key, str | algopy.String)
120
+ else algopy.Bytes(as_bytes(key))
121
+ )
122
+
123
+ def __bool__(self) -> bool:
124
+ """Returns True if the box has a value set, regardless of the truthiness of that value"""
74
125
  context = get_test_context()
75
- name_bytes = a.value if isinstance(a, algopy.Bytes) else a
76
- content = b.value if isinstance(b, algopy.Bytes) else b
77
- existing_content = context.get_box(name_bytes)
78
- if existing_content and len(existing_content) != len(content):
79
- raise ValueError("New content length does not match existing box length")
80
- context.set_box(name_bytes, content)
126
+ return context.does_box_exist(self.key)
81
127
 
82
- @staticmethod
83
- def replace(
84
- a: algopy.Bytes | bytes, b: algopy.UInt64 | int, c: algopy.Bytes | bytes, /
85
- ) -> None:
86
- import algopy
128
+ @property
129
+ def key(self) -> algopy.Bytes:
130
+ """Provides access to the raw storage key"""
131
+ if not self._key:
132
+ raise RuntimeError("Box key is empty")
133
+
134
+ return self._key
135
+
136
+ def create(self, *, size: algopy.UInt64 | int) -> bool:
137
+ """
138
+ Creates a box with the specified size, setting all bits to zero. Fails if the box already
139
+ exists with a different size. Fails if the specified size is greater than the max box size
140
+ (32,768)
141
+
142
+ Returns True if the box was created, False if the box already existed
143
+ """
144
+ size_int = int(size)
145
+ if size_int > MAX_BOX_SIZE:
146
+ raise ValueError(f"Box size cannot exceed {MAX_BOX_SIZE}")
147
+
148
+ box_content, box_exists = self._maybe()
149
+ if box_exists and len(box_content) != size_int:
150
+ raise ValueError("Box already exists with a different size")
151
+ if box_exists:
152
+ return False
153
+ context = get_test_context()
154
+ context.set_box(self.key, b"\x00" * size_int)
155
+ return True
87
156
 
157
+ def delete(self) -> bool:
158
+ """
159
+ Deletes the box if it exists and returns a value indicating if the box existed
160
+ """
88
161
  context = get_test_context()
89
- name_bytes = a.value if isinstance(a, algopy.Bytes) else a
90
- start = int(b)
91
- new_content = c.value if isinstance(c, algopy.Bytes) else c
92
- box_content = context.get_box(name_bytes)
93
- if not box_content:
94
- raise ValueError("Box does not exist")
95
- if start + len(new_content) > len(box_content):
96
- raise ValueError("Replacement content exceeds box size")
97
- updated_content = (
98
- box_content[:start] + new_content + box_content[start + len(new_content) :]
99
- )
100
- context.set_box(name_bytes, updated_content)
162
+ return context.clear_box(self.key)
101
163
 
102
- @staticmethod
103
- def resize(a: algopy.Bytes | bytes, b: algopy.UInt64 | int, /) -> None:
164
+ def extract(
165
+ self, start_index: algopy.UInt64 | int, length: algopy.UInt64 | int
166
+ ) -> algopy.Bytes:
167
+ """
168
+ Extract a slice of bytes from the box.
169
+
170
+ Fails if the box does not exist, or if `start_index + length > len(box)`
171
+
172
+ :arg start_index: The offset to start extracting bytes from
173
+ :arg length: The number of bytes to extract
174
+ """
104
175
  import algopy
105
176
 
177
+ box_content, box_exists = self._maybe()
178
+ start_int = int(start_index)
179
+ length_int = int(length)
180
+ if not box_exists:
181
+ raise RuntimeError("Box has not been created")
182
+ if (start_int + length_int) > len(box_content):
183
+ raise ValueError("Index out of bounds")
184
+ result = box_content[start_int : start_int + length_int]
185
+ return algopy.Bytes(result)
186
+
187
+ def resize(self, new_size: algopy.UInt64 | int) -> None:
188
+ """
189
+ Resizes the box the specified `new_size`. Truncating existing data if the new value is
190
+ shorter or padding with zero bytes if it is longer.
191
+
192
+ :arg new_size: The new size of the box
193
+ """
106
194
  context = get_test_context()
107
- name_bytes = a.value if isinstance(a, algopy.Bytes) else a
108
- new_size = int(b)
109
- if not name_bytes or new_size > 32768:
110
- raise ValueError("Invalid box name or size")
111
- box_content = context.get_box(name_bytes)
112
- if not box_content:
113
- raise ValueError("Box does not exist")
114
- if new_size > len(box_content):
115
- updated_content = box_content + b"\x00" * (new_size - len(box_content))
195
+ new_size_int = int(new_size)
196
+
197
+ if new_size_int > MAX_BOX_SIZE:
198
+ raise ValueError(f"Box size cannot exceed {MAX_BOX_SIZE}")
199
+ box_content, box_exists = self._maybe()
200
+ if not box_exists:
201
+ raise RuntimeError("Box has not been created")
202
+ if new_size_int > len(box_content):
203
+ updated_content = box_content + b"\x00" * (new_size_int - len(box_content))
116
204
  else:
117
- updated_content = box_content[:new_size]
118
- context.set_box(name_bytes, updated_content)
205
+ updated_content = box_content[:new_size_int]
206
+ context.set_box(self.key, updated_content)
207
+
208
+ def replace(self, start_index: algopy.UInt64 | int, value: algopy.Bytes | bytes) -> None:
209
+ """
210
+ Write `value` to the box starting at `start_index`. Fails if the box does not exist,
211
+ or if `start_index + len(value) > len(box)`
212
+
213
+ :arg start_index: The offset to start writing bytes from
214
+ :arg value: The bytes to be written
215
+ """
216
+ context = get_test_context()
217
+ box_content, box_exists = self._maybe()
218
+ if not box_exists:
219
+ raise RuntimeError("Box has not been created")
220
+ start = int(start_index)
221
+ length = len(value)
222
+ if (start + length) > len(box_content):
223
+ raise ValueError("Replacement content exceeds box size")
224
+ updated_content = box_content[:start] + value + box_content[start + length :]
225
+ context.set_box(self.key, updated_content)
119
226
 
120
- @staticmethod
121
227
  def splice(
122
- a: algopy.Bytes | bytes,
123
- b: algopy.UInt64 | int,
124
- c: algopy.UInt64 | int,
125
- d: algopy.Bytes | bytes,
126
- /,
228
+ self,
229
+ start_index: algopy.UInt64 | int,
230
+ length: algopy.UInt64 | int,
231
+ value: algopy.Bytes | bytes,
127
232
  ) -> None:
233
+ """
234
+ set box to contain its previous bytes up to index `start_index`, followed by `bytes`,
235
+ followed by the original bytes of the box that began at index `start_index + length`
236
+
237
+ **Important: This op does not resize the box**
238
+ If the new value is longer than the box size, it will be truncated.
239
+ If the new value is shorter than the box size, it will be padded with zero bytes
240
+
241
+ :arg start_index: The index to start inserting `value`
242
+ :arg length: The number of bytes after `start_index` to omit from the new value
243
+ :arg value: The `value` to be inserted.
244
+ """
128
245
  import algopy
129
246
 
130
247
  context = get_test_context()
131
- name_bytes = a.value if isinstance(a, algopy.Bytes) else a
132
- start = int(b)
133
- delete_count = int(c)
134
- insert_content = d.value if isinstance(d, algopy.Bytes) else d
135
- box_content = context.get_box(name_bytes)
248
+ box_content, box_exists = self._maybe()
249
+
250
+ start = int(start_index)
251
+ delete_count = int(length)
252
+ insert_content = value.value if isinstance(value, algopy.Bytes) else value
136
253
 
137
- if not box_content:
138
- raise ValueError("Box does not exist")
254
+ if not box_exists:
255
+ raise RuntimeError("Box has not been created")
139
256
 
140
257
  if start > len(box_content):
141
258
  raise ValueError("Start index exceeds box size")
@@ -155,4 +272,249 @@ class Box:
155
272
  new_content += b"\x00" * (len(box_content) - len(new_content))
156
273
 
157
274
  # Update the box with the new content
158
- context.set_box(name_bytes, new_content)
275
+ context.set_box(self.key, new_content)
276
+
277
+ def get(self, *, default: algopy.Bytes | bytes) -> algopy.Bytes:
278
+ """
279
+ Retrieve the contents of the box, or return the default value if the box has not been
280
+ created.
281
+
282
+ :arg default: The default value to return if the box has not been created
283
+ """
284
+ import algopy
285
+
286
+ box_content, box_exists = self._maybe()
287
+ default_bytes = default if isinstance(default, algopy.Bytes) else algopy.Bytes(default)
288
+ return default_bytes if not box_exists else algopy.Bytes(box_content)
289
+
290
+ def put(self, value: algopy.Bytes | bytes) -> None:
291
+ """
292
+ Replaces the contents of box with value. Fails if box exists and len(box) != len(value).
293
+ Creates box if it does not exist
294
+
295
+ :arg value: The value to write to the box
296
+ """
297
+ import algopy
298
+
299
+ box_content, box_exists = self._maybe()
300
+ if box_exists and len(box_content) != len(value):
301
+ raise ValueError("Box already exists with a different size")
302
+
303
+ context = get_test_context()
304
+ content = value if isinstance(value, algopy.Bytes) else algopy.Bytes(value)
305
+ context.set_box(self.key, content)
306
+
307
+ def maybe(self) -> tuple[algopy.Bytes, bool]:
308
+ """
309
+ Retrieve the contents of the box if it exists, and return a boolean indicating if the box
310
+ exists.
311
+ """
312
+ import algopy
313
+
314
+ box_content, box_exists = self._maybe()
315
+ return algopy.Bytes(box_content), box_exists
316
+
317
+ def _maybe(self) -> tuple[bytes, bool]:
318
+ context = get_test_context()
319
+ box_exists = context.does_box_exist(self.key)
320
+ box_content = context.get_box(self.key)
321
+ return box_content, box_exists
322
+
323
+ @property
324
+ def length(self) -> algopy.UInt64:
325
+ """
326
+ Get the length of this Box. Fails if the box does not exist
327
+ """
328
+ import algopy
329
+
330
+ box_content, box_exists = self._maybe()
331
+ if not box_exists:
332
+ raise RuntimeError("Box has not been created")
333
+ return algopy.UInt64(len(box_content))
334
+
335
+
336
+ class BoxMap(typing.Generic[_TKey, _TValue]):
337
+ """
338
+ BoxMap abstracts the reading and writing of a set of boxes using a common key and content type.
339
+ Each composite key (prefix + key) still needs to be made available to the application via the
340
+ `boxes` property of the Transaction.
341
+ """
342
+
343
+ def __init__(
344
+ self,
345
+ key_type: type[_TKey],
346
+ value_type: type[_TValue],
347
+ /,
348
+ *,
349
+ key_prefix: bytes | str | algopy.Bytes | algopy.String = "",
350
+ ) -> None:
351
+ """Declare a box map.
352
+
353
+ :arg key_type: The type of the keys
354
+ :arg value_type: The type of the values
355
+ :arg key_prefix: The value used as a prefix to key data, can be empty.
356
+ When the BoxMap is being assigned to a member variable,
357
+ this argument is optional and defaults to the member variable name,
358
+ and if a custom value is supplied it must be static.
359
+ """
360
+ import algopy
361
+
362
+ self._key_type = key_type
363
+ self._value_type = value_type
364
+ self._key_prefix = (
365
+ algopy.String(as_string(key_prefix)).bytes
366
+ if isinstance(key_prefix, str | algopy.String)
367
+ else algopy.Bytes(as_bytes(key_prefix))
368
+ )
369
+
370
+ @property
371
+ def key_prefix(self) -> algopy.Bytes:
372
+ """Provides access to the raw storage key-prefix"""
373
+ if not self._key_prefix:
374
+ raise RuntimeError("Box key prefix is empty")
375
+ return self._key_prefix
376
+
377
+ def __getitem__(self, key: _TKey) -> _TValue:
378
+ """
379
+ Retrieve the contents of a keyed box. Fails if the box for the key has not been created.
380
+ """
381
+ box_content, box_exists = self.maybe(key)
382
+ if not box_exists:
383
+ raise RuntimeError("Box has not been created")
384
+ return box_content
385
+
386
+ def __setitem__(self, key: _TKey, value: _TValue) -> None:
387
+ """Write _value_ to a keyed box. Creates the box if it does not exist"""
388
+ context = get_test_context()
389
+ key_bytes = self._full_key(key)
390
+ bytes_value = _cast_to_bytes(value)
391
+ context.set_box(key_bytes, bytes_value)
392
+
393
+ def __delitem__(self, key: _TKey) -> None:
394
+ """Deletes a keyed box"""
395
+ context = get_test_context()
396
+ key_bytes = self._full_key(key)
397
+ context.clear_box(key_bytes)
398
+
399
+ def __contains__(self, key: _TKey) -> bool:
400
+ """
401
+ Returns True if a box with the specified key exists in the map, regardless of the
402
+ truthiness of the contents of the box
403
+ """
404
+ context = get_test_context()
405
+ key_bytes = self._full_key(key)
406
+ return context.does_box_exist(key_bytes)
407
+
408
+ def get(self, key: _TKey, *, default: _TValue) -> _TValue:
409
+ """
410
+ Retrieve the contents of a keyed box, or return the default value if the box has not been
411
+ created.
412
+
413
+ :arg key: The key of the box to get
414
+ :arg default: The default value to return if the box has not been created.
415
+ """
416
+ box_content, box_exists = self.maybe(key)
417
+ return default if not box_exists else box_content
418
+
419
+ def maybe(self, key: _TKey) -> tuple[_TValue, bool]:
420
+ """
421
+ Retrieve the contents of a keyed box if it exists, and return a boolean indicating if the
422
+ box exists.
423
+
424
+ :arg key: The key of the box to get
425
+ """
426
+ context = get_test_context()
427
+ key_bytes = self._full_key(key)
428
+ box_exists = context.does_box_exist(key_bytes)
429
+ box_content_bytes = context.get_box(key_bytes)
430
+ box_content = _cast_to_value_type(self._value_type, box_content_bytes)
431
+ return box_content, box_exists
432
+
433
+ def length(self, key: _TKey) -> algopy.UInt64:
434
+ """
435
+ Get the length of an item in this BoxMap. Fails if the box does not exist
436
+
437
+ :arg key: The key of the box to get
438
+ """
439
+ import algopy
440
+
441
+ context = get_test_context()
442
+ key_bytes = self._full_key(key)
443
+ box_exists = context.does_box_exist(key_bytes)
444
+ if not box_exists:
445
+ raise RuntimeError("Box has not been created")
446
+ box_content_bytes = context.get_box(key_bytes)
447
+ return algopy.UInt64(len(box_content_bytes))
448
+
449
+ def _full_key(self, key: _TKey) -> algopy.Bytes:
450
+ return self.key_prefix + _cast_to_bytes(key)
451
+
452
+
453
+ def _cast_to_value_type(t: type[_TValue], value: bytes) -> _TValue: # noqa: PLR0911
454
+ """
455
+ assuming _TValue to be one of the followings:
456
+ - bool,
457
+ - algopy.Bytes,
458
+ - algopy.UInt64
459
+ - algopy.Asset,
460
+ - algopy.Application,
461
+ - algopy.UInt64 enums
462
+ - algopy.arc4.Struct
463
+ - algopy_testing.BytesBacked
464
+ - any type with `from_bytes` class method and `bytes` property
465
+ - .e.g algopy.String, algopy.Address, algopy.arc4.DynamicArray etc.
466
+ """
467
+ import algopy
468
+
469
+ context = get_test_context()
470
+
471
+ if t is bool:
472
+ return algopy.op.btoi(value) == 1 # type: ignore[return-value]
473
+ elif t is algopy.Bytes:
474
+ return algopy.Bytes(value) # type: ignore[return-value]
475
+ elif t is algopy.UInt64:
476
+ return algopy.op.btoi(value) # type: ignore[return-value]
477
+ elif t is algopy.OnCompleteAction:
478
+ return algopy.OnCompleteAction(algopy.op.btoi(value).value) # type: ignore[return-value]
479
+ elif t is algopy.TransactionType:
480
+ return algopy.TransactionType(algopy.op.btoi(value).value) # type: ignore[return-value]
481
+ elif t is algopy.Asset:
482
+ asset_id = algopy.op.btoi(value)
483
+ return context.get_asset(asset_id) # type: ignore[return-value]
484
+ elif t is algopy.Application:
485
+ application_id = algopy.op.btoi(value)
486
+ return context.get_application(application_id) # type: ignore[return-value]
487
+ elif hasattr(t, "from_bytes"):
488
+ return t.from_bytes(value) # type: ignore[attr-defined, no-any-return]
489
+
490
+ raise ValueError(f"Unsupported type: {t}")
491
+
492
+
493
+ def _cast_to_bytes(value: _TValue) -> algopy.Bytes:
494
+ """
495
+ assuming _TValue to be one of the followings:
496
+ - bool,
497
+ - algopy.Bytes,
498
+ - algopy.UInt64
499
+ - algopy.Asset,
500
+ - algopy.Application,
501
+ - algopy.UInt64 enums
502
+ - algopy.arc4.Struct
503
+ - algopy_testing.BytesBacked
504
+ - any type with `from_bytes` class method and `bytes` property
505
+ - .e.g algopy.String, algopy.Address, algopy.arc4.DynamicArray etc.
506
+ """
507
+ import algopy
508
+
509
+ if isinstance(value, bool):
510
+ return algopy.op.itob(1 if value else 0)
511
+ elif isinstance(value, algopy.Bytes):
512
+ return value
513
+ elif isinstance(value, algopy.UInt64):
514
+ return algopy.op.itob(value)
515
+ elif isinstance(value, algopy.Asset | algopy.Application):
516
+ return algopy.op.itob(value.id)
517
+ elif hasattr(value, "bytes"):
518
+ return typing.cast(algopy.Bytes, value.bytes)
519
+
520
+ raise ValueError(f"Unsupported type: {type(value)}")
@@ -69,6 +69,18 @@ class Contract(metaclass=_ContractMeta):
69
69
  return wrapper
70
70
  return attr
71
71
 
72
+ def __setattr__(self, name: str, value: Any) -> None:
73
+ import algopy
74
+
75
+ name_bytes = algopy.String(name).bytes
76
+ match value:
77
+ case algopy.Box() | algopy.BoxRef() | algopy.GlobalState() | algopy.LocalState():
78
+ value._key = name_bytes
79
+ case algopy.BoxMap():
80
+ value._key_prefix = name_bytes
81
+
82
+ super().__setattr__(name, value)
83
+
72
84
 
73
85
  class ARC4Contract(Contract):
74
86
  @final
algopy_testing/op.py CHANGED
@@ -22,12 +22,13 @@ from ecdsa import ( # type: ignore # noqa: PGH003
22
22
  from algopy_testing.constants import (
23
23
  BITS_IN_BYTE,
24
24
  DEFAULT_ACCOUNT_MIN_BALANCE,
25
+ MAX_BOX_SIZE,
25
26
  MAX_BYTES_SIZE,
26
27
  MAX_UINT64,
27
28
  )
29
+ from algopy_testing.context import get_test_context
28
30
  from algopy_testing.enums import EC, ECDSA, Base64, VrfVerify
29
31
  from algopy_testing.models.block import Block
30
- from algopy_testing.models.box import Box
31
32
  from algopy_testing.models.gitxn import GITxn
32
33
  from algopy_testing.models.global_values import Global
33
34
  from algopy_testing.models.gtxn import GTxn
@@ -1023,6 +1024,160 @@ class _EllipticCurve:
1023
1024
 
1024
1025
  EllipticCurve = _EllipticCurve()
1025
1026
 
1027
+
1028
+ class Box:
1029
+ @staticmethod
1030
+ def create(a: algopy.Bytes | bytes, b: algopy.UInt64 | int, /) -> bool:
1031
+ import algopy
1032
+
1033
+ context = get_test_context()
1034
+ name_bytes = a.value if isinstance(a, algopy.Bytes) else a
1035
+ size = int(b)
1036
+ if not name_bytes or size > MAX_BOX_SIZE:
1037
+ raise ValueError("Invalid box name or size")
1038
+ if context.get_box(name_bytes):
1039
+ return False
1040
+ context.set_box(name_bytes, b"\x00" * size)
1041
+ return True
1042
+
1043
+ @staticmethod
1044
+ def delete(a: algopy.Bytes | bytes, /) -> bool:
1045
+ import algopy
1046
+
1047
+ context = get_test_context()
1048
+ name_bytes = a.value if isinstance(a, algopy.Bytes) else a
1049
+ if context.get_box(name_bytes):
1050
+ context.clear_box(name_bytes)
1051
+ return True
1052
+ return False
1053
+
1054
+ @staticmethod
1055
+ def extract(
1056
+ a: algopy.Bytes | bytes, b: algopy.UInt64 | int, c: algopy.UInt64 | int, /
1057
+ ) -> algopy.Bytes:
1058
+ import algopy
1059
+
1060
+ context = get_test_context()
1061
+ name_bytes = a.value if isinstance(a, algopy.Bytes) else a
1062
+ start = int(b)
1063
+ length = int(c)
1064
+ box_content = context.get_box(name_bytes)
1065
+ if not box_content:
1066
+ raise RuntimeError("Box does not exist")
1067
+ result = box_content[start : start + length]
1068
+ return algopy.Bytes(result)
1069
+
1070
+ @staticmethod
1071
+ def get(a: algopy.Bytes | bytes, /) -> tuple[algopy.Bytes, bool]:
1072
+ import algopy
1073
+
1074
+ context = get_test_context()
1075
+ name_bytes = a.value if isinstance(a, algopy.Bytes) else a
1076
+ box_content = algopy.Bytes(context.get_box(name_bytes))
1077
+ box_exists = context.does_box_exist(name_bytes)
1078
+ return box_content, box_exists
1079
+
1080
+ @staticmethod
1081
+ def length(a: algopy.Bytes | bytes, /) -> tuple[algopy.UInt64, bool]:
1082
+ import algopy
1083
+
1084
+ context = get_test_context()
1085
+ name_bytes = a.value if isinstance(a, algopy.Bytes) else a
1086
+ box_content = context.get_box(name_bytes)
1087
+ box_exists = context.does_box_exist(name_bytes)
1088
+ return algopy.UInt64(len(box_content)), box_exists
1089
+
1090
+ @staticmethod
1091
+ def put(a: algopy.Bytes | bytes, b: algopy.Bytes | bytes, /) -> None:
1092
+ import algopy
1093
+
1094
+ context = get_test_context()
1095
+ name_bytes = a.value if isinstance(a, algopy.Bytes) else a
1096
+ content = b.value if isinstance(b, algopy.Bytes) else b
1097
+ existing_content = context.get_box(name_bytes)
1098
+ if existing_content and len(existing_content) != len(content):
1099
+ raise ValueError("New content length does not match existing box length")
1100
+ context.set_box(name_bytes, algopy.Bytes(content))
1101
+
1102
+ @staticmethod
1103
+ def replace(
1104
+ a: algopy.Bytes | bytes, b: algopy.UInt64 | int, c: algopy.Bytes | bytes, /
1105
+ ) -> None:
1106
+ import algopy
1107
+
1108
+ context = get_test_context()
1109
+ name_bytes = a.value if isinstance(a, algopy.Bytes) else a
1110
+ start = int(b)
1111
+ new_content = c.value if isinstance(c, algopy.Bytes) else c
1112
+ box_content = context.get_box(name_bytes)
1113
+ if not box_content:
1114
+ raise RuntimeError("Box does not exist")
1115
+ if start + len(new_content) > len(box_content):
1116
+ raise ValueError("Replacement content exceeds box size")
1117
+ updated_content = (
1118
+ box_content[:start] + new_content + box_content[start + len(new_content) :]
1119
+ )
1120
+ context.set_box(name_bytes, updated_content)
1121
+
1122
+ @staticmethod
1123
+ def resize(a: algopy.Bytes | bytes, b: algopy.UInt64 | int, /) -> None:
1124
+ import algopy
1125
+
1126
+ context = get_test_context()
1127
+ name_bytes = a.value if isinstance(a, algopy.Bytes) else a
1128
+ new_size = int(b)
1129
+ if not name_bytes or new_size > MAX_BOX_SIZE:
1130
+ raise ValueError("Invalid box name or size")
1131
+ box_content = context.get_box(name_bytes)
1132
+ if not box_content:
1133
+ raise RuntimeError("Box does not exist")
1134
+ if new_size > len(box_content):
1135
+ updated_content = box_content + b"\x00" * (new_size - len(box_content))
1136
+ else:
1137
+ updated_content = box_content[:new_size]
1138
+ context.set_box(name_bytes, updated_content)
1139
+
1140
+ @staticmethod
1141
+ def splice(
1142
+ a: algopy.Bytes | bytes,
1143
+ b: algopy.UInt64 | int,
1144
+ c: algopy.UInt64 | int,
1145
+ d: algopy.Bytes | bytes,
1146
+ /,
1147
+ ) -> None:
1148
+ import algopy
1149
+
1150
+ context = get_test_context()
1151
+ name_bytes = a.value if isinstance(a, algopy.Bytes) else a
1152
+ start = int(b)
1153
+ delete_count = int(c)
1154
+ insert_content = d.value if isinstance(d, algopy.Bytes) else d
1155
+ box_content = context.get_box(name_bytes)
1156
+
1157
+ if not box_content:
1158
+ raise RuntimeError("Box does not exist")
1159
+
1160
+ if start > len(box_content):
1161
+ raise ValueError("Start index exceeds box size")
1162
+
1163
+ # Calculate the end index for deletion
1164
+ end = min(start + delete_count, len(box_content))
1165
+
1166
+ # Construct the new content
1167
+ new_content = box_content[:start] + insert_content + box_content[end:]
1168
+
1169
+ # Adjust the size if necessary
1170
+ if len(new_content) > len(box_content):
1171
+ # Truncate if the new content is too long
1172
+ new_content = new_content[: len(box_content)]
1173
+ elif len(new_content) < len(box_content):
1174
+ # Pad with zeros if the new content is too short
1175
+ new_content += b"\x00" * (len(box_content) - len(new_content))
1176
+
1177
+ # Update the box with the new content
1178
+ context.set_box(name_bytes, new_content)
1179
+
1180
+
1026
1181
  __all__ = [
1027
1182
  "AcctParamsGet",
1028
1183
  "AppGlobal",
@@ -3,6 +3,9 @@ from __future__ import annotations
3
3
  import typing
4
4
  from typing import cast, overload
5
5
 
6
+ if typing.TYPE_CHECKING:
7
+ import algopy
8
+
6
9
  _T = typing.TypeVar("_T")
7
10
 
8
11
 
@@ -35,6 +38,8 @@ class GlobalState(typing.Generic[_T]):
35
38
  key: bytes | str = "",
36
39
  description: str = "",
37
40
  ) -> None:
41
+ import algopy
42
+
38
43
  if isinstance(type_or_value, type):
39
44
  self.type_ = type_or_value
40
45
  self._value: _T | None = None
@@ -42,9 +47,21 @@ class GlobalState(typing.Generic[_T]):
42
47
  self.type_ = type(type_or_value)
43
48
  self._value = type_or_value
44
49
 
45
- self.key = key
50
+ match key:
51
+ case bytes(key):
52
+ self._key = algopy.Bytes(key)
53
+ case str(key):
54
+ self._key = algopy.String(key).bytes
55
+ case _:
56
+ raise ValueError("Key must be bytes or str")
57
+
46
58
  self.description = description
47
59
 
60
+ @property
61
+ def key(self) -> algopy.Bytes:
62
+ """Provides access to the raw storage key"""
63
+ return self._key
64
+
48
65
  @property
49
66
  def value(self) -> _T:
50
67
  if self._value is None:
@@ -18,11 +18,24 @@ class LocalState(typing.Generic[_T]):
18
18
  key: bytes | str = "",
19
19
  description: str = "",
20
20
  ) -> None:
21
+ import algopy
22
+
21
23
  self.type_ = type_
22
- self.key = key
24
+ match key:
25
+ case bytes(key):
26
+ self._key = algopy.Bytes(key)
27
+ case str(key):
28
+ self._key = algopy.String(key).bytes
29
+ case _:
30
+ raise ValueError("Key must be bytes or str")
23
31
  self.description = description
24
32
  self._state: dict[object, _T] = {}
25
33
 
34
+ @property
35
+ def key(self) -> algopy.Bytes:
36
+ """Provides access to the raw storage key"""
37
+ return self._key
38
+
26
39
  def _validate_local_state_key(self, key: algopy.Account | algopy.UInt64 | int) -> None:
27
40
  from algopy import Account, UInt64
28
41
 
@@ -1,3 +1,4 @@
1
1
  from algopy_testing.utilities.budget import OpUpFeeSource, ensure_budget
2
+ from algopy_testing.utilities.log import log
2
3
 
3
4
  __all__ = ["OpUpFeeSource", "ensure_budget", "log"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: algorand-python-testing
3
- Version: 0.2.1
3
+ Version: 0.2.2b2
4
4
  Summary: Algorand Python testing library
5
5
  Project-URL: Documentation, https://github.com/algorandfoundation/puya/tree/main/algopy_testing#README.md
6
6
  Project-URL: Issues, https://github.com/algorandfoundation/puya/issues
@@ -14,14 +14,25 @@ Classifier: Programming Language :: Python
14
14
  Classifier: Programming Language :: Python :: 3.12
15
15
  Classifier: Topic :: Software Development :: Testing
16
16
  Requires-Python: >=3.12
17
+ Requires-Dist: algorand-python>=1.2
17
18
  Requires-Dist: coincurve>=19.0.1
18
19
  Requires-Dist: ecdsa>=0.17.0
19
20
  Requires-Dist: pycryptodomex<4,>=3.6.0
20
21
  Requires-Dist: pynacl<2,>=1.4.0
21
- Requires-Dist: algorand-python>=1,<1.3.0
22
22
  Description-Content-Type: text/markdown
23
23
 
24
- # Algorand Python Testing
24
+ <div align="center">
25
+ <a href="https://github.com/algorandfoundation/algorand-python-testing"><img src="https://bafybeiaibjaf6zy6hvef2rrysaacsfsyb3hw4qqtgn657gw7k5tdzqdxzi.ipfs.nftstorage.link/" width=60%></a>
26
+ </div>
27
+
28
+ <p align="center">
29
+ <a target="_blank" href="https://algorandfoundation.github.io/algorand-python-testing/"><img src="https://img.shields.io/badge/docs-repository-74dfdc?logo=github&style=flat.svg" /></a>
30
+ <a target="_blank" href="https://developer.algorand.org/algokit/"><img src="https://img.shields.io/badge/learn-AlgoKit-74dfdc?logo=algorand&mac=flat.svg" /></a>
31
+ <a target="_blank" href="https://github.com/algorandfoundation/algorand-python-testing"><img src="https://img.shields.io/github/stars/algorandfoundation/algorand-python-testing?color=74dfdc&logo=star&style=flat" /></a>
32
+ <a target="_blank" href="https://developer.algorand.org/algokit/"><img src="https://api.visitorbadge.io/api/visitors?path=https%3A%2F%2Fgithub.com%2Falgorandfoundation%2Falgorand-python-testing&countColor=%2374dfdc&style=flat" /></a>
33
+ </p>
34
+
35
+ ---
25
36
 
26
37
  Algorand Python Testing is a companion package to [Algorand Python](https://github.com/algorandfoundation/puya) that enables efficient unit testing of Algorand Python smart contracts in an offline environment. It emulates key AVM behaviors without requiring a network connection, offering fast and reliable testing capabilities with a familiar Pythonic interface.
27
38
 
@@ -1,4 +1,4 @@
1
- algopy/__init__.py,sha256=nd5hpdHpkfb5srMM6vHKfbOmEIxTutr4Dh0b7wwFq8Y,1143
1
+ algopy/__init__.py,sha256=aAz8sf_DjKyyEAhpMQNGs-l4NCVROrZgKblsqWIqs7A,1273
2
2
  algopy/arc4.py,sha256=zE-cwwmeEBA0m22QiHIujeY0S5rdyf3gDqTyGgROAqI,48
3
3
  algopy/gtxn.py,sha256=tS2waKIr3g79T1xcmE2iVcvu524xANsR8awXduUVoT0,48
4
4
  algopy/itxn.py,sha256=gXida1ee8xMfRufZ24G4xPGGSe26Iwm9nqfHpQCxbpE,48
@@ -7,11 +7,11 @@ algopy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  algopy_testing/__init__.py,sha256=x7WpDet7utqy6M9MdE5OJeKLqNd1uAIPg_U2ZpDOenI,1150
8
8
  algopy_testing/arc4.py,sha256=3rCZGKs4-mH3SymHOrp190edGS9MV_v3L5oP_5_GBsA,52015
9
9
  algopy_testing/constants.py,sha256=L1-Vod3_k6NAmeHJ9d8qIIovSMlakudvd4VeR6BjPeo,733
10
- algopy_testing/context.py,sha256=TswTYyLMw1_novoEnoLoIIGrkN-c2xBCWoWFW7AOvs8,39371
11
- algopy_testing/enums.py,sha256=MJE7g_5wiI94a5oAaEdPGGaO7QS1wm5GgZlYtADwB1U,690
10
+ algopy_testing/context.py,sha256=BFcMfIZvpO4n2LXr1n3chc5vN30qoPfPuBaYNQDFjUw,39570
11
+ algopy_testing/enums.py,sha256=6ed2GvC-yRw6cYS06uwkgbFbfQ1C1H2qDqkWXgwul5g,1531
12
12
  algopy_testing/gtxn.py,sha256=ul1iBYt8Kwc9Xob_pWVwa6dj9ueQ0iIz2DHkC72Aga4,7954
13
13
  algopy_testing/itxn.py,sha256=lLz6dPBwTSvU5zmuKPQd0LkCLG1VJN2Q8Dmb6HRNb3s,23363
14
- algopy_testing/op.py,sha256=e_VY3pvgUuW_y64yolzzuxeN2Gpg4XkDcJM5fNK-iOg,33046
14
+ algopy_testing/op.py,sha256=2Z9GETVphRAe4kRWRbS1L77yEhKTmN55dqfXVoqMEfU,38650
15
15
  algopy_testing/protocols.py,sha256=fPvuYAWKVVYM8oZE9hJtmRKW24Ba8VSzrBGz-7mDnIw,448
16
16
  algopy_testing/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
17
  algopy_testing/utils.py,sha256=PkDkHPAKJxyq6Jc0kWSVhF9MtIyNZmNv-hY2bPtwQ40,8393
@@ -24,8 +24,8 @@ algopy_testing/models/account.py,sha256=vR_furAVjNGkT1x8vJ8BQeOuCyjg3Cwc1pIGUkwX
24
24
  algopy_testing/models/application.py,sha256=leMvgKkYt7Kct4ayUwkL7rQy65mpqp52oGnuoNO-1xA,2203
25
25
  algopy_testing/models/asset.py,sha256=qsvC7vGDKTH_PHqybc0SAgzPLAjPgzgwK0aDliUxoLk,3471
26
26
  algopy_testing/models/block.py,sha256=JwzoQ3Z7EIvOaSjbMlzQTQ7DztYejw43rQeRptolADs,876
27
- algopy_testing/models/box.py,sha256=CJrj9v0SwoJztyMVbe0Xu9ZHsAhWUdurAXjWo9lped8,5548
28
- algopy_testing/models/contract.py,sha256=9TwDC-5Ncxor2wn4_EVQ8ku6RpRkTNDTeuU64RKAmP0,2307
27
+ algopy_testing/models/box.py,sha256=cJrKHtwBs0EVvnfAHEZylmbywrg-Vx_-uVaQAsfIxP0,18820
28
+ algopy_testing/models/contract.py,sha256=jNFWwa_wxhpTJwP4LKQDLEA7wQWxlK8xfVu-uxFccQE,2714
29
29
  algopy_testing/models/gitxn.py,sha256=MEWkVoK81Hoqz6Bb7uFAuGCZTb_G5lH9Vl1Dx_2nO7w,1594
30
30
  algopy_testing/models/global_values.py,sha256=cT5JKbcSY2_VenF6Pn3BkSj_iaV2D5o4uJZYyKXs2xQ,2215
31
31
  algopy_testing/models/gtxn.py,sha256=RJCwFyJvIbxwbF27RpWJaS04x7ZACKr1IPP3iXRS6Rk,1993
@@ -41,12 +41,12 @@ algopy_testing/primitives/bytes.py,sha256=TI-M5hA4NNTdiNwZg_70EH08VFHOW0f9Wm7RwV
41
41
  algopy_testing/primitives/string.py,sha256=7eWrbFxu74nQ7ZdDg6GVsZ73VF_p24os7SnIoXKZuFk,2189
42
42
  algopy_testing/primitives/uint64.py,sha256=-vQHLYza-K7Y_Pms-49pQQ44B78CNLEYBb6X6Ny-G6c,7110
43
43
  algopy_testing/state/__init__.py,sha256=uRFuGSn7RMnhjVKWWdHnVtPNhNfTlQIoQaDPbcmkr3U,155
44
- algopy_testing/state/global_state.py,sha256=1eJRY9SYzOdj_dnNUqF_vghh0t9Q6N1B1MzLX0hKPxU,1662
45
- algopy_testing/state/local_state.py,sha256=7m98XD5Afbn72yvgw9Q_VLAEhObLBv_5trp11ZkGwuE,1788
46
- algopy_testing/utilities/__init__.py,sha256=y43zYxwO37xO9CodAENU9y5p6DBVrkDOHnoZZO77nxc,126
44
+ algopy_testing/state/global_state.py,sha256=Kp_Utr2YLQmMYL_Y-94gp5EIhxephbqKUWDaZEGnGBA,2090
45
+ algopy_testing/state/local_state.py,sha256=8dtmyIQXKyonLRHxNykAFqmcodTn30QEGt1PR-eJjVM,2171
46
+ algopy_testing/utilities/__init__.py,sha256=juZqPBATNsIUYDvZJf2KSVwt21anHloc4bgI5KWfLFg,171
47
47
  algopy_testing/utilities/budget.py,sha256=P1pOKWCyya4wsqAwKgvp7y0p97hn2Kr2qFVRKm-sJZo,540
48
48
  algopy_testing/utilities/log.py,sha256=5MDJY8Oh_COt5OLZXWqkvAJk2qdfBpfSGIpidV-HJ4k,1870
49
- algorand_python_testing-0.2.1.dist-info/METADATA,sha256=Pw-Gf-qZuEf-8xHrleA8MGA8eKcJWa4l4yIEmiGf79M,3139
50
- algorand_python_testing-0.2.1.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
51
- algorand_python_testing-0.2.1.dist-info/licenses/LICENSE,sha256=jRyAzzkz3HPr-knS8XWyDY8CyuU-L4RtydPP8uGWsUw,657
52
- algorand_python_testing-0.2.1.dist-info/RECORD,,
49
+ algorand_python_testing-0.2.2b2.dist-info/METADATA,sha256=YUTScpM6LeXfKQfMhFCiExhIp0WNJATWO653RTQMQMY,4172
50
+ algorand_python_testing-0.2.2b2.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
51
+ algorand_python_testing-0.2.2b2.dist-info/licenses/LICENSE,sha256=jRyAzzkz3HPr-knS8XWyDY8CyuU-L4RtydPP8uGWsUw,657
52
+ algorand_python_testing-0.2.2b2.dist-info/RECORD,,