orgx 1.0.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.
- orgx-1.0.0/LICENSE +4 -0
- orgx-1.0.0/PKG-INFO +40 -0
- orgx-1.0.0/README.md +22 -0
- orgx-1.0.0/orgx.egg-info/PKG-INFO +40 -0
- orgx-1.0.0/orgx.egg-info/SOURCES.txt +9 -0
- orgx-1.0.0/orgx.egg-info/dependency_links.txt +1 -0
- orgx-1.0.0/orgx.egg-info/top_level.txt +1 -0
- orgx-1.0.0/orgx_client/__init__.py +5 -0
- orgx-1.0.0/orgx_client/client.py +1133 -0
- orgx-1.0.0/pyproject.toml +29 -0
- orgx-1.0.0/setup.cfg +4 -0
orgx-1.0.0/LICENSE
ADDED
orgx-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: orgx
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python client for the OrgX v1 API
|
|
5
|
+
Author-email: OrgX <support@useorgx.com>
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Project-URL: Documentation, https://docs.useorgx.com/docs/api/clients
|
|
8
|
+
Project-URL: Homepage, https://useorgx.com
|
|
9
|
+
Project-URL: Repository, https://github.com/useorgx/orgx-sdk-python
|
|
10
|
+
Project-URL: Issues, https://github.com/useorgx/orgx-sdk-python/issues
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: License :: Other/Proprietary License
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# OrgX Python client
|
|
20
|
+
|
|
21
|
+
Dependency-free client for OrgX REST API v1.
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from orgx_client import OrgXClient
|
|
25
|
+
|
|
26
|
+
orgx = OrgXClient(api_key="oxk_...")
|
|
27
|
+
created = orgx.create_work(
|
|
28
|
+
"Review the launch plan",
|
|
29
|
+
idempotency_key="launch-plan-review-001",
|
|
30
|
+
)
|
|
31
|
+
orgx.complete_work(
|
|
32
|
+
created["taskId"],
|
|
33
|
+
created["task"]["updated_at"],
|
|
34
|
+
created["aggregateVersion"],
|
|
35
|
+
evidence={"reviewed_sections": 12, "broken_links": 0},
|
|
36
|
+
idempotency_key="launch-plan-review-complete-001",
|
|
37
|
+
)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
API reference: https://docs.useorgx.com/docs/api/overview
|
orgx-1.0.0/README.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# OrgX Python client
|
|
2
|
+
|
|
3
|
+
Dependency-free client for OrgX REST API v1.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from orgx_client import OrgXClient
|
|
7
|
+
|
|
8
|
+
orgx = OrgXClient(api_key="oxk_...")
|
|
9
|
+
created = orgx.create_work(
|
|
10
|
+
"Review the launch plan",
|
|
11
|
+
idempotency_key="launch-plan-review-001",
|
|
12
|
+
)
|
|
13
|
+
orgx.complete_work(
|
|
14
|
+
created["taskId"],
|
|
15
|
+
created["task"]["updated_at"],
|
|
16
|
+
created["aggregateVersion"],
|
|
17
|
+
evidence={"reviewed_sections": 12, "broken_links": 0},
|
|
18
|
+
idempotency_key="launch-plan-review-complete-001",
|
|
19
|
+
)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
API reference: https://docs.useorgx.com/docs/api/overview
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: orgx
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python client for the OrgX v1 API
|
|
5
|
+
Author-email: OrgX <support@useorgx.com>
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Project-URL: Documentation, https://docs.useorgx.com/docs/api/clients
|
|
8
|
+
Project-URL: Homepage, https://useorgx.com
|
|
9
|
+
Project-URL: Repository, https://github.com/useorgx/orgx-sdk-python
|
|
10
|
+
Project-URL: Issues, https://github.com/useorgx/orgx-sdk-python/issues
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: License :: Other/Proprietary License
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# OrgX Python client
|
|
20
|
+
|
|
21
|
+
Dependency-free client for OrgX REST API v1.
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from orgx_client import OrgXClient
|
|
25
|
+
|
|
26
|
+
orgx = OrgXClient(api_key="oxk_...")
|
|
27
|
+
created = orgx.create_work(
|
|
28
|
+
"Review the launch plan",
|
|
29
|
+
idempotency_key="launch-plan-review-001",
|
|
30
|
+
)
|
|
31
|
+
orgx.complete_work(
|
|
32
|
+
created["taskId"],
|
|
33
|
+
created["task"]["updated_at"],
|
|
34
|
+
created["aggregateVersion"],
|
|
35
|
+
evidence={"reviewed_sections": 12, "broken_links": 0},
|
|
36
|
+
idempotency_key="launch-plan-review-complete-001",
|
|
37
|
+
)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
API reference: https://docs.useorgx.com/docs/api/overview
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
orgx_client
|
|
@@ -0,0 +1,1133 @@
|
|
|
1
|
+
"""Dependency-free transport for the OrgX v1 OpenAPI contract.
|
|
2
|
+
|
|
3
|
+
The client intentionally owns only HTTP concerns. Authorization, workspace
|
|
4
|
+
scope, idempotency semantics, and lifecycle transitions remain server-owned.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Any, Iterator, Mapping, MutableMapping, Optional
|
|
12
|
+
from urllib.error import HTTPError, URLError
|
|
13
|
+
from urllib.parse import quote
|
|
14
|
+
from urllib.request import Request, urlopen
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class OrgXApiError(RuntimeError):
|
|
18
|
+
"""An HTTP error returned by the OrgX API."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, status: int, code: str, message: str, details: Any = None):
|
|
21
|
+
super().__init__(message)
|
|
22
|
+
self.status = status
|
|
23
|
+
self.code = code
|
|
24
|
+
self.details = details
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class EventStreamSubscription:
|
|
28
|
+
"""Blocking, reconnectable iterator over the v1 SSE ledger transport."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, response: Any):
|
|
31
|
+
self._response = response
|
|
32
|
+
self._closed = False
|
|
33
|
+
|
|
34
|
+
def __iter__(self) -> Iterator[tuple[Mapping[str, Any], str]]:
|
|
35
|
+
event_name = "message"
|
|
36
|
+
event_id = ""
|
|
37
|
+
data_lines: list[str] = []
|
|
38
|
+
for raw_line in self._response:
|
|
39
|
+
if self._closed:
|
|
40
|
+
break
|
|
41
|
+
line = raw_line.decode("utf-8").rstrip("\r\n")
|
|
42
|
+
if line == "":
|
|
43
|
+
if data_lines:
|
|
44
|
+
data = "\n".join(data_lines)
|
|
45
|
+
if event_name == "ledger_event":
|
|
46
|
+
yield json.loads(data), event_id
|
|
47
|
+
elif event_name == "error":
|
|
48
|
+
payload = json.loads(data)
|
|
49
|
+
raise OrgXApiError(
|
|
50
|
+
503,
|
|
51
|
+
payload.get("code", "event_stream_unavailable"),
|
|
52
|
+
payload.get(
|
|
53
|
+
"message", "OrgX event stream unavailable"
|
|
54
|
+
),
|
|
55
|
+
)
|
|
56
|
+
event_name = "message"
|
|
57
|
+
event_id = ""
|
|
58
|
+
data_lines = []
|
|
59
|
+
continue
|
|
60
|
+
if line.startswith(":"):
|
|
61
|
+
continue
|
|
62
|
+
field, separator, value = line.partition(":")
|
|
63
|
+
if separator and value.startswith(" "):
|
|
64
|
+
value = value[1:]
|
|
65
|
+
if field == "event":
|
|
66
|
+
event_name = value
|
|
67
|
+
elif field == "id":
|
|
68
|
+
event_id = value
|
|
69
|
+
elif field == "data":
|
|
70
|
+
data_lines.append(value)
|
|
71
|
+
|
|
72
|
+
def close(self) -> None:
|
|
73
|
+
self._closed = True
|
|
74
|
+
self._response.close()
|
|
75
|
+
|
|
76
|
+
def __enter__(self) -> "EventStreamSubscription":
|
|
77
|
+
return self
|
|
78
|
+
|
|
79
|
+
def __exit__(self, *_: object) -> None:
|
|
80
|
+
self.close()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass(frozen=True)
|
|
84
|
+
class OrgXClient:
|
|
85
|
+
"""Synchronous OrgX v1 client using Python's standard library only."""
|
|
86
|
+
|
|
87
|
+
api_key: Optional[str] = None
|
|
88
|
+
token: Optional[str] = None
|
|
89
|
+
base_url: str = "https://useorgx.com/api/v1"
|
|
90
|
+
timeout_seconds: float = 30.0
|
|
91
|
+
|
|
92
|
+
def start_discovery_run(
|
|
93
|
+
self,
|
|
94
|
+
workspace_id: str,
|
|
95
|
+
*,
|
|
96
|
+
idempotency_key: str,
|
|
97
|
+
mode: str = "bounded_sync",
|
|
98
|
+
query: Optional[str] = None,
|
|
99
|
+
source_kinds: Optional[list[str]] = None,
|
|
100
|
+
) -> Mapping[str, Any]:
|
|
101
|
+
return self._request(
|
|
102
|
+
"/discovery-runs",
|
|
103
|
+
method="POST",
|
|
104
|
+
idempotency_key=idempotency_key,
|
|
105
|
+
body={
|
|
106
|
+
"workspace_id": workspace_id,
|
|
107
|
+
"mode": mode,
|
|
108
|
+
"query": query,
|
|
109
|
+
"source_kinds": source_kinds or [],
|
|
110
|
+
},
|
|
111
|
+
)["data"]
|
|
112
|
+
|
|
113
|
+
def list_discovery_runs(self, workspace_id: str, *, limit: int = 20) -> list[Mapping[str, Any]]:
|
|
114
|
+
payload = self._request(
|
|
115
|
+
f"/discovery-runs?workspace_id={quote(workspace_id)}&limit={limit}"
|
|
116
|
+
)
|
|
117
|
+
return list(payload["data"])
|
|
118
|
+
|
|
119
|
+
def get_discovery_run(self, workspace_id: str, run_id: str) -> Mapping[str, Any]:
|
|
120
|
+
payload = self._request(
|
|
121
|
+
f"/discovery-runs/{quote(run_id)}?workspace_id={quote(workspace_id)}"
|
|
122
|
+
)
|
|
123
|
+
return payload["data"]
|
|
124
|
+
|
|
125
|
+
def propose_process_from_discovery(
|
|
126
|
+
self,
|
|
127
|
+
workspace_id: str,
|
|
128
|
+
discovery_run_id: str,
|
|
129
|
+
process_candidate_id: str,
|
|
130
|
+
*,
|
|
131
|
+
idempotency_key: str,
|
|
132
|
+
) -> Mapping[str, Any]:
|
|
133
|
+
return self._request(
|
|
134
|
+
f"/discovery-runs/{quote(discovery_run_id)}/propose",
|
|
135
|
+
method="POST",
|
|
136
|
+
idempotency_key=idempotency_key,
|
|
137
|
+
body={
|
|
138
|
+
"workspace_id": workspace_id,
|
|
139
|
+
"process_candidate_id": process_candidate_id,
|
|
140
|
+
},
|
|
141
|
+
)["data"]
|
|
142
|
+
|
|
143
|
+
def list_operating_processes(self, workspace_id: str) -> list[Mapping[str, Any]]:
|
|
144
|
+
payload = self._request(
|
|
145
|
+
f"/operating-processes?workspace_id={quote(workspace_id)}"
|
|
146
|
+
)
|
|
147
|
+
return list(payload["data"])
|
|
148
|
+
|
|
149
|
+
def list_episodes(self, workspace_id: str, *, limit: int = 50) -> list[Mapping[str, Any]]:
|
|
150
|
+
payload = self._request(
|
|
151
|
+
f"/episodes?workspace_id={quote(workspace_id)}&limit={limit}"
|
|
152
|
+
)
|
|
153
|
+
return list(payload["data"])
|
|
154
|
+
|
|
155
|
+
def list_events(
|
|
156
|
+
self,
|
|
157
|
+
workspace_id: str,
|
|
158
|
+
*,
|
|
159
|
+
cursor: Optional[str] = None,
|
|
160
|
+
limit: Optional[int] = None,
|
|
161
|
+
event_types: Optional[list[str]] = None,
|
|
162
|
+
aggregate_type: Optional[str] = None,
|
|
163
|
+
) -> Mapping[str, Any]:
|
|
164
|
+
params = [f"workspace_id={quote(workspace_id)}"]
|
|
165
|
+
if cursor:
|
|
166
|
+
params.append(f"cursor={quote(cursor)}")
|
|
167
|
+
if limit is not None:
|
|
168
|
+
params.append(f"limit={quote(str(limit))}")
|
|
169
|
+
for event_type in event_types or []:
|
|
170
|
+
params.append(f"event_type={quote(event_type)}")
|
|
171
|
+
if aggregate_type:
|
|
172
|
+
params.append(f"aggregate_type={quote(aggregate_type)}")
|
|
173
|
+
return self._request(f"/events/stream?{'&'.join(params)}")
|
|
174
|
+
|
|
175
|
+
def subscribe_events(
|
|
176
|
+
self,
|
|
177
|
+
workspace_id: str,
|
|
178
|
+
*,
|
|
179
|
+
after: Optional[str] = None,
|
|
180
|
+
limit: Optional[int] = None,
|
|
181
|
+
event_types: Optional[list[str]] = None,
|
|
182
|
+
aggregate_type: Optional[str] = None,
|
|
183
|
+
stream_ms: Optional[int] = None,
|
|
184
|
+
poll_ms: Optional[int] = None,
|
|
185
|
+
) -> EventStreamSubscription:
|
|
186
|
+
"""Open a bounded SSE lease and iterate ``(event, opaque_cursor)`` pairs."""
|
|
187
|
+
|
|
188
|
+
params = [
|
|
189
|
+
f"workspace_id={quote(workspace_id)}",
|
|
190
|
+
"transport=sse",
|
|
191
|
+
]
|
|
192
|
+
if after:
|
|
193
|
+
params.append(f"after={quote(after)}")
|
|
194
|
+
if limit is not None:
|
|
195
|
+
params.append(f"limit={quote(str(limit))}")
|
|
196
|
+
if stream_ms is not None:
|
|
197
|
+
params.append(f"stream_ms={quote(str(stream_ms))}")
|
|
198
|
+
if poll_ms is not None:
|
|
199
|
+
params.append(f"poll_ms={quote(str(poll_ms))}")
|
|
200
|
+
if aggregate_type:
|
|
201
|
+
params.append(f"aggregate_type={quote(aggregate_type)}")
|
|
202
|
+
for event_type in event_types or []:
|
|
203
|
+
params.append(f"event_type={quote(event_type)}")
|
|
204
|
+
|
|
205
|
+
headers = {"Accept": "text/event-stream"}
|
|
206
|
+
credential = self.api_key or self.token
|
|
207
|
+
if credential:
|
|
208
|
+
headers["Authorization"] = f"Bearer {credential}"
|
|
209
|
+
request = Request(
|
|
210
|
+
f"{self.base_url.rstrip('/')}/events/stream?{'&'.join(params)}",
|
|
211
|
+
headers=headers,
|
|
212
|
+
method="GET",
|
|
213
|
+
)
|
|
214
|
+
try:
|
|
215
|
+
response = urlopen(request, timeout=self.timeout_seconds)
|
|
216
|
+
except HTTPError as error:
|
|
217
|
+
try:
|
|
218
|
+
error_payload = json.loads(error.read().decode("utf-8"))
|
|
219
|
+
except (OSError, ValueError):
|
|
220
|
+
error_payload = {}
|
|
221
|
+
detail = (
|
|
222
|
+
error_payload.get("error", {})
|
|
223
|
+
if isinstance(error_payload, dict)
|
|
224
|
+
else {}
|
|
225
|
+
)
|
|
226
|
+
raise OrgXApiError(
|
|
227
|
+
error.code,
|
|
228
|
+
detail.get("code", "event_stream_failed"),
|
|
229
|
+
detail.get("message", "OrgX event stream failed"),
|
|
230
|
+
detail.get("details"),
|
|
231
|
+
) from error
|
|
232
|
+
except URLError as error:
|
|
233
|
+
raise OrgXApiError(0, "transport_failed", str(error.reason)) from error
|
|
234
|
+
return EventStreamSubscription(response)
|
|
235
|
+
|
|
236
|
+
def get_work_ledger(
|
|
237
|
+
self,
|
|
238
|
+
workspace_id: str,
|
|
239
|
+
*,
|
|
240
|
+
from_iso: Optional[str] = None,
|
|
241
|
+
to_iso: Optional[str] = None,
|
|
242
|
+
granularity: Optional[str] = None,
|
|
243
|
+
timezone: Optional[str] = None,
|
|
244
|
+
include_source_health: Optional[bool] = None,
|
|
245
|
+
) -> MutableMapping[str, Any]:
|
|
246
|
+
params = [f"workspace_id={quote(workspace_id)}"]
|
|
247
|
+
if from_iso:
|
|
248
|
+
params.append(f"from={quote(from_iso)}")
|
|
249
|
+
if to_iso:
|
|
250
|
+
params.append(f"to={quote(to_iso)}")
|
|
251
|
+
if granularity:
|
|
252
|
+
params.append(f"granularity={quote(granularity)}")
|
|
253
|
+
if timezone:
|
|
254
|
+
params.append(f"timezone={quote(timezone)}")
|
|
255
|
+
if include_source_health is False:
|
|
256
|
+
params.append("include_source_health=false")
|
|
257
|
+
return self._request(f"/projections/work-ledger?{'&'.join(params)}")
|
|
258
|
+
|
|
259
|
+
def get_adoption_projection(
|
|
260
|
+
self, workspace_id: str, process_id: str
|
|
261
|
+
) -> MutableMapping[str, Any]:
|
|
262
|
+
"""Read evidence-gated OperatingProcess adoption metrics.
|
|
263
|
+
|
|
264
|
+
Behavioral and self-reported adoption are returned separately. The
|
|
265
|
+
server returns explicit limitations when source metrics are absent;
|
|
266
|
+
bound episode count is provenance context, not an adoption denominator.
|
|
267
|
+
"""
|
|
268
|
+
params = [
|
|
269
|
+
f"workspace_id={quote(workspace_id)}",
|
|
270
|
+
f"process_id={quote(process_id)}",
|
|
271
|
+
]
|
|
272
|
+
return self._request(f"/projections/adoption?{'&'.join(params)}")
|
|
273
|
+
|
|
274
|
+
def get_value_case_projection(
|
|
275
|
+
self, workspace_id: str, process_id: str
|
|
276
|
+
) -> MutableMapping[str, Any]:
|
|
277
|
+
"""Read an evidence-gated process ValueCase projection."""
|
|
278
|
+
params = [
|
|
279
|
+
f"workspace_id={quote(workspace_id)}",
|
|
280
|
+
f"process_id={quote(process_id)}",
|
|
281
|
+
]
|
|
282
|
+
return self._request(f"/projections/value-case?{'&'.join(params)}")
|
|
283
|
+
|
|
284
|
+
def get_meter_usage_projection(
|
|
285
|
+
self,
|
|
286
|
+
workspace_id: str,
|
|
287
|
+
*,
|
|
288
|
+
from_iso: Optional[str] = None,
|
|
289
|
+
to_iso: Optional[str] = None,
|
|
290
|
+
) -> MutableMapping[str, Any]:
|
|
291
|
+
"""Read immutable meter-event usage evidence.
|
|
292
|
+
|
|
293
|
+
The response is explicitly non-billing-ready until verified provider
|
|
294
|
+
ingress, rating, entitlement, invoice, and double-billing controls are
|
|
295
|
+
proven in the deployment environment.
|
|
296
|
+
"""
|
|
297
|
+
params = [f"workspace_id={quote(workspace_id)}"]
|
|
298
|
+
if from_iso:
|
|
299
|
+
params.append(f"from={quote(from_iso)}")
|
|
300
|
+
if to_iso:
|
|
301
|
+
params.append(f"to={quote(to_iso)}")
|
|
302
|
+
return self._request(f"/projections/meter-usage?{'&'.join(params)}")
|
|
303
|
+
|
|
304
|
+
def list_handoffs(self, workspace_id: str) -> list[Mapping[str, Any]]:
|
|
305
|
+
payload = self._request(f"/handoffs?workspace_id={quote(workspace_id)}")
|
|
306
|
+
return list(payload["data"])
|
|
307
|
+
|
|
308
|
+
def create_work(
|
|
309
|
+
self,
|
|
310
|
+
title: str,
|
|
311
|
+
*,
|
|
312
|
+
idempotency_key: str,
|
|
313
|
+
workspace_id: Optional[str] = None,
|
|
314
|
+
command_id: Optional[str] = None,
|
|
315
|
+
initiative_id: Optional[str] = None,
|
|
316
|
+
workstream_id: Optional[str] = None,
|
|
317
|
+
milestone_id: Optional[str] = None,
|
|
318
|
+
description: Optional[str] = None,
|
|
319
|
+
priority: str = "medium",
|
|
320
|
+
due_date: Optional[str] = None,
|
|
321
|
+
metadata: Optional[Mapping[str, Any]] = None,
|
|
322
|
+
estimated_cost_cents: int = 0,
|
|
323
|
+
causation_id: Optional[str] = None,
|
|
324
|
+
correlation_id: Optional[str] = None,
|
|
325
|
+
) -> Mapping[str, Any]:
|
|
326
|
+
body: dict[str, Any] = {
|
|
327
|
+
"title": title,
|
|
328
|
+
"description": description,
|
|
329
|
+
"priority": priority,
|
|
330
|
+
"due_date": due_date,
|
|
331
|
+
"metadata": dict(metadata or {}),
|
|
332
|
+
"estimated_cost_cents": estimated_cost_cents,
|
|
333
|
+
"causation_id": causation_id,
|
|
334
|
+
"correlation_id": correlation_id,
|
|
335
|
+
}
|
|
336
|
+
if workspace_id:
|
|
337
|
+
body["workspace_id"] = workspace_id
|
|
338
|
+
if command_id:
|
|
339
|
+
body["command_id"] = command_id
|
|
340
|
+
hierarchy = (initiative_id, workstream_id, milestone_id)
|
|
341
|
+
if any(hierarchy) and not all(hierarchy):
|
|
342
|
+
raise ValueError(
|
|
343
|
+
"initiative_id, workstream_id, and milestone_id must be supplied together"
|
|
344
|
+
)
|
|
345
|
+
if all(hierarchy):
|
|
346
|
+
body.update(
|
|
347
|
+
initiative_id=initiative_id,
|
|
348
|
+
workstream_id=workstream_id,
|
|
349
|
+
milestone_id=milestone_id,
|
|
350
|
+
)
|
|
351
|
+
return self._request(
|
|
352
|
+
"/work",
|
|
353
|
+
method="POST",
|
|
354
|
+
idempotency_key=idempotency_key,
|
|
355
|
+
body=body,
|
|
356
|
+
)["data"]
|
|
357
|
+
|
|
358
|
+
def complete_work(
|
|
359
|
+
self,
|
|
360
|
+
task_id: str,
|
|
361
|
+
expected_updated_at: str,
|
|
362
|
+
expected_aggregate_version: int,
|
|
363
|
+
*,
|
|
364
|
+
idempotency_key: str,
|
|
365
|
+
summary: Optional[str] = None,
|
|
366
|
+
evidence: Optional[Mapping[str, Any]] = None,
|
|
367
|
+
cost_cents: int = 0,
|
|
368
|
+
causation_id: Optional[str] = None,
|
|
369
|
+
correlation_id: Optional[str] = None,
|
|
370
|
+
) -> Mapping[str, Any]:
|
|
371
|
+
return self._request(
|
|
372
|
+
f"/work/{quote(task_id)}/complete",
|
|
373
|
+
method="POST",
|
|
374
|
+
idempotency_key=idempotency_key,
|
|
375
|
+
body={
|
|
376
|
+
"expected_updated_at": expected_updated_at,
|
|
377
|
+
"expected_aggregate_version": expected_aggregate_version,
|
|
378
|
+
"summary": summary,
|
|
379
|
+
"evidence": dict(evidence or {}),
|
|
380
|
+
"cost_cents": cost_cents,
|
|
381
|
+
"causation_id": causation_id,
|
|
382
|
+
"correlation_id": correlation_id,
|
|
383
|
+
},
|
|
384
|
+
)["data"]
|
|
385
|
+
|
|
386
|
+
def get_handoff(self, workspace_id: str, handoff_id: str) -> Mapping[str, Any]:
|
|
387
|
+
payload = self._request(
|
|
388
|
+
f"/handoffs/{quote(handoff_id)}?workspace_id={quote(workspace_id)}"
|
|
389
|
+
)
|
|
390
|
+
return payload["data"]
|
|
391
|
+
|
|
392
|
+
def create_handoff(
|
|
393
|
+
self,
|
|
394
|
+
workspace_id: str,
|
|
395
|
+
handoff_key: str,
|
|
396
|
+
from_stage_key: str,
|
|
397
|
+
to_stage_key: str,
|
|
398
|
+
title: str,
|
|
399
|
+
*,
|
|
400
|
+
idempotency_key: str,
|
|
401
|
+
handoff_id: Optional[str] = None,
|
|
402
|
+
source_process_ref: Optional[str] = None,
|
|
403
|
+
source_revision_ref: Optional[str] = None,
|
|
404
|
+
summary: Optional[str] = None,
|
|
405
|
+
priority: str = "normal",
|
|
406
|
+
sla_minutes: Optional[int] = None,
|
|
407
|
+
due_at: Optional[str] = None,
|
|
408
|
+
proof_requirements: Optional[list[Mapping[str, Any]]] = None,
|
|
409
|
+
) -> Mapping[str, Any]:
|
|
410
|
+
return self._request(
|
|
411
|
+
"/handoffs",
|
|
412
|
+
method="POST",
|
|
413
|
+
idempotency_key=idempotency_key,
|
|
414
|
+
body={
|
|
415
|
+
"workspace_id": workspace_id,
|
|
416
|
+
"handoff_id": handoff_id,
|
|
417
|
+
"handoff_key": handoff_key,
|
|
418
|
+
"from_stage_key": from_stage_key,
|
|
419
|
+
"to_stage_key": to_stage_key,
|
|
420
|
+
"source_process_ref": source_process_ref,
|
|
421
|
+
"source_revision_ref": source_revision_ref,
|
|
422
|
+
"title": title,
|
|
423
|
+
"summary": summary,
|
|
424
|
+
"priority": priority,
|
|
425
|
+
"sla_minutes": sla_minutes,
|
|
426
|
+
"due_at": due_at,
|
|
427
|
+
"proof_requirements": proof_requirements or [],
|
|
428
|
+
},
|
|
429
|
+
)["data"]
|
|
430
|
+
|
|
431
|
+
def claim_handoff(
|
|
432
|
+
self,
|
|
433
|
+
workspace_id: str,
|
|
434
|
+
handoff_id: str,
|
|
435
|
+
expected_aggregate_version: int,
|
|
436
|
+
*,
|
|
437
|
+
idempotency_key: str,
|
|
438
|
+
) -> Mapping[str, Any]:
|
|
439
|
+
return self._request(
|
|
440
|
+
f"/handoffs/{quote(handoff_id)}/claim",
|
|
441
|
+
method="POST",
|
|
442
|
+
idempotency_key=idempotency_key,
|
|
443
|
+
body={
|
|
444
|
+
"workspace_id": workspace_id,
|
|
445
|
+
"expected_aggregate_version": expected_aggregate_version,
|
|
446
|
+
},
|
|
447
|
+
)["data"]
|
|
448
|
+
|
|
449
|
+
def fulfill_handoff(
|
|
450
|
+
self,
|
|
451
|
+
workspace_id: str,
|
|
452
|
+
handoff_id: str,
|
|
453
|
+
expected_aggregate_version: int,
|
|
454
|
+
result: Mapping[str, Any],
|
|
455
|
+
*,
|
|
456
|
+
idempotency_key: str,
|
|
457
|
+
) -> Mapping[str, Any]:
|
|
458
|
+
return self._request(
|
|
459
|
+
f"/handoffs/{quote(handoff_id)}/fulfill",
|
|
460
|
+
method="POST",
|
|
461
|
+
idempotency_key=idempotency_key,
|
|
462
|
+
body={
|
|
463
|
+
"workspace_id": workspace_id,
|
|
464
|
+
"expected_aggregate_version": expected_aggregate_version,
|
|
465
|
+
"result": dict(result),
|
|
466
|
+
},
|
|
467
|
+
)["data"]
|
|
468
|
+
|
|
469
|
+
def propose_operating_process(
|
|
470
|
+
self,
|
|
471
|
+
workspace_id: str,
|
|
472
|
+
process: Mapping[str, Any],
|
|
473
|
+
revision: Mapping[str, Any],
|
|
474
|
+
*,
|
|
475
|
+
idempotency_key: str,
|
|
476
|
+
) -> Mapping[str, Any]:
|
|
477
|
+
return self._request(
|
|
478
|
+
"/operating-processes",
|
|
479
|
+
method="POST",
|
|
480
|
+
idempotency_key=idempotency_key,
|
|
481
|
+
body={
|
|
482
|
+
"workspace_id": workspace_id,
|
|
483
|
+
"process": dict(process),
|
|
484
|
+
"revision": dict(revision),
|
|
485
|
+
},
|
|
486
|
+
)["data"]
|
|
487
|
+
|
|
488
|
+
def confirm_operating_process(
|
|
489
|
+
self,
|
|
490
|
+
workspace_id: str,
|
|
491
|
+
process_id: str,
|
|
492
|
+
expected_aggregate_version: int,
|
|
493
|
+
*,
|
|
494
|
+
idempotency_key: str,
|
|
495
|
+
) -> Mapping[str, Any]:
|
|
496
|
+
return self._transition(
|
|
497
|
+
"confirm", workspace_id, process_id, expected_aggregate_version, idempotency_key
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
def activate_operating_process(
|
|
501
|
+
self,
|
|
502
|
+
workspace_id: str,
|
|
503
|
+
process_id: str,
|
|
504
|
+
expected_aggregate_version: int,
|
|
505
|
+
*,
|
|
506
|
+
idempotency_key: str,
|
|
507
|
+
) -> Mapping[str, Any]:
|
|
508
|
+
return self._transition(
|
|
509
|
+
"activate", workspace_id, process_id, expected_aggregate_version, idempotency_key
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
def get_operating_process(
|
|
513
|
+
self, workspace_id: str, process_id: str
|
|
514
|
+
) -> Mapping[str, Any]:
|
|
515
|
+
payload = self._request(
|
|
516
|
+
f"/operating-processes/{quote(process_id)}?workspace_id={quote(workspace_id)}"
|
|
517
|
+
)
|
|
518
|
+
return payload["data"]
|
|
519
|
+
|
|
520
|
+
def get_operating_map(
|
|
521
|
+
self, workspace_id: str, *, limit: Optional[int] = None
|
|
522
|
+
) -> MutableMapping[str, Any]:
|
|
523
|
+
params = [f"workspace_id={quote(workspace_id)}"]
|
|
524
|
+
if limit is not None:
|
|
525
|
+
params.append(f"limit={quote(str(limit))}")
|
|
526
|
+
return self._request(f"/operating-map?{'&'.join(params)}")
|
|
527
|
+
|
|
528
|
+
def return_handoff(
|
|
529
|
+
self,
|
|
530
|
+
workspace_id: str,
|
|
531
|
+
handoff_id: str,
|
|
532
|
+
expected_aggregate_version: int,
|
|
533
|
+
*,
|
|
534
|
+
idempotency_key: str,
|
|
535
|
+
) -> Mapping[str, Any]:
|
|
536
|
+
return self._handoff_transition(
|
|
537
|
+
"return", workspace_id, handoff_id, expected_aggregate_version, idempotency_key
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
def escalate_handoff(
|
|
541
|
+
self,
|
|
542
|
+
workspace_id: str,
|
|
543
|
+
handoff_id: str,
|
|
544
|
+
expected_aggregate_version: int,
|
|
545
|
+
*,
|
|
546
|
+
idempotency_key: str,
|
|
547
|
+
) -> Mapping[str, Any]:
|
|
548
|
+
return self._handoff_transition(
|
|
549
|
+
"escalate", workspace_id, handoff_id, expected_aggregate_version, idempotency_key
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
def cancel_handoff(
|
|
553
|
+
self,
|
|
554
|
+
workspace_id: str,
|
|
555
|
+
handoff_id: str,
|
|
556
|
+
expected_aggregate_version: int,
|
|
557
|
+
*,
|
|
558
|
+
idempotency_key: str,
|
|
559
|
+
) -> Mapping[str, Any]:
|
|
560
|
+
return self._handoff_transition(
|
|
561
|
+
"cancel", workspace_id, handoff_id, expected_aggregate_version, idempotency_key
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
def list_work(
|
|
565
|
+
self,
|
|
566
|
+
workspace_id: str,
|
|
567
|
+
*,
|
|
568
|
+
initiative_id: Optional[str] = None,
|
|
569
|
+
status: Optional[str] = None,
|
|
570
|
+
updated_since: Optional[str] = None,
|
|
571
|
+
cursor: Optional[str] = None,
|
|
572
|
+
limit: Optional[int] = None,
|
|
573
|
+
) -> MutableMapping[str, Any]:
|
|
574
|
+
"""List owned work items; pass ``meta["nextCursor"]`` back as ``cursor``."""
|
|
575
|
+
params = [f"workspace_id={quote(workspace_id)}"]
|
|
576
|
+
if initiative_id:
|
|
577
|
+
params.append(f"initiative_id={quote(initiative_id)}")
|
|
578
|
+
if status:
|
|
579
|
+
params.append(f"status={quote(status)}")
|
|
580
|
+
if updated_since:
|
|
581
|
+
params.append(f"updated_since={quote(updated_since)}")
|
|
582
|
+
if cursor:
|
|
583
|
+
params.append(f"cursor={quote(cursor)}")
|
|
584
|
+
if limit is not None:
|
|
585
|
+
params.append(f"limit={quote(str(limit))}")
|
|
586
|
+
return self._request(f"/work?{'&'.join(params)}")
|
|
587
|
+
|
|
588
|
+
def get_work_task(self, workspace_id: str, task_id: str) -> Mapping[str, Any]:
|
|
589
|
+
"""Read one work item with the ``concurrency`` block completion requires.
|
|
590
|
+
|
|
591
|
+
Echo ``concurrency.expected_updated_at`` back to ``complete_work``
|
|
592
|
+
exactly as received; the server compares it for exact equality.
|
|
593
|
+
"""
|
|
594
|
+
payload = self._request(
|
|
595
|
+
f"/work/{quote(task_id)}?workspace_id={quote(workspace_id)}"
|
|
596
|
+
)
|
|
597
|
+
return payload["data"]
|
|
598
|
+
|
|
599
|
+
def create_initiative(
|
|
600
|
+
self,
|
|
601
|
+
*,
|
|
602
|
+
idempotency_key: str,
|
|
603
|
+
workspace_id: Optional[str] = None,
|
|
604
|
+
title: Optional[str] = None,
|
|
605
|
+
summary: Optional[str] = None,
|
|
606
|
+
plan: Optional[Mapping[str, Any]] = None,
|
|
607
|
+
plan_digest: Optional[str] = None,
|
|
608
|
+
proposal_id: Optional[str] = None,
|
|
609
|
+
proposal_digest: Optional[str] = None,
|
|
610
|
+
initiative_id: Optional[str] = None,
|
|
611
|
+
overrides: Optional[Mapping[str, Any]] = None,
|
|
612
|
+
expected_aggregate_version: Optional[int] = None,
|
|
613
|
+
) -> Mapping[str, Any]:
|
|
614
|
+
"""Create an initiative in one of the three contract forms.
|
|
615
|
+
|
|
616
|
+
Supply ``proposal_id`` + ``proposal_digest`` to commit a reviewed
|
|
617
|
+
proposal, ``plan`` + ``plan_digest`` to commit an inline plan, or
|
|
618
|
+
``title`` (with optional ``summary``) for the starter scaffold.
|
|
619
|
+
"""
|
|
620
|
+
body: dict[str, Any] = {}
|
|
621
|
+
if workspace_id:
|
|
622
|
+
body["workspace_id"] = workspace_id
|
|
623
|
+
if proposal_id or proposal_digest:
|
|
624
|
+
if not (proposal_id and proposal_digest):
|
|
625
|
+
raise ValueError(
|
|
626
|
+
"proposal_id and proposal_digest must be supplied together"
|
|
627
|
+
)
|
|
628
|
+
body["proposal_id"] = proposal_id
|
|
629
|
+
body["proposal_digest"] = proposal_digest
|
|
630
|
+
elif plan is not None or plan_digest:
|
|
631
|
+
if plan is None or not plan_digest:
|
|
632
|
+
raise ValueError("plan and plan_digest must be supplied together")
|
|
633
|
+
body["plan"] = dict(plan)
|
|
634
|
+
body["plan_digest"] = plan_digest
|
|
635
|
+
elif title:
|
|
636
|
+
body["title"] = title
|
|
637
|
+
if summary is not None:
|
|
638
|
+
body["summary"] = summary
|
|
639
|
+
else:
|
|
640
|
+
raise ValueError(
|
|
641
|
+
"supply proposal_id+proposal_digest, plan+plan_digest, or title"
|
|
642
|
+
)
|
|
643
|
+
if "title" not in body:
|
|
644
|
+
if initiative_id:
|
|
645
|
+
body["initiative_id"] = initiative_id
|
|
646
|
+
if overrides is not None:
|
|
647
|
+
body["overrides"] = dict(overrides)
|
|
648
|
+
if expected_aggregate_version is not None:
|
|
649
|
+
body["expected_aggregate_version"] = expected_aggregate_version
|
|
650
|
+
return self._request(
|
|
651
|
+
"/initiatives",
|
|
652
|
+
method="POST",
|
|
653
|
+
idempotency_key=idempotency_key,
|
|
654
|
+
body=body,
|
|
655
|
+
)["data"]
|
|
656
|
+
|
|
657
|
+
def get_initiative(
|
|
658
|
+
self,
|
|
659
|
+
initiative_id: str,
|
|
660
|
+
*,
|
|
661
|
+
workspace_id: Optional[str] = None,
|
|
662
|
+
include: Optional[str] = None,
|
|
663
|
+
) -> Mapping[str, Any]:
|
|
664
|
+
"""Read an initiative; ``include`` is comma-separated (``tree,launches``)."""
|
|
665
|
+
params = []
|
|
666
|
+
if workspace_id:
|
|
667
|
+
params.append(f"workspace_id={quote(workspace_id)}")
|
|
668
|
+
if include:
|
|
669
|
+
params.append(f"include={quote(include)}")
|
|
670
|
+
suffix = f"?{'&'.join(params)}" if params else ""
|
|
671
|
+
payload = self._request(f"/initiatives/{quote(initiative_id)}{suffix}")
|
|
672
|
+
return payload["data"]
|
|
673
|
+
|
|
674
|
+
def propose_initiative_scaffold(
|
|
675
|
+
self,
|
|
676
|
+
title: str,
|
|
677
|
+
*,
|
|
678
|
+
idempotency_key: str,
|
|
679
|
+
workspace_id: Optional[str] = None,
|
|
680
|
+
summary: Optional[str] = None,
|
|
681
|
+
prompt: Optional[str] = None,
|
|
682
|
+
goal_ids: Optional[list[str]] = None,
|
|
683
|
+
context: Optional[Mapping[str, Any]] = None,
|
|
684
|
+
depth: Optional[str] = None,
|
|
685
|
+
workstreams: Optional[list[Mapping[str, Any]]] = None,
|
|
686
|
+
agent_assignment: Optional[str] = None,
|
|
687
|
+
) -> Mapping[str, Any]:
|
|
688
|
+
body: dict[str, Any] = {"title": title}
|
|
689
|
+
if workspace_id:
|
|
690
|
+
body["workspace_id"] = workspace_id
|
|
691
|
+
if summary is not None:
|
|
692
|
+
body["summary"] = summary
|
|
693
|
+
if prompt is not None:
|
|
694
|
+
body["prompt"] = prompt
|
|
695
|
+
if goal_ids is not None:
|
|
696
|
+
body["goal_ids"] = list(goal_ids)
|
|
697
|
+
if context is not None:
|
|
698
|
+
body["context"] = dict(context)
|
|
699
|
+
if depth:
|
|
700
|
+
body["depth"] = depth
|
|
701
|
+
if workstreams is not None:
|
|
702
|
+
body["workstreams"] = [dict(workstream) for workstream in workstreams]
|
|
703
|
+
if agent_assignment:
|
|
704
|
+
body["agent_assignment"] = agent_assignment
|
|
705
|
+
return self._request(
|
|
706
|
+
"/initiatives/proposals",
|
|
707
|
+
method="POST",
|
|
708
|
+
idempotency_key=idempotency_key,
|
|
709
|
+
body=body,
|
|
710
|
+
)["data"]
|
|
711
|
+
|
|
712
|
+
def get_initiative_scaffold_proposal(
|
|
713
|
+
self, proposal_id: str, *, workspace_id: Optional[str] = None
|
|
714
|
+
) -> Mapping[str, Any]:
|
|
715
|
+
suffix = f"?workspace_id={quote(workspace_id)}" if workspace_id else ""
|
|
716
|
+
payload = self._request(f"/initiatives/proposals/{quote(proposal_id)}{suffix}")
|
|
717
|
+
return payload["data"]
|
|
718
|
+
|
|
719
|
+
def create_decision(
|
|
720
|
+
self,
|
|
721
|
+
workspace_id: str,
|
|
722
|
+
title: str,
|
|
723
|
+
*,
|
|
724
|
+
idempotency_key: Optional[str] = None,
|
|
725
|
+
description: Optional[str] = None,
|
|
726
|
+
shape: Optional[str] = None,
|
|
727
|
+
shape_context: Optional[Mapping[str, Any]] = None,
|
|
728
|
+
urgency: Optional[str] = None,
|
|
729
|
+
blocks_task: Optional[bool] = None,
|
|
730
|
+
task_id: Optional[str] = None,
|
|
731
|
+
initiative_id: Optional[str] = None,
|
|
732
|
+
) -> Mapping[str, Any]:
|
|
733
|
+
"""Raise a decision for a human ruling; replay safety is caller-owned."""
|
|
734
|
+
body: dict[str, Any] = {"workspace_id": workspace_id, "title": title}
|
|
735
|
+
if description is not None:
|
|
736
|
+
body["description"] = description
|
|
737
|
+
if shape:
|
|
738
|
+
body["shape"] = shape
|
|
739
|
+
if shape_context is not None:
|
|
740
|
+
body["shape_context"] = dict(shape_context)
|
|
741
|
+
if urgency:
|
|
742
|
+
body["urgency"] = urgency
|
|
743
|
+
if blocks_task is not None:
|
|
744
|
+
body["blocks_task"] = blocks_task
|
|
745
|
+
if task_id:
|
|
746
|
+
body["task_id"] = task_id
|
|
747
|
+
if initiative_id:
|
|
748
|
+
body["initiative_id"] = initiative_id
|
|
749
|
+
return self._request(
|
|
750
|
+
"/decisions",
|
|
751
|
+
method="POST",
|
|
752
|
+
idempotency_key=idempotency_key,
|
|
753
|
+
body=body,
|
|
754
|
+
)["decision"]
|
|
755
|
+
|
|
756
|
+
def list_decisions(
|
|
757
|
+
self,
|
|
758
|
+
workspace_id: str,
|
|
759
|
+
*,
|
|
760
|
+
shape: Optional[str] = None,
|
|
761
|
+
urgency: Optional[str] = None,
|
|
762
|
+
status: Optional[str] = None,
|
|
763
|
+
limit: Optional[int] = None,
|
|
764
|
+
) -> list[Mapping[str, Any]]:
|
|
765
|
+
params = [f"workspace_id={quote(workspace_id)}"]
|
|
766
|
+
if shape:
|
|
767
|
+
params.append(f"shape={quote(shape)}")
|
|
768
|
+
if urgency:
|
|
769
|
+
params.append(f"urgency={quote(urgency)}")
|
|
770
|
+
if status:
|
|
771
|
+
params.append(f"status={quote(status)}")
|
|
772
|
+
if limit is not None:
|
|
773
|
+
params.append(f"limit={quote(str(limit))}")
|
|
774
|
+
payload = self._request(f"/decisions?{'&'.join(params)}")
|
|
775
|
+
return list(payload["decisions"])
|
|
776
|
+
|
|
777
|
+
def list_artifact_types(self) -> list[Mapping[str, Any]]:
|
|
778
|
+
"""List the global artifact type vocabulary ``create_artifact`` accepts."""
|
|
779
|
+
payload = self._request("/artifact-types")
|
|
780
|
+
return list(payload["data"])
|
|
781
|
+
|
|
782
|
+
def create_artifact(
|
|
783
|
+
self,
|
|
784
|
+
entity_type: str,
|
|
785
|
+
entity_id: str,
|
|
786
|
+
name: str,
|
|
787
|
+
artifact_type: str,
|
|
788
|
+
*,
|
|
789
|
+
idempotency_key: Optional[str] = None,
|
|
790
|
+
artifact_url: Optional[str] = None,
|
|
791
|
+
external_url: Optional[str] = None,
|
|
792
|
+
description: Optional[str] = None,
|
|
793
|
+
preview_markdown: Optional[str] = None,
|
|
794
|
+
initiative_id: Optional[str] = None,
|
|
795
|
+
status: Optional[str] = None,
|
|
796
|
+
metadata: Optional[Mapping[str, Any]] = None,
|
|
797
|
+
created_by_type: Optional[str] = None,
|
|
798
|
+
created_by_id: Optional[str] = None,
|
|
799
|
+
) -> MutableMapping[str, Any]:
|
|
800
|
+
"""Register produced work; one of ``artifact_url`` or ``external_url`` is required."""
|
|
801
|
+
body: dict[str, Any] = {
|
|
802
|
+
"entity_type": entity_type,
|
|
803
|
+
"entity_id": entity_id,
|
|
804
|
+
"name": name,
|
|
805
|
+
"artifact_type": artifact_type,
|
|
806
|
+
}
|
|
807
|
+
if artifact_url:
|
|
808
|
+
body["artifact_url"] = artifact_url
|
|
809
|
+
if external_url:
|
|
810
|
+
body["external_url"] = external_url
|
|
811
|
+
if description is not None:
|
|
812
|
+
body["description"] = description
|
|
813
|
+
if preview_markdown is not None:
|
|
814
|
+
body["preview_markdown"] = preview_markdown
|
|
815
|
+
if initiative_id:
|
|
816
|
+
body["initiative_id"] = initiative_id
|
|
817
|
+
if status:
|
|
818
|
+
body["status"] = status
|
|
819
|
+
if metadata is not None:
|
|
820
|
+
body["metadata"] = dict(metadata)
|
|
821
|
+
if created_by_type:
|
|
822
|
+
body["created_by_type"] = created_by_type
|
|
823
|
+
if created_by_id:
|
|
824
|
+
body["created_by_id"] = created_by_id
|
|
825
|
+
return self._request(
|
|
826
|
+
"/artifacts",
|
|
827
|
+
method="POST",
|
|
828
|
+
idempotency_key=idempotency_key,
|
|
829
|
+
body=body,
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
def list_artifacts(
|
|
833
|
+
self,
|
|
834
|
+
workspace_id: str,
|
|
835
|
+
*,
|
|
836
|
+
initiative_id: Optional[str] = None,
|
|
837
|
+
task_id: Optional[str] = None,
|
|
838
|
+
status: Optional[str] = None,
|
|
839
|
+
since: Optional[str] = None,
|
|
840
|
+
limit: Optional[int] = None,
|
|
841
|
+
) -> list[Mapping[str, Any]]:
|
|
842
|
+
params = [f"workspace_id={quote(workspace_id)}"]
|
|
843
|
+
if initiative_id:
|
|
844
|
+
params.append(f"initiative_id={quote(initiative_id)}")
|
|
845
|
+
if task_id:
|
|
846
|
+
params.append(f"task_id={quote(task_id)}")
|
|
847
|
+
if status:
|
|
848
|
+
params.append(f"status={quote(status)}")
|
|
849
|
+
if since:
|
|
850
|
+
params.append(f"since={quote(since)}")
|
|
851
|
+
if limit is not None:
|
|
852
|
+
params.append(f"limit={quote(str(limit))}")
|
|
853
|
+
payload = self._request(f"/artifacts?{'&'.join(params)}")
|
|
854
|
+
return list(payload["artifacts"])
|
|
855
|
+
|
|
856
|
+
def list_artifacts_by_entity(
|
|
857
|
+
self,
|
|
858
|
+
entity_type: str,
|
|
859
|
+
entity_id: str,
|
|
860
|
+
*,
|
|
861
|
+
kind: Optional[str] = None,
|
|
862
|
+
limit: Optional[int] = None,
|
|
863
|
+
) -> list[Mapping[str, Any]]:
|
|
864
|
+
params = [
|
|
865
|
+
f"entity_type={quote(entity_type)}",
|
|
866
|
+
f"entity_id={quote(entity_id)}",
|
|
867
|
+
]
|
|
868
|
+
if kind:
|
|
869
|
+
params.append(f"kind={quote(kind)}")
|
|
870
|
+
if limit is not None:
|
|
871
|
+
params.append(f"limit={quote(str(limit))}")
|
|
872
|
+
payload = self._request(f"/artifacts/by-entity?{'&'.join(params)}")
|
|
873
|
+
return list(payload["artifacts"])
|
|
874
|
+
|
|
875
|
+
def get_artifact(self, artifact_id: str) -> MutableMapping[str, Any]:
|
|
876
|
+
"""Read one artifact with its entity relationships."""
|
|
877
|
+
return self._request(f"/artifacts/{quote(artifact_id)}")
|
|
878
|
+
|
|
879
|
+
def control_run(
|
|
880
|
+
self,
|
|
881
|
+
run_id: str,
|
|
882
|
+
action: str,
|
|
883
|
+
*,
|
|
884
|
+
idempotency_key: Optional[str] = None,
|
|
885
|
+
checkpoint_id: Optional[str] = None,
|
|
886
|
+
reason: Optional[str] = None,
|
|
887
|
+
) -> Mapping[str, Any]:
|
|
888
|
+
"""Apply ``pause``, ``resume``, ``cancel``, or ``rollback`` to a run.
|
|
889
|
+
|
|
890
|
+
``checkpoint_id`` is required for ``rollback``.
|
|
891
|
+
"""
|
|
892
|
+
body: dict[str, Any] = {}
|
|
893
|
+
if checkpoint_id:
|
|
894
|
+
body["checkpointId"] = checkpoint_id
|
|
895
|
+
if reason is not None:
|
|
896
|
+
body["reason"] = reason
|
|
897
|
+
return self._request(
|
|
898
|
+
f"/runs/{quote(run_id)}/actions/{quote(action)}",
|
|
899
|
+
method="POST",
|
|
900
|
+
idempotency_key=idempotency_key,
|
|
901
|
+
body=body or None,
|
|
902
|
+
)["data"]
|
|
903
|
+
|
|
904
|
+
def apply_lifecycle_action(
|
|
905
|
+
self,
|
|
906
|
+
level: str,
|
|
907
|
+
node_id: str,
|
|
908
|
+
action: str,
|
|
909
|
+
*,
|
|
910
|
+
idempotency_key: Optional[str] = None,
|
|
911
|
+
) -> MutableMapping[str, Any]:
|
|
912
|
+
"""Pause, resume, retry, or cancel an initiative, workstream, milestone, task, or run."""
|
|
913
|
+
return self._request(
|
|
914
|
+
"/lifecycle",
|
|
915
|
+
method="POST",
|
|
916
|
+
idempotency_key=idempotency_key,
|
|
917
|
+
body={"level": level, "id": node_id, "action": action},
|
|
918
|
+
)
|
|
919
|
+
|
|
920
|
+
def import_agent_work_receipt(
|
|
921
|
+
self,
|
|
922
|
+
receipt: Mapping[str, Any],
|
|
923
|
+
*,
|
|
924
|
+
idempotency_key: str,
|
|
925
|
+
workspace_id: Optional[str] = None,
|
|
926
|
+
) -> MutableMapping[str, Any]:
|
|
927
|
+
"""Validate and store a portable Agent Work Receipt in the workspace."""
|
|
928
|
+
body: dict[str, Any] = {"receipt": dict(receipt)}
|
|
929
|
+
if workspace_id:
|
|
930
|
+
body["workspace_id"] = workspace_id
|
|
931
|
+
return self._request(
|
|
932
|
+
"/agent-work-receipts",
|
|
933
|
+
method="POST",
|
|
934
|
+
idempotency_key=idempotency_key,
|
|
935
|
+
body=body,
|
|
936
|
+
)
|
|
937
|
+
|
|
938
|
+
def get_agent_work_receipt_validator(self) -> MutableMapping[str, Any]:
|
|
939
|
+
"""Get the supported receipt schema, limits, and a runnable example."""
|
|
940
|
+
return self._request("/agent-work-receipts/validate")
|
|
941
|
+
|
|
942
|
+
def validate_agent_work_receipt(
|
|
943
|
+
self, receipt: Mapping[str, Any]
|
|
944
|
+
) -> MutableMapping[str, Any]:
|
|
945
|
+
"""Validate a portable Agent Work Receipt without storing it."""
|
|
946
|
+
return self._request(
|
|
947
|
+
"/agent-work-receipts/validate",
|
|
948
|
+
method="POST",
|
|
949
|
+
body=dict(receipt),
|
|
950
|
+
)
|
|
951
|
+
|
|
952
|
+
def get_workload_doctor_metadata(self) -> MutableMapping[str, Any]:
|
|
953
|
+
"""Get the workload diagnosis request schema and a runnable example."""
|
|
954
|
+
return self._request("/doctor/workload")
|
|
955
|
+
|
|
956
|
+
def diagnose_workload_boundaries(
|
|
957
|
+
self,
|
|
958
|
+
workload: Mapping[str, Any],
|
|
959
|
+
*,
|
|
960
|
+
schema_version: str = "workload-diagnosis/0.1",
|
|
961
|
+
) -> MutableMapping[str, Any]:
|
|
962
|
+
"""Score a workload across time, agents, systems, authority, and accountability."""
|
|
963
|
+
return self._request(
|
|
964
|
+
"/doctor/workload",
|
|
965
|
+
method="POST",
|
|
966
|
+
body={"schema_version": schema_version, "workload": dict(workload)},
|
|
967
|
+
)
|
|
968
|
+
|
|
969
|
+
def claim_dedup_fingerprint(
|
|
970
|
+
self,
|
|
971
|
+
source: str,
|
|
972
|
+
event_key: str,
|
|
973
|
+
*,
|
|
974
|
+
idempotency_key: Optional[str] = None,
|
|
975
|
+
initiative_id: Optional[str] = None,
|
|
976
|
+
ttl_seconds: Optional[int] = None,
|
|
977
|
+
active_run_id: Optional[str] = None,
|
|
978
|
+
) -> MutableMapping[str, Any]:
|
|
979
|
+
"""Claim a durable duplicate-trigger fingerprint; the first claimant wins."""
|
|
980
|
+
body: dict[str, Any] = {"source": source, "event_key": event_key}
|
|
981
|
+
if initiative_id:
|
|
982
|
+
body["initiative_id"] = initiative_id
|
|
983
|
+
if ttl_seconds is not None:
|
|
984
|
+
body["ttl_seconds"] = ttl_seconds
|
|
985
|
+
if active_run_id:
|
|
986
|
+
body["active_run_id"] = active_run_id
|
|
987
|
+
return self._request(
|
|
988
|
+
"/live/dedup/claim",
|
|
989
|
+
method="POST",
|
|
990
|
+
idempotency_key=idempotency_key,
|
|
991
|
+
body=body,
|
|
992
|
+
)
|
|
993
|
+
|
|
994
|
+
def create_estimate(
|
|
995
|
+
self,
|
|
996
|
+
prompt: str,
|
|
997
|
+
content_types: list[str],
|
|
998
|
+
*,
|
|
999
|
+
variant_count: Optional[int] = None,
|
|
1000
|
+
brand_url: Optional[str] = None,
|
|
1001
|
+
brand_id: Optional[str] = None,
|
|
1002
|
+
platform: Optional[str] = None,
|
|
1003
|
+
) -> MutableMapping[str, Any]:
|
|
1004
|
+
"""Calculate the price and delivery range for a Content Studio request."""
|
|
1005
|
+
body: dict[str, Any] = {
|
|
1006
|
+
"prompt": prompt,
|
|
1007
|
+
"contentTypes": list(content_types),
|
|
1008
|
+
}
|
|
1009
|
+
if variant_count is not None:
|
|
1010
|
+
body["variantCount"] = variant_count
|
|
1011
|
+
if brand_url:
|
|
1012
|
+
body["brandUrl"] = brand_url
|
|
1013
|
+
if brand_id:
|
|
1014
|
+
body["brandId"] = brand_id
|
|
1015
|
+
if platform:
|
|
1016
|
+
body["platform"] = platform
|
|
1017
|
+
return self._request("/studio/estimate", method="POST", body=body)
|
|
1018
|
+
|
|
1019
|
+
def get_showcase(
|
|
1020
|
+
self,
|
|
1021
|
+
*,
|
|
1022
|
+
query: Optional[str] = None,
|
|
1023
|
+
content_type: Optional[str] = None,
|
|
1024
|
+
industry: Optional[str] = None,
|
|
1025
|
+
style: Optional[str] = None,
|
|
1026
|
+
featured: Optional[bool] = None,
|
|
1027
|
+
limit: Optional[int] = None,
|
|
1028
|
+
offset: Optional[int] = None,
|
|
1029
|
+
) -> MutableMapping[str, Any]:
|
|
1030
|
+
"""Browse completed Content Studio examples."""
|
|
1031
|
+
params = []
|
|
1032
|
+
if query:
|
|
1033
|
+
params.append(f"query={quote(query)}")
|
|
1034
|
+
if content_type:
|
|
1035
|
+
params.append(f"contentType={quote(content_type)}")
|
|
1036
|
+
if industry:
|
|
1037
|
+
params.append(f"industry={quote(industry)}")
|
|
1038
|
+
if style:
|
|
1039
|
+
params.append(f"style={quote(style)}")
|
|
1040
|
+
if featured is not None:
|
|
1041
|
+
params.append(f"featured={'true' if featured else 'false'}")
|
|
1042
|
+
if limit is not None:
|
|
1043
|
+
params.append(f"limit={quote(str(limit))}")
|
|
1044
|
+
if offset is not None:
|
|
1045
|
+
params.append(f"offset={quote(str(offset))}")
|
|
1046
|
+
suffix = f"?{'&'.join(params)}" if params else ""
|
|
1047
|
+
return self._request(f"/studio/showcase{suffix}")
|
|
1048
|
+
|
|
1049
|
+
def create_checkout(self, estimate_id: str) -> MutableMapping[str, Any]:
|
|
1050
|
+
"""Create a checkout session for an accepted Content Studio estimate."""
|
|
1051
|
+
return self._request(
|
|
1052
|
+
"/studio/checkout",
|
|
1053
|
+
method="POST",
|
|
1054
|
+
body={"estimateId": estimate_id},
|
|
1055
|
+
)
|
|
1056
|
+
|
|
1057
|
+
def _handoff_transition(
|
|
1058
|
+
self,
|
|
1059
|
+
action: str,
|
|
1060
|
+
workspace_id: str,
|
|
1061
|
+
handoff_id: str,
|
|
1062
|
+
expected_aggregate_version: int,
|
|
1063
|
+
idempotency_key: str,
|
|
1064
|
+
) -> Mapping[str, Any]:
|
|
1065
|
+
return self._request(
|
|
1066
|
+
f"/handoffs/{quote(handoff_id)}/{action}",
|
|
1067
|
+
method="POST",
|
|
1068
|
+
idempotency_key=idempotency_key,
|
|
1069
|
+
body={
|
|
1070
|
+
"workspace_id": workspace_id,
|
|
1071
|
+
"expected_aggregate_version": expected_aggregate_version,
|
|
1072
|
+
},
|
|
1073
|
+
)["data"]
|
|
1074
|
+
|
|
1075
|
+
def _transition(
|
|
1076
|
+
self,
|
|
1077
|
+
action: str,
|
|
1078
|
+
workspace_id: str,
|
|
1079
|
+
process_id: str,
|
|
1080
|
+
expected_aggregate_version: int,
|
|
1081
|
+
idempotency_key: str,
|
|
1082
|
+
) -> Mapping[str, Any]:
|
|
1083
|
+
return self._request(
|
|
1084
|
+
f"/operating-processes/{quote(process_id)}/{action}",
|
|
1085
|
+
method="POST",
|
|
1086
|
+
idempotency_key=idempotency_key,
|
|
1087
|
+
body={
|
|
1088
|
+
"workspace_id": workspace_id,
|
|
1089
|
+
"expected_aggregate_version": expected_aggregate_version,
|
|
1090
|
+
},
|
|
1091
|
+
)["data"]
|
|
1092
|
+
|
|
1093
|
+
def _request(
|
|
1094
|
+
self,
|
|
1095
|
+
path: str,
|
|
1096
|
+
*,
|
|
1097
|
+
method: str = "GET",
|
|
1098
|
+
body: Optional[Mapping[str, Any]] = None,
|
|
1099
|
+
idempotency_key: Optional[str] = None,
|
|
1100
|
+
) -> MutableMapping[str, Any]:
|
|
1101
|
+
headers = {"Accept": "application/json"}
|
|
1102
|
+
credential = self.api_key or self.token
|
|
1103
|
+
if credential:
|
|
1104
|
+
headers["Authorization"] = f"Bearer {credential}"
|
|
1105
|
+
payload: Optional[bytes] = None
|
|
1106
|
+
if body is not None:
|
|
1107
|
+
headers["Content-Type"] = "application/json"
|
|
1108
|
+
payload = json.dumps(body).encode("utf-8")
|
|
1109
|
+
if idempotency_key:
|
|
1110
|
+
headers["Idempotency-Key"] = idempotency_key
|
|
1111
|
+
request = Request(
|
|
1112
|
+
f"{self.base_url.rstrip('/')}{path}",
|
|
1113
|
+
data=payload,
|
|
1114
|
+
headers=headers,
|
|
1115
|
+
method=method,
|
|
1116
|
+
)
|
|
1117
|
+
try:
|
|
1118
|
+
with urlopen(request, timeout=self.timeout_seconds) as response:
|
|
1119
|
+
return json.loads(response.read().decode("utf-8"))
|
|
1120
|
+
except HTTPError as error:
|
|
1121
|
+
try:
|
|
1122
|
+
error_payload = json.loads(error.read().decode("utf-8"))
|
|
1123
|
+
except (OSError, ValueError):
|
|
1124
|
+
error_payload = {}
|
|
1125
|
+
detail = error_payload.get("error", {}) if isinstance(error_payload, dict) else {}
|
|
1126
|
+
raise OrgXApiError(
|
|
1127
|
+
error.code,
|
|
1128
|
+
detail.get("code", "request_failed"),
|
|
1129
|
+
detail.get("message", f"OrgX API request failed ({error.code})"),
|
|
1130
|
+
detail.get("details"),
|
|
1131
|
+
) from error
|
|
1132
|
+
except URLError as error:
|
|
1133
|
+
raise OrgXApiError(0, "transport_failed", str(error.reason)) from error
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "orgx"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Python client for the OrgX v1 API"
|
|
5
|
+
requires-python = ">=3.10"
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
license = { text = "Proprietary" }
|
|
8
|
+
authors = [{ name = "OrgX", email = "support@useorgx.com" }]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Programming Language :: Python :: 3",
|
|
11
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
12
|
+
"License :: Other/Proprietary License",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.urls]
|
|
16
|
+
Documentation = "https://docs.useorgx.com/docs/api/clients"
|
|
17
|
+
Homepage = "https://useorgx.com"
|
|
18
|
+
Repository = "https://github.com/useorgx/orgx-sdk-python"
|
|
19
|
+
Issues = "https://github.com/useorgx/orgx-sdk-python/issues"
|
|
20
|
+
|
|
21
|
+
[build-system]
|
|
22
|
+
requires = ["setuptools>=77"]
|
|
23
|
+
build-backend = "setuptools.build_meta"
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.packages.find]
|
|
26
|
+
include = ["orgx_client*"]
|
|
27
|
+
|
|
28
|
+
[tool.ruff]
|
|
29
|
+
line-length = 100
|
orgx-1.0.0/setup.cfg
ADDED