flowstash-cli 0.1.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.
- flowstash/__init__.py +0 -0
- flowstash/commands/__init__.py +0 -0
- flowstash/commands/auth.py +112 -0
- flowstash/commands/build.py +127 -0
- flowstash/commands/deploy.py +143 -0
- flowstash/commands/project.py +555 -0
- flowstash/commands/run.py +65 -0
- flowstash/core/__init__.py +0 -0
- flowstash/core/api_client.py +62 -0
- flowstash/core/auth_server.py +45 -0
- flowstash/core/builder.py +40 -0
- flowstash/core/config.py +81 -0
- flowstash/core/docker_utils.py +33 -0
- flowstash/main.py +269 -0
- flowstash/templates/AGENTS.md +222 -0
- flowstash/templates/README.md +135 -0
- flowstash/templates/_.dockerignore +8 -0
- flowstash/templates/_.flowstash +4 -0
- flowstash/templates/_api_main.py +21 -0
- flowstash/templates/_config/[env]/(backend-asyncio)/backend.yaml +4 -0
- flowstash/templates/_config/[env]/(backend-dramatiq)/backend.yaml +8 -0
- flowstash/templates/_config/[env]/(backend-managed)/backend.yaml +6 -0
- flowstash/templates/_config/[env]/_backend.yaml +4 -0
- flowstash/templates/_config/shared/.env +1 -0
- flowstash/templates/_config/shared/backend.yaml +4 -0
- flowstash/templates/_config/shared/clients/demoClient.yaml +39 -0
- flowstash/templates/_config/shared/clients.yaml +3 -0
- flowstash/templates/_deployment/[env]/(backend-asyncio)/docker-compose.yaml +24 -0
- flowstash/templates/_deployment/[env]/(backend-dramatiq)/docker-compose.yaml +34 -0
- flowstash/templates/_deployment/[env]/.env +3 -0
- flowstash/templates/_deployment/shared/.env +5 -0
- flowstash/templates/_deployment/shared/api.Dockerfile +18 -0
- flowstash/templates/_deployment/shared/worker.Dockerfile +18 -0
- flowstash/templates/_pyproject.toml +40 -0
- flowstash/templates/_src/_api/__init__.py +1 -0
- flowstash/templates/_src/_api/_routes/webhooks.py +25 -0
- flowstash/templates/_src/_shared/__init__.py +1 -0
- flowstash/templates/_src/_shared/clients/client.py +18 -0
- flowstash/templates/_src/_shared/models/models.py +1 -0
- flowstash/templates/_src/_shared/tasks/sharedTasks.py +10 -0
- flowstash/templates/_src/_worker/__init__.py +1 -0
- flowstash/templates/_src/_worker/tasks/tasks.py +15 -0
- flowstash/templates/_worker_main.py +25 -0
- flowstash/ui/__init__.py +0 -0
- flowstash_cli-0.1.0.dist-info/METADATA +19 -0
- flowstash_cli-0.1.0.dist-info/RECORD +48 -0
- flowstash_cli-0.1.0.dist-info/WHEEL +4 -0
- flowstash_cli-0.1.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# flowstash Agents Guide
|
|
2
|
+
|
|
3
|
+
This guide explains how to build AI Agents and robust integrations using flowstash. It covers the core concepts you need to know to bring data in, process it, and interact with external systems.
|
|
4
|
+
|
|
5
|
+
## Core Concepts
|
|
6
|
+
|
|
7
|
+
flowstash integrations revolve around the following core ideas:
|
|
8
|
+
- **Integration Context**: Automatically tracks the current integration, pipeline, run, and context across distributed operations.
|
|
9
|
+
- **Ingress**: How data enters your pipelines (via Webhooks or Polled schedules).
|
|
10
|
+
- **Clients**: How you make authenticated requests to external APIs.
|
|
11
|
+
- **Records Feed**: How you queue and deduplicate records for processing.
|
|
12
|
+
- **Project Structure**: How to organize code between API, Worker, and Shared modules.
|
|
13
|
+
|
|
14
|
+
## Bringing Data In: Ingress
|
|
15
|
+
|
|
16
|
+
You can trigger your pipelines through two main ingress decorators provided by `flowstash_lib.pipelines.ingress`:
|
|
17
|
+
|
|
18
|
+
### 1. Webhooks (`@ingress.webhook`)
|
|
19
|
+
Use webhooks when an external system can push events to your application. This decorator registers the handler as part of the integration but leaves the exact handling logic to you.
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from flowstash_lib.pipelines.ingress import ingress
|
|
23
|
+
|
|
24
|
+
@router.post("/webhook/slack")
|
|
25
|
+
@ingress.webhook(pipeline="slack.messages", integration="slack")
|
|
26
|
+
async def handle_slack_webhook(request):
|
|
27
|
+
data = await request.json()
|
|
28
|
+
# Read the webhook and process the data!
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### 2. Polling (`@ingress.poll`)
|
|
32
|
+
Use polling when you need to fetch data on a schedule. This decorator acts as a specialized scheduler entrypoint that injects a durable `state` dictionary into your function. This is perfect for remembering "watermarks" (like the last processed token or timestamp) to paginate stateful APIs.
|
|
33
|
+
|
|
34
|
+
The state is automatically saved for you as long as the function executes without exceptions.
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from flowstash_lib.pipelines.ingress import ingress
|
|
38
|
+
from datetime import datetime
|
|
39
|
+
|
|
40
|
+
@ingress.poll(pipeline="slack.messages", integration="slack", schedule="*/5 * * * *")
|
|
41
|
+
async def poll_slack(state: dict):
|
|
42
|
+
# Retrieve the watermark from the previous run
|
|
43
|
+
since = state.get("since")
|
|
44
|
+
|
|
45
|
+
# Fetch new data using the watermark...
|
|
46
|
+
page = await fetch_messages_from_api(since=since)
|
|
47
|
+
|
|
48
|
+
# Process or publish data
|
|
49
|
+
|
|
50
|
+
# Update the watermark; it will be automatically saved!
|
|
51
|
+
state["since"] = datetime.now()
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Making External Requests: Clients
|
|
55
|
+
|
|
56
|
+
flowstash makes it easy to interact with external APIs via Clients. They handle setting Base URLs, automatic Authentication headers, observability tracing, and defaults out of the box.
|
|
57
|
+
|
|
58
|
+
### Adding a Client
|
|
59
|
+
To configure a new REST client, you place a configuration file (like `slackClient.yaml`) in your shared clients folder. flowstash dynamically loads these clients if your configuration registry points to the folder. For example, if you have `_config/shared/clients.yaml` configured as follows:
|
|
60
|
+
```yaml
|
|
61
|
+
path: clients
|
|
62
|
+
pattern: "*.yaml"
|
|
63
|
+
```
|
|
64
|
+
You can simply define `clients/slackClient.yaml` and the system will expose it to your tasks.
|
|
65
|
+
|
|
66
|
+
### Using a Client
|
|
67
|
+
Once configured, you can retrieve the standard HTTP client (from the `flowstash_clients` package) anywhere.
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from flowstash_clients import get_client
|
|
71
|
+
from flowstash_lib.decorators import integration_task
|
|
72
|
+
|
|
73
|
+
# Load the client configuration
|
|
74
|
+
slack_client = get_client("slackClient")
|
|
75
|
+
|
|
76
|
+
@integration_task(integration="slack", integration_pipeline="send_message")
|
|
77
|
+
async def send_slack_message_task(message: str):
|
|
78
|
+
# Authorization and base URL logic are automatically handled here
|
|
79
|
+
await slack_client.request("POST", "/chat.postMessage", json={"text": message})
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Custom Clients
|
|
83
|
+
If you have an API you interact with heavily, you can define a custom typed client. This encapsulates specific API routes for a better developer experience.
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from flowstash_lib.clients import HttpClient, client
|
|
87
|
+
from typing import List
|
|
88
|
+
|
|
89
|
+
# Extending HttpClient and registering with the @client decorator
|
|
90
|
+
@client("DemoClient")
|
|
91
|
+
class DemoClient(HttpClient):
|
|
92
|
+
"""Custom client for the Demo API."""
|
|
93
|
+
|
|
94
|
+
async def get_users(self) -> List[dict]:
|
|
95
|
+
response = await self.request("GET", "/users")
|
|
96
|
+
return response.json()
|
|
97
|
+
|
|
98
|
+
async def get_user(self, user_id: int) -> dict:
|
|
99
|
+
response = await self.request("GET", f"/users/{user_id}")
|
|
100
|
+
return response.json()
|
|
101
|
+
|
|
102
|
+
# You can then resolve this custom client by name:
|
|
103
|
+
# demo = get_client("DemoClient")
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Processing Data: Records Feed
|
|
107
|
+
|
|
108
|
+
When you receive payloads via Webhooks or Polling, you usually want to process them robustly and asynchronously on the backend. The `RecordsFeed` module provides an opinionated record queue with built-in deduplication that prevents overwhelming pipelines.
|
|
109
|
+
|
|
110
|
+
### Publishing Records
|
|
111
|
+
The framework resolves deduplication using a unique combination of `(integration, record_type, record_id)`. If multiple records arrive with the same identity, "latest-wins" semantics are applied (keeping the one with the latest timestamp).
|
|
112
|
+
|
|
113
|
+
It also seamlessly supports feeding massive data payloads -- large objects (> 5KB) are automatically offloaded to a BlobStore.
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from flowstash_lib.pipelines.records_feed import RecordsFeed
|
|
117
|
+
from flowstash_lib.pipelines.records_model import RecordData
|
|
118
|
+
|
|
119
|
+
# Retrieve your specific feed
|
|
120
|
+
feed = RecordsFeed.get(feed_id="slack.messages")
|
|
121
|
+
|
|
122
|
+
# A common pattern is inserting data retrieved via an `@ingress.poll` into a feed
|
|
123
|
+
await feed.publish(RecordData(
|
|
124
|
+
record_id="msg_123",
|
|
125
|
+
record_type="message",
|
|
126
|
+
data={"text": "Hello World", "user": "U123"}
|
|
127
|
+
))
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Consuming Records
|
|
131
|
+
To process the data asynchronously, decorate a function with `@feed_consumer`. This supports robust configurations like automatic batched receiving, delays, rate-limiting, and concurrency control.
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from flowstash_lib import feed_consumer, RecordData
|
|
135
|
+
|
|
136
|
+
@feed_consumer(
|
|
137
|
+
feed_id="slack.messages",
|
|
138
|
+
batch=True,
|
|
139
|
+
max_batch_size=50,
|
|
140
|
+
max_delay_ms=2000 # wait up to 2 seconds for the queue to fill
|
|
141
|
+
)
|
|
142
|
+
async def process_batch_messages(records: list[RecordData]):
|
|
143
|
+
"""
|
|
144
|
+
Consumes a maximum of 50 records in a batch, waiting
|
|
145
|
+
up to 2 seconds for the queue to fill.
|
|
146
|
+
"""
|
|
147
|
+
print(f"Began processing {len(records)} records.")
|
|
148
|
+
for record in records:
|
|
149
|
+
print(f"Record: {record.record_id} of type {record.record_type}")
|
|
150
|
+
print(f"Data Payload: {record.data}")
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
@feed_consumer(
|
|
155
|
+
feed_id="slack.messages",
|
|
156
|
+
batch=False
|
|
157
|
+
)
|
|
158
|
+
async def process_batch_messages(record: RecordData):
|
|
159
|
+
"""
|
|
160
|
+
Consumes a single record at a time.
|
|
161
|
+
"""
|
|
162
|
+
print(f"Began processing {record.record_id} of type {record.record_type}")
|
|
163
|
+
print(f"Data Payload: {record.data}")
|
|
164
|
+
```
|
|
165
|
+
## Project Structure
|
|
166
|
+
|
|
167
|
+
A typical flowstash project is organized into three main areas: Configuration, Deployment, and Source Code.
|
|
168
|
+
|
|
169
|
+
```text
|
|
170
|
+
.
|
|
171
|
+
├── config/ # Application configuration
|
|
172
|
+
│ ├── shared/ # Base configuration for all environments
|
|
173
|
+
│ │ ├── backend.yaml # Task backend settings (shared)
|
|
174
|
+
│ │ ├── clients.yaml # Points to the clients folder
|
|
175
|
+
│ │ ├── clients/ # Shared client configurations
|
|
176
|
+
│ │ │ └── fooClient.yaml
|
|
177
|
+
│ │ └── .env # Shared environment variables
|
|
178
|
+
│ └── local/ # Environment-specific overrides (e.g., local, dev, prod)
|
|
179
|
+
│ ├── backend.yaml # Local-specific backend settings
|
|
180
|
+
│ └── .env # Local-specific environment variables
|
|
181
|
+
├── deployment/ # deployment-related files
|
|
182
|
+
│ ├── shared/ # Base Dockerfiles
|
|
183
|
+
│ │ ├── api.Dockerfile
|
|
184
|
+
│ │ └── worker.Dockerfile
|
|
185
|
+
│ └── local/ # Local deployment configuration
|
|
186
|
+
│ └── docker-compose.yaml
|
|
187
|
+
├── src/ # Source code for your integration
|
|
188
|
+
│ ├── api/ # API-specific code (Routes, Webhooks)
|
|
189
|
+
│ │ └── routes/
|
|
190
|
+
│ │ └── webhooks.py # typical place for @ingress.webhook ... we can also add the webhooks into purpose specific files ie slack_webhooks.py / stripe_webhooks.py etc
|
|
191
|
+
│ │ └── routes.py # we can also add standard api routes ... like health check etc
|
|
192
|
+
│ ├── worker/ # Worker-specific code (Tasks, Consumers)
|
|
193
|
+
│ │ └── tasks/
|
|
194
|
+
│ │ └── xyzTask.py # place for feed consumers, polling tasks, integration tasks etc
|
|
195
|
+
│ └── shared/ # Shared code (Models, Clients, Utils)
|
|
196
|
+
│ ├── clients/ # here should to the custom clients
|
|
197
|
+
│ └── models/ # here should to the custom models
|
|
198
|
+
├── api_main.py # API Entry point
|
|
199
|
+
└── worker_main.py # Worker Entry point
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Source Organization (`src/`)
|
|
203
|
+
|
|
204
|
+
flowstash separates code based on where it executes to ensure clean isolation and efficient scaling:
|
|
205
|
+
|
|
206
|
+
1. **API (`src/api/`)**: Code that runs on the webserver. Its primary role is to handle incoming requests, typically defined using the `@ingress.webhook` decorator.
|
|
207
|
+
2. **Worker (`src/worker/`)**: Code that runs on the background worker. This is where the "heavy lifting" happens, including `@integration_task`, `@ingress.poll`, and `@feed_consumer` handlers.
|
|
208
|
+
3. **Shared (`src/shared/`)**: Logic used by both the API and Worker, such as custom typed Clients, Pydantic models, and utility functions.
|
|
209
|
+
|
|
210
|
+
### Configuration & Environments
|
|
211
|
+
|
|
212
|
+
flowstash uses a tiered configuration system that allows for seamless transitions between environments:
|
|
213
|
+
|
|
214
|
+
- **Inheritance**: Configuration is loaded from `config/shared/` first, and then overridden by environment-specific files in `config/{env}/`.
|
|
215
|
+
- **Backend Setup**: The `backend.yaml` file defines how tasks are executed (e.g., `asyncio` for local dev or `dramatiq` or `managed` for production).
|
|
216
|
+
- **Environment Variables**: `.env` files in both the shared and environment folders are automatically resolved and injected into the application.
|
|
217
|
+
|
|
218
|
+
### Execution Entry Points
|
|
219
|
+
|
|
220
|
+
- **`api_main.py`**: The entry point for the web server. It sets up the FastAPI application and automatically imports modules from `src/api` to register routes.
|
|
221
|
+
- **`worker_main.py`**: The entry point for the background process. It connects to the task backend (like Redis) and automatically imports modules from `src/worker` to register task signatures.
|
|
222
|
+
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# {project_name}
|
|
2
|
+
|
|
3
|
+
flowstash 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 flowstash CLI to run your environments: `flowstash run local`)*
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## flowstash Core Concepts Usage
|
|
92
|
+
|
|
93
|
+
This project is built on `flowstash_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 flowstash_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 flowstash_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 flowstash_lib import logger
|
|
133
|
+
|
|
134
|
+
logger.info("Processing the invoice batch", details="step 1 completed")
|
|
135
|
+
```
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from fastapi import Request
|
|
3
|
+
from flowstash_lib.config.env_loader import load_config_dir
|
|
4
|
+
from flowstash_runtime.ingress.app import create_fastapi_app
|
|
5
|
+
import os
|
|
6
|
+
env = os.getenv("ENVIRONMENT", "dev")
|
|
7
|
+
# Load configuration relative to this file
|
|
8
|
+
config = load_config_dir("config", environment=env)
|
|
9
|
+
|
|
10
|
+
# Create FastAPI app
|
|
11
|
+
app = create_fastapi_app(config, auto_import=[Path(__file__).parent / "src" / "api" / "routes" / "webhooks.py"])
|
|
12
|
+
|
|
13
|
+
@app.get("/health")
|
|
14
|
+
async def health():
|
|
15
|
+
"""Simple status webhook."""
|
|
16
|
+
return {"status": "ok", "service": "{project_name}-api"}
|
|
17
|
+
|
|
18
|
+
if __name__ == "__main__":
|
|
19
|
+
import uvicorn
|
|
20
|
+
port = int(os.getenv("PORT", 8000))
|
|
21
|
+
uvicorn.run(app, host="0.0.0.0", port=port)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
flowstash_API_URL="http://localhost:8080"
|
|
@@ -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
|
+
# ------------------------------------------s
|
|
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,24 @@
|
|
|
1
|
+
version: '3.8'
|
|
2
|
+
services:
|
|
3
|
+
api:
|
|
4
|
+
build:
|
|
5
|
+
context: ../..
|
|
6
|
+
dockerfile: deployment/shared/api.Dockerfile
|
|
7
|
+
image: flowstash-{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: flowstash-{project_name_slug}-worker:latest
|
|
20
|
+
environment:
|
|
21
|
+
- ENVIRONMENT={env}
|
|
22
|
+
extra_hosts:
|
|
23
|
+
- "host.docker.internal:host-gateway"
|
|
24
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
version: '3.8'
|
|
2
|
+
services:
|
|
3
|
+
api:
|
|
4
|
+
build:
|
|
5
|
+
context: ../..
|
|
6
|
+
dockerfile: deployment/shared/api.Dockerfile
|
|
7
|
+
image: flowstash-{project_name_slug}-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/shared/worker.Dockerfile
|
|
22
|
+
image: flowstash-{project_name_slug}-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,18 @@
|
|
|
1
|
+
# API Dockerfile for {project_name}
|
|
2
|
+
FROM flowstash/flowstash-base:latest
|
|
3
|
+
|
|
4
|
+
WORKDIR /app
|
|
5
|
+
COPY . .
|
|
6
|
+
|
|
7
|
+
# Build arg that chooses which extra(s) to install
|
|
8
|
+
# e.g. SERVICE_EXTRAS="api" or "worker" or "api,worker"
|
|
9
|
+
ARG SERVICE_EXTRAS="api"
|
|
10
|
+
|
|
11
|
+
# Install base deps + extras into system site-packages
|
|
12
|
+
# Base deps:
|
|
13
|
+
RUN uv pip install --system -r pyproject.toml
|
|
14
|
+
RUN uv pip install --system -r pyproject.toml --extra api || echo "no worker extra; skipping"
|
|
15
|
+
RUN rm -rf src/worker
|
|
16
|
+
EXPOSE 8000
|
|
17
|
+
ENV PYTHONPATH=/app/src:/app
|
|
18
|
+
CMD ["python", "api_main.py"]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Worker Dockerfile for {project_name}
|
|
2
|
+
FROM flowstash/flowstash-base:latest
|
|
3
|
+
|
|
4
|
+
WORKDIR /app
|
|
5
|
+
COPY . .
|
|
6
|
+
|
|
7
|
+
# Build arg that chooses which extra(s) to install
|
|
8
|
+
# e.g. SERVICE_EXTRAS="api" or "worker" or "api,worker"
|
|
9
|
+
ARG SERVICE_EXTRAS="worker"
|
|
10
|
+
|
|
11
|
+
# Install base deps + extras into system site-packages
|
|
12
|
+
# Base deps:
|
|
13
|
+
RUN uv pip install --system -r pyproject.toml
|
|
14
|
+
RUN uv pip install --system -r pyproject.toml --extra worker || echo "no worker extra; skipping"
|
|
15
|
+
|
|
16
|
+
RUN rm -rf src/api
|
|
17
|
+
ENV PYTHONPATH=/app/src:/app
|
|
18
|
+
CMD ["python", "-u", "worker_main.py"]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "{project_name}-app"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "flowstash 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 flowstash_lib import ingress
|
|
2
|
+
from flowstash_lib import RecordData
|
|
3
|
+
from fastapi import Request
|
|
4
|
+
from flowstash_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
|