Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
820840d
first query for flow sheets
skeating Feb 2, 2026
fb3d6da
building up scripts
skeating Feb 4, 2026
8476351
adding more files
skeating Feb 19, 2026
47240ff
Merge branch 'dev' into sk/queries
skeating Aug 5, 2026
56ceb52
Add and refine SQL scripts for EHR data extraction
skeating Aug 9, 2026
de65f8d
Refactor lab results query for wide format output
skeating Aug 24, 2026
4426711
Started on functions to wrap snakemake around sql calls
thompson318 Aug 11, 2026
1b8545a
Started to implement caboodle look up code
thompson318 Aug 12, 2026
a854492
Setting up config and docker bits
thompson318 Aug 12, 2026
7784854
Add a ehr location to snakefile
thompson318 Aug 17, 2026
bdf0eb2
Tidied up implementation and rewrote Snakefile logic, with help from …
thompson318 Aug 17, 2026
f4382d2
We don't need a separate container for ehr lookup
thompson318 Aug 17, 2026
de12629
Added ehr variables to settings and added a testing flag.
thompson318 Aug 17, 2026
314a613
added some caboodle faking logic to enable local running
thompson318 Aug 17, 2026
f33621d
Caboodle setup goes into exporter.env
thompson318 Aug 18, 2026
de875db
Format fix
thompson318 Aug 18, 2026
43e9f54
Beginning of test for ehr integration
thompson318 Aug 18, 2026
21e0e5b
SQL formatting
thompson318 Aug 18, 2026
2b28e42
Test with ehr lookup
thompson318 Aug 18, 2026
d4f2121
use caboodle testing
thompson318 Aug 18, 2026
a704072
Trying to test ehr and hash lookup with ftps
thompson318 Aug 18, 2026
8bd10d2
Moved sql
thompson318 Aug 19, 2026
76c8d9f
refactored db to make adding ehr lookup implementation clearer
thompson318 Aug 19, 2026
b7e5ce3
Started on logic to get flowsheets, first need hospital visit number
thompson318 Aug 19, 2026
87a8ea1
The exporter needs to access uds, do duplicated some of the config fr…
thompson318 Aug 19, 2026
8219da8
Use enviroment to manage sql locations as it varies between containers
thompson318 Aug 20, 2026
76144af
path into test
thompson318 Aug 20, 2026
093563f
Use format rather than parameters for schema name
thompson318 Aug 20, 2026
ece4174
Default for sql path to simplify testing
thompson318 Aug 20, 2026
6d3f470
Updated mocked function name in test controller
thompson318 Aug 20, 2026
d6b51e8
Enable a fake star db shortcut so that snakemake integration test can…
thompson318 Aug 24, 2026
f4a629b
Don't connect for fake star
thompson318 Aug 24, 2026
df0db43
Tidied up getting for hospital visit number
thompson318 Aug 26, 2026
e84f031
return integer hospital visit id
thompson318 Aug 26, 2026
1363c98
(with rewritten history) Merge branch 'sk/queries' into st/queries-to…
jeremyestein Sep 9, 2026
ca4789a
Added flowsheet query to ehr
thompson318 Aug 26, 2026
cba9b54
moved sql and implemented lab results
thompson318 Aug 27, 2026
c0850e5
Fixed column names in pseudon
thompson318 Aug 27, 2026
f497fc4
Flowsheets and labresults are star, not caboodle
thompson318 Aug 27, 2026
8a08337
Auto stash before merge of "sk/queries" and "st/queries-to-snakemake"
skeating Aug 31, 2026
b93c17d
star flow sheet query
skeating Sep 7, 2026
2fc2240
finished star lab queries
skeating Sep 7, 2026
f946149
removed un new scripts and moved read me to current script location
skeating Sep 8, 2026
e249346
added note about getting scripts from the waveform-private-queries re…
skeating Sep 8, 2026
65c74fd
Merge branch 'dev' into sk/queries
jeremyestein Sep 9, 2026
1218cb5
Post-merge linting fix
jeremyestein Sep 9, 2026
6de2a63
SCHEMA_NAME is accessed regardless of testing mode, so it has to exist
jeremyestein Sep 9, 2026
8c4bf82
Create a space for scripts that can't be in this repo to be installed to
jeremyestein Sep 10, 2026
7968f17
Bring in private SQL scripts
jeremyestein Sep 10, 2026
85ea6ad
Linting fix
jeremyestein Sep 11, 2026
1e44668
Add private repo to setup instructions
jeremyestein Sep 11, 2026
5b3222a
fixup docs for private sql scripts
jeremyestein Sep 14, 2026
d990007
Match changed signature in PIXL ftp method
jeremyestein Sep 11, 2026
1dac661
Use importlib.resources to locate SQL files and thus remove need for
jeremyestein Sep 14, 2026
44b1cf3
Document how to do an ad hoc run
jeremyestein Sep 14, 2026
0163626
First go at split of db.py into PG and SQL Server versions
jeremyestein Sep 14, 2026
edc39a7
Cursor's first go at PG->MSSQL conversion
jeremyestein Sep 14, 2026
3d22cba
Delete pointless test that doesn't test anything
jeremyestein Sep 14, 2026
39d5be6
Define all Caboodle timeouts in seconds to match API
jeremyestein Sep 14, 2026
523407f
timeout needs to be an int
jeremyestein Sep 15, 2026
4312789
Was getting runtime type error in pd.concat. Now always convert
jeremyestein Sep 15, 2026
d2a6775
Fix typo in SQL query
jeremyestein Sep 15, 2026
3a85bc9
Also return DataFrame for MSSQL queries
jeremyestein Sep 15, 2026
1d5f07d
MSSQL column info is a bit different
jeremyestein Sep 15, 2026
76989a9
Update the list of safe EHR columns to match the queries
jeremyestein Sep 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
.git
**/.mypy_cache
**/.pytest_tmp
**/build
16 changes: 16 additions & 0 deletions .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ jobs:
PROJECT_ROOT: ${{ github.workspace }}/waveform-controller
environment: hasher
steps:
- name: Generate GitHub App Token
id: generate-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.SAFEHR_READ_ONLY_WORKER_ID }}
private-key: ${{ secrets.SAFEHR_READ_ONLY_WORKER_KEY }}
repositories: waveform-private-queries
- uses: actions/checkout@v5
with:
path: waveform-controller
Expand All @@ -23,6 +30,13 @@ jobs:
repository: SAFEHR-data/PIXL
ref: e29f4b15c3b9d21c9a6e08c272aca7773311b32c # pragma: allowlist secret
path: PIXL
- name: Checkout private SQL scripts
uses: actions/checkout@v5
with:
repository: SAFEHR-data/waveform-private-queries
token: ${{ steps.generate-token.outputs.token }}
ref: 0345bb6e3ed51b05e46982434911c9138c84a035 # pragma: allowlist secret
path: waveform-private-queries

