Source code for sqlspec.storage.registry

"""Storage registry for ObjectStore backends.

Provides a storage registry that supports URI-first access
pattern with automatic backend detection, ObStore preferred with FSSpec fallback,
scheme-based routing, and named aliases for common configurations.
"""

import logging
import re
from pathlib import Path
from typing import Any, Final, cast
from urllib.parse import unquote, urlparse, urlunparse

from mypy_extensions import mypyc_attr

from sqlspec.exceptions import ImproperConfigurationError, MissingDependencyError
from sqlspec.protocols import ObjectStoreProtocol
from sqlspec.storage._paths import is_file_destination, strip_windows_drive_prefix
from sqlspec.typing import FSSPEC_INSTALLED, OBSTORE_INSTALLED
from sqlspec.utils.logging import get_logger, log_with_context
from sqlspec.utils.type_guards import is_local_path

__all__ = ("StorageRegistry", "storage_registry")

logger = get_logger(__name__)

SCHEME_REGEX: Final = re.compile(r"([a-zA-Z0-9+.-]+)://")


FSSPEC_ONLY_SCHEMES: Final[frozenset[str]] = frozenset({"http", "https", "ftp", "sftp", "ssh"})


@mypyc_attr(allow_interpreted_subclasses=True)
class StorageRegistry:
    """Global storage registry for named backend configurations.

    Allows registering named storage backends that can be accessed from anywhere
    in your application. Backends are automatically selected based on URI scheme
    unless explicitly overridden.
    """

    __slots__ = ("_alias_configs", "_instances")

    def __init__(self) -> None:
        self._alias_configs: dict[str, tuple[type[ObjectStoreProtocol], str, dict[str, Any]]] = {}
        self._instances: dict[str | tuple[str, tuple[tuple[str, Any], ...]], ObjectStoreProtocol] = {}
def _make_hashable(self, obj: Any) -> Any: """Convert nested dict/list structures to hashable tuples.""" if isinstance(obj, dict): return tuple(sorted((k, self._make_hashable(v)) for k, v in obj.items())) if isinstance(obj, list): return tuple(self._make_hashable(item) for item in obj) if isinstance(obj, set): return tuple(sorted(self._make_hashable(item) for item in obj)) return obj def register_alias( self, alias: str, uri: str, *, backend: str | None = None, base_path: str = "", **kwargs: Any ) -> None: """Register a named alias for a storage configuration. Args: alias: Unique alias name uri: Storage URI backend: Force specific backend ("local", "fsspec", "obstore") instead of auto-detection base_path: Base path to prepend to all operations **kwargs: Backend-specific configuration options """ backend_cls = self._backend_class(backend) if backend else self._determine_backend_class(uri) backend_config = dict(kwargs) if base_path: backend_config["base_path"] = base_path self._alias_configs[alias] = (backend_cls, uri, backend_config) self.clear_cache(alias) log_with_context( logger, logging.DEBUG, "storage.alias.register", alias=alias, uri=uri, backend_type=backend_cls.__name__, base_path=base_path or None, )