diff --git a/pyproject.toml b/pyproject.toml index 0d227cf..a17a527 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,6 @@ dependencies = [ dev = [ "pytest>=8.1.1", "mypy>=1.15.0", - "pytest>=8.3.5", "ruff>=0.11.2", "pymarkdownlnt>=0.9.25", "pre-commit>=4.2.0", diff --git a/stapi-pydantic/pyproject.toml b/stapi-pydantic/pyproject.toml index 8c4f1e8..e2f5d17 100644 --- a/stapi-pydantic/pyproject.toml +++ b/stapi-pydantic/pyproject.toml @@ -25,3 +25,8 @@ include = ["src/stapi_pydantic"] [build-system] requires = ["hatchling"] build-backend = "hatchling.build" + +[dependency-groups] +dev = [ + "pytest>=8.1.1", +] diff --git a/stapi-pydantic/src/stapi_pydantic/datetime_interval.py b/stapi-pydantic/src/stapi_pydantic/datetime_interval.py index a581cf7..0139bb2 100644 --- a/stapi-pydantic/src/stapi_pydantic/datetime_interval.py +++ b/stapi-pydantic/src/stapi_pydantic/datetime_interval.py @@ -10,32 +10,37 @@ WrapSerializer, ) +_DatetimeTuple = tuple[datetime | None, datetime | None] -def validate_before( - value: str | tuple[datetime, datetime], -) -> tuple[datetime, datetime]: + +def validate_before(value: str | _DatetimeTuple) -> _DatetimeTuple: if isinstance(value, str): - start, end = value.split("/", 1) - return (datetime.fromisoformat(start), datetime.fromisoformat(end)) + start_str, end_str = value.split("/", 1) + start = None if start_str == ".." else datetime.fromisoformat(start_str) + end = None if end_str == ".." else datetime.fromisoformat(end_str) + value = (start, end) return value -def validate_after(value: tuple[datetime, datetime]) -> tuple[datetime, datetime]: - if value[1] < value[0]: +def validate_after(value: _DatetimeTuple) -> _DatetimeTuple: + # None/date & date/None are always valid + if value[1] and value[0] and value[1] < value[0]: raise ValueError("end before start") return value def serialize( - value: tuple[datetime, datetime], - serializer: Callable[[tuple[datetime, datetime]], tuple[str, str]], + value: _DatetimeTuple, + serializer: Callable[[_DatetimeTuple], tuple[str, str]], ) -> str: del serializer # unused - return f"{value[0].isoformat()}/{value[1].isoformat()}" + start = value[0].isoformat() if value[0] else ".." + end = value[1].isoformat() if value[1] else ".." + return f"{start}/{end}" DatetimeInterval = Annotated[ - tuple[AwareDatetime, AwareDatetime], + tuple[AwareDatetime | None, AwareDatetime | None], BeforeValidator(validate_before), AfterValidator(validate_after), WrapSerializer(serialize, return_type=str), diff --git a/stapi-pydantic/tests/test_datetime_interval.py b/stapi-pydantic/tests/test_datetime_interval.py new file mode 100644 index 0000000..01ee6c6 --- /dev/null +++ b/stapi-pydantic/tests/test_datetime_interval.py @@ -0,0 +1,21 @@ +import pytest +from stapi_pydantic import DatetimeInterval + + +def test_valid_datetime_intervals() -> None: + """Test the datetime interval validator.""" + dt1 = DatetimeInterval.__metadata__[0].func("2025-04-01T00:00:00Z/2025-04-01T23:59:59Z") + _ = DatetimeInterval.__metadata__[1].func(dt1) + dt2 = DatetimeInterval.__metadata__[0].func("2025-04-01T00:00:00Z/..") + _ = DatetimeInterval.__metadata__[1].func(dt2) + dt3 = DatetimeInterval.__metadata__[0].func("../2025-04-01T23:59:59Z") + _ = DatetimeInterval.__metadata__[1].func(dt3) + dt4 = DatetimeInterval.__metadata__[0].func("../..") + _ = DatetimeInterval.__metadata__[1].func(dt4) + + +def test_invalid_datetime_intervals() -> None: + """Test the datetime interval validator.""" + with pytest.raises(ValueError, match="end before start"): + dt1 = DatetimeInterval.__metadata__[0].func("2025-04-01T00:00:00Z/2025-03-01T23:59:59Z") + _ = DatetimeInterval.__metadata__[1].func(dt1) diff --git a/uv.lock b/uv.lock index dc69de0..f16c05f 100644 --- a/uv.lock +++ b/uv.lock @@ -1553,7 +1553,6 @@ dev = [ { name = "pygithub", specifier = ">=2.6.1" }, { name = "pymarkdownlnt", specifier = ">=0.9.25" }, { name = "pytest", specifier = ">=8.1.1" }, - { name = "pytest", specifier = ">=8.3.5" }, { name = "respx", specifier = ">=0.22.0" }, { name = "ruff", specifier = ">=0.11.2" }, { name = "types-click", specifier = ">=7.1.8" }, @@ -2152,12 +2151,20 @@ dependencies = [ { name = "geojson-pydantic" }, ] +[package.dev-dependencies] +dev = [ + { name = "pytest" }, +] + [package.metadata] requires-dist = [ { name = "cql2", specifier = ">=0.3.6" }, { name = "geojson-pydantic", specifier = ">=1.2.0" }, ] +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = ">=8.1.1" }] + [[package]] name = "starlette" version = "0.46.2"