11import os
2+ import re
23import threading
3- from typing import Any , Dict
4+ import logging
5+ from typing import Any , Dict , Optional , Union
46
57import yaml
68
9+ logger = logging .getLogger (__name__ )
10+
11+
12+ def _parse_string_type (value : Any ) -> Any :
13+ """Convert string values to appropriate Python types (bool, int, float)."""
14+ if not isinstance (value , str ):
15+ return value
16+
17+ stripped = value .strip ()
18+ if not stripped :
19+ return stripped
20+
21+ lower_val = stripped .lower ()
22+
23+ # Boolean conversion (support multiple formats)
24+ if lower_val == "true" :
25+ return True
26+ if lower_val == "false" :
27+ return False
28+
29+ # Numeric conversion
30+ try :
31+ if "." in stripped and stripped .count ("." ) == 1 :
32+ if not stripped .startswith ("." ) and not stripped .endswith ("." ):
33+ return float (stripped )
34+ return int (stripped )
35+ except ValueError :
36+ return stripped
37+
738
839class ConfigUtils :
940 """
10- Singleton Configuration Utility
11- Provides methods to read and access YAML configuration files.
41+ Thread-safe singleton configuration utility.
42+
43+ Features:
44+ - YAML config with environment variable substitution ${VAR:-default}
45+ - Automatic type conversion for string values
46+ - Nested key access using dot notation
1247 """
1348
14- _instance = None
15- _lock = threading .Lock () # Ensure thread-safe singleton creation
49+ _instance : Optional ["ConfigUtils" ] = None
50+ _lock = threading .Lock ()
51+ _init_lock = threading .Lock ()
1652
17- def __init__ (self ):
18- self ._config = None
53+ ENV_PATTERN = re .compile (r"^\$\{(\w+)(?::-([^}]*))?\}$" )
1954
20- def __new__ (cls , config_file : str = None ):
21- # Double-checked locking
55+ def __new__ (cls , config_file : Optional [ str ] = None , ** kwargs ):
56+ """ Double-checked locking for thread-safe singleton"""
2257 if cls ._instance is None :
2358 with cls ._lock :
2459 if cls ._instance is None :
2560 instance = super ().__new__ (cls )
26- instance ._init_config (config_file )
61+ instance ._config = None
62+ instance .config_file = None
2763 cls ._instance = instance
64+ instance ._init_config (config_file )
65+
2866 return cls ._instance
2967
30- def _init_config (self , config_file : str = None ):
31- """Initialize configuration file path and load config"""
68+ def __init__ (self ):
69+ self ._config = None
70+
71+ @classmethod
72+ def get_instance (cls , config_file : Optional [str ] = None ) -> "ConfigUtils" :
73+ """Get singleton instance"""
74+ return cls (config_file )
75+
76+ def _init_config (self , config_file : Optional [str ] = None ):
77+ """Initialize config file path"""
3278 if config_file is None :
3379 current_dir = os .path .dirname (os .path .abspath (__file__ ))
3480 config_file = os .path .join (current_dir , ".." , "config.yaml" )
3581
3682 self .config_file = os .path .abspath (config_file )
37- self ._config = None # Lazy load
83+ logger .info (f"Configuration file path set to: { self .config_file } " )
84+
85+ def _substitute_env_vars (self , data : Any ) -> Any :
86+ """Recursively substitute environment variables"""
87+ if isinstance (data , dict ):
88+ return {k : self ._substitute_env_vars (v ) for k , v in data .items ()}
89+ elif isinstance (data , list ):
90+ return [self ._substitute_env_vars (item ) for item in data ]
91+ elif isinstance (data , str ):
92+ return self ._process_string_value (data )
93+ return data
94+
95+ def _process_string_value (self , value : str ) -> Any :
96+ """Process string: check for env var pattern, then type conversion"""
97+ match = self .ENV_PATTERN .fullmatch (value .strip ())
98+
99+ if match :
100+ var_name = match .group (1 )
101+ default_val = match .group (2 )
102+
103+ env_value = os .getenv (var_name )
104+ if env_value is not None :
105+ return _parse_string_type (env_value .strip ())
106+ elif default_val is not None :
107+ return _parse_string_type (default_val .strip ())
108+ else :
109+ logger .warning (f"Env var '{ var_name } ' not found, no default. Keeping: { value } " )
110+ return value
111+ else :
112+ return _parse_string_type (value )
38113
39114 def _load_config (self ) -> Dict [str , Any ]:
40- """Internal method to read configuration from file"""
115+ """Load config from file"""
116+ if not self .config_file :
117+ logger .error ("Config file path not initialized" )
118+ return {}
119+
120+ if not os .path .exists (self .config_file ):
121+ logger .warning (f"Configuration file not found: { self .config_file } " )
122+ return {}
123+
41124 try :
42125 with open (self .config_file , "r" , encoding = "utf-8" ) as f :
43- return yaml .safe_load (f ) or {}
44- except FileNotFoundError :
45- print (f"[WARN] Config file not found: { self .config_file } " )
46- return {}
126+ raw_config = yaml .safe_load (f ) or {}
127+ return self ._substitute_env_vars (raw_config )
47128 except yaml .YAMLError as e :
48- print (f"[ERROR] Failed to parse YAML config: { e } " )
129+ logger .error (f"YAML parsing error: { e } " )
130+ return {}
131+ except Exception as e :
132+ logger .error (f"Error loading config: { e } " )
49133 return {}
50134
51135 def read_config (self ) -> Dict [str , Any ]:
52- """Read configuration file (lazy load) """
136+ """Lazy load configuration """
53137 if self ._config is None :
54- self ._config = self ._load_config ()
138+ with self ._init_lock :
139+ if self ._config is None :
140+ self ._config = self ._load_config ()
55141 return self ._config
56142
57- def reload_config (self ):
58- """Force reload configuration file"""
59- self ._config = self ._load_config ()
143+ def reload_config (self ) -> Dict [str , Any ]:
144+ """Force reload configuration"""
145+ with self ._init_lock :
146+ self ._config = self ._load_config ()
147+ logger .info (f"Configuration reloaded successfully" )
148+ return self ._config
60149
61150 def get_config (self , key : str , default : Any = None ) -> Any :
62- """Get top-level configuration item"""
63- config = self .read_config ()
64- return config .get (key , default )
151+ """Get top-level config item"""
152+ return self .read_config ().get (key , default )
65153
66- def get_nested_config (self , key_path : str , default : Any = None ) -> Any :
67- """Get nested configuration, e.g., 'influxdb .host'"""
154+ def get_nested_config (self , key_path : str , default : Any = None , separator : str = "." ) -> Any :
155+ """Get nested config using dot notation ( e.g., 'database .host') """
68156 config = self .read_config ()
69- keys = key_path .split ("." )
157+ keys = key_path .split (separator )
70158 value = config
159+
71160 try :
72- for k in keys :
73- value = value [k ]
161+ for key in keys :
162+ if not isinstance (value , dict ):
163+ return default
164+ value = value [key ]
74165 return value
75166 except (KeyError , TypeError ):
76167 return default
77168
169+ def __getitem__ (self , key : str ) -> Any :
170+ """Support config_utils['key']"""
171+ return self .get_config (key )
172+
173+ def __contains__ (self , key : str ) -> bool :
174+ """Support 'key' in config_utils"""
175+ return key in self .read_config ()
176+
78177
79178# Global instance
80179config_utils = ConfigUtils ()
81180
82181if __name__ == "__main__" :
83- print ("DataBase config:" , config_utils .get_config ("database" ))
84- print (
85- "DataBase host:" , config_utils .get_nested_config ("database.host" , "localhost" )
86- )
182+ print ("=== Configuration Test ===" )
183+ print (f"Config file path: { config_utils .config_file } " )
184+ print (f"LLM Connection: { config_utils .get_config ('llm_connection' )} " )
185+ print (f"EXTRA_INFO: { config_utils .get_nested_config ('llm_connection.extra_info' )} " )
186+ os .environ ["LLM_EX_INFO" ] = "prefix cache and gsa"
187+ config_utils .reload_config ()
188+ print (f"EXTRA_INFO: { config_utils .get_nested_config ('llm_connection.extra_info' )} " )
189+ print (f"Timeout: { config_utils .get_nested_config ('llm_connection.timeout' )} (type: { type (config_utils .get_nested_config ('llm_connection.timeout' ))} )" )
0 commit comments