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 "projects": ConfigProperty( 71 list, 72 default_lambda=lambda: [], 73 ), 74 } 75 self._settings = self.load_settings() 76 77 @classmethod 78 def shared(cls): 79 if cls._shared_instance is None: 80 cls._shared_instance = cls() 81 return cls._shared_instance 82 83 # Get a value, mockable for testing 84 def get_value(self, name: str) -> Any: 85 try: 86 return self.__getattr__(name) 87 except AttributeError: 88 return None 89 90 def __getattr__(self, name: str) -> Any: 91 if name == "_properties": 92 return super().__getattribute__("_properties") 93 if name not in self._properties: 94 return super().__getattribute__(name) 95 96 property_config = self._properties[name] 97 98 # Check if the value is in settings 99 if name in self._settings: 100 return property_config.type(self._settings[name]) 101 102 # Check environment variable 103 if property_config.env_var and property_config.env_var in os.environ: 104 value = os.environ[property_config.env_var] 105 return property_config.type(value) 106 107 # Use default value or default_lambda 108 if property_config.default_lambda: 109 value = property_config.default_lambda() 110 else: 111 value = property_config.default 112 113 return property_config.type(value) 114 115 def __setattr__(self, name, value): 116 if name in ("_properties", "_settings"): 117 super().__setattr__(name, value) 118 elif name in self._properties: 119 self.update_settings({name: value}) 120 else: 121 raise AttributeError(f"Config has no attribute '{name}'") 122 123 @classmethod 124 def settings_path(cls, create=True): 125 settings_dir = os.path.join(Path.home(), ".kiln_ai") 126 if create and not os.path.exists(settings_dir): 127 os.makedirs(settings_dir) 128 return os.path.join(settings_dir, "settings.yaml") 129 130 @classmethod 131 def load_settings(cls): 132 if not os.path.isfile(cls.settings_path(create=False)): 133 return {} 134 with open(cls.settings_path(), "r") as f: 135 settings = yaml.safe_load(f.read()) or {} 136 return settings 137 138 def settings(self, hide_sensitive=False): 139 if hide_sensitive: 140 return { 141 k: "[hidden]" 142 if k in self._properties and self._properties[k].sensitive 143 else v 144 for k, v in self._settings.items() 145 } 146 return self._settings 147 148 def save_setting(self, name: str, value: Any): 149 self.update_settings({name: value}) 150 151 def update_settings(self, new_settings: Dict[str, Any]): 152 # Lock to prevent race conditions in multi-threaded scenarios 153 with threading.Lock(): 154 # Fresh load to avoid clobbering changes from other instances 155 current_settings = self.load_settings() 156 current_settings.update(new_settings) 157 # remove None values 158 current_settings = { 159 k: v for k, v in current_settings.items() if v is not None 160 } 161 with open(self.settings_path(), "w") as f: 162 yaml.dump(current_settings, f) 163 self._settings = current_settings 164 165 166def _get_user_id(): 167 try: 168 return getpass.getuser() or "unknown_user" 169 except Exception: 170 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 "projects": ConfigProperty( 72 list, 73 default_lambda=lambda: [], 74 ), 75 } 76 self._settings = self.load_settings() 77 78 @classmethod 79 def shared(cls): 80 if cls._shared_instance is None: 81 cls._shared_instance = cls() 82 return cls._shared_instance 83 84 # Get a value, mockable for testing 85 def get_value(self, name: str) -> Any: 86 try: 87 return self.__getattr__(name) 88 except AttributeError: 89 return None 90 91 def __getattr__(self, name: str) -> Any: 92 if name == "_properties": 93 return super().__getattribute__("_properties") 94 if name not in self._properties: 95 return super().__getattribute__(name) 96 97 property_config = self._properties[name] 98 99 # Check if the value is in settings 100 if name in self._settings: 101 return property_config.type(self._settings[name]) 102 103 # Check environment variable 104 if property_config.env_var and property_config.env_var in os.environ: 105 value = os.environ[property_config.env_var] 106 return property_config.type(value) 107 108 # Use default value or default_lambda 109 if property_config.default_lambda: 110 value = property_config.default_lambda() 111 else: 112 value = property_config.default 113 114 return property_config.type(value) 115 116 def __setattr__(self, name, value): 117 if name in ("_properties", "_settings"): 118 super().__setattr__(name, value) 119 elif name in self._properties: 120 self.update_settings({name: value}) 121 else: 122 raise AttributeError(f"Config has no attribute '{name}'") 123 124 @classmethod 125 def settings_path(cls, create=True): 126 settings_dir = os.path.join(Path.home(), ".kiln_ai") 127 if create and not os.path.exists(settings_dir): 128 os.makedirs(settings_dir) 129 return os.path.join(settings_dir, "settings.yaml") 130 131 @classmethod 132 def load_settings(cls): 133 if not os.path.isfile(cls.settings_path(create=False)): 134 return {} 135 with open(cls.settings_path(), "r") as f: 136 settings = yaml.safe_load(f.read()) or {} 137 return settings 138 139 def settings(self, hide_sensitive=False): 140 if hide_sensitive: 141 return { 142 k: "[hidden]" 143 if k in self._properties and self._properties[k].sensitive 144 else v 145 for k, v in self._settings.items() 146 } 147 return self._settings 148 149 def save_setting(self, name: str, value: Any): 150 self.update_settings({name: value}) 151 152 def update_settings(self, new_settings: Dict[str, Any]): 153 # Lock to prevent race conditions in multi-threaded scenarios 154 with threading.Lock(): 155 # Fresh load to avoid clobbering changes from other instances 156 current_settings = self.load_settings() 157 current_settings.update(new_settings) 158 # remove None values 159 current_settings = { 160 k: v for k, v in current_settings.items() if v is not None 161 } 162 with open(self.settings_path(), "w") as f: 163 yaml.dump(current_settings, f) 164 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 "projects": ConfigProperty( 72 list, 73 default_lambda=lambda: [], 74 ), 75 } 76 self._settings = self.load_settings()
def
update_settings(self, new_settings: Dict[str, Any]):
152 def update_settings(self, new_settings: Dict[str, Any]): 153 # Lock to prevent race conditions in multi-threaded scenarios 154 with threading.Lock(): 155 # Fresh load to avoid clobbering changes from other instances 156 current_settings = self.load_settings() 157 current_settings.update(new_settings) 158 # remove None values 159 current_settings = { 160 k: v for k, v in current_settings.items() if v is not None 161 } 162 with open(self.settings_path(), "w") as f: 163 yaml.dump(current_settings, f) 164 self._settings = current_settings