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.
- brawny/__init__.py +2 -0
- brawny/_context.py +5 -5
- brawny/_rpc/__init__.py +36 -12
- brawny/_rpc/broadcast.py +14 -13
- brawny/_rpc/caller.py +243 -0
- brawny/_rpc/client.py +539 -0
- brawny/_rpc/clients.py +11 -11
- brawny/_rpc/context.py +23 -0
- brawny/_rpc/errors.py +465 -31
- brawny/_rpc/gas.py +7 -6
- brawny/_rpc/pool.py +18 -0
- brawny/_rpc/retry.py +266 -0
- brawny/_rpc/retry_policy.py +81 -0
- brawny/accounts.py +28 -9
- brawny/alerts/__init__.py +15 -18
- brawny/alerts/abi_resolver.py +212 -36
- brawny/alerts/base.py +2 -2
- brawny/alerts/contracts.py +77 -10
- brawny/alerts/errors.py +30 -3
- brawny/alerts/events.py +38 -5
- brawny/alerts/health.py +19 -13
- brawny/alerts/send.py +513 -55
- brawny/api.py +39 -11
- brawny/assets/AGENTS.md +325 -0
- brawny/async_runtime.py +48 -0
- brawny/chain.py +3 -3
- brawny/cli/commands/__init__.py +2 -0
- brawny/cli/commands/console.py +69 -19
- brawny/cli/commands/contract.py +2 -2
- brawny/cli/commands/controls.py +121 -0
- brawny/cli/commands/health.py +2 -2
- brawny/cli/commands/job_dev.py +6 -5
- brawny/cli/commands/jobs.py +99 -2
- brawny/cli/commands/maintenance.py +13 -29
- brawny/cli/commands/migrate.py +1 -0
- brawny/cli/commands/run.py +10 -3
- brawny/cli/commands/script.py +8 -3
- brawny/cli/commands/signer.py +143 -26
- brawny/cli/helpers.py +0 -3
- brawny/cli_templates.py +25 -349
- brawny/config/__init__.py +4 -1
- brawny/config/models.py +43 -57
- brawny/config/parser.py +268 -57
- brawny/config/validation.py +52 -15
- brawny/daemon/context.py +4 -2
- brawny/daemon/core.py +185 -63
- brawny/daemon/loops.py +166 -98
- brawny/daemon/supervisor.py +261 -0
- brawny/db/__init__.py +14 -26
- brawny/db/base.py +248 -151
- brawny/db/global_cache.py +11 -1
- brawny/db/migrate.py +175 -28
- brawny/db/migrations/001_init.sql +4 -3
- brawny/db/migrations/010_add_nonce_gap_index.sql +1 -1
- brawny/db/migrations/011_add_job_logs.sql +1 -2
- brawny/db/migrations/012_add_claimed_by.sql +2 -2
- brawny/db/migrations/013_attempt_unique.sql +10 -0
- brawny/db/migrations/014_add_lease_expires_at.sql +5 -0
- brawny/db/migrations/015_add_signer_alias.sql +14 -0
- brawny/db/migrations/016_runtime_controls_and_quarantine.sql +32 -0
- brawny/db/migrations/017_add_job_drain.sql +6 -0
- brawny/db/migrations/018_add_nonce_reset_audit.sql +20 -0
- brawny/db/migrations/019_add_job_cooldowns.sql +8 -0
- brawny/db/migrations/020_attempt_unique_initial.sql +7 -0
- brawny/db/ops/__init__.py +3 -25
- brawny/db/ops/logs.py +1 -2
- brawny/db/queries.py +47 -91
- brawny/db/serialized.py +65 -0
- brawny/db/sqlite/__init__.py +1001 -0
- brawny/db/sqlite/connection.py +231 -0
- brawny/db/sqlite/execute.py +116 -0
- brawny/db/sqlite/mappers.py +190 -0
- brawny/db/sqlite/repos/attempts.py +372 -0
- brawny/db/sqlite/repos/block_state.py +102 -0
- brawny/db/sqlite/repos/cache.py +104 -0
- brawny/db/sqlite/repos/intents.py +1021 -0
- brawny/db/sqlite/repos/jobs.py +200 -0
- brawny/db/sqlite/repos/maintenance.py +182 -0
- brawny/db/sqlite/repos/signers_nonces.py +566 -0
- brawny/db/sqlite/tx.py +119 -0
- brawny/http.py +194 -0
- brawny/invariants.py +11 -24
- brawny/jobs/base.py +8 -0
- brawny/jobs/job_validation.py +2 -1
- brawny/keystore.py +83 -7
- brawny/lifecycle.py +64 -12
- brawny/logging.py +0 -2
- brawny/metrics.py +84 -12
- brawny/model/contexts.py +111 -9
- brawny/model/enums.py +1 -0
- brawny/model/errors.py +18 -0
- brawny/model/types.py +47 -131
- brawny/network_guard.py +133 -0
- brawny/networks/__init__.py +5 -5
- brawny/networks/config.py +1 -7
- brawny/networks/manager.py +14 -11
- brawny/runtime_controls.py +74 -0
- brawny/scheduler/poller.py +11 -7
- brawny/scheduler/reorg.py +95 -39
- brawny/scheduler/runner.py +442 -168
- brawny/scheduler/shutdown.py +3 -3
- brawny/script_tx.py +3 -3
- brawny/telegram.py +53 -7
- brawny/testing.py +1 -0
- brawny/timeout.py +38 -0
- brawny/tx/executor.py +922 -308
- brawny/tx/intent.py +54 -16
- brawny/tx/monitor.py +31 -12
- brawny/tx/nonce.py +212 -90
- brawny/tx/replacement.py +69 -18
- brawny/tx/retry_policy.py +24 -0
- brawny/tx/stages/types.py +75 -0
- brawny/types.py +18 -0
- brawny/utils.py +41 -0
- {brawny-0.1.13.dist-info → brawny-0.1.22.dist-info}/METADATA +3 -3
- brawny-0.1.22.dist-info/RECORD +163 -0
- brawny/_rpc/manager.py +0 -982
- brawny/_rpc/selector.py +0 -156
- brawny/db/base_new.py +0 -165
- brawny/db/mappers.py +0 -182
- brawny/db/migrations/008_add_transactions.sql +0 -72
- brawny/db/ops/attempts.py +0 -108
- brawny/db/ops/blocks.py +0 -83
- brawny/db/ops/cache.py +0 -93
- brawny/db/ops/intents.py +0 -296
- brawny/db/ops/jobs.py +0 -110
- brawny/db/ops/nonces.py +0 -322
- brawny/db/postgres.py +0 -2535
- brawny/db/postgres_new.py +0 -196
- brawny/db/sqlite.py +0 -2733
- brawny/db/sqlite_new.py +0 -191
- brawny-0.1.13.dist-info/RECORD +0 -141
- {brawny-0.1.13.dist-info → brawny-0.1.22.dist-info}/WHEEL +0 -0
- {brawny-0.1.13.dist-info → brawny-0.1.22.dist-info}/entry_points.txt +0 -0
- {brawny-0.1.13.dist-info → brawny-0.1.22.dist-info}/top_level.txt +0 -0
brawny/scheduler/shutdown.py
CHANGED
|
@@ -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.
|
|
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:
|
|
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:
|
|
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.
|
|
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: "
|
|
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: "
|
|
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
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
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
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)))
|