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,74 @@
1
+ """Continuum Task Server SDK for Python."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .client import ContinuumClient
6
+ from .exceptions import (
7
+ AmbiguousCommandError,
8
+ BadRequestError,
9
+ CommandTimeoutError,
10
+ ConflictError,
11
+ ContinuumError,
12
+ ForbiddenError,
13
+ NotFoundError,
14
+ ProtocolError,
15
+ RateLimitError,
16
+ ServerError,
17
+ TransportError,
18
+ UnauthorizedError,
19
+ )
20
+ from .models import (
21
+ Content,
22
+ EnqueueAndSubscribeWaitResult,
23
+ EventAckResult,
24
+ QueueEvent,
25
+ QueueEventType,
26
+ QueueItem,
27
+ TaskItem,
28
+ TaskItemVersion,
29
+ TaskStatus,
30
+ TaskType,
31
+ TransportMode,
32
+ WaitMode,
33
+ WaitSubscriptionResult,
34
+ WaitTargetStatus,
35
+ WorkSubscriptionResult,
36
+ )
37
+ from .server import TaskServer
38
+ from .websocket import ContinuumWebSocketClient, WebSocketOptions
39
+
40
+ __all__ = [
41
+ "AmbiguousCommandError",
42
+ "BadRequestError",
43
+ "CommandTimeoutError",
44
+ "ConflictError",
45
+ "Content",
46
+ "ContinuumClient",
47
+ "ContinuumError",
48
+ "ContinuumWebSocketClient",
49
+ "EnqueueAndSubscribeWaitResult",
50
+ "EventAckResult",
51
+ "ForbiddenError",
52
+ "NotFoundError",
53
+ "ProtocolError",
54
+ "QueueEvent",
55
+ "QueueEventType",
56
+ "QueueItem",
57
+ "RateLimitError",
58
+ "ServerError",
59
+ "TaskItem",
60
+ "TaskItemVersion",
61
+ "TaskServer",
62
+ "TaskStatus",
63
+ "TaskType",
64
+ "TransportError",
65
+ "TransportMode",
66
+ "UnauthorizedError",
67
+ "WaitMode",
68
+ "WaitSubscriptionResult",
69
+ "WaitTargetStatus",
70
+ "WebSocketOptions",
71
+ "WorkSubscriptionResult",
72
+ ]
73
+
74
+ __version__ = "0.1.0"
@@ -0,0 +1,142 @@
1
+ """Internal HTTP wrapper. Handles Api-Key auth, JSON (de)serialization, and error mapping."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ from typing import Any
8
+
9
+ import httpx
10
+
11
+ from .exceptions import ContinuumError, error_for_status
12
+
13
+ logger = logging.getLogger("continuum_task_server")
14
+
15
+
16
+ def _normalize_base_url(url: str) -> str:
17
+ return url[:-1] if url.endswith("/") else url
18
+
19
+
20
+ class HttpClient:
21
+ """Thin httpx wrapper. Not part of the public API."""
22
+
23
+ def __init__(
24
+ self,
25
+ base_url: str,
26
+ api_key: str,
27
+ *,
28
+ connect_timeout: float = 10.0,
29
+ request_timeout: float = 30.0,
30
+ transport: httpx.BaseTransport | None = None,
31
+ ) -> None:
32
+ if not base_url:
33
+ raise ValueError("base_url must not be empty")
34
+ if not api_key:
35
+ raise ValueError("api_key must not be empty")
36
+ self.base_url = _normalize_base_url(base_url)
37
+ self.api_key = api_key
38
+ timeout = httpx.Timeout(request_timeout, connect=connect_timeout)
39
+ self._client = httpx.Client(
40
+ base_url=self.base_url,
41
+ timeout=timeout,
42
+ headers={"Api-Key": api_key},
43
+ transport=transport,
44
+ )
45
+
46
+ def close(self) -> None:
47
+ self._client.close()
48
+
49
+ def __enter__(self) -> HttpClient:
50
+ return self
51
+
52
+ def __exit__(self, *exc: object) -> None:
53
+ self.close()
54
+
55
+ def _raise_for_status(self, response: httpx.Response) -> None:
56
+ if response.status_code >= 400:
57
+ body = response.text
58
+ logger.error(
59
+ "HTTP %s on %s %s: %s",
60
+ response.status_code,
61
+ response.request.method,
62
+ response.request.url,
63
+ body,
64
+ )
65
+ raise error_for_status(response.status_code, body)
66
+
67
+ def get_json(self, path: str) -> Any:
68
+ try:
69
+ response = self._client.get(path, headers={"Content-Type": "application/json"})
70
+ except httpx.HTTPError as e:
71
+ raise ContinuumError(f"Request failed: {e}") from e
72
+ self._raise_for_status(response)
73
+ if response.status_code == 204 or not response.content:
74
+ return None
75
+ return response.json()
76
+
77
+ def get_bytes(self, path: str) -> tuple[bytes, str] | None:
78
+ """Returns (data, content_type) tuple, or None on 204."""
79
+ try:
80
+ response = self._client.get(path)
81
+ except httpx.HTTPError as e:
82
+ raise ContinuumError(f"Request failed: {e}") from e
83
+ if response.status_code == 204:
84
+ return None
85
+ self._raise_for_status(response)
86
+ mime = response.headers.get("Content-Type", "application/octet-stream")
87
+ return response.content, mime
88
+
89
+ def post_json(self, path: str, body: Any) -> Any:
90
+ payload = json.dumps(body, default=_json_default) if body is not None else "{}"
91
+ try:
92
+ response = self._client.post(
93
+ path,
94
+ content=payload,
95
+ headers={"Content-Type": "application/json"},
96
+ )
97
+ except httpx.HTTPError as e:
98
+ raise ContinuumError(f"Request failed: {e}") from e
99
+ self._raise_for_status(response)
100
+ if response.status_code == 204 or not response.content:
101
+ return None
102
+ return response.json()
103
+
104
+ def post_optional(self, path: str, body: Any) -> Any | None:
105
+ """POST that may return 204 No Content (e.g. claim)."""
106
+ return self.post_json(path, body)
107
+
108
+ def patch_json(self, path: str, body: Any) -> Any:
109
+ payload = json.dumps(body, default=_json_default) if body is not None else "{}"
110
+ try:
111
+ response = self._client.request(
112
+ "PATCH",
113
+ path,
114
+ content=payload,
115
+ headers={"Content-Type": "application/json"},
116
+ )
117
+ except httpx.HTTPError as e:
118
+ raise ContinuumError(f"Request failed: {e}") from e
119
+ self._raise_for_status(response)
120
+ if response.status_code == 204 or not response.content:
121
+ return None
122
+ return response.json()
123
+
124
+
125
+ def _json_default(obj: Any) -> Any:
126
+ """Fallback JSON encoder for UUIDs, datetimes, enums."""
127
+ import datetime as _dt
128
+ import enum as _enum
129
+ import uuid as _uuid
130
+
131
+ if isinstance(obj, _uuid.UUID):
132
+ return str(obj)
133
+ if isinstance(obj, _dt.datetime):
134
+ return obj.isoformat()
135
+ if isinstance(obj, _enum.Enum):
136
+ return obj.value
137
+ raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable")
138
+
139
+
140
+ def drop_none(d: dict[str, Any]) -> dict[str, Any]:
141
+ """Strip None values from a dict (mirrors @JsonInclude.NON_NULL)."""
142
+ return {k: v for k, v in d.items() if v is not None}
@@ -0,0 +1,137 @@
1
+ """STOMP 1.2 frame codec. Internal."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+
7
+ NULL = "\x00"
8
+ LF = "\n"
9
+
10
+ _ESCAPE = {
11
+ "\\": "\\\\",
12
+ "\r": "\\r",
13
+ "\n": "\\n",
14
+ ":": "\\c",
15
+ }
16
+ _UNESCAPE = {
17
+ "\\\\": "\\",
18
+ "\\r": "\r",
19
+ "\\n": "\n",
20
+ "\\c": ":",
21
+ }
22
+
23
+
24
+ @dataclass
25
+ class StompFrame:
26
+ command: str
27
+ headers: dict[str, str] = field(default_factory=dict)
28
+ body: str = ""
29
+
30
+
31
+ def encode_frame(frame: StompFrame) -> str:
32
+ """Serialize a STOMP frame including the terminating NUL."""
33
+ lines = [frame.command]
34
+ headers = dict(frame.headers)
35
+ body = frame.body or ""
36
+ if body and "content-length" not in {k.lower() for k in headers}:
37
+ headers["content-length"] = str(len(body.encode("utf-8")))
38
+ for key, value in headers.items():
39
+ lines.append(f"{_escape(key)}:{_escape(value)}")
40
+ return LF.join(lines) + LF + LF + body + NULL
41
+
42
+
43
+ def encode_heartbeat() -> str:
44
+ return LF
45
+
46
+
47
+ def _escape(text: str) -> str:
48
+ out: list[str] = []
49
+ for ch in text:
50
+ out.append(_ESCAPE.get(ch, ch))
51
+ return "".join(out)
52
+
53
+
54
+ def _unescape(text: str) -> str:
55
+ out: list[str] = []
56
+ i = 0
57
+ while i < len(text):
58
+ if text[i] == "\\" and i + 1 < len(text):
59
+ pair = text[i : i + 2]
60
+ if pair in _UNESCAPE:
61
+ out.append(_UNESCAPE[pair])
62
+ i += 2
63
+ continue
64
+ out.append(text[i])
65
+ i += 1
66
+ return "".join(out)
67
+
68
+
69
+ class StompDecoder:
70
+ """Incremental decoder for one or more STOMP frames, including heartbeats."""
71
+
72
+ def __init__(self) -> None:
73
+ self._buffer = ""
74
+
75
+ def feed(self, data: str | bytes) -> list[StompFrame | None]:
76
+ """Return complete frames. ``None`` entries are heartbeat newlines."""
77
+ if isinstance(data, bytes):
78
+ chunk = data.decode("utf-8")
79
+ else:
80
+ chunk = data
81
+ self._buffer += chunk
82
+ frames: list[StompFrame | None] = []
83
+ while self._buffer:
84
+ if self._buffer[0] in "\r\n":
85
+ if self._buffer.startswith("\r\n"):
86
+ self._buffer = self._buffer[2:]
87
+ else:
88
+ self._buffer = self._buffer[1:]
89
+ frames.append(None)
90
+ continue
91
+ nul = self._buffer.find(NULL)
92
+ if nul < 0:
93
+ break
94
+ raw = self._buffer[:nul]
95
+ self._buffer = self._buffer[nul + 1 :]
96
+ if raw.startswith("\r\n"):
97
+ raw = raw[2:]
98
+ elif raw.startswith("\n"):
99
+ raw = raw[1:]
100
+ if raw == "":
101
+ frames.append(None)
102
+ continue
103
+ frames.append(_parse_frame(raw))
104
+ return frames
105
+
106
+
107
+ def _parse_frame(raw: str) -> StompFrame:
108
+ header_end = raw.find("\n\n")
109
+ if header_end < 0:
110
+ header_block = raw
111
+ body = ""
112
+ else:
113
+ header_block = raw[:header_end]
114
+ body = raw[header_end + 2 :]
115
+ lines = header_block.split("\n")
116
+ command = lines[0].strip()
117
+ headers: dict[str, str] = {}
118
+ content_length: int | None = None
119
+ for line in lines[1:]:
120
+ if not line:
121
+ continue
122
+ idx = line.find(":")
123
+ if idx < 0:
124
+ continue
125
+ key = _unescape(line[:idx])
126
+ value = _unescape(line[idx + 1 :])
127
+ if key.lower() == "content-length":
128
+ try:
129
+ content_length = int(value)
130
+ except ValueError:
131
+ content_length = None
132
+ if key not in headers:
133
+ headers[key] = value
134
+ if content_length is not None:
135
+ encoded = body.encode("utf-8")
136
+ body = encoded[:content_length].decode("utf-8")
137
+ return StompFrame(command=command, headers=headers, body=body)
@@ -0,0 +1,351 @@
1
+ """ContinuumClient - pythonic mirror of the Java SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import json
7
+ from typing import Any
8
+ from urllib.parse import quote
9
+ from uuid import UUID
10
+
11
+ import httpx
12
+
13
+ from ._http import HttpClient, drop_none
14
+ from .models import Content, QueueItem, TaskItem, TaskItemVersion, TaskStatus, TaskType
15
+
16
+
17
+ def _encode_input(data: Any) -> str | None:
18
+ """Accept dict/list/str/None and return a JSON string (or None)."""
19
+ if data is None:
20
+ return None
21
+ if isinstance(data, str):
22
+ return data
23
+ return json.dumps(data)
24
+
25
+
26
+ class TaskTypesApi:
27
+ _PATH = "/api/management/task-types"
28
+
29
+ def __init__(self, http: HttpClient) -> None:
30
+ self._http = http
31
+
32
+ def list(self) -> list[TaskType]:
33
+ data = self._http.get_json(self._PATH) or []
34
+ return [TaskType.model_validate(item) for item in data]
35
+
36
+ def get_by_id(self, task_type_id: UUID | str) -> TaskType:
37
+ return TaskType.model_validate(self._http.get_json(f"{self._PATH}/{task_type_id}"))
38
+
39
+ def get_by_name(self, name: str) -> TaskType:
40
+ return TaskType.model_validate(
41
+ self._http.get_json(f"{self._PATH}/by-name?name={quote(name)}")
42
+ )
43
+
44
+ def save(
45
+ self,
46
+ *,
47
+ id: UUID | str | None = None,
48
+ name: str | None = None,
49
+ description: str | None = None,
50
+ active_flag: bool | None = None,
51
+ organization_id: UUID | str | None = None,
52
+ is_core: bool | None = None,
53
+ max_duration_sec: int | None = None,
54
+ heartbeat_timeout_sec: int | None = None,
55
+ ) -> TaskType:
56
+ """Create (no id) or update (with id) a task type."""
57
+ body = drop_none(
58
+ {
59
+ "id": str(id) if id else None,
60
+ "name": name,
61
+ "description": description,
62
+ "activeFlag": active_flag,
63
+ "organizationId": str(organization_id) if organization_id else None,
64
+ "isCore": is_core,
65
+ "maxDurationSec": max_duration_sec,
66
+ "heartbeatTimeoutSec": heartbeat_timeout_sec,
67
+ }
68
+ )
69
+ return TaskType.model_validate(self._http.post_json(self._PATH, body))
70
+
71
+ def create(
72
+ self,
73
+ name: str,
74
+ *,
75
+ organization_id: UUID | str | None = None,
76
+ is_core: bool = False,
77
+ **kwargs: Any,
78
+ ) -> TaskType:
79
+ return self.save(
80
+ name=name,
81
+ organization_id=organization_id,
82
+ is_core=is_core,
83
+ **kwargs,
84
+ )
85
+
86
+
87
+ class TaskItemVersionsApi:
88
+ _PATH = "/api/management/task-items"
89
+
90
+ def __init__(self, http: HttpClient) -> None:
91
+ self._http = http
92
+
93
+ def list(self, task_item_id: UUID | str) -> list[TaskItemVersion]:
94
+ data = self._http.get_json(f"{self._PATH}/{task_item_id}/versions") or []
95
+ return [TaskItemVersion.model_validate(item) for item in data]
96
+
97
+ def get_by_id(self, version_id: UUID | str) -> TaskItemVersion:
98
+ return TaskItemVersion.model_validate(
99
+ self._http.get_json(f"{self._PATH}/versions/{version_id}")
100
+ )
101
+
102
+ def create(
103
+ self,
104
+ task_item_id: UUID | str,
105
+ *,
106
+ main_path: str,
107
+ item_definition: str | dict | list | None = None,
108
+ content: bytes | None = None,
109
+ mime_type: str | None = None,
110
+ ) -> TaskItemVersion:
111
+ body: dict[str, Any] = {"mainPath": main_path}
112
+ if item_definition is not None:
113
+ body["itemDefinition"] = (
114
+ item_definition if isinstance(item_definition, str) else json.dumps(item_definition)
115
+ )
116
+ if content is not None:
117
+ body["contentBytes"] = base64.b64encode(content).decode("ascii")
118
+ if mime_type:
119
+ body["mimeType"] = mime_type
120
+ elif mime_type:
121
+ body["mimeType"] = mime_type
122
+
123
+ return TaskItemVersion.model_validate(
124
+ self._http.post_json(f"{self._PATH}/{task_item_id}/versions", body)
125
+ )
126
+
127
+ def update(
128
+ self,
129
+ task_item_id: UUID | str,
130
+ version_id: UUID | str,
131
+ *,
132
+ main_path: str | None = None,
133
+ item_definition: str | dict | list | None = None,
134
+ active_flag: bool | None = None,
135
+ ) -> TaskItemVersion:
136
+ body = drop_none(
137
+ {
138
+ "mainPath": main_path,
139
+ "itemDefinition": (
140
+ item_definition
141
+ if item_definition is None or isinstance(item_definition, str)
142
+ else json.dumps(item_definition)
143
+ ),
144
+ "activeFlag": active_flag,
145
+ }
146
+ )
147
+ return TaskItemVersion.model_validate(
148
+ self._http.post_json(f"{self._PATH}/versions/{version_id}", body)
149
+ )
150
+
151
+ def deactivate(self, task_item_id: UUID | str, version_id: UUID | str) -> TaskItemVersion:
152
+ return self.update(task_item_id, version_id, active_flag=False)
153
+
154
+
155
+ class TaskItemsApi:
156
+ _PATH = "/api/management/task-items"
157
+
158
+ def __init__(self, http: HttpClient) -> None:
159
+ self._http = http
160
+ self.versions = TaskItemVersionsApi(http)
161
+
162
+ def list(self) -> list[TaskItem]:
163
+ data = self._http.get_json(self._PATH) or []
164
+ return [TaskItem.model_validate(item) for item in data]
165
+
166
+ def get_by_id(self, task_item_id: UUID | str) -> TaskItem:
167
+ return TaskItem.model_validate(self._http.get_json(f"{self._PATH}/{task_item_id}"))
168
+
169
+ def get_by_name(self, name: str) -> TaskItem:
170
+ return TaskItem.model_validate(
171
+ self._http.get_json(f"{self._PATH}/by-name?name={quote(name)}")
172
+ )
173
+
174
+ def create(
175
+ self,
176
+ name: str,
177
+ *,
178
+ organization_id: UUID | str | None = None,
179
+ task_type_id: UUID | str | None = None,
180
+ is_core: bool = False,
181
+ ) -> TaskItem:
182
+ body = drop_none(
183
+ {
184
+ "name": name,
185
+ "isCore": is_core,
186
+ "organizationId": str(organization_id) if organization_id else None,
187
+ "taskTypeId": str(task_type_id) if task_type_id else None,
188
+ }
189
+ )
190
+ return TaskItem.model_validate(self._http.post_json(self._PATH, body))
191
+
192
+ def update(
193
+ self,
194
+ task_item_id: UUID | str,
195
+ *,
196
+ name: str | None = None,
197
+ active_flag: bool | None = None,
198
+ task_type_id: UUID | str | None = None,
199
+ ) -> TaskItem:
200
+ body = drop_none(
201
+ {
202
+ "name": name,
203
+ "activeFlag": active_flag,
204
+ "taskTypeId": str(task_type_id) if task_type_id else None,
205
+ }
206
+ )
207
+ return TaskItem.model_validate(self._http.post_json(f"{self._PATH}/{task_item_id}", body))
208
+
209
+ def publish_version(self, task_item_id: UUID | str, version_id: UUID | str) -> TaskItem:
210
+ return TaskItem.model_validate(
211
+ self._http.post_json(
212
+ f"{self._PATH}/{task_item_id}/publish",
213
+ {"versionId": str(version_id)},
214
+ )
215
+ )
216
+
217
+
218
+ class QueueApi:
219
+ _MGMT = "/api/management/queue-items"
220
+ _QUEUE = "/api/queue"
221
+
222
+ def __init__(self, http: HttpClient) -> None:
223
+ self._http = http
224
+
225
+ def add(
226
+ self,
227
+ *,
228
+ task_name: str | None = None,
229
+ task_item_name: str | None = None,
230
+ input_data: Any = None,
231
+ priority: int | None = None,
232
+ parent_id: UUID | str | None = None,
233
+ idempotency_token: str | None = None,
234
+ ) -> QueueItem:
235
+ """Add a queue item. At least one of task_name or task_item_name is required."""
236
+ if task_name is None and task_item_name is None:
237
+ raise ValueError("at least one of task_name or task_item_name is required")
238
+ body = drop_none(
239
+ {
240
+ "parent_id": str(parent_id) if parent_id else None,
241
+ "taskName": task_name,
242
+ "taskItemName": task_item_name,
243
+ "priority": priority,
244
+ "inputData": _encode_input(input_data),
245
+ "idempotencyToken": idempotency_token,
246
+ }
247
+ )
248
+ return QueueItem.model_validate(self._http.post_json(self._MGMT, body))
249
+
250
+ def get_by_id(self, queue_item_id: UUID | str) -> QueueItem:
251
+ return QueueItem.model_validate(self._http.get_json(f"{self._MGMT}/{queue_item_id}"))
252
+
253
+ def get_content(self, queue_item_id: UUID | str) -> Content | None:
254
+ result = self._http.get_bytes(f"{self._QUEUE}/queue-items/{queue_item_id}/content")
255
+ if result is None:
256
+ return None
257
+ data, mime = result
258
+ return Content(data=data, mime_type=mime)
259
+
260
+ def claim(self, task_name: str) -> QueueItem | None:
261
+ """Claim one OPEN queue item for the given task name. Returns None if none available."""
262
+ result = self._http.post_optional(f"{self._QUEUE}/claim", {"taskName": task_name})
263
+ if result is None:
264
+ return None
265
+ return QueueItem.model_validate(result)
266
+
267
+ def heartbeat(self, queue_item_id: UUID | str) -> QueueItem:
268
+ return QueueItem.model_validate(
269
+ self._http.post_json(f"{self._QUEUE}/queue-items/{queue_item_id}/heartbeat", {})
270
+ )
271
+
272
+ def update_status(
273
+ self,
274
+ queue_item_id: UUID | str,
275
+ status: TaskStatus,
276
+ *,
277
+ output_data: Any = None,
278
+ ) -> QueueItem:
279
+ # Server expects `status` on this endpoint (not `taskStatus` on queue item JSON).
280
+ body: dict[str, Any] = {"status": status.value}
281
+ encoded = _encode_input(output_data)
282
+ if encoded is not None:
283
+ body["outputData"] = encoded
284
+ return QueueItem.model_validate(
285
+ self._http.post_json(f"{self._QUEUE}/queue-items/{queue_item_id}/status", body)
286
+ )
287
+
288
+
289
+ class ContentStoreApi:
290
+ _PATH = "/api/management/content-store"
291
+
292
+ def __init__(self, http: HttpClient) -> None:
293
+ self._http = http
294
+
295
+ def get_by_id(self, content_id: UUID | str) -> Content | None:
296
+ result = self._http.get_bytes(f"{self._PATH}/{content_id}")
297
+ if result is None:
298
+ return None
299
+ data, mime = result
300
+ return Content(data=data, mime_type=mime)
301
+
302
+ def get_by_url(self, content_url: str) -> Content | None:
303
+ result = self._http.get_bytes(f"{self._PATH}?url={quote(content_url, safe='')}")
304
+ if result is None:
305
+ return None
306
+ data, mime = result
307
+ return Content(data=data, mime_type=mime)
308
+
309
+
310
+ class ContinuumClient:
311
+ """Client for the Continuum Task Server.
312
+
313
+ Example:
314
+ >>> client = ContinuumClient(base_url="http://localhost:8080", api_key="...")
315
+ >>> task_types = client.task_types.list()
316
+ >>> item = client.queue.claim("echo")
317
+ """
318
+
319
+ def __init__(
320
+ self,
321
+ base_url: str,
322
+ api_key: str,
323
+ *,
324
+ connect_timeout: float = 10.0,
325
+ request_timeout: float = 30.0,
326
+ transport: httpx.BaseTransport | None = None,
327
+ ) -> None:
328
+ self._http = HttpClient(
329
+ base_url=base_url,
330
+ api_key=api_key,
331
+ connect_timeout=connect_timeout,
332
+ request_timeout=request_timeout,
333
+ transport=transport,
334
+ )
335
+ self.task_types = TaskTypesApi(self._http)
336
+ self.task_items = TaskItemsApi(self._http)
337
+ self.queue = QueueApi(self._http)
338
+ self.content_store = ContentStoreApi(self._http)
339
+
340
+ @property
341
+ def base_url(self) -> str:
342
+ return self._http.base_url
343
+
344
+ def close(self) -> None:
345
+ self._http.close()
346
+
347
+ def __enter__(self) -> ContinuumClient:
348
+ return self
349
+
350
+ def __exit__(self, *exc: object) -> None:
351
+ self.close()