Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/common_utils/load_config_utils.py: 19%
149 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
1import asyncio
2import os
3import posixpath
4from collections.abc import Awaitable, Mapping
5from types import MappingProxyType
6from typing import TYPE_CHECKING, Final, Protocol
8import yaml
9from pydantic import TypeAdapter, ValidationError
11from litellm._logging import verbose_proxy_logger
12from litellm.proxy.common_utils.config_includes import resolve_includes
14if TYPE_CHECKING: 14 ↛ 15line 14 didn't jump to line 15 because the condition on line 14 was never true
15 from litellm.integrations.gcs_bucket.gcs_bucket_base import GCSBucketBase
17_BUCKET_CONFIG_ADAPTER: Final = TypeAdapter(dict[str, object])
20class BucketObjectFetcher(Protocol):
21 def __call__(self, object_key: str, /) -> Awaitable[Mapping[str, object] | None]: ... 21 ↛ exitline 21 didn't return from function '__call__' because
24class BucketObjectReader(Protocol):
25 def __call__(self, object_key: str, /) -> Awaitable[object | None]: ... 25 ↛ exitline 25 didn't return from function '__call__' because
28class SyncBucketObjectReader(Protocol):
29 def __call__(self, object_key: str, /) -> object | None: ... 29 ↛ exitline 29 didn't return from function '__call__' because
32def _parsed_config(object_key: str, file_contents: str) -> object | None:
33 try:
34 parsed: Final = yaml.safe_load(file_contents)
35 except yaml.YAMLError as e:
36 verbose_proxy_logger.error("Config object %s is not valid YAML: %s", object_key, e)
37 return None
38 return MappingProxyType({}) if parsed is None else parsed
41def s3_object_reader(bucket_name: str) -> SyncBucketObjectReader:
42 """
43 Build one reader for a whole config, so an `include` tree costs one S3 client rather than one per object.
44 """
45 try:
46 # v0 rely on boto3 for authentication - allowing boto3 to handle IAM credentials etc
47 import boto3
48 from botocore.credentials import Credentials
50 from litellm.main import bedrock_converse_chat_completion
52 credentials: Final[Credentials] = bedrock_converse_chat_completion.get_credentials()
53 s3_client: Final = boto3.client(
54 "s3",
55 aws_access_key_id=credentials.access_key,
56 aws_secret_access_key=credentials.secret_key,
57 aws_session_token=credentials.token, # Optional, if using temporary credentials
58 )
59 except ImportError as e:
60 # this is most likely if a user is not using the litellm docker container
61 verbose_proxy_logger.error("ImportError: %s", e)
62 return lambda object_key: None
63 except Exception as e:
64 verbose_proxy_logger.error("Error creating the S3 client for bucket %s: %s", bucket_name, e)
65 return lambda object_key: None
67 def read(object_key: str) -> object | None:
68 try:
69 verbose_proxy_logger.debug("Retrieving %s from S3 bucket: %s", object_key, bucket_name)
70 response: Final = s3_client.get_object(Bucket=bucket_name, Key=object_key)
71 file_contents: Final = response["Body"].read().decode("utf-8")
72 except Exception as e: # noqa: BLE001 # any boto3 error must read as a missing object
73 verbose_proxy_logger.error("Error retrieving %s from S3 bucket %s: %s", object_key, bucket_name, e)
74 return None
76 return _parsed_config(object_key, file_contents)
78 return read
81def get_file_contents_from_s3(bucket_name: str, object_key: str) -> object | None:
82 return s3_object_reader(bucket_name)(object_key)
85def gcs_config_bucket(bucket_name: str) -> "GCSBucketBase | None":
86 """
87 Build a plain GCS client for reading config objects.
89 Reading a config out of a bucket is not GCS logging, so it neither needs the enterprise license
90 that gate covers nor the batching task the logger starts and never stops.
91 """
92 try:
93 from litellm.integrations.gcs_bucket.gcs_bucket_base import GCSBucketBase
95 return GCSBucketBase(bucket_name=bucket_name)
96 except Exception as e: # noqa: BLE001 # an unbuildable client must read as an unreadable bucket
97 verbose_proxy_logger.error("Error creating the GCS client for bucket %s: %s", bucket_name, e)
98 return None
101async def get_config_file_contents_from_gcs(
102 bucket_name: str,
103 object_key: str,
104 gcs_bucket: "GCSBucketBase | None" = None,
105) -> object | None:
106 try:
107 bucket: Final = gcs_config_bucket(bucket_name) if gcs_bucket is None else gcs_bucket
108 if bucket is None:
109 return None
110 file_contents: Final = await bucket.download_gcs_object(object_key)
111 if file_contents is None:
112 raise Exception(f"File contents are None for {object_key}")
113 decoded: Final = file_contents.decode("utf-8")
115 except Exception as e:
116 verbose_proxy_logger.error("Error retrieving %s from GCS bucket %s: %s", object_key, bucket_name, e)
117 return None
119 return _parsed_config(object_key, decoded)
122def resolve_include_object_key(config_object_key: str, include_entry: str) -> str:
123 """
124 Resolve one `include` entry to the object key it names, relative to the config object's prefix.
126 A leading "/" means the bucket root, mirroring how an absolute path on disk ignores the
127 directory the including config sits in.
128 """
129 if include_entry.startswith("/"):
130 return posixpath.normpath(include_entry).lstrip("/")
131 return posixpath.normpath(posixpath.join(posixpath.dirname(config_object_key), include_entry))
134async def resolve_bucket_includes(
135 *,
136 config: Mapping[str, object],
137 object_key: str,
138 fetch: BucketObjectFetcher,
139) -> dict[str, object]:
140 async def read(include_key: str) -> Mapping[str, object]:
141 included: Final = await fetch(include_key)
142 if included is None:
143 raise FileNotFoundError(
144 f"Included config could not be read from bucket: {include_key}. "
145 "The underlying bucket error is logged above."
146 )
147 return included
149 def resolve(include_entry: str, declared_in: str) -> str:
150 return resolve_include_object_key(declared_in, include_entry)
152 return await resolve_includes(config=config, location=object_key, resolve=resolve, read=read)
155async def bucket_object_reader(bucket_type: str | None, bucket_name: str) -> BucketObjectReader:
156 """
157 Build one reader for a whole config, so an `include` tree costs one bucket client rather than one per object.
158 """
159 if bucket_type != "gcs":
160 read_object: Final = await asyncio.to_thread(s3_object_reader, bucket_name)
162 async def read_from_s3(object_key: str) -> object | None:
163 return await asyncio.to_thread(read_object, object_key)
165 return read_from_s3
167 gcs_bucket: Final = gcs_config_bucket(bucket_name)
169 async def read_from_gcs(object_key: str) -> object | None:
170 if gcs_bucket is None:
171 return None
172 return await get_config_file_contents_from_gcs(bucket_name, object_key, gcs_bucket)
174 return read_from_gcs
177async def get_config_from_bucket(
178 *,
179 bucket_type: str | None,
180 bucket_name: str,
181 object_key: str,
182) -> dict[str, object] | None:
183 read: Final = await bucket_object_reader(bucket_type, bucket_name)
185 async def fetch(key: str) -> Mapping[str, object] | None:
186 raw: Final = await read(key)
187 if raw is None:
188 return None
189 try:
190 return _BUCKET_CONFIG_ADAPTER.validate_python(raw)
191 except ValidationError as e:
192 raise ValueError(f"Config object in bucket is not a YAML mapping: {key}") from e
194 config: Final = await fetch(object_key)
195 if not config:
196 return None
198 return await resolve_bucket_includes(config=config, object_key=object_key, fetch=fetch)
201def download_python_file_from_s3(
202 bucket_name: str,
203 object_key: str,
204 local_file_path: str,
205) -> bool:
206 """
207 Download a Python file from S3 and save it to local filesystem.
209 Args:
210 bucket_name (str): S3 bucket name
211 object_key (str): S3 object key (file path in bucket)
212 local_file_path (str): Local path where file should be saved
214 Returns:
215 bool: True if successful, False otherwise
216 """
217 try:
218 import boto3
219 from botocore.credentials import Credentials
221 from litellm.llms.bedrock.base_aws_llm import BaseAWSLLM
223 base_aws_llm: Final = BaseAWSLLM()
225 credentials: Final[Credentials] = base_aws_llm.get_credentials()
226 s3_client: Final = boto3.client(
227 "s3",
228 aws_access_key_id=credentials.access_key,
229 aws_secret_access_key=credentials.secret_key,
230 aws_session_token=credentials.token,
231 )
233 verbose_proxy_logger.debug("Downloading Python file %s from S3 bucket: %s", object_key, bucket_name)
234 response: Final = s3_client.get_object(Bucket=bucket_name, Key=object_key)
236 # Read the file contents
237 file_contents: Final = response["Body"].read().decode("utf-8")
238 verbose_proxy_logger.debug("File contents: %s", file_contents)
240 # Ensure directory exists
241 os.makedirs(os.path.dirname(local_file_path), exist_ok=True)
243 # Write to local file
244 with open(local_file_path, "w") as f:
245 f.write(file_contents)
247 verbose_proxy_logger.debug("Python file downloaded successfully to %s", local_file_path)
248 return True
250 except ImportError as e:
251 verbose_proxy_logger.error("ImportError: %s", e)
252 return False
253 except Exception as e:
254 verbose_proxy_logger.exception("Error downloading Python file: %s", e)
255 return False
258async def download_python_file_from_gcs(
259 bucket_name: str,
260 object_key: str,
261 local_file_path: str,
262) -> bool:
263 """
264 Download a Python file from GCS and save it to local filesystem.
266 Args:
267 bucket_name (str): GCS bucket name
268 object_key (str): GCS object key (file path in bucket)
269 local_file_path (str): Local path where file should be saved
271 Returns:
272 bool: True if successful, False otherwise
273 """
274 try:
275 from litellm.integrations.gcs_bucket.gcs_bucket_base import GCSBucketBase
277 gcs_bucket: Final = GCSBucketBase(bucket_name=bucket_name)
278 file_contents = await gcs_bucket.download_gcs_object(object_key)
279 if file_contents is None:
280 raise Exception(f"File contents are None for {object_key}")
282 # file_contents is a bytes object, decode it
283 file_contents = file_contents.decode("utf-8")
285 # Ensure directory exists
286 os.makedirs(os.path.dirname(local_file_path), exist_ok=True)
288 # Write to local file
289 with open(local_file_path, "w") as f:
290 f.write(file_contents)
292 verbose_proxy_logger.debug("Python file downloaded successfully to %s", local_file_path)
293 return True
295 except Exception as e:
296 verbose_proxy_logger.exception("Error downloading Python file from GCS: %s", e)
297 return False
300# # Example usage
301# bucket_name = 'litellm-proxy'
302# object_key = 'litellm_proxy_config.yaml'