covia 0.2.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.
covia/__init__.py ADDED
@@ -0,0 +1,128 @@
1
+ """Covia Python SDK — federated AI orchestration grid client.
2
+
3
+ Connect to a Covia venue and invoke operations::
4
+
5
+ from covia import Grid
6
+
7
+ with Grid.connect("https://venue.covia.ai") as venue:
8
+ result = venue.run("my-operation", {"prompt": "hello"})
9
+ """
10
+
11
+ from covia.agents import AgentManager
12
+ from covia.asset import Asset
13
+ from covia.auth import Auth, BasicAuth, BearerAuth, Ed25519Auth, NoAuth
14
+ from covia.exceptions import (
15
+ AssetNotFoundError,
16
+ CoviaConnectionError,
17
+ CoviaError,
18
+ CoviaTimeoutError,
19
+ GridError,
20
+ JobFailedError,
21
+ JobNotFoundError,
22
+ NotFoundError,
23
+ )
24
+ from covia.grid import Grid
25
+ from covia.job import Job
26
+ from covia.models import (
27
+ AgentCard,
28
+ AgentChatResult,
29
+ AgentCreateResult,
30
+ AgentDeleteResult,
31
+ AgentListEntry,
32
+ AgentListResult,
33
+ AgentMessageResult,
34
+ AgentQueryResult,
35
+ AgentRequestResult,
36
+ AgentSuspendResult,
37
+ AgentTriggerResult,
38
+ AssetList,
39
+ DIDDocument,
40
+ InvokeRequest,
41
+ JobData,
42
+ MCPDiscovery,
43
+ OperationInfo,
44
+ SecretExtractResult,
45
+ SecretSetResult,
46
+ UCANAttenuation,
47
+ VenueStatus,
48
+ WorkspaceAppendResult,
49
+ WorkspaceDeleteResult,
50
+ WorkspaceListResult,
51
+ WorkspaceReadResult,
52
+ WorkspaceSliceResult,
53
+ WorkspaceWriteResult,
54
+ )
55
+ from covia.secrets import SecretManager
56
+ from covia.status import JobStatus
57
+ from covia.ucan import UCANManager
58
+ from covia.venue import Venue
59
+ from covia.workspace import WorkspaceManager
60
+
61
+ __version__ = "0.2.0"
62
+
63
+ # Library-level NullHandler — prevents "No handlers could be found" warnings.
64
+ # Users must configure logging themselves to see SDK log output.
65
+ import logging as _logging
66
+
67
+ _logging.getLogger("covia").addHandler(_logging.NullHandler())
68
+
69
+ __all__ = [
70
+ "__version__",
71
+ # Core classes
72
+ "Grid",
73
+ "Venue",
74
+ "Job",
75
+ "Asset",
76
+ "JobStatus",
77
+ # Managers
78
+ "AgentManager",
79
+ "SecretManager",
80
+ "UCANManager",
81
+ "WorkspaceManager",
82
+ # Auth
83
+ "Auth",
84
+ "NoAuth",
85
+ "BearerAuth",
86
+ "BasicAuth",
87
+ "Ed25519Auth",
88
+ # Models
89
+ "VenueStatus",
90
+ "AssetList",
91
+ "JobData",
92
+ "DIDDocument",
93
+ "MCPDiscovery",
94
+ "AgentCard",
95
+ "InvokeRequest",
96
+ "OperationInfo",
97
+ # Agent models
98
+ "AgentCreateResult",
99
+ "AgentRequestResult",
100
+ "AgentMessageResult",
101
+ "AgentChatResult",
102
+ "AgentTriggerResult",
103
+ "AgentQueryResult",
104
+ "AgentListEntry",
105
+ "AgentListResult",
106
+ "AgentDeleteResult",
107
+ "AgentSuspendResult",
108
+ # Workspace models
109
+ "WorkspaceReadResult",
110
+ "WorkspaceWriteResult",
111
+ "WorkspaceDeleteResult",
112
+ "WorkspaceAppendResult",
113
+ "WorkspaceListResult",
114
+ "WorkspaceSliceResult",
115
+ # UCAN + Secret models
116
+ "UCANAttenuation",
117
+ "SecretSetResult",
118
+ "SecretExtractResult",
119
+ # Exceptions
120
+ "CoviaError",
121
+ "GridError",
122
+ "CoviaConnectionError",
123
+ "CoviaTimeoutError",
124
+ "JobFailedError",
125
+ "NotFoundError",
126
+ "AssetNotFoundError",
127
+ "JobNotFoundError",
128
+ ]
covia/_async_client.py ADDED
@@ -0,0 +1,295 @@
1
+ """Asynchronous HTTP client for the Covia REST API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from collections.abc import AsyncIterator
7
+ from typing import Any
8
+
9
+ import httpx
10
+ from httpx_sse import aconnect_sse
11
+
12
+ from covia._sse import SSEEvent
13
+ from covia._transport import TransportConfig
14
+ from covia.exceptions import (
15
+ AssetNotFoundError,
16
+ CoviaConnectionError,
17
+ CoviaTimeoutError,
18
+ GridError,
19
+ JobNotFoundError,
20
+ )
21
+ from covia.models import (
22
+ AgentCard,
23
+ AssetList,
24
+ DIDDocument,
25
+ JobData,
26
+ MCPDiscovery,
27
+ OperationInfo,
28
+ VenueStatus,
29
+ )
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ class AsyncCoviaHTTPClient:
35
+ """Low-level async HTTP client wrapping ``httpx.AsyncClient``.
36
+
37
+ Async mirror of :class:`~covia._client.CoviaHTTPClient`.
38
+ """
39
+
40
+ def __init__(self, config: TransportConfig) -> None:
41
+ self._config = config
42
+ self._client = httpx.AsyncClient(
43
+ base_url=config.api_url,
44
+ timeout=config.timeout,
45
+ headers=config.headers,
46
+ follow_redirects=config.follow_redirects,
47
+ )
48
+
49
+ async def aclose(self) -> None:
50
+ """Close the underlying HTTP connection pool."""
51
+ await self._client.aclose()
52
+
53
+ async def __aenter__(self) -> AsyncCoviaHTTPClient:
54
+ return self
55
+
56
+ async def __aexit__(self, *args: object) -> None:
57
+ await self.aclose()
58
+
59
+ # ------------------------------------------------------------------
60
+ # Status
61
+ # ------------------------------------------------------------------
62
+
63
+ async def get_status(self) -> VenueStatus:
64
+ """``GET /api/v1/status``"""
65
+ resp = await self._request("GET", "status")
66
+ return VenueStatus.model_validate(resp.json())
67
+
68
+ # ------------------------------------------------------------------
69
+ # Assets
70
+ # ------------------------------------------------------------------
71
+
72
+ async def list_assets(self, offset: int = 0, limit: int | None = None) -> AssetList:
73
+ """``GET /api/v1/assets``"""
74
+ params: dict[str, Any] = {"offset": offset}
75
+ if limit is not None:
76
+ params["limit"] = limit
77
+ resp = await self._request("GET", "assets", params=params)
78
+ return AssetList.model_validate(resp.json())
79
+
80
+ async def register_asset(self, metadata: dict[str, Any]) -> str:
81
+ """``POST /api/v1/assets`` — returns the new asset ID."""
82
+ resp = await self._request("POST", "assets", json=metadata)
83
+ return resp.text.strip().strip('"')
84
+
85
+ async def get_asset_metadata(self, asset_id: str) -> tuple[dict[str, Any], str]:
86
+ """``GET /api/v1/assets/{id}``
87
+
88
+ Returns:
89
+ A tuple of (parsed metadata dict, raw UTF-8 response text).
90
+ The raw text is preserved for asset ID computation/validation
91
+ (asset IDs are the SHA-256 hash of canonical metadata bytes).
92
+ """
93
+ resp = await self._request_asset("GET", f"assets/{asset_id}", asset_id)
94
+ result: dict[str, Any] = resp.json()
95
+ return result, resp.text
96
+
97
+ async def get_asset_content(self, asset_id: str) -> bytes:
98
+ """``GET /api/v1/assets/{id}/content``"""
99
+ resp = await self._request_asset("GET", f"assets/{asset_id}/content", asset_id)
100
+ return resp.content
101
+
102
+ async def put_asset_content(self, asset_id: str, content: bytes) -> str:
103
+ """``PUT /api/v1/assets/{id}/content`` — returns the content hash."""
104
+ resp = await self._request_asset("PUT", f"assets/{asset_id}/content", asset_id, content=content)
105
+ return resp.text.strip().strip('"')
106
+
107
+ # ------------------------------------------------------------------
108
+ # Jobs / Invoke
109
+ # ------------------------------------------------------------------
110
+
111
+ async def invoke(self, operation: str, input: Any = None, *, ucans: list[str] | None = None) -> JobData:
112
+ """``POST /api/v1/invoke``.
113
+
114
+ ``ucans`` is an optional list of UCAN proof tokens authorising
115
+ capability-gated operations.
116
+ """
117
+ body: dict[str, Any] = {"operation": operation}
118
+ if input is not None:
119
+ body["input"] = input
120
+ if ucans:
121
+ body["ucans"] = list(ucans)
122
+ resp = await self._request("POST", "invoke", json=body)
123
+ return JobData.model_validate(resp.json())
124
+
125
+ async def get_job(self, job_id: str) -> JobData:
126
+ """``GET /api/v1/jobs/{id}``"""
127
+ resp = await self._request_job("GET", f"jobs/{job_id}", job_id)
128
+ return JobData.model_validate(resp.json())
129
+
130
+ async def list_jobs(self) -> list[str]:
131
+ """``GET /api/v1/jobs``"""
132
+ resp = await self._request("GET", "jobs")
133
+ result: list[str] = resp.json()
134
+ return result
135
+
136
+ async def cancel_job(self, job_id: str) -> JobData:
137
+ """``PUT /api/v1/jobs/{id}/cancel``"""
138
+ resp = await self._request_job("PUT", f"jobs/{job_id}/cancel", job_id)
139
+ return JobData.model_validate(resp.json())
140
+
141
+ async def delete_job(self, job_id: str) -> None:
142
+ """``PUT /api/v1/jobs/{id}/delete``"""
143
+ await self._request_job("PUT", f"jobs/{job_id}/delete", job_id)
144
+
145
+ async def stream_job_events(self, job_id: str) -> AsyncIterator[SSEEvent]:
146
+ """``GET /api/v1/jobs/{id}/sse`` — yields SSE events."""
147
+ async with aconnect_sse(self._client, "GET", f"jobs/{job_id}/sse") as event_source:
148
+ async for sse in event_source.aiter_sse():
149
+ yield SSEEvent(
150
+ event=sse.event if sse.event else None,
151
+ data=sse.data,
152
+ id=sse.id if sse.id else None,
153
+ retry=sse.retry,
154
+ )
155
+
156
+ # ------------------------------------------------------------------
157
+ # Operations
158
+ # ------------------------------------------------------------------
159
+
160
+ async def list_operations(self) -> list[OperationInfo]:
161
+ """``GET /api/v1/operations``"""
162
+ resp = await self._request("GET", "operations")
163
+ return [OperationInfo.model_validate(item) for item in resp.json()]
164
+
165
+ async def get_operation(self, name: str) -> OperationInfo:
166
+ """``GET /api/v1/operations/{name}``"""
167
+ resp = await self._request("GET", f"operations/{name}")
168
+ return OperationInfo.model_validate(resp.json())
169
+
170
+ # ------------------------------------------------------------------
171
+ # Secrets
172
+ # ------------------------------------------------------------------
173
+
174
+ async def list_secrets(self) -> list[str]:
175
+ """``GET /api/v1/secrets`` — returns the list of secret names."""
176
+ resp = await self._request("GET", "secrets")
177
+ body: dict[str, Any] = resp.json()
178
+ items: list[str] = body.get("items", [])
179
+ return items
180
+
181
+ async def put_secret(self, name: str, value: str) -> None:
182
+ """``PUT /api/v1/secrets/{name}`` — store a secret value."""
183
+ await self._request("PUT", f"secrets/{name}", json={"value": value})
184
+
185
+ async def delete_secret(self, name: str) -> None:
186
+ """``DELETE /api/v1/secrets/{name}`` — delete a secret."""
187
+ await self._request("DELETE", f"secrets/{name}")
188
+
189
+ # ------------------------------------------------------------------
190
+ # Discovery
191
+ # ------------------------------------------------------------------
192
+
193
+ async def get_did_document(self) -> DIDDocument:
194
+ """``GET /.well-known/did.json``"""
195
+ base = self._config.base_url.rstrip("/")
196
+ resp = await self._raw_request("GET", f"{base}/.well-known/did.json")
197
+ return DIDDocument.model_validate(resp.json())
198
+
199
+ async def get_asset_did_document(self, asset_id: str) -> DIDDocument:
200
+ """``GET /a/{id}/did.json``"""
201
+ base = self._config.base_url.rstrip("/")
202
+ resp = await self._raw_request("GET", f"{base}/a/{asset_id}/did.json")
203
+ return DIDDocument.model_validate(resp.json())
204
+
205
+ async def get_mcp_discovery(self) -> MCPDiscovery:
206
+ """``GET /.well-known/mcp``"""
207
+ base = self._config.base_url.rstrip("/")
208
+ resp = await self._raw_request("GET", f"{base}/.well-known/mcp")
209
+ return MCPDiscovery.model_validate(resp.json())
210
+
211
+ async def get_agent_card(self) -> AgentCard:
212
+ """``GET /.well-known/agent-card.json``"""
213
+ base = self._config.base_url.rstrip("/")
214
+ resp = await self._raw_request("GET", f"{base}/.well-known/agent-card.json")
215
+ return AgentCard.model_validate(resp.json())
216
+
217
+ # ------------------------------------------------------------------
218
+ # Internal helpers
219
+ # ------------------------------------------------------------------
220
+
221
+ async def _request_asset(self, method: str, path: str, asset_id: str, **kwargs: Any) -> httpx.Response:
222
+ """Like ``_request`` but raises :class:`AssetNotFoundError` on 404."""
223
+ try:
224
+ return await self._request(method, path, **kwargs)
225
+ except GridError as exc:
226
+ if exc.status_code == 404:
227
+ raise AssetNotFoundError(asset_id) from exc
228
+ raise
229
+
230
+ async def _request_job(self, method: str, path: str, job_id: str, **kwargs: Any) -> httpx.Response:
231
+ """Like ``_request`` but raises :class:`JobNotFoundError` on 404."""
232
+ try:
233
+ return await self._request(method, path, **kwargs)
234
+ except GridError as exc:
235
+ if exc.status_code == 404:
236
+ raise JobNotFoundError(job_id) from exc
237
+ raise
238
+
239
+ async def _request(self, method: str, path: str, **kwargs: Any) -> httpx.Response:
240
+ """Make an API request (relative to the /api/v1/ base)."""
241
+ self._apply_auth(kwargs)
242
+ logger.debug("%s %s", method, path)
243
+ try:
244
+ response = await self._client.request(method, path, **kwargs)
245
+ except httpx.ConnectError as exc:
246
+ logger.debug("Connection failed: %s %s — %s", method, path, exc)
247
+ raise CoviaConnectionError(str(exc)) from exc
248
+ except httpx.TimeoutException as exc:
249
+ logger.debug("Request timed out: %s %s — %s", method, path, exc)
250
+ raise CoviaTimeoutError(str(exc)) from exc
251
+ logger.debug("%s %s → %d", method, path, response.status_code)
252
+ self._handle_error(response)
253
+ return response
254
+
255
+ async def _raw_request(self, method: str, url: str, **kwargs: Any) -> httpx.Response:
256
+ """Make a request to an absolute URL (for discovery endpoints)."""
257
+ self._apply_auth(kwargs)
258
+ logger.debug("%s %s", method, url)
259
+ try:
260
+ response = await self._client.request(method, url, **kwargs)
261
+ except httpx.ConnectError as exc:
262
+ logger.debug("Connection failed: %s %s — %s", method, url, exc)
263
+ raise CoviaConnectionError(str(exc)) from exc
264
+ except httpx.TimeoutException as exc:
265
+ logger.debug("Request timed out: %s %s — %s", method, url, exc)
266
+ raise CoviaTimeoutError(str(exc)) from exc
267
+ logger.debug("%s %s → %d", method, url, response.status_code)
268
+ self._handle_error(response)
269
+ return response
270
+
271
+ def _apply_auth(self, kwargs: dict[str, Any]) -> None:
272
+ """Inject authentication headers into request kwargs."""
273
+ if self._config.auth is not None:
274
+ auth_headers: dict[str, str] = {}
275
+ self._config.auth.apply(auth_headers)
276
+ if auth_headers:
277
+ headers = dict(kwargs.get("headers", {}))
278
+ headers.update(auth_headers)
279
+ kwargs["headers"] = headers
280
+
281
+ def _handle_error(self, response: httpx.Response) -> None:
282
+ """Raise an appropriate exception for error responses."""
283
+ if response.status_code < 400:
284
+ return
285
+ try:
286
+ body = response.json()
287
+ message = body.get("error", response.text) if isinstance(body, dict) else response.text
288
+ except Exception:
289
+ body = None
290
+ message = response.text
291
+ raise GridError(
292
+ status_code=response.status_code,
293
+ message=message,
294
+ response_body=body,
295
+ )