gvlktejaswi commited on
Commit
4e66320
·
verified ·
1 Parent(s): ff89d22

Update page_files/categorized/Backend/category_push.py

Browse files
page_files/categorized/Backend/category_push.py CHANGED
@@ -124,7 +124,15 @@ def _s(v: Any) -> str:
124
  return ""
125
  except (TypeError, ValueError):
126
  pass
127
- return str(v)
 
 
 
 
 
 
 
 
128
 
129
 
130
  def _page_int(v: Any) -> Optional[int]:
@@ -180,6 +188,12 @@ def conform_to_category(
180
  plots_root = plots_root or os.path.join(out_dir, "rds_plots")
181
  dest_dir = os.path.join(plots_root, category_table, stem)
182
 
 
 
 
 
 
 
183
  main_rows: List[Dict[str, Any]] = []
184
  extras_rows: List[Dict[str, Any]] = []
185
  n_plots = 0
@@ -252,7 +266,7 @@ def conform_to_category(
252
  "unit_canonical": _s(row.get("unit")),
253
  "value_si": None,
254
  "source_pdf": _s(row.get("pdf_stem")) or stem,
255
- "source_sha1": "",
256
  "page": _page_int(row.get("source_page")),
257
  "source_quote": _s(row.get("source_text")),
258
  "status": "extracted",
@@ -315,6 +329,22 @@ def push_by_category(
315
  df_store, category_table, out_dir, stem,
316
  plots_root=plots_root, embed_image=embed_image)
317
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
318
  extras_table = f"{category_table}_extras"
319
  # idempotency: clear this paper first (tables may not exist yet -> ignore)
320
  with engine.begin() as conn:
@@ -324,13 +354,33 @@ def push_by_category(
324
  except Exception:
325
  pass
326
 
327
- main_df.to_sql(category_table, engine, if_exists="append", index=False)
328
  extras_df.to_sql(extras_table, engine, if_exists="append", index=False)
329
  if _s3_ready():
330
  plots_dir = _s3_url(_s3_key(category_table, stem, "")).rstrip("/")
331
  else:
332
  plots_dir = os.path.join(plots_root or os.path.join(out_dir, "rds_plots"),
333
  category_table, stem)
334
- return {"pushed": len(main_df), "table": category_table,
335
  "extras_table": extras_table, "extras": len(extras_df),
336
- "plots_copied": n_plots, "plots_dir": plots_dir}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  return ""
125
  except (TypeError, ValueError):
126
  pass
127
+ s = str(v)
128
+ # Postgres text/varchar columns cannot contain NUL (0x00). PDF text
129
+ # extraction occasionally emits embedded nulls (and other C0 control
130
+ # chars) inside quotes/comments — strip them so the INSERT doesn't fail
131
+ # with "A string literal cannot contain NUL (0x00) characters".
132
+ if "\x00" in s or any(ord(c) < 32 and c not in "\t\n\r" for c in s):
133
+ s = s.replace("\x00", "")
134
+ s = "".join(c for c in s if ord(c) >= 32 or c in "\t\n\r")
135
+ return s
136
 
137
 
138
  def _page_int(v: Any) -> Optional[int]:
 
188
  plots_root = plots_root or os.path.join(out_dir, "rds_plots")
189
  dest_dir = os.path.join(plots_root, category_table, stem)
190
 
191
+ # source_sha1 scopes the DB dedup index to THIS paper. It was previously ""
192
+ # for every row, which collapsed the unique key across all papers. A stable
193
+ # hash of the stem keeps re-pushes of the same paper consistent.
194
+ import hashlib
195
+ source_sha1 = hashlib.sha1((stem or "").encode("utf-8")).hexdigest()
196
+
197
  main_rows: List[Dict[str, Any]] = []
198
  extras_rows: List[Dict[str, Any]] = []
199
  n_plots = 0
 
266
  "unit_canonical": _s(row.get("unit")),
267
  "value_si": None,
268
  "source_pdf": _s(row.get("pdf_stem")) or stem,
269
+ "source_sha1": source_sha1,
270
  "page": _page_int(row.get("source_page")),
271
  "source_quote": _s(row.get("source_text")),
272
  "status": "extracted",
 
329
  df_store, category_table, out_dir, stem,
330
  plots_root=plots_root, embed_image=embed_image)
331
 
332
+ # keep-first dedup on the exact columns of ix_..._pipeline_dedup, so two
333
+ # identical measurements in one paper collapse to one row instead of
334
+ # tripping the unique constraint mid-batch.
335
+ _DEDUP_KEYS = ["source_sha1", "material_key", "section",
336
+ "property_name", "test_condition", "value_raw"]
337
+ keys = [k for k in _DEDUP_KEYS if k in main_df.columns]
338
+ if keys:
339
+ before = len(main_df)
340
+ main_df = main_df.drop_duplicates(subset=keys, keep="first").reset_index(drop=True)
341
+ dropped = before - len(main_df)
342
+ if dropped:
343
+ log.info(f"category_push: dropped {dropped} in-batch duplicate row(s) on {keys}")
344
+ # keep extras aligned to the surviving main rows (1:1 by position)
345
+ extras_df = extras_df.loc[main_df.index].reset_index(drop=True) \
346
+ if len(extras_df) == before else extras_df
347
+
348
  extras_table = f"{category_table}_extras"
349
  # idempotency: clear this paper first (tables may not exist yet -> ignore)
350
  with engine.begin() as conn:
 
354
  except Exception:
355
  pass
356
 
357
+ pushed = _insert_idempotent(engine, category_table, main_df)
358
  extras_df.to_sql(extras_table, engine, if_exists="append", index=False)
359
  if _s3_ready():
360
  plots_dir = _s3_url(_s3_key(category_table, stem, "")).rstrip("/")
361
  else:
362
  plots_dir = os.path.join(plots_root or os.path.join(out_dir, "rds_plots"),
363
  category_table, stem)
364
+ return {"pushed": pushed, "table": category_table,
365
  "extras_table": extras_table, "extras": len(extras_df),
366
+ "plots_copied": n_plots, "plots_dir": plots_dir}
367
+
368
+
369
+ def _insert_idempotent(engine, table: str, df: pd.DataFrame) -> int:
370
+ """Append rows one INSERT ... ON CONFLICT DO NOTHING at a time, so a row that
371
+ still collides with the table's unique dedup index is skipped instead of
372
+ rolling back the whole batch. Returns the number of rows actually inserted."""
373
+ from sqlalchemy import text
374
+ if df is None or df.empty:
375
+ return 0
376
+ cols = list(df.columns)
377
+ collist = ", ".join(f'"{c}"' for c in cols)
378
+ params = ", ".join(f":{c}" for c in cols)
379
+ sql = text(f'INSERT INTO "{table}" ({collist}) VALUES ({params}) ON CONFLICT DO NOTHING')
380
+ inserted = 0
381
+ with engine.begin() as conn:
382
+ for _, r in df.iterrows():
383
+ payload = {c: (None if pd.isna(v) else v) for c, v in r.items()}
384
+ res = conn.execute(sql, payload)
385
+ inserted += (res.rowcount or 0)
386
+ return inserted