Skip to content

Commit c8163c7

Browse files
committed
refactor: consolidate transferer classes to use ChemistryTransferer and remove unused caching logic
1 parent a98e036 commit c8163c7

15 files changed

Lines changed: 312 additions & 516 deletions

core/initializers.py

Lines changed: 107 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,18 @@
1616
from pathlib import Path
1717

1818
from fastapi_pagination import add_pagination
19-
from sqlalchemy import text
19+
from sqlalchemy import text, select
20+
from sqlalchemy.dialects.postgresql import insert
2021
from sqlalchemy.exc import DatabaseError
2122

2223
from db import Base
2324
from db.engine import session_ctx
25+
from db.lexicon import (
26+
LexiconCategory,
27+
LexiconTerm,
28+
LexiconTermCategoryAssociation,
29+
)
2430
from db.parameter import Parameter
25-
from services.lexicon_helper import add_lexicon_term, add_lexicon_category
2631

2732

2833
def init_parameter(path: str = None) -> None:
@@ -77,33 +82,112 @@ def init_lexicon(path: str = None) -> None:
7782

7883
default_lexicon = json.load(f)
7984

80-
# populate lexicon
81-
8285
with session_ctx() as session:
8386
terms = default_lexicon["terms"]
8487
categories = default_lexicon["categories"]
85-
for category in categories:
86-
try:
87-
add_lexicon_category(session, category["name"], category["description"])
88-
except DatabaseError as e:
89-
print(f"Failed to add category {category['name']}: error: {e}")
90-
session.rollback()
91-
continue
92-
93-
for term_dict in terms:
94-
try:
95-
add_lexicon_term(
96-
session,
97-
term_dict["term"],
98-
term_dict["definition"],
99-
term_dict["categories"],
88+
category_names = [category["name"] for category in categories]
89+
existing_categories = dict(
90+
session.execute(
91+
select(LexiconCategory.name, LexiconCategory.id).where(
92+
LexiconCategory.name.in_(category_names)
10093
)
101-
except DatabaseError as e:
102-
print(
103-
f"Failed to add term {term_dict['term']}: {term_dict['definition']} error: {e}"
94+
).all()
95+
)
96+
category_rows = [
97+
{"name": category["name"], "description": category["description"]}
98+
for category in categories
99+
if category["name"] not in existing_categories
100+
]
101+
if category_rows:
102+
session.execute(
103+
insert(LexiconCategory)
104+
.values(category_rows)
105+
.on_conflict_do_nothing(index_elements=["name"])
106+
)
107+
session.commit()
108+
existing_categories = dict(
109+
session.execute(
110+
select(LexiconCategory.name, LexiconCategory.id).where(
111+
LexiconCategory.name.in_(category_names)
112+
)
113+
).all()
114+
)
115+
116+
term_names = [term_dict["term"] for term_dict in terms]
117+
existing_terms = dict(
118+
session.execute(
119+
select(LexiconTerm.term, LexiconTerm.id).where(
120+
LexiconTerm.term.in_(term_names)
121+
)
122+
).all()
123+
)
124+
term_rows = [
125+
{"term": term_dict["term"], "definition": term_dict["definition"]}
126+
for term_dict in terms
127+
if term_dict["term"] not in existing_terms
128+
]
129+
if term_rows:
130+
session.execute(
131+
insert(LexiconTerm)
132+
.values(term_rows)
133+
.on_conflict_do_nothing(index_elements=["term"])
134+
)
135+
session.commit()
136+
existing_terms = dict(
137+
session.execute(
138+
select(LexiconTerm.term, LexiconTerm.id).where(
139+
LexiconTerm.term.in_(term_names)
140+
)
141+
).all()
142+
)
143+
144+
term_ids = [existing_terms.get(term_name) for term_name in term_names]
145+
category_ids = [
146+
existing_categories.get(category_name) for category_name in category_names
147+
]
148+
existing_links = set()
149+
if term_ids and category_ids:
150+
existing_links = set(
151+
session.execute(
152+
select(
153+
LexiconTermCategoryAssociation.term_id,
154+
LexiconTermCategoryAssociation.category_id,
155+
).where(
156+
LexiconTermCategoryAssociation.term_id.in_(
157+
[term_id for term_id in term_ids if term_id is not None]
158+
),
159+
LexiconTermCategoryAssociation.category_id.in_(
160+
[
161+
category_id
162+
for category_id in category_ids
163+
if category_id is not None
164+
]
165+
),
166+
)
167+
).all()
168+
)
169+
170+
association_rows = []
171+
for term_dict in terms:
172+
term_id = existing_terms.get(term_dict["term"])
173+
if term_id is None:
174+
continue
175+
for category in term_dict["categories"]:
176+
category_id = existing_categories.get(category)
177+
if category_id is None:
178+
continue
179+
key = (term_id, category_id)
180+
if key in existing_links:
181+
continue
182+
association_rows.append(
183+
{"term_id": term_id, "category_id": category_id}
104184
)
105185

106-
session.rollback()
186+
if association_rows:
187+
session.execute(
188+
insert(LexiconTermCategoryAssociation).values(association_rows)
189+
)
190+
session.commit()
107191

108192

109193
def register_routes(app):

transfers/associated_data.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -169,18 +169,6 @@ def _normalize_point_id(value: str) -> str:
169169
def _normalize_location_id(value: str) -> str:
170170
return value.strip().lower()
171171

172-
def _dedupe_rows(
173-
self, rows: list[dict[str, Any]], key: str
174-
) -> list[dict[str, Any]]:
175-
"""Dedupe rows by unique key to avoid ON CONFLICT loops. Later rows win."""
176-
deduped = {}
177-
for row in rows:
178-
assoc_id = row.get(key)
179-
if assoc_id is None:
180-
continue
181-
deduped[assoc_id] = row
182-
return list(deduped.values())
183-
184172
def _uuid_val(self, value: Any) -> Optional[UUID]:
185173
if value is None or pd.isna(value):
186174
return None

transfers/chemistry_sampleinfo.py

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -361,21 +361,6 @@ def bool_val(key: str) -> Optional[bool]:
361361
"SampleNotes": str_val("SampleNotes"),
362362
}
363363

364-
def _dedupe_rows(
365-
self, rows: list[dict[str, Any]], key: str
366-
) -> list[dict[str, Any]]:
367-
"""
368-
Deduplicate rows within a batch by the given key to avoid ON CONFLICT loops.
369-
Later rows win.
370-
"""
371-
deduped = {}
372-
for row in rows:
373-
oid = row.get(key)
374-
if oid is None:
375-
continue
376-
deduped[oid] = row
377-
return list(deduped.values())
378-
379364

380365
def run(batch_size: int = 1000) -> None:
381366
"""Entrypoint to execute the transfer."""

transfers/field_parameters_transfer.py

Lines changed: 3 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -31,20 +31,17 @@
3131
from __future__ import annotations
3232

3333
from typing import Any, Optional
34-
from uuid import UUID
3534

3635
import pandas as pd
3736
from sqlalchemy.dialects.postgresql import insert
3837
from sqlalchemy.orm import Session
3938

40-
from db import NMA_Chemistry_SampleInfo, NMA_FieldParameters
41-
from db.engine import session_ctx
39+
from db import NMA_FieldParameters
4240
from transfers.logger import logger
43-
from transfers.transferer import Transferer
44-
from transfers.util import read_csv
41+
from transfers.transferer import ChemistryTransferer
4542

4643

47-
class FieldParametersTransferer(Transferer):
44+
class FieldParametersTransferer(ChemistryTransferer):
4845
"""
4946
Transfer FieldParameters records to NMA_FieldParameters.
5047
@@ -54,59 +51,6 @@ class FieldParametersTransferer(Transferer):
5451

5552
source_table = "FieldParameters"
5653

57-
def __init__(self, *args, batch_size: int = 1000, **kwargs):
58-
super().__init__(*args, **kwargs)
59-
self.batch_size = batch_size
60-
# Cache: legacy UUID -> Integer id
61-
self._sample_info_cache: dict[UUID, int] = {}
62-
self._build_sample_info_cache()
63-
64-
def _build_sample_info_cache(self) -> None:
65-
"""Build cache of nma_sample_pt_id -> id for FK lookups."""
66-
with session_ctx() as session:
67-
sample_infos = (
68-
session.query(
69-
NMA_Chemistry_SampleInfo.nma_sample_pt_id,
70-
NMA_Chemistry_SampleInfo.id,
71-
)
72-
.filter(NMA_Chemistry_SampleInfo.nma_sample_pt_id.isnot(None))
73-
.all()
74-
)
75-
self._sample_info_cache = {
76-
nma_sample_pt_id: csi_id for nma_sample_pt_id, csi_id in sample_infos
77-
}
78-
logger.info(
79-
f"Built ChemistrySampleInfo cache with {len(self._sample_info_cache)} entries"
80-
)
81-
82-
def _get_dfs(self) -> tuple[pd.DataFrame, pd.DataFrame]:
83-
input_df = read_csv(self.source_table)
84-
cleaned_df = self._filter_to_valid_sample_infos(input_df)
85-
return input_df, cleaned_df
86-
87-
def _filter_to_valid_sample_infos(self, df: pd.DataFrame) -> pd.DataFrame:
88-
"""
89-
Filter to only include rows where SamplePtID matches a ChemistrySampleInfo.
90-
91-
This prevents orphan records and ensures the FK constraint will be satisfied.
92-
"""
93-
valid_sample_pt_ids = set(self._sample_info_cache.keys())
94-
before_count = len(df)
95-
mask = df["SamplePtID"].apply(
96-
lambda value: self._uuid_val(value) in valid_sample_pt_ids
97-
)
98-
filtered_df = df[mask].copy()
99-
after_count = len(filtered_df)
100-
101-
if before_count > after_count:
102-
skipped = before_count - after_count
103-
logger.warning(
104-
f"Filtered out {skipped} FieldParameters records without matching "
105-
f"ChemistrySampleInfo ({after_count} valid, {skipped} orphan records prevented)"
106-
)
107-
108-
return filtered_df
109-
11054
def _transfer_hook(self, session: Session) -> None:
11155
"""
11256
Override transfer hook to use batch upsert for idempotent transfers.
@@ -206,55 +150,6 @@ def _row_to_dict(self, row) -> Optional[dict[str, Any]]:
206150
"AnalysesAgency": self._safe_str(row, "AnalysesAgency"),
207151
}
208152

