algokit-utils 3.0.0b4__py3-none-any.whl → 3.0.0b6__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.

@@ -77,8 +77,8 @@ def _get_config_from_environment(environment_prefix: str) -> AlgoClientNetworkCo
77
77
  port = os.getenv(f"{environment_prefix}_PORT")
78
78
  if port:
79
79
  parsed = parse.urlparse(server)
80
- server = parsed._replace(netloc=f"{parsed.hostname}:{port}").geturl()
81
- return AlgoClientNetworkConfig(server, os.getenv(f"{environment_prefix}_TOKEN", ""))
80
+ server = parsed._replace(netloc=f"{parsed.hostname}").geturl()
81
+ return AlgoClientNetworkConfig(server, os.getenv(f"{environment_prefix}_TOKEN", ""), port=port)
82
82
 
83
83
 
84
84
  class ClientManager:
@@ -351,15 +351,18 @@ class ClientManager:
351
351
  )
352
352
 
353
353
  @staticmethod
354
- def get_algod_client(config: AlgoClientNetworkConfig | None = None) -> AlgodClient:
354
+ def get_algod_client(config: AlgoClientNetworkConfig) -> AlgodClient:
355
355
  """Get an Algod client from config or environment.
356
356
 
357
357
  :param config: Optional client configuration
358
358
  :return: Algod client instance
359
359
  """
360
- config = config or _get_config_from_environment("ALGOD")
361
360
  headers = {"X-Algo-API-Token": config.token or ""}
362
- return AlgodClient(algod_token=config.token or "", algod_address=config.server, headers=headers)
361
+ return AlgodClient(
362
+ algod_token=config.token or "",
363
+ algod_address=config.full_url(),
364
+ headers=headers,
365
+ )
363
366
 
364
367
  @staticmethod
365
368
  def get_algod_client_from_environment() -> AlgodClient:
@@ -370,14 +373,13 @@ class ClientManager:
370
373
  return ClientManager.get_algod_client(ClientManager.get_algod_config_from_environment())
371
374
 
372
375
  @staticmethod
373
- def get_kmd_client(config: AlgoClientNetworkConfig | None = None) -> KMDClient:
376
+ def get_kmd_client(config: AlgoClientNetworkConfig) -> KMDClient:
374
377
  """Get a KMD client from config or environment.
375
378
 
376
379
  :param config: Optional client configuration
377
380
  :return: KMD client instance
378
381
  """
379
- config = config or _get_config_from_environment("KMD")
380
- return KMDClient(config.token, config.server)
382
+ return KMDClient(config.token, config.full_url())
381
383
 
382
384
  @staticmethod
383
385
  def get_kmd_client_from_environment() -> KMDClient:
@@ -388,15 +390,18 @@ class ClientManager:
388
390
  return ClientManager.get_kmd_client(ClientManager.get_kmd_config_from_environment())
389
391
 
390
392
  @staticmethod
391
- def get_indexer_client(config: AlgoClientNetworkConfig | None = None) -> IndexerClient:
393
+ def get_indexer_client(config: AlgoClientNetworkConfig) -> IndexerClient:
392
394
  """Get an Indexer client from config or environment.
393
395
 
394
396
  :param config: Optional client configuration
395
397
  :return: Indexer client instance
396
398
  """
397
- config = config or _get_config_from_environment("INDEXER")
398
399
  headers = {"X-Indexer-API-Token": config.token}
399
- return IndexerClient(indexer_token=config.token, indexer_address=config.server, headers=headers)
400
+ return IndexerClient(
401
+ indexer_token=config.token,
402
+ indexer_address=config.full_url(),
403
+ headers=headers,
404
+ )
400
405
 
401
406
  @staticmethod
402
407
  def get_indexer_client_from_environment() -> IndexerClient:
@@ -611,7 +616,7 @@ class ClientManager:
611
616
  else {"algod": 4001, "indexer": 8980, "kmd": 4002}[config_or_port]
612
617
  )
613
618
 
614
- return AlgoClientNetworkConfig(server=f"http://localhost:{port}", token="a" * 64)
619
+ return AlgoClientNetworkConfig(server="http://localhost", token="a" * 64, port=port)
615
620
 
616
621
  @staticmethod
617
622
  def get_algod_config_from_environment() -> AlgoClientNetworkConfig:
algokit_utils/config.py CHANGED
@@ -2,71 +2,67 @@ import logging
2
2
  import os
