Skip to content

Commit 93ea382

Browse files
committed
👔 Update records in REDCap if already exist.
1 parent 4bf4785 commit 93ea382

5 files changed

Lines changed: 99 additions & 17 deletions

File tree

python_jobs/src/hbnmigration/_config_variables/redcap_variables/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,18 @@
77
)
88
"""Ripple endpoints."""
99

10+
headers = ImportWithFallback.literal(".redcap_variables", "headers", {})
11+
"""REDCap headers."""
12+
1013
Tokens = ImportWithFallback.literal(".redcap_variables", "Tokens", NotImplemented)
1114
"""REDCap API tokens."""
1215

1316
redcap_import_file = ImportWithFallback.literal(
1417
".redcap_variables", "redcap_import_file", NotImplemented
1518
)
1619
"""Path to REDCap import data."""
20+
21+
redcap_update_file = ImportWithFallback.literal(
22+
".redcap_variables", "redcap_update_file", NotImplemented
23+
)
24+
"""Path to REDCap update data."""
Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
"""Type stubs for REDCap variables."""
22

3-
from .redcap_variables import Endpoints, redcap_import_file, Tokens
3+
from .redcap_variables import (
4+
Endpoints,
5+
headers,
6+
redcap_import_file,
7+
redcap_update_file,
8+
Tokens,
9+
)
410

5-
__all__ = ["Endpoints", "Tokens", "redcap_import_file"]
11+
__all__ = ["Endpoints", "Tokens", "headers", "redcap_import_file", "redcap_update_file"]

python_jobs/src/hbnmigration/_config_variables/redcap_variables/redcap_variables.pyi

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@ class Endpoints(EndpointsABC):
1010
@property
1111
def base_url(self) -> str: ...
1212

13+
headers: dict[str, str]
14+
1315
@dataclass
1416
class Tokens:
1517
pid247: str
1618
pid757: str
1719

1820
redcap_import_file: Path
21+
redcap_update_file: Path

python_jobs/src/hbnmigration/_config_variables/ripple_variables/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,7 @@
1515

1616
study_ids = ImportWithFallback.literal(".ripple_variables", "study_ids", {})
1717
"""Ripple study IDs."""
18+
19+
ripple_import_file = ImportWithFallback.literal(
20+
".ripple_variables", "ripple_import_file", NotImplemented
21+
)

python_jobs/src/hbnmigration/from_ripple/to_redcap.py

Lines changed: 76 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@
33
from dataclasses import dataclass
44
import logging
55
from pathlib import Path
6-
from typing import Literal
6+
from typing import Literal, Optional
77

88
import numpy as np
99
import pandas as pd
1010
import requests
1111

1212
from .._config_variables import redcap_variables, ripple_variables
1313
from ..exceptions import NoData
14-
from ..utility_functions import initialize_logging, yesterday
14+
from ..utility_functions import fetch_api_data, initialize_logging, yesterday
1515

1616
initialize_logging()
1717
logger = logging.getLogger(__name__)
@@ -125,8 +125,58 @@ def prepare_redcap_data(df: pd.DataFrame) -> None:
125125
"""Prepare Ripple API returned data to be imported into REDCap."""
126126
copy_selected_redcap_df = set_redcap_columns(df)
127127

