Skip to content

Commit f3db745

Browse files
committed
add support for Children Extension
1 parent 9e8d21e commit f3db745

File tree

3 files changed

+117
-3
lines changed

3 files changed

+117
-3
lines changed

poetry.lock

Lines changed: 35 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Shapely = "1.8.4"
2828
more_itertools = "^8.14.0"
2929
stac-check = "^1.3.1"
3030
stac-validator = "^3.2.0"
31+
deepdiff = "^6.2.3"
3132

3233
[tool.poetry.dev-dependencies]
3334
Pygments = ">=2.10.0"

src/stac_api_validator/validations.py

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from typing import Union
1717

1818
import yaml
19+
from deepdiff import DeepDiff
1920
from more_itertools import take
2021
from pystac import Collection
2122
from pystac import Item
@@ -94,6 +95,7 @@ class Context(Enum):
9495
COLLECTIONS = "Collections"
9596
ITEM_SEARCH_FILTER = "Item Search - Filter Ext"
9697
FEATURES_FILTER = "Features - Filter Ext"
98+
CHILDREN = "Children Ext"
9799

98100
def __str__(self) -> str:
99101
return self.value
@@ -532,7 +534,7 @@ def validate_api(
532534

533535
if "children" in conformance_classes:
534536
logger.info("Validating STAC API - Children conformance class.")
535-
validate_children(landing_page_body, errors, warnings)
537+
validate_children(landing_page_body, errors, warnings, r_session)
536538
else:
537539
logger.info("Skipping STAC API - Children conformance class.")
538540

@@ -694,8 +696,79 @@ def validate_children(
694696
root_body: Dict[str, Any],
695697
errors: Errors,
696698
warnings: Warnings,
699+
r_session: Session,
697700
) -> None:
698-
logger.info("Children validation is not yet implemented.")
701+
children_link = link_by_rel(root_body.get("links"), "children")
702+
if (
703+
not children_link
704+
or not children_link.get("href", "").endswith("/children")
705+
or not is_json_type(children_link.get("type"))
706+
):
707+
errors += f"[{Context.CHILDREN}] /: Link[rel=children] must href /children"
708+
return
709+
710+
if not (children_href := children_link.get("href")):
711+
errors += f"[{Context.CHILDREN}] /: Link[rel=children] missing href"
712+
else:
713+
_, children_body, resp_headers = retrieve(
714+
Method.GET,
715+
children_href,
716+
errors,
717+
Context.CHILDREN,
718+
r_session=r_session,
719+
)
720+
if not children_body:
721+
errors += f"[{Context.CHILDREN}] /children body was empty"
722+
return
723+
724+
if not resp_headers or not has_json_content_type(resp_headers):
725+
errors += f"[{Context.CHILDREN}] /children content-type header was not application/json"
726+
727+
if not (self_link := link_by_rel(children_body.get("links", []), "self")):
728+
errors += f"[{Context.CHILDREN}] /children does not have self link"
729+
elif children_link.get("href") != self_link.get("href"):
730+
errors += (
731+
f"[{Context.CHILDREN}] /children self link does not match requested url"
732+
)
733+
734+
if not link_by_rel(children_body.get("links", []), "root"):
735+
errors += f"[{Context.CHILDREN}] /children does not have root link"
736+
737+
# each child link in Landing Page must have an entry in children
738+
child_links = links_by_rel(root_body.get("links"), "child")
739+
740+
child_link_bodies = []
741+
for child_link in child_links:
742+
if child_href := child_link.get("href"):
743+
_, child_body, child_resp_headers = retrieve(
744+
Method.GET,
745+
child_href,
746+
errors,
747+
Context.CHILDREN,
748+
r_session=r_session,
749+
)
750+
child_link_bodies.append(child_body)
751+
else:
752+
errors += f"[{Context.CHILDREN}] child link {json.dumps(child_link)} missing href field"
753+
754+
child_links_vs_children_diff = DeepDiff(
755+
child_link_bodies, children_body.get("children"), ignore_order=True
756+
)
757+
if iterable_item_removed := child_links_vs_children_diff.get(
758+
"iterable_item_removed"
759+
):
760+
errors += (
761+
f"[{Context.CHILDREN}] /: child links contained these objects that /children does not: "
762+
f"{json.dumps(iterable_item_removed)}"
763+
)
764+
765+
if iterable_item_added := child_links_vs_children_diff.get(
766+
"iterable_item_added"
767+
):
768+
errors += (
769+
f"[{Context.CHILDREN}] /: child links missing these objects that /children contains: "
770+
f"{json.dumps(iterable_item_added)}"
771+
)
699772

700773

701774
def validate_collections(
@@ -745,6 +818,12 @@ def validate_collections(
745818
f"[{Context.COLLECTIONS}] /collections does not have root link"
746819
)
747820

821+
if collections_type := body.get("type"):
822+
warnings += (
823+
f"[{Context.COLLECTIONS}] /collections entity has a field 'type: {collections_type}', "
824+
"but the STAC API entity schema does not define this field"
825+
)
826+
748827
if body.get("collections") is None:
749828
errors += f"[{Context.COLLECTIONS}] /collections does not have 'collections' field"
750829

0 commit comments

Comments
 (0)