3
3
  from collections.abc import Callable
4
4
  from pathlib import Path
5
- from typing import Any
6
5
 
7
6
  # Environment variable to override the project root
8
7
  ALGOKIT_PROJECT_ROOT = os.getenv("ALGOKIT_PROJECT_ROOT")
9
8
  ALGOKIT_CONFIG_FILENAME = ".algokit.toml"
10
9
 
11
10
 
12
- class AlgoKitLogger:
13
- def __init__(self) -> None:
14
- self._logger = logging.getLogger("algokit")
15
- self._setup_logger()
16
-
17
- def _setup_logger(self) -> None:
18
- formatter = logging.Formatter("%(levelname)s: %(message)s")
19
- handler = logging.StreamHandler()
20
- handler.setFormatter(formatter)
21
- self._logger.addHandler(handler)
22
- self._logger.setLevel(logging.INFO)
23
-
24
- def _get_logger(self, *, suppress_log: bool = False) -> logging.Logger:
25
- if suppress_log:
26
- null_logger = logging.getLogger("null")
27
- null_logger.addHandler(logging.NullHandler())
28
- return null_logger
29
- return self._logger
11
+ class AlgoKitLogger(logging.Logger):
12
+ def __init__(self, name: str = "algokit-utils-py", level: int = logging.NOTSET):
13
+ super().__init__(name, level)
14
+ self._setup_handler()
30
15
 
31
- def error(self, message: str, *args: Any, suppress_log: bool = False, **kwargs: Any) -> None:
32
- """Log an error message, optionally suppressing output"""
33
- self._get_logger(suppress_log=suppress_log).error(message, *args, **kwargs)
16
+ def _setup_handler(self) -> None:
17
+ # Only add the handler if no handlers are already set.
18
+ if not self.handlers:
19
+ formatter = logging.Formatter("%(levelname)s: %(message)s")
20
+ handler = logging.StreamHandler()
21
+ handler.setFormatter(formatter)
22
+ self.addHandler(handler)
34
23
 
35
- def exception(self, message: str, *args: Any, suppress_log: bool = False, **kwargs: Any) -> None:
36
- """Log an exception message, optionally suppressing output"""
37
- self._get_logger(suppress_log=suppress_log).exception(message, *args, **kwargs)
24
+ 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
25
+ """
26
+ Overrides the base _log method to allow suppressing individual log calls.
27
+ When a caller passes suppress_log=True in the extra keyword, the log call is ignored.
28
+ """
38
29
 
39
- def warning(self, message: str, *args: Any, suppress_log: bool = False, **kwargs: Any) -> None:
40
- """Log a warning message, optionally suppressing output"""
41
- self._get_logger(suppress_log=suppress_log).warning(message, *args, **kwargs)
30
+ # Check if the 'suppress_log' flag is provided in the extra dictionary.
31
+ if extra and extra.get("suppress_log", False):
32
+ return
33
+ # Call the parent _log
34
+ super()._log(level, msg, args, exc_info, extra, stack_info, stacklevel)
42
35
 
43
- def info(self, message: str, *args: Any, suppress_log: bool = False, **kwargs: Any) -> None:
44
- """Log an info message, optionally suppressing output"""
45
- self._get_logger(suppress_log=suppress_log).info(message, *args, **kwargs)
36
+ @classmethod
37
+ def get_null_logger(cls) -> logging.Logger:
38
+ """Return a logger that does nothing (a null logger)."""
39
+ null_logger = logging.getLogger("null")
40
+ null_logger.handlers.clear()
41
+ null_logger.addHandler(logging.NullHandler())
42
+ null_logger.propagate = False
43
+ return null_logger
46
44
 
47
- def debug(self, message: str, *args: Any, suppress_log: bool = False, **kwargs: Any) -> None:
48
- """Log a debug message, optionally suppressing output"""
49
- self._get_logger(suppress_log=suppress_log).debug(message, *args, **kwargs)
50
45
 
51
- def verbose(self, message: str, *args: Any, suppress_log: bool = False, **kwargs: Any) -> None:
52
- """Log a verbose message (maps to debug), optionally suppressing output"""
53
- self._get_logger(suppress_log=suppress_log).debug(message, *args, **kwargs)
46
+ # Set our custom logger class as the default.
47
+ logging.setLoggerClass(AlgoKitLogger)
54
48
 
55
49
 
56
50
  class UpdatableConfig:
