Skip to content

Commit 4063cb1

Browse files
authored
Merge pull request #1 from flamingo-run/feature/first-version
First Version
2 parents 586e69b + 0c2074e commit 4063cb1

45 files changed

Lines changed: 2645 additions & 2 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: Setup FastAPI Cloudflow Environment
2+
description: Common environment setup for all FastAPI Cloudflow jobs
3+
4+
inputs:
5+
github-token:
6+
description: 'GitHub token for authentication'
7+
required: false
8+
9+
runs:
10+
using: "composite"
11+
steps:
12+
- uses: actions/checkout@v4
13+
- name: Install uv
14+
uses: astral-sh/setup-uv@v4
15+
- name: Install Task
16+
uses: arduino/setup-task@v2
17+
with:
18+
version: 3.x
19+
repo-token: ${{ inputs.github-token }}
20+
- name: Install dependencies
21+
run: task install-dev
22+
shell: bash

.github/workflows/ci.yml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
jobs:
9+
lint:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
- uses: ./.github/actions/setup-env
14+
with:
15+
github-token: ${{ secrets.GITHUB_TOKEN }}
16+
- name: Lint with Ruff and Ty
17+
run: task lint
18+
19+
test:
20+
runs-on: ubuntu-latest
21+
steps:
22+
- uses: actions/checkout@v4
23+
- uses: ./.github/actions/setup-env
24+
with:
25+
github-token: ${{ secrets.GITHUB_TOKEN }}
26+
- name: Run tests
27+
run: task test
28+
- name: Report Coverage
29+
run: uv run coverage lcov
30+
- uses: qltysh/qlty-action/coverage@v1
31+
with:
32+
token: ${{ secrets.QLTY_COVERAGE_TOKEN }}
33+
files: coverage.lcov
34+
35+
build:
36+
runs-on: ubuntu-latest
37+
steps:
38+
- uses: actions/checkout@v4
39+
- uses: ./.github/actions/setup-env
40+
with:
41+
github-token: ${{ secrets.GITHUB_TOKEN }}
42+
- name: Build package
43+
run: task build

