Skip to content

Commit 7133848

Browse files
dougborgclaude
andcommitted
feat(client)!: add catalog Cached* siblings + FTS5 specs (#472 Phase A)
Extends the SQLModel cache-table generator to emit Cached* siblings for the 11 catalog entity types previously cached in the legacy single-table CatalogCache. Phase B will wire the typed-cache sync specs and FTS5 sidecar; Phase A just lays the schema groundwork. Generator changes (scripts/generate_pydantic_models.py): - CACHE_TABLES gains 11 catalog entries (Variant, Product, Material, Service, Customer, Supplier, Location1, TaxRate, Operator, Factory, AdditionalCost) with json_columns / extra_fields / unique_columns / flatten_parent specs as appropriate. - New CACHE_FTS_SPECS sidecar declares per-entity FTS5 search columns; generator emits __fts_columns__: ClassVar[tuple[str, ...]] on each Cached* class with an entry. Lookup-only entities (Location, TaxRate, Operator, Factory, AdditionalCost) skip FTS — get_all + difflib fuzzy fallback covers them in Phase B. - New CacheTableSpec.unique_columns + inject_unique_indexes pass: rewrites Field(...) to SQLField(unique=True, index=True, ...) on declared columns. CachedVariant.sku gets DB-level uniqueness mirroring the legacy entity_index.sku COLLATE NOCASE constraint. - New CacheTableSpec.flatten_parent + flatten_intermediate_inheritance pass: inlines an intermediate non-base, non-cached parent class's fields into the cache class body so SQLModel's column inference sees every field (Material/Product extend InventoryItem, which has nested list/object fields SQLModel can't auto-map). Detects child-overriding fields like Material.type: Literal["material"] and keeps the parent's enum-typed declaration so SQLModel doesn't choke on Literal[...]. - New _rewrite_identifiers_outside_strings helper using tokenize so cross-cache type rewrites (Customer → CachedCustomer in type annotations) skip string literals — descriptions like "Customer's reference number" no longer get corrupted. - inject_json_columns now handles bare ``name: T = Default`` field declarations (Location1.address) plus multi-line type annotations with inner commas (Factory.legal_address: dict[str, Any]). Variant gets four cache-only extra_fields (parent_archived_at, display_name, parent_name, supplier_item_codes_text) lifted from the legacy _variant_to_cache_dict denormalization. Phase B's attrs_postprocess hook populates them at sync time; the FTS spec references them so the schema and search-column contract stay honest. New public Cached* classes ship in the generated client: - CachedVariant - CachedProduct - CachedMaterial - CachedService - CachedCustomer - CachedSupplier - CachedLocation (Location1 → name_override="Location") - CachedTaxRate - CachedOperator - CachedFactory - CachedAdditionalCost Tests cover the new generator passes (inject_unique_indexes, inject_fts_columns, flatten_intermediate_inheritance, _rewrite_identifiers_outside_strings) plus end-to-end assertions on the regenerated output. Closes part of #472 (Phase A). BREAKING CHANGE: New public Cached* classes (CachedVariant, CachedProduct, CachedMaterial, CachedService, CachedCustomer, CachedSupplier, CachedLocation, CachedTaxRate, CachedOperator, CachedFactory, CachedAdditionalCost) ship in the generated client. CachedPurchaseOrder.supplier field type changed from Supplier to CachedSupplier (now references the cache sibling, consistent with the typed-cache pattern). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c845c69 commit 7133848

8 files changed

Lines changed: 1759 additions & 30 deletions

File tree

katana_public_api_client/models_pydantic/_generated/__init__.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@
2626
Attribute,
2727
Attribute1,
2828
Attribute3,
29+
CachedAdditionalCost,
30+
CachedFactory,
31+
CachedLocation,
32+
CachedOperator,
33+
CachedTaxRate,
2934
ClearDemandForecastRequest,
3035
Comparison,
3136
Comparison1,
@@ -100,6 +105,8 @@
100105
VariantType,
101106
)
102107
from .contacts import (
108+
CachedCustomer,
109+
CachedSupplier,
103110
CreateCustomerAddressRequest,
104111
CreateCustomerRequest,
105112
CreatePriceListCustomerRequest,
@@ -199,6 +206,10 @@
199206
UniqueItemsValidationError,
200207
)
201208
from .inventory import (
209+
CachedMaterial,
210+
CachedProduct,
211+
CachedService,
212+
CachedVariant,
202213
CreateInventoryReorderPointRequest,
203214
CreateMaterialRequest,
204215
CreateProductOperationRowItem,
@@ -466,16 +477,27 @@
466477
"BatchTransactionRequest",
467478
"BomRow",
468479
"BomRowListResponse",
480+
"CachedAdditionalCost",
481+
"CachedCustomer",
482+
"CachedFactory",
483+
"CachedLocation",
469484
"CachedManufacturingOrder",
470485
"CachedManufacturingOrderRecipeRow",
486+
"CachedMaterial",
487+
"CachedOperator",
488+
"CachedProduct",
471489
"CachedPurchaseOrder",
472490
"CachedPurchaseOrderRow",
473491
"CachedSalesOrder",
474492
"CachedSalesOrderRow",
493+
"CachedService",
475494
"CachedStockAdjustment",
476495
"CachedStockAdjustmentRow",
477496
"CachedStockTransfer",
478497
"CachedStockTransferRow",
498+
"CachedSupplier",
499+
"CachedTaxRate",
500+
"CachedVariant",
479501
"ClearDemandForecastRequest",
480502
"Code",
481503
"Code1",

katana_public_api_client/models_pydantic/_generated/common.py

Lines changed: 148 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,19 @@
66
uv run poe generate-pydantic
77
"""
88

9-
from __future__ import annotations
10-
9+
from datetime import datetime
1110
from enum import StrEnum
1211
from typing import Annotated, Any
1312

1413
from pydantic import AwareDatetime, ConfigDict, Field, RootModel
14+
from sqlalchemy import Column
15+
from sqlmodel import (
16+
Field as SQLField,
17+
)
1518

1619
from katana_public_api_client.models_pydantic._base import KatanaPydanticBase
20+
from katana_public_api_client.models_pydantic._mapped_shim import Mapped
21+
from katana_public_api_client.models_pydantic._pydantic_json import PydanticJSON
1722

1823
from .base import DeletableEntity, UpdatableEntity
1924

@@ -1125,3 +1130,144 @@ class CustomFieldsCollectionListResponse(KatanaPydanticBase):
11251130
description="Array of custom field collections with their field definitions and configuration"
11261131
),
11271132
] = None
1133+
1134+
1135+
class CachedAdditionalCost(DeletableEntity, table=True):
1136+
__tablename__ = "additional_cost"
1137+
model_config = ConfigDict(frozen=False)
1138+
1139+
id: Annotated[
1140+
Mapped[int], SQLField(primary_key=True, description="Unique identifier")
1141+
]
1142+
1143+
name: Mapped[str]
1144+
1145+
1146+
class CachedLocation(KatanaPydanticBase, table=True):
1147+
__tablename__ = "location"
1148+
model_config = ConfigDict(frozen=False)
1149+
1150+
id: Annotated[
1151+
Mapped[int], SQLField(primary_key=True, description="Unique identifier")
1152+
]
1153+
1154+
name: Mapped[str]
1155+
legal_name: Mapped[str | None] = None
1156+
address_id: Mapped[int | None] = None
1157+
address: Annotated[
1158+
Mapped[LocationAddress | None], SQLField(sa_column=Column(PydanticJSON))
1159+
] = None
1160+
is_primary: Mapped[bool | None] = None
1161+
sales_allowed: Mapped[bool | None] = None
1162+
purchase_allowed: Mapped[bool | None] = None
1163+
manufacturing_allowed: Mapped[bool | None] = None
1164+
1165+
1166+
class CachedTaxRate(UpdatableEntity, table=True):
1167+
__tablename__ = "tax_rate"
1168+
model_config = ConfigDict(frozen=False)
1169+
1170+
id: Annotated[
1171+
Mapped[int], SQLField(primary_key=True, description="Unique identifier")
1172+
]
1173+
1174+
name: Annotated[
1175+
Mapped[str | None],
1176+
Field(
1177+
description='Descriptive name for the tax rate (e.g., "VAT 20%", "Sales Tax", "GST")'
1178+
),
1179+
] = None
1180+
rate: Annotated[
1181+
Mapped[float | None],
1182+
Field(description="Tax rate as a percentage (e.g., 20.5 for 20.5% tax)"),
1183+
] = None
1184+
is_default_sales: Annotated[
1185+
Mapped[bool | None],
1186+
Field(
1187+
description="Whether this tax rate is the default for sales transactions"
1188+
),
1189+
] = None
1190+
is_default_purchases: Annotated[
1191+
Mapped[bool | None],
1192+
Field(
1193+
description="Whether this tax rate is the default for purchase transactions"
1194+
),
1195+
] = None
1196+
display_name: Annotated[
1197+
Mapped[str | None],
1198+
Field(description="Formatted display name for user interface presentation"),
1199+
] = None
1200+
1201+
1202+
class CachedFactory(KatanaPydanticBase, table=True):
1203+
__tablename__ = "factory"
1204+
model_config = ConfigDict(frozen=False)
1205+
1206+
id: Annotated[
1207+
Mapped[int], SQLField(primary_key=True, description="Unique identifier")
1208+
]
1209+
1210+
name: Annotated[
1211+
Mapped[str | None],
1212+
Field(description="Display name of the manufacturing facility"),
1213+
] = None
1214+
address: Annotated[
1215+
Mapped[str | None],
1216+
Field(
1217+
description="Physical address of the manufacturing facility for shipping and logistics"
1218+
),
1219+
] = None
1220+
currency: Annotated[
1221+
Mapped[str | None],
1222+
Field(
1223+
description="Default currency code (ISO 4217) used for financial transactions at this facility"
1224+
),
1225+
] = None
1226+
timezone: Annotated[
1227+
Mapped[str | None],
1228+
Field(
1229+
description="Timezone identifier for the facility location used for scheduling and time tracking"
1230+
),
1231+
] = None
1232+
legal_address: Annotated[
1233+
Mapped[dict[str, Any] | None],
1234+
SQLField(
1235+
sa_column=Column(PydanticJSON), description="Legal address information"
1236+
),
1237+
] = None
1238+
legal_name: Annotated[
1239+
Mapped[str | None], Field(description="Legal name of the company")
1240+
] = None
1241+
display_name: Annotated[
1242+
Mapped[str], Field(description="Display name of the company")
1243+
]
1244+
base_currency_code: Annotated[Mapped[str], Field(description="Base currency code")]
1245+
default_so_delivery_time: Annotated[
1246+
Mapped[datetime | None], Field(description="Default sales order delivery time")
1247+
] = None
1248+
default_po_lead_time: Annotated[
1249+
Mapped[datetime | None], Field(description="Default purchase order lead time")
1250+
] = None
1251+
default_manufacturing_location_id: Annotated[
1252+
Mapped[int | None], Field(description="Default manufacturing location ID")
1253+
] = None
1254+
default_purchases_location_id: Annotated[
1255+
Mapped[int | None], Field(description="Default purchases location ID")
1256+
] = None
1257+
default_sales_location_id: Annotated[
1258+
Mapped[int | None], Field(description="Default sales location ID")
1259+
] = None
1260+
inventory_closing_date: Annotated[
1261+
Mapped[datetime | None], Field(description="Inventory closing date")
1262+
] = None
1263+
1264+
1265+
class CachedOperator(DeletableEntity, table=True):
1266+
__tablename__ = "operator"
1267+
model_config = ConfigDict(frozen=False)
1268+
1269+
id: Annotated[
1270+
Mapped[int], SQLField(primary_key=True, description="Unique identifier")
1271+
]
1272+
1273+
operator_name: Mapped[str]

katana_public_api_client/models_pydantic/_generated/contacts.py

Lines changed: 139 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,18 @@
66
uv run poe generate-pydantic
77
"""
88

9-
from __future__ import annotations
10-
119
from enum import StrEnum
12-
from typing import Annotated
10+
from typing import Annotated, ClassVar
1311

1412
from pydantic import ConfigDict, EmailStr, Field, RootModel
13+
from sqlalchemy import Column
14+
from sqlmodel import (
15+
Field as SQLField,
16+
)
1517

1618
from katana_public_api_client.models_pydantic._base import KatanaPydanticBase
19+
from katana_public_api_client.models_pydantic._mapped_shim import Mapped
20+
from katana_public_api_client.models_pydantic._pydantic_json import PydanticJSON
1721

1822
from .base import DeletableEntity, UpdatableEntity
1923
from .common import Address, AddressEntityType, PriceAdjustmentMethod
@@ -656,3 +660,135 @@ class CustomerListResponse(KatanaPydanticBase):
656660
data: Annotated[
657661
list[Customer] | None, Field(description="Array of customer entities")
658662
] = None
663+
664+
665+
class CachedSupplier(DeletableEntity, table=True):
666+
__tablename__ = "supplier"
667+
__fts_columns__: ClassVar[tuple[str, ...]] = (
668+
"name",
669+
"email",
670+
"phone",
671+
)
672+
model_config = ConfigDict(frozen=False)
673+
674+
id: Annotated[
675+
Mapped[int], SQLField(primary_key=True, description="Unique identifier")
676+
]
677+
678+
name: Annotated[
679+
Mapped[str | None],
680+
Field(description="Business name of the supplier company or individual"),
681+
] = None
682+
email: Annotated[
683+
Mapped[str | None],
684+
Field(
685+
description="Primary email address for supplier communication and order confirmations"
686+
),
687+
] = None
688+
phone: Annotated[
689+
Mapped[str | None],
690+
Field(
691+
description="Primary phone number for supplier contact and communication"
692+
),
693+
] = None
694+
currency: Annotated[
695+
Mapped[str | None],
696+
Field(
697+
description="Default currency used for transactions with this supplier (ISO 4217 format)",
698+
pattern="^[A-Z]{3}$",
699+
),
700+
] = None
701+
comment: Annotated[
702+
Mapped[str | None],
703+
Field(description="Optional notes or comments about the supplier relationship"),
704+
] = None
705+
default_address_id: Annotated[
706+
Mapped[int | None],
707+
Field(description="Unique identifier of the default address for this supplier"),
708+
] = None
709+
addresses: Annotated[
710+
Mapped[list[SupplierAddress] | None],
711+
SQLField(
712+
sa_column=Column(PydanticJSON),
713+
description="List of addresses associated with this supplier",
714+
),
715+
] = None
716+
717+
718+
class CachedCustomer(DeletableEntity, table=True):
719+
__tablename__ = "customer"
720+
__fts_columns__: ClassVar[tuple[str, ...]] = (
721+
"name",
722+
"email",
723+
"phone",
724+
)
725+
model_config = ConfigDict(frozen=False)
726+
727+
id: Annotated[
728+
Mapped[int], SQLField(primary_key=True, description="Unique identifier")
729+
]
730+
731+
name: Annotated[
732+
Mapped[str],
733+
Field(
734+
description="Customer display name, either individual name or company name"
735+
),
736+
]
737+
first_name: Annotated[
738+
Mapped[str | None],
739+
Field(description="Customer's first name for individual contacts"),
740+
] = None
741+
last_name: Annotated[
742+
Mapped[str | None],
743+
Field(description="Customer's last name for individual contacts"),
744+
] = None
745+
company: Annotated[
746+
Mapped[str | None], Field(description="Company name for business customers")
747+
] = None
748+
email: Annotated[
749+
Mapped[str | None],
750+
Field(
751+
description="Primary email address for communication and order notifications"
752+
),
753+
] = None
754+
phone: Annotated[
755+
Mapped[str | None],
756+
Field(description="Primary phone number for customer contact"),
757+
] = None
758+
comment: Annotated[
759+
Mapped[str | None],
760+
Field(description="Internal notes and comments about the customer"),
761+
] = None
762+
currency: Annotated[
763+
Mapped[str | None],
764+
Field(
765+
description="Default currency code for all transactions with this customer"
766+
),
767+
] = None
768+
reference_id: Annotated[
769+
Mapped[str | None],
770+
Field(description="External reference ID for integration with other systems"),
771+
] = None
772+
category: Annotated[
773+
Mapped[str | None],
774+
Field(description="Customer category for segmentation and reporting"),
775+
] = None
776+
discount_rate: Annotated[
777+
Mapped[float | None],
778+
Field(description="Default discount percentage applied to all orders (0-100)"),
779+
] = None
780+
default_billing_id: Annotated[
781+
Mapped[int | None],
782+
Field(description="ID of the default billing address for this customer"),
783+
] = None
784+
default_shipping_id: Annotated[
785+
Mapped[int | None],
786+
Field(description="ID of the default shipping address for this customer"),
787+
] = None
788+
addresses: Annotated[
789+
Mapped[list[CustomerAddress] | None],
790+
SQLField(
791+
sa_column=Column(PydanticJSON),
792+
description="Complete list of billing and shipping addresses for this customer",
793+
),
794+
] = None

0 commit comments

Comments
 (0)