11import json
22import sys
3+ import time
34from typing import Any , Dict , List , Optional , Tuple
45
56import click # type: ignore
67
78from .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+
1050def 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
4983def 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"\n Validation completed in { format_duration (duration )} " , fg = "green"
348+ )
349+ click .secho ()
263350 sys .exit (0 if valid else 1 )
264351
265352
0 commit comments