dbbyte 0.1.0__tar.gz → 0.2.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dbbyte
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Official DBByte SDK for Python
5
5
  Author: DBByte, LLC
6
6
  License: MIT
@@ -21,7 +21,7 @@ Official DBByte SDK for Python. A thin, typed wrapper over the DBByte v1 REST AP
21
21
 
22
22
  ## Status
23
23
 
24
- Covers object storage: upload, list, download, delete, versioning, and signed URLs. There is no bucket management, CDN cache control, or analytics API yet - those aren't part of the v1 REST API, so they aren't in the SDK.
24
+ Covers object storage (upload, list, download, delete, versioning, signed URLs, signed upload URLs), bucket management, CDN cache purge, and Serve data APIs. There is no analytics API.
25
25
 
26
26
  ## Installation
27
27
 
@@ -161,3 +161,26 @@ except DBByteError as e:
161
161
  ## License
162
162
 
163
163
  MIT
164
+
165
+ ## Buckets, cache purge, upload links, and data APIs
166
+
167
+ ```python
168
+ db.buckets.list()
169
+ db.buckets.create("Media", "us-east", is_public=True)
170
+ db.buckets.update("media", versioning_enabled=True)
171
+ db.buckets.delete("media")
172
+
173
+ # Needs a write-permission API key.
174
+ db.cdn.purge("media", scope="file", path="logo.png")
175
+ db.cdn.purge("media", scope="bucket")
176
+
177
+ # Direct browser uploads without exposing your API key.
178
+ link = db.objects.upload_url("media", "photo.jpg", content_type="image/jpeg", size=2_000_000)
179
+
180
+ # Serve data APIs
181
+ db.data.get("people", limit=20, filters={"city": "Oslo"})
182
+ db.data.create("people", {"name": "Ada"})
183
+ db.data.update("people", "record-id", {"name": "Ada L."})
184
+ db.data.append_text("notes", "New line")
185
+ db.data.delete("people", "record-id")
186
+ ```
@@ -1,146 +1,169 @@
1
- # dbbyte
2
-
3
- Official DBByte SDK for Python. A thin, typed wrapper over the DBByte v1 REST API. Standard library only - no dependencies.
4
-
5
- ## Status
6
-
7
- Covers object storage: upload, list, download, delete, versioning, and signed URLs. There is no bucket management, CDN cache control, or analytics API yet - those aren't part of the v1 REST API, so they aren't in the SDK.
8
-
9
- ## Installation
10
-
11
- ```bash
12
- pip install dbbyte
13
- ```
14
-
15
- Requires Python 3.9+.
16
-
17
- ## Usage
18
-
19
- ```python
20
- import os
21
- from dbbyte import DBByte
22
-
23
- db = DBByte(api_key=os.environ["DBBYTE_API_KEY"])
24
- ```
25
-
26
- | Argument | Type | Description |
27
- | --- | --- | --- |
28
- | `api_key` | `str` | Your DBByte API key. Required. |
29
- | `base_url` | `str` | Override the API base URL. Defaults to `https://api.dbbyte.com`. |
30
- | `timeout` | `float` | Request timeout in seconds. Defaults to `30.0`. |
31
-
32
- ### Health check
33
-
34
- ```python
35
- status = db.health()
36
- ```
37
-
38
- ### Upload
39
-
40
- ```python
41
- with open("logo.png", "rb") as f:
42
- body = f.read()
43
-
44
- obj = db.objects.upload(
45
- "my-bucket",
46
- key="images/logo.png",
47
- body=body,
48
- content_type="image/png",
49
- )
50
-
51
- # Re-uploading the same name fails with 409 unless you opt in:
52
- db.objects.upload("my-bucket", key="images/logo.png", body=body, overwrite=True)
53
- ```
54
-
55
- ### List
56
-
57
- ```python
58
- result = db.objects.list("my-bucket")
59
- files = result["files"]
60
-
61
- # Paginate:
62
- next_page = db.objects.list("my-bucket", cursor=result["nextCursor"])
63
- ```
64
-
65
- ### Download
66
-
67
- ```python
68
- result = db.objects.download("my-bucket", "images/logo.png")
69
- data = result["data"] # bytes
70
-
71
- # A specific historical version:
72
- db.objects.download("my-bucket", "images/logo.png", version="some-version-key")
73
-
74
- # Every version, zipped:
75
- zip_result = db.objects.download_all_versions("my-bucket", "images/logo.png")
76
- ```
77
-
78
- ### Versions
79
-
80
- ```python
81
- result = db.objects.versions("my-bucket", "images/logo.png")
82
- ```
83
-
84
- ### Signed URLs
85
-
86
- ```python
87
- result = db.objects.sign("my-bucket", "images/logo.png", expires_in=3600) # max 604800 (7 days)
88
- url = result["url"]
89
- ```
90
-
91
- ### Delete
92
-
93
- ```python
94
- db.objects.delete("my-bucket", "images/logo.png")
95
-
96
- # A specific version only:
97
- db.objects.delete("my-bucket", "images/logo.png", version="some-version-key")
98
-
99
- # Every version, permanently:
100
- db.objects.delete_all_versions("my-bucket", "images/logo.png")
101
- ```
102
-
103
- ## Error handling
104
-
105
- All SDK methods raise a `DBByteError` on API errors.
106
-
107
- ```python
108
- from dbbyte import DBByte, DBByteError
109
-
110
- db = DBByte(api_key=os.environ["DBBYTE_API_KEY"])
111
-
112
- try:
113
- db.objects.download("my-bucket", "does-not-exist.png")
114
- except DBByteError as e:
115
- print(e.status) # e.g. 404
116
- print(e.message) # e.g. "File not found"
117
- print(e.hint) # present on some errors, e.g. upload conflicts
118
- ```
119
-
120
- ### Rate limits vs quota limits
121
-
122
- A `429` can mean two different things, and only one is worth retrying:
123
-
124
- - **Burst rate limit** - `e.retry_after` is set (seconds until you can retry). Transient, safe to retry after that delay.
125
- - **Plan API request quota exceeded** - `e.retry_after` is `None`. Retrying immediately won't help; you're out of quota until your billing period resets or you upgrade.
126
-
127
- ```python
128
- except DBByteError as e:
129
- if e.status == 429:
130
- if e.retry_after is not None:
131
- # wait e.retry_after seconds, then retry
132
- pass
133
- else:
134
- # plan quota exceeded, don't retry blindly
135
- pass
136
- ```
137
-
138
- ## Known limitations
139
-
140
- - **Upload allowed types**: the server enforces a fixed MIME type whitelist (common image, video, audio, PDF, Word, and plain-text formats). Uploading anything else returns a `400` from the server - the SDK doesn't pre-validate this client-side.
141
- - **50MB max file size**, enforced server-side.
142
- - **Synchronous only.** No async client yet.
143
-
144
- ## License
145
-
146
- MIT
1
+ # dbbyte
2
+
3
+ Official DBByte SDK for Python. A thin, typed wrapper over the DBByte v1 REST API. Standard library only - no dependencies.
4
+
5
+ ## Status
6
+
7
+ Covers object storage (upload, list, download, delete, versioning, signed URLs, signed upload URLs), bucket management, CDN cache purge, and Serve data APIs. There is no analytics API.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ pip install dbbyte
13
+ ```
14
+
15
+ Requires Python 3.9+.
16
+
17
+ ## Usage
18
+
19
+ ```python
20
+ import os
21
+ from dbbyte import DBByte
22
+
23
+ db = DBByte(api_key=os.environ["DBBYTE_API_KEY"])
24
+ ```
25
+
26
+ | Argument | Type | Description |
27
+ | --- | --- | --- |
28
+ | `api_key` | `str` | Your DBByte API key. Required. |
29
+ | `base_url` | `str` | Override the API base URL. Defaults to `https://api.dbbyte.com`. |
30
+ | `timeout` | `float` | Request timeout in seconds. Defaults to `30.0`. |
31
+
32
+ ### Health check
33
+
34
+ ```python
35
+ status = db.health()
36
+ ```
37
+
38
+ ### Upload
39
+
40
+ ```python
41
+ with open("logo.png", "rb") as f:
42
+ body = f.read()
43
+
44
+ obj = db.objects.upload(
45
+ "my-bucket",
46
+ key="images/logo.png",
47
+ body=body,
48
+ content_type="image/png",
49
+ )
50
+
51
+ # Re-uploading the same name fails with 409 unless you opt in:
52
+ db.objects.upload("my-bucket", key="images/logo.png", body=body, overwrite=True)
53
+ ```
54
+
55
+ ### List
56
+
57
+ ```python
58
+ result = db.objects.list("my-bucket")
59
+ files = result["files"]
60
+
61
+ # Paginate:
62
+ next_page = db.objects.list("my-bucket", cursor=result["nextCursor"])
63
+ ```
64
+
65
+ ### Download
66
+
67
+ ```python
68
+ result = db.objects.download("my-bucket", "images/logo.png")
69
+ data = result["data"] # bytes
70
+
71
+ # A specific historical version:
72
+ db.objects.download("my-bucket", "images/logo.png", version="some-version-key")
73
+
74
+ # Every version, zipped:
75
+ zip_result = db.objects.download_all_versions("my-bucket", "images/logo.png")
76
+ ```
77
+
78
+ ### Versions
79
+
80
+ ```python
81
+ result = db.objects.versions("my-bucket", "images/logo.png")
82
+ ```
83
+
84
+ ### Signed URLs
85
+
86
+ ```python
87
+ result = db.objects.sign("my-bucket", "images/logo.png", expires_in=3600) # max 604800 (7 days)
88
+ url = result["url"]
89
+ ```
90
+
91
+ ### Delete
92
+
93
+ ```python
94
+ db.objects.delete("my-bucket", "images/logo.png")
95
+
96
+ # A specific version only:
97
+ db.objects.delete("my-bucket", "images/logo.png", version="some-version-key")
98
+
99
+ # Every version, permanently:
100
+ db.objects.delete_all_versions("my-bucket", "images/logo.png")
101
+ ```
102
+
103
+ ## Error handling
104
+
105
+ All SDK methods raise a `DBByteError` on API errors.
106
+
107
+ ```python
108
+ from dbbyte import DBByte, DBByteError
109
+
110
+ db = DBByte(api_key=os.environ["DBBYTE_API_KEY"])
111
+
112
+ try:
113
+ db.objects.download("my-bucket", "does-not-exist.png")
114
+ except DBByteError as e:
115
+ print(e.status) # e.g. 404
116
+ print(e.message) # e.g. "File not found"
117
+ print(e.hint) # present on some errors, e.g. upload conflicts
118
+ ```
119
+
120
+ ### Rate limits vs quota limits
121
+
122
+ A `429` can mean two different things, and only one is worth retrying:
123
+
124
+ - **Burst rate limit** - `e.retry_after` is set (seconds until you can retry). Transient, safe to retry after that delay.
125
+ - **Plan API request quota exceeded** - `e.retry_after` is `None`. Retrying immediately won't help; you're out of quota until your billing period resets or you upgrade.
126
+
127
+ ```python
128
+ except DBByteError as e:
129
+ if e.status == 429:
130
+ if e.retry_after is not None:
131
+ # wait e.retry_after seconds, then retry
132
+ pass
133
+ else:
134
+ # plan quota exceeded, don't retry blindly
135
+ pass
136
+ ```
137
+
138
+ ## Known limitations
139
+
140
+ - **Upload allowed types**: the server enforces a fixed MIME type whitelist (common image, video, audio, PDF, Word, and plain-text formats). Uploading anything else returns a `400` from the server - the SDK doesn't pre-validate this client-side.
141
+ - **50MB max file size**, enforced server-side.
142
+ - **Synchronous only.** No async client yet.
143
+
144
+ ## License
145
+
146
+ MIT
147
+
148
+ ## Buckets, cache purge, upload links, and data APIs
149
+
150
+ ```python
151
+ db.buckets.list()
152
+ db.buckets.create("Media", "us-east", is_public=True)
153
+ db.buckets.update("media", versioning_enabled=True)
154
+ db.buckets.delete("media")
155
+
156
+ # Needs a write-permission API key.
157
+ db.cdn.purge("media", scope="file", path="logo.png")
158
+ db.cdn.purge("media", scope="bucket")
159
+
160
+ # Direct browser uploads without exposing your API key.
161
+ link = db.objects.upload_url("media", "photo.jpg", content_type="image/jpeg", size=2_000_000)
162
+
163
+ # Serve data APIs
164
+ db.data.get("people", limit=20, filters={"city": "Oslo"})
165
+ db.data.create("people", {"name": "Ada"})
166
+ db.data.update("people", "record-id", {"name": "Ada L."})
167
+ db.data.append_text("notes", "New line")
168
+ db.data.delete("people", "record-id")
169
+ ```
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dbbyte"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  description = "Official DBByte SDK for Python"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -1,49 +1,77 @@
1
- from __future__ import annotations
2
-
3
- from typing import Optional
4
-
5
- from .client import DEFAULT_BASE_URL, DEFAULT_TIMEOUT, DBByteClient
6
- from .errors import DBByteError
7
- from .objects import ObjectsResource
8
- from .types import (
9
- DeleteResponse,
10
- DownloadResult,
11
- FileListItem,
12
- FileVersionPreview,
13
- HealthResponse,
14
- ListObjectsResponse,
15
- ListVersionsResponse,
16
- SignedUrlResponse,
17
- UploadedObject,
18
- VersionEntry,
19
- )
20
-
21
- __all__ = [
22
- "DBByte",
23
- "DBByteError",
24
- "HealthResponse",
25
- "UploadedObject",
26
- "ListObjectsResponse",
27
- "FileListItem",
28
- "FileVersionPreview",
29
- "SignedUrlResponse",
30
- "ListVersionsResponse",
31
- "VersionEntry",
32
- "DeleteResponse",
33
- "DownloadResult",
34
- ]
35
-
36
-
37
- class DBByte:
38
- def __init__(
39
- self,
40
- api_key: str,
41
- base_url: str = DEFAULT_BASE_URL,
42
- timeout: float = DEFAULT_TIMEOUT,
43
- ) -> None:
44
- self._client = DBByteClient(api_key, base_url, timeout)
45
- self.objects = ObjectsResource(self._client)
46
-
47
- def health(self) -> HealthResponse:
48
- result = self._client.request_json("GET", "/health")
49
- return result # type: ignore[no-any-return]
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+
5
+ from .client import DEFAULT_BASE_URL, DEFAULT_TIMEOUT, DBByteClient
6
+ from .errors import DBByteError
7
+ from .buckets import BucketsResource
8
+ from .cdn import CdnResource
9
+ from .data import DataResource
10
+ from .objects import ObjectsResource
11
+ from .types import (
12
+ AppendTextResponse,
13
+ Bucket,
14
+ BucketDeleteResponse,
15
+ BucketListResponse,
16
+ BucketResponse,
17
+ DataDeleteResponse,
18
+ DataPage,
19
+ DataRecord,
20
+ DataRecordResponse,
21
+ PurgeResponse,
22
+ UploadUrlResponse,
23
+ DeleteResponse,
24
+ DownloadResult,
25
+ FileListItem,
26
+ FileVersionPreview,
27
+ HealthResponse,
28
+ ListObjectsResponse,
29
+ ListVersionsResponse,
30
+ SignedUrlResponse,
31
+ UploadedObject,
32
+ VersionEntry,
33
+ )
34
+
35
+ __all__ = [
36
+ "DBByte",
37
+ "DBByteError",
38
+ "HealthResponse",
39
+ "UploadedObject",
40
+ "ListObjectsResponse",
41
+ "FileListItem",
42
+ "FileVersionPreview",
43
+ "SignedUrlResponse",
44
+ "ListVersionsResponse",
45
+ "VersionEntry",
46
+ "DeleteResponse",
47
+ "DownloadResult",
48
+ "Bucket",
49
+ "BucketListResponse",
50
+ "BucketResponse",
51
+ "BucketDeleteResponse",
52
+ "PurgeResponse",
53
+ "DataRecord",
54
+ "DataPage",
55
+ "DataRecordResponse",
56
+ "AppendTextResponse",
57
+ "DataDeleteResponse",
58
+ "UploadUrlResponse",
59
+ ]
60
+
61
+
62
+ class DBByte:
63
+ def __init__(
64
+ self,
65
+ api_key: str,
66
+ base_url: str = DEFAULT_BASE_URL,
67
+ timeout: float = DEFAULT_TIMEOUT,
68
+ ) -> None:
69
+ self._client = DBByteClient(api_key, base_url, timeout)
70
+ self.buckets = BucketsResource(self._client)
71
+ self.cdn = CdnResource(self._client)
72
+ self.data = DataResource(self._client)
73
+ self.objects = ObjectsResource(self._client)
74
+
75
+ def health(self) -> HealthResponse:
76
+ result = self._client.request_json("GET", "/health")
77
+ return result # type: ignore[no-any-return]
@@ -0,0 +1,64 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, Optional
4
+ from urllib.parse import quote
5
+
6
+ from .client import DBByteClient
7
+ from .types import (
8
+ BucketDeleteResponse,
9
+ BucketListResponse,
10
+ BucketResponse,
11
+ )
12
+
13
+
14
+ class BucketsResource:
15
+ def __init__(self, client: DBByteClient) -> None:
16
+ self._client = client
17
+
18
+ def list(self) -> BucketListResponse:
19
+ return self._client.request_json("GET", "/buckets") # type: ignore[no-any-return]
20
+
21
+ def get(self, slug: str) -> BucketResponse:
22
+ return self._client.request_json( # type: ignore[no-any-return]
23
+ "GET", f"/buckets/{quote(slug, safe='')}"
24
+ )
25
+
26
+ def create(
27
+ self,
28
+ name: str,
29
+ region: str,
30
+ replica_count: Optional[int] = None,
31
+ is_public: Optional[bool] = None,
32
+ cdn: Optional[bool] = None,
33
+ ) -> BucketResponse:
34
+ body: Dict[str, Any] = {"name": name, "region": region}
35
+ if replica_count is not None:
36
+ body["replicaCount"] = replica_count
37
+ if is_public is not None:
38
+ body["isPublic"] = is_public
39
+ if cdn is not None:
40
+ body["cdn"] = cdn
41
+ return self._client.request_json("POST", "/buckets", body=body) # type: ignore[no-any-return]
42
+
43
+ def update(
44
+ self,
45
+ slug: str,
46
+ versioning_enabled: Optional[bool] = None,
47
+ is_public: Optional[bool] = None,
48
+ cdn: Optional[bool] = None,
49
+ ) -> BucketResponse:
50
+ body: Dict[str, Any] = {}
51
+ if versioning_enabled is not None:
52
+ body["versioningEnabled"] = versioning_enabled
53
+ if is_public is not None:
54
+ body["isPublic"] = is_public
55
+ if cdn is not None:
56
+ body["cdn"] = cdn
57
+ return self._client.request_json( # type: ignore[no-any-return]
58
+ "PATCH", f"/buckets/{quote(slug, safe='')}", body=body
59
+ )
60
+
61
+ def delete(self, slug: str) -> BucketDeleteResponse:
62
+ return self._client.request_json( # type: ignore[no-any-return]
63
+ "DELETE", f"/buckets/{quote(slug, safe='')}"
64
+ )
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+ from .client import DBByteClient
6
+ from .types import PurgeResponse
7
+
8
+
9
+ class CdnResource:
10
+ def __init__(self, client: DBByteClient) -> None:
11
+ self._client = client
12
+
13
+ def purge(
14
+ self, slug: str, scope: str = "bucket", path: Optional[str] = None
15
+ ) -> PurgeResponse:
16
+ """Purge one public file (scope="file", with path) or every public
17
+ file in a bucket (scope="bucket"). The API key needs write
18
+ permission."""
19
+ if scope not in ("file", "bucket"):
20
+ raise ValueError('scope must be "file" or "bucket"')
21
+ if scope == "file" and not path:
22
+ raise ValueError('path is required when scope is "file"')
23
+ body: Dict[str, Any] = {"slug": slug, "scope": scope}
24
+ if path:
25
+ body["path"] = path
26
+ return self._client.request_json("POST", "/purge", body=body) # type: ignore[no-any-return]
@@ -0,0 +1,63 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, List, Optional
4
+ from urllib.parse import quote
5
+
6
+ from .client import DBByteClient
7
+ from .types import (
8
+ AppendTextResponse,
9
+ DataDeleteResponse,
10
+ DataPage,
11
+ DataRecord,
12
+ DataRecordResponse,
13
+ )
14
+
15
+
16
+ def _seg(value: str) -> str:
17
+ return quote(value, safe="")
18
+
19
+
20
+ class DataResource:
21
+ """Serve data APIs published from CSV, JSON, or Google Docs."""
22
+
23
+ def __init__(self, client: DBByteClient) -> None:
24
+ self._client = client
25
+
26
+ def get(
27
+ self,
28
+ slug: str,
29
+ fields: Optional[List[str]] = None,
30
+ cursor: Optional[str] = None,
31
+ limit: Optional[int] = None,
32
+ filters: Optional[Dict[str, str]] = None,
33
+ ) -> DataPage:
34
+ query: Dict[str, Any] = dict(filters or {})
35
+ if fields:
36
+ query["fields"] = ",".join(fields)
37
+ query["cursor"] = cursor
38
+ query["limit"] = limit
39
+ return self._client.request_json( # type: ignore[no-any-return]
40
+ "GET", f"/v1/data/{_seg(slug)}", query=query
41
+ )
42
+
43
+ def create(self, slug: str, record: DataRecord) -> DataRecordResponse:
44
+ return self._client.request_json( # type: ignore[no-any-return]
45
+ "POST", f"/v1/data/{_seg(slug)}", body=record
46
+ )
47
+
48
+ def append_text(self, slug: str, text: str) -> AppendTextResponse:
49
+ return self._client.request_json( # type: ignore[no-any-return]
50
+ "POST", f"/v1/data/{_seg(slug)}", body={"appendText": text}
51
+ )
52
+
53
+ def update(
54
+ self, slug: str, record_id: str, patch: DataRecord
55
+ ) -> DataRecordResponse:
56
+ return self._client.request_json( # type: ignore[no-any-return]
57
+ "PATCH", f"/v1/data/{_seg(slug)}/{_seg(record_id)}", body=patch
58
+ )
59
+
60
+ def delete(self, slug: str, record_id: str) -> DataDeleteResponse:
61
+ return self._client.request_json( # type: ignore[no-any-return]
62
+ "DELETE", f"/v1/data/{_seg(slug)}/{_seg(record_id)}"
63
+ )