python-corekit 0.1.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 (125) hide show
  1. corekit/__init__.py +0 -0
  2. corekit/api/__init__.py +9 -0
  3. corekit/api/handler.py +76 -0
  4. corekit/api/responses.py +40 -0
  5. corekit/api/routers.py +115 -0
  6. corekit/concurrency/__init__.py +9 -0
  7. corekit/concurrency/decorators.py +72 -0
  8. corekit/concurrency/thread_local.py +99 -0
  9. corekit/concurrency/worker.py +65 -0
  10. corekit/config/__init__.py +47 -0
  11. corekit/config/loader.py +153 -0
  12. corekit/config/settings.py +161 -0
  13. corekit/config/sources.py +125 -0
  14. corekit/connections/__init__.py +31 -0
  15. corekit/connections/connectable.py +212 -0
  16. corekit/connections/decorators.py +92 -0
  17. corekit/connections/redis/__init__.py +7 -0
  18. corekit/connections/redis/connection.py +239 -0
  19. corekit/connections/registry.py +80 -0
  20. corekit/connections/sql/__init__.py +10 -0
  21. corekit/connections/sql/connection.py +342 -0
  22. corekit/connections/sql/fields/__init__.py +7 -0
  23. corekit/connections/sql/fields/jsonb.py +67 -0
  24. corekit/connections/sql/migration/__init__.py +57 -0
  25. corekit/connections/sql/migration/base.py +40 -0
  26. corekit/connections/sql/migration/operations.py +416 -0
  27. corekit/connections/sql/migration/registry.py +166 -0
  28. corekit/connections/sql/migration/table.py +27 -0
  29. corekit/connections/sql/query.py +68 -0
  30. corekit/connections/sql/table.py +96 -0
  31. corekit/constants.py +45 -0
  32. corekit/crypto/__init__.py +1 -0
  33. corekit/crypto/constants.py +7 -0
  34. corekit/crypto/enum.py +11 -0
  35. corekit/crypto/hasher.py +89 -0
  36. corekit/data/__init__.py +81 -0
  37. corekit/data/dataset.py +340 -0
  38. corekit/data/expressions/__init__.py +46 -0
  39. corekit/data/expressions/comparison.py +252 -0
  40. corekit/data/expressions/expression.py +98 -0
  41. corekit/data/record.py +147 -0
  42. corekit/data/stats.py +157 -0
  43. corekit/decorators/__init__.py +2 -0
  44. corekit/decorators/exception_handling.py +43 -0
  45. corekit/decorators/warnings.py +35 -0
  46. corekit/docker/__init__.py +7 -0
  47. corekit/docker/watchdog.py +222 -0
  48. corekit/etl/__init__.py +44 -0
  49. corekit/etl/connection.py +44 -0
  50. corekit/etl/extract/__init__.py +0 -0
  51. corekit/etl/extract/extractor.py +48 -0
  52. corekit/etl/extract/schemas.py +18 -0
  53. corekit/etl/load/__init__.py +0 -0
  54. corekit/etl/load/loader.py +53 -0
  55. corekit/etl/load/schemas.py +33 -0
  56. corekit/etl/orchestrator.py +201 -0
  57. corekit/etl/schemas.py +22 -0
  58. corekit/etl/transform/__init__.py +0 -0
  59. corekit/etl/transform/schemas.py +15 -0
  60. corekit/etl/transform/transformer.py +28 -0
  61. corekit/events/__init__.py +38 -0
  62. corekit/events/enum.py +58 -0
  63. corekit/events/frames.py +51 -0
  64. corekit/events/models.py +23 -0
  65. corekit/events/publisher.py +75 -0
  66. corekit/events/reader.py +132 -0
  67. corekit/events/sse.py +109 -0
  68. corekit/events/websocket.py +97 -0
  69. corekit/exceptions/__init__.py +0 -0
  70. corekit/exceptions/base.py +45 -0
  71. corekit/exceptions/custom/__init__.py +0 -0
  72. corekit/exceptions/http/__init__.py +0 -0
  73. corekit/exceptions/http/exceptions.py +37 -0
  74. corekit/exceptions/types.py +17 -0
  75. corekit/files/__init__.py +25 -0
  76. corekit/files/base.py +117 -0
  77. corekit/files/enum.py +30 -0
  78. corekit/files/json.py +12 -0
  79. corekit/files/pickle.py +12 -0
  80. corekit/files/toml.py +43 -0
  81. corekit/http/__init__.py +0 -0
  82. corekit/http/client.py +176 -0
  83. corekit/http/exponential_backoff.py +100 -0
  84. corekit/http/response.py +12 -0
  85. corekit/log_monitor/__init__.py +23 -0
  86. corekit/log_monitor/constants.py +8 -0
  87. corekit/log_monitor/models.py +150 -0
  88. corekit/log_monitor/service.py +418 -0
  89. corekit/notifications/__init__.py +8 -0
  90. corekit/notifications/base.py +51 -0
  91. corekit/notifications/models.py +34 -0
  92. corekit/observability/__init__.py +21 -0
  93. corekit/observability/benchmarkable.py +12 -0
  94. corekit/observability/loggable.py +29 -0
  95. corekit/observability/timing/__init__.py +0 -0
  96. corekit/observability/timing/constants.py +1 -0
  97. corekit/observability/timing/split.py +20 -0
  98. corekit/observability/timing/timer.py +30 -0
  99. corekit/py.typed +0 -0
  100. corekit/registry/__init__.py +12 -0
  101. corekit/registry/registry.py +134 -0
  102. corekit/schemas/__init__.py +0 -0
  103. corekit/schemas/dataclasses/__init__.py +0 -0
  104. corekit/schemas/enum.py +49 -0
  105. corekit/schemas/models/__init__.py +0 -0
  106. corekit/schemas/models/arbitrary.py +11 -0
  107. corekit/schemas/models/date_models.py +18 -0
  108. corekit/schemas/pydantic/__init__.py +0 -0
  109. corekit/schemas/pydantic/fields.py +35 -0
  110. corekit/schemas/types.py +40 -0
  111. corekit/serialization/__init__.py +0 -0
  112. corekit/serialization/enum.py +21 -0
  113. corekit/serialization/serializable.py +42 -0
  114. corekit/serialization/serializer.py +179 -0
  115. corekit/utils/__init__.py +5 -0
  116. corekit/utils/ids.py +5 -0
  117. corekit/utils/raise_exc.py +8 -0
  118. corekit/utils/time.py +21 -0
  119. corekit/utils/validators.py +15 -0
  120. corekit/utils/void.py +8 -0
  121. python_corekit-0.1.0.dist-info/METADATA +417 -0
  122. python_corekit-0.1.0.dist-info/RECORD +125 -0
  123. python_corekit-0.1.0.dist-info/WHEEL +5 -0
  124. python_corekit-0.1.0.dist-info/licenses/LICENSE +21 -0
  125. python_corekit-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,67 @@
