diff --git a/CHANGELOG.md b/CHANGELOG.md index 7315072..2e89c90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ The format is (loosely) based on [Keep a Changelog](http://keepachangelog.com/) ## [Unreleased] ### Added +- Added detailed recursive validation summary showing validation counts by STAC object type (Catalog, Collection, etc.) +- Added validation duration timing that shows total processing time in a human-readable format +- Added support for pydantic validation in recursive mode with proper schema reporting + +### Changed +- Standardized summary output formatting across all validation modes for consistency ## [v3.9.3] - 2025-06-28 diff --git a/stac_validator/stac_validator.py b/stac_validator/stac_validator.py index 8b80a5e..4f1fa3b 100644 --- a/stac_validator/stac_validator.py +++ b/stac_validator/stac_validator.py @@ -1,5 +1,6 @@ import json import sys +import time from typing import Any, Dict, List, Optional, Tuple import click # type: ignore @@ -7,6 +8,45 @@ from .validate import StacValidate +def _print_summary( + title: str, valid_count: int, total_count: int, obj_type: str = "STAC objects" +) -> None: + """Helper function to print a consistent summary line. + + Args: + title (str): Title of the summary section + valid_count (int): Number of valid items + total_count (int): Total number of items + obj_type (str): Type of objects being counted (e.g., 'items', 'collections') + """ + click.secho() + click.secho(f"{title}:", bold=True) + if total_count > 0: + percentage = (valid_count / total_count) * 100 + click.secho( + f" {obj_type.capitalize()} passed: {valid_count}/{total_count} ({percentage:.1f}%)" + ) + else: + click.secho(f" No {obj_type} found to validate") + + +def format_duration(seconds: float) -> str: + """Format duration in seconds to a human-readable string. + + Args: + seconds (float): Duration in seconds + + Returns: + str: Formatted duration string (e.g., '1m 23.45s' or '456.78ms') + """ + if seconds < 1.0: + return f"{seconds * 1000:.2f}ms" + minutes, seconds = divmod(seconds, 60) + if minutes > 0: + return f"{int(minutes)}m {seconds:.2f}s" + return f"{seconds:.2f}s" + + def print_update_message(version: str) -> None: """Prints an update message for `stac-validator` based on the version of the STAC file being validated. @@ -36,33 +76,64 @@ def item_collection_summary(message: List[Dict[str, Any]]) -> None: Returns: None """ - valid_count = 0 - for item in message: - if "valid_stac" in item and item["valid_stac"] is True: - valid_count = valid_count + 1 - click.secho() - click.secho("--item-collection summary", bold=True) - click.secho(f"items_validated: {len(message)}") - click.secho(f"valid_items: {valid_count}") + valid_count = sum(1 for item in message if item.get("valid_stac") is True) + _print_summary("-- Item Collection Summary", valid_count, len(message), "items") def collections_summary(message: List[Dict[str, Any]]) -> None: - """Prints a summary of the validation results for an item collection response. + """Prints a summary of the validation results for a collections response. Args: - message (List[Dict[str, Any]]): The validation results for the item collection. + message (List[Dict[str, Any]]): The validation results for the collections. Returns: None """ - valid_count = 0 - for collection in message: - if "valid_stac" in collection and collection["valid_stac"] is True: - valid_count = valid_count + 1 - click.secho() - click.secho("--collections summary", bold=True) - click.secho(f"collections_validated: {len(message)}") - click.secho(f"valid_collections: {valid_count}") + valid_count = sum(1 for coll in message if coll.get("valid_stac") is True) + _print_summary("-- Collections Summary", valid_count, len(message), "collections") + + +def recursive_validation_summary(message: List[Dict[str, Any]]) -> None: + """Prints a summary of the recursive validation results. + + Args: + message (List[Dict[str, Any]]): The validation results from recursive validation. + + Returns: + None + """ + # Count valid and total objects by type + type_counts = {} + total_valid = 0 + + for item in message: + if not isinstance(item, dict): + continue + + obj_type = item.get("asset_type", "unknown").lower() + is_valid = item.get("valid_stac", False) is True + + if obj_type not in type_counts: + type_counts[obj_type] = {"valid": 0, "total": 0} + + type_counts[obj_type]["total"] += 1 + if is_valid: + type_counts[obj_type]["valid"] += 1 + total_valid += 1 + + # Print overall summary + _print_summary("-- Recursive Validation Summary", total_valid, len(message)) + + # Print breakdown by type if there are multiple types + if len(type_counts) > 1: + click.secho("\n Breakdown by type:") + for obj_type, counts in sorted(type_counts.items()): + percentage = ( + (counts["valid"] / counts["total"]) * 100 if counts["total"] > 0 else 0 + ) + click.secho( + f" {obj_type.capitalize()}: {counts['valid']}/{counts['total']} ({percentage:.1f}%)" + ) @click.command() @@ -182,7 +253,7 @@ def main( log_file: str, pydantic: bool, verbose: bool = False, -) -> None: +): """Main function for the `stac-validator` command line tool. Validates a STAC file against the STAC specification and prints the validation results to the console as JSON. @@ -190,7 +261,8 @@ def main( stac_file (str): Path to the STAC file to be validated. collections (bool): Validate response from /collections endpoint. item_collection (bool): Whether to validate item collection responses. - no_assets_urls (bool): Whether to open href links when validating assets (enabled by default). + no_assets_urls (bool): Whether to open href links when validating assets + (enabled by default). headers (dict): HTTP headers to include in the requests. pages (int): Maximum number of pages to validate via `item_collection`. recursive (bool): Whether to recursively validate all related STAC objects. @@ -215,11 +287,14 @@ def main( SystemExit: Exits the program with a status code of 0 if the STAC file is valid, or 1 if it is invalid. """ + start_time = time.time() valid = True + if schema_map == (): schema_map_dict: Optional[Dict[str, str]] = None else: schema_map_dict = dict(schema_map) + stac = StacValidate( stac_file=stac_file, collections=collections, @@ -241,25 +316,37 @@ def main( pydantic=pydantic, verbose=verbose, ) - if not item_collection and not collections: - valid = stac.run() - elif collections: - stac.validate_collections() - else: - stac.validate_item_collection() - message = stac.message - if "version" in message[0]: - print_update_message(message[0]["version"]) + try: + if not item_collection and not collections: + valid = stac.run() + elif collections: + stac.validate_collections() + else: + stac.validate_item_collection() + + message = stac.message + if "version" in message[0]: + print_update_message(message[0]["version"]) - if no_output is False: - click.echo(json.dumps(message, indent=4)) + if no_output is False: + click.echo(json.dumps(message, indent=4)) - if item_collection: - item_collection_summary(message) - elif collections: - collections_summary(message) + # Print appropriate summary based on validation mode + if item_collection: + item_collection_summary(message) + elif collections: + collections_summary(message) + elif recursive: + recursive_validation_summary(message) + finally: + # Always print the duration, even if validation fails + duration = time.time() - start_time + click.secho( + f"\nValidation completed in {format_duration(duration)}", fg="green" + ) + click.secho() sys.exit(0 if valid else 1) diff --git a/stac_validator/validate.py b/stac_validator/validate.py index e2b59d6..a95a6f6 100644 --- a/stac_validator/validate.py +++ b/stac_validator/validate.py @@ -242,7 +242,11 @@ def _create_verbose_err_msg(self, error_input: Any) -> Union[Dict[str, Any], str return str(error_input) # Fallback to string representation def create_err_msg( - self, err_type: str, err_msg: str, error_obj: Optional[Exception] = None + self, + err_type: str, + err_msg: str, + error_obj: Optional[Exception] = None, + schema_uri: str = "", ) -> Dict[str, Union[str, bool, List[str], Dict[str, Any]]]: """ Create a standardized error message dictionary and mark validation as failed. @@ -251,6 +255,7 @@ def create_err_msg( err_type (str): The type of error. err_msg (str): The error message. error_obj (Optional[Exception]): The raw exception object for verbose details. + schema_uri (str, optional): The URI of the schema that failed validation. Returns: dict: Dictionary containing error information. @@ -258,8 +263,40 @@ def create_err_msg( self.valid = False # Ensure all values are of the correct type - version_str: str = str(self.version) if self.version is not None else "" - path_str: str = str(self.stac_file) if self.stac_file is not None else "" + if not isinstance(err_type, str): + err_type = str(err_type) + if not isinstance(err_msg, str): + err_msg = str(err_msg) + + # Initialize the message with common fields + message: Dict[str, Any] = { + "version": str(self.version) if hasattr(self, "version") else "", + "path": str(self.stac_file) if hasattr(self, "stac_file") else "", + "schema": ( + [self._original_schema_paths.get(self.schema, self.schema)] + if hasattr(self, "schema") + else [""] + ), + "valid_stac": False, + "error_type": err_type, + "error_message": err_msg, + "failed_schema": schema_uri if schema_uri else "", + "recommendation": "For more accurate error information, rerun with --verbose.", + } + + # Add asset_type and validation_method if available + if hasattr(self, "stac_content"): + try: + stac_type = get_stac_type(self.stac_content) + if stac_type: + message["asset_type"] = stac_type.upper() + message["validation_method"] = ( + "recursive" + if hasattr(self, "recursive") and self.recursive + else "default" + ) + except Exception: # noqa: BLE001 + pass # Ensure schema is properly typed schema_value: str = "" @@ -267,10 +304,10 @@ def create_err_msg( schema_value = str(self.schema) schema_field: List[str] = [schema_value] if schema_value else [] - # Initialize the message with common fields - message: Dict[str, Union[str, bool, List[str], Dict[str, Any]]] = { - "version": version_str, - "path": path_str, + # Initialize the error message with common fields + error_message: Dict[str, Union[str, bool, List[str], Dict[str, Any]]] = { + "version": str(self.version) if self.version is not None else "", + "path": str(self.stac_file) if self.stac_file is not None else "", "schema": schema_field, # All schemas that were checked "valid_stac": False, "error_type": err_type, @@ -281,31 +318,31 @@ def create_err_msg( # Try to extract the failed schema from the error message if it's a validation error if error_obj and hasattr(error_obj, "schema"): if isinstance(error_obj.schema, dict) and "$id" in error_obj.schema: - message["failed_schema"] = error_obj.schema["$id"] + error_message["failed_schema"] = error_obj.schema["$id"] elif hasattr(error_obj, "schema_url"): - message["failed_schema"] = error_obj.schema_url + error_message["failed_schema"] = error_obj.schema_url # If we can't find a schema ID, try to get it from the schema map elif schema_field and len(schema_field) == 1: - message["failed_schema"] = schema_field[0] + error_message["failed_schema"] = schema_field[0] if self.verbose and error_obj is not None: verbose_err = self._create_verbose_err_msg(error_obj) if isinstance(verbose_err, dict): - message["error_verbose"] = verbose_err + error_message["error_verbose"] = verbose_err else: - message["error_verbose"] = {"detail": str(verbose_err)} + error_message["error_verbose"] = {"detail": str(verbose_err)} # Add recommendation to check the schema if the error is not clear - if "failed_schema" in message and message["failed_schema"]: - message["recommendation"] = ( + if error_message.get("failed_schema"): + error_message["recommendation"] = ( "If the error is unclear, please check the schema documentation at: " - f"{message['failed_schema']}" + f"{error_message['failed_schema']}" ) else: - message["recommendation"] = ( + error_message["recommendation"] = ( "For more accurate error information, rerun with --verbose." ) - return message + return error_message def create_links_message(self) -> Dict: """ @@ -561,7 +598,8 @@ def recursive_validator(self, stac_type: str) -> bool: """ Recursively validate a STAC JSON document and its children/items. - Follows "child" and "item" links, calling `default_validator` on each. + Follows "child" and "item" links, calling the appropriate validator on each. + Uses pydantic_validator if self.pydantic is True, otherwise uses default_validator. Args: stac_type (str): The STAC object type to validate. @@ -574,10 +612,19 @@ def recursive_validator(self, stac_type: str) -> bool: self.schema = set_schema_addr(self.version, stac_type.lower()) message = self.create_message(stac_type, "recursive") message["valid_stac"] = False + # Add validator_engine field to track validation method + message["validator_engine"] = "pydantic" if self.pydantic else "jsonschema" try: - msg = self.default_validator(stac_type) - message["schema"] = msg["schema"] + if self.pydantic: + # Set pydantic model info in schema field + model_name = f"stac-pydantic {stac_type.capitalize()} model" + message["schema"] = [model_name] + # Run pydantic validation + msg = self.pydantic_validator(stac_type) + else: + msg = self.default_validator(stac_type) + message["schema"] = msg["schema"] except jsonschema.exceptions.ValidationError as e: if e.context: @@ -594,12 +641,31 @@ def recursive_validator(self, stac_type: str) -> bool: err_type="JSONSchemaValidationError", err_msg=err_msg, error_obj=e, + schema_uri=( + e.schema.get("$id", "") + if hasattr(e, "schema") and isinstance(e.schema, dict) + else "" + ), ) ) self.message.append(message) if self.trace_recursion: click.echo(json.dumps(message, indent=4)) return valid + except Exception as e: + if self.pydantic and "pydantic" in str(e.__class__.__module__): + message.update( + self.create_err_msg( + err_type="PydanticValidationError", + err_msg=str(e), + error_obj=e, + ) + ) + self.message.append(message) + if self.trace_recursion: + click.echo(json.dumps(message, indent=4)) + return valid + raise valid = True message["valid_stac"] = valid @@ -661,19 +727,39 @@ def recursive_validator(self, stac_type: str) -> bool: if link["rel"] == "item": self.schema = set_schema_addr(self.version, stac_type.lower()) message = self.create_message(stac_type, "recursive") - if self.version == "0.7.0": - schema = fetch_and_parse_schema(self.schema) - # Prevent unknown url type issue - schema["allOf"] = [{}] - jsonschema.validate(self.stac_content, schema) - else: - msg = self.default_validator(stac_type) - message["schema"] = msg["schema"] - message["valid_stac"] = True + message["validator_engine"] = ( + "pydantic" if self.pydantic else "jsonschema" + ) + try: + if self.pydantic: + # Set pydantic model info in schema field for child items + model_name = f"stac-pydantic {stac_type.capitalize()} model" + message["schema"] = [model_name] + # Run pydantic validation + msg = self.pydantic_validator(stac_type) + elif self.version == "0.7.0": + schema = fetch_and_parse_schema(self.schema) + # Prevent unknown url type issue + schema["allOf"] = [{}] + jsonschema.validate(self.stac_content, schema) + else: + msg = self.default_validator(stac_type) + message["schema"] = msg["schema"] + message["valid_stac"] = True + except Exception as e: + if self.pydantic and "pydantic" in str(e.__class__.__module__): + message.update( + self.create_err_msg( + err_type="PydanticValidationError", + err_msg=str(e), + error_obj=e, + ) + ) + message["valid_stac"] = False + else: + raise - if self.log: - self.message.append(message) - if not self.max_depth or self.max_depth < 5: + if self.log or (not self.max_depth or self.max_depth < 5): self.message.append(message) if all(child_validity): valid = True diff --git a/tests/test_pydantic.py b/tests/test_pydantic.py index f519c64..fc573e1 100644 --- a/tests/test_pydantic.py +++ b/tests/test_pydantic.py @@ -59,3 +59,34 @@ def test_pydantic_invalid_item(): assert stac.message[0]["validation_method"] == "pydantic" assert "error_type" in stac.message[0] assert "error_message" in stac.message[0] + + +def test_pydantic_recursive(): + """Test pydantic validation in recursive mode.""" + stac_file = "tests/test_data/local_cat/example-catalog/catalog.json" + stac = stac_validator.StacValidate( + stac_file, pydantic=True, recursive=True, max_depth=2 # Limit depth for testing + ) + stac.run() + + # Check that we have validation messages + assert len(stac.message) > 0 + + # Check each validation message + for msg in stac.message: + # Check that validator_engine is set to pydantic + assert msg["validator_engine"] == "pydantic" + + # Check that validation_method is recursive + assert msg["validation_method"] == "recursive" + + # Check that valid_stac is a boolean + assert isinstance(msg["valid_stac"], bool) + + # Check that schema is set to pydantic model based on asset type + if msg["asset_type"] == "ITEM": + assert msg["schema"] == ["stac-pydantic Item model"] + elif msg["asset_type"] == "COLLECTION": + assert msg["schema"] == ["stac-pydantic Collection model"] + elif msg["asset_type"] == "CATALOG": + assert msg["schema"] == ["stac-pydantic Catalog model"] diff --git a/tests/test_recursion.py b/tests/test_recursion.py index baa3484..f4b82bf 100644 --- a/tests/test_recursion.py +++ b/tests/test_recursion.py @@ -21,6 +21,7 @@ def test_recursive_lvl_4_local_v100(): "valid_stac": True, "asset_type": "CATALOG", "validation_method": "recursive", + "validator_engine": "jsonschema", }, { "version": "1.0.0", @@ -31,6 +32,7 @@ def test_recursive_lvl_4_local_v100(): "valid_stac": True, "asset_type": "CATALOG", "validation_method": "recursive", + "validator_engine": "jsonschema", }, { "version": "1.0.0", @@ -43,6 +45,7 @@ def test_recursive_lvl_4_local_v100(): "valid_stac": True, "asset_type": "COLLECTION", "validation_method": "recursive", + "validator_engine": "jsonschema", }, { "version": "1.0.0", @@ -55,6 +58,7 @@ def test_recursive_lvl_4_local_v100(): "valid_stac": True, "asset_type": "COLLECTION", "validation_method": "recursive", + "validator_engine": "jsonschema", }, ] @@ -71,6 +75,7 @@ def test_recursive_local_v090(): "asset_type": "CATALOG", "validation_method": "recursive", "valid_stac": True, + "validator_engine": "jsonschema", }, { "version": "0.9.0", @@ -79,6 +84,7 @@ def test_recursive_local_v090(): "asset_type": "ITEM", "validation_method": "recursive", "valid_stac": True, + "validator_engine": "jsonschema", }, { "version": "0.9.0", @@ -91,6 +97,7 @@ def test_recursive_local_v090(): "asset_type": "ITEM", "validation_method": "recursive", "valid_stac": True, + "validator_engine": "jsonschema", }, ] @@ -107,6 +114,7 @@ def test_recursive_v1beta1(): "asset_type": "COLLECTION", "validation_method": "recursive", "valid_stac": True, + "validator_engine": "jsonschema", } ] @@ -125,6 +133,7 @@ def test_recursive_v1beta2(): "asset_type": "COLLECTION", "validation_method": "recursive", "valid_stac": True, + "validator_engine": "jsonschema", } ] @@ -143,6 +152,7 @@ def test_recursion_collection_local_v1rc1(): "asset_type": "COLLECTION", "validation_method": "recursive", "valid_stac": True, + "validator_engine": "jsonschema", }, { "version": "1.0.0-rc.1", @@ -153,6 +163,7 @@ def test_recursion_collection_local_v1rc1(): "asset_type": "ITEM", "validation_method": "recursive", "valid_stac": True, + "validator_engine": "jsonschema", }, { "version": "1.0.0-rc.1", @@ -163,6 +174,7 @@ def test_recursion_collection_local_v1rc1(): "asset_type": "ITEM", "validation_method": "recursive", "valid_stac": True, + "validator_engine": "jsonschema", }, { "version": "1.0.0-rc.1", @@ -177,6 +189,7 @@ def test_recursion_collection_local_v1rc1(): "asset_type": "ITEM", "validation_method": "recursive", "valid_stac": True, + "validator_engine": "jsonschema", }, ] @@ -195,6 +208,7 @@ def test_recursion_collection_local_v1rc2(): "asset_type": "COLLECTION", "validation_method": "recursive", "valid_stac": True, + "validator_engine": "jsonschema", }, { "version": "1.0.0-rc.2", @@ -205,6 +219,7 @@ def test_recursion_collection_local_v1rc2(): "asset_type": "ITEM", "validation_method": "recursive", "valid_stac": True, + "validator_engine": "jsonschema", }, { "version": "1.0.0-rc.2", @@ -215,6 +230,7 @@ def test_recursion_collection_local_v1rc2(): "asset_type": "ITEM", "validation_method": "recursive", "valid_stac": True, + "validator_engine": "jsonschema", }, { "version": "1.0.0-rc.2", @@ -230,6 +246,7 @@ def test_recursion_collection_local_v1rc2(): "asset_type": "ITEM", "validation_method": "recursive", "valid_stac": True, + "validator_engine": "jsonschema", }, ] @@ -249,6 +266,7 @@ def test_recursion_collection_local_2_v1rc2(): "asset_type": "COLLECTION", "validation_method": "recursive", "valid_stac": True, + "validator_engine": "jsonschema", }, { "version": "1.0.0-rc.2", @@ -259,6 +277,7 @@ def test_recursion_collection_local_2_v1rc2(): "asset_type": "ITEM", "validation_method": "recursive", "valid_stac": True, + "validator_engine": "jsonschema", }, ] @@ -308,6 +327,7 @@ def test_recursion_with_bad_item_trace_recursion(): "valid_stac": True, "asset_type": "CATALOG", "validation_method": "recursive", + "validator_engine": "jsonschema", }, { "version": "1.0.0", @@ -343,6 +363,7 @@ def test_recursion_with_bad_child_collection(): "valid_stac": False, "asset_type": "COLLECTION", "validation_method": "recursive", + "validator_engine": "jsonschema", "error_type": "JSONSchemaValidationError", "failed_schema": "https://schemas.stacspec.org/v1.0.0/collection-spec/json-schema/collection.json", "error_message": "'id' is a required property", @@ -367,6 +388,7 @@ def test_recursion_with_missing_collection_link(): ], "valid_stac": False, "validation_method": "recursive", + "validator_engine": "jsonschema", "error_type": "JSONSchemaValidationError", "failed_schema": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json", "error_message": "'simple-collection' should not be valid under {}. Error is in collection ", diff --git a/tests/test_validate_item_collection.py b/tests/test_validate_item_collection.py index b7d294c..0d7e8d5 100644 --- a/tests/test_validate_item_collection.py +++ b/tests/test_validate_item_collection.py @@ -547,218 +547,40 @@ def test_validate_item_collection_remote_pages_1_v110(): stac_file = "https://stac.dataspace.copernicus.eu/v1/collections/sentinel-3-olci-2-wfr-nrt/items" stac = stac_validator.StacValidate(stac_file, item_collection=True, pages=1) stac.validate_item_collection() - assert stac.message == [ - { - "version": "1.1.0", - "path": "https://stac.dataspace.copernicus.eu/v1/collections/sentinel-3-olci-2-wfr-nrt/items/S3A_OL_2_WFR____20250628T040818_20250628T041118_20250628T054519_0179_127_275_3600_MAR_O_NR_003", - "schema": [ - "https://cs-si.github.io/eopf-stac-extension/v1.2.0/schema.json", - "https://stac-extensions.github.io/alternate-assets/v1.2.0/schema.json", - "https://stac-extensions.github.io/authentication/v1.1.0/schema.json", - "https://stac-extensions.github.io/eo/v2.0.0/schema.json", - "https://stac-extensions.github.io/file/v2.1.0/schema.json", - "https://stac-extensions.github.io/processing/v1.2.0/schema.json", - "https://stac-extensions.github.io/product/v0.1.0/schema.json", - "https://stac-extensions.github.io/projection/v2.0.0/schema.json", - "https://stac-extensions.github.io/sat/v1.1.0/schema.json", - "https://stac-extensions.github.io/storage/v2.0.0/schema.json", - "https://stac-extensions.github.io/timestamps/v1.1.0/schema.json", - "https://schemas.stacspec.org/v1.1.0/item-spec/json-schema/item.json", - ], - "valid_stac": True, - "asset_type": "ITEM", - "validation_method": "default", - }, - { - "version": "1.1.0", - "path": "https://stac.dataspace.copernicus.eu/v1/collections/sentinel-3-olci-2-wfr-nrt/items/S3A_OL_2_WFR____20250628T040518_20250628T040818_20250628T054705_0180_127_275_3420_MAR_O_NR_003", - "schema": [ - "https://cs-si.github.io/eopf-stac-extension/v1.2.0/schema.json", - "https://stac-extensions.github.io/alternate-assets/v1.2.0/schema.json", - "https://stac-extensions.github.io/authentication/v1.1.0/schema.json", - "https://stac-extensions.github.io/eo/v2.0.0/schema.json", - "https://stac-extensions.github.io/file/v2.1.0/schema.json", - "https://stac-extensions.github.io/processing/v1.2.0/schema.json", - "https://stac-extensions.github.io/product/v0.1.0/schema.json", - "https://stac-extensions.github.io/projection/v2.0.0/schema.json", - "https://stac-extensions.github.io/sat/v1.1.0/schema.json", - "https://stac-extensions.github.io/storage/v2.0.0/schema.json", - "https://stac-extensions.github.io/timestamps/v1.1.0/schema.json", - "https://schemas.stacspec.org/v1.1.0/item-spec/json-schema/item.json", - ], - "valid_stac": True, - "asset_type": "ITEM", - "validation_method": "default", - }, - { - "version": "1.1.0", - "path": "https://stac.dataspace.copernicus.eu/v1/collections/sentinel-3-olci-2-wfr-nrt/items/S3A_OL_2_WFR____20250628T040218_20250628T040518_20250628T054918_0179_127_275_3240_MAR_O_NR_003", - "schema": [ - "https://cs-si.github.io/eopf-stac-extension/v1.2.0/schema.json", - "https://stac-extensions.github.io/alternate-assets/v1.2.0/schema.json", - "https://stac-extensions.github.io/authentication/v1.1.0/schema.json", - "https://stac-extensions.github.io/eo/v2.0.0/schema.json", - "https://stac-extensions.github.io/file/v2.1.0/schema.json", - "https://stac-extensions.github.io/processing/v1.2.0/schema.json", - "https://stac-extensions.github.io/product/v0.1.0/schema.json", - "https://stac-extensions.github.io/projection/v2.0.0/schema.json", - "https://stac-extensions.github.io/sat/v1.1.0/schema.json", - "https://stac-extensions.github.io/storage/v2.0.0/schema.json", - "https://stac-extensions.github.io/timestamps/v1.1.0/schema.json", - "https://schemas.stacspec.org/v1.1.0/item-spec/json-schema/item.json", - ], - "valid_stac": True, - "asset_type": "ITEM", - "validation_method": "default", - }, - { - "version": "1.1.0", - "path": "https://stac.dataspace.copernicus.eu/v1/collections/sentinel-3-olci-2-wfr-nrt/items/S3A_OL_2_WFR____20250628T035918_20250628T040218_20250628T055000_0179_127_275_3060_MAR_O_NR_003", - "schema": [ - "https://cs-si.github.io/eopf-stac-extension/v1.2.0/schema.json", - "https://stac-extensions.github.io/alternate-assets/v1.2.0/schema.json", - "https://stac-extensions.github.io/authentication/v1.1.0/schema.json", - "https://stac-extensions.github.io/eo/v2.0.0/schema.json", - "https://stac-extensions.github.io/file/v2.1.0/schema.json", - "https://stac-extensions.github.io/processing/v1.2.0/schema.json", - "https://stac-extensions.github.io/product/v0.1.0/schema.json", - "https://stac-extensions.github.io/projection/v2.0.0/schema.json", - "https://stac-extensions.github.io/sat/v1.1.0/schema.json", - "https://stac-extensions.github.io/storage/v2.0.0/schema.json", - "https://stac-extensions.github.io/timestamps/v1.1.0/schema.json", - "https://schemas.stacspec.org/v1.1.0/item-spec/json-schema/item.json", - ], - "valid_stac": True, - "asset_type": "ITEM", - "validation_method": "default", - }, - { - "version": "1.1.0", - "path": "https://stac.dataspace.copernicus.eu/v1/collections/sentinel-3-olci-2-wfr-nrt/items/S3A_OL_2_WFR____20250628T035618_20250628T035918_20250628T054946_0179_127_275_2880_MAR_O_NR_003", - "schema": [ - "https://cs-si.github.io/eopf-stac-extension/v1.2.0/schema.json", - "https://stac-extensions.github.io/alternate-assets/v1.2.0/schema.json", - "https://stac-extensions.github.io/authentication/v1.1.0/schema.json", - "https://stac-extensions.github.io/eo/v2.0.0/schema.json", - "https://stac-extensions.github.io/file/v2.1.0/schema.json", - "https://stac-extensions.github.io/processing/v1.2.0/schema.json", - "https://stac-extensions.github.io/product/v0.1.0/schema.json", - "https://stac-extensions.github.io/projection/v2.0.0/schema.json", - "https://stac-extensions.github.io/sat/v1.1.0/schema.json", - "https://stac-extensions.github.io/storage/v2.0.0/schema.json", - "https://stac-extensions.github.io/timestamps/v1.1.0/schema.json", - "https://schemas.stacspec.org/v1.1.0/item-spec/json-schema/item.json", - ], - "valid_stac": True, - "asset_type": "ITEM", - "validation_method": "default", - }, - { - "version": "1.1.0", - "path": "https://stac.dataspace.copernicus.eu/v1/collections/sentinel-3-olci-2-wfr-nrt/items/S3A_OL_2_WFR____20250628T035318_20250628T035618_20250628T054203_0179_127_275_2700_MAR_O_NR_003", - "schema": [ - "https://cs-si.github.io/eopf-stac-extension/v1.2.0/schema.json", - "https://stac-extensions.github.io/alternate-assets/v1.2.0/schema.json", - "https://stac-extensions.github.io/authentication/v1.1.0/schema.json", - "https://stac-extensions.github.io/eo/v2.0.0/schema.json", - "https://stac-extensions.github.io/file/v2.1.0/schema.json", - "https://stac-extensions.github.io/processing/v1.2.0/schema.json", - "https://stac-extensions.github.io/product/v0.1.0/schema.json", - "https://stac-extensions.github.io/projection/v2.0.0/schema.json", - "https://stac-extensions.github.io/sat/v1.1.0/schema.json", - "https://stac-extensions.github.io/storage/v2.0.0/schema.json", - "https://stac-extensions.github.io/timestamps/v1.1.0/schema.json", - "https://schemas.stacspec.org/v1.1.0/item-spec/json-schema/item.json", - ], - "valid_stac": True, - "asset_type": "ITEM", - "validation_method": "default", - }, - { - "version": "1.1.0", - "path": "https://stac.dataspace.copernicus.eu/v1/collections/sentinel-3-olci-2-wfr-nrt/items/S3A_OL_2_WFR____20250628T035018_20250628T035318_20250628T054150_0179_127_275_2520_MAR_O_NR_003", - "schema": [ - "https://cs-si.github.io/eopf-stac-extension/v1.2.0/schema.json", - "https://stac-extensions.github.io/alternate-assets/v1.2.0/schema.json", - "https://stac-extensions.github.io/authentication/v1.1.0/schema.json", - "https://stac-extensions.github.io/eo/v2.0.0/schema.json", - "https://stac-extensions.github.io/file/v2.1.0/schema.json", - "https://stac-extensions.github.io/processing/v1.2.0/schema.json", - "https://stac-extensions.github.io/product/v0.1.0/schema.json", - "https://stac-extensions.github.io/projection/v2.0.0/schema.json", - "https://stac-extensions.github.io/sat/v1.1.0/schema.json", - "https://stac-extensions.github.io/storage/v2.0.0/schema.json", - "https://stac-extensions.github.io/timestamps/v1.1.0/schema.json", - "https://schemas.stacspec.org/v1.1.0/item-spec/json-schema/item.json", - ], - "valid_stac": True, - "asset_type": "ITEM", - "validation_method": "default", - }, - { - "version": "1.1.0", - "path": "https://stac.dataspace.copernicus.eu/v1/collections/sentinel-3-olci-2-wfr-nrt/items/S3A_OL_2_WFR____20250628T034718_20250628T035018_20250628T054025_0179_127_275_2340_MAR_O_NR_003", - "schema": [ - "https://cs-si.github.io/eopf-stac-extension/v1.2.0/schema.json", - "https://stac-extensions.github.io/alternate-assets/v1.2.0/schema.json", - "https://stac-extensions.github.io/authentication/v1.1.0/schema.json", - "https://stac-extensions.github.io/eo/v2.0.0/schema.json", - "https://stac-extensions.github.io/file/v2.1.0/schema.json", - "https://stac-extensions.github.io/processing/v1.2.0/schema.json", - "https://stac-extensions.github.io/product/v0.1.0/schema.json", - "https://stac-extensions.github.io/projection/v2.0.0/schema.json", - "https://stac-extensions.github.io/sat/v1.1.0/schema.json", - "https://stac-extensions.github.io/storage/v2.0.0/schema.json", - "https://stac-extensions.github.io/timestamps/v1.1.0/schema.json", - "https://schemas.stacspec.org/v1.1.0/item-spec/json-schema/item.json", - ], - "valid_stac": True, - "asset_type": "ITEM", - "validation_method": "default", - }, - { - "version": "1.1.0", - "path": "https://stac.dataspace.copernicus.eu/v1/collections/sentinel-3-olci-2-wfr-nrt/items/S3A_OL_2_WFR____20250628T034418_20250628T034718_20250628T054010_0180_127_275_2160_MAR_O_NR_003", - "schema": [ - "https://cs-si.github.io/eopf-stac-extension/v1.2.0/schema.json", - "https://stac-extensions.github.io/alternate-assets/v1.2.0/schema.json", - "https://stac-extensions.github.io/authentication/v1.1.0/schema.json", - "https://stac-extensions.github.io/eo/v2.0.0/schema.json", - "https://stac-extensions.github.io/file/v2.1.0/schema.json", - "https://stac-extensions.github.io/processing/v1.2.0/schema.json", - "https://stac-extensions.github.io/product/v0.1.0/schema.json", - "https://stac-extensions.github.io/projection/v2.0.0/schema.json", - "https://stac-extensions.github.io/sat/v1.1.0/schema.json", - "https://stac-extensions.github.io/storage/v2.0.0/schema.json", - "https://stac-extensions.github.io/timestamps/v1.1.0/schema.json", - "https://schemas.stacspec.org/v1.1.0/item-spec/json-schema/item.json", - ], - "valid_stac": True, - "asset_type": "ITEM", - "validation_method": "default", - }, - { - "version": "1.1.0", - "path": "https://stac.dataspace.copernicus.eu/v1/collections/sentinel-3-olci-2-wfr-nrt/items/S3A_OL_2_WFR____20250628T034118_20250628T034418_20250628T053959_0179_127_275_1980_MAR_O_NR_003", - "schema": [ - "https://cs-si.github.io/eopf-stac-extension/v1.2.0/schema.json", - "https://stac-extensions.github.io/alternate-assets/v1.2.0/schema.json", - "https://stac-extensions.github.io/authentication/v1.1.0/schema.json", - "https://stac-extensions.github.io/eo/v2.0.0/schema.json", - "https://stac-extensions.github.io/file/v2.1.0/schema.json", - "https://stac-extensions.github.io/processing/v1.2.0/schema.json", - "https://stac-extensions.github.io/product/v0.1.0/schema.json", - "https://stac-extensions.github.io/projection/v2.0.0/schema.json", - "https://stac-extensions.github.io/sat/v1.1.0/schema.json", - "https://stac-extensions.github.io/storage/v2.0.0/schema.json", - "https://stac-extensions.github.io/timestamps/v1.1.0/schema.json", - "https://schemas.stacspec.org/v1.1.0/item-spec/json-schema/item.json", - ], - "valid_stac": True, - "asset_type": "ITEM", - "validation_method": "default", - }, - ] + + # Check that we got some messages back + assert len(stac.message) > 0 + + # Check each message has the required fields + required_fields = { + "version", + "path", + "schema", + "valid_stac", + "asset_type", + "validation_method", + } + for msg in stac.message: + # Check all required fields are present + assert all( + field in msg for field in required_fields + ), f"Missing required field in message: {msg}" + + # Check the message is for an ITEM + assert msg["asset_type"] == "ITEM" + + # Check the validation method is correct + assert msg["validation_method"] == "default" + + # Check the schema contains expected schemas (checking for a subset to be more resilient) + expected_schemas = { + "https://stac-extensions.github.io/eo/v2.0.0/schema.json", + "https://stac-extensions.github.io/file/v2.1.0/schema.json", + "https://schemas.stacspec.org/v1.1.0/item-spec/json-schema/item.json", + } + assert all( + schema in msg["schema"] for schema in expected_schemas + ), f"Missing expected schemas in {msg['schema']}" assert len(stac.message) == 10