Skip to content

katana_public_api_client.domain

katana_public_api_client.domain

Pydantic domain models for Katana entities.

This package provides clean, ergonomic Pydantic models representing business entities from the Katana Manufacturing ERP system.

Domain models are separate from the generated API request/response models and are optimized for: - ETL and data processing - Business logic - Data validation - JSON schema generation

Example
from katana_public_api_client import KatanaClient
from katana_public_api_client.domain import KatanaVariant

async with KatanaClient() as client:
    # Helpers return domain models
    variants = await client.variants.search("fox fork", limit=10)

    # Use business methods
    for v in variants:
        print(f"{v.get_display_name()}: ${v.sales_price}")

    # Easy ETL export
    csv_rows = [v.to_csv_row() for v in variants]

    # JSON schema generation
    schema = KatanaVariant.model_json_schema()

Classes

KatanaBaseModel

Bases: BaseModel

Base class for all Pydantic domain models.

Provides: - Immutability by default (frozen=True) - Automatic validation - JSON schema generation - Easy serialization for ETL - Common timestamp fields

Example
class ProductDomain(KatanaBaseModel):
    id: int
    name: str
    sku: str

    @computed_field
    @property
    def display_name(self) -> str:
        return f"{self.name} ({self.sku})"
Functions
model_dump_for_etl()

Export to ETL-friendly format.

Removes None values and uses field aliases for cleaner output.

Returns:

  • dict[str, Any]

    Dictionary with all non-None fields

Example
variant = KatanaVariant(id=123, sku="ABC-001", sales_price=99.99)
data = variant.model_dump_for_etl()
# {"id": 123, "sku": "ABC-001", "sales_price": 99.99}
Source code in katana_public_api_client/domain/base.py
def model_dump_for_etl(self) -> dict[str, Any]:
    """Export to ETL-friendly format.

    Removes None values and uses field aliases for cleaner output.

    Returns:
        Dictionary with all non-None fields

    Example:
        ```python
        variant = KatanaVariant(id=123, sku="ABC-001", sales_price=99.99)
        data = variant.model_dump_for_etl()
        # {"id": 123, "sku": "ABC-001", "sales_price": 99.99}
        ```
    """
    return self.model_dump(exclude_none=True, by_alias=True)
to_dict_with_computed()

Export including computed fields.

Unlike model_dump(), this includes @computed_field properties.

Returns:

  • dict[str, Any]

    Dictionary with all fields including computed ones

Source code in katana_public_api_client/domain/base.py
def to_dict_with_computed(self) -> dict[str, Any]:
    """Export including computed fields.

    Unlike model_dump(), this includes @computed_field properties.

    Returns:
        Dictionary with all fields including computed ones
    """
    # Pydantic v2 automatically includes computed fields in model_dump
    return self.model_dump(mode="python", exclude_none=True)
to_warehouse_json()

Export as JSON for data warehouse.

Returns:

  • str

    JSON string with all non-None fields

Example
variant = KatanaVariant(id=123, sku="ABC-001")
json_str = variant.to_warehouse_json()
# '{"id":123,"sku":"ABC-001"}'
Source code in katana_public_api_client/domain/base.py
def to_warehouse_json(self) -> str:
    """Export as JSON for data warehouse.

    Returns:
        JSON string with all non-None fields

    Example:
        ```python
        variant = KatanaVariant(id=123, sku="ABC-001")
        json_str = variant.to_warehouse_json()
        # '{"id":123,"sku":"ABC-001"}'
        ```
    """
    return self.model_dump_json(exclude_none=True, by_alias=True)

KatanaMaterial

Bases: KatanaBaseModel

Domain model for a Material.

A Material represents raw materials and components used in manufacturing, including inventory tracking, supplier information, and batch management. This is a Pydantic model optimized for: - ETL and data processing - Business logic - Data validation - JSON schema generation

This model uses composition with the auto-generated Pydantic model, exposing a curated subset of fields with business methods.

Example
material = KatanaMaterial(
    id=3201,
    name="Stainless Steel Sheet 304",
    type="material",
    uom="m²",
    category_name="Raw Materials",
    is_sellable=False,
    batch_tracked=True,
)

# Business methods available
print(material.get_display_name())  # "Stainless Steel Sheet 304"

# ETL export
csv_row = material.to_csv_row()
schema = KatanaMaterial.model_json_schema()
Functions
from_attrs(attrs_material) classmethod

Create a KatanaMaterial from an attrs Material model (API response).

This method leverages the generated Pydantic model's from_attrs() method to handle UNSET sentinel conversion, then creates the domain model.

Parameters:

  • attrs_material (Material) –

    The attrs Material model from API response.

Returns:

  • KatanaMaterial

    A new KatanaMaterial instance with business methods.

Example
from katana_public_api_client.api.material import get_material
from katana_public_api_client.utils import unwrap

response = await get_material.asyncio_detailed(client=client, id=123)
attrs_material = unwrap(response)
domain = KatanaMaterial.from_attrs(attrs_material)
Source code in katana_public_api_client/domain/material.py
@classmethod
def from_attrs(cls, attrs_material: AttrsMaterial) -> KatanaMaterial:
    """Create a KatanaMaterial from an attrs Material model (API response).

    This method leverages the generated Pydantic model's `from_attrs()` method
    to handle UNSET sentinel conversion, then creates the domain model.

    Args:
        attrs_material: The attrs Material model from API response.

    Returns:
        A new KatanaMaterial instance with business methods.

    Example:
        ```python
        from katana_public_api_client.api.material import get_material
        from katana_public_api_client.utils import unwrap

        response = await get_material.asyncio_detailed(client=client, id=123)
        attrs_material = unwrap(response)
        domain = KatanaMaterial.from_attrs(attrs_material)
        ```
    """
    from ..models_pydantic._generated.inventory import Material as GeneratedMaterial

    # Use generated model's from_attrs() to handle UNSET conversion
    generated = GeneratedMaterial.from_attrs(attrs_material)
    return cls.from_generated(generated)
from_generated(generated) classmethod

Create a KatanaMaterial from a generated Pydantic Material model.

This method extracts the curated subset of fields from the generated model.

Parameters:

  • generated (Material) –

    The auto-generated Pydantic Material model.

Returns:

  • KatanaMaterial

    A new KatanaMaterial instance with business methods.

Example
from katana_public_api_client.models_pydantic import Material

