dao-treasury 0.0.10__cp310-cp310-win32.whl → 0.0.70__cp310-cp310-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.
Files changed (58) hide show
  1. dao_treasury/.grafana/provisioning/dashboards/breakdowns/Expenses.json +551 -0
  2. dao_treasury/.grafana/provisioning/dashboards/breakdowns/Revenue.json +551 -0
  3. dao_treasury/.grafana/provisioning/dashboards/dashboards.yaml +7 -7
  4. dao_treasury/.grafana/provisioning/dashboards/streams/LlamaPay.json +220 -0
  5. dao_treasury/.grafana/provisioning/dashboards/summary/Monthly.json +153 -29
  6. dao_treasury/.grafana/provisioning/dashboards/transactions/Treasury Transactions.json +181 -29
  7. dao_treasury/.grafana/provisioning/dashboards/treasury/Cashflow (Including Unsorted).json +808 -0
  8. dao_treasury/.grafana/provisioning/dashboards/treasury/Cashflow.json +602 -0
  9. dao_treasury/.grafana/provisioning/dashboards/treasury/Current Treasury Assets.json +981 -0
  10. dao_treasury/.grafana/provisioning/dashboards/treasury/Historical Treasury Balances.json +2989 -0
  11. dao_treasury/.grafana/provisioning/dashboards/treasury/Operating Cashflow.json +478 -0
  12. dao_treasury/.grafana/provisioning/datasources/datasources.yaml +17 -0
  13. dao_treasury/ENVIRONMENT_VARIABLES.py +20 -0
  14. dao_treasury/__init__.py +36 -10
  15. dao_treasury/_docker.cp310-win32.pyd +0 -0
  16. dao_treasury/_docker.py +169 -37
  17. dao_treasury/_nicknames.cp310-win32.pyd +0 -0
  18. dao_treasury/_nicknames.py +32 -0
  19. dao_treasury/_wallet.cp310-win32.pyd +0 -0
  20. dao_treasury/_wallet.py +164 -12
  21. dao_treasury/constants.cp310-win32.pyd +0 -0
  22. dao_treasury/constants.py +39 -0
  23. dao_treasury/db.py +925 -150
  24. dao_treasury/docker-compose.yaml +6 -5
  25. dao_treasury/main.py +238 -28
  26. dao_treasury/sorting/__init__.cp310-win32.pyd +0 -0
  27. dao_treasury/sorting/__init__.py +219 -115
  28. dao_treasury/sorting/_matchers.cp310-win32.pyd +0 -0
  29. dao_treasury/sorting/_matchers.py +261 -17
  30. dao_treasury/sorting/_rules.cp310-win32.pyd +0 -0
  31. dao_treasury/sorting/_rules.py +166 -21
  32. dao_treasury/sorting/factory.cp310-win32.pyd +0 -0
  33. dao_treasury/sorting/factory.py +245 -37
  34. dao_treasury/sorting/rule.cp310-win32.pyd +0 -0
  35. dao_treasury/sorting/rule.py +228 -46
  36. dao_treasury/sorting/rules/__init__.cp310-win32.pyd +0 -0
  37. dao_treasury/sorting/rules/__init__.py +1 -0
  38. dao_treasury/sorting/rules/ignore/__init__.cp310-win32.pyd +0 -0
  39. dao_treasury/sorting/rules/ignore/__init__.py +1 -0
  40. dao_treasury/sorting/rules/ignore/llamapay.cp310-win32.pyd +0 -0
  41. dao_treasury/sorting/rules/ignore/llamapay.py +20 -0
  42. dao_treasury/streams/__init__.cp310-win32.pyd +0 -0
  43. dao_treasury/streams/__init__.py +0 -0
  44. dao_treasury/streams/llamapay.cp310-win32.pyd +0 -0
  45. dao_treasury/streams/llamapay.py +388 -0
  46. dao_treasury/treasury.py +118 -25
  47. dao_treasury/types.cp310-win32.pyd +0 -0
  48. dao_treasury/types.py +104 -7
  49. dao_treasury-0.0.70.dist-info/METADATA +134 -0
  50. dao_treasury-0.0.70.dist-info/RECORD +54 -0
  51. dao_treasury-0.0.70.dist-info/top_level.txt +2 -0
  52. dao_treasury__mypyc.cp310-win32.pyd +0 -0
  53. a743a720bbc4482d330e__mypyc.cp310-win32.pyd +0 -0
  54. dao_treasury/.grafana/provisioning/datasources/sqlite.yaml +0 -10
  55. dao_treasury-0.0.10.dist-info/METADATA +0 -36
  56. dao_treasury-0.0.10.dist-info/RECORD +0 -28
  57. dao_treasury-0.0.10.dist-info/top_level.txt +0 -2
  58. {dao_treasury-0.0.10.dist-info → dao_treasury-0.0.70.dist-info}/WHEEL +0 -0
