mm-web3 0.5.7__tar.gz → 0.6.0__tar.gz

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.
Files changed (35) hide show
  1. {mm_web3-0.5.7 → mm_web3-0.6.0}/.claude/settings.local.json +2 -1
  2. mm_web3-0.6.0/CLAUDE.md +25 -0
  3. mm_web3-0.6.0/PKG-INFO +8 -0
  4. {mm_web3-0.5.7 → mm_web3-0.6.0}/README.md +0 -40
  5. {mm_web3-0.5.7 → mm_web3-0.6.0}/justfile +1 -1
  6. {mm_web3-0.5.7 → mm_web3-0.6.0}/pyproject.toml +10 -10
  7. {mm_web3-0.5.7 → mm_web3-0.6.0}/src/mm_web3/calcs.py +6 -6
  8. {mm_web3-0.5.7 → mm_web3-0.6.0}/src/mm_web3/config.py +16 -14
  9. {mm_web3-0.5.7 → mm_web3-0.6.0}/src/mm_web3/log.py +7 -0
  10. {mm_web3-0.5.7 → mm_web3-0.6.0}/src/mm_web3/proxy.py +2 -5
  11. {mm_web3-0.5.7 → mm_web3-0.6.0}/src/mm_web3/validators.py +16 -16
  12. {mm_web3-0.5.7 → mm_web3-0.6.0}/tests/test_config.py +5 -5
  13. {mm_web3-0.5.7 → mm_web3-0.6.0}/tests/test_retry.py +24 -24
  14. {mm_web3-0.5.7 → mm_web3-0.6.0}/uv.lock +71 -394
  15. mm_web3-0.5.7/ADR.md +0 -3
  16. mm_web3-0.5.7/CLAUDE.md +0 -19
  17. mm_web3-0.5.7/PKG-INFO +0 -8
  18. {mm_web3-0.5.7 → mm_web3-0.6.0}/.gitignore +0 -0
  19. {mm_web3-0.5.7 → mm_web3-0.6.0}/.pre-commit-config.yaml +0 -0
  20. {mm_web3-0.5.7 → mm_web3-0.6.0}/src/mm_web3/__init__.py +0 -0
  21. {mm_web3-0.5.7 → mm_web3-0.6.0}/src/mm_web3/account.py +0 -0
  22. {mm_web3-0.5.7 → mm_web3-0.6.0}/src/mm_web3/network.py +0 -0
  23. {mm_web3-0.5.7 → mm_web3-0.6.0}/src/mm_web3/node.py +0 -0
  24. {mm_web3-0.5.7 → mm_web3-0.6.0}/src/mm_web3/py.typed +0 -0
  25. {mm_web3-0.5.7 → mm_web3-0.6.0}/src/mm_web3/retry.py +0 -0
  26. {mm_web3-0.5.7 → mm_web3-0.6.0}/src/mm_web3/utils.py +0 -0
  27. {mm_web3-0.5.7 → mm_web3-0.6.0}/tests/__init__.py +0 -0
  28. {mm_web3-0.5.7 → mm_web3-0.6.0}/tests/common.py +0 -0
  29. {mm_web3-0.5.7 → mm_web3-0.6.0}/tests/test_account.py +0 -0
  30. {mm_web3-0.5.7 → mm_web3-0.6.0}/tests/test_calcs.py +0 -0
  31. {mm_web3-0.5.7 → mm_web3-0.6.0}/tests/test_network.py +0 -0
  32. {mm_web3-0.5.7 → mm_web3-0.6.0}/tests/test_node.py +0 -0
  33. {mm_web3-0.5.7 → mm_web3-0.6.0}/tests/test_proxy.py +0 -0
  34. {mm_web3-0.5.7 → mm_web3-0.6.0}/tests/test_utils.py +0 -0
  35. {mm_web3-0.5.7 → mm_web3-0.6.0}/tests/test_validators.py +0 -0
@@ -4,7 +4,8 @@
4
4
  "Bash(just lint)",
5
5
  "mcp__ide__getDiagnostics",
6
6
  "Bash(tree:*)",
