From 55f706b396d12f9f68325b4d157ea64e04b39664 Mon Sep 17 00:00:00 2001 From: Michael Harbarth Date: Tue, 16 Jan 2024 18:30:46 +0100 Subject: [PATCH] feat: add a get_work_item function to get a single work item with all fields --- polarion_rest_api_client/base_client.py | 9 + polarion_rest_api_client/client.py | 157 +++++----- tests/__init__.py | 1 + .../mock_api_responses/get_work_item.json | 279 ++++++++++++++++++ .../get_work_item.json.license | 2 + tests/test_client_workitems.py | 27 ++ 6 files changed, 408 insertions(+), 67 deletions(-) create mode 100644 tests/data/mock_api_responses/get_work_item.json create mode 100644 tests/data/mock_api_responses/get_work_item.json.license diff --git a/polarion_rest_api_client/base_client.py b/polarion_rest_api_client/base_client.py index 56d9b6d6..e1565b76 100644 --- a/polarion_rest_api_client/base_client.py +++ b/polarion_rest_api_client/base_client.py @@ -204,6 +204,15 @@ def get_work_items( """ raise NotImplementedError + @abc.abstractmethod + def get_work_item( + self, + work_item_id: str, + retry: bool = True, + ) -> WorkItemType | None: + """Return one specific work item with all fields.""" + raise NotImplementedError + def create_work_item(self, work_item: WorkItemType): """Create a single given work item.""" self.create_work_items([work_item]) diff --git a/polarion_rest_api_client/client.py b/polarion_rest_api_client/client.py index 90b3384e..cf8c441c 100644 --- a/polarion_rest_api_client/client.py +++ b/polarion_rest_api_client/client.py @@ -33,6 +33,7 @@ ) from polarion_rest_api_client.open_api_client.api.work_items import ( delete_work_items, + get_work_item, get_work_items, patch_work_item, post_work_items, @@ -567,73 +568,8 @@ def get_work_items( ): for work_item in work_items_response.data: if not getattr(work_item.meta, "errors", []): - assert work_item.attributes - assert isinstance(work_item.id, str) - work_item_id = work_item.id.split("/")[-1] - - work_item_links = [] - work_item_attachments = [] - - if ( - work_item.relationships - and work_item.relationships.linked_work_items - and work_item.relationships.linked_work_items.data - ): - for ( - link - ) in work_item.relationships.linked_work_items.data: - work_item_links.append( - self._parse_work_item_link( - link.id, - link.additional_properties.get( - "suspect", False - ), - work_item_id, - ) - ) - - if ( - work_item.relationships - and work_item.relationships.attachments - and work_item.relationships.attachments.data - ): - for ( - attachment - ) in work_item.relationships.attachments.data: - assert attachment.id - work_item_attachments.append( - dm.WorkItemAttachment( - work_item_id, - attachment.id.split("/")[-1], - None, # title isn't provided - ) - ) - - work_items.append( - self._work_item( - work_item_id, - unset_str_builder(work_item.attributes.title), - ( - unset_str_builder( - work_item.attributes.description.type - ) - if work_item.attributes.description - else None - ), - ( - unset_str_builder( - work_item.attributes.description.value - ) - if work_item.attributes.description - else None - ), - unset_str_builder(work_item.attributes.type), - unset_str_builder(work_item.attributes.status), - work_item.attributes.additional_properties, - work_item_links, - work_item_attachments, - ) - ) + work_item_obj = self._generate_work_item(work_item) + work_items.append(work_item_obj) next_page = isinstance( work_items_response.links, api_models.WorkitemsListGetResponseLinks, @@ -641,6 +577,93 @@ def get_work_items( return work_items, next_page + def get_work_item( + self, + work_item_id: str, + retry: bool = True, + ) -> base_client.WorkItemType | None: + """Return one specific work item with all fields.""" + response = get_work_item.sync_detailed( + self.project_id, + work_item_id, + client=self.client, + fields=_build_sparse_fields( + { + "workitems": "@all", + "workitem_attachments": "@all", + "linkedworkitems": "@all", + } + ), + ) + if not self._check_response(response, not retry) and retry: + sleep_random_time() + return self.get_work_item(work_item_id, False) + + if response.parsed and isinstance( + response.parsed.data, api_models.WorkitemsSingleGetResponseData + ): + return self._generate_work_item(response.parsed.data) + + return None + + def _generate_work_item( + self, + work_item: api_models.WorkitemsListGetResponseDataItem + | api_models.WorkitemsSingleGetResponseData, + ) -> base_client.WorkItemType: + assert work_item.attributes + assert isinstance(work_item.id, str) + work_item_id = work_item.id.split("/")[-1] + work_item_links = [] + work_item_attachments = [] + if ( + work_item.relationships + and work_item.relationships.linked_work_items + and work_item.relationships.linked_work_items.data + ): + for link in work_item.relationships.linked_work_items.data: + work_item_links.append( + self._parse_work_item_link( + link.id, + link.additional_properties.get("suspect", False), + work_item_id, + ) + ) + if ( + work_item.relationships + and work_item.relationships.attachments + and work_item.relationships.attachments.data + ): + for attachment in work_item.relationships.attachments.data: + assert attachment.id + work_item_attachments.append( + dm.WorkItemAttachment( + work_item_id, + attachment.id.split("/")[-1], + None, # title isn't provided + ) + ) + work_item_obj = self._work_item( + work_item_id, + unset_str_builder(work_item.attributes.title), + ( + unset_str_builder(work_item.attributes.description.type) + if work_item.attributes.description + else None + ), + ( + unset_str_builder(work_item.attributes.description.value) + if work_item.attributes.description + else None + ), + unset_str_builder(work_item.attributes.type), + unset_str_builder(work_item.attributes.status), + work_item.attributes.additional_properties, + work_item_links, + work_item_attachments, + ) + return work_item_obj + def get_document( self, space_id: str, diff --git a/tests/__init__.py b/tests/__init__.py index 9bade752..022b6014 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -59,6 +59,7 @@ TEST_RESPONSES / "workitems_next_page_error.json" ) TEST_WI_NEXT_PAGE_RESPONSE = TEST_RESPONSES / "workitems_next_page.json" +TEST_WI_SINGLE_RESPONSE = TEST_RESPONSES / "get_work_item.json" TEST_DOCUMENT_RESPONSE = TEST_RESPONSES / "get_document.json" TEST_ERROR_RESPONSE = TEST_RESPONSES / "error.json" TEST_PROJECT_RESPONSE_JSON = TEST_RESPONSES / "project.json" diff --git a/tests/data/mock_api_responses/get_work_item.json b/tests/data/mock_api_responses/get_work_item.json new file mode 100644 index 00000000..083f0372 --- /dev/null +++ b/tests/data/mock_api_responses/get_work_item.json @@ -0,0 +1,279 @@ +{ + "data": { + "type": "workitems", + "id": "MyProjectId/MyWorkItemId", + "revision": "1234", + "attributes": { + "created": "1970-01-01T00:00:00Z", + "description": { + "type": "text/html", + "value": "My text value" + }, + "test_custom_field": { + "type": "text/html", + "value": "My text value" + }, + "dueDate": "1970-01-01", + "hyperlinks": [ + { + "role": "ref_ext", + "uri": "https://polarion.plm.automation.siemens.com" + } + ], + "id": "MyWorkItemId", + "initialEstimate": "5 1/2d", + "outlineNumber": "1.11", + "plannedEnd": "1970-01-01T00:00:00Z", + "plannedStart": "1970-01-01T00:00:00Z", + "priority": "90.0", + "remainingEstimate": "5 1/2d", + "resolution": "done", + "resolvedOn": "1970-01-01T00:00:00Z", + "severity": "blocker", + "status": "open", + "timeSpent": "5 1/2d", + "title": "Title", + "type": "task", + "updated": "1970-01-01T00:00:00Z" + }, + "relationships": { + "approvals": { + "data": [ + { + "type": "workrecords", + "id": "MyProjectId/MyWorkItemId/MyWorkRecordId", + "revision": "1234" + } + ], + "meta": { + "totalCount": 0 + } + }, + "assignee": { + "data": [ + { + "type": "users", + "id": "MyUserId", + "revision": "1234" + } + ], + "meta": { + "totalCount": 0 + } + }, + "attachments": { + "data": [ + { + "type": "workitem_attachments", + "id": "MyProjectId/MyWorkItemId/MyAttachmentId", + "revision": "1234" + } + ], + "meta": { + "totalCount": 0 + }, + "links": { + "related": "server-host-name/application-path/projects/MyProjectId/workitems/MyWorkItemId/attachments?revision=1234" + } + }, + "author": { + "data": { + "type": "users", + "id": "MyUserId", + "revision": "1234" + } + }, + "backlinkedWorkItems": { + "data": [ + { + "type": "linkedworkitems", + "id": "MyProjectId/MyWorkItemId/parent/MyProjectId/MyLinkedWorkItemId", + "revision": "1234" + } + ], + "meta": { + "totalCount": 0 + } + }, + "categories": { + "data": [ + { + "type": "categories", + "id": "MyProjectId/MyCategoryId", + "revision": "1234" + } + ], + "meta": { + "totalCount": 0 + } + }, + "comments": { + "data": [ + { + "type": "workitem_comments", + "id": "MyProjectId/MyWorkItemId/MyCommentId", + "revision": "1234" + } + ], + "meta": { + "totalCount": 0 + }, + "links": { + "related": "server-host-name/application-path/projects/MyProjectId/workitems/MyWorkItemId/comments?revision=1234" + } + }, + "externallyLinkedWorkItems": { + "data": [ + { + "type": "externallylinkedworkitems", + "id": "MyProjectId/MyWorkItemId/parent/hostname/MyProjectId/MyLinkedWorkItemId", + "revision": "1234" + } + ], + "meta": { + "totalCount": 0 + } + }, + "linkedOslcResources": { + "data": [ + { + "type": "linkedoslcresources", + "id": "MyProjectId/MyWorkItemId/http://server-host-name/ns/cm#relatedChangeRequest/http://server-host-name/application-path/oslc/services/projects/MyProjectId/workitems/MyWorkItemId", + "revision": "1234" + } + ], + "meta": { + "totalCount": 0 + } + }, + "linkedRevisions": { + "data": [ + { + "type": "revisions", + "id": "default/1234", + "revision": "1234" + } + ], + "meta": { + "totalCount": 0 + } + }, + "linkedWorkItems": { + "data": [ + { + "type": "linkedworkitems", + "id": "MyProjectId/MyWorkItemId/parent/MyProjectId/MyLinkedWorkItemId", + "revision": "1234" + } + ], + "meta": { + "totalCount": 0 + }, + "links": { + "related": "server-host-name/application-path/projects/MyProjectId/workitems/MyWorkItemId/linkedworkitems?revision=1234" + } + }, + "module": { + "data": { + "type": "documents", + "id": "MyProjectId/MySpaceId/MyDocumentId", + "revision": "1234" + } + }, + "plannedIn": { + "data": [ + { + "type": "plans", + "id": "MyProjectId/MyPlanId", + "revision": "1234" + } + ], + "meta": { + "totalCount": 0 + } + }, + "project": { + "data": { + "type": "projects", + "id": "MyProjectId", + "revision": "1234" + } + }, + "testSteps": { + "data": [ + { + "type": "teststeps", + "id": "MyProjectId/MyWorkItemId/MyTestStepIndex", + "revision": "1234" + } + ], + "meta": { + "totalCount": 0 + } + }, + "votes": { + "data": [ + { + "type": "users", + "id": "MyUserId", + "revision": "1234" + } + ], + "meta": { + "totalCount": 0 + } + }, + "watches": { + "data": [ + { + "type": "users", + "id": "MyUserId", + "revision": "1234" + } + ], + "meta": { + "totalCount": 0 + } + }, + "workRecords": { + "data": [ + { + "type": "workrecords", + "id": "MyProjectId/MyWorkItemId/MyWorkRecordId", + "revision": "1234" + } + ], + "meta": { + "totalCount": 0 + } + } + }, + "meta": { + "errors": [ + { + "status": "400", + "title": "Bad Request", + "detail": "Unexpected token, BEGIN_ARRAY expected, but was : BEGIN_OBJECT (at $.data)", + "source": { + "pointer": "$.data", + "parameter": "revision", + "resource": { + "id": "MyProjectId/id", + "type": "type" + } + } + } + ] + }, + "links": { + "self": "server-host-name/application-path/projects/MyProjectId/workitems/MyWorkItemId?revision=1234", + "portal": "server-host-name/application-path/polarion/redirect/project/MyProjectId/workitem?id=MyWorkItemId&revision=1234" + } + }, + "included": [ + {} + ], + "links": { + "self": "server-host-name/application-path/projects/MyProjectId/workitems/MyWorkItemId?revision=1234" + } +} diff --git a/tests/data/mock_api_responses/get_work_item.json.license b/tests/data/mock_api_responses/get_work_item.json.license new file mode 100644 index 00000000..02c8c230 --- /dev/null +++ b/tests/data/mock_api_responses/get_work_item.json.license @@ -0,0 +1,2 @@ +Copyright DB InfraGO AG and contributors +SPDX-License-Identifier: Apache-2.0 diff --git a/tests/test_client_workitems.py b/tests/test_client_workitems.py index fb2a6e89..b0620a72 100644 --- a/tests/test_client_workitems.py +++ b/tests/test_client_workitems.py @@ -26,9 +26,36 @@ TEST_WI_PATCH_STATUS_REQUEST, TEST_WI_PATCH_TITLE_REQUEST, TEST_WI_POST_REQUEST, + TEST_WI_SINGLE_RESPONSE, ) +def test_get_one_work_item( + client: polarion_api.OpenAPIPolarionProjectClient, + httpx_mock: pytest_httpx.HTTPXMock, +): + with open(TEST_WI_SINGLE_RESPONSE, encoding="utf8") as f: + httpx_mock.add_response(json=json.load(f)) + + work_item = client.get_work_item("MyWorkItemId") + + query = { + "fields[workitems]": "@all", + "fields[workitem_attachments]": "@all", + "fields[linkedworkitems]": "@all", + } + reqs = httpx_mock.get_requests() + + assert reqs[0].method == "GET" + assert dict(reqs[0].url.params) == query + assert len(reqs) == 1 + + assert work_item is not None + assert len(work_item.linked_work_items) == 1 + assert len(work_item.attachments) == 1 + assert "test_custom_field" in work_item.additional_attributes + + def test_get_all_work_items_multi_page( client: polarion_api.OpenAPIPolarionProjectClient, httpx_mock: pytest_httpx.HTTPXMock,