@@ -1,53 +1,148 @@
1
+ # dao_treasury/sorting/rule.py
2
+
3
+ """Module defining transaction sorting rules for the DAO treasury.
4
+
5
+ This module provides the `_SortRule` base class and subclasses for categorizing
6
+ `TreasuryTx` entries based on their attributes or a custom function. When a rule
7
+ is instantiated, it registers itself in the global `SORT_RULES` mapping under its
8
+ class and configures which transaction attributes to match via `_match_all`.
9
+
10
+ Examples:
11
+ # Define a revenue rule for sales (assuming you only transact in DAI for sales)
12
+ >>> from dao_treasury.sorting.rule import RevenueSortRule, SORT_RULES
13
+ >>> RevenueSortRule(
14
+ ... txgroup='Sale',
15
+ ... token_address='0x6B175474E89094d879c81e570a000000000000',
16
+ ... symbol='DAI'
17
+ ... )
18
+ # Inspect rules registered for RevenueSortRule
19
+ >>> len(SORT_RULES[RevenueSortRule])
20
+ 1
21
+
22
+ # Iterate over all ExpenseSortRule instances
23
+ >>> from dao_treasury.sorting.rule import ExpenseSortRule
24
+ >>> for rule in SORT_RULES[ExpenseSortRule]:
25
+ ... print(rule.txgroup)
26
+
27
+ See Also:
28
+ :const:`~dao_treasury.sorting.rule.SORT_RULES`
29
+ :class:`~dao_treasury.sorting.rule._SortRule`
30
+ """
31
+
32
+ from collections import defaultdict
1
33
  from dataclasses import dataclass
2
- from typing import TYPE_CHECKING, ClassVar, Dict, Final, List, Optional
34
+ from logging import getLogger
35
+ from typing import (
36
+ TYPE_CHECKING,
37
+ DefaultDict,
38
+ Dict,
39
+ Final,
40
+ List,
41
+ Optional,
42
+ Type,
43
+ TypeVar,
44
+ )
3
45
 
4
46
  from brownie.convert.datatypes import EthAddress
5
47
  from eth_typing import HexStr
48
+ from mypy_extensions import mypyc_attr
6
49
 
7
50
  from dao_treasury._wallet import TreasuryWallet
8
- from dao_treasury.types import SortFunction, TxGroupDbid, TxGroupName
51
+ from dao_treasury.types import SortFunction, SortRule, TxGroupDbid, TxGroupName
9
52
 
10
53
  if TYPE_CHECKING:
11
54
  from dao_treasury.db import TreasuryTx
12
55
 
13
56
 
14
- SORT_RULES: List["_SortRule"] = []
57
+ logger: Final = getLogger(__name__)
58
+ _log_debug: Final = logger.debug
15
59
 
60
+ SORT_RULES: DefaultDict[Type[SortRule], List[SortRule]] = defaultdict(list)
61
+ """Mapping from sort rule classes to lists of instantiated rules, in creation order per class.
62
+
63
+ Each key is a subclass of :class:`~dao_treasury.types.SortRule` and the corresponding
64
+ value is the list of rule instances of that class.
65
+
66
+ Examples:
67
+ >>> from dao_treasury.sorting.rule import RevenueSortRule, SORT_RULES
68
+ >>> RevenueSortRule(txgroup='Interest', symbol='DAI')
69
+ >>> SORT_RULES[RevenueSortRule][0].txgroup
70
+ 'Revenue:Interest'
71
+ """
16
72
 
17
73
  _match_all: Final[Dict[TxGroupName, List[str]]] = {}
18
- """An internal cache defining a list of which matcher attributes are used for each SortRule"""
74
+ """An internal cache defining which matcher attributes are used for each `txgroup`."""
19
75
 
76
+ _MATCHING_ATTRS: Final = (
77
+ "hash",
78
+ "from_address",
79
+ "from_nickname",
80
+ "to_address",
81
+ "to_nickname",
82
+ "token_address",
83
+ "symbol",
84
+ "log_index",
85
+ )
20
86
 
87
+
88
+ @mypyc_attr(native_class=False)
21
89
  @dataclass(kw_only=True, frozen=True)
22
90
  class _SortRule:
91
+ """Base class for defining transaction matching rules.
92
+
93
+ When instantiated, a rule validates its inputs, determines which transaction
94
+ attributes to match (or uses a custom function), and registers itself
95
+ in the global `SORT_RULES` mapping under its class.
96
+
97
+ Matched transactions are assigned to the specified `txgroup`.
98
+
99
+ See Also:
100
+ :const:`dao_treasury.sorting.rule.SORT_RULES`
101
+ """
102
+
23
103
  txgroup: TxGroupName
104
+ """Name of the transaction group to assign upon match."""
105
+
24
106
  hash: Optional[HexStr] = None
107
+ """Exact transaction hash to match."""
108
+
25
109
  from_address: Optional[EthAddress] = None
110
+ """Source wallet address to match."""
111
+
26
112
  from_nickname: Optional[str] = None
113
+ """Sender nickname (alias) to match."""
114
+
27
115
  to_address: Optional[EthAddress] = None
116
+ """Recipient wallet address to match."""
117
+
28
118
  to_nickname: Optional[str] = None
119
+ """Recipient nickname (alias) to match."""
120
+
29
121
  token_address: Optional[EthAddress] = None
122
+ """Token contract address to match."""
123
+
30
124
  symbol: Optional[str] = None
125
+ """Token symbol to match."""
126
+
31
127
  log_index: Optional[int] = None
32
- func: Optional[SortFunction] = None
128
+ """Log index within the transaction receipt to match."""
33
129
 
34
- __instances__: ClassVar[List["_SortRule"]] = []
35
- __matching_attrs__: ClassVar[List[str]] = [
36
- "hash",
37
- "from_address",
38
- "from_nickname",
39
- "to_address",
40
- "to_nickname",
41
- "token_address",
42
- "symbol",
43
- "log_index",
44
- ]
130
+ func: Optional[SortFunction] = None
131
+ """Custom matching function that takes a `TreasuryTx` and returns a bool or an awaitable that returns a bool."""
45
132
 
46
133
  def __post_init__(self) -> None:
47
- """Validates inputs, checksums addresses, and adds the newly initialized SortRule to __instances__ class var"""
48
-
134
+ """Validate inputs, checksum addresses, and register the rule.
135
+
136
+ - Ensures no duplicate rule exists for the same `txgroup`.
137
+ - Converts address fields to checksummed format.
138
+ - Determines which attributes will be used for direct matching.
139
+ - Validates that exactly one of attribute-based or function-based matching is provided.
140
+ - Registers the instance in :attr:`SORT_RULES` and :data:`_match_all`.
141
+ """
49
142
  if self.txgroup in _match_all:
50
- raise ValueError(f"there is already a matcher defined for txgroup {self.txgroup}: {self}")
143
+ raise ValueError(
144
+ f"there is already a matcher defined for txgroup {self.txgroup}: {self}"
145
+ )
51
146
 
52
147
  # ensure addresses are checksummed if applicable
53
148
  for attr in ["from_address", "to_address", "token_address"]:
@@ -58,105 +153,192 @@ class _SortRule:
58
153
  object.__setattr__(self, attr, checksummed)
59
154
 
60
155
  # define matchers used for this instance
61
- # TODO: maybe import the string matchers and use them here too? They're a lot faster
62
- matchers = [
63
- attr
64
- for attr in self.__matching_attrs__
65
- if getattr(self, attr) is not None
66
- ]
67
-
156
+ matchers = [attr for attr in _MATCHING_ATTRS if getattr(self, attr) is not None]
68
157
  _match_all[self.txgroup] = matchers
69
158
 
70
159
  if self.func is not None and matchers:
71
160
  raise ValueError(
72
161
  "You must specify attributes for matching or pass in a custom matching function, not both."
73
162
  )
74
-
163
+
75
164
  if self.func is None and not matchers:
76
165
  raise ValueError(
77
166
  "You must specify attributes for matching or pass in a custom matching function."
78
167
  )
79
-
168
+
80
169
  if self.func is not None and not callable(self.func):
81
170
  raise TypeError(f"func must be callable. You passed {self.func}")
82
171
 
83
172
  # append new instance to instances classvar
84
- self.__instances__.append(self)
85
-
173
+ # TODO: fix dataclass ClassVar handling in mypyc and reenable
174
+ # self.__instances__.append(self)
175
+
176
+ # append new instance under its class key
177
+ SORT_RULES[type(self)].append(self)
178
+
86
179
  @property
87
180
  def txgroup_dbid(self) -> TxGroupDbid:
181
+ """Compute the database ID for this rule's `txgroup`.
182
+
183
+ Splits the `txgroup` string on ':' and resolves or creates the hierarchical
184
+ `TxGroup` entries in the database, returning the final group ID.
185
+
186
+ See Also:
187
+ :class:`~dao_treasury.db.TxGroup`.
188
+ """
88
189
  from dao_treasury.db import TxGroup
89
190
 
90
191
  txgroup = None
91
- for part in reversed(self.txgroup.split(":")):
192
+ for part in self.txgroup.split(":"):
92
193
  txgroup = TxGroup.get_dbid(part, txgroup)
93
194
  return txgroup
94
195
 
95
196
  async def match(self, tx: "TreasuryTx") -> bool:
96
- """Returns True if `tx` matches this SortRule, False otherwise"""
197
+ """Determine if the given transaction matches this rule.
198
+
199
+ Args:
200
+ tx: A `TreasuryTx` entity to test against this rule.
201
+
202
+ Returns:
203
+ True if the transaction matches the rule criteria; otherwise False.
204
+
205
+ Examples:
206
+ # match by symbol and recipient
207
+ >>> rule = _SortRule(txgroup='Foo', symbol='DAI', to_address='0xabc...')
208
+ >>> await rule.match(tx) # where tx.symbol == 'DAI' and tx.to_address == '0xabc...'
209
+ True
210
+
211
+ See Also:
212
+ :attr:`_match_all`
213
+ """
97
214
  if matchers := _match_all[self.txgroup]:
98
215
  return all(
99
- getattr(self, matcher) == getattr(tx, matcher)
100
- for matcher in matchers
216
+ getattr(tx, matcher) == getattr(self, matcher) for matcher in matchers
101
217
  )
102
218
 
219
+ _log_debug("checking %s for %s", tx, self.func)
103
220
  match = self.func(tx) # type: ignore [misc]
104
221
  return match if isinstance(match, bool) else await match
105
222
 
106
223
 
224
+ @mypyc_attr(native_class=False)
107
225
  class _InboundSortRule(_SortRule):
226
+ """Sort rule that applies only to inbound transactions (to the DAO's wallet).
227
+
228
+ Checks that the transaction's `to_address` belongs to a known `TreasuryWallet`
229
+ before applying the base matching logic.
230
+ """
231
+
108
232
  async def match(self, tx: "TreasuryTx") -> bool:
109
233
  return (
110
234
  tx.to_address is not None
111
- and TreasuryWallet._get_instance(tx.to_address.address) is not None
112
- and await super().match(tx)
235
+ and TreasuryWallet.check_membership(tx.to_address.address, tx.block)
236
+ and await super(_InboundSortRule, self).match(tx)
113
237
  )
114
238
 
239
+
240
+ @mypyc_attr(native_class=False)
115
241
  class _OutboundSortRule(_SortRule):
242
+ """Sort rule that applies only to outbound transactions (from the DAO's wallet).
243
+
244
+ Checks that the transaction's `from_address` belongs to a known `TreasuryWallet`
245
+ before applying the base matching logic.
246
+ """
247
+
116
248
  async def match(self, tx: "TreasuryTx") -> bool:
117
- return (
118
- TreasuryWallet._get_instance(tx.from_address.address) is not None
119
- and await super().match(tx)
120
- )
249
+ return TreasuryWallet.check_membership(
250
+ tx.from_address.address, tx.block
251
+ ) and await super(_OutboundSortRule, self).match(tx)
121
252
 
122
253
 
254
+ @mypyc_attr(native_class=False)
123
255
  class RevenueSortRule(_InboundSortRule):
256
+ """Rule to categorize inbound transactions as revenue.
257
+
258
+ Prepends 'Revenue:' to the `txgroup` name before registration.
259
+
260
+ Examples:
261
+ >>> RevenueSortRule(txgroup='Sale', to_address='0xabc...', symbol='DAI')
262
+ # results in a rule with txgroup 'Revenue:Sale'
263
+ """
264
+
124
265
  def __post_init__(self) -> None:
125
266
  """Prepends `self.txgroup` with 'Revenue:'."""
126
267
  object.__setattr__(self, "txgroup", f"Revenue:{self.txgroup}")
127
- super().__post_init__()
268
+ super(RevenueSortRule, self).__post_init__()
128
269
 
129
270
 
271
+ @mypyc_attr(native_class=False)
130
272
  class CostOfRevenueSortRule(_OutboundSortRule):
