3131from __future__ import annotations
3232
3333from typing import Any , Optional
34- from uuid import UUID
3534
3635import pandas as pd
3736from sqlalchemy .dialects .postgresql import insert
3837from 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
4240from 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
259154def run (flags : dict = None ) -> tuple [pd .DataFrame , pd .DataFrame , list ]:
260155 """Entrypoint to execute the transfer."""
0 commit comments