- name: Install uv
uses: astral-sh/setup-uv@v7
Expand Down Expand Up @@ -65,6 +79,8 @@ jobs:
echo "AZURE_KEY_VAULT_SECRET_NAME=${AZURE_KEY_VAULT_SECRET_NAME}"
} >> ../config/hasher.env

# install private SQL scripts
cp ../waveform-private-queries/src/sql/*.sql src/sql/private/
# exporter config can't be done here because test_snakemake_integration.py wires in its own config file

- name: Run the tests
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ wheels/

# IDEs
.idea/
.vscode/

# settings files (should not be in the source tree anyway, but just in case)
*.env
Expand Down
6 changes: 5 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ LABEL authors="Stephen Thompson, Jeremy Stein"
# put it on both images even though we only need it on exporter.
RUN export DEBIAN_FRONTEND=noninteractive && \
apt-get update && \
apt-get install --yes --no-install-recommends cron && \
apt-get install --yes --no-install-recommends \
cron \
libgssapi-krb5-2 \
libkrb5-3 \
libltdl7 && \
apt-get autoremove --yes && apt-get clean --yes && rm -rf /var/lib/apt/lists/*
# uv image label "0.12.5"
COPY --from=ghcr.io/astral-sh/uv@sha256:e85be844203885286c60ffad8a858d48afb6c5a5c237ca0e67f12e74b8f174b1 /uv /uvx /bin/
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,14 @@ separate to the Emap project root.


##### Clone repos
Clone this repo (`waveform-controller`) and [PIXL](https://github.com/SAFEHR-data/PIXL),
Clone
* this repo (`waveform-controller`)
* [PIXL](https://github.com/SAFEHR-data/PIXL),
* [Private query repo](https://github.com/SAFEHR-data/waveform-private-queries)
both inside your root directory.

Inside the PIXL repo, checkout the commit that we have pinned in [workflow file](.github/workflows/pytest.yml).
Inside the PIXL and Private query repos, checkout the respective commits that
we have pinned in [the workflow file](.github/workflows/pytest.yml).

If on a system that has access to sensitive data, disable push remotes on all cloned repos as follows:
```
Expand Down
1 change: 1 addition & 0 deletions config.EXAMPLE/controller.env.EXAMPLE
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ RABBITMQ_PASSWORD="my_pw"
RABBITMQ_HOST="localhost"
RABBITMQ_PORT=5672
RABBITMQ_QUEUE="waveform"

# OpenTelemetry OTLP/HTTP endpoint of the LGTM collector.
OTEL_EXPORTER_OTLP_ENDPOINT="http://lgtm:4318"
OTEL_SERVICE_NAME=waveform-controller
25 changes: 25 additions & 0 deletions config.EXAMPLE/exporter.env.EXAMPLE
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,31 @@ ONLY_USE_CSV_FROM_YESTERDAY=TRUE
# expression to match multiple date
PROCESS_CSV_FROM_DATE=

# We query Caboodle to get electronic healthcare record date per patient per day
CABOODLE_DBNAME="fakecab"
CABOODLE_USERNAME="inform_user"
CABOODLE_PASSWORD="inform"
CABOODLE_HOST="localhost"
CABOODLE_PORT="1433"
CABOODLE_CONNECT_TIMEOUT="10" # in seconds
CABOODLE_QUERY_TIMEOUT="10" # in seconds

# To avoid having to deploy a fake caboodle for testing we have
# a testing flag for Caboodle. If set TRUE caboodle connection will
# fail silently and ehr file will be created with fake data
CABOODLE_TESTING="FALSE"

# The following is duplicated from controller.env
# the exporter needs access to uds
UDS_DBNAME="fakeuds"
UDS_USERNAME="inform_user"
UDS_PASSWORD="inform"
UDS_HOST="172.17.0.1"
UDS_PORT="5433"
UDS_CONNECT_TIMEOUT="10"
UDS_QUERY_TIMEOUT="3000"
SCHEMA_NAME="schemaname"

# OpenTelemetry OTLP/HTTP endpoint of the LGTM collector.
OTEL_EXPORTER_OTLP_ENDPOINT="http://lgtm:4318"
OTEL_SERVICE_NAME=waveform-exporter
12 changes: 6 additions & 6 deletions docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ sledgehammer approach which is rather similar to
* Delete all Emap tables in `star_dev` as per Emap deployment instructions.
* Waveform: `docker compose down` to bring everything down
* git pull and rebuild containers for the two repos.
* **REMEMBER: that the files from waveform-private-queries repository need to be copied to the waveform-controller/src/sql/private directory.**
* Change config if necessary
* Bring it all up again

Expand Down Expand Up @@ -183,15 +184,14 @@ Here is the

Filtering by variable is not currently possible.

# Run de-id on ad adhoc basis
# Run the Snakemake workflow on an ad hoc basis (ie. de-id, EHR lookup, upload)

> [!NOTE]
> Due to the way scheduled-script.sh pulls in its config from the config file, the contents of
> that file will override any env vars you specify on the command line below.
> that file will override any env vars you specify via docker below. So, temporarily
> changing the exporter.env config file is the only way to pass in a certain config.

You need to temporarily change the exporter.env config file to run this command.

You are likely to want to set the following values (example date shown):
Variables you may wish to modify:
```
ONLY_USE_CSV_FROM_YESTERDAY=FALSE
# something shorter than the standard 180 may be needed if you only just processed the data
Expand All @@ -202,4 +202,4 @@ PROCESS_CSV_FROM_DATE=1234-12-12
docker compose run --entrypoint /app/exporter-scripts/scheduled-script.sh waveform-exporter
```

Remember to put the config back afterwards.
Remember to revert the config changes you made if applicable.
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ dependencies = [
"pika>=1.3.2",
"pre-commit>=4.5.0",
"snakemake==9.14.5",
"mssql-python>=1.13.0",
"opentelemetry-distro==0.63b1",
"opentelemetry-exporter-otlp-proto-http==1.42.1",
# need to be compatible with PIXL, which currently pins 2.9.11 (arguably it shouldn't)
Expand All @@ -36,6 +37,10 @@ coverage = [
[project.scripts]
emap-extract-waveform = "controller:receiver"

# Include .sql files as part of the package (otherwise only editable installs would work)
[tool.setuptools.package-data]
sql = ["*.sql", "private/*.sql"]

[tool.pytest.ini_options]
# Force temp dirs under the repo so Docker can mount them on macOS.
# The default under /private/var/folders seems to silently fail (gives you an empty directory)
Expand Down
11 changes: 6 additions & 5 deletions src/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from pika import spec
from pika.adapters.blocking_connection import BlockingChannel

import db as db # type:ignore
import db_pg
import settings as settings # type:ignore
import csv_writer as writer # type:ignore
import telemetry as telemetry # type:ignore
Expand Down Expand Up @@ -107,8 +107,7 @@ def finalise_message(outcome: MessageOutcome):

class WaveformController:
def __init__(self):
self.emap_db = db.starDB()
self.emap_db.init_query()
self.emap_db = db_pg.starDB()
self.emap_db.connect()

def waveform_callback(
Expand Down Expand Up @@ -210,7 +209,9 @@ def outcome(
)
lookup_success = True
try:
matched_mrn = self.emap_db.get_row(location_string, observation_time)
matched_mrn = self.emap_db.get_matched_mrn(
location_string, observation_time
)
except ValueError:
lookup_success = False
logger.error(
Expand All @@ -220,6 +221,7 @@ def outcome(
exc_info=True,
)
matched_mrn = ("unmatched_mrn", "unmatched_nhs", "unmatched_csn", False)
# matched_mrn = ("1234568", "12345678", "12345678", False)
except ConnectionError:
logger.error("Database error, will try again", exc_info=True)
return outcome("reject", reason="db_conn_err", requeue=True)
Expand All @@ -228,7 +230,6 @@ def outcome(
if opt_out:
logger.info("Research opt-out is set for mrn %s, not writing.", mrn)
return outcome("reject", reason="opt_out", requeue=False)

writer.write_frame(
source_variable_id=source_variable_id,
source_channel_id=source_channel_id,
Expand Down
30 changes: 28 additions & 2 deletions src/csv_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,18 @@

import csv
import json
import pandas as pd

from datetime import datetime
from typing import Optional

from locations import WAVEFORM_ORIGINAL_CSV, make_file_name, FILE_STEM_PATTERN
from locations import (
WAVEFORM_ORIGINAL_CSV,
WAVEFORM_PSEUDONYMISED_EHR,
make_file_name,
FILE_STEM_PATTERN,
EHR_STEM_PATTERN_HASHED,
)


def create_file_name(
Expand Down Expand Up @@ -76,7 +84,6 @@ def write_frame(
wv_writer = csv.writer(
fileout, delimiter=",", quoting=csv.QUOTE_ALL, lineterminator="\n"
)

# Encode value lists as JSON so parquet conversion can use json.loads
# (Python list repr breaks on commas / quotes in string values).
row_array = [
Expand All @@ -93,3 +100,22 @@ def write_frame(
]

wv_writer.writerow(row_array)


def write_ehr(
df: pd.DataFrame,
date_str: str,
hashed_csn: str,
) -> bool:
"""Writes a frame of electronic healthcare data to a csv file.

:return: True if write was successful.
"""
subs_dict = dict(date=date_str, hashed_csn=hashed_csn)
stem = make_file_name(EHR_STEM_PATTERN_HASHED, subs_dict)
filename = WAVEFORM_PSEUDONYMISED_EHR / f"{stem}_ehr.csv"
filename.parent.mkdir(exist_ok=True, parents=True)

df.to_csv(filename, index=False)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be output as parquet for consistency with the other parquet outputs, and disk space, typing, etc reasons.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we have an intermediate step (i.e. to_csv, then a second process converts csv to parquet, or go straight to parquet?)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we should go straight to parquet. The only purpose for the CSV intermediate for waveform data is that it's easy to append to. (Hence the desire to switch to a better appendable format #15)
But this is a batch operation so it can all be dumped in one go to parquet.


return True
55 changes: 0 additions & 55 deletions src/db.py

This file was deleted.

Loading
Loading