128-
# Save the new dataframe to a CSV file
129-
copy_selected_redcap_df.to_csv(redcap_variables.redcap_import_file, index=False)
128+
# Split into update and new
129+
to_update, new_subjects = get_redcap_subjects_to_update(copy_selected_redcap_df)
130+
131+
# Save the new dataframes to CSV files
132+
if not to_update.empty:
133+
to_update.to_csv(redcap_variables.redcap_update_file, index=False)
134+
if not new_subjects.empty:
135+
new_subjects.to_csv(redcap_variables.redcap_import_file, index=False)
136+
137+
138+
def get_redcap_subjects_to_update(
139+
df: pd.DataFrame,
140+
) -> tuple[pd.DataFrame, pd.DataFrame]:
141+
"""
142+
Get subjects from REDCap that need to be updated vs. new subjects.
143+
144+
Returns
145+
-------
146+
pd.DataFrame
147+
subjects to update
148+
pd.DataFrame
149+
new subjects
150+
151+
"""
152+
redcap_participant_consent_data = {
153+
"token": redcap_variables.Tokens.pid247,
154+
"content": "record",
155+
"action": "export",
156+
"format": "csv",
157+
"type": "flat",
158+
"csvDelimiter": "",
159+
"fields": "mrn,record_id",
160+
"rawOrLabel": "raw",
161+
"rawOrLabelHeaders": "raw",
162+
"exportCheckboxLabel": "false",
163+
"exportSurveyFields": "false",
164+
"exportDataAccessGroups": "false",
165+
"returnFormat": "csv",
166+
}
167+
df_redcap_consent_instruments = fetch_api_data(
168+
Endpoints.REDCap.base_url,
169+
redcap_variables.headers,
170+
redcap_participant_consent_data,
171+
)
172+
to_update = df[df["mrn"].isin(df_redcap_consent_instruments["mrn"])].copy()
173+
to_update = to_update.drop(columns=["record_id"]).merge(
174+
df_redcap_consent_instruments[["mrn", "record_id"]], on="mrn", how="left"
175+
)
176+
to_update = to_update[["record_id", "mrn", "email_consent"]]
177+
return to_update[to_update["mrn"].isin(df_redcap_consent_instruments["mrn"])], df[
178+
~df["mrn"].isin(df_redcap_consent_instruments["mrn"])
179+
]
130180

131181

132182
def prepare_ripple_to_ripple(df: pd.DataFrame) -> dict[str, str]:
@@ -160,17 +210,26 @@ def prepare_ripple_to_ripple(df: pd.DataFrame) -> dict[str, str]:
160210
return ripple_import_files
161211

162212

163-
def push_to_redcap(project_token: str) -> None:
213+
def push_to_redcap(project_token: str, update: Optional[bool] = None) -> None:
164214
"""Push the HBN Potential Participants MRN and Contact email to RedCap."""
165-
# Read the content of the CSV file
215+
if update is None:
216+
# Try both
217+
for _update in [True, False]:
218+
push_to_redcap(project_token, _update)
219+
return
220+
filepath = (
221+
redcap_variables.redcap_update_file
222+
if update
223+
else redcap_variables.redcap_import_file
224+
)
225+
if not filepath.exists():
226+
return
166227
try:
167-
with open(redcap_variables.redcap_import_file, "r") as file:
228+
# Read the content of the CSV file
229+
with open(filepath, "r") as file:
168230
csv_content = file.read()
169231
except FileNotFoundError:
170-
logger.exception(
171-
"Error: The file '%s' was not found.", redcap_variables.redcap_import_file
172-
)
173-
# You might want to create a placeholder or exit
232+
logger.exception("Error: The file '%s' was not found.", filepath)
174233
csv_content = "" # Default to empty string if file not found
175234
if csv_content:
176235
data = {
@@ -180,7 +239,7 @@ def push_to_redcap(project_token: str) -> None:
180239
"format": "csv",
181240
"type": "flat",
182241
"overwriteBehavior": "normal",
183-
"forceAutoNumber": "true",
242+
"forceAutoNumber": str(not update).lower,
184243
"data": csv_content,
185244
"returnContent": "count",
186245
"returnFormat": "csv",
@@ -240,11 +299,12 @@ def set_status_in_ripple(ripple_study: str, ripple_import_file: str) -> None:
240299
raise
241300

242301

243-
def cleanup() -> None:
302+
def cleanup(temp_files: list[str | Path]) -> None:
244303
"""Delete temporary files."""
245304
for filepath in [
246305
redcap_variables.redcap_import_file,
247-
ripple_variables.ripple_import_file,
306+
redcap_variables.redcap_update_file,
307+
*[Path(filepath) for filepath in temp_files],
248308
]:
249309
try:
250310
filepath.unlink(missing_ok=True)
@@ -258,6 +318,7 @@ def main(project_status: Literal["dev", "prod"] = "prod") -> None:
258318
"dev": {"token": redcap_variables.Tokens.pid757},
259319
"prod": {"token": redcap_variables.Tokens.pid247},
260320
}
321+
ripple_import_files: dict[str, str] = {}
261322
try:
262323
filtered_ripple_df = request_potential_participants()
263324
prepare_redcap_data(filtered_ripple_df)
@@ -268,7 +329,7 @@ def main(project_status: Literal["dev", "prod"] = "prod") -> None:
268329
except NoData:
269330
pass
270331
finally:
271-
cleanup()
332+
cleanup(list(ripple_import_files.values()))
272333

273334

274335
if __name__ == "__main__":

0 commit comments

Comments
 (0)