Skip to content

Commit 478d70f

Browse files
authored
Recursive summary, add pydantic support to --recursive mode (#265)
### 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
1 parent 3abdee2 commit 478d70f

File tree

7 files changed

+337
-280
lines changed

7 files changed

+337
-280
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,16 @@ The format is (loosely) based on [Keep a Changelog](http://keepachangelog.com/)
66

77
## [Unreleased]
88

9+
10+
## [v3.10.0] - 2025-07-20
11+
912
### Added
13+
- Added detailed recursive validation summary showing validation counts by STAC object type (Catalog, Collection, etc.) [#265](https://github.com/stac-utils/stac-validator/pull/265)
14+
- Added validation duration timing that shows total processing time in a human-readable format [#265](https://github.com/stac-utils/stac-validator/pull/265)
15+
- Added support for pydantic validation in recursive mode with proper schema reporting [#265](https://github.com/stac-utils/stac-validator/pull/265)
16+
17+
### Changed
18+
- Standardized summary output formatting across all validation modes for consistency [#265](https://github.com/stac-utils/stac-validator/pull/265)
1019

1120
## [v3.9.3] - 2025-06-28
1221

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from setuptools import setup
44

5-
__version__ = "3.9.3"
5+
__version__ = "3.10.0"
66

77
with open("README.md", "r") as fh:
88
long_description = fh.read()

stac_validator/stac_validator.py

Lines changed: 122 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,52 @@
11
import json
22
import sys
3+
import time
34
from typing import Any, Dict, List, Optional, Tuple
45

56
import click # type: ignore
67

78
from .validate import StacValidate
89

910

11+
def _print_summary(
12+
title: str, valid_count: int, total_count: int, obj_type: str = "STAC objects"
13+
) -> None:
14+
"""Helper function to print a consistent summary line.
15+
16+
Args:
17+
title (str): Title of the summary section
18+
valid_count (int): Number of valid items
19+
total_count (int): Total number of items
20+
obj_type (str): Type of objects being counted (e.g., 'items', 'collections')
21+
"""
22+
click.secho()
23+
click.secho(f"{title}:", bold=True)
24+
if total_count > 0:
25+
percentage = (valid_count / total_count) * 100
26+
click.secho(
27+
f" {obj_type.capitalize()} passed: {valid_count}/{total_count} ({percentage:.1f}%)"
28+
)
29+
else:
30+
click.secho(f" No {obj_type} found to validate")
31+
32+
33+
def format_duration(seconds: float) -> str:
34+
"""Format duration in seconds to a human-readable string.
35+
36+
Args:
37+
seconds (float): Duration in seconds
38+
39+
Returns:
40+
str: Formatted duration string (e.g., '1m 23.45s' or '456.78ms')
41+
"""
42+
if seconds < 1.0:
43+
return f"{seconds * 1000:.2f}ms"
44+
minutes, seconds = divmod(seconds, 60)
45+
if minutes > 0:
46+
return f"{int(minutes)}m {seconds:.2f}s"
47+
return f"{seconds:.2f}s"
48+
49+
1050
def print_update_message(version: str) -> None:
1151
"""Prints an update message for `stac-validator` based on the version of the
1252
STAC file being validated.
@@ -36,33 +76,64 @@ def item_collection_summary(message: List[Dict[str, Any]]) -> None:
3676
Returns:
3777
None
3878
"""
39-
valid_count = 0
40-
for item in message:
41-
if "valid_stac" in item and item["valid_stac"] is True:
42-
valid_count = valid_count + 1
43-
click.secho()
44-
click.secho("--item-collection summary", bold=True)
45-
click.secho(f"items_validated: {len(message)}")
46-
click.secho(f"valid_items: {valid_count}")
79+
valid_count = sum(1 for item in message if item.get("valid_stac") is True)
80+
_print_summary("-- Item Collection Summary", valid_count, len(message), "items")
4781

4882

4983
def collections_summary(message: List[Dict[str, Any]]) -> None:
50-
"""Prints a summary of the validation results for an item collection response.
84+
"""Prints a summary of the validation results for a collections response.
5185
5286
Args:
53-
message (List[Dict[str, Any]]): The validation results for the item collection.
87+
message (List[Dict[str, Any]]): The validation results for the collections.
5488
5589
Returns:
5690
None
5791
"""
58-
valid_count = 0
59-
for collection in message:
60-
if "valid_stac" in collection and collection["valid_stac"] is True:
61-
valid_count = valid_count + 1
62-
click.secho()
63-
click.secho("--collections summary", bold=True)
64-
click.secho(f"collections_validated: {len(message)}")
65-
click.secho(f"valid_collections: {valid_count}")
92+
valid_count = sum(1 for coll in message if coll.get("valid_stac") is True)
93+
_print_summary("-- Collections Summary", valid_count, len(message), "collections")
94+
95+
96+
def recursive_validation_summary(message: List[Dict[str, Any]]) -> None:
97+
"""Prints a summary of the recursive validation results.
98+
99+
Args:
100+
message (List[Dict[str, Any]]): The validation results from recursive validation.
101+
102+
Returns:
103+
None
104+
"""
105+
# Count valid and total objects by type
106+
type_counts = {}
107+
total_valid = 0
108+
109+
for item in message:
110+
if not isinstance(item, dict):
111+
continue
112+
113+
obj_type = item.get("asset_type", "unknown").lower()
114+
is_valid = item.get("valid_stac", False) is True
115+
116+
if obj_type not in type_counts:
117+
type_counts[obj_type] = {"valid": 0, "total": 0}
118+
119+
type_counts[obj_type]["total"] += 1
120+
if is_valid:
121+
type_counts[obj_type]["valid"] += 1
122+
total_valid += 1
123+
124+
# Print overall summary
125+
_print_summary("-- Recursive Validation Summary", total_valid, len(message))
126+
127+
# Print breakdown by type if there are multiple types
128+
if len(type_counts) > 1:
129+
click.secho("\n Breakdown by type:")
130+
for obj_type, counts in sorted(type_counts.items()):
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+
)
66137

67138

68139
@click.command()
@@ -182,15 +253,16 @@ def main(
182253
log_file: str,
183254
pydantic: bool,
184255
verbose: bool = False,
185-
) -> None:
256+
):
186257
"""Main function for the `stac-validator` command line tool. Validates a STAC file
187258
against the STAC specification and prints the validation results to the console as JSON.
188259
189260
Args:
190261
stac_file (str): Path to the STAC file to be validated.
191262
collections (bool): Validate response from /collections endpoint.
192263
item_collection (bool): Whether to validate item collection responses.
193-
no_assets_urls (bool): Whether to open href links when validating assets (enabled by default).
264+
no_assets_urls (bool): Whether to open href links when validating assets
265+
(enabled by default).
194266
headers (dict): HTTP headers to include in the requests.
195267
pages (int): Maximum number of pages to validate via `item_collection`.
196268
recursive (bool): Whether to recursively validate all related STAC objects.
@@ -215,11 +287,14 @@ def main(
215287
SystemExit: Exits the program with a status code of 0 if the STAC file is valid,
216288
or 1 if it is invalid.
217289
"""
290+
start_time = time.time()
218291
valid = True
292+
219293
if schema_map == ():
220294
schema_map_dict: Optional[Dict[str, str]] = None
221295
else:
222296
schema_map_dict = dict(schema_map)
297+
223298
stac = StacValidate(
224299
stac_file=stac_file,
225300
collections=collections,
@@ -241,25 +316,37 @@ def main(
241316
pydantic=pydantic,
242317
verbose=verbose,
243318
)
244-
if not item_collection and not collections:
245-
valid = stac.run()
246-
elif collections:
247-
stac.validate_collections()
248-
else:
249-
stac.validate_item_collection()
250319

251-
message = stac.message
252-
if "version" in message[0]:
253-
print_update_message(message[0]["version"])
320+
try:
321+
if not item_collection and not collections:
322+
valid = stac.run()
323+
elif collections:
324+
stac.validate_collections()
325+
else:
326+
stac.validate_item_collection()
327+
328+
message = stac.message
329+
if "version" in message[0]:
330+
print_update_message(message[0]["version"])
254331

255-
if no_output is False:
256-
click.echo(json.dumps(message, indent=4))
332+
if no_output is False:
333+
click.echo(json.dumps(message, indent=4))
257334

258-
if item_collection:
259-
item_collection_summary(message)
260-
elif collections:
261-
collections_summary(message)
335+
# Print appropriate summary based on validation mode
336+
if item_collection:
337+
item_collection_summary(message)
338+
elif collections:
339+
collections_summary(message)
340+
elif recursive:
341+
recursive_validation_summary(message)
262342

343+
finally:
344+
# Always print the duration, even if validation fails
345+
duration = time.time() - start_time
346+
click.secho(
347+
f"\nValidation completed in {format_duration(duration)}", fg="green"
348+
)
349+
click.secho()
263350
sys.exit(0 if valid else 1)
264351

265352

0 commit comments

Comments
 (0)