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.
Files changed (47) hide show
  1. modulex/__init__.py +39 -0
  2. modulex/_base.py +281 -0
  3. modulex/_client.py +237 -0
  4. modulex/_compat.py +39 -0
  5. modulex/_config.py +26 -0
  6. modulex/_exceptions.py +131 -0
  7. modulex/_streaming.py +118 -0
  8. modulex/py.typed +0 -0
  9. modulex/resources/__init__.py +1 -0
  10. modulex/resources/api_keys.py +39 -0
  11. modulex/resources/auth.py +38 -0
  12. modulex/resources/chats.py +62 -0
  13. modulex/resources/composer.py +134 -0
  14. modulex/resources/credentials.py +197 -0
  15. modulex/resources/dashboard.py +110 -0
  16. modulex/resources/deployments.py +92 -0
  17. modulex/resources/executions.py +97 -0
  18. modulex/resources/integrations.py +110 -0
  19. modulex/resources/knowledge.py +343 -0
  20. modulex/resources/notifications.py +39 -0
  21. modulex/resources/organizations.py +72 -0
  22. modulex/resources/schedules.py +172 -0
  23. modulex/resources/subscriptions.py +38 -0
  24. modulex/resources/system.py +28 -0
  25. modulex/resources/templates.py +115 -0
  26. modulex/resources/workflows.py +156 -0
  27. modulex/types/__init__.py +294 -0
  28. modulex/types/api_keys.py +19 -0
  29. modulex/types/auth.py +62 -0
  30. modulex/types/chats.py +55 -0
  31. modulex/types/composer.py +27 -0
  32. modulex/types/credentials.py +79 -0
  33. modulex/types/dashboard.py +54 -0
  34. modulex/types/executions.py +104 -0
  35. modulex/types/integrations.py +29 -0
  36. modulex/types/knowledge.py +75 -0
  37. modulex/types/notifications.py +16 -0
  38. modulex/types/organizations.py +43 -0
  39. modulex/types/schedules.py +48 -0
  40. modulex/types/shared.py +39 -0
  41. modulex/types/subscriptions.py +59 -0
  42. modulex/types/templates.py +50 -0
  43. modulex/types/workflows.py +253 -0
  44. modulex_python-0.1.0.dist-info/METADATA +435 -0
  45. modulex_python-0.1.0.dist-info/RECORD +47 -0
  46. modulex_python-0.1.0.dist-info/WHEEL +4 -0
  47. modulex_python-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,343 @@
