cadreen-sdk 0.1.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.
cadreen/__init__.py ADDED
@@ -0,0 +1,208 @@
1
+ from .client import CadreenError
2
+ from .types import (
3
+ CadreenConfig,
4
+ HealthStatus,
5
+ ConnectorType,
6
+ CapabilitySource,
7
+ TransportType,
8
+ EscalationStatus,
9
+ CredentialType,
10
+ AtomScope,
11
+ AtomType,
12
+ ErrorCategory,
13
+ RecoveryStrategyType,
14
+ StackItemSource,
15
+ StackItemStatus,
16
+ GovernanceDecisionType,
17
+ RecoveryStatus,
18
+ IntentMode,
19
+ Pagination,
20
+ Pathway,
21
+ ConnectionGroup,
22
+ ListConnectionsResponse,
23
+ AtomContent,
24
+ Atom,
25
+ CreateMemoryResponse,
26
+ SearchMemoryResponse,
27
+ Policy,
28
+ PolicyBundle,
29
+ GovernanceDecision,
30
+ EvaluatePolicyResponse,
31
+ CreatePolicyResponse,
32
+ ConfirmPolicyResponse,
33
+ Escalation,
34
+ ListEscalationsResponse,
35
+ CredentialMetadata,
36
+ ListCredentialsResponse,
37
+ CapabilityMatch,
38
+ ListCapabilitiesResponse,
39
+ ListPoliciesResponse,
40
+ Gap,
41
+ Outcome,
42
+ Assessment,
43
+ PolicyRecommendation,
44
+ StackItem,
45
+ StackBreakdown,
46
+ CapabilityTrace,
47
+ ReasoningTrace,
48
+ MemoryTrace,
49
+ GovernanceTrace,
50
+ HumilityTrace,
51
+ ProcessTrace,
52
+ IntelligenceMeta,
53
+ IntelligenceTraceEntry,
54
+ ListIntelligenceResponse,
55
+ IntelligenceStats,
56
+ IntentMessage,
57
+ IntentContext,
58
+ IntentRequest,
59
+ ResponseMessage,
60
+ TraceExplain,
61
+ ExecutionEvent,
62
+ ExecutionStatus,
63
+ IntentResult,
64
+ IntentResultType,
65
+ DirectResult,
66
+ ClarifyResult,
67
+ ExecutionResult,
68
+ BlockedResult,
69
+ ConnectRequiredResult,
70
+ RegisterOpenAPIRequest,
71
+ RegisterOpenAPIResponse,
72
+ RegisterMCPRequest,
73
+ RegisterMCPResponse,
74
+ InstallComposioRequest,
75
+ SearchMemoryRequest,
76
+ RememberRequest,
77
+ CreatePolicyRequest,
78
+ EvaluatePolicyRequest,
79
+ RequestOptions,
80
+ ConnectResult,
81
+ ConnectResultType,
82
+ ConnectPrebuiltDetail,
83
+ ConnectSchemaRequiredDetail,
84
+ ConnectManualDetail,
85
+ ConnectPathway,
86
+ ConnectUnknownDetail,
87
+ intent_status,
88
+ SetupRequest,
89
+ SetupResult,
90
+ SetupConnection,
91
+ SetupCredential,
92
+ SetupMemory,
93
+ SetupPolicy,
94
+ SetupConnectionResult,
95
+ SetupCredentialResult,
96
+ SetupMemoryResult,
97
+ SetupPolicyResult,
98
+ )
99
+
100
+ from .client import HttpClient
101
+ from .resources.intent import IntentResource
102
+ from .resources.memory import MemoryResource
103
+ from .resources.policies import PoliciesResource
104
+ from .resources.connections import ConnectionsResource
105
+ from .resources.traces import TracesResource
106
+ from .resources.executions import ExecutionsResource
107
+ from .resources.guardrails import GuardrailsResource
108
+ from .redaction import redact_string, redact_trace, redact_messages, RedactOptions
109
+ from .telemetry import TelemetryProvider, TelemetrySpan, TelemetryMeter, OpenTelemetryAdapter, NoOpProvider
110
+
111
+
112
+ class Cadreen:
113
+ def __init__(self, api_key: str, base_url: str | None = None, max_retries: int | None = None, timeout: int | None = None, *, sandbox: bool = False, fixtures: dict | None = None) -> None:
114
+ config = CadreenConfig(api_key=api_key, base_url=base_url, max_retries=max_retries, timeout=timeout, sandbox=sandbox, fixtures=fixtures)
115
+ self._client = HttpClient(config)
116
+ self.intent = IntentResource(self._client)
117
+ self.memory = MemoryResource(self._client)
118
+ self.policies = PoliciesResource(self._client)
119
+ self.connections = ConnectionsResource(self._client)
120
+ self.traces = TracesResource(self._client)
121
+ self.executions = ExecutionsResource(self._client)
122
+ self.guardrails = GuardrailsResource(self.policies)
123
+
124
+ async def invoke(self, request: IntentRequest) -> IntentResult:
125
+ return await self.intent.invoke(request)
126
+
127
+ async def ask(
128
+ self,
129
+ prompt: str,
130
+ *,
131
+ conversation_id: str | None = None,
132
+ context: IntentContext | None = None,
133
+ stream: bool | None = None,
134
+ ) -> IntentResult:
135
+ return await self.intent.invoke(
136
+ IntentRequest(
137
+ messages=[IntentMessage(role="user", content=prompt)],
138
+ mode="chat",
139
+ conversation_id=conversation_id,
140
+ context=context,
141
+ stream=stream,
142
+ )
143
+ )
144
+
145
+ async def act(
146
+ self,
147
+ prompt: str,
148
+ *,
149
+ conversation_id: str | None = None,
150
+ context: IntentContext | None = None,
151
+ stream: bool | None = None,
152
+ ) -> IntentResult:
153
+ return await self.intent.invoke(
154
+ IntentRequest(
155
+ messages=[IntentMessage(role="user", content=prompt)],
156
+ mode="execution",
157
+ conversation_id=conversation_id,
158
+ context=context,
159
+ stream=stream,
160
+ )
161
+ )
162
+
163
+ async def remember(
164
+ self,
165
+ type: str,
166
+ content: dict,
167
+ *,
168
+ domain: str | None = None,
169
+ scope: str | None = None,
170
+ authority: int | None = None,
171
+ tags: list[str] | None = None,
172
+ ) -> CreateMemoryResponse:
173
+ return await self.memory.remember(type, content, domain=domain, scope=scope, authority=authority, tags=tags)
174
+
175
+ async def context(self, request: SearchMemoryRequest) -> SearchMemoryResponse:
176
+ return await self.memory.search(request)
177
+
178
+ async def connect(self, capability: str) -> ConnectResult:
179
+ return await self.connections.connect(capability)
180
+
181
+ async def setup(self, request: SetupRequest) -> SetupResult:
182
+ payload: dict = {}
183
+ if request.connections:
184
+ payload["connections"] = [{"capability": c.capability} for c in request.connections]
185
+ if request.credentials:
186
+ payload["credentials"] = [{"provider": c.provider, "name": c.name, "key_data": c.key_data} for c in request.credentials]
187
+ if request.memory:
188
+ mem_items = []
189
+ for m in request.memory:
190
+ item: dict = {"content": m.content}
191
+ if m.type:
192
+ item["type"] = m.type
193
+ if m.domain:
194
+ item["domain"] = m.domain
195
+ if m.tags:
196
+ item["tags"] = m.tags
197
+ if m.authority:
198
+ item["authority"] = m.authority
199
+ mem_items.append(item)
200
+ payload["memory"] = mem_items
201
+ if request.policies:
202
+ payload["policies"] = [{"name": p.name, "rule": p.rule, "description": p.description, "severity": p.severity} for p in request.policies]
203
+ resp = await self._client.post("/api/v1/cadreen/setup", json=payload)
204
+ conns = [SetupConnectionResult(capability=c["capability"], status=c["status"], detail=c.get("detail"), error=c.get("error")) for c in resp.get("connections", [])]
205
+ creds = [SetupCredentialResult(provider=c["provider"], name=c.get("name", ""), status=c["status"], id=c.get("id"), error=c.get("error")) for c in resp.get("credentials", [])]
206
+ mems = [SetupMemoryResult(id=m["id"], type=m["type"], classified=m["classified"], status=m["status"], kind=m.get("kind"), error=m.get("error")) for m in resp.get("memory", [])]
207
+ pols = [SetupPolicyResult(name=p["name"], status=p["status"], id=p.get("id"), error=p.get("error")) for p in resp.get("policies", [])]
208
+ return SetupResult(connections=conns, credentials=creds, memory=mems, policies=pols, applied=resp["applied"], failed=resp["failed"])
cadreen/client.py ADDED
@@ -0,0 +1,192 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ import uuid
5
+ from typing import Any, Optional
6
+ from urllib.parse import urlencode, quote
7
+
8
+ import httpx
9
+ from httpx_sse import aconnect_sse
10
+
11
+ from .types import CadreenConfig, RequestOptions
12
+ from .telemetry import TelemetryHooks, NoOpProvider
13
+
14
+
15
+ class CadreenError(Exception):
16
+ status: int
17
+ code: str
18
+ error_type: str
19
+ details: Optional[list[dict[str, str]]]
20
+ intelligence: Optional[Any]
21
+
22
+ def __init__(
23
+ self,
24
+ status: int,
25
+ code: str,
26
+ error_type: str,
27
+ message: str,
28
+ details: Optional[list[dict[str, str]]] = None,
29
+ intelligence: Optional[Any] = None,
30
+ ) -> None:
31
+ super().__init__(message)
32
+ self.status = status
33
+ self.code = code
34
+ self.error_type = error_type
35
+ self.details = details
36
+ self.intelligence = intelligence
37
+
38
+
39
+ DEFAULT_BASE_URL = "https://accomplishanything.today"
40
+ DEFAULT_MAX_RETRIES = 2
41
+ DEFAULT_TIMEOUT = 30
42
+ RETRYABLE_STATUS_CODES = {408, 429, 502, 503, 504}
43
+ IDEMPOTENT_METHODS = {"GET", "PUT"}
44
+
45
+
46
+ def _build_query_string(params: dict[str, Any]) -> str:
47
+ parts: list[str] = []
48
+ for key, value in params.items():
49
+ if value is not None and value != "":
50
+ parts.append(f"{quote(key)}={quote(str(value))}")
51
+ return f"?{'&'.join(parts)}" if parts else ""
52
+
53
+
54
+ class HttpClient:
55
+ def __init__(self, config: CadreenConfig) -> None:
56
+ self._base_url = (config.base_url or DEFAULT_BASE_URL).rstrip("/")
57
+ self._api_key = config.api_key
58
+ self._max_retries = config.max_retries if config.max_retries is not None else DEFAULT_MAX_RETRIES
59
+ self._timeout = config.timeout if config.timeout is not None else DEFAULT_TIMEOUT
60
+ self._sandbox = getattr(config, "sandbox", False) or False
61
+ self._fixtures = getattr(config, "fixtures", None) or {}
62
+ self._profile = getattr(config, "profile", None) or "full"
63
+ provider = config.telemetry if config.telemetry else NoOpProvider()
64
+ self._telemetry = TelemetryHooks(provider=provider)
65
+
66
+ async def request(
67
+ self,
68
+ method: str,
69
+ path: str,
70
+ body: Optional[Any] = None,
71
+ options: Optional[RequestOptions] = None,
72
+ ) -> Any:
73
+ if self._sandbox:
74
+ fixture_key = f"{method} {path}"
75
+ if fixture_key in self._fixtures:
76
+ return self._fixtures[fixture_key]
77
+ if path in self._fixtures:
78
+ return self._fixtures[path]
79
+ raise CadreenError(404, "not_found", "not_found", f"No fixture for {fixture_key}. Provide fixtures via CadreenConfig.fixtures keyed by 'METHOD /path' or '/path'.")
80
+
81
+ url = f"{self._base_url}{path}"
82
+ span = self._telemetry.on_request_start(method, path)
83
+ start_time = time.monotonic()
84
+ headers: dict[str, str] = {
85
+ "Content-Type": "application/json",
86
+ "Authorization": f"Bearer {self._api_key}",
87
+ "Accept": f'application/json; profile="{self._profile}"',
88
+ **(options.headers if options and options.headers else {}),
89
+ }
90
+
91
+ if method in ("POST", "PUT", "PATCH"):
92
+ headers["Idempotency-Key"] = (
93
+ (options.idempotency_key if options else None) or str(uuid.uuid4())
94
+ )
95
+
96
+ is_idempotent = method in IDEMPOTENT_METHODS or "Idempotency-Key" in headers
97
+ max_attempts = (self._max_retries + 1) if is_idempotent else 1
98
+
99
+ last_error: Optional[CadreenError] = None
100
+
101
+ for attempt in range(max_attempts):
102
+ if attempt > 0:
103
+ import asyncio
104
+ delay = min(1.0 * (2 ** (attempt - 1)), 10.0)
105
+ self._telemetry.on_retry(method, path, attempt)
106
+ await asyncio.sleep(delay)
107
+
108
+ try:
109
+ async with httpx.AsyncClient(timeout=self._timeout) as client:
110
+ response = await client.request(
111
+ method=method,
112
+ url=url,
113
+ headers=headers,
114
+ json=body if body is not None else None,
115
+ )
116
+
117
+ if not response.is_success:
118
+ error_body: Optional[dict[str, Any]] = None
119
+ try:
120
+ error_body = response.json()
121
+ except Exception:
122
+ pass
123
+
124
+ err = CadreenError(
125
+ status=response.status_code,
126
+ code=error_body.get("error", {}).get("code", "unknown") if error_body else "unknown",
127
+ error_type=error_body.get("error", {}).get("type", "error") if error_body else "error",
128
+ message=error_body.get("error", {}).get("message", response.text) if error_body else response.text,
129
+ details=error_body.get("error", {}).get("details") if error_body else None,
130
+ intelligence=error_body.get("intelligence") if error_body else None,
131
+ )
132
+
133
+ if response.status_code in RETRYABLE_STATUS_CODES and is_idempotent and attempt < max_attempts - 1:
134
+ last_error = err
135
+ continue
136
+
137
+ self._telemetry.on_error(span, err.code, err.error_type)
138
+ raise err
139
+
140
+ if response.status_code == 204:
141
+ duration_ms = (time.monotonic() - start_time) * 1000
142
+ self._telemetry.on_request_end(span, method, path, 204, duration_ms)
143
+ return None
144
+
145
+ duration_ms = (time.monotonic() - start_time) * 1000
146
+ self._telemetry.on_request_end(span, method, path, response.status_code, duration_ms)
147
+ return response.json()
148
+
149
+ except CadreenError:
150
+ raise
151
+ except httpx.TimeoutException:
152
+ if is_idempotent and attempt < max_attempts - 1:
153
+ last_error = CadreenError(408, "timeout", "timeout", "Request timed out")
154
+ continue
155
+ raise CadreenError(408, "timeout", "timeout", "Request timed out")
156
+ except Exception as exc:
157
+ if is_idempotent and attempt < max_attempts - 1:
158
+ last_error = CadreenError(0, "network_error", "network", str(exc))
159
+ continue
160
+ raise CadreenError(0, "network_error", "network", str(exc))
161
+
162
+ raise last_error or CadreenError(0, "network_error", "network", "Request failed after retries")
163
+
164
+ async def get(self, path: str, params: Optional[dict[str, Any]] = None) -> Any:
165
+ qs = _build_query_string(params) if params else ""
166
+ return await self.request("GET", f"{path}{qs}")
167
+
168
+ async def post(self, path: str, body: Optional[Any] = None, options: Optional[RequestOptions] = None) -> Any:
169
+ return await self.request("POST", path, body, options)
170
+
171
+ async def put(self, path: str, body: Optional[Any] = None, options: Optional[RequestOptions] = None) -> Any:
172
+ return await self.request("PUT", path, body, options)
173
+
174
+ async def delete(self, path: str) -> Any:
175
+ return await self.request("DELETE", path)
176
+
177
+ async def stream(self, path: str) -> Any:
178
+ url = f"{self._base_url}{path}"
179
+ headers = {
180
+ "Authorization": f"Bearer {self._api_key}",
181
+ "Accept": "text/event-stream",
182
+ }
183
+
184
+ async with httpx.AsyncClient(timeout=None) as client:
185
+ async with aconnect_sse(client, "GET", url, headers=headers) as event_source:
186
+ async for event in event_source.aiter_sse():
187
+ try:
188
+ import json
189
+ data = json.loads(event.data)
190
+ except Exception:
191
+ data = {"raw": event.data}
192
+ yield {"type": event.event or "message", "data": data}
cadreen/py.typed ADDED
File without changes
cadreen/redaction.py ADDED
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from dataclasses import replace
5
+ from typing import Any, Sequence
6
+
7
+ _EMAIL_RE = re.compile(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}")
8
+ _PHONE_RE = re.compile(r"(?:\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}")
9
+ _UUID_RE = re.compile(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", re.IGNORECASE)
10
+ _API_KEY_RE = re.compile(r"sk_[a-zA-Z]+_[a-zA-Z0-9]{8,}")
11
+ _IP_RE = re.compile(r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b")
12
+
13
+
14
+ class RedactOptions:
15
+ def __init__(
16
+ self,
17
+ *,
18
+ preserve_uuids: bool = False,
19
+ keys_to_redact: Sequence[str] | None = None,
20
+ ) -> None:
21
+ self.preserve_uuids = preserve_uuids
22
+ self.keys_to_redact = list(keys_to_redact) if keys_to_redact else [
23
+ "content", "message", "text", "body", "email", "phone", "address", "name",
24
+ ]
25
+
26
+
27
+ def redact_string(text: str, options: RedactOptions | None = None) -> str:
28
+ opts = options or RedactOptions()
29
+ result = _EMAIL_RE.sub("[email]", text)
30
+ result = _PHONE_RE.sub("[phone]", result)
31
+ result = _API_KEY_RE.sub("[api_key]", result)
32
+ result = _IP_RE.sub("[ip]", result)
33
+ if not opts.preserve_uuids:
34
+ result = _UUID_RE.sub("[id]", result)
35
+ return result
36
+
37
+
38
+ def redact_value(value: Any, options: RedactOptions | None = None) -> Any:
39
+ opts = options or RedactOptions()
40
+ if isinstance(value, str):
41
+ return redact_string(value, opts)
42
+ if isinstance(value, list):
43
+ return [redact_value(v, opts) for v in value]
44
+ if isinstance(value, dict):
45
+ result: dict[str, Any] = {}
46
+ for key, val in value.items():
47
+ if key.lower() in opts.keys_to_redact and isinstance(val, str):
48
+ result[key] = redact_string(val, opts)
49
+ else:
50
+ result[key] = redact_value(val, opts)
51
+ return result
52
+ if hasattr(value, "__dict__") and hasattr(value, "__dataclass_fields__"):
53
+ changes: dict[str, Any] = {}
54
+ for field_name in value.__dataclass_fields__:
55
+ field_val = getattr(value, field_name)
56
+ if isinstance(field_val, str) and field_name.lower() in opts.keys_to_redact:
57
+ changes[field_name] = redact_string(field_val, opts)
58
+ elif isinstance(field_val, (list, dict)) or hasattr(field_val, "__dataclass_fields__"):
59
+ changes[field_name] = redact_value(field_val, opts)
60
+ elif isinstance(field_val, str):
61
+ changes[field_name] = redact_string(field_val, opts)
62
+ return replace(value, **changes)
63
+ return value
64
+
65
+
66
+ def redact_trace(intel: Any, options: RedactOptions | None = None) -> Any:
67
+ return redact_value(intel, options)
68
+
69
+
70
+ def redact_messages(messages: Sequence[Any], options: RedactOptions | None = None) -> list[Any]:
71
+ return [redact_value(msg, options) for msg in messages]
File without changes
@@ -0,0 +1,178 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from ..client import HttpClient
6
+ from ..types import (
7
+ RegisterOpenAPIRequest,
8
+ RegisterOpenAPIResponse,
9
+ RegisterMCPRequest,
10
+ RegisterMCPResponse,
11
+ ListConnectionsResponse,
12
+ InstallComposioRequest,
13
+ ConnectionGroup,
14
+ Pathway,
15
+ Pagination,
16
+ ConnectResult,
17
+ ConnectPrebuiltDetail,
18
+ ConnectSchemaRequiredDetail,
19
+ ConnectManualDetail,
20
+ ConnectPathway,
21
+ ConnectUnknownDetail,
22
+ )
23
+
24
+
25
+ class ConnectionsResource:
26
+ def __init__(self, client: HttpClient) -> None:
27
+ self._client = client
28
+
29
+ async def register_openapi(
30
+ self,
31
+ name: str,
32
+ *,
33
+ spec_url: str | None = None,
34
+ spec_content: str | None = None,
35
+ credential_id: str | None = None,
36
+ ) -> RegisterOpenAPIResponse:
37
+ body: dict[str, Any] = {"name": name}
38
+ if spec_url is not None:
39
+ body["spec_url"] = spec_url
40
+ if spec_content is not None:
41
+ body["spec_content"] = spec_content
42
+ if credential_id is not None:
43
+ body["credential_id"] = credential_id
44
+
45
+ raw = await self._client.post("/api/v1/cadreen/connections/openapi", body)
46
+ return RegisterOpenAPIResponse(
47
+ id=raw["id"],
48
+ name=raw["name"],
49
+ type=raw.get("type", ""),
50
+ status=raw.get("status", ""),
51
+ tools_generated=raw.get("tools_generated"),
52
+ tools_registered=raw.get("tools_registered"),
53
+ functions=raw.get("functions"),
54
+ spec_url=raw.get("spec_url"),
55
+ )
56
+
57
+ async def register_mcp(
58
+ self,
59
+ name: str,
60
+ url: str,
61
+ *,
62
+ transport: str | None = None,
63
+ headers: dict[str, str] | None = None,
64
+ ) -> RegisterMCPResponse:
65
+ body: dict[str, Any] = {"name": name, "url": url}
66
+ if transport is not None:
67
+ body["transport"] = transport
68
+ if headers is not None:
69
+ body["headers"] = headers
70
+
71
+ raw = await self._client.post("/api/v1/cadreen/connections/mcp", body)
72
+ return RegisterMCPResponse(
73
+ id=raw["id"],
74
+ name=raw["name"],
75
+ type=raw.get("type", ""),
76
+ status=raw.get("status", ""),
77
+ transport=raw.get("transport"),
78
+ url=raw.get("url"),
79
+ )
80
+
81
+ async def install_composio(self, toolkit: str, *, user_id: str | None = None) -> dict[str, Any]:
82
+ body: dict[str, Any] = {"toolkit": toolkit}
83
+ if user_id is not None:
84
+ body["user_id"] = user_id
85
+ return await self._client.post("/api/v1/cadreen/connections/composio/install", body)
86
+
87
+ async def search_composio(self, query: str) -> dict[str, Any]:
88
+ return await self._client.post("/api/v1/cadreen/connections/composio/search", {"query": query})
89
+
90
+ async def composio_status(self, toolkit: str | None = None, user_id: str | None = None) -> dict[str, Any]:
91
+ params: dict[str, Any] = {}
92
+ if toolkit is not None:
93
+ params["toolkit"] = toolkit
94
+ if user_id is not None:
95
+ params["user_id"] = user_id
96
+ return await self._client.get("/api/v1/cadreen/connections/composio/status", params)
97
+
98
+ async def list(self) -> ListConnectionsResponse:
99
+ raw = await self._client.get("/api/v1/cadreen/connections")
100
+ connections: list[ConnectionGroup] = []
101
+ for cg in raw.get("connections", []):
102
+ pathways = None
103
+ if cg.get("pathways"):
104
+ pathways = [
105
+ Pathway(
106
+ id=p["id"],
107
+ capability=p["capability"],
108
+ connector=p["connector"],
109
+ transport=p["transport"],
110
+ health=p["health"],
111
+ tool_id=p["tool_id"],
112
+ )
113
+ for p in cg["pathways"]
114
+ ]
115
+ connections.append(ConnectionGroup(
116
+ capability=cg["capability"],
117
+ pathways=pathways,
118
+ status=cg.get("status", "unknown"),
119
+ ))
120
+ pagination = None
121
+ if raw.get("pagination"):
122
+ pagination = Pagination(
123
+ limit=raw["pagination"]["limit"],
124
+ offset=raw["pagination"]["offset"],
125
+ has_more=raw["pagination"]["has_more"],
126
+ )
127
+ return ListConnectionsResponse(
128
+ connections=connections,
129
+ total_capabilities=raw.get("total_capabilities", 0),
130
+ total_pathways=raw.get("total_pathways", 0),
131
+ pagination=pagination,
132
+ )
133
+
134
+ async def delete(self, id: str) -> None:
135
+ await self._client.delete(f"/api/v1/cadreen/connections/{id}")
136
+
137
+ async def connect(self, capability: str) -> ConnectResult:
138
+ raw = await self._client.post("/api/v1/cadreen/connections", {"capability": capability})
139
+ result_type = raw.get("type", "unknown")
140
+ detail_raw = raw.get("detail", {})
141
+
142
+ detail: ConnectPrebuiltDetail | ConnectSchemaRequiredDetail | ConnectManualDetail | ConnectUnknownDetail
143
+ if result_type == "prebuilt":
144
+ detail = ConnectPrebuiltDetail(
145
+ tool_id=detail_raw.get("tool_id", ""),
146
+ tool_name=detail_raw.get("tool_name", ""),
147
+ service_id=detail_raw.get("service_id", ""),
148
+ service_name=detail_raw.get("service_name", ""),
149
+ auth_type=detail_raw.get("auth_type", ""),
150
+ account_id=detail_raw.get("account_id"),
151
+ source=detail_raw.get("source", ""),
152
+ )
153
+ elif result_type == "schema_required":
154
+ detail = ConnectSchemaRequiredDetail(
155
+ tool_id=detail_raw.get("tool_id", ""),
156
+ tool_name=detail_raw.get("tool_name", ""),
157
+ auth_url=detail_raw.get("auth_url", ""),
158
+ connector=detail_raw.get("connector", ""),
159
+ )
160
+ elif result_type == "manual":
161
+ pathways = [
162
+ ConnectPathway(
163
+ id=p["id"],
164
+ connector=p["connector"],
165
+ tool_id=p["tool_id"],
166
+ health=p["health"],
167
+ priority=p["priority"],
168
+ )
169
+ for p in detail_raw.get("pathways", [])
170
+ ]
171
+ detail = ConnectManualDetail(pathways=pathways)
172
+ else:
173
+ detail = ConnectUnknownDetail(
174
+ searched=detail_raw.get("searched", ""),
175
+ hints=detail_raw.get("hints"),
176
+ )
177
+
178
+ return ConnectResult(type=result_type, capability=raw.get("capability", capability), detail=detail)
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from ..client import HttpClient
6
+ from ..types import ExecutionEvent, ExecutionStatus
7
+
8
+
9
+ class ExecutionsResource:
10
+ def __init__(self, client: HttpClient) -> None:
11
+ self._client = client
12
+
13
+ async def stream(self, execution_id: str):
14
+ path = f"/api/v1/cadreen/executions/{execution_id}/stream"
15
+ async for event in self._client.stream(path):
16
+ yield ExecutionEvent(
17
+ type=event["type"],
18
+ data=event["data"],
19
+ )
20
+
21
+ async def get_status(self, execution_id: str) -> ExecutionStatus:
22
+ raw = await self._client.get(f"/api/v1/cadreen/executions/{execution_id}")
23
+ return ExecutionStatus(
24
+ id=raw["id"],
25
+ status=raw.get("status", ""),
26
+ progress=raw.get("progress"),
27
+ result=raw.get("result"),
28
+ error=raw.get("error"),
29
+ )