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,213 @@
1
+ """Row hydration: the HOTTEST path of the ORM, with the plan compiled ONCE per table.
2
+
3
+ Which attribute, which converter (or none) and in which position is FIXED per table, so it gets
4
+ compiled once and reused instead of being resolved per row. It writes straight into the descriptor's
5
+ storage key WITHOUT calling the constructor (exactly what its `__set__` does): validating arguments
6
+ and applying defaults is pointless work when the values come from the database itself.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections.abc import Callable, Sequence
12
+ from typing import TypeVar
13
+
14
+ from snakeorm.registry import registry_of
15
+ from snakeorm.metadata import SnakeTableInfo
16
+ from snakeorm.core.sentinels import NOT_LOADED
17
+ from snakeorm.session.coercion import converter_for
18
+
19
+ T = TypeVar("T")
20
+
21
+ _Instruction = tuple[str, Callable[[object], object] | None]
22
+ """(storage key, converter, or None if the value passes through as is)."""
23
+
24
+ _Dispatch = tuple[int, dict[str, type]] | None
25
+ """(position of the discriminator in the row, value -> subclass). `None` on a normal table.
26
+
27
+ Resolved HERE and not per row: the position and the map are fixed per table (resolving them per row
28
+ cost 55% more on the hot path).
29
+ """
30
+
31
+ _Entry = tuple[SnakeTableInfo, tuple[_Instruction, ...], tuple[int, ...], _Dispatch]
32
+ """(table that generated it, instructions, PK positions, polymorphic dispatch)."""
33
+
34
+ _CACHE: dict[tuple[type, type], _Entry] = {}
35
+ """Compiled plan, indexed by (model being INSTANTIATED, model whose TABLE was read).
36
+
37
+ It is indexed by models (not by `id(table)`, which leaked: every `snake_link()` creates new tables).
38
+ The second element tells polymorphism apart, where `Dog` is hydrated from TWO row shapes (its own
39
+ and `Animal`'s). The table is stored TOO and the REFERENCE is compared, so a relink invalidates the
40
+ old plan.
41
+ """
42
+
43
+ _DISCARD = ""
44
+ """Storage key of a column the row carries and this class does NOT have.
45
+
46
+ It happens under polymorphism: `Animal`'s row includes `Cat`'s `lives` column, and when hydrating a
47
+ `Dog` that value goes nowhere; writing it would leave a phantom attribute, so it gets thrown away.
48
+ """
49
+
50
+
51
+ def _compile(
52
+ table: SnakeTableInfo, own: frozenset[str] | None
53
+ ) -> tuple[_Instruction, ...]:
54
+ """Translates the columns into instructions: where to write and which conversion applies, if any.
55
+
56
+ `own` are the column names the class being instantiated really declares. With `None` — the normal
57
+ case — they are all assumed to be its own. Only polymorphism passes a set: there the row comes
58
+ from the BASE's table and carries columns of the sibling classes.
59
+ """
60
+ return tuple(
61
+ (
62
+ f"__snake_{column.attr_name or column.name}"
63
+ if own is None or column.name in own
64
+ else _DISCARD,
65
+ converter_for(column.python_type, column.scale),
66
+ )
67
+ for column in table.columns
68
+ )
69
+
70
+
71
+ def _entry(model: type, table: SnakeTableInfo, owner: type) -> _Entry:
72
+ """The entry for that pair, compiling it the first time and redoing it if the table changed.
73
+
74
+ Everything precomputed lives in ONE entry so it gets invalidated in one go (two caches = two
75
+ sources of truth).
76
+ """
77
+ key = (model, owner)
78
+ cached = _CACHE.get(key)
79
+ if cached is not None and cached[0] is table:
80
+ return cached
81
+ names = [column.name for column in table.columns]
82
+ entry: _Entry = (
83
+ table,
84
+ _compile(table, _own_columns(model) if owner is not model else None),
85
+ tuple(names.index(pk.name) for pk in table.primary_key.columns),
86
+ _dispatch(table, names, model),
87
+ )
88
+ _CACHE[key] = entry
89
+ return entry
90
+
91
+
92
+ def _own_columns(model: type) -> frozenset[str] | None:
93
+ """The column names THAT model declares, so its siblings' ones can be discarded."""
94
+ table = registry_of(model).table_of(model)
95
+ return None if table is None else frozenset(column.name for column in table.columns)
96
+
97
+
98
+ def _dispatch(table: SnakeTableInfo, names: list[str], owner: type) -> _Dispatch:
99
+ """Where the discriminator is and which class each value stands for. `None` if there is no hierarchy.
100
+
101
+ Only the BASE dispatches: querying a child already filters by its value. The map is frozen along
102
+ with the plan and invalidated by the table's reference, which is why linking is an explicit pass
103
+ at the end.
104
+ """
105
+ if table.polymorphic is None or not table.polymorphic.is_base:
106
+ return None
107
+ if (
108
+ table.polymorphic.column not in names
109
+ ): # pragma: no cover - the compiler guarantees it
110
+ return None
111
+ # The OWNER's registry, not the global one. Asking the global one for a model that lives in
112
+ # a private registry returns `{}`, and `{}` is falsy, so the dispatch simply switches off:
113
+ # every row of a polymorphic hierarchy hydrates as the BASE class, with no error anywhere.
114
+ # This is the one site where resolving in the wrong registry stops shouting and starts lying.
115
+ by_value = registry_of(owner).polymorphic_map(table)
116
+ return (names.index(table.polymorphic.column), by_value) if by_value else None
117
+
118
+
119
+ def dispatch_for(model: type, table: SnakeTableInfo) -> _Dispatch:
120
+ """The polymorphic dispatch of that table, resolved ONCE and cached along with the plan."""
121
+ return _entry(model, table, model)[3]
122
+
123
+
124
+ def plan_for(
125
+ model: type, table: SnakeTableInfo, owner: type | None = None
126
+ ) -> tuple[_Instruction, ...]:
127
+ """The hydration instructions of that model over that row shape."""
128
+ return _entry(model, table, owner if owner is not None else model)[1]
129
+
130
+
131
+ def pk_positions(model: type, table: SnakeTableInfo) -> tuple[int, ...]:
132
+ """Positions of the PK inside the row, computed ONCE (used by the "LEFT JOIN with no partner"
133
+ detection; resolving them per row was O(columns²) on the `include` path)."""
134
+ return _entry(model, table, model)[2]
135
+
136
+
137
+ def partial_plan_for(
138
+ model: type, table: SnakeTableInfo, columns: frozenset[str]
139
+ ) -> tuple[tuple[_Instruction, ...], tuple[str, ...]]:
140
+ """The plan for a row that carries only SOME columns, plus the keys to mark as not loaded.
141
+
142
+ It SLICES the full plan instead of compiling a second one, and that is the point: the two would
143
+ be two answers to "where does column N go", and the day one of them learns about a new converter
144
+ the other keeps the old one. The projection is emitted in the table's declaration order for
145
+ exactly this reason, so position `i` of the row is the `i`-th kept column.
146
+
147
+ The second half is what makes the feature safe. Every column left out has its storage key filled
148
+ with `NOT_LOADED`, so the descriptor can tell "you did not ask for this" from "this is `None`"
149
+ — without it a deferred column reads as its default and nobody finds out.
150
+
151
+ `_DISCARD` keys are skipped: they belong to a sibling class under polymorphism and writing the
152
+ sentinel there would leave a phantom attribute on an object that never had one.
153
+ """
154
+ full = plan_for(model, table)
155
+ kept: list[_Instruction] = []
156
+ missing: list[str] = []
157
+ for instruction, column in zip(full, table.columns, strict=True):
158
+ if column.name in columns:
159
+ kept.append(instruction)
160
+ elif instruction[0] is not _DISCARD:
161
+ missing.append(instruction[0])
162
+ return tuple(kept), tuple(missing)
163
+
164
+
165
+ def hydrate_partial(
166
+ model: type[T],
167
+ plan: tuple[_Instruction, ...],
168
+ missing: tuple[str, ...],
169
+ row: Sequence[object],
170
+ ) -> T:
171
+ """Builds an instance from a partial row, marking every absent column as not loaded."""
172
+ instance = _hydrate_with_plan(model, plan, row)
173
+ write = object.__setattr__
174
+ for storage_key in missing:
175
+ write(instance, storage_key, NOT_LOADED)
176
+ return instance
177
+
178
+
179
+ def hydrate(
180
+ model: type[T],
181
+ table: SnakeTableInfo,
182
+ row: Sequence[object],
183
+ owner: type | None = None,
184
+ ) -> T:
185
+ """Builds an instance with the row's values, in column order.
186
+
187
+ No kwargs, no constructor and no resolving converters: the plan already says where each value
188
+ goes and what to do to it. `owner` is the model whose TABLE generated the row; it only differs
189
+ from `model` under polymorphism (the query goes against `Animal` and each row is instantiated as
190
+ a `Dog` or a `Cat`).
191
+ """
192
+ return _hydrate_with_plan(model, plan_for(model, table, owner), row)
193
+
194
+
195
+ def _hydrate_with_plan(
196
+ model: type[T], plan: tuple[_Instruction, ...], row: Sequence[object]
197
+ ) -> T:
198
+ """The hydration loop with the plan ALREADY resolved: it hoists the resolution OUT of the
199
+ per-row loop (`plan_for`/`dispatch_for` are fixed per table). Whoever maps N rows resolves the
200
+ plan once."""
201
+ instance = object.__new__(model)
202
+ write = object.__setattr__
203
+ for (storage_key, converter), value in zip(plan, row, strict=True):
204
+ if storage_key is not _DISCARD:
205
+ # `or value is None`: a NULL passes through untouched, with no conversion
206
+ # (`_to_bool(None)` would give `False`, `_to_uuid(None)` would blow up). Inline, not in a
207
+ # closure: +132 ns/column.
208
+ write(
209
+ instance,
210
+ storage_key,
211
+ value if converter is None or value is None else converter(value),
212
+ )
213
+ return instance