getstack 0.5.1__tar.gz → 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. {getstack-0.5.1 → getstack-0.6.0}/PKG-INFO +1 -1
  2. {getstack-0.5.1 → getstack-0.6.0}/pyproject.toml +1 -1
  3. {getstack-0.5.1 → getstack-0.6.0}/src/getstack/__init__.py +53 -4
  4. getstack-0.6.0/src/getstack/_async/__init__.py +14 -0
  5. getstack-0.6.0/src/getstack/_async/agents.py +53 -0
  6. getstack-0.6.0/src/getstack/_async/audit.py +46 -0
  7. getstack-0.6.0/src/getstack/_async/credentials.py +43 -0
  8. getstack-0.6.0/src/getstack/_async/dropoffs.py +49 -0
  9. getstack-0.6.0/src/getstack/_async/identity.py +92 -0
  10. getstack-0.6.0/src/getstack/_async/intents.py +128 -0
  11. getstack-0.6.0/src/getstack/_async/notifications.py +49 -0
  12. getstack-0.6.0/src/getstack/_async/passports.py +364 -0
  13. getstack-0.6.0/src/getstack/_async/proxy.py +73 -0
  14. getstack-0.6.0/src/getstack/_async/reviews.py +53 -0
  15. getstack-0.6.0/src/getstack/_async/scan.py +40 -0
  16. getstack-0.6.0/src/getstack/_async/security_events.py +26 -0
  17. getstack-0.6.0/src/getstack/_async/services.py +41 -0
  18. getstack-0.6.0/src/getstack/_async/skills.py +162 -0
  19. {getstack-0.5.1 → getstack-0.6.0}/src/getstack/client.py +47 -13
  20. {getstack-0.5.1 → getstack-0.6.0}/src/getstack/passports.py +7 -3
  21. {getstack-0.5.1 → getstack-0.6.0}/src/getstack/proxy.py +7 -1
  22. {getstack-0.5.1 → getstack-0.6.0}/src/getstack/skills.py +21 -1
  23. {getstack-0.5.1 → getstack-0.6.0}/.gitignore +0 -0
  24. {getstack-0.5.1 → getstack-0.6.0}/CLAUDE.md +0 -0
  25. {getstack-0.5.1 → getstack-0.6.0}/LICENSE +0 -0
  26. {getstack-0.5.1 → getstack-0.6.0}/README.md +0 -0
  27. {getstack-0.5.1 → getstack-0.6.0}/src/getstack/agent_auth.py +0 -0
  28. {getstack-0.5.1 → getstack-0.6.0}/src/getstack/agents.py +0 -0
  29. {getstack-0.5.1 → getstack-0.6.0}/src/getstack/audit.py +0 -0
  30. {getstack-0.5.1 → getstack-0.6.0}/src/getstack/auth.py +0 -0
  31. {getstack-0.5.1 → getstack-0.6.0}/src/getstack/browser_bootstrap.py +0 -0
  32. {getstack-0.5.1 → getstack-0.6.0}/src/getstack/credentials.py +0 -0
  33. {getstack-0.5.1 → getstack-0.6.0}/src/getstack/dropoffs.py +0 -0
  34. {getstack-0.5.1 → getstack-0.6.0}/src/getstack/errors.py +0 -0
  35. {getstack-0.5.1 → getstack-0.6.0}/src/getstack/identity.py +0 -0
  36. {getstack-0.5.1 → getstack-0.6.0}/src/getstack/intents.py +0 -0
  37. {getstack-0.5.1 → getstack-0.6.0}/src/getstack/notifications.py +0 -0
  38. {getstack-0.5.1 → getstack-0.6.0}/src/getstack/py.typed +0 -0
  39. {getstack-0.5.1 → getstack-0.6.0}/src/getstack/reviews.py +0 -0
  40. {getstack-0.5.1 → getstack-0.6.0}/src/getstack/scan.py +0 -0
  41. {getstack-0.5.1 → getstack-0.6.0}/src/getstack/security_events.py +0 -0
  42. {getstack-0.5.1 → getstack-0.6.0}/src/getstack/services.py +0 -0
  43. {getstack-0.5.1 → getstack-0.6.0}/src/getstack/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: getstack
3
- Version: 0.5.1
3
+ Version: 0.6.0
4
4
  Summary: Python SDK for STACK — trust infrastructure for AI agents
5
5
  Project-URL: Homepage, https://getstack.run