57
- """Class to manage and update configuration settings for the AlgoKit project.
51
+ """
52
+ Class to manage and update configuration settings for the AlgoKit project.
58
53
 
59
54
  Attributes:
60
55
  debug (bool): Indicates whether debug mode is enabled.
61
56
  project_root (Path | None): The path to the project root directory.
62
57
  trace_all (bool): Indicates whether to trace all operations.
63
- trace_buffer_size_mb (int): The size of the trace buffer in megabytes.
58
+ trace_buffer_size_mb (int | float): The size of the trace buffer in megabytes.
64
59
  max_search_depth (int): The maximum depth to search for a specific file.
65
- populate_app_call_resources (bool): Indicates whether to populate app call resources.
60
+ populate_app_call_resources (bool): Whether to populate app call resources.
61
+ logger (logging.Logger): The logger instance to use. Defaults to an AlgoKitLogger instance.
66
62
  """
67
63
 
68
64
  def __init__(self) -> None:
69
- self._logger = AlgoKitLogger()
65
+ self._logger: logging.Logger = AlgoKitLogger()
70
66
  self._debug: bool = False
71
67
  self._project_root: Path | None = None
72
68
  self._trace_all: bool = False
@@ -76,17 +72,20 @@ class UpdatableConfig:
76
72
  self._configure_project_root()
77
73
 
78
74
  def _configure_project_root(self) -> None:
79
- """Configures the project root by searching for a specific file within a depth limit."""
75
+ """
76
+ Configures the project root by searching for a specific file within a depth limit.
77
+ """
80
78
  current_path = Path(__file__).resolve()
81
79
  for _ in range(self._max_search_depth):
82
- self.logger.debug(f"Searching in: {current_path}")
80
+ self._logger.debug(f"Searching in: {current_path}")
83
81
  if (current_path / ALGOKIT_CONFIG_FILENAME).exists():
84
82
  self._project_root = current_path
85
83
  break
86
84
  current_path = current_path.parent
87
85
 
88
86
  @property
89
- def logger(self) -> AlgoKitLogger:
87
+ def logger(self) -> logging.Logger:
88
+ """Returns the logger instance."""
90
89
  return self._logger
91
90
 
92
91
  @property
@@ -101,7 +100,7 @@ class UpdatableConfig:
101
100
 
102
101
  @property
103
102
  def trace_all(self) -> bool:
104
- """Indicates whether to store simulation traces for all operations."""
103
+ """Indicates whether simulation traces for all operations should be stored."""
105
104
  return self._trace_all
106
105
 
107
106
  @property
@@ -111,10 +110,13 @@ class UpdatableConfig:
111
110
 
112
111
  @property
113
112
  def populate_app_call_resource(self) -> bool:
113
+ """Indicates whether or not to populate app call resources."""
114
114
  return self._populate_app_call_resources
115
115
 
116
116
  def with_debug(self, func: Callable[[], str | None]) -> None:
117
- """Executes a function with debug mode temporarily enabled."""
117
+ """
118
+ Executes a function with debug mode temporarily enabled.
119
+ """
118
120
  original_debug = self._debug
119
121
  try:
120
122
  self._debug = True
@@ -131,26 +133,27 @@ class UpdatableConfig:
131
133
  trace_buffer_size_mb: float = 256,
132
134
  max_search_depth: int = 10,
133
135
  populate_app_call_resources: bool = False,
136
+ logger: logging.Logger | None = None,
134
137
  ) -> None:
135
138
  """
136
139
  Configures various settings for the application.
