trashdb 0.1.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.
trashdb-0.1.0/PKG-INFO ADDED
@@ -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,25 @@
1
+ # TrashDB Python SDK
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/trashdb)](https://pypi.org/project/trashdb/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/trashdb)](https://pypi.org/project/trashdb/)
5
+ [![License](https://img.shields.io/pypi/l/trashdb)](LICENSE)
6
+
7
+ Official Python SDK for [TrashDB](https://trashdb.dev) — ephemeral database containers on demand.
8
+
9
+ ```python
10
+ from trashdb import TrashDB, TrashDBOptions, CreateContainerParams
11
+
12
+ db = TrashDB(TrashDBOptions(api_key="trdb_your_key_here"))
13
+ container = db.create_container(CreateContainerParams(engine="postgres", ttl_minutes=5))
14
+ print(container.connection_string)
15
+ ```
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ pip install trashdb
21
+ ```
22
+
23
+ ## Documentation
24
+
25
+ See https://trashdb.dev/docs
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["setuptools>=70", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "trashdb"
7
+ version = "0.1.0"
8
+ description = "Official Python SDK for TrashDB — ephemeral database containers on demand"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ authors = [
12
+ { name = "trashdb", email = "hello@trashdb.dev" },
13
+ ]
14
+ keywords = [
15
+ "trashdb",
16
+ "ephemeral",
17
+ "database",
18
+ "docker",
19
+ "postgres",
20
+ "mongodb",
21
+ "redis",
22
+ "chromadb",
23
+ "qdrant",
24
+ ]
25
+ classifiers = [
26
+ "Development Status :: 4 - Beta",
27
+ "Intended Audience :: Developers",
28
+ "Programming Language :: Python :: 3",
29
+ "Programming Language :: Python :: 3.9",
30
+ "Programming Language :: Python :: 3.10",
31
+ "Programming Language :: Python :: 3.11",
32
+ "Programming Language :: Python :: 3.12",
33
+ "Programming Language :: Python :: 3.13",
34
+ ]
35
+ requires-python = ">=3.9"
36
+
37
+ [tool.setuptools.packages.find]
38
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ ]
@@ -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)
@@ -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}
@@ -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,11 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/trashdb/__init__.py
4
+ src/trashdb/client.py
5
+ src/trashdb/errors.py
6
+ src/trashdb/types.py
7
+ src/trashdb.egg-info/PKG-INFO
8
+ src/trashdb.egg-info/SOURCES.txt
9
+ src/trashdb.egg-info/dependency_links.txt
10
+ src/trashdb.egg-info/top_level.txt
11
+ tests/test_client.py
@@ -0,0 +1 @@
1
+ trashdb
@@ -0,0 +1,105 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from typing import Generator
5
+
6
+ import pytest
7
+
8
+ from trashdb import (
9
+ ContainerResponse,
10
+ CreateContainerParams,
11
+ EngineInfo,
12
+ TrashDB,
13
+ TrashDBAPIError,
14
+ TrashDBOptions,
15
+ )
16
+
17
+
18
+ @pytest.fixture
19
+ def api_key() -> str:
20
+ key = os.environ.get("TRASHDB_API_KEY")
21
+ if not key:
22
+ pytest.skip("TRASHDB_API_KEY not set — skipping integration tests")
23
+ return key
24
+
25
+
26
+ @pytest.fixture
27
+ def client(api_key: str) -> Generator[TrashDB, None, None]:
28
+ db = TrashDB(TrashDBOptions(api_key=api_key))
29
+ yield db
30
+ # No explicit close needed — urllib has no persistent connection
31
+
32
+
33
+ class TestEngines:
34
+ def test_list_engines(self, client: TrashDB) -> None:
35
+ engines = client.get_engines()
36
+ assert isinstance(engines, list)
37
+ assert len(engines) > 0
38
+
39
+ engine = engines[0]
40
+ assert isinstance(engine, EngineInfo)
41
+ assert engine.id
42
+ assert engine.name
43
+ assert engine.max_ttl_minutes > 0
44
+
45
+
46
+ class TestContainers:
47
+ @pytest.fixture
48
+ def container(self, client: TrashDB) -> Generator[ContainerResponse, None, None]:
49
+ params = CreateContainerParams(engine="postgres", ttl_minutes=3)
50
+ c = client.create_container(params)
51
+ yield c
52
+ client.destroy_container(c.id)
53
+
54
+ def test_create_and_destroy(self, client: TrashDB) -> None:
55
+ params = CreateContainerParams(engine="postgres", ttl_minutes=3)
56
+ c = client.create_container(params)
57
+ assert isinstance(c, ContainerResponse)
58
+ assert c.id
59
+ assert c.engine == "postgres"
60
+ assert c.port > 0
61
+ assert c.connection_string
62
+ assert c.ttl_minutes == 3
63
+
64
+ destroyed = client.destroy_container(c.id)
65
+ assert destroyed is True
66
+
67
+ def test_create_with_name(self, client: TrashDB) -> None:
68
+ params = CreateContainerParams(
69
+ engine="postgres", ttl_minutes=3, name="my-integration-test"
70
+ )
71
+ c = client.create_container(params)
72
+ assert c.name == "my-integration-test"
73
+ client.destroy_container(c.id)
74
+
75
+ def test_get_running_containers(
76
+ self, client: TrashDB, container: ContainerResponse
77
+ ) -> None:
78
+ containers = client.get_running_containers()
79
+ ids = [c.id for c in containers]
80
+ assert container.id in ids
81
+
82
+ def test_destroy_nonexistent(self, client: TrashDB) -> None:
83
+ result = client.destroy_container("nonexistent-id")
84
+ assert result is False
85
+
86
+ def test_unauthorized(self) -> None:
87
+ bad_client = TrashDB(TrashDBOptions(api_key="bad-key"))
88
+ with pytest.raises(TrashDBAPIError) as exc_info:
89
+ bad_client.create_container(CreateContainerParams(engine="postgres"))
90
+ assert exc_info.value.status == 401
91
+ assert exc_info.value.code == 4001
92
+
93
+ def test_unsupported_engine(self, client: TrashDB) -> None:
94
+ with pytest.raises(TrashDBAPIError) as exc_info:
95
+ client.create_container(CreateContainerParams(engine="nonexistent"))
96
+ assert exc_info.value.code == 1001
97
+
98
+ def test_container_logs(self, client: TrashDB) -> None:
99
+ params = CreateContainerParams(engine="postgres", ttl_minutes=3)
100
+ c = client.create_container(params)
101
+ try:
102
+ logs = client.get_container_logs(c.id)
103
+ assert isinstance(logs, str)
104
+ finally:
105
+ client.destroy_container(c.id)