dbbyte 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.
dbbyte/__init__.py ADDED
@@ -0,0 +1,49 @@
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]
dbbyte/client.py ADDED
@@ -0,0 +1,165 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import urllib.error
5
+ import urllib.parse
6
+ import urllib.request
7
+ import uuid
8
+ from typing import Any, Dict, NoReturn, Optional
9
+
10
+ from .errors import DBByteError
11
+
12
+ DEFAULT_BASE_URL = "https://api.dbbyte.com"
13
+ DEFAULT_TIMEOUT = 30.0
14
+ USER_AGENT = "dbbyte-python/0.1.0"
15
+
16
+
17
+ class DBByteClient:
18
+ """Thin HTTP wrapper. Paths passed to request_* do NOT include /v1 -
19
+ in production the Cloudflare Worker gateway in front of
20
+ api.dbbyte.com inserts /api/v1 itself. If you point base_url at a
21
+ Next.js dev server directly (bypassing the Worker), include /api/v1
22
+ in base_url instead."""
23
+
24
+ def __init__(
25
+ self,
26
+ api_key: str,
27
+ base_url: str = DEFAULT_BASE_URL,
28
+ timeout: float = DEFAULT_TIMEOUT,
29
+ ) -> None:
30
+ if not api_key:
31
+ raise ValueError("DBByte: api_key is required")
32
+ self.api_key = api_key
33
+ self.base_url = base_url.rstrip("/")
34
+ self.timeout = timeout
35
+
36
+ def _build_url(self, path: str, query: Optional[Dict[str, Any]] = None) -> str:
37
+ url = f"{self.base_url}{path}"
38
+ if query:
39
+ filtered = {k: v for k, v in query.items() if v is not None}
40
+ if filtered:
41
+ url += "?" + urllib.parse.urlencode(filtered)
42
+ return url
43
+
44
+ def _headers(self, extra: Optional[Dict[str, str]] = None) -> Dict[str, str]:
45
+ headers = {
46
+ "Authorization": f"Bearer {self.api_key}",
47
+ "User-Agent": USER_AGENT,
48
+ }
49
+ if extra:
50
+ headers.update(extra)
51
+ return headers
52
+
53
+ def _raise_for_error(self, e: urllib.error.HTTPError) -> NoReturn:
54
+ raw = e.read()
55
+ retry_after = None
56
+ ra_header = e.headers.get("Retry-After") if e.headers else None
57
+ if ra_header is not None:
58
+ try:
59
+ retry_after = int(ra_header)
60
+ except ValueError:
61
+ retry_after = None
62
+ message = e.reason
63
+ hint = None
64
+ try:
65
+ body = json.loads(raw.decode("utf-8"))
66
+ message = body.get("error", message)
67
+ hint = body.get("hint")
68
+ except (ValueError, UnicodeDecodeError):
69
+ # Non-JSON body: this didn't come from the DBByte API itself
70
+ # (which always returns {"error": ...}) - likely blocked
71
+ # upstream, e.g. by Cloudflare's edge/WAF before reaching
72
+ # the app.
73
+ hint = "Response was not JSON - request may have been blocked before reaching the API"
74
+ raise DBByteError(message, e.code, hint, retry_after) from None
75
+
76
+ def request_json(
77
+ self,
78
+ method: str,
79
+ path: str,
80
+ query: Optional[Dict[str, Any]] = None,
81
+ body: Optional[Dict[str, Any]] = None,
82
+ ) -> Any:
83
+ url = self._build_url(path, query)
84
+ data = None
85
+ headers = self._headers()
86
+ if body is not None:
87
+ data = json.dumps(body).encode("utf-8")
88
+ headers["Content-Type"] = "application/json"
89
+ req = urllib.request.Request(url, data=data, headers=headers, method=method)
90
+ try:
91
+ with urllib.request.urlopen(req, timeout=self.timeout) as res:
92
+ raw = res.read()
93
+ return json.loads(raw.decode("utf-8")) if raw else None
94
+ except urllib.error.HTTPError as e:
95
+ self._raise_for_error(e)
96
+ except urllib.error.URLError as e:
97
+ raise DBByteError(f"Request failed: {e.reason}", 0) from None
98
+
99
+ def request_form(
100
+ self,
101
+ method: str,
102
+ path: str,
103
+ filename: str,
104
+ file_bytes: bytes,
105
+ content_type: Optional[str],
106
+ query: Optional[Dict[str, Any]] = None,
107
+ ) -> Any:
108
+ url = self._build_url(path, query)
109
+ boundary = uuid.uuid4().hex
110
+ ct = content_type or "application/octet-stream"
111
+ # Filename is only escaped for embedded quotes, matching the
112
+ # Node SDK's simplicity - the server is the source of truth for
113
+ # what filenames it accepts.
114
+ safe_filename = filename.replace('"', '\\"')
115
+ parts = [
116
+ f"--{boundary}\r\n".encode(),
117
+ (
118
+ f'Content-Disposition: form-data; name="file"; '
119
+ f'filename="{safe_filename}"\r\n'
120
+ f"Content-Type: {ct}\r\n\r\n"
121
+ ).encode(),
122
+ file_bytes,
123
+ f"\r\n--{boundary}--\r\n".encode(),
124
+ ]
125
+ data = b"".join(parts)
126
+
127
+ headers = self._headers(
128
+ {"Content-Type": f"multipart/form-data; boundary={boundary}"}
129
+ )
130
+ req = urllib.request.Request(url, data=data, headers=headers, method=method)
131
+ try:
132
+ with urllib.request.urlopen(req, timeout=self.timeout) as res:
133
+ raw = res.read()
134
+ return json.loads(raw.decode("utf-8")) if raw else None
135
+ except urllib.error.HTTPError as e:
136
+ self._raise_for_error(e)
137
+ except urllib.error.URLError as e:
138
+ raise DBByteError(f"Request failed: {e.reason}", 0) from None
139
+
140
+ def request_binary(
141
+ self,
142
+ method: str,
143
+ path: str,
144
+ query: Optional[Dict[str, Any]] = None,
145
+ ) -> Dict[str, Any]:
146
+ url = self._build_url(path, query)
147
+ req = urllib.request.Request(url, headers=self._headers(), method=method)
148
+ try:
149
+ with urllib.request.urlopen(req, timeout=self.timeout) as res:
150
+ data = res.read()
151
+ content_type = res.headers.get(
152
+ "Content-Type", "application/octet-stream"
153
+ )
154
+ disposition = res.headers.get("Content-Disposition")
155
+ filename = None
156
+ if disposition and "filename=" in disposition:
157
+ filename = disposition.split("filename=", 1)[1].strip('"')
158
+ result: Dict[str, Any] = {"data": data, "content_type": content_type}
159
+ if filename:
160
+ result["filename"] = filename
161
+ return result
162
+ except urllib.error.HTTPError as e:
163
+ self._raise_for_error(e)
164
+ except urllib.error.URLError as e:
165
+ raise DBByteError(f"Request failed: {e.reason}", 0) from None
dbbyte/errors.py ADDED
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+
5
+
6
+ class DBByteError(Exception):
7
+ """Raised on any DBByte API error.
8
+
9
+ The API returns {"error": str, "hint"?: str} on failure. There is no
10
+ "code" field - do not rely on one.
11
+
12
+ A 429 can mean two different things and only one is retryable:
13
+ - Burst rate limit: retry_after is set (seconds until you can
14
+ retry). Transient, safe to retry after that delay.
15
+ - Plan API request quota exceeded: retry_after is None. Retrying
16
+ immediately won't help until the billing period resets or the
17
+ plan is upgraded.
18
+ """
19
+
20
+ def __init__(
21
+ self,
22
+ message: str,
23
+ status: int,
24
+ hint: Optional[str] = None,
25
+ retry_after: Optional[int] = None,
26
+ ) -> None:
27
+ super().__init__(message)
28
+ self.status = status
29
+ self.message = message
30
+ self.hint = hint
31
+ self.retry_after = retry_after
32
+
33
+ def __repr__(self) -> str:
34
+ return (
35
+ f"DBByteError(status={self.status!r}, message={self.message!r}, "
36
+ f"hint={self.hint!r}, retry_after={self.retry_after!r})"
37
+ )
dbbyte/objects.py ADDED
@@ -0,0 +1,128 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+ from .client import DBByteClient
6
+ from .types import (
7
+ DeleteResponse,
8
+ DownloadResult,
9
+ ListObjectsResponse,
10
+ ListVersionsResponse,
11
+ SignedUrlResponse,
12
+ UploadedObject,
13
+ )
14
+ from .utils import encode_key
15
+
16
+
17
+ class ObjectsResource:
18
+ def __init__(self, client: DBByteClient) -> None:
19
+ self._client = client
20
+
21
+ def upload(
22
+ self,
23
+ bucket: str,
24
+ key: str,
25
+ body: bytes,
26
+ content_type: Optional[str] = None,
27
+ overwrite: bool = False,
28
+ ) -> UploadedObject:
29
+ result = self._client.request_form(
30
+ "POST",
31
+ f"/storage/{bucket}/upload",
32
+ filename=key,
33
+ file_bytes=body,
34
+ content_type=content_type,
35
+ query={"overwrite": "true" if overwrite else None},
36
+ )
37
+ return result # type: ignore[no-any-return]
38
+
39
+ def list(
40
+ self,
41
+ bucket: str,
42
+ prefix: Optional[str] = None,
43
+ cursor: Optional[str] = None,
44
+ limit: Optional[int] = None,
45
+ ) -> ListObjectsResponse:
46
+ result = self._client.request_json(
47
+ "GET",
48
+ f"/storage/{bucket}/list",
49
+ query={"prefix": prefix, "cursor": cursor, "limit": limit},
50
+ )
51
+ return result # type: ignore[no-any-return]
52
+
53
+ def download(
54
+ self,
55
+ bucket: str,
56
+ key: str,
57
+ version: Optional[str] = None,
58
+ ) -> DownloadResult:
59
+ result = self._client.request_binary(
60
+ "GET",
61
+ f"/storage/{bucket}/download/{encode_key(key)}",
62
+ query={"version": version},
63
+ )
64
+ return result # type: ignore[return-value]
65
+
66
+ def download_all_versions(self, bucket: str, key: str) -> DownloadResult:
67
+ """Returns a zip of every version of this key."""
68
+ result = self._client.request_binary(
69
+ "GET",
70
+ f"/storage/{bucket}/download/{encode_key(key)}",
71
+ query={"version": "all"},
72
+ )
73
+ return result # type: ignore[return-value]
74
+
75
+ def delete(
76
+ self,
77
+ bucket: str,
78
+ key: str,
79
+ version: Optional[str] = None,
80
+ ) -> DeleteResponse:
81
+ result = self._client.request_json(
82
+ "DELETE",
83
+ f"/storage/{bucket}/delete/{encode_key(key)}",
84
+ query={"version": version},
85
+ )
86
+ return result # type: ignore[no-any-return]
87
+
88
+ def delete_all_versions(self, bucket: str, key: str) -> DeleteResponse:
89
+ """Permanently deletes every version of this key."""
90
+ result = self._client.request_json(
91
+ "DELETE",
92
+ f"/storage/{bucket}/delete/{encode_key(key)}",
93
+ query={"version": "all"},
94
+ )
95
+ return result # type: ignore[no-any-return]
96
+
97
+ def sign(
98
+ self,
99
+ bucket: str,
100
+ key: str,
101
+ version: Optional[str] = None,
102
+ expires_in: Optional[int] = None,
103
+ ) -> SignedUrlResponse:
104
+ body: Dict[str, Any] = {}
105
+ if version is not None:
106
+ body["version"] = version
107
+ if expires_in is not None:
108
+ body["expiresIn"] = expires_in
109
+ result = self._client.request_json(
110
+ "POST",
111
+ f"/storage/{bucket}/sign/{encode_key(key)}",
112
+ body=body,
113
+ )
114
+ return result # type: ignore[no-any-return]
115
+
116
+ def versions(
117
+ self,
118
+ bucket: str,
119
+ key: str,
120
+ limit: Optional[int] = None,
121
+ cursor: Optional[str] = None,
122
+ ) -> ListVersionsResponse:
123
+ result = self._client.request_json(
124
+ "GET",
125
+ f"/storage/{bucket}/versions/{encode_key(key)}",
126
+ query={"limit": limit, "cursor": cursor},
127
+ )
128
+ return result # type: ignore[no-any-return]
dbbyte/py.typed ADDED
File without changes
dbbyte/types.py ADDED
@@ -0,0 +1,94 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, List, Optional, TypedDict
4
+
5
+
6
+ class HealthChecks(TypedDict):
7
+ api: Dict[str, Any]
8
+ database: Dict[str, Any]
9
+
10
+
11
+ class HealthResponse(TypedDict):
12
+ status: str # "ok" | "degraded"
13
+ timestamp: str
14
+ uptimeSeconds: int
15
+ checks: HealthChecks
16
+ responseTimeMs: int
17
+
18
+
19
+ class UploadedObject(TypedDict):
20
+ """Returned on a successful upload. Matches the storage server's
21
+ real response shape exactly - not inferred."""
22
+
23
+ success: bool
24
+ key: str # server-generated storage key, not the name you passed
25
+ name: str # the name you uploaded it as
26
+ size: int
27
+ mimeType: str
28
+ fileId: str
29
+
30
+
31
+ class FileVersionPreview(TypedDict):
32
+ key: str
33
+ size: int
34
+ mimeType: str
35
+ isLatest: bool
36
+ createdAt: str
37
+ updatedAt: str
38
+
39
+
40
+ class FileListItem(TypedDict):
41
+ key: str
42
+ name: str
43
+ size: int
44
+ isPublic: bool
45
+ createdAt: str
46
+ updatedAt: str
47
+ versionCount: int
48
+ versions: List[FileVersionPreview]
49
+ hasMoreVersions: bool
50
+
51
+
52
+ class ListObjectsResponse(TypedDict):
53
+ files: List[FileListItem]
54
+ nextCursor: Optional[str]
55
+ hasMore: bool
56
+
57
+
58
+ class SignedUrlResponse(TypedDict):
59
+ url: str
60
+ expiresIn: int
61
+
62
+
63
+ class VersionEntry(TypedDict):
64
+ versionKey: str
65
+ size: int
66
+ mimeType: str
67
+ isLatest: bool
68
+ isDeleted: bool
69
+ createdAt: str
70
+ updatedAt: str
71
+
72
+
73
+ class ListVersionsResponse(TypedDict):
74
+ versions: List[VersionEntry]
75
+ nextCursor: Optional[str]
76
+ hasMore: bool
77
+
78
+
79
+ class _DeleteResponseOptional(TypedDict, total=False):
80
+ # Only present when deleting every version at once.
81
+ deletedVersions: int
82
+
83
+
84
+ class DeleteResponse(_DeleteResponseOptional):
85
+ success: bool
86
+
87
+
88
+ class _DownloadResultOptional(TypedDict, total=False):
89
+ filename: str
90
+
91
+
92
+ class DownloadResult(_DownloadResultOptional):
93
+ data: bytes
94
+ content_type: str
dbbyte/utils.py ADDED
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ from urllib.parse import quote
4
+
5
+
6
+ def encode_key(key: str) -> str:
7
+ """Object keys can contain slashes (e.g. "images/logo.png"). The
8
+ API's catch-all routes expect each path segment encoded
9
+ individually, then rejoined with "/" - encoding the whole key would
10
+ also encode the slashes themselves."""
11
+ return "/".join(quote(segment, safe="") for segment in key.split("/"))
@@ -0,0 +1,163 @@
1
+ Metadata-Version: 2.4
2
+ Name: dbbyte
3
+ Version: 0.1.0
4
+ Summary: Official DBByte SDK for Python
5
+ Author: DBByte, LLC
6
+ License: MIT
7
+ Project-URL: Homepage, https://dbbyte.com
8
+ Project-URL: Repository, https://github.com/dbbyte/python-sdk
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Typing :: Typed
13
+ Requires-Python: >=3.9
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Dynamic: license-file
17
+
18
+ # dbbyte
19
+
20
+ Official DBByte SDK for Python. A thin, typed wrapper over the DBByte v1 REST API. Standard library only - no dependencies.
21
+
22
+ ## Status
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.
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ pip install dbbyte
30
+ ```
31
+
32
+ Requires Python 3.9+.
33
+
34
+ ## Usage
35
+
36
+ ```python
37
+ import os
38
+ from dbbyte import DBByte
39
+
40
+ db = DBByte(api_key=os.environ["DBBYTE_API_KEY"])
41
+ ```
42
+
43
+ | Argument | Type | Description |
44
+ | --- | --- | --- |
45
+ | `api_key` | `str` | Your DBByte API key. Required. |
46
+ | `base_url` | `str` | Override the API base URL. Defaults to `https://api.dbbyte.com`. |
47
+ | `timeout` | `float` | Request timeout in seconds. Defaults to `30.0`. |
48
+
49
+ ### Health check
50
+
51
+ ```python
52
+ status = db.health()
53
+ ```
54
+
55
+ ### Upload
56
+
57
+ ```python
58
+ with open("logo.png", "rb") as f:
59
+ body = f.read()
60
+
61
+ obj = db.objects.upload(
62
+ "my-bucket",
63
+ key="images/logo.png",
64
+ body=body,
65
+ content_type="image/png",
66
+ )
67
+
68
+ # Re-uploading the same name fails with 409 unless you opt in:
69
+ db.objects.upload("my-bucket", key="images/logo.png", body=body, overwrite=True)
70
+ ```
71
+
72
+ ### List
73
+
74
+ ```python
75
+ result = db.objects.list("my-bucket")
76
+ files = result["files"]
77
+
78
+ # Paginate:
79
+ next_page = db.objects.list("my-bucket", cursor=result["nextCursor"])
80
+ ```
81
+
82
+ ### Download
83
+
84
+ ```python
85
+ result = db.objects.download("my-bucket", "images/logo.png")
86
+ data = result["data"] # bytes
87
+
88
+ # A specific historical version:
89
+ db.objects.download("my-bucket", "images/logo.png", version="some-version-key")
90
+
91
+ # Every version, zipped:
92
+ zip_result = db.objects.download_all_versions("my-bucket", "images/logo.png")
93
+ ```
94
+
95
+ ### Versions
96
+
97
+ ```python
98
+ result = db.objects.versions("my-bucket", "images/logo.png")
99
+ ```
100
+
101
+ ### Signed URLs
102
+
103
+ ```python
104
+ result = db.objects.sign("my-bucket", "images/logo.png", expires_in=3600) # max 604800 (7 days)
105
+ url = result["url"]
106
+ ```
107
+
108
+ ### Delete
109
+
110
+ ```python
111
+ db.objects.delete("my-bucket", "images/logo.png")
112
+
113
+ # A specific version only:
114
+ db.objects.delete("my-bucket", "images/logo.png", version="some-version-key")
115
+
116
+ # Every version, permanently:
117
+ db.objects.delete_all_versions("my-bucket", "images/logo.png")
118
+ ```
119
+
120
+ ## Error handling
121
+
122
+ All SDK methods raise a `DBByteError` on API errors.
123
+
124
+ ```python
125
+ from dbbyte import DBByte, DBByteError
126
+
127
+ db = DBByte(api_key=os.environ["DBBYTE_API_KEY"])
128
+
129
+ try:
130
+ db.objects.download("my-bucket", "does-not-exist.png")
131
+ except DBByteError as e:
132
+ print(e.status) # e.g. 404
133
+ print(e.message) # e.g. "File not found"
134
+ print(e.hint) # present on some errors, e.g. upload conflicts
135
+ ```
136
+
137
+ ### Rate limits vs quota limits
138
+
139
+ A `429` can mean two different things, and only one is worth retrying:
140
+
141
+ - **Burst rate limit** - `e.retry_after` is set (seconds until you can retry). Transient, safe to retry after that delay.
142
+ - **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.
143
+
144
+ ```python
145
+ except DBByteError as e:
146
+ if e.status == 429:
147
+ if e.retry_after is not None:
148
+ # wait e.retry_after seconds, then retry
149
+ pass
150
+ else:
151
+ # plan quota exceeded, don't retry blindly
152
+ pass
153
+ ```
154
+
155
+ ## Known limitations
156
+
157
+ - **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.
158
+ - **50MB max file size**, enforced server-side.
159
+ - **Synchronous only.** No async client yet.
160
+
161
+ ## License
162
+
163
+ MIT
@@ -0,0 +1,12 @@
1
+ dbbyte/__init__.py,sha256=oPgWsCtp3Yz9uwIloSRgIUWhVYk0nOMezwZK4H9oWcA,1162
2
+ dbbyte/client.py,sha256=oG38mUu1445xG0dFcSjtX4TGxFExEYuImbkyNezRYrk,6205
3
+ dbbyte/errors.py,sha256=ji_WLZ_WD9LxMrFtVU29b7_vol8B5OVbKlC7Cbl6mzU,1152
4
+ dbbyte/objects.py,sha256=atxpsMPU8BR3s7MeNDAacx54IuNUWsW9Fe2Upq8LEx0,3792
5
+ dbbyte/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ dbbyte/types.py,sha256=ZaN1Gjk9OqmrXDIIv3qekP7Xs4HYQNKYo-Yviy4HUe8,1876
7
+ dbbyte/utils.py,sha256=gfsDOIhMlT1iG16lldRAEDB8x18-S-APMBxZxhBhp8k,421
8
+ dbbyte-0.1.0.dist-info/licenses/LICENSE,sha256=gLycZuuP67_sfgACPG4TWFugJi7cpQMFHEcd1KMZ5jA,1068
9
+ dbbyte-0.1.0.dist-info/METADATA,sha256=95VJ0B6jaY6tgYUCWPCKsR_HZrmzTZsMSXdTcflIp0g,4371
10
+ dbbyte-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
11
+ dbbyte-0.1.0.dist-info/top_level.txt,sha256=aaFgCSCELmGozk0K4pT0Aj97PzKkMMSrU41EZ8xRnH8,7
12
+ dbbyte-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DBByte, LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ dbbyte