Adding autovivify aps

This commit is contained in:
Ian Roddis committed 2026-08-18 16:33:23 -03:00
1 parent 34d538f9a0
commit 7e246be39c
2 files changed
+85 -9

No files matched your search

+36 -9
View File
@@ -25,8 +25,11 @@ and colour defaults to an "Unknown" PartColour, since the sheet carries neither.
Fits are created with label POSSIBLE. The referenced part is looked up by Fits are created with label POSSIBLE. The referenced part is looked up by
``product_number`` and linked to its first attachment point (chosen answer: ``product_number`` and linked to its first attachment point (chosen answer:
"sole/first AP"). Both directions are resolved in a second pass so a fit between "sole/first AP"). If the referenced part has no attachment points at all, one
two scopes in the same spreadsheet resolves regardless of row order. is created for it that mirrors the scope's attachment point's connection, so
the fit has something valid to attach to. Both directions are resolved in a
second pass so a fit between two scopes in the same spreadsheet resolves
regardless of row order.
Bad rows (missing key, unknown fit target, unparseable thread) are skipped and Bad rows (missing key, unknown fit target, unparseable thread) are skipped and
reported per-row; good rows are committed as they go. reported per-row; good rows are committed as they go.
@@ -186,6 +189,24 @@ async def _thread_ap(db: AsyncSession, *, augmented_part_id: int, label: str, sp
) )
async def _mirrored_ap(db: AsyncSession, *, augmented_part_id: int, scope_ap):
"""Create an attachment point mirroring ``scope_ap``'s connection.
Used when the referenced part has no attachment point that already fits
the scope, so the explicit ocular_fit/objective_fit link has something
valid to attach to.
"""
return await attachment_point_crud.create(
db,
augmented_part_id=augmented_part_id,
attachment_point_name_id=scope_ap.attachment_point_name_id,
iso_thread_id=scope_ap.iso_thread_id,
unc_thread_id=scope_ap.unc_thread_id,
clamp_connection_id=scope_ap.clamp_connection_id,
static_connection_id=scope_ap.static_connection_id,
)
async def _link_fit( async def _link_fit(
db: AsyncSession, db: AsyncSession,
*, *,
@@ -195,7 +216,11 @@ async def _link_fit(
scope_part_id: int, scope_part_id: int,
messages: list[str], messages: list[str],
) -> None: ) -> None:
"""Create a POSSIBLE fit from the scope's AP to the referenced part's first AP.""" """Create a POSSIBLE fit from the scope's AP to the referenced part's first AP.
If the referenced part has no attachment points at all, one is created
mirroring the scope's attachment point's connection.
"""
if scope_ap is None: if scope_ap is None:
messages.append( messages.append(
f"{scope_role}_fit '{target_sku}' skipped: scope has no {scope_role} attachment point" f"{scope_role}_fit '{target_sku}' skipped: scope has no {scope_role} attachment point"
@@ -209,12 +234,14 @@ async def _link_fit(
messages.append(f"{scope_role}_fit '{target_sku}' skipped: self-reference") messages.append(f"{scope_role}_fit '{target_sku}' skipped: self-reference")
return return
target_aps = await attachment_point_crud.list_for_augmented_part(db, target.id) target_aps = await attachment_point_crud.list_for_augmented_part(db, target.id)
if not target_aps: if target_aps:
messages.append(
f"{scope_role}_fit '{target_sku}' skipped: referenced part has no attachment points"
)
return
target_ap = target_aps[0] target_ap = target_aps[0]
else:
target_ap = await _mirrored_ap(db, augmented_part_id=target.id, scope_ap=scope_ap)
messages.append(
f"{scope_role}_fit '{target_sku}': referenced part had no attachment points, "
"created one matching the scope's attachment point"
)
existing = await fit_crud.get_by_attachment_points(db, scope_ap, target_ap) existing = await fit_crud.get_by_attachment_points(db, scope_ap, target_ap)
if existing is None: if existing is None:
await fit_crud.create(db, ap_a=scope_ap, ap_b=target_ap, label=FitLabel.POSSIBLE) await fit_crud.create(db, ap_a=scope_ap, ap_b=target_ap, label=FitLabel.POSSIBLE)
@@ -253,7 +280,7 @@ async def import_scopes(db: AsyncSession, rows: list[dict]) -> ScopeImportResult
if objective_lens_diameter_mm: if objective_lens_diameter_mm:
default_product_number += f"x{objective_lens_diameter_mm}" default_product_number += f"x{objective_lens_diameter_mm}"
if model_suffix: if model_suffix:
default_product_number += " {model_suffix}" default_product_number += f" {model_suffix}"
product_number = _text(row.get("product_number")) or default_product_number product_number = _text(row.get("product_number")) or default_product_number
result = ScopeImportRowResult( result = ScopeImportRowResult(
row=row_num, product_number=product_number, status="skipped", messages=[] row=row_num, product_number=product_number, status="skipped", messages=[]
+49
View File
@@ -235,6 +235,55 @@ async def test_unknown_fit_reference_is_reported(db_sessionmaker):
assert await fit_crud.list_for_part(db, part.id) == [] assert await fit_crud.list_for_part(db, part.id) == []
async def _make_bare_part(db, sku):
"""A non-scope part with no attachment points at all."""
colour = await part_colour_crud.get_by_name(db, "Black")
if colour is None:
colour = await part_colour_crud.create(db, name="Black")
manu = await manufacturer_crud.get_by_name(db, "Armament")
if manu is None:
manu = await manufacturer_crud.create(db, name="Armament", url="")
pt = await part_type_crud.get_by_name(db, "Flip Cover")
if pt is None:
pt = await part_type_crud.create(db, name="Flip Cover")
return await augmented_part_crud.create(
db,
sku=sku,
description=f"Accessory {sku}",
colour_id=colour.id,
manufacturer_id=manu.id,
scope_model_id=None,
part_type_id=pt.id,
sub_part_type_id=None,
)
async def test_fit_reference_without_attachment_points_creates_matching_one(
db_sessionmaker,
):
async with db_sessionmaker() as db:
await _make_bare_part(db, "ACC-BARE")
data = _workbook([_scope_row(ocular_fit="ACC-BARE")])
result = await scope_import_service.import_from_bytes(db, data)
assert result.created == 1
msgs = " ".join(result.rows[0].messages)
assert "created one matching the scope's attachment point" in msgs
acc = await augmented_part_crud.get_by_sku(db, "ACC-BARE")
acc_aps = await attachment_point_crud.list_for_augmented_part(db, acc.id)
assert len(acc_aps) == 1
assert acc_aps[0].static_connection is not None
assert acc_aps[0].static_connection.od_mm == 40
assert acc_aps[0].attachment_point_name.name == "Ocular"
part = await augmented_part_crud.get_by_sku(db, "SCOPE-A")
fits = await fit_crud.list_for_part(db, part.id)
assert len(fits) == 1
linked_ap_ids = {fits[0].attachment_point_a_id, fits[0].attachment_point_b_id}
assert acc_aps[0].id in linked_ap_ids
async def test_unc_objective_thread_creates_unc_attachment_point(db_sessionmaker): async def test_unc_objective_thread_creates_unc_attachment_point(db_sessionmaker):
async with db_sessionmaker() as db: async with db_sessionmaker() as db:
data = _workbook([_scope_row(objective_thread="1.750-32 UNS")]) data = _workbook([_scope_row(objective_thread="1.750-32 UNS")])