Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add add_attributes serializer #139

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 45 additions & 44 deletions capella2polarion/converters/converter_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,50 @@ class CapellaTypeConfig:

def __post_init__(self):
"""Post processing for the initialization."""
self.converters = _force_dict(self.converters)
self.converters = self._force_dict()

def _force_dict(self) -> dict[str, dict[str, t.Any]]:
match self.converters:
case None:
return {}
case str():
return {self.converters: {}}
case list():
return {c: {} for c in self.converters}
case dict():
return self._filter_converter_config()
case _:
raise TypeError("Unsupported Type")

def _filter_converter_config(self) -> dict[str, dict[str, t.Any]]:
custom_converters = (
"include_pre_and_post_condition",
"linked_text_as_description",
"add_attributes",
"add_context_diagram",
"add_tree_diagram",
"add_jinja_fields",
"jinja_as_description",
)
filtered_config = {}
assert isinstance(self.converters, dict)
for name, params in self.converters.items():
params = params or {}
if name not in custom_converters:
logger.error("Unknown converter in config %r", name)
continue

if name in ("add_context_diagram", "add_tree_diagram"):
assert isinstance(params, dict)
params = _filter_context_diagram_config(params)

if name in ("add_attributes"):
ewuerger marked this conversation as resolved.
Show resolved Hide resolved
assert isinstance(params, list) # type: ignore[unreachable]
params = {"attributes": params} # type: ignore[unreachable]

filtered_config[name] = params

return filtered_config


def _default_type_conversion(c_type: str) -> str:
Expand Down Expand Up @@ -283,7 +326,7 @@ def config_matches(config: CapellaTypeConfig | None, **kwargs: t.Any) -> bool:


def _read_capella_type_configs(
conf: dict[str, t.Any] | list[dict[str, t.Any]] | None
conf: dict[str, t.Any] | list[dict[str, t.Any]] | None,
) -> list[dict]:
if conf is None:
return [{}]
Expand All @@ -299,55 +342,13 @@ def _read_capella_type_configs(
)


def _force_dict(
config: str | list[str] | dict[str, dict[str, t.Any]] | None
) -> dict[str, dict[str, t.Any]]:
match config:
case None:
return {}
case str():
return {config: {}}
case list():
return {c: {} for c in config}
case dict():
return _filter_converter_config(config)
case _:
raise TypeError("Unsupported Type")


def add_prefix(polarion_type: str, prefix: str) -> str:
"""Add a prefix to the given ``polarion_type``."""
if prefix:
return f"{prefix}_{polarion_type}"
return polarion_type


def _filter_converter_config(
config: dict[str, dict[str, t.Any]]
) -> dict[str, dict[str, t.Any]]:
custom_converters = (
"include_pre_and_post_condition",
"linked_text_as_description",
"add_context_diagram",
"add_tree_diagram",
"add_jinja_fields",
"jinja_as_description",
)
filtered_config = {}
for name, params in config.items():
params = params or {}
if name not in custom_converters:
logger.error("Unknown converter in config %r", name)
continue

if name in ("add_context_diagram", "add_tree_diagram"):
params = _filter_context_diagram_config(params)

filtered_config[name] = params

return filtered_config


def _filter_context_diagram_config(
config: dict[str, t.Any]
) -> dict[str, t.Any]:
Expand Down
56 changes: 56 additions & 0 deletions capella2polarion/converters/element_converter.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# Copyright DB InfraGO AG and contributors
# SPDX-License-Identifier: Apache-2.0
"""Objects for serialization of capella objects to workitems."""

from __future__ import annotations

import collections
import enum
import hashlib
import logging
import mimetypes
Expand Down Expand Up @@ -33,6 +35,14 @@
logger = logging.getLogger(__name__)
C2P_IMAGE_PREFIX = "__C2P__"
JINJA_RENDERED_IMG_CLS = "jinja-rendered-image"
ARCHITECTURE_LAYERS: dict[str, str] = {
"common": "Common",
"oa": "Operational Analysis",
"sa": "System Analysis",
"la": "Logical Architecture",
"pa": "Physical Architecture",
"epbs": "EPBS",
}
ewuerger marked this conversation as resolved.
Show resolved Hide resolved


def resolve_element_type(type_: str) -> str:
Expand All @@ -57,6 +67,16 @@ def _format(texts: list[str]) -> dict[str, str]:
return requirement_types


