brawny 0.1.13__py3-none-any.whl → 0.1.22__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. brawny/__init__.py +2 -0
  2. brawny/_context.py +5 -5
  3. brawny/_rpc/__init__.py +36 -12
  4. brawny/_rpc/broadcast.py +14 -13
  5. brawny/_rpc/caller.py +243 -0
  6. brawny/_rpc/client.py +539 -0
  7. brawny/_rpc/clients.py +11 -11
  8. brawny/_rpc/context.py +23 -0
  9. brawny/_rpc/errors.py +465 -31
  10. brawny/_rpc/gas.py +7 -6
  11. brawny/_rpc/pool.py +18 -0
  12. brawny/_rpc/retry.py +266 -0
  13. brawny/_rpc/retry_policy.py +81 -0
  14. brawny/accounts.py +28 -9
  15. brawny/alerts/__init__.py +15 -18
  16. brawny/alerts/abi_resolver.py +212 -36
  17. brawny/alerts/base.py +2 -2
  18. brawny/alerts/contracts.py +77 -10
  19. brawny/alerts/errors.py +30 -3
  20. brawny/alerts/events.py +38 -5
  21. brawny/alerts/health.py +19 -13
  22. brawny/alerts/send.py +513 -55
  23. brawny/api.py +39 -11
  24. brawny/assets/AGENTS.md +325 -0
  25. brawny/async_runtime.py +48 -0
  26. brawny/chain.py +3 -3
  27. brawny/cli/commands/__init__.py +2 -0
  28. brawny/cli/commands/console.py +69 -19
  29. brawny/cli/commands/contract.py +2 -2
  30. brawny/cli/commands/controls.py +121 -0
  31. brawny/cli/commands/health.py +2 -2
  32. brawny/cli/commands/job_dev.py +6 -5
  33. brawny/cli/commands/jobs.py +99 -2
  34. brawny/cli/commands/maintenance.py +13 -29
  35. brawny/cli/commands/migrate.py +1 -0
  36. brawny/cli/commands/run.py +10 -3
  37. brawny/cli/commands/script.py +8 -3
  38. brawny/cli/commands/signer.py +143 -26
  39. brawny/cli/helpers.py +0 -3
  40. brawny/cli_templates.py +25 -349
  41. brawny/config/__init__.py +4 -1
  42. brawny/config/models.py +43 -57
  43. brawny/config/parser.py +268 -57
  44. brawny/config/validation.py +52 -15
  45. brawny/daemon/context.py +4 -2
  46. brawny/daemon/core.py +185 -63
  47. brawny/daemon/loops.py +166 -98
  48. brawny/daemon/supervisor.py +261 -0
  49. brawny/db/__init__.py +14 -26
  50. brawny/db/base.py +248 -151
  51. brawny/db/global_cache.py +11 -1
  52. brawny/db/migrate.py +175 -28
  53. brawny/db/migrations/001_init.sql +4 -3
  54. brawny/db/migrations/010_add_nonce_gap_index.sql +1 -1
  55. brawny/db/migrations/011_add_job_logs.sql +1 -2
  56. brawny/db/migrations/012_add_claimed_by.sql +2 -2
  57. brawny/db/migrations/013_attempt_unique.sql +10 -0
  58. brawny/db/migrations/014_add_lease_expires_at.sql +5 -0
  59. brawny/db/migrations/015_add_signer_alias.sql +14 -0
  60. brawny/db/migrations/016_runtime_controls_and_quarantine.sql +32 -0
  61. brawny/db/migrations/017_add_job_drain.sql +6 -0
  62. brawny/db/migrations/018_add_nonce_reset_audit.sql +20 -0
  63. brawny/db/migrations/019_add_job_cooldowns.sql +8 -0
  64. brawny/db/migrations/020_attempt_unique_initial.sql +7 -0
  65. brawny/db/ops/__init__.py +3 -25
  66. brawny/db/ops/logs.py +1 -2
  67. brawny/db/queries.py +47 -91
  68. brawny/db/serialized.py +65 -0
  69. brawny/db/sqlite/__init__.py +1001 -0
  70. brawny/db/sqlite/connection.py +231 -0
  71. brawny/db/sqlite/execute.py +116 -0
  72. brawny/db/sqlite/mappers.py +190 -0
  73. brawny/db/sqlite/repos/attempts.py +372 -0
  74. brawny/db/sqlite/repos/block_state.py +102 -0
  75. brawny/db/sqlite/repos/cache.py +104 -0
  76. brawny/db/sqlite/repos/intents.py +1021 -0
  77. brawny/db/sqlite/repos/jobs.py +200 -0
  78. brawny/db/sqlite/repos/maintenance.py +182 -0
  79. brawny/db/sqlite/repos/signers_nonces.py +566 -0
  80. brawny/db/sqlite/tx.py +119 -0
  81. brawny/http.py +194 -0
  82. brawny/invariants.py +11 -24
  83. brawny/jobs/base.py +8 -0
  84. brawny/jobs/job_validation.py +2 -1
  85. brawny/keystore.py +83 -7
  86. brawny/lifecycle.py +64 -12
  87. brawny/logging.py +0 -2
  88. brawny/metrics.py +84 -12
  89. brawny/model/contexts.py +111 -9
  90. brawny/model/enums.py +1 -0
  91. brawny/model/errors.py +18 -0
  92. brawny/model/types.py +47 -131
  93. brawny/network_guard.py +133 -0
  94. brawny/networks/__init__.py +5 -5
  95. brawny/networks/config.py +1 -7
  96. brawny/networks/manager.py +14 -11
  97. brawny/runtime_controls.py +74 -0
  98. brawny/scheduler/poller.py +11 -7
  99. brawny/scheduler/reorg.py +95 -39
  100. brawny/scheduler/runner.py +442 -168
  101. brawny/scheduler/shutdown.py +3 -3
  102. brawny/script_tx.py +3 -3
  103. brawny/telegram.py +53 -7
  104. brawny/testing.py +1 -0
  105. brawny/timeout.py +38 -0
  106. brawny/tx/executor.py +922 -308
  107. brawny/tx/intent.py +54 -16
  108. brawny/tx/monitor.py +31 -12
  109. brawny/tx/nonce.py +212 -90
  110. brawny/tx/replacement.py +69 -18
  111. brawny/tx/retry_policy.py +24 -0
  112. brawny/tx/stages/types.py +75 -0
  113. brawny/types.py +18 -0
  114. brawny/utils.py +41 -0
  115. {brawny-0.1.13.dist-info → brawny-0.1.22.dist-info}/METADATA +3 -3
  116. brawny-0.1.22.dist-info/RECORD +163 -0
  117. brawny/_rpc/manager.py +0 -982
  118. brawny/_rpc/selector.py +0 -156
  119. brawny/db/base_new.py +0 -165
  120. brawny/db/mappers.py +0 -182
  121. brawny/db/migrations/008_add_transactions.sql +0 -72
  122. brawny/db/ops/attempts.py +0 -108
  123. brawny/db/ops/blocks.py +0 -83
  124. brawny/db/ops/cache.py +0 -93
  125. brawny/db/ops/intents.py +0 -296
  126. brawny/db/ops/jobs.py +0 -110
  127. brawny/db/ops/nonces.py +0 -322
  128. brawny/db/postgres.py +0 -2535
  129. brawny/db/postgres_new.py +0 -196
  130. brawny/db/sqlite.py +0 -2733
  131. brawny/db/sqlite_new.py +0 -191
  132. brawny-0.1.13.dist-info/RECORD +0 -141
  133. {brawny-0.1.13.dist-info → brawny-0.1.22.dist-info}/WHEEL +0 -0
  134. {brawny-0.1.13.dist-info → brawny-0.1.22.dist-info}/entry_points.txt +0 -0
  135. {brawny-0.1.13.dist-info → brawny-0.1.22.dist-info}/top_level.txt +0 -0
