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)
|