209-
def _dedupe_rows(self, rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
210-
"""Dedupe rows by unique key to avoid ON CONFLICT loops. Later rows win."""
211-
deduped = {}
212-
for row in rows:
213-
key = row.get("nma_GlobalID")
214-
if key is None:
215-
continue
216-
deduped[key] = row
217-
return list(deduped.values())
218-
219-
def _safe_str(self, row, attr: str) -> Optional[str]:
220-
"""Safely get a string value, returning None for NaN."""
221-
val = getattr(row, attr, None)
222-
if val is None or pd.isna(val):
223-
return None
224-
return str(val)
225-
226-
def _safe_float(self, row, attr: str) -> Optional[float]:
227-
"""Safely get a float value, returning None for NaN."""
228-
val = getattr(row, attr, None)
229-
if val is None or pd.isna(val):
230-
return None
231-
try:
232-
return float(val)
233-
except (TypeError, ValueError):
234-
return None
235-
236-
def _safe_int(self, row, attr: str) -> Optional[int]:
237-
"""Safely get an int value, returning None for NaN."""
238-
val = getattr(row, attr, None)
239-
if val is None or pd.isna(val):
240-
return None
241-
try:
242-
return int(val)
243-
except (TypeError, ValueError):
244-
return None
245-
246-
def _uuid_val(self, value: Any) -> Optional[UUID]:
247-
if value is None or pd.isna(value):
248-
return None
249-
if isinstance(value, UUID):
250-
return value
251-
if isinstance(value, str):
252-
try:
253-
return UUID(value)
254-
except ValueError:
255-
return None
256-
return None
257-
258153

259154
def run(flags: dict = None) -> tuple[pd.DataFrame, pd.DataFrame, list]:
260155
"""Entrypoint to execute the transfer."""

transfers/hydraulicsdata.py

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ def _transfer_hook(self, session: Session) -> None:
100100
f"(orphan prevention)"
101101
)
102102

103-
rows = self._dedupe_rows(row_dicts, key="nma_GlobalID")
103+
rows = self._dedupe_rows(row_dicts)
104104

105105
insert_stmt = insert(NMA_HydraulicsData)
106106
excluded = insert_stmt.excluded
@@ -198,21 +198,6 @@ def as_int(key: str) -> Optional[int]:
198198
"Data Source": val("Data Source"),
199199
}
200200

201-
def _dedupe_rows(
202-
self, rows: list[dict[str, Any]], key: str
203-
) -> list[dict[str, Any]]:
204-
"""
205-
Deduplicate rows within a batch by the given key to avoid ON CONFLICT loops.
206-
Later rows win.
207-
"""
208-
deduped = {}
209-
for row in rows:
210-
gid = row.get(key)
211-
if gid is None:
212-
continue
213-
deduped[gid] = row
214-
return list(deduped.values())
215-
216201

217202
def run(batch_size: int = 1000) -> None:
218203
"""Entrypoint to execute the transfer."""

0 commit comments

Comments
 (0)