kiln_ai.utils.config
1import copy 2import getpass 3import os 4import threading 5from pathlib import Path 6from typing import Any, Callable, Dict, List, Optional 7 8import yaml 9 10# Configuration keys 11MCP_SECRETS_KEY = "mcp_secrets" 12 13 14class ConfigProperty: 15 def __init__( 16 self, 17 type_: type, 18 default: Any = None, 19 env_var: Optional[str] = None, 20 default_lambda: Optional[Callable[[], Any]] = None, 21 sensitive: bool = False, 22 sensitive_keys: Optional[List[str]] = None, 23 ): 24 self.type = type_ 25 self.default = default 26 self.env_var = env_var 27 self.default_lambda = default_lambda 28 self.sensitive = sensitive 29 self.sensitive_keys = sensitive_keys 30 31 32class Config: 33 _shared_instance = None 34 35 def __init__(self, properties: Dict[str, ConfigProperty] | None = None): 36 self._properties: Dict[str, ConfigProperty] = properties or { 37 "user_id": ConfigProperty( 38 str, 39 env_var="KILN_USER_ID", 40 default_lambda=_get_user_id, 41 ), 42 "autosave_runs": ConfigProperty( 43 bool, 44 env_var="KILN_AUTOSAVE_RUNS", 45 default=True, 46 ), 47 "open_ai_api_key": ConfigProperty( 48 str, 49 env_var="OPENAI_API_KEY", 50 sensitive=True, 51 ), 52 "groq_api_key": ConfigProperty( 53 str, 54 env_var="GROQ_API_KEY", 55 sensitive=True, 56 ), 57 "ollama_base_url": ConfigProperty( 58 str, 59 env_var="OLLAMA_BASE_URL", 60 ), 61 "docker_model_runner_base_url": ConfigProperty( 62 str, 63 env_var="DOCKER_MODEL_RUNNER_BASE_URL", 64 ), 65 "bedrock_access_key": ConfigProperty( 66 str, 67 env_var="AWS_ACCESS_KEY_ID", 68 sensitive=True, 69 ), 70 "bedrock_secret_key": ConfigProperty( 71 str, 72 env_var="AWS_SECRET_ACCESS_KEY", 73 sensitive=True, 74 ), 75 "open_router_api_key": ConfigProperty( 76 str, 77 env_var="OPENROUTER_API_KEY", 78 sensitive=True, 79 ), 80 "fireworks_api_key": ConfigProperty( 81 str, 82 env_var="FIREWORKS_API_KEY", 83 sensitive=True, 84 ), 85 "fireworks_account_id": ConfigProperty( 86 str, 87 env_var="FIREWORKS_ACCOUNT_ID", 88 ), 89 "anthropic_api_key": ConfigProperty( 90 str, 91 env_var="ANTHROPIC_API_KEY", 92 sensitive=True, 93 ), 94 "gemini_api_key": ConfigProperty( 95 str, 96 env_var="GEMINI_API_KEY", 97 sensitive=True, 98 ), 99 "projects": ConfigProperty( 100 list, 101 default_lambda=lambda: [], 102 ), 103 "azure_openai_api_key": ConfigProperty( 104 str, 105 env_var="AZURE_OPENAI_API_KEY", 106 sensitive=True, 107 ), 108 "azure_openai_endpoint": ConfigProperty( 109 str, 110 env_var="AZURE_OPENAI_ENDPOINT", 111 ), 112 "huggingface_api_key": ConfigProperty( 113 str, 114 env_var="HUGGINGFACE_API_KEY", 115 sensitive=True, 116 ), 117 "vertex_project_id": ConfigProperty( 118 str, 119 env_var="VERTEX_PROJECT_ID", 120 ), 121 "vertex_location": ConfigProperty( 122 str, 123 env_var="VERTEX_LOCATION", 124 ), 125 "together_api_key": ConfigProperty( 126 str, 127 env_var="TOGETHERAI_API_KEY", 128 sensitive=True, 129 ), 130 "wandb_api_key": ConfigProperty( 131 str, 132 env_var="WANDB_API_KEY", 133 sensitive=True, 134 ), 135 "wandb_entity": ConfigProperty( 136 str, 137 env_var="WANDB_ENTITY", 138 ), 139 "siliconflow_cn_api_key": ConfigProperty( 140 str, 141 env_var="SILICONFLOW_CN_API_KEY", 142 sensitive=True, 143 ), 144 "wandb_base_url": ConfigProperty( 145 str, 146 env_var="WANDB_BASE_URL", 147 ), 148 # Legacy custom models, replaced by user_model_registry below 149 "custom_models": ConfigProperty( 150 list, 151 default_lambda=lambda: [], 152 ), 153 "user_model_registry": ConfigProperty( 154 list, 155 default_lambda=lambda: [], 156 ), 157 "openai_compatible_providers": ConfigProperty( 158 list, 159 default_lambda=lambda: [], 160 sensitive_keys=["api_key"], 161 ), 162 "cerebras_api_key": ConfigProperty( 163 str, 164 env_var="CEREBRAS_API_KEY", 165 sensitive=True, 166 ), 167 "kiln_copilot_api_key": ConfigProperty( 168 str, 169 env_var="KILN_COPILOT_API_KEY", 170 sensitive=True, 171 ), 172 "enable_demo_tools": ConfigProperty( 173 bool, 174 env_var="ENABLE_DEMO_TOOLS", 175 default=False, 176 ), 177 # Allow the user to set the path to lookup MCP server commands, like npx. 178 "custom_mcp_path": ConfigProperty( 179 str, 180 env_var="CUSTOM_MCP_PATH", 181 ), 182 # Allow the user to set secrets for MCP servers, the key is mcp_server_id::key_name 183 MCP_SECRETS_KEY: ConfigProperty( 184 dict[str, str], 185 sensitive=True, 186 ), 187 # has the user indicated it's for personal or work use? 188 "user_type": ConfigProperty( 189 str, # "personal" or "work" 190 ), 191 # if the user has provided their work contact 192 "work_use_contact": ConfigProperty( 193 str, 194 ), 195 # if the user has provided their personal contact 196 "personal_use_contact": ConfigProperty( 197 str, 198 ), 199 } 200 self._lock = threading.Lock() 201 self._settings = self.load_settings() 202 203 @classmethod 204 def shared(cls): 205 if cls._shared_instance is None: 206 cls._shared_instance = cls() 207 return cls._shared_instance 208 209 # Get a value, mockable for testing 210 def get_value(self, name: str) -> Any: 211 try: 212 return self.__getattr__(name) 213 except AttributeError: 214 return None 215 216 def __getattr__(self, name: str) -> Any: 217 if name == "_properties": 218 return super().__getattribute__("_properties") 219 if name not in self._properties: 220 return super().__getattribute__(name) 221 222 property_config = self._properties[name] 223 224 # Check if the value is in settings 225 if name in self._settings: 226 value = self._settings[name] 227 return value if value is None else property_config.type(value) 228 229 # Check environment variable 230 if property_config.env_var and property_config.env_var in os.environ: 231 value = os.environ[property_config.env_var] 232 return property_config.type(value) 233 234 # Use default value or default_lambda 235 if property_config.default_lambda: 236 value = property_config.default_lambda() 237 else: 238 value = property_config.default 239 240 return None if value is None else property_config.type(value) 241 242 def __setattr__(self, name, value): 243 if name in ("_properties", "_settings", "_lock"): 244 super().__setattr__(name, value) 245 elif name in self._properties: 246 self.update_settings({name: value}) 247 else: 248 raise AttributeError(f"Config has no attribute '{name}'") 249 250 @classmethod 251 def settings_dir(cls, create=True) -> str: 252 settings_dir = os.path.join(Path.home(), ".kiln_ai") 253 if create and not os.path.exists(settings_dir): 254 os.makedirs(settings_dir) 255 return settings_dir 256 257 @classmethod 258 def settings_path(cls, create=True) -> str: 259 settings_dir = cls.settings_dir(create) 260 return os.path.join(settings_dir, "settings.yaml") 261 262 @classmethod 263 def load_settings(cls): 264 if not os.path.isfile(cls.settings_path(create=False)): 265 return {} 266 with open(cls.settings_path(), "r") as f: 267 settings = yaml.safe_load(f.read()) or {} 268 return settings 269 270 def settings(self, hide_sensitive=False) -> Dict[str, Any]: 271 if not hide_sensitive: 272 return self._settings 273 274 settings = { 275 k: "[hidden]" 276 if k in self._properties and self._properties[k].sensitive 277 else copy.deepcopy(v) 278 for k, v in self._settings.items() 279 } 280 # Hide sensitive keys in lists. Could generalize this if we every have more types, but right not it's only needed for root elements of lists 281 for key, value in settings.items(): 282 if key in self._properties and self._properties[key].sensitive_keys: 283 sensitive_keys = self._properties[key].sensitive_keys or [] 284 for sensitive_key in sensitive_keys: 285 if isinstance(value, list): 286 for item in value: 287 if sensitive_key in item: 288 item[sensitive_key] = "[hidden]" 289 290 return settings 291 292 def save_setting(self, name: str, value: Any): 293 self.update_settings({name: value}) 294 295 def update_settings(self, new_settings: Dict[str, Any]): 296 # Lock to prevent race conditions in multi-threaded scenarios 297 with self._lock: 298 # Fresh load to avoid clobbering changes from other instances 299 current_settings = self.load_settings() 300 current_settings.update(new_settings) 301 # remove None values 302 current_settings = { 303 k: v for k, v in current_settings.items() if v is not None 304 } 305 with open(self.settings_path(), "w") as f: 306 yaml.dump(current_settings, f) 307 self._settings = current_settings 308 309 310def _get_user_id(): 311 try: 312 return getpass.getuser() or "unknown_user" 313 except Exception: 314 return "unknown_user"
MCP_SECRETS_KEY =
'mcp_secrets'
class
ConfigProperty:
15class ConfigProperty: 16 def __init__( 17 self, 18 type_: type, 19 default: Any = None, 20 env_var: Optional[str] = None, 21 default_lambda: Optional[Callable[[], Any]] = None, 22 sensitive: bool = False, 23 sensitive_keys: Optional[List[str]] = None, 24 ): 25 self.type = type_ 26 self.default = default 27 self.env_var = env_var 28 self.default_lambda = default_lambda 29 self.sensitive = sensitive 30 self.sensitive_keys = sensitive_keys
ConfigProperty( type_: type, default: Any = None, env_var: Optional[str] = None, default_lambda: Optional[Callable[[], Any]] = None, sensitive: bool = False, sensitive_keys: Optional[List[str]] = None)
16 def __init__( 17 self, 18 type_: type, 19 default: Any = None, 20 env_var: Optional[str] = None, 21 default_lambda: Optional[Callable[[], Any]] = None, 22 sensitive: bool = False, 23 sensitive_keys: Optional[List[str]] = None, 24 ): 25 self.type = type_ 26 self.default = default 27 self.env_var = env_var 28 self.default_lambda = default_lambda 29 self.sensitive = sensitive 30 self.sensitive_keys = sensitive_keys
class
Config:
33class Config: 34 _shared_instance = None 35 36 def __init__(self, properties: Dict[str, ConfigProperty] | None = None): 37 self._properties: Dict[str, ConfigProperty] = properties or { 38 "user_id": ConfigProperty( 39 str, 40 env_var="KILN_USER_ID", 41 default_lambda=_get_user_id, 42 ), 43 "autosave_runs": ConfigProperty( 44 bool, 45 env_var="KILN_AUTOSAVE_RUNS", 46 default=True, 47 ), 48 "open_ai_api_key": ConfigProperty( 49 str, 50 env_var="OPENAI_API_KEY", 51 sensitive=True, 52 ), 53 "groq_api_key": ConfigProperty( 54 str, 55 env_var="GROQ_API_KEY", 56 sensitive=True, 57 ), 58 "ollama_base_url": ConfigProperty( 59 str, 60 env_var="OLLAMA_BASE_URL", 61 ), 62 "docker_model_runner_base_url": ConfigProperty( 63 str, 64 env_var="DOCKER_MODEL_RUNNER_BASE_URL", 65 ), 66 "bedrock_access_key": ConfigProperty( 67 str, 68 env_var="AWS_ACCESS_KEY_ID", 69 sensitive=True, 70 ), 71 "bedrock_secret_key": ConfigProperty( 72 str, 73 env_var="AWS_SECRET_ACCESS_KEY", 74 sensitive=True, 75 ), 76 "open_router_api_key": ConfigProperty( 77 str, 78 env_var="OPENROUTER_API_KEY", 79 sensitive=True, 80 ), 81 "fireworks_api_key": ConfigProperty( 82 str, 83 env_var="FIREWORKS_API_KEY", 84 sensitive=True, 85 ), 86 "fireworks_account_id": ConfigProperty( 87 str, 88 env_var="FIREWORKS_ACCOUNT_ID", 89 ), 90 "anthropic_api_key": ConfigProperty( 91 str, 92 env_var="ANTHROPIC_API_KEY", 93 sensitive=True, 94 ), 95 "gemini_api_key": ConfigProperty( 96 str, 97 env_var="GEMINI_API_KEY", 98 sensitive=True, 99 ), 100 "projects": ConfigProperty( 101 list, 102 default_lambda=lambda: [], 103 ), 104 "azure_openai_api_key": ConfigProperty( 105 str, 106 env_var="AZURE_OPENAI_API_KEY", 107 sensitive=True, 108 ), 109 "azure_openai_endpoint": ConfigProperty( 110 str, 111 env_var="AZURE_OPENAI_ENDPOINT", 112 ), 113 "huggingface_api_key": ConfigProperty( 114 str, 115 env_var="HUGGINGFACE_API_KEY", 116 sensitive=True, 117 ), 118 "vertex_project_id": ConfigProperty( 119 str, 120 env_var="VERTEX_PROJECT_ID", 121 ), 122 "vertex_location": ConfigProperty( 123 str, 124 env_var="VERTEX_LOCATION", 125 ), 126 "together_api_key": ConfigProperty( 127 str, 128 env_var="TOGETHERAI_API_KEY", 129 sensitive=True, 130 ), 131 "wandb_api_key": ConfigProperty( 132 str, 133 env_var="WANDB_API_KEY", 134 sensitive=True, 135 ), 136 "wandb_entity": ConfigProperty( 137 str, 138 env_var="WANDB_ENTITY", 139 ), 140 "siliconflow_cn_api_key": ConfigProperty( 141 str, 142 env_var="SILICONFLOW_CN_API_KEY", 143 sensitive=True, 144 ), 145 "wandb_base_url": ConfigProperty( 146 str, 147 env_var="WANDB_BASE_URL", 148 ), 149 # Legacy custom models, replaced by user_model_registry below 150 "custom_models": ConfigProperty( 151 list, 152 default_lambda=lambda: [], 153 ), 154 "user_model_registry": ConfigProperty( 155 list, 156 default_lambda=lambda: [], 157 ), 158 "openai_compatible_providers": ConfigProperty( 159 list, 160 default_lambda=lambda: [], 161 sensitive_keys=["api_key"], 162 ), 163 "cerebras_api_key": ConfigProperty( 164 str, 165 env_var="CEREBRAS_API_KEY", 166 sensitive=True, 167 ), 168 "kiln_copilot_api_key": ConfigProperty( 169 str, 170 env_var="KILN_COPILOT_API_KEY", 171 sensitive=True, 172 ), 173 "enable_demo_tools": ConfigProperty( 174 bool, 175 env_var="ENABLE_DEMO_TOOLS", 176 default=False, 177 ), 178 # Allow the user to set the path to lookup MCP server commands, like npx. 179 "custom_mcp_path": ConfigProperty( 180 str, 181 env_var="CUSTOM_MCP_PATH", 182 ), 183 # Allow the user to set secrets for MCP servers, the key is mcp_server_id::key_name 184 MCP_SECRETS_KEY: ConfigProperty( 185 dict[str, str], 186 sensitive=True, 187 ), 188 # has the user indicated it's for personal or work use? 189 "user_type": ConfigProperty( 190 str, # "personal" or "work" 191 ), 192 # if the user has provided their work contact 193 "work_use_contact": ConfigProperty( 194 str, 195 ), 196 # if the user has provided their personal contact 197 "personal_use_contact": ConfigProperty( 198 str, 199 ), 200 } 201 self._lock = threading.Lock() 202 self._settings = self.load_settings() 203 204 @classmethod 205 def shared(cls): 206 if cls._shared_instance is None: 207 cls._shared_instance = cls() 208 return cls._shared_instance 209 210 # Get a value, mockable for testing 211 def get_value(self, name: str) -> Any: 212 try: 213 return self.__getattr__(name) 214 except AttributeError: 215 return None 216 217 def __getattr__(self, name: str) -> Any: 218 if name == "_properties": 219 return super().__getattribute__("_properties") 220 if name not in self._properties: 221 return super().__getattribute__(name) 222 223 property_config = self._properties[name] 224 225 # Check if the value is in settings 226 if name in self._settings: 227 value = self._settings[name] 228 return value if value is None else property_config.type(value) 229 230 # Check environment variable 231 if property_config.env_var and property_config.env_var in os.environ: 232 value = os.environ[property_config.env_var] 233 return property_config.type(value) 234 235 # Use default value or default_lambda 236 if property_config.default_lambda: 237 value = property_config.default_lambda() 238 else: 239 value = property_config.default 240 241 return None if value is None else property_config.type(value) 242 243 def __setattr__(self, name, value): 244 if name in ("_properties", "_settings", "_lock"): 245 super().__setattr__(name, value) 246 elif name in self._properties: 247 self.update_settings({name: value}) 248 else: 249 raise AttributeError(f"Config has no attribute '{name}'") 250 251 @classmethod 252 def settings_dir(cls, create=True) -> str: 253 settings_dir = os.path.join(Path.home(), ".kiln_ai") 254 if create and not os.path.exists(settings_dir): 255 os.makedirs(settings_dir) 256 return settings_dir 257 258 @classmethod 259 def settings_path(cls, create=True) -> str: 260 settings_dir = cls.settings_dir(create) 261 return os.path.join(settings_dir, "settings.yaml") 262 263 @classmethod 264 def load_settings(cls): 265 if not os.path.isfile(cls.settings_path(create=False)): 266 return {} 267 with open(cls.settings_path(), "r") as f: 268 settings = yaml.safe_load(f.read()) or {} 269 return settings 270 271 def settings(self, hide_sensitive=False) -> Dict[str, Any]: 272 if not hide_sensitive: 273 return self._settings 274 275 settings = { 276 k: "[hidden]" 277 if k in self._properties and self._properties[k].sensitive 278 else copy.deepcopy(v) 279 for k, v in self._settings.items() 280 } 281 # Hide sensitive keys in lists. Could generalize this if we every have more types, but right not it's only needed for root elements of lists 282 for key, value in settings.items(): 283 if key in self._properties and self._properties[key].sensitive_keys: 284 sensitive_keys = self._properties[key].sensitive_keys or [] 285 for sensitive_key in sensitive_keys: 286 if isinstance(value, list): 287 for item in value: 288 if sensitive_key in item: 289 item[sensitive_key] = "[hidden]" 290 291 return settings 292 293 def save_setting(self, name: str, value: Any): 294 self.update_settings({name: value}) 295 296 def update_settings(self, new_settings: Dict[str, Any]): 297 # Lock to prevent race conditions in multi-threaded scenarios 298 with self._lock: 299 # Fresh load to avoid clobbering changes from other instances 300 current_settings = self.load_settings() 301 current_settings.update(new_settings) 302 # remove None values 303 current_settings = { 304 k: v for k, v in current_settings.items() if v is not None 305 } 306 with open(self.settings_path(), "w") as f: 307 yaml.dump(current_settings, f) 308 self._settings = current_settings
Config( properties: Optional[Dict[str, ConfigProperty]] = None)
36 def __init__(self, properties: Dict[str, ConfigProperty] | None = None): 37 self._properties: Dict[str, ConfigProperty] = properties or { 38 "user_id": ConfigProperty( 39 str, 40 env_var="KILN_USER_ID", 41 default_lambda=_get_user_id, 42 ), 43 "autosave_runs": ConfigProperty( 44 bool, 45 env_var="KILN_AUTOSAVE_RUNS", 46 default=True, 47 ), 48 "open_ai_api_key": ConfigProperty( 49 str, 50 env_var="OPENAI_API_KEY", 51 sensitive=True, 52 ), 53 "groq_api_key": ConfigProperty( 54 str, 55 env_var="GROQ_API_KEY", 56 sensitive=True, 57 ), 58 "ollama_base_url": ConfigProperty( 59 str, 60 env_var="OLLAMA_BASE_URL", 61 ), 62 "docker_model_runner_base_url": ConfigProperty( 63 str, 64 env_var="DOCKER_MODEL_RUNNER_BASE_URL", 65 ), 66 "bedrock_access_key": ConfigProperty( 67 str, 68 env_var="AWS_ACCESS_KEY_ID", 69 sensitive=True, 70 ), 71 "bedrock_secret_key": ConfigProperty( 72 str, 73 env_var="AWS_SECRET_ACCESS_KEY", 74 sensitive=True, 75 ), 76 "open_router_api_key": ConfigProperty( 77 str, 78 env_var="OPENROUTER_API_KEY", 79 sensitive=True, 80 ), 81 "fireworks_api_key": ConfigProperty( 82 str, 83 env_var="FIREWORKS_API_KEY", 84 sensitive=True, 85 ), 86 "fireworks_account_id": ConfigProperty( 87 str, 88 env_var="FIREWORKS_ACCOUNT_ID", 89 ), 90 "anthropic_api_key": ConfigProperty( 91 str, 92 env_var="ANTHROPIC_API_KEY", 93 sensitive=True, 94 ), 95 "gemini_api_key": ConfigProperty( 96 str, 97 env_var="GEMINI_API_KEY", 98 sensitive=True, 99 ), 100 "projects": ConfigProperty( 101 list, 102 default_lambda=lambda: [], 103 ), 104 "azure_openai_api_key": ConfigProperty( 105 str, 106 env_var="AZURE_OPENAI_API_KEY", 107 sensitive=True, 108 ), 109 "azure_openai_endpoint": ConfigProperty( 110 str, 111 env_var="AZURE_OPENAI_ENDPOINT", 112 ), 113 "huggingface_api_key": ConfigProperty( 114 str, 115 env_var="HUGGINGFACE_API_KEY", 116 sensitive=True, 117 ), 118 "vertex_project_id": ConfigProperty( 119 str, 120 env_var="VERTEX_PROJECT_ID", 121 ), 122 "vertex_location": ConfigProperty( 123 str, 124 env_var="VERTEX_LOCATION", 125 ), 126 "together_api_key": ConfigProperty( 127 str, 128 env_var="TOGETHERAI_API_KEY", 129 sensitive=True, 130 ), 131 "wandb_api_key": ConfigProperty( 132 str, 133 env_var="WANDB_API_KEY", 134 sensitive=True, 135 ), 136 "wandb_entity": ConfigProperty( 137 str, 138 env_var="WANDB_ENTITY", 139 ), 140 "siliconflow_cn_api_key": ConfigProperty( 141 str, 142 env_var="SILICONFLOW_CN_API_KEY", 143 sensitive=True, 144 ), 145 "wandb_base_url": ConfigProperty( 146 str, 147 env_var="WANDB_BASE_URL", 148 ), 149 # Legacy custom models, replaced by user_model_registry below 150 "custom_models": ConfigProperty( 151 list, 152 default_lambda=lambda: [], 153 ), 154 "user_model_registry": ConfigProperty( 155 list, 156 default_lambda=lambda: [], 157 ), 158 "openai_compatible_providers": ConfigProperty( 159 list, 160 default_lambda=lambda: [], 161 sensitive_keys=["api_key"], 162 ), 163 "cerebras_api_key": ConfigProperty( 164 str, 165 env_var="CEREBRAS_API_KEY", 166 sensitive=True, 167 ), 168 "kiln_copilot_api_key": ConfigProperty( 169 str, 170 env_var="KILN_COPILOT_API_KEY", 171 sensitive=True, 172 ), 173 "enable_demo_tools": ConfigProperty( 174 bool, 175 env_var="ENABLE_DEMO_TOOLS", 176 default=False, 177 ), 178 # Allow the user to set the path to lookup MCP server commands, like npx. 179 "custom_mcp_path": ConfigProperty( 180 str, 181 env_var="CUSTOM_MCP_PATH", 182 ), 183 # Allow the user to set secrets for MCP servers, the key is mcp_server_id::key_name 184 MCP_SECRETS_KEY: ConfigProperty( 185 dict[str, str], 186 sensitive=True, 187 ), 188 # has the user indicated it's for personal or work use? 189 "user_type": ConfigProperty( 190 str, # "personal" or "work" 191 ), 192 # if the user has provided their work contact 193 "work_use_contact": ConfigProperty( 194 str, 195 ), 196 # if the user has provided their personal contact 197 "personal_use_contact": ConfigProperty( 198 str, 199 ), 200 } 201 self._lock = threading.Lock() 202 self._settings = self.load_settings()
def
settings(self, hide_sensitive=False) -> Dict[str, Any]:
271 def settings(self, hide_sensitive=False) -> Dict[str, Any]: 272 if not hide_sensitive: 273 return self._settings 274 275 settings = { 276 k: "[hidden]" 277 if k in self._properties and self._properties[k].sensitive 278 else copy.deepcopy(v) 279 for k, v in self._settings.items() 280 } 281 # Hide sensitive keys in lists. Could generalize this if we every have more types, but right not it's only needed for root elements of lists 282 for key, value in settings.items(): 283 if key in self._properties and self._properties[key].sensitive_keys: 284 sensitive_keys = self._properties[key].sensitive_keys or [] 285 for sensitive_key in sensitive_keys: 286 if isinstance(value, list): 287 for item in value: 288 if sensitive_key in item: 289 item[sensitive_key] = "[hidden]" 290 291 return settings
def
update_settings(self, new_settings: Dict[str, Any]):
296 def update_settings(self, new_settings: Dict[str, Any]): 297 # Lock to prevent race conditions in multi-threaded scenarios 298 with self._lock: 299 # Fresh load to avoid clobbering changes from other instances 300 current_settings = self.load_settings() 301 current_settings.update(new_settings) 302 # remove None values 303 current_settings = { 304 k: v for k, v in current_settings.items() if v is not None 305 } 306 with open(self.settings_path(), "w") as f: 307 yaml.dump(current_settings, f) 308 self._settings = current_settings