kiln_ai.utils.config

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