diff --git a/app/services/augmented_part_sync.py b/app/services/augmented_part_sync.py index a77b198..5fcd9cd 100644 --- a/app/services/augmented_part_sync.py +++ b/app/services/augmented_part_sync.py @@ -38,6 +38,7 @@ from app.crud import manufacturer as manufacturer_crud from app.crud import part_colour as part_colour_crud from app.crud import part_type as part_type_crud from app.crud import static_connection as static_connection_crud +from app.crud import clamp_connection as clamp_connection_crud from app.crud import unc_thread as unc_thread_crud from app.models.associations import augmented_part_imported_parts from app.models.augmented_part import AugmentedPart @@ -70,6 +71,11 @@ _UNC_THREAD_RE = re.compile( re.IGNORECASE, ) +_CLAMP_RANGE_RE = re.compile( + r"\((?P\d*\.?\d+)\s*-\s*(?P\d*\.?\d+)\s*mm\)", + re.IGNORECASE, +) + def _parse_float(value: str | None) -> float | None: if value is None: @@ -101,6 +107,13 @@ def _parse_thread_attribute(value: str) -> tuple[str, dict] | None: "series": series, } + if m := _CLAMP_RANGE_RE.match(value): + # Source data often omits the series letters; UNS ("unified special") is the + # correct designation when the diameter/TPI pairing isn't a standard pitch. + return "clamp", { + "min_diametere": float(m.group("min_diameter")), + "max_diameter": float(m.group("max_diameter")), + } return None @@ -111,7 +124,9 @@ async def _get_or_create_attachment_point_name(db: AsyncSession, name: str): return apn -async def _add_attachment_point(db: AsyncSession, ap: AugmentedPart, ip: ImportedPart) -> None: +async def _add_attachment_point( + db: AsyncSession, ap: AugmentedPart, ip: ImportedPart +) -> None: """Infer and create an attachment point for a newly-synced AugmentedPart. Preference order: TBXThreadAttribute (ISO/UNC thread) then ProductODmm/in @@ -131,7 +146,9 @@ async def _add_attachment_point(db: AsyncSession, ap: AugmentedPart, ip: Importe if kind == "iso": depth_mm = _parse_float(ip.ProductMaxThreadDepthmm) - thread = await iso_thread_crud.get_by_values(db, depth_mm=depth_mm, **fields) + thread = await iso_thread_crud.get_by_values( + db, depth_mm=depth_mm, **fields + ) if thread is None: thread = await iso_thread_crud.create(db, depth_mm=depth_mm, **fields) await attachment_point_crud.create( @@ -140,9 +157,12 @@ async def _add_attachment_point(db: AsyncSession, ap: AugmentedPart, ip: Importe attachment_point_name_id=apn.id, iso_thread_id=thread.id, ) + return elif kind == "unc": depth_in = _parse_float(ip.ProductMaxThreadDepthin) - thread = await unc_thread_crud.get_by_values(db, depth_in=depth_in, **fields) + thread = await unc_thread_crud.get_by_values( + db, depth_in=depth_in, **fields + ) if thread is None: thread = await unc_thread_crud.create(db, depth_in=depth_in, **fields) await attachment_point_crud.create( @@ -151,9 +171,21 @@ async def _add_attachment_point(db: AsyncSession, ap: AugmentedPart, ip: Importe attachment_point_name_id=apn.id, unc_thread_id=thread.id, ) + return + elif kind == "clamp": + clamp = await clamp_connection_crud.get_by_values(db, **fields) + if clamp is None: + clamp = await clamp_connection_crud.create(db, **fields) + await attachment_point_crud.create( + db, + augmented_part_id=ap.id, + attachment_point_name_id=apn.id, + clamp_connection_id=clamp.id, + ) + return else: + # Unable to determine pass - return od_mm = _parse_float(ip.ProductODmm) od_in = _parse_float(ip.ProductODin) @@ -240,7 +272,9 @@ async def sync(db: AsyncSession) -> AugmentedPartSyncResult: # ------------------------------------------------------------------ phase 1 # IDs referenced in existing associations - result = await db.execute(select(augmented_part_imported_parts.c.imported_part_id).distinct()) + result = await db.execute( + select(augmented_part_imported_parts.c.imported_part_id).distinct() + ) all_linked_ids: set[str] = {row[0] for row in result.all()} # IDs that still exist in imported_parts @@ -254,7 +288,9 @@ async def sync(db: AsyncSession) -> AugmentedPartSyncResult: # Which AugmentedParts are affected? result = await db.execute( select(augmented_part_imported_parts.c.augmented_part_id).where( - augmented_part_imported_parts.c.imported_part_id.in_(orphaned_import_ids) + augmented_part_imported_parts.c.imported_part_id.in_( + orphaned_import_ids + ) ) ) orphaned_ap_ids: set[int] = {row[0] for row in result.all()} @@ -262,7 +298,9 @@ async def sync(db: AsyncSession) -> AugmentedPartSyncResult: # Remove the stale association rows await db.execute( augmented_part_imported_parts.delete().where( - augmented_part_imported_parts.c.imported_part_id.in_(orphaned_import_ids) + augmented_part_imported_parts.c.imported_part_id.in_( + orphaned_import_ids + ) ) ) await db.flush() @@ -285,7 +323,9 @@ async def sync(db: AsyncSession) -> AugmentedPartSyncResult: # ------------------------------------------------------------------ phase 2 # Re-query linked IDs after phase 1 cleanup - result = await db.execute(select(augmented_part_imported_parts.c.imported_part_id).distinct()) + result = await db.execute( + select(augmented_part_imported_parts.c.imported_part_id).distinct() + ) now_linked_ids: set[str] = {row[0] for row in result.all()} unlinked_ids = current_ids - now_linked_ids @@ -293,7 +333,9 @@ async def sync(db: AsyncSession) -> AugmentedPartSyncResult: skipped = 0 if not unlinked_ids: - return AugmentedPartSyncResult(created=created, deleted=deleted, skipped=skipped) + return AugmentedPartSyncResult( + created=created, deleted=deleted, skipped=skipped + ) result = await db.execute( select(ImportedPart).where(ImportedPart.InventoryID.in_(unlinked_ids)) diff --git a/data/TODO b/data/TODO index 55555f0..8ff96a2 100644 --- a/data/TODO +++ b/data/TODO @@ -2,14 +2,9 @@ - [ ] Add an `archived` attribute that indicates the part should not show up in default searches - [ ] Admins and users with permissions can opt into seeing archived parts - [ ] Add bulk "mark archived" -- [ ] AugmentedPart Details - - [x] Title should be the SKU - - [ ] Add new inference for attachment point "Clamp" - - [ ] Appears as `(44.5-47.5mm)` in the description - [ ] Scope Import - [ ] Test scopes with explicit fits populated -- [ ] Attachment Points - - [ ] Remove objective / occular labels and matching logic + - [ ] Dashboard - [ ] Make this more useful. - [ ] Number of @@ -17,6 +12,10 @@ - [ ] Skipped parts from last Sync - [ ] Handy buttons to run the import / sync / recompute actions +- [x] AugmentedPart Details + - [x] Title should be the SKU + - [x] Add new inference for attachment point "Clamp" + - [x] Appears as `(44.5-47.5mm)` in the description - [x] Better layout for Scopes page - [x] Layout is too wide, requiring side-scrolling @@ -28,5 +27,7 @@ - [x] Part Import - [x] Example query of `ZC5000` should return multiple parts - [x] Filter text should search SKU and description +- [x] Attachment Points + - [x] Remove objective / occular labels and matching logic