tortoise-extensions 0.0.1__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.
- tortoise_extensions/__init__.py +10 -0
- tortoise_extensions/_version.py +24 -0
- tortoise_extensions/py.typed +0 -0
- tortoise_extensions/pydantic.py +74 -0
- tortoise_extensions/s3.py +279 -0
- tortoise_extensions/uuid6.py +74 -0
- tortoise_extensions-0.0.1.dist-info/METADATA +148 -0
- tortoise_extensions-0.0.1.dist-info/RECORD +10 -0
- tortoise_extensions-0.0.1.dist-info/WHEEL +4 -0
- tortoise_extensions-0.0.1.dist-info/licenses/LICENSE +15 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.0.1'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 0, 1)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
|
File without changes
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# SPDX-License-Identifier: ISC
|
|
2
|
+
# Copyright: 2026 NiceBots.xyz
|
|
3
|
+
"""Pydantic-backed JSON field for Tortoise ORM."""
|
|
4
|
+
|
|
5
|
+
from typing import Any, override
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
from tortoise.fields.data import JSONField
|
|
9
|
+
from tortoise.models import Model
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PydanticJSONField[T: BaseModel](JSONField[dict[str, object]]):
|
|
13
|
+
"""JSONField that round-trips values through a Pydantic model.
|
|
14
|
+
|
|
15
|
+
On save, serializes ``BaseModel`` instances with ``model_dump(mode="json")``
|
|
16
|
+
before JSON encoding. On load, validates decoded data with the configured
|
|
17
|
+
Pydantic model, including:
|
|
18
|
+
|
|
19
|
+
- JSON strings or bytes (typical for SQLite).
|
|
20
|
+
- Plain dicts returned by PostgreSQL JSONB drivers such as asyncpg.
|
|
21
|
+
|
|
22
|
+
Examples:
|
|
23
|
+
>>> class MyModel(Model):
|
|
24
|
+
... data: PydanticJSONField[MyPydanticModel] = PydanticJSONField(
|
|
25
|
+
... MyPydanticModel, default=MyPydanticModel
|
|
26
|
+
... )
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, model_class: type[T], **kwargs: Any) -> None:
|
|
30
|
+
"""Initialize the field.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
model_class: Pydantic model class used for validation.
|
|
34
|
+
**kwargs: Additional arguments forwarded to ``JSONField``.
|
|
35
|
+
"""
|
|
36
|
+
self._pydantic_model: type[T] = model_class
|
|
37
|
+
super().__init__(**kwargs)
|
|
38
|
+
|
|
39
|
+
@override
|
|
40
|
+
def to_python_value(self, value: Any) -> T | None: # pyright: ignore[reportExplicitAny, reportIncompatibleMethodOverride]
|
|
41
|
+
"""Convert a database value to a Pydantic model instance.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
value: Raw value from the database driver.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
A validated Pydantic model, or ``None`` when the value is null.
|
|
48
|
+
"""
|
|
49
|
+
if value is None:
|
|
50
|
+
return None
|
|
51
|
+
if isinstance(value, self._pydantic_model):
|
|
52
|
+
return value
|
|
53
|
+
if isinstance(value, (str, bytes)):
|
|
54
|
+
raw = value if isinstance(value, str) else value.decode()
|
|
55
|
+
return self._pydantic_model.model_validate_json(raw)
|
|
56
|
+
return self._pydantic_model.model_validate(value)
|
|
57
|
+
|
|
58
|
+
@override
|
|
59
|
+
def to_db_value(self, value: Any, instance: type[Model] | Model) -> str | None: # pyright: ignore[reportExplicitAny]
|
|
60
|
+
"""Convert a Python value to a JSON string for storage.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
value: Model instance or JSON-serializable data.
|
|
64
|
+
instance: Tortoise model class or instance (unused).
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Encoded JSON string, or ``None`` when the value is null.
|
|
68
|
+
"""
|
|
69
|
+
if isinstance(value, BaseModel):
|
|
70
|
+
value = value.model_dump(mode="json")
|
|
71
|
+
return super().to_db_value(value, instance)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
__all__ = ["PydanticJSONField"]
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# SPDX-License-Identifier: ISC
|
|
2
|
+
# Copyright: 2026 NiceBots.xyz
|
|
3
|
+
"""S3-backed field for Tortoise ORM (via the ``obstore`` package)."""
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from collections.abc import Awaitable, Callable, Mapping
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import TYPE_CHECKING, Any, override
|
|
9
|
+
|
|
10
|
+
from obstore.store import ObjectStore, S3Store
|
|
11
|
+
from tortoise import fields
|
|
12
|
+
from tortoise.exceptions import ConfigurationError
|
|
13
|
+
from tortoise.models import Model
|
|
14
|
+
|
|
15
|
+
if sys.version_info >= (3, 14):
|
|
16
|
+
from uuid import uuid7
|
|
17
|
+
else:
|
|
18
|
+
from uuid6 import uuid7
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from obstore.store import S3Config
|
|
22
|
+
|
|
23
|
+
_stores: dict[str, ObjectStore] = {}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
27
|
+
class S3Storage:
|
|
28
|
+
"""Connection settings for one S3-compatible endpoint.
|
|
29
|
+
|
|
30
|
+
Attributes:
|
|
31
|
+
endpoint: Host and optional port, without scheme (e.g. ``"minio:9000"``).
|
|
32
|
+
bucket: Bucket holding the objects.
|
|
33
|
+
access_key: Access key ID.
|
|
34
|
+
secret_key: Secret access key.
|
|
35
|
+
region: Signing region, or ``None`` to let ``obstore`` resolve it.
|
|
36
|
+
ssl: Use ``https`` when ``True``, plain ``http`` when ``False``.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
endpoint: str
|
|
40
|
+
bucket: str
|
|
41
|
+
access_key: str
|
|
42
|
+
secret_key: str
|
|
43
|
+
region: str | None = None
|
|
44
|
+
ssl: bool = True
|
|
45
|
+
|
|
46
|
+
def __post_init__(self) -> None:
|
|
47
|
+
"""Validate the endpoint.
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
ValueError: If the endpoint contains a scheme, which ``ssl`` controls.
|
|
51
|
+
"""
|
|
52
|
+
if "://" in self.endpoint:
|
|
53
|
+
raise ValueError("S3Storage.endpoint must not contain a scheme, use `ssl` instead")
|
|
54
|
+
|
|
55
|
+
def to_store(self) -> S3Store:
|
|
56
|
+
"""Build the ``obstore`` store for these settings.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
A configured ``S3Store``.
|
|
60
|
+
"""
|
|
61
|
+
config: S3Config = {
|
|
62
|
+
"endpoint": f"{'https' if self.ssl else 'http'}://{self.endpoint}",
|
|
63
|
+
"access_key_id": self.access_key,
|
|
64
|
+
"secret_access_key": self.secret_key,
|
|
65
|
+
}
|
|
66
|
+
if self.region is not None:
|
|
67
|
+
config["region"] = self.region
|
|
68
|
+
return S3Store(self.bucket, config=config, client_options={"allow_http": not self.ssl})
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def init_storages(storages: Mapping[str, S3Storage | ObjectStore]) -> None:
|
|
72
|
+
"""Register the named storages used by ``S3Field``, replacing any previous ones.
|
|
73
|
+
|
|
74
|
+
Names are Tortoise app labels (the default storage of every model in that app)
|
|
75
|
+
or free names referenced through ``S3Field(storage=...)``.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
storages: Storage settings, or ready ``obstore`` stores, by name.
|
|
79
|
+
"""
|
|
80
|
+
_stores.clear()
|
|
81
|
+
_stores.update(
|
|
82
|
+
{name: value.to_store() if isinstance(value, S3Storage) else value for name, value in storages.items()}
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def get_store(name: str) -> ObjectStore:
|
|
87
|
+
"""Return a registered store.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
name: Storage name.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
The registered store.
|
|
94
|
+
|
|
95
|
+
Raises:
|
|
96
|
+
ConfigurationError: If no storage is registered under ``name``.
|
|
97
|
+
"""
|
|
98
|
+
try:
|
|
99
|
+
return _stores[name]
|
|
100
|
+
except KeyError:
|
|
101
|
+
raise ConfigurationError(f"S3 storage {name!r} is not initialized, see `init_storages`") from None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass(slots=True)
|
|
105
|
+
class S3Object:
|
|
106
|
+
"""Handle on an object stored in S3, as loaded from an ``S3Field``.
|
|
107
|
+
|
|
108
|
+
An empty handle (``key is None``) maps to ``NULL`` and can still be written to.
|
|
109
|
+
Writing to an empty handle generates a key, so the model must be saved afterwards.
|
|
110
|
+
|
|
111
|
+
Attributes:
|
|
112
|
+
storage: Name of the storage holding the object.
|
|
113
|
+
key: Object key, or ``None`` when nothing is stored.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
storage: str
|
|
117
|
+
key: str | None = None
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def is_empty(self) -> bool:
|
|
121
|
+
"""Whether the handle has no key (no I/O performed)."""
|
|
122
|
+
return self.key is None
|
|
123
|
+
|
|
124
|
+
async def read(self) -> bytes | None:
|
|
125
|
+
"""Download the object.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
The object content, or ``None`` when the handle is empty.
|
|
129
|
+
"""
|
|
130
|
+
if self.key is None:
|
|
131
|
+
return None
|
|
132
|
+
result = await get_store(self.storage).get_async(self.key)
|
|
133
|
+
return bytes(await result.bytes_async())
|
|
134
|
+
|
|
135
|
+
async def write(self, data: bytes, *, key: str | None = None) -> str:
|
|
136
|
+
"""Upload ``data``, generating a UUID7 key when the handle has none.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
data: Content to store.
|
|
140
|
+
key: Explicit key to write to, replacing the current one.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
The key written to.
|
|
144
|
+
"""
|
|
145
|
+
target = key or self.key or uuid7().hex
|
|
146
|
+
await get_store(self.storage).put_async(target, data)
|
|
147
|
+
self.key = target
|
|
148
|
+
return target
|
|
149
|
+
|
|
150
|
+
async def delete(self) -> None:
|
|
151
|
+
"""Delete the object and empty the handle. Does nothing when already empty."""
|
|
152
|
+
if self.key is None:
|
|
153
|
+
return
|
|
154
|
+
await get_store(self.storage).delete_async(self.key)
|
|
155
|
+
self.key = None
|
|
156
|
+
|
|
157
|
+
async def exists(self) -> bool:
|
|
158
|
+
"""Check whether the object exists in the storage.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
``False`` when the handle is empty or the object is missing.
|
|
162
|
+
"""
|
|
163
|
+
if self.key is None:
|
|
164
|
+
return False
|
|
165
|
+
try:
|
|
166
|
+
await get_store(self.storage).head_async(self.key)
|
|
167
|
+
except FileNotFoundError:
|
|
168
|
+
return False
|
|
169
|
+
return True
|
|
170
|
+
|
|
171
|
+
async def read_or(self, default: bytes) -> bytes:
|
|
172
|
+
"""Download the object, falling back to ``default`` without writing it.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
default: Value returned when the handle is empty or the object is missing.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
The object content, or ``default``.
|
|
179
|
+
"""
|
|
180
|
+
try:
|
|
181
|
+
data = await self.read()
|
|
182
|
+
except FileNotFoundError:
|
|
183
|
+
return default
|
|
184
|
+
return default if data is None else data
|
|
185
|
+
|
|
186
|
+
async def get_or_write(self, value: bytes | Callable[[], Awaitable[bytes]]) -> bytes:
|
|
187
|
+
"""Download the object, writing ``value`` first when it is empty or missing.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
value: Content to store, or an async factory only awaited when needed.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
The stored content.
|
|
194
|
+
"""
|
|
195
|
+
try:
|
|
196
|
+
data = await self.read()
|
|
197
|
+
except FileNotFoundError:
|
|
198
|
+
data = None
|
|
199
|
+
if data is not None:
|
|
200
|
+
return data
|
|
201
|
+
data = value if isinstance(value, bytes) else await value()
|
|
202
|
+
await self.write(data)
|
|
203
|
+
return data
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class S3Field(fields.Field[S3Object]):
|
|
207
|
+
"""Field storing an S3 object key, loaded as an ``S3Object`` handle.
|
|
208
|
+
|
|
209
|
+
The storage is ``storage`` when given, otherwise the model's Tortoise app label.
|
|
210
|
+
``NULL`` columns and new instances load as empty, writable handles.
|
|
211
|
+
|
|
212
|
+
Examples:
|
|
213
|
+
>>> class Attachment(Model):
|
|
214
|
+
... file = S3Field(null=True)
|
|
215
|
+
... thumb = S3Field(storage="cdn", null=True)
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
SQL_TYPE = "VARCHAR(1024)"
|
|
219
|
+
field_type = S3Object
|
|
220
|
+
|
|
221
|
+
def __init__(self, storage: str | None = None, **kwargs: Any) -> None: # pyright: ignore[reportExplicitAny]
|
|
222
|
+
"""Initialize the field.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
storage: Storage name overriding the model's app label.
|
|
226
|
+
**kwargs: Tortoise field options.
|
|
227
|
+
"""
|
|
228
|
+
self._storage: str | None = storage
|
|
229
|
+
kwargs.setdefault("default", self._empty)
|
|
230
|
+
super().__init__(**kwargs)
|
|
231
|
+
|
|
232
|
+
@property
|
|
233
|
+
def storage_name(self) -> str:
|
|
234
|
+
"""Storage name resolved from the override or the model's app label.
|
|
235
|
+
|
|
236
|
+
Raises:
|
|
237
|
+
ConfigurationError: If neither is available (Tortoise not initialized).
|
|
238
|
+
"""
|
|
239
|
+
if self._storage is not None:
|
|
240
|
+
return self._storage
|
|
241
|
+
app = self.model._meta.app if self.model else None # noqa: SLF001 # pyright: ignore[reportPrivateUsage, reportUnnecessaryComparison]
|
|
242
|
+
if app is None:
|
|
243
|
+
raise ConfigurationError(f"{self.model_field_name!r} has no storage, pass one or initialize Tortoise")
|
|
244
|
+
return app
|
|
245
|
+
|
|
246
|
+
def _empty(self) -> S3Object:
|
|
247
|
+
return S3Object(self.storage_name)
|
|
248
|
+
|
|
249
|
+
@override
|
|
250
|
+
def to_python_value(self, value: Any) -> S3Object: # pyright: ignore[reportExplicitAny]
|
|
251
|
+
"""Convert a database value to an ``S3Object`` handle.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
value: Stored key, handle, or ``None``.
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
A handle, empty when the value is ``None``.
|
|
258
|
+
"""
|
|
259
|
+
if isinstance(value, S3Object):
|
|
260
|
+
return value
|
|
261
|
+
return S3Object(self.storage_name, None if value is None else str(value))
|
|
262
|
+
|
|
263
|
+
@override
|
|
264
|
+
def to_db_value(self, value: Any, instance: type[Model] | Model) -> str | None: # pyright: ignore[reportExplicitAny]
|
|
265
|
+
"""Convert a handle or key to its database representation.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
value: Handle, raw key, or ``None``.
|
|
269
|
+
instance: Tortoise model class or instance (unused).
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
The object key, or ``None`` when empty.
|
|
273
|
+
"""
|
|
274
|
+
if isinstance(value, S3Object):
|
|
275
|
+
return value.key
|
|
276
|
+
return None if value is None else str(value)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
__all__ = ["S3Field", "S3Object", "S3Storage", "get_store", "init_storages"]
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# SPDX-License-Identifier: ISC
|
|
2
|
+
# Copyright: 2026 NiceBots.xyz
|
|
3
|
+
"""UUID v6+ field for Tortoise ORM (via the ``uuid6`` package)."""
|
|
4
|
+
|
|
5
|
+
from typing import Any, override
|
|
6
|
+
|
|
7
|
+
from tortoise import fields
|
|
8
|
+
from tortoise.models import Model
|
|
9
|
+
from uuid6 import UUID, uuid7
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FutureUUIDField(fields.Field[UUID], UUID): # pyright: ignore[reportUnsafeMultipleInheritance]
|
|
13
|
+
"""UUID field compatible with UUID v6+ and the ``uuid6`` package.
|
|
14
|
+
|
|
15
|
+
Stores UUIDs as ``CHAR(36)`` on most backends and as native ``UUID`` on
|
|
16
|
+
PostgreSQL. When used as a primary key without an explicit ``default``,
|
|
17
|
+
assigns UUID7 via ``uuid6.uuid7``.
|
|
18
|
+
|
|
19
|
+
Examples:
|
|
20
|
+
>>> class Item(Model):
|
|
21
|
+
... id: FutureUUIDField = FutureUUIDField(primary_key=True)
|
|
22
|
+
... class Meta:
|
|
23
|
+
... table = "items"
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
SQL_TYPE = "CHAR(36)"
|
|
27
|
+
|
|
28
|
+
class _db_postgres:
|
|
29
|
+
SQL_TYPE = "UUID"
|
|
30
|
+
|
|
31
|
+
def __init__(self, **kwargs: Any) -> None: # pyright: ignore[reportExplicitAny]
|
|
32
|
+
"""Initialize the field.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
**kwargs: Tortoise field options. Primary keys without ``default``
|
|
36
|
+
receive ``uuid7`` automatically.
|
|
37
|
+
"""
|
|
38
|
+
if kwargs.pop("pk", False):
|
|
39
|
+
kwargs["primary_key"] = True
|
|
40
|
+
if kwargs.get("primary_key") and "default" not in kwargs:
|
|
41
|
+
kwargs["default"] = uuid7
|
|
42
|
+
super().__init__(**kwargs)
|
|
43
|
+
|
|
44
|
+
@override
|
|
45
|
+
def to_db_value(self, value: Any, instance: type[Model] | Model) -> str | None: # pyright: ignore[reportExplicitAny]
|
|
46
|
+
"""Convert a UUID to its string database representation.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
value: UUID value or other supported input.
|
|
50
|
+
instance: Tortoise model class or instance (unused).
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
String UUID, or ``None`` when the value is falsy.
|
|
54
|
+
"""
|
|
55
|
+
return value and str(value)
|
|
56
|
+
|
|
57
|
+
@override
|
|
58
|
+
def to_python_value(self, value: Any) -> UUID | None: # pyright: ignore[reportExplicitAny]
|
|
59
|
+
"""Convert a database value to a ``uuid6.UUID`` instance.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
value: Raw value from the database driver.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Parsed UUID, or ``None`` when the value is null.
|
|
66
|
+
"""
|
|
67
|
+
if value is None:
|
|
68
|
+
return None
|
|
69
|
+
if isinstance(value, UUID):
|
|
70
|
+
return value
|
|
71
|
+
return UUID(str(value))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
__all__ = ["FutureUUIDField"]
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: tortoise-extensions
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Extension field types for Tortoise ORM
|
|
5
|
+
Project-URL: Documentation, https://docs.nicebots.xyz/tortoise-extensions/
|
|
6
|
+
Project-URL: Repository, https://github.com/nicebots-xyz/tortoise-extensions
|
|
7
|
+
Project-URL: Issues, https://github.com/nicebots-xyz/tortoise-extensions/issues
|
|
8
|
+
Project-URL: Source Archive, https://github.com/nicebots-xyz/tortoise-extensions/archive/be1848c93a01c69d6ccce132b5820ad41b5337ee.zip
|
|
9
|
+
Author-email: Paillat-dev <me@paillat.dev>
|
|
10
|
+
License-Expression: ISC
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: fields,orm,pydantic,s3,tortoise,uuid6
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: ISC License (ISCL)
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: <3.15,>=3.12
|
|
22
|
+
Requires-Dist: tortoise-orm<1.0.0,>=0.25.4
|
|
23
|
+
Provides-Extra: pydantic
|
|
24
|
+
Requires-Dist: pydantic~=2.0; extra == 'pydantic'
|
|
25
|
+
Provides-Extra: s3
|
|
26
|
+
Requires-Dist: obstore~=0.12.1; extra == 's3'
|
|
27
|
+
Requires-Dist: uuid6>=2024.0.0; (python_version < '3.14') and extra == 's3'
|
|
28
|
+
Provides-Extra: uuid6
|
|
29
|
+
Requires-Dist: uuid6>=2024.0.0; extra == 'uuid6'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
<!--
|
|
33
|
+
SPDX-License-Identifier: ISC
|
|
34
|
+
Copyright: 2026 NiceBots.xyz
|
|
35
|
+
-->
|
|
36
|
+
# tortoise-extensions
|
|
37
|
+
|
|
38
|
+
Optional extension field types for [Tortoise ORM](https://tortoise.github.io/). This package targets **Tortoise ORM 0.25.4 and later, but before 1.0** — Tortoise 1.x is not supported yet.
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
Base package (requires Tortoise ORM in the supported range):
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install tortoise-extensions
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Optional extras:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install tortoise-extensions[pydantic]
|
|
52
|
+
pip install tortoise-extensions[uuid6]
|
|
53
|
+
pip install tortoise-extensions[s3]
|
|
54
|
+
pip install tortoise-extensions[pydantic,uuid6,s3]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Usage
|
|
58
|
+
|
|
59
|
+
### Pydantic JSON (`pydantic` extra)
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from pydantic import BaseModel
|
|
63
|
+
from tortoise.models import Model
|
|
64
|
+
|
|
65
|
+
from tortoise_extensions.pydantic import PydanticJSONField
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Settings(BaseModel):
|
|
69
|
+
theme: str = "dark"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class AppConfig(Model):
|
|
73
|
+
settings: PydanticJSONField[Settings] = PydanticJSONField(Settings, default=Settings)
|
|
74
|
+
|
|
75
|
+
class Meta:
|
|
76
|
+
table = "app_configs"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`PydanticJSONField` round-trips through your Pydantic model: it accepts dicts from PostgreSQL JSONB, JSON strings from SQLite, and serializes models with `model_dump(mode="json")` on save.
|
|
80
|
+
|
|
81
|
+
### UUID v6+ (`uuid6` extra)
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from tortoise.models import Model
|
|
85
|
+
|
|
86
|
+
from tortoise_extensions.uuid6 import FutureUUIDField
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class Item(Model):
|
|
90
|
+
id: FutureUUIDField = FutureUUIDField(primary_key=True)
|
|
91
|
+
|
|
92
|
+
class Meta:
|
|
93
|
+
table = "items"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Primary keys without an explicit `default` receive a UUID7 from the [`uuid6`](https://pypi.org/project/uuid6/) package. PostgreSQL uses the native `UUID` column type; other backends store `CHAR(36)`.
|
|
97
|
+
|
|
98
|
+
### S3 objects (`s3` extra)
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from tortoise.models import Model
|
|
102
|
+
|
|
103
|
+
from tortoise_extensions.s3 import S3Field
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class Attachment(Model):
|
|
107
|
+
file = S3Field(null=True)
|
|
108
|
+
|
|
109
|
+
class Meta:
|
|
110
|
+
table = "attachments"
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The column stores an object key and loads as an `S3Object` handle (`read`, `write`, `delete`, `get_or_write`, ...) backed by [`obstore`](https://pypi.org/project/obstore/). Storages are registered per Tortoise app with `init_storages`, each with its own `ssl` setting. See the [usage docs](https://docs.nicebots.xyz/tortoise-extensions/usage/) for details.
|
|
114
|
+
|
|
115
|
+
## Contributing a new extension
|
|
116
|
+
|
|
117
|
+
1. **Open an issue first** — describe the field behavior, SQL types per backend, which Tortoise versions you need, and the proposed optional-extra name (e.g. `pip install tortoise-extensions[myextra]`).
|
|
118
|
+
2. After maintainers agree on the design, open a PR that includes:
|
|
119
|
+
- Implementation in `src/tortoise_extensions/<module>.py`
|
|
120
|
+
- Tests under `tests/`
|
|
121
|
+
- An entry in `[project.optional-dependencies]` if the field needs third-party packages
|
|
122
|
+
- README and docs updates for the new field
|
|
123
|
+
- ISC SPDX headers on new files (`licensor-config.yaml` applies)
|
|
124
|
+
3. Run `pdm run quality` locally before pushing.
|
|
125
|
+
|
|
126
|
+
## Documentation
|
|
127
|
+
|
|
128
|
+
Published at [docs.nicebots.xyz/tortoise-extensions](https://docs.nicebots.xyz/tortoise-extensions/).
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
pdm install -G docs
|
|
132
|
+
pdm run docs:build
|
|
133
|
+
pdm run docs:preview
|
|
134
|
+
pdm run docs:dev
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Development
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
pdm install -G dev -G test -G docs
|
|
141
|
+
pdm run quality
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Public APIs use [Google-style docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings); `ruff` enforces this via `convention = "google"`.
|
|
145
|
+
|
|
146
|
+
## License
|
|
147
|
+
|
|
148
|
+
ISC.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
tortoise_extensions/__init__.py,sha256=53rMk6fOZ73lLLyfN4mw3jjKCgwJ-RG-2vr9KX4sfo0,248
|
|
2
|
+
tortoise_extensions/_version.py,sha256=8OsTLsIVB9D0HdPTmt5rVwyVUBe9xTVGkRslXicxzkM,520
|
|
3
|
+
tortoise_extensions/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
tortoise_extensions/pydantic.py,sha256=lqazG4Ek91KmSzhg-hpMOOXPmO2-wOxJkKe2YsqJC4g,2643
|
|
5
|
+
tortoise_extensions/s3.py,sha256=R6-4bmtGHf4jKBTweJ-5MVCMVoELbeFht6FqvCQGMfs,9001
|
|
6
|
+
tortoise_extensions/uuid6.py,sha256=LqoCktYxjvxhtpbJ3mwd40PbxNkKCJJ1I_pfM5QN-Pg,2358
|
|
7
|
+
tortoise_extensions-0.0.1.dist-info/METADATA,sha256=NJsKaLKkZRm8bjpoyEU-VRn4uVKmlrVtmASFndrgi8E,4759
|
|
8
|
+
tortoise_extensions-0.0.1.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
9
|
+
tortoise_extensions-0.0.1.dist-info/licenses/LICENSE,sha256=lh4wKPdjQxcYj8MlbyIUjEoaTk6SkcKOiVpNJsJnMl0,743
|
|
10
|
+
tortoise_extensions-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 NiceBots.xyz
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
10
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
11
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
12
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
13
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
|
|
14
|
+
OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
15
|
+
PERFORMANCE OF THIS SOFTWARE.
|