# Convert from generated pydantic model
generated = Material.from_attrs(attrs_material)
domain = KatanaMaterial.from_generated(generated)
Source code in katana_public_api_client/domain/material.py
@classmethod
def from_generated(cls, generated: GeneratedMaterial) -> KatanaMaterial:
    """Create a KatanaMaterial from a generated Pydantic Material model.

    This method extracts the curated subset of fields from the generated model.

    Args:
        generated: The auto-generated Pydantic Material model.

    Returns:
        A new KatanaMaterial instance with business methods.

    Example:
        ```python
        from katana_public_api_client.models_pydantic import Material

        # Convert from generated pydantic model
        generated = Material.from_attrs(attrs_material)
        domain = KatanaMaterial.from_generated(generated)
        ```
    """
    # Count nested collections
    variant_count = len(generated.variants) if generated.variants else 0
    config_count = len(generated.configs) if generated.configs else 0

    return cls(
        id=generated.id,
        name=generated.name,
        type="material",
        uom=generated.uom,
        category_name=generated.category_name,
        is_sellable=generated.is_sellable,
        batch_tracked=generated.batch_tracked,
        default_supplier_id=generated.default_supplier_id,
        purchase_uom=generated.purchase_uom,
        purchase_uom_conversion_rate=generated.purchase_uom_conversion_rate,
        additional_info=generated.additional_info,
        custom_field_collection_id=generated.custom_field_collection_id,
        archived_at=generated.archived_at,
        variant_count=variant_count,
        config_count=config_count,
        created_at=generated.created_at,
        updated_at=generated.updated_at,
        deleted_at=None,  # Material uses archived_at, not deleted_at
    )
get_display_name()

Get formatted display name.

Returns:

  • str

    Material name, or "Unnamed Material {id}" if no name

Example
material = KatanaMaterial(id=3201, name="Steel Sheet")
print(material.get_display_name())  # "Steel Sheet"
Source code in katana_public_api_client/domain/material.py
def get_display_name(self) -> str:
    """Get formatted display name.

    Returns:
        Material name, or "Unnamed Material {id}" if no name

    Example:
        ```python
        material = KatanaMaterial(id=3201, name="Steel Sheet")
        print(material.get_display_name())  # "Steel Sheet"
        ```
    """
    return self.name or f"Unnamed Material {self.id}"

Check if material matches search query.

Searches across: - Material name - Category name

Parameters:

  • query (str) –

    Search query string (case-insensitive)

Returns:

  • bool

    True if material matches query

Example
material = KatanaMaterial(
    id=3201, name="Stainless Steel Sheet", category_name="Raw Materials"
)
material.matches_search("steel")  # True
material.matches_search("raw")  # True
material.matches_search("aluminum")  # False
Source code in katana_public_api_client/domain/material.py
def matches_search(self, query: str) -> bool:
    """Check if material matches search query.

    Searches across:
    - Material name
    - Category name

    Args:
        query: Search query string (case-insensitive)

    Returns:
        True if material matches query

    Example:
        ```python
        material = KatanaMaterial(
            id=3201, name="Stainless Steel Sheet", category_name="Raw Materials"
        )
        material.matches_search("steel")  # True
        material.matches_search("raw")  # True
        material.matches_search("aluminum")  # False
        ```
    """
    query_lower = query.lower()

    # Check name
    if self.name and query_lower in self.name.lower():
        return True

    # Check category
    return bool(self.category_name and query_lower in self.category_name.lower())
to_csv_row()

Export as CSV-friendly row.

Returns:

  • dict[str, Any]

    Dictionary with flattened data suitable for CSV export

Example
material = KatanaMaterial(
    id=3201, name="Test Material", is_sellable=False
)
row = material.to_csv_row()
# {
#   "ID": 3201,
#   "Name": "Test Material",
#   "Type": "material",
#   "Category": "",
#   ...
# }
Source code in katana_public_api_client/domain/material.py
def to_csv_row(self) -> dict[str, Any]:
    """Export as CSV-friendly row.

    Returns:
        Dictionary with flattened data suitable for CSV export

    Example:
        ```python
        material = KatanaMaterial(
            id=3201, name="Test Material", is_sellable=False
        )
        row = material.to_csv_row()
        # {
        #   "ID": 3201,
        #   "Name": "Test Material",
        #   "Type": "material",
        #   "Category": "",
        #   ...
        # }
        ```
    """
    return {
        "ID": self.id,
        "Name": self.get_display_name(),
        "Type": self.type_,
        "Category": self.category_name or "",
        "UOM": self.uom or "",
        "Is Sellable": self.is_sellable or False,
        "Batch Tracked": self.batch_tracked or False,
        "Variant Count": self.variant_count,
        "Config Count": self.config_count,
        "Created At": self.created_at.isoformat() if self.created_at else "",
        "Updated At": self.updated_at.isoformat() if self.updated_at else "",
        "Archived At": self.archived_at.isoformat() if self.archived_at else "",
    }

KatanaProduct

Bases: KatanaBaseModel

Domain model for a Product.

A Product represents a finished good or component that can be sold, manufactured, or purchased, with support for variants and configurations. This is a Pydantic model optimized for: - ETL and data processing - Business logic - Data validation - JSON schema generation

This model uses composition with the auto-generated Pydantic model, exposing a curated subset of fields with business methods.

Example
product = KatanaProduct(
    id=1,
    name="Standard-hilt lightsaber",
    type="product",
    uom="pcs",
    category_name="lightsaber",
    is_sellable=True,
    is_producible=True,
    is_purchasable=True,
)

# Business methods available
print(product.get_display_name())  # "Standard-hilt lightsaber"

# ETL export
csv_row = product.to_csv_row()
schema = KatanaProduct.model_json_schema()
Functions
from_attrs(attrs_product) classmethod

Create a KatanaProduct from an attrs Product model (API response).

This method leverages the generated Pydantic model's from_attrs() method to handle UNSET sentinel conversion, then creates the domain model.

Parameters:

  • attrs_product (Product) –

    The attrs Product model from API response.

Returns:

  • KatanaProduct

    A new KatanaProduct instance with business methods.

Example
from katana_public_api_client.api.product import get_product
from katana_public_api_client.utils import unwrap

