modulex-python 0.1.0__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.
- modulex/__init__.py +39 -0
- modulex/_base.py +281 -0
- modulex/_client.py +237 -0
- modulex/_compat.py +39 -0
- modulex/_config.py +26 -0
- modulex/_exceptions.py +131 -0
- modulex/_streaming.py +118 -0
- modulex/py.typed +0 -0
- modulex/resources/__init__.py +1 -0
- modulex/resources/api_keys.py +39 -0
- modulex/resources/auth.py +38 -0
- modulex/resources/chats.py +62 -0
- modulex/resources/composer.py +134 -0
- modulex/resources/credentials.py +197 -0
- modulex/resources/dashboard.py +110 -0
- modulex/resources/deployments.py +92 -0
- modulex/resources/executions.py +97 -0
- modulex/resources/integrations.py +110 -0
- modulex/resources/knowledge.py +343 -0
- modulex/resources/notifications.py +39 -0
- modulex/resources/organizations.py +72 -0
- modulex/resources/schedules.py +172 -0
- modulex/resources/subscriptions.py +38 -0
- modulex/resources/system.py +28 -0
- modulex/resources/templates.py +115 -0
- modulex/resources/workflows.py +156 -0
- modulex/types/__init__.py +294 -0
- modulex/types/api_keys.py +19 -0
- modulex/types/auth.py +62 -0
- modulex/types/chats.py +55 -0
- modulex/types/composer.py +27 -0
- modulex/types/credentials.py +79 -0
- modulex/types/dashboard.py +54 -0
- modulex/types/executions.py +104 -0
- modulex/types/integrations.py +29 -0
- modulex/types/knowledge.py +75 -0
- modulex/types/notifications.py +16 -0
- modulex/types/organizations.py +43 -0
- modulex/types/schedules.py +48 -0
- modulex/types/shared.py +39 -0
- modulex/types/subscriptions.py +59 -0
- modulex/types/templates.py +50 -0
- modulex/types/workflows.py +253 -0
- modulex_python-0.1.0.dist-info/METADATA +435 -0
- modulex_python-0.1.0.dist-info/RECORD +47 -0
- modulex_python-0.1.0.dist-info/WHEEL +4 -0
- modulex_python-0.1.0.dist-info/licenses/LICENSE +21 -0
modulex/__init__.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""ModuleX Python SDK — Official client for the ModuleX AI workflow orchestration platform."""
|
|
2
|
+
|
|
3
|
+
from modulex._client import Modulex
|
|
4
|
+
from modulex._exceptions import (
|
|
5
|
+
AuthenticationError,
|
|
6
|
+
BadRequestError,
|
|
7
|
+
ConflictError,
|
|
8
|
+
ExternalServiceError,
|
|
9
|
+
InternalError,
|
|
10
|
+
ModulexError,
|
|
11
|
+
NotFoundError,
|
|
12
|
+
PermissionError,
|
|
13
|
+
RateLimitError,
|
|
14
|
+
ServiceUnavailableError,
|
|
15
|
+
StreamError,
|
|
16
|
+
TimeoutError,
|
|
17
|
+
ValidationError,
|
|
18
|
+
)
|
|
19
|
+
from modulex._streaming import SSEEvent
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"Modulex",
|
|
23
|
+
"ModulexError",
|
|
24
|
+
"AuthenticationError",
|
|
25
|
+
"PermissionError",
|
|
26
|
+
"NotFoundError",
|
|
27
|
+
"BadRequestError",
|
|
28
|
+
"ValidationError",
|
|
29
|
+
"ConflictError",
|
|
30
|
+
"RateLimitError",
|
|
31
|
+
"InternalError",
|
|
32
|
+
"ExternalServiceError",
|
|
33
|
+
"ServiceUnavailableError",
|
|
34
|
+
"StreamError",
|
|
35
|
+
"TimeoutError",
|
|
36
|
+
"SSEEvent",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
__version__ = "0.1.0"
|
modulex/_base.py
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""Base resource class with HTTP methods, retry logic, and error handling."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import random
|
|
6
|
+
from collections.abc import AsyncIterator
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from modulex._exceptions import (
|
|
12
|
+
RETRYABLE_STATUS_CODES,
|
|
13
|
+
TimeoutError,
|
|
14
|
+
raise_for_status,
|
|
15
|
+
)
|
|
16
|
+
from modulex._streaming import EventSourceStream
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from modulex._client import Modulex
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _BaseResource:
|
|
23
|
+
"""Base class for all API resource classes."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, client: Modulex) -> None:
|
|
26
|
+
self._client = client
|
|
27
|
+
|
|
28
|
+
def _resolve_org_id(self, organization_id: str | None) -> str | None:
|
|
29
|
+
"""Resolve organization ID: per-request > client default > None."""
|
|
30
|
+
if organization_id is not None:
|
|
31
|
+
return organization_id
|
|
32
|
+
return self._client._config.organization_id
|
|
33
|
+
|
|
34
|
+
def _build_headers(self, organization_id: str | None = None) -> dict[str, str]:
|
|
35
|
+
"""Build request headers with auth and optional org context."""
|
|
36
|
+
headers: dict[str, str] = {
|
|
37
|
+
"Authorization": f"Bearer {self._client._config.api_key}",
|
|
38
|
+
"Content-Type": "application/json",
|
|
39
|
+
}
|
|
40
|
+
org_id = self._resolve_org_id(organization_id)
|
|
41
|
+
if org_id:
|
|
42
|
+
headers["X-Organization-ID"] = org_id
|
|
43
|
+
return headers
|
|
44
|
+
|
|
45
|
+
def _should_retry(self, method: str, status_code: int, attempt: int) -> bool:
|
|
46
|
+
"""Determine if a request should be retried."""
|
|
47
|
+
if attempt >= self._client._config.max_retries:
|
|
48
|
+
return False
|
|
49
|
+
if status_code not in RETRYABLE_STATUS_CODES:
|
|
50
|
+
return False
|
|
51
|
+
if method.upper() not in ("GET", "HEAD"):
|
|
52
|
+
return False
|
|
53
|
+
return True
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def _backoff_delay(attempt: int, retry_after: float | None = None) -> float:
|
|
57
|
+
"""Calculate backoff delay with jitter."""
|
|
58
|
+
if retry_after is not None:
|
|
59
|
+
return retry_after
|
|
60
|
+
base = 0.5
|
|
61
|
+
delay: float = min(base * (2**attempt) + random.random() * 0.5, 30.0)
|
|
62
|
+
return delay
|
|
63
|
+
|
|
64
|
+
async def _request(
|
|
65
|
+
self,
|
|
66
|
+
method: str,
|
|
67
|
+
path: str,
|
|
68
|
+
*,
|
|
69
|
+
params: dict[str, Any] | None = None,
|
|
70
|
+
json: dict[str, Any] | None = None,
|
|
71
|
+
organization_id: str | None = None,
|
|
72
|
+
**kwargs: Any,
|
|
73
|
+
) -> Any:
|
|
74
|
+
"""Execute an HTTP request with retry logic."""
|
|
75
|
+
url = f"{self._client._config.base_url}{path}"
|
|
76
|
+
headers = self._build_headers(organization_id)
|
|
77
|
+
|
|
78
|
+
# Filter None values from params
|
|
79
|
+
if params:
|
|
80
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
81
|
+
|
|
82
|
+
last_exc: Exception | None = None
|
|
83
|
+
for attempt in range(self._client._config.max_retries + 1):
|
|
84
|
+
try:
|
|
85
|
+
response = await self._client._http.request(
|
|
86
|
+
method,
|
|
87
|
+
url,
|
|
88
|
+
headers=headers,
|
|
89
|
+
params=params,
|
|
90
|
+
json=json,
|
|
91
|
+
timeout=self._client._config.timeout,
|
|
92
|
+
**kwargs,
|
|
93
|
+
)
|
|
94
|
+
except httpx.TimeoutException as e:
|
|
95
|
+
if attempt < self._client._config.max_retries and method.upper() in ("GET", "HEAD"):
|
|
96
|
+
last_exc = e
|
|
97
|
+
import asyncio
|
|
98
|
+
|
|
99
|
+
await asyncio.sleep(self._backoff_delay(attempt))
|
|
100
|
+
continue
|
|
101
|
+
raise TimeoutError(f"Request timed out: {e}") from e
|
|
102
|
+
|
|
103
|
+
if response.status_code < 400:
|
|
104
|
+
if response.status_code == 204:
|
|
105
|
+
return None
|
|
106
|
+
content_type = response.headers.get("content-type", "")
|
|
107
|
+
if "application/json" in content_type:
|
|
108
|
+
return response.json()
|
|
109
|
+
try:
|
|
110
|
+
return response.json()
|
|
111
|
+
except Exception:
|
|
112
|
+
return response.text
|
|
113
|
+
|
|
114
|
+
if self._should_retry(method, response.status_code, attempt):
|
|
115
|
+
retry_after: float | None = None
|
|
116
|
+
if response.status_code == 429:
|
|
117
|
+
retry_after_header = response.headers.get("Retry-After")
|
|
118
|
+
if retry_after_header:
|
|
119
|
+
retry_after = float(retry_after_header)
|
|
120
|
+
|
|
121
|
+
import asyncio
|
|
122
|
+
|
|
123
|
+
await asyncio.sleep(self._backoff_delay(attempt, retry_after))
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
raise_for_status(response)
|
|
127
|
+
|
|
128
|
+
if last_exc:
|
|
129
|
+
raise TimeoutError(f"Request timed out after {self._client._config.max_retries} retries") from last_exc
|
|
130
|
+
raise_for_status(response)
|
|
131
|
+
|
|
132
|
+
async def _get(
|
|
133
|
+
self,
|
|
134
|
+
path: str,
|
|
135
|
+
*,
|
|
136
|
+
params: dict[str, Any] | None = None,
|
|
137
|
+
organization_id: str | None = None,
|
|
138
|
+
**kwargs: Any,
|
|
139
|
+
) -> Any:
|
|
140
|
+
"""Execute a GET request."""
|
|
141
|
+
return await self._request("GET", path, params=params, organization_id=organization_id, **kwargs)
|
|
142
|
+
|
|
143
|
+
async def _post(
|
|
144
|
+
self,
|
|
145
|
+
path: str,
|
|
146
|
+
*,
|
|
147
|
+
json: dict[str, Any] | None = None,
|
|
148
|
+
organization_id: str | None = None,
|
|
149
|
+
**kwargs: Any,
|
|
150
|
+
) -> Any:
|
|
151
|
+
"""Execute a POST request."""
|
|
152
|
+
return await self._request("POST", path, json=json, organization_id=organization_id, **kwargs)
|
|
153
|
+
|
|
154
|
+
async def _put(
|
|
155
|
+
self,
|
|
156
|
+
path: str,
|
|
157
|
+
*,
|
|
158
|
+
json: dict[str, Any] | None = None,
|
|
159
|
+
organization_id: str | None = None,
|
|
160
|
+
**kwargs: Any,
|
|
161
|
+
) -> Any:
|
|
162
|
+
"""Execute a PUT request."""
|
|
163
|
+
return await self._request("PUT", path, json=json, organization_id=organization_id, **kwargs)
|
|
164
|
+
|
|
165
|
+
async def _patch(
|
|
166
|
+
self,
|
|
167
|
+
path: str,
|
|
168
|
+
*,
|
|
169
|
+
json: dict[str, Any] | None = None,
|
|
170
|
+
organization_id: str | None = None,
|
|
171
|
+
**kwargs: Any,
|
|
172
|
+
) -> Any:
|
|
173
|
+
"""Execute a PATCH request."""
|
|
174
|
+
return await self._request("PATCH", path, json=json, organization_id=organization_id, **kwargs)
|
|
175
|
+
|
|
176
|
+
async def _delete(
|
|
177
|
+
self,
|
|
178
|
+
path: str,
|
|
179
|
+
*,
|
|
180
|
+
params: dict[str, Any] | None = None,
|
|
181
|
+
json: dict[str, Any] | None = None,
|
|
182
|
+
organization_id: str | None = None,
|
|
183
|
+
**kwargs: Any,
|
|
184
|
+
) -> Any:
|
|
185
|
+
"""Execute a DELETE request."""
|
|
186
|
+
return await self._request("DELETE", path, params=params, json=json, organization_id=organization_id, **kwargs)
|
|
187
|
+
|
|
188
|
+
def _stream_sse(
|
|
189
|
+
self,
|
|
190
|
+
path: str,
|
|
191
|
+
*,
|
|
192
|
+
method: str = "GET",
|
|
193
|
+
organization_id: str | None = None,
|
|
194
|
+
**kwargs: Any,
|
|
195
|
+
) -> EventSourceStream:
|
|
196
|
+
"""Create an SSE stream connection."""
|
|
197
|
+
url = f"{self._client._config.base_url}{path}"
|
|
198
|
+
headers = self._build_headers(organization_id)
|
|
199
|
+
headers.pop("Content-Type", None)
|
|
200
|
+
return EventSourceStream(
|
|
201
|
+
self._client._http,
|
|
202
|
+
method,
|
|
203
|
+
url,
|
|
204
|
+
headers=headers,
|
|
205
|
+
timeout=httpx.Timeout(self._client._config.timeout, read=None),
|
|
206
|
+
**kwargs,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
async def _upload(
|
|
210
|
+
self,
|
|
211
|
+
path: str,
|
|
212
|
+
*,
|
|
213
|
+
file: Any,
|
|
214
|
+
filename: str,
|
|
215
|
+
data: dict[str, str] | None = None,
|
|
216
|
+
organization_id: str | None = None,
|
|
217
|
+
**kwargs: Any,
|
|
218
|
+
) -> Any:
|
|
219
|
+
"""Execute a multipart file upload."""
|
|
220
|
+
url = f"{self._client._config.base_url}{path}"
|
|
221
|
+
headers = self._build_headers(organization_id)
|
|
222
|
+
headers.pop("Content-Type", None)
|
|
223
|
+
|
|
224
|
+
files = {"file": (filename, file)}
|
|
225
|
+
|
|
226
|
+
response = await self._client._http.post(
|
|
227
|
+
url,
|
|
228
|
+
headers=headers,
|
|
229
|
+
files=files,
|
|
230
|
+
data=data,
|
|
231
|
+
timeout=self._client._config.timeout,
|
|
232
|
+
**kwargs,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
if response.status_code >= 400:
|
|
236
|
+
raise_for_status(response)
|
|
237
|
+
|
|
238
|
+
return response.json()
|
|
239
|
+
|
|
240
|
+
async def _paginate(
|
|
241
|
+
self,
|
|
242
|
+
path: str,
|
|
243
|
+
*,
|
|
244
|
+
items_key: str = "items",
|
|
245
|
+
params: dict[str, Any] | None = None,
|
|
246
|
+
organization_id: str | None = None,
|
|
247
|
+
page_size: int = 20,
|
|
248
|
+
**kwargs: Any,
|
|
249
|
+
) -> AsyncIterator[dict[str, Any]]:
|
|
250
|
+
"""Auto-paginate through a list endpoint."""
|
|
251
|
+
params = dict(params or {})
|
|
252
|
+
|
|
253
|
+
# Detect pagination style
|
|
254
|
+
if "page" in params or "page_size" in params:
|
|
255
|
+
# Page-based pagination
|
|
256
|
+
page = params.pop("page", 1)
|
|
257
|
+
params["page_size"] = params.pop("page_size", page_size)
|
|
258
|
+
while True:
|
|
259
|
+
params["page"] = page
|
|
260
|
+
result = await self._get(path, params=params, organization_id=organization_id, **kwargs)
|
|
261
|
+
items = result.get(items_key, [])
|
|
262
|
+
for item in items:
|
|
263
|
+
yield item
|
|
264
|
+
total_pages = result.get("total_pages", 1)
|
|
265
|
+
if page >= total_pages:
|
|
266
|
+
break
|
|
267
|
+
page += 1
|
|
268
|
+
else:
|
|
269
|
+
# Limit/offset pagination
|
|
270
|
+
offset = params.pop("offset", 0)
|
|
271
|
+
limit = params.pop("limit", page_size)
|
|
272
|
+
params["limit"] = limit
|
|
273
|
+
while True:
|
|
274
|
+
params["offset"] = offset
|
|
275
|
+
result = await self._get(path, params=params, organization_id=organization_id, **kwargs)
|
|
276
|
+
items = result.get(items_key, [])
|
|
277
|
+
for item in items:
|
|
278
|
+
yield item
|
|
279
|
+
if not result.get("has_next", False) or len(items) < limit:
|
|
280
|
+
break
|
|
281
|
+
offset += limit
|
modulex/_client.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""Main ModuleX client class."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from modulex._config import DEFAULT_BASE_URL, DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT, ClientConfig
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from modulex.resources.api_keys import ApiKeys
|
|
13
|
+
from modulex.resources.auth import Auth
|
|
14
|
+
from modulex.resources.chats import Chats
|
|
15
|
+
from modulex.resources.composer import Composer
|
|
16
|
+
from modulex.resources.credentials import Credentials
|
|
17
|
+
from modulex.resources.dashboard import Dashboard
|
|
18
|
+
from modulex.resources.deployments import Deployments
|
|
19
|
+
from modulex.resources.executions import Executions
|
|
20
|
+
from modulex.resources.integrations import Integrations
|
|
21
|
+
from modulex.resources.knowledge import Knowledge
|
|
22
|
+
from modulex.resources.notifications import Notifications
|
|
23
|
+
from modulex.resources.organizations import Organizations
|
|
24
|
+
from modulex.resources.schedules import Schedules
|
|
25
|
+
from modulex.resources.subscriptions import Subscriptions
|
|
26
|
+
from modulex.resources.system import System
|
|
27
|
+
from modulex.resources.templates import Templates
|
|
28
|
+
from modulex.resources.workflows import Workflows
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Modulex:
|
|
32
|
+
"""Async client for the ModuleX API.
|
|
33
|
+
|
|
34
|
+
Usage:
|
|
35
|
+
async with Modulex(api_key="mx_live_...") as client:
|
|
36
|
+
me = await client.auth.me()
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
api_key: str,
|
|
42
|
+
*,
|
|
43
|
+
organization_id: str | None = None,
|
|
44
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
45
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
46
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
47
|
+
) -> None:
|
|
48
|
+
self._config = ClientConfig(
|
|
49
|
+
api_key=api_key,
|
|
50
|
+
organization_id=organization_id,
|
|
51
|
+
base_url=base_url,
|
|
52
|
+
timeout=timeout,
|
|
53
|
+
max_retries=max_retries,
|
|
54
|
+
)
|
|
55
|
+
self._http = httpx.AsyncClient()
|
|
56
|
+
|
|
57
|
+
# Lazy resource cache
|
|
58
|
+
self._auth: object | None = None
|
|
59
|
+
self._workflows: object | None = None
|
|
60
|
+
self._executions: object | None = None
|
|
61
|
+
self._deployments: object | None = None
|
|
62
|
+
self._chats: object | None = None
|
|
63
|
+
self._credentials: object | None = None
|
|
64
|
+
self._integrations: object | None = None
|
|
65
|
+
self._knowledge: object | None = None
|
|
66
|
+
self._schedules: object | None = None
|
|
67
|
+
self._templates: object | None = None
|
|
68
|
+
self._composer: object | None = None
|
|
69
|
+
self._dashboard: object | None = None
|
|
70
|
+
self._subscriptions: object | None = None
|
|
71
|
+
self._notifications: object | None = None
|
|
72
|
+
self._api_keys: object | None = None
|
|
73
|
+
self._system: object | None = None
|
|
74
|
+
self._organizations: object | None = None
|
|
75
|
+
|
|
76
|
+
async def __aenter__(self) -> Modulex:
|
|
77
|
+
return self
|
|
78
|
+
|
|
79
|
+
async def __aexit__(self, *args: object) -> None:
|
|
80
|
+
await self.close()
|
|
81
|
+
|
|
82
|
+
async def close(self) -> None:
|
|
83
|
+
"""Close the HTTP client."""
|
|
84
|
+
await self._http.aclose()
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def auth(self) -> Auth:
|
|
88
|
+
"""Access auth endpoints."""
|
|
89
|
+
if self._auth is None:
|
|
90
|
+
from modulex.resources.auth import Auth
|
|
91
|
+
|
|
92
|
+
self._auth = Auth(self)
|
|
93
|
+
return self._auth # type: ignore[return-value]
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def workflows(self) -> Workflows:
|
|
97
|
+
"""Access workflow endpoints."""
|
|
98
|
+
if self._workflows is None:
|
|
99
|
+
from modulex.resources.workflows import Workflows
|
|
100
|
+
|
|
101
|
+
self._workflows = Workflows(self)
|
|
102
|
+
return self._workflows # type: ignore[return-value]
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def executions(self) -> Executions:
|
|
106
|
+
"""Access execution endpoints."""
|
|
107
|
+
if self._executions is None:
|
|
108
|
+
from modulex.resources.executions import Executions
|
|
109
|
+
|
|
110
|
+
self._executions = Executions(self)
|
|
111
|
+
return self._executions # type: ignore[return-value]
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def deployments(self) -> Deployments:
|
|
115
|
+
"""Access deployment endpoints."""
|
|
116
|
+
if self._deployments is None:
|
|
117
|
+
from modulex.resources.deployments import Deployments
|
|
118
|
+
|
|
119
|
+
self._deployments = Deployments(self)
|
|
120
|
+
return self._deployments # type: ignore[return-value]
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def chats(self) -> Chats:
|
|
124
|
+
"""Access chat endpoints."""
|
|
125
|
+
if self._chats is None:
|
|
126
|
+
from modulex.resources.chats import Chats
|
|
127
|
+
|
|
128
|
+
self._chats = Chats(self)
|
|
129
|
+
return self._chats # type: ignore[return-value]
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def credentials(self) -> Credentials:
|
|
133
|
+
"""Access credential endpoints."""
|
|
134
|
+
if self._credentials is None:
|
|
135
|
+
from modulex.resources.credentials import Credentials
|
|
136
|
+
|
|
137
|
+
self._credentials = Credentials(self)
|
|
138
|
+
return self._credentials # type: ignore[return-value]
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def integrations(self) -> Integrations:
|
|
142
|
+
"""Access integration endpoints."""
|
|
143
|
+
if self._integrations is None:
|
|
144
|
+
from modulex.resources.integrations import Integrations
|
|
145
|
+
|
|
146
|
+
self._integrations = Integrations(self)
|
|
147
|
+
return self._integrations # type: ignore[return-value]
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def knowledge(self) -> Knowledge:
|
|
151
|
+
"""Access knowledge base endpoints."""
|
|
152
|
+
if self._knowledge is None:
|
|
153
|
+
from modulex.resources.knowledge import Knowledge
|
|
154
|
+
|
|
155
|
+
self._knowledge = Knowledge(self)
|
|
156
|
+
return self._knowledge # type: ignore[return-value]
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def schedules(self) -> Schedules:
|
|
160
|
+
"""Access schedule endpoints."""
|
|
161
|
+
if self._schedules is None:
|
|
162
|
+
from modulex.resources.schedules import Schedules
|
|
163
|
+
|
|
164
|
+
self._schedules = Schedules(self)
|
|
165
|
+
return self._schedules # type: ignore[return-value]
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def templates(self) -> Templates:
|
|
169
|
+
"""Access template endpoints."""
|
|
170
|
+
if self._templates is None:
|
|
171
|
+
from modulex.resources.templates import Templates
|
|
172
|
+
|
|
173
|
+
self._templates = Templates(self)
|
|
174
|
+
return self._templates # type: ignore[return-value]
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def composer(self) -> Composer:
|
|
178
|
+
"""Access composer endpoints."""
|
|
179
|
+
if self._composer is None:
|
|
180
|
+
from modulex.resources.composer import Composer
|
|
181
|
+
|
|
182
|
+
self._composer = Composer(self)
|
|
183
|
+
return self._composer # type: ignore[return-value]
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def dashboard(self) -> Dashboard:
|
|
187
|
+
"""Access dashboard endpoints."""
|
|
188
|
+
if self._dashboard is None:
|
|
189
|
+
from modulex.resources.dashboard import Dashboard
|
|
190
|
+
|
|
191
|
+
self._dashboard = Dashboard(self)
|
|
192
|
+
return self._dashboard # type: ignore[return-value]
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def subscriptions(self) -> Subscriptions:
|
|
196
|
+
"""Access subscription endpoints."""
|
|
197
|
+
if self._subscriptions is None:
|
|
198
|
+
from modulex.resources.subscriptions import Subscriptions
|
|
199
|
+
|
|
200
|
+
self._subscriptions = Subscriptions(self)
|
|
201
|
+
return self._subscriptions # type: ignore[return-value]
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
def notifications(self) -> Notifications:
|
|
205
|
+
"""Access notification endpoints."""
|
|
206
|
+
if self._notifications is None:
|
|
207
|
+
from modulex.resources.notifications import Notifications
|
|
208
|
+
|
|
209
|
+
self._notifications = Notifications(self)
|
|
210
|
+
return self._notifications # type: ignore[return-value]
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def api_keys(self) -> ApiKeys:
|
|
214
|
+
"""Access API key endpoints."""
|
|
215
|
+
if self._api_keys is None:
|
|
216
|
+
from modulex.resources.api_keys import ApiKeys
|
|
217
|
+
|
|
218
|
+
self._api_keys = ApiKeys(self)
|
|
219
|
+
return self._api_keys # type: ignore[return-value]
|
|
220
|
+
|
|
221
|
+
@property
|
|
222
|
+
def system(self) -> System:
|
|
223
|
+
"""Access system endpoints."""
|
|
224
|
+
if self._system is None:
|
|
225
|
+
from modulex.resources.system import System
|
|
226
|
+
|
|
227
|
+
self._system = System(self)
|
|
228
|
+
return self._system # type: ignore[return-value]
|
|
229
|
+
|
|
230
|
+
@property
|
|
231
|
+
def organizations(self) -> Organizations:
|
|
232
|
+
"""Access organization endpoints."""
|
|
233
|
+
if self._organizations is None:
|
|
234
|
+
from modulex.resources.organizations import Organizations
|
|
235
|
+
|
|
236
|
+
self._organizations = Organizations(self)
|
|
237
|
+
return self._organizations # type: ignore[return-value]
|
modulex/_compat.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Synchronous compatibility wrapper for the ModuleX async client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from typing import Any, TypeVar
|
|
7
|
+
|
|
8
|
+
T = TypeVar("T")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _get_or_create_event_loop() -> asyncio.AbstractEventLoop:
|
|
12
|
+
"""Get the current event loop or create a new one."""
|
|
13
|
+
try:
|
|
14
|
+
loop = asyncio.get_event_loop()
|
|
15
|
+
if loop.is_closed():
|
|
16
|
+
loop = asyncio.new_event_loop()
|
|
17
|
+
asyncio.set_event_loop(loop)
|
|
18
|
+
return loop
|
|
19
|
+
except RuntimeError:
|
|
20
|
+
loop = asyncio.new_event_loop()
|
|
21
|
+
asyncio.set_event_loop(loop)
|
|
22
|
+
return loop
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def run_sync(coro: Any) -> Any:
|
|
26
|
+
"""Run an async coroutine synchronously."""
|
|
27
|
+
try:
|
|
28
|
+
loop = asyncio.get_running_loop()
|
|
29
|
+
except RuntimeError:
|
|
30
|
+
loop = None
|
|
31
|
+
|
|
32
|
+
if loop and loop.is_running():
|
|
33
|
+
import concurrent.futures
|
|
34
|
+
|
|
35
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
|
|
36
|
+
future = pool.submit(asyncio.run, coro)
|
|
37
|
+
return future.result()
|
|
38
|
+
else:
|
|
39
|
+
return asyncio.run(coro)
|
modulex/_config.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Client configuration for the ModuleX SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
|
|
7
|
+
DEFAULT_BASE_URL = "https://api.modulex.dev"
|
|
8
|
+
DEFAULT_TIMEOUT = 30.0
|
|
9
|
+
DEFAULT_MAX_RETRIES = 3
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class ClientConfig:
|
|
14
|
+
"""Configuration for the ModuleX client."""
|
|
15
|
+
|
|
16
|
+
api_key: str
|
|
17
|
+
organization_id: str | None = None
|
|
18
|
+
base_url: str = DEFAULT_BASE_URL
|
|
19
|
+
timeout: float = DEFAULT_TIMEOUT
|
|
20
|
+
max_retries: int = DEFAULT_MAX_RETRIES
|
|
21
|
+
default_headers: dict[str, str] = field(default_factory=dict)
|
|
22
|
+
|
|
23
|
+
def __post_init__(self) -> None:
|
|
24
|
+
self.base_url = self.base_url.rstrip("/")
|
|
25
|
+
if not self.api_key:
|
|
26
|
+
raise ValueError("api_key is required")
|