splox 0.2.3__tar.gz → 0.2.4__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 (34) hide show
  1. {splox-0.2.3 → splox-0.2.4}/PKG-INFO +1 -1
  2. {splox-0.2.3 → splox-0.2.4}/pyproject.toml +1 -1
  3. {splox-0.2.3 → splox-0.2.4}/src/splox/__init__.py +20 -19
  4. {splox-0.2.3 → splox-0.2.4}/src/splox/_client.py +2 -4
  5. {splox-0.2.3 → splox-0.2.4}/src/splox/_models.py +194 -200
  6. splox-0.2.4/src/splox/_resources.py +42 -0
  7. splox-0.2.4/src/splox/_workflows.py +222 -0
  8. splox-0.2.4/tests/integration_test.py +126 -0
  9. splox-0.2.4/tests/test_workflows.py +388 -0
  10. splox-0.2.3/src/splox/_resources.py +0 -491
  11. splox-0.2.3/tests/integration_test.py +0 -140
  12. {splox-0.2.3 → splox-0.2.4}/.github/workflows/publish.yml +0 -0
  13. {splox-0.2.3 → splox-0.2.4}/.github/workflows/test.yml +0 -0
  14. {splox-0.2.3 → splox-0.2.4}/.gitignore +0 -0
  15. {splox-0.2.3 → splox-0.2.4}/CHANGELOG.md +0 -0
  16. {splox-0.2.3 → splox-0.2.4}/LICENSE +0 -0
  17. {splox-0.2.3 → splox-0.2.4}/README.md +0 -0
  18. {splox-0.2.3 → splox-0.2.4}/src/splox/_ids.py +0 -0
  19. {splox-0.2.3 → splox-0.2.4}/src/splox/_interactions.py +0 -0
  20. {splox-0.2.3 → splox-0.2.4}/src/splox/_mcp.py +0 -0
  21. {splox-0.2.3 → splox-0.2.4}/src/splox/_runs.py +0 -0
  22. {splox-0.2.3 → splox-0.2.4}/src/splox/_transport.py +0 -0
  23. {splox-0.2.3 → splox-0.2.4}/src/splox/exceptions.py +0 -0
  24. {splox-0.2.3 → splox-0.2.4}/src/splox/py.typed +0 -0
  25. {splox-0.2.3 → splox-0.2.4}/tests/__init__.py +0 -0
  26. {splox-0.2.3 → splox-0.2.4}/tests/integration_test_mcp.py +0 -0
  27. {splox-0.2.3 → splox-0.2.4}/tests/integration_test_v2.py +0 -0
  28. {splox-0.2.3 → splox-0.2.4}/tests/test_client.py +0 -0
  29. {splox-0.2.3 → splox-0.2.4}/tests/test_codemode_contract.py +0 -0
  30. {splox-0.2.3 → splox-0.2.4}/tests/test_exceptions.py +0 -0
  31. {splox-0.2.3 → splox-0.2.4}/tests/test_ids.py +0 -0
  32. {splox-0.2.3 → splox-0.2.4}/tests/test_models.py +0 -0
  33. {splox-0.2.3 → splox-0.2.4}/tests/test_runs.py +0 -0
  34. {splox-0.2.3 → splox-0.2.4}/tests/test_sse.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: splox
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: Official Python SDK for the Splox API — run workflows, manage chats, and monitor execution
5
5
  Project-URL: Homepage, https://splox.io
6
6
  Project-URL: Documentation, https://docs.splox.io
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "splox"
7
- version = "0.2.3"
7
+ version = "0.2.4"
8
8
  description = "Official Python SDK for the Splox API — run workflows, manage chats, and monitor execution"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -4,8 +4,11 @@ from splox._client import AsyncSploxClient, SploxClient
4
4
  from splox._ids import decode_id, encode_id
