forktex-flow 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.
- forktex_flow-0.1.0/LICENSE +45 -0
- forktex_flow-0.1.0/NOTICE +23 -0
- forktex_flow-0.1.0/PKG-INFO +357 -0
- forktex_flow-0.1.0/README.md +322 -0
- forktex_flow-0.1.0/pyproject.toml +148 -0
- forktex_flow-0.1.0/src/forktex_flow/__init__.py +106 -0
- forktex_flow-0.1.0/src/forktex_flow/domain/__init__.py +76 -0
- forktex_flow-0.1.0/src/forktex_flow/domain/config.py +88 -0
- forktex_flow-0.1.0/src/forktex_flow/domain/definition.py +125 -0
- forktex_flow-0.1.0/src/forktex_flow/domain/filter.py +71 -0
- forktex_flow-0.1.0/src/forktex_flow/domain/node.py +137 -0
- forktex_flow-0.1.0/src/forktex_flow/domain/state.py +107 -0
- forktex_flow-0.1.0/src/forktex_flow/domain/types.py +106 -0
- forktex_flow-0.1.0/src/forktex_flow/errors.py +108 -0
- forktex_flow-0.1.0/src/forktex_flow/extension.py +81 -0
- forktex_flow-0.1.0/src/forktex_flow/flow.py +345 -0
- forktex_flow-0.1.0/src/forktex_flow/persist/__init__.py +50 -0
- forktex_flow-0.1.0/src/forktex_flow/persist/mappers.py +73 -0
- forktex_flow-0.1.0/src/forktex_flow/persist/migrations/__init__.py +68 -0
- forktex_flow-0.1.0/src/forktex_flow/persist/migrations/v0001__schema.sql +125 -0
- forktex_flow-0.1.0/src/forktex_flow/persist/models.py +206 -0
- forktex_flow-0.1.0/src/forktex_flow/persist/runs.py +231 -0
- forktex_flow-0.1.0/src/forktex_flow/persist/steps.py +284 -0
- forktex_flow-0.1.0/src/forktex_flow/persist/store.py +66 -0
- forktex_flow-0.1.0/src/forktex_flow/py.typed +0 -0
- forktex_flow-0.1.0/src/forktex_flow/read/__init__.py +49 -0
- forktex_flow-0.1.0/src/forktex_flow/read/instance.py +347 -0
- forktex_flow-0.1.0/src/forktex_flow/read/query.py +231 -0
- forktex_flow-0.1.0/src/forktex_flow/runtime/__init__.py +51 -0
- forktex_flow-0.1.0/src/forktex_flow/runtime/compiler.py +168 -0
- forktex_flow-0.1.0/src/forktex_flow/runtime/context.py +53 -0
- forktex_flow-0.1.0/src/forktex_flow/runtime/ctx.py +99 -0
- forktex_flow-0.1.0/src/forktex_flow/runtime/driver.py +312 -0
- forktex_flow-0.1.0/src/forktex_flow/runtime/execution.py +363 -0
- forktex_flow-0.1.0/src/forktex_flow/runtime/registry.py +73 -0
- forktex_flow-0.1.0/src/forktex_flow/testing.py +153 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
ForkTex Python -- Dual License
|
|
2
|
+
|
|
3
|
+
Copyright (C) 2026 FORKTEX S.R.L.
|
|
4
|
+
|
|
5
|
+
This software is licensed under a dual-license model:
|
|
6
|
+
|
|
7
|
+
============================================================================
|
|
8
|
+
|
|
9
|
+
1. OPEN-SOURCE LICENSE -- GNU Affero General Public License v3.0 (AGPL-3.0)
|
|
10
|
+
|
|
11
|
+
You may use, modify, and redistribute this software under the terms of the
|
|
12
|
+
GNU Affero General Public License v3.0 or later, as published by the Free
|
|
13
|
+
Software Foundation. The full license text is available at:
|
|
14
|
+
https://www.gnu.org/licenses/agpl-3.0.html
|
|
15
|
+
|
|
16
|
+
Key obligations under AGPL-3.0:
|
|
17
|
+
- Any modified version MUST be released under the same AGPL-3.0 license.
|
|
18
|
+
- If you run a modified version on a server and let users interact with it
|
|
19
|
+
over a network, you MUST make the complete source code available to those
|
|
20
|
+
users under AGPL-3.0.
|
|
21
|
+
- You MUST preserve all copyright notices and attribution to FORKTEX S.R.L.
|
|
22
|
+
|
|
23
|
+
============================================================================
|
|
24
|
+
|
|
25
|
+
2. COMMERCIAL LICENSE
|
|
26
|
+
|
|
27
|
+
If you wish to use this software without the obligations of AGPL-3.0 --
|
|
28
|
+
for example, to keep your modifications proprietary, to embed this software
|
|
29
|
+
in a closed-source product, or to operate it as part of a commercial service
|
|
30
|
+
without releasing your source code -- you MUST obtain a commercial license
|
|
31
|
+
from FORKTEX S.R.L.
|
|
32
|
+
|
|
33
|
+
Commercial licensing includes, but is not limited to:
|
|
34
|
+
- SaaS / hosted deployments generating revenue
|
|
35
|
+
- Embedding in proprietary products or platforms
|
|
36
|
+
- Redistribution in closed-source form
|
|
37
|
+
- Use by organizations exceeding the AGPL-3.0 compliance scope
|
|
38
|
+
|
|
39
|
+
For commercial licensing inquiries, contact:
|
|
40
|
+
info@forktex.com
|
|
41
|
+
|
|
42
|
+
============================================================================
|
|
43
|
+
|
|
44
|
+
Unless you have obtained a separate commercial license from FORKTEX S.R.L.,
|
|
45
|
+
your use of this software is governed exclusively by the AGPL-3.0 license.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
ForkTex Python
|
|
2
|
+
Copyright (C) 2026 FORKTEX S.R.L.
|
|
3
|
+
|
|
4
|
+
ForkTex Python is the unified command-line interface and agent runtime for
|
|
5
|
+
the ForkTex self-managed software factory. It bundles the local developer
|
|
6
|
+
agent, FSD (ForkTex Standard for Delivery) compliance tooling, architecture
|
|
7
|
+
discovery and audit, the menu-driven root loop, and orchestration glue
|
|
8
|
+
across the cloud, intelligence, and network services.
|
|
9
|
+
|
|
10
|
+
This program is free software: you can redistribute it and/or modify
|
|
11
|
+
it under the terms of the GNU Affero General Public License as published by
|
|
12
|
+
the Free Software Foundation, either version 3 of the License, or
|
|
13
|
+
(at your option) any later version.
|
|
14
|
+
|
|
15
|
+
This program is distributed in the hope that it will be useful,
|
|
16
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
17
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
18
|
+
GNU Affero General Public License for more details.
|
|
19
|
+
|
|
20
|
+
You should have received a copy of the GNU Affero General Public License
|
|
21
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
22
|
+
|
|
23
|
+
For commercial licensing inquiries, contact: info@forktex.com
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: forktex-flow
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Postgres-native durable workflow execution — pipelines and cron-scheduled runs, built on the forktex substrate.
|
|
5
|
+
License-Expression: AGPL-3.0-or-later OR LicenseRef-ForkTex-Commercial
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
License-File: NOTICE
|
|
8
|
+
Author: FORKTEX
|
|
9
|
+
Author-email: info@forktex.com
|
|
10
|
+
Requires-Python: >=3.14,<4.0
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Topic :: Database
|
|
18
|
+
Classifier: Topic :: Software Development
|
|
19
|
+
Classifier: Topic :: System :: Distributed Computing
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Provides-Extra: testing
|
|
22
|
+
Requires-Dist: asyncpg (>=0.31)
|
|
23
|
+
Requires-Dist: croniter (>=2.0)
|
|
24
|
+
Requires-Dist: forktex (>=0.11,<1)
|
|
25
|
+
Requires-Dist: pydantic (>=2.0)
|
|
26
|
+
Requires-Dist: sqlalchemy[asyncio] (>=2.0)
|
|
27
|
+
Requires-Dist: testcontainers[postgres] (>=4.0) ; extra == "testing"
|
|
28
|
+
Project-URL: Bug Tracker, https://github.com/forktex/forktex-flow/issues
|
|
29
|
+
Project-URL: Changelog, https://github.com/forktex/forktex-flow/blob/master/CHANGELOG.md
|
|
30
|
+
Project-URL: Documentation, https://github.com/forktex/forktex-flow/tree/master/docs
|
|
31
|
+
Project-URL: Homepage, https://forktex.com
|
|
32
|
+
Project-URL: Repository, https://github.com/forktex/forktex-flow
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# forktex-flow
|
|
36
|
+
|
|
37
|
+
Durable workflows in Postgres. No broker, no scheduler process, no separate
|
|
38
|
+
control plane — the database you already run, plus your own worker processes, is
|
|
39
|
+
the whole infrastructure.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install forktex-flow
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Python 3.14, PostgreSQL 14+. Built on the
|
|
46
|
+
[`forktex`](https://github.com/forktex/forktex-py) substrate, which pip resolves
|
|
47
|
+
for you.
|
|
48
|
+
|
|
49
|
+
## What it does
|
|
50
|
+
|
|
51
|
+
You write ordinary async functions. The library makes them survive a crash.
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
import operator
|
|
55
|
+
from typing import Annotated, TypedDict
|
|
56
|
+
|
|
57
|
+
from forktex_flow import Ctx, Flow, step
|
|
58
|
+
|
|
59
|
+
flow = Flow(database_url="postgresql+asyncpg://user:pass@host/db")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class DeployState(TypedDict):
|
|
63
|
+
logs: Annotated[list[str], operator.add] # accumulates across steps
|
|
64
|
+
server_id: str # last write wins
|
|
65
|
+
dns_enabled: bool
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
async def provision(ctx: Ctx, state: DeployState) -> dict:
|
|
69
|
+
server = await hetzner.create_server()
|
|
70
|
+
return {"server_id": server.id, "logs": [f"provisioned {server.id}"]}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
async def configure_dns(ctx: Ctx, state: DeployState) -> dict:
|
|
74
|
+
await dns.point(state["server_id"])
|
|
75
|
+
return {"logs": ["dns configured"]}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@flow.pipeline("deploy.apply", version=1, state=DeployState)
|
|
79
|
+
class DeployApply:
|
|
80
|
+
steps = [provision, step(configure_dns, when=lambda s: s["dns_enabled"])]
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Start a worker and dispatch — Postgres must be running and reachable at that DSN:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
await flow.start_driver() # migrates the schema, then competes for leadership
|
|
87
|
+
|
|
88
|
+
instance = await flow.run("deploy.apply", state={"dns_enabled": True, "logs": []})
|
|
89
|
+
final = await instance.wait(timeout=600)
|
|
90
|
+
print(final.status, final.state["logs"])
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
If the worker dies after `provision` and before `configure_dns`, another worker
|
|
94
|
+
picks the run up, `provision` is **not** re-run — its recorded output is
|
|
95
|
+
returned — and `configure_dns` executes for the first time. No second server
|
|
96
|
+
gets created.
|
|
97
|
+
|
|
98
|
+
## The model, in four sentences
|
|
99
|
+
|
|
100
|
+
A workflow is an **ordered list of steps**. A step returns the *delta* it wants
|
|
101
|
+
merged into the run's state, not the whole state. Every step is memoised on
|
|
102
|
+
`(run_id, step, input state)`, so a run picked up after a crash re-executes from
|
|
103
|
+
the top and skips straight past work that already happened. There is no graph, no
|
|
104
|
+
router and no signal inbox: a run visits every step once, in order, executing the
|
|
105
|
+
ones whose guard passes.
|
|
106
|
+
|
|
107
|
+
That is deliberately less than a general workflow engine offers. It is what a
|
|
108
|
+
deploy pipeline, a nightly reconcile or an ingest job actually needs, and it fits
|
|
109
|
+
in one file you can read.
|
|
110
|
+
|
|
111
|
+
## Installing
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
pip install forktex-flow
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
To test your own workflows, take the extra — it brings in `testcontainers`:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
pip install "forktex-flow[testing]"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**Install with Python 3.14.** `forktex` and `forktex-flow` both declare
|
|
124
|
+
`requires-python = ">=3.14"`, and pip hides a release whose floor your
|
|
125
|
+
interpreter does not meet rather than saying why — so an older interpreter
|
|
126
|
+
reports the newest `forktex` as `0.2.4`, an unrelated lineage, and the
|
|
127
|
+
dependency looks unsatisfiable. It is not; the interpreter is.
|
|
128
|
+
|
|
129
|
+
Developing against a local checkout of the substrate:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
pip install -e ../forktex-py
|
|
133
|
+
pip install -e .
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Wiring it into your app
|
|
137
|
+
|
|
138
|
+
The library runs inside a process you already have. In FastAPI that is the
|
|
139
|
+
lifespan.
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from contextlib import asynccontextmanager
|
|
143
|
+
|
|
144
|
+
from fastapi import FastAPI
|
|
145
|
+
from forktex_flow import Flow
|
|
146
|
+
|
|
147
|
+
flow = Flow(database_url=settings.database_url)
|
|
148
|
+
import myapp.workflows # noqa: F401 — importing runs the @flow.pipeline decorators
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@asynccontextmanager
|
|
152
|
+
async def lifespan(app: FastAPI):
|
|
153
|
+
await flow.start_driver() # migrates, then competes for the leader lock
|
|
154
|
+
yield
|
|
155
|
+
await flow.close() # stops driving, releases the pool
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
app = FastAPI(lifespan=lifespan)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Run this on **every** replica. Exactly one wins the advisory lock and executes;
|
|
162
|
+
the rest stand by and take over if it dies. Workflows must be imported before
|
|
163
|
+
`start_driver()` — a decorator only runs when its module does.
|
|
164
|
+
|
|
165
|
+
`start_driver()` calls `init()` for you. Call `init()` alone when you want the
|
|
166
|
+
schema migrated by something that does not execute anything, such as a deploy
|
|
167
|
+
hook.
|
|
168
|
+
|
|
169
|
+
## How consumers use it
|
|
170
|
+
|
|
171
|
+
One example and one sentence each; **[`docs/flow.md`](docs/flow.md) has the exact
|
|
172
|
+
semantics.**
|
|
173
|
+
|
|
174
|
+
### Conditional steps
|
|
175
|
+
|
|
176
|
+
`when=` is a predicate over state, evaluated immediately before dispatch.
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
@flow.pipeline("deploy.apply", version=4, state=DeployState)
|
|
180
|
+
class DeployApply:
|
|
181
|
+
steps = [
|
|
182
|
+
load_manifest,
|
|
183
|
+
step(provision_vps, when=lambda s: s["is_new_server"]),
|
|
184
|
+
deploy_services,
|
|
185
|
+
step(ssl_provision, when=lambda s: s["ssl_enabled"]),
|
|
186
|
+
health_check,
|
|
187
|
+
]
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Accumulating state
|
|
191
|
+
|
|
192
|
+
Any callable in the `Annotated` metadata is the reducer; a field without one is
|
|
193
|
+
last-write-wins.
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
class State(TypedDict):
|
|
197
|
+
logs: Annotated[list[str], operator.add] # every step appends
|
|
198
|
+
server_id: str # each step overwrites
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Cron workflows
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
@flow.scheduled("lifecycle.reconcile", version=1, cron="*/30 * * * *", state=State)
|
|
205
|
+
async def reconcile(ctx: Ctx, state: State) -> dict:
|
|
206
|
+
return {"logs": ["reconciled"]}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Registered on every worker; fired once, by whichever holds the lock.
|
|
210
|
+
|
|
211
|
+
### Per-step retries
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
@step(max_attempts=5, backoff=(30.0, 120.0, 300.0))
|
|
215
|
+
async def flaky(ctx: Ctx, state: State) -> dict: ...
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Progress from inside a long step
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
await ctx.emit("progress", {"done": i, "total": len(chunks)})
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Stopping a run
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
from forktex_flow import WorkflowCancelled
|
|
228
|
+
|
|
229
|
+
async def check_target(ctx: Ctx, state: State) -> dict:
|
|
230
|
+
if not await target_exists(state["server_id"]):
|
|
231
|
+
raise WorkflowCancelled("the target was deleted while we were deploying")
|
|
232
|
+
return {}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
From outside, `await instance.cancel()` stops the run before its **next** step
|
|
236
|
+
and returns whether it was still stoppable. Work already done stays done.
|
|
237
|
+
|
|
238
|
+
### Finding runs
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
page = await (
|
|
242
|
+
flow.query()
|
|
243
|
+
.workflow("deploy.apply", version=4)
|
|
244
|
+
.status("running", "pending")
|
|
245
|
+
.metadata(org_id=org)
|
|
246
|
+
.limit(25)
|
|
247
|
+
.fetch(cursor)
|
|
248
|
+
)
|
|
249
|
+
stuck = await flow.query().current_node("provision_vps").count()
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Testing your workflows
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
from forktex_flow import isolated_flow, make_ctx, run_to_completion
|
|
256
|
+
|
|
257
|
+
# A step is an ordinary callable — no database needed.
|
|
258
|
+
assert await provision(make_ctx(), {"logs": []}) == {"server_id": "srv-1", ...}
|
|
259
|
+
|
|
260
|
+
# Or run the whole pipeline against a throwaway schema.
|
|
261
|
+
async with isolated_flow(postgres_url) as flow:
|
|
262
|
+
register_my_workflows(flow)
|
|
263
|
+
final = await run_to_completion(flow, "deploy.apply", state={"logs": []})
|
|
264
|
+
assert final.state["logs"] == ["provisioned", "dns configured"]
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Where it flexes
|
|
268
|
+
|
|
269
|
+
- **Share your pool** — pass a `Database` instead of a URL and `close()` leaves
|
|
270
|
+
it alone, because it is yours.
|
|
271
|
+
- **Many schemas in one process** — `Flow(url, schema="flow_tenant_a")`. Two
|
|
272
|
+
instances see nothing of each other.
|
|
273
|
+
- **Stamp context onto every run** — a `FlowExtension` with `before_start`
|
|
274
|
+
returns a dict merged into metadata, which reaches every step via
|
|
275
|
+
`Ctx.metadata` and is queryable through `.metadata(**kv)` against a GIN index.
|
|
276
|
+
- **React to outcomes** — `after_complete` / `after_fail`, awaited, with a
|
|
277
|
+
failure in one isolated from the run.
|
|
278
|
+
- **Tune retries and timings** — `RuntimeConfig(poll_interval=…,
|
|
279
|
+
election_interval=…, heartbeat_interval=…, stale_threshold=…, max_attempts=…)`.
|
|
280
|
+
- **Read the engine's own tables** — `flow.session()` is public, because tailing
|
|
281
|
+
`run_event` for a progress stream should not need a second pool.
|
|
282
|
+
|
|
283
|
+
## What you get for free
|
|
284
|
+
|
|
285
|
+
- **Steps are memoised.** Side effects happen once, across any number of resumes.
|
|
286
|
+
- **A stalled step is reclaimed** and re-dispatched; the run holding it is swept
|
|
287
|
+
back so it can be claimed again.
|
|
288
|
+
- **One leader executes.** The holder's death drops its connection, releases the
|
|
289
|
+
lock, and the next election has a new leader — the entire failover mechanism.
|
|
290
|
+
- **Work is partitioned, not duplicated.** Two leaders overlapping at handover
|
|
291
|
+
divide the queue rather than racing over it.
|
|
292
|
+
|
|
293
|
+
Each is asserted by a test against a real Postgres, with two independent
|
|
294
|
+
connection pools where contention is the point.
|
|
295
|
+
|
|
296
|
+
## Errors
|
|
297
|
+
|
|
298
|
+
| Class | From | Raised when |
|
|
299
|
+
|---|---|---|
|
|
300
|
+
| `FlowError` | `forktex_flow` | The library itself failed |
|
|
301
|
+
| `StepFailed` | `forktex_flow` | A step body raised past its retries |
|
|
302
|
+
| `WorkflowFailed` | `forktex_flow` | A run reached terminal `failed` |
|
|
303
|
+
| `WorkflowCancelled` | `forktex_flow` | A run was cancelled |
|
|
304
|
+
| `NotFoundError` | **`forktex.error`** | Unregistered workflow, or unknown run id |
|
|
305
|
+
| `AlreadyExistsError` | **`forktex.error`** | A `(name, version)` registered twice |
|
|
306
|
+
| `BadRequestError` | **`forktex.error`** | A bad argument — each method's docs say which |
|
|
307
|
+
|
|
308
|
+
The last three come from the substrate, not this package:
|
|
309
|
+
|
|
310
|
+
```python
|
|
311
|
+
from forktex.error import AlreadyExistsError, BadRequestError, NotFoundError
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
All of them derive from `forktex.error.AppError`, so a transport that renders an
|
|
315
|
+
`AppError` renders these with the right code rather than a masked 500.
|
|
316
|
+
|
|
317
|
+
## Stability
|
|
318
|
+
|
|
319
|
+
**The public surface is `forktex_flow.__all__`** — 26 names, asserted as an exact
|
|
320
|
+
set by `tests/test_architecture/test_public_surface.py`. Adding or removing one
|
|
321
|
+
is a deliberate edit to that test. Anything reachable but not in `__all__` is
|
|
322
|
+
internal and may change in any release.
|
|
323
|
+
|
|
324
|
+
**This is 0.x, but not a free-for-all.** A public name is removed only after one
|
|
325
|
+
minor release in which it still works and its docstring says it is deprecated.
|
|
326
|
+
Behaviour changes a correct consumer would notice are called out under *Changed*
|
|
327
|
+
in `CHANGELOG.md`.
|
|
328
|
+
|
|
329
|
+
**`FlowExtension`'s hook signatures are frozen.** New lifecycle points arrive as
|
|
330
|
+
new optional methods, never as new parameters on existing ones — so an extension
|
|
331
|
+
written today keeps working.
|
|
332
|
+
|
|
333
|
+
**Migrations are forward-only, and an applied one is never edited.** A schema
|
|
334
|
+
change is a new `v000N`; `tests/test_persist/test_migration_immutability.py`
|
|
335
|
+
pins the content hash of every shipped migration, and
|
|
336
|
+
`test_schema_drift.py` proves the ORM and the SQL describe the same columns.
|
|
337
|
+
|
|
338
|
+
**Two things outside the Python API are also contracts:** the `forktex_flow`
|
|
339
|
+
schema name, and `run_event(id, ts, event_type, payload, run_id)`, which
|
|
340
|
+
consumers read directly by SQL without importing this package.
|
|
341
|
+
|
|
342
|
+
**Deliberately absent, and not returning by oversight:** graph workflows,
|
|
343
|
+
`step_template`, config-defined workflows, signals, `parallel`, and child
|
|
344
|
+
workflows. Each was measured to have zero call sites before it was cut; see
|
|
345
|
+
*Removed* in `CHANGELOG.md`.
|
|
346
|
+
|
|
347
|
+
## Documentation
|
|
348
|
+
|
|
349
|
+
[`docs/flow.md`](docs/flow.md) — the reference: every public name, exact
|
|
350
|
+
durability and cancellation semantics, operating notes, and the gotchas.
|
|
351
|
+
[`docs/development.md`](docs/development.md) — contributing.
|
|
352
|
+
|
|
353
|
+
## Licence
|
|
354
|
+
|
|
355
|
+
AGPL-3.0-or-later, or a commercial licence from FORKTEX S.R.L.
|
|
356
|
+
(info@forktex.com). See `LICENSE` and `NOTICE`.
|
|
357
|
+
|