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

Add support for documents in separate projects, status filters and text work items #106

Merged
merged 14 commits into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from 13 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
11 changes: 4 additions & 7 deletions capella2polarion/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,13 +167,10 @@ def render_documents(
overwrite_layouts,
)

new_documents, updated_documents, work_items = renderer.render_documents(
configs, documents
)

polarion_worker.post_documents(new_documents)
polarion_worker.update_documents(updated_documents)
polarion_worker.update_work_items(work_items)
projects_document_data = renderer.render_documents(configs, documents)
for project, project_data in projects_document_data.items():
polarion_worker.create_documents(project_data.new_docs, project)
polarion_worker.update_documents(project_data.updated_docs, project)


if __name__ == "__main__":
Expand Down
13 changes: 13 additions & 0 deletions capella2polarion/connectors/polarion_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import collections.abc as cabc

import bidict
import polarion_rest_api_client as polarion_api

from capella2polarion import data_models

Expand Down Expand Up @@ -114,3 +115,15 @@ def remove_work_items_by_capella_uuid(self, uuids: cabc.Iterable[str]):
for uuid in uuids:
del self._work_items[uuid]
del self._id_mapping[uuid]


DocumentRepository = dict[
tuple[str | None, str, str],
tuple[polarion_api.Document | None, list[polarion_api.WorkItem]],
]
"""A dict providing a mapping for documents and their text workitems.

It has (project, space, name) of the document as key and (document,
workitems) as value. The project can be None and the None value means
that the document is in the same project as the model sync work items.
"""
219 changes: 172 additions & 47 deletions capella2polarion/connectors/polarion_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@
int: 0,
bool: False,
}
WORK_ITEMS_IN_PROJECT_QUERY = (
"SQL:(SELECT item.* FROM POLARION.WORKITEM item, POLARION.MODULE doc, "
"POLARION.PROJECT proj WHERE proj.C_ID = '{project}' AND "
"doc.FK_PROJECT = proj.C_PK AND doc.C_ID = '{doc_name}' AND "
"doc.C_MODULEFOLDER = '{doc_folder}' AND item.C_TYPE = '{wi_type}' AND "
"EXISTS (SELECT rel1.* FROM POLARION.REL_MODULE_WORKITEM rel1 WHERE "
"rel1.FK_URI_MODULE = doc.C_URI AND rel1.FK_URI_WORKITEM = item.C_URI))"
)
ewuerger marked this conversation as resolved.
Show resolved Hide resolved
"""An SQL query to get work items which are inserted in a given document."""


