Repository: ungarj/mapchete
Pull request #475 (2022-07-07 11:57:41)

move format functions into subpackages


Patch:
diff --git a/.flake8 b/.flake8
index 1c58776..8d04eb1 100644
--- a/.flake8
+++ b/.flake8
@@ -1,6 +1,6 @@
 [flake8]
 max-line-length = 88
-ignore = E203, E266, E501, W503, F403, F401, E741
+ignore = E203, E266, E501, W503, F403, F401, E741, F821
 max-complexity = 18
 select = B,C,E,F,W,T4,B9
 exclude = test/testdata/syntax_error.py
\ No newline at end of file
diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml
index 6d55f1c..8fa6275 100644
--- a/.github/workflows/python-package.yml
+++ b/.github/workflows/python-package.yml
@@ -35,7 +35,7 @@ jobs:
         sudo apt-get -y update
         sudo apt-get install -y gdal-bin python-tk libgdal-dev libproj-dev libgeos-dev
         python -m pip install --upgrade pip wheel
-        pip install -r test/requirements.txt
+        pip install -r test/requirements.txt -r requirements.txt
         pip install -e .[complete]
 
     - name: Lint with flake8
diff --git a/mapchete/formats/__init__.py b/mapchete/formats/__init__.py
index 3bcfae1..9aacb20 100644
--- a/mapchete/formats/__init__.py
+++ b/mapchete/formats/__init__.py
@@ -1,292 +1,35 @@
-"""
-Functions handling output formats.
-
-This module deserves a cleaner rewrite some day.
-"""
-
-import fiona
-import logging
-import os
-from pprint import pformat
-import rasterio
-from rasterio.crs import CRS
-import warnings
-
-from mapchete.errors import MapcheteConfigError, MapcheteDriverError
-from mapchete.io import read_json, write_json, path_exists
-from mapchete._registered import drivers
-from mapchete.tile import BufferedTilePyramid
-
-
-logger = logging.getLogger(__name__)
-
-
-def available_output_formats():
-    """
-    Return all available output formats.
-
-    Returns
-    -------
-    formats : list
-        all available output formats
-    """
-    output_formats = {}
-    for v in drivers:
-        driver_ = v.load()
-        if hasattr(driver_, "METADATA") and (driver_.METADATA["mode"] in ["w", "rw"]):
-            output_formats[driver_.METADATA["driver_name"]] = driver_.METADATA
-    return output_formats
-
-
-def available_input_formats():
-    """
-    Return all available input formats.
-
-    Returns
-    -------
-    formats : list
-        all available input formats
-    """
-    input_formats = {}
-    for v in drivers:
-        logger.debug("driver found: %s", v)
-        driver_ = v.load()
-        if hasattr(driver_, "METADATA") and (driver_.METADATA["mode"] in ["r", "rw"]):
-            input_formats[driver_.METADATA["driver_name"]] = driver_.METADATA
-    return input_formats
-
-
-def load_output_reader(output_params):
-    """
-    Return OutputReader class of driver.
-
-    Returns
-    -------
-    output : ``OutputDataReader``
-        output reader object
-    """
-    if not isinstance(output_params, dict):
-        raise TypeError("output_params must be a dictionary")
-    driver_name = output_params["format"]
-    for v in drivers:
-        _driver = v.load()
-        if all(
-            [hasattr(_driver, attr) for attr in ["OutputDataReader", "METADATA"]]
-        ) and (_driver.METADATA["driver_name"] == driver_name):
-            return _driver.OutputDataReader(output_params, readonly=True)
-    raise MapcheteDriverError("no loader for driver '%s' could be found." % driver_name)
-
-
-def load_output_writer(output_params, readonly=False):
-    """
-    Return output class of driver.
-
-    Returns
-    -------
-    output : ``OutputDataWriter``
-        output writer object
-    """
-    if not isinstance(output_params, dict):
-        raise TypeError("output_params must be a dictionary")
-    driver_name = output_params["format"]
-    for v in drivers:
-        _driver = v.load()
-        if all(
-            [hasattr(_driver, attr) for attr in ["OutputDataWriter", "METADATA"]]
-        ) and (_driver.METADATA["driver_name"] == driver_name):
-            return _driver.OutputDataWriter(output_params, readonly=readonly)
-    raise MapcheteDriverError("no loader for driver '%s' could be found." % driver_name)
-
-
-def load_input_reader(input_params, readonly=False, input_key=None):
-    """
-    Return input class of driver.
-
-    Returns
-    -------
-    input_params : ``InputData``
-        input parameters
-    """
-    logger.debug("find input reader with params %s", input_params)
-    if not isinstance(input_params, dict):
-        raise TypeError("input_params must be a dictionary")
-    if "abstract" in input_params:
-        driver_name = input_params["abstract"]["format"]
-    elif "path" in input_params:
-        if os.path.splitext(input_params["path"])[1]:
-            input_file = input_params["path"]
-            driver_name = driver_from_file(input_file)
-        else:
-            logger.debug("%s is a directory", input_params["path"])
-            driver_name = "TileDirectory"
-    else:
-        raise MapcheteDriverError("invalid input parameters %s" % input_params)
-    for v in drivers:
-        driver_ = v.load()
-        if hasattr(driver_, "METADATA") and (
-            driver_.METADATA["driver_name"] == driver_name
-        ):
-            return driver_.InputData(
-                input_params, readonly=readonly, input_key=input_key
-            )
-    raise MapcheteDriverError("no loader for driver '%s' could be found." % driver_name)
-
-
-def driver_metadata(driver_name):
-    """Return driver metadata."""
-    for v in drivers:
-        driver_ = v.load()
-        if hasattr(driver_, "METADATA") and (
-            driver_.METADATA["driver_name"] == driver_name
-        ):
-            return dict(driver_.METADATA)
-    else:  # pragma: no cover
-        raise ValueError(f"driver '{driver_name}' not found")
-
-
-def driver_from_file(input_file, quick=True):
-    """
-    Guess driver from file by opening it.
-
-    Returns
-    -------
-    driver : string
-        driver name
-    """
-    file_ext = os.path.splitext(input_file)[1].split(".")[1]
-
-    # mapchete files can immediately be returned:
-    if file_ext == "mapchete":
-        return "Mapchete"
-
-    # use the most common file extensions to quickly determine input driver for file:
-    if quick:
-        if file_ext in ["tif", "jp2"]:
-            return "raster_file"
-        if file_ext in ["shp", "geojson", "gpkg"]:
-            return "vector_file"
-
-    # brute force by trying to open file with rasterio and fiona:
-    try:
-        logger.debug("try to open %s with rasterio...", input_file)
-        with rasterio.open(input_file):  # pragma: no cover
-            return "raster_file"
-    except Exception as rio_exception:
-        try:
-            logger.debug("try to open %s with fiona...", input_file)
-            with fiona.open(input_file):  # pragma: no cover
-                return "vector_file"
-        except Exception as fio_exception:
-            if path_exists(input_file):
-                logger.exception(f"fiona error: {fio_exception}")
-                logger.exception(f"rasterio error: {rio_exception}")
-                raise MapcheteDriverError(
-                    "%s has an unknown file extension or could not be opened by neither "
-                    "rasterio nor fiona." % input_file
-                )
-            else:
-                raise FileNotFoundError("%s does not exist" % input_file)
-
-
-def data_type_from_extension(file_extension):
-    """
-    Guess data_type (raster or vector) from file extension.
-
-    Returns
-    -------
-    driver : string
-        driver name
-    """
-    for v in drivers:
-        driver = v.load()
-        try:
-            driver_extensions = driver.METADATA.get("file_extensions")
-            if driver_extensions and file_extension in driver_extensions:
-                return driver.METADATA["data_type"]
-        except AttributeError:  # pragma: no cover
-            pass
-    else:
-        raise ValueError(
-            f"data type for file extension {file_extension} could not be found"
-        )
-
-
-def params_to_dump(params):
-    # in case GridDefinition was not yet initialized
-    return dict(
-        pyramid=BufferedTilePyramid(
-            grid=params["grid"],
-            tile_size=params.get("tile_size", 256),
-            metatiling=params.get("metatiling", 1),
-            pixelbuffer=params.get("pixelbuffer", 0),
-        ).to_dict(),
-        driver={
-            k: v
-            for k, v in params.items()
-            if k not in ["path", "grid", "pixelbuffer", "metatiling"]
-        },
-    )
-
-
-def read_output_metadata(metadata_json, **kwargs):
-    params = read_json(metadata_json, **kwargs)
-    grid = params["pyramid"]["grid"]
-    if grid["type"] == "geodetic" and grid["shape"] == [2, 1]:  # pragma: no cover
-        warnings.warn(
-            DeprecationWarning(
-                "Deprecated grid shape ordering found. "
-                "Please change grid shape from [2, 1] to [1, 2] in %s." % metadata_json
-            )
-        )
-        params["pyramid"]["grid"]["shape"] = [1, 2]
-    if "crs" in grid and isinstance(grid["crs"], str):
-        crs = CRS.from_string(grid["crs"])
-        warnings.warn(
-            DeprecationWarning(
-                "Deprecated 'srs' found in %s: '%s'. "
-                "Use WKT representation instead: %s"
-                % (metadata_json, grid["crs"], pformat(dict(wkt=crs.to_wkt())))
-            )
-        )
-        params["pyramid"]["grid"].update(srs=dict(wkt=crs.to_wkt()))
-    params.update(
-        pyramid=BufferedTilePyramid(
-            params["pyramid"]["grid"],
-            metatiling=params["pyramid"].get("metatiling", 1),
-            tile_size=params["pyramid"].get("tile_size", 256),
-            pixelbuffer=params["pyramid"].get("pixelbuffer", 0),
-        )
-    )
-    return params
-
-
-def write_output_metadata(output_params):
-    """Dump output JSON and verify parameters if output metadata exist."""
-    if "path" in output_params:
-        metadata_path = os.path.join(output_params["path"], "metadata.json")
-        logger.debug("check for output %s", metadata_path)
-        try:
-            existing_params = read_output_metadata(metadata_path)
-            logger.debug("%s exists", metadata_path)
-            logger.debug("existing output parameters: %s", pformat(existing_params))
-            existing_tp = existing_params["pyramid"]
-            current_params = params_to_dump(output_params)
-            logger.debug("current output parameters: %s", pformat(current_params))
-            current_tp = BufferedTilePyramid(**current_params["pyramid"])
-            if existing_tp != current_tp:  # pragma: no cover
-                raise MapcheteConfigError(
-                    "pyramid definitions between existing and new output do not match: "
-                    "%s != %s" % (existing_tp, current_tp)
-                )
-            existing_format = existing_params["driver"]["format"]
-            current_format = current_params["driver"]["format"]
-            if existing_format != current_format:  # pragma: no cover
-                raise MapcheteConfigError(
-                    "existing output format does not match new output format: "
-                    "%s != %s" % ((existing_format, current_format))
-                )
-        except FileNotFoundError:
-            logger.debug("%s does not exist", metadata_path)
-            dump_params = params_to_dump(output_params)
-            # dump output metadata
-            write_json(metadata_path, dump_params)
+from mapchete.formats.loaders import (
+    load_input_reader,
+    load_output_reader,
+    load_output_writer,
+)
+from mapchete.formats.tools import (
+    available_input_formats,
+    available_output_formats,
+    driver_metadata,
+    driver_from_extension,
+    driver_from_file,
+    data_type_from_extension,
+    dump_metadata,
+    load_metadata,
+    read_output_metadata,
+    write_output_metadata,
+    compare_metadata_params,
+)
+
+__all__ = [
+    "available_input_formats",
+    "available_output_formats",
+    "load_input_reader",
+    "load_output_reader",
+    "load_output_writer",
+    "driver_metadata",
+    "driver_from_extension",
+    "driver_from_file",
+    "data_type_from_extension",
+    "dump_metadata",
+    "load_metadata",
+    "read_output_metadata",
+    "write_output_metadata",
+    "compare_metadata_params",
+]
diff --git a/mapchete/formats/default/tile_directory.py b/mapchete/formats/default/tile_directory.py
index 14edf44..715c686 100644
--- a/mapchete/formats/default/tile_directory.py
+++ b/mapchete/formats/default/tile_directory.py
@@ -14,7 +14,7 @@ from mapchete.formats import (
     read_output_metadata,
 )
 from mapchete.io import path_exists, absolute_path, tile_to_zoom_level
