continuum-task-server-sdk 1.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.
- continuum_task_server/__init__.py +74 -0
- continuum_task_server/_http.py +142 -0
- continuum_task_server/_stomp.py +137 -0
- continuum_task_server/client.py +351 -0
- continuum_task_server/exceptions.py +94 -0
- continuum_task_server/models.py +197 -0
- continuum_task_server/server.py +511 -0
- continuum_task_server/websocket.py +852 -0
- continuum_task_server_sdk-1.1.0.dist-info/METADATA +258 -0
- continuum_task_server_sdk-1.1.0.dist-info/RECORD +11 -0
- continuum_task_server_sdk-1.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: continuum-task-server-sdk
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Python SDK for the Continuum Task Server
|
|
5
|
+
Project-URL: Homepage, https://github.com/ContinuumWorkflow/continuum-task-server-sdk-python
|
|
6
|
+
Project-URL: Issues, https://github.com/ContinuumWorkflow/continuum-task-server-sdk-python/issues
|
|
7
|
+
Author: Continuum
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: continuum,queue,sdk,task,worker
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: httpx<1.0,>=0.27
|
|
21
|
+
Requires-Dist: pydantic<3.0,>=2.6
|
|
22
|
+
Requires-Dist: websocket-client<2.0,>=1.8
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# Continuum Task Server SDK for Python
|
|
31
|
+
|
|
32
|
+
Python client for the [Continuum](https://github.com/ContinuumWorkflow) task server. Designed so a 20-line script can stand up a worker that claims queue items, runs your code, and reports results.
|
|
33
|
+
|
|
34
|
+
- **`TaskServer`** — decorator-based worker loop: claim, heartbeat **while the handler runs**, status updates, backoff, graceful shutdown. Defaults to HTTP polling; set ``transport="websocket"`` (or ``CONTINUUM_TRANSPORT=websocket`` in the example) to claim from STOMP ``work.available`` hints instead.
|
|
35
|
+
- **`ContinuumClient`** — thin pythonic client over the management + queue REST APIs (task types, task items, versions, queue, content store).
|
|
36
|
+
- **`ContinuumWebSocketClient`** — synchronous STOMP-over-WebSocket client for the worker hot path (enqueue, claim, heartbeat, status, ``subscribeWork``, durable ``subscribeWait`` / ``ackEvent``).
|
|
37
|
+
|
|
38
|
+
Requires Python 3.10+.
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
Tagged releases (`v*`) are published to [PyPI](https://pypi.org/project/continuum-task-server-sdk/):
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install continuum-task-server-sdk
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Dev and PR builds are still attached to private GitHub Releases. Install those with a token:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install \
|
|
52
|
+
"https://${GITHUB_TOKEN}@github.com/ContinuumWorkflow/continuum-task-server-sdk-python/releases/download/v0.1.0/continuum_task_server_sdk-0.1.0-py3-none-any.whl"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`GITHUB_TOKEN` must be a PAT with repo read access.
|
|
56
|
+
|
|
57
|
+
## Quickstart: build a task server in 20 lines
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
import os
|
|
61
|
+
from continuum_task_server import TaskServer
|
|
62
|
+
|
|
63
|
+
server = TaskServer(
|
|
64
|
+
base_url=os.environ["CONTINUUM_URL"],
|
|
65
|
+
api_key=os.environ["CONTINUUM_API_KEY"],
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@server.task("echo")
|
|
70
|
+
def echo(item):
|
|
71
|
+
return {"echoed": item.input_data_json}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@server.task("greet")
|
|
75
|
+
def greet(item):
|
|
76
|
+
name = (item.input_data_json or {}).get("name", "world")
|
|
77
|
+
return {"message": f"hello, {name}"}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
if __name__ == "__main__":
|
|
81
|
+
server.run()
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Set `CONTINUUM_TRANSPORT=websocket` (and enable Queue API WebSockets) to replace polling with STOMP wake-ups. HTTP polling remains the default.
|
|
85
|
+
|
|
86
|
+
Run it. The server polls `/api/queue/claim` for `echo` and `greet`, claims items as they become available, runs your handler in a thread, heartbeats while it runs, and:
|
|
87
|
+
|
|
88
|
+
- **Handler returns** a value → queue item marked `ENDED`, return value JSON-encoded as `outputData` (default `auto_complete=True`).
|
|
89
|
+
- **Handler raises** → queue item marked `KILLED`, error info written to `outputData`.
|
|
90
|
+
|
|
91
|
+
Press `Ctrl+C` (or send `SIGTERM`) and the server stops polling and waits up to `shutdown_timeout` seconds for in-flight handlers to finish.
|
|
92
|
+
|
|
93
|
+
### Deferred completion (`auto_complete=False`)
|
|
94
|
+
|
|
95
|
+
The handler runs under the normal **in-handler** heartbeat (same as any task). When it returns, the server **does not** send `ENDED` and **does not** keep heartbeating: you own the claim until you finish or lose it to timeouts.
|
|
96
|
+
|
|
97
|
+
Typical pattern: persist `item.id` (and anything else you need), then on each pass of **your** poller (cron, loop, worker restart), call **`server.client.queue.heartbeat(queue_item_id)`** so the Continuum claim stays alive, and when the real-world condition is met call **`server.complete_queue_item(...)`** or **`server.fail_queue_item(...)`**. Use the **same worker API key** as the process that claimed the item (often the same `TaskServer` / `ContinuumClient` config loaded from env).
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
@server.task("wait-for-mail", auto_complete=False)
|
|
101
|
+
def wait_for_mail(item):
|
|
102
|
+
db.insert_outstanding(queue_item_id=str(item.id), payload=item.input_data_json)
|
|
103
|
+
# Returns without ENDED — no background heartbeat from TaskServer
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# Elsewhere: each time you poll your DB for outstanding work (including after restart):
|
|
107
|
+
for row in db.outstanding_rows():
|
|
108
|
+
server.client.queue.heartbeat(row.queue_item_id)
|
|
109
|
+
if mail_arrived(row):
|
|
110
|
+
server.complete_queue_item(row.queue_item_id, output_data={"received": True})
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Standalone process (no `TaskServer`): build a `ContinuumClient` with the worker key and call `client.queue.heartbeat` / `client.queue.update_status` the same way.
|
|
114
|
+
|
|
115
|
+
### TaskServer options
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
TaskServer(
|
|
119
|
+
base_url="http://localhost:8080",
|
|
120
|
+
api_key="...",
|
|
121
|
+
max_workers=4, # thread pool size across all tasks
|
|
122
|
+
poll_interval=1.0, # initial poll delay (backs off when idle)
|
|
123
|
+
max_poll_interval=5.0, # max idle poll delay
|
|
124
|
+
heartbeat_interval=15.0, # how often to call /heartbeat per running task
|
|
125
|
+
shutdown_timeout=30.0, # how long to wait for handlers during shutdown
|
|
126
|
+
transport="http", # "websocket" uses STOMP; httpx transports still accepted
|
|
127
|
+
)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Per-task concurrency limit:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
@server.task("docker-run", concurrency=2)
|
|
134
|
+
def run_docker(item): ...
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Handler signature: `def handler(item: QueueItem) -> dict | list | str | None`. Inside, you have:
|
|
138
|
+
|
|
139
|
+
- `item.input_data` — raw JSON string from the queue item (or `None`).
|
|
140
|
+
- `item.input_data_json` — parsed value (`dict` / `list` / `str` / `None`).
|
|
141
|
+
- `server.client` — full `ContinuumClient` if you need to chain management calls, fetch content, enqueue child tasks, etc.
|
|
142
|
+
|
|
143
|
+
## Using `ContinuumClient` directly
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from continuum_task_server import ContinuumClient, TaskStatus
|
|
147
|
+
|
|
148
|
+
with ContinuumClient(base_url="http://localhost:8080", api_key="...") as client:
|
|
149
|
+
# Task types
|
|
150
|
+
types = client.task_types.list()
|
|
151
|
+
echo_type = client.task_types.get_by_name("echo")
|
|
152
|
+
|
|
153
|
+
# Task items + versions
|
|
154
|
+
item = client.task_items.get_by_name("my-task")
|
|
155
|
+
versions = client.task_items.versions.list(item.id)
|
|
156
|
+
|
|
157
|
+
# Enqueue work
|
|
158
|
+
queued = client.queue.add(task_name="echo", input_data={"hello": "world"})
|
|
159
|
+
|
|
160
|
+
# Worker-side primitives (normally handled by TaskServer)
|
|
161
|
+
claimed = client.queue.claim("echo")
|
|
162
|
+
if claimed is not None:
|
|
163
|
+
client.queue.heartbeat(claimed.id)
|
|
164
|
+
client.queue.update_status(claimed.id, TaskStatus.ENDED, output_data={"ok": True})
|
|
165
|
+
|
|
166
|
+
# Content store
|
|
167
|
+
content = client.content_store.get_by_url("db://...")
|
|
168
|
+
if content is not None:
|
|
169
|
+
print(content.as_text())
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`input_data` / `output_data` accept `dict` / `list` / `str` / `None`; non-string values are JSON-encoded for you.
|
|
173
|
+
|
|
174
|
+
## WebSocket / STOMP
|
|
175
|
+
|
|
176
|
+
The Queue API worker socket is native STOMP at `/ws/queue` (API-key handshake, no SockJS/JWT). `http://` base URLs become `ws://`; `https://` becomes `wss://`.
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from continuum_task_server import ContinuumWebSocketClient, TaskStatus, WaitMode
|
|
180
|
+
|
|
181
|
+
with ContinuumWebSocketClient(base_url, api_key) as queue:
|
|
182
|
+
queue.subscribe_work("echo")
|
|
183
|
+
hint = queue.next_event(timeout=30) # work.available is a hint, not a reservation
|
|
184
|
+
item = queue.claim("echo") # None if another worker won
|
|
185
|
+
queue.heartbeat(item.id)
|
|
186
|
+
wait = queue.subscribe_wait(
|
|
187
|
+
"run-42",
|
|
188
|
+
mode=WaitMode.EACH,
|
|
189
|
+
queue_item_ids=[item.id],
|
|
190
|
+
)
|
|
191
|
+
# Or create the child and wait in one transaction:
|
|
192
|
+
# created = queue.enqueue_and_subscribe_wait(
|
|
193
|
+
# task_name="echo", subscriber_id="run-42", mode=WaitMode.EACH, input_data={"hello": "world"},
|
|
194
|
+
# )
|
|
195
|
+
queue.update_status(item.id, TaskStatus.ENDED, output_data={"ok": True})
|
|
196
|
+
completed = queue.next_event(timeout=30) # queue.completed; dedupe by event_id
|
|
197
|
+
queue.ack_event("run-42", completed.event_id)
|
|
198
|
+
queue.unsubscribe_work("echo")
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
- **TaskServer WebSocket mode** uses this client for subscribe / claim / heartbeat / status. There is no HTTP fallback poll; recovery is reconnect + resubscribe. The API also checks PostgreSQL at subscribe time and periodically for connected sessions.
|
|
202
|
+
- **Reconnect** restores `/user/queue/replies` and `/user/queue/events`, every `subscribeWork` task name, and every durable `subscriberId` via bind-only `subscribeWait` (replays unacked completions from PostgreSQL).
|
|
203
|
+
- **Acks are explicit.** Do not assume the SDK acked a delivery. Completions are at-least-once; ignore duplicate `eventId`s.
|
|
204
|
+
- **Claim and enqueue are not retried** if the socket drops after the frame is written. The reply may have been lost; check queue state instead of sending the same command again. Heartbeat and status wait for reconnect and retry.
|
|
205
|
+
- **Durable waits** use a client-chosen `subscriberId` scoped to the API-key owner. `EACH` delivers per terminal item; `ALL` delivers one summary. Bind with only `subscriberId` after reconnect.
|
|
206
|
+
|
|
207
|
+
See `examples/websocket_bindings.py` and `examples/wait_subscriber.py`.
|
|
208
|
+
|
|
209
|
+
### Errors
|
|
210
|
+
|
|
211
|
+
All API failures raise `ContinuumError` or a specific subclass: `BadRequestError`, `UnauthorizedError`, `ForbiddenError`, `NotFoundError`, `ConflictError`, `RateLimitError`, `ServerError`, plus WebSocket `TransportError`, `ProtocolError`, `CommandTimeoutError`, and `AmbiguousCommandError`. Each carries `status_code` and `body` when they come from the API.
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
from continuum_task_server import ContinuumClient, NotFoundError
|
|
215
|
+
|
|
216
|
+
with ContinuumClient(...) as client:
|
|
217
|
+
try:
|
|
218
|
+
client.task_types.get_by_name("does-not-exist")
|
|
219
|
+
except NotFoundError as e:
|
|
220
|
+
print(e.status_code, e.body)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Endpoints covered
|
|
224
|
+
|
|
225
|
+
| Group | Method | Path |
|
|
226
|
+
| --- | --- | --- |
|
|
227
|
+
| Task Types | `GET`/`POST` | `/api/management/task-types[/{id}\|/by-name]` |
|
|
228
|
+
| Task Items | `GET`/`POST` | `/api/management/task-items[/{id}\|/by-name\|/{id}/publish]` |
|
|
229
|
+
| Task Item Versions | `GET`/`POST`/`PATCH` | `/api/management/task-items/{id}/versions[...]` |
|
|
230
|
+
| Queue (management) | `GET`/`POST` | `/api/management/queue-items[/{id}]` |
|
|
231
|
+
| Queue (worker) | `POST` | `/api/queue/claim`, `/api/queue/queue-items/{id}/heartbeat`, `/api/queue/queue-items/{id}/status` |
|
|
232
|
+
| Queue content | `GET` | `/api/queue/queue-items/{id}/content` |
|
|
233
|
+
| Content store | `GET` | `/api/management/content-store[/{id}\|?url=db://...]` |
|
|
234
|
+
|
|
235
|
+
All requests send `Api-Key: <your-key>`. `204 No Content` responses are normalized to `None` (e.g. `client.queue.claim()` returns `None` when nothing's available).
|
|
236
|
+
|
|
237
|
+
## Development
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
python -m venv .venv && source .venv/bin/activate
|
|
241
|
+
pip install -e ".[dev]"
|
|
242
|
+
|
|
243
|
+
ruff check .
|
|
244
|
+
ruff format --check .
|
|
245
|
+
pytest
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Tests use [`respx`](https://lundberg.github.io/respx/) to mock the httpx transport and an in-memory WebSocket for STOMP tests — no live server required.
|
|
249
|
+
|
|
250
|
+
## Versioning
|
|
251
|
+
|
|
252
|
+
- **Tag `v1.2.3`** → release wheel `1.2.3` attached to a `v1.2.3` GitHub Release.
|
|
253
|
+
- **Push to `main`** → prerelease wheel `0.1.0.dev{run}+{shortsha}` attached to a `dev-{shortsha}` Release.
|
|
254
|
+
- **Pull request** → prerelease wheel `0.1.0b{pr}.{run}` attached to a `pr-{pr}` Release; install command posted as a PR comment.
|
|
255
|
+
|
|
256
|
+
## License
|
|
257
|
+
|
|
258
|
+
MIT.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
continuum_task_server/__init__.py,sha256=QH6eYhZahD6MjzN7QcmA_okMBxnhRjwTJN5savEvUDE,1545
|
|
2
|
+
continuum_task_server/_http.py,sha256=feeOeqCUAwDASEfH3RS6mtEysHPkmj_6mtNCyGo3J9o,4776
|
|
3
|
+
continuum_task_server/_stomp.py,sha256=5CNVCsu-0NXIpjob3KE8u9zmx5EhM4daWM853SrwMkA,3780
|
|
4
|
+
continuum_task_server/client.py,sha256=wHpY_R5w2uB9djS05j1KyZOAH1QZQPOPaizT_KrqaUA,11658
|
|
5
|
+
continuum_task_server/exceptions.py,sha256=W9IHIoKn0ZEjFSzbBO5KQI9cSDrqw16Bil-6N25YmPU,2351
|
|
6
|
+
continuum_task_server/models.py,sha256=mdlUtDzUg5XAYro3fRVn1gZmZnNrEggWEu_EahK-0Oo,6829
|
|
7
|
+
continuum_task_server/server.py,sha256=0flv02prTcf3hihwfK3MboK0NLyYG8fs2pXkChrk0MQ,18859
|
|
8
|
+
continuum_task_server/websocket.py,sha256=C238qOBvr30bLSmMqPlmaEfafmFcL3ubFzOEa9bX_MY,31748
|
|
9
|
+
continuum_task_server_sdk-1.1.0.dist-info/METADATA,sha256=rXbb2k9OmaY_U8B003PXFT0qWkJtyF8UgrjNcUA2w7Y,11692
|
|
10
|
+
continuum_task_server_sdk-1.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
11
|
+
continuum_task_server_sdk-1.1.0.dist-info/RECORD,,
|