From 12e6984c63d6cb251fdc8a85f5089eb8c0b0fe35 Mon Sep 17 00:00:00 2001 From: Ian Roddis <31021769+iroddis@users.noreply.github.com> Date: Tue, 4 Aug 2026 16:48:00 -0300 Subject: [PATCH] Making objective_lens a thing --- CLAUDE.md | 2 +- .../versions/1b99be8a9b1e_add_fits_tables.py | 15 ++++-- app/api/fits.py | 4 +- app/core/database.py | 1 + app/crud/role.py | 4 +- app/crud/scope_info.py | 6 +-- app/models/scope_info.py | 5 ++ app/schemas/scope_info.py | 1 + app/services/fit_matching.py | 8 ++-- app/services/scope_guide.py | 35 +++++--------- app/services/scope_import.py | 28 ++++------- app/templates/_scopes_table_card.html | 6 +-- .../admin/augmented_part_detail.html | 6 +++ tests/test_fit_matching.py | 42 +++++++++++++---- tests/test_ldap_login.py | 4 +- tests/test_scope_guide.py | 47 +++++-------------- tests/test_scope_import.py | 20 ++++---- 17 files changed, 115 insertions(+), 119 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index aa36866..3abb87a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -87,7 +87,7 @@ The business domain, independent of the auth/RBAC skeleton: - **`app/services/scope_import.py`** is a separate, manually-triggered bulk importer: reads an uploaded `.xlsx` of third-party scopes, upserting by `product_number` (→ `AugmentedPart.sku`) and creating `ScopeModel`, - `ScopeInfo`, up to three `AttachmentPoint`s, and up to two `Fit`s per row + `ScopeInfo`, up to two `AttachmentPoint`s, and up to two `Fit`s per row (see the module docstring for the full column mapping). - **`app/services/scope_guide.py`** assembles the `/scopes` page: one row per Scope-type `AugmentedPart`, one column per other `PartType`, each cell diff --git a/alembic/versions/1b99be8a9b1e_add_fits_tables.py b/alembic/versions/1b99be8a9b1e_add_fits_tables.py index 6005c88..b65e432 100644 --- a/alembic/versions/1b99be8a9b1e_add_fits_tables.py +++ b/alembic/versions/1b99be8a9b1e_add_fits_tables.py @@ -31,10 +31,16 @@ def upgrade() -> None: nullable=False, ), sa.Column( - "created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False + "created_at", + sa.DateTime(timezone=True), + server_default=sa.func.now(), + nullable=False, ), sa.Column( - "updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False + "updated_at", + sa.DateTime(timezone=True), + server_default=sa.func.now(), + nullable=False, ), sa.ForeignKeyConstraint( ["attachment_point_a_id"], ["attachment_points.id"], ondelete="CASCADE" @@ -59,7 +65,10 @@ def upgrade() -> None: sa.Column("username", sa.String(length=255), nullable=False), sa.Column("body", sa.Text(), nullable=False), sa.Column( - "created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False + "created_at", + sa.DateTime(timezone=True), + server_default=sa.func.now(), + nullable=False, ), sa.ForeignKeyConstraint(["fit_id"], ["fits.id"], ondelete="CASCADE"), sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="SET NULL"), diff --git a/app/api/fits.py b/app/api/fits.py index 545f19f..6d30885 100644 --- a/app/api/fits.py +++ b/app/api/fits.py @@ -67,9 +67,7 @@ async def create_fit( if ap_a is None or ap_b is None: raise HTTPException(status_code=404, detail="Attachment point not found") if ap_a.augmented_part_id == ap_b.augmented_part_id: - raise HTTPException( - status_code=422, detail="a part cannot fit itself" - ) + raise HTTPException(status_code=422, detail="a part cannot fit itself") existing = await fit_crud.get_by_attachment_points(db, ap_a, ap_b) if existing is not None: raise HTTPException(status_code=409, detail="Fit already exists") diff --git a/app/core/database.py b/app/core/database.py index 95397fd..5b86be1 100644 --- a/app/core/database.py +++ b/app/core/database.py @@ -35,6 +35,7 @@ if engine.sync_engine.dialect.name == "sqlite": cursor.execute("PRAGMA busy_timeout=5000") cursor.close() + AsyncSessionLocal = async_sessionmaker( engine, class_=AsyncSession, diff --git a/app/crud/role.py b/app/crud/role.py index e07fc51..258f013 100644 --- a/app/crud/role.py +++ b/app/crud/role.py @@ -23,9 +23,7 @@ async def get_default(db: AsyncSession) -> Role | None: async def set_default(db: AsyncSession, role: Role) -> Role: """Make ``role`` the sole default, clearing the flag on every other role.""" - await db.execute( - sa_update(Role).where(Role.id != role.id).values(is_default=False) - ) + await db.execute(sa_update(Role).where(Role.id != role.id).values(is_default=False)) role.is_default = True await db.commit() await db.refresh(role) diff --git a/app/crud/scope_info.py b/app/crud/scope_info.py index 54cd901..59b8112 100644 --- a/app/crud/scope_info.py +++ b/app/crud/scope_info.py @@ -24,9 +24,7 @@ async def list_by_augmented_parts( if not augmented_part_ids: return [] result = await db.execute( - select(ScopeInfo).where( - ScopeInfo.augmented_part_id.in_(augmented_part_ids) - ) + select(ScopeInfo).where(ScopeInfo.augmented_part_id.in_(augmented_part_ids)) ) return list(result.scalars().all()) @@ -38,6 +36,7 @@ async def create( magnification_min: float | None = None, magnification_max: float | None = None, model_suffix: str | None = None, + objective_lens_diameter_mm: float | None = None, objective_thread_pitch: str | None = None, ) -> ScopeInfo: info = ScopeInfo( @@ -45,6 +44,7 @@ async def create( magnification_min=magnification_min, magnification_max=magnification_max, model_suffix=model_suffix, + objective_lens_diameter_mm=objective_lens_diameter_mm, objective_thread_pitch=objective_thread_pitch, ) db.add(info) diff --git a/app/models/scope_info.py b/app/models/scope_info.py index abc9d29..7167766 100644 --- a/app/models/scope_info.py +++ b/app/models/scope_info.py @@ -27,6 +27,11 @@ class ScopeInfo(Base): model_suffix: Mapped[str | None] = mapped_column( String, nullable=True, default=None ) + # The lens diameter is a descriptive attribute (the "50" in "3-18x50"), not a + # connection dimension — it has no AttachmentPoint. + objective_lens_diameter_mm: Mapped[float | None] = mapped_column( + Float, nullable=True, default=None + ) # Kept as the raw string from the sheet (e.g. "0.75", "1/16") so no precision # or notation is lost; the parsed thread lives on its own AttachmentPoint. objective_thread_pitch: Mapped[str | None] = mapped_column( diff --git a/app/schemas/scope_info.py b/app/schemas/scope_info.py index 3b6cfa2..ffa359f 100644 --- a/app/schemas/scope_info.py +++ b/app/schemas/scope_info.py @@ -5,6 +5,7 @@ class ScopeInfoBase(BaseModel): magnification_min: float | None = None magnification_max: float | None = None model_suffix: str | None = None + objective_lens_diameter_mm: float | None = None objective_thread_pitch: str | None = None diff --git a/app/services/fit_matching.py b/app/services/fit_matching.py index 7d56775..da79916 100644 --- a/app/services/fit_matching.py +++ b/app/services/fit_matching.py @@ -142,9 +142,7 @@ async def _reconcile( for own_ap in own_points: for other_ap in points_by_part.get(other_id, []): if attachment_points_match(own_ap, other_ap): - candidate_pairs.add( - _canonical_pair(own_ap.id, other_ap.id) - ) + candidate_pairs.add(_canonical_pair(own_ap.id, other_ap.id)) result = RecomputeResult() @@ -171,7 +169,9 @@ async def _reconcile( return result -async def recompute_for_part(db: AsyncSession, augmented_part_id: int) -> RecomputeResult: +async def recompute_for_part( + db: AsyncSession, augmented_part_id: int +) -> RecomputeResult: """Reconcile Possible fits involving this part against the current algorithm. Creates Possible fits for newly-matching attachment point pairs, and removes diff --git a/app/services/scope_guide.py b/app/services/scope_guide.py index e720423..893b2bd 100644 --- a/app/services/scope_guide.py +++ b/app/services/scope_guide.py @@ -7,7 +7,6 @@ the parts of that type that fit the row's scope. from sqlalchemy.ext.asyncio import AsyncSession -from app.crud import attachment_point as attachment_point_crud from app.crud import augmented_part as augmented_part_crud from app.crud import fit as fit_crud from app.crud import part_type as part_type_crud @@ -15,7 +14,6 @@ from app.crud import scope_info as scope_info_crud from app.models.fit import FitLabel SCOPE_TYPE_NAME = "Scope" -OBJECTIVE_ATTACHMENT_POINT_NAME = "Objective" # "exact" outranks "possible" when the same part fits a scope via several fits. _LABEL_RANK = {FitLabel.EXACT.value: 2, FitLabel.POSSIBLE.value: 1} @@ -45,7 +43,6 @@ async def build( { "scope": AugmentedPart, "info": ScopeInfo | None, - "objective_diameter_mm": float | None, "cells": {part_type_id: [{"part": AugmentedPart, "label": str}]}, }, ..., @@ -54,7 +51,9 @@ async def build( """ part_types = await part_type_crud.list_all(db, limit=1000) scope_type = next((pt for pt in part_types if pt.name == SCOPE_TYPE_NAME), None) - column_types = [pt for pt in part_types if scope_type is None or pt.id != scope_type.id] + column_types = [ + pt for pt in part_types if scope_type is None or pt.id != scope_type.id + ] if scope_type is None: return {"column_types": column_types, "rows": []} @@ -72,7 +71,9 @@ async def build( if not scope_ids: return {"column_types": column_types, "rows": []} - fits = await fit_crud.list_for_parts(db, list(scope_ids), exclude_label=FitLabel.NONE) + fits = await fit_crud.list_for_parts( + db, list(scope_ids), exclude_label=FitLabel.NONE + ) # Resolve the "other" (non-scope) part on each fit in a single batched query. other_ids: set[int] = set() @@ -80,28 +81,15 @@ async def build( scope_id, other_id = _scope_and_other(fit, scope_ids) if scope_id is not None and other_id not in scope_ids: other_ids.add(other_id) - other_parts = {p.id: p for p in await augmented_part_crud.get_many(db, list(other_ids))} + other_parts = { + p.id: p for p in await augmented_part_crud.get_many(db, list(other_ids)) + } infos = { info.augmented_part_id: info for info in await scope_info_crud.list_by_augmented_parts(db, list(scope_ids)) } - # The lens outside diameter lives on the "Objective" attachment point's - # StaticConnection, not on ScopeInfo (which only carries the objective - # *thread* spec) — see app/services/scope_import.py's column-mapping doc. - objective_diameters: dict[int, float] = {} - objective_aps = await attachment_point_crud.list_by_augmented_parts_and_name( - db, list(scope_ids), OBJECTIVE_ATTACHMENT_POINT_NAME - ) - for ap in objective_aps: - if ap.static_connection is not None: - objective_diameters[ap.augmented_part_id] = ap.static_connection.od_mm - elif ap.iso_thread is not None: - objective_diameters[ap.augmented_part_id] = ap.iso_thread.diameter_mm - elif ap.unc_thread is not None: - objective_diameters[ap.augmented_part_id] = float(ap.unc_thread.diameter_fraction) - # cell_map[scope_id][type_id][other_part_id] -> {"part", "label"}, dedup'd so a # part appears once per cell, keeping its strongest fit label. cell_map: dict[int, dict[int, dict[int, dict]]] = {sid: {} for sid in scope_ids} @@ -121,7 +109,9 @@ async def build( rows = [] for scope in scopes: cells = { - type_id: sorted(entries.values(), key=lambda e: e["part"].description.lower()) + type_id: sorted( + entries.values(), key=lambda e: e["part"].description.lower() + ) for type_id, entries in cell_map[scope.id].items() } if fits_part_type_id is not None and fits_part_type_id not in cells: @@ -130,7 +120,6 @@ async def build( { "scope": scope, "info": infos.get(scope.id), - "objective_diameter_mm": objective_diameters.get(scope.id), "cells": cells, } ) diff --git a/app/services/scope_import.py b/app/services/scope_import.py index 26a0e33..3ccd897 100644 --- a/app/services/scope_import.py +++ b/app/services/scope_import.py @@ -2,7 +2,7 @@ The spreadsheet describes parts of PartType "Scope". Each row maps onto an AugmentedPart (keyed on ``product_number`` → ``sku``, so re-importing upserts), -a ScopeModel, a ScopeInfo record for the scope-specific fields, up to three +a ScopeModel, a ScopeInfo record for the scope-specific fields, up to two attachment points, and up to two Fits against other parts. Column → destination @@ -14,9 +14,9 @@ model_suffix → ScopeInfo.model_suffix (+ description) sub_part_type → SubPartType (get-or-create under "Scope") product_number → AugmentedPart.sku (also the upsert / fit-lookup key) magnification_min / _max → ScopeInfo +objective_lens_diameter_mm → ScopeInfo (descriptive only, no attachment point) objective_thread_pitch → ScopeInfo ocular_outer_diameter_mm → StaticConnection + AttachmentPoint "Ocular" -objective_lens_outside_diameter_mm → StaticConnection + AttachmentPoint "Objective" objective_thread → ISO/UNC thread + AttachmentPoint "Objective" ocular_fit → Fit: scope's Ocular AP ↔ referenced part's first AP objective_fit → Fit: scope's Objective AP ↔ referenced part's first AP @@ -68,7 +68,7 @@ EXPECTED_COLUMNS = [ "model", "magnification_min", "magnification_max", - "objective_lens_outside_diameter_mm", + "objective_lens_diameter_mm", "model_suffix", "sub_part_type", "product_number", @@ -330,6 +330,7 @@ async def import_scopes(db: AsyncSession, rows: list[dict]) -> ScopeImportResult magnification_min=_num(row.get("magnification_min")), magnification_max=_num(row.get("magnification_max")), model_suffix=model_suffix, + objective_lens_diameter_mm=_num(row.get("objective_lens_diameter_mm")), objective_thread_pitch=_text(row.get("objective_thread_pitch")), ) @@ -350,34 +351,23 @@ async def import_scopes(db: AsyncSession, rows: list[dict]) -> ScopeImportResult od_mm=ocular_od, ) - objective_static_ap = None - objective_od = _num(row.get("objective_lens_outside_diameter_mm")) - if objective_od is not None: - objective_static_ap = await _static_ap( - db, - augmented_part_id=part.id, - label=OBJECTIVE_LABEL, - od_mm=objective_od, - ) - + # The objective attachment point comes from the thread alone — the + # lens diameter is a descriptive attribute, not a connection. objective_thread_spec = _text(row.get("objective_thread")) - objective_thread_ap = None + objective_ap = None if objective_thread_spec: - objective_thread_ap = await _thread_ap( + objective_ap = await _thread_ap( db, augmented_part_id=part.id, label=OBJECTIVE_LABEL, spec=objective_thread_spec, ) - if objective_thread_ap is None: + if objective_ap is None: result.messages.append( f"could not parse objective_thread " f"'{objective_thread_spec}'" ) - # For objective_fit prefer the lens-OD point, falling back to the thread. - objective_ap = objective_static_ap or objective_thread_ap - staged.append((row, ocular_ap, objective_ap, part.id, result)) row_results.append(result) except Exception as exc: # noqa: BLE001 - report, don't abort the batch diff --git a/app/templates/_scopes_table_card.html b/app/templates/_scopes_table_card.html index 8245507..5f97cdc 100644 --- a/app/templates/_scopes_table_card.html +++ b/app/templates/_scopes_table_card.html @@ -8,10 +8,10 @@ {{ mag(info.magnification_max) }} {%- endif -%} {%- endmacro -%} -{%- macro magnification(info, objective_diameter_mm) -%} +{%- macro magnification(info) -%} {%- set range = magnification_range(info) -%} {%- if range -%} - {{ range }}x{% if objective_diameter_mm is not none %}{{ mag(objective_diameter_mm) }}{% endif %} + {{ range }}x{% if info.objective_lens_diameter_mm is not none %}{{ mag(info.objective_lens_diameter_mm) }}{% endif %} {%- else -%} — {%- endif -%} @@ -43,7 +43,7 @@ {{ scope.manufacturer.name if scope.manufacturer else "—" }} {{ scope.scope_model.name if scope.scope_model else "—" }} {{ scope.sub_part_type.name if scope.sub_part_type else "—" }} - {{ magnification(info, row.objective_diameter_mm) }} + {{ magnification(info) }} {% if info and info.objective_thread_pitch %} {{ info.objective_thread_pitch }} diff --git a/app/templates/admin/augmented_part_detail.html b/app/templates/admin/augmented_part_detail.html index 14834d3..e60a6fe 100644 --- a/app/templates/admin/augmented_part_detail.html +++ b/app/templates/admin/augmented_part_detail.html @@ -44,6 +44,12 @@ {% else %}—{% endif %}
Model suffix
{{ scope_info.model_suffix or "—" }}
+
Objective lens diameter
+
+ {% if scope_info.objective_lens_diameter_mm is not none %} + {{ scope_info.objective_lens_diameter_mm }} mm + {% else %}—{% endif %} +
Objective thread
{{ scope_info.objective_thread_pitch or "—" }}
diff --git a/tests/test_fit_matching.py b/tests/test_fit_matching.py index ef862a6..6659247 100644 --- a/tests/test_fit_matching.py +++ b/tests/test_fit_matching.py @@ -34,20 +34,40 @@ def test_same_label_never_matches_even_with_identical_threads(): def test_unc_thread_exact_match(): - a = _ap(1, 1, unc_thread=UncThread(diameter_fraction="1/4", tpi=20, series=UncSeries.UNC)) - b = _ap(2, 2, unc_thread=UncThread(diameter_fraction="1/4", tpi=20, series=UncSeries.UNC)) + a = _ap( + 1, + 1, + unc_thread=UncThread(diameter_fraction="1/4", tpi=20, series=UncSeries.UNC), + ) + b = _ap( + 2, + 2, + unc_thread=UncThread(diameter_fraction="1/4", tpi=20, series=UncSeries.UNC), + ) assert attachment_points_match(a, b) def test_unc_thread_series_mismatch(): - a = _ap(1, 1, unc_thread=UncThread(diameter_fraction="1/4", tpi=20, series=UncSeries.UNC)) - b = _ap(2, 2, unc_thread=UncThread(diameter_fraction="1/4", tpi=20, series=UncSeries.UNF)) + a = _ap( + 1, + 1, + unc_thread=UncThread(diameter_fraction="1/4", tpi=20, series=UncSeries.UNC), + ) + b = _ap( + 2, + 2, + unc_thread=UncThread(diameter_fraction="1/4", tpi=20, series=UncSeries.UNF), + ) assert not attachment_points_match(a, b) def test_iso_and_unc_never_match_each_other(): a = _ap(1, 1, iso_thread=IsoThread(diameter_mm=8, pitch_mm=1.25)) - b = _ap(2, 2, unc_thread=UncThread(diameter_fraction="1/4", tpi=20, series=UncSeries.UNC)) + b = _ap( + 2, + 2, + unc_thread=UncThread(diameter_fraction="1/4", tpi=20, series=UncSeries.UNC), + ) assert not attachment_points_match(a, b) @@ -94,9 +114,15 @@ def test_two_bare_attachment_points_never_match(): def test_parts_could_fit_requires_different_types_and_identity(): - a = AugmentedPart(id=1, part_type_id=1, description="a", colour_id=1, manufacturer_id=1) - b = AugmentedPart(id=2, part_type_id=1, description="b", colour_id=1, manufacturer_id=1) - c = AugmentedPart(id=3, part_type_id=2, description="c", colour_id=1, manufacturer_id=1) + a = AugmentedPart( + id=1, part_type_id=1, description="a", colour_id=1, manufacturer_id=1 + ) + b = AugmentedPart( + id=2, part_type_id=1, description="b", colour_id=1, manufacturer_id=1 + ) + c = AugmentedPart( + id=3, part_type_id=2, description="c", colour_id=1, manufacturer_id=1 + ) assert not parts_could_fit(a, b) assert parts_could_fit(a, c) assert not parts_could_fit(a, a) diff --git a/tests/test_ldap_login.py b/tests/test_ldap_login.py index 42794d1..4873a77 100644 --- a/tests/test_ldap_login.py +++ b/tests/test_ldap_login.py @@ -80,9 +80,7 @@ async def test_ldap_login_is_idempotent( assert await user_crud.count(db) == 1 -async def test_ldap_login_bad_credentials( - client, seeded, monkeypatch, ldap_enabled -): +async def test_ldap_login_bad_credentials(client, seeded, monkeypatch, ldap_enabled): async def fake_authenticate(username, password): return None diff --git a/tests/test_scope_guide.py b/tests/test_scope_guide.py index d9f43c4..8dc6401 100644 --- a/tests/test_scope_guide.py +++ b/tests/test_scope_guide.py @@ -1,12 +1,9 @@ import pytest -from app.models.attachment_point import AttachmentPoint -from app.models.attachment_point_name import AttachmentPointName from app.models.augmented_part import AugmentedPart from app.models.fit import Fit, FitLabel from app.models.part_type import PartType from app.models.scope_info import ScopeInfo -from app.models.static_connection import StaticConnection from app.models.sub_part_type import SubPartType from app.services import scope_guide @@ -34,7 +31,9 @@ async def _seed_guide_data(db): db.add(p) return p - s1 = part("Alpha 3-18x50", scope_t.id, manufacturer_id=10, sub_part_type_id=rifle.id) + s1 = part( + "Alpha 3-18x50", scope_t.id, manufacturer_id=10, sub_part_type_id=rifle.id + ) s2 = part("Zeta 1-6x24", scope_t.id, manufacturer_id=20, sub_part_type_id=reddot.id) fc1 = part("Flip cover 50mm", cover_t.id) a1 = part("ARD 50mm", ard_t.id) @@ -46,6 +45,7 @@ async def _seed_guide_data(db): augmented_part_id=s1.id, magnification_min=3, magnification_max=18, + objective_lens_diameter_mm=50, objective_thread_pitch="M52x0.75", ) ) @@ -172,9 +172,7 @@ async def test_build_filters_by_required_fit_type(db_sessionmaker): async def _web_login(client, email, password="secret123"): - resp = await client.post( - "/login", data={"email": email, "password": password} - ) + resp = await client.post("/login", data={"email": email, "password": password}) assert resp.status_code == 303 @@ -206,45 +204,22 @@ async def test_scopes_page_renders_rows_and_fits( @pytest.mark.asyncio -async def test_build_includes_objective_diameter(db_sessionmaker): +async def test_build_includes_objective_lens_diameter(db_sessionmaker): async with db_sessionmaker() as db: - ids = await _seed_guide_data(db) - apn = AttachmentPointName(name="Objective") - db.add(apn) - await db.flush() - db.add( - AttachmentPoint( - augmented_part_id=ids["s1"].id, - attachment_point_name_id=apn.id, - static_connection=StaticConnection(od_mm=50, od_in=1.9685), - ) - ) - await db.commit() - + await _seed_guide_data(db) guide = await scope_guide.build(db) rows = {r["scope"].description: r for r in guide["rows"]} - assert rows["Alpha 3-18x50"]["objective_diameter_mm"] == 50 - assert rows["Zeta 1-6x24"]["objective_diameter_mm"] is None + assert rows["Alpha 3-18x50"]["info"].objective_lens_diameter_mm == 50 + assert rows["Zeta 1-6x24"]["info"] is None @pytest.mark.asyncio -async def test_scopes_page_renders_objective_diameter_in_magnification( +async def test_scopes_page_renders_objective_lens_diameter_in_magnification( client, seeded, make_user, db_sessionmaker ): async with db_sessionmaker() as db: - ids = await _seed_guide_data(db) - apn = AttachmentPointName(name="Objective") - db.add(apn) - await db.flush() - db.add( - AttachmentPoint( - augmented_part_id=ids["s1"].id, - attachment_point_name_id=apn.id, - static_connection=StaticConnection(od_mm=50, od_in=1.9685), - ) - ) - await db.commit() + await _seed_guide_data(db) await make_user("reader4@example.com", role_ids=[seeded["roles"]["user"].id]) await _web_login(client, "reader4@example.com") resp = await client.get("/scopes") diff --git a/tests/test_scope_import.py b/tests/test_scope_import.py index e4a1438..d758297 100644 --- a/tests/test_scope_import.py +++ b/tests/test_scope_import.py @@ -19,7 +19,7 @@ HEADERS = [ "model", "magnification_min", "magnification_max", - "objective_lens_outside_diameter_mm", + "objective_lens_diameter_mm", "model_suffix", "sub_part_type", "product_number", @@ -52,7 +52,7 @@ def _scope_row(**overrides): "model": "Viper", "magnification_min": 5, "magnification_max": 25, - "objective_lens_outside_diameter_mm": 56, + "objective_lens_diameter_mm": 56, "model_suffix": "PST", "sub_part_type": "Riflescope", "product_number": "SCOPE-A", @@ -141,6 +141,7 @@ async def test_import_creates_scope_with_info_and_attachment_points(db_sessionma assert info.magnification_min == 5 assert info.magnification_max == 25 assert info.model_suffix == "PST" + assert info.objective_lens_diameter_mm == 56 assert info.objective_thread_pitch == "0.75" aps = await attachment_point_crud.list_for_augmented_part(db, part.id) @@ -151,14 +152,13 @@ async def test_import_creates_scope_with_info_and_attachment_points(db_sessionma assert len(by_label["Ocular"]) == 1 assert by_label["Ocular"][0].static_connection.od_mm == 40 + # The lens diameter is descriptive only: the sole Objective point is the + # thread. objectives = by_label["Objective"] - assert len(objectives) == 2 # static OD + thread - static_obj = [a for a in objectives if a.static_connection is not None] - thread_obj = [a for a in objectives if a.iso_thread is not None] - assert static_obj and static_obj[0].static_connection.od_mm == 56 - assert thread_obj - assert thread_obj[0].iso_thread.diameter_mm == 40 - assert thread_obj[0].iso_thread.pitch_mm == 0.75 + assert len(objectives) == 1 + assert objectives[0].static_connection is None + assert objectives[0].iso_thread.diameter_mm == 40 + assert objectives[0].iso_thread.pitch_mm == 0.75 async def test_import_resolves_ocular_and_objective_fits(db_sessionmaker): @@ -200,7 +200,7 @@ async def test_import_upserts_on_product_number(db_sessionmaker): # Attachment points were rebuilt, not duplicated. aps = await attachment_point_crud.list_for_augmented_part(db, matching[0].id) - assert len(aps) == 3 + assert len(aps) == 2 async def test_row_without_product_number_is_skipped(db_sessionmaker):