noclick 0.1.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.
noclick-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,93 @@
1
+ Metadata-Version: 2.4
2
+ Name: noclick
3
+ Version: 0.1.0
4
+ Summary: Python SDK for NoClick workflow automation
5
+ License: BUSL-1.1
6
+ Project-URL: Homepage, https://docs.noclick.com/sdk/overview
7
+ Project-URL: Documentation, https://docs.noclick.com/sdk/external-apps
8
+ Project-URL: Repository, https://github.com/noclickapp/noclick
9
+ Keywords: noclick,workflow,automation,sdk
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: Other/Proprietary License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: python-socketio[asyncio_client]>=5.0
20
+ Requires-Dist: aiohttp>=3.0
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest; extra == "dev"
23
+ Requires-Dist: pytest-asyncio; extra == "dev"
24
+
25
+ # noclick-sdk
26
+
27
+ Python SDK for [NoClick](https://noclick.com) workflow automation. Build external applications that interact with NoClick workflows — read node outputs, trigger execution, manage state, and work with files and datasets.
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ pip install noclick
33
+ ```
34
+
35
+ ## Quick Start
36
+
37
+ ```python
38
+ import asyncio
39
+ from noclick import NoClickSDK
40
+
41
+ async def main():
42
+ sdk = NoClickSDK(
43
+ url="https://api.noclick.io",
44
+ api_key="nk_live_...",
45
+ workflow_id="your-workflow-id",
46
+ )
47
+ await sdk.connect()
48
+
49
+ # List nodes
50
+ nodes = await sdk.nodes.list()
51
+ for node in nodes:
52
+ print(f"{node['label']} ({node['type']})")
53
+
54
+ # Read node output
55
+ output = await sdk.nodes.get_output("gmail-node-id")
56
+
57
+ # Run a node and get results
58
+ results = await sdk.execution.run_nodes_and_get_output(
59
+ ["data-fetcher"], ["formatter"]
60
+ )
61
+
62
+ # State management
63
+ await sdk.state.set("counter", 42)
64
+ val = await sdk.state.get("counter")
65
+
66
+ # Dataset CRUD
67
+ ds_id = await sdk.dataset.create("My Data")
68
+ await sdk.dataset.append_rows(ds_id, [{"name": "Alice", "score": 95}])
69
+
70
+ await sdk.disconnect()
71
+
72
+ asyncio.run(main())
73
+ ```
74
+
75
+ ## API
76
+
77
+ | Namespace | Methods |
78
+ |-----------|---------|
79
+ | `sdk.nodes` | `get_output`, `get_config`, `list` |
80
+ | `sdk.execution` | `run_nodes_and_get_output`, `run_nodes_in_background`, `on_node_output`, `on_node_state` |
81
+ | `sdk.state` | `get`, `set`, `delete`, `update`, `keys` |
82
+ | `sdk.auth` | `list_credentials`, `has_credential`, `create_credential` |
83
+ | `sdk.resources` | `upload`, `get_url`, `remove`, `list` |
84
+ | `sdk.dataset` | `create`, `list`, `get_rows`, `append_rows`, `update_row`, `delete_rows` |
85
+ | `sdk.workflow` | `get_info` |
86
+
87
+ ## Documentation
88
+
89
+ Full documentation at [docs.noclick.com/sdk](https://docs.noclick.com/sdk/overview).
90
+
91
+ ## License
92
+
93
+ [Business Source License 1.1](../LICENSE) — free to use with NoClick, converts to Apache 2.0 after 4 years.
@@ -0,0 +1,69 @@
1
+ # noclick-sdk
2
+
3
+ Python SDK for [NoClick](https://noclick.com) workflow automation. Build external applications that interact with NoClick workflows — read node outputs, trigger execution, manage state, and work with files and datasets.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install noclick
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```python
14
+ import asyncio
15
+ from noclick import NoClickSDK
16
+
17
+ async def main():
18
+ sdk = NoClickSDK(
19
+ url="https://api.noclick.io",
20
+ api_key="nk_live_...",
21
+ workflow_id="your-workflow-id",
22
+ )
23
+ await sdk.connect()
24
+
25
+ # List nodes
26
+ nodes = await sdk.nodes.list()
27
+ for node in nodes:
28
+ print(f"{node['label']} ({node['type']})")
29
+
30
+ # Read node output
31
+ output = await sdk.nodes.get_output("gmail-node-id")
32
+
33
+ # Run a node and get results
34
+ results = await sdk.execution.run_nodes_and_get_output(
35
+ ["data-fetcher"], ["formatter"]
36
+ )
37
+
38
+ # State management
39
+ await sdk.state.set("counter", 42)
40
+ val = await sdk.state.get("counter")
41
+
42
+ # Dataset CRUD
43
+ ds_id = await sdk.dataset.create("My Data")
44
+ await sdk.dataset.append_rows(ds_id, [{"name": "Alice", "score": 95}])
45
+
46
+ await sdk.disconnect()
47
+
48
+ asyncio.run(main())
49
+ ```
50
+
51
+ ## API
52
+
53
+ | Namespace | Methods |
54
+ |-----------|---------|
55
+ | `sdk.nodes` | `get_output`, `get_config`, `list` |
56
+ | `sdk.execution` | `run_nodes_and_get_output`, `run_nodes_in_background`, `on_node_output`, `on_node_state` |
57
+ | `sdk.state` | `get`, `set`, `delete`, `update`, `keys` |
58
+ | `sdk.auth` | `list_credentials`, `has_credential`, `create_credential` |
59
+ | `sdk.resources` | `upload`, `get_url`, `remove`, `list` |
60
+ | `sdk.dataset` | `create`, `list`, `get_rows`, `append_rows`, `update_row`, `delete_rows` |
61
+ | `sdk.workflow` | `get_info` |
62
+
63
+ ## Documentation
64
+
65
+ Full documentation at [docs.noclick.com/sdk](https://docs.noclick.com/sdk/overview).
66
+
67
+ ## License
68
+
69
+ [Business Source License 1.1](../LICENSE) — free to use with NoClick, converts to Apache 2.0 after 4 years.
@@ -0,0 +1,38 @@
1
+ """
2
+ @noclick/sdk — Python SDK for NoClick workflow automation.
3
+
4
+ Usage:
5
+ import asyncio
6
+ from noclick import NoClickSDK
7
+
8
+ async def main():
9
+ sdk = NoClickSDK(
10
+ url="http://localhost:8005",
11
+ api_key="nk_live_...",
12
+ workflow_id="..."
13
+ )
14
+ await sdk.connect()
15
+
16
+ # Read node output
17
+ output = await sdk.nodes.get_output("node-id")
18
+
19
+ # Run nodes
20
+ await sdk.execution.run_nodes_in_background(["node-id"])
21
+
22
+ # State CRUD
23
+ await sdk.state.set("counter", 42)
24
+ val = await sdk.state.get("counter")
25
+
26
+ # Dataset CRUD
27
+ resource_id = await sdk.dataset.create("My Data")
28
+ await sdk.dataset.append_rows(resource_id, [{"name": "Alice"}])
29
+
30
+ await sdk.disconnect()
31
+
32
+ asyncio.run(main())
33
+ """
34
+
35
+ from noclick.client import NoClickSDK
36
+
37
+ __all__ = ["NoClickSDK"]
38
+ __version__ = "0.1.0"
@@ -0,0 +1,140 @@
1
+ """
2
+ Main SDK client. Connects to NoClick backend via Socket.IO.
3
+ """
4
+
5
+ import asyncio
6
+ import json
7
+ import logging
8
+ import uuid
9
+ from typing import Any, Dict, List, Optional
10
+
11
+ import socketio
12
+
13
+ from noclick.namespaces import (
14
+ NodesNamespace,
15
+ ExecutionNamespace,
16
+ StateNamespace,
17
+ AuthNamespace,
18
+ ResourcesNamespace,
19
+ DatasetNamespace,
20
+ WorkflowNamespace,
21
+ )
22
+
23
+ logger = logging.getLogger("noclick")
24
+
25
+ DEFAULT_TIMEOUT = 30.0
26
+
27
+
28
+ class NoClickSDK:
29
+ """
30
+ NoClick SDK client for Python.
31
+
32
+ Connects to the NoClick backend via Socket.IO using an API key.
33
+ Provides namespaced access to nodes, execution, state, auth, resources, and dataset APIs.
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ url: str,
39
+ api_key: str,
40
+ workflow_id: Optional[str] = None,
41
+ timeout: float = DEFAULT_TIMEOUT,
42
+ ):
43
+ self.url = url
44
+ self.api_key = api_key
45
+ self.workflow_id = workflow_id
46
+ self.timeout = timeout
47
+
48
+ self._sio = socketio.AsyncClient(reconnection=True, reconnection_attempts=5)
49
+ self._pending: Dict[str, asyncio.Future] = {}
50
+ self._event_handlers: Dict[str, List[Any]] = {}
51
+
52
+ # Register response handler
53
+ self._sio.on("response", self._handle_response)
54
+ self._sio.on("workflow:node:output", self._handle_node_output)
55
+ self._sio.on("workflow:node:state", self._handle_node_state)
56
+ self._sio.on("workflow:complete", self._handle_workflow_complete)
57
+
58
+ # Namespaces
59
+ self.nodes = NodesNamespace(self)
60
+ self.execution = ExecutionNamespace(self)
61
+ self.state = StateNamespace(self)
62
+ self.auth = AuthNamespace(self)
63
+ self.resources = ResourcesNamespace(self)
64
+ self.dataset = DatasetNamespace(self)
65
+ self.workflow = WorkflowNamespace(self)
66
+
67
+ async def connect(self) -> None:
68
+ """Connect to the NoClick backend."""
69
+ logger.info(f"Connecting to {self.url}...")
70
+ await self._sio.connect(
71
+ self.url,
72
+ auth={"api_key": self.api_key},
73
+ transports=["websocket"],
74
+ )
75
+ logger.info("Connected")
76
+
77
+ async def disconnect(self) -> None:
78
+ """Disconnect from the backend."""
79
+ await self._sio.disconnect()
80
+ logger.info("Disconnected")
81
+
82
+ async def send_event(self, event: str, data: Dict[str, Any]) -> Dict[str, Any]:
83
+ """
84
+ Send a socket event and wait for a response.
85
+
86
+ Uses request_id correlation to match request → response.
87
+ """
88
+ request_id = f"py-{uuid.uuid4().hex[:12]}"
89
+ data["request_id"] = request_id
90
+
91
+ loop = asyncio.get_event_loop()
92
+ future: asyncio.Future = loop.create_future()
93
+ self._pending[request_id] = future
94
+
95
+ await self._sio.emit(event, data)
96
+
97
+ try:
98
+ result = await asyncio.wait_for(future, timeout=self.timeout)
99
+ return result
100
+ except asyncio.TimeoutError:
101
+ self._pending.pop(request_id, None)
102
+ raise TimeoutError(f"Request '{event}' timed out after {self.timeout}s")
103
+
104
+ async def send_event_no_wait(self, event: str, data: Dict[str, Any]) -> None:
105
+ """Send a socket event without waiting for a response."""
106
+ request_id = f"py-{uuid.uuid4().hex[:12]}"
107
+ data["request_id"] = request_id
108
+ await self._sio.emit(event, data)
109
+
110
+ def on_event(self, event: str, handler) -> None:
111
+ """Subscribe to push events."""
112
+ if event not in self._event_handlers:
113
+ self._event_handlers[event] = []
114
+ self._event_handlers[event].append(handler)
115
+
116
+ def _handle_response(self, data: Any) -> None:
117
+ """Handle response events, resolving pending futures."""
118
+ if not isinstance(data, dict):
119
+ return
120
+ request_id = data.get("request_id")
121
+ if not request_id:
122
+ return
123
+ future = self._pending.pop(request_id, None)
124
+ if future and not future.done():
125
+ if data.get("error"):
126
+ future.set_exception(Exception(str(data["error"])))
127
+ else:
128
+ future.set_result(data.get("data", data))
129
+
130
+ def _handle_node_output(self, data: Any) -> None:
131
+ for handler in self._event_handlers.get("node:output", []):
132
+ handler(data.get("node_id"), data.get("output"))
133
+
134
+ def _handle_node_state(self, data: Any) -> None:
135
+ for handler in self._event_handlers.get("node:state", []):
136
+ handler(data.get("node_id"), data.get("state"))
137
+
138
+ def _handle_workflow_complete(self, data: Any) -> None:
139
+ for handler in self._event_handlers.get("workflow:complete", []):
140
+ handler(data)
@@ -0,0 +1,345 @@
1
+ """
2
+ SDK namespace classes — thin wrappers that map to socket events.
3
+ Each method mirrors the TypeScript SDK API.
4
+ """
5
+
6
+ from typing import Any, Callable, Dict, List, Optional, TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from noclick.client import NoClickSDK
10
+
11
+
12
+ class NodesNamespace:
13
+ def __init__(self, sdk: "NoClickSDK"):
14
+ self._sdk = sdk
15
+
16
+ async def get_output(self, node_id: str) -> Any:
17
+ """Read a node's last output."""
18
+ resp = await self._sdk.send_event("workflow:get_node_outputs", {
19
+ "workflow_id": self._sdk.workflow_id,
20
+ "node_ids": [node_id],
21
+ })
22
+ outputs = resp.get("outputs", {})
23
+ node_output = outputs.get(node_id, {})
24
+ return node_output.get("output")
25
+
26
+ async def get_config(self, node_id: str) -> Dict[str, Any]:
27
+ """Read a node's config."""
28
+ resp = await self._sdk.send_event("workflow:get", {
29
+ "workflow_id": self._sdk.workflow_id,
30
+ })
31
+ workflow_data = resp.get("workflow", {}).get("workflow_data", {})
32
+ for node in workflow_data.get("nodes", []):
33
+ if node.get("id") == node_id:
34
+ return node.get("config", {})
35
+ raise ValueError(f"Node not found: {node_id}")
36
+
37
+ async def list(self) -> List[Dict[str, Any]]:
38
+ """List all nodes in the workflow."""
39
+ resp = await self._sdk.send_event("workflow:get", {
40
+ "workflow_id": self._sdk.workflow_id,
41
+ })
42
+ workflow_data = resp.get("workflow", {}).get("workflow_data", {})
43
+ return [
44
+ {
45
+ "id": n.get("id"),
46
+ "type": n.get("type"),
47
+ "label": n.get("config", {}).get("label", n.get("type", "")),
48
+ "hasOutput": False, # Would need separate output check
49
+ }
50
+ for n in workflow_data.get("nodes", [])
51
+ ]
52
+
53
+
54
+ class ExecutionNamespace:
55
+ def __init__(self, sdk: "NoClickSDK"):
56
+ self._sdk = sdk
57
+
58
+ async def run_nodes_in_background(self, node_ids: List[str]) -> None:
59
+ """Run nodes without waiting for output."""
60
+ await self._sdk.send_event_no_wait("workflow:execute", {
61
+ "workflow_id": self._sdk.workflow_id,
62
+ "start_node_id": node_ids[0] if node_ids else None,
63
+ })
64
+
65
+ async def run_nodes_and_get_output(
66
+ self, run_nodes: List[str], target_nodes: List[str], timeout: Optional[float] = None
67
+ ) -> Dict[str, Any]:
68
+ """Run nodes and wait for target outputs."""
69
+ import asyncio
70
+
71
+ # Start execution
72
+ await self._sdk.send_event_no_wait("workflow:execute", {
73
+ "workflow_id": self._sdk.workflow_id,
74
+ "start_node_id": run_nodes[0] if run_nodes else None,
75
+ })
76
+
77
+ # Collect outputs via node:output events
78
+ results: Dict[str, Any] = {}
79
+ remaining = set(target_nodes)
80
+ done_event = asyncio.Event()
81
+
82
+ def on_output(node_id: str, output: Any):
83
+ if node_id in remaining:
84
+ results[node_id] = output
85
+ remaining.discard(node_id)
86
+ if not remaining:
87
+ done_event.set()
88
+
89
+ self._sdk.on_event("node:output", on_output)
90
+ try:
91
+ await asyncio.wait_for(done_event.wait(), timeout=timeout or self._sdk.timeout)
92
+ finally:
93
+ if on_output in self._sdk._event_handlers.get("node:output", []):
94
+ self._sdk._event_handlers["node:output"].remove(on_output)
95
+
96
+ return results
97
+
98
+ def stop(self) -> None:
99
+ """Stop the current execution."""
100
+ # Fire and forget
101
+ import asyncio
102
+ asyncio.create_task(self._sdk.send_event_no_wait("workflow:stop", {
103
+ "workflow_id": self._sdk.workflow_id,
104
+ }))
105
+
106
+ def on_node_state(self, node_id: str, handler: Callable) -> None:
107
+ """Subscribe to a node's state changes."""
108
+ def filtered_handler(nid: str, state: str):
109
+ if nid == node_id:
110
+ handler(state)
111
+ self._sdk.on_event("node:state", filtered_handler)
112
+
113
+ def on_node_output(self, node_id: str, handler: Callable) -> None:
114
+ """Subscribe to a node's output changes."""
115
+ def filtered_handler(nid: str, output: Any):
116
+ if nid == node_id:
117
+ handler(output)
118
+ self._sdk.on_event("node:output", filtered_handler)
119
+
120
+
121
+ class StateNamespace:
122
+ def __init__(self, sdk: "NoClickSDK"):
123
+ self._sdk = sdk
124
+
125
+ async def get(self, key: str, node_id: Optional[str] = None) -> Any:
126
+ """Read a state value."""
127
+ resp = await self._sdk.send_event("workflow:load_node_state", {
128
+ "workflow_id": self._sdk.workflow_id,
129
+ "node_id": node_id, # Handler resolves state-manager if None
130
+ "key": key,
131
+ })
132
+ return resp.get("state", {}).get(key)
133
+
134
+ async def set(self, key: str, value: Any, node_id: Optional[str] = None) -> None:
135
+ """Set a state value."""
136
+ await self._sdk.send_event("workflow:save_node_state", {
137
+ "workflow_id": self._sdk.workflow_id,
138
+ "node_id": node_id,
139
+ "state": {key: value},
140
+ })
141
+
142
+ async def delete(self, key: str, node_id: Optional[str] = None) -> None:
143
+ """Delete a state key."""
144
+ # Read current, remove key, write back
145
+ current = await self.get(key, node_id)
146
+ if current is not None:
147
+ await self._sdk.send_event("workflow:save_node_state", {
148
+ "workflow_id": self._sdk.workflow_id,
149
+ "node_id": node_id,
150
+ "state": {key: None}, # Convention: None = delete
151
+ })
152
+
153
+ async def update(self, key: str, updater: Callable, node_id: Optional[str] = None) -> None:
154
+ """Update a state value with a function."""
155
+ current = await self.get(key, node_id)
156
+ new_value = updater(current)
157
+ await self.set(key, new_value, node_id)
158
+
159
+ async def keys(self, node_id: Optional[str] = None) -> List[str]:
160
+ """List available state keys."""
161
+ resp = await self._sdk.send_event("workflow:load_node_state", {
162
+ "workflow_id": self._sdk.workflow_id,
163
+ "node_id": node_id,
164
+ })
165
+ state_data = resp.get("state", {})
166
+ return list(state_data.keys()) if isinstance(state_data, dict) else []
167
+
168
+
169
+ class AuthNamespace:
170
+ def __init__(self, sdk: "NoClickSDK"):
171
+ self._sdk = sdk
172
+
173
+ async def list_credentials(self) -> List[Dict[str, str]]:
174
+ """List available credentials."""
175
+ resp = await self._sdk.send_event("credential:list", {})
176
+ return [
177
+ {"id": c["id"], "type": c.get("credential_type", ""), "name": c.get("name", "")}
178
+ for c in resp.get("credentials", [])
179
+ ]
180
+
181
+ async def has_credential(self, credential_type: str) -> bool:
182
+ """Check if a credential of the given type exists."""
183
+ creds = await self.list_credentials()
184
+ return any(c["type"] == credential_type for c in creds)
185
+
186
+ async def create_credential(
187
+ self, credential_type: str, data: Dict[str, Any], name: Optional[str] = None
188
+ ) -> Dict[str, str]:
189
+ """Create a non-OAuth credential (API key, token, etc)."""
190
+ resp = await self._sdk.send_event("credential:create", {
191
+ "name": name or credential_type,
192
+ "credential_type": credential_type,
193
+ "credential_data": data,
194
+ "metadata": {},
195
+ })
196
+ cred = resp.get("credential", {})
197
+ return {"id": cred.get("id", ""), "type": credential_type, "name": cred.get("name", "")}
198
+
199
+
200
+ class ResourcesNamespace:
201
+ def __init__(self, sdk: "NoClickSDK"):
202
+ self._sdk = sdk
203
+
204
+ async def upload(self, name: str, mime_type: str, size_bytes: int, resource_type: str = "file"):
205
+ """Create a resource and get a presigned upload URL."""
206
+ create_resp = await self._sdk.send_event("resource:create", {
207
+ "workflow_id": self._sdk.workflow_id,
208
+ "resource_type": resource_type,
209
+ "name": name,
210
+ "mime_type": mime_type,
211
+ "size_bytes": size_bytes,
212
+ })
213
+ resource = create_resp.get("resource", {})
214
+ resource_id = resource.get("id")
215
+ if not resource_id:
216
+ raise ValueError("Failed to create resource")
217
+
218
+ upload_resp = await self._sdk.send_event("resource:upload_url", {
219
+ "resource_id": resource_id,
220
+ "filename": name,
221
+ "content_type": mime_type,
222
+ })
223
+ return {
224
+ "resource_id": resource_id,
225
+ "upload_url": upload_resp.get("upload_url", ""),
226
+ }
227
+
228
+ async def get_url(self, resource_id: str) -> str:
229
+ """Get a presigned download URL."""
230
+ resp = await self._sdk.send_event("resource:download_url", {
231
+ "resource_id": resource_id,
232
+ })
233
+ return resp.get("download_url", "")
234
+
235
+ async def remove(self, resource_id: str) -> None:
236
+ """Delete a resource."""
237
+ await self._sdk.send_event("resource:delete", {
238
+ "resource_id": resource_id,
239
+ })
240
+
241
+ async def list(self, resource_type: Optional[str] = None) -> List[Dict[str, Any]]:
242
+ """List resources in the workflow."""
243
+ resp = await self._sdk.send_event("resource:list", {
244
+ "workflow_id": self._sdk.workflow_id,
245
+ "resource_type": resource_type,
246
+ })
247
+ return [
248
+ {
249
+ "id": r.get("id"),
250
+ "name": r.get("name"),
251
+ "resource_type": r.get("resource_type"),
252
+ "mime_type": r.get("mime_type"),
253
+ "size_bytes": r.get("size_bytes"),
254
+ }
255
+ for r in resp.get("resources", [])
256
+ ]
257
+
258
+
259
+ class DatasetNamespace:
260
+ def __init__(self, sdk: "NoClickSDK"):
261
+ self._sdk = sdk
262
+
263
+ async def create(self, name: str) -> str:
264
+ """Create a new dataset. Returns resource_id."""
265
+ resp = await self._sdk.send_event("resource:create", {
266
+ "workflow_id": self._sdk.workflow_id,
267
+ "resource_type": "dataset",
268
+ "name": name,
269
+ })
270
+ resource = resp.get("resource", {})
271
+ resource_id = resource.get("id")
272
+ if not resource_id:
273
+ raise ValueError("Failed to create dataset")
274
+ return resource_id
275
+
276
+ async def list(self) -> List[Dict[str, Any]]:
277
+ """List all datasets in the workflow."""
278
+ resp = await self._sdk.send_event("resource:list", {
279
+ "workflow_id": self._sdk.workflow_id,
280
+ "resource_type": "dataset",
281
+ })
282
+ return [
283
+ {
284
+ "id": r.get("id"),
285
+ "name": r.get("name"),
286
+ "row_count": r.get("metadata", {}).get("row_count", 0),
287
+ }
288
+ for r in resp.get("resources", [])
289
+ ]
290
+
291
+ async def get_rows(
292
+ self, resource_id: str, limit: int = 100, offset: int = 0
293
+ ) -> Dict[str, Any]:
294
+ """Get paginated rows from a dataset."""
295
+ resp = await self._sdk.send_event("resource:dataset:rows", {
296
+ "resource_id": resource_id,
297
+ "limit": limit,
298
+ "offset": offset,
299
+ })
300
+ return {
301
+ "rows": resp.get("rows", []),
302
+ "total_count": resp.get("total_count", 0),
303
+ }
304
+
305
+ async def append_rows(self, resource_id: str, rows: List[Dict[str, Any]]) -> int:
306
+ """Append rows to a dataset. Returns inserted count."""
307
+ resp = await self._sdk.send_event("resource:dataset:append", {
308
+ "resource_id": resource_id,
309
+ "rows": rows,
310
+ })
311
+ return resp.get("inserted_count", 0)
312
+
313
+ async def update_row(self, resource_id: str, row_id: str, data: Dict[str, Any]) -> None:
314
+ """Update a single row."""
315
+ await self._sdk.send_event("resource:dataset:update_row", {
316
+ "resource_id": resource_id,
317
+ "row_id": row_id,
318
+ "data": data,
319
+ })
320
+
321
+ async def delete_rows(self, resource_id: str, row_ids: List[str]) -> int:
322
+ """Delete rows. Returns deleted count."""
323
+ resp = await self._sdk.send_event("resource:dataset:delete_rows", {
324
+ "resource_id": resource_id,
325
+ "row_ids": row_ids,
326
+ })
327
+ return resp.get("deleted_count", 0)
328
+
329
+
330
+ class WorkflowNamespace:
331
+ def __init__(self, sdk: "NoClickSDK"):
332
+ self._sdk = sdk
333
+
334
+ async def get_info(self) -> Dict[str, Any]:
335
+ """Get workflow info."""
336
+ resp = await self._sdk.send_event("workflow:get", {
337
+ "workflow_id": self._sdk.workflow_id,
338
+ })
339
+ wf = resp.get("workflow", {})
340
+ nodes = wf.get("workflow_data", {}).get("nodes", [])
341
+ return {
342
+ "id": wf.get("id", ""),
343
+ "name": wf.get("name", ""),
344
+ "node_count": len(nodes),
345
+ }
@@ -0,0 +1,93 @@
1
+ Metadata-Version: 2.4
2
+ Name: noclick
3
+ Version: 0.1.0
4
+ Summary: Python SDK for NoClick workflow automation
5
+ License: BUSL-1.1
6
+ Project-URL: Homepage, https://docs.noclick.com/sdk/overview
7
+ Project-URL: Documentation, https://docs.noclick.com/sdk/external-apps
8
+ Project-URL: Repository, https://github.com/noclickapp/noclick
9
+ Keywords: noclick,workflow,automation,sdk
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: Other/Proprietary License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: python-socketio[asyncio_client]>=5.0
20
+ Requires-Dist: aiohttp>=3.0
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest; extra == "dev"
23
+ Requires-Dist: pytest-asyncio; extra == "dev"
24
+
25
+ # noclick-sdk
26
+
27
+ Python SDK for [NoClick](https://noclick.com) workflow automation. Build external applications that interact with NoClick workflows — read node outputs, trigger execution, manage state, and work with files and datasets.
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ pip install noclick
33
+ ```
34
+
35
+ ## Quick Start
36
+
37
+ ```python
38
+ import asyncio
39
+ from noclick import NoClickSDK
40
+
41
+ async def main():
42
+ sdk = NoClickSDK(
43
+ url="https://api.noclick.io",
44
+ api_key="nk_live_...",
45
+ workflow_id="your-workflow-id",
46
+ )
47
+ await sdk.connect()
48
+
49
+ # List nodes
50
+ nodes = await sdk.nodes.list()
51
+ for node in nodes:
52
+ print(f"{node['label']} ({node['type']})")
53
+
54
+ # Read node output
55
+ output = await sdk.nodes.get_output("gmail-node-id")
56
+
57
+ # Run a node and get results
58
+ results = await sdk.execution.run_nodes_and_get_output(
59
+ ["data-fetcher"], ["formatter"]
60
+ )
61
+
62
+ # State management
63
+ await sdk.state.set("counter", 42)
64
+ val = await sdk.state.get("counter")
65
+
66
+ # Dataset CRUD
67
+ ds_id = await sdk.dataset.create("My Data")
68
+ await sdk.dataset.append_rows(ds_id, [{"name": "Alice", "score": 95}])
69
+
70
+ await sdk.disconnect()
71
+
72
+ asyncio.run(main())
73
+ ```
74
+
75
+ ## API
76
+
77
+ | Namespace | Methods |
78
+ |-----------|---------|
79
+ | `sdk.nodes` | `get_output`, `get_config`, `list` |
80
+ | `sdk.execution` | `run_nodes_and_get_output`, `run_nodes_in_background`, `on_node_output`, `on_node_state` |
81
+ | `sdk.state` | `get`, `set`, `delete`, `update`, `keys` |
82
+ | `sdk.auth` | `list_credentials`, `has_credential`, `create_credential` |
83
+ | `sdk.resources` | `upload`, `get_url`, `remove`, `list` |
84
+ | `sdk.dataset` | `create`, `list`, `get_rows`, `append_rows`, `update_row`, `delete_rows` |
85
+ | `sdk.workflow` | `get_info` |
86
+
87
+ ## Documentation
88
+
89
+ Full documentation at [docs.noclick.com/sdk](https://docs.noclick.com/sdk/overview).
90
+
91
+ ## License
92
+
93
+ [Business Source License 1.1](../LICENSE) — free to use with NoClick, converts to Apache 2.0 after 4 years.
@@ -0,0 +1,10 @@
1
+ README.md
2
+ pyproject.toml
3
+ noclick/__init__.py
4
+ noclick/client.py
5
+ noclick/namespaces.py
6
+ noclick.egg-info/PKG-INFO
7
+ noclick.egg-info/SOURCES.txt
8
+ noclick.egg-info/dependency_links.txt
9
+ noclick.egg-info/requires.txt
10
+ noclick.egg-info/top_level.txt
@@ -0,0 +1,6 @@
1
+ python-socketio[asyncio_client]>=5.0
2
+ aiohttp>=3.0
3
+
4
+ [dev]
5
+ pytest
6
+ pytest-asyncio
@@ -0,0 +1 @@
1
+ noclick
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "noclick"
7
+ version = "0.1.0"
8
+ description = "Python SDK for NoClick workflow automation"
9
+ readme = "README.md"
10
+ license = {text = "BUSL-1.1"}
11
+ requires-python = ">=3.10"
12
+ keywords = ["noclick", "workflow", "automation", "sdk"]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Intended Audience :: Developers",
16
+ "License :: Other/Proprietary License",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ ]
22
+ dependencies = [
23
+ "python-socketio[asyncio_client]>=5.0",
24
+ "aiohttp>=3.0",
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://docs.noclick.com/sdk/overview"
29
+ Documentation = "https://docs.noclick.com/sdk/external-apps"
30
+ Repository = "https://github.com/noclickapp/noclick"
31
+
32
+ [project.optional-dependencies]
33
+ dev = ["pytest", "pytest-asyncio"]
34
+
35
+ [tool.setuptools.packages.find]
36
+ include = ["noclick*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+