eth-portfolio 0.5.7__cp312-cp312-win32.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.

Potentially problematic release.


This version of eth-portfolio might be problematic. Click here for more details.

Files changed (83) hide show
  1. eth_portfolio/__init__.py +24 -0
  2. eth_portfolio/_argspec.cp312-win32.pyd +0 -0
  3. eth_portfolio/_argspec.py +43 -0
  4. eth_portfolio/_cache.py +119 -0
  5. eth_portfolio/_config.cp312-win32.pyd +0 -0
  6. eth_portfolio/_config.py +4 -0
  7. eth_portfolio/_db/__init__.py +0 -0
  8. eth_portfolio/_db/decorators.py +147 -0
  9. eth_portfolio/_db/entities.py +311 -0
  10. eth_portfolio/_db/utils.py +616 -0
  11. eth_portfolio/_decimal.py +154 -0
  12. eth_portfolio/_decorators.py +84 -0
  13. eth_portfolio/_exceptions.py +65 -0
  14. eth_portfolio/_ledgers/__init__.py +0 -0
  15. eth_portfolio/_ledgers/address.py +924 -0
  16. eth_portfolio/_ledgers/portfolio.py +328 -0
  17. eth_portfolio/_loaders/__init__.py +33 -0
  18. eth_portfolio/_loaders/_nonce.cp312-win32.pyd +0 -0
  19. eth_portfolio/_loaders/_nonce.py +193 -0
  20. eth_portfolio/_loaders/balances.cp312-win32.pyd +0 -0
  21. eth_portfolio/_loaders/balances.py +95 -0
  22. eth_portfolio/_loaders/token_transfer.py +215 -0
  23. eth_portfolio/_loaders/transaction.py +240 -0
  24. eth_portfolio/_loaders/utils.cp312-win32.pyd +0 -0
  25. eth_portfolio/_loaders/utils.py +67 -0
  26. eth_portfolio/_shitcoins.cp312-win32.pyd +0 -0
  27. eth_portfolio/_shitcoins.py +342 -0
  28. eth_portfolio/_stableish.cp312-win32.pyd +0 -0
  29. eth_portfolio/_stableish.py +42 -0
  30. eth_portfolio/_submodules.py +72 -0
  31. eth_portfolio/_utils.py +215 -0
  32. eth_portfolio/_ydb/__init__.py +0 -0
  33. eth_portfolio/_ydb/token_transfers.py +145 -0
  34. eth_portfolio/address.py +396 -0
  35. eth_portfolio/buckets.py +212 -0
  36. eth_portfolio/constants.cp312-win32.pyd +0 -0
  37. eth_portfolio/constants.py +87 -0
  38. eth_portfolio/portfolio.py +662 -0
  39. eth_portfolio/protocols/__init__.py +64 -0
  40. eth_portfolio/protocols/_base.py +107 -0
  41. eth_portfolio/protocols/convex.py +17 -0
  42. eth_portfolio/protocols/dsr.py +50 -0
  43. eth_portfolio/protocols/lending/README.md +6 -0
  44. eth_portfolio/protocols/lending/__init__.py +50 -0
  45. eth_portfolio/protocols/lending/_base.py +56 -0
  46. eth_portfolio/protocols/lending/compound.py +186 -0
  47. eth_portfolio/protocols/lending/liquity.py +108 -0
  48. eth_portfolio/protocols/lending/maker.py +110 -0
  49. eth_portfolio/protocols/lending/unit.py +44 -0
  50. eth_portfolio/protocols/liquity.py +17 -0
  51. eth_portfolio/py.typed +0 -0
  52. eth_portfolio/structs/__init__.py +43 -0
  53. eth_portfolio/structs/modified.py +69 -0
  54. eth_portfolio/structs/structs.py +626 -0
  55. eth_portfolio/typing/__init__.py +1418 -0
  56. eth_portfolio/typing/balance/single.py +176 -0
  57. eth_portfolio-0.5.7.dist-info/METADATA +26 -0
  58. eth_portfolio-0.5.7.dist-info/RECORD +83 -0
  59. eth_portfolio-0.5.7.dist-info/WHEEL +5 -0
  60. eth_portfolio-0.5.7.dist-info/entry_points.txt +2 -0
  61. eth_portfolio-0.5.7.dist-info/top_level.txt +3 -0
  62. eth_portfolio__mypyc.cp312-win32.pyd +0 -0
  63. eth_portfolio_scripts/__init__.py +17 -0
  64. eth_portfolio_scripts/_args.py +26 -0
  65. eth_portfolio_scripts/_logging.py +14 -0
  66. eth_portfolio_scripts/_portfolio.py +209 -0
  67. eth_portfolio_scripts/_utils.py +106 -0
  68. eth_portfolio_scripts/balances.cp312-win32.pyd +0 -0
  69. eth_portfolio_scripts/balances.py +56 -0
  70. eth_portfolio_scripts/docker/.grafana/dashboards/Portfolio/Balances.json +1962 -0
  71. eth_portfolio_scripts/docker/.grafana/dashboards/dashboards.yaml +10 -0
  72. eth_portfolio_scripts/docker/.grafana/datasources/datasources.yml +11 -0
  73. eth_portfolio_scripts/docker/__init__.cp312-win32.pyd +0 -0
  74. eth_portfolio_scripts/docker/__init__.py +16 -0
  75. eth_portfolio_scripts/docker/check.cp312-win32.pyd +0 -0
  76. eth_portfolio_scripts/docker/check.py +66 -0
  77. eth_portfolio_scripts/docker/docker-compose.yaml +61 -0
  78. eth_portfolio_scripts/docker/docker_compose.cp312-win32.pyd +0 -0
  79. eth_portfolio_scripts/docker/docker_compose.py +97 -0
  80. eth_portfolio_scripts/main.py +118 -0
  81. eth_portfolio_scripts/py.typed +1 -0
  82. eth_portfolio_scripts/victoria/__init__.py +72 -0
  83. eth_portfolio_scripts/victoria/types.py +38 -0
