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,184 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ from .base import Operation
7
+
8
+ if TYPE_CHECKING:
9
+ from plain.postgres.migrations.state import ProjectState
10
+ from plain.postgres.schema import DatabaseSchemaEditor
11
+
12
+
13
+ class SeparateDatabaseAndState(Operation):
14
+ """
15
+ Take two lists of operations - ones that will be used for the database,
16
+ and ones that will be used for the state change. This allows operations
17
+ that don't support state change to have it applied, or have operations
18
+ that affect the state or not the database, or so on.
19
+ """
20
+
21
+ serialization_expand_args = ["database_operations", "state_operations"]
22
+
23
+ def __init__(
24
+ self,
25
+ database_operations: list[Operation] | None = None,
26
+ state_operations: list[Operation] | None = None,
27
+ ) -> None:
28
+ self.database_operations = database_operations or []
29
+ self.state_operations = state_operations or []
30
+
31
+ def deconstruct(self) -> tuple[str, tuple[Any, ...], dict[str, list[Operation]]]:
32
+ kwargs: dict[str, list[Operation]] = {}
33
+ if self.database_operations:
34
+ kwargs["database_operations"] = self.database_operations
35
+ if self.state_operations:
36
+ kwargs["state_operations"] = self.state_operations
37
+ return (self.__class__.__qualname__, (), kwargs)
38
+
39
+ def state_forwards(self, package_label: str, state: ProjectState) -> None:
40
+ for state_operation in self.state_operations:
41
+ state_operation.state_forwards(package_label, state)
42
+
43
+ def database_forwards(
44
+ self,
45
+ package_label: str,
46
+ schema_editor: DatabaseSchemaEditor,
47
+ from_state: ProjectState,
48
+ to_state: ProjectState,
49
+ ) -> None:
50
+ # We calculate state separately in here since our state functions aren't useful
51
+ for database_operation in self.database_operations:
52
+ to_state = from_state.clone()
53
+ database_operation.state_forwards(package_label, to_state)
54
+ database_operation.database_forwards(
55
+ package_label, schema_editor, from_state, to_state
56
+ )
57
+ from_state = to_state
58
+
59
+ def describe(self) -> str:
60
+ return "Custom state/database change combination"
61
+
62
+
63
+ class RunSQL(Operation):
64
+ """
65
+ Run some raw SQL.
66
+
67
+ Also accept a list of operations that represent the state change effected
68
+ by this SQL change, in case it's custom column/table creation/deletion.
69
+ """
70
+
71
+ def __init__(
72
+ self,
73
+ sql: str
74
+ | list[str | tuple[str, list[Any]]]
75
+ | tuple[str | tuple[str, list[Any]], ...],
76
+ *,
77
+ state_operations: list[Operation] | None = None,
78
+ elidable: bool = False,
79
+ ) -> None:
80
+ self.sql = sql
81
+ self.state_operations = state_operations or []
82
+ self.elidable = elidable
83
+
84
+ def deconstruct(self) -> tuple[str, tuple[Any, ...], dict[str, Any]]:
85
+ kwargs: dict[str, Any] = {
86
+ "sql": self.sql,
87
+ }
88
+ if self.state_operations:
89
+ kwargs["state_operations"] = self.state_operations
90
+ return (self.__class__.__qualname__, (), kwargs)
91
+
92
+ def state_forwards(self, package_label: str, state: ProjectState) -> None:
93
+ for state_operation in self.state_operations:
94
+ state_operation.state_forwards(package_label, state)
95
+
96
+ def database_forwards(
97
+ self,
98
+ package_label: str,
99
+ schema_editor: DatabaseSchemaEditor,
100
+ from_state: ProjectState,
101
+ to_state: ProjectState,
102
+ ) -> None:
103
+ self._run_sql(schema_editor, self.sql)
104
+
105
+ def describe(self) -> str:
106
+ return "Raw SQL operation"
107
+
108
+ def _run_sql(
109
+ self,
110
+ schema_editor: DatabaseSchemaEditor,
111
+ sqls: str
112
+ | list[str | tuple[str, list[Any]]]
113
+ | tuple[str | tuple[str, list[Any]], ...],
114
+ ) -> None:
115
+ if isinstance(sqls, list | tuple):
116
+ for sql_item in sqls:
117
+ params: list[Any] | None = None
118
+ sql: str
119
+ if isinstance(sql_item, list | tuple):
120
+ elements = len(sql_item)
121
+ if elements == 2:
122
+ sql, params = sql_item
123
+ else:
124
+ raise ValueError("Expected a 2-tuple but got %d" % elements) # noqa: UP031
125
+ else:
126
+ sql = sql_item
127
+ schema_editor.execute(sql, params=params)
128
+ else:
129
+ # PostgreSQL can handle multi-statement scripts in a single execute call
130
+ schema_editor.execute(sqls, params=None)
131
+
132
+
133
+ class RunPython(Operation):
134
+ """
135
+ Run Python code in a context suitable for doing versioned ORM operations.
136
+ """
137
+
138
+ reduces_to_sql = False
139
+
140
+ def __init__(
141
+ self,
142
+ code: Callable[..., Any],
143
+ *,
144
+ atomic: bool | None = None,
145
+ elidable: bool = False,
146
+ ) -> None:
147
+ self.atomic = atomic
148
+ # Forwards code
149
+ if not callable(code):
150
+ raise ValueError("RunPython must be supplied with a callable")
151
+ self.code = code
152
+ self.elidable = elidable
153
+
154
+ def deconstruct(self) -> tuple[str, tuple[Any, ...], dict[str, Any]]:
155
+ kwargs: dict[str, Any] = {
156
+ "code": self.code,
157
+ }
158
+ if self.atomic is not None:
159
+ kwargs["atomic"] = self.atomic
160
+ return (self.__class__.__qualname__, (), kwargs)
161
+
162
+ def state_forwards(self, package_label: str, state: Any) -> None:
163
+ # RunPython objects have no state effect. To add some, combine this
164
+ # with SeparateDatabaseAndState.
165
+ pass
166
+
167
+ def database_forwards(
168
+ self,
169
+ package_label: str,
170
+ schema_editor: DatabaseSchemaEditor,
171
+ from_state: ProjectState,
172
+ to_state: ProjectState,
173
+ ) -> None:
174
+ # RunPython has access to all models. Ensure that all models are
175
+ # reloaded in case any are delayed.
176
+ from_state.clear_delayed_models_cache()
177
+ # We now execute the Python code in a context that contains a 'models'
178
+ # object, representing the versioned models as an app registry.
179
+ # We could try to override the global cache, but then people will still
180
+ # use direct imports, so we go with a documentation approach instead.
181
+ self.code(from_state.models_registry, schema_editor)
182
+
183
+ def describe(self) -> str:
184
+ return "Raw Python operation"
@@ -0,0 +1,74 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ class MigrationOptimizer:
7
+ """
8
+ Power the optimization process, where you provide a list of Operations
9
+ and you are returned a list of equal or shorter length - operations
10
+ are merged into one if possible.
11
+
12
+ For example, a CreateModel and an AddField can be optimized into a
13
+ new CreateModel, and CreateModel and DeleteModel can be optimized into
14
+ nothing.
15
+ """
16
+
17
+ def optimize(self, operations: list[Any], package_label: str) -> list[Any]:
18
+ """
19
+ Main optimization entry point. Pass in a list of Operation instances,
20
+ get out a new list of Operation instances.
21
+
22
+ Unfortunately, due to the scope of the optimization (two combinable
23
+ operations might be separated by several hundred others), this can't be
24
+ done as a peephole optimization with checks/output implemented on
25
+ the Operations themselves; instead, the optimizer looks at each
26
+ individual operation and scans forwards in the list to see if there
27
+ are any matches, stopping at boundaries - operations which can't
28
+ be optimized over (RunSQL, operations on the same field/model, etc.)
29
+
30
+ The inner loop is run until the starting list is the same as the result
31
+ list, and then the result is returned. This means that operation
32
+ optimization must be stable and always return an equal or shorter list.
33
+ """
34
+ # Internal tracking variable for test assertions about # of loops
35
+ if package_label is None:
36
+ raise TypeError("package_label must be a str.")
37
+ while True:
38
+ result = self.optimize_inner(operations, package_label)
39
+ if result == operations:
40
+ return result
41
+ operations = result
42
+
43
+ def optimize_inner(self, operations: list[Any], package_label: str) -> list[Any]:
44
+ """Inner optimization loop."""
45
+ new_operations = []
46
+ for i, operation in enumerate(operations):
47
+ right = True # Should we reduce on the right or on the left.
48
+ # Compare it to each operation after it
49
+ for j, other in enumerate(operations[i + 1 :]):
50
+ result = operation.reduce(other, package_label)
51
+ if isinstance(result, list):
52
+ in_between = operations[i + 1 : i + j + 1]
53
+ if right:
54
+ new_operations.extend(in_between)
55
+ new_operations.extend(result)
56
+ elif all(
57
+ op.reduce(other, package_label) is True for op in in_between
58
+ ):
59
+ # Perform a left reduction if all of the in-between
60
+ # operations can optimize through other.
61
+ new_operations.extend(result)
62
+ new_operations.extend(in_between)
63
+ else:
64
+ # Otherwise keep trying.
65
+ new_operations.append(operation)
66
+ break
67
+ new_operations.extend(operations[i + j + 2 :])
68
+ return new_operations
69
+ elif not result:
70
+ # Can't perform a right reduction.
71
+ right = False
72
+ else:
73
+ new_operations.append(operation)
74
+ return new_operations
@@ -0,0 +1,340 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime
4
+ import importlib
5
+ import os
6
+ import sys
7
+ from collections.abc import Callable
8
+ from typing import TYPE_CHECKING, Any
9
+
10
+ import click
11
+
12
+ from plain.packages import packages_registry
13
+ from plain.postgres.fields import NOT_PROVIDED
14
+ from plain.utils import timezone
15
+
16
+ from .loader import MigrationLoader
17
+
18
+ if TYPE_CHECKING:
19
+ from plain.postgres.fields import Field
20
+
21
+
22
+ class MigrationQuestioner:
23
+ """
24
+ Give the autodetector responses to questions it might have.
25
+ This base class has a built-in noninteractive mode, but the
26
+ interactive subclass is what the command-line arguments will use.
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ defaults: dict[str, Any] | None = None,
32
+ specified_packages: set[str] | None = None,
33
+ dry_run: bool | None = None,
34
+ ) -> None:
35
+ self.defaults = defaults or {}
36
+ self.specified_packages = specified_packages or set()
37
+ self.dry_run = dry_run
38
+
39
+ def ask_initial(self, package_label: str) -> bool:
40
+ """Should we create an initial migration for the app?"""
41
+ # If it was specified on the command line, definitely true
42
+ if package_label in self.specified_packages:
43
+ return True
44
+ # Otherwise, we look to see if it has a migrations module
45
+ # without any Python files in it, apart from __init__.py.
46
+ # Packages from the new app template will have these; the Python
47
+ # file check will ensure we skip South ones.
48
+ try:
49
+ package_config = packages_registry.get_package_config(package_label)
50
+ except LookupError: # It's a fake app.
51
+ return self.defaults.get("ask_initial", False)
52
+ migrations_import_path, _ = MigrationLoader.migrations_module(
53
+ package_config.package_label
54
+ )
55
+ if migrations_import_path is None:
56
+ # It's an application with migrations disabled.
57
+ return self.defaults.get("ask_initial", False)
58
+ try:
59
+ migrations_module = importlib.import_module(migrations_import_path)
60
+ except ImportError:
61
+ return self.defaults.get("ask_initial", False)
62
+ else:
63
+ if file := getattr(migrations_module, "__file__", None):
64
+ filenames = os.listdir(os.path.dirname(file))
65
+ elif hasattr(migrations_module, "__path__"):
66
+ if len(migrations_module.__path__) > 1:
67
+ return False
68
+ filenames = os.listdir(list(migrations_module.__path__)[0])
69
+ return not any(x.endswith(".py") for x in filenames if x != "__init__.py")
70
+
71
+ def ask_not_null_addition(self, field_name: str, model_name: str) -> Any:
72
+ """Adding a NOT NULL field to a model."""
73
+ # None means quit
74
+ return None
75
+
76
+ def ask_not_null_alteration(self, field_name: str, model_name: str) -> Any:
77
+ """Changing a NULL field to NOT NULL."""
78
+ # None means quit
79
+ return None
80
+
81
+ def ask_rename(
82
+ self, model_name: str, old_name: str, new_name: str, field_instance: Field
83
+ ) -> bool:
84
+ """Was this field really renamed?"""
85
+ return self.defaults.get("ask_rename", False)
86
+
87
+ def ask_rename_model(self, old_model_state: Any, new_model_state: Any) -> bool:
88
+ """Was this model really renamed?"""
89
+ return self.defaults.get("ask_rename_model", False)
90
+
91
+ def ask_auto_now_add_addition(self, field_name: str, model_name: str) -> Any:
92
+ """Adding an auto_now_add field to a model."""
93
+ # None means quit
94
+ return None
95
+
96
+ def ask_unique_callable_default_addition(
97
+ self, field_name: str, model_name: str
98
+ ) -> Any:
99
+ """Adding a unique field with a callable default."""
100
+ # None means continue.
101
+ return None
102
+
103
+
104
+ class InteractiveMigrationQuestioner(MigrationQuestioner):
105
+ def __init__(
106
+ self,
107
+ defaults: dict[str, Any] | None = None,
108
+ specified_packages: set[str] | None = None,
109
+ dry_run: bool | None = None,
110
+ ) -> None:
111
+ super().__init__(
112
+ defaults=defaults, specified_packages=specified_packages, dry_run=dry_run
113
+ )
114
+
115
+ def _boolean_input(self, question: str, default: bool | None = None) -> bool:
116
+ return click.confirm(question, default=default)
117
+
118
+ def _choice_input(self, question: str, choices: list[str]) -> int:
119
+ choice_map = {str(i + 1): choice for i, choice in enumerate(choices)}
120
+ choice_map_str = "\n".join(
121
+ [f"{i}) {choice}" for i, choice in choice_map.items()]
122
+ )
123
+ choice = click.prompt(
124
+ f"{question}\n{choice_map_str}\nSelect an option",
125
+ type=click.Choice(choice_map.keys()),
126
+ )
127
+ return int(choice)
128
+
129
+ def _ask_default(self, default: str = "") -> Any:
130
+ """
131
+ Prompt for a default value.
132
+
133
+ The ``default`` argument allows providing a custom default value (as a
134
+ string) which will be shown to the user and used as the return value
135
+ if the user doesn't provide any other input.
136
+ """
137
+ click.echo("Please enter the default value as valid Python.")
138
+ if default:
139
+ click.echo(
140
+ f"Accept the default '{default}' by pressing 'Enter' or "
141
+ f"provide another value."
142
+ )
143
+ click.echo(
144
+ "The datetime and plain.utils.timezone modules are available, so "
145
+ "it is possible to provide e.g. timezone.now as a value."
146
+ )
147
+ click.echo("Type 'exit' to exit this prompt")
148
+ while True:
149
+ if default:
150
+ prompt = f"[default: {default}] >>> "
151
+ else:
152
+ prompt = ">>> "
153
+ code = click.prompt(prompt, default=default, show_default=False)
154
+ if not code and default:
155
+ code = default
156
+ if not code:
157
+ click.echo(
158
+ "Please enter some code, or 'exit' (without quotes) to exit."
159
+ )
160
+ elif code == "exit":
161
+ sys.exit(1)
162
+ else:
163
+ try:
164
+ return eval(code, {}, {"datetime": datetime, "timezone": timezone})
165
+ except (SyntaxError, NameError) as e:
166
+ click.echo(f"Invalid input: {e}")
167
+
168
+ def ask_not_null_addition(self, field_name: str, model_name: str) -> Any:
169
+ """Adding a NOT NULL field to a model."""
170
+ if not self.dry_run:
171
+ choice = self._choice_input(
172
+ f"It is impossible to add a non-nullable field '{field_name}' "
173
+ f"to {model_name} without specifying a default. This is "
174
+ f"because the database needs something to populate existing "
175
+ f"rows.\n"
176
+ f"Please select a fix:",
177
+ [
178
+ (
179
+ "Provide a one-off default now (will be set on all existing "
180
+ "rows with a null value for this column)"
181
+ ),
182
+ "Quit and manually define a default value in models.py.",
183
+ ],
184
+ )
185
+ if choice == 2:
186
+ sys.exit(3)
187
+ else:
188
+ return self._ask_default()
189
+ return None
190
+
191
+ def ask_not_null_alteration(self, field_name: str, model_name: str) -> Any:
192
+ """Changing a NULL field to NOT NULL."""
193
+ if not self.dry_run:
194
+ choice = self._choice_input(
195
+ f"It is impossible to change a nullable field '{field_name}' "
196
+ f"on {model_name} to non-nullable without providing a "
197
+ f"default. This is because the database needs something to "
198
+ f"populate existing rows.\n"
199
+ f"Please select a fix:",
200
+ [
201
+ (
202
+ "Provide a one-off default now (will be set on all existing "
203
+ "rows with a null value for this column)"
204
+ ),
205
+ "Ignore for now. Existing rows that contain NULL values "
206
+ "will have to be handled manually, for example with a "
207
+ "RunPython or RunSQL operation.",
208
+ "Quit and manually define a default value in models.py.",
209
+ ],
210
+ )
211
+ if choice == 2:
212
+ return NOT_PROVIDED
213
+ elif choice == 3:
214
+ sys.exit(3)
215
+ else:
216
+ return self._ask_default()
217
+ return None
218
+
219
+ def ask_rename(
220
+ self, model_name: str, old_name: str, new_name: str, field_instance: Field
221
+ ) -> bool:
222
+ """Was this field really renamed?"""
223
+ msg = "Was %s.%s renamed to %s.%s (a %s)?"
224
+ return self._boolean_input(
225
+ msg
226
+ % (
227
+ model_name,
228
+ old_name,
229
+ model_name,
230
+ new_name,
231
+ field_instance.__class__.__name__,
232
+ ),
233
+ default=False,
234
+ )
235
+
236
+ def ask_rename_model(self, old_model_state: Any, new_model_state: Any) -> bool:
237
+ """Was this model really renamed?"""
238
+ msg = "Was the model %s.%s renamed to %s?"
239
+ return self._boolean_input(
240
+ msg
241
+ % (
242
+ old_model_state.package_label,
243
+ old_model_state.name,
244
+ new_model_state.name,
245
+ ),
246
+ default=False,
247
+ )
248
+
249
+ def ask_auto_now_add_addition(self, field_name: str, model_name: str) -> Any:
250
+ """Adding an auto_now_add field to a model."""
251
+ if not self.dry_run:
252
+ choice = self._choice_input(
253
+ f"It is impossible to add the field '{field_name}' with "
254
+ f"'auto_now_add=True' to {model_name} without providing a "
255
+ f"default. This is because the database needs something to "
256
+ f"populate existing rows.\n",
257
+ [
258
+ "Provide a one-off default now which will be set on all "
259
+ "existing rows",
260
+ "Quit and manually define a default value in models.py.",
261
+ ],
262
+ )
263
+ if choice == 2:
264
+ sys.exit(3)
265
+ else:
266
+ return self._ask_default(default="timezone.now")
267
+ return None
268
+
269
+ def ask_unique_callable_default_addition(
270
+ self, field_name: str, model_name: str
271
+ ) -> Any:
272
+ """Adding a unique field with a callable default."""
273
+ if not self.dry_run:
274
+ choice = self._choice_input(
275
+ f"Callable default on unique field {model_name}.{field_name} "
276
+ f"will not generate unique values upon migrating.\n"
277
+ f"Please choose how to proceed:\n",
278
+ [
279
+ "Continue making this migration as the first step in "
280
+ "writing a manual migration to generate unique values.",
281
+ "Quit and edit field options in models.py.",
282
+ ],
283
+ )
284
+ if choice == 2:
285
+ sys.exit(3)
286
+ return None
287
+
288
+
289
+ class NonInteractiveMigrationQuestioner(MigrationQuestioner):
290
+ def __init__(
291
+ self,
292
+ defaults: dict[str, Any] | None = None,
293
+ specified_packages: set[str] | None = None,
294
+ dry_run: bool | None = None,
295
+ verbosity: int = 1,
296
+ log: Callable[[str], Any] | None = None,
297
+ ) -> None:
298
+ self.verbosity = verbosity
299
+ self.log = log
300
+ super().__init__(
301
+ defaults=defaults,
302
+ specified_packages=specified_packages,
303
+ dry_run=dry_run,
304
+ )
305
+
306
+ def log_lack_of_migration(
307
+ self, field_name: str, model_name: str, reason: str
308
+ ) -> None:
309
+ if self.verbosity > 0 and self.log:
310
+ self.log(
311
+ f"Field '{field_name}' on model '{model_name}' not migrated: {reason}."
312
+ )
313
+
314
+ def ask_not_null_addition(self, field_name: str, model_name: str) -> Any:
315
+ # We can't ask the user, so act like the user aborted.
316
+ self.log_lack_of_migration(
317
+ field_name,
318
+ model_name,
319
+ "it is impossible to add a non-nullable field without specifying a default",
320
+ )
321
+ sys.exit(3)
322
+
323
+ def ask_not_null_alteration(self, field_name: str, model_name: str) -> Any:
324
+ # We can't ask the user, so set as not provided.
325
+ if self.log:
326
+ self.log(
327
+ f"Field '{field_name}' on model '{model_name}' given a default of "
328
+ f"NOT PROVIDED and must be corrected."
329
+ )
330
+ return NOT_PROVIDED
331
+
332
+ def ask_auto_now_add_addition(self, field_name: str, model_name: str) -> Any:
333
+ # We can't ask the user, so act like the user aborted.
334
+ self.log_lack_of_migration(
335
+ field_name,
336
+ model_name,
337
+ "it is impossible to add a field with 'auto_now_add=True' without "
338
+ "specifying a default",
339
+ )
340
+ sys.exit(3)