137
- Please note, when `project_root` is not specified, by default config will attempt to find the `algokit.toml` by
138
- scanning the parent directories according to the `max_search_depth` parameter.
139
- Alternatively value can also be set via the `ALGOKIT_PROJECT_ROOT` environment variable.
140
- If you are executing the config from an algokit compliant project, you can simply call
141
- `config.configure(debug=True)`.
142
-
143
- :param debug: Indicates whether debug mode is enabled.
144
- :param project_root: The path to the project root directory. Defaults to None.
145
- :param trace_all: Indicates whether to trace all operations. Defaults to False. Which implies that
146
- only the operations that are failed will be traced by default.
147
- :param trace_buffer_size_mb: The size of the trace buffer in megabytes. Defaults to 256
148
- :param max_search_depth: The maximum depth to search for a specific file. Defaults to 10
149
- :param populate_app_call_resources: Indicates whether to populate app call resources. Defaults to False
140
+
141
+ :param debug: Whether debug mode is enabled.
142
+ :param project_root: The path to the project root directory.
143
+ :param trace_all: Whether to trace all operations. Defaults to False.
144
+ :param trace_buffer_size_mb: The trace buffer size in megabytes. Defaults to 256.
145
+ :param max_search_depth: The maximum depth to search for a specific file. Defaults to 10.
146
+ :param populate_app_call_resources: Whether to populate app call resources. Defaults to False.
147
+ :param logger: A custom logger to use. Defaults to AlgoKitLogger instance.
150
148
  """
149
+ if logger is not None:
150
+ self._logger = logger
151
151
 
152
152
  if debug is not None:
153
153
  self._debug = debug
154
+ # Update logger's level so debug messages are processed only when debug is True.
155
+ self._logger.setLevel(logging.DEBUG)
156
+
154
157
  if project_root is not None:
155
158
  self._project_root = project_root.resolve(strict=True)
156
159
  elif debug is not None and ALGOKIT_PROJECT_ROOT:
@@ -13,58 +13,34 @@ class AlgoAmount:
13
13
  """Wrapper class to ensure safe, explicit conversion between µAlgo, Algo and numbers.
14
14
 
15
15
  :example:
16
- >>> amount = AlgoAmount(algos=1)
17
- >>> amount = AlgoAmount(algo=1)
18
- >>> amount = AlgoAmount.from_algos(1)
19
- >>> amount = AlgoAmount.from_algo(1)
20
- >>> amount = AlgoAmount(micro_algos=1_000_000)
21
- >>> amount = AlgoAmount(micro_algo=1_000_000)
22
- >>> amount = AlgoAmount.from_micro_algos(1_000_000)
23
- >>> amount = AlgoAmount.from_micro_algo(1_000_000)
16
+ >>> amount = AlgoAmount(algo=1)
17
+ >>> amount = AlgoAmount.from_algo(1)
18
+ >>> amount = AlgoAmount(micro_algo=1_000_000)
19
+ >>> amount = AlgoAmount.from_micro_algo(1_000_000)
24
20
  """
25
21
 
26
- @overload
27
- def __init__(self, *, micro_algos: int) -> None: ...
28
-
29
22
  @overload
30
23
  def __init__(self, *, micro_algo: int) -> None: ...
31
24
 
32
- @overload
33
- def __init__(self, *, algos: int | Decimal) -> None: ...
34
-
35
25
  @overload
36
26
  def __init__(self, *, algo: int | Decimal) -> None: ...
37
27
 
38
28
  def __init__(
39
29
  self,
40
30
  *,
41
- micro_algos: int | None = None,
42
31
  micro_algo: int | None = None,
43
- algos: int | Decimal | None = None,
44
32
  algo: int | Decimal | None = None,
45
33
  ):
46
- if micro_algos is None and micro_algo is None and algos is None and algo is None:
34
+ if micro_algo is None and algo is None:
47
35
  raise ValueError("No amount provided")
48
36
 
49
- if micro_algos is not None:
50
- self.amount_in_micro_algo = int(micro_algos)
51
- elif micro_algo is not None:
37
+ if micro_algo is not None:
52
38
  self.amount_in_micro_algo = int(micro_algo)
53
- elif algos is not None:
54
- self.amount_in_micro_algo = int(algos * algosdk.constants.MICROALGOS_TO_ALGOS_RATIO)
55
39
  elif algo is not None:
56
40
  self.amount_in_micro_algo = int(algo * algosdk.constants.MICROALGOS_TO_ALGOS_RATIO)
57
41
  else:
58
42
  raise ValueError("Invalid amount provided")
59
43
 
60
- @property
61
- def micro_algos(self) -> int:
62
- """Return the amount as a number in µAlgo.
63
-
64
- :returns: The amount in µAlgo.
65
- """
66
- return self.amount_in_micro_algo
67
-
68
44
  @property
69
45
  def micro_algo(self) -> int:
70
46
  """Return the amount as a number in µAlgo.
@@ -73,14 +49,6 @@ class AlgoAmount:
73
49
  """
74
50
  return self.amount_in_micro_algo
75
51
 
76
- @property
77
- def algos(self) -> Decimal:
78
- """Return the amount as a number in Algo.
79
-
80
- :returns: The amount in Algo.
81
- """
82
- return algosdk.util.microalgos_to_algos(self.amount_in_micro_algo) # type: ignore[no-any-return]
83
-
84
52
  @property
85
53
  def algo(self) -> Decimal:
86
54
  """Return the amount as a number in Algo.
