pyoq-sql 1.0.2__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 (267) hide show
  1. pyoq/__init__.py +10 -0
  2. pyoq/__main__.py +5 -0
  3. pyoq/_native.pyi +5 -0
  4. pyoq/cli/__init__.py +5 -0
  5. pyoq/cli/commands.py +270 -0
  6. pyoq/cli/defaults.py +98 -0
  7. pyoq/cli/services.py +97 -0
  8. pyoq/config/__init__.py +31 -0
  9. pyoq/config/connection.py +161 -0
  10. pyoq/config/loader.py +289 -0
  11. pyoq/config/models.py +245 -0
  12. pyoq/config/values.py +142 -0
  13. pyoq/descriptors.py +165 -0
  14. pyoq/diagnostics/__init__.py +68 -0
  15. pyoq/diagnostics/budget.py +136 -0
  16. pyoq/diagnostics/events.py +137 -0
  17. pyoq/diagnostics/fingerprint.py +267 -0
  18. pyoq/diagnostics/instrumented.py +237 -0
  19. pyoq/diagnostics/metrics.py +61 -0
  20. pyoq/diagnostics/observation.py +227 -0
  21. pyoq/diagnostics/scoped.py +103 -0
  22. pyoq/django/__init__.py +15 -0
  23. pyoq/django/apps.py +17 -0
  24. pyoq/django/execution.py +317 -0
  25. pyoq/django/generation.py +59 -0
  26. pyoq/django/management/__init__.py +0 -0
  27. pyoq/django/management/commands/__init__.py +0 -0
  28. pyoq/django/management/commands/makemigrations.py +53 -0
  29. pyoq/django/management/commands/pyoq_codegen.py +75 -0
  30. pyoq/django/parameters.py +101 -0
  31. pyoq/django/schema.py +379 -0
  32. pyoq/django/settings.py +87 -0
  33. pyoq/django/timeouts.py +105 -0
  34. pyoq/dsl/__init__.py +64 -0
  35. pyoq/dsl/aio/__init__.py +31 -0
  36. pyoq/dsl/aio/context.py +295 -0
  37. pyoq/dsl/aio/queries.py +335 -0
  38. pyoq/dsl/aio/writes.py +368 -0
  39. pyoq/dsl/context.py +326 -0
  40. pyoq/dsl/entry.py +37 -0
  41. pyoq/dsl/labels.py +36 -0
  42. pyoq/dsl/queries.py +339 -0
  43. pyoq/dsl/result.py +164 -0
  44. pyoq/dsl/writes.py +360 -0
  45. pyoq/errors.py +317 -0
  46. pyoq/fastapi/__init__.py +32 -0
  47. pyoq/fastapi/dependencies.py +167 -0
  48. pyoq/fastapi/lifespan.py +119 -0
  49. pyoq/fetching/__init__.py +55 -0
  50. pyoq/fetching/collections.py +136 -0
  51. pyoq/fetching/execution.py +587 -0
  52. pyoq/fetching/joined.py +79 -0
  53. pyoq/fetching/nesting.py +183 -0
  54. pyoq/fetching/plans.py +541 -0
  55. pyoq/fetching/select_in.py +149 -0
  56. pyoq/fetching/tables.py +110 -0
  57. pyoq/generation/__init__.py +54 -0
  58. pyoq/generation/cleanup.py +44 -0
  59. pyoq/generation/contracts.py +248 -0
  60. pyoq/generation/drift.py +169 -0
  61. pyoq/generation/lock.py +33 -0
  62. pyoq/generation/manifest.py +114 -0
  63. pyoq/generation/model.py +1001 -0
  64. pyoq/generation/pipeline.py +119 -0
  65. pyoq/generation/rendering/__init__.py +5 -0
  66. pyoq/generation/rendering/domains.py +51 -0
  67. pyoq/generation/rendering/enums.py +29 -0
  68. pyoq/generation/rendering/exports.py +70 -0
  69. pyoq/generation/rendering/imports.py +63 -0
  70. pyoq/generation/rendering/package.py +56 -0
  71. pyoq/generation/rendering/relations.py +133 -0
  72. pyoq/generation/rendering/routines.py +396 -0
  73. pyoq/generation/rendering/rows.py +79 -0
  74. pyoq/generation/rendering/source.py +121 -0
  75. pyoq/generation/rendering/tables.py +300 -0
  76. pyoq/generation/rendering/writes.py +514 -0
  77. pyoq/generation/validation.py +27 -0
  78. pyoq/generation/writer.py +184 -0
  79. pyoq/hydration/__init__.py +24 -0
  80. pyoq/hydration/engine.py +155 -0
  81. pyoq/hydration/identity.py +194 -0
  82. pyoq/hydration/plan.py +116 -0
  83. pyoq/migrations/__init__.py +9 -0
  84. pyoq/migrations/alembic.py +106 -0
  85. pyoq/migrations/hooks.py +75 -0
  86. pyoq/naming.py +261 -0
  87. pyoq/policies/__init__.py +47 -0
  88. pyoq/policies/bypass.py +122 -0
  89. pyoq/policies/governed.py +430 -0
  90. pyoq/policies/model.py +242 -0
  91. pyoq/policies/rewriting.py +263 -0
  92. pyoq/py.typed +1 -0
  93. pyoq/query/__init__.py +312 -0
  94. pyoq/query/aggregates.py +172 -0
  95. pyoq/query/arrays.py +65 -0
  96. pyoq/query/binding.py +52 -0
  97. pyoq/query/capabilities.py +317 -0
  98. pyoq/query/casts.py +73 -0
  99. pyoq/query/choices.py +185 -0
  100. pyoq/query/decoding.py +360 -0
  101. pyoq/query/documents.py +56 -0
  102. pyoq/query/execution/__init__.py +63 -0
  103. pyoq/query/execution/aio/__init__.py +31 -0
  104. pyoq/query/execution/aio/operations.py +228 -0
  105. pyoq/query/execution/aio/pooling.py +233 -0
  106. pyoq/query/execution/aio/streaming.py +161 -0
  107. pyoq/query/execution/aio/transactions.py +105 -0
  108. pyoq/query/execution/batch.py +96 -0
  109. pyoq/query/execution/binding_style.py +30 -0
  110. pyoq/query/execution/compilation.py +48 -0
  111. pyoq/query/execution/context.py +61 -0
  112. pyoq/query/execution/control.py +50 -0
  113. pyoq/query/execution/operations.py +224 -0
  114. pyoq/query/execution/planning.py +107 -0
  115. pyoq/query/execution/pooling.py +279 -0
  116. pyoq/query/execution/results.py +36 -0
  117. pyoq/query/execution/streaming.py +178 -0
  118. pyoq/query/execution/transactions.py +95 -0
  119. pyoq/query/expressions.py +1200 -0
  120. pyoq/query/fields.py +60 -0
  121. pyoq/query/mysql/__init__.py +59 -0
  122. pyoq/query/mysql/aio/__init__.py +38 -0
  123. pyoq/query/mysql/aio/commands.py +389 -0
  124. pyoq/query/mysql/aio/driver.py +196 -0
  125. pyoq/query/mysql/aio/executor.py +123 -0
  126. pyoq/query/mysql/aio/factory.py +26 -0
  127. pyoq/query/mysql/aio/operations.py +38 -0
  128. pyoq/query/mysql/aio/pool.py +53 -0
  129. pyoq/query/mysql/aio/transactions.py +313 -0
  130. pyoq/query/mysql/commands.py +354 -0
  131. pyoq/query/mysql/compiler.py +134 -0
  132. pyoq/query/mysql/context.py +20 -0
  133. pyoq/query/mysql/executor.py +126 -0
  134. pyoq/query/mysql/expressions.py +244 -0
  135. pyoq/query/mysql/factory.py +46 -0
  136. pyoq/query/mysql/health.py +66 -0
  137. pyoq/query/mysql/identifiers.py +9 -0
  138. pyoq/query/mysql/model.py +79 -0
  139. pyoq/query/mysql/operations.py +43 -0
  140. pyoq/query/mysql/parameters.py +69 -0
  141. pyoq/query/mysql/planning.py +20 -0
  142. pyoq/query/mysql/pool.py +67 -0
  143. pyoq/query/mysql/transactions.py +331 -0
  144. pyoq/query/mysql/writes.py +73 -0
  145. pyoq/query/nodes.py +750 -0
  146. pyoq/query/postgres/__init__.py +48 -0
  147. pyoq/query/postgres/aio/__init__.py +25 -0
  148. pyoq/query/postgres/aio/bulk.py +56 -0
  149. pyoq/query/postgres/aio/commands.py +264 -0
  150. pyoq/query/postgres/aio/executor.py +152 -0
  151. pyoq/query/postgres/aio/factory.py +26 -0
  152. pyoq/query/postgres/aio/operations.py +26 -0
  153. pyoq/query/postgres/aio/pool.py +40 -0
  154. pyoq/query/postgres/aio/transactions.py +295 -0
  155. pyoq/query/postgres/bulk.py +62 -0
  156. pyoq/query/postgres/commands.py +238 -0
  157. pyoq/query/postgres/compiler.py +114 -0
  158. pyoq/query/postgres/context.py +20 -0
  159. pyoq/query/postgres/executor.py +147 -0
  160. pyoq/query/postgres/expressions.py +311 -0
  161. pyoq/query/postgres/factory.py +24 -0
  162. pyoq/query/postgres/health.py +24 -0
  163. pyoq/query/postgres/identifiers.py +9 -0
  164. pyoq/query/postgres/model.py +81 -0
  165. pyoq/query/postgres/operations.py +25 -0
  166. pyoq/query/postgres/parameters.py +71 -0
  167. pyoq/query/postgres/planning.py +20 -0
  168. pyoq/query/postgres/pool.py +52 -0
  169. pyoq/query/postgres/transactions.py +295 -0
  170. pyoq/query/postgres/writes.py +37 -0
  171. pyoq/query/projections.py +105 -0
  172. pyoq/query/raw.py +90 -0
  173. pyoq/query/recursion.py +265 -0
  174. pyoq/query/rendering/__init__.py +1 -0
  175. pyoq/query/rendering/expressions.py +913 -0
  176. pyoq/query/rendering/identifiers.py +40 -0
  177. pyoq/query/rendering/projections.py +63 -0
  178. pyoq/query/rendering/queries.py +334 -0
  179. pyoq/query/rendering/sources.py +66 -0
  180. pyoq/query/rendering/writes.py +176 -0
  181. pyoq/query/results.py +459 -0
  182. pyoq/query/routines.py +196 -0
  183. pyoq/query/rows.py +156 -0
  184. pyoq/query/select.py +793 -0
  185. pyoq/query/select_nodes.py +277 -0
  186. pyoq/query/sources.py +236 -0
  187. pyoq/query/sqlite/__init__.py +43 -0
  188. pyoq/query/sqlite/commands.py +201 -0
  189. pyoq/query/sqlite/compiler.py +139 -0
  190. pyoq/query/sqlite/context.py +20 -0
  191. pyoq/query/sqlite/executor.py +119 -0
  192. pyoq/query/sqlite/expressions.py +224 -0
  193. pyoq/query/sqlite/factory.py +32 -0
  194. pyoq/query/sqlite/health.py +28 -0
  195. pyoq/query/sqlite/identifiers.py +9 -0
  196. pyoq/query/sqlite/model.py +73 -0
  197. pyoq/query/sqlite/operations.py +36 -0
  198. pyoq/query/sqlite/parameters.py +50 -0
  199. pyoq/query/sqlite/planning.py +20 -0
  200. pyoq/query/sqlite/pool.py +50 -0
  201. pyoq/query/sqlite/streaming.py +13 -0
  202. pyoq/query/sqlite/transactions.py +274 -0
  203. pyoq/query/sqlite/writes.py +35 -0
  204. pyoq/query/statements.py +27 -0
  205. pyoq/query/values.py +23 -0
  206. pyoq/query/vendor.py +162 -0
  207. pyoq/query/windows.py +424 -0
  208. pyoq/query/write_nodes.py +174 -0
  209. pyoq/query/writes.py +628 -0
  210. pyoq/relations/__init__.py +66 -0
  211. pyoq/relations/batching.py +219 -0
  212. pyoq/relations/derivation.py +111 -0
  213. pyoq/relations/fetching.py +355 -0
  214. pyoq/relations/graph.py +245 -0
  215. pyoq/relations/loading.py +74 -0
  216. pyoq/relations/model.py +75 -0
  217. pyoq/relations/planning.py +206 -0
  218. pyoq/runtime/__init__.py +9 -0
  219. pyoq/runtime/kernels.py +25 -0
  220. pyoq/runtime/python.py +43 -0
  221. pyoq/runtime/selection.py +73 -0
  222. pyoq/sanic/__init__.py +32 -0
  223. pyoq/sanic/scope.py +197 -0
  224. pyoq/sanic/workers.py +129 -0
  225. pyoq/schema/__init__.py +108 -0
  226. pyoq/schema/codec.py +711 -0
  227. pyoq/schema/models.py +604 -0
  228. pyoq/schema/mysql/__init__.py +16 -0
  229. pyoq/schema/mysql/connection.py +73 -0
  230. pyoq/schema/mysql/dsn.py +72 -0
  231. pyoq/schema/mysql/records.py +354 -0
  232. pyoq/schema/mysql/reflection.py +309 -0
  233. pyoq/schema/mysql/source.py +30 -0
  234. pyoq/schema/mysql/sql.py +128 -0
  235. pyoq/schema/mysql/types.py +105 -0
  236. pyoq/schema/postgres/__init__.py +13 -0
  237. pyoq/schema/postgres/connection.py +63 -0
  238. pyoq/schema/postgres/records.py +384 -0
  239. pyoq/schema/postgres/reflection.py +466 -0
  240. pyoq/schema/postgres/source.py +30 -0
  241. pyoq/schema/postgres/sql.py +246 -0
  242. pyoq/schema/postgres/types.py +98 -0
  243. pyoq/schema/registry.py +45 -0
  244. pyoq/schema/source.py +15 -0
  245. pyoq/schema/sqlite/__init__.py +6 -0
  246. pyoq/schema/sqlite/connection.py +54 -0
  247. pyoq/schema/sqlite/records.py +167 -0
  248. pyoq/schema/sqlite/reflection.py +393 -0
  249. pyoq/schema/sqlite/source.py +30 -0
  250. pyoq/schema/sqlite/sql.py +254 -0
  251. pyoq/schema/sqlite/types.py +74 -0
  252. pyoq/serving/__init__.py +23 -0
  253. pyoq/serving/databases.py +107 -0
  254. pyoq/serving/opening.py +331 -0
  255. pyoq/snapshots/__init__.py +20 -0
  256. pyoq/snapshots/drift.py +312 -0
  257. pyoq/snapshots/files.py +96 -0
  258. pyoq/snapshots/routing.py +40 -0
  259. pyoq/snapshots/source.py +33 -0
  260. pyoq/tracing/__init__.py +5 -0
  261. pyoq/tracing/spans.py +89 -0
  262. pyoq/unset.py +14 -0
  263. pyoq_sql-1.0.2.dist-info/METADATA +3050 -0
  264. pyoq_sql-1.0.2.dist-info/RECORD +267 -0
  265. pyoq_sql-1.0.2.dist-info/WHEEL +4 -0
  266. pyoq_sql-1.0.2.dist-info/entry_points.txt +3 -0
  267. pyoq_sql-1.0.2.dist-info/licenses/LICENSE +373 -0
