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/db/queries.py CHANGED
@@ -1,11 +1,4 @@
1
- """Canonical SQL queries for brawny database operations.
2
-
3
- All queries use :name placeholder style.
4
- - SQLite: Supports :name natively with dict params
5
- - Postgres: Rewritten to %(name)s in postgres.py
6
-
7
- Dialect-specific queries use dict format: {"postgres": "...", "sqlite": "..."}
8
- """
1
+ """Canonical SQL queries for brawny database operations (SQLite-only)."""
9
2
 
10
3
  from __future__ import annotations
11
4
 
@@ -70,7 +63,11 @@ GET_OLDEST_BLOCK_IN_HISTORY = """
70
63
 
71
64
  GET_JOB = "SELECT * FROM jobs WHERE job_id = :job_id"
72
65
 
73
- GET_ENABLED_JOBS = "SELECT * FROM jobs WHERE enabled = 1 ORDER BY job_id"
66
+ GET_ENABLED_JOBS = (
67
+ "SELECT * FROM jobs WHERE enabled = 1 "
68
+ "AND (drain_until IS NULL OR drain_until <= CURRENT_TIMESTAMP) "
69
+ "ORDER BY job_id"
70
+ )
74
71
 
75
72
  LIST_ALL_JOBS = "SELECT * FROM jobs ORDER BY job_id"
76
73
 
@@ -231,19 +228,11 @@ UPDATE_NONCE_RESERVATION_STATUS_WITH_INTENT = """
231
228
  WHERE chain_id = :chain_id AND signer_address = :address AND nonce = :nonce
232
229
  """
233
230
 
234
- # Dialect-specific: lock signer for nonce reservation
235
- # Postgres uses FOR UPDATE, SQLite doesn't need it (uses BEGIN IMMEDIATE)
236
- LOCK_SIGNER_FOR_UPDATE = {
237
- "postgres": """
238
- SELECT * FROM signers
239
- WHERE chain_id = :chain_id AND signer_address = :address
240
- FOR UPDATE
241
- """,
242
- "sqlite": """
243
- SELECT * FROM signers
244
- WHERE chain_id = :chain_id AND signer_address = :address
245
- """,
246
- }
231
+ # Lock signer for nonce reservation (SQLite uses BEGIN IMMEDIATE in caller).
232
+ LOCK_SIGNER_FOR_UPDATE = """
233
+ SELECT * FROM signers
234
+ WHERE chain_id = :chain_id AND signer_address = :address
235
+ """
247
236
 
248
237
  ENSURE_SIGNER_EXISTS = """
249
238
  INSERT INTO signers (chain_id, signer_address, next_nonce, last_synced_chain_nonce)
@@ -251,21 +240,13 @@ ENSURE_SIGNER_EXISTS = """
251
240
  ON CONFLICT(chain_id, signer_address) DO NOTHING
252
241
  """
253
242
 
254
- # Dialect-specific: cleanup orphaned nonces
255
- CLEANUP_ORPHANED_NONCES = {
256
- "postgres": """
257
- DELETE FROM nonce_reservations
258
- WHERE chain_id = :chain_id
259
- AND status = 'orphaned'
260
- AND updated_at < NOW() - make_interval(hours => :hours)
261
- """,
262
- "sqlite": """
263
- DELETE FROM nonce_reservations
264
- WHERE chain_id = :chain_id
265
- AND status = 'orphaned'
266
- AND updated_at < datetime('now', :hours_offset)
267
- """,
268
- }
243
+ # Cleanup orphaned nonces (SQLite).
244
+ CLEANUP_ORPHANED_NONCES = """
245
+ DELETE FROM nonce_reservations
246
+ WHERE chain_id = :chain_id
247
+ AND status = 'orphaned'
248
+ AND updated_at < datetime('now', :hours_offset)
249
+ """
269
250
 
270
251
  # =============================================================================
271
252
  # Intents
@@ -298,43 +279,24 @@ GET_INTENT_BY_IDEMPOTENCY_KEY = """
298
279
  AND idempotency_key = :idempotency_key
299
280
  """
300
281
 
301
- # Dialect-specific: claim next intent (uses FOR UPDATE SKIP LOCKED on Postgres)
302
- CLAIM_NEXT_INTENT = {
303
- "postgres": """
304
- WITH claimed AS (
305
- SELECT intent_id FROM tx_intents
306
- WHERE status = 'created'
307
- AND (deadline_ts IS NULL OR deadline_ts > CURRENT_TIMESTAMP)
308
- AND (retry_after IS NULL OR retry_after <= CURRENT_TIMESTAMP)
309
- ORDER BY created_at ASC
310
- FOR UPDATE SKIP LOCKED
311
- LIMIT 1
312
- )
313
- UPDATE tx_intents
314
- SET status = 'claimed', claim_token = :claim_token,
315
- claimed_at = CURRENT_TIMESTAMP, claimed_by = :claimed_by,
316
- retry_after = NULL,
317
- updated_at = CURRENT_TIMESTAMP
318
- WHERE intent_id = (SELECT intent_id FROM claimed)
319
- RETURNING *
320
- """,
321
- "sqlite": """
322
- UPDATE tx_intents
323
- SET status = 'claimed', claim_token = :claim_token,
324
- claimed_at = CURRENT_TIMESTAMP, claimed_by = :claimed_by,
325
- retry_after = NULL,
326
- updated_at = CURRENT_TIMESTAMP
327
- WHERE intent_id = (
328
- SELECT intent_id FROM tx_intents
329
- WHERE status = 'created'
330
- AND (deadline_ts IS NULL OR deadline_ts > CURRENT_TIMESTAMP)
331
- AND (retry_after IS NULL OR retry_after <= CURRENT_TIMESTAMP)
332
- ORDER BY created_at ASC
333
- LIMIT 1
334
- )
335
- RETURNING *
336
- """,
337
- }
282
+ # Claim next intent (SQLite).
283
+ CLAIM_NEXT_INTENT = """
284
+ UPDATE tx_intents
285
+ SET status = 'claimed', claim_token = :claim_token,
286
+ claimed_at = CURRENT_TIMESTAMP, claimed_by = :claimed_by,
287
+ lease_expires_at = datetime(CURRENT_TIMESTAMP, :lease_offset),
288
+ retry_after = NULL,
289
+ updated_at = CURRENT_TIMESTAMP
290
+ WHERE intent_id = (
291
+ SELECT intent_id FROM tx_intents
292
+ WHERE status = 'created'
293
+ AND (deadline_ts IS NULL OR deadline_ts > CURRENT_TIMESTAMP)
294
+ AND (retry_after IS NULL OR retry_after <= CURRENT_TIMESTAMP)
295
+ ORDER BY created_at ASC
296
+ LIMIT 1
297
+ )
298
+ RETURNING *
299
+ """
338
300
 
