6161import os
6262import sqlite3
6363import sys
64- from typing import Any , Dict , Optional
64+ from typing import Any , Dict , Optional , Callable
6565
6666import pydicom # type: ignore
6767import pyorthanc
@@ -83,15 +83,39 @@ def studies_for_date(date_str: str):
8383 return sorted (studies , key = lambda s : (s .date , s .identifier ))
8484
8585
86- def process_date_arg (date_token : str , conn : sqlite3 .Connection ) -> None :
87- """Handle an argument that is a date (YYYYMM or YYYYMMDD)."""
86+ # Callable already imported above via typing import Any, Dict, Optional, Callable
87+
88+
89+ def process_date_arg (
90+ date_token : str ,
91+ conn : sqlite3 .Connection ,
92+ * ,
93+ force : bool = False ,
94+ _skip_cb : Optional [Callable [[str ], None ]] = None ,
95+ ) -> None :
96+ """Handle an argument that is a date (YYYYMM or YYYYMMDD).
97+
98+ The *force* flag has the same semantics as in :func:`store_study` – when
99+ False, studies whose accession already exists in the database are skipped.
100+ When True, they are purged beforehand.
101+ """
88102
89103 if len (date_token ) == 8 : # YYYYMMDD
90104 # single day
91105 for study in studies_for_date (date_token ):
92106 acc = study .main_dicom_tags .get ("AccessionNumber" , "" )
107+
93108 if not acc .startswith ("E" ):
94109 continue
110+
111+ if accession_exists (conn , acc ):
112+ if not force :
113+ if _skip_cb :
114+ _skip_cb (acc )
115+ continue # skip existing study
116+ # purge and reprocess before re-acquiring
117+ purge_accession (conn , acc )
118+
95119 print (acc )
96120 _process_study (study , conn )
97121
@@ -108,6 +132,14 @@ def process_date_arg(date_token: str, conn: sqlite3.Connection) -> None:
108132 acc = study .main_dicom_tags .get ("AccessionNumber" , "" )
109133 if not acc .startswith ("E" ):
110134 continue
135+
136+ if accession_exists (conn , acc ):
137+ if not force :
138+ if _skip_cb :
139+ _skip_cb (acc )
140+ continue
141+ purge_accession (conn , acc )
142+
111143 print (acc )
112144 _process_study (study , conn )
113145 else :
@@ -178,6 +210,27 @@ def get_db_connection() -> sqlite3.Connection:
178210 return conn
179211
180212
213+ # ---------------------------------------------------------------------------
214+ # DB convenience helpers
215+ # ---------------------------------------------------------------------------
216+
217+
218+ def accession_exists (conn : sqlite3 .Connection , accession : str ) -> bool :
219+ """Return *True* if *accession* is already present in *studies* table."""
220+
221+ cur = conn .execute ("SELECT 1 FROM studies WHERE accession = ? LIMIT 1" , (accession ,))
222+ return cur .fetchone () is not None
223+
224+
225+ def purge_accession (conn : sqlite3 .Connection , accession : str ) -> None :
226+ """Remove *accession* from *studies* and all related rows from *series*."""
227+
228+ # Delete child rows first to satisfy the foreign-key relation.
229+ conn .execute ("DELETE FROM series WHERE accession = ?" , (accession ,))
230+ conn .execute ("DELETE FROM studies WHERE accession = ?" , (accession ,))
231+ conn .commit ()
232+
233+
181234# ---------------------------------------------------------------------------
182235# DICOM helpers
183236# ---------------------------------------------------------------------------
@@ -449,8 +502,25 @@ def _process_study(study: pyorthanc.Study, conn: sqlite3.Connection) -> None:
449502# Public helper --------------------------------------------------------------
450503
451504
452- def store_study (accession : str , conn : sqlite3 .Connection ) -> None :
453- """Locate study by accession and store its information."""
505+ def store_study (accession : str , conn : sqlite3 .Connection , * , force : bool = False ) -> None :
506+ """Locate study by *accession* and store its information.
507+
508+ Behaviour is influenced by *force*:
509+
510+ • If *force* is False (default) and the accession already exists in the
511+ database, the function returns immediately.
512+ • If *force* is True, any existing rows for the accession (including
513+ related *series*) are removed before the data are retrieved again from
514+ Orthanc.
515+ """
516+
517+ if accession_exists (conn , accession ):
518+ if not force :
519+ # Skip silently – caller decides whether to print something.
520+ return
521+
522+ # Remove stale data before re-importing.
523+ purge_accession (conn , accession )
454524
455525 studies = pyorthanc .find_studies (client = ORTHANC , query = {"AccessionNumber" : accession })
456526
@@ -473,6 +543,17 @@ def parse_args() -> argparse.Namespace:
473543 nargs = "+" ,
474544 help = "Accession numbers starting with 'E', or date strings YYYYMM / YYYYMMDD" ,
475545 )
546+
547+ parser .add_argument (
548+ "-f" ,
549+ "--force" ,
550+ action = "store_true" ,
551+ help = (
552+ "Re-fetch information even if the accession already exists in the local "
553+ "database. When used, existing rows for that accession (including related "
554+ "series) are removed before acquisition."
555+ ),
556+ )
476557 return parser .parse_args ()
477558
478559
@@ -481,11 +562,18 @@ def main() -> None:
481562
482563 conn = get_db_connection ()
483564
565+ def _print_skip (acc : str ) -> None :
566+ print (f"{ acc } already in db, skipping" )
567+
484568 for token in args .tokens :
485569 if token .startswith ("E" ):
486- store_study (token , conn )
570+ if not args .force and accession_exists (conn , token ):
571+ _print_skip (token )
572+ continue
573+
574+ store_study (token , conn , force = args .force )
487575 elif token .isdigit () and len (token ) in (6 , 8 ):
488- process_date_arg (token , conn )
576+ process_date_arg (token , conn , force = args . force , _skip_cb = _print_skip )
489577 else :
490578 print (
491579 f"Unrecognised argument '{ token } '. Expected accession starting with 'E' or date string." ,
0 commit comments