kiln_ai.utils.config
1import getpass 2import os 3import threading 4from pathlib import Path 5from typing import Any, Callable, Dict, Optional 6 7import yaml 8 9 10class ConfigProperty: 11 def __init__( 12 self, 13 type_: type, 14 default: Any = None, 15 env_var: Optional[str] = None, 16 default_lambda: Optional[Callable[[], Any]] = None, 17 sensitive: bool = False, 18 ): 19 self.type = type_ 20 self.default = default 21 self.env_var = env_var 22 self.default_lambda = default_lambda 23 self.sensitive = sensitive 24 25 26class Config: 27 _shared_instance = None 28 29 def __init__(self, properties: Dict[str, ConfigProperty] | None = None): 30 self._properties: Dict[str, ConfigProperty] = properties or { 31 "user_id": ConfigProperty( 32 str, 33 env_var="KILN_USER_ID", 34 default_lambda=_get_user_id, 35 ), 36 "autosave_runs": ConfigProperty( 37 bool, 38 env_var="KILN_AUTOSAVE_RUNS", 39 default=True, 40 ), 41 "open_ai_api_key": ConfigProperty( 42 str, 43 env_var="OPENAI_API_KEY", 44 sensitive=True, 45 ), 46 "groq_api_key": ConfigProperty( 47 str, 48 env_var="GROQ_API_KEY", 49 sensitive=True, 50 ), 51 "ollama_base_url": ConfigProperty( 52 str, 53 env_var="OLLAMA_BASE_URL", 54 ), 55 "bedrock_access_key": ConfigProperty( 56 str, 57 env_var="AWS_ACCESS_KEY_ID", 58 sensitive=True, 59 ), 60 "bedrock_secret_key": ConfigProperty( 61 str, 62 env_var="AWS_SECRET_ACCESS_KEY", 63 sensitive=True, 64 ), 65 "open_router_api_key": ConfigProperty( 66 str, 67 env_var="OPENROUTER_API_KEY", 68 sensitive=True, 69 ), 70 "fireworks_api_key": ConfigProperty( 71 str, 72 env_var="FIREWORKS_API_KEY", 73 sensitive=True, 74 ), 75 "fireworks_account_id": ConfigProperty( 76 str, 77 env_var="FIREWORKS_ACCOUNT_ID", 78 ), 79 "projects": ConfigProperty( 80 list, 81 default_lambda=lambda: [], 82 ), 83 "custom_models": ConfigProperty( 84 list, 85 default_lambda=lambda: [], 86 ), 87 } 88 self._settings = self.load_settings() 89 90 @classmethod 91 def shared(cls): 92 if cls._shared_instance is None: 93 cls._shared_instance = cls() 94 return cls._shared_instance 95 96 # Get a value, mockable for testing 97 def get_value(self, name: str) -> Any: 98 try: 99 return self.__getattr__(name) 100 except AttributeError: 101 return None 102 103 def __getattr__(self, name: str) -> Any: 104 if name == "_properties": 105 return super().__getattribute__("_properties") 106 if name not in self._properties: 107 return super().__getattribute__(name) 108 109 property_config = self._properties[name] 110 111 # Check if the value is in settings 112 if name in self._settings: 113 value = self._settings[name] 114 return value if value is None else property_config.type(value) 115 116 # Check environment variable 117 if property_config.env_var and property_config.env_var in os.environ: 118 value = os.environ[property_config.env_var] 119 return property_config.type(value) 120 121 # Use default value or default_lambda 122 if property_config.default_lambda: 123 value = property_config.default_lambda() 124 else: 125 value = property_config.default 126 127 return None if value is None else property_config.type(value) 128 129 def __setattr__(self, name, value): 130 if name in ("_properties", "_settings"): 131 super().__setattr__(name, value) 132 elif name in self._properties: 133 self.update_settings({name: value}) 134 else: 135 raise AttributeError(f"Config has no attribute '{name}'") 136 137 @classmethod 138 def settings_path(cls, create=True): 139 settings_dir = os.path.join(Path.home(), ".kiln_ai") 140 if create and not os.path.exists(settings_dir): 141 os.makedirs(settings_dir) 142 return os.path.join(settings_dir, "settings.yaml") 143 144 @classmethod 145 def load_settings(cls): 146 if not os.path.isfile(cls.settings_path(create=False)): 147 return {} 148 with open(cls.settings_path(), "r") as f: 149 settings = yaml.safe_load(f.read()) or {} 150 return settings 151 152 def settings(self, hide_sensitive=False) -> Dict[str, Any]: 153 if hide_sensitive: 154 return { 155 k: "[hidden]" 156 if k in self._properties and self._properties[k].sensitive 157 else v 158 for k, v in self._settings.items() 159 } 160 return self._settings 161 162 def save_setting(self, name: str, value: Any): 163 self.update_settings({name: value}) 164 165 def update_settings(self, new_settings: Dict[str, Any]): 166 # Lock to prevent race conditions in multi-threaded scenarios 167 with threading.Lock(): 168 # Fresh load to avoid clobbering changes from other instances 169 current_settings = self.load_settings() 170 current_settings.update(new_settings) 171 # remove None values 172 current_settings = { 173 k: v for k, v in current_settings.items() if v is not None 174 } 175 with open(self.settings_path(), "w") as f: 176 yaml.dump(current_settings, f) 177 self._settings = current_settings 178 179 180def _get_user_id(): 181 try: 182 return getpass.getuser() or "unknown_user" 183 except Exception: 184 return "unknown_user"
class
ConfigProperty:
11class ConfigProperty: 12 def __init__( 13 self, 14 type_: type, 15 default: Any = None, 16 env_var: Optional[str] = None, 17 default_lambda: Optional[Callable[[], Any]] = None, 18 sensitive: bool = False, 19 ): 20 self.type = type_ 21 self.default = default 22 self.env_var = env_var 23 self.default_lambda = default_lambda 24 self.sensitive = sensitive
ConfigProperty( type_: type, default: Any = None, env_var: Optional[str] = None, default_lambda: Optional[Callable[[], Any]] = None, sensitive: bool = False)
12 def __init__( 13 self, 14 type_: type, 15 default: Any = None, 16 env_var: Optional[str] = None, 17 default_lambda: Optional[Callable[[], Any]] = None, 18 sensitive: bool = False, 19 ): 20 self.type = type_ 21 self.default = default 22 self.env_var = env_var 23 self.default_lambda = default_lambda 24 self.sensitive = sensitive
class
Config:
27class Config: 28 _shared_instance = None 29 30 def __init__(self, properties: Dict[str, ConfigProperty] | None = None): 31 self._properties: Dict[str, ConfigProperty] = properties or { 32 "user_id": ConfigProperty( 33 str, 34 env_var="KILN_USER_ID", 35 default_lambda=_get_user_id, 36 ), 37 "autosave_runs": ConfigProperty( 38 bool, 39 env_var="KILN_AUTOSAVE_RUNS", 40 default=True, 41 ), 42 "open_ai_api_key": ConfigProperty( 43 str, 44 env_var="OPENAI_API_KEY", 45 sensitive=True, 46 ), 47 "groq_api_key": ConfigProperty( 48 str, 49 env_var="GROQ_API_KEY", 50 sensitive=True, 51 ), 52 "ollama_base_url": ConfigProperty( 53 str, 54 env_var="OLLAMA_BASE_URL", 55 ), 56 "bedrock_access_key": ConfigProperty( 57 str, 58 env_var="AWS_ACCESS_KEY_ID", 59 sensitive=True, 60 ), 61 "bedrock_secret_key": ConfigProperty( 62 str, 63 env_var="AWS_SECRET_ACCESS_KEY", 64 sensitive=True, 65 ), 66 "open_router_api_key": ConfigProperty( 67 str, 68 env_var="OPENROUTER_API_KEY", 69 sensitive=True, 70 ), 71 "fireworks_api_key": ConfigProperty( 72 str, 73 env_var="FIREWORKS_API_KEY", 74 sensitive=True, 75 ), 76 "fireworks_account_id": ConfigProperty( 77 str, 78 env_var="FIREWORKS_ACCOUNT_ID", 79 ), 80 "projects": ConfigProperty( 81 list, 82 default_lambda=lambda: [], 83 ), 84 "custom_models": ConfigProperty( 85 list, 86 default_lambda=lambda: [], 87 ), 88 } 89 self._settings = self.load_settings() 90 91 @classmethod 92 def shared(cls): 93 if cls._shared_instance is None: 94 cls._shared_instance = cls() 95 return cls._shared_instance 96 97 # Get a value, mockable for testing 98 def get_value(self, name: str) -> Any: 99 try: 100 return self.__getattr__(name) 101 except AttributeError: 102 return None 103 104 def __getattr__(self, name: str) -> Any: 105 if name == "_properties": 106 return super().__getattribute__("_properties") 107 if name not in self._properties: 108 return super().__getattribute__(name) 109 110 property_config = self._properties[name] 111 112 # Check if the value is in settings 113 if name in self._settings: 114 value = self._settings[name] 115 return value if value is None else property_config.type(value) 116 117 # Check environment variable 118 if property_config.env_var and property_config.env_var in os.environ: 119 value = os.environ[property_config.env_var] 120 return property_config.type(value) 121 122 # Use default value or default_lambda 123 if property_config.default_lambda: 124 value = property_config.default_lambda() 125 else: 126 value = property_config.default 127 128 return None if value is None else property_config.type(value) 129 130 def __setattr__(self, name, value): 131 if name in ("_properties", "_settings"): 132 super().__setattr__(name, value) 133 elif name in self._properties: 134 self.update_settings({name: value}) 135 else: 136 raise AttributeError(f"Config has no attribute '{name}'") 137 138 @classmethod 139 def settings_path(cls, create=True): 140 settings_dir = os.path.join(Path.home(), ".kiln_ai") 141 if create and not os.path.exists(settings_dir): 142 os.makedirs(settings_dir) 143 return os.path.join(settings_dir, "settings.yaml") 144 145 @classmethod 146 def load_settings(cls): 147 if not os.path.isfile(cls.settings_path(create=False)): 148 return {} 149 with open(cls.settings_path(), "r") as f: 150 settings = yaml.safe_load(f.read()) or {} 151 return settings 152 153 def settings(self, hide_sensitive=False) -> Dict[str, Any]: 154 if hide_sensitive: 155 return { 156 k: "[hidden]" 157 if k in self._properties and self._properties[k].sensitive 158 else v 159 for k, v in self._settings.items() 160 } 161 return self._settings 162 163 def save_setting(self, name: str, value: Any): 164 self.update_settings({name: value}) 165 166 def update_settings(self, new_settings: Dict[str, Any]): 167 # Lock to prevent race conditions in multi-threaded scenarios 168 with threading.Lock(): 169 # Fresh load to avoid clobbering changes from other instances 170 current_settings = self.load_settings() 171 current_settings.update(new_settings) 172 # remove None values 173 current_settings = { 174 k: v for k, v in current_settings.items() if v is not None 175 } 176 with open(self.settings_path(), "w") as f: 177 yaml.dump(current_settings, f) 178 self._settings = current_settings
Config( properties: Optional[Dict[str, ConfigProperty]] = None)
30 def __init__(self, properties: Dict[str, ConfigProperty] | None = None): 31 self._properties: Dict[str, ConfigProperty] = properties or { 32 "user_id": ConfigProperty( 33 str, 34 env_var="KILN_USER_ID", 35 default_lambda=_get_user_id, 36 ), 37 "autosave_runs": ConfigProperty( 38 bool, 39 env_var="KILN_AUTOSAVE_RUNS", 40 default=True, 41 ), 42 "open_ai_api_key": ConfigProperty( 43 str, 44 env_var="OPENAI_API_KEY", 45 sensitive=True, 46 ), 47 "groq_api_key": ConfigProperty( 48 str, 49 env_var="GROQ_API_KEY", 50 sensitive=True, 51 ), 52 "ollama_base_url": ConfigProperty( 53 str, 54 env_var="OLLAMA_BASE_URL", 55 ), 56 "bedrock_access_key": ConfigProperty( 57 str, 58 env_var="AWS_ACCESS_KEY_ID", 59 sensitive=True, 60 ), 61 "bedrock_secret_key": ConfigProperty( 62 str, 63 env_var="AWS_SECRET_ACCESS_KEY", 64 sensitive=True, 65 ), 66 "open_router_api_key": ConfigProperty( 67 str, 68 env_var="OPENROUTER_API_KEY", 69 sensitive=True, 70 ), 71 "fireworks_api_key": ConfigProperty( 72 str, 73 env_var="FIREWORKS_API_KEY", 74 sensitive=True, 75 ), 76 "fireworks_account_id": ConfigProperty( 77 str, 78 env_var="FIREWORKS_ACCOUNT_ID", 79 ), 80 "projects": ConfigProperty( 81 list, 82 default_lambda=lambda: [], 83 ), 84 "custom_models": ConfigProperty( 85 list, 86 default_lambda=lambda: [], 87 ), 88 } 89 self._settings = self.load_settings()
def
update_settings(self, new_settings: Dict[str, Any]):
166 def update_settings(self, new_settings: Dict[str, Any]): 167 # Lock to prevent race conditions in multi-threaded scenarios 168 with threading.Lock(): 169 # Fresh load to avoid clobbering changes from other instances 170 current_settings = self.load_settings() 171 current_settings.update(new_settings) 172 # remove None values 173 current_settings = { 174 k: v for k, v in current_settings.items() if v is not None 175 } 176 with open(self.settings_path(), "w") as f: 177 yaml.dump(current_settings, f) 178 self._settings = current_settings