plain.models 0.39.1__tar.gz → 0.40.0__tar.gz

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.
Files changed (133) hide show
  1. plain_models-0.40.0/PKG-INFO +312 -0
  2. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/CHANGELOG.md +26 -0
  3. plain_models-0.40.0/plain/models/README.md +301 -0
  4. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/backends/base/creation.py +1 -1
  5. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/cli.py +19 -118
  6. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/fields/related_descriptors.py +9 -0
  7. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/forms.py +3 -4
  8. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/migrations/executor.py +13 -110
  9. {plain_models-0.39.1 → plain_models-0.40.0}/pyproject.toml +2 -2
  10. plain_models-0.39.1/PKG-INFO +0 -147
  11. plain_models-0.39.1/plain/models/README.md +0 -136
  12. {plain_models-0.39.1 → plain_models-0.40.0}/.gitignore +0 -0
  13. {plain_models-0.39.1 → plain_models-0.40.0}/LICENSE +0 -0
  14. {plain_models-0.39.1 → plain_models-0.40.0}/README.md +0 -0
  15. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/__init__.py +0 -0
  16. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/aggregates.py +0 -0
  17. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/backends/__init__.py +0 -0
  18. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/backends/base/__init__.py +0 -0
  19. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/backends/base/base.py +0 -0
  20. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/backends/base/client.py +0 -0
  21. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/backends/base/features.py +0 -0
  22. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/backends/base/introspection.py +0 -0
  23. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/backends/base/operations.py +0 -0
  24. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/backends/base/schema.py +0 -0
  25. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/backends/base/validation.py +0 -0
  26. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/backends/ddl_references.py +0 -0
  27. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/backends/mysql/__init__.py +0 -0
  28. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/backends/mysql/base.py +0 -0
  29. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/backends/mysql/client.py +0 -0
  30. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/backends/mysql/compiler.py +0 -0
  31. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/backends/mysql/creation.py +0 -0
  32. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/backends/mysql/features.py +0 -0
  33. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/backends/mysql/introspection.py +0 -0
  34. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/backends/mysql/operations.py +0 -0
  35. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/backends/mysql/schema.py +0 -0
  36. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/backends/mysql/validation.py +0 -0
  37. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/backends/postgresql/__init__.py +0 -0
  38. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/backends/postgresql/base.py +0 -0
  39. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/backends/postgresql/client.py +0 -0
  40. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/backends/postgresql/creation.py +0 -0
  41. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/backends/postgresql/features.py +0 -0
  42. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/backends/postgresql/introspection.py +0 -0
  43. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/backends/postgresql/operations.py +0 -0
  44. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/backends/postgresql/schema.py +0 -0
  45. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/backends/sqlite3/__init__.py +0 -0
  46. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/backends/sqlite3/_functions.py +0 -0
  47. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/backends/sqlite3/base.py +0 -0
  48. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/backends/sqlite3/client.py +0 -0
  49. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/backends/sqlite3/creation.py +0 -0
  50. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/backends/sqlite3/features.py +0 -0
  51. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/backends/sqlite3/introspection.py +0 -0
  52. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/backends/sqlite3/operations.py +0 -0
  53. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/backends/sqlite3/schema.py +0 -0
  54. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/backends/utils.py +0 -0
  55. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/backups/__init__.py +0 -0
  56. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/backups/cli.py +0 -0
  57. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/backups/clients.py +0 -0
  58. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/backups/core.py +0 -0
  59. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/base.py +0 -0
  60. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/config.py +0 -0
  61. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/connections.py +0 -0
  62. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/constants.py +0 -0
  63. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/constraints.py +0 -0
  64. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/database_url.py +0 -0
  65. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/db.py +0 -0
  66. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/default_settings.py +0 -0
  67. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/deletion.py +0 -0
  68. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/entrypoints.py +0 -0
  69. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/enums.py +0 -0
  70. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/exceptions.py +0 -0
  71. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/expressions.py +0 -0
  72. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/fields/__init__.py +0 -0
  73. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/fields/json.py +0 -0
  74. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/fields/mixins.py +0 -0
  75. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/fields/related.py +0 -0
  76. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/fields/related_lookups.py +0 -0
  77. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/fields/reverse_related.py +0 -0
  78. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/functions/__init__.py +0 -0
  79. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/functions/comparison.py +0 -0
  80. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/functions/datetime.py +0 -0
  81. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/functions/math.py +0 -0
  82. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/functions/mixins.py +0 -0
  83. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/functions/text.py +0 -0
  84. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/functions/window.py +0 -0
  85. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/indexes.py +0 -0
  86. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/lookups.py +0 -0
  87. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/manager.py +0 -0
  88. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/migrations/__init__.py +0 -0
  89. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/migrations/autodetector.py +0 -0
  90. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/migrations/exceptions.py +0 -0
  91. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/migrations/graph.py +0 -0
  92. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/migrations/loader.py +0 -0
  93. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/migrations/migration.py +0 -0
  94. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/migrations/operations/__init__.py +0 -0
  95. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/migrations/operations/base.py +0 -0
  96. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/migrations/operations/fields.py +0 -0
  97. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/migrations/operations/models.py +0 -0
  98. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/migrations/operations/special.py +0 -0
  99. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/migrations/optimizer.py +0 -0
  100. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/migrations/questioner.py +0 -0
  101. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/migrations/recorder.py +0 -0
  102. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/migrations/serializer.py +0 -0
  103. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/migrations/state.py +0 -0
  104. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/migrations/utils.py +0 -0
  105. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/migrations/writer.py +0 -0
  106. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/options.py +0 -0
  107. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/otel.py +0 -0
  108. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/preflight.py +0 -0
  109. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/query.py +0 -0
  110. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/query_utils.py +0 -0
  111. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/registry.py +0 -0
  112. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/sql/__init__.py +0 -0
  113. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/sql/compiler.py +0 -0
  114. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/sql/constants.py +0 -0
  115. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/sql/datastructures.py +0 -0
  116. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/sql/query.py +0 -0
  117. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/sql/subqueries.py +0 -0
  118. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/sql/where.py +0 -0
  119. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/test/__init__.py +0 -0
  120. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/test/pytest.py +0 -0
  121. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/test/utils.py +0 -0
  122. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/transaction.py +0 -0
  123. {plain_models-0.39.1 → plain_models-0.40.0}/plain/models/utils.py +0 -0
  124. {plain_models-0.39.1 → plain_models-0.40.0}/tests/app/examples/migrations/0001_initial.py +0 -0
  125. {plain_models-0.39.1 → plain_models-0.40.0}/tests/app/examples/migrations/0002_test_field_removed.py +0 -0
  126. {plain_models-0.39.1 → plain_models-0.40.0}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
  127. {plain_models-0.39.1 → plain_models-0.40.0}/tests/app/examples/migrations/__init__.py +0 -0
  128. {plain_models-0.39.1 → plain_models-0.40.0}/tests/app/examples/models.py +0 -0
  129. {plain_models-0.39.1 → plain_models-0.40.0}/tests/app/settings.py +0 -0
  130. {plain_models-0.39.1 → plain_models-0.40.0}/tests/app/urls.py +0 -0
  131. {plain_models-0.39.1 → plain_models-0.40.0}/tests/test_database_url.py +0 -0
  132. {plain_models-0.39.1 → plain_models-0.40.0}/tests/test_delete_behaviors.py +0 -0
  133. {plain_models-0.39.1 → plain_models-0.40.0}/tests/test_models.py +0 -0