response = await get_product.asyncio_detailed(client=client, id=123)
attrs_product = unwrap(response)
domain = KatanaProduct.from_attrs(attrs_product)
Source code in katana_public_api_client/domain/product.py
@classmethod
def from_attrs(cls, attrs_product: AttrsProduct) -> KatanaProduct:
    """Create a KatanaProduct from an attrs Product model (API response).

    This method leverages the generated Pydantic model's `from_attrs()` method
    to handle UNSET sentinel conversion, then creates the domain model.

    Args:
        attrs_product: The attrs Product model from API response.

    Returns:
        A new KatanaProduct instance with business methods.

    Example:
        ```python
        from katana_public_api_client.api.product import get_product
        from katana_public_api_client.utils import unwrap

        response = await get_product.asyncio_detailed(client=client, id=123)
        attrs_product = unwrap(response)
        domain = KatanaProduct.from_attrs(attrs_product)
        ```
    """
    from ..models_pydantic._generated.inventory import Product as GeneratedProduct

    # Use generated model's from_attrs() to handle UNSET conversion
    generated = GeneratedProduct.from_attrs(attrs_product)
    return cls.from_generated(generated)
from_generated(generated) classmethod

Create a KatanaProduct from a generated Pydantic Product model.

This method extracts the curated subset of fields from the generated model.

Parameters:

  • generated (Product) –

    The auto-generated Pydantic Product model.

Returns:

  • KatanaProduct

    A new KatanaProduct instance with business methods.

Example
from katana_public_api_client.models_pydantic import Product

# Convert from generated pydantic model
generated = Product.from_attrs(attrs_product)
domain = KatanaProduct.from_generated(generated)
Source code in katana_public_api_client/domain/product.py
@classmethod
def from_generated(cls, generated: GeneratedProduct) -> KatanaProduct:
    """Create a KatanaProduct from a generated Pydantic Product model.

    This method extracts the curated subset of fields from the generated model.

    Args:
        generated: The auto-generated Pydantic Product model.

    Returns:
        A new KatanaProduct instance with business methods.

    Example:
        ```python
        from katana_public_api_client.models_pydantic import Product

        # Convert from generated pydantic model
        generated = Product.from_attrs(attrs_product)
        domain = KatanaProduct.from_generated(generated)
        ```
    """
    # Count nested collections
    variant_count = len(generated.variants) if generated.variants else 0
    config_count = len(generated.configs) if generated.configs else 0

    return cls(
        id=generated.id,
        name=generated.name,
        type="product",
        uom=generated.uom,
        category_name=generated.category_name,
        is_sellable=generated.is_sellable,
        is_producible=generated.is_producible,
        is_purchasable=generated.is_purchasable,
        is_auto_assembly=generated.is_auto_assembly,
        batch_tracked=generated.batch_tracked,
        serial_tracked=generated.serial_tracked,
        operations_in_sequence=generated.operations_in_sequence,
        default_supplier_id=generated.default_supplier_id,
        lead_time=generated.lead_time,
        minimum_order_quantity=generated.minimum_order_quantity,
        purchase_uom=generated.purchase_uom,
        purchase_uom_conversion_rate=generated.purchase_uom_conversion_rate,
        additional_info=generated.additional_info,
        custom_field_collection_id=generated.custom_field_collection_id,
        archived_at=generated.archived_at,
        variant_count=variant_count,
        config_count=config_count,
        created_at=generated.created_at,
        updated_at=generated.updated_at,
        deleted_at=None,  # Product uses archived_at, not deleted_at
    )
get_display_name()

Get formatted display name.

Returns:

  • str

    Product name, or "Unnamed Product {id}" if no name

Example
product = KatanaProduct(id=1, name="Kitchen Knife")
print(product.get_display_name())  # "Kitchen Knife"
Source code in katana_public_api_client/domain/product.py
def get_display_name(self) -> str:
    """Get formatted display name.

    Returns:
        Product name, or "Unnamed Product {id}" if no name

    Example:
        ```python
        product = KatanaProduct(id=1, name="Kitchen Knife")
        print(product.get_display_name())  # "Kitchen Knife"
        ```
    """
    return self.name or f"Unnamed Product {self.id}"

Check if product matches search query.

Searches across: - Product name - Category name

Parameters:

  • query (str) –

    Search query string (case-insensitive)

Returns:

  • bool

    True if product matches query

Example
product = KatanaProduct(
    id=1, name="Kitchen Knife", category_name="Cutlery"
)
product.matches_search("knife")  # True
product.matches_search("cutlery")  # True
product.matches_search("fork")  # False
Source code in katana_public_api_client/domain/product.py
def matches_search(self, query: str) -> bool:
    """Check if product matches search query.

    Searches across:
    - Product name
    - Category name

    Args:
        query: Search query string (case-insensitive)

    Returns:
        True if product matches query

    Example:
        ```python
        product = KatanaProduct(
            id=1, name="Kitchen Knife", category_name="Cutlery"
        )
        product.matches_search("knife")  # True
        product.matches_search("cutlery")  # True
        product.matches_search("fork")  # False
        ```
    """
    query_lower = query.lower()

    # Check name
    if self.name and query_lower in self.name.lower():
        return True

    # Check category
    return bool(self.category_name and query_lower in self.category_name.lower())
to_csv_row()

Export as CSV-friendly row.

Returns:

  • dict[str, Any]

    Dictionary with flattened data suitable for CSV export

Example
product = KatanaProduct(id=1, name="Test Product", is_sellable=True)
row = product.to_csv_row()
# {
#   "ID": 1,
#   "Name": "Test Product",
#   "Type": "product",
#   "Category": "",
#   ...
# }
Source code in katana_public_api_client/domain/product.py
def to_csv_row(self) -> dict[str, Any]:
    """Export as CSV-friendly row.

    Returns:
        Dictionary with flattened data suitable for CSV export

    Example:
        ```python
        product = KatanaProduct(id=1, name="Test Product", is_sellable=True)
        row = product.to_csv_row()
        # {
        #   "ID": 1,
        #   "Name": "Test Product",
        #   "Type": "product",
        #   "Category": "",
        #   ...
        # }
        ```
    """
    return {
        "ID": self.id,
        "Name": self.get_display_name(),
        "Type": self.type_,
        "Category": self.category_name or "",
        "UOM": self.uom or "",
        "Is Sellable": self.is_sellable or False,
        "Is Producible": self.is_producible or False,
        "Is Purchasable": self.is_purchasable or False,
        "Batch Tracked": self.batch_tracked or False,
        "Serial Tracked": self.serial_tracked or False,
        "Lead Time (days)": self.lead_time or 0,
        "Min Order Qty": self.minimum_order_quantity or 0,
        "Variant Count": self.variant_count,
        "Config Count": self.config_count,
        "Created At": self.created_at.isoformat() if self.created_at else "",
        "Updated At": self.updated_at.isoformat() if self.updated_at else "",
        "Archived At": self.archived_at.isoformat() if self.archived_at else "",
    }

