memorysync 1.0.0__tar.gz → 1.1.0__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.
@@ -0,0 +1,48 @@
1
+ # ==============================================================================
2
+ # MemorySync — Git Ignore Rules (Transparency-First Philosophy)
3
+ # ==============================================================================
4
+
5
+ # -------------------------
6
+ # Category 1: Secrets & Credentials (NEVER COMMIT)
7
+ # -------------------------
8
+ .env
9
+ .env.*
10
+ !memorysync-benchmark/longmemeval/.env.example
11
+ *.pem
12
+ *.key
13
+ *.crt
14
+ secrets/
15
+
16
+ # -------------------------
17
+ # Category 2: User / Customer / Private Data (NEVER COMMIT)
18
+ # -------------------------
19
+ uploads/
20
+ backups/
21
+ /storage/
22
+ chroma/
23
+ chroma_data/
24
+ *.db
25
+ *.sqlite
26
+ *.sqlite3
27
+ *.dump
28
+ *.bak
29
+
30
+ # -------------------------
31
+ # Category 3: Massive Dependency / Build / Cache Artifacts (BLOAT)
32
+ # -------------------------
33
+ node_modules/
34
+ venv/
35
+ .venv/
36
+ __pycache__/
37
+ .pytest_cache/
38
+ .next/
39
+ dist/
40
+ build/
41
+ htmlcov/
42
+ .coverage
43
+ *.cover
44
+ out/
45
+ /dashboard/tsconfig.tsbuildinfo
46
+
47
+ # Responsive audit output (regenerated by npm run audit:responsive)
48
+ dashboard/responsive-audit-report.json
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memorysync
3
- Version: 1.0.0
3
+ Version: 1.1.0
4
4
  Summary: Official Python client for the MemorySync API.
5
- Project-URL: Homepage, https://memorysync.dev
6
- Project-URL: Documentation, https://memorysync.dev/docs
5
+ Project-URL: Homepage, https://memorysync.io
6
+ Project-URL: Documentation, https://docs.memorysync.io
7
7
  Author: MemorySync
8
8
  License: MIT
9
9
  License-File: LICENSE
@@ -38,7 +38,7 @@ from memorysync import MemorySyncClient
38
38
 
39
39
  ms = MemorySyncClient(
40
40
  api_key="...",
41
- base_url="https://api.memorysync.dev",
41
+ base_url="https://api.memorysync.io",
42
42
  project_id="proj_xxxxxxxxxxxxxxxx", # optional
43
43
  end_user_id="user_42", # optional
44
44
  )
@@ -119,6 +119,61 @@ else:
119
119
  print(result.id, result.text)
120
120
  ```
121
121
 
122
+ ## Control-plane client
123
+
124
+ Dashboard and administrative routes use bearer authentication, not the memory
125
+ client's API key. Tokens are never persisted or refreshed automatically.
126
+ Responses are typed dictionaries with snake_case keys, including routes whose
127
+ wire response uses camelCase.
128
+
129
+ ```python
130
+ import os
131
+ from memorysync import ControlPlaneClient
132
+
133
+ with ControlPlaneClient(
134
+ "https://api.memorysync.io",
135
+ access_token=os.environ["MEMORYSYNC_ACCESS_TOKEN"],
136
+ project_id="project_abc123", # optional X-Project-ID default
137
+ ) as control:
138
+ members = control.list_team_members()
139
+ hooks = control.list_webhooks()
140
+
141
+ # Login is the only operation that does not require a configured token.
142
+ with ControlPlaneClient("https://api.memorysync.io") as control:
143
+ login = control.login("developer@example.com", os.environ["MEMORYSYNC_PASSWORD"])
144
+ ```
145
+
146
+ `AsyncControlPlaneClient` exposes the same methods with `await` and `aclose()`.
147
+ Both clients accept `base_url`, optional `access_token`, optional `project_id`,
148
+ `timeout`, and an injectable sync/async `httpx` `transport`. Project-scoped
149
+ operations accept a per-call `project_id` override.
150
+
151
+ | Method | HTTP route |
152
+ | --- | --- |
153
+ | `bulk_revoke_api_keys` | `POST /org/api-keys/bulk-revoke` |
154
+ | `test_api_key` | `POST /org/api-keys/{key_id}/test` |
155
+ | `login` | `POST /auth/login` |
156
+ | `get_current_plan` | `GET /org/billing/current-plan` |
157
+ | `list_team_members` | `GET /admin/team/members` |
158
+ | `suspend_team_member` | `PATCH /admin/team/members/{member_id}` |
159
+ | `remove_team_member` | `DELETE /admin/team/members/{member_id}` |
160
+ | `list_sessions` | `GET /auth/sessions` |
161
+ | `revoke_session` | `POST /auth/sessions/{session_id}/revoke` |
162
+ | `list_audit_events` | `GET /admin/audit-logs` |
163
+ | `list_integrations` | `GET /api/v1/integrations/catalog` |
164
+ | `create_organization` | `POST /organizations` |
165
+ | `list_organizations` | `GET /organizations` |
166
+ | `list_organization_members` | delegates to `list_team_members` |
167
+ | `get_organization_settings` | `GET /admin/tenant-settings` |
168
+ | `list_projects` | `GET /org/projects` |
169
+ | `create_webhook` | `POST /org/webhooks` |
170
+ | `list_webhooks` | `GET /org/webhooks` |
171
+ | `update_webhook` | `PATCH /org/webhooks/{endpoint_id}` |
172
+ | `delete_webhook` | `DELETE /org/webhooks/{endpoint_id}` |
173
+ | `test_webhook` | `POST /org/webhooks/{endpoint_id}/test` |
174
+ | `replay_webhook_deliveries` | `POST /org/webhooks/{endpoint_id}/replay` |
175
+ | `list_webhook_deliveries` | `GET /org/webhooks/{endpoint_id}/deliveries` |
176
+
122
177
  ## Errors
123
178
 
124
179
  Every non-2xx response raises a typed subclass of `MemorySyncError`:
@@ -13,7 +13,7 @@ from memorysync import MemorySyncClient
13
13
 
14
14
  ms = MemorySyncClient(
15
15
  api_key="...",
16
- base_url="https://api.memorysync.dev",
16
+ base_url="https://api.memorysync.io",
17
17
  project_id="proj_xxxxxxxxxxxxxxxx", # optional
18
18
  end_user_id="user_42", # optional
19
19
  )
@@ -94,6 +94,61 @@ else:
94
94
  print(result.id, result.text)
95
95
  ```
96
96
 
