66import os
77import sys
88import threading
9- from collections import namedtuple
9+ from collections import defaultdict , namedtuple
1010from pathlib import Path
1111from queue import Queue , Empty
1212from concurrent .futures import ThreadPoolExecutor , as_completed
@@ -200,6 +200,9 @@ def process_single_tiff_gdal(args):
200200 (idx , tif , _bbox , cellsize , destination_srs , grid_type , grid_type_name ,
201201 srs_definition , _extent_name , tz_name , tz_offset , is_interval ) = args
202202
203+ # Extract product_id early for error reporting
204+ product_id = tif .get ('product_id' , 'UNKNOWN' )
205+
203206 try :
204207 TifCfg = namedtuple ("TifCfg" , tif )(** tif )
205208 s3_path = f"/vsis3_streaming/{ TifCfg .bucket } /{ TifCfg .key } "
@@ -210,6 +213,7 @@ def process_single_tiff_gdal(args):
210213 return {
211214 'success' : False ,
212215 'index' : idx ,
216+ 'product_id' : product_id ,
213217 'error' : f"Failed to open { TifCfg .key } "
214218 }
215219
@@ -232,6 +236,7 @@ def process_single_tiff_gdal(args):
232236 return {
233237 'success' : False ,
234238 'index' : idx ,
239+ 'product_id' : product_id ,
235240 'error' : f"Failed to warp { TifCfg .key } "
236241 }
237242
@@ -309,6 +314,7 @@ def process_single_tiff_gdal(args):
309314 return {
310315 'success' : True ,
311316 'index' : idx ,
317+ 'product_id' : product_id ,
312318 'tif_key' : TifCfg .key ,
313319 'gd' : gd ,
314320 'compressed_data' : compressed_data ,
@@ -322,6 +328,7 @@ def process_single_tiff_gdal(args):
322328 return {
323329 'success' : False ,
324330 'index' : idx ,
331+ 'product_id' : product_id ,
325332 'error' : str (e )
326333 }
327334
@@ -373,12 +380,19 @@ def process_tiffs_with_bounded_queue(src, _bbox, cellsize, destination_srs, dss,
373380
374381 Returns:
375382 --------
376- int : Number of successfully processed files
383+ tuple : (processed_count, product_stats) where processed_count is total files
384+ attempted and product_stats is {product_id: {"expected": N, "successful": M}}
377385 """
378386 # Auto-detect optimal number of workers if not provided
379387 if max_workers is None :
380388 max_workers = get_optimal_workers ()
381389
390+ # Build expected counts per product
391+ expected_counts = defaultdict (int )
392+ for tif in src :
393+ expected_counts [tif .get ('product_id' , 'UNKNOWN' )] += 1
394+ success_counts = defaultdict (int )
395+
382396 try :
383397 # Estimate bbox dimensions for queue sizing
384398 first_tif = src [0 ]
@@ -455,6 +469,7 @@ def producer():
455469 compressed_data = result ['compressed_data' ]
456470 compressed_size = result ['compressed_size' ]
457471 tif_key = result ['tif_key' ]
472+ _product_id = result .get ('product_id' , 'UNKNOWN' )
458473
459474 # Write precompressed data to DSS (GriddedData already created in worker)
460475 t = Timer (name = "accumuluated" , logger = None )
@@ -464,8 +479,10 @@ def producer():
464479
465480 if dss_result != 0 :
466481 logger .warning (f'HEC-DSS-PY write record failed for "{ tif_key } ": { dss_result } ' )
467- elif logger .isEnabledFor (logging .DEBUG ):
468- logger .debug (f'DSS writePrecompressedGrid processed "{ tif_key } " in { elapsed_time :.4f} s' )
482+ else :
483+ if logger .isEnabledFor (logging .DEBUG ):
484+ logger .debug (f'DSS writePrecompressedGrid processed "{ tif_key } " in { elapsed_time :.4f} s' )
485+ success_counts [_product_id ] += 1
469486
470487 processed_count += 1
471488
@@ -489,24 +506,36 @@ def producer():
489506 logger .error (f"Error writing to DSS for file { result ['index' ]} : { e } " )
490507 import traceback
491508 logger .error (traceback .format_exc ())
509+ processed_count += 1
492510 continue
493511 else :
494512 logger .error (f"Error processing file { result ['index' ]} : { result .get ('error' , 'Unknown error' )} " )
513+ processed_count += 1
495514
496515 except Empty :
497516 logger .error ("Timeout waiting for results from queue" )
498517 break
499518
500519 producer_thread .join ()
501520
502- logger .info (f"Parallel GDAL with compression: Successfully processed { processed_count } /{ len (src )} files" )
503- return processed_count
521+ total_successful = sum (success_counts .values ())
522+ logger .info (f"Parallel GDAL with compression: Successfully wrote { total_successful } /{ len (src )} files" )
523+
524+ product_stats = {
525+ pid : {"expected" : expected_counts [pid ], "successful" : success_counts .get (pid , 0 )}
526+ for pid in expected_counts
527+ }
528+ return processed_count , product_stats
504529
505530 except Exception as e :
506531 logger .error (f"Parallel GDAL processing failed: { e } " )
507532 import traceback
508533 logger .error (traceback .format_exc ())
509- return 0
534+ product_stats = {
535+ pid : {"expected" : expected_counts [pid ], "successful" : 0 }
536+ for pid in expected_counts
537+ }
538+ return 0 , product_stats
510539
511540
512541@pyplugs .register
@@ -537,8 +566,9 @@ def writer(
537566
538567 Returns
539568 -------
540- str
541- FQPN to dss file
569+ dict or None
570+ {"file": FQPN to dss file, "product_stats": {product_id: {"expected": N, "successful": M}}}
571+ or None if no files were processed
542572 """
543573
544574 try :
@@ -579,11 +609,17 @@ def writer(
579609
580610 dssfilename = Path (dst ).joinpath (id ).with_suffix (".dss" ).as_posix ()
581611
612+ # Build expected counts per product for single-file path
613+ expected_counts = defaultdict (int )
614+ for tif in src :
615+ expected_counts [tif .get ('product_id' , 'UNKNOWN' )] += 1
616+ success_counts = defaultdict (int )
617+
582618 with HecDss (dssfilename ) as dss :
583619 # Use parallel GDAL processing with compression and precompressed writes for multiple files
584620 if len (src ) > 1 :
585621 logger .info ("Using parallel GDAL processing with compression and precompressed writes" )
586- processed_count = process_tiffs_with_bounded_queue (
622+ processed_count , product_stats = process_tiffs_with_bounded_queue (
587623 src , _bbox , cellsize , destination_srs , dss ,
588624 grid_type , grid_type_name , srs_definition ,
589625 _extent_name , tz_name , tz_offset , is_interval ,
@@ -662,14 +698,17 @@ def writer(
662698 t .start ()
663699 result = dss .put (gd )
664700 elapsed_time = t .stop ()
665- if logger .isEnabledFor (logging .DEBUG ):
666- logger .debug (
667- f'DSS put Processed "{ TifCfg .key } " in { elapsed_time :.4f} seconds'
668- )
669701 if result != 0 :
670702 logger .info (
671703 f'HEC-DSS-PY write record failed for "{ TifCfg .key } ": { result } '
672704 )
705+ else :
706+ if logger .isEnabledFor (logging .DEBUG ):
707+ logger .debug (
708+ f'DSS put Processed "{ TifCfg .key } " in { elapsed_time :.4f} seconds'
709+ )
710+ _product_id = tif .get ('product_id' , 'UNKNOWN' )
711+ success_counts [_product_id ] += 1
673712
674713 _progress = int (((idx + 1 ) / gridcount ) * 100 )
675714 # Update progress at predefined interval
@@ -698,6 +737,13 @@ def writer(
698737 warp_ds = None
699738 ds = None
700739
740+ # Build product_stats for single-file path (multi-file path already has it)
741+ if len (src ) == 1 :
742+ product_stats = {
743+ pid : {"expected" : expected_counts [pid ], "successful" : success_counts .get (pid , 0 )}
744+ for pid in expected_counts
745+ }
746+
701747 # If no progress was made for any items in the payload (ex: all tifs could not be projected properly),
702748 # don't return a dssfilename
703749 if _progress == 0 :
@@ -710,4 +756,4 @@ def writer(
710756 f'Total processing time for download ID "{ id } " in { total_time :.4f} seconds'
711757 )
712758
713- return dssfilename
759+ return { "file" : dssfilename , "product_stats" : product_stats }
0 commit comments