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.
@@ -0,0 +1 @@
1
+ version = "0.0.0"
@@ -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
@@ -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,6 @@
1
+ class QueryError(Exception):
2
+ pass
3
+
4
+
5
+ class QueryNotSupported(QueryError):
6
+ pass
@@ -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
+ )