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.
Files changed (141) hide show
  1. brawny/__init__.py +106 -0
  2. brawny/_context.py +232 -0
  3. brawny/_rpc/__init__.py +38 -0
  4. brawny/_rpc/broadcast.py +172 -0
  5. brawny/_rpc/clients.py +98 -0
  6. brawny/_rpc/context.py +49 -0
  7. brawny/_rpc/errors.py +252 -0
  8. brawny/_rpc/gas.py +158 -0
  9. brawny/_rpc/manager.py +982 -0
  10. brawny/_rpc/selector.py +156 -0
  11. brawny/accounts.py +534 -0
  12. brawny/alerts/__init__.py +132 -0
  13. brawny/alerts/abi_resolver.py +530 -0
  14. brawny/alerts/base.py +152 -0
  15. brawny/alerts/context.py +271 -0
  16. brawny/alerts/contracts.py +635 -0
  17. brawny/alerts/encoded_call.py +201 -0
  18. brawny/alerts/errors.py +267 -0
  19. brawny/alerts/events.py +680 -0
  20. brawny/alerts/function_caller.py +364 -0
  21. brawny/alerts/health.py +185 -0
  22. brawny/alerts/routing.py +118 -0
  23. brawny/alerts/send.py +364 -0
  24. brawny/api.py +660 -0
  25. brawny/chain.py +93 -0
  26. brawny/cli/__init__.py +16 -0
  27. brawny/cli/app.py +17 -0
  28. brawny/cli/bootstrap.py +37 -0
  29. brawny/cli/commands/__init__.py +41 -0
  30. brawny/cli/commands/abi.py +93 -0
  31. brawny/cli/commands/accounts.py +632 -0
  32. brawny/cli/commands/console.py +495 -0
  33. brawny/cli/commands/contract.py +139 -0
  34. brawny/cli/commands/health.py +112 -0
  35. brawny/cli/commands/init_project.py +86 -0
  36. brawny/cli/commands/intents.py +130 -0
  37. brawny/cli/commands/job_dev.py +254 -0
  38. brawny/cli/commands/jobs.py +308 -0
  39. brawny/cli/commands/logs.py +87 -0
  40. brawny/cli/commands/maintenance.py +182 -0
  41. brawny/cli/commands/migrate.py +51 -0
  42. brawny/cli/commands/networks.py +253 -0
  43. brawny/cli/commands/run.py +249 -0
  44. brawny/cli/commands/script.py +209 -0
  45. brawny/cli/commands/signer.py +248 -0
  46. brawny/cli/helpers.py +265 -0
  47. brawny/cli_templates.py +1445 -0
  48. brawny/config/__init__.py +74 -0
  49. brawny/config/models.py +404 -0
  50. brawny/config/parser.py +633 -0
  51. brawny/config/routing.py +55 -0
  52. brawny/config/validation.py +246 -0
  53. brawny/daemon/__init__.py +14 -0
  54. brawny/daemon/context.py +69 -0
  55. brawny/daemon/core.py +702 -0
  56. brawny/daemon/loops.py +327 -0
  57. brawny/db/__init__.py +78 -0
  58. brawny/db/base.py +986 -0
  59. brawny/db/base_new.py +165 -0
  60. brawny/db/circuit_breaker.py +97 -0
  61. brawny/db/global_cache.py +298 -0
  62. brawny/db/mappers.py +182 -0
  63. brawny/db/migrate.py +349 -0
  64. brawny/db/migrations/001_init.sql +186 -0
  65. brawny/db/migrations/002_add_included_block.sql +7 -0
  66. brawny/db/migrations/003_add_broadcast_at.sql +10 -0
  67. brawny/db/migrations/004_broadcast_binding.sql +20 -0
  68. brawny/db/migrations/005_add_retry_after.sql +9 -0
  69. brawny/db/migrations/006_add_retry_count_column.sql +11 -0
  70. brawny/db/migrations/007_add_gap_tracking.sql +18 -0
  71. brawny/db/migrations/008_add_transactions.sql +72 -0
  72. brawny/db/migrations/009_add_intent_metadata.sql +5 -0
  73. brawny/db/migrations/010_add_nonce_gap_index.sql +9 -0
  74. brawny/db/migrations/011_add_job_logs.sql +24 -0
  75. brawny/db/migrations/012_add_claimed_by.sql +5 -0
  76. brawny/db/ops/__init__.py +29 -0
  77. brawny/db/ops/attempts.py +108 -0
  78. brawny/db/ops/blocks.py +83 -0
  79. brawny/db/ops/cache.py +93 -0
  80. brawny/db/ops/intents.py +296 -0
  81. brawny/db/ops/jobs.py +110 -0
  82. brawny/db/ops/logs.py +97 -0
  83. brawny/db/ops/nonces.py +322 -0
  84. brawny/db/postgres.py +2535 -0
  85. brawny/db/postgres_new.py +196 -0
  86. brawny/db/queries.py +584 -0
  87. brawny/db/sqlite.py +2733 -0
  88. brawny/db/sqlite_new.py +191 -0
  89. brawny/history.py +126 -0
  90. brawny/interfaces.py +136 -0
  91. brawny/invariants.py +155 -0
  92. brawny/jobs/__init__.py +26 -0
  93. brawny/jobs/base.py +287 -0
  94. brawny/jobs/discovery.py +233 -0
  95. brawny/jobs/job_validation.py +111 -0
  96. brawny/jobs/kv.py +125 -0
  97. brawny/jobs/registry.py +283 -0
  98. brawny/keystore.py +484 -0
  99. brawny/lifecycle.py +551 -0
  100. brawny/logging.py +290 -0
  101. brawny/metrics.py +594 -0
  102. brawny/model/__init__.py +53 -0
  103. brawny/model/contexts.py +319 -0
  104. brawny/model/enums.py +70 -0
  105. brawny/model/errors.py +194 -0
  106. brawny/model/events.py +93 -0
  107. brawny/model/startup.py +20 -0
  108. brawny/model/types.py +483 -0
  109. brawny/networks/__init__.py +96 -0
  110. brawny/networks/config.py +269 -0
  111. brawny/networks/manager.py +423 -0
  112. brawny/obs/__init__.py +67 -0
  113. brawny/obs/emit.py +158 -0
  114. brawny/obs/health.py +175 -0
  115. brawny/obs/heartbeat.py +133 -0
  116. brawny/reconciliation.py +108 -0
  117. brawny/scheduler/__init__.py +19 -0
  118. brawny/scheduler/poller.py +472 -0
  119. brawny/scheduler/reorg.py +632 -0
  120. brawny/scheduler/runner.py +708 -0
  121. brawny/scheduler/shutdown.py +371 -0
  122. brawny/script_tx.py +297 -0
  123. brawny/scripting.py +251 -0
  124. brawny/startup.py +76 -0
  125. brawny/telegram.py +393 -0
  126. brawny/testing.py +108 -0
  127. brawny/tx/__init__.py +41 -0
  128. brawny/tx/executor.py +1071 -0
  129. brawny/tx/fees.py +50 -0
  130. brawny/tx/intent.py +423 -0
  131. brawny/tx/monitor.py +628 -0
  132. brawny/tx/nonce.py +498 -0
  133. brawny/tx/replacement.py +456 -0
  134. brawny/tx/utils.py +26 -0
  135. brawny/utils.py +205 -0
  136. brawny/validation.py +69 -0
  137. brawny-0.1.13.dist-info/METADATA +156 -0
  138. brawny-0.1.13.dist-info/RECORD +141 -0
  139. brawny-0.1.13.dist-info/WHEEL +5 -0
  140. brawny-0.1.13.dist-info/entry_points.txt +2 -0
  141. brawny-0.1.13.dist-info/top_level.txt +1 -0
