pyoq-sql 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. pyoq/__init__.py +7 -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 +19 -0
  9. pyoq/config/loader.py +204 -0
  10. pyoq/config/models.py +243 -0
  11. pyoq/descriptors.py +165 -0
  12. pyoq/diagnostics/__init__.py +68 -0
  13. pyoq/diagnostics/budget.py +136 -0
  14. pyoq/diagnostics/events.py +137 -0
  15. pyoq/diagnostics/fingerprint.py +267 -0
  16. pyoq/diagnostics/instrumented.py +237 -0
  17. pyoq/diagnostics/metrics.py +61 -0
  18. pyoq/diagnostics/observation.py +227 -0
  19. pyoq/diagnostics/scoped.py +103 -0
  20. pyoq/django/__init__.py +15 -0
  21. pyoq/django/apps.py +17 -0
  22. pyoq/django/execution.py +317 -0
  23. pyoq/django/generation.py +59 -0
  24. pyoq/django/management/__init__.py +0 -0
  25. pyoq/django/management/commands/__init__.py +0 -0
  26. pyoq/django/management/commands/makemigrations.py +53 -0
  27. pyoq/django/management/commands/pyoq_codegen.py +75 -0
  28. pyoq/django/parameters.py +101 -0
  29. pyoq/django/schema.py +379 -0
  30. pyoq/django/settings.py +87 -0
  31. pyoq/django/timeouts.py +105 -0
  32. pyoq/dsl/__init__.py +64 -0
  33. pyoq/dsl/aio/__init__.py +31 -0
  34. pyoq/dsl/aio/context.py +295 -0
  35. pyoq/dsl/aio/queries.py +335 -0
  36. pyoq/dsl/aio/writes.py +368 -0
  37. pyoq/dsl/context.py +326 -0
  38. pyoq/dsl/entry.py +37 -0
  39. pyoq/dsl/labels.py +36 -0
  40. pyoq/dsl/queries.py +339 -0
  41. pyoq/dsl/result.py +164 -0
  42. pyoq/dsl/writes.py +360 -0
  43. pyoq/errors.py +317 -0
  44. pyoq/fastapi/__init__.py +32 -0
  45. pyoq/fastapi/dependencies.py +167 -0
  46. pyoq/fastapi/lifespan.py +119 -0
  47. pyoq/fetching/__init__.py +55 -0
  48. pyoq/fetching/collections.py +136 -0
  49. pyoq/fetching/execution.py +587 -0
  50. pyoq/fetching/joined.py +79 -0
  51. pyoq/fetching/nesting.py +183 -0
  52. pyoq/fetching/plans.py +541 -0
  53. pyoq/fetching/select_in.py +149 -0
  54. pyoq/fetching/tables.py +110 -0
  55. pyoq/generation/__init__.py +54 -0
  56. pyoq/generation/cleanup.py +44 -0
  57. pyoq/generation/contracts.py +248 -0
  58. pyoq/generation/drift.py +169 -0
  59. pyoq/generation/lock.py +33 -0
  60. pyoq/generation/manifest.py +114 -0
  61. pyoq/generation/model.py +1001 -0
  62. pyoq/generation/pipeline.py +119 -0
  63. pyoq/generation/rendering/__init__.py +5 -0
  64. pyoq/generation/rendering/domains.py +51 -0
  65. pyoq/generation/rendering/enums.py +29 -0
  66. pyoq/generation/rendering/exports.py +70 -0
  67. pyoq/generation/rendering/imports.py +63 -0
  68. pyoq/generation/rendering/package.py +56 -0
  69. pyoq/generation/rendering/relations.py +133 -0
  70. pyoq/generation/rendering/routines.py +396 -0
  71. pyoq/generation/rendering/rows.py +79 -0
  72. pyoq/generation/rendering/source.py +121 -0
  73. pyoq/generation/rendering/tables.py +300 -0
  74. pyoq/generation/rendering/writes.py +514 -0
  75. pyoq/generation/validation.py +27 -0
  76. pyoq/generation/writer.py +184 -0
  77. pyoq/hydration/__init__.py +24 -0
  78. pyoq/hydration/engine.py +155 -0
  79. pyoq/hydration/identity.py +194 -0
  80. pyoq/hydration/plan.py +116 -0
  81. pyoq/migrations/__init__.py +9 -0
  82. pyoq/migrations/alembic.py +106 -0
  83. pyoq/migrations/hooks.py +75 -0
  84. pyoq/naming.py +261 -0
  85. pyoq/policies/__init__.py +47 -0
  86. pyoq/policies/bypass.py +122 -0
  87. pyoq/policies/governed.py +430 -0
  88. pyoq/policies/model.py +242 -0
  89. pyoq/policies/rewriting.py +263 -0
  90. pyoq/py.typed +1 -0
  91. pyoq/query/__init__.py +312 -0
  92. pyoq/query/aggregates.py +172 -0
  93. pyoq/query/arrays.py +65 -0
  94. pyoq/query/binding.py +52 -0
  95. pyoq/query/capabilities.py +317 -0
  96. pyoq/query/casts.py +73 -0
  97. pyoq/query/choices.py +185 -0
  98. pyoq/query/decoding.py +360 -0
  99. pyoq/query/documents.py +56 -0
  100. pyoq/query/execution/__init__.py +63 -0
  101. pyoq/query/execution/aio/__init__.py +31 -0
  102. pyoq/query/execution/aio/operations.py +228 -0
  103. pyoq/query/execution/aio/pooling.py +233 -0
  104. pyoq/query/execution/aio/streaming.py +161 -0
  105. pyoq/query/execution/aio/transactions.py +105 -0
  106. pyoq/query/execution/batch.py +96 -0
  107. pyoq/query/execution/binding_style.py +30 -0
  108. pyoq/query/execution/compilation.py +48 -0
  109. pyoq/query/execution/context.py +61 -0
  110. pyoq/query/execution/control.py +50 -0
  111. pyoq/query/execution/operations.py +224 -0
  112. pyoq/query/execution/planning.py +107 -0
  113. pyoq/query/execution/pooling.py +279 -0
  114. pyoq/query/execution/results.py +36 -0
  115. pyoq/query/execution/streaming.py +178 -0
  116. pyoq/query/execution/transactions.py +95 -0
  117. pyoq/query/expressions.py +1200 -0
  118. pyoq/query/fields.py +60 -0
  119. pyoq/query/mysql/__init__.py +59 -0
  120. pyoq/query/mysql/aio/__init__.py +38 -0
  121. pyoq/query/mysql/aio/commands.py +389 -0
  122. pyoq/query/mysql/aio/driver.py +196 -0
  123. pyoq/query/mysql/aio/executor.py +123 -0
  124. pyoq/query/mysql/aio/factory.py +26 -0
  125. pyoq/query/mysql/aio/operations.py +38 -0
  126. pyoq/query/mysql/aio/pool.py +53 -0
  127. pyoq/query/mysql/aio/transactions.py +313 -0
  128. pyoq/query/mysql/commands.py +354 -0
  129. pyoq/query/mysql/compiler.py +134 -0
  130. pyoq/query/mysql/context.py +20 -0
  131. pyoq/query/mysql/executor.py +126 -0
  132. pyoq/query/mysql/expressions.py +244 -0
  133. pyoq/query/mysql/factory.py +46 -0
  134. pyoq/query/mysql/health.py +66 -0
  135. pyoq/query/mysql/identifiers.py +9 -0
  136. pyoq/query/mysql/model.py +79 -0
  137. pyoq/query/mysql/operations.py +43 -0
  138. pyoq/query/mysql/parameters.py +69 -0
  139. pyoq/query/mysql/planning.py +20 -0
  140. pyoq/query/mysql/pool.py +67 -0
  141. pyoq/query/mysql/transactions.py +331 -0
  142. pyoq/query/mysql/writes.py +73 -0
  143. pyoq/query/nodes.py +750 -0
  144. pyoq/query/postgres/__init__.py +48 -0
  145. pyoq/query/postgres/aio/__init__.py +25 -0
  146. pyoq/query/postgres/aio/bulk.py +56 -0
  147. pyoq/query/postgres/aio/commands.py +264 -0
  148. pyoq/query/postgres/aio/executor.py +152 -0
  149. pyoq/query/postgres/aio/factory.py +26 -0
  150. pyoq/query/postgres/aio/operations.py +26 -0
  151. pyoq/query/postgres/aio/pool.py +40 -0
  152. pyoq/query/postgres/aio/transactions.py +295 -0
  153. pyoq/query/postgres/bulk.py +62 -0
  154. pyoq/query/postgres/commands.py +238 -0
  155. pyoq/query/postgres/compiler.py +114 -0
  156. pyoq/query/postgres/context.py +20 -0
  157. pyoq/query/postgres/executor.py +147 -0
  158. pyoq/query/postgres/expressions.py +311 -0
  159. pyoq/query/postgres/factory.py +24 -0
  160. pyoq/query/postgres/health.py +24 -0
  161. pyoq/query/postgres/identifiers.py +9 -0
  162. pyoq/query/postgres/model.py +81 -0
  163. pyoq/query/postgres/operations.py +25 -0
  164. pyoq/query/postgres/parameters.py +71 -0
  165. pyoq/query/postgres/planning.py +20 -0
  166. pyoq/query/postgres/pool.py +52 -0
  167. pyoq/query/postgres/transactions.py +295 -0
  168. pyoq/query/postgres/writes.py +37 -0
  169. pyoq/query/projections.py +105 -0
  170. pyoq/query/raw.py +90 -0
  171. pyoq/query/recursion.py +265 -0
  172. pyoq/query/rendering/__init__.py +1 -0
  173. pyoq/query/rendering/expressions.py +913 -0
  174. pyoq/query/rendering/identifiers.py +40 -0
  175. pyoq/query/rendering/projections.py +63 -0
  176. pyoq/query/rendering/queries.py +334 -0
  177. pyoq/query/rendering/sources.py +66 -0
  178. pyoq/query/rendering/writes.py +176 -0
  179. pyoq/query/results.py +459 -0
  180. pyoq/query/routines.py +196 -0
  181. pyoq/query/rows.py +156 -0
  182. pyoq/query/select.py +793 -0
  183. pyoq/query/select_nodes.py +277 -0
  184. pyoq/query/sources.py +236 -0
  185. pyoq/query/sqlite/__init__.py +43 -0
  186. pyoq/query/sqlite/commands.py +201 -0
  187. pyoq/query/sqlite/compiler.py +139 -0
  188. pyoq/query/sqlite/context.py +20 -0
  189. pyoq/query/sqlite/executor.py +119 -0
  190. pyoq/query/sqlite/expressions.py +224 -0
  191. pyoq/query/sqlite/factory.py +32 -0
  192. pyoq/query/sqlite/health.py +28 -0
  193. pyoq/query/sqlite/identifiers.py +9 -0
  194. pyoq/query/sqlite/model.py +73 -0
  195. pyoq/query/sqlite/operations.py +36 -0
  196. pyoq/query/sqlite/parameters.py +50 -0
  197. pyoq/query/sqlite/planning.py +20 -0
  198. pyoq/query/sqlite/pool.py +50 -0
  199. pyoq/query/sqlite/streaming.py +13 -0
  200. pyoq/query/sqlite/transactions.py +274 -0
  201. pyoq/query/sqlite/writes.py +35 -0
  202. pyoq/query/statements.py +27 -0
  203. pyoq/query/values.py +23 -0
  204. pyoq/query/vendor.py +162 -0
  205. pyoq/query/windows.py +424 -0
  206. pyoq/query/write_nodes.py +174 -0
  207. pyoq/query/writes.py +628 -0
  208. pyoq/relations/__init__.py +66 -0
  209. pyoq/relations/batching.py +219 -0
  210. pyoq/relations/derivation.py +111 -0
  211. pyoq/relations/fetching.py +355 -0
  212. pyoq/relations/graph.py +245 -0
  213. pyoq/relations/loading.py +74 -0
  214. pyoq/relations/model.py +75 -0
  215. pyoq/relations/planning.py +206 -0
  216. pyoq/runtime/__init__.py +9 -0
  217. pyoq/runtime/kernels.py +25 -0
  218. pyoq/runtime/python.py +43 -0
  219. pyoq/runtime/selection.py +73 -0
  220. pyoq/sanic/__init__.py +32 -0
  221. pyoq/sanic/scope.py +197 -0
  222. pyoq/sanic/workers.py +129 -0
  223. pyoq/schema/__init__.py +108 -0
  224. pyoq/schema/codec.py +711 -0
  225. pyoq/schema/models.py +604 -0
  226. pyoq/schema/mysql/__init__.py +16 -0
  227. pyoq/schema/mysql/connection.py +72 -0
  228. pyoq/schema/mysql/dsn.py +72 -0
  229. pyoq/schema/mysql/records.py +354 -0
  230. pyoq/schema/mysql/reflection.py +309 -0
  231. pyoq/schema/mysql/source.py +30 -0
  232. pyoq/schema/mysql/sql.py +128 -0
  233. pyoq/schema/mysql/types.py +105 -0
  234. pyoq/schema/postgres/__init__.py +13 -0
  235. pyoq/schema/postgres/connection.py +62 -0
  236. pyoq/schema/postgres/records.py +384 -0
  237. pyoq/schema/postgres/reflection.py +466 -0
  238. pyoq/schema/postgres/source.py +30 -0
  239. pyoq/schema/postgres/sql.py +246 -0
  240. pyoq/schema/postgres/types.py +98 -0
  241. pyoq/schema/registry.py +45 -0
  242. pyoq/schema/source.py +15 -0
  243. pyoq/schema/sqlite/__init__.py +6 -0
  244. pyoq/schema/sqlite/connection.py +54 -0
  245. pyoq/schema/sqlite/records.py +167 -0
  246. pyoq/schema/sqlite/reflection.py +393 -0
  247. pyoq/schema/sqlite/source.py +30 -0
  248. pyoq/schema/sqlite/sql.py +254 -0
  249. pyoq/schema/sqlite/types.py +74 -0
  250. pyoq/serving/__init__.py +19 -0
  251. pyoq/serving/databases.py +107 -0
  252. pyoq/snapshots/__init__.py +20 -0
  253. pyoq/snapshots/drift.py +312 -0
  254. pyoq/snapshots/files.py +96 -0
  255. pyoq/snapshots/routing.py +40 -0
  256. pyoq/snapshots/source.py +33 -0
  257. pyoq/tracing/__init__.py +5 -0
  258. pyoq/tracing/spans.py +89 -0
  259. pyoq/unset.py +14 -0
  260. pyoq_sql-1.0.0.dist-info/METADATA +3034 -0
  261. pyoq_sql-1.0.0.dist-info/RECORD +264 -0
  262. pyoq_sql-1.0.0.dist-info/WHEEL +4 -0
  263. pyoq_sql-1.0.0.dist-info/entry_points.txt +3 -0
  264. pyoq_sql-1.0.0.dist-info/licenses/LICENSE +373 -0
