mm-web3 0.5.7__py3-none-any.whl → 0.6.0__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 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
- 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
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 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()
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", "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 ("", "/")
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
- 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")
@@ -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
@@ -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.0.dist-info/METADATA,sha256=Iy2yL-8SHM8SCn8MEHX2fS36uY9Xa9EXKcfnNDT7A10,194
14
+ mm_web3-0.6.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
15
+ mm_web3-0.6.0.dist-info/RECORD,,
@@ -1,8 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: mm-web3
3
- Version: 0.5.7
4
- Requires-Python: >=3.13
5
- Requires-Dist: loguru>=0.7.3
6
- Requires-Dist: mm-http~=0.2.5
7
- Requires-Dist: mm-print~=0.2.2
8
- Requires-Dist: mm-std~=0.6.0
@@ -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,,