339
301
  UPDATE_INTENT_STATUS = """
340
302
  UPDATE tx_intents
@@ -414,10 +376,12 @@ GET_BACKING_OFF_INTENT_COUNT = """
414
376
  CREATE_ATTEMPT = """
415
377
  INSERT INTO tx_attempts (
416
378
  attempt_id, intent_id, nonce, tx_hash, gas_params_json,
417
- status, broadcast_block, broadcast_at, broadcast_group, endpoint_url
379
+ status, broadcast_block, broadcast_at, broadcast_group, endpoint_url,
380
+ endpoint_binding_id
418
381
  ) VALUES (
419
382
  :attempt_id, :intent_id, :nonce, :tx_hash, :gas_params_json,
420
- :status, :broadcast_block, :broadcast_at, :broadcast_group, :endpoint_url
383
+ :status, :broadcast_block, :broadcast_at, :broadcast_group, :endpoint_url,
384
+ :endpoint_binding_id
421
385
  )
422
386
  RETURNING *
423
387
  """
@@ -562,21 +526,13 @@ LIST_ALL_JOB_LOGS = """
562
526
  LIMIT :limit
563
527
  """
564
528
 
565
- LIST_LATEST_JOB_LOGS = {
566
- "postgres": """
567
- SELECT DISTINCT ON (job_id) *
568
- FROM job_logs
569
- WHERE chain_id = :chain_id
570
- ORDER BY job_id, ts DESC
571
- """,
572
- "sqlite": """
573
- SELECT * FROM job_logs l1
574
- WHERE chain_id = :chain_id
575
- AND ts = (SELECT MAX(ts) FROM job_logs l2
576
- WHERE l2.job_id = l1.job_id AND l2.chain_id = :chain_id)
577
- ORDER BY job_id
578
- """,
579
- }
529
+ LIST_LATEST_JOB_LOGS = """
530
+ SELECT * FROM job_logs l1
531
+ WHERE chain_id = :chain_id
532
+ AND ts = (SELECT MAX(ts) FROM job_logs l2
533
+ WHERE l2.job_id = l1.job_id AND l2.chain_id = :chain_id)
534
+ ORDER BY job_id
535
+ """
580
536
 
581
537
  DELETE_OLD_JOB_LOGS = """
582
538
  DELETE FROM job_logs
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+
3
+ import threading
4
+ from contextlib import contextmanager
5
+ from typing import Any, Iterator
6
+
7
+ from brawny.model.errors import DatabaseError
8
+
9
+
10
+ class SerializedDatabase:
11
+ """Serialize all DB access with a single process-wide lock."""
12
+
13
+ def __init__(self, inner: Any) -> None:
14
+ self._inner = inner
15
+ self._lock = threading.RLock()
16
+ self._closed = False
17
+
18
+ @property
19
+ def dialect(self) -> str:
20
+ return self._inner.dialect
21
+
22
+ def _ensure_open(self) -> None:
23
+ if self._closed:
24
+ raise DatabaseError("Database is closed")
25
+
26
+ def connect(self) -> None:
27
+ self._ensure_open()
28
+ with self._lock:
29
+ self._ensure_open()
30
+ self._inner.connect()
31
+
32
+ def close(self) -> None:
33
+ with self._lock:
34
+ if not self._closed:
35
+ self._inner.close()
36
+ self._closed = True
37
+
38
+ def is_connected(self) -> bool:
39
+ if self._closed:
40
+ return False
41
+ with self._lock:
42
+ return self._inner.is_connected()
43
+
44
+ @contextmanager
45
+ def transaction(self, isolation_level: str | None = None) -> Iterator[None]:
46
+ self._ensure_open()
47
+ with self._lock:
48
+ self._ensure_open()
49
+ with self._inner.transaction(isolation_level):
50
+ yield
51
+
52
+ def __getattr__(self, name: str) -> Any:
53
+ if name.startswith("_"):
54
+ raise AttributeError(name)
55
+ attr = getattr(self._inner, name)
56
+ if not callable(attr):
57
+ return attr
58
+
59
+ def _wrapped(*args: Any, **kwargs: Any) -> Any:
60
+ self._ensure_open()
61
+ with self._lock:
62
+ self._ensure_open()
63
+ return attr(*args, **kwargs)
64
+
65
+ return _wrapped