specstream-client 1.0.0__py3-none-any.whl
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.
- specstream_client/__init__.py +14 -0
- specstream_client/__main__.py +3 -0
- specstream_client/authorization.py +32 -0
- specstream_client/cli.py +139 -0
- specstream_client/client.py +1451 -0
- specstream_client/families.py +36 -0
- specstream_client/lifecycle_commands.py +969 -0
- specstream_client/py.typed +0 -0
- specstream_client-1.0.0.dist-info/METADATA +120 -0
- specstream_client-1.0.0.dist-info/RECORD +13 -0
- specstream_client-1.0.0.dist-info/WHEEL +4 -0
- specstream_client-1.0.0.dist-info/entry_points.txt +2 -0
- specstream_client-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1451 @@
|
|
|
1
|
+
"""OpenAPI-aligned SPECSTREAM client: one product, explicit quotes and shared dollar balance."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import urllib.error
|
|
8
|
+
import urllib.request
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from typing import Any, cast
|
|
11
|
+
from urllib.parse import quote as quote_path
|
|
12
|
+
from urllib.parse import urlencode, urljoin, urlsplit
|
|
13
|
+
|
|
14
|
+
from specstream_client.authorization import require_work_authorization
|
|
15
|
+
from specstream_client.families import FAMILIES
|
|
16
|
+
|
|
17
|
+
DEFAULT_BASE_URL = "https://api.specstream.ai"
|
|
18
|
+
USER_AGENT = "specstream-python/1.0.0"
|
|
19
|
+
|
|
20
|
+
OpenFn = Callable[[urllib.request.Request, float], Any]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _NoRedirect(urllib.request.HTTPRedirectHandler):
|
|
24
|
+
def redirect_request(
|
|
25
|
+
self, req: Any, fp: Any, code: int, msg: str, headers: Any, newurl: str
|
|
26
|
+
) -> None:
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SpecstreamError(RuntimeError):
|
|
31
|
+
def __init__(self, message: str, status: int | None = None, body: Any = None) -> None:
|
|
32
|
+
super().__init__(message)
|
|
33
|
+
self.status = status
|
|
34
|
+
self.body = body
|
|
35
|
+
self.code: str | None = None
|
|
36
|
+
self.details: dict[str, Any] = {}
|
|
37
|
+
parsed = body
|
|
38
|
+
if isinstance(parsed, (str, bytes)):
|
|
39
|
+
try:
|
|
40
|
+
parsed = json.loads(parsed)
|
|
41
|
+
except (ValueError, UnicodeError):
|
|
42
|
+
parsed = None
|
|
43
|
+
if isinstance(parsed, dict) and isinstance(parsed.get("error"), dict):
|
|
44
|
+
error = parsed["error"]
|
|
45
|
+
self.code = error.get("code") if isinstance(error.get("code"), str) else None
|
|
46
|
+
if isinstance(error.get("details"), dict):
|
|
47
|
+
self.details = dict(error["details"])
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Client:
|
|
51
|
+
"""Typed HTTP client for finder.cross-source.v1 public operations."""
|
|
52
|
+
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
api_key: str,
|
|
56
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
57
|
+
opener: OpenFn | None = None,
|
|
58
|
+
timeout: float = 60.0,
|
|
59
|
+
*,
|
|
60
|
+
export_origin: str | None = None,
|
|
61
|
+
) -> None:
|
|
62
|
+
key = (api_key or "").strip()
|
|
63
|
+
if not key:
|
|
64
|
+
raise SpecstreamError("SPECSTREAM_API_KEY is required")
|
|
65
|
+
self.api_key = key
|
|
66
|
+
self.base_url = (base_url or DEFAULT_BASE_URL).rstrip("/") + "/"
|
|
67
|
+
self.timeout = timeout
|
|
68
|
+
self.export_origin = export_origin
|
|
69
|
+
self._opener: OpenFn
|
|
70
|
+
if opener is None:
|
|
71
|
+
transport = urllib.request.build_opener(urllib.request.ProxyHandler({}), _NoRedirect())
|
|
72
|
+
self._opener = lambda request, timeout: transport.open(request, timeout=timeout)
|
|
73
|
+
else:
|
|
74
|
+
self._opener = opener
|
|
75
|
+
|
|
76
|
+
def compile(self, q: str, locale: str = "en-US") -> dict[str, Any]:
|
|
77
|
+
"""POST /v1/parse (operationId parseQuery) — compile language into query_ast.v1."""
|
|
78
|
+
data = self._request("POST", "/v1/parse", {"q": q, "locale": locale})
|
|
79
|
+
return data
|
|
80
|
+
|
|
81
|
+
def edit_spec(
|
|
82
|
+
self, ast: dict[str, Any], operations: list[dict[str, Any]] | None = None
|
|
83
|
+
) -> dict[str, Any]:
|
|
84
|
+
"""Validate an edited Spec, retaining required facts and exact target scope."""
|
|
85
|
+
return self._request(
|
|
86
|
+
"POST",
|
|
87
|
+
"/v1/parse/from-chips",
|
|
88
|
+
{"ast": ast, "operations": operations if operations is not None else []},
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def spec_options(self, target: str) -> dict[str, Any]:
|
|
92
|
+
"""Read supported conditions and return facts for a target."""
|
|
93
|
+
return self._request("GET", f"/v1/spec-options/{quote_path(target, safe='')}", None)
|
|
94
|
+
|
|
95
|
+
def revisit(self, find_id: str, *, after: str = "", limit: int = 100) -> dict[str, Any]:
|
|
96
|
+
"""Read exact-target outcomes, including unknown and not-reached records."""
|
|
97
|
+
query = urlencode({"after": after, "limit": limit})
|
|
98
|
+
return self._request(
|
|
99
|
+
"GET", f"/v1/finds/{quote_path(find_id, safe='')}/revisit?{query}", None
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def rows(self, find_id: str, *, cursor: str | None = None, limit: int = 100) -> dict[str, Any]:
|
|
103
|
+
"""Read accepted rows and separate unbilled possible candidates.
|
|
104
|
+
|
|
105
|
+
Follow next_cursor for accepted rows. Possible candidates have open
|
|
106
|
+
conditions, identity-only cells and billable=False; do not count them as Matches.
|
|
107
|
+
"""
|
|
108
|
+
if type(limit) is not int or not 1 <= limit <= 100:
|
|
109
|
+
raise ValueError("Row page limit must be an integer from 1 to 100")
|
|
110
|
+
if cursor is not None and (not isinstance(cursor, str) or not cursor or len(cursor) > 2048):
|
|
111
|
+
raise ValueError("Use the opaque next_cursor returned by this Delivery")
|
|
112
|
+
query = {"limit": str(limit)}
|
|
113
|
+
if cursor is not None:
|
|
114
|
+
query["cursor"] = cursor
|
|
115
|
+
path = f"/v1/finds/{quote_path(find_id, safe='')}/rows?" + urlencode(query)
|
|
116
|
+
return self._request("GET", path, None)
|
|
117
|
+
|
|
118
|
+
def export(self, find_id: str, format: str = "csv") -> dict[str, Any]:
|
|
119
|
+
"""POST /v1/finds/{id}/export (operationId exportFind)."""
|
|
120
|
+
return self._request(
|
|
121
|
+
"POST", f"/v1/finds/{quote_path(find_id, safe='')}/export", {"format": format}
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def download_export(
|
|
125
|
+
self, find_id: str, format: str = "xlsx", *, max_bytes: int = 64 * 1024 * 1024
|
|
126
|
+
) -> bytes:
|
|
127
|
+
"""Fetch a fresh one-use export without forwarding the account bearer.
|
|
128
|
+
|
|
129
|
+
Custom deployments with a separate website origin must set export_origin.
|
|
130
|
+
A failed download is not automatically retried: request a fresh capability.
|
|
131
|
+
"""
|
|
132
|
+
if type(max_bytes) is not int or max_bytes <= 0:
|
|
133
|
+
raise ValueError("max_bytes must be a positive integer")
|
|
134
|
+
data = self.export(find_id, format)
|
|
135
|
+
url = data.get("download_url")
|
|
136
|
+
if not isinstance(url, str) or any(ord(c) <= 32 or c == "\\" for c in url):
|
|
137
|
+
raise SpecstreamError("Export returned an invalid download location")
|
|
138
|
+
try:
|
|
139
|
+
parsed = urlsplit(url)
|
|
140
|
+
base = urlsplit(self.base_url)
|
|
141
|
+
|
|
142
|
+
def origin_of(value: Any) -> tuple[str, str | None, int | None]:
|
|
143
|
+
default = {"https": 443, "http": 80}.get(value.scheme)
|
|
144
|
+
port = value.port if value.port is not None else default
|
|
145
|
+
return value.scheme, value.hostname, port
|
|
146
|
+
|
|
147
|
+
origin = origin_of(parsed)
|
|
148
|
+
allowed = {origin_of(base)}
|
|
149
|
+
if self.export_origin is not None:
|
|
150
|
+
declared = urlsplit(self.export_origin)
|
|
151
|
+
if (
|
|
152
|
+
declared.scheme not in {"http", "https"}
|
|
153
|
+
or not declared.hostname
|
|
154
|
+
or declared.username is not None
|
|
155
|
+
or declared.password is not None
|
|
156
|
+
or declared.query
|
|
157
|
+
or declared.fragment
|
|
158
|
+
or declared.path not in {"", "/"}
|
|
159
|
+
):
|
|
160
|
+
raise ValueError("invalid export origin")
|
|
161
|
+
allowed.add(origin_of(declared))
|
|
162
|
+
elif (
|
|
163
|
+
base.scheme == "https"
|
|
164
|
+
and base.hostname == "api.specstream.ai"
|
|
165
|
+
and base.port in {None, 443}
|
|
166
|
+
):
|
|
167
|
+
allowed.add(("https", "specstream.ai", 443))
|
|
168
|
+
if (
|
|
169
|
+
origin not in allowed
|
|
170
|
+
or parsed.scheme not in {"http", "https"}
|
|
171
|
+
or parsed.username is not None
|
|
172
|
+
or parsed.password is not None
|
|
173
|
+
or parsed.query
|
|
174
|
+
or parsed.fragment
|
|
175
|
+
or not re.fullmatch(r"/export/[A-Za-z0-9_-]+", parsed.path)
|
|
176
|
+
):
|
|
177
|
+
raise ValueError("untrusted download location")
|
|
178
|
+
except ValueError as exc:
|
|
179
|
+
raise SpecstreamError("Export returned an untrusted download location") from exc
|
|
180
|
+
req = urllib.request.Request(
|
|
181
|
+
url,
|
|
182
|
+
method="GET",
|
|
183
|
+
headers={"Accept": "*/*", "Accept-Encoding": "identity", "User-Agent": USER_AGENT},
|
|
184
|
+
)
|
|
185
|
+
try:
|
|
186
|
+
with self._opener(req, self.timeout) as response:
|
|
187
|
+
status = getattr(response, "status", 200)
|
|
188
|
+
if status != 200:
|
|
189
|
+
raise SpecstreamError("Export download failed", status=status)
|
|
190
|
+
payload = response.read(max_bytes + 1)
|
|
191
|
+
if not payload:
|
|
192
|
+
raise SpecstreamError("Export download returned no body", status=status)
|
|
193
|
+
if len(payload) > max_bytes:
|
|
194
|
+
raise SpecstreamError("Export exceeds max_bytes", status=status)
|
|
195
|
+
return bytes(payload)
|
|
196
|
+
except urllib.error.HTTPError as exc:
|
|
197
|
+
# The capability URL and any response body are deliberately omitted.
|
|
198
|
+
raise SpecstreamError("Export download failed", status=exc.code) from None
|
|
199
|
+
except (urllib.error.URLError, OSError):
|
|
200
|
+
raise SpecstreamError("Export download transport failed") from None
|
|
201
|
+
|
|
202
|
+
def get_find(self, find_id: str) -> dict[str, Any]:
|
|
203
|
+
"""GET /v1/finds/{id} (operationId getFind)."""
|
|
204
|
+
return self._request("GET", f"/v1/finds/{quote_path(find_id, safe='')}", None)
|
|
205
|
+
|
|
206
|
+
# BEGIN GENERATED LIFECYCLE
|
|
207
|
+
def get_vocabulary(self) -> dict[str, Any]:
|
|
208
|
+
"""Read the public bar vocabulary and ready-made details."""
|
|
209
|
+
path = "/v1/vocabulary"
|
|
210
|
+
return self._request("GET", path, None)
|
|
211
|
+
|
|
212
|
+
def workspace_list_audit_events(
|
|
213
|
+
self,
|
|
214
|
+
*,
|
|
215
|
+
project_id: str | None = None,
|
|
216
|
+
actor_account_id: str | None = None,
|
|
217
|
+
cursor: str | None = None,
|
|
218
|
+
kind: str | None = None,
|
|
219
|
+
limit: int | None = None,
|
|
220
|
+
) -> dict[str, Any]:
|
|
221
|
+
"""Read immutable twelve-month activity with current owner permissions."""
|
|
222
|
+
path = "/v1/workspace/audit"
|
|
223
|
+
query: dict[str, Any] = {
|
|
224
|
+
"project_id": project_id,
|
|
225
|
+
"actor_account_id": actor_account_id,
|
|
226
|
+
"cursor": cursor,
|
|
227
|
+
"kind": kind,
|
|
228
|
+
"limit": limit,
|
|
229
|
+
}
|
|
230
|
+
encoded = urlencode(
|
|
231
|
+
{
|
|
232
|
+
key: str(value).lower() if isinstance(value, bool) else value
|
|
233
|
+
for key, value in query.items()
|
|
234
|
+
if value is not None
|
|
235
|
+
}
|
|
236
|
+
)
|
|
237
|
+
if encoded:
|
|
238
|
+
path += "?" + encoded
|
|
239
|
+
return self._request("GET", path, None)
|
|
240
|
+
|
|
241
|
+
def understand_live(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
242
|
+
"""Read an exact editor snapshot without saving or authorizing work."""
|
|
243
|
+
path = "/v1/understand"
|
|
244
|
+
return self._request("POST", path, request)
|
|
245
|
+
|
|
246
|
+
def workspace_search(
|
|
247
|
+
self,
|
|
248
|
+
*,
|
|
249
|
+
q: str,
|
|
250
|
+
limit: int | None = None,
|
|
251
|
+
cursor: str | None = None,
|
|
252
|
+
) -> dict[str, Any]:
|
|
253
|
+
"""Search the requests, lists and sources you can open."""
|
|
254
|
+
path = "/v1/search"
|
|
255
|
+
query: dict[str, Any] = {
|
|
256
|
+
"q": q,
|
|
257
|
+
"limit": limit,
|
|
258
|
+
"cursor": cursor,
|
|
259
|
+
}
|
|
260
|
+
encoded = urlencode(
|
|
261
|
+
{
|
|
262
|
+
key: str(value).lower() if isinstance(value, bool) else value
|
|
263
|
+
for key, value in query.items()
|
|
264
|
+
if value is not None
|
|
265
|
+
}
|
|
266
|
+
)
|
|
267
|
+
if encoded:
|
|
268
|
+
path += "?" + encoded
|
|
269
|
+
return self._request("GET", path, None)
|
|
270
|
+
|
|
271
|
+
def workspace_list_notifications(
|
|
272
|
+
self,
|
|
273
|
+
*,
|
|
274
|
+
tab: str | None = None,
|
|
275
|
+
limit: int | None = None,
|
|
276
|
+
cursor: str | None = None,
|
|
277
|
+
) -> dict[str, Any]:
|
|
278
|
+
"""Read your inbox with current resource authorization."""
|
|
279
|
+
path = "/v1/workspace/notifications"
|
|
280
|
+
query: dict[str, Any] = {
|
|
281
|
+
"tab": tab,
|
|
282
|
+
"limit": limit,
|
|
283
|
+
"cursor": cursor,
|
|
284
|
+
}
|
|
285
|
+
encoded = urlencode(
|
|
286
|
+
{
|
|
287
|
+
key: str(value).lower() if isinstance(value, bool) else value
|
|
288
|
+
for key, value in query.items()
|
|
289
|
+
if value is not None
|
|
290
|
+
}
|
|
291
|
+
)
|
|
292
|
+
if encoded:
|
|
293
|
+
path += "?" + encoded
|
|
294
|
+
return self._request("GET", path, None)
|
|
295
|
+
|
|
296
|
+
def workspace_update_notification(
|
|
297
|
+
self,
|
|
298
|
+
identifier: str,
|
|
299
|
+
request: dict[str, Any],
|
|
300
|
+
) -> dict[str, Any]:
|
|
301
|
+
"""Persist read or archive state from your signed-in browser."""
|
|
302
|
+
path = f"/v1/workspace/notifications/{quote_path(identifier, safe='')}"
|
|
303
|
+
return self._request("PATCH", path, request)
|
|
304
|
+
|
|
305
|
+
def workspace_read_notifications(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
306
|
+
"""Mark the supplied authorized notifications read atomically."""
|
|
307
|
+
path = "/v1/workspace/notifications/read"
|
|
308
|
+
return self._request("POST", path, request)
|
|
309
|
+
|
|
310
|
+
def get_me(self) -> dict[str, Any]:
|
|
311
|
+
"""Spend ceiling and remaining_spend authorization state."""
|
|
312
|
+
path = "/v1/me"
|
|
313
|
+
return self._request("GET", path, None)
|
|
314
|
+
|
|
315
|
+
def get_account(self) -> dict[str, Any]:
|
|
316
|
+
"""Paid history."""
|
|
317
|
+
path = "/v1/account"
|
|
318
|
+
return self._request("GET", path, None)
|
|
319
|
+
|
|
320
|
+
def list_account_keys(self) -> dict[str, Any]:
|
|
321
|
+
"""List API keys."""
|
|
322
|
+
path = "/v1/account/keys"
|
|
323
|
+
return self._request("GET", path, None)
|
|
324
|
+
|
|
325
|
+
def create_account_key(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
326
|
+
"""Create ss_live_ or ss_test_ key."""
|
|
327
|
+
path = "/v1/account/keys"
|
|
328
|
+
return self._request("POST", path, request)
|
|
329
|
+
|
|
330
|
+
def update_account_spend(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
331
|
+
"""Set an explicit owner authorization ceiling without funding or spending."""
|
|
332
|
+
path = "/v1/account/spend"
|
|
333
|
+
return self._request("PATCH", path, request)
|
|
334
|
+
|
|
335
|
+
def revoke_account_key(self, key_id: str) -> dict[str, Any]:
|
|
336
|
+
"""Revoke an API key."""
|
|
337
|
+
path = f"/v1/account/keys/{quote_path(key_id, safe='')}"
|
|
338
|
+
return self._request("DELETE", path, None)
|
|
339
|
+
|
|
340
|
+
def get_find_commerce(self, find_id: str) -> dict[str, Any]:
|
|
341
|
+
"""Read payment, fulfillment and access truth."""
|
|
342
|
+
path = f"/v1/finds/{quote_path(find_id, safe='')}/commerce"
|
|
343
|
+
return self._request("GET", path, None)
|
|
344
|
+
|
|
345
|
+
def reconcile_find_commerce(self, find_id: str) -> dict[str, Any]:
|
|
346
|
+
"""Recover the existing purchase without rerunning work."""
|
|
347
|
+
path = f"/v1/finds/{quote_path(find_id, safe='')}/reconcile"
|
|
348
|
+
return self._request("POST", path, None)
|
|
349
|
+
|
|
350
|
+
def cancel_find(self, find_id: str) -> dict[str, Any]:
|
|
351
|
+
"""Request cancel."""
|
|
352
|
+
path = f"/v1/finds/{quote_path(find_id, safe='')}/cancel"
|
|
353
|
+
return self._request("POST", path, None)
|
|
354
|
+
|
|
355
|
+
def share_find(self, find_id: str, request: dict[str, Any]) -> dict[str, Any]:
|
|
356
|
+
"""Snapshot share token."""
|
|
357
|
+
path = f"/v1/finds/{quote_path(find_id, safe='')}/share"
|
|
358
|
+
return self._request("POST", path, request)
|
|
359
|
+
|
|
360
|
+
def revoke_share(self, share_id: str, request: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
361
|
+
"""Revoke snapshot."""
|
|
362
|
+
path = f"/v1/share/{quote_path(share_id, safe='')}/revoke"
|
|
363
|
+
return self._request("POST", path, request)
|
|
364
|
+
|
|
365
|
+
def create_spec(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
366
|
+
"""Save a request as its first revision."""
|
|
367
|
+
path = "/v1/specs"
|
|
368
|
+
return self._request("POST", path, request)
|
|
369
|
+
|
|
370
|
+
def list_specs(
|
|
371
|
+
self,
|
|
372
|
+
*,
|
|
373
|
+
limit: int | None = None,
|
|
374
|
+
cursor: str | None = None,
|
|
375
|
+
project_id: str | None = None,
|
|
376
|
+
archived: bool | None = None,
|
|
377
|
+
) -> dict[str, Any]:
|
|
378
|
+
"""List the saved requests you can open."""
|
|
379
|
+
path = "/v1/specs"
|
|
380
|
+
query: dict[str, Any] = {
|
|
381
|
+
"limit": limit,
|
|
382
|
+
"cursor": cursor,
|
|
383
|
+
"project_id": project_id,
|
|
384
|
+
"archived": archived,
|
|
385
|
+
}
|
|
386
|
+
encoded = urlencode(
|
|
387
|
+
{
|
|
388
|
+
key: str(value).lower() if isinstance(value, bool) else value
|
|
389
|
+
for key, value in query.items()
|
|
390
|
+
if value is not None
|
|
391
|
+
}
|
|
392
|
+
)
|
|
393
|
+
if encoded:
|
|
394
|
+
path += "?" + encoded
|
|
395
|
+
return self._request("GET", path, None)
|
|
396
|
+
|
|
397
|
+
def get_spec(self, spec_id: str) -> dict[str, Any]:
|
|
398
|
+
"""Read a saved request and its current revision."""
|
|
399
|
+
path = f"/v1/specs/{quote_path(spec_id, safe='')}"
|
|
400
|
+
return self._request("GET", path, None)
|
|
401
|
+
|
|
402
|
+
def revise_spec(self, spec_id: str, request: dict[str, Any]) -> dict[str, Any]:
|
|
403
|
+
"""Create an immutable revision only if the expected head still matches."""
|
|
404
|
+
path = f"/v1/specs/{quote_path(spec_id, safe='')}/revisions"
|
|
405
|
+
return self._request("POST", path, request)
|
|
406
|
+
|
|
407
|
+
def list_spec_revisions(
|
|
408
|
+
self,
|
|
409
|
+
spec_id: str,
|
|
410
|
+
*,
|
|
411
|
+
limit: int | None = None,
|
|
412
|
+
cursor: str | None = None,
|
|
413
|
+
) -> dict[str, Any]:
|
|
414
|
+
"""Read a saved request's revision history."""
|
|
415
|
+
path = f"/v1/specs/{quote_path(spec_id, safe='')}/revisions"
|
|
416
|
+
query: dict[str, Any] = {
|
|
417
|
+
"limit": limit,
|
|
418
|
+
"cursor": cursor,
|
|
419
|
+
}
|
|
420
|
+
encoded = urlencode(
|
|
421
|
+
{
|
|
422
|
+
key: str(value).lower() if isinstance(value, bool) else value
|
|
423
|
+
for key, value in query.items()
|
|
424
|
+
if value is not None
|
|
425
|
+
}
|
|
426
|
+
)
|
|
427
|
+
if encoded:
|
|
428
|
+
path += "?" + encoded
|
|
429
|
+
return self._request("GET", path, None)
|
|
430
|
+
|
|
431
|
+
def get_spec_revision(self, spec_id: str, revision_id: str) -> dict[str, Any]:
|
|
432
|
+
"""Read one revision of a saved request."""
|
|
433
|
+
path = (
|
|
434
|
+
"/v1/specs/"
|
|
435
|
+
f"{quote_path(spec_id, safe='')}"
|
|
436
|
+
"/revisions/"
|
|
437
|
+
f"{quote_path(revision_id, safe='')}"
|
|
438
|
+
)
|
|
439
|
+
return self._request("GET", path, None)
|
|
440
|
+
|
|
441
|
+
def archive_spec(self, spec_id: str, request: dict[str, Any]) -> dict[str, Any]:
|
|
442
|
+
"""Archive or restore a saved request."""
|
|
443
|
+
path = f"/v1/specs/{quote_path(spec_id, safe='')}"
|
|
444
|
+
return self._request("PATCH", path, request)
|
|
445
|
+
|
|
446
|
+
def list_spec_finds(
|
|
447
|
+
self,
|
|
448
|
+
spec_id: str,
|
|
449
|
+
*,
|
|
450
|
+
limit: int | None = None,
|
|
451
|
+
cursor: str | None = None,
|
|
452
|
+
) -> dict[str, Any]:
|
|
453
|
+
"""Read the finds a saved request has run."""
|
|
454
|
+
path = f"/v1/specs/{quote_path(spec_id, safe='')}/finds"
|
|
455
|
+
query: dict[str, Any] = {
|
|
456
|
+
"limit": limit,
|
|
457
|
+
"cursor": cursor,
|
|
458
|
+
}
|
|
459
|
+
encoded = urlencode(
|
|
460
|
+
{
|
|
461
|
+
key: str(value).lower() if isinstance(value, bool) else value
|
|
462
|
+
for key, value in query.items()
|
|
463
|
+
if value is not None
|
|
464
|
+
}
|
|
465
|
+
)
|
|
466
|
+
if encoded:
|
|
467
|
+
path += "?" + encoded
|
|
468
|
+
return self._request("GET", path, None)
|
|
469
|
+
|
|
470
|
+
def get_find_lineage(self, find_id: str) -> dict[str, Any]:
|
|
471
|
+
"""Read which request revision a find ran, and the find it continued."""
|
|
472
|
+
path = f"/v1/finds/{quote_path(find_id, safe='')}/lineage"
|
|
473
|
+
return self._request("GET", path, None)
|
|
474
|
+
|
|
475
|
+
def get_delivery(self, find_id: str) -> dict[str, Any]:
|
|
476
|
+
"""Read your finished results with the request revision and sources behind them."""
|
|
477
|
+
path = f"/v1/finds/{quote_path(find_id, safe='')}/delivery"
|
|
478
|
+
return self._request("GET", path, None)
|
|
479
|
+
|
|
480
|
+
def get_find_decisions(
|
|
481
|
+
self,
|
|
482
|
+
find_id: str,
|
|
483
|
+
*,
|
|
484
|
+
limit: int | None = None,
|
|
485
|
+
cursor: str | None = None,
|
|
486
|
+
) -> dict[str, Any]:
|
|
487
|
+
"""Read currently authorized Match decisions."""
|
|
488
|
+
path = f"/v1/finds/{quote_path(find_id, safe='')}/decisions"
|
|
489
|
+
query: dict[str, Any] = {
|
|
490
|
+
"limit": limit,
|
|
491
|
+
"cursor": cursor,
|
|
492
|
+
}
|
|
493
|
+
encoded = urlencode(
|
|
494
|
+
{
|
|
495
|
+
key: str(value).lower() if isinstance(value, bool) else value
|
|
496
|
+
for key, value in query.items()
|
|
497
|
+
if value is not None
|
|
498
|
+
}
|
|
499
|
+
)
|
|
500
|
+
if encoded:
|
|
501
|
+
path += "?" + encoded
|
|
502
|
+
return self._request("GET", path, None)
|
|
503
|
+
|
|
504
|
+
def create_project(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
505
|
+
"""Create a project with an explicit bounded authorization ceiling."""
|
|
506
|
+
path = "/v1/projects"
|
|
507
|
+
return self._request("POST", path, request)
|
|
508
|
+
|
|
509
|
+
def list_projects(
|
|
510
|
+
self,
|
|
511
|
+
*,
|
|
512
|
+
limit: int | None = None,
|
|
513
|
+
cursor: str | None = None,
|
|
514
|
+
) -> dict[str, Any]:
|
|
515
|
+
"""List currently authorized projects."""
|
|
516
|
+
path = "/v1/projects"
|
|
517
|
+
query: dict[str, Any] = {
|
|
518
|
+
"limit": limit,
|
|
519
|
+
"cursor": cursor,
|
|
520
|
+
}
|
|
521
|
+
encoded = urlencode(
|
|
522
|
+
{
|
|
523
|
+
key: str(value).lower() if isinstance(value, bool) else value
|
|
524
|
+
for key, value in query.items()
|
|
525
|
+
if value is not None
|
|
526
|
+
}
|
|
527
|
+
)
|
|
528
|
+
if encoded:
|
|
529
|
+
path += "?" + encoded
|
|
530
|
+
return self._request("GET", path, None)
|
|
531
|
+
|
|
532
|
+
def get_project(self, project_id: str) -> dict[str, Any]:
|
|
533
|
+
"""Read current project membership and authorization."""
|
|
534
|
+
path = f"/v1/projects/{quote_path(project_id, safe='')}"
|
|
535
|
+
return self._request("GET", path, None)
|
|
536
|
+
|
|
537
|
+
def update_project(self, project_id: str, request: dict[str, Any]) -> dict[str, Any]:
|
|
538
|
+
"""Update a project only if the expected version matches."""
|
|
539
|
+
path = f"/v1/projects/{quote_path(project_id, safe='')}"
|
|
540
|
+
return self._request("PATCH", path, request)
|
|
541
|
+
|
|
542
|
+
def put_project_member(self, project_id: str, request: dict[str, Any]) -> dict[str, Any]:
|
|
543
|
+
"""Set a current project member role and bounded spending ceiling."""
|
|
544
|
+
path = f"/v1/projects/{quote_path(project_id, safe='')}/members"
|
|
545
|
+
return self._request("POST", path, request)
|
|
546
|
+
|
|
547
|
+
def list_project_members(
|
|
548
|
+
self,
|
|
549
|
+
project_id: str,
|
|
550
|
+
*,
|
|
551
|
+
limit: int | None = None,
|
|
552
|
+
cursor: str | None = None,
|
|
553
|
+
) -> dict[str, Any]:
|
|
554
|
+
"""List current project membership."""
|
|
555
|
+
path = f"/v1/projects/{quote_path(project_id, safe='')}/members"
|
|
556
|
+
query: dict[str, Any] = {
|
|
557
|
+
"limit": limit,
|
|
558
|
+
"cursor": cursor,
|
|
559
|
+
}
|
|
560
|
+
encoded = urlencode(
|
|
561
|
+
{
|
|
562
|
+
key: str(value).lower() if isinstance(value, bool) else value
|
|
563
|
+
for key, value in query.items()
|
|
564
|
+
if value is not None
|
|
565
|
+
}
|
|
566
|
+
)
|
|
567
|
+
if encoded:
|
|
568
|
+
path += "?" + encoded
|
|
569
|
+
return self._request("GET", path, None)
|
|
570
|
+
|
|
571
|
+
def revoke_project_member(self, project_id: str, account_id: str) -> dict[str, Any]:
|
|
572
|
+
"""Revoke project membership without deleting owned work."""
|
|
573
|
+
path = (
|
|
574
|
+
"/v1/projects/"
|
|
575
|
+
f"{quote_path(project_id, safe='')}"
|
|
576
|
+
"/members/"
|
|
577
|
+
f"{quote_path(account_id, safe='')}"
|
|
578
|
+
)
|
|
579
|
+
return self._request("DELETE", path, None)
|
|
580
|
+
|
|
581
|
+
def list_customer_webhooks(self) -> dict[str, Any]:
|
|
582
|
+
"""List owned callback destinations without secrets."""
|
|
583
|
+
path = "/v1/account/webhooks"
|
|
584
|
+
return self._request("GET", path, None)
|
|
585
|
+
|
|
586
|
+
def create_customer_webhook(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
587
|
+
"""Register find.completed webhook."""
|
|
588
|
+
path = "/v1/account/webhooks"
|
|
589
|
+
return self._request("POST", path, request)
|
|
590
|
+
|
|
591
|
+
def revoke_customer_webhook(self, hook_id: str) -> dict[str, Any]:
|
|
592
|
+
"""Revoke an owned callback and cancel queued deliveries."""
|
|
593
|
+
path = f"/v1/account/webhooks/{quote_path(hook_id, safe='')}"
|
|
594
|
+
return self._request("DELETE", path, None)
|
|
595
|
+
|
|
596
|
+
def get_capacity_catalog(self) -> dict[str, Any]:
|
|
597
|
+
"""Read the current dollar balance and membership offer."""
|
|
598
|
+
path = "/v1/capacity/catalog"
|
|
599
|
+
return self._request("GET", path, None)
|
|
600
|
+
|
|
601
|
+
def get_capacity_balance(self, *, project_id: str | None = None) -> dict[str, Any]:
|
|
602
|
+
"""Read the available balance without approving new work."""
|
|
603
|
+
path = "/v1/capacity/balance"
|
|
604
|
+
query: dict[str, Any] = {
|
|
605
|
+
"project_id": project_id,
|
|
606
|
+
}
|
|
607
|
+
encoded = urlencode(
|
|
608
|
+
{
|
|
609
|
+
key: str(value).lower() if isinstance(value, bool) else value
|
|
610
|
+
for key, value in query.items()
|
|
611
|
+
if value is not None
|
|
612
|
+
}
|
|
613
|
+
)
|
|
614
|
+
if encoded:
|
|
615
|
+
path += "?" + encoded
|
|
616
|
+
return self._request("GET", path, None)
|
|
617
|
+
|
|
618
|
+
def get_capacity_entitlements(self, *, project_id: str | None = None) -> dict[str, Any]:
|
|
619
|
+
"""Read what the account's plan includes and how many members it uses."""
|
|
620
|
+
path = "/v1/capacity/entitlements"
|
|
621
|
+
query: dict[str, Any] = {
|
|
622
|
+
"project_id": project_id,
|
|
623
|
+
}
|
|
624
|
+
encoded = urlencode(
|
|
625
|
+
{
|
|
626
|
+
key: str(value).lower() if isinstance(value, bool) else value
|
|
627
|
+
for key, value in query.items()
|
|
628
|
+
if value is not None
|
|
629
|
+
}
|
|
630
|
+
)
|
|
631
|
+
if encoded:
|
|
632
|
+
path += "?" + encoded
|
|
633
|
+
return self._request("GET", path, None)
|
|
634
|
+
|
|
635
|
+
def get_capacity_referral(self) -> dict[str, Any]:
|
|
636
|
+
"""Read the verified account's stable referral link and rewards."""
|
|
637
|
+
path = "/v1/capacity/referral"
|
|
638
|
+
return self._request("GET", path, None)
|
|
639
|
+
|
|
640
|
+
def capture_capacity_referral(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
641
|
+
"""Remember the first referral visit for sign-up within thirty days."""
|
|
642
|
+
path = "/v1/capacity/referral/visit"
|
|
643
|
+
return self._request("POST", path, request)
|
|
644
|
+
|
|
645
|
+
def get_capacity_history(self) -> dict[str, Any]:
|
|
646
|
+
"""Read account balance events including the reason for each bonus grant."""
|
|
647
|
+
path = "/v1/capacity/history"
|
|
648
|
+
return self._request("GET", path, None)
|
|
649
|
+
|
|
650
|
+
def claim_free_capacity(self) -> dict[str, Any]:
|
|
651
|
+
"""Claim the one-time free balance for a verified eligible account."""
|
|
652
|
+
path = "/v1/capacity/free"
|
|
653
|
+
return self._request("POST", path, None)
|
|
654
|
+
|
|
655
|
+
def set_capacity_budget(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
656
|
+
"""Set an owner-approved monthly account spending maximum."""
|
|
657
|
+
path = "/v1/capacity/budget"
|
|
658
|
+
return self._request("PATCH", path, request)
|
|
659
|
+
|
|
660
|
+
def quote_capacity_work(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
661
|
+
"""Get a quote for a request before anything runs."""
|
|
662
|
+
path = "/v1/capacity/quotes"
|
|
663
|
+
return self._request("POST", path, request)
|
|
664
|
+
|
|
665
|
+
def get_capacity_quote(self, quote_id: str) -> dict[str, Any]:
|
|
666
|
+
"""Recover your signed quote and any find already approved from it."""
|
|
667
|
+
path = f"/v1/capacity/quotes/{quote_path(quote_id, safe='')}"
|
|
668
|
+
return self._request("GET", path, None)
|
|
669
|
+
|
|
670
|
+
def authorize_capacity_work(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
671
|
+
"""
|
|
672
|
+
Approve the exact quote to start one find and reserve its maximum from your balance.
|
|
673
|
+
Needs the specs:write, finds:write and finds:spend scopes.
|
|
674
|
+
"""
|
|
675
|
+
path = "/v1/capacity/executions"
|
|
676
|
+
require_work_authorization(request)
|
|
677
|
+
return self._request("POST", path, request)
|
|
678
|
+
|
|
679
|
+
def get_capacity_request(
|
|
680
|
+
self,
|
|
681
|
+
request_key: str,
|
|
682
|
+
*,
|
|
683
|
+
project_id: str | None = None,
|
|
684
|
+
) -> dict[str, Any]:
|
|
685
|
+
"""Recover the find already started with this request key."""
|
|
686
|
+
path = f"/v1/capacity/requests/{quote_path(request_key, safe='')}"
|
|
687
|
+
query: dict[str, Any] = {
|
|
688
|
+
"project_id": project_id,
|
|
689
|
+
}
|
|
690
|
+
encoded = urlencode(
|
|
691
|
+
{
|
|
692
|
+
key: str(value).lower() if isinstance(value, bool) else value
|
|
693
|
+
for key, value in query.items()
|
|
694
|
+
if value is not None
|
|
695
|
+
}
|
|
696
|
+
)
|
|
697
|
+
if encoded:
|
|
698
|
+
path += "?" + encoded
|
|
699
|
+
return self._request("GET", path, None)
|
|
700
|
+
|
|
701
|
+
def get_capacity_execution(self, find_id: str) -> dict[str, Any]:
|
|
702
|
+
"""Read a find's approved work, final charge and your access to its results."""
|
|
703
|
+
path = f"/v1/capacity/executions/{quote_path(find_id, safe='')}"
|
|
704
|
+
return self._request("GET", path, None)
|
|
705
|
+
|
|
706
|
+
def create_capacity_funding(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
707
|
+
"""Prepare owner-authorized balance or membership checkout without starting work."""
|
|
708
|
+
path = "/v1/capacity/funding"
|
|
709
|
+
return self._request("POST", path, request)
|
|
710
|
+
|
|
711
|
+
def get_capacity_funding(self, funding_id: str) -> dict[str, Any]:
|
|
712
|
+
"""Recover the original funding request from verified provider facts."""
|
|
713
|
+
path = f"/v1/capacity/funding/{quote_path(funding_id, safe='')}"
|
|
714
|
+
return self._request("GET", path, None)
|
|
715
|
+
|
|
716
|
+
def cancel_capacity_funding(self, funding_id: str) -> dict[str, Any]:
|
|
717
|
+
"""Close an unpaid funding request without duplicating payment."""
|
|
718
|
+
path = f"/v1/capacity/funding/{quote_path(funding_id, safe='')}"
|
|
719
|
+
return self._request("DELETE", path, None)
|
|
720
|
+
|
|
721
|
+
def get_capacity_membership(self) -> dict[str, Any]:
|
|
722
|
+
"""Read the current paid period and next-renewal choice."""
|
|
723
|
+
path = "/v1/capacity/membership"
|
|
724
|
+
return self._request("GET", path, None)
|
|
725
|
+
|
|
726
|
+
def change_capacity_membership(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
727
|
+
"""Change, cancel, or resume the membership for its next renewal."""
|
|
728
|
+
path = "/v1/capacity/membership"
|
|
729
|
+
return self._request("POST", path, request)
|
|
730
|
+
|
|
731
|
+
def create_capacity_authority(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
732
|
+
"""Approve a spending limit for one agent or member in a project."""
|
|
733
|
+
path = "/v1/capacity/authorities"
|
|
734
|
+
return self._request("POST", path, request)
|
|
735
|
+
|
|
736
|
+
def revoke_capacity_authority(self, authority_id: str) -> dict[str, Any]:
|
|
737
|
+
"""Revoke a delegated spending limit without removing saved results."""
|
|
738
|
+
path = f"/v1/capacity/authorities/{quote_path(authority_id, safe='')}"
|
|
739
|
+
return self._request("DELETE", path, None)
|
|
740
|
+
|
|
741
|
+
def create_capacity_schedule(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
742
|
+
"""Approve future runs of an exact request within set spending limits."""
|
|
743
|
+
path = "/v1/capacity/schedules"
|
|
744
|
+
return self._request("POST", path, request)
|
|
745
|
+
|
|
746
|
+
def list_capacity_schedules(
|
|
747
|
+
self,
|
|
748
|
+
*,
|
|
749
|
+
project_id: str | None = None,
|
|
750
|
+
after: str | None = None,
|
|
751
|
+
limit: int | None = None,
|
|
752
|
+
) -> dict[str, Any]:
|
|
753
|
+
"""Read authorized recurring work and its current state."""
|
|
754
|
+
path = "/v1/capacity/schedules"
|
|
755
|
+
query: dict[str, Any] = {
|
|
756
|
+
"project_id": project_id,
|
|
757
|
+
"after": after,
|
|
758
|
+
"limit": limit,
|
|
759
|
+
}
|
|
760
|
+
encoded = urlencode(
|
|
761
|
+
{
|
|
762
|
+
key: str(value).lower() if isinstance(value, bool) else value
|
|
763
|
+
for key, value in query.items()
|
|
764
|
+
if value is not None
|
|
765
|
+
}
|
|
766
|
+
)
|
|
767
|
+
if encoded:
|
|
768
|
+
path += "?" + encoded
|
|
769
|
+
return self._request("GET", path, None)
|
|
770
|
+
|
|
771
|
+
def update_capacity_schedule(self, schedule_id: str, request: dict[str, Any]) -> dict[str, Any]:
|
|
772
|
+
"""Pause, resume, or delete recurring work without buying missed occurrences."""
|
|
773
|
+
path = f"/v1/capacity/schedules/{quote_path(schedule_id, safe='')}"
|
|
774
|
+
return self._request("PATCH", path, request)
|
|
775
|
+
|
|
776
|
+
def list_capacity_occurrences(
|
|
777
|
+
self,
|
|
778
|
+
schedule_id: str,
|
|
779
|
+
*,
|
|
780
|
+
before: str | None = None,
|
|
781
|
+
limit: int | None = None,
|
|
782
|
+
) -> dict[str, Any]:
|
|
783
|
+
"""Read completed, blocked, and missed occurrences."""
|
|
784
|
+
path = f"/v1/capacity/schedules/{quote_path(schedule_id, safe='')}/occurrences"
|
|
785
|
+
query: dict[str, Any] = {
|
|
786
|
+
"before": before,
|
|
787
|
+
"limit": limit,
|
|
788
|
+
}
|
|
789
|
+
encoded = urlencode(
|
|
790
|
+
{
|
|
791
|
+
key: str(value).lower() if isinstance(value, bool) else value
|
|
792
|
+
for key, value in query.items()
|
|
793
|
+
if value is not None
|
|
794
|
+
}
|
|
795
|
+
)
|
|
796
|
+
if encoded:
|
|
797
|
+
path += "?" + encoded
|
|
798
|
+
return self._request("GET", path, None)
|
|
799
|
+
|
|
800
|
+
def run_capacity_schedule_now(self, schedule_id: str) -> dict[str, Any]:
|
|
801
|
+
"""
|
|
802
|
+
Run active recurring work once now, inside its own limits, without moving its schedule.
|
|
803
|
+
"""
|
|
804
|
+
path = f"/v1/capacity/schedules/{quote_path(schedule_id, safe='')}/run"
|
|
805
|
+
return self._request("POST", path, None)
|
|
806
|
+
|
|
807
|
+
def create_capacity_refund_claim(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
808
|
+
"""Report a wrong result for review against its sources."""
|
|
809
|
+
path = "/v1/capacity/refund-claims"
|
|
810
|
+
return self._request("POST", path, request)
|
|
811
|
+
|
|
812
|
+
def list_capacity_refund_claims(self) -> dict[str, Any]:
|
|
813
|
+
"""Read the account's correction claims, newest first."""
|
|
814
|
+
path = "/v1/capacity/refund-claims"
|
|
815
|
+
return self._request("GET", path, None)
|
|
816
|
+
|
|
817
|
+
def get_capacity_refund_claim(self, claim_id: str) -> dict[str, Any]:
|
|
818
|
+
"""Read the status of an owned correction claim."""
|
|
819
|
+
path = f"/v1/capacity/refund-claims/{quote_path(claim_id, safe='')}"
|
|
820
|
+
return self._request("GET", path, None)
|
|
821
|
+
|
|
822
|
+
def request_capacity_custom_plan(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
823
|
+
"""Request an Enterprise conversation without granting an entitlement."""
|
|
824
|
+
path = "/v1/capacity/enterprise"
|
|
825
|
+
return self._request("POST", path, request)
|
|
826
|
+
|
|
827
|
+
def get_capacity_store_config(self) -> dict[str, Any]:
|
|
828
|
+
"""Read the app store products and this account's store purchases."""
|
|
829
|
+
path = "/v1/capacity/stores/config"
|
|
830
|
+
return self._request("GET", path, None)
|
|
831
|
+
|
|
832
|
+
def verify_capacity_store_purchase(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
833
|
+
"""Confirm an app store purchase before adding its balance."""
|
|
834
|
+
path = "/v1/capacity/stores/purchases"
|
|
835
|
+
return self._request("POST", path, request)
|
|
836
|
+
|
|
837
|
+
def restore_capacity_store_purchases(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
838
|
+
"""Recover verified purchases without duplicate balance grants."""
|
|
839
|
+
path = "/v1/capacity/stores/restore"
|
|
840
|
+
return self._request("POST", path, request)
|
|
841
|
+
|
|
842
|
+
def transfer_capacity_store_to_card(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
843
|
+
"""Authorize a recoverable Google Play to card billing transfer."""
|
|
844
|
+
path = "/v1/capacity/stores/card-transfer"
|
|
845
|
+
return self._request("POST", path, request)
|
|
846
|
+
|
|
847
|
+
def create_spec_link(self, spec_id: str, request: dict[str, Any]) -> dict[str, Any]:
|
|
848
|
+
"""Share a saved request revision as a link, without its results or permission to spend."""
|
|
849
|
+
path = f"/v1/specs/{quote_path(spec_id, safe='')}/links"
|
|
850
|
+
return self._request("POST", path, request)
|
|
851
|
+
|
|
852
|
+
def get_shared_spec(self, token: str) -> dict[str, Any]:
|
|
853
|
+
"""Read a public request snapshot while its issuer, access and link remain valid."""
|
|
854
|
+
path = f"/v1/shared-specs/{quote_path(token, safe='')}"
|
|
855
|
+
return self._request("GET", path, None)
|
|
856
|
+
|
|
857
|
+
def revoke_spec_link(self, spec_id: str, link_id: str) -> dict[str, Any]:
|
|
858
|
+
"""Turn off a saved-request link without changing the request or its results."""
|
|
859
|
+
path = (
|
|
860
|
+
"/v1/specs/"
|
|
861
|
+
f"{quote_path(spec_id, safe='')}"
|
|
862
|
+
"/links/"
|
|
863
|
+
f"{quote_path(link_id, safe='')}"
|
|
864
|
+
"/revoke"
|
|
865
|
+
)
|
|
866
|
+
return self._request("POST", path, None)
|
|
867
|
+
|
|
868
|
+
def workspace_set_schedule_delivery(
|
|
869
|
+
self,
|
|
870
|
+
schedule_id: str,
|
|
871
|
+
request: dict[str, Any],
|
|
872
|
+
) -> dict[str, Any]:
|
|
873
|
+
"""Set versioned monitor delivery settings without starting work."""
|
|
874
|
+
path = f"/v1/workspace/schedules/{quote_path(schedule_id, safe='')}/delivery"
|
|
875
|
+
return self._request("PATCH", path, request)
|
|
876
|
+
|
|
877
|
+
def workspace_list_collections(
|
|
878
|
+
self,
|
|
879
|
+
*,
|
|
880
|
+
project_id: str | None = None,
|
|
881
|
+
after: str | None = None,
|
|
882
|
+
limit: int | None = None,
|
|
883
|
+
sort: str | None = None,
|
|
884
|
+
) -> dict[str, Any]:
|
|
885
|
+
"""Read the lists you can open, from your finds and imports."""
|
|
886
|
+
path = "/v1/workspace/collections"
|
|
887
|
+
query: dict[str, Any] = {
|
|
888
|
+
"project_id": project_id,
|
|
889
|
+
"after": after,
|
|
890
|
+
"limit": limit,
|
|
891
|
+
"sort": sort,
|
|
892
|
+
}
|
|
893
|
+
encoded = urlencode(
|
|
894
|
+
{
|
|
895
|
+
key: str(value).lower() if isinstance(value, bool) else value
|
|
896
|
+
for key, value in query.items()
|
|
897
|
+
if value is not None
|
|
898
|
+
}
|
|
899
|
+
)
|
|
900
|
+
if encoded:
|
|
901
|
+
path += "?" + encoded
|
|
902
|
+
return self._request("GET", path, None)
|
|
903
|
+
|
|
904
|
+
def workspace_read_collection(self, collection_id: str) -> dict[str, Any]:
|
|
905
|
+
"""Read one list, without rows removed by an opt-out."""
|
|
906
|
+
path = f"/v1/workspace/collections/{quote_path(collection_id, safe='')}"
|
|
907
|
+
return self._request("GET", path, None)
|
|
908
|
+
|
|
909
|
+
def workspace_combine_collections(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
910
|
+
"""Combine your lists into one and keep each row's sources."""
|
|
911
|
+
path = "/v1/workspace/collections/combine"
|
|
912
|
+
return self._request("POST", path, request)
|
|
913
|
+
|
|
914
|
+
def workspace_copy_collection(
|
|
915
|
+
self,
|
|
916
|
+
collection_id: str,
|
|
917
|
+
request: dict[str, Any],
|
|
918
|
+
) -> dict[str, Any]:
|
|
919
|
+
"""Copy rows from a list you can open into a separate list you own."""
|
|
920
|
+
path = f"/v1/workspace/collections/{quote_path(collection_id, safe='')}/copy"
|
|
921
|
+
return self._request("POST", path, request)
|
|
922
|
+
|
|
923
|
+
def workspace_filter_collection(
|
|
924
|
+
self,
|
|
925
|
+
collection_id: str,
|
|
926
|
+
request: dict[str, Any],
|
|
927
|
+
) -> dict[str, Any]:
|
|
928
|
+
"""Filter a list's rows by IDs, sources or a map area."""
|
|
929
|
+
path = f"/v1/workspace/collections/{quote_path(collection_id, safe='')}/filter"
|
|
930
|
+
return self._request("POST", path, request)
|
|
931
|
+
|
|
932
|
+
def workspace_list_views(self, collection_id: str) -> dict[str, Any]:
|
|
933
|
+
"""List your saved views of a list."""
|
|
934
|
+
path = f"/v1/workspace/views/{quote_path(collection_id, safe='')}"
|
|
935
|
+
return self._request("GET", path, None)
|
|
936
|
+
|
|
937
|
+
def workspace_save_view(self, collection_id: str, request: dict[str, Any]) -> dict[str, Any]:
|
|
938
|
+
"""Save a versioned personal view without changing source records."""
|
|
939
|
+
path = f"/v1/workspace/views/{quote_path(collection_id, safe='')}"
|
|
940
|
+
return self._request("PUT", path, request)
|
|
941
|
+
|
|
942
|
+
def workspace_list_stars(self) -> dict[str, Any]:
|
|
943
|
+
"""List starred work that remains currently authorized."""
|
|
944
|
+
path = "/v1/workspace/stars"
|
|
945
|
+
return self._request("GET", path, None)
|
|
946
|
+
|
|
947
|
+
def workspace_star_item(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
948
|
+
"""Star or unstar currently authorized work with an expected version."""
|
|
949
|
+
path = "/v1/workspace/stars"
|
|
950
|
+
return self._request("PUT", path, request)
|
|
951
|
+
|
|
952
|
+
def workspace_list_notes(
|
|
953
|
+
self,
|
|
954
|
+
*,
|
|
955
|
+
project_id: str | None = None,
|
|
956
|
+
collection_id: str | None = None,
|
|
957
|
+
record_id: str | None = None,
|
|
958
|
+
) -> dict[str, Any]:
|
|
959
|
+
"""Read authorized project or collection-record notes."""
|
|
960
|
+
path = "/v1/workspace/notes"
|
|
961
|
+
query: dict[str, Any] = {
|
|
962
|
+
"project_id": project_id,
|
|
963
|
+
"collection_id": collection_id,
|
|
964
|
+
"record_id": record_id,
|
|
965
|
+
}
|
|
966
|
+
encoded = urlencode(
|
|
967
|
+
{
|
|
968
|
+
key: str(value).lower() if isinstance(value, bool) else value
|
|
969
|
+
for key, value in query.items()
|
|
970
|
+
if value is not None
|
|
971
|
+
}
|
|
972
|
+
)
|
|
973
|
+
if encoded:
|
|
974
|
+
path += "?" + encoded
|
|
975
|
+
return self._request("GET", path, None)
|
|
976
|
+
|
|
977
|
+
def workspace_add_note(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
978
|
+
"""Add a versioned note to an authorized project or collection record."""
|
|
979
|
+
path = "/v1/workspace/notes"
|
|
980
|
+
return self._request("POST", path, request)
|
|
981
|
+
|
|
982
|
+
def workspace_resolve_note(self, note_id: str, request: dict[str, Any]) -> dict[str, Any]:
|
|
983
|
+
"""Resolve or reopen an authorized note with an expected version."""
|
|
984
|
+
path = f"/v1/workspace/notes/{quote_path(note_id, safe='')}"
|
|
985
|
+
return self._request("PATCH", path, request)
|
|
986
|
+
|
|
987
|
+
def workspace_edit_note(self, note_id: str, request: dict[str, Any]) -> dict[str, Any]:
|
|
988
|
+
"""Edit a note you wrote with an expected version."""
|
|
989
|
+
path = f"/v1/workspace/notes/{quote_path(note_id, safe='')}"
|
|
990
|
+
return self._request("PUT", path, request)
|
|
991
|
+
|
|
992
|
+
def workspace_delete_note(self, note_id: str, request: dict[str, Any]) -> dict[str, Any]:
|
|
993
|
+
"""Remove an authorized note with an expected version."""
|
|
994
|
+
path = f"/v1/workspace/notes/{quote_path(note_id, safe='')}"
|
|
995
|
+
return self._request("DELETE", path, request)
|
|
996
|
+
|
|
997
|
+
def workspace_register_web_push_subscription(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
998
|
+
"""Register an encrypted browser push subscription for this signed-in account."""
|
|
999
|
+
path = "/v1/workspace/push/subscriptions"
|
|
1000
|
+
return self._request("POST", path, request)
|
|
1001
|
+
|
|
1002
|
+
def workspace_push_capabilities(self) -> dict[str, Any]:
|
|
1003
|
+
"""Read configured notification transports, never credentials."""
|
|
1004
|
+
path = "/v1/workspace/push/capabilities"
|
|
1005
|
+
return self._request("GET", path, None)
|
|
1006
|
+
|
|
1007
|
+
def workspace_list_push_devices(self) -> dict[str, Any]:
|
|
1008
|
+
"""List your registered notification devices."""
|
|
1009
|
+
path = "/v1/workspace/push/devices"
|
|
1010
|
+
return self._request("GET", path, None)
|
|
1011
|
+
|
|
1012
|
+
def workspace_register_push_device(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
1013
|
+
"""Register the current signed-in device for native notifications."""
|
|
1014
|
+
path = "/v1/workspace/push/devices"
|
|
1015
|
+
return self._request("POST", path, request)
|
|
1016
|
+
|
|
1017
|
+
def workspace_revoke_push_device(self, device_id: str) -> dict[str, Any]:
|
|
1018
|
+
"""Stop notifications to a registered device."""
|
|
1019
|
+
path = f"/v1/workspace/push/devices/{quote_path(device_id, safe='')}"
|
|
1020
|
+
return self._request("DELETE", path, None)
|
|
1021
|
+
|
|
1022
|
+
def workspace_list_your_fields(self, collection_id: str) -> dict[str, Any]:
|
|
1023
|
+
"""Read the fields you added to a collection and their values."""
|
|
1024
|
+
path = f"/v1/workspace/collections/{quote_path(collection_id, safe='')}/fields"
|
|
1025
|
+
return self._request("GET", path, None)
|
|
1026
|
+
|
|
1027
|
+
def workspace_save_your_fields(
|
|
1028
|
+
self,
|
|
1029
|
+
collection_id: str,
|
|
1030
|
+
request: dict[str, Any],
|
|
1031
|
+
) -> dict[str, Any]:
|
|
1032
|
+
"""Add, rename or remove your own fields on a collection."""
|
|
1033
|
+
path = f"/v1/workspace/collections/{quote_path(collection_id, safe='')}/fields"
|
|
1034
|
+
return self._request("PUT", path, request)
|
|
1035
|
+
|
|
1036
|
+
def workspace_set_your_field_value(
|
|
1037
|
+
self,
|
|
1038
|
+
collection_id: str,
|
|
1039
|
+
request: dict[str, Any],
|
|
1040
|
+
) -> dict[str, Any]:
|
|
1041
|
+
"""Set or clear one of your fields on selected records."""
|
|
1042
|
+
path = f"/v1/workspace/collections/{quote_path(collection_id, safe='')}/fields/values"
|
|
1043
|
+
return self._request("PUT", path, request)
|
|
1044
|
+
|
|
1045
|
+
def begin_connection_authorization(self, provider: str) -> dict[str, Any]:
|
|
1046
|
+
"""Begin account-bound provider authorization."""
|
|
1047
|
+
path = f"/v1/connections/{quote_path(provider, safe='')}/authorize"
|
|
1048
|
+
return self._request("POST", path, None)
|
|
1049
|
+
|
|
1050
|
+
def disconnect_connection(self, connection_id: str) -> dict[str, Any]:
|
|
1051
|
+
"""Revoke a connected provider."""
|
|
1052
|
+
path = f"/v1/connections/{quote_path(connection_id, safe='')}"
|
|
1053
|
+
return self._request("DELETE", path, None)
|
|
1054
|
+
|
|
1055
|
+
def list_cloud_imports(self) -> dict[str, Any]:
|
|
1056
|
+
"""List daily source checks and import status."""
|
|
1057
|
+
path = "/v1/imports/cloud"
|
|
1058
|
+
return self._request("GET", path, None)
|
|
1059
|
+
|
|
1060
|
+
def list_connected_tools(self) -> dict[str, Any]:
|
|
1061
|
+
"""List supported tables, CRM, automation and chat tools."""
|
|
1062
|
+
path = "/v1/connections/catalog"
|
|
1063
|
+
return self._request("GET", path, None)
|
|
1064
|
+
|
|
1065
|
+
def list_connections(self) -> dict[str, Any]:
|
|
1066
|
+
"""List this account's connected providers."""
|
|
1067
|
+
path = "/v1/connections"
|
|
1068
|
+
return self._request("GET", path, None)
|
|
1069
|
+
|
|
1070
|
+
def list_connection_destinations(
|
|
1071
|
+
self,
|
|
1072
|
+
connection_id: str,
|
|
1073
|
+
*,
|
|
1074
|
+
base_id: str | None = None,
|
|
1075
|
+
cursor: str | None = None,
|
|
1076
|
+
) -> dict[str, Any]:
|
|
1077
|
+
"""List provider-authorized destinations."""
|
|
1078
|
+
path = f"/v1/connections/{quote_path(connection_id, safe='')}/destinations"
|
|
1079
|
+
query: dict[str, Any] = {
|
|
1080
|
+
"base_id": base_id,
|
|
1081
|
+
"cursor": cursor,
|
|
1082
|
+
}
|
|
1083
|
+
encoded = urlencode(
|
|
1084
|
+
{
|
|
1085
|
+
key: str(value).lower() if isinstance(value, bool) else value
|
|
1086
|
+
for key, value in query.items()
|
|
1087
|
+
if value is not None
|
|
1088
|
+
}
|
|
1089
|
+
)
|
|
1090
|
+
if encoded:
|
|
1091
|
+
path += "?" + encoded
|
|
1092
|
+
return self._request("GET", path, None)
|
|
1093
|
+
|
|
1094
|
+
def get_connection_columns(self, connection_id: str, request: dict[str, Any]) -> dict[str, Any]:
|
|
1095
|
+
"""Read destination columns before mapping fields."""
|
|
1096
|
+
path = f"/v1/connections/{quote_path(connection_id, safe='')}/columns"
|
|
1097
|
+
return self._request("POST", path, request)
|
|
1098
|
+
|
|
1099
|
+
def save_connection_destination(
|
|
1100
|
+
self,
|
|
1101
|
+
connection_id: str,
|
|
1102
|
+
request: dict[str, Any],
|
|
1103
|
+
) -> dict[str, Any]:
|
|
1104
|
+
"""Save a provider-verified destination and field mapping."""
|
|
1105
|
+
path = f"/v1/connections/{quote_path(connection_id, safe='')}/destination"
|
|
1106
|
+
return self._request("POST", path, request)
|
|
1107
|
+
|
|
1108
|
+
def send_to_destination(self, connection_id: str, request: dict[str, Any]) -> dict[str, Any]:
|
|
1109
|
+
"""Send selected owned results with durable delivery identities."""
|
|
1110
|
+
path = f"/v1/connections/{quote_path(connection_id, safe='')}/send"
|
|
1111
|
+
return self._request("POST", path, request)
|
|
1112
|
+
|
|
1113
|
+
def reconcile_destination_export(self, connection_id: str, export_id: str) -> dict[str, Any]:
|
|
1114
|
+
"""Check an uncertain delivery without sending it again."""
|
|
1115
|
+
path = (
|
|
1116
|
+
"/v1/connections/"
|
|
1117
|
+
f"{quote_path(connection_id, safe='')}"
|
|
1118
|
+
"/exports/"
|
|
1119
|
+
f"{quote_path(export_id, safe='')}"
|
|
1120
|
+
"/reconcile"
|
|
1121
|
+
)
|
|
1122
|
+
return self._request("POST", path, None)
|
|
1123
|
+
|
|
1124
|
+
def list_destination_exports(self) -> dict[str, Any]:
|
|
1125
|
+
"""List delivery receipts for connected tools."""
|
|
1126
|
+
path = "/v1/connections/exports"
|
|
1127
|
+
return self._request("GET", path, None)
|
|
1128
|
+
|
|
1129
|
+
def create_destination_sync(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
1130
|
+
"""Keep sending new and changed records to this destination."""
|
|
1131
|
+
path = "/v1/destination-syncs"
|
|
1132
|
+
return self._request("POST", path, request)
|
|
1133
|
+
|
|
1134
|
+
def update_destination_sync(self, sync_id: str, request: dict[str, Any]) -> dict[str, Any]:
|
|
1135
|
+
"""Start or stop continuous destination delivery."""
|
|
1136
|
+
path = f"/v1/destination-syncs/{quote_path(sync_id, safe='')}"
|
|
1137
|
+
return self._request("PATCH", path, request)
|
|
1138
|
+
|
|
1139
|
+
def list_destination_syncs(self) -> dict[str, Any]:
|
|
1140
|
+
"""List continuous destination deliveries."""
|
|
1141
|
+
path = "/v1/destination-syncs"
|
|
1142
|
+
return self._request("GET", path, None)
|
|
1143
|
+
|
|
1144
|
+
def preview_cloud_list(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
1145
|
+
"""Read a provider-authorized list before importing."""
|
|
1146
|
+
path = "/v1/imports/cloud/preview"
|
|
1147
|
+
return self._request("POST", path, request)
|
|
1148
|
+
|
|
1149
|
+
def import_cloud_list(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
1150
|
+
"""Import supplied records from a connected table or workbook."""
|
|
1151
|
+
path = "/v1/imports/cloud"
|
|
1152
|
+
return self._request("POST", path, request)
|
|
1153
|
+
|
|
1154
|
+
def update_cloud_import(self, import_id: str, request: dict[str, Any]) -> dict[str, Any]:
|
|
1155
|
+
"""Start or stop daily source checks."""
|
|
1156
|
+
path = f"/v1/imports/cloud/{quote_path(import_id, safe='')}"
|
|
1157
|
+
return self._request("PATCH", path, request)
|
|
1158
|
+
|
|
1159
|
+
def workspace_invite_member(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
1160
|
+
"""Invite a project member under current project management authority."""
|
|
1161
|
+
path = "/v1/workspace/invitations"
|
|
1162
|
+
return self._request("POST", path, request)
|
|
1163
|
+
|
|
1164
|
+
def import_file_command(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
1165
|
+
"""Import supplied CSV or XLSX values without starting research."""
|
|
1166
|
+
path = "/v1/imports/file"
|
|
1167
|
+
return self._request("POST", path, request)
|
|
1168
|
+
|
|
1169
|
+
def workspace_activity(
|
|
1170
|
+
self,
|
|
1171
|
+
*,
|
|
1172
|
+
project_id: str | None = None,
|
|
1173
|
+
cursor: str | None = None,
|
|
1174
|
+
limit: int | None = None,
|
|
1175
|
+
) -> dict[str, Any]:
|
|
1176
|
+
"""Read activity through current account and project permissions."""
|
|
1177
|
+
path = "/v1/activity"
|
|
1178
|
+
query: dict[str, Any] = {
|
|
1179
|
+
"project_id": project_id,
|
|
1180
|
+
"cursor": cursor,
|
|
1181
|
+
"limit": limit,
|
|
1182
|
+
}
|
|
1183
|
+
encoded = urlencode(
|
|
1184
|
+
{
|
|
1185
|
+
key: str(value).lower() if isinstance(value, bool) else value
|
|
1186
|
+
for key, value in query.items()
|
|
1187
|
+
if value is not None
|
|
1188
|
+
}
|
|
1189
|
+
)
|
|
1190
|
+
if encoded:
|
|
1191
|
+
path += "?" + encoded
|
|
1192
|
+
return self._request("GET", path, None)
|
|
1193
|
+
|
|
1194
|
+
def workspace_relationships(
|
|
1195
|
+
self,
|
|
1196
|
+
collection_id: str,
|
|
1197
|
+
*,
|
|
1198
|
+
field_id: str | None = None,
|
|
1199
|
+
) -> dict[str, Any]:
|
|
1200
|
+
"""Group accessible records by actual shared column values."""
|
|
1201
|
+
path = f"/v1/workspace/collections/{quote_path(collection_id, safe='')}/relationships"
|
|
1202
|
+
query: dict[str, Any] = {
|
|
1203
|
+
"field_id": field_id,
|
|
1204
|
+
}
|
|
1205
|
+
encoded = urlencode(
|
|
1206
|
+
{
|
|
1207
|
+
key: str(value).lower() if isinstance(value, bool) else value
|
|
1208
|
+
for key, value in query.items()
|
|
1209
|
+
if value is not None
|
|
1210
|
+
}
|
|
1211
|
+
)
|
|
1212
|
+
if encoded:
|
|
1213
|
+
path += "?" + encoded
|
|
1214
|
+
return self._request("GET", path, None)
|
|
1215
|
+
|
|
1216
|
+
def workspace_save_relationships(
|
|
1217
|
+
self,
|
|
1218
|
+
collection_id: str,
|
|
1219
|
+
request: dict[str, Any],
|
|
1220
|
+
) -> dict[str, Any]:
|
|
1221
|
+
"""Save a personal relationship column with an expected version."""
|
|
1222
|
+
path = f"/v1/workspace/collections/{quote_path(collection_id, safe='')}/relationships"
|
|
1223
|
+
return self._request("PUT", path, request)
|
|
1224
|
+
|
|
1225
|
+
def workspace_report_result(
|
|
1226
|
+
self,
|
|
1227
|
+
collection_id: str,
|
|
1228
|
+
request: dict[str, Any],
|
|
1229
|
+
) -> dict[str, Any]:
|
|
1230
|
+
"""Report a billed result for review within its guarantee period."""
|
|
1231
|
+
path = f"/v1/workspace/collections/{quote_path(collection_id, safe='')}/reports"
|
|
1232
|
+
return self._request("POST", path, request)
|
|
1233
|
+
|
|
1234
|
+
def list_request_templates(self, *, record_type: str | None = None) -> dict[str, Any]:
|
|
1235
|
+
"""List request examples; compile and quote before work."""
|
|
1236
|
+
path = "/v1/request-templates"
|
|
1237
|
+
query: dict[str, Any] = {
|
|
1238
|
+
"record_type": record_type,
|
|
1239
|
+
}
|
|
1240
|
+
encoded = urlencode(
|
|
1241
|
+
{
|
|
1242
|
+
key: str(value).lower() if isinstance(value, bool) else value
|
|
1243
|
+
for key, value in query.items()
|
|
1244
|
+
if value is not None
|
|
1245
|
+
}
|
|
1246
|
+
)
|
|
1247
|
+
if encoded:
|
|
1248
|
+
path += "?" + encoded
|
|
1249
|
+
return self._request("GET", path, None)
|
|
1250
|
+
|
|
1251
|
+
def workspace_collection_changes(self, collection_id: str) -> dict[str, Any]:
|
|
1252
|
+
"""Read source-backed changes against immutable prior results."""
|
|
1253
|
+
path = f"/v1/workspace/collections/{quote_path(collection_id, safe='')}/changes"
|
|
1254
|
+
return self._request("GET", path, None)
|
|
1255
|
+
|
|
1256
|
+
def workspace_list_passkeys(self) -> dict[str, Any]:
|
|
1257
|
+
"""List the current account's registered passkeys."""
|
|
1258
|
+
path = "/v1/workspace/security/passkeys"
|
|
1259
|
+
return self._request("GET", path, None)
|
|
1260
|
+
|
|
1261
|
+
def workspace_register_passkey_options(self) -> dict[str, Any]:
|
|
1262
|
+
"""Start a browser-bound passkey registration ceremony."""
|
|
1263
|
+
path = "/v1/workspace/security/passkeys/options"
|
|
1264
|
+
return self._request("POST", path, None)
|
|
1265
|
+
|
|
1266
|
+
def workspace_register_passkey(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
1267
|
+
"""Verify and register a passkey for the current account."""
|
|
1268
|
+
path = "/v1/workspace/security/passkeys"
|
|
1269
|
+
return self._request("POST", path, request)
|
|
1270
|
+
|
|
1271
|
+
def workspace_name_passkey(self, credential_id: str, request: dict[str, Any]) -> dict[str, Any]:
|
|
1272
|
+
"""Name an account-owned passkey."""
|
|
1273
|
+
path = f"/v1/workspace/security/passkeys/{quote_path(credential_id, safe='')}"
|
|
1274
|
+
return self._request("PATCH", path, request)
|
|
1275
|
+
|
|
1276
|
+
def workspace_delete_passkey(self, credential_id: str) -> dict[str, Any]:
|
|
1277
|
+
"""Revoke an account-owned passkey."""
|
|
1278
|
+
path = f"/v1/workspace/security/passkeys/{quote_path(credential_id, safe='')}"
|
|
1279
|
+
return self._request("DELETE", path, None)
|
|
1280
|
+
|
|
1281
|
+
def workspace_passkey_sign_in_options(self) -> dict[str, Any]:
|
|
1282
|
+
"""Start a browser-bound passkey sign-in ceremony."""
|
|
1283
|
+
path = "/v1/workspace/auth/passkey/options"
|
|
1284
|
+
return self._request("POST", path, None)
|
|
1285
|
+
|
|
1286
|
+
def workspace_passkey_sign_in(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
1287
|
+
"""Verify a passkey assertion and establish a session."""
|
|
1288
|
+
path = "/v1/workspace/auth/passkey/verify"
|
|
1289
|
+
return self._request("POST", path, request)
|
|
1290
|
+
|
|
1291
|
+
def workspace_list_s_s_o_domains(self) -> dict[str, Any]:
|
|
1292
|
+
"""List account-owned verified SSO domains."""
|
|
1293
|
+
path = "/v1/workspace/security/sso"
|
|
1294
|
+
return self._request("GET", path, None)
|
|
1295
|
+
|
|
1296
|
+
def workspace_add_s_s_o_domain(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
1297
|
+
"""Create an account-owned domain verification challenge."""
|
|
1298
|
+
path = "/v1/workspace/security/sso"
|
|
1299
|
+
return self._request("POST", path, request)
|
|
1300
|
+
|
|
1301
|
+
def workspace_verify_s_s_o_domain(self, domain_id: str) -> dict[str, Any]:
|
|
1302
|
+
"""Verify the account's domain through its DNS TXT record."""
|
|
1303
|
+
path = f"/v1/workspace/security/sso/{quote_path(domain_id, safe='')}/verify"
|
|
1304
|
+
return self._request("POST", path, None)
|
|
1305
|
+
|
|
1306
|
+
def workspace_configure_s_s_o(self, domain_id: str, request: dict[str, Any]) -> dict[str, Any]:
|
|
1307
|
+
"""Configure verified SAML metadata for an owned domain."""
|
|
1308
|
+
path = f"/v1/workspace/security/sso/{quote_path(domain_id, safe='')}/provider"
|
|
1309
|
+
return self._request("PUT", path, request)
|
|
1310
|
+
|
|
1311
|
+
def workspace_set_s_s_o_policy(self, domain_id: str, request: dict[str, Any]) -> dict[str, Any]:
|
|
1312
|
+
"""Set SSO enforcement after a successful provider test."""
|
|
1313
|
+
path = f"/v1/workspace/security/sso/{quote_path(domain_id, safe='')}"
|
|
1314
|
+
return self._request("PATCH", path, request)
|
|
1315
|
+
|
|
1316
|
+
def workspace_start_s_s_o(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
1317
|
+
"""Start a single-use browser-bound SAML sign-in."""
|
|
1318
|
+
path = "/v1/workspace/auth/sso/start"
|
|
1319
|
+
return self._request("POST", path, request)
|
|
1320
|
+
|
|
1321
|
+
def workspace_rotate_s_c_i_m_token(self, domain_id: str) -> dict[str, Any]:
|
|
1322
|
+
"""Rotate a verified domain's provisioning credential."""
|
|
1323
|
+
path = f"/v1/workspace/security/sso/{quote_path(domain_id, safe='')}/scim/token"
|
|
1324
|
+
return self._request("POST", path, None)
|
|
1325
|
+
|
|
1326
|
+
def workspace_revoke_s_c_i_m_token(self, domain_id: str) -> dict[str, Any]:
|
|
1327
|
+
"""Revoke a domain's provisioning credential."""
|
|
1328
|
+
path = f"/v1/workspace/security/sso/{quote_path(domain_id, safe='')}/scim/token"
|
|
1329
|
+
return self._request("DELETE", path, None)
|
|
1330
|
+
|
|
1331
|
+
def workspace_read_s_c_i_m_mappings(self, domain_id: str) -> dict[str, Any]:
|
|
1332
|
+
"""Read provisioning mappings, provider groups and token state."""
|
|
1333
|
+
path = f"/v1/workspace/security/sso/{quote_path(domain_id, safe='')}/scim/mappings"
|
|
1334
|
+
return self._request("GET", path, None)
|
|
1335
|
+
|
|
1336
|
+
def workspace_set_s_c_i_m_mappings(
|
|
1337
|
+
self,
|
|
1338
|
+
domain_id: str,
|
|
1339
|
+
request: dict[str, Any],
|
|
1340
|
+
) -> dict[str, Any]:
|
|
1341
|
+
"""Set domain-to-project mappings under current owner authority."""
|
|
1342
|
+
path = f"/v1/workspace/security/sso/{quote_path(domain_id, safe='')}/scim/mappings"
|
|
1343
|
+
return self._request("PUT", path, request)
|
|
1344
|
+
|
|
1345
|
+
def workspace_read_photo(self) -> dict[str, Any]:
|
|
1346
|
+
"""Read the account-owned photo version and private URL."""
|
|
1347
|
+
path = "/v1/workspace/profile/photo"
|
|
1348
|
+
return self._request("GET", path, None)
|
|
1349
|
+
|
|
1350
|
+
def workspace_delete_photo(self) -> dict[str, Any]:
|
|
1351
|
+
"""Remove the current account's private profile photo."""
|
|
1352
|
+
path = "/v1/workspace/profile/photo"
|
|
1353
|
+
return self._request("DELETE", path, None)
|
|
1354
|
+
|
|
1355
|
+
def workspace_start_native_provider(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
1356
|
+
"""Begin native provider authorization bound to an app-held PKCE secret."""
|
|
1357
|
+
path = "/v1/workspace/auth/native/start"
|
|
1358
|
+
return self._request("POST", path, request)
|
|
1359
|
+
|
|
1360
|
+
def workspace_redeem_native_provider(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
1361
|
+
"""Redeem verified provider proof for an app session or connection."""
|
|
1362
|
+
path = "/v1/workspace/auth/native/redeem"
|
|
1363
|
+
return self._request("POST", path, request)
|
|
1364
|
+
|
|
1365
|
+
def workspace_placement_allowance(self) -> dict[str, Any]:
|
|
1366
|
+
"""Read this billing account's UTC monthly free placement allowance."""
|
|
1367
|
+
path = "/v1/workspace/placement/allowance"
|
|
1368
|
+
return self._request("GET", path, None)
|
|
1369
|
+
|
|
1370
|
+
def workspace_quote_placement(self, import_id: str) -> dict[str, Any]:
|
|
1371
|
+
"""Quote the maximum cost of placing imported addresses on the map."""
|
|
1372
|
+
path = f"/v1/workspace/imports/{quote_path(import_id, safe='')}/placement/quote"
|
|
1373
|
+
return self._request("POST", path, None)
|
|
1374
|
+
|
|
1375
|
+
def workspace_start_placement(self, import_id: str, request: dict[str, Any]) -> dict[str, Any]:
|
|
1376
|
+
"""Approve the placement quote and enqueue durable background placement."""
|
|
1377
|
+
path = f"/v1/workspace/imports/{quote_path(import_id, safe='')}/placement"
|
|
1378
|
+
return self._request("POST", path, request)
|
|
1379
|
+
|
|
1380
|
+
def workspace_placement_status(self, import_id: str, placement_id: str) -> dict[str, Any]:
|
|
1381
|
+
"""Read placement progress and per-row geocoding outcomes."""
|
|
1382
|
+
path = (
|
|
1383
|
+
"/v1/workspace/imports/"
|
|
1384
|
+
f"{quote_path(import_id, safe='')}"
|
|
1385
|
+
"/placement/"
|
|
1386
|
+
f"{quote_path(placement_id, safe='')}"
|
|
1387
|
+
)
|
|
1388
|
+
return self._request("GET", path, None)
|
|
1389
|
+
# END GENERATED LIFECYCLE
|
|
1390
|
+
|
|
1391
|
+
def families(self) -> tuple[str, ...]:
|
|
1392
|
+
return FAMILIES
|
|
1393
|
+
|
|
1394
|
+
def _headers(self) -> dict[str, str]:
|
|
1395
|
+
token = self.api_key
|
|
1396
|
+
auth = token if token.lower().startswith("bearer ") else f"Bearer {token}"
|
|
1397
|
+
return {
|
|
1398
|
+
"Authorization": auth,
|
|
1399
|
+
"Accept": "application/json",
|
|
1400
|
+
"Content-Type": "application/json",
|
|
1401
|
+
"User-Agent": USER_AGENT,
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
def _request(self, method: str, path: str, body: dict[str, Any] | None) -> dict[str, Any]:
|
|
1405
|
+
url = urljoin(self.base_url, path.lstrip("/"))
|
|
1406
|
+
payload = None if body is None else json.dumps(body, separators=(",", ":")).encode("utf-8")
|
|
1407
|
+
req = urllib.request.Request(url, data=payload, method=method, headers=self._headers())
|
|
1408
|
+
try:
|
|
1409
|
+
with self._opener(req, self.timeout) as resp:
|
|
1410
|
+
status = getattr(resp, "status", 200)
|
|
1411
|
+
text = resp.read().decode("utf-8", "replace")
|
|
1412
|
+
except urllib.error.HTTPError as exc:
|
|
1413
|
+
text = exc.read().decode("utf-8", "replace") if exc.fp else ""
|
|
1414
|
+
try:
|
|
1415
|
+
parsed: Any = json.loads(text)
|
|
1416
|
+
except ValueError:
|
|
1417
|
+
parsed = text
|
|
1418
|
+
raise SpecstreamError(
|
|
1419
|
+
f"{method} {path} failed with HTTP {exc.code}", status=exc.code, body=parsed
|
|
1420
|
+
) from exc
|
|
1421
|
+
except (urllib.error.URLError, OSError) as exc:
|
|
1422
|
+
raise SpecstreamError(f"{method} {path} transport failed") from exc
|
|
1423
|
+
try:
|
|
1424
|
+
loaded = json.loads(text)
|
|
1425
|
+
except ValueError as exc:
|
|
1426
|
+
raise SpecstreamError(
|
|
1427
|
+
f"{method} {path} returned invalid JSON", status=status, body=text
|
|
1428
|
+
) from exc
|
|
1429
|
+
if not 200 <= status < 300:
|
|
1430
|
+
raise SpecstreamError(
|
|
1431
|
+
f"{method} {path} failed with HTTP {status}", status=status, body=loaded
|
|
1432
|
+
)
|
|
1433
|
+
if not isinstance(loaded, dict):
|
|
1434
|
+
raise SpecstreamError(
|
|
1435
|
+
f"{method} {path} returned non-object JSON", status=status, body=loaded
|
|
1436
|
+
)
|
|
1437
|
+
if loaded.get("error") is not None or not isinstance(loaded.get("data"), dict):
|
|
1438
|
+
raise SpecstreamError(
|
|
1439
|
+
f"{method} {path} returned an invalid success envelope",
|
|
1440
|
+
status=status,
|
|
1441
|
+
body=loaded,
|
|
1442
|
+
)
|
|
1443
|
+
return cast(dict[str, Any], loaded["data"])
|
|
1444
|
+
|
|
1445
|
+
|
|
1446
|
+
__all__ = [
|
|
1447
|
+
"Client",
|
|
1448
|
+
"DEFAULT_BASE_URL",
|
|
1449
|
+
"FAMILIES",
|
|
1450
|
+
"SpecstreamError",
|
|
1451
|
+
]
|