C2SM / Sirocco

AiiDA based Weather and climate workflow tool
1 stars 0 forks source link

Replacing strictyml #16

Closed agoscinski closed 1 month ago

agoscinski commented 2 months ago

I looked at schema which would work, but I rather would like to use pydantic since it incorporates the type validation and python object creation. So we have less code too manage. Here is an example how it would look like


from pydantic import BaseModel, field_validator, model_validator 
from datetime import datetime
from isoduration import parse_duration
from isoduration.types import Duration
from pydantic_yaml import parse_yaml_raw_as
from typing import Self

class NamedBaseModel(BaseModel):
    """Our custom Pydantic model."""
    name: str

    def __init__(self, /, **data):
        name_and_spec = {}
        assert(len(data.keys()) == 1)
        assert(len(data.values()) == 1)
        name_and_spec["name"] = next(iter(data.keys()))
        name_and_spec.update(next(iter(data.values())))
        super().__init__(**name_and_spec)

class CycleTaskInput(NamedBaseModel):
    """Our custom Pydantic model."""
    date: datetime | None = None
    lag: str | None = None

    @model_validator(mode='after')
    def check_lag_xor_date_is_set(self) -> Self:
        if self.lag is not None and self.date is not None:
            msg = "Only one key 'lag' or 'date' is allowed. Not both."
            raise ValueError(msg)
        return self 

    @field_validator('lag')
    @classmethod
    def _is_duration(cls, value: str | None) -> Duration | None:  # noqa
        """Check if is Duration."""
        if value is None:
            return None
        return parse_duration(value)

# returns an instance
print('Passing {"grid_file": {"date": "2027-01-01T00:00"}}')
cycle_task_input = CycleTaskInput.model_validate_json('{"grid_file": {"date": "2027-01-01T00:00"}}')
print(cycle_task_input.name, cycle_task_input.date)
print()

print('Passing with typo {"grid_file": {"date": "2027-01-01T00:00typo"}}')
try:
    CycleTaskInput.model_validate_json('{"grid_file": {"date": "2027-01-01T00:00typo"}}') # typo in date
except Exception as err:
    print(err)
print()

print('Passing with lag and date {"grid_file": {"date": "2027-01-01T00:00", "lag": "-P2M"}}')
try:
    CycleTaskInput.model_validate_json('{"grid_file": {"date": "2027-01-01T00:00", "lag": "-P2M"}}')
except Exception as err:
    print(err)
print()

print('Passing yml "grid_file:\\n  date: \'2027-01-01T00:00\'"')
yml = "grid_file:\n  date: '2027-01-01T00:00'"
print(parse_yaml_raw_as(CycleTaskInput, yml))

Output

Passing {"grid_file": {"date": "2027-01-01T00:00"}}
grid_file 2027-01-01 00:00:00

Passing with typo {"grid_file": {"date": "2027-01-01T00:00typo"}}
1 validation error for CycleTaskInput
date
  Input should be a valid datetime or date, unexpected extra characters at the end of the input [type=datetime_from_date_parsing, input_value='2027-01-01T00:00typo', input_type=str]
    For further information visit https://errors.pydantic.dev/2.8/v/datetime_from_date_parsing

Passing with lag and date {"grid_file": {"date": "2027-01-01T00:00", "lag": "-P2M"}}
1 validation error for CycleTaskInput
  Value error, Only one key 'lag' or 'date' is allowed. Not both. [type=value_error, input_value={'name': 'grid_file', 'da...1T00:00', 'lag': '-P2M'}, input_type=dict]
    For further information visit https://errors.pydantic.dev/2.8/v/value_error

Passing yml "grid_file:\n  date: '2027-01-01T00:00'"
name='grid_file' date=datetime.datetime(2027, 1, 1, 0, 0) lag=None

What do you think @leclairm?

leclairm commented 2 months ago

@agoscinski I agree, it's way better to have the validation included in the class.

agoscinski commented 1 month ago

Solved with PR #17