snake-orm 0.1.0b1__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 (198) hide show
  1. snake_orm-0.1.0b1.dist-info/METADATA +366 -0
  2. snake_orm-0.1.0b1.dist-info/RECORD +198 -0
  3. snake_orm-0.1.0b1.dist-info/WHEEL +4 -0
  4. snake_orm-0.1.0b1.dist-info/entry_points.txt +2 -0
  5. snake_orm-0.1.0b1.dist-info/licenses/LICENSE +21 -0
  6. snakeorm/__init__.py +214 -0
  7. snakeorm/advisor.py +109 -0
  8. snakeorm/cli/__init__.py +3 -0
  9. snakeorm/cli/app.py +992 -0
  10. snakeorm/cli/discovery.py +179 -0
  11. snakeorm/cli/hooks.py +86 -0
  12. snakeorm/compiler/__init__.py +3 -0
  13. snakeorm/compiler/compiler.py +330 -0
  14. snakeorm/connection.py +228 -0
  15. snakeorm/contrib/__init__.py +17 -0
  16. snakeorm/contrib/asgi.py +192 -0
  17. snakeorm/contrib/config.py +264 -0
  18. snakeorm/contrib/deliver.py +256 -0
  19. snakeorm/contrib/django.py +245 -0
  20. snakeorm/contrib/sidecar.py +44 -0
  21. snakeorm/contrib/wsgi.py +176 -0
  22. snakeorm/core/__init__.py +1 -0
  23. snakeorm/core/config.py +173 -0
  24. snakeorm/core/converters.py +172 -0
  25. snakeorm/core/exceptions.py +175 -0
  26. snakeorm/core/placement.py +15 -0
  27. snakeorm/core/sentinels.py +37 -0
  28. snakeorm/core/signals.py +114 -0
  29. snakeorm/debug/__init__.py +42 -0
  30. snakeorm/debug/assets/css/panel.css +616 -0
  31. snakeorm/debug/assets/css/snake_orm_app.css +293 -0
  32. snakeorm/debug/assets/img/icon.svg +5 -0
  33. snakeorm/debug/assets/js/history.js +425 -0
  34. snakeorm/debug/assets/js/language.js +283 -0
  35. snakeorm/debug/assets/js/panel.js +241 -0
  36. snakeorm/debug/assets/js/snake_orm_app.js +82 -0
  37. snakeorm/debug/assets/pages/cli.html +28 -0
  38. snakeorm/debug/assets/pages/config.html +34 -0
  39. snakeorm/debug/assets/pages/database_first.html +29 -0
  40. snakeorm/debug/assets/pages/help.html +21 -0
  41. snakeorm/debug/assets/pages/history.html +27 -0
  42. snakeorm/debug/capture.py +234 -0
  43. snakeorm/debug/channel.py +131 -0
  44. snakeorm/debug/collector.py +144 -0
  45. snakeorm/debug/config.py +149 -0
  46. snakeorm/debug/html.py +578 -0
  47. snakeorm/debug/matrix.py +60 -0
  48. snakeorm/debug/origin.py +42 -0
  49. snakeorm/debug/otel/__init__.py +68 -0
  50. snakeorm/debug/otel/context.py +43 -0
  51. snakeorm/debug/otel/exporter.py +277 -0
  52. snakeorm/debug/otel/payload.py +97 -0
  53. snakeorm/debug/otel/report.py +29 -0
  54. snakeorm/debug/otel/spans.py +393 -0
  55. snakeorm/debug/otel/summary.py +59 -0
  56. snakeorm/debug/record.py +54 -0
  57. snakeorm/debug/report.py +311 -0
  58. snakeorm/debug/testing.py +23 -0
  59. snakeorm/decorators/__init__.py +13 -0
  60. snakeorm/decorators/abstract.py +26 -0
  61. snakeorm/decorators/db_first.py +104 -0
  62. snakeorm/decorators/function.py +28 -0
  63. snakeorm/decorators/model.py +268 -0
  64. snakeorm/decorators/polymorphic.py +140 -0
  65. snakeorm/decorators/result.py +167 -0
  66. snakeorm/decorators/row.py +90 -0
  67. snakeorm/decorators/trigger.py +47 -0
  68. snakeorm/decorators/view.py +149 -0
  69. snakeorm/dialects/__init__.py +6 -0
  70. snakeorm/dialects/base.py +373 -0
  71. snakeorm/dialects/capabilities.py +571 -0
  72. snakeorm/dialects/literals.py +46 -0
  73. snakeorm/dialects/matrix.py +384 -0
  74. snakeorm/dialects/mysql.py +519 -0
  75. snakeorm/dialects/postgres.py +504 -0
  76. snakeorm/dialects/sqlite.py +417 -0
  77. snakeorm/drivers/__init__.py +18 -0
  78. snakeorm/drivers/asyncbase.py +73 -0
  79. snakeorm/drivers/asyncdecorators.py +244 -0
  80. snakeorm/drivers/asyncpool.py +228 -0
  81. snakeorm/drivers/asyncpsycopg.py +124 -0
  82. snakeorm/drivers/asyncpymysql.py +50 -0
  83. snakeorm/drivers/asyncsqlite.py +30 -0
  84. snakeorm/drivers/base.py +76 -0
  85. snakeorm/drivers/failures.py +181 -0
  86. snakeorm/drivers/logging.py +167 -0
  87. snakeorm/drivers/pool.py +352 -0
  88. snakeorm/drivers/psycopg.py +188 -0
  89. snakeorm/drivers/pymysql.py +176 -0
  90. snakeorm/drivers/savepoints.py +46 -0
  91. snakeorm/drivers/sqlite.py +170 -0
  92. snakeorm/drivers/threaded.py +143 -0
  93. snakeorm/drivers/timeout.py +87 -0
  94. snakeorm/dto/__init__.py +55 -0
  95. snakeorm/dto/read.py +231 -0
  96. snakeorm/dto/region.py +326 -0
  97. snakeorm/dto/resolve.py +354 -0
  98. snakeorm/dto/spec.py +201 -0
  99. snakeorm/expressions/__init__.py +88 -0
  100. snakeorm/expressions/conditional.py +178 -0
  101. snakeorm/expressions/expression.py +810 -0
  102. snakeorm/expressions/functions.py +71 -0
  103. snakeorm/expressions/keys.py +299 -0
  104. snakeorm/expressions/paths.py +43 -0
  105. snakeorm/expressions/scalar.py +293 -0
  106. snakeorm/expressions/window.py +248 -0
  107. snakeorm/fields/__init__.py +68 -0
  108. snakeorm/fields/check.py +79 -0
  109. snakeorm/fields/column.py +316 -0
  110. snakeorm/fields/enum.py +64 -0
  111. snakeorm/fields/index.py +46 -0
  112. snakeorm/fields/relationship.py +738 -0
  113. snakeorm/fields/typed.py +736 -0
  114. snakeorm/helpers/__init__.py +1 -0
  115. snakeorm/helpers/annotations.py +21 -0
  116. snakeorm/helpers/inheritance.py +41 -0
  117. snakeorm/helpers/pyliteral.py +52 -0
  118. snakeorm/helpers/pytype.py +50 -0
  119. snakeorm/introspection/__init__.py +13 -0
  120. snakeorm/introspection/base.py +30 -0
  121. snakeorm/introspection/drift.py +78 -0
  122. snakeorm/introspection/models.py +633 -0
  123. snakeorm/introspection/mysql.py +367 -0
  124. snakeorm/introspection/postgres.py +397 -0
  125. snakeorm/introspection/sqlite.py +217 -0
  126. snakeorm/introspection/unsupported.py +94 -0
  127. snakeorm/linker/__init__.py +3 -0
  128. snakeorm/linker/linker.py +521 -0
  129. snakeorm/metadata/__init__.py +41 -0
  130. snakeorm/metadata/check.py +37 -0
  131. snakeorm/metadata/column.py +201 -0
  132. snakeorm/metadata/enum_storage.py +33 -0
  133. snakeorm/metadata/fk_action.py +19 -0
  134. snakeorm/metadata/foreign_key.py +26 -0
  135. snakeorm/metadata/index.py +43 -0
  136. snakeorm/metadata/index_method.py +19 -0
  137. snakeorm/metadata/int_size.py +20 -0
  138. snakeorm/metadata/json_storage.py +21 -0
  139. snakeorm/metadata/polymorphic.py +30 -0
  140. snakeorm/metadata/primary_key.py +23 -0
  141. snakeorm/metadata/relationship.py +60 -0
  142. snakeorm/metadata/relationship_kind.py +43 -0
  143. snakeorm/metadata/routine.py +26 -0
  144. snakeorm/metadata/server_default.py +20 -0
  145. snakeorm/metadata/table.py +97 -0
  146. snakeorm/metadata/table_kind.py +23 -0
  147. snakeorm/metadata/trigger.py +62 -0
  148. snakeorm/metadata/type_params.py +348 -0
  149. snakeorm/migration/__init__.py +88 -0
  150. snakeorm/migration/asyncrunner.py +168 -0
  151. snakeorm/migration/autodetect.py +96 -0
  152. snakeorm/migration/ddl.py +1172 -0
  153. snakeorm/migration/diff.py +701 -0
  154. snakeorm/migration/loader.py +77 -0
  155. snakeorm/migration/operations.py +952 -0
  156. snakeorm/migration/planner.py +78 -0
  157. snakeorm/migration/realize.py +420 -0
  158. snakeorm/migration/renames.py +153 -0
  159. snakeorm/migration/render.py +1095 -0
  160. snakeorm/migration/runner.py +314 -0
  161. snakeorm/migration/squash.py +60 -0
  162. snakeorm/migration/state.py +107 -0
  163. snakeorm/model.py +162 -0
  164. snakeorm/py.typed +0 -0
  165. snakeorm/query/__init__.py +9 -0
  166. snakeorm/query/compound.py +288 -0
  167. snakeorm/query/join_kind.py +19 -0
  168. snakeorm/query/joined.py +143 -0
  169. snakeorm/query/query.py +1001 -0
  170. snakeorm/query/recursive.py +244 -0
  171. snakeorm/registry/__init__.py +5 -0
  172. snakeorm/registry/by_module.py +163 -0
  173. snakeorm/registry/registry.py +166 -0
  174. snakeorm/session/__init__.py +8 -0
  175. snakeorm/session/asyncsession.py +638 -0
  176. snakeorm/session/coercion.py +306 -0
  177. snakeorm/session/factory.py +29 -0
  178. snakeorm/session/guards.py +269 -0
  179. snakeorm/session/isolation.py +23 -0
  180. snakeorm/session/mapper.py +213 -0
  181. snakeorm/session/planning.py +815 -0
  182. snakeorm/session/retry.py +96 -0
  183. snakeorm/session/session.py +871 -0
  184. snakeorm/session/shared.py +355 -0
  185. snakeorm/sql/__init__.py +18 -0
  186. snakeorm/sql/adapt.py +104 -0
  187. snakeorm/sql/aggregate.py +232 -0
  188. snakeorm/sql/condition.py +359 -0
  189. snakeorm/sql/delete.py +46 -0
  190. snakeorm/sql/insert.py +149 -0
  191. snakeorm/sql/joins.py +158 -0
  192. snakeorm/sql/pk_subquery.py +54 -0
  193. snakeorm/sql/refs.py +19 -0
  194. snakeorm/sql/resolver.py +28 -0
  195. snakeorm/sql/select.py +175 -0
  196. snakeorm/sql/update.py +82 -0
  197. snakeorm/sql/value.py +552 -0
  198. snakeorm/times.py +276 -0
