Source code for shapelib.factories.geo_cloud_factory

"""Factory for handling remote geospatial files from cloud storage."""

from __future__ import annotations

from pathlib import Path
from typing import TYPE_CHECKING, Any, ClassVar

from shapelib.core.cloud.cloud_path import CloudPath
from shapelib.core.geometries.base import BaseShapes

if TYPE_CHECKING:
    from shapelib.core.cloud.handlers.remote_handler import RemoteGeoFileHandler
    from shapelib.core.cloud.providers.cloud_provider import CloudProvider


[docs] class GeoCloudFactory: """ Factory for creating handlers for remote geospatial files. This class mirrors GeoFileFactory's architecture but for remote files. It maintains a registry of handlers for different file formats and a registry of providers for different cloud protocols. Each handler knows whether to use streaming (vsicurl) or download for its specific format. Each provider handles upload/download for its specific protocol. Attributes ---------- _handlers : dict[str, type[RemoteGeoFileHandler]] Registry mapping file extensions to handler classes. _providers : list[CloudProvider] Registry of cloud providers (S3, HTTPS, etc.). Examples -------- Read a remote FlatGeobuf file: >>> geometry = GeoCloudFactory.get_handler( ... "https://bucket.s3.amazonaws.com/file.fgb?signature=..." ... ) >>> print(type(geometry)) <class 'shapelib.core.geometries.lines.Lines'> Big-O ----- O(n) for streaming formats, O(n) + disk I/O for download formats. """ _handlers: ClassVar[dict[str, type[RemoteGeoFileHandler]]] = {} _providers: ClassVar[list[CloudProvider]] = []
[docs] @classmethod def register_handler(cls, file_extension: str, handler_class: type[RemoteGeoFileHandler]) -> None: """ Register a handler for a specific file extension. Parameters ---------- file_extension : str The file extension (e.g., ".fgb", ".shp"). handler_class : type[RemoteGeoFileHandler] The handler class for this extension. """ cls._handlers[file_extension] = handler_class
[docs] @classmethod def register_provider(cls, provider: CloudProvider) -> None: """ Register a cloud provider. Providers are checked in registration order (last registered = first checked). Parameters ---------- provider : CloudProvider The provider instance to register. """ cls._providers.insert(0, provider)
[docs] @classmethod def get_provider(cls, cloud_path: CloudPath) -> CloudProvider: """ Get the appropriate provider for a cloud path. Parameters ---------- cloud_path : CloudPath The parsed cloud path. Returns ------- CloudProvider The provider that supports this path. Raises ------ ValueError If no provider supports this path. """ for provider in cls._providers: if provider.supports(cloud_path): return provider raise ValueError(f"No provider supports scheme: {cloud_path.scheme}")
[docs] @classmethod def get_handler(cls, path: str, keep_z: bool = False, **kwargs: Any) -> BaseShapes: """ Get the appropriate handler and create geometry from remote file. Parameters ---------- path : str The remote URL to the geospatial file. keep_z : bool, optional Keep the Z coordinate of 3-D geometries (default False). Handed to the handler, never to the file reader underneath it. **kwargs : Any Additional arguments passed to the handler. Returns ------- BaseShapes The geometry object (Polygons, Lines, Points, or Empty). Raises ------ ValueError If no handler is registered for the file extension. ConnectionError If the remote file cannot be accessed. FileNotFoundError If the remote file does not exist. """ cloud_path = CloudPath.from_string(path) handler_class = cls._handlers.get(cloud_path.extension) if handler_class is None: raise ValueError(f"Remote file format '{cloud_path.extension}' is not supported.") handler = handler_class(cloud_path) handler.keep_z = keep_z return handler.create_obj(**kwargs)
[docs] @classmethod def upload_file(cls, source: Path, destination: str) -> None: """ Upload a local file to cloud storage. Parameters ---------- source : Path The local file to upload. destination : str The remote URL destination (presigned PUT URL or s3://). Raises ------ FileNotFoundError If the source file does not exist. ConnectionError If the upload fails. PermissionError If access is denied. ValueError If no provider supports the destination protocol. """ if not source.exists(): raise FileNotFoundError(f"Source file not found: {source}") cloud_path = CloudPath.from_string(destination) provider = cls.get_provider(cloud_path) provider.upload(source, cloud_path)
[docs] @classmethod def download_file(cls, cloud_path: CloudPath, destination: Path) -> Path: """ Download a remote file to local destination. Used internally by download-based handlers. Parameters ---------- cloud_path : CloudPath The remote source path. destination : Path The local destination path. Returns ------- Path The path to the downloaded file. Raises ------ FileNotFoundError If the remote file does not exist. ConnectionError If the download fails. PermissionError If access is denied. ValueError If no provider supports the source protocol. """ provider = cls.get_provider(cloud_path) return provider.download(cloud_path, destination)
[docs] @classmethod def file_exists(cls, cloud_path: CloudPath) -> bool: """ Check if a remote file exists without downloading. Parameters ---------- cloud_path : CloudPath The remote file path to check. Returns ------- bool True if the file exists, False otherwise. """ try: provider = cls.get_provider(cloud_path) return provider.file_exists(cloud_path) except ValueError: return False
# Providers are auto-registered via @register_cloud_provider decorator # Import triggers registration import shapelib.core.cloud.providers.https_provider # noqa: F401, E402 import shapelib.core.cloud.providers.s3_provider # noqa: F401, E402