1515from typing import Tuple
1616from typing import Union
1717
18+ import pystac
1819import yaml
1920from deepdiff import DeepDiff
2021from more_itertools import take
22+ from pystac import Catalog
2123from pystac import Collection
2224from pystac import Item
2325from pystac import ItemCollection
@@ -96,6 +98,7 @@ class Context(Enum):
9698 ITEM_SEARCH_FILTER = "Item Search - Filter Ext"
9799 FEATURES_FILTER = "Features - Filter Ext"
98100 CHILDREN = "Children Ext"
101+ BROWSEABLE = "Browseable Ext"
99102
100103 def __str__ (self ) -> str :
101104 return self .value
@@ -528,21 +531,15 @@ def validate_api(
528531
529532 if "browseable" in conformance_classes :
530533 logger .info ("Validating STAC API - Browseable conformance class." )
531- validate_browseable (landing_page_body , errors , warnings )
532- else :
533- logger .info ("Skipping STAC API - Browseable conformance class." )
534+ validate_browseable (landing_page_body , errors , warnings , r_session )
534535
535536 if "children" in conformance_classes :
536537 logger .info ("Validating STAC API - Children conformance class." )
537538 validate_children (landing_page_body , errors , warnings , r_session )
538- else :
539- logger .info ("Skipping STAC API - Children conformance class." )
540539
541540 if "collections" in conformance_classes :
542541 logger .info ("Validating STAC API - Collections conformance class." )
543542 validate_collections (landing_page_body , collection , errors , warnings , r_session )
544- else :
545- logger .info ("Skipping STAC API - Collections conformance class." )
546543
547544 conforms_to = landing_page_body .get ("conformsTo" , [])
548545
@@ -558,8 +555,6 @@ def validate_api(
558555 errors ,
559556 r_session ,
560557 )
561- else :
562- logger .info ("Skipping STAC API - Features conformance class." )
563558
564559 if "item-search" in conformance_classes :
565560 logger .info ("Validating STAC API - Item Search conformance class." )
@@ -574,8 +569,6 @@ def validate_api(
574569 conformance_classes = conformance_classes ,
575570 r_session = r_session ,
576571 )
577- else :
578- logger .info ("Skipping STAC API - Item Search conformance class." )
579572
580573 if not errors :
581574 try :
@@ -683,13 +676,53 @@ def validate_core(
683676 r_session = r_session ,
684677 )
685678
679+ # this validates, among other things, that the child and item link relations reference
680+ # valid STAC Catalogs, Collections, and/or Items
681+ try :
682+ list (take (1000 , Catalog .from_dict (root_body ).get_all_items ()))
683+ except pystac .errors .STACTypeError as e :
684+ errors += (
685+ f"[{ Context .CORE } ] Error while traversing Catalog child/item links to find Items: { e } "
686+ "This can be reproduced with 'list(pystac.Catalog.from_file(root_url).get_all_items())'"
687+ )
688+
686689
687690def validate_browseable (
688691 root_body : Dict [str , Any ],
689692 errors : Errors ,
690693 warnings : Warnings ,
694+ r_session : Session ,
691695) -> None :
692- logger .info ("Browseable validation is not yet implemented." )
696+ # child or item links exist in the root
697+ child_links = links_by_rel (root_body .get ("links" ), "child" )
698+ item_links = links_by_rel (root_body .get ("links" ), "item" )
699+ if not (child_links or item_links ):
700+ errors += f"[{ Context .BROWSEABLE } ] /: Root catalog does not contain any child or item link relations"
701+
702+ # check that at least a few of the items that can be reached from child/item link relations
703+ # can be found through search
704+ try :
705+ for item in take (10 , Catalog .from_dict (root_body ).get_all_items ()):
706+ if link := link_by_rel (root_body .get ("links" ), "search" ):
707+ _ , body , _ = retrieve (
708+ Method .GET ,
709+ link ["href" ],
710+ errors ,
711+ Context .BROWSEABLE ,
712+ params = {"ids" : item .id , "collections" : item .collection },
713+ r_session = r_session ,
714+ )
715+ if body and len (body .get ("features" , [])) != 1 :
716+ errors += f"[{ Context .BROWSEABLE } ] /: Link[rel=children] must href /children"
717+ else :
718+ errors += (
719+ f"[{ Context .BROWSEABLE } ] /: Link[rel=search] could not be found"
720+ )
721+ except pystac .errors .STACTypeError as e :
722+ errors += (
723+ f"[{ Context .BROWSEABLE } ] Error while traversing Catalog child/item links to find Items: { e } . "
724+ "This can be reproduced with 'pystac.Catalog.from_file(root_url).get_all_items()'"
725+ )
693726
694727
695728def validate_children (
@@ -1093,10 +1126,6 @@ def validate_features(
10931126 errors = errors ,
10941127 r_session = r_session ,
10951128 )
1096- else :
1097- logger .info (
1098- "Skipping STAC API - Features - Filter Extension conformance class."
1099- )
11001129
11011130
11021131def validate_item_search (
@@ -1227,10 +1256,6 @@ def validate_item_search(
12271256 errors = errors ,
12281257 r_session = r_session ,
12291258 )
1230- else :
1231- logger .info (
1232- "Skipping STAC API - Item Search - Filter Extension conformance class."
1233- )
12341259
12351260
12361261def validate_filter_queryables (
@@ -1333,10 +1358,6 @@ def validate_item_search_filter(
13331358 logger .info (
13341359 "Validating STAC API - Item Search - Filter Extension - CQL2-Text conformance class."
13351360 )
1336- else :
1337- logger .info (
1338- "Skipping STAC API - Item Search - Filter Extension - CQL2-Text conformance class."
1339- )
13401361
13411362 cql2_json_supported = (
13421363 "http://www.opengis.net/spec/cql2/1.0/conf/cql2-json" in conforms_to
@@ -1349,10 +1370,6 @@ def validate_item_search_filter(
13491370 logger .info (
13501371 "Validating STAC API - Item Search - Filter Extension - CQL2-JSON conformance class."
13511372 )
1352- else :
1353- logger .info (
1354- "Skipping STAC API - Item Search - Filter Extension - CQL2-JSON conformance class."
1355- )
13561373
13571374 basic_cql2_supported = (
13581375 "http://www.opengis.net/spec/cql2/1.0/conf/basic-cql2" in conforms_to
@@ -1365,10 +1382,6 @@ def validate_item_search_filter(
13651382 logger .info (
13661383 "Validating STAC API - Item Search - Filter Extension - Basic CQL2 conformance class."
13671384 )
1368- else :
1369- logger .info (
1370- "Skipping STAC API - Item Search - Filter Extension - Basic CQL2 conformance class."
1371- )
13721385
13731386 advanced_comparison_operators_supported = (
13741387 "http://www.opengis.net/spec/cql2/1.0/conf/advanced-comparison-operators"
@@ -1379,10 +1392,6 @@ def validate_item_search_filter(
13791392 logger .info (
13801393 "Validating STAC API - Item Search - Filter Extension - Advanced Comparison Operators conformance class."
13811394 )
1382- else :
1383- logger .info (
1384- "Skipping STAC API - Item Search - Filter Extension - Advanced Comparison Operators conformance class."
1385- )
13861395
13871396 basic_spatial_operators_supported = (
13881397 "http://www.opengis.net/spec/cql2/1.0/conf/basic-spatial-operators"
@@ -1393,10 +1402,6 @@ def validate_item_search_filter(
13931402 logger .info (
13941403 "Validating STAC API - Item Search - Filter Extension - Basic Spatial Operators conformance class."
13951404 )
1396- else :
1397- logger .info (
1398- "Skipping STAC API - Item Search - Filter Extension - Basic Spatial Operators conformance class."
1399- )
14001405
14011406 temporal_operators_supported = (
14021407 "http://www.opengis.net/spec/cql2/1.0/conf/temporal-operators" in conforms_to
@@ -1406,10 +1411,6 @@ def validate_item_search_filter(
14061411 logger .info (
14071412 "Validating STAC API - Item Search - Filter Extension - Temporal Operators conformance class."
14081413 )
1409- else :
1410- logger .info (
1411- "Skipping STAC API - Item Search - Filter Extension - Temporal Operators conformance class."
1412- )
14131414
14141415 # todo: validate these
14151416 # Spatial Operators: http://www.opengis.net/spec/cql2/1.0/conf/spatial-operators
0 commit comments