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)