init public release

This commit is contained in:
2026-09-08 10:59:05 +02:00
commit 9cdf012717
133 changed files with 2950635 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
[bumpversion]
current_version = 1.0.1
commit = True
tag = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)
serialize = {major}.{minor}.{patch}
[bumpversion:file:pyproject.toml]
+27
View File
@@ -0,0 +1,27 @@
# Never copy a host virtual environment into the image
.venv
venv
env
# Python caches
__pycache__/
*.py[cod]
*.egg-info/
.pytest_cache/
.mypy_cache/
.ruff_cache/
# VCS / editor
.git
.gitignore
.idea/
.vscode/
# Local env / secrets (manage these via compose env_file or secrets instead)
.env
.env.*
!.env.example
# Docker
Dockerfile
.dockerignore
+40
View File
@@ -0,0 +1,40 @@
MODE= #possible values: none or testserver
COMPLEX_PATIENTS= #TRUE if only complex patients should be used otherwise FALSE
COMPLEX_FHIR_SEARCH= #URL for the patient selection on the server, e.g. all gout patients:
IS_SYNTHEA= #TRUE if synthea data is used otherwise FALSE
FHIR_SERVER_URL= #URL to the FHIR server, e.g. http://hapi.fhir.org/baseR4; use http://blaze:8080/fhir when COMPOSE_PROFILES includes blaze
BOLT_ADVERTISED_ADDRESS=localhost:8081 #change in production
NEO4J_AUTH= # set if the server needs authentification: none otherwise true
ENABLE_BOLT_TLS= #true to require TLS on the bolt connector (needs real certs in ./neo4j-certs/bolt/, used in production); leave unset/false for local runs
DISABLE_IMPORT= #true if only neo4j should be started and load existing import data, false if the complete pipeline should run
### Settings for deployment - might be adapted depending on the network its deployed in and
#FHIR_SERVER_USER=
#FHIR_SERVER_PW=
#HTTP_PROXY=
#HTTPS_PROXY=
#NO_PROXY=
### Settings for how to get the data
NUMBER_OF_PATIENTS=2 # depends on the goal, very high to get all patients, lower numbers for testing; depends on disk space, plan with 30GB per 60k patients
BATCH_SIZE=10 # depends on the computational power, plan with 12GB per 250 patients; higher batch sizes improve the needed time to run the pipeline
# MDM
MDM_MODE= #biocypher or mdm (beta)
#OAUTH2_COOKIE_SECURE=true # or: NEO4J_COOKIE_SECRET= #for local setup
#Keycloak settings if a local keycloak instance is used
#KEYCLOAK_ISSUER_URL=https://---your productive keycloak url---/realms/yourRealm #if using the setup with a local keycloak use: http://localhost:4040/realms/testPipeline
#NEO4J_OAUTH_CLIENT_ID=neo4j-client
#NEO4J_OAUTH_SECRET=secret-from-production-keycloak or secret from local keycloak setup
#NEO4J_PROXY_REDIRECT_URL=https://your-domain.com/oauth2/callback
COMPOSE_PROFILES= #comma-separated: blaze, keycloak, proxy, server - activates parts of docker compose depending on the setup (see README)
#Test vars
TEST_MODE=FALSE
TEST_DEPTH=4
+41
View File
@@ -0,0 +1,41 @@
name: "Test and code quality"
description: "Run tests and code quality checks"
inputs:
NEO4J_VERSION:
description: "Neo4j version"
runs:
using: "composite"
steps:
#----------------------------------------------
# setup docker containers for testing
#----------------------------------------------
# currently only running on Linux due to technical limitations
# - name: Install Docker
# uses: douglascamata/setup-docker-macos-action@v1-alpha
# if: ${{ runner.os == 'macOS' }}
- name: Start Neo4j Docker
run: docker run --restart always --publish=7474:7474 --publish=7687:7687 --env NEO4J_AUTH=neo4j/your_password_here --env NEO4J_PLUGINS='["apoc"]' --env=NEO4J_ACCEPT_LICENSE_AGREEMENT=yes -d neo4j:${{ inputs.NEO4J_VERSION }}
shell: bash
if: ${{ runner.os == 'Linux' }}
- name: Start Postgres Docker
run: docker run --restart always --publish=5432:5432 --env POSTGRES_PASSWORD=postgres -d postgres:11.21-bullseye
shell: bash
if: ${{ runner.os == 'Linux' }}
#----------------------------------------------
# run tests and code quality checks
#----------------------------------------------
- name: Run Tests (Windows)
run: |
uv run pytest --version
uv run pytest --password=your_password_here
shell: bash
if: runner.os == 'Windows'
- name: Run tests (Linux and MacOS)
run: |
uv run pytest --version
uv run pytest --password=your_password_here
shell: bash
if: runner.os != 'Windows'
- name: Check code quality
uses: pre-commit/action@v3.0.0
+38
View File
@@ -0,0 +1,38 @@
*~
*__pycache__
build/
docs/pypath_log/
docs/_build/
docs/biocypher-log/
docs/modules/
docs/notebooks/*.yaml
docs/notebooks/*.py
.DS_Store
.vscode
biocypher.egg-info/
*.egg
dist/
*.prof
*.coverage
*.pickle
out/*
biocypher-log/*
biocypher-out/*
*.log
dist/*
*.pye
*.pyc
*.kate-swp
.hypothesis/
.venv/
.empty
.pytest_cache
*.graphml
.idea/*
.cache
*.iml
# Local env files with real secrets - keep *.example templates tracked
.env
.env.*
!.env.example
+6
View File
@@ -0,0 +1,6 @@
[submodule "networkx-based"]
path = networkx-based
url = git@git.uni-greifswald.de:MeDaX/networkx-based.git
[submodule "mdm2neo4j"]
path = mdm2neo4j
url = https://git.uni-greifswald.de/guetebierl/mdm2neo4j.git
+50
View File
@@ -0,0 +1,50 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
fail_fast: false
default_language_version:
python: python3
default_stages:
- commit
- push
minimum_pre_commit_version: 2.7.1
repos:
- repo: https://github.com/ambv/black
rev: 23.7.0
hooks:
- id: black
- repo: https://github.com/timothycrosley/isort
rev: 5.12.0
hooks:
- id: isort
additional_dependencies: [toml]
- repo: https://github.com/snok/pep585-upgrade
rev: v1.0
hooks:
- id: upgrade-type-hints
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: check-docstring-first
- id: end-of-file-fixer
- id: check-added-large-files
- id: mixed-line-ending
- id: trailing-whitespace
exclude: ^.bumpversion.cfg$
- id: check-merge-conflict
- id: check-case-conflict
- id: check-symlinks
- id: check-yaml
args: [--unsafe]
- id: check-ast
- id: fix-encoding-pragma
args: [--remove] # for Python3 codebase, it's not necessary
- id: requirements-txt-fixer
- repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.10.0
hooks:
- id: python-no-eval
- id: python-use-type-annotations
- id: python-check-blanket-noqa
- id: rst-backticks
- id: rst-directive-colons
- id: rst-inline-touching-normal
+28
View File
@@ -0,0 +1,28 @@
FROM python:3.11-slim
WORKDIR /app
# Bring in the uv binary from Astral's official image (pinned-ish, reproducible)
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
# Compile bytecode for faster cold starts; install into a project-local
# venv at /app/.venv and put it on PATH so `python` resolves to it directly
# (this replaces `poetry config virtualenvs.create false`).
ENV UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy \
VIRTUAL_ENV=/app/.venv \
PATH="/app/.venv/bin:$PATH"
# --- Dependency layer: cached unless these files change ---
COPY pyproject.toml uv.lock ./
# --no-dev installs only the main deps (equivalent to poetry --only main).
# --frozen fails the build instead of silently re-resolving if uv.lock is stale.
RUN uv sync --no-dev --frozen
# --- Application layer ---
COPY . .
# Normalize line endings and make the entrypoint executable
RUN sed -i 's/\r$//' /app/entrypoint.sh \
&& chmod +x /app/entrypoint.sh
ENTRYPOINT ["/app/entrypoint.sh"]
+22
View File
@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2022 Saez Lab
Copyright (c) 2025 MeDaX research group
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+193
View File
@@ -0,0 +1,193 @@
# MeDaX Pipeline
## 📋 Description
The MeDaX pipeline transforms healthcare data from FHIR databases into Neo4j graph databases. This conversion enables efficient searching, querying, and analyses of interconnected health data that would otherwise be complex to retrieve using traditional SQL databases.
## ✨ Features
- Seamless conversion from FHIR to Neo4j graph structure
- Support for patient-centric data retrieval using FHIR's `$everything` operation
- Configurable batch processing for handling large datasets
- Docker-based deployment for easy setup and portability
- Compatible with public FHIR servers (e.g., HAPI FHIR) and private authenticated instances
## ⚙️ Prerequisites
- [Docker](https://docs.docker.com/engine/install/) with the [Docker Compose plugin](https://docs.docker.com/compose/install/linux/)
- A FHIR database with API access and the `$everything` operation enabled for retrieving patient data
- Alternatively: Use a public FHIR server such as [HAPI FHIR](https://hapi.fhir.org/) (default configuration)
- Optional: [Semspect](https://www.semspect.de/) license
- free scientific licenses can be requested
- helps with graph exploration
## 🚀 Installation
### Setup
1. Clone this repository
2. Create an environment configuration file
3. Configure the environment variables in `.env`:
- For HAPI test server (default): No changes needed
- For custom FHIR server:
- Change `MODE` to anything else
- Uncomment and set `URL`, `PASSWORD`, and `USERNAME` variables
- Adjust `BATCH_SIZE` and `NUMBER_OF_PATIENTS` according to your needs
- Configure any required proxy settings
4. If needed, modify proxy settings in the `Dockerfile`
- Uncomment and set proxy variables
### Running the Pipeline
#### Option A: Connect to an existing FHIR server (e.g. HAPI)
**Start the containers:**
```bash
docker compose up --build
```
**Stop and clean up (between runs):**
```bash
docker compose down --volumes
```
**Complete removal (containers and images):**
```bash
docker compose down --volumes --rmi all
```
#### Option B: Build a local FHIR server with POLAR data
This spins up a local [Blaze](https://github.com/samply/blaze) FHIR server pre-loaded with synthetic POLAR test bundles, using the `blaze` Compose profile.
**Setup:** in your `.env`, set:
```
COMPOSE_PROFILES=blaze
FHIR_SERVER_URL=http://blaze:8080/fhir
```
**Start the containers:**
```bash
docker compose up --build
```
**Stop and clean up (between runs):**
```bash
docker compose down --volumes
```
**Complete removal (containers and images):**
```bash
docker compose down --volumes --rmi all
```
#### Option C: Build a local FHIR server with POLAR data and a local keycloak server
This builds on Option B and adds a local Keycloak instance plus an OAuth2 proxy in front of Neo4j, using the `blaze`, `keycloak`, and `proxy` Compose profiles.
**Setup:** in your `.env`, set:
```
COMPOSE_PROFILES=blaze,keycloak,proxy
FHIR_SERVER_URL=http://blaze:8080/fhir
```
**Start the containers:**
##### Init containers
```bash
docker compose up --build
```
##### Create realm, client and user
1. Open keycloak in a browser: localhost:4040 and login with user: admin and password: admin
2. Create the realm "testPipeline"
3. Create a client:
* Client ID: neo4j-client
* Always display in UI "on"
* Hit the "Next" button
* Client authentification: on
* Hit the "Next" button
* Root URL: http://localhost:8082
* Home URL: http://localhost:8082
* Valid redirects URIs: http://localhost:8082/oauth2/callback
* Valid post logout redirect URIs: http://localhost:8082/*
* Web origin: http://localhost:8082
* Hit the "Save" button
* Go to the "Credentials" tab
* Copy the "Client Secret" to "NEO4J_OAUTH_SECRET" in your .env
4. Create a user:
* "Email verified": on
* Choose a user name
* Use random mail, e.g.: test@example.com
* Hit the "Create button"
* Click on the new user, go to the "Credentials" tab
* Set a password and disable "Temporary"
##### Restart containers without destroying the volumes
```bash
docker compose down
docker compose up
```
**Stop**
```bash
docker compose down
```
> **Note:** When adding --volumes to down, the keycloak setup will be lost
**Complete removal (containers and images):**
```bash
docker compose down --volumes --rmi all
```
> **Note:** Depending on your Docker installation, you might need to use `docker-compose` instead of `docker compose`.
> **Note:** With `COMPOSE_PROFILES` set in your `.env`, every `docker compose` command automatically includes the right services - no `-f` files needed.
## 🔍 Accessing the Neo4j Database
Once the pipeline has completed processing, you can access the Neo4j database:
1. Open your browser and navigate to `http://localhost:8080/` or `http://localhost:8082/` if you used keycloak (Option C, served via the `proxy` profile's `neo4j-proxy` service)
2. Connect by clicking the button, user name and password are not needed (disabled by config)
## 📊 Example Queries
Here are some basic Cypher queries to get you started with exploring your health data:
```cypher
// Count all nodes by type
MATCH (n) RETURN labels(n) as NodeType, count(*) as Count;
// Find all records for a specific patient
MATCH (p:Patient {id: 'patient-id'})-[r]-(connected)
RETURN p, r, connected;
// Retrieve all medication prescriptions
MATCH (m:Medication)-[r]-(p:Patient)
RETURN m, r, p;
```
## ❓ Troubleshooting
**Common Issues:**
- **Connection refused to FHIR server**: Check your network settings and ensure the FHIR server is accessible from within the Docker container.
- **Authentication failures**: Verify your credentials in the `.env` file.
- **Container startup failures**: Ensure all required Docker ports are available and not used by other applications.
- **No data found in fhir bundle**: Ensure that the FHIR server is up and responding patient data. Try set the COMPLEX_PATIENTS variable to FALSE in your .env file. Some FHIR servers might not support the FHIR search logic.
## 📚 Architecture
The MeDaX pipeline consists of the following components:
1. **FHIR Client**: Connects to the FHIR server and retrieves patient data
2. **Data Transformer**: Converts FHIR resources into graph entities and relationships
3. **Reference Processor**: Converts references to relationships
3. **BioCypher Adapter**: Prepares the transformed data for Neo4j admin import
4. **Neo4j Database**: Stores and serves the graph representation of the health data
## ✍️ Citation
If you use the MeDaX pipeline in your research, please cite: 10.5281/zenodo.15229077 and Mazein, I and Gebhardt, T et al. [MeDaX, a knowledge graph on FHIR.](https://doi.org/10.3233/shti240423)
## 🙏 Acknowledgements
- We are leveraging [BioCypher](https://biocypher.org) [![DOI](https://zenodo.org/badge/DOI/10.1038/s41587-023-01848-y.svg)](https://doi.org/10.1038/s41587-023-01848-y) to create the Neo4j admin input.
- Remark: We introduced slight adjustments to BioCypher's code to support batching.
- We used BioCypher's git template as a starting point for our development:
- Lobentanzer, S., BioCypher Consortium, & Saez-Rodriguez, J. Democratizing knowledge representation with BioCypher [Computer software]. https://github.com/biocypher/biocypher
- We used synthetic data generated with [Synthea](https://doi.org/10.1093/jamia/ocx079) during the development process. This data is provided in the testData folder.
- We are using the [HAPI](https://hapifhir.io/) R4 server while developing and showcasing the capabilities of our tool.
- This project has been funded by the BMBF, FKZ: 01ZZ2019.
+215
View File
@@ -0,0 +1,215 @@
# BioCypher ODM Import Schema Configuration with BioLink Model Integration
# Node types extracted from ODM generators mapped to BioLink classes
study:
represented_as: node
preferred_id: oid
label_in_input: Study
is_a: biolink:ClinicalTrial
properties:
OID: str
Name: str
Description: str
Protocol: str
ModelID: str
studyevent:
represented_as: node
preferred_id: oid
label_in_input: StudyEvent
is_a: biolink:ClinicalEntity
properties:
OID: str
Name: str
Type: str
StudyOID: str
ModelID: str
form:
represented_as: node
preferred_id: oid
label_in_input: Form
is_a: biolink:InformationContentEntity
properties:
OID: str
Name: str
StudyOID: str
ModelID: str
itemgroup:
represented_as: node
preferred_id: oid
label_in_input: ItemGroup
is_a: biolink:InformationContentEntity
properties:
OID: str
Name: str
StudyOID: str
ModelID: str
item:
represented_as: node
preferred_id: oid
label_in_input: Item
is_a: biolink:ClinicalFinding
properties:
OID: str
Name: str
DataType: str
Question: str
StudyOID: str
ModelID: str
alias:
represented_as: node
preferred_id: [context, name]
label_in_input: Alias
is_a: biolink:InformationContentEntity
properties:
Context: str
Name: str
ItemOID: str
StudyOID: str
ModelID: str
measurementunit:
represented_as: node
preferred_id: oid
label_in_input: MeasurementUnit
is_a: biolink:Unit
properties:
OID: str
Name: str
Symbol: str
basicdefinitions:
represented_as: node
preferred_id: [studyoid, modelid]
label_in_input: BasicDefinitions
is_a: biolink:InformationContentEntity
properties:
StudyOID: str
ModelID: str
rangecheck:
represented_as: node
preferred_id: [comparator, constraint, checkvalue]
label_in_input: RangeCheck
is_a: biolink:ClinicalFinding
properties:
Comparator: str
Constraint: str
CheckValue: str
codelist:
represented_as: node
preferred_id: oid
label_in_input: CodeList
is_a: biolink:InformationContentEntity
properties:
OID: str
Name: str
DataType: str
StudyOID: str
ModelID: str
codelistitem:
represented_as: node
preferred_id: codedvalue
label_in_input: CodeListItem
is_a: biolink:ConceptualEntity
properties:
CodedValue: str
Decode: str
# Edge types extracted from ODM generators mapped to BioLink predicates
study_has_studyevent:
represented_as: edge
label_in_input: STUDY_HAS_STUDYEVENT
source: study
target: studyevent
is_a: biolink:related_to
itemgroup_has_item:
represented_as: edge
label_in_input: ITEMGROUP_HAS_ITEM
source: itemgroup
target: item
is_a: biolink:has_part
form_has_itemgroup:
represented_as: edge
label_in_input: FORM_HAS_ITEMGROUP
source: form
target: itemgroup
is_a: biolink:has_part
studyevent_has_form:
represented_as: edge
label_in_input: STUDYEVENT_HAS_FORM
source: studyevent
target: form
is_a: biolink:has_part
item_has_alias:
represented_as: edge
label_in_input: ITEM_HAS_ALIAS
source: item
target: alias
is_a: biolink:same_as
itemgroup_has_alias:
represented_as: edge
label_in_input: ITEMGROUP_HAS_ALIAS
source: itemgroup
target: alias
is_a: biolink:same_as
item_has_measurementunit:
represented_as: edge
label_in_input: ITEM_HAS_MEASUREMENTUNIT
source: item
target: measurementunit
is_a: biolink:has_attribute
has_basedef:
represented_as: edge
label_in_input: HAS_BASEDEF
source: study
target: basicdefinitions
is_a: biolink:has_part
basedef_has_measurementunit:
represented_as: edge
label_in_input: BASEDEF_HAS_MEASUREMENTUNIT
source: basicdefinitions
target: measurementunit
is_a: biolink:has_part
item_has_rangecheck:
represented_as: edge
label_in_input: ITEM_HAS_RANGECHECK
source: item
target: rangecheck
is_a: biolink:has_attribute
item_has_codelist:
represented_as: edge
label_in_input: ITEM_HAS_CODELIST
source: item
target: codelist
is_a: biolink:has_attribute
codelist_has_codelistitem:
represented_as: edge
label_in_input: CODELIST_HAS_CODELISTITEM
source: codelist
target: codelistitem
is_a: biolink:has_part
composite:
represented_as: edge
label_in_input: COMPOSITE
source: alias
target: alias
is_a: biolink:related_to
+18
View File
@@ -0,0 +1,18 @@
# add your settings here (overriding the defaults)
biocypher:
dbms: neo4j
offline: true
#debug: true
output_directory: /neo4j_import #comment if you want to debug, so that bc creates a new folder for each run in /biocypher-out
schema_config_path: config/automated_schema.yaml #config/automated_schema.yaml
head_ontology:
url: config/head_ontology/biolink-model.owl.ttl
root_node: entity
neo4j:
delimiter: '\t'
array_delimiter: '|'
skip_duplicate_nodes: true
skip_bad_relationships: true
File diff suppressed because it is too large Load Diff
+283
View File
@@ -0,0 +1,283 @@
Title: BioCypher graph schema configuration file
# This configuration file establishes the hierarchy and connectivity in a newly
# set-up BioCypher property graph database. Naming should adhere to Biolink
# nomenclature (available at https://biolink.github.io/biolink-model/ or via
# the python module 'biolink-model-toolkit').
# The BioCypher YAML file specifies only the leaves of the hierarchy tree of
# the desired graph; the hierarchical structure of entities will be derived
# from the Biolink model + BRO model. Thus, only the immediate constituents
# of the graph need to be specified in the schema config.
# ---
# "Named Things"
# ---
# The implementation of named things is fairly straightforward, since they are
# usually represented in node form, which is also the Biolink recommendation.
# The same is not true for associations.
#
# A little more complex is the representation of aggregates of named things.
clinicalStatus:
is_a: ClinicalEntity
represented_as: node
preferred_id: fhir_id
label_in_input: clinicalStatus
properties:
coding_system: str
label: str
coding_code: str
Condition:
is_a: ClinicalEntity
represented_as: node
preferred_id: fhir_id
label_in_input: Condition
properties:
input_format: HL7 FHIR
data_specification: Medical Informatics Initiative Germany Core Data Set, Basic Modules
diagnosis:
is_a: ClinicalEntity
represented_as: node
preferred_id: fhir_id
label_in_input: diagnosis
properties:
type.coding_code: str
sequence: str
label: str
type.coding_system: str
DiagnosticReport:
is_a: ClinicalEntity
represented_as: node
preferred_id: fhir_id
label_in_input: DiagnosticReport
properties:
resourceType: str
label: str
status: str
id: str
Encounter:
is_a: ClinicalEntity
represented_as: node
preferred_id: fhir_id
label_in_input: Encounter
properties:
resourceType: str
label: str
status: str
id: str
identifier:
is_a: Attribute
represented_as: node
preferred_id: fhir_id
label_in_input: identifier
properties:
label: str
value: str
system: str
interpretation: #
is_a: named thing
represented_as: node
preferred_id: fhir_id
label_in_input: interpretation
properties:
extension.valueCoding_system: str
extension_url: str
extension.valueCoding_display: str
coding_code: str
coding_system: str
label: str
extension.valueCoding_code: str
maritalStatus:
is_a: OrganismAttribute
represented_as: node
preferred_id: fhir_id
label_in_input: maritalStatus
properties:
label: str
coding_system: str
coding_code: str
Observation:
is_a: ClinicalEntity
represented_as: node
preferred_id: fhir_id
label_in_input: Observation
properties:
resourceType: str
label: str
effectiveDateTime: str
status: str
id: str
Organization:
is_a: AdministrativeEntity
represented_as: node
preferred_id: fhir_id
label_in_input: Organization
properties:
label: str
id: str
name: str
resourceType: str
Patient:
is_a: Human
represented_as: node
preferred_id: fhir_id
label_in_input: Patient
properties:
resourceType: str
label: str
gender: str
id: str
birthDate: str
procedure:
is_a: named thing
represented_as: node
preferred_id: fhir_id
label_in_input: Procedure
properties:
label: str
performedDateTime: str
resourceType: str
status: str
id: str
referenceRange: #
is_a: named thing
represented_as: node
preferred_id: fhir_id
label_in_input: referenceRange
properties:
high_system: str
high_value: str
high_code: str
label: str
high_unit: str
search: #
is_a: named thing
represented_as: node
preferred_id: fhir_id
label_in_input: search
properties:
label: str
mode: str
type:
is_a: Attribute
represented_as: node
preferred_id: fhir_id
label_in_input: type
properties:
coding_system: str
label: str
coding_code: str
coding_display: str
verificationStatus:
is_a: Attribute
represented_as: node
preferred_id: fhir_id
label_in_input: verificationStatus
properties:
coding_system: str
label: str
coding_code: str
coding_display: str
# ---
# Associations
# ---
# Associations are not supposed to be represented in node form as per the
# specifications of Biolink. However, in an analytic context, it often makes
# sense to represent interactions as nodes in Neo4j, because it enables, for
# instance, the annotation of a relationship with a publication as source of
# evidence (also known as reification in the knowledge graph world).
# The Biolink specifications for these types of relationships do
# not go into depth; for example, the hierarchy for molecular interactions
# (ie, "associations") ends at "PairwiseMolecularInteraction", there are no
# explicit terms for protein-protein-interaction, phosphorylation, miRNA-
# targeting, etc. Biolink proposes to use interaction identifiers from
# ontologies, such as https://www.ebi.ac.uk/ols/ontologies/mi/.
# association to connect anything to an identifier node
# if functional, includes:
# IDENTIFIED_BY_Condition_Identifier,
# IDENTIFIED_BY_DiagnosticReport_Identifier,
# IDENTIFIED_BY_Encounter_Identifier,
# IDENTIFIED_BY_Observation_Identifier,
# IDENTIFIED_BY_Organization_Identifier
# IDENTIFIED_BY_Patient_Identifier,
# IDENTIFIED_BY_Procedure_Identifier
condition to identifier association:
is_a: association
represented_as: edge
label_in_input: IDENTIFIED_BY_Condition_Identifier
diagnostic report to identifier association:
is_a: association
represented_as: edge
label_in_input: IDENTIFIED_BY_DiagnosticReport_Identifier
observation to identifier association:
is_a: association
represented_as: edge
label_in_input: IDENTIFIED_BY_Observation_Identifier
observation derived from observation association:
is_a: association
represented_as: edge
label_in_input: DERIVED_FROM_Observation_Observation
observation has member observation association:
is_a: association
represented_as: edge
label_in_input: HAS_MEMBER_Observation_Observation
procedure to identifier association:
is_a: association
represented_as: edge
label_in_input: IDENTIFIED_BY_Procedure_Identifier
procedure to diagnostic report association:
is_a: association
represented_as: edge
label_in_input: IDENTIFIED_BY_Procedure_Identifier
procedure reasoned by observation association:
is_a: association
represented_as: edge
label_in_input: HAS_REASON_REFERENCE_Procedure_Observation
procedure performer is practitioner association:
is_a: association
represented_as: edge
label_in_input: HAS_ACTOR_ProcedurePerformer_Practitioner
#represented_as: edge
#label_in_input: DERIVED_FROM_Observation_Observation:
#represented_as: edge
#label_in_input: DERIVED_FROM_Observation_Observation
#protein interaction:
# is_a: Pairwise molecular interaction
# represented_as: edge
# label_in_input: protein_protein_interaction
#protein to disease association:
# is_a: Association
# represented_as: edge
# label_in_input: protein_disease_association
+284
View File
@@ -0,0 +1,284 @@
services:
neo4j-certs-perm-fix:
image: busybox
command: ["chmod", "-R", "755", "/certs"]
volumes:
- ./neo4j-certs:/certs
networks:
- medax-network
neo4j:
image: neo4j:5.7
restart: unless-stopped
depends_on:
neo4j-certs-perm-fix:
condition: service_completed_successfully
environment:
- NEO4J_AUTH=${NEO4J_AUTH:-neo4j/password}
- NEO4J_PLUGINS=["apoc"]
- NEO4J_server_config_strict__validation_enabled=false
- NEO4J_apoc_export_file_enabled=true
- NEO4J_apoc_import_file_enabled=true
- NEO4J_apoc_import_file_use__neo4j__config=true
- SHARED_PATH=/neo4j_import
- NO_PROXY=${NO_PROXY},10.0.0.0/8
- DISABLE_IMPORT=${DISABLE_IMPORT:-false}
- ENABLE_BOLT_TLS=${ENABLE_BOLT_TLS:-false}
command: >
bash -c '
if [ "$${DISABLE_IMPORT:-false}" != "true" ]; then
echo "running cmd from docker compose" &&
# Copy plugin files if they exist
if [ -f /init_files/semspect_neo4j-plugin-8.2.0.jar ]; then
echo "Copying Semspect plugin..." &&
cp /init_files/semspect_neo4j-plugin-8.2.0.jar /var/lib/neo4j/plugins/
fi
if [ -f /init_files/semspect.lic ]; then
echo "Copying Semspect license..." &&
cp /init_files/semspect.lic /var/lib/neo4j/plugins/
fi
# Copy neo4j.conf if it exists
ls
ls /init_files/
if [ -f /init_files/neo4j.conf ]; then
echo "Copying Neo4j configuration..." &&
cp /init_files/neo4j.conf /var/lib/neo4j/conf/
fi
if [ "$${ENABLE_BOLT_TLS:-false}" != "true" ]; then
echo "ENABLE_BOLT_TLS is not true - disabling required bolt TLS (no certs mounted in ./neo4j-certs locally)." &&
sed -i \
-e "s/^server.bolt.tls_level=REQUIRED/#server.bolt.tls_level=REQUIRED/" \
-e "s/^dbms.ssl.policy.bolt.enabled=true/#dbms.ssl.policy.bolt.enabled=true/" \
/var/lib/neo4j/conf/neo4j.conf
fi
echo "<----------------"
# Set proper permissions
#chown -R neo4j:neo4j /var/lib/neo4j/plugins
#chown -R neo4j:neo4j /var/lib/neo4j/conf
echo "Import loop is enabled."
while true; do
if [ -f /neo4j_import/ready-to-import ]; then
echo "Starting import process..."
neo4j stop &&
bash /neo4j_import/neo4j-admin-import-call.sh &&
rm /neo4j_import/ready-to-import &&
touch /neo4j_import/import-complete &&
chmod 777 /neo4j_import/import-complete
neo4j start
echo "The container is running. CTRL+C will end the bash command and thus, the neo4j container"
fi
sleep 10
done
else
echo "Import loop is disabled. Keeping Neo4j container alive..."
cp /init_files/neo4j.conf /var/lib/neo4j/conf/
if [ "$${ENABLE_BOLT_TLS:-false}" != "true" ]; then
echo "ENABLE_BOLT_TLS is not true - disabling required bolt TLS (no certs mounted in ./neo4j-certs locally)." &&
sed -i \
-e "s/^server.bolt.tls_level=REQUIRED/#server.bolt.tls_level=REQUIRED/" \
-e "s/^dbms.ssl.policy.bolt.enabled=true/#dbms.ssl.policy.bolt.enabled=true/" \
/var/lib/neo4j/conf/neo4j.conf
fi
neo4j start
echo "The container is running. CTRL+C will end the bash command and thus, the neo4j container"
tail -f /dev/null
fi
'
healthcheck:
test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:7474 || exit 1"]
interval: 10s
timeout: 5s
retries: 5
ports:
- "8080:7474" # direct access — remove in production
- "8081:7687" # remove in production
#- "127.0.0.1:8080:7474" #production
#- "127.0.0.1:8081:7687"
volumes:
- neo4j_data:/data
- neo4j_logs:/logs
- neo4j_import:/neo4j_import
- ${INPUT_DATA_PATH:-./data}:/input_data
- ./init_scripts:/init_scripts
- ./init_files:/init_files
- ./importData:/importData
- ./neo4j-certs:/var/lib/neo4j/certificates
networks:
- medax-network
python_app:
depends_on:
loader:
condition: service_completed_successfully
required: false
build:
context: .
dockerfile: Dockerfile
args:
HTTP_PROXY: ${HTTP_PROXY}
HTTPS_PROXY: ${HTTPS_PROXY}
NO_PROXY: ${NO_PROXY}
env_file:
- .env
environment:
- NEO4J_URI=bolt://neo4j:7687
- NEO4J_USER=${NEO4J_USER:-neo4j}
- NEO4J_PASSWORD=${NEO4J_PASSWORD:-password}
- DISABLE_IMPORT=${DISABLE_IMPORT:-false}
- INPUT_DATA_PATH=/input_data
- POETRY_VIRTUALENVS_CREATE=false
- NEO4J_dbms_directories_import=/neo4j_import
volumes:
- neo4j_import:/neo4j_import
- ${INPUT_DATA_PATH:-./data}:/input_data
- ./importData:/importData # Share the import data directory
# depends_on:
# neo4j:
# condition: service_healthy
networks:
- medax-network
oauth2-proxy:
profiles:
- server
image: quay.io/oauth2-proxy/oauth2-proxy:v7.6.0
container_name: oauth2-proxy
restart: unless-stopped
extra_hosts:
- "keycloak-test.miracum.med.uni-greifswald.de:10.66.82.11"
- "host.docker.internal:host-gateway"
command:
- --config=/etc/oauth2-proxy.cfg
ports:
# - "80:80"
- "443:443"
volumes:
- ./oauth_conf/oauth2-proxy.cfg:/etc/oauth2-proxy.cfg:ro
- ./oauth_templates:/templates:ro
- ./oauth-certs:/etc/oauth-certs:ro
- ./logs:/var/log/oauth2-proxy
depends_on:
- neo4j
networks:
- medax-network
nginx:
profiles:
- server
image: nginx:latest
container_name: neo4j-nginx
## ports:
## - "8088:80"
# - "7687:7687"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
- ./logs:/var/log/nginx
depends_on:
- neo4j
networks:
- medax-network
neo4j-proxy:
profiles:
- proxy
image: quay.io/oauth2-proxy/oauth2-proxy:latest
restart: on-failure
environment:
OAUTH2_PROXY_PROVIDER: keycloak-oidc
OAUTH2_PROXY_CLIENT_ID: ${NEO4J_OAUTH_CLIENT_ID:-neo4j-client}
OAUTH2_PROXY_CLIENT_SECRET: ${NEO4J_OAUTH_SECRET}
OAUTH2_PROXY_OIDC_ISSUER_URL: ${KEYCLOAK_ISSUER_URL}
OAUTH2_PROXY_UPSTREAMS: http://neo4j:7474/
OAUTH2_PROXY_HTTP_ADDRESS: 0.0.0.0:4180
OAUTH2_PROXY_REDIRECT_URL: ${NEO4J_PROXY_REDIRECT_URL:-http://localhost:8082/oauth2/callback}
OAUTH2_PROXY_COOKIE_SECRET: ${NEO4J_COOKIE_SECRET}
OAUTH2_PROXY_EMAIL_DOMAINS: "*"
OAUTH2_PROXY_SKIP_PROVIDER_BUTTON: "true"
OAUTH2_PROXY_INSECURE_OIDC_SKIP_ISSUER_VERIFICATION: "true"
OAUTH2_PROXY_COOKIE_SECURE: "false"
extra_hosts:
- "localhost:host-gateway"
ports:
- "8082:4180"
depends_on:
- neo4j
#- keycloak # enable if a local keycloak is set up
keycloak:
profiles:
- keycloak
image: quay.io/keycloak/keycloak:latest
command: start-dev
restart: on-failure
environment:
KC_BOOTSTRAP_ADMIN_USERNAME: admin
KC_BOOTSTRAP_ADMIN_PASSWORD: admin
KC_HOSTNAME: http://localhost:4040
KC_HOSTNAME_BACKCHANNEL_DYNAMIC: "true"
ports:
- "4040:8080"
volumes:
- keycloak_data:/opt/keycloak/data
blaze:
profiles:
- blaze
image: samply/blaze:latest
ports:
- "8090:8080"
environment:
JAVA_TOOL_OPTIONS: "-Xmx2g"
ENFORCE_REFERENTIAL_INTEGRITY: "false"
volumes:
- blaze-data:/app/data
healthcheck:
test: curl -f http://localhost:8080/fhir/metadata || exit 1
interval: 5s
timeout: 10s
retries: 30
networks:
- medax-network
loader:
profiles:
- blaze
image: curlimages/curl:latest
depends_on:
blaze:
condition: service_healthy
volumes:
- ./testData/POLAR_mock:/bundles:ro #adapt first part to whereever the testfiles are stored
entrypoint: /bin/sh
command: >
-c '
for f in /bundles/*.json; do
echo "Loading $$f ...";
curl -s -X POST http://blaze:8080/fhir \
-H "Content-Type: application/fhir+json" \
-d @"$$f" > /dev/null;
echo "";
done;
echo "Done loading all bundles."
'
networks:
- medax-network
# Define named volumes
volumes:
neo4j_data:
neo4j_logs:
neo4j_import:
keycloak_data:
blaze-data:
networks:
medax-network:
driver: bridge
+50
View File
@@ -0,0 +1,50 @@
#!/bin/bash
set -euo pipefail
if [ "${DISABLE_IMPORT:-false}" = "true" ]; then
echo "DISABLE_IMPORT is true - skipping ETL pipeline to only up the GDB"
exit 0
fi
# Wait for a file to appear, with a timeout so the container can't hang forever.
# Usage: wait_for_file <path> [timeout_seconds]
wait_for_file() {
local file="$1"
local timeout="${2:-300}"
local waited=0
while [ ! -f "$file" ]; do
if [ "$waited" -ge "$timeout" ]; then
echo "Timed out after ${timeout}s waiting for $(basename "$file")" >&2
exit 1
fi
echo "Waiting for $(basename "$file")..."
sleep 5
waited=$((waited + 5))
done
}
# Make the import dir accessible to both the Python app and Neo4j
chmod -R 777 /neo4j_import
#echo "Waiting for Neo4j to be ready..."
#python wait-for-neo4j.py
echo "Running Python data processing script..."
#python import_fhir_to_nx_diGraph.py
python multiple_adapters.py
echo "Running Neo4j import..."
# Give downstream services a moment before touching the database
sleep 5
# Wait for the shell script to finish writing its data
wait_for_file /neo4j_import/shell-scipt-complete
# Signal that data is prepared and ready to import
touch /neo4j_import/ready-to-import
chmod 777 /neo4j_import/ready-to-import
# Wait for the import to complete
echo "Waiting for Neo4j import to complete..."
wait_for_file /neo4j_import/import-complete
echo "Database setup complete!"
+72
View File
@@ -0,0 +1,72 @@
import requests
from typing import List, Dict, Any
from dotenv import load_dotenv
import os
from requests.auth import HTTPBasicAuth
# Load environment variables from .env file
load_dotenv()
def get_bundle(url, search):
headers = {
'Accept': 'application/fhir+json',
'Content-Type': 'application/fhir+json'
}
# Get configuration from environment variables
mode = os.getenv('MODE')
fhir_server = os.getenv('FHIR_SERVER_URL')
if mode != 'testsever':
username = os.getenv('FHIR_SERVER_USER')
password = os.getenv('FHIR_SERVER_PW')
if not fhir_server:
raise ValueError("FHIR_SERVER_URL not found in environment variables")
if (not username or not password) and mode != 'testserver':
raise ValueError("FHIR_USERNAME and FHIR_SERVER_PW must be set in environment variables")
# Setup basic authentication
auth = HTTPBasicAuth(username, password)
if url is not None:
if '?' in url:
link = url + '&_format=json'
else:
link = url + '?_format=json'
else:
link = fhir_server + search + '&_format=json'
#print(link)
if mode != 'testserver':
response = requests.get(
link,
headers=headers,
auth=auth,
timeout=300
)
else:
response = requests.get(
link,
headers=headers,
timeout=300
)
return response
def get_patient_everything(id: str):
search = '/Patient/' + id + '/$everything?'
return get_bundle(None, search)
# Example usage
if __name__ == "__main__":
bundles = get_bundles(None)
data = bundles.json()
# Process the bundles
for entry in data['entry']:
print(f"{entry['fullUrl']}")
+42
View File
@@ -0,0 +1,42 @@
import json
import networkx as nx
def add_nodes_from_dict(graph, parent_node, current_dict):
for key, value in current_dict.items():
if isinstance(value, dict):
# Create a new node for the nested dictionary
new_node = f"{parent_node}.{key}"
graph.add_node(new_node, label=key)
# Add an edge from the parent node to the new node
graph.add_edge(parent_node, new_node, edge_type=key)
# Recurse into the nested dictionary
add_nodes_from_dict(graph, new_node, value)
elif isinstance(value, list):
# if list doesn't contain any nested dictionaries, make it a value in the node
if any(isinstance(item, dict) for item in value)==False:
graph.nodes[parent_node][key] = value
else:
process_dictionaries(value, parent_node, key, graph)
else:
# For non-dict and non-list values, add them as attributes to the parent node
graph.nodes[parent_node][key] = value
def process_dictionaries(value, parent_node, key, graph):
# Process each dictionary in the list
for index, item in enumerate(value):
if isinstance(item, dict):
if len(value)>1:
item_node = f"{parent_node}.{key}[{index}]"
else:
item_node = f"{parent_node}.{key}"
graph.add_node(item_node, label=key)
graph.add_edge(parent_node, item_node, edge_type=key)
add_nodes_from_dict(graph, item_node, item)
def add_json_to_networkx(json_data, bundle_name, graph):
if not isinstance(graph, nx.DiGraph):
raise ValueError("The provided graph must be a networkx.DiGraph")
root_node = bundle_name+'_bundle'
graph.add_node(root_node, label='root')
add_nodes_from_dict(graph, root_node, json_data)
+38
View File
@@ -0,0 +1,38 @@
import networkx as nx
class Resource:
def __init__(self, resource_type):
self.resource_type = resource_type
def create_resource_class(resource_type):
return type(resource_type, (Resource,), {})
def set_resource_type(graph):
for node, data in graph.nodes(data=True):
print(node, data)
print("-----------------------------")
nodes_to_replace = []
for node, data in graph.nodes(data=True):
print(isinstance(node, Resource), node, type(node))
if isinstance(node, Resource):
print("Found a resource: ", node)
resource_type = node.resource_type
if resource_type:
# Dynamically create a new class based on the resource_type
new_resource_class = create_resource_class(resource_type)
new_node = new_resource_class(resource_type)
nodes_to_replace.append((node, new_node, data))
else:
print(f"Warning: Node {node} is a resource but has no resource_type")
# Replace old nodes with new ones
for old_node, new_node, data in nodes_to_replace:
graph.add_node(new_node, **data)
for pred in graph.predecessors(old_node):
graph.add_edge(pred, new_node)
for succ in graph.successors(old_node):
graph.add_edge(new_node, succ)
graph.remove_node(old_node)
+96
View File
@@ -0,0 +1,96 @@
import networkx as nx
import os
def parse_synthea_reference(ref):
if not ref.startswith('#'):
#print("reference: ", ref)
if '?' in ref and '|' in ref:
parsed_ref = ref.split('|')[1]
# elif '/' in ref:
# parsed_ref = ref.split('/')[1]
else:
parsed_ref = ref.split(':')[2]
else:
parsed_ref = 'mock'
return(parsed_ref)
def process_references(graph):
is_synthea = os.getenv('IS_SYNTHEA')
print("--------------->", is_synthea)
nodes_with_reference = [[n, attr['reference']] for n, attr in graph.nodes(data=True) if 'reference' in attr]
directly_referenced_nodes = []
indirectly_referenced_nodes = []
dummy_references = []
if is_synthea and is_synthea.upper() == 'TRUE':
nodes_with_mock_reference = []
for i in range(len(nodes_with_reference)):
reference = nodes_with_reference[i][1]
parsed_reference = parse_synthea_reference(reference)
if parsed_reference != 'mock':
nodes_with_reference[i].append(parsed_reference)
else:
nodes_with_mock_reference.append(i)
for i in sorted(nodes_with_mock_reference, reverse=True):
del nodes_with_reference[i]
id_to_node = {data["id"]: node for node, data in graph.nodes(data=True) if "id" in data}
id_to_identifier_node = {data["value"]: node for node, data in graph.nodes(data=True) if ("value" in data and data['label'] == 'identifier')}
for i in nodes_with_reference:
ref_id=i[2]
if ref_id in id_to_node.keys():
directly_referenced_nodes.append([i[0], id_to_node[ref_id]])
elif ref_id in id_to_identifier_node.keys():
indirectly_referenced_nodes.append([i[0], id_to_identifier_node[ref_id]])
#else:
# print("KEY ERROR: Key neither in to_node nor in to_identifier_node", i)
for i in indirectly_referenced_nodes:
node_from=list(graph.predecessors(i[0]))[0]
node_to=list(graph.predecessors(i[1]))[0]
ref_type=graph.nodes[i[0]]['label']
graph.add_edge(node_from, node_to, edge_type='reference', reference_type=ref_type)
else:
id_to_node = {data["resourceType"]+'/'+data["id"]: node for node, data in graph.nodes(data=True) if ("id" in data and "resourceType" in data)}
for i in nodes_with_reference:
ref_id=i[1]
if ref_id in id_to_node.keys():
#print("everything is here?", ref_id)
directly_referenced_nodes.append([i[0], id_to_node[ref_id]])
else:
#print("ALL WE KNOW ABOUT DUMMY NODES: ", ref_id)
dummy_references.append([i[0], ref_id])
for i in directly_referenced_nodes:
node_from=list(graph.predecessors(i[0]))[0]
node_to=i[1]
ref_type=graph.nodes[i[0]]['label']
graph.add_edge(node_from, node_to, edge_type='reference', reference_type=ref_type)
for i in dummy_references:
ref_type = i[1][:i[1].find('/')]
node_to='dummy_' + i[1]
graph.add_node(node_to, label='dummy', unique_id=i[1], edge_label=ref_type)
node_from=list(graph.predecessors(i[0]))[0]
graph.add_edge(node_from, node_to, edge_type='reference', reference_type=ref_type)
graph.remove_nodes_from([i[0] for i in directly_referenced_nodes])
graph.remove_nodes_from([i[0] for i in indirectly_referenced_nodes])
graph.remove_nodes_from([i[0] for i in dummy_references])
nodes_to_remove = [n for n, attr in graph.nodes(data=True) if attr.get('label') in ['root', 'entry', 'request']]
graph.remove_nodes_from(nodes_to_remove)
+107
View File
@@ -0,0 +1,107 @@
import networkx as nx
def find_paths(graph, start_node):
def is_leaf(node):
#Checks if a node is a leaf (no outgoing edges)
return graph.out_degree(node) == 0
def custom_dfs(path, reference_count):
#Performs a DFS to find paths for both patterns
current_node = path[-1]
'''if the current node is labeled 'resource', the path length is greater than 3,
and we have exactly one 'reference' edge in the path'''
if len(path) > 3 and graph.nodes[current_node].get('label') == 'resource' and reference_count == 1:
# add path to the list of property paths containing a reference
reference_paths.append(list(path))
'''if the current node is a leaf node (no outgoing edges),
the path length is greater than 2, and we have no references in the path'''
if len(path) > 2 and is_leaf(current_node) and reference_count == 0:
'''add path to the dictionary of property paths ending in leaves,
by the corresponding property key'''
leaf_paths.setdefault(path[1].split('.')[-1], []).extend(list(path))
# check neighbors
for neighbor in graph.successors(current_node):
edge_type = graph.edges[current_node, neighbor].get('edge_type', None)
new_reference_count = reference_count + (1 if edge_type == 'reference' else 0)
# continue the search only if we have at most one 'reference' edge so far
if new_reference_count <= 1:
custom_dfs(path + [neighbor], new_reference_count)
reference_paths = []
leaf_paths = {}
custom_dfs([start_node], 0)
return reference_paths, leaf_paths
def property_convolution(graph):
# Find all nodes with label 'resource'
resource_nodes = [n for n, attr in graph.nodes(data=True) if attr.get('label') == 'resource']
#print("Got all nodes with label 'resource'", flush=True)
'''collect all paths starting with a resource node, that contain one reference edge,
end with a resource node and are >3 nodes long'''
'''collect all paths starting with a resource node, that do not contain reference edges,
end with a leaf node and are >2 nodes long'''
property_paths_with_reference = []
property_paths_with_leaves = {}
for resource_node in resource_nodes:
temp_ref_paths, temp_leaf_paths = find_paths(graph, resource_node)
# add paths to the list of property paths containing a reference, for all nodes
property_paths_with_reference.extend(temp_ref_paths)
# add paths to the dictionary of property paths ending in leaves, by the corresponding resouce key
property_paths_with_leaves[resource_node] = temp_leaf_paths
# print("Collected all paths", flush=True)
# transfer reference edge to first property node for all reference paths
for i in property_paths_with_reference:
ref_edge_data = graph.get_edge_data(i[-2], i[-1])
ref_type = ref_edge_data.get('reference_type')
graph.remove_edge(i[-2], i[-1])
graph.add_edge(i[1], i[-1], edge_type='reference', reference_type=ref_type)
'''after transferrence, add the modified reference path (that now ends in a leaf)
to the dictionary of leaf paths, by corresponding resource and property keys'''
property_paths_with_leaves[i[0]].setdefault(i[1].split('.')[-1], []).extend(i[:-1])
#print("Transfered all references edges", flush=True)
'''create a list of collections of property paths ending in leaves,
removing duplicate nodes from each path collection'''
list_property_paths_with_leaves = [list(dict.fromkeys(i)) for j in property_paths_with_leaves.values() for i in j.values()]
nodes_to_remove=[]
for i in list_property_paths_with_leaves:
for j in range(len(i)-1, 1, -1):
source_attributes = graph.nodes[i[j]]
marker='|'.join(i[j].split('resource.')[1].split('.')[1:])
# transfer attributes to first property node
for attr, value in source_attributes.items():
if attr != 'label':
graph.nodes[i[1]][marker+'_'+attr] = value
nodes_to_remove.append(i[j])
#print("Transferred attributes for all paths", flush=True)
graph.remove_nodes_from(nodes_to_remove)
for i in resource_nodes:
unique_resource_id = graph.nodes[i]['resourceType']+'/'+graph.nodes[i]['id']
graph.nodes[i]['unique_id'] = unique_resource_id
for j in graph.successors(i):
if graph[i][j].get('edge_type') != 'reference':
graph.nodes[j]['unique_id'] = unique_resource_id+'/'+j.split('.')[-1]
+303
View File
@@ -0,0 +1,303 @@
from biocypher import BioCypher
import networkx as nx
import json
import os
import sys
import re
import uuid
import gc
from dotenv import load_dotenv
from graphCreation import create_graph
from graphCreation.process_references import process_references
from graphCreation.property_convolution import property_convolution
from schema_config_generation import write_automated_schema
from fhirImport import get_patient_everything, get_bundle
is_test = os.getenv('TEST_MODE')
test_depth = os.getenv('TEST_DEPTH')
def load_multiple_fhir_patients(n):
init_load = True
ids = []
#get n ids
init_ids = True
while len(ids) < n and init_ids:
if init_load:
is_complex = os.getenv('COMPLEX_PATIENTS')
selected_patients_file = os.getenv('SELECTED_PATIENTS_FILE')
#print("> /dev/null")
if selected_patients_file and os.path.isfile(selected_patients_file):
print(f"-- Loading patient IDs from file: {selected_patients_file} --")
with open(selected_patients_file, 'r') as f:
for line in f:
patient_id = line.strip()
if patient_id:
ids.append(patient_id)
print(f"Loaded {len(ids)} patient IDs from file.")
n = len(ids)
break # Exit the while loop since we got IDs from the file
elif is_complex and is_complex.upper() != 'TRUE':
bundle = get_bundle(None, '/Patient?_count=' + str(n))
else:
#print("-- Looking for complex patients --")
print("-- Looking for gout patients --")
#bundle = get_bundle(None, '/Patient?_has:Observation:subject:status=final&_count=' + str(n))
bundle = get_bundle(None, '/Patient?_has:Condition:patient:code=M10.00,M10.01,M10.02,M10.03,M10.04,M10.05,M10.06,M10.07,M10.08,M10.09,M10.10,M10.11,M10.12,M10.13,M10.14,M10.15,M10.16,M10.17,M10.18,M10.19,M10.20,M10.21,M10.22,M10.23,M10.24,M10.25,M10.26,M10.27,M10.28,M10.29,M10.30,M10.31,M10.32,M10.33,M10.34,M10.35,M10.36,M10.37,M10.38,M10.39,M10.40,M10.41,M10.42,M10.43,M10.44,M10.45,M10.46,M10.47,M10.48,M10.49,M10.90,M10.91,M10.92,M10.93,M10.94,M10.95,M10.96,M10.97,M10.98,M10.99&_count=' + str(n))
print("Got patient ids")
init_load = False
else:
print("NEXT LINK: ", next_link, flush=True)
bundle = get_bundle(next_link, None)
#print(bundle.json())
if 'entry' not in bundle.json():
print("ERROR -- No data found in the fhir bundle. Check the request and if the server is up and responding")
sys.exit(1)
for entry in bundle.json()['entry']:
ids.append(entry['resource']['id'])
init_ids = False
for l in bundle.json()['link']:
if l['relation'] == "next":
next_link = l['url']
init_ids = True
if len(ids) < n:
n = len(ids)
print("####GRABBED ", n , " IDs from the DB####", flush=True)
bundles_to_graph(ids, n)
def bundles_to_graph(ids, n):
init = True
batch_size = int(os.getenv('BATCH_SIZE'))
c = 0
print(len(ids))
#get bundle for each ID
for id in ids:
c += 1
bundle = get_patient_everything(id).json()
bundle = replace_single_quotes(bundle) ### maybe not needed for german data
if init:
graph = nx.DiGraph()
init = False
create_graph.add_json_to_networkx(bundle, id + '_bundle', graph)
if c % 50 == 0:
print("---------- ", c, " patients loaded ----------", flush=True)
if c % batch_size == 0 or c == n:
print(c, " patients imported, reducing graph", flush = True)
process_references(graph)
property_convolution(graph)
run_biocypher(graph)
init = True
#print("Full batch:", c)
del graph
def replace_single_quotes(obj):
if isinstance(obj, str): # If it's a string, replace single quotes
return obj.replace("'", "''")
elif isinstance(obj, dict): # If it's a dictionary, process each key-value pair
return {key: replace_single_quotes(value) for key, value in obj.items()}
elif isinstance(obj, list): # If it's a list, process each item
return [replace_single_quotes(item) for item in obj]
else:
return obj # Leave other data types unchanged
def main():
## create networkX and run improvement scripts
print("Creating the graph...", flush=True)
n_patients = int(os.getenv('NUMBER_OF_PATIENTS'))
load_multiple_fhir_patients(n_patients)
#write the import script -- we are creating our own script since BC would only consider the last batch as an input
print("CREATING THE SCRIPT")
generate_neo4j_import_script()
with open('/neo4j_import/shell-scipt-complete', 'w') as f:
f.write('Import completed successfully')
print("FHIR import completed successfully")
def run_biocypher(nx_graph):
#get lists of node and edge types
print("Generate auto schema...", flush=True)
write_automated_schema(nx_graph, 'config/automated_schema.yaml', 'config/manual_schema_config.yaml')
# create Biocypher driver
bc = BioCypher(
biocypher_config_path="config/biocypher_config.yaml",
)
#bc.show_ontology_structure() #very extensive
#BioCypher preperation
bc.write_nodes(node_generator(nx_graph))
bc.write_edges(edge_generator(nx_graph))
def node_generator(nx_graph):
for node in nx_graph.nodes():
label = nx_graph.nodes[node].get('label')
if(label == 'dummy' or label == 'Dummy'):
print("Skipped dummy: ", nx_graph.nodes[node])
continue
if label == "resource":
label = nx_graph.nodes[node].get('resourceType')
label = label.capitalize()
nx_graph.nodes[node]['label'] = label
if(nx_graph.nodes[node].get('label') in ['search', 'meta', 'link', 'Search', 'Meta', 'Link']):
continue
yield(
nx_graph.nodes[node].get('unique_id', node), #remark: this returns the node id if this attribute exists. otherwise it returns node which equals the identifier that is used by nx
label,
nx_graph.nodes[node] # get properties
)
def edge_generator(nx_graph):
for edge in nx_graph.edges(data = True):
source, target, attributes = edge
s_label = nx_graph.nodes[source].get('label')
if s_label == 'resource':
s_label = nx_graph.nodes[source].get('resourceType')
elif s_label == 'dummy' or s_label == 'Dummy':
s_label = nx_graph.nodes[source].get('edge_label')
t_label = nx_graph.nodes[target].get('label')
if t_label == 'resource':
t_label = nx_graph.nodes[target].get('resourceType')
elif t_label == 'dummy' or t_label == 'Dummy':
t_label = nx_graph.nodes[target].get('edge_label')
label = s_label.capitalize() + '_to_' + t_label.capitalize()
yield(
attributes.get('id', str(uuid.uuid4())), # Edge ID (if exists, otherwise use nx internal id)
nx_graph.nodes[source].get('unique_id', source),
nx_graph.nodes[target].get('unique_id', target),
label,
attributes # All edge attributes
)
def generate_neo4j_import_script(directory_path="/neo4j_import/", output_file="neo4j-admin-import-call.sh"):
"""
Reads files in a directory and generates a Neo4j import shell script.
Args:
directory_path (str): Path to the directory containing CSV files
output_file (str): Name of the output shell script file
Returns:
str: Path to the generated shell script
"""
# Get all files in the directory
all_files = os.listdir(directory_path)
# Dictionary to store entity types (nodes and relationships)
entity_types = {}
# Find all header files and use them to identify entity types
for filename in all_files:
if '-header.csv' in filename:
entity_name = filename.split('-header.csv')[0]
# Check if it's a relationship (contains "To" and "Association")
is_relationship = ("To" in entity_name and "Association" in entity_name) or "_has_" in entity_name or "Has_" in entity_name
# Store in entity_types dictionary
if is_relationship:
entity_type = "relationships"
else:
entity_type = "nodes"
# Initialize the entity if not already present
if entity_name not in entity_types:
entity_types[entity_name] = {
"type": entity_type,
"header": f"/neo4j_import/{filename}",
"has_parts": False
}
# Check for part files for each entity
for entity_name in entity_types:
# Create pattern to match part files for this entity
part_pattern = f"{entity_name}-part"
# Check if any file matches the pattern
for filename in all_files:
if part_pattern in filename:
entity_types[entity_name]["has_parts"] = True
break
# Generate the import commands
nodes_command = ""
relationships_command = ""
for entity_name, info in entity_types.items():
if info["has_parts"]:
# Create the command string with wildcard for part files
command = f" --{info['type']}=\"{info['header']},/neo4j_import/{entity_name}-part.*\""
# Add to appropriate command string
if info['type'] == "nodes":
nodes_command += command
else: # relationships
relationships_command += command
# Create the shell script content
script_content = """#!/bin/bash
version=$(bin/neo4j-admin --version | cut -d '.' -f 1)
if [[ $version -ge 5 ]]; then
\tbin/neo4j-admin database import full neo4j --delimiter="\\t" --array-delimiter="|" --quote="'" --overwrite-destination=true --skip-bad-relationships=true --skip-duplicate-nodes=true{nodes}{relationships}
else
\tbin/neo4j-admin import --database=neo4j --delimiter="\\t" --array-delimiter="|" --quote="'" --force=true --skip-bad-relationships=true --skip-duplicate-nodes=true{nodes}{relationships}
fi
""".format(nodes=nodes_command, relationships=relationships_command)
# Write the script to file
script_path = os.path.join(directory_path, output_file)
with open(script_path, 'w') as f:
f.write(script_content)
# Make the script executable
os.chmod(script_path, 0o755)
print("Shell import script created", flush=True)
if __name__ == "__main__":
main()
+152
View File
@@ -0,0 +1,152 @@
from biocypher import BioCypher
import networkx as nx
import json
import os
import uuid
from graphCreation import create_graph
from graphCreation.process_references import process_references
from graphCreation.property_convolution import property_convolution
from schema_config_generation import write_automated_schema
from graphCreation.node_typing import set_resource_type
def load_multiple_fhir_bundles(directory_path):
graph = nx.DiGraph()
init = True
# Iterate over all files in the directory
for filename in os.listdir(directory_path):
if filename.endswith('.json'): # Assuming FHIR bundles are in JSON format
file_path = os.path.join(directory_path, filename)
with open(file_path, 'r') as f:
bundle_json = json.load(f)
#fix all strings to to enable ' in neo4j
fixed_quotes = replace_single_quotes(bundle_json)
if init:
#print(bundle_json, filename, graph)
create_graph.json_to_networkx(fixed_quotes, filename, graph)
init = False
else:
create_graph.add_json_to_networkx(fixed_quotes, filename, graph)
print("Imported: ", filename)
return graph
def replace_single_quotes(obj):
if isinstance(obj, str): # If it's a string, replace single quotes
return obj.replace("'", "''")
elif isinstance(obj, dict): # If it's a dictionary, process each key-value pair
return {key: replace_single_quotes(value) for key, value in obj.items()}
elif isinstance(obj, list): # If it's a list, process each item
return [replace_single_quotes(item) for item in obj]
else:
return obj # Leave other data types unchanged
def main():
#get a list of nodes that should be imported
## create networkX and run improvement scripts
print("Creating the graph...", flush=True)
nx_graph = load_multiple_fhir_bundles('./testData/') # 'mockData' for unit test data, 'testData' for Synthea files
print(nx_graph)
print("Reducing references...", flush=True)
process_references(nx_graph)
print(nx_graph)
print("Convolute references...", flush=True)
property_convolution(nx_graph)
print(nx_graph)
print("Generate auto schema...")
write_automated_schema(nx_graph, 'config/automated_schema.yaml', None)
# create Biocypher driver
bc = BioCypher(
biocypher_config_path="config/biocypher_config.yaml",
)
bc.show_ontology_structure()
#BioCypher preperation
## node generator: extract id, label and property dictionary
def node_generator():
for node in nx_graph.nodes():
""" #single qoutes break neo4j import, e.g. 'CHILDREN'S Hospital'
checkDisplay = nx_graph.nodes[node].get('display')
if checkDisplay:
checkDisplay = checkDisplay.replace("'", "''")
nx_graph.nodes[node]['display'] = checkDisplay
#print("------->", nx_graph.nodes[node].get('display'))
checkName = nx_graph.nodes[node].get('name')
if checkName:
checkName = checkName.replace("'", "''")
nx_graph.nodes[node]['name'] = checkName
#print("------->", nx_graph.nodes[node].get('name')) """
label = nx_graph.nodes[node].get('label')
if label == "resource":
label = nx_graph.nodes[node].get('resourceType')
'''
elif label == 'identifier':
label = nx_graph.nodes[node].get('system')
print('/' in label)
if '/' in label:
lastSlash = label.rfind('/') + 1
label = label[lastSlash:] + '-ID'
elif label == 'telecom':
label = nx_graph.nodes[node].get('system')
print('/' in label)
if '/' in label:
lastSlash = label.rfind('/') + 1
label = 'telecom-' + label[lastSlash:]
elif label == 'address':
extension = nx_graph.nodes[node].get('extension_url')
print("EX!: ", extension)
if extension:
lastSlash = extension.rfind('/') + 1
label = label + '-' + extension[lastSlash:]
'''
yield(
nx_graph.nodes[node].get('id', node), #remark: this returns the node id if this attribute exists. otherwise it returns node which equals the identifier that is used by nx
label,
nx_graph.nodes[node] # get properties
)
def edge_generator():
for edge in nx_graph.edges(data = True):
source, target, attributes = edge
s_label = nx_graph.nodes[source].get('label')
if s_label == 'resource':
s_label = nx_graph.nodes[source].get('resourceType')
t_label = nx_graph.nodes[target].get('label')
if t_label == 'resource':
t_label = nx_graph.nodes[target].get('resourceType')
label = s_label + '_to_' + t_label
yield(
attributes.get('id', str(uuid.uuid4())), # Edge ID (if exists, otherwise use nx internal id)
nx_graph.nodes[source].get('id', source),
nx_graph.nodes[target].get('id', target),
label,
attributes # All edge attributes
)
bc.write_nodes(node_generator())
bc.write_edges(edge_generator())
#write the import script
bc.write_import_call()
if __name__ == "__main__":
#print("Called import script. Should run its main function now...")
main()
+8
View File
@@ -0,0 +1,8 @@
// Example initialization script - modify according to your schema
CREATE CONSTRAINT IF NOT EXISTS FOR (n:YourLabel) REQUIRE n.id IS UNIQUE;
CREATE INDEX IF NOT EXISTS FOR (n:YourLabel) ON (n.someProperty);
// Add any other initialization queries here
// For example:
// CREATE CONSTRAINT IF NOT EXISTS FOR (n:Person) REQUIRE n.email IS UNIQUE;
// CREATE INDEX IF NOT EXISTS FOR (n:Product) ON (n.sku);
+377
View File
@@ -0,0 +1,377 @@
#*****************************************************************
# Neo4j configuration
#
# For more details and a complete list of settings, please see
# https://neo4j.com/docs/operations-manual/current/reference/configuration-settings/
#*****************************************************************
# The name of the default database
#initial.dbms.default_database=neo4j
# Paths of directories in the installation.
#server.directories.data=data
#server.directories.plugins=plugins
#server.directories.logs=logs
#server.directories.lib=lib
#server.directories.run=run
#server.directories.licenses=licenses
#server.directories.transaction.logs.root=data/transactions
# This setting constrains all `LOAD CSV` import files to be under the `import` directory. Remove or comment it out to
# allow files to be loaded from anywhere in the filesystem; this introduces possible security problems. See the
# `LOAD CSV` section of the manual for details.
server.directories.import=import
# Whether requests to Neo4j are authenticated.
# To disable authentication, uncomment this line
dbms.security.auth_enabled=false
#********************************************************************
# Memory Settings
#********************************************************************
#
# Memory settings are specified kilobytes with the 'k' suffix, megabytes with
# 'm' and gigabytes with 'g'.
# If Neo4j is running on a dedicated server, then it is generally recommended
# to leave about 2-4 gigabytes for the operating system, give the JVM enough
# heap to hold all your transaction state and query context, and then leave the
# rest for the page cache.
# Java Heap Size: by default the Java heap size is dynamically calculated based
# on available system resources. Uncomment these lines to set specific initial
# and maximum heap size.
#server.memory.heap.initial_size=512m
#server.memory.heap.max_size=512m
# The amount of memory to use for mapping the store files.
# The default page cache memory assumes the machine is dedicated to running
# Neo4j, and is heuristically set to 50% of RAM minus the Java heap size.
#server.memory.pagecache.size=10g
# Limit the amount of memory that all of the running transaction can consume.
# The default value is 70% of the heap size limit.
#dbms.memory.transaction.total.max=256m
# Limit the amount of memory that a single transaction can consume.
# By default there is no limit.
#db.memory.transaction.max=16m
# Transaction state location. It is recommended to use ON_HEAP.
# db.tx_state.memory_allocation=ON_HEAP
#*****************************************************************
# Network connector configuration
#*****************************************************************
# With default configuration Neo4j only accepts local connections.
# To accept non-local connections, uncomment this line:
#server.default_listen_address=0.0.0.0
# You can also choose a specific network interface, and configure a non-default
# port for each connector, by setting their individual listen_address.
# The address at which this server can be reached by its clients. This may be the server's IP address or DNS name, or
# it may be the address of a reverse proxy which sits in front of the server. This setting may be overridden for
# individual connectors below.
#server.default_advertised_address=localhost
# You can also choose a specific advertised hostname or IP address, and
# configure an advertised port for each connector, by setting their
# individual advertised_address.
# By default, encryption is turned off.
# To turn on encryption, an ssl policy for the connector needs to be configured
# Read more in SSL policy section in this file for how to define a SSL policy.
# Bolt connector
server.bolt.enabled=true
#server.bolt.tls_level=DISABLED
#server.bolt.listen_address=:7687
server.bolt.advertised_address=localhost:8081
#maybe has to be changed in production
# HTTP Connector. There can be zero or one HTTP connectors.
server.http.enabled=true
#server.http.listen_address=:7474
#server.http.advertised_address=:7474
# HTTPS Connector. There can be zero or one HTTPS connectors.
server.https.enabled=false
#server.https.listen_address=:7473
#server.https.advertised_address=:7473
# Number of Neo4j worker threads.
#server.threads.worker_count=
#*****************************************************************
# SSL policy configuration
#*****************************************************************
# Each policy is configured under a separate namespace, e.g.
# dbms.ssl.policy.<scope>.*
# <scope> can be any of 'bolt', 'https', 'cluster' or 'backup'
#
# The scope is the name of the component where the policy will be used
# Each component where the use of an ssl policy is desired needs to declare at least one setting of the policy.
# Allowable values are 'bolt', 'https', 'cluster' or 'backup'.
# E.g if bolt and https connectors should use the same policy, the following could be declared
# dbms.ssl.policy.bolt.base_directory=certificates/default
# dbms.ssl.policy.https.base_directory=certificates/default
# However, it's strongly encouraged to not use the same key pair for multiple scopes.
#
# N.B: Note that a connector must be configured to support/require
# SSL/TLS for the policy to actually be utilized.
#
# see: dbms.connector.*.tls_level
# SSL settings (dbms.ssl.policy.<scope>.*)
# .base_directory Base directory for SSL policies paths. All relative paths within the
# SSL configuration will be resolved from the base dir.
#
# .private_key A path to the key file relative to the '.base_directory'.
#
# .private_key_password The password for the private key.
#
# .public_certificate A path to the public certificate file relative to the '.base_directory'.
#
# .trusted_dir A path to a directory containing trusted certificates.
#
# .revoked_dir Path to the directory with Certificate Revocation Lists (CRLs).
#
# .verify_hostname If true, the server will verify the hostname that the client uses to connect with. In order
# for this to work, the server public certificate must have a valid CN and/or matching
# Subject Alternative Names.
#
# .client_auth How the client should be authorized. Possible values are: 'none', 'optional', 'require'.
#
# .tls_versions A comma-separated list of allowed TLS versions. By default only TLSv1.2 is allowed.
#
# .trust_all Setting this to 'true' will ignore the trust truststore, trusting all clients and servers.
# Use of this mode is discouraged. It would offer encryption but no security.
#
# .ciphers A comma-separated list of allowed ciphers. The default ciphers are the defaults of
# the JVM platform.
# Bolt SSL configuration
#dbms.ssl.policy.bolt.enabled=true
#dbms.ssl.policy.bolt.base_directory=certificates/bolt
#dbms.ssl.policy.bolt.private_key=private.key
#dbms.ssl.policy.bolt.public_certificate=public.crt
#dbms.ssl.policy.bolt.client_auth=NONE
# Https SSL configuration
#dbms.ssl.policy.https.enabled=true
#dbms.ssl.policy.https.base_directory=certificates/https
#dbms.ssl.policy.https.private_key=private.key
#dbms.ssl.policy.https.public_certificate=public.crt
#dbms.ssl.policy.https.client_auth=NONE
# Cluster SSL configuration
#dbms.ssl.policy.cluster.enabled=true
#dbms.ssl.policy.cluster.base_directory=certificates/cluster
#dbms.ssl.policy.cluster.private_key=private.key
#dbms.ssl.policy.cluster.public_certificate=public.crt
# Backup SSL configuration
#dbms.ssl.policy.backup.enabled=true
#dbms.ssl.policy.backup.base_directory=certificates/backup
#dbms.ssl.policy.backup.private_key=private.key
#dbms.ssl.policy.backup.public_certificate=public.crt
#*****************************************************************
# Logging configuration
#*****************************************************************
# To enable HTTP logging, uncomment this line
#dbms.logs.http.enabled=true
# To enable GC Logging, uncomment this line
#server.logs.gc.enabled=true
# GC Logging Options
# see https://docs.oracle.com/en/java/javase/11/tools/java.html#GUID-BE93ABDC-999C-4CB5-A88B-1994AAAC74D5
#server.logs.gc.options=-Xlog:gc*,safepoint,age*=trace
# Number of GC logs to keep.
#server.logs.gc.rotation.keep_number=5
# Size of each GC log that is kept.
#server.logs.gc.rotation.size=20m
#*****************************************************************
# Miscellaneous configuration
#*****************************************************************
# Determines if Cypher will allow using file URLs when loading data using
# `LOAD CSV`. Setting this value to `false` will cause Neo4j to fail `LOAD CSV`
# clauses that load data from the file system.
#dbms.security.allow_csv_import_from_file_urls=true
# Value of the Access-Control-Allow-Origin header sent over any HTTP or HTTPS
# connector. This defaults to '*', which allows broadest compatibility. Note
# that any URI provided here limits HTTP/HTTPS access to that URI only.
#dbms.security.http_access_control_allow_origin=*
# Value of the HTTP Strict-Transport-Security (HSTS) response header. This header
# tells browsers that a webpage should only be accessed using HTTPS instead of HTTP.
# It is attached to every HTTPS response. Setting is not set by default so
# 'Strict-Transport-Security' header is not sent. Value is expected to contain
# directives like 'max-age', 'includeSubDomains' and 'preload'.
#dbms.security.http_strict_transport_security=
# Retention policy for transaction logs needed to perform recovery and backups.
#db.tx_log.rotation.retention_policy=2 days
# Whether or not any database on this instance are read_only by default.
# If false, individual databases may be marked as read_only using dbms.database.read_only.
# If true, individual databases may be marked as writable using dbms.databases.writable.
#dbms.databases.default_to_read_only=false
# Comma separated list of JAX-RS packages containing JAX-RS resources, one
# package name for each mountpoint. The listed package names will be loaded
# under the mountpoints specified. Uncomment this line to mount the
# org.neo4j.examples.server.unmanaged.HelloWorldResource.java from
# neo4j-server-examples under /examples/unmanaged, resulting in a final URL of
# http://localhost:7474/examples/unmanaged/helloworld/{nodeId}
#server.unmanaged_extension_classes=org.neo4j.examples.server.unmanaged=/examples/unmanaged
# A comma separated list of procedures and user defined functions that are allowed
# full access to the database through unsupported/insecure internal APIs.
#dbms.security.procedures.unrestricted=my.extensions.example,my.procedures.*
# A comma separated list of procedures to be loaded by default.
# Leaving this unconfigured will load all procedures found.
#dbms.security.procedures.allowlist=apoc.coll.*,apoc.load.*,gds.*
#********************************************************************
# JVM Parameters
#********************************************************************
# G1GC generally strikes a good balance between throughput and tail
# latency, without too much tuning.
server.jvm.additional=-XX:+UseG1GC
# Have common exceptions keep producing stack traces, so they can be
# debugged regardless of how often logs are rotated.
server.jvm.additional=-XX:-OmitStackTraceInFastThrow
# Make sure that `initmemory` is not only allocated, but committed to
# the process, before starting the database. This reduces memory
# fragmentation, increasing the effectiveness of transparent huge
# pages. It also reduces the possibility of seeing performance drop
# due to heap-growing GC events, where a decrease in available page
# cache leads to an increase in mean IO response time.
# Try reducing the heap memory, if this flag degrades performance.
server.jvm.additional=-XX:+AlwaysPreTouch
# Trust that non-static final fields are really final.
# This allows more optimizations and improves overall performance.
# NOTE: Disable this if you use embedded mode, or have extensions or dependencies that may use reflection or
# serialization to change the value of final fields!
server.jvm.additional=-XX:+UnlockExperimentalVMOptions
server.jvm.additional=-XX:+TrustFinalNonStaticFields
# Disable explicit garbage collection, which is occasionally invoked by the JDK itself.
server.jvm.additional=-XX:+DisableExplicitGC
# Restrict size of cached JDK buffers to 1 KB
server.jvm.additional=-Djdk.nio.maxCachedBufferSize=1024
# More efficient buffer allocation in Netty by allowing direct no cleaner buffers.
server.jvm.additional=-Dio.netty.tryReflectionSetAccessible=true
# Exits JVM on the first occurrence of an out-of-memory error. Its preferable to restart VM in case of out of memory errors.
# server.jvm.additional=-XX:+ExitOnOutOfMemoryError
# Expand Diffie Hellman (DH) key size from default 1024 to 2048 for DH-RSA cipher suites used in server TLS handshakes.
# This is to protect the server from any potential passive eavesdropping.
server.jvm.additional=-Djdk.tls.ephemeralDHKeySize=2048
# This mitigates a DDoS vector.
server.jvm.additional=-Djdk.tls.rejectClientInitiatedRenegotiation=true
# Enable remote debugging
#server.jvm.additional=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
# This filter prevents deserialization of arbitrary objects via java object serialization, addressing potential vulnerabilities.
# By default this filter whitelists all neo4j classes, as well as classes from the hazelcast library and the java standard library.
# These defaults should only be modified by expert users!
# For more details (including filter syntax) see: https://openjdk.java.net/jeps/290
#server.jvm.additional=-Djdk.serialFilter=java.**;org.neo4j.**;com.neo4j.**;com.hazelcast.**;net.sf.ehcache.Element;com.sun.proxy.*;org.openjdk.jmh.**;!*
# Increase the default flight recorder stack sampling depth from 64 to 256, to avoid truncating frames when profiling.
server.jvm.additional=-XX:FlightRecorderOptions=stackdepth=256
# Allow profilers to sample between safepoints. Without this, sampling profilers may produce less accurate results.
server.jvm.additional=-XX:+UnlockDiagnosticVMOptions
server.jvm.additional=-XX:+DebugNonSafepoints
# Open modules for neo4j to allow internal access
server.jvm.additional=--add-opens=java.base/java.nio=ALL-UNNAMED
server.jvm.additional=--add-opens=java.base/java.io=ALL-UNNAMED
server.jvm.additional=--add-opens=java.base/sun.nio.ch=ALL-UNNAMED
# Disable logging JMX endpoint.
server.jvm.additional=-Dlog4j2.disable.jmx=true
# Limit JVM metaspace and code cache to allow garbage collection. Used by cypher for code generation and may grow indefinitely unless constrained.
# Useful for memory constrained environments
#server.jvm.additional=-XX:MaxMetaspaceSize=1024m
#server.jvm.additional=-XX:ReservedCodeCacheSize=512m
# Allow big methods to be JIT compiled.
# Useful for big queries and big expressions where cypher code generation can create large methods.
#server.jvm.additional=-XX:-DontCompileHugeMethods
#********************************************************************
# Wrapper Windows NT/2000/XP Service Properties
#********************************************************************
# WARNING - Do not modify any of these properties when an application
# using this configuration file has been installed as a service.
# Please uninstall the service before modifying this section. The
# service can then be reinstalled.
# Name of the service
server.windows_service_name=neo4j
#********************************************************************
# Other Neo4j system properties
#********************************************************************
dbms.security.procedures.unrestricted=apoc.*
db.tx_log.rotation.retention_policy=100M size
server.memory.pagecache.size=512M
server.default_listen_address=0.0.0.0
#### SEMSPECT ####
server.unmanaged_extension_classes=de.derivo.semspect.server.neo4japp.server=/semspect
#dbms.security.http_auth_allowlist=/,/semspect.*
dbms.security.http_auth_allowlist=/,/browser.*,/semspect.*
server.directories.logs=/logs
server.config.strict_validation.enabled=false
dbms.security.auth_enabled=false
## new advertise
server.default_listen_address=0.0.0.0
server.bolt.enabled=true
server.bolt.listen_address=:7687
server.bolt.advertised_address=srvdizsrvmedaxdev.med.uni-greifswald.de:8081
server.bolt.tls_level=REQUIRED
dbms.ssl.policy.bolt.enabled=true
dbms.ssl.policy.bolt.base_directory=certificates/bolt
dbms.ssl.policy.bolt.private_key=private.key
dbms.ssl.policy.bolt.public_certificate=public.crt
dbms.ssl.policy.bolt.client_auth=NONE
server.http.enabled=true
server.http.listen_address=:7474
server.http.advertised_address=srvdizsrvmedaxdev.med.uni-greifswald.de:443
Submodule
+1
Submodule mdm2neo4j added at 3f91b1847d
File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 366 KiB

+210
View File
@@ -0,0 +1,210 @@
{
"resourceType": "Bundle",
"type": "transaction",
"entry": [ {
"fullUrl": "urn:uuid:a7a285c0-4714-dd3c-4837-8719c9b67873",
"resource": {
"resourceType": "Patient",
"id": "a7a285c0-4714-dd3c-4837-8719c9b67873",
"meta": {
"profile": [ "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient" ]
},
"text": {
"status": "generated",
"div": "<div xmlns=\"http://www.w3.org/1999/xhtml\">Generated by <a href=\"https://github.com/synthetichealth/synthea\">Synthea</a>.Version identifier: 3c23908\n . Person seed: -5557164924473669144 Population seed: 1693908535569</div>"
},
"extension": [ {
"url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race",
"extension": [ {
"url": "ombCategory",
"valueCoding": {
"system": "urn:oid:2.16.840.1.113883.6.238",
"code": "2106-3",
"display": "White"
}
}, {
"url": "text",
"valueString": "White"
} ]
}, {
"url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity",
"extension": [ {
"url": "ombCategory",
"valueCoding": {
"system": "urn:oid:2.16.840.1.113883.6.238",
"code": "2186-5",
"display": "Not Hispanic or Latino"
}
}, {
"url": "text",
"valueString": "Not Hispanic or Latino"
} ]
}, {
"url": "http://hl7.org/fhir/StructureDefinition/patient-mothersMaidenName",
"valueString": "Leana211 Sauer652"
}, {
"url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex",
"valueCode": "M"
}, {
"url": "http://hl7.org/fhir/StructureDefinition/patient-birthPlace",
"valueAddress": {
"city": "Quincy",
"state": "Massachusetts",
"country": "US"
}
}, {
"url": "http://synthetichealth.github.io/synthea/disability-adjusted-life-years",
"valueDecimal": 0.0
}, {
"url": "http://synthetichealth.github.io/synthea/quality-adjusted-life-years",
"valueDecimal": 1.0
} ],
"identifier": [
{
"system": "https://github.com/synthetichealth/synthea",
"value": "a7a285c0-4714-dd3c-4837-8719c9b67873"
},
{
"type": {
"coding": [ {
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
"code": "MR",
"display": "Medical Record Number"
} ],
"text": "Medical Record Number"
},
"system": "http://hospital.smarthealthit.org",
"value": "a7a285c0-4714-dd3c-4837-8719c9b67873"
}, {
"type": {
"coding": [ {
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
"code": "SS",
"display": "Social Security Number"
} ],
"text": "Social Security Number"
},
"system": "http://hl7.org/fhir/sid/us-ssn",
"value": "999-89-9528"
} ],
"name": [ {
"use": "official",
"family": "Schoen8",
"given": [ "Johnny786", "Vince741" ]
} ],
"telecom": [ {
"system": "phone",
"value": "555-753-6560",
"use": "home"
} ],
"gender": "male",
"birthDate": "2021-05-22",
"address": [ {
"extension": [ {
"url": "http://hl7.org/fhir/StructureDefinition/geolocation",
"extension": [ {
"url": "latitude",
"valueDecimal": 42.05921178859317
}, {
"url": "longitude",
"valueDecimal": -70.79219595855132
} ]
} ],
"line": [ "463 Rempel Ranch Unit 81" ],
"city": "Pembroke",
"state": "MA",
"postalCode": "00000",
"country": "US"
} ],
"maritalStatus": {
"coding": [ {
"system": "http://terminology.hl7.org/CodeSystem/v3-MaritalStatus",
"code": "S",
"display": "Never Married"
} ],
"text": "Never Married"
},
"multipleBirthBoolean": false,
"communication": [ {
"language": {
"coding": [ {
"system": "urn:ietf:bcp:47",
"code": "en-US",
"display": "English (United States)"
} ],
"text": "English (United States)"
}
} ]
},
"request": {
"method": "POST",
"url": "Patient"
}
}, {
"fullUrl": "urn:uuid:0eb53bda-2881-5e8e-3597-87a9430af96a",
"resource": {
"resourceType": "Encounter",
"id": "0eb53bda-2881-5e8e-3597-87a9430af96a",
"meta": {
"profile": [ "http://hl7.org/fhir/us/core/StructureDefinition/us-core-encounter" ]
},
"identifier": [ {
"use": "official",
"system": "https://github.com/synthetichealth/synthea",
"value": "0eb53bda-2881-5e8e-3597-87a9430af96a"
} ],
"status": "finished",
"class": {
"system": "http://terminology.hl7.org/CodeSystem/v3-ActCode",
"code": "AMB"
},
"type": [ {
"coding": [ {
"system": "http://snomed.info/sct",
"code": "410620009",
"display": "Well child visit (procedure)"
} ],
"text": "Well child visit (procedure)"
} ],
"subject": {
"reference": "urn:uuid:a7a285c0-4714-dd3c-4837-8719c9b67873",
"display": "Johnny786 Vince741 Schoen8"
},
"participant": [ {
"type": [ {
"coding": [ {
"system": "http://terminology.hl7.org/CodeSystem/v3-ParticipationType",
"code": "PPRF",
"display": "primary performer"
} ],
"text": "primary performer"
} ],
"period": {
"start": "2021-05-22T00:13:45+02:00",
"end": "2021-05-22T00:28:45+02:00"
},
"individual": {
"reference": "Practitioner?identifier=http://hl7.org/fhir/sid/us-npi|9999942599",
"display": "Dr. Regenia619 Bosco882"
}
} ],
"period": {
"start": "2021-05-22T00:13:45+02:00",
"end": "2021-05-22T00:28:45+02:00"
},
"location": [ {
"location": {
"reference": "Location?identifier=https://github.com/synthetichealth/synthea|6e3d04a3-9064-33e4-b8b5-63bb468d7629",
"display": "UNITED MEDICAL CARE LLC"
}
} ],
"serviceProvider": {
"reference": "Organization?identifier=https://github.com/synthetichealth/synthea|4e56c7ec-99e5-3023-8e4f-95ad18a03f06",
"display": "UNITED MEDICAL CARE LLC"
}
},
"request": {
"method": "POST",
"url": "Encounter"
}
}]}
+98
View File
@@ -0,0 +1,98 @@
{
"resourceType": "Bundle",
"type": "batch",
"entry": [ {
"fullUrl": "urn:uuid:4e56c7ec-99e5-3023-8e4f-95ad18a03f06",
"resource": {
"resourceType": "Organization",
"id": "4e56c7ec-99e5-3023-8e4f-95ad18a03f06",
"meta": {
"profile": [ "http://hl7.org/fhir/us/core/StructureDefinition/us-core-organization" ]
},
"extension": [ {
"url": "http://synthetichealth.github.io/synthea/utilization-encounters-extension",
"valueInteger": 9
}, {
"url": "http://synthetichealth.github.io/synthea/utilization-procedures-extension",
"valueInteger": 2
}, {
"url": "http://synthetichealth.github.io/synthea/utilization-labs-extension",
"valueInteger": 1
}, {
"url": "http://synthetichealth.github.io/synthea/utilization-prescriptions-extension",
"valueInteger": 3
} ],
"identifier": [ {
"system": "https://github.com/synthetichealth/synthea",
"value": "4e56c7ec-99e5-3023-8e4f-95ad18a03f06"
} ],
"active": true,
"type": [ {
"coding": [ {
"system": "http://terminology.hl7.org/CodeSystem/organization-type",
"code": "prov",
"display": "Healthcare Provider"
} ],
"text": "Healthcare Provider"
} ],
"name": "UNITED MEDICAL CARE LLC",
"telecom": [ {
"system": "phone",
"value": "5089715500"
} ],
"address": [ {
"line": [ "28 RIVERSIDE DR STE 101" ],
"city": "PEMBROKE",
"state": "MA",
"postalCode": "023594947",
"country": "US"
} ]
},
"request": {
"method": "POST",
"url": "Organization",
"ifNoneExist": "identifier=https://github.com/synthetichealth/synthea|4e56c7ec-99e5-3023-8e4f-95ad18a03f06"
}
}, {
"fullUrl": "urn:uuid:6e3d04a3-9064-33e4-b8b5-63bb468d7629",
"resource": {
"resourceType": "Location",
"id": "6e3d04a3-9064-33e4-b8b5-63bb468d7629",
"meta": {
"profile": [ "http://hl7.org/fhir/us/core/StructureDefinition/us-core-location" ]
},
"identifier": [ {
"system": "https://github.com/synthetichealth/synthea",
"value": "6e3d04a3-9064-33e4-b8b5-63bb468d7629"
} ],
"status": "active",
"name": "UNITED MEDICAL CARE LLC",
"telecom": [ {
"system": "phone",
"value": "5089715500"
} ],
"address": {
"line": [ "28 RIVERSIDE DR STE 101" ],
"city": "PEMBROKE",
"state": "MA",
"postalCode": "023594947",
"country": "US"
},
"position": {
"longitude": -70.77534154695786,
"latitude": 42.11004715
},
"managingOrganization": {
"identifier": {
"system": "https://github.com/synthetichealth/synthea",
"value": "4e56c7ec-99e5-3023-8e4f-95ad18a03f06"
},
"display": "UNITED MEDICAL CARE LLC"
}
},
"request": {
"method": "POST",
"url": "Location",
"ifNoneExist": "identifier=https://github.com/synthetichealth/synthea|6e3d04a3-9064-33e4-b8b5-63bb468d7629"
}
}]}
@@ -0,0 +1,50 @@
{
"resourceType": "Bundle",
"type": "batch",
"entry": [ {
"fullUrl": "urn:uuid:0368f101-0e65-3251-a809-566ebd6b2c2a",
"resource": {
"resourceType": "Practitioner",
"id": "0368f101-0e65-3251-a809-566ebd6b2c2a",
"meta": {
"profile": [ "http://hl7.org/fhir/us/core/StructureDefinition/us-core-practitioner" ]
},
"extension": [ {
"url": "http://synthetichealth.github.io/synthea/utilization-encounters-extension",
"valueInteger": 9
} ],
"identifier": [ {
"system": "http://hl7.org/fhir/sid/us-npi",
"value": "9999942599"
} ],
"active": true,
"name": [ {
"family": "Bosco882",
"given": [ "Regenia619" ],
"prefix": [ "Dr." ]
} ],
"telecom": [ {
"extension": [ {
"url": "http://hl7.org/fhir/us/core/StructureDefinition/us-core-direct",
"valueBoolean": true
} ],
"system": "email",
"value": "Regenia619.Bosco882@example.com",
"use": "work"
} ],
"address": [ {
"line": [ "28 RIVERSIDE DR STE 101" ],
"city": "PEMBROKE",
"state": "MA",
"postalCode": "023594947",
"country": "US"
} ],
"gender": "female"
},
"request": {
"method": "POST",
"url": "Practitioner",
"ifNoneExist": "identifier=http://hl7.org/fhir/sid/us-npi|9999942599"
}
}]}
+63
View File
@@ -0,0 +1,63 @@
import sys
import os
from biocypher import BioCypher
from schema_config_generation import write_automated_schema
# Add submodule to path BEFORE any other imports
sys.path.insert(0, '/app/mdm2neo4j/src') # Absolute path in Docker
# OR
#sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'mdm2neo4j')) # Relative path
# Now your imports should work
import mdm2neo4j.src.xml_processor.xml_processor as xp
from import_fhir_to_nx_diGraph import generate_neo4j_import_script
from import_fhir_to_nx_diGraph import load_multiple_fhir_patients
#import mdm2neo4j.src.xml_processor.xml_processor as xp
def main():
# create Biocypher driver
bc = BioCypher(
biocypher_config_path="config/biocypher_config.yaml",
)
#bc.show_ontology_structure() #very extensive
#BioCypher preperation
## create networkX and run improvement scripts
adapter_mode = 0
xml_file_path = "./odm_models"
write_automated_schema(None, 'config/automated_schema.yaml', ['config/manual_schema_config.yaml', 'config/ODM_schema.yaml'])
if(adapter_mode == 0 or adapter_mode == -1):
n_patients = int(os.getenv('NUMBER_OF_PATIENTS'))
print("--- load ", n_patients, " fhir patients ---")
load_multiple_fhir_patients(n_patients)
if(adapter_mode == 1 or adapter_mode == -1):
#bc.show_ontology_structure() #very extensive
bc.write_nodes(xp.parse_xml_generate_nodes(xml_file_path))
bc.write_edges(xp.parse_xml_generate_edges(xml_file_path))
print("CREATING THE SCRIPT", flush=True)
generate_neo4j_import_script()
with open('/neo4j_import/shell-scipt-complete', 'w') as f:
f.write('Import completed successfully')
print("FHIR import completed successfully")
if __name__ == "__main__":
main()
+94
View File
@@ -0,0 +1,94 @@
@startuml Server Architecture
left to right direction
!define RECTANGLE class
' Define styles
skinparam rectangle {
BackgroundColor<<zone>> LightBlue
BorderColor<<zone>> Navy
BackgroundColor<<server>> LightGreen
BorderColor<<server>> DarkGreen
BackgroundColor<<admin>> LightYellow
BorderColor<<admin>> Orange
BackgroundColor<<client>> LightPink
BorderColor<<client>> Red
BackgroundColor<<database>> LightCyan
BorderColor<<database>> DarkCyan
BackgroundColor<<component>> Wheat
BorderColor<<component>> Brown
}
' Zone 2 container
rectangle "Zone 2" <<zone>> {
' Admin DIZ (provider)
actor "Admin DIZ\n(Provider)" <<admin>> as AdminD
' BLAZE Server
rectangle "BLAZE Server" <<server>> as BlazeServer {
note "FHIR data source for\nthe pipeline" as n1
}
' Server M with internal components
rectangle "Server MeDaX" <<server>> as ServerM {
' Web Interface
rectangle "Web Interface" <<component>> as WebInterface
' Pipeline components
rectangle "Pipeline" <<component>> as Pipeline {
rectangle "Python Scripts" <<component>> as PythonScripts
collections "Input Files" <<files>> as InputFiles
note top of PythonScripts : 1. Python scripts fetch data\n2. Create input files\n3. Load into GDB
' Graph Database
database "Graph Database\n(GDB)" <<database>> as GraphDB
}
}
rectangle WTSDIZ <<client>> as DIZclient
}
' Admin MeDaX
rectangle "MeDaX Admins" <<admin>> as AdminGroup {
actor "Admin 1" as Admin1
actor "Admin 2" as Admin2
}
' Clients
rectangle "Clients" <<client>> as Clients {
rectangle "Clients 1 - MeDaX" as Client1
rectangle "Clients 2 - Allgemeinmedizin" as Client2
}
' Hidden alignment helpers
BlazeServer -[hidden]right- ServerM
BlazeServer -[hidden]down- AdminD
' Pipeline flow within Server M
PythonScripts -down-> InputFiles : creates
InputFiles -down-> GraphDB : loads into
' BLAZE Server connection
PythonScripts -up-> BlazeServer : requests data
' Web Interface connection
WebInterface -up-> GraphDB : connected to
' External relationships
AdminD -up-> ServerM : manages, secures & installs
AdminGroup -up-> DIZclient : access via VPN
DIZclient -left-> ServerM : ssh connection
Clients -up-> DIZclient : access via VPN
DIZclient -left-> WebInterface : access
@enduml
+143
View File
@@ -0,0 +1,143 @@
[project]
name = "medax-pipeline"
version = "1.0.1"
description = "An ETL pipeline to transfer FHIR data into a graph database"
authors = [
"Ilya Mazien",
"Tom Gebhardt",
"Lea Michaelis",
"Ron Henkel",
"Benjamin Winter",
"Dagmar Waltemath",
"Judith Wodke"
]
license = "MIT"
readme = "README.md"
requires-python = ">=3.10"
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"Intended Audience :: Science/Research",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Natural Language :: English",
"Topic :: Scientific/Engineering :: Medical-Informatics",
]
dependencies = [
"pyyaml>=5.0",
"biocypher>=0.16.0,<1.0.0",
"more-itertools",
"appdirs",
"treelib==1.6.4",
"rdflib>=6.2.0,<7.0.0",
"networkx>=3.0,<4.0",
"stringcase>=1.2.0,<2.0.0",
"neo4j-utils==0.0.7",
"pandas>=2.0.1,<3.0.0",
"pooch>=1.7.0,<2.0.0",
"tqdm>=4.65.0,<5.0.0",
"requests>=2.31.0,<3.0.0",
"python-dotenv>=1.0.0,<2.0.0",
"beautifulsoup4>=4.12.3,<5.0.0",
"lxml>=5.1.0,<6.0.0",
]
[project.urls]
Homepage = "https://www.medizin.uni-greifswald.de/medizininformatik/research/current-projects/medax/"
Repository = "https://git.uni-greifswald.de/MILA_public/medax_pipeline"
# biocypher is pulled from a feature branch via uv's source override
[tool.uv.sources]
#biocypher = { git = "https://github.com/biocypher/biocypher.git", branch = "feat/dynamic-node-property-headers" } #enable this to test the feature branch again
[tool.uv]
package = false
# Dev dependencies (PEP 735 dependency groups, natively understood by uv)
[dependency-groups]
dev = [
"sphinx>=5.0.0",
"sphinx-design>=0.3.0,<0.4.0",
"sphinx-rtd-theme>=1.0.0",
"sphinx-last-updated-by-git>=0.3",
"sphinx-autodoc-typehints>=1.18.0",
"myst-parser>=0.18.0,<0.19.0",
"yapf>=0.32.0,<0.33.0",
"pytest>=6.0",
"tox>=3.20.1",
"pre-commit>=2.17.0",
"bump2version",
"coverage>=6.0",
"pytest-cov>=3.0.0,<4.0.0",
"hypothesis>=6.50.1,<7.0.0",
"isort>=5.10.1,<6.0.0",
"ipython>=8.7.0,<9.0.0",
"ipykernel>=6.23.1,<7.0.0",
"sphinxext-opengraph>=0.8.2,<0.9.0",
"coverage-badge>=1.1.0,<2.0.0",
"nbsphinx>=0.9.2,<0.10.0",
"black>=23.9.1,<24.0.0",
"flake8>=6.1.0,<7.0.0",
]
# --- Tool configs below are build-tool-agnostic and carried over unchanged ---
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "--ignore=mdm2neo4j"
log_cli = true
log_level = "INFO"
markers = [
"requires_neo4j: Requires connection to a Neo4j server",
"requires_postgresql: Requires connection to a PostgreSQL server",
"inject_driver_args(driver_args): Arguments for the Driver",
]
[tool.black]
line-length = 80
target-version = ['py310']
include = '\.pyi?$'
exclude = '''
(
/(
\.eggs
| \.git
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| _build
| buck-out
| build
| dist
)/
)
'''
[tool.isort]
from_first = true
line_length = 80
multi_line_output = 3
include_trailing_comma = true
use_parentheses = true
known_num = "numpy,pandas"
sections = "FUTURE,STDLIB,THIRDPARTY,NUM,FIRSTPARTY,LOCALFOLDER"
no_lines_before = "LOCALFOLDER"
balanced_wrapping = true
force_grid_wrap = 0
length_sort = "1"
indent = " "
profile = "black"
[tool.flake8]
ignore = ["E203", "D200", "D202", "D401", "D105", "W504"]
per-file-ignores = [
"docs/source/conf.py:D100",
"tests/*:D100,D101,D102",
"*/__init__.py:F401",
]
max-line-length = 80
count = true
+105
View File
@@ -0,0 +1,105 @@
"""
Reproduction: vanilla BioCypher corrupts already-written part files when a later
batch of the same node type introduces a new property.
Run:
pip install biocypher
python reproduce_header_misalignment.py
Tested against BioCypher 0.15.1. Self-contained: writes its own config files
into a temp dir, needs no Neo4j (offline CSV writer only).
Background
----------
MeDaX ingests data in successive batches and extends the schema on the fly:
when a node/edge type or attribute first appears, it is added to the schema so
no information is dropped. BioCypher's Neo4j batch writer rewrites the *header*
file on every write_nodes() call, but leaves the *part files already on disk*
untouched. As soon as a new attribute widens/reorders the header, the rows in
earlier part files no longer line up with it -> silent column misalignment on
neo4j-admin import.
Exact location upstream:
biocypher/output/write/graph/_neo4j.py
_Neo4jBatchWriter._write_node_headers() (and ._write_edge_headers())
-> the `if os.path.exists(header_path): logger.warning("... Overwriting.")`
branch rewrites the header but never reconciles existing -part*.csv files.
"""
import glob
import os
import sys
import tempfile
from biocypher import BioCypher
workdir = tempfile.mkdtemp(prefix="bc_repro_")
os.chdir(workdir)
with open("schema_config.yaml", "w") as fh:
fh.write(
"patient:\n"
" represented_as: node\n"
" input_label: patient\n"
" is_a: entity\n" # no fixed `properties:` -> inferred from data per batch
)
with open("biocypher_config.yaml", "w") as fh:
fh.write(
"biocypher:\n"
" dbms: neo4j\n"
" offline: true\n"
" strict_mode: false\n"
" schema_config_path: schema_config.yaml\n"
" head_ontology:\n"
" url: https://raw.githubusercontent.com/biolink/biolink-model/v3.2.1/biolink-model.owl.ttl\n"
" root_node: entity\n"
)
bc = BioCypher(
biocypher_config_path="biocypher_config.yaml",
schema_config_path="schema_config.yaml",
output_directory="out",
)
# Batch 1: patients described by {name, age}
bc.write_nodes(
[
("p1", "patient", {"name": "Alice", "age": 30}),
("p2", "patient", {"name": "Bob", "age": 41}),
],
force=True,
)
# Batch 2: same node type, now with an extra `diagnosis` attribute.
bc.write_nodes(
[("p3", "patient", {"name": "Carol", "age": 52, "diagnosis": "E11"})],
force=True,
)
# --- Inspect what ended up on disk -----------------------------------------
header_path = "out/Patient-header.csv"
header_cols = open(header_path).read().strip().split(";")
n_header = len(header_cols)
print("HEADER :", header_cols, f"({n_header} columns)")
ok = True
for part in sorted(glob.glob("out/Patient-part*.csv")):
for row in open(part).read().splitlines():
n_row = len(row.split(";"))
flag = "" if n_row == n_header else " <-- MISALIGNED"
if n_row != n_header:
ok = False
print(f"{os.path.basename(part)}: {n_row} columns{flag} {row}")
print()
if ok:
print("PASS: every part file matches the header.")
sys.exit(0)
else:
print(
"FAIL: a part file written before the schema grew no longer matches the\n"
" header. On import, its values land under the wrong columns\n"
" (e.g. the node id ends up under `diagnosis`, :LABEL ends up empty)."
)
sys.exit(1)
+243
View File
@@ -0,0 +1,243 @@
#!/usr/bin/env python
# coding: utf-8
from pathlib import Path
import yaml
from collections import defaultdict
#extract all node types and generate basic yaml config part for nodes
def write_automated_schema(graph, file_path, manual_schema_path):
schema_data = {
'nodes': {},
'edges': {}
}
if Path(file_path).exists():
print("-- Using existing schema --")
schema_data = load_manual_schema(file_path)
elif isinstance(manual_schema_path, list):
print("-- Using two schema files --")
schema_data = schema_merger(manual_schema_path[0], manual_schema_path[1])
elif manual_schema_path:
print("-- Using the manual schema --")
schema_data = load_manual_schema(manual_schema_path)
if graph is not None:
for node in graph.nodes():
label = graph.nodes[node].get('label')
if label == 'resource':
label = graph.nodes[node].get('resourceType')
label = label.capitalize()
if label not in schema_data['nodes']:
schema_data['nodes'][label] = {}
if 'properties' not in schema_data['nodes'][label] or schema_data['nodes'][label]['properties'] is None:
schema_data['nodes'][label]['properties'] = {}
for k in graph.nodes[node].keys():
schema_data['nodes'][label]['properties'][k] = 'str'
#schema_data['nodes'][label]['properties'].update(graph.nodes[node].keys())
file=open(file_path, 'w')
for n in schema_data['nodes']:
temp = n+':\n'
if 'is_a' in schema_data['nodes'][n]:
is_a_value = schema_data['nodes'][n]['is_a']
temp += ' is_a: ' + (', '.join(is_a_value) if isinstance(is_a_value, list) else str(is_a_value)) + '\n'
else:
temp += ' is_a: named thing\n'
if 'represented_as' in schema_data['nodes'][n]:
represented_as_value = schema_data['nodes'][n]['represented_as']
temp += ' represented_as: ' + (', '.join(represented_as_value) if isinstance(represented_as_value, list) else str(represented_as_value)) + '\n'
else:
temp += ' represented_as: node\n'
if 'label_in_input' in schema_data['nodes'][n]:
label_in_input_value = schema_data['nodes'][n]['label_in_input']
temp += ' label_in_input: ' + (', '.join(label_in_input_value) if isinstance(label_in_input_value, list) else str(label_in_input_value)) + '\n'
if 'preferred_id' in schema_data['nodes'][n]:
preferred_id_value = schema_data['nodes'][n]['preferred_id']
temp += ' preferred_id: ' + (', '.join(preferred_id_value) if isinstance(preferred_id_value, list) else str(preferred_id_value)) + '\n'
else:
temp += ' preferred_id: fhir_id\n'
temp += ' label_in_input: ' + n + '\n'
temp += ' properties:\n'
# get property values from schema_data if exists
#print("---------->", str(schema_data['nodes'][n]))
if schema_data['nodes'][n]['properties'] is not None:
for p_key in schema_data['nodes'][n]['properties']:
prop_value = schema_data['nodes'][n]['properties'][p_key]
temp += ' ' + p_key + ': ' + (', '.join(prop_value) if isinstance(prop_value, list) else str(prop_value)) + '\n'
#elif schema_data['nodes']['properties']:
#print("----> ", schema_data['nodes']['properties'])
""" else:
for attr in schema_data['nodes'][n]:
temp += ' ' + attr + ': str\n' """
temp += '\n'
file.write(temp)
file.write('\n')
#extract all relationship types and generate basic yaml config part for relationships
#if not edgeTypes: edgeTypes = set()
if graph is not None:
for u, v, a in graph.edges(data=True):
source_label = graph.nodes[u].get('label')
target_label = graph.nodes[v].get('label')
if source_label == 'resource':
source_label = graph.nodes[u].get('resourceType', str(u))
elif source_label == 'dummy' or source_label == 'Dummy':
source_label = graph.nodes[u].get('edge_label', str(u))
if target_label == 'resource':
target_label = graph.nodes[v].get('resourceType', str(v))
elif target_label == 'dummy' or target_label == 'Dummy':
target_label = graph.nodes[v].get('edge_label', str(v))
source_label = source_label.capitalize()
target_label = target_label.capitalize()
CONST_ASSOCIATION = ' association'
if source_label + ' to ' + target_label + CONST_ASSOCIATION in schema_data['edges']:
# add missing attributes
continue
elif source_label + ' derived from ' + target_label + CONST_ASSOCIATION in schema_data['edges']:
continue
elif source_label + ' has member ' + target_label + CONST_ASSOCIATION in schema_data['edges']:
continue
elif source_label + ' reasoned by ' + target_label + CONST_ASSOCIATION in schema_data['edges']:
continue
elif source_label + ' is ' + target_label + CONST_ASSOCIATION in schema_data['edges']:
continue
else:
schema_data['edges'][source_label + ' to ' + target_label + ' association'] = {
'is_a': 'association',
'represented_as': 'edge',
'label_in_input': source_label + '_to_' + target_label,
'properties': a
}
for label in schema_data['edges']:
temp = '' + label + ':\n'
for key in schema_data['edges'][label]:
if key == 'properties':
if schema_data['edges'][label][key] is not None:
temp += ' properties:\n'
for prop in schema_data['edges'][label][key]:
prop_value = schema_data['edges'][label][key][prop]
temp += ' ' + prop + ': ' + (', '.join(prop_value) if isinstance(prop_value, list) else str(prop_value)) + '\n'
else:
field_value = schema_data['edges'][label][key]
temp += ' ' + key + ': ' + (', '.join(field_value) if isinstance(field_value, list) else str(field_value)) + '\n'
temp += '\n'
file.write(temp)
file.close()
def load_manual_schema(path):
schema_data = {
'nodes': {},
'edges': {}
}
edgeTypes = set()
with open(path, 'r') as file:
# Load YAML with comments stripped
data = yaml.safe_load(file)
for label, attrs in data.items():
cLabel = label#.capitalize() # less n, e and p with capitalize
if label != 'Title':
if attrs["represented_as"] == 'node':
if not hasattr(schema_data['nodes'], cLabel):
schema_data['nodes'][cLabel] = set()
#assuming uniqueness in schema file here. If the same node type exits twice, it will be overwritten.
schema_data['nodes'][cLabel] = attrs
#for a in attrs:
#print(v)
""" for k, v in attrs:
if not k == ''
schema_data['nodes'][label][k] = v """
else:
if not hasattr(schema_data['edges'], cLabel):
schema_data['edges'][cLabel] = set()
#assuming uniqueness in schema file here. If the same node type exits twice, it will be overwritten.
schema_data['edges'][cLabel] = attrs
return schema_data
def schema_merger(schema_path1, schema_path2):
schema1 = load_manual_schema(schema_path1)
schema2 = load_manual_schema(schema_path2)
merged = {
'nodes': {**schema1['nodes'], **schema2['nodes']},
'edges': {**schema1['edges'], **schema2['edges']}
}
return merged
def schema_diff_report(manual_path, auto_path):
#manual_path = "config/manual_schema_config.yaml"
#auto_path = "config/automated_schema.yaml"
man_schema = load_manual_schema(manual_path)
automated_schema = load_manual_schema(auto_path)
print("\nNODES:\n")
for label, attributes in automated_schema['nodes'].items():
if 'nodes' in man_schema and label in man_schema['nodes']: # node already exists
print("Found in man schema: ", label)
for prop, val in attributes.items():
if prop in man_schema['nodes'][label]: # property exists
if isinstance(val, dict):
for attr, attr_val in val.items():
if attr not in man_schema['nodes'][label][prop]:
print("Added the property " + attr + " to " + prop + " of " + label)
elif attr_val != man_schema['nodes'][label][prop][attr]:
print("Updated property value of " + label + "." + prop + "." + attr + "to: " + attr_val)
elif val != man_schema['nodes'][label][prop]: # property value is not the same
print("Updated property value of " + label + "." + prop + " to: " + val)
else:
print(label + "." + prop + " has not changed")
else:
print(label + ": added property " + prop + ". Value: " + val)
else: # node not found in manual schema
print('Resource type added: ' + label)
print("\nEDGES:\n")
for label, attributes in automated_schema['edges'].items():
if 'edges' in man_schema and label in man_schema['edges']: # node already exists
print("Found in man schema: ", label)
for prop, val in attributes.items():
if prop in man_schema['edges'][label]: # property exists
if isinstance(val, dict):
for attr, attr_val in val.items():
if attr not in man_schema['edges'][label][prop]:
print("Added the property " + attr + " to " + prop)
elif attr_val != man_schema['edges'][label][prop][attr]:
print("Updated property value of " + label + "." + prop + "." + attr + "to: " + attr_val)
elif val != man_schema['edges'][label][prop]: # property value is not the same
print("Updated property value of " + label + "." + prop + " to: " + val)
else:
print(label + "." + prop + " has not changed")
else:
print(label + ": added property " + prop + ". Value: " + val)
else: # node not found in manual schema
print('Resource type added: ' + label)
+250
View File
@@ -0,0 +1,250 @@
52466995
52467955
52466558
5
13
14
22
30
37
57
110
118
134
170
177
id1568758349858
233
567
768
1006
1009
1021
1029
1059
1091
1190
1194
1200
1201
1203
1208
1222
1316
1526
1654
2000
patient
24739
25420
30163
30358
31678
38100
39254
40052
40625
42024
45276
45280
49006
50427
123a
example
cf-1572285491892
51779
52865
53254
53373
55190
55193
55810
55812
59530
69050
76107
141547
141549
206342
206686
221706
221709
257273
Patient1
pat1
pat2
579296
579367
581516
591093
591164
591221
591229
591245
591252
591253
591264
591286
591309
591352
591360
591372
591378
591427
591626
591645
591653
591661
591695
591702
591723
591727
591744
591788
591829
591841
591874
591956
591975
592014
592030
592215
592228
592274
592275
592294
592295
592318
592350
592353
592473
592474
592485
592502
592529
592530
592580
592621
592733
592736
592738
592740
592741
592760
592761
592766
592773
592808
592809
592817
592824
592841
592845
592911
592912
592913
592922
592940
592943
592944
592955
592956
592995
593004
593043
593160
593165
593166
593170
593171
593178
593186
593190
593210
593218
593271
593321
593380
594843
595184
595251
595262
595271
595280
595284
595606
596328
596336
596341
596357
596380
596388
596407
596438
596439
596467
596490
596492
596495
596496
596498
596500
596505
596506
596510
596513
596517
596518
596520
596522
596544
596545
596546
596547
596555
596557
596561
596562
596563
596568
596570
596571
596574
596575
596599
596615
596634
596638
596644
596805
596813
596857
597041
597063
597064
597065
597067
597068
597069
597083
597110
597118
597172
597176
597178
597226
597291
597296
597305
597306
597309
597395
597521
597879
597991
599548
602313
611763
618626
618670
618750
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,959 @@
{
"resourceType": "Bundle",
"type": "transaction",
"entry": [ {
"fullUrl": "Patient/UKE-0013",
"resource": {
"resourceType": "Patient",
"id": "UKE-0013",
"meta": {
"profile": [ "https://www.medizininformatik-initiative.de/fhir/core/modul-person/StructureDefinition/Patient" ]
},
"identifier": [ {
"use": "usual",
"type": {
"coding": [ {
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
"code": "MR"
} ]
},
"system": "https://UKE.de/pid",
"value": "UKE-0013"
} ],
"name": [ {
"use": "official",
"family": "l",
"given": [ "i" ]
} ],
"gender": "male",
"birthDate": "1943-01-01",
"address": [ {
"extension": [ {
"url": "http://terminology.hl7.org/CodeSystem/data-absent-reason",
"valueCode": "unknown"
} ]
} ],
"generalPractitioner": [ {
"display": "Barmer"
} ]
},
"request": {
"method": "PUT",
"url": "Patient/UKE-0013"
}
}, {
"fullUrl": "Encounter/UKE-0013-E-1",
"resource": {
"resourceType": "Encounter",
"id": "UKE-0013-E-1",
"meta": {
"profile": [ "https://www.medizininformatik-initiative.de/fhir/core/modul-fall/StructureDefinition/KontaktGesundheitseinrichtung" ]
},
"identifier": [ {
"type": {
"coding": [ {
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
"code": "VN"
} ]
},
"_system": {
"extension": [ {
"url": "http://terminology.hl7.org/CodeSystem/data-absent-reason",
"valueCode": "unknown"
} ]
},
"value": "UKE-0013-E-1",
"assigner": {
"identifier": {
"system": "https://www.medizininformatik-initiative.de/fhir/core/NamingSystem/org-identifier",
"value": "UKE"
}
}
} ],
"status": "finished",
"class": {
"system": "http://terminology.hl7.org/CodeSystem/v3-ActCode",
"code": "IMP",
"display": "inpatient encounter"
},
"type": [ {
"coding": [ {
"code": "einrichtungskontakt",
"display": "Einrichtungskontakt"
} ]
} ],
"subject": {
"reference": "Patient/UKE-0013"
},
"period": {
"start": "2020-06-13T00:00:00+02:00",
"end": "2020-06-18T00:00:00+02:00"
},
"diagnosis": [ {
"condition": {
"reference": "Condition/UKE-0013-E-1-D-1"
},
"use": {
"coding": [ {
"system": "http://terminology.hl7.org/CodeSystem/diagnosis-role",
"code": "AD",
"display": "Admission diagnosis"
} ]
}
} ]
},
"request": {
"method": "PUT",
"url": "Encounter/UKE-0013-E-1"
}
}, {
"fullUrl": "Encounter/UKE-0013-E-1-A-1",
"resource": {
"resourceType": "Encounter",
"id": "UKE-0013-E-1-A-1",
"meta": {
"profile": [ "https://www.medizininformatik-initiative.de/fhir/core/modul-fall/StructureDefinition/KontaktGesundheitseinrichtung" ]
},
"status": "finished",
"class": {
"extension": [ {
"url": "http://terminology.hl7.org/CodeSystem/data-absent-reason",
"valueCode": "unknown"
} ]
},
"type": [ {
"coding": [ {
"code": "abteilungskontakt",
"display": "Abteilungskontakt"
} ]
} ],
"serviceType": {
"coding": [ {
"system": "http://fhir.de/CodeSystem/dkgev/Fachabteilungsschluessel",
"code": "0300",
"display": "Kardiologie"
} ]
},
"subject": {
"reference": "Patient/UKE-0013"
},
"period": {
"start": "2020-06-13T00:00:00+02:00",
"end": "2020-06-18T00:00:00+02:00"
},
"diagnosis": [ {
"condition": {
"_reference": {
"extension": [ {
"url": "http://terminology.hl7.org/CodeSystem/data-absent-reason",
"valueCode": "unknown"
} ]
}
}
} ],
"partOf": {
"reference": "Encounter/UKE-0013-E-1"
}
},
"request": {
"method": "PUT",
"url": "Encounter/UKE-0013-E-1-A-1"
}
}, {
"fullUrl": "Condition/UKE-0013-E-1-D-1",
"resource": {
"resourceType": "Condition",
"id": "UKE-0013-E-1-D-1",
"meta": {
"profile": [ "https://www.medizininformatik-initiative.de/fhir/core/modul-diagnose/StructureDefinition/Diagnose" ]
},
"identifier": [ {
"value": "UKE-0013-E-1-D-1"
} ],
"code": {
"coding": [ {
"system": "http://fhir.de/CodeSystem/bfarm/icd-10-gm",
"version": "2020",
"code": "I25.22"
} ],
"text": "Myokardinfarkt in Vergangenheit"
},
"subject": {
"reference": "Patient/UKE-0013"
},
"recordedDate": "2020-06-13T00:00:00+02:00"
},
"request": {
"method": "PUT",
"url": "Condition/UKE-0013-E-1-D-1"
}
}, {
"fullUrl": "Medication/Medication-154102257",
"resource": {
"resourceType": "Medication",
"id": "Medication-154102257",
"meta": {
"profile": [ "https://www.medizininformatik-initiative.de/fhir/core/modul-medikation/StructureDefinition/Medication" ]
},
"identifier": [ {
"value": "Medication-154102257"
} ],
"code": {
"coding": [ {
"system": "http://fhir.de/CodeSystem/ifa/pzn",
"code": "05484267"
}, {
"system": "http://fhir.de/CodeSystem/bfarm/atc",
"code": "L03AA02"
} ],
"text": "Filgastrim"
},
"ingredient": [ {
"itemCodeableConcept": {
"coding": [ {
"system": "http://fhir.de/CodeSystem/ask",
"_code": {
"extension": [ {
"url": "http://terminology.hl7.org/CodeSystem/data-absent-reason",
"valueCode": "unknown"
} ]
}
} ]
},
"strength": {
"numerator": {
"value": 48,
"unit": "Mio I.E./0,5ml",
"system": "http://unitsofmeasure.org",
"code": "Mio I.E./0,5ml"
},
"denominator": {
"value": 1,
"system": "http://XXX",
"code": "Fertigspritzen"
}
}
} ]
},
"request": {
"method": "PUT",
"url": "Medication/Medication-154102257"
}
}, {
"fullUrl": "MedicationStatement/UKE-0013-E-1-MS-1",
"resource": {
"resourceType": "MedicationStatement",
"id": "UKE-0013-E-1-MS-1",
"meta": {
"profile": [ "https://www.medizininformatik-initiative.de/fhir/core/modul-medikation/StructureDefinition/MedicationStatement" ]
},
"status": "active",
"medicationReference": {
"reference": "Medication/Medication-154102257"
},
"subject": {
"reference": "Patient/UKE-0013"
},
"context": {
"reference": "Encounter/UKE-0013-E-1"
},
"effectiveDateTime": "2020-06-13T00:00:00+02:00",
"dosage": [ {
"doseAndRate": [ {
"doseQuantity": {
"value": 1,
"unit": "Fertigspritzen",
"system": "http://unitsofmeasure.org",
"code": "1"
}
} ]
} ]
},
"request": {
"method": "PUT",
"url": "MedicationStatement/UKE-0013-E-1-MS-1"
}
}, {
"fullUrl": "Medication/Medication--132842199",
"resource": {
"resourceType": "Medication",
"id": "Medication--132842199",
"meta": {
"profile": [ "https://www.medizininformatik-initiative.de/fhir/core/modul-medikation/StructureDefinition/Medication" ]
},
"identifier": [ {
"value": "Medication--132842199"
} ],
"code": {
"coding": [ {
"system": "http://fhir.de/CodeSystem/ifa/pzn",
"code": "12636016"
}, {
"system": "http://fhir.de/CodeSystem/bfarm/atc",
"code": "B01AF01"
} ],
"text": "Rivaroxaban"
},
"ingredient": [ {
"itemCodeableConcept": {
"coding": [ {
"system": "http://fhir.de/CodeSystem/ask",
"_code": {
"extension": [ {
"url": "http://terminology.hl7.org/CodeSystem/data-absent-reason",
"valueCode": "unknown"
} ]
}
} ]
},
"strength": {
"numerator": {
"value": 10,
"unit": "milligram",
"system": "http://unitsofmeasure.org",
"code": "mg"
},
"denominator": {
"value": 1,
"system": "http://XXX",
"code": "Tabletten"
}
}
} ]
},
"request": {
"method": "PUT",
"url": "Medication/Medication--132842199"
}
}, {
"fullUrl": "MedicationStatement/UKE-0013-E-1-MS-2",
"resource": {
"resourceType": "MedicationStatement",
"id": "UKE-0013-E-1-MS-2",
"meta": {
"profile": [ "https://www.medizininformatik-initiative.de/fhir/core/modul-medikation/StructureDefinition/MedicationStatement" ]
},
"status": "active",
"medicationReference": {
"reference": "Medication/Medication--132842199"
},
"subject": {
"reference": "Patient/UKE-0013"
},
"context": {
"reference": "Encounter/UKE-0013-E-1"
},
"effectiveDateTime": "2020-06-13T00:00:00+02:00",
"dosage": [ {
"doseAndRate": [ {
"doseQuantity": {
"value": 1,
"unit": "Tabletten",
"system": "http://unitsofmeasure.org",
"code": "1"
}
} ]
} ]
},
"request": {
"method": "PUT",
"url": "MedicationStatement/UKE-0013-E-1-MS-2"
}
}, {
"fullUrl": "Medication/Medication-1928216850",
"resource": {
"resourceType": "Medication",
"id": "Medication-1928216850",
"meta": {
"profile": [ "https://www.medizininformatik-initiative.de/fhir/core/modul-medikation/StructureDefinition/Medication" ]
},
"identifier": [ {
"value": "Medication-1928216850"
} ],
"code": {
"coding": [ {
"system": "http://fhir.de/CodeSystem/ifa/pzn",
"code": "06882768"
}, {
"system": "http://fhir.de/CodeSystem/bfarm/atc",
"code": "N02BB02"
} ],
"text": "Metamizol natrium-1-Wasser"
},
"ingredient": [ {
"itemCodeableConcept": {
"coding": [ {
"system": "http://fhir.de/CodeSystem/ask",
"_code": {
"extension": [ {
"url": "http://terminology.hl7.org/CodeSystem/data-absent-reason",
"valueCode": "unknown"
} ]
}
} ]
},
"strength": {
"numerator": {
"value": 1000,
"unit": "milligram",
"system": "http://unitsofmeasure.org",
"code": "mg"
},
"denominator": {
"value": 1,
"system": "http://XXX",
"code": "Injektionslösung"
}
}
} ]
},
"request": {
"method": "PUT",
"url": "Medication/Medication-1928216850"
}
}, {
"fullUrl": "MedicationStatement/UKE-0013-E-1-MS-3",
"resource": {
"resourceType": "MedicationStatement",
"id": "UKE-0013-E-1-MS-3",
"meta": {
"profile": [ "https://www.medizininformatik-initiative.de/fhir/core/modul-medikation/StructureDefinition/MedicationStatement" ]
},
"status": "active",
"medicationReference": {
"reference": "Medication/Medication-1928216850"
},
"subject": {
"reference": "Patient/UKE-0013"
},
"context": {
"reference": "Encounter/UKE-0013-E-1"
},
"effectiveDateTime": "2020-06-13T00:00:00+02:00",
"dosage": [ {
"doseAndRate": [ {
"doseQuantity": {
"value": 4,
"unit": "Injektionslösung",
"system": "http://unitsofmeasure.org",
"code": "1"
}
} ]
} ]
},
"request": {
"method": "PUT",
"url": "MedicationStatement/UKE-0013-E-1-MS-3"
}
}, {
"fullUrl": "MedicationStatement/UKE-0013-E-1-MS-4",
"resource": {
"resourceType": "MedicationStatement",
"id": "UKE-0013-E-1-MS-4",
"meta": {
"profile": [ "https://www.medizininformatik-initiative.de/fhir/core/modul-medikation/StructureDefinition/MedicationStatement" ]
},
"status": "active",
"medicationReference": {
"reference": "Medication/Medication-154102257"
},
"subject": {
"reference": "Patient/UKE-0013"
},
"context": {
"reference": "Encounter/UKE-0013-E-1"
},
"effectiveDateTime": "2020-06-14T00:00:00+02:00",
"dosage": [ {
"doseAndRate": [ {
"doseQuantity": {
"value": 1,
"unit": "Fertigspritzen",
"system": "http://unitsofmeasure.org",
"code": "1"
}
} ]
} ]
},
"request": {
"method": "PUT",
"url": "MedicationStatement/UKE-0013-E-1-MS-4"
}
}, {
"fullUrl": "MedicationStatement/UKE-0013-E-1-MS-5",
"resource": {
"resourceType": "MedicationStatement",
"id": "UKE-0013-E-1-MS-5",
"meta": {
"profile": [ "https://www.medizininformatik-initiative.de/fhir/core/modul-medikation/StructureDefinition/MedicationStatement" ]
},
"status": "active",
"medicationReference": {
"reference": "Medication/Medication--132842199"
},
"subject": {
"reference": "Patient/UKE-0013"
},
"context": {
"reference": "Encounter/UKE-0013-E-1"
},
"effectiveDateTime": "2020-06-14T00:00:00+02:00",
"dosage": [ {
"doseAndRate": [ {
"doseQuantity": {
"value": 1,
"unit": "Tabletten",
"system": "http://unitsofmeasure.org",
"code": "1"
}
} ]
} ]
},
"request": {
"method": "PUT",
"url": "MedicationStatement/UKE-0013-E-1-MS-5"
}
}, {
"fullUrl": "MedicationStatement/UKE-0013-E-1-MS-6",
"resource": {
"resourceType": "MedicationStatement",
"id": "UKE-0013-E-1-MS-6",
"meta": {
"profile": [ "https://www.medizininformatik-initiative.de/fhir/core/modul-medikation/StructureDefinition/MedicationStatement" ]
},
"status": "active",
"medicationReference": {
"reference": "Medication/Medication-1928216850"
},
"subject": {
"reference": "Patient/UKE-0013"
},
"context": {
"reference": "Encounter/UKE-0013-E-1"
},
"effectiveDateTime": "2020-06-14T00:00:00+02:00",
"dosage": [ {
"doseAndRate": [ {
"doseQuantity": {
"value": 4,
"unit": "Injektionslösung",
"system": "http://unitsofmeasure.org",
"code": "1"
}
} ]
} ]
},
"request": {
"method": "PUT",
"url": "MedicationStatement/UKE-0013-E-1-MS-6"
}
}, {
"fullUrl": "MedicationStatement/UKE-0013-E-1-MS-7",
"resource": {
"resourceType": "MedicationStatement",
"id": "UKE-0013-E-1-MS-7",
"meta": {
"profile": [ "https://www.medizininformatik-initiative.de/fhir/core/modul-medikation/StructureDefinition/MedicationStatement" ]
},
"status": "active",
"medicationReference": {
"reference": "Medication/Medication-154102257"
},
"subject": {
"reference": "Patient/UKE-0013"
},
"context": {
"reference": "Encounter/UKE-0013-E-1"
},
"effectiveDateTime": "2020-06-15T00:00:00+02:00",
"dosage": [ {
"doseAndRate": [ {
"doseQuantity": {
"value": 1,
"unit": "Fertigspritzen",
"system": "http://unitsofmeasure.org",
"code": "1"
}
} ]
} ]
},
"request": {
"method": "PUT",
"url": "MedicationStatement/UKE-0013-E-1-MS-7"
}
}, {
"fullUrl": "MedicationStatement/UKE-0013-E-1-MS-8",
"resource": {
"resourceType": "MedicationStatement",
"id": "UKE-0013-E-1-MS-8",
"meta": {
"profile": [ "https://www.medizininformatik-initiative.de/fhir/core/modul-medikation/StructureDefinition/MedicationStatement" ]
},
"status": "active",
"medicationReference": {
"reference": "Medication/Medication--132842199"
},
"subject": {
"reference": "Patient/UKE-0013"
},
"context": {
"reference": "Encounter/UKE-0013-E-1"
},
"effectiveDateTime": "2020-06-15T00:00:00+02:00",
"dosage": [ {
"doseAndRate": [ {
"doseQuantity": {
"value": 1,
"unit": "Tabletten",
"system": "http://unitsofmeasure.org",
"code": "1"
}
} ]
} ]
},
"request": {
"method": "PUT",
"url": "MedicationStatement/UKE-0013-E-1-MS-8"
}
}, {
"fullUrl": "MedicationStatement/UKE-0013-E-1-MS-9",
"resource": {
"resourceType": "MedicationStatement",
"id": "UKE-0013-E-1-MS-9",
"meta": {
"profile": [ "https://www.medizininformatik-initiative.de/fhir/core/modul-medikation/StructureDefinition/MedicationStatement" ]
},
"status": "active",
"medicationReference": {
"reference": "Medication/Medication-1928216850"
},
"subject": {
"reference": "Patient/UKE-0013"
},
"context": {
"reference": "Encounter/UKE-0013-E-1"
},
"effectiveDateTime": "2020-06-15T00:00:00+02:00",
"dosage": [ {
"doseAndRate": [ {
"doseQuantity": {
"value": 4,
"unit": "Injektionslösung",
"system": "http://unitsofmeasure.org",
"code": "1"
}
} ]
} ]
},
"request": {
"method": "PUT",
"url": "MedicationStatement/UKE-0013-E-1-MS-9"
}
}, {
"fullUrl": "MedicationStatement/UKE-0013-E-1-MS-10",
"resource": {
"resourceType": "MedicationStatement",
"id": "UKE-0013-E-1-MS-10",
"meta": {
"profile": [ "https://www.medizininformatik-initiative.de/fhir/core/modul-medikation/StructureDefinition/MedicationStatement" ]
},
"status": "active",
"medicationReference": {
"reference": "Medication/Medication-154102257"
},
"subject": {
"reference": "Patient/UKE-0013"
},
"context": {
"reference": "Encounter/UKE-0013-E-1"
},
"effectiveDateTime": "2020-06-16T00:00:00+02:00",
"dosage": [ {
"doseAndRate": [ {
"doseQuantity": {
"value": 1,
"unit": "Fertigspritzen",
"system": "http://unitsofmeasure.org",
"code": "1"
}
} ]
} ]
},
"request": {
"method": "PUT",
"url": "MedicationStatement/UKE-0013-E-1-MS-10"
}
}, {
"fullUrl": "MedicationStatement/UKE-0013-E-1-MS-11",
"resource": {
"resourceType": "MedicationStatement",
"id": "UKE-0013-E-1-MS-11",
"meta": {
"profile": [ "https://www.medizininformatik-initiative.de/fhir/core/modul-medikation/StructureDefinition/MedicationStatement" ]
},
"status": "active",
"medicationReference": {
"reference": "Medication/Medication--132842199"
},
"subject": {
"reference": "Patient/UKE-0013"
},
"context": {
"reference": "Encounter/UKE-0013-E-1"
},
"effectiveDateTime": "2020-06-16T00:00:00+02:00",
"dosage": [ {
"doseAndRate": [ {
"doseQuantity": {
"value": 1,
"unit": "Tabletten",
"system": "http://unitsofmeasure.org",
"code": "1"
}
} ]
} ]
},
"request": {
"method": "PUT",
"url": "MedicationStatement/UKE-0013-E-1-MS-11"
}
}, {
"fullUrl": "MedicationStatement/UKE-0013-E-1-MS-12",
"resource": {
"resourceType": "MedicationStatement",
"id": "UKE-0013-E-1-MS-12",
"meta": {
"profile": [ "https://www.medizininformatik-initiative.de/fhir/core/modul-medikation/StructureDefinition/MedicationStatement" ]
},
"status": "active",
"medicationReference": {
"reference": "Medication/Medication-1928216850"
},
"subject": {
"reference": "Patient/UKE-0013"
},
"context": {
"reference": "Encounter/UKE-0013-E-1"
},
"effectiveDateTime": "2020-06-16T00:00:00+02:00",
"dosage": [ {
"doseAndRate": [ {
"doseQuantity": {
"value": 4,
"unit": "Injektionslösung",
"system": "http://unitsofmeasure.org",
"code": "1"
}
} ]
} ]
},
"request": {
"method": "PUT",
"url": "MedicationStatement/UKE-0013-E-1-MS-12"
}
}, {
"fullUrl": "MedicationStatement/UKE-0013-E-1-MS-13",
"resource": {
"resourceType": "MedicationStatement",
"id": "UKE-0013-E-1-MS-13",
"meta": {
"profile": [ "https://www.medizininformatik-initiative.de/fhir/core/modul-medikation/StructureDefinition/MedicationStatement" ]
},
"status": "active",
"medicationReference": {
"reference": "Medication/Medication-154102257"
},
"subject": {
"reference": "Patient/UKE-0013"
},
"context": {
"reference": "Encounter/UKE-0013-E-1"
},
"effectiveDateTime": "2020-06-17T00:00:00+02:00",
"dosage": [ {
"doseAndRate": [ {
"doseQuantity": {
"value": 1,
"unit": "Fertigspritzen",
"system": "http://unitsofmeasure.org",
"code": "1"
}
} ]
} ]
},
"request": {
"method": "PUT",
"url": "MedicationStatement/UKE-0013-E-1-MS-13"
}
}, {
"fullUrl": "MedicationStatement/UKE-0013-E-1-MS-14",
"resource": {
"resourceType": "MedicationStatement",
"id": "UKE-0013-E-1-MS-14",
"meta": {
"profile": [ "https://www.medizininformatik-initiative.de/fhir/core/modul-medikation/StructureDefinition/MedicationStatement" ]
},
"status": "active",
"medicationReference": {
"reference": "Medication/Medication--132842199"
},
"subject": {
"reference": "Patient/UKE-0013"
},
"context": {
"reference": "Encounter/UKE-0013-E-1"
},
"effectiveDateTime": "2020-06-17T00:00:00+02:00",
"dosage": [ {
"doseAndRate": [ {
"doseQuantity": {
"value": 1,
"unit": "Tabletten",
"system": "http://unitsofmeasure.org",
"code": "1"
}
} ]
} ]
},
"request": {
"method": "PUT",
"url": "MedicationStatement/UKE-0013-E-1-MS-14"
}
}, {
"fullUrl": "MedicationStatement/UKE-0013-E-1-MS-15",
"resource": {
"resourceType": "MedicationStatement",
"id": "UKE-0013-E-1-MS-15",
"meta": {
"profile": [ "https://www.medizininformatik-initiative.de/fhir/core/modul-medikation/StructureDefinition/MedicationStatement" ]
},
"status": "active",
"medicationReference": {
"reference": "Medication/Medication-1928216850"
},
"subject": {
"reference": "Patient/UKE-0013"
},
"context": {
"reference": "Encounter/UKE-0013-E-1"
},
"effectiveDateTime": "2020-06-17T00:00:00+02:00",
"dosage": [ {
"doseAndRate": [ {
"doseQuantity": {
"value": 4,
"unit": "Injektionslösung",
"system": "http://unitsofmeasure.org",
"code": "1"
}
} ]
} ]
},
"request": {
"method": "PUT",
"url": "MedicationStatement/UKE-0013-E-1-MS-15"
}
}, {
"fullUrl": "MedicationStatement/UKE-0013-E-1-MS-16",
"resource": {
"resourceType": "MedicationStatement",
"id": "UKE-0013-E-1-MS-16",
"meta": {
"profile": [ "https://www.medizininformatik-initiative.de/fhir/core/modul-medikation/StructureDefinition/MedicationStatement" ]
},
"status": "active",
"medicationReference": {
"reference": "Medication/Medication-154102257"
},
"subject": {
"reference": "Patient/UKE-0013"
},
"context": {
"reference": "Encounter/UKE-0013-E-1"
},
"effectiveDateTime": "2020-06-18T00:00:00+02:00",
"dosage": [ {
"doseAndRate": [ {
"doseQuantity": {
"value": 1,
"unit": "Fertigspritzen",
"system": "http://unitsofmeasure.org",
"code": "1"
}
} ]
} ]
},
"request": {
"method": "PUT",
"url": "MedicationStatement/UKE-0013-E-1-MS-16"
}
}, {
"fullUrl": "MedicationStatement/UKE-0013-E-1-MS-17",
"resource": {
"resourceType": "MedicationStatement",
"id": "UKE-0013-E-1-MS-17",
"meta": {
"profile": [ "https://www.medizininformatik-initiative.de/fhir/core/modul-medikation/StructureDefinition/MedicationStatement" ]
},
"status": "active",
"medicationReference": {
"reference": "Medication/Medication--132842199"
},
"subject": {
"reference": "Patient/UKE-0013"
},
"context": {
"reference": "Encounter/UKE-0013-E-1"
},
"effectiveDateTime": "2020-06-18T00:00:00+02:00",
"dosage": [ {
"doseAndRate": [ {
"doseQuantity": {
"value": 1,
"unit": "Tabletten",
"system": "http://unitsofmeasure.org",
"code": "1"
}
} ]
} ]
},
"request": {
"method": "PUT",
"url": "MedicationStatement/UKE-0013-E-1-MS-17"
}
}, {
"fullUrl": "MedicationStatement/UKE-0013-E-1-MS-18",
"resource": {
"resourceType": "MedicationStatement",
"id": "UKE-0013-E-1-MS-18",
"meta": {
"profile": [ "https://www.medizininformatik-initiative.de/fhir/core/modul-medikation/StructureDefinition/MedicationStatement" ]
},
"status": "active",
"medicationReference": {
"reference": "Medication/Medication-1928216850"
},
"subject": {
"reference": "Patient/UKE-0013"
},
"context": {
"reference": "Encounter/UKE-0013-E-1"
},
"effectiveDateTime": "2020-06-18T00:00:00+02:00",
"dosage": [ {
"doseAndRate": [ {
"doseQuantity": {
"value": 4,
"unit": "Injektionslösung",
"system": "http://unitsofmeasure.org",
"code": "1"
}
} ]
} ]
},
"request": {
"method": "PUT",
"url": "MedicationStatement/UKE-0013-E-1-MS-18"
}
} ]
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More