Skip to content

Commit 8da801a

Browse files
committed
add test, update tests
1 parent afb1b9c commit 8da801a

File tree

5 files changed

+179
-253
lines changed

5 files changed

+179
-253
lines changed

stac_validator/stac_validator.py

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88
from .validate import StacValidate
99

1010

11-
def _print_summary(title: str, valid_count: int, total_count: int, obj_type: str = "STAC objects") -> None:
11+
def _print_summary(
12+
title: str, valid_count: int, total_count: int, obj_type: str = "STAC objects"
13+
) -> None:
1214
"""Helper function to print a consistent summary line.
13-
15+
1416
Args:
1517
title (str): Title of the summary section
1618
valid_count (int): Number of valid items
@@ -21,17 +23,19 @@ def _print_summary(title: str, valid_count: int, total_count: int, obj_type: str
2123
click.secho(f"{title}:", bold=True)
2224
if total_count > 0:
2325
percentage = (valid_count / total_count) * 100
24-
click.secho(f" {obj_type.capitalize()} passed: {valid_count}/{total_count} ({percentage:.1f}%)")
26+
click.secho(
27+
f" {obj_type.capitalize()} passed: {valid_count}/{total_count} ({percentage:.1f}%)"
28+
)
2529
else:
2630
click.secho(f" No {obj_type} found to validate")
2731

2832

2933
def format_duration(seconds: float) -> str:
3034
"""Format duration in seconds to a human-readable string.
31-
35+
3236
Args:
3337
seconds (float): Duration in seconds
34-
38+
3539
Returns:
3640
str: Formatted duration string (e.g., '1m 23.45s' or '456.78ms')
3741
"""
@@ -91,41 +95,45 @@ def collections_summary(message: List[Dict[str, Any]]) -> None:
9195

9296
def recursive_validation_summary(message: List[Dict[str, Any]]) -> None:
9397
"""Prints a summary of the recursive validation results.
94-
98+
9599
Args:
96100
message (List[Dict[str, Any]]): The validation results from recursive validation.
97-
101+
98102
Returns:
99103
None
100104
"""
101105
# Count valid and total objects by type
102106
type_counts = {}
103107
total_valid = 0
104-
108+
105109
for item in message:
106110
if not isinstance(item, dict):
107111
continue
108-
112+
109113
obj_type = item.get("asset_type", "unknown").lower()
110114
is_valid = item.get("valid_stac", False) is True
111-
115+
112116
if obj_type not in type_counts:
113117
type_counts[obj_type] = {"valid": 0, "total": 0}
114-
118+
115119
type_counts[obj_type]["total"] += 1
116120
if is_valid:
117121
type_counts[obj_type]["valid"] += 1
118122
total_valid += 1
119-
123+
120124
# Print overall summary
121125
_print_summary("-- Recursive Validation Summary", total_valid, len(message))
122-
126+
123127
# Print breakdown by type if there are multiple types
124128
if len(type_counts) > 1:
125129
click.secho("\n Breakdown by type:")
126130
for obj_type, counts in sorted(type_counts.items()):
127-
percentage = (counts["valid"] / counts["total"]) * 100 if counts["total"] > 0 else 0
128-
click.secho(f" {obj_type.capitalize()}: {counts['valid']}/{counts['total']} ({percentage:.1f}%)")
131+
percentage = (
132+
(counts["valid"] / counts["total"]) * 100 if counts["total"] > 0 else 0
133+
)
134+
click.secho(
135+
f" {obj_type.capitalize()}: {counts['valid']}/{counts['total']} ({percentage:.1f}%)"
136+
)
129137

130138

131139
@click.command()
@@ -281,12 +289,12 @@ def main(
281289
"""
282290
start_time = time.time()
283291
valid = True
284-
292+
285293
if schema_map == ():
286294
schema_map_dict: Optional[Dict[str, str]] = None
287295
else:
288296
schema_map_dict = dict(schema_map)
289-
297+
290298
stac = StacValidate(
291299
stac_file=stac_file,
292300
collections=collections,
@@ -308,34 +316,36 @@ def main(
308316
pydantic=pydantic,
309317
verbose=verbose,
310318
)
311-
319+
312320
try:
313321
if not item_collection and not collections:
314322
valid = stac.run()
315323
elif collections:
316324
stac.validate_collections()
317325
else:
318326
stac.validate_item_collection()
319-
327+
320328
message = stac.message
321329
if "version" in message[0]:
322330
print_update_message(message[0]["version"])
323331

324332
if no_output is False:
325333
click.echo(json.dumps(message, indent=4))
326-
334+
327335
# Print appropriate summary based on validation mode
328336
if item_collection:
329337
item_collection_summary(message)
330338
elif collections:
331339
collections_summary(message)
332340
elif recursive:
333341
recursive_validation_summary(message)
334-
342+
335343
finally:
336344
# Always print the duration, even if validation fails
337345
duration = time.time() - start_time
338-
click.secho(f"\nValidation completed in {format_duration(duration)}", fg='green')
346+
click.secho(
347+
f"\nValidation completed in {format_duration(duration)}", fg="green"
348+
)
339349
click.secho()
340350
sys.exit(0 if valid else 1)
341351

stac_validator/validate.py

Lines changed: 60 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,11 @@ def _create_verbose_err_msg(self, error_input: Any) -> Union[Dict[str, Any], str
242242
return str(error_input) # Fallback to string representation
243243

244244
def create_err_msg(
245-
self, err_type: str, err_msg: str, error_obj: Optional[Exception] = None
245+
self,
246+
err_type: str,
247+
err_msg: str,
248+
error_obj: Optional[Exception] = None,
249+
schema_uri: str = "",
246250
) -> Dict[str, Union[str, bool, List[str], Dict[str, Any]]]:
247251
"""
248252
Create a standardized error message dictionary and mark validation as failed.
@@ -251,26 +255,59 @@ def create_err_msg(
251255
err_type (str): The type of error.
252256
err_msg (str): The error message.
253257
error_obj (Optional[Exception]): The raw exception object for verbose details.
258+
schema_uri (str, optional): The URI of the schema that failed validation.
254259
255260
Returns:
256261
dict: Dictionary containing error information.
257262
"""
258263
self.valid = False
259264

260265
# Ensure all values are of the correct type
261-
version_str: str = str(self.version) if self.version is not None else ""
262-
path_str: str = str(self.stac_file) if self.stac_file is not None else ""
266+
if not isinstance(err_type, str):
267+
err_type = str(err_type)
268+
if not isinstance(err_msg, str):
269+
err_msg = str(err_msg)
270+
271+
# Initialize the message with common fields
272+
message: Dict[str, Any] = {
273+
"version": str(self.version) if hasattr(self, "version") else "",
274+
"path": str(self.stac_file) if hasattr(self, "stac_file") else "",
275+
"schema": (
276+
[self._original_schema_paths.get(self.schema, self.schema)]
277+
if hasattr(self, "schema")
278+
else [""]
279+
),
280+
"valid_stac": False,
281+
"error_type": err_type,
282+
"error_message": err_msg,
283+
"failed_schema": schema_uri if schema_uri else "",
284+
"recommendation": "For more accurate error information, rerun with --verbose.",
285+
}
286+
287+
# Add asset_type and validation_method if available
288+
if hasattr(self, "stac_content"):
289+
try:
290+
stac_type = get_stac_type(self.stac_content)
291+
if stac_type:
292+
message["asset_type"] = stac_type.upper()
293+
message["validation_method"] = (
294+
"recursive"
295+
if hasattr(self, "recursive") and self.recursive
296+
else "default"
297+
)
298+
except Exception: # noqa: BLE001
299+
pass
263300

264301
# Ensure schema is properly typed
265302
schema_value: str = ""
266303
if self.schema is not None:
267304
schema_value = str(self.schema)
268305
schema_field: List[str] = [schema_value] if schema_value else []
269306

270-
# Initialize the message with common fields
271-
message: Dict[str, Union[str, bool, List[str], Dict[str, Any]]] = {
272-
"version": version_str,
273-
"path": path_str,
307+
# Initialize the error message with common fields
308+
error_message: Dict[str, Union[str, bool, List[str], Dict[str, Any]]] = {
309+
"version": str(self.version) if self.version is not None else "",
310+
"path": str(self.stac_file) if self.stac_file is not None else "",
274311
"schema": schema_field, # All schemas that were checked
275312
"valid_stac": False,
276313
"error_type": err_type,
@@ -281,31 +318,31 @@ def create_err_msg(
281318
# Try to extract the failed schema from the error message if it's a validation error
282319
if error_obj and hasattr(error_obj, "schema"):
283320
if isinstance(error_obj.schema, dict) and "$id" in error_obj.schema:
284-
message["failed_schema"] = error_obj.schema["$id"]
321+
error_message["failed_schema"] = error_obj.schema["$id"]
285322
elif hasattr(error_obj, "schema_url"):
286-
message["failed_schema"] = error_obj.schema_url
323+
error_message["failed_schema"] = error_obj.schema_url
287324
# If we can't find a schema ID, try to get it from the schema map
288325
elif schema_field and len(schema_field) == 1:
289-
message["failed_schema"] = schema_field[0]
326+
error_message["failed_schema"] = schema_field[0]
290327

291328
if self.verbose and error_obj is not None:
292329
verbose_err = self._create_verbose_err_msg(error_obj)
293330
if isinstance(verbose_err, dict):
294-
message["error_verbose"] = verbose_err
331+
error_message["error_verbose"] = verbose_err
295332
else:
296-
message["error_verbose"] = {"detail": str(verbose_err)}
333+
error_message["error_verbose"] = {"detail": str(verbose_err)}
297334
# Add recommendation to check the schema if the error is not clear
298-
if "failed_schema" in message and message["failed_schema"]:
299-
message["recommendation"] = (
335+
if error_message.get("failed_schema"):
336+
error_message["recommendation"] = (
300337
"If the error is unclear, please check the schema documentation at: "
301-
f"{message['failed_schema']}"
338+
f"{error_message['failed_schema']}"
302339
)
303340
else:
304-
message["recommendation"] = (
341+
error_message["recommendation"] = (
305342
"For more accurate error information, rerun with --verbose."
306343
)
307344

308-
return message
345+
return error_message
309346

310347
def create_links_message(self) -> Dict:
311348
"""
@@ -604,7 +641,9 @@ def recursive_validator(self, stac_type: str) -> bool:
604641
err_type="JSONSchemaValidationError",
605642
err_msg=err_msg,
606643
error_obj=e,
607-
schema_uri=e.schema.get("$id", "") if hasattr(e, "schema") else "",
644+
schema_uri=(
645+
e.schema.get("$id", "") if hasattr(e, "schema") else ""
646+
),
608647
)
609648
)
610649
self.message.append(message)
@@ -686,7 +725,9 @@ def recursive_validator(self, stac_type: str) -> bool:
686725
if link["rel"] == "item":
687726
self.schema = set_schema_addr(self.version, stac_type.lower())
688727
message = self.create_message(stac_type, "recursive")
689-
message["validator_engine"] = "pydantic" if self.pydantic else "jsonschema"
728+
message["validator_engine"] = (
729+
"pydantic" if self.pydantic else "jsonschema"
730+
)
690731
try:
691732
if self.pydantic:
692733
# Set pydantic model info in schema field for child items

tests/test_pydantic.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,34 @@ def test_pydantic_invalid_item():
5959
assert stac.message[0]["validation_method"] == "pydantic"
6060
assert "error_type" in stac.message[0]
6161
assert "error_message" in stac.message[0]
62+
63+
64+
def test_pydantic_recursive():
65+
"""Test pydantic validation in recursive mode."""
66+
stac_file = "tests/test_data/local_cat/example-catalog/catalog.json"
67+
stac = stac_validator.StacValidate(
68+
stac_file, pydantic=True, recursive=True, max_depth=2 # Limit depth for testing
69+
)
70+
stac.run()
71+
72+
# Check that we have validation messages
73+
assert len(stac.message) > 0
74+
75+
# Check each validation message
76+
for msg in stac.message:
77+
# Check that validator_engine is set to pydantic
78+
assert msg["validator_engine"] == "pydantic"
79+
80+
# Check that validation_method is recursive
81+
assert msg["validation_method"] == "recursive"
82+
83+
# Check that valid_stac is a boolean
84+
assert isinstance(msg["valid_stac"], bool)
85+
86+
# Check that schema is set to pydantic model based on asset type
87+
if msg["asset_type"] == "ITEM":
88+
assert msg["schema"] == ["stac-pydantic Item model"]
89+
elif msg["asset_type"] == "COLLECTION":
90+
assert msg["schema"] == ["stac-pydantic Collection model"]
91+
elif msg["asset_type"] == "CATALOG":
92+
assert msg["schema"] == ["stac-pydantic Catalog model"]

0 commit comments

Comments
 (0)