@@ -0,0 +1,312 @@
1
+ Metadata-Version: 2.4
2
+ Name: plain.models
3
+ Version: 0.40.0
4
+ Summary: Model your data and store it in a database.
5
+ Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.11
8
+ Requires-Dist: plain<1.0.0
9
+ Requires-Dist: sqlparse>=0.3.1
10
+ Description-Content-Type: text/markdown
11
+
12
+ # plain.models
13
+
14
+ **Model your data and store it in a database.**
15
+
16
+ - [Overview](#overview)
17
+ - [Database connection](#database-connection)
18
+ - [Querying](#querying)
19
+ - [Migrations](#migrations)
20
+ - [Fields](#fields)
21
+ - [Validation](#validation)
22
+ - [Indexes and constraints](#indexes-and-constraints)
23
+ - [Managers](#managers)
24
+ - [Forms](#forms)
25
+ - [Sharing fields across models](#sharing-fields-across-models)
26
+ - [Installation](#installation)
27
+
28
+ ## Overview
29
+
30
+ ```python
31
+ # app/users/models.py
32
+ from plain import models
33
+ from plain.passwords.models import PasswordField
34
+
35
+
36
+ @models.register_model
37
+ class User(models.Model):
38
+ email = models.EmailField()
39
+ password = PasswordField()
40
+ is_admin = models.BooleanField(default=False)
41
+ created_at = models.DateTimeField(auto_now_add=True)
42
+
43
+ def __str__(self):
44
+ return self.email
45
+ ```
46
+
47
+ Every model automatically includes an `id` field which serves as the primary
48
+ key. The name `id` is reserved and can't be used for other fields.
49
+
50
+ Create, update, and delete instances of your models:
51
+
52
+ ```python
53
+ from .models import User
54
+
55
+
56
+ # Create a new user
57
+ user = User.objects.create(
58
+ email="test@example.com",
59
+ password="password",
60
+ )
61
+
62
+ # Update a user
63
+ user.email = "new@example.com"
64
+ user.save()
65
+
66
+ # Delete a user
67
+ user.delete()
68
+
69
+ # Query for users
70
+ admin_users = User.objects.filter(is_admin=True)
71
+ ```
72
+
73
+ ## Database connection
74
+
75
+ To connect to a database, you can provide a `DATABASE_URL` environment variable:
76
+
77
+ ```sh
78
+ DATABASE_URL=postgresql://user:password@localhost:5432/dbname
79
+ ```
80
+
81
+ Or you can manually define the `DATABASE` setting:
82
+
83
+ ```python
84
+ # app/settings.py
85
+ DATABASE = {
86
+ "ENGINE": "plain.models.backends.postgresql",
87
+ "NAME": "dbname",
88
+ "USER": "user",
89
+ "PASSWORD": "password",
90
+ "HOST": "localhost",
91
+ "PORT": "5432",
92
+ }
93
+ ```
94
+
95
+ Multiple backends are supported, including Postgres, MySQL, and SQLite.
96
+
97
+ ## Querying
98
+
99
+ Models come with a powerful query API through their [`Manager`](./manager.py#Manager) interface:
100
+
101
+ ```python
102
+ # Get all users
103
+ all_users = User.objects.all()
104
+
105
+ # Filter users
106
+ admin_users = User.objects.filter(is_admin=True)
107
+ recent_users = User.objects.filter(created_at__gte=datetime.now() - timedelta(days=7))
108
+
109
+ # Get a single user
110
+ user = User.objects.get(email="test@example.com")
111
+
112
+ # Complex queries with Q objects
113
+ from plain.models import Q
114
+ users = User.objects.filter(
115
+ Q(is_admin=True) | Q(email__endswith="@example.com")
116
+ )
117
+
118
+ # Ordering
119
+ users = User.objects.order_by("-created_at")
120
+
121
+ # Limiting results
122
+ first_10_users = User.objects.all()[:10]
123
+ ```
124
+
125
+ For more advanced querying options, see the [`QuerySet`](./query.py#QuerySet) class.
126
+
127
+ ## Migrations
128
+
129
+ Migrations track changes to your models and update the database schema accordingly:
130
+
131
+ ```bash
132
+ # Create migrations for model changes
133
+ plain makemigrations
134
+
135
+ # Apply migrations to the database
136
+ plain migrate
137
+
138
+ # See migration status
139
+ plain models show-migrations
140
+ ```
141
+
142
+ Migrations are Python files that describe database schema changes. They're stored in your app's `migrations/` directory.
143
+
144
+ ## Fields
145
+
146
+ Plain provides many field types for different data:
147
+
148
+ ```python
149
+ from plain import models
150
+
151
+ class Product(models.Model):
152
+ # Text fields
153
+ name = models.CharField(max_length=200)
154
+ description = models.TextField()
155
+
156
+ # Numeric fields
157
+ price = models.DecimalField(max_digits=10, decimal_places=2)
158
+ quantity = models.IntegerField(default=0)
159
+
160
+ # Boolean fields
161
+ is_active = models.BooleanField(default=True)
162
+
163
+ # Date and time fields
164
+ created_at = models.DateTimeField(auto_now_add=True)
165
+ updated_at = models.DateTimeField(auto_now=True)
166
+
167
+ # Relationships
168
+ category = models.ForeignKey("Category", on_delete=models.CASCADE)
169
+ tags = models.ManyToManyField("Tag")
170
+ ```
171
+
172
+ Common field types include:
173
+
174
+ - [`CharField`](./fields/__init__.py#CharField)
175
+ - [`TextField`](./fields/__init__.py#TextField)
176
+ - [`IntegerField`](./fields/__init__.py#IntegerField)
177
+ - [`DecimalField`](./fields/__init__.py#DecimalField)
178
+ - [`BooleanField`](./fields/__init__.py#BooleanField)
179
+ - [`DateTimeField`](./fields/__init__.py#DateTimeField)
180
+ - [`EmailField`](./fields/__init__.py#EmailField)
181
+ - [`URLField`](./fields/__init__.py#URLField)
182
+ - [`UUIDField`](./fields/__init__.py#UUIDField)
183
+
184
+ ## Validation
185
+
186
+ Models can be validated before saving:
187
+
188
+ ```python
189
+ class User(models.Model):
190
+ email = models.EmailField(unique=True)
191
+ age = models.IntegerField()
192
+
193
+ def clean(self):
194
+ if self.age < 18:
195
+ raise ValidationError("User must be 18 or older")
196
+
197
+ def save(self, *args, **kwargs):
198
+ self.full_clean() # Runs validation
199
+ super().save(*args, **kwargs)
200
+ ```
201
+
202
+ Field-level validation happens automatically based on field types and constraints.
203
+
204
+ ## Indexes and constraints
205
+
206
+ Optimize queries and ensure data integrity with indexes and constraints:
207
+
208
+ ```python
209
+ class User(models.Model):
210
+ email = models.EmailField()
211
+ username = models.CharField(max_length=150)
212
+ age = models.IntegerField()
213
+
214
+ class Meta:
215
+ indexes = [
216
+ models.Index(fields=["email"]),
217
+ models.Index(fields=["-created_at"], name="user_created_idx"),
218
+ ]
219
+ constraints = [
220
+ models.UniqueConstraint(fields=["email", "username"], name="unique_user"),
221
+ models.CheckConstraint(check=models.Q(age__gte=0), name="age_positive"),
222
+ ]
223
+ ```
224
+
225
+ ## Managers
226
+
227
+ [`Manager`](./manager.py#Manager) objects provide the interface for querying models:
228
+
229
+ ```python
230
+ class PublishedManager(models.Manager):
231
+ def get_queryset(self):
232
+ return super().get_queryset().filter(status="published")
233
+
234
+ class Article(models.Model):
235
+ title = models.CharField(max_length=200)
236
+ status = models.CharField(max_length=20)
237
+
238
+ # Default manager
239
+ objects = models.Manager()
240
+
241
+ # Custom manager
242
+ published = PublishedManager()
243
+
244
+ # Usage
245
+ all_articles = Article.objects.all()
246
+ published_articles = Article.published.all()
247
+ ```
248
+
249
+ ## Forms
250
+
251
+ Models integrate with Plain's form system:
252
+
253
+ ```python
254
+ from plain import forms
255
+ from .models import User
256
+
257
+ class UserForm(forms.ModelForm):
258
+ class Meta:
259
+ model = User
260
+ fields = ["email", "is_admin"]
261
+
262
+ # Usage
263
+ form = UserForm(data=request.data)
264
+ if form.is_valid():
265
+ user = form.save()
266
+ ```
267
+
268
+ ## Sharing fields across models
269
+
270
+ To share common fields across multiple models, use Python classes as mixins. The final, registered model must inherit directly from `models.Model` and the mixins should not.
271
+
272
+ ```python
273
+ from plain import models
274
+
275
+
276
+ # Regular Python class for shared fields
277
+ class TimestampedMixin:
278
+ created_at = models.DateTimeField(auto_now_add=True)
279
+ updated_at = models.DateTimeField(auto_now=True)
280
+
281
+
282
+ # Models inherit from the mixin AND models.Model
283
+ @models.register_model
284
+ class User(TimestampedMixin, models.Model):
285
+ email = models.EmailField()
286
+ password = PasswordField()
287
+ is_admin = models.BooleanField(default=False)
288
+
289
+
290
+ @models.register_model
291
+ class Note(TimestampedMixin, models.Model):
292
+ content = models.TextField(max_length=1024)
293
+ liked = models.BooleanField(default=False)
294
+ ```
295
+
296
+ ## Installation
297
+
298
+ Install the `plain.models` package from [PyPI](https://pypi.org/project/plain.models/):
299
+
300
+ ```bash
301
+ uv add plain.models
302
+ ```
303
+
304
+ Then add to your `INSTALLED_PACKAGES`:
305
+
306
+ ```python
307
+ # app/settings.py
308
+ INSTALLED_PACKAGES = [
309
+ ...
310
+ "plain.models",
311
+ ]
312
+ ```
@@ -1,5 +1,31 @@
1
1
  # plain-models changelog
2
2
 
3
+ ## [0.40.0](https://github.com/dropseed/plain/releases/plain-models@0.40.0) (2025-08-05)
4
+
5
+ ### What's changed
6
+
7
+ - Foreign key fields now accept lazy objects (like `SimpleLazyObject` used for `request.user`) by automatically evaluating them ([eb78dcc76d](https://github.com/dropseed/plain/commit/eb78dcc76d))
8
+ - Added `--no-input` option to `plain migrate` command to skip user prompts ([0bdaf0409e](https://github.com/dropseed/plain/commit/0bdaf0409e))
9
+ - Removed the `plain models optimize-migration` command ([6e4131ab29](https://github.com/dropseed/plain/commit/6e4131ab29))
10
+ - Removed the `--fake-initial` option from `plain migrate` command ([6506a8bfb9](https://github.com/dropseed/plain/commit/6506a8bfb9))
11
+ - Fixed CLI help text to reference `plain` commands instead of `manage.py` ([8071854d61](https://github.com/dropseed/plain/commit/8071854d61))
12
+
13
+ ### Upgrade instructions
14
+
15
+ - Remove any usage of `plain models optimize-migration` command - it is no longer available
16
+ - Remove any usage of `--fake-initial` option from `plain migrate` commands - it is no longer supported
17
+ - It is no longer necessary to do `user=request.user or None`, for example, when setting foreign key fields with a lazy object like `request.user`. These will now be automatically evaluated.
18
+
19
+ ## [0.39.2](https://github.com/dropseed/plain/releases/plain-models@0.39.2) (2025-07-25)
20
+
21
+ ### What's changed
22
+
23
+ - Fixed remaining `to_field_name` attribute usage in `ModelMultipleChoiceField` validation to use `id` directly ([26c80356d3](https://github.com/dropseed/plain/commit/26c80356d3))
24
+
25
+ ### Upgrade instructions
26
+
27
+ - No changes required
28
+
3
29
  ## [0.39.1](https://github.com/dropseed/plain/releases/plain-models@0.39.1) (2025-07-22)
4
30
 
5
31
  ### What's changed
@@ -0,0 +1,301 @@
1
+ # plain.models
2
+
3
+ **Model your data and store it in a database.**
4
+
5
+ - [Overview](#overview)
6
+ - [Database connection](#database-connection)
7
+ - [Querying](#querying)
8
+ - [Migrations](#migrations)
9
+ - [Fields](#fields)
10
+ - [Validation](#validation)
11
+ - [Indexes and constraints](#indexes-and-constraints)
12
+ - [Managers](#managers)
13
+ - [Forms](#forms)
14
+ - [Sharing fields across models](#sharing-fields-across-models)
15
+ - [Installation](#installation)
16
+
17
+ ## Overview
18
+
19
+ ```python
20
+ # app/users/models.py
21
+ from plain import models
22
+ from plain.passwords.models import PasswordField
23
+
24
+
25
+ @models.register_model
26
+ class User(models.Model):
27
+ email = models.EmailField()
28
+ password = PasswordField()
29
+ is_admin = models.BooleanField(default=False)
30
+ created_at = models.DateTimeField(auto_now_add=True)
31
+
32
+ def __str__(self):
33
+ return self.email
34
+ ```
35
+
36
+ Every model automatically includes an `id` field which serves as the primary
37
+ key. The name `id` is reserved and can't be used for other fields.
38
+
39
+ Create, update, and delete instances of your models:
40
+
41
+ ```python
42
+ from .models import User
43
+
44
+
45
+ # Create a new user
46
+ user = User.objects.create(
47
+ email="test@example.com",
48
+ password="password",
49
+ )
50
+
51
+ # Update a user
52
+ user.email = "new@example.com"
53
+ user.save()
54
+
55
+ # Delete a user
56
+ user.delete()
57
+
58
+ # Query for users
59
+ admin_users = User.objects.filter(is_admin=True)
60
+ ```
61
+
62
+ ## Database connection
63
+
64
+ To connect to a database, you can provide a `DATABASE_URL` environment variable:
65
+
66
+ ```sh
67
+ DATABASE_URL=postgresql://user:password@localhost:5432/dbname
68
+ ```
69
+
70
+ Or you can manually define the `DATABASE` setting:
71
+
72
+ ```python
73
+ # app/settings.py
74
+ DATABASE = {
75
+ "ENGINE": "plain.models.backends.postgresql",
76
+ "NAME": "dbname",
77
+ "USER": "user",
78
+ "PASSWORD": "password",
79
+ "HOST": "localhost",
80
+ "PORT": "5432",
81
+ }
82
+ ```
83
+
84
+ Multiple backends are supported, including Postgres, MySQL, and SQLite.
85
+
86
+ ## Querying
87
+
88
+ Models come with a powerful query API through their [`Manager`](./manager.py#Manager) interface:
89
+
90
+ ```python
91
+ # Get all users
92
+ all_users = User.objects.all()
93
+
94
+ # Filter users
95
+ admin_users = User.objects.filter(is_admin=True)
96
+ recent_users = User.objects.filter(created_at__gte=datetime.now() - timedelta(days=7))
97
+
98
+ # Get a single user
99
+ user = User.objects.get(email="test@example.com")
100
+
101
+ # Complex queries with Q objects
102
+ from plain.models import Q
103
+ users = User.objects.filter(
104
+ Q(is_admin=True) | Q(email__endswith="@example.com")
105
+ )
106
+
107
+ # Ordering
108
+ users = User.objects.order_by("-created_at")
109
+
110
+ # Limiting results
111
+ first_10_users = User.objects.all()[:10]
112
+ ```
113
+
114
+ For more advanced querying options, see the [`QuerySet`](./query.py#QuerySet) class.
115
+
116
+ ## Migrations
117
+
118
+ Migrations track changes to your models and update the database schema accordingly:
119
+
120
+ ```bash
121
+ # Create migrations for model changes
122
+ plain makemigrations
123
+
124
+ # Apply migrations to the database
125
+ plain migrate
126
+
127
+ # See migration status
128
+ plain models show-migrations
129
+ ```
130
+
131
+ Migrations are Python files that describe database schema changes. They're stored in your app's `migrations/` directory.
132
+
133
+ ## Fields
134
+
135
+ Plain provides many field types for different data:
136
+
137
+ ```python
138
+ from plain import models
139
+
140
+ class Product(models.Model):
141
+ # Text fields
142
+ name = models.CharField(max_length=200)
143
+ description = models.TextField()
144
+
145
+ # Numeric fields
146
+ price = models.DecimalField(max_digits=10, decimal_places=2)
147
+ quantity = models.IntegerField(default=0)
148
+
149
+ # Boolean fields
150
+ is_active = models.BooleanField(default=True)
151
+
152
+ # Date and time fields
153
+ created_at = models.DateTimeField(auto_now_add=True)
154
+ updated_at = models.DateTimeField(auto_now=True)
155
+
156
+ # Relationships
157
+ category = models.ForeignKey("Category", on_delete=models.CASCADE)
158
+ tags = models.ManyToManyField("Tag")
159
+ ```
160
+
161
+ Common field types include:
162
+
163
+ - [`CharField`](./fields/__init__.py#CharField)
164
+ - [`TextField`](./fields/__init__.py#TextField)
165
+ - [`IntegerField`](./fields/__init__.py#IntegerField)
166
+ - [`DecimalField`](./fields/__init__.py#DecimalField)
167
+ - [`BooleanField`](./fields/__init__.py#BooleanField)
168
+ - [`DateTimeField`](./fields/__init__.py#DateTimeField)
169
+ - [`EmailField`](./fields/__init__.py#EmailField)
170
+ - [`URLField`](./fields/__init__.py#URLField)
171
+ - [`UUIDField`](./fields/__init__.py#UUIDField)
172
+
173
+ ## Validation
174
+
175
+ Models can be validated before saving:
176
+
177
+ ```python
178
+ class User(models.Model):
179
+ email = models.EmailField(unique=True)
180
+ age = models.IntegerField()
181
+
182
+ def clean(self):
183
+ if self.age < 18:
184
+ raise ValidationError("User must be 18 or older")
185
+
186
+ def save(self, *args, **kwargs):
187
+ self.full_clean() # Runs validation
188
+ super().save(*args, **kwargs)
189
+ ```
190
+
191
+ Field-level validation happens automatically based on field types and constraints.
192
+
193
+ ## Indexes and constraints
194
+
195
+ Optimize queries and ensure data integrity with indexes and constraints:
196
+
197
+ ```python
198
+ class User(models.Model):
199
+ email = models.EmailField()
200
+ username = models.CharField(max_length=150)
201
+ age = models.IntegerField()
202
+
203
+ class Meta:
204
+ indexes = [
205
+ models.Index(fields=["email"]),
206
+ models.Index(fields=["-created_at"], name="user_created_idx"),
207
+ ]
208
+ constraints = [
209
+ models.UniqueConstraint(fields=["email", "username"], name="unique_user"),
210
+ models.CheckConstraint(check=models.Q(age__gte=0), name="age_positive"),
211
+ ]
212
+ ```
213
+
214
+ ## Managers
215
+
216
+ [`Manager`](./manager.py#Manager) objects provide the interface for querying models:
217
+
218
+ ```python
219
+ class PublishedManager(models.Manager):
220
+ def get_queryset(self):
221
+ return super().get_queryset().filter(status="published")
222
+
223
+ class Article(models.Model):
224
+ title = models.CharField(max_length=200)
225
+ status = models.CharField(max_length=20)
226
+
227
+ # Default manager
228
+ objects = models.Manager()
229
+
230
+ # Custom manager
231
+ published = PublishedManager()
232
+
233
+ # Usage
234
+ all_articles = Article.objects.all()
235
+ published_articles = Article.published.all()
236
+ ```
237
+
238
+ ## Forms
239
+
240
+ Models integrate with Plain's form system:
241
+
242
+ ```python
243
+ from plain import forms
244
+ from .models import User
245
+
246
+ class UserForm(forms.ModelForm):
247
+ class Meta:
248
+ model = User
249
+ fields = ["email", "is_admin"]
250
+
251
+ # Usage
252
+ form = UserForm(data=request.data)
253
+ if form.is_valid():
254
+ user = form.save()
255
+ ```
256
+
257
+ ## Sharing fields across models
258
+
259
+ To share common fields across multiple models, use Python classes as mixins. The final, registered model must inherit directly from `models.Model` and the mixins should not.
260
+
261
+ ```python
262
+ from plain import models
263
+
264
+
265
+ # Regular Python class for shared fields
266
+ class TimestampedMixin:
267
+ created_at = models.DateTimeField(auto_now_add=True)
268
+ updated_at = models.DateTimeField(auto_now=True)
269
+
270
+
271
+ # Models inherit from the mixin AND models.Model
272
+ @models.register_model
273
+ class User(TimestampedMixin, models.Model):
274
+ email = models.EmailField()
275
+ password = PasswordField()
276
+ is_admin = models.BooleanField(default=False)
277
+
278
+
279
+ @models.register_model
280
+ class Note(TimestampedMixin, models.Model):
281
+ content = models.TextField(max_length=1024)
282
+ liked = models.BooleanField(default=False)
283
+ ```
284
+
285
+ ## Installation
286
+
287
+ Install the `plain.models` package from [PyPI](https://pypi.org/project/plain.models/):
288
+
289
+ ```bash
290
+ uv add plain.models
291
+ ```
292
+
293
+ Then add to your `INSTALLED_PACKAGES`:
294
+
295
+ ```python
296
+ # app/settings.py
297
+ INSTALLED_PACKAGES = [
298
+ ...
299
+ "plain.models",
300
+ ]
301
+ ```
@@ -53,11 +53,11 @@ class BaseDatabaseCreation:
53
53
  package_label=None,
54
54
  migration_name=None,
55
55
  fake=False,
56
- fake_initial=False,
57
56
  plan=False,
58
57
  check_unapplied=False,
59
58
  backup=False,
60
59
  prune=False,
60
+ no_input=True,
61
61
  verbosity=max(verbosity - 1, 0),
62
62
  )
63
63