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
@@ -0,0 +1,246 @@
1
+ """Configuration validation for brawny.
2
+
3
+ Provides validation logic for config values and endpoint canonicalization.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from urllib.parse import urlparse
9
+
10
+ from brawny.model.enums import KeystoreType
11
+ from brawny.model.errors import ConfigError
12
+
13
+
14
+ class InvalidEndpointError(Exception):
15
+ """Raised when an endpoint URL is malformed."""
16
+
17
+ pass
18
+
19
+
20
+ def canonicalize_endpoint(url: str) -> str:
21
+ """Normalize endpoint URL for consistent comparison.
22
+
23
+ Rules:
24
+ - Strip whitespace
25
+ - Require http or https scheme
26
+ - Require non-empty hostname
27
+ - Lowercase scheme and hostname
28
+ - Remove default ports (80/443)
29
+ - Remove trailing slash (except root)
30
+ - Drop query string (RPC endpoints don't use them)
31
+
32
+ Raises:
33
+ InvalidEndpointError: If URL is missing scheme or hostname
34
+ """
35
+ url = url.strip()
36
+ if not url:
37
+ raise InvalidEndpointError("Empty endpoint URL")
38
+
39
+ parsed = urlparse(url)
40
+ scheme = parsed.scheme.lower()
41
+
42
+ # Validate scheme
43
+ if scheme not in {"http", "https"}:
44
+ raise InvalidEndpointError(
45
+ f"Invalid endpoint '{url}': scheme must be http or https, got '{scheme or '(none)'}'"
46
+ )
47
+
48
+ # Validate hostname
49
+ hostname = (parsed.hostname or "").lower()
50
+ if not hostname:
51
+ raise InvalidEndpointError(f"Invalid endpoint '{url}': missing hostname")
52
+
53
+ port = parsed.port
54
+ if (scheme == "https" and port == 443) or (scheme == "http" and port == 80):
55
+ port = None
56
+
57
+ # Preserve Basic Auth credentials if present (e.g., https://user:pass@host)
58
+ userinfo = ""
59
+ if parsed.username:
60
+ userinfo = parsed.username
61
+ if parsed.password:
62
+ userinfo += f":{parsed.password}"
63
+ userinfo += "@"
64
+
65
+ netloc = f"{userinfo}{hostname}:{port}" if port else f"{userinfo}{hostname}"
66
+ path = parsed.path.rstrip("/") if len(parsed.path) > 1 else ""
67
+
68
+ # Drop query string — RPC endpoints don't use them, and they cause
69
+ # accidental uniqueness (e.g., tracking params, cache busters)
70
+ return f"{scheme}://{netloc}{path}"
71
+
72
+
73
+ def dedupe_preserve_order(endpoints: list[str]) -> list[str]:
74
+ """Remove duplicates while preserving order."""
75
+ seen: set[str] = set()
76
+ result: list[str] = []
77
+ for ep in endpoints:
78
+ if ep not in seen:
79
+ seen.add(ep)
80
+ result.append(ep)
81
+ return result
82
+
83
+
84
+ REMOVED_FIELDS = {
85
+ "alerts_dx_enabled",
86
+ "allowed_signers",
87
+ "broadcast_rpc",
88
+ "broadcast_url",
89
+ "rpc_endpoints",
90
+ "deep_reorg_alert_enabled",
91
+ "etherscan_api_key",
92
+ "etherscan_api_url",
93
+ "job_modules",
94
+ "jobs_path",
95
+ "log_level",
96
+ "log_format",
97
+ "metrics_enabled",
98
+ "metrics_bind",
99
+ "networks",
100
+ "signers",
101
+ "sourcify_enabled",
102
+ "strict_job_validation",
103
+ "telegram_chat_ids",
104
+ "webhook_url",
105
+ "block_hash_history_size",
106
+ "brownie_password_fallback",
107
+ "claim_timeout_seconds",
108
+ "db_circuit_breaker_failures",
109
+ "db_circuit_breaker_seconds",
110
+ "deep_reorg_pause",
111
+ "intent_retry_backoff_seconds",
112
+ "rpc_circuit_breaker_seconds",
113
+ "shutdown_grace_seconds",
114
+ "shutdown_timeout_seconds",
115
+ }
116
+
117
+
118
+ def validate_no_removed_fields(raw_config: dict) -> None:
119
+ """Fail fast if removed config options are present."""
120
+ forbidden: set[str] = set()
121
+ for key in raw_config.keys():
122
+ if key in REMOVED_FIELDS:
123
+ forbidden.add(key)
124
+
125
+ advanced = raw_config.get("advanced")
126
+ if isinstance(advanced, dict):
127
+ for key in advanced.keys():
128
+ if key in REMOVED_FIELDS:
129
+ forbidden.add(key)
130
+
131
+ if forbidden:
132
+ raise ConfigError(
133
+ "Removed config options detected: "
134
+ f"{sorted(forbidden)}. These options no longer exist."
135
+ )
136
+
137
+
138
+ def validate_config(config: "Config") -> None:
139
+ """Validate all configuration values.
140
+
141
+ Args:
142
+ config: Config instance to validate
143
+
144
+ Raises:
145
+ ConfigError: If validation fails
146
+ """
147
+ from brawny.config.models import Config
148
+
149
+ errors: list[str] = []
150
+
151
+ # Required fields
152
+ if not config.database_url:
153
+ errors.append("database_url is required")
154
+ elif not (
155
+ config.database_url.startswith("postgresql://")
156
+ or config.database_url.startswith("postgres://")
157
+ or config.database_url.startswith("sqlite:///")
158
+ ):
159
+ errors.append(
160
+ "database_url must start with postgresql://, postgres://, or sqlite:///"
161
+ )
162
+ elif config.database_url.startswith("sqlite:///") and config.worker_count > 1:
163
+ errors.append("SQLite does not support worker_count > 1. Use Postgres for production.")
164
+
165
+ if not config.rpc_groups:
166
+ errors.append("rpc_groups is required (at least one group)")
167
+
168
+ if config.chain_id <= 0:
169
+ errors.append("chain_id must be positive")
170
+
171
+ # RPC Groups validation
172
+ if config.rpc_groups:
173
+ # All groups must have at least one endpoint
174
+ for name, group in config.rpc_groups.items():
175
+ if not group.endpoints:
176
+ errors.append(f"rpc_groups.{name} has no endpoints")
177
+
178
+ if config.rpc_default_group:
179
+ if config.rpc_default_group not in config.rpc_groups:
180
+ errors.append(
181
+ f"rpc_default_group '{config.rpc_default_group}' not found in rpc_groups"
182
+ )
183
+ elif len(config.rpc_groups) > 1:
184
+ errors.append(
185
+ "Multiple rpc_groups configured; set rpc_default_group to avoid ambiguity"
186
+ )
187
+
188
+ if config.worker_count <= 0:
189
+ errors.append("worker_count must be positive")
190
+
191
+ # Keystore validation
192
+ if config.keystore_type == KeystoreType.FILE and not config.keystore_path:
193
+ errors.append("keystore_path is required when keystore_type is 'file'")
194
+
195
+ if errors:
196
+ raise ConfigError(
197
+ "Configuration validation failed:\n" + "\n".join(f" - {e}" for e in errors)
198
+ )
199
+
200
+
201
+ def validate_advanced_config(advanced: "AdvancedConfig") -> None:
202
+ """Validate advanced configuration values."""
203
+ from brawny.config.models import AdvancedConfig
204
+
205
+ if not isinstance(advanced, AdvancedConfig):
206
+ raise ConfigError("advanced config must be an AdvancedConfig instance")
207
+
208
+ errors: list[str] = []
209
+
210
+ if advanced.poll_interval_seconds <= 0:
211
+ errors.append("poll_interval_seconds must be positive")
212
+ if advanced.reorg_depth <= 0:
213
+ errors.append("reorg_depth must be positive")
214
+ if advanced.finality_confirmations < 0:
215
+ errors.append("finality_confirmations must be non-negative")
216
+ if advanced.default_deadline_seconds <= 0:
217
+ errors.append("default_deadline_seconds must be positive")
218
+ if advanced.stuck_tx_seconds <= 0:
219
+ errors.append("stuck_tx_seconds must be positive")
220
+ if advanced.max_replacement_attempts < 0:
221
+ errors.append("max_replacement_attempts cannot be negative")
222
+
223
+ if advanced.gas_limit_multiplier < 1.0:
224
+ errors.append("gas_limit_multiplier must be at least 1.0")
225
+ if advanced.default_priority_fee_gwei < 0:
226
+ errors.append("default_priority_fee_gwei must be non-negative")
227
+ if advanced.max_fee_cap_gwei is not None and advanced.max_fee_cap_gwei <= 0:
228
+ errors.append("max_fee_cap_gwei must be positive when set")
229
+ if advanced.fee_bump_percent < 10:
230
+ errors.append("fee_bump_percent must be at least 10 (Ethereum protocol minimum)")
231
+
232
+ if advanced.rpc_timeout_seconds <= 0:
233
+ errors.append("rpc_timeout_seconds must be positive")
234
+ if advanced.rpc_max_retries < 0:
235
+ errors.append("rpc_max_retries cannot be negative")
236
+
237
+ if advanced.database_pool_size <= 0:
238
+ errors.append("database_pool_size must be positive")
239
+ if advanced.database_pool_max_overflow < 0:
240
+ errors.append("database_pool_max_overflow cannot be negative")
241
+
242
+ if errors:
243
+ raise ConfigError(
244
+ "Advanced configuration validation failed:\n"
245
+ + "\n".join(f" - {e}" for e in errors)
246
+ )
@@ -0,0 +1,14 @@
1
+ """Daemon orchestration for brawny.
2
+
3
+ Provides the main BrawnyDaemon class for running the job executor.
4
+ """
5
+
6
+ from brawny.daemon.context import DaemonContext, DaemonState, RuntimeOverrides
7
+ from brawny.daemon.core import BrawnyDaemon
8
+
9
+ __all__ = [
10
+ "BrawnyDaemon",
11
+ "DaemonContext",
12
+ "DaemonState",
13
+ "RuntimeOverrides",
14
+ ]
@@ -0,0 +1,69 @@
1
+ """Daemon context and state for brawny.
2
+
3
+ Provides DaemonContext (shared component references) and DaemonState (loop callbacks).
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass
9
+ from logging import Logger
10
+ from typing import TYPE_CHECKING, Callable
11
+
12
+ if TYPE_CHECKING:
13
+ from brawny.config import Config
14
+ from brawny.db.base import Database
15
+ from brawny._rpc.manager import RPCManager
16
+ from brawny.tx.executor import TxExecutor
17
+ from brawny.tx.monitor import TxMonitor
18
+ from brawny.tx.replacement import TxReplacer
19
+ from brawny.tx.nonce import NonceManager
20
+
21
+
22
+ @dataclass
23
+ class DaemonContext:
24
+ """Shared context for daemon loops.
25
+
26
+ Contains references to all components needed by worker and monitor loops.
27
+ """
28
+
29
+ config: "Config"
30
+ log: Logger
31
+ db: "Database"
32
+ rpc: "RPCManager"
33
+ executor: "TxExecutor | None"
34
+ monitor: "TxMonitor | None"
35
+ replacer: "TxReplacer | None"
36
+ nonce_manager: "NonceManager | None"
37
+ chain_id: int
38
+
39
+ # Health alerts (optional - None means disabled)
40
+ health_send_fn: Callable[..., None] | None = None
41
+ health_chat_id: str | None = None
42
+ health_cooldown: int = 1800
43
+
44
+
45
+ @dataclass
46
+ class DaemonState:
47
+ """State callbacks for daemon loops.
48
+
49
+ Provides callbacks to track inflight operations and generate claim tokens.
50
+ Keeps loops decoupled from daemon internals.
51
+ """
52
+
53
+ make_claim_token: Callable[[int], str]
54
+ make_claimed_by: Callable[[int], str]
55
+ inflight_inc: Callable[[], None]
56
+ inflight_dec: Callable[[], None]
57
+
58
+
59
+ @dataclass
60
+ class RuntimeOverrides:
61
+ """Runtime overrides for daemon configuration.
62
+
63
+ Allows CLI and programmatic callers to override config values.
64
+ """
65
+
66
+ dry_run: bool = False
67
+ once: bool = False
68
+ worker_count: int | None = None
69
+ strict_validation: bool = True