stac-valid 4.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- stac_valid-4.2.0.dist-info/METADATA +1218 -0
- stac_valid-4.2.0.dist-info/RECORD +12 -0
- stac_valid-4.2.0.dist-info/WHEEL +5 -0
- stac_valid-4.2.0.dist-info/entry_points.txt +3 -0
- stac_valid-4.2.0.dist-info/licenses/LICENSE +202 -0
- stac_valid-4.2.0.dist-info/top_level.txt +1 -0
- stac_validator/__init__.py +0 -0
- stac_validator/batch_validator.py +520 -0
- stac_validator/fast_validator.py +312 -0
- stac_validator/stac_validator.py +574 -0
- stac_validator/utilities.py +547 -0
- stac_validator/validate.py +1107 -0
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sys
|
|
3
|
+
import time
|
|
4
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
5
|
+
|
|
6
|
+
import click # type: ignore
|
|
7
|
+
|
|
8
|
+
from .batch_validator import validate_concurrently
|
|
9
|
+
from .fast_validator import FastValidator
|
|
10
|
+
from .utilities import set_schema_cache_size
|
|
11
|
+
from .validate import StacValidate
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _print_summary(
|
|
15
|
+
title: str, valid_count: int, total_count: int, obj_type: str = "STAC objects"
|
|
16
|
+
) -> None:
|
|
17
|
+
"""Helper function to print a consistent summary line.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
title (str): Title of the summary section
|
|
21
|
+
valid_count (int): Number of valid items
|
|
22
|
+
total_count (int): Total number of items
|
|
23
|
+
obj_type (str): Type of objects being counted (e.g., 'items', 'collections')
|
|
24
|
+
"""
|
|
25
|
+
click.secho()
|
|
26
|
+
click.secho(f"{title}:", bold=True)
|
|
27
|
+
if total_count > 0:
|
|
28
|
+
percentage = (valid_count / total_count) * 100
|
|
29
|
+
click.secho(
|
|
30
|
+
f" {obj_type.capitalize()} passed: {valid_count}/{total_count} ({percentage:.1f}%)"
|
|
31
|
+
)
|
|
32
|
+
else:
|
|
33
|
+
click.secho(f" No {obj_type} found to validate")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def format_duration(seconds: float) -> str:
|
|
37
|
+
"""Format duration in seconds to a human-readable string.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
seconds (float): Duration in seconds
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
str: Formatted duration string (e.g., '1m 23.45s' or '456.78ms')
|
|
44
|
+
"""
|
|
45
|
+
if seconds < 1.0:
|
|
46
|
+
return f"{seconds * 1000:.2f}ms"
|
|
47
|
+
minutes, seconds = divmod(seconds, 60)
|
|
48
|
+
if minutes > 0:
|
|
49
|
+
return f"{int(minutes)}m {seconds:.2f}s"
|
|
50
|
+
return f"{seconds:.2f}s"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def print_update_message(version: str) -> None:
|
|
54
|
+
"""Prints an update message for `stac-validator` based on the version of the
|
|
55
|
+
STAC file being validated.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
version (str): The version of the STAC file being validated.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
None
|
|
62
|
+
"""
|
|
63
|
+
click.secho()
|
|
64
|
+
if version != "1.1.0":
|
|
65
|
+
click.secho(
|
|
66
|
+
f"Please upgrade from version {version} to version 1.1.0!", fg="red"
|
|
67
|
+
)
|
|
68
|
+
else:
|
|
69
|
+
click.secho("Thanks for using STAC version 1.1.0!", fg="green")
|
|
70
|
+
click.secho()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def item_collection_summary(message: List[Dict[str, Any]]) -> None:
|
|
74
|
+
"""Prints a summary of the validation results for an item collection response.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
message (List[Dict[str, Any]]): The validation results for the item collection.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
None
|
|
81
|
+
"""
|
|
82
|
+
valid_count = sum(1 for item in message if item.get("valid_stac") is True)
|
|
83
|
+
_print_summary("-- Item Collection Summary", valid_count, len(message), "items")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def collections_summary(message: List[Dict[str, Any]]) -> None:
|
|
87
|
+
"""Prints a summary of the validation results for a collections response.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
message (List[Dict[str, Any]]): The validation results for the collections.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
None
|
|
94
|
+
"""
|
|
95
|
+
valid_count = sum(1 for coll in message if coll.get("valid_stac") is True)
|
|
96
|
+
_print_summary("-- Collections Summary", valid_count, len(message), "collections")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def recursive_validation_summary(message: List[Dict[str, Any]]) -> None:
|
|
100
|
+
"""Prints a summary of the recursive validation results.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
message (List[Dict[str, Any]]): The validation results from recursive validation.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
None
|
|
107
|
+
"""
|
|
108
|
+
# Count valid and total objects by type
|
|
109
|
+
type_counts = {}
|
|
110
|
+
total_valid = 0
|
|
111
|
+
|
|
112
|
+
for item in message:
|
|
113
|
+
if not isinstance(item, dict):
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
obj_type = item.get("asset_type", "unknown").lower()
|
|
117
|
+
is_valid = item.get("valid_stac", False) is True
|
|
118
|
+
|
|
119
|
+
if obj_type not in type_counts:
|
|
120
|
+
type_counts[obj_type] = {"valid": 0, "total": 0}
|
|
121
|
+
|
|
122
|
+
type_counts[obj_type]["total"] += 1
|
|
123
|
+
if is_valid:
|
|
124
|
+
type_counts[obj_type]["valid"] += 1
|
|
125
|
+
total_valid += 1
|
|
126
|
+
|
|
127
|
+
# Print overall summary
|
|
128
|
+
_print_summary("-- Recursive Validation Summary", total_valid, len(message))
|
|
129
|
+
|
|
130
|
+
# Print breakdown by type if there are multiple types
|
|
131
|
+
if len(type_counts) > 1:
|
|
132
|
+
click.secho("\n Breakdown by type:")
|
|
133
|
+
for obj_type, counts in sorted(type_counts.items()):
|
|
134
|
+
percentage = (
|
|
135
|
+
(counts["valid"] / counts["total"]) * 100 if counts["total"] > 0 else 0
|
|
136
|
+
)
|
|
137
|
+
click.secho(
|
|
138
|
+
f" {obj_type.capitalize()}: {counts['valid']}/{counts['total']} ({percentage:.1f}%)"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@click.command()
|
|
143
|
+
@click.argument("stac_file")
|
|
144
|
+
@click.option(
|
|
145
|
+
"--core", is_flag=True, help="Validate core stac object only without extensions."
|
|
146
|
+
)
|
|
147
|
+
@click.option("--extensions", is_flag=True, help="Validate extensions only.")
|
|
148
|
+
@click.option(
|
|
149
|
+
"--links",
|
|
150
|
+
is_flag=True,
|
|
151
|
+
help="Additionally validate links. Only works with default mode.",
|
|
152
|
+
)
|
|
153
|
+
@click.option(
|
|
154
|
+
"--assets",
|
|
155
|
+
is_flag=True,
|
|
156
|
+
help="Additionally validate assets. Only works with default mode.",
|
|
157
|
+
)
|
|
158
|
+
@click.option(
|
|
159
|
+
"--custom",
|
|
160
|
+
"-c",
|
|
161
|
+
default="",
|
|
162
|
+
help="Validate against a custom schema (local filepath or remote schema).",
|
|
163
|
+
)
|
|
164
|
+
@click.option(
|
|
165
|
+
"--schema-config",
|
|
166
|
+
"-sc",
|
|
167
|
+
default="",
|
|
168
|
+
help="Validate against a custom schema config (local filepath or remote schema config).",
|
|
169
|
+
)
|
|
170
|
+
@click.option(
|
|
171
|
+
"--schema-map",
|
|
172
|
+
"-s",
|
|
173
|
+
type=(str, str),
|
|
174
|
+
multiple=True,
|
|
175
|
+
help="Schema path to replaced by (local) schema path during validation. Can be used multiple times.",
|
|
176
|
+
)
|
|
177
|
+
@click.option(
|
|
178
|
+
"--recursive",
|
|
179
|
+
"-r",
|
|
180
|
+
is_flag=True,
|
|
181
|
+
help="Recursively validate all related stac objects.",
|
|
182
|
+
)
|
|
183
|
+
@click.option(
|
|
184
|
+
"--max-depth",
|
|
185
|
+
"-m",
|
|
186
|
+
type=int,
|
|
187
|
+
help="Maximum depth to traverse when recursing. Omit this argument to get full recursion. Ignored if `recursive == False`.",
|
|
188
|
+
)
|
|
189
|
+
@click.option(
|
|
190
|
+
"--collections",
|
|
191
|
+
is_flag=True,
|
|
192
|
+
help="Validate /collections response.",
|
|
193
|
+
)
|
|
194
|
+
@click.option(
|
|
195
|
+
"--item-collection",
|
|
196
|
+
"--feature-collection",
|
|
197
|
+
is_flag=True,
|
|
198
|
+
help="Validate item collection response. Can be combined with --pages. Defaults to one page.",
|
|
199
|
+
)
|
|
200
|
+
@click.option(
|
|
201
|
+
"--no-assets-urls",
|
|
202
|
+
is_flag=True,
|
|
203
|
+
help="Disables the opening of href links when validating assets (enabled by default).",
|
|
204
|
+
)
|
|
205
|
+
@click.option(
|
|
206
|
+
"--header",
|
|
207
|
+
type=(str, str),
|
|
208
|
+
multiple=True,
|
|
209
|
+
help="HTTP header to include in the requests. Can be used multiple times.",
|
|
210
|
+
)
|
|
211
|
+
@click.option(
|
|
212
|
+
"--pages",
|
|
213
|
+
"-p",
|
|
214
|
+
type=int,
|
|
215
|
+
help="Maximum number of pages to validate via --item-collection. Defaults to one page.",
|
|
216
|
+
)
|
|
217
|
+
@click.option(
|
|
218
|
+
"-t",
|
|
219
|
+
"--trace-recursion",
|
|
220
|
+
is_flag=True,
|
|
221
|
+
help="Enables verbose output for recursive mode.",
|
|
222
|
+
)
|
|
223
|
+
@click.option("--no_output", is_flag=True, help="Do not print output to console.")
|
|
224
|
+
@click.option(
|
|
225
|
+
"--log_file",
|
|
226
|
+
default="",
|
|
227
|
+
help="Save full recursive output to log file (local filepath).",
|
|
228
|
+
)
|
|
229
|
+
@click.option(
|
|
230
|
+
"--pydantic",
|
|
231
|
+
is_flag=True,
|
|
232
|
+
help="Validate using stac-pydantic models for enhanced type checking and validation.",
|
|
233
|
+
)
|
|
234
|
+
@click.option(
|
|
235
|
+
"--verbose",
|
|
236
|
+
is_flag=True,
|
|
237
|
+
help="Enable verbose output. This will output additional information during validation.",
|
|
238
|
+
)
|
|
239
|
+
@click.option(
|
|
240
|
+
"--schema-cache-size",
|
|
241
|
+
type=int,
|
|
242
|
+
default=None,
|
|
243
|
+
help="Max number of schema entries to cache in memory. Defaults to 16.",
|
|
244
|
+
)
|
|
245
|
+
def main(
|
|
246
|
+
stac_file: str,
|
|
247
|
+
collections: bool,
|
|
248
|
+
item_collection: bool,
|
|
249
|
+
no_assets_urls: bool,
|
|
250
|
+
header: list,
|
|
251
|
+
pages: int,
|
|
252
|
+
recursive: bool,
|
|
253
|
+
max_depth: int,
|
|
254
|
+
core: bool,
|
|
255
|
+
extensions: bool,
|
|
256
|
+
links: bool,
|
|
257
|
+
assets: bool,
|
|
258
|
+
custom: str,
|
|
259
|
+
schema_config: str,
|
|
260
|
+
schema_map: List[Tuple],
|
|
261
|
+
trace_recursion: bool,
|
|
262
|
+
no_output: bool,
|
|
263
|
+
log_file: str,
|
|
264
|
+
pydantic: bool,
|
|
265
|
+
verbose: bool = False,
|
|
266
|
+
schema_cache_size: Optional[int] = None,
|
|
267
|
+
):
|
|
268
|
+
"""Validate a STAC file against the STAC specification.
|
|
269
|
+
|
|
270
|
+
Prints validation results to the console as JSON.
|
|
271
|
+
Exits with status code 0 if valid, 1 if invalid.
|
|
272
|
+
"""
|
|
273
|
+
start_time = time.time()
|
|
274
|
+
valid = True
|
|
275
|
+
|
|
276
|
+
if schema_map == ():
|
|
277
|
+
schema_map_dict: Optional[Dict[str, str]] = None
|
|
278
|
+
else:
|
|
279
|
+
schema_map_dict = dict(schema_map)
|
|
280
|
+
|
|
281
|
+
if schema_cache_size is not None:
|
|
282
|
+
if schema_cache_size < 0:
|
|
283
|
+
raise click.BadParameter(
|
|
284
|
+
"must be greater than or equal to 0",
|
|
285
|
+
param_hint="--schema-cache-size",
|
|
286
|
+
)
|
|
287
|
+
set_schema_cache_size(schema_cache_size)
|
|
288
|
+
|
|
289
|
+
stac = StacValidate(
|
|
290
|
+
stac_file=stac_file,
|
|
291
|
+
collections=collections,
|
|
292
|
+
item_collection=item_collection,
|
|
293
|
+
pages=pages,
|
|
294
|
+
recursive=recursive,
|
|
295
|
+
max_depth=max_depth,
|
|
296
|
+
core=core,
|
|
297
|
+
links=links,
|
|
298
|
+
assets=assets,
|
|
299
|
+
assets_open_urls=not no_assets_urls,
|
|
300
|
+
headers=dict(header),
|
|
301
|
+
extensions=extensions,
|
|
302
|
+
custom=custom,
|
|
303
|
+
schema_config=schema_config,
|
|
304
|
+
schema_map=schema_map_dict,
|
|
305
|
+
trace_recursion=trace_recursion,
|
|
306
|
+
log=log_file,
|
|
307
|
+
pydantic=pydantic,
|
|
308
|
+
verbose=verbose,
|
|
309
|
+
show_progress=True,
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
try:
|
|
313
|
+
if not item_collection and not collections:
|
|
314
|
+
valid = stac.run()
|
|
315
|
+
elif collections:
|
|
316
|
+
stac.validate_collections()
|
|
317
|
+
else:
|
|
318
|
+
stac.validate_item_collection()
|
|
319
|
+
|
|
320
|
+
message = stac.message
|
|
321
|
+
if "version" in message[0]:
|
|
322
|
+
print_update_message(message[0]["version"])
|
|
323
|
+
|
|
324
|
+
if no_output is False:
|
|
325
|
+
click.echo(json.dumps(message, indent=4))
|
|
326
|
+
|
|
327
|
+
# Print appropriate summary based on validation mode
|
|
328
|
+
if item_collection:
|
|
329
|
+
item_collection_summary(message)
|
|
330
|
+
elif collections:
|
|
331
|
+
collections_summary(message)
|
|
332
|
+
elif recursive:
|
|
333
|
+
recursive_validation_summary(message)
|
|
334
|
+
|
|
335
|
+
finally:
|
|
336
|
+
# Always print the duration, even if validation fails
|
|
337
|
+
duration = time.time() - start_time
|
|
338
|
+
click.secho(
|
|
339
|
+
f"\nValidation completed in {format_duration(duration)}", fg="green"
|
|
340
|
+
)
|
|
341
|
+
click.secho()
|
|
342
|
+
sys.exit(0 if valid else 1)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@click.command()
|
|
346
|
+
@click.argument("files", nargs=-1, required=True, type=click.Path(exists=True))
|
|
347
|
+
@click.option(
|
|
348
|
+
"--cores",
|
|
349
|
+
type=int,
|
|
350
|
+
default=None,
|
|
351
|
+
help="Number of CPU cores to use for parallel validation. Defaults to all available cores.",
|
|
352
|
+
)
|
|
353
|
+
@click.option(
|
|
354
|
+
"--no-progress",
|
|
355
|
+
is_flag=True,
|
|
356
|
+
help="Disable progress bar during validation.",
|
|
357
|
+
)
|
|
358
|
+
@click.option(
|
|
359
|
+
"--no-output",
|
|
360
|
+
is_flag=True,
|
|
361
|
+
help="Do not print output to console.",
|
|
362
|
+
)
|
|
363
|
+
@click.option(
|
|
364
|
+
"--item-collection",
|
|
365
|
+
"--feature-collection",
|
|
366
|
+
is_flag=True,
|
|
367
|
+
help="Treat files as ItemCollections and validate each item individually.",
|
|
368
|
+
)
|
|
369
|
+
@click.option(
|
|
370
|
+
"--verbose",
|
|
371
|
+
is_flag=True,
|
|
372
|
+
help="Show full JSON output for all items. By default, only invalid items are shown.",
|
|
373
|
+
)
|
|
374
|
+
@click.option(
|
|
375
|
+
"--schema-cache-size",
|
|
376
|
+
type=int,
|
|
377
|
+
default=16,
|
|
378
|
+
help="Max number of schema entries to cache per worker process. Defaults to 16.",
|
|
379
|
+
)
|
|
380
|
+
@click.option(
|
|
381
|
+
"--batch-size",
|
|
382
|
+
type=click.IntRange(min=1),
|
|
383
|
+
default=2000,
|
|
384
|
+
help="Batch size for chunked processing. Larger batches use more memory but may be faster. Defaults to 2000.",
|
|
385
|
+
)
|
|
386
|
+
def batch(
|
|
387
|
+
files: Tuple[str, ...],
|
|
388
|
+
cores: Optional[int],
|
|
389
|
+
no_progress: bool,
|
|
390
|
+
no_output: bool,
|
|
391
|
+
item_collection: bool,
|
|
392
|
+
verbose: bool,
|
|
393
|
+
schema_cache_size: int,
|
|
394
|
+
batch_size: int,
|
|
395
|
+
):
|
|
396
|
+
"""Validate multiple STAC files concurrently using all available CPU cores.
|
|
397
|
+
|
|
398
|
+
This command uses multiprocessing to validate STAC files in parallel,
|
|
399
|
+
bypassing Python's Global Interpreter Lock (GIL) for maximum performance.
|
|
400
|
+
Each CPU core gets its own schema cache, which is warmed up on the first
|
|
401
|
+
file and reused for subsequent files.
|
|
402
|
+
|
|
403
|
+
Examples:
|
|
404
|
+
|
|
405
|
+
# Validate all JSON files in a directory
|
|
406
|
+
stac-validator batch *.json
|
|
407
|
+
|
|
408
|
+
# Validate specific files
|
|
409
|
+
stac-validator batch file1.json file2.json file3.json
|
|
410
|
+
|
|
411
|
+
# Use only 4 cores
|
|
412
|
+
stac-validator batch *.json --cores 4
|
|
413
|
+
|
|
414
|
+
# Disable progress bar
|
|
415
|
+
stac-validator batch *.json --no-progress
|
|
416
|
+
"""
|
|
417
|
+
if not files:
|
|
418
|
+
click.secho("Error: No files provided", fg="red")
|
|
419
|
+
sys.exit(1)
|
|
420
|
+
|
|
421
|
+
start_time = time.time()
|
|
422
|
+
|
|
423
|
+
try:
|
|
424
|
+
# Configure schema cache size BEFORE spawning workers
|
|
425
|
+
# This ensures all worker processes inherit the configured cache size
|
|
426
|
+
if schema_cache_size != 16:
|
|
427
|
+
set_schema_cache_size(schema_cache_size)
|
|
428
|
+
|
|
429
|
+
# Get optimal worker count
|
|
430
|
+
from .batch_validator import get_optimal_worker_count
|
|
431
|
+
|
|
432
|
+
optimal_workers = get_optimal_worker_count(cores)
|
|
433
|
+
|
|
434
|
+
# Validate concurrently
|
|
435
|
+
results = validate_concurrently(
|
|
436
|
+
list(files),
|
|
437
|
+
max_workers=cores,
|
|
438
|
+
show_progress=not no_progress,
|
|
439
|
+
feature_collection=item_collection,
|
|
440
|
+
batch_size=batch_size,
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
# Calculate statistics
|
|
444
|
+
valid_count = sum(1 for r in results if r.get("valid_stac", False))
|
|
445
|
+
invalid_count = len(results) - valid_count
|
|
446
|
+
|
|
447
|
+
# Print details of failed validations first (only in non-verbose mode)
|
|
448
|
+
if not no_output and invalid_count > 0 and not verbose:
|
|
449
|
+
click.secho("Failed validations:", fg="red")
|
|
450
|
+
|
|
451
|
+
# Group errors by (message, schema) and collect item IDs
|
|
452
|
+
from collections import defaultdict
|
|
453
|
+
|
|
454
|
+
error_groups = defaultdict(list)
|
|
455
|
+
|
|
456
|
+
for result in results:
|
|
457
|
+
if not result.get("valid_stac", False):
|
|
458
|
+
# Try to get item ID from result, fallback to path
|
|
459
|
+
item_id = result.get("item_id", result.get("path", "unknown"))
|
|
460
|
+
|
|
461
|
+
# Handle both old format (errors array) and new format (error_message/failed_schema)
|
|
462
|
+
if "errors" in result:
|
|
463
|
+
# Old format: errors array with dict entries
|
|
464
|
+
for error in result["errors"]:
|
|
465
|
+
if isinstance(error, dict):
|
|
466
|
+
error_msg = error.get("message", str(error))
|
|
467
|
+
error_schema = error.get("schema", "")
|
|
468
|
+
error_key = (error_msg, error_schema)
|
|
469
|
+
else:
|
|
470
|
+
error_key = (str(error), "")
|
|
471
|
+
error_groups[error_key].append(item_id)
|
|
472
|
+
elif "error_message" in result:
|
|
473
|
+
# New format: error_message and failed_schema fields
|
|
474
|
+
error_msg = result.get("error_message", "")
|
|
475
|
+
error_schema = result.get("failed_schema", "")
|
|
476
|
+
error_key = (error_msg, error_schema)
|
|
477
|
+
error_groups[error_key].append(item_id)
|
|
478
|
+
|
|
479
|
+
# Print grouped errors with compact formatting
|
|
480
|
+
for idx, ((error_msg, error_schema), item_ids) in enumerate(
|
|
481
|
+
sorted(error_groups.items())
|
|
482
|
+
):
|
|
483
|
+
# Add blank line between error sections (but not before the first one)
|
|
484
|
+
if idx > 0:
|
|
485
|
+
click.secho()
|
|
486
|
+
|
|
487
|
+
# Format item IDs compactly (max 5 per line)
|
|
488
|
+
if len(item_ids) <= 5:
|
|
489
|
+
ids_str = ", ".join(str(id) for id in item_ids)
|
|
490
|
+
click.secho(f" [{ids_str}]", fg="red")
|
|
491
|
+
else:
|
|
492
|
+
# For many items, show first few and count
|
|
493
|
+
first_few = ", ".join(str(id) for id in item_ids[:5])
|
|
494
|
+
remaining = len(item_ids) - 5
|
|
495
|
+
click.secho(f" [{first_few}, ... +{remaining} more]", fg="red")
|
|
496
|
+
|
|
497
|
+
# Display error message
|
|
498
|
+
click.secho(f" - {error_msg}", fg="yellow")
|
|
499
|
+
|
|
500
|
+
# Display schema if available
|
|
501
|
+
if error_schema:
|
|
502
|
+
click.secho(f" Schema: {error_schema}", fg="cyan")
|
|
503
|
+
|
|
504
|
+
# Print full JSON output in verbose mode
|
|
505
|
+
if not no_output and verbose:
|
|
506
|
+
click.echo(json.dumps(results, indent=4))
|
|
507
|
+
|
|
508
|
+
# Print summary at the bottom
|
|
509
|
+
click.secho()
|
|
510
|
+
click.secho("Validation Summary:", bold=True)
|
|
511
|
+
click.secho(f" Total files: {len(results)}")
|
|
512
|
+
click.secho(f" CPU cores used: {optimal_workers}")
|
|
513
|
+
click.secho(f" ✅ Valid: {valid_count}")
|
|
514
|
+
click.secho(f" ❌ Invalid: {invalid_count}")
|
|
515
|
+
|
|
516
|
+
duration = time.time() - start_time
|
|
517
|
+
click.secho()
|
|
518
|
+
click.secho(f"Validation completed in {format_duration(duration)}", fg="green")
|
|
519
|
+
click.secho()
|
|
520
|
+
|
|
521
|
+
sys.exit(0 if invalid_count == 0 else 1)
|
|
522
|
+
|
|
523
|
+
except Exception as e:
|
|
524
|
+
click.secho(f"Error during batch validation: {e}", fg="red")
|
|
525
|
+
sys.exit(1)
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
@click.command()
|
|
529
|
+
@click.argument("stac_file")
|
|
530
|
+
@click.option(
|
|
531
|
+
"--quiet",
|
|
532
|
+
"-q",
|
|
533
|
+
is_flag=True,
|
|
534
|
+
help="Suppress individual item logs.",
|
|
535
|
+
)
|
|
536
|
+
@click.option(
|
|
537
|
+
"--verbose",
|
|
538
|
+
"-v",
|
|
539
|
+
is_flag=True,
|
|
540
|
+
help="Show full validation logs for all items. By default, a limited sample of item logs is shown.",
|
|
541
|
+
)
|
|
542
|
+
def fast(stac_file: str, quiet: bool, verbose: bool):
|
|
543
|
+
"""High-speed validation using fastjsonschema and local caching."""
|
|
544
|
+
try:
|
|
545
|
+
fv = FastValidator(stac_file, quiet=quiet, verbose=verbose)
|
|
546
|
+
fv.run()
|
|
547
|
+
sys.exit(0 if fv.valid else 1)
|
|
548
|
+
except RuntimeError as e:
|
|
549
|
+
click.secho(f"\n🚨 FATAL ERROR: {e}", fg="red", bold=True)
|
|
550
|
+
sys.exit(1)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
@click.group()
|
|
554
|
+
def cli():
|
|
555
|
+
"""STAC Validator - Validate STAC files against the STAC specification.
|
|
556
|
+
|
|
557
|
+
\b
|
|
558
|
+
Usage:
|
|
559
|
+
stac-validator validate <file> [options]
|
|
560
|
+
stac-validator batch <files> [options]
|
|
561
|
+
stac-validator batch <file> --feature-collection [options]
|
|
562
|
+
stac-validator fast <file> [options]
|
|
563
|
+
"""
|
|
564
|
+
pass
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
# Register commands
|
|
568
|
+
cli.add_command(main, name="validate")
|
|
569
|
+
cli.add_command(batch, name="batch")
|
|
570
|
+
cli.add_command(fast, name="fast")
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
if __name__ == "__main__":
|
|
574
|
+
cli()
|