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