plain.models 0.49.2__py3-none-any.whl → 0.51.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/models/CHANGELOG.md +27 -0
- plain/models/README.md +26 -42
- plain/models/__init__.py +2 -0
- plain/models/aggregates.py +42 -19
- plain/models/backends/base/base.py +125 -105
- plain/models/backends/base/client.py +11 -3
- plain/models/backends/base/creation.py +24 -14
- plain/models/backends/base/features.py +10 -4
- plain/models/backends/base/introspection.py +37 -20
- plain/models/backends/base/operations.py +187 -91
- plain/models/backends/base/schema.py +338 -218
- plain/models/backends/base/validation.py +13 -4
- plain/models/backends/ddl_references.py +85 -43
- plain/models/backends/mysql/base.py +29 -26
- plain/models/backends/mysql/client.py +7 -2
- plain/models/backends/mysql/compiler.py +13 -4
- plain/models/backends/mysql/creation.py +5 -2
- plain/models/backends/mysql/features.py +24 -22
- plain/models/backends/mysql/introspection.py +22 -13
- plain/models/backends/mysql/operations.py +107 -40
- plain/models/backends/mysql/schema.py +52 -28
- plain/models/backends/mysql/validation.py +13 -6
- plain/models/backends/postgresql/base.py +41 -34
- plain/models/backends/postgresql/client.py +7 -2
- plain/models/backends/postgresql/creation.py +10 -5
- plain/models/backends/postgresql/introspection.py +15 -8
- plain/models/backends/postgresql/operations.py +110 -43
- plain/models/backends/postgresql/schema.py +88 -49
- plain/models/backends/sqlite3/_functions.py +151 -115
- plain/models/backends/sqlite3/base.py +37 -23
- plain/models/backends/sqlite3/client.py +7 -1
- plain/models/backends/sqlite3/creation.py +9 -5
- plain/models/backends/sqlite3/features.py +5 -3
- plain/models/backends/sqlite3/introspection.py +32 -16
- plain/models/backends/sqlite3/operations.py +126 -43
- plain/models/backends/sqlite3/schema.py +127 -92
- plain/models/backends/utils.py +52 -29
- plain/models/backups/cli.py +8 -6
- plain/models/backups/clients.py +16 -7
- plain/models/backups/core.py +24 -13
- plain/models/base.py +221 -229
- plain/models/cli.py +98 -67
- plain/models/config.py +1 -1
- plain/models/connections.py +23 -7
- plain/models/constraints.py +79 -56
- plain/models/database_url.py +1 -1
- plain/models/db.py +6 -2
- plain/models/deletion.py +80 -56
- plain/models/entrypoints.py +1 -1
- plain/models/enums.py +22 -11
- plain/models/exceptions.py +23 -8
- plain/models/expressions.py +441 -258
- plain/models/fields/__init__.py +272 -217
- plain/models/fields/json.py +123 -57
- plain/models/fields/mixins.py +12 -8
- plain/models/fields/related.py +324 -290
- plain/models/fields/related_descriptors.py +33 -24
- plain/models/fields/related_lookups.py +24 -12
- plain/models/fields/related_managers.py +102 -79
- plain/models/fields/reverse_related.py +66 -63
- plain/models/forms.py +101 -75
- plain/models/functions/comparison.py +71 -18
- plain/models/functions/datetime.py +79 -29
- plain/models/functions/math.py +43 -10
- plain/models/functions/mixins.py +24 -7
- plain/models/functions/text.py +104 -25
- plain/models/functions/window.py +12 -6
- plain/models/indexes.py +57 -32
- plain/models/lookups.py +228 -153
- plain/models/meta.py +505 -0
- plain/models/migrations/autodetector.py +86 -43
- plain/models/migrations/exceptions.py +7 -3
- plain/models/migrations/executor.py +33 -7
- plain/models/migrations/graph.py +79 -50
- plain/models/migrations/loader.py +45 -22
- plain/models/migrations/migration.py +23 -18
- plain/models/migrations/operations/base.py +38 -20
- plain/models/migrations/operations/fields.py +95 -48
- plain/models/migrations/operations/models.py +246 -142
- plain/models/migrations/operations/special.py +82 -25
- plain/models/migrations/optimizer.py +7 -2
- plain/models/migrations/questioner.py +58 -31
- plain/models/migrations/recorder.py +27 -16
- plain/models/migrations/serializer.py +50 -39
- plain/models/migrations/state.py +232 -156
- plain/models/migrations/utils.py +30 -14
- plain/models/migrations/writer.py +17 -14
- plain/models/options.py +189 -518
- plain/models/otel.py +16 -6
- plain/models/preflight.py +42 -17
- plain/models/query.py +400 -251
- plain/models/query_utils.py +109 -69
- plain/models/registry.py +40 -21
- plain/models/sql/compiler.py +190 -127
- plain/models/sql/datastructures.py +38 -25
- plain/models/sql/query.py +320 -225
- plain/models/sql/subqueries.py +36 -25
- plain/models/sql/where.py +54 -29
- plain/models/test/pytest.py +15 -11
- plain/models/test/utils.py +4 -2
- plain/models/transaction.py +20 -7
- plain/models/utils.py +17 -6
- {plain_models-0.49.2.dist-info → plain_models-0.51.0.dist-info}/METADATA +27 -43
- plain_models-0.51.0.dist-info/RECORD +123 -0
- plain_models-0.49.2.dist-info/RECORD +0 -122
- {plain_models-0.49.2.dist-info → plain_models-0.51.0.dist-info}/WHEEL +0 -0
- {plain_models-0.49.2.dist-info → plain_models-0.51.0.dist-info}/entry_points.txt +0 -0
- {plain_models-0.49.2.dist-info → plain_models-0.51.0.dist-info}/licenses/LICENSE +0 -0
plain/models/CHANGELOG.md
CHANGED
@@ -1,5 +1,32 @@
|
|
1
1
|
# plain-models changelog
|
2
2
|
|
3
|
+
## [0.51.0](https://github.com/dropseed/plain/releases/plain-models@0.51.0) (2025-10-07)
|
4
|
+
|
5
|
+
### What's changed
|
6
|
+
|
7
|
+
- Model metadata has been split into two separate descriptors: `model_options` for user-defined configuration and `_model_meta` for internal metadata ([73ba469](https://github.com/dropseed/plain/commit/73ba469ba0), [17a378d](https://github.com/dropseed/plain/commit/17a378dcfb))
|
8
|
+
- The `_meta` attribute has been replaced with `model_options` for user-defined options like indexes, constraints, and database settings ([17a378d](https://github.com/dropseed/plain/commit/17a378dcfb))
|
9
|
+
- Custom QuerySets are now assigned directly to the `query` class attribute instead of using `Meta.queryset_class` ([2578301](https://github.com/dropseed/plain/commit/2578301819))
|
10
|
+
- Added comprehensive type improvements to model metadata and related fields for better IDE support ([3b477a0](https://github.com/dropseed/plain/commit/3b477a0d43))
|
11
|
+
|
12
|
+
### Upgrade instructions
|
13
|
+
|
14
|
+
- Replace `Meta.queryset_class = CustomQuerySet` with `query = CustomQuerySet()` as a class attribute on your models
|
15
|
+
- Replace `class Meta:` with `model_options = models.Options(...)` in your models
|
16
|
+
|
17
|
+
## [0.50.0](https://github.com/dropseed/plain/releases/plain-models@0.50.0) (2025-10-06)
|
18
|
+
|
19
|
+
### What's changed
|
20
|
+
|
21
|
+
- Added comprehensive type annotations throughout plain-models, improving IDE support and type checking capabilities ([ea1a7df](https://github.com/dropseed/plain/commit/ea1a7df622), [f49ee32](https://github.com/dropseed/plain/commit/f49ee32a90), [369353f](https://github.com/dropseed/plain/commit/369353f9d6), [13b7d16](https://github.com/dropseed/plain/commit/13b7d16f8d), [e23a0ca](https://github.com/dropseed/plain/commit/e23a0cae7c), [02d8551](https://github.com/dropseed/plain/commit/02d85518f0))
|
22
|
+
- The `QuerySet` class is now generic and the `model` parameter is now required in the `__init__` method ([719e792](https://github.com/dropseed/plain/commit/719e792c96))
|
23
|
+
- Database wrapper classes have been renamed for consistency: `DatabaseWrapper` classes are now named `MySQLDatabaseWrapper`, `PostgreSQLDatabaseWrapper`, and `SQLiteDatabaseWrapper` ([5a39e85](https://github.com/dropseed/plain/commit/5a39e851e5))
|
24
|
+
- The plain-models package now has 100% type annotation coverage and is validated in CI to prevent regressions
|
25
|
+
|
26
|
+
### Upgrade instructions
|
27
|
+
|
28
|
+
- No changes required
|
29
|
+
|
3
30
|
## [0.49.2](https://github.com/dropseed/plain/releases/plain-models@0.49.2) (2025-10-02)
|
4
31
|
|
5
32
|
### What's changed
|
plain/models/README.md
CHANGED
@@ -200,31 +200,32 @@ class User(models.Model):
|
|
200
200
|
username = models.CharField(max_length=150)
|
201
201
|
age = models.IntegerField()
|
202
202
|
|
203
|
-
|
204
|
-
indexes
|
203
|
+
model_options = models.Options(
|
204
|
+
indexes=[
|
205
205
|
models.Index(fields=["email"]),
|
206
206
|
models.Index(fields=["-created_at"], name="user_created_idx"),
|
207
|
-
]
|
208
|
-
constraints
|
207
|
+
],
|
208
|
+
constraints=[
|
209
209
|
models.UniqueConstraint(fields=["email", "username"], name="unique_user"),
|
210
210
|
models.CheckConstraint(check=models.Q(age__gte=0), name="age_positive"),
|
211
|
-
]
|
211
|
+
],
|
212
|
+
)
|
212
213
|
```
|
213
214
|
|
214
215
|
## Custom QuerySets
|
215
216
|
|
216
|
-
With the Manager functionality now merged into QuerySet, you can customize [`QuerySet`](./query.py#QuerySet) classes to provide specialized query methods.
|
217
|
-
|
218
|
-
### Setting a default QuerySet for a model
|
217
|
+
With the Manager functionality now merged into QuerySet, you can customize [`QuerySet`](./query.py#QuerySet) classes to provide specialized query methods.
|
219
218
|
|
220
|
-
|
219
|
+
Define a custom QuerySet and assign it to your model's `query` attribute:
|
221
220
|
|
222
221
|
```python
|
223
|
-
|
224
|
-
|
222
|
+
from typing import Self
|
223
|
+
|
224
|
+
class PublishedQuerySet(models.QuerySet["Article"]):
|
225
|
+
def published_only(self) -> Self:
|
225
226
|
return self.filter(status="published")
|
226
227
|
|
227
|
-
def draft_only(self):
|
228
|
+
def draft_only(self) -> Self:
|
228
229
|
return self.filter(status="draft")
|
229
230
|
|
230
231
|
@models.register_model
|
@@ -232,50 +233,33 @@ class Article(models.Model):
|
|
232
233
|
title = models.CharField(max_length=200)
|
233
234
|
status = models.CharField(max_length=20)
|
234
235
|
|
235
|
-
|
236
|
-
queryset_class = PublishedQuerySet
|
236
|
+
query = PublishedQuerySet()
|
237
237
|
|
238
|
-
# Usage - all methods available on Article.
|
238
|
+
# Usage - all methods available on Article.query
|
239
239
|
all_articles = Article.query.all()
|
240
240
|
published_articles = Article.query.published_only()
|
241
241
|
draft_articles = Article.query.draft_only()
|
242
242
|
```
|
243
243
|
|
244
|
-
|
245
|
-
|
246
|
-
You can also use custom QuerySets manually without setting them as the default:
|
244
|
+
Custom methods can be chained with built-in QuerySet methods:
|
247
245
|
|
248
246
|
```python
|
249
|
-
|
250
|
-
|
251
|
-
return self.filter(special=True)
|
252
|
-
|
253
|
-
# Create and use the QuerySet manually
|
254
|
-
special_qs = SpecialQuerySet(model=Article)
|
255
|
-
special_articles = special_qs.special_filter()
|
247
|
+
# Chaining works naturally
|
248
|
+
recent_published = Article.query.published_only().order_by("-created_at")[:10]
|
256
249
|
```
|
257
250
|
|
258
|
-
###
|
251
|
+
### Programmatic QuerySet usage
|
259
252
|
|
260
|
-
For
|
253
|
+
For internal code that needs to create QuerySet instances programmatically, use `from_model()`:
|
261
254
|
|
262
255
|
```python
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
status = models.CharField(max_length=20)
|
267
|
-
|
268
|
-
@classmethod
|
269
|
-
def published(cls):
|
270
|
-
return PublishedQuerySet(model=cls).published_only()
|
271
|
-
|
272
|
-
@classmethod
|
273
|
-
def drafts(cls):
|
274
|
-
return PublishedQuerySet(model=cls).draft_only()
|
256
|
+
class SpecialQuerySet(models.QuerySet["Article"]):
|
257
|
+
def special_filter(self) -> Self:
|
258
|
+
return self.filter(special=True)
|
275
259
|
|
276
|
-
#
|
277
|
-
|
278
|
-
|
260
|
+
# Create and use the QuerySet programmatically
|
261
|
+
special_qs = SpecialQuerySet.from_model(Article)
|
262
|
+
special_articles = special_qs.special_filter()
|
279
263
|
```
|
280
264
|
|
281
265
|
## Forms
|
plain/models/__init__.py
CHANGED
@@ -63,6 +63,7 @@ from .registry import models_registry, register_model
|
|
63
63
|
|
64
64
|
# Imports that would create circular imports if sorted
|
65
65
|
from .base import DEFERRED, Model # isort:skip
|
66
|
+
from .options import Options # isort:skip
|
66
67
|
from .fields.related import ( # isort:skip
|
67
68
|
ForeignKey,
|
68
69
|
ManyToManyField,
|
@@ -104,6 +105,7 @@ __all__ += [
|
|
104
105
|
"JSONField",
|
105
106
|
"Lookup",
|
106
107
|
"Transform",
|
108
|
+
"Options",
|
107
109
|
"Prefetch",
|
108
110
|
"Q",
|
109
111
|
"QuerySet",
|
plain/models/aggregates.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import TYPE_CHECKING, Any
|
4
4
|
|
5
5
|
from plain.models.exceptions import FieldError, FullResultSet
|
6
6
|
from plain.models.expressions import Case, Func, Star, Value, When
|
@@ -11,6 +11,12 @@ from plain.models.functions.mixins import (
|
|
11
11
|
NumericOutputFieldMixin,
|
12
12
|
)
|
13
13
|
|
14
|
+
if TYPE_CHECKING:
|
15
|
+
from plain.models.backends.base.base import BaseDatabaseWrapper
|
16
|
+
from plain.models.expressions import Expression
|
17
|
+
from plain.models.query_utils import Q
|
18
|
+
from plain.models.sql.compiler import SQLCompiler
|
19
|
+
|
14
20
|
__all__ = [
|
15
21
|
"Aggregate",
|
16
22
|
"Avg",
|
@@ -33,8 +39,13 @@ class Aggregate(Func):
|
|
33
39
|
empty_result_set_value = None
|
34
40
|
|
35
41
|
def __init__(
|
36
|
-
self,
|
37
|
-
|
42
|
+
self,
|
43
|
+
*expressions: Any,
|
44
|
+
distinct: bool = False,
|
45
|
+
filter: Q | Expression | None = None,
|
46
|
+
default: Any = None,
|
47
|
+
**extra: Any,
|
48
|
+
) -> None:
|
38
49
|
if distinct and not self.allow_distinct:
|
39
50
|
raise TypeError(f"{self.__class__.__name__} does not allow distinct.")
|
40
51
|
if default is not None and self.empty_result_set_value is not None:
|
@@ -44,23 +55,28 @@ class Aggregate(Func):
|
|
44
55
|
self.default = default
|
45
56
|
super().__init__(*expressions, **extra)
|
46
57
|
|
47
|
-
def get_source_fields(self):
|
58
|
+
def get_source_fields(self) -> list[Any]:
|
48
59
|
# Don't return the filter expression since it's not a source field.
|
49
60
|
return [e._output_field_or_none for e in super().get_source_expressions()]
|
50
61
|
|
51
|
-
def get_source_expressions(self):
|
62
|
+
def get_source_expressions(self) -> list[Expression]:
|
52
63
|
source_expressions = super().get_source_expressions()
|
53
64
|
if self.filter:
|
54
65
|
return source_expressions + [self.filter]
|
55
66
|
return source_expressions
|
56
67
|
|
57
|
-
def set_source_expressions(self, exprs):
|
68
|
+
def set_source_expressions(self, exprs: list[Expression]) -> list[Expression]:
|
58
69
|
self.filter = self.filter and exprs.pop()
|
59
70
|
return super().set_source_expressions(exprs)
|
60
71
|
|
61
72
|
def resolve_expression(
|
62
|
-
self,
|
63
|
-
|
73
|
+
self,
|
74
|
+
query: Any = None,
|
75
|
+
allow_joins: bool = True,
|
76
|
+
reuse: Any = None,
|
77
|
+
summarize: bool = False,
|
78
|
+
for_save: bool = False,
|
79
|
+
) -> Expression:
|
64
80
|
# Aggregates are not allowed in UPDATE queries, so ignore for_save
|
65
81
|
c = super().resolve_expression(query, allow_joins, reuse, summarize)
|
66
82
|
c.filter = c.filter and c.filter.resolve_expression(
|
@@ -95,16 +111,21 @@ class Aggregate(Func):
|
|
95
111
|
return coalesce
|
96
112
|
|
97
113
|
@property
|
98
|
-
def default_alias(self):
|
114
|
+
def default_alias(self) -> str:
|
99
115
|
expressions = self.get_source_expressions()
|
100
116
|
if len(expressions) == 1 and hasattr(expressions[0], "name"):
|
101
117
|
return f"{expressions[0].name}__{self.name.lower()}"
|
102
118
|
raise TypeError("Complex expressions require an alias")
|
103
119
|
|
104
|
-
def get_group_by_cols(self):
|
120
|
+
def get_group_by_cols(self) -> list[Any]:
|
105
121
|
return []
|
106
122
|
|
107
|
-
def as_sql(
|
123
|
+
def as_sql(
|
124
|
+
self,
|
125
|
+
compiler: SQLCompiler,
|
126
|
+
connection: BaseDatabaseWrapper,
|
127
|
+
**extra_context: Any,
|
128
|
+
) -> tuple[str, tuple[Any, ...]]:
|
108
129
|
extra_context["distinct"] = "DISTINCT " if self.distinct else ""
|
109
130
|
if self.filter:
|
110
131
|
if connection.features.supports_aggregate_filter_clause:
|
@@ -135,7 +156,7 @@ class Aggregate(Func):
|
|
135
156
|
)
|
136
157
|
return super().as_sql(compiler, connection, **extra_context)
|
137
158
|
|
138
|
-
def _get_repr_options(self):
|
159
|
+
def _get_repr_options(self) -> dict[str, Any]:
|
139
160
|
options = super()._get_repr_options()
|
140
161
|
if self.distinct:
|
141
162
|
options["distinct"] = self.distinct
|
@@ -157,7 +178,9 @@ class Count(Aggregate):
|
|
157
178
|
allow_distinct = True
|
158
179
|
empty_result_set_value = 0
|
159
180
|
|
160
|
-
def __init__(
|
181
|
+
def __init__(
|
182
|
+
self, expression: Any, filter: Q | Expression | None = None, **extra: Any
|
183
|
+
) -> None:
|
161
184
|
if expression == "*":
|
162
185
|
expression = Star()
|
163
186
|
if isinstance(expression, Star) and filter is not None:
|
@@ -178,11 +201,11 @@ class Min(Aggregate):
|
|
178
201
|
class StdDev(NumericOutputFieldMixin, Aggregate):
|
179
202
|
name = "StdDev"
|
180
203
|
|
181
|
-
def __init__(self, expression, sample=False, **extra):
|
204
|
+
def __init__(self, expression: Any, sample: bool = False, **extra: Any) -> None:
|
182
205
|
self.function = "STDDEV_SAMP" if sample else "STDDEV_POP"
|
183
206
|
super().__init__(expression, **extra)
|
184
207
|
|
185
|
-
def _get_repr_options(self):
|
208
|
+
def _get_repr_options(self) -> dict[str, Any]:
|
186
209
|
return {**super()._get_repr_options(), "sample": self.function == "STDDEV_SAMP"}
|
187
210
|
|
188
211
|
|
@@ -195,9 +218,9 @@ class Sum(FixDurationInputMixin, Aggregate):
|
|
195
218
|
class Variance(NumericOutputFieldMixin, Aggregate):
|
196
219
|
name = "Variance"
|
197
220
|
|
198
|
-
def __init__(self, expression, sample=False, **extra):
|
221
|
+
def __init__(self, expression: Any, sample: bool = False, **extra: Any) -> None:
|
199
222
|
self.function = "VAR_SAMP" if sample else "VAR_POP"
|
200
223
|
super().__init__(expression, **extra)
|
201
224
|
|
202
|
-
def _get_repr_options(self):
|
225
|
+
def _get_repr_options(self) -> dict[str, Any]:
|
203
226
|
return {**super()._get_repr_options(), "sample": self.function == "VAR_SAMP"}
|