class PolarionWorkerParams:
Expand Down Expand Up @@ -68,29 +77,54 @@ def __init__(
"Polarion PAT (Personal Access Token) parameter "
"is not a set properly."
)
self.client = polarion_api.OpenAPIPolarionProjectClient(
self.polarion_params.project_id,
self.polarion_params.delete_work_items,

self.polarion_client = polarion_api.PolarionClient(
polarion_api_endpoint=f"{self.polarion_params.url}/rest/v1",
polarion_access_token=self.polarion_params.private_access_token,
custom_work_item=data_models.CapellaWorkItem,
)
self.project_client = self.polarion_client.generate_project_client(
project_id=self.polarion_params.project_id,
delete_status=(
"deleted" if self.polarion_params.delete_work_items else None
),
add_work_item_checksum=True,
)
self._additional_clients: dict[str, polarion_api.ProjectClient] = {}
self.check_client()

def _get_client(
self, project_id: str | None
) -> polarion_api.ProjectClient:
if project_id is None:
return self.project_client
if project_id in self._additional_clients:
return self._additional_clients[project_id]
client = self.polarion_client.generate_project_client(
project_id=project_id,
delete_status=(
"deleted" if self.polarion_params.delete_work_items else None
),
add_work_item_checksum=True,
)
if not client.exists():
raise KeyError(f"Miss Polarion project with id {project_id}")
self._additional_clients[project_id] = client
return client

def check_client(self) -> None:
"""Instantiate the polarion client as member."""
if not self.client.project_exists():
if not self.project_client.exists():
raise KeyError(
"Miss Polarion project with id "
f"{self.polarion_params.project_id}"
)

def load_polarion_work_item_map(self):
"""Return a map from Capella UUIDs to Polarion work items."""
work_items = self.client.get_all_work_items(
work_items = self.project_client.work_items.get_all(
"HAS_VALUE:uuid_capella",
{"workitems": "id,uuid_capella,checksum,status,type"},
fields={"workitems": "id,uuid_capella,checksum,status,type"},
work_item_cls=data_models.CapellaWorkItem,
)
self.polarion_data_repo.update_work_items(work_items)

Expand All @@ -103,21 +137,21 @@ def delete_orphaned_work_items(
are marked as ``to be deleted`` via the status attribute.
"""

def serialize_for_delete(uuid: str) -> str:
work_item_id, _ = self.polarion_data_repo[uuid]
logger.info("Delete work item %r...", work_item_id)
return work_item_id

micha91 marked this conversation as resolved.
Show resolved Hide resolved
existing_work_items = {
uuid
for uuid, _, work_item in self.polarion_data_repo.items()
if work_item.status != "deleted"
}
uuids: set[str] = existing_work_items - set(converter_session)
work_item_ids = [serialize_for_delete(uuid) for uuid in uuids]
if work_item_ids:
work_items: list[data_models.CapellaWorkItem] = []
for uuid in uuids:
if wi := self.polarion_data_repo.get_work_item_by_capella_uuid(
uuid
):
logger.info("Delete work item %r...", wi.id)
work_items.append(wi)
try:
self.client.delete_work_items(work_item_ids)
self.project_client.work_items.delete(work_items)
self.polarion_data_repo.remove_work_items_by_capella_uuid(
uuids
)
Expand Down Expand Up @@ -146,7 +180,7 @@ def create_missing_work_items(
logger.info("Create work item for %r...", work_item.title)
if missing_work_items:
try:
self.client.create_work_items(missing_work_items)
self.project_client.work_items.create(missing_work_items)
self.polarion_data_repo.update_work_items(missing_work_items)
except polarion_api.PolarionApiException as error:
logger.error("Creating work items failed. %s", error.args[0])
Expand Down Expand Up @@ -184,18 +218,22 @@ def compare_and_update_work_item(
work_item_changed = new_work_item_check_sum != old_work_item_check_sum
try:
if work_item_changed or self.force_update:
old = self.client.get_work_item(old.id)
old = self.project_client.work_items.get(
old.id, work_item_cls=data_models.CapellaWorkItem
)
if old.attachments:
old_attachments = (
self.client.get_all_work_item_attachments(
self.project_client.work_items.attachments.get_all(
work_item_id=old.id
)
)
else:
old_attachments = []
else:
old_attachments = self.client.get_all_work_item_attachments(
work_item_id=old.id
old_attachments = (
self.project_client.work_items.attachments.get_all(
work_item_id=old.id
)
)
if old_attachments or new.attachments:
work_item_changed |= self.update_attachments(
Expand All @@ -221,8 +259,8 @@ def compare_and_update_work_item(
del old.additional_attributes["uuid_capella"]

if old.linked_work_items_truncated:
old.linked_work_items = self.client.get_all_work_item_links(
old.id
old.linked_work_items = (
self.project_client.work_items.links.get_all(old.id)
)

# Type will only be updated, if set and should be used carefully
Expand Down Expand Up @@ -256,7 +294,7 @@ def compare_and_update_work_item(
new.title = None

try:
self.client.update_work_item(new)
self.project_client.work_items.update(new)
if delete_links:
id_list_str = ", ".join(delete_links.keys())
logger.info(
Expand All @@ -265,7 +303,9 @@ def compare_and_update_work_item(
new.type,
new.title,
)
self.client.delete_work_item_links(list(delete_links.values()))
self.project_client.work_items.links.delete(
list(delete_links.values())
)
ewuerger marked this conversation as resolved.
Show resolved Hide resolved

if create_links:
id_list_str = ", ".join(create_links.keys())
Expand All @@ -275,7 +315,9 @@ def compare_and_update_work_item(
new.type,
new.title,
)
self.client.create_work_item_links(list(create_links.values()))
self.project_client.work_items.links.create(
list(create_links.values())
)

except polarion_api.PolarionApiException as error:
logger.error(
Expand Down Expand Up @@ -346,12 +388,12 @@ def update_attachments(
attachment.file_name,
attachment.id,
)
self.client.delete_work_item_attachment(attachment)
self.project_client.work_items.attachments.delete(attachment)

old_attachment_file_names = set(old_attachment_dict)
new_attachment_file_names = set(new_attachment_dict)
for file_name in old_attachment_file_names - new_attachment_file_names:
self.client.delete_work_item_attachment(
self.project_client.work_items.attachments.delete(
old_attachment_dict[file_name]
)

Expand All @@ -361,7 +403,7 @@ def update_attachments(
new_attachment_file_names - old_attachment_file_names,
)
):
self.client.create_work_item_attachments(new_attachments)
self.project_client.work_items.attachments.create(new_attachments)
created = True

attachments_for_update = {}
Expand All @@ -386,7 +428,7 @@ def update_attachments(
):
continue

self.client.update_work_item_attachment(attachment)
self.project_client.work_items.attachments.update(attachment)
return created

@staticmethod
Expand Down Expand Up @@ -425,36 +467,119 @@ def compare_and_update_work_items(
if uuid in self.polarion_data_repo and data.work_item is not None:
self.compare_and_update_work_item(data)

def post_documents(self, documents: list[polarion_api.Document]):
"""Create new documents."""
self.client.project_client.documents.create(documents)
def create_documents(
self,
document_datas: list[data_models.DocumentData],
document_project: str | None = None,
):
"""Create new documents.

Notes
-----
If the ``document_project`` is ``None`` the default client is
taken.
"""
client = self._get_client(document_project)
documents, _ = self._process_document_datas(client, document_datas)

def update_documents(self, documents: list[polarion_api.Document]):
"""Update existing documents."""
self.client.project_client.documents.update(documents)
client.documents.create(documents)

def update_documents(
self,
document_datas: list[data_models.DocumentData],
document_project: str | None = None,
):
"""Update existing documents.

Notes
-----
If the ``document_project`` is ``None`` the default client is
taken.
"""
client = self._get_client(document_project)
documents, headings = self._process_document_datas(
client, document_datas
)

client.work_items.update(headings)
client.documents.update(documents)

def _process_document_datas(
self,
client: polarion_api.ProjectClient,
document_datas: list[data_models.DocumentData],
):
documents: list[polarion_api.Document] = []
headings: list[polarion_api.WorkItem] = []
for document_data in document_datas:
headings.extend(document_data.headings)
documents.append(document_data.document)
if document_data.text_work_item_provider.new_text_work_items:
self._create_and_update_text_work_items(
document_data.text_work_item_provider.new_text_work_items,
client,
)
document_data.text_work_item_provider.insert_text_work_items(
document_data.document,
)
return documents, headings

def get_document(
self, space: str, name: str
self, space: str, name: str, document_project: str | None = None
) -> polarion_api.Document | None:
"""Get a document from polarion and return None if not found."""
"""Get a document from polarion and return None if not found.

Notes
-----
If the ``document_project`` is ``None`` the default client is
taken.
"""
client = self._get_client(document_project)
try:
return self.client.project_client.documents.get(
return client.documents.get(
space, name, fields={"documents": "@all"}
)
except polarion_api.PolarionApiBaseException as e:
if e.args[0] == 404:
return None
raise e

def update_work_items(self, work_items: list[polarion_api.WorkItem]):
"""Update the given workitems without any additional checks."""
self.client.project_client.work_items.update(work_items)

def load_polarion_documents(
self, document_paths: t.Iterable[tuple[str, str]]
) -> dict[tuple[str, str], polarion_api.Document | None]:
"""Load the given document references from Polarion."""
self,
document_infos: t.Iterable[data_models.DocumentInfo],
) -> polarion_repo.DocumentRepository:
"""Load the documents referenced and text work items from Polarion."""
return {
(space, name): self.get_document(space, name)
for space, name in document_paths
(di.project_id, di.module_folder, di.module_name): (
self.get_document(
di.module_folder, di.module_name, di.project_id
),
ewuerger marked this conversation as resolved.
Show resolved Hide resolved
self._get_client(di.project_id).work_items.get_all(
WORK_ITEMS_IN_PROJECT_QUERY.format(
project=di.project_id
or self.polarion_params.project_id,
doc_folder=di.module_folder,
doc_name=di.module_name,
wi_type=di.text_work_item_type,
),
fields={"workitems": f"id,{di.text_work_item_id_field}"},
),
)
for di in document_infos
}

def _create_and_update_text_work_items(
self,
work_items: dict[str, polarion_api.WorkItem],
client: polarion_api.ProjectClient,
):
client.work_items.update(
[work_item for work_item in work_items.values() if work_item.id]
)
client.work_items.create(
[
work_item
for work_item in work_items.values()
if not work_item.id
]
)
Loading
Loading