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,119 @@
1
+ """Generation planning and execution orchestration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import PurePosixPath
7
+
8
+ from pyoq.config import Configuration
9
+ from pyoq.errors import (
10
+ GenerationDriftError,
11
+ GenerationOwnershipError,
12
+ GenerationValidationError,
13
+ )
14
+ from pyoq.generation.contracts import (
15
+ ConcernRenderer,
16
+ DriftChecker,
17
+ DriftReport,
18
+ GenerationMode,
19
+ GenerationPlan,
20
+ ManifestStore,
21
+ OwnershipManifest,
22
+ PlanValidator,
23
+ PlanWriter,
24
+ ProjectLock,
25
+ RenderedFile,
26
+ )
27
+ from pyoq.schema import SchemaSnapshot, SchemaSource
28
+
29
+
30
+ @dataclass(frozen=True, slots=True)
31
+ class GenerationPlanner:
32
+ renderers: tuple[ConcernRenderer, ...]
33
+
34
+ def __post_init__(self) -> None:
35
+ if any(not renderer.concern for renderer in self.renderers):
36
+ message = "rendering concern must be a non-empty string"
37
+ raise GenerationValidationError(message)
38
+ duplicate_concerns = _duplicate_concerns(self.renderers)
39
+ if duplicate_concerns:
40
+ message = f"duplicate rendering concerns: {duplicate_concerns!r}"
41
+ raise GenerationValidationError(message)
42
+
43
+ def plan(
44
+ self,
45
+ snapshot: SchemaSnapshot,
46
+ configuration: Configuration,
47
+ ) -> GenerationPlan:
48
+ rendered_files: list[RenderedFile] = []
49
+ for renderer in sorted(self.renderers, key=lambda candidate: candidate.concern):
50
+ rendered_files.extend(renderer.render(snapshot, configuration))
51
+ return GenerationPlan(tuple(rendered_files))
52
+
53
+
54
+ def _duplicate_concerns(
55
+ renderers: tuple[ConcernRenderer, ...],
56
+ ) -> tuple[str, ...]:
57
+ seen: set[str] = set()
58
+ duplicates: set[str] = set()
59
+ for renderer in renderers:
60
+ if renderer.concern in seen:
61
+ duplicates.add(renderer.concern)
62
+ seen.add(renderer.concern)
63
+ return tuple(sorted(duplicates))
64
+
65
+
66
+ @dataclass(frozen=True, slots=True)
67
+ class GenerationPipeline:
68
+ schema_source: SchemaSource
69
+ planner: GenerationPlanner
70
+ validator: PlanValidator
71
+ manifest_store: ManifestStore
72
+ drift_checker: DriftChecker
73
+ writer: PlanWriter
74
+ project_lock: ProjectLock
75
+
76
+ def execute(self, configuration: Configuration, mode: GenerationMode) -> str:
77
+ snapshot = self.schema_source.load(configuration)
78
+ plan = self.planner.plan(snapshot, configuration)
79
+ self.validator.validate(plan)
80
+ desired_manifest = OwnershipManifest.from_plan(
81
+ configuration.codegen_package,
82
+ plan,
83
+ )
84
+ with self.project_lock.acquire(configuration.project_root):
85
+ current_manifest = self.manifest_store.load(configuration.generated_path)
86
+ report = self.drift_checker.compare(
87
+ configuration.generated_path,
88
+ plan,
89
+ current_manifest,
90
+ desired_manifest,
91
+ )
92
+ if mode is GenerationMode.DRY_RUN:
93
+ return f"generation dry run: {report.summary()}"
94
+ if mode is GenerationMode.CHECK:
95
+ return _checked_message(report)
96
+ if report.is_current:
97
+ return "generated package is current"
98
+ _require_no_conflicts(report.conflicts)
99
+ self.writer.write(
100
+ configuration.generated_path,
101
+ plan,
102
+ current_manifest,
103
+ desired_manifest,
104
+ )
105
+ return f"generated {len(plan.files)} files"
106
+
107
+
108
+ def _checked_message(report: DriftReport) -> str:
109
+ if report.is_current:
110
+ return "generated package is current"
111
+ message = f"generated package has drift: {report.summary()}"
112
+ raise GenerationDriftError(message)
113
+
114
+
115
+ def _require_no_conflicts(conflicts: tuple[PurePosixPath, ...]) -> None:
116
+ if conflicts:
117
+ joined_paths = ", ".join(str(path) for path in conflicts)
118
+ message = f"generated package contains ownership conflicts: {joined_paths}"
119
+ raise GenerationOwnershipError(message)
@@ -0,0 +1,5 @@
1
+ """Generated database package rendering."""
2
+
3
+ from pyoq.generation.rendering.package import GeneratedTypesRenderer
4
+
5
+ __all__ = ("GeneratedTypesRenderer",)
@@ -0,0 +1,51 @@
1
+ """Generated database domain type rendering."""
2
+
3
+ from pathlib import PurePosixPath
4
+
5
+ from pyoq.generation.contracts import RenderedFile
6
+ from pyoq.generation.model import GeneratedPackage
7
+ from pyoq.generation.rendering.imports import plan_annotation_imports
8
+ from pyoq.generation.rendering.source import (
9
+ PythonSource,
10
+ literal,
11
+ render_all,
12
+ render_from_import,
13
+ )
14
+
15
+
16
+ def render_domains(package: GeneratedPackage) -> RenderedFile:
17
+ """A domain becomes a name of its own over the type it is written over.
18
+
19
+ Two domains over one base are two types, and a distinct name is what lets
20
+ a checker tell them apart. The value is still the base value, so nothing
21
+ is converted on the way in or out.
22
+ """
23
+ source = PythonSource()
24
+ source.line('"""Generated database domain types."""')
25
+ annotations = tuple(domain.base_annotation for domain in package.domains)
26
+ imports = plan_annotation_imports(annotations, package)
27
+ if package.domains:
28
+ source.line()
29
+ source.lines(tuple(sorted((*imports.standard, "from typing import NewType"))))
30
+ if imports.descriptor_names:
31
+ source.line()
32
+ source.lines(
33
+ render_from_import("pyoq.descriptors", imports.descriptor_names)
34
+ )
35
+ if imports.enum_names:
36
+ source.line()
37
+ source.lines(render_from_import(".enums", imports.enum_names))
38
+ for domain in package.domains:
39
+ source.line()
40
+ source.line(
41
+ f"{domain.type_name} = NewType("
42
+ f"{literal(domain.type_name)}, {domain.base_annotation})"
43
+ )
44
+ source.line()
45
+ if package.domains:
46
+ source.line()
47
+ source.lines(render_all(tuple(domain.type_name for domain in package.domains)))
48
+ return RenderedFile(PurePosixPath("domains.py"), source.render())
49
+
50
+
51
+ __all__ = ("render_domains",)
@@ -0,0 +1,29 @@
1
+ """Generated enum source rendering."""
2
+
3
+ from pathlib import PurePosixPath
4
+
5
+ from pyoq.generation.contracts import RenderedFile
6
+ from pyoq.generation.model import GeneratedPackage
7
+ from pyoq.generation.rendering.source import PythonSource, literal, render_all
8
+
9
+
10
+ def render_enums(package: GeneratedPackage) -> RenderedFile:
11
+ source = PythonSource()
12
+ source.line('"""Generated database enum types."""')
13
+ if package.enums:
14
+ source.line()
15
+ source.line("from enum import StrEnum")
16
+ for enum in package.enums:
17
+ source.line()
18
+ source.line()
19
+ source.line(f"class {enum.class_name}(StrEnum):")
20
+ for member_name, member_value in enum.members:
21
+ source.line(f" {member_name} = {literal(member_value)}")
22
+ source.line()
23
+ if package.enums:
24
+ source.line()
25
+ source.lines(render_all(tuple(enum.class_name for enum in package.enums)))
26
+ return RenderedFile(PurePosixPath("enums.py"), source.render())
27
+
28
+
29
+ __all__ = ("render_enums",)
@@ -0,0 +1,70 @@
1
+ """Generated package facade source rendering."""
2
+
3
+ from pathlib import PurePosixPath
4
+
5
+ from pyoq.generation.contracts import RenderedFile
6
+ from pyoq.generation.model import GeneratedPackage
7
+ from pyoq.generation.rendering.source import (
8
+ PythonSource,
9
+ render_all,
10
+ render_from_import,
11
+ )
12
+
13
+
14
+ def render_exports(package: GeneratedPackage) -> RenderedFile:
15
+ source = PythonSource()
16
+ source.line('"""Generated database package."""')
17
+ module_exports = _module_exports(package)
18
+ if any(names for _, names in module_exports):
19
+ source.line()
20
+ for module_name, names in module_exports:
21
+ if names:
22
+ source.lines(render_from_import(f".{module_name}", names))
23
+ source.line()
24
+ source.lines(
25
+ render_all(tuple(name for _, names in module_exports for name in names))
26
+ )
27
+ return RenderedFile(PurePosixPath("__init__.py"), source.render())
28
+
29
+
30
+ def _module_exports(
31
+ package: GeneratedPackage,
32
+ ) -> tuple[tuple[str, tuple[str, ...]], ...]:
33
+ enum_names = tuple(enum.class_name for enum in package.enums)
34
+ row_names = tuple(
35
+ name
36
+ for table in package.tables
37
+ for name in (table.row_name, *(key.type_name for key in table.keys))
38
+ )
39
+ write_names = tuple(
40
+ name
41
+ for table in package.tables
42
+ for name in (
43
+ table.insert_name,
44
+ f"{table.insert_name}Values",
45
+ table.update_name,
46
+ f"{table.update_name}Values",
47
+ table.builder_name,
48
+ table.update_builder_name,
49
+ )
50
+ )
51
+ table_names = tuple(
52
+ name
53
+ for table in package.tables
54
+ for name in (table.class_name, table.constant_name)
55
+ )
56
+ relation_names = tuple(
57
+ relation.constant_name
58
+ for table in package.tables
59
+ for relation in table.relations
60
+ )
61
+ return (
62
+ ("enums", enum_names),
63
+ ("relations", relation_names),
64
+ ("rows", row_names),
65
+ ("tables", table_names),
66
+ ("writes", write_names),
67
+ )
68
+
69
+
70
+ __all__ = ("render_exports",)
@@ -0,0 +1,63 @@
1
+ """Import planning for generated type annotations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from pyoq.generation.model import GeneratedPackage
8
+
9
+
10
+ @dataclass(frozen=True, slots=True)
11
+ class AnnotationImports:
12
+ standard: tuple[str, ...]
13
+ descriptor_names: tuple[str, ...]
14
+ enum_names: tuple[str, ...]
15
+ domain_names: tuple[str, ...] = ()
16
+
17
+
18
+ def plan_annotation_imports(
19
+ annotations: tuple[str, ...],
20
+ package: GeneratedPackage,
21
+ ) -> AnnotationImports:
22
+ standard: list[str] = []
23
+ datetime_names = tuple(
24
+ name
25
+ for name in ("date", "datetime", "time", "timedelta")
26
+ if any(_contains_type(annotation, name) for annotation in annotations)
27
+ )
28
+ if datetime_names:
29
+ standard.append(f"from datetime import {', '.join(datetime_names)}")
30
+ if any(_contains_type(annotation, "Decimal") for annotation in annotations):
31
+ standard.append("from decimal import Decimal")
32
+ if any(_contains_type(annotation, "UUID") for annotation in annotations):
33
+ standard.append("from uuid import UUID")
34
+ descriptor_names = (
35
+ ("JsonValue",)
36
+ if any(_contains_type(annotation, "JsonValue") for annotation in annotations)
37
+ else ()
38
+ )
39
+ enum_names = _named(tuple(enum.class_name for enum in package.enums), annotations)
40
+ domain_names = _named(
41
+ tuple(domain.type_name for domain in package.domains), annotations
42
+ )
43
+ return AnnotationImports(
44
+ tuple(sorted(standard)), descriptor_names, enum_names, domain_names
45
+ )
46
+
47
+
48
+ def _named(generated: tuple[str, ...], annotations: tuple[str, ...]) -> tuple[str, ...]:
49
+ """The generated names an annotation writes, which are the ones to import."""
50
+ return tuple(
51
+ name
52
+ for name in generated
53
+ if any(_contains_type(annotation, name) for annotation in annotations)
54
+ )
55
+
56
+
57
+ def _contains_type(annotation: str, type_name: str) -> bool:
58
+ normalized = annotation.replace("[", " ").replace("]", " ").replace(",", " ")
59
+ tokens = normalized.replace("|", " ").replace(".", " ").split()
60
+ return type_name in tokens
61
+
62
+
63
+ __all__ = ("AnnotationImports", "plan_annotation_imports")
@@ -0,0 +1,56 @@
1
+ """Generated database package rendering orchestration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from dataclasses import dataclass, field
7
+
8
+ from pyoq.config import Configuration
9
+ from pyoq.generation.contracts import RenderedFile
10
+ from pyoq.generation.model import GeneratedPackage, build_codegen_package
11
+ from pyoq.generation.rendering.domains import render_domains
12
+ from pyoq.generation.rendering.enums import render_enums
13
+ from pyoq.generation.rendering.exports import render_exports
14
+ from pyoq.generation.rendering.relations import render_relations
15
+ from pyoq.generation.rendering.routines import render_routines
16
+ from pyoq.generation.rendering.rows import render_rows
17
+ from pyoq.generation.rendering.tables import render_tables
18
+ from pyoq.generation.rendering.writes import render_writes
19
+ from pyoq.naming import NamingPolicy
20
+ from pyoq.schema import SchemaSnapshot
21
+
22
+ PackageFileRenderer = Callable[[GeneratedPackage], RenderedFile]
23
+ DEFAULT_FILE_RENDERERS: tuple[PackageFileRenderer, ...] = (
24
+ render_domains,
25
+ render_enums,
26
+ render_exports,
27
+ render_relations,
28
+ render_routines,
29
+ render_rows,
30
+ render_tables,
31
+ render_writes,
32
+ )
33
+
34
+
35
+ @dataclass(frozen=True, slots=True)
36
+ class GeneratedTypesRenderer:
37
+ naming_policy: NamingPolicy = field(default_factory=NamingPolicy)
38
+ file_renderers: tuple[PackageFileRenderer, ...] = DEFAULT_FILE_RENDERERS
39
+
40
+ @property
41
+ def concern(self) -> str:
42
+ return "generated-types"
43
+
44
+ def render(
45
+ self,
46
+ snapshot: SchemaSnapshot,
47
+ configuration: Configuration,
48
+ ) -> tuple[RenderedFile, ...]:
49
+ codegen_package = build_codegen_package(snapshot, self.naming_policy)
50
+ rendered_files = tuple(
51
+ renderer(codegen_package) for renderer in self.file_renderers
52
+ )
53
+ return tuple(sorted(rendered_files, key=lambda generated: str(generated.path)))
54
+
55
+
56
+ __all__ = ("DEFAULT_FILE_RENDERERS", "GeneratedTypesRenderer", "PackageFileRenderer")
@@ -0,0 +1,133 @@
1
+ """Generated relationship descriptor source rendering."""
2
+
3
+ from pathlib import PurePosixPath
4
+
5
+ from pyoq.generation.contracts import RenderedFile
6
+ from pyoq.generation.model import (
7
+ GeneratedPackage,
8
+ GeneratedRelation,
9
+ GeneratedTable,
10
+ )
11
+ from pyoq.generation.rendering.source import (
12
+ PythonSource,
13
+ literal,
14
+ render_all,
15
+ render_from_import,
16
+ render_string_tuple,
17
+ render_subscript,
18
+ )
19
+ from pyoq.schema import Identifier
20
+
21
+
22
+ def render_relations(package: GeneratedPackage) -> RenderedFile:
23
+ source = PythonSource()
24
+ source.line('"""Generated typed relationship descriptors."""')
25
+ has_relations = any(table.relations for table in package.tables)
26
+ if has_relations:
27
+ source.line()
28
+ source.line("from __future__ import annotations")
29
+ source.line()
30
+ source.line("from pyoq.descriptors import RelationshipDescriptor")
31
+ source.line("from pyoq.relations import RelationCardinality, RelationDirection")
32
+ source.line("from pyoq.schema import ReferentialAction")
33
+ source.line()
34
+ source.lines(
35
+ render_from_import(
36
+ ".rows",
37
+ _imported_row_names(package),
38
+ )
39
+ )
40
+ _render_all_relations(source, package)
41
+ source.line()
42
+ if has_relations:
43
+ source.line()
44
+ source.lines(render_all(_exported_names(package)))
45
+ return RenderedFile(PurePosixPath("relations.py"), source.render())
46
+
47
+
48
+ def _render_all_relations(source: PythonSource, package: GeneratedPackage) -> None:
49
+ """One blank line separates the imports from the first constant.
50
+
51
+ Two is what a definition gets. These are assignments, and the import
52
+ ordering check reads the difference.
53
+ """
54
+ written = False
55
+ for table in package.tables:
56
+ for relation in table.relations:
57
+ source.line()
58
+ if written:
59
+ source.line()
60
+ _render_relation(source, table, relation)
61
+ written = True
62
+
63
+
64
+ def _render_relation(
65
+ source: PythonSource,
66
+ table: GeneratedTable,
67
+ relation: GeneratedRelation,
68
+ ) -> None:
69
+ fact = relation.source
70
+ source.lines(
71
+ render_subscript(
72
+ f"{relation.constant_name}: ",
73
+ "RelationshipDescriptor",
74
+ (table.row_name, relation.target_row_name),
75
+ argument_indent=" ",
76
+ closing_indent="",
77
+ suffix=" = RelationshipDescriptor(",
78
+ )
79
+ )
80
+ source.line(f" source_table={literal(fact.source.table.name.value)},")
81
+ source.line(f" source_columns={_columns(fact.source.columns)},")
82
+ source.line(f" target_table={literal(fact.target.table.name.value)},")
83
+ source.line(f" target_columns={_columns(fact.target.columns)},")
84
+ source.line(f" source_schema={literal(_part(fact.source.table.schema))},")
85
+ source.line(f" source_catalog={literal(_part(fact.source.table.catalog))},")
86
+ source.line(f" target_schema={literal(_part(fact.target.table.schema))},")
87
+ source.line(f" target_catalog={literal(_part(fact.target.table.catalog))},")
88
+ database_name = fact.constraint.value if fact.constraint is not None else None
89
+ source.line(f" database_name={literal(database_name)},")
90
+ source.line(f" on_update=ReferentialAction.{fact.on_update.name},")
91
+ source.line(f" on_delete=ReferentialAction.{fact.on_delete.name},")
92
+ source.line(f" direction=RelationDirection.{fact.direction.name},")
93
+ source.line(f" cardinality=RelationCardinality.{fact.cardinality.name},")
94
+ source.line(f" optional={fact.optional!r},")
95
+ source.line(f" target_row_type={relation.target_row_name},")
96
+ source.line(")")
97
+
98
+
99
+ def _part(value: Identifier | None) -> str | None:
100
+ return None if value is None else value.value
101
+
102
+
103
+ def _imported_row_names(package: GeneratedPackage) -> tuple[str, ...]:
104
+ """Only the rows this file names, because an unused import is a defect.
105
+
106
+ A table without relations is never mentioned here, so importing its row
107
+ would leave the generated package failing a check the project runs.
108
+ """
109
+ return tuple(
110
+ row_name
111
+ for table in package.tables
112
+ if table.relations
113
+ for row_name in (
114
+ table.row_name,
115
+ *(relation.target_row_name for relation in table.relations),
116
+ )
117
+ if row_name != "object"
118
+ )
119
+
120
+
121
+ def _columns(columns: tuple[Identifier, ...]) -> str:
122
+ return render_string_tuple(tuple(column.value for column in columns))
123
+
124
+
125
+ def _exported_names(package: GeneratedPackage) -> tuple[str, ...]:
126
+ return tuple(
127
+ relation.constant_name
128
+ for table in package.tables
129
+ for relation in table.relations
130
+ )
131
+
132
+
133
+ __all__ = ("render_relations",)