From 394b5f627e90623889ffb0b727fb0c526cfef413 Mon Sep 17 00:00:00 2001 From: Francis Charette Migneault Date: Fri, 4 Jul 2025 21:00:57 -0400 Subject: [PATCH 1/3] fix Link 'label:assets' field type as list --- CHANGELOG.md | 4 ++++ stac_pydantic/links.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5877d32..e0cb334 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ ## Unreleased +- Fix invalid `label:assets` type in `stac_pydantic.links.Link`. + A single `str` was specified instead of `List[str]` as expected by the extension field + (see [Label Extension - Links](https://github.com/stac-extensions/label?tab=readme-ov-file#links-source-imagery)). + ## 3.3.2 (2025-06-18) - Remove restriction on valid media types for links (#182, @mishaschwartz) diff --git a/stac_pydantic/links.py b/stac_pydantic/links.py index 5ba2131..eed968e 100644 --- a/stac_pydantic/links.py +++ b/stac_pydantic/links.py @@ -19,7 +19,7 @@ class Link(StacBaseModel): title: Optional[str] = None # Label extension - label: Optional[str] = Field(default=None, alias="label:assets") + label: Optional[List[str]] = Field(default=None, alias="label:assets") model_config = ConfigDict(use_enum_values=True, extra="allow") def resolve(self, base_url: str) -> None: From c2c082b04a75f557770f85ca269d584fdcc8984b Mon Sep 17 00:00:00 2001 From: Francis Charette Migneautl Date: Thu, 10 Jul 2025 12:01:42 -0400 Subject: [PATCH 2/3] fix 'label:assets' link example with expected list[str] --- tests/example_stac/roads_item.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/example_stac/roads_item.json b/tests/example_stac/roads_item.json index d139299..f69aabd 100644 --- a/tests/example_stac/roads_item.json +++ b/tests/example_stac/roads_item.json @@ -156,7 +156,9 @@ "href": "roads_source.json", "rel": "source", "title": "The source imagery these road labels were derived from", - "label:assets": "road_labels" + "label:assets": [ + "road_labels" + ] } ], "assets": { From e811d0dd993837853284717df29c863b2d4178b6 Mon Sep 17 00:00:00 2001 From: Francis Charette Migneautl Date: Fri, 11 Jul 2025 22:27:26 -0400 Subject: [PATCH 3/3] ensure link relation is validated against label:assets property --- CHANGELOG.md | 3 ++- stac_pydantic/links.py | 22 +++++++++++++++++++--- tests/test_models.py | 9 +++++++++ 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0cb334..37fd8f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,8 @@ - Fix invalid `label:assets` type in `stac_pydantic.links.Link`. A single `str` was specified instead of `List[str]` as expected by the extension field - (see [Label Extension - Links](https://github.com/stac-extensions/label?tab=readme-ov-file#links-source-imagery)). + (see [Label Extension - Links](https://github.com/stac-extensions/label?tab=readme-ov-file#links-source-imagery)) (#183, @fmigneault). +- Add validation of `Link` ensuring that `rel=source` is employed if `label:assets` is specified (#183, @fmigneault). ## 3.3.2 (2025-06-18) diff --git a/stac_pydantic/links.py b/stac_pydantic/links.py index eed968e..695cee5 100644 --- a/stac_pydantic/links.py +++ b/stac_pydantic/links.py @@ -1,8 +1,8 @@ from enum import auto -from typing import Iterator, List, Optional, Union +from typing import Iterator, List, Optional, Union, Any from urllib.parse import urljoin -from pydantic import ConfigDict, Field, RootModel +from pydantic import AliasChoices, ConfigDict, Field, RootModel, ValidationInfo, field_validator from stac_pydantic.shared import MimeTypes, StacBaseModel from stac_pydantic.utils import AutoValueEnum @@ -19,13 +19,29 @@ class Link(StacBaseModel): title: Optional[str] = None # Label extension - label: Optional[List[str]] = Field(default=None, alias="label:assets") + label: Optional[List[str]] = Field( + default=None, + min_length=1, + alias="label:assets", + validation_alias=AliasChoices("label:assets", "label_assets", "label"), + ) model_config = ConfigDict(use_enum_values=True, extra="allow") def resolve(self, base_url: str) -> None: """resolve a link to the given base URL""" self.href = urljoin(base_url, self.href) + @field_validator("label", mode="after") + @classmethod + def validate_label_rel_source(cls, label: Optional[List[str]], info: ValidationInfo) -> Optional[List[str]]: + # check requirement: https://github.com/stac-extensions/label#links-source-imagery + if label and info.data["rel"] != "source": + raise ValueError( + "Label extension link with 'label:assets' is only allowed for link with 'rel=source'. " + f"Values ('rel': {info.data['rel']}, 'label:assets': {label})" + ) + return label + class Links(RootModel[List[Link]]): root: List[Link] diff --git a/tests/test_models.py b/tests/test_models.py index 837313b..0d76c21 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -105,6 +105,15 @@ def test_label_extension() -> None: dict_match(test_item, valid_item) +def test_label_extension_rel_source() -> None: + with pytest.raises(ValidationError): + Link(href=LABEL_EXTENSION, rel="random", label_assets=["road_labels"]) + + link = Link(href=LABEL_EXTENSION, rel="source", label_assets=["road_labels"]) + assert link.rel == "source" + assert link.label == ["road_labels"] + + def test_explicit_extension_validation() -> None: test_item = request(EO_EXTENSION)