273
+ """Rule to categorize outbound transactions as cost of revenue.
274
+
275
+ Prepends 'Cost of Revenue:' to the `txgroup` name before registration.
276
+ """
277
+
131
278
  def __post_init__(self) -> None:
132
279
  """Prepends `self.txgroup` with 'Cost of Revenue:'."""
133
280
  object.__setattr__(self, "txgroup", f"Cost of Revenue:{self.txgroup}")
134
- super().__post_init__()
281
+ super(CostOfRevenueSortRule, self).__post_init__()
135
282
 
136
283
 
284
+ @mypyc_attr(native_class=False)
137
285
  class ExpenseSortRule(_OutboundSortRule):
286
+ """Rule to categorize outbound transactions as expenses.
287
+
288
+ Prepends 'Expenses:' to the `txgroup` name before registration.
289
+ """
290
+
138
291
  def __post_init__(self) -> None:
139
292
  """Prepends `self.txgroup` with 'Expenses:'."""
140
293
  object.__setattr__(self, "txgroup", f"Expenses:{self.txgroup}")
141
- super().__post_init__()
294
+ super(ExpenseSortRule, self).__post_init__()
142
295
 
143
296
 
297
+ @mypyc_attr(native_class=False)
144
298
  class OtherIncomeSortRule(_InboundSortRule):
299
+ """Rule to categorize inbound transactions as other income.
300
+
301
+ Prepends 'Other Income:' to the `txgroup` name before registration.
302
+ """
303
+
145
304
  def __post_init__(self) -> None:
146
305
  """Prepends `self.txgroup` with 'Other Income:'."""
147
306
  object.__setattr__(self, "txgroup", f"Other Income:{self.txgroup}")
148
- super().__post_init__()
307
+ super(OtherIncomeSortRule, self).__post_init__()
149
308
 
150
309
 
310
+ @mypyc_attr(native_class=False)
151
311
  class OtherExpenseSortRule(_OutboundSortRule):
312
+ """Rule to categorize outbound transactions as other expenses.
313
+
314
+ Prepends 'Other Expenses:' to the `txgroup` name before registration.
315
+ """
316
+
152
317
  def __post_init__(self) -> None:
153
318
  """Prepends `self.txgroup` with 'Other Expenses:'."""
154
319
  object.__setattr__(self, "txgroup", f"Other Expenses:{self.txgroup}")
155
- super().__post_init__()
320
+ super(OtherExpenseSortRule, self).__post_init__()
156
321
 
157
322
 
323
+ @mypyc_attr(native_class=False)
158
324
  class IgnoreSortRule(_SortRule):
325
+ """Rule to ignore certain transactions.
326
+
327
+ Prepends 'Ignore:' to the `txgroup` name before registration.
328
+ """
329
+
159
330
  def __post_init__(self) -> None:
160
331
  """Prepends `self.txgroup` with 'Ignore:'."""
161
332
  object.__setattr__(self, "txgroup", f"Ignore:{self.txgroup}")
162
- super().__post_init__()
333
+ super(IgnoreSortRule, self).__post_init__()
334
+
335
+
336
+ TRule = TypeVar(
337
+ "TRule",
338
+ RevenueSortRule,
339
+ CostOfRevenueSortRule,
340
+ ExpenseSortRule,
341
+ OtherIncomeSortRule,
342
+ OtherExpenseSortRule,
343
+ IgnoreSortRule,
344
+ )
@@ -0,0 +1 @@
1
+ from dao_treasury.sorting.rules.ignore import *
@@ -0,0 +1 @@
1
+ from dao_treasury.sorting.rules.ignore.llamapay import *
@@ -0,0 +1,20 @@
1
+ from dao_treasury import TreasuryTx
2
+ from dao_treasury.sorting.factory import ignore
3
+ from dao_treasury.streams import llamapay
4
+
5
+
6
+ @ignore("LlamaPay")
7
+ def is_llamapay_stream_replenishment(tx: TreasuryTx) -> bool:
8
+ if tx.to_address.address in llamapay.factories: # type: ignore [operator]
9
+ # We amortize these streams daily in the `llamapay` module, you'll sort each stream appropriately.
10
+ return True
11
+
12
+ # NOTE: not sure if we want this yet
13
+ # Puling unused funds back from vesting escrow / llamapay
14
+ # elif tx.from_address == "Contract: LlamaPay" and "StreamCancelled" in tx.events:
15
+ # if tx.amount > 0:
16
+ # tx.amount *= -1
17
+ # if tx.value_usd > 0:
18
+ # tx.value_usd *= -1
19
+ # return True
20
+ return False
File without changes