diff --git a/docs/usage/migrations.rst b/docs/usage/migrations.rst index 82acfb69..17811446 100644 --- a/docs/usage/migrations.rst +++ b/docs/usage/migrations.rst @@ -139,6 +139,81 @@ All database configs (sync and async) provide these migration methods: ``get_current_migration(verbose=False)`` Get the current migration version. +Template Profiles & Author Metadata +=================================== + +Migrations inherit their header text, metadata comments, and default file format +from ``migration_config["templates"]``. Each project can define multiple +profiles and select one globally: + +.. code-block:: python + + migration_config={ + "default_format": "py", # CLI default when --format omitted + "title": "Acme Migration", # Shared title for all templates + "author": "env:SQLSPEC_AUTHOR", # Read from environment variable + "templates": { + "sql": { + "header": "-- {title} - {message}", + "metadata": ["-- Version: {version}", "-- Owner: {author}"], + "body": "-- custom SQL body" + }, + "py": { + "docstring": """{title}\nDescription: {description}""", + "imports": ["from typing import Iterable"], + "body": """def up(context: object | None = None) -> str | Iterable[str]: + return "SELECT 1" + +def down(context: object | None = None) -> str | Iterable[str]: + return "DROP TABLE example;" +""" + } + } + } + +Template fragments accept the following variables: + +- ``{title}`` – shared template title +- ``{version}`` – generated revision identifier +- ``{message}`` – CLI/command message +- ``{description}`` – message fallback used in logs and docstrings +- ``{created_at}`` – UTC timestamp in ISO 8601 format +- ``{author}`` – resolved author string +- ``{adapter}`` – config driver class (useful for docstrings) +- ``{project_slug}`` / ``{slug}`` – sanitized project and message slugs + +Missing placeholders raise ``TemplateValidationError`` so mistakes are caught +immediately. SQL templates list metadata rows (``metadata``) and a ``body`` +block. Python templates expose ``docstring``, optional ``imports``, and ``body``. + +Author attribution can be controlled via ``migration_config["author"]``: + +- Literal strings (``"Data Platform"``) are stamped verbatim +- ``"env:VAR_NAME"`` pulls from the environment and fails fast if unset +- ``"callable:pkg.module:get_author"`` invokes a helper that can inspect the + config or environment when determining the author string +- ``"git"`` reads git user.name/email; ``"system"`` uses ``$USER`` + +CLI Enhancements +---------------- + +``sqlspec create-migration`` (and ``litestar database create-migration``) +accept ``--format`` / ``--file-type`` flags: + +.. code-block:: bash + + sqlspec --config myapp.config create-migration -m "Add seed data" --format py + +When omitted, the CLI uses ``migration_config["default_format"]`` (``"sql"`` by default). +Upgrade/downgrade commands now echo ``{version}: {description}``, so the rich +description captured in templates is visible during deployments and matches the +continue-on-error logs. + +The default Python template ships with both ``up`` and ``down`` functions that +accept an optional ``context`` argument. When migrations run via SQLSpec, that +parameter receives the active ``MigrationContext`` so you can reach the config +or connection objects directly inside your migration logic. + ``create_migration(message, file_type="sql")`` Create a new migration file. @@ -452,7 +527,8 @@ SQLSpec uses a tracking table to record applied migrations: renamed migrations (e.g., timestamp → sequential conversion). ``applied_by`` - Unix username of user who applied the migration. + Author string recorded for the migration. Defaults to the git user/system + account but can be overridden via ``migration_config["author"]``. Schema Migration ---------------- diff --git a/pyproject.toml b/pyproject.toml index 629845b9..41212605 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -503,6 +503,7 @@ split-on-trailing-comma = false "docs/examples/**" = ["T201"] "sqlspec/builder/mixins/**/*.*" = ["SLF001"] "sqlspec/extensions/adk/converters.py" = ["S403"] +"sqlspec/migrations/utils.py" = ["S404"] "tests/**/*.*" = [ "A", "ARG", diff --git a/sqlspec/cli.py b/sqlspec/cli.py index 2346d82d..f6e3db4b 100644 --- a/sqlspec/cli.py +++ b/sqlspec/cli.py @@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Any, cast import rich_click as click +from click.core import ParameterSource if TYPE_CHECKING: from rich_click import Group @@ -80,6 +81,16 @@ def sqlspec_group(ctx: "click.Context", config: str, validate_config: bool) -> N return sqlspec_group +def _ensure_click_context() -> "click.Context": + """Return the active Click context, raising if missing (for type-checkers).""" + + context = click.get_current_context() + if context is None: # pragma: no cover - click guarantees context in commands + msg = "SQLSpec CLI commands require an active Click context" + raise RuntimeError(msg) + return cast("click.Context", context) + + def add_migration_commands(database_group: "Group | None" = None) -> "Group": """Add migration commands to the database group. @@ -270,17 +281,12 @@ def show_database_revision( # pyright: ignore[reportUnusedFunction] from sqlspec.migrations.commands import create_migration_commands from sqlspec.utils.sync_tools import run_ - ctx = click.get_current_context() + ctx = _ensure_click_context() async def _show_current_revision() -> None: # Check if this is a multi-config operation configs_to_process = process_multiple_configs( - cast("click.Context", ctx), - bind_key, - include, - exclude, - dry_run=False, - operation_name="show current revision", + ctx, bind_key, include, exclude, dry_run=False, operation_name="show current revision" ) if configs_to_process is not None: @@ -300,7 +306,7 @@ async def _show_current_revision() -> None: console.print(f"[red]✗ Failed to get current revision for {config_name}: {e}[/]") else: console.rule("[yellow]Listing current revision[/]", align="left") - sqlspec_config = get_config_by_bind_key(cast("click.Context", ctx), bind_key) + sqlspec_config = get_config_by_bind_key(ctx, bind_key) migration_commands = create_migration_commands(config=sqlspec_config) await maybe_await(migration_commands.current(verbose=verbose)) @@ -327,17 +333,12 @@ def downgrade_database( # pyright: ignore[reportUnusedFunction] from sqlspec.migrations.commands import create_migration_commands from sqlspec.utils.sync_tools import run_ - ctx = click.get_current_context() + ctx = _ensure_click_context() async def _downgrade_database() -> None: # Check if this is a multi-config operation configs_to_process = process_multiple_configs( - cast("click.Context", ctx), - bind_key, - include, - exclude, - dry_run=dry_run, - operation_name=f"downgrade to {revision}", + ctx, bind_key, include, exclude, dry_run=dry_run, operation_name=f"downgrade to {revision}" ) if configs_to_process is not None: @@ -371,7 +372,7 @@ async def _downgrade_database() -> None: else Confirm.ask(f"Are you sure you want to downgrade the database to the `{revision}` revision?") ) if input_confirmed: - sqlspec_config = get_config_by_bind_key(cast("click.Context", ctx), bind_key) + sqlspec_config = get_config_by_bind_key(ctx, bind_key) migration_commands = create_migration_commands(config=sqlspec_config) await maybe_await(migration_commands.downgrade(revision=revision, dry_run=dry_run)) @@ -402,7 +403,7 @@ def upgrade_database( # pyright: ignore[reportUnusedFunction] from sqlspec.migrations.commands import create_migration_commands from sqlspec.utils.sync_tools import run_ - ctx = click.get_current_context() + ctx = _ensure_click_context() async def _upgrade_database() -> None: # Report execution mode when specified @@ -411,7 +412,7 @@ async def _upgrade_database() -> None: # Check if this is a multi-config operation configs_to_process = process_multiple_configs( - cast("click.Context", ctx), bind_key, include, exclude, dry_run, operation_name=f"upgrade to {revision}" + ctx, bind_key, include, exclude, dry_run, operation_name=f"upgrade to {revision}" ) if configs_to_process is not None: @@ -449,7 +450,7 @@ async def _upgrade_database() -> None: ) ) if input_confirmed: - sqlspec_config = get_config_by_bind_key(cast("click.Context", ctx), bind_key) + sqlspec_config = get_config_by_bind_key(ctx, bind_key) migration_commands = create_migration_commands(config=sqlspec_config) await maybe_await( migration_commands.upgrade(revision=revision, auto_sync=not no_auto_sync, dry_run=dry_run) @@ -465,10 +466,10 @@ def stamp(bind_key: str | None, revision: str) -> None: # pyright: ignore[repor from sqlspec.migrations.commands import create_migration_commands from sqlspec.utils.sync_tools import run_ - ctx = click.get_current_context() + ctx = _ensure_click_context() async def _stamp() -> None: - sqlspec_config = get_config_by_bind_key(cast("click.Context", ctx), bind_key) + sqlspec_config = get_config_by_bind_key(ctx, bind_key) migration_commands = create_migration_commands(config=sqlspec_config) await maybe_await(migration_commands.stamp(revision=revision)) @@ -488,7 +489,7 @@ def init_sqlspec( # pyright: ignore[reportUnusedFunction] from sqlspec.migrations.commands import create_migration_commands from sqlspec.utils.sync_tools import run_ - ctx = click.get_current_context() + ctx = _ensure_click_context() async def _init_sqlspec() -> None: console.rule("[yellow]Initializing database migrations.", align="left") @@ -498,11 +499,7 @@ async def _init_sqlspec() -> None: else Confirm.ask("[bold]Are you sure you want initialize migrations for the project?[/]") ) if input_confirmed: - configs = ( - [get_config_by_bind_key(cast("click.Context", ctx), bind_key)] - if bind_key is not None - else cast("click.Context", ctx).obj["configs"] - ) + configs = [get_config_by_bind_key(ctx, bind_key)] if bind_key is not None else ctx.obj["configs"] for config in configs: migration_config = getattr(config, "migration_config", {}) @@ -519,9 +516,17 @@ async def _init_sqlspec() -> None: ) @bind_key_option @click.option("-m", "--message", default=None, help="Revision message") + @click.option( + "--format", + "--file-type", + "file_format", + type=click.Choice(["sql", "py"]), + default=None, + help="File format for the generated migration (defaults to template profile)", + ) @no_prompt_option def create_revision( # pyright: ignore[reportUnusedFunction] - bind_key: str | None, message: str | None, no_prompt: bool + bind_key: str | None, message: str | None, file_format: str | None, no_prompt: bool ) -> None: """Create a new database revision.""" from rich.prompt import Prompt @@ -529,7 +534,7 @@ def create_revision( # pyright: ignore[reportUnusedFunction] from sqlspec.migrations.commands import create_migration_commands from sqlspec.utils.sync_tools import run_ - ctx = click.get_current_context() + ctx = _ensure_click_context() async def _create_revision() -> None: console.rule("[yellow]Creating new migration revision[/]", align="left") @@ -539,9 +544,11 @@ async def _create_revision() -> None: "new migration" if no_prompt else Prompt.ask("Please enter a message describing this revision") ) - sqlspec_config = get_config_by_bind_key(cast("click.Context", ctx), bind_key) + sqlspec_config = get_config_by_bind_key(ctx, bind_key) + param_source = ctx.get_parameter_source("file_format") + effective_format = None if param_source is ParameterSource.DEFAULT else file_format migration_commands = create_migration_commands(config=sqlspec_config) - await maybe_await(migration_commands.revision(message=message_text)) + await maybe_await(migration_commands.revision(message=message_text, file_type=effective_format)) run_(_create_revision)() @@ -557,11 +564,11 @@ def fix_migrations( # pyright: ignore[reportUnusedFunction] from sqlspec.migrations.commands import create_migration_commands from sqlspec.utils.sync_tools import run_ - ctx = click.get_current_context() + ctx = _ensure_click_context() async def _fix_migrations() -> None: console.rule("[yellow]Migration Fix Command[/]", align="left") - sqlspec_config = get_config_by_bind_key(cast("click.Context", ctx), bind_key) + sqlspec_config = get_config_by_bind_key(ctx, bind_key) migration_commands = create_migration_commands(config=sqlspec_config) await maybe_await(migration_commands.fix(dry_run=dry_run, update_database=not no_database, yes=yes)) @@ -573,20 +580,20 @@ def show_config(bind_key: str | None = None) -> None: # pyright: ignore[reportU """Show and display all configurations with migrations enabled.""" from rich.table import Table - ctx = click.get_current_context() + ctx = _ensure_click_context() # If bind_key is provided, filter to only that config if bind_key is not None: - get_config_by_bind_key(cast("click.Context", ctx), bind_key) + get_config_by_bind_key(ctx, bind_key) # Convert single config to list format for compatibility - all_configs = cast("click.Context", ctx).obj["configs"] + all_configs = ctx.obj["configs"] migration_configs = [] for cfg in all_configs: config_name = cfg.bind_key if config_name == bind_key and hasattr(cfg, "migration_config") and cfg.migration_config: migration_configs.append((config_name, cfg)) else: - migration_configs = get_configs_with_migrations(cast("click.Context", ctx)) + migration_configs = get_configs_with_migrations(ctx) if not migration_configs: console.print("[yellow]No configurations with migrations detected.[/]") diff --git a/sqlspec/migrations/base.py b/sqlspec/migrations/base.py index da872495..4106fd68 100644 --- a/sqlspec/migrations/base.py +++ b/sqlspec/migrations/base.py @@ -1,8 +1,6 @@ -"""Base classes for SQLSpec migrations. - -This module provides abstract base classes for migration components. -""" +"""Base classes for SQLSpec migrations.""" +import ast import hashlib from abc import ABC, abstractmethod from pathlib import Path @@ -12,6 +10,7 @@ from sqlspec.builder._ddl import CreateTable from sqlspec.loader import SQLFileLoader from sqlspec.migrations.loaders import get_migration_loader +from sqlspec.migrations.templates import MigrationTemplateSettings, TemplateDescriptionHints, build_template_settings from sqlspec.utils.logging import get_logger from sqlspec.utils.module_loader import module_to_os_path from sqlspec.utils.sync_tools import await_ @@ -270,6 +269,7 @@ def __init__( extension_migrations: "dict[str, Path] | None" = None, context: "Any | None" = None, extension_configs: "dict[str, dict[str, Any]] | None" = None, + description_hints: "TemplateDescriptionHints | None" = None, ) -> None: """Initialize the migration runner. @@ -278,6 +278,8 @@ def __init__( extension_migrations: Optional mapping of extension names to their migration paths. context: Optional migration context for Python migrations. extension_configs: Optional mapping of extension names to their configurations. + description_hints: Preferred metadata keys for extracting human descriptions + from SQL comments and Python docstrings. """ self.migrations_path = migrations_path self.extension_migrations = extension_migrations or {} @@ -285,6 +287,7 @@ def __init__( self.project_root: Path | None = None self.context = context self.extension_configs = extension_configs or {} + self.description_hints = description_hints or TemplateDescriptionHints() def _extract_version(self, filename: str) -> str | None: """Extract version from filename. @@ -387,7 +390,9 @@ def _load_migration_metadata(self, file_path: Path, version: "str | None" = None loader.validate_migration_file(file_path) content = file_path.read_text(encoding="utf-8") checksum = self._calculate_checksum(content) - description = file_path.stem.split("_", 1)[1] if "_" in file_path.stem else "" + description = self._extract_description(content, file_path) + if not description: + description = file_path.stem.split("_", 1)[1] if "_" in file_path.stem else "" has_upgrade, has_downgrade = True, False @@ -412,6 +417,49 @@ def _load_migration_metadata(self, file_path: Path, version: "str | None" = None "loader": loader, } + def _extract_description(self, content: str, file_path: Path) -> str: + if file_path.suffix == ".sql": + return self._extract_sql_description(content) + if file_path.suffix == ".py": + return self._extract_python_description(content) + return "" + + def _extract_sql_description(self, content: str) -> str: + keys = self.description_hints.sql_keys + for line in content.splitlines(): + stripped = line.strip() + if not stripped: + continue + if stripped.startswith("--"): + body = stripped.lstrip("-").strip() + if not body: + continue + if ":" in body: + key, value = body.split(":", 1) + if key.strip() in keys: + return value.strip() + continue + break + return "" + + def _extract_python_description(self, content: str) -> str: + try: + module = ast.parse(content) + except SyntaxError: + return "" + docstring = ast.get_docstring(module) or "" + keys = self.description_hints.python_keys + for line in docstring.splitlines(): + stripped = line.strip() + if not stripped: + continue + if ":" in stripped: + key, value = stripped.split(":", 1) + if key.strip() in keys: + return value.strip() + return stripped + return "" + def _get_migration_sql(self, migration: "dict[str, Any]", direction: str) -> "list[str] | None": """Get migration SQL for given direction. @@ -491,6 +539,7 @@ def __init__(self, config: ConfigT) -> None: self.project_root = Path(migration_config["project_root"]) if "project_root" in migration_config else None self.include_extensions = migration_config.get("include_extensions", []) self.extension_configs = self._parse_extension_configs() + self._template_settings: MigrationTemplateSettings = build_template_settings(migration_config) self._runtime: ObservabilityRuntime | None = self.config.get_observability_runtime() self._last_command_error: Exception | None = None self._last_command_metrics: dict[str, float] | None = None diff --git a/sqlspec/migrations/commands.py b/sqlspec/migrations/commands.py index 53a7f1a7..8d21f54a 100644 --- a/sqlspec/migrations/commands.py +++ b/sqlspec/migrations/commands.py @@ -176,6 +176,7 @@ def __init__(self, config: "SyncConfigT") -> None: context, self.extension_configs, runtime=self._runtime, + description_hints=self._template_settings.description_hints, ) def init(self, directory: str, package: bool = True) -> None: @@ -527,7 +528,7 @@ def stamp(self, revision: str) -> None: self.tracker.record_migration(driver, revision, f"Stamped to {revision}", 0, "manual-stamp") console.print(f"[green]Database stamped at revision {revision}[/]") - def revision(self, message: str, file_type: str = "sql") -> None: + def revision(self, message: str, file_type: str | None = None) -> None: """Create a new migration file with timestamp-based versioning. Generates a unique timestamp version (YYYYMMDDHHmmss format) to avoid @@ -538,7 +539,15 @@ def revision(self, message: str, file_type: str = "sql") -> None: file_type: Type of migration file to create ('sql' or 'py'). """ version = generate_timestamp_version() - file_path = create_migration_file(self.migrations_path, version, message, file_type) + selected_format = file_type or self._template_settings.default_format + file_path = create_migration_file( + self.migrations_path, + version, + message, + selected_format, + config=self.config, + template_settings=self._template_settings, + ) console.print(f"[green]Created migration:[/] {file_path}") def fix(self, dry_run: bool = False, update_database: bool = True, yes: bool = False) -> None: @@ -650,6 +659,7 @@ def __init__(self, config: "AsyncConfigT") -> None: context, self.extension_configs, runtime=self._runtime, + description_hints=self._template_settings.description_hints, ) async def init(self, directory: str, package: bool = True) -> None: @@ -1005,7 +1015,7 @@ async def stamp(self, revision: str) -> None: await self.tracker.record_migration(driver, revision, f"Stamped to {revision}", 0, "manual-stamp") console.print(f"[green]Database stamped at revision {revision}[/]") - async def revision(self, message: str, file_type: str = "sql") -> None: + async def revision(self, message: str, file_type: str | None = None) -> None: """Create a new migration file with timestamp-based versioning. Generates a unique timestamp version (YYYYMMDDHHmmss format) to avoid @@ -1016,7 +1026,15 @@ async def revision(self, message: str, file_type: str = "sql") -> None: file_type: Type of migration file to create ('sql' or 'py'). """ version = generate_timestamp_version() - file_path = create_migration_file(self.migrations_path, version, message, file_type) + selected_format = file_type or self._template_settings.default_format + file_path = create_migration_file( + self.migrations_path, + version, + message, + selected_format, + config=self.config, + template_settings=self._template_settings, + ) console.print(f"[green]Created migration:[/] {file_path}") async def fix(self, dry_run: bool = False, update_database: bool = True, yes: bool = False) -> None: diff --git a/sqlspec/migrations/runner.py b/sqlspec/migrations/runner.py index d7b726a6..6e2c7fd9 100644 --- a/sqlspec/migrations/runner.py +++ b/sqlspec/migrations/runner.py @@ -1,9 +1,6 @@ -"""Migration execution engine for SQLSpec. - -This module provides separate sync and async migration runners with clean separation -of concerns and proper type safety. -""" +"""Migration execution engine for SQLSpec.""" +import ast import hashlib import inspect import re @@ -15,6 +12,7 @@ from sqlspec.core import SQL from sqlspec.migrations.context import MigrationContext from sqlspec.migrations.loaders import get_migration_loader +from sqlspec.migrations.templates import TemplateDescriptionHints from sqlspec.utils.logging import get_logger from sqlspec.utils.sync_tools import async_, await_ from sqlspec.utils.version import parse_version @@ -64,6 +62,7 @@ def __init__( context: "MigrationContext | None" = None, extension_configs: "dict[str, dict[str, Any]] | None" = None, runtime: "ObservabilityRuntime | None" = None, + description_hints: "TemplateDescriptionHints | None" = None, ) -> None: """Initialize the migration runner. @@ -73,6 +72,7 @@ def __init__( context: Optional migration context for Python migrations. extension_configs: Optional mapping of extension names to their configurations. runtime: Observability runtime shared with command/context consumers. + description_hints: Hints for extracting migration descriptions. """ self.migrations_path = migrations_path self.extension_migrations = extension_migrations or {} @@ -87,6 +87,7 @@ def __init__( self._listing_cache: list[tuple[str, Path]] | None = None self._listing_signatures: dict[str, tuple[int, int]] = {} self._metadata_cache: dict[str, _CachedMigrationMetadata] = {} + self.description_hints = description_hints or TemplateDescriptionHints() def _metric(self, name: str, amount: float = 1.0) -> None: if self.runtime is None: @@ -312,7 +313,9 @@ def _load_migration_metadata_common(self, file_path: Path, version: "str | None" checksum = self._calculate_checksum(content) if version is None: version = self._extract_version(file_path.name) - description = file_path.stem.split("_", 1)[1] if "_" in file_path.stem else "" + description = self._extract_description(content, file_path) + if not description: + description = file_path.stem.split("_", 1)[1] if "_" in file_path.stem else "" transactional_match = re.search( r"^--\s*transactional:\s*(true|false)\s*$", content, re.MULTILINE | re.IGNORECASE @@ -338,6 +341,49 @@ def _load_migration_metadata_common(self, file_path: Path, version: "str | None" logger.debug("Cached migration metadata: %s", cache_key) return metadata + def _extract_description(self, content: str, file_path: Path) -> str: + if file_path.suffix == ".sql": + return self._extract_sql_description(content) + if file_path.suffix == ".py": + return self._extract_python_description(content) + return "" + + def _extract_sql_description(self, content: str) -> str: + keys = self.description_hints.sql_keys + for line in content.splitlines(): + stripped = line.strip() + if not stripped: + continue + if stripped.startswith("--"): + body = stripped.lstrip("-").strip() + if not body: + continue + if ":" in body: + key, value = body.split(":", 1) + if key.strip() in keys: + return value.strip() + continue + break + return "" + + def _extract_python_description(self, content: str) -> str: + try: + module = ast.parse(content) + except SyntaxError: + return "" + docstring = ast.get_docstring(module) or "" + keys = self.description_hints.python_keys + for line in docstring.splitlines(): + stripped = line.strip() + if not stripped: + continue + if ":" in stripped: + key, value = stripped.split(":", 1) + if key.strip() in keys: + return value.strip() + return stripped + return "" + def _get_context_for_migration(self, file_path: Path) -> "MigrationContext | None": """Get the appropriate context for a migration file. @@ -920,6 +966,7 @@ def create_migration_runner( extension_configs: "dict[str, Any]", is_async: "Literal[False]" = False, runtime: "ObservabilityRuntime | None" = None, + description_hints: "TemplateDescriptionHints | None" = None, ) -> SyncMigrationRunner: ... @@ -931,6 +978,7 @@ def create_migration_runner( extension_configs: "dict[str, Any]", is_async: "Literal[True]", runtime: "ObservabilityRuntime | None" = None, + description_hints: "TemplateDescriptionHints | None" = None, ) -> AsyncMigrationRunner: ... @@ -941,6 +989,7 @@ def create_migration_runner( extension_configs: "dict[str, Any]", is_async: bool = False, runtime: "ObservabilityRuntime | None" = None, + description_hints: "TemplateDescriptionHints | None" = None, ) -> "SyncMigrationRunner | AsyncMigrationRunner": """Factory function to create the appropriate migration runner. @@ -951,10 +1000,25 @@ def create_migration_runner( extension_configs: Extension configurations. is_async: Whether to create async or sync runner. runtime: Observability runtime shared with loaders and execution steps. + description_hints: Optional description extraction hints from template profiles. Returns: Appropriate migration runner instance. """ if is_async: - return AsyncMigrationRunner(migrations_path, extension_migrations, context, extension_configs, runtime=runtime) - return SyncMigrationRunner(migrations_path, extension_migrations, context, extension_configs, runtime=runtime) + return AsyncMigrationRunner( + migrations_path, + extension_migrations, + context, + extension_configs, + runtime=runtime, + description_hints=description_hints, + ) + return SyncMigrationRunner( + migrations_path, + extension_migrations, + context, + extension_configs, + runtime=runtime, + description_hints=description_hints, + ) diff --git a/sqlspec/migrations/templates.py b/sqlspec/migrations/templates.py new file mode 100644 index 00000000..4ffdbfc0 --- /dev/null +++ b/sqlspec/migrations/templates.py @@ -0,0 +1,234 @@ +"""Migration template rendering and configuration utilities.""" + +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Any + +from sqlspec.exceptions import SQLSpecError + +if TYPE_CHECKING: + from collections.abc import Mapping + +__all__ = ( + "MigrationTemplateProfile", + "MigrationTemplateSettings", + "PythonTemplateDefinition", + "SQLTemplateDefinition", + "TemplateDescriptionHints", + "TemplateValidationError", + "build_template_settings", +) + + +class TemplateValidationError(SQLSpecError): + """Raised when a migration template definition is invalid.""" + + +@dataclass(slots=True) +class TemplateDescriptionHints: + """Hints for extracting descriptions from rendered templates.""" + + sql_keys: "tuple[str, ...]" = ("Description",) + python_keys: "tuple[str, ...]" = ("Description",) + + +@dataclass(slots=True) +class SQLTemplateDefinition: + """SQL migration template fragments.""" + + header: str + metadata: "list[str]" = field(default_factory=list) + body: str = "" + description_keys: "tuple[str, ...]" = ("Description",) + + def render(self, context: "Mapping[str, str]") -> str: + """Render the SQL template using the supplied context.""" + + rendered_lines: list[str] = [self._format(self.header, context)] + rendered_lines.extend(self._format(line, context) for line in self.metadata if line) + rendered_lines.append("") + rendered_lines.append(self._format(self.body, context)) + return "\n".join(_normalize_newlines(rendered_lines)).rstrip() + "\n" + + def _format(self, template: str, context: "Mapping[str, str]") -> str: + try: + return template.format_map(context) + except KeyError as exc: # pragma: no cover - defensive + missing = str(exc).strip("'") + msg = f"Missing template variable '{missing}' in SQL template" + raise TemplateValidationError(msg) from exc + except ValueError as exc: # pragma: no cover - defensive + msg = f"Invalid SQL template fragment: {exc}" + raise TemplateValidationError(msg) from exc + + +@dataclass(slots=True) +class PythonTemplateDefinition: + """Python migration template fragments.""" + + docstring: str + body: str + imports: "list[str]" = field(default_factory=list) + description_keys: "tuple[str, ...]" = ("Description",) + + def render(self, context: "Mapping[str, str]") -> str: + """Render the Python template using the supplied context.""" + + docstring_block = f'"""{self._format(self.docstring, context)}"""' + rendered_lines: list[str] = [docstring_block, ""] + rendered_lines.extend(self.imports) + if self.imports: + rendered_lines.append("") + rendered_lines.append(self._format(self.body, context)) + return "\n".join(_normalize_newlines(rendered_lines)).rstrip() + "\n" + + def _format(self, template: str, context: "Mapping[str, str]") -> str: + try: + return template.format_map(context) + except KeyError as exc: # pragma: no cover - defensive + missing = str(exc).strip("'") + msg = f"Missing template variable '{missing}' in Python template" + raise TemplateValidationError(msg) from exc + except ValueError as exc: # pragma: no cover - defensive + msg = f"Invalid Python template fragment: {exc}" + raise TemplateValidationError(msg) from exc + + +@dataclass(slots=True) +class MigrationTemplateProfile: + """Concrete template profile selected via configuration.""" + + name: str + title: str + sql: "SQLTemplateDefinition" + python: "PythonTemplateDefinition" + + +@dataclass(slots=True) +class MigrationTemplateSettings: + """Resolved template configuration for a migration command context.""" + + default_format: str + profile: "MigrationTemplateProfile" + + def resolve_format(self, requested: str | None) -> str: + """Resolve the effective file format to render.""" + + format_choice = (requested or self.default_format or "sql").lower() + if format_choice not in {"sql", "py"}: + msg = f"Unsupported migration format '{format_choice}'" + raise TemplateValidationError(msg) + return format_choice + + @property + def description_hints(self) -> "TemplateDescriptionHints": + """Expose description extraction hints derived from the active profile.""" + + return TemplateDescriptionHints( + sql_keys=self.profile.sql.description_keys, python_keys=self.profile.python.description_keys + ) + + +def build_template_settings(migration_config: dict[str, Any] | None) -> "MigrationTemplateSettings": + """Build template settings from migration configuration.""" + + config = migration_config or {} + templates_config = config.get("templates") or {} + default_format = str(config.get("default_format") or "sql").lower() + if default_format not in {"sql", "py"}: + default_format = "sql" + title = str(config.get("title") or templates_config.get("title") or _DEFAULT_TITLE) + sql_definition = _build_sql_definition(templates_config.get("sql")) + python_definition = _build_python_definition(templates_config.get("py")) + profile = MigrationTemplateProfile(name="default", title=title, sql=sql_definition, python=python_definition) + return MigrationTemplateSettings(default_format=default_format, profile=profile) + + +def _build_sql_definition(overrides: Any) -> "SQLTemplateDefinition": + if overrides is None: + return _DEFAULT_SQL_TEMPLATE + if not isinstance(overrides, dict): + msg = "SQL template override must be a mapping" + raise TemplateValidationError(msg) + header = str(overrides.get("header") or _DEFAULT_SQL_TEMPLATE.header) + metadata = _coerce_string_list(overrides.get("metadata"), _DEFAULT_SQL_TEMPLATE.metadata) + body = str(overrides.get("body") or _DEFAULT_SQL_TEMPLATE.body) + description = _coerce_string_list(overrides.get("description_key"), list(_DEFAULT_SQL_TEMPLATE.description_keys)) + description_keys = tuple(description) if description else _DEFAULT_SQL_TEMPLATE.description_keys + return SQLTemplateDefinition(header=header, metadata=metadata, body=body, description_keys=description_keys) + + +def _build_python_definition(overrides: Any) -> "PythonTemplateDefinition": + if overrides is None: + return _DEFAULT_PY_TEMPLATE + if not isinstance(overrides, dict): + msg = "Python template override must be a mapping" + raise TemplateValidationError(msg) + docstring = str(overrides.get("docstring") or _DEFAULT_PY_TEMPLATE.docstring) + body = str(overrides.get("body") or _DEFAULT_PY_TEMPLATE.body) + imports = _coerce_string_list(overrides.get("imports"), _DEFAULT_PY_TEMPLATE.imports) + description = _coerce_string_list(overrides.get("description_key"), list(_DEFAULT_PY_TEMPLATE.description_keys)) + description_keys = tuple(description) if description else _DEFAULT_PY_TEMPLATE.description_keys + return PythonTemplateDefinition(docstring=docstring, body=body, imports=imports, description_keys=description_keys) + + +def _coerce_string_list(value: Any, default: "list[str]") -> "list[str]": + if value is None: + return list(default) + if isinstance(value, str): + return [line for line in value.splitlines() if line] + if isinstance(value, (list, tuple)): + return [str(item) for item in value if str(item)] + msg = "Template list override must be a string or list" + raise TemplateValidationError(msg) + + +def _normalize_newlines(lines: "list[str]") -> "list[str]": + normalized: list[str] = [line.rstrip("\r") for line in lines] + return normalized + + +_DEFAULT_TITLE = "SQLSpec Migration" + +_DEFAULT_SQL_TEMPLATE = SQLTemplateDefinition( + header="-- {title}", + metadata=[ + "-- Version: {version}", + "-- Description: {description}", + "-- Created: {created_at}", + "-- Author: {author}", + ], + body=( + "-- name: migrate-{version}-up\n" + "CREATE TABLE placeholder (\n" + " id INTEGER PRIMARY KEY\n" + ");\n\n" + "-- name: migrate-{version}-down\n" + "DROP TABLE placeholder;" + ), +) + +_DEFAULT_PY_TEMPLATE = PythonTemplateDefinition( + docstring=( + "{title} - {message}\n" + "Description: {description}\n" + "Version: {version}\n" + "Created: {created_at}\n" + "Author: {author}\n\n" + "Replace 'def' with 'async def' if you need awaitables. The optional" + " context argument receives the SQLSpec migration context when provided." + ), + imports=["from typing import Iterable"], + body=( + "def up(context: object | None = None) -> str | Iterable[str]:\n" + ' """Apply the migration (upgrade)."""\n' + ' return "\n' + " CREATE TABLE example (\n" + " id INTEGER PRIMARY KEY,\n" + " name TEXT NOT NULL\n" + " );\n" + ' "\n\n' + "def down(context: object | None = None) -> str | Iterable[str]:\n" + ' """Reverse the migration."""\n' + ' return "DROP TABLE example;"' + ), +) diff --git a/sqlspec/migrations/utils.py b/sqlspec/migrations/utils.py index 2b41d704..681b91ca 100644 --- a/sqlspec/migrations/utils.py +++ b/sqlspec/migrations/utils.py @@ -1,16 +1,21 @@ -"""Utility functions for SQLSpec migrations. - -This module provides helper functions for migration operations. -""" +"""Utility functions for SQLSpec migrations.""" +import importlib +import inspect import logging import os import subprocess from datetime import datetime, timezone from pathlib import Path -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, cast + +from sqlspec.migrations.templates import MigrationTemplateSettings, TemplateValidationError, build_template_settings +from sqlspec.utils.text import slugify if TYPE_CHECKING: + from collections.abc import Callable + + from sqlspec.config import DatabaseConfigProtocol from sqlspec.driver import AsyncDriverAdapterBase __all__ = ("create_migration_file", "drop_all", "get_author") @@ -18,114 +23,96 @@ logger = logging.getLogger(__name__) -def create_migration_file(migrations_dir: Path, version: str, message: str, file_type: str = "sql") -> Path: - """Create a new migration file from template. - - Args: - migrations_dir: Directory to create the migration in. - version: Version number for the migration. - message: Description message for the migration. - file_type: Type of migration file to create ('sql' or 'py'). - - Returns: - Path to the created migration file. - """ - safe_message = message.lower() - safe_message = "".join(c if c.isalnum() or c in " -" else "" for c in safe_message) - safe_message = safe_message.replace(" ", "_").replace("-", "_") - safe_message = "_".join(filter(None, safe_message.split("_")))[:50] - - if file_type == "py": - filename = f"{version}_{safe_message}.py" - file_path = migrations_dir / filename - template = f'''"""SQLSpec Migration - {message} - -Version: {version} -Created: {datetime.now(timezone.utc).isoformat()} -Author: {get_author()} - -Migration functions can use either naming convention: -- Preferred: up()/down() -- Alternate: migrate_up()/migrate_down() - -Both can be synchronous or asynchronous: -- def up(): ... -- async def up(): ... -""" - -from typing import List, Union - - -def up() -> Union[str, List[str]]: - """Apply the migration (upgrade). - - Returns: - SQL statement(s) to execute for upgrade. - Can return a single string or list of strings. - - Note: You can use either 'up()' or 'migrate_up()' for function names. - Both support async versions: 'async def up()' or 'async def migrate_up()' - """ - return """ - CREATE TABLE example ( - id INTEGER PRIMARY KEY, - name TEXT NOT NULL - ); - """ - - -def down() -> Union[str, List[str]]: - """Reverse the migration. - - Returns: - SQL statement(s) to execute for downgrade. - Can return a single string or list of strings. - Return empty string or empty list if downgrade is not supported. - - Note: You can use either 'down()' or 'migrate_down()' for function names. - Both support async versions: 'async def down()' or 'async def migrate_down()' - """ - return "DROP TABLE example;" -''' - else: - filename = f"{version}_{safe_message}.sql" - file_path = migrations_dir / filename - template = f"""-- SQLSpec Migration --- Version: {version} --- Description: {message} --- Created: {datetime.now(timezone.utc).isoformat()} --- Author: {get_author()} - --- name: migrate-{version}-up -CREATE TABLE placeholder ( - id INTEGER PRIMARY KEY -); - --- name: migrate-{version}-down -DROP TABLE placeholder; -""" - - file_path.write_text(template) +def create_migration_file( + migrations_dir: Path, + version: str, + message: str, + file_type: str | None = None, + *, + config: "DatabaseConfigProtocol[Any, Any, Any] | None" = None, + template_settings: "MigrationTemplateSettings | None" = None, +) -> Path: + """Create a new migration file from template.""" + + migration_config = getattr(config, "migration_config", {}) or {} + settings = template_settings or build_template_settings(migration_config) + author = get_author(migration_config.get("author"), config=config) + safe_message = _slugify_message(message) + file_format = settings.resolve_format(file_type) + extension = "py" if file_format == "py" else "sql" + filename = f"{version}_{safe_message or 'migration'}.{extension}" + file_path = migrations_dir / filename + context = _build_template_context( + settings=settings, + version=version, + message=message, + author=author, + adapter=_resolve_adapter_name(config), + project_slug=_derive_project_slug(config), + safe_message=safe_message, + ) + renderer = settings.profile.python.render if file_format == "py" else settings.profile.sql.render + content = renderer(context) + file_path.write_text(content, encoding="utf-8") return file_path -def get_author() -> str: - """Get current user for migration metadata. - - Attempts to retrieve git user configuration (name and email). - Falls back to system username if git is not configured or unavailable. - - Returns: - Author string in format 'Name ' if git configured, - otherwise system username from environment. - """ - git_name = _get_git_config("user.name") - git_email = _get_git_config("user.email") - - if git_name and git_email: - return f"{git_name} <{git_email}>" - - return _get_system_username() +def get_author( + author_config: Any | None = None, *, config: "DatabaseConfigProtocol[Any, Any, Any] | None" = None +) -> str: + """Resolve author metadata for migration templates.""" + + if isinstance(author_config, str): + token = author_config.strip() + if not token: + return _resolve_git_author() + lowered = token.lower() + if lowered == "git": + return _resolve_git_author() + if lowered == "system": + return _get_system_username() + if lowered.startswith("env:"): + env_var = token.split(":", 1)[1].strip() + if not env_var: + msg = "Environment author token requires a variable name" + raise TemplateValidationError(msg) + return _resolve_author_from_env(env_var) + if lowered.startswith("callable:"): + import_path = token.split(":", 1)[1].strip() + if not import_path: + msg = "Callable author token requires an import path" + raise TemplateValidationError(msg) + return _resolve_author_callable(import_path, config) + if ":" in token and " " not in token: + return _resolve_author_callable(token, config) + return token + + if isinstance(author_config, dict): + mode = str(author_config.get("mode") or "static").lower() + value = author_config.get("value") + if mode == "static": + if not isinstance(value, str) or not value.strip(): + msg = "Static author value must be a non-empty string" + raise TemplateValidationError(msg) + return value.strip() + if mode == "env": + if not isinstance(value, str) or not value.strip(): + msg = "Environment author mode requires an environment variable name" + raise TemplateValidationError(msg) + return _resolve_author_from_env(value.strip()) + if mode == "callable": + if not isinstance(value, str) or not value.strip(): + msg = "Callable author mode requires an import path" + raise TemplateValidationError(msg) + return _resolve_author_callable(value.strip(), config) + if mode == "system": + return _get_system_username() + if mode == "git": + return _resolve_git_author() + msg = f"Unsupported author mode '{mode}'" + raise TemplateValidationError(msg) + + return _resolve_git_author() def _get_git_config(config_key: str) -> str | None: @@ -162,6 +149,98 @@ def _get_system_username() -> str: return os.environ.get("USER", "unknown") +def _resolve_git_author() -> str: + git_name = _get_git_config("user.name") + git_email = _get_git_config("user.email") + if git_name and git_email: + return f"{git_name} <{git_email}>" + return _get_system_username() + + +def _resolve_author_from_env(env_var: str) -> str: + value = os.environ.get(env_var) + if value: + return value.strip() + msg = f"Environment variable '{env_var}' is not set for migration author" + raise TemplateValidationError(msg) + + +def _resolve_author_callable(import_path: str, config: "DatabaseConfigProtocol[Any, Any, Any] | None") -> str: + def _raise_callable_error(message: str) -> None: + msg = message + raise TemplateValidationError(msg) + + module_name, _, attr_name = import_path.partition(":") + if not module_name or not attr_name: + _raise_callable_error("Callable author path must be in 'module:function' format") + module = importlib.import_module(module_name) + candidate_obj = getattr(module, attr_name, None) + if candidate_obj is None or not callable(candidate_obj): + _raise_callable_error(f"Callable '{import_path}' is not callable") + candidate = cast("Callable[..., Any]", candidate_obj) + signature = inspect.signature(candidate) + param_count = len(signature.parameters) + if param_count > 1: + _raise_callable_error("Author callable must accept zero or one positional argument") + try: + result_value: object = candidate() if param_count == 0 else candidate(config) + except Exception as exc: # pragma: no cover - passthrough + msg = f"Author callable '{import_path}' raised an error: {exc}" + raise TemplateValidationError(msg) from exc + result_str: str = str(result_value) + return result_str + + +def _build_template_context( + *, + settings: "MigrationTemplateSettings", + version: str, + message: str, + author: str, + adapter: str, + project_slug: str, + safe_message: str, +) -> "dict[str, str]": + created_at = datetime.now(timezone.utc).isoformat() + display_message = message or "New migration" + description = display_message.strip() or safe_message or version + return { + "title": settings.profile.title, + "version": version, + "message": display_message, + "description": description, + "created_at": created_at, + "author": author, + "adapter": adapter, + "project_slug": project_slug, + "slug": safe_message, + } + + +def _derive_project_slug(config: "DatabaseConfigProtocol[Any, Any, Any] | None") -> str: + if config and config.bind_key: + source = config.bind_key + elif config: + source = config.__class__.__module__.split(".")[0] + else: + source = Path.cwd().name + return _slugify_message(source) + + +def _resolve_adapter_name(config: "DatabaseConfigProtocol[Any, Any, Any] | None") -> str: + if config is None: + return "UnknownAdapter" + driver_type = getattr(config, "driver_type", None) + if driver_type is not None and hasattr(driver_type, "__name__"): + return str(driver_type.__name__) + return type(config).__name__ + + +def _slugify_message(message: str) -> str: + slug = slugify(message or "", separator="_") + return slug[:50] + + async def drop_all(engine: "AsyncDriverAdapterBase", version_table_name: str, metadata: Any | None = None) -> None: """Drop all tables from the database. diff --git a/tests/unit/test_cli/test_migration_commands.py b/tests/unit/test_cli/test_migration_commands.py index c66095d0..7c89f922 100644 --- a/tests/unit/test_cli/test_migration_commands.py +++ b/tests/unit/test_cli/test_migration_commands.py @@ -325,7 +325,7 @@ def get_config(): os.chdir(original_dir) assert result.exit_code == 0 - mock_commands.revision.assert_called_once_with(message="test migration") + mock_commands.revision.assert_called_once_with(message="test migration", file_type=None) @patch("sqlspec.migrations.commands.create_migration_commands") @@ -361,7 +361,97 @@ def get_config(): os.chdir(original_dir) assert result.exit_code == 0 - mock_commands.revision.assert_called_once_with(message="test migration") + mock_commands.revision.assert_called_once_with(message="test migration", file_type=None) + + +@patch("sqlspec.migrations.commands.create_migration_commands") +def test_create_migration_command_with_format(mock_create_commands: "Mock", cleanup_test_modules: None) -> None: + runner = CliRunner() + + mock_commands = Mock() + mock_commands.revision = Mock(return_value=None) + mock_create_commands.return_value = mock_commands + + with tempfile.TemporaryDirectory() as temp_dir: + original_dir = os.getcwd() + os.chdir(temp_dir) + try: + config_module = """ +from sqlspec.adapters.sqlite.config import SqliteConfig + +def get_config(): + config = SqliteConfig(pool_config={"database": ":memory:"}) + config.bind_key = "revision_test" + config.migration_config = {"enabled": True} + return config +""" + module_name = _create_module(config_module, Path(temp_dir)) + + result = runner.invoke( + add_migration_commands(), + [ + "--config", + f"{module_name}.get_config", + "create-migration", + "-m", + "test migration", + "--format", + "py", + "--no-prompt", + ], + ) + + finally: + os.chdir(original_dir) + + assert result.exit_code == 0 + mock_commands.revision.assert_called_once_with(message="test migration", file_type="py") + + +@patch("sqlspec.migrations.commands.create_migration_commands") +def test_create_migration_command_with_file_type_alias( + mock_create_commands: "Mock", cleanup_test_modules: None +) -> None: + runner = CliRunner() + + mock_commands = Mock() + mock_commands.revision = Mock(return_value=None) + mock_create_commands.return_value = mock_commands + + with tempfile.TemporaryDirectory() as temp_dir: + original_dir = os.getcwd() + os.chdir(temp_dir) + try: + config_module = """ +from sqlspec.adapters.sqlite.config import SqliteConfig + +def get_config(): + config = SqliteConfig(pool_config={"database": ":memory:"}) + config.bind_key = "revision_test" + config.migration_config = {"enabled": True} + return config +""" + module_name = _create_module(config_module, Path(temp_dir)) + + result = runner.invoke( + add_migration_commands(), + [ + "--config", + f"{module_name}.get_config", + "create-migration", + "-m", + "test migration", + "--file-type", + "sql", + "--no-prompt", + ], + ) + + finally: + os.chdir(original_dir) + + assert result.exit_code == 0 + mock_commands.revision.assert_called_once_with(message="test migration", file_type="sql") @patch("sqlspec.migrations.commands.create_migration_commands") diff --git a/tests/unit/test_migrations/test_migration_runner.py b/tests/unit/test_migrations/test_migration_runner.py index a919977a..044fe61f 100644 --- a/tests/unit/test_migrations/test_migration_runner.py +++ b/tests/unit/test_migrations/test_migration_runner.py @@ -274,6 +274,57 @@ def test_load_migration_metadata_integration() -> None: assert "loader" in metadata +def test_load_migration_metadata_prefers_sql_description() -> None: + with tempfile.TemporaryDirectory() as temp_dir: + migrations_path = Path(temp_dir) + migration_file = migrations_path / "0001_custom.sql" + migration_file.write_text( + """ +-- SQLSpec Migration +-- Description: Custom summary +-- Author: Example + +-- name: migrate-0001-up +SELECT 1; +""" + ) + + runner = create_migration_runner_with_metadata(migrations_path) + + with ( + patch.object(type(runner.loader), "clear_cache"), + patch.object(type(runner.loader), "load_sql"), + patch.object(type(runner.loader), "has_query", return_value=True), + ): + metadata = runner.load_migration(migration_file) + + assert metadata["description"] == "Custom summary" + + +def test_load_migration_metadata_prefers_python_docstring() -> None: + with tempfile.TemporaryDirectory() as temp_dir: + migrations_path = Path(temp_dir) + migration_file = migrations_path / "0002_feature.py" + migration_file.write_text('"""Description: Add feature"""\n\ndef up():\n return "SELECT 1"\n') + + runner = create_migration_runner_with_metadata(migrations_path) + + with ( + patch("sqlspec.migrations.base.get_migration_loader") as mock_get_loader, + patch("sqlspec.migrations.base.await_") as mock_await, + ): + mock_loader = Mock() + mock_loader.validate_migration_file = Mock() + mock_loader.get_up_sql = Mock(return_value=["SELECT 1"]) + mock_loader.get_down_sql = Mock(return_value=None) + mock_get_loader.return_value = mock_loader + mock_await.return_value = Mock(return_value=True) + + metadata = runner.load_migration(migration_file) + + assert metadata["description"] == "Add feature" + + def test_load_migration_metadata_python_file() -> None: """Test metadata loading for Python migration files.""" with tempfile.TemporaryDirectory() as temp_dir: diff --git a/tests/unit/test_migrations/test_utils.py b/tests/unit/test_migrations/test_utils.py index e9133434..8b702a62 100644 --- a/tests/unit/test_migrations/test_utils.py +++ b/tests/unit/test_migrations/test_utils.py @@ -10,11 +10,20 @@ import os import subprocess from pathlib import Path +from typing import Any, cast from unittest.mock import Mock, patch +import pytest + +from sqlspec.config import DatabaseConfigProtocol +from sqlspec.migrations.templates import TemplateValidationError from sqlspec.migrations.utils import _get_git_config, _get_system_username, create_migration_file, get_author +def _callable_author(_: Any | None = None) -> str: + return "callable-user" + + def test_get_git_config_success() -> None: """Test successful git config retrieval.""" mock_result = Mock() @@ -226,6 +235,67 @@ def test_create_migration_file_python_includes_git_author(tmp_path: Path) -> Non assert "Author: Python Dev " in content +def test_create_migration_file_slugifies_message(tmp_path: Path) -> None: + migrations_dir = tmp_path / "migrations" + migrations_dir.mkdir() + + with patch("sqlspec.migrations.utils.get_author", return_value="Author "): + file_path = create_migration_file(migrations_dir, "0001", "Test Migration!!!", "sql") + + assert file_path.name.startswith("0001_test_migration") + + +def test_create_migration_file_respects_default_format(tmp_path: Path) -> None: + migrations_dir = tmp_path / "migrations" + migrations_dir.mkdir() + + class DummyConfig: + migration_config = {"default_format": "py", "author": "Static"} + bind_key = None + driver_type = None + + file_path = create_migration_file( + migrations_dir, "0001", "custom", None, config=cast(DatabaseConfigProtocol[Any, Any, Any], DummyConfig()) + ) + + assert file_path.suffix == ".py" + + +def test_create_migration_file_uses_custom_sql_template(tmp_path: Path) -> None: + migrations_dir = tmp_path / "migrations" + migrations_dir.mkdir() + + class DummyConfig: + migration_config = { + "author": "Acme Ops", + "title": "Acme Migration", + "templates": { + "sql": {"header": "-- {title} [ACME]", "metadata": ["-- Owner: {author}"], "body": "-- custom body"} + }, + } + bind_key = None + driver_type = None + + file_path = create_migration_file( + migrations_dir, "0001", "custom", "sql", config=cast(DatabaseConfigProtocol[Any, Any, Any], DummyConfig()) + ) + content = file_path.read_text() + + assert "-- Acme Migration [ACME]" in content + assert "-- Owner: Acme Ops" in content + + +def test_python_template_includes_down_and_context(tmp_path: Path) -> None: + migrations_dir = tmp_path / "migrations" + migrations_dir.mkdir() + + file_path = create_migration_file(migrations_dir, "0001", "ctx", "py") + content = file_path.read_text() + + assert "def up(context: object | None = None)" in content + assert "def down(context: object | None = None)" in content + + def test_git_config_with_special_characters() -> None: """Test git config with special characters in name.""" mock_result = Mock() @@ -272,3 +342,44 @@ def mock_run(cmd: list[str], **kwargs: object) -> Mock: result = get_author() assert result == "John Doe " + + +def test_get_author_env_string_mode() -> None: + with patch.dict(os.environ, {"CI_AUTHOR": "CI User"}): + result = get_author("env:CI_AUTHOR") + + assert result == "CI User" + + +def test_get_author_env_mode_dict() -> None: + with patch.dict(os.environ, {"CI_AUTHOR": "CI User"}): + result = get_author({"mode": "env", "value": "CI_AUTHOR"}) + + assert result == "CI User" + + +def test_get_author_env_missing_raises() -> None: + with pytest.raises(TemplateValidationError): + get_author({"mode": "env", "value": "MISSING_VAR"}) + + +def test_get_author_callable_string() -> None: + result = get_author(f"{__name__}:_callable_author") + + assert result == "callable-user" + + +def test_get_author_callable_dict_with_config() -> None: + result = get_author({"mode": "callable", "value": f"{__name__}:_callable_author"}) + + assert result == "callable-user" + + +def test_get_author_invalid_callable_path() -> None: + with pytest.raises(TemplateValidationError): + get_author("callable:badpath") + + +def test_get_author_invalid_mode() -> None: + with pytest.raises(TemplateValidationError): + get_author({"mode": "unknown"}) diff --git a/uv.lock b/uv.lock index 2ae2406e..6098b873 100644 --- a/uv.lock +++ b/uv.lock @@ -3776,15 +3776,15 @@ wheels = [ [[package]] name = "polyfactory" -version = "2.22.4" +version = "3.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "faker" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c4/74/193e3035e33adcb88399bb89fcb57578c15ea3060a085c5fff10e2fcd162/polyfactory-2.22.4.tar.gz", hash = "sha256:e63a5a55e8363830dfd71c0bcfc1651a29d9fc98048b54c8333de1971dc98547", size = 264413, upload-time = "2025-11-10T16:03:37.152Z" } +sdist = { url = "https://files.pythonhosted.org/packages/45/c6/2f795ede27e82bf6ed10a6b689f975017e36bf497915bfeafc3932b1a613/polyfactory-3.0.0.tar.gz", hash = "sha256:c1be54fc10fe738415eef2248bcfc687e3d74634f5e73b70e4ba58952df31363", size = 304234, upload-time = "2025-11-15T11:23:02.003Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/12/95b5e48b07378df89be9f56e1bdc4fcc98928e2f4e7f5f38b3e8e479deb9/polyfactory-2.22.4-py3-none-any.whl", hash = "sha256:6c4ebe24e16e7e8461bdd56dfd7d4df3172936a5077c5e5d3b101a5517f267dc", size = 63888, upload-time = "2025-11-10T16:03:35.897Z" }, + { url = "https://files.pythonhosted.org/packages/54/86/742c4eb237cdb5e7e6dfb1474f5f0dac967896185f832c012dff4d7bdce2/polyfactory-3.0.0-py3-none-any.whl", hash = "sha256:ad9ba3b3f04c292d30673587946e1c5d18e249074368b09abb7c9455288cfc37", size = 61237, upload-time = "2025-11-15T11:23:00.524Z" }, ] [[package]] @@ -3967,90 +3967,90 @@ wheels = [ [[package]] name = "psqlpy" -version = "0.11.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/67/b5/f6808ad6989477ea6ae7025b86dbf5eedabd38184bba42ca2b42e9c6a479/psqlpy-0.11.8.tar.gz", hash = "sha256:8df39e4ea5d5260ccf41e13c8417a65321d9b45064526cac3dd50b0098446009", size = 294731, upload-time = "2025-10-21T11:57:08.019Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/bd/d0e63b6ebc45b362ce5c97d7091eaad1876fb0ddeb4a4535cb85a2cf3f30/psqlpy-0.11.8-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:0587558df1eef54abe7dcc6c914f2fb3ca5a851aece2400a52e7d986aa1584ec", size = 4309470, upload-time = "2025-10-21T11:23:49.095Z" }, - { url = "https://files.pythonhosted.org/packages/f5/de/4ecc13ef54418ebf586dc91554da36cee80ccab6a052a9f59f88cb80b2da/psqlpy-0.11.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d416c055b1dc48e9207ebb2663cdae096e245a0dfc1b98a9114f96fe29e70f6c", size = 4510603, upload-time = "2025-10-21T11:23:52.643Z" }, - { url = "https://files.pythonhosted.org/packages/78/b2/03f28b575695d66e0bc0fcc93d3134fbbb683fef7305c86285cdba3704c9/psqlpy-0.11.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:413dbe0cc124e5983ed508cff2bd8a9ffe3325601dcd81b8997d6b201f31e693", size = 5041528, upload-time = "2025-10-21T11:23:54.581Z" }, - { url = "https://files.pythonhosted.org/packages/d8/13/bcf502c6c512d7fcfd2eb85c5d8d0953e24aadc22233c9d767bfac998159/psqlpy-0.11.8-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8d5b0ddae9a4ce1fd6ffac2a23e4b4a48ad46b8086628b113744d0b3f8c17947", size = 4292969, upload-time = "2025-10-21T11:23:56.97Z" }, - { url = "https://files.pythonhosted.org/packages/b4/5d/bb3c693d6b7b9756c97de2399c71a78543830dcfacb4b78b90448d521034/psqlpy-0.11.8-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0ce7fc481a08c53488295f74cab6a8419d4d5122f49a10550aaa9ff22a9b2540", size = 4913501, upload-time = "2025-10-21T11:23:58.813Z" }, - { url = "https://files.pythonhosted.org/packages/99/f4/6fb22338176e299ac00dc1f767f5857c5a9984cd129211fa07143065563c/psqlpy-0.11.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d77f1d387796e1f36bd792ab8bd1e36afe32d45fc35b25167d2320ac6aef9ed", size = 5019542, upload-time = "2025-10-21T11:24:01.11Z" }, - { url = "https://files.pythonhosted.org/packages/e4/c7/06899d92fb7f60d73a5ed7d759408ce1b8aaf5ac62d37842c63231ce983b/psqlpy-0.11.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b021300fbf840a7ec61e4a9c6a22b81c11c99004b346d670b2399996e5ea4ef8", size = 4677733, upload-time = "2025-10-21T11:24:02.684Z" }, - { url = "https://files.pythonhosted.org/packages/60/bd/d92de227f668290ce7d49d6c379b8f30afacd4b62af16fd16fadb3c3e78b/psqlpy-0.11.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30fe3c6a2eddde105a2f9818ab116bf57050593e54359dc699e8482029a52a46", size = 4798490, upload-time = "2025-10-21T11:24:04.629Z" }, - { url = "https://files.pythonhosted.org/packages/d7/87/a4e5b84ef73c79216c3d1123f1755c1c0e4aa2bf18592c6ebbbd49fe3dee/psqlpy-0.11.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c2c820f9b163bb76fb058389417067bedc9d51b12207b8b8ecca6852ce6cdc6b", size = 4944872, upload-time = "2025-10-21T11:24:06.428Z" }, - { url = "https://files.pythonhosted.org/packages/86/d4/da60eccd555ebd0a912fb9aefb34bcd8c9a66427349abded7b3aad0b0228/psqlpy-0.11.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:065e8bb9fc03cc252f30151856b2a69435791db5d01abd723ab7d17874776be3", size = 5059862, upload-time = "2025-10-21T11:24:08.522Z" }, - { url = "https://files.pythonhosted.org/packages/bc/0f/f3ef9f8324499d8484cdf83e887a20758d40f679e17a429fd4dc2d3678de/psqlpy-0.11.8-cp310-cp310-win32.whl", hash = "sha256:6ccb092ed9eae82485ba3dceb64a2aa7b20522a601e7e4ae396d60e6702d8c33", size = 3533789, upload-time = "2025-10-21T11:24:10.548Z" }, - { url = "https://files.pythonhosted.org/packages/ac/59/e1fb303530e1d4f6980aa9954f5ef1f8f64f48afc4da4844541fccf87b7d/psqlpy-0.11.8-cp310-cp310-win_amd64.whl", hash = "sha256:935e9c16b0a62346482b3f4601dccfefbb2b2d43357be3ef2e60f15d0657d7ed", size = 4080929, upload-time = "2025-10-21T11:24:12.161Z" }, - { url = "https://files.pythonhosted.org/packages/41/00/57414d817c906a39cc6f38bbda02bfe789f767202abcd690a0c8f678dbc5/psqlpy-0.11.8-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8b03513b9de6edaa651f05317530ebfc61e8804f9731a863ae7ba6a27e9277c5", size = 4307124, upload-time = "2025-10-21T11:24:13.869Z" }, - { url = "https://files.pythonhosted.org/packages/48/4b/55dabc9bcde7dee73bbd9eadc91a39e8cd073f2163d2c18537b17d5c0288/psqlpy-0.11.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:85864a71218ae56d90339a5ce84fa6a35003b254b2cf2f0cb0f25e0b1ee2e0eb", size = 4511595, upload-time = "2025-10-21T11:24:15.782Z" }, - { url = "https://files.pythonhosted.org/packages/2c/8c/997ad04f733029455518d479080ffd600748f5d7a7286836c6689b354500/psqlpy-0.11.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ac4b3d973555b2f9f5cae295192ab05bfcf536c370e3923f32de25fb662da5c", size = 5039535, upload-time = "2025-10-21T11:24:17.881Z" }, - { url = "https://files.pythonhosted.org/packages/1e/97/c92896d4058a494f097375a46de9b133e889776457f8f13883711be41242/psqlpy-0.11.8-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:65b0b3143d998517cbd0b9dca0bf25b4aae5e3c1d11d553b6ce4916151df2fa1", size = 4291680, upload-time = "2025-10-21T11:24:19.566Z" }, - { url = "https://files.pythonhosted.org/packages/36/aa/23975a6ea4795fecb436d008afa8dc290fcdbad9864feeaded6ea4163643/psqlpy-0.11.8-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:63a0b826da5f760ade785f11ae89e04a8bd2ac2e887731bf33863232410bc67e", size = 4912166, upload-time = "2025-10-21T11:24:21.525Z" }, - { url = "https://files.pythonhosted.org/packages/0e/42/d64915fbd46dbdbb53a3e56855ae248144c6346fd55005c9f8e6f78840f8/psqlpy-0.11.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3d935554232accbcedc7e341909da6f9ca4ca230bb99fdd1998beb9048f8c9e", size = 5021053, upload-time = "2025-10-21T11:24:23.369Z" }, - { url = "https://files.pythonhosted.org/packages/57/74/e1cea4396c53a0ccb4e09640aac7993ec6b6dbf60cc9924451bb2aec73b4/psqlpy-0.11.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:34c25cd266521f6b1d52cc00881c293aa049360543c2b1eeb697281a21bcaae6", size = 4695553, upload-time = "2025-10-21T11:24:25.625Z" }, - { url = "https://files.pythonhosted.org/packages/8b/b9/a205be1ed0bf77c731511737247a92d9c91287c45ee6c621b2431b8c6c3b/psqlpy-0.11.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9036c4c323b023a476c22f587f65d00fe51f155f49159a12b9871ed2d51e0f71", size = 4798186, upload-time = "2025-10-21T11:24:27.369Z" }, - { url = "https://files.pythonhosted.org/packages/e4/99/6f39b8b6f5414445564cb5a2c02c132695de2da65e68adaac1e1b7fb08c0/psqlpy-0.11.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:52a64c72276338e913965d83090763cabc035222da8be7771ec714f6d4048a41", size = 4943660, upload-time = "2025-10-21T11:24:29.035Z" }, - { url = "https://files.pythonhosted.org/packages/74/1f/1f6ad1c6ddb883fdb8552969c6c32916b41414ec5636120b666341f0d014/psqlpy-0.11.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:95f216f16d6121f0deef9e4822ff010f21e86672a9317d5bb103e449caddfee7", size = 5060289, upload-time = "2025-10-21T11:24:30.677Z" }, - { url = "https://files.pythonhosted.org/packages/76/c8/032abf48c5c7426d88e74621ee2c328142b0d02af137ff8897df1586e41a/psqlpy-0.11.8-cp311-cp311-win32.whl", hash = "sha256:a3e54a7e91a9b7788778f5b0cf54521495c991fbf48db3935d72d46584183e9a", size = 3535627, upload-time = "2025-10-21T11:24:32.265Z" }, - { url = "https://files.pythonhosted.org/packages/4b/2c/444ded8fb7ea0fa8a298408f941f7e34605a3a8a0a3df82be2a2f9dad5c3/psqlpy-0.11.8-cp311-cp311-win_amd64.whl", hash = "sha256:1cac3ec2bca3185fb927b83736c76f2cc8d43e326a61bd233b267a294cfdb755", size = 4082081, upload-time = "2025-10-21T11:24:33.988Z" }, - { url = "https://files.pythonhosted.org/packages/2f/7d/5becf3f8045033578c856ffc3cb84e54c9fe2003f775d9560d948379d805/psqlpy-0.11.8-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bcd58399c115eb26415866ef735c6f6b58bb5081c46b6ba8122bb52e149d9592", size = 4285882, upload-time = "2025-10-21T11:24:36.386Z" }, - { url = "https://files.pythonhosted.org/packages/f2/98/b60baf8d843bcc16d44d3adaf0f23d6750e5717a30644c2b22928687b8c3/psqlpy-0.11.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4764201db31c60ed331733c5d8505f6f9ee41a5757fa4c8aaf0823a8742c4653", size = 4491826, upload-time = "2025-10-21T11:24:38.443Z" }, - { url = "https://files.pythonhosted.org/packages/0c/64/5ea6073c9f699d6ec061a7822efc156434c79e48cf77a023c1dd8d54c077/psqlpy-0.11.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c2ce1165a16414afb3ca8481b86be99462298713757e418d53b76dc3f503706", size = 5035111, upload-time = "2025-10-21T11:24:40.095Z" }, - { url = "https://files.pythonhosted.org/packages/9f/3f/c604bc3b93e21b8593d6fc6f81f30cba755533bb90c61412b97e4943d85b/psqlpy-0.11.8-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2dea5116b0d087433f7f2adda71acd0f79a82b894c05fea288dd1113219f85d2", size = 4294348, upload-time = "2025-10-21T11:24:41.938Z" }, - { url = "https://files.pythonhosted.org/packages/10/c6/8382f6ead24cc760732ed41e8f0faa571fceb00ed3f8b9163fbe68debfa6/psqlpy-0.11.8-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0974df1051b8bae44f62dc4cc0a0dcf1b675b12116524a52994df2838154f495", size = 4908719, upload-time = "2025-10-21T11:24:44.1Z" }, - { url = "https://files.pythonhosted.org/packages/8b/dc/a53a20838b38634cd701cd4249c89c4299635938c6a4e91aa1503f2dc2cf/psqlpy-0.11.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3eb957591217325dd8e6dd56b871105d5189ee80d8e915d70506ab6189bdf601", size = 5013755, upload-time = "2025-10-21T11:24:45.777Z" }, - { url = "https://files.pythonhosted.org/packages/ce/46/83d6b0a65c2f82477280665eaacdf42af917df53e843bb8d5af75d826112/psqlpy-0.11.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:405a72fe6984e96994602e4188e8cfb229049c407d35df841f1349267b69ea56", size = 4695712, upload-time = "2025-10-21T11:24:47.385Z" }, - { url = "https://files.pythonhosted.org/packages/f7/f5/af4a85239ddfa992be42864f3a391912e341b98912ab5b97eed79a3a1898/psqlpy-0.11.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f845627d4bb1603670aa8333589c4a8849539bc4f7508ed63bd3a6822e3b387", size = 4795175, upload-time = "2025-10-21T11:24:49.178Z" }, - { url = "https://files.pythonhosted.org/packages/cc/7d/efb66debe2da0ad3ba504ee836e1d2e915f337ba502fee62583cd1fe62ef/psqlpy-0.11.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:454891a20c34340c65d2d1318205947472f289c35eb4eb51241382315a3cc341", size = 4941598, upload-time = "2025-10-21T11:24:50.829Z" }, - { url = "https://files.pythonhosted.org/packages/ff/f3/ceca47de59450d3082b8e8ab83f3bfd54305a638b681660fc639eec831d8/psqlpy-0.11.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c9b8219bdfe25c4509bfda147b9bcc6feea079cf4fd55c8fbd53991bc377aedf", size = 5056491, upload-time = "2025-10-21T11:24:52.706Z" }, - { url = "https://files.pythonhosted.org/packages/52/f4/cbfee647aad5dd65a364013c48e062e598f2ad188f8287e99000ce02e86f/psqlpy-0.11.8-cp312-cp312-win32.whl", hash = "sha256:c85361d8290829983de762292b6b2e7a9e8d210654a2c9e162035dd6d48a7af7", size = 3527081, upload-time = "2025-10-21T11:24:54.428Z" }, - { url = "https://files.pythonhosted.org/packages/cc/ae/f1d548062fcd7bcd66aa0c34cdfbbc10e0a65a7c4f68ac9a8b358bfa487b/psqlpy-0.11.8-cp312-cp312-win_amd64.whl", hash = "sha256:c8070c5a5394870c1c3a7196158ca96bcafcf46cb3fc5c4a55d5bf21e044e7f2", size = 4089275, upload-time = "2025-10-21T11:24:56.407Z" }, - { url = "https://files.pythonhosted.org/packages/6b/15/893131682f15430d435f22746fe6de738f27af0a7f068c47c4cb6ea42829/psqlpy-0.11.8-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:660b7a34610d04b05c4dbe9fddef10de9adfefe1de99ed22fbfcae2d1878351d", size = 4284607, upload-time = "2025-10-21T11:24:58.172Z" }, - { url = "https://files.pythonhosted.org/packages/b6/a5/9e32ba56801eb771aaa7e78c876edc83ee828c9c3958af38873bdaf9e58a/psqlpy-0.11.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a3a7091b044d077a82f5b0e92ecd56d6b01b91ac1cf87e646b5d965fb8870ce1", size = 4491004, upload-time = "2025-10-21T11:24:59.889Z" }, - { url = "https://files.pythonhosted.org/packages/61/4d/5998a10d10a1531bc3f7db53667aec1ac65a81f2aec26360e6f52579ce02/psqlpy-0.11.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e22ad63729c4669ad5288be4b272d167910df03df9dda02af1faf16069ba0d1f", size = 5034048, upload-time = "2025-10-21T11:25:01.765Z" }, - { url = "https://files.pythonhosted.org/packages/3b/e2/86b8fd8d9086918f352069c56a21fdc77f312b827236c5ba0eddd3aecfbb/psqlpy-0.11.8-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e0d723514cc13fe4b2bfce240d94976aca0698e3bf54e05cd9df57a034f2f872", size = 4290681, upload-time = "2025-10-21T11:25:03.532Z" }, - { url = "https://files.pythonhosted.org/packages/06/9c/840fff06fef63198f4151a306925c8d4fd8243ecac3e00dd7fc6207451e2/psqlpy-0.11.8-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8edf276806426666956021ea474e709a7cf0df1c18340eb43804f820a94aa81e", size = 4912302, upload-time = "2025-10-21T11:25:07.683Z" }, - { url = "https://files.pythonhosted.org/packages/a5/e5/c240bc2d3e64993a0ebfbd5d2efcabcf239a23efe99fde832b780636d9a7/psqlpy-0.11.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c31a140eeac771921584a1d1321821f9b6366683c49fe29d96cd8a997af39a92", size = 5011771, upload-time = "2025-10-21T11:55:09.146Z" }, - { url = "https://files.pythonhosted.org/packages/8a/35/02536873a6d85bcfa4089ebd3ab4692fdc8e6bf4ac3ff9e09d77d466d27b/psqlpy-0.11.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:828fdb088649602dab971da618899db31a3161a5c5747e9b67fc6f417a4b9165", size = 4690661, upload-time = "2025-10-21T11:55:11.101Z" }, - { url = "https://files.pythonhosted.org/packages/61/1c/e75da749021a24f93a2d6556d500e60afa806af706388035df6a9bbe54fd/psqlpy-0.11.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:febd494779ec2743981fa7a7116d20def5150db83f85c06a6c43fb893e122b81", size = 4794216, upload-time = "2025-10-21T11:55:12.905Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ff/81b6256a5af5bd9d314ed39d4de9ea75995952591e5e2a3d93a52d98dcc7/psqlpy-0.11.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30febdc622d19a19c156f2332d4566e2153947416f3020357e36d1fe253e2ba1", size = 4943811, upload-time = "2025-10-21T11:55:14.994Z" }, - { url = "https://files.pythonhosted.org/packages/1b/67/4b3df826e403df9666296ea38310ef957df4deeee6a11cc2f62842bef66d/psqlpy-0.11.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4557c76547bb7accf86189e445cd5c640d59e7e18748264dede15276747b74d", size = 5055150, upload-time = "2025-10-21T11:55:17.064Z" }, - { url = "https://files.pythonhosted.org/packages/c8/f1/252e8899cf86c1535f816513082b96bd80c92645b6968989a8125321d4f4/psqlpy-0.11.8-cp313-cp313-win32.whl", hash = "sha256:42e0699c9c53c4b607ea2ba08ae00fb1c4e9ad64d0676da235091a618b6f2d64", size = 3526516, upload-time = "2025-10-21T11:55:19.009Z" }, - { url = "https://files.pythonhosted.org/packages/4a/be/221a2f3a56e901066ebc095e15a9e1327cac044d0ae7e57854fdfcbc9e0d/psqlpy-0.11.8-cp313-cp313-win_amd64.whl", hash = "sha256:d8accd16f34bc9ccd88df19039c204449c5654929d2873c56c63e3e84f5687bb", size = 4086655, upload-time = "2025-10-21T11:55:20.661Z" }, - { url = "https://files.pythonhosted.org/packages/15/b2/16bd6fad8def763b223837b61070fb2a8cd82d37617822d41af8fb61ffe1/psqlpy-0.11.8-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:990653372affd88fcdb908c44a984a27ac44bd0a4fa16c4b5bd250dda8832477", size = 4285940, upload-time = "2025-10-21T11:55:22.33Z" }, - { url = "https://files.pythonhosted.org/packages/2a/74/0fec0e1e82205a46c6acb6e77c5217b34506d126e4fe9cab94c32ffe6804/psqlpy-0.11.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3c17e457f2fe02c4cd7c4435bdc90ddf2e904052d92299a45ef71ca14e6576ef", size = 4485404, upload-time = "2025-10-21T11:55:24.077Z" }, - { url = "https://files.pythonhosted.org/packages/82/85/1e5788b22172039bfd811a332bc487797c291d46a2eb3ffd08d049318086/psqlpy-0.11.8-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61a4cfc71c971a6974fbff019f7f5a67233cb4c619c9c732309cf28a37bee54a", size = 5035754, upload-time = "2025-10-21T11:55:25.9Z" }, - { url = "https://files.pythonhosted.org/packages/16/7a/58875a85c321fde7264f18ffcdff6b1e1b72958d4b14232a7c84e8f54a2e/psqlpy-0.11.8-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a579c7239db4a7341a4019142b5a35ceb1c404f40a7d350bf58ccbc9f3fbba73", size = 4284703, upload-time = "2025-10-21T11:55:27.907Z" }, - { url = "https://files.pythonhosted.org/packages/18/d8/aee9b580058e0a03b7679ad2c605d869c742a691b0c75fe9ca70a16e5ae5/psqlpy-0.11.8-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d59154f73379d13a019ed0878c8ac6d6ec9f9d8b88aa23a86bca15fc32850404", size = 4913587, upload-time = "2025-10-21T11:55:29.798Z" }, - { url = "https://files.pythonhosted.org/packages/bf/9c/6fd360aa52bcd9101970d633e4415bf34fcd4cb2030a897259f6afc87dc9/psqlpy-0.11.8-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0e905c570256d3fd08e0390d4c4b51b6ddf92ba78350c947c4dc2d1553349a0f", size = 5006436, upload-time = "2025-10-21T11:55:31.519Z" }, - { url = "https://files.pythonhosted.org/packages/92/5e/8038e15f150bdafc34fea8bcd3f02cf7dfc589ea575b3f7fd0ca12a2960f/psqlpy-0.11.8-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e8f84c132702a2a8ec4653630729c1146543727d56c391a93ebe61c203203063", size = 4674817, upload-time = "2025-10-21T11:55:33.268Z" }, - { url = "https://files.pythonhosted.org/packages/5a/1e/3c7450fb1ef29a8933b19e298fcc5acc15f3ba76bd9987571a73e0ac8468/psqlpy-0.11.8-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a43b556f36d3e62c52f6aa35d1abb660a60a12d0c9cd8360205c91568680ecdd", size = 4788628, upload-time = "2025-10-21T11:55:35.267Z" }, - { url = "https://files.pythonhosted.org/packages/67/c0/201a5097113237a570e89e9823ca09a2d8347569a0c3ed1f12457cfb2ad5/psqlpy-0.11.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:d9efec0e69bc29375897497b59da2990b9b17d1657b5144f94fdb3bafe1eacaf", size = 4943714, upload-time = "2025-10-21T11:55:37.056Z" }, - { url = "https://files.pythonhosted.org/packages/c3/52/48733dd4229933e704dfeb61859d771cc040ff6eb411e8fdc37446347fbe/psqlpy-0.11.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:318e26e355a789939c4cc601c3c7751a1e0426a7102a42c257e7ac254cefb8b2", size = 5051549, upload-time = "2025-10-21T11:55:39.083Z" }, - { url = "https://files.pythonhosted.org/packages/e1/58/f0d927fe9fc588df92dfde55b803213cf37b9203f1d51d4f48d461ee308b/psqlpy-0.11.8-cp314-cp314-win32.whl", hash = "sha256:2936d7c9f1eb68e463256bc5f64fa6ab3ac053b9065c3af3ab94bdbb440b075a", size = 3529821, upload-time = "2025-10-21T11:55:41.24Z" }, - { url = "https://files.pythonhosted.org/packages/53/8b/871362d24c52a414ab95c89e92a5131cdd6403d66f5218a44290dc1f997d/psqlpy-0.11.8-cp314-cp314-win_amd64.whl", hash = "sha256:c5e5c8ad948dce0eb0f2d8eab3f0c370d16b804c2d9fddf83825463fc08cbd7a", size = 4085871, upload-time = "2025-10-21T11:55:42.949Z" }, - { url = "https://files.pythonhosted.org/packages/3a/a5/7c341424adc76d67ab6847ebbc9f03a108eb5bc790e3c29bc5267f540b6f/psqlpy-0.11.8-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:86fd112dd723e2f083ef18b8189698a2aa29c969292e1bfa5c9888852c5c9702", size = 4303659, upload-time = "2025-10-21T11:56:07.855Z" }, - { url = "https://files.pythonhosted.org/packages/73/fa/ba09d6041af79e17db5a5f0cd5e1f90fb9ba30a57c631c74b7d66a64a146/psqlpy-0.11.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:4f909134fe32ac9967f38528f01c8d9d92d2ba19e24d11b0b55b24833a97f744", size = 4513442, upload-time = "2025-10-21T11:56:09.934Z" }, - { url = "https://files.pythonhosted.org/packages/b8/bf/05855f47c8fd236dfb83c363b2c82657f5c5a9b62dd598abefad47d0f33e/psqlpy-0.11.8-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67826aff627dc6cb6758bb90cb32687d71740f28cd66147e2378389a576cd1e0", size = 5040761, upload-time = "2025-10-21T11:56:12.574Z" }, - { url = "https://files.pythonhosted.org/packages/56/70/a47ecaa5d85179a4ffbf9d842c5eaba75c6a9c983626b8eeac932919f8b6/psqlpy-0.11.8-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16a72c32ae718187d0390c40a6f1f9637be40ec1be1eb6ba6c999d46415213e0", size = 4291609, upload-time = "2025-10-21T11:56:14.794Z" }, - { url = "https://files.pythonhosted.org/packages/74/69/a9016cc3bb19aa3136ba6e18a70218fede765f931bfff662cd887cd5839a/psqlpy-0.11.8-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b767d04389cc27680886f4d4b8db2e5e6bf9406f1d52965b7e59381d49f535d1", size = 4915834, upload-time = "2025-10-21T11:56:16.492Z" }, - { url = "https://files.pythonhosted.org/packages/d8/b8/a6242cdbd809fd2d34a16d20d9298905e1ac8c1b44d21d6d88203bdabc57/psqlpy-0.11.8-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:27b869ba7d04737509ebdc4ab9a5a360d3ab1433afa01a7da1f41bcce74e00d7", size = 5020542, upload-time = "2025-10-21T11:56:18.479Z" }, - { url = "https://files.pythonhosted.org/packages/01/58/012f28206a7e8c4036f1d4a3537061c5ca5fda8e7351ccdd37576127de42/psqlpy-0.11.8-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63184be6d271bd7b7b17891a70084d1ad5e683f58075700e08553ed2a971ebb6", size = 4699631, upload-time = "2025-10-21T11:56:20.553Z" }, - { url = "https://files.pythonhosted.org/packages/04/b8/553ae6baf8deaf4ca41232343e33487c573b9790603ac022903a95e86c8f/psqlpy-0.11.8-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4bdabae3903797fee994deb6ba5937481b99649359fdf473c757be98a9e6d20a", size = 4803327, upload-time = "2025-10-21T11:56:22.364Z" }, - { url = "https://files.pythonhosted.org/packages/ef/c8/4ba41ba2fd75babf62adbe5bf5a0f0686bd62a53a0b9138d37bc0b52693a/psqlpy-0.11.8-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:7973d4ed5d1b4abda5427f50b4d86d6f0118c8624d1f63e55635a31563b596a6", size = 4947861, upload-time = "2025-10-21T11:56:24.093Z" }, - { url = "https://files.pythonhosted.org/packages/e4/1d/f301f423cd0444f29efe6f85ad30486e9bcb1db3467afdbd339dc78e36a1/psqlpy-0.11.8-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:085c772b0f61199e1f208c8cb47654cf15dc9798c69be8448165084c9bdcf9ad", size = 5067475, upload-time = "2025-10-21T11:56:25.939Z" }, - { url = "https://files.pythonhosted.org/packages/9d/6b/b51485ac1d50cb793819e8a0f3a784478bc4fd8a301864efecfc79831340/psqlpy-0.11.8-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0dd933274b8d985aaac51c75c102cf808e2514fbfd272126864d1c9264ed7a7c", size = 4305224, upload-time = "2025-10-21T11:56:28.174Z" }, - { url = "https://files.pythonhosted.org/packages/f4/4d/cb952740501008787da9d714d37b1b60c8c11335757737cf28b13b7e7e27/psqlpy-0.11.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:778e54ea9fbdc0e73b4ca6047808f7999db5e050f47b482db4e6e71483cfcc39", size = 4514304, upload-time = "2025-10-21T11:56:30.142Z" }, - { url = "https://files.pythonhosted.org/packages/98/e0/6e30fa12d33d68207dfb1aa3f6615df807f4af1b9d1d4b1738e202790551/psqlpy-0.11.8-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b650cd7d3cdf73d7c5047c32fa167caf2fceab9976eefffb61832bcc5c4653", size = 5042233, upload-time = "2025-10-21T11:56:31.998Z" }, - { url = "https://files.pythonhosted.org/packages/4f/fd/e3ed220c91942ddcd634fa9e176f39363776ea540132079e2ffc32ca498f/psqlpy-0.11.8-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1768d302d9e004afb10bd881615a2ebf242dbc541a7af4f596f6fd090adc00ee", size = 4292408, upload-time = "2025-10-21T11:56:34.126Z" }, - { url = "https://files.pythonhosted.org/packages/20/94/f5c889686f0608fe86d3da05161175ef9e786d67f115d57f06288f207b2d/psqlpy-0.11.8-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27db08732a16f9abddddf83c14583d27bb365d9fb56e7ed56bf620af992f0091", size = 4916966, upload-time = "2025-10-21T11:56:35.836Z" }, - { url = "https://files.pythonhosted.org/packages/78/35/7a69708d8f534c720870f2ff8190faae218f4ba7ae8557cd822e4d7d2b50/psqlpy-0.11.8-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3493fdb57e0e67a0ffd8e678c68e877541d7600b57dde381c4b30089b55c4987", size = 5019434, upload-time = "2025-10-21T11:56:38.277Z" }, - { url = "https://files.pythonhosted.org/packages/32/c6/94143c4954aa7d6211e741fc9d7a5455640570601b53b9921c713b96d847/psqlpy-0.11.8-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3edd56047d37d776118a2123692fc0fead38ad485b9b27fa673b395a8471b97", size = 4699046, upload-time = "2025-10-21T11:56:40.379Z" }, - { url = "https://files.pythonhosted.org/packages/47/d3/1d94683aff540b4b596dd6f81db7749089081ab64c46764e0ffd53cf0372/psqlpy-0.11.8-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:350081cee4584eb778f198ab97d3e4c640b67ff6b37bf045b06cc55f7e262a2c", size = 4803103, upload-time = "2025-10-21T11:56:42.255Z" }, - { url = "https://files.pythonhosted.org/packages/b2/fa/4ef2c0789c722821321573a1bba79f5746b82e177b121941659bcee388b3/psqlpy-0.11.8-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:678369aab8bceae5c3e2d074e2a75fdf315f1abccd2711d5c8af856a349d08fe", size = 4947668, upload-time = "2025-10-21T11:56:43.998Z" }, - { url = "https://files.pythonhosted.org/packages/51/a7/f4e693a38b64384bbf5545bba6cda4e006a3ba420664b466c59fd0874a2a/psqlpy-0.11.8-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:415a0b8aa04e8bea44f16a7d6d2a6cad80267997d7e42582350bc1b341f03865", size = 5066219, upload-time = "2025-10-21T11:56:45.992Z" }, +version = "0.11.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7a/48/431f11082d4d4f94728870b77f7bb6205a291f1a2c900f15b7dada9ee59e/psqlpy-0.11.9.tar.gz", hash = "sha256:ae31bdb837d86a6b9523788ee21b2719bf8032dc6e4808155f68e75350a05dde", size = 287462, upload-time = "2025-11-15T18:38:51.576Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/0a/fd39108b1d83b7827535bd769264e53eddf712d9c0fe9579a3b0ae28e261/psqlpy-0.11.9-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a37b883f1e20e55e3b088a3b1a3446cfc4de55de3c4ddc54cebc26d04899f797", size = 4371678, upload-time = "2025-11-15T18:36:18.474Z" }, + { url = "https://files.pythonhosted.org/packages/eb/4a/f127cb4a1b968536d2f7f340c45ced1dd6a8691a00657e6581d48aafa361/psqlpy-0.11.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8832b8f2a26bacbf9cca9c6c883600aaff8274fc7e3c8408a3a65e65c1e9db84", size = 4609079, upload-time = "2025-11-15T18:36:20.313Z" }, + { url = "https://files.pythonhosted.org/packages/42/6f/933f102c98ede986b9f8e72dc35c39339ec1309eb303ae82d0b34affaf1e/psqlpy-0.11.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:784214067937164e624f57cd0e563d157d7c391c054ca16145ab85e5e96a1935", size = 5141219, upload-time = "2025-11-15T18:36:21.46Z" }, + { url = "https://files.pythonhosted.org/packages/35/78/2cc562c9b3fc689f87bdab326d818f43b825d93439de11557879dec8762c/psqlpy-0.11.9-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:737f3f214d3003a2091e36ad12eb2415129b964eb310eedf85f0ac6e9b637f9f", size = 4407252, upload-time = "2025-11-15T18:36:22.915Z" }, + { url = "https://files.pythonhosted.org/packages/df/95/c2f8e6c3c9b7b0e6b1d4c11f8dfa36dff39438f960b75f5a01e8a6b11148/psqlpy-0.11.9-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43abd0fc78ec1a5ba7d5323dfaf80db9a6997d0842ef48c6b7dde7a582ecb365", size = 5039373, upload-time = "2025-11-15T18:36:24.514Z" }, + { url = "https://files.pythonhosted.org/packages/a0/07/a0f3fd6dceb0e66d8fe43e4151872f8eb5b1dcf50864cd8b0c554b8953f3/psqlpy-0.11.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cea3ce587163bc376c91dbb7db10994dbf3939c0753ef76d6c040d111d9ced78", size = 5070147, upload-time = "2025-11-15T18:36:25.854Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4e/d85e5b1400dbd56df676cd75195ed7c6e5e428d3c79607858b2c1e85f983/psqlpy-0.11.9-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5fe27df0921ab4bb139cb74817f622a13d0de4600800a09dcc77187d1616fcfe", size = 4750141, upload-time = "2025-11-15T18:36:27.455Z" }, + { url = "https://files.pythonhosted.org/packages/e5/29/313cf75ef1c5c0f4dccfaa8ceaebaa260fccf39c765d24cfa5ddb455df4d/psqlpy-0.11.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa2626817346c5c4b2772d4703df457a070bb4802f3012c8359df08dbdf3901e", size = 4899161, upload-time = "2025-11-15T18:36:29.07Z" }, + { url = "https://files.pythonhosted.org/packages/e0/c8/1b20a9ed0d4e3c84af32d2732454f9e4e7e9d1eb9b5a3c2b9303b983765b/psqlpy-0.11.9-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ca98bb42cbd150c6c28df6ce7b1b28500fd36a79e5db854c64788c48ac85c0ce", size = 5070423, upload-time = "2025-11-15T18:36:30.98Z" }, + { url = "https://files.pythonhosted.org/packages/0a/70/13bda234c2d993d988f7cf0d0e7151145cd72499c1c5d532209b91c2c57e/psqlpy-0.11.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:acba66f1041167c23df4775fbd72c6f9099e8ac1ac7c0b9870f1b0f27bd35c70", size = 5166334, upload-time = "2025-11-15T18:36:32.292Z" }, + { url = "https://files.pythonhosted.org/packages/3b/5a/f0885a62120262b4e81e38d11655bbc830097c5bdf728956931273280e21/psqlpy-0.11.9-cp310-cp310-win32.whl", hash = "sha256:350c8ff8f4a50559d4a919941bee5a98bafe2516302b8fdf1e70052fc72df0b8", size = 3599557, upload-time = "2025-11-15T18:36:34.025Z" }, + { url = "https://files.pythonhosted.org/packages/15/6f/cf5be32b90604988ae0aebaefd1171f70a407685acb737b786c1f9baa88c/psqlpy-0.11.9-cp310-cp310-win_amd64.whl", hash = "sha256:acd7124cb3bbacb368740807b9adc463c9270a2c9a0a8be5dd4705fd8c88e04c", size = 4230715, upload-time = "2025-11-15T18:36:35.551Z" }, + { url = "https://files.pythonhosted.org/packages/33/ba/b6927637566af1f09476b00eb9024aa59bc96a9e7e75de0117f8cfd1c6cc/psqlpy-0.11.9-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9bdc56458a1bbab7ea0c2150b291e9138c9175a7bc7196a26e0c5d5fee05885f", size = 4374571, upload-time = "2025-11-15T18:36:36.968Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cd/5c8caa158948553466c74ff5a5663bd062b507640ec44bc8d26691528ee2/psqlpy-0.11.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7bc39d2f28020a26cb152b31f654f9a3eb260ce35ffbb8ebd3093ccfb49dceec", size = 4609775, upload-time = "2025-11-15T18:36:38.175Z" }, + { url = "https://files.pythonhosted.org/packages/74/ef/3b70c57015c1ac2eafb228b9188ef0cc25debc50c41567c5c7649760cab2/psqlpy-0.11.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f04d8d3df3a9dc384336ad417c3fdc1fd7d13b7749df66dfdb9a36f4f04ff03", size = 5140311, upload-time = "2025-11-15T18:36:39.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/c0/aac3a5066ff4e6071b56bc48f14ec627bee03b30a437f43ac78407d0b25d/psqlpy-0.11.9-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a1130d1011b15a30b5ac8c2d70045a5f9368979f39fe694a6c257832e8b9660", size = 4404472, upload-time = "2025-11-15T18:36:41.306Z" }, + { url = "https://files.pythonhosted.org/packages/33/8d/995e7c4cc29f34f92e9a96d080ffa0c508890ce6dcd19b76a2e4bfd162f1/psqlpy-0.11.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c72c5ab90269bd72700d73e8f12542f6b4d66fe5c9dc33c06b77450a82d82d4", size = 5038348, upload-time = "2025-11-15T18:36:42.942Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/a6dec79467f7e81004db6bb0391c3c527c0a0d85b4b5e97855dc7d675044/psqlpy-0.11.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:810a73fcb26d3c78b789f1b0f8a7832ae14d1453c92534ddbf55f9c677c71f9b", size = 5071259, upload-time = "2025-11-15T18:36:44.425Z" }, + { url = "https://files.pythonhosted.org/packages/db/64/a908862f790879c36c5cc45126a1e0e51bdc950d073fd6a8bcc92d90b272/psqlpy-0.11.9-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9a5333708d52060da91d6d707595f530467e7d3cf5cba91c28805e21521a7fdb", size = 4750815, upload-time = "2025-11-15T18:36:45.824Z" }, + { url = "https://files.pythonhosted.org/packages/83/6b/618d1289686fef87ff0e7c61a00a9dad74c1453e5e71f92bf781b78ac692/psqlpy-0.11.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:960e0c38e2f6f6a9d744c745fe0e68c601aef39abb0688a12a9d0b891320df1f", size = 4898763, upload-time = "2025-11-15T18:36:47.12Z" }, + { url = "https://files.pythonhosted.org/packages/08/d5/35e94b6616b60ab33a7ba0c5fea4cc841a6e6adf03882c1f850820db0892/psqlpy-0.11.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8b6389f56cfdf762d476cbd6e2637836a186401da11c1219b0b2357c44a4a3b", size = 5069085, upload-time = "2025-11-15T18:36:48.488Z" }, + { url = "https://files.pythonhosted.org/packages/8d/cf/391c0afcd97ee038f3421b74cfd4d23295efc7278f5813131efb673d9e50/psqlpy-0.11.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a748c4ef343dc497c8e2f4699b5237dad23ba155c33f892a7362398f7e3674ee", size = 5165649, upload-time = "2025-11-15T18:36:49.912Z" }, + { url = "https://files.pythonhosted.org/packages/76/67/1fc25edd1fd3122c3d0d0df772d4f73b68c570391909cb9f00e07e0cd552/psqlpy-0.11.9-cp311-cp311-win32.whl", hash = "sha256:66e36854be17062510f2c8ea06d4b8afc5dd5f5bb6b0b63f3c6304c5ff05e9f9", size = 3599170, upload-time = "2025-11-15T18:36:51.127Z" }, + { url = "https://files.pythonhosted.org/packages/23/53/098bbb6b6d2d7d49a25d5f7fb4b4cf34956d058e830df2b3ae493922b0a6/psqlpy-0.11.9-cp311-cp311-win_amd64.whl", hash = "sha256:bc5c404923c23262d7b25d5247e2a90a84789338a2c09c2c0c9ec26154948420", size = 4231147, upload-time = "2025-11-15T18:36:52.4Z" }, + { url = "https://files.pythonhosted.org/packages/b3/97/1556e78df58001676d3c6ea8b9f22e17ceac4b52f8c61b6f1d92e65be26e/psqlpy-0.11.9-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0c4d3db23666a79c89170e004731a37d0e98741173e8d68c3baa2deee397a9dd", size = 4350470, upload-time = "2025-11-15T18:36:53.603Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c8/f5fa0ad5a63c45b92cfcead90edc354a77271f1db6f317e4e2f9e14a2112/psqlpy-0.11.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd2a416564f3574bafe512a1c175e9e2a97421217595a8097dcc236ecaeda2b7", size = 4581139, upload-time = "2025-11-15T18:36:54.845Z" }, + { url = "https://files.pythonhosted.org/packages/5a/42/c9c7a233b7f9470e79e1ec5c0f6bc92d332840521fdfdc3dd1c67452e097/psqlpy-0.11.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70a74a2c12c8516d0c9249f3cdd4c97baa3e60ea1367d0be70a08b4ab19a66ef", size = 5129771, upload-time = "2025-11-15T18:36:56.446Z" }, + { url = "https://files.pythonhosted.org/packages/8b/57/7a027563e64bac5886c2ad75505a52ef18b34c3b2974450073866a9869c1/psqlpy-0.11.9-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ea707cdb263359e4373f188dcd909b9db6d35f5b8a777dfe15cd8b46ed5ba41b", size = 4409302, upload-time = "2025-11-15T18:36:57.995Z" }, + { url = "https://files.pythonhosted.org/packages/32/d3/4d050d09d3345bed33f7e144bd033a20a048f482d5c50366ce4df8c83f7b/psqlpy-0.11.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb48b28e8882b106d73c744f2a47afc3098af14d2c9dad4658d224c5360faa04", size = 5037508, upload-time = "2025-11-15T18:36:59.212Z" }, + { url = "https://files.pythonhosted.org/packages/77/b4/99a52bb510846409844eaba86de10acec95ab009104f9651ac70bb339f42/psqlpy-0.11.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ca61d21789ab87d43937e8c63774ae99fd038d3a3e1e7126fcac6fc2227eb33", size = 5049495, upload-time = "2025-11-15T18:37:01.083Z" }, + { url = "https://files.pythonhosted.org/packages/06/53/caaa249eec823778079875b88f215fdc52621a08c6a267de0f9d211c5b4b/psqlpy-0.11.9-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab1dba60c562da8ba1a5e9fbd2daa6128f80fa57b0aa3b4c5fc2b93e8995c204", size = 4744129, upload-time = "2025-11-15T18:37:02.489Z" }, + { url = "https://files.pythonhosted.org/packages/78/f7/b2c4c00e602e7d2ec7821c4e95724a39eb3074b4a2abde3b4e78dd371df5/psqlpy-0.11.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b7993cb0fd3f5175008d240daef8de081cb8baddd29b0774c6417cd66e16032", size = 4890850, upload-time = "2025-11-15T18:37:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/df/b0/992c6ddab263de0687d816bed57f94f3b2c6ed278a9bca3cf015b923e968/psqlpy-0.11.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e216dad45044f27ddb4d754d7183bbde3b75634ceaa82a5f93011bc6e2910f03", size = 5063912, upload-time = "2025-11-15T18:37:05.435Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d8/6c05da696abe6db3a33f6cb5502a044c832b8e02560510aaa2e1e986716a/psqlpy-0.11.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0bfda9b16b803bbc70cfc08a0ae66fc750354c5525b25d3690ce97bdb53d44cb", size = 5160217, upload-time = "2025-11-15T18:37:06.704Z" }, + { url = "https://files.pythonhosted.org/packages/bf/21/619b1aa75c1e0ee3bd884aa7f352720d841f6db77e283464178dadeb850c/psqlpy-0.11.9-cp312-cp312-win32.whl", hash = "sha256:e4ffede2f7e69d705690835a24d8476abca6f5d431b99e3d38f58ae89cebcaca", size = 3594308, upload-time = "2025-11-15T18:37:07.974Z" }, + { url = "https://files.pythonhosted.org/packages/ef/4f/7ecd1b5085a29422bcb8b37617bc35a8b1d0bb36d279cd2228bd5ddc4b13/psqlpy-0.11.9-cp312-cp312-win_amd64.whl", hash = "sha256:9912766c87f2b72526b810eadd032d86f190ce864ad8d1897a5a9757f5666f84", size = 4231163, upload-time = "2025-11-15T18:37:09.27Z" }, + { url = "https://files.pythonhosted.org/packages/24/3b/36352555ca80639e8f2a8ea49cf3ac6845311e9c992c9b13e19b405143ba/psqlpy-0.11.9-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:c8912d5978be49ee2c3f7c4dbf52eec13ab8150365b56c6913b97e5555375d95", size = 4348381, upload-time = "2025-11-15T18:37:11.028Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/47d19c52a23eaedd5ac14620c7001932f561b01376e724c61d9be55b56e4/psqlpy-0.11.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc1083d5784d08cc1911add6bdc8e536e62291c63a97a637922a5d900a4276c7", size = 4578810, upload-time = "2025-11-15T18:37:12.284Z" }, + { url = "https://files.pythonhosted.org/packages/16/d6/9b00ecab538bc830961dc2262b3ddde22acd50b9526a77c6cb587c5c19af/psqlpy-0.11.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a434f5cef30396b4b5a2a2e7ce8a0bcdedc9bf94f4f5854ca217d3a859e92a32", size = 5128570, upload-time = "2025-11-15T18:37:13.582Z" }, + { url = "https://files.pythonhosted.org/packages/42/87/71f11ccccfb25902a6e94074bbaf02c940ba08e58ca51c70398840bdad69/psqlpy-0.11.9-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:377d88d0b1c12e365aab00a42e780af805a77b65fcd4dd0ae89f4355c5a88b72", size = 4406871, upload-time = "2025-11-15T18:37:15.262Z" }, + { url = "https://files.pythonhosted.org/packages/78/09/aafe917c322b8021fd7de910344922a7dce13b70aaf78180c0f5e9743a01/psqlpy-0.11.9-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac2b4312410626ab698641847f86ddbfb452aaec84643d76a1c856a1bf79265b", size = 5043092, upload-time = "2025-11-15T18:37:16.89Z" }, + { url = "https://files.pythonhosted.org/packages/56/7a/87e855eb447790de1061f695171b77464becd34417926162b75435b9121b/psqlpy-0.11.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b3516346ef88f03804156b3d53a9f937d82d99ee12e7292f22b4e51f233735e", size = 5055734, upload-time = "2025-11-15T18:37:18.872Z" }, + { url = "https://files.pythonhosted.org/packages/d3/d2/859ecbe788e1c13c92c284c73bb3104dbe61b86022cf00c0302734dee072/psqlpy-0.11.9-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c78101d8b64ca98515a4f0ffd727fca9aecb6710ccee686f10a08d5264e327cc", size = 4744799, upload-time = "2025-11-15T18:37:20.202Z" }, + { url = "https://files.pythonhosted.org/packages/c2/7f/c32cbe53e4eadf88b5891dde081462efda60815c0dfeeb0cba68ee6ac6ea/psqlpy-0.11.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5082f851677c7b2ac28c25052ab3985b0ed28f10f33a8f149212d13112e4700", size = 4886377, upload-time = "2025-11-15T18:37:21.53Z" }, + { url = "https://files.pythonhosted.org/packages/c9/93/1715af20e0a83b11dbba9486a74167735e91da2ab30992b83cf1612ef6ad/psqlpy-0.11.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fd7a073d9135849b0a3ea778a1e6d44eb2fbc41f3af8924b5337c152d0e20fda", size = 5066113, upload-time = "2025-11-15T18:37:22.906Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7b/1a92c12544db0524a8dc4ba70dd9789f6f88592def77738fc16ccdc895c1/psqlpy-0.11.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ceb8a7ae06e94fa609bc0f69b924d09901b746e57f7d72c2249bd1101297e0ae", size = 5155299, upload-time = "2025-11-15T18:37:24.182Z" }, + { url = "https://files.pythonhosted.org/packages/25/fe/c6c1a1f917ab19a34de65ef2dca2768ed8c2a61e48ecd57e8bdb0d9cefd2/psqlpy-0.11.9-cp313-cp313-win32.whl", hash = "sha256:4d710807e067f4ea3eb0fb01ba348eb0b2292df7ad778dc2644d14d65875158f", size = 3593689, upload-time = "2025-11-15T18:37:25.74Z" }, + { url = "https://files.pythonhosted.org/packages/0f/cf/15351fb861025cc0703813307eef09d9499fd71637bea557f52d38909d1a/psqlpy-0.11.9-cp313-cp313-win_amd64.whl", hash = "sha256:d28b59d2ea97d99a1f3062df8f6de2240bb7c1ca4233f496a0c59e8c6296add4", size = 4229513, upload-time = "2025-11-15T18:37:27.307Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d4/43f443f2d9cbd137e64e81d53338e005ad0c047d3053aaf0246ae82ba99b/psqlpy-0.11.9-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:f63c8f66c99ee454f662b1c1d7f8996faa3986c58e9944237251361d1bda91d0", size = 4343691, upload-time = "2025-11-15T18:37:28.576Z" }, + { url = "https://files.pythonhosted.org/packages/e1/1a/38126c788409888000d586dcbe7bbba68ff0d63993346496cceead86ad07/psqlpy-0.11.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7557ba13bf6648a25d7e74c54bbf3e41aefd3168e94eeb86786d941dd8e1eb2e", size = 4579360, upload-time = "2025-11-15T18:37:29.852Z" }, + { url = "https://files.pythonhosted.org/packages/fc/a9/81ee66d66813855ea1b02224d4ee0ddce4dde5fde21905591509f585756b/psqlpy-0.11.9-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9c45d1702d70027cdb622dc5230a65e6705f00748dc2542cbda52f4dd14f218", size = 5127742, upload-time = "2025-11-15T18:37:31.164Z" }, + { url = "https://files.pythonhosted.org/packages/de/de/992fbdd3c77ac98342764871bebdb327fa72a60007187538f1af0ffa8860/psqlpy-0.11.9-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:af2afdae8aef64de6b3946da1f382fcfac1f2db564560fd92436a3793889b1af", size = 4410336, upload-time = "2025-11-15T18:37:32.703Z" }, + { url = "https://files.pythonhosted.org/packages/41/c6/ef40623485f2c31ee776f27fd249cddd74358a6b106441df94bbd5d92a60/psqlpy-0.11.9-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8be2bd7eabf0c80a2d9b52bb91ef8e7232e4ea538c8f0418ed61e73be6f96723", size = 5045468, upload-time = "2025-11-15T18:37:34.027Z" }, + { url = "https://files.pythonhosted.org/packages/ec/40/a59e88a2e4d889f3027e842dff5a4aa47c67ce9b5bc2a6d106f97594d0d5/psqlpy-0.11.9-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c40f22bf6387c4a2b86029f09d9287e135670da1505edd34bf3b92507107a25", size = 5060534, upload-time = "2025-11-15T18:37:35.699Z" }, + { url = "https://files.pythonhosted.org/packages/a6/4e/5ed5a1c43bb389c5745bd22b9ef55522afd1cc5b3245ab76938b442be057/psqlpy-0.11.9-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:44b38b169054d9c090bc9424d4b519a041a471fe4c343420be4fee3a8d277404", size = 4747839, upload-time = "2025-11-15T18:37:37.518Z" }, + { url = "https://files.pythonhosted.org/packages/04/13/a646e378b4e6893d3e3776dc38789f786bacc0c78a306d4e59e88e4538d1/psqlpy-0.11.9-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50bc4fc484d4356793b764909432df7c613bf018fa0ddd0b9b9c824b083454ba", size = 4887085, upload-time = "2025-11-15T18:37:39.713Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ec/031b00c0707f9de89b726ba943c9bbae7796e67da22c1bf8895689264a73/psqlpy-0.11.9-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:09a6d2846ee1d877303b58f55fd166143cf04bc03e3445b6eac676445fd75901", size = 5072630, upload-time = "2025-11-15T18:37:41.06Z" }, + { url = "https://files.pythonhosted.org/packages/bb/77/88fb5ee6b6b9b41d49ac7e9d534bfb2fbff25c9e652f3323c519f63020b7/psqlpy-0.11.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3ba512b60ebea80d62918a2960a37639a6e429318a22eb3c25efcb84b33f3f5", size = 5156237, upload-time = "2025-11-15T18:37:42.348Z" }, + { url = "https://files.pythonhosted.org/packages/36/1c/36d43380783e394705331af82c7d220cc502bebe2704a2ed08cdeb3f5dc7/psqlpy-0.11.9-cp314-cp314-win32.whl", hash = "sha256:c175bde8cef83d3784364df2e8b618458e26f7671a87b452643583e90fc761e7", size = 3598006, upload-time = "2025-11-15T18:37:43.935Z" }, + { url = "https://files.pythonhosted.org/packages/39/36/7100003d4bdeecb1afdd30fb3e9cf8c8c09387b6acd13e95485521a32ae9/psqlpy-0.11.9-cp314-cp314-win_amd64.whl", hash = "sha256:fae6452ad95a529e2d65fbc4d6b205fdf4daad48b578016f1016a832244a790a", size = 4227714, upload-time = "2025-11-15T18:37:45.34Z" }, + { url = "https://files.pythonhosted.org/packages/9c/0c/65b7417f2aaff35f21b762cc76e7e42b6d9ea8e01818a65229a8aef58453/psqlpy-0.11.9-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:fa11c8f35bb9c9636c405ae0c490eaab6486fb5167d292900649c621b87aae28", size = 4369239, upload-time = "2025-11-15T18:38:04.833Z" }, + { url = "https://files.pythonhosted.org/packages/0a/cd/408c0ea6d9d04131278400cb8f8639e66dcb72ec0e647a4dd963de2cb420/psqlpy-0.11.9-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:49fdb7a62459d7df4a8d202c84fc7a7922176c1fa87be0c622a8125bde9adfcd", size = 4615740, upload-time = "2025-11-15T18:38:06.738Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fd/9db77d856fc4668e8f29fb27c62c11bffc5d2ef93a01b5c503d6b2fd694d/psqlpy-0.11.9-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2c712a78f3fae5cedf845fced3784c65fe033c2e8baf0fa33d72ad860fa4878", size = 5142776, upload-time = "2025-11-15T18:38:08.413Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0e/701bf9353c3a2bd287eb3c7b6187fdeed1774203dc02045c164a3e23edeb/psqlpy-0.11.9-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a43b9a8e295527b8a0c393b70f03520e3ff54753385cfe2435e444f7f405b43", size = 4415231, upload-time = "2025-11-15T18:38:09.952Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f3/a3f6d7009afa7678eb06b78efafc87c4944030970bf1f3326d5aaab8543a/psqlpy-0.11.9-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6737ee0c4cdafac7ec5b98283e9bf97545a7e3080c70c2eb962caca3ca5215c9", size = 5047864, upload-time = "2025-11-15T18:38:11.78Z" }, + { url = "https://files.pythonhosted.org/packages/cb/01/0df380180f74be4d3e2999397cdfc21a9bbaac23a5cf0d4949d718534f87/psqlpy-0.11.9-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:08b59f56e18629755d35236c5c4f41f92cab3500bfb4266ff0c133796d8af32f", size = 5066473, upload-time = "2025-11-15T18:38:13.136Z" }, + { url = "https://files.pythonhosted.org/packages/e4/31/d6577324ddd295151c873974a7e16ef4da020a3344024b0f449a37031a80/psqlpy-0.11.9-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3ac25ced1d054c5d1a8b19891a34fc72043d230b607d541e19aecf573b51489", size = 4753922, upload-time = "2025-11-15T18:38:14.499Z" }, + { url = "https://files.pythonhosted.org/packages/55/05/cdb89d13aaeda09d6f040bbb6e39145cbfe9c6f3b66d7c7237ca7d998b46/psqlpy-0.11.9-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:835725b1074c946543d885fbc11e6167a3f8f71d042bcfd024f27cbf94cef782", size = 4897679, upload-time = "2025-11-15T18:38:16.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/39/33873c286fb00f95bc2128bfcbb57a92898db94f48c6c5d5f87789ceef9f/psqlpy-0.11.9-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:6ef192b897c914825e71717f21fea43721fb33b5860013d2e11aabd488d21c21", size = 5073763, upload-time = "2025-11-15T18:38:17.626Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bb/0864de210d6b6523c72321903de78b6debda97c4e2b662bd77443d6e70b4/psqlpy-0.11.9-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:e569647c93e0d2d9a10e1378fba615608545a10aca686a5094073c4fadbfea43", size = 5167787, upload-time = "2025-11-15T18:38:19.012Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2d/cc297d84ddfaeff8cfcee0e9e455934c9cf213a5f17b47c03fdbe480ca7e/psqlpy-0.11.9-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:352d4c26d42d368349fb045dc0e44023bb8fbd7f9d26b54c72ce4c58dcc89bdf", size = 4370154, upload-time = "2025-11-15T18:38:20.441Z" }, + { url = "https://files.pythonhosted.org/packages/ca/70/6a6d7e2f634f423a96fcc2825b359be11fb87b49a46cee9af818236cda58/psqlpy-0.11.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b89ad3a57216abfc29a535920320a41c261e6ea6b924e89a22d685c142f489fe", size = 4616665, upload-time = "2025-11-15T18:38:21.818Z" }, + { url = "https://files.pythonhosted.org/packages/8b/1a/db0cfb6437dd84a6cf15c0c309574875de6849b8989a25652262be091794/psqlpy-0.11.9-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac37826260ffec640fba421ac165ae8a205d5c7098ef7d04c05ba6c091fc2405", size = 5141684, upload-time = "2025-11-15T18:38:23.188Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e1/b5ceedbdd184095aaa97b54c38ee72650c2c4989650539a0dd94ef2d0e11/psqlpy-0.11.9-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:738bb19a869c2894ff99458bf1dab25736f9058228a6ce022f15a1b07cc34468", size = 4417003, upload-time = "2025-11-15T18:38:25.005Z" }, + { url = "https://files.pythonhosted.org/packages/5e/8a/57b3e23a14c6db8a7b7a9f7d404df1bccb4c8d22a4623f5bead203f5fe95/psqlpy-0.11.9-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c9c014dfbe4b2ffb19d09f7a292570afc6b883fc62807bed6cf987fc66fbb79", size = 5048952, upload-time = "2025-11-15T18:38:26.394Z" }, + { url = "https://files.pythonhosted.org/packages/24/b9/7a93230fa6e87c8206ee1ff02aa788446876da47c9d40ccaf9a7ad3e1401/psqlpy-0.11.9-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcd7db5015ec47abc07c59404c1fcb92d93db9612f10d0303cc44001df4c555d", size = 5067410, upload-time = "2025-11-15T18:38:27.859Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e2/fbf97c6ff4c7f56f6e86b64c816a41b1504af52ad04a5fd5b50d7cb8bcc9/psqlpy-0.11.9-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ecfa440cb42f174b1633d6f2cc1abc9c6953b16980fab0cd6abc66c9121e987b", size = 4752954, upload-time = "2025-11-15T18:38:29.501Z" }, + { url = "https://files.pythonhosted.org/packages/6c/0f/a1a486730b84379c4c3b9c42e3c26bed2c0d53ad98d9848eaa71e0931852/psqlpy-0.11.9-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a13e5ef0f89bfdec8fbd78444988dcc9a001ac798cbfc21f37810903dc6f4ede", size = 4898923, upload-time = "2025-11-15T18:38:30.9Z" }, + { url = "https://files.pythonhosted.org/packages/85/27/8c43e8f2e503ae42f1e12e9d94e3eed2e96088bce0dddc6fac32a5822622/psqlpy-0.11.9-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:7de27ea3cc4fa1e3a751466374ff0bbfe40fa30cb83fd37f30c5b0197f7b43e3", size = 5074736, upload-time = "2025-11-15T18:38:32.59Z" }, + { url = "https://files.pythonhosted.org/packages/04/5b/dab8c5b50f5ba5f15529b6c8feeb166918506435e5265baa598606ecd4be/psqlpy-0.11.9-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:fd1275166eebbd00010d380a7a8b72b96558bbdaecd55d70e2b6a9bd0b78cfe2", size = 5168560, upload-time = "2025-11-15T18:38:34.084Z" }, ] [[package]] @@ -5151,6 +5151,8 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6b/fa/3234f913fe9a6525a7b97c6dad1f51e72b917e6872e051a5e2ffd8b16fbb/ruamel.yaml.clib-0.2.14-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:70eda7703b8126f5e52fcf276e6c0f40b0d314674f896fc58c47b0aef2b9ae83", size = 137970, upload-time = "2025-09-22T19:51:09.472Z" }, { url = "https://files.pythonhosted.org/packages/ef/ec/4edbf17ac2c87fa0845dd366ef8d5852b96eb58fcd65fc1ecf5fe27b4641/ruamel.yaml.clib-0.2.14-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a0cb71ccc6ef9ce36eecb6272c81afdc2f565950cdcec33ae8e6cd8f7fc86f27", size = 739639, upload-time = "2025-09-22T19:51:10.566Z" }, { url = "https://files.pythonhosted.org/packages/15/18/b0e1fafe59051de9e79cdd431863b03593ecfa8341c110affad7c8121efc/ruamel.yaml.clib-0.2.14-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e7cb9ad1d525d40f7d87b6df7c0ff916a66bc52cb61b66ac1b2a16d0c1b07640", size = 764456, upload-time = "2025-09-22T19:51:11.736Z" }, + { url = "https://files.pythonhosted.org/packages/e7/cd/150fdb96b8fab27fe08d8a59fe67554568727981806e6bc2677a16081ec7/ruamel_yaml_clib-0.2.14-cp314-cp314-win32.whl", hash = "sha256:9b4104bf43ca0cd4e6f738cb86326a3b2f6eef00f417bd1e7efb7bdffe74c539", size = 102394, upload-time = "2025-11-14T21:57:36.703Z" }, + { url = "https://files.pythonhosted.org/packages/bd/e6/a3fa40084558c7e1dc9546385f22a93949c890a8b2e445b2ba43935f51da/ruamel_yaml_clib-0.2.14-cp314-cp314-win_amd64.whl", hash = "sha256:13997d7d354a9890ea1ec5937a219817464e5cc344805b37671562a401ca3008", size = 122673, upload-time = "2025-11-14T21:57:38.177Z" }, ] [[package]] @@ -6493,11 +6495,11 @@ wheels = [ [[package]] name = "types-docutils" -version = "0.22.2.20251006" +version = "0.22.3.20251115" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/30/79/3b5419ad9af32d99c1a953f2c96faa396280fddba22201d3788ff5b41b8a/types_docutils-0.22.2.20251006.tar.gz", hash = "sha256:c36c0459106eda39e908e9147bcff9dbd88535975cde399433c428a517b9e3b2", size = 56658, upload-time = "2025-10-06T02:55:19.477Z" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/d7/576ec24bf61a280f571e1f22284793adc321610b9bcfba1bf468cf7b334f/types_docutils-0.22.3.20251115.tar.gz", hash = "sha256:0f79ea6a7bd4d12d56c9f824a0090ffae0ea4204203eb0006392906850913e16", size = 56828, upload-time = "2025-11-15T02:59:57.371Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/47/c1eed8aef21d010e8d726855c1a6346f526c40ce1f76ceabf5cd6775f6a1/types_docutils-0.22.2.20251006-py3-none-any.whl", hash = "sha256:1e61afdeb4fab4ae802034deea3e853ced5c9b5e1d156179000cb68c85daf384", size = 91880, upload-time = "2025-10-06T02:55:18.119Z" }, + { url = "https://files.pythonhosted.org/packages/9c/01/61ac9eb38f1f978b47443dc6fd2e0a3b0f647c2da741ddad30771f1b2b6f/types_docutils-0.22.3.20251115-py3-none-any.whl", hash = "sha256:c6e53715b65395d00a75a3a8a74e352c669bc63959e65a207dffaa22f4a2ad6e", size = 91951, upload-time = "2025-11-15T02:59:56.413Z" }, ] [[package]] @@ -6511,11 +6513,11 @@ wheels = [ [[package]] name = "types-psutil" -version = "7.0.0.20251111" +version = "7.0.0.20251115" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1a/ba/4f48c927f38c7a4d6f7ff65cde91c49d28a95a56e00ec19b2813e1e0b1c1/types_psutil-7.0.0.20251111.tar.gz", hash = "sha256:d109ee2da4c0a9b69b8cefc46e195db8cf0fc0200b6641480df71e7f3f51a239", size = 20287, upload-time = "2025-11-11T03:06:37.482Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/e0/c29fd96ced361ffb3cd891a56a97eae2f236ce1f47b5fbeba48a267984e0/types_psutil-7.0.0.20251115.tar.gz", hash = "sha256:67db0fbe0f2ed540f9a7d419273086c7d65cbaed8ccc32c8197d12963eedde72", size = 21485, upload-time = "2025-11-15T03:00:09.583Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/bc/b081d10fbd933cdf839109707a693c668a174e2276d64159a582a9cebd3f/types_psutil-7.0.0.20251111-py3-none-any.whl", hash = "sha256:85ba00205dcfa3c73685122e5a360205d2fbc9b56f942b591027bf401ce0cc47", size = 23052, upload-time = "2025-11-11T03:06:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/ee/fe/4f8e8eec1965bfc5c6c772e77308591e8247c96bbd522effb6bd5d92a159/types_psutil-7.0.0.20251115-py3-none-any.whl", hash = "sha256:7ddc79d1f0cb672b0b5c43473a18ebc5d76b806c7080934fd6005b8550a6006c", size = 24173, upload-time = "2025-11-15T03:00:08.298Z" }, ] [[package]]