Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
274 changes: 155 additions & 119 deletions docs/katana-openapi.yaml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import asyncio
import datetime as _datetime
from datetime import UTC, datetime
from decimal import Decimal
from enum import StrEnum
from typing import Annotated, Any, Literal

Expand Down Expand Up @@ -289,9 +290,17 @@ def _build_row_requests(
"""Convert pydantic row inputs to attrs request rows."""
out: list[StockTransferRowRequest] = []
for row in rows:
# ``StockTransferRowRequest.quantity`` is a wire-format string
# (fixed-precision decimal) per the spec; the MCP input is a
# pydantic ``float`` so emit a non-exponent decimal string at
# the boundary. Going through ``Decimal(str(float))`` preserves
# the shortest round-trip representation that ``str(float)``
# uses, then ``format(..., "f")`` on the Decimal emits decimal
# form (no exponent) at full precision — unlike
# ``format(float, "f")`` which silently rounds to 6 digits.
api_row = StockTransferRowRequest(
variant_id=row.variant_id,
quantity=row.quantity,
quantity=format(Decimal(str(row.quantity)), "f"),
Comment thread
dougborg marked this conversation as resolved.
)
Comment thread
dougborg marked this conversation as resolved.
if row.batch_transactions:
api_row.additional_properties = {
Expand Down
71 changes: 71 additions & 0 deletions katana_mcp_server/tests/tools/test_stock_transfers.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
StockTransferHeaderPatch,
StockTransferRowInput,
StockTransferStatusPatch,
_build_row_requests,
_create_stock_transfer_impl,
_delete_stock_transfer_impl,
_list_stock_transfers_impl,
Expand Down Expand Up @@ -374,6 +375,76 @@ async def test_create_stock_transfer_rejects_empty_rows():
)


# ----------------------------------------------------------------------
# _build_row_requests — quantity float → wire-decimal-string boundary
# ----------------------------------------------------------------------
#
# StockTransferRowRequest.quantity is a wire-format string per the spec
# (decimal, no scientific notation, no silent rounding). The MCP input
# is a pydantic float, so the boundary must:
# 1. Avoid exponent notation for small/large values (str(1e-7) ==
# '1e-07', format(1e-7, 'f') == '0.000000', neither acceptable).
# 2. Preserve the shortest round-trip representation Python's str()
# gives, without rounding past the default 6 fractional digits
# that format(float, 'f') silently applies.


@pytest.mark.parametrize(
"quantity, expected",
[
# Whole numbers — Python float ``1.0`` carries the trailing
# ``.0`` through ``str()`` and ``Decimal``; the live API
# accepts either form so we just pin the deterministic output.
(1.0, "1.0"),
(100.0, "100.0"),
# Common fractional values
(1.5, "1.5"),
(0.25, "0.25"),
# High-precision input — must NOT round to 6 decimals
(0.123456789, "0.123456789"),
# Very small value — must NOT use exponent notation OR
# silently round to "0.000000"
(1e-7, "0.0000001"),
# Large value (well-formed)
(1_000_000.5, "1000000.5"),
],
)
def test_build_row_requests_quantity_serialization(
quantity: float, expected: str
) -> None:
"""``_build_row_requests`` emits decimal-form strings without
scientific notation or 6-digit rounding (live API rejects both)."""
rows = _build_row_requests(
[StockTransferRowInput(variant_id=42, quantity=quantity)]
)
assert len(rows) == 1
quantity_str = rows[0].quantity
assert isinstance(quantity_str, str)
assert quantity_str == expected
assert "e" not in quantity_str.lower()


def test_build_row_requests_preserves_batch_transactions() -> None:
"""Batch transactions ride along in ``additional_properties``
untouched (the quantity-string conversion only affects the row's
own ``quantity`` field)."""
rows = _build_row_requests(
[
StockTransferRowInput(
variant_id=42,
quantity=0.1,
batch_transactions=[
StockTransferBatchTransactionInput(batch_id=7, quantity=0.1),
],
)
]
)
assert rows[0].quantity == "0.1"
batch = rows[0].additional_properties["batch_transactions"]
assert batch[0]["batch_id"] == 7
assert batch[0]["quantity"] == 0.1 # type stays as float in batch payload


# ============================================================================
# list_stock_transfers — pattern v2
# ============================================================================
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,9 @@ def sync_detailed(

Args:
body (CreateCustomFieldDefinitionRequest): Request payload for creating a new custom field
definition. Example: {'label': 'Quality Grade', 'field_type': 'select', 'entity_type':
'product', 'source': 'user', 'description': 'Customer-facing quality classification',
'options': {'values': ['A', 'B', 'C']}}.
definition. Example: {'label': 'Channel', 'field_type': 'shortText', 'entity_type':
'SalesOrder', 'source': 'your-integration', 'description': 'Customer-facing sales channel
classification'}.

Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
Expand Down Expand Up @@ -129,9 +129,9 @@ def sync(

Args:
body (CreateCustomFieldDefinitionRequest): Request payload for creating a new custom field
definition. Example: {'label': 'Quality Grade', 'field_type': 'select', 'entity_type':
'product', 'source': 'user', 'description': 'Customer-facing quality classification',
'options': {'values': ['A', 'B', 'C']}}.
definition. Example: {'label': 'Channel', 'field_type': 'shortText', 'entity_type':
'SalesOrder', 'source': 'your-integration', 'description': 'Customer-facing sales channel
classification'}.

Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
Expand Down Expand Up @@ -159,9 +159,9 @@ async def asyncio_detailed(

Args:
body (CreateCustomFieldDefinitionRequest): Request payload for creating a new custom field
definition. Example: {'label': 'Quality Grade', 'field_type': 'select', 'entity_type':
'product', 'source': 'user', 'description': 'Customer-facing quality classification',
'options': {'values': ['A', 'B', 'C']}}.
definition. Example: {'label': 'Channel', 'field_type': 'shortText', 'entity_type':
'SalesOrder', 'source': 'your-integration', 'description': 'Customer-facing sales channel
classification'}.

Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
Expand Down Expand Up @@ -192,9 +192,9 @@ async def asyncio(

Args:
body (CreateCustomFieldDefinitionRequest): Request payload for creating a new custom field
definition. Example: {'label': 'Quality Grade', 'field_type': 'select', 'entity_type':
'product', 'source': 'user', 'description': 'Customer-facing quality classification',
'options': {'values': ['A', 'B', 'C']}}.
definition. Example: {'label': 'Channel', 'field_type': 'shortText', 'entity_type':
'SalesOrder', 'source': 'your-integration', 'description': 'Customer-facing sales channel
classification'}.

Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from http import HTTPStatus
from typing import Any, cast
from urllib.parse import quote
from uuid import UUID

import httpx

Expand All @@ -11,7 +12,7 @@


def _get_kwargs(
id: int,
id: UUID,
) -> dict[str, Any]:

_kwargs: dict[str, Any] = {
Expand Down Expand Up @@ -69,7 +70,7 @@ def _build_response(


def sync_detailed(
id: int,
id: UUID,
*,
client: AuthenticatedClient | Client,
) -> Response[Any | ErrorResponse]:
Expand All @@ -78,7 +79,7 @@ def sync_detailed(
Deletes an existing custom field definition.

Args:
id (int):
id (UUID):

Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
Expand All @@ -101,7 +102,7 @@ def sync_detailed(


def sync(
id: int,
id: UUID,
*,
client: AuthenticatedClient | Client,
) -> Any | ErrorResponse | None:
Expand All @@ -110,7 +111,7 @@ def sync(
Deletes an existing custom field definition.

Args:
id (int):
id (UUID):

Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
Expand All @@ -128,7 +129,7 @@ def sync(


async def asyncio_detailed(
id: int,
id: UUID,
*,
client: AuthenticatedClient | Client,
) -> Response[Any | ErrorResponse]:
Expand All @@ -137,7 +138,7 @@ async def asyncio_detailed(
Deletes an existing custom field definition.

Args:
id (int):
id (UUID):

Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
Expand All @@ -158,7 +159,7 @@ async def asyncio_detailed(


async def asyncio(
id: int,
id: UUID,
*,
client: AuthenticatedClient | Client,
) -> Any | ErrorResponse | None:
Expand All @@ -167,7 +168,7 @@ async def asyncio(
Deletes an existing custom field definition.

Args:
id (int):
id (UUID):

Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from http import HTTPStatus
from typing import Any
from urllib.parse import quote
from uuid import UUID

import httpx

Expand All @@ -12,7 +13,7 @@


def _get_kwargs(
id: int,
id: UUID,
) -> dict[str, Any]:

_kwargs: dict[str, Any] = {
Expand Down Expand Up @@ -71,7 +72,7 @@ def _build_response(


def sync_detailed(
id: int,
id: UUID,
*,
client: AuthenticatedClient | Client,
) -> Response[CustomFieldDefinition | ErrorResponse]:
Expand All @@ -80,7 +81,7 @@ def sync_detailed(
Retrieves a single custom field definition by ID.

Args:
id (int):
id (UUID):

Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
Expand All @@ -103,7 +104,7 @@ def sync_detailed(


def sync(
id: int,
id: UUID,
*,
client: AuthenticatedClient | Client,
) -> CustomFieldDefinition | ErrorResponse | None:
Expand All @@ -112,7 +113,7 @@ def sync(
Retrieves a single custom field definition by ID.

Args:
id (int):
id (UUID):

Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
Expand All @@ -130,7 +131,7 @@ def sync(


async def asyncio_detailed(
id: int,
id: UUID,
*,
client: AuthenticatedClient | Client,
) -> Response[CustomFieldDefinition | ErrorResponse]:
Expand All @@ -139,7 +140,7 @@ async def asyncio_detailed(
Retrieves a single custom field definition by ID.

Args:
id (int):
id (UUID):

Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
Expand All @@ -160,7 +161,7 @@ async def asyncio_detailed(


async def asyncio(
id: int,
id: UUID,
*,
client: AuthenticatedClient | Client,
) -> CustomFieldDefinition | ErrorResponse | None:
Expand All @@ -169,7 +170,7 @@ async def asyncio(
Retrieves a single custom field definition by ID.

Args:
id (int):
id (UUID):

Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
Expand Down
Loading
Loading