7
- "WebFetch(domain:docs.astral.sh)"
7
+ "WebFetch(domain:docs.astral.sh)",
8
+ "Bash(just test:*)"
8
9
  ],
9
10
  "deny": [],
10
11
  "ask": []
@@ -0,0 +1,25 @@
1
+ # AI Agent Start Guide
2
+
3
+ ## Critical: Language
4
+ RESPOND IN ENGLISH. Always. No exceptions.
5
+ User's language does NOT determine your response language.
6
+ Only switch if user EXPLICITLY requests it (e.g., "respond in {language}").
7
+ Language switching applies ONLY to chat. All code, comments, commit messages, and files must ALWAYS be in English — no exceptions.
8
+
9
+
10
+ ## Mandatory Rules (external)
11
+ These files are REQUIRED. Read them fully and follow all rules.
12
+ - `~/.claude/shared-rules/general.md`
13
+ - `~/.claude/shared-rules/python.md`
14
+
15
+ ## Project Reading (context)
16
+ These files are REQUIRED for project understanding.
17
+ - `README.md` - Project overview and API
18
+
19
+ ## Preflight (mandatory)
20
+ Before your first response:
21
+ 1. Read all files listed above.
22
+ 2. Do not answer until all are read.
23
+ 3. In your first reply, list every file you have read from this document.
24
+
25
+ Failure to follow this protocol is considered an error.
mm_web3-0.6.0/PKG-INFO ADDED
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: mm-web3
3
+ Version: 0.6.0
4
+ Requires-Python: >=3.14
5
+ Requires-Dist: loguru>=0.7.3
6
+ Requires-Dist: mm-http~=0.3.0
7
+ Requires-Dist: mm-print~=0.3.0
8
+ Requires-Dist: mm-std~=0.7.0
@@ -170,46 +170,6 @@ max_attempts = 3
170
170
  backoff_seconds = 1.0
