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,94 @@
|
|
|
1
|
+
"""Exception types raised by the Continuum SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ContinuumError(Exception):
|
|
7
|
+
"""Base exception for all Continuum SDK errors."""
|
|
8
|
+
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
message: str,
|
|
12
|
+
*,
|
|
13
|
+
status_code: int | None = None,
|
|
14
|
+
body: str | None = None,
|
|
15
|
+
) -> None:
|
|
16
|
+
super().__init__(message)
|
|
17
|
+
self.status_code = status_code
|
|
18
|
+
self.body = body
|
|
19
|
+
|
|
20
|
+
def __str__(self) -> str:
|
|
21
|
+
base = super().__str__()
|
|
22
|
+
if self.status_code is not None:
|
|
23
|
+
return f"[{self.status_code}] {base}"
|
|
24
|
+
return base
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class BadRequestError(ContinuumError):
|
|
28
|
+
"""HTTP / command 400."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class UnauthorizedError(ContinuumError):
|
|
32
|
+
"""HTTP 401 - check API key type (Organization vs Worker)."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ForbiddenError(ContinuumError):
|
|
36
|
+
"""HTTP 403."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class NotFoundError(ContinuumError):
|
|
40
|
+
"""HTTP 404."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ConflictError(ContinuumError):
|
|
44
|
+
"""HTTP 409."""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class RateLimitError(ContinuumError):
|
|
48
|
+
"""HTTP / command 429."""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ServerError(ContinuumError):
|
|
52
|
+
"""HTTP 5xx."""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class TransportError(ContinuumError):
|
|
56
|
+
"""WebSocket / STOMP connection failed or dropped."""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ProtocolError(ContinuumError):
|
|
60
|
+
"""Malformed STOMP frame or unexpected protocol response."""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class CommandTimeoutError(ContinuumError):
|
|
64
|
+
"""Timed out waiting for a correlated command reply."""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class AmbiguousCommandError(TransportError):
|
|
68
|
+
"""A non-idempotent command was sent but its reply was lost.
|
|
69
|
+
|
|
70
|
+
The server may or may not have applied the mutation (claim / enqueue).
|
|
71
|
+
Do not retry with the same intent without checking queue state.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
_STATUS_TO_EXC: dict[int, type[ContinuumError]] = {
|
|
76
|
+
400: BadRequestError,
|
|
77
|
+
401: UnauthorizedError,
|
|
78
|
+
403: ForbiddenError,
|
|
79
|
+
404: NotFoundError,
|
|
80
|
+
409: ConflictError,
|
|
81
|
+
429: RateLimitError,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def error_for_status(status_code: int, body: str) -> ContinuumError:
|
|
86
|
+
"""Build the appropriate exception for a non-2xx HTTP or command status."""
|
|
87
|
+
exc_cls = _STATUS_TO_EXC.get(status_code)
|
|
88
|
+
if exc_cls is None:
|
|
89
|
+
if 500 <= status_code < 600:
|
|
90
|
+
exc_cls = ServerError
|
|
91
|
+
else:
|
|
92
|
+
exc_cls = ContinuumError
|
|
93
|
+
message = body if body else f"Request failed with status {status_code}"
|
|
94
|
+
return exc_cls(message, status_code=status_code, body=body)
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Pydantic models mirroring the Continuum Task Server JSON shapes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Any
|
|
9
|
+
from uuid import UUID
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TaskStatus(str, Enum):
|
|
15
|
+
"""Status of a queue item in the task processing lifecycle."""
|
|
16
|
+
|
|
17
|
+
OPEN = "OPEN"
|
|
18
|
+
CLAIMED = "CLAIMED"
|
|
19
|
+
STARTED = "STARTED"
|
|
20
|
+
TIMEDOUT = "TIMEDOUT"
|
|
21
|
+
CANCELLED = "CANCELLED"
|
|
22
|
+
KILLED = "KILLED"
|
|
23
|
+
ENDED = "ENDED"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _Base(BaseModel):
|
|
27
|
+
model_config = ConfigDict(populate_by_name=True, extra="ignore")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TaskType(_Base):
|
|
31
|
+
id: UUID | None = None
|
|
32
|
+
name: str | None = None
|
|
33
|
+
description: str | None = None
|
|
34
|
+
active_flag: bool = Field(default=False, alias="activeFlag")
|
|
35
|
+
organization_id: UUID | None = Field(default=None, alias="organizationId")
|
|
36
|
+
is_global: bool = Field(default=False, alias="isGlobal")
|
|
37
|
+
max_duration_sec: int | None = Field(default=None, alias="maxDurationSec")
|
|
38
|
+
heartbeat_timeout_sec: int | None = Field(default=None, alias="heartbeatTimeoutSec")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TaskItem(_Base):
|
|
42
|
+
id: UUID | None = None
|
|
43
|
+
name: str | None = None
|
|
44
|
+
organization_id: UUID | None = Field(default=None, alias="organizationId")
|
|
45
|
+
is_global: bool = Field(default=False, alias="isGlobal")
|
|
46
|
+
active_flag: bool = Field(default=False, alias="activeFlag")
|
|
47
|
+
published_version: int | None = Field(default=None, alias="publishedVersion")
|
|
48
|
+
task_type_id: UUID | None = Field(default=None, alias="taskTypeId")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class TaskItemVersion(_Base):
|
|
52
|
+
id: UUID | None = None
|
|
53
|
+
task_item_id: UUID | None = Field(default=None, alias="taskItemId")
|
|
54
|
+
version_number: int = Field(default=0, alias="versionNumber")
|
|
55
|
+
content_url: str | None = Field(default=None, alias="contentUrl")
|
|
56
|
+
main_path: str | None = Field(default=None, alias="mainPath")
|
|
57
|
+
item_definition: str | None = Field(default=None, alias="itemDefinition")
|
|
58
|
+
active_flag: bool = Field(default=False, alias="activeFlag")
|
|
59
|
+
created_by: UUID | None = Field(default=None, alias="createdBy")
|
|
60
|
+
created_dtm: datetime | None = Field(default=None, alias="createdDtm")
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def item_definition_json(self) -> Any:
|
|
64
|
+
if not self.item_definition:
|
|
65
|
+
return None
|
|
66
|
+
try:
|
|
67
|
+
return json.loads(self.item_definition)
|
|
68
|
+
except (ValueError, TypeError):
|
|
69
|
+
return self.item_definition
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class QueueItem(_Base):
|
|
73
|
+
id: UUID
|
|
74
|
+
depth: int = 0
|
|
75
|
+
task_type_id: UUID | None = Field(default=None, alias="taskTypeId")
|
|
76
|
+
task_item_version_id: UUID | None = Field(default=None, alias="taskItemVersionId")
|
|
77
|
+
priority: int = 0
|
|
78
|
+
task_status: TaskStatus | None = Field(default=None, alias="taskStatus")
|
|
79
|
+
organization_id: UUID | None = Field(default=None, alias="organizationId")
|
|
80
|
+
created_dtm: datetime | None = Field(default=None, alias="createdDtm")
|
|
81
|
+
created_by: UUID | None = Field(default=None, alias="createdBy")
|
|
82
|
+
input_data: str | None = Field(default=None, alias="inputData")
|
|
83
|
+
output_data: str | None = Field(default=None, alias="outputData")
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def input_data_json(self) -> Any:
|
|
87
|
+
"""Parse `input_data` as JSON. Returns None if input_data is None/empty,
|
|
88
|
+
or the raw string if it's not valid JSON."""
|
|
89
|
+
if not self.input_data:
|
|
90
|
+
return None
|
|
91
|
+
try:
|
|
92
|
+
return json.loads(self.input_data)
|
|
93
|
+
except (ValueError, TypeError):
|
|
94
|
+
return self.input_data
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def output_data_json(self) -> Any:
|
|
98
|
+
if not self.output_data:
|
|
99
|
+
return None
|
|
100
|
+
try:
|
|
101
|
+
return json.loads(self.output_data)
|
|
102
|
+
except (ValueError, TypeError):
|
|
103
|
+
return self.output_data
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class Content(_Base):
|
|
107
|
+
"""Raw content returned from a content-store or queue-item content endpoint."""
|
|
108
|
+
|
|
109
|
+
data: bytes
|
|
110
|
+
mime_type: str = "application/octet-stream"
|
|
111
|
+
|
|
112
|
+
def as_text(self, encoding: str = "utf-8") -> str:
|
|
113
|
+
return self.data.decode(encoding)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class TransportMode(str, Enum):
|
|
117
|
+
"""How ``TaskServer`` obtains work."""
|
|
118
|
+
|
|
119
|
+
HTTP = "http"
|
|
120
|
+
WEBSOCKET = "websocket"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class WaitMode(str, Enum):
|
|
124
|
+
"""Durable completion-wait mode."""
|
|
125
|
+
|
|
126
|
+
EACH = "EACH"
|
|
127
|
+
ALL = "ALL"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class QueueEventType(str, Enum):
|
|
131
|
+
"""Wire ``eventType`` values on ``/user/queue/events``."""
|
|
132
|
+
|
|
133
|
+
WORK_AVAILABLE = "work.available"
|
|
134
|
+
COMPLETED = "queue.completed"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class WaitTargetStatus(_Base):
|
|
138
|
+
"""Per-item outcome on an ALL-mode completion summary."""
|
|
139
|
+
|
|
140
|
+
queue_item_id: UUID = Field(alias="queueItemId")
|
|
141
|
+
status: TaskStatus | str | None = None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class QueueEvent(_Base):
|
|
145
|
+
"""Compact ``work.available`` hint or durable ``queue.completed`` delivery."""
|
|
146
|
+
|
|
147
|
+
event_id: UUID = Field(alias="eventId")
|
|
148
|
+
event_version: int = Field(default=1, alias="eventVersion")
|
|
149
|
+
event_type: QueueEventType = Field(alias="eventType")
|
|
150
|
+
occurred_at: datetime | None = Field(default=None, alias="occurredAt")
|
|
151
|
+
queue_item_id: UUID | None = Field(default=None, alias="queueItemId")
|
|
152
|
+
organization_id: UUID | None = Field(default=None, alias="organizationId")
|
|
153
|
+
task_type_id: UUID | None = Field(default=None, alias="taskTypeId")
|
|
154
|
+
task_name: str | None = Field(default=None, alias="taskName")
|
|
155
|
+
status: TaskStatus | None = None
|
|
156
|
+
subscription_id: UUID | None = Field(default=None, alias="subscriptionId")
|
|
157
|
+
correlation: str | None = None
|
|
158
|
+
wait_mode: WaitMode | None = Field(default=None, alias="waitMode")
|
|
159
|
+
targets: list[WaitTargetStatus] | None = None
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class WorkSubscriptionResult(_Base):
|
|
163
|
+
"""Result of ``subscribeWork`` / ``unsubscribeWork``."""
|
|
164
|
+
|
|
165
|
+
task_name: str | None = Field(default=None, alias="taskName")
|
|
166
|
+
subscribed: bool | None = None
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class WaitSubscriptionResult(_Base):
|
|
170
|
+
"""Result of ``subscribeWait`` / bind-only replay."""
|
|
171
|
+
|
|
172
|
+
subscription_id: UUID | None = Field(default=None, alias="subscriptionId")
|
|
173
|
+
subscriber_id: str | None = Field(default=None, alias="subscriberId")
|
|
174
|
+
wait_mode: WaitMode | None = Field(default=None, alias="waitMode")
|
|
175
|
+
pending_count: int = Field(default=0, alias="pendingCount")
|
|
176
|
+
closed: bool = False
|
|
177
|
+
subscribed: bool | None = None
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class EventAckResult(_Base):
|
|
181
|
+
"""Result of ``ackEvent``."""
|
|
182
|
+
|
|
183
|
+
subscription_id: UUID | None = Field(default=None, alias="subscriptionId")
|
|
184
|
+
closed: bool = False
|
|
185
|
+
acked: bool = True
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class EnqueueAndSubscribeWaitResult(_Base):
|
|
189
|
+
"""Combined result of ``enqueueAndSubscribeWait``."""
|
|
190
|
+
|
|
191
|
+
item: QueueItem
|
|
192
|
+
subscription_id: UUID | None = Field(default=None, alias="subscriptionId")
|
|
193
|
+
subscriber_id: str | None = Field(default=None, alias="subscriberId")
|
|
194
|
+
wait_mode: WaitMode | None = Field(default=None, alias="waitMode")
|
|
195
|
+
pending_count: int = Field(default=0, alias="pendingCount")
|
|
196
|
+
closed: bool = False
|
|
197
|
+
subscribed: bool | None = None
|