5
5
  from splox._models import (
6
6
  ContentPart,
7
- Edge,
8
- EntryNodesResponse,
7
+ GraphEdge,
8
+ GraphEdgeInput,
9
+ GraphNode,
10
+ GraphNodeInput,
11
+ GraphPosition,
9
12
  Interaction,
10
13
  InteractionPage,
11
14
  InteractionResponse,
@@ -17,12 +20,10 @@ from splox._models import (
17
20
  MCPServerToolsResponse,
18
21
  Message,
19
22
  MessagePage,
20
- Node,
21
23
  NodeExecution,
22
24
  Output,
23
25
  OutputPage,
24
26
  PageInfo,
25
- Pagination,
26
27
  RunEvent,
27
28
  RunEventPage,
28
29
  RunFailure,
@@ -32,12 +33,11 @@ from splox._models import (
32
33
  RunUsage,
33
34
  SSEEvent,
34
35
  Workflow,
35
- WorkflowFull,
36
- WorkflowListResponse,
36
+ WorkflowGraph,
37
+ WorkflowGraphUpdate,
38
+ WorkflowPage,
37
39
  WorkflowRequest,
38
40
  WorkflowRequestFile,
39
- WorkflowVersion,
40
- WorkflowVersionListResponse,
41
41
  TERMINAL_RUN_STATUSES,
42
42
  )
43
43
  from splox._mcp import generate_connection_token, generate_connection_link
@@ -69,30 +69,31 @@ __all__ = [
69
69
  "TERMINAL_RUN_STATUSES",
70
70
  "encode_id",
71
71
  "decode_id",
72
- # v1 resources (workflows CRUD, chats, billing, memory, MCP, LLM)
73
- "Edge",
72
+ # v2 workflows (graph API)
73
+ "Workflow",
74
+ "WorkflowPage",
75
+ "WorkflowGraph",
76
+ "WorkflowGraphUpdate",
77
+ "GraphPosition",
78
+ "GraphNode",
79
+ "GraphEdge",
80
+ "GraphNodeInput",
81
+ "GraphEdgeInput",
82
+ # v1 resources (MCP, legacy stream events)
74
83
  "MCPCatalogItem",
75
84
  "MCPCatalogListResponse",
76
85
  "MCPConnection",
77
86
  "MCPConnectionListResponse",
78
87
  "MCPServerToolOption",
79
88
  "MCPServerToolsResponse",
80
- "Node",
81
89
  "NodeExecution",
82
- "Pagination",
83
90
  "SSEEvent",
84
- "EntryNodesResponse",
85
- "Workflow",
86
- "WorkflowFull",
87
- "WorkflowListResponse",
88
91
  "WorkflowRequest",
89
92
  "WorkflowRequestFile",
90
- "WorkflowVersion",
91
- "WorkflowVersionListResponse",
92
93
  "generate_connection_token",
93
94
  "generate_connection_link",
94
95
  "notify",
95
96
  "async_notify",
96
97
  ]
97
98
 
98
- __version__ = "0.2.3"
99
+ __version__ = "0.2.4"
@@ -5,14 +5,12 @@ from __future__ import annotations
5
5
  import os
6
6
  from typing import Optional, Any
7
7
 
8
- from splox._resources import (
9
- AsyncWorkflows,
10
- Workflows,
11
- )
8
+ from splox._resources import async_notify, notify
12
9
  from splox._interactions import AsyncInteractions, Interactions
13
10
  from splox._mcp import AsyncMCP, MCP
14
11
  from splox._runs import AsyncRuns, Runs
15
12
  from splox._transport import DEFAULT_TIMEOUT, AsyncTransport, SyncTransport
13
+ from splox._workflows import AsyncWorkflows, Workflows
16
14
 
17
15
 
18
16
  class SploxClient:
@@ -101,206 +101,6 @@ class NodeExecution:
101
101
  )
102
102
 
103
103
 
104
- @dataclass
105
- class Pagination:
106
- """Cursor-based pagination info."""
107
-
108
- limit: int
109
- next_cursor: Optional[str] = None
110
- has_more: bool = False
111
-
112
- @classmethod
113
- def from_dict(cls, data: Dict[str, Any]) -> Pagination:
114
- return cls(
115
- limit=data["limit"],
116
- next_cursor=data.get("next_cursor"),
117
- has_more=data.get("has_more", False),
118
- )
119
-
120
-
121
- # --- Workflow / Version / Node / Edge models ---
122
-
123
-
124
- @dataclass
125
- class WorkflowVersion:
126
- """Represents a specific version of a workflow."""
127
-
128
- id: str
129
- workflow_id: str
130
- version_number: int
131
- name: str
132
- status: str
133
- created_at: Optional[str] = None
134
- updated_at: Optional[str] = None
135
- description: Optional[str] = None
136
- metadata: Optional[Dict[str, Any]] = None
137
-
138
- @classmethod
139
- def from_dict(cls, data: Dict[str, Any]) -> WorkflowVersion:
140
- return cls(
141
- id=data["id"],
142
- workflow_id=data["workflow_id"],
143
- version_number=data["version_number"],
144
- name=data["name"],
145
- status=data["status"],
146
- created_at=data.get("created_at"),
147
- updated_at=data.get("updated_at"),
148
- description=data.get("description"),
149
- metadata=data.get("metadata"),
150
- )
151
-
152
-
153
- @dataclass
154
- class Workflow:
155
- """Represents a workflow."""
156
-
157
- id: str
158
- user_id: str
159
- created_at: Optional[str] = None
160
- updated_at: Optional[str] = None
161
- latest_version: Optional[WorkflowVersion] = None
162
- is_public: Optional[bool] = None
163
-
164
- @classmethod
165
- def from_dict(cls, data: Dict[str, Any]) -> Workflow:
166
- lv = None
167
- if data.get("latest_version"):
168
- lv = WorkflowVersion.from_dict(data["latest_version"])
169
- return cls(
170
- id=data["id"],
171
- user_id=data["user_id"],
172
- created_at=data.get("created_at"),
173
- updated_at=data.get("updated_at"),
174
- latest_version=lv,
175
- is_public=data.get("is_public"),
176
- )
177
-
178
-
179
- @dataclass
180
- class Node:
181
- """Represents a node in a workflow."""
182
-
183
- id: str
184
- workflow_version_id: str
185
- node_type: str
186
- label: str
187
- pos_x: Optional[float] = None
188
- pos_y: Optional[float] = None
189
- parent_id: Optional[str] = None
190
- extent: Optional[str] = None
191
- data: Optional[Dict[str, Any]] = None
192
- created_at: Optional[str] = None
193
- updated_at: Optional[str] = None
194
-
195
- @classmethod
196
- def from_dict(cls, data: Dict[str, Any]) -> Node:
197
- return cls(
198
- id=data["id"],
199
- workflow_version_id=data["workflow_version_id"],
200
- node_type=data["node_type"],
201
- label=data["label"],
202
- pos_x=data.get("pos_x"),
203
- pos_y=data.get("pos_y"),
204
- parent_id=data.get("parent_id"),
205
- extent=data.get("extent"),
206
- data=data.get("data"),
207
- created_at=data.get("created_at"),
208
- updated_at=data.get("updated_at"),
209
- )
210
-
211
-
212
- @dataclass
213
- class Edge:
214
- """Represents an edge (connection) between nodes."""
215
-
216
- id: str
217
- workflow_version_id: str
218
- source: str
219
- target: str
220
- edge_type: str
221
- source_handle: Optional[str] = None
222
- data: Optional[Dict[str, Any]] = None
223
- created_at: Optional[str] = None
224
- updated_at: Optional[str] = None
225
-
226
- @classmethod
227
- def from_dict(cls, data: Dict[str, Any]) -> Edge:
228
- return cls(
229
- id=data["id"],
230
- workflow_version_id=data["workflow_version_id"],
231
- source=data["source"],
232
- target=data["target"],
233
- edge_type=data["edge_type"],
234
- source_handle=data.get("source_handle"),
235
- data=data.get("data"),
236
- created_at=data.get("created_at"),
237
- updated_at=data.get("updated_at"),
238
- )
239
-
240
-
241
- @dataclass
242
- class WorkflowFull:
243
- """Full workflow with version, nodes, and edges."""
244
-
245
- workflow: Workflow
246
- workflow_version: WorkflowVersion
247
- nodes: List[Node] = field(default_factory=list)
248
- edges: List[Edge] = field(default_factory=list)
249
-
250
- @classmethod
251
- def from_dict(cls, data: Dict[str, Any]) -> WorkflowFull:
252
- return cls(
253
- workflow=Workflow.from_dict(data["workflow"]),
254
- workflow_version=WorkflowVersion.from_dict(data["workflow_version"]),
255
- nodes=[Node.from_dict(n) for n in (data.get("nodes") or [])],
256
- edges=[Edge.from_dict(e) for e in (data.get("edges") or [])],
257
- )
258
-
259
-
260
- # --- Response wrappers ---
261
-
262
-
263
- @dataclass
264
- class WorkflowListResponse:
265
- """Response from listing workflows."""
266
-
267
- workflows: List[Workflow]
268
- pagination: Pagination
269
-
270
- @classmethod
271
- def from_dict(cls, data: Dict[str, Any]) -> WorkflowListResponse:
272
- return cls(
273
- workflows=[Workflow.from_dict(w) for w in (data.get("workflows") or [])],
274
- pagination=Pagination.from_dict(data["pagination"]),
275
- )
276
-
277
-
278
- @dataclass
279
- class EntryNodesResponse:
280
- """Response from getting entry nodes."""
281
-
282
- nodes: List[Node]
283
-
284
- @classmethod
285
- def from_dict(cls, data: Dict[str, Any]) -> EntryNodesResponse:
286
- return cls(
287
- nodes=[Node.from_dict(n) for n in (data.get("nodes") or [])],
288
- )
289
-
290
-
291
- @dataclass
292
- class WorkflowVersionListResponse:
293
- """Response from listing workflow versions."""
294
-
295
- versions: List[WorkflowVersion]
296
-
297
- @classmethod
298
- def from_dict(cls, data: Dict[str, Any]) -> WorkflowVersionListResponse:
299
- return cls(
300
- versions=[WorkflowVersion.from_dict(v) for v in (data.get("versions") or [])],
301
- )
302
-
303
-
304
104
  @dataclass
305
105
  class SSEEvent:
306
106
  """A single Server-Sent Event from a listen stream.
@@ -981,3 +781,197 @@ class InteractionPage:
981
781
  data=[Interaction.from_dict(i) for i in (raw.get("data") or [])],
982
782
  page=PageInfo.from_dict(raw.get("page") or {}),
983
783
  )
784
+
785
+
786
+ # ---------------------------------------------------------------------------
787
+ # v2 workflows (graph API)
788
+ # ---------------------------------------------------------------------------
789
+
790
+
791
+ @dataclass
792
+ class Workflow:
793
+ """A v2 workflow (management surface; graphs live on versions)."""
794
+
795
+ id: str
796
+ name: str
797
+ description: str = ""
798
+ created_at: str = ""
799
+ updated_at: str = ""
800
+
801
+ @classmethod
802
+ def from_dict(cls, data: Dict[str, Any]) -> Workflow:
803
+ return cls(
804
+ id=data["id"],
805
+ name=data["name"],
806
+ description=data.get("description") or "",
807
+ created_at=data.get("created_at", ""),
808
+ updated_at=data.get("updated_at", ""),
809
+ )
810
+
811
+
812
+ @dataclass
813
+ class WorkflowPage:
814
+ """One page of workflows (newest first)."""
815
+
816
+ data: List[Workflow]
817
+ page: PageInfo
818
+
819
+ @classmethod
820
+ def from_dict(cls, raw: Dict[str, Any]) -> WorkflowPage:
821
+ return cls(
822
+ data=[Workflow.from_dict(w) for w in (raw.get("data") or [])],
823
+ page=PageInfo.from_dict(raw.get("page") or {}),
824
+ )
825
+
826
+
827
+ @dataclass
828
+ class GraphPosition:
829
+ """Canvas position of a graph node."""
830
+
831
+ x: float = 0.0
832
+ y: float = 0.0
833
+
834
+ @classmethod
835
+ def from_dict(cls, data: Dict[str, Any]) -> GraphPosition:
836
+ return cls(x=data.get("x", 0.0), y=data.get("y", 0.0))
837
+
838
+ def to_dict(self) -> Dict[str, Any]:
839
+ return {"x": self.x, "y": self.y}
840
+
841
+
842
+ @dataclass
843
+ class GraphNode:
844
+ """A node of a workflow version's graph.
845
+
846
+ ``id`` is the server-assigned node UUID (graph-local, not an API resource
847
+ id); ``data`` is the raw node configuration document exactly as stored.
848
+ """
849
+
850
+ id: str
851
+ type: str
852
+ label: str = ""
853
+ position: GraphPosition = field(default_factory=GraphPosition)
854
+ data: Dict[str, Any] = field(default_factory=dict)
855
+
856
+ @classmethod
857
+ def from_dict(cls, data: Dict[str, Any]) -> GraphNode:
858
+ return cls(
859
+ id=data["id"],
860
+ type=data["type"],
861
+ label=data.get("label", ""),
862
+ position=GraphPosition.from_dict(data.get("position") or {}),
863
+ data=data.get("data") or {},
864
+ )
865
+
866
+
867
+ @dataclass
868
+ class GraphEdge:
869
+ """An edge of a workflow version's graph.
870
+
871
+ ``edge_type``: ``parallel`` edges drive execution flow; ``tool`` edges
872
+ attach tool / sub-agent nodes to an agent; ``error`` edges route
873
+ failures.
874
+ """
875
+
876
+ id: str
877
+ source: str
878
+ target: str
879
+ edge_type: str
880
+ source_handle: Optional[str] = None
881
+ data: Dict[str, Any] = field(default_factory=dict)
882
+
883
+ @classmethod
884
+ def from_dict(cls, data: Dict[str, Any]) -> GraphEdge:
885
+ return cls(
886
+ id=data["id"],
887
+ source=data["source"],
888
+ target=data["target"],
889
+ edge_type=data["edge_type"],
890
+ source_handle=data.get("source_handle"),
891
+ data=data.get("data") or {},
892
+ )
893
+
894
+
895
+ @dataclass
896
+ class WorkflowGraph:
897
+ """The graph of a workflow's presented (active) version."""
898
+
899
+ workflow_version: str
900
+ nodes: List[GraphNode] = field(default_factory=list)
901
+ edges: List[GraphEdge] = field(default_factory=list)
902
+
903
+ @classmethod
904
+ def from_dict(cls, data: Dict[str, Any]) -> WorkflowGraph:
905
+ return cls(
906
+ workflow_version=data["workflow_version"],
907
+ nodes=[GraphNode.from_dict(n) for n in (data.get("nodes") or [])],
908
+ edges=[GraphEdge.from_dict(e) for e in (data.get("edges") or [])],
909
+ )
910
+
911
+
912
+ @dataclass
913
+ class GraphNodeInput:
914
+ """Node input for ``workflows.set_graph``.
915
+
916
+ ``id`` is a client-chosen key, unique within the request; edges reference
917
+ nodes by these keys. ``data`` omitted means the server defaults for the
918
+ node type.
919
+ """
920
+
921
+ type: str
922
+ id: Optional[str] = None
923
+ label: Optional[str] = None
924
+ position: Optional[GraphPosition] = None
925
+ data: Optional[Dict[str, Any]] = None
926
+
927
+ def to_dict(self) -> Dict[str, Any]:
928
+ d: Dict[str, Any] = {"type": self.type}
929
+ if self.id is not None:
930
+ d["id"] = self.id
931
+ if self.label is not None:
932
+ d["label"] = self.label
933
+ if self.position is not None:
934
+ d["position"] = self.position.to_dict()
935
+ if self.data is not None:
936
+ d["data"] = self.data
937
+ return d
938
+
939
+
940
+ @dataclass
941
+ class GraphEdgeInput:
942
+ """Edge input for ``workflows.set_graph``.
943
+
944
+ ``source``/``target`` are client node keys (``nodes[].id``). ``edge_type``
945
+ defaults to ``tool`` when the target is a tool node, else ``parallel``.
946
+ """
947
+
948
+ source: str
949
+ target: str
950
+ edge_type: Optional[str] = None
951
+ source_handle: Optional[str] = None
952
+ data: Optional[Dict[str, Any]] = None
953
+
954
+ def to_dict(self) -> Dict[str, Any]:
955
+ d: Dict[str, Any] = {"source": self.source, "target": self.target}
956
+ if self.edge_type is not None:
957
+ d["edge_type"] = self.edge_type
958
+ if self.source_handle is not None:
959
+ d["source_handle"] = self.source_handle
960
+ if self.data is not None:
961
+ d["data"] = self.data
962
+ return d
963
+
964
+
965
+ @dataclass
966
+ class WorkflowGraphUpdate:
967
+ """Result of ``workflows.set_graph``: the new version and node id map."""
968
+
969
+ workflow_version: str
970
+ node_ids: Dict[str, str] = field(default_factory=dict)
971
+
972
+ @classmethod
973
+ def from_dict(cls, data: Dict[str, Any]) -> WorkflowGraphUpdate:
974
+ return cls(
975
+ workflow_version=data["workflow_version"],
976
+ node_ids=data.get("node_ids") or {},
977
+ )
@@ -0,0 +1,42 @@
1
+ """Webhook notification helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+
8
+ def notify(webhook_url: str, data: Any) -> None:
9
+ """POST data to a webhook URL as JSON.
10
+
11
+ Args:
12
+ webhook_url: The URL to POST to.
13
+ data: Any JSON-serialisable payload.
14
+ """
15
+ import urllib.request, json as _json
16
+ body = _json.dumps(data).encode()
17
+ req = urllib.request.Request(
18
+ webhook_url,
19
+ data=body,
20
+ headers={"Content-Type": "application/json"},
21
+ method="POST",
22
+ )
23
+ with urllib.request.urlopen(req, timeout=10):
24
+ pass
25
+
26
+
27
+ async def async_notify(webhook_url: str, data: Any) -> None:
28
+ """Async POST data to a webhook URL as JSON.
29
+
30
+ Args:
31
+ webhook_url: The URL to POST to.
32
+ data: Any JSON-serialisable payload.
33
+ """
34
+ import json as _json
35
+ try:
36
+ import aiohttp
37
+ async with aiohttp.ClientSession() as session:
38
+ await session.post(webhook_url, json=data, timeout=aiohttp.ClientTimeout(total=10))
39
+ except ImportError:
40
+ import asyncio
41
+ loop = asyncio.get_event_loop()
42
+ await loop.run_in_executor(None, notify, webhook_url, data)