@@ -89,18 +57,6 @@ class AlgoAmount:
89
57
  """
90
58
  return algosdk.util.microalgos_to_algos(self.amount_in_micro_algo) # type: ignore[no-any-return]
91
59
 
92
- @staticmethod
93
- def from_algos(amount: int | Decimal) -> AlgoAmount:
94
- """Create an AlgoAmount object representing the given number of Algo.
95
-
96
- :param amount: The amount in Algo.
97
- :returns: An AlgoAmount instance.
98
-
99
- :example:
100
- >>> amount = AlgoAmount.from_algos(1)
101
- """
102
- return AlgoAmount(algos=amount)
103
-
104
60
  @staticmethod
105
61
  def from_algo(amount: int | Decimal) -> AlgoAmount:
106
62
  """Create an AlgoAmount object representing the given number of Algo.
@@ -109,22 +65,10 @@ class AlgoAmount:
109
65
  :returns: An AlgoAmount instance.
110
66
 
111
67
  :example:
112
- >>> amount = AlgoAmount.from_algo(1)
68
+ >>> amount = AlgoAmount.from_algo(1)
113
69
  """
114
70
  return AlgoAmount(algo=amount)
115
71
 
116
- @staticmethod
117
- def from_micro_algos(amount: int) -> AlgoAmount:
118
- """Create an AlgoAmount object representing the given number of µAlgo.
119
-
120
- :param amount: The amount in µAlgo.
121
- :returns: An AlgoAmount instance.
122
-
123
- :example:
124
- >>> amount = AlgoAmount.from_micro_algos(1_000_000)
125
- """
126
- return AlgoAmount(micro_algos=amount)
127
-
128
72
  @staticmethod
129
73
  def from_micro_algo(amount: int) -> AlgoAmount:
130
74
  """Create an AlgoAmount object representing the given number of µAlgo.
@@ -133,7 +77,7 @@ class AlgoAmount:
133
77
  :returns: An AlgoAmount instance.
134
78
 
135
79
  :example:
136
- >>> amount = AlgoAmount.from_micro_algo(1_000_000)
80
+ >>> amount = AlgoAmount.from_micro_algo(1_000_000)
137
81
  """
138
82
  return AlgoAmount(micro_algo=amount)
139
83
 
@@ -141,21 +85,21 @@ class AlgoAmount:
141
85
  return f"{self.micro_algo:,} µALGO"
142
86
 
143
87
  def __int__(self) -> int:
144
- return self.micro_algos
88
+ return self.micro_algo
145
89
 
146
90
  def __add__(self, other: AlgoAmount) -> AlgoAmount:
147
91
  if isinstance(other, AlgoAmount):
148
- total_micro_algos = self.micro_algos + other.micro_algos
92
+ total_micro_algos = self.micro_algo + other.micro_algo
149
93
  else:
150
94
  raise TypeError(f"Unsupported operand type(s) for +: 'AlgoAmount' and '{type(other).__name__}'")
151
- return AlgoAmount.from_micro_algos(total_micro_algos)
95
+ return AlgoAmount.from_micro_algo(total_micro_algos)
152
96
 
153
97
  def __radd__(self, other: AlgoAmount) -> AlgoAmount:
154
98
  return self.__add__(other)
155
99
 
156
100
  def __iadd__(self, other: AlgoAmount) -> Self:
157
101
  if isinstance(other, AlgoAmount):
158
- self.amount_in_micro_algo += other.micro_algos
102
+ self.amount_in_micro_algo += other.micro_algo
159
103
  else:
160
104
  raise TypeError(f"Unsupported operand type(s) for +: 'AlgoAmount' and '{type(other).__name__}'")
161
105
  return self
@@ -204,42 +148,42 @@ class AlgoAmount:
204
148
 
205
149
  def __sub__(self, other: AlgoAmount) -> AlgoAmount:
206
150
  if isinstance(other, AlgoAmount):
207
- total_micro_algos = self.micro_algos - other.micro_algos
151
+ total_micro_algos = self.micro_algo - other.micro_algo
208
152
  else:
209
153
  raise TypeError(f"Unsupported operand type(s) for -: 'AlgoAmount' and '{type(other).__name__}'")
210
- return AlgoAmount.from_micro_algos(total_micro_algos)
154
+ return AlgoAmount.from_micro_algo(total_micro_algos)
211
155
 
212
156
  def __rsub__(self, other: int) -> AlgoAmount:
213
157
  if isinstance(other, (int)):
214
- total_micro_algos = int(other) - self.micro_algos
215
- return AlgoAmount.from_micro_algos(total_micro_algos)
158
+ total_micro_algos = int(other) - self.micro_algo
159
+ return AlgoAmount.from_micro_algo(total_micro_algos)
216
160
  raise TypeError(f"Unsupported operand type(s) for -: '{type(other).__name__}' and 'AlgoAmount'")
217
161
 
218
162
  def __isub__(self, other: AlgoAmount) -> Self:
219
163
  if isinstance(other, AlgoAmount):
220
- self.amount_in_micro_algo -= other.micro_algos
164
+ self.amount_in_micro_algo -= other.micro_algo
221
165
  else:
222
166
  raise TypeError(f"Unsupported operand type(s) for -: 'AlgoAmount' and '{type(other).__name__}'")
223
167
  return self
224
168
 
225
169
 
226
170
  # Helper functions
227
- def algo(algos: int) -> AlgoAmount:
171
+ def algo(algo: int) -> AlgoAmount:
228
172
  """Create an AlgoAmount object representing the given number of Algo.
