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
django_tortoise/conf.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Settings loading from Django's ``TORTOISE_OBJECTS`` configuration.
|
|
3
|
+
|
|
4
|
+
Provides ``get_config()`` which merges user-supplied settings with sensible
|
|
5
|
+
defaults. The ``TORTOISE_OBJECTS`` dict in Django settings can override any
|
|
6
|
+
of the keys defined in ``DEFAULTS``.
|
|
7
|
+
|
|
8
|
+
Also provides ``should_include()`` for model inclusion/exclusion filtering
|
|
9
|
+
based on fnmatch patterns.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import fnmatch
|
|
13
|
+
import logging
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from django.conf import settings
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger("django_tortoise")
|
|
19
|
+
|
|
20
|
+
# Default configuration values
|
|
21
|
+
DEFAULTS: dict[str, Any] = {
|
|
22
|
+
"INCLUDE_MODELS": None, # None means all models are included
|
|
23
|
+
"EXCLUDE_MODELS": None, # None means no models are excluded
|
|
24
|
+
"DB_ENGINE_MAP": {}, # Custom DB engine -> Tortoise backend overrides
|
|
25
|
+
"CONNECTION_POOL": {}, # Connection pool configuration overrides
|
|
26
|
+
"LOG_LEVEL": "WARNING", # Logging level for django_tortoise logger
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_config() -> dict[str, Any]:
|
|
31
|
+
"""
|
|
32
|
+
Load TORTOISE_OBJECTS from Django settings, merged with defaults.
|
|
33
|
+
|
|
34
|
+
User-supplied keys in ``settings.TORTOISE_OBJECTS`` override the
|
|
35
|
+
corresponding keys in ``DEFAULTS``. Keys not present in the user
|
|
36
|
+
config fall back to their default values.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
A dict containing the merged configuration.
|
|
40
|
+
"""
|
|
41
|
+
user_config: dict[str, Any] = getattr(settings, "TORTOISE_OBJECTS", {})
|
|
42
|
+
config = {**DEFAULTS, **user_config}
|
|
43
|
+
return config
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def should_include(
|
|
47
|
+
label: str,
|
|
48
|
+
include_patterns: list[str] | None,
|
|
49
|
+
exclude_patterns: list[str] | None,
|
|
50
|
+
) -> bool:
|
|
51
|
+
"""
|
|
52
|
+
Check if a model label matches include/exclude patterns.
|
|
53
|
+
|
|
54
|
+
EXCLUDE takes precedence over INCLUDE.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
label: Model label in ``"app_label.ModelName"`` format.
|
|
58
|
+
include_patterns: List of fnmatch patterns for models to include.
|
|
59
|
+
``None`` means include all.
|
|
60
|
+
exclude_patterns: List of fnmatch patterns for models to exclude.
|
|
61
|
+
``None`` means exclude none.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
True if the model should be included.
|
|
65
|
+
"""
|
|
66
|
+
# If excluded, always skip
|
|
67
|
+
if exclude_patterns is not None:
|
|
68
|
+
for pattern in exclude_patterns:
|
|
69
|
+
if fnmatch.fnmatch(label, pattern):
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
# If include is specified (including empty list), model must match at least one pattern
|
|
73
|
+
if include_patterns is not None:
|
|
74
|
+
return any(fnmatch.fnmatch(label, pattern) for pattern in include_patterns)
|
|
75
|
+
|
|
76
|
+
# No include specified (None) = include all
|
|
77
|
+
return True
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Database configuration translation.
|
|
3
|
+
|
|
4
|
+
Translates Django ``DATABASES`` settings into the Tortoise ORM configuration
|
|
5
|
+
format, mapping database backends and connection parameters.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from django.conf import settings
|
|
12
|
+
|
|
13
|
+
from django_tortoise.conf import get_config
|
|
14
|
+
from django_tortoise.exceptions import UnsupportedBackendError
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger("django_tortoise")
|
|
17
|
+
|
|
18
|
+
DEFAULT_ENGINE_MAP: dict[str, str] = {
|
|
19
|
+
"django.db.backends.postgresql": "tortoise.backends.psycopg",
|
|
20
|
+
"django.db.backends.mysql": "tortoise.backends.asyncmy",
|
|
21
|
+
"django.db.backends.sqlite3": "tortoise.backends.sqlite",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def build_tortoise_config(tortoise_app_name: str = "django_tortoise") -> dict[str, Any]:
|
|
26
|
+
"""
|
|
27
|
+
Build a complete Tortoise ORM config dict from Django settings.
|
|
28
|
+
|
|
29
|
+
Reads ``settings.DATABASES`` and the ``TORTOISE_OBJECTS`` configuration
|
|
30
|
+
to produce a dict suitable for passing to ``Tortoise.init(config=...)``.
|
|
31
|
+
"""
|
|
32
|
+
config = get_config()
|
|
33
|
+
user_engine_map: dict[str, str] = config.get("DB_ENGINE_MAP", {})
|
|
34
|
+
pool_config: dict[str, dict[str, Any]] = config.get("CONNECTION_POOL", {})
|
|
35
|
+
engine_map = {**DEFAULT_ENGINE_MAP, **user_engine_map}
|
|
36
|
+
|
|
37
|
+
django_databases: dict[str, dict[str, Any]] = settings.DATABASES
|
|
38
|
+
tortoise_connections: dict[str, dict[str, Any]] = {}
|
|
39
|
+
|
|
40
|
+
for alias, db_conf in django_databases.items():
|
|
41
|
+
engine = db_conf.get("ENGINE", "")
|
|
42
|
+
tortoise_engine = engine_map.get(engine)
|
|
43
|
+
|
|
44
|
+
if tortoise_engine is None:
|
|
45
|
+
raise UnsupportedBackendError(
|
|
46
|
+
f"Django database backend '{engine}' (alias '{alias}') "
|
|
47
|
+
f"has no Tortoise ORM equivalent. Supported: {list(engine_map.keys())}"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
credentials = _build_credentials(engine, db_conf)
|
|
51
|
+
|
|
52
|
+
# Apply connection pool overrides
|
|
53
|
+
alias_pool = pool_config.get(alias, {})
|
|
54
|
+
if alias_pool:
|
|
55
|
+
credentials.update(alias_pool)
|
|
56
|
+
|
|
57
|
+
tortoise_connections[alias] = {
|
|
58
|
+
"engine": tortoise_engine,
|
|
59
|
+
"credentials": credentials,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
tortoise_config: dict[str, Any] = {
|
|
63
|
+
"connections": tortoise_connections,
|
|
64
|
+
"apps": {
|
|
65
|
+
tortoise_app_name: {
|
|
66
|
+
"models": ["django_tortoise._models"],
|
|
67
|
+
"default_connection": "default",
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"use_tz": getattr(settings, "USE_TZ", False),
|
|
71
|
+
"timezone": getattr(settings, "TIME_ZONE", "UTC"),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return tortoise_config
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _build_credentials(engine: str, db_conf: dict[str, Any]) -> dict[str, Any]:
|
|
78
|
+
"""Build Tortoise credentials dict from a Django DB config entry."""
|
|
79
|
+
if "sqlite" in engine:
|
|
80
|
+
return {"file_path": db_conf.get("NAME", ":memory:")}
|
|
81
|
+
|
|
82
|
+
# PostgreSQL and MySQL use the same credential keys
|
|
83
|
+
credentials: dict[str, Any] = {
|
|
84
|
+
"host": db_conf.get("HOST", "localhost"),
|
|
85
|
+
"port": int(db_conf.get("PORT", 5432 if "postgresql" in engine else 3306)),
|
|
86
|
+
"user": db_conf.get("USER", ""),
|
|
87
|
+
"password": db_conf.get("PASSWORD", ""),
|
|
88
|
+
"database": db_conf.get("NAME", ""),
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return credentials
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom exceptions for django-tortoise-objects.
|
|
3
|
+
|
|
4
|
+
All exceptions inherit from ``DjangoTortoiseError`` to allow callers to
|
|
5
|
+
catch library-specific errors with a single base class.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DjangoTortoiseError(Exception):
|
|
10
|
+
"""Base exception for django-tortoise-objects."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ConnectionError(DjangoTortoiseError):
|
|
14
|
+
"""Raised when Tortoise DB connection fails during lazy init."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ConfigurationError(DjangoTortoiseError):
|
|
18
|
+
"""Raised for invalid configuration in TORTOISE_OBJECTS settings."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class UnsupportedFieldError(DjangoTortoiseError):
|
|
22
|
+
"""Raised/logged when a Django field type has no Tortoise mapping."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class UnsupportedBackendError(DjangoTortoiseError):
|
|
26
|
+
"""Raised when Django DB backend has no Tortoise equivalent."""
|
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Field type mapping registry.
|
|
3
|
+
|
|
4
|
+
Maps Django field type strings (from ``get_internal_type()``) to Tortoise ORM
|
|
5
|
+
field constructors with correct parameter translation.
|
|
6
|
+
|
|
7
|
+
The registry key is the string returned by Django's ``Field.get_internal_type()``.
|
|
8
|
+
A decorator-based registration pattern allows adding new mappings cleanly.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from tortoise import fields as tortoise_fields
|
|
16
|
+
|
|
17
|
+
from django_tortoise.introspection import FieldInfo
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger("django_tortoise")
|
|
20
|
+
|
|
21
|
+
# Type alias for the converter function signature
|
|
22
|
+
# Converter takes FieldInfo and returns a Tortoise Field instance
|
|
23
|
+
FieldConverter = Callable[[FieldInfo], tortoise_fields.Field]
|
|
24
|
+
|
|
25
|
+
# The main registry: Django internal_type string -> converter function
|
|
26
|
+
FIELD_MAP: dict[str, FieldConverter] = {}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def register_field(internal_type: str):
|
|
30
|
+
"""Decorator to register a field converter for a Django internal_type string."""
|
|
31
|
+
|
|
32
|
+
def decorator(func: FieldConverter) -> FieldConverter:
|
|
33
|
+
FIELD_MAP[internal_type] = func
|
|
34
|
+
return func
|
|
35
|
+
|
|
36
|
+
return decorator
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _common_kwargs(field_info: FieldInfo) -> dict[str, Any]:
|
|
40
|
+
"""
|
|
41
|
+
Extract common kwargs shared by most Tortoise fields.
|
|
42
|
+
|
|
43
|
+
Maps null, unique, db_index, primary_key, source_field (when DB column
|
|
44
|
+
differs from field name), and default values.
|
|
45
|
+
"""
|
|
46
|
+
kwargs: dict[str, Any] = {}
|
|
47
|
+
if field_info.null:
|
|
48
|
+
kwargs["null"] = True
|
|
49
|
+
if field_info.unique:
|
|
50
|
+
kwargs["unique"] = True
|
|
51
|
+
if field_info.db_index:
|
|
52
|
+
kwargs["db_index"] = True
|
|
53
|
+
if field_info.primary_key:
|
|
54
|
+
kwargs["primary_key"] = True
|
|
55
|
+
# Map db_column -> source_field
|
|
56
|
+
if field_info.column and field_info.column != field_info.name:
|
|
57
|
+
kwargs["source_field"] = field_info.column
|
|
58
|
+
# Handle defaults
|
|
59
|
+
# Bug fix: when has_default is True, always set the default, even if it's None.
|
|
60
|
+
# A field with has_default=True and default=None is a valid Django pattern
|
|
61
|
+
# (e.g., nullable fields with default=None).
|
|
62
|
+
if field_info.has_default:
|
|
63
|
+
default = field_info.default
|
|
64
|
+
kwargs["default"] = default
|
|
65
|
+
return kwargs
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def convert_field(field_info: FieldInfo) -> tortoise_fields.Field | None:
|
|
69
|
+
"""
|
|
70
|
+
Convert a FieldInfo to a Tortoise field instance.
|
|
71
|
+
|
|
72
|
+
Returns None if no converter is registered for the field's internal_type,
|
|
73
|
+
logging a warning in that case.
|
|
74
|
+
"""
|
|
75
|
+
converter = FIELD_MAP.get(field_info.internal_type)
|
|
76
|
+
if converter is None:
|
|
77
|
+
logger.warning(
|
|
78
|
+
"Unsupported Django field type '%s' on field '%s'. Skipping.",
|
|
79
|
+
field_info.internal_type,
|
|
80
|
+
field_info.name,
|
|
81
|
+
)
|
|
82
|
+
return None
|
|
83
|
+
return converter(field_info)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _try_enum_field(info: FieldInfo) -> tortoise_fields.Field | None:
|
|
87
|
+
"""Return an IntEnumField or CharEnumField if the field has an enum_type, else None."""
|
|
88
|
+
if info.enum_type is None:
|
|
89
|
+
return None
|
|
90
|
+
kwargs = _common_kwargs(info)
|
|
91
|
+
if issubclass(info.enum_type, int):
|
|
92
|
+
return tortoise_fields.IntEnumField(info.enum_type, **kwargs) # type: ignore[type-var,return-value]
|
|
93
|
+
if issubclass(info.enum_type, str):
|
|
94
|
+
kwargs["max_length"] = info.max_length or 255
|
|
95
|
+
return tortoise_fields.CharEnumField(info.enum_type, **kwargs) # type: ignore[type-var,return-value]
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
# Auto fields (primary key, generated)
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@register_field("AutoField")
|
|
105
|
+
def _auto_field(info: FieldInfo):
|
|
106
|
+
return tortoise_fields.IntField(primary_key=True, generated=True)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@register_field("BigAutoField")
|
|
110
|
+
def _big_auto_field(info: FieldInfo):
|
|
111
|
+
return tortoise_fields.BigIntField(primary_key=True, generated=True)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@register_field("SmallAutoField")
|
|
115
|
+
def _small_auto_field(info: FieldInfo):
|
|
116
|
+
return tortoise_fields.SmallIntField(primary_key=True, generated=True)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
# Integer fields
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@register_field("IntegerField")
|
|
125
|
+
def _int_field(info: FieldInfo):
|
|
126
|
+
return _try_enum_field(info) or tortoise_fields.IntField(**_common_kwargs(info))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@register_field("BigIntegerField")
|
|
130
|
+
def _bigint_field(info: FieldInfo):
|
|
131
|
+
return _try_enum_field(info) or tortoise_fields.BigIntField(**_common_kwargs(info))
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@register_field("SmallIntegerField")
|
|
135
|
+
def _smallint_field(info: FieldInfo):
|
|
136
|
+
return _try_enum_field(info) or tortoise_fields.SmallIntField(**_common_kwargs(info))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@register_field("PositiveIntegerField")
|
|
140
|
+
def _pos_int(info: FieldInfo):
|
|
141
|
+
return _try_enum_field(info) or tortoise_fields.IntField(**_common_kwargs(info))
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@register_field("PositiveBigIntegerField")
|
|
145
|
+
def _pos_bigint(info: FieldInfo):
|
|
146
|
+
return _try_enum_field(info) or tortoise_fields.BigIntField(**_common_kwargs(info))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@register_field("PositiveSmallIntegerField")
|
|
150
|
+
def _pos_smallint(info: FieldInfo):
|
|
151
|
+
return _try_enum_field(info) or tortoise_fields.SmallIntField(**_common_kwargs(info))
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# ---------------------------------------------------------------------------
|
|
155
|
+
# String fields
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@register_field("CharField")
|
|
160
|
+
def _char_field(info: FieldInfo):
|
|
161
|
+
enum_field = _try_enum_field(info)
|
|
162
|
+
if enum_field is not None:
|
|
163
|
+
return enum_field
|
|
164
|
+
kwargs = _common_kwargs(info)
|
|
165
|
+
kwargs["max_length"] = info.max_length or 255
|
|
166
|
+
return tortoise_fields.CharField(**kwargs)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@register_field("TextField")
|
|
170
|
+
def _text_field(info: FieldInfo):
|
|
171
|
+
return tortoise_fields.TextField(**_common_kwargs(info))
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# ---------------------------------------------------------------------------
|
|
175
|
+
# Boolean field
|
|
176
|
+
# ---------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@register_field("BooleanField")
|
|
180
|
+
def _bool_field(info: FieldInfo):
|
|
181
|
+
return tortoise_fields.BooleanField(**_common_kwargs(info))
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# ---------------------------------------------------------------------------
|
|
185
|
+
# Date/Time fields
|
|
186
|
+
# ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@register_field("DateField")
|
|
190
|
+
def _date_field(info: FieldInfo):
|
|
191
|
+
return tortoise_fields.DateField(**_common_kwargs(info))
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@register_field("DateTimeField")
|
|
195
|
+
def _datetime_field(info: FieldInfo):
|
|
196
|
+
return tortoise_fields.DatetimeField(**_common_kwargs(info))
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@register_field("TimeField")
|
|
200
|
+
def _time_field(info: FieldInfo):
|
|
201
|
+
return tortoise_fields.TimeField(**_common_kwargs(info))
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@register_field("DurationField")
|
|
205
|
+
def _duration_field(info: FieldInfo):
|
|
206
|
+
return tortoise_fields.TimeDeltaField(**_common_kwargs(info))
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# ---------------------------------------------------------------------------
|
|
210
|
+
# Numeric fields
|
|
211
|
+
# ---------------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@register_field("DecimalField")
|
|
215
|
+
def _decimal_field(info: FieldInfo):
|
|
216
|
+
kwargs = _common_kwargs(info)
|
|
217
|
+
kwargs["max_digits"] = info.max_digits
|
|
218
|
+
kwargs["decimal_places"] = info.decimal_places
|
|
219
|
+
return tortoise_fields.DecimalField(**kwargs)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@register_field("FloatField")
|
|
223
|
+
def _float_field(info: FieldInfo):
|
|
224
|
+
return tortoise_fields.FloatField(**_common_kwargs(info))
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# ---------------------------------------------------------------------------
|
|
228
|
+
# Binary / UUID / JSON fields
|
|
229
|
+
# ---------------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@register_field("BinaryField")
|
|
233
|
+
def _binary_field(info: FieldInfo):
|
|
234
|
+
return tortoise_fields.BinaryField(**_common_kwargs(info))
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@register_field("UUIDField")
|
|
238
|
+
def _uuid_field(info: FieldInfo):
|
|
239
|
+
return tortoise_fields.UUIDField(**_common_kwargs(info))
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@register_field("JSONField")
|
|
243
|
+
def _json_field(info: FieldInfo):
|
|
244
|
+
return tortoise_fields.JSONField(**_common_kwargs(info))
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
# ---------------------------------------------------------------------------
|
|
248
|
+
# File / path fields (approximate mappings -- store path as CharField)
|
|
249
|
+
# ---------------------------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@register_field("FileField")
|
|
253
|
+
def _file_field(info: FieldInfo):
|
|
254
|
+
logger.info("Mapping FileField '%s' to CharField (stores path).", info.name)
|
|
255
|
+
kwargs = _common_kwargs(info)
|
|
256
|
+
kwargs["max_length"] = info.max_length or 100
|
|
257
|
+
return tortoise_fields.CharField(**kwargs)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
# NOTE: ImageField's get_internal_type() returns "CharField", so this converter
|
|
261
|
+
# is only reached if a custom subclass overrides get_internal_type() to return
|
|
262
|
+
# "ImageField". Registered for completeness and forward-compatibility.
|
|
263
|
+
@register_field("ImageField")
|
|
264
|
+
def _image_field(info: FieldInfo):
|
|
265
|
+
logger.info("Mapping ImageField '%s' to CharField (stores path).", info.name)
|
|
266
|
+
kwargs = _common_kwargs(info)
|
|
267
|
+
kwargs["max_length"] = info.max_length or 100
|
|
268
|
+
return tortoise_fields.CharField(**kwargs)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@register_field("FilePathField")
|
|
272
|
+
def _filepath_field(info: FieldInfo):
|
|
273
|
+
logger.info("Mapping FilePathField '%s' to CharField.", info.name)
|
|
274
|
+
kwargs = _common_kwargs(info)
|
|
275
|
+
kwargs["max_length"] = info.max_length or 100
|
|
276
|
+
return tortoise_fields.CharField(**kwargs)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
# ---------------------------------------------------------------------------
|
|
280
|
+
# Specialised string fields (approximate mappings)
|
|
281
|
+
# ---------------------------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
@register_field("SlugField")
|
|
285
|
+
def _slug_field(info: FieldInfo):
|
|
286
|
+
kwargs = _common_kwargs(info)
|
|
287
|
+
kwargs["max_length"] = info.max_length or 50
|
|
288
|
+
return tortoise_fields.CharField(**kwargs)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# NOTE: EmailField's get_internal_type() returns "CharField", so this converter
|
|
292
|
+
# is only reached if a custom subclass overrides get_internal_type() to return
|
|
293
|
+
# "EmailField". Registered for completeness and forward-compatibility.
|
|
294
|
+
@register_field("EmailField")
|
|
295
|
+
def _email_field(info: FieldInfo):
|
|
296
|
+
kwargs = _common_kwargs(info)
|
|
297
|
+
kwargs["max_length"] = info.max_length or 254
|
|
298
|
+
return tortoise_fields.CharField(**kwargs)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# NOTE: URLField's get_internal_type() returns "CharField", so this converter
|
|
302
|
+
# is only reached if a custom subclass overrides get_internal_type() to return
|
|
303
|
+
# "URLField". Registered for completeness and forward-compatibility.
|
|
304
|
+
@register_field("URLField")
|
|
305
|
+
def _url_field(info: FieldInfo):
|
|
306
|
+
kwargs = _common_kwargs(info)
|
|
307
|
+
kwargs["max_length"] = info.max_length or 200
|
|
308
|
+
return tortoise_fields.CharField(**kwargs)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
@register_field("GenericIPAddressField")
|
|
312
|
+
def _ip_field(info: FieldInfo):
|
|
313
|
+
kwargs = _common_kwargs(info)
|
|
314
|
+
kwargs["max_length"] = 39
|
|
315
|
+
return tortoise_fields.CharField(**kwargs)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
# ---------------------------------------------------------------------------
|
|
319
|
+
# Relational field mapping
|
|
320
|
+
# ---------------------------------------------------------------------------
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
# Lazy import to avoid importing tortoise.fields.relational at module level
|
|
324
|
+
def _get_on_delete_enum():
|
|
325
|
+
"""Lazy import of Tortoise OnDelete enum."""
|
|
326
|
+
from tortoise.fields.relational import OnDelete
|
|
327
|
+
|
|
328
|
+
return OnDelete
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
# Django on_delete name -> Tortoise OnDelete enum member name
|
|
332
|
+
ON_DELETE_MAP: dict[str, str] = {
|
|
333
|
+
"CASCADE": "CASCADE",
|
|
334
|
+
"SET_NULL": "SET_NULL",
|
|
335
|
+
"SET_DEFAULT": "SET_DEFAULT",
|
|
336
|
+
"PROTECT": "RESTRICT", # Tortoise has RESTRICT, not PROTECT
|
|
337
|
+
"RESTRICT": "RESTRICT",
|
|
338
|
+
"DO_NOTHING": "NO_ACTION", # Tortoise uses NO_ACTION, not DO_NOTHING
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _map_on_delete(django_on_delete: str | None):
|
|
343
|
+
"""Map a Django on_delete name to the Tortoise OnDelete enum value."""
|
|
344
|
+
OnDelete = _get_on_delete_enum()
|
|
345
|
+
if django_on_delete is None:
|
|
346
|
+
return OnDelete.CASCADE
|
|
347
|
+
mapped_name = ON_DELETE_MAP.get(django_on_delete, "CASCADE")
|
|
348
|
+
return OnDelete[mapped_name]
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def convert_relation_field(
|
|
352
|
+
field_info: FieldInfo,
|
|
353
|
+
tortoise_app_name: str = "django_tortoise",
|
|
354
|
+
) -> tuple[str, object] | None:
|
|
355
|
+
"""
|
|
356
|
+
Convert a relational FieldInfo to a Tortoise relation field.
|
|
357
|
+
|
|
358
|
+
Uses the model registry to resolve the target model. Returns
|
|
359
|
+
``(field_name, tortoise_field)`` or ``None`` if the relation
|
|
360
|
+
target is unavailable (e.g., excluded model).
|
|
361
|
+
"""
|
|
362
|
+
from django_tortoise.registry import model_registry
|
|
363
|
+
|
|
364
|
+
target_model = field_info.related_model
|
|
365
|
+
if target_model is None:
|
|
366
|
+
logger.warning(
|
|
367
|
+
"Relation field '%s' has no related model. Skipping.",
|
|
368
|
+
field_info.name,
|
|
369
|
+
)
|
|
370
|
+
return None
|
|
371
|
+
|
|
372
|
+
if not model_registry.is_registered(target_model):
|
|
373
|
+
logger.warning(
|
|
374
|
+
"Relation field '%s' points to unregistered/excluded model '%s'. Skipping.",
|
|
375
|
+
field_info.name,
|
|
376
|
+
field_info.related_model_label,
|
|
377
|
+
)
|
|
378
|
+
return None
|
|
379
|
+
|
|
380
|
+
target_tortoise = model_registry.get_tortoise_model(target_model)
|
|
381
|
+
if target_tortoise is None:
|
|
382
|
+
return None
|
|
383
|
+
target_ref = f"{tortoise_app_name}.{target_tortoise.__name__}"
|
|
384
|
+
|
|
385
|
+
return _convert_relation_to_field(field_info, target_ref)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def convert_relation_field_by_name(
|
|
389
|
+
field_info: FieldInfo,
|
|
390
|
+
tortoise_app_name: str = "django_tortoise",
|
|
391
|
+
class_name_map: dict[type, str] | None = None,
|
|
392
|
+
) -> tuple[str, object] | None:
|
|
393
|
+
"""
|
|
394
|
+
Convert a relational FieldInfo using a pre-computed class name map.
|
|
395
|
+
|
|
396
|
+
This variant does not require the target Tortoise model to exist yet.
|
|
397
|
+
It uses *class_name_map* (Django model class -> Tortoise class name)
|
|
398
|
+
to build the string reference. Returns ``(field_name, tortoise_field)``
|
|
399
|
+
or ``None`` if the target is not in the map.
|
|
400
|
+
"""
|
|
401
|
+
target_model = field_info.related_model
|
|
402
|
+
if target_model is None:
|
|
403
|
+
logger.warning(
|
|
404
|
+
"Relation field '%s' has no related model. Skipping.",
|
|
405
|
+
field_info.name,
|
|
406
|
+
)
|
|
407
|
+
return None
|
|
408
|
+
|
|
409
|
+
if class_name_map is None:
|
|
410
|
+
class_name_map = {}
|
|
411
|
+
|
|
412
|
+
tortoise_class_name = class_name_map.get(target_model)
|
|
413
|
+
if tortoise_class_name is None:
|
|
414
|
+
logger.warning(
|
|
415
|
+
"Relation field '%s' points to unregistered/excluded model '%s'. Skipping.",
|
|
416
|
+
field_info.name,
|
|
417
|
+
field_info.related_model_label,
|
|
418
|
+
)
|
|
419
|
+
return None
|
|
420
|
+
|
|
421
|
+
target_ref = f"{tortoise_app_name}.{tortoise_class_name}"
|
|
422
|
+
return _convert_relation_to_field(field_info, target_ref)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _convert_relation_to_field(field_info: FieldInfo, target_ref: str) -> tuple[str, object] | None:
|
|
426
|
+
"""Dispatch to the appropriate relational field builder."""
|
|
427
|
+
internal_type = field_info.internal_type
|
|
428
|
+
|
|
429
|
+
if internal_type == "ForeignKey":
|
|
430
|
+
return _build_fk(field_info, target_ref)
|
|
431
|
+
elif internal_type == "OneToOneField":
|
|
432
|
+
return _build_o2o(field_info, target_ref)
|
|
433
|
+
elif field_info.many_to_many:
|
|
434
|
+
return _build_m2m(field_info, target_ref)
|
|
435
|
+
|
|
436
|
+
return None
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _build_fk(info: FieldInfo, target_ref: str) -> tuple[str, object]:
|
|
440
|
+
"""Build a Tortoise ForeignKeyField from introspected field info."""
|
|
441
|
+
on_delete = _map_on_delete(info.on_delete)
|
|
442
|
+
related_name: str | bool | None = None
|
|
443
|
+
if info.related_name and info.related_name != "+":
|
|
444
|
+
related_name = info.related_name
|
|
445
|
+
else:
|
|
446
|
+
# Tortoise requires a related_name or False to disable.
|
|
447
|
+
# Use False to avoid auto-generated reverse accessors clashing.
|
|
448
|
+
related_name = False
|
|
449
|
+
|
|
450
|
+
kwargs: dict[str, Any] = {}
|
|
451
|
+
if info.column:
|
|
452
|
+
kwargs["source_field"] = info.column
|
|
453
|
+
if info.null:
|
|
454
|
+
kwargs["null"] = True
|
|
455
|
+
return (
|
|
456
|
+
info.name,
|
|
457
|
+
tortoise_fields.ForeignKeyField(
|
|
458
|
+
target_ref,
|
|
459
|
+
related_name=related_name, # type: ignore[arg-type]
|
|
460
|
+
on_delete=on_delete,
|
|
461
|
+
**kwargs,
|
|
462
|
+
),
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _build_o2o(info: FieldInfo, target_ref: str) -> tuple[str, object]:
|
|
467
|
+
"""Build a Tortoise OneToOneField from introspected field info."""
|
|
468
|
+
on_delete = _map_on_delete(info.on_delete)
|
|
469
|
+
related_name: str | bool | None = None
|
|
470
|
+
if info.related_name and info.related_name != "+":
|
|
471
|
+
related_name = info.related_name
|
|
472
|
+
else:
|
|
473
|
+
related_name = False
|
|
474
|
+
|
|
475
|
+
kwargs: dict[str, Any] = {}
|
|
476
|
+
if info.column:
|
|
477
|
+
kwargs["source_field"] = info.column
|
|
478
|
+
if info.null:
|
|
479
|
+
kwargs["null"] = True
|
|
480
|
+
return (
|
|
481
|
+
info.name,
|
|
482
|
+
tortoise_fields.OneToOneField(
|
|
483
|
+
target_ref,
|
|
484
|
+
related_name=related_name, # type: ignore[arg-type]
|
|
485
|
+
on_delete=on_delete,
|
|
486
|
+
**kwargs,
|
|
487
|
+
),
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def _build_m2m(info: FieldInfo, target_ref: str) -> tuple[str, object]:
|
|
492
|
+
"""Build a Tortoise ManyToManyField from introspected field info."""
|
|
493
|
+
related_name: str | bool | None = None
|
|
494
|
+
if info.related_name and info.related_name != "+":
|
|
495
|
+
related_name = info.related_name
|
|
496
|
+
else:
|
|
497
|
+
related_name = False
|
|
498
|
+
|
|
499
|
+
kwargs: dict[str, Any] = {}
|
|
500
|
+
if info.through_db_table:
|
|
501
|
+
kwargs["through"] = info.through_db_table
|
|
502
|
+
return (
|
|
503
|
+
info.name,
|
|
504
|
+
tortoise_fields.ManyToManyField(target_ref, related_name=related_name, **kwargs), # type: ignore[arg-type]
|
|
505
|
+
)
|