1
+ """
2
+ Columns that store a Pydantic model as JSON.
3
+
4
+ ``JSONBField`` round-trips a model through a JSON column, so a table can hold
5
+ structured data without a separate table and without hand-written encoding at
6
+ every call site::
7
+
8
+ class Profile(BaseModel):
9
+ theme: str = "dark"
10
+
11
+ class User(NamedTable, table=True):
12
+ id: str = Field(primary_key=True)
13
+ profile: Profile = JSONBField(Profile)
14
+
15
+ Reading ``user.profile`` gives a ``Profile``, not a dict.
16
+ """
17
+
18
+ from typing import Any
19
+
20
+ from pydantic import BaseModel
21
+ from sqlalchemy import Column
22
+ from sqlalchemy.types import JSON, TypeDecorator
23
+ from sqlmodel import Field
24
+
25
+ __all__ = ["JSONBField", "PydanticJSON"]
26
+
27
+
28
+ class PydanticJSON(TypeDecorator):
29
+ """
30
+ A JSON column that decodes into a Pydantic model on the way out.
31
+ """
32
+
33
+ impl = JSON
34
+ cache_ok = True
35
+
36
+ def __init__(self, pydantic_model: type[BaseModel], *args: Any, **kwargs: Any) -> None:
37
+ super().__init__(*args, **kwargs)
38
+ self.pydantic_model = pydantic_model
39
+
40
+ def process_bind_param(self, value: Any, dialect: Any) -> Any:
41
+ """
42
+ Encode a model, or an already-plain dict, for storage.
43
+ """
44
+ if value is None:
45
+ return None
46
+ if isinstance(value, BaseModel):
47
+ return value.model_dump(mode="json", exclude_unset=True)
48
+ if isinstance(value, dict):
49
+ return value
50
+ raise TypeError(
51
+ f"{type(self).__name__} expected {self.pydantic_model.__name__}, dict or None, got {type(value).__name__}"
52
+ )
53
+
54
+ def process_result_value(self, value: Any, dialect: Any) -> Any:
55
+ """
56
+ Decode stored JSON back into the model.
57
+ """
58
+ if value is None:
59
+ return None
60
+ return self.pydantic_model(**value)
61
+
62
+
63
+ def JSONBField(pydantic_model: type[BaseModel], **kwargs: Any) -> Any: # noqa: N802 - reads as a field constructor
64
+ """
65
+ Declare a column holding ``pydantic_model`` as JSON.
66
+ """
67
+ return Field(sa_column=Column(PydanticJSON(pydantic_model)), **kwargs)
@@ -0,0 +1,57 @@
1
+ """
2
+ Schema migrations with checksummed history.
3
+
4
+ Define a migration by subclassing ``Migration`` and listing its operations::
5
+
6
+ class AddUserFlag(Migration):
7
+ version = 1
8
+ name = "user_is_active"
9
+ description = "Adds the is_active flag to users."
10
+
11
+ def operations(self) -> list[Operation]:
12
+ return [AddColumn(table="user", column="is_active", dtype="BOOLEAN", default="true")]
13
+
14
+ Collect them from a package and run them::
15
+
16
+ MigrationRegistry.discover(my_app.migrations).run(conn)
17
+
18
+ Each migration's checksum is taken over what its operations *do*, not over the
19
+ SQL they render, so reformatting is safe but changing an already-applied
20
+ migration is caught at startup.
21
+ """
22
+
23
+ from corekit.connections.sql.migration.base import Migration
24
+ from corekit.connections.sql.migration.operations import (
25
+ AddColumn,
26
+ AlterColumnType,
27
+ CreateIndex,
28
+ DataOperation,
29
+ DropColumn,
30
+ DropColumnDefault,
31
+ DropIndex,
32
+ DropTable,
33
+ Operation,
34
+ RenameColumn,
35
+ RenameTable,
36
+ SetColumnDefault,
37
+ )
38
+ from corekit.connections.sql.migration.registry import MigrationChecksumError, MigrationRegistry
39
+ from corekit.connections.sql.migration.table import SchemaMigration
40
+
41
+ __all__ = [
42
+ "AddColumn",
43
+ "AlterColumnType",
44
+ "CreateIndex",
45
+ "DataOperation",
46
+ "DropColumn",
47
+ "DropColumnDefault",
48
+ "DropIndex",
49
+ "DropTable",
50
+ "Migration",
51
+ "MigrationChecksumError",
52
+ "MigrationRegistry",
53
+ "Operation",
54
+ "RenameColumn",
55
+ "RenameTable",
56
+ "SchemaMigration",
57
+ ]
@@ -0,0 +1,40 @@
1
+ """
2
+ Base class for all migrations.
3
+
4
+ A Migration is defined by:
5
+ - version: unique positive integer, determines execution order
6
+ - name: short human-readable label (used in logs and the DB table)
7
+ - description: longer explanation of what and why
8
+ - operations: ordered list of Operation instances to execute
9
+
10
+ The checksum is computed at class definition time from the canonical
11
+ representations of all operations. It is stored in the schema_migration
12
+ table when the migration is applied, and verified on every subsequent
13
+ startup. A mismatch means the migration was changed after being applied
14
+ and will cause a hard crash.
15
+ """
16
+
17
+ import hashlib
18
+ from abc import ABC, abstractmethod
19
+
20
+ from corekit.connections.sql.migration.operations import Operation
21
+
22
+
23
+ class Migration(ABC):
24
+ version: int
25
+ name: str
26
+ description: str
27
+
28
+ @abstractmethod
29
+ def operations(self) -> list[Operation]:
30
+ raise NotImplementedError
31
+
32
+ @property
33
+ def checksum(self) -> str:
34
+ canonical = "\n".join(op.canonical() for op in self.operations())
35
+ return hashlib.sha256(canonical.encode()).hexdigest()
36
+
37
+ def __repr__(self) -> str:
38
+ version = getattr(self, "version", None)
39
+ name = getattr(self, "name", None)
40
+ return f"{type(self).__name__}(version={version}, name={name!r})"
@@ -0,0 +1,416 @@
1
+ """
2
+ Migration operations.
3
+
4
+ Each Operation knows how to:
5
+ - render itself to SQL (for execution) OR execute Python logic
6
+ - render a canonical string (for checksumming)
7
+
8
+ The canonical string must be deterministic and change whenever the
9
+ operation's intent changes — it is intentionally human-readable so that
10
+ diffs are obvious in code review.
11
+
12
+ Operations use keyword-only arguments to keep call sites explicit and readable.
13
+
14
+ The SQL rendered here targets Postgres. Most of it is standard and works on
15
+ SQLite too, but SQLite cannot drop or alter a column in place, so those
16
+ operations will fail there.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from abc import ABC, abstractmethod
22
+ from dataclasses import dataclass
23
+ from typing import TYPE_CHECKING, Any, Callable
24
+
25
+ if TYPE_CHECKING:
26
+ from corekit.connections.sql.connection import SQLConnection
27
+
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # Constants
31
+ # ---------------------------------------------------------------------------
32
+ ALTER = "ALTER"
33
+ ADD = "ADD"
34
+ DROP = "DROP"
35
+
36
+ # Entities
37
+ COLUMN = "COLUMN"
38
+ INDEX = "INDEX"
39
+ TABLE = "TABLE"
40
+ TYPE = "TYPE"
41
+ USING = "USING"
42
+
43
+ DEFAULT = "DEFAULT"
44
+ IF_EXISTS = "IF EXISTS"
45
+ IF_NOT_EXISTS = "IF NOT EXISTS"
46
+ NOT_NULL = "NOT NULL"
47
+
48
+
49
+ # ---------------------------------------------------------------------------
50
+ # Base classes
51
+ # ---------------------------------------------------------------------------
52
+ class QueryBuilder:
53
+ """ """
54
+
55
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
56
+ self._query = ""
57
+
58
+ def __repr__(self) -> str:
59
+ return self.build()
60
+
61
+ def __str__(self) -> str:
62
+ return self.build()
63
+
64
+ @staticmethod
65
+ def _build_subcommand(command: str, entity: str, name: str | None = None) -> str:
66
+ """
67
+ Build a subcommand for the query
68
+ """
69
+ subcommand = f"{command} {entity} "
70
+ if name:
71
+ subcommand += f"{name} "
72
+ return subcommand
73
+
74
+ def append(self, part: str) -> QueryBuilder:
75
+ """
76
+ Append a string to the query
77
+ """
78
+ self._query += f"{part} "
79
+ return self
80
+
81
+ def extend(self, *parts: str) -> QueryBuilder:
82
+ """
83
+ Append multiple strings to the query
84
+ """
85
+ self._query += " ".join(parts) + " "
86
+ return self
87
+
88
+ def alter(self, entity: str, name: str | None = None) -> QueryBuilder:
89
+ """
90
+ Append ALTER command to the query
91
+ """
92
+ self._query += self._build_subcommand(ALTER, entity, name)
93
+ return self
94
+
95
+ def add(self, entity: str) -> QueryBuilder:
96
+ """
97
+ Append ADD command to the query
98
+ """
99
+ self._query += f"{ADD} {entity} "
100
+ return self
101
+
102
+ def drop(self, entity: str, name: str | None = None) -> QueryBuilder:
103
+ """
104
+ Append DROP command to the query
105
+ """
106
+ self._query += self._build_subcommand(DROP, entity, name)
107
+ return self
108
+
109
+ def not_null(self) -> QueryBuilder:
110
+ """
111
+ Append NOT NULL to the query
112
+ """
113
+ self._query += f"{NOT_NULL} "
114
+ return self
115
+
116
+ def if_not_exists(self) -> QueryBuilder:
117
+ """
118
+ Append IF NOT EXISTS to the query
119
+ """
120
+ self._query += f"{IF_NOT_EXISTS} "
121
+ return self
122
+
123
+ def if_exists(self) -> QueryBuilder:
124
+ """
125
+ Append IF EXISTS to the query
126
+ """
127
+ self._query += f"{IF_EXISTS} "
128
+ return self
129
+
130
+ def default(self, default: str) -> QueryBuilder:
131
+ self._query += f"{DEFAULT} {default} "
132
+ return self
133
+
134
+ def set_default(self, default: str) -> QueryBuilder:
135
+ self._query += f"SET {DEFAULT} {default} "
136
+ return self
137
+
138
+ def drop_default(self) -> QueryBuilder:
139
+ self._query += f"DROP {DEFAULT} "
140
+ return self
141
+
142
+ def type(self, name: str) -> QueryBuilder:
143
+ self._query += f"{TYPE} {name} "
144
+ return self
145
+
146
+ def using(self, name: str) -> QueryBuilder:
147
+ self._query += f"{USING} {name} "
148
+ return self
149
+
150
+ def build(self) -> str:
151
+ return self._query.strip()
152
+
153
+
154
+ class Operation(ABC):
155
+ """
156
+ Base class for all operations.
157
+ """
158
+
159
+ def _builder(self) -> QueryBuilder:
160
+ return QueryBuilder()
161
+
162
+ @abstractmethod
163
+ def to_sql(self) -> str:
164
+ """
165
+ Render the SQL to execute against the database
166
+
167
+ Each sub-class must implement this method and provide and example in the docstring.
168
+ Examples must use the format:
169
+
170
+ Example: <PRE-DEFINED_COMMAND> <table_name> <PRE-DEFINED_COMMAND> [IF NOT EXISTS] <column_name> <dtype>
171
+
172
+ Where:
173
+ - <PRE-DEFINED_COMMAND>: (all-caps inside <>) represents a pre-defined SQL command unique to the sub-class
174
+ - <*>: (lowercase inside <>) represents a placeholder variable added at runtime
175
+ - [*]: (anything inside []) represents an optional part of the command
176
+ """
177
+ raise NotImplementedError
178
+
179
+ @abstractmethod
180
+ def canonical(self) -> str:
181
+ """
182
+ Deterministic string representation used for checksumming
183
+ """
184
+ raise NotImplementedError
185
+
186
+ def __repr__(self) -> str:
187
+ return self.canonical()
188
+
189
+
190
+ # ---------------------------------------------------------------------------
191
+ # Column operations
192
+ # ---------------------------------------------------------------------------
193
+
194
+
195
+ @dataclass
196
+ class AddColumn(Operation):
197
+ """
198
+ Add a column to an existing table.
199
+
200
+ ``safe`` emits ``IF NOT EXISTS``, which Postgres accepts and SQLite does
201
+ not. Set it to False when targeting SQLite.
202
+ """
203
+
204
+ table: str
205
+ column: str
206
+ dtype: str
207
+ safe: bool = True
208
+ nullable: bool = True
209
+ default: str | None = None
210
+
211
+ def to_sql(self) -> str:
212
+ """
213
+ Example: ALTER TABLE <table_name> ADD COLUMN [IF NOT EXISTS] <column_name> <dtype>
214
+ """
215
+ builder = self._builder().alter(TABLE, self.table).add(COLUMN)
216
+ if self.safe:
217
+ builder.if_not_exists()
218
+
219
+ builder.append(self.column).append(self.dtype)
220
+ if self.default is not None:
221
+ builder.default(self.default)
222
+
223
+ if not self.nullable:
224
+ builder.not_null()
225
+
226
+ return builder.build()
227
+
228
+ def canonical(self) -> str:
229
+ return (
230
+ f"AddColumn(table={self.table}, column={self.column}, dtype={self.dtype}, "
231
+ f"nullable={self.nullable}, default={self.default})"
232
+ )
233
+
234
+
235
+ @dataclass
236
+ class DropColumn(Operation):
237
+ table: str
238
+ column: str
239
+
240
+ def to_sql(self) -> str:
241
+ """
242
+ Example: ALTER TABLE <table_name> DROP COLUMN [IF EXISTS] <column_name>
243
+ """
244
+ return self._builder().alter(TABLE, self.table).drop(COLUMN).if_exists().append(self.column).build()
245
+
246
+ def canonical(self) -> str:
247
+ return f"DropColumn(table={self.table}, column={self.column})"
248
+
249
+
250
+ @dataclass
251
+ class AlterColumnType(Operation):
252
+ table: str
253
+ column: str
254
+ new_dtype: str
255
+ using: str | None = None
256
+
257
+ def to_sql(self) -> str:
258
+ """
259
+ Example: ALTER TABLE <table_name> ALTER COLUMN <column_name> TYPE <new_dtype> [USING <using>]
260
+ """
261
+ builder = self._builder().alter(TABLE, self.table).alter(COLUMN, self.column).type(self.new_dtype)
262
+ if self.using:
263
+ builder.using(self.using)
264
+
265
+ return builder.build()
266
+
267
+ def canonical(self) -> str:
268
+ return (
269
+ f"AlterColumnType(table={self.table}, column={self.column}, new_dtype={self.new_dtype}, using={self.using})"
270
+ )
271
+
272
+
273
+ @dataclass
274
+ class SetColumnDefault(Operation):
275
+ table: str
276
+ column: str
277
+ default: str
278
+
279
+ def to_sql(self) -> str:
280
+ """
281
+ Example: ALTER TABLE <table_name> ALTER COLUMN <column_name> SET DEFAULT <default>
282
+ """
283
+ return self._builder().alter(TABLE, self.table).alter(COLUMN, self.column).set_default(self.default).build()
284
+
285
+ def canonical(self) -> str:
286
+ return f"SetColumnDefault(table={self.table}, column={self.column}, default={self.default})"
287
+
288
+
289
+ @dataclass
290
+ class DropColumnDefault(Operation):
291
+ table: str
292
+ column: str
293
+
294
+ def to_sql(self) -> str:
295
+ """
296
+ Example: ALTER TABLE <table_name> ALTER COLUMN <column_name> DROP DEFAULT
297
+ """
298
+ return self._builder().alter(TABLE, self.table).alter(COLUMN, self.column).drop_default().build()
299
+
300
+ def canonical(self) -> str:
301
+ return f"DropColumnDefault(table={self.table}, column={self.column})"
302
+
303
+
304
+ # ---------------------------------------------------------------------------
305
+ # Index operations
306
+ # ---------------------------------------------------------------------------
307
+
308
+
309
+ @dataclass
310
+ class CreateIndex(Operation):
311
+ index_name: str
312
+ table: str
313
+ columns: list[str]
314
+ unique: bool = False
315
+
316
+ def to_sql(self) -> str:
317
+ unique_clause = "UNIQUE " if self.unique else ""
318
+ cols = ", ".join(self.columns)
319
+ return f"CREATE {unique_clause}INDEX IF NOT EXISTS {self.index_name} ON {self.table} ({cols})"
320
+
321
+ def canonical(self) -> str:
322
+ return f"CreateIndex(name={self.index_name}, table={self.table}, columns={self.columns}, unique={self.unique})"
323
+
324
+
325
+ @dataclass
326
+ class DropIndex(Operation):
327
+ index_name: str
328
+
329
+ def to_sql(self) -> str:
330
+ return f"DROP INDEX IF EXISTS {self.index_name}"
331
+
332
+ def canonical(self) -> str:
333
+ return f"DropIndex(name={self.index_name})"
334
+
335
+
336
+ # ---------------------------------------------------------------------------
337
+ # Table operations
338
+ # ---------------------------------------------------------------------------
339
+
340
+
341
+ @dataclass
342
+ class DropTable(Operation):
343
+ table: str
344
+ cascade: bool = False
345
+
346
+ def to_sql(self) -> str:
347
+ cascade_clause = " CASCADE" if self.cascade else ""
348
+ return f"DROP TABLE IF EXISTS {self.table}{cascade_clause}"
349
+
350
+ def canonical(self) -> str:
351
+ return f"DropTable(table={self.table}, cascade={self.cascade})"
352
+
353
+
354
+ @dataclass
355
+ class RenameTable(Operation):
356
+ old_name: str
357
+ new_name: str
358
+
359
+ def to_sql(self) -> str:
360
+ return f"ALTER TABLE {self.old_name} RENAME TO {self.new_name}"
361
+
362
+ def canonical(self) -> str:
363
+ return f"RenameTable(old={self.old_name}, new={self.new_name})"
364
+
365
+
366
+ @dataclass
367
+ class RenameColumn(Operation):
368
+ table: str
369
+ old_name: str
370
+ new_name: str
371
+
372
+ def to_sql(self) -> str:
373
+ return f"ALTER TABLE {self.table} RENAME COLUMN {self.old_name} TO {self.new_name}"
374
+
375
+ def canonical(self) -> str:
376
+ return f"RenameColumn(table={self.table}, old={self.old_name}, new={self.new_name})"
377
+
378
+
379
+ # ---------------------------------------------------------------------------
380
+ # Data operations
381
+ # ---------------------------------------------------------------------------
382
+
383
+
384
+ @dataclass
385
+ class DataOperation(Operation):
386
+ """
387
+ Executes a Python function for data migrations (seeding, transformations).
388
+
389
+ The function receives a SQLConnection and should use ORM methods to
390
+ read/write data. The `name` is used for checksumming - changing it will
391
+ invalidate the checksum, so choose a stable, descriptive name.
392
+
393
+ Example:
394
+ DataOperation(
395
+ name="seed_user_permissions_v1",
396
+ func=seed_permissions_from_roles,
397
+ )
398
+
399
+ IMPORTANT: It is not appropriate to use DataOperation in place of other operations. This is
400
+ reserved for the most complex operations that CANNOT be expressed otherwise.
401
+ """
402
+
403
+ name: str
404
+ func: Callable[[SQLConnection], None]
405
+
406
+ def execute(self, conn: SQLConnection) -> None:
407
+ """
408
+ Execute the data operation with the given connection
409
+ """
410
+ self.func(conn)
411
+
412
+ def to_sql(self) -> str:
413
+ raise NotImplementedError("DataOperation does not produce SQL; use execute() instead")
414
+
415
+ def canonical(self) -> str:
416
+ return f"DataOperation(name={self.name})"