codini 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codini/__init__.py +298 -0
- codini-0.2.0.dist-info/METADATA +129 -0
- codini-0.2.0.dist-info/RECORD +5 -0
- codini-0.2.0.dist-info/WHEEL +5 -0
- codini-0.2.0.dist-info/top_level.txt +1 -0
codini/__init__.py
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Codini Python SDK
|
|
3
|
+
|
|
4
|
+
Official Python SDK for the Codini AI API.
|
|
5
|
+
Build, manage, and execute AI flows programmatically.
|
|
6
|
+
|
|
7
|
+
pip install codini
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
from codini import CodiniClient
|
|
11
|
+
|
|
12
|
+
codini = CodiniClient("your-api-key")
|
|
13
|
+
result = codini.sync_execute(1, input="Hello")
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import time
|
|
19
|
+
import json
|
|
20
|
+
from typing import Any, Optional
|
|
21
|
+
from urllib.request import Request, urlopen
|
|
22
|
+
from urllib.error import HTTPError
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CodiniAPIError(Exception):
|
|
26
|
+
"""Raised when the Codini API returns a non-2xx response."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, status_code: int, message: str):
|
|
29
|
+
self.status_code = status_code
|
|
30
|
+
self.message = message
|
|
31
|
+
super().__init__(f"Codini API error ({status_code}): {message}")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _request(base_url: str, headers: dict, path: str, method: str = "GET", body: Any = None) -> Any:
|
|
35
|
+
url = f"{base_url}{path}"
|
|
36
|
+
data = json.dumps(body).encode("utf-8") if body is not None else None
|
|
37
|
+
|
|
38
|
+
req = Request(url, data=data, headers=headers, method=method)
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
with urlopen(req) as resp:
|
|
42
|
+
content = resp.read().decode("utf-8")
|
|
43
|
+
if content:
|
|
44
|
+
return json.loads(content)
|
|
45
|
+
return {}
|
|
46
|
+
except HTTPError as e:
|
|
47
|
+
error_body = e.read().decode("utf-8")
|
|
48
|
+
try:
|
|
49
|
+
error_data = json.loads(error_body)
|
|
50
|
+
msg = error_data.get("message") or error_data.get("error") or e.reason
|
|
51
|
+
except (json.JSONDecodeError, ValueError):
|
|
52
|
+
msg = e.reason
|
|
53
|
+
raise CodiniAPIError(e.code, msg) from None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ProjectsAPI:
|
|
57
|
+
"""Manage projects that contain flows."""
|
|
58
|
+
|
|
59
|
+
def __init__(self, base_url: str, headers: dict):
|
|
60
|
+
self._base_url = base_url
|
|
61
|
+
self._headers = headers
|
|
62
|
+
|
|
63
|
+
def _req(self, path: str, method: str = "GET", body: Any = None) -> Any:
|
|
64
|
+
return _request(self._base_url, self._headers, path, method, body)
|
|
65
|
+
|
|
66
|
+
def list(self) -> dict:
|
|
67
|
+
"""List all projects."""
|
|
68
|
+
return self._req("/projects/list/", "GET")
|
|
69
|
+
|
|
70
|
+
def create(self, name: str, description: str = "") -> dict:
|
|
71
|
+
"""Create a new project."""
|
|
72
|
+
return self._req("/projects/create/", "POST", {"name": name, "description": description})
|
|
73
|
+
|
|
74
|
+
def get(self, project_id: int) -> dict:
|
|
75
|
+
"""Get project details including its flows."""
|
|
76
|
+
return self._req(f"/projects/{project_id}/", "GET")
|
|
77
|
+
|
|
78
|
+
def update(self, project_id: int, name: str = None, description: str = None) -> dict:
|
|
79
|
+
"""Update project name and/or description."""
|
|
80
|
+
body = {}
|
|
81
|
+
if name is not None:
|
|
82
|
+
body["name"] = name
|
|
83
|
+
if description is not None:
|
|
84
|
+
body["description"] = description
|
|
85
|
+
return self._req(f"/projects/{project_id}/update/", "POST", body)
|
|
86
|
+
|
|
87
|
+
def delete(self, project_id: int) -> dict:
|
|
88
|
+
"""Delete a project and all its flows."""
|
|
89
|
+
return self._req(f"/projects/{project_id}/delete/", "DELETE")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class FlowsAPI:
|
|
93
|
+
"""Manage flows — CRUD, programmatic building, and node catalog."""
|
|
94
|
+
|
|
95
|
+
def __init__(self, base_url: str, headers: dict):
|
|
96
|
+
self._base_url = base_url
|
|
97
|
+
self._headers = headers
|
|
98
|
+
|
|
99
|
+
def _req(self, path: str, method: str = "GET", body: Any = None) -> Any:
|
|
100
|
+
return _request(self._base_url, self._headers, path, method, body)
|
|
101
|
+
|
|
102
|
+
def list(self, project_id: int = None) -> dict:
|
|
103
|
+
"""List flows. Pass project_id to filter, or omit for all flows."""
|
|
104
|
+
body = {"project_id": project_id} if project_id else None
|
|
105
|
+
return self._req("/flows/list/", "POST", body)
|
|
106
|
+
|
|
107
|
+
def get(self, flow_id: int) -> dict:
|
|
108
|
+
"""Get full flow details including nodes and edges."""
|
|
109
|
+
return self._req(f"/flows/{flow_id}/", "POST")
|
|
110
|
+
|
|
111
|
+
def create(self, name: str, project_id: int, description: str = "") -> dict:
|
|
112
|
+
"""Create a new empty flow within a project."""
|
|
113
|
+
return self._req("/flows/create/", "POST", {
|
|
114
|
+
"name": name,
|
|
115
|
+
"project_id": project_id,
|
|
116
|
+
"description": description,
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
def update_details(self, flow_id: int, name: str = None, description: str = None) -> dict:
|
|
120
|
+
"""Update flow name and/or description."""
|
|
121
|
+
body = {}
|
|
122
|
+
if name is not None:
|
|
123
|
+
body["name"] = name
|
|
124
|
+
if description is not None:
|
|
125
|
+
body["description"] = description
|
|
126
|
+
return self._req(f"/flows/{flow_id}/update-details/", "POST", body)
|
|
127
|
+
|
|
128
|
+
def save(self, flow_id: int, nodes: list, edges: list, name: str = None, description: str = None, ui_state: dict = None) -> dict:
|
|
129
|
+
"""Save full flow state — replaces all nodes and edges."""
|
|
130
|
+
body: dict[str, Any] = {"nodes": nodes, "edges": edges}
|
|
131
|
+
if name is not None:
|
|
132
|
+
body["name"] = name
|
|
133
|
+
if description is not None:
|
|
134
|
+
body["description"] = description
|
|
135
|
+
if ui_state is not None:
|
|
136
|
+
body["ui_state"] = ui_state
|
|
137
|
+
return self._req(f"/flows/{flow_id}/update/", "POST", body)
|
|
138
|
+
|
|
139
|
+
def delete(self, flow_id: int) -> dict:
|
|
140
|
+
"""Delete a flow."""
|
|
141
|
+
return self._req(f"/flows/{flow_id}/delete/", "DELETE")
|
|
142
|
+
|
|
143
|
+
def add_nodes(self, flow_id: int, nodes: list[dict]) -> dict:
|
|
144
|
+
"""
|
|
145
|
+
Programmatically add nodes to a flow. Appends without deleting existing nodes.
|
|
146
|
+
|
|
147
|
+
Each node: {"type": "create-agent", "position": {"x": 0, "y": 0}, "params": {"name": "...", ...}}
|
|
148
|
+
|
|
149
|
+
Returns created node IDs.
|
|
150
|
+
"""
|
|
151
|
+
return self._req("/flows/create-api/", "POST", {
|
|
152
|
+
"flow_id": flow_id,
|
|
153
|
+
"nodes": nodes,
|
|
154
|
+
"edges": [],
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
def add_edges(self, flow_id: int, edges: list[dict]) -> dict:
|
|
158
|
+
"""
|
|
159
|
+
Programmatically add edges between existing nodes.
|
|
160
|
+
|
|
161
|
+
Each edge: {"source": "node-id", "target": "node-id", "sourceHandle": "...", "targetHandle": "..."}
|
|
162
|
+
"""
|
|
163
|
+
return self._req("/flows/create-api/", "POST", {
|
|
164
|
+
"flow_id": flow_id,
|
|
165
|
+
"nodes": [],
|
|
166
|
+
"edges": edges,
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
def build(self, flow_id: int, nodes: list[dict], edges: list[dict]) -> dict:
|
|
170
|
+
"""Add both nodes and edges in a single call."""
|
|
171
|
+
return self._req("/flows/create-api/", "POST", {
|
|
172
|
+
"flow_id": flow_id,
|
|
173
|
+
"nodes": nodes,
|
|
174
|
+
"edges": edges,
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
def list_node_types(self) -> list:
|
|
178
|
+
"""List all available node types with their categories."""
|
|
179
|
+
return self._req("/flow-nodes/types", "GET")
|
|
180
|
+
|
|
181
|
+
def get_node_definitions(self, node_types: list[str]) -> list:
|
|
182
|
+
"""Fetch full definitions for specific node types."""
|
|
183
|
+
return self._req("/flow-nodes/select-types", "POST", {
|
|
184
|
+
"node_types": node_types,
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class SecretsAPI:
|
|
189
|
+
"""Store and retrieve credentials for use in flow nodes."""
|
|
190
|
+
|
|
191
|
+
def __init__(self, base_url: str, headers: dict):
|
|
192
|
+
self._base_url = base_url
|
|
193
|
+
self._headers = headers
|
|
194
|
+
|
|
195
|
+
def _req(self, path: str, method: str = "GET", body: Any = None) -> Any:
|
|
196
|
+
return _request(self._base_url, self._headers, path, method, body)
|
|
197
|
+
|
|
198
|
+
def list(self) -> dict:
|
|
199
|
+
"""List all secrets."""
|
|
200
|
+
return self._req("/secrets/", "POST")
|
|
201
|
+
|
|
202
|
+
def create(self, secret_name: str, secret_data: str) -> dict:
|
|
203
|
+
"""Create or update a secret."""
|
|
204
|
+
return self._req("/secrets/create/", "POST", {
|
|
205
|
+
"secret_name": secret_name,
|
|
206
|
+
"secret_data": secret_data,
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
def get(self, secret_name: str) -> dict:
|
|
210
|
+
"""Retrieve a secret value by name."""
|
|
211
|
+
return self._req(f"/secrets/{secret_name}/", "GET")
|
|
212
|
+
|
|
213
|
+
def delete(self, secret_name: str) -> dict:
|
|
214
|
+
"""Delete a secret by name."""
|
|
215
|
+
return self._req(f"/secrets/{secret_name}/delete/", "DELETE")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class CodiniClient:
|
|
219
|
+
"""
|
|
220
|
+
Codini API client — server-side, requires an API key.
|
|
221
|
+
|
|
222
|
+
Usage:
|
|
223
|
+
codini = CodiniClient("your-api-key")
|
|
224
|
+
|
|
225
|
+
# Execute flows
|
|
226
|
+
result = codini.sync_execute(1, input="Hello")
|
|
227
|
+
|
|
228
|
+
# Manage projects and flows
|
|
229
|
+
codini.projects.create("My App")
|
|
230
|
+
codini.flows.build(flow_id, nodes, edges)
|
|
231
|
+
|
|
232
|
+
# Store secrets
|
|
233
|
+
codini.secrets.create("my_key", "my_value")
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
def __init__(self, api_key: str, base_url: str = "http://api.codini.ai.local/api/v1"):
|
|
237
|
+
self._base_url = base_url
|
|
238
|
+
self._headers = {
|
|
239
|
+
"Content-Type": "application/json",
|
|
240
|
+
"Authorization": api_key,
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
self.projects = ProjectsAPI(base_url, self._headers)
|
|
244
|
+
self.flows = FlowsAPI(base_url, self._headers)
|
|
245
|
+
self.secrets = SecretsAPI(base_url, self._headers)
|
|
246
|
+
|
|
247
|
+
def _req(self, path: str, method: str = "GET", body: Any = None) -> Any:
|
|
248
|
+
return _request(self._base_url, self._headers, path, method, body)
|
|
249
|
+
|
|
250
|
+
# ---- Tickets ----
|
|
251
|
+
|
|
252
|
+
def create_ticket(self, **kwargs) -> dict:
|
|
253
|
+
"""
|
|
254
|
+
Create an execution ticket for client use.
|
|
255
|
+
|
|
256
|
+
Required: flow_id
|
|
257
|
+
Optional: use_websocket, allowed_flows, rpm, max_runs,
|
|
258
|
+
max_credits_usage, expires_in_seconds, single_use,
|
|
259
|
+
input, variables, webhook_url, webhook_events_subscribed,
|
|
260
|
+
webhook_secret, metadata
|
|
261
|
+
"""
|
|
262
|
+
return self._req("/execution-tickets/create/", "POST", kwargs)
|
|
263
|
+
|
|
264
|
+
# ---- Execution ----
|
|
265
|
+
|
|
266
|
+
def sync_execute(self, flow_id: int, input: str = None, variables: dict = None, timeout: int = None) -> dict:
|
|
267
|
+
"""Execute a flow synchronously — blocks until complete."""
|
|
268
|
+
body: dict[str, Any] = {}
|
|
269
|
+
if input is not None:
|
|
270
|
+
body["input"] = input
|
|
271
|
+
if variables is not None:
|
|
272
|
+
body["variables"] = variables
|
|
273
|
+
if timeout is not None:
|
|
274
|
+
body["timeout"] = timeout
|
|
275
|
+
return self._req(f"/flows/{flow_id}/execute-sync/", "POST", body)
|
|
276
|
+
|
|
277
|
+
def async_execute(self, flow_id: int, input: str = None, variables: dict = None) -> dict:
|
|
278
|
+
"""Execute a flow asynchronously — returns immediately with a run_id."""
|
|
279
|
+
body: dict[str, Any] = {}
|
|
280
|
+
if input is not None:
|
|
281
|
+
body["input"] = input
|
|
282
|
+
if variables is not None:
|
|
283
|
+
body["variables"] = variables
|
|
284
|
+
return self._req(f"/flows/{flow_id}/execute/", "POST", body)
|
|
285
|
+
|
|
286
|
+
def get_run_status(self, run_id: int) -> dict:
|
|
287
|
+
"""Get the status of a run."""
|
|
288
|
+
return self._req(f"/runs/{run_id}/", "GET")
|
|
289
|
+
|
|
290
|
+
def poll_run_status(self, run_id: int, interval: float = 2.0, max_attempts: int = 150) -> dict:
|
|
291
|
+
"""Poll a run until it completes or fails."""
|
|
292
|
+
for _ in range(max_attempts):
|
|
293
|
+
result = self.get_run_status(run_id)
|
|
294
|
+
status = result.get("data", {}).get("run_status")
|
|
295
|
+
if status in ("COMPLETED", "FAILED"):
|
|
296
|
+
return result
|
|
297
|
+
time.sleep(interval)
|
|
298
|
+
raise TimeoutError(f"Polling timed out after {max_attempts} attempts for run {run_id}")
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: codini
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Official Python SDK for the Codini AI API
|
|
5
|
+
License: ISC
|
|
6
|
+
Project-URL: Homepage, https://codini.ai
|
|
7
|
+
Project-URL: Repository, https://github.com/codini/codini-python
|
|
8
|
+
Project-URL: Documentation, https://www.npmjs.com/package/codini
|
|
9
|
+
Keywords: codini,ai,sdk,api,flows,agents
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Requires-Python: >=3.8
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# codini
|
|
23
|
+
|
|
24
|
+
Official Python SDK for the [Codini AI](https://codini.ai) API.
|
|
25
|
+
|
|
26
|
+
Build, manage, and execute AI flows programmatically. Zero dependencies.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install codini
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from codini import CodiniClient
|
|
38
|
+
|
|
39
|
+
codini = CodiniClient("your-api-key")
|
|
40
|
+
|
|
41
|
+
# Execute a flow
|
|
42
|
+
result = codini.sync_execute(1, input="Hello")
|
|
43
|
+
print(result["data"]["output"])
|
|
44
|
+
|
|
45
|
+
# Build a flow programmatically
|
|
46
|
+
project = codini.projects.create("My App")
|
|
47
|
+
flow = codini.flows.create("Chat Agent", project["data"]["id"])
|
|
48
|
+
|
|
49
|
+
nodes = codini.flows.add_nodes(flow["data"]["id"], [
|
|
50
|
+
{"type": "onMessageTrigger", "position": {"x": 0, "y": 200}},
|
|
51
|
+
{"type": "create-agent", "position": {"x": 300, "y": 200}, "params": {
|
|
52
|
+
"name": "Assistant", "model": "gpt-4o", "type": "auto",
|
|
53
|
+
"role description": "Helpful assistant"
|
|
54
|
+
}},
|
|
55
|
+
{"type": "replyToChat", "position": {"x": 600, "y": 200}},
|
|
56
|
+
])
|
|
57
|
+
|
|
58
|
+
trigger, agent, reply = nodes["data"]["nodes"]
|
|
59
|
+
codini.flows.add_edges(flow["data"]["id"], [
|
|
60
|
+
{"source": trigger["id"], "target": agent["id"]},
|
|
61
|
+
{"source": agent["id"], "target": reply["id"]},
|
|
62
|
+
])
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## API
|
|
66
|
+
|
|
67
|
+
### Execution
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
codini.create_ticket(flow_id=1, use_websocket=True)
|
|
71
|
+
codini.sync_execute(flow_id, input="Hello", variables={"lang": "en"})
|
|
72
|
+
codini.async_execute(flow_id, input="Hello")
|
|
73
|
+
codini.get_run_status(run_id)
|
|
74
|
+
codini.poll_run_status(run_id, interval=2.0, max_attempts=150)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Projects
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
codini.projects.list()
|
|
81
|
+
codini.projects.create("My App", description="Optional")
|
|
82
|
+
codini.projects.get(project_id)
|
|
83
|
+
codini.projects.update(project_id, name="New Name")
|
|
84
|
+
codini.projects.delete(project_id)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Flows
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
codini.flows.list() # All flows
|
|
91
|
+
codini.flows.list(project_id=38) # Flows in a project
|
|
92
|
+
codini.flows.get(flow_id)
|
|
93
|
+
codini.flows.create("My Flow", project_id=38)
|
|
94
|
+
codini.flows.update_details(flow_id, name="New Name")
|
|
95
|
+
codini.flows.delete(flow_id)
|
|
96
|
+
|
|
97
|
+
# Programmatic building
|
|
98
|
+
codini.flows.add_nodes(flow_id, [{...}])
|
|
99
|
+
codini.flows.add_edges(flow_id, [{...}])
|
|
100
|
+
codini.flows.build(flow_id, nodes, edges)
|
|
101
|
+
|
|
102
|
+
# Node catalog
|
|
103
|
+
codini.flows.list_node_types()
|
|
104
|
+
codini.flows.get_node_definitions(["create-agent", "chat-memory"])
|
|
105
|
+
|
|
106
|
+
# Full canvas save
|
|
107
|
+
codini.flows.save(flow_id, nodes=[...], edges=[...])
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Secrets
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
codini.secrets.list()
|
|
114
|
+
codini.secrets.create("my_key", "my_value")
|
|
115
|
+
codini.secrets.get("my_key")
|
|
116
|
+
codini.secrets.delete("my_key")
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Zero Dependencies
|
|
120
|
+
|
|
121
|
+
Uses only Python standard library (`urllib`, `json`). No `requests` needed.
|
|
122
|
+
|
|
123
|
+
## Requirements
|
|
124
|
+
|
|
125
|
+
Python 3.8+
|
|
126
|
+
|
|
127
|
+
## License
|
|
128
|
+
|
|
129
|
+
ISC
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
codini/__init__.py,sha256=QRzNlfn6o5AavXj801bIlKsq38Z8io9CCTQiGYHzvn4,10815
|
|
2
|
+
codini-0.2.0.dist-info/METADATA,sha256=EYz_IntizJyouwxLCKIA1r6C-tffVntEFgx_Mnc60Ac,3398
|
|
3
|
+
codini-0.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
4
|
+
codini-0.2.0.dist-info/top_level.txt,sha256=R-mw0grWxVWM9zmD5KX9MjWO5DLGTrPeEq10WymgtYw,7
|
|
5
|
+
codini-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
codini
|