KatanaService

Bases: KatanaBaseModel

Domain model for a Service.

A Service represents an external service that can be used as part of manufacturing operations or business processes. This is a Pydantic model optimized for: - ETL and data processing - Business logic - Data validation - JSON schema generation

This model uses composition with the auto-generated Pydantic model, exposing a curated subset of fields with business methods.

Example
service = KatanaService(
    id=1,
    name="External Assembly Service",
    type="service",
    uom="pcs",
    category_name="Assembly",
    is_sellable=True,
)

# Business methods available
print(service.get_display_name())  # "External Assembly Service"

# ETL export
csv_row = service.to_csv_row()
schema = KatanaService.model_json_schema()
Functions
from_attrs(attrs_service) classmethod

Create a KatanaService from an attrs Service model (API response).

This method leverages the generated Pydantic model's from_attrs() method to handle UNSET sentinel conversion, then creates the domain model.

Parameters:

  • attrs_service (Service) –

    The attrs Service model from API response.

Returns:

  • KatanaService

    A new KatanaService instance with business methods.

Example
from katana_public_api_client.api.service import get_service
from katana_public_api_client.utils import unwrap

response = await get_service.asyncio_detailed(client=client, id=123)
attrs_service = unwrap(response)
domain = KatanaService.from_attrs(attrs_service)
Source code in katana_public_api_client/domain/service.py
@classmethod
def from_attrs(cls, attrs_service: AttrsService) -> KatanaService:
    """Create a KatanaService from an attrs Service model (API response).

    This method leverages the generated Pydantic model's `from_attrs()` method
    to handle UNSET sentinel conversion, then creates the domain model.

    Args:
        attrs_service: The attrs Service model from API response.

    Returns:
        A new KatanaService instance with business methods.

    Example:
        ```python
        from katana_public_api_client.api.service import get_service
        from katana_public_api_client.utils import unwrap

        response = await get_service.asyncio_detailed(client=client, id=123)
        attrs_service = unwrap(response)
        domain = KatanaService.from_attrs(attrs_service)
        ```
    """
    from ..models_pydantic._generated.inventory import Service as GeneratedService

    # Use generated model's from_attrs() to handle UNSET conversion
    generated = GeneratedService.from_attrs(attrs_service)
    return cls.from_generated(generated)
from_generated(generated) classmethod

Create a KatanaService from a generated Pydantic Service model.

This method extracts the curated subset of fields from the generated model.

Parameters:

  • generated (Service) –

    The auto-generated Pydantic Service model.

Returns:

  • KatanaService

    A new KatanaService instance with business methods.

Example
from katana_public_api_client.models_pydantic import Service

# Convert from generated pydantic model
generated = Service.from_attrs(attrs_service)
domain = KatanaService.from_generated(generated)
Source code in katana_public_api_client/domain/service.py
@classmethod
def from_generated(cls, generated: GeneratedService) -> KatanaService:
    """Create a KatanaService from a generated Pydantic Service model.

    This method extracts the curated subset of fields from the generated model.

    Args:
        generated: The auto-generated Pydantic Service model.

    Returns:
        A new KatanaService instance with business methods.

    Example:
        ```python
        from katana_public_api_client.models_pydantic import Service

        # Convert from generated pydantic model
        generated = Service.from_attrs(attrs_service)
        domain = KatanaService.from_generated(generated)
        ```
    """
    # Count nested collections
    variant_count = len(generated.variants) if generated.variants else 0

    # Type is always "service" for Service entities
    return cls(
        id=generated.id,
        name=generated.name,
        type="service",  # Always "service" - required field
        uom=generated.uom,
        category_name=generated.category_name,
        is_sellable=generated.is_sellable,
        additional_info=generated.additional_info,
        custom_field_collection_id=generated.custom_field_collection_id,
        archived_at=generated.archived_at,
        variant_count=variant_count,
        created_at=generated.created_at,
        updated_at=generated.updated_at,
        deleted_at=generated.deleted_at,
    )
get_display_name()

Get formatted display name.

Returns:

  • str

    Service name, or "Unnamed Service {id}" if no name

Example
service = KatanaService(id=1, name="Assembly Service")
print(service.get_display_name())  # "Assembly Service"
Source code in katana_public_api_client/domain/service.py
def get_display_name(self) -> str:
    """Get formatted display name.

    Returns:
        Service name, or "Unnamed Service {id}" if no name

    Example:
        ```python
        service = KatanaService(id=1, name="Assembly Service")
        print(service.get_display_name())  # "Assembly Service"
        ```
    """
    return self.name or f"Unnamed Service {self.id}"

Check if service matches search query.

Searches across: - Service name - Category name

Parameters:

  • query (str) –

    Search query string (case-insensitive)

Returns:

  • bool

    True if service matches query

Example
service = KatanaService(
    id=1, name="Assembly Service", category_name="Manufacturing"
)
service.matches_search("assembly")  # True
service.matches_search("manufacturing")  # True
service.matches_search("packaging")  # False
Source code in katana_public_api_client/domain/service.py
def matches_search(self, query: str) -> bool:
    """Check if service matches search query.

    Searches across:
    - Service name
    - Category name

    Args:
        query: Search query string (case-insensitive)

    Returns:
        True if service matches query

    Example:
        ```python
        service = KatanaService(
            id=1, name="Assembly Service", category_name="Manufacturing"
        )
        service.matches_search("assembly")  # True
        service.matches_search("manufacturing")  # True
        service.matches_search("packaging")  # False
        ```
    """
    query_lower = query.lower()

    # Check name
    if self.name and query_lower in self.name.lower():
        return True

    # Check category
    return bool(self.category_name and query_lower in self.category_name.lower())
to_csv_row()

Export as CSV-friendly row.

Returns:

  • dict[str, Any]

    Dictionary with flattened data suitable for CSV export

Example
service = KatanaService(id=1, name="Test Service", is_sellable=True)
row = service.to_csv_row()
# {
#   "ID": 1,
#   "Name": "Test Service",
#   "Type": "service",
#   "Category": "",
#   ...
# }
Source code in katana_public_api_client/domain/service.py
def to_csv_row(self) -> dict[str, Any]:
    """Export as CSV-friendly row.

    Returns:
        Dictionary with flattened data suitable for CSV export

    Example:
        ```python
        service = KatanaService(id=1, name="Test Service", is_sellable=True)
        row = service.to_csv_row()
        # {
        #   "ID": 1,
        #   "Name": "Test Service",
        #   "Type": "service",
        #   "Category": "",
        #   ...
        # }
        ```
    """
    return {
        "ID": self.id,
        "Name": self.get_display_name(),
        "Type": self.type_,
        "Category": self.category_name or "",
        "UOM": self.uom or "",
        "Is Sellable": self.is_sellable or False,
        "Variant Count": self.variant_count,
        "Created At": self.created_at.isoformat() if self.created_at else "",
        "Updated At": self.updated_at.isoformat() if self.updated_at else "",
        "Archived At": self.archived_at.isoformat() if self.archived_at else "",
        "Deleted At": self.deleted_at.isoformat() if self.deleted_at else "",
    }

