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
type
default
env_var
default_lambda
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()
@classmethod
def shared(cls):
78    @classmethod
79    def shared(cls):
80        if cls._shared_instance is None:
81            cls._shared_instance = cls()
82        return cls._shared_instance
def get_value(self, name: str) -> Any:
85    def get_value(self, name: str) -> Any:
86        try:
87            return self.__getattr__(name)
88        except AttributeError:
89            return None
@classmethod
def settings_path(cls, create=True):
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")
@classmethod
def load_settings(cls):
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
def settings(self, hide_sensitive=False):
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
def save_setting(self, name: str, value: Any):
149    def save_setting(self, name: str, value: Any):
150        self.update_settings({name: value})
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