mm-balance 0.2.4__py3-none-any.whl → 0.3.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_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,16 @@ def networks_callback(value: bool) -> None:
42
43
 
43
44
  @app.command()
44
45
  def cli(
45
- config_path: Annotated[pathlib.Path, typer.Argument()],
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_file: Annotated[Path | None, typer.Option("--save-balances-file", help="Save balances file.")] = None,
53
+ diff_from_balances_file: Annotated[
54
+ Path | None, typer.Option("--diff-from-balances-file", help="Diff from balances file.")
55
+ ] = None,
51
56
  _example: Annotated[bool | None, typer.Option("--example", callback=example_callback, help="Print a config example.")] = None,
52
57
  _networks: Annotated[
53
58
  bool | None, typer.Option("--networks", callback=networks_callback, help="Print supported networks.")
@@ -93,6 +98,15 @@ def cli(
93
98
  else:
94
99
  fatal("Unsupported print format")
95
100
 
101
+ if save_balances_file:
102
+ BalancesDict.from_balances_result(result).save_to_path(save_balances_file)
103
+
104
+ if diff_from_balances_file:
105
+ old_balances = BalancesDict.from_file(diff_from_balances_file)
106
+ new_balances = BalancesDict.from_balances_result(result)
107
+ diff = Diff.calc(old_balances, new_balances)
108
+ diff.print(config.settings.print_format)
109
+
96
110
 
97
111
  if __name__ == "__main__":
98
112
  app()
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
@@ -1,9 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mm-balance
3
- Version: 0.2.4
3
+ Version: 0.3.0
4
4
  Requires-Python: >=3.12
5
+ Requires-Dist: deepdiff==8.2.0
5
6
  Requires-Dist: mm-aptos==0.2.0
6
7
  Requires-Dist: mm-btc==0.3.0
7
8
  Requires-Dist: mm-eth==0.5.3
8
9
  Requires-Dist: mm-sol==0.5.1
9
- Requires-Dist: typer>=0.15.1
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=AHaD6w97H8ru8nnQv2dcqh9v-d9BVirnlsClbLyHf6M,3796
2
+ mm_balance/cli.py,sha256=nRDVejN-dnr1iRezBxNk4z0oLa59tiwzIVhgeOGzvbo,4498
3
3
  mm_balance/config.py,sha256=gywHNWEQL1xeG778cuca5aBoXElLIfoGle3xxW6m_0U,4564
4
4
  mm_balance/constants.py,sha256=kqG2zuwv0l-PzDHIrMJVQpfQWiXjr2DsqGPcKmqNJLo,2334
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=2bgK4PqS_uFGkYMj6LBiMRh6jRtzsKIkB5csZo0mAEQ,4584
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.2.4.dist-info/METADATA,sha256=Qw5u6BzDKrcp7ZvP55NIWQx_PjpNVWuZt4wCsenugaM,225
22
- mm_balance-0.2.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
23
- mm_balance-0.2.4.dist-info/entry_points.txt,sha256=rSnP0ZW1a3ACNwTWM7T53CmOycKbzhG43m2_wseENng,50
24
- mm_balance-0.2.4.dist-info/RECORD,,
22
+ mm_balance-0.3.0.dist-info/METADATA,sha256=Owy1HtXoCk7RhiqeEwijv80KKLuDBHbgQdHOrjAxvEs,256
23
+ mm_balance-0.3.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
24
+ mm_balance-0.3.0.dist-info/entry_points.txt,sha256=rSnP0ZW1a3ACNwTWM7T53CmOycKbzhG43m2_wseENng,50
25
+ mm_balance-0.3.0.dist-info/RECORD,,