KatanaVariant

Bases: KatanaBaseModel

Domain model for a Product or Material Variant.

A Variant represents a specific SKU with unique pricing, configuration, and inventory tracking. This is a Pydantic model optimized for: - ETL and data processing - Business logic - Data validation - JSON schema generation

This model uses composition with the auto-generated Pydantic model, exposing a curated subset of fields with business methods.

Example
variant = KatanaVariant(
    id=123,
    sku="KNF-PRO-8PC",
    sales_price=299.99,
    purchase_price=150.00,
)

# Business methods available
print(variant.get_display_name())  # "Professional Knife Set / 8-Piece"

# ETL export
csv_row = variant.to_csv_row()
schema = KatanaVariant.model_json_schema()
Functions
from_attrs(attrs_variant, product_or_material_name=None) classmethod

Create a KatanaVariant from an attrs Variant model (API response).

This method leverages the generated Pydantic model's from_attrs() method to handle UNSET sentinel conversion, then creates the domain model.

Parameters:

  • attrs_variant (Variant) –

    The attrs Variant model from API response.

  • product_or_material_name (str | None, default: None ) –

    Optional name of parent product/material (must be provided separately as it comes from extend query).

Returns:

  • KatanaVariant

    A new KatanaVariant instance with business methods.

Example
from katana_public_api_client.api.variant import get_variant
from katana_public_api_client.utils import unwrap

response = await get_variant.asyncio_detailed(client=client, id=123)
attrs_variant = unwrap(response)
domain = KatanaVariant.from_attrs(attrs_variant)
Source code in katana_public_api_client/domain/variant.py
@classmethod
def from_attrs(
    cls,
    attrs_variant: AttrsVariant,
    product_or_material_name: str | None = None,
) -> KatanaVariant:
    """Create a KatanaVariant from an attrs Variant model (API response).

    This method leverages the generated Pydantic model's `from_attrs()` method
    to handle UNSET sentinel conversion, then creates the domain model.

    Args:
        attrs_variant: The attrs Variant model from API response.
        product_or_material_name: Optional name of parent product/material
            (must be provided separately as it comes from extend query).

    Returns:
        A new KatanaVariant instance with business methods.

    Example:
        ```python
        from katana_public_api_client.api.variant import get_variant
        from katana_public_api_client.utils import unwrap

        response = await get_variant.asyncio_detailed(client=client, id=123)
        attrs_variant = unwrap(response)
        domain = KatanaVariant.from_attrs(attrs_variant)
        ```
    """
    from ..models_pydantic._generated.inventory import Variant as GeneratedVariant

    # Use generated model's from_attrs() to handle UNSET conversion
    generated = GeneratedVariant.from_attrs(attrs_variant)

    # Extract product_or_material_name from extended data if not provided
    if product_or_material_name is None and hasattr(
        attrs_variant, "product_or_material"
    ):
        from ..client_types import UNSET

        pom = attrs_variant.product_or_material
        if pom is not UNSET and pom is not None and hasattr(pom, "name"):
            name = pom.name
            if name is not UNSET and isinstance(name, str):
                product_or_material_name = name

    return cls.from_generated(generated, product_or_material_name)
from_generated(generated, product_or_material_name=None) classmethod

Create a KatanaVariant from a generated Pydantic Variant model.

This method extracts the curated subset of fields from the generated model and converts nested objects (config_attributes, custom_fields) to simple dicts.

Parameters:

  • generated (Variant) –

    The auto-generated Pydantic Variant model.

  • product_or_material_name (str | None, default: None ) –

    Optional name of parent product/material (must be provided separately as it comes from extend query).

Returns:

  • KatanaVariant

    A new KatanaVariant instance with business methods.

Example
from katana_public_api_client.models_pydantic import Variant

# Convert from generated pydantic model
generated = Variant.from_attrs(attrs_variant)
domain = KatanaVariant.from_generated(generated)
Source code in katana_public_api_client/domain/variant.py
@classmethod
def from_generated(
    cls,
    generated: GeneratedVariant,
    product_or_material_name: str | None = None,
) -> KatanaVariant:
    """Create a KatanaVariant from a generated Pydantic Variant model.

    This method extracts the curated subset of fields from the generated model
    and converts nested objects (config_attributes, custom_fields) to simple dicts.

    Args:
        generated: The auto-generated Pydantic Variant model.
        product_or_material_name: Optional name of parent product/material
            (must be provided separately as it comes from extend query).

    Returns:
        A new KatanaVariant instance with business methods.

    Example:
        ```python
        from katana_public_api_client.models_pydantic import Variant

        # Convert from generated pydantic model
        generated = Variant.from_attrs(attrs_variant)
        domain = KatanaVariant.from_generated(generated)
        ```
    """
    # Convert config attributes to simple dicts
    config_attrs: list[dict[str, str]] = []
    if generated.config_attributes:
        for attr in generated.config_attributes:
            config_attrs.append(
                {
                    "config_name": getattr(attr, "config_name", "") or "",
                    "config_value": getattr(attr, "config_value", "") or "",
                }
            )

    # Convert custom fields to simple dicts
    custom: list[dict[str, str]] = []
    if generated.custom_fields:
        for field in generated.custom_fields:
            custom.append(
                {
                    "field_name": getattr(field, "field_name", "") or "",
                    "field_value": getattr(field, "field_value", "") or "",
                }
            )

    # Extract type value from enum if present
    # Only "product" or "material" are valid per OpenAPI spec
    type_value: Literal["product", "material"] | None = None
    if generated.type is not None:
        raw_type = (
            generated.type.value
            if hasattr(generated.type, "value")
            else generated.type
        )
        if raw_type in ("product", "material"):
            type_value = raw_type  # type: ignore[assignment]

    return cls(
        id=generated.id,
        sku=generated.sku,
        sales_price=generated.sales_price,
        purchase_price=generated.purchase_price,
        product_id=generated.product_id,
        material_id=generated.material_id,
        product_or_material_name=product_or_material_name,
        type=type_value,
        internal_barcode=generated.internal_barcode,
        registered_barcode=generated.registered_barcode,
        supplier_item_codes=generated.supplier_item_codes or [],
        lead_time=generated.lead_time,
        minimum_order_quantity=generated.minimum_order_quantity,
        config_attributes=config_attrs,
        custom_fields=custom,
        created_at=generated.created_at,
        updated_at=generated.updated_at,
        deleted_at=generated.deleted_at,
    )
