dominus-sdk-python 2.16.0__tar.gz → 2.17.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.
Files changed (52) hide show
  1. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/PKG-INFO +1 -1
  2. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus/__init__.py +1 -1
  3. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus/namespaces/workflow.py +343 -0
  4. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus_sdk_python.egg-info/PKG-INFO +1 -1
  5. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/pyproject.toml +1 -1
  6. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/tests/test_workflow_lifecycle.py +225 -0
  7. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/README.md +0 -0
  8. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus/config/__init__.py +0 -0
  9. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus/config/endpoints.py +0 -0
  10. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus/errors.py +0 -0
  11. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus/helpers/__init__.py +0 -0
  12. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus/helpers/auth.py +0 -0
  13. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus/helpers/cache.py +0 -0
  14. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus/helpers/console_capture.py +0 -0
  15. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus/helpers/core.py +0 -0
  16. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus/helpers/crypto.py +0 -0
  17. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus/helpers/sse.py +0 -0
  18. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus/helpers/trace.py +0 -0
  19. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus/namespaces/__init__.py +0 -0
  20. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus/namespaces/admin.py +0 -0
  21. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus/namespaces/ai.py +0 -0
  22. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus/namespaces/artifacts.py +0 -0
  23. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus/namespaces/auth.py +0 -0
  24. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus/namespaces/courier.py +0 -0
  25. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus/namespaces/db.py +0 -0
  26. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus/namespaces/ddl.py +0 -0
  27. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus/namespaces/fastapi.py +0 -0
  28. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus/namespaces/files.py +0 -0
  29. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus/namespaces/health.py +0 -0
  30. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus/namespaces/jobs.py +0 -0
  31. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus/namespaces/logs.py +0 -0
  32. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus/namespaces/open.py +0 -0
  33. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus/namespaces/oracle/__init__.py +0 -0
  34. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus/namespaces/oracle/audio_capture.py +0 -0
  35. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus/namespaces/oracle/oracle_websocket.py +0 -0
  36. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus/namespaces/oracle/session.py +0 -0
  37. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus/namespaces/oracle/types.py +0 -0
  38. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus/namespaces/oracle/vad_gate.py +0 -0
  39. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus/namespaces/portal.py +0 -0
  40. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus/namespaces/processor.py +0 -0
  41. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus/namespaces/redis.py +0 -0
  42. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus/namespaces/secrets.py +0 -0
  43. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus/namespaces/secure.py +0 -0
  44. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus/namespaces/sync.py +0 -0
  45. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus/services/__init__.py +0 -0
  46. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus/start.py +0 -0
  47. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus_sdk_python.egg-info/SOURCES.txt +0 -0
  48. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus_sdk_python.egg-info/dependency_links.txt +0 -0
  49. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus_sdk_python.egg-info/requires.txt +0 -0
  50. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/dominus_sdk_python.egg-info/top_level.txt +0 -0
  51. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/setup.cfg +0 -0
  52. {dominus_sdk_python-2.16.0 → dominus_sdk_python-2.17.0}/tests/test_workflow_refs.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dominus-sdk-python
3
- Version: 2.16.0
3
+ Version: 2.17.0
4
4
  Summary: Python SDK for the Dominus Orchestrator Platform
5
5
  Author-email: CareBridge Systems <dev@carebridge.io>
6
6
  License: Proprietary
@@ -180,7 +180,7 @@ from .errors import (
180
180
  TimeoutError as DominusTimeoutError,
181
181
  )
182
182
 
