diff --git a/pyproject.toml b/pyproject.toml index 5a5563c1..88a1b9d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -91,6 +91,7 @@ extras = [ "adbc_driver_postgresql", "adbc_driver_flightsql", "adbc_driver_bigquery", + "dishka ; python_version >= \"3.10\"", ] lint = [ "mypy>=1.13.0", @@ -162,11 +163,11 @@ include = [ "sqlspec/loader.py", # Loader module # === ADAPTER TYPE CONVERTERS === - "sqlspec/adapters/adbc/type_converter.py", # ADBC type converter - "sqlspec/adapters/bigquery/type_converter.py", # BigQuery type converter - "sqlspec/adapters/duckdb/type_converter.py", # DuckDB type converter - "sqlspec/adapters/oracledb/type_converter.py", # Oracle type converter - "sqlspec/adapters/psqlpy/type_converter.py", # Psqlpy type converter + "sqlspec/adapters/adbc/type_converter.py", # ADBC type converter + "sqlspec/adapters/bigquery/type_converter.py", # BigQuery type converter + "sqlspec/adapters/duckdb/type_converter.py", # DuckDB type converter + "sqlspec/adapters/oracledb/type_converter.py", # Oracle type converter + "sqlspec/adapters/psqlpy/type_converter.py", # Psqlpy type converter # === UTILITY MODULES === "sqlspec/utils/text.py", # Text utilities diff --git a/sqlspec/adapters/adbc/config.py b/sqlspec/adapters/adbc/config.py index 711efc77..0e80a332 100644 --- a/sqlspec/adapters/adbc/config.py +++ b/sqlspec/adapters/adbc/config.py @@ -77,6 +77,7 @@ def __init__( migration_config: Optional[dict[str, Any]] = None, statement_config: Optional[StatementConfig] = None, driver_features: Optional[dict[str, Any]] = None, + bind_key: Optional[str] = None, ) -> None: """Initialize configuration. @@ -85,6 +86,7 @@ def __init__( migration_config: Migration configuration statement_config: Default SQL statement configuration driver_features: Driver feature configuration + bind_key: Optional unique identifier for this configuration """ if connection_config is None: connection_config = {} @@ -104,6 +106,7 @@ def __init__( migration_config=migration_config, statement_config=statement_config, driver_features=driver_features or {}, + bind_key=bind_key, ) def _resolve_driver_name(self) -> str: diff --git a/sqlspec/adapters/aiosqlite/config.py b/sqlspec/adapters/aiosqlite/config.py index 2aa9639f..86765fc0 100644 --- a/sqlspec/adapters/aiosqlite/config.py +++ b/sqlspec/adapters/aiosqlite/config.py @@ -62,6 +62,7 @@ def __init__( migration_config: "Optional[dict[str, Any]]" = None, statement_config: "Optional[StatementConfig]" = None, driver_features: "Optional[dict[str, Any]]" = None, + bind_key: "Optional[str]" = None, ) -> None: """Initialize AioSQLite configuration. @@ -71,6 +72,7 @@ def __init__( migration_config: Optional migration configuration. statement_config: Optional statement configuration. driver_features: Optional driver feature configuration. + bind_key: Optional unique identifier for this configuration. """ config_dict = dict(pool_config) if pool_config else {} @@ -84,6 +86,7 @@ def __init__( migration_config=migration_config, statement_config=statement_config or aiosqlite_statement_config, driver_features=driver_features or {}, + bind_key=bind_key, ) def _get_pool_config_dict(self) -> "dict[str, Any]": diff --git a/sqlspec/adapters/asyncmy/config.py b/sqlspec/adapters/asyncmy/config.py index f49d37b6..fffd2ecc 100644 --- a/sqlspec/adapters/asyncmy/config.py +++ b/sqlspec/adapters/asyncmy/config.py @@ -71,6 +71,7 @@ def __init__( migration_config: Optional[dict[str, Any]] = None, statement_config: "Optional[StatementConfig]" = None, driver_features: "Optional[dict[str, Any]]" = None, + bind_key: "Optional[str]" = None, ) -> None: """Initialize Asyncmy configuration. @@ -80,6 +81,7 @@ def __init__( migration_config: Migration configuration statement_config: Statement configuration override driver_features: Driver feature configuration + bind_key: Optional unique identifier for this configuration """ processed_pool_config: dict[str, Any] = dict(pool_config) if pool_config else {} if "extra" in processed_pool_config: @@ -100,6 +102,7 @@ def __init__( migration_config=migration_config, statement_config=statement_config, driver_features=driver_features or {}, + bind_key=bind_key, ) async def _create_pool(self) -> "AsyncmyPool": # pyright: ignore diff --git a/sqlspec/adapters/asyncmy/driver.py b/sqlspec/adapters/asyncmy/driver.py index 6a7a2a8b..25ce928c 100644 --- a/sqlspec/adapters/asyncmy/driver.py +++ b/sqlspec/adapters/asyncmy/driver.py @@ -8,8 +8,8 @@ from typing import TYPE_CHECKING, Any, Optional, Union import asyncmy -import asyncmy.errors -from asyncmy.cursors import Cursor, DictCursor +import asyncmy.errors # pyright: ignore +from asyncmy.cursors import Cursor, DictCursor # pyright: ignore from sqlspec.core.cache import get_cache_config from sqlspec.core.parameters import ParameterStyle, ParameterStyleConfig diff --git a/sqlspec/adapters/asyncpg/config.py b/sqlspec/adapters/asyncpg/config.py index 2a51bb1b..a2bfe7b9 100644 --- a/sqlspec/adapters/asyncpg/config.py +++ b/sqlspec/adapters/asyncpg/config.py @@ -84,6 +84,7 @@ def __init__( migration_config: "Optional[dict[str, Any]]" = None, statement_config: "Optional[StatementConfig]" = None, driver_features: "Optional[Union[AsyncpgDriverFeatures, dict[str, Any]]]" = None, + bind_key: "Optional[str]" = None, ) -> None: """Initialize AsyncPG configuration. @@ -93,6 +94,7 @@ def __init__( migration_config: Migration configuration statement_config: Statement configuration override driver_features: Driver features configuration (TypedDict or dict) + bind_key: Optional unique identifier for this configuration """ features_dict: dict[str, Any] = dict(driver_features) if driver_features else {} @@ -106,6 +108,7 @@ def __init__( migration_config=migration_config, statement_config=statement_config or asyncpg_statement_config, driver_features=features_dict, + bind_key=bind_key, ) def _get_pool_config_dict(self) -> "dict[str, Any]": diff --git a/sqlspec/adapters/bigquery/config.py b/sqlspec/adapters/bigquery/config.py index aede4829..1dd32121 100644 --- a/sqlspec/adapters/bigquery/config.py +++ b/sqlspec/adapters/bigquery/config.py @@ -94,6 +94,7 @@ def __init__( migration_config: Optional[dict[str, Any]] = None, statement_config: "Optional[StatementConfig]" = None, driver_features: "Optional[Union[BigQueryDriverFeatures, dict[str, Any]]]" = None, + bind_key: "Optional[str]" = None, ) -> None: """Initialize BigQuery configuration. @@ -102,6 +103,7 @@ def __init__( migration_config: Migration configuration statement_config: Statement configuration override driver_features: BigQuery-specific driver features + bind_key: Optional unique identifier for this configuration """ self.connection_config: dict[str, Any] = dict(connection_config) if connection_config else {} @@ -124,6 +126,7 @@ def __init__( migration_config=migration_config, statement_config=statement_config, driver_features=self.driver_features, + bind_key=bind_key, ) def _setup_default_job_config(self) -> None: diff --git a/sqlspec/adapters/duckdb/config.py b/sqlspec/adapters/duckdb/config.py index 4613d674..ca53b646 100644 --- a/sqlspec/adapters/duckdb/config.py +++ b/sqlspec/adapters/duckdb/config.py @@ -149,6 +149,7 @@ def __init__( migration_config: Optional[dict[str, Any]] = None, statement_config: "Optional[StatementConfig]" = None, driver_features: "Optional[Union[DuckDBDriverFeatures, dict[str, Any]]]" = None, + bind_key: "Optional[str]" = None, ) -> None: """Initialize DuckDB configuration.""" if pool_config is None: @@ -160,6 +161,7 @@ def __init__( pool_config["database"] = ":memory:shared_db" super().__init__( + bind_key=bind_key, pool_config=dict(pool_config), pool_instance=pool_instance, migration_config=migration_config, diff --git a/sqlspec/adapters/oracledb/config.py b/sqlspec/adapters/oracledb/config.py index 5a65ccc8..59d67654 100644 --- a/sqlspec/adapters/oracledb/config.py +++ b/sqlspec/adapters/oracledb/config.py @@ -94,6 +94,7 @@ def __init__( migration_config: Optional[dict[str, Any]] = None, statement_config: "Optional[StatementConfig]" = None, driver_features: "Optional[dict[str, Any]]" = None, + bind_key: "Optional[str]" = None, ) -> None: """Initialize Oracle synchronous configuration. @@ -103,6 +104,7 @@ def __init__( migration_config: Migration configuration statement_config: Default SQL statement configuration driver_features: Optional driver feature configuration + bind_key: Optional unique identifier for this configuration """ processed_pool_config: dict[str, Any] = dict(pool_config) if pool_config else {} @@ -116,6 +118,7 @@ def __init__( migration_config=migration_config, statement_config=statement_config, driver_features=driver_features or {}, + bind_key=bind_key, ) def _create_pool(self) -> "OracleSyncConnectionPool": @@ -220,6 +223,7 @@ def __init__( migration_config: Optional[dict[str, Any]] = None, statement_config: "Optional[StatementConfig]" = None, driver_features: "Optional[dict[str, Any]]" = None, + bind_key: "Optional[str]" = None, ) -> None: """Initialize Oracle asynchronous configuration. @@ -229,6 +233,7 @@ def __init__( migration_config: Migration configuration statement_config: Default SQL statement configuration driver_features: Optional driver feature configuration + bind_key: Optional unique identifier for this configuration """ processed_pool_config: dict[str, Any] = dict(pool_config) if pool_config else {} @@ -242,6 +247,7 @@ def __init__( migration_config=migration_config, statement_config=statement_config or oracledb_statement_config, driver_features=driver_features or {}, + bind_key=bind_key, ) async def _create_pool(self) -> "OracleAsyncConnectionPool": diff --git a/sqlspec/adapters/psqlpy/config.py b/sqlspec/adapters/psqlpy/config.py index b8820783..1ef61ff2 100644 --- a/sqlspec/adapters/psqlpy/config.py +++ b/sqlspec/adapters/psqlpy/config.py @@ -90,6 +90,7 @@ def __init__( migration_config: Optional[dict[str, Any]] = None, statement_config: Optional[StatementConfig] = None, driver_features: Optional[dict[str, Any]] = None, + bind_key: Optional[str] = None, ) -> None: """Initialize Psqlpy configuration. @@ -99,6 +100,7 @@ def __init__( migration_config: Migration configuration statement_config: SQL statement configuration driver_features: Driver feature configuration + bind_key: Optional unique identifier for this configuration """ processed_pool_config: dict[str, Any] = dict(pool_config) if pool_config else {} if "extra" in processed_pool_config: @@ -110,6 +112,7 @@ def __init__( migration_config=migration_config, statement_config=statement_config or psqlpy_statement_config, driver_features=driver_features or {}, + bind_key=bind_key, ) def _get_pool_config_dict(self) -> dict[str, Any]: diff --git a/sqlspec/adapters/psycopg/config.py b/sqlspec/adapters/psycopg/config.py index 80f4509d..56b920d1 100644 --- a/sqlspec/adapters/psycopg/config.py +++ b/sqlspec/adapters/psycopg/config.py @@ -88,6 +88,7 @@ def __init__( migration_config: Optional[dict[str, Any]] = None, statement_config: "Optional[StatementConfig]" = None, driver_features: "Optional[dict[str, Any]]" = None, + bind_key: "Optional[str]" = None, ) -> None: """Initialize Psycopg synchronous configuration. @@ -97,6 +98,7 @@ def __init__( migration_config: Migration configuration statement_config: Default SQL statement configuration driver_features: Optional driver feature configuration + bind_key: Optional unique identifier for this configuration """ processed_pool_config: dict[str, Any] = dict(pool_config) if pool_config else {} if "extra" in processed_pool_config: @@ -109,6 +111,7 @@ def __init__( migration_config=migration_config, statement_config=statement_config or psycopg_statement_config, driver_features=driver_features or {}, + bind_key=bind_key, ) def _create_pool(self) -> "ConnectionPool": @@ -270,6 +273,7 @@ def __init__( migration_config: "Optional[dict[str, Any]]" = None, statement_config: "Optional[StatementConfig]" = None, driver_features: "Optional[dict[str, Any]]" = None, + bind_key: "Optional[str]" = None, ) -> None: """Initialize Psycopg asynchronous configuration. @@ -279,6 +283,7 @@ def __init__( migration_config: Migration configuration statement_config: Default SQL statement configuration driver_features: Optional driver feature configuration + bind_key: Optional unique identifier for this configuration """ processed_pool_config: dict[str, Any] = dict(pool_config) if pool_config else {} if "extra" in processed_pool_config: @@ -291,6 +296,7 @@ def __init__( migration_config=migration_config, statement_config=statement_config or psycopg_statement_config, driver_features=driver_features or {}, + bind_key=bind_key, ) async def _create_pool(self) -> "AsyncConnectionPool": diff --git a/sqlspec/adapters/sqlite/config.py b/sqlspec/adapters/sqlite/config.py index 1b11318f..8338437b 100644 --- a/sqlspec/adapters/sqlite/config.py +++ b/sqlspec/adapters/sqlite/config.py @@ -47,6 +47,7 @@ def __init__( migration_config: "Optional[dict[str, Any]]" = None, statement_config: "Optional[StatementConfig]" = None, driver_features: "Optional[dict[str, Any]]" = None, + bind_key: "Optional[str]" = None, ) -> None: """Initialize SQLite configuration. @@ -56,6 +57,7 @@ def __init__( migration_config: Migration configuration statement_config: Default SQL statement configuration driver_features: Optional driver feature configuration + bind_key: Optional bind key for the configuration """ if pool_config is None: pool_config = {} @@ -64,6 +66,7 @@ def __init__( pool_config["uri"] = True super().__init__( + bind_key=bind_key, pool_instance=pool_instance, pool_config=cast("dict[str, Any]", pool_config), migration_config=migration_config, diff --git a/sqlspec/cli.py b/sqlspec/cli.py index db59610c..aadd6e87 100644 --- a/sqlspec/cli.py +++ b/sqlspec/cli.py @@ -2,6 +2,7 @@ import inspect import sys from collections.abc import Sequence +from pathlib import Path from typing import TYPE_CHECKING, Any, Optional, Union, cast if TYPE_CHECKING: @@ -51,6 +52,14 @@ def sqlspec_group(ctx: "click.Context", config: str, validate_config: bool) -> N console = get_console() ctx.ensure_object(dict) + + # Add current working directory to sys.path to allow loading local config modules + cwd = str(Path.cwd()) + cwd_added = False + if cwd not in sys.path: + sys.path.insert(0, cwd) + cwd_added = True + try: config_result = resolve_config_sync(config) if isinstance(config_result, Sequence) and not isinstance(config_result, str): @@ -63,17 +72,19 @@ def sqlspec_group(ctx: "click.Context", config: str, validate_config: bool) -> N if validate_config: console.print(f"[green]✓[/] Successfully loaded {len(ctx.obj['configs'])} config(s)") for i, cfg in enumerate(ctx.obj["configs"]): - config_name = getattr(cfg, "name", None) or getattr(cfg, "bind_key", None) or f"config-{i}" + config_name = cfg.bind_key or f"config-{i}" config_type = type(cfg).__name__ - is_async = "Async" in config_type or ( - hasattr(cfg, "driver_type") and "Async" in getattr(cfg.driver_type, "__name__", "") - ) + is_async = cfg.is_async execution_hint = "[dim cyan](async-capable)[/]" if is_async else "[dim](sync)[/]" console.print(f" [dim]•[/] {config_name}: {config_type} {execution_hint}") except (ImportError, ConfigResolverError) as e: console.print(f"[red]Error loading config: {e}[/]") ctx.exit(1) + finally: + # Clean up: remove the cwd from sys.path if we added it + if cwd_added and cwd in sys.path and sys.path[0] == cwd: + sys.path.remove(cwd) return sqlspec_group @@ -153,7 +164,7 @@ def get_config_by_bind_key( else: config = None for cfg in configs: - config_name = getattr(cfg, "name", None) or getattr(cfg, "bind_key", None) + config_name = cfg.bind_key if config_name == bind_key: config = cfg break @@ -189,15 +200,11 @@ def get_configs_with_migrations(ctx: "click.Context", enabled_only: bool = False # Extract the actual config from DatabaseConfig wrapper if needed actual_config = config.config if isinstance(config, DatabaseConfig) else config - migration_config = getattr(actual_config, "migration_config", None) + migration_config = actual_config.migration_config if migration_config: enabled = migration_config.get("enabled", True) if not enabled_only or enabled: - config_name = ( - getattr(actual_config, "name", None) - or getattr(actual_config, "bind_key", None) - or str(type(actual_config).__name__) - ) + config_name = actual_config.bind_key or str(type(actual_config).__name__) migration_configs.append((config_name, actual_config)) return migration_configs @@ -492,6 +499,7 @@ def init_sqlspec( # pyright: ignore[reportUnusedFunction] """Initialize the database migrations.""" from rich.prompt import Confirm + from sqlspec.extensions.litestar.config import DatabaseConfig from sqlspec.migrations.commands import create_migration_commands from sqlspec.utils.sync_tools import run_ @@ -510,7 +518,6 @@ async def _init_sqlspec() -> None: if bind_key is not None else cast("click.Context", ctx).obj["configs"] ) - from sqlspec.extensions.litestar.config import DatabaseConfig for config in configs: # Extract the actual config from DatabaseConfig wrapper if needed @@ -554,12 +561,25 @@ async def _create_revision() -> None: run_(_create_revision)() @database_group.command(name="show-config", help="Show all configurations with migrations enabled.") - def show_config() -> None: # pyright: ignore[reportUnusedFunction] + @bind_key_option + def show_config(bind_key: Optional[str] = None) -> None: # pyright: ignore[reportUnusedFunction] """Show and display all configurations with migrations enabled.""" from rich.table import Table ctx = click.get_current_context() - migration_configs = get_configs_with_migrations(cast("click.Context", ctx)) + + # 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) + # Convert single config to list format for compatibility + all_configs = cast("click.Context", 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)) if not migration_configs: console.print("[yellow]No configurations with migrations detected.[/]") diff --git a/sqlspec/config.py b/sqlspec/config.py index 5cd4f720..819634a1 100644 --- a/sqlspec/config.py +++ b/sqlspec/config.py @@ -1,4 +1,5 @@ from abc import ABC, abstractmethod +from pathlib import Path from typing import TYPE_CHECKING, Any, Callable, ClassVar, Generic, Optional, TypeVar, Union, cast from typing_extensions import NotRequired, TypedDict @@ -11,7 +12,6 @@ if TYPE_CHECKING: from collections.abc import Awaitable from contextlib import AbstractAsyncContextManager, AbstractContextManager - from pathlib import Path from sqlspec.driver import AsyncDriverAdapterBase, SyncDriverAdapterBase from sqlspec.loader import SQLFileLoader @@ -89,6 +89,7 @@ class DatabaseConfigProtocol(ABC, Generic[ConnectionT, PoolT, DriverT]): __slots__ = ( "_migration_commands", "_migration_loader", + "bind_key", "driver_features", "migration_config", "pool_instance", @@ -105,6 +106,7 @@ class DatabaseConfigProtocol(ABC, Generic[ConnectionT, PoolT, DriverT]): supports_native_arrow_export: "ClassVar[bool]" = False supports_native_parquet_import: "ClassVar[bool]" = False supports_native_parquet_export: "ClassVar[bool]" = False + bind_key: "Optional[str]" statement_config: "StatementConfig" pool_instance: "Optional[PoolT]" migration_config: "Union[dict[str, Any], MigrationConfig]" @@ -225,7 +227,6 @@ def load_migration_sql_files(self, *paths: "Union[str, Path]") -> None: Args: *paths: One or more file paths or directory paths to load migration SQL files from. """ - from pathlib import Path loader = self._ensure_migration_loader() for path in paths: @@ -320,7 +321,9 @@ def __init__( migration_config: "Optional[Union[dict[str, Any], MigrationConfig]]" = None, statement_config: "Optional[StatementConfig]" = None, driver_features: "Optional[dict[str, Any]]" = None, + bind_key: "Optional[str]" = None, ) -> None: + self.bind_key = bind_key self.pool_instance = None self.connection_config = connection_config or {} self.migration_config: Union[dict[str, Any], MigrationConfig] = migration_config or {} @@ -374,7 +377,9 @@ def __init__( migration_config: "Optional[Union[dict[str, Any], MigrationConfig]]" = None, statement_config: "Optional[StatementConfig]" = None, driver_features: "Optional[dict[str, Any]]" = None, + bind_key: "Optional[str]" = None, ) -> None: + self.bind_key = bind_key self.pool_instance = None self.connection_config = connection_config or {} self.migration_config: Union[dict[str, Any], MigrationConfig] = migration_config or {} @@ -429,7 +434,9 @@ def __init__( migration_config: "Optional[Union[dict[str, Any], MigrationConfig]]" = None, statement_config: "Optional[StatementConfig]" = None, driver_features: "Optional[dict[str, Any]]" = None, + bind_key: "Optional[str]" = None, ) -> None: + self.bind_key = bind_key self.pool_instance = pool_instance self.pool_config = pool_config or {} self.migration_config: Union[dict[str, Any], MigrationConfig] = migration_config or {} @@ -506,7 +513,9 @@ def __init__( migration_config: "Optional[Union[dict[str, Any], MigrationConfig]]" = None, statement_config: "Optional[StatementConfig]" = None, driver_features: "Optional[dict[str, Any]]" = None, + bind_key: "Optional[str]" = None, ) -> None: + self.bind_key = bind_key self.pool_instance = pool_instance self.pool_config = pool_config or {} self.migration_config: Union[dict[str, Any], MigrationConfig] = migration_config or {} diff --git a/sqlspec/utils/config_resolver.py b/sqlspec/utils/config_resolver.py index 6a887d62..8eb22741 100644 --- a/sqlspec/utils/config_resolver.py +++ b/sqlspec/utils/config_resolver.py @@ -77,7 +77,10 @@ def resolve_config_sync( return _validate_config_result(config_obj, config_path) try: - result = await_(config_obj)() if inspect.iscoroutinefunction(config_obj) else config_obj() + if inspect.iscoroutinefunction(config_obj): + result = await_(config_obj, raise_sync_error=False)() + else: + result = config_obj() except Exception as e: msg = f"Failed to execute callable config '{config_path}': {e}" raise ConfigResolverError(msg) from e @@ -132,4 +135,19 @@ def _is_valid_config(config: Any) -> bool: Returns: True if object appears to be a valid config. """ - return hasattr(config, "database_url") and hasattr(config, "bind_key") and hasattr(config, "migration_config") + # Check for litestar extension DatabaseConfig wrapper + nested_config = getattr(config, "config", None) + if nested_config is not None and hasattr(nested_config, "migration_config"): + return True + + # Check for direct database config with migration support + migration_config = getattr(config, "migration_config", None) + if migration_config is not None: + # Modern SQLSpec config with pool_config + if hasattr(config, "pool_config"): + return True + # Legacy config with database_url and bind_key + if hasattr(config, "database_url") and hasattr(config, "bind_key"): + return True + + return False diff --git a/tests/integration/test_dishka/__init__.py b/tests/integration/test_dishka/__init__.py new file mode 100644 index 00000000..bdb88d77 --- /dev/null +++ b/tests/integration/test_dishka/__init__.py @@ -0,0 +1 @@ +"""Dishka integration tests.""" diff --git a/tests/integration/test_dishka/conftest.py b/tests/integration/test_dishka/conftest.py new file mode 100644 index 00000000..80ede1ed --- /dev/null +++ b/tests/integration/test_dishka/conftest.py @@ -0,0 +1,113 @@ +"""Dishka integration test fixtures and configuration.""" + +from typing import TYPE_CHECKING + +import pytest + +dishka = pytest.importorskip("dishka") + +if TYPE_CHECKING: + from dishka import Provider # type: ignore[import-not-found] + + +@pytest.fixture +def simple_sqlite_provider() -> "Provider": + """Create a simple Dishka provider that provides an SQLite config.""" + from dishka import Provider, Scope, provide # pyright: ignore[reportMissingImports] + + from sqlspec.adapters.sqlite.config import SqliteConfig + + class DatabaseProvider(Provider): # type: ignore[misc] + @provide(scope=Scope.APP) # type: ignore[misc] + def get_database_config(self) -> SqliteConfig: + config = SqliteConfig(pool_config={"database": ":memory:"}) + config.bind_key = "dishka_sqlite" + return config + + return DatabaseProvider() + + +@pytest.fixture +def async_sqlite_provider() -> "Provider": + """Create an async Dishka provider that provides an SQLite config.""" + import asyncio + + from dishka import Provider, Scope, provide # pyright: ignore[reportMissingImports] + + from sqlspec.adapters.sqlite.config import SqliteConfig + + class AsyncDatabaseProvider(Provider): # type: ignore[misc] + @provide(scope=Scope.APP) # type: ignore[misc] + async def get_database_config(self) -> SqliteConfig: + # Simulate some async work (e.g., fetching config from remote service) + await asyncio.sleep(0.001) + config = SqliteConfig(pool_config={"database": ":memory:"}) + config.bind_key = "async_dishka_sqlite" + return config + + return AsyncDatabaseProvider() + + +@pytest.fixture +def multi_config_provider() -> "Provider": + """Create a Dishka provider that provides multiple database configs.""" + from dishka import Provider, Scope, provide # pyright: ignore[reportMissingImports] + + from sqlspec.adapters.duckdb.config import DuckDBConfig + from sqlspec.adapters.sqlite.config import SqliteConfig + + class MultiDatabaseProvider(Provider): # type: ignore[misc] + @provide(scope=Scope.APP) # type: ignore[misc] + def get_sqlite_config(self) -> SqliteConfig: + config = SqliteConfig(pool_config={"database": ":memory:"}) + config.bind_key = "dishka_multi_sqlite" + config.migration_config = {"enabled": True, "script_location": "sqlite_migrations"} + return config + + @provide(scope=Scope.APP) # type: ignore[misc] + def get_duckdb_config(self) -> DuckDBConfig: + config = DuckDBConfig(pool_config={"database": ":memory:"}) + config.bind_key = "dishka_multi_duckdb" + config.migration_config = {"enabled": True, "script_location": "duckdb_migrations"} + return config + + return MultiDatabaseProvider() + + +@pytest.fixture +def async_multi_config_provider() -> "Provider": + """Create an async Dishka provider that provides multiple database configs.""" + import asyncio + + from dishka import Provider, Scope, provide # pyright: ignore[reportMissingImports] + + from sqlspec.adapters.aiosqlite.config import AiosqliteConfig + from sqlspec.adapters.duckdb.config import DuckDBConfig + from sqlspec.adapters.sqlite.config import SqliteConfig + + class AsyncMultiDatabaseProvider(Provider): # type: ignore[misc] + @provide(scope=Scope.APP) # type: ignore[misc] + async def get_sqlite_config(self) -> SqliteConfig: + await asyncio.sleep(0.001) + config = SqliteConfig(pool_config={"database": ":memory:"}) + config.bind_key = "async_multi_sqlite" + config.migration_config = {"enabled": True} + return config + + @provide(scope=Scope.APP) # type: ignore[misc] + async def get_aiosqlite_config(self) -> AiosqliteConfig: + await asyncio.sleep(0.001) + config = AiosqliteConfig(pool_config={"database": ":memory:"}) + config.bind_key = "async_multi_aiosqlite" + config.migration_config = {"enabled": True} + return config + + @provide(scope=Scope.APP) # type: ignore[misc] + async def get_duckdb_config(self) -> DuckDBConfig: + await asyncio.sleep(0.001) + config = DuckDBConfig(pool_config={"database": ":memory:"}) + config.bind_key = "async_multi_duckdb" + config.migration_config = {"enabled": True} + return config + + return AsyncMultiDatabaseProvider() diff --git a/tests/integration/test_dishka/test_dishka_integration.py b/tests/integration/test_dishka/test_dishka_integration.py new file mode 100644 index 00000000..1996787f --- /dev/null +++ b/tests/integration/test_dishka/test_dishka_integration.py @@ -0,0 +1,536 @@ +"""Integration tests for Dishka DI framework with SQLSpec CLI.""" + +import os +import tempfile +from pathlib import Path +from typing import Any + +import pytest +from click.testing import CliRunner + +from sqlspec.cli import add_migration_commands + +dishka = pytest.importorskip("dishka") + + +def test_simple_sync_dishka_provider(simple_sqlite_provider: Any) -> None: + """Test CLI with a simple synchronous Dishka provider.""" + runner = CliRunner() + + with tempfile.TemporaryDirectory() as temp_dir: + original_dir = os.getcwd() + os.chdir(temp_dir) + try: + # Create a module that uses Dishka container + config_module = ''' +from dishka import make_container +from tests.integration.test_dishka.conftest import simple_sqlite_provider + +def get_config_from_dishka(): + """Get config from Dishka container synchronously.""" + from sqlspec.adapters.sqlite.config import SqliteConfig + + # Create the provider directly (simulating the fixture) + from dishka import Provider, provide, Scope + + class DatabaseProvider(Provider): + @provide(scope=Scope.APP) + def get_database_config(self) -> SqliteConfig: + return SqliteConfig( + pool_config={"database": ":memory:"}, + migration_config={"enabled": True, "script_location": "migrations"}, + bind_key="dishka_sqlite" + ) + + container = make_container(DatabaseProvider()) + with container() as request_container: + return request_container.get(SqliteConfig) +''' + Path("dishka_config.py").write_text(config_module) + + result = runner.invoke( + add_migration_commands(), ["--config", "dishka_config.get_config_from_dishka", "show-config"] + ) + + finally: + os.chdir(original_dir) + + assert result.exit_code == 0 + assert "dishka_sqlite" in result.output + assert "Migration Enabled" in result.output or "migrations enabled" in result.output + + +def test_async_dishka_provider() -> None: + """Test CLI with an asynchronous Dishka provider.""" + runner = CliRunner() + + with tempfile.TemporaryDirectory() as temp_dir: + original_dir = os.getcwd() + os.chdir(temp_dir) + try: + config_module = ''' +import asyncio +from dishka import make_async_container, Provider, provide, Scope +from sqlspec.adapters.sqlite.config import SqliteConfig + +class AsyncDatabaseProvider(Provider): + @provide(scope=Scope.APP) + async def get_database_config(self) -> SqliteConfig: + # Simulate some async work + await asyncio.sleep(0.001) + return SqliteConfig( + pool_config={"database": ":memory:"}, + migration_config={"enabled": True, "script_location": "migrations"}, + bind_key="async_dishka_sqlite" + ) + +async def get_async_config_from_dishka(): + """Get config from async Dishka container.""" + container = make_async_container(AsyncDatabaseProvider()) + async with container() as request_container: + return await request_container.get(SqliteConfig) +''' + Path("async_dishka_config.py").write_text(config_module) + + result = runner.invoke( + add_migration_commands(), + ["--config", "async_dishka_config.get_async_config_from_dishka", "show-config"], + ) + + finally: + os.chdir(original_dir) + + assert result.exit_code == 0 + assert "async_dishka_sqlite" in result.output + assert "Migration Enabled" in result.output or "migrations enabled" in result.output + + +def test_multi_config_dishka_provider() -> None: + """Test CLI with Dishka provider returning multiple configs.""" + runner = CliRunner() + + with tempfile.TemporaryDirectory() as temp_dir: + original_dir = os.getcwd() + os.chdir(temp_dir) + try: + config_module = ''' +from dishka import make_container, Provider, provide, Scope +from sqlspec.adapters.sqlite import SqliteConfig +from sqlspec.adapters.duckdb import DuckDBConfig + +class MultiDatabaseProvider(Provider): + @provide(scope=Scope.APP) + def get_sqlite_config(self) -> SqliteConfig: + return SqliteConfig( + pool_config={"database": ":memory:"}, + migration_config={"enabled": True, "script_location": "sqlite_migrations"}, + bind_key="dishka_multi_sqlite" + ) + + @provide(scope=Scope.APP) + def get_duckdb_config(self) -> DuckDBConfig: + return DuckDBConfig( + pool_config={"database": ":memory:"}, + migration_config={"enabled": True, "script_location": "duckdb_migrations"}, + bind_key="dishka_multi_duckdb" + ) + +def get_multi_configs_from_dishka(): + """Get multiple configs from Dishka container.""" + container = make_container(MultiDatabaseProvider()) + with container() as request_container: + sqlite_config = request_container.get(SqliteConfig) + duckdb_config = request_container.get(DuckDBConfig) + return [sqlite_config, duckdb_config] +''' + Path("multi_dishka_config.py").write_text(config_module) + + result = runner.invoke( + add_migration_commands(), + ["--config", "multi_dishka_config.get_multi_configs_from_dishka", "show-config"], + ) + + finally: + os.chdir(original_dir) + + assert result.exit_code == 0 + assert "dishka_multi_sqlite" in result.output + assert "dishka_multi_duckdb" in result.output + assert "2 configuration(s)" in result.output + + +def test_async_multi_config_dishka_provider() -> None: + """Test CLI with async Dishka provider returning multiple configs.""" + runner = CliRunner() + + with tempfile.TemporaryDirectory() as temp_dir: + original_dir = os.getcwd() + os.chdir(temp_dir) + try: + config_module = ''' +import asyncio +from dishka import make_async_container, Provider, provide, Scope +from sqlspec.adapters.sqlite.config import SqliteConfig +from sqlspec.adapters.duckdb.config import DuckDBConfig +from sqlspec.adapters.aiosqlite.config import AiosqliteConfig + +class AsyncMultiDatabaseProvider(Provider): + @provide(scope=Scope.APP) + async def get_sqlite_config(self) -> SqliteConfig: + await asyncio.sleep(0.001) + return SqliteConfig( + pool_config={"database": ":memory:"}, + migration_config={"enabled": True}, + bind_key="async_multi_sqlite" + ) + + @provide(scope=Scope.APP) + async def get_aiosqlite_config(self) -> AiosqliteConfig: + await asyncio.sleep(0.001) + return AiosqliteConfig( + pool_config={"database": ":memory:"}, + migration_config={"enabled": True}, + bind_key="async_multi_aiosqlite" + ) + + @provide(scope=Scope.APP) + async def get_duckdb_config(self) -> DuckDBConfig: + await asyncio.sleep(0.001) + return DuckDBConfig( + pool_config={"database": ":memory:"}, + migration_config={"enabled": True}, + bind_key="async_multi_duckdb" + ) + +async def get_async_multi_configs_from_dishka(): + """Get multiple configs from async Dishka container.""" + container = make_async_container(AsyncMultiDatabaseProvider()) + async with container() as request_container: + sqlite_config = await request_container.get(SqliteConfig) + aiosqlite_config = await request_container.get(AiosqliteConfig) + duckdb_config = await request_container.get(DuckDBConfig) + return [sqlite_config, aiosqlite_config, duckdb_config] +''' + Path("async_multi_dishka_config.py").write_text(config_module) + + result = runner.invoke( + add_migration_commands(), + ["--config", "async_multi_dishka_config.get_async_multi_configs_from_dishka", "show-config"], + ) + + finally: + os.chdir(original_dir) + + assert result.exit_code == 0 + assert "async_multi_sqlite" in result.output + assert "async_multi_aiosqlite" in result.output + assert "async_multi_duckdb" in result.output + assert "3 configuration(s)" in result.output + + +def test_dishka_provider_with_dependencies() -> None: + """Test Dishka provider that has complex dependencies.""" + runner = CliRunner() + + with tempfile.TemporaryDirectory() as temp_dir: + original_dir = os.getcwd() + os.chdir(temp_dir) + try: + config_module = ''' +from dishka import make_container, Provider, provide, Scope +from sqlspec.adapters.sqlite.config import SqliteConfig + +class SettingsProvider(Provider): + @provide(scope=Scope.APP) + def get_database_url(self) -> str: + return ":memory:" + + @provide(scope=Scope.APP) + def get_bind_key(self) -> str: + return "complex_dishka" + +class DatabaseProvider(Provider): + @provide(scope=Scope.APP) + def get_database_config(self, database_url: str, bind_key: str) -> SqliteConfig: + return SqliteConfig( + pool_config={"database": database_url}, + migration_config={"enabled": True, "script_location": "complex_migrations"}, + bind_key=bind_key + ) + +def get_complex_config_from_dishka(): + """Get config with dependencies from Dishka container.""" + container = make_container(SettingsProvider(), DatabaseProvider()) + with container() as request_container: + return request_container.get(SqliteConfig) +''' + Path("complex_dishka_config.py").write_text(config_module) + + result = runner.invoke( + add_migration_commands(), + ["--config", "complex_dishka_config.get_complex_config_from_dishka", "show-config"], + ) + + finally: + os.chdir(original_dir) + + assert result.exit_code == 0 + assert "complex_dishka" in result.output + assert "complex_migrations" in result.output + + +def test_dishka_error_handling() -> None: + """Test proper error handling when Dishka container fails.""" + runner = CliRunner() + + with tempfile.TemporaryDirectory() as temp_dir: + original_dir = os.getcwd() + os.chdir(temp_dir) + try: + config_module = ''' +from dishka import make_container, Provider +from sqlspec.adapters.sqlite.config import SqliteConfig + +class EmptyProvider(Provider): + pass # No providers for SqliteConfig + +def get_failing_dishka_config(): + """Try to get config when no provider exists.""" + container = make_container(EmptyProvider()) + with container() as request_container: + # This should raise an exception + return request_container.get(SqliteConfig) +''' + Path("failing_dishka_config.py").write_text(config_module) + + result = runner.invoke( + add_migration_commands(), ["--config", "failing_dishka_config.get_failing_dishka_config", "show-config"] + ) + + finally: + os.chdir(original_dir) + + assert result.exit_code == 1 + assert "Error loading config" in result.output + assert "Failed to execute callable config" in result.output + + +def test_dishka_async_with_migration_commands() -> None: + """Test that migration commands work with async Dishka configs.""" + runner = CliRunner() + + with tempfile.TemporaryDirectory() as temp_dir: + original_dir = os.getcwd() + os.chdir(temp_dir) + try: + config_module = ''' +import asyncio +from dishka import make_async_container, Provider, provide, Scope +from sqlspec.adapters.sqlite.config import SqliteConfig + +class MigrationProvider(Provider): + @provide(scope=Scope.APP) + async def get_database_config(self) -> SqliteConfig: + await asyncio.sleep(0.001) + return SqliteConfig( + pool_config={"database": ":memory:"}, + migration_config={ + "enabled": True, + "script_location": "dishka_migrations" + }, + bind_key="migration_dishka" + ) + +async def get_migration_config_from_dishka(): + """Get migration-enabled config from async Dishka container.""" + container = make_async_container(MigrationProvider()) + async with container() as request_container: + return await request_container.get(SqliteConfig) +''' + Path("migration_dishka_config.py").write_text(config_module) + + # Test that the config loads properly for migration commands + result = runner.invoke( + add_migration_commands(), + ["--config", "migration_dishka_config.get_migration_config_from_dishka", "show-config"], + ) + + finally: + os.chdir(original_dir) + + assert result.exit_code == 0 + assert "migration_dishka" in result.output + assert "dishka_migrations" in result.output or "Migration Enabled" in result.output + + +def test_dishka_with_config_validation() -> None: + """Test Dishka integration with config validation enabled.""" + runner = CliRunner() + + with tempfile.TemporaryDirectory() as temp_dir: + original_dir = os.getcwd() + os.chdir(temp_dir) + try: + config_module = ''' +import asyncio +from dishka import make_async_container, Provider, provide, Scope +from sqlspec.adapters.duckdb.config import DuckDBConfig + +class ValidatedProvider(Provider): + @provide(scope=Scope.APP) + async def get_database_config(self) -> DuckDBConfig: + await asyncio.sleep(0.001) + return DuckDBConfig( + pool_config={"database": ":memory:"}, + migration_config={"enabled": True}, + bind_key="validated_dishka" + ) + +async def get_validated_config_from_dishka(): + """Get config for validation testing.""" + container = make_async_container(ValidatedProvider()) + async with container() as request_container: + return await request_container.get(DuckDBConfig) +''' + Path("validated_dishka_config.py").write_text(config_module) + + result = runner.invoke( + add_migration_commands(), + [ + "--config", + "validated_dishka_config.get_validated_config_from_dishka", + "--validate-config", + "show-config", + ], + ) + + finally: + os.chdir(original_dir) + + assert result.exit_code == 0 + assert "Successfully loaded 1 config(s)" in result.output + assert "validated_dishka" in result.output + + +def test_real_world_dishka_scenario() -> None: + """Test a real-world scenario mimicking the user's issue.""" + runner = CliRunner() + + with tempfile.TemporaryDirectory() as temp_dir: + original_dir = os.getcwd() + os.chdir(temp_dir) + try: + # Create a structure similar to user's litestar_dishka_modular.sqlspec_main.main + Path("litestar_dishka_modular").mkdir() + Path("litestar_dishka_modular/__init__.py").write_text("") + Path("litestar_dishka_modular/sqlspec_main.py").write_text("") + + config_module = ''' +"""Simulates the user's actual Dishka configuration.""" +import asyncio +from typing import List +from dishka import make_async_container, Provider, provide, Scope +from sqlspec.adapters.sqlite.config import SqliteConfig +from sqlspec.adapters.duckdb.config import DuckDBConfig + +class DatabaseConfigProvider(Provider): + """Provider for database configurations.""" + + @provide(scope=Scope.APP) + async def get_primary_db_config(self) -> SqliteConfig: + # Simulate loading config from environment or remote service + await asyncio.sleep(0.002) # Simulate I/O + return SqliteConfig( + pool_config={"database": ":memory:"}, + migration_config={ + "enabled": True, + "script_location": "migrations/primary" + }, + bind_key="primary_db" + ) + + @provide(scope=Scope.APP) + async def get_analytics_db_config(self) -> DuckDBConfig: + await asyncio.sleep(0.002) + return DuckDBConfig( + pool_config={"database": ":memory:"}, + migration_config={ + "enabled": True, + "script_location": "migrations/analytics" + }, + bind_key="analytics_db" + ) + +async def main() -> List: + """Main entry point - this is what the user was trying to call.""" + container = make_async_container(DatabaseConfigProvider()) + + async with container() as request_container: + primary_config = await request_container.get(SqliteConfig) + analytics_config = await request_container.get(DuckDBConfig) + return [primary_config, analytics_config] +''' + Path("litestar_dishka_modular/sqlspec_main.py").write_text(config_module) + + # Test the exact command that was failing for the user + result = runner.invoke( + add_migration_commands(), ["--config", "litestar_dishka_modular.sqlspec_main.main", "show-config"] + ) + + finally: + os.chdir(original_dir) + + assert result.exit_code == 0 + assert "primary_db" in result.output + assert "analytics_db" in result.output + assert "2 configuration(s)" in result.output + + +def test_dishka_provider_cleanup() -> None: + """Test that Dishka providers are properly cleaned up.""" + runner = CliRunner() + + with tempfile.TemporaryDirectory() as temp_dir: + original_dir = os.getcwd() + os.chdir(temp_dir) + try: + config_module = ''' +import asyncio +from dishka import make_async_container, Provider, provide, Scope +from sqlspec.adapters.sqlite.config import SqliteConfig + +cleanup_called = False + +class CleanupProvider(Provider): + @provide(scope=Scope.APP) + async def get_database_config(self) -> SqliteConfig: + await asyncio.sleep(0.001) + return SqliteConfig( + pool_config={"database": ":memory:"}, + migration_config={"enabled": True}, + bind_key="cleanup_test" + ) + + def __del__(self): + global cleanup_called + cleanup_called = True + +async def get_cleanup_config(): + """Test that container cleanup works properly.""" + container = make_async_container(CleanupProvider()) + async with container() as request_container: + config = await request_container.get(SqliteConfig) + return config +''' + Path("cleanup_dishka_config.py").write_text(config_module) + + result = runner.invoke( + add_migration_commands(), ["--config", "cleanup_dishka_config.get_cleanup_config", "show-config"] + ) + + finally: + os.chdir(original_dir) + + assert result.exit_code == 0 + assert "cleanup_test" in result.output + # The container should have been cleaned up without errors diff --git a/tests/unit/test_cli/__init__.py b/tests/unit/test_cli/__init__.py new file mode 100644 index 00000000..529efc1d --- /dev/null +++ b/tests/unit/test_cli/__init__.py @@ -0,0 +1 @@ +"""CLI unit tests.""" diff --git a/tests/unit/test_cli/test_config_loading.py b/tests/unit/test_cli/test_config_loading.py new file mode 100644 index 00000000..89eb62c9 --- /dev/null +++ b/tests/unit/test_cli/test_config_loading.py @@ -0,0 +1,129 @@ +"""Tests for CLI configuration loading functionality.""" + +import os +import sys +import tempfile +from collections.abc import Iterator +from pathlib import Path + +import pytest +from click.testing import CliRunner + +from sqlspec.cli import add_migration_commands + + +@pytest.fixture +def cleanup_test_modules() -> Iterator[None]: + """Fixture to clean up test modules from sys.modules after each test.""" + modules_before = set(sys.modules.keys()) + yield + # Remove any test modules that were imported during the test + modules_after = set(sys.modules.keys()) + test_modules = {m for m in modules_after - modules_before if m.startswith("test_config")} + for module in test_modules: + if module in sys.modules: + del sys.modules[module] + + +def test_direct_config_instance_loading(cleanup_test_modules: None) -> None: + """Test loading a direct config instance through CLI.""" + runner = CliRunner() + + with tempfile.TemporaryDirectory() as temp_dir: + # Create a test module with a direct config instance + config_module = """ +from sqlspec.adapters.sqlite.config import SqliteConfig + +config = SqliteConfig( + bind_key="test", + pool_config={"database": ":memory:"}, + migration_config={"enabled": True, "script_location": "migrations"} +) +database_config = config +""" + (Path(temp_dir) / "test_config.py").write_text(config_module) + + # Change to the temp directory + original_cwd = os.getcwd() + try: + os.chdir(temp_dir) + result = runner.invoke(add_migration_commands(), ["--config", "test_config.database_config", "show-config"]) + finally: + os.chdir(original_cwd) + + assert result.exit_code == 0 + assert "test" in result.output + assert "Migration Enabled" in result.output or "migrations enabled" in result.output + + +def test_sync_callable_config_loading(cleanup_test_modules: None) -> None: + """Test loading config from synchronous callable through CLI.""" + runner = CliRunner() + + with tempfile.TemporaryDirectory() as temp_dir: + # Create a test module with sync callable + config_module = """ +from sqlspec.adapters.sqlite.config import SqliteConfig + +def get_database_config(): + config = SqliteConfig( + bind_key="sync_test", + pool_config={"database": ":memory:"}, + migration_config={"enabled": True} + ) + return config +""" + (Path(temp_dir) / "test_config.py").write_text(config_module) + + # Change to the temp directory + original_cwd = os.getcwd() + try: + os.chdir(temp_dir) + result = runner.invoke( + add_migration_commands(), ["--config", "test_config.get_database_config", "show-config"] + ) + finally: + os.chdir(original_cwd) + + assert result.exit_code == 0 + assert "sync_test" in result.output + assert "Migration Enabled" in result.output or "migrations enabled" in result.output + + +def test_async_callable_config_loading(cleanup_test_modules: None) -> None: + """Test loading config from asynchronous callable through CLI.""" + runner = CliRunner() + + with tempfile.TemporaryDirectory() as temp_dir: + # Create a test module with async callable + config_module = """ +import asyncio +from sqlspec.adapters.sqlite.config import SqliteConfig + +async def get_database_config(): + # Simulate some async work + await asyncio.sleep(0.001) + config = SqliteConfig( + bind_key="async_test", + pool_config={"database": ":memory:"}, + migration_config={"enabled": True} + ) + return config +""" + (Path(temp_dir) / "test_config.py").write_text(config_module) + + # Change to the temp directory + original_cwd = os.getcwd() + try: + os.chdir(temp_dir) + result = runner.invoke( + add_migration_commands(), ["--config", "test_config.get_database_config", "show-config"] + ) + finally: + os.chdir(original_cwd) + + if result.exception: + pass + assert result.exit_code == 0 + assert "async_test" in result.output + assert "Migration Enabled" in result.output or "migrations enabled" in result.output diff --git a/tests/unit/test_cli/test_migration_commands.py b/tests/unit/test_cli/test_migration_commands.py new file mode 100644 index 00000000..6f6008b1 --- /dev/null +++ b/tests/unit/test_cli/test_migration_commands.py @@ -0,0 +1,742 @@ +"""Tests for CLI migration commands functionality.""" + +import os +import sys +import tempfile +from collections.abc import Iterator +from pathlib import Path +from typing import TYPE_CHECKING +from unittest.mock import Mock, patch + +import pytest +from click.testing import CliRunner + +from sqlspec.cli import add_migration_commands + +if TYPE_CHECKING: + from unittest.mock import Mock + + +@pytest.fixture +def cleanup_test_modules() -> Iterator[None]: + """Fixture to clean up test modules from sys.modules after each test.""" + modules_before = set(sys.modules.keys()) + yield + # Remove any test modules that were imported during the test + modules_after = set(sys.modules.keys()) + test_modules = {m for m in modules_after - modules_before if m.startswith("test_config")} + for module in test_modules: + if module in sys.modules: + del sys.modules[module] + + +def test_show_config_command(cleanup_test_modules: None) -> None: + """Test show-config command displays migration configurations.""" + runner = CliRunner() + + 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( + bind_key="migration_test", + pool_config={"database": ":memory:"}, + migration_config={ + "enabled": True, + "script_location": "migrations" + } + ) + return config +""" + Path("test_config.py").write_text(config_module) + + result = runner.invoke(add_migration_commands(), ["--config", "test_config.get_config", "show-config"]) + + finally: + os.chdir(original_dir) + + assert result.exit_code == 0 + assert "migration_test" in result.output + assert "Migration Enabled" in result.output or "SqliteConfig" in result.output + + +def test_show_config_with_multiple_configs(cleanup_test_modules: None) -> None: + """Test show-config with multiple migration configurations.""" + runner = CliRunner() + + with tempfile.TemporaryDirectory() as temp_dir: + original_dir = os.getcwd() + os.chdir(temp_dir) + try: + config_module = """ +from sqlspec.adapters.sqlite.config import SqliteConfig +from sqlspec.adapters.duckdb.config import DuckDBConfig + +def get_configs(): + sqlite_config = SqliteConfig( + bind_key="sqlite_migrations", + pool_config={"database": ":memory:"}, + migration_config={"enabled": True, "script_location": "sqlite_migrations"} + ) + + duckdb_config = DuckDBConfig( + bind_key="duckdb_migrations", + pool_config={"database": ":memory:"}, + migration_config={"enabled": True, "script_location": "duckdb_migrations"} + ) + + return [sqlite_config, duckdb_config] +""" + Path("test_config.py").write_text(config_module) + + result = runner.invoke(add_migration_commands(), ["--config", "test_config.get_configs", "show-config"]) + + finally: + os.chdir(original_dir) + + assert result.exit_code == 0 + assert "sqlite_migrations" in result.output + assert "duckdb_migrations" in result.output + assert "2 configuration(s)" in result.output + + +def test_show_config_no_migrations(cleanup_test_modules: None) -> None: + """Test show-config when no migrations are configured.""" + runner = CliRunner() + + 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 without migration_config + config = SqliteConfig( + bind_key="no_migrations", + pool_config={"database": ":memory:"} + ) + return config +""" + Path("test_config.py").write_text(config_module) + + result = runner.invoke(add_migration_commands(), ["--config", "test_config.get_config", "show-config"]) + + finally: + os.chdir(original_dir) + + assert result.exit_code == 0 + assert ( + "No configurations with migrations detected" in result.output or "no_migrations" in result.output + ) # Depends on validation logic + + +@patch("sqlspec.migrations.commands.create_migration_commands") +def test_show_current_revision_command(mock_create_commands: "Mock", cleanup_test_modules: None) -> None: + """Test show-current-revision command.""" + runner = CliRunner() + + # Mock the migration commands + mock_commands = Mock() + mock_commands.current = Mock(return_value=None) # Sync function + 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( + bind_key="revision_test", + pool_config={"database": ":memory:"}, + migration_config={"enabled": True, "script_location": "migrations"} + ) + return config +""" + Path("test_config.py").write_text(config_module) + + result = runner.invoke( + add_migration_commands(), ["--config", "test_config.get_config", "show-current-revision"] + ) + + finally: + os.chdir(original_dir) + + assert result.exit_code == 0 + mock_commands.current.assert_called_once_with(verbose=False) + + +@patch("sqlspec.migrations.commands.create_migration_commands") +def test_show_current_revision_verbose(mock_create_commands: "Mock", cleanup_test_modules: None) -> None: + """Test show-current-revision command with verbose flag.""" + runner = CliRunner() + + mock_commands = Mock() + mock_commands.current = 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( + bind_key="verbose_test", + pool_config={"database": ":memory:"}, + migration_config={"enabled": True} + ) + return config +""" + Path("test_config.py").write_text(config_module) + + result = runner.invoke( + add_migration_commands(), ["--config", "test_config.get_config", "show-current-revision", "--verbose"] + ) + + finally: + os.chdir(original_dir) + + assert result.exit_code == 0 + mock_commands.current.assert_called_once_with(verbose=True) + + +@patch("sqlspec.migrations.commands.create_migration_commands") +def test_init_command(mock_create_commands: "Mock", cleanup_test_modules: None) -> None: + """Test init command for initializing migrations.""" + runner = CliRunner() + + mock_commands = Mock() + mock_commands.init = 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 = "init_test" + config.migration_config = {"script_location": "test_migrations"} + return config +""" + Path("test_config.py").write_text(config_module) + + result = runner.invoke( + add_migration_commands(), ["--config", "test_config.get_config", "init", "--no-prompt"] + ) + + finally: + os.chdir(original_dir) + + assert result.exit_code == 0 + mock_commands.init.assert_called_once_with(directory="test_migrations", package=True) + + +@patch("sqlspec.migrations.commands.create_migration_commands") +def test_init_command_custom_directory(mock_create_commands: "Mock", cleanup_test_modules: None) -> None: + """Test init command with custom directory.""" + runner = CliRunner() + + mock_commands = Mock() + mock_commands.init = 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 = "custom_init" + config.migration_config = {"script_location": "migrations"} + return config +""" + Path("test_config.py").write_text(config_module) + + result = runner.invoke( + add_migration_commands(), + ["--config", "test_config.get_config", "init", "custom_migrations", "--no-prompt"], + ) + + finally: + os.chdir(original_dir) + + assert result.exit_code == 0 + mock_commands.init.assert_called_once_with(directory="custom_migrations", package=True) + + +@patch("sqlspec.migrations.commands.create_migration_commands") +def test_make_migrations_command(mock_create_commands: "Mock", cleanup_test_modules: None) -> None: + """Test make-migrations command.""" + 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 +""" + Path("test_config.py").write_text(config_module) + + result = runner.invoke( + add_migration_commands(), + ["--config", "test_config.get_config", "make-migrations", "-m", "test migration", "--no-prompt"], + ) + + finally: + os.chdir(original_dir) + + assert result.exit_code == 0 + mock_commands.revision.assert_called_once_with(message="test migration") + + +@patch("sqlspec.migrations.commands.create_migration_commands") +def test_upgrade_command(mock_create_commands: "Mock", cleanup_test_modules: None) -> None: + """Test upgrade command.""" + runner = CliRunner() + + mock_commands = Mock() + mock_commands.upgrade = 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 = "upgrade_test" + config.migration_config = {"enabled": True} + return config +""" + Path("test_config.py").write_text(config_module) + + result = runner.invoke( + add_migration_commands(), ["--config", "test_config.get_config", "upgrade", "--no-prompt"] + ) + + finally: + os.chdir(original_dir) + + assert result.exit_code == 0 + mock_commands.upgrade.assert_called_once_with(revision="head") + + +@patch("sqlspec.migrations.commands.create_migration_commands") +def test_upgrade_command_specific_revision(mock_create_commands: "Mock", cleanup_test_modules: None) -> None: + """Test upgrade command with specific revision.""" + runner = CliRunner() + + mock_commands = Mock() + mock_commands.upgrade = 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 = "upgrade_revision_test" + config.migration_config = {"enabled": True} + return config +""" + Path("test_config.py").write_text(config_module) + + result = runner.invoke( + add_migration_commands(), ["--config", "test_config.get_config", "upgrade", "abc123", "--no-prompt"] + ) + + finally: + os.chdir(original_dir) + + assert result.exit_code == 0 + mock_commands.upgrade.assert_called_once_with(revision="abc123") + + +@patch("sqlspec.migrations.commands.create_migration_commands") +def test_downgrade_command(mock_create_commands: "Mock", cleanup_test_modules: None) -> None: + """Test downgrade command.""" + runner = CliRunner() + + mock_commands = Mock() + mock_commands.downgrade = 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 = "downgrade_test" + config.migration_config = {"enabled": True} + return config +""" + Path("test_config.py").write_text(config_module) + + result = runner.invoke( + add_migration_commands(), ["--config", "test_config.get_config", "downgrade", "--no-prompt"] + ) + + finally: + os.chdir(original_dir) + + assert result.exit_code == 0 + mock_commands.downgrade.assert_called_once_with(revision="-1") + + +@patch("sqlspec.migrations.commands.create_migration_commands") +def test_stamp_command(mock_create_commands: "Mock", cleanup_test_modules: None) -> None: + """Test stamp command.""" + runner = CliRunner() + + mock_commands = Mock() + mock_commands.stamp = 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 = "stamp_test" + config.migration_config = {"enabled": True} + return config +""" + Path("test_config.py").write_text(config_module) + + result = runner.invoke(add_migration_commands(), ["--config", "test_config.get_config", "stamp", "abc123"]) + + finally: + os.chdir(original_dir) + + assert result.exit_code == 0 + mock_commands.stamp.assert_called_once_with(revision="abc123") + + +@patch("sqlspec.migrations.commands.create_migration_commands") +def test_multi_config_operations(mock_create_commands: "Mock", cleanup_test_modules: None) -> None: + """Test multi-configuration operations with include/exclude filters.""" + runner = CliRunner() + + mock_commands = Mock() + mock_commands.current = 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 +from sqlspec.adapters.duckdb.config import DuckDBConfig + +def get_configs(): + sqlite_config = SqliteConfig(pool_config={"database": ":memory:"}) + sqlite_config.bind_key = "sqlite_multi" + sqlite_config.migration_config = {"enabled": True} + + duckdb_config = DuckDBConfig(pool_config={"database": ":memory:"}) + duckdb_config.bind_key = "duckdb_multi" + duckdb_config.migration_config = {"enabled": True} + + return [sqlite_config, duckdb_config] +""" + Path("test_config.py").write_text(config_module) + + result = runner.invoke( + add_migration_commands(), + ["--config", "test_config.get_configs", "show-current-revision", "--include", "sqlite_multi"], + ) + + finally: + os.chdir(original_dir) + + assert result.exit_code == 0 + # Should process only the included configuration + assert "sqlite_multi" in result.output + + +@patch("sqlspec.migrations.commands.create_migration_commands") +def test_dry_run_operations(mock_create_commands: "Mock", cleanup_test_modules: None) -> None: + """Test dry-run operations show what would be executed.""" + runner = CliRunner() + + mock_commands = Mock() + mock_commands.upgrade = 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_configs(): + config1 = SqliteConfig(pool_config={"database": ":memory:"}) + config1.bind_key = "dry_run_test1" + config1.migration_config = {"enabled": True} + + config2 = SqliteConfig(pool_config={"database": "test.db"}) + config2.bind_key = "dry_run_test2" + config2.migration_config = {"enabled": True} + + return [config1, config2] +""" + Path("test_config.py").write_text(config_module) + + result = runner.invoke( + add_migration_commands(), ["--config", "test_config.get_configs", "upgrade", "--dry-run"] + ) + + finally: + os.chdir(original_dir) + + assert result.exit_code == 0 + assert "Dry run" in result.output + assert "Would upgrade" in result.output + # Should not actually call the upgrade method with dry-run + mock_commands.upgrade.assert_not_called() + + +def test_execution_mode_reporting(cleanup_test_modules: None) -> None: + """Test that execution mode is reported when specified.""" + runner = CliRunner() + + 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 = "execution_mode_test" + config.migration_config = {"enabled": True} + return config +""" + Path("test_config.py").write_text(config_module) + + with patch("sqlspec.migrations.commands.create_migration_commands") as mock_create: + mock_commands = Mock() + mock_commands.upgrade = Mock(return_value=None) + mock_create.return_value = mock_commands + + result = runner.invoke( + add_migration_commands(), + ["--config", "test_config.get_config", "upgrade", "--execution-mode", "sync", "--no-prompt"], + ) + + finally: + os.chdir(original_dir) + + assert result.exit_code == 0 + assert "Execution mode: sync" in result.output + + +def test_bind_key_filtering_single_config(cleanup_test_modules: None) -> None: + """Test --bind-key filtering with single config.""" + runner = CliRunner() + + 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(): + return SqliteConfig( + bind_key="target_config", + pool_config={"database": ":memory:"}, + migration_config={"enabled": True, "script_location": "migrations"} + ) +""" + Path("test_config.py").write_text(config_module) + + result = runner.invoke( + add_migration_commands(), + ["--config", "test_config.get_config", "show-config", "--bind-key", "target_config"], + ) + + finally: + os.chdir(original_dir) + + assert result.exit_code == 0 + assert "target_config" in result.output + + +def test_bind_key_filtering_multiple_configs(cleanup_test_modules: None) -> None: + """Test --bind-key filtering with multiple configs.""" + runner = CliRunner() + + with tempfile.TemporaryDirectory() as temp_dir: + original_dir = os.getcwd() + os.chdir(temp_dir) + try: + config_module = """ +from sqlspec.adapters.sqlite.config import SqliteConfig +from sqlspec.adapters.duckdb.config import DuckDBConfig + +def get_configs(): + sqlite_config = SqliteConfig( + bind_key="sqlite_db", + pool_config={"database": ":memory:"}, + migration_config={"enabled": True, "script_location": "sqlite_migrations"} + ) + + duckdb_config = DuckDBConfig( + bind_key="duckdb_db", + pool_config={"database": ":memory:"}, + migration_config={"enabled": True, "script_location": "duckdb_migrations"} + ) + + postgres_config = SqliteConfig( + bind_key="postgres_db", + pool_config={"database": ":memory:"}, + migration_config={"enabled": True, "script_location": "postgres_migrations"} + ) + + return [sqlite_config, duckdb_config, postgres_config] +""" + Path("test_config.py").write_text(config_module) + + # Test filtering for sqlite_db only + result = runner.invoke( + add_migration_commands(), + ["--config", "test_config.get_configs", "show-config", "--bind-key", "sqlite_db"], + ) + + finally: + os.chdir(original_dir) + + assert result.exit_code == 0 + assert "sqlite_db" in result.output + # Should only show one config, not all three + assert "Found 1 configuration(s)" in result.output or "sqlite_migrations" in result.output + assert "duckdb_db" not in result.output + assert "postgres_db" not in result.output + + +def test_bind_key_filtering_nonexistent_key(cleanup_test_modules: None) -> None: + """Test --bind-key filtering with nonexistent bind key.""" + runner = CliRunner() + + 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_configs(): + return [ + SqliteConfig( + bind_key="existing_config", + pool_config={"database": ":memory:"}, + migration_config={"enabled": True} + ) + ] +""" + Path("test_config.py").write_text(config_module) + + result = runner.invoke( + add_migration_commands(), + ["--config", "test_config.get_configs", "show-config", "--bind-key", "nonexistent"], + ) + + finally: + os.chdir(original_dir) + + assert result.exit_code == 1 + assert "No config found for bind key: nonexistent" in result.output + + +@patch("sqlspec.migrations.commands.create_migration_commands") +def test_bind_key_filtering_with_migration_commands(mock_create_commands: "Mock", cleanup_test_modules: None) -> None: + """Test --bind-key filtering works with actual migration commands.""" + runner = CliRunner() + + mock_commands = Mock() + mock_commands.upgrade = 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 +from sqlspec.adapters.duckdb.config import DuckDBConfig + +def get_multi_configs(): + return [ + SqliteConfig( + bind_key="primary_db", + pool_config={"database": "primary.db"}, + migration_config={"enabled": True, "script_location": "primary_migrations"} + ), + DuckDBConfig( + bind_key="analytics_db", + pool_config={"database": "analytics.duckdb"}, + migration_config={"enabled": True, "script_location": "analytics_migrations"} + ) + ] +""" + Path("test_config.py").write_text(config_module) + + result = runner.invoke( + add_migration_commands(), + ["--config", "test_config.get_multi_configs", "upgrade", "--bind-key", "analytics_db", "--no-prompt"], + ) + + finally: + os.chdir(original_dir) + + assert result.exit_code == 0 + # Should only process the analytics_db config + mock_commands.upgrade.assert_called_once_with(revision="head") diff --git a/tests/unit/test_config_resolver.py b/tests/unit/test_config_resolver.py index 07b745e1..3d9dd554 100644 --- a/tests/unit/test_config_resolver.py +++ b/tests/unit/test_config_resolver.py @@ -150,14 +150,16 @@ def mixed_config_list() -> list[Any]: async def test_config_validation_attributes(self) -> None: """Test that config validation checks for required attributes.""" - # Test config missing database_url - mock_config = Mock() - mock_config.bind_key = "test" - mock_config.migration_config = {} - del mock_config.database_url # Remove the attribute - def incomplete_config() -> Mock: - return mock_config + # Test config missing both database_url and pool_config + class IncompleteConfig: + def __init__(self) -> None: + self.bind_key = "test" + self.migration_config: dict[str, Any] = {} + # Missing both pool_config and database_url + + def incomplete_config() -> "IncompleteConfig": + return IncompleteConfig() with patch("sqlspec.utils.config_resolver.import_string", return_value=incomplete_config): with pytest.raises(ConfigResolverError, match="returned invalid type"): diff --git a/uv.lock b/uv.lock index 9fee9fe7..b2d5b400 100644 --- a/uv.lock +++ b/uv.lock @@ -1165,6 +1165,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fe/47/290daabcf91628f4fc0e17c75a1690b354ba067066cd14407712600e609f/dict2css-0.3.0.post1-py3-none-any.whl", hash = "sha256:f006a6b774c3e31869015122ae82c491fd25e7de4a75607a62aa3e798f837e0d", size = 25647, upload-time = "2023-11-22T11:09:19.221Z" }, ] +[[package]] +name = "dishka" +version = "1.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/53/41679bbc5f738655c320b3f32eb957111a89abb4a0c5cd652a7cb4cb244b/dishka-1.7.1.tar.gz", hash = "sha256:a5a5eee9f2cd233c08ddaa11e95b8b0229dd9b712e2117ee39ce475c2d65f597", size = 67891, upload-time = "2025-09-13T16:37:13.296Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/41/fae3b2ee895ea5b3f42a5572e17e19ab0085cb4d92f9ac8feecdaa8bd19f/dishka-1.7.1-py3-none-any.whl", hash = "sha256:42f756f685ebc71c126e82dd9e002b7f277d97c8c7868c1ee412d265a3c98b63", size = 94567, upload-time = "2025-09-13T16:37:11.671Z" }, +] + [[package]] name = "distlib" version = "0.4.0" @@ -3891,16 +3903,16 @@ wheels = [ [[package]] name = "pydantic-settings" -version = "2.10.1" +version = "2.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583, upload-time = "2025-06-24T13:26:46.841Z" } +sdist = { url = "https://files.pythonhosted.org/packages/20/c5/dbbc27b814c71676593d1c3f718e6cd7d4f00652cefa24b75f7aa3efb25e/pydantic_settings-2.11.0.tar.gz", hash = "sha256:d0e87a1c7d33593beb7194adb8470fc426e95ba02af83a0f23474a04c9a08180", size = 188394, upload-time = "2025-09-24T14:19:11.764Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload-time = "2025-06-24T13:26:45.485Z" }, + { url = "https://files.pythonhosted.org/packages/83/d6/887a1ff844e64aa823fb4905978d882a633cfe295c32eacad582b78a7d8b/pydantic_settings-2.11.0-py3-none-any.whl", hash = "sha256:fe2cea3413b9530d10f3a5875adffb17ada5c1e1bab0b2885546d7310415207c", size = 48608, upload-time = "2025-09-24T14:19:10.015Z" }, ] [[package]] @@ -4460,7 +4472,7 @@ wheels = [ [[package]] name = "shibuya" -version = "2025.9.23" +version = "2025.9.24" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pygments-styles" }, @@ -4468,9 +4480,9 @@ dependencies = [ { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/66/d3077799d46fb6212b271b346c26245eead4e66fe2001eb0d38d76192312/shibuya-2025.9.23.tar.gz", hash = "sha256:5d788a27bcec947f747c28ec783ff8b4246497324531f80e4b1e4d5582df5856", size = 80450, upload-time = "2025-09-22T15:02:41.012Z" } +sdist = { url = "https://files.pythonhosted.org/packages/39/a6/fe2a0b54d4ff83041e8ebd7423de73a00146a3ad46deedab7670a74d4ece/shibuya-2025.9.24.tar.gz", hash = "sha256:9bf9eb2f08e3582f538839aa83e656afbf55b8dc217236cda7690067c806afe2", size = 81687, upload-time = "2025-09-24T03:19:00.194Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/98/9cce79eedb8c1e974f5e0131048a284213411feb242e8433c3f280d1d99c/shibuya-2025.9.23-py3-none-any.whl", hash = "sha256:089c51c0905b4131c11c6442f69ab587f007bb9e68fde54e7ae0510c1fafdb8f", size = 96090, upload-time = "2025-09-22T15:02:39.692Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c8/d473b1c9d4d8685081d9a25848bb1ca63feccef50b014d4d886b8a2bef24/shibuya-2025.9.24-py3-none-any.whl", hash = "sha256:78dc1db63af964e19b75abbf730fd01d35fa9c8ce72190a5da3c42dfc3cea4d9", size = 97547, upload-time = "2025-09-24T03:18:58.572Z" }, ] [[package]] @@ -5261,6 +5273,7 @@ dev = [ { name = "auto-pytabs", extra = ["sphinx"] }, { name = "bump-my-version" }, { name = "coverage" }, + { name = "dishka", marker = "python_full_version >= '3.10'" }, { name = "duckdb" }, { name = "fsspec", extra = ["s3"] }, { name = "hatch-mypyc" }, @@ -5339,6 +5352,7 @@ extras = [ { name = "adbc-driver-manager" }, { name = "adbc-driver-postgresql" }, { name = "adbc-driver-sqlite" }, + { name = "dishka", marker = "python_full_version >= '3.10'" }, { name = "fsspec", extra = ["s3"] }, { name = "pgvector" }, { name = "polars" }, @@ -5444,6 +5458,7 @@ dev = [ { name = "auto-pytabs", extras = ["sphinx"], specifier = ">=0.5.0" }, { name = "bump-my-version" }, { name = "coverage", specifier = ">=7.6.1" }, + { name = "dishka", marker = "python_full_version >= '3.10'" }, { name = "duckdb" }, { name = "fsspec", extras = ["s3"] }, { name = "hatch-mypyc" }, @@ -5510,6 +5525,7 @@ extras = [ { name = "adbc-driver-manager" }, { name = "adbc-driver-postgresql" }, { name = "adbc-driver-sqlite" }, + { name = "dishka", marker = "python_full_version >= '3.10'" }, { name = "fsspec", extras = ["s3"] }, { name = "pgvector" }, { name = "polars" }, @@ -5662,11 +5678,11 @@ wheels = [ [[package]] name = "types-docutils" -version = "0.22.1.20250923" +version = "0.22.2.20250924" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/80/76/3b2b841d2ea3b59685c59cfcfeddd439146287319c1566099796507ee866/types_docutils-0.22.1.20250923.tar.gz", hash = "sha256:f7754c7eeab44144095e287bb3afcb96d6936dc3fa332cff4e5ef5533baa198f", size = 56652, upload-time = "2025-09-23T02:51:20.573Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/6d/60326ba08f44629f778937d5021a342da996682d932261d48b4043c437f7/types_docutils-0.22.2.20250924.tar.gz", hash = "sha256:a13fb412676c164edec7c2f26fe52ab7b0b7c868168dacc4298f6a8069298f3d", size = 56679, upload-time = "2025-09-24T02:53:26.251Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/29/22/c762f76ace7de3c8d469f9f25a8b8870bff47fb31ecc15453ac96426f36a/types_docutils-0.22.1.20250923-py3-none-any.whl", hash = "sha256:a01a8036b373dd578ec1a170db04f4159be1282ca92f0a085e50f4afbeb8d0bc", size = 91872, upload-time = "2025-09-23T02:51:19.305Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2b/844f3a6e972515ef0890fd8bf631890b6d74c8eacb1acbf31a72820c3b45/types_docutils-0.22.2.20250924-py3-none-any.whl", hash = "sha256:a6d52e21fa70998d34d13db6891ea35920bbb20f91459ca528a3845fd0b9ec03", size = 91873, upload-time = "2025-09-24T02:53:24.824Z" }, ] [[package]]