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.
Files changed (93) hide show
  1. plain/postgres/CHANGELOG.md +1028 -0
  2. plain/postgres/README.md +925 -0
  3. plain/postgres/__init__.py +120 -0
  4. plain/postgres/agents/.claude/rules/plain-postgres.md +78 -0
  5. plain/postgres/aggregates.py +236 -0
  6. plain/postgres/backups/__init__.py +0 -0
  7. plain/postgres/backups/cli.py +148 -0
  8. plain/postgres/backups/clients.py +94 -0
  9. plain/postgres/backups/core.py +172 -0
  10. plain/postgres/base.py +1415 -0
  11. plain/postgres/cli/__init__.py +3 -0
  12. plain/postgres/cli/db.py +142 -0
  13. plain/postgres/cli/migrations.py +1085 -0
  14. plain/postgres/config.py +18 -0
  15. plain/postgres/connection.py +1331 -0
  16. plain/postgres/connections.py +77 -0
  17. plain/postgres/constants.py +13 -0
  18. plain/postgres/constraints.py +495 -0
  19. plain/postgres/database_url.py +94 -0
  20. plain/postgres/db.py +59 -0
  21. plain/postgres/default_settings.py +38 -0
  22. plain/postgres/deletion.py +475 -0
  23. plain/postgres/dialect.py +640 -0
  24. plain/postgres/entrypoints.py +4 -0
  25. plain/postgres/enums.py +103 -0
  26. plain/postgres/exceptions.py +217 -0
  27. plain/postgres/expressions.py +1912 -0
  28. plain/postgres/fields/__init__.py +2118 -0
  29. plain/postgres/fields/encrypted.py +354 -0
  30. plain/postgres/fields/json.py +413 -0
  31. plain/postgres/fields/mixins.py +30 -0
  32. plain/postgres/fields/related.py +1192 -0
  33. plain/postgres/fields/related_descriptors.py +290 -0
  34. plain/postgres/fields/related_lookups.py +223 -0
  35. plain/postgres/fields/related_managers.py +661 -0
  36. plain/postgres/fields/reverse_descriptors.py +229 -0
  37. plain/postgres/fields/reverse_related.py +328 -0
  38. plain/postgres/fields/timezones.py +143 -0
  39. plain/postgres/forms.py +773 -0
  40. plain/postgres/functions/__init__.py +189 -0
  41. plain/postgres/functions/comparison.py +127 -0
  42. plain/postgres/functions/datetime.py +454 -0
  43. plain/postgres/functions/math.py +140 -0
  44. plain/postgres/functions/mixins.py +59 -0
  45. plain/postgres/functions/text.py +282 -0
  46. plain/postgres/functions/window.py +125 -0
  47. plain/postgres/indexes.py +286 -0
  48. plain/postgres/lookups.py +758 -0
  49. plain/postgres/meta.py +584 -0
  50. plain/postgres/migrations/__init__.py +53 -0
  51. plain/postgres/migrations/autodetector.py +1379 -0
  52. plain/postgres/migrations/exceptions.py +54 -0
  53. plain/postgres/migrations/executor.py +188 -0
  54. plain/postgres/migrations/graph.py +364 -0
  55. plain/postgres/migrations/loader.py +377 -0
  56. plain/postgres/migrations/migration.py +180 -0
  57. plain/postgres/migrations/operations/__init__.py +34 -0
  58. plain/postgres/migrations/operations/base.py +139 -0
  59. plain/postgres/migrations/operations/fields.py +373 -0
  60. plain/postgres/migrations/operations/models.py +798 -0
  61. plain/postgres/migrations/operations/special.py +184 -0
  62. plain/postgres/migrations/optimizer.py +74 -0
  63. plain/postgres/migrations/questioner.py +340 -0
  64. plain/postgres/migrations/recorder.py +119 -0
  65. plain/postgres/migrations/serializer.py +378 -0
  66. plain/postgres/migrations/state.py +882 -0
  67. plain/postgres/migrations/utils.py +147 -0
  68. plain/postgres/migrations/writer.py +302 -0
  69. plain/postgres/options.py +207 -0
  70. plain/postgres/otel.py +231 -0
  71. plain/postgres/preflight.py +336 -0
  72. plain/postgres/query.py +2242 -0
  73. plain/postgres/query_utils.py +456 -0
  74. plain/postgres/registry.py +217 -0
  75. plain/postgres/schema.py +1885 -0
  76. plain/postgres/sql/__init__.py +40 -0
  77. plain/postgres/sql/compiler.py +1869 -0
  78. plain/postgres/sql/constants.py +22 -0
  79. plain/postgres/sql/datastructures.py +222 -0
  80. plain/postgres/sql/query.py +2947 -0
  81. plain/postgres/sql/where.py +374 -0
  82. plain/postgres/test/__init__.py +0 -0
  83. plain/postgres/test/pytest.py +117 -0
  84. plain/postgres/test/utils.py +18 -0
  85. plain/postgres/transaction.py +222 -0
  86. plain/postgres/types.py +92 -0
  87. plain/postgres/types.pyi +751 -0
  88. plain/postgres/utils.py +345 -0
  89. plain_postgres-0.84.0.dist-info/METADATA +937 -0
  90. plain_postgres-0.84.0.dist-info/RECORD +93 -0
  91. plain_postgres-0.84.0.dist-info/WHEEL +4 -0
  92. plain_postgres-0.84.0.dist-info/entry_points.txt +5 -0
  93. plain_postgres-0.84.0.dist-info/licenses/LICENSE +61 -0