get_config_value(config_name)

Get value of a configuration attribute by name.

Parameters:

  • config_name (str) –

    Name of the configuration attribute

Returns:

  • str | None

    Config value or None if not found

Example
variant = KatanaVariant(
    id=1,
    sku="TEST",
    config_attributes=[
        {"config_name": "Size", "config_value": "Large"}
    ],
)
print(variant.get_config_value("Size"))  # "Large"
print(variant.get_config_value("Color"))  # None
Source code in katana_public_api_client/domain/variant.py
def get_config_value(self, config_name: str) -> str | None:
    """Get value of a configuration attribute by name.

    Args:
        config_name: Name of the configuration attribute

    Returns:
        Config value or None if not found

    Example:
        ```python
        variant = KatanaVariant(
            id=1,
            sku="TEST",
            config_attributes=[
                {"config_name": "Size", "config_value": "Large"}
            ],
        )
        print(variant.get_config_value("Size"))  # "Large"
        print(variant.get_config_value("Color"))  # None
        ```
    """
    for attr in self.config_attributes:
        if attr.get("config_name") == config_name:
            return attr.get("config_value")
    return None
get_custom_field(field_name)

Get value of a custom field by name.

Parameters:

  • field_name (str) –

    Name of the custom field

Returns:

  • str | None

    Field value or None if not found

Example
variant = KatanaVariant(
    id=1,
    sku="TEST",
    custom_fields=[
        {"field_name": "Warranty", "field_value": "5 years"}
    ],
)
print(variant.get_custom_field("Warranty"))  # "5 years"
print(variant.get_custom_field("Missing"))  # None
Source code in katana_public_api_client/domain/variant.py
def get_custom_field(self, field_name: str) -> str | None:
    """Get value of a custom field by name.

    Args:
        field_name: Name of the custom field

    Returns:
        Field value or None if not found

    Example:
        ```python
        variant = KatanaVariant(
            id=1,
            sku="TEST",
            custom_fields=[
                {"field_name": "Warranty", "field_value": "5 years"}
            ],
        )
        print(variant.get_custom_field("Warranty"))  # "5 years"
        print(variant.get_custom_field("Missing"))  # None
        ```
    """
    for field in self.custom_fields:
        if field.get("field_name") == field_name:
            return field.get("field_value")
    return None
get_display_name()

Get formatted display name matching Katana UI format.

Format: "{Product/Material Name} / {Config Value 1} / {Config Value 2} / ..."

Returns:

  • str

    Formatted variant name, or SKU if no name available

Example
variant = KatanaVariant(
    id=1,
    sku="KNF-001",
    product_or_material_name="Kitchen Knife",
    config_attributes=[
        {"config_name": "Size", "config_value": "8-inch"},
        {"config_name": "Color", "config_value": "Black"},
    ],
)
print(variant.get_display_name())
# "Kitchen Knife / 8-inch / Black"
Source code in katana_public_api_client/domain/variant.py
def get_display_name(self) -> str:
    """Get formatted display name matching Katana UI format.

    Format: "{Product/Material Name} / {Config Value 1} / {Config Value 2} / ..."

    Returns:
        Formatted variant name, or SKU if no name available

    Example:
        ```python
        variant = KatanaVariant(
            id=1,
            sku="KNF-001",
            product_or_material_name="Kitchen Knife",
            config_attributes=[
                {"config_name": "Size", "config_value": "8-inch"},
                {"config_name": "Color", "config_value": "Black"},
            ],
        )
        print(variant.get_display_name())
        # "Kitchen Knife / 8-inch / Black"
        ```
    """
    if not self.product_or_material_name:
        return self.sku

    parts = [self.product_or_material_name]

    # Append config attribute values
    for attr in self.config_attributes:
        if value := attr.get("config_value"):
            parts.append(value)

    return " / ".join(parts)

Check if variant matches search query.

Searches across: - SKU - Product/material name - Supplier item codes - Config attribute values

Parameters:

  • query (str) –

    Search query string (case-insensitive)

Returns:

  • bool

    True if variant matches query

Example
variant = KatanaVariant(id=1, sku="FOX-FORK-160", ...)
variant.matches_search("fox")      # True
variant.matches_search("fork")     # True
variant.matches_search("160")      # True
variant.matches_search("shimano")  # False
Source code in katana_public_api_client/domain/variant.py
def matches_search(self, query: str) -> bool:
    """Check if variant matches search query.

    Searches across:
    - SKU
    - Product/material name
    - Supplier item codes
    - Config attribute values

    Args:
        query: Search query string (case-insensitive)

    Returns:
        True if variant matches query

    Example:
        ```python
        variant = KatanaVariant(id=1, sku="FOX-FORK-160", ...)
        variant.matches_search("fox")      # True
        variant.matches_search("fork")     # True
        variant.matches_search("160")      # True
        variant.matches_search("shimano")  # False
        ```
    """
    query_lower = query.lower()

    # Check SKU
    if query_lower in self.sku.lower():
        return True

    # Check product/material name
    if (
        self.product_or_material_name
        and query_lower in self.product_or_material_name.lower()
    ):
        return True

    # Check supplier codes
    if any(query_lower in code.lower() for code in self.supplier_item_codes):
        return True

    # Check config attribute values
    for attr in self.config_attributes:
        if (value := attr.get("config_value")) and query_lower in value.lower():
            return True

    return False
to_csv_row()

Export as CSV-friendly row.

Returns:

  • dict[str, Any]

    Dictionary with flattened data suitable for CSV export

