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.
- mm_balance-0.1.18/PKG-INFO +9 -0
- {mm_balance-0.1.16 → mm_balance-0.1.18}/justfile +2 -2
- mm_balance-0.1.18/pyproject.toml +72 -0
- {mm_balance-0.1.16 → mm_balance-0.1.18}/src/mm_balance/cli.py +1 -1
- {mm_balance-0.1.16 → mm_balance-0.1.18}/src/mm_balance/config.py +3 -0
- {mm_balance-0.1.16 → mm_balance-0.1.18}/src/mm_balance/constants.py +3 -3
- {mm_balance-0.1.16 → mm_balance-0.1.18}/src/mm_balance/output/utils.py +1 -1
- mm_balance-0.1.18/src/mm_balance/price.py +48 -0
- {mm_balance-0.1.16 → mm_balance-0.1.18}/src/mm_balance/result.py +9 -10
- {mm_balance-0.1.16 → mm_balance-0.1.18}/src/mm_balance/rpc/aptos.py +1 -1
- {mm_balance-0.1.16 → mm_balance-0.1.18}/src/mm_balance/token_decimals.py +1 -3
- {mm_balance-0.1.16 → mm_balance-0.1.18}/src/mm_balance/utils.py +1 -1
- {mm_balance-0.1.16 → mm_balance-0.1.18}/src/mm_balance/workers.py +6 -5
- mm_balance-0.1.18/tests/conftest.py +0 -0
- mm_balance-0.1.18/uv.lock +2029 -0
- mm_balance-0.1.16/PKG-INFO +0 -9
- mm_balance-0.1.16/pyproject.toml +0 -66
- mm_balance-0.1.16/src/mm_balance/price.py +0 -48
- mm_balance-0.1.16/uv.lock +0 -1947
- {mm_balance-0.1.16 → mm_balance-0.1.18}/.gitignore +0 -0
- {mm_balance-0.1.16 → mm_balance-0.1.18}/README.md +0 -0
- {mm_balance-0.1.16 → mm_balance-0.1.18}/src/mm_balance/__init__.py +0 -0
- {mm_balance-0.1.16 → mm_balance-0.1.18}/src/mm_balance/config/example.yml +0 -0
- {mm_balance-0.1.16 → mm_balance-0.1.18}/src/mm_balance/output/__init__.py +0 -0
- {mm_balance-0.1.16/src/mm_balance/rpc → mm_balance-0.1.18/src/mm_balance/output/formats}/__init__.py +0 -0
- {mm_balance-0.1.16 → mm_balance-0.1.18}/src/mm_balance/output/formats/json_format.py +0 -0
- {mm_balance-0.1.16 → mm_balance-0.1.18}/src/mm_balance/output/formats/table_format.py +0 -0
- {mm_balance-0.1.16/tests → mm_balance-0.1.18/src/mm_balance/rpc}/__init__.py +0 -0
- {mm_balance-0.1.16 → mm_balance-0.1.18}/src/mm_balance/rpc/btc.py +0 -0
- {mm_balance-0.1.16 → mm_balance-0.1.18}/src/mm_balance/rpc/evm.py +0 -0
- {mm_balance-0.1.16 → mm_balance-0.1.18}/src/mm_balance/rpc/solana.py +0 -0
- /mm_balance-0.1.16/tests/conftest.py → /mm_balance-0.1.18/tests/__init__.py +0 -0
- {mm_balance-0.1.16 → mm_balance-0.1.18}/tests/test_dummy.py +0 -0
|
@@ -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
|
-
|
|
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
|
|
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.
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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}")
|
|
@@ -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
|