brawny/logging.py ADDED
@@ -0,0 +1,290 @@
1
+ """Structured logging for brawny.
2
+
3
+ Provides JSON-formatted logs with correlation IDs for request tracing.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ import re
10
+ import sys
11
+ import uuid
12
+ from contextvars import ContextVar
13
+ from datetime import datetime, timezone
14
+ from typing import Any, Literal
15
+
16
+ import structlog
17
+
18
+ from brawny.model.enums import LogFormat
19
+
20
+ # Module-level state for mode switching
21
+ _runtime_log_level: str = "INFO"
22
+ _runtime_log_format: LogFormat = LogFormat.JSON
23
+ _runtime_chain_id: int | None = None
24
+
25
+ # Context variable for correlation ID propagation
26
+ _correlation_id: ContextVar[str | None] = ContextVar("correlation_id", default=None)
27
+
28
+
29
+ def get_correlation_id() -> str:
30
+ """Get the current correlation ID or generate a new one."""
31
+ cid = _correlation_id.get()
32
+ if cid is None:
33
+ cid = generate_correlation_id()
34
+ _correlation_id.set(cid)
35
+ return cid
36
+
37
+
38
+ def set_correlation_id(correlation_id: str) -> None:
39
+ """Set the correlation ID for the current context."""
40
+ _correlation_id.set(correlation_id)
41
+
42
+
43
+ def generate_correlation_id() -> str:
44
+ """Generate a new correlation ID."""
45
+ return f"req_{uuid.uuid4().hex[:12]}"
46
+
47
+
48
+ def add_correlation_id(
49
+ logger: logging.Logger, method_name: str, event_dict: dict[str, Any]
50
+ ) -> dict[str, Any]:
51
+ """Processor to add correlation_id to log entries."""
52
+ event_dict["correlation_id"] = get_correlation_id()
53
+ return event_dict
54
+
55
+
56
+ def add_timestamp(
57
+ logger: logging.Logger, method_name: str, event_dict: dict[str, Any]
58
+ ) -> dict[str, Any]:
59
+ """Processor to add ISO8601 timestamp to log entries."""
60
+ event_dict["timestamp"] = datetime.now(timezone.utc).isoformat()
61
+ return event_dict
62
+
63
+
64
+ def rename_event_to_event_key(
65
+ logger: logging.Logger, method_name: str, event_dict: dict[str, Any]
66
+ ) -> dict[str, Any]:
67
+ """Rename 'event' to match our log format."""
68
+ # structlog uses 'event' by default, we keep it
69
+ return event_dict
70
+
71
+
72
+ def add_log_level(
73
+ logger: logging.Logger, method_name: str, event_dict: dict[str, Any]
74
+ ) -> dict[str, Any]:
75
+ """Add uppercase log level."""
76
+ event_dict["level"] = method_name.upper()
77
+ return event_dict
78
+
79
+
80
+ _URL_CRED_RE = re.compile(r"(https?://)([^/@\s]+):([^/@\s]+)@")
81
+
82
+
83
+ def _redact_url_creds(value: str) -> str:
84
+ return _URL_CRED_RE.sub(r"\1***@", value)
85
+
86
+
87
+ def _redact_value(value: Any) -> Any:
88
+ if isinstance(value, str):
89
+ return _redact_url_creds(value)
90
+ if isinstance(value, dict):
91
+ redacted: dict[str, Any] = {}
92
+ for key, item in value.items():
93
+ key_lower = str(key).lower()
94
+ if key_lower in {"authorization", "proxy-authorization"}:
95
+ redacted[key] = "***"
96
+ else:
97
+ redacted[key] = _redact_value(item)
98
+ return redacted
99
+ if isinstance(value, (list, tuple)):
100
+ return type(value)(_redact_value(item) for item in value)
101
+ return value
102
+
103
+
104
+ def redact_sensitive(
105
+ logger: logging.Logger, method_name: str, event_dict: dict[str, Any]
106
+ ) -> dict[str, Any]:
107
+ """Redact credentials or auth tokens from log fields."""
108
+ return {key: _redact_value(value) for key, value in event_dict.items()}
109
+
110
+
111
+ def setup_logging(
112
+ log_level: str = "INFO",
113
+ log_format: LogFormat = LogFormat.JSON,
114
+ chain_id: int | None = None,
115
+ mode: Literal["startup", "runtime"] = "runtime",
116
+ ) -> None:
117
+ """Configure structured logging for the application.
118
+
119
+ Args:
120
+ log_level: Log level (DEBUG, INFO, WARNING, ERROR)
121
+ log_format: Output format (json or text)
122
+ chain_id: Chain ID to include in all log entries
123
+ mode: "startup" uses human-readable ConsoleRenderer with WARNING level,
124
+ "runtime" uses configured format and level
125
+ """
126
+ global _runtime_log_level, _runtime_log_format, _runtime_chain_id
127
+
128
+ # Store runtime config for later switch
129
+ _runtime_log_level = log_level
130
+ _runtime_log_format = log_format
131
+ _runtime_chain_id = chain_id
132
+
133
+ # Startup mode: only show warnings/errors, human-readable format
134
+ effective_level = "WARNING" if mode == "startup" else log_level
135
+
136
+ # Set up stdlib logging
137
+ logging.basicConfig(
138
+ format="%(message)s",
139
+ stream=sys.stdout,
140
+ level=getattr(logging, effective_level.upper()),
141
+ force=True, # Allow reconfiguration
142
+ )
143
+
144
+ # Suppress noisy HTTP client logs (they can leak tokens in URLs)
145
+ logging.getLogger("httpx").setLevel(logging.WARNING)
146
+ logging.getLogger("httpcore").setLevel(logging.WARNING)
147
+
148
+ # Common processors
149
+ processors: list[structlog.types.Processor] = [
150
+ structlog.stdlib.filter_by_level,
151
+ add_timestamp,
152
+ add_log_level,
153
+ add_correlation_id,
154
+ structlog.stdlib.add_logger_name,
155
+ structlog.stdlib.PositionalArgumentsFormatter(),
156
+ structlog.processors.format_exc_info,
157
+ redact_sensitive,
158
+ structlog.processors.StackInfoRenderer(),
159
+ structlog.processors.UnicodeDecoder(),
160
+ ]
161
+
162
+ # Add chain_id to all log entries if provided
163
+ if chain_id is not None:
164
+
165
+ def add_chain_id(
166
+ logger: logging.Logger, method_name: str, event_dict: dict[str, Any]
167
+ ) -> dict[str, Any]:
168
+ event_dict.setdefault("chain_id", chain_id)
169
+ return event_dict
170
+
171
+ processors.insert(3, add_chain_id)
172
+
173
+ # Choose renderer based on mode
174
+ if mode == "startup":
175
+ # Human-friendly with colors for startup warnings/errors
176
+ processors.append(structlog.dev.ConsoleRenderer(colors=True))
177
+ elif log_format == LogFormat.JSON:
178
+ processors.append(structlog.processors.JSONRenderer())
179
+ else:
180
+ processors.append(structlog.dev.ConsoleRenderer(colors=True))
181
+
182
+ structlog.configure(
183
+ processors=processors,
184
+ wrapper_class=structlog.stdlib.BoundLogger,
185
+ context_class=dict,
186
+ logger_factory=structlog.stdlib.LoggerFactory(),
187
+ cache_logger_on_first_use=False, # Allow reconfiguration
188
+ )
189
+
190
+
191
+ def set_runtime_logging() -> None:
192
+ """Switch from startup mode to runtime mode.
193
+
194
+ Call after "--- Starting brawny ---" to enable full structured logging.
195
+ """
196
+ setup_logging(_runtime_log_level, _runtime_log_format, _runtime_chain_id, mode="runtime")
197
+
198
+
199
+ def get_logger(name: str | None = None, **initial_context: Any) -> structlog.stdlib.BoundLogger:
200
+ """Get a structured logger instance.
201
+
202
+ Args:
203
+ name: Logger name (usually module name)
204
+ **initial_context: Initial context to bind to the logger
205
+
206
+ Returns:
207
+ Bound logger instance
208
+ """
209
+ logger = structlog.get_logger(name)
210
+ if initial_context:
211
+ logger = logger.bind(**initial_context)
212
+ return logger
213
+
214
+
215
+ class LogContext:
216
+ """Context manager for scoped logging context."""
217
+
218
+ def __init__(self, **context: Any) -> None:
219
+ self._context = context
220
+ self._token: Any = None
221
+
222
+ def __enter__(self) -> LogContext:
223
+ # Generate correlation ID if not present
224
+ if "correlation_id" not in self._context:
225
+ cid = generate_correlation_id()
226
+ self._context["correlation_id"] = cid
227
+ set_correlation_id(cid)
228
+ return self
229
+
230
+ def __exit__(self, *args: Any) -> None:
231
+ pass
232
+
233
+ @property
234
+ def correlation_id(self) -> str:
235
+ """Get the correlation ID for this context."""
236
+ return self._context.get("correlation_id", get_correlation_id())
237
+
238
+
239
+ # Pre-defined log event names per SPEC §11
240
+ class LogEvents:
241
+ """Standard log event names."""
242
+
243
+ # Block processing
244
+ BLOCK_INGEST_START = "block.ingest.start"
245
+ BLOCK_INGEST_DONE = "block.ingest.done"
246
+ BLOCK_REORG_DETECTED = "block.reorg.detected"
247
+ BLOCK_REORG_REWIND = "block.reorg.rewind"
248
+ BLOCK_REORG_DEEP = "block.reorg.deep"
249
+
250
+ # Job execution
251
+ JOB_CHECK_START = "job.check.start"
252
+ JOB_CHECK_SKIP = "job.check.skip"
253
+ JOB_CHECK_TRIGGERED = "job.check.triggered"
254
+ JOB_CHECK_TIMEOUT = "job.check.timeout"
255
+
256
+ # Intent lifecycle
257
+ INTENT_CREATE = "intent.create"
258
+ INTENT_DEDUPE = "intent.dedupe"
259
+ INTENT_CLAIM = "intent.claim"
260
+ INTENT_STATUS = "intent.status"
261
+ INTENT_REORG = "intent.reorg"
262
+
263
+ # Nonce management
264
+ NONCE_RESERVE = "nonce.reserve"
265
+ NONCE_RECONCILE = "nonce.reconcile"
266
+ NONCE_ORPHANED = "nonce.orphaned"
267
+
268
+ # Transaction lifecycle
269
+ TX_SIGN = "tx.sign"
270
+ TX_BROADCAST = "tx.broadcast"
271
+ TX_PENDING = "tx.pending"
272
+ TX_CONFIRMED = "tx.confirmed"
273
+ TX_FAILED = "tx.failed"
274
+ TX_REPLACED = "tx.replaced"
275
+ TX_ABANDONED = "tx.abandoned"
276
+
277
+ # Alerts
278
+ ALERT_SEND = "alert.send"
279
+ ALERT_ERROR = "alert.error"
280
+
281
+ # RPC
282
+ RPC_REQUEST = "rpc.request"
283
+ RPC_ERROR = "rpc.error"
284
+ RPC_ALL_ENDPOINTS_FAILED = "rpc.all_endpoints_failed"
285
+ RPC_CIRCUIT_BREAKER_OPEN = "rpc.circuit_breaker_open"
286
+ RPC_CIRCUIT_BREAKER_CLOSED = "rpc.circuit_breaker_closed"
287
+
288
+ # Shutdown
289
+ SHUTDOWN_INITIATED = "shutdown.initiated"
290
+ SHUTDOWN_COMPLETE = "shutdown.complete"