From ed1832f7d34c02a7d97a3205a9335e93b478a333 Mon Sep 17 00:00:00 2001 From: huyenngn Date: Thu, 5 Oct 2023 16:36:12 +0200 Subject: [PATCH 1/2] refactor: improve readability and apply style rules --- capella2polarion/elements/api_helper.py | 2 +- tests/test_elements.py | 38 +++++++++++++------------ 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/capella2polarion/elements/api_helper.py b/capella2polarion/elements/api_helper.py index b6295b29..72f8dc95 100644 --- a/capella2polarion/elements/api_helper.py +++ b/capella2polarion/elements/api_helper.py @@ -81,7 +81,7 @@ def patch_work_item( def split_and_decode_diagram(diagram: str) -> tuple[str, bytes]: - """Split the diagram into type and data and decode the data.""" + """Split the diagram into type and decoded data.""" prefix, encoded = diagram.split(";base64,") return prefix.replace("data:image/", ""), b64.b64decode(encoded) diff --git a/tests/test_elements.py b/tests/test_elements.py index 802cb9ad..bde768fd 100644 --- a/tests/test_elements.py +++ b/tests/test_elements.py @@ -3,8 +3,7 @@ from __future__ import annotations -import base64 -import io +import base64 as b64 import logging import pathlib import re @@ -16,7 +15,6 @@ import polarion_rest_api_client as polarion_api import pytest from capellambse.model import common -from PIL import Image from capella2polarion import elements from capella2polarion.elements import ( @@ -79,17 +77,18 @@ class TestAPIHelper: SVG_PATH = ( pathlib.Path(__file__).parent / "data" / "svg_diff" / "example.svg" ) - SVG_PREFIX = "data:image/svg+xml;base64," - def encode_svg(self, params) -> str: + def encode_svg(self, params: dict[str, str]) -> str: svg = self.SVG_PATH.read_text() for key, value in params.items(): svg = re.sub(f"{key}=[\"'][^\"']*[\"']", f'{key}="{value}"', svg) - encoded = base64.b64encode(svg.encode("utf-8")).decode("utf-8") - return f"{self.SVG_PREFIX}{encoded}" + content_encoded = b64.b64encode(svg.encode("utf-8")) + image_data = b"data:image/svg+xml;base64," + content_encoded + src = image_data.decode() + return src @pytest.mark.parametrize( - "changed_params,expected", + "params,expected", [ pytest.param( ({}, {}), @@ -97,10 +96,15 @@ def encode_svg(self, params) -> str: id="unchanged", ), pytest.param( - ({"fill": "red"}, {"fill": "blue"}), + ({"fill": "blue"}, {"fill": "red"}), True, id="fill_changed", ), + pytest.param( + ({"height": "100"}, {"height": "50"}), + True, + id="height_changed", + ), pytest.param( ({"id": "test"}, {"id": "test2"}), False, @@ -116,16 +120,14 @@ def encode_svg(self, params) -> str: ), ], ) - def test_image_diff(self, changed_params, expected): - old_params, new_params = changed_params + def test_image_diff(self, params, expected): + old_params, new_params = params + old_svg = self.encode_svg(old_params) + new_svg = self.encode_svg(new_params) - assert ( - api_helper.has_visual_changes( - self.encode_svg(old_params), - self.encode_svg(new_params), - ) - is expected - ) + is_different = api_helper.has_visual_changes(old_svg, new_svg) + + assert is_different is expected class TestDiagramElements: From d4257d4eb081006c1f44d2cb2f9f70559a46b877 Mon Sep 17 00:00:00 2001 From: huyenngn Date: Mon, 9 Oct 2023 15:55:53 +0200 Subject: [PATCH 2/2] feat: Attach diagrams as images --- .gitignore | 3 ++ capella2polarion/elements/api_helper.py | 20 +++++----- capella2polarion/elements/serialize.py | 43 ++++++++++---------- tests/test_elements.py | 53 ++++++------------------- 4 files changed, 47 insertions(+), 72 deletions(-) diff --git a/.gitignore b/.gitignore index 975287e9..a7020574 100644 --- a/.gitignore +++ b/.gitignore @@ -156,3 +156,6 @@ dmypy.json # Cython debug symbols cython_debug/ + +# VSCode project settings +.vscode/ diff --git a/capella2polarion/elements/api_helper.py b/capella2polarion/elements/api_helper.py index 72f8dc95..149de16f 100644 --- a/capella2polarion/elements/api_helper.py +++ b/capella2polarion/elements/api_helper.py @@ -80,27 +80,29 @@ def patch_work_item( logger.error("Updating work item %r failed. %s", wi, error.args[0]) -def split_and_decode_diagram(diagram: str) -> tuple[str, bytes]: - """Split the diagram into type and decoded data.""" - prefix, encoded = diagram.split(";base64,") - return prefix.replace("data:image/", ""), b64.b64decode(encoded) - - def has_visual_changes(old: str, new: str) -> bool: """Return True if the images of the diagrams differ.""" - type_old, decoded_old = split_and_decode_diagram(old) - type_new, decoded_new = split_and_decode_diagram(new) + type_old, decoded_old = _typify_and_decode_diagram(old) + type_new, decoded_new = _typify_and_decode_diagram(new) if type_old != type_new: return True - if type_old == "svg+xml": + if type_old == "image/svg+xml": decoded_old = cairosvg.svg2png(bytestring=decoded_old) decoded_new = cairosvg.svg2png(bytestring=decoded_new) return decoded_old != decoded_new +def _typify_and_decode_diagram(diagram: str) -> tuple[str, bytes]: + """Split the diagram into type and decoded data.""" + prefix, content = diagram.split(";base64,") + mimetype = prefix.replace("data:", "") + content_decoded = b64.b64decode(content) + return mimetype, content_decoded + + def handle_links( left: cabc.Iterable[polarion_api.WorkItemLink], right: cabc.Iterable[polarion_api.WorkItemLink], diff --git a/capella2polarion/elements/serialize.py b/capella2polarion/elements/serialize.py index 674c3670..fd4b340c 100644 --- a/capella2polarion/elements/serialize.py +++ b/capella2polarion/elements/serialize.py @@ -3,7 +3,7 @@ """Objects for serialization of capella objects to workitems.""" from __future__ import annotations -import base64 +import base64 as b64 import collections.abc as cabc import logging import mimetypes @@ -11,6 +11,7 @@ import re import typing as t +import cairosvg import markupsafe import polarion_rest_api_client as polarion_api from capellambse import helpers as chelpers @@ -66,35 +67,31 @@ def element( def diagram(diag: diagr.Diagram, ctx: dict[str, t.Any]) -> CapellaWorkItem: """Serialize a diagram for Polarion.""" diagram_path = ctx["DIAGRAM_CACHE"] / f"{diag.uuid}.svg" - src = _decode_diagram(diagram_path) - style = "; ".join( - (f"{key}: {value}" for key, value in DIAGRAM_STYLES.items()) - ) - description = f'

' + content = diagram_path.read_bytes() + content_svg = b64.standard_b64encode(content) + svg_attachment = attachment("image/svg+xml", f"{diag.uuid}", content_svg) + content_png = b64.b16encode(cairosvg.svg2png(content.decode("utf8"))) + png_attachment = attachment("image/png", f"{diag.uuid}", content_png) return CapellaWorkItem( type="diagram", title=diag.name, - description_type="text/html", - description=description, status="open", + attachments=[svg_attachment, png_attachment], uuid_capella=diag.uuid, ) -def _decode_diagram(diagram_path: pathlib.Path) -> str: - mime_type, _ = mimetypes.guess_type(diagram_path) - if mime_type is None: - logger.error( - "Do not understand the MIME subtype for the diagram '%s'!", - diagram_path, - ) - return "" - content = diagram_path.read_bytes() - content_encoded = base64.standard_b64encode(content) - assert mime_type is not None - image_data = b"data:" + mime_type.encode() + b";base64," + content_encoded - src = image_data.decode() - return src +def attachment( + mime_type: str, name: str, content: bytes +) -> polarion_api.WorkItemAttachment: + """Serialize an attachment for Polarion.""" + return polarion_api.WorkItemAttachment( + work_item_id="", + id="", + content_bytes=content, + mime_type=mime_type, + file_name=name, + ) def generic_work_item( @@ -143,7 +140,7 @@ def repair_images(node: etree._Element) -> None: filehandler = resources[["\x00", workspace][workspace in resources]] try: with filehandler.open(file_path, "r") as img: - b64_img = base64.b64encode(img.read()).decode("utf8") + b64_img = b64.b64encode(img.read()).decode("utf8") node.attrib["src"] = f"data:{mime_type};base64,{b64_img}" except FileNotFoundError: logger.error("Inline image can't be found from %r", file_path) diff --git a/tests/test_elements.py b/tests/test_elements.py index bde768fd..93e3db24 100644 --- a/tests/test_elements.py +++ b/tests/test_elements.py @@ -55,9 +55,7 @@ TEST_OCAP_UUID: "OperationalCapability", TEST_WE_UUID: "Entity", } -TEST_DIAG_DESCR = ( - '

str: - svg = self.SVG_PATH.read_text() - for key, value in params.items(): - svg = re.sub(f"{key}=[\"'][^\"']*[\"']", f'{key}="{value}"', svg) - content_encoded = b64.b64encode(svg.encode("utf-8")) - image_data = b"data:image/svg+xml;base64," + content_encoded - src = image_data.decode() - return src - @pytest.mark.parametrize( "params,expected", [ @@ -122,13 +111,23 @@ def encode_svg(self, params: dict[str, str]) -> str: ) def test_image_diff(self, params, expected): old_params, new_params = params - old_svg = self.encode_svg(old_params) - new_svg = self.encode_svg(new_params) + old_svg = self._parameterize_and_decode_svg(self.SVG_PATH, old_params) + new_svg = self._parameterize_and_decode_svg(self.SVG_PATH, new_params) is_different = api_helper.has_visual_changes(old_svg, new_svg) assert is_different is expected + def _parameterize_and_decode_svg( + self, diagram_path: pathlib.Path, params: dict[str, str] + ) -> str: + svg = diagram_path.read_text() + for key, value in params.items(): + svg = re.sub(f"{key}=[\"'][^\"']*[\"']", f'{key}="{value}"', svg) + content_encoded = b64.b64encode(svg.encode("utf-8")) + image_data = b"data:image/svg+xml;base64," + content_encoded + return image_data.decode() + class TestDiagramElements: @staticmethod @@ -494,32 +493,6 @@ def test_resolve_element_type(): class TestSerializers: - @staticmethod - def test_diagram(model: capellambse.MelodyModel): - diag = model.diagrams.by_uuid(TEST_DIAG_UUID) - - serialized_diagram = serialize.diagram( - diag, {"DIAGRAM_CACHE": TEST_DIAGRAM_CACHE} - ) - serialized_diagram.description = None - - assert serialized_diagram == serialize.CapellaWorkItem( - type="diagram", - uuid_capella=TEST_DIAG_UUID, - title="[CC] Capability", - description_type="text/html", - status="open", - linked_work_items=[], - ) - - @staticmethod - def test__decode_diagram(): - diagram_path = TEST_DIAGRAM_CACHE / "_APMboAPhEeynfbzU12yy7w.svg" - - diagram = serialize._decode_diagram(diagram_path) - - assert diagram.startswith("data:image/svg+xml;base64,") - @staticmethod @pytest.mark.parametrize( "uuid,expected",