|
16 | 16 | from typing import Union
|
17 | 17 |
|
18 | 18 | import yaml
|
| 19 | +from deepdiff import DeepDiff |
19 | 20 | from more_itertools import take
|
20 | 21 | from pystac import Collection
|
21 | 22 | from pystac import Item
|
@@ -94,6 +95,7 @@ class Context(Enum):
|
94 | 95 | COLLECTIONS = "Collections"
|
95 | 96 | ITEM_SEARCH_FILTER = "Item Search - Filter Ext"
|
96 | 97 | FEATURES_FILTER = "Features - Filter Ext"
|
| 98 | + CHILDREN = "Children Ext" |
97 | 99 |
|
98 | 100 | def __str__(self) -> str:
|
99 | 101 | return self.value
|
@@ -532,7 +534,7 @@ def validate_api(
|
532 | 534 |
|
533 | 535 | if "children" in conformance_classes:
|
534 | 536 | 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) |
536 | 538 | else:
|
537 | 539 | logger.info("Skipping STAC API - Children conformance class.")
|
538 | 540 |
|
@@ -694,8 +696,79 @@ def validate_children(
|
694 | 696 | root_body: Dict[str, Any],
|
695 | 697 | errors: Errors,
|
696 | 698 | warnings: Warnings,
|
| 699 | + r_session: Session, |
697 | 700 | ) -> 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 | + ) |
699 | 772 |
|
700 | 773 |
|
701 | 774 | def validate_collections(
|
@@ -745,6 +818,12 @@ def validate_collections(
|
745 | 818 | f"[{Context.COLLECTIONS}] /collections does not have root link"
|
746 | 819 | )
|
747 | 820 |
|
| 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 | + |
748 | 827 | if body.get("collections") is None:
|
749 | 828 | errors += f"[{Context.COLLECTIONS}] /collections does not have 'collections' field"
|
750 | 829 |
|
|
0 commit comments