183
- __version__ = "2.16.0"
183
+ __version__ = "2.17.0"
184
184
  __all__ = [
185
185
  # Main SDK instance
186
186
  "dominus",
@@ -86,6 +86,74 @@ class WorkflowNamespace:
86
86
  return {"workflow_id": str(workflow_id).strip()}
87
87
  raise ValueError("workflow_id or workflow_ref is required")
88
88
 
89
+ @staticmethod
90
+ def _instance_base_endpoint(workflow_id: str) -> str:
91
+ from urllib.parse import quote
92
+
93
+ return f"/api/workflow/workflows/{quote(workflow_id, safe='')}/instances"
94
+
95
+ @staticmethod
96
+ def _build_instance_nudge_policy(
97
+ nudge_policy: Optional[Dict[str, Any]] = None,
98
+ ) -> Optional[Dict[str, Any]]:
99
+ if not nudge_policy:
100
+ return None
101
+
102
+ policy = dict(nudge_policy)
103
+ result: Dict[str, Any] = {}
104
+ if "mode" in policy and policy["mode"] is not None:
105
+ result["mode"] = policy["mode"]
106
+ if "max_rerun_count" in policy and policy["max_rerun_count"] is not None:
107
+ result["max_rerun_count"] = policy["max_rerun_count"]
108
+ elif "maxRerunCount" in policy and policy["maxRerunCount"] is not None:
109
+ result["max_rerun_count"] = policy["maxRerunCount"]
110
+ if "rerun_window_seconds" in policy and policy["rerun_window_seconds"] is not None:
111
+ result["rerun_window_seconds"] = policy["rerun_window_seconds"]
112
+ elif "rerunWindowSeconds" in policy and policy["rerunWindowSeconds"] is not None:
113
+ result["rerun_window_seconds"] = policy["rerunWindowSeconds"]
114
+
115
+ for key, value in policy.items():
116
+ if value is None:
117
+ continue
118
+ if key in {"mode", "max_rerun_count", "maxRerunCount", "rerun_window_seconds", "rerunWindowSeconds"}:
119
+ continue
120
+ result[key] = value
121
+
122
+ return result or None
123
+
124
+ def _build_ensure_instance_body(
125
+ self,
126
+ *,
127
+ group: str,
128
+ owner: str,
129
+ instance_id: Optional[str] = None,
130
+ environment: Optional[str] = None,
131
+ nudge_policy: Optional[Dict[str, Any]] = None,
132
+ bindings: Optional[Dict[str, Any]] = None,
133
+ mode: Optional[str] = None,
134
+ context: Optional[Dict[str, Any]] = None,
135
+ ) -> Dict[str, Any]:
136
+ body: Dict[str, Any] = {
137
+ "params": {
138
+ "group": group,
139
+ "owner": owner,
140
+ }
141
+ }
142
+ if instance_id:
143
+ body["instance_id"] = instance_id
144
+ if environment:
145
+ body["params"]["env"] = environment
146
+ policy = self._build_instance_nudge_policy(nudge_policy)
147
+ if policy:
148
+ body["params"]["nudge_policy"] = policy
149
+ if bindings:
150
+ body["bindings"] = bindings
151
+ if mode:
152
+ body["mode"] = mode
153
+ if context:
154
+ body["context"] = context
155
+ return body
156
+
89
157
  async def _api(
90
158
  self,
91
159
  endpoint: str,
@@ -620,6 +688,281 @@ class WorkflowNamespace:
620
688
  body=body,
621
689
  )
622
690
 
691
+ async def ensure(
692
+ self,
693
+ workflow_id: str,
694
+ *,
695
+ instance_id: Optional[str] = None,
696
+ group: str,
697
+ owner: str,
698
+ environment: Optional[str] = None,
699
+ nudge_policy: Optional[Dict[str, Any]] = None,
700
+ bindings: Optional[Dict[str, Any]] = None,
701
+ mode: str = "blocking",
702
+ context: Optional[Dict[str, Any]] = None,
703
+ ) -> Dict[str, Any]:
704
+ """Ensure a workflow instance exists and optionally launch execution."""
705
+ return await self.ensure_instance(
706
+ workflow_id,
707
+ instance_id=instance_id,
708
+ group=group,
709
+ owner=owner,
710
+ environment=environment,
711
+ nudge_policy=nudge_policy,
712
+ bindings=bindings,
713
+ mode=mode,
714
+ context=context,
715
+ )
716
+
717
+ async def ensure_instance(
718
+ self,
719
+ workflow_id: str,
720
+ *,
721
+ instance_id: Optional[str] = None,
722
+ group: str,
723
+ owner: str,
724
+ environment: Optional[str] = None,
725
+ nudge_policy: Optional[Dict[str, Any]] = None,
726
+ bindings: Optional[Dict[str, Any]] = None,
727
+ mode: str = "blocking",
728
+ context: Optional[Dict[str, Any]] = None,
729
+ ) -> Dict[str, Any]:
730
+ """Ensure a workflow instance exists and optionally launch execution."""
731
+ if mode == "streaming":
732
+ raise ValueError("Use stream_ensure_instance() for streaming instance execution")
733
+
734
+ return await self._api(
735
+ endpoint=f"{self._instance_base_endpoint(workflow_id)}/ensure",
736
+ body=self._build_ensure_instance_body(
737
+ instance_id=instance_id,
738
+ group=group,
739
+ owner=owner,
740
+ environment=environment,
741
+ nudge_policy=nudge_policy,
742
+ bindings=bindings,
743
+ mode=mode,
744
+ context=context,
745
+ ),
746
+ )
747
+
748
+ async def stream_ensure_instance(
749
+ self,
750
+ workflow_id: str,
751
+ *,
752
+ instance_id: Optional[str] = None,
753
+ group: str,
754
+ owner: str,
755
+ environment: Optional[str] = None,
756
+ nudge_policy: Optional[Dict[str, Any]] = None,
757
+ bindings: Optional[Dict[str, Any]] = None,
758
+ context: Optional[Dict[str, Any]] = None,
759
+ on_chunk: Optional[Any] = None,
760
+ ):
761
+ """Ensure a workflow instance and stream execution events."""
762
+ async for chunk in self._client._stream_request(
763
+ endpoint=f"{self._instance_base_endpoint(workflow_id)}/ensure",
764
+ body=self._build_ensure_instance_body(
765
+ instance_id=instance_id,
766
+ group=group,
767
+ owner=owner,
768
+ environment=environment,
769
+ nudge_policy=nudge_policy,
770
+ bindings=bindings,
771
+ mode="streaming",
772
+ context=context,
773
+ ),
774
+ on_chunk=on_chunk,
775
+ use_gateway=True,
776
+ ):
777
+ yield chunk
778
+
779
+ async def get_instance_status(self, workflow_id: str, instance_id: str) -> Dict[str, Any]:
780
+ """Get workflow instance status."""
781
+ from urllib.parse import quote
782
+
783
+ return await self._api(
784
+ endpoint=f"{self._instance_base_endpoint(workflow_id)}/{quote(instance_id, safe='')}/status",
785
+ method="GET",
786
+ )
787
+
788
+ async def list_instances(
789
+ self,
790
+ *,
791
+ workflow_id: Optional[str] = None,
792
+ group: Optional[str] = None,
793
+ owner: Optional[str] = None,
794
+ environment: Optional[str] = None,
795
+ status: Optional[str] = None,
796
+ limit: int = 50,
797
+ offset: int = 0,
798
+ ) -> List[Dict[str, Any]]:
799
+ """List workflow instances."""
800
+ params = [f"limit={limit}", f"offset={offset}"]
801
+ if workflow_id:
802
+ params.append(f"workflow_id={workflow_id}")
803
+ if group:
804
+ params.append(f"group={group}")
805
+ if owner:
806
+ params.append(f"owner={owner}")
807
+ if environment:
808
+ params.append(f"environment={environment}")
809
+ if status:
810
+ params.append(f"status={status}")
811
+
812
+ result = await self._api(
813
+ endpoint=f"/api/workflow/instances?{'&'.join(params)}",
814
+ method="GET",
815
+ )
816
+ return result.get("instances", result) if isinstance(result, dict) else result
817
+
818
+ async def get_instance_artifacts(self, workflow_id: str, instance_id: str) -> Dict[str, Any]:
819
+ """Get workflow instance artifact addresses."""
820
+ from urllib.parse import quote
821
+
822
+ return await self._api(
823
+ endpoint=f"{self._instance_base_endpoint(workflow_id)}/{quote(instance_id, safe='')}/artifacts",
824
+ method="GET",
825
+ )
826
+
827
+ async def get_instance_outputs(self, workflow_id: str, instance_id: str) -> Dict[str, Any]:
828
+ """Get workflow instance accepted outputs."""
829
+ from urllib.parse import quote
830
+
831
+ return await self._api(
832
+ endpoint=f"{self._instance_base_endpoint(workflow_id)}/{quote(instance_id, safe='')}/outputs",
833
+ method="GET",
834
+ )
835
+
836
+ async def get_instance_history(
837
+ self,
838
+ workflow_id: str,
839
+ instance_id: str,
840
+ *,
841
+ limit: int = 50,
842
+ offset: int = 0,
843
+ ) -> Dict[str, Any]:
844
+ """Get workflow instance execution history."""
845
+ from urllib.parse import quote
846
+
847
+ return await self._api(
848
+ endpoint=(
849
+ f"{self._instance_base_endpoint(workflow_id)}/{quote(instance_id, safe='')}/history"
850
+ f"?limit={limit}&offset={offset}"
851
+ ),
852
+ method="GET",
853
+ )
854
+
855
+ async def nudge(
856
+ self,
857
+ workflow_id: str,
858
+ instance_id: str,
859
+ *,
860
+ reason: str,
861
+ trigger_artifact: Optional[str] = None,
862
+ mode: str = "async",
863
+ ) -> Dict[str, Any]:
864
+ """Nudge a workflow instance into a new execution."""
865
+ return await self.nudge_instance(
866
+ workflow_id,
867
+ instance_id,
868
+ reason=reason,
869
+ trigger_artifact=trigger_artifact,
870
+ mode=mode,
871
+ )
872
+
873
+ async def nudge_instance(
874
+ self,
875
+ workflow_id: str,
876
+ instance_id: str,
877
+ *,
878
+ reason: str,
879
+ trigger_artifact: Optional[str] = None,
880
+ mode: str = "async",
881
+ ) -> Dict[str, Any]:
882
+ """Nudge a workflow instance into a new execution."""
883
+ from urllib.parse import quote
884
+
885
+ if mode == "streaming":
886
+ raise ValueError("Use stream_nudge_instance() for streaming instance execution")
887
+
888
+ body: Dict[str, Any] = {"reason": reason, "mode": mode}
889
+ if trigger_artifact:
890
+ body["trigger_artifact"] = trigger_artifact
891
+
892
+ return await self._api(
893
+ endpoint=f"{self._instance_base_endpoint(workflow_id)}/{quote(instance_id, safe='')}/nudge",
894
+ body=body,
895
+ )
896
+
897
+ async def stream_nudge_instance(
898
+ self,
899
+ workflow_id: str,
900
+ instance_id: str,
901
+ *,
902
+ reason: str,
903
+ trigger_artifact: Optional[str] = None,
904
+ on_chunk: Optional[Any] = None,
905
+ ):
906
+ """Nudge a workflow instance and stream execution events."""
907
+ from urllib.parse import quote
908
+
909
+ body: Dict[str, Any] = {"reason": reason, "mode": "streaming"}
910
+ if trigger_artifact:
911
+ body["trigger_artifact"] = trigger_artifact
912
+
913
+ async for chunk in self._client._stream_request(
914
+ endpoint=f"{self._instance_base_endpoint(workflow_id)}/{quote(instance_id, safe='')}/nudge",
915
+ body=body,
916
+ on_chunk=on_chunk,
917
+ use_gateway=True,
918
+ ):
919
+ yield chunk
920
+
921
+ async def retry(
922
+ self,
923
+ workflow_id: str,
924
+ instance_id: str,
925
+ *,
926
+ reason: str = "retry",
927
+ trigger_artifact: Optional[str] = None,
928
+ mode: str = "async",
929
+ ) -> Dict[str, Any]:
930
+ """Retry a workflow instance. Alias for nudge with a default retry reason."""
931
+ return await self.retry_instance(
932
+ workflow_id,
933
+ instance_id,
934
+ reason=reason,
935
+ trigger_artifact=trigger_artifact,
936
+ mode=mode,
937
+ )
938
+
939
+ async def retry_instance(
940
+ self,
941
+ workflow_id: str,
942
+ instance_id: str,
943
+ *,
944
+ reason: str = "retry",
945
+ trigger_artifact: Optional[str] = None,
946
+ mode: str = "async",
947
+ ) -> Dict[str, Any]:
948
+ """Retry a workflow instance. Alias for nudge with a default retry reason."""
949
+ return await self.nudge_instance(
950
+ workflow_id,
951
+ instance_id,
952
+ reason=reason,
953
+ trigger_artifact=trigger_artifact,
954
+ mode=mode,
955
+ )
956
+
957
+ async def cancel_instance(self, workflow_id: str, instance_id: str) -> Dict[str, Any]:
958
+ """Cancel a running workflow instance."""
959
+ from urllib.parse import quote
960
+
961
+ return await self._api(
962
+ endpoint=f"{self._instance_base_endpoint(workflow_id)}/{quote(instance_id, safe='')}/cancel",
963
+ body={},
964
+ )
965
+
623
966
  async def get_run(self, run_id: str) -> Dict[str, Any]:
624
967
  """Get a saved-workflow run."""
625
968
  return await self._api(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dominus-sdk-python
3
- Version: 2.16.0
3
+ Version: 2.17.0
4
4
  Summary: Python SDK for the Dominus Orchestrator Platform
5
5
  Author-email: CareBridge Systems <dev@carebridge.io>
6
6
  License: Proprietary
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dominus-sdk-python"
7
- version = "2.16.0"
7
+ version = "2.17.0"
8
8
  description = "Python SDK for the Dominus Orchestrator Platform"
9
9
  readme = "README.md"
10
10
  license = {text = "Proprietary"}
@@ -314,3 +314,228 @@ async def test_execute_pipeline_forwards_resume_execution_id():
314
314
 
315
315
  call = client.calls[0]
316
316
  assert call["body"]["config"]["resume_execution_id"] == "resume-1"
317
+
318
+
319
+ @pytest.mark.asyncio
320
+ async def test_saved_workflow_instance_ensure_contract_matches_workflow_manager():
321
+ client = FakeClient()
322
+ namespace = WorkflowNamespace(client)
323
+
324
+ await namespace.ensure_instance(
325
+ "wf-instance",
326
+ instance_id="inst-1",
327
+ group="reports",
328
+ owner="company:acme",
329
+ environment="production",
330
+ nudge_policy={
331
+ "mode": "coalesce",
332
+ "maxRerunCount": 3,
333
+ "rerunWindowSeconds": 120,
334
+ "cooldown_seconds": 15,
335
+ },
336
+ bindings={"subject": "PCM47474562"},
337
+ mode="async",
338
+ context={"company": "acme"},
339
+ )
340
+
341
+ call = client.calls[0]
342
+ assert call["endpoint"] == "/api/workflow/workflows/wf-instance/instances/ensure"
343
+ assert call["body"] == {
344
+ "instance_id": "inst-1",
345
+ "params": {
346
+ "group": "reports",
347
+ "owner": "company:acme",
348
+ "env": "production",
349
+ "nudge_policy": {
350
+ "mode": "coalesce",
351
+ "max_rerun_count": 3,
352
+ "rerun_window_seconds": 120,
353
+ "cooldown_seconds": 15,
354
+ },
355
+ },
356
+ "bindings": {"subject": "PCM47474562"},
357
+ "mode": "async",
358
+ "context": {"company": "acme"},
359
+ }
360
+
361
+
362
+ @pytest.mark.asyncio
363
+ async def test_saved_workflow_instance_ensure_alias_and_stream_guard():
364
+ client = FakeClient()
365
+ namespace = WorkflowNamespace(client)
366
+
367
+ await namespace.ensure(
368
+ "wf-instance",
369
+ group="reports",
370
+ owner="company:acme",
371
+ mode="ensure_only",
372
+ )
373
+
374
+ call = client.calls[0]
375
+ assert call["endpoint"] == "/api/workflow/workflows/wf-instance/instances/ensure"
376
+ assert call["body"] == {
377
+ "params": {
378
+ "group": "reports",
379
+ "owner": "company:acme",
380
+ },
381
+ "mode": "ensure_only",
382
+ }
383
+
384
+ with pytest.raises(ValueError, match="Use stream_ensure_instance\\(\\) for streaming instance execution"):
385
+ await namespace.ensure_instance(
386
+ "wf-instance",
387
+ group="reports",
388
+ owner="company:acme",
389
+ mode="streaming",
390
+ )
391
+
392
+
393
+ @pytest.mark.asyncio
394
+ async def test_stream_ensure_instance_uses_streaming_mode():
395
+ client = FakeClient(stream_chunks=[{"_event": "instance_ensured"}, {"_event": "workflow_complete"}])
396
+ namespace = WorkflowNamespace(client)
397
+
398
+ streamed = [
399
+ chunk
400
+ async for chunk in namespace.stream_ensure_instance(
401
+ "wf-instance",
402
+ group="reports",
403
+ owner="company:acme",
404
+ bindings={"subject": "PCM47474562"},
405
+ )
406
+ ]
407
+
408
+ assert client.stream_calls == [
409
+ {
410
+ "endpoint": "/api/workflow/workflows/wf-instance/instances/ensure",
411
+ "body": {
412
+ "params": {
413
+ "group": "reports",
414
+ "owner": "company:acme",
415
+ },
416
+ "bindings": {"subject": "PCM47474562"},
417
+ "mode": "streaming",
418
+ },
419
+ "timeout": 300.0,
420
+ "use_gateway": True,
421
+ }
422
+ ]
423
+ assert streamed == [{"_event": "instance_ensured"}, {"_event": "workflow_complete"}]
424
+
425
+
426
+ @pytest.mark.asyncio
427
+ async def test_workflow_instance_read_routes_match_contract():
428
+ client = FakeClient()
429
+ namespace = WorkflowNamespace(client)
430
+
431
+ await namespace.get_instance_status("wf-instance", "inst/1")
432
+ await namespace.list_instances(
433
+ workflow_id="wf-instance",
434
+ group="reports",
435
+ owner="company:acme",
436
+ environment="production",
437
+ status="idle",
438
+ limit=25,
439
+ offset=5,
440
+ )
441
+ await namespace.get_instance_artifacts("wf-instance", "inst-1")
442
+ await namespace.get_instance_outputs("wf-instance", "inst-1")
443
+ await namespace.get_instance_history("wf-instance", "inst-1", limit=10, offset=2)
444
+
445
+ assert client.calls[0]["endpoint"] == "/api/workflow/workflows/wf-instance/instances/inst%2F1/status"
446
+ assert client.calls[0]["method"] == "GET"
447
+ assert client.calls[1]["endpoint"] == (
448
+ "/api/workflow/instances"
449
+ "?limit=25&offset=5&workflow_id=wf-instance&group=reports&owner=company:acme"
450
+ "&environment=production&status=idle"
451
+ )
452
+ assert client.calls[1]["method"] == "GET"
453
+ assert client.calls[2]["endpoint"] == "/api/workflow/workflows/wf-instance/instances/inst-1/artifacts"
454
+ assert client.calls[2]["method"] == "GET"
455
+ assert client.calls[3]["endpoint"] == "/api/workflow/workflows/wf-instance/instances/inst-1/outputs"
456
+ assert client.calls[3]["method"] == "GET"
457
+ assert client.calls[4]["endpoint"] == (
458
+ "/api/workflow/workflows/wf-instance/instances/inst-1/history?limit=10&offset=2"
459
+ )
460
+ assert client.calls[4]["method"] == "GET"
461
+
462
+
463
+ @pytest.mark.asyncio
464
+ async def test_workflow_instance_nudge_retry_and_cancel_contract():
465
+ client = FakeClient(stream_chunks=[{"_event": "instance_nudged"}])
466
+ namespace = WorkflowNamespace(client)
467
+
468
+ await namespace.nudge_instance(
469
+ "wf-instance",
470
+ "inst-1",
471
+ reason="artifact-updated",
472
+ trigger_artifact="report_snapshot",
473
+ mode="blocking",
474
+ )
475
+ await namespace.nudge(
476
+ "wf-instance",
477
+ "inst-2",
478
+ reason="manual-retry",
479
+ )
480
+ await namespace.retry_instance(
481
+ "wf-instance",
482
+ "inst-3",
483
+ trigger_artifact="report_snapshot",
484
+ mode="async",
485
+ )
486
+ await namespace.cancel_instance("wf-instance", "inst-4")
487
+
488
+ streamed = [
489
+ chunk
490
+ async for chunk in namespace.stream_nudge_instance(
491
+ "wf-instance",
492
+ "inst-5",
493
+ reason="artifact-updated",
494
+ trigger_artifact="report_snapshot",
495
+ )
496
+ ]
497
+
498
+ assert client.calls[0]["endpoint"] == "/api/workflow/workflows/wf-instance/instances/inst-1/nudge"
499
+ assert client.calls[0]["body"] == {
500
+ "reason": "artifact-updated",
501
+ "trigger_artifact": "report_snapshot",
502
+ "mode": "blocking",
503
+ }
504
+
505
+ assert client.calls[1]["endpoint"] == "/api/workflow/workflows/wf-instance/instances/inst-2/nudge"
506
+ assert client.calls[1]["body"] == {
507
+ "reason": "manual-retry",
508
+ "mode": "async",
509
+ }
510
+
511
+ assert client.calls[2]["endpoint"] == "/api/workflow/workflows/wf-instance/instances/inst-3/nudge"
512
+ assert client.calls[2]["body"] == {
513
+ "reason": "retry",
514
+ "trigger_artifact": "report_snapshot",
515
+ "mode": "async",
516
+ }
517
+
518
+ assert client.calls[3]["endpoint"] == "/api/workflow/workflows/wf-instance/instances/inst-4/cancel"
519
+ assert client.calls[3]["body"] == {}
520
+
521
+ assert client.stream_calls == [
522
+ {
523
+ "endpoint": "/api/workflow/workflows/wf-instance/instances/inst-5/nudge",
524
+ "body": {
525
+ "reason": "artifact-updated",
526
+ "trigger_artifact": "report_snapshot",
527
+ "mode": "streaming",
528
+ },
529
+ "timeout": 300.0,
530
+ "use_gateway": True,
531
+ }
532
+ ]
533
+ assert streamed == [{"_event": "instance_nudged"}]
534
+
535
+ with pytest.raises(ValueError, match="Use stream_nudge_instance\\(\\) for streaming instance execution"):
536
+ await namespace.nudge_instance(
537
+ "wf-instance",
538
+ "inst-1",
539
+ reason="artifact-updated",
540
+ mode="streaming",
541
+ )