15
15
from typing import Tuple
16
16
from typing import Union
17
17
18
+ import pystac
18
19
import yaml
19
20
from deepdiff import DeepDiff
20
21
from more_itertools import take
22
+ from pystac import Catalog
21
23
from pystac import Collection
22
24
from pystac import Item
23
25
from pystac import ItemCollection
@@ -96,6 +98,7 @@ class Context(Enum):
96
98
ITEM_SEARCH_FILTER = "Item Search - Filter Ext"
97
99
FEATURES_FILTER = "Features - Filter Ext"
98
100
CHILDREN = "Children Ext"
101
+ BROWSEABLE = "Browseable Ext"
99
102
100
103
def __str__ (self ) -> str :
101
104
return self .value
@@ -528,21 +531,15 @@ def validate_api(
528
531
529
532
if "browseable" in conformance_classes :
530
533
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 )
534
535
535
536
if "children" in conformance_classes :
536
537
logger .info ("Validating STAC API - Children conformance class." )
537
538
validate_children (landing_page_body , errors , warnings , r_session )
538
- else :
539
- logger .info ("Skipping STAC API - Children conformance class." )
540
539
541
540
if "collections" in conformance_classes :
542
541
logger .info ("Validating STAC API - Collections conformance class." )
543
542
validate_collections (landing_page_body , collection , errors , warnings , r_session )
544
- else :
545
- logger .info ("Skipping STAC API - Collections conformance class." )
546
543
547
544
conforms_to = landing_page_body .get ("conformsTo" , [])
548
545
@@ -558,8 +555,6 @@ def validate_api(
558
555
errors ,
559
556
r_session ,
560
557
)
561
- else :
562
- logger .info ("Skipping STAC API - Features conformance class." )
563
558
564
559
if "item-search" in conformance_classes :
565
560
logger .info ("Validating STAC API - Item Search conformance class." )
@@ -574,8 +569,6 @@ def validate_api(
574
569
conformance_classes = conformance_classes ,
575
570
r_session = r_session ,
576
571
)
577
- else :
578
- logger .info ("Skipping STAC API - Item Search conformance class." )
579
572
580
573
if not errors :
581
574
try :
@@ -683,13 +676,53 @@ def validate_core(
683
676
r_session = r_session ,
684
677
)
685
678
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
+
686
689
687
690
def validate_browseable (
688
691
root_body : Dict [str , Any ],
689
692
errors : Errors ,
690
693
warnings : Warnings ,
694
+ r_session : Session ,
691
695
) -> 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
+ )
693
726
694
727
695
728
def validate_children (
@@ -1093,10 +1126,6 @@ def validate_features(
1093
1126
errors = errors ,
1094
1127
r_session = r_session ,
1095
1128
)
1096
- else :
1097
- logger .info (
1098
- "Skipping STAC API - Features - Filter Extension conformance class."
1099
- )
1100
1129
1101
1130
1102
1131
def validate_item_search (
@@ -1227,10 +1256,6 @@ def validate_item_search(
1227
1256
errors = errors ,
1228
1257
r_session = r_session ,
1229
1258
)
1230
- else :
1231
- logger .info (
1232
- "Skipping STAC API - Item Search - Filter Extension conformance class."
1233
- )
1234
1259
1235
1260
1236
1261
def validate_filter_queryables (
@@ -1333,10 +1358,6 @@ def validate_item_search_filter(
1333
1358
logger .info (
1334
1359
"Validating STAC API - Item Search - Filter Extension - CQL2-Text conformance class."
1335
1360
)
1336
- else :
1337
- logger .info (
1338
- "Skipping STAC API - Item Search - Filter Extension - CQL2-Text conformance class."
1339
- )
1340
1361
1341
1362
cql2_json_supported = (
1342
1363
"http://www.opengis.net/spec/cql2/1.0/conf/cql2-json" in conforms_to
@@ -1349,10 +1370,6 @@ def validate_item_search_filter(
1349
1370
logger .info (
1350
1371
"Validating STAC API - Item Search - Filter Extension - CQL2-JSON conformance class."
1351
1372
)
1352
- else :
1353
- logger .info (
1354
- "Skipping STAC API - Item Search - Filter Extension - CQL2-JSON conformance class."
1355
- )
1356
1373
1357
1374
basic_cql2_supported = (
1358
1375
"http://www.opengis.net/spec/cql2/1.0/conf/basic-cql2" in conforms_to
@@ -1365,10 +1382,6 @@ def validate_item_search_filter(
1365
1382
logger .info (
1366
1383
"Validating STAC API - Item Search - Filter Extension - Basic CQL2 conformance class."
1367
1384
)
1368
- else :
1369
- logger .info (
1370
- "Skipping STAC API - Item Search - Filter Extension - Basic CQL2 conformance class."
1371
- )
1372
1385
1373
1386
advanced_comparison_operators_supported = (
1374
1387
"http://www.opengis.net/spec/cql2/1.0/conf/advanced-comparison-operators"
@@ -1379,10 +1392,6 @@ def validate_item_search_filter(
1379
1392
logger .info (
1380
1393
"Validating STAC API - Item Search - Filter Extension - Advanced Comparison Operators conformance class."
1381
1394
)
1382
- else :
1383
- logger .info (
1384
- "Skipping STAC API - Item Search - Filter Extension - Advanced Comparison Operators conformance class."
1385
- )
1386
1395
1387
1396
basic_spatial_operators_supported = (
1388
1397
"http://www.opengis.net/spec/cql2/1.0/conf/basic-spatial-operators"
@@ -1393,10 +1402,6 @@ def validate_item_search_filter(
1393
1402
logger .info (
1394
1403
"Validating STAC API - Item Search - Filter Extension - Basic Spatial Operators conformance class."
1395
1404
)
1396
- else :
1397
- logger .info (
1398
- "Skipping STAC API - Item Search - Filter Extension - Basic Spatial Operators conformance class."
1399
- )
1400
1405
1401
1406
temporal_operators_supported = (
1402
1407
"http://www.opengis.net/spec/cql2/1.0/conf/temporal-operators" in conforms_to
@@ -1406,10 +1411,6 @@ def validate_item_search_filter(
1406
1411
logger .info (
1407
1412
"Validating STAC API - Item Search - Filter Extension - Temporal Operators conformance class."
1408
1413
)
1409
- else :
1410
- logger .info (
1411
- "Skipping STAC API - Item Search - Filter Extension - Temporal Operators conformance class."
1412
- )
1413
1414
1414
1415
# todo: validate these
1415
1416
# Spatial Operators: http://www.opengis.net/spec/cql2/1.0/conf/spatial-operators
0 commit comments