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/cli/helpers.py ADDED
@@ -0,0 +1,265 @@
1
+ """Shared helpers for CLI commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import keyword
7
+ import logging
8
+ import os
9
+ import re
10
+ import sys
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ import click
15
+
16
+
17
+ def suppress_logging() -> None:
18
+ """Suppress all logging output for CLI commands.
19
+
20
+ Must be called BEFORE importing any brawny modules that use logging.
21
+ """
22
+ # Suppress stdlib logging
23
+ logging.disable(logging.CRITICAL)
24
+
25
+ # Configure structlog to drop all messages
26
+ import structlog
27
+
28
+ structlog.configure(
29
+ processors=[structlog.dev.ConsoleRenderer()],
30
+ wrapper_class=structlog.stdlib.BoundLogger,
31
+ context_class=dict,
32
+ logger_factory=structlog.stdlib.LoggerFactory(),
33
+ cache_logger_on_first_use=False, # Don't cache so we can reconfigure later
34
+ )
35
+
36
+
37
+ def get_config(config_path: str | None = None):
38
+ """Get config, applying env overrides.
39
+
40
+ Args:
41
+ config_path: Path to config.yaml. If None, uses default lookup.
42
+ """
43
+ from brawny.config import Config
44
+ from brawny.config import get_config as _get_config
45
+
46
+ if config_path:
47
+ config = Config.from_yaml(config_path)
48
+ config, _ = config.apply_env_overrides()
49
+ else:
50
+ config = _get_config()
51
+
52
+ return config
53
+
54
+
55
+ def get_db(config_path: str | None = None):
56
+ """Get database connection from config.
57
+
58
+ Args:
59
+ config_path: Path to config.yaml. If None, uses get_config() default.
60
+ """
61
+ from brawny.db import create_database
62
+
63
+ config = get_config(config_path)
64
+
65
+ db = create_database(
66
+ config.database_url,
67
+ pool_size=config.database_pool_size,
68
+ pool_max_overflow=config.database_pool_max_overflow,
69
+ pool_timeout=config.database_pool_timeout_seconds,
70
+ circuit_breaker_failures=config.db_circuit_breaker_failures,
71
+ circuit_breaker_seconds=config.db_circuit_breaker_seconds,
72
+ )
73
+ db.connect()
74
+ return db
75
+
76
+
77
+ def discover_jobs_for_cli(config, extra_modules: tuple[str, ...] = ()):
78
+ """Discover jobs using same logic as brawny start.
79
+
80
+ Priority: CLI modules > auto-discovery
81
+
82
+ Note: Call suppress_logging() BEFORE this function if you want silent discovery.
83
+
84
+ Args:
85
+ config: Config instance
86
+ extra_modules: Additional modules from CLI flags
87
+ """
88
+ from brawny.jobs.discovery import auto_discover_jobs, discover_jobs
89
+
90
+ if extra_modules:
91
+ discover_jobs(list(extra_modules))
92
+ else:
93
+ auto_discover_jobs()
94
+
95
+
96
+ def print_json(data: Any) -> None:
97
+ """Print data as formatted JSON."""
98
+ click.echo(json.dumps(data, indent=2, default=str))
99
+
100
+
101
+ def _validate_project_name(name: str) -> str:
102
+ """Validate and normalize project name."""
103
+ if not name or not name.strip():
104
+ raise click.ClickException("Project name cannot be empty")
105
+
106
+ stripped = name.strip()
107
+ if len(stripped) > 100:
108
+ raise click.ClickException("Project name must be 100 characters or less")
109
+ if not stripped[0].isalpha():
110
+ raise click.ClickException("Project name must start with a letter")
111
+ if not re.match(r"^[A-Za-z0-9_-]+$", stripped):
112
+ raise click.ClickException(
113
+ "Project name contains invalid characters. Use only letters, numbers, hyphens, and underscores."
114
+ )
115
+
116
+ package_name = stripped.lower().replace("-", "_")
117
+ if keyword.iskeyword(package_name):
118
+ raise click.ClickException(f"Project name '{name}' is a Python keyword")
119
+
120
+ stdlib_names = getattr(sys, "stdlib_module_names", set())
121
+ if package_name in stdlib_names:
122
+ raise click.ClickException(
123
+ f"Project name '{name}' conflicts with Python standard library"
124
+ )
125
+
126
+ return package_name
127
+
128
+
129
+ def _check_directory_empty(project_dir: Path) -> None:
130
+ """Verify target directory is empty (or has only ignorable files)."""
131
+ if not project_dir.exists():
132
+ raise click.ClickException(f"Directory {project_dir} does not exist")
133
+
134
+ if not project_dir.is_dir():
135
+ raise click.ClickException(f"{project_dir} is not a directory")
136
+
137
+ if not os.access(project_dir, os.W_OK):
138
+ raise click.ClickException(f"Cannot write to {project_dir}: permission denied")
139
+
140
+ ignorable = {
141
+ ".git", ".gitignore", "README.md", "LICENSE", ".DS_Store",
142
+ "venv", ".venv", "env", ".env",
143
+ ".idea", ".vscode",
144
+ }
145
+ existing = set(p.name for p in project_dir.iterdir())
146
+ non_ignorable = existing - ignorable
147
+
148
+ if non_ignorable:
149
+ raise click.ClickException(
150
+ f"Directory is not empty. Found: {', '.join(sorted(non_ignorable))}\n"
151
+ "Run 'brawny init' in an empty directory."
152
+ )
153
+
154
+
155
+ def _write_file(path: Path, content: str) -> None:
156
+ """Write content to file, creating parent directories if needed."""
157
+ try:
158
+ path.parent.mkdir(parents=True, exist_ok=True)
159
+ path.write_text(content, encoding="utf-8")
160
+ except OSError as exc:
161
+ raise click.ClickException(f"Failed to create {path}: {exc}") from exc
162
+
163
+
164
+ def _write_pyproject(project_dir: Path, project_name: str, package_name: str) -> None:
165
+ from brawny.cli_templates import PYPROJECT_TEMPLATE
166
+
167
+ content = PYPROJECT_TEMPLATE.format(
168
+ project_name=project_name,
169
+ package_name=package_name,
170
+ )
171
+ _write_file(project_dir / "pyproject.toml", content)
172
+
173
+
174
+ def _write_config(project_dir: Path, package_name: str) -> None:
175
+ from brawny.cli_templates import CONFIG_TEMPLATE
176
+
177
+ content = CONFIG_TEMPLATE.format(package_name=package_name)
178
+ _write_file(project_dir / "config.yaml", content)
179
+
180
+
181
+ def _write_env_example(project_dir: Path) -> None:
182
+ from brawny.cli_templates import ENV_EXAMPLE_TEMPLATE
183
+
184
+ _write_file(project_dir / ".env.example", ENV_EXAMPLE_TEMPLATE)
185
+
186
+ def _write_agents(project_dir: Path) -> None:
187
+ from brawny.cli_templates import AGENTS_TEMPLATE
188
+
189
+ _write_file(project_dir / "AGENTS.md", AGENTS_TEMPLATE)
190
+
191
+
192
+ def _write_gitignore(project_dir: Path) -> None:
193
+ from brawny.cli_templates import GITIGNORE_TEMPLATE
194
+
195
+ _write_file(project_dir / ".gitignore", GITIGNORE_TEMPLATE)
196
+
197
+
198
+ def _write_jobs_init(jobs_dir: Path) -> None:
199
+ from brawny.cli_templates import INIT_JOBS_TEMPLATE
200
+
201
+ _write_file(jobs_dir / "__init__.py", INIT_JOBS_TEMPLATE)
202
+
203
+
204
+ def _write_examples(path: Path) -> None:
205
+ from brawny.cli_templates import EXAMPLES_TEMPLATE
206
+
207
+ _write_file(path, EXAMPLES_TEMPLATE)
208
+
209
+
210
+ def _write_monitoring(project_dir: Path) -> None:
211
+ from brawny.cli_templates import (
212
+ DOCKER_COMPOSE_MONITORING_TEMPLATE,
213
+ PROMETHEUS_CONFIG_TEMPLATE,
214
+ GRAFANA_DATASOURCE_TEMPLATE,
215
+ GRAFANA_DASHBOARDS_PROVIDER_TEMPLATE,
216
+ GRAFANA_DASHBOARD_TEMPLATE,
217
+ )
218
+
219
+ monitoring_dir = project_dir / "monitoring"
220
+ _write_file(monitoring_dir / "docker-compose.yml", DOCKER_COMPOSE_MONITORING_TEMPLATE)
221
+ _write_file(monitoring_dir / "prometheus.yml", PROMETHEUS_CONFIG_TEMPLATE)
222
+ _write_file(
223
+ monitoring_dir / "grafana" / "provisioning" / "datasources" / "datasource.yml",
224
+ GRAFANA_DATASOURCE_TEMPLATE,
225
+ )
226
+ _write_file(
227
+ monitoring_dir / "grafana" / "provisioning" / "dashboards" / "dashboards.yml",
228
+ GRAFANA_DASHBOARDS_PROVIDER_TEMPLATE,
229
+ )
230
+ _write_file(
231
+ monitoring_dir / "grafana" / "provisioning" / "dashboards" / "brawny-overview.json",
232
+ GRAFANA_DASHBOARD_TEMPLATE,
233
+ )
234
+
235
+
236
+ def _print_success(project_name: str, package_name: str) -> None:
237
+ """Print success message with next steps."""
238
+ click.echo()
239
+ click.echo(click.style(f"Initialized {project_name}", fg="green", bold=True))
240
+ click.echo()
241
+ click.echo("Project structure:")
242
+ click.echo(" .")
243
+ click.echo(" ├── pyproject.toml")
244
+ click.echo(" ├── config.yaml")
245
+ click.echo(" ├── .env.example")
246
+ click.echo(" ├── .gitignore")
247
+ click.echo(f" ├── {package_name}/")
248
+ click.echo(" │ └── __init__.py")
249
+ click.echo(" ├── jobs/")
250
+ click.echo(" │ ├── __init__.py")
251
+ click.echo(" │ └── _examples.py")
252
+ click.echo(" ├── interfaces/")
253
+ click.echo(" ├── data/ # SQLite database")
254
+ click.echo(" └── monitoring/ # Prometheus + Grafana")
255
+ click.echo()
256
+ click.echo("Next steps:")
257
+ click.echo(click.style(" pip install -e .", fg="cyan"))
258
+ click.echo(click.style(" cp .env.example .env", fg="cyan"))
259
+ click.echo(" # Edit .env with your RPC URL and signer keys")
260
+ click.echo(click.style(" brawny run", fg="cyan"))
261
+ click.echo()
262
+ click.echo("To create your first job:")
263
+ click.echo(" 1. Copy a class from jobs/_examples.py")
264
+ click.echo(" 2. Create a new file in jobs/")
265
+ click.echo(" 3. Add @job decorator and customize")