@@ -0,0 +1,168 @@
1
+ """Asynchronous migration runtime: same logic, the other execution seam.
2
+
3
+ SQL generation has no "colour" (`up_sql()` does not execute), so diff, DDL, `realize()` and the
4
+ operations are reused as they are; only who talks to the DB changes. The real difference: DATA
5
+ migrations do NOT run here — `RunPython` receives a SYNCHRONOUS `SnakeSession`, which would block the
6
+ event loop — and it stops dead rather than marking them applied with the data never executed.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from snakeorm.core.placement import DEFAULT_SCHEMA
12
+ from snakeorm.dialects import SnakeDialect
13
+ from snakeorm.drivers.asyncbase import AsyncDriver
14
+ from snakeorm.core.exceptions import SnakeMigrationError
15
+ from snakeorm.migration.operations import (
16
+ SnakeDataOperation,
17
+ SnakeMigrationOperation,
18
+ SnakeOperation,
19
+ )
20
+ from snakeorm.migration.realize import realize
21
+ from snakeorm.migration.runner import (
22
+ Migration,
23
+ explain_rebuild_failure,
24
+ squash_already_done,
25
+ tracking_table_ddl,
26
+ )
27
+ from snakeorm.sql.refs import qualified
28
+
29
+ _TRACKING_TABLE = "snake_migrations"
30
+ _SCHEMA = DEFAULT_SCHEMA
31
+
32
+
33
+ class AsyncMigrationRunner:
34
+ """Applies/reverts migrations over an `AsyncDriver`, with the same tracking as the sync one."""
35
+
36
+ def __init__(self, driver: AsyncDriver, dialect: SnakeDialect) -> None:
37
+ self._driver = driver
38
+ self._dialect = dialect
39
+
40
+ async def ensure_tracking_table(self) -> None:
41
+ """Creates the tracking table if it does not exist (idempotent)."""
42
+ await self._driver.execute(tracking_table_ddl(self._dialect), ())
43
+ await self._driver.commit()
44
+
45
+ async def applied_versions(self) -> set[str]:
46
+ """Returns the set of versions already applied."""
47
+ version = self._dialect.quote_ident("version")
48
+ rows = await self._driver.fetch_all(
49
+ f"SELECT {version} FROM {self._table_ref()}", ()
50
+ )
51
+ return {str(row[0]) for row in rows}
52
+
53
+ async def apply(self, migrations: list[Migration]) -> list[str]:
54
+ """Applies the pending migrations in order. Returns the versions applied just now.
55
+
56
+ Same semantics as the synchronous one: idempotent, atomic per migration with transactional
57
+ DDL, and a hard stop if a squash replaces a history that was applied HALFWAY.
58
+ """
59
+ await self.ensure_tracking_table()
60
+ already = await self.applied_versions()
61
+ replaced = {
62
+ version for migration in migrations for version in migration.replaces
63
+ }
64
+ newly: list[str] = []
65
+ for migration in migrations:
66
+ if migration.version in already or migration.version in replaced:
67
+ continue
68
+ if migration.replaces and squash_already_done(migration, already):
69
+ await self._record(migration.version)
70
+ await self._driver.commit()
71
+ newly.append(migration.version)
72
+ continue
73
+ if self._dialect.supports_transactional_ddl:
74
+ await self._apply_atomic(migration)
75
+ else:
76
+ await self._apply_stepwise(migration)
77
+ newly.append(migration.version)
78
+ return newly
79
+
80
+ async def rollback(self, migration: Migration) -> None:
81
+ """Reverts a migration (operations in reverse order) and deletes its record."""
82
+ for operation in reversed(realize(migration.operations, self._dialect)):
83
+ await self._backward(operation)
84
+ await self._unrecord(migration.version)
85
+ await self._driver.commit()
86
+
87
+ async def _apply_atomic(self, migration: Migration) -> None:
88
+ """Applies a migration wrapped in the engine's transaction: all or nothing."""
89
+ plan = realize(migration.operations, self._dialect)
90
+ try:
91
+ for operation in plan:
92
+ await self._forward(operation)
93
+ await self._record(migration.version)
94
+ await self._driver.commit()
95
+ except Exception as error:
96
+ await self._driver.rollback()
97
+ # The SAME sentence the synchronous runner gives, out of the same function: a rebuild
98
+ # that the deferred keys could not cover fails identically here, and this repository has
99
+ # already paid for one complaint told two ways.
100
+ explained = explain_rebuild_failure(plan, self._dialect, error)
101
+ if explained is not None:
102
+ raise explained from error
103
+ raise
104
+
105
+ async def _apply_stepwise(self, migration: Migration) -> None:
106
+ """Applies operation by operation (engine without transactional DDL); on error, reports how many."""
107
+ applied = 0
108
+ plan = realize(migration.operations, self._dialect)
109
+ try:
110
+ for operation in plan:
111
+ await self._forward(operation)
112
+ applied += 1
113
+ await self._record(migration.version)
114
+ await self._driver.commit()
115
+ except SnakeMigrationError:
116
+ raise
117
+ except Exception as error:
118
+ raise SnakeMigrationError(
119
+ f"Migration '{migration.version}' failed after applying {applied} of "
120
+ f"{len(plan)} operation(s): {error}. The engine does not support "
121
+ f"transactional DDL, so the database was left half-way: review it and "
122
+ f"fix it by hand."
123
+ ) from error
124
+
125
+ async def _forward(self, operation: SnakeMigrationOperation) -> None:
126
+ """Emits a schema operation's DDL; a data one cannot be executed here."""
127
+ _reject_data_operation(operation)
128
+ if isinstance(operation, SnakeOperation):
129
+ for sql in operation.up_sql(self._dialect):
130
+ await self._driver.execute(sql, ())
131
+
132
+ async def _backward(self, operation: SnakeMigrationOperation) -> None:
133
+ """Emits a schema operation's reverse DDL."""
134
+ _reject_data_operation(operation)
135
+ if isinstance(operation, SnakeOperation):
136
+ for sql in operation.down_sql(self._dialect):
137
+ await self._driver.execute(sql, ())
138
+
139
+ def _table_ref(self) -> str:
140
+ """Reference to the tracking table, qualified only if the engine has schemas."""
141
+ return qualified(_SCHEMA, _TRACKING_TABLE, self._dialect)
142
+
143
+ async def _record(self, version: str) -> None:
144
+ column = self._dialect.quote_ident("version")
145
+ await self._driver.execute(
146
+ f"INSERT INTO {self._table_ref()} ({column}) "
147
+ f"VALUES ({self._dialect.placeholder(1)})",
148
+ (version,),
149
+ )
150
+
151
+ async def _unrecord(self, version: str) -> None:
152
+ column = self._dialect.quote_ident("version")
153
+ await self._driver.execute(
154
+ f"DELETE FROM {self._table_ref()} WHERE {column} = {self._dialect.placeholder(1)}",
155
+ (version,),
156
+ )
157
+
158
+
159
+ def _reject_data_operation(operation: SnakeMigrationOperation) -> None:
160
+ """Stops dead at a DATA migration: skipping it would mark it applied with the data never run,
161
+ and that never raises an error, only wrong rows months later."""
162
+ if isinstance(operation, SnakeDataOperation):
163
+ raise SnakeMigrationError(
164
+ "The async runner cannot execute a data operation (`RunPython`): its "
165
+ "`forward`/`backward` take a SYNCHRONOUS `SnakeSession`, and running it here would "
166
+ "block the event loop for the whole migration. Apply that migration with "
167
+ "`MigrationRunner` (synchronous) over a synchronous driver; the schema ones do go here."
168
+ )
@@ -0,0 +1,96 @@
1
+ """Django-style autogen: rebuilds the state by replaying migrations and diffs it.
2
+
3
+ `replay` applies the operations of every migration onto an empty SchemaState -> the state the
4
+ schema SHOULD have according to the history. `autodetect` diffs that against the current
5
+ metadata (the code-first source of truth) and returns the operations for the new migration.
6
+ Neither snapshots nor reflection: the migration history IS the record of the state.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections.abc import Iterable
12
+
13
+ from snakeorm.metadata import SnakeRoutineInfo, SnakeTableInfo, SnakeTriggerInfo
14
+ from snakeorm.migration.diff import diff_routines, diff_schema, diff_triggers
15
+ from snakeorm.migration.operations import SnakeOperation
16
+ from snakeorm.migration.runner import Migration
17
+ from snakeorm.migration.state import SchemaState
18
+ from snakeorm.registry import SnakeRegistry, registry
19
+
20
+
21
+ def current_schema(
22
+ reg: SnakeRegistry = registry,
23
+ *,
24
+ database: str | None = None,
25
+ include_unmanaged: bool = False,
26
+ ) -> list[SnakeTableInfo]:
27
+ """The current metadata (code-first source of truth): the tables of the registered models.
28
+
29
+ Excludes MIRROR models (`@snake_db_first`): they are not a source of truth, and autogen must not
30
+ touch them. With `database`, only those of THAT connection (which avoids creating every table in
31
+ every DB). `include_unmanaged=True` returns them too: drift detection uses it (comparing the
32
+ mirror against the DB), but MIGRATIONS never touch them (the default behaviour).
33
+ """
34
+ return [
35
+ table
36
+ for model in reg.models()
37
+ if (table := reg.table_of(model)) is not None
38
+ and (include_unmanaged or table.is_managed)
39
+ and (database is None or table.database == database)
40
+ # A polymorphic CHILD contributes no table: it shares its base's (which already carries
41
+ # the whole hierarchy after `snake_link()`). Including it would duplicate the `CREATE TABLE`
42
+ # and produce phantom drift on every `makemigrations`.
43
+ and not table.is_polymorphic_child
44
+ ]
45
+
46
+
47
+ def current_routines(reg: SnakeRegistry = registry) -> list[SnakeRoutineInfo]:
48
+ """The desired routines (code-first source of truth): those declared with `snake_function`."""
49
+ return list(reg.routines())
50
+
51
+
52
+ def current_triggers(reg: SnakeRegistry = registry) -> list[SnakeTriggerInfo]:
53
+ """The DESIRED triggers: those declared with `snake_trigger(...)`."""
54
+ return list(reg.triggers())
55
+
56
+
57
+ def replay(migrations: Iterable[Migration]) -> SchemaState:
58
+ """Rebuilds the schema state by applying the migrations' operations in order."""
59
+ state = SchemaState()
60
+ for migration in migrations:
61
+ for operation in migration.operations:
62
+ operation.apply_to_state(state)
63
+ return state
64
+
65
+
66
+ def autodetect(
67
+ migrations: Iterable[Migration],
68
+ current: Iterable[SnakeTableInfo],
69
+ routines: Iterable[SnakeRoutineInfo] | None = None,
70
+ triggers: Iterable[SnakeTriggerInfo] | None = None,
71
+ ) -> list[SnakeOperation]:
72
+ """Diffs the state replayed from the history against the current metadata -> new operations.
73
+
74
+ Resolves FK targets by model name through the global registry. Global order: tables -> FKs ->
75
+ views (topological) -> functions -> triggers (each one depends on the ones before it).
76
+ """
77
+ state = replay(migrations)
78
+ desired_routines = current_routines() if routines is None else list(routines)
79
+ # The state's triggers go in with the tables, and NOT because they are diffed here — that is
80
+ # `diff_triggers`, further down. A `RebuildTable` on an engine without `ALTER TABLE ADD
81
+ # CONSTRAINT` drops the table it remakes, which takes its triggers with it, and the operation
82
+ # has to carry them because `SnakeTableInfo` has no field for one. This is the only caller that
83
+ # has them, so this is where they are handed over.
84
+ schema_ops = diff_schema(
85
+ state.tables(),
86
+ current,
87
+ registry.table_by_name,
88
+ registry.table_by_qualified,
89
+ triggers=state.triggers(),
90
+ )
91
+ routine_ops = diff_routines(state.routines(), desired_routines)
92
+ # TRIGGERS go last: a Postgres one executes a FUNCTION, which has to exist beforehand.
93
+ trigger_ops = diff_triggers(
94
+ state.triggers(), current_triggers() if triggers is None else list(triggers)
95
+ )
96
+ return [*schema_ops, *routine_ops, *trigger_ops]