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.
- memorysync-1.1.0/.gitignore +48 -0
- {memorysync-1.0.0 → memorysync-1.1.0}/PKG-INFO +59 -4
- {memorysync-1.0.0 → memorysync-1.1.0}/README.md +56 -1
- {memorysync-1.0.0 → memorysync-1.1.0}/pyproject.toml +3 -3
- memorysync-1.1.0/src/memorysync/__init__.py +94 -0
- memorysync-1.1.0/src/memorysync/_version.py +1 -0
- memorysync-1.1.0/src/memorysync/control_plane.py +629 -0
- memorysync-1.1.0/src/memorysync/control_plane_types.py +267 -0
- memorysync-1.0.0/.gitignore +0 -15
- memorysync-1.0.0/src/memorysync/__init__.py +0 -46
- memorysync-1.0.0/src/memorysync/_version.py +0 -1
- {memorysync-1.0.0 → memorysync-1.1.0}/LICENSE +0 -0
- {memorysync-1.0.0 → memorysync-1.1.0}/src/memorysync/client.py +0 -0
- {memorysync-1.0.0 → memorysync-1.1.0}/src/memorysync/errors.py +0 -0
- {memorysync-1.0.0 → memorysync-1.1.0}/src/memorysync/types.py +0 -0
|
@@ -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.
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Official Python client for the MemorySync API.
|
|
5
|
-
Project-URL: Homepage, https://memorysync.
|
|
6
|
-
Project-URL: Documentation, https://memorysync.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
33
|
-
Documentation = "https://memorysync.
|
|
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
|
memorysync-1.0.0/.gitignore
DELETED
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|