97
+ ## Control-plane client
98
+
99
+ Dashboard and administrative routes use bearer authentication, not the memory
100
+ client's API key. Tokens are never persisted or refreshed automatically.
101
+ Responses are typed dictionaries with snake_case keys, including routes whose
102
+ wire response uses camelCase.
103
+
104
+ ```python
105
+ import os
106
+ from memorysync import ControlPlaneClient
107
+
108
+ with ControlPlaneClient(
109
+ "https://api.memorysync.io",
110
+ access_token=os.environ["MEMORYSYNC_ACCESS_TOKEN"],
111
+ project_id="project_abc123", # optional X-Project-ID default
112
+ ) as control:
113
+ members = control.list_team_members()
114
+ hooks = control.list_webhooks()
115
+
116
+ # Login is the only operation that does not require a configured token.
117
+ with ControlPlaneClient("https://api.memorysync.io") as control:
118
+ login = control.login("developer@example.com", os.environ["MEMORYSYNC_PASSWORD"])
119
+ ```
120
+
121
+ `AsyncControlPlaneClient` exposes the same methods with `await` and `aclose()`.
122
+ Both clients accept `base_url`, optional `access_token`, optional `project_id`,
123
+ `timeout`, and an injectable sync/async `httpx` `transport`. Project-scoped
124
+ operations accept a per-call `project_id` override.
125
+
126
+ | Method | HTTP route |
127
+ | --- | --- |
128
+ | `bulk_revoke_api_keys` | `POST /org/api-keys/bulk-revoke` |
129
+ | `test_api_key` | `POST /org/api-keys/{key_id}/test` |
130
+ | `login` | `POST /auth/login` |
131
+ | `get_current_plan` | `GET /org/billing/current-plan` |
132
+ | `list_team_members` | `GET /admin/team/members` |
133
+ | `suspend_team_member` | `PATCH /admin/team/members/{member_id}` |
134
+ | `remove_team_member` | `DELETE /admin/team/members/{member_id}` |
135
+ | `list_sessions` | `GET /auth/sessions` |
136
+ | `revoke_session` | `POST /auth/sessions/{session_id}/revoke` |
137
+ | `list_audit_events` | `GET /admin/audit-logs` |
138
+ | `list_integrations` | `GET /api/v1/integrations/catalog` |
139
+ | `create_organization` | `POST /organizations` |
140
+ | `list_organizations` | `GET /organizations` |
141
+ | `list_organization_members` | delegates to `list_team_members` |
142
+ | `get_organization_settings` | `GET /admin/tenant-settings` |
143
+ | `list_projects` | `GET /org/projects` |
144
+ | `create_webhook` | `POST /org/webhooks` |
145
+ | `list_webhooks` | `GET /org/webhooks` |
146
+ | `update_webhook` | `PATCH /org/webhooks/{endpoint_id}` |
147
+ | `delete_webhook` | `DELETE /org/webhooks/{endpoint_id}` |
148
+ | `test_webhook` | `POST /org/webhooks/{endpoint_id}/test` |
149
+ | `replay_webhook_deliveries` | `POST /org/webhooks/{endpoint_id}/replay` |
150
+ | `list_webhook_deliveries` | `GET /org/webhooks/{endpoint_id}/deliveries` |
151
+
97
152
  ## Errors
98
153
 
99
154
  Every non-2xx response raises a typed subclass of `MemorySyncError`:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "memorysync"
7
- version = "1.0.0"
7
+ version = "1.1.0"
8
8
  description = "Official Python client for the MemorySync API."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -29,8 +29,8 @@ dependencies = [
29
29
  ]
30
30
 
31
31
  [project.urls]
32
- Homepage = "https://memorysync.dev"
33
- Documentation = "https://memorysync.dev/docs"
32
+ Homepage = "https://memorysync.io"
33
+ Documentation = "https://docs.memorysync.io"
34
34
 
35
35
  [tool.hatch.build.targets.wheel]
36
36
  packages = ["src/memorysync"]
