Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/config_resolvers/settings_store.py: 78%
104 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
1from __future__ import annotations
3from collections.abc import Iterator, Mapping, MutableMapping
4from types import MappingProxyType
5from typing import Final
7from litellm.proxy.config_resolvers._descriptors import FieldSource
8from litellm.proxy.config_resolvers.settings_rules import (
9 ABSENT,
10 Absent,
11 DbRow,
12 JsonValue,
13 Resolved,
14 Section,
15 SettingValue,
16 is_resource_list,
17 resolve,
18 rule_for,
19)
22class ConfigOwnedKeyError(RuntimeError):
23 def __init__(self, section: Section, key: str, *, shadows_db_value: bool = False) -> None:
24 super().__init__(config_ownership_message(section=section, key=key, shadows_db_value=shadows_db_value))
25 self.section: Final = section
26 self.key: Final = key
27 self.shadows_db_value: Final = shadows_db_value
30def config_ownership_message(*, section: Section, key: str, shadows_db_value: bool) -> str:
31 stored: Final = (
32 " The value stored in the database for it is ignored and will never be applied." if shadows_db_value else ""
33 )
34 return (
35 f"{section}.{key} is set in the config file, so the config file owns it and it cannot be changed "
36 f"here.{stored} Edit the config file to change it, or remove it from the file to let the database own it."
37 )
40_EMPTY_VALUES: Final[Mapping[str, JsonValue]] = MappingProxyType({})
41_EMPTY_ROWS: Final[Mapping[DbRow, Mapping[str, JsonValue]]] = MappingProxyType({})
44class SettingsStore(MutableMapping[str, JsonValue]):
45 def __init__(self, section: Section) -> None:
46 self._section: Final = section
47 self._yaml_values: Mapping[str, JsonValue] = _EMPTY_VALUES
48 self._database_rows: Mapping[DbRow, Mapping[str, JsonValue]] = _EMPTY_ROWS
49 self._runtime_values: Mapping[str, JsonValue] = _EMPTY_VALUES
50 self._deleted_runtime_keys: frozenset[str] = frozenset()
52 def load_yaml(self, mapping: Mapping[str, JsonValue]) -> None:
53 self._yaml_values = MappingProxyType(
54 {key: value for key, value in mapping.items() if not is_resource_list(self._section, key)}
55 )
56 self._runtime_values = MappingProxyType(
57 {key: value for key, value in self._runtime_values.items() if is_resource_list(self._section, key)}
58 )
59 self._deleted_runtime_keys = frozenset()
61 def config_value(self, key: str) -> JsonValue:
62 return self._yaml_values.get(key)
64 def owned_by_config(self, key: str) -> bool:
65 return key in self._yaml_values
67 def rejected_writes(self, incoming: Mapping[str, JsonValue]) -> tuple[str, ...]:
68 return tuple(
69 sorted(key for key, value in incoming.items() if self.owned_by_config(key) and value != self.get(key))
70 )
72 def shadowed_db_keys(self) -> tuple[str, ...]:
73 """Keys the config file owns whose stored value differs, so the stored one never reaches a reader."""
74 return tuple(sorted(key for key in self._yaml_values if self._db_value_is_shadowed(key)))
76 def shadows_db_value(self, key: str) -> bool:
77 return self.owned_by_config(key) and self._db_value_is_shadowed(key)
79 def apply_db_row(self, row: DbRow, db_row: Mapping[str, JsonValue]) -> None:
80 previous_row: Final = self._database_rows.get(row, _EMPTY_VALUES)
81 changed: Final = frozenset(
82 key
83 for key in (*previous_row, *db_row)
84 if previous_row.get(key, ABSENT) != db_row.get(key, ABSENT) # pyright: ignore[reportUnknownArgumentType] # JsonValue vs Absent compare
85 )
86 self._database_rows = MappingProxyType({**self._database_rows, row: MappingProxyType(dict(db_row))})
87 self._clear_runtime_keys(changed)
89 def resolved(self) -> Mapping[str, JsonValue]:
90 return MappingProxyType(dict(self))
92 def apply_runtime_values(self, values: Mapping[str, JsonValue]) -> None:
93 self._runtime_values = MappingProxyType(dict(values))
94 self._deleted_runtime_keys = frozenset()
96 def source(self, key: str) -> FieldSource:
97 return self._resolution_for(key).source
99 def __getitem__(self, key: str) -> JsonValue:
100 if key in self._deleted_runtime_keys: 100 ↛ 101line 100 didn't jump to line 101 because the condition on line 100 was never true
101 raise KeyError(key)
102 if key in self._runtime_values:
103 return self._runtime_values[key]
104 resolved: Final = self._resolution_for(key)
105 if isinstance(resolved.value, Absent):
106 raise KeyError(key)
107 return resolved.value
109 def __setitem__(self, key: str, value: JsonValue) -> None:
110 if self.owned_by_config(key) and value != self.get(key): 110 ↛ 111line 110 didn't jump to line 111 because the condition on line 110 was never true
111 raise ConfigOwnedKeyError(self._section, key, shadows_db_value=self._db_value_is_shadowed(key))
112 self._runtime_values = MappingProxyType({**self._runtime_values, key: value})
113 self._deleted_runtime_keys = self._deleted_runtime_keys - frozenset((key,))
115 def __delitem__(self, key: str) -> None:
116 if key not in self:
117 raise KeyError(key)
118 if self.owned_by_config(key):
119 raise ConfigOwnedKeyError(self._section, key, shadows_db_value=self._db_value_is_shadowed(key))
120 self._runtime_values = MappingProxyType(
121 {key_: value for key_, value in self._runtime_values.items() if key_ != key}
122 )
123 self._deleted_runtime_keys = self._deleted_runtime_keys | frozenset((key,))
125 def clear(self) -> None:
126 self._deleted_runtime_keys = frozenset(key for key in self._keys() if not self.owned_by_config(key))
127 self._runtime_values = MappingProxyType(
128 {key: value for key, value in self._runtime_values.items() if self.owned_by_config(key)}
129 )
131 def __iter__(self) -> Iterator[str]:
132 return iter(
133 key
134 for key in self._keys()
135 if key not in self._deleted_runtime_keys
136 and (key in self._runtime_values or not isinstance(self._resolution_for(key).value, Absent))
137 )
139 def __len__(self) -> int:
140 return sum(1 for _ in self)
142 def __bool__(self) -> bool:
143 return any(True for _ in self)
145 def _clear_runtime_keys(self, keys: frozenset[str]) -> None:
146 stale: Final = frozenset(key for key in keys if not self.owned_by_config(key))
147 if not stale:
148 return
149 self._runtime_values = MappingProxyType(
150 {key: value for key, value in self._runtime_values.items() if key not in stale}
151 )
152 self._deleted_runtime_keys = self._deleted_runtime_keys - stale
154 def _keys(self) -> tuple[str, ...]:
155 return tuple(
156 dict.fromkeys(
157 (
158 *self._yaml_values,
159 *(key for row in self._database_rows.values() for key in row),
160 *self._runtime_values,
161 )
162 )
163 )
165 def db_value(self, key: str) -> SettingValue:
166 return self._db_value(key) if is_resource_list(self._section, key) else ABSENT
168 def _db_value(self, key: str) -> SettingValue:
169 rule: Final = rule_for(self._section, key)
170 return self._database_rows.get(rule.db_row, _EMPTY_VALUES).get(key, ABSENT)
172 def _db_value_is_shadowed(self, key: str) -> bool:
173 db_value: Final = self._db_value(key)
174 return (
175 not isinstance(db_value, Absent)
176 and db_value is not None
177 and db_value != self.get(key)
178 and db_value != self.config_value(key)
179 )
181 def _resolution_for(self, key: str) -> Resolved:
182 yaml_value: Final[SettingValue] = self._yaml_values.get(key, ABSENT)
183 return resolve(yaml_value, self._db_value(key))
186def source_for(settings: SettingsStore, key: str, default: object = None) -> FieldSource:
187 source: Final = settings.source(key)
188 if source == "unset": 188 ↛ 190line 188 didn't jump to line 190 because the condition on line 188 was always true
189 return "default" if default is not None else "unset"
190 return source