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
@@ -22,7 +22,7 @@ from brawny.model.enums import IntentStatus, NonceStatus
22
22
  if TYPE_CHECKING:
23
23
  from brawny.config import Config
24
24
  from brawny.db.base import Database
25
- from brawny._rpc.manager import RPCManager
25
+ from brawny._rpc.clients import ReadClient
26
26
  from brawny.tx.nonce import NonceManager
27
27
 
28
28
  logger = get_logger(__name__)
@@ -56,7 +56,7 @@ class ShutdownHandler:
56
56
  self,
57
57
  config: Config,
58
58
  db: Database | None = None,
59
- rpc: RPCManager | None = None,
59
+ rpc: ReadClient | None = None,
60
60
  nonce_manager: NonceManager | None = None,
61
61
  ) -> None:
62
62
  """Initialize shutdown handler.
@@ -90,7 +90,7 @@ class ShutdownHandler:
90
90
  """Set database connection."""
91
91
  self._db = db
92
92
 
93
- def set_rpc(self, rpc: RPCManager) -> None:
93
+ def set_rpc(self, rpc: ReadClient) -> None:
94
94
  """Set RPC manager."""
95
95
  self._rpc = rpc
96
96
 
brawny/script_tx.py CHANGED
@@ -15,7 +15,7 @@ from brawny.tx.utils import normalize_tx_dict
15
15
 
16
16
  if TYPE_CHECKING:
17
17
  from brawny.keystore import Keystore
18
- from brawny._rpc.manager import RPCManager
18
+ from brawny._rpc.clients import BroadcastClient
19
19
  from brawny.jobs.base import TxReceipt
20
20
 
21
21
 
@@ -28,7 +28,7 @@ class TransactionBroadcaster:
28
28
 
29
29
  def __init__(
30
30
  self,
31
- rpc: "RPCManager",
31
+ rpc: "BroadcastClient",
32
32
  keystore: "Keystore",
33
33
  chain_id: int,
34
34
  timeout_seconds: int = 120,
@@ -278,7 +278,7 @@ _broadcaster: TransactionBroadcaster | None = None
278
278
 
279
279
 
280
280
  def _init_broadcaster(
281
- rpc: "RPCManager",
281
+ rpc: "BroadcastClient",
282
282
  keystore: "Keystore",
283
283
  chain_id: int,
284
284
  ) -> None:
brawny/telegram.py CHANGED
@@ -40,12 +40,31 @@ from __future__ import annotations
40
40
  import os # Used by _LazyTelegram for TELEGRAM_BOT_TOKEN
41
41
  from typing import Any
42
42
 
43
- import requests
43
+ import httpx
44
44
 
45
45
  from brawny.logging import get_logger
46
+ from brawny.network_guard import allow_network_calls
46
47
 
47
48
  logger = get_logger(__name__)
48
49
 
50
+ # Module-level HTTP client for connection pooling
51
+ _http_client: httpx.Client | None = httpx.Client(timeout=30.0)
52
+
53
+
54
+ def close_http_client() -> None:
55
+ """Close HTTP client on shutdown. Idempotent + safe in partial init."""
56
+ global _http_client
57
+ client, _http_client = _http_client, None
58
+ if client is not None:
59
+ client.close()
60
+
61
+
62
+ def _client() -> httpx.Client:
63
+ """Get HTTP client, fail fast if closed."""
64
+ if _http_client is None:
65
+ raise RuntimeError("HTTP client is closed")
66
+ return _http_client
67
+
49
68
  # Telegram API limits
50
69
  MAX_MESSAGE_LENGTH = 4096
51
70
  TRUNCATION_SUFFIX = "\n...[truncated]"
@@ -74,15 +93,18 @@ class TelegramBot:
74
93
  self,
75
94
  token: str | None = None,
76
95
  timeout: int = 30,
96
+ default_parse_mode: str | None = "Markdown",
77
97
  ) -> None:
78
98
  """Initialize Telegram bot.
79
99
 
80
100
  Args:
81
101
  token: Bot token. Required for API calls.
82
102
  timeout: Request timeout in seconds.
