Adding automatic attachment point addition

This commit is contained in:
Ian Roddis committed 2026-07-03 09:57:10 -03:00
1 parent c5d1186b95
commit 87ba8cd169
4 files changed
+175 -1

No files matched your search

+20
View File
@@ -8,6 +8,26 @@ async def get(db: AsyncSession, iso_thread_id: int) -> IsoThread | None:
return await db.get(IsoThread, iso_thread_id)
async def get_by_values(
db: AsyncSession,
*,
diameter_mm: float,
pitch_mm: float,
depth_mm: float | None,
) -> IsoThread | None:
depth_clause = (
IsoThread.depth_mm.is_(None) if depth_mm is None else IsoThread.depth_mm == depth_mm
)
result = await db.execute(
select(IsoThread).where(
IsoThread.diameter_mm == diameter_mm,
IsoThread.pitch_mm == pitch_mm,
depth_clause,
)
)
return result.scalar_one_or_none()
async def list_all(db: AsyncSession, skip: int = 0, limit: int = 100) -> list[IsoThread]:
result = await db.execute(
select(IsoThread).order_by(IsoThread.id).offset(skip).limit(limit)
+22
View File
@@ -8,6 +8,28 @@ async def get(db: AsyncSession, unc_thread_id: int) -> UncThread | None:
return await db.get(UncThread, unc_thread_id)
async def get_by_values(
db: AsyncSession,
*,
diameter_fraction: str,
tpi: int,
series: UncSeries,
depth_in: float | None,
) -> UncThread | None:
depth_clause = (
UncThread.depth_in.is_(None) if depth_in is None else UncThread.depth_in == depth_in
)
result = await db.execute(
select(UncThread).where(
UncThread.diameter_fraction == diameter_fraction,
UncThread.tpi == tpi,
UncThread.series == series,
depth_clause,
)
)
return result.scalar_one_or_none()
async def list_all(db: AsyncSession, skip: int = 0, limit: int = 100) -> list[UncThread]:
result = await db.execute(
select(UncThread).order_by(UncThread.id).offset(skip).limit(limit)
+131
View File
@@ -17,20 +17,149 @@ Phase 2 – create entries for new ImportedParts
• Type ← first PartType whose name is a substring of the description
(longest name wins; skipped if no match)
• SubType ← null
• Attachment point ← always named "Occular"; one of:
- ISO/UNC thread parsed from TBXThreadAttribute (using the
matching ProductMaxThreadDepth mm/in if populated), or
- StaticConnection from ProductODmm/ProductODin (whichever
unit is missing is derived via mm/in conversion) if the
thread attribute is absent or unparseable
Skipped entirely if none of those fields are usable.
"""
import re
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.crud import attachment_point as attachment_point_crud
from app.crud import attachment_point_name as attachment_point_name_crud
from app.crud import iso_thread as iso_thread_crud
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 unc_thread as unc_thread_crud
from app.models.associations import augmented_part_imported_parts
from app.models.augmented_part import AugmentedPart
from app.models.imported_part import ImportedPart
from app.models.part_type import PartType
from app.models.unc_thread import UncSeries
from app.schemas.augmented_part import AugmentedPartSyncResult
ATTACHMENT_POINT_NAME = "Occular"
MM_PER_INCH = 25.4
_ISO_THREAD_RE = re.compile(
r"^M\s*(?P<diameter>\d+(?:\.\d+)?)\s*[xX]\s*(?P<pitch>\d+(?:\.\d+)?)$"
)
_UNC_THREAD_RE = re.compile(
r"^(?P<diameter>\d+(?:\.\d+)?(?:/\d+)?)\s*-\s*(?P<tpi>\d+)"
r"\s*(?P<series>UNEF|UNC|UNF|UNS|UN)?$",
re.IGNORECASE,
)
def _parse_float(value: str | None) -> float | None:
if value is None:
return None
value = value.strip()
if not value:
return None
try:
return float(value)
except ValueError:
return None
def _parse_thread_attribute(value: str) -> tuple[str, dict] | None:
"""Parse a TBXThreadAttribute string as an ISO metric or UNC/UNF thread spec."""
m = _ISO_THREAD_RE.match(value)
if m:
return "iso", {
"diameter_mm": float(m.group("diameter")),
"pitch_mm": float(m.group("pitch")),
}
m = _UNC_THREAD_RE.match(value)
if m:
# Source data often omits the series letters; UNS ("unified special") is the
# correct designation when the diameter/TPI pairing isn't a standard pitch.
series = UncSeries((m.group("series") or "UNS").upper())
return "unc", {
"diameter_fraction": m.group("diameter"),
"tpi": int(m.group("tpi")),
"series": series,
}
return None
async def _get_or_create_attachment_point_name(db: AsyncSession, name: str):
apn = await attachment_point_name_crud.get_by_name(db, name)
if apn is None:
apn = await attachment_point_name_crud.create(db, name=name)
return apn
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
(StaticConnection). Falls through to the OD-based connection if the thread
attribute is present but unparseable.
"""
thread_value = (ip.TBXThreadAttribute or "").strip()
parsed = _parse_thread_attribute(thread_value) if thread_value else None
if parsed is not None:
kind, fields = parsed
apn = await _get_or_create_attachment_point_name(db, ATTACHMENT_POINT_NAME)
if kind == "iso":
depth_mm = _parse_float(ip.ProductMaxThreadDepthmm)
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(
db,
augmented_part_id=ap.id,
attachment_point_name_id=apn.id,
iso_thread_id=thread.id,
)
else:
depth_in = _parse_float(ip.ProductMaxThreadDepthin)
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(
db,
augmented_part_id=ap.id,
attachment_point_name_id=apn.id,
unc_thread_id=thread.id,
)
return
od_mm = _parse_float(ip.ProductODmm)
od_in = _parse_float(ip.ProductODin)
if od_mm is None and od_in is None:
return
if od_mm is None:
od_mm = od_in * MM_PER_INCH
elif od_in is None:
od_in = od_mm / MM_PER_INCH
apn = await _get_or_create_attachment_point_name(db, ATTACHMENT_POINT_NAME)
sc = await static_connection_crud.get_by_od_mm(db, od_mm)
if sc is None:
sc = await static_connection_crud.create(db, od_mm=od_mm, od_in=od_in)
await attachment_point_crud.create(
db,
augmented_part_id=ap.id,
attachment_point_name_id=apn.id,
static_connection_id=sc.id,
)
async def sync_one(
db: AsyncSession,
@@ -73,6 +202,7 @@ async def sync_one(
imported_part_id=ip.InventoryID,
)
)
await _add_attachment_point(db, ap, ip)
await db.commit()
return ap
@@ -196,6 +326,7 @@ async def sync(db: AsyncSession) -> AugmentedPartSyncResult:
imported_part_id=ip.InventoryID,
)
)
await _add_attachment_point(db, ap, ip)
created += 1
await db.commit()