229
173
 
230
- :param algos: The number of Algo to create an AlgoAmount object for.
174
+ :param algo: The number of Algo to create an AlgoAmount object for.
231
175
  :return: An AlgoAmount object representing the given number of Algo.
232
176
  """
233
- return AlgoAmount.from_algos(algos)
177
+ return AlgoAmount.from_algo(algo)
234
178
 
235
179
 
236
- def micro_algo(microalgos: int) -> AlgoAmount:
180
+ def micro_algo(micro_algo: int) -> AlgoAmount:
237
181
  """Create an AlgoAmount object representing the given number of µAlgo.
238
182
 
239
- :param microalgos: The number of µAlgo to create an AlgoAmount object for.
183
+ :param micro_algo: The number of µAlgo to create an AlgoAmount object for.
240
184
  :return: An AlgoAmount object representing the given number of µAlgo.
241
185
  """
242
- return AlgoAmount.from_micro_algos(microalgos)
186
+ return AlgoAmount.from_micro_algo(micro_algo)
243
187
 
244
188
 
245
189
  ALGORAND_MIN_TX_FEE = micro_algo(1_000)
@@ -252,5 +196,5 @@ def transaction_fees(number_of_transactions: int) -> AlgoAmount:
252
196
  :return: The total transaction fees.
253
197
  """
254
198
 
255
- total_micro_algos = number_of_transactions * ALGORAND_MIN_TX_FEE.micro_algos
256
- return micro_algo(total_micro_algos)
199
+ total_micro_algos = number_of_transactions * ALGORAND_MIN_TX_FEE.micro_algo
200
+ return AlgoAmount.from_micro_algo(total_micro_algos)
@@ -12,11 +12,15 @@ class AlgoClientNetworkConfig:
12
12
  {py:class}`algosdk.v2client.indexer.IndexerClient`"""
13
13
 
14
14
  server: str
15
- """URL for the service e.g. `http://localhost:4001` or `https://testnet-api.algonode.cloud`"""
15
+ """URL for the service e.g. `http://localhost` or `https://testnet-api.algonode.cloud`"""
16
16
  token: str | None = None
17
- """API Token to authenticate with the service"""
17
+ """API Token to authenticate with the service e.g '4001' or '8980'"""
18
18
  port: str | int | None = None
19
19
 
20
+ def full_url(self) -> str:
21
+ """Returns the full URL for the service"""
22
+ return f"{self.server.rstrip('/')}{f':{self.port}' if self.port else ''}"
23
+
20
24
 
21
25
  @dataclasses.dataclass
22
26
  class AlgoClientConfigs:
@@ -45,7 +45,7 @@ TransactionNote = bytes | TransactionNoteData | Arc2TransactionNote
45
45
  TxnTypeT = TypeVar("TxnTypeT", bound=algosdk.transaction.Transaction)
46
46
 
47
47
 
48
- class TransactionWrapper(algosdk.transaction.Transaction):
48
+ class TransactionWrapper:
49
49
  """Wrapper around algosdk.transaction.Transaction with optional property validators"""
50
50
 
51
51
  def __init__(self, transaction: algosdk.transaction.Transaction) -> None: