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 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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",):
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",
}
Comment on lines +38 to +45
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it would be better to have this as enum defined in Polarion, wouldn't it? This way we could just post the actual values like oa to Polarion and there it can be handled including localization.



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)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why do we return TextContent for simple strings?


raise ValueError(f"Unsupported attribute type: {value!r}")
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we should be more generic here. We could add an HTML-Flag to the config of an attribute. If this flag is set, we export the value of the attribute as HtmlContent. If not, we just cast the value to string and use the string



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,
Copy link
Collaborator

Choose a reason for hiding this comment

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

I thought we would add the layer optional via the add_attributes serializer as we often already have the layer as part of the work item type

**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
)
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,
Copy link
Collaborator

Choose a reason for hiding this comment

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

So the layer will in the future always be part of a diagram workitem and this is not configurable. I think this is fine, but needs to be communicated as a change in the release notes

)
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
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ disable = [
"too-many-public-methods",
"too-many-return-statements",
"too-many-statements",
"too-many-positional-arguments",

# Auto-formatting
"bad-indentation",
Expand Down
Loading
Loading