Skip to content

Commit e9af9b5

Browse files
authored
Merge pull request #115 from NHSDigital/DTOSS-12809-consolidate-utility-scripts
Consolidate database utility scripts
2 parents 764d9cf + bb131b4 commit e9af9b5

11 files changed

Lines changed: 188 additions & 209 deletions

File tree

Makefile

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,11 @@ _install-uv:
6868
# ---------------------------------------------------------------------------
6969
# Database backup
7070
# ---------------------------------------------------------------------------
71-
backup-db: # Backup sqlite databases @Operations
72-
PYTHONPATH=src uv run python -m backup_main
71+
backup-dbs: # Backup sqlite databases @Operations
72+
PYTHONPATH=src uv run python -c'import scripts.python.database; scripts.python.database.backup_databases()'
73+
74+
reset-worklist: # Reset worklist database @Operations
75+
PYTHONPATH=src uv run python -c'import scripts.python.database; scripts.python.database.reset_worklist_database()'
7376

7477
# ---------------------------------------------------------------------------
7578
# Testing

scripts/bash/package_release.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ echo ""
6161

6262
# ── Validate required files ───────────────────────────────────────────────────
6363

64-
REQUIRED_FILES=("src" "pyproject.toml" "uv.lock" "README.md")
64+
REQUIRED_FILES=("src/" "scripts/python/" "pyproject.toml" "uv.lock" "README.md")
6565

6666
for item in "${REQUIRED_FILES[@]}"; do
6767
if [[ ! -e "${REPO_ROOT}/${item}" ]]; then
@@ -84,7 +84,7 @@ cd "$REPO_ROOT"
8484
# This ensures only tracked files are included, preventing accidental inclusion of
8585
# .env files, local secrets, or untracked files.
8686
echo "Creating archive from git tracked files..."
87-
git archive --format=zip -o "$ARTIFACT_PATH" HEAD src/ pyproject.toml uv.lock README.md
87+
git archive --format=zip -o "$ARTIFACT_PATH" HEAD "${REQUIRED_FILES[@]}"
8888

8989
echo "Archive created successfully."
9090

scripts/bat/backup_database.bat

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
:: Adjust the Python path below if the deployment creates the venv elsewhere.
44
@echo off
55
set rootdir=%~1
6-
"%rootdir%\.venv\Scripts\python.exe" -m mwl_reset
6+
"%rootdir%\.venv\Scripts\python.exe" -c'import scripts.python.database; scripts.python.database.reset_worklist_database()'

scripts/python/database.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import logging
2+
import os
3+
import sqlite3
4+
from datetime import datetime
5+
6+
logging.basicConfig(
7+
level=os.getenv("LOG_LEVEL", "INFO").upper(),
8+
format=os.getenv("LOG_FORMAT", "%(asctime)s - %(name)s - %(levelname)s - %(message)s"),
9+
)
10+
logger = logging.getLogger(__name__)
11+
12+
13+
def backup_database(db_path: str, backup_dir: str) -> str:
14+
"""
15+
Backup a SQLite database to a timestamped file in backup_dir.
16+
17+
Returns the path of the backup file.
18+
"""
19+
os.makedirs(backup_dir, exist_ok=True)
20+
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
21+
db_filename = os.path.basename(db_path)
22+
backup_path = os.path.join(backup_dir, f"{timestamp}.{db_filename}.backup")
23+
with sqlite3.connect(db_path) as conn:
24+
with sqlite3.connect(backup_path) as backup_conn:
25+
conn.backup(backup_conn)
26+
return backup_path
27+
28+
29+
def backup_databases():
30+
"""
31+
Backup all configured databases.
32+
33+
Environment variables:
34+
PACS_DB_PATH: Path to the PACS SQLite database
35+
MWL_DB_PATH: Path to the MWL SQLite database
36+
BACKUP_PATH: Directory for backups (default: ./backups)
37+
"""
38+
pacs_db_path = os.getenv("PACS_DB_PATH")
39+
mwl_db_path = os.getenv("MWL_DB_PATH")
40+
backup_path = os.getenv("BACKUP_PATH", "./backups")
41+
42+
if not pacs_db_path and not mwl_db_path:
43+
logger.warning("No database paths configured (PACS_DB_PATH or MWL_DB_PATH), skipping backup")
44+
return
45+
46+
success = True
47+
48+
if pacs_db_path:
49+
try:
50+
pacs_backup_path = backup_database(pacs_db_path, backup_path)
51+
except Exception as e:
52+
logging.error(f"PACS backup failed: {e}")
53+
success = False
54+
else:
55+
logging.info("PACS_DB_PATH not set, skipping PACS backup")
56+
57+
if mwl_db_path:
58+
try:
59+
mwl_backup_path = backup_database(mwl_db_path, backup_path)
60+
except Exception as e:
61+
logging.error(f"MWL backup failed: {e}")
62+
success = False
63+
else:
64+
logging.info("MWL_DB_PATH not set, skipping MWL backup")
65+
66+
if success:
67+
logger.info("All database backups completed successfully. Backup files:")
68+
if pacs_db_path:
69+
logger.info(f" PACS backup: {pacs_backup_path}")
70+
if mwl_db_path:
71+
logger.info(f" MWL backup: {mwl_backup_path}")
72+
73+
74+
def reset_worklist_database() -> int:
75+
"""
76+
Backs up and clears the MWL database..
77+
78+
Environment variables:
79+
MWL_DB_PATH: Path to the MWL SQLite database (default: /var/lib/pacs/worklist.db)
80+
BACKUP_PATH: Directory for database backups (default: /var/lib/pacs/backups)
81+
"""
82+
mwl_db_path = os.getenv("MWL_DB_PATH", "/var/lib/pacs/worklist.db")
83+
backup_path = os.getenv("BACKUP_PATH", "/var/lib/pacs/backups")
84+
count = 0
85+
86+
try:
87+
path = backup_database(mwl_db_path, backup_path)
88+
logger.info(f"Backup complete: {path}")
89+
except Exception as e:
90+
logger.error(f"MWL backup failed: {e}", exc_info=True)
91+
92+
try:
93+
with sqlite3.connect(mwl_db_path) as conn:
94+
cursor = conn.execute("DELETE FROM worklist_items")
95+
conn.commit()
96+
count = cursor.rowcount
97+
logger.info(f"MWL reset complete: {count} items deleted")
98+
except Exception as e:
99+
logger.error(f"MWL clear failed: {e}", exc_info=True)
100+
101+
return count

