trashdb 0.1.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.
- trashdb/__init__.py +17 -0
- trashdb/client.py +156 -0
- trashdb/errors.py +20 -0
- trashdb/types.py +47 -0
- trashdb-0.1.0.dist-info/METADATA +43 -0
- trashdb-0.1.0.dist-info/RECORD +8 -0
- trashdb-0.1.0.dist-info/WHEEL +5 -0
- trashdb-0.1.0.dist-info/top_level.txt +1 -0
trashdb/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from .client import TrashDB
|
|
2
|
+
from .errors import TrashDBAPIError
|
|
3
|
+
from .types import (
|
|
4
|
+
ContainerResponse,
|
|
5
|
+
CreateContainerParams,
|
|
6
|
+
EngineInfo,
|
|
7
|
+
TrashDBOptions,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"TrashDB",
|
|
12
|
+
"TrashDBAPIError",
|
|
13
|
+
"TrashDBOptions",
|
|
14
|
+
"CreateContainerParams",
|
|
15
|
+
"ContainerResponse",
|
|
16
|
+
"EngineInfo",
|
|
17
|
+
]
|
trashdb/client.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
import urllib.error
|
|
6
|
+
import urllib.request
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
from .errors import RETRYABLE_STATUSES, TrashDBAPIError
|
|
10
|
+
from .types import (
|
|
11
|
+
ContainerResponse,
|
|
12
|
+
CreateContainerParams,
|
|
13
|
+
EngineInfo,
|
|
14
|
+
TrashDBOptions,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _dict_to_container(data: dict[str, Any]) -> ContainerResponse:
|
|
19
|
+
return ContainerResponse(
|
|
20
|
+
id=data["id"],
|
|
21
|
+
engine=data["engine"],
|
|
22
|
+
port=data["port"],
|
|
23
|
+
connection_string=data["connectionString"],
|
|
24
|
+
created_at=data["createdAt"],
|
|
25
|
+
ttl_minutes=data["ttlMinutes"],
|
|
26
|
+
name=data.get("name"),
|
|
27
|
+
expires_at=data.get("expiresAt"),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _dict_to_engine(data: dict[str, Any]) -> EngineInfo:
|
|
32
|
+
return EngineInfo(
|
|
33
|
+
id=data["id"],
|
|
34
|
+
name=data["name"],
|
|
35
|
+
max_ttl_minutes=data["maxTtlMinutes"],
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TrashDB:
|
|
40
|
+
def __init__(self, options: TrashDBOptions) -> None:
|
|
41
|
+
self._api_key = options.api_key
|
|
42
|
+
self._max_retries = max(0, options.max_retries)
|
|
43
|
+
self._initial_backoff_ms = options.initial_backoff_ms
|
|
44
|
+
self._base_url = options.base_url.rstrip("/")
|
|
45
|
+
|
|
46
|
+
def create_container(self, params: CreateContainerParams) -> ContainerResponse:
|
|
47
|
+
body = {
|
|
48
|
+
"engine": params.engine,
|
|
49
|
+
"ttlMinutes": params.ttl_minutes,
|
|
50
|
+
}
|
|
51
|
+
if params.name is not None:
|
|
52
|
+
body["name"] = params.name
|
|
53
|
+
|
|
54
|
+
data = self._request(
|
|
55
|
+
f"{self._base_url}/containers",
|
|
56
|
+
method="POST",
|
|
57
|
+
headers={"Content-Type": "application/json", "x-api-key": self._api_key},
|
|
58
|
+
body=json.dumps(body).encode("utf-8"),
|
|
59
|
+
)
|
|
60
|
+
return _dict_to_container(data)
|
|
61
|
+
|
|
62
|
+
def get_running_containers(self) -> list[ContainerResponse]:
|
|
63
|
+
data = self._request(
|
|
64
|
+
f"{self._base_url}/containers",
|
|
65
|
+
headers={"x-api-key": self._api_key},
|
|
66
|
+
)
|
|
67
|
+
return [_dict_to_container(c) for c in data]
|
|
68
|
+
|
|
69
|
+
def destroy_container(self, container_id: str) -> bool:
|
|
70
|
+
try:
|
|
71
|
+
self._raw_request(
|
|
72
|
+
f"{self._base_url}/containers/{container_id}",
|
|
73
|
+
method="DELETE",
|
|
74
|
+
headers={"x-api-key": self._api_key},
|
|
75
|
+
)
|
|
76
|
+
return True
|
|
77
|
+
except TrashDBAPIError as exc:
|
|
78
|
+
if exc.status == 404:
|
|
79
|
+
return False
|
|
80
|
+
raise
|
|
81
|
+
|
|
82
|
+
def get_engines(self) -> list[EngineInfo]:
|
|
83
|
+
data = self._request(f"{self._base_url}/engines")
|
|
84
|
+
return [_dict_to_engine(e) for e in data]
|
|
85
|
+
|
|
86
|
+
def get_container_logs(
|
|
87
|
+
self,
|
|
88
|
+
container_id: str,
|
|
89
|
+
tail: int = 200,
|
|
90
|
+
since_seconds: Optional[int] = None,
|
|
91
|
+
) -> str:
|
|
92
|
+
url = f"{self._base_url}/containers/{container_id}/logs?tail={tail}"
|
|
93
|
+
if since_seconds is not None:
|
|
94
|
+
url += f"&sinceSeconds={since_seconds}"
|
|
95
|
+
|
|
96
|
+
data = self._request(url, headers={"x-api-key": self._api_key})
|
|
97
|
+
return data["logs"]
|
|
98
|
+
|
|
99
|
+
# ------------------------------------------------------------------
|
|
100
|
+
# Internal helpers
|
|
101
|
+
# ------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
def _request(
|
|
104
|
+
self,
|
|
105
|
+
url: str,
|
|
106
|
+
method: str = "GET",
|
|
107
|
+
headers: Optional[dict[str, str]] = None,
|
|
108
|
+
body: Optional[bytes] = None,
|
|
109
|
+
) -> Any:
|
|
110
|
+
res = self._raw_request(url, method=method, headers=headers, body=body)
|
|
111
|
+
content = res.read()
|
|
112
|
+
if not content:
|
|
113
|
+
return None
|
|
114
|
+
return json.loads(content.decode("utf-8"))
|
|
115
|
+
|
|
116
|
+
def _raw_request(
|
|
117
|
+
self,
|
|
118
|
+
url: str,
|
|
119
|
+
method: str = "GET",
|
|
120
|
+
headers: Optional[dict[str, str]] = None,
|
|
121
|
+
body: Optional[bytes] = None,
|
|
122
|
+
) -> Any:
|
|
123
|
+
max_attempts = self._max_retries + 1
|
|
124
|
+
|
|
125
|
+
for attempt in range(1, max_attempts + 1):
|
|
126
|
+
req = urllib.request.Request(url, data=body, method=method)
|
|
127
|
+
if headers:
|
|
128
|
+
for k, v in headers.items():
|
|
129
|
+
req.add_header(k, v)
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
return urllib.request.urlopen(req, timeout=30)
|
|
133
|
+
|
|
134
|
+
except urllib.error.HTTPError as exc:
|
|
135
|
+
if attempt == max_attempts or exc.code not in RETRYABLE_STATUSES:
|
|
136
|
+
error_body = exc.read()
|
|
137
|
+
error_data: dict[str, Any] = {}
|
|
138
|
+
if error_body:
|
|
139
|
+
try:
|
|
140
|
+
error_data = json.loads(error_body.decode("utf-8"))
|
|
141
|
+
except (json.JSONDecodeError, ValueError):
|
|
142
|
+
pass
|
|
143
|
+
raise TrashDBAPIError(exc.code, error_data) from exc
|
|
144
|
+
|
|
145
|
+
self._sleep(attempt)
|
|
146
|
+
|
|
147
|
+
except urllib.error.URLError as exc:
|
|
148
|
+
if attempt == max_attempts:
|
|
149
|
+
raise TrashDBAPIError(0, {"message": str(exc.reason)}) from exc
|
|
150
|
+
self._sleep(attempt)
|
|
151
|
+
|
|
152
|
+
raise TrashDBAPIError(0, {"message": "Unreachable"})
|
|
153
|
+
|
|
154
|
+
def _sleep(self, attempt: int) -> None:
|
|
155
|
+
delay_ms = self._initial_backoff_ms * (2 ** (attempt - 1))
|
|
156
|
+
time.sleep(delay_ms / 1000)
|
trashdb/errors.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TrashDBAPIError(Exception):
|
|
7
|
+
status: int
|
|
8
|
+
code: int
|
|
9
|
+
|
|
10
|
+
def __init__(self, status: int, error: dict[str, Any]) -> None:
|
|
11
|
+
self.status = status
|
|
12
|
+
self.code = error.get("code", 0)
|
|
13
|
+
message = error.get("message", "Unknown error")
|
|
14
|
+
super().__init__(message)
|
|
15
|
+
|
|
16
|
+
def __str__(self) -> str:
|
|
17
|
+
return f"[{self.status}] code={self.code}: {self.args[0]}"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
RETRYABLE_STATUSES: set[int] = {502, 503, 504}
|
trashdb/types.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Callable, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class TrashDBOptions:
|
|
9
|
+
api_key: str
|
|
10
|
+
base_url: str = "https://api.trashdb.dev/api/v1"
|
|
11
|
+
max_retries: int = 3
|
|
12
|
+
initial_backoff_ms: int = 500
|
|
13
|
+
http_client: Optional[Callable[..., object]] = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class CreateContainerParams:
|
|
18
|
+
engine: str
|
|
19
|
+
ttl_minutes: int = 5
|
|
20
|
+
name: Optional[str] = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class ContainerResponse:
|
|
25
|
+
id: str
|
|
26
|
+
engine: str
|
|
27
|
+
port: int
|
|
28
|
+
connection_string: str
|
|
29
|
+
created_at: str
|
|
30
|
+
ttl_minutes: int
|
|
31
|
+
name: Optional[str] = None
|
|
32
|
+
expires_at: Optional[str] = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class EngineInfo:
|
|
37
|
+
id: str
|
|
38
|
+
name: str
|
|
39
|
+
max_ttl_minutes: int
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class ContainerLogs:
|
|
44
|
+
logs: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
ErrorInfo = dict # {"code": int, "message": str}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: trashdb
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for TrashDB — ephemeral database containers on demand
|
|
5
|
+
Author-email: trashdb <hello@trashdb.dev>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: trashdb,ephemeral,database,docker,postgres,mongodb,redis,chromadb,qdrant
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# TrashDB Python SDK
|
|
20
|
+
|
|
21
|
+
[](https://pypi.org/project/trashdb/)
|
|
22
|
+
[](https://pypi.org/project/trashdb/)
|
|
23
|
+
[](LICENSE)
|
|
24
|
+
|
|
25
|
+
Official Python SDK for [TrashDB](https://trashdb.dev) — ephemeral database containers on demand.
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from trashdb import TrashDB, TrashDBOptions, CreateContainerParams
|
|
29
|
+
|
|
30
|
+
db = TrashDB(TrashDBOptions(api_key="trdb_your_key_here"))
|
|
31
|
+
container = db.create_container(CreateContainerParams(engine="postgres", ttl_minutes=5))
|
|
32
|
+
print(container.connection_string)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install trashdb
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Documentation
|
|
42
|
+
|
|
43
|
+
See https://trashdb.dev/docs
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
trashdb/__init__.py,sha256=Z4H8nweOhynOHSIPnGEQSOmSzEJ1vV5xRDu0ipj7_eA,320
|
|
2
|
+
trashdb/client.py,sha256=hbpcaCRTMLyojL-P2IP-P2fETXl_kywTHAQNnPk40oo,5079
|
|
3
|
+
trashdb/errors.py,sha256=dVBr2g8JnwiWqtWr-O_9nsI_BL1c9Y2_SezmLwmMriY,501
|
|
4
|
+
trashdb/types.py,sha256=7Izoxp52ApdajU3mLFVPh7t-yQybis64Zn7TTTLKRhc,847
|
|
5
|
+
trashdb-0.1.0.dist-info/METADATA,sha256=AYgowdEM0mG_E5DZEP2_MjH1xYKeMXeQt7hqbPXy8HE,1482
|
|
6
|
+
trashdb-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
7
|
+
trashdb-0.1.0.dist-info/top_level.txt,sha256=MpXvNcOBR7HCJM2BdEiNF1cM6BLPaJIbuARBWkvpQj0,8
|
|
8
|
+
trashdb-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
trashdb
|