pgflows 0.1.0__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.
pgflows-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,304 @@
1
+ Metadata-Version: 2.3
2
+ Name: pgflows
3
+ Version: 0.1.0
4
+ Summary: Durable workflow engine SDK for Python + Postgres
5
+ Keywords: workflow,durable,postgres,async
6
+ Author: Nir Adler
7
+ Author-email: Nir Adler <me@niradler.com>
8
+ License: MIT
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Typing :: Typed
15
+ Requires-Dist: pydantic
16
+ Requires-Dist: asyncpg
17
+ Requires-Dist: tembo-pgmq-python
18
+ Requires-Dist: opentelemetry-api
19
+ Requires-Dist: opentelemetry-sdk
20
+ Requires-Dist: opentelemetry-exporter-otlp-proto-grpc
21
+ Requires-Dist: fastapi ; extra == 'fastapi'
22
+ Requires-Dist: uvicorn[standard] ; extra == 'fastapi'
23
+ Requires-Python: >=3.13
24
+ Project-URL: Homepage, https://github.com/niradler/pyflows
25
+ Project-URL: Repository, https://github.com/niradler/pyflows
26
+ Project-URL: Issues, https://github.com/niradler/pyflows/issues
27
+ Provides-Extra: fastapi
28
+ Description-Content-Type: text/markdown
29
+
30
+ # pyflows
31
+
32
+ [![PyPI](https://img.shields.io/pypi/v/pyflows)](https://pypi.org/project/pyflows/)
33
+ [![Python](https://img.shields.io/pypi/pyversions/pyflows)](https://pypi.org/project/pyflows/)
34
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
35
+
36
+ > Durable workflow engine SDK for Python + Postgres
37
+
38
+ pyflows lets you write long-running, fault-tolerant workflows as plain async Python functions — backed entirely by your existing Postgres database. No extra infrastructure, no separate orchestration service, no new runtime to operate.
39
+
40
+ > [!WARNING]
41
+ > **Early development (alpha).** The core API is stabilizing but not yet 1.0. Expect breaking changes before the first stable release.
42
+
43
+ ## How it works
44
+
45
+ Each workflow step is persisted to Postgres before execution. If the process crashes mid-run, the worker replays from the last checkpoint — re-executing only the steps that haven't completed. All state, retries, and scheduling live in the database.
46
+
47
+ ```text
48
+ @workflow fn → WorkflowApp.start()
49
+ ↓ enqueues to pgmq
50
+ Python async worker
51
+ ↓ executes steps
52
+ PgStateBackend ←→ Postgres
53
+ ```
54
+
55
+ ## Quick start
56
+
57
+ ```bash
58
+ docker compose up -d # start Postgres with pgmq
59
+ uv add pyflows
60
+ ```
61
+
62
+ ```python
63
+ import asyncio
64
+ from pydantic import BaseModel
65
+ from pyflows import PyflowsConfig, RetryConfig, StepContext, WorkflowApp, WorkflowContext
66
+
67
+ config = PyflowsConfig(
68
+ dsn="postgresql://pyflows:pyflows@127.0.0.1:5433/pyflows_test",
69
+ otel_enabled=False,
70
+ db_ssl=False,
71
+ )
72
+ app = WorkflowApp(config=config)
73
+
74
+
75
+ class OrderInput(BaseModel):
76
+ order_id: str
77
+ amount: float
78
+
79
+
80
+ class OrderResult(BaseModel):
81
+ charged: bool
82
+ confirmation: str
83
+
84
+
85
+ @app.step(retry=RetryConfig(max_retries=3, initial_delay_seconds=1.0))
86
+ async def charge_payment(ctx: StepContext, input: OrderInput) -> OrderResult:
87
+ # call your payment API here
88
+ return OrderResult(charged=True, confirmation=f"CHG-{input.order_id}")
89
+
90
+
91
+ @app.workflow()
92
+ async def process_order(ctx: WorkflowContext, input: OrderInput) -> OrderResult:
93
+ return await ctx.step(charge_payment, input)
94
+
95
+
96
+ async def main() -> None:
97
+ await app.initialize()
98
+ instance_id = await app.start(process_order, OrderInput(order_id="ORD-1", amount=99.0))
99
+ await app.process_once()
100
+ status = await app.get_status(instance_id)
101
+ print(status.state, status.output)
102
+ await app.close()
103
+
104
+ asyncio.run(main())
105
+ ```
106
+
107
+ ## Features
108
+
109
+ - **Checkpoint replay** — workflows survive crashes; completed steps are never re-executed
110
+ - **Typed end-to-end** — step inputs and outputs are Pydantic models; no `dict[str, Any]` at the boundary
111
+ - **Configurable retries** — per-step `RetryConfig` with exponential or linear backoff and jitter
112
+ - **Plugin hooks** — `before_workflow`, `after_workflow`, `on_workflow_error`, `before_step`, `after_step`, `on_step_error`
113
+ - **Automatic migrations** — `await app.initialize()` applies schema migrations; no manual SQL required
114
+ - **Cron scheduler** — trigger recurring workflows via `PgCronBackend` (backed by pg_durable `df.wait_for_schedule`)
115
+ - **Dead-letter queue** — failed workflows are archived to `pgmq.a_{queue}` instead of being re-queued indefinitely
116
+ - **Worker coordination** — atomic `pending→running` claim prevents duplicate processing when multiple workers race on the same instance
117
+ - **Swappable backends** — orchestrator, queue, and scheduler implement ABCs; swap without touching workflow code
118
+ - **OpenTelemetry** — built-in span management for workflows and steps
119
+
120
+ ## Plugin system
121
+
122
+ ```python
123
+ from pyflows import LoggingPlugin, PyflowsPlugin, StepEvent, WorkflowEvent
124
+
125
+ # Built-in: log all lifecycle events
126
+ app.register_plugin(LoggingPlugin())
127
+
128
+ # Custom: implement any subset of hooks
129
+ class MetricsPlugin(PyflowsPlugin):
130
+ async def after_step(self, event: StepEvent, result: object) -> None:
131
+ metrics.record("step.completed", tags={"step": event.step_name})
132
+
133
+ async def on_workflow_error(self, event: WorkflowEvent, error: Exception) -> None:
134
+ metrics.record("workflow.failed", tags={"workflow": event.workflow_name})
135
+
136
+ app.register_plugin(MetricsPlugin())
137
+ ```
138
+
139
+ Plugins are called in registration order. A plugin that raises never affects other plugins or the workflow itself.
140
+
141
+ ## Retry configuration
142
+
143
+ ```python
144
+ from pyflows import RetryConfig
145
+
146
+ # Per-step retry (backoff can be "exponential" or "linear")
147
+ @app.step(retry=RetryConfig(max_retries=5, initial_delay_seconds=2.0, max_delay_seconds=60.0, backoff="exponential"))
148
+ async def my_step(ctx, input: MyInput) -> MyOutput: ...
149
+
150
+ # Workflow-level defaults (applied to all steps unless overridden)
151
+ @app.workflow(step_defaults=RetryConfig(max_retries=2))
152
+ async def my_workflow(ctx, input: MyInput) -> MyOutput: ...
153
+ ```
154
+
155
+ ## SQL export and runtime workflows
156
+
157
+ pyflows can export any registered workflow to a [pg_durable](https://github.com/microsoft/pg_durable) SQL DSL. Use this to:
158
+
159
+ - Transfer workflow definitions from dev → prod without code deployment
160
+ - Create workflows at runtime from config, API payloads, or external systems
161
+ - Inspect the step sequence of any workflow before executing it
162
+
163
+ ### Export a Python workflow to SQL
164
+
165
+ ```python
166
+ from pyflows import SqlExporter
167
+
168
+ exporter = SqlExporter(registry=app.registry, base_url="http://my-app:8000")
169
+
170
+ # Full SQL ready to run against a Postgres database with pg_durable
171
+ sql = exporter.export_workflow("process_order")
172
+
173
+ # Dry-run: inspect steps without producing runnable SQL
174
+ result = exporter.dry_run("process_order")
175
+ print(result.steps) # [StepSql(step_name='charge_payment', ...)]
176
+ print(result.sql) # pg_durable DSL
177
+ ```
178
+
179
+ ### Compose a workflow at runtime from step names
180
+
181
+ When you want to define a workflow without writing a Python function — from a config file, an API request, or a database record — use `compose()`. Each step name must already be registered with the app.
182
+
183
+ ```python
184
+ # No Python workflow function needed — compose step sequences dynamically
185
+ sql = exporter.compose(
186
+ workflow_name="on_call_response",
187
+ steps=["check_service_health", "diagnose_incident", "apply_remediation"],
188
+ )
189
+
190
+ # Execute sql against Postgres with pg_durable to start the workflow
191
+ ```
192
+
193
+ The `compose()` call validates that every step name is registered, so typos raise a `KeyError` immediately rather than failing silently at runtime.
194
+
195
+ ### Export all workflows
196
+
197
+ ```python
198
+ # All registered workflows in one SQL file (dev → prod migration)
199
+ sql = exporter.export_all()
200
+ ```
201
+
202
+ ## Scheduling
203
+
204
+ `PgCronBackend` schedules recurring workflows using pg_durable's `df.wait_for_schedule()` — no `pg_cron` extension required, only the `df` extension from [pg_durable](https://github.com/microsoft/pg_durable).
205
+
206
+ ```python
207
+ from pyflows import PgCronBackend
208
+
209
+ scheduler = PgCronBackend(dsn=config.dsn)
210
+ await scheduler.initialize()
211
+
212
+ # Schedule a workflow to run every hour (job_id is a pg_durable instance ID string)
213
+ job_id = await scheduler.schedule(
214
+ job_name="hourly_health_check",
215
+ cron="0 * * * *",
216
+ command="SELECT pyflows.enqueue_workflow('health_check', '{}')",
217
+ )
218
+
219
+ jobs = await scheduler.list_jobs()
220
+ await scheduler.unschedule(job_id)
221
+ ```
222
+
223
+ Check whether pg_durable is installed at runtime:
224
+
225
+ ```python
226
+ await app.initialize()
227
+ if app.pg_durable_available:
228
+ # scheduler and push-mode SQL export are usable
229
+ ...
230
+ ```
231
+
232
+ ## Backend abstraction
233
+
234
+ Every infrastructure concern is behind an ABC in `backends/base.py`. Swap backends without touching workflow code:
235
+
236
+ | Component | Default | Interface |
237
+ | --------- | ------- | --------- |
238
+ | State + checkpoints | `PgStateBackend` | `OrchestratorBackend` |
239
+ | Step queue | `PgmqBackend` | `QueueBackend` |
240
+ | Cron scheduling | `PgCronBackend` | `SchedulerBackend` |
241
+
242
+ ```python
243
+ # Bring your own queue backend
244
+ class RedisQueueBackend(QueueBackend):
245
+ ...
246
+
247
+ app = WorkflowApp(config=config)
248
+ # Use custom backend by injecting into WorkflowWorker directly
249
+ ```
250
+
251
+ ## Requirements
252
+
253
+ **Python:** 3.13+
254
+
255
+ **Postgres extensions** (15+):
256
+
257
+ | Extension | Purpose | Required |
258
+ | --------- | ------- | -------- |
259
+ | [`pgmq`](https://github.com/tembo-io/pgmq) | Step queue | Yes |
260
+ | [`pg_durable` (`df`)](https://github.com/microsoft/pg_durable) | Cron scheduling, push-mode SQL export | Optional |
261
+
262
+ The bundled `docker-compose.yml` starts a Postgres instance with `pgmq` pre-installed:
263
+
264
+ ```bash
265
+ docker compose up -d
266
+ ```
267
+
268
+ ## Installation
269
+
270
+ ```bash
271
+ pip install pyflows
272
+ # or
273
+ uv add pyflows
274
+ ```
275
+
276
+ ## Development
277
+
278
+ ```bash
279
+ uv sync # install deps
280
+ uv run pytest tests/unit/ # unit tests (no DB needed)
281
+ docker compose up -d # start Postgres
282
+ uv run pytest tests/e2e/ # E2E tests
283
+ uv run ruff check src/ tests/ # lint
284
+ ```
285
+
286
+ ## AI SRE example
287
+
288
+ See [`examples/ai_sre/workflow.py`](examples/ai_sre/workflow.py) for a full incident response workflow: health check → AI diagnosis → auto-remediation, with retries, plugin hooks, and typed I/O.
289
+
290
+ ## Roadmap
291
+
292
+ - [x] M1 — Project scaffold: backend ABCs, Pydantic types, exception hierarchy
293
+ - [x] M2 — Core SDK: `WorkflowApp`, `@step`, `@workflow`, `WorkflowContext`, replay engine
294
+ - [x] M3 — SqlExporter: Python workflow → pg_durable DSL (AST-based)
295
+ - [x] M4 — E2E test suite: basic, retry, monitor/cancel (Docker-based)
296
+ - [x] M6 — Plugin system: `PyflowsPlugin` ABC, `LoggingPlugin`, lifecycle hooks
297
+ - [x] M7 — Migrations + scheduler: versioned schema migrations, `PgCronBackend` via pg_durable
298
+ - [x] M8 — AI SRE example, README, production hardening: DLQ, worker coordination, linear backoff, pg_durable detection
299
+ - [ ] M5 — FastAPI integration: push endpoint (deferred; pull mode works without it)
300
+ - [ ] M9 — PyPI release + full documentation
301
+
302
+ ## License
303
+
304
+ MIT
@@ -0,0 +1,275 @@
1
+ # pyflows
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/pyflows)](https://pypi.org/project/pyflows/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/pyflows)](https://pypi.org/project/pyflows/)
5
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
+
7
+ > Durable workflow engine SDK for Python + Postgres
8
+
9
+ pyflows lets you write long-running, fault-tolerant workflows as plain async Python functions — backed entirely by your existing Postgres database. No extra infrastructure, no separate orchestration service, no new runtime to operate.
10
+
11
+ > [!WARNING]
12
+ > **Early development (alpha).** The core API is stabilizing but not yet 1.0. Expect breaking changes before the first stable release.
13
+
14
+ ## How it works
15
+
16
+ Each workflow step is persisted to Postgres before execution. If the process crashes mid-run, the worker replays from the last checkpoint — re-executing only the steps that haven't completed. All state, retries, and scheduling live in the database.
17
+
18
+ ```text
19
+ @workflow fn → WorkflowApp.start()
20
+ ↓ enqueues to pgmq
21
+ Python async worker
22
+ ↓ executes steps
23
+ PgStateBackend ←→ Postgres
24
+ ```
25
+
26
+ ## Quick start
27
+
28
+ ```bash
29
+ docker compose up -d # start Postgres with pgmq
30
+ uv add pyflows
31
+ ```
32
+
33
+ ```python
34
+ import asyncio
35
+ from pydantic import BaseModel
36
+ from pyflows import PyflowsConfig, RetryConfig, StepContext, WorkflowApp, WorkflowContext
37
+
38
+ config = PyflowsConfig(
39
+ dsn="postgresql://pyflows:pyflows@127.0.0.1:5433/pyflows_test",
40
+ otel_enabled=False,
41
+ db_ssl=False,
42
+ )
43
+ app = WorkflowApp(config=config)
44
+
45
+
46
+ class OrderInput(BaseModel):
47
+ order_id: str
48
+ amount: float
49
+
50
+
51
+ class OrderResult(BaseModel):
52
+ charged: bool
53
+ confirmation: str
54
+
55
+
56
+ @app.step(retry=RetryConfig(max_retries=3, initial_delay_seconds=1.0))
57
+ async def charge_payment(ctx: StepContext, input: OrderInput) -> OrderResult:
58
+ # call your payment API here
59
+ return OrderResult(charged=True, confirmation=f"CHG-{input.order_id}")
60
+
61
+
62
+ @app.workflow()
63
+ async def process_order(ctx: WorkflowContext, input: OrderInput) -> OrderResult:
64
+ return await ctx.step(charge_payment, input)
65
+
66
+
67
+ async def main() -> None:
68
+ await app.initialize()
69
+ instance_id = await app.start(process_order, OrderInput(order_id="ORD-1", amount=99.0))
70
+ await app.process_once()
71
+ status = await app.get_status(instance_id)
72
+ print(status.state, status.output)
73
+ await app.close()
74
+
75
+ asyncio.run(main())
76
+ ```
77
+
78
+ ## Features
79
+
80
+ - **Checkpoint replay** — workflows survive crashes; completed steps are never re-executed
81
+ - **Typed end-to-end** — step inputs and outputs are Pydantic models; no `dict[str, Any]` at the boundary
82
+ - **Configurable retries** — per-step `RetryConfig` with exponential or linear backoff and jitter
83
+ - **Plugin hooks** — `before_workflow`, `after_workflow`, `on_workflow_error`, `before_step`, `after_step`, `on_step_error`
84
+ - **Automatic migrations** — `await app.initialize()` applies schema migrations; no manual SQL required
85
+ - **Cron scheduler** — trigger recurring workflows via `PgCronBackend` (backed by pg_durable `df.wait_for_schedule`)
86
+ - **Dead-letter queue** — failed workflows are archived to `pgmq.a_{queue}` instead of being re-queued indefinitely
87
+ - **Worker coordination** — atomic `pending→running` claim prevents duplicate processing when multiple workers race on the same instance
88
+ - **Swappable backends** — orchestrator, queue, and scheduler implement ABCs; swap without touching workflow code
89
+ - **OpenTelemetry** — built-in span management for workflows and steps
90
+
91
+ ## Plugin system
92
+
93
+ ```python
94
+ from pyflows import LoggingPlugin, PyflowsPlugin, StepEvent, WorkflowEvent
95
+
96
+ # Built-in: log all lifecycle events
97
+ app.register_plugin(LoggingPlugin())
98
+
99
+ # Custom: implement any subset of hooks
100
+ class MetricsPlugin(PyflowsPlugin):
101
+ async def after_step(self, event: StepEvent, result: object) -> None:
102
+ metrics.record("step.completed", tags={"step": event.step_name})
103
+
104
+ async def on_workflow_error(self, event: WorkflowEvent, error: Exception) -> None:
105
+ metrics.record("workflow.failed", tags={"workflow": event.workflow_name})
106
+
107
+ app.register_plugin(MetricsPlugin())
108
+ ```
109
+
110
+ Plugins are called in registration order. A plugin that raises never affects other plugins or the workflow itself.
111
+
112
+ ## Retry configuration
113
+
114
+ ```python
115
+ from pyflows import RetryConfig
116
+
117
+ # Per-step retry (backoff can be "exponential" or "linear")
118
+ @app.step(retry=RetryConfig(max_retries=5, initial_delay_seconds=2.0, max_delay_seconds=60.0, backoff="exponential"))
119
+ async def my_step(ctx, input: MyInput) -> MyOutput: ...
120
+
121
+ # Workflow-level defaults (applied to all steps unless overridden)
122
+ @app.workflow(step_defaults=RetryConfig(max_retries=2))
123
+ async def my_workflow(ctx, input: MyInput) -> MyOutput: ...
124
+ ```
125
+
126
+ ## SQL export and runtime workflows
127
+
128
+ pyflows can export any registered workflow to a [pg_durable](https://github.com/microsoft/pg_durable) SQL DSL. Use this to:
129
+
130
+ - Transfer workflow definitions from dev → prod without code deployment
131
+ - Create workflows at runtime from config, API payloads, or external systems
132
+ - Inspect the step sequence of any workflow before executing it
133
+
134
+ ### Export a Python workflow to SQL
135
+
136
+ ```python
137
+ from pyflows import SqlExporter
138
+
139
+ exporter = SqlExporter(registry=app.registry, base_url="http://my-app:8000")
140
+
141
+ # Full SQL ready to run against a Postgres database with pg_durable
142
+ sql = exporter.export_workflow("process_order")
143
+
144
+ # Dry-run: inspect steps without producing runnable SQL
145
+ result = exporter.dry_run("process_order")
146
+ print(result.steps) # [StepSql(step_name='charge_payment', ...)]
147
+ print(result.sql) # pg_durable DSL
148
+ ```
149
+
150
+ ### Compose a workflow at runtime from step names
151
+
152
+ When you want to define a workflow without writing a Python function — from a config file, an API request, or a database record — use `compose()`. Each step name must already be registered with the app.
153
+
154
+ ```python
155
+ # No Python workflow function needed — compose step sequences dynamically
156
+ sql = exporter.compose(
157
+ workflow_name="on_call_response",
158
+ steps=["check_service_health", "diagnose_incident", "apply_remediation"],
159
+ )
160
+
161
+ # Execute sql against Postgres with pg_durable to start the workflow
162
+ ```
163
+
164
+ The `compose()` call validates that every step name is registered, so typos raise a `KeyError` immediately rather than failing silently at runtime.
165
+
166
+ ### Export all workflows
167
+
168
+ ```python
169
+ # All registered workflows in one SQL file (dev → prod migration)
170
+ sql = exporter.export_all()
171
+ ```
172
+
173
+ ## Scheduling
174
+
175
+ `PgCronBackend` schedules recurring workflows using pg_durable's `df.wait_for_schedule()` — no `pg_cron` extension required, only the `df` extension from [pg_durable](https://github.com/microsoft/pg_durable).
176
+
177
+ ```python
178
+ from pyflows import PgCronBackend
179
+
180
+ scheduler = PgCronBackend(dsn=config.dsn)
181
+ await scheduler.initialize()
182
+
183
+ # Schedule a workflow to run every hour (job_id is a pg_durable instance ID string)
184
+ job_id = await scheduler.schedule(
185
+ job_name="hourly_health_check",
186
+ cron="0 * * * *",
187
+ command="SELECT pyflows.enqueue_workflow('health_check', '{}')",
188
+ )
189
+
190
+ jobs = await scheduler.list_jobs()
191
+ await scheduler.unschedule(job_id)
192
+ ```
193
+
194
+ Check whether pg_durable is installed at runtime:
195
+
196
+ ```python
197
+ await app.initialize()
198
+ if app.pg_durable_available:
199
+ # scheduler and push-mode SQL export are usable
200
+ ...
201
+ ```
202
+
203
+ ## Backend abstraction
204
+
205
+ Every infrastructure concern is behind an ABC in `backends/base.py`. Swap backends without touching workflow code:
206
+
207
+ | Component | Default | Interface |
208
+ | --------- | ------- | --------- |
209
+ | State + checkpoints | `PgStateBackend` | `OrchestratorBackend` |
210
+ | Step queue | `PgmqBackend` | `QueueBackend` |
211
+ | Cron scheduling | `PgCronBackend` | `SchedulerBackend` |
212
+
213
+ ```python
214
+ # Bring your own queue backend
215
+ class RedisQueueBackend(QueueBackend):
216
+ ...
217
+
218
+ app = WorkflowApp(config=config)
219
+ # Use custom backend by injecting into WorkflowWorker directly
220
+ ```
221
+
222
+ ## Requirements
223
+
224
+ **Python:** 3.13+
225
+
226
+ **Postgres extensions** (15+):
227
+
228
+ | Extension | Purpose | Required |
229
+ | --------- | ------- | -------- |
230
+ | [`pgmq`](https://github.com/tembo-io/pgmq) | Step queue | Yes |
231
+ | [`pg_durable` (`df`)](https://github.com/microsoft/pg_durable) | Cron scheduling, push-mode SQL export | Optional |
232
+
233
+ The bundled `docker-compose.yml` starts a Postgres instance with `pgmq` pre-installed:
234
+
235
+ ```bash
236
+ docker compose up -d
237
+ ```
238
+
239
+ ## Installation
240
+
241
+ ```bash
242
+ pip install pyflows
243
+ # or
244
+ uv add pyflows
245
+ ```
246
+
247
+ ## Development
248
+
249
+ ```bash
250
+ uv sync # install deps
251
+ uv run pytest tests/unit/ # unit tests (no DB needed)
252
+ docker compose up -d # start Postgres
253
+ uv run pytest tests/e2e/ # E2E tests
254
+ uv run ruff check src/ tests/ # lint
255
+ ```
256
+
257
+ ## AI SRE example
258
+
259
+ See [`examples/ai_sre/workflow.py`](examples/ai_sre/workflow.py) for a full incident response workflow: health check → AI diagnosis → auto-remediation, with retries, plugin hooks, and typed I/O.
260
+
261
+ ## Roadmap
262
+
263
+ - [x] M1 — Project scaffold: backend ABCs, Pydantic types, exception hierarchy
264
+ - [x] M2 — Core SDK: `WorkflowApp`, `@step`, `@workflow`, `WorkflowContext`, replay engine
265
+ - [x] M3 — SqlExporter: Python workflow → pg_durable DSL (AST-based)
266
+ - [x] M4 — E2E test suite: basic, retry, monitor/cancel (Docker-based)
267
+ - [x] M6 — Plugin system: `PyflowsPlugin` ABC, `LoggingPlugin`, lifecycle hooks
268
+ - [x] M7 — Migrations + scheduler: versioned schema migrations, `PgCronBackend` via pg_durable
269
+ - [x] M8 — AI SRE example, README, production hardening: DLQ, worker coordination, linear backoff, pg_durable detection
270
+ - [ ] M5 — FastAPI integration: push endpoint (deferred; pull mode works without it)
271
+ - [ ] M9 — PyPI release + full documentation
272
+
273
+ ## License
274
+
275
+ MIT
@@ -0,0 +1,62 @@
1
+ [project]
2
+ name = "pgflows"
3
+ version = "0.1.0"
4
+ description = "Durable workflow engine SDK for Python + Postgres"
5
+ readme = "README.md"
6
+ license = { text = "MIT" }
7
+ authors = [{ name = "Nir Adler", email = "me@niradler.com" }]
8
+ keywords = ["workflow", "durable", "postgres", "async"]
9
+ classifiers = [
10
+ "Development Status :: 3 - Alpha",
11
+ "Intended Audience :: Developers",
12
+ "License :: OSI Approved :: MIT License",
13
+ "Programming Language :: Python :: 3",
14
+ "Programming Language :: Python :: 3.13",
15
+ "Typing :: Typed",
16
+ ]
17
+ requires-python = ">=3.13"
18
+ dependencies = [
19
+ "pydantic",
20
+ "asyncpg",
21
+ "tembo-pgmq-python",
22
+ "opentelemetry-api",
23
+ "opentelemetry-sdk",
24
+ "opentelemetry-exporter-otlp-proto-grpc",
25
+ ]
26
+
27
+ [project.optional-dependencies]
28
+ fastapi = ["fastapi", "uvicorn[standard]"]
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/niradler/pyflows"
32
+ Repository = "https://github.com/niradler/pyflows"
33
+ Issues = "https://github.com/niradler/pyflows/issues"
34
+
35
+ [build-system]
36
+ requires = ["uv_build>=0.10.0,<0.11.0"]
37
+ build-backend = "uv_build"
38
+
39
+ [tool.uv.build-backend]
40
+ module-name = "pyflows"
41
+
42
+ [dependency-groups]
43
+ dev = [
44
+ "pytest>=8.0",
45
+ "pytest-asyncio",
46
+ "pytest-timeout",
47
+ "anyio",
48
+ "ruff>=0.9",
49
+ "fastapi",
50
+ "uvicorn[standard]",
51
+ "httpx",
52
+ ]
53
+
54
+ [tool.ruff]
55
+ line-length = 100
56
+
57
+ [tool.ruff.lint]
58
+ select = ["E", "F", "I", "UP"]
59
+
60
+ [tool.pytest.ini_options]
61
+ asyncio_mode = "auto"
62
+ timeout = 30