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/__init__.py +35 -0
- prismalog/argparser.py +216 -0
- prismalog/config.py +783 -0
- prismalog/log.py +927 -0
- prismalog/py.typed +0 -0
- prismalog-0.1.0.dist-info/METADATA +184 -0
- prismalog-0.1.0.dist-info/RECORD +10 -0
- prismalog-0.1.0.dist-info/WHEEL +5 -0
- prismalog-0.1.0.dist-info/licenses/LICENSE +21 -0
- prismalog-0.1.0.dist-info/top_level.txt +1 -0
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
|