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.
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any