171
171
  ```
172
172
 
173
- ## Development
174
-
175
- ### Setup
176
-
177
- ```bash
178
- # Clone and setup
179
- git clone <repository-url>
180
- cd mm-cryptocurrency
181
- uv sync
182
-
183
- # Run tests
184
- just test
185
-
186
- # Format code
187
- just format
188
-
189
- # Run linting
190
- just lint
191
-
192
- # Run security audit
193
- just audit
194
- ```
195
-
196
- ### Requirements
197
-
198
- - **Python 3.13+**
199
- - **uv** for package management
200
- - Dependencies: `mm-http`, `mm-print`, `mm-result`
201
-
202
- ### Testing
203
-
204
- The library includes comprehensive tests covering:
205
- - Network definitions and utilities
206
- - Proxy fetching and validation
207
- - Configuration loading and validation
208
- - Retry logic and error handling
209
- - Utility functions
210
-
211
- Run tests with: `just test` or `uv run pytest`
212
-
213
173
  ## API Reference
214
174
 
215
175
  ### Core Classes
@@ -16,8 +16,8 @@ test:
16
16
 
17
17
  lint *args: format pre-commit
18
18
  uv run ruff check {{args}} src tests
19
- uv run ty check
20
19
  uv run mypy src
20
+ uv run ty check src
21
21
 
22
22
  audit:
23
23
  uv export --no-dev --all-extras --format requirements-txt --no-emit-project > requirements.txt
@@ -1,12 +1,12 @@
1
1
  [project]
2
2
  name = "mm-web3"
3
- version = "0.5.7"
3
+ version = "0.6.0"
4
4
  description = ""
5
- requires-python = ">=3.13"
5
+ requires-python = ">=3.14"
6
6
  dependencies = [
7
- "mm-std~=0.6.0",
8
- "mm-http~=0.2.5",
9
- "mm-print~=0.2.2",
7
+ "mm-std~=0.7.0",
8
+ "mm-http~=0.3.0",
9
+ "mm-print~=0.3.0",
10
10
  "loguru>=0.7.3",
11
11
  ]
12
12
 
@@ -16,7 +16,7 @@ build-backend = "hatchling.build"
16
16
 
17
17
  [dependency-groups]
18
18
  dev = [
19
- "bandit~=1.9.2",
19
+ "bandit~=1.9.3",
20
20
  "mypy~=1.19.1",
21
21
  "pip-audit~=2.10.0",
22
22
  "pre-commit~=4.5.1",
@@ -24,21 +24,21 @@ dev = [
24
24
  "pytest-asyncio~=1.3.0",
25
25
  "pytest-xdist~=3.8.0",
26
26
  "pytest-httpserver~=1.1.3",
27
- "ruff~=0.14.13",
27
+ "ruff~=0.14.14",
28
28
  "python-dotenv~=1.2.1",
29
29
  "eth-account~=0.13.7",
30
- "ty~=0.0.12",
30
+ "ty~=0.0.14",
31
31
  ]
32
32
 
33
33
  [tool.mypy]
34
- python_version = "3.13"
34
+ python_version = "3.14"
35
35
  warn_no_return = false
36
36
  strict = true
37
37
  exclude = ["^tests/", "^tmp/"]
38
38
 
39
39
  [tool.ruff]
40
40
  line-length = 130
41
- target-version = "py313"
41
+ target-version = "py314"
42
42
  [tool.ruff.lint]
43
43
  select = ["ALL"]
44
44
  ignore = [
@@ -120,6 +120,8 @@ def calc_expression_with_vars(
120
120
  term_value = int(term)
121
121
  elif suffix is not None:
122
122
  term_value = convert_value_with_units(term, unit_decimals)
123
+ elif term.startswith("random(") and term.endswith(")"):
124
+ term_value = _parse_random_function(term, unit_decimals)
123
125
  elif variables:
124
126
  # Check if term ends with any variable name
125
127
  matched_var = None
@@ -132,21 +134,19 @@ def calc_expression_with_vars(
132
134
  multiplier_part = term.removesuffix(matched_var)
133
135
  multiplier = Decimal(multiplier_part) if multiplier_part else Decimal(1)
134
136
  term_value = int(multiplier * variables[matched_var])
135
- # Check for random function
136
- elif term.startswith("random(") and term.endswith(")"):
137
- term_value = _parse_random_function(term, unit_decimals)
138
137
  else:
138
+ # Re-raise as ValueError for consistent error type from function
139
139
  raise ValueError(f"unrecognized term: {term}") # noqa: TRY301
140
- elif term.startswith("random(") and term.endswith(")"):
141
- term_value = _parse_random_function(term, unit_decimals)
142
140
  else:
141
+ # Re-raise as ValueError for consistent error type from function
143
142
  raise ValueError(f"unrecognized term: {term}") # noqa: TRY301
144
143
 
145
144
  if operator == "+":
146
145
  result += term_value
147
- if operator == "-":
146
+ elif operator == "-":
148
147
  result -= term_value
149
148
 
149
+ # Return inside try is intentional - exception handling wraps entire calculation
150
150
  return result # noqa: TRY300
151
151
  except Exception as e:
152
152
  raise ValueError(e) from e
@@ -4,7 +4,7 @@ from pathlib import Path
4
4
  from typing import Any, NoReturn, Self, TypeVar
5
5
  from zipfile import ZipFile
6
6
 
7
- import mm_print
7
+ from mm_print import print_json, print_plain
8
8
  from mm_result import Result
9
9
  from pydantic import BaseModel, ConfigDict, ValidationError
10
10
 
@@ -31,11 +31,11 @@ class Web3CliConfig(BaseModel):
31
31
  if count:
32
32
  for k in count:
33
33
  data[k] = len(data[k])
34
- mm_print.json(data)
34
+ print_json(data)
35
35
  sys.exit(0)
36
36
 
37
37
  @classmethod
38
- def read_toml_config_or_exit(cls, config_path: Path, zip_password: str = "") -> Self: # nosec
38
+ def read_toml_config_or_exit(cls, config_path: Path, zip_password: str = "") -> Self: # nosec: empty default is for optional password, not hardcoded secret
39
39
  """Read TOML config file, exit on error.
40
40
 
41
41
  Args:
@@ -51,7 +51,7 @@ class Web3CliConfig(BaseModel):
51
51
  cls._print_error_and_exit(res)
52
52
 
53
53
  @classmethod
54
- async def read_toml_config_or_exit_async(cls, config_path: Path, zip_password: str = "") -> Self: # nosec
54
+ async def read_toml_config_or_exit_async(cls, config_path: Path, zip_password: str = "") -> Self: # nosec: empty default is for optional password, not hardcoded secret
55
55
  """Read TOML config file with async validation, exit on error.
56
56
 
57
57
  Args:
@@ -67,7 +67,7 @@ class Web3CliConfig(BaseModel):
67
67
  cls._print_error_and_exit(res)
68
68
 
69
69
  @classmethod
70
- def _load_toml_data(cls, config_path: Path, zip_password: str = "") -> dict[str, Any]: # nosec
70
+ def _load_toml_data(cls, config_path: Path, zip_password: str = "") -> dict[str, Any]: # nosec: empty default is for optional password, not hardcoded secret
71
71
  """Load TOML data from file or ZIP archive.
72
72
 
73
73
  Args:
@@ -84,7 +84,7 @@ class Web3CliConfig(BaseModel):
84
84
  return tomllib.load(f)
85
85
 
86
86
  @classmethod
87
- def read_toml_config(cls, config_path: Path, zip_password: str = "") -> Result[Self]: # nosec
87
+ def read_toml_config(cls, config_path: Path, zip_password: str = "") -> Result[Self]: # nosec: empty default is for optional password, not hardcoded secret
88
88
  """Read and validate TOML config file.
89
89
 
90
90
  Args:
@@ -98,12 +98,12 @@ class Web3CliConfig(BaseModel):
98
98
  data = cls._load_toml_data(config_path, zip_password)
99
99
  return Result.ok(cls(**data))
100
100
  except ValidationError as e:
101
- return Result.err(("validator_error", e), extra={"errors": e.errors()})
101
+ return Result.err(("validator_error", e), context={"errors": e.errors()})
102
102
  except Exception as e:
103
103
  return Result.err(e)
104
104
 
105
105
  @classmethod
106
- async def read_toml_config_async(cls, config_path: Path, zip_password: str = "") -> Result[Self]: # nosec
106
+ async def read_toml_config_async(cls, config_path: Path, zip_password: str = "") -> Result[Self]: # nosec: empty default is for optional password, not hardcoded secret
107
107
  """Read and validate TOML config file with async validators.
108
108
 
109
109
  Use this method when your config has async model validators that
@@ -121,7 +121,7 @@ class Web3CliConfig(BaseModel):
121
121
  model = await cls.model_validate(data) # type: ignore[misc]
122
122
  return Result.ok(model)
123
123
  except ValidationError as e:
124
- return Result.err(("validator_error", e), extra={"errors": e.errors()})
124
+ return Result.err(("validator_error", e), context={"errors": e.errors()})
125
125
  except Exception as e:
126
126
  return Result.err(e)
127
127
 
@@ -132,14 +132,14 @@ class Web3CliConfig(BaseModel):
132
132
  Args:
133
133
  res: Failed Result containing error information
134
134
  """
135
- if res.error == "validator_error" and res.extra:
136
- mm_print.plain("config validation errors")
137
- for e in res.extra["errors"]:
135
+ if res.error == "validator_error" and res.context:
136
+ print_plain("config validation errors")
137
+ for e in res.context["errors"]:
138
138
  loc = e["loc"]
139
139
  field = ".".join(str(lo) for lo in loc) if len(loc) > 0 else ""
140
- mm_print.plain(f"{field} {e['msg']}")
140
+ print_plain(f"{field} {e['msg']}")
141
141
  else:
142
- mm_print.plain(f"can't parse config file: {res.error} {res.extra}")
142
+ print_plain(f"can't parse config file: {res.error} {res.context}")
143
143
  sys.exit(1)
144
144
 
145
145
 
@@ -156,5 +156,7 @@ def read_text_from_zip_archive(zip_archive_path: Path, filename: str | None = No
156
156
  """
157
157
  with ZipFile(zip_archive_path) as zipfile:
158
158
  if filename is None:
159
+ if not zipfile.filelist:
160
+ raise ValueError(f"ZIP archive is empty: {zip_archive_path}")
159
161
  filename = zipfile.filelist[0].filename
160
162
  return zipfile.read(filename, pwd=password.encode() if password else None).decode()
@@ -5,6 +5,13 @@ from loguru import logger
5
5
 
6
6
 
7
7
  def init_loguru(debug: bool, debug_file: Path | None, info_file: Path | None) -> None:
8
+ """Initialize loguru logger with console and optional file outputs.
9
+
10
+ Args:
11
+ debug: If True, set DEBUG level with timestamps; otherwise INFO level with plain format
12
+ debug_file: Optional file path for DEBUG level logs with timestamps
13
+ info_file: Optional file path for INFO level logs with plain format
14
+ """
8
15
  if debug:
9
16
  level = "DEBUG"
10
17
  format_ = "<green>{time:YYYY-MM-DD HH:mm:ss}</green> <level>{level}</level> {message}"
@@ -65,7 +65,7 @@ def is_valid_proxy_url(proxy_url: str) -> bool:
65
65
  Check if the given URL is a valid proxy URL.
66
66
 
67
67
  A valid proxy URL must have:
68
- - A scheme in {"http", "https", "socks4", "socks5", "zsocks5h"}.
68
+ - A scheme in {"http", "https", "socks4", "socks5", "socks5h"}.
69
69
  - A non-empty hostname.
70
70
  - A specified port.
71
71
  - No extra path components (the path must be empty or "/").
@@ -100,7 +100,4 @@ def is_valid_proxy_url(proxy_url: str) -> bool:
100
100
  return False
101
101
 
102
102
  # Ensure that there is no extra path (only allow an empty path or a single "/")
103
- if parsed.path and parsed.path not in ("", "/"): # noqa: SIM103
104
- return False
105
-
106
- return True
103
+ return not parsed.path or parsed.path in ("", "/")
@@ -62,32 +62,32 @@ class ConfigValidators:
62
62
  ValueError: If addresses are invalid, format is wrong, or no transfers found
63
63
  """
64
64
 
65
+ def _parse_transfer_line(line: str, source: str) -> Transfer:
66
+ arr = line.split()
67
+ if len(arr) < 2 or len(arr) > 3:
68
+ raise ValueError(f"illegal {source}: {line}")
69
+ return Transfer(from_address=arr[0], to_address=arr[1], value=arr[2] if len(arr) > 2 else "")
70
+
65
71
  def validator(v: str) -> list[Transfer]:
66
- result = []
72
+ result: list[Transfer] = []
67
73
  for line in parse_lines(v, remove_comments=True): # don't use lowercase here because it can be a file: /To/Path.txt
68
74
  if line.startswith("file:"):
69
- for file_line in read_lines_from_file(line.removeprefix("file:").strip()):
70
- arr = file_line.split()
71
- if len(arr) < 2 or len(arr) > 3:
72
- raise ValueError(f"illegal file_line: {file_line}")
73
- result.append(Transfer(from_address=arr[0], to_address=arr[1], value=arr[2] if len(arr) > 2 else ""))
74
-
75
+ result.extend(
76
+ _parse_transfer_line(fl, "file_line") for fl in read_lines_from_file(line.removeprefix("file:").strip())
77
+ )
75
78
  else:
76
- arr = line.split()
77
- if len(arr) < 2 or len(arr) > 3:
78
- raise ValueError(f"illegal line: {line}")
79
- result.append(Transfer(from_address=arr[0], to_address=arr[1], value=arr[2] if len(arr) > 2 else ""))
79
+ result.append(_parse_transfer_line(line, "line"))
80
80
 
81
81
  if lowercase:
82
82
  result = [
83
83
  Transfer(from_address=r.from_address.lower(), to_address=r.to_address.lower(), value=r.value) for r in result
84
84
  ]
85
85
 
86
- for route in result:
87
- if not is_address(route.from_address):
88
- raise ValueError(f"illegal address: {route.from_address}")
89
- if not is_address(route.to_address):
90
- raise ValueError(f"illegal address: {route.to_address}")
86
+ for transfer in result:
87
+ if not is_address(transfer.from_address):
88
+ raise ValueError(f"illegal address: {transfer.from_address}")
89
+ if not is_address(transfer.to_address):
90
+ raise ValueError(f"illegal address: {transfer.to_address}")
91
91
 
92
92
  if not result:
93
93
  raise ValueError("No valid transfers found")
@@ -84,7 +84,7 @@ count = -5
84
84
  result = SimpleTestConfig.read_toml_config(validation_error_config)
85
85
  assert result.is_err()
86
86
  assert result.error == "validator_error"
87
- assert result.extra is not None and "errors" in result.extra
87
+ assert result.context is not None and "errors" in result.context
88
88
 
89
89
  # Test extra fields (should fail due to forbid extra)
90
90
  extra_fields_config = config_dir / "extra.toml"
@@ -200,7 +200,7 @@ name = "test"
200
200
  count = -10
201
201
  """)
202
202
 
203
- with patch("sys.exit") as mock_exit, patch("mm_print.plain") as mock_print:
203
+ with patch("sys.exit") as mock_exit, patch("mm_web3.config.print_plain") as mock_print:
204
204
  SimpleTestConfig.read_toml_config_or_exit(validation_error_config)
205
205
  mock_exit.assert_called_with(1)
206
206
  mock_print.assert_called()
@@ -210,7 +210,7 @@ count = -10
210
210
 
211
211
  # Test file error exit
212
212
  missing_file = config_dir / "missing.toml"
213
- with patch("sys.exit") as mock_exit, patch("mm_print.plain") as mock_print:
213
+ with patch("sys.exit") as mock_exit, patch("mm_web3.config.print_plain") as mock_print:
214
214
  SimpleTestConfig.read_toml_config_or_exit(missing_file)
215
215
  mock_exit.assert_called_with(1)
216
216
  mock_print.assert_called()
@@ -220,7 +220,7 @@ def test_print_and_exit():
220
220
  """Test the print_and_exit method functionality."""
221
221
  config = SimpleTestConfig(name="test", count=42, enabled=True)
222
222
 
223
- with patch("sys.exit") as mock_exit, patch("mm_print.json") as mock_print_json:
223
+ with patch("sys.exit") as mock_exit, patch("mm_web3.config.print_json") as mock_print_json:
224
224
  config.print_and_exit()
225
225
  mock_exit.assert_called_with(0)
226
226
  mock_print_json.assert_called_once()
@@ -230,7 +230,7 @@ def test_print_and_exit():
230
230
  assert printed_data["enabled"] is True
231
231
 
232
232
  # Test with exclude and count parameters
233
- with patch("sys.exit") as mock_exit, patch("mm_print.json") as mock_print_json:
233
+ with patch("sys.exit") as mock_exit, patch("mm_web3.config.print_json") as mock_print_json:
234
234
  config.print_and_exit(exclude={"enabled"}, count={"name"})
235
235
  printed_data = mock_print_json.call_args[0][0]
236
236
  assert "enabled" not in printed_data
@@ -13,9 +13,9 @@ class TestRetryWithNodeAndProxy:
13
13
  result = await retry_with_node_and_proxy(3, "node1", "proxy1", func)
14
14
  assert result.is_ok()
15
15
  assert result.value == "success"
16
- assert result.extra is not None
17
- assert "retry_logs" in result.extra
18
- assert len(result.extra["retry_logs"]) == 1
16
+ assert result.context is not None
17
+ assert "retry_logs" in result.context
18
+ assert len(result.context["retry_logs"]) == 1
19
19
 
20
20
  async def test_success_on_second_try(self) -> None:
21
21
  attempts = 0
@@ -30,9 +30,9 @@ class TestRetryWithNodeAndProxy:
30
30
  result = await retry_with_node_and_proxy(3, "node1", "proxy1", func)
31
31
  assert result.is_ok()
32
32
  assert result.value == "success"
33
- assert result.extra is not None
34
- assert "retry_logs" in result.extra
35
- assert len(result.extra["retry_logs"]) == 2
33
+ assert result.context is not None
34
+ assert "retry_logs" in result.context
35
+ assert len(result.context["retry_logs"]) == 2
36
36
 
37
37
  async def test_all_attempts_fail(self) -> None:
38
38
  async def func(_node: str, _proxy: str | None) -> Result[str]:
@@ -41,9 +41,9 @@ class TestRetryWithNodeAndProxy:
41
41
  result = await retry_with_node_and_proxy(3, "node1", "proxy1", func)
42
42
  assert result.is_err()
43
43
  assert result.error == "failure"
44
- assert result.extra is not None
45
- assert "retry_logs" in result.extra
46
- assert len(result.extra["retry_logs"]) == 3
44
+ assert result.context is not None
45
+ assert "retry_logs" in result.context
46
+ assert len(result.context["retry_logs"]) == 3
47
47
 
48
48
  async def test_multiple_nodes_and_proxies(self) -> None:
49
49
  nodes = ["node1", "node2"]
@@ -60,9 +60,9 @@ class TestRetryWithNodeAndProxy:
60
60
  result = await retry_with_node_and_proxy(3, nodes, proxies, func)
61
61
  assert result.is_ok()
62
62
  assert result.value == "success"
63
- assert result.extra is not None
64
- assert "retry_logs" in result.extra
65
- assert len(result.extra["retry_logs"]) == 2
63
+ assert result.context is not None
64
+ assert "retry_logs" in result.context
65
+ assert len(result.context["retry_logs"]) == 2
66
66
 
67
67
 
68
68
  class TestRetryWithProxy:
@@ -73,9 +73,9 @@ class TestRetryWithProxy:
73
73
  result = await retry_with_proxy(3, "proxy1", func)
74
74
  assert result.is_ok()
75
75
  assert result.value == "success"
76
- assert result.extra is not None
77
- assert "retry_logs" in result.extra
78
- assert len(result.extra["retry_logs"]) == 1
76
+ assert result.context is not None
77
+ assert "retry_logs" in result.context
78
+ assert len(result.context["retry_logs"]) == 1
79
79
 
80
80
  async def test_success_on_second_try(self) -> None:
81
81
  attempts = 0
@@ -90,9 +90,9 @@ class TestRetryWithProxy:
90
90
  result = await retry_with_proxy(3, "proxy1", func)
91
91
  assert result.is_ok()
92
92
  assert result.value == "success"
93
- assert result.extra is not None
94
- assert "retry_logs" in result.extra
95
- assert len(result.extra["retry_logs"]) == 2
93
+ assert result.context is not None
94
+ assert "retry_logs" in result.context
95
+ assert len(result.context["retry_logs"]) == 2
96
96
 
97
97
  async def test_all_attempts_fail(self) -> None:
98
98
  async def func(_proxy: str | None) -> Result[str]:
@@ -101,9 +101,9 @@ class TestRetryWithProxy:
101
101
  result = await retry_with_proxy(3, "proxy1", func)
102
102
  assert result.is_err()
103
103
  assert result.error == "failure"
104
- assert result.extra is not None
105
- assert "retry_logs" in result.extra
106
- assert len(result.extra["retry_logs"]) == 3
104
+ assert result.context is not None
105
+ assert "retry_logs" in result.context
106
+ assert len(result.context["retry_logs"]) == 3
107
107
 
108
108
  async def test_multiple_proxies(self) -> None:
109
109
  proxies = ["proxy1", "proxy2"]
@@ -119,6 +119,6 @@ class TestRetryWithProxy:
119
119
  result = await retry_with_proxy(3, proxies, func)
120
120
  assert result.is_ok()
121
121
  assert result.value == "success"
122
- assert result.extra is not None
123
- assert "retry_logs" in result.extra
124
- assert len(result.extra["retry_logs"]) == 2
122
+ assert result.context is not None
123
+ assert "retry_logs" in result.context
124
+ assert len(result.context["retry_logs"]) == 2