From 5fb95b52c4f0a42eccf20508b2b703ecff6fb011 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Mon, 21 Aug 2023 09:18:11 +0200 Subject: [PATCH 01/22] feat: Add `checksum` attribute to `CapellaWorkItem` The `checksum` attribute is used to filter updating work items. --- capella2polarion/elements/api_helper.py | 34 ++++++---- capella2polarion/elements/serialize.py | 33 +++++++++- tests/test_elements.py | 87 ++++++++++++++++++------- 3 files changed, 117 insertions(+), 37 deletions(-) diff --git a/capella2polarion/elements/api_helper.py b/capella2polarion/elements/api_helper.py index fb9252f5..0c417822 100644 --- a/capella2polarion/elements/api_helper.py +++ b/capella2polarion/elements/api_helper.py @@ -6,17 +6,25 @@ import typing as t import polarion_rest_api_client as polarion_api -from capellambse.model.common import element +from capellambse.model import common from capella2polarion.elements import serialize logger = logging.getLogger(__name__) +class Diagram(t.TypedDict): + """A Diagram object from the Diagram Cache Index.""" + + uuid: str + name: str + success: bool + + def patch_work_item( ctx: dict[str, t.Any], wid: str, - capella_object: element.GenericElement, + obj: common.GenericElement | Diagram, serializer: cabc.Callable[ [t.Any, dict[str, t.Any]], serialize.CapellaWorkItem ], @@ -31,8 +39,8 @@ def patch_work_item( The context to execute the patch for. wid The ID of the polarion WorkItem - capella_object - The capella object to update the WorkItem from + obj + The Capella object to update the WorkItem from serializer The serializer, which should be used to create the WorkItem. name @@ -46,16 +54,20 @@ def patch_work_item( _type, name, ) - if work_item := serialize.element(capella_object, ctx, serializer): - if work_item.uuid_capella: - del work_item.additional_attributes["uuid_capella"] + if new := serialize.element(obj, ctx, serializer): + uuid = obj["uuid"] if isinstance(obj, dict) else obj.uuid + old: serialize.CapellaWorkItem = ctx["POLARION_WI_MAP"][uuid] + if new.checksum == old.checksum: + return - work_item.type = None - work_item.status = "open" - work_item.id = wid + if new.uuid_capella: + del new.additional_attributes["uuid_capella"] + new.type = None + new.status = "open" + new.id = wid try: - ctx["API"].update_work_item(work_item) + ctx["API"].update_work_item(new) except polarion_api.PolarionApiException as error: wi = f"{wid}({_type} {name})" logger.error("Updating work item %r failed. %s", wi, error.args[0]) diff --git a/capella2polarion/elements/serialize.py b/capella2polarion/elements/serialize.py index 34f6491b..a76bc744 100644 --- a/capella2polarion/elements/serialize.py +++ b/capella2polarion/elements/serialize.py @@ -5,6 +5,8 @@ import base64 import collections.abc as cabc +import hashlib +import json import logging import mimetypes import pathlib @@ -47,6 +49,28 @@ class Condition(t.TypedDict): uuid_capella: str | None preCondition: Condition | None postCondition: Condition | None + checksum: str | None + + +def _convert_work_item_to_dict(work_item: CapellaWorkItem) -> dict[str, t.Any]: + """Convert the instance to a dictionary.""" + return { + "id": work_item.id, + "title": work_item.title, + "description_type": work_item.description_type, + "description": work_item.description, + "type": work_item.type, + "status": work_item.status, + "additional_attributes": work_item.additional_attributes, + "uuid_capella": work_item.uuid_capella, + "checksum": work_item.checksum, + } + + +def _calculate_checksum(payload: dict[str, t.Any]) -> str: + """Return the ``checksum`` of this CapellaWorkItem.""" + converted = json.dumps(payload).encode("utf8") + return hashlib.sha256(converted).hexdigest() def _condition(html: bool, value: str) -> CapellaWorkItem.Condition: @@ -82,6 +106,7 @@ def diagram(diag: dict[str, t.Any], ctx: dict[str, t.Any]) -> CapellaWorkItem: description=description, status="open", uuid_capella=diag["uuid"], + checksum=_calculate_checksum({"description": description}), ) @@ -104,10 +129,14 @@ def _decode_diagram(diagram_path: pathlib.Path) -> str: def generic_work_item( obj: common.GenericElement, ctx: dict[str, t.Any] ) -> CapellaWorkItem: - """Return an attributes dictionary for the given model element.""" + """Return a work item for the given model element.""" xtype = ctx["POLARION_TYPE_MAP"].get(obj.uuid, type(obj).__name__) serializer = SERIALIZERS.get(xtype, _generic_work_item) - return serializer(obj, ctx) + work_item = serializer(obj, ctx) + wi_dict = _convert_work_item_to_dict(work_item) + del wi_dict["checksum"] + work_item.checksum = _calculate_checksum(wi_dict) + return work_item def _generic_work_item( diff --git a/tests/test_elements.py b/tests/test_elements.py index cacf4fd7..a7802d5f 100644 --- a/tests/test_elements.py +++ b/tests/test_elements.py @@ -43,16 +43,28 @@ TEST_OCAP_UUID: "OperationalCapability", TEST_WE_UUID: "Entity", } +TEST_DIAG_DESCR = ( + '

dict[str, t.Any]: api = mock.MagicMock(spec=polarion_api.OpenAPIPolarionProjectClient) uuid = diagram_cache_index[0]["uuid"] + work_item = serialize.CapellaWorkItem(id="Diag-1", checksum="123") return { "API": api, "PROJECT_ID": "project_id", "CAPELLA_UUIDS": [d["uuid"] for d in diagram_cache_index], - "POLARION_WI_MAP": {uuid: polarion_api.WorkItem("Diag-1")}, + "POLARION_WI_MAP": {uuid: work_item}, "POLARION_ID_MAP": {uuid: "Diag-1"}, "DIAGRAM_IDX": diagram_cache_index, "DIAGRAM_CACHE": TEST_DIAGRAM_CACHE, @@ -79,9 +92,9 @@ def context( def test_create_diagrams(context: dict[str, t.Any]): diagram.create_diagrams(context) - work_item = context["API"].create_work_items.call_args[0][0][0] assert context["API"].create_work_items.call_count == 1 - assert isinstance(work_item, polarion_api.WorkItem) + work_item = context["API"].create_work_items.call_args[0][0][0] + assert isinstance(work_item, serialize.CapellaWorkItem) assert { "id": work_item.id, "status": work_item.status, @@ -109,8 +122,8 @@ def test_create_diagrams_filters_non_diagram_elements( def test_update_diagrams(context: dict[str, t.Any]): diagram.update_diagrams(context) - work_item = context["API"].update_work_item.call_args[0][0] assert context["API"].update_work_item.call_count == 1 + work_item = context["API"].update_work_item.call_args[0][0] assert isinstance(work_item.description, str) assert work_item.id == "Diag-1" assert work_item.status == "open" @@ -131,6 +144,17 @@ def test_update_diagrams_filter_non_diagram_elements( assert context["API"].update_work_item.call_count == 0 + @staticmethod + def test_update_diagrams_filters_diagrams_with_same_checksum( + context: dict[str, t.Any] + ): + uuid = context["DIAGRAM_IDX"][0]["uuid"] + context["POLARION_WI_MAP"][uuid].checksum = TEST_DIAG_CHECKSUM + + diagram.update_diagrams(context) + + assert context["API"].update_work_item.call_count == 0 + @staticmethod def test_delete_diagrams(context: dict[str, t.Any]): context["CAPELLA_UUIDS"] = [] @@ -168,6 +192,7 @@ class TestModelElements: def context() -> dict[str, t.Any]: api = mock.MagicMock(spec=polarion_api.OpenAPIPolarionProjectClient) fake = FakeModelObject("uuid1", name="Fake 1") + work_item = serialize.CapellaWorkItem(id="Obj-1", uuid_capella="uuid1") return { "API": api, "PROJECT_ID": "project_id", @@ -180,7 +205,9 @@ def context() -> dict[str, t.Any]: UnsupportedFakeModelObject("uuid3") ], }, + "POLARION_WI_MAP": {"uuid1": work_item}, "POLARION_ID_MAP": {"uuid1": "Obj-1"}, + "POLARION_TYPE_MAP": {"uuid1": "FakeModelObject"}, "CONFIG": {}, "ROLES": {"FakeModelObject": ["attribute"]}, } @@ -213,41 +240,48 @@ def test_create_work_items( element.create_work_items(context) - wi, wi1 = context["API"].create_work_items.call_args[0][0] assert context["API"].create_work_items.call_count == 1 + wi, wi1 = context["API"].create_work_items.call_args[0][0] assert wi == wi_ # type: ignore[arg-type] assert wi1 == wi_1 # type: ignore[arg-type] @staticmethod - def test_update_work_items( - monkeypatch: pytest.MonkeyPatch, context: dict[str, t.Any] - ): - monkeypatch.setattr( - serialize, - "generic_work_item", - mock_generic_work_item := mock.MagicMock(), - ) - mock_generic_work_item.return_value = serialize.CapellaWorkItem( + def test_update_work_items(context: dict[str, t.Any]): + context["POLARION_WI_MAP"]["uuid1"] = serialize.CapellaWorkItem( + id="Obj-1", type="type", uuid_capella="uuid1", title="Something", description_type="text/html", - description=(expected_markup := markupsafe.Markup("Test")), + description=markupsafe.Markup("Test"), + checksum="123", ) element.update_work_items(context) - work_item = context["API"].update_work_item.call_args[0][0] assert context["API"].update_work_item.call_count == 1 - assert isinstance(work_item, polarion_api.WorkItem) + work_item = context["API"].update_work_item.call_args[0][0] + assert isinstance(work_item, serialize.CapellaWorkItem) assert work_item.id == "Obj-1" - assert work_item.title == "Something" + assert work_item.title == "Fake 1" assert work_item.description_type == "text/html" - assert work_item.description == expected_markup + assert work_item.description == markupsafe.Markup("") assert work_item.type is None assert work_item.uuid_capella is None assert work_item.status == "open" + @staticmethod + def test_update_work_items_filters_work_items_with_same_checksum( + context: dict[str, t.Any] + ): + context["POLARION_WI_MAP"]["uuid1"] = serialize.CapellaWorkItem( + checksum=TEST_WI_CHECKSUM, + ) + + element.update_work_items(context) + + assert context["API"].update_work_item.call_count == 0 + @staticmethod def test_update_links_with_no_elements(context: dict[str, t.Any]): context["POLARION_ID_MAP"] = {} @@ -306,6 +340,7 @@ def test_diagram(): title="test_diagram", description_type="text/html", status="open", + checksum=TEST_DIAG_CHECKSUM, ) @staticmethod @@ -429,7 +464,11 @@ def test_generic_work_item( "POLARION_TYPE_MAP": TEST_POL_TYPE_MAP, }, ) - - work_item.status = None # TODO generic_work_item sets status to open - does this make sense? + checksum = work_item.checksum + del work_item.additional_attributes["checksum"] + status = work_item.status + work_item.status = None assert work_item == serialize.CapellaWorkItem(**expected) + assert isinstance(checksum, str) and checksum + assert status == "open" From 47209dc6ac32d7bbd7c51ad92b93f31c138f1667 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Wed, 23 Aug 2023 15:41:49 +0200 Subject: [PATCH 02/22] fix: Remove duplicated `uuid_capella` from conversion --- capella2polarion/elements/serialize.py | 1 - tests/test_elements.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/capella2polarion/elements/serialize.py b/capella2polarion/elements/serialize.py index a76bc744..43da9836 100644 --- a/capella2polarion/elements/serialize.py +++ b/capella2polarion/elements/serialize.py @@ -62,7 +62,6 @@ def _convert_work_item_to_dict(work_item: CapellaWorkItem) -> dict[str, t.Any]: "type": work_item.type, "status": work_item.status, "additional_attributes": work_item.additional_attributes, - "uuid_capella": work_item.uuid_capella, "checksum": work_item.checksum, } diff --git a/tests/test_elements.py b/tests/test_elements.py index a7802d5f..dd2daeb3 100644 --- a/tests/test_elements.py +++ b/tests/test_elements.py @@ -64,7 +64,7 @@ }, } TEST_WI_CHECKSUM = ( - "44f48650534f1405ae09a806d25762bf618884cff9fa71ab427358b71ac440a6" + "736923b9019ae8bc3550609fcf6f2fad255f0aa0e46f41f5662ca4a4f7e35e79" ) From a9d13a4cb0c62fe83d1283a88154acba50f64e5e Mon Sep 17 00:00:00 2001 From: ewuerger Date: Tue, 29 Aug 2023 07:51:32 +0200 Subject: [PATCH 03/22] feat(model-elements): Reduce amount of requests for model elements sync This commit also simplifies the whole synchronization routine by calling less functions. --- capella2polarion/__main__.py | 58 +++------- capella2polarion/elements/__init__.py | 85 ++++++++++++++- capella2polarion/elements/api_helper.py | 49 +++++++-- capella2polarion/elements/element.py | 137 +++++++----------------- capella2polarion/elements/serialize.py | 46 ++------ 5 files changed, 186 insertions(+), 189 deletions(-) diff --git a/capella2polarion/__main__.py b/capella2polarion/__main__.py index 34894a9b..ccf802cf 100644 --- a/capella2polarion/__main__.py +++ b/capella2polarion/__main__.py @@ -79,6 +79,7 @@ def _get_roles_from_config(ctx: dict[str, t.Any]) -> dict[str, list[str]]: roles[key] = list(role_ids) else: roles[typ] = [] + roles["Diagram"] = ["diagram_elements"] return roles @@ -107,21 +108,6 @@ def _sanitize_config( return new_config -def get_polarion_wi_map( - ctx: dict[str, t.Any], type_: str = "" -) -> dict[str, t.Any]: - """Return a map from Capella UUIDs to Polarion work items.""" - types_ = map(elements.helpers.resolve_element_type, ctx.get("TYPES", [])) - work_item_types = [type_] if type_ else list(types_) - _type = " ".join(work_item_types) - work_items = ctx["API"].get_all_work_items( - f"type:({_type})", {"workitems": "id,uuid_capella,status"} - ) - return { - wi.uuid_capella: wi for wi in work_items if wi.id and wi.uuid_capella - } - - @click.group() @click.option("--debug/--no-debug", is_flag=True, default=False) @click.option("--project-id", required=True, type=str) @@ -179,14 +165,16 @@ def diagrams(ctx: click.core.Context, diagram_cache: pathlib.Path) -> None: ctx.obj["CAPELLA_UUIDS"] = [ d["uuid"] for d in ctx.obj["DIAGRAM_IDX"] if d["success"] ] - ctx.obj["POLARION_WI_MAP"] = get_polarion_wi_map(ctx.obj, "diagram") + ctx.obj["POLARION_WI_MAP"] = elements.get_polarion_wi_map( + ctx.obj, "diagram" + ) ctx.obj["POLARION_ID_MAP"] = { uuid: wi.id for uuid, wi in ctx.obj["POLARION_WI_MAP"].items() } elements.delete_work_items(ctx.obj) - elements.diagram.update_diagrams(ctx.obj) elements.diagram.create_diagrams(ctx.obj) + elements.diagram.update_diagrams(ctx.obj) @cli.command() @@ -199,6 +187,11 @@ def model_elements( config_file: t.TextIO, ) -> None: """Synchronise model elements.""" + logger.debug( + "Synchronising model elements (%r) to Polarion project with id %r...", + str(elements.ELEMENTS_IDX_PATH), + ctx.obj["PROJECT_ID"], + ) ctx.obj["MODEL"] = model ctx.obj["CONFIG"] = yaml.safe_load(config_file) ctx.obj["ROLES"] = _get_roles_from_config(ctx.obj) @@ -208,39 +201,16 @@ def model_elements( ) = elements.get_elements_and_type_map(ctx.obj) ctx.obj["CAPELLA_UUIDS"] = set(ctx.obj["POLARION_TYPE_MAP"]) ctx.obj["TYPES"] = elements.get_types(ctx.obj) - ctx.obj["POLARION_WI_MAP"] = get_polarion_wi_map(ctx.obj) + ctx.obj["POLARION_WI_MAP"] = elements.get_polarion_wi_map(ctx.obj) ctx.obj["POLARION_ID_MAP"] = { uuid: wi.id for uuid, wi in ctx.obj["POLARION_WI_MAP"].items() } elements.delete_work_items(ctx.obj) - elements.element.update_work_items(ctx.obj) - elements.element.create_work_items(ctx.obj) + elements.create_work_items(ctx.obj) + elements.update_work_items(ctx.obj) - ctx.obj["POLARION_WI_MAP"] = get_polarion_wi_map(ctx.obj) - ctx.obj["POLARION_ID_MAP"] = { - uuid: wi.id for uuid, wi in ctx.obj["POLARION_WI_MAP"].items() - } - elements.element.update_links(ctx.obj) - - diagram_work = get_polarion_wi_map(ctx.obj, "diagram") - ctx.obj["POLARION_ID_MAP"] |= { - uuid: wi.id for uuid, wi in diagram_work.items() - } - _diagrams = [ - diagram - for diagram in model.diagrams - if diagram.uuid in ctx.obj["POLARION_ID_MAP"] - ] - ctx.obj["ROLES"]["Diagram"] = ["diagram_elements"] - elements.element.update_links(ctx.obj, _diagrams) - - elements_index_file = elements.make_model_elements_index(ctx.obj) - logger.debug( - "Synchronising model objects (%r) to Polarion project with id %r...", - str(elements_index_file), - ctx.obj["PROJECT_ID"], - ) + elements.make_model_elements_index(ctx.obj) if __name__ == "__main__": diff --git a/capella2polarion/elements/__init__.py b/capella2polarion/elements/__init__.py index b844780a..a5cba84e 100644 --- a/capella2polarion/elements/__init__.py +++ b/capella2polarion/elements/__init__.py @@ -12,6 +12,7 @@ "STATUS_DELETE", ] +import functools import logging import pathlib import typing as t @@ -45,6 +46,21 @@ ) +def get_polarion_wi_map( + ctx: dict[str, t.Any], type_: str = "" +) -> dict[str, t.Any]: + """Return a map from Capella UUIDs to Polarion work items.""" + types_ = map(helpers.resolve_element_type, ctx.get("TYPES", [])) + work_item_types = [type_] if type_ else list(types_) + _type = " ".join(work_item_types) + work_items = ctx["API"].get_all_work_items( + f"type:({_type})", {"workitems": "id,uuid_capella,checksum,status"} + ) + return { + wi.uuid_capella: wi for wi in work_items if wi.id and wi.uuid_capella + } + + def delete_work_items(ctx: dict[str, t.Any]) -> None: """Delete work items in a Polarion project. @@ -78,6 +94,57 @@ def serialize_for_delete(uuid: str) -> str: logger.error("Deleting work items failed. %s", error.args[0]) +def create_work_items(ctx: dict[str, t.Any]) -> None: + """Create work items for a Polarion project. + + Parameters + ---------- + ctx + The context for the workitem operation to be processed. + """ + if work_items := element.create_work_items(ctx): + try: + ctx["API"].create_work_items(work_items) + except polarion_api.PolarionApiException as error: + logger.error("Creating work items failed. %s", error.args[0]) + + +def update_work_items(ctx: dict[str, t.Any]) -> None: + """Update work items in a Polarion project. + + Parameters + ---------- + ctx + The context for the workitem operation to be processed. + """ + + def prepare_for_update( + obj: common.GenericElement, ctx: dict[str, t.Any], **kwargs + ) -> serialize.CapellaWorkItem: + work_item = serialize.generic_work_item(obj, ctx) + for key, value in kwargs.items(): + if getattr(work_item, key, None) is None: + continue + + setattr(work_item, key, value) + return work_item + + for obj in chain.from_iterable(ctx["ELEMENTS"].values()): + if obj.uuid not in ctx["POLARION_ID_MAP"]: + continue + + links = element.create_links(obj, ctx) + + api_helper.patch_work_item( + ctx, + ctx["POLARION_ID_MAP"][obj.uuid], + obj, + functools.partial(prepare_for_update, links=links), + obj._short_repr_(), + "element", + ) + + def get_types(ctx: dict[str, t.Any]) -> set[str]: """Return a set of Polarion types from the current context.""" xtypes = set[str]() @@ -107,6 +174,13 @@ def get_elements_and_type_map( type_map[obj.uuid] = typ _fix_components(elements, type_map) + elements["Diagram"] = diagrams = [ + diagram + for diagram in ctx["MODEL"].diagrams + if diagram.uuid in ctx["POLARION_ID_MAP"] + ] + for diag in diagrams: + type_map[diag.uuid] = "Diagram" return elements, type_map @@ -150,7 +224,7 @@ def _fix_components( elements["PhysicalComponent"] = components -def make_model_elements_index(ctx: dict[str, t.Any]) -> pathlib.Path: +def make_model_elements_index(ctx: dict[str, t.Any]) -> None: """Create an elements index file for all migrated elements.""" elements: list[dict[str, t.Any]] = [] for obj in chain.from_iterable(ctx["ELEMENTS"].values()): @@ -176,7 +250,12 @@ def make_model_elements_index(ctx: dict[str, t.Any]) -> pathlib.Path: elements.append(element_) ELEMENTS_IDX_PATH.write_text(yaml.dump(elements), encoding="utf8") - return ELEMENTS_IDX_PATH -from . import diagram, element, helpers, serialize +from . import ( # pylint: disable=cyclic-import + api_helper, + diagram, + element, + helpers, + serialize, +) diff --git a/capella2polarion/elements/api_helper.py b/capella2polarion/elements/api_helper.py index 0c417822..9d0bb685 100644 --- a/capella2polarion/elements/api_helper.py +++ b/capella2polarion/elements/api_helper.py @@ -48,26 +48,63 @@ def patch_work_item( _type The type of element, which should be shown in log messages. """ - logger.debug( - "Update work item %r for model %s %r...", - wid, - _type, - name, - ) if new := serialize.element(obj, ctx, serializer): uuid = obj["uuid"] if isinstance(obj, dict) else obj.uuid old: serialize.CapellaWorkItem = ctx["POLARION_WI_MAP"][uuid] if new.checksum == old.checksum: return + log_args = (wid, _type, name) + logger.debug("Update work item %r for model %s %r...", *log_args) if new.uuid_capella: del new.additional_attributes["uuid_capella"] + old.links = ctx["API"].get_all_work_item_links(old.id) new.type = None new.status = "open" new.id = wid try: ctx["API"].update_work_item(new) + + nlinks, dlinks = get_new_and_dead_links(old.links, new.links) + for link in dlinks: + log_args = (_get_link_id(link), _type, name) + logger.debug( + "Delete work item link %r for model %s %r", *log_args + ) + if dlinks: + ctx["API"].delete_work_item_links(dlinks) + + for link in nlinks: + log_args = (_get_link_id(link), _type, name) + logger.debug( + "Create work item link %r for model %s %r", *log_args + ) + if nlinks: + ctx["API"].create_work_item_links(nlinks) except polarion_api.PolarionApiException as error: wi = f"{wid}({_type} {name})" logger.error("Updating work item %r failed. %s", wi, error.args[0]) + + +def get_new_and_dead_links( + new_links: cabc.Iterable[polarion_api.WorkItemLink], + old_links: cabc.Iterable[polarion_api.WorkItemLink], +) -> tuple[list[polarion_api.WorkItemLink], list[polarion_api.WorkItemLink]]: + """Return new work item links for ceate and dead links for delete.""" + news = {_get_link_id(link): link for link in new_links} + olds = {_get_link_id(link): link for link in old_links} + nlinks = [news[lid] for lid in set(news) - set(olds)] + dlinks = [olds[lid] for lid in set(olds) - set(news)] + return nlinks, dlinks + + +def _get_link_id(link: polarion_api.WorkItemLink) -> str: + return "/".join( + ( + link.primary_work_item_id, + link.role, + link.secondary_work_item_project, + link.secondary_work_item_id, + ) + ) diff --git a/capella2polarion/elements/element.py b/capella2polarion/elements/element.py index 616065ed..d71da51d 100644 --- a/capella2polarion/elements/element.py +++ b/capella2polarion/elements/element.py @@ -11,17 +11,21 @@ import polarion_rest_api_client as polarion_api from capellambse.model import common -from capella2polarion.elements import POL2CAPELLA_TYPES, api_helper, serialize +from capella2polarion import elements +from capella2polarion.elements import serialize logger = logging.getLogger(__name__) TYPE_RESOLVERS = {"Part": lambda obj: obj.type.uuid} TYPES_POL2CAPELLA = { - ctype: ptype for ptype, ctype in POL2CAPELLA_TYPES.items() + ctype: ptype for ptype, ctype in elements.POL2CAPELLA_TYPES.items() } -def create_work_items(ctx: dict[str, t.Any]) -> None: +def create_work_items( + ctx: dict[str, t.Any], + objects: cabc.Iterable[common.GenericElement] | None = None, +) -> list[common.GenericElement]: """Create a set of work items in Polarion.""" def serialize_for_create( @@ -32,34 +36,13 @@ def serialize_for_create( ) return serialize.element(obj, ctx, serialize.generic_work_item) - objects = chain.from_iterable(ctx["ELEMENTS"].values()) + objects = objects or chain.from_iterable(ctx["ELEMENTS"].values()) work_items = [ serialize_for_create(obj) for obj in objects if obj.uuid not in ctx["POLARION_ID_MAP"] ] - work_items = list(filter(None.__ne__, work_items)) - if work_items: - try: - ctx["API"].create_work_items(work_items) - except polarion_api.PolarionApiException as error: - logger.error("Creating work items failed. %s", error.args[0]) - - -def update_work_items(ctx: dict[str, t.Any]) -> None: - """Update a set of work items in Polarion.""" - for obj in chain.from_iterable(ctx["ELEMENTS"].values()): - if obj.uuid not in ctx["POLARION_ID_MAP"]: - continue - - api_helper.patch_work_item( - ctx, - ctx["POLARION_ID_MAP"][obj.uuid], - obj, - serialize.generic_work_item, - obj._short_repr_(), - "element", - ) + return list(filter(None.__ne__, work_items)) class LinkBuilder(t.NamedTuple): @@ -88,62 +71,31 @@ def create( ) -def update_links( - ctx: dict[str, t.Any], - elements: cabc.Iterable[common.GenericElement] | None = None, -) -> None: - """Create and update work item links in Polarion.""" +def create_links( + obj: common.GenericElement, ctx: dict[str, t.Any] +) -> list[polarion_api.WorkItemLink]: + """Create work item links in Polarion.""" custom_link_resolvers = CUSTOM_LINKS reverse_type_map = TYPES_POL2CAPELLA - for elt in elements or chain.from_iterable(ctx["ELEMENTS"].values()): - if elt.uuid not in ctx["POLARION_ID_MAP"]: + link_builder = LinkBuilder(ctx, obj) + ptype = reverse_type_map.get(type(obj).__name__, type(obj).__name__) + new_links: list[polarion_api.WorkItemLink] = [] + for role_id in ctx["ROLES"].get(ptype, []): + if resolver := custom_link_resolvers.get(role_id): + new_links.extend(resolver(link_builder, role_id, {})) continue - workitem_id = ctx["POLARION_ID_MAP"][elt.uuid] - logger.debug( - "Fetching links for work item %r(%r)...", - workitem_id, - elt._short_repr_(), - ) - links: list[polarion_api.WorkItemLink] - try: - links = ctx["API"].get_all_work_item_links(workitem_id) - except polarion_api.PolarionApiException as error: - logger.error( - "Fetching links for work item %r(%r). failed %s", - workitem_id, - elt._short_repr_(), - error.args[0], - ) + if (refs := getattr(obj, role_id, None)) is None: continue - link_builder = LinkBuilder(ctx, elt) - ptype = reverse_type_map.get(type(elt).__name__, type(elt).__name__) - for role_id in ctx["ROLES"].get(ptype, []): - id_link_map: dict[str, polarion_api.WorkItemLink] = {} - for link in links: - if role_id != link.role: - continue - - id_link_map[link.secondary_work_item_id] = link - - if resolver := custom_link_resolvers.get(role_id): - resolver(link_builder, role_id, id_link_map) - continue - - if (refs := getattr(elt, role_id, None)) is None: - continue - - if isinstance(refs, common.ElementList): - new = refs.by_uuid - else: - assert hasattr(refs, "uuid") - new = [refs.uuid] + if isinstance(refs, common.ElementList): + new = refs.by_uuid + else: + assert hasattr(refs, "uuid") + new = [refs.uuid] - new = set(_get_work_item_ids(ctx, new, role_id)) - _handle_create_and_delete( - link_builder, role_id, new, id_link_map, id_link_map - ) + new_links.extend(_create(link_builder, role_id, new, {})) + return new_links def _get_work_item_ids( @@ -166,26 +118,28 @@ def _handle_description_reference_links( link_builder: LinkBuilder, role_id: str, links: dict[str, polarion_api.WorkItemLink], -) -> None: +) -> list[polarion_api.WorkItemLink]: refs = link_builder.context["DESCR_REFERENCES"].get(link_builder.obj.uuid) refs = set(_get_work_item_ids(link_builder.context, refs, role_id)) - _handle_create_and_delete(link_builder, role_id, refs, links, links) + return _create(link_builder, role_id, refs, links) def _handle_diagram_reference_links( link_builder: LinkBuilder, role_id: str, links: dict[str, polarion_api.WorkItemLink], -) -> None: +) -> list[polarion_api.WorkItemLink]: try: refs = set(_collect_uuids(link_builder.obj.nodes)) refs = set(_get_work_item_ids(link_builder.context, refs, role_id)) - _handle_create_and_delete(link_builder, role_id, refs, links, links) + ref_links = _create(link_builder, role_id, refs, links) except StopIteration: logger.exception( "Could not create links for diagram %r", link_builder.obj._short_repr_(), ) + ref_links = [] + return ref_links def _collect_uuids(nodes: list[common.GenericElement]) -> cabc.Iterator[str]: @@ -198,29 +152,16 @@ def _collect_uuids(nodes: list[common.GenericElement]) -> cabc.Iterator[str]: yield uuid -def _handle_create_and_delete( +def _create( link_builder: LinkBuilder, role_id: str, new: cabc.Iterable[str], old: cabc.Iterable[str], - links: dict[str, t.Any], -) -> None: - create = set(new) - set(old) - new_links = [link_builder.create(id, role_id) for id in create] - new_links = list(filter(None.__ne__, new_links)) - if new_links: - link_builder.context["API"].create_work_item_links(new_links) - - delete = set(old) - set(new) - dead_links = [links.get(id) for id in delete] - dead_links = list(filter(None.__ne__, dead_links)) - for link in dead_links: - rep = link_builder.obj._short_repr_() - logger.debug( - "Delete work item link %r for model element %r", link, rep - ) - if dead_links: - link_builder.context["API"].delete_work_item_links(dead_links) +) -> list[polarion_api.WorkItemLink]: + new = set(new) - set(old) + new = set(_get_work_item_ids(link_builder.context, new, role_id)) + _new_links = [link_builder.create(id, role_id) for id in new] + return list(filter(None.__ne__, _new_links)) CUSTOM_LINKS = { diff --git a/capella2polarion/elements/serialize.py b/capella2polarion/elements/serialize.py index 43da9836..ca10faf5 100644 --- a/capella2polarion/elements/serialize.py +++ b/capella2polarion/elements/serialize.py @@ -5,8 +5,6 @@ import base64 import collections.abc as cabc -import hashlib -import json import logging import mimetypes import pathlib @@ -52,31 +50,6 @@ class Condition(t.TypedDict): checksum: str | None -def _convert_work_item_to_dict(work_item: CapellaWorkItem) -> dict[str, t.Any]: - """Convert the instance to a dictionary.""" - return { - "id": work_item.id, - "title": work_item.title, - "description_type": work_item.description_type, - "description": work_item.description, - "type": work_item.type, - "status": work_item.status, - "additional_attributes": work_item.additional_attributes, - "checksum": work_item.checksum, - } - - -def _calculate_checksum(payload: dict[str, t.Any]) -> str: - """Return the ``checksum`` of this CapellaWorkItem.""" - converted = json.dumps(payload).encode("utf8") - return hashlib.sha256(converted).hexdigest() - - -def _condition(html: bool, value: str) -> CapellaWorkItem.Condition: - _type = "text/html" if html else "text/plain" - return {"type": _type, "value": value} - - def element( obj: dict[str, t.Any] | common.GenericElement, ctx: dict[str, t.Any], @@ -105,7 +78,7 @@ def diagram(diag: dict[str, t.Any], ctx: dict[str, t.Any]) -> CapellaWorkItem: description=description, status="open", uuid_capella=diag["uuid"], - checksum=_calculate_checksum({"description": description}), + links=[], ) @@ -131,11 +104,7 @@ def generic_work_item( """Return a work item for the given model element.""" xtype = ctx["POLARION_TYPE_MAP"].get(obj.uuid, type(obj).__name__) serializer = SERIALIZERS.get(xtype, _generic_work_item) - work_item = serializer(obj, ctx) - wi_dict = _convert_work_item_to_dict(work_item) - del wi_dict["checksum"] - work_item.checksum = _calculate_checksum(wi_dict) - return work_item + return serializer(obj, ctx) def _generic_work_item( @@ -152,6 +121,7 @@ def _generic_work_item( description=value, status="open", uuid_capella=obj.uuid, + links=[], ) @@ -163,7 +133,6 @@ def _sanitize_description( lambda match: replace_markup(match, ctx, referenced_uuids), descr ) - # XXX: Can be removed after fix in capellambse def repair_images(node: etree._Element) -> None: if node.tag != "img": return @@ -242,6 +211,11 @@ def matcher(match: re.Match) -> str: return work_item +def _condition(html: bool, value: str) -> CapellaWorkItem.Condition: + _type = "text/html" if html else "text/plain" + return {"type": _type, "value": value} + + def component_or_actor( obj: cs.Component, ctx: dict[str, t.Any] ) -> CapellaWorkItem: @@ -251,9 +225,7 @@ def component_or_actor( xtype = RE_CAMEL_CASE_2ND_WORD_PATTERN.sub( r"\1Actor", type(obj).__name__ ) - # pylint: disable=attribute-defined-outside-init work_item.type = helpers.resolve_element_type(xtype) - # pylint: enable=attribute-defined-outside-init return work_item @@ -264,9 +236,7 @@ def physical_component( work_item = component_or_actor(obj, ctx) xtype = work_item.type if obj.nature is not None: - # pylint: disable=attribute-defined-outside-init work_item.type = f"{xtype}{obj.nature.name.capitalize()}" - # pylint: enable=attribute-defined-outside-init return work_item From 07db611ba677d8956a9be7167cbc279b7f60a645 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernst=20W=C3=BCrger?= Date: Wed, 30 Aug 2023 07:39:03 +0200 Subject: [PATCH 04/22] refactor(model-elements): Generalization of a helper function --- capella2polarion/elements/api_helper.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/capella2polarion/elements/api_helper.py b/capella2polarion/elements/api_helper.py index 9d0bb685..0dd77f4b 100644 --- a/capella2polarion/elements/api_helper.py +++ b/capella2polarion/elements/api_helper.py @@ -66,7 +66,7 @@ def patch_work_item( try: ctx["API"].update_work_item(new) - nlinks, dlinks = get_new_and_dead_links(old.links, new.links) + dlinks = get_links(old.links, new.links) for link in dlinks: log_args = (_get_link_id(link), _type, name) logger.debug( @@ -75,6 +75,7 @@ def patch_work_item( if dlinks: ctx["API"].delete_work_item_links(dlinks) + nlinks = get_links(new.links, old.links) for link in nlinks: log_args = (_get_link_id(link), _type, name) logger.debug( @@ -87,16 +88,14 @@ def patch_work_item( logger.error("Updating work item %r failed. %s", wi, error.args[0]) -def get_new_and_dead_links( - new_links: cabc.Iterable[polarion_api.WorkItemLink], - old_links: cabc.Iterable[polarion_api.WorkItemLink], -) -> tuple[list[polarion_api.WorkItemLink], list[polarion_api.WorkItemLink]]: +def get_links( + left: cabc.Iterable[polarion_api.WorkItemLink], + right: cabc.Iterable[polarion_api.WorkItemLink], +) -> list[polarion_api.WorkItemLink]: """Return new work item links for ceate and dead links for delete.""" - news = {_get_link_id(link): link for link in new_links} - olds = {_get_link_id(link): link for link in old_links} - nlinks = [news[lid] for lid in set(news) - set(olds)] - dlinks = [olds[lid] for lid in set(olds) - set(news)] - return nlinks, dlinks + news = {_get_link_id(link): link for link in left} + olds = {_get_link_id(link): link for link in right} + return [news[lid] for lid in set(news) - set(olds)] def _get_link_id(link: polarion_api.WorkItemLink) -> str: From 8d1cf17fee73c1b1fb286d0eade298e06f161ab8 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Thu, 31 Aug 2023 13:30:58 +0200 Subject: [PATCH 05/22] fix!: Merge diagrams into model-elements synchronization command The diagram-cache index is needed when updating diagrams and model- elements is happening in the same function call. --- capella2polarion/__main__.py | 51 +++-- capella2polarion/elements/__init__.py | 61 ++++-- capella2polarion/elements/api_helper.py | 76 ++++---- capella2polarion/elements/diagram.py | 30 +-- capella2polarion/elements/element.py | 119 ++++++------ capella2polarion/elements/serialize.py | 2 - tests/test_cli.py | 60 ++---- tests/test_elements.py | 238 ++++++++++++++++-------- 8 files changed, 341 insertions(+), 296 deletions(-) diff --git a/capella2polarion/__main__.py b/capella2polarion/__main__.py index ccf802cf..46bd34a0 100644 --- a/capella2polarion/__main__.py +++ b/capella2polarion/__main__.py @@ -130,12 +130,14 @@ def cli( polarion_api_endpoint=f"{ctx.obj['POLARION_HOST']}/rest/v1", polarion_access_token=os.environ["POLARION_PAT"], custom_work_item=serialize.CapellaWorkItem, + add_work_item_checksum=True, ) if not ctx.obj["API"].project_exists(): sys.exit(1) @cli.command() +@click.argument("model", type=cli_helpers.ModelCLI()) @click.argument( "diagram_cache", type=click.Path( @@ -146,10 +148,16 @@ def cli( path_type=pathlib.Path, ), ) +@click.argument("config_file", type=click.File(mode="r", encoding="utf8")) @click.pass_context -def diagrams(ctx: click.core.Context, diagram_cache: pathlib.Path) -> None: - """Synchronise diagrams.""" - logger.debug( +def model_elements( + ctx: click.core.Context, + model: capellambse.MelodyModel, + diagram_cache: pathlib.Path, + config_file: t.TextIO, +) -> None: + """Synchronise model elements.""" + logger.info( "Synchronising diagrams from diagram cache at '%s' " "to Polarion project with id %r...", diagram_cache, @@ -162,32 +170,8 @@ def diagrams(ctx: click.core.Context, diagram_cache: pathlib.Path) -> None: ctx.obj["DIAGRAM_CACHE"] = diagram_cache ctx.obj["DIAGRAM_IDX"] = json.loads(idx_file.read_text(encoding="utf8")) - ctx.obj["CAPELLA_UUIDS"] = [ - d["uuid"] for d in ctx.obj["DIAGRAM_IDX"] if d["success"] - ] - ctx.obj["POLARION_WI_MAP"] = elements.get_polarion_wi_map( - ctx.obj, "diagram" - ) - ctx.obj["POLARION_ID_MAP"] = { - uuid: wi.id for uuid, wi in ctx.obj["POLARION_WI_MAP"].items() - } - - elements.delete_work_items(ctx.obj) - elements.diagram.create_diagrams(ctx.obj) - elements.diagram.update_diagrams(ctx.obj) - -@cli.command() -@click.argument("model", type=cli_helpers.ModelCLI()) -@click.argument("config_file", type=click.File(mode="r", encoding="utf8")) -@click.pass_context -def model_elements( - ctx: click.core.Context, - model: capellambse.MelodyModel, - config_file: t.TextIO, -) -> None: - """Synchronise model elements.""" - logger.debug( + logger.info( "Synchronising model elements (%r) to Polarion project with id %r...", str(elements.ELEMENTS_IDX_PATH), ctx.obj["PROJECT_ID"], @@ -205,10 +189,17 @@ def model_elements( ctx.obj["POLARION_ID_MAP"] = { uuid: wi.id for uuid, wi in ctx.obj["POLARION_WI_MAP"].items() } + diagrams = ctx.obj["ELEMENTS"].pop("Diagram", []) + work_items = elements.element.create_work_items(ctx.obj) + ctx.obj["ELEMENTS"]["Diagram"] = diagrams + pdiagrams = elements.diagram.create_diagrams(ctx.obj) + ctx.obj["WORK_ITEMS"] = { + wi.uuid_capella: wi for wi in work_items + pdiagrams + } elements.delete_work_items(ctx.obj) - elements.create_work_items(ctx.obj) - elements.update_work_items(ctx.obj) + elements.post_work_items(ctx.obj) + elements.patch_work_items(ctx.obj) elements.make_model_elements_index(ctx.obj) diff --git a/capella2polarion/elements/__init__.py b/capella2polarion/elements/__init__.py index a5cba84e..c08f97ff 100644 --- a/capella2polarion/elements/__init__.py +++ b/capella2polarion/elements/__init__.py @@ -21,6 +21,7 @@ import polarion_rest_api_client as polarion_api import yaml from capellambse.model import common +from capellambse.model import diagram as diag logger = logging.getLogger(__name__) @@ -35,7 +36,7 @@ "PhysicalComponentNode": "PhysicalComponent", "PhysicalComponentBehavior": "PhysicalComponent", } -POL2CAPELLA_TYPES = ( +POL2CAPELLA_TYPES: dict[str, str] = ( { "OperationalEntity": "Entity", "OperationalInteraction": "FunctionalExchange", @@ -74,7 +75,7 @@ def delete_work_items(ctx: dict[str, t.Any]) -> None: """ def serialize_for_delete(uuid: str) -> str: - logger.debug( + logger.info( "Delete work item %r...", workitem_id := ctx["POLARION_ID_MAP"][uuid], ) @@ -94,22 +95,30 @@ def serialize_for_delete(uuid: str) -> str: logger.error("Deleting work items failed. %s", error.args[0]) -def create_work_items(ctx: dict[str, t.Any]) -> None: - """Create work items for a Polarion project. +def post_work_items(ctx: dict[str, t.Any]) -> None: + """Post work items in a Polarion project. Parameters ---------- ctx The context for the workitem operation to be processed. """ - if work_items := element.create_work_items(ctx): + work_items = [ + wi + for wi in ctx["WORK_ITEMS"].values() + if wi.uuid_capella not in ctx["POLARION_ID_MAP"] + ] + for work_item in work_items: + assert work_item is not None + logger.info("Create work item for %r...", work_item.title) + if work_items: try: ctx["API"].create_work_items(work_items) except polarion_api.PolarionApiException as error: logger.error("Creating work items failed. %s", error.args[0]) -def update_work_items(ctx: dict[str, t.Any]) -> None: +def patch_work_items(ctx: dict[str, t.Any]) -> None: """Update work items in a Polarion project. Parameters @@ -118,10 +127,12 @@ def update_work_items(ctx: dict[str, t.Any]) -> None: The context for the workitem operation to be processed. """ - def prepare_for_update( - obj: common.GenericElement, ctx: dict[str, t.Any], **kwargs + def add_content( + obj: common.GenericElement | diag.Diagram, + ctx: dict[str, t.Any], + **kwargs, ) -> serialize.CapellaWorkItem: - work_item = serialize.generic_work_item(obj, ctx) + work_item = ctx["WORK_ITEMS"][obj.uuid] for key, value in kwargs.items(): if getattr(work_item, key, None) is None: continue @@ -129,17 +140,28 @@ def prepare_for_update( setattr(work_item, key, value) return work_item - for obj in chain.from_iterable(ctx["ELEMENTS"].values()): - if obj.uuid not in ctx["POLARION_ID_MAP"]: + ctx["POLARION_WI_MAP"] = get_polarion_wi_map(ctx) + ctx["POLARION_ID_MAP"] = uuids = { + uuid: wi.id + for uuid, wi in ctx["POLARION_WI_MAP"].items() + if wi.status == "open" and uuid in ctx["WORK_ITEMS"] + } + for uuid in uuids: + elements = ctx["MODEL"] + if uuid.startswith("_"): + elements = ctx["MODEL"].diagrams + try: + obj = elements.by_uuid(uuid) + except KeyError: + logger.error("Weird %r", uuid) continue links = element.create_links(obj, ctx) api_helper.patch_work_item( ctx, - ctx["POLARION_ID_MAP"][obj.uuid], obj, - functools.partial(prepare_for_update, links=links), + functools.partial(add_content, linked_work_items=links), obj._short_repr_(), "element", ) @@ -174,13 +196,14 @@ def get_elements_and_type_map( type_map[obj.uuid] = typ _fix_components(elements, type_map) - elements["Diagram"] = diagrams = [ - diagram - for diagram in ctx["MODEL"].diagrams - if diagram.uuid in ctx["POLARION_ID_MAP"] + diagrams_from_cache = { + d["uuid"] for d in ctx["DIAGRAM_IDX"] if d["success"] + } + elements["Diagram"] = [ + d for d in ctx["MODEL"].diagrams if d.uuid in diagrams_from_cache ] - for diag in diagrams: - type_map[diag.uuid] = "Diagram" + for obj in elements["Diagram"]: + type_map[obj.uuid] = "Diagram" return elements, type_map diff --git a/capella2polarion/elements/api_helper.py b/capella2polarion/elements/api_helper.py index 0dd77f4b..d72403f7 100644 --- a/capella2polarion/elements/api_helper.py +++ b/capella2polarion/elements/api_helper.py @@ -13,19 +13,10 @@ logger = logging.getLogger(__name__) -class Diagram(t.TypedDict): - """A Diagram object from the Diagram Cache Index.""" - - uuid: str - name: str - success: bool - - def patch_work_item( ctx: dict[str, t.Any], - wid: str, - obj: common.GenericElement | Diagram, - serializer: cabc.Callable[ + obj: common.GenericElement, + receiver: cabc.Callable[ [t.Any, dict[str, t.Any]], serialize.CapellaWorkItem ], name: str, @@ -37,57 +28,64 @@ def patch_work_item( ---------- ctx The context to execute the patch for. - wid - The ID of the polarion WorkItem obj The Capella object to update the WorkItem from - serializer - The serializer, which should be used to create the WorkItem. + receiver + A function that receives the WorkItem from the created + instances. name The name of the object, which should be displayed in log messages. _type The type of element, which should be shown in log messages. """ - if new := serialize.element(obj, ctx, serializer): - uuid = obj["uuid"] if isinstance(obj, dict) else obj.uuid - old: serialize.CapellaWorkItem = ctx["POLARION_WI_MAP"][uuid] - if new.checksum == old.checksum: + if new := receiver(obj, ctx): + wid = ctx["POLARION_ID_MAP"][obj.uuid] + old: serialize.CapellaWorkItem = ctx["POLARION_WI_MAP"][obj.uuid] + if new == old: return log_args = (wid, _type, name) - logger.debug("Update work item %r for model %s %r...", *log_args) + logger.info("Update work item %r for model %s %r...", *log_args) if new.uuid_capella: del new.additional_attributes["uuid_capella"] - old.links = ctx["API"].get_all_work_item_links(old.id) + old.linked_work_items = ctx["API"].get_all_work_item_links(old.id) new.type = None new.status = "open" new.id = wid try: ctx["API"].update_work_item(new) - - dlinks = get_links(old.links, new.links) - for link in dlinks: - log_args = (_get_link_id(link), _type, name) - logger.debug( - "Delete work item link %r for model %s %r", *log_args - ) - if dlinks: - ctx["API"].delete_work_item_links(dlinks) - - nlinks = get_links(new.links, old.links) - for link in nlinks: - log_args = (_get_link_id(link), _type, name) - logger.debug( - "Create work item link %r for model %s %r", *log_args - ) - if nlinks: - ctx["API"].create_work_item_links(nlinks) + handle_links( + old.linked_work_items, + new.linked_work_items, + ("Delete", _type, name), + ctx["API"].delete_work_item_links, + ) + handle_links( + new.linked_work_items, + old.linked_work_items, + ("Create", _type, name), + ctx["API"].create_work_item_links, + ) except polarion_api.PolarionApiException as error: wi = f"{wid}({_type} {name})" logger.error("Updating work item %r failed. %s", wi, error.args[0]) +def handle_links( + left: cabc.Iterable[polarion_api.WorkItemLink], + right: cabc.Iterable[polarion_api.WorkItemLink], + log_args: tuple[str, ...], + handler: cabc.Callable[[cabc.Iterable[polarion_api.WorkItemLink]], None], +): + """Handle work item links on Polarion.""" + for link in (links := get_links(left, right)): + largs = (log_args[0], _get_link_id(link), *log_args[1:]) + logger.info("%s work item link %r for model %s %r", *largs) + if links: + handler(links) + + def get_links( left: cabc.Iterable[polarion_api.WorkItemLink], right: cabc.Iterable[polarion_api.WorkItemLink], diff --git a/capella2polarion/elements/diagram.py b/capella2polarion/elements/diagram.py index 91ee2369..33c6c863 100644 --- a/capella2polarion/elements/diagram.py +++ b/capella2polarion/elements/diagram.py @@ -6,39 +6,17 @@ import logging import typing as t -import polarion_rest_api_client as polarion_api - -from capella2polarion.elements import api_helper, serialize +from capella2polarion.elements import serialize logger = logging.getLogger(__name__) -def create_diagrams(ctx: dict[str, t.Any]) -> None: - """Create a set of work items of type ``diagram`` in Polarion.""" +def create_diagrams(ctx: dict[str, t.Any]) -> list[serialize.CapellaWorkItem]: + """Return a set of new work items of type ``diagram``.""" uuids = set(ctx["CAPELLA_UUIDS"]) - set(ctx["POLARION_ID_MAP"]) diagrams = [diag for diag in ctx["DIAGRAM_IDX"] if diag["uuid"] in uuids] work_items = [ serialize.element(diagram, ctx, serialize.diagram) for diagram in diagrams ] - work_items = list(filter(None.__ne__, work_items)) - for work_item in work_items: - assert work_item is not None - logger.debug("Create work item for diagram %r...", work_item.title) - if work_items: - try: - ctx["API"].create_work_items(work_items) - except polarion_api.PolarionApiException as error: - logger.error("Creating diagrams failed. %s", error.args[0]) - - -def update_diagrams(ctx: dict[str, t.Any]) -> None: - """Update a set of work items of type ``diagram`` in Polarion.""" - uuids: set[str] = set(ctx["POLARION_ID_MAP"]) & set(ctx["CAPELLA_UUIDS"]) - diagrams = {d["uuid"]: d for d in ctx["DIAGRAM_IDX"] if d["uuid"] in uuids} - for uuid in uuids: - wid = ctx["POLARION_ID_MAP"][uuid] - diagram = diagrams[uuid] - api_helper.patch_work_item( - ctx, wid, diagram, serialize.diagram, diagram["name"], "diagram" - ) + return list(filter(None.__ne__, work_items)) # type:ignore[arg-type] diff --git a/capella2polarion/elements/element.py b/capella2polarion/elements/element.py index d71da51d..516409c7 100644 --- a/capella2polarion/elements/element.py +++ b/capella2polarion/elements/element.py @@ -10,6 +10,7 @@ import polarion_rest_api_client as polarion_api from capellambse.model import common +from capellambse.model import diagram as diag from capella2polarion import elements from capella2polarion.elements import serialize @@ -23,69 +24,45 @@ def create_work_items( - ctx: dict[str, t.Any], - objects: cabc.Iterable[common.GenericElement] | None = None, -) -> list[common.GenericElement]: + ctx: dict[str, t.Any] +) -> list[serialize.CapellaWorkItem]: """Create a set of work items in Polarion.""" - - def serialize_for_create( - obj: common.GenericElement, - ) -> serialize.CapellaWorkItem | None: - logger.debug( - "Create work item for model element %r...", obj._short_repr_() - ) - return serialize.element(obj, ctx, serialize.generic_work_item) - - objects = objects or chain.from_iterable(ctx["ELEMENTS"].values()) + objects = chain.from_iterable(ctx["ELEMENTS"].values()) work_items = [ - serialize_for_create(obj) + serialize.element(obj, ctx, serialize.generic_work_item) for obj in objects - if obj.uuid not in ctx["POLARION_ID_MAP"] ] - return list(filter(None.__ne__, work_items)) - - -class LinkBuilder(t.NamedTuple): - """Helper class for creating workitem links.""" - - context: dict[str, t.Any] - obj: common.GenericElement - - def create( - self, secondary_id: str, role_id: str - ) -> polarion_api.WorkItemLink | None: - """Post a work item link create request.""" - primary_id = self.context["POLARION_ID_MAP"][self.obj.uuid] - logger.debug( - "Create work item link %r from %r to %r for model element %r", - role_id, - primary_id, - secondary_id, - self.obj._short_repr_(), - ) - return polarion_api.WorkItemLink( - primary_id, - secondary_id, - role_id, - secondary_work_item_project=self.context["PROJECT_ID"], - ) + return list(filter(None.__ne__, work_items)) # type: ignore[arg-type] def create_links( - obj: common.GenericElement, ctx: dict[str, t.Any] + obj: common.GenericElement | diag.Diagram, ctx: dict[str, t.Any] ) -> list[polarion_api.WorkItemLink]: - """Create work item links in Polarion.""" + """Create work item links for a given Capella object.""" custom_link_resolvers = CUSTOM_LINKS reverse_type_map = TYPES_POL2CAPELLA - link_builder = LinkBuilder(ctx, obj) + if isinstance(obj, diag.Diagram): + repres = f"" + else: + repres = obj._short_repr_() + + wid = ctx["POLARION_ID_MAP"][obj.uuid] ptype = reverse_type_map.get(type(obj).__name__, type(obj).__name__) new_links: list[polarion_api.WorkItemLink] = [] for role_id in ctx["ROLES"].get(ptype, []): if resolver := custom_link_resolvers.get(role_id): - new_links.extend(resolver(link_builder, role_id, {})) + new_links.extend(resolver(ctx, obj, role_id, {})) continue if (refs := getattr(obj, role_id, None)) is None: + logger.info( + "Unable to create work item link %r for [%s]. " + "There is no %r attribute on %s", + role_id, + wid, + role_id, + repres, + ) continue if isinstance(refs, common.ElementList): @@ -94,49 +71,57 @@ def create_links( assert hasattr(refs, "uuid") new = [refs.uuid] - new_links.extend(_create(link_builder, role_id, new, {})) + new = set(_get_work_item_ids(ctx, wid, new, role_id)) + new_links.extend(_create(ctx, wid, role_id, new, {})) return new_links def _get_work_item_ids( - ctx: dict[str, t.Any], uuids: cabc.Iterable[str], role_id: str + ctx: dict[str, t.Any], + primary_id: str, + uuids: cabc.Iterable[str], + role_id: str, ) -> cabc.Iterator[str]: for uuid in uuids: if wid := ctx["POLARION_ID_MAP"].get(uuid): yield wid else: obj = ctx["MODEL"].by_uuid(uuid) - logger.debug( - "Unable to create work item link %r. " + logger.info( + "Unable to create work item link %r for [%s]. " "Couldn't identify work item for %r", role_id, + primary_id, obj._short_repr_(), ) def _handle_description_reference_links( - link_builder: LinkBuilder, + context: dict[str, t.Any], + obj: common.GenericElement, role_id: str, links: dict[str, polarion_api.WorkItemLink], ) -> list[polarion_api.WorkItemLink]: - refs = link_builder.context["DESCR_REFERENCES"].get(link_builder.obj.uuid) - refs = set(_get_work_item_ids(link_builder.context, refs, role_id)) - return _create(link_builder, role_id, refs, links) + refs = context["DESCR_REFERENCES"].get(obj.uuid) + wid = context["POLARION_ID_MAP"][obj.uuid] + refs = set(_get_work_item_ids(context, wid, refs, role_id)) + return _create(context, wid, role_id, refs, links) def _handle_diagram_reference_links( - link_builder: LinkBuilder, + context: dict[str, t.Any], + obj: diag.Diagram, role_id: str, links: dict[str, polarion_api.WorkItemLink], ) -> list[polarion_api.WorkItemLink]: try: - refs = set(_collect_uuids(link_builder.obj.nodes)) - refs = set(_get_work_item_ids(link_builder.context, refs, role_id)) - ref_links = _create(link_builder, role_id, refs, links) + refs = set(_collect_uuids(obj.nodes)) + wid = context["POLARION_ID_MAP"][obj.uuid] + refs = set(_get_work_item_ids(context, wid, refs, role_id)) + ref_links = _create(context, wid, role_id, refs, links) except StopIteration: logger.exception( - "Could not create links for diagram %r", - link_builder.obj._short_repr_(), + "Could not create links for diagram %r", obj._short_repr_() ) ref_links = [] return ref_links @@ -153,14 +138,22 @@ def _collect_uuids(nodes: list[common.GenericElement]) -> cabc.Iterator[str]: def _create( - link_builder: LinkBuilder, + context: dict[str, t.Any], + primary_id: str, role_id: str, new: cabc.Iterable[str], old: cabc.Iterable[str], ) -> list[polarion_api.WorkItemLink]: new = set(new) - set(old) - new = set(_get_work_item_ids(link_builder.context, new, role_id)) - _new_links = [link_builder.create(id, role_id) for id in new] + _new_links = [ + polarion_api.WorkItemLink( + primary_id, + id, + role_id, + secondary_work_item_project=context["PROJECT_ID"], + ) + for id in new + ] return list(filter(None.__ne__, _new_links)) diff --git a/capella2polarion/elements/serialize.py b/capella2polarion/elements/serialize.py index ca10faf5..932b335c 100644 --- a/capella2polarion/elements/serialize.py +++ b/capella2polarion/elements/serialize.py @@ -78,7 +78,6 @@ def diagram(diag: dict[str, t.Any], ctx: dict[str, t.Any]) -> CapellaWorkItem: description=description, status="open", uuid_capella=diag["uuid"], - links=[], ) @@ -121,7 +120,6 @@ def _generic_work_item( description=value, status="open", uuid_capella=obj.uuid, - links=[], ) diff --git a/tests/test_cli.py b/tests/test_cli.py index 8ad34c9e..a766ff82 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -35,7 +35,9 @@ def prepare_cli_test( mock_api = mock.MagicMock(spec=polarion_api.OpenAPIPolarionProjectClient) monkeypatch.setattr(polarion_api, "OpenAPIPolarionProjectClient", mock_api) mock_get_polarion_wi_map = mock.MagicMock() - monkeypatch.setattr(main, "get_polarion_wi_map", mock_get_polarion_wi_map) + monkeypatch.setattr( + elements, "get_polarion_wi_map", mock_get_polarion_wi_map + ) if isinstance(return_value, cabc.Iterable) and not isinstance( return_value, (str, dict) ): @@ -47,35 +49,6 @@ def prepare_cli_test( return mock_get_polarion_wi_map -def test_migrate_diagrams(monkeypatch: pytest.MonkeyPatch): - mock_get_polarion_wi_map = prepare_cli_test( - monkeypatch, - { - "uuid1": polarion_api.WorkItem("project/W-1"), - "uuid2": polarion_api.WorkItem("project/W-2"), - }, - ) - mock_delete_work_items = mock.MagicMock() - monkeypatch.setattr(elements, "delete_work_items", mock_delete_work_items) - mock_update_diagrams = mock.MagicMock() - monkeypatch.setattr( - elements.diagram, "update_diagrams", mock_update_diagrams - ) - mock_create_diagrams = mock.MagicMock() - monkeypatch.setattr( - elements.diagram, "create_diagrams", mock_create_diagrams - ) - command = ["--project-id=project_id", "diagrams", str(TEST_DIAGRAM_CACHE)] - - result = testing.CliRunner().invoke(main.cli, command) - - assert result.exit_code == 0 - assert mock_get_polarion_wi_map.call_count == 1 - assert mock_delete_work_items.call_count == 1 - assert mock_update_diagrams.call_count == 1 - assert mock_create_diagrams.call_count == 1 - - def test_migrate_model_elements(monkeypatch: pytest.MonkeyPatch): mock_get_polarion_wi_map = prepare_cli_test( monkeypatch, @@ -94,32 +67,31 @@ def test_migrate_model_elements(monkeypatch: pytest.MonkeyPatch): {}, ), ) - mock_delete_work_items = mock.MagicMock() - monkeypatch.setattr(elements, "delete_work_items", mock_delete_work_items) - mock_update_work_items = mock.MagicMock() - monkeypatch.setattr( - elements.element, "update_work_items", mock_update_work_items - ) - mock_create_work_items = mock.MagicMock() + mock_create_diagrams = mock.MagicMock() monkeypatch.setattr( - elements.element, "create_work_items", mock_create_work_items + elements.diagram, "create_diagrams", mock_create_diagrams ) - mock_update_links = mock.MagicMock() - monkeypatch.setattr(elements.element, "update_links", mock_update_links) + mock_delete_work_items = mock.MagicMock() + monkeypatch.setattr(elements, "delete_work_items", mock_delete_work_items) + mock_post_work_items = mock.MagicMock() + monkeypatch.setattr(elements, "post_work_items", mock_post_work_items) + mock_patch_work_items = mock.MagicMock() + monkeypatch.setattr(elements, "patch_work_items", mock_patch_work_items) command = [ "--project-id=project_id", "model-elements", str(TEST_MODEL), + str(TEST_DIAGRAM_CACHE), str(TEST_MODEL_ELEMENTS_CONFIG), ] result = testing.CliRunner().invoke(main.cli, command) assert result.exit_code == 0 - assert mock_get_polarion_wi_map.call_count == 3 + assert mock_get_polarion_wi_map.call_count == 1 assert mock_delete_work_items.call_count == 1 - assert mock_update_work_items.call_count == 1 - assert mock_create_work_items.call_count == 1 - assert mock_update_links.call_count == 2 + assert mock_patch_work_items.call_count == 1 + assert mock_post_work_items.call_count == 1 + assert mock_create_diagrams.call_count == 1 assert ELEMENTS_IDX_PATH.exists() diff --git a/tests/test_elements.py b/tests/test_elements.py index dd2daeb3..5e56f8ca 100644 --- a/tests/test_elements.py +++ b/tests/test_elements.py @@ -3,6 +3,7 @@ from __future__ import annotations +import logging import typing as t from unittest import mock @@ -10,6 +11,7 @@ import markupsafe import polarion_rest_api_client as polarion_api import pytest +from capellambse.model import common from capella2polarion import elements from capella2polarion.elements import diagram, element, helpers, serialize @@ -46,12 +48,6 @@ TEST_DIAG_DESCR = ( '

FakeModelObject: + return element + def _short_repr_(self) -> str: return f"<{type(self).__name__} {self.name!r} ({self.uuid})>" @@ -189,10 +154,12 @@ class UnsupportedFakeModelObject(FakeModelObject): class TestModelElements: @staticmethod @pytest.fixture - def context() -> dict[str, t.Any]: + def context(model: capellambse.MelodyModel) -> dict[str, t.Any]: api = mock.MagicMock(spec=polarion_api.OpenAPIPolarionProjectClient) fake = FakeModelObject("uuid1", name="Fake 1") - work_item = serialize.CapellaWorkItem(id="Obj-1", uuid_capella="uuid1") + work_item = serialize.CapellaWorkItem( + id="Obj-1", uuid_capella="uuid1", status="open" + ) return { "API": api, "PROJECT_ID": "project_id", @@ -205,31 +172,34 @@ def context() -> dict[str, t.Any]: UnsupportedFakeModelObject("uuid3") ], }, + "MODEL": model, "POLARION_WI_MAP": {"uuid1": work_item}, "POLARION_ID_MAP": {"uuid1": "Obj-1"}, "POLARION_TYPE_MAP": {"uuid1": "FakeModelObject"}, "CONFIG": {}, "ROLES": {"FakeModelObject": ["attribute"]}, + "WORK_ITEMS": {}, } @staticmethod def test_create_work_items( monkeypatch: pytest.MonkeyPatch, context: dict[str, t.Any] ): + del context["ELEMENTS"]["UnsupportedFakeModelObject"] monkeypatch.setattr( serialize, "generic_work_item", mock_generic_work_item := mock.MagicMock(), ) mock_generic_work_item.side_effect = [ - wi_ := serialize.CapellaWorkItem( + expected := serialize.CapellaWorkItem( uuid_capella="uuid1", title="Fake 1", type="fakeModelObject", description_type="text/html", description=markupsafe.Markup(""), ), - wi_1 := serialize.CapellaWorkItem( + expected1 := serialize.CapellaWorkItem( uuid_capella="uuid2", title="Fake 2", type="fakeModelObject", @@ -238,27 +208,130 @@ def test_create_work_items( ), ] - element.create_work_items(context) + work_items = element.create_work_items(context) - assert context["API"].create_work_items.call_count == 1 - wi, wi1 = context["API"].create_work_items.call_args[0][0] - assert wi == wi_ # type: ignore[arg-type] - assert wi1 == wi_1 # type: ignore[arg-type] + assert work_items == [expected, expected1] @staticmethod - def test_update_work_items(context: dict[str, t.Any]): + def test_create_links_custom_resolver(context: dict[str, t.Any]): + obj = context["ELEMENTS"]["FakeModelObject"][1] + context["POLARION_ID_MAP"]["uuid2"] = "Obj-2" + context["ROLES"] = {"FakeModelObject": ["description_reference"]} + context["DESCR_REFERENCES"] = {"uuid2": ["uuid1"]} + expected = polarion_api.WorkItemLink( + "Obj-2", + "Obj-1", + "description_reference", + secondary_work_item_project="project_id", + ) + + links = element.create_links(obj, context) + + assert links == [expected] + + @staticmethod + def test_create_links_missing_attribute( + context: dict[str, t.Any], caplog: pytest.LogCaptureFixture + ): + obj = context["ELEMENTS"]["FakeModelObject"][0] + expected = ( + "Unable to create work item link 'attribute' for [Obj-1]. " + "There is no 'attribute' attribute on " + "" + ) + + with caplog.at_level(logging.DEBUG): + links = element.create_links(obj, context) + + assert not links + assert caplog.messages[0] == expected + + @staticmethod + def test_create_links_from_ElementList(context: dict[str, t.Any]): + fake = FakeModelObject("uuid4", name="Fake 4") + fake1 = FakeModelObject("uuid5", name="Fake 5") + obj = FakeModelObject( + "uuid6", + name="Fake 6", + attribute=common.ElementList( + context["MODEL"], [fake, fake1], FakeModelObject + ), + ) + context["ELEMENTS"]["FakeModelObject"].append(obj) + context["POLARION_ID_MAP"] |= { + f"uuid{i}": f"Obj-{i}" for i in range(4, 7) + } + expected_link = polarion_api.WorkItemLink( + "Obj-6", + "Obj-5", + "attribute", + secondary_work_item_project="project_id", + ) + expected_link1 = polarion_api.WorkItemLink( + "Obj-6", + "Obj-4", + "attribute", + secondary_work_item_project="project_id", + ) + + links = element.create_links(obj, context) + + assert expected_link in links + assert expected_link1 in links + + @staticmethod + def test_create_link_from_single_attribute(context: dict[str, t.Any]): + obj = context["ELEMENTS"]["FakeModelObject"][1] + context["POLARION_ID_MAP"]["uuid2"] = "Obj-2" + expected = polarion_api.WorkItemLink( + "Obj-2", + "Obj-1", + "attribute", + secondary_work_item_project="project_id", + ) + + links = element.create_links(obj, context) + + assert links == [expected] + + @staticmethod + def test_update_work_items( + monkeypatch: pytest.MonkeyPatch, context: dict[str, t.Any] + ): context["POLARION_WI_MAP"]["uuid1"] = serialize.CapellaWorkItem( id="Obj-1", type="type", uuid_capella="uuid1", + status="open", title="Something", description_type="text/html", description=markupsafe.Markup("Test"), checksum="123", ) + mock_get_polarion_wi_map = mock.MagicMock() + monkeypatch.setattr( + elements, "get_polarion_wi_map", mock_get_polarion_wi_map + ) + mock_get_polarion_wi_map.return_value = context["POLARION_WI_MAP"] + context["WORK_ITEMS"] = { + "uuid1": serialize.CapellaWorkItem( + id="Obj-1", + uuid_capella="uuid1", + title="Fake 1", + description_type="text/html", + description=markupsafe.Markup(""), + ) + } + context["MODEL"] = mock_model = mock.MagicMock() + mock_model.by_uuid.return_value = context["ELEMENTS"][ + "FakeModelObject" + ][0] - element.update_work_items(context) + elements.patch_work_items(context) + assert context["API"].get_all_work_item_links.call_count == 1 + assert context["API"].delete_work_item_links.call_count == 0 + assert context["API"].create_work_item_links.call_count == 0 assert context["API"].update_work_item.call_count == 1 work_item = context["API"].update_work_item.call_args[0][0] assert isinstance(work_item, serialize.CapellaWorkItem) @@ -278,7 +351,7 @@ def test_update_work_items_filters_work_items_with_same_checksum( checksum=TEST_WI_CHECKSUM, ) - element.update_work_items(context) + elements.patch_work_items(context) assert context["API"].update_work_item.call_count == 0 @@ -286,23 +359,45 @@ def test_update_work_items_filters_work_items_with_same_checksum( def test_update_links_with_no_elements(context: dict[str, t.Any]): context["POLARION_ID_MAP"] = {} - element.update_links(context) + elements.patch_work_items(context) assert context["API"].get_all_work_item_links.call_count == 0 @staticmethod - def test_update_links(context: dict[str, t.Any]): - context["POLARION_ID_MAP"]["uuid2"] = "Obj-2" - context["API"].get_all_work_item_links.return_value = [ - link := polarion_api.WorkItemLink( - "Obj-1", "Obj-2", "attribute", True, "project_id" - ) - ] + def test_update_links( + monkeypatch: pytest.MonkeyPatch, context: dict[str, t.Any] + ): + link = polarion_api.WorkItemLink( + "Obj-1", "Obj-2", "attribute", True, "project_id" + ) + context["POLARION_WI_MAP"]["uuid1"].linked_work_items = [link] + context["POLARION_WI_MAP"]["uuid2"] = serialize.CapellaWorkItem( + id="Obj-2", uuid_capella="uuid2", status="open" + ) + context["WORK_ITEMS"] = { + "uuid1": serialize.CapellaWorkItem( + id="Obj-1", uuid_capella="uuid1", status="open" + ), + "uuid2": serialize.CapellaWorkItem( + id="Obj-2", uuid_capella="uuid2", status="open" + ), + } + mock_get_polarion_wi_map = mock.MagicMock() + monkeypatch.setattr( + elements, "get_polarion_wi_map", mock_get_polarion_wi_map + ) + mock_get_polarion_wi_map.return_value = context["POLARION_WI_MAP"] + context["API"].get_all_work_item_links.side_effect = ( + [link], + [], + ) + context["MODEL"] = mock_model = mock.MagicMock() + mock_model.by_uuid.side_effect = context["ELEMENTS"]["FakeModelObject"] expected_new_link = polarion_api.WorkItemLink( "Obj-2", "Obj-1", "attribute", None, "project_id" ) - element.update_links(context) + elements.patch_work_items(context) links = context["API"].get_all_work_item_links.call_args_list assert context["API"].get_all_work_item_links.call_count == 2 @@ -340,7 +435,7 @@ def test_diagram(): title="test_diagram", description_type="text/html", status="open", - checksum=TEST_DIAG_CHECKSUM, + linked_work_items=[], ) @staticmethod @@ -464,11 +559,8 @@ def test_generic_work_item( "POLARION_TYPE_MAP": TEST_POL_TYPE_MAP, }, ) - checksum = work_item.checksum - del work_item.additional_attributes["checksum"] status = work_item.status work_item.status = None assert work_item == serialize.CapellaWorkItem(**expected) - assert isinstance(checksum, str) and checksum assert status == "open" From e5f47bbd466ae124ee8ee4f2a22b22bbcf648c6d Mon Sep 17 00:00:00 2001 From: ewuerger Date: Thu, 31 Aug 2023 14:46:14 +0200 Subject: [PATCH 06/22] fix: Fix diagram serialization Now diagrams are always created as Work Items and will be compared. --- capella2polarion/elements/diagram.py | 6 ++-- capella2polarion/elements/serialize.py | 36 +++++++++++++++---- ...zHzXCA.svg => _APMboAPhEeynfbzU12yy7w.svg} | 2 +- tests/data/diagram_cache/index.json | 4 +-- tests/test_elements.py | 34 +++++++++++++----- 5 files changed, 62 insertions(+), 20 deletions(-) rename tests/data/diagram_cache/{_6Td1kOQ8Ee2tXvmHzHzXCA.svg => _APMboAPhEeynfbzU12yy7w.svg} (99%) diff --git a/capella2polarion/elements/diagram.py b/capella2polarion/elements/diagram.py index 33c6c863..4c82436e 100644 --- a/capella2polarion/elements/diagram.py +++ b/capella2polarion/elements/diagram.py @@ -13,8 +13,10 @@ def create_diagrams(ctx: dict[str, t.Any]) -> list[serialize.CapellaWorkItem]: """Return a set of new work items of type ``diagram``.""" - uuids = set(ctx["CAPELLA_UUIDS"]) - set(ctx["POLARION_ID_MAP"]) - diagrams = [diag for diag in ctx["DIAGRAM_IDX"] if diag["uuid"] in uuids] + uuids = {diag["uuid"] for diag in ctx["DIAGRAM_IDX"] if diag["success"]} + diagrams = [ + diag for diag in ctx["ELEMENTS"]["Diagram"] if diag.uuid in uuids + ] work_items = [ serialize.element(diagram, ctx, serialize.diagram) for diagram in diagrams diff --git a/capella2polarion/elements/serialize.py b/capella2polarion/elements/serialize.py index 932b335c..4a62e73a 100644 --- a/capella2polarion/elements/serialize.py +++ b/capella2polarion/elements/serialize.py @@ -15,7 +15,8 @@ import polarion_rest_api_client as polarion_api from capellambse import helpers as chelpers from capellambse.model import common -from capellambse.model.crosslayer import cs, interaction +from capellambse.model import diagram as diagr +from capellambse.model.crosslayer import capellacore, cs, interaction from capellambse.model.layers import oa, pa from lxml import etree @@ -51,7 +52,7 @@ class Condition(t.TypedDict): def element( - obj: dict[str, t.Any] | common.GenericElement, + obj: diagr.Diagram | common.GenericElement, ctx: dict[str, t.Any], serializer: cabc.Callable[[t.Any, dict[str, t.Any]], CapellaWorkItem], ) -> CapellaWorkItem | None: @@ -63,9 +64,9 @@ def element( return None -def diagram(diag: dict[str, t.Any], ctx: dict[str, t.Any]) -> CapellaWorkItem: +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" + 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()) @@ -73,11 +74,11 @@ def diagram(diag: dict[str, t.Any], ctx: dict[str, t.Any]) -> CapellaWorkItem: description = f'

' return CapellaWorkItem( type="diagram", - title=diag["name"], + title=diag.name, description_type="text/html", description=description, status="open", - uuid_capella=diag["uuid"], + uuid_capella=diag.uuid, ) @@ -185,7 +186,7 @@ def include_pre_and_post_condition( def get_condition(cap: PrePostConditionElement, name: str) -> str: if not (condition := getattr(cap, name)): return "" - return condition.specification["capella:linkedText"].striptags() + return get_linked_text(condition, ctx) def strike_through(string: str) -> str: if match := RE_DESCR_DELETED_PATTERN.match(string): @@ -209,6 +210,26 @@ def matcher(match: re.Match) -> str: return work_item +def get_linked_text( + obj: capellacore.Constraint, ctx: dict[str, t.Any] +) -> markupsafe.Markup: + """Return sanitized markup of the given ``obj`` linked text.""" + description = obj.specification["capella:linkedText"].striptags() + uuids, value = _sanitize_description(description, ctx) + if uuids: + ctx.setdefault("DESCR_REFERENCES", {})[obj.uuid] = uuids + return value + + +def constraint( + obj: capellacore.Constraint, ctx: dict[str, t.Any] +) -> CapellaWorkItem: + """Return attributes for a ``Constraint``.""" + work_item = _generic_work_item(obj, ctx) + work_item.description = get_linked_text(obj, ctx) + return work_item + + def _condition(html: bool, value: str) -> CapellaWorkItem.Condition: _type = "text/html" if html else "text/plain" return {"type": _type, "value": value} @@ -245,4 +266,5 @@ def physical_component( "PhysicalComponent": physical_component, "SystemComponent": component_or_actor, "Scenario": include_pre_and_post_condition, + "Constraint": constraint, } diff --git a/tests/data/diagram_cache/_6Td1kOQ8Ee2tXvmHzHzXCA.svg b/tests/data/diagram_cache/_APMboAPhEeynfbzU12yy7w.svg similarity index 99% rename from tests/data/diagram_cache/_6Td1kOQ8Ee2tXvmHzHzXCA.svg rename to tests/data/diagram_cache/_APMboAPhEeynfbzU12yy7w.svg index b3106e05..8f2ff68f 100644 --- a/tests/data/diagram_cache/_6Td1kOQ8Ee2tXvmHzHzXCA.svg +++ b/tests/data/diagram_cache/_APMboAPhEeynfbzU12yy7w.svg @@ -168,4 +168,4 @@ fill="none" x1="485" x2="482" y1="209" y2="216" stroke-dasharray="none" /> - + \ No newline at end of file diff --git a/tests/data/diagram_cache/index.json b/tests/data/diagram_cache/index.json index 0e51fd36..32772bfe 100644 --- a/tests/data/diagram_cache/index.json +++ b/tests/data/diagram_cache/index.json @@ -1,6 +1,6 @@ [ { - "uuid": "_6Td1kOQ8Ee2tXvmHzHzXCA", + "uuid": "_APMboAPhEeynfbzU12yy7w", "name": "[CC] Capability", "success": true }, @@ -9,4 +9,4 @@ "name": "[CDB] Class tests", "success": false } -] +] \ No newline at end of file diff --git a/tests/test_elements.py b/tests/test_elements.py index 5e56f8ca..c0502c22 100644 --- a/tests/test_elements.py +++ b/tests/test_elements.py @@ -20,7 +20,7 @@ from .conftest import TEST_DIAGRAM_CACHE, TEST_HOST # type: ignore[import] # pylint: disable=redefined-outer-name -TEST_DIAG_UUID = "_6Td1kOQ8Ee2tXvmHzHzXCA" +TEST_DIAG_UUID = "_APMboAPhEeynfbzU12yy7w" TEST_ELEMENT_UUID = "0d2edb8f-fa34-4e73-89ec-fb9a63001440" TEST_OCAP_UUID = "83d1334f-6180-46c4-a80d-6839341df688" TEST_DESCR = ( @@ -39,6 +39,7 @@ TEST_PHYS_NODE = "8a6d68c8-ac3d-4654-a07e-ada7adeed09f" TEST_SCENARIO = "afdaa095-e2cd-4230-b5d3-6cb771a90f51" TEST_CAP_REAL = "b80b3141-a7fc-48c7-84b2-1467dcef5fce" +TEST_CONSTRAINT = "95cbd4af-7224-43fe-98cb-f13dda540b8e" TEST_POL_ID_MAP = {TEST_E_UUID: "TEST"} TEST_POL_TYPE_MAP = { TEST_ELEMENT_UUID: "LogicalComponent", @@ -50,12 +51,12 @@ ) TEST_SER_DIAGRAM: dict[str, t.Any] = { "id": None, - "title": "[CDB] Class tests", + "title": "[CC] Capability", "description_type": "text/html", "type": "diagram", "status": "open", "additional_attributes": { - "uuid_capella": "_Eiw7IOQ9Ee2tXvmHzHzXCA", + "uuid_capella": "_APMboAPhEeynfbzU12yy7w", }, } TEST_WI_CHECKSUM = ( @@ -67,7 +68,8 @@ class TestDiagramElements: @staticmethod @pytest.fixture def context( - diagram_cache_index: list[dict[str, t.Any]] + diagram_cache_index: list[dict[str, t.Any]], + model: capellambse.MelodyModel, ) -> dict[str, t.Any]: api = mock.MagicMock(spec=polarion_api.OpenAPIPolarionProjectClient) uuid = diagram_cache_index[0]["uuid"] @@ -76,6 +78,7 @@ def context( "API": api, "PROJECT_ID": "project_id", "CAPELLA_UUIDS": [d["uuid"] for d in diagram_cache_index], + "MODEL": model, "POLARION_WI_MAP": {uuid: work_item}, "POLARION_ID_MAP": {uuid: "Diag-1"}, "DIAGRAM_IDX": diagram_cache_index, @@ -85,6 +88,8 @@ def context( @staticmethod def test_create_diagrams(context: dict[str, t.Any]): + context["ELEMENTS"] = {"Diagram": context["MODEL"].diagrams} + diagrams = diagram.create_diagrams(context) assert len(diagrams) == 1 @@ -106,6 +111,7 @@ def test_create_diagrams(context: dict[str, t.Any]): def test_create_diagrams_filters_non_diagram_elements( monkeypatch: pytest.MonkeyPatch, context: dict[str, t.Any] ): + context["ELEMENTS"] = {"Diagram": context["MODEL"].diagrams} attributes = mock.MagicMock() attributes.return_value = None monkeypatch.setattr(serialize, "element", attributes) @@ -421,8 +427,8 @@ def test_resolve_element_type(): class TestSerializers: @staticmethod - def test_diagram(): - diag = {"uuid": TEST_DIAG_UUID, "name": "test_diagram"} + def test_diagram(model: capellambse.MelodyModel): + diag = model.diagrams.by_uuid(TEST_DIAG_UUID) serialized_diagram = serialize.diagram( diag, {"DIAGRAM_CACHE": TEST_DIAGRAM_CACHE} @@ -432,7 +438,7 @@ def test_diagram(): assert serialized_diagram == serialize.CapellaWorkItem( type="diagram", uuid_capella=TEST_DIAG_UUID, - title="test_diagram", + title="[CC] Capability", description_type="text/html", status="open", linked_work_items=[], @@ -440,7 +446,7 @@ def test_diagram(): @staticmethod def test__decode_diagram(): - diagram_path = TEST_DIAGRAM_CACHE / "_6Td1kOQ8Ee2tXvmHzHzXCA.svg" + diagram_path = TEST_DIAGRAM_CACHE / "_APMboAPhEeynfbzU12yy7w.svg" diagram = serialize._decode_diagram(diagram_path) @@ -545,6 +551,18 @@ def test__decode_diagram(): }, }, ), + ( + TEST_CONSTRAINT, + { + "type": "constraint", + "title": "", + "uuid_capella": TEST_CONSTRAINT, + "description_type": "text/html", + "description": markupsafe.Markup( + "This is a test context.Make Food" + ), + }, + ), ], ) def test_generic_work_item( From a05015c559b00422c09d60d949e2369e1d227bdd Mon Sep 17 00:00:00 2001 From: ewuerger Date: Thu, 31 Aug 2023 14:52:09 +0200 Subject: [PATCH 07/22] fix(ci-templates): Remove obsolete diagrams ci-template --- ci-templates/gitlab/synchronise_diagrams.yml | 20 -------------------- ci-templates/gitlab/synchronise_elements.yml | 5 +++-- 2 files changed, 3 insertions(+), 22 deletions(-) delete mode 100644 ci-templates/gitlab/synchronise_diagrams.yml diff --git a/ci-templates/gitlab/synchronise_diagrams.yml b/ci-templates/gitlab/synchronise_diagrams.yml deleted file mode 100644 index 0146b5e1..00000000 --- a/ci-templates/gitlab/synchronise_diagrams.yml +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright DB Netz AG and contributors -# SPDX-License-Identifier: Apache-2.0 - -variables: - CAPELLA2POLARION_DEBUG: "1" - -capella2polarion_synchronise_diagrams: - needs: - - job: update_capella_diagram_cache - artifacts: true - - script: - - pip install capella2polarion --pre - - > - python \ - -m capella2polarion \ - $([[ $CAPELLA2POLARION_DEBUG -eq 1 ]] && echo '--debug') \ - --project-id=${CAPELLA2POLARION_PROJECT_ID} \ - diagrams \ - ./diagram_cache diff --git a/ci-templates/gitlab/synchronise_elements.yml b/ci-templates/gitlab/synchronise_elements.yml index 217ec9b3..a089ada8 100644 --- a/ci-templates/gitlab/synchronise_elements.yml +++ b/ci-templates/gitlab/synchronise_elements.yml @@ -6,8 +6,8 @@ variables: capella2polarion_synchronise_elements: needs: - - job: capella2polarion_synchronise_diagrams - optional: True + - job: update_capella_diagram_cache + artifacts: true script: - pip install capella2polarion --pre @@ -18,6 +18,7 @@ capella2polarion_synchronise_elements: --project-id=${CAPELLA2POLARION_PROJECT_ID:?} \ model-elements \ "${CAPELLA2POLARION_MODEL_JSON:?}" \ + ./diagram_cache \ ${CAPELLA2POLARION_CONFIG:?} artifacts: From 16cf044d2ae0ce15a7aa5bd2b6fb9ed31e366968 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Thu, 31 Aug 2023 15:01:30 +0200 Subject: [PATCH 08/22] wip!: Fixate open-api-client to `feat-add-attachments-workitem-relations` --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index aa1c077a..b9a600b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ dependencies = [ "capellambse", "click", "PyYAML", - "polarion-rest-api-client @ git+https://github.com/DSD-DBS/polarion-rest-api-client.git@v0.2.0", + "polarion-rest-api-client @ git+https://github.com/DSD-DBS/polarion-rest-api-client.git@feat-add-attachments-workitem-relations", "requests", ] From 4c5809b924d0cdca6e7bc776e7452177f48b7d43 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Thu, 31 Aug 2023 15:03:29 +0200 Subject: [PATCH 09/22] ci: Please pre-commit hooks --- tests/data/diagram_cache/_APMboAPhEeynfbzU12yy7w.svg | 2 +- tests/data/diagram_cache/index.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/data/diagram_cache/_APMboAPhEeynfbzU12yy7w.svg b/tests/data/diagram_cache/_APMboAPhEeynfbzU12yy7w.svg index 8f2ff68f..b3106e05 100644 --- a/tests/data/diagram_cache/_APMboAPhEeynfbzU12yy7w.svg +++ b/tests/data/diagram_cache/_APMboAPhEeynfbzU12yy7w.svg @@ -168,4 +168,4 @@ fill="none" x1="485" x2="482" y1="209" y2="216" stroke-dasharray="none" /> - \ No newline at end of file + diff --git a/tests/data/diagram_cache/index.json b/tests/data/diagram_cache/index.json index 32772bfe..0091e583 100644 --- a/tests/data/diagram_cache/index.json +++ b/tests/data/diagram_cache/index.json @@ -9,4 +9,4 @@ "name": "[CDB] Class tests", "success": false } -] \ No newline at end of file +] From 4a90b547f5bedb08722134c1b26bc0440a6d6682 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Thu, 31 Aug 2023 15:16:16 +0200 Subject: [PATCH 10/22] ci: Please pylint --- capella2polarion/elements/serialize.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/capella2polarion/elements/serialize.py b/capella2polarion/elements/serialize.py index 4a62e73a..13b73a96 100644 --- a/capella2polarion/elements/serialize.py +++ b/capella2polarion/elements/serialize.py @@ -226,7 +226,9 @@ def constraint( ) -> CapellaWorkItem: """Return attributes for a ``Constraint``.""" work_item = _generic_work_item(obj, ctx) - work_item.description = get_linked_text(obj, ctx) + work_item.description = ( # pylint: disable=attribute-defined-outside-init + get_linked_text(obj, ctx) + ) return work_item @@ -244,7 +246,9 @@ def component_or_actor( xtype = RE_CAMEL_CASE_2ND_WORD_PATTERN.sub( r"\1Actor", type(obj).__name__ ) - work_item.type = helpers.resolve_element_type(xtype) + work_item.type = helpers.resolve_element_type( # pylint: disable=attribute-defined-outside-init + xtype + ) return work_item @@ -255,7 +259,7 @@ def physical_component( work_item = component_or_actor(obj, ctx) xtype = work_item.type if obj.nature is not None: - work_item.type = f"{xtype}{obj.nature.name.capitalize()}" + work_item.type = f"{xtype}{obj.nature.name.capitalize()}" # pylint: disable=attribute-defined-outside-init return work_item From f80bf347e2d2f9c0342f1ae30f8048fe7a350654 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Sat, 2 Sep 2023 10:04:07 +0200 Subject: [PATCH 11/22] ci(ci-templates)!: Install packages on special tags for experiments This is just for the staging pipeline. --- .gitlab-ci.yml | 1 + ci-templates/gitlab/synchronise_elements.yml | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c217c946..f2dca510 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -8,6 +8,7 @@ stages: .patch-pyproject-toml: &patch-pyproject-toml - sed -i -e 's/\(^ "polarion-rest-api-client\).*",/\1",/' pyproject.toml + - pip install https://$PYPI_ARTIFACTORY_USERNAME:$PYPI_ARTIFACTORY_PASSWORD@$PYPI_ARTIFACTORY/polarion-rest-api-client/attachments/polarion_rest_api_client-attachments-py3-none-any.whl wheel: stage: build diff --git a/ci-templates/gitlab/synchronise_elements.yml b/ci-templates/gitlab/synchronise_elements.yml index a089ada8..bffb77dd 100644 --- a/ci-templates/gitlab/synchronise_elements.yml +++ b/ci-templates/gitlab/synchronise_elements.yml @@ -10,7 +10,8 @@ capella2polarion_synchronise_elements: artifacts: true script: - - pip install capella2polarion --pre + - pip install https://$PYPI_ARTIFACTORY_USERNAME:$PYPI_ARTIFACTORY_PASSWORD@$PYPI_ARTIFACTORY/polarion-rest-api-client/attachments/polarion_rest_api_client-attachments-py3-none-any.whl + - pip install https://$PYPI_ARTIFACTORY_USERNAME:$PYPI_ARTIFACTORY_PASSWORD@$PYPI_ARTIFACTORY/capella2polarion/less-requests/capella2polarion-less_requests-py3-none-any.whl - > python \ -m capella2polarion \ From 63426efb38f39d399144fb806455cb64b1910fe9 Mon Sep 17 00:00:00 2001 From: Michael Harbarth Date: Tue, 12 Sep 2023 16:49:48 +0200 Subject: [PATCH 12/22] fix: don't use get_linked_text for conditions as it does not work properly for deleted references --- capella2polarion/elements/serialize.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/capella2polarion/elements/serialize.py b/capella2polarion/elements/serialize.py index 13b73a96..3a50d308 100644 --- a/capella2polarion/elements/serialize.py +++ b/capella2polarion/elements/serialize.py @@ -186,7 +186,7 @@ def include_pre_and_post_condition( def get_condition(cap: PrePostConditionElement, name: str) -> str: if not (condition := getattr(cap, name)): return "" - return get_linked_text(condition, ctx) + return condition.specification["capella:linkedText"].striptags() def strike_through(string: str) -> str: if match := RE_DESCR_DELETED_PATTERN.match(string): From 30469109362fe2145dac787669f9ce94e73425da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernst=20W=C3=BCrger?= Date: Wed, 13 Sep 2023 13:21:07 +0200 Subject: [PATCH 13/22] fix: Prevent infinite SystemActor creation Picked changes from b09ce597. --- capella2polarion/elements/element.py | 20 +++++++++++++++++--- tests/test_elements.py | 2 ++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/capella2polarion/elements/element.py b/capella2polarion/elements/element.py index 516409c7..fe39b2ee 100644 --- a/capella2polarion/elements/element.py +++ b/capella2polarion/elements/element.py @@ -13,7 +13,7 @@ from capellambse.model import diagram as diag from capella2polarion import elements -from capella2polarion.elements import serialize +from capella2polarion.elements import helpers, serialize logger = logging.getLogger(__name__) @@ -28,11 +28,25 @@ def create_work_items( ) -> list[serialize.CapellaWorkItem]: """Create a set of work items in Polarion.""" objects = chain.from_iterable(ctx["ELEMENTS"].values()) - work_items = [ + _work_items = [ serialize.element(obj, ctx, serialize.generic_work_item) for obj in objects ] - return list(filter(None.__ne__, work_items)) # type: ignore[arg-type] + _work_items = list(filter(None.__ne__, _work_items)) + valid_types = set(map(helpers.resolve_element_type, set(ctx["ELEMENTS"]))) + work_items: list[polarion_api.CapellaWorkItem] = [] + missing_types: set[str] = set() + for work_item in _work_items: + assert work_item is not None + if work_item.type in valid_types: + work_items.append(work_item) + else: + missing_types.add(work_item.type) + logger.debug( + "%r are missing in the capella2polarion configuration", + ", ".join(missing_types), + ) + return work_items def create_links( diff --git a/tests/test_elements.py b/tests/test_elements.py index c0502c22..d83b2954 100644 --- a/tests/test_elements.py +++ b/tests/test_elements.py @@ -192,6 +192,8 @@ def test_create_work_items( monkeypatch: pytest.MonkeyPatch, context: dict[str, t.Any] ): del context["ELEMENTS"]["UnsupportedFakeModelObject"] + context["MODEL"] = model = mock.MagicMock() + model.by_uuid.side_effect = context["ELEMENTS"]["FakeModelObject"] monkeypatch.setattr( serialize, "generic_work_item", From 373c00b03c3298a82f807938bc0ca429a71dcb01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernst=20W=C3=BCrger?= <50786483+ewuerger@users.noreply.github.com> Date: Tue, 26 Sep 2023 16:59:57 +0200 Subject: [PATCH 14/22] fix: Apply suggestions from code review Co-authored-by: micha91 --- capella2polarion/elements/__init__.py | 12 ++++++------ capella2polarion/elements/serialize.py | 1 - 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/capella2polarion/elements/__init__.py b/capella2polarion/elements/__init__.py index c08f97ff..c8b161f5 100644 --- a/capella2polarion/elements/__init__.py +++ b/capella2polarion/elements/__init__.py @@ -103,13 +103,13 @@ def post_work_items(ctx: dict[str, t.Any]) -> None: ctx The context for the workitem operation to be processed. """ - work_items = [ - wi - for wi in ctx["WORK_ITEMS"].values() - if wi.uuid_capella not in ctx["POLARION_ID_MAP"] - ] - for work_item in work_items: + work_items: list[serialize.CapellaWorkItem] = [] + for work_item in ctx["WORK_ITEMS"].values(): + if work_item.uuid_capella not in ctx["POLARION_ID_MAP"]: + continue + assert work_item is not None + work_items.append(work_item) logger.info("Create work item for %r...", work_item.title) if work_items: try: diff --git a/capella2polarion/elements/serialize.py b/capella2polarion/elements/serialize.py index 3a50d308..674c3670 100644 --- a/capella2polarion/elements/serialize.py +++ b/capella2polarion/elements/serialize.py @@ -48,7 +48,6 @@ class Condition(t.TypedDict): uuid_capella: str | None preCondition: Condition | None postCondition: Condition | None - checksum: str | None def element( From 94cf5a32094ec6511bf19157e9e6f87f18497d3f Mon Sep 17 00:00:00 2001 From: ewuerger Date: Wed, 11 Oct 2023 16:33:45 +0200 Subject: [PATCH 15/22] feat(model-elements): Add `input|output_exchanges` link handler --- capella2polarion/elements/element.py | 41 ++++++++++++++++++++++---- capella2polarion/elements/serialize.py | 8 +++-- tests/test_elements.py | 24 +++++++++++++-- 3 files changed, 63 insertions(+), 10 deletions(-) diff --git a/capella2polarion/elements/element.py b/capella2polarion/elements/element.py index fe39b2ee..46b831e6 100644 --- a/capella2polarion/elements/element.py +++ b/capella2polarion/elements/element.py @@ -4,6 +4,7 @@ from __future__ import annotations import collections.abc as cabc +import functools import logging import typing as t from itertools import chain @@ -11,6 +12,7 @@ import polarion_rest_api_client as polarion_api from capellambse.model import common from capellambse.model import diagram as diag +from capellambse.model.crosslayer import fa from capella2polarion import elements from capella2polarion.elements import helpers, serialize @@ -80,7 +82,7 @@ def create_links( continue if isinstance(refs, common.ElementList): - new = refs.by_uuid + new: cabc.Iterable[str] = refs.by_uuid # type: ignore[assignment] else: assert hasattr(refs, "uuid") new = [refs.uuid] @@ -141,7 +143,9 @@ def _handle_diagram_reference_links( return ref_links -def _collect_uuids(nodes: list[common.GenericElement]) -> cabc.Iterator[str]: +def _collect_uuids( + nodes: cabc.Iterable[common.GenericElement], +) -> cabc.Iterator[str]: type_resolvers = TYPE_RESOLVERS for node in nodes: uuid = node.uuid @@ -171,7 +175,34 @@ def _create( return list(filter(None.__ne__, _new_links)) -CUSTOM_LINKS = { - "description_reference": _handle_description_reference_links, - "diagram_elements": _handle_diagram_reference_links, +def _handle_exchanges( + context: dict[str, t.Any], + obj: fa.Function, + role_id: str, + links: dict[str, polarion_api.WorkItemLink], + attr: str = "inputs", +) -> list[polarion_api.WorkItemLink]: + wid = context["POLARION_ID_MAP"][obj.uuid] + exchanges: list[str] = [] + for element in getattr(obj, attr): + uuids = element.exchanges.by_uuid + exs = _get_work_item_ids(context, wid, uuids, role_id) + exchanges.extend(set(exs)) + return _create(context, wid, role_id, exchanges, links) + + +CustomLinkMaker = cabc.Callable[ + [ + dict[str, t.Any], + diag.Diagram | common.GenericElement, + str, + dict[str, t.Any], + ], + list[polarion_api.WorkItemLink], +] +CUSTOM_LINKS: dict[str, CustomLinkMaker] = { + "description_reference": _handle_description_reference_links, # type: ignore[dict-item] + "diagram_elements": _handle_diagram_reference_links, # type: ignore[dict-item] + "input_exchanges": functools.partial(_handle_exchanges, attr="inputs"), + "output_exchanges": functools.partial(_handle_exchanges, attr="outputs"), } diff --git a/capella2polarion/elements/serialize.py b/capella2polarion/elements/serialize.py index 674c3670..5c01e6cd 100644 --- a/capella2polarion/elements/serialize.py +++ b/capella2polarion/elements/serialize.py @@ -173,8 +173,7 @@ def replace_markup( f'id="fake" data-item-id="{pid}" data-option-id="long">' "" ) - else: - return non_matcher(match.group(0)) + return non_matcher(match.group(0)) def include_pre_and_post_condition( @@ -262,7 +261,10 @@ def physical_component( return work_item -SERIALIZERS = { +Serializer = cabc.Callable[ + [common.GenericElement, dict[str, t.Any]], CapellaWorkItem +] +SERIALIZERS: dict[str, Serializer] = { "CapabilityRealization": include_pre_and_post_condition, "LogicalComponent": component_or_actor, "OperationalCapability": include_pre_and_post_condition, diff --git a/tests/test_elements.py b/tests/test_elements.py index d83b2954..4a070740 100644 --- a/tests/test_elements.py +++ b/tests/test_elements.py @@ -137,7 +137,7 @@ def __init__( self, uuid: str, name: str = "", - attribute: FakeModelObject | None = None, + attribute: t.Any | None = None, ): self.uuid = uuid self.name = name @@ -237,6 +237,26 @@ def test_create_links_custom_resolver(context: dict[str, t.Any]): assert links == [expected] + @staticmethod + def test_create_links_custom_exchanges_resolver(context: dict[str, t.Any]): + function_uuid = "ceffa011-7b66-4b3c-9885-8e075e312ffa" + obj = context["MODEL"].by_uuid(function_uuid) + context["POLARION_ID_MAP"][function_uuid] = "Obj-1" + context["POLARION_ID_MAP"][ + "1a414995-f4cd-488c-8152-486e459fb9de" + ] = "Obj-2" + context["ROLES"] = {"SystemFunction": ["input_exchanges"]} + expected = polarion_api.WorkItemLink( + "Obj-1", + "Obj-2", + "input_exchanges", + secondary_work_item_project="project_id", + ) + + links = element.create_links(obj, context) + + assert links == [expected] + @staticmethod def test_create_links_missing_attribute( context: dict[str, t.Any], caplog: pytest.LogCaptureFixture @@ -282,7 +302,7 @@ def test_create_links_from_ElementList(context: dict[str, t.Any]): secondary_work_item_project="project_id", ) - links = element.create_links(obj, context) + links = element.create_links(obj, context) # type: ignore[arg-type] assert expected_link in links assert expected_link1 in links From 24968560f5a7897d4e6c55111d927c1f830925a5 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Wed, 11 Oct 2023 17:04:35 +0200 Subject: [PATCH 16/22] fix!: Fix `post_work_items` --- capella2polarion/elements/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/capella2polarion/elements/__init__.py b/capella2polarion/elements/__init__.py index c8b161f5..a2899692 100644 --- a/capella2polarion/elements/__init__.py +++ b/capella2polarion/elements/__init__.py @@ -105,7 +105,7 @@ def post_work_items(ctx: dict[str, t.Any]) -> None: """ work_items: list[serialize.CapellaWorkItem] = [] for work_item in ctx["WORK_ITEMS"].values(): - if work_item.uuid_capella not in ctx["POLARION_ID_MAP"]: + if work_item.uuid_capella in ctx["POLARION_ID_MAP"]: continue assert work_item is not None From 85f28c1543945684903d59b48838a364ffe9cf13 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Thu, 19 Oct 2023 16:38:30 +0200 Subject: [PATCH 17/22] fix: Fix various bugs - Fix debug log message for empty missing types - Merge diagram work item creator into generic work item creator - Handle all roles from the wildcard entry in the config. Also include `Diagram`. --- capella2polarion/__main__.py | 63 ++++++++++++++++++++++----- capella2polarion/elements/__init__.py | 18 ++++---- capella2polarion/elements/diagram.py | 24 ---------- capella2polarion/elements/element.py | 29 ++++++++---- tests/data/model_elements/config.yaml | 15 ++++--- tests/test_cli.py | 5 --- tests/test_elements.py | 8 ++-- 7 files changed, 94 insertions(+), 68 deletions(-) delete mode 100644 capella2polarion/elements/diagram.py diff --git a/capella2polarion/__main__.py b/capella2polarion/__main__.py index 46bd34a0..097ade32 100644 --- a/capella2polarion/__main__.py +++ b/capella2polarion/__main__.py @@ -79,13 +79,30 @@ def _get_roles_from_config(ctx: dict[str, t.Any]) -> dict[str, list[str]]: roles[key] = list(role_ids) else: roles[typ] = [] - roles["Diagram"] = ["diagram_elements"] return roles def _sanitize_config( - config: dict[str, list[str | dict[str, t.Any]]], special: dict[str, t.Any] + config: dict[str, list[str | dict[str, t.Any]]], + special: list[str | dict[str, t.Any]], ) -> dict[str, t.Any]: + special_config: dict[str, t.Any] = {} + for typ in special: + if isinstance(typ, str): + special_config[typ] = None + else: + special_config.update(typ) + + lookup: dict[str, dict[str, list[str]]] = {} + for layer, xtypes in config.items(): + for xt in xtypes: + if isinstance(xt, str): + item: dict[str, list[str]] = {xt: []} + else: + item = xt + + lookup.setdefault(layer, {}).update(item) + new_config: dict[str, t.Any] = {} for layer, xtypes in config.items(): new_entries: list[str | dict[str, t.Any]] = [] @@ -93,16 +110,36 @@ def _sanitize_config( if isinstance(xtype, dict): for sub_key, sub_value in xtype.items(): new_value = ( - special.get("*", []) - + special.get(sub_key, []) + special_config.get("*", []) + + special_config.get(sub_key, []) + sub_value ) new_entries.append({sub_key: new_value}) else: - if new_value := special.get("*", []) + special.get(xtype, []): + star = special_config.get("*", []) + special_xtype = special_config.get(xtype, []) + if new_value := star + special_xtype: new_entries.append({xtype: new_value}) else: new_entries.append(xtype) + + wildcard_values = special_config.get("*", []) + for key, value in special_config.items(): + if key == "*": + continue + + if isinstance(value, list): + new_value = ( + lookup.get(layer, {}).get(key, []) + + wildcard_values + + value + ) + new_entries.append({key: new_value}) + elif value is None and key not in [ + entry if isinstance(entry, str) else list(entry.keys())[0] + for entry in new_entries + ]: + new_entries.append({key: wildcard_values}) new_config[layer] = new_entries return new_config @@ -189,16 +226,20 @@ def model_elements( ctx.obj["POLARION_ID_MAP"] = { uuid: wi.id for uuid, wi in ctx.obj["POLARION_WI_MAP"].items() } - diagrams = ctx.obj["ELEMENTS"].pop("Diagram", []) - work_items = elements.element.create_work_items(ctx.obj) - ctx.obj["ELEMENTS"]["Diagram"] = diagrams - pdiagrams = elements.diagram.create_diagrams(ctx.obj) - ctx.obj["WORK_ITEMS"] = { - wi.uuid_capella: wi for wi in work_items + pdiagrams + duuids = { + diag["uuid"] for diag in ctx.obj["DIAGRAM_IDX"] if diag["success"] } + ctx.obj["ELEMENTS"]["Diagram"] = [ + diag for diag in ctx.obj["ELEMENTS"]["Diagram"] if diag.uuid in duuids + ] + work_items = elements.element.create_work_items(ctx.obj) + ctx.obj["WORK_ITEMS"] = {wi.uuid_capella: wi for wi in work_items} elements.delete_work_items(ctx.obj) elements.post_work_items(ctx.obj) + + work_items = elements.element.create_work_items(ctx.obj) + ctx.obj["WORK_ITEMS"] = {wi.uuid_capella: wi for wi in work_items} elements.patch_work_items(ctx.obj) elements.make_model_elements_index(ctx.obj) diff --git a/capella2polarion/elements/__init__.py b/capella2polarion/elements/__init__.py index a2899692..ab9c208a 100644 --- a/capella2polarion/elements/__init__.py +++ b/capella2polarion/elements/__init__.py @@ -114,6 +114,11 @@ def post_work_items(ctx: dict[str, t.Any]) -> None: if work_items: try: ctx["API"].create_work_items(work_items) + workitems = {wi.uuid_capella: wi for wi in work_items if wi.id} + ctx["POLARION_WI_MAP"].update(workitems) + ctx["POLARION_ID_MAP"] = { + uuid: wi.id for uuid, wi in ctx["POLARION_WI_MAP"].items() + } except polarion_api.PolarionApiException as error: logger.error("Creating work items failed. %s", error.args[0]) @@ -140,21 +145,16 @@ def add_content( setattr(work_item, key, value) return work_item - ctx["POLARION_WI_MAP"] = get_polarion_wi_map(ctx) ctx["POLARION_ID_MAP"] = uuids = { uuid: wi.id for uuid, wi in ctx["POLARION_WI_MAP"].items() - if wi.status == "open" and uuid in ctx["WORK_ITEMS"] + if wi.status == "open" and wi.uuid_capella and wi.id } for uuid in uuids: elements = ctx["MODEL"] if uuid.startswith("_"): elements = ctx["MODEL"].diagrams - try: - obj = elements.by_uuid(uuid) - except KeyError: - logger.error("Weird %r", uuid) - continue + obj = elements.by_uuid(uuid) links = element.create_links(obj, ctx) @@ -189,6 +189,9 @@ def get_elements_and_type_map( if isinstance(typ, dict): typ = list(typ.keys())[0] + if typ == "Diagram": + continue + xtype = convert_type.get(typ, typ) objects = ctx["MODEL"].search(xtype, below=below) elements.setdefault(typ, []).extend(objects) @@ -277,7 +280,6 @@ def make_model_elements_index(ctx: dict[str, t.Any]) -> None: from . import ( # pylint: disable=cyclic-import api_helper, - diagram, element, helpers, serialize, diff --git a/capella2polarion/elements/diagram.py b/capella2polarion/elements/diagram.py deleted file mode 100644 index 4c82436e..00000000 --- a/capella2polarion/elements/diagram.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright DB Netz AG and contributors -# SPDX-License-Identifier: Apache-2.0 -"""Objects for synchronization of Capella diagrams to polarion.""" -from __future__ import annotations - -import logging -import typing as t - -from capella2polarion.elements import serialize - -logger = logging.getLogger(__name__) - - -def create_diagrams(ctx: dict[str, t.Any]) -> list[serialize.CapellaWorkItem]: - """Return a set of new work items of type ``diagram``.""" - uuids = {diag["uuid"] for diag in ctx["DIAGRAM_IDX"] if diag["success"]} - diagrams = [ - diag for diag in ctx["ELEMENTS"]["Diagram"] if diag.uuid in uuids - ] - work_items = [ - serialize.element(diagram, ctx, serialize.diagram) - for diagram in diagrams - ] - return list(filter(None.__ne__, work_items)) # type:ignore[arg-type] diff --git a/capella2polarion/elements/element.py b/capella2polarion/elements/element.py index 46b831e6..84ae08d5 100644 --- a/capella2polarion/elements/element.py +++ b/capella2polarion/elements/element.py @@ -28,12 +28,21 @@ def create_work_items( ctx: dict[str, t.Any] ) -> list[serialize.CapellaWorkItem]: - """Create a set of work items in Polarion.""" + """Create a list of work items for Polarion.""" objects = chain.from_iterable(ctx["ELEMENTS"].values()) - _work_items = [ - serialize.element(obj, ctx, serialize.generic_work_item) - for obj in objects + _work_items = [] + serializer: cabc.Callable[ + [diag.Diagram | common.GenericElement, dict[str, t.Any]], + serialize.CapellaWorkItem, ] + for obj in objects: + if isinstance(obj, diag.Diagram): + serializer = serialize.diagram + else: + serializer = serialize.generic_work_item + + _work_items.append(serialize.element(obj, ctx, serializer)) + _work_items = list(filter(None.__ne__, _work_items)) valid_types = set(map(helpers.resolve_element_type, set(ctx["ELEMENTS"]))) work_items: list[polarion_api.CapellaWorkItem] = [] @@ -44,10 +53,12 @@ def create_work_items( work_items.append(work_item) else: missing_types.add(work_item.type) - logger.debug( - "%r are missing in the capella2polarion configuration", - ", ".join(missing_types), - ) + + if missing_types: + logger.debug( + "%r are missing in the capella2polarion configuration", + ", ".join(missing_types), + ) return work_items @@ -118,7 +129,7 @@ def _handle_description_reference_links( role_id: str, links: dict[str, polarion_api.WorkItemLink], ) -> list[polarion_api.WorkItemLink]: - refs = context["DESCR_REFERENCES"].get(obj.uuid) + refs = context["DESCR_REFERENCES"].get(obj.uuid, []) wid = context["POLARION_ID_MAP"][obj.uuid] refs = set(_get_work_item_ids(context, wid, refs, role_id)) return _create(context, wid, role_id, refs, links) diff --git a/tests/data/model_elements/config.yaml b/tests/data/model_elements/config.yaml index e8647854..e032dcbb 100644 --- a/tests/data/model_elements/config.yaml +++ b/tests/data/model_elements/config.yaml @@ -2,11 +2,14 @@ # SPDX-License-Identifier: Apache-2.0 "*": # All layers - "*": # All class types - - parent # Specify workitem links - - description_reference # Custom attribute - Class: - - state_machines + - "*": # All class types + - parent # Specify workitem links + - description_reference # Custom attribute + - Class: + - state_machines + - Diagram: + - diagram_elements + - Constraint oa: # Specify below - OperationalCapability: # Capella Type with references @@ -19,7 +22,6 @@ oa: # Specify below - CommunicationMean - Class - StateMachine - - Constraint sa: - SystemComponent: @@ -34,7 +36,6 @@ sa: - exchanged_items - ExchangeItem - Class - - Constraint pa: - PhysicalComponent: diff --git a/tests/test_cli.py b/tests/test_cli.py index a766ff82..0b752e17 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -67,10 +67,6 @@ def test_migrate_model_elements(monkeypatch: pytest.MonkeyPatch): {}, ), ) - mock_create_diagrams = mock.MagicMock() - monkeypatch.setattr( - elements.diagram, "create_diagrams", mock_create_diagrams - ) mock_delete_work_items = mock.MagicMock() monkeypatch.setattr(elements, "delete_work_items", mock_delete_work_items) mock_post_work_items = mock.MagicMock() @@ -93,5 +89,4 @@ def test_migrate_model_elements(monkeypatch: pytest.MonkeyPatch): assert mock_delete_work_items.call_count == 1 assert mock_patch_work_items.call_count == 1 assert mock_post_work_items.call_count == 1 - assert mock_create_diagrams.call_count == 1 assert ELEMENTS_IDX_PATH.exists() diff --git a/tests/test_elements.py b/tests/test_elements.py index 4a070740..de07e915 100644 --- a/tests/test_elements.py +++ b/tests/test_elements.py @@ -14,7 +14,7 @@ from capellambse.model import common from capella2polarion import elements -from capella2polarion.elements import diagram, element, helpers, serialize +from capella2polarion.elements import element, helpers, serialize # pylint: disable-next=relative-beyond-top-level, useless-suppression from .conftest import TEST_DIAGRAM_CACHE, TEST_HOST # type: ignore[import] @@ -90,7 +90,7 @@ def context( def test_create_diagrams(context: dict[str, t.Any]): context["ELEMENTS"] = {"Diagram": context["MODEL"].diagrams} - diagrams = diagram.create_diagrams(context) + diagrams = element.create_work_items(context) assert len(diagrams) == 1 work_item = diagrams[0] @@ -116,7 +116,7 @@ def test_create_diagrams_filters_non_diagram_elements( attributes.return_value = None monkeypatch.setattr(serialize, "element", attributes) - diagram.create_diagrams(context) + element.create_work_items(context) assert context["API"].create_work_items.call_count == 0 @@ -385,7 +385,7 @@ def test_update_work_items_filters_work_items_with_same_checksum( @staticmethod def test_update_links_with_no_elements(context: dict[str, t.Any]): - context["POLARION_ID_MAP"] = {} + context["POLARION_WI_MAP"] = {} elements.patch_work_items(context) From 5088d8470e0bd5c46faad0cd4b90f098a31a78a2 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Thu, 19 Oct 2023 16:47:08 +0200 Subject: [PATCH 18/22] refactor: Simplify high level calls --- capella2polarion/__main__.py | 7 +++---- capella2polarion/elements/element.py | 5 +++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/capella2polarion/__main__.py b/capella2polarion/__main__.py index 097ade32..23e0f2e8 100644 --- a/capella2polarion/__main__.py +++ b/capella2polarion/__main__.py @@ -232,14 +232,13 @@ def model_elements( ctx.obj["ELEMENTS"]["Diagram"] = [ diag for diag in ctx.obj["ELEMENTS"]["Diagram"] if diag.uuid in duuids ] - work_items = elements.element.create_work_items(ctx.obj) - ctx.obj["WORK_ITEMS"] = {wi.uuid_capella: wi for wi in work_items} + elements.element.create_work_items(ctx.obj) elements.delete_work_items(ctx.obj) elements.post_work_items(ctx.obj) - work_items = elements.element.create_work_items(ctx.obj) - ctx.obj["WORK_ITEMS"] = {wi.uuid_capella: wi for wi in work_items} + # Create missing links b/c of unresolved references + elements.element.create_work_items(ctx.obj) elements.patch_work_items(ctx.obj) elements.make_model_elements_index(ctx.obj) diff --git a/capella2polarion/elements/element.py b/capella2polarion/elements/element.py index 84ae08d5..c64ef780 100644 --- a/capella2polarion/elements/element.py +++ b/capella2polarion/elements/element.py @@ -59,6 +59,7 @@ def create_work_items( "%r are missing in the capella2polarion configuration", ", ".join(missing_types), ) + ctx["WORK_ITEMS"] = {wi.uuid_capella: wi for wi in work_items} return work_items @@ -212,8 +213,8 @@ def _handle_exchanges( list[polarion_api.WorkItemLink], ] CUSTOM_LINKS: dict[str, CustomLinkMaker] = { - "description_reference": _handle_description_reference_links, # type: ignore[dict-item] - "diagram_elements": _handle_diagram_reference_links, # type: ignore[dict-item] + "description_reference": _handle_description_reference_links, + "diagram_elements": _handle_diagram_reference_links, "input_exchanges": functools.partial(_handle_exchanges, attr="inputs"), "output_exchanges": functools.partial(_handle_exchanges, attr="outputs"), } From 678ba2f74fdb79e56ac3a01a418f9bffda959f01 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Thu, 19 Oct 2023 16:56:05 +0200 Subject: [PATCH 19/22] fix: Prepare for production pipelines --- .gitlab-ci.yml | 1 - ci-templates/gitlab/synchronise_elements.yml | 3 +-- pyproject.toml | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f2dca510..c217c946 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -8,7 +8,6 @@ stages: .patch-pyproject-toml: &patch-pyproject-toml - sed -i -e 's/\(^ "polarion-rest-api-client\).*",/\1",/' pyproject.toml - - pip install https://$PYPI_ARTIFACTORY_USERNAME:$PYPI_ARTIFACTORY_PASSWORD@$PYPI_ARTIFACTORY/polarion-rest-api-client/attachments/polarion_rest_api_client-attachments-py3-none-any.whl wheel: stage: build diff --git a/ci-templates/gitlab/synchronise_elements.yml b/ci-templates/gitlab/synchronise_elements.yml index bffb77dd..a089ada8 100644 --- a/ci-templates/gitlab/synchronise_elements.yml +++ b/ci-templates/gitlab/synchronise_elements.yml @@ -10,8 +10,7 @@ capella2polarion_synchronise_elements: artifacts: true script: - - pip install https://$PYPI_ARTIFACTORY_USERNAME:$PYPI_ARTIFACTORY_PASSWORD@$PYPI_ARTIFACTORY/polarion-rest-api-client/attachments/polarion_rest_api_client-attachments-py3-none-any.whl - - pip install https://$PYPI_ARTIFACTORY_USERNAME:$PYPI_ARTIFACTORY_PASSWORD@$PYPI_ARTIFACTORY/capella2polarion/less-requests/capella2polarion-less_requests-py3-none-any.whl + - pip install capella2polarion --pre - > python \ -m capella2polarion \ diff --git a/pyproject.toml b/pyproject.toml index b9a600b9..0bb414cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ dependencies = [ "capellambse", "click", "PyYAML", - "polarion-rest-api-client @ git+https://github.com/DSD-DBS/polarion-rest-api-client.git@feat-add-attachments-workitem-relations", + "polarion-rest-api-client @ git+https://github.com/DSD-DBS/polarion-rest-api-client.git@v0.2.1", "requests", ] From a013f43e12c08f1219730ec5bfb54309b5dc7b9d Mon Sep 17 00:00:00 2001 From: ewuerger Date: Mon, 23 Oct 2023 16:16:33 +0200 Subject: [PATCH 20/22] refactor: Apply changes from code review --- capella2polarion/elements/api_helper.py | 12 ++++++++---- capella2polarion/elements/element.py | 4 ++-- capella2polarion/elements/serialize.py | 8 ++------ 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/capella2polarion/elements/api_helper.py b/capella2polarion/elements/api_helper.py index d72403f7..966c2fc9 100644 --- a/capella2polarion/elements/api_helper.py +++ b/capella2polarion/elements/api_helper.py @@ -29,12 +29,16 @@ def patch_work_item( ctx The context to execute the patch for. obj - The Capella object to update the WorkItem from + The Capella object to update the WorkItem from. receiver A function that receives the WorkItem from the created - instances. + instances. This function alters the WorkItem instances by adding + attributes, e.g.: `linked_work_items`. It can be useful to add + attributes which can only be computed after the work item and + its default attributes were instantiated. name - The name of the object, which should be displayed in log messages. + The name of the object, which should be displayed in log + messages. _type The type of element, which should be shown in log messages. """ @@ -46,7 +50,7 @@ def patch_work_item( log_args = (wid, _type, name) logger.info("Update work item %r for model %s %r...", *log_args) - if new.uuid_capella: + if "uuid_capella" in new.additional_attributes: del new.additional_attributes["uuid_capella"] old.linked_work_items = ctx["API"].get_all_work_item_links(old.id) diff --git a/capella2polarion/elements/element.py b/capella2polarion/elements/element.py index c64ef780..a3db417d 100644 --- a/capella2polarion/elements/element.py +++ b/capella2polarion/elements/element.py @@ -43,7 +43,7 @@ def create_work_items( _work_items.append(serialize.element(obj, ctx, serializer)) - _work_items = list(filter(None.__ne__, _work_items)) + _work_items = list(filter(None, _work_items)) valid_types = set(map(helpers.resolve_element_type, set(ctx["ELEMENTS"]))) work_items: list[polarion_api.CapellaWorkItem] = [] missing_types: set[str] = set() @@ -184,7 +184,7 @@ def _create( ) for id in new ] - return list(filter(None.__ne__, _new_links)) + return list(filter(None, _new_links)) def _handle_exchanges( diff --git a/capella2polarion/elements/serialize.py b/capella2polarion/elements/serialize.py index 5c01e6cd..d6c5f5b8 100644 --- a/capella2polarion/elements/serialize.py +++ b/capella2polarion/elements/serialize.py @@ -224,9 +224,7 @@ def constraint( ) -> CapellaWorkItem: """Return attributes for a ``Constraint``.""" work_item = _generic_work_item(obj, ctx) - work_item.description = ( # pylint: disable=attribute-defined-outside-init - get_linked_text(obj, ctx) - ) + work_item.description = get_linked_text(obj, ctx) return work_item @@ -244,9 +242,7 @@ def component_or_actor( xtype = RE_CAMEL_CASE_2ND_WORD_PATTERN.sub( r"\1Actor", type(obj).__name__ ) - work_item.type = helpers.resolve_element_type( # pylint: disable=attribute-defined-outside-init - xtype - ) + work_item.type = helpers.resolve_element_type(xtype) return work_item From 8a44a03d7ca30f572acf430b46408722a89e41fd Mon Sep 17 00:00:00 2001 From: ewuerger Date: Mon, 23 Oct 2023 16:23:34 +0200 Subject: [PATCH 21/22] ci: Please `pylint` --- capella2polarion/elements/serialize.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/capella2polarion/elements/serialize.py b/capella2polarion/elements/serialize.py index d6c5f5b8..64d94bc6 100644 --- a/capella2polarion/elements/serialize.py +++ b/capella2polarion/elements/serialize.py @@ -224,6 +224,7 @@ def constraint( ) -> CapellaWorkItem: """Return attributes for a ``Constraint``.""" work_item = _generic_work_item(obj, ctx) + # pylint: disable-next=attribute-defined-outside-init work_item.description = get_linked_text(obj, ctx) return work_item @@ -242,6 +243,7 @@ def component_or_actor( xtype = RE_CAMEL_CASE_2ND_WORD_PATTERN.sub( r"\1Actor", type(obj).__name__ ) + # pylint: disable-next=attribute-defined-outside-init work_item.type = helpers.resolve_element_type(xtype) return work_item @@ -253,7 +255,8 @@ def physical_component( work_item = component_or_actor(obj, ctx) xtype = work_item.type if obj.nature is not None: - work_item.type = f"{xtype}{obj.nature.name.capitalize()}" # pylint: disable=attribute-defined-outside-init + # pylint: disable-next=attribute-defined-outside-init + work_item.type = f"{xtype}{obj.nature.name.capitalize()}" return work_item From 35a93117954436cd8ec5f56c3c09824ada277fc2 Mon Sep 17 00:00:00 2001 From: ewuerger Date: Wed, 25 Oct 2023 10:19:08 +0200 Subject: [PATCH 22/22] fix: Add missing parameter to `get_linked_text` --- capella2polarion/elements/serialize.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/capella2polarion/elements/serialize.py b/capella2polarion/elements/serialize.py index 7138f69b..6a0bfcf8 100644 --- a/capella2polarion/elements/serialize.py +++ b/capella2polarion/elements/serialize.py @@ -217,7 +217,7 @@ def get_linked_text( ) -> markupsafe.Markup: """Return sanitized markup of the given ``obj`` linked text.""" description = obj.specification["capella:linkedText"].striptags() - uuids, value = _sanitize_description(description, ctx) + uuids, value = _sanitize_description(obj, description, ctx) if uuids: ctx.setdefault("DESCR_REFERENCES", {})[obj.uuid] = uuids return value