Skip to content

katana_public_api_client.models_pydantic._base

katana_public_api_client.models_pydantic._base

Base class for Katana Pydantic models with attrs conversion support.

This module provides the base class that all generated Pydantic models inherit from, enabling bi-directional conversion between attrs models (used by the API transport layer) and Pydantic models (for validation, serialization, and user-facing operations).

Classes

KatanaPydanticBase

Bases: BaseModel

Base class for all generated Pydantic models.

This base class provides: - Immutable (frozen) models for data integrity - Strict validation that forbids extra fields - Bi-directional conversion with attrs models

Example
from katana_public_api_client.models import Product as AttrsProduct
from katana_public_api_client.models_pydantic import (
    Product as PydanticProduct,
)

# Convert attrs -> pydantic
attrs_product = await get_product(client, 123)
pydantic_product = PydanticProduct.from_attrs(attrs_product)

# Convert pydantic -> attrs (for API calls)
attrs_product = pydantic_product.to_attrs()
Functions
from_attrs(attrs_obj) classmethod

Convert an attrs model instance to this Pydantic model.

Handles: - UNSET sentinel -> None conversion - Nested object conversion (via registry lookup) - Enum value extraction - Field name mapping (type_ -> type)

Parameters:

  • attrs_obj (Any) –

    An instance of the corresponding attrs model.

Returns:

  • T

    A new instance of this Pydantic model.

Raises:

  • ValueError

    If attrs_obj is None or type doesn't match expected.

Source code in katana_public_api_client/models_pydantic/_base.py
@classmethod
def from_attrs(cls: type[T], attrs_obj: Any) -> T:
    """Convert an attrs model instance to this Pydantic model.

    Handles:
    - UNSET sentinel -> None conversion
    - Nested object conversion (via registry lookup)
    - Enum value extraction
    - Field name mapping (type_ -> type)

    Args:
        attrs_obj: An instance of the corresponding attrs model.

    Returns:
        A new instance of this Pydantic model.

    Raises:
        ValueError: If attrs_obj is None or type doesn't match expected.
    """
    from . import _registry

    if attrs_obj is None:
        msg = f"Cannot convert None to {cls.__name__}"
        raise ValueError(msg)

    # Extract field values from attrs object
    data: dict[str, Any] = {}

    # Get the attrs object's fields
    if hasattr(attrs_obj, "__attrs_attrs__"):
        field_names = [attr.name for attr in attrs_obj.__attrs_attrs__]
    else:
        # Fallback: use __dict__ for non-attrs objects
        field_names = list(vars(attrs_obj).keys())

    for field_name in field_names:
        value = getattr(attrs_obj, field_name)

        # Skip additional_properties field (handled separately)
        if field_name == "additional_properties":
            continue

        # Convert UNSET -> None
        if _is_unset(value):
            value = None
        elif isinstance(value, list):
            # Handle lists of nested objects
            value = [_convert_nested_value(item, _registry) for item in value]
        elif isinstance(value, dict) and field_name != "additional_properties":
            # Handle dict values (but not additional_properties)
            value = {
                k: _convert_nested_value(v, _registry) for k, v in value.items()
            }
        else:
            value = _convert_nested_value(value, _registry)

        # Map field names (type_ -> type for pydantic)
        pydantic_field_name = field_name
        if field_name.endswith("_") and not field_name.startswith("_"):
            # Remove trailing underscore for pydantic field
            pydantic_field_name = field_name[:-1]

        data[pydantic_field_name] = value

    return cls.model_validate(data)
to_attrs()

Convert this Pydantic model to the corresponding attrs model.

Handles: - None -> UNSET conversion (where appropriate based on attrs field types) - Nested object conversion (via registry lookup) - Enum reconstruction from values - Field name mapping (type -> type_)

Returns:

  • Any

    An instance of the corresponding attrs model.

Raises:

  • RuntimeError

    If no attrs model is registered for this class.

Source code in katana_public_api_client/models_pydantic/_base.py
def to_attrs(self) -> Any:
    """Convert this Pydantic model to the corresponding attrs model.

    Handles:
    - None -> UNSET conversion (where appropriate based on attrs field types)
    - Nested object conversion (via registry lookup)
    - Enum reconstruction from values
    - Field name mapping (type -> type_)

    Returns:
        An instance of the corresponding attrs model.

    Raises:
        RuntimeError: If no attrs model is registered for this class.
    """
    from . import _registry

    attrs_class = _registry.get_attrs_class(type(self))
    if attrs_class is None:
        msg = f"No attrs model registered for {type(self).__name__}"
        raise RuntimeError(msg)

    # Get UNSET sentinel
    unset = _get_unset()

    # Build kwargs for attrs constructor
    kwargs: dict[str, Any] = {}

    # Get attrs field info to know which fields accept UNSET
    attrs_fields: dict[str, Any] = {}
    if hasattr(attrs_class, "__attrs_attrs__"):
        attrs_attrs = cast(Iterable[Any], attrs_class.__attrs_attrs__)
        for attr in attrs_attrs:
            attrs_fields[attr.name] = attr

    for field_name, field_value in self.model_dump().items():
        # Map field names (type -> type_ for attrs)
        attrs_field_name = field_name
        # Check if attrs model uses trailing underscore (skip private fields)
        if not field_name.startswith("_") and f"{field_name}_" in attrs_fields:
            attrs_field_name = f"{field_name}_"

        # Convert None -> UNSET where the attrs field type includes Unset
        converted_value = field_value
        if field_value is None and attrs_field_name in attrs_fields:
            # Check if the field type includes Unset
            attr_info = attrs_fields[attrs_field_name]
            type_hint = attr_info.type if hasattr(attr_info, "type") else None
            if type_hint is not None and "Unset" in str(type_hint):
                converted_value = unset

        # Handle nested objects
        if isinstance(converted_value, dict):
            # Try to find the corresponding attrs class for nested objects
            nested_pydantic_class = _get_field_type(type(self), field_name)
            if nested_pydantic_class and issubclass(
                nested_pydantic_class, KatanaPydanticBase
            ):
                nested_attrs_class = _registry.get_attrs_class(
                    nested_pydantic_class
                )
                if nested_attrs_class and hasattr(nested_attrs_class, "from_dict"):
                    from_dict_fn = cast(
                        Callable[[dict[str, Any]], Any],
                        nested_attrs_class.from_dict,
                    )
                    converted_value = from_dict_fn(converted_value)
        elif isinstance(converted_value, list):
            # Handle lists of nested objects
            new_list = []
            for item in converted_value:
                if isinstance(item, dict):
                    # We'd need more type info to convert dicts in lists properly
                    new_list.append(item)
                else:
                    new_list.append(
                        _convert_to_attrs_value(item, _registry, attrs_fields, None)
                    )
            converted_value = new_list
        else:
            converted_value = _convert_to_attrs_value(
                converted_value, _registry, attrs_fields, attrs_field_name
            )

        kwargs[attrs_field_name] = converted_value

    return attrs_class(**kwargs)