brawny/alerts/events.py CHANGED
@@ -17,11 +17,15 @@ from brawny.alerts.errors import (
17
17
  EventNotFoundError,
18
18
  ReceiptRequiredError,
19
19
  )
20
+ from brawny.logging import get_logger
20
21
 
21
22
  if TYPE_CHECKING:
22
23
  from brawny.jobs.base import TxReceipt
23
24
 
24
25
 
26
+ logger = get_logger(__name__)
27
+
28
+
25
29
  class AttributeDict(dict):
26
30
  """Dictionary that allows attribute-style access to values.
27
31
 
@@ -145,7 +149,16 @@ class EventAccessor:
145
149
  log = LogEntry.from_dict(log_dict)
146
150
 
147
151
  # Filter by contract address
148
- if to_checksum_address(log.address) != self._address:
152
+ try:
153
+ log_address = to_checksum_address(log.address)
154
+ except ValueError:
155
+ logger.debug(
156
+ "events.invalid_log_address",
157
+ address=log.address,
158
+ tx_hash=log.transaction_hash,
159
+ )
160
+ continue
161
+ if log_address != self._address:
149
162
  continue
150
163
 
151
164
  # Skip if no topics (anonymous event)
@@ -168,8 +181,14 @@ class EventAccessor:
168
181
  if event_name not in result:
169
182
  result[event_name] = []
170
183
  result[event_name].append(decoded)
171
- except Exception:
172
- # Skip logs that fail to decode
184
+ except Exception as e:
185
+ logger.debug(
186
+ "events.decode_log_failed",
187
+ event_name=event_name,
188
+ tx_hash=log.transaction_hash,
189
+ log_index=log.log_index,
190
+ error=str(e)[:200],
191
+ )
173
192
  continue
174
193
  break
175
194
 
@@ -213,7 +232,14 @@ class EventAccessor:
213
232
  types = [i["type"] for i in non_indexed_inputs]
214
233
  try:
215
234
  non_indexed_values = list(abi_decode(types, data_bytes))
216
- except Exception:
235
+ except Exception as e:
236
+ logger.debug(
237
+ "events.decode_non_indexed_failed",
238
+ event_name=event_name,
239
+ tx_hash=log.transaction_hash,
240
+ log_index=log.log_index,
241
+ error=str(e)[:200],
242
+ )
217
243
  non_indexed_values = [None] * len(non_indexed_inputs)
218
244
 
219
245
  # Build args dict
@@ -607,7 +633,14 @@ def decode_logs(
607
633
  decoded["_address"] = log.get("address", "")
608
634
  decoded["_name"] = event_abi["name"]
609
635
  decoded_events.append(decoded)
610
- except Exception:
636
+ except Exception as e:
637
+ logger.debug(
638
+ "events.decode_event_failed",
639
+ event_name=event_abi.get("name"),
640
+ tx_hash=log.get("transactionHash"),
641
+ log_index=log.get("logIndex"),
642
+ error=str(e)[:200],
643
+ )
611
644
  continue
612
645
 
613
646
  return EventDict(decoded_events)
brawny/alerts/health.py CHANGED
@@ -7,6 +7,8 @@ import threading
7
7
  from datetime import datetime, timedelta
8
8
  from typing import Any, Callable, Literal
9
9
 
10
+ from cachetools import TTLCache
11
+
10
12
  from brawny.logging import get_logger
11
13
 
12
14
  logger = get_logger(__name__)
@@ -14,9 +16,11 @@ logger = get_logger(__name__)
14
16
  DEFAULT_COOLDOWN_SECONDS = 1800
15
17
  MAX_FIELD_LEN = 200
16
18
 
17
- _last_fired: dict[str, datetime] = {}
18
- _first_seen: dict[str, datetime] = {}
19
- _suppressed_count: dict[str, int] = {}
19
+ # Multi-threaded access - protected by _lock
20
+ # Medium cardinality keys (fingerprint hashes): maxsize=10K, ttl=1h
21
+ _last_fired: TTLCache[str, datetime] = TTLCache(maxsize=10_000, ttl=3600)
22
+ _first_seen: TTLCache[str, datetime] = TTLCache(maxsize=10_000, ttl=3600)
23
+ _suppressed_count: TTLCache[str, int] = TTLCache(maxsize=10_000, ttl=3600)
20
24
  _lock = threading.Lock()
21
25
 
22
26
 
@@ -172,14 +176,16 @@ def health_alert(
172
176
 
173
177
 
174
178
  def cleanup_stale_fingerprints(cooldown_seconds: int = DEFAULT_COOLDOWN_SECONDS) -> int:
175
- """Remove fingerprints older than 2x cooldown. Returns count removed."""
176
- cutoff = datetime.utcnow() - timedelta(seconds=cooldown_seconds * 2)
177
- removed = 0
179
+ """Remove fingerprints older than 2x cooldown. Returns count removed.
180
+
181
+ Note: With TTLCache, stale entries are automatically evicted. This function
182
+ now triggers cache expiration and returns 0 (actual eviction count is not
183
+ tracked by TTLCache). Kept for API compatibility.
184
+ """
178
185
  with _lock:
179
- stale = [fp for fp, ts in _last_fired.items() if ts < cutoff]
180
- for fp in stale:
181
- _last_fired.pop(fp, None)
182
- _first_seen.pop(fp, None)
183
- _suppressed_count.pop(fp, None)
184
- removed += 1
185
- return removed
186
+ # TTLCache automatically evicts expired entries on access
187
+ # Trigger expiration by calling expire() if available, or just access
188
+ _last_fired.expire()
189
+ _first_seen.expire()
190
+ _suppressed_count.expire()
191
+ return 0 # TTLCache doesn't track eviction count