src/backup_main.py

Lines changed: 0 additions & 58 deletions
This file was deleted.

src/db_backup.py

Lines changed: 0 additions & 19 deletions
This file was deleted.

src/mwl_clear.py

Lines changed: 0 additions & 9 deletions
This file was deleted.

src/mwl_reset.py

Lines changed: 0 additions & 49 deletions
This file was deleted.

tests/scripts/test_database.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import pytest
2+
import sqlite3
3+
import sys
4+
from pathlib import Path
5+
6+
sys.path.append(f"{Path(__file__).parent.parent.parent}/scripts/python")
7+
8+
from database import backup_database, reset_worklist_database
9+
10+
11+
@pytest.fixture
12+
def worklist_db(tmp_dir, monkeypatch):
13+
db_path = f"{tmp_dir}/test_worklist.db"
14+
with sqlite3.connect(db_path) as conn:
15+
conn.execute("CREATE TABLE worklist_items (accession_number TEXT PRIMARY KEY, patient_id TEXT)")
16+
conn.execute("INSERT INTO worklist_items VALUES ('ACC001', 'P001')")
17+
conn.execute("INSERT INTO worklist_items VALUES ('ACC002', 'P002')")
18+
conn.commit()
19+
20+
monkeypatch.setenv("BACKUP_PATH", f"{tmp_dir}/backups")
21+
monkeypatch.setenv("MWL_DB_PATH", db_path)
22+
return db_path
23+
24+
25+
# Tests for backup_database
26+
def test_backup_creates_file(tmp_dir):
27+
db_path = f"{tmp_dir}/test.db"
28+
sqlite3.connect(db_path).close()
29+
30+
backup_path = backup_database(db_path, f"{tmp_dir}/backups")
31+
32+
assert Path(backup_path).exists()
33+
34+
35+
def test_backup_returns_timestamped_path(tmp_dir):
36+
db_path = f"{tmp_dir}/test.db"
37+
sqlite3.connect(db_path).close()
38+
39+
backup_path = backup_database(db_path, f"{tmp_dir}/backups")
40+
41+
assert backup_path.endswith(".db.backup")
42+
43+
44+
def test_backup_creates_backup_dir_if_missing(tmp_dir):
45+
db_path = f"{tmp_dir}/test.db"
46+
sqlite3.connect(db_path).close()
47+
backup_dir = f"{tmp_dir}/backups/nested"
48+
49+
backup_path = backup_database(db_path, backup_dir)
50+
51+
assert Path(backup_path).exists()
52+
53+
54+
def test_backup_database_creates_backup(worklist_db):
55+
backup_path = backup_database(worklist_db, str(Path(worklist_db).parent / "backups"))
56+
assert Path(backup_path).exists()
57+
58+
59+
# Tests for reset_worklist_database
60+
def test_reset_worklist_database_deletes_all_rows(worklist_db):
61+
reset_worklist_database()
62+
63+
with sqlite3.connect(worklist_db) as conn:
64+
count = conn.execute("SELECT COUNT(*) FROM worklist_items").fetchone()[0]
65+
assert count == 0
66+
67+
68+
def test_reset_worklist_database_returns_row_count(worklist_db):
69+
assert reset_worklist_database() == 2
70+
71+
72+
def test_reset_worklist_database_returns_zero_when_empty(tmp_dir, monkeypatch):
73+
db_path = f"{tmp_dir}/worklist.db"
74+
with sqlite3.connect(db_path) as conn:
75+
conn.execute("CREATE TABLE worklist_items (accession_number TEXT PRIMARY KEY)")
76+
conn.commit()
77+
78+
monkeypatch.setenv("MWL_DB_PATH", db_path)
79+
assert reset_worklist_database() == 0

tests/test_db_backup.py

Lines changed: 0 additions & 32 deletions
This file was deleted.

0 commit comments

Comments
 (0)