.github/workflows/publish.yml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
name: Tag & Release Package
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
8+
jobs:
9+
release:
10+
runs-on: ubuntu-latest
11+
permissions:
12+
contents: write
13+
id-token: write
14+
steps:
15+
- uses: actions/checkout@v4
16+
- uses: ./.github/actions/setup-env
17+
with:
18+
github-token: ${{ secrets.GITHUB_TOKEN }}
19+
- name: Detect version upgrade
20+
id: versioning
21+
run: |
22+
package_version=$(uv run python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")
23+
echo "package_version=$package_version" >> $GITHUB_OUTPUT
24+
upgraded=$(git tag --list | grep -q "${package_version}$" && echo "false" || echo "true")
25+
echo "upgraded=$upgraded" >> $GITHUB_OUTPUT
26+
pre_release=$([[ $package_version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] && echo "false" || echo "true")
27+
echo "pre_release=$pre_release" >> $GITHUB_OUTPUT
28+
main_branch_release=$([[ $pre_release == "false" && $GITHUB_REF_NAME == "main" ]] && echo "true" || echo "false")
29+
alternative_branch_release=$([[ $pre_release == "true" && $GITHUB_REF_NAME != "main" ]] && echo "true" || echo "false")
30+
should_release=$([[ $upgraded == "true" && ($main_branch_release == "true" || $alternative_branch_release == "true") ]] && echo "true" || echo "false")
31+
echo "should_release=$should_release" >> $GITHUB_OUTPUT
32+
echo "upgraded=$upgraded"
33+
echo "pre_release=$pre_release"
34+
echo "git_ref=$GITHUB_REF_NAME"
35+
echo "main_branch_release=$main_branch_release"
36+
echo "alternative_branch_release=$alternative_branch_release"
37+
echo "should_release=$should_release"
38+
- name: Create Release
39+
if: ${{ steps.versioning.outputs.should_release == 'true' }}
40+
run: gh release create ${{ steps.versioning.outputs.package_version }} --generate-notes
41+
env:
42+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
43+
- name: Build & Publish package
44+
if: ${{ steps.versioning.outputs.should_release == 'true' }}
45+
run: |
46+
task build
47+
uv publish

.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.13

.vscode/settings.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"[python]": {
3+
"editor.formatOnSave": true,
4+
"editor.codeActionsOnSave": {
5+
"source.fixAll.ruff": "always",
6+
"source.organizeImports.ruff": "always"
7+
},
8+
"editor.detectIndentation": false,
9+
"editor.tabSize": 4,
10+
"editor.defaultFormatter": "charliermarsh.ruff",
11+
"editor.rulers": [
12+
120
13+
],
14+
},
15+
"python.analysis.autoImportCompletions": true,
16+
"python.formatting.provider": "none",
17+
"python.languageServer": "None",
18+
"python.testing.unittestEnabled": false,
19+
"python.testing.pytestEnabled": true,
20+
"python.defaultInterpreterPath": "${workspaceRoot}/.venv/bin/python",
21+
"debug.allowBreakpointsEverywhere": true,
22+
"ruff.nativeServer": "on"
23+
}

README.md

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,35 @@
1-
# fastapi-google-workflows
2-
Framework to create Google Cloud Workflows using FastAPI server
1+
# fastapi-cloudflow
2+
Typed Python-to-Google Cloud Workflows for FastAPI apps. Define steps as typed functions, compose with `>>`, generate Workflows YAML and attach endpoints to your FastAPI app automatically.
3+
4+
## Quickstart (uv + Python 3.13)
5+
6+
```bash
7+
uv run fastapi-cloudflow serve --module app.flows.order
8+
```
9+
10+
Call your workflow locally:
11+
12+
```bash
13+
curl -s localhost:8000/wf/order-flow/run \
14+
-H 'Content-Type: application/json' \
15+
-d '{"account_id":1,"sku":"abc","qty":2}'
16+
```
17+
18+
Emit YAML:
19+
20+
```bash
21+
uv run fastapi-cloudflow build --module app.flows.order
22+
```
23+
24+
Attach to an existing FastAPI app:
25+
26+
```python
27+
from fastapi import FastAPI
28+
from fastapi_cloudflow import attach_to_fastapi
29+
from app.flows.order import WORKFLOWS
30+
31+
app = FastAPI()
32+
attach_to_fastapi(app, WORKFLOWS)
33+
```
34+
35+
See `docs/` for architecture, decisions, and roadmap.

Taskfile.yml

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
version: '3'
2+
3+
tasks:
4+
install:
5+
desc: Install production dependencies
6+
cmds:
7+
- uv sync --no-dev
8+
9+
install-dev:
10+
desc: Install development dependencies
11+
cmds:
12+
- uv sync --group docs --group dev
13+
- uv pip install -e .
14+
15+
install-docs:
16+
desc: Install documentation dependencies
17+
cmds:
18+
- uv sync --group docs
19+
20+
update:
21+
desc: Update dependencies
22+
cmds:
23+
- uv lock --upgrade
24+
25+
test:
26+
desc: Run tests
27+
deps: [install-dev]
28+
cmds:
29+
- uv run pytest
30+
31+
lint:
32+
desc: Run linting
33+
cmds:
34+
- uv run ruff check .
35+
- uv run ruff format --check .
36+
- uv run ty check
37+
38+
format:
39+
desc: Format code
40+
cmds:
41+
- uv run ruff format .
42+
- uv run ruff check --fix --unsafe-fixes .
43+
44+
clean:
45+
desc: Clean build artifacts
46+
cmds:
47+
- rm -rf build/
48+
- rm -rf dist/
49+
- rm -rf src/*.egg-info/
50+
- rm -rf .coverage
51+
- rm -rf htmlcov/
52+
53+
build:
54+
desc: Build package
55+
deps: [clean]
56+
cmds:
57+
- uv build
58+
59+
publish:
60+
desc: Publish to PyPI
61+
deps: [build]
62+
cmds:
63+
- uv publish
64+
65+
docs-build:
66+
desc: Build the documentation
67+
deps: [install-docs]
68+
cmds:
69+
- uv run mkdocs build --config-file ./mkdocs.yml
70+
71+
docs-serve:
72+
desc: Serve the documentation locally
73+
deps: [install-docs]
74+
cmds:
75+
- uv run mkdocs serve --config-file ./mkdocs.yml
76+
77+
docs-deploy:
78+
desc: Deploy documentation to GitHub Pages
79+
deps: [install-docs]
80+
cmds:
81+
- uv run mkdocs gh-deploy --config-file ./mkdocs.yml

app/flows/order.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from __future__ import annotations
2+
3+
from decimal import Decimal
4+
from uuid import uuid4
5+
6+
from pydantic import BaseModel
7+
8+
from fastapi_cloudflow import Context, step, workflow
9+
10+
11+
class CreateOrder(BaseModel):
12+
account_id: int
13+
sku: str
14+
qty: int
15+
16+
17+
class OrderDraft(BaseModel):
18+
order_id: str
19+
price: Decimal
20+
21+
22+
class PaymentAuth(BaseModel):
23+
order_id: str
24+
status: str
25+
26+
27+
@step(name="price-order")
28+
async def price_order(ctx: Context, data: CreateOrder) -> OrderDraft:
29+
return OrderDraft(order_id=f"o-{uuid4().hex}", price=Decimal("12.34"))
30+
31+
32+
@step(name="auth-payment")
33+
async def auth_payment(ctx: Context, data: OrderDraft) -> PaymentAuth:
34+
return PaymentAuth(order_id=data.order_id, status="approved")
35+
36+
37+
# Building the workflow auto-registers it in the registry
38+
ORDER_FLOW = (workflow("order-flow") >> price_order >> auth_payment).build()

app/flows/payments.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from __future__ import annotations
2+
3+
from pydantic import BaseModel
4+
5+
from fastapi_cloudflow import Arg, AssignStep, Context, HttpStep, step, workflow
6+
7+
8+
class Cart(BaseModel):
9+
total: float
10+
currency: str
11+
12+
13+
class PSPReq(BaseModel):
14+
amount: float
15+
currency: str
16+
17+
18+
class PSPRes(BaseModel):
19+
status: str
20+
psp_id: str
21+
22+
23+
@step(name="validate-cart")
24+
async def validate_cart(ctx: Context, data: Cart) -> Cart:
25+
assert data.total >= 0
26+
return data
27+
28+
29+
to_psp = AssignStep("cart->psp", Cart, PSPReq, expr={"amount": "${payload.total}", "currency": "${payload.currency}"})
30+
31+
psp = HttpStep("psp-charge", PSPReq, PSPRes, method="POST", url=Arg.env("PSP_URL") / "charge")
32+
33+
34+
class ChargeResult(BaseModel):
35+
ok: bool
36+
txn_id: str | None
37+
38+
39+
@step(name="summarize-charge")
40+
async def summarize_charge(ctx: Context, data: PSPRes) -> ChargeResult:
41+
return ChargeResult(ok=(data.status == "approved"), txn_id=data.psp_id)
42+
43+
44+
PAYMENT_FLOW = (workflow("payment-flow") >> validate_cart >> to_psp >> psp >> summarize_charge).build()

0 commit comments

Comments
 (0)