memorysync 1.2.0__tar.gz → 1.3.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
1
  Metadata-Version: 2.4
2
2
  Name: memorysync
3
- Version: 1.2.0
3
+ Version: 1.3.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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "memorysync"
7
- version = "1.2.0"
7
+ version = "1.3.0"
8
8
  description = "Official Python client for the MemorySync API."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -0,0 +1 @@
1
+ __version__ = "1.3.0"
@@ -260,6 +260,29 @@ class MemorySyncClient:
260
260
  self._project_id = project_id
261
261
  self._end_user_id = end_user_id
262
262
  self._http = httpx.Client(timeout=timeout, transport=transport)
263
+ self._attach_namespaces()
264
+
265
+ def _attach_namespaces(self) -> None:
266
+ """Wire the connector namespaces onto this client.
267
+
268
+ The namespace classes are shared with the async client — they only ever
269
+ hand a path to ``self._request``, so what comes back is a dict here and a
270
+ coroutine there. See ``connections.py`` for why that is one set of classes
271
+ rather than two.
272
+ """
273
+ from .connections import (
274
+ ConnectionsNamespace,
275
+ IntegrationsNamespace,
276
+ ObjectsNamespace,
277
+ ProvidersNamespace,
278
+ SyncJobsNamespace,
279
+ )
280
+
281
+ self.connections = ConnectionsNamespace(self._request)
282
+ self.objects = ObjectsNamespace(self._request)
283
+ self.providers = ProvidersNamespace(self._request)
284
+ self.sync_jobs = SyncJobsNamespace(self._request)
285
+ self.integrations = IntegrationsNamespace(self._request)
263
286
 
264
287
  def __enter__(self) -> "MemorySyncClient":
265
288
  return self
@@ -913,6 +936,29 @@ class AsyncMemorySyncClient:
913
936
  self._project_id = project_id
914
937
  self._end_user_id = end_user_id
915
938
  self._http = httpx.AsyncClient(timeout=timeout, transport=transport)
939
+ self._attach_namespaces()
940
+
941
+ def _attach_namespaces(self) -> None:
942
+ """Wire the connector namespaces onto this client.
943
+
944
+ Same classes as the sync client. Because they simply return whatever
945
+ ``self._request`` returns, and this client's ``_request`` is a coroutine
946
+ function, every namespace method here is awaitable:
947
+ ``await client.connections.list()``.
948
+ """
949
+ from .connections import (
950
+ ConnectionsNamespace,
951
+ IntegrationsNamespace,
952
+ ObjectsNamespace,
953
+ ProvidersNamespace,
954
+ SyncJobsNamespace,
955
+ )
956
+
957
+ self.connections = ConnectionsNamespace(self._request)
958
+ self.objects = ObjectsNamespace(self._request)
959
+ self.providers = ProvidersNamespace(self._request)
960
+ self.sync_jobs = SyncJobsNamespace(self._request)
961
+ self.integrations = IntegrationsNamespace(self._request)
916
962
 
917
963
  async def __aenter__(self) -> "AsyncMemorySyncClient":
918
964
  return self
