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

Unlike the generated attrs model, this model: - Has no Unset sentinel values - Provides ETL-friendly methods - Is immutable by default - Clean Optional types

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
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 or "",
    }

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

Unlike the generated attrs model, this model: - Has no Unset sentinel values - Provides ETL-friendly methods - Is immutable by default - Clean Optional types

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
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 or "",
    }

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

Unlike the generated attrs model, this model: - Has no Unset sentinel values - Provides ETL-friendly methods - Is immutable by default - Clean Optional types

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
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_ or "service",
        "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 or "",
        "Deleted At": self.deleted_at or "",
    }

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

Unlike the generated attrs model, this model: - Has no Unset sentinel values - Provides ETL-friendly methods - Is immutable by default - Clean Optional types

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

Handles: - Unwrapping Unset sentinel values - Extracting enum values - Converting nested variants/configs to counts

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.

    Handles:
    - Unwrapping Unset sentinel values
    - Extracting enum values
    - Converting nested variants/configs to counts

    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

    # Count nested collections
    variants_list = unwrap_unset(material.variants, [])
    configs_list = unwrap_unset(material.configs, [])
    variant_count = len(variants_list) if variants_list else 0
    config_count = len(configs_list) if configs_list else 0

    # Handle archived_at datetime conversion
    archived_at_raw = unwrap_unset(material.archived_at)
    archived_at_str: str | None = None
    if archived_at_raw and hasattr(archived_at_raw, "isoformat"):
        archived_at_str = archived_at_raw.isoformat()

    return KatanaMaterial(
        id=material.id,
        name=material.name,
        type="material",  # Always "material" literal
        uom=unwrap_unset(material.uom),
        category_name=unwrap_unset(material.category_name),
        is_sellable=unwrap_unset(material.is_sellable),
        batch_tracked=unwrap_unset(material.batch_tracked),
        default_supplier_id=unwrap_unset(material.default_supplier_id),
        purchase_uom=unwrap_unset(material.purchase_uom),
        purchase_uom_conversion_rate=unwrap_unset(
            material.purchase_uom_conversion_rate
        ),
        additional_info=unwrap_unset(material.additional_info),
        custom_field_collection_id=unwrap_unset(material.custom_field_collection_id),
        archived_at=archived_at_str,
        variant_count=variant_count,
        config_count=config_count,
        created_at=unwrap_unset(material.created_at),
        updated_at=unwrap_unset(material.updated_at),
        deleted_at=unwrap_unset(material.deleted_at)  # type: ignore[arg-type]
        if hasattr(material, "deleted_at")
        else None,
    )

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.

Handles: - Unwrapping Unset sentinel values - Extracting enum values - Converting nested variants/configs to counts

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.

    Handles:
    - Unwrapping Unset sentinel values
    - Extracting enum values
    - Converting nested variants/configs to counts

    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

    # Count nested collections
    variants_list = unwrap_unset(product.variants, [])
    configs_list = unwrap_unset(product.configs, [])
    variant_count = len(variants_list) if variants_list else 0
    config_count = len(configs_list) if configs_list else 0

    # Handle archived_at datetime conversion
    archived_at_raw = unwrap_unset(product.archived_at)
    archived_at_str: str | None = None
    if archived_at_raw and hasattr(archived_at_raw, "isoformat"):
        archived_at_str = archived_at_raw.isoformat()

    return KatanaProduct(
        id=product.id,
        name=product.name,
        type="product",  # Always "product" literal
        uom=unwrap_unset(product.uom),
        category_name=unwrap_unset(product.category_name),
        is_sellable=unwrap_unset(product.is_sellable),
        is_producible=unwrap_unset(product.is_producible),
        is_purchasable=unwrap_unset(product.is_purchasable),
        is_auto_assembly=unwrap_unset(product.is_auto_assembly),
        batch_tracked=unwrap_unset(product.batch_tracked),
        serial_tracked=unwrap_unset(product.serial_tracked),
        operations_in_sequence=unwrap_unset(product.operations_in_sequence),
        default_supplier_id=unwrap_unset(product.default_supplier_id),
        lead_time=unwrap_unset(product.lead_time),
        minimum_order_quantity=unwrap_unset(product.minimum_order_quantity),
        purchase_uom=unwrap_unset(product.purchase_uom),
        purchase_uom_conversion_rate=unwrap_unset(product.purchase_uom_conversion_rate),
        additional_info=unwrap_unset(product.additional_info),
        custom_field_collection_id=unwrap_unset(product.custom_field_collection_id),
        archived_at=archived_at_str,
        variant_count=variant_count,
        config_count=config_count,
        created_at=unwrap_unset(product.created_at),
        updated_at=unwrap_unset(product.updated_at),
        deleted_at=None,  # Product model has archived_at, not deleted_at
    )

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.

