django-tortoise-objects 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.
- django_tortoise/__init__.py +18 -0
- django_tortoise/_models.py +16 -0
- django_tortoise/apps.py +106 -0
- django_tortoise/code_generator.py +652 -0
- django_tortoise/conf.py +77 -0
- django_tortoise/db_config.py +91 -0
- django_tortoise/exceptions.py +26 -0
- django_tortoise/fields.py +505 -0
- django_tortoise/generator.py +206 -0
- django_tortoise/initialization.py +102 -0
- django_tortoise/introspection.py +280 -0
- django_tortoise/management/__init__.py +0 -0
- django_tortoise/management/commands/__init__.py +0 -0
- django_tortoise/management/commands/generate_tortoise_models.py +113 -0
- django_tortoise/manager.py +184 -0
- django_tortoise/registry.py +123 -0
- django_tortoise_objects-0.1.0.dist-info/METADATA +302 -0
- django_tortoise_objects-0.1.0.dist-info/RECORD +20 -0
- django_tortoise_objects-0.1.0.dist-info/WHEEL +4 -0
- django_tortoise_objects-0.1.0.dist-info/licenses/LICENSE +191 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TortoiseObjects descriptor for the ``tortoise_objects`` attribute.
|
|
3
|
+
|
|
4
|
+
Provides a Django-model-attached descriptor that lazily initializes
|
|
5
|
+
Tortoise ORM and returns an async-capable queryset interface.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("django_tortoise")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TortoiseObjects:
|
|
14
|
+
"""
|
|
15
|
+
Descriptor/proxy that provides access to Tortoise QuerySet for a Django model.
|
|
16
|
+
|
|
17
|
+
Usage: ``MyDjangoModel.tortoise_objects.filter(name="foo")``
|
|
18
|
+
|
|
19
|
+
All async QuerySet operations automatically trigger lazy initialization
|
|
20
|
+
of Tortoise ORM connections on first use.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, tortoise_model_class):
|
|
24
|
+
self._tortoise_model = tortoise_model_class
|
|
25
|
+
|
|
26
|
+
def __get__(self, obj, objtype=None):
|
|
27
|
+
"""
|
|
28
|
+
When accessed as class attribute, return self (the manager).
|
|
29
|
+
When accessed as instance attribute, also return self.
|
|
30
|
+
"""
|
|
31
|
+
return self
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def model(self):
|
|
35
|
+
"""The underlying Tortoise model class."""
|
|
36
|
+
return self._tortoise_model
|
|
37
|
+
|
|
38
|
+
def _get_queryset(self):
|
|
39
|
+
"""Get a fresh Tortoise QuerySet for the model."""
|
|
40
|
+
return self._tortoise_model.all()
|
|
41
|
+
|
|
42
|
+
# --- QuerySet proxy methods ---
|
|
43
|
+
# Each method returns a _LazyQuerySet that ensures init before execution.
|
|
44
|
+
|
|
45
|
+
def all(self):
|
|
46
|
+
return _LazyQuerySet(self._tortoise_model, "all")
|
|
47
|
+
|
|
48
|
+
def filter(self, *args, **kwargs):
|
|
49
|
+
return _LazyQuerySet(self._tortoise_model, "filter", *args, **kwargs)
|
|
50
|
+
|
|
51
|
+
def exclude(self, *args, **kwargs):
|
|
52
|
+
return _LazyQuerySet(self._tortoise_model, "exclude", *args, **kwargs)
|
|
53
|
+
|
|
54
|
+
def get(self, *args, **kwargs):
|
|
55
|
+
return _LazyQuerySet(self._tortoise_model, "get", *args, **kwargs)
|
|
56
|
+
|
|
57
|
+
def create(self, **kwargs):
|
|
58
|
+
return _LazyQuerySet(self._tortoise_model, "create", **kwargs)
|
|
59
|
+
|
|
60
|
+
def first(self):
|
|
61
|
+
return _LazyQuerySet(self._tortoise_model, "first")
|
|
62
|
+
|
|
63
|
+
def count(self):
|
|
64
|
+
return _LazyQuerySet(self._tortoise_model, "count")
|
|
65
|
+
|
|
66
|
+
def exists(self):
|
|
67
|
+
return _LazyQuerySet(self._tortoise_model, "exists")
|
|
68
|
+
|
|
69
|
+
def values(self, *args, **kwargs):
|
|
70
|
+
return _LazyQuerySet(self._tortoise_model, "values", *args, **kwargs)
|
|
71
|
+
|
|
72
|
+
def values_list(self, *args, **kwargs):
|
|
73
|
+
return _LazyQuerySet(self._tortoise_model, "values_list", *args, **kwargs)
|
|
74
|
+
|
|
75
|
+
def delete(self):
|
|
76
|
+
return _LazyQuerySet(self._tortoise_model, "delete")
|
|
77
|
+
|
|
78
|
+
def update(self, **kwargs):
|
|
79
|
+
return _LazyQuerySet(self._tortoise_model, "update", **kwargs)
|
|
80
|
+
|
|
81
|
+
def get_or_create(self, **kwargs):
|
|
82
|
+
return _LazyQuerySet(self._tortoise_model, "get_or_create", **kwargs)
|
|
83
|
+
|
|
84
|
+
def bulk_create(self, objects, **kwargs):
|
|
85
|
+
return _LazyQuerySet(self._tortoise_model, "bulk_create", objects, **kwargs)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class _LazyQuerySet:
|
|
89
|
+
"""
|
|
90
|
+
A lazy wrapper around a Tortoise QuerySet method call.
|
|
91
|
+
|
|
92
|
+
Ensures Tortoise is initialized before executing the query.
|
|
93
|
+
Supports both ``__await__`` (for direct ``await``) and chaining.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def __init__(self, tortoise_model, method_name, *args, **kwargs):
|
|
97
|
+
self._tortoise_model = tortoise_model
|
|
98
|
+
self._method_name = method_name
|
|
99
|
+
self._args = args
|
|
100
|
+
self._kwargs = kwargs
|
|
101
|
+
self._chain = [] # List of (method_name, args, kwargs) for chaining
|
|
102
|
+
|
|
103
|
+
def filter(self, *args, **kwargs):
|
|
104
|
+
self._chain.append(("filter", args, kwargs))
|
|
105
|
+
return self
|
|
106
|
+
|
|
107
|
+
def exclude(self, *args, **kwargs):
|
|
108
|
+
self._chain.append(("exclude", args, kwargs))
|
|
109
|
+
return self
|
|
110
|
+
|
|
111
|
+
def order_by(self, *args):
|
|
112
|
+
self._chain.append(("order_by", args, {}))
|
|
113
|
+
return self
|
|
114
|
+
|
|
115
|
+
def limit(self, limit):
|
|
116
|
+
self._chain.append(("limit", (limit,), {}))
|
|
117
|
+
return self
|
|
118
|
+
|
|
119
|
+
def offset(self, offset):
|
|
120
|
+
self._chain.append(("offset", (offset,), {}))
|
|
121
|
+
return self
|
|
122
|
+
|
|
123
|
+
def values(self, *args, **kwargs):
|
|
124
|
+
self._chain.append(("values", args, kwargs))
|
|
125
|
+
return self
|
|
126
|
+
|
|
127
|
+
def values_list(self, *args, **kwargs):
|
|
128
|
+
self._chain.append(("values_list", args, kwargs))
|
|
129
|
+
return self
|
|
130
|
+
|
|
131
|
+
def count(self):
|
|
132
|
+
self._chain.append(("count", (), {}))
|
|
133
|
+
return self
|
|
134
|
+
|
|
135
|
+
def first(self):
|
|
136
|
+
self._chain.append(("first", (), {}))
|
|
137
|
+
return self
|
|
138
|
+
|
|
139
|
+
def exists(self):
|
|
140
|
+
self._chain.append(("exists", (), {}))
|
|
141
|
+
return self
|
|
142
|
+
|
|
143
|
+
def delete(self):
|
|
144
|
+
self._chain.append(("delete", (), {}))
|
|
145
|
+
return self
|
|
146
|
+
|
|
147
|
+
def update(self, **kwargs):
|
|
148
|
+
self._chain.append(("update", (), kwargs))
|
|
149
|
+
return self
|
|
150
|
+
|
|
151
|
+
async def _execute(self):
|
|
152
|
+
"""Execute the query chain with lazy initialization."""
|
|
153
|
+
from django_tortoise.initialization import ensure_initialized
|
|
154
|
+
|
|
155
|
+
await ensure_initialized()
|
|
156
|
+
|
|
157
|
+
# Start with the initial method call
|
|
158
|
+
qs = getattr(self._tortoise_model, self._method_name)(*self._args, **self._kwargs)
|
|
159
|
+
|
|
160
|
+
# Apply chain
|
|
161
|
+
for method_name, args, kwargs in self._chain:
|
|
162
|
+
qs = getattr(qs, method_name)(*args, **kwargs)
|
|
163
|
+
|
|
164
|
+
# If the result is awaitable (queryset), await it
|
|
165
|
+
if hasattr(qs, "__await__"):
|
|
166
|
+
return await qs
|
|
167
|
+
return qs
|
|
168
|
+
|
|
169
|
+
def __await__(self):
|
|
170
|
+
return self._execute().__await__()
|
|
171
|
+
|
|
172
|
+
def __aiter__(self):
|
|
173
|
+
return self._async_iter()
|
|
174
|
+
|
|
175
|
+
async def _async_iter(self):
|
|
176
|
+
result = await self._execute()
|
|
177
|
+
if hasattr(result, "__aiter__"):
|
|
178
|
+
async for item in result:
|
|
179
|
+
yield item
|
|
180
|
+
elif hasattr(result, "__iter__"):
|
|
181
|
+
for item in result:
|
|
182
|
+
yield item
|
|
183
|
+
else:
|
|
184
|
+
yield result
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Central registry of Django-to-Tortoise model mappings.
|
|
3
|
+
|
|
4
|
+
Maintains a bidirectional mapping between Django model classes and their
|
|
5
|
+
dynamically generated Tortoise ORM counterparts. Provides lookup by
|
|
6
|
+
Django model class, Tortoise model class, or ``"app_label.ModelName"``
|
|
7
|
+
label string.
|
|
8
|
+
|
|
9
|
+
A module-level ``model_registry`` singleton is the single source of truth.
|
|
10
|
+
The public ``get_tortoise_model()`` function is a convenience wrapper
|
|
11
|
+
re-exported by ``django_tortoise.__init__``.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
from typing import TYPE_CHECKING
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from tortoise.models import Model as TortoiseModel
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger("django_tortoise")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ModelRegistry:
|
|
26
|
+
"""Central registry mapping Django models to Tortoise models."""
|
|
27
|
+
|
|
28
|
+
def __init__(self) -> None:
|
|
29
|
+
self._django_to_tortoise: dict[type, type[TortoiseModel]] = {}
|
|
30
|
+
self._tortoise_to_django: dict[type[TortoiseModel], type] = {}
|
|
31
|
+
self._by_label: dict[str, type[TortoiseModel]] = {} # "app_label.ModelName" -> Tortoise
|
|
32
|
+
self._tortoise_models: list[type[TortoiseModel]] = []
|
|
33
|
+
|
|
34
|
+
def register(
|
|
35
|
+
self,
|
|
36
|
+
django_model: type,
|
|
37
|
+
tortoise_model: type[TortoiseModel],
|
|
38
|
+
label: str,
|
|
39
|
+
) -> None:
|
|
40
|
+
"""Register a Django <-> Tortoise model pair."""
|
|
41
|
+
self._django_to_tortoise[django_model] = tortoise_model
|
|
42
|
+
self._tortoise_to_django[tortoise_model] = django_model
|
|
43
|
+
self._by_label[label] = tortoise_model
|
|
44
|
+
self._tortoise_models.append(tortoise_model)
|
|
45
|
+
logger.debug("Registered: %s -> %s", label, tortoise_model.__name__)
|
|
46
|
+
|
|
47
|
+
def get_tortoise_model(self, django_model: type) -> type[TortoiseModel] | None:
|
|
48
|
+
"""Get the Tortoise model for a Django model."""
|
|
49
|
+
return self._django_to_tortoise.get(django_model)
|
|
50
|
+
|
|
51
|
+
def get_django_model(self, tortoise_model: type[TortoiseModel]) -> type | None:
|
|
52
|
+
"""Get the Django model for a Tortoise model."""
|
|
53
|
+
return self._tortoise_to_django.get(tortoise_model)
|
|
54
|
+
|
|
55
|
+
def get_by_label(self, label: str) -> type[TortoiseModel] | None:
|
|
56
|
+
"""Get Tortoise model by ``'app_label.ModelName'`` string."""
|
|
57
|
+
return self._by_label.get(label)
|
|
58
|
+
|
|
59
|
+
def get_all_tortoise_models(self) -> list[type[TortoiseModel]]:
|
|
60
|
+
"""Get all registered Tortoise models."""
|
|
61
|
+
return list(self._tortoise_models)
|
|
62
|
+
|
|
63
|
+
def get_all_mappings(self) -> dict[type, type[TortoiseModel]]:
|
|
64
|
+
"""Return a copy of the full Django -> Tortoise model registry."""
|
|
65
|
+
return dict(self._django_to_tortoise)
|
|
66
|
+
|
|
67
|
+
def is_registered(self, django_model: type) -> bool:
|
|
68
|
+
"""Check if a Django model has a Tortoise counterpart."""
|
|
69
|
+
return django_model in self._django_to_tortoise
|
|
70
|
+
|
|
71
|
+
def clear(self) -> None:
|
|
72
|
+
"""Clear all registrations. Primarily used in testing."""
|
|
73
|
+
self._django_to_tortoise.clear()
|
|
74
|
+
self._tortoise_to_django.clear()
|
|
75
|
+
self._by_label.clear()
|
|
76
|
+
self._tortoise_models.clear()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# Global singleton instance
|
|
80
|
+
model_registry = ModelRegistry()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
# Public convenience functions (re-exported by django_tortoise.__init__)
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def register_model(
|
|
89
|
+
django_model: type,
|
|
90
|
+
tortoise_model: type[TortoiseModel],
|
|
91
|
+
label: str | None = None,
|
|
92
|
+
) -> None:
|
|
93
|
+
"""
|
|
94
|
+
Register a Django -> Tortoise model mapping.
|
|
95
|
+
|
|
96
|
+
If *label* is not provided, it is derived from the Django model's
|
|
97
|
+
``_meta.app_label`` and class name.
|
|
98
|
+
"""
|
|
99
|
+
if label is None:
|
|
100
|
+
try:
|
|
101
|
+
label = f"{django_model._meta.app_label}.{django_model.__name__}" # type: ignore[attr-defined]
|
|
102
|
+
except AttributeError:
|
|
103
|
+
label = django_model.__name__
|
|
104
|
+
model_registry.register(django_model, tortoise_model, label)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def get_tortoise_model(django_model: type) -> type[TortoiseModel] | None:
|
|
108
|
+
"""
|
|
109
|
+
Retrieve the Tortoise ORM model class generated for the given Django model.
|
|
110
|
+
|
|
111
|
+
Returns ``None`` if no mapping has been registered for this model.
|
|
112
|
+
"""
|
|
113
|
+
return model_registry.get_tortoise_model(django_model)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def get_all_mappings() -> dict[type, type[TortoiseModel]]:
|
|
117
|
+
"""Return a copy of the full Django -> Tortoise model registry."""
|
|
118
|
+
return model_registry.get_all_mappings()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def clear_registry() -> None:
|
|
122
|
+
"""Clear all registered mappings. Primarily used in testing."""
|
|
123
|
+
model_registry.clear()
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-tortoise-objects
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Bridge Django ORM models to Tortoise ORM for truly async database access
|
|
5
|
+
Project-URL: Homepage, https://github.com/tortoise/django-tortoise-objects
|
|
6
|
+
Project-URL: Repository, https://github.com/tortoise/django-tortoise-objects
|
|
7
|
+
Project-URL: Issues, https://github.com/tortoise/django-tortoise-objects/issues
|
|
8
|
+
Author: Andrei Bondar
|
|
9
|
+
License: Apache-2.0
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: async,database,django,orm,tortoise-orm
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Framework :: Django
|
|
14
|
+
Classifier: Framework :: Django :: 4.2
|
|
15
|
+
Classifier: Framework :: Django :: 5.0
|
|
16
|
+
Classifier: Framework :: Django :: 5.1
|
|
17
|
+
Classifier: Intended Audience :: Developers
|
|
18
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Database
|
|
25
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
26
|
+
Classifier: Typing :: Typed
|
|
27
|
+
Requires-Python: >=3.10
|
|
28
|
+
Requires-Dist: django>=4.2
|
|
29
|
+
Requires-Dist: tortoise-orm>=1.1.2
|
|
30
|
+
Provides-Extra: all
|
|
31
|
+
Requires-Dist: aiosqlite>=0.19; extra == 'all'
|
|
32
|
+
Requires-Dist: asyncmy>=0.2; extra == 'all'
|
|
33
|
+
Requires-Dist: psycopg[binary]>=3.1; extra == 'all'
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: aiosqlite>=0.19; extra == 'dev'
|
|
36
|
+
Requires-Dist: django-stubs>=5.1; extra == 'dev'
|
|
37
|
+
Requires-Dist: mypy>=1.14; extra == 'dev'
|
|
38
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
|
|
39
|
+
Requires-Dist: pytest-django>=4.5; extra == 'dev'
|
|
40
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
41
|
+
Requires-Dist: ruff>=0.9; extra == 'dev'
|
|
42
|
+
Provides-Extra: mysql
|
|
43
|
+
Requires-Dist: asyncmy>=0.2; extra == 'mysql'
|
|
44
|
+
Provides-Extra: pg
|
|
45
|
+
Requires-Dist: psycopg[binary]>=3.1; extra == 'pg'
|
|
46
|
+
Provides-Extra: sqlite
|
|
47
|
+
Requires-Dist: aiosqlite>=0.19; extra == 'sqlite'
|
|
48
|
+
Description-Content-Type: text/markdown
|
|
49
|
+
|
|
50
|
+
# django-tortoise-objects
|
|
51
|
+
|
|
52
|
+
Bridge Django ORM models to [Tortoise ORM](https://github.com/tortoise/tortoise-orm) for truly async database access. Define your models once in Django, query them asynchronously via Tortoise — no duplicate schema, no manual sync.
|
|
53
|
+
|
|
54
|
+
## Why?
|
|
55
|
+
|
|
56
|
+
Django's async ORM wraps synchronous calls in `sync_to_async` threads. Tortoise ORM is natively async but requires its own model definitions. This library eliminates that trade-off:
|
|
57
|
+
|
|
58
|
+
- **Single source of truth** — Django models define the schema, migrations, and admin
|
|
59
|
+
- **Truly async queries** — Tortoise ORM handles the actual database I/O with native async drivers
|
|
60
|
+
- **Zero boilerplate** — Add to `INSTALLED_APPS`, and every model gets a `tortoise_objects` manager
|
|
61
|
+
|
|
62
|
+
## Installation
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install django-tortoise-objects
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
With database drivers:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# PostgreSQL
|
|
72
|
+
pip install django-tortoise-objects[pg]
|
|
73
|
+
|
|
74
|
+
# SQLite
|
|
75
|
+
pip install django-tortoise-objects[sqlite]
|
|
76
|
+
|
|
77
|
+
# MySQL
|
|
78
|
+
pip install django-tortoise-objects[mysql]
|
|
79
|
+
|
|
80
|
+
# All drivers
|
|
81
|
+
pip install django-tortoise-objects[all]
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Quick Start
|
|
85
|
+
|
|
86
|
+
### 1. Add to INSTALLED_APPS
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
INSTALLED_APPS = [
|
|
90
|
+
# ...
|
|
91
|
+
"django_tortoise",
|
|
92
|
+
# ... your apps
|
|
93
|
+
]
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### 2. Query your existing Django models asynchronously
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from myapp.models import Article
|
|
100
|
+
|
|
101
|
+
# All standard query operations — fully async, no sync_to_async wrapper
|
|
102
|
+
articles = await Article.tortoise_objects.filter(published=True)
|
|
103
|
+
article = await Article.tortoise_objects.get(id=42)
|
|
104
|
+
count = await Article.tortoise_objects.count()
|
|
105
|
+
exists = await Article.tortoise_objects.filter(title="foo").exists()
|
|
106
|
+
|
|
107
|
+
# Create, update, delete
|
|
108
|
+
article = await Article.tortoise_objects.create(title="Hello", body="World")
|
|
109
|
+
await Article.tortoise_objects.filter(published=False).update(draft=True)
|
|
110
|
+
await Article.tortoise_objects.filter(id=42).delete()
|
|
111
|
+
|
|
112
|
+
# Chaining
|
|
113
|
+
recent = await Article.tortoise_objects.filter(published=True).order_by("-created_at").limit(10)
|
|
114
|
+
|
|
115
|
+
# Prefetch relations (via underlying Tortoise model)
|
|
116
|
+
employees = await Employee.tortoise_objects.model.all().prefetch_related("team")
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### 3. Use in async Django views
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from django.http import JsonResponse
|
|
123
|
+
from myapp.models import Tag
|
|
124
|
+
|
|
125
|
+
async def tag_list(request):
|
|
126
|
+
tags = await Tag.tortoise_objects.all()
|
|
127
|
+
return JsonResponse({"tags": [{"id": t.id, "name": t.name} for t in tags]})
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
That's it. No Tortoise model definitions, no configuration files, no manual initialization.
|
|
131
|
+
|
|
132
|
+
## Configuration
|
|
133
|
+
|
|
134
|
+
Optional settings via `TORTOISE_OBJECTS` in your Django settings:
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
TORTOISE_OBJECTS = {
|
|
138
|
+
# Include only specific app models (fnmatch patterns)
|
|
139
|
+
"INCLUDE_MODELS": ["myapp.*", "blog.*"],
|
|
140
|
+
|
|
141
|
+
# Exclude specific models
|
|
142
|
+
"EXCLUDE_MODELS": ["auth.*", "admin.*"],
|
|
143
|
+
|
|
144
|
+
# Override database backend mapping
|
|
145
|
+
"DB_ENGINE_MAP": {
|
|
146
|
+
"django.db.backends.postgresql": "tortoise.backends.psycopg",
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
# Connection pool settings per database alias
|
|
150
|
+
"CONNECTION_POOL": {
|
|
151
|
+
"default": {"minsize": 5, "maxsize": 20},
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
# Logging level
|
|
155
|
+
"LOG_LEVEL": "WARNING",
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
All settings are optional. With no configuration, all models are included and database backends are auto-detected from Django's `DATABASES` setting.
|
|
160
|
+
|
|
161
|
+
## Supported Field Types
|
|
162
|
+
|
|
163
|
+
| Django Field | Tortoise Field |
|
|
164
|
+
|---|---|
|
|
165
|
+
| CharField, SlugField, EmailField, URLField | CharField |
|
|
166
|
+
| TextField | TextField |
|
|
167
|
+
| IntegerField, BigIntegerField, SmallIntegerField | IntField, BigIntField, SmallIntField |
|
|
168
|
+
| BooleanField | BooleanField |
|
|
169
|
+
| FloatField | FloatField |
|
|
170
|
+
| DecimalField | DecimalField |
|
|
171
|
+
| DateField, DateTimeField, TimeField | DateField, DatetimeField, TimeField |
|
|
172
|
+
| DurationField | TimeDeltaField |
|
|
173
|
+
| UUIDField | UUIDField |
|
|
174
|
+
| JSONField | JSONField |
|
|
175
|
+
| BinaryField | BinaryField |
|
|
176
|
+
| FileField, ImageField | CharField (stores path) |
|
|
177
|
+
| ForeignKey | ForeignKeyField |
|
|
178
|
+
| OneToOneField | OneToOneField |
|
|
179
|
+
| ManyToManyField | ManyToManyField |
|
|
180
|
+
| AutoField, BigAutoField, SmallAutoField | IntField/BigIntField/SmallIntField (pk=True) |
|
|
181
|
+
|
|
182
|
+
## ASGI Lifespan
|
|
183
|
+
|
|
184
|
+
For production ASGI deployments, explicitly manage Tortoise connections:
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
# asgi.py
|
|
188
|
+
from django_tortoise import init, close
|
|
189
|
+
|
|
190
|
+
async def lifespan(scope, receive, send):
|
|
191
|
+
if scope["type"] == "lifespan":
|
|
192
|
+
while True:
|
|
193
|
+
message = await receive()
|
|
194
|
+
if message["type"] == "lifespan.startup":
|
|
195
|
+
await init()
|
|
196
|
+
await send({"type": "lifespan.startup.complete"})
|
|
197
|
+
elif message["type"] == "lifespan.shutdown":
|
|
198
|
+
await close()
|
|
199
|
+
await send({"type": "lifespan.shutdown.complete"})
|
|
200
|
+
return
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
If you don't set up lifespan, connections are initialized lazily on first query.
|
|
204
|
+
|
|
205
|
+
## Generating Static Tortoise Models
|
|
206
|
+
|
|
207
|
+
By default, Tortoise models are generated dynamically at runtime. If you want static files you can inspect, customize, or version-control, use the management command:
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
# Generate for all apps
|
|
211
|
+
python manage.py generate_tortoise_models --output-dir ./tortoise_models
|
|
212
|
+
|
|
213
|
+
# Generate for a specific app
|
|
214
|
+
python manage.py generate_tortoise_models --app-label demo --output-dir .
|
|
215
|
+
|
|
216
|
+
# Custom Tortoise app name
|
|
217
|
+
python manage.py generate_tortoise_models --tortoise-app-name myapp
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
This produces one file per Django app (e.g., `tortoise_models_demo.py`):
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
# Auto-generated by django-tortoise-objects. Do not edit manually.
|
|
224
|
+
|
|
225
|
+
import uuid
|
|
226
|
+
|
|
227
|
+
from tortoise import fields
|
|
228
|
+
from tortoise.fields.relational import OnDelete
|
|
229
|
+
from tortoise.models import Model
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class DepartmentTortoise(Model):
|
|
233
|
+
id = fields.BigIntField(primary_key=True, generated=True)
|
|
234
|
+
name = fields.CharField(max_length=200)
|
|
235
|
+
code = fields.CharField(unique=True, max_length=20)
|
|
236
|
+
budget = fields.DecimalField(default=0, max_digits=14, decimal_places=2)
|
|
237
|
+
is_active = fields.BooleanField(default=True)
|
|
238
|
+
|
|
239
|
+
class Meta:
|
|
240
|
+
table = "demo_department"
|
|
241
|
+
app = "django_tortoise"
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class TeamTortoise(Model):
|
|
245
|
+
id = fields.BigIntField(primary_key=True, generated=True)
|
|
246
|
+
name = fields.CharField(max_length=200)
|
|
247
|
+
department = fields.ForeignKeyField(
|
|
248
|
+
"django_tortoise.DepartmentTortoise",
|
|
249
|
+
related_name='teams',
|
|
250
|
+
on_delete=OnDelete.CASCADE,
|
|
251
|
+
source_field='department_id',
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
class Meta:
|
|
255
|
+
table = "demo_team"
|
|
256
|
+
app = "django_tortoise"
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
The command respects `INCLUDE_MODELS` and `EXCLUDE_MODELS` from your `TORTOISE_OBJECTS` settings.
|
|
260
|
+
|
|
261
|
+
## Limitations & Non-Goals
|
|
262
|
+
|
|
263
|
+
**Query results are Tortoise model instances, not Django models.** Methods like `tortoise_objects.get()` and `tortoise_objects.filter()` return Tortoise ORM objects. They cannot be passed directly to Django forms, serializers, admin, or template tags that expect Django model instances. Use `tortoise_objects` for async read/write paths (APIs, WebSockets, background tasks) and the regular Django ORM for everything else.
|
|
264
|
+
|
|
265
|
+
This library is **not** intended to:
|
|
266
|
+
|
|
267
|
+
- **Replace Django ORM** — it is a complementary tool for async-critical paths, not a full substitute. Django ORM remains the right choice for admin, forms, management commands, and sync views.
|
|
268
|
+
- **Manage schema or migrations** — all schema management is delegated to Django. Tortoise never writes to your database schema.
|
|
269
|
+
- **Support cross-ORM transactions** — you cannot mix Django and Tortoise queries in a single database transaction.
|
|
270
|
+
- **Provide Django admin integration** — Tortoise query results don't work with Django's admin site. Use Django's ORM for admin.
|
|
271
|
+
- **Expose Tortoise-specific features** — Tortoise signals, custom managers, and validators are not bridged.
|
|
272
|
+
- **Generate Django models from Tortoise** — the bridge is one-way only (Django → Tortoise).
|
|
273
|
+
|
|
274
|
+
**Other things to keep in mind:**
|
|
275
|
+
|
|
276
|
+
- Tortoise maintains its own connection pool, separate from Django's. Configure pool sizes via the `CONNECTION_POOL` setting to avoid excess connections.
|
|
277
|
+
- Unsupported or custom Django field types are silently skipped during model generation. Check logs at `DEBUG` level if a field is missing.
|
|
278
|
+
- ManyToManyField relations require that the related model is also included in the Tortoise bridge (not excluded via `EXCLUDE_MODELS`).
|
|
279
|
+
|
|
280
|
+
## Performance
|
|
281
|
+
|
|
282
|
+
Benchmarks comparing `tortoise_objects` vs Django's native async ORM (`aget`, `acreate`, etc.) on the same models and data. Measured on PostgreSQL (both using psycopg).
|
|
283
|
+
|
|
284
|
+
### PostgreSQL
|
|
285
|
+
|
|
286
|
+

|
|
287
|
+
|
|
288
|
+
- `tortoise_objects` wins on single-record ops: `get` 1.3-2.0x, `count` 1.3-1.5x, `exists` 1.5x
|
|
289
|
+
- `tortoise_objects` wins on writes: `create+delete` 1.4-2.6x
|
|
290
|
+
- Bulk fetches nearly tied
|
|
291
|
+
|
|
292
|
+
See [`example_project/README.md`](example_project/README.md) for full benchmark details, methodology, and raw data.
|
|
293
|
+
|
|
294
|
+
## Requirements
|
|
295
|
+
|
|
296
|
+
- Python >= 3.10
|
|
297
|
+
- Django >= 4.2
|
|
298
|
+
- Tortoise ORM >= 1.1.2
|
|
299
|
+
|
|
300
|
+
## License
|
|
301
|
+
|
|
302
|
+
Apache 2.0 — see [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
django_tortoise/__init__.py,sha256=xzz7Ik8KOFbGG_unJHBmMkebf0uNcGi0MBizM5gzeps,689
|
|
2
|
+
django_tortoise/_models.py,sha256=aVTW3RjLPzBzVE66y1laGqMdbojal8W9WHiAgwPMXqk,478
|
|
3
|
+
django_tortoise/apps.py,sha256=3Xlyrot4GCW6SGoMdJGEDmzJX1dLDeF0U44vkhFQRds,4116
|
|
4
|
+
django_tortoise/code_generator.py,sha256=Ao5acrJYw01wwLg6bbWZE9iNGRV81oxl7vc4Ifwqtcg,21544
|
|
5
|
+
django_tortoise/conf.py,sha256=AlmPg5ckYOSEmgnXpVyW10zTmbon_UvLOczt0CHwZfw,2503
|
|
6
|
+
django_tortoise/db_config.py,sha256=-SyesMFKrkvrBNhkV3sr7b7DhRjGlItJrHq-L_-IXB0,3127
|
|
7
|
+
django_tortoise/exceptions.py,sha256=7xgmf70kTSmhnAg-23NdjGHZ8urNogBKfd5yCWp-y54,763
|
|
8
|
+
django_tortoise/fields.py,sha256=ldrF5YH44Guzg3ylZr9RLe58PYZQAwHGJrQQHCLXP2w,16893
|
|
9
|
+
django_tortoise/generator.py,sha256=rS6adLXeVTr84aWpB-f_jKH9ifUtOhgRq3kLU3v2_ek,6606
|
|
10
|
+
django_tortoise/initialization.py,sha256=zAWRHPdMHPcAUlRPXESQ6A3klsBuAbKC3A2Rrc5POSc,2841
|
|
11
|
+
django_tortoise/introspection.py,sha256=MicVL6UFxKU4_WEnsjokLXoB8LP_BMopM8tTNK-SgUI,9094
|
|
12
|
+
django_tortoise/manager.py,sha256=olaStapBU9FlQ2lxlVgEkdtQ4n7IeiBrowZvKOhZ6SY,5553
|
|
13
|
+
django_tortoise/registry.py,sha256=6PIlWM8KvDvF3QhwQ8nvRdrXmORMVVFBBT8uYUnn498,4465
|
|
14
|
+
django_tortoise/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
django_tortoise/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
+
django_tortoise/management/commands/generate_tortoise_models.py,sha256=UaIigB4jgzvLq9DtOq3Gevn6q57HnH-hAdeGy7waaKQ,4063
|
|
17
|
+
django_tortoise_objects-0.1.0.dist-info/METADATA,sha256=x-O67hPVa2vCtMzKT_Ub3w_v03_nYF_6FFzJ300XwN4,10914
|
|
18
|
+
django_tortoise_objects-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
19
|
+
django_tortoise_objects-0.1.0.dist-info/licenses/LICENSE,sha256=nMXg2ykzQ9M9PgmkGBI-CJOY2yQ7alX5EcDugUFV1VY,10765
|
|
20
|
+
django_tortoise_objects-0.1.0.dist-info/RECORD,,
|