brawny 0.1.13__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 +106 -0
- brawny/_context.py +232 -0
- brawny/_rpc/__init__.py +38 -0
- brawny/_rpc/broadcast.py +172 -0
- brawny/_rpc/clients.py +98 -0
- brawny/_rpc/context.py +49 -0
- brawny/_rpc/errors.py +252 -0
- brawny/_rpc/gas.py +158 -0
- brawny/_rpc/manager.py +982 -0
- brawny/_rpc/selector.py +156 -0
- brawny/accounts.py +534 -0
- brawny/alerts/__init__.py +132 -0
- brawny/alerts/abi_resolver.py +530 -0
- brawny/alerts/base.py +152 -0
- brawny/alerts/context.py +271 -0
- brawny/alerts/contracts.py +635 -0
- brawny/alerts/encoded_call.py +201 -0
- brawny/alerts/errors.py +267 -0
- brawny/alerts/events.py +680 -0
- brawny/alerts/function_caller.py +364 -0
- brawny/alerts/health.py +185 -0
- brawny/alerts/routing.py +118 -0
- brawny/alerts/send.py +364 -0
- brawny/api.py +660 -0
- brawny/chain.py +93 -0
- brawny/cli/__init__.py +16 -0
- brawny/cli/app.py +17 -0
- brawny/cli/bootstrap.py +37 -0
- brawny/cli/commands/__init__.py +41 -0
- brawny/cli/commands/abi.py +93 -0
- brawny/cli/commands/accounts.py +632 -0
- brawny/cli/commands/console.py +495 -0
- brawny/cli/commands/contract.py +139 -0
- brawny/cli/commands/health.py +112 -0
- brawny/cli/commands/init_project.py +86 -0
- brawny/cli/commands/intents.py +130 -0
- brawny/cli/commands/job_dev.py +254 -0
- brawny/cli/commands/jobs.py +308 -0
- brawny/cli/commands/logs.py +87 -0
- brawny/cli/commands/maintenance.py +182 -0
- brawny/cli/commands/migrate.py +51 -0
- brawny/cli/commands/networks.py +253 -0
- brawny/cli/commands/run.py +249 -0
- brawny/cli/commands/script.py +209 -0
- brawny/cli/commands/signer.py +248 -0
- brawny/cli/helpers.py +265 -0
- brawny/cli_templates.py +1445 -0
- brawny/config/__init__.py +74 -0
- brawny/config/models.py +404 -0
- brawny/config/parser.py +633 -0
- brawny/config/routing.py +55 -0
- brawny/config/validation.py +246 -0
- brawny/daemon/__init__.py +14 -0
- brawny/daemon/context.py +69 -0
- brawny/daemon/core.py +702 -0
- brawny/daemon/loops.py +327 -0
- brawny/db/__init__.py +78 -0
- brawny/db/base.py +986 -0
- brawny/db/base_new.py +165 -0
- brawny/db/circuit_breaker.py +97 -0
- brawny/db/global_cache.py +298 -0
- brawny/db/mappers.py +182 -0
- brawny/db/migrate.py +349 -0
- brawny/db/migrations/001_init.sql +186 -0
- brawny/db/migrations/002_add_included_block.sql +7 -0
- brawny/db/migrations/003_add_broadcast_at.sql +10 -0
- brawny/db/migrations/004_broadcast_binding.sql +20 -0
- brawny/db/migrations/005_add_retry_after.sql +9 -0
- brawny/db/migrations/006_add_retry_count_column.sql +11 -0
- brawny/db/migrations/007_add_gap_tracking.sql +18 -0
- brawny/db/migrations/008_add_transactions.sql +72 -0
- brawny/db/migrations/009_add_intent_metadata.sql +5 -0
- brawny/db/migrations/010_add_nonce_gap_index.sql +9 -0
- brawny/db/migrations/011_add_job_logs.sql +24 -0
- brawny/db/migrations/012_add_claimed_by.sql +5 -0
- brawny/db/ops/__init__.py +29 -0
- brawny/db/ops/attempts.py +108 -0
- brawny/db/ops/blocks.py +83 -0
- brawny/db/ops/cache.py +93 -0
- brawny/db/ops/intents.py +296 -0
- brawny/db/ops/jobs.py +110 -0
- brawny/db/ops/logs.py +97 -0
- brawny/db/ops/nonces.py +322 -0
- brawny/db/postgres.py +2535 -0
- brawny/db/postgres_new.py +196 -0
- brawny/db/queries.py +584 -0
- brawny/db/sqlite.py +2733 -0
- brawny/db/sqlite_new.py +191 -0
- brawny/history.py +126 -0
- brawny/interfaces.py +136 -0
- brawny/invariants.py +155 -0
- brawny/jobs/__init__.py +26 -0
- brawny/jobs/base.py +287 -0
- brawny/jobs/discovery.py +233 -0
- brawny/jobs/job_validation.py +111 -0
- brawny/jobs/kv.py +125 -0
- brawny/jobs/registry.py +283 -0
- brawny/keystore.py +484 -0
- brawny/lifecycle.py +551 -0
- brawny/logging.py +290 -0
- brawny/metrics.py +594 -0
- brawny/model/__init__.py +53 -0
- brawny/model/contexts.py +319 -0
- brawny/model/enums.py +70 -0
- brawny/model/errors.py +194 -0
- brawny/model/events.py +93 -0
- brawny/model/startup.py +20 -0
- brawny/model/types.py +483 -0
- brawny/networks/__init__.py +96 -0
- brawny/networks/config.py +269 -0
- brawny/networks/manager.py +423 -0
- brawny/obs/__init__.py +67 -0
- brawny/obs/emit.py +158 -0
- brawny/obs/health.py +175 -0
- brawny/obs/heartbeat.py +133 -0
- brawny/reconciliation.py +108 -0
- brawny/scheduler/__init__.py +19 -0
- brawny/scheduler/poller.py +472 -0
- brawny/scheduler/reorg.py +632 -0
- brawny/scheduler/runner.py +708 -0
- brawny/scheduler/shutdown.py +371 -0
- brawny/script_tx.py +297 -0
- brawny/scripting.py +251 -0
- brawny/startup.py +76 -0
- brawny/telegram.py +393 -0
- brawny/testing.py +108 -0
- brawny/tx/__init__.py +41 -0
- brawny/tx/executor.py +1071 -0
- brawny/tx/fees.py +50 -0
- brawny/tx/intent.py +423 -0
- brawny/tx/monitor.py +628 -0
- brawny/tx/nonce.py +498 -0
- brawny/tx/replacement.py +456 -0
- brawny/tx/utils.py +26 -0
- brawny/utils.py +205 -0
- brawny/validation.py +69 -0
- brawny-0.1.13.dist-info/METADATA +156 -0
- brawny-0.1.13.dist-info/RECORD +141 -0
- brawny-0.1.13.dist-info/WHEEL +5 -0
- brawny-0.1.13.dist-info/entry_points.txt +2 -0
- brawny-0.1.13.dist-info/top_level.txt +1 -0
brawny/telegram.py
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
"""Telegram Bot API wrapper with strict parameter naming.
|
|
2
|
+
|
|
3
|
+
All method signatures use Telegram Bot API parameter names verbatim.
|
|
4
|
+
No aliases. No renaming.
|
|
5
|
+
|
|
6
|
+
See: https://core.telegram.org/bots/api
|
|
7
|
+
|
|
8
|
+
Usage in jobs:
|
|
9
|
+
|
|
10
|
+
from brawny.telegram import telegram
|
|
11
|
+
|
|
12
|
+
class MyJob(Job):
|
|
13
|
+
def check(self, ctx):
|
|
14
|
+
# Send a simple message
|
|
15
|
+
telegram.send_message("Something happened!", chat_id="-100...")
|
|
16
|
+
|
|
17
|
+
# Send with markdown
|
|
18
|
+
telegram.send_message("*Bold*", chat_id="-100...", parse_mode="Markdown")
|
|
19
|
+
|
|
20
|
+
# Disable link preview
|
|
21
|
+
telegram.send_message("Check https://example.com", chat_id="-100...",
|
|
22
|
+
disable_web_page_preview=False)
|
|
23
|
+
|
|
24
|
+
# Silent notification
|
|
25
|
+
telegram.send_message("Low priority", chat_id="-100...",
|
|
26
|
+
disable_notification=True)
|
|
27
|
+
|
|
28
|
+
Configuration:
|
|
29
|
+
Set in config.yaml:
|
|
30
|
+
telegram:
|
|
31
|
+
bot_token: "${TELEGRAM_BOT_TOKEN}"
|
|
32
|
+
chats:
|
|
33
|
+
ops: "-1001234567890"
|
|
34
|
+
dev: "-1009876543210"
|
|
35
|
+
default: ["ops"]
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import os # Used by _LazyTelegram for TELEGRAM_BOT_TOKEN
|
|
41
|
+
from typing import Any
|
|
42
|
+
|
|
43
|
+
import requests
|
|
44
|
+
|
|
45
|
+
from brawny.logging import get_logger
|
|
46
|
+
|
|
47
|
+
logger = get_logger(__name__)
|
|
48
|
+
|
|
49
|
+
# Telegram API limits
|
|
50
|
+
MAX_MESSAGE_LENGTH = 4096
|
|
51
|
+
TRUNCATION_SUFFIX = "\n...[truncated]"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _truncate_message(text: str, max_length: int = MAX_MESSAGE_LENGTH) -> str:
|
|
55
|
+
"""Truncate message to fit Telegram's limit."""
|
|
56
|
+
if len(text) <= max_length:
|
|
57
|
+
return text
|
|
58
|
+
suffix_len = len(TRUNCATION_SUFFIX)
|
|
59
|
+
if max_length <= suffix_len:
|
|
60
|
+
return text[:max_length]
|
|
61
|
+
return text[: max_length - suffix_len] + TRUNCATION_SUFFIX
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class TelegramBot:
|
|
65
|
+
"""Telegram Bot API wrapper.
|
|
66
|
+
|
|
67
|
+
All methods use Telegram API parameter names verbatim.
|
|
68
|
+
No aliases or renaming.
|
|
69
|
+
|
|
70
|
+
See: https://core.telegram.org/bots/api
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
token: str | None = None,
|
|
76
|
+
timeout: int = 30,
|
|
77
|
+
) -> None:
|
|
78
|
+
"""Initialize Telegram bot.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
token: Bot token. Required for API calls.
|
|
82
|
+
timeout: Request timeout in seconds.
|
|
83
|
+
"""
|
|
84
|
+
self._token = token
|
|
85
|
+
self._timeout = timeout
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def configured(self) -> bool:
|
|
89
|
+
"""Check if bot is configured with token."""
|
|
90
|
+
return bool(self._token)
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def api_url(self) -> str:
|
|
94
|
+
"""Base URL for Telegram API."""
|
|
95
|
+
return f"https://api.telegram.org/bot{self._token}"
|
|
96
|
+
|
|
97
|
+
def _request(self, method: str, **params: Any) -> dict | bool | None:
|
|
98
|
+
"""Make a request to the Telegram API.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
method: API method name (e.g., "sendMessage")
|
|
102
|
+
**params: Method parameters
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
API response (dict or bool depending on endpoint), or None on failure
|
|
106
|
+
"""
|
|
107
|
+
if not self._token:
|
|
108
|
+
# Silent no-op: startup already warned about missing bot_token
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
# Extract chat_id for error logging before filtering
|
|
112
|
+
chat_id = params.get("chat_id")
|
|
113
|
+
|
|
114
|
+
# Filter out None values
|
|
115
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
response = requests.post(
|
|
119
|
+
f"{self.api_url}/{method}",
|
|
120
|
+
json=params,
|
|
121
|
+
timeout=self._timeout,
|
|
122
|
+
)
|
|
123
|
+
response.raise_for_status()
|
|
124
|
+
result = response.json()
|
|
125
|
+
|
|
126
|
+
if not result.get("ok"):
|
|
127
|
+
logger.error(
|
|
128
|
+
"telegram.api_error",
|
|
129
|
+
method=method,
|
|
130
|
+
error=result.get("description"),
|
|
131
|
+
chat_id=chat_id,
|
|
132
|
+
)
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
return result.get("result")
|
|
136
|
+
|
|
137
|
+
except requests.exceptions.RequestException as e:
|
|
138
|
+
logger.error(
|
|
139
|
+
"telegram.request_failed",
|
|
140
|
+
method=method,
|
|
141
|
+
error=str(e)[:200],
|
|
142
|
+
chat_id=chat_id,
|
|
143
|
+
)
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
def send_message(
|
|
147
|
+
self,
|
|
148
|
+
text: str,
|
|
149
|
+
*,
|
|
150
|
+
chat_id: str | int,
|
|
151
|
+
parse_mode: str | None = None,
|
|
152
|
+
disable_web_page_preview: bool | None = None,
|
|
153
|
+
disable_notification: bool | None = None,
|
|
154
|
+
message_thread_id: int | None = None,
|
|
155
|
+
reply_to_message_id: int | None = None,
|
|
156
|
+
) -> dict | None:
|
|
157
|
+
"""Send a text message.
|
|
158
|
+
|
|
159
|
+
https://core.telegram.org/bots/api#sendmessage
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
text: Message text (up to 4096 characters, auto-truncated)
|
|
163
|
+
chat_id: Target chat ID (required)
|
|
164
|
+
parse_mode: "Markdown", "MarkdownV2", "HTML", or None
|
|
165
|
+
disable_web_page_preview: Disable link previews
|
|
166
|
+
disable_notification: Send without notification sound
|
|
167
|
+
message_thread_id: Thread ID for forum topics
|
|
168
|
+
reply_to_message_id: Message ID to reply to
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Message object from Telegram API, or None on failure
|
|
172
|
+
"""
|
|
173
|
+
return self._request(
|
|
174
|
+
"sendMessage",
|
|
175
|
+
chat_id=chat_id,
|
|
176
|
+
text=_truncate_message(text),
|
|
177
|
+
parse_mode=parse_mode,
|
|
178
|
+
disable_web_page_preview=disable_web_page_preview,
|
|
179
|
+
disable_notification=disable_notification,
|
|
180
|
+
message_thread_id=message_thread_id,
|
|
181
|
+
reply_to_message_id=reply_to_message_id,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
def send_photo(
|
|
185
|
+
self,
|
|
186
|
+
*,
|
|
187
|
+
chat_id: str | int,
|
|
188
|
+
photo: str,
|
|
189
|
+
caption: str | None = None,
|
|
190
|
+
parse_mode: str | None = None,
|
|
191
|
+
disable_notification: bool | None = None,
|
|
192
|
+
) -> dict | None:
|
|
193
|
+
"""Send a photo.
|
|
194
|
+
|
|
195
|
+
https://core.telegram.org/bots/api#sendphoto
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
chat_id: Target chat ID (required)
|
|
199
|
+
photo: Photo URL or file_id
|
|
200
|
+
caption: Optional caption
|
|
201
|
+
parse_mode: Caption parse mode
|
|
202
|
+
disable_notification: Send without notification sound
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Message object or None
|
|
206
|
+
"""
|
|
207
|
+
return self._request(
|
|
208
|
+
"sendPhoto",
|
|
209
|
+
chat_id=chat_id,
|
|
210
|
+
photo=photo,
|
|
211
|
+
caption=caption,
|
|
212
|
+
parse_mode=parse_mode,
|
|
213
|
+
disable_notification=disable_notification,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
def send_document(
|
|
217
|
+
self,
|
|
218
|
+
*,
|
|
219
|
+
chat_id: str | int,
|
|
220
|
+
document: str,
|
|
221
|
+
caption: str | None = None,
|
|
222
|
+
parse_mode: str | None = None,
|
|
223
|
+
disable_notification: bool | None = None,
|
|
224
|
+
) -> dict | None:
|
|
225
|
+
"""Send a document.
|
|
226
|
+
|
|
227
|
+
https://core.telegram.org/bots/api#senddocument
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
chat_id: Target chat ID (required)
|
|
231
|
+
document: Document URL or file_id
|
|
232
|
+
caption: Optional caption
|
|
233
|
+
parse_mode: Caption parse mode
|
|
234
|
+
disable_notification: Send without notification sound
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
Message object or None
|
|
238
|
+
"""
|
|
239
|
+
return self._request(
|
|
240
|
+
"sendDocument",
|
|
241
|
+
chat_id=chat_id,
|
|
242
|
+
document=document,
|
|
243
|
+
caption=caption,
|
|
244
|
+
parse_mode=parse_mode,
|
|
245
|
+
disable_notification=disable_notification,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
def edit_message_text(
|
|
249
|
+
self,
|
|
250
|
+
text: str,
|
|
251
|
+
*,
|
|
252
|
+
chat_id: str | int,
|
|
253
|
+
message_id: int,
|
|
254
|
+
parse_mode: str | None = None,
|
|
255
|
+
disable_web_page_preview: bool | None = None,
|
|
256
|
+
) -> dict | None:
|
|
257
|
+
"""Edit a message's text.
|
|
258
|
+
|
|
259
|
+
https://core.telegram.org/bots/api#editmessagetext
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
text: New message text
|
|
263
|
+
chat_id: Chat containing the message
|
|
264
|
+
message_id: ID of message to edit
|
|
265
|
+
parse_mode: Text parse mode
|
|
266
|
+
disable_web_page_preview: Disable link previews
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
Edited message object or None
|
|
270
|
+
"""
|
|
271
|
+
return self._request(
|
|
272
|
+
"editMessageText",
|
|
273
|
+
chat_id=chat_id,
|
|
274
|
+
message_id=message_id,
|
|
275
|
+
text=_truncate_message(text),
|
|
276
|
+
parse_mode=parse_mode,
|
|
277
|
+
disable_web_page_preview=disable_web_page_preview,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
def delete_message(
|
|
281
|
+
self,
|
|
282
|
+
*,
|
|
283
|
+
chat_id: str | int,
|
|
284
|
+
message_id: int,
|
|
285
|
+
) -> bool:
|
|
286
|
+
"""Delete a message.
|
|
287
|
+
|
|
288
|
+
https://core.telegram.org/bots/api#deletemessage
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
chat_id: Chat containing the message
|
|
292
|
+
message_id: ID of message to delete
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
True if deleted successfully
|
|
296
|
+
"""
|
|
297
|
+
result = self._request(
|
|
298
|
+
"deleteMessage",
|
|
299
|
+
chat_id=chat_id,
|
|
300
|
+
message_id=message_id,
|
|
301
|
+
)
|
|
302
|
+
return result is True
|
|
303
|
+
|
|
304
|
+
def pin_chat_message(
|
|
305
|
+
self,
|
|
306
|
+
*,
|
|
307
|
+
chat_id: str | int,
|
|
308
|
+
message_id: int,
|
|
309
|
+
disable_notification: bool | None = None,
|
|
310
|
+
) -> bool:
|
|
311
|
+
"""Pin a message in a chat.
|
|
312
|
+
|
|
313
|
+
https://core.telegram.org/bots/api#pinchatmessage
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
chat_id: Chat containing the message
|
|
317
|
+
message_id: ID of message to pin
|
|
318
|
+
disable_notification: Pin without notification
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
True if pinned successfully
|
|
322
|
+
"""
|
|
323
|
+
result = self._request(
|
|
324
|
+
"pinChatMessage",
|
|
325
|
+
chat_id=chat_id,
|
|
326
|
+
message_id=message_id,
|
|
327
|
+
disable_notification=disable_notification,
|
|
328
|
+
)
|
|
329
|
+
return result is True
|
|
330
|
+
|
|
331
|
+
def get_me(self) -> dict | None:
|
|
332
|
+
"""Get bot information.
|
|
333
|
+
|
|
334
|
+
https://core.telegram.org/bots/api#getme
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
Bot user object or None
|
|
338
|
+
"""
|
|
339
|
+
return self._request("getMe")
|
|
340
|
+
|
|
341
|
+
def get_chat(self, *, chat_id: str | int) -> dict | None:
|
|
342
|
+
"""Get chat information.
|
|
343
|
+
|
|
344
|
+
https://core.telegram.org/bots/api#getchat
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
chat_id: Chat to get info for
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
Chat object or None
|
|
351
|
+
"""
|
|
352
|
+
return self._request("getChat", chat_id=chat_id)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
class _LazyTelegram:
|
|
357
|
+
"""Lazy proxy for TelegramBot that initializes on first access.
|
|
358
|
+
|
|
359
|
+
This defers TelegramBot creation until first use, ensuring environment
|
|
360
|
+
variables from .env are loaded before reading TELEGRAM_BOT_TOKEN.
|
|
361
|
+
"""
|
|
362
|
+
|
|
363
|
+
_instance: TelegramBot | None = None
|
|
364
|
+
|
|
365
|
+
def _get_instance(self) -> TelegramBot:
|
|
366
|
+
if self._instance is None:
|
|
367
|
+
self._instance = TelegramBot(token=os.environ.get("TELEGRAM_BOT_TOKEN"))
|
|
368
|
+
return self._instance
|
|
369
|
+
|
|
370
|
+
def __getattr__(self, name: str) -> Any:
|
|
371
|
+
return getattr(self._get_instance(), name)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
# Global instance using environment variables
|
|
375
|
+
# Users can import and use directly: from brawny.telegram import telegram
|
|
376
|
+
telegram: TelegramBot = _LazyTelegram() # type: ignore[assignment]
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def get_telegram(token: str | None = None) -> TelegramBot:
|
|
380
|
+
"""Get a Telegram bot instance.
|
|
381
|
+
|
|
382
|
+
Use this to create a bot with custom configuration.
|
|
383
|
+
For the default instance, just import `telegram` directly.
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
token: Bot token (defaults to env var)
|
|
387
|
+
|
|
388
|
+
Returns:
|
|
389
|
+
TelegramBot instance
|
|
390
|
+
"""
|
|
391
|
+
if token:
|
|
392
|
+
return TelegramBot(token=token)
|
|
393
|
+
return telegram._get_instance() # type: ignore[union-attr]
|
brawny/testing.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Testing utilities for jobs with implicit context.
|
|
2
|
+
|
|
3
|
+
Provides helpers to set up the implicit context for unit testing jobs
|
|
4
|
+
without running the full framework.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from brawny.testing import job_context
|
|
8
|
+
|
|
9
|
+
def test_my_job():
|
|
10
|
+
job = MyJob()
|
|
11
|
+
with job_context(job, block_number=1000) as ctx:
|
|
12
|
+
result = job.check(ctx)
|
|
13
|
+
assert result is not None
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from contextlib import contextmanager
|
|
19
|
+
from typing import TYPE_CHECKING, Any, Generator, Literal
|
|
20
|
+
from unittest.mock import MagicMock
|
|
21
|
+
|
|
22
|
+
from brawny._context import _job_ctx, _current_job, set_check_block, reset_check_block
|
|
23
|
+
from brawny.jobs.kv import InMemoryJobKVStore
|
|
24
|
+
from brawny.model.contexts import BlockContext, CheckContext
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from brawny.jobs.base import Job
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@contextmanager
|
|
31
|
+
def job_context(
|
|
32
|
+
job: Job,
|
|
33
|
+
block_number: int = 1000,
|
|
34
|
+
chain_id: int = 1,
|
|
35
|
+
block_hash: str | None = None,
|
|
36
|
+
timestamp: int | None = None,
|
|
37
|
+
rpc: Any | None = None,
|
|
38
|
+
contract_system: Any | None = None,
|
|
39
|
+
kv: Any | None = None,
|
|
40
|
+
phase: Literal["check", "build", "alert"] = "check",
|
|
41
|
+
) -> Generator[CheckContext, None, None]:
|
|
42
|
+
"""Set up implicit context for testing job hooks.
|
|
43
|
+
|
|
44
|
+
Creates a CheckContext and sets the contextvars so that implicit
|
|
45
|
+
context functions (contract, trigger, tx, block) work correctly.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
job: The job instance to test
|
|
49
|
+
block_number: Block number for the context (default 1000)
|
|
50
|
+
chain_id: Chain ID (default 1)
|
|
51
|
+
block_hash: Block hash (default generates one)
|
|
52
|
+
timestamp: Block timestamp (default 1700000000)
|
|
53
|
+
rpc: RPC manager (default MagicMock)
|
|
54
|
+
contract_system: Contract system for ContractFactory (default None)
|
|
55
|
+
kv: KV store (default InMemoryJobKVStore)
|
|
56
|
+
phase: Which phase to simulate. "check" pins reads to block_number,
|
|
57
|
+
"build"/"alert" use latest (default: "check")
|
|
58
|
+
|
|
59
|
+
Yields:
|
|
60
|
+
The CheckContext for additional assertions or setup
|
|
61
|
+
|
|
62
|
+
Example:
|
|
63
|
+
from brawny import Contract, trigger
|
|
64
|
+
from brawny.testing import job_context
|
|
65
|
+
|
|
66
|
+
def test_harvest_check():
|
|
67
|
+
job = HarvestJob()
|
|
68
|
+
with job_context(job, block_number=12345) as ctx:
|
|
69
|
+
# Implicit context is now available
|
|
70
|
+
result = job.check(ctx)
|
|
71
|
+
assert result is not None
|
|
72
|
+
"""
|
|
73
|
+
from brawny.alerts.contracts import SimpleContractFactory
|
|
74
|
+
|
|
75
|
+
block = BlockContext(
|
|
76
|
+
number=block_number,
|
|
77
|
+
timestamp=timestamp or 1700000000,
|
|
78
|
+
hash=block_hash or ("0x" + "ab" * 32),
|
|
79
|
+
base_fee=0,
|
|
80
|
+
chain_id=chain_id,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Build ContractFactory if contract_system provided
|
|
84
|
+
contracts = SimpleContractFactory(contract_system) if contract_system else None
|
|
85
|
+
|
|
86
|
+
ctx = CheckContext(
|
|
87
|
+
block=block,
|
|
88
|
+
kv=kv or InMemoryJobKVStore(),
|
|
89
|
+
job_id=job.job_id,
|
|
90
|
+
rpc=rpc or MagicMock(),
|
|
91
|
+
logger=MagicMock(),
|
|
92
|
+
contracts=contracts,
|
|
93
|
+
_db=None,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
ctx_token = _job_ctx.set(ctx)
|
|
97
|
+
job_token = _current_job.set(job)
|
|
98
|
+
|
|
99
|
+
# Only pin block in check phase (matches real runner behavior)
|
|
100
|
+
check_block_token = set_check_block(block_number) if phase == "check" else None
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
yield ctx
|
|
104
|
+
finally:
|
|
105
|
+
if check_block_token is not None:
|
|
106
|
+
reset_check_block(check_block_token)
|
|
107
|
+
_job_ctx.reset(ctx_token)
|
|
108
|
+
_current_job.reset(job_token)
|
brawny/tx/__init__.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Transaction intent, attempt, nonce management, and execution."""
|
|
2
|
+
|
|
3
|
+
from brawny.tx.executor import ExecutionOutcome, ExecutionResult, TxExecutor
|
|
4
|
+
from brawny.tx.intent import (
|
|
5
|
+
abandon_intent,
|
|
6
|
+
claim_intent,
|
|
7
|
+
create_intent,
|
|
8
|
+
get_or_create_intent,
|
|
9
|
+
get_pending_for_signer,
|
|
10
|
+
release_claim,
|
|
11
|
+
revert_to_pending,
|
|
12
|
+
update_status,
|
|
13
|
+
)
|
|
14
|
+
from brawny.tx.monitor import ConfirmationResult, ConfirmationStatus, TxMonitor
|
|
15
|
+
from brawny.tx.nonce import NonceManager
|
|
16
|
+
from brawny.tx.replacement import ReplacementResult, TxReplacer
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
# Executor
|
|
20
|
+
"TxExecutor",
|
|
21
|
+
"ExecutionResult",
|
|
22
|
+
"ExecutionOutcome",
|
|
23
|
+
# Monitor
|
|
24
|
+
"TxMonitor",
|
|
25
|
+
"ConfirmationResult",
|
|
26
|
+
"ConfirmationStatus",
|
|
27
|
+
# Replacement
|
|
28
|
+
"TxReplacer",
|
|
29
|
+
"ReplacementResult",
|
|
30
|
+
# Nonce Manager
|
|
31
|
+
"NonceManager",
|
|
32
|
+
# Intent functions
|
|
33
|
+
"create_intent",
|
|
34
|
+
"get_or_create_intent",
|
|
35
|
+
"claim_intent",
|
|
36
|
+
"release_claim",
|
|
37
|
+
"update_status",
|
|
38
|
+
"abandon_intent",
|
|
39
|
+
"get_pending_for_signer",
|
|
40
|
+
"revert_to_pending",
|
|
41
|
+
]
|