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            "custom_models": ConfigProperty(
149                list,
150                default_lambda=lambda: [],
151            ),
152            "openai_compatible_providers": ConfigProperty(
153                list,
154                default_lambda=lambda: [],
155                sensitive_keys=["api_key"],
156            ),
157            "cerebras_api_key": ConfigProperty(
158                str,
159                env_var="CEREBRAS_API_KEY",
160                sensitive=True,
161            ),
162            "enable_demo_tools": ConfigProperty(
163                bool,
164                env_var="ENABLE_DEMO_TOOLS",
165                default=False,
166            ),
167            # Allow the user to set the path to lookup MCP server commands, like npx.
168            "custom_mcp_path": ConfigProperty(
169                str,
170                env_var="CUSTOM_MCP_PATH",
171            ),
172            # Allow the user to set secrets for MCP servers, the key is mcp_server_id::key_name
173            MCP_SECRETS_KEY: ConfigProperty(
174                dict[str, str],
175                sensitive=True,
176            ),
177            # has the user indicated it's for personal or work use?
178            "user_type": ConfigProperty(
179                str,  # "personal" or "work"
180            ),
181            # if the user has provided their work contact
182            "work_use_contact": ConfigProperty(
183                str,
184            ),
185        }
186        self._lock = threading.Lock()
187        self._settings = self.load_settings()
188
189    @classmethod
190    def shared(cls):
191        if cls._shared_instance is None:
192            cls._shared_instance = cls()
193        return cls._shared_instance
194
195    # Get a value, mockable for testing
196    def get_value(self, name: str) -> Any:
197        try:
198            return self.__getattr__(name)
199        except AttributeError:
200            return None
201
202    def __getattr__(self, name: str) -> Any:
203        if name == "_properties":
204            return super().__getattribute__("_properties")
205        if name not in self._properties:
206            return super().__getattribute__(name)
207
208        property_config = self._properties[name]
209
210        # Check if the value is in settings
211        if name in self._settings:
212            value = self._settings[name]
213            return value if value is None else property_config.type(value)
214
215        # Check environment variable
216        if property_config.env_var and property_config.env_var in os.environ:
217            value = os.environ[property_config.env_var]
218            return property_config.type(value)
219
220        # Use default value or default_lambda
221        if property_config.default_lambda:
222            value = property_config.default_lambda()
223        else:
224            value = property_config.default
225
226        return None if value is None else property_config.type(value)
227
228    def __setattr__(self, name, value):
229        if name in ("_properties", "_settings", "_lock"):
230            super().__setattr__(name, value)
231        elif name in self._properties:
232            self.update_settings({name: value})
233        else:
234            raise AttributeError(f"Config has no attribute '{name}'")
235
236    @classmethod
237    def settings_dir(cls, create=True) -> str:
238        settings_dir = os.path.join(Path.home(), ".kiln_ai")
239        if create and not os.path.exists(settings_dir):
240            os.makedirs(settings_dir)
241        return settings_dir
242
243    @classmethod
244    def settings_path(cls, create=True) -> str:
245        settings_dir = cls.settings_dir(create)
246        return os.path.join(settings_dir, "settings.yaml")
247
248    @classmethod
249    def load_settings(cls):
250        if not os.path.isfile(cls.settings_path(create=False)):
251            return {}
252        with open(cls.settings_path(), "r") as f:
253            settings = yaml.safe_load(f.read()) or {}
254        return settings
255
256    def settings(self, hide_sensitive=False) -> Dict[str, Any]:
257        if not hide_sensitive:
258            return self._settings
259
260        settings = {
261            k: "[hidden]"
262            if k in self._properties and self._properties[k].sensitive
263            else copy.deepcopy(v)
264            for k, v in self._settings.items()
265        }
266        # 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
267        for key, value in settings.items():
268            if key in self._properties and self._properties[key].sensitive_keys:
269                sensitive_keys = self._properties[key].sensitive_keys or []
270                for sensitive_key in sensitive_keys:
271                    if isinstance(value, list):
272                        for item in value:
273                            if sensitive_key in item:
274                                item[sensitive_key] = "[hidden]"
275
276        return settings
277
278    def save_setting(self, name: str, value: Any):
279        self.update_settings({name: value})
280
281    def update_settings(self, new_settings: Dict[str, Any]):
282        # Lock to prevent race conditions in multi-threaded scenarios
283        with self._lock:
284            # Fresh load to avoid clobbering changes from other instances
285            current_settings = self.load_settings()
286            current_settings.update(new_settings)
287            # remove None values
288            current_settings = {
289                k: v for k, v in current_settings.items() if v is not None
290            }
291            with open(self.settings_path(), "w") as f:
292                yaml.dump(current_settings, f)
293            self._settings = current_settings
294
295
296def _get_user_id():
297    try:
298        return getpass.getuser() or "unknown_user"
299    except Exception:
300        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
type
default
env_var
default_lambda
sensitive
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            "custom_models": ConfigProperty(
150                list,
151                default_lambda=lambda: [],
152            ),
153            "openai_compatible_providers": ConfigProperty(
154                list,
155                default_lambda=lambda: [],
156                sensitive_keys=["api_key"],
157            ),
158            "cerebras_api_key": ConfigProperty(
159                str,
160                env_var="CEREBRAS_API_KEY",
161                sensitive=True,
162            ),
163            "enable_demo_tools": ConfigProperty(
164                bool,
165                env_var="ENABLE_DEMO_TOOLS",
166                default=False,
167            ),
168            # Allow the user to set the path to lookup MCP server commands, like npx.
169            "custom_mcp_path": ConfigProperty(
170                str,
171                env_var="CUSTOM_MCP_PATH",
172            ),
173            # Allow the user to set secrets for MCP servers, the key is mcp_server_id::key_name
174            MCP_SECRETS_KEY: ConfigProperty(
175                dict[str, str],
176                sensitive=True,
177            ),
178            # has the user indicated it's for personal or work use?
179            "user_type": ConfigProperty(
180                str,  # "personal" or "work"
181            ),
182            # if the user has provided their work contact
183            "work_use_contact": ConfigProperty(
184                str,
185            ),
186        }
187        self._lock = threading.Lock()
188        self._settings = self.load_settings()
189
190    @classmethod
191    def shared(cls):
192        if cls._shared_instance is None:
193            cls._shared_instance = cls()
194        return cls._shared_instance
195
196    # Get a value, mockable for testing
197    def get_value(self, name: str) -> Any:
198        try:
199            return self.__getattr__(name)
200        except AttributeError:
201            return None
202
203    def __getattr__(self, name: str) -> Any:
204        if name == "_properties":
205            return super().__getattribute__("_properties")
206        if name not in self._properties:
207            return super().__getattribute__(name)
208
209        property_config = self._properties[name]
210
211        # Check if the value is in settings
212        if name in self._settings:
213            value = self._settings[name]
214            return value if value is None else property_config.type(value)
215
216        # Check environment variable
217        if property_config.env_var and property_config.env_var in os.environ:
218            value = os.environ[property_config.env_var]
219            return property_config.type(value)
220
221        # Use default value or default_lambda
222        if property_config.default_lambda:
223            value = property_config.default_lambda()
224        else:
225            value = property_config.default
226
227        return None if value is None else property_config.type(value)
228
229    def __setattr__(self, name, value):
230        if name in ("_properties", "_settings", "_lock"):
231            super().__setattr__(name, value)
232        elif name in self._properties:
233            self.update_settings({name: value})
234        else:
235            raise AttributeError(f"Config has no attribute '{name}'")
236
237    @classmethod
238    def settings_dir(cls, create=True) -> str:
239        settings_dir = os.path.join(Path.home(), ".kiln_ai")
240        if create and not os.path.exists(settings_dir):
241            os.makedirs(settings_dir)
242        return settings_dir
243
244    @classmethod
245    def settings_path(cls, create=True) -> str:
246        settings_dir = cls.settings_dir(create)
247        return os.path.join(settings_dir, "settings.yaml")
248
249    @classmethod
250    def load_settings(cls):
251        if not os.path.isfile(cls.settings_path(create=False)):
252            return {}
253        with open(cls.settings_path(), "r") as f:
254            settings = yaml.safe_load(f.read()) or {}
255        return settings
256
257    def settings(self, hide_sensitive=False) -> Dict[str, Any]:
258        if not hide_sensitive:
259            return self._settings
260
261        settings = {
262            k: "[hidden]"
263            if k in self._properties and self._properties[k].sensitive
264            else copy.deepcopy(v)
265            for k, v in self._settings.items()
266        }
267        # 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
268        for key, value in settings.items():
269            if key in self._properties and self._properties[key].sensitive_keys:
270                sensitive_keys = self._properties[key].sensitive_keys or []
271                for sensitive_key in sensitive_keys:
272                    if isinstance(value, list):
273                        for item in value:
274                            if sensitive_key in item:
275                                item[sensitive_key] = "[hidden]"
276
277        return settings
278
279    def save_setting(self, name: str, value: Any):
280        self.update_settings({name: value})
281
282    def update_settings(self, new_settings: Dict[str, Any]):
283        # Lock to prevent race conditions in multi-threaded scenarios
284        with self._lock:
285            # Fresh load to avoid clobbering changes from other instances
286            current_settings = self.load_settings()
287            current_settings.update(new_settings)
288            # remove None values
289            current_settings = {
290                k: v for k, v in current_settings.items() if v is not None
291            }
292            with open(self.settings_path(), "w") as f:
293                yaml.dump(current_settings, f)
294            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            "custom_models": ConfigProperty(
150                list,
151                default_lambda=lambda: [],
152            ),
153            "openai_compatible_providers": ConfigProperty(
154                list,
155                default_lambda=lambda: [],
156                sensitive_keys=["api_key"],
157            ),
158            "cerebras_api_key": ConfigProperty(
159                str,
160                env_var="CEREBRAS_API_KEY",
161                sensitive=True,
162            ),
163            "enable_demo_tools": ConfigProperty(
164                bool,
165                env_var="ENABLE_DEMO_TOOLS",
166                default=False,
167            ),
168            # Allow the user to set the path to lookup MCP server commands, like npx.
169            "custom_mcp_path": ConfigProperty(
170                str,
171                env_var="CUSTOM_MCP_PATH",
172            ),
173            # Allow the user to set secrets for MCP servers, the key is mcp_server_id::key_name
174            MCP_SECRETS_KEY: ConfigProperty(
175                dict[str, str],
176                sensitive=True,
177            ),
178            # has the user indicated it's for personal or work use?
179            "user_type": ConfigProperty(
180                str,  # "personal" or "work"
181            ),
182            # if the user has provided their work contact
183            "work_use_contact": ConfigProperty(
184                str,
185            ),
186        }
187        self._lock = threading.Lock()
188        self._settings = self.load_settings()
@classmethod
def shared(cls):
190    @classmethod
191    def shared(cls):
192        if cls._shared_instance is None:
193            cls._shared_instance = cls()
194        return cls._shared_instance
def get_value(self, name: str) -> Any:
197    def get_value(self, name: str) -> Any:
198        try:
199            return self.__getattr__(name)
200        except AttributeError:
201            return None
@classmethod
def settings_dir(cls, create=True) -> str:
237    @classmethod
238    def settings_dir(cls, create=True) -> str:
239        settings_dir = os.path.join(Path.home(), ".kiln_ai")
240        if create and not os.path.exists(settings_dir):
241            os.makedirs(settings_dir)
242        return settings_dir
@classmethod
def settings_path(cls, create=True) -> str:
244    @classmethod
245    def settings_path(cls, create=True) -> str:
246        settings_dir = cls.settings_dir(create)
247        return os.path.join(settings_dir, "settings.yaml")
@classmethod
def load_settings(cls):
249    @classmethod
250    def load_settings(cls):
251        if not os.path.isfile(cls.settings_path(create=False)):
252            return {}
253        with open(cls.settings_path(), "r") as f:
254            settings = yaml.safe_load(f.read()) or {}
255        return settings
def settings(self, hide_sensitive=False) -> Dict[str, Any]:
257    def settings(self, hide_sensitive=False) -> Dict[str, Any]:
258        if not hide_sensitive:
259            return self._settings
260
261        settings = {
262            k: "[hidden]"
263            if k in self._properties and self._properties[k].sensitive
264            else copy.deepcopy(v)
265            for k, v in self._settings.items()
266        }
267        # 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
268        for key, value in settings.items():
269            if key in self._properties and self._properties[key].sensitive_keys:
270                sensitive_keys = self._properties[key].sensitive_keys or []
271                for sensitive_key in sensitive_keys:
272                    if isinstance(value, list):
273                        for item in value:
274                            if sensitive_key in item:
275                                item[sensitive_key] = "[hidden]"
276
277        return settings
def save_setting(self, name: str, value: Any):
279    def save_setting(self, name: str, value: Any):
280        self.update_settings({name: value})
def update_settings(self, new_settings: Dict[str, Any]):
282    def update_settings(self, new_settings: Dict[str, Any]):
283        # Lock to prevent race conditions in multi-threaded scenarios
284        with self._lock:
285            # Fresh load to avoid clobbering changes from other instances
286            current_settings = self.load_settings()
287            current_settings.update(new_settings)
288            # remove None values
289            current_settings = {
290                k: v for k, v in current_settings.items() if v is not None
291            }
292            with open(self.settings_path(), "w") as f:
293                yaml.dump(current_settings, f)
294            self._settings = current_settings