django-ormql 0.0.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_ormql/__init__.py +1 -0
- django_ormql/columns.py +194 -0
- django_ormql/db_func.py +161 -0
- django_ormql/engine.py +26 -0
- django_ormql/exceptions.py +6 -0
- django_ormql/model_utils.py +260 -0
- django_ormql/query.py +863 -0
- django_ormql/tables.py +265 -0
- django_ormql-0.0.0.dist-info/METADATA +56 -0
- django_ormql-0.0.0.dist-info/RECORD +13 -0
- django_ormql-0.0.0.dist-info/WHEEL +5 -0
- django_ormql-0.0.0.dist-info/licenses/LICENSE +201 -0
- django_ormql-0.0.0.dist-info/top_level.txt +1 -0
django_ormql/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
version = "0.0.0"
|
django_ormql/columns.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import inspect
|
|
3
|
+
|
|
4
|
+
from django.db.models import F, Expression, OuterRef, Subquery
|
|
5
|
+
from django.db.models.expressions import ResolvedOuterRef
|
|
6
|
+
from django.utils import tree
|
|
7
|
+
from django.utils.module_loading import import_string
|
|
8
|
+
|
|
9
|
+
from django_ormql.exceptions import QueryNotSupported
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BaseColumn:
|
|
13
|
+
def __init__(self, **kwargs):
|
|
14
|
+
self.source = kwargs.get("source")
|
|
15
|
+
self._nullable = kwargs.get("nullable")
|
|
16
|
+
self.enum_options = kwargs.get("enum_options", None)
|
|
17
|
+
|
|
18
|
+
def bind(self, field_name, parent):
|
|
19
|
+
self.field_name = field_name
|
|
20
|
+
self.parent = parent
|
|
21
|
+
if self.source is None:
|
|
22
|
+
self.source = field_name
|
|
23
|
+
|
|
24
|
+
def resolve_column_path(self, remaining_path):
|
|
25
|
+
return F(self.source)
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def sql_type(self):
|
|
29
|
+
return ""
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def nullable(self):
|
|
33
|
+
return self._nullable
|
|
34
|
+
|
|
35
|
+
@nullable.setter
|
|
36
|
+
def nullable(self, v):
|
|
37
|
+
self._nullable = v
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class IntColumn(BaseColumn):
|
|
41
|
+
sql_type = "INT"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class FloatColumn(BaseColumn):
|
|
45
|
+
sql_type = "FLOAT"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class BooleanColumn(BaseColumn):
|
|
49
|
+
sql_type = "BOOLEAN"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class TextColumn(BaseColumn):
|
|
53
|
+
sql_type = "TEXT"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class DateColumn(BaseColumn):
|
|
57
|
+
sql_type = "DATE"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class DateTimeColumn(BaseColumn):
|
|
61
|
+
sql_type = "DATETIME"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class TimeColumn(BaseColumn):
|
|
65
|
+
sql_type = "TIME"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class DurationColumn(BaseColumn):
|
|
69
|
+
sql_type = "DURATION"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class DecimalColumn(BaseColumn):
|
|
73
|
+
sql_type = "DECIMAL"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class JsonColumn(BaseColumn):
|
|
77
|
+
sql_type = "JSONB"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ModelColumn(BaseColumn):
|
|
81
|
+
# Fallback if no type matches
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class ForeignKeyColumn(BaseColumn):
|
|
86
|
+
def __init__(self, related_table, **kwargs):
|
|
87
|
+
self.related_table = related_table
|
|
88
|
+
super().__init__(**kwargs)
|
|
89
|
+
|
|
90
|
+
def _prefix_expression(self, expr, prefix):
|
|
91
|
+
if isinstance(expr, tree.Node):
|
|
92
|
+
new_expr = expr.create(connector=expr.connector, negated=expr.negated)
|
|
93
|
+
children = []
|
|
94
|
+
for e in expr.children:
|
|
95
|
+
e = self._prefix_expression(e, prefix)
|
|
96
|
+
if isinstance(e, F):
|
|
97
|
+
e = F(f"{prefix}__{expr}")
|
|
98
|
+
children.append(e)
|
|
99
|
+
new_expr.children = children
|
|
100
|
+
return new_expr
|
|
101
|
+
elif isinstance(expr, Expression):
|
|
102
|
+
source_expressions = []
|
|
103
|
+
for e in expr.get_source_expressions():
|
|
104
|
+
e = self._prefix_expression(e, prefix)
|
|
105
|
+
source_expressions.append(e)
|
|
106
|
+
expr = copy.deepcopy(expr)
|
|
107
|
+
expr.set_source_expressions(source_expressions)
|
|
108
|
+
elif isinstance(expr, tuple) and len(expr) == 2:
|
|
109
|
+
# kwarg of Q()
|
|
110
|
+
return f"{prefix}__{expr[0]}", expr[1]
|
|
111
|
+
elif isinstance(expr, OuterRef):
|
|
112
|
+
return OuterRef(f"{prefix}__{expr.name}")
|
|
113
|
+
elif isinstance(expr, ResolvedOuterRef):
|
|
114
|
+
return ResolvedOuterRef(f"{prefix}__{expr.name}")
|
|
115
|
+
elif isinstance(expr, F):
|
|
116
|
+
return F(f"{prefix}__{expr.name}")
|
|
117
|
+
elif isinstance(expr, Subquery):
|
|
118
|
+
expr = expr.copy()
|
|
119
|
+
expr.query.where = self._prefix_expression(expr.query.where, self.source)
|
|
120
|
+
return expr
|
|
121
|
+
else:
|
|
122
|
+
raise TypeError(f"Unexpected type {expr!r}")
|
|
123
|
+
return expr
|
|
124
|
+
|
|
125
|
+
def bind(self, field_name, parent):
|
|
126
|
+
from .tables import ModelTable
|
|
127
|
+
|
|
128
|
+
super().bind(field_name, parent)
|
|
129
|
+
if self.related_table == "self":
|
|
130
|
+
self.related_table = parent.__class__
|
|
131
|
+
elif isinstance(self.related_table, str):
|
|
132
|
+
if "." in self.related_table:
|
|
133
|
+
self.related_table = import_string(self.related_table)
|
|
134
|
+
else:
|
|
135
|
+
self.related_table = getattr(
|
|
136
|
+
inspect.getmodule(parent), self.related_table
|
|
137
|
+
)
|
|
138
|
+
elif not issubclass(self.related_table, ModelTable):
|
|
139
|
+
raise TypeError("Related field does not point to table")
|
|
140
|
+
|
|
141
|
+
def resolve_column_path(self, remaining_path):
|
|
142
|
+
if len(remaining_path) > 20:
|
|
143
|
+
raise QueryNotSupported("Upper limit of JOINs reached.")
|
|
144
|
+
rt = self.related_table(is_related=True)
|
|
145
|
+
if remaining_path:
|
|
146
|
+
related_field = rt.resolve_column_path(remaining_path)
|
|
147
|
+
|
|
148
|
+
if isinstance(related_field, ResolvedOuterRef):
|
|
149
|
+
return ResolvedOuterRef("__".join([self.source, related_field.name]))
|
|
150
|
+
|
|
151
|
+
elif isinstance(related_field, F):
|
|
152
|
+
return F("__".join([self.source, related_field.name]))
|
|
153
|
+
|
|
154
|
+
elif isinstance(related_field, (Expression, tree.Node)):
|
|
155
|
+
return self._prefix_expression(related_field, self.source)
|
|
156
|
+
|
|
157
|
+
elif isinstance(related_field, Subquery):
|
|
158
|
+
expr = related_field.copy()
|
|
159
|
+
expr.query.where = self._prefix_expression(
|
|
160
|
+
related_field.query.where, self.source
|
|
161
|
+
)
|
|
162
|
+
return expr
|
|
163
|
+
|
|
164
|
+
else:
|
|
165
|
+
raise TypeError(f"Unexpected type {type(related_field)}")
|
|
166
|
+
else:
|
|
167
|
+
return F("__".join([self.source, "pk"]))
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class GeneratedColumn(BaseColumn):
|
|
171
|
+
nullable = True
|
|
172
|
+
|
|
173
|
+
def __init__(self, expr, **kwargs):
|
|
174
|
+
self.expr = expr
|
|
175
|
+
super().__init__(**kwargs)
|
|
176
|
+
|
|
177
|
+
def resolve_column_path(self, remaining_path):
|
|
178
|
+
return self.expr
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def get_column_kwargs(model_field):
|
|
182
|
+
"""
|
|
183
|
+
Creates a default instance of a basic non-relational field.
|
|
184
|
+
"""
|
|
185
|
+
kwargs = {}
|
|
186
|
+
|
|
187
|
+
# The following will only be used by ModelField classes.
|
|
188
|
+
# Gets removed for everything else.
|
|
189
|
+
kwargs["model_field"] = model_field
|
|
190
|
+
|
|
191
|
+
if model_field.null:
|
|
192
|
+
kwargs["nullable"] = True
|
|
193
|
+
|
|
194
|
+
return kwargs
|
django_ormql/db_func.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
from django.core.exceptions import FieldError
|
|
2
|
+
from django.db.models import Func, fields, Value, ExpressionWrapper, Case, Subquery
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Equal(Func):
|
|
6
|
+
arg_joiner = " = "
|
|
7
|
+
arity = 2
|
|
8
|
+
function = ""
|
|
9
|
+
conditional = True
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class NotEqual(Func):
|
|
13
|
+
arg_joiner = " != "
|
|
14
|
+
arity = 2
|
|
15
|
+
function = ""
|
|
16
|
+
conditional = True
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class GreaterThan(Func):
|
|
20
|
+
arg_joiner = " > "
|
|
21
|
+
arity = 2
|
|
22
|
+
function = ""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class GreaterEqualThan(Func):
|
|
26
|
+
arg_joiner = " >= "
|
|
27
|
+
arity = 2
|
|
28
|
+
function = ""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class LowerEqualThan(Func):
|
|
32
|
+
arg_joiner = " <= "
|
|
33
|
+
arity = 2
|
|
34
|
+
function = ""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class LowerThan(Func):
|
|
38
|
+
arg_joiner = " < "
|
|
39
|
+
arity = 2
|
|
40
|
+
function = ""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Is(Func):
|
|
44
|
+
arg_joiner = " IS "
|
|
45
|
+
arity = 2
|
|
46
|
+
function = ""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Like(Func):
|
|
50
|
+
arg_joiner = " LIKE "
|
|
51
|
+
arity = 2
|
|
52
|
+
function = ""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class NumericResolveMixin:
|
|
56
|
+
def _resolve_output_field(self):
|
|
57
|
+
# Auto-resolve of INT*DECIMAL to DECIMAL etc
|
|
58
|
+
source_types = set(
|
|
59
|
+
type(source) for source in self.get_source_fields() if source is not None
|
|
60
|
+
)
|
|
61
|
+
if len(source_types) == 1:
|
|
62
|
+
return list(source_types)[0]()
|
|
63
|
+
elif source_types == {fields.DecimalField, fields.IntegerField}:
|
|
64
|
+
return fields.DecimalField(
|
|
65
|
+
max_digits=max(
|
|
66
|
+
f.max_digits
|
|
67
|
+
for f in self.get_source_fields()
|
|
68
|
+
if isinstance(f, fields.DecimalField)
|
|
69
|
+
),
|
|
70
|
+
decimal_places=max(
|
|
71
|
+
f.decimal_places
|
|
72
|
+
for f in self.get_source_fields()
|
|
73
|
+
if isinstance(f, fields.DecimalField)
|
|
74
|
+
),
|
|
75
|
+
)
|
|
76
|
+
elif source_types == {fields.FloatField, fields.IntegerField}:
|
|
77
|
+
return fields.FloatField()
|
|
78
|
+
elif source_types == {fields.FloatField, fields.DecimalField}:
|
|
79
|
+
return fields.DecimalField(
|
|
80
|
+
max_digits=max(
|
|
81
|
+
f.max_digits
|
|
82
|
+
for f in self.get_source_fields()
|
|
83
|
+
if isinstance(f, fields.DecimalField)
|
|
84
|
+
),
|
|
85
|
+
decimal_places=max(
|
|
86
|
+
f.decimal_places
|
|
87
|
+
for f in self.get_source_fields()
|
|
88
|
+
if isinstance(f, fields.DecimalField)
|
|
89
|
+
),
|
|
90
|
+
)
|
|
91
|
+
elif source_types == {
|
|
92
|
+
fields.FloatField,
|
|
93
|
+
fields.DecimalField,
|
|
94
|
+
fields.IntegerField,
|
|
95
|
+
}:
|
|
96
|
+
return fields.DecimalField(
|
|
97
|
+
max_digits=max(
|
|
98
|
+
f.max_digits
|
|
99
|
+
for f in self.get_source_fields()
|
|
100
|
+
if isinstance(f, fields.DecimalField)
|
|
101
|
+
),
|
|
102
|
+
decimal_places=max(
|
|
103
|
+
f.decimal_places
|
|
104
|
+
for f in self.get_source_fields()
|
|
105
|
+
if isinstance(f, fields.DecimalField)
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
else:
|
|
109
|
+
raise FieldError(
|
|
110
|
+
"Expression contains mixed types: %s."
|
|
111
|
+
% ", ".join(t.__name__ for t in source_types)
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class Add(NumericResolveMixin, Func):
|
|
116
|
+
arg_joiner = " + "
|
|
117
|
+
arity = 2
|
|
118
|
+
function = ""
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class Sub(NumericResolveMixin, Func):
|
|
122
|
+
arg_joiner = " - "
|
|
123
|
+
arity = 2
|
|
124
|
+
function = ""
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class Mul(NumericResolveMixin, Func):
|
|
128
|
+
arg_joiner = " * "
|
|
129
|
+
arity = 2
|
|
130
|
+
function = ""
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class Div(NumericResolveMixin, Func):
|
|
134
|
+
arg_joiner = " / "
|
|
135
|
+
arity = 2
|
|
136
|
+
function = ""
|
|
137
|
+
|
|
138
|
+
def __init__(self, *expressions, output_field=None, **extra):
|
|
139
|
+
# We never want integer division
|
|
140
|
+
super().__init__(
|
|
141
|
+
expressions[0],
|
|
142
|
+
ExpressionWrapper(
|
|
143
|
+
expressions[1] * Value(1.0), output_field=fields.FloatField()
|
|
144
|
+
),
|
|
145
|
+
output_field=output_field,
|
|
146
|
+
**extra,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class Mod(NumericResolveMixin, Func):
|
|
151
|
+
arg_joiner = " %% "
|
|
152
|
+
arity = 2
|
|
153
|
+
function = ""
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class NumericAwareCase(NumericResolveMixin, Case):
|
|
157
|
+
pass
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class AutoTypedSubquery(Subquery):
|
|
161
|
+
pass
|
django_ormql/engine.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
|
|
3
|
+
from .query import Query
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class QueryEngine:
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self.tables = {}
|
|
9
|
+
|
|
10
|
+
def register_table(self, table):
|
|
11
|
+
self.tables[table.Meta.name] = table
|
|
12
|
+
|
|
13
|
+
def query(
|
|
14
|
+
self,
|
|
15
|
+
query,
|
|
16
|
+
placeholders=None,
|
|
17
|
+
timezone=datetime.timezone.utc,
|
|
18
|
+
default_limit=None,
|
|
19
|
+
):
|
|
20
|
+
return Query(
|
|
21
|
+
query,
|
|
22
|
+
self.tables,
|
|
23
|
+
placeholders,
|
|
24
|
+
timezone,
|
|
25
|
+
default_limit,
|
|
26
|
+
).evaluate()
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Vendored from https://github.com/encode/django-rest-framework
|
|
3
|
+
|
|
4
|
+
Copyright © 2011-present, [Encode OSS Ltd](https://www.encode.io/).
|
|
5
|
+
All rights reserved.
|
|
6
|
+
|
|
7
|
+
Redistribution and use in source and binary forms, with or without
|
|
8
|
+
modification, are permitted provided that the following conditions are met:
|
|
9
|
+
|
|
10
|
+
* Redistributions of source code must retain the above copyright notice, this
|
|
11
|
+
list of conditions and the following disclaimer.
|
|
12
|
+
|
|
13
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
|
14
|
+
this list of conditions and the following disclaimer in the documentation
|
|
15
|
+
and/or other materials provided with the distribution.
|
|
16
|
+
|
|
17
|
+
* Neither the name of the copyright holder nor the names of its
|
|
18
|
+
contributors may be used to endorse or promote products derived from
|
|
19
|
+
this software without specific prior written permission.
|
|
20
|
+
|
|
21
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
22
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
23
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
24
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
25
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
26
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
27
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
28
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
29
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
30
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
import inspect
|
|
34
|
+
from collections import namedtuple
|
|
35
|
+
from typing import MutableMapping
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class BindingDict(MutableMapping):
|
|
39
|
+
"""
|
|
40
|
+
This dict-like object is used to store fields on a serializer.
|
|
41
|
+
|
|
42
|
+
This ensures that whenever fields are added to the serializer we call
|
|
43
|
+
`field.bind()` so that the `field_name` and `parent` attributes
|
|
44
|
+
can be set correctly.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, serializer):
|
|
48
|
+
self.serializer = serializer
|
|
49
|
+
self.fields = {}
|
|
50
|
+
|
|
51
|
+
def __setitem__(self, key, field):
|
|
52
|
+
self.fields[key] = field
|
|
53
|
+
field.bind(field_name=key, parent=self.serializer)
|
|
54
|
+
|
|
55
|
+
def __getitem__(self, key):
|
|
56
|
+
return self.fields[key]
|
|
57
|
+
|
|
58
|
+
def __delitem__(self, key):
|
|
59
|
+
del self.fields[key]
|
|
60
|
+
|
|
61
|
+
def __iter__(self):
|
|
62
|
+
return iter(self.fields)
|
|
63
|
+
|
|
64
|
+
def __len__(self):
|
|
65
|
+
return len(self.fields)
|
|
66
|
+
|
|
67
|
+
def __repr__(self):
|
|
68
|
+
return dict.__repr__(self.fields)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ClassLookupDict:
|
|
72
|
+
"""
|
|
73
|
+
Takes a dictionary with classes as keys.
|
|
74
|
+
Lookups against this object will traverses the object's inheritance
|
|
75
|
+
hierarchy in method resolution order, and returns the first matching value
|
|
76
|
+
from the dictionary or raises a KeyError if nothing matches.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def __init__(self, mapping):
|
|
80
|
+
self.mapping = mapping
|
|
81
|
+
|
|
82
|
+
def __getitem__(self, key):
|
|
83
|
+
if hasattr(key, "_proxy_class"):
|
|
84
|
+
# Deal with proxy classes. Ie. BoundField behaves as if it
|
|
85
|
+
# is a Field instance when using ClassLookupDict.
|
|
86
|
+
base_class = key._proxy_class
|
|
87
|
+
else:
|
|
88
|
+
base_class = key.__class__
|
|
89
|
+
|
|
90
|
+
for cls in inspect.getmro(base_class):
|
|
91
|
+
if cls in self.mapping:
|
|
92
|
+
return self.mapping[cls]
|
|
93
|
+
raise KeyError("Class %s not found in lookup." % base_class.__name__)
|
|
94
|
+
|
|
95
|
+
def __setitem__(self, key, value):
|
|
96
|
+
self.mapping[key] = value
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
FieldInfo = namedtuple(
|
|
100
|
+
"FieldInfo",
|
|
101
|
+
[
|
|
102
|
+
"pk", # Model field instance
|
|
103
|
+
"fields", # Dict of field name -> model field instance
|
|
104
|
+
"forward_relations", # Dict of field name -> RelationInfo
|
|
105
|
+
"reverse_relations", # Dict of field name -> RelationInfo
|
|
106
|
+
"fields_and_pk", # Shortcut for 'pk' + 'fields'
|
|
107
|
+
"relations", # Shortcut for 'forward_relations' + 'reverse_relations'
|
|
108
|
+
],
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
RelationInfo = namedtuple(
|
|
112
|
+
"RelationInfo",
|
|
113
|
+
[
|
|
114
|
+
"model_field",
|
|
115
|
+
"related_model",
|
|
116
|
+
"to_many",
|
|
117
|
+
"to_field",
|
|
118
|
+
"has_through_model",
|
|
119
|
+
"reverse",
|
|
120
|
+
],
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def is_abstract_model(model):
|
|
125
|
+
"""
|
|
126
|
+
Given a model class, returns a boolean True if it is abstract and False if it is not.
|
|
127
|
+
"""
|
|
128
|
+
return (
|
|
129
|
+
hasattr(model, "_meta")
|
|
130
|
+
and hasattr(model._meta, "abstract")
|
|
131
|
+
and model._meta.abstract
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _get_pk(opts):
|
|
136
|
+
pk = opts.pk
|
|
137
|
+
rel = pk.remote_field
|
|
138
|
+
|
|
139
|
+
while rel and rel.parent_link:
|
|
140
|
+
# If model is a child via multi-table inheritance, use parent's pk.
|
|
141
|
+
pk = pk.remote_field.model._meta.pk
|
|
142
|
+
rel = pk.remote_field
|
|
143
|
+
|
|
144
|
+
return pk
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _get_fields(opts):
|
|
148
|
+
fields = {}
|
|
149
|
+
for field in [
|
|
150
|
+
field for field in opts.fields if field.serialize and not field.remote_field
|
|
151
|
+
]:
|
|
152
|
+
fields[field.name] = field
|
|
153
|
+
|
|
154
|
+
return fields
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _get_to_field(field):
|
|
158
|
+
return getattr(field, "to_fields", None) and field.to_fields[0]
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _merge_fields_and_pk(pk, fields):
|
|
162
|
+
fields_and_pk = {"pk": pk, pk.name: pk}
|
|
163
|
+
fields_and_pk.update(fields)
|
|
164
|
+
|
|
165
|
+
return fields_and_pk
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _merge_relationships(forward_relations, reverse_relations):
|
|
169
|
+
return {**forward_relations, **reverse_relations}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _get_forward_relationships(opts):
|
|
173
|
+
"""
|
|
174
|
+
Returns a dict of field names to `RelationInfo`.
|
|
175
|
+
"""
|
|
176
|
+
forward_relations = {}
|
|
177
|
+
for field in [
|
|
178
|
+
field for field in opts.fields if field.serialize and field.remote_field
|
|
179
|
+
]:
|
|
180
|
+
forward_relations[field.name] = RelationInfo(
|
|
181
|
+
model_field=field,
|
|
182
|
+
related_model=field.remote_field.model,
|
|
183
|
+
to_many=False,
|
|
184
|
+
to_field=_get_to_field(field),
|
|
185
|
+
has_through_model=False,
|
|
186
|
+
reverse=False,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Deal with forward many-to-many relationships.
|
|
190
|
+
for field in [field for field in opts.many_to_many if field.serialize]:
|
|
191
|
+
forward_relations[field.name] = RelationInfo(
|
|
192
|
+
model_field=field,
|
|
193
|
+
related_model=field.remote_field.model,
|
|
194
|
+
to_many=True,
|
|
195
|
+
# manytomany do not have to_fields
|
|
196
|
+
to_field=None,
|
|
197
|
+
has_through_model=(not field.remote_field.through._meta.auto_created),
|
|
198
|
+
reverse=False,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
return forward_relations
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _get_reverse_relationships(opts):
|
|
205
|
+
"""
|
|
206
|
+
Returns a dict of field names to `RelationInfo`.
|
|
207
|
+
"""
|
|
208
|
+
reverse_relations = {}
|
|
209
|
+
all_related_objects = [r for r in opts.related_objects if not r.field.many_to_many]
|
|
210
|
+
for relation in all_related_objects:
|
|
211
|
+
accessor_name = relation.get_accessor_name()
|
|
212
|
+
reverse_relations[accessor_name] = RelationInfo(
|
|
213
|
+
model_field=None,
|
|
214
|
+
related_model=relation.related_model,
|
|
215
|
+
to_many=relation.field.remote_field.multiple,
|
|
216
|
+
to_field=_get_to_field(relation.field),
|
|
217
|
+
has_through_model=False,
|
|
218
|
+
reverse=True,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# Deal with reverse many-to-many relationships.
|
|
222
|
+
all_related_many_to_many_objects = [
|
|
223
|
+
r for r in opts.related_objects if r.field.many_to_many
|
|
224
|
+
]
|
|
225
|
+
for relation in all_related_many_to_many_objects:
|
|
226
|
+
accessor_name = relation.get_accessor_name()
|
|
227
|
+
reverse_relations[accessor_name] = RelationInfo(
|
|
228
|
+
model_field=None,
|
|
229
|
+
related_model=relation.related_model,
|
|
230
|
+
to_many=True,
|
|
231
|
+
# manytomany do not have to_fields
|
|
232
|
+
to_field=None,
|
|
233
|
+
has_through_model=(
|
|
234
|
+
(getattr(relation.field.remote_field, "through", None) is not None)
|
|
235
|
+
and not relation.field.remote_field.through._meta.auto_created
|
|
236
|
+
),
|
|
237
|
+
reverse=True,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
return reverse_relations
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def get_field_info(model):
|
|
244
|
+
"""
|
|
245
|
+
Given a model class, returns a `FieldInfo` instance, which is a
|
|
246
|
+
`namedtuple`, containing metadata about the various field types on the model
|
|
247
|
+
including information about their relationships.
|
|
248
|
+
"""
|
|
249
|
+
opts = model._meta.concrete_model._meta
|
|
250
|
+
|
|
251
|
+
pk = _get_pk(opts)
|
|
252
|
+
fields = _get_fields(opts)
|
|
253
|
+
forward_relations = _get_forward_relationships(opts)
|
|
254
|
+
reverse_relations = _get_reverse_relationships(opts)
|
|
255
|
+
fields_and_pk = _merge_fields_and_pk(pk, fields)
|
|
256
|
+
relationships = _merge_relationships(forward_relations, reverse_relations)
|
|
257
|
+
|
|
258
|
+
return FieldInfo(
|
|
259
|
+
pk, fields, forward_relations, reverse_relations, fields_and_pk, relationships
|
|
260
|
+
)
|