den-sdk 0.0.3__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.
- den_sdk-0.0.3/.gitignore +48 -0
- den_sdk-0.0.3/PKG-INFO +13 -0
- den_sdk-0.0.3/pyproject.toml +35 -0
- den_sdk-0.0.3/src/den/__init__.py +56 -0
- den_sdk-0.0.3/src/den/client.py +166 -0
- den_sdk-0.0.3/src/den/exceptions.py +40 -0
- den_sdk-0.0.3/src/den/py.typed +0 -0
- den_sdk-0.0.3/src/den/sandbox.py +566 -0
- den_sdk-0.0.3/src/den/types.py +178 -0
- den_sdk-0.0.3/uv.lock +452 -0
den_sdk-0.0.3/.gitignore
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Binaries
|
|
2
|
+
bin/
|
|
3
|
+
/den
|
|
4
|
+
*.exe
|
|
5
|
+
|
|
6
|
+
# Database
|
|
7
|
+
*.db
|
|
8
|
+
|
|
9
|
+
# Config
|
|
10
|
+
den.yaml
|
|
11
|
+
!den.example.yaml
|
|
12
|
+
|
|
13
|
+
# Go
|
|
14
|
+
vendor/
|
|
15
|
+
|
|
16
|
+
# Node
|
|
17
|
+
node_modules/
|
|
18
|
+
sdk/*/dist/
|
|
19
|
+
|
|
20
|
+
# Python
|
|
21
|
+
__pycache__/
|
|
22
|
+
*.pyc
|
|
23
|
+
.venv/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
|
|
26
|
+
# IDE
|
|
27
|
+
.idea/
|
|
28
|
+
.vscode/
|
|
29
|
+
*.swp
|
|
30
|
+
*.swo
|
|
31
|
+
*~
|
|
32
|
+
|
|
33
|
+
# OS
|
|
34
|
+
.DS_Store
|
|
35
|
+
Thumbs.db
|
|
36
|
+
|
|
37
|
+
# Test
|
|
38
|
+
coverage.out
|
|
39
|
+
coverage.html
|
|
40
|
+
ws_test*.py
|
|
41
|
+
.venv-test/
|
|
42
|
+
|
|
43
|
+
# Temp
|
|
44
|
+
FIXPLAN.md
|
|
45
|
+
PROGRESS.md
|
|
46
|
+
|
|
47
|
+
# GoReleaser
|
|
48
|
+
/dist/
|
den_sdk-0.0.3/PKG-INFO
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: den-sdk
|
|
3
|
+
Version: 0.0.3
|
|
4
|
+
Summary: Python SDK for the Den sandbox API
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Requires-Dist: httpx>=0.27.0
|
|
8
|
+
Requires-Dist: pydantic>=2.0.0
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
11
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
12
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
13
|
+
Requires-Dist: ruff>=0.8; extra == 'dev'
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "den-sdk"
|
|
7
|
+
version = "0.0.3"
|
|
8
|
+
description = "Python SDK for the Den sandbox API"
|
|
9
|
+
license = "MIT"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"httpx>=0.27.0",
|
|
13
|
+
"pydantic>=2.0.0",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
dev = [
|
|
18
|
+
"pytest>=8.0",
|
|
19
|
+
"pytest-asyncio>=0.24",
|
|
20
|
+
"respx>=0.21",
|
|
21
|
+
"ruff>=0.8",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[tool.hatch.build.targets.wheel]
|
|
25
|
+
packages = ["src/den"]
|
|
26
|
+
|
|
27
|
+
[tool.ruff]
|
|
28
|
+
target-version = "py310"
|
|
29
|
+
line-length = 100
|
|
30
|
+
|
|
31
|
+
[tool.ruff.lint]
|
|
32
|
+
select = ["E", "F", "I", "N", "W", "UP"]
|
|
33
|
+
|
|
34
|
+
[tool.pytest.ini_options]
|
|
35
|
+
asyncio_mode = "auto"
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Den Python SDK - manage cloud sandboxes programmatically."""
|
|
2
|
+
|
|
3
|
+
from den.client import Den
|
|
4
|
+
from den.exceptions import (
|
|
5
|
+
DenError,
|
|
6
|
+
AuthenticationError,
|
|
7
|
+
NotFoundError,
|
|
8
|
+
RateLimitError,
|
|
9
|
+
ValidationError,
|
|
10
|
+
)
|
|
11
|
+
from den.sandbox import Sandbox, SandboxManager
|
|
12
|
+
from den.types import (
|
|
13
|
+
ExecResult,
|
|
14
|
+
FileInfo,
|
|
15
|
+
PortMapping,
|
|
16
|
+
S3ExportRequest,
|
|
17
|
+
S3ExportResponse,
|
|
18
|
+
S3ImportRequest,
|
|
19
|
+
S3ImportResponse,
|
|
20
|
+
S3SyncConfig,
|
|
21
|
+
SandboxConfig,
|
|
22
|
+
SandboxInfo,
|
|
23
|
+
SandboxStats,
|
|
24
|
+
SnapshotInfo,
|
|
25
|
+
StorageConfig,
|
|
26
|
+
TmpfsMount,
|
|
27
|
+
VolumeMount,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"Den",
|
|
32
|
+
"DenError",
|
|
33
|
+
"AuthenticationError",
|
|
34
|
+
"ExecResult",
|
|
35
|
+
"FileInfo",
|
|
36
|
+
"NotFoundError",
|
|
37
|
+
"PortMapping",
|
|
38
|
+
"RateLimitError",
|
|
39
|
+
"S3ExportRequest",
|
|
40
|
+
"S3ExportResponse",
|
|
41
|
+
"S3ImportRequest",
|
|
42
|
+
"S3ImportResponse",
|
|
43
|
+
"S3SyncConfig",
|
|
44
|
+
"Sandbox",
|
|
45
|
+
"SandboxConfig",
|
|
46
|
+
"SandboxInfo",
|
|
47
|
+
"SandboxManager",
|
|
48
|
+
"SandboxStats",
|
|
49
|
+
"SnapshotInfo",
|
|
50
|
+
"StorageConfig",
|
|
51
|
+
"TmpfsMount",
|
|
52
|
+
"ValidationError",
|
|
53
|
+
"VolumeMount",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
__version__ = "0.0.2"
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Main client for the Den API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from den.sandbox import SandboxManager
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Den:
|
|
11
|
+
"""Client for the Den sandbox API.
|
|
12
|
+
|
|
13
|
+
Provides access to sandbox management and operations via both
|
|
14
|
+
synchronous and asynchronous interfaces.
|
|
15
|
+
|
|
16
|
+
Example (sync)::
|
|
17
|
+
|
|
18
|
+
client = Den("http://localhost:8080", api_key="sk-...")
|
|
19
|
+
sandbox = client.sandbox.create(image="python:3.12")
|
|
20
|
+
result = sandbox.exec(["python", "-c", "print('hello')"])
|
|
21
|
+
print(result.stdout)
|
|
22
|
+
sandbox.destroy()
|
|
23
|
+
client.close()
|
|
24
|
+
|
|
25
|
+
Example (async)::
|
|
26
|
+
|
|
27
|
+
client = Den("http://localhost:8080", api_key="sk-...")
|
|
28
|
+
sandbox = await client.sandbox.acreate(image="python:3.12")
|
|
29
|
+
result = await sandbox.aexec(["python", "-c", "print('hello')"])
|
|
30
|
+
print(result.stdout)
|
|
31
|
+
await sandbox.adestroy()
|
|
32
|
+
await client.aclose()
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
url: str = "http://localhost:8080",
|
|
38
|
+
*,
|
|
39
|
+
api_key: str | None = None,
|
|
40
|
+
timeout: float = 120.0,
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Initialize the Den client.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
url: Base URL of the Den server (e.g. ``http://localhost:8080``).
|
|
46
|
+
api_key: Optional API key for authentication.
|
|
47
|
+
timeout: Request timeout in seconds. Defaults to 120.
|
|
48
|
+
"""
|
|
49
|
+
self._url = url.rstrip("/")
|
|
50
|
+
self._api_key = api_key
|
|
51
|
+
self._base_url = f"{self._url}/api/v1"
|
|
52
|
+
|
|
53
|
+
headers: dict[str, str] = {}
|
|
54
|
+
if api_key:
|
|
55
|
+
headers["X-API-Key"] = api_key
|
|
56
|
+
|
|
57
|
+
self._client = httpx.Client(
|
|
58
|
+
base_url="",
|
|
59
|
+
headers=headers,
|
|
60
|
+
timeout=timeout,
|
|
61
|
+
)
|
|
62
|
+
self._async_client = httpx.AsyncClient(
|
|
63
|
+
base_url="",
|
|
64
|
+
headers=headers,
|
|
65
|
+
timeout=timeout,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
self._sandbox_manager = SandboxManager(
|
|
69
|
+
client=self._client,
|
|
70
|
+
async_client=self._async_client,
|
|
71
|
+
base_url=self._base_url,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def sandbox(self) -> SandboxManager:
|
|
76
|
+
"""Access the sandbox manager for creating and managing sandboxes."""
|
|
77
|
+
return self._sandbox_manager
|
|
78
|
+
|
|
79
|
+
def health(self) -> dict:
|
|
80
|
+
"""Check server health (sync).
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Dictionary with server health status.
|
|
84
|
+
"""
|
|
85
|
+
resp = self._client.get(f"{self._base_url}/health")
|
|
86
|
+
resp.raise_for_status()
|
|
87
|
+
return resp.json()
|
|
88
|
+
|
|
89
|
+
async def ahealth(self) -> dict:
|
|
90
|
+
"""Check server health (async).
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Dictionary with server health status.
|
|
94
|
+
"""
|
|
95
|
+
resp = await self._async_client.get(f"{self._base_url}/health")
|
|
96
|
+
resp.raise_for_status()
|
|
97
|
+
return resp.json()
|
|
98
|
+
|
|
99
|
+
def version(self) -> dict:
|
|
100
|
+
"""Get server version info (sync).
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Dictionary with version, commit, and build_date.
|
|
104
|
+
"""
|
|
105
|
+
resp = self._client.get(f"{self._base_url}/version")
|
|
106
|
+
resp.raise_for_status()
|
|
107
|
+
return resp.json()
|
|
108
|
+
|
|
109
|
+
async def aversion(self) -> dict:
|
|
110
|
+
"""Get server version info (async).
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Dictionary with version, commit, and build_date.
|
|
114
|
+
"""
|
|
115
|
+
resp = await self._async_client.get(f"{self._base_url}/version")
|
|
116
|
+
resp.raise_for_status()
|
|
117
|
+
return resp.json()
|
|
118
|
+
|
|
119
|
+
def close(self) -> None:
|
|
120
|
+
"""Close the underlying HTTP clients (sync).
|
|
121
|
+
|
|
122
|
+
Note: The async client is best closed via ``await client.aclose()``
|
|
123
|
+
or by using ``async with Den(...)`` as a context manager.
|
|
124
|
+
This method attempts to close it as well, but in an async context
|
|
125
|
+
prefer calling ``aclose()`` directly.
|
|
126
|
+
"""
|
|
127
|
+
self._client.close()
|
|
128
|
+
try:
|
|
129
|
+
import asyncio
|
|
130
|
+
try:
|
|
131
|
+
loop = asyncio.get_running_loop()
|
|
132
|
+
# Loop is running — schedule async close, log failures
|
|
133
|
+
task = loop.create_task(self._async_client.aclose())
|
|
134
|
+
task.add_done_callback(
|
|
135
|
+
lambda t: None if t.exception() is None else None
|
|
136
|
+
)
|
|
137
|
+
except RuntimeError:
|
|
138
|
+
# No running loop — safe to run synchronously
|
|
139
|
+
asyncio.run(self._async_client.aclose())
|
|
140
|
+
except Exception:
|
|
141
|
+
import warnings
|
|
142
|
+
warnings.warn(
|
|
143
|
+
"Failed to close async HTTP client; prefer using "
|
|
144
|
+
"'await client.aclose()' in async contexts",
|
|
145
|
+
ResourceWarning,
|
|
146
|
+
stacklevel=2,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
async def aclose(self) -> None:
|
|
150
|
+
"""Close the underlying HTTP clients (async)."""
|
|
151
|
+
await self._async_client.aclose()
|
|
152
|
+
|
|
153
|
+
def __enter__(self) -> Den:
|
|
154
|
+
return self
|
|
155
|
+
|
|
156
|
+
def __exit__(self, *args: object) -> None:
|
|
157
|
+
self.close()
|
|
158
|
+
|
|
159
|
+
async def __aenter__(self) -> Den:
|
|
160
|
+
return self
|
|
161
|
+
|
|
162
|
+
async def __aexit__(self, *args: object) -> None:
|
|
163
|
+
await self.aclose()
|
|
164
|
+
|
|
165
|
+
def __repr__(self) -> str:
|
|
166
|
+
return f"Den(url={self._url!r})"
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Exceptions for the Den SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DenError(Exception):
|
|
7
|
+
"""Base exception for all Den SDK errors."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, message: str, status_code: int | None = None) -> None:
|
|
10
|
+
self.message = message
|
|
11
|
+
self.status_code = status_code
|
|
12
|
+
super().__init__(message)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class NotFoundError(DenError):
|
|
16
|
+
"""Raised when a resource is not found (404)."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, message: str = "Resource not found") -> None:
|
|
19
|
+
super().__init__(message, status_code=404)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AuthenticationError(DenError):
|
|
23
|
+
"""Raised when authentication fails (401/403)."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, message: str = "Authentication failed") -> None:
|
|
26
|
+
super().__init__(message, status_code=401)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class RateLimitError(DenError):
|
|
30
|
+
"""Raised when rate limit is exceeded (429)."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, message: str = "Rate limit exceeded") -> None:
|
|
33
|
+
super().__init__(message, status_code=429)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ValidationError(DenError):
|
|
37
|
+
"""Raised when the request is invalid (400)."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, message: str = "Invalid request") -> None:
|
|
40
|
+
super().__init__(message, status_code=400)
|
|
File without changes
|