103
+ default_parse_mode: Default parse mode when not explicitly provided.
83
104
  """
84
105
  self._token = token
85
106
  self._timeout = timeout
107
+ self._default_parse_mode = default_parse_mode
86
108
 
87
109
  @property
88
110
  def configured(self) -> bool:
@@ -115,11 +137,11 @@ class TelegramBot:
115
137
  params = {k: v for k, v in params.items() if v is not None}
116
138
 
117
139
  try:
118
- response = requests.post(
119
- f"{self.api_url}/{method}",
120
- json=params,
121
- timeout=self._timeout,
122
- )
140
+ with allow_network_calls(reason="alerts"):
141
+ response = _client().post(
142
+ f"{self.api_url}/{method}",
143
+ json=params,
144
+ )
123
145
  response.raise_for_status()
124
146
  result = response.json()
125
147
 
@@ -134,7 +156,23 @@ class TelegramBot:
134
156
 
135
157
  return result.get("result")
136
158
 
137
- except requests.exceptions.RequestException as e:
159
+ except httpx.TimeoutException:
160
+ # Don't log full URL (contains bot token)
161
+ logger.error(
162
+ "telegram.timeout",
163
+ method=method,
164
+ chat_id=chat_id,
165
+ )
166
+ return None
167
+ except httpx.HTTPStatusError as e:
168
+ logger.error(
169
+ "telegram.http_error",
170
+ method=method,
171
+ chat_id=chat_id,
172
+ status=e.response.status_code,
173
+ )
174
+ return None
175
+ except httpx.RequestError as e:
138
176
  logger.error(
139
177
  "telegram.request_failed",
140
178
  method=method,
@@ -170,6 +208,8 @@ class TelegramBot:
170
208
  Returns:
171
209
  Message object from Telegram API, or None on failure
172
210
  """
211
+ if parse_mode is None:
212
+ parse_mode = self._default_parse_mode
173
213
  return self._request(
174
214
  "sendMessage",
175
215
  chat_id=chat_id,
@@ -204,6 +244,8 @@ class TelegramBot:
204
244
  Returns:
205
245
  Message object or None
206
246
  """
247
+ if parse_mode is None:
248
+ parse_mode = self._default_parse_mode
207
249
  return self._request(
208
250
  "sendPhoto",
209
251
  chat_id=chat_id,
@@ -236,6 +278,8 @@ class TelegramBot:
236
278
  Returns:
237
279
  Message object or None
238
280
  """
281
+ if parse_mode is None:
282
+ parse_mode = self._default_parse_mode
239
283
  return self._request(
240
284
  "sendDocument",
241
285
  chat_id=chat_id,
@@ -268,6 +312,8 @@ class TelegramBot:
268
312
  Returns:
269
313
  Edited message object or None
270
314
  """
315
+ if parse_mode is None:
316
+ parse_mode = self._default_parse_mode
271
317
  return self._request(
272
318
  "editMessageText",
273
319
  chat_id=chat_id,
brawny/testing.py CHANGED
@@ -88,6 +88,7 @@ def job_context(
88
88
  kv=kv or InMemoryJobKVStore(),
89
89
  job_id=job.job_id,
90
90
  rpc=rpc or MagicMock(),
91
+ http=MagicMock(),
91
92
  logger=MagicMock(),
92
93
  contracts=contracts,
93
94
  _db=None,
brawny/timeout.py ADDED
@@ -0,0 +1,38 @@
1
+ """Timeout budget helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from dataclasses import dataclass
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class Deadline:
11
+ """Monotonic deadline with child budgets.
12
+
13
+ Child deadlines may only consume remaining time; they never extend parents.
14
+ """
15
+
16
+ _deadline: float
17
+
18
+ @classmethod
19
+ def from_seconds(cls, seconds: float) -> "Deadline":
20
+ """Create a deadline seconds from now."""
21
+ now = time.monotonic()
22
+ return cls(now + max(0.0, seconds))
23
+
24
+ def remaining(self) -> float:
25
+ """Return seconds remaining (clamped at 0)."""
26
+ return max(0.0, self._deadline - time.monotonic())
27
+
28
+ def expired(self) -> bool:
29
+ """Return True if the deadline is exhausted."""
30
+ return self.remaining() <= 0.0
31
+
32
+ def child(self, seconds: float | None = None) -> "Deadline":
33
+ """Create a child deadline bounded by this deadline."""
34
+ now = time.monotonic()
35
+ parent_deadline = self._deadline
36
+ if seconds is None:
37
+ return Deadline(parent_deadline)
38
+ return Deadline(min(parent_deadline, now + max(0.0, seconds)))