Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/format.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Check formatting

on:
push:
branches: [ main ]
pull_request:
branches: [main]

jobs:
black:
name: Black
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.x"

- name: Install pipx
run: sudo apt update && sudo apt install pipx && pipx ensurepath

- name: Install Black
run: pipx install --global black

- name: Check formatting
run: black --check --diff --include='.*\.py' ./service
8 changes: 4 additions & 4 deletions .github/workflows/unittest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ name: Run Unittests

# Controls when the action will run.
on:
push:
paths: [ main ]
pull_request:
branches: [ main ]
# push:
# paths: [ main ]
# pull_request:
# branches: [ main ]

# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
Expand Down
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"justMyCode": true,
"args": [
"--project-dir",
"../../projects/erp",
"../../../structure-comparer-projects/erp",
"--html",
"--json"
],
Expand Down
11 changes: 11 additions & 0 deletions service/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,17 @@ From `service/` build and start the image with
docker compose up
```

## CLI mode (WIP)
The CLI mode is currently a work in progress. However, the following functionality is already available:

### Generating a file containing a mapping
(Currently only HTML is supported, but JSON support is coming soon)
```bash
python -m structure_comparer output --project-dir {project directory} --format html --mapping_id {mapping id}
```
Both --format and --mapping_id are optional.
The default format is HTML. If --mapping_id is omitted, the tool will generate files for all the mappings contained in the project.

### Developers

The project uses _Poetry_ for the project set-up but can also be installed with plain `pip`.
Expand Down
57 changes: 33 additions & 24 deletions service/src/mapper.py
Original file line number Diff line number Diff line change
@@ -1,48 +1,56 @@
import json


def load_json_file(file_path):
"""
Lädt JSON-Daten aus einer Datei.
"""
with open(file_path, 'r') as file:
with open(file_path, "r") as file:
return json.load(file)


def map_medication_code_coding(kbv_medication, epa_medication):
"""
Mappt das Medication.code.coding-Element von einem KBV-Profil zum ePA-Profil.
"""
if 'code' in kbv_medication and 'coding' in kbv_medication['code']:
epa_medication['code'] = {'coding': []}
for coding in kbv_medication['code']['coding']:
epa_medication['code']['coding'].append({
'system': coding.get('system', ''),
'code': coding.get('code', ''),
'display': coding.get('display', '')
})
if "code" in kbv_medication and "coding" in kbv_medication["code"]:
epa_medication["code"] = {"coding": []}
for coding in kbv_medication["code"]["coding"]:
epa_medication["code"]["coding"].append(
{
"system": coding.get("system", ""),
"code": coding.get("code", ""),
"display": coding.get("display", ""),
}
)


def map_medication_amount(kbv_medication, epa_medication):
"""
Mappt das Medication.amount-Element von einem KBV-Profil zum ePA-Profil.
"""
if 'amount' in kbv_medication:
epa_medication['amount'] = {
'numerator': {
'value': kbv_medication['amount'].get('numerator', {}).get('value', ''),
'unit': kbv_medication['amount'].get('numerator', {}).get('unit', ''),
'system': 'http://unitsofmeasure.org',
'code': kbv_medication['amount'].get('numerator', {}).get('code', '')
if "amount" in kbv_medication:
epa_medication["amount"] = {
"numerator": {
"value": kbv_medication["amount"].get("numerator", {}).get("value", ""),
"unit": kbv_medication["amount"].get("numerator", {}).get("unit", ""),
"system": "http://unitsofmeasure.org",
"code": kbv_medication["amount"].get("numerator", {}).get("code", ""),
},
"denominator": {
"value": kbv_medication["amount"]
.get("denominator", {})
.get("value", ""),
"unit": kbv_medication["amount"].get("denominator", {}).get("unit", ""),
"system": "http://unitsofmeasure.org",
"code": kbv_medication["amount"].get("denominator", {}).get("code", ""),
},
'denominator': {
'value': kbv_medication['amount'].get('denominator', {}).get('value', ''),
'unit': kbv_medication['amount'].get('denominator', {}).get('unit', ''),
'system': 'http://unitsofmeasure.org',
'code': kbv_medication['amount'].get('denominator', {}).get('code', '')
}
}


def main():
kbv_file_path = 'data/Instances/KBV_PR_ERP_Medication.json'
epa_file_path = 'data/Instances/example-epa-medication-2.json'
kbv_file_path = "data/Instances/KBV_PR_ERP_Medication.json"
epa_file_path = "data/Instances/example-epa-medication-2.json"

# Lade KBV- und ePA-Medikationsdaten
kbv_medication = load_json_file(kbv_file_path)
Expand All @@ -55,5 +63,6 @@ def main():
# Ergebnis ausgeben
print("Mapped ePA Medication:", json.dumps(epa_medication, indent=4))


if __name__ == "__main__":
main()
29 changes: 27 additions & 2 deletions service/src/structure_comparer/__main__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,39 @@
import argparse
from pathlib import Path

from .serve import serve
from .output import output

parser = argparse.ArgumentParser(description="Compare profiles and generate mapping")

subparsers = parser.add_subparsers(dest="cmd")
subparsers = parser.add_subparsers(dest="cmd", required=True)

parser_serve = subparsers.add_parser("serve", help="start the server")

args = parser.parse_args()
parser_output = subparsers.add_parser("output", help="generate output files")
parser_output.add_argument(
"--project-dir",
type=Path,
required=True,
help="The project directory containing the profiles and config",
)
parser_output.add_argument(
"--format",
choices=["json", "html"],
default="html",
help="The output format (default: html)",
)
parser_output.add_argument(
"--mapping_id",
type=str,
default=None,
help="The ID of the mapping to generate output for (default: all mappings)",
)

args = parser.parse_args()
if args.cmd == "serve":
serve()
elif args.cmd == "output":
output(args.project_dir, args.format, args.mapping_id)
else:
parser.print_help()
2 changes: 1 addition & 1 deletion service/src/structure_comparer/data/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ def ref_types(self) -> list[str]:
[
p
for t in self.__data.type
if t.code == "Reference"
if t.code == "Reference" and t.targetProfile is not None
for p in t.targetProfile
]
if self.__data.type is not None
Expand Down
22 changes: 12 additions & 10 deletions service/src/structure_comparer/files/template.html.j2
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,34 @@
<html>

<head>
<title>Mapping: {% for profile in source_profiles %}{{ profile['key'] }}{% if not loop.last %}, {% endif %}{% endfor %} in {{ target_profile['key'] }}</title>
<link rel='stylesheet' type='text/css' href='./{{ css_file }}'>
<title>Mapping: {% for profile in source_profiles %}{{ profile['key'] }}{% if not loop.last %}, {% endif %}{% endfor %} in {{ target_profile['key'] }}</title>
<link rel='stylesheet' type='text/css' href='https://cdn.datatables.net/1.11.3/css/jquery.dataTables.min.css'>
<link rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css'>
<style>
{{ inline_css | safe }}
</style>
<script type='text/javascript' src='https://code.jquery.com/jquery-3.6.0.min.js'></script>
<script type='text/javascript' src='https://cdn.datatables.net/1.11.3/js/jquery.dataTables.min.js'></script>
</head>

<body>
<h2>Mapping: {% for profile in source_profiles %}{{ profile['key'] }}{% if not loop.last %}, {% endif %}{% endfor %} in {{ target_profile['key'] }}</h2>
<h2>Mapping: {% for profile in source_profiles %}{{ profile['name'] ~ "|" ~ profile['version'] }}{% if not loop.last %}, {% endif %}{% endfor %} in {{ target_profile['name'] ~ "|" ~ target_profile['version'] }}</h2>

<div style="display: flex; justify-content: space-between;">
<div style="flex: 1;">
{% if source_profiles|length > 1 %}
<p>Source Profiles:</p>
<ul>
{% for profile in source_profiles %}
<li><a href="{{ profile['url'] }}" target="_blank">{{ profile['key'] }}</a></li>
<li><a href="{{ profile['url'] }}" target="_blank">{{ profile['name'] ~ "|" ~ profile['version'] }}</a></li>
{% endfor %}
</ul>
{% else %}
<p>Source Profile:
<a href="{{ source_profiles[0]['url'] }}" target="_blank">{{ source_profiles[0]['key'] }}</a></p>
<a href="{{ source_profiles[0]['url'] }}" target="_blank">{{ source_profiles[0]['name'] ~ "|" ~ source_profiles[0]['version'] }}</a></p>
{% endif %}

<p>Target Profile: <a href="{{ target_profile['url'] }}" target="_blank">{{ target_profile['key'] }}</a></p>
<p>Target Profile: <a href="{{ target_profile['url'] }}" target="_blank">{{ target_profile['name'] ~ "|" ~ target_profile['version'] }}</a></p>
<p>Version: {{version}}, Status: {{status}}</p>
<p>Last updated on: {{last_updated}}</p>
</div>
Expand Down Expand Up @@ -60,9 +62,9 @@
</tr>
<tr>
{% for profile in source_profiles %}
<th style="text-align: center;">{{ profile['key'] }}</th>
<th style="text-align: center;">{{ profile['name'] ~ "|" ~ profile['version'] }}</th>
{% endfor %}
<th style="text-align: center;">{{ target_profile['key'] }}</th>
<th style="text-align: center;">{{ target_profile['name'] ~ "|" ~ target_profile['version'] }}</th>
</tr>
</thead>
<tbody>
Expand All @@ -73,8 +75,8 @@
</td>
{% for profile in source_profiles + [target_profile] %}
<td style="text-align: center;">
{% if entry.profiles[profile['key']].present %}
{{ entry.profiles[profile['key']].min_cardinality }}..{{ entry.profiles[profile['key']].max_cardinality | format_cardinality }}
{% if entry.profiles[profile['key']] %}
{{ entry.profiles[profile['key']].min }}..{{ entry.profiles[profile['key']].max | format_cardinality }}
{% endif %}
</td>
{% endfor %}
Expand Down
24 changes: 23 additions & 1 deletion service/src/structure_comparer/handler/mapping.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import List
from typing import List, Optional

from ..action import Action
from ..data.mapping import MappingField
Expand All @@ -20,6 +20,7 @@
from ..model.mapping import MappingFieldMinimal as MappingFieldMinimalModel
from ..model.mapping import MappingFieldsOutput as MappingFieldsOutputModel
from .project import ProjectsHandler
from ..results_html import create_results_html


class MappingHandler:
Expand Down Expand Up @@ -55,6 +56,27 @@ def get_field(

return field.to_model()

def get_html(
self,
project_key: str,
mapping_id: str,
show_remarks: bool,
show_warnings: bool,
html_output_dir: Optional[str] = None,
) -> str:
mapping = self.get(project_key, mapping_id)
mappingDict = {mapping.name: mapping}

if html_output_dir is None:
html_output_dir = self.project_handler._get(
project_key
).config.html_output_dir

return create_results_html(
mappingDict, html_output_dir, show_remarks, show_warnings
Copy link
Contributor

Choose a reason for hiding this comment

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

I think there is some strange mismatch here between types. mapping in mappingDict is of type MappingDetailsModel and create_results_html expects Mapping. In the implementation of create_results_html the variable of type Mapping is used like MappingDetailsModel, which causes the linter to go wild. I think what you really intended was the function create_result_html to receive a variable of type MappingDetailsModel.

Please adjust the type in the function description.

Copy link
Author

Choose a reason for hiding this comment

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

Thank you. I have adjusted it

)
# return mapping

def set_field(
self,
project_key: str,
Expand Down
12 changes: 10 additions & 2 deletions service/src/structure_comparer/handler/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
class ProjectsHandler:
def __init__(self, projects_dir: Path):
self.projs_dir = projects_dir
self.__projs: Dict[str, Project]
self.__projs: Dict[str, Project] = {}

@property
def keys(self) -> List[str]:
Expand All @@ -35,13 +35,21 @@ def load(self) -> None:
logger.error(e.errors())
raise e

if not self.__projs:
path = self.projs_dir
if path.is_dir() and (path / "config.json").exists():
try:
self.__projs[path.name] = Project(path)
except ValidationError as e:
logger.error(e.errors())
raise e

def get_list(self) -> ProjectListModel:
projects = [p.to_overview_model() for p in self.__projs.values()]
return ProjectListModel(projects=projects)

def _get(self, project_key: str) -> Project:
proj = self.__projs.get(project_key)

if proj is None:
raise ProjectNotFound()

Expand Down
Loading