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 +128 -0
- covia/_async_client.py +295 -0
- covia/_client.py +295 -0
- covia/_sse.py +23 -0
- covia/_transport.py +76 -0
- covia/agents.py +241 -0
- covia/asset.py +226 -0
- covia/async_api/__init__.py +27 -0
- covia/async_api/grid.py +65 -0
- covia/async_api/job.py +162 -0
- covia/async_api/venue.py +273 -0
- covia/auth.py +300 -0
- covia/exceptions.py +81 -0
- covia/grid.py +74 -0
- covia/job.py +195 -0
- covia/models.py +326 -0
- covia/py.typed +0 -0
- covia/secrets.py +92 -0
- covia/status.py +52 -0
- covia/ucan.py +89 -0
- covia/venue.py +339 -0
- covia/workspace.py +191 -0
- covia-0.2.0.dist-info/METADATA +248 -0
- covia-0.2.0.dist-info/RECORD +26 -0
- covia-0.2.0.dist-info/WHEEL +4 -0
- covia-0.2.0.dist-info/licenses/LICENSE +191 -0
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
|
+
)
|