6
6
  Project-URL: Documentation, https://getstack.run/docs/sdk
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "getstack"
7
- version = "0.5.1"
7
+ version = "0.6.0"
8
8
  description = "Python SDK for STACK — trust infrastructure for AI agents"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -256,15 +256,31 @@ class Stack:
256
256
  class AsyncStack:
257
257
  """Asynchronous STACK client.
258
258
 
259
- Uses httpx.AsyncClient under the hood. All service methods are sync
260
- (they use the sync HttpClient wrapper) — for true async, the async
261
- client needs async service modules. This provides the async HTTP
262
- transport layer for future async service implementations.
259
+ Sub-chunk 2.4 full parity with Stack. All 14 typed services are
260
+ async-native, plus AsyncMission for the bounded-mission context
261
+ manager. Uses httpx.AsyncClient under the hood.
263
262
 
264
263
  Args:
265
264
  api_key: A STACK API key (``sk_live_...``).
266
265
  base_url: Override the API base URL.
267
266
  timeout: HTTP timeout in seconds.
267
+
268
+ Example::
269
+
270
+ from getstack import AsyncStack
271
+
272
+ async with AsyncStack(api_key="sk_live_...") as stack:
273
+ agents = await stack.agents.list()
274
+ async with stack.passports.mission(
275
+ agent_id=agents[0].id,
276
+ intent="Process invoices",
277
+ services=["slack", "stripe"],
278
+ ) as mission:
279
+ resp = await mission.proxy(
280
+ "stripe", "https://api.stripe.com/v1/charges",
281
+ method="POST", body={"amount": 50},
282
+ _proxy_service=stack.proxy,
283
+ )
268
284
  """
269
285
 
