ando-ai 0.4.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.
ando_ai/__init__.py ADDED
@@ -0,0 +1,147 @@
1
+ """Ando Platform API — official Python SDK (sync, httpx-based).
2
+
3
+ Public surface:
4
+ AndoPlatform — the client (context manager)
5
+ ApiError — envelope errors (stable ``code`` + HTTP ``status``)
6
+ NetworkError — transport failures (DNS/connect/TLS/timeout)
7
+ JobFailed — ingest job ended ``failed`` (carries ``error_code``)
8
+ JobTimeout — ``wait_for_job`` deadline exceeded
9
+ AndoPlatformError — common base class of all of the above
10
+
11
+ Three levels of the same stack (docs/PLATFORM_API_CONTRACT.md):
12
+ ando.query(...) — chunks + scores, you generate
13
+ ando.answer(...) — grounded synthesis over those chunks
14
+ ando.agentic_answer(...) — PREVIEW: the full agent loop (T1/T2/T3 +
15
+ text-to-SQL over published structured
16
+ sources), read-only
17
+
18
+ Higher-order surfaces, all methods on the client:
19
+ ando.generate(...) — render a docx/pptx/xlsx/pdf/md from your data
20
+ (async; wait_for_generation + download_generation)
21
+ ando.cad_analyze(...) / cad_dfm_check / cad_convert / cad_find_similar
22
+ — STEP/DXF analysis, DFM, conversion (async;
23
+ wait_for_cad_job + get_cad_result)
24
+ ando.list_sources() — what this key can query (discovery)
25
+ ando.get_source_schema / curate_source_schema / publish_source_schema
26
+ — the structured-source K-Schema + publish gate
27
+ ando.create_webhook / list_webhooks / delete_webhook / test_webhook
28
+ — outbound webhook endpoints
29
+
30
+ DownloadedFile — a fetched binary artifact (content/content_type/filename,
31
+ with ``.json()`` and ``.save(path)``)
32
+ GenerationFailed — a generation ended ``failed`` (carries ``error_code``)
33
+ GenerationTimeout — ``wait_for_generation`` deadline exceeded
34
+
35
+ Connector Push API (docs/PLATFORM_CONNECTOR_SDK.md):
36
+ AndoConnector — batching/retrying push helper bound to one slug
37
+ SyncSession — one open run; context manager, aborts on exception
38
+ SyncResult — counters of a completed run
39
+ RecordRejected — one refused record (absolute index, field, reason)
40
+ SyncAborted — pushing into an aborted/completed run
41
+ list_connectors — every connector of the project (also
42
+ ``ando.connectors()`` / ``ando.connector(slug)``)
43
+
44
+ Actions write-loop (the symmetric write half):
45
+ AndoActions — ergonomic helper bound to a client (``ando.actions()``);
46
+ ``run(...)`` proposes + executes in one call
47
+ declare_connector_action / list_connector_actions /
48
+ delete_connector_action — the declared-action surface on a connector
49
+ (``binding=`` declares the optional HTTP binding)
50
+ invoke_connector_action — synchronous invoke of an HTTP-bound capability
51
+ (same params validation + risk gate as execute)
52
+ propose_action / execute_action / complete_action /
53
+ get_action / list_actions — the request lifecycle (also as ``ando.*``)
54
+ ActionBinding — the HTTP binding shape ({method, path, body_template?,
55
+ headers?, response?, timeout_seconds?})
56
+ ACTION_RISK_LEVELS — informational risk vocabulary (never validated
57
+ client-side)
58
+ """
59
+
60
+ from ando_ai.actions import (
61
+ ACTION_RISK_LEVELS,
62
+ ActionBinding,
63
+ AndoActions,
64
+ complete_action,
65
+ declare_connector_action,
66
+ delete_connector_action,
67
+ execute_action,
68
+ get_action,
69
+ invoke_connector_action,
70
+ list_actions,
71
+ list_connector_actions,
72
+ propose_action,
73
+ )
74
+ from ando_ai.client import (
75
+ AGENTIC_TIMEOUT,
76
+ DEFAULT_BASE_URL,
77
+ DEFAULT_K,
78
+ FILTER_SOURCES,
79
+ GENERATE_FORMATS,
80
+ MAX_K,
81
+ WEBHOOK_EVENTS,
82
+ AndoPlatform,
83
+ AndoPlatformError,
84
+ ApiError,
85
+ DownloadedFile,
86
+ GenerationFailed,
87
+ GenerationTimeout,
88
+ JobFailed,
89
+ JobTimeout,
90
+ NetworkError,
91
+ )
92
+ from ando_ai.connectors import (
93
+ DEFAULT_BATCH_SIZE,
94
+ MAX_BATCH_SIZE,
95
+ AndoConnector,
96
+ RecordRejected,
97
+ SyncAborted,
98
+ SyncResult,
99
+ SyncSession,
100
+ connector_kinds,
101
+ list_connectors,
102
+ validate_records,
103
+ )
104
+
105
+ __version__ = "0.4.0"
106
+
107
+ __all__ = [
108
+ "AndoPlatform",
109
+ "AndoPlatformError",
110
+ "ApiError",
111
+ "JobFailed",
112
+ "JobTimeout",
113
+ "GenerationFailed",
114
+ "GenerationTimeout",
115
+ "NetworkError",
116
+ "DownloadedFile",
117
+ "DEFAULT_BASE_URL",
118
+ "DEFAULT_K",
119
+ "MAX_K",
120
+ "FILTER_SOURCES",
121
+ "GENERATE_FORMATS",
122
+ "WEBHOOK_EVENTS",
123
+ "AGENTIC_TIMEOUT",
124
+ "AndoConnector",
125
+ "SyncSession",
126
+ "SyncResult",
127
+ "RecordRejected",
128
+ "SyncAborted",
129
+ "list_connectors",
130
+ "connector_kinds",
131
+ "validate_records",
132
+ "DEFAULT_BATCH_SIZE",
133
+ "MAX_BATCH_SIZE",
134
+ "AndoActions",
135
+ "ActionBinding",
136
+ "ACTION_RISK_LEVELS",
137
+ "declare_connector_action",
138
+ "list_connector_actions",
139
+ "delete_connector_action",
140
+ "invoke_connector_action",
141
+ "propose_action",
142
+ "execute_action",
143
+ "complete_action",
144
+ "get_action",
145
+ "list_actions",
146
+ "__version__",
147
+ ]
ando_ai/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """``python -m ando_ai`` → the ``ando`` CLI."""
2
+
3
+ from ando_ai.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())
ando_ai/actions.py ADDED
@@ -0,0 +1,502 @@
1
+ """Actions helper for the Ando Platform write-loop.
2
+
3
+ The Actions API is the symmetric WRITE half of the Push API — where the
4
+ Push API lets a connector send records IN, the Actions API lets a caller
5
+ ask the project's own app to DO something (open a ticket, post a message,
6
+ move a deal) and audit the outcome. Four moves, connector-agnostic and
7
+ white-label by construction (only the developer's own slug + action names
8
+ ever appear):
9
+
10
+ * A connector DECLARES the actions it supports —
11
+ ``POST /connectors/{slug}/actions`` (name + a JSON Schema of params + a
12
+ risk level + optionally an HTTP ``binding``). Requires ``sources:manage``.
13
+ * A caller PROPOSES an action — ``POST /actions`` — params validated
14
+ against the declared schema, persisted with a preview, NO side effect
15
+ yet. Requires ``actions:write``.
16
+ * EXECUTE — ``POST /actions/{id}/execute`` — dispatches a signed
17
+ ``action.requested`` webhook to the project's app. ``high``/``critical``
18
+ actions require ``confirm_risk``. Requires ``actions:write``.
19
+ * The app reports back — ``POST /actions/{id}/complete``. Requires
20
+ ``actions:write``.
21
+
22
+ A capability declared WITH a ``binding`` (:class:`ActionBinding`) also has a
23
+ synchronous path — ``POST /connectors/{slug}/actions/{name}/invoke``
24
+ (:func:`invoke_connector_action`): Ando calls the bound endpoint and returns
25
+ the outcome in one round trip, same params validation, same risk gate.
26
+
27
+ Read paths (``GET /actions``, ``GET /actions/{id}``,
28
+ ``GET /connectors/{slug}/actions``) use ``actions:read``.
29
+
30
+ This module is the thin, DRY glue over that surface: every call goes through
31
+ :meth:`AndoPlatform._request`, so auth, the stable error envelope and the
32
+ transport (including the ``MockTransport`` used in tests) have exactly ONE
33
+ implementation. The module functions take a client as their first argument
34
+ (mirroring :func:`ando.connectors.list_connectors`); the
35
+ :class:`AndoActions` helper binds a client so you drop it, and adds
36
+ :meth:`AndoActions.run` — propose + execute in one call, the write-loop's
37
+ happy path.
38
+
39
+ Usage:
40
+
41
+ from ando_ai import AndoPlatform
42
+
43
+ with AndoPlatform("ando_sk_live_...") as ando:
44
+ actions = ando.actions()
45
+ # One-time, with a sources:manage key:
46
+ actions.declare(
47
+ "acme-desk",
48
+ "create_ticket",
49
+ params_schema={
50
+ "type": "object",
51
+ "properties": {"subject": {"type": "string"}},
52
+ "required": ["subject"],
53
+ },
54
+ risk="medium",
55
+ )
56
+ # Per request, with an actions:write key:
57
+ dispatched = actions.run(
58
+ "acme-desk",
59
+ "create_ticket",
60
+ {"subject": "Refund request"},
61
+ )
62
+ print(dispatched["id"], dispatched["status"]) # ... "dispatched"
63
+ """
64
+
65
+ from __future__ import annotations
66
+
67
+ from typing import Any, Dict, Optional, TypedDict
68
+
69
+ from ando_ai.client import AndoPlatform
70
+
71
+ #: Risk levels a declared action can carry, in ascending order. ``high`` and
72
+ #: ``critical`` require ``confirm_risk`` on execute. Informational ONLY: the
73
+ #: SDK never validates ``risk`` client-side — the server is the authority, so
74
+ #: a server-side addition needs no SDK release (the same stance the client
75
+ #: takes on ``k`` and ``filters.sources``).
76
+ ACTION_RISK_LEVELS: tuple[str, ...] = ("low", "medium", "high", "critical")
77
+
78
+
79
+ class _ActionBindingRequired(TypedDict):
80
+ """The required half of :class:`ActionBinding` (see there)."""
81
+
82
+ method: str
83
+ path: str
84
+
85
+
86
+ class ActionBinding(_ActionBindingRequired, total=False):
87
+ """HTTP binding of a declared capability — how Ando calls YOUR system.
88
+
89
+ Declared alongside the action (:func:`declare_connector_action`
90
+ ``binding=``) and consumed by :func:`invoke_connector_action`. ``method``
91
+ (``POST``/``PUT``/``PATCH``/``GET``/``DELETE``) and ``path`` (relative,
92
+ starts with ``/``, e.g. ``"/orders"``) are required; the rest is optional:
93
+ ``body_template`` (a JSON object with ``{{param}}`` placeholders),
94
+ ``headers``, ``response`` (``{"id_field": "id"}`` — where the created
95
+ resource id lives in your response) and ``timeout_seconds`` (1..60).
96
+ A plain ``dict`` with these keys is accepted wherever an
97
+ :class:`ActionBinding` is — only structure is typed here, the server is
98
+ the authority on the exact rules (the SDK's usual stance).
99
+ """
100
+
101
+ body_template: Dict[str, Any]
102
+ headers: Dict[str, str]
103
+ response: Dict[str, Any]
104
+ timeout_seconds: int
105
+
106
+ #: Header that opts a propose into idempotent replay (see :func:`propose_action`).
107
+ _IDEMPOTENCY_KEY_HEADER = "Idempotency-Key"
108
+
109
+
110
+ # ---------------------------------------------------------------------------
111
+ # Action definitions (declared on a connector) — sources:manage / actions:read
112
+ # ---------------------------------------------------------------------------
113
+
114
+
115
+ def declare_connector_action(
116
+ client: AndoPlatform,
117
+ connector: str,
118
+ name: str,
119
+ description: Optional[str] = None,
120
+ params_schema: Optional[dict] = None,
121
+ risk: Optional[str] = None,
122
+ binding: Optional[ActionBinding] = None,
123
+ ) -> dict:
124
+ """``POST /connectors/{slug}/actions`` — declare an action (sources:manage).
125
+
126
+ Declare (or re-declare — it is idempotent on ``name``) an action the
127
+ connector can perform. ``params_schema`` is the JSON Schema (Draft
128
+ 2020-12) that :func:`propose_action` validates the caller's params
129
+ against; omit it for an unconstrained action. ``risk`` gates the
130
+ confirmation: ``high``/``critical`` need ``confirm_risk`` on execute
131
+ (see :data:`ACTION_RISK_LEVELS`). ``binding`` is the optional HTTP
132
+ binding (:class:`ActionBinding` — ``{method, path, body_template?,
133
+ headers?, response?, timeout_seconds?}``) that lets Ando call your
134
+ system for this capability via :func:`invoke_connector_action`; omit it
135
+ for webhook-only dispatch. A declaration is a FULL replacement:
136
+ re-declaring without ``binding`` clears a stored one. Returns the stored
137
+ definition ``{connector, name, description, params_schema, risk,
138
+ binding, ...}``.
139
+
140
+ Optional fields travel ONLY when set, so a bare declaration puts the
141
+ exact same body on the wire as before they existed.
142
+ """
143
+ if not connector:
144
+ raise ValueError("connector is required")
145
+ if not name:
146
+ raise ValueError("name is required")
147
+ body: Dict[str, Any] = {"name": name}
148
+ if description is not None:
149
+ body["description"] = description
150
+ if params_schema is not None:
151
+ if not isinstance(params_schema, dict):
152
+ raise ValueError("params_schema must be a dict (a JSON Schema object)")
153
+ body["params_schema"] = params_schema
154
+ if risk is not None:
155
+ body["risk"] = risk
156
+ if binding is not None:
157
+ if not isinstance(binding, dict):
158
+ raise ValueError("binding must be a dict (an ActionBinding object)")
159
+ body["binding"] = binding
160
+ return client._request("POST", f"/connectors/{connector}/actions", json=body)
161
+
162
+
163
+ def list_connector_actions(client: AndoPlatform, connector: str) -> dict:
164
+ """``GET /connectors/{slug}/actions`` — a connector's declared actions.
165
+
166
+ Requires ``actions:read``. Returns ``{actions: [...], count}``.
167
+ """
168
+ if not connector:
169
+ raise ValueError("connector is required")
170
+ return client._request("GET", f"/connectors/{connector}/actions")
171
+
172
+
173
+ def delete_connector_action(
174
+ client: AndoPlatform, connector: str, name: str
175
+ ) -> dict:
176
+ """``DELETE /connectors/{slug}/actions/{name}`` — remove a declaration.
177
+
178
+ Requires ``sources:manage``. Returns ``{deleted, connector, name}``.
179
+ """
180
+ if not connector:
181
+ raise ValueError("connector is required")
182
+ if not name:
183
+ raise ValueError("name is required")
184
+ return client._request(
185
+ "DELETE", f"/connectors/{connector}/actions/{name}"
186
+ )
187
+
188
+
189
+ def invoke_connector_action(
190
+ client: AndoPlatform,
191
+ connector: str,
192
+ name: str,
193
+ params: Optional[dict] = None,
194
+ *,
195
+ confirm_risk: bool = False,
196
+ ) -> dict:
197
+ """``POST /connectors/{slug}/actions/{name}/invoke`` — invoke an
198
+ HTTP-bound capability synchronously (actions:write).
199
+
200
+ The synchronous sibling of the propose/execute loop: Ando calls the
201
+ endpoint the capability's declared ``binding`` points at and returns
202
+ the outcome in one round trip — ``{ok, status_code, response}``.
203
+ ``params`` are validated against the declared ``params_schema`` (the
204
+ same seam propose uses).
205
+
206
+ RISK GATE — the same one execute enforces, one vocabulary, one policy:
207
+ a ``high``/``critical``-risk capability is REFUSED (``invalid_request``,
208
+ 400) unless ``confirm_risk=True`` is passed explicitly. With a TEST key
209
+ validation and gating run for real but the bound endpoint is NEVER
210
+ called — the response carries ``test: true`` (the same Twilio semantics
211
+ as execute).
212
+
213
+ Errors: ``invalid_request`` (400 — params fail the schema, missing
214
+ ``confirm_risk``, or the capability declares no binding), ``not_found``
215
+ (404 — unknown connector or action), ``insufficient_scope`` (403).
216
+ """
217
+ if not connector:
218
+ raise ValueError("connector is required")
219
+ if not name:
220
+ raise ValueError("name is required")
221
+ body: Dict[str, Any] = {"confirm_risk": bool(confirm_risk)}
222
+ if params is not None:
223
+ if not isinstance(params, dict):
224
+ raise ValueError("params must be a dict")
225
+ body["params"] = params
226
+ return client._request(
227
+ "POST", f"/connectors/{connector}/actions/{name}/invoke", json=body
228
+ )
229
+
230
+
231
+ # ---------------------------------------------------------------------------
232
+ # Request lifecycle — actions:write (+ resource_policy) / actions:read
233
+ # ---------------------------------------------------------------------------
234
+
235
+
236
+ def propose_action(
237
+ client: AndoPlatform,
238
+ connector: str,
239
+ action: str,
240
+ params: Optional[dict] = None,
241
+ idempotency_key: Optional[str] = None,
242
+ ) -> dict:
243
+ """``POST /actions`` — propose an action (validate + preview, no run).
244
+
245
+ Requires ``actions:write`` AND a resource policy that allows
246
+ ``connector``. ``params`` are validated against the action's declared
247
+ schema and the request is persisted ``proposed`` with a ``preview`` —
248
+ nothing runs on your systems until :func:`execute_action`. Returns the
249
+ ``201`` request ``{id, connector, action, params, status, risk,
250
+ preview, ...}``.
251
+
252
+ Pass ``idempotency_key`` (opaque, <=255 chars) so a retried propose
253
+ (same connector + action + params) REPLAYS the first ``201`` instead of
254
+ creating a duplicate; reusing a key for a DIFFERENT request is a ``409``
255
+ (:class:`~ando_ai.client.ApiError` code ``invalid_request``).
256
+ Omit it and the request is sent exactly as before idempotency existed.
257
+ """
258
+ if not connector:
259
+ raise ValueError("connector is required")
260
+ if not action:
261
+ raise ValueError("action is required")
262
+ body: Dict[str, Any] = {"connector": connector, "action": action}
263
+ if params is not None:
264
+ if not isinstance(params, dict):
265
+ raise ValueError("params must be a dict")
266
+ body["params"] = params
267
+ if idempotency_key is not None:
268
+ if not isinstance(idempotency_key, str) or not idempotency_key:
269
+ raise ValueError("idempotency_key must be a non-empty string")
270
+ return client._request(
271
+ "POST",
272
+ "/actions",
273
+ json=body,
274
+ headers={_IDEMPOTENCY_KEY_HEADER: idempotency_key},
275
+ )
276
+ return client._request("POST", "/actions", json=body)
277
+
278
+
279
+ def execute_action(
280
+ client: AndoPlatform, action_id: str, confirm_risk: bool = False
281
+ ) -> dict:
282
+ """``POST /actions/{id}/execute`` — dispatch a proposed action.
283
+
284
+ Requires ``actions:write`` AND a policy that allows the request's
285
+ connector. Dispatches a signed ``action.requested`` webhook to your app
286
+ and returns the request, now ``dispatched``. ``high``/``critical``-risk
287
+ actions require ``confirm_risk=True``.
288
+
289
+ If no webhook is subscribed to ``action.requested`` the call raises
290
+ :class:`~ando_ai.client.ApiError` (``invalid_request``) and the
291
+ request STAYS ``proposed`` (retryable) — register one via
292
+ ``POST /v1/webhooks``, then execute again.
293
+ """
294
+ if not action_id:
295
+ raise ValueError("action_id is required")
296
+ return client._request(
297
+ "POST",
298
+ f"/actions/{action_id}/execute",
299
+ json={"confirm_risk": bool(confirm_risk)},
300
+ )
301
+
302
+
303
+ def complete_action(
304
+ client: AndoPlatform,
305
+ action_id: str,
306
+ status: str,
307
+ result: Optional[dict] = None,
308
+ error: Optional[str] = None,
309
+ ) -> dict:
310
+ """``POST /actions/{id}/complete`` — report a dispatched action's outcome.
311
+
312
+ Requires ``actions:write`` AND a policy that allows the request's
313
+ connector. ``status`` is ``completed`` or ``failed``; pass ``result``
314
+ (a free-form outcome payload) on success or ``error`` (a human-readable
315
+ reason) on failure. Only a ``dispatched`` request can be completed (no
316
+ double-completes). Returns the terminal request.
317
+ """
318
+ if not action_id:
319
+ raise ValueError("action_id is required")
320
+ if not status:
321
+ raise ValueError("status is required")
322
+ body: Dict[str, Any] = {"status": status}
323
+ if result is not None:
324
+ if not isinstance(result, dict):
325
+ raise ValueError("result must be a dict")
326
+ body["result"] = result
327
+ if error is not None:
328
+ body["error"] = error
329
+ return client._request(
330
+ "POST", f"/actions/{action_id}/complete", json=body
331
+ )
332
+
333
+
334
+ def get_action(client: AndoPlatform, action_id: str) -> dict:
335
+ """``GET /actions/{id}`` — poll one action request (actions:read).
336
+
337
+ Returns the request with its current ``status`` (``proposed`` ->
338
+ ``dispatched`` -> ``completed`` | ``failed``) and detail.
339
+ """
340
+ if not action_id:
341
+ raise ValueError("action_id is required")
342
+ return client._request("GET", f"/actions/{action_id}")
343
+
344
+
345
+ def list_actions(
346
+ client: AndoPlatform,
347
+ connector: Optional[str] = None,
348
+ limit: Optional[int] = None,
349
+ cursor: Optional[str] = None,
350
+ ) -> dict:
351
+ """``GET /actions`` — the project's action requests, newest first.
352
+
353
+ Requires ``actions:read``. Returns ``{actions, count, has_more,
354
+ next_cursor}``. Cursor-paginated: pass ``next_cursor`` back as
355
+ ``cursor`` until ``has_more`` is false. ``connector`` filters to one
356
+ slug. Params travel ONLY when set.
357
+ """
358
+ params: Dict[str, Any] = {}
359
+ if connector is not None:
360
+ params["connector"] = connector
361
+ if limit is not None:
362
+ params["limit"] = limit
363
+ if cursor is not None:
364
+ params["cursor"] = cursor
365
+ return client._request("GET", "/actions", params=params)
366
+
367
+
368
+ class AndoActions:
369
+ """Ergonomic wrapper over the Actions write-loop, bound to one client.
370
+
371
+ Constructed by :meth:`AndoPlatform.actions` (``ando.actions()``), the
372
+ mirror of ``ando.connector(slug)`` for the write half: it binds the
373
+ client so you drop the first argument on every call, and adds
374
+ :meth:`run` — propose + execute in one shot.
375
+
376
+ Args:
377
+ client: A configured :class:`~ando_ai.client.AndoPlatform`.
378
+ """
379
+
380
+ def __init__(self, client: AndoPlatform) -> None:
381
+ self._client = client
382
+
383
+ # -- definitions (sources:manage / actions:read) ------------------------
384
+
385
+ def declare(
386
+ self,
387
+ connector: str,
388
+ name: str,
389
+ description: Optional[str] = None,
390
+ params_schema: Optional[dict] = None,
391
+ risk: Optional[str] = None,
392
+ binding: Optional[ActionBinding] = None,
393
+ ) -> dict:
394
+ """Declare an action on ``connector`` (:func:`declare_connector_action`)."""
395
+ return declare_connector_action(
396
+ self._client,
397
+ connector,
398
+ name,
399
+ description=description,
400
+ params_schema=params_schema,
401
+ risk=risk,
402
+ binding=binding,
403
+ )
404
+
405
+ def definitions(self, connector: str) -> dict:
406
+ """A connector's declared actions (:func:`list_connector_actions`)."""
407
+ return list_connector_actions(self._client, connector)
408
+
409
+ def undeclare(self, connector: str, name: str) -> dict:
410
+ """Remove a declaration (:func:`delete_connector_action`)."""
411
+ return delete_connector_action(self._client, connector, name)
412
+
413
+ def invoke(
414
+ self,
415
+ connector: str,
416
+ name: str,
417
+ params: Optional[dict] = None,
418
+ *,
419
+ confirm_risk: bool = False,
420
+ ) -> dict:
421
+ """Invoke an HTTP-bound capability (:func:`invoke_connector_action`)."""
422
+ return invoke_connector_action(
423
+ self._client,
424
+ connector,
425
+ name,
426
+ params=params,
427
+ confirm_risk=confirm_risk,
428
+ )
429
+
430
+ # -- request lifecycle (actions:write / actions:read) -------------------
431
+
432
+ def propose(
433
+ self,
434
+ connector: str,
435
+ action: str,
436
+ params: Optional[dict] = None,
437
+ idempotency_key: Optional[str] = None,
438
+ ) -> dict:
439
+ """Propose an action (:func:`propose_action`)."""
440
+ return propose_action(
441
+ self._client,
442
+ connector,
443
+ action,
444
+ params=params,
445
+ idempotency_key=idempotency_key,
446
+ )
447
+
448
+ def execute(self, action_id: str, confirm_risk: bool = False) -> dict:
449
+ """Dispatch a proposed action (:func:`execute_action`)."""
450
+ return execute_action(self._client, action_id, confirm_risk=confirm_risk)
451
+
452
+ def complete(
453
+ self,
454
+ action_id: str,
455
+ status: str,
456
+ result: Optional[dict] = None,
457
+ error: Optional[str] = None,
458
+ ) -> dict:
459
+ """Report a dispatched action's outcome (:func:`complete_action`)."""
460
+ return complete_action(
461
+ self._client, action_id, status, result=result, error=error
462
+ )
463
+
464
+ def get(self, action_id: str) -> dict:
465
+ """Poll one request (:func:`get_action`)."""
466
+ return get_action(self._client, action_id)
467
+
468
+ def list(
469
+ self,
470
+ connector: Optional[str] = None,
471
+ limit: Optional[int] = None,
472
+ cursor: Optional[str] = None,
473
+ ) -> dict:
474
+ """List the project's requests (:func:`list_actions`)."""
475
+ return list_actions(
476
+ self._client, connector=connector, limit=limit, cursor=cursor
477
+ )
478
+
479
+ # -- happy path ---------------------------------------------------------
480
+
481
+ def run(
482
+ self,
483
+ connector: str,
484
+ action: str,
485
+ params: Optional[dict] = None,
486
+ confirm_risk: bool = False,
487
+ idempotency_key: Optional[str] = None,
488
+ ) -> dict:
489
+ """Propose then execute in one call — the write-loop's happy path.
490
+
491
+ Returns the ``dispatched`` request. The ``action.requested`` webhook
492
+ is now on its way to your app; the app still reports the outcome via
493
+ :meth:`complete` (or its own webhook handler). ``confirm_risk`` is
494
+ required for ``high``/``critical`` actions, exactly as on
495
+ :meth:`execute`. Pass ``idempotency_key`` to make the PROPOSE half
496
+ idempotent (a retried ``run`` with the same key replays the proposal
497
+ rather than duplicating it before the execute).
498
+ """
499
+ proposed = self.propose(
500
+ connector, action, params=params, idempotency_key=idempotency_key
501
+ )
502
+ return self.execute(proposed["id"], confirm_risk=confirm_risk)