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.
@@ -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/
@@ -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.
@@ -0,0 +1,5 @@
1
+ # osva-sdk
2
+
3
+ Official OSVA SDK for Python (>=3.11).
4
+
5
+ The package is publish-ready but not yet published to PyPI.
@@ -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,6 @@
1
+ """Official OSVA SDK for Python."""
2
+
3
+ from osva.client import OSVAClient
4
+ from osva.errors import OSVAAPIError, OSVATransportError
5
+
6
+ __all__ = ["OSVAClient", "OSVAAPIError", "OSVATransportError"]
@@ -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")