Example
variant = KatanaVariant(id=1, sku="TEST", sales_price=99.99)
row = variant.to_csv_row()
# {
#   "ID": 1,
#   "SKU": "TEST",
#   "Name": "TEST",
#   "Sales Price": 99.99,
#   ...
# }
Source code in katana_public_api_client/domain/variant.py
def to_csv_row(self) -> dict[str, Any]:
    """Export as CSV-friendly row.

    Returns:
        Dictionary with flattened data suitable for CSV export

    Example:
        ```python
        variant = KatanaVariant(id=1, sku="TEST", sales_price=99.99)
        row = variant.to_csv_row()
        # {
        #   "ID": 1,
        #   "SKU": "TEST",
        #   "Name": "TEST",
        #   "Sales Price": 99.99,
        #   ...
        # }
        ```
    """

    return {
        "ID": self.id,
        "SKU": self.sku,
        "Name": self.get_display_name(),
        "Type": self.type_ or "unknown",
        "Sales Price": self.sales_price or 0.0,
        "Purchase Price": self.purchase_price or 0.0,
        "Lead Time (days)": self.lead_time or 0,
        "Min Order Qty": self.minimum_order_quantity or 0,
        "Internal Barcode": self.internal_barcode or "",
        "Registered Barcode": self.registered_barcode or "",
        "Created At": self.created_at.isoformat() if self.created_at else "",
        "Updated At": self.updated_at.isoformat() if self.updated_at else "",
    }

Functions

material_to_katana(material)

Convert attrs Material model to Pydantic KatanaMaterial.

This function delegates to KatanaMaterial.from_attrs(), which uses the auto-generated Pydantic model's from_attrs() for UNSET conversion.

Parameters:

  • material (Material) –

    attrs Material model from API response

Returns:

Example
from katana_public_api_client.api.material import get_material
from katana_public_api_client.utils import unwrap

response = await get_material.asyncio_detailed(client=client, id=123)
material_attrs = unwrap(response)
material_domain = material_to_katana(material_attrs)

# Now use domain model features
print(material_domain.get_display_name())
print(material_domain.to_csv_row())
Source code in katana_public_api_client/domain/converters.py
def material_to_katana(material: Material) -> KatanaMaterial:
    """Convert attrs Material model to Pydantic KatanaMaterial.

    This function delegates to KatanaMaterial.from_attrs(), which uses the
    auto-generated Pydantic model's from_attrs() for UNSET conversion.

    Args:
        material: attrs Material model from API response

    Returns:
        KatanaMaterial with all fields populated

    Example:
        ```python
        from katana_public_api_client.api.material import get_material
        from katana_public_api_client.utils import unwrap

        response = await get_material.asyncio_detailed(client=client, id=123)
        material_attrs = unwrap(response)
        material_domain = material_to_katana(material_attrs)

        # Now use domain model features
        print(material_domain.get_display_name())
        print(material_domain.to_csv_row())
        ```
    """
    from .material import KatanaMaterial

    return KatanaMaterial.from_attrs(material)

materials_to_katana(materials)

Convert list of attrs Material models to list of KatanaMaterial.

Parameters:

  • materials (list[Material]) –

    List of attrs Material models

Returns:

Example
from katana_public_api_client.api.material import get_all_materials
from katana_public_api_client.utils import unwrap_data

response = await get_all_materials.asyncio_detailed(client=client)
materials_attrs = unwrap_data(response)
materials_domain = materials_to_katana(materials_attrs)

# Now use domain model features
batch_tracked = [m for m in materials_domain if m.batch_tracked]
Source code in katana_public_api_client/domain/converters.py
def materials_to_katana(materials: list[Material]) -> list[KatanaMaterial]:
    """Convert list of attrs Material models to list of KatanaMaterial.

    Args:
        materials: List of attrs Material models

    Returns:
        List of KatanaMaterial models

    Example:
        ```python
        from katana_public_api_client.api.material import get_all_materials
        from katana_public_api_client.utils import unwrap_data

        response = await get_all_materials.asyncio_detailed(client=client)
        materials_attrs = unwrap_data(response)
        materials_domain = materials_to_katana(materials_attrs)

        # Now use domain model features
        batch_tracked = [m for m in materials_domain if m.batch_tracked]
        ```
    """
    return [material_to_katana(m) for m in materials]

product_to_katana(product)

Convert attrs Product model to Pydantic KatanaProduct.

This function delegates to KatanaProduct.from_attrs(), which uses the auto-generated Pydantic model's from_attrs() for UNSET conversion.

Parameters:

  • product (Product) –

    attrs Product model from API response

Returns:

Example
from katana_public_api_client.api.product import get_product
from katana_public_api_client.utils import unwrap

response = await get_product.asyncio_detailed(client=client, id=123)
product_attrs = unwrap(response)
product_domain = product_to_katana(product_attrs)

# Now use domain model features
print(product_domain.get_display_name())
print(product_domain.to_csv_row())
Source code in katana_public_api_client/domain/converters.py
def product_to_katana(product: Product) -> KatanaProduct:
    """Convert attrs Product model to Pydantic KatanaProduct.

    This function delegates to KatanaProduct.from_attrs(), which uses the
    auto-generated Pydantic model's from_attrs() for UNSET conversion.

    Args:
        product: attrs Product model from API response

    Returns:
        KatanaProduct with all fields populated

    Example:
        ```python
        from katana_public_api_client.api.product import get_product
        from katana_public_api_client.utils import unwrap

        response = await get_product.asyncio_detailed(client=client, id=123)
        product_attrs = unwrap(response)
        product_domain = product_to_katana(product_attrs)

        # Now use domain model features
        print(product_domain.get_display_name())
        print(product_domain.to_csv_row())
        ```
    """
    from .product import KatanaProduct

    return KatanaProduct.from_attrs(product)

products_to_katana(products)

Convert list of attrs Product models to list of KatanaProduct.

Parameters:

  • products (list[Product]) –

    List of attrs Product models

Returns:

Example
from katana_public_api_client.api.product import get_all_products
from katana_public_api_client.utils import unwrap_data

response = await get_all_products.asyncio_detailed(client=client)
products_attrs = unwrap_data(response)
products_domain = products_to_katana(products_attrs)

# Now use domain model features
sellable = [p for p in products_domain if p.is_sellable]
Source code in katana_public_api_client/domain/converters.py
def products_to_katana(products: list[Product]) -> list[KatanaProduct]:
    """Convert list of attrs Product models to list of KatanaProduct.

    Args:
        products: List of attrs Product models

    Returns:
        List of KatanaProduct models

    Example:
        ```python
        from katana_public_api_client.api.product import get_all_products
        from katana_public_api_client.utils import unwrap_data

        response = await get_all_products.asyncio_detailed(client=client)
        products_attrs = unwrap_data(response)
        products_domain = products_to_katana(products_attrs)

        # Now use domain model features
        sellable = [p for p in products_domain if p.is_sellable]
        ```
    """
    return [product_to_katana(p) for p in products]

service_to_katana(service)

