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,520 @@
|
|
|
1
|
+
"""Concurrent batch validation of STAC files using multiprocessing."""
|
|
2
|
+
|
|
3
|
+
import concurrent.futures
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import multiprocessing
|
|
7
|
+
from itertools import islice
|
|
8
|
+
from typing import Any, Dict, Iterable, List, Optional, Tuple
|
|
9
|
+
|
|
10
|
+
from .utilities import _build_cached_validator
|
|
11
|
+
from .validate import StacValidate
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _warm_schema_cache(stac_version: str = "1.0.0") -> None:
|
|
17
|
+
"""
|
|
18
|
+
Pre-warm the schema cache before forking worker processes.
|
|
19
|
+
|
|
20
|
+
This function loads and compiles core STAC schemas in the main process.
|
|
21
|
+
When ProcessPoolExecutor forks child processes, they inherit the parent's
|
|
22
|
+
memory via Copy-on-Write (CoW), so all workers get a fully-warmed cache
|
|
23
|
+
without making any network requests.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
stac_version: STAC version to use for schema selection (e.g., "1.0.0" or "1.1.0")
|
|
27
|
+
|
|
28
|
+
Core schemas warmed:
|
|
29
|
+
- STAC Item
|
|
30
|
+
- STAC Collection
|
|
31
|
+
- STAC Catalog
|
|
32
|
+
"""
|
|
33
|
+
try:
|
|
34
|
+
import os
|
|
35
|
+
|
|
36
|
+
# Use local schema files bundled with the package
|
|
37
|
+
# Support both v1.0.0 and v1.1.0
|
|
38
|
+
schema_dir = os.path.join(
|
|
39
|
+
os.path.dirname(__file__), "local_schemas", stac_version
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
core_schemas = [
|
|
43
|
+
os.path.join(schema_dir, "item.json"),
|
|
44
|
+
os.path.join(schema_dir, "collection.json"),
|
|
45
|
+
os.path.join(schema_dir, "catalog.json"),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
from .utilities import fetch_and_parse_schema
|
|
49
|
+
|
|
50
|
+
# Load and cache each schema
|
|
51
|
+
for schema_path in core_schemas:
|
|
52
|
+
try:
|
|
53
|
+
schema = fetch_and_parse_schema(schema_path)
|
|
54
|
+
# Convert to JSON string and cache via _build_cached_validator
|
|
55
|
+
schema_json = json.dumps(schema, sort_keys=True, separators=(",", ":"))
|
|
56
|
+
_build_cached_validator(schema_json)
|
|
57
|
+
except Exception:
|
|
58
|
+
# If a schema fails to load, continue with others
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
except Exception:
|
|
62
|
+
# If cache warming fails entirely, continue without it
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_optimal_worker_count(max_workers: Optional[int] = None) -> int:
|
|
67
|
+
"""
|
|
68
|
+
Get the optimal number of worker processes.
|
|
69
|
+
|
|
70
|
+
Detects available CPU cores, accounting for containerized environments.
|
|
71
|
+
Falls back to os.sched_getaffinity() on Linux for Docker/container support.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
max_workers: Maximum number of workers to use. Options:
|
|
75
|
+
- None or 0: Use all available cores (default)
|
|
76
|
+
- Positive int: Use that many cores (capped at available)
|
|
77
|
+
- Negative int: Use all cores minus that many (e.g., -1 reserves 1 core for OS)
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Optimal number of worker processes
|
|
81
|
+
|
|
82
|
+
Note:
|
|
83
|
+
In containerized environments (Docker, ECS), this uses os.sched_getaffinity()
|
|
84
|
+
on Linux to detect the actual cores allocated to the container, rather than
|
|
85
|
+
the host machine's total cores.
|
|
86
|
+
"""
|
|
87
|
+
import os as os_module
|
|
88
|
+
|
|
89
|
+
# Try to get container-aware core count on Linux
|
|
90
|
+
try:
|
|
91
|
+
if hasattr(os_module, "sched_getaffinity"):
|
|
92
|
+
# Linux: respects Docker/container CPU limits
|
|
93
|
+
total_cores = len(os_module.sched_getaffinity(0))
|
|
94
|
+
else:
|
|
95
|
+
# Fallback for non-Linux systems
|
|
96
|
+
total_cores = multiprocessing.cpu_count()
|
|
97
|
+
except Exception:
|
|
98
|
+
# Fallback if anything goes wrong
|
|
99
|
+
total_cores = multiprocessing.cpu_count()
|
|
100
|
+
|
|
101
|
+
if max_workers is None or max_workers == 0:
|
|
102
|
+
# Use all available cores
|
|
103
|
+
return total_cores
|
|
104
|
+
elif max_workers < 0:
|
|
105
|
+
# Use all cores minus the specified amount (useful for reserving cores for OS)
|
|
106
|
+
return max(1, total_cores + max_workers)
|
|
107
|
+
else:
|
|
108
|
+
# Use the specified number, but cap at available cores
|
|
109
|
+
return min(max_workers, total_cores)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _validate_single_file(file_path: str) -> Tuple[str, bool, List[str]]:
|
|
113
|
+
"""
|
|
114
|
+
Worker function that runs on an individual CPU core.
|
|
115
|
+
Validates a single STAC file and returns results.
|
|
116
|
+
|
|
117
|
+
Each worker process has its own isolated schema cache for optimal performance.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
file_path: Path to the STAC JSON file to validate.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Tuple of (file_path, is_valid, list_of_errors)
|
|
124
|
+
"""
|
|
125
|
+
errors = []
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
# Use StacValidate for comprehensive validation (includes version check, core, and extensions)
|
|
129
|
+
# Each worker process has its own isolated schema cache (no need to reconfigure)
|
|
130
|
+
validator = StacValidate(file_path)
|
|
131
|
+
validator.run()
|
|
132
|
+
|
|
133
|
+
# Collect errors from validation
|
|
134
|
+
# validator.message is a list of result objects
|
|
135
|
+
if validator.message:
|
|
136
|
+
try:
|
|
137
|
+
messages = validator.message
|
|
138
|
+
if isinstance(messages, list) and len(messages) > 0:
|
|
139
|
+
msg_obj = messages[0]
|
|
140
|
+
if not msg_obj.get("valid_stac", False):
|
|
141
|
+
# Try multiple error field names
|
|
142
|
+
if "errors" in msg_obj:
|
|
143
|
+
errors.extend(msg_obj["errors"])
|
|
144
|
+
elif "error_message" in msg_obj:
|
|
145
|
+
errors.append(msg_obj["error_message"])
|
|
146
|
+
else:
|
|
147
|
+
errors.append(
|
|
148
|
+
f"{msg_obj.get('error_type', 'ValidationError')}"
|
|
149
|
+
)
|
|
150
|
+
except (KeyError, IndexError, TypeError):
|
|
151
|
+
# If we can't parse, just use the raw message
|
|
152
|
+
if validator.message:
|
|
153
|
+
errors.append(str(validator.message))
|
|
154
|
+
|
|
155
|
+
except Exception as e:
|
|
156
|
+
# Catch any exception including StacValidate instantiation failures
|
|
157
|
+
# This prevents UnboundLocalError if validator creation fails
|
|
158
|
+
return file_path, False, [f"Critical error processing file: {str(e)}"]
|
|
159
|
+
|
|
160
|
+
is_valid = len(errors) == 0
|
|
161
|
+
return file_path, is_valid, errors
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _validate_dict(
|
|
165
|
+
item_dict: Dict[str, Any], source_path: str
|
|
166
|
+
) -> Tuple[str, bool, Dict[str, Any], str]:
|
|
167
|
+
"""
|
|
168
|
+
Worker function that validates a STAC item dictionary directly (no temp files).
|
|
169
|
+
|
|
170
|
+
This function receives pickled dictionaries from the main process, avoiding
|
|
171
|
+
expensive disk I/O. Each worker process has its own isolated schema cache.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
item_dict: Dictionary representation of a STAC item to validate.
|
|
175
|
+
source_path: Display path for result reporting (e.g., "file.json[0]").
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
Tuple of (source_path, is_valid, message_dict, item_id)
|
|
179
|
+
where message_dict is the complete validation result from StacValidate
|
|
180
|
+
"""
|
|
181
|
+
item_id = item_dict.get("id", "unknown")
|
|
182
|
+
message = {}
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
# Validate the dictionary directly without writing to disk
|
|
186
|
+
# StacValidate can accept a dict via validate_dict() method
|
|
187
|
+
validator = StacValidate()
|
|
188
|
+
validator.stac_content = item_dict
|
|
189
|
+
|
|
190
|
+
# Set STAC version from item
|
|
191
|
+
validator.version = item_dict.get("stac_version", "1.0.0")
|
|
192
|
+
|
|
193
|
+
# Run validation on the dictionary
|
|
194
|
+
validator.validate_dict(item_dict)
|
|
195
|
+
|
|
196
|
+
# Get the validation result message
|
|
197
|
+
if validator.message:
|
|
198
|
+
messages = validator.message
|
|
199
|
+
if isinstance(messages, list) and len(messages) > 0:
|
|
200
|
+
message = messages[0]
|
|
201
|
+
else:
|
|
202
|
+
message = messages if isinstance(messages, dict) else {}
|
|
203
|
+
|
|
204
|
+
# Set path to the display path (not a real file)
|
|
205
|
+
message["path"] = source_path
|
|
206
|
+
|
|
207
|
+
except Exception as e:
|
|
208
|
+
# Catch any exception during validation
|
|
209
|
+
message = {
|
|
210
|
+
"path": source_path,
|
|
211
|
+
"valid_stac": False,
|
|
212
|
+
"error_type": "Exception",
|
|
213
|
+
"error_message": f"Critical error processing item: {str(e)}",
|
|
214
|
+
"failed_schema": "",
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
is_valid = message.get("valid_stac", False)
|
|
218
|
+
return source_path, is_valid, message, item_id
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def validate_concurrently(
|
|
222
|
+
file_paths: List[str],
|
|
223
|
+
max_workers: Optional[int] = None,
|
|
224
|
+
show_progress: bool = True,
|
|
225
|
+
feature_collection: bool = False,
|
|
226
|
+
batch_size: int = 2000,
|
|
227
|
+
) -> List[Dict[str, Any]]:
|
|
228
|
+
"""
|
|
229
|
+
Validates a list of STAC files concurrently using available CPU cores.
|
|
230
|
+
|
|
231
|
+
Uses ProcessPoolExecutor to bypass Python's GIL by creating separate Python
|
|
232
|
+
processes for each worker. Pre-warms the schema cache in the main process
|
|
233
|
+
before forking, so all workers inherit a fully-populated cache via Copy-on-Write.
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
file_paths: List of paths to STAC JSON files or FeatureCollections.
|
|
237
|
+
max_workers: Number of CPU cores to use. Options:
|
|
238
|
+
- None or 0: Use all available cores (default)
|
|
239
|
+
- Positive int: Use that many cores (capped at available)
|
|
240
|
+
- Negative int: Use all cores minus that many (e.g., -1 = all cores - 1)
|
|
241
|
+
show_progress: Whether to display a progress bar (requires tqdm).
|
|
242
|
+
feature_collection: If True, treat files as FeatureCollections and validate each feature.
|
|
243
|
+
batch_size: Number of items to process at a time to bound memory usage (default: 2000).
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
List of result dictionaries with keys: path, valid_stac, errors
|
|
247
|
+
"""
|
|
248
|
+
# If feature_collection mode, extract features and delegate to validate_dicts
|
|
249
|
+
# for memory-safe chunked processing
|
|
250
|
+
if feature_collection:
|
|
251
|
+
error_results: List[Dict[str, Any]] = []
|
|
252
|
+
|
|
253
|
+
def item_iter() -> Iterable[Dict[str, Any]]:
|
|
254
|
+
"""
|
|
255
|
+
Lazily iterate over all items (features or standalone objects)
|
|
256
|
+
from the provided file_paths, annotating each with source
|
|
257
|
+
metadata. Any file-level read/parse errors are recorded in
|
|
258
|
+
error_results and skipped.
|
|
259
|
+
"""
|
|
260
|
+
for file_path in file_paths:
|
|
261
|
+
try:
|
|
262
|
+
with open(file_path, "r") as f:
|
|
263
|
+
data = json.load(f)
|
|
264
|
+
|
|
265
|
+
# Check if it's a FeatureCollection
|
|
266
|
+
if (
|
|
267
|
+
isinstance(data, dict)
|
|
268
|
+
and data.get("type") == "FeatureCollection"
|
|
269
|
+
):
|
|
270
|
+
features = data.get("features", [])
|
|
271
|
+
for idx, feature in enumerate(features):
|
|
272
|
+
# Attach source info for result display
|
|
273
|
+
feature["__source_file__"] = file_path
|
|
274
|
+
feature["__feature_index__"] = idx
|
|
275
|
+
yield feature
|
|
276
|
+
else:
|
|
277
|
+
# Regular file, treat as single item
|
|
278
|
+
data["__source_file__"] = file_path
|
|
279
|
+
data["__feature_index__"] = None
|
|
280
|
+
yield data
|
|
281
|
+
|
|
282
|
+
except Exception as e:
|
|
283
|
+
# Accumulate error for this file and continue processing others
|
|
284
|
+
error_results.append(
|
|
285
|
+
{
|
|
286
|
+
"path": file_path,
|
|
287
|
+
"valid_stac": False,
|
|
288
|
+
"errors": [f"Failed to read file: {str(e)}"],
|
|
289
|
+
}
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# Delegate to validate_dicts for chunked, memory-safe processing
|
|
293
|
+
# validate_dicts will detect STAC version from first item and warm cache
|
|
294
|
+
validation_results = validate_dicts(
|
|
295
|
+
item_iter(),
|
|
296
|
+
max_workers=max_workers,
|
|
297
|
+
show_progress=show_progress,
|
|
298
|
+
chunk_size=batch_size,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
# Combine error results with validation results
|
|
302
|
+
return error_results + validation_results
|
|
303
|
+
|
|
304
|
+
# Standard file path validation (no FeatureCollection expansion)
|
|
305
|
+
results: List[Dict[str, Any]] = []
|
|
306
|
+
|
|
307
|
+
try:
|
|
308
|
+
from tqdm import tqdm
|
|
309
|
+
|
|
310
|
+
has_tqdm = True
|
|
311
|
+
except ImportError:
|
|
312
|
+
has_tqdm = False
|
|
313
|
+
show_progress = False
|
|
314
|
+
|
|
315
|
+
optimal_workers = get_optimal_worker_count(max_workers)
|
|
316
|
+
|
|
317
|
+
if not file_paths:
|
|
318
|
+
return results
|
|
319
|
+
|
|
320
|
+
try:
|
|
321
|
+
with concurrent.futures.ProcessPoolExecutor(
|
|
322
|
+
max_workers=optimal_workers
|
|
323
|
+
) as executor:
|
|
324
|
+
|
|
325
|
+
# Submit all tasks to the pool
|
|
326
|
+
future_to_file = {
|
|
327
|
+
executor.submit(_validate_single_file, path): path
|
|
328
|
+
for path in file_paths
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
# Wrap the as_completed iterator with tqdm for a nice progress bar
|
|
332
|
+
iterator = concurrent.futures.as_completed(future_to_file)
|
|
333
|
+
if show_progress and has_tqdm:
|
|
334
|
+
iterator = tqdm( # type: ignore
|
|
335
|
+
iterator,
|
|
336
|
+
total=len(file_paths),
|
|
337
|
+
desc="Validating STAC Items",
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
# Collect results as they finish
|
|
341
|
+
for future in iterator:
|
|
342
|
+
file_path, is_valid, errors = future.result()
|
|
343
|
+
|
|
344
|
+
result = {
|
|
345
|
+
"path": file_path,
|
|
346
|
+
"valid_stac": is_valid,
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if errors:
|
|
350
|
+
result["errors"] = errors
|
|
351
|
+
|
|
352
|
+
results.append(result)
|
|
353
|
+
|
|
354
|
+
finally:
|
|
355
|
+
pass
|
|
356
|
+
|
|
357
|
+
return results
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _chunked_iterable(iterable: Iterable, size: int):
|
|
361
|
+
"""Yield successive n-sized chunks from an iterable."""
|
|
362
|
+
it = iter(iterable)
|
|
363
|
+
while True:
|
|
364
|
+
chunk = list(islice(it, size))
|
|
365
|
+
if not chunk:
|
|
366
|
+
break
|
|
367
|
+
yield chunk
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def validate_dicts(
|
|
371
|
+
items: Iterable[Dict[str, Any]],
|
|
372
|
+
max_workers: Optional[int] = None,
|
|
373
|
+
show_progress: bool = False,
|
|
374
|
+
chunk_size: int = 2000,
|
|
375
|
+
) -> List[Dict[str, Any]]:
|
|
376
|
+
"""
|
|
377
|
+
Validate an iterable of STAC item dictionaries concurrently.
|
|
378
|
+
|
|
379
|
+
Passes dictionaries directly to worker processes via pickle (memory-based),
|
|
380
|
+
avoiding expensive disk I/O. Pre-warms the schema cache in the main process
|
|
381
|
+
before forking, so all workers inherit a fully-populated cache via Copy-on-Write.
|
|
382
|
+
Processes items in bounded chunks to prevent out-of-memory errors.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
items: Iterable of STAC item dictionaries to validate
|
|
386
|
+
max_workers: Maximum number of worker processes. Options:
|
|
387
|
+
- None or 0: Use all available cores (default)
|
|
388
|
+
- Positive int: Use that many cores (capped at available)
|
|
389
|
+
- Negative int: Use all cores minus that many
|
|
390
|
+
show_progress: Whether to show progress bar (default: False). Set to True for CLI usage.
|
|
391
|
+
chunk_size: Number of items to process at a time to bound memory usage.
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
List of validation results with path, valid_stac, and errors (if any)
|
|
395
|
+
"""
|
|
396
|
+
all_results = []
|
|
397
|
+
|
|
398
|
+
try:
|
|
399
|
+
from tqdm import tqdm
|
|
400
|
+
|
|
401
|
+
has_tqdm = True
|
|
402
|
+
except ImportError:
|
|
403
|
+
has_tqdm = False
|
|
404
|
+
show_progress = False
|
|
405
|
+
|
|
406
|
+
optimal_workers = get_optimal_worker_count(max_workers)
|
|
407
|
+
|
|
408
|
+
# Detect STAC version from first item and warm cache accordingly
|
|
409
|
+
stac_version = "1.0.0" # default
|
|
410
|
+
items_list = list(items)
|
|
411
|
+
if items_list:
|
|
412
|
+
first_item = items_list[0]
|
|
413
|
+
stac_version = first_item.get("stac_version", "1.0.0")
|
|
414
|
+
|
|
415
|
+
# Pre-warm the schema cache in the main process before forking workers
|
|
416
|
+
# This leverages Copy-on-Write so all workers inherit the warmed cache
|
|
417
|
+
_warm_schema_cache(stac_version=stac_version)
|
|
418
|
+
|
|
419
|
+
# Create a single executor for the entire run to reuse workers across chunks
|
|
420
|
+
# This preserves the schema cache across chunks and eliminates fork/spawn overhead
|
|
421
|
+
try:
|
|
422
|
+
with concurrent.futures.ProcessPoolExecutor(
|
|
423
|
+
max_workers=optimal_workers
|
|
424
|
+
) as executor:
|
|
425
|
+
total_items = len(items_list)
|
|
426
|
+
progress_bar = None
|
|
427
|
+
|
|
428
|
+
# Initialize progress bar if available
|
|
429
|
+
if show_progress and has_tqdm:
|
|
430
|
+
progress_bar = tqdm( # type: ignore
|
|
431
|
+
total=total_items,
|
|
432
|
+
desc="Validating Items",
|
|
433
|
+
unit="item",
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
# Process items in chunks: submit a chunk, collect results, then submit next
|
|
437
|
+
# This bounds in-flight futures and memory usage while reusing workers
|
|
438
|
+
for chunk in _chunked_iterable(items_list, chunk_size):
|
|
439
|
+
if not chunk:
|
|
440
|
+
continue
|
|
441
|
+
|
|
442
|
+
# Submit all items in this chunk
|
|
443
|
+
future_to_item = {}
|
|
444
|
+
for idx, item in enumerate(chunk):
|
|
445
|
+
# Extract source metadata if present
|
|
446
|
+
source_file = item.get("__source_file__")
|
|
447
|
+
feature_index = item.get("__feature_index__")
|
|
448
|
+
|
|
449
|
+
# Create display path for results
|
|
450
|
+
if source_file is not None:
|
|
451
|
+
display_path = (
|
|
452
|
+
f"{source_file}[{feature_index}]"
|
|
453
|
+
if feature_index is not None
|
|
454
|
+
else source_file
|
|
455
|
+
)
|
|
456
|
+
else:
|
|
457
|
+
display_path = f"item-{idx}"
|
|
458
|
+
|
|
459
|
+
# Create payload without internal metadata keys
|
|
460
|
+
payload = {
|
|
461
|
+
k: v
|
|
462
|
+
for k, v in item.items()
|
|
463
|
+
if k not in ("__source_file__", "__feature_index__")
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
# Submit to worker - dictionary is pickled automatically
|
|
467
|
+
future = executor.submit(_validate_dict, payload, display_path)
|
|
468
|
+
future_to_item[future] = (display_path, idx)
|
|
469
|
+
|
|
470
|
+
# Collect results for this chunk as they finish
|
|
471
|
+
for future in concurrent.futures.as_completed(future_to_item):
|
|
472
|
+
display_path, idx = future_to_item[future]
|
|
473
|
+
source_path, is_valid, message, item_id = future.result()
|
|
474
|
+
|
|
475
|
+
# Use the complete message from validation
|
|
476
|
+
result = message.copy() if message else {}
|
|
477
|
+
|
|
478
|
+
# Ensure required fields are present
|
|
479
|
+
if "path" not in result:
|
|
480
|
+
result["path"] = source_path
|
|
481
|
+
if "valid_stac" not in result:
|
|
482
|
+
result["valid_stac"] = is_valid
|
|
483
|
+
|
|
484
|
+
# Add item_id for reference
|
|
485
|
+
result["item_id"] = item_id
|
|
486
|
+
|
|
487
|
+
all_results.append(result)
|
|
488
|
+
|
|
489
|
+
# Update progress bar
|
|
490
|
+
if progress_bar:
|
|
491
|
+
progress_bar.update(1)
|
|
492
|
+
|
|
493
|
+
# Close progress bar
|
|
494
|
+
if progress_bar:
|
|
495
|
+
progress_bar.close()
|
|
496
|
+
|
|
497
|
+
except Exception as e:
|
|
498
|
+
# If processing fails, record error for all items
|
|
499
|
+
for idx, item in enumerate(items_list):
|
|
500
|
+
source_file = item.get("__source_file__")
|
|
501
|
+
feature_index = item.get("__feature_index__")
|
|
502
|
+
|
|
503
|
+
if source_file is not None:
|
|
504
|
+
display_path = (
|
|
505
|
+
f"{source_file}[{feature_index}]"
|
|
506
|
+
if feature_index is not None
|
|
507
|
+
else source_file
|
|
508
|
+
)
|
|
509
|
+
else:
|
|
510
|
+
display_path = f"item-{idx}"
|
|
511
|
+
|
|
512
|
+
all_results.append(
|
|
513
|
+
{
|
|
514
|
+
"path": display_path,
|
|
515
|
+
"valid_stac": False,
|
|
516
|
+
"errors": [f"Chunk processing error: {str(e)}"],
|
|
517
|
+
}
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
return all_results
|