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: SQLModel

Base class for all generated Pydantic models.

Extends SQLModel (not plain pydantic.BaseModel) so that subclasses can opt into SQLAlchemy table semantics via table=True without forking the generated model hierarchy. SQLModel is itself a pydantic BaseModel subclass, so existing consumers continue working unchanged.

This base class provides: - Immutable (frozen) models for data integrity - Strict validation that forbids extra fields (request models) - BaseEntity overrides with extra="ignore" for API response tolerance - 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):
            name = (
                field_name[:-1]
                if field_name.endswith("_") and not field_name.startswith("_")
                else field_name
            )
            field_info = cls.model_fields.get(name)
            if field_info is not None and not field_info.is_required():
                continue
            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":
            # Normalize an empty dict to None. Katana sometimes returns {}
            # instead of null for absent optional nested objects (e.g.
            # shipping_fee). An empty mapping cannot satisfy any schema
            # that has required fields, so treat it the same as null.
            if not value:
                value = None
            else:
                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]

        # Normalize Katana's literal ``"null"``/``"undefined"`` sentinel to
        # None for typed (non-string) fields so a single quirky record
        # doesn't fail ``model_validate`` below (and, in the typed-cache
        # sync, abort the whole batch).
        model_field = cls.model_fields.get(pydantic_field_name)
        if model_field is not None:
            value = _coerce_sentinel_null(value, model_field.annotation)

        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)

    # The generated parser knows each field's actual wire shape, including
    # free-form object wrappers, unions and RootModel-backed legacy arrays.
    # Reconstruct through it instead of guessing nested types from generic
    # annotations or passing model_dump dictionaries to attrs constructors.
    data = self.model_dump(mode="json", by_alias=True, exclude_unset=True)
    attrs_fields = {
        attr.name: attr for attr in getattr(attrs_class, "__attrs_attrs__", ())
    }
    for field_name, field_info in type(self).model_fields.items():
        wire_name = field_info.serialization_alias or field_info.alias or field_name
        if wire_name in data and data[wire_name] is not None:
            data[wire_name] = _nested_wire_value(
                getattr(self, field_name), data[wire_name]
            )
        attrs_name = (
            f"{field_name}_" if f"{field_name}_" in attrs_fields else field_name
        )
        attr = attrs_fields.get(attrs_name)
        if data.get(wire_name) is not None or attr is None:
            continue
        annotation = str(attr.type)
        if "Unset" in annotation and (
            field_name not in self.model_fields_set or "None" not in annotation
        ):
            data.pop(wire_name, None)
    if not hasattr(attrs_class, "from_dict"):
        raise TypeError(
            f"Registered attrs model {attrs_class.__name__} has no wire parser"
        )
    from_dict = cast(Callable[[dict[str, Any]], Any], attrs_class.from_dict)
    return from_dict(data)