eth-portfolio-temp 0.0.38.dev0__cp311-cp311-win_amd64.whl → 0.3.0__cp311-cp311-win_amd64.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.
Files changed (46) hide show
  1. eth_portfolio/_argspec.cp311-win_amd64.pyd +0 -0
  2. eth_portfolio/_cache.py +2 -2
  3. eth_portfolio/_config.cp311-win_amd64.pyd +0 -0
  4. eth_portfolio/_db/utils.py +7 -9
  5. eth_portfolio/_decimal.py +11 -10
  6. eth_portfolio/_ledgers/address.py +1 -1
  7. eth_portfolio/_loaders/_nonce.cp311-win_amd64.pyd +0 -0
  8. eth_portfolio/_loaders/_nonce.py +4 -4
  9. eth_portfolio/_loaders/balances.cp311-win_amd64.pyd +0 -0
  10. eth_portfolio/_loaders/token_transfer.py +1 -1
  11. eth_portfolio/_loaders/transaction.py +1 -1
  12. eth_portfolio/_loaders/utils.cp311-win_amd64.pyd +0 -0
  13. eth_portfolio/_loaders/utils.py +1 -1
  14. eth_portfolio/_shitcoins.cp311-win_amd64.pyd +0 -0
  15. eth_portfolio/_shitcoins.py +53 -0
  16. eth_portfolio/_stableish.cp311-win_amd64.pyd +0 -0
  17. eth_portfolio/_utils.py +12 -10
  18. eth_portfolio/_ydb/token_transfers.py +32 -23
  19. eth_portfolio/address.py +2 -1
  20. eth_portfolio/buckets.py +22 -4
  21. eth_portfolio/constants.cp311-win_amd64.pyd +0 -0
  22. eth_portfolio/portfolio.py +1 -1
  23. eth_portfolio/protocols/lending/liquity.py +1 -1
  24. eth_portfolio/protocols/lending/maker.py +13 -14
  25. eth_portfolio/structs/structs.py +2 -2
  26. eth_portfolio/typing/__init__.py +6 -6
  27. eth_portfolio__mypyc.cp311-win_amd64.pyd +0 -0
  28. eth_portfolio_scripts/_portfolio.py +72 -44
  29. eth_portfolio_scripts/_utils.py +20 -6
  30. eth_portfolio_scripts/balances.cp311-win_amd64.pyd +0 -0
  31. eth_portfolio_scripts/balances.py +7 -4
  32. eth_portfolio_scripts/docker/__init__.cp311-win_amd64.pyd +0 -0
  33. eth_portfolio_scripts/docker/check.cp311-win_amd64.pyd +0 -0
  34. eth_portfolio_scripts/docker/check.py +28 -17
  35. eth_portfolio_scripts/docker/docker-compose.yaml +2 -2
  36. eth_portfolio_scripts/docker/docker_compose.cp311-win_amd64.pyd +0 -0
  37. eth_portfolio_scripts/docker/docker_compose.py +38 -18
  38. eth_portfolio_scripts/main.py +6 -0
  39. eth_portfolio_scripts/victoria/__init__.py +3 -0
  40. {eth_portfolio_temp-0.0.38.dev0.dist-info → eth_portfolio_temp-0.3.0.dist-info}/METADATA +8 -7
  41. eth_portfolio_temp-0.3.0.dist-info/RECORD +83 -0
  42. {eth_portfolio_temp-0.0.38.dev0.dist-info → eth_portfolio_temp-0.3.0.dist-info}/top_level.txt +1 -1
  43. 295eace8438df6ec133b__mypyc.cp311-win_amd64.pyd +0 -0
  44. eth_portfolio_temp-0.0.38.dev0.dist-info/RECORD +0 -83
  45. {eth_portfolio_temp-0.0.38.dev0.dist-info → eth_portfolio_temp-0.3.0.dist-info}/WHEEL +0 -0
  46. {eth_portfolio_temp-0.0.38.dev0.dist-info → eth_portfolio_temp-0.3.0.dist-info}/entry_points.txt +0 -0
