beamflow-cli 0.3.0__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.
- beamflow/__init__.py +0 -0
- beamflow/commands/__init__.py +0 -0
- beamflow/commands/auth.py +111 -0
- beamflow/commands/build.py +127 -0
- beamflow/commands/deploy.py +104 -0
- beamflow/commands/project.py +451 -0
- beamflow/commands/run.py +65 -0
- beamflow/core/__init__.py +0 -0
- beamflow/core/api_client.py +62 -0
- beamflow/core/auth_server.py +36 -0
- beamflow/core/builder.py +40 -0
- beamflow/core/config.py +81 -0
- beamflow/core/docker_utils.py +33 -0
- beamflow/main.py +227 -0
- beamflow/templates/_.beamflow +4 -0
- beamflow/templates/_.dockerignore +9 -0
- beamflow/templates/_README.md +28 -0
- beamflow/templates/_api_main.py +19 -0
- beamflow/templates/_config/[env]/(backend-asyncio)/backend.yaml +4 -0
- beamflow/templates/_config/[env]/(backend-dramatiq)/backend.yaml +8 -0
- beamflow/templates/_config/[env]/(backend-managed)/backend.yaml +6 -0
- beamflow/templates/_config/[env]/_backend.yaml +4 -0
- beamflow/templates/_config/shared/backend.yaml +4 -0
- beamflow/templates/_deployment/[env]/(backend-dramatiq)/docker-compose.yaml +34 -0
- beamflow/templates/_deployment/[env]/.env +3 -0
- beamflow/templates/_deployment/shared/.env +5 -0
- beamflow/templates/_deployment/shared/api.Dockerfile +10 -0
- beamflow/templates/_deployment/shared/clients/demoClient.yaml +39 -0
- beamflow/templates/_deployment/shared/worker.Dockerfile +9 -0
- beamflow/templates/_pyproject.toml +40 -0
- beamflow/templates/_src/_api/__init__.py +1 -0
- beamflow/templates/_src/_api/_routes/webhooks.py +25 -0
- beamflow/templates/_src/_shared/__init__.py +1 -0
- beamflow/templates/_src/_shared/clients/client.py +18 -0
- beamflow/templates/_src/_shared/models/models.py +1 -0
- beamflow/templates/_src/_shared/tasks/sharedTasks.py +10 -0
- beamflow/templates/_src/_worker/__init__.py +1 -0
- beamflow/templates/_src/_worker/tasks/tasks.py +15 -0
- beamflow/templates/_worker_main.py +25 -0
- beamflow/templates/tests/_test_config_loading.py +10 -0
- beamflow/templates/tests/_test_integration.py +20 -0
- beamflow/ui/__init__.py +0 -0
- beamflow_cli-0.3.0.dist-info/METADATA +22 -0
- beamflow_cli-0.3.0.dist-info/RECORD +46 -0
- beamflow_cli-0.3.0.dist-info/WHEEL +4 -0
- beamflow_cli-0.3.0.dist-info/entry_points.txt +3 -0
beamflow/core/config.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
from pydantic import Field
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional, Dict, Any
|
|
6
|
+
import yaml
|
|
7
|
+
import keyring
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
BEAMFLOW_DIR = Path.home() / ".beamflow"
|
|
11
|
+
GLOBAL_CONFIG_FILE = BEAMFLOW_DIR / "config.yaml"
|
|
12
|
+
PROJECT_CONFIG_FILE = ".beamflow"
|
|
13
|
+
|
|
14
|
+
SERVICE_NAME = "beamflow"
|
|
15
|
+
TOKEN_KEY = "access_token"
|
|
16
|
+
|
|
17
|
+
class GlobalConfig(BaseModel):
|
|
18
|
+
api_url: str = "http://localhost:8080" # Default to local dev
|
|
19
|
+
|
|
20
|
+
class EnvironmentMode(BaseModel):
|
|
21
|
+
name: str
|
|
22
|
+
managed: bool = False
|
|
23
|
+
options: Dict[str, str] = Field(default_factory=dict)
|
|
24
|
+
|
|
25
|
+
class ProjectConfig(BaseModel):
|
|
26
|
+
project_name: Optional[str] = None
|
|
27
|
+
project_id: Optional[str] = None
|
|
28
|
+
tenant_id: Optional[str] = None
|
|
29
|
+
user_email: Optional[str] = None
|
|
30
|
+
environments: List[EnvironmentMode] = Field(default_factory=list)
|
|
31
|
+
|
|
32
|
+
def load_global_config() -> GlobalConfig:
|
|
33
|
+
if not GLOBAL_CONFIG_FILE.exists():
|
|
34
|
+
return GlobalConfig()
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
with open(GLOBAL_CONFIG_FILE, "r") as f:
|
|
38
|
+
data = yaml.safe_load(f) or {}
|
|
39
|
+
return GlobalConfig(**data)
|
|
40
|
+
except Exception:
|
|
41
|
+
return GlobalConfig()
|
|
42
|
+
|
|
43
|
+
def save_global_config(config: GlobalConfig):
|
|
44
|
+
BEAMFLOW_DIR.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
with open(GLOBAL_CONFIG_FILE, "w") as f:
|
|
46
|
+
yaml.safe_dump(config.model_dump(), f)
|
|
47
|
+
|
|
48
|
+
def get_access_token() -> Optional[str]:
|
|
49
|
+
return keyring.get_password(SERVICE_NAME, TOKEN_KEY)
|
|
50
|
+
|
|
51
|
+
def set_access_token(token: str):
|
|
52
|
+
keyring.set_password(SERVICE_NAME, TOKEN_KEY, token)
|
|
53
|
+
|
|
54
|
+
def delete_access_token():
|
|
55
|
+
try:
|
|
56
|
+
keyring.delete_password(SERVICE_NAME, TOKEN_KEY)
|
|
57
|
+
except keyring.errors.PasswordDeleteError:
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
def load_project_config() -> Optional[ProjectConfig]:
|
|
61
|
+
path = Path(PROJECT_CONFIG_FILE)
|
|
62
|
+
if not path.exists():
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
with open(path, "r") as f:
|
|
67
|
+
data = yaml.safe_load(f) or {}
|
|
68
|
+
return ProjectConfig(**data)
|
|
69
|
+
except Exception:
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
def save_project_config(config: ProjectConfig):
|
|
73
|
+
with open(PROJECT_CONFIG_FILE, "w") as f:
|
|
74
|
+
yaml.safe_dump(config.model_dump(), f)
|
|
75
|
+
|
|
76
|
+
# Legacy aliases for compatibility if needed, though we should update callers
|
|
77
|
+
def load_config() -> GlobalConfig:
|
|
78
|
+
return load_global_config()
|
|
79
|
+
|
|
80
|
+
def save_config(config: GlobalConfig):
|
|
81
|
+
save_global_config(config)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
import subprocess
|
|
3
|
+
from typing import Tuple, Optional
|
|
4
|
+
|
|
5
|
+
def check_docker_binary() -> bool:
|
|
6
|
+
return shutil.which("docker") is not None
|
|
7
|
+
|
|
8
|
+
def check_docker_compose_binary() -> bool:
|
|
9
|
+
# Check for 'docker compose' (V2) or 'docker-compose' (V1)
|
|
10
|
+
if shutil.which("docker-compose"):
|
|
11
|
+
return True
|
|
12
|
+
|
|
13
|
+
# Check if 'docker compose' works
|
|
14
|
+
try:
|
|
15
|
+
subprocess.run(["docker", "compose", "version"], capture_output=True, check=True)
|
|
16
|
+
return True
|
|
17
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
18
|
+
return False
|
|
19
|
+
|
|
20
|
+
def check_docker_daemon() -> Tuple[bool, Optional[str]]:
|
|
21
|
+
"""Returns (is_running, error_message)"""
|
|
22
|
+
try:
|
|
23
|
+
subprocess.run(["docker", "info"], capture_output=True, check=True)
|
|
24
|
+
return True, None
|
|
25
|
+
except subprocess.CalledProcessError as e:
|
|
26
|
+
return False, e.stderr.decode().strip()
|
|
27
|
+
except FileNotFoundError:
|
|
28
|
+
return False, "Docker binary not found"
|
|
29
|
+
|
|
30
|
+
def get_docker_compose_cmd() -> list:
|
|
31
|
+
if shutil.which("docker-compose"):
|
|
32
|
+
return ["docker-compose"]
|
|
33
|
+
return ["docker", "compose"]
|
beamflow/main.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
import click
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from .commands import auth as auth_cmds
|
|
5
|
+
from .commands import project as project_cmds
|
|
6
|
+
from .commands import build as build_cmds
|
|
7
|
+
from .commands import deploy as deploy_cmds
|
|
8
|
+
from .commands import run as run_cmds
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(
|
|
13
|
+
name="beamflow",
|
|
14
|
+
help="""
|
|
15
|
+
# 🚀 Beamflow CLI
|
|
16
|
+
Manage your Beamflow projects and deployments with ease.
|
|
17
|
+
|
|
18
|
+
## 🛠 Usage
|
|
19
|
+
Run commands below to initialize, build, and deploy your integrations.
|
|
20
|
+
""",
|
|
21
|
+
add_completion=False,
|
|
22
|
+
no_args_is_help=True,
|
|
23
|
+
rich_markup_mode="rich"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
env_app = typer.Typer(
|
|
27
|
+
name="env",
|
|
28
|
+
help="Manage environments",
|
|
29
|
+
no_args_is_help=True,
|
|
30
|
+
rich_markup_mode="rich"
|
|
31
|
+
)
|
|
32
|
+
app.add_typer(env_app, name="env")
|
|
33
|
+
|
|
34
|
+
@app.command()
|
|
35
|
+
def help(ctx: typer.Context):
|
|
36
|
+
"""
|
|
37
|
+
Show help information for all commands.
|
|
38
|
+
"""
|
|
39
|
+
console.print(ctx.parent.get_help()) if ctx.parent else console.print(ctx.get_help())
|
|
40
|
+
|
|
41
|
+
# Shortcuts / Top-level commands
|
|
42
|
+
@app.command()
|
|
43
|
+
def login(
|
|
44
|
+
username: Optional[str] = typer.Option(None, "--username", "-u", help="Username for manual login"),
|
|
45
|
+
password: Optional[str] = typer.Option(None, "--password", "-p", help="Password for manual login")
|
|
46
|
+
):
|
|
47
|
+
"""Log in to the Beamflow Managed Platform."""
|
|
48
|
+
auth_cmds.login(username=username, password=password)
|
|
49
|
+
|
|
50
|
+
@app.command()
|
|
51
|
+
def init(
|
|
52
|
+
name: Optional[str] = typer.Option(None, "--name", "-n", help="Project name"),
|
|
53
|
+
fix: bool = typer.Option(False, "--fix", help="Repair missing components"),
|
|
54
|
+
force: bool = typer.Option(False, "--force", help="Force overwrite existing files")
|
|
55
|
+
):
|
|
56
|
+
"""
|
|
57
|
+
[bold green]Initialize[/bold green] or [bold yellow]repair[/bold yellow] a Beamflow project.
|
|
58
|
+
|
|
59
|
+
This command sets up the local structure, creates the `.beamflow` config,
|
|
60
|
+
and lets you configure your first environments.
|
|
61
|
+
"""
|
|
62
|
+
project_cmds.init(name=name, fix=fix, force=force)
|
|
63
|
+
|
|
64
|
+
@app.command()
|
|
65
|
+
def check():
|
|
66
|
+
"""Inspect the current project structure."""
|
|
67
|
+
project_cmds.check()
|
|
68
|
+
|
|
69
|
+
@env_app.command("add")
|
|
70
|
+
def env_add(
|
|
71
|
+
env_name: Optional[str] = typer.Argument(None, help="Environment name to add"),
|
|
72
|
+
force: bool = typer.Option(False, "--force", help="Force overwrite existing files")
|
|
73
|
+
):
|
|
74
|
+
"""
|
|
75
|
+
[bold blue]Add a new environment[/bold blue] to your project.
|
|
76
|
+
|
|
77
|
+
Choose between Local, Dev, Prod, or Custom. It will automatically filter
|
|
78
|
+
out existing environments and scaffold necessary files.
|
|
79
|
+
"""
|
|
80
|
+
project_cmds.env_add(env_name=env_name, force=force)
|
|
81
|
+
|
|
82
|
+
@env_app.command("del")
|
|
83
|
+
def env_del(
|
|
84
|
+
env_name: str = typer.Argument(..., help="Environment name to delete")
|
|
85
|
+
):
|
|
86
|
+
"""
|
|
87
|
+
[bold red]Delete an environment[/bold red] from your project.
|
|
88
|
+
|
|
89
|
+
Requires confirmation before removal.
|
|
90
|
+
"""
|
|
91
|
+
project_cmds.env_delete(env_name=env_name)
|
|
92
|
+
|
|
93
|
+
@app.command()
|
|
94
|
+
def run(
|
|
95
|
+
ctx: typer.Context,
|
|
96
|
+
env: str = typer.Argument("local", help="Environment to run (default: local)"),
|
|
97
|
+
build: bool = typer.Option(False, "--build", help="Build images before starting"),
|
|
98
|
+
detach: bool = typer.Option(False, "--detach", "-d", help="Run in background"),
|
|
99
|
+
logs: bool = typer.Option(False, "--logs", help="Follow logs (useful with --detach)")
|
|
100
|
+
):
|
|
101
|
+
"""
|
|
102
|
+
[bold magenta]Run[/bold magenta] the project locally using Docker Compose.
|
|
103
|
+
|
|
104
|
+
Defaults to the 'local' environment.
|
|
105
|
+
|
|
106
|
+
[yellow]Note:[/yellow] To run a specific environment, use: [bold]beamflow run <env_name>[/bold]
|
|
107
|
+
"""
|
|
108
|
+
from .core.config import load_project_config
|
|
109
|
+
from rich.prompt import Confirm
|
|
110
|
+
|
|
111
|
+
if ctx.get_parameter_source("env") == click.core.ParameterSource.DEFAULT:
|
|
112
|
+
console.print(f"[dim]Env argument not specified... using [bold]{env}[/bold] as default[/dim]")
|
|
113
|
+
|
|
114
|
+
project_config = load_project_config()
|
|
115
|
+
if project_config:
|
|
116
|
+
env_mode = next((e for e in project_config.environments if e.name == env), None)
|
|
117
|
+
if not env_mode:
|
|
118
|
+
if env == "local":
|
|
119
|
+
if Confirm.ask(f"Environment '{env}' not found. Would you like to set it up now?"):
|
|
120
|
+
project_cmds.add_environment(project_config, env_name=env)
|
|
121
|
+
else:
|
|
122
|
+
console.print(f"[red]Aborting. Run 'beamflow env add' to create it manually.[/red]")
|
|
123
|
+
raise typer.Exit(code=1)
|
|
124
|
+
else:
|
|
125
|
+
console.print(f"[red]Environment '{env}' not found.[/red]")
|
|
126
|
+
raise typer.Exit(code=1)
|
|
127
|
+
|
|
128
|
+
run_cmds.run(env=env, build=build, detach=detach, logs=logs)
|
|
129
|
+
|
|
130
|
+
@app.command()
|
|
131
|
+
def build(
|
|
132
|
+
ctx: typer.Context,
|
|
133
|
+
env: str = typer.Argument("local", help="Environment to build (default: local)"),
|
|
134
|
+
tag: str = typer.Option("latest", "--tag", "-t", help="Tag for the image")
|
|
135
|
+
):
|
|
136
|
+
"""
|
|
137
|
+
[bold yellow]Build[/bold yellow] project artifacts/images.
|
|
138
|
+
|
|
139
|
+
Defaults to the 'local' environment.
|
|
140
|
+
|
|
141
|
+
[yellow]Note:[/yellow] To build for a specific environment, use: [bold]beamflow build <env_name>[/bold]
|
|
142
|
+
"""
|
|
143
|
+
if ctx.get_parameter_source("env") == click.core.ParameterSource.DEFAULT:
|
|
144
|
+
console.print(f"[dim]Env argument not specified... using [bold]{env}[/bold] as default[/dim]")
|
|
145
|
+
|
|
146
|
+
from .core.config import load_project_config
|
|
147
|
+
from rich.prompt import Confirm
|
|
148
|
+
|
|
149
|
+
project_config = load_project_config()
|
|
150
|
+
if project_config:
|
|
151
|
+
env_mode = next((e for e in project_config.environments if e.name == env), None)
|
|
152
|
+
if not env_mode:
|
|
153
|
+
if env == "local":
|
|
154
|
+
if Confirm.ask(f"Environment '{env}' not found. Would you like to set it up now?"):
|
|
155
|
+
project_cmds.add_environment(project_config, env_name=env)
|
|
156
|
+
else:
|
|
157
|
+
console.print(f"[red]Aborting. Run 'beamflow env add' to create it manually.[/red]")
|
|
158
|
+
raise typer.Exit(code=1)
|
|
159
|
+
else:
|
|
160
|
+
console.print(f"[red]Environment '{env}' not found.[/red]")
|
|
161
|
+
raise typer.Exit(code=1)
|
|
162
|
+
|
|
163
|
+
build_cmds.build(env=env, tag=tag)
|
|
164
|
+
|
|
165
|
+
@app.command()
|
|
166
|
+
def deploy(
|
|
167
|
+
ctx: typer.Context,
|
|
168
|
+
env: str = typer.Argument("prod", help="Environment to deploy to (default: prod)"),
|
|
169
|
+
artifact: Optional[str] = typer.Option(None, "--artifact", "-a", help="Artifact ID to deploy")
|
|
170
|
+
):
|
|
171
|
+
"""
|
|
172
|
+
[bold cyan]Deploy[/bold cyan] your project to the Beamflow Managed Platform.
|
|
173
|
+
|
|
174
|
+
Defaults to the 'prod' environment. If 'prod' is missing, it will prompt you to set it up.
|
|
175
|
+
|
|
176
|
+
[yellow]Note:[/yellow] To deploy to a specific environment, use: [bold]beamflow deploy <env_name>[/bold]
|
|
177
|
+
"""
|
|
178
|
+
from .core.config import load_project_config
|
|
179
|
+
from .commands.project import add_environment
|
|
180
|
+
from rich.prompt import Confirm
|
|
181
|
+
|
|
182
|
+
if ctx.get_parameter_source("env") == click.core.ParameterSource.DEFAULT:
|
|
183
|
+
console.print(f"[dim]Env argument not specified... using [bold]{env}[/bold] as default[/dim]")
|
|
184
|
+
|
|
185
|
+
project_config = load_project_config()
|
|
186
|
+
if not project_config:
|
|
187
|
+
console.print("[red]No .beamflow found. Please run 'beamflow init' first.[/red]")
|
|
188
|
+
raise typer.Exit(code=1)
|
|
189
|
+
|
|
190
|
+
# Find the requested environment
|
|
191
|
+
env_mode = next((e for e in project_config.environments if e.name == env), None)
|
|
192
|
+
|
|
193
|
+
if not env_mode:
|
|
194
|
+
if env == "prod":
|
|
195
|
+
if Confirm.ask(f"Environment '{env}' not found. Would you like to set it up now?"):
|
|
196
|
+
# We need to pass the actual project_config object to add_environment
|
|
197
|
+
# But project_cmds is a module, we should be careful about cyclic imports or just use it.
|
|
198
|
+
project_cmds.add_environment(project_config, env_name=env)
|
|
199
|
+
# Reload or refresh check
|
|
200
|
+
project_config = load_project_config()
|
|
201
|
+
env_mode = next((e for e in project_config.environments if e.name == env), None)
|
|
202
|
+
if not env_mode:
|
|
203
|
+
console.print(f"[red]Environment '{env}' was not created. Aborting.[/red]")
|
|
204
|
+
raise typer.Exit(code=1)
|
|
205
|
+
else:
|
|
206
|
+
console.print(f"[red]Aborting. Use 'beamflow env add' to create environments manually.[/red]")
|
|
207
|
+
raise typer.Exit(code=1)
|
|
208
|
+
else:
|
|
209
|
+
console.print(f"[red]Environment '{env}' not found.[/red]")
|
|
210
|
+
console.print(f"[yellow]Available environments: {', '.join(e.name for e in project_config.environments)}[/yellow]")
|
|
211
|
+
console.print(f"To deploy to a specific environment, use: [bold]beamflow deploy <env_name>[/bold]")
|
|
212
|
+
raise typer.Exit(code=1)
|
|
213
|
+
|
|
214
|
+
deploy_cmds.deploy(env=env, artifact=artifact)
|
|
215
|
+
|
|
216
|
+
@app.command()
|
|
217
|
+
def whoami():
|
|
218
|
+
"""Show current login status."""
|
|
219
|
+
auth_cmds.whoami()
|
|
220
|
+
|
|
221
|
+
@app.command()
|
|
222
|
+
def logout():
|
|
223
|
+
"""Log out from the platform."""
|
|
224
|
+
auth_cmds.logout()
|
|
225
|
+
|
|
226
|
+
if __name__ == "__main__":
|
|
227
|
+
app()
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from fastapi import Request
|
|
3
|
+
from beamflow_lib.config.env_loader import load_config_dir
|
|
4
|
+
from beamflow_runtime.ingress.app import create_fastapi_app
|
|
5
|
+
|
|
6
|
+
# Load configuration relative to this file
|
|
7
|
+
config = load_config_dir("config", environment="{env}")
|
|
8
|
+
|
|
9
|
+
# Create FastAPI app
|
|
10
|
+
app = create_fastapi_app(config, auto_import=[Path(__file__).parent / "src" / "api" / "routes" / "webhooks.py"])
|
|
11
|
+
|
|
12
|
+
@app.get("/health")
|
|
13
|
+
async def health():
|
|
14
|
+
"""Simple status webhook."""
|
|
15
|
+
return {"status": "ok", "service": "{project_name}-api"}
|
|
16
|
+
|
|
17
|
+
if __name__ == "__main__":
|
|
18
|
+
import uvicorn
|
|
19
|
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
version: '3.8'
|
|
2
|
+
services:
|
|
3
|
+
api:
|
|
4
|
+
build:
|
|
5
|
+
context: ../..
|
|
6
|
+
dockerfile: deployment/{env}/api.Dockerfile
|
|
7
|
+
image: beamflow-{project_name}-api:latest
|
|
8
|
+
ports:
|
|
9
|
+
- "8000:8000"
|
|
10
|
+
environment:
|
|
11
|
+
- REDIS_URL=redis://redis:6379/0
|
|
12
|
+
- ENVIRONMENT={env}
|
|
13
|
+
extra_hosts:
|
|
14
|
+
- "host.docker.internal:host-gateway"
|
|
15
|
+
depends_on:
|
|
16
|
+
- redis
|
|
17
|
+
|
|
18
|
+
worker:
|
|
19
|
+
build:
|
|
20
|
+
context: ../..
|
|
21
|
+
dockerfile: deployment/{env}/worker.Dockerfile
|
|
22
|
+
image: beamflow-{project_name}-worker:latest
|
|
23
|
+
environment:
|
|
24
|
+
- REDIS_URL=redis://redis:6379/0
|
|
25
|
+
- ENVIRONMENT={env}
|
|
26
|
+
extra_hosts:
|
|
27
|
+
- "host.docker.internal:host-gateway"
|
|
28
|
+
depends_on:
|
|
29
|
+
- redis
|
|
30
|
+
|
|
31
|
+
redis:
|
|
32
|
+
image: redis:7-alpine
|
|
33
|
+
ports:
|
|
34
|
+
- "6379:6379"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
client_id: demoClient
|
|
2
|
+
baseUrl: https://jsonplaceholder.typicode.com
|
|
3
|
+
timeout: 10
|
|
4
|
+
handleRedirects: false
|
|
5
|
+
# Authentication configuration (OAuth2 example)
|
|
6
|
+
auth:
|
|
7
|
+
type: oauth2
|
|
8
|
+
client_id: "${DEMO_CLIENT_ID}"
|
|
9
|
+
client_secret: "${DEMO_CLIENT_SECRET}"
|
|
10
|
+
token_url: "https://auth.example.com/token"
|
|
11
|
+
refresh_token: null
|
|
12
|
+
scopes: []
|
|
13
|
+
extra_params: {}
|
|
14
|
+
|
|
15
|
+
# Authentication Alternatives (Commented out)
|
|
16
|
+
# ------------------------------------------
|
|
17
|
+
# Basic Auth:
|
|
18
|
+
# auth:
|
|
19
|
+
# type: basic
|
|
20
|
+
# username: "${DEMO_USERNAME}"
|
|
21
|
+
# password: "${DEMO_PASSWORD}"
|
|
22
|
+
#
|
|
23
|
+
# API Key Auth:
|
|
24
|
+
# auth:
|
|
25
|
+
# type: api_key
|
|
26
|
+
# key: "X-API-Key"
|
|
27
|
+
# value: "${DEMO_API_KEY}"
|
|
28
|
+
# in: header # Supported values: header, query
|
|
29
|
+
|
|
30
|
+
# Retry configuration
|
|
31
|
+
retry:
|
|
32
|
+
maxRetries: 0
|
|
33
|
+
whitelist: [] # Keywords in response to retry (e.g., ["rate limit"])
|
|
34
|
+
blacklist: [] # Keywords in response NOT to retry (e.g., ["400"])
|
|
35
|
+
|
|
36
|
+
# Extra parameters (e.g., global headers)
|
|
37
|
+
extra:
|
|
38
|
+
headers:
|
|
39
|
+
User-Agent: DemoClient/1.0
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "{project_name}-app"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Beamflow application {project_name}"
|
|
5
|
+
requires-python = ">=3.11"
|
|
6
|
+
|
|
7
|
+
# Base dependencies (required by both API and Worker)
|
|
8
|
+
dependencies = [
|
|
9
|
+
"pyyaml>=6.0",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[project.optional-dependencies]
|
|
13
|
+
|
|
14
|
+
# API-only dependencies
|
|
15
|
+
api = [
|
|
16
|
+
"fastapi>=0.100.0",
|
|
17
|
+
"uvicorn>=0.23.0",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
# Worker-only dependencies
|
|
21
|
+
worker = [
|
|
22
|
+
"dramatiq[redis]>=1.14.0",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
# Development dependencies (optional)
|
|
26
|
+
dev = [
|
|
27
|
+
"pytest>=7.0",
|
|
28
|
+
"ipython>=8.0",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[tool.setuptools]
|
|
32
|
+
package-dir = {"" = "src"}
|
|
33
|
+
|
|
34
|
+
[tool.setuptools.packages.find]
|
|
35
|
+
where = ["src"]
|
|
36
|
+
|
|
37
|
+
[tool.pytest.ini_options]
|
|
38
|
+
addopts = "-v -s"
|
|
39
|
+
testpaths = ["tests"]
|
|
40
|
+
python_files = "test_*.py"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# API package
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from beamflow_lib import ingress
|
|
2
|
+
from beamflow_lib import RecordData
|
|
3
|
+
from fastapi import Request
|
|
4
|
+
from beamflow_lib.pipelines.records_feed import RecordsFeed
|
|
5
|
+
|
|
6
|
+
@ingress.webhook(integration="demo", pipeline="process_user", path="/demo/process_user", method="POST")
|
|
7
|
+
async def process_user_webhook(request: Request):
|
|
8
|
+
"""Webhook that triggers a background worker task."""
|
|
9
|
+
try:
|
|
10
|
+
data = await request.json()
|
|
11
|
+
except Exception:
|
|
12
|
+
data = {}
|
|
13
|
+
|
|
14
|
+
user_id = data.get("user_id")
|
|
15
|
+
|
|
16
|
+
if not user_id:
|
|
17
|
+
return {"error": "user_id required"}, 400
|
|
18
|
+
|
|
19
|
+
feed = RecordsFeed.get("test_feed")
|
|
20
|
+
await feed.publish(RecordData(record_id=user_id, record_type="user", data={"user_id": user_id}))
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
"status": "submitted",
|
|
24
|
+
"user_id": user_id
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Shared package
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
from beamflow_lib.clients import HttpClient, client
|
|
3
|
+
|
|
4
|
+
@client("DemoClient")
|
|
5
|
+
class DemoClient(HttpClient):
|
|
6
|
+
"""
|
|
7
|
+
Client for the Demo API.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
async def get_users(self) -> List[dict]:
|
|
11
|
+
response = await self.request("GET", "/users")
|
|
12
|
+
data = response.json()
|
|
13
|
+
return data
|
|
14
|
+
|
|
15
|
+
async def get_user(self, user_id: int) -> dict:
|
|
16
|
+
response = await self.request("GET", f"/users/{{user_id}}")
|
|
17
|
+
data = response.json()
|
|
18
|
+
return data
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Shared models for the integration
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from beamflow_lib.decorators import integration_task
|
|
2
|
+
from beamflow_clients import get_client
|
|
3
|
+
|
|
4
|
+
@integration_task(integration="demo", integration_pipeline="adhoc_task")
|
|
5
|
+
async def adhoc_task(data: dict):
|
|
6
|
+
"""
|
|
7
|
+
Ad-hoc task triggered via webhook.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
print(f"Running ad-hoc task with data: {data}")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Worker package
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from beamflow_lib.decorators import integration_task
|
|
2
|
+
from beamflow_lib.queue.backend import Schedule
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@integration_task(integration="demo", integration_pipeline="demo", default_schedule=Schedule(cron="* * * * *"))
|
|
8
|
+
def demo_task(user_id: int):
|
|
9
|
+
"""
|
|
10
|
+
Task to process a user by ID.
|
|
11
|
+
"""
|
|
12
|
+
print(f"Processing user {user_id}...")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|