synapsedx-sdk 0.3.0.dev20261012__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.
@@ -0,0 +1,277 @@
1
+ # Copyright 2026 SynapseDX Contributors
2
+ # Licensed under the Apache License, Version 2.0 (the "License");
3
+ """synapsedx Python SDK — gateway + IAM + workflow client."""
4
+
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass
8
+ from typing import Any
9
+ from urllib.parse import quote, urlencode
10
+
11
+ import requests
12
+
13
+
14
+ class SynapseDXError(Exception):
15
+ """Mọi lỗi SDK đều là SynapseDXError (network hoặc HTTP)."""
16
+
17
+
18
+ __all__ = ["SynapseDXClient", "SynapseDXError"]
19
+
20
+
21
+ @dataclass
22
+ class SynapseDXClient:
23
+ base_url: str
24
+ token: str | None = None
25
+ api_key: str | None = None
26
+ timeout: int = 15
27
+
28
+ def __post_init__(self) -> None:
29
+ self.base_url = self.base_url.rstrip("/")
30
+
31
+ def _headers(self) -> dict[str, str]:
32
+ h = {"Content-Type": "application/json"}
33
+ if self.token:
34
+ h["Authorization"] = f"Bearer {self.token}"
35
+ if self.api_key:
36
+ h["X-API-Key"] = self.api_key
37
+ return h
38
+
39
+ def set_token(self, token: str) -> None:
40
+ """Set/replace the Bearer JWT (mirrors JS setToken / Go SetToken)."""
41
+ self.token = token
42
+
43
+ def set_api_key(self, api_key: str) -> None:
44
+ """Set/replace the X-API-Key (mirrors JS setApiKey / Go SetAPIKey)."""
45
+ self.api_key = api_key
46
+
47
+ def _req(self, method: str, path: str, body: Any | None = None) -> Any:
48
+ try:
49
+ r = requests.request(
50
+ method,
51
+ self.base_url + path,
52
+ json=body,
53
+ headers=self._headers(),
54
+ timeout=self.timeout,
55
+ )
56
+ except Exception as e: # network, DNS, timeout...
57
+ raise SynapseDXError(f"synapsedx network {path}: {e}") from e
58
+ try:
59
+ r.raise_for_status()
60
+ except Exception as e:
61
+ raise SynapseDXError(
62
+ f"synapsedx {r.status_code} {path}: {r.text[:500]}"
63
+ ) from e
64
+ return r.json() if r.text else None
65
+
66
+ # ---- auth ----
67
+ def login(self, email: str, org_slug: str = "acme") -> dict:
68
+ return self._req(
69
+ "POST", "/v1/auth/login", {"email": email, "orgSlug": org_slug}
70
+ )
71
+
72
+ def refresh(self, refresh_token: str) -> dict:
73
+ return self._req("POST", "/v1/auth/refresh", {"refreshToken": refresh_token})
74
+
75
+ def logout(self, refresh_token: str) -> dict:
76
+ return self._req("POST", "/v1/auth/logout", {"refreshToken": refresh_token})
77
+
78
+ def logout_all(self, refresh_token: str) -> dict:
79
+ return self._req("POST", "/v1/auth/logout-all", {"refreshToken": refresh_token})
80
+
81
+ def verify(self) -> dict:
82
+ return self._req("GET", "/v1/auth/verify")
83
+
84
+ def me(self) -> dict:
85
+ return self._req("GET", "/v1/auth/me")
86
+
87
+ # ---- orgs ----
88
+ def list_orgs(self) -> list:
89
+ return self._req("GET", "/v1/orgs")
90
+
91
+ def create_org(self, slug: str, display_name: str) -> dict:
92
+ return self._req(
93
+ "POST", "/v1/orgs", {"slug": slug, "displayName": display_name}
94
+ )
95
+
96
+ def create_account(self, org_id: str, slug: str, display_name: str) -> dict:
97
+ return self._req(
98
+ "POST",
99
+ f"/v1/orgs/{quote(org_id, safe='')}/accounts",
100
+ {"slug": slug, "displayName": display_name},
101
+ )
102
+
103
+ def list_accounts(self, org_id: str) -> list:
104
+ return self._req("GET", f"/v1/orgs/{quote(org_id, safe='')}/accounts")
105
+
106
+ def create_user(
107
+ self, org_id: str, email: str, display_name: str | None = None
108
+ ) -> dict:
109
+ return self._req(
110
+ "POST",
111
+ f"/v1/orgs/{quote(org_id, safe='')}/users",
112
+ {"email": email, "displayName": display_name},
113
+ )
114
+
115
+ def list_users(self, org_id: str) -> list:
116
+ return self._req("GET", f"/v1/orgs/{quote(org_id, safe='')}/users")
117
+
118
+ def user_policies(self, user_id: str) -> list:
119
+ """Effective policy documents of a user (debug helper for authorize)."""
120
+ return self._req("GET", f"/v1/users/{quote(user_id, safe='')}/policies")
121
+
122
+ # ---- iam ----
123
+ def authorize(
124
+ self, user_id: str, action: str, resource: str, context: dict | None = None
125
+ ) -> dict:
126
+ return self._req(
127
+ "POST",
128
+ "/v1/authorize",
129
+ {
130
+ "userId": user_id,
131
+ "action": action,
132
+ "resource": resource,
133
+ "context": context,
134
+ },
135
+ )
136
+
137
+ def attach_policy(self, role_id: str, policy_id: str) -> dict:
138
+ return self._req(
139
+ "POST",
140
+ f"/v1/roles/{quote(role_id, safe='')}/attach",
141
+ {"policyId": policy_id},
142
+ )
143
+
144
+ def assign_role(self, user_id: str, role_id: str) -> dict:
145
+ return self._req(
146
+ "POST", f"/v1/users/{quote(user_id, safe='')}/assign", {"roleId": role_id}
147
+ )
148
+
149
+ def simulate(
150
+ self, documents: list, action: str, resource: str, context: dict | None = None
151
+ ) -> dict:
152
+ return self._req(
153
+ "POST",
154
+ "/v1/authorize/dry-run",
155
+ {
156
+ "documents": documents,
157
+ "action": action,
158
+ "resource": resource,
159
+ "context": context,
160
+ },
161
+ )
162
+
163
+ def list_audit(self, limit: int = 50, actor: str | None = None) -> list:
164
+ params: dict[str, Any] = {"limit": limit}
165
+ if actor:
166
+ params["actor"] = actor
167
+ return self._req("GET", f"/v1/audit?{urlencode(params)}")
168
+
169
+ def create_api_key(
170
+ self, account_id: str, name: str = "default", scopes: list | None = None
171
+ ) -> dict:
172
+ return self._req(
173
+ "POST",
174
+ f"/v1/accounts/{quote(account_id, safe='')}/keys",
175
+ {"name": name, "scopes": scopes or ["*"]},
176
+ )
177
+
178
+ def revoke_api_key(self, key_id: str) -> dict:
179
+ return self._req("DELETE", f"/v1/keys/{quote(key_id, safe='')}")
180
+
181
+ def verify_api_key(self, api_key: str) -> dict:
182
+ """Check a raw API key via the internal verify endpoint."""
183
+ return self._req("POST", "/v1/keys/verify", {"apiKey": api_key})
184
+
185
+ def create_service_account(
186
+ self, account_id: str, slug: str, description: str = ""
187
+ ) -> dict:
188
+ return self._req(
189
+ "POST",
190
+ f"/v1/accounts/{quote(account_id, safe='')}/service-accounts",
191
+ {"slug": slug, "description": description},
192
+ )
193
+
194
+ def list_service_accounts(self, account_id: str) -> list:
195
+ return self._req(
196
+ "GET", f"/v1/accounts/{quote(account_id, safe='')}/service-accounts"
197
+ )
198
+
199
+ def list_roles(self, account_id: str) -> list:
200
+ return self._req("GET", f"/v1/accounts/{quote(account_id, safe='')}/roles")
201
+
202
+ def create_role(self, account_id: str, slug: str, description: str = "") -> dict:
203
+ return self._req(
204
+ "POST",
205
+ f"/v1/accounts/{quote(account_id, safe='')}/roles",
206
+ {"slug": slug, "description": description},
207
+ )
208
+
209
+ def delete_role(self, role_id: str) -> dict:
210
+ return self._req("DELETE", f"/v1/roles/{quote(role_id, safe='')}")
211
+
212
+ def list_policies(self, account_id: str) -> list:
213
+ return self._req("GET", f"/v1/accounts/{quote(account_id, safe='')}/policies")
214
+
215
+ def create_policy(
216
+ self, account_id: str, slug: str, document: dict, description: str = ""
217
+ ) -> dict:
218
+ return self._req(
219
+ "POST",
220
+ f"/v1/accounts/{quote(account_id, safe='')}/policies",
221
+ {"slug": slug, "document": document, "description": description},
222
+ )
223
+
224
+ def delete_policy(self, policy_id: str) -> dict:
225
+ return self._req("DELETE", f"/v1/policies/{quote(policy_id, safe='')}")
226
+
227
+ def service_account_token(self, sa_id: str, scopes: list | None = None) -> dict:
228
+ return self._req(
229
+ "POST",
230
+ f"/v1/service-accounts/{quote(sa_id, safe='')}/token",
231
+ {"scopes": scopes or ["*"]},
232
+ )
233
+
234
+ # ---- workflows ----
235
+ def list_templates(self) -> Any:
236
+ return self._req("GET", "/v1/templates")
237
+
238
+ def get_template(self, name: str) -> Any:
239
+ """Fetch a single built-in template definition by name."""
240
+ return self._req("GET", f"/v1/templates/{quote(name, safe='')}")
241
+
242
+ def get_workflow(self, wf_id: str) -> Any:
243
+ return self._req("GET", f"/v1/workflows/{quote(wf_id, safe='')}")
244
+
245
+ def activate_workflow(self, wf_id: str) -> Any:
246
+ return self._req("POST", f"/v1/workflows/{quote(wf_id, safe='')}/activate")
247
+
248
+ def deactivate_workflow(self, wf_id: str) -> Any:
249
+ return self._req("POST", f"/v1/workflows/{quote(wf_id, safe='')}/deactivate")
250
+
251
+ def execute_workflow(self, wf_id: str, payload: dict | None = None) -> Any:
252
+ """Trigger a manual execution (proxied to the engine)."""
253
+ return self._req(
254
+ "POST", f"/v1/workflows/{quote(wf_id, safe='')}/execute", payload or {}
255
+ )
256
+
257
+ def list_workflows(self) -> Any:
258
+ return self._req("GET", "/v1/workflows")
259
+
260
+ def create_workflow_from_template(
261
+ self, template: str, name: str | None = None
262
+ ) -> Any:
263
+ return self._req("POST", "/v1/workflows", {"template": template, "name": name})
264
+
265
+ def create_workflow(self, name: str, definition: dict) -> Any:
266
+ """Create a workflow from a raw n8n definition."""
267
+ return self._req(
268
+ "POST", "/v1/workflows", {"name": name, "definition": definition}
269
+ )
270
+
271
+ def list_executions(self, query: str = "") -> Any:
272
+ """Proxy the engine execution list (e.g. query='limit=20')."""
273
+ q = query if not query or query.startswith("?") else f"?{query}"
274
+ return self._req("GET", f"/v1/executions{q}")
275
+
276
+ def get_execution(self, exec_id: str) -> Any:
277
+ return self._req("GET", f"/v1/executions/{quote(exec_id, safe='')}")
@@ -0,0 +1,12 @@
1
+ """Smoke example for synapsedx-sdk."""
2
+
3
+ import os
4
+
5
+ from synapsedx_sdk import SynapseDXClient
6
+
7
+ base = os.environ.get("SYNAPSEDX_BASE_URL", "http://localhost:8080")
8
+ c = SynapseDXClient(base)
9
+ login = c.login("demo@acme.local")
10
+ print("login:", login["user"]["email"])
11
+ c.token = login["accessToken"]
12
+ print(c.list_orgs())
@@ -0,0 +1,118 @@
1
+ """Offline SDK tests — stdlib echo server, no platform needed."""
2
+
3
+ import json
4
+ import threading
5
+ from http.server import BaseHTTPRequestHandler, HTTPServer
6
+
7
+ from synapsedx_sdk import SynapseDXClient, SynapseDXError
8
+
9
+ _seen: dict = {}
10
+
11
+
12
+ class Echo(BaseHTTPRequestHandler):
13
+ def _send(self):
14
+ _seen["auth"] = self.headers.get("Authorization")
15
+ _seen["key"] = self.headers.get("X-API-Key")
16
+ _seen["path"] = self.path
17
+ _seen["method"] = self.command
18
+ # List endpoints return arrays like the real API.
19
+ is_list = self.command == "GET" and (
20
+ self.path == "/v1/orgs"
21
+ or self.path.startswith("/v1/executions")
22
+ or self.path in ("/v1/templates", "/v1/workflows", "/v1/audit")
23
+ or self.path.endswith(
24
+ ("/accounts", "/users", "/roles", "/policies", "/service-accounts")
25
+ )
26
+ )
27
+ payload = b"[]" if is_list else json.dumps({"ok": True}).encode()
28
+ self.send_response(200)
29
+ self.send_header("Content-Type", "application/json")
30
+ self.send_header("Content-Length", str(len(payload)))
31
+ self.end_headers()
32
+ self.wfile.write(payload)
33
+
34
+ do_GET = _send
35
+ do_POST = _send
36
+ do_DELETE = _send
37
+
38
+ def log_message(self, *a): # silent
39
+ pass
40
+
41
+
42
+ def _server():
43
+ srv = HTTPServer(("127.0.0.1", 0), Echo)
44
+ threading.Thread(target=srv.serve_forever, daemon=True).start()
45
+ return srv
46
+
47
+
48
+ def test_trim_and_headers():
49
+ srv = _server()
50
+ try:
51
+ base = f"http://127.0.0.1:{srv.server_port}"
52
+ c = SynapseDXClient(base + "/")
53
+ assert c.base_url == base
54
+ c.token = "t1"
55
+ c.list_orgs()
56
+ assert _seen["auth"] == "Bearer t1", _seen
57
+ assert _seen["path"] == "/v1/orgs"
58
+ c2 = SynapseDXClient(base, api_key="k1")
59
+ c2.list_templates()
60
+ assert _seen["key"] == "k1", _seen
61
+ finally:
62
+ srv.shutdown()
63
+
64
+
65
+ def test_network_error_wrapped():
66
+ c = SynapseDXClient("http://127.0.0.1:1", timeout=2)
67
+ try:
68
+ c.list_orgs()
69
+ except SynapseDXError as e:
70
+ assert "synapsedx" in str(e)
71
+ return
72
+ raise AssertionError("expected SynapseDXError")
73
+
74
+
75
+ def test_import():
76
+ from synapsedx_sdk import SynapseDXClient
77
+
78
+ c = SynapseDXClient("http://localhost:8080")
79
+ assert c.base_url == "http://localhost:8080"
80
+
81
+
82
+ def test_full_coverage_paths():
83
+ srv = _server()
84
+ try:
85
+ base = f"http://127.0.0.1:{srv.server_port}"
86
+ c = SynapseDXClient(base)
87
+ c.login("a@b.c")
88
+ assert _seen["path"] == "/v1/auth/login"
89
+ c.refresh("rt")
90
+ assert _seen["path"] == "/v1/auth/refresh"
91
+ c.list_accounts("org1")
92
+ assert _seen["path"] == "/v1/orgs/org1/accounts"
93
+ c.user_policies("u1")
94
+ assert _seen["path"] == "/v1/users/u1/policies"
95
+ c.get_template("webhook-to-log")
96
+ assert _seen["path"] == "/v1/templates/webhook-to-log"
97
+ c.get_workflow("a/b")
98
+ assert _seen["path"] == "/v1/workflows/a%2Fb"
99
+ c.deactivate_workflow("w1")
100
+ assert _seen["path"] == "/v1/workflows/w1/deactivate"
101
+ c.execute_workflow("w1", {})
102
+ assert _seen["path"] == "/v1/workflows/w1/execute"
103
+ c.list_executions("limit=5")
104
+ assert _seen["path"] == "/v1/executions?limit=5"
105
+ c.get_execution("e1")
106
+ assert _seen["path"] == "/v1/executions/e1"
107
+ c.verify_api_key("k")
108
+ assert _seen["path"] == "/v1/keys/verify"
109
+ c.list_audit(actor="u@x")
110
+ assert "actor=u%40x" in _seen["path"], _seen
111
+ c.set_token("t")
112
+ c.set_api_key("k")
113
+ assert c.token == "t" and c.api_key == "k"
114
+ doc = {"Statement": [{"Effect": "Allow", "Action": "*", "Resource": "*"}]}
115
+ c.create_workflow("n", doc)
116
+ assert _seen["path"] == "/v1/workflows"
117
+ finally:
118
+ srv.shutdown()
@@ -0,0 +1,47 @@
1
+ Metadata-Version: 2.4
2
+ Name: synapsedx-sdk
3
+ Version: 0.3.0.dev20261012
4
+ Summary: synapsedx SDK for Python (Apache-2.0)
5
+ License: Apache-2.0
6
+ Requires-Python: >=3.9
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: requests>=2.28
9
+ Provides-Extra: dev
10
+ Requires-Dist: pytest; extra == "dev"
11
+ Requires-Dist: ruff; extra == "dev"
12
+
13
+ # synapsedx SDK for Python
14
+ Apache-2.0. See root LICENSE. Part of [SynapseDX](../../README.md) Tier-1 PaaS.
15
+
16
+ ```bash
17
+ pip install synapsedx-sdk
18
+ ```
19
+
20
+ ```python
21
+ from synapsedx_sdk import SynapseDXClient
22
+
23
+ c = SynapseDXClient("http://localhost:8080")
24
+ login = c.login("demo@acme.local")
25
+ c.set_token(login["accessToken"])
26
+
27
+ c.create_org("acme-2", "ACME 2")
28
+ c.authorize(login["user"]["id"], "workflow:Execute", "*")
29
+ c.create_workflow_from_template("webhook-to-log", "demo")
30
+ ```
31
+
32
+ Auth: `SynapseDXClient(base_url, token=..., api_key=...)`, or `set_token(jwt)` /
33
+ `set_api_key(key)` after login. `timeout` configurable (default 15s).
34
+ See `synapsedx_sdk/example.py` and [docs/api.md](../../docs/api.md).
35
+
36
+ ## Full method coverage (mirrors `docs/api.md`)
37
+
38
+ - Auth: `login`, `refresh`, `logout`, `logout_all`, `verify`, `me`
39
+ - Orgs/accounts/users: `list_orgs`, `create_org`, `list_accounts`, `create_account`,
40
+ `list_users`, `create_user`, `user_policies`
41
+ - IAM: `create_role`, `list_roles`, `delete_role`, `create_policy`, `list_policies`,
42
+ `delete_policy`, `attach_policy`, `assign_role`, `authorize`, `simulate`,
43
+ `list_audit`, `create_api_key`, `revoke_api_key`, `verify_api_key`,
44
+ `create_service_account`, `list_service_accounts`, `service_account_token`
45
+ - Workflows: `list_workflows`, `get_workflow`, `create_workflow_from_template`,
46
+ `create_workflow`, `activate_workflow`, `deactivate_workflow`, `execute_workflow`,
47
+ `list_templates`, `get_template`, `list_executions`, `get_execution`
@@ -0,0 +1,7 @@
1
+ synapsedx_sdk/__init__.py,sha256=lZG9AAZA1to5e0LNxFT-7okKBc6t9o84U-OF-lb_ss4,9783
2
+ synapsedx_sdk/example.py,sha256=NRC5baEUwQfnfEqa4IINUp0lXtvAPFNQike6r8X9j8E,316
3
+ synapsedx_sdk/test_client.py,sha256=YuCi8FJfa3qg_NvRuUC71scZk8GDbu25Nt9wr68D0vU,3855
4
+ synapsedx_sdk-0.3.0.dev20261012.dist-info/METADATA,sha256=prssFAEonMEb24WEWEFPP_LgqzfThYcDYmNuwpbY4yc,1806
5
+ synapsedx_sdk-0.3.0.dev20261012.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
6
+ synapsedx_sdk-0.3.0.dev20261012.dist-info/top_level.txt,sha256=Z-344B3E557x3g82iRDzss7X6LX6vGohdsO1S1bo3Iw,14
7
+ synapsedx_sdk-0.3.0.dev20261012.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ synapsedx_sdk