mm-balance 0.1.16__tar.gz → 0.1.18__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 (33) hide show
  1. mm_balance-0.1.18/PKG-INFO +9 -0
  2. {mm_balance-0.1.16 → mm_balance-0.1.18}/justfile +2 -2
  3. mm_balance-0.1.18/pyproject.toml +72 -0
  4. {mm_balance-0.1.16 → mm_balance-0.1.18}/src/mm_balance/cli.py +1 -1
  5. {mm_balance-0.1.16 → mm_balance-0.1.18}/src/mm_balance/config.py +3 -0
  6. {mm_balance-0.1.16 → mm_balance-0.1.18}/src/mm_balance/constants.py +3 -3
  7. {mm_balance-0.1.16 → mm_balance-0.1.18}/src/mm_balance/output/utils.py +1 -1
  8. mm_balance-0.1.18/src/mm_balance/price.py +48 -0
  9. {mm_balance-0.1.16 → mm_balance-0.1.18}/src/mm_balance/result.py +9 -10
  10. {mm_balance-0.1.16 → mm_balance-0.1.18}/src/mm_balance/rpc/aptos.py +1 -1
  11. {mm_balance-0.1.16 → mm_balance-0.1.18}/src/mm_balance/token_decimals.py +1 -3
  12. {mm_balance-0.1.16 → mm_balance-0.1.18}/src/mm_balance/utils.py +1 -1
  13. {mm_balance-0.1.16 → mm_balance-0.1.18}/src/mm_balance/workers.py +6 -5
  14. mm_balance-0.1.18/tests/conftest.py +0 -0
  15. mm_balance-0.1.18/uv.lock +2029 -0
  16. mm_balance-0.1.16/PKG-INFO +0 -9
  17. mm_balance-0.1.16/pyproject.toml +0 -66
  18. mm_balance-0.1.16/src/mm_balance/price.py +0 -48
  19. mm_balance-0.1.16/uv.lock +0 -1947
  20. {mm_balance-0.1.16 → mm_balance-0.1.18}/.gitignore +0 -0
  21. {mm_balance-0.1.16 → mm_balance-0.1.18}/README.md +0 -0
  22. {mm_balance-0.1.16 → mm_balance-0.1.18}/src/mm_balance/__init__.py +0 -0
  23. {mm_balance-0.1.16 → mm_balance-0.1.18}/src/mm_balance/config/example.yml +0 -0
  24. {mm_balance-0.1.16 → mm_balance-0.1.18}/src/mm_balance/output/__init__.py +0 -0
  25. {mm_balance-0.1.16/src/mm_balance/rpc → mm_balance-0.1.18/src/mm_balance/output/formats}/__init__.py +0 -0
  26. {mm_balance-0.1.16 → mm_balance-0.1.18}/src/mm_balance/output/formats/json_format.py +0 -0
  27. {mm_balance-0.1.16 → mm_balance-0.1.18}/src/mm_balance/output/formats/table_format.py +0 -0
  28. {mm_balance-0.1.16/tests → mm_balance-0.1.18/src/mm_balance/rpc}/__init__.py +0 -0
  29. {mm_balance-0.1.16 → mm_balance-0.1.18}/src/mm_balance/rpc/btc.py +0 -0
  30. {mm_balance-0.1.16 → mm_balance-0.1.18}/src/mm_balance/rpc/evm.py +0 -0
  31. {mm_balance-0.1.16 → mm_balance-0.1.18}/src/mm_balance/rpc/solana.py +0 -0
  32. /mm_balance-0.1.16/tests/conftest.py → /mm_balance-0.1.18/tests/__init__.py +0 -0
  33. {mm_balance-0.1.16 → mm_balance-0.1.18}/tests/test_dummy.py +0 -0
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: mm-balance
3
+ Version: 0.1.18
4
+ Requires-Python: >=3.12
5
+ Requires-Dist: mm-aptos==0.1.5
6
+ Requires-Dist: mm-btc==0.2.1
7
+ Requires-Dist: mm-eth==0.2.3
8
+ Requires-Dist: mm-solana==0.2.3
9
+ Requires-Dist: typer>=0.15.1
@@ -6,14 +6,14 @@ clean:
6
6
  rm -rf .pytest_cache .mypy_cache .ruff_cache .coverage dist build src/*.egg-info
7
7
 
8
8
  build: clean lint audit test
9
- uvx --from build pyproject-build --installer uv
9
+ uv build
10
10
 
11
11
  format:
12
12
  uv run ruff check --select I --fix src tests
13
13
  uv run ruff format src tests
14
14
 
15
15
  test:
16
- uv run coverage run -m pytest -n auto tests
16
+ uv run pytest -n auto tests
17
17
 
18
18
  lint: format
19
19
  uv run ruff check src tests
@@ -0,0 +1,72 @@
1
+ [project]
2
+ name = "mm-balance"
3
+ version = "0.1.18"
4
+ description = ""
5
+ requires-python = ">=3.12"
6
+ dependencies = [
7
+ "mm-btc==0.2.1",
8
+ "mm-eth==0.2.3",
9
+ "mm-solana==0.2.3",
10
+ "mm-aptos==0.1.5",
11
+ "typer>=0.15.1",
12
+ ]
13
+ [project.scripts]
14
+ mm-balance = "mm_balance.cli:app"
15
+
16
+ [build-system]
17
+ requires = ["hatchling"]
18
+ build-backend = "hatchling.build"
19
+
20
+ [tool.uv]
21
+ dev-dependencies = [
22
+ "pytest~=8.3.4",
23
+ "pytest-xdist~=3.6.1",
24
+ "ruff~=0.9.2",
25
+ "pip-audit~=2.7.3",
26
+ "bandit~=1.8.2",
27
+ "mypy~=1.14.1",
28
+ "types-PyYAML~=6.0.12.20241230",
29
+ ]
30
+
31
+ [tool.mypy]
32
+ python_version = "3.13"
33
+ warn_no_return = false
34
+ strict = true
35
+ exclude = ["^tests/", "^tmp/"]
36
+
37
+ [tool.ruff]
38
+ line-length = 130
39
+ target-version = "py313"
40
+ [tool.ruff.lint]
41
+ select = ["ALL"]
42
+ ignore = [
43
+ "A005", # flake8-builtins: stdlib-module-shadowing
44
+ "ERA001", # eradicate: commented-out-code
45
+ "PT", # flake8-pytest-style
46
+ "D", # pydocstyle
47
+ "FIX", # flake8-fixme
48
+ "PLR0911", # pylint: too-many-return-statements
49
+ "PLR0912", # pylint: too-many-branches
50
+ "PLR0913", # pylint: too-many-arguments
51
+ "PLR2004", # pylint: magic-value-comparison
52
+ "PLC0414", # pylint: useless-import-alias
53
+ "FBT", # flake8-boolean-trap
54
+ "EM", # flake8-errmsg
55
+ "TRY003", # tryceratops: raise-vanilla-args
56
+ "C901", # mccabe: complex-structure,
57
+ "BLE001", # flake8-blind-except
58
+ "S311", # bandit: suspicious-non-cryptographic-random-usage
59
+ "TD002", # flake8-todos: missing-todo-author
60
+ "TD003", # flake8-todos: missing-todo-link
61
+ "RET503", # flake8-return: implicit-return
62
+ "COM812", # it's used in ruff formatter
63
+ ]
64
+ [tool.ruff.lint.per-file-ignores]
65
+ "tests/*.py" = ["ANN", "S"]
66
+ [tool.ruff.format]
67
+ quote-style = "double"
68
+ indent-style = "space"
69
+
70
+ [tool.bandit]
71
+ exclude_dirs = ["tests"]
72
+ skips = ["B311"]
@@ -46,7 +46,7 @@ def cli(
46
46
  zip_password = "" # nosec
47
47
  if config_path.name.endswith(".zip"):
48
48
  zip_password = getpass.getpass("zip password")
49
- config = Config.read_config(config_path, zip_password=zip_password)
49
+ config = Config.read_config_or_exit(config_path, zip_password=zip_password)
50
50
 
51
51
  if print_format is not None:
52
52
  config.print_format = print_format
@@ -30,10 +30,12 @@ class Group(BaseConfig):
30
30
  result += " / " + self.network
31
31
  return result
32
32
 
33
+ @classmethod
33
34
  @field_validator("ticker", mode="after")
34
35
  def ticker_validator(cls, v: str) -> str:
35
36
  return v.upper()
36
37
 
38
+ @classmethod
37
39
  @field_validator("addresses", mode="before")
38
40
  def to_list_validator(cls, v: str | list[str] | None) -> list[str]:
39
41
  return cls.to_list_str_validator(v, unique=True, remove_comments=True, split_line=True)
@@ -67,6 +69,7 @@ class AddressGroup(BaseConfig):
67
69
  name: str
68
70
  addresses: list[str]
69
71
 
72
+ @classmethod
70
73
  @field_validator("addresses", mode="before")
71
74
  def to_list_validator(cls, v: str | list[str] | None) -> list[str]:
72
75
  return cls.to_list_str_validator(v, unique=True, remove_comments=True, split_line=True)
@@ -1,5 +1,3 @@
1
- from typing import Any
2
-
3
1
  from pydantic import GetCoreSchemaHandler
4
2
  from pydantic_core import CoreSchema, core_schema
5
3
 
@@ -11,11 +9,13 @@ TIMEOUT_DECIMALS = 5
11
9
 
12
10
 
13
11
  class Network(str):
12
+ __slots__ = ()
13
+
14
14
  def is_evm_network(self) -> bool:
15
15
  return self in [NETWORK_ETHEREUM, NETWORK_ARBITRUM_ONE, NETWORK_OP_MAINNET] or self.startswith("evm-")
16
16
 
17
17
  @classmethod
18
- def __get_pydantic_core_schema__(cls, _source_type: Any, handler: GetCoreSchemaHandler) -> CoreSchema:
18
+ def __get_pydantic_core_schema__(cls, _source_type: object, handler: GetCoreSchemaHandler) -> CoreSchema:
19
19
  return core_schema.no_info_after_validator_function(cls, handler(str))
20
20
 
21
21
 
@@ -7,7 +7,7 @@ def format_number(value: Decimal, separator: str, extra: str | None = None) -> s
7
7
  str_value = f"{value:,}".replace(",", separator)
8
8
  if extra == "$":
9
9
  return "$" + str_value
10
- elif extra == "%":
10
+ if extra == "%":
11
11
  return str_value + "%"
12
12
  return str_value
13
13
 
@@ -0,0 +1,48 @@
1
+ from collections import defaultdict
2
+ from decimal import Decimal
3
+
4
+ import pydash
5
+ from mm_std import hr
6
+ from mm_std.random_ import random_str_choice
7
+
8
+ from mm_balance.config import Config, Group
9
+ from mm_balance.constants import RETRIES_COINGECKO_PRICES, TICKER_TO_COINGECKO_ID
10
+
11
+
12
+ class Prices(defaultdict[str, Decimal]):
13
+ """
14
+ A Prices class representing a mapping from coin names to their prices.
15
+
16
+ Inherits from:
17
+ Dict[str, Decimal]: A dictionary with coin names as keys and their prices as Decimal values.
18
+ """
19
+
20
+
21
+ def get_prices(config: Config) -> Prices:
22
+ result = Prices()
23
+
24
+ coingecko_map: dict[str, str] = {} # ticker -> coingecko_id
25
+
26
+ for group in config.groups:
27
+ coingecko_id = get_coingecko_id(group)
28
+ if coingecko_id:
29
+ coingecko_map[group.ticker] = coingecko_id
30
+
31
+ url = f"https://api.coingecko.com/api/v3/simple/price?ids={','.join(coingecko_map.values())}&vs_currencies=usd"
32
+ for _ in range(RETRIES_COINGECKO_PRICES):
33
+ res = hr(url, proxy=random_str_choice(config.proxies))
34
+ if res.code != 200:
35
+ continue
36
+
37
+ for ticker, coingecko_id in coingecko_map.items():
38
+ if coingecko_id in res.json:
39
+ result[ticker] = Decimal(str(pydash.get(res.json, f"{coingecko_id}.usd")))
40
+ break
41
+
42
+ return result
43
+
44
+
45
+ def get_coingecko_id(group: Group) -> str | None:
46
+ if group.coingecko_id:
47
+ return group.coingecko_id
48
+ return TICKER_TO_COINGECKO_ID.get(group.ticker)
@@ -102,17 +102,16 @@ def _create_group_result(config: Config, group: Group, tasks: list[Task], prices
102
102
  balance: Balance | str
103
103
  if task.balance is None:
104
104
  balance = "balance is None! Something went wrong."
105
+ elif isinstance(task.balance, Ok):
106
+ coin_value = task.balance.ok
107
+ usd_value = Decimal(0)
108
+ if group.ticker in prices:
109
+ usd_value = round(coin_value * prices[group.ticker], config.round_ndigits)
110
+ balance = Balance(balance=coin_value, usd_value=usd_value)
111
+ balance_sum += balance.balance
112
+ usd_sum += balance.usd_value
105
113
  else:
106
- if isinstance(task.balance, Ok):
107
- coin_value = task.balance.ok
108
- usd_value = Decimal(0)
109
- if group.ticker in prices:
110
- usd_value = round(coin_value * prices[group.ticker], config.round_ndigits)
111
- balance = Balance(balance=coin_value, usd_value=usd_value)
112
- balance_sum += balance.balance
113
- usd_sum += balance.usd_value
114
- else:
115
- balance = task.balance.err
114
+ balance = task.balance.err
116
115
  addresses.append(AddressBalance(address=task.wallet_address, balance=balance))
117
116
 
118
117
  balance_sum_share = balance_sum * group.share
@@ -10,7 +10,7 @@ def get_balance(
10
10
  nodes: list[str], wallet: str, token: str | None, decimals: int, proxies: list[str], round_ndigits: int
11
11
  ) -> Result[Decimal]:
12
12
  if token is None:
13
- token = "0x1::aptos_coin::AptosCoin" # nosec
13
+ token = "0x1::aptos_coin::AptosCoin" # noqa: S105 # nosec
14
14
  return balance.get_decimal_balance_with_retries(
15
15
  RETRIES_BALANCE,
16
16
  nodes,
@@ -28,9 +28,7 @@ def get_token_decimals(config: Config) -> TokenDecimals:
28
28
  result[group.network][None] = 18
29
29
  elif group.network == NETWORK_SOLANA:
30
30
  result[group.network][None] = 9
31
- elif group.network == NETWORK_BITCOIN:
32
- result[group.network][None] = 8
33
- elif group.network == NETWORK_APTOS:
31
+ elif group.network in (NETWORK_BITCOIN, NETWORK_APTOS):
34
32
  result[group.network][None] = 8
35
33
  else:
36
34
  fatal(f"Can't get token decimals for native token on network: {group.network}")
@@ -5,6 +5,6 @@ def fnumber(value: Decimal, separator: str, extra: str | None = None) -> str:
5
5
  str_value = f"{value:,}".replace(",", separator)
6
6
  if extra == "$":
7
7
  return "$" + str_value
8
- elif extra == "%":
8
+ if extra == "%":
9
9
  return str_value + "%"
10
10
  return str_value
@@ -1,8 +1,8 @@
1
1
  from dataclasses import dataclass
2
2
  from decimal import Decimal
3
+ from typing import TYPE_CHECKING
3
4
 
4
5
  from mm_std import ConcurrentTasks, PrintFormat, Result
5
- from rich.progress import TaskID
6
6
 
7
7
  from mm_balance.config import Config
8
8
  from mm_balance.constants import NETWORK_APTOS, NETWORK_BITCOIN, NETWORK_SOLANA, Network
@@ -10,6 +10,9 @@ from mm_balance.output import utils
10
10
  from mm_balance.rpc import aptos, btc, evm, solana
11
11
  from mm_balance.token_decimals import TokenDecimals
12
12
 
13
+ if TYPE_CHECKING:
14
+ from rich.progress import TaskID
15
+
13
16
 
14
17
  @dataclass
15
18
  class Task:
@@ -20,7 +23,7 @@ class Task:
20
23
 
21
24
 
22
25
  class Workers:
23
- def __init__(self, config: Config, token_decimals: TokenDecimals):
26
+ def __init__(self, config: Config, token_decimals: TokenDecimals) -> None:
24
27
  self.config = config
25
28
  self.token_decimals = token_decimals
26
29
  self.tasks: dict[Network, list[Task]] = {network: [] for network in config.networks()}
@@ -49,9 +52,7 @@ class Workers:
49
52
  def get_errors(self) -> list[Task]:
50
53
  result = []
51
54
  for network in self.tasks:
52
- for task in self.tasks[network]:
53
- if task.balance is not None and task.balance.is_err():
54
- result.append(task)
55
+ result.extend([task for task in self.tasks[network] if task.balance is not None and task.balance.is_err()])
55
56
  return result
56
57
 
57
58
  def _process_network(self, network: Network) -> None:
File without changes