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.
- pyoq/__init__.py +10 -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 +31 -0
- pyoq/config/connection.py +161 -0
- pyoq/config/loader.py +289 -0
- pyoq/config/models.py +245 -0
- pyoq/config/values.py +142 -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 +73 -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 +63 -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 +23 -0
- pyoq/serving/databases.py +107 -0
- pyoq/serving/opening.py +331 -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.2.dist-info/METADATA +3050 -0
- pyoq_sql-1.0.2.dist-info/RECORD +267 -0
- pyoq_sql-1.0.2.dist-info/WHEEL +4 -0
- pyoq_sql-1.0.2.dist-info/entry_points.txt +3 -0
- pyoq_sql-1.0.2.dist-info/licenses/LICENSE +373 -0
pyoq/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Public package interface for PyOQ."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import version
|
|
4
|
+
|
|
5
|
+
from pyoq.config import Dialect
|
|
6
|
+
from pyoq.serving import Database, connect
|
|
7
|
+
|
|
8
|
+
__version__ = version("pyoq-sql")
|
|
9
|
+
|
|
10
|
+
__all__ = ("Database", "Dialect", "__version__", "connect")
|
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,31 @@
|
|
|
1
|
+
"""Public configuration interface."""
|
|
2
|
+
|
|
3
|
+
from pyoq.config.connection import (
|
|
4
|
+
ConnectionConfiguration,
|
|
5
|
+
PoolConfiguration,
|
|
6
|
+
ResolvedPoolConfiguration,
|
|
7
|
+
)
|
|
8
|
+
from pyoq.config.models import Configuration, DatabaseProfile, load_configuration
|
|
9
|
+
from pyoq.config.values import (
|
|
10
|
+
DatabaseDialect,
|
|
11
|
+
DataSourceName,
|
|
12
|
+
Dialect,
|
|
13
|
+
EnvironmentReference,
|
|
14
|
+
SecretValue,
|
|
15
|
+
SettingValue,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = (
|
|
19
|
+
"Configuration",
|
|
20
|
+
"ConnectionConfiguration",
|
|
21
|
+
"DataSourceName",
|
|
22
|
+
"DatabaseDialect",
|
|
23
|
+
"DatabaseProfile",
|
|
24
|
+
"Dialect",
|
|
25
|
+
"EnvironmentReference",
|
|
26
|
+
"PoolConfiguration",
|
|
27
|
+
"ResolvedPoolConfiguration",
|
|
28
|
+
"SecretValue",
|
|
29
|
+
"SettingValue",
|
|
30
|
+
"load_configuration",
|
|
31
|
+
)
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Structured server connection and pool configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from urllib.parse import quote, urlunsplit
|
|
8
|
+
|
|
9
|
+
from pyoq.config.values import DatabaseDialect, SecretValue, SettingValue
|
|
10
|
+
from pyoq.errors import ConfigurationValidationError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True, slots=True)
|
|
14
|
+
class ConnectionConfiguration:
|
|
15
|
+
database: SettingValue
|
|
16
|
+
username: SettingValue
|
|
17
|
+
host: SettingValue | None = None
|
|
18
|
+
port: SettingValue | None = None
|
|
19
|
+
password: SettingValue | None = None
|
|
20
|
+
|
|
21
|
+
def resolve_dsn(
|
|
22
|
+
self,
|
|
23
|
+
dialect: DatabaseDialect,
|
|
24
|
+
environment: Mapping[str, str] | None = None,
|
|
25
|
+
) -> SecretValue:
|
|
26
|
+
scheme, default_port = _server_defaults(dialect)
|
|
27
|
+
host = _resolved_host(self.host, environment)
|
|
28
|
+
port = _resolved_port(self.port, default_port, environment)
|
|
29
|
+
username = self.username.resolve(environment).reveal()
|
|
30
|
+
password = _resolved_password(self.password, environment)
|
|
31
|
+
database = self.database.resolve(environment).reveal()
|
|
32
|
+
authority = _connection_authority(host, port, username, password)
|
|
33
|
+
return SecretValue(
|
|
34
|
+
urlunsplit((scheme, authority, f"/{quote(database, safe='')}", "", ""))
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True, slots=True)
|
|
39
|
+
class ResolvedPoolConfiguration:
|
|
40
|
+
minimum_size: int | None
|
|
41
|
+
maximum_size: int | None
|
|
42
|
+
checkout_timeout: float | None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True, slots=True)
|
|
46
|
+
class PoolConfiguration:
|
|
47
|
+
minimum_size: SettingValue | None = None
|
|
48
|
+
maximum_size: SettingValue | None = None
|
|
49
|
+
checkout_timeout: SettingValue | None = None
|
|
50
|
+
|
|
51
|
+
def resolve(
|
|
52
|
+
self,
|
|
53
|
+
environment: Mapping[str, str] | None = None,
|
|
54
|
+
) -> ResolvedPoolConfiguration:
|
|
55
|
+
return ResolvedPoolConfiguration(
|
|
56
|
+
_resolved_integer(self.minimum_size, "minimum-pool-size", environment),
|
|
57
|
+
_resolved_integer(self.maximum_size, "maximum-pool-size", environment),
|
|
58
|
+
_resolved_float(
|
|
59
|
+
self.checkout_timeout,
|
|
60
|
+
"pool-checkout-timeout",
|
|
61
|
+
environment,
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _server_defaults(dialect: DatabaseDialect) -> tuple[str, int]:
|
|
67
|
+
if dialect is DatabaseDialect.MYSQL:
|
|
68
|
+
return "mysql", 3306
|
|
69
|
+
if dialect is DatabaseDialect.POSTGRES:
|
|
70
|
+
return "postgresql", 5432
|
|
71
|
+
message = "structured connection fields require a server database dialect"
|
|
72
|
+
raise ConfigurationValidationError(message)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _resolved_host(
|
|
76
|
+
configured: SettingValue | None,
|
|
77
|
+
environment: Mapping[str, str] | None,
|
|
78
|
+
) -> str:
|
|
79
|
+
host = (
|
|
80
|
+
"localhost" if configured is None else configured.resolve(environment).reveal()
|
|
81
|
+
)
|
|
82
|
+
if any(character.isspace() or character in "/@?#[]" for character in host):
|
|
83
|
+
message = f"invalid database host: {host!r}"
|
|
84
|
+
raise ConfigurationValidationError(message)
|
|
85
|
+
return host
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _resolved_port(
|
|
89
|
+
configured: SettingValue | None,
|
|
90
|
+
default: int,
|
|
91
|
+
environment: Mapping[str, str] | None,
|
|
92
|
+
) -> int:
|
|
93
|
+
if configured is None:
|
|
94
|
+
return default
|
|
95
|
+
written = configured.resolve(environment).reveal()
|
|
96
|
+
try:
|
|
97
|
+
port = int(written)
|
|
98
|
+
except ValueError as error:
|
|
99
|
+
message = f"database port must be an integer: {written!r}"
|
|
100
|
+
raise ConfigurationValidationError(message) from error
|
|
101
|
+
if not 1 <= port <= 65535:
|
|
102
|
+
message = "database port must be between 1 and 65535"
|
|
103
|
+
raise ConfigurationValidationError(message)
|
|
104
|
+
return port
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _resolved_password(
|
|
108
|
+
configured: SettingValue | None,
|
|
109
|
+
environment: Mapping[str, str] | None,
|
|
110
|
+
) -> str:
|
|
111
|
+
return "" if configured is None else configured.resolve(environment).reveal()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _connection_authority(
|
|
115
|
+
host: str,
|
|
116
|
+
port: int,
|
|
117
|
+
username: str,
|
|
118
|
+
password: str,
|
|
119
|
+
) -> str:
|
|
120
|
+
credentials = quote(username, safe="")
|
|
121
|
+
if password:
|
|
122
|
+
credentials = f"{credentials}:{quote(password, safe='')}"
|
|
123
|
+
address = f"[{host}]" if ":" in host else host
|
|
124
|
+
return f"{credentials}@{address}:{port}"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _resolved_integer(
|
|
128
|
+
configured: SettingValue | None,
|
|
129
|
+
label: str,
|
|
130
|
+
environment: Mapping[str, str] | None,
|
|
131
|
+
) -> int | None:
|
|
132
|
+
if configured is None:
|
|
133
|
+
return None
|
|
134
|
+
written = configured.resolve(environment).reveal()
|
|
135
|
+
try:
|
|
136
|
+
return int(written)
|
|
137
|
+
except ValueError as error:
|
|
138
|
+
message = f"{label} must be an integer: {written!r}"
|
|
139
|
+
raise ConfigurationValidationError(message) from error
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _resolved_float(
|
|
143
|
+
configured: SettingValue | None,
|
|
144
|
+
label: str,
|
|
145
|
+
environment: Mapping[str, str] | None,
|
|
146
|
+
) -> float | None:
|
|
147
|
+
if configured is None:
|
|
148
|
+
return None
|
|
149
|
+
written = configured.resolve(environment).reveal()
|
|
150
|
+
try:
|
|
151
|
+
return float(written)
|
|
152
|
+
except ValueError as error:
|
|
153
|
+
message = f"{label} must be numeric: {written!r}"
|
|
154
|
+
raise ConfigurationValidationError(message) from error
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
__all__ = (
|
|
158
|
+
"ConnectionConfiguration",
|
|
159
|
+
"PoolConfiguration",
|
|
160
|
+
"ResolvedPoolConfiguration",
|
|
161
|
+
)
|