1
+ """Knowledge resource for the ModuleX Python SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import builtins
6
+ import json as json_module
7
+ import os
8
+ from typing import Any
9
+
10
+ from modulex._base import _BaseResource
11
+
12
+
13
+ class Knowledge(_BaseResource):
14
+ """Resource for managing knowledge bases, documents, and semantic search."""
15
+
16
+ async def list(
17
+ self,
18
+ *,
19
+ status: str | None = None,
20
+ limit: int = 100,
21
+ offset: int = 0,
22
+ organization_id: str | None = None,
23
+ ) -> Any:
24
+ """Return all knowledge bases accessible to the caller."""
25
+ params: dict[str, Any] = {
26
+ k: v for k, v in {"status": status, "limit": limit, "offset": offset}.items() if v is not None
27
+ }
28
+ return await self._get("/knowledge-bases", params=params, organization_id=organization_id)
29
+
30
+ async def create(
31
+ self,
32
+ name: str,
33
+ *,
34
+ description: str | None = None,
35
+ embedding_config: dict[str, Any] | None = None,
36
+ chunking_config: dict[str, Any] | None = None,
37
+ organization_id: str | None = None,
38
+ ) -> Any:
39
+ """Create a new knowledge base with the given name and optional configuration."""
40
+ body: dict[str, Any] = {
41
+ k: v
42
+ for k, v in {
43
+ "name": name,
44
+ "description": description,
45
+ "embedding_config": embedding_config,
46
+ "chunking_config": chunking_config,
47
+ }.items()
48
+ if v is not None
49
+ }
50
+ return await self._post("/knowledge-bases", json=body, organization_id=organization_id)
51
+
52
+ async def get(
53
+ self,
54
+ knowledge_base_id: str,
55
+ *,
56
+ organization_id: str | None = None,
57
+ ) -> Any:
58
+ """Return a single knowledge base by its ID."""
59
+ return await self._get(f"/knowledge-bases/{knowledge_base_id}", organization_id=organization_id)
60
+
61
+ async def update(
62
+ self,
63
+ knowledge_base_id: str,
64
+ *,
65
+ organization_id: str | None = None,
66
+ **kwargs: Any,
67
+ ) -> Any:
68
+ """Update an existing knowledge base with the provided field values."""
69
+ body: dict[str, Any] = {k: v for k, v in kwargs.items() if v is not None}
70
+ return await self._put(
71
+ f"/knowledge-bases/{knowledge_base_id}",
72
+ json=body,
73
+ organization_id=organization_id,
74
+ )
75
+
76
+ async def delete(
77
+ self,
78
+ knowledge_base_id: str,
79
+ *,
80
+ delete_files: bool = True,
81
+ organization_id: str | None = None,
82
+ ) -> None:
83
+ """Delete a knowledge base and optionally its associated files."""
84
+ params: dict[str, Any] = {"delete_files": delete_files}
85
+ await self._delete(
86
+ f"/knowledge-bases/{knowledge_base_id}",
87
+ params=params,
88
+ organization_id=organization_id,
89
+ )
90
+
91
+ async def archive(
92
+ self,
93
+ knowledge_base_id: str,
94
+ *,
95
+ organization_id: str | None = None,
96
+ ) -> Any:
97
+ """Archive a knowledge base, making it read-only."""
98
+ return await self._post(
99
+ f"/knowledge-bases/{knowledge_base_id}/archive",
100
+ organization_id=organization_id,
101
+ )
102
+
103
+ async def stats(self, *, organization_id: str | None = None) -> Any:
104
+ """Return aggregate statistics for all knowledge bases in the organization."""
105
+ return await self._get("/knowledge-bases/stats", organization_id=organization_id)
106
+
107
+ async def list_documents(
108
+ self,
109
+ knowledge_base_id: str,
110
+ *,
111
+ status: str | None = None,
112
+ limit: int = 100,
113
+ offset: int = 0,
114
+ organization_id: str | None = None,
115
+ ) -> Any:
116
+ """Return all documents within a knowledge base."""
117
+ params: dict[str, Any] = {
118
+ k: v for k, v in {"status": status, "limit": limit, "offset": offset}.items() if v is not None
119
+ }
120
+ return await self._get(
121
+ f"/knowledge-bases/{knowledge_base_id}/documents",
122
+ params=params,
123
+ organization_id=organization_id,
124
+ )
125
+
126
+ async def upload_document(
127
+ self,
128
+ knowledge_base_id: str,
129
+ *,
130
+ file_path: str | None = None,
131
+ file: Any | None = None,
132
+ filename: str | None = None,
133
+ metadata: dict[str, Any] | None = None,
134
+ organization_id: str | None = None,
135
+ ) -> Any:
136
+ """Upload a document to a knowledge base via multipart form data."""
137
+ if file_path is not None:
138
+ f = open(file_path, "rb")
139
+ fn = filename or os.path.basename(file_path)
140
+ elif file is not None:
141
+ f = file
142
+ fn = filename or "document"
143
+ else:
144
+ raise ValueError("Either file_path or file must be provided")
145
+
146
+ data: dict[str, str] = {}
147
+ if metadata is not None:
148
+ data["metadata"] = json_module.dumps(metadata)
149
+
150
+ try:
151
+ return await self._upload(
152
+ f"/knowledge-bases/{knowledge_base_id}/documents",
153
+ file=f,
154
+ filename=fn,
155
+ data=data if data else None,
156
+ organization_id=organization_id,
157
+ )
158
+ finally:
159
+ if file_path is not None:
160
+ f.close()
161
+
162
+ async def get_document(
163
+ self,
164
+ knowledge_base_id: str,
165
+ document_id: str,
166
+ *,
167
+ organization_id: str | None = None,
168
+ ) -> Any:
169
+ """Return a single document within a knowledge base by its ID."""
170
+ return await self._get(
171
+ f"/knowledge-bases/{knowledge_base_id}/documents/{document_id}",
172
+ organization_id=organization_id,
173
+ )
174
+
175
+ async def document_status(
176
+ self,
177
+ knowledge_base_id: str,
178
+ document_id: str,
179
+ *,
180
+ organization_id: str | None = None,
181
+ ) -> Any:
182
+ """Return the processing status of a document."""
183
+ return await self._get(
184
+ f"/knowledge-bases/{knowledge_base_id}/documents/{document_id}/status",
185
+ organization_id=organization_id,
186
+ )
187
+
188
+ async def delete_document(
189
+ self,
190
+ knowledge_base_id: str,
191
+ document_id: str,
192
+ *,
193
+ delete_file: bool = True,
194
+ organization_id: str | None = None,
195
+ ) -> None:
196
+ """Delete a document from a knowledge base and optionally its underlying file."""
197
+ params: dict[str, Any] = {"delete_file": delete_file}
198
+ await self._delete(
199
+ f"/knowledge-bases/{knowledge_base_id}/documents/{document_id}",
200
+ params=params,
201
+ organization_id=organization_id,
202
+ )
203
+
204
+ async def retry_document(
205
+ self,
206
+ knowledge_base_id: str,
207
+ document_id: str,
208
+ *,
209
+ organization_id: str | None = None,
210
+ ) -> Any:
211
+ """Retry processing for a failed document."""
212
+ return await self._post(
213
+ f"/knowledge-bases/{knowledge_base_id}/documents/{document_id}/retry",
214
+ organization_id=organization_id,
215
+ )
216
+
217
+ async def document_chunks(
218
+ self,
219
+ knowledge_base_id: str,
220
+ document_id: str,
221
+ *,
222
+ limit: int = 100,
223
+ offset: int = 0,
224
+ organization_id: str | None = None,
225
+ ) -> Any:
226
+ """Return the text chunks produced from a document after processing."""
227
+ params: dict[str, Any] = {"limit": limit, "offset": offset}
228
+ return await self._get(
229
+ f"/knowledge-bases/{knowledge_base_id}/documents/{document_id}/chunks",
230
+ params=params,
231
+ organization_id=organization_id,
232
+ )
233
+
234
+ async def search(
235
+ self,
236
+ knowledge_base_id: str,
237
+ query: str,
238
+ *,
239
+ top_k: int = 5,
240
+ min_score: float = 0.0,
241
+ filters: dict[str, Any] | None = None,
242
+ include_content: bool = True,
243
+ include_metadata: bool = True,
244
+ organization_id: str | None = None,
245
+ ) -> Any:
246
+ """Perform a semantic vector search against a knowledge base."""
247
+ body: dict[str, Any] = {
248
+ k: v
249
+ for k, v in {
250
+ "query": query,
251
+ "top_k": top_k,
252
+ "min_score": min_score,
253
+ "filters": filters,
254
+ "include_content": include_content,
255
+ "include_metadata": include_metadata,
256
+ }.items()
257
+ if v is not None
258
+ }
259
+ return await self._post(
260
+ f"/knowledge-bases/{knowledge_base_id}/search",
261
+ json=body,
262
+ organization_id=organization_id,
263
+ )
264
+
265
+ async def multi_search(
266
+ self,
267
+ knowledge_base_ids: builtins.list[str],
268
+ query: str,
269
+ *,
270
+ top_k: int = 5,
271
+ min_score: float = 0.0,
272
+ organization_id: str | None = None,
273
+ ) -> Any:
274
+ """Search across multiple knowledge bases in a single request."""
275
+ body: dict[str, Any] = {
276
+ k: v
277
+ for k, v in {
278
+ "knowledge_base_ids": knowledge_base_ids,
279
+ "query": query,
280
+ "top_k": top_k,
281
+ "min_score": min_score,
282
+ }.items()
283
+ if v is not None
284
+ }
285
+ return await self._post("/knowledge-bases/search", json=body, organization_id=organization_id)
286
+
287
+ async def hybrid_search(
288
+ self,
289
+ knowledge_base_id: str,
290
+ query: str,
291
+ *,
292
+ top_k: int = 5,
293
+ keyword_weight: float = 0.3,
294
+ semantic_weight: float = 0.7,
295
+ min_score: float = 0.0,
296
+ filters: dict[str, Any] | None = None,
297
+ organization_id: str | None = None,
298
+ ) -> Any:
299
+ """Perform a hybrid keyword-plus-semantic search against a knowledge base."""
300
+ body: dict[str, Any] = {
301
+ k: v
302
+ for k, v in {
303
+ "query": query,
304
+ "top_k": top_k,
305
+ "keyword_weight": keyword_weight,
306
+ "semantic_weight": semantic_weight,
307
+ "min_score": min_score,
308
+ "filters": filters,
309
+ }.items()
310
+ if v is not None
311
+ }
312
+ return await self._post(
313
+ f"/knowledge-bases/{knowledge_base_id}/hybrid-search",
314
+ json=body,
315
+ organization_id=organization_id,
316
+ )
317
+
318
+ async def retrieve_context(
319
+ self,
320
+ knowledge_base_id: str,
321
+ query: str,
322
+ *,
323
+ max_tokens: int = 2000,
324
+ top_k: int = 10,
325
+ min_score: float = 0.3,
326
+ organization_id: str | None = None,
327
+ ) -> Any:
328
+ """Retrieve a token-bounded context string suitable for LLM prompts."""
329
+ body: dict[str, Any] = {
330
+ "query": query,
331
+ "max_tokens": max_tokens,
332
+ "top_k": top_k,
333
+ "min_score": min_score,
334
+ }
335
+ return await self._post(
336
+ f"/knowledge-bases/{knowledge_base_id}/retrieve-context",
337
+ json=body,
338
+ organization_id=organization_id,
339
+ )
340
+
341
+ async def supported_file_types(self) -> Any:
342
+ """Return the list of file types supported for document upload."""
343
+ return await self._get("/knowledge-bases/info/supported-file-types")
@@ -0,0 +1,39 @@
1
+ """Notifications resource for the ModuleX Python SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from modulex._base import _BaseResource
8
+
9
+
10
+ class Notifications(_BaseResource):
11
+ """Resource for listing and creating organization notifications."""
12
+
13
+ async def list(self, *, organization_id: str | None = None) -> Any:
14
+ """Return all notifications for the organization."""
15
+ return await self._get("/notifications", organization_id=organization_id)
16
+
17
+ async def create(
18
+ self,
19
+ notification_topic: str,
20
+ message: str,
21
+ *,
22
+ notified_to: str | None = None,
23
+ notification_url: str | None = None,
24
+ expires_at: str | None = None,
25
+ organization_id: str | None = None,
26
+ ) -> Any:
27
+ """Create and dispatch a new notification to the organization."""
28
+ body: dict[str, Any] = {
29
+ k: v
30
+ for k, v in {
31
+ "notification_topic": notification_topic,
32
+ "message": message,
33
+ "notified_to": notified_to,
34
+ "notification_url": notification_url,
35
+ "expires_at": expires_at,
36
+ }.items()
37
+ if v is not None
38
+ }
39
+ return await self._post("/notifications/organization", json=body, organization_id=organization_id)
@@ -0,0 +1,72 @@
1
+ """Organizations resource for the ModuleX Python SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from modulex._base import _BaseResource
8
+
9
+
10
+ class Organizations(_BaseResource):
11
+ """Resource for managing organizations and their memberships."""
12
+
13
+ async def create(self, name: str, *, slug: str | None = None) -> Any:
14
+ """Create a new organization with the given name and optional slug."""
15
+ body: dict[str, Any] = {"name": name}
16
+ if slug is not None:
17
+ body["slug"] = slug
18
+ return await self._post("/organizations", json=body)
19
+
20
+ async def llms(self, *, organization_id: str | None = None) -> Any:
21
+ """Return the LLM configurations available to the organization."""
22
+ return await self._get("/organizations/llms", organization_id=organization_id)
23
+
24
+ async def invite(
25
+ self,
26
+ invited_email: str,
27
+ *,
28
+ role: str = "member",
29
+ invitation_message: str | None = None,
30
+ organization_id: str | None = None,
31
+ ) -> Any:
32
+ """Send an invitation email to a user to join the organization."""
33
+ body: dict[str, Any] = {"invited_email": invited_email, "role": role}
34
+ if invitation_message is not None:
35
+ body["invitation_message"] = invitation_message
36
+ return await self._post("/organizations/invite", json=body, organization_id=organization_id)
37
+
38
+ async def cancel_invitation(self, invitation_id: str, *, organization_id: str | None = None) -> Any:
39
+ """Cancel a pending organization invitation by its ID."""
40
+ return await self._post(
41
+ f"/organizations/invitations/{invitation_id}/cancel",
42
+ organization_id=organization_id,
43
+ )
44
+
45
+ async def reinvite(self, invitation_id: str, *, organization_id: str | None = None) -> Any:
46
+ """Resend an organization invitation by its ID."""
47
+ return await self._post(
48
+ f"/organizations/invitations/{invitation_id}/reinvite",
49
+ organization_id=organization_id,
50
+ )
51
+
52
+ async def update_user_role(
53
+ self,
54
+ org_id: str,
55
+ user_id: str,
56
+ role: str,
57
+ *,
58
+ organization_id: str | None = None,
59
+ ) -> Any:
60
+ """Update the role of a user within an organization."""
61
+ return await self._put(
62
+ f"/organizations/{org_id}/users/{user_id}/role",
63
+ json={"role": role},
64
+ organization_id=organization_id,
65
+ )
66
+
67
+ async def remove_user(self, org_id: str, user_id: str, *, organization_id: str | None = None) -> Any:
68
+ """Remove a user from an organization."""
69
+ return await self._delete(
70
+ f"/organizations/{org_id}/users/{user_id}",
71
+ organization_id=organization_id,
72
+ )
@@ -0,0 +1,172 @@
1
+ """Schedules resource for the ModuleX Python SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from modulex._base import _BaseResource
8
+
9
+
10
+ class Schedules(_BaseResource):
11
+ """Resource for managing workflow schedules and their execution runs."""
12
+
13
+ async def create(
14
+ self,
15
+ workflow_id: str,
16
+ name: str,
17
+ schedule_type: str,
18
+ *,
19
+ interval_seconds: int | None = None,
20
+ cron_expression: str | None = None,
21
+ timezone: str = "UTC",
22
+ description: str | None = None,
23
+ input: dict[str, Any] | None = None,
24
+ config: dict[str, Any] | None = None,
25
+ organization_id: str | None = None,
26
+ ) -> Any:
27
+ """Create a new schedule that triggers a workflow on a defined cadence."""
28
+ body: dict[str, Any] = {
29
+ k: v
30
+ for k, v in {
31
+ "workflow_id": workflow_id,
32
+ "name": name,
33
+ "schedule_type": schedule_type,
34
+ "interval_seconds": interval_seconds,
35
+ "cron_expression": cron_expression,
36
+ "timezone": timezone,
37
+ "description": description,
38
+ "input": input,
39
+ "config": config,
40
+ }.items()
41
+ if v is not None
42
+ }
43
+ return await self._post("/schedules", json=body, organization_id=organization_id)
44
+
45
+ async def list(
46
+ self,
47
+ *,
48
+ workflow_id: str | None = None,
49
+ is_active: bool | None = None,
50
+ limit: int = 50,
51
+ offset: int = 0,
52
+ organization_id: str | None = None,
53
+ ) -> Any:
54
+ """Return all schedules, optionally filtered by workflow or active status."""
55
+ params: dict[str, Any] = {
56
+ k: v
57
+ for k, v in {
58
+ "workflow_id": workflow_id,
59
+ "is_active": is_active,
60
+ "limit": limit,
61
+ "offset": offset,
62
+ }.items()
63
+ if v is not None
64
+ }
65
+ return await self._get("/schedules", params=params, organization_id=organization_id)
66
+
67
+ async def get(
68
+ self,
69
+ schedule_id: str,
70
+ *,
71
+ organization_id: str | None = None,
72
+ ) -> Any:
73
+ """Return a single schedule by its ID."""
74
+ return await self._get(f"/schedules/{schedule_id}", organization_id=organization_id)
75
+
76
+ async def update(
77
+ self,
78
+ schedule_id: str,
79
+ *,
80
+ organization_id: str | None = None,
81
+ **kwargs: Any,
82
+ ) -> Any:
83
+ """Update an existing schedule with the provided field values."""
84
+ body: dict[str, Any] = {k: v for k, v in kwargs.items() if v is not None}
85
+ return await self._put(f"/schedules/{schedule_id}", json=body, organization_id=organization_id)
86
+
87
+ async def delete(
88
+ self,
89
+ schedule_id: str,
90
+ *,
91
+ organization_id: str | None = None,
92
+ ) -> Any:
93
+ """Delete a schedule permanently by its ID."""
94
+ return await self._delete(f"/schedules/{schedule_id}", organization_id=organization_id)
95
+
96
+ async def pause(
97
+ self,
98
+ schedule_id: str,
99
+ *,
100
+ organization_id: str | None = None,
101
+ ) -> Any:
102
+ """Pause an active schedule so it no longer triggers new runs."""
103
+ return await self._post(f"/schedules/{schedule_id}/pause", organization_id=organization_id)
104
+
105
+ async def resume(
106
+ self,
107
+ schedule_id: str,
108
+ *,
109
+ organization_id: str | None = None,
110
+ ) -> Any:
111
+ """Resume a paused schedule so it resumes triggering runs."""
112
+ return await self._post(f"/schedules/{schedule_id}/resume", organization_id=organization_id)
113
+
114
+ async def list_runs(
115
+ self,
116
+ schedule_id: str,
117
+ *,
118
+ status: str | None = None,
119
+ limit: int = 50,
120
+ offset: int = 0,
121
+ organization_id: str | None = None,
122
+ ) -> Any:
123
+ """Return execution runs for a schedule, optionally filtered by status."""
124
+ params: dict[str, Any] = {
125
+ k: v for k, v in {"status": status, "limit": limit, "offset": offset}.items() if v is not None
126
+ }
127
+ return await self._get(
128
+ f"/schedules/{schedule_id}/runs",
129
+ params=params,
130
+ organization_id=organization_id,
131
+ )
132
+
133
+ async def run_stats(
134
+ self,
135
+ schedule_id: str,
136
+ *,
137
+ days: int = 7,
138
+ organization_id: str | None = None,
139
+ ) -> Any:
140
+ """Return aggregated run statistics for a schedule over the given number of days."""
141
+ params: dict[str, Any] = {"days": days}
142
+ return await self._get(
143
+ f"/schedules/{schedule_id}/runs/stats",
144
+ params=params,
145
+ organization_id=organization_id,
146
+ )
147
+
148
+ async def get_run(
149
+ self,
150
+ schedule_id: str,
151
+ run_id: str,
152
+ *,
153
+ organization_id: str | None = None,
154
+ ) -> Any:
155
+ """Return a single run record for a schedule by run ID."""
156
+ return await self._get(
157
+ f"/schedules/{schedule_id}/runs/{run_id}",
158
+ organization_id=organization_id,
159
+ )
160
+
161
+ async def retry_run(
162
+ self,
163
+ schedule_id: str,
164
+ run_id: str,
165
+ *,
166
+ organization_id: str | None = None,
167
+ ) -> Any:
168
+ """Retry a failed schedule run by its ID."""
169
+ return await self._post(
170
+ f"/schedules/{schedule_id}/runs/{run_id}/retry",
171
+ organization_id=organization_id,
172
+ )
@@ -0,0 +1,38 @@
1
+ """Subscriptions resource for the ModuleX Python SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from modulex._base import _BaseResource
8
+
9
+
10
+ class Subscriptions(_BaseResource):
11
+ """Resource for managing organization subscription plans and billing."""
12
+
13
+ async def organization_plans(self, *, organization_id: str | None = None) -> Any:
14
+ """Return available subscription plans for the organization."""
15
+ return await self._get("/subscriptions/organization-plans", organization_id=organization_id)
16
+
17
+ async def organization_billing(self, *, organization_id: str | None = None) -> Any:
18
+ """Return the current billing details for the organization."""
19
+ return await self._get("/subscriptions/organization-billing", organization_id=organization_id)
20
+
21
+ async def checkout_link(
22
+ self,
23
+ plan_id: str,
24
+ interval: str,
25
+ *,
26
+ organization_id: str | None = None,
27
+ ) -> Any:
28
+ """Generate a Stripe checkout link for upgrading to a given plan and billing interval."""
29
+ params: dict[str, Any] = {"plan_id": plan_id, "interval": interval}
30
+ return await self._post(
31
+ "/subscriptions/checkout-link",
32
+ json=params,
33
+ organization_id=organization_id,
34
+ )
35
+
36
+ async def customer_portal(self, *, organization_id: str | None = None) -> Any:
37
+ """Generate a Stripe customer portal link for managing the organization's subscription."""
38
+ return await self._post("/subscriptions/customer-portal", organization_id=organization_id)
@@ -0,0 +1,28 @@
1
+ """System resource for the ModuleX Python SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from modulex._base import _BaseResource
8
+
9
+
10
+ class System(_BaseResource):
11
+ """Resource for system-level health, metrics, and reference data."""
12
+
13
+ async def health(self) -> Any:
14
+ """Return the current health status of the API service."""
15
+ return await self._get("/system/health")
16
+
17
+ async def metrics(self) -> Any:
18
+ """Return system-level performance and usage metrics."""
19
+ return await self._get("/system/metrics")
20
+
21
+ async def timezones(self) -> Any:
22
+ """Return the full list of supported IANA timezone identifiers."""
23
+ return await self._get("/system/timezones")
24
+
25
+ async def search_timezones(self, query: str) -> Any:
26
+ """Search supported timezones by a keyword query string."""
27
+ params: dict[str, Any] = {"q": query}
28
+ return await self._get("/system/timezones/search", params=params)