@@ -0,0 +1,514 @@
1
+ """Generated immutable write values and builder source rendering."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import PurePosixPath
6
+
7
+ from pyoq.generation.contracts import RenderedFile
8
+ from pyoq.generation.model import GeneratedColumn, GeneratedPackage, GeneratedTable
9
+ from pyoq.generation.rendering.imports import plan_annotation_imports
10
+ from pyoq.generation.rendering.source import (
11
+ LINE_LIMIT,
12
+ PythonSource,
13
+ literal,
14
+ render_all,
15
+ render_from_import,
16
+ render_subscript,
17
+ within_limit,
18
+ )
19
+
20
+
21
+ def render_writes(package: GeneratedPackage) -> RenderedFile:
22
+ source = PythonSource()
23
+ source.line('"""Generated immutable insert and update values."""')
24
+ source.line()
25
+ source.line("from __future__ import annotations")
26
+ if package.tables:
27
+ _render_imports(source, package)
28
+ if any(table.required_columns for table in package.tables):
29
+ _render_required_value(source)
30
+ for table in package.tables:
31
+ _render_table_writes(source, table)
32
+ source.line()
33
+ source.line()
34
+ source.lines(render_all(_exported_names(package)))
35
+ return RenderedFile(PurePosixPath("writes.py"), source.render())
36
+
37
+
38
+ def _render_imports(source: PythonSource, package: GeneratedPackage) -> None:
39
+ annotations = plan_annotation_imports(_annotations(package), package)
40
+ has_writable = any(table.writable_columns for table in package.tables)
41
+ has_required = any(table.required_columns for table in package.tables)
42
+ has_optional = any(
43
+ column.writable and not column.required
44
+ for table in package.tables
45
+ for column in table.columns
46
+ )
47
+ has_overloads = any(len(table.required_columns) > 1 for table in package.tables)
48
+ typing_names = ["TypedDict"]
49
+ if has_writable:
50
+ typing_names.append("NotRequired")
51
+ if has_required:
52
+ typing_names.extend(("Generic", "Required", "TypeVar"))
53
+ if has_optional:
54
+ typing_names.append("Self")
55
+ if has_overloads:
56
+ typing_names.append("overload")
57
+ source.line()
58
+ typing_import = render_from_import("typing", tuple(typing_names))
59
+ standard_imports = (
60
+ "from dataclasses import dataclass",
61
+ *annotations.standard,
62
+ "\n".join(typing_import),
63
+ )
64
+ for standard_import in sorted(standard_imports):
65
+ source.lines(tuple(standard_import.splitlines()))
66
+ descriptor_names = list(annotations.descriptor_names)
67
+ if has_writable:
68
+ descriptor_names.extend(("UNSET", "UnsetType"))
69
+ if has_required:
70
+ descriptor_names.extend(("Missing", "Present"))
71
+ if descriptor_names:
72
+ source.line()
73
+ source.lines(render_from_import("pyoq.descriptors", tuple(descriptor_names)))
74
+ if annotations.enum_names:
75
+ source.line()
76
+ source.lines(render_from_import(".enums", annotations.enum_names))
77
+ if annotations.domain_names:
78
+ source.line()
79
+ source.lines(render_from_import(".domains", annotations.domain_names))
80
+
81
+
82
+ def _annotations(package: GeneratedPackage) -> tuple[str, ...]:
83
+ return tuple(
84
+ column.value_annotation
85
+ for table in package.tables
86
+ for column in table.columns
87
+ if column.writable
88
+ )
89
+
90
+
91
+ def _unset_union(annotation: str) -> str:
92
+ """`None` reads last in a union, so an unset marker goes before it."""
93
+ optional = " | None"
94
+ if annotation.endswith(optional):
95
+ return f"{annotation[: -len(optional)]} | UnsetType | None"
96
+ return f"{annotation} | UnsetType"
97
+
98
+
99
+ def _render_required_value(source: PythonSource) -> None:
100
+ source.line()
101
+ source.line('_Value = TypeVar("_Value")')
102
+ source.line()
103
+ source.line()
104
+ source.line("def _required_value(")
105
+ source.line(" value: _Value | UnsetType,")
106
+ source.line(" field_name: str,")
107
+ source.line(") -> _Value:")
108
+ source.line(" if isinstance(value, UnsetType):")
109
+ source.line(' message = f"required field is missing: {field_name}"')
110
+ source.line(" raise ValueError(message)")
111
+ source.line(" return value")
112
+
113
+
114
+ def _state_type_variable(state_name: str) -> tuple[str, ...]:
115
+ """A long enough column name pushes the declaration past the line limit."""
116
+ inline = f'{state_name} = TypeVar("{state_name}", bound=Missing | Present)'
117
+ if len(inline) <= 88:
118
+ return (inline,)
119
+ return (
120
+ within_limit(f"{state_name} = TypeVar("),
121
+ f' "{state_name}",',
122
+ " bound=Missing | Present,",
123
+ ")",
124
+ )
125
+
126
+
127
+ def _render_table_writes(source: PythonSource, table: GeneratedTable) -> None:
128
+ writable = table.writable_columns
129
+ _render_typed_dict(source, f"{table.insert_name}Values", writable, insert=True)
130
+ _render_typed_dict(source, f"{table.update_name}Values", writable, insert=False)
131
+ _render_write_dataclass(source, table.insert_name, writable, insert=True)
132
+ _render_write_dataclass(source, table.update_name, writable, insert=False)
133
+ state_names = _render_state_variables(source, table)
134
+ _render_insert_builder(source, table, state_names)
135
+ _render_complete_builder(source, table, state_names)
136
+ _render_update_builder(source, table)
137
+
138
+
139
+ def _render_typed_dict(
140
+ source: PythonSource,
141
+ name: str,
142
+ columns: tuple[GeneratedColumn, ...],
143
+ *,
144
+ insert: bool,
145
+ ) -> None:
146
+ source.line()
147
+ source.line()
148
+ source.line(f"class {name}(TypedDict):")
149
+ if not columns:
150
+ source.line(" pass")
151
+ return
152
+ for column in columns:
153
+ required = insert and column.required
154
+ wrapper = "Required" if required else "NotRequired"
155
+ source.line(f" {column.field_name}: {wrapper}[{column.value_annotation}]")
156
+
157
+
158
+ def _render_write_dataclass(
159
+ source: PythonSource,
160
+ name: str,
161
+ columns: tuple[GeneratedColumn, ...],
162
+ *,
163
+ insert: bool,
164
+ ) -> None:
165
+ source.line()
166
+ source.line()
167
+ source.line("@dataclass(frozen=True, slots=True)")
168
+ source.line(f"class {name}:")
169
+ ordered = _ordered_insert_columns(columns) if insert else columns
170
+ if not ordered:
171
+ source.line(" pass")
172
+ return
173
+ for column in ordered:
174
+ if insert and column.required:
175
+ source.line(f" {column.field_name}: {column.value_annotation}")
176
+ else:
177
+ annotation = _unset_union(column.value_annotation)
178
+ source.line(f" {column.field_name}: {annotation} = UNSET")
179
+
180
+
181
+ def _ordered_insert_columns(
182
+ columns: tuple[GeneratedColumn, ...],
183
+ ) -> tuple[GeneratedColumn, ...]:
184
+ required = tuple(column for column in columns if column.required)
185
+ optional = tuple(column for column in columns if not column.required)
186
+ return required + optional
187
+
188
+
189
+ def _render_state_variables(
190
+ source: PythonSource,
191
+ table: GeneratedTable,
192
+ ) -> tuple[str, ...]:
193
+ state_names = tuple(
194
+ f"_{table.class_name}{_pascal(column.field_name)}State"
195
+ for column in table.required_columns
196
+ )
197
+ if state_names:
198
+ source.line()
199
+ for state_name in state_names:
200
+ source.line()
201
+ source.lines(_state_type_variable(state_name))
202
+ return state_names
203
+
204
+
205
+ def _render_insert_builder(
206
+ source: PythonSource,
207
+ table: GeneratedTable,
208
+ state_names: tuple[str, ...],
209
+ ) -> None:
210
+ source.line()
211
+ source.line()
212
+ source.line("@dataclass(frozen=True, slots=True)")
213
+ _render_builder_class_header(source, table.builder_name, "Generic", state_names)
214
+ if not table.writable_columns:
215
+ source.line(" pass")
216
+ return
217
+ for column in table.writable_columns:
218
+ source.line(
219
+ f" _{column.field_name}: {_unset_union(column.value_annotation)} = UNSET"
220
+ )
221
+ for column in table.writable_columns:
222
+ if column.required:
223
+ _render_required_setter(source, table, column, state_names)
224
+ else:
225
+ _render_optional_setter(source, table, column)
226
+
227
+
228
+ def _render_required_setter(
229
+ source: PythonSource,
230
+ table: GeneratedTable,
231
+ column: GeneratedColumn,
232
+ state_names: tuple[str, ...],
233
+ ) -> None:
234
+ required = table.required_columns
235
+ column_index = required.index(column)
236
+ fallback_states = list(state_names)
237
+ fallback_states[column_index] = "Present"
238
+ fallback_state_tuple = tuple(fallback_states)
239
+ if len(required) > 1:
240
+ completion_states = ["Present"] * len(required)
241
+ completion_states[column_index] = state_names[column_index]
242
+ source.line()
243
+ source.line(" @overload")
244
+ source.line(f" def {column.field_name}(")
245
+ source.lines(
246
+ render_subscript(
247
+ " self: ",
248
+ table.builder_name,
249
+ tuple(completion_states),
250
+ argument_indent=" ",
251
+ closing_indent=" ",
252
+ suffix=",",
253
+ )
254
+ )
255
+ source.line(f" value: {column.value_annotation},")
256
+ source.line(f" ) -> {table.complete_builder_name}: ...")
257
+ source.line()
258
+ source.line(" @overload")
259
+ source.line(f" def {column.field_name}(")
260
+ source.line(" self,")
261
+ source.line(f" value: {column.value_annotation},")
262
+ source.lines(
263
+ render_subscript(
264
+ " ) -> ",
265
+ table.builder_name,
266
+ fallback_state_tuple,
267
+ argument_indent=" ",
268
+ closing_indent=" ",
269
+ suffix=": ...",
270
+ )
271
+ )
272
+ source.line()
273
+ source.line(f" def {column.field_name}(")
274
+ source.line(" self,")
275
+ source.line(f" value: {column.value_annotation},")
276
+ if len(required) == 1:
277
+ source.line(f" ) -> {table.complete_builder_name}:")
278
+ else:
279
+ _render_union_return(source, table, fallback_state_tuple)
280
+ if len(required) == 1:
281
+ source.line(f" return {table.complete_builder_name}(")
282
+ for candidate in table.writable_columns:
283
+ candidate_value = (
284
+ "value" if candidate is column else f"self._{candidate.field_name}"
285
+ )
286
+ source.line(f" _{candidate.field_name}={candidate_value},")
287
+ source.line(" )")
288
+ else:
289
+ _render_candidate(source, table, column, "value", fallback_state_tuple)
290
+ _render_completion_check(source, table)
291
+ source.line(" return candidate")
292
+
293
+
294
+ def _render_builder_class_header(
295
+ source: PythonSource,
296
+ class_name: str,
297
+ base_name: str,
298
+ arguments: tuple[str, ...],
299
+ ) -> None:
300
+ if not arguments:
301
+ base = "" if base_name == "Generic" else f"({base_name})"
302
+ source.line(f"class {class_name}{base}:")
303
+ return
304
+ inline = f"class {class_name}({base_name}[{', '.join(arguments)}]):"
305
+ if len(inline) <= 88:
306
+ source.line(inline)
307
+ return
308
+ source.line(f"class {class_name}(")
309
+ source.lines(
310
+ render_subscript(
311
+ " ",
312
+ base_name,
313
+ arguments,
314
+ argument_indent=" ",
315
+ closing_indent=" ",
316
+ suffix="",
317
+ )
318
+ )
319
+ source.line("):")
320
+
321
+
322
+ def _render_union_return(
323
+ source: PythonSource,
324
+ table: GeneratedTable,
325
+ builder_states: tuple[str, ...],
326
+ ) -> None:
327
+ builder_type = f"{table.builder_name}[{', '.join(builder_states)}]"
328
+ inline = f" ) -> {builder_type} | {table.complete_builder_name}:"
329
+ if len(inline) <= 88:
330
+ source.line(inline)
331
+ return
332
+ source.line(" ) -> (")
333
+ source.lines(
334
+ render_subscript(
335
+ " ",
336
+ table.builder_name,
337
+ builder_states,
338
+ argument_indent=" ",
339
+ closing_indent=" ",
340
+ suffix="",
341
+ )
342
+ )
343
+ source.line(f" | {table.complete_builder_name}")
344
+ source.line(" ):")
345
+
346
+
347
+ def _render_optional_setter(
348
+ source: PythonSource,
349
+ table: GeneratedTable,
350
+ column: GeneratedColumn,
351
+ ) -> None:
352
+ source.line()
353
+ source.line(f" def {column.field_name}(")
354
+ source.line(" self,")
355
+ source.line(f" value: {column.value_annotation},")
356
+ source.line(" ) -> Self:")
357
+ source.line(" return type(self)(")
358
+ for candidate in table.writable_columns:
359
+ value = "value" if candidate is column else f"self._{candidate.field_name}"
360
+ source.line(f" _{candidate.field_name}={value},")
361
+ source.line(" )")
362
+
363
+
364
+ def _render_candidate(
365
+ source: PythonSource,
366
+ table: GeneratedTable,
367
+ replaced: GeneratedColumn,
368
+ value: str,
369
+ candidate_states: tuple[str, ...],
370
+ ) -> None:
371
+ source.lines(
372
+ render_subscript(
373
+ " candidate: ",
374
+ table.builder_name,
375
+ candidate_states,
376
+ argument_indent=" ",
377
+ closing_indent=" ",
378
+ suffix=f" = {table.builder_name}(",
379
+ )
380
+ )
381
+ for column in table.writable_columns:
382
+ candidate_value = value if column is replaced else f"self._{column.field_name}"
383
+ source.line(f" _{column.field_name}={candidate_value},")
384
+ source.line(" )")
385
+
386
+
387
+ def _render_completion_check(source: PythonSource, table: GeneratedTable) -> None:
388
+ source.line(" if all(")
389
+ source.line(" (")
390
+ for column in table.required_columns:
391
+ condition = f"not isinstance(candidate._{column.field_name}, UnsetType)"
392
+ source.line(f" {condition},")
393
+ source.line(" )")
394
+ source.line(" ):")
395
+ source.line(f" return {table.complete_builder_name}(")
396
+ _render_constructor_values(source, table, indent=" ")
397
+ source.line(" )")
398
+
399
+
400
+ def _render_constructor_values(
401
+ source: PythonSource,
402
+ table: GeneratedTable,
403
+ *,
404
+ indent: str,
405
+ prefix: str = "candidate",
406
+ ) -> None:
407
+ for column in table.writable_columns:
408
+ source.line(f"{indent}_{column.field_name}={prefix}._{column.field_name},")
409
+
410
+
411
+ def _render_complete_builder(
412
+ source: PythonSource,
413
+ table: GeneratedTable,
414
+ state_names: tuple[str, ...],
415
+ ) -> None:
416
+ states = tuple("Present" for _ in state_names)
417
+ source.line()
418
+ source.line()
419
+ source.line("@dataclass(frozen=True, slots=True)")
420
+ _render_builder_class_header(
421
+ source,
422
+ table.complete_builder_name,
423
+ table.builder_name,
424
+ states,
425
+ )
426
+ source.line(f" def build(self) -> {table.insert_name}:")
427
+ columns = _ordered_insert_columns(table.writable_columns)
428
+ if not columns:
429
+ source.line(f" return {table.insert_name}()")
430
+ return
431
+ source.line(f" return {table.insert_name}(")
432
+ for column in columns:
433
+ source.lines(_build_argument(column))
434
+ source.line(" )")
435
+
436
+
437
+ def _build_argument(column: GeneratedColumn) -> tuple[str, ...]:
438
+ """Wrap the call rather than the name, because only the call can wrap."""
439
+ if not column.required:
440
+ return (f" {column.field_name}=self._{column.field_name},",)
441
+ inline = (
442
+ f" {column.field_name}=_required_value("
443
+ f"self._{column.field_name}, {literal(column.field_name)}),"
444
+ )
445
+ if len(inline) <= LINE_LIMIT:
446
+ return (inline,)
447
+ return (
448
+ f" {column.field_name}=_required_value(",
449
+ f" self._{column.field_name},",
450
+ f" {literal(column.field_name)},",
451
+ " ),",
452
+ )
453
+
454
+
455
+ def _render_update_builder(source: PythonSource, table: GeneratedTable) -> None:
456
+ source.line()
457
+ source.line()
458
+ source.line("@dataclass(frozen=True, slots=True)")
459
+ source.line(f"class {table.update_builder_name}:")
460
+ if not table.writable_columns:
461
+ source.line(f" def build(self) -> {table.update_name}:")
462
+ source.line(f" return {table.update_name}()")
463
+ return
464
+ for column in table.writable_columns:
465
+ source.line(
466
+ f" _{column.field_name}: {_unset_union(column.value_annotation)} = UNSET"
467
+ )
468
+ for column in table.writable_columns:
469
+ _render_update_setter(source, table, column)
470
+ source.line()
471
+ source.line(f" def build(self) -> {table.update_name}:")
472
+ source.line(f" return {table.update_name}(")
473
+ for column in table.writable_columns:
474
+ source.line(f" {column.field_name}=self._{column.field_name},")
475
+ source.line(" )")
476
+
477
+
478
+ def _render_update_setter(
479
+ source: PythonSource,
480
+ table: GeneratedTable,
481
+ replaced: GeneratedColumn,
482
+ ) -> None:
483
+ source.line()
484
+ source.line(f" def {replaced.field_name}(")
485
+ source.line(" self,")
486
+ source.line(f" value: {replaced.value_annotation},")
487
+ source.line(f" ) -> {table.update_builder_name}:")
488
+ source.line(f" return {table.update_builder_name}(")
489
+ for column in table.writable_columns:
490
+ value = "value" if column is replaced else f"self._{column.field_name}"
491
+ source.line(f" _{column.field_name}={value},")
492
+ source.line(" )")
493
+
494
+
495
+ def _pascal(value: str) -> str:
496
+ return "".join(part[:1].upper() + part[1:] for part in value.split("_"))
497
+
498
+
499
+ def _exported_names(package: GeneratedPackage) -> tuple[str, ...]:
500
+ return tuple(
501
+ name
502
+ for table in package.tables
503
+ for name in (
504
+ table.insert_name,
505
+ f"{table.insert_name}Values",
506
+ table.update_name,
507
+ f"{table.update_name}Values",
508
+ table.builder_name,
509
+ table.update_builder_name,
510
+ )
511
+ )
512
+
513
+
514
+ __all__ = ("render_writes",)
@@ -0,0 +1,27 @@
1
+ """Generated Python syntax validation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ast
6
+
7
+ from pyoq.errors import GenerationValidationError
8
+ from pyoq.generation.contracts import GenerationPlan
9
+
10
+
11
+ class PythonSyntaxValidator:
12
+ def validate(self, plan: GenerationPlan) -> None:
13
+ for generated_file in plan.files:
14
+ if generated_file.path.suffix not in {".py", ".pyi"}:
15
+ continue
16
+ try:
17
+ ast.parse(
18
+ generated_file.content,
19
+ filename=str(generated_file.path),
20
+ type_comments=True,
21
+ )
22
+ except SyntaxError as error:
23
+ message = (
24
+ f"invalid generated Python in {generated_file.path}: "
25
+ f"line {error.lineno}"
26
+ )
27
+ raise GenerationValidationError(message) from error