prismalog 0.1.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.
prismalog/config.py ADDED
@@ -0,0 +1,783 @@
1
+ """
2
+ Configuration Management Module for prismalog
3
+
4
+ This module provides a centralized configuration system for the prismalog package,
5
+ enabling flexible and hierarchical configuration from multiple sources. It handles
6
+ loading, prioritizing, and accessing configuration settings throughout the application.
7
+
8
+ Key features:
9
+ - Hierarchical configuration with clear priority order
10
+ - Multi-source configuration (default values, files, environment variables, CLI)
11
+ - Support for YAML configuration files
12
+ - Automatic type conversion for numeric and boolean values
13
+ - Command-line argument integration with argparse
14
+ - Singleton pattern to ensure configuration consistency
15
+
16
+ The configuration system follows this priority order (highest to lowest):
17
+ 1. Programmatic settings via direct API calls
18
+ 2. Command-line arguments
19
+ 3. Configuration files (YAML)
20
+ 4. Environment variables (with support for CI/CD environments)
21
+ 5. Default values
22
+
23
+ Environment Variables:
24
+ Standard environment variables use the ``LOG_`` prefix:
25
+
26
+ - ``LOG_DIR``: Directory for log files
27
+ - ``LOG_LEVEL``: Default logging level
28
+ - ``LOG_ROTATION_SIZE``: Size in MB for log rotation
29
+ - ``LOG_BACKUP_COUNT``: Number of backup log files
30
+ - ``LOG_FORMAT``: Log message format
31
+ - ``LOG_COLORED_CONSOLE``: Whether to use colored console output
32
+ - ``LOG_DISABLE_ROTATION``: Whether to disable log rotation
33
+ - ``LOG_EXIT_ON_CRITICAL``: Whether to exit on critical logs
34
+ - ``LOG_TEST_MODE``: Whether logger is in test mode
35
+
36
+ For GitHub Actions, the same variables are supported with ``GITHUB_`` prefix:
37
+
38
+ - ``GITHUB_LOG_DIR``, ``GITHUB_LOG_LEVEL``, etc.
39
+
40
+ Type Conversion:
41
+ String values from configuration files and environment variables are automatically
42
+ converted to the appropriate types (boolean, integer) based on the default config's
43
+ type definition. Boolean values support multiple string representations:
44
+ - True: "true", "1", "yes", "y", "t", "on"
45
+ - False: "false", "0", "no", "n", "f", "off", "none"
46
+
47
+ Usage examples:
48
+ # Basic initialization with defaults
49
+ LoggingConfig.initialize()
50
+
51
+ # Initialization with configuration file
52
+ LoggingConfig.initialize(config_file="logging_config.yaml")
53
+
54
+ # Accessing configuration values
55
+ log_dir = LoggingConfig.get("log_dir")
56
+
57
+ # Setting configuration values programmatically
58
+ LoggingConfig.set("colored_console", False)
59
+
60
+ # Getting appropriate log level for a specific logger
61
+ level = LoggingConfig.get_level("requests.packages.urllib3")
62
+ """
63
+
64
+ import argparse
65
+ import os
66
+ from typing import Any, Dict, Optional, Type
67
+
68
+
69
+ class LoggingConfig:
70
+ """
71
+ Configuration manager for prismalog package.
72
+
73
+ Handles loading configuration from multiple sources with a priority order:
74
+ 1. Programmatic settings
75
+ 2. Command-line arguments
76
+ 3. Configuration files (YAML)
77
+ 4. Environment variables (including GitHub Actions secrets)
78
+ 5. Default values
79
+
80
+ This class uses a singleton pattern to ensure consistent configuration
81
+ throughout the application lifecycle. It supports configuration from
82
+ multiple sources and provides automatic type conversion.
83
+
84
+ Attributes:
85
+ DEFAULT_CONFIG (Dict[str, Any]): Default configuration values
86
+ _instance (LoggingConfig): Singleton instance of LoggingConfig
87
+ _config (Dict[str, Any]): Current active configuration
88
+ _initialized (bool): Whether the configuration has been initialized
89
+ _debug_mode (bool): Whether to print debug messages during configuration
90
+
91
+ Examples:
92
+ # Initialize with default settings
93
+ LoggingConfig.initialize()
94
+
95
+ # Initialize with a configuration file
96
+ LoggingConfig.initialize(config_file="logging.yaml")
97
+
98
+ # Get a configuration value
99
+ log_dir = LoggingConfig.get("log_dir")
100
+ """
101
+
102
+ DEFAULT_CONFIG = {
103
+ "log_dir": "logs",
104
+ "default_level": "INFO",
105
+ "rotation_size_mb": 10,
106
+ "backup_count": 5,
107
+ "log_format": "%(asctime)s - %(filename)s - %(name)s - [%(levelname)s] - %(message)s",
108
+ "colored_console": True,
109
+ "disable_rotation": False,
110
+ "exit_on_critical": False, # Whether to exit the program on critical logs
111
+ "test_mode": False, # Whether the logger is running in test mode
112
+ }
113
+
114
+ _instance = None
115
+ _config: Dict[str, Any] = {} # Add type annotation here
116
+ _initialized = False
117
+ _debug_mode = False
118
+
119
+ def __new__(cls) -> "LoggingConfig":
120
+ """Singleton pattern to ensure only one instance of LoggingConfig exists."""
121
+ if cls._instance is None:
122
+ cls._instance = super(LoggingConfig, cls).__new__(cls)
123
+ cls._config = cls.DEFAULT_CONFIG.copy()
124
+ return cls._instance
125
+
126
+ @classmethod
127
+ def debug_print(cls, message: str) -> None:
128
+ """
129
+ Print debug message only if debug mode is enabled.
130
+
131
+ Args:
132
+ message: The message to print when debugging is enabled
133
+ """
134
+ if cls._debug_mode:
135
+ print(message)
136
+
137
+ @classmethod
138
+ def initialize(cls, config_file: Optional[str] = None, use_cli_args: bool = True, **kwargs: Any) -> Dict[str, Any]:
139
+ """
140
+ Initialize configuration from various sources using a two-phase approach.
141
+
142
+ The configuration is loaded in two phases:
143
+ 1. Collection Phase: Gather and convert configurations from all sources
144
+ 2. Application Phase: Apply configurations in priority order
145
+
146
+ Args:
147
+ config_file: Path to configuration file (YAML)
148
+ use_cli_args: Whether to parse command-line arguments
149
+ **kwargs: Direct configuration values (highest priority)
150
+
151
+ Returns:
152
+ The complete configuration dictionary
153
+
154
+ Examples:
155
+ # Basic initialization
156
+ LoggingConfig.initialize()
157
+
158
+ # Initialize with a YAML config file
159
+ LoggingConfig.initialize(config_file="logging.yaml")
160
+
161
+ # Initialize with direct override values
162
+ LoggingConfig.initialize(log_level="DEBUG", colored_console=False)
163
+ """
164
+ # Phase 1: Collect configurations from all sources
165
+ config_sources = cls._collect_configurations(config_file, use_cli_args, kwargs)
166
+
167
+ # Phase 2: Apply configurations in priority order
168
+ cls._apply_configurations(config_sources)
169
+
170
+ return cls._config
171
+
172
+ @classmethod
173
+ def _collect_configurations(
174
+ cls, config_file: Optional[str], use_cli_args: bool, kwargs: Dict[str, Any]
175
+ ) -> Dict[str, Dict[str, Any]]:
176
+ """
177
+ Collect configurations from all possible sources and convert types immediately.
178
+
179
+ This method gathers configuration from multiple sources:
180
+ - Default values
181
+ - Environment variables
182
+ - Configuration files (if specified)
183
+ - Command-line arguments (if use_cli_args is True)
184
+ - Direct keyword arguments
185
+
186
+ Each source's values are converted to appropriate types during collection.
187
+
188
+ Args:
189
+ config_file: Optional path to a configuration file
190
+ use_cli_args: Whether to parse command-line arguments
191
+ kwargs: Direct configuration values passed to initialize()
192
+
193
+ Returns:
194
+ Dictionary containing configuration values from each source
195
+ """
196
+ # Start with empty collections for each source
197
+ sources: Dict[str, Dict[str, Any]] = {
198
+ "defaults": cls.DEFAULT_CONFIG.copy(),
199
+ "file": {},
200
+ "env": {},
201
+ "cli": {},
202
+ "kwargs": kwargs.copy() if kwargs else {},
203
+ }
204
+
205
+ # Collect and convert file configuration
206
+ if config_file and os.path.exists(config_file):
207
+ raw_file_config = cls._load_raw_file_config(config_file)
208
+ if raw_file_config:
209
+ # Convert types immediately
210
+ file_config = cls._convert_config_values(raw_file_config)
211
+ sources["file"] = file_config
212
+ cls.debug_print(f"Collected from file: {file_config}")
213
+
214
+ # Collect and convert environment variables
215
+ raw_env_config = cls._load_raw_env_config()
216
+ if raw_env_config:
217
+ # Convert types immediately
218
+ env_config = cls._convert_config_values(raw_env_config)
219
+ sources["env"] = env_config
220
+ cls.debug_print(f"Collected from environment: {env_config}")
221
+
222
+ # Collect and convert command line arguments
223
+ if use_cli_args:
224
+ raw_arg_config = cls._load_raw_cli_args()
225
+ if raw_arg_config:
226
+ # Convert types immediately
227
+ arg_config = cls._convert_config_values(raw_arg_config)
228
+ sources["cli"] = arg_config
229
+ cls.debug_print(f"Collected from CLI: {arg_config}")
230
+
231
+ return sources
232
+
233
+ @classmethod
234
+ def _apply_configurations(cls, sources: Dict[str, Dict[str, Any]]) -> None:
235
+ """
236
+ Apply configurations in the correct priority order.
237
+
238
+ Priority order (lowest to highest):
239
+ 1. Default values (starting point)
240
+ 2. Environment variables (override defaults)
241
+ 3. Configuration file (overrides environment)
242
+ 4. Command-line arguments (overrides file)
243
+ 5. Direct keyword arguments (highest priority)
244
+
245
+ Args:
246
+ sources: Dictionary containing configurations from different sources
247
+ """
248
+ # 1. Start with defaults
249
+ cls._config = sources["defaults"].copy()
250
+ cls.debug_print(f"1. Starting with defaults: {cls._config}")
251
+
252
+ # 2. Apply environment variables (overrides defaults)
253
+ if sources["env"]:
254
+ cls.debug_print("2. Applying environment variables")
255
+ cls._config.update(sources["env"])
256
+ cls.debug_print(f" Config after env vars: {cls._config}")
257
+
258
+ # 3. Apply configuration file (overrides env)
259
+ if sources["file"]:
260
+ cls.debug_print("3. Applying file configuration")
261
+ cls._config.update(sources["file"])
262
+ cls.debug_print(f" Config after file: {cls._config}")
263
+
264
+ # 4. Apply command-line arguments (overrides file)
265
+ if sources["cli"]:
266
+ cls.debug_print("4. Applying command line arguments")
267
+ cls._config.update(sources["cli"])
268
+ cls.debug_print(f" Config after CLI args: {cls._config}")
269
+
270
+ # 5. Apply direct kwargs (highest priority)
271
+ if sources["kwargs"]:
272
+ cls.debug_print("5. Applying kwargs")
273
+ cls._config.update(sources["kwargs"])
274
+ cls.debug_print(f" Config after kwargs: {cls._config}")
275
+
276
+ cls._initialized = True
277
+ cls.debug_print(f"Final configuration: {cls._config}")
278
+
279
+ @classmethod
280
+ def _convert_config_values(cls, config_dict: Dict[str, Any]) -> Dict[str, Any]:
281
+ """
282
+ Convert string configuration values to their appropriate types.
283
+
284
+ This is the central type conversion method used by all config sources.
285
+ It handles two types of conversions:
286
+ 1. Boolean values: Converts strings like "true", "yes", "1" to True and their opposites to False
287
+ 2. Numeric values: Converts digit strings to integers
288
+
289
+ The method processes boolean conversions first, then numeric conversions.
290
+ Invalid conversion attempts result in the key being removed from the result.
291
+
292
+ Args:
293
+ config_dict: Dictionary containing configuration values to convert
294
+
295
+ Returns:
296
+ Dictionary with values converted to appropriate types
297
+
298
+ Examples:
299
+ >>> LoggingConfig._convert_config_values({"colored_console": "true", "rotation_size_mb": "50"})
300
+ {'colored_console': True, 'rotation_size_mb': 50}
301
+ """
302
+ if not config_dict:
303
+ return {}
304
+
305
+ result = config_dict.copy()
306
+
307
+ # Get the keys and their types from DEFAULT_CONFIG
308
+ numeric_keys = [k for k, v in cls.DEFAULT_CONFIG.items() if isinstance(v, int)]
309
+ boolean_keys = [k for k, v in cls.DEFAULT_CONFIG.items() if isinstance(v, bool)]
310
+
311
+ # First, convert boolean values
312
+ for key in boolean_keys:
313
+ if key in result and isinstance(result[key], str):
314
+ val = result[key].strip().lower()
315
+ if val in ["true", "1", "yes", "y", "t", "on"]:
316
+ result[key] = True
317
+ cls.debug_print(f"Converted '{key}={val}' to boolean True")
318
+ elif val in ["false", "0", "no", "n", "f", "off", "none"]:
319
+ result[key] = False
320
+ cls.debug_print(f"Converted '{key}={val}' to boolean False")
321
+ else:
322
+ cls.debug_print(f"Warning: Cannot convert '{key}={val}' to boolean")
323
+ result.pop(key, None) # Remove invalid values
324
+
325
+ # Then, convert numeric values
326
+ for key in numeric_keys:
327
+ if key in result and isinstance(result[key], str):
328
+ val = result[key].strip()
329
+ if val.isdigit() or (val.startswith("-") and val[1:].isdigit()):
330
+ result[key] = int(val)
331
+ cls.debug_print(f"Converted '{key}={val}' to integer {int(val)}")
332
+ else:
333
+ cls.debug_print(f"Warning: Cannot convert '{key}={val}' to integer")
334
+ result.pop(key, None) # Remove invalid values
335
+
336
+ return result
337
+
338
+ @classmethod
339
+ def _load_raw_file_config(cls, config_path: str) -> Dict[str, Any]:
340
+ """
341
+ Load raw configuration from file without type conversion.
342
+
343
+ Supports YAML file format. If PyYAML is not installed,
344
+ YAML files cannot be loaded and an empty dictionary is returned.
345
+
346
+ Args:
347
+ config_path: Path to the configuration file
348
+
349
+ Returns:
350
+ Dictionary with raw configuration values from the file,
351
+ or empty dictionary if file doesn't exist or has invalid format
352
+ """
353
+ file_config: Dict[str, Any] = {} # Add type annotation here
354
+ if not os.path.exists(config_path):
355
+ return file_config
356
+
357
+ try:
358
+ with open(config_path, mode="r", encoding="utf-8") as f:
359
+ if config_path.endswith((".yaml", ".yml")):
360
+ try:
361
+ import yaml
362
+
363
+ file_config = yaml.safe_load(f)
364
+ except ImportError:
365
+ print("YAML configuration requires PyYAML. Install with: pip install PyYAML")
366
+ print("Continuing with default configuration.")
367
+ return file_config
368
+ else:
369
+ cls.debug_print(f"Unsupported config file format: {config_path}")
370
+ return file_config
371
+
372
+ except Exception as e:
373
+ cls.debug_print(f"Error loading config file: {e}")
374
+ return file_config
375
+
376
+ # Process level values to ensure they are uppercase
377
+ if "default_level" in file_config and isinstance(file_config["default_level"], str):
378
+ file_config["default_level"] = file_config["default_level"].upper()
379
+
380
+ # Also handle external_loggers levels
381
+ if "external_loggers" in file_config and isinstance(file_config["external_loggers"], dict):
382
+ for logger, level in file_config["external_loggers"].items():
383
+ if isinstance(level, str):
384
+ file_config["external_loggers"][logger] = level.upper()
385
+
386
+ return file_config
387
+
388
+ @classmethod
389
+ def _load_raw_env_config(cls) -> Dict[str, Any]:
390
+ """
391
+ Load raw environment variables without type conversion.
392
+
393
+ Looks for environment variables with both LOG_ and GITHUB_ prefixes.
394
+ For each configuration key, it checks the variables in order and uses
395
+ the first one found.
396
+
397
+ Returns:
398
+ Dictionary mapping configuration keys to environment variable values
399
+ """
400
+ env_config = {}
401
+
402
+ # Define lookup tables with ordered priorities
403
+ env_vars = {
404
+ "log_dir": ["LOG_DIR", "GITHUB_LOG_DIR"],
405
+ "default_level": ["LOG_LEVEL", "GITHUB_LOG_LEVEL"],
406
+ "rotation_size_mb": ["LOG_ROTATION_SIZE", "GITHUB_LOG_ROTATION_SIZE"],
407
+ "backup_count": ["LOG_BACKUP_COUNT", "GITHUB_LOG_BACKUP_COUNT"],
408
+ "log_format": ["LOG_FORMAT", "GITHUB_LOG_FORMAT"],
409
+ "colored_console": ["LOG_COLORED_CONSOLE", "GITHUB_LOG_COLORED_CONSOLE"],
410
+ "disable_rotation": ["LOG_DISABLE_ROTATION", "GITHUB_LOG_DISABLE_ROTATION"],
411
+ "exit_on_critical": ["LOG_EXIT_ON_CRITICAL", "GITHUB_LOG_EXIT_ON_CRITICAL"],
412
+ "test_mode": ["LOG_TEST_MODE", "GITHUB_LOG_TEST_MODE"],
413
+ }
414
+
415
+ # Efficiently check each config key using direct lookup
416
+ for config_key, env_vars_list in env_vars.items():
417
+ for env_var in env_vars_list:
418
+ if env_var in os.environ:
419
+ env_config[config_key] = os.environ[env_var]
420
+ break # Stop after finding the first matching env var
421
+
422
+ return env_config
423
+
424
+ @classmethod
425
+ def _load_raw_cli_args(cls) -> Dict[str, Any]:
426
+ """
427
+ Parse command-line arguments for logging configuration without type conversion.
428
+
429
+ This method creates an ArgumentParser that only handles logging-specific
430
+ arguments and uses parse_known_args() to avoid conflicts with
431
+ application-specific arguments.
432
+
433
+ If a config file is specified in the arguments, its content is loaded
434
+ and merged with the CLI arguments (with CLI args taking precedence).
435
+
436
+ Returns:
437
+ Dictionary of raw string values from command-line arguments
438
+ """
439
+ parser = argparse.ArgumentParser(add_help=False)
440
+ cls._add_logging_args_to_parser(parser)
441
+
442
+ # Parse only known args to avoid conflicts with application args
443
+ args, _ = parser.parse_known_args()
444
+
445
+ # Process the parsed arguments - collect only non-None values
446
+ arg_dict = {k: v for k, v in vars(args).items() if v is not None}
447
+
448
+ # Handle config file if present, but don't convert types yet
449
+ if "config_file" in arg_dict:
450
+ config_path = arg_dict.pop("config_file") # Remove the path from arg_dict
451
+ raw_file_config = cls._load_raw_file_config(config_path)
452
+
453
+ # Merge file config with arg_dict (CLI args take precedence)
454
+ merged_config = raw_file_config.copy()
455
+ merged_config.update(arg_dict) # CLI args override file config
456
+ return merged_config
457
+
458
+ return arg_dict
459
+
460
+ @classmethod
461
+ def load_from_file(cls, config_path: str) -> Dict[str, Any]:
462
+ """
463
+ Load and convert configuration from a YAML file.
464
+
465
+ This is a convenience method that loads raw configuration from a file
466
+ and then converts the values to appropriate types.
467
+
468
+ Args:
469
+ config_path: Path to the configuration file
470
+
471
+ Returns:
472
+ Dictionary with configuration values converted to appropriate types
473
+ """
474
+ raw_config = cls._load_raw_file_config(config_path)
475
+ return cls._convert_config_values(raw_config)
476
+
477
+ @classmethod
478
+ def load_from_env(cls) -> Dict[str, Any]:
479
+ """
480
+ Load and convert configuration from environment variables.
481
+
482
+ This is a convenience method that loads raw configuration from
483
+ environment variables and then converts the values to appropriate types.
484
+
485
+ Returns:
486
+ Dictionary with configuration values converted to appropriate types
487
+ """
488
+ raw_config = cls._load_raw_env_config()
489
+ return cls._convert_config_values(raw_config)
490
+
491
+ @classmethod
492
+ def _add_logging_args_to_parser(cls, parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
493
+ """
494
+ Add standard logging arguments to an existing parser.
495
+
496
+ Adds the following arguments to the parser:
497
+ - --log-config, --log-conf: Path to logging configuration file
498
+ - --log-level, --logging-level: Default logging level
499
+ - --log-dir, --logging-dir: Directory for log files
500
+
501
+ Args:
502
+ parser: An existing ArgumentParser to add arguments to
503
+
504
+ Returns:
505
+ The modified ArgumentParser with logging arguments added
506
+ """
507
+ parser.add_argument(
508
+ "--log-config",
509
+ "--log-conf",
510
+ dest="config_file",
511
+ help="Path to logging configuration file",
512
+ )
513
+
514
+ parser.add_argument(
515
+ "--log-level",
516
+ "--logging-level",
517
+ dest="default_level",
518
+ type=str.upper,
519
+ choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
520
+ help="Set the default logging level",
521
+ )
522
+
523
+ parser.add_argument(
524
+ "--log-dir", "--logging-dir", dest="log_dir", help="Directory where log files will be stored"
525
+ )
526
+
527
+ parser.add_argument(
528
+ "--log-format",
529
+ "--logging-format",
530
+ dest="log_format",
531
+ help="Format string for log messages (e.g. '%(asctime)s - %(message)s')",
532
+ )
533
+
534
+ return parser
535
+
536
+ @classmethod
537
+ def parse_cli_args(cls) -> Dict[str, Any]:
538
+ """
539
+ Parse command-line arguments for logging configuration.
540
+
541
+ Parses command-line arguments for logging configuration options.
542
+ Only recognizes specific logging-related arguments and ignores
543
+ other arguments that might be intended for the application.
544
+
545
+ Supported arguments:
546
+ --log-config, --log-conf: Path to logging configuration file
547
+ --log-level, --logging-level: Default logging level
548
+ --log-dir, --logging-dir: Directory for log files
549
+
550
+ Returns:
551
+ Dictionary of parsed command-line arguments with proper type conversion
552
+
553
+ Note:
554
+ This method uses argparse.parse_known_args() to avoid conflicts
555
+ with application-specific command-line arguments.
556
+ """
557
+ parser = argparse.ArgumentParser(add_help=False)
558
+ cls._add_logging_args_to_parser(parser)
559
+
560
+ # Parse only known args to avoid conflicts with application args
561
+ args, _ = parser.parse_known_args()
562
+
563
+ # Process the parsed arguments
564
+ arg_dict = {k: v for k, v in vars(args).items() if v is not None}
565
+
566
+ # If --log-config was provided, load that config file
567
+ if "config_file" in arg_dict:
568
+ config_path = arg_dict.pop("config_file") # Remove the path from arg_dict
569
+ file_config = cls.load_from_file(config_path)
570
+
571
+ # Merge file config with arg_dict (CLI args take precedence)
572
+ merged_config = file_config.copy()
573
+ merged_config.update(arg_dict) # CLI args override file config
574
+ return merged_config
575
+
576
+ return arg_dict
577
+
578
+ @classmethod
579
+ def get_config(cls) -> Dict[str, Any]:
580
+ """
581
+ Get the complete current configuration.
582
+
583
+ If the configuration hasn't been initialized yet, this will
584
+ initialize it with default values before returning.
585
+
586
+ Returns:
587
+ Dictionary containing all configuration values
588
+
589
+ Example:
590
+ ```python
591
+ config = LoggingConfig.get_config()
592
+ print(f"Log directory: {config['log_dir']}")
593
+ ```
594
+ """
595
+ if not cls._initialized:
596
+ cls.initialize()
597
+ return cls._config
598
+
599
+ @classmethod
600
+ def get(cls, key: str, default: Any = None) -> Any:
601
+ """
602
+ Get a specific configuration value.
603
+
604
+ Args:
605
+ key: The configuration key to retrieve
606
+ default: Value to return if the key is not found
607
+
608
+ Returns:
609
+ The configuration value or the default if not found
610
+
611
+ Example:
612
+ ```python
613
+ log_dir = LoggingConfig.get("log_dir")
614
+ level = LoggingConfig.get("default_level", "INFO")
615
+ ```
616
+ """
617
+ if not cls._initialized:
618
+ cls.initialize()
619
+
620
+ # Special handling for nested keys
621
+ if key == "module_levels":
622
+ # Return the module_levels dictionary if it exists
623
+ if "module_levels" in cls._config and isinstance(cls._config["module_levels"], dict):
624
+ return cls._config["module_levels"]
625
+ return {}
626
+
627
+ # Normal key
628
+ # First check if it's in current config
629
+ if key in cls._config:
630
+ return cls._config[key]
631
+
632
+ # If not, check if it's in DEFAULT_CONFIG
633
+ if key in cls.DEFAULT_CONFIG:
634
+ return cls.DEFAULT_CONFIG[key]
635
+
636
+ # If not found in either place, return the provided default
637
+ return default
638
+
639
+ @classmethod
640
+ def set(cls, key: str, value: Any) -> None:
641
+ """
642
+ Set or update a configuration value.
643
+
644
+ This allows runtime modification of configuration values. Changes
645
+ will be reflected in any new loggers created after the change.
646
+
647
+ Supports nested keys with dot notation (e.g., "module_levels.my_module").
648
+
649
+ Args:
650
+ key: The configuration key to set
651
+ value: The value to assign to the key
652
+
653
+ Example:
654
+ >>> # Disable exiting on critical logs
655
+ >>> LoggingConfig.set("exit_on_critical", False)
656
+ >>>
657
+ >>> # Set module-specific log level
658
+ >>> LoggingConfig.set("module_levels.my_module", "DEBUG")
659
+ """
660
+ # Handle nested keys (with dot notation)
661
+ if "." in key:
662
+ # Split the key path
663
+ parts = key.split(".")
664
+ main_key = parts[0]
665
+ sub_key = parts[1]
666
+
667
+ # Ensure parent dictionary exists
668
+ if main_key not in cls._config:
669
+ cls._config[main_key] = {}
670
+ elif not isinstance(cls._config[main_key], dict):
671
+ # If it exists but is not a dict, convert it to a dict
672
+ cls._config[main_key] = {}
673
+
674
+ # Set the value in the nested dict
675
+ cls._config[main_key][sub_key] = value
676
+ else:
677
+ # Simple case: direct key
678
+ cls._config[key] = value
679
+
680
+ @classmethod
681
+ def get_level(cls, name: Optional[str] = None, default_level: Optional[str] = None) -> int:
682
+ """
683
+ Get the numeric log level based on configuration priority.
684
+
685
+ This method determines the appropriate log level based on priority order:
686
+ 1. Explicit level parameter (highest priority)
687
+ 2. Logger-specific configuration from external_loggers
688
+ 3. Default level from configuration
689
+
690
+ Args:
691
+ name: Optional logger name to check for specific configuration
692
+ default_level: Optional override for the default level
693
+
694
+ Returns:
695
+ The numeric logging level (from logging module constants)
696
+
697
+ Examples:
698
+ Get default level for application:
699
+
700
+ >>> default_level = LoggingConfig.get_level()
701
+
702
+ Get level for a specific module:
703
+
704
+ >>> requests_level = LoggingConfig.get_level("requests.packages.urllib3")
705
+
706
+ Override with explicit level:
707
+
708
+ >>> debug_level = LoggingConfig.get_level(default_level="DEBUG")
709
+ """
710
+ # 1. First priority: Use explicit level if provided
711
+ if default_level:
712
+ return cls.map_level(default_level)
713
+
714
+ # 2. Second priority: Check logger-specific config
715
+ if name:
716
+ external_loggers = cls.get("external_loggers", {})
717
+ if name in external_loggers:
718
+ return cls.map_level(external_loggers[name])
719
+
720
+ # 3. Third priority: Use configured default level
721
+ config_level = cls.get("default_level", "INFO")
722
+ return cls.map_level(config_level)
723
+
724
+ @classmethod
725
+ def map_level(cls, level: str) -> int:
726
+ """
727
+ Map string log level to numeric level.
728
+
729
+ Converts string log level names to their corresponding numeric values
730
+ from the logging module. If the level string is not recognized,
731
+ defaults to logging.INFO.
732
+
733
+ Args:
734
+ level: String log level ('DEBUG', 'INFO', etc.)
735
+
736
+ Returns:
737
+ The corresponding numeric logging level (from logging module constants)
738
+
739
+ Example:
740
+ ```python
741
+ debug_level = LoggingConfig.map_level("DEBUG") # Returns 10
742
+ warn_level = LoggingConfig.map_level("WARNING") # Returns 30
743
+ ```
744
+ """
745
+ import logging # pylint: disable=import-outside-toplevel
746
+
747
+ return {
748
+ "DEBUG": logging.DEBUG,
749
+ "INFO": logging.INFO,
750
+ "WARNING": logging.WARNING,
751
+ "WARN": logging.WARNING,
752
+ "ERROR": logging.ERROR,
753
+ "CRITICAL": logging.CRITICAL,
754
+ }.get(level, logging.INFO)
755
+
756
+ @classmethod
757
+ def reset(cls) -> Type["LoggingConfig"]:
758
+ """
759
+ Reset configuration to default values.
760
+
761
+ This method restores all configuration settings to their default values,
762
+ which is particularly useful for testing where each test needs to start
763
+ with a clean configuration state.
764
+
765
+ Returns:
766
+ The LoggingConfig class for method chaining
767
+
768
+ Example:
769
+ ```python
770
+ # Reset to defaults and then set a specific value
771
+ LoggingConfig.reset().set("colored_console", False)
772
+ ```
773
+ """
774
+ # Reset to default values
775
+ cls._config = cls.DEFAULT_CONFIG.copy()
776
+
777
+ # Reset initialization state
778
+ cls._initialized = False
779
+
780
+ # For debugging
781
+ cls.debug_print("LoggingConfig reset to default values")
782
+
783
+ return cls