Skip to content

Commit ac36df7

Browse files
authored
Merge pull request #270 from stac-utils/pv/browseable
implement browsable extension validation
2 parents cc1d6a1 + 4064ff1 commit ac36df7

File tree

1 file changed

+45
-44
lines changed

1 file changed

+45
-44
lines changed

src/stac_api_validator/validations.py

Lines changed: 45 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@
1515
from typing import Tuple
1616
from typing import Union
1717

18+
import pystac
1819
import yaml
1920
from deepdiff import DeepDiff
2021
from more_itertools import take
22+
from pystac import Catalog
2123
from pystac import Collection
2224
from pystac import Item
2325
from 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

687690
def 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

695728
def 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

11021131
def 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

12361261
def 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

Comments
 (0)