Source code for shapelib.core.cloud.handlers.remote_handler
"""Abstract base class for remote geospatial file handlers."""
from abc import ABC, abstractmethod
from typing import Any
import geopandas as gpd
from shapelib.core.cloud.cloud_path import CloudPath
from shapelib.core.geometries.base import BaseShapes
from shapelib.core.geometries.empty import Empty
from shapelib.core.geometries.lines import Lines
from shapelib.core.geometries.points import Points
from shapelib.core.geometries.polygons import Polygons
from shapelib.utils.utils import assume_wgs84_if_lonlat, get_most_common_geometry_type
[docs]
class RemoteGeoFileHandler(ABC):
"""
Abstract base class for handling remote geospatial files.
This class defines the interface for remote file handlers. Each handler
knows how to read its specific format from remote storage, either via
streaming (GDAL virtual filesystem) or download.
Subclasses must implement `load_data()` to define how data is retrieved
from the remote source.
Attributes
----------
driver : str
The GDAL driver name for the format.
extension : str
The file extension this handler supports.
keep_z : bool
Whether the object created from the file keeps the Z coordinate of 3-D
geometries. False by default; set by the factory from ``read_file``.
Parameters
----------
cloud_path : CloudPath
The remote path to the file.
Examples
--------
Creating a custom remote handler:
>>> @register_cloud_handler(".myformat")
... class RemoteMyFormatHandler(RemoteGeoFileHandler):
... driver = "MyFormat"
... extension = ".myformat"
...
... def load_data(self, **kwargs) -> gpd.GeoDataFrame:
... return gpd.read_file(self.cloud_path.vsicurl_path, **kwargs)
"""
driver: str = ""
extension: str = ""
keep_z: bool = False # Set by GeoCloudFactory before create_obj
def __init__(self, cloud_path: CloudPath) -> None:
"""
Initialize the remote handler with a cloud path.
Parameters
----------
cloud_path : CloudPath
The remote path to the geospatial file.
"""
self.cloud_path = cloud_path
[docs]
def create_obj(self, **kwargs: Any) -> BaseShapes:
"""
Load data and create the appropriate geometry object.
Parameters
----------
**kwargs : Any
Additional arguments passed to load_data().
Returns
-------
BaseShapes
The geometry object (Polygons, Lines, Points, or Empty).
Raises
------
ConnectionError
If the remote file cannot be accessed.
FileNotFoundError
If the remote file does not exist.
"""
data = self.load_data(**kwargs)
return self._create_geometry_instance(data)
[docs]
@abstractmethod
def load_data(self, **kwargs: Any) -> gpd.GeoDataFrame:
"""
Load data from the remote source.
Subclasses implement this to define how data is retrieved:
- Streaming handlers use GDAL's vsicurl/vsis3
- Download handlers fetch to temp and delegate to local handlers
Parameters
----------
**kwargs : Any
Additional arguments (e.g., layer name for GeoPackage).
Returns
-------
gpd.GeoDataFrame
The loaded geospatial data.
Raises
------
ConnectionError
If the remote file cannot be accessed.
FileNotFoundError
If the remote file does not exist.
"""
pass
def _create_geometry_instance(self, data: gpd.GeoDataFrame) -> BaseShapes:
"""
Create the appropriate geometry instance from GeoDataFrame.
Data read without a CRS is assumed to be WGS84 when its coordinates are
lon/lat, and rejected otherwise (see ``assume_wgs84_if_lonlat``).
Parameters
----------
data : gpd.GeoDataFrame
The loaded GeoDataFrame.
Returns
-------
BaseShapes
The geometry object (Polygons, Lines, Points, or Empty).
Raises
------
ValueError
If the data has no CRS and its coordinates are not lon/lat.
"""
# The path without the query string: a presigned URL's signature stays out of the logs.
data = assume_wgs84_if_lonlat(data, self.cloud_path.path)
common_type = get_most_common_geometry_type(data)
match common_type:
case "Polygon" | "MultiPolygon":
return Polygons(data, path=None, crs=data.crs, driver=self.driver, keep_z=self.keep_z)
case "LineString" | "MultiLineString":
return Lines(data, path=None, crs=data.crs, driver=self.driver, keep_z=self.keep_z)
case "Point" | "MultiPoint":
return Points(data, path=None, crs=data.crs, driver=self.driver, keep_z=self.keep_z)
case _:
return Empty(data, path=None, crs=data.crs, driver=self.driver, keep_z=self.keep_z)