Source code for contextgem.public.utils

#
# ContextGem
#
# Copyright 2025 Shcherbak AI AS. All rights reserved. Developed by Sergii Shcherbak.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

"""
Module defining public utility functions and classes of the framework.
"""

from __future__ import annotations

import base64
import io
from pathlib import Path
from typing import TYPE_CHECKING, BinaryIO

from PIL import Image as PILImage

from contextgem.internal.base.utils import _JsonObjectClassStruct
from contextgem.internal.loggers import _configure_logger_from_env


if TYPE_CHECKING:
    from contextgem.public.images import Image


[docs] def image_to_base64(source: str | Path | BinaryIO | bytes) -> str: """ Converts an image to its Base64 encoded string representation. Helper function that can be used when constructing ``Image`` objects. :param source: The image source - can be a file path (str or Path), file-like object (BytesIO, file handle, etc.), or raw bytes data. :type source: str | Path | BinaryIO | bytes :return: A Base64 encoded string representation of the image. :rtype: str :raises FileNotFoundError: If the image file path does not exist. :raises OSError: If the image cannot be read. Example: >>> from pathlib import Path >>> import io >>> >>> # From file path >>> base64_str = image_to_base64("path/to/image.jpg") >>> >>> # From file handle >>> with open("image.png", "rb") as f: ... base64_str = image_to_base64(f) >>> >>> # From bytes data >>> with open("image.webp", "rb") as f: ... image_bytes = f.read() >>> base64_str = image_to_base64(image_bytes) >>> >>> # From BytesIO >>> buffer = io.BytesIO(image_bytes) >>> base64_str = image_to_base64(buffer) """ if isinstance(source, str | Path): # File path image_path = Path(source) if not image_path.exists(): raise FileNotFoundError(f"Image file not found: {image_path}") try: with open(image_path, "rb") as image_file: return base64.b64encode(image_file.read()).decode("utf-8") except Exception as e: raise OSError(f"Cannot read image file {image_path}: {e}") from e elif isinstance(source, bytes): # Raw bytes data return base64.b64encode(source).decode("utf-8") else: # File-like object (BinaryIO, BytesIO, file handles, etc.) try: return base64.b64encode(source.read()).decode("utf-8") except Exception as e: raise OSError(f"Cannot read from file-like object: {e}") from e
[docs] def create_image(source: str | Path | PILImage.Image | BinaryIO | bytes) -> Image: """ Creates an Image instance from various image sources. This function automatically determines the MIME type and converts the image to base64 format using Pillow functionality. It supports common image formats including JPEG, PNG, and WebP. :param source: The image source - can be a file path (str or Path), PIL Image object, file-like object (BytesIO, file handle, etc.), or raw bytes data. :type source: str | Path | PILImage.Image | BinaryIO | bytes :return: An Image instance with the appropriate MIME type and base64 data. :rtype: Image :raises ValueError: If the image format is not supported or cannot be determined. :raises FileNotFoundError: If the image file path does not exist. :raises OSError: If the image cannot be opened or processed. Example: >>> from pathlib import Path >>> from PIL import Image as PILImage >>> import io >>> >>> # From file path >>> img = create_image("path/to/image.jpg") >>> >>> # From PIL Image object >>> pil_img = PILImage.open("path/to/image.png") >>> img = create_image(pil_img) >>> >>> # From file-like object >>> with open("image.jpg", "rb") as f: ... img = create_image(f) >>> >>> # From bytes data >>> with open("image.png", "rb") as f: ... image_bytes = f.read() >>> img = create_image(image_bytes) >>> >>> # From BytesIO >>> buffer = io.BytesIO(image_bytes) >>> img = create_image(buffer) """ # Avoid circular imports from contextgem.public.images import Image # Format mapping from PIL format to MIME type format_to_mime = { "JPEG": "image/jpeg", "JPG": "image/jpg", "PNG": "image/png", "WEBP": "image/webp", } # Handle different input types and get original bytes when possible # Note: We avoid using PIL compression for non-PIL sources to prevent cross-platform # issues (different compression algorithms, library versions, platform-specific defaults) original_bytes = None if isinstance(source, PILImage.Image): # PIL Image object - we need to use PIL since we don't have original bytes pil_image = source image_format = pil_image.format if image_format is None: raise ValueError( "Cannot determine image format from PIL Image object. " "Please save the image with a specific format first." ) # For PIL images, we'll need to save to buffer to get bytes elif isinstance(source, str | Path): # File path - read original bytes directly, use PIL only for format detection image_path = Path(source) if not image_path.exists(): raise FileNotFoundError(f"Image file not found: {image_path}") try: # Read original bytes original_bytes = image_path.read_bytes() # Use PIL only to detect format pil_image = PILImage.open(io.BytesIO(original_bytes)) image_format = pil_image.format except Exception as e: raise OSError(f"Cannot open image file {image_path}: {e}") from e elif isinstance(source, bytes): # Raw bytes data - use directly original_bytes = source try: # Use PIL only to detect format pil_image = PILImage.open(io.BytesIO(original_bytes)) image_format = pil_image.format except Exception as e: raise OSError(f"Cannot open image from bytes data: {e}") from e else: # File-like object - read original bytes directly try: # Read original bytes original_bytes = source.read() # Use PIL only to detect format pil_image = PILImage.open(io.BytesIO(original_bytes)) image_format = pil_image.format except Exception as e: raise OSError(f"Cannot open image from file-like object: {e}") from e # Convert format to MIME type if image_format not in format_to_mime: raise ValueError( f"Unsupported image format: {image_format}. " f"Supported formats: {', '.join(format_to_mime.keys())}" ) mime_type = format_to_mime[image_format] # Convert to base64 - use original bytes when available to avoid recompression if original_bytes is not None: # Use original bytes directly - no compression, no cross-platform issues base64_data = image_to_base64(original_bytes) else: # For PIL Image objects - save to buffer to get bytes buffer = io.BytesIO() try: pil_image.save(buffer, format=image_format, optimize=False) buffer.seek(0) base64_data = image_to_base64(buffer) except Exception as e: raise OSError(f"Cannot convert image to base64: {e}") from e finally: buffer.close() return Image(mime_type=mime_type, base64_data=base64_data) # type: ignore[arg-type]
[docs] def reload_logger_settings(): """ Reloads logger settings from environment variables. This function should be called when environment variables related to logging have been changed after the module was imported. It re-reads the environment variables and reconfigures the logger accordingly. :return: None Example: .. literalinclude:: ../../../dev/usage_examples/docstrings/utils/reload_logger_settings.py :language: python :caption: Reload logger settings """ _configure_logger_from_env()
[docs] class JsonObjectClassStruct(_JsonObjectClassStruct): """ A base class that automatically converts class hierarchies to dictionary representations. This class enables the use of existing class hierarchies (such as dataclasses or Pydantic models) with nested type hints as a structure definition for JsonObjectConcept. When you need to use typed class hierarchies with JsonObjectConcept, inherit from this class in all parts of your class structure. Example: .. literalinclude:: ../../../dev/usage_examples/docstrings/utils/json_object_cls_struct.py :language: python :caption: Using JsonObjectClassStruct for class hierarchies """ pass