Skip to content

katana_public_api_client.models_pydantic

katana_public_api_client.models_pydantic

Pydantic v2 models for Katana API.

This module provides Pydantic v2 models that mirror the attrs models in the main models/ package. These models offer:

  • Strong validation: Pydantic's validation ensures data integrity
  • Immutability: All models are frozen to prevent accidental modification
  • Serialization: Easy conversion to/from JSON and dictionaries
  • IDE support: Full type hints and autocomplete

Usage

Converting from API Responses
from katana_public_api_client import KatanaClient
from katana_public_api_client.api.product import get_all_products
from katana_public_api_client.models_pydantic import Product
from katana_public_api_client.models_pydantic.converters import (
    convert_response,
)

async with KatanaClient() as client:
    response = await get_all_products.asyncio_detailed(client=client)
    products: list[Product] = convert_response(response, Product)

    for product in products:
        print(f"{product.name}: {product.id}")
Converting Individual Objects
from katana_public_api_client.models_pydantic import Product
from katana_public_api_client.models_pydantic.converters import (
    to_pydantic,
    to_attrs,
)

# Convert attrs -> pydantic
pydantic_product = Product.from_attrs(attrs_product)

# Or use the convenience function
pydantic_product = to_pydantic(attrs_product)

# Convert back to attrs for API calls
attrs_product = pydantic_product.to_attrs()

Model Layers

This package contains three distinct model layers:

  1. models/ (attrs): Auto-generated from OpenAPI, used by API transport
  2. models_pydantic/ (pydantic): Auto-generated from OpenAPI, user-facing
  3. domain/ (hand-written): Custom business logic models (e.g., ItemSearchResult)

The Pydantic models here are designed for user-facing operations while the attrs models handle API communication internally.

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)

Functions

get_attrs_class(pydantic_class)

Get the attrs class for a given Pydantic class.

Parameters:

Returns:

  • type | None

    The corresponding attrs model class, or None if not registered.

Source code in katana_public_api_client/models_pydantic/_registry.py
def get_attrs_class(pydantic_class: type[KatanaPydanticBase]) -> type | None:
    """Get the attrs class for a given Pydantic class.

    Args:
        pydantic_class: A Pydantic model class.

    Returns:
        The corresponding attrs model class, or None if not registered.
    """
    return _pydantic_to_attrs.get(pydantic_class)

get_attrs_class_by_name(name)

Get an attrs class by its name.

Parameters:

  • name (str) –

    The class name to look up.

Returns:

  • type | None

    The attrs model class, or None if not found.

Source code in katana_public_api_client/models_pydantic/_registry.py
def get_attrs_class_by_name(name: str) -> type | None:
    """Get an attrs class by its name.

    Args:
        name: The class name to look up.

    Returns:
        The attrs model class, or None if not found.
    """
    return _attrs_name_to_class.get(name)

get_pydantic_class(attrs_class)

Get the Pydantic class for a given attrs class.

Parameters:

  • attrs_class (type) –

    An attrs model class.

Returns:

Source code in katana_public_api_client/models_pydantic/_registry.py
def get_pydantic_class(attrs_class: type) -> type[KatanaPydanticBase] | None:
    """Get the Pydantic class for a given attrs class.

    Args:
        attrs_class: An attrs model class.

    Returns:
        The corresponding Pydantic model class, or None if not registered.
    """
    return _attrs_to_pydantic.get(attrs_class)

get_pydantic_class_by_name(name)

Get a Pydantic class by its name.

Parameters:

  • name (str) –

    The class name to look up.

Returns:

Source code in katana_public_api_client/models_pydantic/_registry.py
def get_pydantic_class_by_name(name: str) -> type[KatanaPydanticBase] | None:
    """Get a Pydantic class by its name.

    Args:
        name: The class name to look up.

    Returns:
        The Pydantic model class, or None if not found.
    """
    return _pydantic_name_to_class.get(name)

get_registration_stats()

Get statistics about the current registry state.

Returns:

  • dict[str, Any]

    Dictionary with counts and other stats.

Source code in katana_public_api_client/models_pydantic/_registry.py
def get_registration_stats() -> dict[str, Any]:
    """Get statistics about the current registry state.

    Returns:
        Dictionary with counts and other stats.
    """
    return {
        "total_pairs": len(_attrs_to_pydantic),
        "attrs_classes": len(_attrs_name_to_class),
        "pydantic_classes": len(_pydantic_name_to_class),
    }

