From 6cf61017906bf9f5f9cd1ca708b3000770192bab Mon Sep 17 00:00:00 2001 From: Hendre Janse van Rensburg Date: Wed, 18 Jun 2025 11:37:53 +0200 Subject: [PATCH 1/9] add results endpoint to download a static html file --- .../structure_comparer/files/template.html.j2 | 16 +++--- .../src/structure_comparer/handler/mapping.py | 13 +++++ .../src/structure_comparer/results_html.py | 52 +++++++++++++------ service/src/structure_comparer/serve.py | 41 +++++++++++++++ 4 files changed, 99 insertions(+), 23 deletions(-) diff --git a/service/src/structure_comparer/files/template.html.j2 b/service/src/structure_comparer/files/template.html.j2 index bfcec08..02c02a3 100644 --- a/service/src/structure_comparer/files/template.html.j2 +++ b/service/src/structure_comparer/files/template.html.j2 @@ -11,7 +11,7 @@ -

Mapping: {% for profile in source_profiles %}{{ profile['key'] }}{% if not loop.last %}, {% endif %}{% endfor %} in {{ target_profile['key'] }}

+

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

@@ -19,15 +19,15 @@

Source Profiles:

{% else %}

Source Profile: - {{ source_profiles[0]['key'] }}

+ {{ source_profiles[0]['name'] ~ "|" ~ source_profiles[0]['version'] }}

{% endif %} -

Target Profile: {{ target_profile['key'] }}

+

Target Profile: {{ target_profile['name'] ~ "|" ~ target_profile['version'] }}

Version: {{version}}, Status: {{status}}

Last updated on: {{last_updated}}

