mm-web3 0.5.6__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.
- {mm_web3-0.5.6 → mm_web3-0.6.0}/.claude/settings.local.json +2 -1
- mm_web3-0.6.0/CLAUDE.md +25 -0
- mm_web3-0.6.0/PKG-INFO +8 -0
- {mm_web3-0.5.6 → mm_web3-0.6.0}/README.md +0 -40
- {mm_web3-0.5.6 → mm_web3-0.6.0}/justfile +1 -1
- {mm_web3-0.5.6 → mm_web3-0.6.0}/pyproject.toml +10 -10
- {mm_web3-0.5.6 → mm_web3-0.6.0}/src/mm_web3/calcs.py +6 -6
- {mm_web3-0.5.6 → mm_web3-0.6.0}/src/mm_web3/config.py +16 -14
- {mm_web3-0.5.6 → mm_web3-0.6.0}/src/mm_web3/log.py +7 -0
- {mm_web3-0.5.6 → mm_web3-0.6.0}/src/mm_web3/proxy.py +2 -5
- {mm_web3-0.5.6 → mm_web3-0.6.0}/src/mm_web3/validators.py +16 -16
- {mm_web3-0.5.6 → mm_web3-0.6.0}/tests/test_config.py +5 -5
- {mm_web3-0.5.6 → mm_web3-0.6.0}/tests/test_retry.py +24 -24
- {mm_web3-0.5.6 → mm_web3-0.6.0}/uv.lock +71 -394
- mm_web3-0.5.6/ADR.md +0 -3
- mm_web3-0.5.6/CLAUDE.md +0 -19
- mm_web3-0.5.6/PKG-INFO +0 -8
- {mm_web3-0.5.6 → mm_web3-0.6.0}/.gitignore +0 -0
- {mm_web3-0.5.6 → mm_web3-0.6.0}/.pre-commit-config.yaml +0 -0
- {mm_web3-0.5.6 → mm_web3-0.6.0}/src/mm_web3/__init__.py +0 -0
- {mm_web3-0.5.6 → mm_web3-0.6.0}/src/mm_web3/account.py +0 -0
- {mm_web3-0.5.6 → mm_web3-0.6.0}/src/mm_web3/network.py +0 -0
- {mm_web3-0.5.6 → mm_web3-0.6.0}/src/mm_web3/node.py +0 -0
- {mm_web3-0.5.6 → mm_web3-0.6.0}/src/mm_web3/py.typed +0 -0
- {mm_web3-0.5.6 → mm_web3-0.6.0}/src/mm_web3/retry.py +0 -0
- {mm_web3-0.5.6 → mm_web3-0.6.0}/src/mm_web3/utils.py +0 -0
- {mm_web3-0.5.6 → mm_web3-0.6.0}/tests/__init__.py +0 -0
- {mm_web3-0.5.6 → mm_web3-0.6.0}/tests/common.py +0 -0
- {mm_web3-0.5.6 → mm_web3-0.6.0}/tests/test_account.py +0 -0
- {mm_web3-0.5.6 → mm_web3-0.6.0}/tests/test_calcs.py +0 -0
- {mm_web3-0.5.6 → mm_web3-0.6.0}/tests/test_network.py +0 -0
- {mm_web3-0.5.6 → mm_web3-0.6.0}/tests/test_node.py +0 -0
- {mm_web3-0.5.6 → mm_web3-0.6.0}/tests/test_proxy.py +0 -0
- {mm_web3-0.5.6 → mm_web3-0.6.0}/tests/test_utils.py +0 -0
- {mm_web3-0.5.6 → mm_web3-0.6.0}/tests/test_validators.py +0 -0
mm_web3-0.6.0/CLAUDE.md
ADDED
|
@@ -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
|
@@ -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
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "mm-web3"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.6.0"
|
|
4
4
|
description = ""
|
|
5
|
-
requires-python = ">=3.
|
|
5
|
+
requires-python = ">=3.14"
|
|
6
6
|
dependencies = [
|
|
7
|
-
"mm-std~=0.
|
|
8
|
-
"mm-http~=0.
|
|
9
|
-
"mm-print~=0.
|
|
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.
|
|
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.
|
|
27
|
+
"ruff~=0.14.14",
|
|
28
28
|
"python-dotenv~=1.2.1",
|
|
29
29
|
"eth-account~=0.13.7",
|
|
30
|
-
"ty~=0.0.
|
|
30
|
+
"ty~=0.0.14",
|
|
31
31
|
]
|
|
32
32
|
|
|
33
33
|
[tool.mypy]
|
|
34
|
-
python_version = "3.
|
|
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 = "
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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),
|
|
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),
|
|
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.
|
|
136
|
-
|
|
137
|
-
for e in res.
|
|
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
|
-
|
|
140
|
+
print_plain(f"{field} {e['msg']}")
|
|
141
141
|
else:
|
|
142
|
-
|
|
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", "
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
|
87
|
-
if not is_address(
|
|
88
|
-
raise ValueError(f"illegal address: {
|
|
89
|
-
if not is_address(
|
|
90
|
-
raise ValueError(f"illegal 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.
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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.
|
|
17
|
-
assert "retry_logs" in result.
|
|
18
|
-
assert len(result.
|
|
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.
|
|
34
|
-
assert "retry_logs" in result.
|
|
35
|
-
assert len(result.
|
|
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.
|
|
45
|
-
assert "retry_logs" in result.
|
|
46
|
-
assert len(result.
|
|
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.
|
|
64
|
-
assert "retry_logs" in result.
|
|
65
|
-
assert len(result.
|
|
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.
|
|
77
|
-
assert "retry_logs" in result.
|
|
78
|
-
assert len(result.
|
|
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.
|
|
94
|
-
assert "retry_logs" in result.
|
|
95
|
-
assert len(result.
|
|
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.
|
|
105
|
-
assert "retry_logs" in result.
|
|
106
|
-
assert len(result.
|
|
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.
|
|
123
|
-
assert "retry_logs" in result.
|
|
124
|
-
assert len(result.
|
|
122
|
+
assert result.context is not None
|
|
123
|
+
assert "retry_logs" in result.context
|
|
124
|
+
assert len(result.context["retry_logs"]) == 2
|