Skip to content

Commit

Permalink
Add scripts and commands for radread and listmode warning emails (#6)
Browse files Browse the repository at this point in the history
  • Loading branch information
tomdoel authored Oct 1, 2024
1 parent fb29919 commit 0e06aa1
Show file tree
Hide file tree
Showing 8 changed files with 681 additions and 3 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,4 +175,4 @@ commands will be available on subject pages.

## Licence

See [LICENSE.txt](`./LICENSE.txt`)
See [LICENSE.txt](./LICENSE.txt)
60 changes: 60 additions & 0 deletions email_listmode.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{
"name": "email-listmode",
"description": "Send email with listmode warnings",
"label": "Send email with listmode warnings",
"version": "1.0",
"schema-version": "1.0",
"type": "docker",
"command-line": "email_listmode #PROJECTID# #EMAILLIST#",
"image": "ghcr.io/ucl-mirsg/drc-containers:latest",
"override-entrypoint": true,
"mounts": [],
"inputs": [
{
"name": "PROJECTID",
"description": "Project ID",
"type": "string",
"user-settable": false,
"required": true,
"replacement-key": "#PROJECTID#"
},
{
"name": "EMAILLIST",
"description": "Comma separated list of email addresses",
"type": "string",
"user-settable": true,
"required": true,
"replacement-key": "#EMAILLIST#"
}
],
"outputs": [],
"xnat": [
{
"name": "project-email-listmode",
"label": "Send email with listmode warnings",
"description": "Send email with listmode warnings",
"contexts": ["xnat:projectData"],
"external-inputs": [
{
"name": "project",
"description": "Input project",
"type": "Project",
"required": true,
"load-children": false
}
],
"derived-inputs": [
{
"name": "project-id",
"type": "string",
"derived-from-wrapper-input": "project",
"derived-from-xnat-object-property": "id",
"provides-value-for-command-input": "PROJECTID",
"user-settable": false,
"required": true
}
],
"output-handlers": []
}
]
}
60 changes: 60 additions & 0 deletions email_radreads.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{
"name": "email-radreads",
"description": "Send email listing sessions without radreads",
"label": "Send email listing sessions without radreads",
"version": "1.0",
"schema-version": "1.0",
"type": "docker",
"command-line": "email_radreads #PROJECTID# #EMAILLIST#",
"image": "ghcr.io/ucl-mirsg/drc-containers:latest",
"override-entrypoint": true,
"mounts": [],
"inputs": [
{
"name": "PROJECTID",
"description": "Project ID",
"type": "string",
"user-settable": false,
"required": true,
"replacement-key": "#PROJECTID#"
},
{
"name": "EMAILLIST",
"description": "Comma separated list of email addresses",
"type": "string",
"user-settable": true,
"required": true,
"replacement-key": "#EMAILLIST#"
}
],
"outputs": [],
"xnat": [
{
"name": "project-email-radreads",
"label": "Send email listing sessions without radreads",
"description": "Send email listing sessions without radreads",
"contexts": ["xnat:projectData"],
"external-inputs": [
{
"name": "project",
"description": "Input project",
"type": "Project",
"required": true,
"load-children": false
}
],
"derived-inputs": [
{
"name": "project-id",
"type": "string",
"derived-from-wrapper-input": "project",
"derived-from-xnat-object-property": "id",
"provides-value-for-command-input": "PROJECTID",
"user-settable": false,
"required": true
}
],
"output-handlers": []
}
]
}
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ dependencies = [
]
name = "drc-containers"
requires-python = ">3.10"
version = "0.0.1"
version = "0.0.2"

[project.scripts]
email_listmode = "drc_containers:email_listmode.main"
email_radreads = "drc_containers:email_radreads.main"
share_subject_to_genetic_project = "drc_containers:share_subject_to_genetic_project.main"

[tool.setuptools.packages.find]
Expand Down
232 changes: 232 additions & 0 deletions src/drc_containers/email_listmode.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import sys
from dataclasses import dataclass
from datetime import datetime, timedelta

from pyxnat import Interface
from pyxnat.core.resources import Experiments

from drc_containers.xnat_utils.email import send_email
from drc_containers.xnat_utils.xnat_credentials import (
open_pyxnat_session,
XnatContainerCredentials,
XnatCredentials,
)


@dataclass(frozen=True)
class ListModeRecord:
"""Store data from sessions with listmode errors
The frozen dataclass allows this to be used in a set to ensure no sessions
are repeated"""

id: str
label: str
subject_id: str
date: str
errors: str


def get_recent_sessions(
pyxnat_interface: Interface, datatype: str, threshold_days: int, project_name: str
) -> Experiments:
threshold_date = datetime.now() - timedelta(threshold_days)
str_threshold_date = threshold_date.strftime("%Y-%m-%d")
columns = [
datatype + "/SESSION_ID",
datatype + "/SUBJECT_ID",
datatype + "/DATE",
datatype + "/LABEL",
datatype + "/PROJECT",
]
constraints = [
(datatype + "/project", "=", project_name),
(datatype + "/date", ">=", str_threshold_date),
"AND",
]
return pyxnat_interface.select(datatype, columns).where(constraints)


def check_session(
pyxnat_interface: Interface, session_id: str, project_name: str
) -> [str]:
resources = (
pyxnat_interface.select.project(project_name).experiment(session_id).resources()
)

num_lm_files = 0
num_norm_files = 0
lm_found = False
norm_found = False
errors = []

for resource in resources:
res_label = resource.label()
if res_label == "LM":
lm_found = True
for file in resource.files():
num_lm_files += 1
file_label = file.label()
try:
if ".bf" in file_label:
file_size = file.size()
if int(file_size) < 1000000:
errors.append(
f"LM File is too small " f"{file_label} - {file_size}"
)
except: # noqa: E722
pass
elif res_label == "Norm":
norm_found = True
for _ in resource.files():
num_norm_files += 1

if not lm_found:
errors.append("LM does not exist")
elif num_lm_files != 2:
errors.append(f"Wrong number of LM files {num_lm_files}")

if not norm_found:
errors.append("Norm does not exist")
elif num_norm_files != 2:
errors.append(f"Wrong number of Norm files: {num_lm_files}")
return errors


def get_listmode_issues(
pyxnat_interface: Interface, threshold_days: int, project_name: str
) -> set[ListModeRecord]:
session_datatypes = [
"xnat:crSessionData",
"xnat:mrSessionData",
"xnat:otherDicomSessionData",
"xnat:petSessionData",
"xnat:petmrSessionData",
"xnat:srSessionData",
]
issue_list = set()

for datatype in session_datatypes:
sessions = get_recent_sessions(
pyxnat_interface=pyxnat_interface,
datatype=datatype,
threshold_days=threshold_days,
project_name=project_name,
)
for session in sessions.data:
session_id = session["session_id"]
session_label = session["label"]
subject_id = session["subject_id"]
session_date = session["date"]

errors = check_session(
pyxnat_interface=pyxnat_interface,
session_id=session_id,
project_name=project_name,
)
if len(errors) > 0:
error_string = ", ".join(errors)
message = (
f"Subject ID: {subject_id} "
f"Scan Date: {session_date} "
f"Errors: {error_string}<br>"
)
issue_list.add(
ListModeRecord(
id=session_id,
label=session_label,
subject_id=subject_id,
date=session_date,
errors=message,
)
)

return issue_list


def construct_email(list_mode_records: set[ListModeRecord]) -> str:
body = "<p>"

for session in list_mode_records:
body += f"Subject ID:{session.subject_id} Scan Date:{session.date} Errors:{session.errors}<br>"
return body


def email_listmode(
credentials: XnatCredentials,
project_name: str,
email_subject: str,
to_emails: list[str],
cc_emails: list[str] = None,
bcc_emails: list[str] = None,
debug_output: bool = True,
):
"""Email notification about image sessions with listmode errors
Args:
credentials: XNAT host name and user login details
project_name: The project to search for sessions
email_subject: subject line of email
to_emails: list of email addresses. XNAT will only send emails
to addresses which already correspond to XNAT users on the server
cc_emails: list of email addresses for cc. XNAT will only send emails
to addresses which already correspond to XNAT users on the server
bcc_emails: list of email addresses for bcc. XNAT will only send emails
to addresses which already correspond to XNAT users on the server
debug_output: set to True to output debugging data to the console
"""

with open_pyxnat_session(credentials=credentials) as xnat_session:
# Get list of ListModeRecords
sessions_to_report = get_listmode_issues(
pyxnat_interface=xnat_session, threshold_days=90, project_name=project_name
)

if debug_output:
print("Sessions failing listmode checks:")
if len(sessions_to_report) > 0:
for s in sessions_to_report:
print(s)
else:
print("None found")

if len(sessions_to_report) > 0:
# Construct email html body
body_html = construct_email(list_mode_records=sessions_to_report)

# Print email content so it is visible in the container log
print("Sending email with the following content:")
print(f"To: {to_emails}")
print(f"cc: {cc_emails}")
print(f"bcc: {bcc_emails}")
print(f"Subject: {email_subject}")
print(body_html)

# Send the email via XNAT
send_email(
session=xnat_session,
subject=email_subject,
to=to_emails,
cc=cc_emails,
bcc=bcc_emails,
content_html=body_html,
)


def main():
if len(sys.argv) < 3:
raise ValueError("No email list specified")
if len(sys.argv) < 2:
raise ValueError("No project name specified")

credentials = XnatContainerCredentials()

email_listmode(
credentials=credentials,
project_name=sys.argv[1],
email_subject="1946 Weekly Listmode Status Check",
to_emails=sys.argv[2].split(","),
)


if __name__ == "__main__":
main()
Loading

0 comments on commit 0e06aa1

Please sign in to comment.