33import fnmatch
44import shutil
55import sys
6- from concurrent .futures import Future , ThreadPoolExecutor , as_completed
76from datetime import datetime
87from pathlib import Path
98from typing import Any
109
10+ import logging
11+
1112import cloup
1213from cloup .constraints import If , IsSet , accept_none , require_one
1314from simple_logger .logger import get_logger
1819from class_generator .core .generator import class_generator
1920from class_generator .core .schema import update_kind_schema , ClusterVersionError
2021from class_generator .tests .test_generation import generate_class_generator_tests
22+ from class_generator .utils import execute_parallel_tasks
2123from ocp_resources .utils .utils import convert_camel_case_to_snake_case
2224
2325LOGGER = get_logger (name = __name__ )
@@ -56,7 +58,7 @@ def handle_schema_update(update_schema: bool, generate_missing: bool) -> bool:
5658 try :
5759 update_kind_schema ()
5860 except (RuntimeError , IOError , ClusterVersionError ) as e :
59- LOGGER .error (f"Failed to update schema: { e } " )
61+ LOGGER .exception (f"Failed to update schema: { e } " )
6062 sys .exit (1 )
6163
6264 # If only updating schema (not generating), exit
@@ -131,7 +133,7 @@ def handle_missing_resources_generation(
131133 if not dry_run :
132134 LOGGER .info (f"Generated { kind_to_generate } " )
133135 except Exception as e :
134- LOGGER .error (f"Failed to generate { kind_to_generate } : { e } " )
136+ LOGGER .exception (f"Failed to generate { kind_to_generate } : { e } " )
135137
136138
137139def create_backup_if_needed (target_file : Path , backup_dir : Path | None ) -> None :
@@ -163,7 +165,7 @@ def handle_regenerate_all(
163165 regenerate_all : bool ,
164166 backup : bool ,
165167 dry_run : bool ,
166- filter : str | None ,
168+ filter_pattern : str | None ,
167169) -> bool :
168170 """
169171 Handle regeneration of all generated resources.
@@ -172,7 +174,7 @@ def handle_regenerate_all(
172174 regenerate_all: Whether to regenerate all resources
173175 backup: Whether to create backups
174176 dry_run: Whether this is a dry run
175- filter : Optional filter pattern for resource names
177+ filter_pattern : Optional filter pattern for resource names
176178
177179 Returns:
178180 True if regeneration was performed and main should exit, False to continue
@@ -197,13 +199,13 @@ def handle_regenerate_all(
197199 LOGGER .info (f"Found { len (discovered )} generated resources" )
198200
199201 # Filter resources if pattern provided
200- if filter :
202+ if filter_pattern :
201203 filtered = []
202204 for resource in discovered :
203- if fnmatch .fnmatch (resource ["kind" ], filter ):
205+ if fnmatch .fnmatch (resource ["kind" ], filter_pattern ):
204206 filtered .append (resource )
205207 discovered = filtered
206- LOGGER .info (f"Filtered to { len (discovered )} resources matching '{ filter } '" )
208+ LOGGER .info (f"Filtered to { len (discovered )} resources matching '{ filter_pattern } '" )
207209
208210 # Regenerate each resource
209211 success_count = 0
@@ -245,23 +247,34 @@ def regenerate_single_resource(resource: dict[str, Any]) -> tuple[str, bool, str
245247 LOGGER .warning (f"Skipped { resource_kind } : Not found in schema mapping" )
246248 return resource_kind , False , "Not found in schema mapping"
247249 except Exception as e :
248- LOGGER .error (f"Failed to regenerate { resource_kind } : { e } " )
250+ LOGGER .exception (f"Failed to regenerate { resource_kind } : { e } " )
249251 return resource_kind , False , str (e )
250252
253+ # Process results from parallel execution
254+ def process_regeneration_result (resource : dict [str , Any ], result : tuple [str , bool , str | None ]) -> None :
255+ nonlocal success_count , error_count
256+ resource_kind , success , error = result
257+ if success :
258+ success_count += 1
259+ else :
260+ error_count += 1
261+
262+ # Handle executor-level exceptions that bypass result processing
263+ def handle_regeneration_error (resource : dict [str , Any ], exc : Exception ) -> None :
264+ nonlocal error_count
265+ resource_kind = resource .get ("kind" , "unknown" )
266+ LOGGER .exception (f"Executor-level failure for { resource_kind } : { exc } " )
267+ error_count += 1
268+
251269 # Process resources in parallel
252- with ThreadPoolExecutor (max_workers = 10 ) as executor :
253- # Submit all tasks
254- regeneration_futures = {
255- executor .submit (regenerate_single_resource , resource ): resource for resource in discovered
256- }
257-
258- # Process results as they complete
259- for future in as_completed (regeneration_futures ):
260- resource_kind , success , error = future .result ()
261- if success :
262- success_count += 1
263- else :
264- error_count += 1
270+ execute_parallel_tasks (
271+ tasks = discovered ,
272+ task_func = regenerate_single_resource ,
273+ max_workers = 10 ,
274+ task_name = "regeneration" ,
275+ result_processor = process_regeneration_result ,
276+ error_handler = handle_regeneration_error ,
277+ )
265278
266279 # Print summary
267280 if not dry_run :
@@ -330,15 +343,24 @@ def handle_normal_kind_generation(
330343 add_tests = add_tests ,
331344 )
332345 except Exception as e :
333- LOGGER .error (f"Failed to generate { kind } : { e } " )
346+ LOGGER .exception (f"Failed to generate { kind } : { e } " )
334347 sys .exit (1 )
335348
336349 if backup_dir and not dry_run :
337350 LOGGER .info (f"Backup files stored in: { backup_dir } " )
338351 else :
339- # Multiple kinds - run in parallel
340- def generate_with_backup (kind_to_generate : str ) -> list [str ]:
341- """Generate a single kind with optional backup."""
352+ # Multiple kinds - run in parallel with result tracking
353+ success_count = 0
354+ error_count = 0
355+ failed_kinds = []
356+
357+ def generate_with_backup (kind_to_generate : str ) -> tuple [str , bool , str | None ]:
358+ """
359+ Generate a single kind with optional backup.
360+
361+ Returns:
362+ Tuple of (kind, success, error_message)
363+ """
342364 if overwrite and backup_dir :
343365 # Determine the output file path for this kind
344366 formatted_kind = convert_camel_case_to_snake_case (name = kind_to_generate )
@@ -348,28 +370,69 @@ def generate_with_backup(kind_to_generate: str) -> list[str]:
348370 create_backup_if_needed (target_file = target_file , backup_dir = backup_dir )
349371
350372 try :
351- return class_generator (
373+ result = class_generator (
352374 kind = kind_to_generate ,
353375 overwrite = overwrite ,
354376 dry_run = dry_run ,
355377 output_file = output_file ,
356378 add_tests = add_tests ,
379+ called_from_cli = False , # Don't prompt for missing resources during batch generation
357380 )
381+ # Check if generation was successful (empty list means failure)
382+ if result :
383+ if not dry_run :
384+ LOGGER .info (f"Successfully generated { kind_to_generate } " )
385+ return kind_to_generate , True , None
386+ else :
387+ LOGGER .warning (f"Skipped { kind_to_generate } : Not found in schema mapping" )
388+ return kind_to_generate , False , "Not found in schema mapping"
358389 except Exception as e :
359- LOGGER .error (f"Failed to generate { kind_to_generate } : { e } " )
360- return []
390+ LOGGER .exception (f"Failed to generate { kind_to_generate } : { e } " )
391+ return kind_to_generate , False , str ( e )
361392
362- futures : list [Future ] = []
363- with ThreadPoolExecutor (max_workers = 10 ) as executor :
364- for _kind in kind_list :
365- futures .append (executor .submit (generate_with_backup , _kind ))
393+ # Process results from parallel execution
394+ def process_generation_result (kind_to_generate : str , result : tuple [str , bool , str | None ]) -> None :
395+ nonlocal success_count , error_count , failed_kinds
396+ kind_name , success , error = result
397+ if success :
398+ success_count += 1
399+ else :
400+ error_count += 1
401+ failed_kinds .append ({"kind" : kind_name , "error" : error })
402+
403+ # Handle executor-level exceptions that bypass result processing
404+ def handle_generation_error (kind_to_generate : str , exc : Exception ) -> None :
405+ nonlocal error_count , failed_kinds
406+ LOGGER .exception (f"Executor-level failure for { kind_to_generate } : { exc } " )
407+ error_count += 1
408+ failed_kinds .append ({"kind" : kind_to_generate , "error" : str (exc )})
409+
410+ # Generate all kinds in parallel
411+ execute_parallel_tasks (
412+ tasks = kind_list ,
413+ task_func = generate_with_backup ,
414+ max_workers = 10 ,
415+ task_name = "generation" ,
416+ result_processor = process_generation_result ,
417+ error_handler = handle_generation_error ,
418+ )
419+
420+ # Print summary and handle failures
421+ if not dry_run :
422+ LOGGER .info (f"\n Generation complete: { success_count } succeeded, { error_count } failed" )
423+ if backup_dir :
424+ LOGGER .info (f"Backup files stored in: { backup_dir } " )
425+ else :
426+ LOGGER .info (f"\n Dry run complete: would generate { len (kind_list )} kinds" )
366427
367- # Wait for all tasks to complete
368- for _ in as_completed (futures ):
369- pass
428+ # Log detailed failure information
429+ if failed_kinds :
430+ LOGGER .error (f"\n Failed to generate { len (failed_kinds )} kind(s):" )
431+ for failure in failed_kinds :
432+ LOGGER .error (f" - { failure ['kind' ]} : { failure ['error' ]} " )
370433
371- if backup_dir and not dry_run :
372- LOGGER . info ( f"Backup files stored in: { backup_dir } " )
434+ # Exit with non-zero status if any failures occurred
435+ sys . exit ( 1 )
373436
374437
375438def handle_test_generation (add_tests : bool ) -> None :
@@ -474,6 +537,13 @@ def handle_test_generation(add_tests: bool) -> None:
474537 type = cloup .STRING ,
475538 default = None ,
476539)
540+ @cloup .option (
541+ "-v" ,
542+ "--verbose" ,
543+ help = "Enable verbose output with debug logs" ,
544+ is_flag = True ,
545+ show_default = True ,
546+ )
477547@cloup .constraint (
478548 If (IsSet ("update_schema" ) & ~ IsSet ("generate_missing" ), then = accept_none ),
479549 [
@@ -509,8 +579,35 @@ def main(
509579 filter : str | None ,
510580 json_output : bool ,
511581 update_schema : bool ,
582+ verbose : bool ,
512583) -> None :
513584 """Generate Python module for K8S resource."""
585+ # Configure logging based on verbose flag
586+ if verbose :
587+ # Set debug level for all class_generator modules
588+ for logger_name in [
589+ "class_generator.core.schema" ,
590+ "class_generator.core.generator" ,
591+ "class_generator.core.coverage" ,
592+ "class_generator.core.discovery" ,
593+ "class_generator.cli" ,
594+ "class_generator.utils" ,
595+ "ocp_resources" ,
596+ ]:
597+ logger = logging .getLogger (logger_name )
598+ logger .setLevel (logging .DEBUG )
599+ # Prevent propagation to avoid duplicate messages
600+ logger .propagate = False
601+ # Also set all handlers to DEBUG to ensure debug logs surface
602+ for handler in logger .handlers :
603+ handler .setLevel (logging .DEBUG )
604+
605+ # Set root logger to DEBUG to cover all configured handlers
606+ root_logger = logging .getLogger ()
607+ root_logger .setLevel (logging .DEBUG )
608+ for handler in root_logger .handlers :
609+ handler .setLevel (logging .DEBUG )
610+
514611 # Validate input parameters
515612 validate_actions (
516613 kind = kind ,
@@ -542,7 +639,7 @@ def main(
542639 )
543640
544641 # Handle regenerate-all operation
545- if handle_regenerate_all (regenerate_all = regenerate_all , backup = backup , dry_run = dry_run , filter = filter ):
642+ if handle_regenerate_all (regenerate_all = regenerate_all , backup = backup , dry_run = dry_run , filter_pattern = filter ):
546643 return
547644
548645 # Exit if we only did discovery/report/generation
0 commit comments