@@ -1,10 +1,12 @@
1
1
  from asyncio import gather
2
- from typing import List, Optional
2
+ from typing import Final, List, Optional
3
3
 
4
4
  from a_sync import igather
5
- from async_lru import alru_cache
5
+ from brownie import ZERO_ADDRESS
6
6
  from dank_mids.exceptions import Revert
7
7
  from eth_typing import HexStr
8
+ from faster_async_lru import alru_cache
9
+ from faster_eth_abi import encode
8
10
  from y import Contract, Network, contract_creation_block_async, get_price
9
11
  from y._decorators import stuck_coro_debugger
10
12
  from y.constants import dai
@@ -14,18 +16,15 @@ from eth_portfolio._utils import Decimal
14
16
  from eth_portfolio.protocols.lending._base import LendingProtocolWithLockedCollateral
15
17
  from eth_portfolio.typing import Balance, TokenBalances
16
18
 
17
- try:
18
- # this is only available in 4.0.0+
19
- from eth_abi import encode
20
19
 
21
- encode_bytes = lambda bytestring: encode(["bytes32"], [bytestring])
22
- except ImportError:
23
- from eth_abi import encode_single
20
+ yfi: Final = "0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e"
21
+ dai: Contract
22
+ _1e18: Final = Decimal(10**18)
23
+ _1e45: Final = Decimal(10**45)
24
24
 
25
- encode_bytes = lambda bytestring: encode_single("bytes32", bytestring)
26
25
 
27
- yfi = "0x0bc529c00C6401aEF6D220BE8C6Ea1667F6Ad93e"
28
- dai: Contract
26
+ def encode_bytes(bytestring: str) -> bytes:
27
+ return encode(["bytes32"], [bytestring])
29
28
 
30
29
 
31
30
  class Maker(LendingProtocolWithLockedCollateral):
@@ -50,8 +49,8 @@ class Maker(LendingProtocolWithLockedCollateral):
50
49
 
51
50
  balances: TokenBalances = TokenBalances(block=block)
52
51
  for token, data in zip(gems, ink_data):
53
- if ink := data.dict()["ink"]:
54
- balance = ink / Decimal(10**18)
52
+ if token != ZERO_ADDRESS and (ink := data.dict()["ink"]):
53
+ balance = ink / _1e18
55
54
  value = round(balance * Decimal(await get_price(token, block, sync=False)), 18)
56
55
  balances[token] = Balance(balance, value, token=token, block=block)
57
56
  return balances
@@ -75,7 +74,7 @@ class Maker(LendingProtocolWithLockedCollateral):
75
74
  for urns, ilk_info in data:
76
75
  art = urns.dict()["art"]
77
76
  rate = ilk_info.dict()["rate"]
78
- debt = art * rate / Decimal(1e45)
77
+ debt = art * rate / _1e45
79
78
  balances[dai.address] += Balance(debt, debt, token=dai, block=block)
80
79
  return balances
81
80
 
