memorysync 1.3.0__tar.gz → 1.5.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.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: memorysync
3
- Version: 1.3.0
3
+ Version: 1.5.0
4
4
  Summary: Official Python client for the MemorySync API.
5
5
  Project-URL: Homepage, https://memorysync.io
6
6
  Project-URL: Documentation, https://docs.memorysync.io
@@ -1,44 +1,44 @@
1
- [build-system]
2
- requires = ["hatchling"]
3
- build-backend = "hatchling.build"
4
-
5
- [project]
6
- name = "memorysync"
7
- version = "1.3.0"
8
- description = "Official Python client for the MemorySync API."
9
- readme = "README.md"
10
- license = { text = "MIT" }
11
- requires-python = ">=3.9"
12
- authors = [{ name = "MemorySync" }]
13
- keywords = ["memorysync", "memory", "sdk", "client"]
14
- classifiers = [
15
- "Development Status :: 5 - Production/Stable",
16
- "Intended Audience :: Developers",
17
- "License :: OSI Approved :: MIT License",
18
- "Programming Language :: Python :: 3",
19
- "Programming Language :: Python :: 3.9",
20
- "Programming Language :: Python :: 3.10",
21
- "Programming Language :: Python :: 3.11",
22
- "Programming Language :: Python :: 3.12",
23
- "Topic :: Software Development :: Libraries :: Python Modules",
24
- "Typing :: Typed",
25
- ]
26
- dependencies = [
27
- "httpx>=0.25,<1.0",
28
- "typing-extensions>=4.5; python_version < '3.11'",
29
- ]
30
-
31
- [project.urls]
32
- Homepage = "https://memorysync.io"
33
- Documentation = "https://docs.memorysync.io"
34
-
35
- [tool.hatch.build.targets.wheel]
36
- packages = ["src/memorysync"]
37
-
38
- [tool.hatch.build.targets.sdist]
39
- include = [
40
- "src/memorysync",
41
- "README.md",
42
- "LICENSE",
43
- "pyproject.toml",
44
- ]
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "memorysync"
7
+ version = "1.5.0"
8
+ description = "Official Python client for the MemorySync API."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.9"
12
+ authors = [{ name = "MemorySync" }]
13
+ keywords = ["memorysync", "memory", "sdk", "client"]
14
+ classifiers = [
15
+ "Development Status :: 5 - Production/Stable",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.9",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Topic :: Software Development :: Libraries :: Python Modules",
24
+ "Typing :: Typed",
25
+ ]
26
+ dependencies = [
27
+ "httpx>=0.25,<1.0",
28
+ "typing-extensions>=4.5; python_version < '3.11'",
29
+ ]
30
+
31
+ [project.urls]
32
+ Homepage = "https://memorysync.io"
33
+ Documentation = "https://docs.memorysync.io"
34
+
35
+ [tool.hatch.build.targets.wheel]
36
+ packages = ["src/memorysync"]
37
+
38
+ [tool.hatch.build.targets.sdist]
39
+ include = [
40
+ "src/memorysync",
41
+ "README.md",
42
+ "LICENSE",
43
+ "pyproject.toml",
44
+ ]
@@ -37,14 +37,20 @@ from .client import AsyncMemorySyncClient, MemorySyncClient
37
37
  from .control_plane import AsyncControlPlaneClient, ControlPlaneClient
38
38
  from .control_plane_types import (
39
39
  ApiKeyTestResponse,
40
- AuditEvent,
41
- AuditEventsResponse,
42
40
  BulkRevokeRequest,
43
41
  BulkRevokeResponse,
44
42
  BulkRevokeResult,
43
+ CreatedWebhook,
44
+ CreateExportRequest,
45
+ CreateExportResponse,
45
46
  CreateOrganizationRequest,
47
+ CreateProjectRequest,
46
48
  CreateWebhookRequest,
47
49
  CurrentPlanResponse,
50
+ ExportDownloadUrl,
51
+ ExportFilters,
52
+ ExportJob,
53
+ ExportJobListResponse,
48
54
  Integration,
49
55
  JsonObject,
50
56
  LoginRequest,
@@ -52,17 +58,18 @@ from .control_plane_types import (
52
58
  LoginSession,
53
59
  LoginTokens,
54
60
  OrganizationMembership,
55
- OrganizationSettings,
56
61
  Project,
62
+ RenameProjectRequest,
57
63
  ReplayWebhookDeliveriesRequest,
58
64
  Session,
59
65
  SessionsResponse,
60
- TeamMember,
61
66
  TestWebhookRequest,
62
67
  UpdateWebhookRequest,
63
68
  Webhook,
64
69
  WebhookDeliveriesResponse,
65
70
  WebhookDelivery,
71
+ WebhookEventTypesResponse,
72
+ WebhookHealth,
66
73
  WebhookReplayResponse,
67
74
  WebhooksResponse,
68
75
  WebhookTestResponse,
@@ -108,12 +115,14 @@ __all__ += [
108
115
  "ControlPlaneClient", "AsyncControlPlaneClient", "JsonObject",
109
116
  "BulkRevokeRequest", "BulkRevokeResult", "BulkRevokeResponse",
110
117
  "ApiKeyTestResponse", "LoginRequest", "LoginTokens", "LoginSession",
111
- "LoginResponse", "CurrentPlanResponse", "TeamMember", "Session",
112
- "SessionsResponse", "AuditEvent", "AuditEventsResponse", "Integration",
113
- "CreateOrganizationRequest", "OrganizationMembership",
114
- "OrganizationSettings", "Project", "CreateWebhookRequest",
118
+ "LoginResponse", "CurrentPlanResponse", "Session", "SessionsResponse",
119
+ "Integration", "CreateOrganizationRequest", "OrganizationMembership",
120
+ "Project", "CreateProjectRequest",
121
+ "RenameProjectRequest", "CreateWebhookRequest", "CreatedWebhook",
115
122
  "UpdateWebhookRequest", "TestWebhookRequest",
116
123
  "ReplayWebhookDeliveriesRequest", "Webhook", "WebhooksResponse",
117
124
  "WebhookTestResponse", "WebhookReplayResponse", "WebhookDelivery",
118
- "WebhookDeliveriesResponse",
125
+ "WebhookDeliveriesResponse", "WebhookEventTypesResponse", "WebhookHealth",
126
+ "ExportFilters", "CreateExportRequest", "ExportJob",
127
+ "ExportJobListResponse", "CreateExportResponse", "ExportDownloadUrl",
119
128
  ]
@@ -0,0 +1 @@
1
+ __version__ = "1.5.0"
@@ -38,7 +38,7 @@ shapes (memories, history, feedback, ontology).
38
38
 
39
39
  from __future__ import annotations
40
40
 
41
- from typing import Any, Callable, Dict, Optional, Sequence
41
+ from typing import Any, Callable, Dict, Mapping, Optional, Sequence
42
42
  from urllib.parse import quote
43
43
 
44
44
  _V2 = "/api/v2/integrations"
@@ -58,6 +58,20 @@ def _seg(value: Any) -> str:
58
58
  return quote(str(value), safe="")
59
59
 
60
60
 
61
+ def _as_item(value: Any, key: str) -> Dict[str, Any]:
62
+ """Normalize one selection entry into the object the API expects.
63
+
64
+ Two of the approval endpoints take a list of objects rather than a list of
65
+ ids — Slack channels carry an optional name and type, S3 prefixes carry an
66
+ optional bucket and label. Callers almost always have just the id, so both
67
+ forms are accepted here and widened to the object form, rather than making
68
+ every caller write ``{"id": channel_id}`` and making the simple case ugly.
69
+ """
70
+ if isinstance(value, Mapping):
71
+ return dict(value)
72
+ return {key: value}
73
+
74
+
61
75
  class _Namespace:
62
76
  """Holds the bound request function. Subclasses add methods."""
63
77
 
@@ -97,11 +111,22 @@ class SlackNamespace(_Namespace):
97
111
  "GET", f"{_V2}/connections/{_seg(connection_id)}/slack/channels"
98
112
  )
99
113
 
100
- def add_channels(self, connection_id: str, channel_ids: Sequence[str]) -> Any:
101
- """Select channels for syncing."""
114
+ def add_channels(self, connection_id: str, channels: Sequence[Any]) -> Any:
115
+ """Select channels for syncing.
116
+
117
+ Accepts bare channel ids, which is the common case::
118
+
119
+ client.connections.slack.add_channels("c1", ["C0123", "C0456"])
120
+
121
+ or dicts when you want to carry the name and type across so the server
122
+ does not have to look them up again::
123
+
124
+ add_channels("c1", [{"id": "C0123", "name": "support",
125
+ "is_private": False}])
126
+ """
102
127
  return self._request(
103
128
  "POST", f"{_V2}/connections/{_seg(connection_id)}/slack/channels",
104
- json={"channel_ids": list(channel_ids)},
129
+ json={"channels": [_as_item(c, "id") for c in channels]},
105
130
  )
106
131
 
107
132
  def remove_channel(self, connection_id: str, channel_id: str) -> Any:
@@ -195,18 +220,47 @@ class S3Namespace(_Namespace):
195
220
  "GET", f"{_V2}/connections/{_seg(connection_id)}/s3/prefixes"
196
221
  )
