beamflow-cli 0.3.0__tar.gz → 0.3.2__tar.gz
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.
- {beamflow_cli-0.3.0 → beamflow_cli-0.3.2}/PKG-INFO +2 -5
- {beamflow_cli-0.3.0 → beamflow_cli-0.3.2}/beamflow/commands/project.py +108 -4
- {beamflow_cli-0.3.0 → beamflow_cli-0.3.2}/beamflow/commands/run.py +1 -1
- beamflow_cli-0.3.2/beamflow/core/auth_server.py +45 -0
- {beamflow_cli-0.3.0 → beamflow_cli-0.3.2}/beamflow/main.py +28 -2
- beamflow_cli-0.3.2/beamflow/templates/README.md +135 -0
- beamflow_cli-0.3.2/beamflow/templates/_deployment/[env]/(backend-asyncio)/docker-compose.yaml +24 -0
- {beamflow_cli-0.3.0 → beamflow_cli-0.3.2}/beamflow/templates/_deployment/[env]/(backend-dramatiq)/docker-compose.yaml +4 -4
- {beamflow_cli-0.3.0 → beamflow_cli-0.3.2}/pyproject.toml +2 -5
- beamflow_cli-0.3.0/beamflow/core/auth_server.py +0 -36
- beamflow_cli-0.3.0/beamflow/templates/_README.md +0 -28
- beamflow_cli-0.3.0/beamflow/templates/tests/_test_config_loading.py +0 -10
- beamflow_cli-0.3.0/beamflow/templates/tests/_test_integration.py +0 -20
- {beamflow_cli-0.3.0 → beamflow_cli-0.3.2}/beamflow/__init__.py +0 -0
- {beamflow_cli-0.3.0 → beamflow_cli-0.3.2}/beamflow/commands/__init__.py +0 -0
- {beamflow_cli-0.3.0 → beamflow_cli-0.3.2}/beamflow/commands/auth.py +0 -0
- {beamflow_cli-0.3.0 → beamflow_cli-0.3.2}/beamflow/commands/build.py +0 -0
- {beamflow_cli-0.3.0 → beamflow_cli-0.3.2}/beamflow/commands/deploy.py +0 -0
- {beamflow_cli-0.3.0 → beamflow_cli-0.3.2}/beamflow/core/__init__.py +0 -0
- {beamflow_cli-0.3.0 → beamflow_cli-0.3.2}/beamflow/core/api_client.py +0 -0
- {beamflow_cli-0.3.0 → beamflow_cli-0.3.2}/beamflow/core/builder.py +0 -0
- {beamflow_cli-0.3.0 → beamflow_cli-0.3.2}/beamflow/core/config.py +0 -0
- {beamflow_cli-0.3.0 → beamflow_cli-0.3.2}/beamflow/core/docker_utils.py +0 -0
- {beamflow_cli-0.3.0 → beamflow_cli-0.3.2}/beamflow/templates/_.beamflow +0 -0
- {beamflow_cli-0.3.0 → beamflow_cli-0.3.2}/beamflow/templates/_.dockerignore +0 -0
- {beamflow_cli-0.3.0 → beamflow_cli-0.3.2}/beamflow/templates/_api_main.py +0 -0
- {beamflow_cli-0.3.0 → beamflow_cli-0.3.2}/beamflow/templates/_config/[env]/(backend-asyncio)/backend.yaml +0 -0
- {beamflow_cli-0.3.0 → beamflow_cli-0.3.2}/beamflow/templates/_config/[env]/(backend-dramatiq)/backend.yaml +0 -0
- {beamflow_cli-0.3.0 → beamflow_cli-0.3.2}/beamflow/templates/_config/[env]/(backend-managed)/backend.yaml +0 -0
- {beamflow_cli-0.3.0 → beamflow_cli-0.3.2}/beamflow/templates/_config/[env]/_backend.yaml +0 -0
- {beamflow_cli-0.3.0/beamflow/templates/_deployment/shared → beamflow_cli-0.3.2/beamflow/templates/_config}/clients/demoClient.yaml +0 -0
- {beamflow_cli-0.3.0 → beamflow_cli-0.3.2}/beamflow/templates/_config/shared/backend.yaml +0 -0
- {beamflow_cli-0.3.0 → beamflow_cli-0.3.2}/beamflow/templates/_deployment/[env]/.env +0 -0
- {beamflow_cli-0.3.0 → beamflow_cli-0.3.2}/beamflow/templates/_deployment/shared/.env +0 -0
- {beamflow_cli-0.3.0 → beamflow_cli-0.3.2}/beamflow/templates/_deployment/shared/api.Dockerfile +0 -0
- {beamflow_cli-0.3.0 → beamflow_cli-0.3.2}/beamflow/templates/_deployment/shared/worker.Dockerfile +0 -0
- {beamflow_cli-0.3.0 → beamflow_cli-0.3.2}/beamflow/templates/_pyproject.toml +0 -0
- {beamflow_cli-0.3.0 → beamflow_cli-0.3.2}/beamflow/templates/_src/_api/__init__.py +0 -0
- {beamflow_cli-0.3.0 → beamflow_cli-0.3.2}/beamflow/templates/_src/_api/_routes/webhooks.py +0 -0
- {beamflow_cli-0.3.0 → beamflow_cli-0.3.2}/beamflow/templates/_src/_shared/__init__.py +0 -0
- {beamflow_cli-0.3.0 → beamflow_cli-0.3.2}/beamflow/templates/_src/_shared/clients/client.py +0 -0
- {beamflow_cli-0.3.0 → beamflow_cli-0.3.2}/beamflow/templates/_src/_shared/models/models.py +0 -0
- {beamflow_cli-0.3.0 → beamflow_cli-0.3.2}/beamflow/templates/_src/_shared/tasks/sharedTasks.py +0 -0
- {beamflow_cli-0.3.0 → beamflow_cli-0.3.2}/beamflow/templates/_src/_worker/__init__.py +0 -0
- {beamflow_cli-0.3.0 → beamflow_cli-0.3.2}/beamflow/templates/_src/_worker/tasks/tasks.py +0 -0
- {beamflow_cli-0.3.0 → beamflow_cli-0.3.2}/beamflow/templates/_worker_main.py +0 -0
- {beamflow_cli-0.3.0 → beamflow_cli-0.3.2}/beamflow/ui/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: beamflow-cli
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: CLI for the Beamflow Managed Platform
|
|
5
5
|
Author: juraj.bezdek@gmail.com
|
|
6
6
|
Author-email: juraj.bezdek@gmail.com
|
|
@@ -9,14 +9,11 @@ Classifier: Programming Language :: Python :: 3
|
|
|
9
9
|
Classifier: Programming Language :: Python :: 3.11
|
|
10
10
|
Classifier: Programming Language :: Python :: 3.12
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
-
Requires-Dist: beamflow-runtime (>=0.3.0,<0.
|
|
13
|
-
Requires-Dist: fastapi (>=0.110.0)
|
|
12
|
+
Requires-Dist: beamflow-runtime (>=0.3.0,<0.3.2)
|
|
14
13
|
Requires-Dist: httpx (>=0.27.0)
|
|
15
14
|
Requires-Dist: keyring (>=25.0.0)
|
|
16
15
|
Requires-Dist: pydantic (>=2.0.0)
|
|
17
|
-
Requires-Dist: python-dotenv (>=1.0.1)
|
|
18
16
|
Requires-Dist: pyyaml (>=6.0.1)
|
|
19
17
|
Requires-Dist: questionary (>=2.0.1)
|
|
20
18
|
Requires-Dist: rich (>=13.7.0)
|
|
21
19
|
Requires-Dist: typer[all] (>=0.12.0)
|
|
22
|
-
Requires-Dist: uvicorn (>=0.29.0)
|
|
@@ -142,6 +142,10 @@ def process_node(
|
|
|
142
142
|
try:
|
|
143
143
|
content = src.read_text()
|
|
144
144
|
content = content.replace("{project_name}", project_name)
|
|
145
|
+
project_name_slug = project_name.lower().replace("_", "-").strip("-")
|
|
146
|
+
if not project_name_slug:
|
|
147
|
+
project_name_slug = "beamflow-project"
|
|
148
|
+
content = content.replace("{project_name_slug}", project_name_slug)
|
|
145
149
|
if env_mode:
|
|
146
150
|
content = content.replace("{env}", env_mode.name)
|
|
147
151
|
content = content.replace("[env]", env_mode.name)
|
|
@@ -238,6 +242,89 @@ def init_env(env_name: str) -> EnvironmentMode:
|
|
|
238
242
|
is_managed = (answers.get("backend") == "managed") or (answers.get("observability") == "managed")
|
|
239
243
|
return EnvironmentMode(name=env_name, managed=is_managed, options=answers)
|
|
240
244
|
|
|
245
|
+
def init_vscode_launch(env_name: str, force: bool = False):
|
|
246
|
+
"""Initialize VS Code launch settings for a specific environment."""
|
|
247
|
+
root = find_project_root()
|
|
248
|
+
if not root:
|
|
249
|
+
console.print("[red]Not in a Beamflow project. Run 'beamflow init' first.[/red]")
|
|
250
|
+
raise typer.Exit(code=1)
|
|
251
|
+
|
|
252
|
+
project_config = load_project_config()
|
|
253
|
+
if not project_config:
|
|
254
|
+
console.print("[red]Invalid project configuration.[/red]")
|
|
255
|
+
raise typer.Exit(code=1)
|
|
256
|
+
|
|
257
|
+
# Find the environment
|
|
258
|
+
env_to_init = next((e for e in project_config.environments if e.name == env_name), None)
|
|
259
|
+
if not env_to_init:
|
|
260
|
+
console.print(f"[red]Environment '{env_name}' not found.[/red]")
|
|
261
|
+
raise typer.Exit(code=1)
|
|
262
|
+
|
|
263
|
+
if env_to_init.managed:
|
|
264
|
+
console.print(f"[red]Cannot initialize VS Code launch for managed environment '{env_name}'.[/red]")
|
|
265
|
+
raise typer.Exit(code=1)
|
|
266
|
+
|
|
267
|
+
vscode_dir = root / ".vscode"
|
|
268
|
+
vscode_dir.mkdir(parents=True, exist_ok=True)
|
|
269
|
+
launch_json_path = vscode_dir / "launch.json"
|
|
270
|
+
|
|
271
|
+
import json
|
|
272
|
+
|
|
273
|
+
launch_data = {"version": "0.2.0", "configurations": []}
|
|
274
|
+
if launch_json_path.exists():
|
|
275
|
+
try:
|
|
276
|
+
with open(launch_json_path, "r") as f:
|
|
277
|
+
launch_data = json.load(f)
|
|
278
|
+
except json.JSONDecodeError:
|
|
279
|
+
# Maybe there are comments or it's malformed. We'll ask to overwrite.
|
|
280
|
+
if not force:
|
|
281
|
+
console.print(f"[yellow]Warning: {launch_json_path} contains invalid JSON or comments. Cannot parse it automatically. Use --force to overwrite it entirely.[/yellow]")
|
|
282
|
+
raise typer.Exit(code=1)
|
|
283
|
+
launch_data = {"version": "0.2.0", "configurations": []}
|
|
284
|
+
|
|
285
|
+
api_config_name = f"Launch {env_name} API"
|
|
286
|
+
worker_config_name = f"Launch {env_name} Worker"
|
|
287
|
+
|
|
288
|
+
existing_configs = {cfg.get("name") for cfg in launch_data.get("configurations", [])}
|
|
289
|
+
|
|
290
|
+
added = False
|
|
291
|
+
|
|
292
|
+
if api_config_name not in existing_configs or force:
|
|
293
|
+
# Remove old if force
|
|
294
|
+
launch_data["configurations"] = [c for c in launch_data.setdefault("configurations", []) if c.get("name") != api_config_name]
|
|
295
|
+
|
|
296
|
+
launch_data["configurations"].append({
|
|
297
|
+
"name": api_config_name,
|
|
298
|
+
"type": "debugpy",
|
|
299
|
+
"request": "launch",
|
|
300
|
+
"program": "${workspaceFolder}/api_main.py",
|
|
301
|
+
"console": "integratedTerminal",
|
|
302
|
+
"env": {"ENVIRONMENT": env_name}
|
|
303
|
+
})
|
|
304
|
+
added = True
|
|
305
|
+
|
|
306
|
+
if worker_config_name not in existing_configs or force:
|
|
307
|
+
# Remove old if force
|
|
308
|
+
launch_data["configurations"] = [c for c in launch_data.setdefault("configurations", []) if c.get("name") != worker_config_name]
|
|
309
|
+
|
|
310
|
+
launch_data["configurations"].append({
|
|
311
|
+
"name": worker_config_name,
|
|
312
|
+
"type": "debugpy",
|
|
313
|
+
"request": "launch",
|
|
314
|
+
"program": "${workspaceFolder}/worker_main.py",
|
|
315
|
+
"console": "integratedTerminal",
|
|
316
|
+
"env": {"ENVIRONMENT": env_name}
|
|
317
|
+
})
|
|
318
|
+
added = True
|
|
319
|
+
|
|
320
|
+
if added:
|
|
321
|
+
with open(launch_json_path, "w") as f:
|
|
322
|
+
json.dump(launch_data, f, indent=4)
|
|
323
|
+
console.print(f"[green]Added VS Code launch configurations for '{env_name}' to {launch_json_path}[/green]")
|
|
324
|
+
else:
|
|
325
|
+
console.print(f"VS Code launch configurations already exist for '{env_name}'.")
|
|
326
|
+
|
|
327
|
+
|
|
241
328
|
|
|
242
329
|
def add_environment(project_config: ProjectConfig, env_name: Optional[str] = None):
|
|
243
330
|
if not env_name:
|
|
@@ -266,11 +353,12 @@ def add_environment(project_config: ProjectConfig, env_name: Optional[str] = Non
|
|
|
266
353
|
|
|
267
354
|
if not env_name or any(e.name == env_name for e in project_config.environments):
|
|
268
355
|
console.print("[red]Environment already exists or invalid name![/red]")
|
|
269
|
-
return
|
|
356
|
+
return None
|
|
270
357
|
|
|
271
358
|
mode = init_env(env_name)
|
|
272
359
|
project_config.environments.append(mode)
|
|
273
360
|
save_project_config(project_config)
|
|
361
|
+
return mode
|
|
274
362
|
|
|
275
363
|
|
|
276
364
|
@app.command("add")
|
|
@@ -289,11 +377,16 @@ def env_add(
|
|
|
289
377
|
console.print("[red]Invalid project configuration.[/red]")
|
|
290
378
|
raise typer.Exit(code=1)
|
|
291
379
|
|
|
292
|
-
add_environment(project_config, env_name=env_name)
|
|
380
|
+
added_mode = add_environment(project_config, env_name=env_name)
|
|
381
|
+
if not added_mode:
|
|
382
|
+
return
|
|
293
383
|
|
|
294
384
|
# Reload config to get the newly added environment
|
|
295
385
|
project_config = load_project_config()
|
|
296
386
|
|
|
387
|
+
# We now know the actual environment name picked
|
|
388
|
+
env_name = added_mode.name
|
|
389
|
+
|
|
297
390
|
# Check if we should ask for linkage
|
|
298
391
|
is_managed = any(e.managed for e in project_config.environments)
|
|
299
392
|
if not project_config.project_id and is_managed:
|
|
@@ -309,6 +402,10 @@ def env_add(
|
|
|
309
402
|
process_node(child, root, None, is_fix=True, is_check=False, force=force, project_name=project_name, root_path=root, errors=errors, target_env=env_name)
|
|
310
403
|
|
|
311
404
|
console.print(f"[green]Environment configuration updated![/green]")
|
|
405
|
+
|
|
406
|
+
if not added_mode.managed:
|
|
407
|
+
if Prompt.ask("Would you like to initialize VS Code launch configurations for this environment?", choices=["y", "n"], default="y") == "y":
|
|
408
|
+
init_vscode_launch(env_name=env_name, force=force)
|
|
312
409
|
|
|
313
410
|
|
|
314
411
|
@app.command("delete")
|
|
@@ -388,11 +485,18 @@ def init(
|
|
|
388
485
|
# Ask to configure at least the first environment
|
|
389
486
|
setup_first = Prompt.ask("Setup first eniroment? y/n?", choices=["y", "n"], default="y")
|
|
390
487
|
if setup_first == "y":
|
|
391
|
-
add_environment(project_config)
|
|
488
|
+
added_mode = add_environment(project_config)
|
|
489
|
+
if added_mode and not added_mode.managed:
|
|
490
|
+
if Prompt.ask(f"Would you like to initialize VS Code launch configurations for {added_mode.name}?", choices=["y", "n"], default="y") == "y":
|
|
491
|
+
init_vscode_launch(env_name=added_mode.name, force=force)
|
|
392
492
|
else:
|
|
393
493
|
if not fix:
|
|
394
494
|
while Prompt.ask("Add another enviroment? y/n?", choices=["y", "n"], default="n") == "y":
|
|
395
|
-
add_environment(project_config)
|
|
495
|
+
added_mode = add_environment(project_config)
|
|
496
|
+
if added_mode and not added_mode.managed:
|
|
497
|
+
if Prompt.ask(f"Would you like to initialize VS Code launch configurations for {added_mode.name}?", choices=["y", "n"], default="y") == "y":
|
|
498
|
+
init_vscode_launch(env_name=added_mode.name, force=force)
|
|
499
|
+
|
|
396
500
|
|
|
397
501
|
templates_dir = Path(__file__).parent.parent / "templates"
|
|
398
502
|
errors = []
|
|
@@ -12,7 +12,7 @@ console = Console()
|
|
|
12
12
|
@app.command()
|
|
13
13
|
def run(
|
|
14
14
|
env: str = typer.Argument(..., help="Environment to run"),
|
|
15
|
-
build: bool = typer.Option(
|
|
15
|
+
build: bool = typer.Option(True, "--build/--no-build", help="Build images before starting"),
|
|
16
16
|
detach: bool = typer.Option(False, "--detach", "-d", help="Run in background"),
|
|
17
17
|
logs: bool = typer.Option(False, "--logs", help="Follow logs (useful with --detach)")
|
|
18
18
|
):
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from typing import Dict, Optional
|
|
2
|
+
import threading
|
|
3
|
+
import queue
|
|
4
|
+
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
5
|
+
from urllib.parse import urlparse, parse_qs
|
|
6
|
+
|
|
7
|
+
result_queue = queue.Queue()
|
|
8
|
+
|
|
9
|
+
class CallbackHandler(BaseHTTPRequestHandler):
|
|
10
|
+
def do_GET(self):
|
|
11
|
+
query_components = parse_qs(urlparse(self.path).query)
|
|
12
|
+
params = {k: v[0] for k, v in query_components.items()}
|
|
13
|
+
result_queue.put(params)
|
|
14
|
+
|
|
15
|
+
self.send_response(200)
|
|
16
|
+
self.send_header('Content-type', 'text/html')
|
|
17
|
+
self.end_headers()
|
|
18
|
+
|
|
19
|
+
content = """
|
|
20
|
+
<html>
|
|
21
|
+
<body style="font-family: sans-serif; text-align: center; padding-top: 50px;">
|
|
22
|
+
<h1>Authentication Successful</h1>
|
|
23
|
+
<p>You can now close this window and return to the CLI.</p>
|
|
24
|
+
</body>
|
|
25
|
+
</html>
|
|
26
|
+
"""
|
|
27
|
+
self.wfile.write(content.encode('utf-8'))
|
|
28
|
+
|
|
29
|
+
def log_message(self, format, *args):
|
|
30
|
+
# Suppress logging
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
def start_callback_server(port: int = 8888) -> Dict[str, str]:
|
|
34
|
+
server = HTTPServer(('127.0.0.1', port), CallbackHandler)
|
|
35
|
+
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
36
|
+
thread.start()
|
|
37
|
+
|
|
38
|
+
# Wait for result
|
|
39
|
+
try:
|
|
40
|
+
result = result_queue.get(timeout=300) # 5 minutes timeout
|
|
41
|
+
server.shutdown()
|
|
42
|
+
return result
|
|
43
|
+
except queue.Empty:
|
|
44
|
+
server.shutdown()
|
|
45
|
+
return {}
|
|
@@ -7,12 +7,19 @@ from .commands import build as build_cmds
|
|
|
7
7
|
from .commands import deploy as deploy_cmds
|
|
8
8
|
from .commands import run as run_cmds
|
|
9
9
|
from rich.console import Console
|
|
10
|
+
import importlib.metadata
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
__version__ = importlib.metadata.version("beamflow-cli")
|
|
14
|
+
except importlib.metadata.PackageNotFoundError:
|
|
15
|
+
__version__ = "unknown"
|
|
16
|
+
|
|
10
17
|
console = Console()
|
|
11
18
|
|
|
12
19
|
app = typer.Typer(
|
|
13
20
|
name="beamflow",
|
|
14
|
-
help="""
|
|
15
|
-
# 🚀 Beamflow CLI
|
|
21
|
+
help=f"""
|
|
22
|
+
# 🚀 Beamflow CLI (v{__version__})
|
|
16
23
|
Manage your Beamflow projects and deployments with ease.
|
|
17
24
|
|
|
18
25
|
## 🛠 Usage
|
|
@@ -90,6 +97,20 @@ def env_del(
|
|
|
90
97
|
"""
|
|
91
98
|
project_cmds.env_delete(env_name=env_name)
|
|
92
99
|
|
|
100
|
+
@env_app.command("init-vscode")
|
|
101
|
+
def env_init_vscode(
|
|
102
|
+
env_name: str = typer.Argument(..., help="Environment name to initialize VS Code for"),
|
|
103
|
+
force: bool = typer.Option(False, "--force", help="Force overwrite existing launch.json if it's malformed or already has the configuration")
|
|
104
|
+
):
|
|
105
|
+
"""
|
|
106
|
+
[bold blue]Initialize VS Code launch configurations[/bold blue] for an environment.
|
|
107
|
+
|
|
108
|
+
This command will add 'Launch <env> API' and 'Launch <env> Worker' to your .vscode/launch.json,
|
|
109
|
+
allowing you to easily debug your environment's API and Worker using VS Code.
|
|
110
|
+
"""
|
|
111
|
+
project_cmds.init_vscode_launch(env_name=env_name, force=force)
|
|
112
|
+
|
|
113
|
+
|
|
93
114
|
@app.command()
|
|
94
115
|
def run(
|
|
95
116
|
ctx: typer.Context,
|
|
@@ -223,5 +244,10 @@ def logout():
|
|
|
223
244
|
"""Log out from the platform."""
|
|
224
245
|
auth_cmds.logout()
|
|
225
246
|
|
|
247
|
+
@app.command()
|
|
248
|
+
def version():
|
|
249
|
+
"""Show the Beamflow CLI version."""
|
|
250
|
+
console.print(f"Beamflow CLI version: [bold green]{__version__}[/bold green]")
|
|
251
|
+
|
|
226
252
|
if __name__ == "__main__":
|
|
227
253
|
app()
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# {project_name}
|
|
2
|
+
|
|
3
|
+
Beamflow application generated for {project_name}.
|
|
4
|
+
|
|
5
|
+
## Structure
|
|
6
|
+
|
|
7
|
+
- `api.py`: Entry point for the API service.
|
|
8
|
+
- `worker.py`: Entry point for the worker service.
|
|
9
|
+
- `src/shared/`: Shared helpers, constants, and client utilities.
|
|
10
|
+
- `src/api/`: Webhook handlers and API startup.
|
|
11
|
+
- `src/worker/`: Worker startup and task definitions (such as feed consumers).
|
|
12
|
+
- `config/`: Environment-based configuration (e.g. `local/`, `dev/`, `prod/`, `shared/`).
|
|
13
|
+
- `deployment/`: Docker Compose setup and Dockerfiles.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Dependency Management
|
|
18
|
+
|
|
19
|
+
This project defines dependencies using optional extras, allowing you to install only what is needed for a specific service.
|
|
20
|
+
|
|
21
|
+
- **Base dependencies**: Required by both API and Worker.
|
|
22
|
+
- **`api` extra**: Dependencies required only by the API service.
|
|
23
|
+
- **`worker` extra**: Dependencies required only by the Worker service.
|
|
24
|
+
- **`dev` extra** (optional): Development tooling.
|
|
25
|
+
|
|
26
|
+
### Standard Installation (pip)
|
|
27
|
+
|
|
28
|
+
Install for local development (recommended, installs everything):
|
|
29
|
+
```bash
|
|
30
|
+
pip install -e ".[api,worker,dev]"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Install API-only:
|
|
34
|
+
```bash
|
|
35
|
+
pip install -e ".[api]"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Install Worker-only:
|
|
39
|
+
```bash
|
|
40
|
+
pip install -e ".[worker]"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Alternative Installation (Poetry)
|
|
44
|
+
|
|
45
|
+
If you prefer managing your environment with Poetry:
|
|
46
|
+
|
|
47
|
+
Install for local development (everything):
|
|
48
|
+
```bash
|
|
49
|
+
poetry install --extras "api worker dev"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Install API-only:
|
|
53
|
+
```bash
|
|
54
|
+
poetry install --extras "api"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Install Worker-only:
|
|
58
|
+
```bash
|
|
59
|
+
poetry install --extras "worker"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Running Locally
|
|
65
|
+
|
|
66
|
+
### 1. Start supporting services (e.g., Redis)
|
|
67
|
+
```bash
|
|
68
|
+
docker compose -f deployment/local/docker-compose.yaml up -d
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 2. Set environment
|
|
72
|
+
```bash
|
|
73
|
+
export BEAMSTACK_ENV=local
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 3. Run API
|
|
77
|
+
```bash
|
|
78
|
+
python api.py
|
|
79
|
+
```
|
|
80
|
+
API will be available at [http://localhost:8000](http://localhost:8000).
|
|
81
|
+
|
|
82
|
+
### 4. Run Worker
|
|
83
|
+
```bash
|
|
84
|
+
python worker.py
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
*(Alternatively, you can use the Beamflow CLI to run your environments: `beamflow run local`)*
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Beamflow Core Concepts Usage
|
|
92
|
+
|
|
93
|
+
This project is built on `beamflow_lib` – a core library providing robust context management, queue abstractions, and observability. Here is how you can utilize its components in your application:
|
|
94
|
+
|
|
95
|
+
### Integration Context
|
|
96
|
+
`IntegrationContext` propagates automatically through all your integration operations. It manages the current `run_id`, `integration`, and `current_record_key` to ensure all logs and traces are correctly correlated without manual passing.
|
|
97
|
+
|
|
98
|
+
### Ingress & Pipelines
|
|
99
|
+
- **Webhook Ingress**: Use the `@ingress.webhook(pipeline="...", integration="...")` decorator on your FastAPI endpoints to attach integration context without manually wrapping your handlers.
|
|
100
|
+
- **Polling Ingress**: Use `@ingress.poll(pipeline="...", integration="...", schedule="...")` to create durable, stateful scheduled tasks (such as API polling).
|
|
101
|
+
|
|
102
|
+
### RecordsFeed & Consumers
|
|
103
|
+
Extract and ingest data efficiently using **RecordsFeed**:
|
|
104
|
+
```python
|
|
105
|
+
from beamflow_lib.pipelines.records_feed import RecordsFeed, RecordData
|
|
106
|
+
|
|
107
|
+
feed = RecordsFeed.get(feed_id="your.pipeline")
|
|
108
|
+
feed.publish(RecordData(record_id="123", record_type="invoice", data={...}))
|
|
109
|
+
```
|
|
110
|
+
Records are automatically deduplicated and optionally ordered by timestamp.
|
|
111
|
+
|
|
112
|
+
Consume them robustly with **Feed Consumers**, which handle batching, rate-limiting, and parallel execution automatically:
|
|
113
|
+
```python
|
|
114
|
+
from beamflow_lib.pipelines.consumer import feed_consumer
|
|
115
|
+
|
|
116
|
+
@feed_consumer(
|
|
117
|
+
feed_id="your.pipeline",
|
|
118
|
+
batch=True,
|
|
119
|
+
max_batch_size=100,
|
|
120
|
+
rate_limit_per_sec=50,
|
|
121
|
+
)
|
|
122
|
+
async def process_records(records: list[RecordData]):
|
|
123
|
+
for record in records:
|
|
124
|
+
# Insert your business logic here
|
|
125
|
+
pass
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Observability
|
|
129
|
+
Use the provided `logger` to naturally correlate your logs with the active context. The framework tracks the lifecycle of records intrinsically (from publishing to consumption) without manual `record_id` logging.
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from beamflow_lib import logger
|
|
133
|
+
|
|
134
|
+
logger.info("Processing the invoice batch", details="step 1 completed")
|
|
135
|
+
```
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
version: '3.8'
|
|
2
|
+
services:
|
|
3
|
+
api:
|
|
4
|
+
build:
|
|
5
|
+
context: ../..
|
|
6
|
+
dockerfile: deployment/shared/api.Dockerfile
|
|
7
|
+
image: beamflow-{project_name_slug}-api:latest
|
|
8
|
+
ports:
|
|
9
|
+
- "8000:8000"
|
|
10
|
+
environment:
|
|
11
|
+
- ENVIRONMENT={env}
|
|
12
|
+
extra_hosts:
|
|
13
|
+
- "host.docker.internal:host-gateway"
|
|
14
|
+
|
|
15
|
+
worker:
|
|
16
|
+
build:
|
|
17
|
+
context: ../..
|
|
18
|
+
dockerfile: deployment/shared/worker.Dockerfile
|
|
19
|
+
image: beamflow-{project_name_slug}-worker:latest
|
|
20
|
+
environment:
|
|
21
|
+
- ENVIRONMENT={env}
|
|
22
|
+
extra_hosts:
|
|
23
|
+
- "host.docker.internal:host-gateway"
|
|
24
|
+
|
|
@@ -3,8 +3,8 @@ services:
|
|
|
3
3
|
api:
|
|
4
4
|
build:
|
|
5
5
|
context: ../..
|
|
6
|
-
dockerfile: deployment/
|
|
7
|
-
image: beamflow-{
|
|
6
|
+
dockerfile: deployment/shared/api.Dockerfile
|
|
7
|
+
image: beamflow-{project_name_slug}-api:latest
|
|
8
8
|
ports:
|
|
9
9
|
- "8000:8000"
|
|
10
10
|
environment:
|
|
@@ -18,8 +18,8 @@ services:
|
|
|
18
18
|
worker:
|
|
19
19
|
build:
|
|
20
20
|
context: ../..
|
|
21
|
-
dockerfile: deployment/
|
|
22
|
-
image: beamflow-{
|
|
21
|
+
dockerfile: deployment/shared/worker.Dockerfile
|
|
22
|
+
image: beamflow-{project_name_slug}-worker:latest
|
|
23
23
|
environment:
|
|
24
24
|
- REDIS_URL=redis://redis:6379/0
|
|
25
25
|
- ENVIRONMENT={env}
|
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "beamflow-cli"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.2"
|
|
4
4
|
description = "CLI for the Beamflow Managed Platform"
|
|
5
5
|
authors = [{name = "juraj.bezdek@gmail.com", email = "juraj.bezdek@gmail.com"}]
|
|
6
6
|
requires-python = ">=3.11"
|
|
7
7
|
dependencies = [
|
|
8
|
-
"beamflow-runtime>=0.3.0,<0.
|
|
8
|
+
"beamflow-runtime>=0.3.0,<0.3.2",
|
|
9
9
|
"typer[all]>=0.12.0",
|
|
10
10
|
"httpx>=0.27.0",
|
|
11
11
|
"pyyaml>=6.0.1",
|
|
12
12
|
"keyring>=25.0.0",
|
|
13
13
|
"rich>=13.7.0",
|
|
14
14
|
"pydantic>=2.0.0",
|
|
15
|
-
"python-dotenv>=1.0.1",
|
|
16
|
-
"fastapi>=0.110.0",
|
|
17
|
-
"uvicorn>=0.29.0",
|
|
18
15
|
"questionary>=2.0.1",
|
|
19
16
|
]
|
|
20
17
|
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
from typing import Dict
|
|
2
|
-
from fastapi import FastAPI, Request
|
|
3
|
-
from fastapi.responses import HTMLResponse
|
|
4
|
-
import uvicorn
|
|
5
|
-
import threading
|
|
6
|
-
import queue
|
|
7
|
-
from typing import Optional
|
|
8
|
-
|
|
9
|
-
app = FastAPI()
|
|
10
|
-
result_queue = queue.Queue()
|
|
11
|
-
|
|
12
|
-
@app.get("/callback")
|
|
13
|
-
async def callback(request: Request):
|
|
14
|
-
params = dict(request.query_params)
|
|
15
|
-
result_queue.put(params)
|
|
16
|
-
return HTMLResponse(content="""
|
|
17
|
-
<html>
|
|
18
|
-
<body style="font-family: sans-serif; text-align: center; padding-top: 50px;">
|
|
19
|
-
<h1>Authentication Successful</h1>
|
|
20
|
-
<p>You can now close this window and return to the CLI.</p>
|
|
21
|
-
</body>
|
|
22
|
-
</html>
|
|
23
|
-
""")
|
|
24
|
-
|
|
25
|
-
def run_server(port: int):
|
|
26
|
-
uvicorn.run(app, host="127.0.0.1", port=port, log_level="error")
|
|
27
|
-
|
|
28
|
-
def start_callback_server(port: int = 8888) -> Dict[str, str]:
|
|
29
|
-
thread = threading.Thread(target=run_server, args=(port,), daemon=True)
|
|
30
|
-
thread.start()
|
|
31
|
-
# Wait for result
|
|
32
|
-
try:
|
|
33
|
-
result = result_queue.get(timeout=300) # 5 minutes timeout
|
|
34
|
-
return result
|
|
35
|
-
except queue.Empty:
|
|
36
|
-
return {}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
# {project_name}
|
|
2
|
-
|
|
3
|
-
Beamflow application generated for {project_name}.
|
|
4
|
-
|
|
5
|
-
## Local Development
|
|
6
|
-
|
|
7
|
-
1. Install dependencies:
|
|
8
|
-
```bash
|
|
9
|
-
pip install -e .
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
2. Run local environment:
|
|
13
|
-
```bash
|
|
14
|
-
beamflow run dev
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
3. Build images:
|
|
18
|
-
```bash
|
|
19
|
-
beamflow build dev
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
## Structure
|
|
23
|
-
|
|
24
|
-
- `src/api`: FastAPI routes and webhooks
|
|
25
|
-
- `src/worker`: Background tasks and feed consumers
|
|
26
|
-
- `src/shared`: Shared models, clients, and tasks
|
|
27
|
-
- `config`: Environment-specific configuration
|
|
28
|
-
- `deployment`: Docker Compose and Dockerfiles
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
from beamflow_lib.config.env_loader import load_config_dir
|
|
3
|
-
|
|
4
|
-
def test_dev_config_loading():
|
|
5
|
-
"""
|
|
6
|
-
Verify that the {env} environment configuration loads correctly.
|
|
7
|
-
"""
|
|
8
|
-
config_dir = Path(__file__).parent.parent / "config"
|
|
9
|
-
config = load_config_dir(config_dir, environment="{env}")
|
|
10
|
-
assert config is not None
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import time
|
|
3
|
-
import requests
|
|
4
|
-
import pytest
|
|
5
|
-
|
|
6
|
-
def get_base_url():
|
|
7
|
-
return "http://localhost:8000"
|
|
8
|
-
|
|
9
|
-
def test_health():
|
|
10
|
-
base_url = get_base_url()
|
|
11
|
-
resp = requests.get(f"{{base_url}}/health", timeout=5)
|
|
12
|
-
assert resp.status_code == 200
|
|
13
|
-
assert resp.json()["status"] == "ok"
|
|
14
|
-
|
|
15
|
-
def test_webhook_process_user():
|
|
16
|
-
base_url = get_base_url()
|
|
17
|
-
resp = requests.post(f"{{base_url}}/webhooks/{project_name}/process_user", json={{"user_id": "test-user-1"}}, timeout=5)
|
|
18
|
-
if resp.status_code == 404:
|
|
19
|
-
resp = requests.post(f"{{base_url}}/{project_name}/process_user", json={{"user_id": "test-user-1"}}, timeout=5)
|
|
20
|
-
assert resp.status_code == 200
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{beamflow_cli-0.3.0 → beamflow_cli-0.3.2}/beamflow/templates/_deployment/shared/api.Dockerfile
RENAMED
|
File without changes
|
{beamflow_cli-0.3.0 → beamflow_cli-0.3.2}/beamflow/templates/_deployment/shared/worker.Dockerfile
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{beamflow_cli-0.3.0 → beamflow_cli-0.3.2}/beamflow/templates/_src/_shared/tasks/sharedTasks.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|