Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable simple region checking for cli #664

Merged
merged 15 commits into from
Dec 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions data/catalogs/deltares_data.yml
Original file line number Diff line number Diff line change
Expand Up @@ -761,7 +761,7 @@ ghs_pop_2015_4326_v2019a:
path: socio_economic/ghs/GHS_POP_E2015_GLOBE_R2019A_4326_9ss_V1_0.tif

ghs_pop_2015_54009_v2019a:
crs: 54009
crs: "ESRI:54009"
data_type: RasterDataset
driver: raster
kwargs:
Expand All @@ -779,7 +779,7 @@ ghs_pop_2015_54009_v2019a:
ghs_smod:
alias: ghs_smod_2015_54009_v2019a
ghs_smod_2015_54009_v2016a:
crs: 54009
crs: "ESRI:54009"
data_type: RasterDataset
driver: raster
kwargs:
Expand All @@ -796,7 +796,7 @@ ghs_smod_2015_54009_v2016a:
path: socio_economic/ghs/GHS_SMOD_POP2015_GLOBE_R2016A_54009_1k_v1_0.tif

ghs_smod_2015_54009_v2019a:
crs: 54009
crs: "ESRI:54009"
data_type: RasterDataset
driver: raster
kwargs:
Expand Down
1 change: 1 addition & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Added
-----
- Export CLI now also accepts time tuples (#660)
- New stats.skills VE and RSR (#666)
- Check CLI command can now validate bbox and geom regions (#664)

Changed
-------
Expand Down
4 changes: 3 additions & 1 deletion docs/user_guide/data_overview.rst
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@ However, we do plan to expand its functionality over time. It can be use for exa

.. code-block:: console

hydromt check grid -d /path/to/data_catalog.yml -i /path/to/model_config.yml
hydromt check grid -d /path/to/data_catalog.yml -i /path/to/model_config.yml -r '{'bbox': [-1,-1,1,1]}'

currently only `bbox` and `geom` variants of regions are supported in validation. Also note that the geom variant will only check whether the file exists not its contents. We also plan to expand this functionality in the future.

.. _get_data_python:

Expand Down
65 changes: 45 additions & 20 deletions hydromt/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@

from hydromt.data_catalog import DataCatalog
from hydromt.validators.data_catalog import DataCatalogValidator
from hydromt.validators.model_config import HydromtModelStep
from hydromt.validators.model_config import HydromtModelSetup
from hydromt.validators.region import validate_region

from .. import __version__, log
from ..models import MODELS
Expand Down Expand Up @@ -332,39 +333,51 @@ def update(


@main.command(
short_help="Validate config files are correct",
short_help="Validate config / data catalog / region",
)
@click.argument(
"MODEL",
@click.option(
"-m",
"--model",
type=str,
default=None,
help="Model name, e.g. wflow, sfincs, etc. to validate config file.",
)
@opt_config
@data_opt
@deltares_data_opt
@quiet_opt
@verbose_opt
@region_opt
@click.pass_context
def check(
ctx,
model,
config,
data,
dd,
region: Optional[Dict[Any, Any]],
quiet: int,
verbose: int,
):
"""Verify that provided data catalog files are in the correct format.
"""
Verify that provided data catalog and config files are in the correct format.

Additionnaly region bbox and geom can also be validated.

Example usage:
--------------

hydromt check grid -d /path/to/data_catalog.yml -i /path/to/model_config.yml
Check data catalog file:
hydromt check -d /path/to/data_catalog.yml -v

Check data catalog and grid_model config file:
hydromt check -m grid_model -d /path/to/data_catalog.yml -i /path/to/model_config.yml -v

With region:
hydromt check -m grid_model -d /path/to/data_catalog.yml -i /path/to/model_config.yml -r '{'bbox': [-1,-1,1,1]}' -v

""" # noqa: E501
# logger
log_level = max(10, 30 - 10 * (verbose - quiet))
logger = log.setuplog("check", join(".", "hydromt.log"), log_level=log_level)
logger.info(f"Output dir: {export_dest_path}")
try:
all_exceptions = []
for cat_path in data:
Expand All @@ -376,24 +389,36 @@ def check(
all_exceptions.append(e)
logger.info("Catalog has errors")

mod = MODELS.load(model)
try:
config_dict = cli_utils.parse_config(config)
logger.info(f"Validating config at {config}")
if region:
logger.info(f"Validating region {region}")
try:
validate_region(region)
logger.info("Region is valid!")

except (ValidationError, ValueError, NotImplementedError) as e:
logger.info("region has errors")
all_exceptions.append(e)

HydromtModelStep.from_dict(config_dict, model=mod)
logger.info("Model config valid!")
if config:
mod = MODELS.load(model)
logger.info(f"Validating for model {model} of type {type(mod).__name__}")
try:
config_dict = cli_utils.parse_config(config)
logger.info(f"Validating config at {config}")

except (ValidationError, ValueError) as e:
logger.info("Model has errors")
all_exceptions.append(e)
HydromtModelSetup.from_dict(config_dict, model=mod)
logger.info("Model config valid!")

except (ValidationError, ValueError) as e:
logger.info("Model has errors")
all_exceptions.append(e)

if len(all_exceptions) > 0:
raise Exception(all_exceptions)
raise ValueError(all_exceptions)

except Exception as e:
logger.exception(e) # catch and log errors
raise
raise e
finally:
for handler in logger.handlers[:]:
handler.close()
Expand Down
35 changes: 6 additions & 29 deletions hydromt/validators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,11 @@
DataCatalogMetaData,
DataCatalogValidator,
)
from .model_config import HydromtModelStep
from .model_config import HydromtModelSetup, HydromtModelStep
from .region import (
BoundingBoxBasinRegion,
BoundingBoxInterBasinRegion,
BoundingBoxRegion,
BoundingBoxSubBasinRegion,
GeometryBasinRegion,
GeometryInterBasinRegion,
GeometryRegion,
GeometrySubBasinRegion,
GridRegion,
MeshRegion,
MultiPointBasinRegion,
MultiPointSubBasinRegion,
PointBasinRegion,
PointSubBasinRegion,
WGS84Point,
PathRegion,
Region,
validate_region,
)

Expand All @@ -31,21 +19,10 @@
"DataCatalogItemMetadata",
"DataCatalogMetaData",
"DataCatalogValidator",
"BoundingBoxBasinRegion",
"BoundingBoxInterBasinRegion",
"BoundingBoxRegion",
"BoundingBoxSubBasinRegion",
"GeometryBasinRegion",
"GeometryInterBasinRegion",
"GeometryRegion",
"GeometrySubBasinRegion",
"GridRegion",
"MeshRegion",
"MultiPointBasinRegion",
"MultiPointSubBasinRegion",
"PointBasinRegion",
"PointSubBasinRegion",
"WGS84Point",
"PathRegion",
"Region",
"validate_region",
"HydromtModelStep",
"HydromtModelSetup",
]
51 changes: 44 additions & 7 deletions hydromt/validators/data_catalog.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
"""Pydantic models for the validation of Data catalogs."""
from pathlib import Path
from typing import Any, Dict, Optional, Union
from typing import Any, Dict, List, Literal, Optional, Union

from pydantic import AnyUrl, BaseModel, ConfigDict
from pydantic import AnyUrl, BaseModel, ConfigDict, model_validator
from pydantic.fields import Field
from pydantic_core import Url
from pyproj import CRS
from pyproj.exceptions import CRSError

from hydromt.data_catalog import _yml_from_uri_or_path
from hydromt.typing import Bbox, Number, TimeRange
Expand All @@ -23,6 +25,17 @@ def from_dict(input_dict):
return SourceSpecDict(**input_dict)


class SourceVariant(BaseModel):
"""A variant for a data source."""

provider: Optional[Literal["local", "aws", "gcs"]] = None
version: Optional[Union[str, Number]] = None
path: Path
rename: Optional[Dict[str, str]] = None
filesystem: Optional[Literal["local", "s3", "gcs"]] = None
storage_options: Optional[Dict[str, Any]] = None


class Extent(BaseModel):
"""A validation model for describing the space and time a dataset covers."""

Expand All @@ -34,7 +47,7 @@ class DataCatalogMetaData(BaseModel):
"""The metadata section of a Hydromt data catalog."""

root: Optional[Path] = None
version: Optional[Union[str, int]] = None
version: Optional[Union[str, Number]] = None
name: Optional[str] = None
model_config: ConfigDict = ConfigDict(
str_strip_whitespace=True,
Expand Down Expand Up @@ -81,24 +94,48 @@ class DataCatalogItem(BaseModel):
"""A validated data source."""

name: str
data_type: str
driver: str
path: Path
crs: Optional[int] = None
data_type: Literal["RasterDataset", "GeoDataset", "GeoDataFrame", "DataFrame"]
driver: Literal[
"csv",
"fwf",
"netcdf",
"parquet",
"raster",
"raster_tindex",
"vector",
"vector_table",
"xls",
"xlsx",
"zarr",
]
path: Optional[Path] = None
crs: Optional[Union[int, str]] = None
filesystem: Optional[str] = None
kwargs: Dict[str, Any] = Field(default_factory=dict)
storage_options: Dict[str, Any] = Field(default_factory=dict)
placeholders: Optional[Dict[str, Any]] = None
rename: Dict[str, str] = Field(default_factory=dict)
nodata: Optional[Number] = None
meta: Optional[DataCatalogItemMetadata] = None
unit_add: Optional[Dict[str, Number]] = None
unit_mult: Optional[Dict[str, Number]] = None
variants: Optional[List[SourceVariant]] = None
version: Optional[Union[str, Number]] = None

model_config: ConfigDict = ConfigDict(
str_strip_whitespace=True,
extra="forbid",
)

@model_validator(mode="after")
def _check_valid_crs(self) -> "DataCatalogItem":
try:
if self.crs:
_ = CRS.from_user_input(self.crs)
except CRSError as e:
raise ValueError(e)
return self

@staticmethod
def from_dict(input_dict, name=None):
"""Convert a dictionary into a validated source item."""
Expand Down
24 changes: 21 additions & 3 deletions hydromt/validators/model_config.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
"""Pydantic models for the validation of model config files."""
from inspect import Parameter, signature
from typing import Any, Callable, Dict, Type
from typing import Any, Callable, Dict, List, Type

from pydantic import BaseModel, ConfigDict, Field

from hydromt.models import Model


class HydromtModelStep(BaseModel):
"""A Pydantic model for the validation of model config files."""
"""A Pydantic model for the validation of model setup functions."""

model: Type[Model]
fn: Callable
Expand All @@ -28,7 +28,9 @@ def from_dict(input_dict: Dict[str, Any], model: Model):
try:
fn = getattr(model, fn_name)
except AttributeError:
raise ValueError(f"Model does not have function {fn_name}")
raise ValueError(
f"Model of type {model.__name__} does not have function {fn_name}"
)

sig = signature(fn)
sig_has_var_keyword = (
Expand All @@ -54,3 +56,19 @@ def from_dict(input_dict: Dict[str, Any], model: Model):
)

return HydromtModelStep(model=model, fn=fn, args=arg_dict)


class HydromtModelSetup(BaseModel):
"""A Pydantic model for the validation of model setup files."""

steps: List[HydromtModelStep]

@staticmethod
def from_dict(input_dict: Dict[str, Any], model: Model):
"""Generate a validated model of a sequence steps in a model config file."""
return HydromtModelSetup(
steps=[
HydromtModelStep.from_dict({fn_name: fn_args}, model)
for fn_name, fn_args in input_dict.items()
]
)
Loading