diff --git a/eolearn/core/eodata_io.py b/eolearn/core/eodata_io.py index 8e2d984e..3ce0054c 100644 --- a/eolearn/core/eodata_io.py +++ b/eolearn/core/eodata_io.py @@ -15,8 +15,11 @@ import gzip import itertools import json +import os import platform +import shutil import sys +import tempfile import warnings from abc import ABCMeta, abstractmethod from collections import defaultdict @@ -651,13 +654,17 @@ def _get_uncompressed_file_extension(cls) -> str: return ".gpkg" def _read_from_file(self, file: BinaryIO | gzip.GzipFile) -> gpd.GeoDataFrame: - dataframe = gpd.read_file(file) + # pyogrio (geopandas >= 1) warns when reading GPKG from virtual in-memory + # files without a .gpkg extension. Suppress this warning as it's harmless. + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", message=".*GPKG application_id.*", category=RuntimeWarning) + dataframe = gpd.read_file(file) if dataframe.crs is not None: # Trying to preserve a standard CRS and passing otherwise with contextlib.suppress(ValueError), warnings.catch_warnings(): warnings.simplefilter("ignore", category=SHUserWarning) - dataframe.crs = CRS(dataframe.crs).pyproj_crs() + dataframe = dataframe.set_crs(CRS(dataframe.crs).pyproj_crs()) if TIMESTAMP_COLUMN in dataframe: dataframe[TIMESTAMP_COLUMN] = pd.to_datetime(dataframe[TIMESTAMP_COLUMN]) @@ -675,6 +682,34 @@ def _write_to_file(cls, data: gpd.GeoDataFrame, file: BinaryIO | gzip.GzipFile, ) return data.to_file(file, driver="GPKG", encoding="utf-8", layer=layer, index=False) + @classmethod + def _save(cls, data: gpd.GeoDataFrame, filesystem: FS, path: str, compress_level: int) -> None: + """Save GeoDataFrame, handling pyogrio (geopandas >= 1) which requires a real file path + for GPKG format and cannot write to open file handles or gzip-wrapped file objects. + + This overrides the parent's _save to first write to a temporary .gpkg file, then + copy (and optionally gzip-compress) the content to the target filesystem. + """ + layer = fs.path.basename(path) + with tempfile.TemporaryDirectory() as tmp_dir: + tmp_gpkg = os.path.join(tmp_dir, "feature.gpkg") + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", + message="You are attempting to write an empty DataFrame to file*", + category=UserWarning, + ) + data.to_file(tmp_gpkg, driver="GPKG", encoding="utf-8", layer=layer, index=False) + + if compress_level == 0: + with open(tmp_gpkg, "rb") as gpkg_file: + filesystem.writebytes(path, gpkg_file.read()) + else: + with filesystem.openbin(path, "w") as file: + with gzip.GzipFile(fileobj=file, compresslevel=compress_level, mode="wb") as gzip_file: + with open(tmp_gpkg, "rb") as gpkg_file: + shutil.copyfileobj(gpkg_file, gzip_file) + class FeatureIOJson(FeatureIOGZip[T]): """FeatureIO object specialized for JSON-like objects.""" diff --git a/pyproject.toml b/pyproject.toml index 9c794c78..90e73842 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,8 +47,8 @@ dependencies = [ "boto3", "fs", "fs-s3fs", - "geopandas>=0.14.4,<1; python_version>='3.9'", - "geopandas>=0.11.0,<1; python_version<'3.9'", + "geopandas>=0.14.4; python_version>='3.9'", + "geopandas>=0.11.0; python_version<'3.9'", "numpy>=1.20.0", "python-dateutil", "sentinelhub>=3.9.0",