pegasus-workflows-sdk 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.
- pegasus_workflows_sdk-0.1.0/.gitignore +6 -0
- pegasus_workflows_sdk-0.1.0/PKG-INFO +224 -0
- pegasus_workflows_sdk-0.1.0/README.md +206 -0
- pegasus_workflows_sdk-0.1.0/pegasus_workflows/__init__.py +113 -0
- pegasus_workflows_sdk-0.1.0/pegasus_workflows/api.py +529 -0
- pegasus_workflows_sdk-0.1.0/pegasus_workflows/cli/__init__.py +45 -0
- pegasus_workflows_sdk-0.1.0/pegasus_workflows/cli/init.py +93 -0
- pegasus_workflows_sdk-0.1.0/pegasus_workflows/cli/integration_config.py +278 -0
- pegasus_workflows_sdk-0.1.0/pegasus_workflows/cli/package.py +108 -0
- pegasus_workflows_sdk-0.1.0/pegasus_workflows/cli/push.py +97 -0
- pegasus_workflows_sdk-0.1.0/pegasus_workflows/cli/run.py +131 -0
- pegasus_workflows_sdk-0.1.0/pegasus_workflows/cli/test.py +186 -0
- pegasus_workflows_sdk-0.1.0/pegasus_workflows/manifest.py +229 -0
- pegasus_workflows_sdk-0.1.0/pegasus_workflows/templates/README.md +41 -0
- pegasus_workflows_sdk-0.1.0/pegasus_workflows/templates/__WORKFLOW_NAME__/__init__.py +1 -0
- pegasus_workflows_sdk-0.1.0/pegasus_workflows/templates/__WORKFLOW_NAME__/workflow.py +36 -0
- pegasus_workflows_sdk-0.1.0/pegasus_workflows/templates/pegasus-workflows.toml +18 -0
- pegasus_workflows_sdk-0.1.0/pegasus_workflows/templates/pyproject.toml +13 -0
- pegasus_workflows_sdk-0.1.0/pyproject.toml +61 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pegasus-workflows-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK and CLI for authoring, packaging, and publishing Pegasus workflows.
|
|
5
|
+
Author: DolasDev
|
|
6
|
+
License: UNLICENSED
|
|
7
|
+
Keywords: automation,pegasus,temporal,workflows
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Requires-Python: >=3.11
|
|
11
|
+
Requires-Dist: httpx<1,>=0.27
|
|
12
|
+
Requires-Dist: temporalio<2,>=1.7
|
|
13
|
+
Requires-Dist: typer<1,>=0.12
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
16
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# Pegasus Workflows SDK
|
|
20
|
+
|
|
21
|
+
`pegasus-workflows-sdk` is the Python SDK and CLI for authoring, packaging, and
|
|
22
|
+
publishing **Pegasus workflows** — Temporal workflows that automate
|
|
23
|
+
cross-domain operations (move lifecycle, billing follow-ups, dispatch
|
|
24
|
+
decisions) against the Pegasus public API.
|
|
25
|
+
|
|
26
|
+
Phase 1 ships the **developer flow**: write a workflow locally, run it against a
|
|
27
|
+
Dockerized Temporal, package it, and upload it. There is no server-side
|
|
28
|
+
execution yet — the API stores the artifact and lists it.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
pip install pegasus-workflows-sdk
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
This installs the `pegasus-workflows` CLI. **Python 3.11+** is required. Pin the
|
|
37
|
+
version in your project's requirements for reproducible builds, e.g.
|
|
38
|
+
`pegasus-workflows-sdk==0.1.0`.
|
|
39
|
+
|
|
40
|
+
### Interim / unreleased install (git)
|
|
41
|
+
|
|
42
|
+
The repository is public, so you can install straight from a tagged commit
|
|
43
|
+
without waiting for a PyPI release — useful for an unreleased fix, or before the
|
|
44
|
+
first PyPI publish lands:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
pip install "pegasus-workflows-sdk @ git+https://github.com/DolasDev/pegasus@sdk-python-v0.1.0#subdirectory=packages/workflows-sdk-python"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Swap the `@sdk-python-v0.1.0` tag for `@main` to track the latest unreleased
|
|
51
|
+
SDK. This clones the whole monorepo to build one subdirectory, so prefer the
|
|
52
|
+
PyPI install for everyday use.
|
|
53
|
+
|
|
54
|
+
## Quick start
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
pegasus-workflows init demo
|
|
58
|
+
cd demo
|
|
59
|
+
pegasus-workflows test demo
|
|
60
|
+
pegasus-workflows package
|
|
61
|
+
pegasus-workflows push --token=vnd_... --base-url=http://localhost:3000
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Authoring
|
|
65
|
+
|
|
66
|
+
Import the Temporal authoring primitives from `pegasus_workflows` and mark your
|
|
67
|
+
workflow class with `@pegasus_workflow`:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from datetime import timedelta
|
|
71
|
+
from pegasus_workflows import activity, pegasus_workflow, workflow
|
|
72
|
+
|
|
73
|
+
@activity.defn
|
|
74
|
+
async def greet(name: str) -> str:
|
|
75
|
+
return f"Hello, {name}!"
|
|
76
|
+
|
|
77
|
+
@pegasus_workflow(name="demo", version="0.1.0")
|
|
78
|
+
class HelloWorkflow:
|
|
79
|
+
@workflow.run
|
|
80
|
+
async def run(self, name: str = "world") -> str:
|
|
81
|
+
return await workflow.execute_activity(
|
|
82
|
+
greet, name, start_to_close_timeout=timedelta(seconds=10)
|
|
83
|
+
)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`@pegasus_workflow` wraps `temporalio.workflow.defn` and records the
|
|
87
|
+
`(name, version)` used by the manifest.
|
|
88
|
+
|
|
89
|
+
### Input contract: how `run()` receives its argument
|
|
90
|
+
|
|
91
|
+
Your `run()` method receives a **single positional argument** whose shape depends on how the workflow
|
|
92
|
+
was started:
|
|
93
|
+
|
|
94
|
+
**1. Trigger-fired (domain-event trigger)** — the dispatcher passes the full event envelope:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
{
|
|
98
|
+
"domainEventId": "<uuid>",
|
|
99
|
+
"eventType": "quote.accepted", # the event type that fired the trigger
|
|
100
|
+
"occurredAt": "<ISO-8601>",
|
|
101
|
+
"payload": {"quoteId": "<id>", "moveId": "<id>"} # entity ids, camelCase
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Read entity ids from `arg["payload"]["quoteId"]` etc. The `payload` is a pointer, not a full snapshot
|
|
106
|
+
— always re-fetch authoritative state from the Pegasus API using those ids rather than relying on the
|
|
107
|
+
payload alone.
|
|
108
|
+
|
|
109
|
+
**2. Manual run** — `POST /api/v1/workflows/:id/run` passes:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
{"executionId": "<uuid>", "input": <user-supplied dict>}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Read your business data from `arg["input"]` (e.g. `arg["input"]["quote_id"]`).
|
|
116
|
+
|
|
117
|
+
**3. CLI test** — `pegasus-workflows test <name>` passes a raw string for local-dev parity.
|
|
118
|
+
|
|
119
|
+
Your `run()` should handle all three shapes. A module-level helper (not a method) is the recommended
|
|
120
|
+
pattern — it stays unit-testable without a Temporal worker context:
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
def _resolve_quote_id(payload: dict | str) -> str:
|
|
124
|
+
if isinstance(payload, str):
|
|
125
|
+
return payload
|
|
126
|
+
event_payload = payload.get("payload") if isinstance(payload, dict) else None
|
|
127
|
+
if isinstance(event_payload, dict) and event_payload.get("quoteId"):
|
|
128
|
+
return str(event_payload["quoteId"])
|
|
129
|
+
inner = payload.get("input") if isinstance(payload, dict) else None
|
|
130
|
+
if isinstance(inner, dict) and inner.get("quote_id"):
|
|
131
|
+
return str(inner["quote_id"])
|
|
132
|
+
return "quote-unknown"
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## The manifest — `pegasus-workflows.toml`
|
|
136
|
+
|
|
137
|
+
Every project has a `pegasus-workflows.toml` at its root. Each `[[workflow]]`
|
|
138
|
+
table is packaged into its own artifact and uploaded as a distinct
|
|
139
|
+
`(name, version)` row:
|
|
140
|
+
|
|
141
|
+
```toml
|
|
142
|
+
[[workflow]]
|
|
143
|
+
name = "demo" # ^[a-z0-9][a-z0-9_-]{0,63}$
|
|
144
|
+
version = "0.1.0" # semver
|
|
145
|
+
entry_points = ["demo.workflow:HelloWorkflow"] # non-empty
|
|
146
|
+
source_dir = "demo" # optional, defaults to name
|
|
147
|
+
description = "..." # optional
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
These rules mirror the server's `ManifestSchema` exactly, so `package`/`push`
|
|
151
|
+
fail fast locally before any HTTP call.
|
|
152
|
+
|
|
153
|
+
## CLI
|
|
154
|
+
|
|
155
|
+
| Command | What it does |
|
|
156
|
+
| ---------------------------------------------------------------------- | ------------------------------------------------------------ |
|
|
157
|
+
| `pegasus-workflows init <name>` | Scaffold a new workflow project. |
|
|
158
|
+
| `pegasus-workflows package` | Zip each declared workflow into `dist/<name>-<version>.zip`. |
|
|
159
|
+
| `pegasus-workflows push --token=<vnd_…> [--base-url=…]` | Package, then `upload-url` → S3 PUT → finalize. |
|
|
160
|
+
| `pegasus-workflows test <workflow>` | Start local Temporal and run the workflow with a stub input. |
|
|
161
|
+
| `pegasus-workflows integration-config validate <id> [-C <dir>]` | Dry-run the publish gate for a config (no write). |
|
|
162
|
+
| `pegasus-workflows integration-config publish <id> [-C <dir>]` | Gate then publish a new config version. |
|
|
163
|
+
| `pegasus-workflows integration-config pull <id> [-C <dir>] [--stdout]` | Fetch the active config; write the editable surface to disk. |
|
|
164
|
+
| `pegasus-workflows integration-config versions <id>` | List the config version history (newest first). |
|
|
165
|
+
| `pegasus-workflows integration-config rollback <id> <version>` | Re-publish a prior version (re-runs the gate). |
|
|
166
|
+
|
|
167
|
+
`push` reads the token from `--token` or the `PEGASUS_WORKFLOW_TOKEN`
|
|
168
|
+
environment variable. The token is a `vnd_*` Pegasus API key whose service
|
|
169
|
+
account holds the `workflow_developer` role.
|
|
170
|
+
|
|
171
|
+
### Authoring an integration-validator config
|
|
172
|
+
|
|
173
|
+
The `integration-config` group manages an integration's declarative **mapping +
|
|
174
|
+
rules** (the DB-backed authoring surface; see
|
|
175
|
+
`apps/api/src/handlers/integration-validation/config.ts`). The editable surface
|
|
176
|
+
lives as three JSON files in a working directory (`-C`, default `.`):
|
|
177
|
+
`mapping.json`, `rules.json`, `corpus.json`. The round-trip is pull → edit →
|
|
178
|
+
validate → publish:
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
pegasus-workflows integration-config pull weichert -C ./weichert
|
|
182
|
+
# …edit mapping.json / rules.json…
|
|
183
|
+
pegasus-workflows integration-config validate weichert -C ./weichert
|
|
184
|
+
pegasus-workflows integration-config publish weichert -C ./weichert
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
`publish`/`rollback` require the token's tenant to be the **platform tenant** to
|
|
188
|
+
write GLOBAL (visibility is derived server-side) and to carry the
|
|
189
|
+
`PublishIntegrationConfig` action; they are gated by the server's
|
|
190
|
+
`INTEGRATION_CONFIG_PUBLISH_ENABLED` switch. `validate` and `pull` are
|
|
191
|
+
read-level and never gated.
|
|
192
|
+
|
|
193
|
+
## Local Temporal
|
|
194
|
+
|
|
195
|
+
`pegasus-workflows test` needs a Temporal server. The repo root ships
|
|
196
|
+
`docker-compose.temporal.yml` (Temporal server + Temporal UI on `7233` / `8080`)
|
|
197
|
+
purely as a local-dev aid — no production connection. `test` runs
|
|
198
|
+
`docker compose -f docker-compose.temporal.yml up -d` automatically if Temporal
|
|
199
|
+
is not already reachable on `127.0.0.1:7233`. To start it by hand:
|
|
200
|
+
|
|
201
|
+
```
|
|
202
|
+
docker compose -f docker-compose.temporal.yml up -d
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
The Temporal Web UI is then at <http://localhost:8080>.
|
|
206
|
+
|
|
207
|
+
## Release
|
|
208
|
+
|
|
209
|
+
The SDK is published to PyPI by `.github/workflows/release-sdk-python.yml` on
|
|
210
|
+
`sdk-python-v*` tags via PyPI **trusted publishing** (OIDC — no API token).
|
|
211
|
+
|
|
212
|
+
To cut a release:
|
|
213
|
+
|
|
214
|
+
1. Bump `version` in `pyproject.toml` and commit it on `main`.
|
|
215
|
+
2. Tag the release commit and push the tag, e.g.
|
|
216
|
+
`git tag sdk-python-v0.1.0 && git push origin sdk-python-v0.1.0`.
|
|
217
|
+
|
|
218
|
+
The workflow then lints, audits, tests, builds, and uploads the sdist + wheel.
|
|
219
|
+
|
|
220
|
+
**One-time setup (before the first release):** a PyPI project owner must add a
|
|
221
|
+
pending publisher at `pegasus-workflows-sdk` → Publishing → owner `DolasDev`,
|
|
222
|
+
repo `pegasus`, workflow `release-sdk-python.yml`, environment `pypi`. Until
|
|
223
|
+
that exists the `publish` job fails at the upload step, and tenants must use the
|
|
224
|
+
[git install](#interim--unreleased-install-git) above.
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# Pegasus Workflows SDK
|
|
2
|
+
|
|
3
|
+
`pegasus-workflows-sdk` is the Python SDK and CLI for authoring, packaging, and
|
|
4
|
+
publishing **Pegasus workflows** — Temporal workflows that automate
|
|
5
|
+
cross-domain operations (move lifecycle, billing follow-ups, dispatch
|
|
6
|
+
decisions) against the Pegasus public API.
|
|
7
|
+
|
|
8
|
+
Phase 1 ships the **developer flow**: write a workflow locally, run it against a
|
|
9
|
+
Dockerized Temporal, package it, and upload it. There is no server-side
|
|
10
|
+
execution yet — the API stores the artifact and lists it.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
pip install pegasus-workflows-sdk
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
This installs the `pegasus-workflows` CLI. **Python 3.11+** is required. Pin the
|
|
19
|
+
version in your project's requirements for reproducible builds, e.g.
|
|
20
|
+
`pegasus-workflows-sdk==0.1.0`.
|
|
21
|
+
|
|
22
|
+
### Interim / unreleased install (git)
|
|
23
|
+
|
|
24
|
+
The repository is public, so you can install straight from a tagged commit
|
|
25
|
+
without waiting for a PyPI release — useful for an unreleased fix, or before the
|
|
26
|
+
first PyPI publish lands:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
pip install "pegasus-workflows-sdk @ git+https://github.com/DolasDev/pegasus@sdk-python-v0.1.0#subdirectory=packages/workflows-sdk-python"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Swap the `@sdk-python-v0.1.0` tag for `@main` to track the latest unreleased
|
|
33
|
+
SDK. This clones the whole monorepo to build one subdirectory, so prefer the
|
|
34
|
+
PyPI install for everyday use.
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
pegasus-workflows init demo
|
|
40
|
+
cd demo
|
|
41
|
+
pegasus-workflows test demo
|
|
42
|
+
pegasus-workflows package
|
|
43
|
+
pegasus-workflows push --token=vnd_... --base-url=http://localhost:3000
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Authoring
|
|
47
|
+
|
|
48
|
+
Import the Temporal authoring primitives from `pegasus_workflows` and mark your
|
|
49
|
+
workflow class with `@pegasus_workflow`:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from datetime import timedelta
|
|
53
|
+
from pegasus_workflows import activity, pegasus_workflow, workflow
|
|
54
|
+
|
|
55
|
+
@activity.defn
|
|
56
|
+
async def greet(name: str) -> str:
|
|
57
|
+
return f"Hello, {name}!"
|
|
58
|
+
|
|
59
|
+
@pegasus_workflow(name="demo", version="0.1.0")
|
|
60
|
+
class HelloWorkflow:
|
|
61
|
+
@workflow.run
|
|
62
|
+
async def run(self, name: str = "world") -> str:
|
|
63
|
+
return await workflow.execute_activity(
|
|
64
|
+
greet, name, start_to_close_timeout=timedelta(seconds=10)
|
|
65
|
+
)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`@pegasus_workflow` wraps `temporalio.workflow.defn` and records the
|
|
69
|
+
`(name, version)` used by the manifest.
|
|
70
|
+
|
|
71
|
+
### Input contract: how `run()` receives its argument
|
|
72
|
+
|
|
73
|
+
Your `run()` method receives a **single positional argument** whose shape depends on how the workflow
|
|
74
|
+
was started:
|
|
75
|
+
|
|
76
|
+
**1. Trigger-fired (domain-event trigger)** — the dispatcher passes the full event envelope:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
{
|
|
80
|
+
"domainEventId": "<uuid>",
|
|
81
|
+
"eventType": "quote.accepted", # the event type that fired the trigger
|
|
82
|
+
"occurredAt": "<ISO-8601>",
|
|
83
|
+
"payload": {"quoteId": "<id>", "moveId": "<id>"} # entity ids, camelCase
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Read entity ids from `arg["payload"]["quoteId"]` etc. The `payload` is a pointer, not a full snapshot
|
|
88
|
+
— always re-fetch authoritative state from the Pegasus API using those ids rather than relying on the
|
|
89
|
+
payload alone.
|
|
90
|
+
|
|
91
|
+
**2. Manual run** — `POST /api/v1/workflows/:id/run` passes:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
{"executionId": "<uuid>", "input": <user-supplied dict>}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Read your business data from `arg["input"]` (e.g. `arg["input"]["quote_id"]`).
|
|
98
|
+
|
|
99
|
+
**3. CLI test** — `pegasus-workflows test <name>` passes a raw string for local-dev parity.
|
|
100
|
+
|
|
101
|
+
Your `run()` should handle all three shapes. A module-level helper (not a method) is the recommended
|
|
102
|
+
pattern — it stays unit-testable without a Temporal worker context:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
def _resolve_quote_id(payload: dict | str) -> str:
|
|
106
|
+
if isinstance(payload, str):
|
|
107
|
+
return payload
|
|
108
|
+
event_payload = payload.get("payload") if isinstance(payload, dict) else None
|
|
109
|
+
if isinstance(event_payload, dict) and event_payload.get("quoteId"):
|
|
110
|
+
return str(event_payload["quoteId"])
|
|
111
|
+
inner = payload.get("input") if isinstance(payload, dict) else None
|
|
112
|
+
if isinstance(inner, dict) and inner.get("quote_id"):
|
|
113
|
+
return str(inner["quote_id"])
|
|
114
|
+
return "quote-unknown"
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## The manifest — `pegasus-workflows.toml`
|
|
118
|
+
|
|
119
|
+
Every project has a `pegasus-workflows.toml` at its root. Each `[[workflow]]`
|
|
120
|
+
table is packaged into its own artifact and uploaded as a distinct
|
|
121
|
+
`(name, version)` row:
|
|
122
|
+
|
|
123
|
+
```toml
|
|
124
|
+
[[workflow]]
|
|
125
|
+
name = "demo" # ^[a-z0-9][a-z0-9_-]{0,63}$
|
|
126
|
+
version = "0.1.0" # semver
|
|
127
|
+
entry_points = ["demo.workflow:HelloWorkflow"] # non-empty
|
|
128
|
+
source_dir = "demo" # optional, defaults to name
|
|
129
|
+
description = "..." # optional
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
These rules mirror the server's `ManifestSchema` exactly, so `package`/`push`
|
|
133
|
+
fail fast locally before any HTTP call.
|
|
134
|
+
|
|
135
|
+
## CLI
|
|
136
|
+
|
|
137
|
+
| Command | What it does |
|
|
138
|
+
| ---------------------------------------------------------------------- | ------------------------------------------------------------ |
|
|
139
|
+
| `pegasus-workflows init <name>` | Scaffold a new workflow project. |
|
|
140
|
+
| `pegasus-workflows package` | Zip each declared workflow into `dist/<name>-<version>.zip`. |
|
|
141
|
+
| `pegasus-workflows push --token=<vnd_…> [--base-url=…]` | Package, then `upload-url` → S3 PUT → finalize. |
|
|
142
|
+
| `pegasus-workflows test <workflow>` | Start local Temporal and run the workflow with a stub input. |
|
|
143
|
+
| `pegasus-workflows integration-config validate <id> [-C <dir>]` | Dry-run the publish gate for a config (no write). |
|
|
144
|
+
| `pegasus-workflows integration-config publish <id> [-C <dir>]` | Gate then publish a new config version. |
|
|
145
|
+
| `pegasus-workflows integration-config pull <id> [-C <dir>] [--stdout]` | Fetch the active config; write the editable surface to disk. |
|
|
146
|
+
| `pegasus-workflows integration-config versions <id>` | List the config version history (newest first). |
|
|
147
|
+
| `pegasus-workflows integration-config rollback <id> <version>` | Re-publish a prior version (re-runs the gate). |
|
|
148
|
+
|
|
149
|
+
`push` reads the token from `--token` or the `PEGASUS_WORKFLOW_TOKEN`
|
|
150
|
+
environment variable. The token is a `vnd_*` Pegasus API key whose service
|
|
151
|
+
account holds the `workflow_developer` role.
|
|
152
|
+
|
|
153
|
+
### Authoring an integration-validator config
|
|
154
|
+
|
|
155
|
+
The `integration-config` group manages an integration's declarative **mapping +
|
|
156
|
+
rules** (the DB-backed authoring surface; see
|
|
157
|
+
`apps/api/src/handlers/integration-validation/config.ts`). The editable surface
|
|
158
|
+
lives as three JSON files in a working directory (`-C`, default `.`):
|
|
159
|
+
`mapping.json`, `rules.json`, `corpus.json`. The round-trip is pull → edit →
|
|
160
|
+
validate → publish:
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
pegasus-workflows integration-config pull weichert -C ./weichert
|
|
164
|
+
# …edit mapping.json / rules.json…
|
|
165
|
+
pegasus-workflows integration-config validate weichert -C ./weichert
|
|
166
|
+
pegasus-workflows integration-config publish weichert -C ./weichert
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`publish`/`rollback` require the token's tenant to be the **platform tenant** to
|
|
170
|
+
write GLOBAL (visibility is derived server-side) and to carry the
|
|
171
|
+
`PublishIntegrationConfig` action; they are gated by the server's
|
|
172
|
+
`INTEGRATION_CONFIG_PUBLISH_ENABLED` switch. `validate` and `pull` are
|
|
173
|
+
read-level and never gated.
|
|
174
|
+
|
|
175
|
+
## Local Temporal
|
|
176
|
+
|
|
177
|
+
`pegasus-workflows test` needs a Temporal server. The repo root ships
|
|
178
|
+
`docker-compose.temporal.yml` (Temporal server + Temporal UI on `7233` / `8080`)
|
|
179
|
+
purely as a local-dev aid — no production connection. `test` runs
|
|
180
|
+
`docker compose -f docker-compose.temporal.yml up -d` automatically if Temporal
|
|
181
|
+
is not already reachable on `127.0.0.1:7233`. To start it by hand:
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
docker compose -f docker-compose.temporal.yml up -d
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
The Temporal Web UI is then at <http://localhost:8080>.
|
|
188
|
+
|
|
189
|
+
## Release
|
|
190
|
+
|
|
191
|
+
The SDK is published to PyPI by `.github/workflows/release-sdk-python.yml` on
|
|
192
|
+
`sdk-python-v*` tags via PyPI **trusted publishing** (OIDC — no API token).
|
|
193
|
+
|
|
194
|
+
To cut a release:
|
|
195
|
+
|
|
196
|
+
1. Bump `version` in `pyproject.toml` and commit it on `main`.
|
|
197
|
+
2. Tag the release commit and push the tag, e.g.
|
|
198
|
+
`git tag sdk-python-v0.1.0 && git push origin sdk-python-v0.1.0`.
|
|
199
|
+
|
|
200
|
+
The workflow then lints, audits, tests, builds, and uploads the sdist + wheel.
|
|
201
|
+
|
|
202
|
+
**One-time setup (before the first release):** a PyPI project owner must add a
|
|
203
|
+
pending publisher at `pegasus-workflows-sdk` → Publishing → owner `DolasDev`,
|
|
204
|
+
repo `pegasus`, workflow `release-sdk-python.yml`, environment `pypi`. Until
|
|
205
|
+
that exists the `publish` job fails at the upload step, and tenants must use the
|
|
206
|
+
[git install](#interim--unreleased-install-git) above.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Pegasus Workflows SDK.
|
|
2
|
+
|
|
3
|
+
Authoring surface for Pegasus workflows. Re-exports the Temporal authoring
|
|
4
|
+
primitives (``workflow`` and ``activity``) so workflow authors only ever
|
|
5
|
+
import from :mod:`pegasus_workflows`, and adds the :func:`pegasus_workflow`
|
|
6
|
+
decorator which stashes ``(name, version)`` metadata used by the CLI's
|
|
7
|
+
``package`` step and the ``pegasus-workflows.toml`` manifest.
|
|
8
|
+
|
|
9
|
+
Example
|
|
10
|
+
-------
|
|
11
|
+
.. code-block:: python
|
|
12
|
+
|
|
13
|
+
from pegasus_workflows import pegasus_workflow, workflow
|
|
14
|
+
|
|
15
|
+
@pegasus_workflow(name="send_quote_followup", version="0.1.0")
|
|
16
|
+
class SendQuoteFollowup:
|
|
17
|
+
@workflow.run
|
|
18
|
+
async def run(self, quote_id: str) -> str:
|
|
19
|
+
return f"followed up on {quote_id}"
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from collections.abc import Callable
|
|
25
|
+
from typing import TYPE_CHECKING, Any, TypeVar
|
|
26
|
+
|
|
27
|
+
from temporalio import activity, workflow
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from .api import PegasusApiError, PegasusClient
|
|
31
|
+
from .manifest import Manifest, ManifestError, load_manifest
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"activity",
|
|
35
|
+
"workflow",
|
|
36
|
+
"pegasus_workflow",
|
|
37
|
+
"PegasusClient",
|
|
38
|
+
"PegasusApiError",
|
|
39
|
+
"Manifest",
|
|
40
|
+
"ManifestError",
|
|
41
|
+
"load_manifest",
|
|
42
|
+
"WORKFLOW_META_ATTR",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
# api.py pulls in httpx (and transitively urllib), which Temporal's workflow
|
|
46
|
+
# sandbox restricts. Workflow files import the authoring primitives from this
|
|
47
|
+
# package, so importing the package must NOT eagerly import api/manifest —
|
|
48
|
+
# otherwise httpx lands in the sandboxed workflow module graph and validation
|
|
49
|
+
# fails. PegasusClient/Manifest/etc. are therefore exposed lazily and only
|
|
50
|
+
# resolved when actually referenced (i.e. inside activities or the CLI).
|
|
51
|
+
_LAZY_EXPORTS = {
|
|
52
|
+
"PegasusClient": "api",
|
|
53
|
+
"PegasusApiError": "api",
|
|
54
|
+
"Manifest": "manifest",
|
|
55
|
+
"ManifestError": "manifest",
|
|
56
|
+
"load_manifest": "manifest",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def __getattr__(name: str) -> Any:
|
|
61
|
+
"""Lazily resolve API/manifest exports (PEP 562 module __getattr__)."""
|
|
62
|
+
module_name = _LAZY_EXPORTS.get(name)
|
|
63
|
+
if module_name is None:
|
|
64
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
65
|
+
import importlib
|
|
66
|
+
|
|
67
|
+
module = importlib.import_module(f".{module_name}", __name__)
|
|
68
|
+
return getattr(module, name)
|
|
69
|
+
|
|
70
|
+
#: Attribute name under which :func:`pegasus_workflow` stores its metadata
|
|
71
|
+
#: dict on the decorated class.
|
|
72
|
+
WORKFLOW_META_ATTR = "__pegasus_workflow__"
|
|
73
|
+
|
|
74
|
+
_T = TypeVar("_T")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def pegasus_workflow(
|
|
78
|
+
*, name: str, version: str, description: str | None = None
|
|
79
|
+
) -> Callable[[_T], _T]:
|
|
80
|
+
"""Mark a class as a Pegasus workflow.
|
|
81
|
+
|
|
82
|
+
Wraps :func:`temporalio.workflow.defn` so the class is a valid Temporal
|
|
83
|
+
workflow definition, and records ``(name, version, description)`` on the
|
|
84
|
+
class under :data:`WORKFLOW_META_ATTR`. The CLI reads this metadata for
|
|
85
|
+
introspection; the authoritative manifest still lives in
|
|
86
|
+
``pegasus-workflows.toml``.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
name: Workflow name. Must match ``^[a-z0-9][a-z0-9_-]{0,63}$``.
|
|
90
|
+
version: Semantic version, e.g. ``1.2.3`` or ``1.2.3-beta.1``.
|
|
91
|
+
description: Optional human-readable description.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
A decorator that returns the (Temporal-registered) class unchanged.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def decorator(cls: _T) -> _T:
|
|
98
|
+
# Register under the PEGASUS name, not the Python class name: the
|
|
99
|
+
# API starts executions with `client.workflow.start(workflow.name,
|
|
100
|
+
# ...)` (apps/api/src/lib/start-workflow-execution.ts), so the
|
|
101
|
+
# Temporal workflow *type* must equal the manifest name or the
|
|
102
|
+
# worker rejects every task with "Workflow class <name> is not
|
|
103
|
+
# registered". (Caught live in the Phase 3 staging smoke — the
|
|
104
|
+
# class-name registration had been latent since Phase 1.)
|
|
105
|
+
defined: Any = workflow.defn(name=name)(cls) # type: ignore[arg-type]
|
|
106
|
+
setattr(
|
|
107
|
+
defined,
|
|
108
|
+
WORKFLOW_META_ATTR,
|
|
109
|
+
{"name": name, "version": version, "description": description},
|
|
110
|
+
)
|
|
111
|
+
return defined # type: ignore[return-value]
|
|
112
|
+
|
|
113
|
+
return decorator
|