From 7e5a28bc208ace226942029b8a5286c1e40db445 Mon Sep 17 00:00:00 2001 From: Milan Malfait Date: Tue, 5 Mar 2024 18:22:51 +0000 Subject: [PATCH] Support configuration for multiple projects (#309) * Update config design diagram * Add example config yaml * Add pydantic model for project configuration * Add tests for pydantic config model * Add config loading helper * fix: Remove `classmethod` decorators and tell ruff that pydantic's validator is also a classmethod Setting the validators as `classmethod` will fail for some validations * Refactor config tests: split up invalid fields tests and create common base data object * Add some more asserts to config test and use `load_config()` helper * Add PyYAML as core dependency `config.py` needs it * Add copyright headers * Move `load_config` to top of file * feat: orthanc-anon project config (#312) ## Project configs ## Common - add env variable for project configs location - mount project configs location - make tag operations a list in config (limited to length 1 for now) - make config functions return `PixlConfig | Any` to get type hints - replace deprecated pydantic features - add utility function to load project config by slug only ### CLI - install cli as editable to find env files correctly - add function to check env for all env.sample keys ### orthanc-anon project config - orthanc anon now cofigurable (tag ops, modalities, destionation) - add query for project slug by non-hashed values - move anonymisation logic to dcmd package ### EHR project config - mark only processing tests with run_containers fixture - use project config to determine destination --------- Co-authored-by: Milan Malfait * Add project config template * Add documentation for Parquet files and export process (#280) * Add documentation for Parquet files and export process * Formatting * Move `TODO` to issue #306 * Remove PR references * Formatting * Move specific details to `pixl_core` docs and add links * Update directory structure on the FTPS server * Formatting * Rename docs/data -> docs/file_types * Link to `file_types` documentation * Add directory structures to docstrings * Update upload.py Co-authored-by: Stef Piatek * Fix docs link Co-authored-by: Jeremy Stein * Clarify that the radiology reports go through Cogstack Co-authored-by: Jeremy Stein * Add note about test files Co-authored-by: Jeremy Stein --------- Co-authored-by: Stef Piatek Co-authored-by: Jeremy Stein * Correct instructions for editable installs (#314) * docs: better editable install instructions * docs: editable install of pytest-pixl * Add copyright header to config template * Add secret fetching from Azure keyvault * Ignore local secrets * Add Azure dependenices * Refactor: move FTPS setup to separet module * Create AzureKeyVault class to handle project secrets fetching * Refactor `_connect_to_ftp` to take FTP settings as parameters * Add abstract `Uploader` and `FTPSUploader` classes to define uploading interface * Update upload tests to use `Uploader` interface * Revert back to `pydantic.validator` `field_validator` is only available in `pydantic>-2.0` * Add `MockFTPSUploader` for tests Opted to mock out the `FTPSUploader` for tests instead of the `AzureKeyVault` to set the configuration fields directly. * Update tests with `MockFTPSUploader` class * Partially added docs to describe project configuration * Small code reordering * Add `upload` method to `ParquetExports` class using the uploader strategy * Update EHR API to use the new uploader strategy * Load project config only when necessary * Move `parquet_export` fixture out of conftest Fixture is only used in `test_upload` anyway, and it avoids an issue where importing the `ParquetEport` class requires the `PROJECT_CONFIGS_DIR` envvar to be set, but is only set after the imports in the conftest. * Update test instruction in EHR API * Use uploader strategy in orthanc-anonn for DICOM uploads * Update required environment variables - Remove the `FTP_` variables (except `FTP_PORT`), as these are now retrievd from the Azure Keyvault - Add Azure Keyvault variables to docker-compose * Format docker-compose * Load `.secrets.env` in system tests Contains the Azure Keyvault credentials * Add instructions for setting up project config * Update core docs * Set PROJECT_CONFIGS_DIR envvar for cli tests * Use same config for `MockFTPSUploader` as for the `ftps_server` fixture * Fix core tests * Format docker-compose * Update cli docs Running tests no longer requires the extra script. * Fix: keyvault name envvar * Load local `.env` if it exists before setting `PROJECT_CONFIGS_DIR` * Improve logging * Move project configs in top-level `projects/` directory * Move exports dir in top-level projects * Allow setting an alias for Azure KV secret fetching * Remove debugging message * Set Azure Keyvault credentials as secrets in CI * Add Azure keyvault setup instructions Copied from the Hasher API docs * Add sample `.secrets.env` * Report which secret is missing * Set default value for `PROJECT_CONFIGS_DIR` in sample `.env` * Update destination options in README * Update Azure KV setup instructions * Raise wanring when destination not supported instead of error * Don't mention unsupported alternatives * No need for line continuation * Better function naming * Get FTP port from Azure Keyvault as well * Remove more references to `FTP_` environment variables * Fix formatting * Forgot another renaming instance * Remove more instances of `FTP_*` env variables * No more FTP dummy service * Refer to pytest-pixl plugin in test docs * Get FTP port from Keyvault as well * Allow 'none' destination for uploading * This string expansion isn't working * Exports are now under `projects/exports` * Docker mounts need absolute paths * Seems that we still need the `FTP_*` environment variables I suspect because they are still used by the pytest plugin ftpserver fixture. * Update exports dir in gitignore * Mostly for debugging but probably a useful check to avoid surprises * Think I finally found the bug * Update docs * Make sure exports dir exists in unit tests * Add check for public parquet exports * Rename system test checks for exports * refactor[config]: remove tag-operations from filenames * Update README.md typo * Update template_config.yaml * Update cli/README.md Co-authored-by: Stef Piatek * Switch back to `pydantic.field_validator` `pydantic.validator` is deprecated * Record pydantic version * Revert "Format docker-compose" This reverts commit 54d3cee7ebda02d41d29ac50aa7bffa052d7d40d. * Cache secret fetching from AZ keyvault * Set secret ENV variables for the whole system-test job This should omit the need to set them explicitly in `.secrets.env` * Fix Keyvault secret names in diagram * Add more abstract methods into the base Uploader class * add slugify reference * fix[dcmd]: type hints * refactor[core]: define uploader subpackage * fix[cli]: convert to_posix to str() * fix[imports] * fix docker compose * fix[core]: add back ftps uploader __init__ * Add static `create` method to `Uploader` base class to handle upload strategies * Make `uploader._base` non-private * Make `uploader.ftps` private * Update client code to use new `Uploader.create` method * Move `FTPSUploader` to `core.uploader.base` and make private Avoids circular imports * Fix test * remove mention of non-existing options in enum and in diagram * clarify lastpass secrets note * Limit scope of secret envvars * Revert "Move `FTPSUploader` to `core.uploader.base` and make private" This reverts commit 6ce589e21a01eda9075c8233473c669ca361c5cc. * Use package-level `get_uploader` factory instead of static method * Update client code to use `get_uploader` * Remove duplicate template --------- Co-authored-by: Peter Tsrunchev Co-authored-by: Stef Piatek Co-authored-by: Jeremy Stein --- .env.sample | 10 +- .github/workflows/main.yml | 14 +- .gitignore | 3 +- .secrets.env.sample | 6 + README.md | 142 +++++ cli/README.md | 5 +- cli/pyproject.toml | 9 +- cli/src/pixl_cli/_io.py | 2 +- cli/src/pixl_cli/main.py | 30 + cli/tests/conftest.py | 13 +- cli/tests/test_check_env.py | 43 ++ docker-compose.yml | 64 +- docker/orthanc-anon/Dockerfile | 1 - .../diagrams/pixl-multi-project-config.drawio | 22 +- .../diagrams/pixl-multi-project-config.png | Bin 126476 -> 138174 bytes docs/file_types/parquet_files.md | 2 +- orthanc/orthanc-anon/plugin/pixl.py | 100 ++- pixl_core/README.md | 14 +- pixl_core/pyproject.toml | 22 +- pixl_core/src/core/db/queries.py | 21 +- pixl_core/src/core/exports.py | 37 +- pixl_core/src/core/project_config.py | 101 +++ pixl_core/src/core/upload.py | 189 ------ pixl_core/src/core/uploader/__init__.py | 43 ++ pixl_core/src/core/uploader/_ftps.py | 198 ++++++ pixl_core/src/core/uploader/_secrets.py | 97 +++ pixl_core/src/core/uploader/base.py | 65 ++ pixl_core/tests/conftest.py | 37 +- pixl_core/tests/test_project_config.py | 72 +++ .../{test_upload.py => uploader/test_ftps.py} | 46 +- pixl_dcmd/pyproject.toml | 11 +- pixl_dcmd/src/pixl_dcmd/main.py | 70 ++- pixl_dcmd/tests/conftest.py | 3 + pixl_dcmd/tests/test_main.py | 11 +- pixl_ehr/README.md | 8 +- pixl_ehr/pyproject.toml | 9 +- pixl_ehr/src/pixl_ehr/main.py | 18 +- pixl_ehr/tests/conftest.py | 3 +- pixl_ehr/tests/test_processing.py | 3 + .../test-extract-uclh-omop-cdm.yaml | 0 ...ic-tube-project-ngt-only-full-dataset.yaml | 593 ++++++++++++++++++ .../configs/test-extract-uclh-omop-cdm.yaml | 24 + ...ic-tube-project-ngt-only-full-dataset.yaml | 24 + {exports => projects/exports}/.gitkeep | 0 pytest-pixl/tests/conftest.py | 6 - .../test_ftpserver_login.py | 2 +- template_config.yaml | 23 + test/.env | 3 + test/README.md | 33 +- test/conftest.py | 4 +- .../ftp-server/mounts/data/.gitkeep | 0 test/run-system-test.sh | 43 +- ...ogy_parquet.py => test_parquet_exports.py} | 15 +- 53 files changed, 1862 insertions(+), 452 deletions(-) create mode 100644 .secrets.env.sample create mode 100644 cli/tests/test_check_env.py create mode 100644 pixl_core/src/core/project_config.py delete mode 100644 pixl_core/src/core/upload.py create mode 100644 pixl_core/src/core/uploader/__init__.py create mode 100644 pixl_core/src/core/uploader/_ftps.py create mode 100644 pixl_core/src/core/uploader/_secrets.py create mode 100644 pixl_core/src/core/uploader/base.py create mode 100644 pixl_core/tests/test_project_config.py rename pixl_core/tests/{test_upload.py => uploader/test_ftps.py} (68%) rename orthanc/orthanc-anon/plugin/tag-operations.yaml => projects/configs/tag-operations/test-extract-uclh-omop-cdm.yaml (100%) create mode 100644 projects/configs/tag-operations/uclh-nasogastric-tube-project-ngt-only-full-dataset.yaml create mode 100644 projects/configs/test-extract-uclh-omop-cdm.yaml create mode 100644 projects/configs/uclh-nasogastric-tube-project-ngt-only-full-dataset.yaml rename {exports => projects/exports}/.gitkeep (100%) create mode 100644 template_config.yaml delete mode 100644 test/dummy-services/ftp-server/mounts/data/.gitkeep rename test/{test_radiology_parquet.py => test_parquet_exports.py} (80%) diff --git a/.env.sample b/.env.sample index 588a4a307..049a7b02a 100644 --- a/.env.sample +++ b/.env.sample @@ -82,10 +82,6 @@ AZ_DICOM_ENDPOINT_CLIENT_SECRET= AZ_DICOM_ENDPOINT_TENANT_ID= # EHR extraction API -PIXL_EHR_API_AZ_SUBSCRIPTION_ID= -PIXL_EHR_API_AZ_CLIENT_ID= -PIXL_EHR_API_AZ_CLIENT_SECRET= -PIXL_EHR_API_AZ_TENANT_ID= PIXL_EHR_API_AZ_STORAGE_ACCOUNT_NAME= PIXL_EHR_API_AZ_STORAGE_CONTAINER_NAME= PIXL_EHR_COGSTACK_REDACT_URL= @@ -100,7 +96,5 @@ PIXL_DICOM_TRANSFER_TIMEOUT=240 PIXL_QUERY_TIMEOUT=10 PIXL_MAX_MESSAGES_IN_FLIGHT=100 -# FTP server -FTP_HOST= -FTP_USER_NAME= -FTP_USER_PASSWORD= +# Project configs directory +PROJECT_CONFIGS_DIR=projects/configs diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 915cc2a9c..8e1582ec6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -131,9 +131,9 @@ jobs: - name: Install Python dependencies run: | - pip install pixl_core/ - pip install pytest-pixl/ - pip install cli/[test] + pip install -e pixl_core/ + pip install -e pytest-pixl/ + pip install -e cli/[test] - name: Run tests working-directory: cli/tests @@ -225,6 +225,9 @@ jobs: pip install -e pytest-pixl/ pip install -e cli/[test] + - name: Create .secrets.env + run: touch .secrets.env + - name: Build test services working-directory: test run: | @@ -236,6 +239,11 @@ jobs: - name: Run tests working-directory: test + env: + EXPORT_AZ_CLIENT_ID: ${{ secrets.EXPORT_AZ_CLIENT_ID }} + EXPORT_AZ_CLIENT_PASSWORD: ${{ secrets.EXPORT_AZ_CLIENT_PASSWORD }} + EXPORT_AZ_TENANT_ID: ${{ secrets.EXPORT_AZ_TENANT_ID }} + EXPORT_AZ_KEY_VAULT_NAME: ${{ secrets.EXPORT_AZ_KEY_VAULT_NAME }} run: | ./run-system-test.sh echo FINISHED SYSTEM TEST SCRIPT diff --git a/.gitignore b/.gitignore index ed0409bdb..3034017ff 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .env local.env pixl_config.yml +.secrets.env # Byte-compiled / optimized / DLL files __pycache__/ @@ -146,6 +147,6 @@ dmypy.json .vscode # project specific files -/exports/ +/projects/exports/ **/test_producer.csv /test/dummy-services/ftp-server/mounts/data/* diff --git a/.secrets.env.sample b/.secrets.env.sample new file mode 100644 index 000000000..ab3955c96 --- /dev/null +++ b/.secrets.env.sample @@ -0,0 +1,6 @@ +# Azure key vault +EXPORT_AZ_CLIENT_ID= +EXPORT_AZ_CLIENT_PASSWORD= +EXPORT_AZ_TENANT_ID= +EXPORT_AZ_KEY_VAULT_NAME= + diff --git a/README.md b/README.md index 74bacf14c..ff0add2ab 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,148 @@ For the deidentification of the EHR extracts, we rely on an instance running the [CogStack API](https://cogstack.org/) with a `/redact` endpoint. The URL of this instance should be set in `.env` as `COGSTACK_REDACT_URL`. +### 3. Configure a new project + +To configure a new project, follow these steps: + +1. Create a new `git` branch from `main` + + ```sh + git checkout main + git pull + git switch -c + ``` + +1. Copy the `template_config.yaml` file to a new file in the `projects/config` directory and fill + in the details. +1. The filename of the project config should be ``.yaml + + >[!NOTE] + > The project slug should match the [slugify](https://github.com/un33k/python-slugify)-ed project name in the `extract_summary.json` log file! + +2. [Open a PR in PIXL](https://github.com/UCLH-Foundry/PIXL/compare) to merge the new project config into `main` + +#### The config YAML file + +The configuration file defines: + +- Project name: the `` name of the Project +- The DICOM dataset modalities to retain (e.g. `["DX", "CR"]` for X-Ray studies) +- The anonymisation operations to be applied to the DICOM tags, by providing a file path to one or multiple YAML files +- The endpoints used to upload the anonymised DICOM data and the public and radiology + [parquet files](./docs/file_types/parquet_files.md). We currently support the following endpoints: + - `"none"`: no upload + - `"ftps"`: a secure FTP server (for both _DICOM_ and _parquet_ files) + + + + + +#### Project secrets + +Any credentials required for uploading the project's results should be stored in an **Azure Key Vault** +(set up instructions below). +PIXL will query this key vault for the required secrets at runtime. This requires the following +environment variables to be set in `.secrets.env` so that PIXL can connect to the key vault: + +- `EXPORT_AZ_CLIENT_ID`: the service principal's client ID, mapped to `AZURE_CLIENT ID` in `docker-compose` +- `EXPORT_AZ_CLIENT_PASSWORD`: the password, mapped to `AZURE_CLIENT_SECRET` in `docker-compose` +- `EXPORT_AZ_TENANT_ID`: ID of the service principal's tenant. Also called its 'directory' ID. Mapped to `AZURE_TENANT_ID` in `docker-compose` +- `EXPORT_AZ_KEY_VAULT_NAME` the name of the key vault, used to connect to the correct key vault + +Create the `.secrets.env` file in the _PIXL_ directory by copying the sample: + +```bash +cp .secrets.env.sample .secrets.env +``` + +and fill in the missing values (for dev purposes find the `pixl_dev_secrets.env` note on LastPass). + +
Azure Keyvault setup + +## Azure Keyvault setup + +_This is done for the \_UCLH_DIF_ `dev` tenancy, will need to be done once in the _UCLHAZ_ `prod` +tenancy when ready to deploy to production.\_ + +This Key Vault and secret must persist any infrastructure changes so should be separate from disposable +infrastructure services. ServicePrincipal is required to connect to the Key Vault. + +The application uses the ServicePrincipal and password to authenticate with Azure via environment +variables. See [here](https://learn.microsoft.com/en-us/python/api/azure-identity/azure.identity.environmentcredential?view=azure-python) +for more info. + +The Key Vault and ServicePrincipal have already been created for the `dev` environment and details +are stored in the `pixl-dev-secrets.env` note in the shared PIXL folder on _LastPass_. + +The process for doing so using the `az` CLI tool is described below. +This process must be repeated for `staging` & `prod` environments. + +### Step 1 + +Create the Azure Key Vault in an appropriate resource group: + +```bash +az keyvault create --resource-group --name --location "UKSouth" +``` + +### Step 2 + +Create Service Principal & grant access as per + +```bash +az ad sp create-for-rbac -n pixl-secrets --skip-assignment +``` + +This will produce the following output + +```json +{ + "appId": "", + "displayName": "", + "name": "http://", + "password": "", + "tenant": "" +} +``` + +### Step 3 + +Assign correct permissions to the newly created ServicePrincipal + +```bash +az keyvault set-policy --name --spn --secret-permissions backup delete get list set +``` + +### Step 4 + +Create a secret and store in the Key Vault + +Use Python to create a secret: + +```python +import secrets +secrets.token_urlsafe(32) +``` + +copy the secret and paste as below + +```bash +az keyvault secret set --vault-name "" --name "" --value "" +``` + +### Step 5 + +Save credentials in `.secrets.env` and a LastPass `PIXL Keyvault secrets` note. + +``` +EXPORT_AZ_CLIENT_ID= +EXPORT_AZ_CLIENT_PASSWORD= +EXPORT_AZ_TENANT_ID= +EXPORT_AZ_KEY_VAULT_NAME= +``` +
+ ## Run ### Start diff --git a/cli/README.md b/cli/README.md index fecdaf2f8..27102c8b2 100644 --- a/cli/README.md +++ b/cli/README.md @@ -133,9 +133,10 @@ pip install -e ../pixl_core/ -e .[test] ### Running tests -Tests can be run with `pytest` from the `tests` directory. +The CLI tests require a running instance of the `rabbitmq` service, for which we provide a +`docker-compose` [file](./tests/docker-compose.yml). The service is automatically started by the +`run_containers` _pytest_ fixture. So to run the tests, run ```bash -cd tests pytest ``` diff --git a/cli/pyproject.toml b/cli/pyproject.toml index 314835baa..f8bf900ed 100644 --- a/cli/pyproject.toml +++ b/cli/pyproject.toml @@ -1,22 +1,17 @@ [project] name = "pixl_cli" version = "0.0.4" -authors = [ - { name="PIXL authors" }, -] +authors = [{ name = "PIXL authors" }] description = "PIXL command line interface" readme = "README.md" requires-python = "<3.12" -classifiers = [ - "Programming Language :: Python :: 3" -] +classifiers = ["Programming Language :: Python :: 3"] dependencies = [ "core", "click==8.1.3", "coloredlogs==15.0.1", "pandas==1.5.1", "pyarrow==14.0.1", - "PyYAML==6.0.1" ] [project.optional-dependencies] diff --git a/cli/src/pixl_cli/_io.py b/cli/src/pixl_cli/_io.py index d4a5129ce..a2def1dd9 100644 --- a/cli/src/pixl_cli/_io.py +++ b/cli/src/pixl_cli/_io.py @@ -27,7 +27,7 @@ # The export root dir from the point of view of the docker host (which is where the CLI runs) # For the view from inside, see pixl_ehr/main.py: EHR_EXPORT_ROOT_DIR -HOST_EXPORT_ROOT_DIR = Path(__file__).parents[3] / "exports" +HOST_EXPORT_ROOT_DIR = Path(__file__).parents[3] / "projects" / "exports" def messages_from_state_file(filepath: Path) -> list[Message]: diff --git a/cli/src/pixl_cli/main.py b/cli/src/pixl_cli/main.py index 282d9d955..be9c8ce50 100644 --- a/cli/src/pixl_cli/main.py +++ b/cli/src/pixl_cli/main.py @@ -24,6 +24,7 @@ import requests from core.patient_queue.producer import PixlProducer from core.patient_queue.subscriber import PixlBlockingConsumer +from decouple import RepositoryEnv, UndefinedValueError, config from pixl_cli._config import SERVICE_SETTINGS, api_config_for_queue from pixl_cli._database import filter_exported_or_add_to_db @@ -48,6 +49,35 @@ def cli(*, debug: bool) -> None: set_log_level("WARNING" if not debug else "DEBUG") +@cli.command() +@click.option( + "--error", + is_flag=True, + show_default=True, + default=False, + help="Exit with error on missing env vars", +) +@click.option( + "--sample_env_file", + show_default=True, + default=None, + type=click.Path(exists=True), + help="Path to the sample env file", +) +def check_env(*, error: bool, sample_env_file: click.Path) -> None: + """Check that all variables from .env.sample are set either in .env or in environ""" + if not sample_env_file: + sample_env_file = Path(__file__).parents[3] / ".env.sample" + sample_config = RepositoryEnv(sample_env_file) + for key in sample_config.data: + try: + config(key) + except UndefinedValueError: # noqa: PERF203 + logger.warning("Environment variable %s is not set", key) + if error: + raise + + @cli.command() @click.option( "--queues", diff --git a/cli/tests/conftest.py b/cli/tests/conftest.py index 74f193a86..1c1ef9c1a 100644 --- a/cli/tests/conftest.py +++ b/cli/tests/conftest.py @@ -15,15 +15,14 @@ from __future__ import annotations import os -from typing import TYPE_CHECKING +from pathlib import Path import pytest from core.db.models import Base, Extract, Image from sqlalchemy import Engine, create_engine from sqlalchemy.orm import Session, sessionmaker -if TYPE_CHECKING: - import pathlib +os.environ["PROJECT_CONFIGS_DIR"] = str(Path(__file__).parents[2] / "projects/configs") # Set the necessary environment variables os.environ["PIXL_EHR_API_HOST"] = "localhost" @@ -47,9 +46,11 @@ @pytest.fixture(autouse=True) -def export_dir(tmp_path_factory: pytest.TempPathFactory) -> pathlib.Path: - """Tmp dir to for tests to extract to.""" - return tmp_path_factory.mktemp("export_base") / "exports" +def export_dir(tmp_path_factory: pytest.TempPathFactory) -> Path: + """Tmp dir for tests to extract to.""" + export_dir = tmp_path_factory.mktemp("export_base") / "projects" / "exports" + export_dir.mkdir(parents=True) + return export_dir @pytest.fixture(scope="module") diff --git a/cli/tests/test_check_env.py b/cli/tests/test_check_env.py new file mode 100644 index 000000000..1dbde5616 --- /dev/null +++ b/cli/tests/test_check_env.py @@ -0,0 +1,43 @@ +# Copyright (c) University College London Hospitals NHS Foundation Trust +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for check_env function of CLI.""" +from pathlib import Path + +from click.testing import CliRunner +from pixl_cli.main import check_env + +SAMPLE_ENV_FILE = Path(__file__).parents[2] / ".env.sample" + + +def test_check_env(): + """ + Test that the check_env command runs without error. + - check_env works + - current test env file matches the sample env file + """ + runner = CliRunner() + result = runner.invoke(check_env) + assert result.exit_code == 0 + + +def test_check_env_fails(tmp_path): + """ + Test that check_env fails when the current test env file does not match the sample env file. + """ # noqa: D200 either this or it's 102 chars + tmp_sample_env_file = tmp_path / ".env.sample" + tmp_sample_env_file.write_text("NONEXISTENT_VARIABLE=") + + runner = CliRunner() + result = runner.invoke(check_env, str(tmp_sample_env_file)) + assert result.exit_code != 0 diff --git a/docker-compose.yml b/docker-compose.yml index 2ccf4c381..ce10ec37d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,7 +35,7 @@ x-pixl-common-env: &pixl-common-env DEBUG: ${DEBUG} x-pixl-rabbit-mq: &pixl-rabbit-mq - RABBITMQ_HOST: "queue" # Name of the queue service + RABBITMQ_HOST: "queue" # Name of the queue service RABBITMQ_PORT: "5672" RABBITMQ_USERNAME: ${RABBITMQ_USERNAME} RABBITMQ_PASSWORD: ${RABBITMQ_PASSWORD} @@ -55,11 +55,11 @@ x-pixl-db: &pixl-db PIXL_DB_PASSWORD: ${PIXL_DB_PASSWORD} PIXL_DB_NAME: ${PIXL_DB_NAME} -x-ftp-host: &ftp-host - FTP_HOST: ${FTP_HOST} - FTP_USER_NAME: ${FTP_USER_NAME} - FTP_USER_PASSWORD: ${FTP_USER_PASSWORD} - FTP_PORT: ${FTP_PORT} +x-azure-keyvault: &azure-keyvault + AZURE_CLIENT_ID: ${EXPORT_AZ_CLIENT_ID} + AZURE_CLIENT_SECRET: ${EXPORT_AZ_CLIENT_PASSWORD} + AZURE_TENANT_ID: ${EXPORT_AZ_TENANT_ID} + AZURE_KEY_VAULT_NAME: ${EXPORT_AZ_KEY_VAULT_NAME} x-logs-volume: &logs-volume type: volume @@ -79,7 +79,6 @@ networks: ################################################################################ # Services services: - hasher-api: build: context: . @@ -102,7 +101,7 @@ services: networks: - pixl-net healthcheck: - test: [ "CMD", "curl", "-f", "http://hasher-api:8000/heart-beat" ] + test: ["CMD", "curl", "-f", "http://hasher-api:8000/heart-beat"] interval: 10s timeout: 30s retries: 5 @@ -116,7 +115,7 @@ services: <<: *build-args-common command: /run/secrets environment: - <<: [*proxy-common, *pixl-common-env, *ftp-host] + <<: [*proxy-common, *pixl-common-env, *azure-keyvault] ORTHANC_NAME: "PIXL: Anon" ORTHANC_USERNAME: ${ORTHANC_ANON_USERNAME} ORTHANC_PASSWORD: ${ORTHANC_ANON_PASSWORD} @@ -143,6 +142,7 @@ services: AZ_DICOM_TOKEN_REFRESH_SECS: "600" TIME_OFFSET: "${STUDY_TIME_OFFSET}" SALT_VALUE: ${SALT_VALUE}" + PROJECT_CONFIGS_DIR: /${PROJECT_CONFIGS_DIR:-/projects/configs} ports: - "${ORTHANC_ANON_DICOM_PORT}:4242" - "${ORTHANC_ANON_WEB_PORT}:8042" @@ -151,6 +151,7 @@ services: source: orthanc-anon-data target: /var/lib/orthanc/db - ${PWD}/orthanc/orthanc-anon/config:/run/secrets:ro + - ${PWD}/projects/configs:/${PROJECT_CONFIGS_DIR:-/projects/configs}:ro networks: - pixl-net # needed for same reason as ehr-api @@ -160,7 +161,15 @@ services: postgres: condition: service_healthy healthcheck: - test: [ "CMD", "curl", "-f", "-u" , "${ORTHANC_ANON_USERNAME}:${ORTHANC_ANON_PASSWORD}", "http://orthanc-anon:8042/heart-beat" ] + test: + [ + "CMD", + "curl", + "-f", + "-u", + "${ORTHANC_ANON_USERNAME}:${ORTHANC_ANON_PASSWORD}", + "http://orthanc-anon:8042/heart-beat", + ] interval: 10s timeout: 30s retries: 5 @@ -190,6 +199,7 @@ services: ORTHANC_ANON_AE_TITLE: ${ORTHANC_ANON_AE_TITLE} ORTHANC_ANON_DICOM_PORT: "4242" ORTHANC_ANON_HOSTNAME: "orthanc-anon" + PROJECT_CONFIGS_DIR: /${PROJECT_CONFIGS_DIR:-/projects/configs} ports: - "${ORTHANC_RAW_DICOM_PORT}:4242" - "${ORTHANC_RAW_WEB_PORT}:8042" @@ -197,6 +207,7 @@ services: - type: volume source: orthanc-raw-data target: /var/lib/orthanc/db + - ${PWD}/projects/configs:/${PROJECT_CONFIGS_DIR:-/projects/configs}:ro networks: - pixl-net depends_on: @@ -205,7 +216,15 @@ services: orthanc-anon: condition: service_started healthcheck: - test: [ "CMD", "curl", "-f", "-u" , "${ORTHANC_RAW_USERNAME}:${ORTHANC_RAW_PASSWORD}", "http://orthanc-raw:8042/heart-beat" ] + test: + [ + "CMD", + "curl", + "-f", + "-u", + "${ORTHANC_RAW_USERNAME}:${ORTHANC_RAW_PASSWORD}", + "http://orthanc-raw:8042/heart-beat", + ] interval: 10s timeout: 30s retries: 5 @@ -234,13 +253,19 @@ services: args: <<: *build-args-common environment: - <<: [*pixl-db, *emap-db, *proxy-common, *pixl-common-env, *pixl-rabbit-mq, *ftp-host] - AZURE_CLIENT_ID: ${PIXL_EHR_API_AZ_CLIENT_ID} - AZURE_CLIENT_SECRET: ${PIXL_EHR_API_AZ_CLIENT_SECRET} - AZURE_TENANT_ID: ${PIXL_EHR_API_AZ_TENANT_ID} + <<: + [ + *pixl-db, + *emap-db, + *proxy-common, + *pixl-common-env, + *pixl-rabbit-mq, + *azure-keyvault, + ] AZ_STORAGE_ACCOUNT_NAME: ${PIXL_EHR_API_AZ_STORAGE_ACCOUNT_NAME} AZ_STORAGE_CONTAINER_NAME: ${PIXL_EHR_API_AZ_STORAGE_CONTAINER_NAME} COGSTACK_REDACT_URL: ${PIXL_EHR_COGSTACK_REDACT_URL} + PROJECT_CONFIGS_DIR: /${PROJECT_CONFIGS_DIR:-/projects/configs} PIXL_MAX_MESSAGES_IN_FLIGHT: ${PIXL_MAX_MESSAGES_IN_FLIGHT} env_file: - ./docker/common.env @@ -264,7 +289,8 @@ services: extra_hosts: - "host.docker.internal:host-gateway" volumes: - - ${PWD}/exports:/run/exports + - ${PWD}/projects/exports:/run/projects/exports + - ${PWD}/projects/configs:/${PROJECT_CONFIGS_DIR:-/projects/configs}:ro imaging-api: build: @@ -302,8 +328,8 @@ services: ports: - "${PIXL_IMAGING_API_PORT}:8000" -################################################################################ -# Data Stores + ################################################################################ + # Data Stores postgres: build: context: . @@ -325,7 +351,7 @@ services: ports: - "${POSTGRES_PORT}:5432" healthcheck: - test: [ "CMD", "pg_isready", "-U", "${PIXL_DB_USER}" ] + test: ["CMD", "pg_isready", "-U", "${PIXL_DB_USER}"] interval: 10s timeout: 30s retries: 5 diff --git a/docker/orthanc-anon/Dockerfile b/docker/orthanc-anon/Dockerfile index 947e8c71e..d8857a5da 100644 --- a/docker/orthanc-anon/Dockerfile +++ b/docker/orthanc-anon/Dockerfile @@ -55,7 +55,6 @@ RUN --mount=type=cache,target=/root/.cache \ && pip3 install pixl_dcmd/ COPY ./orthanc/orthanc-anon/plugin/pixl.py /etc/orthanc/pixl.py -COPY ./orthanc/orthanc-anon/plugin/tag-operations.yaml /etc/orthanc/tag-operations.yaml COPY ./pixl_core/ ./pixl_core RUN --mount=type=cache,target=/root/.cache \ diff --git a/docs/design/diagrams/pixl-multi-project-config.drawio b/docs/design/diagrams/pixl-multi-project-config.drawio index 1b8512b03..62a6f07ac 100644 --- a/docs/design/diagrams/pixl-multi-project-config.drawio +++ b/docs/design/diagrams/pixl-multi-project-config.drawio @@ -1,6 +1,6 @@ - + - + @@ -72,7 +72,7 @@ - + @@ -85,7 +85,7 @@ - + @@ -97,13 +97,13 @@ - + - + - + @@ -115,8 +115,8 @@ - - + + @@ -126,10 +126,10 @@ - + - + diff --git a/docs/design/diagrams/pixl-multi-project-config.png b/docs/design/diagrams/pixl-multi-project-config.png index 9cffa92b4ae1b93a72bd071b1525f09d2e81254e..a79b08fefa4ca7c6fc30a0e1447997a1a6ba056e 100644 GIT binary patch literal 138174 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 literal 126476 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 diff --git a/docs/file_types/parquet_files.md b/docs/file_types/parquet_files.md index 0502202fe..1288bb003 100644 --- a/docs/file_types/parquet_files.md +++ b/docs/file_types/parquet_files.md @@ -42,5 +42,5 @@ Various _parquet_ files are provided throughout the repo to enable unit and syst `extract_summary.json` file to mimic the input received from OMOP-ES for the system tests During the system test, a `radiology.parquet` file is generated and temporarily stored in -`exports/test-extract-uclh-omop-cdm/latest/radiology/radiology.parquet` to check the successful +`projects/exports/test-extract-uclh-omop-cdm/latest/radiology/radiology.parquet` to check the successful de-identification before the DSH upload. This file is then deleted after the test. diff --git a/orthanc/orthanc-anon/plugin/pixl.py b/orthanc/orthanc-anon/plugin/pixl.py index 00c93f979..46273e87c 100644 --- a/orthanc/orthanc-anon/plugin/pixl.py +++ b/orthanc/orthanc-anon/plugin/pixl.py @@ -26,18 +26,18 @@ import threading import traceback from io import BytesIO -from pathlib import Path from time import sleep from typing import TYPE_CHECKING import requests -import yaml -from core import upload +from core.db.queries import get_project_slug_from_hashid +from core.project_config import load_project_config +from core.uploader import get_uploader from decouple import config from pydicom import dcmread import orthanc -import pixl_dcmd +from pixl_dcmd.main import anonymise_dicom, write_dataset_to_bytes if TYPE_CHECKING: from typing import Any @@ -135,6 +135,34 @@ def AzureDICOMTokenRefresh(): return None +def Send(resourceId: str) -> None: + """Send the resource to the appropriate destination""" + msg = f"Sending {resourceId}" + logger.debug(msg) + + hashed_patient_id = _get_patient_id(resourceId) + project_slug = get_project_slug_from_hashid(hashed_patient_id) + project_config = load_project_config(project_slug) + destination = project_config.destination.dicom + + uploader = get_uploader(project_slug, destination, project_config.project.azure_kv_alias) + msg = f"Sending {resourceId} via '{destination}'" + logger.debug(msg) + zip_content = _get_study_zip_archive(resourceId) + uploader.upload_dicom_image(zip_content, hashed_patient_id) + + +def _get_study_zip_archive(resourceId: str) -> BytesIO: + # Download zip archive of the DICOM resource + query = f"{ORTHANC_URL}/studies/{resourceId}/archive" + fail_msg = "Could not download archive of resource '%s'" + response_study = _query(resourceId, query, fail_msg) + + # get the zip content + logger.debug("Downloaded data for resource %s", resourceId) + return BytesIO(response_study.content) + + def SendViaStow(resourceId): """ Makes a POST API call to upload the resource to a dicom-web server @@ -164,25 +192,6 @@ def SendViaStow(resourceId): orthanc.LogError("Failed to send via STOW") -def SendViaFTPS(resourceId: str) -> None: - """ - Makes a POST API call to upload the resource to an FTPS server - using orthanc credentials as authorisation - """ - msg = f"Sending {resourceId} via FTPS" - logger.debug(msg) - # Download zip archive of the DICOM resource - query = f"{ORTHANC_URL}/studies/{resourceId}/archive" - fail_msg = "Could not download archive of resource '%s'" - response_study = _query(resourceId, query, fail_msg) - - # get the zip content - zip_content = response_study.content - logger.debug("Downloaded data for resource %s", resourceId) - - upload.upload_dicom_image(BytesIO(zip_content), _get_patient_id(resourceId)) - - def _get_patient_id(resourceId: str) -> str: """ Queries the Orthanc instance to get the PatientID for a given resource. @@ -226,7 +235,7 @@ def OnChange(changeType, level, resource): # noqa: ARG001 """ Three ChangeTypes included in this function: - If a study is stable and if should_auto_route returns true - then SendViaFTPS is called + then Send is called - If orthanc has started then message added to Orthanc LogWarning and AzureDICOMTokenRefresh called - If orthanc has stopped and TIMER is not none then message added @@ -238,7 +247,7 @@ def OnChange(changeType, level, resource): # noqa: ARG001 if changeType == orthanc.ChangeType.STABLE_STUDY: msg = f"Stable study: {resource}" logger.info(msg) - SendViaFTPS(resource) + Send(resource) if changeType == orthanc.ChangeType.ORTHANC_STARTED and _azure_available(): orthanc.LogWarning("Starting the scheduler") @@ -265,52 +274,15 @@ def ReceivedInstanceCallback(receivedDicom: bytes, origin: str) -> Any: # Read the bytes as DICOM/ dataset = dcmread(BytesIO(receivedDicom)) - # Drop anything that is not an X-Ray - if dataset.Modality not in ("DX", "CR"): - msg = f"Dropping DICOM Modality: {dataset.Modality}" - orthanc.LogError(msg) - return orthanc.ReceivedInstanceAction.DISCARD, None - # Attempt to anonymise and drop the study if any exceptions occur try: - return AnonymiseCallback(dataset) + dataset = anonymise_dicom(dataset) + return orthanc.ReceivedInstanceAction.MODIFY, write_dataset_to_bytes(dataset) except Exception: # noqa: BLE001 orthanc.LogError("Failed to anonymize study due to\n" + traceback.format_exc()) return orthanc.ReceivedInstanceAction.DISCARD, None -def AnonymiseCallback(dataset): - """ - Anonymisation of a dataset - Involves removing private tags and overlays and applying the - tag operations through functions in pixl_dcmd module - Returns writing anonymised dataset to disk - """ - orthanc.LogWarning("Anonymising received instance") - # Rip out all private tags/ - dataset.remove_private_tags() - orthanc.LogInfo("Removed private tags") - - # Rip out overlays/ - dataset = pixl_dcmd.remove_overlays(dataset) - orthanc.LogInfo("Removed overlays") - - # Apply anonymisation. - with Path("/etc/orthanc/tag-operations.yaml").open() as file: - # Load tag operations scheme from YAML. - tags = yaml.safe_load(file) - # Apply scheme to instance - dataset = pixl_dcmd.apply_tag_scheme(dataset, tags) - # Apply whitelist - dataset = pixl_dcmd.enforce_whitelist(dataset, tags) - orthanc.LogInfo("DICOM tag anonymisation applied") - - orthanc.LogWarning("DICOM tag anonymisation applied") - - # Write anonymised instance to disk. - return orthanc.ReceivedInstanceAction.MODIFY, pixl_dcmd.write_dataset_to_bytes(dataset) - - orthanc.RegisterOnChangeCallback(OnChange) orthanc.RegisterReceivedInstanceCallback(ReceivedInstanceCallback) orthanc.RegisterRestCallback("/heart-beat", OnHeartBeat) diff --git a/pixl_core/README.md b/pixl_core/README.md index 5f46be419..8e13d7d7e 100644 --- a/pixl_core/README.md +++ b/pixl_core/README.md @@ -96,16 +96,12 @@ for convenience `latest` is a symlink to the most recent extract. ## Uploading to an FTPS server The `core.upload` module implements functionality to upload DICOM images and parquet files to an -**FTPS server**. This requires the following environment variables to be set: +several destinations. This requires the following environment variables to be set: -- `FTP_HOST`: URL to the FTPS server -- `FTP_PORT`: port on which the FTPS server is listening -- `FTP_USER_NAME`: name of user with access to the FTPS server -- `FTP_USER_PASSWORD`: password for the authorised user - -We provide mock values for these for the unit tests (see -[`./tests/conftest.py`](./tests/conftest.py)). When running in production, these should be defined -in the `.env` file (see [the example](../.env.sample)). +The `Uploader` abstract class provides a consistent interface for uploading files. Child classes +such as the `FTPSUploader` implement the actual upload functionality. The credentials required for +uploading are queried from an **Azure Keyvault** instance (implemented in `_secrets.py`), for which +the setup instructions are in the [top-level README](../README.md#project-secrets) When an extract is ready to be published to the DSH, the PIXL pipeline will upload the **Public** and **Radiology** [_parquet_ files](../docs/data/parquet_files.md) to the `` directory diff --git a/pixl_core/pyproject.toml b/pixl_core/pyproject.toml index 26071427b..4a4efbea7 100644 --- a/pixl_core/pyproject.toml +++ b/pixl_core/pyproject.toml @@ -1,16 +1,14 @@ [project] name = "core" version = "0.0.1" -authors = [ - { name="PIXL core functionality" }, -] +authors = [{ name = "PIXL core functionality" }] description = "" readme = "README.md" requires-python = ">=3.9" -classifiers = [ - "Programming Language :: Python :: 3" -] +classifiers = ["Programming Language :: Python :: 3"] dependencies = [ + "azure-identity==1.12.0", + "azure-keyvault==4.2.0", "fastapi==0.109.1", "token-bucket==0.3.0", "python-decouple==3.6", @@ -24,6 +22,8 @@ dependencies = [ "psycopg2-binary==2.9.9", "pandas==1.5.1", "pyarrow==14.0.1", + "PyYAML==6.0.1", + "pydantic==2.6.3", ] [project.optional-dependencies] @@ -33,11 +33,7 @@ test = [ "httpx==0.24.*", "pytest-pixl", ] -dev = [ - "mypy", - "pre-commit", - "ruff", -] +dev = ["mypy", "pre-commit", "ruff"] [build-system] requires = ["setuptools>=61.0"] @@ -51,3 +47,7 @@ extend = "../ruff.toml" [tool.ruff.extend-per-file-ignores] "./tests/**" = ["D100"] + +[tool.ruff.pep8-naming] +# Allow Pydantic's `@field_validator` decorator to trigger class method treatment. +classmethod-decorators = ["classmethod", "pydantic.field_validator"] diff --git a/pixl_core/src/core/db/queries.py b/pixl_core/src/core/db/queries.py index b35978be3..743b246f8 100644 --- a/pixl_core/src/core/db/queries.py +++ b/pixl_core/src/core/db/queries.py @@ -34,7 +34,7 @@ engine = create_engine(url) -def get_project_slug_from_db(hashed_value: str) -> str: +def get_project_slug_from_hashid(hashed_value: str) -> str: """ Get the project slug from the PIXL database for a given hashed identifier. Throws an exception if the image has already been exported. @@ -75,3 +75,22 @@ def _query_existing_image(pixl_session: Session, hashed_value: str) -> Image: ) .one() ) + + +def get_project_slug(patientid: str, accession_number: str) -> str: + """Get the project slug from the PIXL database for a given patientid.""" + PixlSession = sessionmaker(engine) + with PixlSession() as pixl_session, pixl_session.begin(): + return str( + pixl_session.query(Extract) + .join(Image, Extract.extract_id == Image.extract_id) + .filter( + Image.mrn == patientid, + Image.accession_number == accession_number, + ) + .filter( + Extract.extract_id == Image.extract_id, + ) + .one() + .slug + ) diff --git a/pixl_core/src/core/exports.py b/pixl_core/src/core/exports.py index f02682004..0b9c32792 100644 --- a/pixl_core/src/core/exports.py +++ b/pixl_core/src/core/exports.py @@ -20,6 +20,9 @@ import slugify +from core.project_config import load_project_config +from core.uploader import get_uploader + if TYPE_CHECKING: import datetime import pathlib @@ -27,7 +30,7 @@ import pandas as pd -logger = logging.getLogger(__file__) +logger = logging.getLogger(__name__) class ParquetExport: @@ -80,9 +83,14 @@ def copy_to_exports(self, input_omop_dir: pathlib.Path) -> str: """ public_input = input_omop_dir / "public" - logging.info("Copying public parquet files from %s to %s", public_input, self.public_output) + logger.info("Copying public parquet files from %s to %s", public_input, self.public_output) + + # Make sure the base export direcotry exsists + if not self.export_dir.exists(): + msg = f"Export directory {self.export_dir} does not exist" + raise FileNotFoundError(msg) - # Make directory for exports if they don't exist + # Make directory for project exports ParquetExport._mkdir(self.public_output) # Copy extract files, overwriting if it exists @@ -97,7 +105,7 @@ def export_radiology(self, export_df: pd.DataFrame) -> pathlib.Path: """Export radiology reports to parquet file""" self._mkdir(self.radiology_output) parquet_file = self.radiology_output / "radiology.parquet" - logging.info("Exporting radiology to %s", self.radiology_output) + logger.info("Exporting radiology to %s", self.radiology_output) export_df.to_parquet(parquet_file) # We are not responsible for making the "latest" symlink, see `copy_to_exports`. # This avoids the confusion caused by EHR API (which calls export_radiology) having a @@ -110,3 +118,24 @@ def export_radiology(self, export_df: pd.DataFrame) -> pathlib.Path: def _mkdir(directory: pathlib.Path) -> pathlib.Path: directory.mkdir(parents=True, exist_ok=True) return directory + + def upload(self) -> None: + """Upload the latest extract to the DSH.""" + project_config = load_project_config(self.project_slug) + destination = project_config.destination.parquet + + if destination == "none": + msg = ( + f"Destination for parquet files for project {self.project_slug} is 'none'." + "Skipping upload." + ) + logger.info(msg) + + else: + uploader = get_uploader( + self.project_slug, destination, project_config.project.azure_kv_alias + ) + + msg = f"Uploading parquet files for project {self.project_slug} via '{destination}'" + logger.info(msg) + uploader.upload_parquet_files(self) diff --git a/pixl_core/src/core/project_config.py b/pixl_core/src/core/project_config.py new file mode 100644 index 000000000..e9ddc9789 --- /dev/null +++ b/pixl_core/src/core/project_config.py @@ -0,0 +1,101 @@ +# Copyright (c) University College London Hospitals NHS Foundation Trust +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Project-specific configuration for Pixl.""" +from __future__ import annotations + +import logging +from enum import Enum +from pathlib import Path +from typing import Any, Optional + +import yaml +from decouple import Config, RepositoryEmpty, RepositoryEnv +from pydantic import BaseModel, field_validator + +# Make sure local .env file is loaded if it exists +env_file = Path.cwd() / ".env" +config = Config(RepositoryEnv(env_file)) if env_file.exists() else Config(RepositoryEmpty()) +PROJECT_CONFIGS_DIR = Path(config("PROJECT_CONFIGS_DIR")) + +logger = logging.getLogger(__name__) + + +def load_project_config(project_slug: str) -> PixlConfig | Any: + """ + Load configuration for a project based on its slug. + Project needs to have a corresponding yaml file in the `$PROJECT_CONFIGS_DIR` directory. + """ + configpath = PROJECT_CONFIGS_DIR / f"{project_slug}.yaml" + logger.warning(f"Loading config for {project_slug} from {configpath}") # noqa: G004 + if not configpath.exists(): + raise FileNotFoundError(f"No config for {project_slug}. Please submit PR and redeploy.") # noqa: EM102, TRY003 + return _load_and_validate(configpath) + + +def _load_and_validate(filename: Path) -> PixlConfig | Any: + """ + Load configuration from a yaml file. + :param filename: Path to the yaml file + """ + yaml_data = yaml.safe_load(filename.read_text()) + return PixlConfig.model_validate(yaml_data) + + +class _Project(BaseModel): + name: str + azure_kv_alias: Optional[str] + modalities: list[str] + + +class _DestinationEnum(str, Enum): + """Defines the valid upload destinations.""" + + none = "none" + ftps = "ftps" + + +class _Destination(BaseModel): + dicom: _DestinationEnum + parquet: _DestinationEnum + + @field_validator("parquet") + def valid_parquet_destination(cls, v: str) -> str: + if v == "dicomweb": + msg = "Parquet destination cannot be dicomweb" + raise ValueError(msg) + return v + + +class PixlConfig(BaseModel): + """Project-specific configuration for Pixl.""" + + project: _Project + tag_operation_files: list[Path] + destination: _Destination + + @field_validator("tag_operation_files") + def _valid_tag_operations(cls, tag_ops_files: list[str]) -> list[Path]: + if not tag_ops_files or len(tag_ops_files) == 0: + msg = "There should be at least 1 tag operations file" + raise ValueError(msg) + + if len(tag_ops_files) > 1: + msg = "There should currently be at most 1 tag operations file." + raise ValueError(msg) + + # Pydantic will automatically check if the file exists + return [ + PROJECT_CONFIGS_DIR / "tag-operations" / tag_ops_file for tag_ops_file in tag_ops_files + ] diff --git a/pixl_core/src/core/upload.py b/pixl_core/src/core/upload.py deleted file mode 100644 index a8c59f730..000000000 --- a/pixl_core/src/core/upload.py +++ /dev/null @@ -1,189 +0,0 @@ -# Copyright (c) University College London Hospitals NHS Foundation Trust -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Functionality to upload files to an endpoint.""" - -from __future__ import annotations - -import ftplib -import logging -import ssl -from datetime import datetime, timezone -from ftplib import FTP_TLS -from pathlib import Path -from typing import TYPE_CHECKING, Any, BinaryIO - -from decouple import config - -if TYPE_CHECKING: - from socket import socket - - from core.exports import ParquetExport - - -from core.db.queries import get_project_slug_from_db, update_exported_at - -logger = logging.getLogger(__name__) - - -class ImplicitFtpTls(ftplib.FTP_TLS): - """ - FTP_TLS subclass that automatically wraps sockets in SSL to support implicit FTPS. - - https://stackoverflow.com/questions/12164470/python-ftp-implicit-tls-connection-issue - """ - - def __init__(self, *args: Any, **kwargs: Any) -> None: - """Create instance from parent class.""" - super().__init__(*args, **kwargs) - self._sock: socket | None = None - - @property - def sock(self) -> socket | None: - """Return the socket.""" - return self._sock - - @sock.setter - def sock(self, value: socket) -> None: - """When modifying the socket, ensure that it is ssl wrapped.""" - if value is not None and not isinstance(value, ssl.SSLSocket): - value = self.context.wrap_socket(value) - self._sock = value - - -def upload_dicom_image(zip_content: BinaryIO, pseudo_anon_id: str) -> None: - """Top level way to upload an image.""" - logger.info("Starting FTPS upload of '%s'", pseudo_anon_id) - - # rename destination to {project-slug}/{study-pseduonymised-id}.zip - remote_directory = get_project_slug_from_db(pseudo_anon_id) - - # Create the remote directory if it doesn't exist - ftp = _connect_to_ftp() - _create_and_set_as_cwd(ftp, remote_directory) - command = f"STOR {pseudo_anon_id}.zip" - logger.debug("Running %s", command) - - # Store the file using a binary handler - try: - logger.info("Running command %s", command) - ftp.storbinary(command, zip_content) - except ftplib.all_errors as ftp_error: - ftp.quit() - error_msg = f"Failed to run STOR command : {command}" - raise ConnectionError(error_msg, command, ftp_error) from ftp_error - - # Close the FTP connection - ftp.quit() - - update_exported_at(pseudo_anon_id, datetime.now(tz=timezone.utc)) - logger.info("Finished FTPS upload of '%s'", pseudo_anon_id) - - -def upload_parquet_files(parquet_export: ParquetExport) -> None: - """ - Upload parquet to FTPS under //parquet. - :param parquet_export: instance of the ParquetExport class - The final directory structure will look like this: - - ├── - │ └── parquet - │ ├── omop - │ │ └── public - │ │ └── PROCEDURE_OCCURRENCE.parquet - │ └── radiology - │ └── radiology.parquet - ├── .zip - └── .zip - ... - """ - logger.info("Starting FTPS upload of files for '%s'", parquet_export.project_slug) - - source_root_dir = parquet_export.current_extract_base - # Create the remote directory if it doesn't exist - ftp = _connect_to_ftp() - _create_and_set_as_cwd(ftp, parquet_export.project_slug) - _create_and_set_as_cwd(ftp, parquet_export.extract_time_slug) - _create_and_set_as_cwd(ftp, "parquet") - - # get the upload root directory before we do anything as we'll need - # to return to it (will it always be absolute?) - upload_root_dir = Path(ftp.pwd()) - if not upload_root_dir.is_absolute(): - logger.error("server remote path is not absolute, what are we going to do?") - - # absolute paths of the source - source_files = [x for x in source_root_dir.rglob("*.parquet") if x.is_file()] - if not source_files: - msg = f"No files found in {source_root_dir}" - raise FileNotFoundError(msg) - - # throw exception if empty dir - for source_path in source_files: - _create_and_set_as_cwd(ftp, str(upload_root_dir)) - source_rel_path = source_path.relative_to(source_root_dir) - source_rel_dir = source_rel_path.parent - source_filename_only = source_rel_path.relative_to(source_rel_dir) - _create_and_set_as_cwd_multi_path(ftp, source_rel_dir) - with source_path.open("rb") as handle: - command = f"STOR {source_filename_only}" - - # Store the file using a binary handler - ftp.storbinary(command, handle) - - # Close the FTP connection - ftp.quit() - logger.info("Finished FTPS upload of files for '%s'", parquet_export.project_slug) - - -def _connect_to_ftp() -> FTP_TLS: - # Set your FTP server details - ftp_host = config("FTP_HOST") - ftp_port = config("FTP_PORT") # FTPS usually uses port 21 - ftp_user = config("FTP_USER_NAME") - ftp_password = config("FTP_USER_PASSWORD") - - # Connect to the server and login - try: - ftp = ImplicitFtpTls() - ftp.connect(ftp_host, int(ftp_port)) - ftp.login(ftp_user, ftp_password) - ftp.prot_p() - except ftplib.all_errors as ftp_error: - error_msg = f"Failed to connect to FTPS server: {ftp_user}@{ftp_host}:{ftp_port}" - raise ConnectionError(error_msg) from ftp_error - return ftp - - -def _create_and_set_as_cwd_multi_path(ftp: FTP_TLS, remote_multi_dir: Path) -> None: - """Create (and cwd into) a multi dir path, analogously to mkdir -p""" - if remote_multi_dir.is_absolute(): - # would require some special handling and we don't need it - err = "must be relative path" - raise ValueError(err) - logger.info("_create_and_set_as_cwd_multi_path %s", remote_multi_dir) - # path should be pretty normalised, so assume split is safe - sub_dirs = str(remote_multi_dir).split("/") - for sd in sub_dirs: - _create_and_set_as_cwd(ftp, sd) - - -def _create_and_set_as_cwd(ftp: FTP_TLS, project_dir: str) -> None: - try: - ftp.cwd(project_dir) - logger.debug("'%s' exists on remote ftp, so moving into it", project_dir) - except ftplib.error_perm: - logger.info("creating '%s' on remote ftp and moving into it", project_dir) - # Directory doesn't exist, so create it - ftp.mkd(project_dir) - ftp.cwd(project_dir) diff --git a/pixl_core/src/core/uploader/__init__.py b/pixl_core/src/core/uploader/__init__.py new file mode 100644 index 000000000..23cbdb0f4 --- /dev/null +++ b/pixl_core/src/core/uploader/__init__.py @@ -0,0 +1,43 @@ +# Copyright (c) University College London Hospitals NHS Foundation Trust +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Uploader package +Contains base uploader class and +- FTPS uploader class +- **in progress** Azure uploader class +- **in progress** DICOMWeb uploader class + +Uploader class gets appropriate secret credentials from Azure key vault and uploads data +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional + +from core.uploader._ftps import FTPSUploader + +if TYPE_CHECKING: + from core.uploader.base import Uploader + + +# Intenitonally defined in __init__.py to avoid circular imports +def get_uploader(project_slug: str, destination: str, keyvault_alias: Optional[str]) -> Uploader: + """Uploader Factory, returns uploader instance based on destination.""" + choices: dict[str, type[Uploader]] = {"ftps": FTPSUploader} + try: + return choices[destination](project_slug, keyvault_alias) + + except KeyError: + error_msg = f"Destination '{destination}' is currently not supported" + raise NotImplementedError(error_msg) from None diff --git a/pixl_core/src/core/uploader/_ftps.py b/pixl_core/src/core/uploader/_ftps.py new file mode 100644 index 000000000..547090e8f --- /dev/null +++ b/pixl_core/src/core/uploader/_ftps.py @@ -0,0 +1,198 @@ +# Copyright (c) University College London Hospitals NHS Foundation Trust +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Helper functions for setting up a connection to an FTPS server.""" + +from __future__ import annotations + +import ftplib +import logging +import ssl +from datetime import datetime, timezone +from ftplib import FTP_TLS +from pathlib import Path +from typing import TYPE_CHECKING, Any, BinaryIO, Optional + +from core.db.queries import get_project_slug_from_hashid, update_exported_at +from core.uploader.base import Uploader + +if TYPE_CHECKING: + from socket import socket + + from core.exports import ParquetExport + +logger = logging.getLogger(__name__) + + +class ImplicitFtpTls(ftplib.FTP_TLS): + """ + FTP_TLS subclass that automatically wraps sockets in SSL to support implicit FTPS. + + https://stackoverflow.com/questions/12164470/python-ftp-implicit-tls-connection-issue + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Create instance from parent class.""" + super().__init__(*args, **kwargs) + self._sock: socket | None = None + + @property + def sock(self) -> socket | None: + """Return the socket.""" + return self._sock + + @sock.setter + def sock(self, value: socket) -> None: + """When modifying the socket, ensure that it is ssl wrapped.""" + if value is not None and not isinstance(value, ssl.SSLSocket): + value = self.context.wrap_socket(value) + self._sock = value + + +class FTPSUploader(Uploader): + """Upload strategy for an FTPS server.""" + + def __init__(self, project_slug: str, keyvault_alias: Optional[str]) -> None: + """Create instance of parent class""" + super().__init__(project_slug, keyvault_alias) + + def _set_config(self) -> None: + # Use the Azure KV alias as prefix if it exists, otherwise use the project name + az_prefix = self.keyvault_alias + az_prefix = az_prefix if az_prefix else self.project_slug + + self.host = self.keyvault.fetch_secret(f"{az_prefix}--ftp--host") + self.user = self.keyvault.fetch_secret(f"{az_prefix}--ftp--username") + self.password = self.keyvault.fetch_secret(f"{az_prefix}--ftp--password") + self.port = int(self.keyvault.fetch_secret(f"{az_prefix}--ftp--port")) + + def upload_dicom_image(self, zip_content: BinaryIO, pseudo_anon_id: str) -> None: + """Upload a DICOM image to the FTPS server.""" + logger.info("Starting FTPS upload of '%s'", pseudo_anon_id) + + # rename destination to {project-slug}/{study-pseduonymised-id}.zip + remote_directory = get_project_slug_from_hashid(pseudo_anon_id) + + # Create the remote directory if it doesn't exist + ftp = _connect_to_ftp(self.host, self.port, self.user, self.password) + _create_and_set_as_cwd(ftp, remote_directory) + command = f"STOR {pseudo_anon_id}.zip" + logger.debug("Running %s", command) + + # Store the file using a binary handler + try: + ftp.storbinary(command, zip_content) + except ftplib.all_errors as ftp_error: + ftp.quit() + error_msg = "Failed to run STOR command '%s': '%s'" + raise ConnectionError(error_msg, command, ftp_error) from ftp_error + + # Close the FTP connection + ftp.quit() + + # Update the exported_at timestamp in the PIXL database + update_exported_at(pseudo_anon_id, datetime.now(tz=timezone.utc)) + logger.info("Finished FTPS upload of '%s'", pseudo_anon_id) + + def upload_parquet_files(self, parquet_export: ParquetExport) -> None: + """ + Upload parquet to FTPS under //parquet. + :param parquet_export: instance of the ParquetExport class + The final directory structure will look like this: + + ├── + │ └── parquet + │ ├── omop + │ │ └── public + │ │ └── PROCEDURE_OCCURRENCE.parquet + │ └── radiology + │ └── radiology.parquet + ├── .zip + └── .zip + ... + """ + logger.info("Starting FTPS upload of files for '%s'", parquet_export.project_slug) + + source_root_dir = parquet_export.current_extract_base + # Create the remote directory if it doesn't exist + ftp = _connect_to_ftp(self.host, self.port, self.user, self.password) + _create_and_set_as_cwd(ftp, parquet_export.project_slug) + _create_and_set_as_cwd(ftp, parquet_export.extract_time_slug) + _create_and_set_as_cwd(ftp, "parquet") + + # get the upload root directory before we do anything as we'll need + # to return to it (will it always be absolute?) + upload_root_dir = Path(ftp.pwd()) + if not upload_root_dir.is_absolute(): + logger.error("server remote path is not absolute, what are we going to do?") + + # absolute paths of the source + source_files = [x for x in source_root_dir.rglob("*.parquet") if x.is_file()] + if not source_files: + msg = f"No files found in {source_root_dir}" + raise FileNotFoundError(msg) + + # throw exception if empty dir + for source_path in source_files: + _create_and_set_as_cwd(ftp, str(upload_root_dir)) + source_rel_path = source_path.relative_to(source_root_dir) + source_rel_dir = source_rel_path.parent + source_filename_only = source_rel_path.relative_to(source_rel_dir) + _create_and_set_as_cwd_multi_path(ftp, source_rel_dir) + with source_path.open("rb") as handle: + command = f"STOR {source_filename_only}" + + # Store the file using a binary handler + ftp.storbinary(command, handle) + + # Close the FTP connection + ftp.quit() + logger.info("Finished FTPS upload of files for '%s'", parquet_export.project_slug) + + +def _connect_to_ftp(ftp_host: str, ftp_port: int, ftp_user: str, ftp_password: str) -> FTP_TLS: + # Connect to the server and login + try: + ftp = ImplicitFtpTls() + ftp.connect(ftp_host, int(ftp_port)) + ftp.login(ftp_user, ftp_password) + ftp.prot_p() + except ftplib.all_errors as ftp_error: + error_msg = "Failed to connect to FTPS server" + raise ConnectionError(error_msg, ftp_error) from ftp_error + return ftp + + +def _create_and_set_as_cwd_multi_path(ftp: FTP_TLS, remote_multi_dir: Path) -> None: + """Create (and cwd into) a multi dir path, analogously to mkdir -p""" + if remote_multi_dir.is_absolute(): + # would require some special handling and we don't need it + err = "must be relative path" + raise ValueError(err) + logger.info("_create_and_set_as_cwd_multi_path %s", remote_multi_dir) + # path should be pretty normalised, so assume split is safe + sub_dirs = str(remote_multi_dir).split("/") + for sd in sub_dirs: + _create_and_set_as_cwd(ftp, sd) + + +def _create_and_set_as_cwd(ftp: FTP_TLS, project_dir: str) -> None: + try: + ftp.cwd(project_dir) + logger.debug("'%s' exists on remote ftp, so moving into it", project_dir) + except ftplib.error_perm: + logger.info("creating '%s' on remote ftp and moving into it", project_dir) + # Directory doesn't exist, so create it + ftp.mkd(project_dir) + ftp.cwd(project_dir) diff --git a/pixl_core/src/core/uploader/_secrets.py b/pixl_core/src/core/uploader/_secrets.py new file mode 100644 index 000000000..c9fe2e834 --- /dev/null +++ b/pixl_core/src/core/uploader/_secrets.py @@ -0,0 +1,97 @@ +# Copyright (c) 2022 University College London Hospitals NHS Foundation Trust +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Handles fetching of project secrets from the Azure Keyvault""" + +import subprocess +from functools import lru_cache + +from azure.identity import DefaultAzureCredential +from azure.keyvault.secrets import SecretClient +from decouple import config + + +class AzureKeyVault: + """Handles fetching of project secrets from the Azure Keyvault""" + + def __init__(self) -> None: + """ + Initialise the AzureKeyVault instance + + Creates an EnvironmentCredential via AzureDefaultCredential to connect with a + ServicePrincipal and secret configured via environment variables. + + This requires the following environment variables to be set, which will be picked up by the + Azure SDK: + - AZURE_CLIENT_ID + - AZURE_CLIENT_SECRET + - AZURE_TENANT_ID + - AZURE_KEY_VAULT_NAME + """ + self._check_envvars() + self.kv_name = config("AZURE_KEY_VAULT_NAME") + self.client = self._connect_to_keyvault() + + def _connect_to_keyvault(self) -> SecretClient: + key_vault_uri = f"https://{self.kv_name}.vault.azure.net" + + credentials = DefaultAzureCredential() + return SecretClient(vault_url=key_vault_uri, credential=credentials) + + def fetch_secret(self, secret_name: str) -> str: + """ + Fetch a secret from the Azure Key Vault instance specified in the environment variables. + :param secret_name: the name of the secret to fetch + :return: the requested secret's value + """ + return _fetch_secret(self.client, secret_name) + + def _check_envvars(self) -> None: + """ + Check if the required environment variables are set. + These need to be set system-wide, as the Azure SDK picks them up from the environment. + :raises OSError: if any of the environment variables are not set + """ + _check_system_envvar("AZURE_CLIENT_ID") + _check_system_envvar("AZURE_CLIENT_SECRET") + _check_system_envvar("AZURE_TENANT_ID") + _check_system_envvar("AZURE_KEY_VAULT_NAME") + + +def _check_system_envvar(var_name: str) -> None: + """Check if an environment variable is set system-wide""" + error_msg = f"Environment variable {var_name} not set" + if not subprocess.check_output(f"echo ${var_name}", shell=True).decode().strip(): # noqa: S602 + raise OSError(error_msg) + + +@lru_cache +def _fetch_secret(client: SecretClient, secret_name: str) -> str: + """ + Fetch a secret from the Azure Key Vault instance specified in the environment variables. + This method is cached to avoid unnecessary calls to the Key Vault using the LRU (least + recently used) strategy. + + This helper is intentionally defined outside the Azure Keyvault to prevent memory leaks (see + ruff rule B019). + + :param client: a SecretClient instance + :param secret_name: the name of the secret to fetch + :return: the requested secret's value + """ + secret = client.get_secret(secret_name).value + if secret is None: + msg = f"Azure Key Vault secret {secret_name} is None" + raise ValueError(msg) + return str(secret) diff --git a/pixl_core/src/core/uploader/base.py b/pixl_core/src/core/uploader/base.py new file mode 100644 index 000000000..2aef17273 --- /dev/null +++ b/pixl_core/src/core/uploader/base.py @@ -0,0 +1,65 @@ +# Copyright (c) University College London Hospitals NHS Foundation Trust +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Functionality to upload files to a remote server.""" + +from __future__ import annotations + +import logging +from abc import ABC, abstractmethod +from typing import Any, Optional + +from core.uploader._secrets import AzureKeyVault + +logger = logging.getLogger(__name__) + + +class Uploader(ABC): + """Upload strategy interface.""" + + @abstractmethod + def __init__(self, project_slug: str, keyvault_alias: Optional[str]) -> None: + """ + Initialise the uploader for a specific project with the destination configuration and an + AzureKeyvault instance. The keyvault is used to fetch the secrets required to connect to + the remote destination. + + Child classes should implement the _set_config method to set the configuration for the + upload strategy. + + :param : + """ + self.project_slug = project_slug + self.keyvault_alias = keyvault_alias + self.keyvault = AzureKeyVault() + self._set_config() + + @abstractmethod + def _set_config(self) -> None: + """Set the configuration for the uploader.""" + + @abstractmethod + def upload_dicom_image(self, *args: Any, **kwargs: Any) -> None: + """ + Abstract method to upload DICOM images. To be overwritten by child classes. + If an upload strategy does not support DICOM images, this method should raise a + NotImplementedError. + """ + + @abstractmethod + def upload_parquet_files(self, *args: Any, **kwargs: Any) -> None: + """ + Abstract method to upload parquet files. To be overwritten by child classes. + If an upload strategy does not support parquet files, this method should raise a + NotImplementedError. + """ diff --git a/pixl_core/tests/conftest.py b/pixl_core/tests/conftest.py index 90ade53d6..e9bf6d2c2 100644 --- a/pixl_core/tests/conftest.py +++ b/pixl_core/tests/conftest.py @@ -22,7 +22,7 @@ import pytest from core.db.models import Base, Extract, Image -from core.exports import ParquetExport +from core.uploader._ftps import FTPSUploader from pytest_pixl.helpers import run_subprocess from pytest_pixl.plugin import FtpHostAddress from sqlalchemy import Engine, create_engine @@ -42,9 +42,11 @@ os.environ["RABBITMQ_PASSWORD"] = "guest" # noqa: S105 Hardcoding password os.environ["RABBITMQ_HOST"] = "localhost" os.environ["RABBITMQ_PORT"] = "25672" +os.environ["PROJECT_CONFIGS_DIR"] = str(TEST_DIR.parents[1] / "projects/configs") + os.environ["FTP_HOST"] = "localhost" os.environ["FTP_USER_NAME"] = "pixl" -os.environ["FTP_USER_PASSWORD"] = "longpassword" # noqa: S105 Hardcoding password +os.environ["FTP_PASSWORD"] = "longpassword" # noqa: S105 Hardcoding password os.environ["FTP_PORT"] = "20021" @@ -68,6 +70,23 @@ def run_containers() -> subprocess.CompletedProcess[bytes]: ) +class MockFTPSUploader(FTPSUploader): + """Mock FTPSUploader for testing.""" + + def __init__(self) -> None: + """Initialise the mock uploader with hardcoded values for FTPS config.""" + self.host = os.environ["FTP_HOST"] + self.user = os.environ["FTP_USER_NAME"] + self.password = os.environ["FTP_PASSWORD"] + self.port = int(os.environ["FTP_PORT"]) + + +@pytest.fixture() +def ftps_uploader() -> MockFTPSUploader: + """Return a MockFTPSUploader object.""" + return MockFTPSUploader() + + @pytest.fixture() def ftps_home_dir(ftps_server) -> Generator[Path, None, None]: """ @@ -185,14 +204,6 @@ def already_exported_dicom_image(rows_in_session) -> Image: @pytest.fixture(autouse=True) def export_dir(tmp_path_factory: pytest.TempPathFactory) -> pathlib.Path: """Tmp dir to for tests to extract to.""" - return tmp_path_factory.mktemp("export_base") / "exports" - - -@pytest.fixture() -def parquet_export(export_dir) -> ParquetExport: - """Return a ParquetExport object.""" - return ParquetExport( - project_name="i-am-a-project", - extract_datetime=datetime.datetime.now(tz=datetime.timezone.utc), - export_dir=export_dir, - ) + export_dir = tmp_path_factory.mktemp("export_base") / "exports" + export_dir.mkdir() + return export_dir diff --git a/pixl_core/tests/test_project_config.py b/pixl_core/tests/test_project_config.py new file mode 100644 index 000000000..21df06081 --- /dev/null +++ b/pixl_core/tests/test_project_config.py @@ -0,0 +1,72 @@ +# Copyright (c) University College London Hospitals NHS Foundation Trust +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from pathlib import Path + +import pytest +from core.project_config import PixlConfig, _load_and_validate +from decouple import config +from pydantic import ValidationError + +PROJECT_CONFIGS_DIR = Path(config("PROJECT_CONFIGS_DIR")) +TEST_CONFIG = PROJECT_CONFIGS_DIR / "test-extract-uclh-omop-cdm.yaml" + + +def test_config_from_file(): + """Test whether config file is correctly parsed and validated.""" + project_config = _load_and_validate(TEST_CONFIG) + + assert project_config.project.name == "test-extract-uclh-omop-cdm" + assert project_config.project.modalities == ["DX", "CR"] + assert project_config.tag_operation_files == [ + PROJECT_CONFIGS_DIR / "tag-operations" / "test-extract-uclh-omop-cdm.yaml" + ] + assert project_config.destination.dicom == "ftps" + assert project_config.destination.parquet == "ftps" + + +BASE_YAML_DATA = { + "project": {"name": "myproject", "modalities": ["DX", "CR"]}, + "tag_operations": ["test-extract-uclh-omop-cdm.yaml"], + "destination": {"dicom": "ftps", "parquet": "ftps"}, +} + + +def test_parquet_dicom_fails(): + """ + Test that the config validation fails for non-valid values: 'dicomweb' not allowed for + parquet destionation + """ + config_data = BASE_YAML_DATA + config_data["destination"]["parquet"] = "dicomweb" + with pytest.raises(ValidationError): + PixlConfig.model_validate(config_data) + + +def test_invalid_destinations(): + """Test that the config validation fails for invalid destinations.""" + config_data = BASE_YAML_DATA + config_data["destination"]["dicom"] = "nope" + config_data["destination"]["parquet"] = "nope" + with pytest.raises(ValidationError): + PixlConfig.model_validate(config_data) + + +def test_invalid_paths(): + """Test that the config validation fails for invalid tag-operation paths.""" + config_data_wrong_base = BASE_YAML_DATA + config_data_wrong_base["tag_operations"][0] = "/i/dont/exist.yaml" + with pytest.raises(ValidationError): + PixlConfig.model_validate(config_data_wrong_base) diff --git a/pixl_core/tests/test_upload.py b/pixl_core/tests/uploader/test_ftps.py similarity index 68% rename from pixl_core/tests/test_upload.py rename to pixl_core/tests/uploader/test_ftps.py index c6abf706e..4e3cdf32f 100644 --- a/pixl_core/tests/test_upload.py +++ b/pixl_core/tests/uploader/test_ftps.py @@ -19,28 +19,32 @@ import pandas as pd import pytest from core.db.models import Image -from core.db.queries import get_project_slug_from_db, update_exported_at -from core.upload import upload_dicom_image, upload_parquet_files +from core.db.queries import get_project_slug_from_hashid, update_exported_at +from core.exports import ParquetExport @pytest.mark.usefixtures("ftps_server") -def test_upload_dicom_image(test_zip_content, not_yet_exported_dicom_image, ftps_home_dir) -> None: +def test_upload_dicom_image( + test_zip_content, not_yet_exported_dicom_image, ftps_uploader, ftps_home_dir +) -> None: """Tests that DICOM image can be uploaded to the correct location""" # ARRANGE # Get the pseudo identifier from the test image pseudo_anon_id = not_yet_exported_dicom_image.hashed_identifier - project_slug = get_project_slug_from_db(pseudo_anon_id) + project_slug = get_project_slug_from_hashid(pseudo_anon_id) expected_output_file = ftps_home_dir / project_slug / (pseudo_anon_id + ".zip") # ACT - upload_dicom_image(test_zip_content, pseudo_anon_id) + ftps_uploader.upload_dicom_image(test_zip_content, pseudo_anon_id) # ASSERT assert expected_output_file.exists() @pytest.mark.usefixtures("ftps_server") -def test_upload_dicom_image_throws(test_zip_content, already_exported_dicom_image) -> None: +def test_upload_dicom_image_throws( + test_zip_content, already_exported_dicom_image, ftps_uploader +) -> None: """Tests that exception thrown if DICOM image already exported""" # ARRANGE # Get the pseudo identifier from the test image @@ -48,7 +52,7 @@ def test_upload_dicom_image_throws(test_zip_content, already_exported_dicom_imag # ASSERT with pytest.raises(RuntimeError, match="Image already exported"): - upload_dicom_image(test_zip_content, pseudo_anon_id) + ftps_uploader.upload_dicom_image(test_zip_content, pseudo_anon_id) def test_update_exported_and_save(rows_in_session) -> None: @@ -67,18 +71,36 @@ def test_update_exported_and_save(rows_in_session) -> None: assert actual_export_time == expected_export_time +@pytest.fixture() +def parquet_export(export_dir) -> ParquetExport: + """ + Return a ParquetExport object. + + This fixture is deliberately not definied in conftest, because it imports the ParquetExport + class, which in turn loads the PixlConfig class, which in turn requres the PROJECT_CONFIGS_DIR + environment to be set. This environment variable is set in conftest, so the import needs to + happen after that. + """ + return ParquetExport( + project_name="i-am-a-project", + extract_datetime=datetime.now(tz=timezone.utc), + export_dir=export_dir, + ) + + @pytest.mark.usefixtures("ftps_server") -def test_upload_parquet(parquet_export, ftps_home_dir) -> None: +def test_upload_parquet(parquet_export, ftps_home_dir, ftps_uploader) -> None: """Tests that parquet files are uploaded to the correct location""" # ARRANGE parquet_export.copy_to_exports( - pathlib.Path(__file__).parents[2] / "test" / "resources" / "omop" + pathlib.Path(__file__).parents[3] / "test" / "resources" / "omop" ) parquet_export.export_radiology(pd.DataFrame(list("dummy"), columns=["D"])) # ACT - upload_parquet_files(parquet_export) + ftps_uploader.upload_parquet_files(parquet_export) + # ASSERT expected_public_parquet_dir = ( ftps_home_dir / parquet_export.project_slug / parquet_export.extract_time_slug / "parquet" @@ -95,8 +117,8 @@ def test_upload_parquet(parquet_export, ftps_home_dir) -> None: @pytest.mark.usefixtures("ftps_server") -def test_no_export_to_upload(parquet_export) -> None: +def test_no_export_to_upload(parquet_export, ftps_uploader) -> None: """If there is nothing in the export directly, an exception is thrown""" parquet_export.public_output.mkdir(parents=True, exist_ok=True) with pytest.raises(FileNotFoundError): - upload_parquet_files(parquet_export) + ftps_uploader.upload_parquet_files(parquet_export) diff --git a/pixl_dcmd/pyproject.toml b/pixl_dcmd/pyproject.toml index 0e284a6b4..a3a10f2fb 100644 --- a/pixl_dcmd/pyproject.toml +++ b/pixl_dcmd/pyproject.toml @@ -1,21 +1,16 @@ [project] name = "pixl_dcmd" version = "0.0.2" -authors = [ - { name="PIXL authors" }, -] +authors = [{ name = "PIXL authors" }] description = "DICOM header anonymisation functions" readme = "README.md" requires-python = "~=3.9" -classifiers = [ - "Programming Language :: Python :: 3" -] +classifiers = ["Programming Language :: Python :: 3"] dependencies = [ "arrow==1.2.3", "pydicom==2.4.4", "pydicom-data", "logger==1.4", - "pyyaml==6.0.1", "requests==2.31.0", "python-decouple==3.6", "types-requests~=2.28", @@ -44,4 +39,4 @@ extend = "./ruff.toml" "./tests/**" = ["D100"] [tool.setuptools.package-data] -pixl_dcmd = ["resources/*.yml"] \ No newline at end of file +pixl_dcmd = ["resources/*.yml"] diff --git a/pixl_dcmd/src/pixl_dcmd/main.py b/pixl_dcmd/src/pixl_dcmd/main.py index 435eba5ae..879f708e4 100644 --- a/pixl_dcmd/src/pixl_dcmd/main.py +++ b/pixl_dcmd/src/pixl_dcmd/main.py @@ -15,8 +15,11 @@ from io import BytesIO from os import PathLike +from pathlib import Path from typing import Any, BinaryIO, Union from logging import getLogger +from core.db.queries import get_project_slug +from core.project_config import load_project_config import requests from decouple import config @@ -25,6 +28,7 @@ from pixl_dcmd._database import add_hashed_identifier_and_save, query_db from pixl_dcmd._datetime import combine_date_time, format_date_time from pixl_dcmd._deid_helpers import get_bounded_age, get_encrypted_uid +import yaml DicomDataSetType = Union[Union[str, bytes, PathLike[Any]], BinaryIO] @@ -44,6 +48,68 @@ def write_dataset_to_bytes(dataset: Dataset) -> bytes: return buffer.read() +def anonymise_dicom(dataset: Dataset) -> Dataset: + """ + Anonymises a DICOM dataset as Received by Orthanc. + Finds appropriate configuration based on project name and anonymises by + - dropping datasets of the wrong modality + - removing private tags + - removing overlays + - applying tag operations based on the config file + Returns anonymised dataset. + """ + slug = get_project_slug(dataset.PatientID, dataset.AccessionNumber) + project_config = load_project_config(slug) + logger.error(f"Received instance for project {slug}") + # Drop anything that is not an X-Ray + if dataset.Modality not in project_config.project.modalities: + msg = f"Dropping DICOM Modality: {dataset.Modality}" + logger.error(msg) + raise ValueError(msg) + + logger.warning("Anonymising received instance") + # Rip out all private tags/ + dataset.remove_private_tags() + logger.info("Removed private tags") + + # Rip out overlays/ + dataset = remove_overlays(dataset) + logger.info("Removed overlays") + + # Merge tag schemes + all_tags = merge_tag_schemes(project_config.tag_operation_files) + + # Apply scheme to instance + dataset = apply_tag_scheme(dataset, all_tags) + # Apply whitelist + dataset = enforce_whitelist(dataset, all_tags) + + logger.info( + f"DICOM tag anonymisation applied according to {project_config.tag_operation_files}" + ) + logger.warning("DICOM tag anonymisation applied") + + # Write anonymised instance to disk. + return dataset + + +def merge_tag_schemes(tag_operation_files: list[Path]) -> list[dict]: + """ + NOT IMPLEMENTED, WORKS ONLY WITH A SINGLE TAG SCHEME + Merge multiple tag schemes into a single dictionary. + """ + if len(tag_operation_files) > 1: + raise NotImplementedError("Multiple tag schemes not supported") + with tag_operation_files[0].open() as file: + # Load tag operations scheme from YAML. + tags = yaml.safe_load(file) + if not isinstance(tags, list) or not all( + [isinstance(tag, dict) for tag in tags] + ): + raise ValueError("Tag operation file must contain a list of dictionaries") + return tags + + def remove_overlays(dataset: Dataset) -> Dataset: """ Search for overlays planes and remove them. @@ -76,7 +142,7 @@ def remove_overlays(dataset: Dataset) -> Dataset: return dataset -def enforce_whitelist(dataset: dict, tags: dict) -> dict: +def enforce_whitelist(dataset: Dataset, tags: list[dict]) -> Dataset: """Delete any tags not in the tagging scheme.""" # For every element: logger.debug("Enforcing whitelist") @@ -105,7 +171,7 @@ def enforce_whitelist(dataset: dict, tags: dict) -> dict: return dataset -def apply_tag_scheme(dataset: dict, tags: dict) -> dict: +def apply_tag_scheme(dataset: Dataset, tags: list[dict]) -> Dataset: """ Apply anonymisation operations for a given set of tags to a dataset. The original study time is kept before any operations are applied. diff --git a/pixl_dcmd/tests/conftest.py b/pixl_dcmd/tests/conftest.py index 0d1bbe5d6..93c4dd1ec 100644 --- a/pixl_dcmd/tests/conftest.py +++ b/pixl_dcmd/tests/conftest.py @@ -29,6 +29,9 @@ os.environ["HASHER_API_AZ_NAME"] = "test_hash_API" os.environ["HASHER_API_PORT"] = "test_hash_API_port" os.environ["TIME_OFFSET"] = "5" +os.environ["PROJECT_CONFIGS_DIR"] = str( + pathlib.Path(__file__).parents[2] / "projects/configs" +) STUDY_DATE = datetime.date.fromisoformat("2023-01-01") diff --git a/pixl_dcmd/tests/test_main.py b/pixl_dcmd/tests/test_main.py index aba26cf7b..d2624c484 100644 --- a/pixl_dcmd/tests/test_main.py +++ b/pixl_dcmd/tests/test_main.py @@ -27,6 +27,7 @@ from core.db.models import Image from pixl_dcmd.main import ( apply_tag_scheme, + merge_tag_schemes, remove_overlays, ) @@ -36,7 +37,7 @@ def tag_scheme() -> dict: """Read the tag scheme from orthanc raw.""" tag_file = ( pathlib.Path(__file__).parents[2] - / "orthanc/orthanc-anon/plugin/tag-operations.yaml" + / "projects/configs/tag-operations/test-extract-uclh-omop-cdm.yaml" ) return yaml.safe_load(tag_file.read_text()) @@ -134,3 +135,11 @@ def test_can_nifti_convert_post_anonymisation( assert anon_nifti.shape == ident_nifti.shape assert np.all(anon_nifti.header.get_sform() == ident_nifti.header.get_sform()) assert np.all(anon_nifti.get_fdata() == ident_nifti.get_fdata()) + + +def test_merge_tag_schemes_single_file(): + tag_ops_file = ( + pathlib.Path(__file__).parents[2] + / "projects/configs/tag-operations/test-extract-uclh-omop-cdm.yaml" + ) + merge_tag_schemes([tag_ops_file]) diff --git a/pixl_ehr/README.md b/pixl_ehr/README.md index 8eb65c4a4..1dccbd1a9 100644 --- a/pixl_ehr/README.md +++ b/pixl_ehr/README.md @@ -34,13 +34,7 @@ pip install -e ../pixl_core/ -e . ```bash pip install -e ../pixl_core/ -e .[test] -pytest -m "not processing" -``` - -and the processing tests with - -```bash -./tests/run-processing-tests.sh +pytest ``` To test the availability of a CogStack instance, we mock up a *FastAPI* server which simply takes in diff --git a/pixl_ehr/pyproject.toml b/pixl_ehr/pyproject.toml index 114b7a12d..f5139bdc5 100644 --- a/pixl_ehr/pyproject.toml +++ b/pixl_ehr/pyproject.toml @@ -1,15 +1,11 @@ [project] name = "pixl_ehr" version = "0.0.2" -authors = [ - { name="PIXL authors" }, -] +authors = [{ name = "PIXL authors" }] description = "PIXL electronic health record extractor" readme = "README.md" requires-python = ">=3.10" -classifiers = [ - "Programming Language :: Python :: 3" -] +classifiers = ["Programming Language :: Python :: 3"] dependencies = [ "core", "uvicorn==0.23.2", @@ -18,7 +14,6 @@ dependencies = [ "azure-identity==1.12.0", "azure-storage-blob==12.14.1", "pyarrow==14.0.1", - "PyYAML==6.0.1", ] [project.optional-dependencies] diff --git a/pixl_ehr/src/pixl_ehr/main.py b/pixl_ehr/src/pixl_ehr/main.py index f419ffc67..10d6e24aa 100644 --- a/pixl_ehr/src/pixl_ehr/main.py +++ b/pixl_ehr/src/pixl_ehr/main.py @@ -28,9 +28,8 @@ from core.exports import ParquetExport from core.patient_queue import PixlConsumer from core.rest_api.router import router, state -from core.upload import upload_parquet_files from decouple import config -from fastapi import FastAPI +from fastapi import FastAPI, HTTPException from fastapi.responses import JSONResponse from pydantic import BaseModel @@ -69,7 +68,7 @@ async def startup_event() -> None: # Export root dir from inside the EHR container. # For the view from outside, see pixl_cli/_io.py: HOST_EXPORT_ROOT_DIR -EHR_EXPORT_ROOT_DIR = Path("/run/exports") +EHR_EXPORT_ROOT_DIR = Path("/run/projects/exports") class ExportRadiologyData(BaseModel): @@ -95,12 +94,17 @@ def export_patient_data(export_params: ExportRadiologyData) -> None: export_radiology_as_parquet(export_params) # Upload Parquet files to the appropriate endpoint - upload_parquet_files( - ParquetExport( - export_params.project_name, export_params.extract_datetime, export_params.output_dir - ) + parquet_export = ParquetExport( + export_params.project_name, export_params.extract_datetime, export_params.output_dir ) + try: + parquet_export.upload() + except ValueError as e: + msg = "Destination for parquet files unavailable" + logger.exception(msg) + raise HTTPException(status_code=400, detail=msg) from e + def export_radiology_as_parquet(export_params: ExportRadiologyData) -> None: """ diff --git a/pixl_ehr/tests/conftest.py b/pixl_ehr/tests/conftest.py index f6abe460f..8bce49bc2 100644 --- a/pixl_ehr/tests/conftest.py +++ b/pixl_ehr/tests/conftest.py @@ -39,11 +39,12 @@ os.environ["EMAP_UDS_PASSWORD"] = "postgres" # noqa: S105 os.environ["EMAP_UDS_SCHEMA_NAME"] = "star" os.environ["COGSTACK_REDACT_URL"] = "test" +os.environ["PROJECT_CONFIGS_DIR"] = str(Path(__file__).parents[2] / "projects/configs") TEST_DIR = Path(__file__).parent -@pytest.fixture(scope="package", autouse=True) +@pytest.fixture(scope="package") def run_containers() -> subprocess.CompletedProcess[bytes]: """Run docker containers for tests which require them.""" yield run_subprocess( diff --git a/pixl_ehr/tests/test_processing.py b/pixl_ehr/tests/test_processing.py index 381ae0295..1ea8b7338 100644 --- a/pixl_ehr/tests/test_processing.py +++ b/pixl_ehr/tests/test_processing.py @@ -235,6 +235,7 @@ def insert_data_into_emap_star_schema() -> None: @pytest.mark.processing() @pytest.mark.asyncio() +@pytest.mark.usefixtures("run_containers") async def test_message_processing(example_messages) -> None: """ GIVEN some patient metadata in Emap @@ -299,6 +300,7 @@ async def test_message_processing(example_messages) -> None: @pytest.mark.processing() @pytest.mark.asyncio() +@pytest.mark.usefixtures("run_containers") async def test_radiology_export(example_messages, tmp_path) -> None: """ GIVEN a message processed by the EHR API @@ -336,6 +338,7 @@ async def test_radiology_export(example_messages, tmp_path) -> None: @pytest.mark.processing() @pytest.mark.asyncio() +@pytest.mark.usefixtures("run_containers") async def test_radiology_export_multiple_projects(example_messages, tmp_path) -> None: """ GIVEN EHR API has processed four messages, each from a different project+extract combination diff --git a/orthanc/orthanc-anon/plugin/tag-operations.yaml b/projects/configs/tag-operations/test-extract-uclh-omop-cdm.yaml similarity index 100% rename from orthanc/orthanc-anon/plugin/tag-operations.yaml rename to projects/configs/tag-operations/test-extract-uclh-omop-cdm.yaml diff --git a/projects/configs/tag-operations/uclh-nasogastric-tube-project-ngt-only-full-dataset.yaml b/projects/configs/tag-operations/uclh-nasogastric-tube-project-ngt-only-full-dataset.yaml new file mode 100644 index 000000000..201b34a2e --- /dev/null +++ b/projects/configs/tag-operations/uclh-nasogastric-tube-project-ngt-only-full-dataset.yaml @@ -0,0 +1,593 @@ +# Copyright (c) University College London Hospitals NHS Foundation Trust +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +- name: "Specific Character Set" + group: 0x0008 + element: 0x0005 + op: "keep" +- name: "Image Type" + group: 0x0008 + element: 0x0008 + op: "keep" +- name: "SOP Class UID" + group: 0x0008 + element: 0x0016 + op: "keep" +- name: "SOP Instance UID" + group: 0x0008 + element: 0x0018 + op: "hash-uid" +- name: "Study Date" + group: 0x0008 + element: 0x0020 + op: "delete" +- name: "Series Date" + group: 0x0008 + element: 0x0021 + op: "delete" +- name: "Acquisition Date" + group: 0x0008 + element: 0x0022 + op: "delete" +- name: "Image Date" + group: 0x0008 + element: 0x0023 + op: "delete" +- name: "Acquisition Date Time" + group: 0x0008 + element: 0x002a + op: "delete" +- name: "Study Time" + group: 0x0008 + element: 0x0030 + op: "delete" +- name: "Series Time" + group: 0x0008 + element: 0x0031 + op: "delete" +- name: "Acquisition Time" + group: 0x0008 + element: 0x0032 + op: "delete" +- name: "Image Time" + group: 0x0008 + element: 0x0033 + op: "delete" +- name: "Accession Number" + group: 0x0008 + element: 0x0050 + op: "secure-hash" +- name: "Modality" + group: 0x0008 + element: 0x0060 + op: "keep" +- name: "Modalities In Study" + group: 0x0008 + element: 0x0061 + op: "keep" +- name: "Presentation Intent Type" + group: 0x0008 + element: 0x0068 + op: "delete" +- name: "Manufacturer" + group: 0x0008 + element: 0x0070 + op: "keep" +- name: "Institution Name" + group: 0x0008 + element: 0x0080 + op: "delete" +- name: "Institution Address" + group: 0x0008 + element: 0x0081 + op: "delete" +- name: "Referring Physicians Name" + group: 0x0008 + element: 0x0090 + op: "delete" +- name: "Station Name" + group: 0x0008 + element: 0x1010 + op: "delete" +- name: "Study Description" + group: 0x0008 + element: 0x1030 + op: "keep" +- name: "Series Description" + group: 0x0008 + element: 0x103e + op: "keep" +- name: "Institutional Department Name" + group: 0x0008 + element: 0x1040 + op: "delete" +- name: "Performing Physicians Name" + group: 0x0008 + element: 0x1050 + op: "delete" +- name: "Operators Name" + group: 0x0008 + element: 0x1070 + op: "delete" +- name: "Manufacturers Model Name" + group: 0x0008 + element: 0x1090 + op: "keep" +- name: "Referenced Study Sequence" + group: 0x0008 + element: 0x1110 + op: "delete" +- name: "Referenced Patient Sequence" + group: 0x0008 + element: 0x1120 + op: "delete" +- name: "Source Image Sequence" + group: 0x0008 + element: 0x2112 + op: "delete" +- name: "Anatomic Region Sequence" + group: 0x0008 + element: 0x2218 + op: "delete" +- name: "Irradiation Event UID" + group: 0x0008 + element: 0x3010 + op: "delete" +- name: "Patients Name" + group: 0x0010 + element: 0x0010 + op: "secure-hash" +- name: "Patient ID" + group: 0x0010 + element: 0x0020 + op: "secure-hash" +- name: "Issuer Of Patient ID" + group: 0x0010 + element: 0x0021 + op: "delete" +- name: "Patients Birth Date" + group: 0x0010 + element: 0x0030 + op: "delete" +- name: "Patients Birth Time" + group: 0x0010 + element: 0x0032 + op: "delete" +- name: "Patients Sex" + group: 0x0010 + element: 0x0040 + op: "delete" +- name: "Other Patient IDs" + group: 0x0010 + element: 0x1000 + op: "delete" +- name: "Other Patient Names" + group: 0x0010 + element: 0x1001 + op: "delete" +- name: "Patients Age" + group: 0x0010 + element: 0x1010 + op: "delete" +- name: "Patients Size" + group: 0x0010 + element: 0x1020 + op: "keep" +- name: "Patients Weight" + group: 0x0010 + element: 0x1030 + op: "keep" +- name: "Patients Address" + group: 0x0010 + element: 0x1040 + op: "delete" +- name: "Medical Alerts" + group: 0x0010 + element: 0x2000 + op: "delete" +- name: "Contrast Allergies" + group: 0x0010 + element: 0x2110 + op: "delete" +- name: "Patient Comments" + group: 0x0010 + element: 0x4000 + op: "delete" +- name: "Private Creator Data Element" + group: 0x0011 + element: 0x0010 + op: "delete" +- name: "Body Part Examined" + group: 0x0018 + element: 0x0015 + op: "keep" +- name: "kVp" + group: 0x0018 + element: 0x0060 + op: "keep" +- name: "Software Version" + group: 0x0018 + element: 0x1020 + op: "keep" +- name: "Protocol Name" + group: 0x0018 + element: 0x1030 + op: "delete" +- name: "Field Of View Dimension" + group: 0x0018 + element: 0x1149 + op: "keep" +- name: "Exposure Time" + group: 0x0018 + element: 0x1150 + op: "keep" +- name: "X Ray Tube Current" + group: 0x0018 + element: 0x1151 + op: "keep" +- name: "Exposure" + group: 0x0018 + element: 0x1152 + op: "keep" +- name: "Exposure In Uas" + group: 0x0018 + element: 0x1153 + op: "keep" +- name: "Image Area Dose Product" + group: 0x0018 + element: 0x115e + op: "keep" +- name: "Imager Pixel Spacing" + group: 0x0018 + element: 0x1164 + op: "keep" +- name: "Grid" + group: 0x0018 + element: 0x1166 + op: "keep" +- name: "Focal Spot" + group: 0x0018 + element: 0x1190 + op: "keep" +- name: "Acquisition Device Processing Description" + group: 0x0018 + element: 0x1400 + op: "keep" +- name: "Exposure Index" + group: 0x0018 + element: 0x1411 + op: "keep" +- name: "Target Exposure Index" + group: 0x0018 + element: 0x1412 + op: "keep" +- name: "Deviation Index" + group: 0x0018 + element: 0x1413 + op: "keep" +- name: "Positioner Type" + group: 0x0018 + element: 0x1508 + op: "keep" +- name: "Collemator Shape" + group: 0x0018 + element: 0x1700 + op: "keep" +- name: "Vertices Of The Polygonal Collimator" + group: 0x0018 + element: 0x1720 + op: "keep" +- name: "View Position" + group: 0x0018 + element: 0x5101 + op: "keep" +- name: "Sensitivity" + group: 0x0018 + element: 0x6000 + op: "keep" +- name: "Detector Temperature" + group: 0x0018 + element: 0x7001 + op: "keep" +- name: "Detector Type" + group: 0x0018 + element: 0x7004 + op: "keep" +- name: "Detector Configuration" + group: 0x0018 + element: 0x7005 + op: "keep" +- name: "Detector ID" + group: 0x0018 + element: 0x700a + op: "keep" +- name: "Detector Binning" + group: 0x0018 + element: 0x701a + op: "keep" +- name: "Detector Element Physical Size" + group: 0x0018 + element: 0x7020 + op: "keep" +- name: "Detector Element Spacing" + group: 0x0018 + element: 0x7022 + op: "keep" +- name: "Detector Active Shape" + group: 0x0018 + element: 0x7024 + op: "keep" +- name: "Detector Active Dimensions" + group: 0x0018 + element: 0x7026 + op: "keep" +- name: "Field Of View Origin" + group: 0x0018 + element: 0x7030 + op: "keep" +- name: "Field Of View Rotation" + group: 0x0018 + element: 0x7032 + op: "keep" +- name: "Field Of View Horizontal Flip" + group: 0x0018 + element: 0x7034 + op: "keep" +- name: "Grid Focal Distance" + group: 0x0018 + element: 0x704c + op: "keep" +- name: "Exposure Control Mode" + group: 0x0018 + element: 0x7060 + op: "keep" +- name: "Study Instance UID" + group: 0x0020 + element: 0x000d + op: "hash-uid" +- name: "Series Instance UID" + group: 0x0020 + element: 0x000e + op: "hash-uid" +- name: "Study ID" + group: 0x0020 + element: 0x0010 + op: "fixed" +- name: "Series Number" + group: 0x0020 + element: 0x0011 + op: "keep" +- name: "Image Number" + group: 0x0020 + element: 0x0013 + op: "keep" +- name: "Patient Orientation" + group: 0x0020 + element: 0x0020 + op: "keep" +- name: "Image Laterality" + group: 0x0020 + element: 0x0062 + op: "keep" +- name: "Number Of Study Related Images" + group: 0x0020 + element: 0x1208 + op: "delete" +- name: "Samples Per Pixel" + group: 0x0028 + element: 0x0002 + op: "keep" +- name: "Photometric Interpretation" + group: 0x0028 + element: 0x0004 + op: "keep" +- name: "Rows" + group: 0x0028 + element: 0x0010 + op: "keep" +- name: "Columns" + group: 0x0028 + element: 0x0011 + op: "keep" +- name: "Pixel Spacing" + group: 0x0028 + element: 0x0030 + op: "keep" +- name: "Bits Allocated" + group: 0x0028 + element: 0x0100 + op: "keep" +- name: "Bits Stored" + group: 0x0028 + element: 0x0101 + op: "keep" +- name: "High Bit" + group: 0x0028 + element: 0x0102 + op: "keep" +- name: "Pixel Representation" + group: 0x0028 + element: 0x0103 + op: "keep" +- name: "Quality Control Image" + group: 0x0028 + element: 0x0300 + op: "keep" +- name: "Burned In Annotation" + group: 0x0028 + element: 0x0301 + op: "keep" +- name: "Pixel Spacing Calibration Type" + group: 0x0028 + element: 0x0a02 + op: "keep" +- name: "Pixel Spacing Calibration Description" + group: 0x0028 + element: 0x0a04 + op: "keep" +- name: "Pixel Intensity Relationship" + group: 0x0028 + element: 0x1040 + op: "keep" +- name: "Pixel Intensity Relationship Sign" + group: 0x0028 + element: 0x1041 + op: "keep" +- name: "Window Center" + group: 0x0028 + element: 0x1050 + op: "keep" +- name: "Window Width" + group: 0x0028 + element: 0x1051 + op: "keep" +- name: "Rescale Intercept" + group: 0x0028 + element: 0x1052 + op: "keep" +- name: "Rescale Slope" + group: 0x0028 + element: 0x1053 + op: "keep" +- name: "Rescale Type" + group: 0x0028 + element: 0x1054 + op: "keep" +- name: "Window Center And Width Explanation" + group: 0x0028 + element: 0x1055 + op: "keep" +- name: "Lossy Image Compression" + group: 0x0028 + element: 0x2110 + op: "keep" +- name: "VOI LUT Sequence" + group: 0x0028 + element: 0x3010 + op: "keep" +- name: "Current Patient Location" + group: 0x0038 + element: 0x0300 + op: "delete" +- name: "Patient State" + group: 0x0038 + element: 0x0500 + op: "delete" +- name: "Performed Procedure Start Date" + group: 0x0040 + element: 0x0244 + op: "delete" +- name: "Performed Procedure Start Time" + group: 0x0040 + element: 0x0245 + op: "delete" +- name: "Performed Procedure Step ID" + group: 0x0040 + element: 0x0253 + op: "delete" +- name: "Performed Procedure Step Description" + group: 0x0040 + element: 0x0254 + op: "delete" +- name: "Performed Action Item Sequence" + group: 0x0040 + element: 0x0260 + op: "delete" +- name: "Request Attributes Sequence" + group: 0x0040 + element: 0x0275 + op: "delete" +- name: "Acquisition Context Sequence" + group: 0x0040 + element: 0x0555 + op: "delete" +- name: "Confidentiality Code" + group: 0x0040 + element: 0x1008 + op: "delete" +- name: "Private Creator Data Element" + group: 0x0045 + element: 0x0010 + op: "delete" +- name: "View Code Sequence" + group: 0x0054 + element: 0x0220 + op: "keep" +- name: "Image Comments" + group: 0x0020 + element: 0x4000 + op: "delete" +- name: "Instance Creator UID" + group: 0x0008 + element: 0x0014 + op: "hash-uid" +- name: "Referenced SOP Instance UID" + group: 0x0008 + element: 0x1155 + op: "hash-uid" +- name: "Frame of Reference UID" + group: 0x0020 + element: 0x0052 + op: "hash-uid" +- name: "Synchronization Frame of Reference UID" + group: 0x0020 + element: 0x0200 + op: "hash-uid" +- name: "Storage Media File-set UID" + group: 0x0088 + element: 0x0140 + op: "hash-uid" +- name: "Referenced Frame of Reference UID" + group: 0x3006 + element: 0x0024 + op: "hash-uid" +- name: "Related Frame of Reference UID" + group: 0x3006 + element: 0x00C2 + op: "hash-uid" +- name: "UID" + group: 0x0040 + element: 0xA124 + op: "hash-uid" +- name: "Study Comments [Retired]" + group: 0x0032 + element: 0x4000 + op: "delete" +- name: "Ethnic Group" + group: 0x0010 + element: 0x2160 + op: "delete" +- name: "Physicians Of Record" + group: 0x0008 + element: 0x1048 + op: "delete" +- name: "Name Of Physicians Reading Study" + group: 0x0008 + element: 0x1060 + op: "delete" +- name: "Device Serial Number" + group: 0x0018 + element: 0x1000 + op: "delete" +- name: "Additional Patient History" + group: 0x0010 + element: 0x21b0 + op: "delete" +- name: "Pregnancy Status" + group: 0x0010 + element: 0x21c0 + op: "delete" +- name: "Pixel Data" + group: 0x7fe0 + element: 0x0010 + op: "keep" diff --git a/projects/configs/test-extract-uclh-omop-cdm.yaml b/projects/configs/test-extract-uclh-omop-cdm.yaml new file mode 100644 index 000000000..818008cf0 --- /dev/null +++ b/projects/configs/test-extract-uclh-omop-cdm.yaml @@ -0,0 +1,24 @@ +# Copyright (c) 2022 University College London Hospitals NHS Foundation Trust +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +project: + name: "test-extract-uclh-omop-cdm" + azure_kv_alias: "test" + modalities: ["DX", "CR"] + +tag_operation_files: ["test-extract-uclh-omop-cdm.yaml"] + +destination: + dicom: "ftps" + parquet: "ftps" diff --git a/projects/configs/uclh-nasogastric-tube-project-ngt-only-full-dataset.yaml b/projects/configs/uclh-nasogastric-tube-project-ngt-only-full-dataset.yaml new file mode 100644 index 000000000..0eb56b19b --- /dev/null +++ b/projects/configs/uclh-nasogastric-tube-project-ngt-only-full-dataset.yaml @@ -0,0 +1,24 @@ +# Copyright (c) 2022 University College London Hospitals NHS Foundation Trust +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +project: + name: "uclh-nasogastric-tube-project-ngt-only-full-dataset" + modalities: ["DX", "CR"] + +tag_operation_files: + ["uclh-nasogastric-tube-project-ngt-only-full-dataset.yaml"] + +destination: + dicom: "ftps" + parquet: "ftps" diff --git a/exports/.gitkeep b/projects/exports/.gitkeep similarity index 100% rename from exports/.gitkeep rename to projects/exports/.gitkeep diff --git a/pytest-pixl/tests/conftest.py b/pytest-pixl/tests/conftest.py index b06da963d..ffc8739ea 100644 --- a/pytest-pixl/tests/conftest.py +++ b/pytest-pixl/tests/conftest.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import os from pathlib import Path # Avoid running samples for fixture tests directly with pytest @@ -20,8 +19,3 @@ pytest_plugins = ["pytester"] TEST_DIR = Path(__file__).parent - -os.environ["FTP_HOST"] = "localhost" -os.environ["FTP_USER_NAME"] = "pixl_user" -os.environ["FTP_USER_PASSWORD"] = "longpassword" # noqa: S105 Hardcoding password -os.environ["FTP_PORT"] = "20021" diff --git a/pytest-pixl/tests/samples_for_fixture_tests/test_ftpserver_fixture/test_ftpserver_login.py b/pytest-pixl/tests/samples_for_fixture_tests/test_ftpserver_fixture/test_ftpserver_login.py index 962089552..93c8be601 100644 --- a/pytest-pixl/tests/samples_for_fixture_tests/test_ftpserver_fixture/test_ftpserver_login.py +++ b/pytest-pixl/tests/samples_for_fixture_tests/test_ftpserver_fixture/test_ftpserver_login.py @@ -15,7 +15,7 @@ from pathlib import Path -from core.upload import _connect_to_ftp +from core.uploader._ftps import _connect_to_ftp TEST_FILE_CONTENT = "test text" TEST_FILENAME = "testfile.txt" diff --git a/template_config.yaml b/template_config.yaml new file mode 100644 index 000000000..f2f9c1736 --- /dev/null +++ b/template_config.yaml @@ -0,0 +1,23 @@ +# Copyright (c) University College London Hospitals NHS Foundation Trust +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +project: + name: "project-slug" + modalities: ["DX", "CR"] # DICOM dataset modalities to retain + +tag_operation_files: ["base-tag-operations.yaml"] # DICOM tag anonymisation operations, can specify multiple files in the future + +destination: + dicom: "ftps" # alternatives: "none" + parquet: "ftps" # alternatives: "none" diff --git a/test/.env b/test/.env index 876258df3..60d80adee 100644 --- a/test/.env +++ b/test/.env @@ -78,3 +78,6 @@ RABBITMQ_PASSWORD=rabbitmq_password FTP_HOST=host.docker.internal FTP_USER_NAME=pixl_user FTP_USER_PASSWORD=longpassword + +# Project configs directory +PROJECT_CONFIGS_DIR=projects/configs diff --git a/test/README.md b/test/README.md index 0e3ad87e7..ac6733066 100644 --- a/test/README.md +++ b/test/README.md @@ -12,7 +12,7 @@ consumers started. **Then** a row in the "anon" EMAP data instance of the PIXL postgres instance exists and the DICOM study exists in the "anon" PIXL Orthanc instance. -You can run the system test with: +After setting up your [.secrets.env](../README.md#project-secrets)), you can run the system test with: ```bash ./run-system-test.sh @@ -34,6 +34,12 @@ Run the following to teardown: ./run-system-test.sh teardown ``` +## The `pytest-pixl` plugin + +We provide a [`pytest` plugin](../pytest-pixl/README.md) with shared functionality for PIXL system +and unit tests. This includes an `ftp_server` fixture to spin up a lightweight FTP server, +to mock the FTP server used by the Data Safe Haven. + ## File organisation ### Docker compose @@ -49,31 +55,6 @@ Run the following to teardown: `./dummy-services` contains a Python script and Dockerfile to mock the CogStack service, used for de-identification of the radiology reports in the EHR API. -#### FTP server - -We spin up a test FTP server from the Docker container defined in `./dummy-services/ftp-server/`. -The Docker container inherits from -[`delfer/alpine-ftp-server`](https://github.com/delfer/docker-alpine-ftp-server) and uses `vsftpd` -as the FTP client. The Docker container requires the following environment variables to be defined: - -- `ADDRESS`: external address to which clients can connect for passive ports -- `USERS`: space and `|` separated list of usernames, passwords, home directories and groups: - - format `name1|password1|[folder1][|uid1][|gid1] name2|password2|[folder2][|uid2][|gid2]` - - the values in `[]` are optional -- `TLS_KEY`: keyfile for the TLS certificate -- `TLS_CERT`: TLS certificate - -> [!warning] The `ADDRESS` should match the `FTP_HOST` environment variable defined in `.env`, -> otherwise FTP commands such as `STOR` or `dir` run from other Docker containers in the network -> (such as `orthanc-anon`) will fail. _Note: connecting and logging into the FTP server might still -> work, as the address name is only checked for protected operations such as listing and transfering -> files._ - -**Volume**: to check succesful uploads, we mount a local data directory to `/home/${FTP_USERNAME}/` - -**SSL certifcates**: the SSL certificate files are defined in `test/dummy-services/ftp-server/ssl` -and are copied into `/etc/ssl/private` when building the Docker container. - ### Resources - `./resources/` provides 2 mock DICOM images used to populate the mock VNA diff --git a/test/conftest.py b/test/conftest.py index 5bfebf7a0..a266d4055 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -25,7 +25,7 @@ @pytest.fixture() def host_export_root_dir(): """Intermediate export dir as seen from the host""" - return Path(__file__).parents[1] / "exports" + return Path(__file__).parents[1] / "projects" / "exports" TEST_DIR = Path(__file__).parent @@ -48,7 +48,7 @@ def _setup_pixl_cli(ftps_server) -> None: "system-test-ehr-api-1", "rm", "-r", - "/run/exports/test-extract-uclh-omop-cdm/", + "/run/projects/exports/test-extract-uclh-omop-cdm/", ], TEST_DIR, ) diff --git a/test/dummy-services/ftp-server/mounts/data/.gitkeep b/test/dummy-services/ftp-server/mounts/data/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/run-system-test.sh b/test/run-system-test.sh index 6fc16038d..f623e0d74 100755 --- a/test/run-system-test.sh +++ b/test/run-system-test.sh @@ -17,21 +17,25 @@ BIN_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) PACKAGE_DIR="${BIN_DIR%/*}" cd "${PACKAGE_DIR}/test" -setup () { - docker compose --env-file .env -p system-test down --volumes - # - # Note: cannot run as single docker compose command due to different build contexts - docker compose --env-file .env -p system-test up --wait -d --build --remove-orphans - # Warning: Requires to be run from the project root - (cd "${PACKAGE_DIR}" && \ - docker compose --env-file test/.env -p system-test up --wait -d --build) +setup() { + docker compose --env-file .env -p system-test down --volumes + # + # Note: cannot run as single docker compose command due to different build contexts + docker compose --env-file .env -p system-test up --wait -d --build --remove-orphans + # Warning: Requires to be run from the project root + ( + cd "${PACKAGE_DIR}" + docker compose --env-file test/.env --env-file .secrets.env -p system-test up --wait -d --build + ) - ./scripts/insert_test_data.sh + ./scripts/insert_test_data.sh } -teardown () { - (cd "${PACKAGE_DIR}" && \ - docker compose -f docker-compose.yml -f test/docker-compose.yml -p system-test down --volumes) +teardown() { + ( + cd "${PACKAGE_DIR}" + docker compose -f docker-compose.yml -f test/docker-compose.yml -p system-test down --volumes + ) } # Allow user to perform just setup so that pytest may be run repeatedly without @@ -39,14 +43,13 @@ teardown () { # for clearing up anything it creates (export temp dir?) subcmd=${1:-""} if [ "$subcmd" = "setup" ]; then - setup + setup elif [ "$subcmd" = "teardown" ]; then - teardown + teardown else - setup - pytest --verbose --log-cli-level INFO - echo FINISHED PYTEST COMMAND - teardown - echo SYSTEM TEST SUCCESSFUL + setup + pytest --verbose --log-cli-level INFO + echo FINISHED PYTEST COMMAND + teardown + echo SYSTEM TEST SUCCESSFUL fi - diff --git a/test/test_radiology_parquet.py b/test/test_parquet_exports.py similarity index 80% rename from test/test_radiology_parquet.py rename to test/test_parquet_exports.py index dcee8b65e..dfe9a0546 100644 --- a/test/test_radiology_parquet.py +++ b/test/test_parquet_exports.py @@ -17,16 +17,29 @@ import pandas as pd import pytest +from conftest import RESOURCES_DIR logger = logging.getLogger(__name__) +@pytest.mark.usefixtures("_setup_pixl_cli") +def test_public_parquet(host_export_root_dir: Path): + """Tests whether the public parquet files have been exported to the right place""" + expected_public_dir = ( + host_export_root_dir / "test-extract-uclh-omop-cdm" / "latest" / "omop" / "public" + ) + expected_files = sorted([x.stem for x in (RESOURCES_DIR / "omop" / "public").glob("*.parquet")]) + + assert expected_public_dir.exists() + assert expected_files == sorted([x.stem for x in expected_public_dir.glob("*.parquet")]) + + @pytest.mark.usefixtures("_extract_radiology_reports") def test_radiology_parquet(host_export_root_dir: Path): """ From: scripts/test_radiology_parquet.py \ - ../exports/test-extract-uclh-omop-cdm/latest/radiology/radiology.parquet + ../projects/exports/test-extract-uclh-omop-cdm/latest/radiology/radiology.parquet Test contents of radiology report parquet file in the export location """ expected_radiology_parquet_file = (