270
286
  def __init__(
@@ -293,6 +309,39 @@ class AsyncStack:
293
309
 
294
310
  self._client = AsyncHttpClient(auth, base_url=base_url, timeout=timeout)
295
311
 
312
+ # Sub-chunk 2.4 — 14 typed async services attached eagerly,
313
+ # matching Stack's posture. Footprint is small; lazy init
314
+ # would only buy us memory we don't need to save.
315
+ from ._async.agents import AsyncAgentService
316
+ from ._async.passports import AsyncPassportService
317
+ from ._async.credentials import AsyncCredentialService
318
+ from ._async.services import AsyncServiceService
319
+ from ._async.dropoffs import AsyncDropoffService
320
+ from ._async.reviews import AsyncReviewService
321
+ from ._async.notifications import AsyncNotificationService
322
+ from ._async.audit import AsyncAuditService
323
+ from ._async.proxy import AsyncProxyService
324
+ from ._async.scan import AsyncScanService
325
+ from ._async.security_events import AsyncSecurityEventService
326
+ from ._async.skills import AsyncSkillService
327
+ from ._async.identity import AsyncIdentityService
328
+ from ._async.intents import AsyncIntentsService
329
+
330
+ self.agents = AsyncAgentService(self._client)
331
+ self.passports = AsyncPassportService(self._client)
332
+ self.credentials = AsyncCredentialService(self._client)
333
+ self.services = AsyncServiceService(self._client)
334
+ self.dropoffs = AsyncDropoffService(self._client)
335
+ self.reviews = AsyncReviewService(self._client)
336
+ self.notifications = AsyncNotificationService(self._client)
337
+ self.audit = AsyncAuditService(self._client)
338
+ self.proxy = AsyncProxyService(self._client)
339
+ self.scan = AsyncScanService(self._client)
340
+ self.security_events = AsyncSecurityEventService(self._client)
341
+ self.skills = AsyncSkillService(self._client)
342
+ self.identity = AsyncIdentityService(self._client)
343
+ self.intents = AsyncIntentsService(self._client)
344
+
296
345
  @classmethod
297
346
  def from_session(cls, session_token: str, **kwargs) -> AsyncStack:
298
347
  """Create an async client authenticated with a session JWT."""
@@ -0,0 +1,14 @@
1
+ """Async service modules (Sub-chunk 2.4 — full parity with sync).
2
+
3
+ Each AsyncXxxService mirrors the sync XxxService surface exactly:
4
+ same method names, same kwargs, same return types. The only
5
+ difference is `async def` and `await self._client.x` instead of
6
+ `self._client.x`. The Mission ergonomic gets an AsyncMission
7
+ context manager that uses asyncio.create_task + asyncio.sleep
8
+ instead of threading.Timer.
9
+
10
+ Import via AsyncStack:
11
+ from getstack import AsyncStack
12
+ async with AsyncStack(api_key="sk_live_...") as stack:
13
+ agents = await stack.agents.list()
14
+ """
@@ -0,0 +1,53 @@
1
+ """Async mirror of AgentService."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from ..client import AsyncHttpClient
8
+ from ..types import Agent
9
+ from ..agents import _to_agent
10
+
11
+
12
+ class AsyncAgentService:
13
+ def __init__(self, client: AsyncHttpClient):
14
+ self._client = client
15
+
16
+ async def register(
17
+ self,
18
+ name: str,
19
+ description: str | None = None,
20
+ accountability_mode: str = "enforced",
21
+ on_warning: str = "notify",
22
+ on_critical: str = "notify",
23
+ ) -> Agent:
24
+ body: dict[str, Any] = {"name": name, "accountability_mode": accountability_mode}
25
+ if description:
26
+ body["description"] = description
27
+ if on_warning != "notify":
28
+ body["on_warning"] = on_warning
29
+ if on_critical != "notify":
30
+ body["on_critical"] = on_critical
31
+ return _to_agent(await self._client.post("/v1/agents/register", json=body))
32
+
33
+ async def get(self, agent_id: str) -> Agent:
34
+ return _to_agent(await self._client.get(f"/v1/agents/{agent_id}"))
35
+
36
+ async def list(self) -> list[Agent]:
37
+ data = await self._client.get("/v1/agents")
38
+ return [_to_agent(a) for a in data]
39
+
40
+ async def update(self, agent_id: str, **kwargs: Any) -> Agent:
41
+ return _to_agent(await self._client.patch(f"/v1/agents/{agent_id}", json=kwargs))
42
+
43
+ async def suspend(self, agent_id: str) -> Agent:
44
+ return await self.update(agent_id, status="suspended")
45
+
46
+ async def activate(self, agent_id: str) -> Agent:
47
+ return await self.update(agent_id, status="active")
48
+
49
+ async def delete(self, agent_id: str) -> dict[str, Any]:
50
+ return await self._client.delete(f"/v1/agents/{agent_id}")
51
+
52
+ async def unblock(self, agent_id: str) -> dict[str, Any]:
53
+ return await self._client.post(f"/v1/agents/{agent_id}/unblock")
@@ -0,0 +1,46 @@
1
+ """Async mirror of AuditService."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from ..client import AsyncHttpClient
8
+
9
+
10
+ class AsyncAuditService:
11
+ def __init__(self, client: AsyncHttpClient):
12
+ self._client = client
13
+
14
+ async def list(
15
+ self,
16
+ limit: int = 20,
17
+ since: int | None = None,
18
+ agent_id: str | None = None,
19
+ passport_jti: str | None = None,
20
+ ) -> dict[str, Any]:
21
+ params: dict[str, str] = {"limit": str(limit)}
22
+ if since is not None:
23
+ params["since"] = str(since)
24
+ if agent_id is not None:
25
+ params["agent_id"] = agent_id
26
+ if passport_jti is not None:
27
+ params["passport_jti"] = passport_jti
28
+ return await self._client.get("/v1/audit", params=params)
29
+
30
+ async def chain_head(self) -> dict[str, Any]:
31
+ return await self._client.get("/v1/audit/chain-head")
32
+
33
+ async def verify_chain(
34
+ self,
35
+ from_date: str | None = None,
36
+ to_date: str | None = None,
37
+ limit: int | None = None,
38
+ ) -> dict[str, Any]:
39
+ params: dict[str, str] = {}
40
+ if from_date is not None:
41
+ params["from"] = from_date
42
+ if to_date is not None:
43
+ params["to"] = to_date
44
+ if limit is not None:
45
+ params["limit"] = str(limit)
46
+ return await self._client.get("/v1/audit/verify-chain", params=params)
@@ -0,0 +1,43 @@
1
+ """Async mirror of CredentialService."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from urllib.parse import quote
6
+
7
+ from ..client import AsyncHttpClient
8
+ from ..types import Credential
9
+
10
+
11
+ class AsyncCredentialService:
12
+ def __init__(self, client: AsyncHttpClient):
13
+ self._client = client
14
+
15
+ async def get(self, provider: str, passport_token: str | None = None) -> Credential:
16
+ extra_headers: dict[str, str] = {}
17
+ if passport_token:
18
+ extra_headers["X-Passport-Token"] = passport_token
19
+ data = await self._client.get(
20
+ f"/v1/credentials/{quote(provider)}",
21
+ extra_headers=extra_headers if extra_headers else None,
22
+ )
23
+ return Credential(
24
+ provider=data["provider"],
25
+ credential=data.get("credential"),
26
+ credentials=data.get("credentials"),
27
+ )
28
+
29
+ async def get_by_connection(
30
+ self, connection_id: str, passport_token: str | None = None
31
+ ) -> Credential:
32
+ extra_headers: dict[str, str] = {}
33
+ if passport_token:
34
+ extra_headers["X-Passport-Token"] = passport_token
35
+ data = await self._client.get(
36
+ f"/v1/credentials/by-connection/{connection_id}",
37
+ extra_headers=extra_headers if extra_headers else None,
38
+ )
39
+ return Credential(
40
+ provider=data["provider"],
41
+ credential=data.get("credential"),
42
+ credentials=data.get("credentials"),
43
+ )
@@ -0,0 +1,49 @@
1
+ """Async mirror of DropoffService."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from ..client import AsyncHttpClient
8
+
9
+
10
+ class AsyncDropoffService:
11
+ def __init__(self, client: AsyncHttpClient):
12
+ self._client = client
13
+
14
+ async def create(
15
+ self,
16
+ from_agent_id: str,
17
+ to_agent_id: str,
18
+ schema: dict[str, Any],
19
+ ttl_seconds: int | None = None,
20
+ on_expire: str | None = None,
21
+ ) -> dict[str, Any]:
22
+ body: dict[str, Any] = {
23
+ "from_agent": from_agent_id,
24
+ "to_agent": to_agent_id,
25
+ "schema": schema,
26
+ }
27
+ if ttl_seconds:
28
+ body["ttl_seconds"] = ttl_seconds
29
+ if on_expire:
30
+ body["on_expire"] = on_expire
31
+ return await self._client.post("/v1/dropoffs", json=body)
32
+
33
+ async def deposit(self, dropoff_id: str, data: dict[str, Any], agent_id: str) -> dict[str, Any]:
34
+ return await self._client.post(
35
+ f"/v1/dropoffs/{dropoff_id}/deposit",
36
+ json={"agent_id": agent_id, "payload": data},
37
+ )
38
+
39
+ async def collect(self, dropoff_id: str, agent_id: str) -> dict[str, Any]:
40
+ return await self._client.post(
41
+ f"/v1/dropoffs/{dropoff_id}/collect",
42
+ json={"agent_id": agent_id},
43
+ )
44
+
45
+ async def get(self, dropoff_id: str) -> dict[str, Any]:
46
+ return await self._client.get(f"/v1/dropoffs/{dropoff_id}")
47
+
48
+ async def list(self) -> list[dict[str, Any]]:
49
+ return await self._client.get("/v1/dropoffs")
@@ -0,0 +1,92 @@
1
+ """Async mirror of IdentityService."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from ..client import AsyncHttpClient
8
+ from ..types import IdentityProvider, IdentityClaim, IdentitySettings, VerificationSession
9
+ from ..identity import _to_provider, _to_claim
10
+
11
+
12
+ class AsyncIdentityService:
13
+ def __init__(self, client: AsyncHttpClient):
14
+ self._client = client
15
+
16
+ async def get_settings(self) -> IdentitySettings:
17
+ data = await self._client.get("/v1/operators/me/identity-settings")
18
+ return IdentitySettings(
19
+ identity_claim_ttl=data["identity_claim_ttl"],
20
+ identity_claim_inheritance=data["identity_claim_inheritance"],
21
+ identity_auto_revoke=data["identity_auto_revoke"],
22
+ )
23
+
24
+ async def update_settings(
25
+ self,
26
+ *,
27
+ identity_claim_ttl: str | None = None,
28
+ identity_claim_inheritance: str | None = None,
29
+ identity_auto_revoke: bool | None = None,
30
+ ) -> IdentitySettings:
31
+ body: dict[str, Any] = {}
32
+ if identity_claim_ttl is not None:
33
+ body["identity_claim_ttl"] = identity_claim_ttl
34
+ if identity_claim_inheritance is not None:
35
+ body["identity_claim_inheritance"] = identity_claim_inheritance
36
+ if identity_auto_revoke is not None:
37
+ body["identity_auto_revoke"] = identity_auto_revoke
38
+ data = await self._client.patch("/v1/operators/me/identity-settings", json=body)
39
+ return IdentitySettings(
40
+ identity_claim_ttl=data["identity_claim_ttl"],
41
+ identity_claim_inheritance=data["identity_claim_inheritance"],
42
+ identity_auto_revoke=data["identity_auto_revoke"],
43
+ )
44
+
45
+ async def list_providers(self) -> list[IdentityProvider]:
46
+ data = await self._client.get("/v1/identity/providers")
47
+ return [_to_provider(p) for p in data]
48
+
49
+ async def initiate_verification(
50
+ self,
51
+ provider_key: str,
52
+ *,
53
+ requested_claims: list[str] | None = None,
54
+ return_url: str | None = None,
55
+ ) -> VerificationSession:
56
+ body: dict[str, Any] = {"provider_key": provider_key}
57
+ if requested_claims:
58
+ body["requested_claims"] = requested_claims
59
+ if return_url:
60
+ body["return_url"] = return_url
61
+ data = await self._client.post("/v1/identity/verify/initiate", json=body)
62
+ return VerificationSession(
63
+ session_ref=data["session_ref"],
64
+ authorization_url=data.get("authorization_url"),
65
+ status=data.get("status", "pending"),
66
+ )
67
+
68
+ async def complete_verification(self, provider_key: str, session_ref: str) -> dict[str, Any]:
69
+ return await self._client.post(
70
+ "/v1/identity/verify/complete",
71
+ json={"provider_key": provider_key, "session_ref": session_ref},
72
+ )
73
+
74
+ async def grant_delegation(self, delegated_scopes: list[str]) -> IdentityClaim:
75
+ data = await self._client.post(
76
+ "/v1/identity/delegate",
77
+ json={"delegated_scopes": delegated_scopes},
78
+ )
79
+ return _to_claim(data)
80
+
81
+ async def list_claims(self) -> list[IdentityClaim]:
82
+ data = await self._client.get("/v1/identity/claims")
83
+ return [_to_claim(c) for c in data]
84
+
85
+ async def revoke_claim(self, claim_id: str) -> dict[str, Any]:
86
+ return await self._client.delete(f"/v1/identity/claims/{claim_id}")
87
+
88
+ async def get_service_requirements(self, service_id: str) -> dict[str, Any]:
89
+ return await self._client.get(f"/v1/services/{service_id}/requirements")
90
+
91
+ async def set_service_requirement(self, service_id: str, **kwargs: Any) -> dict[str, Any]:
92
+ return await self._client.post(f"/v1/services/{service_id}/requirements", json=kwargs)
@@ -0,0 +1,128 @@
1
+ """Async mirror of IntentsService. asyncio.sleep for the poll loop."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from typing import Any
7
+
8
+ from ..client import AsyncHttpClient
9
+
10
+
11
+ class AsyncIntentsService:
12
+ def __init__(self, client: AsyncHttpClient):
13
+ self._client = client
14
+
15
+ async def simulate(
16
+ self,
17
+ intent: dict[str, Any],
18
+ passport_token: str,
19
+ *,
20
+ intent_claim_ref: str | None = None,
21
+ persist: bool | None = None,
22
+ ) -> dict[str, Any]:
23
+ body: dict[str, Any] = {"intent": intent}
24
+ if intent_claim_ref is not None:
25
+ body["intent_claim_ref"] = intent_claim_ref
26
+ if persist is not None:
27
+ body["persist"] = persist
28
+ return await self._client.request(
29
+ "POST",
30
+ "/v1/intents/simulate",
31
+ json=body,
32
+ extra_headers={"X-Passport-Token": passport_token},
33
+ )
34
+
35
+ async def submit(
36
+ self,
37
+ intent: dict[str, Any],
38
+ passport_token: str,
39
+ *,
40
+ expires_in_seconds: int | None = None,
41
+ skip_simulation: bool = False,
42
+ ) -> dict[str, Any]:
43
+ body: dict[str, Any] = {"intent": intent}
44
+ if expires_in_seconds is not None:
45
+ body["expires_in_seconds"] = expires_in_seconds
46
+ if skip_simulation:
47
+ body["skip_simulation"] = True
48
+ return await self._client.request(
49
+ "POST",
50
+ "/v1/intents/submit",
51
+ json=body,
52
+ extra_headers={"X-Passport-Token": passport_token},
53
+ )
54
+
55
+ async def submit_and_wait(
56
+ self,
57
+ intent: dict[str, Any],
58
+ passport_token: str,
59
+ *,
60
+ expires_in_seconds: int | None = None,
61
+ skip_simulation: bool = False,
62
+ timeout_seconds: float = 60.0,
63
+ poll_interval_seconds: float = 1.0,
64
+ ) -> dict[str, Any]:
65
+ """Async submit + poll. Uses asyncio.sleep instead of time.sleep
66
+ so the event loop stays free during the wait."""
67
+ initial = await self.submit(
68
+ intent,
69
+ passport_token,
70
+ expires_in_seconds=expires_in_seconds,
71
+ skip_simulation=skip_simulation,
72
+ )
73
+ approval_id = initial["approval_id"]
74
+ loop = asyncio.get_event_loop()
75
+ deadline = loop.time() + timeout_seconds
76
+ last_row: dict[str, Any] | None = None
77
+ while loop.time() < deadline:
78
+ row = await self.get(approval_id)
79
+ last_row = row
80
+ if row.get("status") != "pending":
81
+ return {"initial": initial, "final": row, "timed_out": False}
82
+ await asyncio.sleep(poll_interval_seconds)
83
+ return {
84
+ "initial": initial,
85
+ "final": last_row or {"id": approval_id, "status": "pending"},
86
+ "timed_out": True,
87
+ }
88
+
89
+ async def get(self, approval_id: str) -> dict[str, Any]:
90
+ return await self._client.request("GET", f"/v1/intents/{approval_id}")
91
+
92
+ async def list_pending(
93
+ self, *, page: int | None = None, limit: int | None = None
94
+ ) -> dict[str, Any]:
95
+ params: dict[str, Any] = {}
96
+ if page is not None:
97
+ params["page"] = page
98
+ if limit is not None:
99
+ params["limit"] = limit
100
+ return await self._client.request("GET", "/v1/intents/pending", params=params or None)
101
+
102
+ async def approve(self, approval_id: str, *, notes: str | None = None) -> dict[str, Any]:
103
+ body: dict[str, Any] = {}
104
+ if notes is not None:
105
+ body["notes"] = notes
106
+ return await self._client.request(
107
+ "POST",
108
+ f"/v1/intents/{approval_id}/approve",
109
+ json=body,
110
+ )
111
+
112
+ async def reject(
113
+ self,
114
+ approval_id: str,
115
+ *,
116
+ notes: str | None = None,
117
+ block_future: bool = False,
118
+ ) -> dict[str, Any]:
119
+ body: dict[str, Any] = {}
120
+ if notes is not None:
121
+ body["notes"] = notes
122
+ if block_future:
123
+ body["block_future"] = True
124
+ return await self._client.request(
125
+ "POST",
126
+ f"/v1/intents/{approval_id}/reject",
127
+ json=body,
128
+ )
@@ -0,0 +1,49 @@
1
+ """Async mirror of NotificationService."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from ..client import AsyncHttpClient
8
+ from ..types import NotificationChannel
9
+ from ..notifications import _to_channel
10
+
11
+
12
+ class AsyncNotificationService:
13
+ def __init__(self, client: AsyncHttpClient):
14
+ self._client = client
15
+
16
+ async def list(self) -> list[NotificationChannel]:
17
+ data = await self._client.get("/v1/notifications/channels")
18
+ return [_to_channel(ch) for ch in data]
19
+
20
+ async def create(
21
+ self,
22
+ channel_type: str,
23
+ destination: str,
24
+ events: list[str] | None = None,
25
+ min_severity: str = "warning",
26
+ ) -> NotificationChannel:
27
+ body: dict[str, Any] = {
28
+ "channel_type": channel_type,
29
+ "destination": destination,
30
+ "min_severity": min_severity,
31
+ }
32
+ if events:
33
+ body["events"] = events
34
+ return _to_channel(await self._client.post("/v1/notifications/channels", json=body))
35
+
36
+ async def send_verification_code(self, channel_id: str) -> dict[str, Any]:
37
+ return await self._client.post(f"/v1/notifications/channels/{channel_id}/send-code")
38
+
39
+ async def verify(self, channel_id: str, code: str) -> dict[str, Any]:
40
+ return await self._client.post(
41
+ f"/v1/notifications/channels/{channel_id}/verify",
42
+ json={"code": code},
43
+ )
44
+
45
+ async def test(self, channel_id: str) -> dict[str, Any]:
46
+ return await self._client.post(f"/v1/notifications/channels/{channel_id}/test")
47
+
48
+ async def delete(self, channel_id: str) -> dict[str, Any]:
49
+ return await self._client.delete(f"/v1/notifications/channels/{channel_id}")