@@ -60,9 +60,9 @@ {% for profile in source_profiles %} - {{ profile['key'] }} + {{ profile['name'] ~ "|" ~ profile['version'] }} {% endfor %} - {{ target_profile['key'] }} + {{ target_profile['name'] ~ "|" ~ target_profile['version'] }} @@ -73,8 +73,8 @@ {% for profile in source_profiles + [target_profile] %} - {% 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 %} {% endfor %} diff --git a/service/src/structure_comparer/handler/mapping.py b/service/src/structure_comparer/handler/mapping.py index 9b9ca65..b24e5ae 100644 --- a/service/src/structure_comparer/handler/mapping.py +++ b/service/src/structure_comparer/handler/mapping.py @@ -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: @@ -54,6 +55,18 @@ def get_field( raise FieldNotFound() return field.to_model() + + def get_html( + self, project_key: str, mapping_id: str, show_remarks: bool, show_warnings: bool + ) -> str: + mapping = self.get(project_key, mapping_id) + mappingDict = {mapping.name: mapping} + html_output_dir = self.project_handler._get(project_key).config.html_output_dir + print(f"Mapping:{mapping.name}") + + return create_results_html( + mappingDict, html_output_dir, show_remarks, show_warnings) + # return mapping def set_field( self, diff --git a/service/src/structure_comparer/results_html.py b/service/src/structure_comparer/results_html.py index 7448eeb..dd87666 100644 --- a/service/src/structure_comparer/results_html.py +++ b/service/src/structure_comparer/results_html.py @@ -4,7 +4,6 @@ from typing import Dict, List from jinja2 import Environment, FileSystemLoader -from structure_comparer.helpers import split_parent_child from .action import Action from .data.mapping import Mapping @@ -64,21 +63,41 @@ def create_results_html( entries = {} number_of_warnings = 0 # Initialize the warning counter - for field, entry in comp.fields.items(): + fields = {field.name: field for field in comp.fields} + for entry in comp.fields: # .items(): + # field, entry + field = entry.name warnings = set() # Use a set to collect unique warnings - target_min_card = entry.profiles[comp.target.key].min_cardinality - target_max_card = entry.profiles[comp.target.key].max_cardinality + if comp.target.key not in entry.profiles: + warnings.add( + "The target profile does not contain this field, so it cannot be compared" + ) + target_min_card = 0 # _cardinality + target_max_card = 0 # _cardinality + else: + target_min_card = entry.profiles[comp.target.key].min # _cardinality + target_max_card = entry.profiles[comp.target.key].max # _cardinality if target_max_card == "*": target_max_card = float("inf") else: target_max_card = int(target_max_card) - parent, _ = split_parent_child(field) - comparison_parent = comp.fields.get(parent) + match = re.search(r"[.:](?=[^.:]*$)", field) + if match: + parent = field[:match.start()] + else: + parent = field + # parent = field.rsplit(".", 1)[0] + comparison_parent = fields.get(parent) for profile in comp.sources: - source_min_card = entry.profiles[profile.key].min_cardinality - source_max_card = entry.profiles[profile.key].max_cardinality + if profile.key in entry.profiles: + source_min_card = entry.profiles[profile.key].min # _cardinality + source_max_card = entry.profiles[profile.key].max # _cardinality + else: + source_min_card = 0 + source_max_card = 0 + if source_max_card == "*": source_max_card = float("inf") else: @@ -118,7 +137,7 @@ def create_results_html( entries[field] = { "classification": entry.action, "css_class": CSS_CLASS[entry.action], - "extension": entry.extension, + "extension": None, # entry.extension, "extra": entry.other, "profiles": entry.profiles, "remark": entry.remark, @@ -129,12 +148,16 @@ def create_results_html( "css_file": STYLE_FILE_NAME, "target_profile": { "key": comp.target.key, - "url": comp.target.simplifier_url, + "url": comp.target.url, # simplifier_url, + "name": comp.target.name, + "version": comp.target.version }, "source_profiles": [ { "key": profile.key, - "url": profile.simplifier_url, + "url": profile.url, # .simplifier_url, + "name": profile.name, + "version": profile.version } for profile in comp.sources ], @@ -150,14 +173,13 @@ def create_results_html( content = template.render(**data) # HOTFIX: prevent filenames to contain '|' but use '#' instead - source_profiles_flat = flatten_profiles( - [profile["key"].replace("|", "#") for profile in data["source_profiles"]] - ) html_file = ( results_folder - / f"{source_profiles_flat}_to_{data['target_profile']['key'].replace("|", "#")}.html" + / f"{comp.name.replace("|", "#").replace(" -> ", "_to_")}.html" ) html_file.write_text(content, encoding="utf-8") + + return str(html_file) def format_links(text: str) -> str: diff --git a/service/src/structure_comparer/serve.py b/service/src/structure_comparer/serve.py index 05eb328..bdcf74b 100644 --- a/service/src/structure_comparer/serve.py +++ b/service/src/structure_comparer/serve.py @@ -4,6 +4,7 @@ import uvicorn from fastapi import FastAPI, Response, UploadFile +from fastapi.responses import FileResponse from fastapi.middleware.cors import CORSMiddleware from .errors import ( @@ -798,6 +799,46 @@ async def get_mapping( return ErrorModel.from_except(e) +@app.get( + "/project/{project_key}/mapping/{mapping_id}/results", + tags=["Mappings"], + response_model_exclude_unset=True, + response_model_exclude_none=True, + responses={404: {}}, +) +async def get_mapping_results( + project_key: str, mapping_id: str, show_remarks: bool, show_warnings: bool, response: Response +) -> FileResponse: # MappingDetailsModel | ErrorModel: + """ + Get a static HTML page with the mappings + Returns a static HTML page with all mappings. + --- + produces: + - text/html + responses: + 200: + description: A static HTML page with the mappings + content: + text/html: + schema: + type: string + format: binary + headers: + Content-Disposition: + description: The filename of the HTML file + schema: + type: string + example: "mapping_results.html" + """ + global mapping_handler + try: + return FileResponse(mapping_handler.get_html(project_key, mapping_id, show_remarks, show_warnings), media_type="text/html") + + except (ProjectNotFound, MappingNotFound) as e: + response.status_code = 404 + return ErrorModel.from_except(e) + + @app.get( "/mapping/{id}/fields", tags=["Fields"], From 4f1de0681cd3e37c30fb015438c6925cc3d3b782 Mon Sep 17 00:00:00 2001 From: Alexander Essenwanger Date: Mon, 23 Jun 2025 15:27:59 +0200 Subject: [PATCH 2/9] check black format on PR --- .github/workflows/format.yaml | 28 +++++++++++++++++ .github/workflows/unittest.yml | 8 ++--- service/src/mapper.py | 57 ++++++++++++++++++++-------------- 3 files changed, 65 insertions(+), 28 deletions(-) create mode 100644 .github/workflows/format.yaml diff --git a/.github/workflows/format.yaml b/.github/workflows/format.yaml new file mode 100644 index 0000000..f5ac7e3 --- /dev/null +++ b/.github/workflows/format.yaml @@ -0,0 +1,28 @@ +name: Check formatting + +on: + 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 diff --git a/.github/workflows/unittest.yml b/.github/workflows/unittest.yml index 710ab3d..db51e1b 100644 --- a/.github/workflows/unittest.yml +++ b/.github/workflows/unittest.yml @@ -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: diff --git a/service/src/mapper.py b/service/src/mapper.py index ed7abbd..141b150 100644 --- a/service/src/mapper.py +++ b/service/src/mapper.py @@ -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) @@ -55,5 +63,6 @@ def main(): # Ergebnis ausgeben print("Mapped ePA Medication:", json.dumps(epa_medication, indent=4)) + if __name__ == "__main__": main() From 1b82d36601e69b69319111e68d30383bc30b732f Mon Sep 17 00:00:00 2001 From: Alexander Essenwanger Date: Mon, 23 Jun 2025 16:14:29 +0200 Subject: [PATCH 3/9] also run formatting checks on push on main --- .github/workflows/format.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/format.yaml b/.github/workflows/format.yaml index f5ac7e3..2897c2e 100644 --- a/.github/workflows/format.yaml +++ b/.github/workflows/format.yaml @@ -1,6 +1,8 @@ name: Check formatting on: + push: + branches: [ main ] pull_request: branches: [main] From 2741e299c09d205bd2e05a8af85a0f3848712a27 Mon Sep 17 00:00:00 2001 From: Hendre Janse van Rensburg Date: Tue, 8 Jul 2025 16:01:03 +0200 Subject: [PATCH 4/9] update format --- service/src/structure_comparer/handler/mapping.py | 9 +++++---- service/src/structure_comparer/results_html.py | 8 ++++---- service/src/structure_comparer/serve.py | 15 ++++++++++++--- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/service/src/structure_comparer/handler/mapping.py b/service/src/structure_comparer/handler/mapping.py index b24e5ae..584e022 100644 --- a/service/src/structure_comparer/handler/mapping.py +++ b/service/src/structure_comparer/handler/mapping.py @@ -55,17 +55,18 @@ def get_field( raise FieldNotFound() return field.to_model() - + def get_html( - self, project_key: str, mapping_id: str, show_remarks: bool, show_warnings: bool + self, project_key: str, mapping_id: str, show_remarks: bool, show_warnings: bool ) -> str: mapping = self.get(project_key, mapping_id) mappingDict = {mapping.name: mapping} html_output_dir = self.project_handler._get(project_key).config.html_output_dir print(f"Mapping:{mapping.name}") - + return create_results_html( - mappingDict, html_output_dir, show_remarks, show_warnings) + mappingDict, html_output_dir, show_remarks, show_warnings + ) # return mapping def set_field( diff --git a/service/src/structure_comparer/results_html.py b/service/src/structure_comparer/results_html.py index dd87666..6ae4b0a 100644 --- a/service/src/structure_comparer/results_html.py +++ b/service/src/structure_comparer/results_html.py @@ -84,7 +84,7 @@ def create_results_html( match = re.search(r"[.:](?=[^.:]*$)", field) if match: - parent = field[:match.start()] + parent = field[: match.start()] else: parent = field # parent = field.rsplit(".", 1)[0] @@ -150,14 +150,14 @@ def create_results_html( "key": comp.target.key, "url": comp.target.url, # simplifier_url, "name": comp.target.name, - "version": comp.target.version + "version": comp.target.version, }, "source_profiles": [ { "key": profile.key, "url": profile.url, # .simplifier_url, "name": profile.name, - "version": profile.version + "version": profile.version, } for profile in comp.sources ], @@ -178,7 +178,7 @@ def create_results_html( / f"{comp.name.replace("|", "#").replace(" -> ", "_to_")}.html" ) html_file.write_text(content, encoding="utf-8") - + return str(html_file) diff --git a/service/src/structure_comparer/serve.py b/service/src/structure_comparer/serve.py index bdcf74b..61a32c9 100644 --- a/service/src/structure_comparer/serve.py +++ b/service/src/structure_comparer/serve.py @@ -800,14 +800,18 @@ async def get_mapping( @app.get( - "/project/{project_key}/mapping/{mapping_id}/results", + "/project/{project_key}/mapping/{mapping_id}/html", tags=["Mappings"], response_model_exclude_unset=True, response_model_exclude_none=True, responses={404: {}}, ) async def get_mapping_results( - project_key: str, mapping_id: str, show_remarks: bool, show_warnings: bool, response: Response + project_key: str, + mapping_id: str, + show_remarks: bool, + show_warnings: bool, + response: Response, ) -> FileResponse: # MappingDetailsModel | ErrorModel: """ Get a static HTML page with the mappings @@ -832,7 +836,12 @@ async def get_mapping_results( """ global mapping_handler try: - return FileResponse(mapping_handler.get_html(project_key, mapping_id, show_remarks, show_warnings), media_type="text/html") + return FileResponse( + mapping_handler.get_html( + project_key, mapping_id, show_remarks, show_warnings + ), + media_type="text/html", + ) except (ProjectNotFound, MappingNotFound) as e: response.status_code = 404 From 26e70a2cac64c7d81c5d1c726b64700c84cbbd97 Mon Sep 17 00:00:00 2001 From: Hendre Janse van Rensburg Date: Fri, 8 Aug 2025 10:48:12 +0200 Subject: [PATCH 5/9] add output as HTML file functionality to CLI mode --- .vscode/launch.json | 2 +- service/README.md | 11 +++++ service/src/structure_comparer/__main__.py | 29 +++++++++++- .../src/structure_comparer/data/profile.py | 2 +- .../src/structure_comparer/handler/mapping.py | 16 +++++-- .../src/structure_comparer/handler/project.py | 12 ++++- service/src/structure_comparer/output.py | 47 +++++++++++++++++++ 7 files changed, 109 insertions(+), 10 deletions(-) create mode 100644 service/src/structure_comparer/output.py diff --git a/.vscode/launch.json b/.vscode/launch.json index 0b07798..8cf1d1e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -13,7 +13,7 @@ "justMyCode": true, "args": [ "--project-dir", - "../../projects/erp", + "../../../structure-comparer-projects/erp", "--html", "--json" ], diff --git a/service/README.md b/service/README.md index fd60950..fd42ceb 100644 --- a/service/README.md +++ b/service/README.md @@ -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`. diff --git a/service/src/structure_comparer/__main__.py b/service/src/structure_comparer/__main__.py index 0da0204..0147c1e 100644 --- a/service/src/structure_comparer/__main__.py +++ b/service/src/structure_comparer/__main__.py @@ -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() diff --git a/service/src/structure_comparer/data/profile.py b/service/src/structure_comparer/data/profile.py index b954c02..a4c578a 100644 --- a/service/src/structure_comparer/data/profile.py +++ b/service/src/structure_comparer/data/profile.py @@ -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 diff --git a/service/src/structure_comparer/handler/mapping.py b/service/src/structure_comparer/handler/mapping.py index 584e022..0f07789 100644 --- a/service/src/structure_comparer/handler/mapping.py +++ b/service/src/structure_comparer/handler/mapping.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Optional from ..action import Action from ..data.mapping import MappingField @@ -57,12 +57,20 @@ def get_field( return field.to_model() def get_html( - self, project_key: str, mapping_id: str, show_remarks: bool, show_warnings: bool + 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} - html_output_dir = self.project_handler._get(project_key).config.html_output_dir - print(f"Mapping:{mapping.name}") + + 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 diff --git a/service/src/structure_comparer/handler/project.py b/service/src/structure_comparer/handler/project.py index f57a6d2..f1da0e1 100644 --- a/service/src/structure_comparer/handler/project.py +++ b/service/src/structure_comparer/handler/project.py @@ -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]: @@ -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() diff --git a/service/src/structure_comparer/output.py b/service/src/structure_comparer/output.py new file mode 100644 index 0000000..613746c --- /dev/null +++ b/service/src/structure_comparer/output.py @@ -0,0 +1,47 @@ +from .errors import ( + MappingNotFound, + ProjectNotFound, +) + +from .model.error import Error as ErrorModel +from structure_comparer.data.config import ProjectConfig +from .handler.mapping import MappingHandler, ProjectsHandler + + +def output(project_dir: str, output_format: str = "html", mapping_id: str = None): + print("Generating output files...") + + try: + config = ProjectConfig.from_json(project_dir / "config.json") + + project_key = config.name + show_remarks = config.show_remarks + show_warnings = config.show_warnings + + for mapping in config.mappings: + if mapping.id == mapping_id or mapping_id is None: + print(f"Processing mapping: {mapping.id}") + project_handler = ProjectsHandler(project_dir) + project_handler.load() + mapping_handler = MappingHandler(project_handler) + + html_output_dir = project_dir / config.html_output_dir + if not html_output_dir.exists(): + html_output_dir.mkdir(parents=True, exist_ok=True) + + if output_format == "html": + print(f"Generating HTML for mapping: {mapping.id}") + mapping_handler.get_html( + project_key, mapping.id, show_remarks, show_warnings, html_output_dir + ), + elif output_format == "json": + raise NotImplementedError("JSON not implemented yet") + + except (ProjectNotFound, MappingNotFound, NotImplementedError) as e: + return ErrorModel.from_except(e) + + print("Output files generated successfully.") + + +if __name__ == "__main__": + output() From 2feddd9549608568f038dcebdf898050b7010a67 Mon Sep 17 00:00:00 2001 From: Hendre Janse van Rensburg Date: Fri, 8 Aug 2025 11:00:14 +0200 Subject: [PATCH 6/9] update the formatting --- service/src/structure_comparer/output.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/service/src/structure_comparer/output.py b/service/src/structure_comparer/output.py index 613746c..5feef1c 100644 --- a/service/src/structure_comparer/output.py +++ b/service/src/structure_comparer/output.py @@ -32,7 +32,11 @@ def output(project_dir: str, output_format: str = "html", mapping_id: str = None if output_format == "html": print(f"Generating HTML for mapping: {mapping.id}") mapping_handler.get_html( - project_key, mapping.id, show_remarks, show_warnings, html_output_dir + project_key, + mapping.id, + show_remarks, + show_warnings, + html_output_dir, ), elif output_format == "json": raise NotImplementedError("JSON not implemented yet") From e6efca742e929546e7538698cd73b8e80c4719ce Mon Sep 17 00:00:00 2001 From: Hendre Janse van Rensburg Date: Mon, 11 Aug 2025 16:08:07 +0200 Subject: [PATCH 7/9] add inline css to html template --- service/src/structure_comparer/files/template.html.j2 | 6 ++++-- service/src/structure_comparer/results_html.py | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/service/src/structure_comparer/files/template.html.j2 b/service/src/structure_comparer/files/template.html.j2 index 02c02a3..6b65537 100644 --- a/service/src/structure_comparer/files/template.html.j2 +++ b/service/src/structure_comparer/files/template.html.j2 @@ -2,10 +2,12 @@ - Mapping: {% for profile in source_profiles %}{{ profile['key'] }}{% if not loop.last %}, {% endif %}{% endfor %} in {{ target_profile['key'] }} - + Mapping: {% for profile in source_profiles %}{{ profile['key'] }}{% if not loop.last %}, {% endif %}{% endfor %} in {{ target_profile['key'] }} + diff --git a/service/src/structure_comparer/results_html.py b/service/src/structure_comparer/results_html.py index 6ae4b0a..77f089b 100644 --- a/service/src/structure_comparer/results_html.py +++ b/service/src/structure_comparer/results_html.py @@ -144,8 +144,9 @@ def create_results_html( "warning": list(warnings), # Convert set back to list } + inline_css = (styles_file).read_text() data = { - "css_file": STYLE_FILE_NAME, + "inline_css": inline_css, "target_profile": { "key": comp.target.key, "url": comp.target.url, # simplifier_url, From 1e9d3f0a0924317589843e43468afc3d92a1f808 Mon Sep 17 00:00:00 2001 From: Hendre Janse van Rensburg Date: Fri, 5 Sep 2025 12:31:09 +0200 Subject: [PATCH 8/9] change MappingModel to MappingDetailsModel --- service/src/structure_comparer/results_html.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/service/src/structure_comparer/results_html.py b/service/src/structure_comparer/results_html.py index 77f089b..1dbf66e 100644 --- a/service/src/structure_comparer/results_html.py +++ b/service/src/structure_comparer/results_html.py @@ -6,7 +6,7 @@ from jinja2 import Environment, FileSystemLoader from .action import Action -from .data.mapping import Mapping +from .model.mapping import MappingDetails as MappingDetailsModel CSS_CLASS = { Action.USE: "row-use", @@ -36,7 +36,7 @@ def format_cardinality(value): def create_results_html( - structured_mapping: Dict[str, Mapping], + structured_mapping: Dict[str, MappingDetailsModel], results_folder: str | Path, show_remarks: bool, show_warnings: bool, From 99a16ae3ca374ea8456e6aaac08dc283e7ee72e5 Mon Sep 17 00:00:00 2001 From: Hendre Janse van Rensburg Date: Fri, 5 Sep 2025 12:38:35 +0200 Subject: [PATCH 9/9] remove commented out code --- .../src/structure_comparer/results_html.py | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/service/src/structure_comparer/results_html.py b/service/src/structure_comparer/results_html.py index 1dbf66e..e372cbb 100644 --- a/service/src/structure_comparer/results_html.py +++ b/service/src/structure_comparer/results_html.py @@ -64,19 +64,18 @@ def create_results_html( number_of_warnings = 0 # Initialize the warning counter fields = {field.name: field for field in comp.fields} - for entry in comp.fields: # .items(): - # field, entry + for entry in comp.fields: field = entry.name warnings = set() # Use a set to collect unique warnings if comp.target.key not in entry.profiles: warnings.add( "The target profile does not contain this field, so it cannot be compared" ) - target_min_card = 0 # _cardinality - target_max_card = 0 # _cardinality + target_min_card = 0 + target_max_card = 0 else: - target_min_card = entry.profiles[comp.target.key].min # _cardinality - target_max_card = entry.profiles[comp.target.key].max # _cardinality + target_min_card = entry.profiles[comp.target.key].min + target_max_card = entry.profiles[comp.target.key].max if target_max_card == "*": target_max_card = float("inf") else: @@ -87,13 +86,13 @@ def create_results_html( parent = field[: match.start()] else: parent = field - # parent = field.rsplit(".", 1)[0] + comparison_parent = fields.get(parent) for profile in comp.sources: if profile.key in entry.profiles: - source_min_card = entry.profiles[profile.key].min # _cardinality - source_max_card = entry.profiles[profile.key].max # _cardinality + source_min_card = entry.profiles[profile.key].min + source_max_card = entry.profiles[profile.key].max else: source_min_card = 0 source_max_card = 0 @@ -137,7 +136,7 @@ def create_results_html( entries[field] = { "classification": entry.action, "css_class": CSS_CLASS[entry.action], - "extension": None, # entry.extension, + "extension": None, "extra": entry.other, "profiles": entry.profiles, "remark": entry.remark, @@ -149,14 +148,14 @@ def create_results_html( "inline_css": inline_css, "target_profile": { "key": comp.target.key, - "url": comp.target.url, # simplifier_url, + "url": comp.target.url, "name": comp.target.name, "version": comp.target.version, }, "source_profiles": [ { "key": profile.key, - "url": profile.url, # .simplifier_url, + "url": profile.url, "name": profile.name, "version": profile.version, }