maeyr 0.2.8__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.
maeyr/__init__.py ADDED
@@ -0,0 +1,57 @@
1
+ """Maeyr platform SDK."""
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ __version__ = "0.2.8"
6
+
7
+ __all__ = [
8
+ "MaeyrApiError",
9
+ "MaeyrAuthenticationError",
10
+ "MaeyrClient",
11
+ "MaeyrNotFoundError",
12
+ "MaeyrRateLimitError",
13
+ "MaeyrStreamError",
14
+ "MaeyrTransportError",
15
+ "MaeyrValidationError",
16
+ "WebhookClient",
17
+ "__version__",
18
+ ]
19
+
20
+
21
+ def __getattr__(name: str) -> Any:
22
+ if name == "MaeyrClient":
23
+ from maeyr.client import MaeyrClient
24
+
25
+ return MaeyrClient
26
+ if name == "WebhookClient":
27
+ from maeyr.client import WebhookClient
28
+
29
+ return WebhookClient
30
+ for exc_name, exc_path in (
31
+ ("MaeyrApiError", "MaeyrApiError"),
32
+ ("MaeyrAuthenticationError", "MaeyrAuthenticationError"),
33
+ ("MaeyrNotFoundError", "MaeyrNotFoundError"),
34
+ ("MaeyrRateLimitError", "MaeyrRateLimitError"),
35
+ ("MaeyrStreamError", "MaeyrStreamError"),
36
+ ("MaeyrTransportError", "MaeyrTransportError"),
37
+ ("MaeyrValidationError", "MaeyrValidationError"),
38
+ ):
39
+ if name == exc_name:
40
+ from maeyr.client import errors as err
41
+
42
+ return getattr(err, exc_path)
43
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
44
+
45
+
46
+ if TYPE_CHECKING:
47
+ from maeyr.client import (
48
+ MaeyrApiError,
49
+ MaeyrAuthenticationError,
50
+ MaeyrClient,
51
+ MaeyrNotFoundError,
52
+ MaeyrRateLimitError,
53
+ MaeyrStreamError,
54
+ MaeyrTransportError,
55
+ MaeyrValidationError,
56
+ WebhookClient,
57
+ )
maeyr/_constants.py ADDED
@@ -0,0 +1,27 @@
1
+ """Platform API constants."""
2
+
3
+ DEFAULT_BASE_URL = "https://api.maeyr.com"
4
+
5
+ SERVICE_PATHS = {
6
+ "auth": "/auth",
7
+ "builder": "/builder",
8
+ "chat": "/chat",
9
+ "pulse": "/pulse",
10
+ "workflow": "/workflow",
11
+ "scheduler": "/scheduler",
12
+ "marketplace": "/marketplace/api/v1/marketplace",
13
+ "org": "/org",
14
+ "project": "/project",
15
+ }
16
+
17
+ ENV_ACCESS_TOKEN = "MAEYR_ACCESS_TOKEN"
18
+ ENV_API_KEY = "MAEYR_API_KEY"
19
+ ENV_EMAIL = "MAEYR_EMAIL"
20
+ ENV_PASSWORD = "MAEYR_PASSWORD"
21
+ ENV_REFRESH_TOKEN = "MAEYR_REFRESH_TOKEN"
22
+ ENV_ORG_ID = "MAEYR_ORG_ID"
23
+ ENV_PROJECT_ID = "MAEYR_PROJECT_ID"
24
+ ENV_BASE_URL = "MAEYR_BASE_URL"
25
+ ENV_AGENT_ALIAS = "MAEYR_AGENT_ALIAS"
26
+ ENV_MCP_TOKEN = "MAEYR_MCP_TOKEN"
27
+ ENV_MCP_GATEWAY_URL = "MAEYR_MCP_GATEWAY_URL"
@@ -0,0 +1,42 @@
1
+ from maeyr.client.base import MaeyrClient
2
+ from maeyr.client.config import ClientConfig, RetryConfig
3
+ from maeyr.client.errors import (
4
+ ErrorDetail,
5
+ MaeyrApiError,
6
+ MaeyrAuthenticationError,
7
+ MaeyrConflictError,
8
+ MaeyrError,
9
+ MaeyrNotFoundError,
10
+ MaeyrPermissionError,
11
+ MaeyrRateLimitError,
12
+ MaeyrServerError,
13
+ MaeyrStreamError,
14
+ MaeyrTransportError,
15
+ MaeyrValidationError,
16
+ parse_error_details,
17
+ raise_for_response,
18
+ )
19
+ from maeyr.client.mcp import McpClient
20
+ from maeyr.client.webhook import WebhookClient
21
+
22
+ __all__ = [
23
+ "ClientConfig",
24
+ "ErrorDetail",
25
+ "RetryConfig",
26
+ "MaeyrApiError",
27
+ "MaeyrAuthenticationError",
28
+ "McpClient",
29
+ "MaeyrClient",
30
+ "MaeyrConflictError",
31
+ "MaeyrError",
32
+ "MaeyrNotFoundError",
33
+ "MaeyrPermissionError",
34
+ "MaeyrRateLimitError",
35
+ "MaeyrServerError",
36
+ "MaeyrStreamError",
37
+ "MaeyrTransportError",
38
+ "MaeyrValidationError",
39
+ "WebhookClient",
40
+ "parse_error_details",
41
+ "raise_for_response",
42
+ ]
maeyr/client/auth.py ADDED
@@ -0,0 +1,346 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Sequence
4
+ from typing import TYPE_CHECKING, Any, Dict, Optional
5
+ from urllib.parse import quote
6
+
7
+ from maeyr._constants import SERVICE_PATHS
8
+ from maeyr.models.auth import (
9
+ ApiKeyRequest,
10
+ KeyValidationRequest,
11
+ KeyValidationResponse,
12
+ LoginRequest,
13
+ RefreshRequest,
14
+ SwitchOrgRequest,
15
+ SwitchProjectRequest,
16
+ TokenResponse,
17
+ UserResponse,
18
+ WorkerKeyCreateRequest,
19
+ WorkerKeyRateLimit,
20
+ WorkerKeyScope,
21
+ )
22
+
23
+ if TYPE_CHECKING:
24
+ from maeyr.client.base import MaeyrClient
25
+
26
+
27
+ class _OrgClient:
28
+ def __init__(self, auth: AuthClient) -> None:
29
+ self._auth = auth
30
+ self._prefix = SERVICE_PATHS["org"]
31
+
32
+ async def list(self) -> Any:
33
+ return await self._auth._client._arequest("GET", self._prefix, "/list")
34
+
35
+ async def create(self, body: Dict[str, Any]) -> Any:
36
+ return await self._auth._client._arequest("POST", self._prefix, "/create", json=body)
37
+
38
+ async def get(self, org_id: str) -> Any:
39
+ return await self._auth._client._arequest("GET", self._prefix, f"/{org_id}")
40
+
41
+ async def update(self, org_id: str, body: Dict[str, Any]) -> Any:
42
+ return await self._auth._client._arequest("PUT", self._prefix, f"/{org_id}", json=body)
43
+
44
+ async def delete(self, org_id: str) -> Any:
45
+ return await self._auth._client._arequest("DELETE", self._prefix, f"/{org_id}")
46
+
47
+
48
+ class _ProjectClient:
49
+ def __init__(self, auth: AuthClient) -> None:
50
+ self._auth = auth
51
+ self._prefix = SERVICE_PATHS["project"]
52
+
53
+ async def create(self, body: Dict[str, Any]) -> Any:
54
+ return await self._auth._client._arequest("POST", self._prefix, "/create", json=body)
55
+
56
+ async def list(self, org_id: str) -> Any:
57
+ return await self._auth._client._arequest("GET", self._prefix, f"/list/{org_id}")
58
+
59
+ async def get(self, project_id: str) -> Any:
60
+ return await self._auth._client._arequest("GET", self._prefix, f"/{project_id}")
61
+
62
+ async def update(self, project_id: str, body: Dict[str, Any]) -> Any:
63
+ return await self._auth._client._arequest("PUT", self._prefix, f"/{project_id}", json=body)
64
+
65
+ async def delete(self, project_id: str) -> Any:
66
+ return await self._auth._client._arequest("DELETE", self._prefix, f"/{project_id}")
67
+
68
+
69
+ class AuthClient:
70
+ def __init__(self, client: MaeyrClient) -> None:
71
+ self._client = client
72
+ self._prefix = SERVICE_PATHS["auth"]
73
+ self.orgs = _OrgClient(self)
74
+ self.projects = _ProjectClient(self)
75
+
76
+ async def login(self, email: str, password: str) -> TokenResponse:
77
+ data = await self._client._arequest(
78
+ "POST",
79
+ self._prefix,
80
+ "/individual/login",
81
+ json=LoginRequest(email=email, password=password).model_dump(),
82
+ )
83
+ tokens = TokenResponse.model_validate(data)
84
+ self._apply_tokens(tokens)
85
+ return tokens
86
+
87
+ def login_sync(self, email: str, password: str) -> TokenResponse:
88
+ data = self._client._request(
89
+ "POST",
90
+ self._prefix,
91
+ "/individual/login",
92
+ json=LoginRequest(email=email, password=password).model_dump(),
93
+ )
94
+ tokens = TokenResponse.model_validate(data)
95
+ self._apply_tokens(tokens)
96
+ return tokens
97
+
98
+ async def refresh(self) -> TokenResponse:
99
+ if not self._client.refresh_token:
100
+ raise ValueError("refresh_token is required to refresh")
101
+ data = await self._client._arequest(
102
+ "POST",
103
+ self._prefix,
104
+ "/refresh",
105
+ json=RefreshRequest(refresh_token=self._client.refresh_token).model_dump(),
106
+ )
107
+ tokens = TokenResponse.model_validate(data)
108
+ self._apply_tokens(tokens)
109
+ return tokens
110
+
111
+ def refresh_sync(self) -> TokenResponse:
112
+ if not self._client.refresh_token:
113
+ raise ValueError("refresh_token is required to refresh")
114
+ data = self._client._request(
115
+ "POST",
116
+ self._prefix,
117
+ "/refresh",
118
+ json=RefreshRequest(refresh_token=self._client.refresh_token).model_dump(),
119
+ )
120
+ tokens = TokenResponse.model_validate(data)
121
+ self._apply_tokens(tokens)
122
+ return tokens
123
+
124
+ async def logout(self) -> Any:
125
+ return await self._client._arequest("POST", self._prefix, "/logout")
126
+
127
+ async def logout_all(self) -> Any:
128
+ return await self._client._arequest("POST", self._prefix, "/logout-all")
129
+
130
+ async def me(self) -> UserResponse:
131
+ data = await self._client._arequest("GET", self._prefix, "/me")
132
+ return UserResponse.model_validate(data)
133
+
134
+ def me_sync(self) -> UserResponse:
135
+ data = self._client._request("GET", self._prefix, "/me")
136
+ return UserResponse.model_validate(data)
137
+
138
+ async def usage(self) -> Any:
139
+ return await self._client._arequest("GET", self._prefix, "/usage")
140
+
141
+ async def switch_org(self, org_id: str) -> TokenResponse:
142
+ data = await self._client._arequest(
143
+ "POST",
144
+ self._prefix,
145
+ "/switch-org",
146
+ json=SwitchOrgRequest(org_id=org_id).model_dump(),
147
+ )
148
+ tokens = TokenResponse.model_validate(data)
149
+ self._apply_tokens(tokens)
150
+ return tokens
151
+
152
+ async def switch_project(self, project_id: str) -> TokenResponse:
153
+ data = await self._client._arequest(
154
+ "POST",
155
+ self._prefix,
156
+ "/switch-project",
157
+ json=SwitchProjectRequest(project_id=project_id).model_dump(),
158
+ )
159
+ tokens = TokenResponse.model_validate(data)
160
+ self._apply_tokens(tokens)
161
+ return tokens
162
+
163
+ async def list_sessions(self) -> Any:
164
+ return await self._client._arequest("GET", self._prefix, "/sessions")
165
+
166
+ async def revoke_session(self, session_id: str) -> Any:
167
+ return await self._client._arequest("DELETE", self._prefix, f"/sessions/{session_id}")
168
+
169
+ async def revoke_all_sessions(self) -> Any:
170
+ return await self._client._arequest("DELETE", self._prefix, "/sessions")
171
+
172
+ async def validate_api_key(self, api_key: str) -> KeyValidationResponse:
173
+ """Validate a project API key (no JWT required)."""
174
+ data = await self._client._arequest(
175
+ "POST",
176
+ self._prefix,
177
+ "/key/validate/api",
178
+ json=KeyValidationRequest(api_key=api_key).model_dump(),
179
+ )
180
+ return KeyValidationResponse.model_validate(data)
181
+
182
+ def validate_api_key_sync(self, api_key: str) -> KeyValidationResponse:
183
+ data = self._client._request(
184
+ "POST",
185
+ self._prefix,
186
+ "/key/validate/api",
187
+ json=KeyValidationRequest(api_key=api_key).model_dump(),
188
+ )
189
+ return KeyValidationResponse.model_validate(data)
190
+
191
+ async def create_api_key(self, name: str, description: Optional[str] = None) -> Any:
192
+ return await self._client._arequest(
193
+ "POST",
194
+ self._prefix,
195
+ "/key/api",
196
+ json=ApiKeyRequest(name=name, description=description).model_dump(),
197
+ )
198
+
199
+ async def list_api_keys(self, *, skip: int = 0, limit: int = 50) -> Any:
200
+ return await self._client._arequest(
201
+ "GET",
202
+ self._prefix,
203
+ "/key/api",
204
+ params={"skip": skip, "limit": limit},
205
+ )
206
+
207
+ async def revoke_api_key(self, key_id: str) -> Any:
208
+ return await self._client._arequest("POST", self._prefix, f"/key/api/{key_id}/revoke")
209
+
210
+ async def delete_api_key(self, key_id: str) -> Any:
211
+ return await self._client._arequest("DELETE", self._prefix, f"/key/api/{key_id}")
212
+
213
+ async def create_worker_key(
214
+ self,
215
+ name: str | WorkerKeyCreateRequest,
216
+ description: Optional[str] = None,
217
+ *,
218
+ project_id: Optional[str] = None,
219
+ scopes: Optional[Sequence[WorkerKeyScope]] = None,
220
+ expires_in_days: Optional[int] = None,
221
+ key_id: Optional[str] = None,
222
+ rate_limit: Optional[WorkerKeyRateLimit] = None,
223
+ ) -> Any:
224
+ """Create a project-scoped worker key using the current Auth contract.
225
+
226
+ Passing a string preserves the pre-0.2.8 call shape. ``description`` is
227
+ still accepted for source compatibility but is not sent because the
228
+ Auth worker-key request does not define that field. New callers may pass
229
+ a :class:`WorkerKeyCreateRequest` as the first argument.
230
+ """
231
+ if isinstance(name, WorkerKeyCreateRequest):
232
+ if any(
233
+ value is not None
234
+ for value in (
235
+ description,
236
+ project_id,
237
+ scopes,
238
+ expires_in_days,
239
+ key_id,
240
+ rate_limit,
241
+ )
242
+ ):
243
+ raise ValueError(
244
+ "worker-key request model cannot be combined with individual fields"
245
+ )
246
+ request = name
247
+ else:
248
+ request_fields: Dict[str, Any] = {
249
+ "name": name,
250
+ "key_type": "worker",
251
+ }
252
+ if project_id is not None:
253
+ request_fields["project_id"] = project_id
254
+ if scopes is not None:
255
+ request_fields["scopes"] = list(scopes)
256
+ if expires_in_days is not None:
257
+ request_fields["expires_in_days"] = expires_in_days
258
+ if key_id is not None:
259
+ request_fields["key_id"] = key_id
260
+ if rate_limit is not None:
261
+ request_fields["rate_limit"] = rate_limit
262
+ request = WorkerKeyCreateRequest.model_validate(request_fields)
263
+ return await self._client._arequest(
264
+ "POST",
265
+ self._prefix,
266
+ "/key/worker",
267
+ json=request.model_dump(exclude_none=True),
268
+ )
269
+
270
+ async def list_worker_keys(
271
+ self,
272
+ *,
273
+ project_id: Optional[str] = None,
274
+ include_revoked: bool = False,
275
+ skip: int = 0,
276
+ limit: int = 50,
277
+ search: Optional[str] = None,
278
+ ) -> Any:
279
+ """List worker keys using Auth's project and pagination filters."""
280
+ if skip < 0:
281
+ raise ValueError("skip must be non-negative")
282
+ if not 1 <= limit <= 500:
283
+ raise ValueError("limit must be between 1 and 500")
284
+ params: Dict[str, Any] = {
285
+ "include_revoked": include_revoked,
286
+ "skip": skip,
287
+ "limit": limit,
288
+ }
289
+ if project_id is not None:
290
+ params["project_id"] = project_id
291
+ if search is not None:
292
+ params["search"] = search
293
+ return await self._client._arequest(
294
+ "GET",
295
+ self._prefix,
296
+ "/key/worker",
297
+ params=params,
298
+ )
299
+
300
+ async def revoke_worker_key(
301
+ self,
302
+ key_id: str,
303
+ *,
304
+ project_id: Optional[str] = None,
305
+ ) -> Any:
306
+ """Revoke a worker key without deleting its audit record."""
307
+ path = self._worker_key_path(key_id, suffix="/revoke")
308
+ params = {"project_id": project_id} if project_id is not None else None
309
+ return await self._client._arequest(
310
+ "POST",
311
+ self._prefix,
312
+ path,
313
+ params=params,
314
+ )
315
+
316
+ async def delete_worker_key(
317
+ self,
318
+ key_id: str,
319
+ *,
320
+ project_id: Optional[str] = None,
321
+ ) -> Any:
322
+ """Permanently delete a worker key in the selected project."""
323
+ path = self._worker_key_path(key_id)
324
+ params = {"project_id": project_id} if project_id is not None else None
325
+ return await self._client._arequest(
326
+ "DELETE",
327
+ self._prefix,
328
+ path,
329
+ params=params,
330
+ )
331
+
332
+ @staticmethod
333
+ def _worker_key_path(key_id: str, *, suffix: str = "") -> str:
334
+ key_id = key_id.strip()
335
+ if not key_id:
336
+ raise ValueError("key_id is required")
337
+ return f"/key/worker/{quote(key_id, safe='')}{suffix}"
338
+
339
+ def _apply_tokens(self, tokens: TokenResponse) -> None:
340
+ self._client.access_token = tokens.access_token
341
+ self._client.refresh_token = tokens.refresh_token
342
+ if tokens.org_id:
343
+ self._client.org_id = tokens.org_id
344
+ if tokens.project_id:
345
+ self._client.project_id = tokens.project_id
346
+ self._client._sync_headers()