fleeks-sdk 0.7.0__tar.gz → 0.7.2__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.
- {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/PKG-INFO +3 -1
- {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/README.md +2 -0
- {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk/__init__.py +4 -2
- {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk/automations.py +2 -2
- {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk/channels.py +43 -8
- {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk/client.py +30 -7
- {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk/exceptions.py +26 -1
- {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk/files.py +2 -8
- {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk/models.py +195 -36
- {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk/previews.py +48 -11
- {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk/schedules.py +2 -2
- {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk/voice.py +11 -5
- {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk.egg-info/PKG-INFO +3 -1
- {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/pyproject.toml +1 -1
- {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/LICENSE +0 -0
- {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk/agents.py +0 -0
- {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk/ai_keys.py +0 -0
- {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk/auth.py +0 -0
- {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk/config.py +0 -0
- {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk/containers.py +0 -0
- {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk/deploy.py +0 -0
- {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk/embeds.py +0 -0
- {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk/lifecycle.py +0 -0
- {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk/streaming.py +0 -0
- {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk/terminal.py +0 -0
- {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk/workspaces.py +0 -0
- {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk.egg-info/SOURCES.txt +0 -0
- {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk.egg-info/dependency_links.txt +0 -0
- {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk.egg-info/requires.txt +0 -0
- {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk.egg-info/top_level.txt +0 -0
- {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/setup.cfg +0 -0
- {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/tests/test_client.py +0 -0
- {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/tests/test_init.py +0 -0
- {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/tests/test_schedules_dashboards.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: fleeks-sdk
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.2
|
|
4
4
|
Summary: Python SDK for the Fleeks AI Development Platform
|
|
5
5
|
Author-email: Fleeks Inc <support@fleeks.com>
|
|
6
6
|
License: MIT
|
|
@@ -86,6 +86,8 @@ Fleeks is a revolutionary AI-powered development platform that provides instant
|
|
|
86
86
|
pip install fleeks-sdk
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
+
For the best compatibility with API key metadata, usage stats, voice, and preview routes, use Fleeks backend `2026.05.13` or newer. The SDK includes fallbacks for older backends where possible.
|
|
90
|
+
|
|
89
91
|
## 🚀 Quick Start
|
|
90
92
|
|
|
91
93
|
```python
|
|
@@ -42,6 +42,8 @@ Fleeks is a revolutionary AI-powered development platform that provides instant
|
|
|
42
42
|
pip install fleeks-sdk
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
+
For the best compatibility with API key metadata, usage stats, voice, and preview routes, use Fleeks backend `2026.05.13` or newer. The SDK includes fallbacks for older backends where possible.
|
|
46
|
+
|
|
45
47
|
## 🚀 Quick Start
|
|
46
48
|
|
|
47
49
|
```python
|
|
@@ -17,7 +17,7 @@ Features:
|
|
|
17
17
|
- Type hints throughout
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
|
-
__version__ = "0.7.
|
|
20
|
+
__version__ = "0.7.2"
|
|
21
21
|
__author__ = "Fleeks Inc"
|
|
22
22
|
__email__ = "support@fleeks.com"
|
|
23
23
|
|
|
@@ -86,7 +86,8 @@ from .exceptions import (
|
|
|
86
86
|
FleeksFeatureUnsupportedError,
|
|
87
87
|
FleeksConnectionError,
|
|
88
88
|
FleeksStreamingError,
|
|
89
|
-
FleeksTimeoutError
|
|
89
|
+
FleeksTimeoutError,
|
|
90
|
+
WorkspaceNotReadyError,
|
|
90
91
|
)
|
|
91
92
|
|
|
92
93
|
# Data models
|
|
@@ -304,4 +305,5 @@ __all__ = [
|
|
|
304
305
|
"FleeksConnectionError",
|
|
305
306
|
"FleeksStreamingError",
|
|
306
307
|
"FleeksTimeoutError",
|
|
308
|
+
"WorkspaceNotReadyError",
|
|
307
309
|
]
|
|
@@ -106,7 +106,7 @@ class AutomationManager:
|
|
|
106
106
|
if context_mapping is not None:
|
|
107
107
|
body["context_mapping"] = context_mapping
|
|
108
108
|
|
|
109
|
-
response = await self.client.post("automations", json=body)
|
|
109
|
+
response = await self.client.post("automations/", json=body)
|
|
110
110
|
return Automation.from_dict(response)
|
|
111
111
|
|
|
112
112
|
async def list(
|
|
@@ -132,7 +132,7 @@ class AutomationManager:
|
|
|
132
132
|
"limit": str(limit),
|
|
133
133
|
"offset": str(offset),
|
|
134
134
|
}
|
|
135
|
-
response = await self.client.get("automations", params=params)
|
|
135
|
+
response = await self.client.get("automations/", params=params)
|
|
136
136
|
return AutomationList.from_dict(response)
|
|
137
137
|
|
|
138
138
|
async def get(self, automation_id: str) -> Automation:
|
|
@@ -49,6 +49,13 @@ class ChannelManager:
|
|
|
49
49
|
>>> # Initiate OAuth / QR auth
|
|
50
50
|
>>> auth = await client.channels.auth(chan.channel_id)
|
|
51
51
|
>>> print(auth.oauth_url or auth.qr_code_data)
|
|
52
|
+
|
|
53
|
+
Notes:
|
|
54
|
+
- There is no ``send()`` method here. Outbound messaging is
|
|
55
|
+
handled by the agent daemon once the channel is authenticated
|
|
56
|
+
and the schedule is running. Use :meth:`test` to verify
|
|
57
|
+
credentials, and ``client.schedules.start(...)`` to bring the
|
|
58
|
+
agent online.
|
|
52
59
|
"""
|
|
53
60
|
|
|
54
61
|
def __init__(self, client):
|
|
@@ -125,7 +132,7 @@ class ChannelManager:
|
|
|
125
132
|
if message_filter is not None:
|
|
126
133
|
body["message_filter"] = message_filter
|
|
127
134
|
|
|
128
|
-
response = await self.client.post("channels", json=body)
|
|
135
|
+
response = await self.client.post("channels/", json=body)
|
|
129
136
|
return Channel.from_dict(response)
|
|
130
137
|
|
|
131
138
|
async def list(
|
|
@@ -151,7 +158,7 @@ class ChannelManager:
|
|
|
151
158
|
"limit": str(limit),
|
|
152
159
|
"offset": str(offset),
|
|
153
160
|
}
|
|
154
|
-
response = await self.client.get("channels", params=params)
|
|
161
|
+
response = await self.client.get("channels/", params=params)
|
|
155
162
|
return ChannelList.from_dict(response)
|
|
156
163
|
|
|
157
164
|
async def get(self, channel_id: str) -> Channel:
|
|
@@ -189,12 +196,30 @@ class ChannelManager:
|
|
|
189
196
|
channel_id: Channel identifier.
|
|
190
197
|
**kwargs: Fields to update. Accepted keys:
|
|
191
198
|
channel_name, route_to_agents, default_agent,
|
|
192
|
-
message_filter, rate_limit_per_minute,
|
|
199
|
+
message_filter, rate_limit_per_minute,
|
|
200
|
+
rate_limit_per_hour, is_active, credentials.
|
|
201
|
+
|
|
202
|
+
``credentials`` is rotated through the secrets vault
|
|
203
|
+
(GCP Secret Manager when available, Fernet otherwise)
|
|
204
|
+
and never persisted to the database directly.
|
|
193
205
|
|
|
194
206
|
Returns:
|
|
195
207
|
Channel: Updated channel.
|
|
196
208
|
"""
|
|
197
|
-
|
|
209
|
+
allowed = {
|
|
210
|
+
"channel_name",
|
|
211
|
+
"route_to_agents",
|
|
212
|
+
"default_agent",
|
|
213
|
+
"message_filter",
|
|
214
|
+
"rate_limit_per_minute",
|
|
215
|
+
"rate_limit_per_hour",
|
|
216
|
+
"is_active",
|
|
217
|
+
"credentials",
|
|
218
|
+
}
|
|
219
|
+
body = {
|
|
220
|
+
k: v for k, v in kwargs.items()
|
|
221
|
+
if v is not None and k in allowed
|
|
222
|
+
}
|
|
198
223
|
response = await self.client.put(f"channels/{channel_id}", json=body)
|
|
199
224
|
return Channel.from_dict(response)
|
|
200
225
|
|
|
@@ -241,25 +266,35 @@ class ChannelManager:
|
|
|
241
266
|
"""
|
|
242
267
|
Check the authentication status of a pending auth flow.
|
|
243
268
|
|
|
269
|
+
The backend returns a flatter payload here than :meth:`auth`
|
|
270
|
+
(no ``data`` envelope): the QR image / OAuth URL is folded
|
|
271
|
+
directly into the response. ``AuthFlowResult.from_dict`` handles
|
|
272
|
+
both shapes, so ``.oauth_url``, ``.qr_image`` and ``.qr_code_data``
|
|
273
|
+
work uniformly.
|
|
274
|
+
|
|
244
275
|
Args:
|
|
245
276
|
channel_id: Channel identifier.
|
|
246
277
|
|
|
247
278
|
Returns:
|
|
248
|
-
AuthFlowResult: Current auth status.
|
|
279
|
+
AuthFlowResult: Current auth status. Use
|
|
280
|
+
:attr:`AuthFlowResult.is_authenticated` to check completion.
|
|
249
281
|
"""
|
|
250
|
-
response = await self.client.get(
|
|
282
|
+
response = await self.client.get(
|
|
283
|
+
f"channels/{channel_id}/auth/status"
|
|
284
|
+
)
|
|
251
285
|
return AuthFlowResult.from_dict(response)
|
|
252
286
|
|
|
253
287
|
async def test(self, channel_id: str) -> Dict[str, Any]:
|
|
254
288
|
"""
|
|
255
289
|
Test a channel connection.
|
|
256
290
|
|
|
257
|
-
|
|
291
|
+
Validates the stored credentials. A full end-to-end message
|
|
292
|
+
round-trip additionally requires the schedule to be running.
|
|
258
293
|
|
|
259
294
|
Args:
|
|
260
295
|
channel_id: Channel identifier.
|
|
261
296
|
|
|
262
297
|
Returns:
|
|
263
|
-
dict:
|
|
298
|
+
dict: ``{channel_id, channel_type, test_result, message}``.
|
|
264
299
|
"""
|
|
265
300
|
return await self.client.post(f"channels/{channel_id}/test")
|
|
@@ -8,11 +8,17 @@ from typing import Dict, Any, Optional, List, Union, AsyncContextManager
|
|
|
8
8
|
from contextlib import asynccontextmanager
|
|
9
9
|
|
|
10
10
|
import httpx
|
|
11
|
-
from tenacity import retry, stop_after_attempt, wait_exponential
|
|
11
|
+
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception
|
|
12
12
|
from pydantic import BaseModel
|
|
13
13
|
|
|
14
14
|
from .config import Config
|
|
15
15
|
from .exceptions import FleeksAPIError, FleeksException, FleeksRateLimitError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _is_transient_error(exc: BaseException) -> bool:
|
|
19
|
+
"""Retry only on rate-limits and network-level errors, never on 4xx API errors."""
|
|
20
|
+
import asyncio
|
|
21
|
+
return isinstance(exc, (FleeksRateLimitError, httpx.RequestError, asyncio.CancelledError))
|
|
16
22
|
from .auth import APIKeyAuth
|
|
17
23
|
from .workspaces import WorkspaceManager
|
|
18
24
|
from .agents import AgentManager
|
|
@@ -104,7 +110,8 @@ class FleeksClient:
|
|
|
104
110
|
base_url=self.base_url,
|
|
105
111
|
timeout=httpx.Timeout(self.config.timeout),
|
|
106
112
|
headers={
|
|
107
|
-
'X-API-Key': self.api_key,
|
|
113
|
+
'X-API-Key': self.api_key,
|
|
114
|
+
'Authorization': f'Bearer {self.api_key}',
|
|
108
115
|
'Content-Type': 'application/json',
|
|
109
116
|
'User-Agent': f'fleeks-python-sdk/{self.config.version}',
|
|
110
117
|
'Accept': 'application/json'
|
|
@@ -114,7 +121,8 @@ class FleeksClient:
|
|
|
114
121
|
|
|
115
122
|
@retry(
|
|
116
123
|
stop=stop_after_attempt(3),
|
|
117
|
-
wait=wait_exponential(multiplier=1, min=1, max=10)
|
|
124
|
+
wait=wait_exponential(multiplier=1, min=1, max=10),
|
|
125
|
+
retry=retry_if_exception(_is_transient_error),
|
|
118
126
|
)
|
|
119
127
|
async def _make_request(
|
|
120
128
|
self,
|
|
@@ -140,8 +148,9 @@ class FleeksClient:
|
|
|
140
148
|
"""
|
|
141
149
|
await self._ensure_client()
|
|
142
150
|
|
|
143
|
-
#
|
|
144
|
-
|
|
151
|
+
# Strip leading slash only; preserve any trailing slash the caller explicitly adds
|
|
152
|
+
# (collection routes like /sdk/schedules/ require it when redirect_slashes=False).
|
|
153
|
+
normalized_endpoint = endpoint.lstrip('/')
|
|
145
154
|
prefix = kwargs.pop('_url_prefix', '/api/v1/sdk')
|
|
146
155
|
url = f"{prefix}/{normalized_endpoint}"
|
|
147
156
|
|
|
@@ -166,6 +175,10 @@ class FleeksClient:
|
|
|
166
175
|
|
|
167
176
|
response.raise_for_status()
|
|
168
177
|
|
|
178
|
+
# Handle empty bodies (e.g. 204 No Content from PUT/DELETE)
|
|
179
|
+
if not response.content:
|
|
180
|
+
return {}
|
|
181
|
+
|
|
169
182
|
# Handle different content types
|
|
170
183
|
content_type = response.headers.get('content-type', '')
|
|
171
184
|
if 'application/json' in content_type:
|
|
@@ -472,11 +485,21 @@ class FleeksClient:
|
|
|
472
485
|
|
|
473
486
|
async def get_usage_stats(self) -> Dict[str, Any]:
|
|
474
487
|
"""Get current usage statistics and rate limits."""
|
|
475
|
-
|
|
488
|
+
try:
|
|
489
|
+
return await self.get('usage/stats') # /api/v1/sdk/usage/stats (backend >= 2026.05.13)
|
|
490
|
+
except FleeksAPIError as exc:
|
|
491
|
+
if exc.status_code in (404, 500):
|
|
492
|
+
return await self._make_request('GET', 'billing/usage', _url_prefix='/api/v1')
|
|
493
|
+
raise
|
|
476
494
|
|
|
477
495
|
async def get_api_key_info(self) -> Dict[str, Any]:
|
|
478
496
|
"""Get information about the current API key."""
|
|
479
|
-
|
|
497
|
+
try:
|
|
498
|
+
return await self.get('auth/key-info') # /api/v1/sdk/auth/key-info (backend >= 2026.05.13)
|
|
499
|
+
except FleeksAPIError as exc:
|
|
500
|
+
if exc.status_code in (404, 500):
|
|
501
|
+
return await self._make_request('GET', 'auth/me', _url_prefix='/api/v1')
|
|
502
|
+
raise
|
|
480
503
|
|
|
481
504
|
|
|
482
505
|
# Convenience function for quick usage
|
|
@@ -122,4 +122,29 @@ class FleeksStreamingError(FleeksException):
|
|
|
122
122
|
|
|
123
123
|
class FleeksTimeoutError(FleeksException):
|
|
124
124
|
"""Exception raised for timeout errors."""
|
|
125
|
-
pass
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class WorkspaceNotReadyError(FleeksAPIError):
|
|
129
|
+
"""
|
|
130
|
+
Raised when an operation requires a running workspace container but none
|
|
131
|
+
is available (HTTP 409 with error_code='container_not_running').
|
|
132
|
+
|
|
133
|
+
Attributes:
|
|
134
|
+
ready_for_preview: Always False when raised.
|
|
135
|
+
remediation: List of suggested remediation steps from the backend.
|
|
136
|
+
project_id: Project ID that triggered the error.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
def __init__(
|
|
140
|
+
self,
|
|
141
|
+
message: str = "Workspace exists but no running container is available. Start the workspace container first.",
|
|
142
|
+
status_code: int = 409,
|
|
143
|
+
response: Optional[Any] = None,
|
|
144
|
+
project_id: Optional[int] = None,
|
|
145
|
+
remediation: Optional[list] = None,
|
|
146
|
+
):
|
|
147
|
+
super().__init__(message, status_code, response)
|
|
148
|
+
self.ready_for_preview: bool = False
|
|
149
|
+
self.project_id = project_id
|
|
150
|
+
self.remediation: list = remediation or []
|
|
@@ -220,17 +220,11 @@ class FileManager:
|
|
|
220
220
|
... content='{"debug": true}'
|
|
221
221
|
... )
|
|
222
222
|
"""
|
|
223
|
-
data = {
|
|
224
|
-
'path': path,
|
|
225
|
-
'content': content,
|
|
226
|
-
'encoding': encoding,
|
|
227
|
-
'create_if_missing': create_if_missing
|
|
228
|
-
}
|
|
229
|
-
|
|
230
223
|
response = await self.client._make_request(
|
|
231
224
|
'PUT',
|
|
232
225
|
f'files/{self.project_id}/content',
|
|
233
|
-
|
|
226
|
+
params={'path': path},
|
|
227
|
+
json={'content': content, 'encoding': encoding}
|
|
234
228
|
)
|
|
235
229
|
return FileInfo.from_dict(response)
|
|
236
230
|
|
|
@@ -1688,44 +1688,111 @@ class ChannelType(str, Enum):
|
|
|
1688
1688
|
|
|
1689
1689
|
@dataclass
|
|
1690
1690
|
class ChannelTypeInfo:
|
|
1691
|
-
"""
|
|
1692
|
-
|
|
1693
|
-
|
|
1691
|
+
"""
|
|
1692
|
+
Information about a supported messaging channel type.
|
|
1693
|
+
|
|
1694
|
+
Matches backend ``ChannelTypeInfo`` in
|
|
1695
|
+
``app/api/api_v1/endpoints/sdk/agent_channels.py``::
|
|
1696
|
+
|
|
1697
|
+
{
|
|
1698
|
+
"type_id": "slack",
|
|
1699
|
+
"name": "Slack",
|
|
1700
|
+
"description": "...",
|
|
1701
|
+
"required_credentials": ["bot_token"],
|
|
1702
|
+
"optional_credentials": ["app_token"],
|
|
1703
|
+
"auth_flow": "oauth", # token | oauth | qr_code | api_key
|
|
1704
|
+
"docs_url": "https://..."
|
|
1705
|
+
}
|
|
1706
|
+
"""
|
|
1707
|
+
type_id: str
|
|
1708
|
+
name: str
|
|
1694
1709
|
description: str
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1710
|
+
auth_flow: str = "token"
|
|
1711
|
+
required_credentials: List[str] = field(default_factory=list)
|
|
1712
|
+
optional_credentials: List[str] = field(default_factory=list)
|
|
1713
|
+
docs_url: str = ""
|
|
1714
|
+
|
|
1715
|
+
# ── Backwards-compat aliases ─────────────────────────────
|
|
1716
|
+
@property
|
|
1717
|
+
def channel_type(self) -> str:
|
|
1718
|
+
"""Alias for ``type_id`` (older SDK callers)."""
|
|
1719
|
+
return self.type_id
|
|
1720
|
+
|
|
1721
|
+
@property
|
|
1722
|
+
def display_name(self) -> str:
|
|
1723
|
+
"""Alias for ``name`` (older SDK callers)."""
|
|
1724
|
+
return self.name
|
|
1725
|
+
|
|
1726
|
+
@property
|
|
1727
|
+
def auth_required(self) -> bool:
|
|
1728
|
+
"""True unless the channel explicitly has no required credentials
|
|
1729
|
+
AND no OAuth/QR step (e.g. a future fully-public channel)."""
|
|
1730
|
+
return bool(self.required_credentials) or self.auth_flow in (
|
|
1731
|
+
"oauth", "qr_code"
|
|
1732
|
+
)
|
|
1698
1733
|
|
|
1699
1734
|
@classmethod
|
|
1700
1735
|
def from_dict(cls, data: Dict[str, Any]) -> 'ChannelTypeInfo':
|
|
1736
|
+
# Accept either backend field names (preferred) or legacy SDK
|
|
1737
|
+
# field names so demo code written against the old model keeps
|
|
1738
|
+
# working.
|
|
1739
|
+
type_id = data.get('type_id') or data.get('channel_type')
|
|
1740
|
+
if not type_id:
|
|
1741
|
+
raise KeyError(
|
|
1742
|
+
"ChannelTypeInfo response missing 'type_id' "
|
|
1743
|
+
f"(got keys: {sorted(data.keys())})"
|
|
1744
|
+
)
|
|
1701
1745
|
return cls(
|
|
1702
|
-
|
|
1703
|
-
|
|
1746
|
+
type_id=type_id,
|
|
1747
|
+
name=data.get('name') or data.get('display_name', ''),
|
|
1704
1748
|
description=data.get('description', ''),
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1749
|
+
auth_flow=data.get('auth_flow', 'token'),
|
|
1750
|
+
required_credentials=list(data.get('required_credentials', [])),
|
|
1751
|
+
optional_credentials=list(data.get('optional_credentials', [])),
|
|
1752
|
+
docs_url=data.get('docs_url', ''),
|
|
1708
1753
|
)
|
|
1709
1754
|
|
|
1710
1755
|
|
|
1711
1756
|
@dataclass
|
|
1712
1757
|
class Channel:
|
|
1713
1758
|
"""
|
|
1714
|
-
Messaging channel — matches backend ChannelResponse
|
|
1759
|
+
Messaging channel — matches backend ``ChannelResponse``.
|
|
1715
1760
|
|
|
1716
1761
|
Represents a connected messaging platform integration.
|
|
1717
1762
|
"""
|
|
1718
1763
|
channel_id: str
|
|
1719
1764
|
schedule_id: str
|
|
1720
1765
|
channel_type: str
|
|
1721
|
-
|
|
1766
|
+
channel_name: Optional[str]
|
|
1722
1767
|
status: str
|
|
1768
|
+
is_active: bool
|
|
1723
1769
|
created_at: str
|
|
1724
|
-
|
|
1725
|
-
|
|
1770
|
+
connected_at: Optional[str] = None
|
|
1771
|
+
route_to_agents: List[str] = field(default_factory=list)
|
|
1772
|
+
default_agent: Optional[str] = None
|
|
1773
|
+
rate_limit_per_minute: int = 60
|
|
1774
|
+
rate_limit_per_hour: int = 1000
|
|
1775
|
+
messages_received: int = 0
|
|
1776
|
+
messages_sent: int = 0
|
|
1726
1777
|
last_message_at: Optional[str] = None
|
|
1727
|
-
|
|
1728
|
-
|
|
1778
|
+
last_error: Optional[str] = None
|
|
1779
|
+
last_error_at: Optional[str] = None
|
|
1780
|
+
|
|
1781
|
+
# ── Backwards-compat aliases ─────────────────────────────
|
|
1782
|
+
@property
|
|
1783
|
+
def name(self) -> str:
|
|
1784
|
+
"""Alias for ``channel_name``."""
|
|
1785
|
+
return self.channel_name or ""
|
|
1786
|
+
|
|
1787
|
+
@property
|
|
1788
|
+
def message_count(self) -> int:
|
|
1789
|
+
"""Total messages handled by the channel (in + out)."""
|
|
1790
|
+
return (self.messages_received or 0) + (self.messages_sent or 0)
|
|
1791
|
+
|
|
1792
|
+
@property
|
|
1793
|
+
def error_message(self) -> Optional[str]:
|
|
1794
|
+
"""Alias for ``last_error``."""
|
|
1795
|
+
return self.last_error
|
|
1729
1796
|
|
|
1730
1797
|
@classmethod
|
|
1731
1798
|
def from_dict(cls, data: Dict[str, Any]) -> 'Channel':
|
|
@@ -1733,14 +1800,20 @@ class Channel:
|
|
|
1733
1800
|
channel_id=data['channel_id'],
|
|
1734
1801
|
schedule_id=data['schedule_id'],
|
|
1735
1802
|
channel_type=data['channel_type'],
|
|
1736
|
-
|
|
1737
|
-
status=data.get('status', '
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1803
|
+
channel_name=data.get('channel_name') or data.get('name'),
|
|
1804
|
+
status=data.get('status', 'disconnected'),
|
|
1805
|
+
is_active=bool(data.get('is_active', True)),
|
|
1806
|
+
created_at=data.get('created_at', ''),
|
|
1807
|
+
connected_at=data.get('connected_at'),
|
|
1808
|
+
route_to_agents=list(data.get('route_to_agents', []) or []),
|
|
1809
|
+
default_agent=data.get('default_agent'),
|
|
1810
|
+
rate_limit_per_minute=int(data.get('rate_limit_per_minute', 60)),
|
|
1811
|
+
rate_limit_per_hour=int(data.get('rate_limit_per_hour', 1000)),
|
|
1812
|
+
messages_received=int(data.get('messages_received', 0) or 0),
|
|
1813
|
+
messages_sent=int(data.get('messages_sent', 0) or 0),
|
|
1741
1814
|
last_message_at=data.get('last_message_at'),
|
|
1742
|
-
|
|
1743
|
-
|
|
1815
|
+
last_error=data.get('last_error') or data.get('error_message'),
|
|
1816
|
+
last_error_at=data.get('last_error_at'),
|
|
1744
1817
|
)
|
|
1745
1818
|
|
|
1746
1819
|
|
|
@@ -1755,25 +1828,102 @@ class ChannelList:
|
|
|
1755
1828
|
channels = [Channel.from_dict(c) for c in data.get('channels', [])]
|
|
1756
1829
|
return cls(
|
|
1757
1830
|
channels=channels,
|
|
1758
|
-
total=data.get('total', len(channels)),
|
|
1831
|
+
total=int(data.get('total', len(channels))),
|
|
1759
1832
|
)
|
|
1760
1833
|
|
|
1761
1834
|
|
|
1762
1835
|
@dataclass
|
|
1763
1836
|
class AuthFlowResult:
|
|
1764
|
-
"""
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1837
|
+
"""
|
|
1838
|
+
Result of initiating a channel auth flow.
|
|
1839
|
+
|
|
1840
|
+
Matches backend ``AuthFlowResponse``::
|
|
1841
|
+
|
|
1842
|
+
{
|
|
1843
|
+
"auth_type": "oauth" | "qr_code" | "token",
|
|
1844
|
+
"status": "pending" | "authenticated" | "incomplete" | ...,
|
|
1845
|
+
"data": { ... provider-specific payload ... },
|
|
1846
|
+
"message": "human readable"
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
Provider-specific payloads in ``data``:
|
|
1850
|
+
- OAuth (Slack/Teams/Google Chat): ``oauth_url`` (or ``auth_url``)
|
|
1851
|
+
- QR (WhatsApp/Signal): ``qr_payload`` and ``qr_image``
|
|
1852
|
+
- Token: empty or ``{"missing_fields": [...]}``
|
|
1853
|
+
"""
|
|
1854
|
+
auth_type: str
|
|
1855
|
+
status: str
|
|
1856
|
+
message: str
|
|
1857
|
+
data: Dict[str, Any] = field(default_factory=dict)
|
|
1858
|
+
channel_id: Optional[str] = None
|
|
1859
|
+
|
|
1860
|
+
# ── Convenience accessors (match the docstring in channels.py) ──
|
|
1861
|
+
def _lookup(self, *keys: str) -> Optional[str]:
|
|
1862
|
+
"""Look up a key in either ``data`` or ``data['details']``.
|
|
1863
|
+
|
|
1864
|
+
``POST /auth`` returns ``{auth_type, status, data: {...}}`` but
|
|
1865
|
+
``GET /auth/status`` returns a flatter shape with a ``details``
|
|
1866
|
+
sub-dict — accept both.
|
|
1867
|
+
"""
|
|
1868
|
+
details = self.data.get('details') if isinstance(
|
|
1869
|
+
self.data.get('details'), dict
|
|
1870
|
+
) else {}
|
|
1871
|
+
for k in keys:
|
|
1872
|
+
v = self.data.get(k) or details.get(k)
|
|
1873
|
+
if v:
|
|
1874
|
+
return v
|
|
1875
|
+
return None
|
|
1876
|
+
|
|
1877
|
+
@property
|
|
1878
|
+
def oauth_url(self) -> Optional[str]:
|
|
1879
|
+
"""OAuth redirect URL (Slack / Teams / Google Chat)."""
|
|
1880
|
+
return self._lookup('oauth_url', 'auth_url')
|
|
1881
|
+
|
|
1882
|
+
# Older callers used ``auth_url`` directly on the result.
|
|
1883
|
+
@property
|
|
1884
|
+
def auth_url(self) -> Optional[str]:
|
|
1885
|
+
return self.oauth_url
|
|
1886
|
+
|
|
1887
|
+
@property
|
|
1888
|
+
def qr_code_data(self) -> Optional[str]:
|
|
1889
|
+
"""Raw QR payload string (e.g. WhatsApp pairing code)."""
|
|
1890
|
+
return self._lookup('qr_payload', 'qr_code_data')
|
|
1891
|
+
|
|
1892
|
+
@property
|
|
1893
|
+
def qr_image(self) -> Optional[str]:
|
|
1894
|
+
"""Base64-encoded PNG of the QR code (data URL), if provided."""
|
|
1895
|
+
return self._lookup('qr_image')
|
|
1896
|
+
|
|
1897
|
+
@property
|
|
1898
|
+
def missing_fields(self) -> List[str]:
|
|
1899
|
+
"""Credential fields the user still needs to supply (token auth)."""
|
|
1900
|
+
return list(self.data.get('missing_fields', []) or [])
|
|
1901
|
+
|
|
1902
|
+
@property
|
|
1903
|
+
def is_authenticated(self) -> bool:
|
|
1904
|
+
return self.status == 'authenticated'
|
|
1905
|
+
|
|
1906
|
+
@property
|
|
1907
|
+
def is_pending(self) -> bool:
|
|
1908
|
+
return self.status == 'pending'
|
|
1769
1909
|
|
|
1770
1910
|
@classmethod
|
|
1771
1911
|
def from_dict(cls, data: Dict[str, Any]) -> 'AuthFlowResult':
|
|
1912
|
+
# Be lenient: some endpoints (auth/status) return a flatter shape
|
|
1913
|
+
# without the outer ``data`` envelope; absorb the whole dict so
|
|
1914
|
+
# ``.oauth_url`` / ``.qr_payload`` still resolve.
|
|
1915
|
+
payload = data.get('data')
|
|
1916
|
+
if not isinstance(payload, dict):
|
|
1917
|
+
payload = {
|
|
1918
|
+
k: v for k, v in data.items()
|
|
1919
|
+
if k not in {'auth_type', 'status', 'message', 'channel_id'}
|
|
1920
|
+
}
|
|
1772
1921
|
return cls(
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1922
|
+
auth_type=data.get('auth_type', 'unknown'),
|
|
1923
|
+
status=data.get('status', 'pending'),
|
|
1924
|
+
message=data.get('message', ''),
|
|
1925
|
+
data=payload,
|
|
1926
|
+
channel_id=data.get('channel_id'),
|
|
1777
1927
|
)
|
|
1778
1928
|
|
|
1779
1929
|
|
|
@@ -1966,12 +2116,21 @@ class PreviewSessionList:
|
|
|
1966
2116
|
project_id: int
|
|
1967
2117
|
|
|
1968
2118
|
@classmethod
|
|
1969
|
-
def from_dict(cls, data:
|
|
2119
|
+
def from_dict(cls, data: Any) -> 'PreviewSessionList':
|
|
2120
|
+
if isinstance(data, list):
|
|
2121
|
+
sessions = [PreviewSession.from_dict(item) for item in data]
|
|
2122
|
+
project_id = sessions[0].project_id if sessions else 0
|
|
2123
|
+
return cls(
|
|
2124
|
+
sessions=sessions,
|
|
2125
|
+
total=len(sessions),
|
|
2126
|
+
project_id=project_id,
|
|
2127
|
+
)
|
|
2128
|
+
|
|
1970
2129
|
sessions = [PreviewSession.from_dict(s) for s in data.get('sessions', [])]
|
|
1971
2130
|
return cls(
|
|
1972
2131
|
sessions=sessions,
|
|
1973
2132
|
total=data.get('total', len(sessions)),
|
|
1974
|
-
project_id=data.get('project_id', 0),
|
|
2133
|
+
project_id=data.get('project_id', sessions[0].project_id if sessions else 0),
|
|
1975
2134
|
)
|
|
1976
2135
|
|
|
1977
2136
|
|
|
@@ -33,9 +33,30 @@ from .exceptions import (
|
|
|
33
33
|
FleeksAPIError,
|
|
34
34
|
FleeksResourceNotFoundError,
|
|
35
35
|
FleeksValidationError,
|
|
36
|
+
WorkspaceNotReadyError,
|
|
36
37
|
)
|
|
37
38
|
|
|
38
39
|
|
|
40
|
+
def _raise_if_container_not_running(exc: FleeksAPIError) -> None:
|
|
41
|
+
"""Convert a 409 container_not_running API error into WorkspaceNotReadyError."""
|
|
42
|
+
if exc.status_code != 409:
|
|
43
|
+
return
|
|
44
|
+
try:
|
|
45
|
+
detail = exc.response.json().get("detail", {}) if exc.response else {}
|
|
46
|
+
if isinstance(detail, dict) and detail.get("error_code") == "container_not_running":
|
|
47
|
+
raise WorkspaceNotReadyError(
|
|
48
|
+
message=detail.get("message", str(exc)),
|
|
49
|
+
status_code=409,
|
|
50
|
+
response=exc.response,
|
|
51
|
+
project_id=detail.get("project_id"),
|
|
52
|
+
remediation=detail.get("remediation", []),
|
|
53
|
+
) from exc
|
|
54
|
+
except WorkspaceNotReadyError:
|
|
55
|
+
raise
|
|
56
|
+
except Exception:
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
|
|
39
60
|
class PreviewManager:
|
|
40
61
|
"""
|
|
41
62
|
Manage preview sessions for projects.
|
|
@@ -127,12 +148,18 @@ class PreviewManager:
|
|
|
127
148
|
if env_vars is not None:
|
|
128
149
|
body["env_vars"] = env_vars
|
|
129
150
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
151
|
+
try:
|
|
152
|
+
response = await self._client._make_request(
|
|
153
|
+
"POST",
|
|
154
|
+
f"preview/sessions/{project_id}/start",
|
|
155
|
+
json=body,
|
|
156
|
+
_url_prefix="/api/v1",
|
|
157
|
+
)
|
|
158
|
+
return PreviewSession.from_dict(response)
|
|
159
|
+
except FleeksAPIError as exc:
|
|
160
|
+
_raise_if_container_not_running(exc)
|
|
161
|
+
raise
|
|
162
|
+
raise
|
|
136
163
|
|
|
137
164
|
async def get(self, session_id: str) -> PreviewSession:
|
|
138
165
|
"""
|
|
@@ -153,6 +180,7 @@ class PreviewManager:
|
|
|
153
180
|
response = await self._client._make_request(
|
|
154
181
|
"GET",
|
|
155
182
|
f"preview/sessions/{session_id}",
|
|
183
|
+
_url_prefix="/api/v1",
|
|
156
184
|
)
|
|
157
185
|
return PreviewSession.from_dict(response)
|
|
158
186
|
except FleeksAPIError as exc:
|
|
@@ -200,6 +228,7 @@ class PreviewManager:
|
|
|
200
228
|
"GET",
|
|
201
229
|
f"preview/sessions/project/{project_id}",
|
|
202
230
|
params=params,
|
|
231
|
+
_url_prefix="/api/v1",
|
|
203
232
|
)
|
|
204
233
|
return PreviewSessionList.from_dict(response)
|
|
205
234
|
|
|
@@ -228,6 +257,7 @@ class PreviewManager:
|
|
|
228
257
|
return await self._client._make_request(
|
|
229
258
|
"DELETE",
|
|
230
259
|
f"preview/sessions/{session_id}",
|
|
260
|
+
_url_prefix="/api/v1",
|
|
231
261
|
)
|
|
232
262
|
except FleeksAPIError as exc:
|
|
233
263
|
if exc.status_code == 404:
|
|
@@ -262,6 +292,7 @@ class PreviewManager:
|
|
|
262
292
|
response = await self._client._make_request(
|
|
263
293
|
"POST",
|
|
264
294
|
f"preview/sessions/{session_id}/refresh",
|
|
295
|
+
_url_prefix="/api/v1",
|
|
265
296
|
)
|
|
266
297
|
return PreviewSession.from_dict(response)
|
|
267
298
|
except FleeksAPIError as exc:
|
|
@@ -296,6 +327,7 @@ class PreviewManager:
|
|
|
296
327
|
response = await self._client._make_request(
|
|
297
328
|
"GET",
|
|
298
329
|
f"preview/sessions/{session_id}/health",
|
|
330
|
+
_url_prefix="/api/v1",
|
|
299
331
|
)
|
|
300
332
|
return PreviewHealth.from_dict(response)
|
|
301
333
|
|
|
@@ -322,11 +354,16 @@ class PreviewManager:
|
|
|
322
354
|
... f"(confidence {det.confidence:.0%})")
|
|
323
355
|
>>> print(f"Suggested command: {det.suggested_command}")
|
|
324
356
|
"""
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
357
|
+
try:
|
|
358
|
+
response = await self._client._make_request(
|
|
359
|
+
"POST",
|
|
360
|
+
f"preview/sessions/{project_id}/detect",
|
|
361
|
+
_url_prefix="/api/v1",
|
|
362
|
+
)
|
|
363
|
+
return PreviewDetectResult.from_dict(response)
|
|
364
|
+
except FleeksAPIError as exc:
|
|
365
|
+
_raise_if_container_not_running(exc)
|
|
366
|
+
raise
|
|
330
367
|
|
|
331
368
|
# ------------------------------------------------------------------
|
|
332
369
|
# Batch / convenience
|
|
@@ -168,7 +168,7 @@ class ScheduleManager:
|
|
|
168
168
|
if tags is not None:
|
|
169
169
|
body["tags"] = tags
|
|
170
170
|
|
|
171
|
-
response = await self.client.post("schedules", json=body)
|
|
171
|
+
response = await self.client.post("schedules/", json=body)
|
|
172
172
|
return Schedule.from_dict(response)
|
|
173
173
|
|
|
174
174
|
async def list(
|
|
@@ -199,7 +199,7 @@ class ScheduleManager:
|
|
|
199
199
|
if schedule_type:
|
|
200
200
|
params["schedule_type"] = schedule_type
|
|
201
201
|
|
|
202
|
-
response = await self.client.get("schedules", params=params)
|
|
202
|
+
response = await self.client.get("schedules/", params=params)
|
|
203
203
|
return ScheduleList.from_dict(response)
|
|
204
204
|
|
|
205
205
|
async def get(self, schedule_id: str) -> Schedule:
|
|
@@ -176,7 +176,7 @@ class VoiceManager:
|
|
|
176
176
|
Returns:
|
|
177
177
|
Dict with models, voices, limits, and default config.
|
|
178
178
|
"""
|
|
179
|
-
return await self._client.
|
|
179
|
+
return await self._client._make_request("GET", "voice/config", _url_prefix="/api/v1")
|
|
180
180
|
|
|
181
181
|
async def get_sessions(self) -> List[Dict[str, Any]]:
|
|
182
182
|
"""
|
|
@@ -185,8 +185,14 @@ class VoiceManager:
|
|
|
185
185
|
Returns:
|
|
186
186
|
List of active voice session info dicts.
|
|
187
187
|
"""
|
|
188
|
-
result = await self._client.
|
|
189
|
-
|
|
188
|
+
result = await self._client._make_request("GET", "voice/sessions", _url_prefix="/api/v1")
|
|
189
|
+
# Backend (>= 2026.05.13) returns {"sessions": [...], "total": N}
|
|
190
|
+
if isinstance(result, dict):
|
|
191
|
+
return result.get("sessions", [])
|
|
192
|
+
# Bare-list fallback for older backends
|
|
193
|
+
if isinstance(result, list):
|
|
194
|
+
return result
|
|
195
|
+
return []
|
|
190
196
|
|
|
191
197
|
async def get_stats(self) -> Dict[str, Any]:
|
|
192
198
|
"""
|
|
@@ -195,7 +201,7 @@ class VoiceManager:
|
|
|
195
201
|
Returns:
|
|
196
202
|
Dict with active sessions count, total, and capacity info.
|
|
197
203
|
"""
|
|
198
|
-
return await self._client.
|
|
204
|
+
return await self._client._make_request("GET", "voice/stats", _url_prefix="/api/v1")
|
|
199
205
|
|
|
200
206
|
async def health(self) -> Dict[str, Any]:
|
|
201
207
|
"""
|
|
@@ -204,7 +210,7 @@ class VoiceManager:
|
|
|
204
210
|
Returns:
|
|
205
211
|
Dict with health status and component checks.
|
|
206
212
|
"""
|
|
207
|
-
return await self._client.
|
|
213
|
+
return await self._client._make_request("GET", "voice/health", _url_prefix="/api/v1")
|
|
208
214
|
|
|
209
215
|
# ── Socket.IO session management ────────────────────────
|
|
210
216
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: fleeks-sdk
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.2
|
|
4
4
|
Summary: Python SDK for the Fleeks AI Development Platform
|
|
5
5
|
Author-email: Fleeks Inc <support@fleeks.com>
|
|
6
6
|
License: MIT
|
|
@@ -86,6 +86,8 @@ Fleeks is a revolutionary AI-powered development platform that provides instant
|
|
|
86
86
|
pip install fleeks-sdk
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
+
For the best compatibility with API key metadata, usage stats, voice, and preview routes, use Fleeks backend `2026.05.13` or newer. The SDK includes fallbacks for older backends where possible.
|
|
90
|
+
|
|
89
91
|
## 🚀 Quick Start
|
|
90
92
|
|
|
91
93
|
```python
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|