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.
Files changed (34) hide show
  1. {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/PKG-INFO +3 -1
  2. {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/README.md +2 -0
  3. {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk/__init__.py +4 -2
  4. {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk/automations.py +2 -2
  5. {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk/channels.py +43 -8
  6. {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk/client.py +30 -7
  7. {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk/exceptions.py +26 -1
  8. {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk/files.py +2 -8
  9. {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk/models.py +195 -36
  10. {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk/previews.py +48 -11
  11. {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk/schedules.py +2 -2
  12. {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk/voice.py +11 -5
  13. {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk.egg-info/PKG-INFO +3 -1
  14. {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/pyproject.toml +1 -1
  15. {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/LICENSE +0 -0
  16. {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk/agents.py +0 -0
  17. {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk/ai_keys.py +0 -0
  18. {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk/auth.py +0 -0
  19. {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk/config.py +0 -0
  20. {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk/containers.py +0 -0
  21. {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk/deploy.py +0 -0
  22. {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk/embeds.py +0 -0
  23. {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk/lifecycle.py +0 -0
  24. {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk/streaming.py +0 -0
  25. {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk/terminal.py +0 -0
  26. {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk/workspaces.py +0 -0
  27. {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk.egg-info/SOURCES.txt +0 -0
  28. {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk.egg-info/dependency_links.txt +0 -0
  29. {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk.egg-info/requires.txt +0 -0
  30. {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/fleeks_sdk.egg-info/top_level.txt +0 -0
  31. {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/setup.cfg +0 -0
  32. {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/tests/test_client.py +0 -0
  33. {fleeks_sdk-0.7.0 → fleeks_sdk-0.7.2}/tests/test_init.py +0 -0
  34. {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.0
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.0"
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, rate_limit_per_hour.
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
- body = {k: v for k, v in kwargs.items() if v is not None}
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(f"channels/{channel_id}/auth/status")
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
- Sends a test message and verifies connectivity.
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: Test result with status and message.
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, # Backend expects X-API-Key header, not Bearer
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
- # Normalize endpoint no trailing slash (FastAPI convention)
144
- normalized_endpoint = endpoint.strip('/')
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
- return await self.get('/usage/stats')
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
- return await self.get('/auth/key-info')
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
- json=data
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
- """Information about a supported channel type."""
1692
- channel_type: str
1693
- display_name: str
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
- auth_required: bool = True
1696
- auth_flow: str = "oauth2"
1697
- supported_features: List[str] = field(default_factory=list)
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
- channel_type=data['channel_type'],
1703
- display_name=data.get('display_name', ''),
1746
+ type_id=type_id,
1747
+ name=data.get('name') or data.get('display_name', ''),
1704
1748
  description=data.get('description', ''),
1705
- auth_required=data.get('auth_required', True),
1706
- auth_flow=data.get('auth_flow', 'oauth2'),
1707
- supported_features=data.get('supported_features', []),
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
- name: str
1766
+ channel_name: Optional[str]
1722
1767
  status: str
1768
+ is_active: bool
1723
1769
  created_at: str
1724
- updated_at: str
1725
- config: Dict[str, Any] = field(default_factory=dict)
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
- message_count: int = 0
1728
- error_message: Optional[str] = None
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
- name=data.get('name', ''),
1737
- status=data.get('status', 'inactive'),
1738
- created_at=data['created_at'],
1739
- updated_at=data.get('updated_at', data['created_at']),
1740
- config=data.get('config', {}),
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
- message_count=data.get('message_count', 0),
1743
- error_message=data.get('error_message'),
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
- """Result of initiating an OAuth/auth flow for a channel."""
1765
- channel_id: str
1766
- auth_url: str
1767
- expires_in: int = 600
1768
- state: str = ""
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
- channel_id=data.get('channel_id', ''),
1774
- auth_url=data['auth_url'],
1775
- expires_in=data.get('expires_in', 600),
1776
- state=data.get('state', ''),
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: Dict[str, Any]) -> 'PreviewSessionList':
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
- response = await self._client._make_request(
131
- "POST",
132
- f"preview/sessions/{project_id}/start",
133
- json=body,
134
- )
135
- return PreviewSession.from_dict(response)
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
- response = await self._client._make_request(
326
- "POST",
327
- f"preview/sessions/{project_id}/detect",
328
- )
329
- return PreviewDetectResult.from_dict(response)
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.get("voice/config")
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.get("voice/sessions")
189
- return result.get("sessions", [])
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.get("voice/stats")
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.get("voice/health")
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.0
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fleeks-sdk"
7
- version = "0.7.0"
7
+ version = "0.7.2"
8
8
  description = "Python SDK for the Fleeks AI Development Platform"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
File without changes
File without changes