mm-web3 0.5.7__py3-none-any.whl → 0.6.1__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.
- mm_web3/calcs.py +6 -6
- mm_web3/config.py +16 -14
- mm_web3/log.py +7 -0
- mm_web3/proxy.py +2 -5
- mm_web3/validators.py +16 -16
- mm_web3-0.6.1.dist-info/METADATA +8 -0
- mm_web3-0.6.1.dist-info/RECORD +15 -0
- mm_web3-0.5.7.dist-info/METADATA +0 -8
- mm_web3-0.5.7.dist-info/RECORD +0 -15
- {mm_web3-0.5.7.dist-info → mm_web3-0.6.1.dist-info}/WHEEL +0 -0
mm_web3/calcs.py
CHANGED
|
@@ -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
|
mm_web3/config.py
CHANGED
|
@@ -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()
|
mm_web3/log.py
CHANGED
|
@@ -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}"
|
mm_web3/proxy.py
CHANGED
|
@@ -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 ("", "/")
|
mm_web3/validators.py
CHANGED
|
@@ -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")
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
mm_web3/__init__.py,sha256=uGPiC3UGbashWlRiIr27CjqGp1xUTHA6goDBO5cGBBU,1296
|
|
2
|
+
mm_web3/account.py,sha256=9IkbcEB3wyyPfvKkwaJqTdZQuHfAGeEoipjkMFXLu54,3676
|
|
3
|
+
mm_web3/calcs.py,sha256=kPS7qvfdvnk6j3Iu8swjhfAJ7d9x2Ezdz_dTg9KH-GM,7924
|
|
4
|
+
mm_web3/config.py,sha256=d6ZCF8-DEnD-C-dV7yntIuqkjJAnT4jrNd_skbfS4QA,6185
|
|
5
|
+
mm_web3/log.py,sha256=RJXTwLwhCT0YiuRRk1U9y17eW31ZOozv-G2aYZ9yO6A,992
|
|
6
|
+
mm_web3/network.py,sha256=99Qv59rGAvf5akp8Sbow_PXkKQk6B3Yh8oy2kH6JhFw,6902
|
|
7
|
+
mm_web3/node.py,sha256=vXO9PsKZ_yfKsLKc5R_HL62CAKTDJbOg4BHijqw4IbM,902
|
|
8
|
+
mm_web3/proxy.py,sha256=YBPZpS7sQ2ivjElaSHSC9DTsjXBxxcFdI_D2vxpodyg,3470
|
|
9
|
+
mm_web3/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
mm_web3/retry.py,sha256=OuiPyxADn16b748ZYe5XVff6hVMumj4bnAteQSvUYs4,2390
|
|
11
|
+
mm_web3/utils.py,sha256=k-x8R8bLRZKBN3xqeepukD9Tzwt97qiTUETSrK7lIHM,2009
|
|
12
|
+
mm_web3/validators.py,sha256=qK__aMbkFF0Sjcdq6LlGvAKH-KKLg92PKJ3lVAdblT8,12809
|
|
13
|
+
mm_web3-0.6.1.dist-info/METADATA,sha256=MMK1q4oDFU8Mjj2irKV_SrBJhEyWstCPzgjGOupwKto,194
|
|
14
|
+
mm_web3-0.6.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
15
|
+
mm_web3-0.6.1.dist-info/RECORD,,
|
mm_web3-0.5.7.dist-info/METADATA
DELETED
mm_web3-0.5.7.dist-info/RECORD
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
mm_web3/__init__.py,sha256=uGPiC3UGbashWlRiIr27CjqGp1xUTHA6goDBO5cGBBU,1296
|
|
2
|
-
mm_web3/account.py,sha256=9IkbcEB3wyyPfvKkwaJqTdZQuHfAGeEoipjkMFXLu54,3676
|
|
3
|
-
mm_web3/calcs.py,sha256=b5mWCDE8UdQjxkdVbWPMRNaMbG6XteSlqG0M2CYDQe4,7860
|
|
4
|
-
mm_web3/config.py,sha256=A_xHvWQgJlAZPI7iJ0q-53gFA1WWOIKQl3uMTlQm6qM,5733
|
|
5
|
-
mm_web3/log.py,sha256=rvrObh-Jo9nO0OEIsLD5-f98mxeuA1GQlkYUY7xLb0Q,653
|
|
6
|
-
mm_web3/network.py,sha256=99Qv59rGAvf5akp8Sbow_PXkKQk6B3Yh8oy2kH6JhFw,6902
|
|
7
|
-
mm_web3/node.py,sha256=vXO9PsKZ_yfKsLKc5R_HL62CAKTDJbOg4BHijqw4IbM,902
|
|
8
|
-
mm_web3/proxy.py,sha256=dfFeb4cUWfPwxmK7EUZ5WBu_XPkytzDMIYvXYrz8gUk,3523
|
|
9
|
-
mm_web3/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
-
mm_web3/retry.py,sha256=OuiPyxADn16b748ZYe5XVff6hVMumj4bnAteQSvUYs4,2390
|
|
11
|
-
mm_web3/utils.py,sha256=k-x8R8bLRZKBN3xqeepukD9Tzwt97qiTUETSrK7lIHM,2009
|
|
12
|
-
mm_web3/validators.py,sha256=nt1AXiLfz1ek5PWsqVMtgvENSsCky0-B0NtrXuf0afI,12894
|
|
13
|
-
mm_web3-0.5.7.dist-info/METADATA,sha256=7pCjbAphA88PPPNPKbxvwElsFYUULUlml1f7nim2e8Q,194
|
|
14
|
-
mm_web3-0.5.7.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
15
|
-
mm_web3-0.5.7.dist-info/RECORD,,
|
|
File without changes
|