Skip to content

Commit 92ae75b

Browse files
committed
skip already done
1 parent ab66909 commit 92ae75b

File tree

2 files changed

+98
-7
lines changed

2 files changed

+98
-7
lines changed

billing/store_study_info.py

Lines changed: 95 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
import os
6262
import sqlite3
6363
import sys
64-
from typing import Any, Dict, Optional
64+
from typing import Any, Dict, Optional, Callable
6565

6666
import pydicom # type: ignore
6767
import 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.",

billing/store_study_info_codex.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ way they do and what caveats to remember when extending it.
2626

2727
# Multiple tokens are accepted
2828
./store_study_info.py 202404 20240427 E87654321
29+
30+
# Force re-acquisition even if the accession is already stored
31+
./store_study_info.py --force E12345678
2932
```
3033

3134
### Token rules

0 commit comments

Comments
 (0)