@@ -0,0 +1,119 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any
4
+
5
+ from plain import postgres
6
+ from plain.postgres.db import DatabaseError
7
+ from plain.postgres.meta import Meta
8
+ from plain.postgres.registry import ModelsRegistry
9
+ from plain.utils.functional import classproperty
10
+ from plain.utils.timezone import now
11
+
12
+ from .exceptions import MigrationSchemaMissing
13
+
14
+ MIGRATION_TABLE_NAME = "plainmigrations"
15
+
16
+ if TYPE_CHECKING:
17
+ from plain.postgres.connection import DatabaseConnection
18
+
19
+
20
+ class MigrationRecorder:
21
+ """
22
+ Deal with storing migration records in the database.
23
+
24
+ Because this table is actually itself used for dealing with model
25
+ creation, it's the one thing we can't do normally via migrations.
26
+ We manually handle table creation/schema updating (using schema backend)
27
+ and then have a floating model to do queries with.
28
+
29
+ If a migration is unapplied its row is removed from the table. Having
30
+ a row in the table always means a migration is applied.
31
+ """
32
+
33
+ _migration_class: type[postgres.Model] | None = None
34
+
35
+ @classproperty # type: ignore[invalid-argument-type]
36
+ def Migration(cls) -> type[postgres.Model]:
37
+ """
38
+ Lazy load to avoid PackageRegistryNotReady if installed packages import
39
+ MigrationRecorder.
40
+ """
41
+ if cls._migration_class is None:
42
+ _models_registry = ModelsRegistry()
43
+ _models_registry.ready = True
44
+
45
+ class Migration(postgres.Model):
46
+ app = postgres.CharField(max_length=255)
47
+ name = postgres.CharField(max_length=255)
48
+ applied = postgres.DateTimeField(default=now)
49
+
50
+ # Use isolated models registry for migrations
51
+ _model_meta = Meta(models_registry=_models_registry)
52
+
53
+ model_options = postgres.Options(
54
+ package_label="migrations",
55
+ db_table=MIGRATION_TABLE_NAME,
56
+ )
57
+
58
+ def __str__(self) -> str:
59
+ return f"Migration {self.name} for {self.app}"
60
+
61
+ cls._migration_class = Migration
62
+ return cls._migration_class
63
+
64
+ def __init__(self, connection: DatabaseConnection) -> None:
65
+ self.connection = connection
66
+
67
+ @property
68
+ def migration_qs(self) -> Any:
69
+ return self.Migration.query.all()
70
+
71
+ def has_table(self) -> bool:
72
+ """Return True if the plainmigrations table exists."""
73
+ with self.connection.cursor() as cursor:
74
+ tables = self.connection.table_names(cursor)
75
+ return self.Migration.model_options.db_table in tables
76
+
77
+ def ensure_schema(self) -> None:
78
+ """Ensure the table exists and has the correct schema."""
79
+ # If the table's there, that's fine - we've never changed its schema
80
+ # in the codebase.
81
+ if self.has_table():
82
+ return
83
+ # Make the table
84
+ try:
85
+ with self.connection.schema_editor() as editor:
86
+ editor.create_model(self.Migration)
87
+ except DatabaseError as exc:
88
+ raise MigrationSchemaMissing(
89
+ f"Unable to create the plainmigrations table ({exc})"
90
+ )
91
+
92
+ def applied_migrations(self) -> dict[tuple[str, str], Any]:
93
+ """
94
+ Return a dict mapping (package_name, migration_name) to Migration instances
95
+ for all applied migrations.
96
+ """
97
+ if self.has_table():
98
+ return {
99
+ (migration.app, migration.name): migration
100
+ for migration in self.migration_qs
101
+ }
102
+ else:
103
+ # If the plainmigrations table doesn't exist, then no migrations
104
+ # are applied.
105
+ return {}
106
+
107
+ def record_applied(self, app: str, name: str) -> None:
108
+ """Record that a migration was applied."""
109
+ self.ensure_schema()
110
+ self.migration_qs.create(app=app, name=name)
111
+
112
+ def record_unapplied(self, app: str, name: str) -> None:
113
+ """Record that a migration was unapplied."""
114
+ self.ensure_schema()
115
+ self.migration_qs.filter(app=app, name=name).delete()
116
+
117
+ def flush(self) -> None:
118
+ """Delete all migration records. Useful for testing migrations."""
119
+ self.migration_qs.all().delete()
@@ -0,0 +1,378 @@
1
+ from __future__ import annotations
2
+
3
+ import builtins
4
+ import collections.abc
5
+ import datetime
6
+ import decimal
7
+ import enum
8
+ import functools
9
+ import math
10
+ import os
11
+ import pathlib
12
+ import re
13
+ import types
14
+ import uuid
15
+ from typing import Any
16
+
17
+ from plain.postgres.base import Model
18
+ from plain.postgres.enums import Choices
19
+ from plain.postgres.fields import Field
20
+ from plain.postgres.migrations.operations.base import Operation
21
+ from plain.postgres.migrations.utils import COMPILED_REGEX_TYPE, RegexObject
22
+ from plain.runtime import SettingsReference
23
+ from plain.utils.functional import LazyObject, Promise
24
+
25
+
26
+ class BaseSerializer:
27
+ def __init__(self, value: Any) -> None:
28
+ self.value = value
29
+
30
+ def serialize(self) -> tuple[str, set[str]]:
31
+ raise NotImplementedError(
32
+ "subclasses of BaseSerializer must provide a serialize() method"
33
+ )
34
+
35
+
36
+ class BaseSequenceSerializer(BaseSerializer):
37
+ def _format(self) -> str:
38
+ raise NotImplementedError(
39
+ "subclasses of BaseSequenceSerializer must provide a _format() method"
40
+ )
41
+
42
+ def serialize(self) -> tuple[str, set[str]]:
43
+ imports: set[str] = set()
44
+ strings = []
45
+ for item in self.value:
46
+ item_string, item_imports = serializer_factory(item).serialize()
47
+ imports.update(item_imports)
48
+ strings.append(item_string)
49
+ value = self._format()
50
+ return value % (", ".join(strings)), imports
51
+
52
+
53
+ class BaseSimpleSerializer(BaseSerializer):
54
+ def serialize(self) -> tuple[str, set[str]]:
55
+ return repr(self.value), set()
56
+
57
+
58
+ class ChoicesSerializer(BaseSerializer):
59
+ def serialize(self) -> tuple[str, set[str]]:
60
+ return serializer_factory(self.value.value).serialize()
61
+
62
+
63
+ class DateTimeSerializer(BaseSerializer):
64
+ """For datetime.*, except datetime.datetime."""
65
+
66
+ def serialize(self) -> tuple[str, set[str]]:
67
+ return repr(self.value), {"import datetime"}
68
+
69
+
70
+ class DatetimeDatetimeSerializer(BaseSerializer):
71
+ """For datetime.datetime."""
72
+
73
+ def serialize(self) -> tuple[str, set[str]]:
74
+ if self.value.tzinfo is not None and self.value.tzinfo != datetime.UTC:
75
+ self.value = self.value.astimezone(datetime.UTC)
76
+ imports = ["import datetime"]
77
+ return repr(self.value), set(imports)
78
+
79
+
80
+ class DecimalSerializer(BaseSerializer):
81
+ def serialize(self) -> tuple[str, set[str]]:
82
+ return repr(self.value), {"from decimal import Decimal"}
83
+
84
+
85
+ class DeconstructableSerializer(BaseSerializer):
86
+ @staticmethod
87
+ def serialize_deconstructed(
88
+ path: str, args: tuple[Any, ...], kwargs: dict[str, Any]
89
+ ) -> tuple[str, set[str]]:
90
+ name, imports = DeconstructableSerializer._serialize_path(path)
91
+ strings = []
92
+ for arg in args:
93
+ arg_string, arg_imports = serializer_factory(arg).serialize()
94
+ strings.append(arg_string)
95
+ imports.update(arg_imports)
96
+ for kw, arg in sorted(kwargs.items()):
97
+ arg_string, arg_imports = serializer_factory(arg).serialize()
98
+ imports.update(arg_imports)
99
+ strings.append(f"{kw}={arg_string}")
100
+ return "{}({})".format(name, ", ".join(strings)), imports
101
+
102
+ @staticmethod
103
+ def _serialize_path(path: str) -> tuple[str, set[str]]:
104
+ module, name = path.rsplit(".", 1)
105
+ if module == "plain.postgres":
106
+ imports: set[str] = {"from plain import postgres"}
107
+ name = f"postgres.{name}"
108
+ else:
109
+ imports = {f"import {module}"}
110
+ name = path
111
+ return name, imports
112
+
113
+ def serialize(self) -> tuple[str, set[str]]:
114
+ return self.serialize_deconstructed(*self.value.deconstruct())
115
+
116
+
117
+ class DictionarySerializer(BaseSerializer):
118
+ def serialize(self) -> tuple[str, set[str]]:
119
+ imports: set[str] = set()
120
+ strings = []
121
+ for k, v in sorted(self.value.items()):
122
+ k_string, k_imports = serializer_factory(k).serialize()
123
+ v_string, v_imports = serializer_factory(v).serialize()
124
+ imports.update(k_imports)
125
+ imports.update(v_imports)
126
+ strings.append((k_string, v_string))
127
+ return "{{{}}}".format(", ".join(f"{k}: {v}" for k, v in strings)), imports
128
+
129
+
130
+ class EnumSerializer(BaseSerializer):
131
+ def serialize(self) -> tuple[str, set[str]]:
132
+ enum_class = self.value.__class__
133
+ module = enum_class.__module__
134
+ if issubclass(enum_class, enum.Flag):
135
+ members = list(self.value)
136
+ else:
137
+ members = (self.value,)
138
+ return (
139
+ " | ".join(
140
+ [
141
+ f"{module}.{enum_class.__qualname__}[{item.name!r}]"
142
+ for item in members
143
+ ]
144
+ ),
145
+ {f"import {module}"},
146
+ )
147
+
148
+
149
+ class FloatSerializer(BaseSimpleSerializer):
150
+ def serialize(self) -> tuple[str, set[str]]:
151
+ if math.isnan(self.value) or math.isinf(self.value):
152
+ return f'float("{self.value}")', set()
153
+ return super().serialize()
154
+
155
+
156
+ class FrozensetSerializer(BaseSequenceSerializer):
157
+ def _format(self) -> str:
158
+ return "frozenset([%s])"
159
+
160
+
161
+ class FunctionTypeSerializer(BaseSerializer):
162
+ def serialize(self) -> tuple[str, set[str]]:
163
+ if getattr(self.value, "__self__", None) and isinstance(
164
+ self.value.__self__, type
165
+ ):
166
+ klass = self.value.__self__
167
+ module = klass.__module__
168
+ return f"{module}.{klass.__name__}.{self.value.__name__}", {
169
+ f"import {module}"
170
+ }
171
+ # Further error checking
172
+ if self.value.__name__ == "<lambda>":
173
+ raise ValueError("Cannot serialize function: lambda")
174
+ if self.value.__module__ is None:
175
+ raise ValueError(f"Cannot serialize function {self.value!r}: No module")
176
+
177
+ module_name = self.value.__module__
178
+
179
+ if "<" not in self.value.__qualname__: # Qualname can include <locals>
180
+ return f"{module_name}.{self.value.__qualname__}", {
181
+ f"import {self.value.__module__}"
182
+ }
183
+
184
+ raise ValueError(
185
+ f"Could not find function {self.value.__name__} in {module_name}.\n"
186
+ )
187
+
188
+
189
+ class FunctoolsPartialSerializer(BaseSerializer):
190
+ def serialize(self) -> tuple[str, set[str]]:
191
+ # Serialize functools.partial() arguments
192
+ func_string, func_imports = serializer_factory(self.value.func).serialize()
193
+ args_string, args_imports = serializer_factory(self.value.args).serialize()
194
+ keywords_string, keywords_imports = serializer_factory(
195
+ self.value.keywords
196
+ ).serialize()
197
+ # Add any imports needed by arguments
198
+ imports: set[str] = {
199
+ "import functools",
200
+ *func_imports,
201
+ *args_imports,
202
+ *keywords_imports,
203
+ }
204
+ return (
205
+ f"functools.{self.value.__class__.__name__}({func_string}, *{args_string}, **{keywords_string})",
206
+ imports,
207
+ )
208
+
209
+
210
+ class IterableSerializer(BaseSerializer):
211
+ def serialize(self) -> tuple[str, set[str]]:
212
+ imports: set[str] = set()
213
+ strings = []
214
+ for item in self.value:
215
+ item_string, item_imports = serializer_factory(item).serialize()
216
+ imports.update(item_imports)
217
+ strings.append(item_string)
218
+ # When len(strings)==0, the empty iterable should be serialized as
219
+ # "()", not "(,)" because (,) is invalid Python syntax.
220
+ value = "(%s)" if len(strings) != 1 else "(%s,)"
221
+ return value % (", ".join(strings)), imports
222
+
223
+
224
+ class ModelFieldSerializer(DeconstructableSerializer):
225
+ def serialize(self) -> tuple[str, set[str]]:
226
+ attr_name, path, args, kwargs = self.value.deconstruct()
227
+ return self.serialize_deconstructed(path, args, kwargs)
228
+
229
+
230
+ class OperationSerializer(BaseSerializer):
231
+ def serialize(self) -> tuple[str, set[str]]:
232
+ from plain.postgres.migrations.writer import OperationWriter
233
+
234
+ string, imports = OperationWriter(self.value, indentation=0).serialize()
235
+ # Nested operation, trailing comma is handled in upper OperationWriter._write()
236
+ return string.rstrip(","), imports
237
+
238
+
239
+ class PathLikeSerializer(BaseSerializer):
240
+ def serialize(self) -> tuple[str, set[str]]:
241
+ return repr(os.fspath(self.value)), set()
242
+
243
+
244
+ class PathSerializer(BaseSerializer):
245
+ def serialize(self) -> tuple[str, set[str]]:
246
+ # Convert concrete paths to pure paths to avoid issues with migrations
247
+ # generated on one platform being used on a different platform.
248
+ prefix = "Pure" if isinstance(self.value, pathlib.Path) else ""
249
+ return f"pathlib.{prefix}{self.value!r}", {"import pathlib"}
250
+
251
+
252
+ class RegexSerializer(BaseSerializer):
253
+ def serialize(self) -> tuple[str, set[str]]:
254
+ regex_pattern, pattern_imports = serializer_factory(
255
+ self.value.pattern
256
+ ).serialize()
257
+ # Turn off default implicit flags (e.g. re.U) because regexes with the
258
+ # same implicit and explicit flags aren't equal.
259
+ flags = self.value.flags ^ re.compile("").flags
260
+ regex_flags, flag_imports = serializer_factory(flags).serialize()
261
+ imports: set[str] = {"import re", *pattern_imports, *flag_imports}
262
+ args = [regex_pattern]
263
+ if flags:
264
+ args.append(regex_flags)
265
+ return "re.compile({})".format(", ".join(args)), imports
266
+
267
+
268
+ class SequenceSerializer(BaseSequenceSerializer):
269
+ def _format(self) -> str:
270
+ return "[%s]"
271
+
272
+
273
+ class SetSerializer(BaseSequenceSerializer):
274
+ def _format(self) -> str:
275
+ # Serialize as a set literal except when value is empty because {}
276
+ # is an empty dict.
277
+ return "{%s}" if self.value else "set(%s)"
278
+
279
+
280
+ class SettingsReferenceSerializer(BaseSerializer):
281
+ def serialize(self) -> tuple[str, set[str]]:
282
+ return f"settings.{self.value.setting_name}", {
283
+ "from plain.runtime import settings"
284
+ }
285
+
286
+
287
+ class TupleSerializer(BaseSequenceSerializer):
288
+ def _format(self) -> str:
289
+ # When len(value)==0, the empty tuple should be serialized as "()",
290
+ # not "(,)" because (,) is invalid Python syntax.
291
+ return "(%s)" if len(self.value) != 1 else "(%s,)"
292
+
293
+
294
+ class TypeSerializer(BaseSerializer):
295
+ def serialize(self) -> tuple[str, set[str]]:
296
+ special_cases = [
297
+ (Model, "postgres.Model", ["from plain import postgres"]),
298
+ (types.NoneType, "types.NoneType", ["import types"]),
299
+ ]
300
+ for case, string, imports in special_cases:
301
+ if case is self.value:
302
+ return string, set(imports)
303
+ if hasattr(self.value, "__module__"):
304
+ module = self.value.__module__
305
+ if module == builtins.__name__:
306
+ return self.value.__name__, set()
307
+ else:
308
+ return f"{module}.{self.value.__qualname__}", {f"import {module}"}
309
+ return "", set()
310
+
311
+
312
+ class UUIDSerializer(BaseSerializer):
313
+ def serialize(self) -> tuple[str, set[str]]:
314
+ return f"uuid.{repr(self.value)}", {"import uuid"}
315
+
316
+
317
+ class Serializer:
318
+ _registry = {
319
+ # Some of these are order-dependent.
320
+ frozenset: FrozensetSerializer,
321
+ list: SequenceSerializer,
322
+ set: SetSerializer,
323
+ tuple: TupleSerializer,
324
+ dict: DictionarySerializer,
325
+ Choices: ChoicesSerializer,
326
+ enum.Enum: EnumSerializer,
327
+ datetime.datetime: DatetimeDatetimeSerializer,
328
+ (datetime.date, datetime.timedelta, datetime.time): DateTimeSerializer,
329
+ SettingsReference: SettingsReferenceSerializer,
330
+ float: FloatSerializer,
331
+ (bool, int, types.NoneType, bytes, str, range): BaseSimpleSerializer,
332
+ decimal.Decimal: DecimalSerializer,
333
+ (functools.partial, functools.partialmethod): FunctoolsPartialSerializer,
334
+ (
335
+ types.FunctionType,
336
+ types.BuiltinFunctionType,
337
+ types.MethodType,
338
+ ): FunctionTypeSerializer,
339
+ collections.abc.Iterable: IterableSerializer,
340
+ (COMPILED_REGEX_TYPE, RegexObject): RegexSerializer,
341
+ uuid.UUID: UUIDSerializer,
342
+ pathlib.PurePath: PathSerializer,
343
+ os.PathLike: PathLikeSerializer,
344
+ }
345
+
346
+ @classmethod
347
+ def register(cls, type_: type[Any], serializer: type[BaseSerializer]) -> None:
348
+ if not issubclass(serializer, BaseSerializer):
349
+ raise ValueError(
350
+ f"'{serializer.__name__}' must inherit from 'BaseSerializer'."
351
+ )
352
+ cls._registry[type_] = serializer # type: ignore[assignment]
353
+
354
+
355
+ def serializer_factory(value: Any) -> BaseSerializer:
356
+ if isinstance(value, Promise):
357
+ value = str(value)
358
+ elif isinstance(value, LazyObject):
359
+ # The unwrapped value is returned as the first item of the arguments
360
+ # tuple.
361
+ value = value.__reduce__()[1][0]
362
+
363
+ if isinstance(value, Field):
364
+ return ModelFieldSerializer(value)
365
+ if isinstance(value, Operation):
366
+ return OperationSerializer(value)
367
+ if isinstance(value, type):
368
+ return TypeSerializer(value)
369
+ # Anything that knows how to deconstruct itself.
370
+ if hasattr(value, "deconstruct"):
371
+ return DeconstructableSerializer(value)
372
+ for type_, serializer_cls in Serializer._registry.items():
373
+ if isinstance(value, type_):
374
+ return serializer_cls(value)
375
+ raise ValueError(
376
+ f"Cannot serialize: {value!r}\nThere are some values Plain cannot serialize into "
377
+ "migration files."
378
+ )