continuum-task-server-sdk 1.2.0__tar.gz → 1.4.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.
- {continuum_task_server_sdk-1.2.0 → continuum_task_server_sdk-1.4.0}/PKG-INFO +9 -1
- {continuum_task_server_sdk-1.2.0 → continuum_task_server_sdk-1.4.0}/README.md +8 -0
- {continuum_task_server_sdk-1.2.0 → continuum_task_server_sdk-1.4.0}/continuum_task_server/__init__.py +3 -1
- {continuum_task_server_sdk-1.2.0 → continuum_task_server_sdk-1.4.0}/continuum_task_server/client.py +27 -1
- {continuum_task_server_sdk-1.2.0 → continuum_task_server_sdk-1.4.0}/continuum_task_server/models.py +8 -0
- {continuum_task_server_sdk-1.2.0 → continuum_task_server_sdk-1.4.0}/continuum_task_server/server.py +66 -10
- {continuum_task_server_sdk-1.2.0 → continuum_task_server_sdk-1.4.0}/pyproject.toml +1 -1
- {continuum_task_server_sdk-1.2.0 → continuum_task_server_sdk-1.4.0}/.gitignore +0 -0
- {continuum_task_server_sdk-1.2.0 → continuum_task_server_sdk-1.4.0}/continuum_task_server/_http.py +0 -0
- {continuum_task_server_sdk-1.2.0 → continuum_task_server_sdk-1.4.0}/continuum_task_server/_stomp.py +0 -0
- {continuum_task_server_sdk-1.2.0 → continuum_task_server_sdk-1.4.0}/continuum_task_server/context.py +0 -0
- {continuum_task_server_sdk-1.2.0 → continuum_task_server_sdk-1.4.0}/continuum_task_server/exceptions.py +0 -0
- {continuum_task_server_sdk-1.2.0 → continuum_task_server_sdk-1.4.0}/continuum_task_server/websocket.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: continuum-task-server-sdk
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
4
4
|
Summary: Python SDK for the Continuum Task Server
|
|
5
5
|
Project-URL: Homepage, https://github.com/ContinuumWorkflow/continuum-task-server-sdk-python
|
|
6
6
|
Project-URL: Issues, https://github.com/ContinuumWorkflow/continuum-task-server-sdk-python/issues
|
|
@@ -112,6 +112,14 @@ for row in db.outstanding_rows():
|
|
|
112
112
|
|
|
113
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
114
|
|
|
115
|
+
By default the deferred claim still occupies the per-task ``concurrency`` slot until ``complete_queue_item``. Set ``occupy_slot_until_complete=False`` so the handler is still gated (you will not insert 10k rows at once) but the slot is freed when the function returns:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
@server.task("discord", auto_complete=False, occupy_slot_until_complete=False)
|
|
119
|
+
def discord(item):
|
|
120
|
+
db.insert_await(queue_item_id=str(item.id), payload=item.input_data_json)
|
|
121
|
+
```
|
|
122
|
+
|
|
115
123
|
### TaskServer options
|
|
116
124
|
|
|
117
125
|
```python
|
|
@@ -83,6 +83,14 @@ for row in db.outstanding_rows():
|
|
|
83
83
|
|
|
84
84
|
Standalone process (no `TaskServer`): build a `ContinuumClient` with the worker key and call `client.queue.heartbeat` / `client.queue.update_status` the same way.
|
|
85
85
|
|
|
86
|
+
By default the deferred claim still occupies the per-task ``concurrency`` slot until ``complete_queue_item``. Set ``occupy_slot_until_complete=False`` so the handler is still gated (you will not insert 10k rows at once) but the slot is freed when the function returns:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
@server.task("discord", auto_complete=False, occupy_slot_until_complete=False)
|
|
90
|
+
def discord(item):
|
|
91
|
+
db.insert_await(queue_item_id=str(item.id), payload=item.input_data_json)
|
|
92
|
+
```
|
|
93
|
+
|
|
86
94
|
### TaskServer options
|
|
87
95
|
|
|
88
96
|
```python
|
|
@@ -22,6 +22,7 @@ from .models import (
|
|
|
22
22
|
Content,
|
|
23
23
|
EnqueueAndSubscribeWaitResult,
|
|
24
24
|
EventAckResult,
|
|
25
|
+
QueueDescendantsCancelResult,
|
|
25
26
|
QueueEvent,
|
|
26
27
|
QueueEventType,
|
|
27
28
|
QueueItem,
|
|
@@ -53,6 +54,7 @@ __all__ = [
|
|
|
53
54
|
"ForbiddenError",
|
|
54
55
|
"NotFoundError",
|
|
55
56
|
"ProtocolError",
|
|
57
|
+
"QueueDescendantsCancelResult",
|
|
56
58
|
"QueueEvent",
|
|
57
59
|
"QueueEventType",
|
|
58
60
|
"QueueItem",
|
|
@@ -75,4 +77,4 @@ __all__ = [
|
|
|
75
77
|
"WorkSubscriptionResult",
|
|
76
78
|
]
|
|
77
79
|
|
|
78
|
-
__version__ = "
|
|
80
|
+
__version__ = "1.4.0"
|
{continuum_task_server_sdk-1.2.0 → continuum_task_server_sdk-1.4.0}/continuum_task_server/client.py
RENAMED
|
@@ -12,7 +12,15 @@ import httpx
|
|
|
12
12
|
|
|
13
13
|
from ._http import HttpClient, drop_none
|
|
14
14
|
from .exceptions import ContinuumError
|
|
15
|
-
from .models import
|
|
15
|
+
from .models import (
|
|
16
|
+
Content,
|
|
17
|
+
QueueDescendantsCancelResult,
|
|
18
|
+
QueueItem,
|
|
19
|
+
TaskItem,
|
|
20
|
+
TaskItemVersion,
|
|
21
|
+
TaskStatus,
|
|
22
|
+
TaskType,
|
|
23
|
+
)
|
|
16
24
|
|
|
17
25
|
|
|
18
26
|
def _encode_input(data: Any) -> str | None:
|
|
@@ -326,6 +334,24 @@ class QueueApi:
|
|
|
326
334
|
raise ContinuumError(message or "control-complete failed")
|
|
327
335
|
return QueueItem.model_validate(result["item"])
|
|
328
336
|
|
|
337
|
+
def cancel_descendants(
|
|
338
|
+
self,
|
|
339
|
+
queue_item_id: UUID | str,
|
|
340
|
+
*,
|
|
341
|
+
claim_token: UUID | str | None = None,
|
|
342
|
+
) -> QueueDescendantsCancelResult:
|
|
343
|
+
"""Cancel descendants of a claimed item without cancelling the root."""
|
|
344
|
+
body = drop_none({"claimToken": str(claim_token) if claim_token else None})
|
|
345
|
+
result = self._http.post_json(
|
|
346
|
+
f"{self._QUEUE}/queue-items/{queue_item_id}/cancel-descendants",
|
|
347
|
+
body,
|
|
348
|
+
)
|
|
349
|
+
status = 0 if result is None else int(result.get("status", 0))
|
|
350
|
+
if result is None or status != 0:
|
|
351
|
+
message = None if result is None else result.get("message")
|
|
352
|
+
raise ContinuumError(message or "cancel-descendants failed")
|
|
353
|
+
return QueueDescendantsCancelResult.model_validate(result)
|
|
354
|
+
|
|
329
355
|
|
|
330
356
|
class ContentStoreApi:
|
|
331
357
|
_PATH = "/api/management/content-store"
|
{continuum_task_server_sdk-1.2.0 → continuum_task_server_sdk-1.4.0}/continuum_task_server/models.py
RENAMED
|
@@ -125,6 +125,14 @@ class QueueItem(_Base):
|
|
|
125
125
|
return self.output_data
|
|
126
126
|
|
|
127
127
|
|
|
128
|
+
class QueueDescendantsCancelResult(_Base):
|
|
129
|
+
"""Result of ``POST /api/queue/queue-items/{id}/cancel-descendants``."""
|
|
130
|
+
|
|
131
|
+
status: int = 0
|
|
132
|
+
message: str | None = None
|
|
133
|
+
cancelled_count: int | None = Field(default=None, alias="cancelledCount")
|
|
134
|
+
|
|
135
|
+
|
|
128
136
|
class Content(_Base):
|
|
129
137
|
"""Raw content returned from a content-store or queue-item content endpoint."""
|
|
130
138
|
|
{continuum_task_server_sdk-1.2.0 → continuum_task_server_sdk-1.4.0}/continuum_task_server/server.py
RENAMED
|
@@ -9,6 +9,10 @@ elsewhere and you will call ``complete_queue_item`` / ``fail_queue_item`` later.
|
|
|
9
9
|
The SDK does **not** heartbeat after the handler returns; you must call
|
|
10
10
|
``client.queue.heartbeat`` yourself (for example on each pass of your DB poll) so
|
|
11
11
|
the claim stays alive across process restarts.
|
|
12
|
+
|
|
13
|
+
By default a deferred claim still occupies the per-task ``concurrency`` slot
|
|
14
|
+
until complete. Pass ``occupy_slot_until_complete=False`` to free the slot when
|
|
15
|
+
the handler returns so another item can be claimed while the first stays open.
|
|
12
16
|
"""
|
|
13
17
|
|
|
14
18
|
from __future__ import annotations
|
|
@@ -45,6 +49,7 @@ class _Registration:
|
|
|
45
49
|
handler: Handler
|
|
46
50
|
concurrency: int
|
|
47
51
|
auto_complete: bool
|
|
52
|
+
occupy_slot_until_complete: bool
|
|
48
53
|
semaphore: threading.Semaphore = field(init=False)
|
|
49
54
|
|
|
50
55
|
def __post_init__(self) -> None:
|
|
@@ -67,7 +72,8 @@ class TaskServer:
|
|
|
67
72
|
With ``auto_complete=False``, the handler returns without ENDED. Automatic
|
|
68
73
|
heartbeats stop when the handler returns. Heartbeat from the completing
|
|
69
74
|
process with ``client.queue.heartbeat`` until ``complete_queue_item`` or
|
|
70
|
-
``fail_queue_item``.
|
|
75
|
+
``fail_queue_item``. The concurrency slot stays occupied until complete
|
|
76
|
+
unless ``occupy_slot_until_complete=False``.
|
|
71
77
|
"""
|
|
72
78
|
|
|
73
79
|
def __init__(
|
|
@@ -135,6 +141,7 @@ class TaskServer:
|
|
|
135
141
|
*,
|
|
136
142
|
concurrency: int = 1,
|
|
137
143
|
auto_complete: bool = True,
|
|
144
|
+
occupy_slot_until_complete: bool = True,
|
|
138
145
|
) -> Callable[[Handler], Handler]:
|
|
139
146
|
"""Decorator registering ``handler`` for queue items of task-type ``name``.
|
|
140
147
|
|
|
@@ -143,6 +150,10 @@ class TaskServer:
|
|
|
143
150
|
If ``auto_complete`` is False, the handler returns without the server sending
|
|
144
151
|
ENDED; call ``complete_queue_item`` or ``fail_queue_item`` when done. You must
|
|
145
152
|
heartbeat the queue item yourself until then (see README).
|
|
153
|
+
|
|
154
|
+
``occupy_slot_until_complete`` (default True) keeps the concurrency slot until
|
|
155
|
+
that complete/fail. Set False so the slot is freed when the handler returns;
|
|
156
|
+
the claim stays in ``_active`` for complete/cancel.
|
|
146
157
|
"""
|
|
147
158
|
if concurrency < 1:
|
|
148
159
|
raise ValueError("concurrency must be >= 1")
|
|
@@ -155,6 +166,7 @@ class TaskServer:
|
|
|
155
166
|
handler=handler,
|
|
156
167
|
concurrency=concurrency,
|
|
157
168
|
auto_complete=auto_complete,
|
|
169
|
+
occupy_slot_until_complete=occupy_slot_until_complete,
|
|
158
170
|
)
|
|
159
171
|
return handler
|
|
160
172
|
|
|
@@ -167,9 +179,15 @@ class TaskServer:
|
|
|
167
179
|
*,
|
|
168
180
|
concurrency: int = 1,
|
|
169
181
|
auto_complete: bool = True,
|
|
182
|
+
occupy_slot_until_complete: bool = True,
|
|
170
183
|
) -> None:
|
|
171
184
|
"""Imperative alternative to ``@task``."""
|
|
172
|
-
self.task(
|
|
185
|
+
self.task(
|
|
186
|
+
name,
|
|
187
|
+
concurrency=concurrency,
|
|
188
|
+
auto_complete=auto_complete,
|
|
189
|
+
occupy_slot_until_complete=occupy_slot_until_complete,
|
|
190
|
+
)(handler)
|
|
173
191
|
|
|
174
192
|
def complete_queue_item(
|
|
175
193
|
self,
|
|
@@ -191,6 +209,7 @@ class TaskServer:
|
|
|
191
209
|
self._safe_update_status_by_id(qid, TaskStatus.ENDED, output=output_data, claim_token=token)
|
|
192
210
|
if claim is not None:
|
|
193
211
|
claim.mark_completed()
|
|
212
|
+
self._finish_claim(claim)
|
|
194
213
|
|
|
195
214
|
def fail_queue_item(
|
|
196
215
|
self,
|
|
@@ -223,6 +242,7 @@ class TaskServer:
|
|
|
223
242
|
self._safe_update_status_by_id(qid, TaskStatus.KILLED, output=payload, claim_token=token)
|
|
224
243
|
if claim is not None:
|
|
225
244
|
claim.mark_completed()
|
|
245
|
+
self._finish_claim(claim)
|
|
226
246
|
|
|
227
247
|
@staticmethod
|
|
228
248
|
def _normalize_queue_id(queue_item_id: uuid.UUID | str) -> uuid.UUID:
|
|
@@ -428,8 +448,13 @@ class TaskServer:
|
|
|
428
448
|
self._inflight.discard(future)
|
|
429
449
|
|
|
430
450
|
def _run_item(self, registration: _Registration, item: QueueItem) -> None:
|
|
431
|
-
claim = _ActiveClaim(
|
|
451
|
+
claim = _ActiveClaim(
|
|
452
|
+
item=item,
|
|
453
|
+
server=self,
|
|
454
|
+
occupy_slot_until_complete=registration.occupy_slot_until_complete,
|
|
455
|
+
)
|
|
432
456
|
self._active[item.id] = claim
|
|
457
|
+
slot_held = True
|
|
433
458
|
try:
|
|
434
459
|
logger.info("Claimed queue item %s (task=%s)", item.id, registration.name)
|
|
435
460
|
heartbeat_thread = threading.Thread(
|
|
@@ -482,15 +507,29 @@ class TaskServer:
|
|
|
482
507
|
item.id,
|
|
483
508
|
)
|
|
484
509
|
self._stop_heartbeat(claim, heartbeat_thread)
|
|
485
|
-
|
|
510
|
+
claim.handler_returned.set()
|
|
511
|
+
if registration.occupy_slot_until_complete:
|
|
512
|
+
self._wait_for_deferred(claim)
|
|
513
|
+
else:
|
|
514
|
+
slot_held = False
|
|
515
|
+
self._release_slot(registration)
|
|
486
516
|
finally:
|
|
487
517
|
self._stop_heartbeat(claim, heartbeat_thread)
|
|
488
|
-
|
|
518
|
+
if slot_held:
|
|
519
|
+
self._unsubscribe_control(claim)
|
|
489
520
|
finally:
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
521
|
+
if slot_held:
|
|
522
|
+
self._active.pop(item.id, None)
|
|
523
|
+
self._release_slot(registration)
|
|
524
|
+
|
|
525
|
+
def _release_slot(self, registration: _Registration) -> None:
|
|
526
|
+
registration.semaphore.release()
|
|
527
|
+
if self._mode is TransportMode.WEBSOCKET:
|
|
528
|
+
self._try_claim_pending()
|
|
529
|
+
|
|
530
|
+
def _finish_claim(self, claim: _ActiveClaim) -> None:
|
|
531
|
+
self._unsubscribe_control(claim)
|
|
532
|
+
self._active.pop(claim.item.id, None)
|
|
494
533
|
|
|
495
534
|
def _wait_for_deferred(self, claim: _ActiveClaim) -> None:
|
|
496
535
|
while (
|
|
@@ -612,13 +651,22 @@ class TaskServer:
|
|
|
612
651
|
self._ws = None
|
|
613
652
|
if self._owns_client:
|
|
614
653
|
self._client.close()
|
|
654
|
+
for leftover in list(self._active.values()):
|
|
655
|
+
self._finish_claim(leftover)
|
|
615
656
|
logger.info("Continuum task server stopped")
|
|
616
657
|
|
|
617
658
|
|
|
618
659
|
class _ActiveClaim:
|
|
619
|
-
def __init__(
|
|
660
|
+
def __init__(
|
|
661
|
+
self,
|
|
662
|
+
*,
|
|
663
|
+
item: QueueItem,
|
|
664
|
+
server: TaskServer,
|
|
665
|
+
occupy_slot_until_complete: bool = True,
|
|
666
|
+
) -> None:
|
|
620
667
|
self.item = item
|
|
621
668
|
self.server = server
|
|
669
|
+
self.occupy_slot_until_complete = occupy_slot_until_complete
|
|
622
670
|
self.context = TaskExecutionContext(item)
|
|
623
671
|
self.stop_heartbeat = threading.Event()
|
|
624
672
|
self.control_seen = threading.Event()
|
|
@@ -626,6 +674,7 @@ class _ActiveClaim:
|
|
|
626
674
|
self.deferred_gate = threading.Event()
|
|
627
675
|
self.control_subscribed = False
|
|
628
676
|
self._control_acked = threading.Event()
|
|
677
|
+
self.handler_returned = threading.Event()
|
|
629
678
|
|
|
630
679
|
@property
|
|
631
680
|
def claim_token(self) -> uuid.UUID | None:
|
|
@@ -637,6 +686,12 @@ class _ActiveClaim:
|
|
|
637
686
|
self.control_seen.set()
|
|
638
687
|
self.context.request_cancel()
|
|
639
688
|
self.deferred_gate.set()
|
|
689
|
+
if self.handler_returned.is_set() and not self.occupy_slot_until_complete:
|
|
690
|
+
threading.Thread(
|
|
691
|
+
target=self.acknowledge_control,
|
|
692
|
+
name=f"control-ack-{self.item.id}",
|
|
693
|
+
daemon=True,
|
|
694
|
+
).start()
|
|
640
695
|
|
|
641
696
|
def mark_completed(self) -> None:
|
|
642
697
|
self.completed.set()
|
|
@@ -659,6 +714,7 @@ class _ActiveClaim:
|
|
|
659
714
|
except ContinuumError as e:
|
|
660
715
|
logger.warning("completeControl(%s) failed: %s", self.item.id, e)
|
|
661
716
|
self.mark_completed()
|
|
717
|
+
self.server._finish_claim(self)
|
|
662
718
|
|
|
663
719
|
|
|
664
720
|
def _accepts_context(handler: Handler) -> bool:
|
|
File without changes
|
{continuum_task_server_sdk-1.2.0 → continuum_task_server_sdk-1.4.0}/continuum_task_server/_http.py
RENAMED
|
File without changes
|
{continuum_task_server_sdk-1.2.0 → continuum_task_server_sdk-1.4.0}/continuum_task_server/_stomp.py
RENAMED
|
File without changes
|
{continuum_task_server_sdk-1.2.0 → continuum_task_server_sdk-1.4.0}/continuum_task_server/context.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|