@@ -80,7 +80,7 @@ class _LedgerEntryBase(DictStruct, kw_only=True, frozen=True, omit_defaults=True
80
80
  The USD value of the cryptocurrency transferred in the {cls_name}, if price is known.
81
81
  """
82
82
 
83
- def __init_subclass__(cls, **kwargs):
83
+ def __init_subclass__(cls, **kwargs: Any) -> None:
84
84
  super().__init_subclass__(**kwargs)
85
85
 
86
86
  # Replace {cls_name} in attribute-level docstrings
@@ -273,7 +273,7 @@ class _TransactionBase(
273
273
  return self.transaction.yParity
274
274
 
275
275
  @property
276
- def __db_primary_key__(self):
276
+ def __db_primary_key__(self) -> Dict[str, tuple[int, Address] | int]:
277
277
  return {"from_address": (chain.id, self.from_address), "nonce": self.nonce}
278
278
 
279
279
 
@@ -43,10 +43,10 @@ from typing import (
43
43
  )
44
44
 
45
45
  from checksum_dict import DefaultChecksumDict
46
- from eth_typing import BlockNumber
46
+ from eth_typing import BlockNumber, HexAddress
47
47
  from pandas import DataFrame, concat
48
48
  from typing_extensions import ParamSpec, Self
49
- from y import ERC20
49
+ from y import Contract, ERC20
50
50
  from y.datatypes import Address
51
51
 
52
52
  from eth_portfolio._decimal import Decimal
@@ -159,10 +159,10 @@ class TokenBalances(DefaultChecksumDict[Balance], _SummableNonNumericMixin): #
159
159
  raise
160
160
  self[token.address] += balance
161
161
 
162
- def __getitem__(self, key) -> Balance:
162
+ def __getitem__(self, key: HexAddress) -> Balance:
163
163
  return super().__getitem__(key) if key in self else Balance(token=key, block=self.block)
164
164
 
165
- def __setitem__(self, key, value):
165
+ def __setitem__(self, key: HexAddress, value: Balance) -> None:
166
166
  """
167
167
  Sets the balance for a given token address.
168
168
 
@@ -393,7 +393,7 @@ class RemoteTokenBalances(DefaultDict[ProtocolLabel, TokenBalances], _SummableNo
393
393
  )
394
394
  self[remote] += token_balances # type: ignore [has-type]
395
395
 
396
- def __setitem__(self, protocol: str, value: TokenBalances):
396
+ def __setitem__(self, protocol: str, value: TokenBalances) -> None:
397
397
  """
398
398
  Sets the token balances for a given protocol.
399
399
 
@@ -911,7 +911,7 @@ class PortfolioBalances(DefaultChecksumDict[WalletBalances], _SummableNonNumeric
911
911
  )
912
912
  self[wallet] += balances
913
913
 
914
- def __setitem__(self, key, value):
914
+ def __setitem__(self, key: HexAddress, value: WalletBalances) -> None:
915
915
  if not isinstance(value, WalletBalances):
916
916
  raise TypeError(
917
917
  f"value must be a `WalletBalances` object. You passed {value}"
Binary file
@@ -1,14 +1,16 @@
1
+ import asyncio
1
2
  from datetime import datetime, timezone
2
3
  from logging import getLogger
3
4
  from math import floor
4
- from typing import Awaitable, Callable, Final, Iterator, List, Optional, Tuple
5
+ from typing import Awaitable, Callable, Final, Iterator, List, Optional, Tuple, Dict
5
6
 
6
7
  import a_sync
7
8
  import eth_retry
9
+ import y
8
10
  from a_sync.functools import cached_property_unsafe as cached_property
9
11
  from eth_typing import BlockNumber, ChecksumAddress
10
12
  from msgspec import ValidationError, json
11
- from y import ERC20, Network, NonStandardERC20, get_block_at_timestamp
13
+ from y import ERC20, Network, NonStandardERC20
12
14
  from y.constants import CHAINID
13
15
  from y.time import NoBlockFound
14
16
 
@@ -33,6 +35,19 @@ logger: Final = getLogger("eth_portfolio")
33
35
  log_debug: Final = logger.debug
34
36
  log_error: Final = logger.error
35
37
 
38
+ _block_at_timestamp_semaphore: Final = a_sync.Semaphore(
39
+ 50, name="eth-portfolio get_block_at_timestamp"
40
+ )
41
+
42
+
43
+ async def get_block_at_timestamp(dt: datetime) -> BlockNumber:
44
+ async with _block_at_timestamp_semaphore:
45
+ while True:
46
+ try:
47
+ return await y.get_block_at_timestamp(dt, sync=False)
48
+ except NoBlockFound:
49
+ await asyncio.sleep(10)
50
+
36
51
 
37
52
  class ExportablePortfolio(Portfolio):
38
53
  """Adds methods to export full portoflio data."""
@@ -40,17 +55,35 @@ class ExportablePortfolio(Portfolio):
40
55
  def __init__(
41
56
  self,
42
57
  addresses: Addresses,
58
+ *,
43
59
  start_block: int = 0,
44
60
  label: str = _DEFAULT_LABEL,
61
+ concurrency: int = 40,
45
62
  load_prices: bool = True,
46
- get_bucket: Callable[[ChecksumAddress], Awaitable[str]] = get_token_bucket,
63
+ get_bucket: Callable[[ChecksumAddress], Awaitable[str]] = None,
47
64
  num_workers_transactions: int = 1000,
48
65
  asynchronous: bool = False,
66
+ custom_buckets: Optional[Dict[str, str]] = None,
49
67
  ):
50
68
  super().__init__(
51
69
  addresses, start_block, label, load_prices, num_workers_transactions, asynchronous
52
70
  )
53
- self.get_bucket = get_bucket
71
+ self._semaphore = a_sync.Semaphore(concurrency)
72
+
73
+ # Lowercase all keys in custom_buckets if provided
74
+ self.custom_buckets = (
75
+ {k.lower(): v for k, v in custom_buckets.items()} if custom_buckets else None
76
+ )
77
+
78
+ # If get_bucket is not provided, use get_token_bucket with the lowercased mapping
79
+ if get_bucket is None:
80
+ self.get_bucket = lambda token: get_token_bucket(token, self.custom_buckets)
81
+ elif custom_buckets:
82
+ raise RuntimeError(
83
+ "You cannot pass in a custom get_bucket function AND a custom_buckets mapping, choose one."
84
+ )
85
+ else:
86
+ self.get_bucket = get_bucket
54
87
 
55
88
  @cached_property
56
89
  def _data_queries(self) -> Tuple[str, str]:
@@ -71,53 +104,48 @@ class ExportablePortfolio(Portfolio):
71
104
  return True
72
105
  return False
73
106
 
74
- async def export_snapshot(self, dt: datetime):
107
+ async def export_snapshot(self, dt: datetime) -> None:
75
108
  log_debug("checking data at %s for %s", dt, self.label)
76
109
  try:
77
- if not await self.data_exists(dt, sync=False):
78
- while True:
79
- try:
80
- block = await get_block_at_timestamp(dt, sync=False)
81
- except NoBlockFound:
82
- pass
83
- else:
84
- break
85
- log_debug("block at %s: %s", dt, block)
86
- data = await self.get_data_for_export(block, dt, sync=False)
87
- await victoria.post_data(data)
110
+ if await self.data_exists(dt, sync=False):
111
+ return
112
+ block = await get_block_at_timestamp(dt)
113
+ log_debug("block at %s: %s", dt, block)
114
+ data = await self.get_data_for_export(block, dt, sync=False)
115
+ await victoria.post_data(data)
88
116
  except Exception as e:
89
117
  log_error("Error processing %s:", dt, exc_info=True)
90
118
 
91
- @a_sync.Semaphore(60)
92
119
  async def get_data_for_export(self, block: BlockNumber, ts: datetime) -> List[victoria.Metric]:
93
- print(f"exporting {ts} for {self.label}")
94
- start = datetime.now(tz=timezone.utc)
95
-
96
- metrics_to_export = []
97
- data: PortfolioBalances = await self.describe(block, sync=False)
98
-
99
- for wallet, wallet_data in dict.items(data):
100
- for section, section_data in wallet_data.items():
101
- if isinstance(section_data, TokenBalances):
102
- for token, bals in dict.items(section_data):
103
- metrics_to_export.extend(
104
- await self.__process_token(ts, section, wallet, token, bals)
105
- )
106
- elif isinstance(section_data, RemoteTokenBalances):
107
- if section == "external":
108
- section = "assets"
109
- for protocol, token_bals in section_data.items():
110
- for token, bals in dict.items(token_bals):
120
+ async with self._semaphore:
121
+ print(f"exporting {ts} for {self.label}")
122
+ start = datetime.now(tz=timezone.utc)
123
+
124
+ metrics_to_export = []
125
+ data: PortfolioBalances = await self.describe(block, sync=False)
126
+
127
+ for wallet, wallet_data in dict.items(data):
128
+ for section, section_data in wallet_data.items():
129
+ if isinstance(section_data, TokenBalances):
130
+ for token, bals in dict.items(section_data):
111
131
  metrics_to_export.extend(
112
- await self.__process_token(
113
- ts, section, wallet, token, bals, protocol=protocol
114
- )
132
+ await self.__process_token(ts, section, wallet, token, bals)
115
133
  )
116
- else:
117
- raise NotImplementedError()
134
+ elif isinstance(section_data, RemoteTokenBalances):
135
+ if section == "external":
136
+ section = "assets"
137
+ for protocol, token_bals in section_data.items():
138
+ for token, bals in dict.items(token_bals):
139
+ metrics_to_export.extend(
140
+ await self.__process_token(
141
+ ts, section, wallet, token, bals, protocol=protocol
142
+ )
143
+ )
144
+ else:
145
+ raise NotImplementedError()
118
146
 
119
- print(f"got data for {ts} in {datetime.now(tz=timezone.utc) - start}")
120
- return metrics_to_export
147
+ print(f"got data for {ts} in {datetime.now(tz=timezone.utc) - start}")
148
+ return metrics_to_export
121
149
 
122
150
  def __get_data_exists_coros(self, dt: datetime) -> Iterator[str]:
123
151
  for query in self._data_queries:
@@ -131,7 +159,7 @@ class ExportablePortfolio(Portfolio):
131
159
  token: ChecksumAddress,
132
160
  bal: Balance,
133
161
  protocol: Optional[str] = None,
134
- ):
162
+ ) -> Tuple[victoria.types.PrometheusItem, victoria.types.PrometheusItem]:
135
163
  # TODO wallet nicknames in grafana
136
164
  # wallet = KNOWN_ADDRESSES[wallet] if wallet in KNOWN_ADDRESSES else wallet
137
165
  if protocol is not None:
@@ -172,7 +200,7 @@ class ExportablePortfolio(Portfolio):
172
200
  )
173
201
 
174
202
 
175
- async def _get_symbol(token) -> str:
203
+ async def _get_symbol(token: str) -> str:
176
204
  if token == "ETH":
177
205
  return "ETH"
178
206
  try:
@@ -1,18 +1,18 @@
1
1
  import re
2
2
  from asyncio import Task, create_task, sleep
3
3
  from datetime import datetime, timedelta, timezone
4
- from typing import Any, AsyncGenerator, Dict, List, Optional
4
+ from typing import Any, AsyncGenerator, Dict, Final, List, Optional
5
5
 
6
6
  from brownie import chain
7
7
 
8
8
 
9
- def parse_timedelta(value: str) -> timedelta:
10
- regex = re.compile(r"(\d+)([dhms]?)")
11
- result = regex.findall(value)
9
+ timedelta_pattern: Final = re.compile(r"(\d+)([dhms]?)")
10
+
12
11
 
12
+ def parse_timedelta(value: str) -> timedelta:
13
13
  days, hours, minutes, seconds = 0, 0, 0, 0
14
14
 
15
- for val, unit in result:
15
+ for val, unit in timedelta_pattern.findall(value):
16
16
  val = int(val)
17
17
  if unit == "d":
18
18
  days = val
@@ -58,10 +58,24 @@ async def aiter_timestamps(
58
58
 
59
59
  timestamp = start
60
60
 
61
+ timestamps = []
61
62
  while timestamp <= datetime.now(tz=timezone.utc):
62
- yield timestamp
63
+ timestamps.append(timestamp)
63
64
  timestamp = timestamp + interval
64
65
 
66
+ # cycle between yielding earliest, latest, and middle from `timestamps` until complete
67
+ while timestamps:
68
+ # yield the earliest timestamp
69
+ yield timestamps.pop(0)
70
+ # yield the most recent timestamp if there is one
71
+ if timestamps:
72
+ yield timestamps.pop(-1)
73
+ # yield the most middle timestamp if there is one
74
+ if timestamps:
75
+ yield timestamps.pop(len(timestamps) // 2)
76
+
77
+ del timestamps
78
+
65
79
  while run_forever:
66
80
  while timestamp > datetime.now(tz=timezone.utc):
67
81
  await _get_waiter(timestamp)
@@ -26,13 +26,16 @@ async def export_balances(args: Namespace) -> None:
26
26
 
27
27
  interval = parse_timedelta(args.interval)
28
28
  portfolio = ExportablePortfolio(
29
- args.wallet, label=args.label, start_block=args.first_tx_block, load_prices=False
29
+ args.wallet,
30
+ label=args.label,
31
+ start_block=args.first_tx_block,
32
+ concurrency=args.concurrency,
33
+ load_prices=False,
30
34
  )
31
35
 
32
36
  if export_start_block := args.export_start_block or args.first_tx_block:
33
- start = datetime.fromtimestamp(
34
- await dank_mids.eth.get_block_timestamp(args.export_start_block), tz=timezone.utc
35
- )
37
+ start_ts = await dank_mids.eth.get_block_timestamp(export_start_block)
38
+ start = datetime.fromtimestamp(start_ts, tz=timezone.utc)
36
39
  else:
37
40
  start = None
38
41
 
@@ -1,5 +1,6 @@
1
1
  from functools import lru_cache
2
2
  from subprocess import CalledProcessError, check_output
3
+ from typing import List
3
4
 
4
5
 
5
6
  def check_docker() -> None:
@@ -9,48 +10,58 @@ def check_docker() -> None:
9
10
  Raises:
10
11
  RuntimeError: If docker is not installed.
11
12
  """
13
+ print(" 🔍 checking your computer for docker")
12
14
  try:
13
15
  check_output(["docker", "--version"])
14
- print("docker found!")
15
16
  except (CalledProcessError, FileNotFoundError):
16
- print("checking your computer for docker")
17
17
  raise RuntimeError(
18
18
  "Docker is not installed. You must install Docker before using dao-treasury."
19
19
  ) from None
20
+ else:
21
+ print(" ✔️ eth-portfolio found docker!")
20
22
 
21
23
 
22
- def check_docker_compose() -> None:
24
+ def check_docker_compose() -> List[str]:
23
25
  """
24
- Check that docker-compose is installed on the user's system.
26
+ Check that either `docker-compose` or `docker compose` is installed on the user's system.
27
+
28
+ Returns:
29
+ A valid compose command.
25
30
 
26
31
  Raises:
27
32
  RuntimeError: If docker-compose is not installed.
28
33
  """
29
- try:
30
- check_output(["docker-compose", "--version"])
31
- print("docker-compose found!")
32
- except (CalledProcessError, FileNotFoundError):
33
- print("checking your computer for docker-compose")
34
+ for cmd in ["docker-compose", "docker compose"]:
35
+ print(f" 🔍 checking your computer for {cmd}")
36
+
34
37
  try:
35
- check_output(["docker", "compose", "--version"])
36
- print("docker compose found!")
38
+ check_output([*cmd.split(" "), "--version"])
37
39
  except (CalledProcessError, FileNotFoundError):
38
- print("docker-compose not found, checking your computer for docker compose")
39
- raise RuntimeError(
40
- "Docker Compose is not installed. You must install Docker Compose before using dao-treasury."
41
- ) from None
40
+ print(f" ❌ {cmd} not found")
41
+ continue
42
+ else:
43
+ print(f" ✔️ eth-portfolio found {cmd}!")
44
+ return cmd.split(" ")
45
+
46
+ raise RuntimeError(
47
+ "Docker Compose is not installed. You must install Docker Compose before using dao-treasury."
48
+ ) from None
42
49
 
43
50
 
44
51
  @lru_cache(maxsize=None)
45
- def check_system() -> None:
52
+ def check_system() -> List[str]:
46
53
  """
47
54
  Check that docker and docker-compose is installed on the user's system.
48
55
 
56
+ Returns:
57
+ A valid compose command.
58
+
49
59
  Raises:
50
60
  RuntimeError: If docker-compose is not installed.
51
61
  """
62
+ print("eth-portfolio is checking for the required docker dependencies...")
52
63
  check_docker()
53
- check_docker_compose()
64
+ return check_docker_compose()
54
65
 
55
66
 
56
67
  __all__ = ["check_docker", "check_docker_compose", "check_system"]
@@ -3,7 +3,7 @@ networks:
3
3
 
4
4
  services:
5
5
  grafana:
6
- image: grafana/grafana:10.2.0
6
+ image: grafana/grafana:12.2.1
7
7
  ports:
8
8
  - 127.0.0.1:${GRAFANA_PORT:-3000}:3000
9
9
  environment:
@@ -47,7 +47,7 @@ services:
47
47
  restart: always
48
48
 
49
49
  victoria-metrics:
50
- image: victoriametrics/victoria-metrics:v1.81.1
50
+ image: victoriametrics/victoria-metrics:v1.129.1
51
51
  volumes:
52
52
  - ~/.eth-portfolio/data/victoria/:/victoria-metrics-data
53
53
  command:
@@ -3,7 +3,7 @@ from functools import wraps
3
3
  from importlib import resources
4
4
  from os import path
5
5
  from subprocess import CalledProcessError, check_output
6
- from typing import Callable, Final, Iterable, List, Tuple, TypeVar
6
+ from typing import Callable, Final, Iterable, List, Literal, Tuple, TypeVar
7
7
 
8
8
  from typing_extensions import ParamSpec
9
9
 
@@ -12,33 +12,33 @@ from eth_portfolio_scripts.docker.check import check_system
12
12
 
13
13
  logger: Final = logging.getLogger(__name__)
14
14
 
15
- compose_file: Final = str(
15
+ COMPOSE_FILE: Final = str(
16
16
  resources.files("eth_portfolio_scripts").joinpath("docker/docker-compose.yaml")
17
17
  )
18
18
 
19
19
 
20
20
  def up(*services: str) -> None:
21
+ """Build and start the specified docker-compose services."""
21
22
  build(*services)
22
- print("starting the infra containers...")
23
+ _print_notice("starting", services)
23
24
  _exec_command(["up", "-d", *services])
24
25
 
25
26
 
26
27
  def down() -> None:
28
+ """Stop all of eth-portfolio's docker-compose services."""
27
29
  _exec_command(["down"])
28
30
 
29
31
 
30
32
  def build(*services: str) -> None:
31
- print("building the grafana containers")
33
+ """Build the specified docker-compose services."""
34
+ _print_notice("building", services)
32
35
  _exec_command(["build", *services])
33
36
 
34
37
 
35
- def stop(container_name: str) -> None:
36
- """
37
- Stop the specified container if it is running.
38
- Defaults to stopping the 'renderer' container.
39
- """
40
- print(f"stopping the {container_name} container...")
41
- _exec_command(["stop", container_name])
38
+ def stop(*services: str) -> None:
39
+ """Stop the specified docker-compose services, if running."""
40
+ _print_notice("stopping", services)
41
+ _exec_command(["stop", *services])
42
42
 
43
43
 
44
44
  _P = ParamSpec("_P")
@@ -67,12 +67,32 @@ def ensure_containers(fn: Callable[_P, _T]) -> Callable[_P, _T]:
67
67
  return compose_wrap
68
68
 
69
69
 
70
- def _exec_command(command: List[str], *, compose_options: Tuple[str, ...] = ()) -> None:
71
- check_system()
70
+ def _print_notice(
71
+ doing: Literal["building", "starting", "stopping"],
72
+ services: Tuple[str, ...],
73
+ ) -> None:
74
+ if len(services) == 0:
75
+ print(f"{doing} the backend containers")
76
+ elif len(services) == 1:
77
+ container = services[0]
78
+ print(f"{doing} the {container} container")
79
+ elif len(services) == 2:
80
+ first, second = services
81
+ print(f"{doing} the {first} and {second} containers")
82
+ else:
83
+ *all_but_last, last = services
84
+ print(f"{doing} the {', '.join(all_but_last)}, and {last} containers")
85
+
86
+
87
+ def _exec_command(
88
+ command: List[str],
89
+ *,
90
+ compose_file: str = COMPOSE_FILE,
91
+ compose_options: Tuple[str, ...] = (),
92
+ ) -> None:
93
+ compose = check_system()
94
+ full_command = [*compose, *compose_options, "-f", compose_file, *command]
72
95
  try:
73
- check_output(["docker", "compose", *compose_options, "-f", compose_file, *command])
96
+ check_output(full_command)
74
97
  except (CalledProcessError, FileNotFoundError) as e:
75
- try:
76
- check_output(["docker-compose", *compose_options, "-f", compose_file, *command])
77
- except (CalledProcessError, FileNotFoundError) as _e:
78
- raise RuntimeError(f"Error occurred while running {' '.join(command)}: {_e}") from _e
98
+ raise RuntimeError(f"Error occurred while running `{' '.join(full_command)}`: {e}") from e
@@ -54,6 +54,12 @@ export_parser.add_argument(
54
54
  help="The time interval between datapoints. default: 6h",
55
55
  default="6h",
56
56
  )
57
+ export_parser.add_argument(
58
+ "--concurrency",
59
+ type=int,
60
+ help="The max number of historical blocks to export concurrently. default: 40",
61
+ default=40,
62
+ )
57
63
  export_parser.add_argument(
58
64
  "--first-tx-block",
59
65
  type=int,
@@ -68,3 +68,6 @@ async def post_data(metrics_to_export: List["Metric"]) -> None:
68
68
  def __set_session(sesh: ClientSession) -> None:
69
69
  global session
70
70
  session = sesh
71
+
72
+
73
+ __all__ = ["Metric", "get", "post_data"]
@@ -1,22 +1,23 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: eth_portfolio_temp
3
- Version: 0.0.38.dev0
3
+ Version: 0.3.0
4
4
  Summary: eth-portfolio makes it easy to analyze your portfolio.
5
5
  Home-page: https://github.com/BobTheBuidler/eth-portfolio
6
6
  Author: BobTheBuidler
7
7
  Author-email: bobthebuidlerdefi@gmail.com
8
- Requires-Python: >=3.9,<3.13
8
+ Requires-Python: >=3.10,<3.14
9
9
  Requires-Dist: checksum_dict>=2.1.7
10
10
  Requires-Dist: dank_mids>=4.20.154
11
- Requires-Dist: eth-brownie<1.23,>=1.21.0
11
+ Requires-Dist: eth-brownie<1.23,>=1.22.0.dev0
12
12
  Requires-Dist: eth_retry<1,>=0.3.4
13
13
  Requires-Dist: evmspec>=0.4.1
14
- Requires-Dist: ez-a-sync>=0.32.7
14
+ Requires-Dist: ez-a-sync>=0.32.27
15
+ Requires-Dist: faster-async-lru==2.0.5
15
16
  Requires-Dist: faster-eth-utils
16
- Requires-Dist: numpy<2
17
- Requires-Dist: pandas<1.6,>=1.4.3
17
+ Requires-Dist: numpy<3
18
+ Requires-Dist: pandas<3,>=1.4.3
18
19
  Requires-Dist: typed-envs>=0.0.9
19
- Requires-Dist: ypricemagic<5,>=4.6.17.dev0
20
+ Requires-Dist: ypricemagic<5,>=4.10.0
20
21
  Dynamic: author
21
22
  Dynamic: author-email
22
23
  Dynamic: home-page