Handles: - Unwrapping Unset sentinel values - Extracting enum values - Converting nested variants to count

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.

    Handles:
    - Unwrapping Unset sentinel values
    - Extracting enum values
    - Converting nested variants to count

    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

    # Extract type value from enum if present
    type_enum = unwrap_unset(service.type_)
    type_value = getattr(type_enum, "value", None) if type_enum else None

    # Count nested collections
    variants_list = unwrap_unset(service.variants, [])
    variant_count = len(variants_list) if variants_list else 0

    return KatanaService(
        id=service.id,
        name=unwrap_unset(service.name),
        type=type_value,
        uom=unwrap_unset(service.uom),
        category_name=unwrap_unset(service.category_name),
        is_sellable=unwrap_unset(service.is_sellable),
        additional_info=unwrap_unset(service.additional_info),
        custom_field_collection_id=unwrap_unset(service.custom_field_collection_id),
        archived_at=unwrap_unset(service.archived_at),
        variant_count=variant_count,
        created_at=unwrap_unset(service.created_at),
        updated_at=unwrap_unset(service.updated_at),
        deleted_at=None,  # Service model uses deleted_at as string, not datetime
    )

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.

Handles: - Unwrapping Unset sentinel values - Extracting nested product_or_material name - Converting config_attributes to dicts - Converting custom_fields to dicts

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

    Handles:
    - Unwrapping Unset sentinel values
    - Extracting nested product_or_material name
    - Converting config_attributes to dicts
    - Converting custom_fields to dicts

    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.profit_margin)
        print(variant_domain.get_display_name())
        ```
    """
    from .variant import KatanaVariant

    # Extract product/material name from nested object if available
    # Note: product_or_material_name flat field doesn't exist in API
    # Only product_or_material.name exists (when extend=product_or_material is used)
    product_or_material_name = None
    if hasattr(variant, "product_or_material"):
        pom = unwrap_unset(variant.product_or_material)
        if pom and hasattr(pom, "name"):
            product_or_material_name = unwrap_unset(pom.name)

    # Convert config attributes to simple dicts
    config_attrs: list[dict[str, str]] = []
    if config_list := unwrap_unset(variant.config_attributes, []):
        for attr in config_list:
            config_name = unwrap_unset(
                cast(str | Unset, getattr(attr, "config_name", None))
            )
            config_value = unwrap_unset(
                cast(str | Unset, getattr(attr, "config_value", None))
            )
            config_attrs.append(
                {
                    "config_name": config_name or "",
                    "config_value": config_value or "",
                }
            )

    # Convert custom fields to simple dicts
    custom: list[dict[str, str]] = []
    if custom_list := unwrap_unset(variant.custom_fields, []):
        for field in custom_list:
            field_name = unwrap_unset(
                cast(str | Unset, getattr(field, "field_name", None))
            )
            field_value = unwrap_unset(
                cast(str | Unset, getattr(field, "field_value", None))
            )
            custom.append(
                {
                    "field_name": field_name or "",
                    "field_value": field_value or "",
                }
            )

    # Extract type value from enum if present
    type_value = None
    if type_enum := unwrap_unset(variant.type_):
        type_value = getattr(type_enum, "value", None)

    return KatanaVariant(
        id=variant.id,
        sku=unwrap_unset(variant.sku) or "",  # Ensure str, not None
        sales_price=unwrap_unset(variant.sales_price),
        purchase_price=unwrap_unset(variant.purchase_price),
        product_id=unwrap_unset(variant.product_id),
        material_id=unwrap_unset(variant.material_id),
        product_or_material_name=product_or_material_name,  # type: ignore[arg-type]
        type=type_value,  # Pydantic uses 'type' not 'type_'
        internal_barcode=unwrap_unset(variant.internal_barcode),
        registered_barcode=unwrap_unset(variant.registered_barcode),
        supplier_item_codes=unwrap_unset(variant.supplier_item_codes)
        or [],  # Ensure list
        lead_time=unwrap_unset(variant.lead_time),
        minimum_order_quantity=unwrap_unset(variant.minimum_order_quantity),
        config_attributes=config_attrs,
        custom_fields=custom,
        created_at=unwrap_unset(variant.created_at),
        updated_at=unwrap_unset(variant.updated_at),
        deleted_at=unwrap_unset(variant.deleted_at),
    )

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]