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 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
+ [![PyPI version](https://img.shields.io/pypi/v/trashdb)](https://pypi.org/project/trashdb/)
22
+ [![Python versions](https://img.shields.io/pypi/pyversions/trashdb)](https://pypi.org/project/trashdb/)
23
+ [![License](https://img.shields.io/pypi/l/trashdb)](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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ trashdb