pyoq/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """Public package interface for PyOQ."""
2
+
3
+ from importlib.metadata import version
4
+
5
+ __version__ = version("pyoq-sql")
6
+
7
+ __all__ = ("__version__",)
pyoq/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Module command entry point."""
2
+
3
+ from pyoq.cli import run
4
+
5
+ run()
pyoq/_native.pyi ADDED
@@ -0,0 +1,5 @@
1
+ from collections.abc import Sequence
2
+
3
+ def fingerprint_fragments(fragments: Sequence[bytes]) -> int: ...
4
+ def group_integer_keys(keys: Sequence[int | None]) -> list[tuple[int, int]]: ...
5
+ def runtime_marker() -> str: ...
pyoq/cli/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Public command-line interface."""
2
+
3
+ from pyoq.cli.commands import ExitCode, main, run
4
+
5
+ __all__ = ("ExitCode", "main", "run")
pyoq/cli/commands.py ADDED
@@ -0,0 +1,270 @@
1
+ """Stable command-line interface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+ from collections.abc import Sequence
8
+ from dataclasses import dataclass
9
+ from enum import IntEnum, StrEnum
10
+ from pathlib import Path
11
+ from typing import IO, NoReturn, cast
12
+
13
+ from pyoq import __version__
14
+ from pyoq.cli.services import CommandServices, GenerationMode
15
+ from pyoq.config import Configuration, load_configuration
16
+ from pyoq.errors import (
17
+ CommandError,
18
+ ConfigurationError,
19
+ OperationUnavailableError,
20
+ PyOQError,
21
+ )
22
+
23
+
24
+ class ExitCode(IntEnum):
25
+ SUCCESS = 0
26
+ COMMAND_FAILED = 1
27
+ USAGE = 2
28
+ CONFIGURATION = 3
29
+ UNAVAILABLE = 4
30
+
31
+
32
+ class CommandName(StrEnum):
33
+ GENERATE = "generate"
34
+ INSPECT = "inspect"
35
+ SNAPSHOT = "snapshot"
36
+ DRIFT = "drift"
37
+
38
+
39
+ @dataclass(frozen=True, slots=True)
40
+ class ConfigurationSource:
41
+ project_root: Path
42
+ pyproject_path: Path | None
43
+ selected_profile: str | None
44
+
45
+
46
+ @dataclass(frozen=True, slots=True)
47
+ class GenerateCommand:
48
+ source: ConfigurationSource
49
+ mode: GenerationMode
50
+
51
+
52
+ @dataclass(frozen=True, slots=True)
53
+ class InspectCommand:
54
+ source: ConfigurationSource
55
+
56
+
57
+ @dataclass(frozen=True, slots=True)
58
+ class SnapshotCommand:
59
+ source: ConfigurationSource
60
+
61
+
62
+ @dataclass(frozen=True, slots=True)
63
+ class DriftCommand:
64
+ source: ConfigurationSource
65
+
66
+
67
+ ParsedCommand = GenerateCommand | InspectCommand | SnapshotCommand | DriftCommand
68
+
69
+
70
+ class ParserExitError(Exception):
71
+ def __init__(self, status: int, message: str | None) -> None:
72
+ super().__init__(message)
73
+ self.status = status
74
+ self.message = message
75
+
76
+
77
+ class CommandParser(argparse.ArgumentParser):
78
+ _output: IO[str]
79
+
80
+ def set_output(self, output: IO[str]) -> None:
81
+ self._output = output
82
+
83
+ def _print_message(self, message: str, file: object | None = None) -> None:
84
+ self._output.write(message or "")
85
+
86
+ def exit(self, status: int = 0, message: str | None = None) -> NoReturn:
87
+ raise ParserExitError(status, message)
88
+
89
+ def error(self, message: str) -> NoReturn:
90
+ formatted = f"{self.format_usage()}{self.prog}: error: {message}\n"
91
+ raise ParserExitError(ExitCode.USAGE, formatted)
92
+
93
+
94
+ def create_parser(output: IO[str] | None = None) -> CommandParser:
95
+ command_output = sys.stdout if output is None else output
96
+ parser = CommandParser(prog="pyoq", description="Typed database tooling")
97
+ parser.set_output(command_output)
98
+ parser.add_argument(
99
+ "--version",
100
+ action="version",
101
+ version=f"%(prog)s {__version__}",
102
+ )
103
+ subparsers = parser.add_subparsers(dest="command", required=True)
104
+
105
+ generate = subparsers.add_parser(
106
+ CommandName.GENERATE,
107
+ help="generate typed database code",
108
+ )
109
+ generate.set_output(command_output)
110
+ _add_configuration_arguments(generate)
111
+ modes = generate.add_mutually_exclusive_group()
112
+ modes.add_argument(
113
+ "--check",
114
+ action="store_true",
115
+ help="fail when generated files are not current",
116
+ )
117
+ modes.add_argument(
118
+ "--dry-run",
119
+ action="store_true",
120
+ help="show planned changes without writing files",
121
+ )
122
+
123
+ inspect = subparsers.add_parser(
124
+ CommandName.INSPECT,
125
+ help="inspect configured database metadata",
126
+ )
127
+ inspect.set_output(command_output)
128
+ _add_configuration_arguments(inspect)
129
+
130
+ snapshot = subparsers.add_parser(
131
+ CommandName.SNAPSHOT,
132
+ help="record the database schema in the configured snapshot file",
133
+ )
134
+ snapshot.set_output(command_output)
135
+ _add_configuration_arguments(snapshot)
136
+
137
+ drift = subparsers.add_parser(
138
+ CommandName.DRIFT,
139
+ help="compare the recorded schema against the database",
140
+ )
141
+ drift.set_output(command_output)
142
+ _add_configuration_arguments(drift)
143
+ return parser
144
+
145
+
146
+ def main(
147
+ arguments: Sequence[str] | None = None,
148
+ *,
149
+ services: CommandServices | None = None,
150
+ output: IO[str] | None = None,
151
+ error: IO[str] | None = None,
152
+ ) -> int:
153
+ standard_output = sys.stdout if output is None else output
154
+ standard_error = sys.stderr if error is None else error
155
+ parser = create_parser(standard_output)
156
+ try:
157
+ namespace = parser.parse_args(arguments)
158
+ command = _parsed_command(namespace)
159
+ selected_services = (
160
+ services if services is not None else CommandServices.defaults()
161
+ )
162
+ _execute(command, selected_services, standard_output)
163
+ except ParserExitError as parser_exit:
164
+ return _handle_parser_exit(parser_exit, standard_output, standard_error)
165
+ except ConfigurationError as configuration_error:
166
+ _write_error(standard_error, "configuration error", configuration_error)
167
+ return ExitCode.CONFIGURATION
168
+ except OperationUnavailableError as unavailable_error:
169
+ _write_error(standard_error, "unavailable", unavailable_error)
170
+ return ExitCode.UNAVAILABLE
171
+ except CommandError as command_error:
172
+ _write_error(standard_error, "command failed", command_error)
173
+ return ExitCode.COMMAND_FAILED
174
+ except PyOQError as failure:
175
+ # Everything PyOQ raises is deliberate, so a command reports it rather
176
+ # than letting a traceback stand in for a message.
177
+ _write_error(standard_error, "command failed", failure)
178
+ return ExitCode.COMMAND_FAILED
179
+ return ExitCode.SUCCESS
180
+
181
+
182
+ def run(arguments: Sequence[str] | None = None) -> NoReturn:
183
+ raise SystemExit(main(arguments))
184
+
185
+
186
+ def _add_configuration_arguments(parser: argparse.ArgumentParser) -> None:
187
+ parser.add_argument(
188
+ "--project-root",
189
+ type=Path,
190
+ default=Path.cwd(),
191
+ help="project root containing pyproject.toml",
192
+ )
193
+ parser.add_argument(
194
+ "--config",
195
+ type=Path,
196
+ dest="pyproject_path",
197
+ help="configuration file relative to the project root",
198
+ )
199
+ parser.add_argument(
200
+ "--profile",
201
+ dest="selected_profile",
202
+ help="configuration profile to select",
203
+ )
204
+
205
+
206
+ def _parsed_command(namespace: argparse.Namespace) -> ParsedCommand:
207
+ source = ConfigurationSource(
208
+ project_root=cast(Path, namespace.project_root),
209
+ pyproject_path=cast(Path | None, namespace.pyproject_path),
210
+ selected_profile=cast(str | None, namespace.selected_profile),
211
+ )
212
+ command = CommandName(cast(str, namespace.command))
213
+ if command is CommandName.GENERATE:
214
+ return GenerateCommand(source, _generation_mode(namespace))
215
+ if command is CommandName.SNAPSHOT:
216
+ return SnapshotCommand(source)
217
+ if command is CommandName.DRIFT:
218
+ return DriftCommand(source)
219
+ return InspectCommand(source)
220
+
221
+
222
+ def _generation_mode(namespace: argparse.Namespace) -> GenerationMode:
223
+ if cast(bool, namespace.check):
224
+ return GenerationMode.CHECK
225
+ if cast(bool, namespace.dry_run):
226
+ return GenerationMode.DRY_RUN
227
+ return GenerationMode.WRITE
228
+
229
+
230
+ def _execute(
231
+ command: ParsedCommand,
232
+ services: CommandServices,
233
+ output: IO[str],
234
+ ) -> None:
235
+ configuration = _load_source(command.source)
236
+ if isinstance(command, GenerateCommand):
237
+ message = services.generation.execute(configuration, command.mode)
238
+ elif isinstance(command, SnapshotCommand):
239
+ message = services.snapshot.execute(configuration)
240
+ elif isinstance(command, DriftCommand):
241
+ message = services.drift.execute(configuration)
242
+ else:
243
+ message = services.inspection.execute(configuration)
244
+ output.write(f"{message}\n")
245
+
246
+
247
+ def _load_source(source: ConfigurationSource) -> Configuration:
248
+ return load_configuration(
249
+ source.project_root,
250
+ pyproject_path=source.pyproject_path,
251
+ selected_profile=source.selected_profile,
252
+ )
253
+
254
+
255
+ def _handle_parser_exit(
256
+ parser_exit: ParserExitError,
257
+ output: IO[str],
258
+ error: IO[str],
259
+ ) -> int:
260
+ if parser_exit.message:
261
+ destination = output if parser_exit.status == ExitCode.SUCCESS else error
262
+ destination.write(parser_exit.message)
263
+ return parser_exit.status
264
+
265
+
266
+ def _write_error(output: IO[str], prefix: str, error: Exception) -> None:
267
+ output.write(f"{prefix}: {error}\n")
268
+
269
+
270
+ __all__ = ("ExitCode", "main", "run")
pyoq/cli/defaults.py ADDED
@@ -0,0 +1,98 @@
1
+ """Default database command service assembly."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+ from pyoq.config import Configuration
9
+ from pyoq.errors import ConfigurationValidationError, SchemaDriftError
10
+ from pyoq.generation import (
11
+ AtomicGenerationWriter,
12
+ DirectoryProjectLock,
13
+ FileDriftChecker,
14
+ GeneratedTypesRenderer,
15
+ GenerationPipeline,
16
+ GenerationPlanner,
17
+ JsonManifestStore,
18
+ ManifestOwnedCleanup,
19
+ PythonSyntaxValidator,
20
+ )
21
+ from pyoq.schema import SchemaSource
22
+ from pyoq.snapshots import compare_snapshots, read_snapshot, write_snapshot
23
+
24
+
25
+ @dataclass(frozen=True, slots=True)
26
+ class SchemaInspectionService:
27
+ source: SchemaSource
28
+
29
+ def execute(self, configuration: Configuration) -> str:
30
+ snapshot = self.source.load(configuration)
31
+ table_count = sum(
32
+ len(schema.tables)
33
+ for catalog in snapshot.catalogs
34
+ for schema in catalog.schemas
35
+ )
36
+ view_count = sum(
37
+ len(schema.views)
38
+ for catalog in snapshot.catalogs
39
+ for schema in catalog.schemas
40
+ )
41
+ return f"inspected tables: {table_count}; views: {view_count}"
42
+
43
+
44
+ def default_generation_pipeline(source: SchemaSource) -> GenerationPipeline:
45
+ manifests = JsonManifestStore()
46
+ return GenerationPipeline(
47
+ schema_source=source,
48
+ planner=GenerationPlanner((GeneratedTypesRenderer(),)),
49
+ validator=PythonSyntaxValidator(),
50
+ manifest_store=manifests,
51
+ drift_checker=FileDriftChecker(),
52
+ writer=AtomicGenerationWriter(manifests, ManifestOwnedCleanup()),
53
+ project_lock=DirectoryProjectLock(),
54
+ )
55
+
56
+
57
+ __all__ = ("SchemaInspectionService", "default_generation_pipeline")
58
+
59
+
60
+ @dataclass(frozen=True, slots=True)
61
+ class SnapshotRecordingService:
62
+ """Writing what the database holds to the file a project reviews."""
63
+
64
+ source: SchemaSource
65
+
66
+ def execute(self, configuration: Configuration) -> str:
67
+ destination = _required_snapshot_path(configuration)
68
+ snapshot = self.source.load(configuration)
69
+ write_snapshot(snapshot, destination)
70
+ return f"recorded the schema in {destination}"
71
+
72
+
73
+ @dataclass(frozen=True, slots=True)
74
+ class SchemaDriftService:
75
+ """Comparing the recorded schema against what the database now holds."""
76
+
77
+ source: SchemaSource
78
+
79
+ def execute(self, configuration: Configuration) -> str:
80
+ recorded_path = _required_snapshot_path(configuration)
81
+ recorded = read_snapshot(recorded_path)
82
+ observed = self.source.load(configuration)
83
+ report = compare_snapshots(recorded, observed)
84
+ if report.is_current:
85
+ return report.summary()
86
+ raise SchemaDriftError(report.summary())
87
+
88
+
89
+ def _required_snapshot_path(configuration: Configuration) -> Path:
90
+ """Where the schema is recorded, which a project has to have chosen."""
91
+ destination = configuration.schema_snapshot_path
92
+ if destination is None:
93
+ message = (
94
+ "no schema snapshot is configured; set tool.pyoq.schema-snapshot "
95
+ "to the file the schema is recorded in"
96
+ )
97
+ raise ConfigurationValidationError(message)
98
+ return destination
pyoq/cli/services.py ADDED
@@ -0,0 +1,97 @@
1
+ """Command service boundaries."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Protocol
7
+
8
+ from pyoq.config import Configuration
9
+ from pyoq.errors import OperationUnavailableError
10
+ from pyoq.generation import GenerationMode
11
+
12
+
13
+ class GenerationService(Protocol):
14
+ def execute(self, configuration: Configuration, mode: GenerationMode) -> str: ...
15
+
16
+
17
+ class InspectionService(Protocol):
18
+ def execute(self, configuration: Configuration) -> str: ...
19
+
20
+
21
+ class SnapshotService(Protocol):
22
+ def execute(self, configuration: Configuration) -> str: ...
23
+
24
+
25
+ class DriftService(Protocol):
26
+ def execute(self, configuration: Configuration) -> str: ...
27
+
28
+
29
+ class UnavailableGenerationService:
30
+ def execute(self, configuration: Configuration, mode: GenerationMode) -> str:
31
+ message = "schema generation is not available"
32
+ raise OperationUnavailableError(message)
33
+
34
+
35
+ class UnavailableInspectionService:
36
+ def execute(self, configuration: Configuration) -> str:
37
+ message = "schema inspection is not available"
38
+ raise OperationUnavailableError(message)
39
+
40
+
41
+ class UnavailableSnapshotService:
42
+ def execute(self, configuration: Configuration) -> str:
43
+ message = "recording a schema snapshot is not available"
44
+ raise OperationUnavailableError(message)
45
+
46
+
47
+ class UnavailableDriftService:
48
+ def execute(self, configuration: Configuration) -> str:
49
+ message = "schema drift comparison is not available"
50
+ raise OperationUnavailableError(message)
51
+
52
+
53
+ @dataclass(frozen=True, slots=True)
54
+ class CommandServices:
55
+ generation: GenerationService
56
+ inspection: InspectionService
57
+ snapshot: SnapshotService
58
+ drift: DriftService
59
+
60
+ @classmethod
61
+ def unavailable(cls) -> CommandServices:
62
+ return cls(
63
+ generation=UnavailableGenerationService(),
64
+ inspection=UnavailableInspectionService(),
65
+ snapshot=UnavailableSnapshotService(),
66
+ drift=UnavailableDriftService(),
67
+ )
68
+
69
+ @classmethod
70
+ def defaults(cls) -> CommandServices:
71
+ from pyoq.cli.defaults import (
72
+ SchemaDriftService,
73
+ SchemaInspectionService,
74
+ SnapshotRecordingService,
75
+ default_generation_pipeline,
76
+ )
77
+ from pyoq.schema import DialectRoutingSchemaSource
78
+ from pyoq.snapshots.routing import ConfiguredSchemaSource
79
+
80
+ configured = ConfiguredSchemaSource()
81
+ live = DialectRoutingSchemaSource()
82
+ return cls(
83
+ generation=default_generation_pipeline(configured),
84
+ inspection=SchemaInspectionService(configured),
85
+ snapshot=SnapshotRecordingService(live),
86
+ drift=SchemaDriftService(live),
87
+ )
88
+
89
+
90
+ __all__ = (
91
+ "CommandServices",
92
+ "DriftService",
93
+ "GenerationMode",
94
+ "GenerationService",
95
+ "InspectionService",
96
+ "SnapshotService",
97
+ )
@@ -0,0 +1,19 @@
1
+ """Public configuration interface."""
2
+
3
+ from pyoq.config.models import (
4
+ Configuration,
5
+ DatabaseDialect,
6
+ DatabaseProfile,
7
+ EnvironmentReference,
8
+ SecretValue,
9
+ load_configuration,
10
+ )
11
+
12
+ __all__ = (
13
+ "Configuration",
14
+ "DatabaseDialect",
15
+ "DatabaseProfile",
16
+ "EnvironmentReference",
17
+ "SecretValue",
18
+ "load_configuration",
19
+ )