osva-sdk 1.0.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.
- osva_sdk-1.0.0/.gitignore +53 -0
- osva_sdk-1.0.0/PKG-INFO +23 -0
- osva_sdk-1.0.0/README.md +5 -0
- osva_sdk-1.0.0/pyproject.toml +48 -0
- osva_sdk-1.0.0/scripts/integration_runtime_server.py +128 -0
- osva_sdk-1.0.0/src/osva/__init__.py +6 -0
- osva_sdk-1.0.0/src/osva/client.py +45 -0
- osva_sdk-1.0.0/src/osva/errors.py +15 -0
- osva_sdk-1.0.0/src/osva/http_client.py +140 -0
- osva_sdk-1.0.0/src/osva/resources/__init__.py +1 -0
- osva_sdk-1.0.0/src/osva/resources/agents.py +26 -0
- osva_sdk-1.0.0/src/osva/resources/approvals.py +23 -0
- osva_sdk-1.0.0/src/osva/resources/artifacts.py +56 -0
- osva_sdk-1.0.0/src/osva/resources/office.py +120 -0
- osva_sdk-1.0.0/src/osva/resources/runs.py +19 -0
- osva_sdk-1.0.0/src/osva/resources/schedules.py +16 -0
- osva_sdk-1.0.0/src/osva/resources/workflow_runs.py +16 -0
- osva_sdk-1.0.0/src/osva/resources/workflows.py +26 -0
- osva_sdk-1.0.0/src/osva/runtime/__init__.py +5 -0
- osva_sdk-1.0.0/src/osva/runtime/context.py +322 -0
- osva_sdk-1.0.0/src/osva/runtime/credential.py +16 -0
- osva_sdk-1.0.0/src/osva/runtime/errors.py +17 -0
- osva_sdk-1.0.0/src/osva/runtime/runtime.py +162 -0
- osva_sdk-1.0.0/tests/conftest.py +14 -0
- osva_sdk-1.0.0/tests/test_artifacts.py +85 -0
- osva_sdk-1.0.0/tests/test_client.py +137 -0
- osva_sdk-1.0.0/tests/test_fixtures.py +29 -0
- osva_sdk-1.0.0/tests/test_package_isolation.py +15 -0
- osva_sdk-1.0.0/tests/test_runtime.py +76 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
node_modules/
|
|
2
|
+
.pnpm-store/
|
|
3
|
+
.next/
|
|
4
|
+
dist/
|
|
5
|
+
/dist-helm-rc/
|
|
6
|
+
build/
|
|
7
|
+
out/
|
|
8
|
+
coverage/
|
|
9
|
+
.turbo/
|
|
10
|
+
.osva/
|
|
11
|
+
|
|
12
|
+
.env
|
|
13
|
+
.env.*
|
|
14
|
+
!.env.example
|
|
15
|
+
*.pem
|
|
16
|
+
*.key
|
|
17
|
+
|
|
18
|
+
*.log
|
|
19
|
+
npm-debug.log*
|
|
20
|
+
pnpm-debug.log*
|
|
21
|
+
|
|
22
|
+
.DS_Store
|
|
23
|
+
Thumbs.db
|
|
24
|
+
desktop.ini
|
|
25
|
+
|
|
26
|
+
.idea/
|
|
27
|
+
.vscode/*
|
|
28
|
+
!.vscode/extensions.json
|
|
29
|
+
|
|
30
|
+
playwright-report/
|
|
31
|
+
test-results/
|
|
32
|
+
blob-report/
|
|
33
|
+
|
|
34
|
+
*.sqlite
|
|
35
|
+
*.sqlite3
|
|
36
|
+
data/
|
|
37
|
+
tmp/
|
|
38
|
+
temp/
|
|
39
|
+
|
|
40
|
+
docker-compose.override.yml
|
|
41
|
+
*.tsbuildinfo
|
|
42
|
+
.cache/
|
|
43
|
+
|
|
44
|
+
# Python SDK artifacts
|
|
45
|
+
__pycache__/
|
|
46
|
+
*.py[cod]
|
|
47
|
+
.pytest_cache/
|
|
48
|
+
.mypy_cache/
|
|
49
|
+
.ruff_cache/
|
|
50
|
+
*.egg-info/
|
|
51
|
+
.venv/
|
|
52
|
+
sdks/python/dist/
|
|
53
|
+
sdks/python/build/
|
osva_sdk-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: osva-sdk
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official OSVA SDK for Python
|
|
5
|
+
Project-URL: Homepage, https://github.com/Osva-AI/osva
|
|
6
|
+
Project-URL: Repository, https://github.com/Osva-AI/osva
|
|
7
|
+
Project-URL: Documentation, https://github.com/Osva-AI/osva/blob/main/docs/OSS-1.0-QUICKSTART.md
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Requires-Dist: httpx>=0.27
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
13
|
+
Requires-Dist: mypy>=1.13; extra == 'dev'
|
|
14
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
15
|
+
Requires-Dist: pytest>=8.3; extra == 'dev'
|
|
16
|
+
Requires-Dist: ruff>=0.8; extra == 'dev'
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# osva-sdk
|
|
20
|
+
|
|
21
|
+
Official OSVA SDK for Python (>=3.11).
|
|
22
|
+
|
|
23
|
+
The package is publish-ready but not yet published to PyPI.
|
osva_sdk-1.0.0/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.25"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "osva-sdk"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Official OSVA SDK for Python"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "Apache-2.0"
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"httpx>=0.27",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://github.com/Osva-AI/osva"
|
|
18
|
+
Repository = "https://github.com/Osva-AI/osva"
|
|
19
|
+
Documentation = "https://github.com/Osva-AI/osva/blob/main/docs/OSS-1.0-QUICKSTART.md"
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
dev = [
|
|
23
|
+
"build>=1.2",
|
|
24
|
+
"mypy>=1.13",
|
|
25
|
+
"pytest>=8.3",
|
|
26
|
+
"pytest-asyncio>=0.24",
|
|
27
|
+
"ruff>=0.8",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[tool.hatch.build.targets.wheel]
|
|
31
|
+
packages = ["src/osva"]
|
|
32
|
+
|
|
33
|
+
[tool.ruff]
|
|
34
|
+
line-length = 100
|
|
35
|
+
target-version = "py311"
|
|
36
|
+
|
|
37
|
+
[tool.ruff.lint]
|
|
38
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
39
|
+
|
|
40
|
+
[tool.mypy]
|
|
41
|
+
python_version = "3.11"
|
|
42
|
+
strict = true
|
|
43
|
+
packages = ["osva"]
|
|
44
|
+
mypy_path = "src"
|
|
45
|
+
|
|
46
|
+
[tool.pytest.ini_options]
|
|
47
|
+
asyncio_mode = "auto"
|
|
48
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Minimal REMOTE_HTTP runtime server for OSVA integration tests."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import asyncio
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
if sys.platform == "win32":
|
|
13
|
+
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
14
|
+
|
|
15
|
+
from osva.runtime.runtime import Runtime
|
|
16
|
+
|
|
17
|
+
runtime = Runtime()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@runtime.execute
|
|
21
|
+
async def execute(input: Any, context: Any) -> Any:
|
|
22
|
+
if isinstance(input, dict) and input.get("mode") == "model":
|
|
23
|
+
result = await context.models.generate_text(
|
|
24
|
+
binding="primary",
|
|
25
|
+
messages=[{"role": "user", "content": "hello from python"}],
|
|
26
|
+
)
|
|
27
|
+
return {"text": result["text"]}
|
|
28
|
+
if isinstance(input, dict):
|
|
29
|
+
return {"echo": input}
|
|
30
|
+
return input
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Handler(BaseHTTPRequestHandler):
|
|
34
|
+
def do_POST(self) -> None: # noqa: N802
|
|
35
|
+
if self.path != "/execute":
|
|
36
|
+
self.send_response(404)
|
|
37
|
+
self.end_headers()
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
raw = self._read_body()
|
|
41
|
+
try:
|
|
42
|
+
body = json.loads(raw.decode("utf-8"))
|
|
43
|
+
except json.JSONDecodeError:
|
|
44
|
+
self._send_json(
|
|
45
|
+
400,
|
|
46
|
+
{
|
|
47
|
+
"protocolVersion": "1",
|
|
48
|
+
"outcome": "FAILED",
|
|
49
|
+
"error": {
|
|
50
|
+
"code": "RUNTIME_PROTOCOL_FAILURE",
|
|
51
|
+
"message": "Request body must be valid JSON.",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
)
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
if not isinstance(body, dict):
|
|
58
|
+
self._send_json(
|
|
59
|
+
400,
|
|
60
|
+
{
|
|
61
|
+
"protocolVersion": "1",
|
|
62
|
+
"outcome": "FAILED",
|
|
63
|
+
"error": {
|
|
64
|
+
"code": "RUNTIME_PROTOCOL_FAILURE",
|
|
65
|
+
"message": "Request body must be a JSON object.",
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
)
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
result = _run_async(runtime.handle_execute_request(body))
|
|
73
|
+
except Exception as error: # noqa: BLE001
|
|
74
|
+
self._send_json(
|
|
75
|
+
400,
|
|
76
|
+
{
|
|
77
|
+
"protocolVersion": "1",
|
|
78
|
+
"outcome": "FAILED",
|
|
79
|
+
"error": {
|
|
80
|
+
"code": "RUNTIME_PROTOCOL_FAILURE",
|
|
81
|
+
"message": str(error),
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
)
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
self._send_json(200, result)
|
|
88
|
+
|
|
89
|
+
def _read_body(self) -> bytes:
|
|
90
|
+
length = self.headers.get("content-length")
|
|
91
|
+
if length is None:
|
|
92
|
+
raise ValueError("Content-Length header is required.")
|
|
93
|
+
return self.rfile.read(int(length))
|
|
94
|
+
|
|
95
|
+
def log_message(self, format: str, *args: Any) -> None:
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
def _send_json(self, status: int, payload: dict[str, Any]) -> None:
|
|
99
|
+
encoded = json.dumps(payload).encode("utf-8")
|
|
100
|
+
self.send_response(status)
|
|
101
|
+
self.send_header("content-type", "application/json")
|
|
102
|
+
self.send_header("content-length", str(len(encoded)))
|
|
103
|
+
self.end_headers()
|
|
104
|
+
self.wfile.write(encoded)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _run_async(coro: Any) -> Any:
|
|
108
|
+
loop = asyncio.new_event_loop()
|
|
109
|
+
try:
|
|
110
|
+
return loop.run_until_complete(coro)
|
|
111
|
+
finally:
|
|
112
|
+
loop.close()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def main() -> None:
|
|
116
|
+
parser = argparse.ArgumentParser()
|
|
117
|
+
parser.add_argument("--host", default="127.0.0.1")
|
|
118
|
+
parser.add_argument("--port", type=int, default=0)
|
|
119
|
+
args = parser.parse_args()
|
|
120
|
+
|
|
121
|
+
server = ThreadingHTTPServer((args.host, args.port), Handler)
|
|
122
|
+
host, port = server.server_address
|
|
123
|
+
print(json.dumps({"host": host, "port": port}), flush=True)
|
|
124
|
+
server.serve_forever()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
if __name__ == "__main__":
|
|
128
|
+
main()
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from osva.http_client import HTTPClient
|
|
4
|
+
from osva.resources.agents import AgentsResource
|
|
5
|
+
from osva.resources.approvals import ApprovalsResource
|
|
6
|
+
from osva.resources.artifacts import ArtifactsResource
|
|
7
|
+
from osva.resources.office import (
|
|
8
|
+
AssignmentsResource,
|
|
9
|
+
GoalsResource,
|
|
10
|
+
OfficeWorkersResource,
|
|
11
|
+
RolesResource,
|
|
12
|
+
TeamsResource,
|
|
13
|
+
)
|
|
14
|
+
from osva.resources.runs import RunsResource
|
|
15
|
+
from osva.resources.schedules import SchedulesResource
|
|
16
|
+
from osva.resources.workflow_runs import WorkflowRunsResource
|
|
17
|
+
from osva.resources.workflows import WorkflowsResource
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class OSVAClient:
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
*,
|
|
24
|
+
base_url: str,
|
|
25
|
+
api_key: str,
|
|
26
|
+
timeout_seconds: float = 30.0,
|
|
27
|
+
client: HTTPClient | None = None,
|
|
28
|
+
) -> None:
|
|
29
|
+
self._http = client or HTTPClient(
|
|
30
|
+
base_url,
|
|
31
|
+
api_key=api_key,
|
|
32
|
+
timeout_seconds=timeout_seconds,
|
|
33
|
+
)
|
|
34
|
+
self.agents = AgentsResource(self._http)
|
|
35
|
+
self.runs = RunsResource(self._http)
|
|
36
|
+
self.workflows = WorkflowsResource(self._http)
|
|
37
|
+
self.workflow_runs = WorkflowRunsResource(self._http)
|
|
38
|
+
self.approvals = ApprovalsResource(self._http)
|
|
39
|
+
self.schedules = SchedulesResource(self._http)
|
|
40
|
+
self.artifacts = ArtifactsResource(self._http)
|
|
41
|
+
self.office_workers = OfficeWorkersResource(self._http)
|
|
42
|
+
self.roles = RolesResource(self._http)
|
|
43
|
+
self.teams = TeamsResource(self._http)
|
|
44
|
+
self.goals = GoalsResource(self._http)
|
|
45
|
+
self.assignments = AssignmentsResource(self._http)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class OSVAAPIError(Exception):
|
|
7
|
+
def __init__(self, status: int, body: dict[str, Any]) -> None:
|
|
8
|
+
domain = body.get("status", "error")
|
|
9
|
+
super().__init__(f"OSVA API request failed with HTTP {status} ({domain}).")
|
|
10
|
+
self.status = status
|
|
11
|
+
self.body = body
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class OSVATransportError(Exception):
|
|
15
|
+
pass
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from osva.errors import OSVAAPIError, OSVATransportError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class HTTPClient:
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
base_url: str,
|
|
15
|
+
*,
|
|
16
|
+
api_key: str,
|
|
17
|
+
timeout_seconds: float = 30.0,
|
|
18
|
+
client: httpx.Client | None = None,
|
|
19
|
+
) -> None:
|
|
20
|
+
normalized = base_url.strip().rstrip("/")
|
|
21
|
+
if not normalized:
|
|
22
|
+
raise ValueError("base_url must not be empty.")
|
|
23
|
+
trimmed_key = api_key.strip()
|
|
24
|
+
if not trimmed_key:
|
|
25
|
+
raise ValueError("api_key must not be empty.")
|
|
26
|
+
self._base_url = normalized
|
|
27
|
+
self._timeout = timeout_seconds
|
|
28
|
+
self._api_key = trimmed_key
|
|
29
|
+
self._client = client or httpx.Client(timeout=timeout_seconds)
|
|
30
|
+
|
|
31
|
+
def _auth_headers(self, headers: dict[str, str] | None = None) -> dict[str, str]:
|
|
32
|
+
merged = dict(headers or {})
|
|
33
|
+
merged.setdefault("authorization", f"Bearer {self._api_key}")
|
|
34
|
+
return merged
|
|
35
|
+
|
|
36
|
+
def request(
|
|
37
|
+
self,
|
|
38
|
+
method: str,
|
|
39
|
+
path: str,
|
|
40
|
+
*,
|
|
41
|
+
query: dict[str, str | None] | None = None,
|
|
42
|
+
body: Any | None = None,
|
|
43
|
+
headers: dict[str, str] | None = None,
|
|
44
|
+
) -> dict[str, Any]:
|
|
45
|
+
params = {key: value for key, value in (query or {}).items() if value is not None}
|
|
46
|
+
url = f"{self._base_url}{path if path.startswith('/') else f'/{path}'}"
|
|
47
|
+
request_headers = self._auth_headers(headers)
|
|
48
|
+
if body is not None:
|
|
49
|
+
request_headers = {**request_headers, "content-type": "application/json"}
|
|
50
|
+
try:
|
|
51
|
+
response = self._client.request(
|
|
52
|
+
method,
|
|
53
|
+
url,
|
|
54
|
+
params=params,
|
|
55
|
+
json=body,
|
|
56
|
+
headers=request_headers,
|
|
57
|
+
)
|
|
58
|
+
except httpx.HTTPError as error:
|
|
59
|
+
raise OSVATransportError("OSVA API transport failed.") from error
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
payload = response.json()
|
|
63
|
+
except json.JSONDecodeError as error:
|
|
64
|
+
raise OSVATransportError("OSVA API returned a non-JSON response.") from error
|
|
65
|
+
|
|
66
|
+
if response.status_code >= 400:
|
|
67
|
+
if isinstance(payload, dict) and isinstance(payload.get("status"), str):
|
|
68
|
+
body_dict = payload
|
|
69
|
+
else:
|
|
70
|
+
body_dict = {"status": "error"}
|
|
71
|
+
raise OSVAAPIError(response.status_code, body_dict)
|
|
72
|
+
|
|
73
|
+
if not isinstance(payload, dict):
|
|
74
|
+
raise OSVATransportError("OSVA API returned an unexpected response shape.")
|
|
75
|
+
return payload
|
|
76
|
+
|
|
77
|
+
def upload_multipart(
|
|
78
|
+
self,
|
|
79
|
+
path: str,
|
|
80
|
+
*,
|
|
81
|
+
data: dict[str, str],
|
|
82
|
+
files: dict[str, tuple[str, bytes, str | None]],
|
|
83
|
+
headers: dict[str, str] | None = None,
|
|
84
|
+
) -> dict[str, Any]:
|
|
85
|
+
url = f"{self._base_url}{path if path.startswith('/') else f'/{path}'}"
|
|
86
|
+
try:
|
|
87
|
+
response = self._client.post(
|
|
88
|
+
url,
|
|
89
|
+
data=data,
|
|
90
|
+
files=files,
|
|
91
|
+
headers=self._auth_headers(headers),
|
|
92
|
+
)
|
|
93
|
+
except httpx.HTTPError as error:
|
|
94
|
+
raise OSVATransportError("OSVA API transport failed.") from error
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
payload = response.json()
|
|
98
|
+
except json.JSONDecodeError as error:
|
|
99
|
+
raise OSVATransportError("OSVA API returned a non-JSON response.") from error
|
|
100
|
+
|
|
101
|
+
if response.status_code >= 400:
|
|
102
|
+
if isinstance(payload, dict) and isinstance(payload.get("status"), str):
|
|
103
|
+
body_dict = payload
|
|
104
|
+
else:
|
|
105
|
+
body_dict = {"status": "error"}
|
|
106
|
+
raise OSVAAPIError(response.status_code, body_dict)
|
|
107
|
+
|
|
108
|
+
if not isinstance(payload, dict):
|
|
109
|
+
raise OSVATransportError("OSVA API returned an unexpected response shape.")
|
|
110
|
+
return payload
|
|
111
|
+
|
|
112
|
+
def download(
|
|
113
|
+
self,
|
|
114
|
+
path: str,
|
|
115
|
+
*,
|
|
116
|
+
query: dict[str, str | None] | None = None,
|
|
117
|
+
) -> httpx.Response:
|
|
118
|
+
params = {key: value for key, value in (query or {}).items() if value is not None}
|
|
119
|
+
url = f"{self._base_url}{path if path.startswith('/') else f'/{path}'}"
|
|
120
|
+
try:
|
|
121
|
+
response = self._client.get(
|
|
122
|
+
url,
|
|
123
|
+
params=params,
|
|
124
|
+
headers=self._auth_headers(),
|
|
125
|
+
)
|
|
126
|
+
except httpx.HTTPError as error:
|
|
127
|
+
raise OSVATransportError("OSVA API transport failed.") from error
|
|
128
|
+
|
|
129
|
+
if response.status_code >= 400:
|
|
130
|
+
try:
|
|
131
|
+
payload = response.json()
|
|
132
|
+
except json.JSONDecodeError:
|
|
133
|
+
payload = {"status": "error"}
|
|
134
|
+
if isinstance(payload, dict) and isinstance(payload.get("status"), str):
|
|
135
|
+
body_dict = payload
|
|
136
|
+
else:
|
|
137
|
+
body_dict = {"status": "error"}
|
|
138
|
+
raise OSVAAPIError(response.status_code, body_dict)
|
|
139
|
+
|
|
140
|
+
return response
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Control-plane resource clients."""
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from osva.http_client import HTTPClient
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AgentsResource:
|
|
9
|
+
def __init__(self, client: HTTPClient) -> None:
|
|
10
|
+
self._client = client
|
|
11
|
+
|
|
12
|
+
def list(self) -> dict[str, Any]:
|
|
13
|
+
return self._client.request("GET", "/v1/agents")
|
|
14
|
+
|
|
15
|
+
def get(self, agent_id: str) -> dict[str, Any]:
|
|
16
|
+
return self._client.request("GET", f"/v1/agents/{agent_id}")
|
|
17
|
+
|
|
18
|
+
def create(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
19
|
+
return self._client.request("POST", "/v1/agents", body=payload)
|
|
20
|
+
|
|
21
|
+
def create_version(self, agent_id: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
22
|
+
return self._client.request(
|
|
23
|
+
"POST",
|
|
24
|
+
f"/v1/agents/{agent_id}/versions",
|
|
25
|
+
body=payload,
|
|
26
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from osva.http_client import HTTPClient
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ApprovalsResource:
|
|
9
|
+
def __init__(self, client: HTTPClient) -> None:
|
|
10
|
+
self._client = client
|
|
11
|
+
|
|
12
|
+
def get(self, approval_request_id: str) -> dict[str, Any]:
|
|
13
|
+
return self._client.request(
|
|
14
|
+
"GET",
|
|
15
|
+
f"/v1/approval-requests/{approval_request_id}",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
def decide(self, approval_request_id: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
19
|
+
return self._client.request(
|
|
20
|
+
"POST",
|
|
21
|
+
f"/v1/approval-requests/{approval_request_id}/decision",
|
|
22
|
+
body=payload,
|
|
23
|
+
)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from osva.http_client import HTTPClient
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ArtifactsResource:
|
|
11
|
+
def __init__(self, http: HTTPClient) -> None:
|
|
12
|
+
self._http = http
|
|
13
|
+
|
|
14
|
+
def list(self, **query: str | None) -> dict[str, Any]:
|
|
15
|
+
return self._http.request("GET", "/v1/artifacts", query=query)
|
|
16
|
+
|
|
17
|
+
def get(self, artifact_id: str) -> dict[str, Any]:
|
|
18
|
+
return self._http.request("GET", f"/v1/artifacts/{artifact_id}")
|
|
19
|
+
|
|
20
|
+
def create(
|
|
21
|
+
self,
|
|
22
|
+
*,
|
|
23
|
+
name: str,
|
|
24
|
+
content: bytes,
|
|
25
|
+
media_type: str | None = None,
|
|
26
|
+
metadata: dict[str, Any] | None = None,
|
|
27
|
+
expected_digest: str | None = None,
|
|
28
|
+
idempotency_key: str | None = None,
|
|
29
|
+
) -> dict[str, Any]:
|
|
30
|
+
data: dict[str, str] = {"name": name}
|
|
31
|
+
if media_type is not None:
|
|
32
|
+
data["mediaType"] = media_type
|
|
33
|
+
if metadata is not None:
|
|
34
|
+
data["metadata"] = __import__("json").dumps(metadata)
|
|
35
|
+
if expected_digest is not None:
|
|
36
|
+
data["expectedDigest"] = expected_digest
|
|
37
|
+
if idempotency_key is not None:
|
|
38
|
+
data["idempotencyKey"] = idempotency_key
|
|
39
|
+
|
|
40
|
+
files: dict[str, tuple[str, bytes, str | None]] = {
|
|
41
|
+
"file": (
|
|
42
|
+
name,
|
|
43
|
+
content,
|
|
44
|
+
media_type or "application/octet-stream",
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
headers = {"Idempotency-Key": idempotency_key} if idempotency_key is not None else None
|
|
48
|
+
return self._http.upload_multipart(
|
|
49
|
+
"/v1/artifacts",
|
|
50
|
+
data=data,
|
|
51
|
+
files=files,
|
|
52
|
+
headers=headers,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def download(self, artifact_id: str) -> httpx.Response:
|
|
56
|
+
return self._http.download(f"/v1/artifacts/{artifact_id}/content")
|