197
222
 
198
- def add_prefixes(self, connection_id: str, prefixes: Sequence[str]) -> Any:
199
- """Select prefixes for syncing."""
223
+ def add_prefixes(self, connection_id: str, prefixes: Sequence[Any]) -> Any:
224
+ """Select prefixes for syncing.
225
+
226
+ Accepts bare prefixes, or dicts carrying ``bucket`` and ``label``::
227
+
228
+ add_prefixes("c1", ["handbook/", "policies/"])
229
+ add_prefixes("c1", [{"prefix": "handbook/", "label": "Handbook"}])
230
+
231
+ An empty string means the bucket root. The bucket defaults to the one the
232
+ connection's credentials were validated against, and the API rejects any
233
+ other bucket rather than indexing one nobody proved access to.
234
+ """
200
235
  return self._request(
201
236
  "POST", f"{_V2}/connections/{_seg(connection_id)}/s3/prefixes",
202
- json={"prefixes": list(prefixes)},
203
- )
204
-
205
- def remove_prefixes(self, connection_id: str, prefixes: Sequence[str]) -> Any:
206
- """Stop syncing the given prefixes."""
237
+ json={"prefixes": [_as_item(p, "prefix") for p in prefixes]},
238
+ )
239
+
240
+ def remove_prefix(
241
+ self,
242
+ connection_id: str,
243
+ prefix: str = "",
244
+ *,
245
+ bucket: Optional[str] = None,
246
+ purge: bool = False,
247
+ ) -> Any:
248
+ """Revoke one prefix approval, optionally purging what it produced.
249
+
250
+ The prefix travels as a query parameter, not in the body and not as a path
251
+ segment: prefixes contain slashes, which a path segment cannot carry
252
+ unambiguously, and this endpoint reads no body at all.
253
+
254
+ Pass ``purge=True`` to also delete the memories already derived from the
255
+ prefix. The default leaves them in place, so revoking an approval does not
256
+ silently destroy knowledge.
257
+ """
258
+ params: Dict[str, Any] = {"prefix": prefix, "purge": purge}
259
+ if bucket is not None:
260
+ params["bucket"] = bucket
207
261
  return self._request(
208
262
  "DELETE", f"{_V2}/connections/{_seg(connection_id)}/s3/prefixes",
209
- json={"prefixes": list(prefixes)},
263
+ params=params,
210
264
  )
211
265
 
212
266
  def exclusion_policy(self, connection_id: str) -> Any:
@@ -286,10 +340,15 @@ class GranolaNamespace(_Namespace):
286
340
  )
287
341
 
288
342
  def relink_identity(self, connection_id: str, **body: Any) -> Any:
289
- """Move an existing mapping to a different end user."""
343
+ """Re-run identity matching for this connection.
344
+
345
+ Takes no arguments: the route reads no body and re-matches the whole
346
+ roster. ``**body`` is kept only so a forward-compatible field can be
347
+ passed once the route grows one.
348
+ """
290
349
  return self._request(
291
350
  "POST", f"{_V2}/connections/{_seg(connection_id)}/granola/identities/relink",
292
- json=body,
351
+ json=body or None,
293
352
  )
294
353
 
295
354
  def settings(self, connection_id: str) -> Any:
@@ -321,7 +380,7 @@ class ConnectionOAuthNamespace(_Namespace):
321
380
  platform back. Poll :meth:`status` to find out how it went — the callback
322
381
  does not come to your backend.
