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.
- pyoq/__init__.py +7 -0
- pyoq/__main__.py +5 -0
- pyoq/_native.pyi +5 -0
- pyoq/cli/__init__.py +5 -0
- pyoq/cli/commands.py +270 -0
- pyoq/cli/defaults.py +98 -0
- pyoq/cli/services.py +97 -0
- pyoq/config/__init__.py +19 -0
- pyoq/config/loader.py +204 -0
- pyoq/config/models.py +243 -0
- pyoq/descriptors.py +165 -0
- pyoq/diagnostics/__init__.py +68 -0
- pyoq/diagnostics/budget.py +136 -0
- pyoq/diagnostics/events.py +137 -0
- pyoq/diagnostics/fingerprint.py +267 -0
- pyoq/diagnostics/instrumented.py +237 -0
- pyoq/diagnostics/metrics.py +61 -0
- pyoq/diagnostics/observation.py +227 -0
- pyoq/diagnostics/scoped.py +103 -0
- pyoq/django/__init__.py +15 -0
- pyoq/django/apps.py +17 -0
- pyoq/django/execution.py +317 -0
- pyoq/django/generation.py +59 -0
- pyoq/django/management/__init__.py +0 -0
- pyoq/django/management/commands/__init__.py +0 -0
- pyoq/django/management/commands/makemigrations.py +53 -0
- pyoq/django/management/commands/pyoq_codegen.py +75 -0
- pyoq/django/parameters.py +101 -0
- pyoq/django/schema.py +379 -0
- pyoq/django/settings.py +87 -0
- pyoq/django/timeouts.py +105 -0
- pyoq/dsl/__init__.py +64 -0
- pyoq/dsl/aio/__init__.py +31 -0
- pyoq/dsl/aio/context.py +295 -0
- pyoq/dsl/aio/queries.py +335 -0
- pyoq/dsl/aio/writes.py +368 -0
- pyoq/dsl/context.py +326 -0
- pyoq/dsl/entry.py +37 -0
- pyoq/dsl/labels.py +36 -0
- pyoq/dsl/queries.py +339 -0
- pyoq/dsl/result.py +164 -0
- pyoq/dsl/writes.py +360 -0
- pyoq/errors.py +317 -0
- pyoq/fastapi/__init__.py +32 -0
- pyoq/fastapi/dependencies.py +167 -0
- pyoq/fastapi/lifespan.py +119 -0
- pyoq/fetching/__init__.py +55 -0
- pyoq/fetching/collections.py +136 -0
- pyoq/fetching/execution.py +587 -0
- pyoq/fetching/joined.py +79 -0
- pyoq/fetching/nesting.py +183 -0
- pyoq/fetching/plans.py +541 -0
- pyoq/fetching/select_in.py +149 -0
- pyoq/fetching/tables.py +110 -0
- pyoq/generation/__init__.py +54 -0
- pyoq/generation/cleanup.py +44 -0
- pyoq/generation/contracts.py +248 -0
- pyoq/generation/drift.py +169 -0
- pyoq/generation/lock.py +33 -0
- pyoq/generation/manifest.py +114 -0
- pyoq/generation/model.py +1001 -0
- pyoq/generation/pipeline.py +119 -0
- pyoq/generation/rendering/__init__.py +5 -0
- pyoq/generation/rendering/domains.py +51 -0
- pyoq/generation/rendering/enums.py +29 -0
- pyoq/generation/rendering/exports.py +70 -0
- pyoq/generation/rendering/imports.py +63 -0
- pyoq/generation/rendering/package.py +56 -0
- pyoq/generation/rendering/relations.py +133 -0
- pyoq/generation/rendering/routines.py +396 -0
- pyoq/generation/rendering/rows.py +79 -0
- pyoq/generation/rendering/source.py +121 -0
- pyoq/generation/rendering/tables.py +300 -0
- pyoq/generation/rendering/writes.py +514 -0
- pyoq/generation/validation.py +27 -0
- pyoq/generation/writer.py +184 -0
- pyoq/hydration/__init__.py +24 -0
- pyoq/hydration/engine.py +155 -0
- pyoq/hydration/identity.py +194 -0
- pyoq/hydration/plan.py +116 -0
- pyoq/migrations/__init__.py +9 -0
- pyoq/migrations/alembic.py +106 -0
- pyoq/migrations/hooks.py +75 -0
- pyoq/naming.py +261 -0
- pyoq/policies/__init__.py +47 -0
- pyoq/policies/bypass.py +122 -0
- pyoq/policies/governed.py +430 -0
- pyoq/policies/model.py +242 -0
- pyoq/policies/rewriting.py +263 -0
- pyoq/py.typed +1 -0
- pyoq/query/__init__.py +312 -0
- pyoq/query/aggregates.py +172 -0
- pyoq/query/arrays.py +65 -0
- pyoq/query/binding.py +52 -0
- pyoq/query/capabilities.py +317 -0
- pyoq/query/casts.py +73 -0
- pyoq/query/choices.py +185 -0
- pyoq/query/decoding.py +360 -0
- pyoq/query/documents.py +56 -0
- pyoq/query/execution/__init__.py +63 -0
- pyoq/query/execution/aio/__init__.py +31 -0
- pyoq/query/execution/aio/operations.py +228 -0
- pyoq/query/execution/aio/pooling.py +233 -0
- pyoq/query/execution/aio/streaming.py +161 -0
- pyoq/query/execution/aio/transactions.py +105 -0
- pyoq/query/execution/batch.py +96 -0
- pyoq/query/execution/binding_style.py +30 -0
- pyoq/query/execution/compilation.py +48 -0
- pyoq/query/execution/context.py +61 -0
- pyoq/query/execution/control.py +50 -0
- pyoq/query/execution/operations.py +224 -0
- pyoq/query/execution/planning.py +107 -0
- pyoq/query/execution/pooling.py +279 -0
- pyoq/query/execution/results.py +36 -0
- pyoq/query/execution/streaming.py +178 -0
- pyoq/query/execution/transactions.py +95 -0
- pyoq/query/expressions.py +1200 -0
- pyoq/query/fields.py +60 -0
- pyoq/query/mysql/__init__.py +59 -0
- pyoq/query/mysql/aio/__init__.py +38 -0
- pyoq/query/mysql/aio/commands.py +389 -0
- pyoq/query/mysql/aio/driver.py +196 -0
- pyoq/query/mysql/aio/executor.py +123 -0
- pyoq/query/mysql/aio/factory.py +26 -0
- pyoq/query/mysql/aio/operations.py +38 -0
- pyoq/query/mysql/aio/pool.py +53 -0
- pyoq/query/mysql/aio/transactions.py +313 -0
- pyoq/query/mysql/commands.py +354 -0
- pyoq/query/mysql/compiler.py +134 -0
- pyoq/query/mysql/context.py +20 -0
- pyoq/query/mysql/executor.py +126 -0
- pyoq/query/mysql/expressions.py +244 -0
- pyoq/query/mysql/factory.py +46 -0
- pyoq/query/mysql/health.py +66 -0
- pyoq/query/mysql/identifiers.py +9 -0
- pyoq/query/mysql/model.py +79 -0
- pyoq/query/mysql/operations.py +43 -0
- pyoq/query/mysql/parameters.py +69 -0
- pyoq/query/mysql/planning.py +20 -0
- pyoq/query/mysql/pool.py +67 -0
- pyoq/query/mysql/transactions.py +331 -0
- pyoq/query/mysql/writes.py +73 -0
- pyoq/query/nodes.py +750 -0
- pyoq/query/postgres/__init__.py +48 -0
- pyoq/query/postgres/aio/__init__.py +25 -0
- pyoq/query/postgres/aio/bulk.py +56 -0
- pyoq/query/postgres/aio/commands.py +264 -0
- pyoq/query/postgres/aio/executor.py +152 -0
- pyoq/query/postgres/aio/factory.py +26 -0
- pyoq/query/postgres/aio/operations.py +26 -0
- pyoq/query/postgres/aio/pool.py +40 -0
- pyoq/query/postgres/aio/transactions.py +295 -0
- pyoq/query/postgres/bulk.py +62 -0
- pyoq/query/postgres/commands.py +238 -0
- pyoq/query/postgres/compiler.py +114 -0
- pyoq/query/postgres/context.py +20 -0
- pyoq/query/postgres/executor.py +147 -0
- pyoq/query/postgres/expressions.py +311 -0
- pyoq/query/postgres/factory.py +24 -0
- pyoq/query/postgres/health.py +24 -0
- pyoq/query/postgres/identifiers.py +9 -0
- pyoq/query/postgres/model.py +81 -0
- pyoq/query/postgres/operations.py +25 -0
- pyoq/query/postgres/parameters.py +71 -0
- pyoq/query/postgres/planning.py +20 -0
- pyoq/query/postgres/pool.py +52 -0
- pyoq/query/postgres/transactions.py +295 -0
- pyoq/query/postgres/writes.py +37 -0
- pyoq/query/projections.py +105 -0
- pyoq/query/raw.py +90 -0
- pyoq/query/recursion.py +265 -0
- pyoq/query/rendering/__init__.py +1 -0
- pyoq/query/rendering/expressions.py +913 -0
- pyoq/query/rendering/identifiers.py +40 -0
- pyoq/query/rendering/projections.py +63 -0
- pyoq/query/rendering/queries.py +334 -0
- pyoq/query/rendering/sources.py +66 -0
- pyoq/query/rendering/writes.py +176 -0
- pyoq/query/results.py +459 -0
- pyoq/query/routines.py +196 -0
- pyoq/query/rows.py +156 -0
- pyoq/query/select.py +793 -0
- pyoq/query/select_nodes.py +277 -0
- pyoq/query/sources.py +236 -0
- pyoq/query/sqlite/__init__.py +43 -0
- pyoq/query/sqlite/commands.py +201 -0
- pyoq/query/sqlite/compiler.py +139 -0
- pyoq/query/sqlite/context.py +20 -0
- pyoq/query/sqlite/executor.py +119 -0
- pyoq/query/sqlite/expressions.py +224 -0
- pyoq/query/sqlite/factory.py +32 -0
- pyoq/query/sqlite/health.py +28 -0
- pyoq/query/sqlite/identifiers.py +9 -0
- pyoq/query/sqlite/model.py +73 -0
- pyoq/query/sqlite/operations.py +36 -0
- pyoq/query/sqlite/parameters.py +50 -0
- pyoq/query/sqlite/planning.py +20 -0
- pyoq/query/sqlite/pool.py +50 -0
- pyoq/query/sqlite/streaming.py +13 -0
- pyoq/query/sqlite/transactions.py +274 -0
- pyoq/query/sqlite/writes.py +35 -0
- pyoq/query/statements.py +27 -0
- pyoq/query/values.py +23 -0
- pyoq/query/vendor.py +162 -0
- pyoq/query/windows.py +424 -0
- pyoq/query/write_nodes.py +174 -0
- pyoq/query/writes.py +628 -0
- pyoq/relations/__init__.py +66 -0
- pyoq/relations/batching.py +219 -0
- pyoq/relations/derivation.py +111 -0
- pyoq/relations/fetching.py +355 -0
- pyoq/relations/graph.py +245 -0
- pyoq/relations/loading.py +74 -0
- pyoq/relations/model.py +75 -0
- pyoq/relations/planning.py +206 -0
- pyoq/runtime/__init__.py +9 -0
- pyoq/runtime/kernels.py +25 -0
- pyoq/runtime/python.py +43 -0
- pyoq/runtime/selection.py +73 -0
- pyoq/sanic/__init__.py +32 -0
- pyoq/sanic/scope.py +197 -0
- pyoq/sanic/workers.py +129 -0
- pyoq/schema/__init__.py +108 -0
- pyoq/schema/codec.py +711 -0
- pyoq/schema/models.py +604 -0
- pyoq/schema/mysql/__init__.py +16 -0
- pyoq/schema/mysql/connection.py +72 -0
- pyoq/schema/mysql/dsn.py +72 -0
- pyoq/schema/mysql/records.py +354 -0
- pyoq/schema/mysql/reflection.py +309 -0
- pyoq/schema/mysql/source.py +30 -0
- pyoq/schema/mysql/sql.py +128 -0
- pyoq/schema/mysql/types.py +105 -0
- pyoq/schema/postgres/__init__.py +13 -0
- pyoq/schema/postgres/connection.py +62 -0
- pyoq/schema/postgres/records.py +384 -0
- pyoq/schema/postgres/reflection.py +466 -0
- pyoq/schema/postgres/source.py +30 -0
- pyoq/schema/postgres/sql.py +246 -0
- pyoq/schema/postgres/types.py +98 -0
- pyoq/schema/registry.py +45 -0
- pyoq/schema/source.py +15 -0
- pyoq/schema/sqlite/__init__.py +6 -0
- pyoq/schema/sqlite/connection.py +54 -0
- pyoq/schema/sqlite/records.py +167 -0
- pyoq/schema/sqlite/reflection.py +393 -0
- pyoq/schema/sqlite/source.py +30 -0
- pyoq/schema/sqlite/sql.py +254 -0
- pyoq/schema/sqlite/types.py +74 -0
- pyoq/serving/__init__.py +19 -0
- pyoq/serving/databases.py +107 -0
- pyoq/snapshots/__init__.py +20 -0
- pyoq/snapshots/drift.py +312 -0
- pyoq/snapshots/files.py +96 -0
- pyoq/snapshots/routing.py +40 -0
- pyoq/snapshots/source.py +33 -0
- pyoq/tracing/__init__.py +5 -0
- pyoq/tracing/spans.py +89 -0
- pyoq/unset.py +14 -0
- pyoq_sql-1.0.0.dist-info/METADATA +3034 -0
- pyoq_sql-1.0.0.dist-info/RECORD +264 -0
- pyoq_sql-1.0.0.dist-info/WHEEL +4 -0
- pyoq_sql-1.0.0.dist-info/entry_points.txt +3 -0
- pyoq_sql-1.0.0.dist-info/licenses/LICENSE +373 -0
pyoq/__init__.py
ADDED
pyoq/__main__.py
ADDED
pyoq/_native.pyi
ADDED
pyoq/cli/__init__.py
ADDED
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
|
+
)
|
pyoq/config/__init__.py
ADDED
|
@@ -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
|
+
)
|