@@ -0,0 +1,94 @@
1
+ """Public package surface for the MemorySync Python SDK."""
2
+
3
+ from ._version import __version__
4
+ from .errors import (
5
+ AuthError,
6
+ MemorySyncError,
7
+ NotFoundError,
8
+ RateLimitError,
9
+ ServerError,
10
+ ValidationError,
11
+ )
12
+ from .types import (
13
+ AddSkippedResponse,
14
+ BulkAddItem,
15
+ BulkAddItemResult,
16
+ BulkAddResponse,
17
+ ComposeResponse,
18
+ ExportResponse,
19
+ Memory,
20
+ QueryResponse,
21
+ Relation,
22
+ RelationshipType,
23
+ )
24
+ from .client import AsyncMemorySyncClient, MemorySyncClient
25
+ from .control_plane import AsyncControlPlaneClient, ControlPlaneClient
26
+ from .control_plane_types import (
27
+ ApiKeyTestResponse,
28
+ AuditEvent,
29
+ AuditEventsResponse,
30
+ BulkRevokeRequest,
31
+ BulkRevokeResponse,
32
+ BulkRevokeResult,
33
+ CreateOrganizationRequest,
34
+ CreateWebhookRequest,
35
+ CurrentPlanResponse,
36
+ Integration,
37
+ JsonObject,
38
+ LoginRequest,
39
+ LoginResponse,
40
+ LoginSession,
41
+ LoginTokens,
42
+ OrganizationMembership,
43
+ OrganizationSettings,
44
+ Project,
45
+ ReplayWebhookDeliveriesRequest,
46
+ Session,
47
+ SessionsResponse,
48
+ TeamMember,
49
+ TestWebhookRequest,
50
+ UpdateWebhookRequest,
51
+ Webhook,
52
+ WebhookDeliveriesResponse,
53
+ WebhookDelivery,
54
+ WebhookReplayResponse,
55
+ WebhooksResponse,
56
+ WebhookTestResponse,
57
+ )
58
+
59
+ __all__ = [
60
+ "__version__",
61
+ "MemorySyncClient",
62
+ "AsyncMemorySyncClient",
63
+ "Memory",
64
+ "QueryResponse",
65
+ "BulkAddItem",
66
+ "BulkAddItemResult",
67
+ "BulkAddResponse",
68
+ "AddSkippedResponse",
69
+ "ComposeResponse",
70
+ "ExportResponse",
71
+ "Relation",
72
+ "RelationshipType",
73
+ "MemorySyncError",
74
+ "AuthError",
75
+ "ValidationError",
76
+ "NotFoundError",
77
+ "RateLimitError",
78
+ "ServerError",
79
+ ]
80
+
81
+
82
+ __all__ += [
83
+ "ControlPlaneClient", "AsyncControlPlaneClient", "JsonObject",
84
+ "BulkRevokeRequest", "BulkRevokeResult", "BulkRevokeResponse",
85
+ "ApiKeyTestResponse", "LoginRequest", "LoginTokens", "LoginSession",
86
+ "LoginResponse", "CurrentPlanResponse", "TeamMember", "Session",
87
+ "SessionsResponse", "AuditEvent", "AuditEventsResponse", "Integration",
88
+ "CreateOrganizationRequest", "OrganizationMembership",
89
+ "OrganizationSettings", "Project", "CreateWebhookRequest",
90
+ "UpdateWebhookRequest", "TestWebhookRequest",
91
+ "ReplayWebhookDeliveriesRequest", "Webhook", "WebhooksResponse",
92
+ "WebhookTestResponse", "WebhookReplayResponse", "WebhookDelivery",
93
+ "WebhookDeliveriesResponse",
94
+ ]
@@ -0,0 +1 @@
1
+ __version__ = "1.1.0"
@@ -0,0 +1,629 @@
1
+ """Bearer-authenticated clients for MemorySync control-plane REST routes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from datetime import datetime, timezone
7
+ from email.utils import parsedate_to_datetime
8
+ from typing import Any, Dict, List, Mapping, Optional, Sequence, TypeVar, cast
9
+ from urllib.parse import urlparse
10
+
11
+ import httpx
12
+
13
+ from ._version import __version__
14
+ from .control_plane_types import (
15
+ ApiKeyTestResponse, AuditEventsResponse, BulkRevokeResponse,
16
+ CurrentPlanResponse, Integration, LoginResponse, OrganizationMembership,
17
+ OrganizationSettings, Project, Session, SessionsResponse, TeamMember,
18
+ Webhook, WebhookDeliveriesResponse, WebhookReplayResponse,
19
+ WebhooksResponse, WebhookTestResponse,
20
+ )
21
+ from .errors import (
22
+ AuthError, MemorySyncError, NotFoundError, RateLimitError, ServerError,
23
+ ValidationError,
24
+ )
25
+
26
+ _USER_AGENT = f"memorysync-sdk-py/{__version__}"
27
+ _T = TypeVar("_T")
28
+ _UNSET = object()
29
+
30
+
31
+ def _positive_id(value: Any, name: str) -> int:
32
+ if not isinstance(value, int) or isinstance(value, bool) or value <= 0:
33
+ raise ValidationError(f"{name} must be a positive integer")
34
+ return value
35
+
36
+
37
+ def _required_text(value: str, name: str) -> str:
38
+ if not isinstance(value, str) or not value.strip():
39
+ raise ValidationError(f"{name} must be a non-empty string")
40
+ return value
41
+
42
+
43
+ def _bounded(value: int, name: str, minimum: int, maximum: int) -> int:
44
+ if not isinstance(value, int) or isinstance(value, bool) or not minimum <= value <= maximum:
45
+ raise ValidationError(f"{name} must be an integer between {minimum} and {maximum}")
46
+ return value
47
+
48
+
49
+ def _project(value: Optional[str], name: str = "project_id") -> Optional[str]:
50
+ if value is not None:
51
+ _required_text(value, name)
52
+ return value
53
+
54
+
55
+ def _body(**values: Any) -> Dict[str, Any]:
56
+ return {key: value for key, value in values.items() if value is not None}
57
+
58
+
59
+ def _snake_key(key: str) -> str:
60
+ return re.sub(r"(?<!^)(?=[A-Z])", "_", key).lower()
61
+
62
+
63
+ def _snake(value: Any) -> Any:
64
+ if isinstance(value, dict):
65
+ return {_snake_key(str(key)): _snake(item) for key, item in value.items()}
66
+ if isinstance(value, list):
67
+ return [_snake(item) for item in value]
68
+ return value
69
+
70
+
71
+ def _detail(body: Any) -> Optional[str]:
72
+ if not isinstance(body, dict):
73
+ return None
74
+ detail = body.get("detail")
75
+ if isinstance(detail, str):
76
+ return detail
77
+ error = body.get("error")
78
+ if isinstance(error, dict) and isinstance(error.get("message"), str):
79
+ return cast(str, error["message"])
80
+ if isinstance(detail, list) and detail and isinstance(detail[0], dict):
81
+ message = detail[0].get("msg")
82
+ return message if isinstance(message, str) else None
83
+ return None
84
+
85
+
86
+ def _retry_after(headers: Mapping[str, str], body: Any) -> float:
87
+ raw = headers.get("retry-after")
88
+ if raw:
89
+ try:
90
+ return max(0.0, float(raw))
91
+ except ValueError:
92
+ try:
93
+ when = parsedate_to_datetime(raw)
94
+ if when.tzinfo is None:
95
+ when = when.replace(tzinfo=timezone.utc)
96
+ return max(0.0, (when - datetime.now(timezone.utc)).total_seconds())
97
+ except (TypeError, ValueError, OverflowError):
98
+ pass
99
+ if isinstance(body, dict):
100
+ value = body.get("retry_after")
101
+ error = body.get("error")
102
+ if value is None and isinstance(error, dict):
103
+ value = error.get("retry_after")
104
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
105
+ return max(0.0, float(value))
106
+ return 0.0
107
+
108
+
109
+ def _raise_response(response: httpx.Response, body: Any) -> None:
110
+ status = response.status_code
111
+ request_id = response.headers.get("x-request-id") or response.headers.get("request-id")
112
+ kwargs = {"status_code": status, "response": body, "request_id": request_id}
113
+ message = _detail(body)
114
+ if status in (401, 403):
115
+ raise AuthError(message or ("Unauthenticated" if status == 401 else "Forbidden"), **kwargs)
116
+ if status == 404:
117
+ raise NotFoundError(message or "Not found", **kwargs)
118
+ if status in (400, 409, 422):
119
+ raise ValidationError(message or "Validation error", **kwargs)
120
+ if status == 429:
121
+ raise RateLimitError(message or "Rate limited", _retry_after(response.headers, body), **kwargs)
122
+ if status >= 500:
123
+ raise ServerError(message or f"Server error ({status})", **kwargs)
124
+ raise MemorySyncError(message or f"Unexpected status {status}", **kwargs)
125
+
126
+
127
+ class _ControlPlaneBase:
128
+ def __init__(self, base_url: str, access_token: Optional[str], project_id: Optional[str]) -> None:
129
+ self._base_url = _required_text(base_url, "base_url").rstrip("/")
130
+ parsed_base = urlparse(self._base_url)
131
+ if parsed_base.scheme not in ("http", "https") or not parsed_base.netloc:
132
+ raise ValidationError("base_url must be an absolute HTTP or HTTPS URL")
133
+ self._access_token = access_token
134
+ if access_token is not None:
135
+ _required_text(access_token, "access_token")
136
+ self._project_id = _project(project_id)
137
+
138
+ def _headers(
139
+ self, *, require_auth: bool = True, project_id: object = _UNSET
140
+ ) -> Dict[str, str]:
141
+ headers = {"Accept": "application/json", "User-Agent": _USER_AGENT}
142
+ if require_auth:
143
+ if not self._access_token:
144
+ raise AuthError("access_token is required for this operation")
145
+ headers["Authorization"] = f"Bearer {self._access_token}"
146
+ selected = self._project_id if project_id is _UNSET else cast(Optional[str], project_id)
147
+ if selected is not None:
148
+ headers["X-Project-ID"] = cast(str, _project(selected))
149
+ return headers
150
+
151
+ @staticmethod
152
+ def _decode(response: httpx.Response) -> Any:
153
+ if response.status_code == 204:
154
+ return None
155
+ try:
156
+ raw: Any = response.json()
157
+ except ValueError:
158
+ raw = response.text or None
159
+ if response.status_code >= 400:
160
+ _raise_response(response, raw)
161
+ return _snake(raw)
162
+
163
+
164
+ def _validate_webhook(name: str, url: str, events: Sequence[str]) -> None:
165
+ _required_text(name, "name")
166
+ if len(name) > 128:
167
+ raise ValidationError("name may contain at most 128 characters")
168
+ _required_text(url, "url")
169
+ parsed = urlparse(url)
170
+ if parsed.scheme not in ("http", "https") or not parsed.netloc:
171
+ raise ValidationError("url must be an absolute HTTP or HTTPS URL")
172
+ if not events or any(not isinstance(event, str) or not event.strip() for event in events):
173
+ raise ValidationError("events must contain at least one non-empty string")
174
+
175
+
176
+ class ControlPlaneClient(_ControlPlaneBase):
177
+ """Synchronous bearer client for dashboard/control-plane routes."""
178
+
179
+ def __init__(
180
+ self, base_url: str, *, access_token: Optional[str] = None,
181
+ project_id: Optional[str] = None, timeout: float = 30.0,
182
+ transport: Optional[httpx.BaseTransport] = None,
183
+ ) -> None:
184
+ super().__init__(base_url, access_token, project_id)
185
+ self._http = httpx.Client(timeout=timeout, transport=transport)
186
+
187
+ def __enter__(self) -> "ControlPlaneClient":
188
+ return self
189
+
190
+ def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
191
+ self.close()
192
+
193
+ def close(self) -> None:
194
+ self._http.close()
195
+
196
+ def _request(
197
+ self, method: str, path: str, *, json: Optional[Dict[str, Any]] = None,
198
+ params: Optional[Dict[str, Any]] = None, require_auth: bool = True,
199
+ project_id: object = _UNSET,
200
+ ) -> Any:
201
+ try:
202
+ response = self._http.request(
203
+ method, f"{self._base_url}{path}",
204
+ headers=self._headers(require_auth=require_auth, project_id=project_id),
205
+ json=json, params=params,
206
+ )
207
+ except httpx.TimeoutException as exc:
208
+ raise MemorySyncError("Control-plane request timed out") from exc
209
+ except httpx.HTTPError as exc:
210
+ raise MemorySyncError("Control-plane network request failed") from exc
211
+ return self._decode(response)
212
+
213
+ def bulk_revoke_api_keys(
214
+ self, key_ids: Sequence[int], *, project_id: Optional[str] = None
215
+ ) -> BulkRevokeResponse:
216
+ ids = list(key_ids)
217
+ if not 1 <= len(ids) <= 100:
218
+ raise ValidationError("key_ids must contain between 1 and 100 entries")
219
+ for key_id in ids:
220
+ _positive_id(key_id, "key_id")
221
+ raw = self._request("POST", "/org/api-keys/bulk-revoke", json={"key_ids": ids}, project_id=project_id or _UNSET)
222
+ return cast(BulkRevokeResponse, raw)
223
+
224
+ def test_api_key(self, key_id: int, *, project_id: Optional[str] = None) -> ApiKeyTestResponse:
225
+ raw = self._request("POST", f"/org/api-keys/{_positive_id(key_id, 'key_id')}/test", project_id=project_id or _UNSET)
226
+ return cast(ApiKeyTestResponse, raw)
227
+
228
+ def login(self, email: str, password: str) -> LoginResponse:
229
+ _required_text(email, "email")
230
+ if "@" not in email:
231
+ raise ValidationError("email must be a valid email address")
232
+ _required_text(password, "password")
233
+ return cast(LoginResponse, self._request("POST", "/auth/login", json={"email": email, "password": password}, require_auth=False, project_id=None))
234
+
235
+ def get_current_plan(self) -> CurrentPlanResponse:
236
+ return cast(CurrentPlanResponse, self._request("GET", "/org/billing/current-plan"))
237
+
238
+ def list_team_members(self) -> List[TeamMember]:
239
+ return cast(List[TeamMember], self._request("GET", "/admin/team/members"))
240
+
241
+ def suspend_team_member(self, member_id: int) -> TeamMember:
242
+ path = f"/admin/team/members/{_positive_id(member_id, 'member_id')}"
243
+ return cast(TeamMember, self._request("PATCH", path, json={"status": "suspended"}))
244
+
245
+ def remove_team_member(self, member_id: int) -> None:
246
+ path = f"/admin/team/members/{_positive_id(member_id, 'member_id')}"
247
+ self._request("DELETE", path)
248
+
249
+ def list_sessions(self) -> SessionsResponse:
250
+ return cast(SessionsResponse, self._request("GET", "/auth/sessions"))
251
+
252
+ def revoke_session(self, session_id: int) -> None:
253
+ self._request("POST", f"/auth/sessions/{_positive_id(session_id, 'session_id')}/revoke")
254
+
255
+
256
+ def list_audit_events(
257
+ self, *, limit: Optional[int] = None, cursor: Optional[int] = None,
258
+ skip: Optional[int] = None, sort_direction: Optional[str] = None,
259
+ tenant_id: Optional[str] = None, actor: Optional[str] = None,
260
+ actor_email: Optional[str] = None, ip: Optional[str] = None,
261
+ action: Optional[str] = None, resource_type: Optional[str] = None,
262
+ resource_id: Optional[str] = None, severity: Optional[str] = None,
263
+ category: Optional[str] = None, start: Optional[str] = None,
264
+ end: Optional[str] = None, success: Optional[bool] = None,
265
+ source: Optional[str] = None, ingest_method: Optional[str] = None,
266
+ search: Optional[str] = None, include_stats: Optional[bool] = None,
267
+ ) -> AuditEventsResponse:
268
+ if limit is not None:
269
+ _bounded(limit, "limit", 1, 200)
270
+ if cursor is not None and cursor < 0:
271
+ raise ValidationError("cursor must be non-negative")
272
+ if skip is not None and skip < 0:
273
+ raise ValidationError("skip must be non-negative")
274
+ if sort_direction not in (None, "asc", "desc"):
275
+ raise ValidationError("sort_direction must be 'asc' or 'desc'")
276
+ params = _body(limit=limit, cursor=cursor, skip=skip, sort=sort_direction,
277
+ tenant_id=tenant_id, actor=actor, actor_email=actor_email,
278
+ ip=ip, action=action, resource_type=resource_type,
279
+ resource_id=resource_id, severity=severity, category=category,
280
+ start=start, end=end, success=success, source=source,
281
+ ingest_method=ingest_method, search=search,
282
+ include_stats=include_stats)
283
+ raw = self._request("GET", "/admin/audit-logs", params=params)
284
+ return cast(AuditEventsResponse, {
285
+ "events": raw.get("logs", []),
286
+ "next_cursor": raw.get("next_cursor"),
287
+ "stats": raw.get("stats"),
288
+ "sort": raw.get("sort", sort_direction or "desc"),
289
+ })
290
+
291
+ def list_integrations(self, *, category: Optional[str] = None) -> List[Integration]:
292
+ if category is not None:
293
+ _required_text(category, "category")
294
+ return cast(List[Integration], self._request("GET", "/api/v1/integrations/catalog", params=_body(category=category)))
295
+
296
+ def create_organization(self, name: str, *, domain: Optional[str] = None) -> OrganizationMembership:
297
+ _required_text(name, "name")
298
+ if domain is not None:
299
+ _required_text(domain, "domain")
300
+ return cast(OrganizationMembership, self._request("POST", "/organizations", json=_body(name=name, domain=domain)))
301
+
302
+ def list_organizations(self) -> List[OrganizationMembership]:
303
+ return cast(List[OrganizationMembership], self._request("GET", "/organizations"))
304
+
305
+ def list_organization_members(self) -> List[TeamMember]:
306
+ return self.list_team_members()
307
+
308
+ def get_organization_settings(self, *, tenant_id: Optional[str] = None) -> OrganizationSettings:
309
+ if tenant_id is not None:
310
+ _required_text(tenant_id, "tenant_id")
311
+ return cast(OrganizationSettings, self._request("GET", "/admin/tenant-settings", params=_body(tenant_id=tenant_id)))
312
+
313
+ def list_projects(self) -> List[Project]:
314
+ return cast(List[Project], self._request("GET", "/org/projects"))
315
+
316
+ def create_webhook(
317
+ self, name: str, url: str, events: Sequence[str], *,
318
+ description: Optional[str] = None, retry_config: Optional[Dict[str, Any]] = None,
319
+ signature_config: Optional[Dict[str, Any]] = None,
320
+ project_id: Optional[str] = None,
321
+ ) -> Webhook:
322
+ _validate_webhook(name, url, events)
323
+ if description is not None and len(description) > 500:
324
+ raise ValidationError("description may contain at most 500 characters")
325
+ selected = project_id or self._project_id
326
+ data = _body(name=name, url=url, events=list(events), description=description,
327
+ retry_config=retry_config, signature_config=signature_config,
328
+ project_id=project_id)
329
+ return cast(Webhook, self._request("POST", "/org/webhooks", json=data, project_id=selected))
330
+
331
+ def list_webhooks(self, *, project_id: Optional[str] = None) -> WebhooksResponse:
332
+ return cast(WebhooksResponse, self._request("GET", "/org/webhooks", project_id=project_id or _UNSET))
333
+
334
+
335
+ def update_webhook(
336
+ self, endpoint_id: int, *, name: Optional[str] = None,
337
+ url: Optional[str] = None, description: Optional[str] = None,
338
+ events: Optional[Sequence[str]] = None,
339
+ retry_config: Optional[Dict[str, Any]] = None,
340
+ signature_config: Optional[Dict[str, Any]] = None,
341
+ project_id: Optional[str] = None,
342
+ ) -> Webhook:
343
+ if name is not None:
344
+ _required_text(name, "name")
345
+ if len(name) > 128:
346
+ raise ValidationError("name may contain at most 128 characters")
347
+ if url is not None:
348
+ parsed = urlparse(url)
349
+ if parsed.scheme not in ("http", "https") or not parsed.netloc:
350
+ raise ValidationError("url must be an absolute HTTP or HTTPS URL")
351
+ if description is not None and len(description) > 500:
352
+ raise ValidationError("description may contain at most 500 characters")
353
+ if events is not None and (not events or any(not event.strip() for event in events)):
354
+ raise ValidationError("events must contain at least one non-empty string")
355
+ data = _body(name=name, url=url, description=description,
356
+ events=list(events) if events is not None else None,
357
+ retry_config=retry_config, signature_config=signature_config)
358
+ if not data:
359
+ raise ValidationError("update_webhook requires at least one editable field")
360
+ path = f"/org/webhooks/{_positive_id(endpoint_id, 'endpoint_id')}"
361
+ return cast(Webhook, self._request("PATCH", path, json=data, project_id=project_id or _UNSET))
362
+
363
+ def delete_webhook(self, endpoint_id: int, *, project_id: Optional[str] = None) -> None:
364
+ path = f"/org/webhooks/{_positive_id(endpoint_id, 'endpoint_id')}"
365
+ self._request("DELETE", path, project_id=project_id or _UNSET)
366
+
367
+ def test_webhook(
368
+ self, endpoint_id: int, *, event_type: str = "memory.created",
369
+ project_id: Optional[str] = None,
370
+ ) -> WebhookTestResponse:
371
+ _required_text(event_type, "event_type")
372
+ path = f"/org/webhooks/{_positive_id(endpoint_id, 'endpoint_id')}/test"
373
+ return cast(WebhookTestResponse, self._request("POST", path, json={"event_type": event_type}, project_id=project_id or _UNSET))
374
+
375
+ def replay_webhook_deliveries(
376
+ self, endpoint_id: int, *, since_minutes: int = 60,
377
+ statuses: Sequence[str] = ("failed", "dead_letter"), limit: int = 100,
378
+ project_id: Optional[str] = None,
379
+ ) -> WebhookReplayResponse:
380
+ _bounded(since_minutes, "since_minutes", 1, 10080)
381
+ _bounded(limit, "limit", 1, 1000)
382
+ if not statuses or any(not isinstance(status, str) or not status.strip() for status in statuses):
383
+ raise ValidationError("statuses must contain at least one non-empty string")
384
+ path = f"/org/webhooks/{_positive_id(endpoint_id, 'endpoint_id')}/replay"
385
+ data = {"since_minutes": since_minutes, "statuses": list(statuses), "limit": limit}
386
+ return cast(WebhookReplayResponse, self._request("POST", path, json=data, project_id=project_id or _UNSET))
387
+
388
+ def list_webhook_deliveries(
389
+ self, endpoint_id: int, *, page: int = 1, page_size: int = 25,
390
+ status: Optional[str] = None, project_id: Optional[str] = None,
391
+ ) -> WebhookDeliveriesResponse:
392
+ _positive_id(page, "page")
393
+ _bounded(page_size, "page_size", 1, 100)
394
+ if status is not None:
395
+ _required_text(status, "status")
396
+ path = f"/org/webhooks/{_positive_id(endpoint_id, 'endpoint_id')}/deliveries"
397
+ params = _body(page=page, page_size=page_size, status_filter=status)
398
+ return cast(WebhookDeliveriesResponse, self._request("GET", path, params=params, project_id=project_id or _UNSET))
399
+
400
+
401
+ class AsyncControlPlaneClient(_ControlPlaneBase):
402
+ """Async-native bearer client for dashboard/control-plane routes."""
403
+
404
+ def __init__(
405
+ self, base_url: str, *, access_token: Optional[str] = None,
406
+ project_id: Optional[str] = None, timeout: float = 30.0,
407
+ transport: Optional[httpx.AsyncBaseTransport] = None,
408
+ ) -> None:
409
+ super().__init__(base_url, access_token, project_id)
410
+ self._http = httpx.AsyncClient(timeout=timeout, transport=transport)
411
+
412
+ async def __aenter__(self) -> "AsyncControlPlaneClient":
413
+ return self
414
+
415
+ async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
416
+ await self.aclose()
417
+
418
+ async def aclose(self) -> None:
419
+ await self._http.aclose()
420
+
421
+ async def _request(
422
+ self, method: str, path: str, *, json: Optional[Dict[str, Any]] = None,
423
+ params: Optional[Dict[str, Any]] = None, require_auth: bool = True,
424
+ project_id: object = _UNSET,
425
+ ) -> Any:
426
+ try:
427
+ response = await self._http.request(
428
+ method, f"{self._base_url}{path}",
429
+ headers=self._headers(require_auth=require_auth, project_id=project_id),
430
+ json=json, params=params,
431
+ )
432
+ except httpx.TimeoutException as exc:
433
+ raise MemorySyncError("Control-plane request timed out") from exc
434
+ except httpx.HTTPError as exc:
435
+ raise MemorySyncError("Control-plane network request failed") from exc
436
+ return self._decode(response)
437
+
438
+ async def bulk_revoke_api_keys(self, key_ids: Sequence[int], *, project_id: Optional[str] = None) -> BulkRevokeResponse:
439
+ ids = list(key_ids)
440
+ if not 1 <= len(ids) <= 100:
441
+ raise ValidationError("key_ids must contain between 1 and 100 entries")
442
+ for key_id in ids:
443
+ _positive_id(key_id, "key_id")
444
+ raw = await self._request("POST", "/org/api-keys/bulk-revoke", json={"key_ids": ids}, project_id=project_id or _UNSET)
445
+ return cast(BulkRevokeResponse, raw)
446
+
447
+ async def test_api_key(self, key_id: int, *, project_id: Optional[str] = None) -> ApiKeyTestResponse:
448
+ raw = await self._request("POST", f"/org/api-keys/{_positive_id(key_id, 'key_id')}/test", project_id=project_id or _UNSET)
449
+ return cast(ApiKeyTestResponse, raw)
450
+
451
+ async def login(self, email: str, password: str) -> LoginResponse:
452
+ _required_text(email, "email")
453
+ if "@" not in email:
454
+ raise ValidationError("email must be a valid email address")
455
+ _required_text(password, "password")
456
+ raw = await self._request("POST", "/auth/login", json={"email": email, "password": password}, require_auth=False, project_id=None)
457
+ return cast(LoginResponse, raw)
458
+
459
+ async def get_current_plan(self) -> CurrentPlanResponse:
460
+ return cast(CurrentPlanResponse, await self._request("GET", "/org/billing/current-plan"))
461
+
462
+ async def list_team_members(self) -> List[TeamMember]:
463
+ return cast(List[TeamMember], await self._request("GET", "/admin/team/members"))
464
+
465
+ async def suspend_team_member(self, member_id: int) -> TeamMember:
466
+ path = f"/admin/team/members/{_positive_id(member_id, 'member_id')}"
467
+ return cast(TeamMember, await self._request("PATCH", path, json={"status": "suspended"}))
468
+
469
+ async def remove_team_member(self, member_id: int) -> None:
470
+ await self._request("DELETE", f"/admin/team/members/{_positive_id(member_id, 'member_id')}")
471
+
472
+ async def list_sessions(self) -> SessionsResponse:
473
+ return cast(SessionsResponse, await self._request("GET", "/auth/sessions"))
474
+
475
+ async def revoke_session(self, session_id: int) -> None:
476
+ await self._request("POST", f"/auth/sessions/{_positive_id(session_id, 'session_id')}/revoke")
477
+
478
+
479
+ async def list_audit_events(
480
+ self, *, limit: Optional[int] = None, cursor: Optional[int] = None,
481
+ skip: Optional[int] = None, sort_direction: Optional[str] = None,
482
+ tenant_id: Optional[str] = None, actor: Optional[str] = None,
483
+ actor_email: Optional[str] = None, ip: Optional[str] = None,
484
+ action: Optional[str] = None, resource_type: Optional[str] = None,
485
+ resource_id: Optional[str] = None, severity: Optional[str] = None,
486
+ category: Optional[str] = None, start: Optional[str] = None,
487
+ end: Optional[str] = None, success: Optional[bool] = None,
488
+ source: Optional[str] = None, ingest_method: Optional[str] = None,
489
+ search: Optional[str] = None, include_stats: Optional[bool] = None,
490
+ ) -> AuditEventsResponse:
491
+ if limit is not None:
492
+ _bounded(limit, "limit", 1, 200)
493
+ if cursor is not None and cursor < 0:
494
+ raise ValidationError("cursor must be non-negative")
495
+ if skip is not None and skip < 0:
496
+ raise ValidationError("skip must be non-negative")
497
+ if sort_direction not in (None, "asc", "desc"):
498
+ raise ValidationError("sort_direction must be 'asc' or 'desc'")
499
+ params = _body(limit=limit, cursor=cursor, skip=skip, sort=sort_direction,
500
+ tenant_id=tenant_id, actor=actor, actor_email=actor_email,
501
+ ip=ip, action=action, resource_type=resource_type,
502
+ resource_id=resource_id, severity=severity, category=category,
503
+ start=start, end=end, success=success, source=source,
504
+ ingest_method=ingest_method, search=search,
505
+ include_stats=include_stats)
506
+ raw = await self._request("GET", "/admin/audit-logs", params=params)
507
+ return cast(AuditEventsResponse, {
508
+ "events": raw.get("logs", []),
509
+ "next_cursor": raw.get("next_cursor"),
510
+ "stats": raw.get("stats"),
511
+ "sort": raw.get("sort", sort_direction or "desc"),
512
+ })
513
+
514
+ async def list_integrations(self, *, category: Optional[str] = None) -> List[Integration]:
515
+ if category is not None:
516
+ _required_text(category, "category")
517
+ raw = await self._request("GET", "/api/v1/integrations/catalog", params=_body(category=category))
518
+ return cast(List[Integration], raw)
519
+
520
+ async def create_organization(self, name: str, *, domain: Optional[str] = None) -> OrganizationMembership:
521
+ _required_text(name, "name")
522
+ if domain is not None:
523
+ _required_text(domain, "domain")
524
+ raw = await self._request("POST", "/organizations", json=_body(name=name, domain=domain))
525
+ return cast(OrganizationMembership, raw)
526
+
527
+ async def list_organizations(self) -> List[OrganizationMembership]:
528
+ return cast(List[OrganizationMembership], await self._request("GET", "/organizations"))
529
+
530
+ async def list_organization_members(self) -> List[TeamMember]:
531
+ return await self.list_team_members()
532
+
533
+ async def get_organization_settings(self, *, tenant_id: Optional[str] = None) -> OrganizationSettings:
534
+ if tenant_id is not None:
535
+ _required_text(tenant_id, "tenant_id")
536
+ raw = await self._request("GET", "/admin/tenant-settings", params=_body(tenant_id=tenant_id))
537
+ return cast(OrganizationSettings, raw)
538
+
539
+ async def list_projects(self) -> List[Project]:
540
+ return cast(List[Project], await self._request("GET", "/org/projects"))
541
+
542
+ async def create_webhook(
543
+ self, name: str, url: str, events: Sequence[str], *,
544
+ description: Optional[str] = None, retry_config: Optional[Dict[str, Any]] = None,
545
+ signature_config: Optional[Dict[str, Any]] = None,
546
+ project_id: Optional[str] = None,
547
+ ) -> Webhook:
548
+ _validate_webhook(name, url, events)
549
+ if description is not None and len(description) > 500:
550
+ raise ValidationError("description may contain at most 500 characters")
551
+ selected = project_id or self._project_id
552
+ data = _body(name=name, url=url, events=list(events), description=description,
553
+ retry_config=retry_config, signature_config=signature_config,
554
+ project_id=project_id)
555
+ raw = await self._request("POST", "/org/webhooks", json=data, project_id=selected)
556
+ return cast(Webhook, raw)
557
+
558
+ async def list_webhooks(self, *, project_id: Optional[str] = None) -> WebhooksResponse:
559
+ return cast(WebhooksResponse, await self._request("GET", "/org/webhooks", project_id=project_id or _UNSET))
560
+
561
+
562
+ async def update_webhook(
563
+ self, endpoint_id: int, *, name: Optional[str] = None,
564
+ url: Optional[str] = None, description: Optional[str] = None,
565
+ events: Optional[Sequence[str]] = None,
566
+ retry_config: Optional[Dict[str, Any]] = None,
567
+ signature_config: Optional[Dict[str, Any]] = None,
568
+ project_id: Optional[str] = None,
569
+ ) -> Webhook:
570
+ if name is not None:
571
+ _required_text(name, "name")
572
+ if len(name) > 128:
573
+ raise ValidationError("name may contain at most 128 characters")
574
+ if url is not None:
575
+ parsed = urlparse(url)
576
+ if parsed.scheme not in ("http", "https") or not parsed.netloc:
577
+ raise ValidationError("url must be an absolute HTTP or HTTPS URL")
578
+ if description is not None and len(description) > 500:
579
+ raise ValidationError("description may contain at most 500 characters")
580
+ if events is not None and (not events or any(not event.strip() for event in events)):
581
+ raise ValidationError("events must contain at least one non-empty string")
582
+ data = _body(name=name, url=url, description=description,
583
+ events=list(events) if events is not None else None,
584
+ retry_config=retry_config, signature_config=signature_config)
585
+ if not data:
586
+ raise ValidationError("update_webhook requires at least one editable field")
587
+ path = f"/org/webhooks/{_positive_id(endpoint_id, 'endpoint_id')}"
588
+ raw = await self._request("PATCH", path, json=data, project_id=project_id or _UNSET)
589
+ return cast(Webhook, raw)
590
+
591
+ async def delete_webhook(self, endpoint_id: int, *, project_id: Optional[str] = None) -> None:
592
+ path = f"/org/webhooks/{_positive_id(endpoint_id, 'endpoint_id')}"
593
+ await self._request("DELETE", path, project_id=project_id or _UNSET)
594
+
595
+ async def test_webhook(
596
+ self, endpoint_id: int, *, event_type: str = "memory.created",
597
+ project_id: Optional[str] = None,
598
+ ) -> WebhookTestResponse:
599
+ _required_text(event_type, "event_type")
600
+ path = f"/org/webhooks/{_positive_id(endpoint_id, 'endpoint_id')}/test"
601
+ raw = await self._request("POST", path, json={"event_type": event_type}, project_id=project_id or _UNSET)
602
+ return cast(WebhookTestResponse, raw)
603
+
604
+ async def replay_webhook_deliveries(
605
+ self, endpoint_id: int, *, since_minutes: int = 60,
606
+ statuses: Sequence[str] = ("failed", "dead_letter"), limit: int = 100,
607
+ project_id: Optional[str] = None,
608
+ ) -> WebhookReplayResponse:
609
+ _bounded(since_minutes, "since_minutes", 1, 10080)
610
+ _bounded(limit, "limit", 1, 1000)
611
+ if not statuses or any(not isinstance(status, str) or not status.strip() for status in statuses):
612
+ raise ValidationError("statuses must contain at least one non-empty string")
613
+ path = f"/org/webhooks/{_positive_id(endpoint_id, 'endpoint_id')}/replay"
614
+ data = {"since_minutes": since_minutes, "statuses": list(statuses), "limit": limit}
615
+ raw = await self._request("POST", path, json=data, project_id=project_id or _UNSET)
616
+ return cast(WebhookReplayResponse, raw)
617
+
618
+ async def list_webhook_deliveries(
619
+ self, endpoint_id: int, *, page: int = 1, page_size: int = 25,
620
+ status: Optional[str] = None, project_id: Optional[str] = None,
621
+ ) -> WebhookDeliveriesResponse:
622
+ _positive_id(page, "page")
623
+ _bounded(page_size, "page_size", 1, 100)
624
+ if status is not None:
625
+ _required_text(status, "status")
626
+ path = f"/org/webhooks/{_positive_id(endpoint_id, 'endpoint_id')}/deliveries"
627
+ params = _body(page=page, page_size=page_size, status_filter=status)
628
+ raw = await self._request("GET", path, params=params, project_id=project_id or _UNSET)
629
+ return cast(WebhookDeliveriesResponse, raw)
@@ -0,0 +1,267 @@
1
+ """Typed request and response shapes for the bearer-authenticated control plane."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Dict, List, Optional, TypedDict
6
+
7
+ JsonObject = Dict[str, Any]
8
+
9
+
10
+ class BulkRevokeResult(TypedDict):
11
+ key_id: int
12
+ status: str
13
+
14
+
15
+ class BulkRevokeResponse(TypedDict):
16
+ revoked: int
17
+ already_revoked: int
18
+ not_found: int
19
+ results: List[BulkRevokeResult]
20
+
21
+
22
+ class ApiKeyTestResponse(TypedDict, total=False):
23
+ key_id: int
24
+ valid: bool
25
+ status: str
26
+ environment: str
27
+ rate_limit_tier: str
28
+ scopes: List[str]
29
+ project_id: Optional[str]
30
+ last_used_at: Optional[str]
31
+ expires_at: Optional[str]
32
+ expired: bool
33
+ server_time: str
34
+
35
+
36
+ class LoginTokens(TypedDict):
37
+ access_token: str
38
+ refresh_token: str
39
+ token_type: str
40
+
41
+
42
+ class LoginSession(TypedDict, total=False):
43
+ user_id: int
44
+ organization_id: int
45
+ role: str
46
+
47
+
48
+ class LoginResponse(TypedDict, total=False):
49
+ tokens: LoginTokens
50
+ session: LoginSession
51
+ mfa_required: bool
52
+ mfa_setup_required: bool
53
+
54
+
55
+ class CurrentPlanResponse(TypedDict, total=False):
56
+ plan: JsonObject
57
+ status: str
58
+ billing_period_start: str
59
+ billing_period_end: str
60
+ payment_failed: bool
61
+ scheduled_plan_id: Optional[str]
62
+ cancel_at_period_end: bool
63
+
64
+
65
+ class TeamMember(TypedDict, total=False):
66
+ id: str
67
+ name: str
68
+ email: str
69
+ role: str
70
+ status: str
71
+ last_active: Optional[str]
72
+ mfa_enabled: bool
73
+ auth_method: str
74
+ login_provider: str
75
+ devices: int
76
+ sessions: int
77
+ is_scim_managed: bool
78
+
79
+
80
+ class Session(TypedDict, total=False):
81
+ id: int
82
+ is_current: bool
83
+ session_type: str
84
+ session_name: str
85
+ user_agent: str
86
+ ip: str
87
+ location: Optional[str]
88
+ geo: Optional[JsonObject]
89
+ created_at: str
90
+ last_activity_at: str
91
+ expires_at: str
92
+
93
+
94
+ class SessionsResponse(TypedDict):
95
+ sessions: List[Session]
96
+ current_session_id: int
97
+
98
+
99
+ class AuditEvent(TypedDict, total=False):
100
+ id: int
101
+ event_id: str
102
+ timestamp: str
103
+ category: str
104
+ actor: JsonObject
105
+ action: str
106
+ resource: JsonObject
107
+ severity: str
108
+ success: bool
109
+
110
+
111
+ class AuditEventsResponse(TypedDict, total=False):
112
+ events: List[AuditEvent]
113
+ next_cursor: Optional[int]
114
+ stats: Optional[JsonObject]
115
+ sort: str
116
+
117
+
118
+ class Integration(TypedDict, total=False):
119
+ id: str
120
+ name: str
121
+ description: str
122
+ category: str
123
+ features: List[str]
124
+ auth_type: str
125
+ is_configured: bool
126
+ is_connected: bool
127
+ coming_soon: bool
128
+
129
+
130
+ class OrganizationMembership(TypedDict):
131
+ user_id: int
132
+ organization_id: int
133
+ role: str
134
+
135
+
136
+ class OrganizationSettings(TypedDict, total=False):
137
+ tenant_id: str
138
+ tenant_name: str
139
+ tenant_slug: str
140
+ description: Optional[str]
141
+ plan: str
142
+ status: str
143
+ created_at: str
144
+ contact_email: str
145
+ custom_domain: Optional[str]
146
+ custom_domain_verified: bool
147
+ logo_url: Optional[str]
148
+ total_memories: int
149
+ active_users: int
150
+ organization_name: str
151
+ scheduled_deletion_at: Optional[str]
152
+ mfa_enrolled: bool
153
+ password_enabled: bool
154
+
155
+
156
+ class Project(TypedDict, total=False):
157
+ id: str
158
+ name: str
159
+ tenant_id: str
160
+ is_default: bool
161
+ memory_count: int
162
+ archived_at: Optional[str]
163
+ created_at: str
164
+ updated_at: str
165
+
166
+
167
+ class Webhook(TypedDict, total=False):
168
+ id: int
169
+ name: str
170
+ url: str
171
+ description: Optional[str]
172
+ secret_prefix: str
173
+ events: List[str]
174
+ enabled: bool
175
+ signature_algorithm: str
176
+ project_id: Optional[str]
177
+ secret: str
178
+ success_rate: float
179
+ updated_at: str
180
+
181
+
182
+ class WebhooksResponse(TypedDict, total=False):
183
+ endpoints: List[Webhook]
184
+ total_endpoints: int
185
+ active_endpoints: int
186
+ total_deliveries: int
187
+ avg_success_rate: float
188
+ failing_endpoints: int
189
+
190
+
191
+ class WebhookTestResponse(TypedDict, total=False):
192
+ delivery_id: int
193
+ event_id: str
194
+ event_type: str
195
+ endpoint_id: int
196
+ status: str
197
+ payload: JsonObject
198
+ message: str
199
+
200
+
201
+ class WebhookReplayResponse(TypedDict):
202
+ endpoint_id: int
203
+ eligible: int
204
+ replayed: int
205
+ skipped: int
206
+ delivery_ids: List[int]
207
+
208
+
209
+ class WebhookDelivery(TypedDict, total=False):
210
+ id: int
211
+ endpoint_id: int
212
+ event_type: str
213
+ event_id: str
214
+ status: str
215
+ payload_hash: str
216
+ status_code: Optional[int]
217
+ latency_ms: Optional[int]
218
+ attempt_number: int
219
+ max_attempts: int
220
+ created_at: str
221
+ completed_at: Optional[str]
222
+
223
+
224
+ class WebhookDeliveriesResponse(TypedDict):
225
+ deliveries: List[WebhookDelivery]
226
+ total: int
227
+ page: int
228
+ page_size: int
229
+
230
+
231
+ class CreateWebhookRequest(TypedDict):
232
+ name: str
233
+ url: str
234
+ events: List[str]
235
+
236
+
237
+ class UpdateWebhookRequest(TypedDict, total=False):
238
+ name: str
239
+ url: str
240
+ description: Optional[str]
241
+ events: List[str]
242
+ retry_config: JsonObject
243
+ signature_config: JsonObject
244
+
245
+
246
+ class BulkRevokeRequest(TypedDict):
247
+ key_ids: List[int]
248
+
249
+
250
+ class LoginRequest(TypedDict):
251
+ email: str
252
+ password: str
253
+
254
+
255
+ class CreateOrganizationRequest(TypedDict, total=False):
256
+ name: str
257
+ domain: str
258
+
259
+
260
+ class TestWebhookRequest(TypedDict):
261
+ event_type: str
262
+
263
+
264
+ class ReplayWebhookDeliveriesRequest(TypedDict):
265
+ since_minutes: int
266
+ statuses: List[str]
267
+ limit: int
@@ -1,15 +0,0 @@
1
- *.iml
2
- .gradle
3
- /local.properties
4
- /.idea/caches
5
- /.idea/libraries
6
- /.idea/modules.xml
7
- /.idea/workspace.xml
8
- /.idea/navEditor.xml
9
- /.idea/assetWizardSettings.xml
10
- .DS_Store
11
- /build
12
- /captures
13
- .externalNativeBuild
14
- .cxx
15
- local.properties
@@ -1,46 +0,0 @@
1
- """Public package surface for the MemorySync Python SDK."""
2
-
3
- from ._version import __version__
4
- from .errors import (
5
- AuthError,
6
- MemorySyncError,
7
- NotFoundError,
8
- RateLimitError,
9
- ServerError,
10
- ValidationError,
11
- )
12
- from .types import (
13
- AddSkippedResponse,
14
- BulkAddItem,
15
- BulkAddItemResult,
16
- BulkAddResponse,
17
- ComposeResponse,
18
- ExportResponse,
19
- Memory,
20
- QueryResponse,
21
- Relation,
22
- RelationshipType,
23
- )
24
- from .client import AsyncMemorySyncClient, MemorySyncClient
25
-
26
- __all__ = [
27
- "__version__",
28
- "MemorySyncClient",
29
- "AsyncMemorySyncClient",
30
- "Memory",
31
- "QueryResponse",
32
- "BulkAddItem",
33
- "BulkAddItemResult",
34
- "BulkAddResponse",
35
- "AddSkippedResponse",
36
- "ComposeResponse",
37
- "ExportResponse",
38
- "Relation",
39
- "RelationshipType",
40
- "MemorySyncError",
41
- "AuthError",
42
- "ValidationError",
43
- "NotFoundError",
44
- "RateLimitError",
45
- "ServerError",
46
- ]
@@ -1 +0,0 @@
1
- __version__ = "1.0.0"
File without changes