323
382
  """
324
- payload = {"provider": provider}
383
+ payload = {"provider_id": provider}
325
384
  payload.update(body)
326
385
  return self._request("POST", f"{_V2}/oauth/initiate", json=payload)
327
386
 
@@ -361,14 +420,18 @@ class ConnectionsNamespace(_Namespace):
361
420
  return self._request("GET", f"{_V2}/connections/{_seg(connection_id)}")
362
421
 
363
422
  def create_with_api_key(self, provider: str, api_key: str, **body: Any) -> Any:
364
- """Connect a provider that authenticates with an API key or bot token."""
365
- payload: Dict[str, Any] = {"provider": provider, "api_key": api_key}
423
+ """Connect a provider that authenticates with an API key or bot token.
424
+
425
+ The wire field is ``provider_id``; the argument is named ``provider``
426
+ because that is what the rest of this namespace calls it.
427
+ """
428
+ payload: Dict[str, Any] = {"provider_id": provider, "api_key": api_key}
366
429
  payload.update(body)
367
430
  return self._request("POST", f"{_V2}/connections/api-key", json=payload)
368
431
 
369
432
  def create_with_credentials(self, provider: str, credentials: Dict[str, Any], **body: Any) -> Any:
370
433
  """Connect a provider that needs a credential bundle, such as S3 keys."""
371
- payload: Dict[str, Any] = {"provider": provider, "credentials": credentials}
434
+ payload: Dict[str, Any] = {"provider_id": provider, "credentials": credentials}
372
435
  payload.update(body)
373
436
  return self._request("POST", f"{_V2}/connections/credentials", json=payload)
374
437
 
@@ -6,16 +6,17 @@ import re
6
6
  from datetime import datetime, timezone
7
7
  from email.utils import parsedate_to_datetime
8
8
  from typing import Any, Dict, List, Mapping, Optional, Sequence, TypeVar, cast
9
- from urllib.parse import urlparse
9
+ from urllib.parse import quote, urlparse
10
10
 
11
11
  import httpx
12
12
 
13
13
  from ._version import __version__
14
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,
15
+ ApiKeyTestResponse, BulkRevokeResponse, CreatedWebhook, CreateExportResponse,
16
+ CurrentPlanResponse, ExportDownloadUrl, ExportJob, ExportJobListResponse,
17
+ Integration, LoginResponse, OrganizationMembership, Project, Session,
18
+ SessionsResponse, Webhook, WebhookDeliveriesResponse, WebhookDelivery,
19
+ WebhookEventTypesResponse, WebhookHealth, WebhookReplayResponse,
19
20
  WebhooksResponse, WebhookTestResponse,
20
21
  )
21
22
  from .errors import (
@@ -200,6 +201,93 @@ def _validate_webhook(name: str, url: str, events: Sequence[str]) -> None:
200
201
  _string_sequence(events, "events")
201
202
 
202
203
 
204
+ def _project_name(value: str) -> str:
205
+ _required_text(value, "name")
206
+ if len(value) > 200:
207
+ raise ValidationError("name may contain at most 200 characters")
208
+ return value
209
+
210
+
211
+ def _webhook_retry_config(value: Optional[Mapping[str, Any]]) -> Optional[Dict[str, Any]]:
212
+ if value is None:
213
+ return None
214
+ if not isinstance(value, Mapping):
215
+ raise ValidationError("retry_config must be a mapping")
216
+ allowed = {
217
+ "enabled", "max_retries", "initial_delay_seconds", "max_delay_seconds",
218
+ "backoff_multiplier", "retry_status_codes",
219
+ }
220
+ unknown = set(value) - allowed
221
+ if unknown:
222
+ raise ValidationError(f"retry_config contains unsupported fields: {sorted(unknown)}")
223
+ result = dict(value)
224
+ if "enabled" in result and not isinstance(result["enabled"], bool):
225
+ raise ValidationError("retry_config.enabled must be a boolean")
226
+ for key, minimum, maximum in (
227
+ ("max_retries", 1, 10), ("initial_delay_seconds", 1, 60),
228
+ ("max_delay_seconds", 60, 86400),
229
+ ):
230
+ if key in result:
231
+ _bounded(result[key], f"retry_config.{key}", minimum, maximum)
232
+ if "backoff_multiplier" in result:
233
+ multiplier = result["backoff_multiplier"]
234
+ if (not isinstance(multiplier, (int, float)) or isinstance(multiplier, bool)
235
+ or not 1.0 <= multiplier <= 5.0):
236
+ raise ValidationError("retry_config.backoff_multiplier must be between 1 and 5")
237
+ if "retry_status_codes" in result:
238
+ _string_sequence(result["retry_status_codes"], "retry_config.retry_status_codes")
239
+ return result
240
+
241
+
242
+ def _webhook_signature_config(value: Optional[Mapping[str, Any]]) -> Optional[Dict[str, Any]]:
243
+ if value is None:
244
+ return None
245
+ if not isinstance(value, Mapping):
246
+ raise ValidationError("signature_config must be a mapping")
247
+ allowed = {"algorithm", "header_name", "timestamp_header", "tolerance_seconds"}
248
+ unknown = set(value) - allowed
249
+ if unknown:
250
+ raise ValidationError(f"signature_config contains unsupported fields: {sorted(unknown)}")
251
+ result = dict(value)
252
+ if "algorithm" in result and result["algorithm"] not in ("hmac-sha256", "hmac-sha512"):
253
+ raise ValidationError("signature_config.algorithm must be 'hmac-sha256' or 'hmac-sha512'")
254
+ for key in ("header_name", "timestamp_header"):
255
+ if key in result:
256
+ _required_text(result[key], f"signature_config.{key}")
257
+ if len(result[key]) > 64:
258
+ raise ValidationError(f"signature_config.{key} may contain at most 64 characters")
259
+ if "tolerance_seconds" in result:
260
+ _bounded(result["tolerance_seconds"], "signature_config.tolerance_seconds", 60, 3600)
261
+ return result
262
+
263
+
264
+ def _path_segment(value: str, name: str) -> str:
265
+ """Validate and percent-encode an opaque identifier as one URL segment."""
266
+ return quote(_required_text(value, name), safe="")
267
+
268
+
269
+ def _export_job_id(value: str) -> str:
270
+ _required_text(value, "job_id")
271
+ if not 8 <= len(value) <= 64:
272
+ raise ValidationError("job_id must contain between 8 and 64 characters")
273
+ return quote(value, safe="")
274
+
275
+
276
+ def _export_filters(value: Optional[Mapping[str, Any]]) -> Optional[Dict[str, Any]]:
277
+ if value is None:
278
+ return None
279
+ if not isinstance(value, Mapping):
280
+ raise ValidationError("filters must be a mapping")
281
+ allowed = {
282
+ "q", "user_id", "is_summary", "tier", "source", "time_range",
283
+ "date_from", "date_to", "include_soft_deleted",
284
+ }
285
+ unknown = set(value) - allowed
286
+ if unknown:
287
+ raise ValidationError(f"filters contains unsupported fields: {sorted(unknown)}")
288
+ return dict(value)
289
+
290
+
203
291
  class ControlPlaneClient(_ControlPlaneBase):
204
292
  """Synchronous bearer client for dashboard/control-plane routes."""
205
293
 
@@ -262,59 +350,12 @@ class ControlPlaneClient(_ControlPlaneBase):
262
350
  def get_current_plan(self) -> CurrentPlanResponse:
263
351
  return cast(CurrentPlanResponse, self._request("GET", "/org/billing/current-plan"))
264
352
 
265
- def list_team_members(self) -> List[TeamMember]:
266
- return cast(List[TeamMember], self._request("GET", "/admin/team/members"))
267
-
268
- def suspend_team_member(self, member_id: int) -> TeamMember:
269
- path = f"/admin/team/members/{_positive_id(member_id, 'member_id')}"
270
- return cast(TeamMember, self._request("PATCH", path, json={"status": "suspended"}))
271
-
272
- def remove_team_member(self, member_id: int) -> None:
273
- path = f"/admin/team/members/{_positive_id(member_id, 'member_id')}"
274
- self._request("DELETE", path)
275
-
276
353
  def list_sessions(self) -> SessionsResponse:
277
354
  return cast(SessionsResponse, self._request("GET", "/auth/sessions"))
278
355
 
279
356
  def revoke_session(self, session_id: int) -> None:
280
357
  self._request("POST", f"/auth/sessions/{_positive_id(session_id, 'session_id')}/revoke")
281
358
 
282
-
283
- def list_audit_events(
284
- self, *, limit: Optional[int] = None, cursor: Optional[int] = None,
285
- skip: Optional[int] = None, sort_direction: Optional[str] = None,
286
- tenant_id: Optional[str] = None, actor: Optional[str] = None,
287
- actor_email: Optional[str] = None, ip: Optional[str] = None,
288
- action: Optional[str] = None, resource_type: Optional[str] = None,
289
- resource_id: Optional[str] = None, severity: Optional[str] = None,
290
- category: Optional[str] = None, start: Optional[str] = None,
291
- end: Optional[str] = None, success: Optional[bool] = None,
292
- source: Optional[str] = None, ingest_method: Optional[str] = None,
293
- search: Optional[str] = None, include_stats: Optional[bool] = None,
294
- ) -> AuditEventsResponse:
295
- if limit is not None:
296
- _bounded(limit, "limit", 1, 200)
297
- if cursor is not None and cursor < 0:
298
- raise ValidationError("cursor must be non-negative")
299
- if skip is not None and skip < 0:
300
- raise ValidationError("skip must be non-negative")
301
- if sort_direction not in (None, "asc", "desc"):
302
- raise ValidationError("sort_direction must be 'asc' or 'desc'")
303
- params = _body(limit=limit, cursor=cursor, skip=skip, sort=sort_direction,
304
- tenant_id=tenant_id, actor=actor, actor_email=actor_email,
305
- ip=ip, action=action, resource_type=resource_type,
306
- resource_id=resource_id, severity=severity, category=category,
307
- start=start, end=end, success=success, source=source,
308
- ingest_method=ingest_method, search=search,
309
- include_stats=include_stats)
310
- raw = self._request("GET", "/admin/audit-logs", params=params)
311
- return cast(AuditEventsResponse, {
312
- "events": raw.get("logs", []),
313
- "next_cursor": raw.get("next_cursor"),
314
- "stats": raw.get("stats"),
315
- "sort": raw.get("sort", sort_direction or "desc"),
316
- })
317
-
318
359
  def list_integrations(self, *, category: Optional[str] = None) -> List[Integration]:
319
360
  if category is not None:
320
361
  _required_text(category, "category")
@@ -329,41 +370,63 @@ class ControlPlaneClient(_ControlPlaneBase):
329
370
  def list_organizations(self) -> List[OrganizationMembership]:
330
371
  return cast(List[OrganizationMembership], self._request("GET", "/organizations"))
331
372
 
332
- def list_organization_members(self) -> List[TeamMember]:
333
- return self.list_team_members()
334
-
335
- def get_organization_settings(self, *, tenant_id: Optional[str] = None) -> OrganizationSettings:
336
- if tenant_id is not None:
337
- _required_text(tenant_id, "tenant_id")
338
- return cast(OrganizationSettings, self._request("GET", "/admin/tenant-settings", params=_body(tenant_id=tenant_id)))
339
-
340
373
  def list_projects(self) -> List[Project]:
341
374
  return cast(List[Project], self._request("GET", "/org/projects"))
342
375
 
376
+ def create_project(self, name: str) -> Project:
377
+ return cast(Project, self._request("POST", "/org/projects", json={"name": _project_name(name)}))
378
+
379
+ def rename_project(self, project_id: str, name: str) -> Project:
380
+ path = f"/org/projects/{_path_segment(project_id, 'project_id')}"
381
+ return cast(Project, self._request("PATCH", path, json={"name": _project_name(name)}))
382
+
383
+ def archive_project(self, project_id: str) -> Project:
384
+ path = f"/org/projects/{_path_segment(project_id, 'project_id')}/archive"
385
+ return cast(Project, self._request("POST", path))
386
+
387
+ def unarchive_project(self, project_id: str) -> Project:
388
+ path = f"/org/projects/{_path_segment(project_id, 'project_id')}/unarchive"
389
+ return cast(Project, self._request("POST", path))
390
+
391
+ def delete_project(self, project_id: str) -> None:
392
+ self._request("DELETE", f"/org/projects/{_path_segment(project_id, 'project_id')}")
393
+
343
394
  def create_webhook(
344
395
  self, name: str, url: str, events: Sequence[str], *,
345
- description: Optional[str] = None, retry_config: Optional[Dict[str, Any]] = None,
346
- signature_config: Optional[Dict[str, Any]] = None,
396
+ description: Optional[str] = None, retry_config: Optional[Mapping[str, Any]] = None,
397
+ signature_config: Optional[Mapping[str, Any]] = None,
347
398
  project_id: Optional[str] = None,
348
- ) -> Webhook:
399
+ ) -> CreatedWebhook:
349
400
  _validate_webhook(name, url, events)
350
401
  if description is not None and len(description) > 500:
351
402
  raise ValidationError("description may contain at most 500 characters")
352
- data = _body(name=name, url=url, events=list(events), description=description,
353
- retry_config=retry_config, signature_config=signature_config,
354
- project_id=project_id)
355
- return cast(Webhook, self._request("POST", "/org/webhooks", json=data, project_id=_project_override(project_id)))
403
+ data = _body(
404
+ name=name, url=url, events=list(events), description=description,
405
+ retry_config=_webhook_retry_config(retry_config),
406
+ signature_config=_webhook_signature_config(signature_config),
407
+ project_id=project_id,
408
+ )
409
+ return cast(CreatedWebhook, self._request("POST", "/org/webhooks", json=data, project_id=_project_override(project_id)))
356
410
 
357
411
  def list_webhooks(self, *, project_id: Optional[str] = None) -> WebhooksResponse:
358
412
  return cast(WebhooksResponse, self._request("GET", "/org/webhooks", project_id=_project_override(project_id)))
359
413
 
414
+ def get_webhook(self, endpoint_id: int, *, project_id: Optional[str] = None) -> Webhook:
415
+ path = f"/org/webhooks/{_positive_id(endpoint_id, 'endpoint_id')}"
416
+ return cast(Webhook, self._request("GET", path, project_id=_project_override(project_id)))
417
+
418
+ def get_webhook_event_types(self, *, project_id: Optional[str] = None) -> WebhookEventTypesResponse:
419
+ return cast(WebhookEventTypesResponse, self._request("GET", "/org/webhooks/event-types", project_id=_project_override(project_id)))
420
+
421
+ def get_webhook_health(self, *, project_id: Optional[str] = None) -> WebhookHealth:
422
+ return cast(WebhookHealth, self._request("GET", "/org/webhooks/health", project_id=_project_override(project_id)))
360
423
 
361
424
  def update_webhook(
362
425
  self, endpoint_id: int, *, name: Optional[str] = None,
363
426
  url: Optional[str] = None, description: Optional[str] = None,
364
427
  events: Optional[Sequence[str]] = None,
365
- retry_config: Optional[Dict[str, Any]] = None,
366
- signature_config: Optional[Dict[str, Any]] = None,
428
+ retry_config: Optional[Mapping[str, Any]] = None,
429
+ signature_config: Optional[Mapping[str, Any]] = None,
367
430
  project_id: Optional[str] = None,
368
431
  ) -> Webhook:
369
432
  if name is not None:
@@ -376,9 +439,12 @@ class ControlPlaneClient(_ControlPlaneBase):
376
439
  raise ValidationError("description may contain at most 500 characters")
377
440
  if events is not None:
378
441
  _string_sequence(events, "events")
379
- data = _body(name=name, url=url, description=description,
380
- events=list(events) if events is not None else None,
381
- retry_config=retry_config, signature_config=signature_config)
442
+ data = _body(
443
+ name=name, url=url, description=description,
444
+ events=list(events) if events is not None else None,
445
+ retry_config=_webhook_retry_config(retry_config),
446
+ signature_config=_webhook_signature_config(signature_config),
447
+ )
382
448
  if not data:
383
449
  raise ValidationError("update_webhook requires at least one editable field")
384
450
  path = f"/org/webhooks/{_positive_id(endpoint_id, 'endpoint_id')}"
@@ -388,6 +454,18 @@ class ControlPlaneClient(_ControlPlaneBase):
388
454
  path = f"/org/webhooks/{_positive_id(endpoint_id, 'endpoint_id')}"
389
455
  self._request("DELETE", path, project_id=_project_override(project_id))
390
456
 
457
+ def pause_webhook(self, endpoint_id: int, *, project_id: Optional[str] = None) -> Webhook:
458
+ path = f"/org/webhooks/{_positive_id(endpoint_id, 'endpoint_id')}/pause"
459
+ return cast(Webhook, self._request("POST", path, project_id=_project_override(project_id)))
460
+
461
+ def resume_webhook(self, endpoint_id: int, *, project_id: Optional[str] = None) -> Webhook:
462
+ path = f"/org/webhooks/{_positive_id(endpoint_id, 'endpoint_id')}/resume"
463
+ return cast(Webhook, self._request("POST", path, project_id=_project_override(project_id)))
464
+
465
+ def rotate_webhook_secret(self, endpoint_id: int, *, project_id: Optional[str] = None) -> CreatedWebhook:
466
+ path = f"/org/webhooks/{_positive_id(endpoint_id, 'endpoint_id')}/rotate-secret"
467
+ return cast(CreatedWebhook, self._request("POST", path, project_id=_project_override(project_id)))
468
+
391
469
  def test_webhook(
392
470
  self, endpoint_id: int, *, event_type: str = "memory.created",
393
471
  project_id: Optional[str] = None,
@@ -408,8 +486,14 @@ class ControlPlaneClient(_ControlPlaneBase):
408
486
  data = {"since_minutes": since_minutes, "statuses": list(statuses), "limit": limit}
409
487
  return cast(WebhookReplayResponse, self._request("POST", path, json=data, project_id=_project_override(project_id)))
410
488
 
489
+ def get_latest_webhook_delivery(
490
+ self, endpoint_id: int, *, project_id: Optional[str] = None,
491
+ ) -> Optional[WebhookDelivery]:
492
+ path = f"/org/webhooks/{_positive_id(endpoint_id, 'endpoint_id')}/deliveries/latest"
493
+ return cast(Optional[WebhookDelivery], self._request("GET", path, project_id=_project_override(project_id)))
494
+
411
495
  def list_webhook_deliveries(
412
- self, endpoint_id: int, *, page: int = 1, page_size: int = 25,
496
+ self, endpoint_id: int, *, page: int = 1, page_size: int = 20,
413
497
  status: Optional[str] = None, project_id: Optional[str] = None,
414
498
  ) -> WebhookDeliveriesResponse:
415
499
  _positive_id(page, "page")
@@ -420,6 +504,60 @@ class ControlPlaneClient(_ControlPlaneBase):
420
504
  params = _body(page=page, page_size=page_size, status_filter=status)
421
505
  return cast(WebhookDeliveriesResponse, self._request("GET", path, params=params, project_id=_project_override(project_id)))
422
506
 
507
+ def list_recent_webhook_deliveries(
508
+ self, *, page: int = 1, page_size: int = 20,
509
+ endpoint_id: Optional[int] = None, status: Optional[str] = None,
510
+ project_id: Optional[str] = None,
511
+ ) -> WebhookDeliveriesResponse:
512
+ _positive_id(page, "page")
513
+ _bounded(page_size, "page_size", 1, 100)
514
+ if endpoint_id is not None:
515
+ _positive_id(endpoint_id, "endpoint_id")
516
+ if status is not None:
517
+ _required_text(status, "status")
518
+ params = _body(page=page, page_size=page_size, endpoint_id=endpoint_id, status_filter=status)
519
+ return cast(WebhookDeliveriesResponse, self._request("GET", "/org/webhooks/deliveries/recent", params=params, project_id=_project_override(project_id)))
520
+
521
+ def get_webhook_delivery(self, delivery_id: int, *, project_id: Optional[str] = None) -> WebhookDelivery:
522
+ path = f"/org/webhooks/deliveries/{_positive_id(delivery_id, 'delivery_id')}"
523
+ return cast(WebhookDelivery, self._request("GET", path, project_id=_project_override(project_id)))
524
+
525
+ def retry_webhook_delivery(self, delivery_id: int, *, project_id: Optional[str] = None) -> WebhookDelivery:
526
+ path = f"/org/webhooks/deliveries/{_positive_id(delivery_id, 'delivery_id')}/retry"
527
+ return cast(WebhookDelivery, self._request("POST", path, project_id=_project_override(project_id)))
528
+
529
+ def create_export(
530
+ self, *, format: str = "csv", scope: str = "filtered",
531
+ filters: Optional[Mapping[str, Any]] = None,
532
+ project_id: Optional[str] = None,
533
+ ) -> CreateExportResponse:
534
+ if format not in ("csv", "jsonl"):
535
+ raise ValidationError("format must be 'csv' or 'jsonl'")
536
+ if scope not in ("filtered", "all", "date_range"):
537
+ raise ValidationError("scope must be 'filtered', 'all', or 'date_range'")
538
+ data = _body(format=format, scope=scope, filters=_export_filters(filters))
539
+ return cast(CreateExportResponse, self._request("POST", "/exports", json=data, project_id=_project_override(project_id)))
540
+
541
+ def list_exports(self, *, limit: int = 20, project_id: Optional[str] = None) -> ExportJobListResponse:
542
+ _bounded(limit, "limit", 1, 100)
543
+ return cast(ExportJobListResponse, self._request("GET", "/exports", params={"limit": limit}, project_id=_project_override(project_id)))
544
+
545
+ def get_export(self, job_id: str, *, project_id: Optional[str] = None) -> ExportJob:
546
+ path = f"/exports/{_export_job_id(job_id)}"
547
+ return cast(ExportJob, self._request("GET", path, project_id=_project_override(project_id)))
548
+
549
+ def cancel_export(self, job_id: str, *, project_id: Optional[str] = None) -> ExportJob:
550
+ path = f"/exports/{_export_job_id(job_id)}/cancel"
551
+ return cast(ExportJob, self._request("POST", path, project_id=_project_override(project_id)))
552
+
553
+ def retry_export(self, job_id: str, *, project_id: Optional[str] = None) -> CreateExportResponse:
554
+ path = f"/exports/{_export_job_id(job_id)}/retry"
555
+ return cast(CreateExportResponse, self._request("POST", path, project_id=_project_override(project_id)))
556
+
557
+ def get_export_download_url(self, job_id: str, *, project_id: Optional[str] = None) -> ExportDownloadUrl:
558
+ path = f"/exports/{_export_job_id(job_id)}/download-url"
559
+ return cast(ExportDownloadUrl, self._request("GET", path, project_id=_project_override(project_id)))
560
+
423
561
 
424
562
  class AsyncControlPlaneClient(_ControlPlaneBase):
425
563
  """Async-native bearer client for dashboard/control-plane routes."""
@@ -482,58 +620,12 @@ class AsyncControlPlaneClient(_ControlPlaneBase):
482
620
  async def get_current_plan(self) -> CurrentPlanResponse:
483
621
  return cast(CurrentPlanResponse, await self._request("GET", "/org/billing/current-plan"))
484
622
 
485
- async def list_team_members(self) -> List[TeamMember]:
486
- return cast(List[TeamMember], await self._request("GET", "/admin/team/members"))
487
-
488
- async def suspend_team_member(self, member_id: int) -> TeamMember:
489
- path = f"/admin/team/members/{_positive_id(member_id, 'member_id')}"
490
- return cast(TeamMember, await self._request("PATCH", path, json={"status": "suspended"}))
491
-
492
- async def remove_team_member(self, member_id: int) -> None:
493
- await self._request("DELETE", f"/admin/team/members/{_positive_id(member_id, 'member_id')}")
494
-
495
623
  async def list_sessions(self) -> SessionsResponse:
496
624
  return cast(SessionsResponse, await self._request("GET", "/auth/sessions"))
497
625
 
498
626
  async def revoke_session(self, session_id: int) -> None:
499
627
  await self._request("POST", f"/auth/sessions/{_positive_id(session_id, 'session_id')}/revoke")
500
628
 
501
-
502
- async def list_audit_events(
503
- self, *, limit: Optional[int] = None, cursor: Optional[int] = None,
504
- skip: Optional[int] = None, sort_direction: Optional[str] = None,
505
- tenant_id: Optional[str] = None, actor: Optional[str] = None,
506
- actor_email: Optional[str] = None, ip: Optional[str] = None,
507
- action: Optional[str] = None, resource_type: Optional[str] = None,
508
- resource_id: Optional[str] = None, severity: Optional[str] = None,
509
- category: Optional[str] = None, start: Optional[str] = None,
510
- end: Optional[str] = None, success: Optional[bool] = None,
511
- source: Optional[str] = None, ingest_method: Optional[str] = None,
512
- search: Optional[str] = None, include_stats: Optional[bool] = None,
513
- ) -> AuditEventsResponse:
514
- if limit is not None:
515
- _bounded(limit, "limit", 1, 200)
516
- if cursor is not None and cursor < 0:
517
- raise ValidationError("cursor must be non-negative")
518
- if skip is not None and skip < 0:
519
- raise ValidationError("skip must be non-negative")
520
- if sort_direction not in (None, "asc", "desc"):
521
- raise ValidationError("sort_direction must be 'asc' or 'desc'")
522
- params = _body(limit=limit, cursor=cursor, skip=skip, sort=sort_direction,
523
- tenant_id=tenant_id, actor=actor, actor_email=actor_email,
524
- ip=ip, action=action, resource_type=resource_type,
525
- resource_id=resource_id, severity=severity, category=category,
526
- start=start, end=end, success=success, source=source,
527
- ingest_method=ingest_method, search=search,
528
- include_stats=include_stats)
529
- raw = await self._request("GET", "/admin/audit-logs", params=params)
530
- return cast(AuditEventsResponse, {
531
- "events": raw.get("logs", []),
532
- "next_cursor": raw.get("next_cursor"),
533
- "stats": raw.get("stats"),
534
- "sort": raw.get("sort", sort_direction or "desc"),
535
- })
536
-
537
629
  async def list_integrations(self, *, category: Optional[str] = None) -> List[Integration]:
538
630
  if category is not None:
539
631
  _required_text(category, "category")
@@ -550,43 +642,67 @@ class AsyncControlPlaneClient(_ControlPlaneBase):
550
642
  async def list_organizations(self) -> List[OrganizationMembership]:
551
643
  return cast(List[OrganizationMembership], await self._request("GET", "/organizations"))
552
644
 
553
- async def list_organization_members(self) -> List[TeamMember]:
554
- return await self.list_team_members()
555
-
556
- async def get_organization_settings(self, *, tenant_id: Optional[str] = None) -> OrganizationSettings:
557
- if tenant_id is not None:
558
- _required_text(tenant_id, "tenant_id")
559
- raw = await self._request("GET", "/admin/tenant-settings", params=_body(tenant_id=tenant_id))
560
- return cast(OrganizationSettings, raw)
561
-
562
645
  async def list_projects(self) -> List[Project]:
563
646
  return cast(List[Project], await self._request("GET", "/org/projects"))
564
647
 
648
+ async def create_project(self, name: str) -> Project:
649
+ raw = await self._request("POST", "/org/projects", json={"name": _project_name(name)})
650
+ return cast(Project, raw)
651
+
652
+ async def rename_project(self, project_id: str, name: str) -> Project:
653
+ path = f"/org/projects/{_path_segment(project_id, 'project_id')}"
654
+ return cast(Project, await self._request("PATCH", path, json={"name": _project_name(name)}))
655
+
656
+ async def archive_project(self, project_id: str) -> Project:
657
+ path = f"/org/projects/{_path_segment(project_id, 'project_id')}/archive"
658
+ return cast(Project, await self._request("POST", path))
659
+
660
+ async def unarchive_project(self, project_id: str) -> Project:
661
+ path = f"/org/projects/{_path_segment(project_id, 'project_id')}/unarchive"
662
+ return cast(Project, await self._request("POST", path))
663
+
664
+ async def delete_project(self, project_id: str) -> None:
665
+ await self._request("DELETE", f"/org/projects/{_path_segment(project_id, 'project_id')}")
666
+
565
667
  async def create_webhook(
566
668
  self, name: str, url: str, events: Sequence[str], *,
567
- description: Optional[str] = None, retry_config: Optional[Dict[str, Any]] = None,
568
- signature_config: Optional[Dict[str, Any]] = None,
669
+ description: Optional[str] = None, retry_config: Optional[Mapping[str, Any]] = None,
670
+ signature_config: Optional[Mapping[str, Any]] = None,
569
671
  project_id: Optional[str] = None,
570
- ) -> Webhook:
672
+ ) -> CreatedWebhook:
571
673
  _validate_webhook(name, url, events)
572
674
  if description is not None and len(description) > 500:
573
675
  raise ValidationError("description may contain at most 500 characters")
574
- data = _body(name=name, url=url, events=list(events), description=description,
575
- retry_config=retry_config, signature_config=signature_config,
576
- project_id=project_id)
676
+ data = _body(
677
+ name=name, url=url, events=list(events), description=description,
678
+ retry_config=_webhook_retry_config(retry_config),
679
+ signature_config=_webhook_signature_config(signature_config),
680
+ project_id=project_id,
681
+ )
577
682
  raw = await self._request("POST", "/org/webhooks", json=data, project_id=_project_override(project_id))
578
- return cast(Webhook, raw)
683
+ return cast(CreatedWebhook, raw)
579
684
 
580
685
  async def list_webhooks(self, *, project_id: Optional[str] = None) -> WebhooksResponse:
581
686
  return cast(WebhooksResponse, await self._request("GET", "/org/webhooks", project_id=_project_override(project_id)))
582
687
 
688
+ async def get_webhook(self, endpoint_id: int, *, project_id: Optional[str] = None) -> Webhook:
689
+ path = f"/org/webhooks/{_positive_id(endpoint_id, 'endpoint_id')}"
690
+ return cast(Webhook, await self._request("GET", path, project_id=_project_override(project_id)))
691
+
692
+ async def get_webhook_event_types(self, *, project_id: Optional[str] = None) -> WebhookEventTypesResponse:
693
+ raw = await self._request("GET", "/org/webhooks/event-types", project_id=_project_override(project_id))
694
+ return cast(WebhookEventTypesResponse, raw)
695
+
696
+ async def get_webhook_health(self, *, project_id: Optional[str] = None) -> WebhookHealth:
697
+ raw = await self._request("GET", "/org/webhooks/health", project_id=_project_override(project_id))
698
+ return cast(WebhookHealth, raw)
583
699
 
584
700
  async def update_webhook(
585
701
  self, endpoint_id: int, *, name: Optional[str] = None,
586
702
  url: Optional[str] = None, description: Optional[str] = None,
587
703
  events: Optional[Sequence[str]] = None,
588
- retry_config: Optional[Dict[str, Any]] = None,
589
- signature_config: Optional[Dict[str, Any]] = None,
704
+ retry_config: Optional[Mapping[str, Any]] = None,
705
+ signature_config: Optional[Mapping[str, Any]] = None,
590
706
  project_id: Optional[str] = None,
591
707
  ) -> Webhook:
592
708
  if name is not None:
@@ -599,19 +715,33 @@ class AsyncControlPlaneClient(_ControlPlaneBase):
599
715
  raise ValidationError("description may contain at most 500 characters")
600
716
  if events is not None:
601
717
  _string_sequence(events, "events")
602
- data = _body(name=name, url=url, description=description,
603
- events=list(events) if events is not None else None,
604
- retry_config=retry_config, signature_config=signature_config)
718
+ data = _body(
719
+ name=name, url=url, description=description,
720
+ events=list(events) if events is not None else None,
721
+ retry_config=_webhook_retry_config(retry_config),
722
+ signature_config=_webhook_signature_config(signature_config),
723
+ )
605
724
  if not data:
606
725
  raise ValidationError("update_webhook requires at least one editable field")
607
726
  path = f"/org/webhooks/{_positive_id(endpoint_id, 'endpoint_id')}"
608
- raw = await self._request("PATCH", path, json=data, project_id=_project_override(project_id))
609
- return cast(Webhook, raw)
727
+ return cast(Webhook, await self._request("PATCH", path, json=data, project_id=_project_override(project_id)))
610
728
 
611
729
  async def delete_webhook(self, endpoint_id: int, *, project_id: Optional[str] = None) -> None:
612
730
  path = f"/org/webhooks/{_positive_id(endpoint_id, 'endpoint_id')}"
613
731
  await self._request("DELETE", path, project_id=_project_override(project_id))
614
732
 
733
+ async def pause_webhook(self, endpoint_id: int, *, project_id: Optional[str] = None) -> Webhook:
734
+ path = f"/org/webhooks/{_positive_id(endpoint_id, 'endpoint_id')}/pause"
735
+ return cast(Webhook, await self._request("POST", path, project_id=_project_override(project_id)))
736
+
737
+ async def resume_webhook(self, endpoint_id: int, *, project_id: Optional[str] = None) -> Webhook:
738
+ path = f"/org/webhooks/{_positive_id(endpoint_id, 'endpoint_id')}/resume"
739
+ return cast(Webhook, await self._request("POST", path, project_id=_project_override(project_id)))
740
+
741
+ async def rotate_webhook_secret(self, endpoint_id: int, *, project_id: Optional[str] = None) -> CreatedWebhook:
742
+ path = f"/org/webhooks/{_positive_id(endpoint_id, 'endpoint_id')}/rotate-secret"
743
+ return cast(CreatedWebhook, await self._request("POST", path, project_id=_project_override(project_id)))
744
+
615
745
  async def test_webhook(
616
746
  self, endpoint_id: int, *, event_type: str = "memory.created",
617
747
  project_id: Optional[str] = None,
@@ -634,8 +764,15 @@ class AsyncControlPlaneClient(_ControlPlaneBase):
634
764
  raw = await self._request("POST", path, json=data, project_id=_project_override(project_id))
635
765
  return cast(WebhookReplayResponse, raw)
636
766
 
767
+ async def get_latest_webhook_delivery(
768
+ self, endpoint_id: int, *, project_id: Optional[str] = None,
769
+ ) -> Optional[WebhookDelivery]:
770
+ path = f"/org/webhooks/{_positive_id(endpoint_id, 'endpoint_id')}/deliveries/latest"
771
+ raw = await self._request("GET", path, project_id=_project_override(project_id))
772
+ return cast(Optional[WebhookDelivery], raw)
773
+
637
774
  async def list_webhook_deliveries(
638
- self, endpoint_id: int, *, page: int = 1, page_size: int = 25,
775
+ self, endpoint_id: int, *, page: int = 1, page_size: int = 20,
639
776
  status: Optional[str] = None, project_id: Optional[str] = None,
640
777
  ) -> WebhookDeliveriesResponse:
641
778
  _positive_id(page, "page")
@@ -646,3 +783,59 @@ class AsyncControlPlaneClient(_ControlPlaneBase):
646
783
  params = _body(page=page, page_size=page_size, status_filter=status)
647
784
  raw = await self._request("GET", path, params=params, project_id=_project_override(project_id))
648
785
  return cast(WebhookDeliveriesResponse, raw)
786
+
787
+ async def list_recent_webhook_deliveries(
788
+ self, *, page: int = 1, page_size: int = 20,
789
+ endpoint_id: Optional[int] = None, status: Optional[str] = None,
790
+ project_id: Optional[str] = None,
791
+ ) -> WebhookDeliveriesResponse:
792
+ _positive_id(page, "page")
793
+ _bounded(page_size, "page_size", 1, 100)
794
+ if endpoint_id is not None:
795
+ _positive_id(endpoint_id, "endpoint_id")
796
+ if status is not None:
797
+ _required_text(status, "status")
798
+ params = _body(page=page, page_size=page_size, endpoint_id=endpoint_id, status_filter=status)
799
+ raw = await self._request("GET", "/org/webhooks/deliveries/recent", params=params, project_id=_project_override(project_id))
800
+ return cast(WebhookDeliveriesResponse, raw)
801
+
802
+ async def get_webhook_delivery(self, delivery_id: int, *, project_id: Optional[str] = None) -> WebhookDelivery:
803
+ path = f"/org/webhooks/deliveries/{_positive_id(delivery_id, 'delivery_id')}"
804
+ return cast(WebhookDelivery, await self._request("GET", path, project_id=_project_override(project_id)))
805
+
806
+ async def retry_webhook_delivery(self, delivery_id: int, *, project_id: Optional[str] = None) -> WebhookDelivery:
807
+ path = f"/org/webhooks/deliveries/{_positive_id(delivery_id, 'delivery_id')}/retry"
808
+ return cast(WebhookDelivery, await self._request("POST", path, project_id=_project_override(project_id)))
809
+
810
+ async def create_export(
811
+ self, *, format: str = "csv", scope: str = "filtered",
812
+ filters: Optional[Mapping[str, Any]] = None,
813
+ project_id: Optional[str] = None,
814
+ ) -> CreateExportResponse:
815
+ if format not in ("csv", "jsonl"):
816
+ raise ValidationError("format must be 'csv' or 'jsonl'")
817
+ if scope not in ("filtered", "all", "date_range"):
818
+ raise ValidationError("scope must be 'filtered', 'all', or 'date_range'")
819
+ data = _body(format=format, scope=scope, filters=_export_filters(filters))
820
+ raw = await self._request("POST", "/exports", json=data, project_id=_project_override(project_id))
821
+ return cast(CreateExportResponse, raw)
822
+
823
+ async def list_exports(self, *, limit: int = 20, project_id: Optional[str] = None) -> ExportJobListResponse:
824
+ _bounded(limit, "limit", 1, 100)
825
+ raw = await self._request("GET", "/exports", params={"limit": limit}, project_id=_project_override(project_id))
826
+ return cast(ExportJobListResponse, raw)
827
+
828
+ async def get_export(self, job_id: str, *, project_id: Optional[str] = None) -> ExportJob:
829
+ return cast(ExportJob, await self._request("GET", f"/exports/{_export_job_id(job_id)}", project_id=_project_override(project_id)))
830
+
831
+ async def cancel_export(self, job_id: str, *, project_id: Optional[str] = None) -> ExportJob:
832
+ path = f"/exports/{_export_job_id(job_id)}/cancel"
833
+ return cast(ExportJob, await self._request("POST", path, project_id=_project_override(project_id)))
834
+
835
+ async def retry_export(self, job_id: str, *, project_id: Optional[str] = None) -> CreateExportResponse:
836
+ path = f"/exports/{_export_job_id(job_id)}/retry"
837
+ return cast(CreateExportResponse, await self._request("POST", path, project_id=_project_override(project_id)))
838
+
839
+ async def get_export_download_url(self, job_id: str, *, project_id: Optional[str] = None) -> ExportDownloadUrl:
840
+ path = f"/exports/{_export_job_id(job_id)}/download-url"
841
+ return cast(ExportDownloadUrl, await self._request("GET", path, project_id=_project_override(project_id)))
@@ -4,6 +4,11 @@ from __future__ import annotations
4
4
 
5
5
  from typing import Any, Dict, List, Optional, TypedDict
6
6
 
7
+ try:
8
+ from typing import NotRequired
9
+ except ImportError: # Python 3.9 and 3.10
10
+ from typing_extensions import NotRequired
11
+
7
12
  JsonObject = Dict[str, Any]
8
13
 
9
14
 
@@ -62,21 +67,6 @@ class CurrentPlanResponse(TypedDict, total=False):
62
67
  cancel_at_period_end: bool
63
68
 
64
69
 
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
70
  class Session(TypedDict, total=False):
81
71
  id: int
82
72
  is_current: bool
@@ -93,26 +83,7 @@ class Session(TypedDict, total=False):
93
83
 
94
84
  class SessionsResponse(TypedDict):
95
85
  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
86
+ current_session_id: Optional[int]
116
87
 
117
88
 
118
89
  class Integration(TypedDict, total=False):
@@ -133,26 +104,6 @@ class OrganizationMembership(TypedDict):
133
104
  role: str
134
105
 
135
106
 
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
107
  class Project(TypedDict, total=False):
157
108
  id: str
158
109
  name: str
@@ -173,10 +124,33 @@ class Webhook(TypedDict, total=False):
173
124
  events: List[str]
174
125
  enabled: bool
175
126
  signature_algorithm: str
176
- project_id: Optional[str]
177
- secret: str
127
+ signature_header: str
128
+ timestamp_header: str
129
+ signature_tolerance: int
130
+ retry_enabled: bool
131
+ max_retries: int
132
+ initial_delay_seconds: int
133
+ max_delay_seconds: int
134
+ backoff_multiplier: float
135
+ retry_status_codes: List[str]
136
+ total_deliveries: int
137
+ successful_deliveries: int
138
+ failed_deliveries: int
139
+ consecutive_failures: int
178
140
  success_rate: float
141
+ last_triggered_at: Optional[str]
142
+ last_success_at: Optional[str]
143
+ last_failure_at: Optional[str]
144
+ last_error: Optional[str]
145
+ last_status_code: Optional[int]
146
+ created_at: str
179
147
  updated_at: str
148
+ created_by_name: Optional[str]
149
+ project_id: Optional[str]
150
+
151
+
152
+ class CreatedWebhook(Webhook):
153
+ secret: str
180
154
 
181
155
 
182
156
  class WebhooksResponse(TypedDict, total=False):
@@ -212,11 +186,16 @@ class WebhookDelivery(TypedDict, total=False):
212
186
  event_type: str
213
187
  event_id: str
214
188
  status: str
189
+ payload: JsonObject
215
190
  payload_hash: str
191
+ signature: Optional[str]
216
192
  status_code: Optional[int]
193
+ response_body: Optional[str]
194
+ error_message: Optional[str]
217
195
  latency_ms: Optional[int]
218
196
  attempt_number: int
219
197
  max_attempts: int
198
+ next_retry_at: Optional[str]
220
199
  created_at: str
221
200
  completed_at: Optional[str]
222
201
 
@@ -232,6 +211,10 @@ class CreateWebhookRequest(TypedDict):
232
211
  name: str
233
212
  url: str
234
213
  events: List[str]
214
+ description: NotRequired[Optional[str]]
215
+ retry_config: NotRequired[JsonObject]
216
+ signature_config: NotRequired[JsonObject]
217
+ project_id: NotRequired[str]
235
218
 
236
219
 
237
220
  class UpdateWebhookRequest(TypedDict, total=False):
@@ -265,3 +248,85 @@ class ReplayWebhookDeliveriesRequest(TypedDict):
265
248
  since_minutes: int
266
249
  statuses: List[str]
267
250
  limit: int
251
+
252
+
253
+ class CreateProjectRequest(TypedDict):
254
+ name: str
255
+
256
+
257
+ class RenameProjectRequest(TypedDict):
258
+ name: str
259
+
260
+
261
+ class WebhookEventTypesResponse(TypedDict):
262
+ event_types: List[str]
263
+ categories: Dict[str, List[str]]
264
+
265
+
266
+ class WebhookHealth(TypedDict):
267
+ active_webhooks: int
268
+ total_webhooks: int
269
+ total_deliveries_all_time: int
270
+ total_deliveries_24h: int
271
+ success_rate_7d: float
272
+ failing_endpoints: int
273
+ pending_retries: int
274
+ dead_letter_count: int
275
+
276
+
277
+ class ExportFilters(TypedDict, total=False):
278
+ q: str
279
+ user_id: str
280
+ is_summary: bool
281
+ tier: str
282
+ source: str
283
+ time_range: str
284
+ date_from: str
285
+ date_to: str
286
+ include_soft_deleted: bool
287
+
288
+
289
+ class CreateExportRequest(TypedDict, total=False):
290
+ format: str
291
+ scope: str
292
+ filters: ExportFilters
293
+
294
+
295
+ class ExportJob(TypedDict, total=False):
296
+ id: str
297
+ project_id: Optional[str]
298
+ environment: str
299
+ format: str
300
+ scope: str
301
+ filters: JsonObject
302
+ status: str
303
+ error_message: Optional[str]
304
+ retry_count: int
305
+ total_rows: Optional[int]
306
+ processed_rows: int
307
+ progress_percentage: float
308
+ file_size_bytes: Optional[int]
309
+ sha256_hash: Optional[str]
310
+ download_count: int
311
+ created_at: Optional[str]
312
+ started_at: Optional[str]
313
+ completed_at: Optional[str]
314
+ expires_at: Optional[str]
315
+ download_url: Optional[str]
316
+
317
+
318
+ class ExportJobListResponse(TypedDict):
319
+ jobs: List[ExportJob]
320
+ total: int
321
+
322
+
323
+ class CreateExportResponse(TypedDict):
324
+ job: ExportJob
325
+ estimated_total: Optional[int]
326
+
327
+
328
+ class ExportDownloadUrl(TypedDict):
329
+ download_url: str
330
+ expires_at: Optional[str]
331
+ file_size_bytes: Optional[int]
332
+ filename: str
@@ -1 +0,0 @@
1
- __version__ = "1.3.0"
File without changes
File without changes
File without changes