diff --git a/stac_pydantic/item.py b/stac_pydantic/item.py index 2f75f4c..4b2d54a 100644 --- a/stac_pydantic/item.py +++ b/stac_pydantic/item.py @@ -2,7 +2,6 @@ from geojson_pydantic import Feature from pydantic import AnyUrl, ConfigDict, Field, model_serializer, model_validator -from typing_extensions import Self from stac_pydantic.links import Links from stac_pydantic.shared import ( @@ -20,20 +19,12 @@ class ItemProperties(StacCommonMetadata): https://github.com/radiantearth/stac-spec/blob/v1.0.0/item-spec/item-spec.md#properties-object """ + # Overide the datetime field to be required datetime: Optional[UtcDatetime] # Check https://docs.pydantic.dev/dev-v2/migration/#changes-to-config for more information. model_config = ConfigDict(extra="allow") - @model_validator(mode="after") - def validate_datetime(self) -> Self: - if not self.datetime and (not self.start_datetime or not self.end_datetime): - raise ValueError( - "start_datetime and end_datetime must be specified when datetime is null" - ) - - return self - class Item(Feature, StacBaseModel): """ diff --git a/stac_pydantic/shared.py b/stac_pydantic/shared.py index f06d6c0..126379b 100644 --- a/stac_pydantic/shared.py +++ b/stac_pydantic/shared.py @@ -9,8 +9,9 @@ BaseModel, ConfigDict, Field, + model_validator, ) -from typing_extensions import Annotated +from typing_extensions import Annotated, Self from stac_pydantic.utils import AutoValueEnum @@ -126,22 +127,49 @@ class Provider(StacBaseModel): class StacCommonMetadata(StacBaseModel): """ - https://github.com/radiantearth/stac-spec/blob/v1.0.0/item-spec/common-metadata.md#date-and-time-range + https://github.com/radiantearth/stac-spec/blob/v1.0.0/item-spec/common-metadata.md """ + # Basic title: Optional[str] = None description: Optional[str] = None - start_datetime: Optional[UtcDatetime] = None - end_datetime: Optional[UtcDatetime] = None + # Date and Time + datetime: Optional[UtcDatetime] = None created: Optional[UtcDatetime] = None updated: Optional[UtcDatetime] = None + # Date and Time Range + start_datetime: Optional[UtcDatetime] = None + end_datetime: Optional[UtcDatetime] = None + # Provider + providers: Optional[List[Provider]] = None + # Instrument platform: Optional[str] = None instruments: Optional[List[str]] = None constellation: Optional[str] = None mission: Optional[str] = None - providers: Optional[List[Provider]] = None gsd: Optional[float] = Field(None, gt=0) + @model_validator(mode="after") + def validate_datetime_or_start_end(self) -> Self: + # When datetime is null, start_datetime and end_datetime must be specified + if not self.datetime and (not self.start_datetime or not self.end_datetime): + raise ValueError( + "start_datetime and end_datetime must be specified when datetime is null" + ) + + return self + + @model_validator(mode="after") + def validate_start_end(self) -> Self: + # Using one of start_datetime or end_datetime requires the use of the other + if (self.start_datetime and not self.end_datetime) or ( + not self.start_datetime and self.end_datetime + ): + raise ValueError( + "use of start_datetime or end_datetime requires the use of the other" + ) + return self + class Asset(StacCommonMetadata): """ @@ -157,3 +185,10 @@ class Asset(StacCommonMetadata): model_config = ConfigDict( populate_by_name=True, use_enum_values=True, extra="allow" ) + + @model_validator(mode="after") + def validate_datetime_or_start_end(self) -> Self: + # Overriding the parent method to avoid requiring datetime or start/end_datetime + # Additional fields MAY be added on the Asset object, but are not required. + # https://github.com/radiantearth/stac-spec/blob/v1.0.0/item-spec/item-spec.md#additional-fields-for-assets + return self diff --git a/tests/test_models.py b/tests/test_models.py index fc348db..6472bee 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -8,7 +8,7 @@ from stac_pydantic import Collection, Item, ItemProperties from stac_pydantic.extensions import validate_extensions from stac_pydantic.links import Link, Links -from stac_pydantic.shared import MimeTypes +from stac_pydantic.shared import MimeTypes, StacCommonMetadata from .conftest import dict_match, request @@ -169,10 +169,15 @@ def test_geo_interface() -> None: "start_datetime": "2024-01-01T00:00:00Z", "end_datetime": "2024-01-02T00:00:00Z", }, + { + "datetime": "2024-01-01T00:00:00Z", + "start_datetime": "2024-01-01T00:00:00Z", + "end_datetime": "2024-01-02T00:00:00Z", + }, ], ) -def test_item_properties_dates(args) -> None: - ItemProperties(**args) +def test_stac_common_dates(args) -> None: + StacCommonMetadata(**args) @pytest.mark.parametrize( @@ -183,12 +188,27 @@ def test_item_properties_dates(args) -> None: {"datetime": None, "end_datetime": "2024-01-01T00:00:00Z"}, ], ) -def test_item_properties_no_dates(args) -> None: +def test_stac_common_no_dates(args) -> None: with pytest.raises( ValueError, match="start_datetime and end_datetime must be specified when datetime is null", ): - ItemProperties(**args) + StacCommonMetadata(**args) + + +@pytest.mark.parametrize( + "args", + [ + {"datetime": "2024-01-01T00:00:00Z", "start_datetime": "2024-01-01T00:00:00Z"}, + {"datetime": "2024-01-01T00:00:00Z", "end_datetime": "2024-01-01T00:00:00Z"}, + ], +) +def test_stac_common_start_and_end(args) -> None: + with pytest.raises( + ValueError, + match="use of start_datetime or end_datetime requires the use of the other", + ): + StacCommonMetadata(**args) def test_declared_model() -> None: