flowra-sdk 0.1.1__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.
@@ -0,0 +1,89 @@
1
+ Metadata-Version: 2.4
2
+ Name: flowra-sdk
3
+ Version: 0.1.1
4
+ Summary: Official Python SDK for the Flowra API
5
+ Author: Flowra
6
+ License: MIT
7
+ Project-URL: Homepage, https://flowra.dev
8
+ Project-URL: Documentation, https://docs.flowra.dev
9
+ Project-URL: Repository, https://github.com/flowradev/sdk
10
+ Project-URL: Issues, https://github.com/flowradev/sdk/issues
11
+ Keywords: flowra,sdk,agents,workflows,tools
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+
15
+ # flowra
16
+
17
+ Python SDK for the Flowra API. Stdlib only (no extra runtime deps). Requires Python 3.10+.
18
+
19
+ ## Install
20
+
21
+ PyPI name is `flowra-sdk`. Import stays `flowra`. Do **not** `pip install flowra` — that package is unrelated.
22
+
23
+ ```bash
24
+ pip install flowra-sdk
25
+ ```
26
+
27
+ From this repo:
28
+
29
+ ```bash
30
+ pip install -e "./python"
31
+ ```
32
+
33
+ ## Quickstart
34
+
35
+ ```python
36
+ import os
37
+ from flowra import Flowra
38
+
39
+ flowra = Flowra(api_key=os.environ["FLOWRA_API_KEY"])
40
+
41
+ profile = flowra.get_profile()
42
+ tools = flowra.tools.list(limit=10)
43
+ run = flowra.workflows.run("WORKFLOW_ID", {"input": {"message": "hello"}})
44
+ ```
45
+
46
+ Act as an external user:
47
+
48
+ ```python
49
+ user_client = flowra.as_user("customer_42")
50
+ user_client.connections.create_link({"authConfigId": "..."})
51
+ ```
52
+
53
+ ## Namespaces
54
+
55
+ `tools`, `toolkits`, `skills`, `workflows`, `connections`, `auth_configs`, `users`, `triggers`, `chat`, `files`, `knowledge`, `database`, `sandbox`, `mcp`, `llm`, `browser`, `usage`
56
+
57
+ Method names are snake_case mirrors of the TypeScript facade (e.g. `create_link`, `set_active`, `ingest_url`).
58
+
59
+ ## Chat streaming and usage
60
+
61
+ ```python
62
+ from flowra import Flowra
63
+
64
+ flowra = Flowra(api_key="...")
65
+
66
+ for event in flowra.chat.stream(thread_id, {"input": {"messages": [...]}}, as_events=True):
67
+ if event["event"] == "usage":
68
+ print(event["data"])
69
+ ```
70
+
71
+ ```python
72
+ balance = flowra.usage.balance()
73
+ ledger = flowra.usage.list(threadId="...")
74
+ ```
75
+
76
+ ## Files
77
+
78
+ ```python
79
+ flowra.files.upload("document", "/path/to/file.pdf")
80
+ content = flowra.files.download(file_id) # bytes
81
+ ```
82
+
83
+ ## Escape hatch
84
+
85
+ ```python
86
+ flowra.request("GET", "/api/v1/toolkits", query={"limit": 5})
87
+ ```
88
+
89
+ HTTP failures raise `FlowraAPIError` (`status_code`, `body`).
@@ -0,0 +1,75 @@
1
+ # flowra
2
+
3
+ Python SDK for the Flowra API. Stdlib only (no extra runtime deps). Requires Python 3.10+.
4
+
5
+ ## Install
6
+
7
+ PyPI name is `flowra-sdk`. Import stays `flowra`. Do **not** `pip install flowra` — that package is unrelated.
8
+
9
+ ```bash
10
+ pip install flowra-sdk
11
+ ```
12
+
13
+ From this repo:
14
+
15
+ ```bash
16
+ pip install -e "./python"
17
+ ```
18
+
19
+ ## Quickstart
20
+
21
+ ```python
22
+ import os
23
+ from flowra import Flowra
24
+
25
+ flowra = Flowra(api_key=os.environ["FLOWRA_API_KEY"])
26
+
27
+ profile = flowra.get_profile()
28
+ tools = flowra.tools.list(limit=10)
29
+ run = flowra.workflows.run("WORKFLOW_ID", {"input": {"message": "hello"}})
30
+ ```
31
+
32
+ Act as an external user:
33
+
34
+ ```python
35
+ user_client = flowra.as_user("customer_42")
36
+ user_client.connections.create_link({"authConfigId": "..."})
37
+ ```
38
+
39
+ ## Namespaces
40
+
41
+ `tools`, `toolkits`, `skills`, `workflows`, `connections`, `auth_configs`, `users`, `triggers`, `chat`, `files`, `knowledge`, `database`, `sandbox`, `mcp`, `llm`, `browser`, `usage`
42
+
43
+ Method names are snake_case mirrors of the TypeScript facade (e.g. `create_link`, `set_active`, `ingest_url`).
44
+
45
+ ## Chat streaming and usage
46
+
47
+ ```python
48
+ from flowra import Flowra
49
+
50
+ flowra = Flowra(api_key="...")
51
+
52
+ for event in flowra.chat.stream(thread_id, {"input": {"messages": [...]}}, as_events=True):
53
+ if event["event"] == "usage":
54
+ print(event["data"])
55
+ ```
56
+
57
+ ```python
58
+ balance = flowra.usage.balance()
59
+ ledger = flowra.usage.list(threadId="...")
60
+ ```
61
+
62
+ ## Files
63
+
64
+ ```python
65
+ flowra.files.upload("document", "/path/to/file.pdf")
66
+ content = flowra.files.download(file_id) # bytes
67
+ ```
68
+
69
+ ## Escape hatch
70
+
71
+ ```python
72
+ flowra.request("GET", "/api/v1/toolkits", query={"limit": 5})
73
+ ```
74
+
75
+ HTTP failures raise `FlowraAPIError` (`status_code`, `body`).
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "flowra-sdk"
7
+ version = "0.1.1"
8
+ description = "Official Python SDK for the Flowra API"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Flowra" }]
13
+ keywords = ["flowra", "sdk", "agents", "workflows", "tools"]
14
+ dependencies = []
15
+
16
+ [project.urls]
17
+ Homepage = "https://flowra.dev"
18
+ Documentation = "https://docs.flowra.dev"
19
+ Repository = "https://github.com/flowradev/sdk"
20
+ Issues = "https://github.com/flowradev/sdk/issues"
21
+
22
+ [tool.setuptools.packages.find]
23
+ where = ["src"]
24
+
25
+ [tool.setuptools.package-data]
26
+ flowra = ["py.typed"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,20 @@
1
+ """Flowra Python SDK."""
2
+
3
+ from .client import Flowra
4
+ from .errors import FlowraAPIError
5
+ from .stream_usage import (
6
+ ParsedSseEvent,
7
+ StreamUsageEvent,
8
+ extract_stream_usage,
9
+ parse_sse_chunk,
10
+ )
11
+
12
+ __all__ = [
13
+ "Flowra",
14
+ "FlowraAPIError",
15
+ "ParsedSseEvent",
16
+ "StreamUsageEvent",
17
+ "extract_stream_usage",
18
+ "parse_sse_chunk",
19
+ ]
20
+ __version__ = "0.1.1"
@@ -0,0 +1,237 @@
1
+ """HTTP helpers for the Flowra SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import mimetypes
7
+ import uuid
8
+ from pathlib import Path
9
+ from typing import Any, BinaryIO, Iterator, Mapping, MutableMapping, Optional, Union
10
+ from urllib.error import HTTPError, URLError
11
+ from urllib.parse import urlencode, urljoin
12
+ from urllib.request import Request, urlopen
13
+
14
+ from .errors import FlowraAPIError
15
+
16
+ FileBody = Union[str, Path, bytes, BinaryIO]
17
+
18
+
19
+ class HttpClient:
20
+ def __init__(
21
+ self,
22
+ *,
23
+ api_key: str,
24
+ base_url: str = "https://flowra.dev",
25
+ username: Optional[str] = None,
26
+ timeout: float = 60.0,
27
+ ) -> None:
28
+ self.api_key = api_key
29
+ self.base_url = base_url.rstrip("/")
30
+ self.username = username
31
+ self.timeout = timeout
32
+
33
+ def _build_url(
34
+ self,
35
+ path: str,
36
+ query: Optional[Mapping[str, Any]] = None,
37
+ ) -> str:
38
+ url = urljoin(f"{self.base_url}/", path.lstrip("/"))
39
+ if query:
40
+ filtered = {
41
+ key: value
42
+ for key, value in query.items()
43
+ if value is not None
44
+ }
45
+ if filtered:
46
+ url = f"{url}?{urlencode(filtered, doseq=True)}"
47
+ return url
48
+
49
+ def _base_headers(
50
+ self,
51
+ *,
52
+ accept: str = "application/json",
53
+ ) -> MutableMapping[str, str]:
54
+ headers: MutableMapping[str, str] = {
55
+ "Accept": accept,
56
+ "x-api-key": self.api_key,
57
+ }
58
+ if self.username:
59
+ headers["x-username"] = self.username
60
+ return headers
61
+
62
+ def _raise_http_error(self, exc: HTTPError) -> None:
63
+ body: Any
64
+ try:
65
+ body = json.loads(exc.read().decode("utf-8"))
66
+ except Exception:
67
+ body = None
68
+ message = None
69
+ if isinstance(body, dict):
70
+ message = body.get("message") or body.get("error")
71
+ raise FlowraAPIError(
72
+ message or f"HTTP {exc.code}",
73
+ status_code=exc.code,
74
+ body=body,
75
+ ) from exc
76
+
77
+ def request(
78
+ self,
79
+ method: str,
80
+ path: str,
81
+ *,
82
+ query: Optional[Mapping[str, Any]] = None,
83
+ json_body: Any = None,
84
+ binary: bool = False,
85
+ extra_headers: Optional[Mapping[str, str]] = None,
86
+ ) -> Any:
87
+ url = self._build_url(path, query)
88
+ headers = self._base_headers(
89
+ accept="application/octet-stream" if binary else "application/json",
90
+ )
91
+ if extra_headers:
92
+ headers.update(extra_headers)
93
+
94
+ data: Optional[bytes] = None
95
+ if json_body is not None:
96
+ headers["Content-Type"] = "application/json"
97
+ data = json.dumps(json_body).encode("utf-8")
98
+
99
+ request = Request(url, data=data, headers=dict(headers), method=method.upper())
100
+ try:
101
+ with urlopen(request, timeout=self.timeout) as response:
102
+ raw = response.read()
103
+ if not raw:
104
+ return None
105
+ if binary:
106
+ return raw
107
+ content_type = response.headers.get("Content-Type", "")
108
+ if "application/json" in content_type:
109
+ return json.loads(raw.decode("utf-8"))
110
+ return raw.decode("utf-8")
111
+ except HTTPError as exc:
112
+ self._raise_http_error(exc)
113
+ except URLError as exc:
114
+ raise FlowraAPIError(str(exc.reason)) from exc
115
+
116
+ def request_multipart(
117
+ self,
118
+ method: str,
119
+ path: str,
120
+ *,
121
+ query: Optional[Mapping[str, Any]] = None,
122
+ fields: Optional[Mapping[str, Any]] = None,
123
+ files: Optional[Mapping[str, FileBody]] = None,
124
+ file_field: str = "file",
125
+ ) -> Any:
126
+ """POST/PUT multipart/form-data (used for file upload)."""
127
+ url = self._build_url(path, query)
128
+ boundary = f"----FlowraFormBoundary{uuid.uuid4().hex}"
129
+ body = bytearray()
130
+
131
+ for key, value in (fields or {}).items():
132
+ if value is None:
133
+ continue
134
+ body.extend(f"--{boundary}\r\n".encode("utf-8"))
135
+ body.extend(
136
+ f'Content-Disposition: form-data; name="{key}"\r\n\r\n'.encode("utf-8")
137
+ )
138
+ body.extend(str(value).encode("utf-8"))
139
+ body.extend(b"\r\n")
140
+
141
+ for name, file_value in (files or {}).items():
142
+ filename, content, content_type = self._read_file(file_value)
143
+ field_name = name or file_field
144
+ body.extend(f"--{boundary}\r\n".encode("utf-8"))
145
+ body.extend(
146
+ (
147
+ f'Content-Disposition: form-data; name="{field_name}"; '
148
+ f'filename="{filename}"\r\n'
149
+ ).encode("utf-8")
150
+ )
151
+ body.extend(f"Content-Type: {content_type}\r\n\r\n".encode("utf-8"))
152
+ body.extend(content)
153
+ body.extend(b"\r\n")
154
+
155
+ body.extend(f"--{boundary}--\r\n".encode("utf-8"))
156
+
157
+ headers = self._base_headers()
158
+ headers["Content-Type"] = f"multipart/form-data; boundary={boundary}"
159
+
160
+ request = Request(
161
+ url,
162
+ data=bytes(body),
163
+ headers=dict(headers),
164
+ method=method.upper(),
165
+ )
166
+ try:
167
+ with urlopen(request, timeout=self.timeout) as response:
168
+ raw = response.read()
169
+ if not raw:
170
+ return None
171
+ content_type = response.headers.get("Content-Type", "")
172
+ if "application/json" in content_type:
173
+ return json.loads(raw.decode("utf-8"))
174
+ return raw.decode("utf-8")
175
+ except HTTPError as exc:
176
+ self._raise_http_error(exc)
177
+ except URLError as exc:
178
+ raise FlowraAPIError(str(exc.reason)) from exc
179
+
180
+ def stream_text(
181
+ self,
182
+ method: str,
183
+ path: str,
184
+ *,
185
+ query: Optional[Mapping[str, Any]] = None,
186
+ json_body: Any = None,
187
+ extra_headers: Optional[Mapping[str, str]] = None,
188
+ chunk_size: int = 1024,
189
+ ) -> Iterator[str]:
190
+ """Yield decoded text chunks from a streaming response (SSE)."""
191
+ url = self._build_url(path, query)
192
+ headers = self._base_headers(accept="text/event-stream")
193
+ if extra_headers:
194
+ headers.update(extra_headers)
195
+ data: Optional[bytes] = None
196
+ if json_body is not None:
197
+ headers["Content-Type"] = "application/json"
198
+ data = json.dumps(json_body).encode("utf-8")
199
+
200
+ request = Request(url, data=data, headers=dict(headers), method=method.upper())
201
+ try:
202
+ response = urlopen(request, timeout=self.timeout)
203
+ except HTTPError as exc:
204
+ self._raise_http_error(exc)
205
+ return # pragma: no cover
206
+ except URLError as exc:
207
+ raise FlowraAPIError(str(exc.reason)) from exc
208
+
209
+ try:
210
+ while True:
211
+ chunk = response.read(chunk_size)
212
+ if not chunk:
213
+ break
214
+ yield chunk.decode("utf-8", errors="replace")
215
+ finally:
216
+ response.close()
217
+
218
+ @staticmethod
219
+ def _read_file(file_value: FileBody) -> tuple[str, bytes, str]:
220
+ if isinstance(file_value, (str, Path)):
221
+ path = Path(file_value)
222
+ content = path.read_bytes()
223
+ filename = path.name
224
+ content_type = mimetypes.guess_type(filename)[0] or "application/octet-stream"
225
+ return filename, content, content_type
226
+
227
+ if isinstance(file_value, bytes):
228
+ return "upload.bin", file_value, "application/octet-stream"
229
+
230
+ # BinaryIO / file-like
231
+ name = getattr(file_value, "name", "upload.bin")
232
+ filename = Path(str(name)).name if name else "upload.bin"
233
+ content = file_value.read()
234
+ if isinstance(content, str):
235
+ content = content.encode("utf-8")
236
+ content_type = mimetypes.guess_type(filename)[0] or "application/octet-stream"
237
+ return filename, content, content_type