@@ -0,0 +1,176 @@
1
+ from typing import Literal, Union, final
2
+
3
+ from dictstruct import DictStruct
4
+ from eth_typing import BlockNumber, ChecksumAddress
5
+ from mypy_extensions import mypyc_attr
6
+
7
+ from eth_portfolio._decimal import Decimal
8
+
9
+
10
+ @final
11
+ @mypyc_attr(native_class=False)
12
+ class Balance(
13
+ DictStruct, frozen=True, omit_defaults=True, repr_omit_defaults=True, forbid_unknown_fields=True
14
+ ):
15
+ """
16
+ Represents the balance of a single token, including its token amount and equivalent USD value.
17
+
18
+ Example:
19
+ >>> balance1 = Balance(Decimal('100'), Decimal('2000'))
20
+ >>> balance2 = Balance(Decimal('50'), Decimal('1000'))
21
+ >>> combined_balance = balance1 + balance2
22
+ >>> combined_balance.balance
23
+ Decimal('150')
24
+ >>> combined_balance.usd_value
25
+ Decimal('3000')
26
+ """
27
+
28
+ balance: Decimal = Decimal(0)
29
+ """
30
+ The amount of the token.
31
+ """
32
+
33
+ usd_value: Decimal = Decimal(0)
34
+ """
35
+ The USD equivalent value of the token amount.
36
+ """
37
+
38
+ token: ChecksumAddress | None = None
39
+ """
40
+ The token the balance is of, if known.
41
+ """
42
+
43
+ block: BlockNumber | None = None
44
+ """
45
+ The block from which the balance was taken, if known.
46
+ """
47
+
48
+ @property
49
+ def usd(self) -> Decimal:
50
+ """
51
+ An alias for `usd_value`. Returns the USD value of the token amount.
52
+ """
53
+ return self.usd_value
54
+
55
+ def __add__(self, other: "Balance") -> "Balance":
56
+ """
57
+ Adds two :class:`~eth_portfolio.typing.Balance` objects together. It is the user's responsibility to ensure that the two
58
+ :class:`~eth_portfolio.typing.Balance` instances represent the same token.
59
+
60
+ Args:
61
+ other: Another :class:`~eth_portfolio.typing.Balance` object.
62
+
63
+ Returns:
64
+ A new :class:`~eth_portfolio.typing.Balance` object with the summed values.
65
+
66
+ Raises:
67
+ TypeError: If the other object is not a :class:`~eth_portfolio.typing.Balance`.
68
+ Exception: If any other error occurs during addition.
69
+
70
+ Example:
71
+ >>> balance1 = Balance(Decimal('100'), Decimal('2000'))
72
+ >>> balance2 = Balance(Decimal('50'), Decimal('1000'))
73
+ >>> combined_balance = balance1 + balance2
74
+ >>> combined_balance.balance
75
+ Decimal('150')
76
+ >>> combined_balance.usd_value
77
+ Decimal('3000')
78
+ """
79
+ if not isinstance(other, Balance):
80
+ raise TypeError(f"{other} is not a `Balance` object")
81
+ if self.token != other.token:
82
+ raise ValueError(
83
+ f"These Balance objects represent balances of different tokens ({self.token} and {other.token})"
84
+ )
85
+ if self.block != other.block:
86
+ raise ValueError(
87
+ f"These Balance objects represent balances from different blocks ({self.block} and {other.block})"
88
+ )
89
+ try:
90
+ return Balance(
91
+ balance=self.balance + other.balance,
92
+ usd_value=self.usd_value + other.usd_value,
93
+ token=self.token,
94
+ block=self.block,
95
+ )
96
+ except Exception as e:
97
+ e.args = (f"Cannot add {self} and {other}: {e}", *e.args)
98
+ raise
99
+
100
+ def __radd__(self, other: Union["Balance", Literal[0]]) -> "Balance":
101
+ """
102
+ Supports the addition operation from the right side to enable use of `sum`.
103
+
104
+ Args:
105
+ other: Another :class:`~eth_portfolio.typing.Balance` object or zero.
106
+
107
+ Returns:
108
+ A new :class:`~eth_portfolio.typing.Balance` object with the summed values.
109
+
110
+ Raises:
111
+ TypeError: If the other object is not a :class:`~eth_portfolio.typing.Balance`.
112
+ Exception: If any other error occurs during addition.
113
+
114
+ Example:
115
+ >>> balance = Balance(Decimal('100'), Decimal('2000'))
116
+ >>> sum_balance = sum([balance, Balance()])
117
+ >>> sum_balance.balance
118
+ Decimal('100')
119
+ """
120
+ return self if other == 0 else self.__add__(other) # type: ignore
121
+
122
+ def __sub__(self, other: "Balance") -> "Balance":
123
+ """
124
+ Subtracts one :class:`~eth_portfolio.typing.Balance` object from another. It is the user's responsibility to ensure that
125
+ the two :class:`~eth_portfolio.typing.Balance` instances represent the same token.
126
+
127
+ Args:
128
+ other: Another :class:`~eth_portfolio.typing.Balance` object.
129
+
130
+ Returns:
131
+ A new :class:`~eth_portfolio.typing.Balance` object with the subtracted values.
132
+
133
+ Raises:
134
+ TypeError: If the other object is not a :class:`~eth_portfolio.typing.Balance`.
135
+ Exception: If any other error occurs during subtraction.
136
+
137
+ Example:
138
+ >>> balance1 = Balance(Decimal('100'), Decimal('2000'))
139
+ >>> balance2 = Balance(Decimal('50'), Decimal('1000'))
140
+ >>> result_balance = balance1 - balance2
141
+ >>> result_balance.balance
142
+ Decimal('50')
143
+ """
144
+ if not isinstance(other, Balance):
145
+ raise TypeError(f"{other} is not a `Balance` object.")
146
+ if self.token != other.token:
147
+ raise ValueError(
148
+ f"These Balance objects represent balances of different tokens ({self.token} and {other.token})"
149
+ )
150
+ if self.block != other.block:
151
+ raise ValueError(
152
+ f"These Balance objects represent balances from different blocks ({self.block} and {other.block})"
153
+ )
154
+ try:
155
+ return Balance(
156
+ balance=self.balance - other.balance,
157
+ usd_value=self.usd_value - other.usd_value,
158
+ token=self.token,
159
+ block=self.block,
160
+ )
161
+ except Exception as e:
162
+ raise e.__class__(f"Cannot subtract {self} and {other}: {e}") from e
163
+
164
+ def __bool__(self) -> bool:
165
+ """
166
+ Evaluates the truth value of the :class:`~eth_portfolio.typing.Balance` object.
167
+
168
+ Returns:
169
+ True if either the balance or the USD value is non-zero, otherwise False.
170
+
171
+ Example:
172
+ >>> balance = Balance(Decimal('0'), Decimal('0'))
173
+ >>> bool(balance)
174
+ False
175
+ """
176
+ return self.balance != 0 or self.usd_value != 0
@@ -0,0 +1,26 @@
1
+ Metadata-Version: 2.4
2
+ Name: eth_portfolio
3
+ Version: 0.5.7
4
+ Summary: eth-portfolio makes it easy to analyze your portfolio.
5
+ Home-page: https://github.com/BobTheBuidler/eth-portfolio
6
+ Author: BobTheBuidler
7
+ Author-email: bobthebuidlerdefi@gmail.com
8
+ Requires-Python: >=3.10,<3.14
9
+ Requires-Dist: checksum_dict>=2.1.7
10
+ Requires-Dist: dank_mids>=4.20.192
11
+ Requires-Dist: eth-brownie<1.23,>=1.22.0.dev0
12
+ Requires-Dist: eth_retry<1,>=0.3.4
13
+ Requires-Dist: evmspec>=0.4.1
14
+ Requires-Dist: ez-a-sync>=0.33.10
15
+ Requires-Dist: faster-async-lru==2.0.5.3
16
+ Requires-Dist: faster-eth-utils
17
+ Requires-Dist: numpy<3
18
+ Requires-Dist: pandas<3,>=1.4.3
19
+ Requires-Dist: typed-envs>=0.2.3
20
+ Requires-Dist: ypricemagic<5.2,>=5.1.3
21
+ Dynamic: author
22
+ Dynamic: author-email
23
+ Dynamic: home-page
24
+ Dynamic: requires-dist
25
+ Dynamic: requires-python
26
+ Dynamic: summary
@@ -0,0 +1,83 @@
1
+ eth_portfolio__mypyc.cp312-win32.pyd,sha256=bwBx2Xjvu1zbViE8tin9f1nyCSgfg5ly4-s8QbcobRU,267264
2
+ eth_portfolio/__init__.py,sha256=uCSi24HXqtcGiC98YQcWl6oqhjQecnr0ndQnTPOKS4E,523
3
+ eth_portfolio/_argspec.cp312-win32.pyd,sha256=yBlUydU2A48VUKLRf7k2ZgHBNCJGLmlYG7y26HDYR5A,9216
4
+ eth_portfolio/_argspec.py,sha256=311dk4kMWCRegjSYvoG3D8wBUSF4be5xJrRAXare1qE,1646
5
+ eth_portfolio/_cache.py,sha256=i1MlpkJvLYzuI3jqIgg_1GsLpUKt98OQWDKzIgGC6Yk,4847
6
+ eth_portfolio/_config.cp312-win32.pyd,sha256=orLZPbDbGbZIzFSr0DNWxS3tuTStJ5kwi4l_DuzS5aI,9216
7
+ eth_portfolio/_config.py,sha256=BSOvFS4D0oqgVTtBbtW2g2kpL8Mk6xiP0PipfXs_91A,102
8
+ eth_portfolio/_decimal.py,sha256=KDTNEfMQo-Ypr3G7hoRsSFLZY_UL5f_KaHpZZHmGqgw,4968
9
+ eth_portfolio/_decorators.py,sha256=8rjemWjJQ3wuVw7kNcxIfwsL_D_NLKuSamTBBE-fKKs,2875
10
+ eth_portfolio/_exceptions.py,sha256=FySY2tnUpqONsp7HXqIxPWr0MUqNy24kJDYNV0PJKzA,2488
11
+ eth_portfolio/_shitcoins.cp312-win32.pyd,sha256=UVQxvATfqwO_TGRWs-NquhQ_oKWh0YOby_Tg-72dCqQ,9216
12
+ eth_portfolio/_shitcoins.py,sha256=OdYTv6UzBF_jQ308Nv7l2VYdomEbx4f6-Ux3VI4uvd0,18006
13
+ eth_portfolio/_stableish.cp312-win32.pyd,sha256=MuAZLMtNpG5tXszhXRTQS6QxG4BaLmxrszdhnVZmgvI,9216
14
+ eth_portfolio/_stableish.py,sha256=nui4S0BZvFM1i-Gp8SeW8W5DMUgwXMw6wW23qFbPGV0,2058
15
+ eth_portfolio/_submodules.py,sha256=MHM7p7jajEolkaw9yIsbdQKgKLSI8JaUIIo-g0wA3mk,2249
16
+ eth_portfolio/_utils.py,sha256=AldFoIAVNQttP-PnitRLn_ArkCSOo1Qaw0q_8Zn-uko,7816
17
+ eth_portfolio/address.py,sha256=uI1oM7PAEiYnOhsuQNWecD2UkvV7ikXQT8yBG8AhTLY,14497
18
+ eth_portfolio/buckets.py,sha256=jGMtcR1ji5_Czte6rJLT9gXusc6Na6ddmOPaefa27Lg,7775
19
+ eth_portfolio/constants.cp312-win32.pyd,sha256=-KF2tdulRhHvQ6zcTEm4jLyyz7qPcteh0dCr4-tD0uA,9216
20
+ eth_portfolio/constants.py,sha256=IKMVKXjoQ74RRnpK3sEtqbbGmwKlYg0pXzuGVO0R0B8,4063
21
+ eth_portfolio/portfolio.py,sha256=mBrwOwhKfT7dwC3EyPhAFsa4QUsMANd5BFOj3ePgz98,24871
22
+ eth_portfolio/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
+ eth_portfolio/_db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
+ eth_portfolio/_db/decorators.py,sha256=zOz-rFj2kIpxdOxBE55E23U9sz0slCUY87nNvZCdtYE,5334
25
+ eth_portfolio/_db/entities.py,sha256=smj7xBHB4jJNLeWI7IYTvPGKCBcy7pu5ihJ4qAheL-k,10326
26
+ eth_portfolio/_db/utils.py,sha256=wuHU1TJr_EiafNqgJWj7ns_paSY7MALeIzY3S4RAEYo,22307
27
+ eth_portfolio/_ledgers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
+ eth_portfolio/_ledgers/address.py,sha256=-Ez2UYp8-zErrth6o-9DnseLS2LxzcJAKtHxC-if1LQ,33653
29
+ eth_portfolio/_ledgers/portfolio.py,sha256=7wLE-tDWtlBfQpwX6SbTeF_hqQx4lc5yMFesS8oz894,13411
30
+ eth_portfolio/_loaders/__init__.py,sha256=1-fvYUf9nDB-udpp4LQmrrRYa0vqwjOCO9MgiPzqQmk,1735
31
+ eth_portfolio/_loaders/_nonce.cp312-win32.pyd,sha256=jFke9C4CZvEj7ywCST4qP6o69ZzIXI_eodnqrz1jEbg,9216
32
+ eth_portfolio/_loaders/_nonce.py,sha256=rT1WDYFCFNEaoOjhMn3hCKTAb08ndjpM1FI4qU6r6To,6406
33
+ eth_portfolio/_loaders/balances.cp312-win32.pyd,sha256=xX81j61-Txr2-XWn5rbuIi5fLekalfN-Ttnh12WIAU0,9216
34
+ eth_portfolio/_loaders/balances.py,sha256=XTl109mc_0IAe_l6kR2C5cPs4RcElBxGqO7fGztKeUw,3406
35
+ eth_portfolio/_loaders/token_transfer.py,sha256=qaHCL6bQec7IfvZDl6PFUeBNQjqZjE8sMYWIRHtsL7U,8707
36
+ eth_portfolio/_loaders/transaction.py,sha256=h9xMQzOu4jMrTa8T4jAt2gx1mMtuTZFVMmcCodtyzOE,9390
37
+ eth_portfolio/_loaders/utils.cp312-win32.pyd,sha256=3KPz3bSnxxfl0dEuQXHsWIaFhqkNF3-8DKeL3Xd4sAg,9216
38
+ eth_portfolio/_loaders/utils.py,sha256=rvgu3H38WG17KX0lQS1NcH_R_1sCdJE15tgJuBcOPTs,2331
39
+ eth_portfolio/_ydb/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
40
+ eth_portfolio/_ydb/token_transfers.py,sha256=ajcPFzPqhIoWHJyyFo_nwfBJ_RUB04oKaHi0GVj30Aw,5317
41
+ eth_portfolio/protocols/__init__.py,sha256=NG6u79GKUzZcR6SBN6mfmnjdoZ9yJ_jfdebHt5vFxuk,2581
42
+ eth_portfolio/protocols/_base.py,sha256=pVCo7JyZ-jvZe3Tx9tHTz5h3Zc07QgFPJZ0QWPgdRZk,3740
43
+ eth_portfolio/protocols/convex.py,sha256=az8FhxLtXf0WKo25Ke0IDRGF8x1yMlffoCRz9aG2fGE,535
44
+ eth_portfolio/protocols/dsr.py,sha256=v8hrcIctOSA6W_gsTDlxaKUBs51-EkQs8bIlUy2oE8Y,1788
45
+ eth_portfolio/protocols/liquity.py,sha256=80W-o_4JaB2rXIXpCCh66gf-hfNWnJrphuwbGPKNrSo,658
46
+ eth_portfolio/protocols/lending/README.md,sha256=0C941RnyfQwT1DE9bvxwLP-gBTHlQcQysF-75FXHINo,498
47
+ eth_portfolio/protocols/lending/__init__.py,sha256=49__JYku_iE59Z2MdNHeGoZspHQyVXOtkQLWgWt0NUc,1681
48
+ eth_portfolio/protocols/lending/_base.py,sha256=MRZBYBc9UyFbjlDmL5Xv76I7G6ugOye7fI3KW-F6kDM,2276
49
+ eth_portfolio/protocols/lending/compound.py,sha256=3YhJIYaQD19BcrR_hOD76Ra_IJCA-OwjpJyp6es_3qc,7392
50
+ eth_portfolio/protocols/lending/liquity.py,sha256=Kcj_glFd5LNVkuCI35QbmPbDmZRYRecqcrdalGxlwcU,4257
51
+ eth_portfolio/protocols/lending/maker.py,sha256=LLuABFg8dMna2qery4NM7Lh4tQ6x9r4bbnXa9VH7b8U,4392
52
+ eth_portfolio/protocols/lending/unit.py,sha256=9x_npC9ZWCTITWcyu2aJL3EY-_y_9aulJq7QnFo9m-0,1974
53
+ eth_portfolio/structs/__init__.py,sha256=x-9CdKe8XMukp8Kjr_lD49ohrSd0iGEvvsMAHtAPrLI,1486
54
+ eth_portfolio/structs/modified.py,sha256=QIuFh-u5VTHe0oborn3oHAiAGD0wqhUQW7XFJVZu2KI,1853
55
+ eth_portfolio/structs/structs.py,sha256=gJYoRiXxnuX_v5imJp6f5fuG8GKUpANrcJZTH2IiqCM,20334
56
+ eth_portfolio/typing/__init__.py,sha256=ja6XhyzBeInat7tejA-l97poDNJvY6ogXaDh8fIoZqE,58478
57
+ eth_portfolio/typing/balance/single.py,sha256=A0bxQvaJsXy7P2zNAYBwWH8GUOFXl1iY10qnT1vMVao,6485
58
+ eth_portfolio_scripts/__init__.py,sha256=DjzksD_vlJC5UeeEucCPMT3D4oClcK17JhLQoHH_hTU,275
59
+ eth_portfolio_scripts/_args.py,sha256=M33vPkja62XEJeCZAqacNSBCqbzse07wepwyBOtkvVo,682
60
+ eth_portfolio_scripts/_logging.py,sha256=Oj_6t99vRjM3gSIMLzjiKWbOGMkt0W3NhespQ7SYzAk,367
61
+ eth_portfolio_scripts/_portfolio.py,sha256=YUerI8eUnsDvG7rLQUa7foaUOh0VbCGImPedx58O8Jk,7994
62
+ eth_portfolio_scripts/_utils.py,sha256=RmaIorH3vrwm8ltd9-0iJCwwfCeEd2swHcdFnTPKWNA,3164
63
+ eth_portfolio_scripts/balances.cp312-win32.pyd,sha256=dpPAUcG6mx6P7FZCLr68Y2MGT_K-8vYtBKUOGDd-72g,9216
64
+ eth_portfolio_scripts/balances.py,sha256=EKKfBLPbWCpbe-e0vZVzWPsdXQnNq6ppyt4dw55HPQ8,1780
65
+ eth_portfolio_scripts/main.py,sha256=T151lGdqdnkvHyjYyLVNsdXU6rkAvfDUj6lqXZKWzx0,3794
66
+ eth_portfolio_scripts/py.typed,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
67
+ eth_portfolio_scripts/docker/__init__.cp312-win32.pyd,sha256=RUkXjlmo-C3JpwjOiYL2VQpl4z6HMpWpnKedEx9lHso,9216
68
+ eth_portfolio_scripts/docker/__init__.py,sha256=LFmgKAsw1hhGHbKZYLambYvAuj_RANZM_JrHMC2vtRI,409
69
+ eth_portfolio_scripts/docker/check.cp312-win32.pyd,sha256=x66h_zZzKjh5pv_R44dmhR1ENMksXY0WuugUprz_-Vk,9216
70
+ eth_portfolio_scripts/docker/check.py,sha256=_NyFsuqyiqnAIWXzA2g-mJhBFMbiRCqtV3gh_yrpHr0,1972
71
+ eth_portfolio_scripts/docker/docker-compose.yaml,sha256=CO7RzS-PYXi7AeRX1DDA8LQB8HOAPfMIBZW9kE85W2k,1870
72
+ eth_portfolio_scripts/docker/docker_compose.cp312-win32.pyd,sha256=40YYPWnE7NnSFMGQMcTrDhmin46Q4VgQBj7B8FIhT1g,9216
73
+ eth_portfolio_scripts/docker/docker_compose.py,sha256=oOAjTzmWjoBYZ1kav-pDqcOA_yKVyn13LZ366Ga-SUI,2885
74
+ eth_portfolio_scripts/docker/.grafana/dashboards/dashboards.yaml,sha256=wktTI-OAdF_khhbciZFo4Gt2V9bUjbe7GLqwdzTKf0U,212
75
+ eth_portfolio_scripts/docker/.grafana/dashboards/Portfolio/Balances.json,sha256=XGMV8e4tDak53e9bmymwAB4uqZmAIcU5JlRT3OiwTeU,70750
76
+ eth_portfolio_scripts/docker/.grafana/datasources/datasources.yml,sha256=8PPH_QDhfbRRh3IidskW46rifJejloa1a9I1KCw2FTk,199
77
+ eth_portfolio_scripts/victoria/__init__.py,sha256=krFduQKal90rA3TFkP5tWZWcuaLVdBODMczroZwHOlY,2036
78
+ eth_portfolio_scripts/victoria/types.py,sha256=oCJizCdTqgFjnR7krW7sLEVeQGa3MpJO4dd8Om41FNI,732
79
+ eth_portfolio-0.5.7.dist-info/METADATA,sha256=N3dA5VED34QtfuJTOlE2zEYvn0eNpUtp6z1rmpsxtmM,837
80
+ eth_portfolio-0.5.7.dist-info/WHEEL,sha256=LwxTQZ0gyDP_uaeNCLm-ZIktY9hv6x0e22Q-hgFd-po,97
81
+ eth_portfolio-0.5.7.dist-info/entry_points.txt,sha256=yqoC6X3LU1NA_-oJ6mloEYEPNmS-0hPS9OtEwgIeDGU,66
82
+ eth_portfolio-0.5.7.dist-info/top_level.txt,sha256=4MlbY-Yj8oGBGL8piXiO4SOpk2gZFF9ZXVTObTZOzqM,57
83
+ eth_portfolio-0.5.7.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: false
4
+ Tag: cp312-cp312-win32
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ eth-portfolio = eth_portfolio_scripts.main:main
@@ -0,0 +1,3 @@
1
+ eth_portfolio
2
+ eth_portfolio__mypyc
3
+ eth_portfolio_scripts
Binary file
@@ -0,0 +1,17 @@
1
+ from os import environ
2
+
3
+ environ["DANKMIDS_GANACHE_FORK"] = "0"
4
+ environ["DANKMIDS_COLLECT_STATS"] = "0"
5
+
6
+
7
+ from dotenv import load_dotenv
8
+
9
+ load_dotenv()
10
+
11
+
12
+ from eth_portfolio_scripts._logging import logger, setup_logging
13
+
14
+ setup_logging()
15
+
16
+
17
+ __all__ = ["logger"]
@@ -0,0 +1,26 @@
1
+ from argparse import ArgumentParser
2
+
3
+
4
+ def get_arg_parser(description: str) -> ArgumentParser:
5
+ return ArgumentParser(description)
6
+
7
+
8
+ def add_infra_port_args(parser: ArgumentParser) -> None:
9
+ parser.add_argument(
10
+ "--grafana-port",
11
+ type=int,
12
+ help="The port that will be used by grafana",
13
+ default=3000,
14
+ )
15
+ parser.add_argument(
16
+ "--renderer-port",
17
+ type=int,
18
+ help="The port that will be used by grafana",
19
+ default=8091,
20
+ )
21
+ parser.add_argument(
22
+ "--victoria-port",
23
+ type=int,
24
+ help="The port that will be used by victoria metrics",
25
+ default=8428,
26
+ )
@@ -0,0 +1,14 @@
1
+ import warnings
2
+ from logging import INFO, basicConfig, getLogger
3
+
4
+ from brownie.exceptions import BrownieCompilerWarning, BrownieEnvironmentWarning
5
+
6
+ logger = getLogger(__name__)
7
+
8
+
9
+ def setup_logging() -> None:
10
+ basicConfig(level=INFO)
11
+
12
+
13
+ warnings.simplefilter("ignore", BrownieCompilerWarning)
14
+ warnings.simplefilter("ignore", BrownieEnvironmentWarning)
@@ -0,0 +1,209 @@
1
+ import asyncio
2
+ from collections.abc import Awaitable, Callable, Iterator
3
+ from datetime import datetime, timezone
4
+ from logging import getLogger
5
+ from math import floor
6
+ from typing import Final
7
+
8
+ import a_sync
9
+ import eth_retry
10
+ import y
11
+ from a_sync.functools import cached_property_unsafe as cached_property
12
+ from eth_typing import BlockNumber, ChecksumAddress
13
+ from msgspec import ValidationError, json
14
+ from y import ERC20, Network, NonStandardERC20
15
+ from y.constants import CHAINID
16
+ from y.time import NoBlockFound
17
+
18
+ from eth_portfolio import Portfolio
19
+ from eth_portfolio.buckets import get_token_bucket
20
+ from eth_portfolio.portfolio import _DEFAULT_LABEL
21
+ from eth_portfolio.typing import (
22
+ Addresses,
23
+ Balance,
24
+ PortfolioBalances,
25
+ RemoteTokenBalances,
26
+ TokenBalances,
27
+ )
28
+ from eth_portfolio_scripts import victoria
29
+
30
+ NETWORK_LABEL: Final = Network.label(CHAINID)
31
+
32
+ decode: Final = json.decode
33
+
34
+ logger: Final = getLogger("eth_portfolio")
35
+ log_debug: Final = logger.debug
36
+ log_error: Final = logger.error
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
+
51
+
52
+ class ExportablePortfolio(Portfolio):
53
+ """Adds methods to export full portoflio data."""
54
+
55
+ def __init__(
56
+ self,
57
+ addresses: Addresses,
58
+ *,
59
+ start_block: int = 0,
60
+ label: str = _DEFAULT_LABEL,
61
+ concurrency: int = 30,
62
+ load_prices: bool = True,
63
+ get_bucket: Callable[[ChecksumAddress], Awaitable[str]] = None,
64
+ num_workers_transactions: int = 1000,
65
+ asynchronous: bool = False,
66
+ custom_buckets: dict[str, str] | None = None,
67
+ ):
68
+ super().__init__(
69
+ addresses, start_block, label, load_prices, num_workers_transactions, asynchronous
70
+ )
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
87
+
88
+ @cached_property
89
+ def _data_queries(self) -> tuple[str, str]:
90
+ label = self.label.lower().replace(" ", "_")
91
+ return f"{label}_assets", f"{label}_debts"
92
+
93
+ @eth_retry.auto_retry
94
+ @a_sync.Semaphore(16)
95
+ async def data_exists(self, dt: datetime) -> bool:
96
+ # sourcery skip: use-contextlib-suppress
97
+ async for data in a_sync.as_completed(list(self.__get_data_exists_coros(dt)), aiter=True):
98
+ try:
99
+ result = decode(data, type=victoria.types.Response)
100
+ except ValidationError:
101
+ raise victoria.VictoriaMetricsError(data.decode()) from None
102
+ if result.status == "success" and len(result.data.result) > 0:
103
+ print(f"{dt} already loaded")
104
+ return True
105
+ return False
106
+
107
+ async def export_snapshot(self, dt: datetime) -> None:
108
+ log_debug("checking data at %s for %s", dt, self.label)
109
+ try:
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)
116
+ except Exception as e:
117
+ log_error("Error processing %s:", dt, exc_info=True)
118
+
119
+ async def get_data_for_export(self, block: BlockNumber, ts: datetime) -> list[victoria.Metric]:
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):
131
+ metrics_to_export.extend(
132
+ await self.__process_token(ts, section, wallet, token, bals)
133
+ )
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()
146
+
147
+ print(f"got data for {ts} in {datetime.now(tz=timezone.utc) - start}")
148
+ return metrics_to_export
149
+
150
+ def __get_data_exists_coros(self, dt: datetime) -> Iterator[str]:
151
+ for query in self._data_queries:
152
+ yield victoria.get(f"/api/v1/query?query={query}&time={int(dt.timestamp())}")
153
+
154
+ async def __process_token(
155
+ self,
156
+ ts: datetime,
157
+ section: str,
158
+ wallet: ChecksumAddress,
159
+ token: ChecksumAddress,
160
+ bal: Balance,
161
+ protocol: str | None = None,
162
+ ) -> tuple[victoria.types.PrometheusItem, victoria.types.PrometheusItem]:
163
+ # TODO wallet nicknames in grafana
164
+ # wallet = KNOWN_ADDRESSES[wallet] if wallet in KNOWN_ADDRESSES else wallet
165
+ if protocol is not None:
166
+ wallet = f"{protocol} | {wallet}"
167
+
168
+ label_and_section = f"{self.label}_{section}".lower().replace(" ", "_")
169
+ symbol = await _get_symbol(token)
170
+ bucket = await self.get_bucket(token)
171
+ ts_millis = floor(ts.timestamp()) * 1000
172
+
173
+ return (
174
+ victoria.types.PrometheusItem(
175
+ metric=victoria.Metric(
176
+ param="balance",
177
+ wallet=wallet,
178
+ token_address=token,
179
+ token=symbol,
180
+ bucket=bucket,
181
+ network=NETWORK_LABEL,
182
+ __name__=label_and_section,
183
+ ),
184
+ values=[float(bal.balance)],
185
+ timestamps=[ts_millis],
186
+ ),
187
+ victoria.types.PrometheusItem(
188
+ metric=victoria.Metric(
189
+ param="usd value",
190
+ wallet=wallet,
191
+ token_address=token,
192
+ token=symbol,
193
+ bucket=bucket,
194
+ network=NETWORK_LABEL,
195
+ __name__=label_and_section,
196
+ ),
197
+ values=[float(bal.usd)],
198
+ timestamps=[ts_millis],
199
+ ),
200
+ )
201
+
202
+
203
+ async def _get_symbol(token: str) -> str:
204
+ if token == "ETH":
205
+ return "ETH"
206
+ try:
207
+ return await ERC20(token, asynchronous=True).symbol
208
+ except NonStandardERC20:
209
+ return "<NonStandardERC20>"
@@ -0,0 +1,106 @@
1
+ import re
2
+ from asyncio import Task, create_task, sleep
3
+ from collections.abc import AsyncGenerator
4
+ from datetime import datetime, timedelta, timezone
5
+ from typing import Any, Final
6
+
7
+ from brownie import chain
8
+
9
+ timedelta_pattern: Final = re.compile(r"(\d+)([dhms]?)")
10
+
11
+
12
+ def parse_timedelta(value: str) -> timedelta:
13
+ days, hours, minutes, seconds = 0, 0, 0, 0
14
+
15
+ for val, unit in timedelta_pattern.findall(value):
16
+ val = int(val)
17
+ if unit == "d":
18
+ days = val
19
+ elif unit == "h":
20
+ hours = val
21
+ elif unit == "m":
22
+ minutes = val
23
+ elif unit == "s":
24
+ seconds = val
25
+
26
+ return timedelta(days=days, hours=hours, minutes=minutes, seconds=seconds)
27
+
28
+
29
+ async def aiter_timestamps(
30
+ *,
31
+ start: datetime | None = None,
32
+ interval: timedelta = timedelta(days=1),
33
+ run_forever: bool = False,
34
+ ) -> AsyncGenerator[datetime, None]:
35
+ """
36
+ Generates the timestamps to be queried based on the specified range and interval.
37
+ """
38
+ if not isinstance(run_forever, bool):
39
+ raise TypeError(f"`run_forever` must be boolean. You passed {run_forever}")
40
+
41
+ if start is None:
42
+ start = datetime.now(tz=timezone.utc)
43
+
44
+ block0_ts = datetime.fromtimestamp(chain[0].timestamp, tz=timezone.utc)
45
+ helper = datetime(
46
+ year=block0_ts.year,
47
+ month=block0_ts.month,
48
+ day=block0_ts.day,
49
+ hour=block0_ts.hour,
50
+ minute=block0_ts.minute,
51
+ tzinfo=timezone.utc,
52
+ )
53
+ while helper + interval < start:
54
+ helper += interval
55
+ start = helper
56
+ if start < block0_ts:
57
+ start += interval
58
+
59
+ timestamp = start
60
+
61
+ timestamps = []
62
+ while timestamp <= datetime.now(tz=timezone.utc):
63
+ timestamps.append(timestamp)
64
+ timestamp = timestamp + interval
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
+
79
+ while run_forever:
80
+ while timestamp > datetime.now(tz=timezone.utc):
81
+ await _get_waiter(timestamp)
82
+ yield timestamp
83
+ timestamp += interval
84
+
85
+
86
+ _waiters: dict[datetime, "Task[None]"] = {}
87
+
88
+
89
+ def _get_waiter(timestamp: datetime) -> "Task[None]":
90
+ if timestamp not in _waiters:
91
+ waiter = create_task(sleep_until(timestamp))
92
+ waiter.add_done_callback(_sleep_done_callback)
93
+ _waiters[timestamp] = waiter
94
+ return _waiters[timestamp]
95
+
96
+
97
+ async def sleep_until(until: datetime) -> None:
98
+ now = datetime.now(tz=timezone.utc)
99
+ await sleep((until - now).total_seconds())
100
+
101
+
102
+ def _sleep_done_callback(t: "Task[Any]") -> None:
103
+ low_to_hi = sorted(_waiters)
104
+ for k in low_to_hi:
105
+ if _waiters[k] is t:
106
+ _waiters.pop(k)