plain.postgres 0.84.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.
- plain/postgres/CHANGELOG.md +1028 -0
- plain/postgres/README.md +925 -0
- plain/postgres/__init__.py +120 -0
- plain/postgres/agents/.claude/rules/plain-postgres.md +78 -0
- plain/postgres/aggregates.py +236 -0
- plain/postgres/backups/__init__.py +0 -0
- plain/postgres/backups/cli.py +148 -0
- plain/postgres/backups/clients.py +94 -0
- plain/postgres/backups/core.py +172 -0
- plain/postgres/base.py +1415 -0
- plain/postgres/cli/__init__.py +3 -0
- plain/postgres/cli/db.py +142 -0
- plain/postgres/cli/migrations.py +1085 -0
- plain/postgres/config.py +18 -0
- plain/postgres/connection.py +1331 -0
- plain/postgres/connections.py +77 -0
- plain/postgres/constants.py +13 -0
- plain/postgres/constraints.py +495 -0
- plain/postgres/database_url.py +94 -0
- plain/postgres/db.py +59 -0
- plain/postgres/default_settings.py +38 -0
- plain/postgres/deletion.py +475 -0
- plain/postgres/dialect.py +640 -0
- plain/postgres/entrypoints.py +4 -0
- plain/postgres/enums.py +103 -0
- plain/postgres/exceptions.py +217 -0
- plain/postgres/expressions.py +1912 -0
- plain/postgres/fields/__init__.py +2118 -0
- plain/postgres/fields/encrypted.py +354 -0
- plain/postgres/fields/json.py +413 -0
- plain/postgres/fields/mixins.py +30 -0
- plain/postgres/fields/related.py +1192 -0
- plain/postgres/fields/related_descriptors.py +290 -0
- plain/postgres/fields/related_lookups.py +223 -0
- plain/postgres/fields/related_managers.py +661 -0
- plain/postgres/fields/reverse_descriptors.py +229 -0
- plain/postgres/fields/reverse_related.py +328 -0
- plain/postgres/fields/timezones.py +143 -0
- plain/postgres/forms.py +773 -0
- plain/postgres/functions/__init__.py +189 -0
- plain/postgres/functions/comparison.py +127 -0
- plain/postgres/functions/datetime.py +454 -0
- plain/postgres/functions/math.py +140 -0
- plain/postgres/functions/mixins.py +59 -0
- plain/postgres/functions/text.py +282 -0
- plain/postgres/functions/window.py +125 -0
- plain/postgres/indexes.py +286 -0
- plain/postgres/lookups.py +758 -0
- plain/postgres/meta.py +584 -0
- plain/postgres/migrations/__init__.py +53 -0
- plain/postgres/migrations/autodetector.py +1379 -0
- plain/postgres/migrations/exceptions.py +54 -0
- plain/postgres/migrations/executor.py +188 -0
- plain/postgres/migrations/graph.py +364 -0
- plain/postgres/migrations/loader.py +377 -0
- plain/postgres/migrations/migration.py +180 -0
- plain/postgres/migrations/operations/__init__.py +34 -0
- plain/postgres/migrations/operations/base.py +139 -0
- plain/postgres/migrations/operations/fields.py +373 -0
- plain/postgres/migrations/operations/models.py +798 -0
- plain/postgres/migrations/operations/special.py +184 -0
- plain/postgres/migrations/optimizer.py +74 -0
- plain/postgres/migrations/questioner.py +340 -0
- plain/postgres/migrations/recorder.py +119 -0
- plain/postgres/migrations/serializer.py +378 -0
- plain/postgres/migrations/state.py +882 -0
- plain/postgres/migrations/utils.py +147 -0
- plain/postgres/migrations/writer.py +302 -0
- plain/postgres/options.py +207 -0
- plain/postgres/otel.py +231 -0
- plain/postgres/preflight.py +336 -0
- plain/postgres/query.py +2242 -0
- plain/postgres/query_utils.py +456 -0
- plain/postgres/registry.py +217 -0
- plain/postgres/schema.py +1885 -0
- plain/postgres/sql/__init__.py +40 -0
- plain/postgres/sql/compiler.py +1869 -0
- plain/postgres/sql/constants.py +22 -0
- plain/postgres/sql/datastructures.py +222 -0
- plain/postgres/sql/query.py +2947 -0
- plain/postgres/sql/where.py +374 -0
- plain/postgres/test/__init__.py +0 -0
- plain/postgres/test/pytest.py +117 -0
- plain/postgres/test/utils.py +18 -0
- plain/postgres/transaction.py +222 -0
- plain/postgres/types.py +92 -0
- plain/postgres/types.pyi +751 -0
- plain/postgres/utils.py +345 -0
- plain_postgres-0.84.0.dist-info/METADATA +937 -0
- plain_postgres-0.84.0.dist-info/RECORD +93 -0
- plain_postgres-0.84.0.dist-info/WHEEL +4 -0
- plain_postgres-0.84.0.dist-info/entry_points.txt +5 -0
- plain_postgres-0.84.0.dist-info/licenses/LICENSE +61 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from plain import exceptions, preflight
|
|
8
|
+
from plain.postgres import expressions, lookups
|
|
9
|
+
from plain.postgres.constants import LOOKUP_SEP
|
|
10
|
+
from plain.postgres.dialect import adapt_json_value
|
|
11
|
+
from plain.postgres.fields import TextField
|
|
12
|
+
from plain.postgres.lookups import (
|
|
13
|
+
FieldGetDbPrepValueMixin,
|
|
14
|
+
Lookup,
|
|
15
|
+
OperatorLookup,
|
|
16
|
+
Transform,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from . import Field
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from plain.postgres.connection import DatabaseConnection
|
|
23
|
+
from plain.postgres.sql.compiler import SQLCompiler
|
|
24
|
+
from plain.preflight.results import PreflightResult
|
|
25
|
+
|
|
26
|
+
__all__ = ["JSONField"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class JSONField(Field):
|
|
30
|
+
empty_strings_allowed = False
|
|
31
|
+
description = "A JSON object"
|
|
32
|
+
default_error_messages = {
|
|
33
|
+
"invalid": "Value must be valid JSON.",
|
|
34
|
+
}
|
|
35
|
+
_default_fix = ("dict", "{}")
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
*,
|
|
40
|
+
encoder: type[json.JSONEncoder] | None = None,
|
|
41
|
+
decoder: type[json.JSONDecoder] | None = None,
|
|
42
|
+
**kwargs: Any,
|
|
43
|
+
):
|
|
44
|
+
if encoder and not callable(encoder):
|
|
45
|
+
raise ValueError("The encoder parameter must be a callable object.")
|
|
46
|
+
if decoder and not callable(decoder):
|
|
47
|
+
raise ValueError("The decoder parameter must be a callable object.")
|
|
48
|
+
self.encoder = encoder
|
|
49
|
+
self.decoder = decoder
|
|
50
|
+
super().__init__(**kwargs)
|
|
51
|
+
|
|
52
|
+
def _check_default(self) -> list[PreflightResult]:
|
|
53
|
+
if (
|
|
54
|
+
self.has_default()
|
|
55
|
+
and self.default is not None
|
|
56
|
+
and not callable(self.default)
|
|
57
|
+
):
|
|
58
|
+
return [
|
|
59
|
+
preflight.PreflightResult(
|
|
60
|
+
fix=(
|
|
61
|
+
f"{self.__class__.__name__} default should be a callable instead of an instance "
|
|
62
|
+
"so that it's not shared between all field instances. "
|
|
63
|
+
"Use a callable instead, e.g., use `{}` instead of "
|
|
64
|
+
"`{}`.".format(*self._default_fix)
|
|
65
|
+
),
|
|
66
|
+
obj=self,
|
|
67
|
+
id="fields.invalid_choice_mixin_default",
|
|
68
|
+
warning=True,
|
|
69
|
+
)
|
|
70
|
+
]
|
|
71
|
+
else:
|
|
72
|
+
return []
|
|
73
|
+
|
|
74
|
+
def preflight(self, **kwargs: Any) -> list[PreflightResult]:
|
|
75
|
+
errors = super().preflight(**kwargs)
|
|
76
|
+
errors.extend(self._check_default())
|
|
77
|
+
errors.extend(self._check_supported())
|
|
78
|
+
return errors
|
|
79
|
+
|
|
80
|
+
def _check_supported(self) -> list[PreflightResult]:
|
|
81
|
+
# PostgreSQL always supports JSONField (native JSONB type).
|
|
82
|
+
return []
|
|
83
|
+
|
|
84
|
+
def deconstruct(self) -> tuple[str | None, str, list[Any], dict[str, Any]]:
|
|
85
|
+
name, path, args, kwargs = super().deconstruct()
|
|
86
|
+
if self.encoder is not None:
|
|
87
|
+
kwargs["encoder"] = self.encoder
|
|
88
|
+
if self.decoder is not None:
|
|
89
|
+
kwargs["decoder"] = self.decoder
|
|
90
|
+
return name, path, args, kwargs
|
|
91
|
+
|
|
92
|
+
def from_db_value(
|
|
93
|
+
self, value: Any, expression: Any, connection: DatabaseConnection
|
|
94
|
+
) -> Any:
|
|
95
|
+
if value is None:
|
|
96
|
+
return value
|
|
97
|
+
# KeyTransform may extract non-string values directly.
|
|
98
|
+
if isinstance(expression, KeyTransform) and not isinstance(value, str):
|
|
99
|
+
return value
|
|
100
|
+
try:
|
|
101
|
+
return json.loads(value, cls=self.decoder)
|
|
102
|
+
except json.JSONDecodeError:
|
|
103
|
+
return value
|
|
104
|
+
|
|
105
|
+
def get_internal_type(self) -> str:
|
|
106
|
+
return "JSONField"
|
|
107
|
+
|
|
108
|
+
def get_db_prep_value(
|
|
109
|
+
self, value: Any, connection: DatabaseConnection, prepared: bool = False
|
|
110
|
+
) -> Any:
|
|
111
|
+
if isinstance(value, expressions.Value) and isinstance(
|
|
112
|
+
value.output_field, JSONField
|
|
113
|
+
):
|
|
114
|
+
value = value.value
|
|
115
|
+
elif hasattr(value, "as_sql"):
|
|
116
|
+
return value
|
|
117
|
+
return adapt_json_value(value, self.encoder)
|
|
118
|
+
|
|
119
|
+
def get_db_prep_save(self, value: Any, connection: DatabaseConnection) -> Any:
|
|
120
|
+
if value is None:
|
|
121
|
+
return value
|
|
122
|
+
return self.get_db_prep_value(value, connection)
|
|
123
|
+
|
|
124
|
+
def get_transform(
|
|
125
|
+
self, lookup_name: str
|
|
126
|
+
) -> type[Transform] | Callable[..., Any] | None:
|
|
127
|
+
# Always returns a transform (never None in practice)
|
|
128
|
+
transform = super().get_transform(lookup_name)
|
|
129
|
+
if transform:
|
|
130
|
+
return transform
|
|
131
|
+
return KeyTransformFactory(lookup_name)
|
|
132
|
+
|
|
133
|
+
def validate(self, value: Any, model_instance: Any) -> None:
|
|
134
|
+
super().validate(value, model_instance)
|
|
135
|
+
try:
|
|
136
|
+
json.dumps(value, cls=self.encoder)
|
|
137
|
+
except TypeError:
|
|
138
|
+
raise exceptions.ValidationError(
|
|
139
|
+
self.error_messages["invalid"],
|
|
140
|
+
code="invalid",
|
|
141
|
+
params={"value": value},
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def value_to_string(self, obj: Any) -> Any:
|
|
145
|
+
return self.value_from_object(obj)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class DataContains(FieldGetDbPrepValueMixin, OperatorLookup):
|
|
149
|
+
lookup_name = "contains"
|
|
150
|
+
# PostgreSQL @> operator checks if left JSON contains right JSON.
|
|
151
|
+
operator = "@>"
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class ContainedBy(FieldGetDbPrepValueMixin, OperatorLookup):
|
|
155
|
+
lookup_name = "contained_by"
|
|
156
|
+
# PostgreSQL <@ operator checks if left JSON is contained by right JSON.
|
|
157
|
+
operator = "<@"
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class HasKeyLookup(OperatorLookup):
|
|
161
|
+
"""Lookup for checking if a JSON field has a key."""
|
|
162
|
+
|
|
163
|
+
logical_operator: str | None = None
|
|
164
|
+
|
|
165
|
+
def as_sql(
|
|
166
|
+
self, compiler: SQLCompiler, connection: DatabaseConnection
|
|
167
|
+
) -> tuple[str, tuple[Any, ...]]:
|
|
168
|
+
# Handle KeyTransform on RHS by expanding it into LHS chain.
|
|
169
|
+
if isinstance(self.rhs, KeyTransform):
|
|
170
|
+
*_, rhs_key_transforms = self.rhs.preprocess_lhs(compiler, connection)
|
|
171
|
+
for key in rhs_key_transforms[:-1]:
|
|
172
|
+
self.lhs = KeyTransform(key, self.lhs)
|
|
173
|
+
self.rhs = rhs_key_transforms[-1]
|
|
174
|
+
return super().as_sql(compiler, connection)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class HasKey(HasKeyLookup):
|
|
178
|
+
lookup_name = "has_key"
|
|
179
|
+
# PostgreSQL ? operator checks if key exists.
|
|
180
|
+
operator = "?"
|
|
181
|
+
prepare_rhs = False
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class HasKeys(HasKeyLookup):
|
|
185
|
+
lookup_name = "has_keys"
|
|
186
|
+
# PostgreSQL ?& operator checks if all keys exist.
|
|
187
|
+
operator = "?&"
|
|
188
|
+
logical_operator = " AND "
|
|
189
|
+
|
|
190
|
+
def get_prep_lookup(self) -> list[str]:
|
|
191
|
+
return [str(item) for item in self.rhs]
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class HasAnyKeys(HasKeys):
|
|
195
|
+
lookup_name = "has_any_keys"
|
|
196
|
+
# PostgreSQL ?| operator checks if any key exists.
|
|
197
|
+
operator = "?|"
|
|
198
|
+
logical_operator = " OR "
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class JSONExact(lookups.Exact):
|
|
202
|
+
can_use_none_as_rhs = True
|
|
203
|
+
|
|
204
|
+
def process_rhs(
|
|
205
|
+
self, compiler: SQLCompiler, connection: DatabaseConnection
|
|
206
|
+
) -> tuple[str, list[Any]] | tuple[list[str], list[Any]]:
|
|
207
|
+
rhs, rhs_params = super().process_rhs(compiler, connection)
|
|
208
|
+
if isinstance(rhs, str):
|
|
209
|
+
# Treat None lookup values as null.
|
|
210
|
+
if rhs == "%s" and rhs_params == [None]:
|
|
211
|
+
rhs_params = ["null"]
|
|
212
|
+
return rhs, rhs_params
|
|
213
|
+
else:
|
|
214
|
+
return rhs, rhs_params
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class JSONIContains(lookups.IContains):
|
|
218
|
+
pass
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
JSONField.register_lookup(DataContains)
|
|
222
|
+
JSONField.register_lookup(ContainedBy)
|
|
223
|
+
JSONField.register_lookup(HasKey)
|
|
224
|
+
JSONField.register_lookup(HasKeys)
|
|
225
|
+
JSONField.register_lookup(HasAnyKeys)
|
|
226
|
+
JSONField.register_lookup(JSONExact)
|
|
227
|
+
JSONField.register_lookup(JSONIContains)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class KeyTransform(Transform):
|
|
231
|
+
# PostgreSQL -> operator extracts JSON object field as JSON.
|
|
232
|
+
operator = "->"
|
|
233
|
+
# PostgreSQL #> operator extracts nested JSON path as JSON.
|
|
234
|
+
nested_operator = "#>"
|
|
235
|
+
|
|
236
|
+
def __init__(self, key_name: str, *args: Any, **kwargs: Any):
|
|
237
|
+
super().__init__(*args, **kwargs)
|
|
238
|
+
self.key_name = str(key_name)
|
|
239
|
+
|
|
240
|
+
def preprocess_lhs(
|
|
241
|
+
self, compiler: SQLCompiler, connection: DatabaseConnection
|
|
242
|
+
) -> tuple[str, tuple[Any, ...], list[str]]:
|
|
243
|
+
key_transforms = [self.key_name]
|
|
244
|
+
previous = self.lhs
|
|
245
|
+
while isinstance(previous, KeyTransform):
|
|
246
|
+
key_transforms.insert(0, previous.key_name)
|
|
247
|
+
previous = previous.lhs
|
|
248
|
+
lhs, params = compiler.compile(previous)
|
|
249
|
+
return lhs, params, key_transforms
|
|
250
|
+
|
|
251
|
+
def as_sql(
|
|
252
|
+
self,
|
|
253
|
+
compiler: SQLCompiler,
|
|
254
|
+
connection: DatabaseConnection,
|
|
255
|
+
function: str | None = None,
|
|
256
|
+
template: str | None = None,
|
|
257
|
+
arg_joiner: str | None = None,
|
|
258
|
+
**extra_context: Any,
|
|
259
|
+
) -> tuple[str, list[Any]]:
|
|
260
|
+
lhs, params, key_transforms = self.preprocess_lhs(compiler, connection)
|
|
261
|
+
if len(key_transforms) > 1:
|
|
262
|
+
sql = f"({lhs} {self.nested_operator} %s)"
|
|
263
|
+
return sql, list(params) + [key_transforms]
|
|
264
|
+
try:
|
|
265
|
+
lookup = int(self.key_name)
|
|
266
|
+
except ValueError:
|
|
267
|
+
lookup = self.key_name
|
|
268
|
+
return f"({lhs} {self.operator} %s)", list(params) + [lookup]
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class KeyTextTransform(KeyTransform):
|
|
272
|
+
# PostgreSQL ->> operator extracts JSON object field as text.
|
|
273
|
+
operator = "->>"
|
|
274
|
+
# PostgreSQL #>> operator extracts nested JSON path as text.
|
|
275
|
+
nested_operator = "#>>"
|
|
276
|
+
output_field = TextField()
|
|
277
|
+
|
|
278
|
+
@classmethod
|
|
279
|
+
def from_lookup(cls, lookup: str) -> Any:
|
|
280
|
+
transform, *keys = lookup.split(LOOKUP_SEP)
|
|
281
|
+
if not keys:
|
|
282
|
+
raise ValueError("Lookup must contain key or index transforms.")
|
|
283
|
+
for key in keys:
|
|
284
|
+
transform = cls(key, transform)
|
|
285
|
+
return transform
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
KT = KeyTextTransform.from_lookup
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class KeyTransformTextLookupMixin(Lookup):
|
|
292
|
+
"""
|
|
293
|
+
Mixin for lookups expecting text LHS from a JSONField key lookup.
|
|
294
|
+
Uses the ->> operator to extract JSON values as text.
|
|
295
|
+
"""
|
|
296
|
+
|
|
297
|
+
def __init__(self, key_transform: Any, *args: Any, **kwargs: Any):
|
|
298
|
+
if not isinstance(key_transform, KeyTransform):
|
|
299
|
+
raise TypeError(
|
|
300
|
+
"Transform should be an instance of KeyTransform in order to "
|
|
301
|
+
"use this lookup."
|
|
302
|
+
)
|
|
303
|
+
key_text_transform = KeyTextTransform(
|
|
304
|
+
key_transform.key_name,
|
|
305
|
+
*key_transform.source_expressions,
|
|
306
|
+
**key_transform.extra,
|
|
307
|
+
)
|
|
308
|
+
super().__init__(key_text_transform, *args, **kwargs)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
class KeyTransformIsNull(lookups.IsNull):
|
|
312
|
+
# key__isnull=False is the same as has_key='key'
|
|
313
|
+
pass
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class KeyTransformIn(lookups.In):
|
|
317
|
+
def resolve_expression_parameter(
|
|
318
|
+
self,
|
|
319
|
+
compiler: SQLCompiler,
|
|
320
|
+
connection: DatabaseConnection,
|
|
321
|
+
sql: str,
|
|
322
|
+
param: Any,
|
|
323
|
+
) -> tuple[str, list[Any]]:
|
|
324
|
+
sql, params = super().resolve_expression_parameter(
|
|
325
|
+
compiler,
|
|
326
|
+
connection,
|
|
327
|
+
sql,
|
|
328
|
+
param,
|
|
329
|
+
)
|
|
330
|
+
return sql, list(params)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class KeyTransformExact(JSONExact):
|
|
334
|
+
def process_rhs(
|
|
335
|
+
self, compiler: SQLCompiler, connection: DatabaseConnection
|
|
336
|
+
) -> tuple[str, list[Any]] | tuple[list[str], list[Any]]:
|
|
337
|
+
if isinstance(self.rhs, KeyTransform):
|
|
338
|
+
return super(lookups.Exact, self).process_rhs(compiler, connection)
|
|
339
|
+
return super().process_rhs(compiler, connection)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
class KeyTransformIExact(KeyTransformTextLookupMixin, lookups.IExact):
|
|
343
|
+
pass
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
class KeyTransformIContains(KeyTransformTextLookupMixin, lookups.IContains):
|
|
347
|
+
pass
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
class KeyTransformStartsWith(KeyTransformTextLookupMixin, lookups.StartsWith):
|
|
351
|
+
pass
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
class KeyTransformIStartsWith(KeyTransformTextLookupMixin, lookups.IStartsWith):
|
|
355
|
+
pass
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
class KeyTransformEndsWith(KeyTransformTextLookupMixin, lookups.EndsWith):
|
|
359
|
+
pass
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
class KeyTransformIEndsWith(KeyTransformTextLookupMixin, lookups.IEndsWith):
|
|
363
|
+
pass
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
class KeyTransformRegex(KeyTransformTextLookupMixin, lookups.Regex):
|
|
367
|
+
pass
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
class KeyTransformIRegex(KeyTransformTextLookupMixin, lookups.IRegex):
|
|
371
|
+
pass
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
class KeyTransformLt(lookups.LessThan):
|
|
375
|
+
pass
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
class KeyTransformLte(lookups.LessThanOrEqual):
|
|
379
|
+
pass
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
class KeyTransformGt(lookups.GreaterThan):
|
|
383
|
+
pass
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
class KeyTransformGte(lookups.GreaterThanOrEqual):
|
|
387
|
+
pass
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
KeyTransform.register_lookup(KeyTransformIn)
|
|
391
|
+
KeyTransform.register_lookup(KeyTransformExact)
|
|
392
|
+
KeyTransform.register_lookup(KeyTransformIExact)
|
|
393
|
+
KeyTransform.register_lookup(KeyTransformIsNull)
|
|
394
|
+
KeyTransform.register_lookup(KeyTransformIContains)
|
|
395
|
+
KeyTransform.register_lookup(KeyTransformStartsWith)
|
|
396
|
+
KeyTransform.register_lookup(KeyTransformIStartsWith)
|
|
397
|
+
KeyTransform.register_lookup(KeyTransformEndsWith)
|
|
398
|
+
KeyTransform.register_lookup(KeyTransformIEndsWith)
|
|
399
|
+
KeyTransform.register_lookup(KeyTransformRegex)
|
|
400
|
+
KeyTransform.register_lookup(KeyTransformIRegex)
|
|
401
|
+
|
|
402
|
+
KeyTransform.register_lookup(KeyTransformLt)
|
|
403
|
+
KeyTransform.register_lookup(KeyTransformLte)
|
|
404
|
+
KeyTransform.register_lookup(KeyTransformGt)
|
|
405
|
+
KeyTransform.register_lookup(KeyTransformGte)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
class KeyTransformFactory:
|
|
409
|
+
def __init__(self, key_name: str):
|
|
410
|
+
self.key_name = key_name
|
|
411
|
+
|
|
412
|
+
def __call__(self, *args: Any, **kwargs: Any) -> KeyTransform:
|
|
413
|
+
return KeyTransform(self.key_name, *args, **kwargs)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
NOT_PROVIDED = object()
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FieldCacheMixin:
|
|
9
|
+
"""Provide an API for working with the model's fields value cache."""
|
|
10
|
+
|
|
11
|
+
def get_cache_name(self) -> str:
|
|
12
|
+
raise NotImplementedError
|
|
13
|
+
|
|
14
|
+
def get_cached_value(self, instance: Any, default: Any = NOT_PROVIDED) -> Any:
|
|
15
|
+
cache_name = self.get_cache_name()
|
|
16
|
+
try:
|
|
17
|
+
return instance._state.fields_cache[cache_name]
|
|
18
|
+
except KeyError:
|
|
19
|
+
if default is NOT_PROVIDED:
|
|
20
|
+
raise
|
|
21
|
+
return default
|
|
22
|
+
|
|
23
|
+
def is_cached(self, instance: Any) -> bool:
|
|
24
|
+
return self.get_cache_name() in instance._state.fields_cache
|
|
25
|
+
|
|
26
|
+
def set_cached_value(self, instance: Any, value: Any) -> None:
|
|
27
|
+
instance._state.fields_cache[self.get_cache_name()] = value
|
|
28
|
+
|
|
29
|
+
def delete_cached_value(self, instance: Any) -> None:
|
|
30
|
+
del instance._state.fields_cache[self.get_cache_name()]
|