APA-URAAS / scripts /backfill_publication_dates.py
Lordkiki's picture
Deploy URAAS β€” African Research Archival & Analytics System
dd2635a verified
Raw
History Blame Contribute Delete
3.09 kB
"""Backfill Item.publication_date for existing rows.
uraas/pipelines/database.py's date parser had a slicing bug (fixed
2026-07-19): it truncated every incoming date string to len(format_string)
characters before calling strptime, e.g. slicing "2024-06-15" (10 chars) to
"2024-06-" (8 chars, matching len("%Y-%m-%d")) β€” which then fails to parse,
for every format, for every item, always. Confirmed live: 0/54 items in the
production DB had publication_date set despite 40/54 having a valid
dc_date_issued string. Since Item.publication_date (not dc_date_issued, a
separate plain-string column that was never affected) is what analytics/
dashboard code actually sorts/filters/groups by (36 references across
uraas/analytics/engine.py, uraas/dashboard/app.py,
uraas/services/comparator_engine.py), every date-based chart or filter has
been silently empty/broken for every item ever crawled.
This re-derives publication_date from the already-correct dc_date_issued
string using the same (now-fixed) parsing logic, for every row currently
missing it. Idempotent β€” only touches rows where publication_date IS NULL.
Usage:
python scripts/backfill_publication_dates.py # DRY RUN
python scripts/backfill_publication_dates.py --apply
"""
import argparse
import os
import sys
from datetime import datetime
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from uraas.database import Item, SessionLocal
_FORMATS = ("%Y-%m-%dT%H:%M:%SZ", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d", "%Y-%m", "%Y")
def _parse(date_str: str):
date_str = (date_str or "").strip()
if not date_str:
return None
for fmt in _FORMATS:
try:
return datetime.strptime(date_str, fmt)
except ValueError:
continue
return None
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--apply", action="store_true", help="Write changes (default: dry run)")
args = parser.parse_args()
session = SessionLocal()
try:
candidates = (
session.query(Item)
.filter(Item.publication_date.is_(None))
.filter(Item.dc_date_issued.isnot(None))
.all()
)
print(f"Items with publication_date NULL but dc_date_issued present: {len(candidates)}")
fixed = unparseable = 0
for it in candidates:
parsed = _parse(it.dc_date_issued)
if parsed:
fixed += 1
if args.apply:
it.publication_date = parsed
else:
unparseable += 1
print(f" UNPARSEABLE id={it.id} dc_date_issued={it.dc_date_issued!r}")
print(f"\nWould fix: {fixed} Unparseable (left as-is): {unparseable}")
if not args.apply:
print("\n[DRY RUN] No changes written. Re-run with --apply.")
return 0
session.commit()
print(f"\nDONE. {fixed} items backfilled.")
return 0
finally:
session.close()
if __name__ == "__main__":
sys.exit(main())