is_registered(model_class)

Check if a model class is registered (either attrs or pydantic).

Parameters:

  • model_class (type) –

    A model class to check.

Returns:

  • bool

    True if the class is registered in either direction.

Source code in katana_public_api_client/models_pydantic/_registry.py
def is_registered(model_class: type) -> bool:
    """Check if a model class is registered (either attrs or pydantic).

    Args:
        model_class: A model class to check.

    Returns:
        True if the class is registered in either direction.
    """
    return model_class in _attrs_to_pydantic or model_class in _pydantic_to_attrs

list_registered_models()

List all registered model pairs.

Returns:

  • list[tuple[str, str]]

    List of (attrs_class_name, pydantic_class_name) tuples.

Source code in katana_public_api_client/models_pydantic/_registry.py
def list_registered_models() -> list[tuple[str, str]]:
    """List all registered model pairs.

    Returns:
        List of (attrs_class_name, pydantic_class_name) tuples.
    """
    return [
        (attrs_cls.__name__, pydantic_cls.__name__)
        for attrs_cls, pydantic_cls in _attrs_to_pydantic.items()
    ]

register(attrs_class, pydantic_class)

Register a mapping between an attrs class and a Pydantic class.

This function should be called for each model pair after generation. It enables the from_attrs() and to_attrs() conversion methods to work.

Parameters:

  • attrs_class (type) –

    The attrs model class (from models/).

  • pydantic_class (type[KatanaPydanticBase]) –

    The corresponding Pydantic model class.

Raises:

  • TypeError

    If attrs_class is not an attrs class or pydantic_class is not a subclass of KatanaPydanticBase.

  • ValueError

    If the classes are already registered with different mappings.

Source code in katana_public_api_client/models_pydantic/_registry.py
def register(attrs_class: type, pydantic_class: type[KatanaPydanticBase]) -> None:
    """Register a mapping between an attrs class and a Pydantic class.

    This function should be called for each model pair after generation.
    It enables the from_attrs() and to_attrs() conversion methods to work.

    Args:
        attrs_class: The attrs model class (from models/).
        pydantic_class: The corresponding Pydantic model class.

    Raises:
        TypeError: If attrs_class is not an attrs class or pydantic_class is not
            a subclass of KatanaPydanticBase.
        ValueError: If the classes are already registered with different mappings.
    """
    # Import KatanaPydanticBase at runtime to avoid circular imports
    from ._base import KatanaPydanticBase as BaseClass

    # Validate attrs_class has attrs attributes
    if not hasattr(attrs_class, "__attrs_attrs__"):
        msg = f"{attrs_class.__name__} is not an attrs class (missing __attrs_attrs__)"
        raise TypeError(msg)

    # Validate pydantic_class is a proper Pydantic model
    if not isinstance(pydantic_class, type) or not issubclass(
        pydantic_class, BaseClass
    ):
        msg = f"{pydantic_class.__name__} is not a subclass of KatanaPydanticBase"
        raise TypeError(msg)

    # Check for conflicting registrations
    existing_pydantic = _attrs_to_pydantic.get(attrs_class)
    if existing_pydantic is not None and existing_pydantic is not pydantic_class:
        msg = (
            f"{attrs_class.__name__} is already registered to "
            f"{existing_pydantic.__name__}, cannot register to {pydantic_class.__name__}"
        )
        raise ValueError(msg)

    existing_attrs = _pydantic_to_attrs.get(pydantic_class)
    if existing_attrs is not None and existing_attrs is not attrs_class:
        msg = (
            f"{pydantic_class.__name__} is already registered to "
            f"{existing_attrs.__name__}, cannot register to {attrs_class.__name__}"
        )
        raise ValueError(msg)

    _attrs_to_pydantic[attrs_class] = pydantic_class
    _pydantic_to_attrs[pydantic_class] = attrs_class
    _attrs_name_to_class[attrs_class.__name__] = attrs_class
    _pydantic_name_to_class[pydantic_class.__name__] = pydantic_class

    # Also set the _attrs_model class variable on the pydantic class
    pydantic_class._attrs_model = attrs_class