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 ADDED
@@ -0,0 +1,4 @@
1
+ Copyright 2026 OrgX, Inc. All rights reserved.
2
+
3
+ Use of this SDK is governed by the OrgX Terms of Service:
4
+ https://useorgx.com/terms
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,9 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ orgx.egg-info/PKG-INFO
5
+ orgx.egg-info/SOURCES.txt
6
+ orgx.egg-info/dependency_links.txt
7
+ orgx.egg-info/top_level.txt
8
+ orgx_client/__init__.py
9
+ orgx_client/client.py
@@ -0,0 +1 @@
1
+ orgx_client
@@ -0,0 +1,5 @@
1
+ """Small, dependency-free OrgX v1 API client."""
2
+
3
+ from .client import EventStreamSubscription, OrgXApiError, OrgXClient
4
+
5
+ __all__ = ["EventStreamSubscription", "OrgXApiError", "OrgXClient"]
@@ -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
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+