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