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.
- {memorysync-1.2.0 → memorysync-1.3.0}/PKG-INFO +1 -1
- {memorysync-1.2.0 → memorysync-1.3.0}/pyproject.toml +1 -1
- memorysync-1.3.0/src/memorysync/_version.py +1 -0
- {memorysync-1.2.0 → memorysync-1.3.0}/src/memorysync/client.py +46 -0
- memorysync-1.3.0/src/memorysync/connections.py +680 -0
- memorysync-1.2.0/src/memorysync/_version.py +0 -1
- {memorysync-1.2.0 → memorysync-1.3.0}/.gitignore +0 -0
- {memorysync-1.2.0 → memorysync-1.3.0}/LICENSE +0 -0
- {memorysync-1.2.0 → memorysync-1.3.0}/README.md +0 -0
- {memorysync-1.2.0 → memorysync-1.3.0}/src/memorysync/__init__.py +0 -0
- {memorysync-1.2.0 → memorysync-1.3.0}/src/memorysync/control_plane.py +0 -0
- {memorysync-1.2.0 → memorysync-1.3.0}/src/memorysync/control_plane_types.py +0 -0
- {memorysync-1.2.0 → memorysync-1.3.0}/src/memorysync/errors.py +0 -0
- {memorysync-1.2.0 → memorysync-1.3.0}/src/memorysync/types.py +0 -0
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|