mm-balance 0.6.0__tar.gz → 0.6.2__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.6.0 → mm_balance-0.6.2}/.claude/settings.local.json +2 -1
- mm_balance-0.6.2/CLAUDE.md +24 -0
- mm_balance-0.6.2/PKG-INFO +10 -0
- {mm_balance-0.6.0 → mm_balance-0.6.2}/pyproject.toml +14 -14
- {mm_balance-0.6.0 → mm_balance-0.6.2}/src/mm_balance/balance_fetcher.py +2 -2
- {mm_balance-0.6.0 → mm_balance-0.6.2}/src/mm_balance/cli.py +4 -4
- {mm_balance-0.6.0 → mm_balance-0.6.2}/src/mm_balance/command_runner.py +2 -3
- {mm_balance-0.6.0 → mm_balance-0.6.2}/src/mm_balance/config.py +2 -5
- {mm_balance-0.6.0 → mm_balance-0.6.2}/src/mm_balance/diff.py +16 -16
- {mm_balance-0.6.0 → mm_balance-0.6.2}/src/mm_balance/output/formats/json_format.py +2 -2
- {mm_balance-0.6.0 → mm_balance-0.6.2}/src/mm_balance/output/formats/table_format.py +11 -9
- {mm_balance-0.6.0 → mm_balance-0.6.2}/src/mm_balance/price.py +1 -1
- {mm_balance-0.6.0 → mm_balance-0.6.2}/src/mm_balance/result.py +2 -0
- {mm_balance-0.6.0 → mm_balance-0.6.2}/src/mm_balance/token_decimals.py +4 -5
- {mm_balance-0.6.0 → mm_balance-0.6.2}/src/mm_balance/utils.py +10 -1
- mm_balance-0.6.2/uv.lock +2038 -0
- mm_balance-0.6.0/CLAUDE.md +0 -13
- mm_balance-0.6.0/PKG-INFO +0 -10
- mm_balance-0.6.0/uv.lock +0 -2226
- {mm_balance-0.6.0 → mm_balance-0.6.2}/.gitignore +0 -0
- {mm_balance-0.6.0 → mm_balance-0.6.2}/README.md +0 -0
- {mm_balance-0.6.0 → mm_balance-0.6.2}/justfile +0 -0
- {mm_balance-0.6.0 → mm_balance-0.6.2}/src/mm_balance/__init__.py +0 -0
- {mm_balance-0.6.0 → mm_balance-0.6.2}/src/mm_balance/config/example.toml +0 -0
- {mm_balance-0.6.0 → mm_balance-0.6.2}/src/mm_balance/constants.py +0 -0
- {mm_balance-0.6.0 → mm_balance-0.6.2}/src/mm_balance/output/__init__.py +0 -0
- {mm_balance-0.6.0 → mm_balance-0.6.2}/src/mm_balance/output/formats/__init__.py +0 -0
- {mm_balance-0.6.0 → mm_balance-0.6.2}/src/mm_balance/output/utils.py +0 -0
- {mm_balance-0.6.0 → mm_balance-0.6.2}/src/mm_balance/rpc.py +0 -0
- {mm_balance-0.6.0 → mm_balance-0.6.2}/tests/__init__.py +0 -0
- {mm_balance-0.6.0 → mm_balance-0.6.2}/tests/conftest.py +0 -0
- {mm_balance-0.6.0 → mm_balance-0.6.2}/tests/test_share_expression.py +0 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# AI Agent Start Guide
|
|
2
|
+
|
|
3
|
+
## Critical: Language
|
|
4
|
+
RESPOND IN ENGLISH. Always. No exceptions.
|
|
5
|
+
User's language does NOT determine your response language.
|
|
6
|
+
Only switch if user EXPLICITLY requests it (e.g., "respond in {language}").
|
|
7
|
+
Language switching applies ONLY to chat. All code, comments, commit messages, and files must ALWAYS be in English — no exceptions.
|
|
8
|
+
|
|
9
|
+
## Mandatory Rules (external)
|
|
10
|
+
These files are REQUIRED. Read them fully and follow all rules.
|
|
11
|
+
- `~/.claude/shared-rules/general.md`
|
|
12
|
+
- `~/.claude/shared-rules/python.md`
|
|
13
|
+
|
|
14
|
+
## Project Reading (context)
|
|
15
|
+
These files are REQUIRED for project understanding.
|
|
16
|
+
- `README.md`
|
|
17
|
+
|
|
18
|
+
## Preflight (mandatory)
|
|
19
|
+
Before your first response:
|
|
20
|
+
1. Read all files listed above.
|
|
21
|
+
2. Do not answer until all are read.
|
|
22
|
+
3. In your first reply, list every file you have read from this document.
|
|
23
|
+
|
|
24
|
+
Failure to follow this protocol is considered an error.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mm-balance
|
|
3
|
+
Version: 0.6.2
|
|
4
|
+
Requires-Python: >=3.14
|
|
5
|
+
Requires-Dist: deepdiff==8.6.1
|
|
6
|
+
Requires-Dist: mm-apt==0.6.0
|
|
7
|
+
Requires-Dist: mm-btc==0.6.0
|
|
8
|
+
Requires-Dist: mm-concurrency~=0.2.0
|
|
9
|
+
Requires-Dist: mm-eth==0.8.0
|
|
10
|
+
Requires-Dist: mm-sol==0.8.0
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "mm-balance"
|
|
3
|
-
version = "0.6.
|
|
3
|
+
version = "0.6.2"
|
|
4
4
|
description = ""
|
|
5
|
-
requires-python = ">=3.
|
|
5
|
+
requires-python = ">=3.14"
|
|
6
6
|
dependencies = [
|
|
7
|
-
"mm-concurrency~=0.
|
|
8
|
-
"mm-apt==0.
|
|
9
|
-
"mm-btc==0.
|
|
10
|
-
"mm-eth==0.
|
|
11
|
-
"mm-sol==0.
|
|
7
|
+
"mm-concurrency~=0.2.0",
|
|
8
|
+
"mm-apt==0.6.0",
|
|
9
|
+
"mm-btc==0.6.0",
|
|
10
|
+
"mm-eth==0.8.0",
|
|
11
|
+
"mm-sol==0.8.0",
|
|
12
12
|
"deepdiff==8.6.1",
|
|
13
13
|
]
|
|
14
14
|
[project.scripts]
|
|
@@ -20,23 +20,23 @@ build-backend = "hatchling.build"
|
|
|
20
20
|
|
|
21
21
|
[dependency-groups]
|
|
22
22
|
dev = [
|
|
23
|
-
"pytest~=
|
|
23
|
+
"pytest~=9.0.2",
|
|
24
24
|
"pytest-xdist~=3.8.0",
|
|
25
|
-
"ruff~=0.
|
|
26
|
-
"pip-audit~=2.
|
|
27
|
-
"bandit~=1.
|
|
28
|
-
"mypy~=1.
|
|
25
|
+
"ruff~=0.15.0",
|
|
26
|
+
"pip-audit~=2.10.0",
|
|
27
|
+
"bandit~=1.9.3",
|
|
28
|
+
"mypy~=1.19.1",
|
|
29
29
|
]
|
|
30
30
|
|
|
31
31
|
[tool.mypy]
|
|
32
|
-
python_version = "3.
|
|
32
|
+
python_version = "3.14"
|
|
33
33
|
warn_no_return = false
|
|
34
34
|
strict = true
|
|
35
35
|
exclude = ["^tests/", "^tmp/"]
|
|
36
36
|
|
|
37
37
|
[tool.ruff]
|
|
38
38
|
line-length = 130
|
|
39
|
-
target-version = "
|
|
39
|
+
target-version = "py314"
|
|
40
40
|
[tool.ruff.lint]
|
|
41
41
|
select = ["ALL"]
|
|
42
42
|
ignore = [
|
|
@@ -39,7 +39,7 @@ class BalanceFetcher:
|
|
|
39
39
|
|
|
40
40
|
async def process(self) -> None:
|
|
41
41
|
with self.progress_bar:
|
|
42
|
-
runner = AsyncTaskRunner(
|
|
42
|
+
runner = AsyncTaskRunner(concurrency=10)
|
|
43
43
|
for network in self.config.networks():
|
|
44
44
|
runner.add(f"process_{network}", self._process_network(network))
|
|
45
45
|
await runner.run()
|
|
@@ -54,7 +54,7 @@ class BalanceFetcher:
|
|
|
54
54
|
return result
|
|
55
55
|
|
|
56
56
|
async def _process_network(self, network: Network) -> None:
|
|
57
|
-
runner = AsyncTaskRunner(
|
|
57
|
+
runner = AsyncTaskRunner(concurrency=self.config.workers[network])
|
|
58
58
|
for idx, task in enumerate(self.tasks[network]):
|
|
59
59
|
runner.add(str(idx), self._get_balance(network, task.wallet_address, task.token_address))
|
|
60
60
|
res = await runner.run()
|
|
@@ -4,13 +4,13 @@ import pkgutil
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from typing import Annotated
|
|
6
6
|
|
|
7
|
-
import mm_print
|
|
8
7
|
import typer
|
|
8
|
+
from mm_print import print_toml
|
|
9
9
|
|
|
10
10
|
from mm_balance import command_runner
|
|
11
11
|
from mm_balance.command_runner import CommandParameters
|
|
12
12
|
from mm_balance.constants import NETWORKS
|
|
13
|
-
from mm_balance.utils import PrintFormat
|
|
13
|
+
from mm_balance.utils import PrintFormat, fatal
|
|
14
14
|
|
|
15
15
|
app = typer.Typer(no_args_is_help=True, pretty_exceptions_enable=False, add_completion=False)
|
|
16
16
|
|
|
@@ -25,8 +25,8 @@ def example_callback(value: bool) -> None:
|
|
|
25
25
|
if value:
|
|
26
26
|
data = pkgutil.get_data(__name__, "config/example.toml")
|
|
27
27
|
if data is None:
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
fatal("Example config not found")
|
|
29
|
+
print_toml(data.decode("utf-8"))
|
|
30
30
|
raise typer.Exit
|
|
31
31
|
|
|
32
32
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import getpass
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
|
|
4
|
-
import mm_print
|
|
5
4
|
from pydantic import BaseModel
|
|
6
5
|
|
|
7
6
|
from mm_balance.balance_fetcher import BalanceFetcher
|
|
@@ -11,7 +10,7 @@ from mm_balance.output.formats import json_format, table_format
|
|
|
11
10
|
from mm_balance.price import Prices, get_prices
|
|
12
11
|
from mm_balance.result import create_balances_result
|
|
13
12
|
from mm_balance.token_decimals import get_token_decimals
|
|
14
|
-
from mm_balance.utils import PrintFormat
|
|
13
|
+
from mm_balance.utils import PrintFormat, fatal
|
|
15
14
|
|
|
16
15
|
|
|
17
16
|
class CommandParameters(BaseModel):
|
|
@@ -63,7 +62,7 @@ async def run(params: CommandParameters) -> None:
|
|
|
63
62
|
elif config.settings.print_format is PrintFormat.JSON:
|
|
64
63
|
json_format.print_result(config, token_decimals, prices, workers, result)
|
|
65
64
|
else:
|
|
66
|
-
|
|
65
|
+
fatal("Unsupported print format")
|
|
67
66
|
|
|
68
67
|
if params.save_balances:
|
|
69
68
|
BalancesDict.from_balances_result(result).save_to_path(params.save_balances)
|
|
@@ -1,16 +1,13 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
1
|
from decimal import Decimal
|
|
4
2
|
from pathlib import Path
|
|
5
3
|
from typing import Annotated, Self
|
|
6
4
|
|
|
7
|
-
import mm_print
|
|
8
5
|
import pydash
|
|
9
6
|
from mm_web3 import ConfigValidators, Web3CliConfig
|
|
10
7
|
from pydantic import BeforeValidator, Field, StringConstraints, model_validator
|
|
11
8
|
|
|
12
9
|
from mm_balance.constants import DEFAULT_NODES, TOKEN_ADDRESS, Network
|
|
13
|
-
from mm_balance.utils import PrintFormat, evaluate_share_expression
|
|
10
|
+
from mm_balance.utils import PrintFormat, evaluate_share_expression, fatal
|
|
14
11
|
|
|
15
12
|
|
|
16
13
|
class Validators(ConfigValidators):
|
|
@@ -62,7 +59,7 @@ class AssetGroup(Web3CliConfig):
|
|
|
62
59
|
if path.is_file():
|
|
63
60
|
result += path.read_text().strip().splitlines()
|
|
64
61
|
else:
|
|
65
|
-
|
|
62
|
+
fatal(f"File with addresses not found: {path}")
|
|
66
63
|
elif line.startswith("group:"):
|
|
67
64
|
group_name = line.removeprefix("group:").strip()
|
|
68
65
|
address_group = next((ag for ag in address_groups if ag.name == group_name), None)
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
1
|
import json
|
|
4
2
|
import re
|
|
5
3
|
from decimal import Decimal
|
|
6
4
|
from pathlib import Path
|
|
7
5
|
|
|
8
|
-
import mm_print
|
|
9
6
|
from deepdiff.diff import DeepDiff
|
|
7
|
+
from mm_print import print_json, print_plain, print_table
|
|
10
8
|
from pydantic import BaseModel, RootModel
|
|
11
9
|
|
|
12
10
|
from mm_balance.result import Balance, BalancesResult
|
|
@@ -21,7 +19,8 @@ class BalancesDict(RootModel[dict[str, dict[str, dict[str, Decimal]]]]): # netw
|
|
|
21
19
|
return set(self.model_dump()[network].keys())
|
|
22
20
|
|
|
23
21
|
def save_to_path(self, balances_file: Path) -> None:
|
|
24
|
-
|
|
22
|
+
with balances_file.open("w") as f:
|
|
23
|
+
json.dump(self.model_dump(), f, default=str, indent=2)
|
|
25
24
|
|
|
26
25
|
@staticmethod
|
|
27
26
|
def from_balances_result(result: BalancesResult) -> BalancesDict:
|
|
@@ -38,7 +37,8 @@ class BalancesDict(RootModel[dict[str, dict[str, dict[str, Decimal]]]]): # netw
|
|
|
38
37
|
|
|
39
38
|
@staticmethod
|
|
40
39
|
def from_file(path: Path) -> BalancesDict:
|
|
41
|
-
|
|
40
|
+
with path.open("r") as f:
|
|
41
|
+
return BalancesDict(json.load(f))
|
|
42
42
|
|
|
43
43
|
|
|
44
44
|
class Diff(BaseModel):
|
|
@@ -72,23 +72,23 @@ class Diff(BaseModel):
|
|
|
72
72
|
and not self.address_removed
|
|
73
73
|
and not self.balance_changed
|
|
74
74
|
):
|
|
75
|
-
|
|
75
|
+
print_plain("Diff: no changes")
|
|
76
76
|
return
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
print_plain("\nDiff")
|
|
79
79
|
|
|
80
80
|
if self.network_added:
|
|
81
|
-
|
|
81
|
+
print_plain(f"networks added: {self.network_added}")
|
|
82
82
|
if self.network_removed:
|
|
83
|
-
|
|
83
|
+
print_plain(f"networks removed: {self.network_removed}")
|
|
84
84
|
if self.ticker_added:
|
|
85
|
-
|
|
85
|
+
print_plain(f"tickers added: {self.ticker_added}")
|
|
86
86
|
if self.ticker_removed:
|
|
87
|
-
|
|
87
|
+
print_plain(f"tickers removed: {self.ticker_removed}")
|
|
88
88
|
if self.address_added:
|
|
89
|
-
|
|
89
|
+
print_plain(f"addresses added: {self.address_added}")
|
|
90
90
|
if self.address_removed:
|
|
91
|
-
|
|
91
|
+
print_plain(f"addresses removed: {self.address_removed}")
|
|
92
92
|
|
|
93
93
|
if self.balance_changed:
|
|
94
94
|
rows = []
|
|
@@ -97,11 +97,11 @@ class Diff(BaseModel):
|
|
|
97
97
|
for address in self.balance_changed[network][ticker]:
|
|
98
98
|
old_value, new_value = self.balance_changed[network][ticker][address]
|
|
99
99
|
rows.append([network, ticker, address, old_value, new_value, new_value - old_value])
|
|
100
|
-
|
|
100
|
+
print_table(["Network", "Ticker", "Address", "Old", "New", "Change"], rows)
|
|
101
101
|
|
|
102
102
|
def _print_json(self) -> None:
|
|
103
|
-
#
|
|
104
|
-
|
|
103
|
+
# print_json(data=self.model_dump(), type_handlers=str) ?? default?
|
|
104
|
+
print_json(data=self.model_dump())
|
|
105
105
|
|
|
106
106
|
@staticmethod
|
|
107
107
|
def calc(balances1: BalancesDict, balances2: BalancesDict) -> Diff:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
from mm_print import print_json
|
|
2
2
|
|
|
3
3
|
from mm_balance.balance_fetcher import BalanceFetcher
|
|
4
4
|
from mm_balance.config import Config
|
|
@@ -27,4 +27,4 @@ def print_result(
|
|
|
27
27
|
if errors:
|
|
28
28
|
data["errors"] = errors
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
print_json(data)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from decimal import Decimal
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
from mm_print import print_table
|
|
4
4
|
|
|
5
5
|
from mm_balance.balance_fetcher import BalanceFetcher
|
|
6
6
|
from mm_balance.config import Config
|
|
@@ -14,18 +14,18 @@ def print_nodes(config: Config) -> None:
|
|
|
14
14
|
rows = []
|
|
15
15
|
for network, nodes in config.nodes.items():
|
|
16
16
|
rows.append([network, "\n".join(nodes)])
|
|
17
|
-
|
|
17
|
+
print_table(["network", "nodes"], rows, title="Nodes")
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
def print_proxy_count(config: Config) -> None:
|
|
21
|
-
|
|
21
|
+
print_table(["count"], [[len(config.settings.proxies)]], title="Proxies")
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
def print_token_decimals(token_decimals: TokenDecimals) -> None:
|
|
25
25
|
rows = []
|
|
26
26
|
for network, decimals in token_decimals.items():
|
|
27
27
|
rows.append([network, decimals])
|
|
28
|
-
|
|
28
|
+
print_table(["network", "decimals"], rows, title="Token Decimals")
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
def print_prices(config: Config, prices: Prices) -> None:
|
|
@@ -35,7 +35,7 @@ def print_prices(config: Config, prices: Prices) -> None:
|
|
|
35
35
|
rows.append(
|
|
36
36
|
[ticker, format_number(price, config.settings.format_number_separator, "$", config.settings.round_ndigits)]
|
|
37
37
|
)
|
|
38
|
-
|
|
38
|
+
print_table(["coin", "usd"], rows, title="Prices")
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
def print_result(config: Config, result: BalancesResult, workers: BalanceFetcher) -> None:
|
|
@@ -56,8 +56,10 @@ def _print_errors(config: Config, workers: BalanceFetcher) -> None:
|
|
|
56
56
|
rows = []
|
|
57
57
|
for task in error_tasks:
|
|
58
58
|
group = config.groups[task.group_index]
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
# task.balance is guaranteed to be non-None ResultErr here since get_errors() filters for is_err()
|
|
60
|
+
if task.balance is not None:
|
|
61
|
+
rows.append([group.ticker + " / " + group.network, task.wallet_address, task.balance.unwrap_err()])
|
|
62
|
+
print_table(["coin", "address", "error"], rows, title="Errors")
|
|
61
63
|
|
|
62
64
|
|
|
63
65
|
def _print_total(config: Config, total: Total, is_share_total: bool) -> None:
|
|
@@ -80,7 +82,7 @@ def _print_total(config: Config, total: Total, is_share_total: bool) -> None:
|
|
|
80
82
|
rows.append(["stablecoin_sum", format_number(total.stablecoin_sum, config.settings.format_number_separator, "$")])
|
|
81
83
|
rows.append(["total_usd_sum", format_number(total.total_usd_sum, config.settings.format_number_separator, "$")])
|
|
82
84
|
|
|
83
|
-
|
|
85
|
+
print_table(headers, rows, title=table_name)
|
|
84
86
|
|
|
85
87
|
|
|
86
88
|
def _print_group(config: Config, group: GroupResult) -> None:
|
|
@@ -118,4 +120,4 @@ def _print_group(config: Config, group: GroupResult) -> None:
|
|
|
118
120
|
table_headers = ["address", "balance"]
|
|
119
121
|
if config.settings.price:
|
|
120
122
|
table_headers += ["usd"]
|
|
121
|
-
|
|
123
|
+
print_table(table_headers, rows, title=group_name)
|
|
@@ -80,6 +80,8 @@ def _create_total(use_share: bool, groups: list[GroupResult]) -> Total:
|
|
|
80
80
|
if total_usd_sum > 0:
|
|
81
81
|
for ticker, usd_value in coin_usd_values.items():
|
|
82
82
|
if ticker in USD_STABLECOINS:
|
|
83
|
+
# Intentionally show total stablecoin share for all stablecoins.
|
|
84
|
+
# We care about combined stablecoin allocation, not individual USDT/USDC breakdown.
|
|
83
85
|
portfolio_share[ticker] = round(stablecoin_sum * 100 / total_usd_sum, 2)
|
|
84
86
|
else:
|
|
85
87
|
portfolio_share[ticker] = round(usd_value * 100 / total_usd_sum, 2)
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import mm_print
|
|
2
|
-
|
|
3
1
|
from mm_balance import rpc
|
|
4
2
|
from mm_balance.config import Config
|
|
5
3
|
from mm_balance.constants import NETWORK_APTOS, NETWORK_BITCOIN, NETWORK_SOLANA, Network
|
|
4
|
+
from mm_balance.utils import fatal
|
|
6
5
|
|
|
7
6
|
|
|
8
7
|
class TokenDecimals(dict[Network, dict[str | None, int]]): # {network: {None: 18}} -- None is for native token, ex. ETH
|
|
@@ -30,7 +29,7 @@ async def get_token_decimals(config: Config) -> TokenDecimals:
|
|
|
30
29
|
elif group.network in (NETWORK_BITCOIN, NETWORK_APTOS):
|
|
31
30
|
result[group.network][None] = 8
|
|
32
31
|
else:
|
|
33
|
-
|
|
32
|
+
fatal(f"Can't get token decimals for native token on network: {group.network}")
|
|
34
33
|
continue
|
|
35
34
|
|
|
36
35
|
# get token_decimals via RPC
|
|
@@ -45,8 +44,8 @@ async def get_token_decimals(config: Config) -> TokenDecimals:
|
|
|
45
44
|
if res.is_err():
|
|
46
45
|
msg = f"can't get decimals for token {group.ticker} / {group.token}, error={res.unwrap_err()}"
|
|
47
46
|
if config.settings.print_debug:
|
|
48
|
-
msg += f"\n{res.
|
|
49
|
-
|
|
47
|
+
msg += f"\n{res.context}"
|
|
48
|
+
fatal(msg)
|
|
50
49
|
|
|
51
50
|
result[group.network][group.token] = res.unwrap()
|
|
52
51
|
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import re
|
|
2
2
|
from decimal import Decimal
|
|
3
3
|
from enum import StrEnum, unique
|
|
4
|
+
from typing import NoReturn
|
|
5
|
+
|
|
6
|
+
import typer
|
|
4
7
|
|
|
5
8
|
|
|
6
9
|
def fnumber(value: Decimal, separator: str, extra: str | None = None) -> str:
|
|
@@ -15,7 +18,7 @@ def fnumber(value: Decimal, separator: str, extra: str | None = None) -> str:
|
|
|
15
18
|
def scale_and_round(value: int, decimals: int, round_ndigits: int) -> Decimal:
|
|
16
19
|
if value == 0:
|
|
17
20
|
return Decimal(0)
|
|
18
|
-
return round(Decimal(value / 10**decimals), round_ndigits)
|
|
21
|
+
return round(Decimal(value) / Decimal(10**decimals), round_ndigits)
|
|
19
22
|
|
|
20
23
|
|
|
21
24
|
def round_decimal(value: Decimal, round_ndigits: int) -> Decimal:
|
|
@@ -161,3 +164,9 @@ class PrintFormat(StrEnum):
|
|
|
161
164
|
PLAIN = "plain"
|
|
162
165
|
TABLE = "table"
|
|
163
166
|
JSON = "json"
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def fatal(message: str) -> NoReturn:
|
|
170
|
+
"""Print an error message and exit with code 1."""
|
|
171
|
+
typer.echo(message)
|
|
172
|
+
raise typer.Exit(1)
|