mm-balance 0.2.4__py3-none-any.whl → 0.3.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_balance/cli.py +14 -2
- mm_balance/config.py +2 -0
- mm_balance/constants.py +8 -1
- mm_balance/diff.py +168 -0
- mm_balance/result.py +13 -0
- mm_balance-0.3.1.dist-info/METADATA +10 -0
- {mm_balance-0.2.4.dist-info → mm_balance-0.3.1.dist-info}/RECORD +9 -8
- mm_balance-0.2.4.dist-info/METADATA +0 -9
- {mm_balance-0.2.4.dist-info → mm_balance-0.3.1.dist-info}/WHEEL +0 -0
- {mm_balance-0.2.4.dist-info → mm_balance-0.3.1.dist-info}/entry_points.txt +0 -0
mm_balance/cli.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import getpass
|
|
2
2
|
import importlib.metadata
|
|
3
|
-
import pathlib
|
|
4
3
|
import pkgutil
|
|
4
|
+
from pathlib import Path
|
|
5
5
|
from typing import Annotated
|
|
6
6
|
|
|
7
7
|
import typer
|
|
@@ -9,6 +9,7 @@ from mm_std import PrintFormat, fatal, pretty_print_toml
|
|
|
9
9
|
|
|
10
10
|
from mm_balance.config import Config
|
|
11
11
|
from mm_balance.constants import NETWORKS
|
|
12
|
+
from mm_balance.diff import BalancesDict, Diff
|
|
12
13
|
from mm_balance.output.formats import json_format, table_format
|
|
13
14
|
from mm_balance.price import Prices, get_prices
|
|
14
15
|
from mm_balance.result import create_balances_result
|
|
@@ -42,12 +43,14 @@ def networks_callback(value: bool) -> None:
|
|
|
42
43
|
|
|
43
44
|
@app.command()
|
|
44
45
|
def cli(
|
|
45
|
-
config_path: Annotated[
|
|
46
|
+
config_path: Annotated[Path, typer.Argument()],
|
|
46
47
|
print_format: Annotated[PrintFormat | None, typer.Option("--format", "-f", help="Print format.")] = None,
|
|
47
48
|
skip_empty: Annotated[bool | None, typer.Option("--skip-empty", "-s", help="Skip empty balances.")] = None,
|
|
48
49
|
debug: Annotated[bool | None, typer.Option("--debug", "-d", help="Print debug info.")] = None,
|
|
49
50
|
print_config: Annotated[bool | None, typer.Option("--config", "-c", help="Print config and exit.")] = None,
|
|
50
51
|
price: Annotated[bool | None, typer.Option("--price/--no-price", help="Print prices.")] = None,
|
|
52
|
+
save_balances: Annotated[Path | None, typer.Option("--save-balances", help="Save balances file.")] = None,
|
|
53
|
+
diff_from_balances: Annotated[Path | None, typer.Option("--diff-from-balances", help="Diff from balances file.")] = None,
|
|
51
54
|
_example: Annotated[bool | None, typer.Option("--example", callback=example_callback, help="Print a config example.")] = None,
|
|
52
55
|
_networks: Annotated[
|
|
53
56
|
bool | None, typer.Option("--networks", callback=networks_callback, help="Print supported networks.")
|
|
@@ -93,6 +96,15 @@ def cli(
|
|
|
93
96
|
else:
|
|
94
97
|
fatal("Unsupported print format")
|
|
95
98
|
|
|
99
|
+
if save_balances:
|
|
100
|
+
BalancesDict.from_balances_result(result).save_to_path(save_balances)
|
|
101
|
+
|
|
102
|
+
if diff_from_balances:
|
|
103
|
+
old_balances = BalancesDict.from_file(diff_from_balances)
|
|
104
|
+
new_balances = BalancesDict.from_balances_result(result)
|
|
105
|
+
diff = Diff.calc(old_balances, new_balances)
|
|
106
|
+
diff.print(config.settings.print_format)
|
|
107
|
+
|
|
96
108
|
|
|
97
109
|
if __name__ == "__main__":
|
|
98
110
|
app()
|
mm_balance/config.py
CHANGED
|
@@ -60,6 +60,8 @@ class Group(BaseConfig):
|
|
|
60
60
|
else:
|
|
61
61
|
result.append(line)
|
|
62
62
|
# TODO: check address is valid. There is network info in the group
|
|
63
|
+
if self.network.need_lowercase_address():
|
|
64
|
+
result = [address.lower() for address in result]
|
|
63
65
|
self.addresses = pydash.uniq(result)
|
|
64
66
|
|
|
65
67
|
|
mm_balance/constants.py
CHANGED
|
@@ -12,13 +12,19 @@ class Network(str):
|
|
|
12
12
|
__slots__ = ()
|
|
13
13
|
|
|
14
14
|
def is_evm_network(self) -> bool:
|
|
15
|
-
return self in
|
|
15
|
+
return self in EVM_NETWORKS or self.startswith("evm-")
|
|
16
|
+
|
|
17
|
+
def need_lowercase_address(self) -> bool:
|
|
18
|
+
return self.is_evm_network()
|
|
16
19
|
|
|
17
20
|
@classmethod
|
|
18
21
|
def __get_pydantic_core_schema__(cls, _source_type: object, handler: GetCoreSchemaHandler) -> CoreSchema:
|
|
19
22
|
return core_schema.no_info_after_validator_function(cls, handler(str))
|
|
20
23
|
|
|
21
24
|
|
|
25
|
+
# evm networks
|
|
26
|
+
|
|
27
|
+
# other networks
|
|
22
28
|
NETWORK_APTOS = Network("aptos")
|
|
23
29
|
NETWORK_ARBITRUM_ONE = Network("arbitrum-one")
|
|
24
30
|
NETWORK_BITCOIN = Network("bitcoin")
|
|
@@ -26,6 +32,7 @@ NETWORK_ETHEREUM = Network("ethereum")
|
|
|
26
32
|
NETWORK_SOLANA = Network("solana")
|
|
27
33
|
NETWORK_OP_MAINNET = Network("op-mainnet")
|
|
28
34
|
NETWORKS = [NETWORK_APTOS, NETWORK_ARBITRUM_ONE, NETWORK_BITCOIN, NETWORK_ETHEREUM, NETWORK_SOLANA, NETWORK_OP_MAINNET]
|
|
35
|
+
EVM_NETWORKS = [NETWORK_ETHEREUM, NETWORK_ARBITRUM_ONE, NETWORK_OP_MAINNET]
|
|
29
36
|
|
|
30
37
|
|
|
31
38
|
TOKEN_ADDRESS: dict[Network, dict[str, str]] = {
|
mm_balance/diff.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from decimal import Decimal
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from deepdiff.diff import DeepDiff
|
|
9
|
+
from mm_std import PrintFormat, print_json, print_plain, print_table
|
|
10
|
+
from pydantic import BaseModel, RootModel
|
|
11
|
+
|
|
12
|
+
from mm_balance.result import Balance, BalancesResult
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BalancesDict(RootModel[dict[str, dict[str, dict[str, Decimal]]]]): # network->ticker->address->balance
|
|
16
|
+
def networks(self) -> set[str]:
|
|
17
|
+
return set(self.model_dump().keys())
|
|
18
|
+
|
|
19
|
+
def tickers(self, network: str) -> set[str]:
|
|
20
|
+
return set(self.model_dump()[network].keys())
|
|
21
|
+
|
|
22
|
+
def save_to_path(self, balances_file: Path) -> None:
|
|
23
|
+
json.dump(self.model_dump(), balances_file.open("w"), default=str, indent=2)
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def from_balances_result(result: BalancesResult) -> BalancesDict:
|
|
27
|
+
data: dict[str, dict[str, dict[str, Decimal]]] = {} # network->ticker->address->balance
|
|
28
|
+
for group in result.groups:
|
|
29
|
+
if group.network not in data:
|
|
30
|
+
data[group.network] = {}
|
|
31
|
+
if group.ticker not in data[group.network]:
|
|
32
|
+
data[group.network][group.ticker] = {}
|
|
33
|
+
for address in group.addresses:
|
|
34
|
+
if isinstance(address.balance, Balance):
|
|
35
|
+
data[group.network][group.ticker][address.address] = address.balance.balance
|
|
36
|
+
return BalancesDict(data)
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def from_file(path: Path) -> BalancesDict:
|
|
40
|
+
return BalancesDict(json.load(path.open("r")))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Diff(BaseModel):
|
|
44
|
+
network_added: list[str]
|
|
45
|
+
network_removed: list[str]
|
|
46
|
+
|
|
47
|
+
ticker_added: dict[str, list[str]] # network -> tickers
|
|
48
|
+
ticker_removed: dict[str, list[str]] # network -> tickers
|
|
49
|
+
|
|
50
|
+
address_added: dict[str, dict[str, list[str]]] # network -> ticker -> addresses
|
|
51
|
+
address_removed: dict[str, dict[str, list[str]]] # network -> ticker -> addresses
|
|
52
|
+
|
|
53
|
+
balance_changed: dict[str, dict[str, dict[str, tuple[Decimal, Decimal]]]] # network->ticker->address->(old_value,new_value)
|
|
54
|
+
|
|
55
|
+
def print(self, print_format: PrintFormat) -> None:
|
|
56
|
+
match print_format:
|
|
57
|
+
case PrintFormat.TABLE:
|
|
58
|
+
self._print_table()
|
|
59
|
+
case PrintFormat.JSON:
|
|
60
|
+
self._print_json()
|
|
61
|
+
case _:
|
|
62
|
+
raise ValueError(f"Unsupported print format: {print_format}")
|
|
63
|
+
|
|
64
|
+
def _print_table(self) -> None:
|
|
65
|
+
if (
|
|
66
|
+
not self.network_added
|
|
67
|
+
and not self.network_removed
|
|
68
|
+
and not self.ticker_added
|
|
69
|
+
and not self.ticker_removed
|
|
70
|
+
and not self.address_added
|
|
71
|
+
and not self.address_removed
|
|
72
|
+
and not self.balance_changed
|
|
73
|
+
):
|
|
74
|
+
print_plain("Diff: no changes")
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
print_plain("\nDiff")
|
|
78
|
+
|
|
79
|
+
if self.network_added:
|
|
80
|
+
print_plain(f"networks added: {self.network_added}")
|
|
81
|
+
if self.network_removed:
|
|
82
|
+
print_plain(f"networks removed: {self.network_removed}")
|
|
83
|
+
if self.ticker_added:
|
|
84
|
+
print_plain(f"tickers added: {self.ticker_added}")
|
|
85
|
+
if self.ticker_removed:
|
|
86
|
+
print_plain(f"tickers removed: {self.ticker_removed}")
|
|
87
|
+
if self.address_added:
|
|
88
|
+
print_plain(f"addresses added: {self.address_added}")
|
|
89
|
+
if self.address_removed:
|
|
90
|
+
print_plain(f"addresses removed: {self.address_removed}")
|
|
91
|
+
|
|
92
|
+
if self.balance_changed:
|
|
93
|
+
rows = []
|
|
94
|
+
for network in self.balance_changed:
|
|
95
|
+
for ticker in self.balance_changed[network]:
|
|
96
|
+
for address in self.balance_changed[network][ticker]:
|
|
97
|
+
old_value, new_value = self.balance_changed[network][ticker][address]
|
|
98
|
+
rows.append([network, ticker, address, old_value, new_value, new_value - old_value])
|
|
99
|
+
print_table("", ["Network", "Ticker", "Address", "Old", "New", "Change"], rows)
|
|
100
|
+
|
|
101
|
+
def _print_json(self) -> None:
|
|
102
|
+
print_json(data=self.model_dump(), default_serializer=str)
|
|
103
|
+
|
|
104
|
+
@staticmethod
|
|
105
|
+
def calc(balances1: BalancesDict, balances2: BalancesDict) -> Diff:
|
|
106
|
+
dd = DeepDiff(balances1.model_dump(), balances2.model_dump(), ignore_order=True)
|
|
107
|
+
# Initialize empty collections for Diff fields.
|
|
108
|
+
|
|
109
|
+
network_added = []
|
|
110
|
+
network_removed = []
|
|
111
|
+
ticker_added: dict[str, list[str]] = {}
|
|
112
|
+
ticker_removed: dict[str, list[str]] = {}
|
|
113
|
+
address_added: dict[str, dict[str, list[str]]] = {}
|
|
114
|
+
address_removed: dict[str, dict[str, list[str]]] = {}
|
|
115
|
+
balance_changed: dict[str, dict[str, dict[str, tuple[Decimal, Decimal]]]] = {}
|
|
116
|
+
|
|
117
|
+
# Helper to extract keys from DeepDiff paths.
|
|
118
|
+
def extract_keys(path: str) -> list[str]:
|
|
119
|
+
# DeepDiff paths look like "root['network']['ticker']['address']"
|
|
120
|
+
return re.findall(r"\['([^']+)'\]", path)
|
|
121
|
+
|
|
122
|
+
# Process dictionary_item_added
|
|
123
|
+
for path in dd.get("dictionary_item_added", []):
|
|
124
|
+
keys = extract_keys(path)
|
|
125
|
+
if len(keys) == 1:
|
|
126
|
+
# New network added.
|
|
127
|
+
network_added.append(keys[0])
|
|
128
|
+
elif len(keys) == 2:
|
|
129
|
+
# New ticker added under an existing network.
|
|
130
|
+
network, ticker = keys
|
|
131
|
+
ticker_added.setdefault(network, []).append(ticker)
|
|
132
|
+
elif len(keys) == 3:
|
|
133
|
+
# New address added under an existing network and ticker.
|
|
134
|
+
network, ticker, address = keys
|
|
135
|
+
address_added.setdefault(network, {}).setdefault(ticker, []).append(address)
|
|
136
|
+
|
|
137
|
+
# Process dictionary_item_removed
|
|
138
|
+
for path in dd.get("dictionary_item_removed", []):
|
|
139
|
+
keys = extract_keys(path)
|
|
140
|
+
if len(keys) == 1:
|
|
141
|
+
network_removed.append(keys[0])
|
|
142
|
+
elif len(keys) == 2:
|
|
143
|
+
network, ticker = keys
|
|
144
|
+
ticker_removed.setdefault(network, []).append(ticker)
|
|
145
|
+
elif len(keys) == 3:
|
|
146
|
+
network, ticker, address = keys
|
|
147
|
+
address_removed.setdefault(network, {}).setdefault(ticker, []).append(address)
|
|
148
|
+
|
|
149
|
+
# Process values_changed for balance differences.
|
|
150
|
+
for path, change in dd.get("values_changed", {}).items():
|
|
151
|
+
keys = extract_keys(path)
|
|
152
|
+
if len(keys) != 3:
|
|
153
|
+
continue
|
|
154
|
+
network, ticker, address = keys
|
|
155
|
+
balance_changed.setdefault(network, {}).setdefault(ticker, {})[address] = (
|
|
156
|
+
Decimal(change["old_value"]),
|
|
157
|
+
Decimal(change["new_value"]),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
return Diff(
|
|
161
|
+
network_added=sorted(network_added),
|
|
162
|
+
network_removed=sorted(network_removed),
|
|
163
|
+
ticker_added={k: sorted(v) for k, v in ticker_added.items()},
|
|
164
|
+
ticker_removed={k: sorted(v) for k, v in ticker_removed.items()},
|
|
165
|
+
address_added={k: {tk: sorted(vv) for tk, vv in v.items()} for k, v in address_added.items()},
|
|
166
|
+
address_removed={k: {tk: sorted(vv) for tk, vv in v.items()} for k, v in address_removed.items()},
|
|
167
|
+
balance_changed=balance_changed,
|
|
168
|
+
)
|
mm_balance/result.py
CHANGED
|
@@ -63,6 +63,19 @@ def create_balances_result(config: Config, prices: Prices, workers: Workers) ->
|
|
|
63
63
|
return BalancesResult(groups=groups, total=total, total_share=total_share)
|
|
64
64
|
|
|
65
65
|
|
|
66
|
+
# def save_balances_file(result: BalancesResult, balances_file: Path) -> None:
|
|
67
|
+
# data = {}
|
|
68
|
+
# for group in result.groups:
|
|
69
|
+
# if group.network not in data:
|
|
70
|
+
# data[group.network] = {}
|
|
71
|
+
# if group.ticker not in data[group.network]:
|
|
72
|
+
# data[group.network][group.ticker] = {}
|
|
73
|
+
# for address in group.addresses:
|
|
74
|
+
# if isinstance(address.balance, Balance):
|
|
75
|
+
# data[group.network][group.ticker][address.address] = float(address.balance.balance)
|
|
76
|
+
# json.dump(data, balances_file.open("w"), indent=2)
|
|
77
|
+
|
|
78
|
+
|
|
66
79
|
def _create_total(use_share: bool, groups: list[GroupResult]) -> Total:
|
|
67
80
|
coin_balances: dict[str, Decimal] = defaultdict(Decimal) # ticker -> balance
|
|
68
81
|
coin_usd_values: dict[str, Decimal] = defaultdict(Decimal) # ticker -> usd value
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mm-balance
|
|
3
|
+
Version: 0.3.1
|
|
4
|
+
Requires-Python: >=3.12
|
|
5
|
+
Requires-Dist: deepdiff==8.2.0
|
|
6
|
+
Requires-Dist: mm-aptos==0.2.0
|
|
7
|
+
Requires-Dist: mm-btc==0.3.0
|
|
8
|
+
Requires-Dist: mm-eth==0.5.4
|
|
9
|
+
Requires-Dist: mm-sol==0.5.2
|
|
10
|
+
Requires-Dist: typer==0.15.1
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
mm_balance/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
mm_balance/cli.py,sha256=
|
|
3
|
-
mm_balance/config.py,sha256=
|
|
4
|
-
mm_balance/constants.py,sha256=
|
|
2
|
+
mm_balance/cli.py,sha256=ROfCm1h0KUm2Uh0a74wNCPIGMgQAOAZKIYBBFl7gcKc,4444
|
|
3
|
+
mm_balance/config.py,sha256=DsADA8jxCZSOT4-acQorKYdn0QbN10B-5GUGEiW9mfk,4675
|
|
4
|
+
mm_balance/constants.py,sha256=2rINgkMYX4SDju21Ep5Djp5WirvvjYbasQgSvtdb3yM,2479
|
|
5
|
+
mm_balance/diff.py,sha256=GPRbykty2TIBBM8jpYXOV9Itjyd_mz0BTUsQ8KX7cNo,7099
|
|
5
6
|
mm_balance/price.py,sha256=DzvcQngS6wgi_4YWoXxGvOuOkwJvUbN0KI8DeIxbB5A,1494
|
|
6
|
-
mm_balance/result.py,sha256
|
|
7
|
+
mm_balance/result.py,sha256=-Ebq07JMLcQAmRs82cA6aYMbsT1qbZSyAOixr9K_wbg,5157
|
|
7
8
|
mm_balance/token_decimals.py,sha256=N3YppB2F3J_OuNkawpAHLVD-4MRCoVjBI01_OoRg5sY,2201
|
|
8
9
|
mm_balance/utils.py,sha256=_UMX3TV350Sr222tAnxGUf0R5McpwloNTQC-U-xiuHc,636
|
|
9
10
|
mm_balance/workers.py,sha256=eg0Ve1xVu3Kd_thfVmPsp6tEdJsYYvs1ipXiu5rKItY,3758
|
|
@@ -18,7 +19,7 @@ mm_balance/rpc/aptos.py,sha256=1JCYCqDim4tk1axXscaAJRXPd4J6vV1ABFbwMbPgrL0,641
|
|
|
18
19
|
mm_balance/rpc/btc.py,sha256=wBMxUjbqdQipVsTFVkj4tk7loErA2czLVfvG8vjFLeE,493
|
|
19
20
|
mm_balance/rpc/evm.py,sha256=LaU2csGL-VlQauCiTX_WFnstvTyZLMP5gDw2LyV53m8,1048
|
|
20
21
|
mm_balance/rpc/solana.py,sha256=10rJ4eEr9sfEfhXx-X2R7bdJ5dL7bVMwHHfJ4R3QR7U,1071
|
|
21
|
-
mm_balance-0.
|
|
22
|
-
mm_balance-0.
|
|
23
|
-
mm_balance-0.
|
|
24
|
-
mm_balance-0.
|
|
22
|
+
mm_balance-0.3.1.dist-info/METADATA,sha256=3itefi1OrREJ0RuW8V_ziuWz5ehJHTMQYU-qwS4cXAE,256
|
|
23
|
+
mm_balance-0.3.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
24
|
+
mm_balance-0.3.1.dist-info/entry_points.txt,sha256=rSnP0ZW1a3ACNwTWM7T53CmOycKbzhG43m2_wseENng,50
|
|
25
|
+
mm_balance-0.3.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|