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.
Files changed (46) hide show
  1. beamflow/__init__.py +0 -0
  2. beamflow/commands/__init__.py +0 -0
  3. beamflow/commands/auth.py +111 -0
  4. beamflow/commands/build.py +127 -0
  5. beamflow/commands/deploy.py +104 -0
  6. beamflow/commands/project.py +451 -0
  7. beamflow/commands/run.py +65 -0
  8. beamflow/core/__init__.py +0 -0
  9. beamflow/core/api_client.py +62 -0
  10. beamflow/core/auth_server.py +36 -0
  11. beamflow/core/builder.py +40 -0
  12. beamflow/core/config.py +81 -0
  13. beamflow/core/docker_utils.py +33 -0
  14. beamflow/main.py +227 -0
  15. beamflow/templates/_.beamflow +4 -0
  16. beamflow/templates/_.dockerignore +9 -0
  17. beamflow/templates/_README.md +28 -0
  18. beamflow/templates/_api_main.py +19 -0
  19. beamflow/templates/_config/[env]/(backend-asyncio)/backend.yaml +4 -0
  20. beamflow/templates/_config/[env]/(backend-dramatiq)/backend.yaml +8 -0
  21. beamflow/templates/_config/[env]/(backend-managed)/backend.yaml +6 -0
  22. beamflow/templates/_config/[env]/_backend.yaml +4 -0
  23. beamflow/templates/_config/shared/backend.yaml +4 -0
  24. beamflow/templates/_deployment/[env]/(backend-dramatiq)/docker-compose.yaml +34 -0
  25. beamflow/templates/_deployment/[env]/.env +3 -0
  26. beamflow/templates/_deployment/shared/.env +5 -0
  27. beamflow/templates/_deployment/shared/api.Dockerfile +10 -0
  28. beamflow/templates/_deployment/shared/clients/demoClient.yaml +39 -0
  29. beamflow/templates/_deployment/shared/worker.Dockerfile +9 -0
  30. beamflow/templates/_pyproject.toml +40 -0
  31. beamflow/templates/_src/_api/__init__.py +1 -0
  32. beamflow/templates/_src/_api/_routes/webhooks.py +25 -0
  33. beamflow/templates/_src/_shared/__init__.py +1 -0
  34. beamflow/templates/_src/_shared/clients/client.py +18 -0
  35. beamflow/templates/_src/_shared/models/models.py +1 -0
  36. beamflow/templates/_src/_shared/tasks/sharedTasks.py +10 -0
  37. beamflow/templates/_src/_worker/__init__.py +1 -0
  38. beamflow/templates/_src/_worker/tasks/tasks.py +15 -0
  39. beamflow/templates/_worker_main.py +25 -0
  40. beamflow/templates/tests/_test_config_loading.py +10 -0
  41. beamflow/templates/tests/_test_integration.py +20 -0
  42. beamflow/ui/__init__.py +0 -0
  43. beamflow_cli-0.3.0.dist-info/METADATA +22 -0
  44. beamflow_cli-0.3.0.dist-info/RECORD +46 -0
  45. beamflow_cli-0.3.0.dist-info/WHEEL +4 -0
  46. beamflow_cli-0.3.0.dist-info/entry_points.txt +3 -0
@@ -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,4 @@
1
+ project_name: {project_name}
2
+ environments:
3
+ - name: dev
4
+ managed: false
@@ -0,0 +1,9 @@
1
+ .git
2
+ .venv
3
+ __pycache__
4
+ .pytest_cache
5
+ *.pyc
6
+ .beamflow
7
+ pyproject.toml
8
+ README.md
9
+ tests
@@ -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,4 @@
1
+ backend:
2
+ type: asyncio
3
+ webhooks:
4
+ prefix: /webhooks
@@ -0,0 +1,8 @@
1
+ backend:
2
+ type: dramatiq
3
+ dramatiq:
4
+ redis_url: redis://redis:6380/0
5
+ webhooks:
6
+ prefix: /webhooks
7
+
8
+
@@ -0,0 +1,6 @@
1
+ backend:
2
+ type: managed
3
+ webhooks:
4
+ prefix: /webhooks
5
+
6
+
@@ -0,0 +1,4 @@
1
+ # Environment-specific overrides for backend
2
+ backend:
3
+ # The type of backend can be 'dramatiq', 'async_backend', or 'managed'
4
+ type: managed | dramatiq | asyncio
@@ -0,0 +1,4 @@
1
+ webhooks:
2
+ prefix: /webhooks
3
+
4
+
@@ -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,3 @@
1
+ # Specific Envirment Variables for this enviroment belong here
2
+ # i.e.
3
+ # FOO_SEVICE_BASE_URL=dev/prod.rest.domain.com/v1
@@ -0,0 +1,5 @@
1
+ # Any ENV variables that should have the same value for any enviroment bellong here
2
+ # i.e.
3
+ # TEST_ENV_VAR=demo-value
4
+ # DEMO_CLIENT_ID=123
5
+ # DEMO_CLIENT_SECRET=456
@@ -0,0 +1,10 @@
1
+ # API Dockerfile for {project_name}
2
+ FROM beamflow/beamflow-base:latest
3
+
4
+ WORKDIR /app
5
+ COPY . .
6
+
7
+ RUN rm -rf src/worker
8
+ EXPOSE 8000
9
+ ENV PYTHONPATH=/app/src:/app
10
+ CMD ["python", "api_main.py"]
@@ -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,9 @@
1
+ # Worker Dockerfile for {project_name}
2
+ FROM beamflow/beamflow-base:latest
3
+
4
+ WORKDIR /app
5
+ COPY . .
6
+
7
+ RUN rm -rf src/api
8
+ ENV PYTHONPATH=/app/src:/app
9
+ CMD ["python", "-u", "worker_main.py"]
@@ -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
+