1+ """
2+ Laravel-like configuration manager for Python applications.
3+ Supports loading config files from the config directory and environment overrides.
4+ """
5+
6+ import os
7+ import importlib .util
8+ from typing import Any , Dict , Optional
9+ from pathlib import Path
10+
11+
12+ class ConfigManager :
13+ """Manages application configuration with support for multiple config files and environment overrides."""
14+
15+ def __init__ (self , config_dir : str = "config" , env_file : str = ".env" ):
16+ self .config_dir = Path (config_dir )
17+ self .env_file = Path (env_file )
18+ self ._config_cache : Dict [str , Dict [str , Any ]] = {}
19+ self ._env_vars : Dict [str , str ] = {}
20+
21+ # Load environment variables from .env file
22+ self ._load_env_file ()
23+
24+ def _load_env_file (self ) -> None :
25+ """Load environment variables from .env file if it exists."""
26+ if self .env_file .exists ():
27+ with open (self .env_file , 'r' ) as f :
28+ for line in f :
29+ line = line .strip ()
30+ if line and not line .startswith ('#' ) and '=' in line :
31+ key , value = line .split ('=' , 1 )
32+ self ._env_vars [key .strip ()] = value .strip ().strip ('"' ).strip ("'" )
33+
34+ def _load_config_file (self , config_name : str ) -> Dict [str , Any ]:
35+ """Load a specific config file."""
36+ config_file = self .config_dir / f"{ config_name } .py"
37+
38+ if not config_file .exists ():
39+ return {}
40+
41+ spec = importlib .util .spec_from_file_location (config_name , config_file )
42+ if spec is None or spec .loader is None :
43+ return {}
44+
45+ module = importlib .util .module_from_spec (spec )
46+ spec .loader .exec_module (module )
47+
48+ # Extract all non-private attributes as config values
49+ config_data = {}
50+ for attr_name in dir (module ):
51+ if not attr_name .startswith ('_' ):
52+ config_data [attr_name ] = getattr (module , attr_name )
53+
54+ return config_data
55+
56+ def get (self , key : str , default : Any = None ) -> Any :
57+ """
58+ Get a configuration value using dot notation (e.g., 'app.name' or 'database.host').
59+ Environment variables take precedence over config files.
60+ """
61+ # Check for environment variable override first
62+ env_key = key .upper ().replace ('.' , '_' )
63+ if env_key in self ._env_vars :
64+ value = self ._env_vars [env_key ]
65+ # Convert string boolean values
66+ if value .lower () in ('true' , 'false' ):
67+ return value .lower () == 'true'
68+ return value
69+ if env_key in os .environ :
70+ value = os .environ [env_key ]
71+ # Convert string boolean values
72+ if value .lower () in ('true' , 'false' ):
73+ return value .lower () == 'true'
74+ return value
75+
76+ # Parse the key to get config file and setting
77+ if '.' not in key :
78+ return default
79+
80+ config_name , setting_path = key .split ('.' , 1 )
81+
82+ # Load config file if not cached
83+ if config_name not in self ._config_cache :
84+ self ._config_cache [config_name ] = self ._load_config_file (config_name )
85+
86+ config_data = self ._config_cache [config_name ]
87+
88+ # Navigate through nested settings using dot notation
89+ current = config_data
90+ for part in setting_path .split ('.' ):
91+ if isinstance (current , dict ) and part in current :
92+ current = current [part ]
93+ else :
94+ return default
95+
96+ return current
97+
98+ def set (self , key : str , value : Any ) -> None :
99+ """Set a configuration value in the cache (runtime only)."""
100+ if '.' not in key :
101+ return
102+
103+ config_name , setting_path = key .split ('.' , 1 )
104+
105+ # Ensure config is loaded
106+ if config_name not in self ._config_cache :
107+ self ._config_cache [config_name ] = self ._load_config_file (config_name )
108+
109+ # Navigate and set the value
110+ current = self ._config_cache [config_name ]
111+ parts = setting_path .split ('.' )
112+
113+ for part in parts [:- 1 ]:
114+ if part not in current or not isinstance (current [part ], dict ):
115+ current [part ] = {}
116+ current = current [part ]
117+
118+ current [parts [- 1 ]] = value
119+
120+ def reload (self , config_name : Optional [str ] = None ) -> None :
121+ """Reload configuration files. If config_name is None, reload all."""
122+ if config_name :
123+ if config_name in self ._config_cache :
124+ del self ._config_cache [config_name ]
125+ else :
126+ self ._config_cache .clear ()
127+ self ._load_env_file ()
128+
129+ def all (self , config_name : str ) -> Dict [str , Any ]:
130+ """Get all configuration values for a specific config file."""
131+ if config_name not in self ._config_cache :
132+ self ._config_cache [config_name ] = self ._load_config_file (config_name )
133+ return self ._config_cache [config_name ].copy ()
134+
135+ def has (self , key : str ) -> bool :
136+ """Check if a configuration key exists."""
137+ sentinel = object ()
138+ try :
139+ result = self .get (key , sentinel )
140+ return result is not sentinel
141+ except :
142+ return False
143+
144+
145+ # Global config manager instance
146+ _config_manager = ConfigManager ()
147+
148+
149+ def config (key : str , default : Any = None ) -> Any :
150+ """Get a configuration value."""
151+ return _config_manager .get (key , default )
152+
153+
154+ def config_set (key : str , value : Any ) -> None :
155+ """Set a configuration value."""
156+ _config_manager .set (key , value )
157+
158+
159+ def config_reload (config_name : Optional [str ] = None ) -> None :
160+ """Reload configuration."""
161+ _config_manager .reload (config_name )
162+
163+
164+ def config_all (config_name : str ) -> Dict [str , Any ]:
165+ """Get all configuration for a config file."""
166+ return _config_manager .all (config_name )
167+
168+
169+ def config_has (key : str ) -> bool :
170+ """Check if a configuration key exists."""
171+ return _config_manager .has (key )
0 commit comments