@@ -0,0 +1,680 @@
1
+ """Connector namespaces — ``client.connections``, ``client.objects`` and friends.
2
+
3
+ Covers the connector API: creating and managing connections to Slack, Google
4
+ Drive, S3 and Granola, driving syncs, and inspecting the objects a sync produced.
5
+ Shaped after ``client.connections.*`` in comparable SDKs so the layout is
6
+ familiar.
7
+
8
+ One set of classes serves both clients
9
+ --------------------------------------
10
+ Every method here is a thin pass-through: build a path, hand it to the client's
11
+ ``_request``, return what comes back. Nothing post-processes the response. That
12
+ means the *same* class works for the sync and async clients — ``MemorySyncClient``
13
+ hands over a ``_request`` that returns a dict, ``AsyncMemorySyncClient`` hands over
14
+ one that returns a coroutine.
15
+
16
+ So on the sync client::
17
+
18
+ conns = client.connections.list()
19
+
20
+ and on the async client::
21
+
22
+ conns = await client.connections.list()
23
+
24
+ The alternative was 22 near-identical classes differing only by ``await``, which
25
+ would double the surface area to review and to keep in step. The cost is that
26
+ return types are annotated ``Any`` rather than a precise dict-or-awaitable union,
27
+ and that is called out on every method group rather than left to be discovered.
28
+
29
+ Why these return raw payloads
30
+ -----------------------------
31
+ Connector responses are large, provider-shaped and still moving — a Slack channel
32
+ listing looks nothing like an S3 prefix listing, and both carry provider fields
33
+ that change when the provider changes. Freezing them into dataclasses would mean
34
+ an SDK release every time a provider adds a field, and callers unable to see the
35
+ new field until then. Typed models are reserved for the small, stable, first-party
36
+ shapes (memories, history, feedback, ontology).
37
+ """
38
+
39
+ from __future__ import annotations
40
+
41
+ from typing import Any, Callable, Dict, Optional, Sequence
42
+ from urllib.parse import quote
43
+
44
+ _V2 = "/api/v2/integrations"
45
+ _V1 = "/api/v1/integrations"
46
+
47
+ # A bound ``_request`` from either client.
48
+ RequestFn = Callable[..., Any]
49
+
50
+
51
+ def _seg(value: Any) -> str:
52
+ """Percent-encode one path segment.
53
+
54
+ Connection, object and channel ids come from providers, not from us. A Slack
55
+ channel id is tame but a Drive resource id or an S3 prefix is not, and an
56
+ unencoded ``/`` in one of them would silently change which route is called.
57
+ """
58
+ return quote(str(value), safe="")
59
+
60
+
61
+ class _Namespace:
62
+ """Holds the bound request function. Subclasses add methods."""
63
+
64
+ __slots__ = ("_request",)
65
+
66
+ def __init__(self, request: RequestFn) -> None:
67
+ self._request = request
68
+
69
+
70
+ # ─────────────────────────────────────────────────────────────────────
71
+ # Provider-specific namespaces
72
+ #
73
+ # Reached as ``client.connections.slack``, ``.gdrive``, ``.s3``, ``.granola``.
74
+ # Each takes the connection id first because a provider setting only means
75
+ # anything relative to one connection.
76
+ # ─────────────────────────────────────────────────────────────────────
77
+
78
+
79
+ class SlackNamespace(_Namespace):
80
+ """Slack connection settings. Awaitable on the async client."""
81
+
82
+ def available_channels(self, connection_id: str, **params: Any) -> Any:
83
+ """Channels the app can see and could be added.
84
+
85
+ Private channels appear only where the deployment allows them *and* a
86
+ human has invited the app, so this never widens what someone already
87
+ granted.
88
+ """
89
+ return self._request(
90
+ "GET", f"{_V2}/connections/{_seg(connection_id)}/slack/available-channels",
91
+ params=params or None,
92
+ )
93
+
94
+ def channels(self, connection_id: str) -> Any:
95
+ """Channels currently selected for syncing."""
96
+ return self._request(
97
+ "GET", f"{_V2}/connections/{_seg(connection_id)}/slack/channels"
98
+ )
99
+
100
+ def add_channels(self, connection_id: str, channel_ids: Sequence[str]) -> Any:
101
+ """Select channels for syncing."""
102
+ return self._request(
103
+ "POST", f"{_V2}/connections/{_seg(connection_id)}/slack/channels",
104
+ json={"channel_ids": list(channel_ids)},
105
+ )
106
+
107
+ def remove_channel(self, connection_id: str, channel_id: str) -> Any:
108
+ """Stop syncing one channel."""
109
+ return self._request(
110
+ "DELETE",
111
+ f"{_V2}/connections/{_seg(connection_id)}/slack/channels/{_seg(channel_id)}",
112
+ )
113
+
114
+ def exclusion_policy(self, connection_id: str) -> Any:
115
+ """Channels this connection will never sync.
116
+
117
+ The deployment-wide floor cannot be removed here; a tenant may only add
118
+ to it.
119
+ """
120
+ return self._request(
121
+ "GET", f"{_V2}/connections/{_seg(connection_id)}/slack/exclusion-policy"
122
+ )
123
+
124
+ def set_exclusion_policy(self, connection_id: str, **policy: Any) -> Any:
125
+ """Replace this connection's additions to the exclusion policy."""
126
+ return self._request(
127
+ "PUT", f"{_V2}/connections/{_seg(connection_id)}/slack/exclusion-policy",
128
+ json=policy,
129
+ )
130
+
131
+ def identities(self, connection_id: str) -> Any:
132
+ """Slack users seen on this connection and who they map to."""
133
+ return self._request(
134
+ "GET", f"{_V2}/connections/{_seg(connection_id)}/slack/identities"
135
+ )
136
+
137
+ def link_identity(self, connection_id: str, **body: Any) -> Any:
138
+ """Map a Slack user to a MemorySync end user."""
139
+ return self._request(
140
+ "POST", f"{_V2}/connections/{_seg(connection_id)}/slack/identities/link",
141
+ json=body,
142
+ )
143
+
144
+ def sync_identities(self, connection_id: str) -> Any:
145
+ """Re-read the Slack member list and refresh the identity table."""
146
+ return self._request(
147
+ "POST", f"{_V2}/connections/{_seg(connection_id)}/slack/identities/sync"
148
+ )
149
+
150
+
151
+ class GoogleDriveNamespace(_Namespace):
152
+ """Google Drive connection settings. Awaitable on the async client."""
153
+
154
+ def picker_config(self, connection_id: str) -> Any:
155
+ """Config for rendering Google's own file picker in your UI."""
156
+ return self._request(
157
+ "GET", f"{_V2}/connections/{_seg(connection_id)}/gdrive/picker-config"
158
+ )
159
+
160
+ def resources(self, connection_id: str, **params: Any) -> Any:
161
+ """Files and folders selected for syncing."""
162
+ return self._request(
163
+ "GET", f"{_V2}/connections/{_seg(connection_id)}/gdrive/resources",
164
+ params=params or None,
165
+ )
166
+
167
+ def add_resources(self, connection_id: str, **body: Any) -> Any:
168
+ """Select files or folders for syncing."""
169
+ return self._request(
170
+ "POST", f"{_V2}/connections/{_seg(connection_id)}/gdrive/resources",
171
+ json=body,
172
+ )
173
+
174
+ def remove_resource(self, connection_id: str, resource_id: str) -> Any:
175
+ """Stop syncing one file or folder."""
176
+ return self._request(
177
+ "DELETE",
178
+ f"{_V2}/connections/{_seg(connection_id)}/gdrive/resources/{_seg(resource_id)}",
179
+ )
180
+
181
+
182
+ class S3Namespace(_Namespace):
183
+ """S3 connection settings. Awaitable on the async client."""
184
+
185
+ def available_prefixes(self, connection_id: str, **params: Any) -> Any:
186
+ """Prefixes visible in the bucket that could be added."""
187
+ return self._request(
188
+ "GET", f"{_V2}/connections/{_seg(connection_id)}/s3/available-prefixes",
189
+ params=params or None,
190
+ )
191
+
192
+ def prefixes(self, connection_id: str) -> Any:
193
+ """Prefixes currently selected for syncing."""
194
+ return self._request(
195
+ "GET", f"{_V2}/connections/{_seg(connection_id)}/s3/prefixes"
196
+ )
197
+
198
+ def add_prefixes(self, connection_id: str, prefixes: Sequence[str]) -> Any:
199
+ """Select prefixes for syncing."""
200
+ return self._request(
201
+ "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."""
207
+ return self._request(
208
+ "DELETE", f"{_V2}/connections/{_seg(connection_id)}/s3/prefixes",
209
+ json={"prefixes": list(prefixes)},
210
+ )
211
+
212
+ def exclusion_policy(self, connection_id: str) -> Any:
213
+ """Keys and patterns this connection will never sync."""
214
+ return self._request(
215
+ "GET", f"{_V2}/connections/{_seg(connection_id)}/s3/exclusion-policy"
216
+ )
217
+
218
+ def set_exclusion_policy(self, connection_id: str, **policy: Any) -> Any:
219
+ """Replace this connection's additions to the exclusion policy."""
220
+ return self._request(
221
+ "PUT", f"{_V2}/connections/{_seg(connection_id)}/s3/exclusion-policy",
222
+ json=policy,
223
+ )
224
+
225
+ def settings(self, connection_id: str) -> Any:
226
+ """Effective S3 settings, including the per-object size ceiling."""
227
+ return self._request(
228
+ "GET", f"{_V2}/connections/{_seg(connection_id)}/s3/settings"
229
+ )
230
+
231
+
232
+ class GranolaNamespace(_Namespace):
233
+ """Granola connection settings. Awaitable on the async client."""
234
+
235
+ def available_folders(self, connection_id: str, **params: Any) -> Any:
236
+ """Folders that could be added."""
237
+ return self._request(
238
+ "GET", f"{_V2}/connections/{_seg(connection_id)}/granola/available-folders",
239
+ params=params or None,
240
+ )
241
+
242
+ def folders(self, connection_id: str) -> Any:
243
+ """Folders currently selected for syncing."""
244
+ return self._request(
245
+ "GET", f"{_V2}/connections/{_seg(connection_id)}/granola/folders"
246
+ )
247
+
248
+ def add_folders(self, connection_id: str, **body: Any) -> Any:
249
+ """Select folders for syncing."""
250
+ return self._request(
251
+ "POST", f"{_V2}/connections/{_seg(connection_id)}/granola/folders",
252
+ json=body,
253
+ )
254
+
255
+ def remove_folder(self, connection_id: str, folder_id: str) -> Any:
256
+ """Stop syncing one folder."""
257
+ return self._request(
258
+ "DELETE",
259
+ f"{_V2}/connections/{_seg(connection_id)}/granola/folders/{_seg(folder_id)}",
260
+ )
261
+
262
+ def exclusion_policy(self, connection_id: str) -> Any:
263
+ """Folders and meetings this connection will never sync."""
264
+ return self._request(
265
+ "GET", f"{_V2}/connections/{_seg(connection_id)}/granola/exclusion-policy"
266
+ )
267
+
268
+ def set_exclusion_policy(self, connection_id: str, **policy: Any) -> Any:
269
+ """Replace this connection's additions to the exclusion policy."""
270
+ return self._request(
271
+ "PUT", f"{_V2}/connections/{_seg(connection_id)}/granola/exclusion-policy",
272
+ json=policy,
273
+ )
274
+
275
+ def identities(self, connection_id: str) -> Any:
276
+ """Meeting participants seen on this connection and who they map to."""
277
+ return self._request(
278
+ "GET", f"{_V2}/connections/{_seg(connection_id)}/granola/identities"
279
+ )
280
+
281
+ def link_identity(self, connection_id: str, **body: Any) -> Any:
282
+ """Map a participant to a MemorySync end user."""
283
+ return self._request(
284
+ "POST", f"{_V2}/connections/{_seg(connection_id)}/granola/identities/link",
285
+ json=body,
286
+ )
287
+
288
+ def relink_identity(self, connection_id: str, **body: Any) -> Any:
289
+ """Move an existing mapping to a different end user."""
290
+ return self._request(
291
+ "POST", f"{_V2}/connections/{_seg(connection_id)}/granola/identities/relink",
292
+ json=body,
293
+ )
294
+
295
+ def settings(self, connection_id: str) -> Any:
296
+ """Effective Granola settings for this connection."""
297
+ return self._request(
298
+ "GET", f"{_V2}/connections/{_seg(connection_id)}/granola/settings"
299
+ )
300
+
301
+ def set_settings(self, connection_id: str, **settings: Any) -> Any:
302
+ """Update Granola settings for this connection."""
303
+ return self._request(
304
+ "PUT", f"{_V2}/connections/{_seg(connection_id)}/granola/settings",
305
+ json=settings,
306
+ )
307
+
308
+
309
+ # ─────────────────────────────────────────────────────────────────────
310
+ # OAuth handshake
311
+ # ─────────────────────────────────────────────────────────────────────
312
+
313
+
314
+ class ConnectionOAuthNamespace(_Namespace):
315
+ """Starting an OAuth connection. Awaitable on the async client."""
316
+
317
+ def initiate(self, provider: str, **body: Any) -> Any:
318
+ """Begin an OAuth connection and get the URL to send the user to.
319
+
320
+ The user completes consent in a browser; the provider then calls the
321
+ platform back. Poll :meth:`status` to find out how it went — the callback
322
+ does not come to your backend.
323
+ """
324
+ payload = {"provider": provider}
325
+ payload.update(body)
326
+ return self._request("POST", f"{_V2}/oauth/initiate", json=payload)
327
+
328
+ def status(self, **params: Any) -> Any:
329
+ """Where an in-flight OAuth connection got to."""
330
+ return self._request("GET", f"{_V2}/oauth/status", params=params or None)
331
+
332
+
333
+ # ─────────────────────────────────────────────────────────────────────
334
+ # Connections
335
+ # ─────────────────────────────────────────────────────────────────────
336
+
337
+
338
+ class ConnectionsNamespace(_Namespace):
339
+ """Connections to external sources. Awaitable on the async client.
340
+
341
+ Provider-specific settings live in sub-namespaces: ``connections.slack``,
342
+ ``connections.gdrive``, ``connections.s3``, ``connections.granola``.
343
+ """
344
+
345
+ def __init__(self, request: RequestFn) -> None:
346
+ super().__init__(request)
347
+ self.slack = SlackNamespace(request)
348
+ self.gdrive = GoogleDriveNamespace(request)
349
+ self.s3 = S3Namespace(request)
350
+ self.granola = GranolaNamespace(request)
351
+ self.oauth = ConnectionOAuthNamespace(request)
352
+
353
+ # ── lifecycle ────────────────────────────────────────────────────
354
+
355
+ def list(self, **params: Any) -> Any:
356
+ """Every connection in this organization."""
357
+ return self._request("GET", f"{_V2}/connections", params=params or None)
358
+
359
+ def get(self, connection_id: str) -> Any:
360
+ """One connection, including its status and last sync."""
361
+ return self._request("GET", f"{_V2}/connections/{_seg(connection_id)}")
362
+
363
+ 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}
366
+ payload.update(body)
367
+ return self._request("POST", f"{_V2}/connections/api-key", json=payload)
368
+
369
+ def create_with_credentials(self, provider: str, credentials: Dict[str, Any], **body: Any) -> Any:
370
+ """Connect a provider that needs a credential bundle, such as S3 keys."""
371
+ payload: Dict[str, Any] = {"provider": provider, "credentials": credentials}
372
+ payload.update(body)
373
+ return self._request("POST", f"{_V2}/connections/credentials", json=payload)
374
+
375
+ def update(self, connection_id: str, **body: Any) -> Any:
376
+ """Change a connection's name, schedule or settings."""
377
+ return self._request(
378
+ "PATCH", f"{_V2}/connections/{_seg(connection_id)}", json=body
379
+ )
380
+
381
+ def delete(self, connection_id: str) -> Any:
382
+ """Remove a connection.
383
+
384
+ This stops future syncing. Memories already extracted are not removed —
385
+ use :meth:`purge` for that, so disconnecting never silently deletes
386
+ knowledge someone still depends on.
387
+ """
388
+ return self._request("DELETE", f"{_V2}/connections/{_seg(connection_id)}")
389
+
390
+ def reconnect(self, connection_id: str, **body: Any) -> Any:
391
+ """Re-authorise a connection whose credentials expired or were revoked."""
392
+ return self._request(
393
+ "POST", f"{_V2}/connections/{_seg(connection_id)}/reconnect", json=body or None
394
+ )
395
+
396
+ def purge(self, connection_id: str, **body: Any) -> Any:
397
+ """Delete the memories this connection produced.
398
+
399
+ Separate from :meth:`delete` on purpose: removing a connection and
400
+ removing what it taught you are different decisions.
401
+ """
402
+ return self._request(
403
+ "POST", f"{_V2}/connections/{_seg(connection_id)}/purge", json=body or None
404
+ )
405
+
406
+ # ── syncing ──────────────────────────────────────────────────────
407
+
408
+ def sync_status(self, connection_id: str) -> Any:
409
+ """Current and recent sync state for a connection."""
410
+ return self._request("GET", f"{_V2}/connections/{_seg(connection_id)}/sync")
411
+
412
+ def trigger_sync(self, connection_id: str, **body: Any) -> Any:
413
+ """Start a sync now instead of waiting for the schedule."""
414
+ return self._request(
415
+ "POST", f"{_V2}/connections/{_seg(connection_id)}/sync", json=body or None
416
+ )
417
+
418
+ # ── objects produced by a connection ─────────────────────────────
419
+
420
+ def objects(self, connection_id: str, **params: Any) -> Any:
421
+ """Objects a connection has ingested — files, messages, meetings."""
422
+ return self._request(
423
+ "GET", f"{_V2}/connections/{_seg(connection_id)}/objects",
424
+ params=params or None,
425
+ )
426
+
427
+ def objects_v2(self, connection_id: str, **params: Any) -> Any:
428
+ """Object listing with richer filtering and paging than :meth:`objects`."""
429
+ return self._request(
430
+ "GET", f"{_V2}/connections/{_seg(connection_id)}/objects/v2",
431
+ params=params or None,
432
+ )
433
+
434
+ def bulk_object_action(self, connection_id: str, **body: Any) -> Any:
435
+ """Apply one action to many objects — pause, resume, re-extract."""
436
+ return self._request(
437
+ "POST", f"{_V2}/connections/{_seg(connection_id)}/objects/bulk", json=body
438
+ )
439
+
440
+ # ── organization-wide ────────────────────────────────────────────
441
+
442
+ def stats(self, **params: Any) -> Any:
443
+ """Connector totals: connections, objects synced, memories produced."""
444
+ return self._request("GET", f"{_V2}/stats", params=params or None)
445
+
446
+ def audit_logs(self, **params: Any) -> Any:
447
+ """Audit trail of connector activity."""
448
+ return self._request("GET", f"{_V2}/audit-logs", params=params or None)
449
+
450
+
451
+ # ─────────────────────────────────────────────────────────────────────
452
+ # Objects
453
+ # ─────────────────────────────────────────────────────────────────────
454
+
455
+
456
+ class ObjectsNamespace(_Namespace):
457
+ """A single synced object. Awaitable on the async client.
458
+
459
+ An "object" is one thing a connector ingested: a Drive file, a Slack message
460
+ batch, an S3 key, a meeting transcript.
461
+ """
462
+
463
+ def get(self, object_id: str) -> Any:
464
+ """Metadata and sync state for one object."""
465
+ return self._request("GET", f"{_V2}/objects/{_seg(object_id)}")
466
+
467
+ def analysis(self, object_id: str) -> Any:
468
+ """What extraction made of this object."""
469
+ return self._request("GET", f"{_V2}/objects/{_seg(object_id)}/analysis")
470
+
471
+ def audit(self, object_id: str, **params: Any) -> Any:
472
+ """Every action taken on this object."""
473
+ return self._request(
474
+ "GET", f"{_V2}/objects/{_seg(object_id)}/audit", params=params or None
475
+ )
476
+
477
+ def history(self, object_id: str, **params: Any) -> Any:
478
+ """Versions of this object seen across syncs."""
479
+ return self._request(
480
+ "GET", f"{_V2}/objects/{_seg(object_id)}/history", params=params or None
481
+ )
482
+
483
+ def memory_status(self, object_id: str) -> Any:
484
+ """Which memories this object produced, and whether extraction finished."""
485
+ return self._request("GET", f"{_V2}/objects/{_seg(object_id)}/memory-status")
486
+
487
+ def structured_stats(self, object_id: str) -> Any:
488
+ """Row and column statistics for spreadsheet-shaped objects."""
489
+ return self._request("GET", f"{_V2}/objects/{_seg(object_id)}/structured-stats")
490
+
491
+ def evaluate(self, object_id: str, **body: Any) -> Any:
492
+ """Score this object for extraction worthiness without extracting."""
493
+ return self._request(
494
+ "POST", f"{_V2}/objects/{_seg(object_id)}/evaluate", json=body or None
495
+ )
496
+
497
+ def pause(self, object_id: str) -> Any:
498
+ """Stop re-syncing this object, leaving its memories in place."""
499
+ return self._request("POST", f"{_V2}/objects/{_seg(object_id)}/pause")
500
+
501
+ def resume(self, object_id: str) -> Any:
502
+ """Resume syncing a paused object."""
503
+ return self._request("POST", f"{_V2}/objects/{_seg(object_id)}/resume")
504
+
505
+ def reextract(self, object_id: str, **body: Any) -> Any:
506
+ """Run extraction again over content already fetched.
507
+
508
+ Counts against the plan's add allowance, exactly like the first
509
+ extraction, because it creates memories the same way.
510
+ """
511
+ return self._request(
512
+ "POST", f"{_V2}/objects/{_seg(object_id)}/reextract", json=body or None
513
+ )
514
+
515
+ def resync(self, object_id: str, **body: Any) -> Any:
516
+ """Fetch this object from the provider again, then extract."""
517
+ return self._request(
518
+ "POST", f"{_V2}/objects/{_seg(object_id)}/resync", json=body or None
519
+ )
520
+
521
+ def delete_memories(self, object_id: str, **params: Any) -> Any:
522
+ """Remove the memories this object produced, keeping the object record."""
523
+ return self._request(
524
+ "DELETE", f"{_V2}/objects/{_seg(object_id)}/memories", params=params or None
525
+ )
526
+
527
+
528
+ # ─────────────────────────────────────────────────────────────────────
529
+ # Providers and sync jobs
530
+ # ─────────────────────────────────────────────────────────────────────
531
+
532
+
533
+ class ProvidersNamespace(_Namespace):
534
+ """Connectors this deployment supports. Awaitable on the async client."""
535
+
536
+ def list(self, **params: Any) -> Any:
537
+ """Every available provider and what it needs to connect."""
538
+ return self._request("GET", f"{_V2}/providers", params=params or None)
539
+
540
+ def get(self, provider_id: str) -> Any:
541
+ """One provider's capabilities, scopes and settings schema."""
542
+ return self._request("GET", f"{_V2}/providers/{_seg(provider_id)}")
543
+
544
+
545
+ class SyncJobsNamespace(_Namespace):
546
+ """Individual sync runs. Awaitable on the async client."""
547
+
548
+ def get(self, job_id: str) -> Any:
549
+ """Progress and outcome of one sync run."""
550
+ return self._request("GET", f"{_V2}/sync-jobs/{_seg(job_id)}")
551
+
552
+ def cancel(self, job_id: str, **body: Any) -> Any:
553
+ """Stop a running sync. Objects already ingested are kept."""
554
+ return self._request(
555
+ "POST", f"{_V2}/sync-jobs/{_seg(job_id)}/cancel", json=body or None
556
+ )
557
+
558
+
559
+ # ─────────────────────────────────────────────────────────────────────
560
+ # Web crawler
561
+ # ─────────────────────────────────────────────────────────────────────
562
+
563
+
564
+ class WebCrawlerNamespace(_Namespace):
565
+ """Turn websites into memories. Awaitable on the async client."""
566
+
567
+ def validate(self, url: str, **body: Any) -> Any:
568
+ """Check a URL is reachable and crawlable before committing to a job."""
569
+ payload: Dict[str, Any] = {"url": url}
570
+ payload.update(body)
571
+ return self._request("POST", f"{_V1}/web-crawler/validate", json=payload)
572
+
573
+ def crawl(self, url: str, **body: Any) -> Any:
574
+ """Start a crawl. Returns a job to poll.
575
+
576
+ Crawling only fetches and stores page content. Nothing becomes a memory
577
+ until you call :meth:`import_job`, so a large crawl cannot quietly consume
578
+ your add allowance.
579
+ """
580
+ payload: Dict[str, Any] = {"url": url}
581
+ payload.update(body)
582
+ return self._request("POST", f"{_V1}/web-crawler/crawl", json=payload)
583
+
584
+ def jobs(self, **params: Any) -> Any:
585
+ """Crawl jobs for this organization."""
586
+ return self._request("GET", f"{_V1}/web-crawler/jobs", params=params or None)
587
+
588
+ def job(self, job_id: str) -> Any:
589
+ """One crawl job's status and progress."""
590
+ return self._request("GET", f"{_V1}/web-crawler/jobs/{_seg(job_id)}")
591
+
592
+ def cancel_job(self, job_id: str) -> Any:
593
+ """Stop a running crawl. Pages already fetched are kept."""
594
+ return self._request("POST", f"{_V1}/web-crawler/jobs/{_seg(job_id)}/cancel")
595
+
596
+ def delete_job(self, job_id: str) -> Any:
597
+ """Delete a crawl job and its fetched pages."""
598
+ return self._request("DELETE", f"{_V1}/web-crawler/jobs/{_seg(job_id)}")
599
+
600
+ def job_content(self, job_id: str, **params: Any) -> Any:
601
+ """Pages a crawl fetched, before any import."""
602
+ return self._request(
603
+ "GET", f"{_V1}/web-crawler/jobs/{_seg(job_id)}/content", params=params or None
604
+ )
605
+
606
+ def job_statistics(self, job_id: str) -> Any:
607
+ """Page counts, byte totals and error breakdown for a crawl."""
608
+ return self._request("GET", f"{_V1}/web-crawler/jobs/{_seg(job_id)}/statistics")
609
+
610
+ def import_job(self, job_id: str, **body: Any) -> Any:
611
+ """Turn a completed crawl's pages into memories.
612
+
613
+ This is the step that creates memories, so this is the step that is
614
+ billed — one unit per memory created, like every other ingestion path.
615
+ """
616
+ return self._request(
617
+ "POST", f"{_V1}/web-crawler/jobs/{_seg(job_id)}/import", json=body or None
618
+ )
619
+
620
+ def active(self) -> Any:
621
+ """Crawls running right now."""
622
+ return self._request("GET", f"{_V1}/web-crawler/active")
623
+
624
+ def config(self) -> Any:
625
+ """Crawler limits in force: depth, page ceiling, rate, timeouts."""
626
+ return self._request("GET", f"{_V1}/web-crawler/config")
627
+
628
+
629
+ # ─────────────────────────────────────────────────────────────────────
630
+ # Legacy integrations surface
631
+ # ─────────────────────────────────────────────────────────────────────
632
+
633
+
634
+ class IntegrationsNamespace(_Namespace):
635
+ """The ``/api/v1/integrations`` surface. Awaitable on the async client.
636
+
637
+ Kept because the catalog and the web crawler live here and have no v2
638
+ equivalent. For connection lifecycle use ``client.connections``, which is the
639
+ current API — ``connected()`` and ``stats()`` below are the older, thinner
640
+ views of the same data.
641
+ """
642
+
643
+ def __init__(self, request: RequestFn) -> None:
644
+ super().__init__(request)
645
+ self.web_crawler = WebCrawlerNamespace(request)
646
+
647
+ def catalog(self, **params: Any) -> Any:
648
+ """Every integration this deployment offers, for building a picker UI."""
649
+ return self._request("GET", f"{_V1}/catalog", params=params or None)
650
+
651
+ def connected(self, **params: Any) -> Any:
652
+ """Integrations currently connected. Older view of ``connections.list()``."""
653
+ return self._request("GET", f"{_V1}/connected", params=params or None)
654
+
655
+ def stats(self, **params: Any) -> Any:
656
+ """Legacy integration counters. Prefer ``connections.stats()``."""
657
+ return self._request("GET", f"{_V1}/stats", params=params or None)
658
+
659
+ def update(self, integration_id: str, **body: Any) -> Any:
660
+ """Update a legacy integration record."""
661
+ return self._request("PATCH", f"{_V1}/{_seg(integration_id)}", json=body)
662
+
663
+ def delete(self, integration_id: str) -> Any:
664
+ """Delete a legacy integration record."""
665
+ return self._request("DELETE", f"{_V1}/{_seg(integration_id)}")
666
+
667
+
668
+ __all__ = [
669
+ "ConnectionOAuthNamespace",
670
+ "ConnectionsNamespace",
671
+ "GoogleDriveNamespace",
672
+ "GranolaNamespace",
673
+ "IntegrationsNamespace",
674
+ "ObjectsNamespace",
675
+ "ProvidersNamespace",
676
+ "S3Namespace",
677
+ "SlackNamespace",
678
+ "SyncJobsNamespace",
679
+ "WebCrawlerNamespace",
680
+ ]
@@ -1 +0,0 @@
1
- __version__ = "1.2.0"
File without changes
File without changes
File without changes