Repository: ungarj/mapchete
Pull request #480 (2022-09-09 12:09:18)

add fs kwarg to makedirs(); extend IndexedFeatures capability to find object geometry


Patch:
diff --git a/mapchete/errors.py b/mapchete/errors.py
index 675897e..66c480a 100644
--- a/mapchete/errors.py
+++ b/mapchete/errors.py
@@ -55,3 +55,7 @@ class NoTaskGeometry(TypeError):
 
 class ReprojectionFailed(RuntimeError):
     """Raised when geometry cannot be reprojected."""
+
+
+class NoGeoError(AttributeError):
+    """Raised when object does not contain geographic information."""
diff --git a/mapchete/io/_path.py b/mapchete/io/_path.py
index f11da7f..dfce35e 100644
--- a/mapchete/io/_path.py
+++ b/mapchete/io/_path.py
@@ -90,7 +90,7 @@ def relative_path(path=None, base_dir=None):
         return os.path.relpath(path, base_dir)
 
 
-def makedirs(path):
+def makedirs(path, fs=None):
     """
     Silently create all subdirectories of path if path is local.
 
@@ -98,7 +98,7 @@ def makedirs(path):
     ----------
     path : path
     """
-    fs = fs_from_path(path)
+    fs = fs or fs_from_path(path)
     # create parent directories on local filesystems
     if fs.protocol == "file":
         fs.makedirs(path, exist_ok=True)
diff --git a/mapchete/io/vector.py b/mapchete/io/vector.py
index 0ac21b1..b2b1ac4 100644
--- a/mapchete/io/vector.py
+++ b/mapchete/io/vector.py
@@ -12,10 +12,11 @@ from rasterio.crs import CRS
 from shapely.geometry import box, mapping, shape
 from shapely.errors import TopologicalError
 from tilematrix import clip_geometry_to_srs_bounds
+from tilematrix._funcs import Bounds
 from itertools import chain
 import warnings
 