Convert attrs Service model to Pydantic KatanaService.

This function delegates to KatanaService.from_attrs(), which uses the auto-generated Pydantic model's from_attrs() for UNSET conversion.

Parameters:

  • service (Service) –

    attrs Service model from API response

Returns:

Example
from katana_public_api_client.api.service import get_service
from katana_public_api_client.utils import unwrap

response = await get_service.asyncio_detailed(client=client, id=123)
service_attrs = unwrap(response)
service_domain = service_to_katana(service_attrs)

# Now use domain model features
print(service_domain.get_display_name())
print(service_domain.to_csv_row())
Source code in katana_public_api_client/domain/converters.py
def service_to_katana(service: Service) -> KatanaService:
    """Convert attrs Service model to Pydantic KatanaService.

    This function delegates to KatanaService.from_attrs(), which uses the
    auto-generated Pydantic model's from_attrs() for UNSET conversion.

    Args:
        service: attrs Service model from API response

    Returns:
        KatanaService with all fields populated

    Example:
        ```python
        from katana_public_api_client.api.service import get_service
        from katana_public_api_client.utils import unwrap

        response = await get_service.asyncio_detailed(client=client, id=123)
        service_attrs = unwrap(response)
        service_domain = service_to_katana(service_attrs)

        # Now use domain model features
        print(service_domain.get_display_name())
        print(service_domain.to_csv_row())
        ```
    """
    from .service import KatanaService

    return KatanaService.from_attrs(service)

services_to_katana(services)

Convert list of attrs Service models to list of KatanaService.

Parameters:

  • services (list[Service]) –

    List of attrs Service models

Returns:

Example
from katana_public_api_client.api.service import get_all_services
from katana_public_api_client.utils import unwrap_data

response = await get_all_services.asyncio_detailed(client=client)
services_attrs = unwrap_data(response)
services_domain = services_to_katana(services_attrs)

# Now use domain model features
sellable = [s for s in services_domain if s.is_sellable]
Source code in katana_public_api_client/domain/converters.py
def services_to_katana(services: list[Service]) -> list[KatanaService]:
    """Convert list of attrs Service models to list of KatanaService.

    Args:
        services: List of attrs Service models

    Returns:
        List of KatanaService models

    Example:
        ```python
        from katana_public_api_client.api.service import get_all_services
        from katana_public_api_client.utils import unwrap_data

        response = await get_all_services.asyncio_detailed(client=client)
        services_attrs = unwrap_data(response)
        services_domain = services_to_katana(services_attrs)

        # Now use domain model features
        sellable = [s for s in services_domain if s.is_sellable]
        ```
    """
    return [service_to_katana(s) for s in services]

unwrap_unset(value, default=None)

Unwrap an Unset sentinel value.

Parameters:

  • value (T | Unset) –

    Value that might be Unset

  • default (T | None, default: None ) –

    Default value to return if Unset

Returns:

  • T | None

    The unwrapped value, or default if value is Unset

Example
from katana_public_api_client.client_types import UNSET

unwrap_unset(42)  # 42
unwrap_unset(UNSET)  # None
unwrap_unset(UNSET, 0)  # 0
Source code in katana_public_api_client/domain/converters.py
def unwrap_unset(value: T | Unset, default: T | None = None) -> T | None:
    """Unwrap an Unset sentinel value.

    Args:
        value: Value that might be Unset
        default: Default value to return if Unset

    Returns:
        The unwrapped value, or default if value is Unset

    Example:
        ```python
        from katana_public_api_client.client_types import UNSET

        unwrap_unset(42)  # 42
        unwrap_unset(UNSET)  # None
        unwrap_unset(UNSET, 0)  # 0
        ```
    """
    return default if value is UNSET else value  # type: ignore[return-value]

variant_to_katana(variant)

Convert attrs Variant model to Pydantic KatanaVariant.

This function delegates to KatanaVariant.from_attrs(), which uses the auto-generated Pydantic model's from_attrs() for UNSET conversion.

Parameters:

  • variant (Variant) –

    attrs Variant model from API response

Returns:

Example
from katana_public_api_client.api.variant import get_variant
from katana_public_api_client.utils import unwrap

response = await get_variant.asyncio_detailed(client=client, id=123)
variant_attrs = unwrap(response)
variant_domain = variant_to_katana(variant_attrs)

# Now use domain model features
print(variant_domain.get_display_name())
Source code in katana_public_api_client/domain/converters.py
def variant_to_katana(variant: Variant) -> KatanaVariant:
    """Convert attrs Variant model to Pydantic KatanaVariant.

    This function delegates to KatanaVariant.from_attrs(), which uses the
    auto-generated Pydantic model's from_attrs() for UNSET conversion.

    Args:
        variant: attrs Variant model from API response

    Returns:
        KatanaVariant with all fields populated

    Example:
        ```python
        from katana_public_api_client.api.variant import get_variant
        from katana_public_api_client.utils import unwrap

        response = await get_variant.asyncio_detailed(client=client, id=123)
        variant_attrs = unwrap(response)
        variant_domain = variant_to_katana(variant_attrs)

        # Now use domain model features
        print(variant_domain.get_display_name())
        ```
    """
    from .variant import KatanaVariant

    return KatanaVariant.from_attrs(variant)

variants_to_katana(variants)

Convert list of attrs Variant models to list of KatanaVariant.

Parameters:

  • variants (list[Variant]) –

    List of attrs Variant models

Returns:

Example
from katana_public_api_client.api.variant import get_all_variants
from katana_public_api_client.utils import unwrap_data

response = await get_all_variants.asyncio_detailed(client=client)
variants_attrs = unwrap_data(response)
variants_domain = variants_to_katana(variants_attrs)

# Now use domain model features
high_margin = [v for v in variants_domain if v.is_high_margin]
Source code in katana_public_api_client/domain/converters.py
def variants_to_katana(variants: list[Variant]) -> list[KatanaVariant]:
    """Convert list of attrs Variant models to list of KatanaVariant.

    Args:
        variants: List of attrs Variant models

    Returns:
        List of KatanaVariant models

    Example:
        ```python
        from katana_public_api_client.api.variant import get_all_variants
        from katana_public_api_client.utils import unwrap_data

        response = await get_all_variants.asyncio_detailed(client=client)
        variants_attrs = unwrap_data(response)
        variants_domain = variants_to_katana(variants_attrs)

        # Now use domain model features
        high_margin = [v for v in variants_domain if v.is_high_margin]
        ```
    """
    return [variant_to_katana(v) for v in variants]