def _resolve_capella_attribute(
element: m.ModelElement | m.Diagram, attribute: str
) -> polarion_api.TextContent:
value = getattr(element, attribute)
if isinstance(value, enum.Enum):
return polarion_api.TextContent(type="string", value=value.name)
ewuerger marked this conversation as resolved.
Show resolved Hide resolved

raise ValueError("Unsupported attribute type: %r", value)


class CapellaWorkItemSerializer(polarion_html_helper.JinjaRendererMixin):
"""The general serializer class for CapellaWorkItems."""

Expand Down Expand Up @@ -424,6 +444,10 @@ def __generic_work_item(
obj, raw_description or markupsafe.Markup("")
)
converter_data.description_references = uuids
layer = polarion_api.TextContent(
type="string",
value=ARCHITECTURE_LAYERS.get(converter_data.layer, "UNKNOWN"),
)
requirement_types = self._get_requirement_types_text(obj)

converter_data.work_item = data_model.CapellaWorkItem(
Expand All @@ -433,6 +457,7 @@ def __generic_work_item(
uuid_capella=obj.uuid,
description=polarion_api.HtmlContent(value),
status="open",
layer=layer,
ewuerger marked this conversation as resolved.
Show resolved Hide resolved
**requirement_types, # type:ignore[arg-type]
)
assert converter_data.work_item is not None
Expand All @@ -441,6 +466,32 @@ def __generic_work_item(

return converter_data.work_item

def _add_attributes(
self,
converter_data: data_session.ConverterData,
attributes: list[dict[str, t.Any]],
):
assert converter_data.work_item is not None
for attribute in attributes:
try:
value = _resolve_capella_attribute(
converter_data.capella_element, attribute["capella_attr"]
)
setattr(
converter_data.work_item, attribute["polarion_id"], value
)
Comment on lines +491 to +493
Copy link
Collaborator

Choose a reason for hiding this comment

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

we could also use the additional_attributes dict here to set the value like we do it in other serializers. This should enhance readability

except AttributeError:
logger.error(
"Attribute %r not found on %r",
attribute["capella_attr"],
converter_data.type_config.p_type,
)
continue
except ValueError as error:
logger.error(error.args[0])

return converter_data.work_item

def _diagram(
self,
converter_data: data_session.ConverterData,
Expand All @@ -451,6 +502,10 @@ def _diagram(
assert converter_data.work_item is not None
assert isinstance(diagram, m.Diagram)
work_item_id = converter_data.work_item.id
layer = polarion_api.TextContent(
type="string",
value=ARCHITECTURE_LAYERS.get(converter_data.layer, "UNKNOWN"),
)

diagram_html, attachment = self._draw_diagram_svg(
diagram,
Expand All @@ -473,6 +528,7 @@ def _diagram(
uuid_capella=diagram.uuid,
description=polarion_api.HtmlContent(diagram_html),
status="open",
layer=layer,
ewuerger marked this conversation as resolved.
Show resolved Hide resolved
)
if attachment:
self._add_attachment(converter_data.work_item, attachment)
Expand Down
31 changes: 30 additions & 1 deletion capella2polarion/converters/model_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import capellambse
import polarion_rest_api_client as polarion_api
from capellambse import model as m

from capella2polarion import data_model
from capella2polarion.connectors import polarion_repo
Expand Down Expand Up @@ -63,8 +64,9 @@ def read_model(

if config.diagram_config:
for d in self.model.diagrams:
layer = get_layer_name(d)
micha91 marked this conversation as resolved.
Show resolved Hide resolved
self.converter_session[d.uuid] = data_session.ConverterData(
"", config.diagram_config, d
layer, config.diagram_config, d
)

if missing_types:
Expand Down Expand Up @@ -176,3 +178,30 @@ def generate_work_item_links(
link_serializer.create_grouped_back_link_fields(
converter_data.work_item, local_back_links
)


def get_layer_name(diagram: m.Diagram) -> str:
"""Return the layer name for a diagram."""
match diagram.type.name:
case (
"OEBD"
| "OAIB"
| "OAB"
| "OABD"
| "ORB"
| "OES"
| "OAS"
| "OPD"
| "OCB"
):
return "oa"
case "CM" | "MB" | "CC" | "MCB" | "SFBD" | "SDFB" | "SAB" | "CSA":
return "sa"
case "LCBD" | "LFBD" | "LDFB" | "LAB" | "CRR":
return "la"
case "PFBD" | "PDFB" | "PCBD" | "PAB" | "PPD":
return "pa"
case "EAB" | "CIBD":
return "epbs"
case _:
return "common"
ewuerger marked this conversation as resolved.
Show resolved Hide resolved
4 changes: 4 additions & 0 deletions docs/source/features/sync.rst
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ specific serializer alone:
| linked_text_as_description | A serializer resolving ``Constraint`` s and their |
| | linked text. |
+--------------------------------------+------------------------------------------------------+
| add_attributes | A serializer adding arbitrary attributes as custom |
| | fields to the work item. For now only supports enum |
| | attributes! |
+--------------------------------------+------------------------------------------------------+
| add_context_diagram | A serializer adding a context diagram to the work |
| | item. This requires node.js to be installed. |
| | The Capella objects where ``context_diagram`` is |
Expand Down
45 changes: 36 additions & 9 deletions tests/test_elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
TEST_ACTOR_UUID = "08e02248-504d-4ed8-a295-c7682a614f66"
TEST_PHYS_COMP = "b9f9a83c-fb02-44f7-9123-9d86326de5f1"
TEST_PHYS_NODE = "8a6d68c8-ac3d-4654-a07e-ada7adeed09f"
TEST_PHYS_FNC = "11906f7b-3ae9-4343-b998-95b170be2e2b"
TEST_SCENARIO = "afdaa095-e2cd-4230-b5d3-6cb771a90f51"
TEST_CAP_REAL = "b80b3141-a7fc-48c7-84b2-1467dcef5fce"
TEST_CONSTRAINT = "95cbd4af-7224-43fe-98cb-f13dda540b8e"
Expand Down Expand Up @@ -1859,26 +1860,54 @@ def test_generic_work_item(
assert work_item == data_model.CapellaWorkItem(**expected)
assert status == "open"

def test_add_attributes(self, model: capellambse.MelodyModel):
converters = {
"add_attributes": [
{"capella_attr": "nature", "polarion_id": "nature"},
{"capella_attr": "kind", "polarion_id": "kind"},
]
}
type_config = converter_config.CapellaTypeConfig(
"PhysicalComponent", converters, []
)
serializer = element_converter.CapellaWorkItemSerializer(
model,
polarion_repo.PolarionDataRepository(),
{
TEST_PHYS_COMP: data_session.ConverterData(
"pa",
type_config,
model.by_uuid(TEST_PHYS_COMP),
)
},
True,
)

work_item = serializer.serialize(TEST_PHYS_COMP)

assert work_item is not None
assert work_item.nature == {"type": "string", "value": "UNSET"}
assert work_item.kind == {"type": "string", "value": "UNSET"}

@staticmethod
def test_add_context_diagram(model: capellambse.MelodyModel):
uuid = "11906f7b-3ae9-4343-b998-95b170be2e2b"
type_config = converter_config.CapellaTypeConfig(
"test", "add_context_diagram", []
)
serializer = element_converter.CapellaWorkItemSerializer(
model,
polarion_repo.PolarionDataRepository(),
{
uuid: data_session.ConverterData(
TEST_PHYS_FNC: data_session.ConverterData(
"pa",
type_config,
model.by_uuid(uuid),
model.by_uuid(TEST_PHYS_FNC),
)
},
True,
)

work_item = serializer.serialize(uuid)
work_item = serializer.serialize(TEST_PHYS_FNC)

assert work_item is not None
assert "context_diagram" in work_item.additional_attributes
Expand Down Expand Up @@ -2154,7 +2183,6 @@ def test_read_config_tree_view_with_params(
with mock.patch.object(
context.ContextDiagram, "render"
) as wrapped_render:

wis = serializer.serialize_all()
_ = wis[0].attachments[0].content_bytes

Expand Down Expand Up @@ -2204,25 +2232,24 @@ def test_read_config_links(caplog: pytest.LogCaptureFixture):

@staticmethod
def test_add_context_diagram_with_caption(model: capellambse.MelodyModel):
uuid = "11906f7b-3ae9-4343-b998-95b170be2e2b"
type_config = converter_config.CapellaTypeConfig(
"test", "add_context_diagram", []
)
serializer = element_converter.CapellaWorkItemSerializer(
model,
polarion_repo.PolarionDataRepository(),
{
uuid: data_session.ConverterData(
TEST_PHYS_FNC: data_session.ConverterData(
"pa",
type_config,
model.by_uuid(uuid),
model.by_uuid(TEST_PHYS_FNC),
)
},
True,
generate_figure_captions=True,
)

work_item = serializer.serialize(uuid)
work_item = serializer.serialize(TEST_PHYS_FNC)

assert work_item is not None
assert "context_diagram" in work_item.additional_attributes
Expand Down
Loading
Loading