-from mapchete.errors import GeometryTypeError, MapcheteIOError
+from mapchete.errors import NoGeoError, MapcheteIOError
 from mapchete.io._misc import MAPCHETE_IO_RETRY_SETTINGS
 from mapchete.io._path import fs_from_path, path_exists, makedirs, copy
 from mapchete.io._geometry_operations import (
@@ -405,9 +406,13 @@ class IndexedFeatures:
             else:
                 id_ = self._get_feature_id(feature)
             self._items[id_] = feature
-            bounds = self._get_feature_bounds(
-                feature, allow_non_geo_objects=allow_non_geo_objects
-            )
+            try:
+                bounds = object_bounds(feature)
+            except NoGeoError:
+                if allow_non_geo_objects:
+                    bounds = None
+                else:
+                    raise
             if bounds is None:
                 self._non_geo_items.add(id_)
             else:
@@ -481,24 +486,29 @@ class IndexedFeatures:
             except TypeError:
                 raise TypeError("features need to have an id or have to be hashable")
 
-    def _get_feature_bounds(self, feature, allow_non_geo_objects=False):
-        try:
-            if hasattr(feature, "bounds"):
-                return validate_bounds(feature.bounds)
-            elif hasattr(feature, "__geo_interface__"):
-                return validate_bounds(shape(feature).bounds)
-            elif feature.get("bounds"):
-                return validate_bounds(feature["bounds"])
-            elif feature.get("geometry"):
-                return validate_bounds(to_shape(feature["geometry"]).bounds)
-            else:
-                raise TypeError("no bounds")
-        except Exception as exc:
-            if allow_non_geo_objects:
-                return None
-            else:
-                logger.exception(exc)
-                raise TypeError(f"cannot determine bounds from feature: {feature}")
+
+def object_bounds(obj) -> Bounds:
+    """
+    Determine geographic bounds from object if available.
+    """
+    try:
+        if hasattr(obj, "bounds"):
+            return validate_bounds(obj.bounds)
+        elif hasattr(obj, "__geo_interface__"):
+            return validate_bounds(shape(obj).bounds)
+        elif hasattr(obj, "geometry"):
+            return validate_bounds(to_shape(obj.geometry).bounds)
+        elif hasattr(obj, "bbox"):
+            return validate_bounds(obj.bbox)
+        elif obj.get("bounds"):
+            return validate_bounds(obj["bounds"])
+        elif obj.get("geometry"):
+            return validate_bounds(to_shape(obj["geometry"]).bounds)
+        else:
+            raise TypeError("no bounds")
+    except Exception as exc:
+        logger.exception(exc)
+        raise NoGeoError(f"cannot determine bounds from object: {obj}") from exc
 
 
 def convert_vector(inp, out, overwrite=False, exists_ok=True, **kwargs):
diff --git a/mapchete/validate.py b/mapchete/validate.py
index 81b9971..3429539 100644
--- a/mapchete/validate.py
+++ b/mapchete/validate.py
@@ -74,7 +74,7 @@ def validate_zoom(zoom):
     return zoom
 
 
-def validate_bounds(bounds):
+def validate_bounds(bounds) -> Bounds:
     """
     Return validated bounds.
 


Test patch:
diff --git a/test/test_io_vector.py b/test/test_io_vector.py
index 10c35fc..f1aa202 100644
--- a/test/test_io_vector.py
+++ b/test/test_io_vector.py
@@ -8,7 +8,12 @@ from shapely.errors import TopologicalError
 from shapely.geometry import shape, box, Polygon, MultiPolygon, LineString, mapping
 
 from mapchete.config import MapcheteConfig
-from mapchete.errors import GeometryTypeError, MapcheteIOError, ReprojectionFailed
+from mapchete.errors import (
+    GeometryTypeError,
+    MapcheteIOError,
+    NoGeoError,
+    ReprojectionFailed,
+)
 from mapchete.io.vector import (
     read_vector_window,
     reproject_geometry,
@@ -19,6 +24,7 @@ from mapchete.io.vector import (
     convert_vector,
     IndexedFeatures,
     bounds_intersect,
+    object_bounds,
 )
 from mapchete.tile import BufferedTilePyramid
 
@@ -381,7 +387,7 @@ def test_indexed_features_bounds():
 
     # no bounds
     feature = {"properties": {"foo": "bar"}, "id": 0}
-    with pytest.raises(TypeError):
+    with pytest.raises(NoGeoError):
         IndexedFeatures([feature])
 
 
@@ -502,3 +508,67 @@ def test_reproject_from_crs_wkt():
     dst_crs = "EPSG:4326"
     with pytest.raises(ReprojectionFailed):
         reproject_geometry(geom, src_crs, dst_crs).is_valid
+
+
+def test_object_bounds_attr_bounds():
+    # if hasattr(obj, "bounds"):
+    #     return validate_bounds(obj.bounds)
+    control = (0, 1, 2, 3)
+
+    class Foo:
+        bounds = control
+
+    assert object_bounds(Foo()) == (0, 1, 2, 3)
+
+
+def test_object_bounds_geo_interface():
+    # elif hasattr(obj, "__geo_interface__"):
+    #     return validate_bounds(shape(obj).bounds)
+    control = (0, 1, 2, 3)
+
+    class Foo:
+        __geo_interface__ = mapping(box(*control))
+
+    assert object_bounds(Foo()) == (0, 1, 2, 3)
+
+
+def test_object_bounds_attr_geometry():
+    # elif hasattr(obj, "geometry"):
+    #     return validate_bounds(to_shape(obj.geometry).bounds)
+    control = (0, 1, 2, 3)
+
+    class Foo:
+        geometry = mapping(box(*control))
+
+    assert object_bounds(Foo()) == (0, 1, 2, 3)
+
+
+def test_object_bounds_attr_bbox():
+    # elif hasattr(obj, "bbox"):
+    #     return validate_bounds(obj.bbox)
+    control = (0, 1, 2, 3)
+
+    class Foo:
+        bbox = control
+
+    assert object_bounds(Foo()) == (0, 1, 2, 3)
+
+
+def test_object_bounds_key_bbox():
+    # elif obj.get("bounds"):
+    #     return validate_bounds(obj["bounds"])
+    control = (0, 1, 2, 3)
+
+    foo = {"bounds": control}
+
+    assert object_bounds(foo) == (0, 1, 2, 3)
+
+
+def test_object_bounds_key_geometry():
+    # elif obj.get("geometry"):
+    #     return validate_bounds(to_shape(obj["geometry"]).bounds)
+    control = (0, 1, 2, 3)
+
+    foo = {"geometry": mapping(box(*control))}
+
+    assert object_bounds(foo) == (0, 1, 2, 3)