-from mapchete.io.vector import reproject_geometry, segmentize_geometry
+from mapchete.io.vector import reproject_geometry
 from mapchete.tile import BufferedTilePyramid
 
 
@@ -129,10 +129,12 @@ class InputData(base.InputData):
 
         # additional params
         self._bounds = self._params.get("bounds", self.td_pyramid.bounds)
-        self.METADATA.update(
-            data_type=self._data_type, file_extensions=[self._params["extension"]]
+        self._metadata = dict(
+            self.METADATA,
+            data_type=self._data_type,
+            file_extensions=[self._params["extension"]],
         )
-        if self.METADATA.get("data_type") == "raster":
+        if self._metadata.get("data_type") == "raster":
             self._params["count"] = self._params.get(
                 "count", self._params.get("bands", None)
             )
@@ -160,7 +162,7 @@ class InputData(base.InputData):
         """
         return InputTile(
             tile,
-            data_type=self.METADATA.get("data_type"),
+            data_type=self._metadata.get("data_type"),
             basepath=self.path,
             file_extension=self._ext,
             profile=self._profile,
diff --git a/mapchete/formats/loaders.py b/mapchete/formats/loaders.py
new file mode 100644
index 0000000..3964234
--- /dev/null
+++ b/mapchete/formats/loaders.py
@@ -0,0 +1,118 @@
+import logging
+import os
+from typing import Dict, Optional, TYPE_CHECKING
+
+# from mapchete.formats.base import OutputDataReader, OutputDataWriter, InputData
+from mapchete.formats.tools import driver_from_file
+from mapchete.errors import MapcheteDriverError
+from mapchete._registered import drivers
+
+
+logger = logging.getLogger(__name__)
+
+
+def load_output_reader(output_params: Dict) -> "OutputDataReader":
+    """
+    Return OutputReader class of driver.
+
+    Parameters
+    ----------
+    output_params : dict
+        Output parameters as given in mapchete configuration.
+
+    Returns
+    -------
+    output : ``OutputDataReader``
+        output reader object
+    """
+    if not isinstance(output_params, dict):
+        raise TypeError("output_params must be a dictionary")
+    driver_name = output_params["format"]
+    for v in drivers:
+        _driver = v.load()
+        if all(
+            [hasattr(_driver, attr) for attr in ["OutputDataReader", "METADATA"]]
+        ) and (_driver.METADATA["driver_name"] == driver_name):
+            return _driver.OutputDataReader(output_params, readonly=True)
+    raise MapcheteDriverError("no loader for driver '%s' could be found." % driver_name)
+
+
+def load_output_writer(
+    output_params: Dict, readonly: bool = False
+) -> "OutputDataWriter":
+    """
+    Return output class of driver.
+
+    Parameters
+    ----------
+    output_params : dict
+        Output parameters as given in mapchete configuration.
+    readonly : bool
+        Initialize as readonly. (default: False)
+
+    Returns
+    -------
+    output : ``OutputDataWriter``
+        output writer object
+    """
+    if not isinstance(output_params, dict):
+        raise TypeError("output_params must be a dictionary")
+    driver_name = output_params["format"]
+    for v in drivers:
+        _driver = v.load()
+        if all(
+            [hasattr(_driver, attr) for attr in ["OutputDataWriter", "METADATA"]]
+        ) and (_driver.METADATA["driver_name"] == driver_name):
+            return _driver.OutputDataWriter(output_params, readonly=readonly)
+    raise MapcheteDriverError("no loader for driver '%s' could be found." % driver_name)
+
+
+def load_input_reader(
+    input_params: Dict, readonly: bool = False, input_key: Optional[str] = None
+) -> "InputData":
+    """
+    Return input class of driver.
+
+    Parameters
+    ----------
+    input_params : dict
+        Input parameters as given in mapchete configuration.
+    readonly : bool
+        Initialize as readonly. (default: False)
+    input_key : str
+        Pass on unique input key assigned by mapchete.
+
+    Returns
+    -------
+    input_params : ``InputData``
+        input parameters
+    """
+    logger.debug("find input reader with params %s", input_params)
+    if not isinstance(input_params, dict):
+        raise TypeError("input_params must be a dictionary")
+
+    # find out driver name
+    if "abstract" in input_params:
+        driver_name = input_params["abstract"]["format"]
+    elif "path" in input_params:
+        # if path has a file extension it is considered a single file
+        if os.path.splitext(input_params["path"])[1]:
+            input_file = input_params["path"]
+            driver_name = driver_from_file(input_file)
+        # else a TileDirectory is assumed
+        else:
+            logger.debug("%s is a directory", input_params["path"])
+            driver_name = "TileDirectory"
+    else:
+        raise MapcheteDriverError("invalid input parameters %s" % input_params)
+
+    # load this driver module and try to initialize an InputData object
+    for v in drivers:
+        driver_ = v.load()
+        if hasattr(driver_, "METADATA") and (
+            driver_.METADATA["driver_name"] == driver_name
+        ):
+            return driver_.InputData(
+                input_params, readonly=readonly, input_key=input_key
+            )
+    raise MapcheteDriverError("no loader for driver '%s' could be found." % driver_name)
diff --git a/mapchete/formats/tools.py b/mapchete/formats/tools.py
new file mode 100644
index 0000000..cfb03c7
--- /dev/null
+++ b/mapchete/formats/tools.py
@@ -0,0 +1,415 @@
+"""
+Functions handling output formats.
+
+This module deserves a cleaner rewrite some day.
+"""
+
+import datetime
+import dateutil.parser
+import fiona
+import logging
+import os
+from pprint import pformat
+import rasterio
+from rasterio.crs import CRS
+from typing import Dict, Type
+import warnings
+
+from mapchete.errors import MapcheteConfigError, MapcheteDriverError
+from mapchete.io import read_json, write_json, path_exists
+from mapchete._registered import drivers
+from mapchete.tile import BufferedTilePyramid
+
+
+logger = logging.getLogger(__name__)
+
+
+def available_output_formats() -> Dict:
+    """
+    Return all available output formats.
+
+    Returns
+    -------
+    formats : list
+        all available output formats
+    """
+    output_formats = {}
+    for v in drivers:
+        driver_ = v.load()
+        if hasattr(driver_, "METADATA") and (driver_.METADATA["mode"] in ["w", "rw"]):
+            output_formats[driver_.METADATA["driver_name"]] = driver_.METADATA
+    return output_formats
+
+
+def available_input_formats() -> Dict:
+    """
+    Return all available input formats.
+
+    Returns
+    -------
+    formats : list
+        all available input formats
+    """
+    input_formats = {}
+    for v in drivers:
+        logger.debug("driver found: %s", v)
+        driver_ = v.load()
+        if hasattr(driver_, "METADATA") and (driver_.METADATA["mode"] in ["r", "rw"]):
+            input_formats[driver_.METADATA["driver_name"]] = driver_.METADATA
+    return input_formats
+
+
+def driver_metadata(driver_name: str) -> Dict:
+    """
+    Return driver metadata.
+
+    Parameters
+    ----------
+    driver_name : str
+        Name of driver.
+
+    Returns
+    -------
+    Driver metadata as dictionary.
+    """
+    for v in drivers:
+        driver_ = v.load()
+        if hasattr(driver_, "METADATA") and (
+            driver_.METADATA["driver_name"] == driver_name
+        ):
+            return dict(driver_.METADATA)
+    else:  # pragma: no cover
+        raise ValueError(f"driver '{driver_name}' not found")
+
+
+def driver_from_file(input_file: str, quick: bool = True) -> str:
+    """
+    Guess driver from file by opening it.
+
+    Parameters
+    ----------
+    input_file : str
+        Path to file.
+    quick : bool
+        Try to guess driver from known file extensions instead of trying to open with
+        fiona and rasterio. (default: True)
+
+    Returns
+    -------
+    driver : string
+        driver name
+    """
+    file_ext = os.path.splitext(input_file)[1].split(".")[1]
+
+    # mapchete files can immediately be returned:
+    if file_ext == "mapchete":
+        return "Mapchete"
+
+    # use the most common file extensions to quickly determine input driver for file:
+    if quick:
+        try:
+            return driver_from_extension(file_ext)
+        except ValueError:
+            pass
+
+    # brute force by trying to open file with rasterio and fiona:
+    try:
+        logger.debug("try to open %s with rasterio...", input_file)
+        with rasterio.open(input_file):  # pragma: no cover
+            return "raster_file"
+    except Exception as rio_exception:
+        try:
+            logger.debug("try to open %s with fiona...", input_file)
+            with fiona.open(input_file):  # pragma: no cover
+                return "vector_file"
+        except Exception as fio_exception:
+            if path_exists(input_file):
+                logger.exception(f"fiona error: {fio_exception}")
+                logger.exception(f"rasterio error: {rio_exception}")
+                raise MapcheteDriverError(
+                    "%s has an unknown file extension and could not be opened by neither "
+                    "rasterio nor fiona." % input_file
+                )
+            else:
+                raise FileNotFoundError("%s does not exist" % input_file)
+
+
+def driver_from_extension(file_extension: str) -> str:
+    """
+    Guess driver name from file extension.
+
+    Paramters
+    ---------
+    file_extension : str
+        File extension to look for.
+
+    Returns
+    -------
+    driver : string
+        driver name
+    """
+    all_drivers_extensions = {}
+    for v in drivers:
+        driver = v.load()
+        try:
+            driver_extensions = driver.METADATA.get("file_extensions", []).copy()
+            all_drivers_extensions[driver.METADATA["driver_name"]] = driver_extensions
+            if driver_extensions and file_extension in driver_extensions:
+                return driver.METADATA["driver_name"]
+        except AttributeError:  # pragma: no cover
+            pass
+    else:
+        raise ValueError(
+            f"driver name for file extension {file_extension} could not be found: {all_drivers_extensions}"
+        )
+
+
+def data_type_from_extension(file_extension: str) -> str:
+    """
+    Guess data_type (raster or vector) from file extension.
+
+    Paramters
+    ---------
+    file_extension : str
+        File extension to look for.
+
+    Returns
+    -------
+    driver data type : string
+        driver data type
+    """
+    for v in drivers:
+        driver = v.load()
+        try:
+            driver_extensions = driver.METADATA.get("file_extensions", [])
+            if driver_extensions and file_extension in driver_extensions:
+                return driver.METADATA["data_type"]
+        except AttributeError:  # pragma: no cover
+            pass
+    else:
+        raise ValueError(
+            f"data type for file extension {file_extension} could not be found"
+        )
+
+
+def dump_metadata(params: Dict, parse_datetime=True) -> Dict:
+    """
+    Transform params to JSON-serializable dictionary for a metadata.json file.
+
+    This also converts the BufferedTilePyramid to a dictionary and any datetime
+    objects into strings.
+
+    Parameters
+    ----------
+    params : dict
+        Mapping of process parameters
+
+    Returns
+    -------
+    Dictionary of output parameters ready to be written as metadata.json.
+    """
+    # in case GridDefinition was not yet initialized
+    out = dict(
+        pyramid=BufferedTilePyramid(
+            grid=params["grid"],
+            tile_size=params.get("tile_size", 256),
+            metatiling=params.get("metatiling", 1),
+            pixelbuffer=params.get("pixelbuffer", 0),
+        ).to_dict(),
+        driver={
+            k: v
+            for k, v in params.items()
+            if k not in ["path", "grid", "pixelbuffer", "metatiling"]
+        },
+    )
+
+    def _datetime_to_str(value):
+        return value.isoformat()
+
+    strategies = [
+        ((datetime.date, datetime.datetime), _datetime_to_str),
+    ]
+    return _unparse_dict(out, strategies=strategies) if parse_datetime else out
+
+
+def read_output_metadata(metadata_json: str, **kwargs: str) -> Dict:
+    """
+    Read and parse metadata.json.
+
+    Parameters
+    ----------
+    metadata_json : str
+        Path to metadata.json file.
+
+    Returns
+    -------
+        Parsed output metadata.
+    """
+
+    return load_metadata(read_json(metadata_json, **kwargs))
+
+
+def load_metadata(params: Dict, parse_known_types=True) -> Dict:
+    """
+    Parse output metadata dictionary.
+
+    This function raises DeprecationWarning instances if needed and initializes the
+    BufferedTilePyramid as well as datetime objects.
+
+    Parameters
+    ----------
+    params : dict
+        Output metadata parameters.
+
+    Returns
+    -------
+    out_params : dict
+        Output metadata parameters with initialized BufferedTilePyramid.
+    """
+    if not isinstance(params, dict):  # pragma: no cover
+        raise TypeError(f"metadata parameters must be a dictionary, not {params}")
+    out = params.copy()
+    grid = out["pyramid"]["grid"]
+    if grid["type"] == "geodetic" and grid["shape"] == [2, 1]:  # pragma: no cover
+        warnings.warn(
+            DeprecationWarning(
+                "Deprecated grid shape ordering found. "
+                "Please change grid shape from [2, 1] to [1, 2]."
+            )
+        )
+        out["pyramid"]["grid"]["shape"] = [1, 2]
+    if "crs" in grid and isinstance(grid["crs"], str):
+        crs = CRS.from_string(grid["crs"])
+        warnings.warn(
+            DeprecationWarning(
+                "Deprecated 'srs' found in: '%s'. "
+                "Use WKT representation instead: %s"
+                % (grid["crs"], pformat(dict(wkt=crs.to_wkt())))
+            )
+        )
+        out["pyramid"]["grid"].update(srs=dict(wkt=crs.to_wkt()))
+
+    out.update(
+        pyramid=BufferedTilePyramid(
+            out["pyramid"]["grid"],
+            metatiling=out["pyramid"].get("metatiling", 1),
+            tile_size=out["pyramid"].get("tile_size", 256),
+            pixelbuffer=out["pyramid"].get("pixelbuffer", 0),
+        )
+    )
+
+    strategies = [
+        # create datetime objects and skip on these allowed exceptions
+        (dateutil.parser.parse, (dateutil.parser.ParserError, TypeError)),
+    ]
+    return _parse_dict(out, strategies=strategies) if parse_known_types else out
+
+
+def write_output_metadata(output_params: Dict) -> None:
+    """
+    Write output JSON and verify parameters if output metadata exist.
+
+    Parameters
+    ----------
+    output_params : dict
+        Output parameters
+    """
+    if "path" in output_params:
+        metadata_path = os.path.join(output_params["path"], "metadata.json")
+        logger.debug("check for output %s", metadata_path)
+        try:
+            existing_params = read_output_metadata(metadata_path)
+            logger.debug("%s exists", metadata_path)
+            logger.debug("existing output parameters: %s", pformat(existing_params))
+            current_params = dump_metadata(output_params)
+            logger.debug("current output parameters: %s", pformat(current_params))
+            compare_metadata_params(existing_params, current_params)
+        except FileNotFoundError:
+            logger.debug("%s does not exist", metadata_path)
+            dump_params = dump_metadata(output_params)
+            # dump output metadata
+            write_json(metadata_path, dump_params)
+
+
+def compare_metadata_params(params1: Dict, params2: Dict) -> None:
+    """
+    Verify that both mappings of output metadata parameters are compatible.
+
+    Parameters
+    ----------
+    params1 : dict
+        Output metadata parameters.
+    params2 : dict
+        Output metadata parameters.
+    """
+
+    def _buffered_pyramid(pyramid):
+        if isinstance(pyramid, dict):
+            return BufferedTilePyramid(**pyramid)
+        else:
+            return pyramid
+
+    params1_tp = _buffered_pyramid(params1["pyramid"])
+    params2_tp = _buffered_pyramid(params2["pyramid"])
+    if params1_tp != params2_tp:  # pragma: no cover
+        raise MapcheteConfigError(
+            "pyramid definitions between existing and new output do not match: "
+            "%s != %s" % (params1_tp, params2_tp)
+        )
+    if params1["driver"]["format"] != params2["driver"]["format"]:  # pragma: no cover
+        raise MapcheteConfigError(
+            "existing output format does not match new output format: "
+            "%s != %s" % ((params1["driver"]["format"], params2["driver"]["format"]))
+        )
+
+
+def _parse_dict(d, strategies=None):
+    """Iterate through dictionary and try to parse values according to strategies."""
+
+    def _parse_val(val):
+        for func, allowed_exception in strategies:
+            try:
+                return func(val)
+            except allowed_exception:
+                pass
+        else:
+            return val
+
+    strategies = strategies or []
+    out = dict()
+    for k, v in d.items():
+        if isinstance(v, dict):
+            v = _parse_dict(v, strategies=strategies)
+        elif isinstance(v, list):
+            v = [_parse_val(val) for val in v]
+        elif isinstance(v, tuple):
+            v = tuple(_parse_val(val) for val in v)
+        else:
+            v = _parse_val(v)
+        out[k] = v
+    return out
+
+
+def _unparse_dict(d, strategies=None):
+    """Iterate through dictionary and try to unparse values according to strategies."""
+
+    def _unparse_val(val):
+        for instance_type, func in strategies:
+            if isinstance(val, instance_type):
+                return func(val)
+        else:
+            return val
+
+    strategies = strategies or []
+    out = dict()
+    for k, v in d.items():
+        if isinstance(v, dict):
+            v = _unparse_dict(v, strategies=strategies)
+        elif isinstance(v, list):
+            v = [_unparse_val(val) for val in v]
+        elif isinstance(v, tuple):
+            v = tuple(_unparse_val(val) for val in v)
+        else:
+            v = _unparse_val(v)
+        out[k] = v
+    return out
diff --git a/requirements.txt b/requirements.txt
index fbd9087..50a7d2f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -7,7 +7,7 @@ cached-property>=1.5.1
 click>=7.1.1
 click-plugins>=1.1.1
 click-spinner>=0.1.10
-fiona>=1.8.13.post1
+fiona>=1.8.13.post1,<=1.8.21
 Flask>=1.1.2
 fsspec[s3]>=0.8.4
 geobuf>=1.1.1
@@ -19,7 +19,7 @@ numpy>=1.16
 oyaml>=0.9
 pyproj
 retry>=0.9.2
-rasterio>=1.0.28
+rasterio>=1.0.28,<1.3.0
 s3fs>=0.5.2
 Shapely>=1.7.1,<1.8.1
 tilematrix>=2022.3.0


Test patch:
diff --git a/test/conftest.py b/test/conftest.py
index 61c7b11..c02c079 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -1,7 +1,7 @@
 """All pytest fixtures."""
 
-import boto3
 from collections import namedtuple
+import datetime
 import os
 import pytest
 from shapely import wkt
@@ -9,6 +9,8 @@ import shutil
 import uuid
 import yaml
 
+from tilematrix import Bounds, GridDefinition
+
 from mapchete.cli.default.serve import create_app
 from mapchete.io import fs_from_path
 from mapchete.testing import ProcessFixture, dict_from_mapchete
@@ -159,6 +161,78 @@ def old_geodetic_shape_metadata_json():
     return os.path.join(TESTDATA_DIR, "old_geodetic_shape_metadata.json")
 
 
+@pytest.fixture
+def driver_metadata_dict():
+    """Content of a metadata.json file as dictionary."""
+    return {
+        "driver": {
+            "bands": 1,
+            "compress": "deflate",
+            "delimiters": {
+                "bounds": [-180.0, -90.0, 180.0, 90.0],
+                "effective_bounds": [-180.17578125, -90.0, 180.17578125, 90.0],
+                "process_bounds": [-180.0, -90.0, 180.0, 90.0],
+                "zoom": [0, 1, 2, 3, 4, 5, 6, 7, 8],
+            },
+            "dtype": "float32",
+            "format": "GTiff",
+            "mode": "continue",
+            "nodata": -32768.0,
+            "predictor": 3,
+        },
+        "pyramid": {
+            "grid": {
+                "bounds": [-180.0, -90.0, 180.0, 90.0],
+                "is_global": True,
+                "shape": [1, 2],
+                "srs": {
+                    "wkt": 'GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]]'
+                },
+                "type": "geodetic",
+            },
+            "metatiling": 8,
+            "pixelbuffer": 64,
+            "tile_size": 256,
+        },
+    }
+
+
+@pytest.fixture
+def driver_output_params_dict():
+    return {
+        "format": "xarray",
+        "path": "/foo/bar",
+        "dtype": "uint16",
+        "bands": 3,
+        "chunksize": 256,
+        "time": {
+            "start": datetime.date(2022, 6, 1),
+            "end": datetime.date(2022, 6, 11),
+            "steps": [
+                datetime.date(2022, 6, 1),
+                datetime.date(2022, 6, 4),
+                datetime.date(2022, 6, 6),
+                datetime.date(2022, 6, 9),
+                datetime.date(2022, 6, 11),
+            ],
+        },
+        "grid": GridDefinition("geodetic"),
+        "pixelbuffer": 0,
+        "metatiling": 1,
+        "delimiters": {
+            "zoom": [5],
+            "bounds": Bounds(left=15.0064, bottom=47.7604, right=16.4863, top=48.7485),
+            "process_bounds": Bounds(
+                left=15.0064, bottom=47.7604, right=16.4863, top=48.7485
+            ),
+            "effective_bounds": Bounds(
+                left=11.25, bottom=45.0, right=16.875, top=50.625
+            ),
+        },
+        "mode": "continue",
+    }
+
+
 @pytest.fixture
 def landpoly():
     """Fixture for landpoly.geojson."""
diff --git a/test/test_formats.py b/test/test_formats.py
index c1698c8..cb80dfe 100644
--- a/test/test_formats.py
+++ b/test/test_formats.py
@@ -1,8 +1,9 @@
 """Test Mapchete default formats."""
 
+import datetime
 import pytest
-from tilematrix import TilePyramid
 from rasterio.crs import CRS
+from tilematrix import TilePyramid
 
 import mapchete
 from mapchete import errors
@@ -11,11 +12,15 @@ from mapchete.formats import (
     available_output_formats,
     base,
     driver_from_file,
+    driver_from_extension,
     load_input_reader,
     load_output_reader,
     load_output_writer,
     read_output_metadata,
+    load_metadata,
+    dump_metadata,
 )
+from mapchete.tile import BufferedTilePyramid
 
 
 def test_available_input_formats():
@@ -58,6 +63,22 @@ def test_input_reader_errors():
         load_input_reader({"abstract": {"format": "invalid_format"}})
 
 
+def test_driver_from_file_tif():
+    assert driver_from_file("some.tif") == "raster_file"
+
+
+def test_driver_from_file_jp2():
+    assert driver_from_file("some.jp2") == "raster_file"
+
+
+def test_driver_from_file_geojson():
+    assert driver_from_file("some.geojson") == "vector_file"
+
+
+def test_driver_from_file_shp():
+    assert driver_from_file("some.shp") == "vector_file"
+
+
 def test_driver_from_file_errors(execute_kwargs_py):
     """Test errors when determining input driver from filename."""
     with pytest.raises(errors.MapcheteDriverError):
@@ -168,3 +189,57 @@ def test_old_style_metadata(old_style_metadata_json, old_geodetic_shape_metadata
     with pytest.deprecated_call():
         params = read_output_metadata(old_geodetic_shape_metadata_json)
         assert params["pyramid"].grid.type == "geodetic"
+
+
+def test_driver_from_extension_tif():
+    assert driver_from_extension("tif") == "raster_file"
+
+
+def test_driver_from_extension_jp2():
+    assert driver_from_extension("jp2") == "raster_file"
+
+
+def test_driver_from_extension_geojson():
+    assert driver_from_extension("geojson") == "vector_file"
+
+
+def test_driver_from_extension_shp():
+    assert driver_from_extension("shp") == "vector_file"
+
+
+def test_driver_from_extension_invalid():
+    with pytest.raises(ValueError):
+        driver_from_extension("invalid")
+
+
+def test_load_metadata_pyramid(driver_metadata_dict):
+    loaded = load_metadata(driver_metadata_dict)
+    assert isinstance(loaded["pyramid"], BufferedTilePyramid)
+
+
+def test_dump_metadata_pyramid(driver_output_params_dict):
+    dumped = dump_metadata(driver_output_params_dict)
+    assert isinstance(dumped, dict)
+
+
+def test_dump_metadata_datetime(driver_output_params_dict):
+    dumped = dump_metadata(driver_output_params_dict)
+    assert isinstance(dumped["driver"]["time"]["start"], str)
+    assert isinstance(dumped["driver"]["time"]["end"], str)
+
+
+def test_dump_metadata_datetime_list(driver_output_params_dict):
+    dumped = dump_metadata(driver_output_params_dict)
+    for t in dumped["driver"]["time"]["steps"]:
+        assert isinstance(t, str)
+
+
+def test_load_metadata_datetime(driver_output_params_dict):
+    loaded = load_metadata(dump_metadata(driver_output_params_dict))
+    assert isinstance(loaded["driver"]["time"]["start"], datetime.date)
+
+
+def test_load_metadata_datetime_list(driver_output_params_dict):
+    loaded = load_metadata(dump_metadata(driver_output_params_dict))
+    for t in loaded["driver"]["time"]["steps"]:
+        assert isinstance(t, datetime.date)