diff --git a/examples/get_test_entities.py b/examples/get_test_entities.py index e1a40df..9537265 100644 --- a/examples/get_test_entities.py +++ b/examples/get_test_entities.py @@ -24,6 +24,8 @@ async def main() -> None: await client.get_user(uid=me.uid) await client.get_users() await client.get_issue('TRACKER-1') + await client.get_issue('TRACKER-1', expand='transitions') + await client.get_issue('TRACKER-1', expand='attachments') await client.get_issue_transitions('TRACKER-1') await client.get_queue('TRACKER') await client.get_issue_relationships('TRACKER-1') @@ -37,6 +39,8 @@ async def main() -> None: await client.get_external_applications() await client.get_external_links("TRACKER-1") await client.get_macros('TRACKER') + await client.find_number_of_issues(issue_filter={'queue': 'TRACKER', "assignee": "empty()"}) + await client.get_history_issue_changes('TRACKER-1') await client.stop() diff --git a/ya_tracker_client/domain/entities/attachment.py b/ya_tracker_client/domain/entities/attachment.py index 98205f0..36b36ec 100644 --- a/ya_tracker_client/domain/entities/attachment.py +++ b/ya_tracker_client/domain/entities/attachment.py @@ -21,3 +21,9 @@ class Attachment(AbstractEntity): mimetype: str = Field(..., examples=["text/plain", "image/png"]) size: int metadata: AttachmentMetadata | None = None + + +class AttachmentShort(AbstractEntity): + url: str + id: int + display: str diff --git a/ya_tracker_client/domain/entities/issue.py b/ya_tracker_client/domain/entities/issue.py index 6638026..295bd2b 100644 --- a/ya_tracker_client/domain/entities/issue.py +++ b/ya_tracker_client/domain/entities/issue.py @@ -1,7 +1,9 @@ from datetime import datetime +from typing import Any from pydantic import Field +from ya_tracker_client.domain.entities.attachment import AttachmentShort from ya_tracker_client.domain.entities.base import AbstractEntity from ya_tracker_client.domain.entities.checklist import ChecklistItem from ya_tracker_client.domain.entities.issue_status import IssueStatus @@ -10,6 +12,7 @@ from ya_tracker_client.domain.entities.queue import QueueIdentifier, QueueShort from ya_tracker_client.domain.entities.sprint import Sprint from ya_tracker_client.domain.entities.status import Status +from ya_tracker_client.domain.entities.transition import TransitionShort from ya_tracker_client.domain.entities.user import UserShort @@ -49,6 +52,9 @@ class Issue(AbstractEntity): previous_status: Status | None = None direction: str | None = None + transitions: list[TransitionShort] = Field(default_factory=list) + attachments: list[AttachmentShort] = Field(default_factory=list) + class IssueCreate(AbstractEntity): summary: str @@ -103,3 +109,10 @@ class IssueWithChecklist(AbstractEntity): deadline: datetime | None = None checklist_items: list[ChecklistItem] = Field(default_factory=list) + + +class IssueFindParameters(AbstractEntity): + filter: dict[str, Any] | None = None + query: str | None = None + keys: str | None = None + queue: str | None = None diff --git a/ya_tracker_client/domain/entities/issue_change_history.py b/ya_tracker_client/domain/entities/issue_change_history.py new file mode 100644 index 0000000..47ae441 --- /dev/null +++ b/ya_tracker_client/domain/entities/issue_change_history.py @@ -0,0 +1,39 @@ +from datetime import datetime + +from pydantic import Field + +from ya_tracker_client.domain.entities.base import AbstractEntity +from ya_tracker_client.domain.entities.issue import IssueShort +from ya_tracker_client.domain.entities.issue_field import IssueFieldShort +from ya_tracker_client.domain.entities.user import UserShort + + +class FieldChangeHistoryState(AbstractEntity): + id: str + url: str + key: str + display: str + + +class FieldChangeHistory(AbstractEntity): + field: IssueFieldShort + change_from: FieldChangeHistoryState | str | list | None = Field(default=None, alias="from") + change_to: FieldChangeHistoryState | str | list | None = Field(default=None, alias="to") + + +class IssueChangeHistory(AbstractEntity): + id: str + url: str + issue: IssueShort + updated_at: datetime + updated_by: UserShort + transport: str + type: str + fields: list[FieldChangeHistory] = Field(default_factory=list) + + +class IssueChangeHistoryParameters(AbstractEntity): + id: str | None = None + per_page: int = 50 + field: str | None = None + type: str | None = None diff --git a/ya_tracker_client/domain/entities/transition.py b/ya_tracker_client/domain/entities/transition.py index b63401f..9ee19bb 100644 --- a/ya_tracker_client/domain/entities/transition.py +++ b/ya_tracker_client/domain/entities/transition.py @@ -7,3 +7,9 @@ class Transition(AbstractEntity): id: str to: IssueStatus display: str + + +class TransitionShort(AbstractEntity): + url: str + id: str + display: str diff --git a/ya_tracker_client/domain/repositories/issue.py b/ya_tracker_client/domain/repositories/issue.py index f346380..82262bf 100644 --- a/ya_tracker_client/domain/repositories/issue.py +++ b/ya_tracker_client/domain/repositories/issue.py @@ -1,4 +1,7 @@ -from ya_tracker_client.domain.entities.issue import Issue, IssueCreate, IssueEdit, IssueShort +from typing import Any + +from ya_tracker_client.domain.entities.issue import Issue, IssueCreate, IssueEdit, IssueFindParameters, IssueShort +from ya_tracker_client.domain.entities.issue_change_history import IssueChangeHistory, IssueChangeHistoryParameters from ya_tracker_client.domain.entities.issue_type import IssueType from ya_tracker_client.domain.entities.priority import Priority from ya_tracker_client.domain.entities.queue import QueueIdentifier @@ -9,13 +12,14 @@ class IssueRepository(EntityRepository): - async def get_issue(self, issue_id: str) -> Issue: + async def get_issue(self, issue_id: str, expand: str = "") -> Issue: """ YC docs: https://cloud.yandex.com/en/docs/tracker/concepts/issues/get-issue """ raw_response = await self._client.request( method="GET", uri=f"/issues/{issue_id}", + params={"expand": expand}, ) return self._decode(raw_response, Issue) @@ -98,3 +102,157 @@ async def get_issue_transitions( uri=f"/issues/{issue_id}/transitions/", ) return self._decode(raw_response, Transition, plural=True) + + async def move_issue_to_another_queue( + self, + issue_id: str, + queue_id: str, + notify: bool = True, + notify_author: bool = False, + move_all_fields: bool = False, + initial_status: bool = False, + expand: str | None = None, + **kwargs, + ) -> Issue: + """ + Move an issue to a different queue. + + Before executing the method, make sure the user has permission + to edit the issues to be moved and is allowed to create them in + the new queue. + + Warning! + If an issue you want to move has a type and status that are + missing in the target queue, no transfer will be made. To reset + the issue status to the initial value when moving it, use the + InitialStatus parameter. + + By default, when an issue is moved, the values of its + components, versions, and projects are cleared. If the new queue + has the same values of the fields specified, use the + MoveAllFields parameter to move the components, versions, and + projects. + + If the issue has the local field values specified, they will be + reset when moving the issue to a different queue. + + You can use the kwargs if you need to change the + parameters of the issue being moved. The kwargs has the + same format as when editing issues. + + YC docs: https://cloud.yandex.com/en/docs/tracker/concepts/issues/move-issue + """ + raw_response = await self._client.request( + method="POST", + uri=f"/issues/{issue_id}/_move", + params={ + "queue": queue_id, + "notify": str(notify), + "notifyAuthor": str(notify_author), + "moveAllFields": str(move_all_fields), + "initialStatus": str(initial_status), + "expand": expand or "", + }, + payload=IssueEdit(**kwargs).model_dump(exclude_unset=True), + ) + return self._decode(raw_response, Issue) + + async def find_number_of_issues(self, issue_filter: dict[str, Any] | None = None, query: str | None = None) -> int: + """ + Use this method to find out how many issues meet the criteria in your request. + + YC docs: https://cloud.yandex.com/en/docs/tracker/concepts/issues/count-issues + """ + raw_response = await self._client.request( + method="POST", + uri="/issues/_count", + payload={ + "filter": issue_filter, + "query": query, + }, + ) + return int(raw_response) + + async def release_scroll_view_resources(self, scroll_id: str, scroll_token: str) -> None: + """ + YC docs: https://cloud.yandex.com/en/docs/tracker/concepts/issues/search-release + + :param scroll_id: ID of the page with scroll results. The ID value is taken from the `X-Scroll-Id` header of the response to the search for issues request. + :param scroll_token: token that certifies that the request belongs to the current user. The ID value is taken from the `X-Scroll-Token` header of the response to the search for issues request. + :return: None + """ # noqa: E501 + await self._client.request( + method="POST", + uri="/system/search/scroll/_clear", + payload={scroll_id: scroll_token}, + ) + + async def make_status_transition( + self, + issue_id: str, + transition_id: int | str, + comment: str, + **kwargs, + ) -> list[Transition]: + """ + YC docs: https://cloud.yandex.com/en/docs/tracker/concepts/issues/new-transition + """ + payload = {"comment": comment} + payload.update(kwargs) + raw_response = await self._client.request( + method="POST", + uri=f"/issues/{issue_id}/transitions/{transition_id}/_execute", + payload=payload, + ) + return self._decode(raw_response, Transition, plural=True) + + async def get_history_issue_changes( + self, + issue_id: str, + change_id: str | None = None, + per_page: int = 50, + field: str | None = None, + change_type: str | None = None, + ) -> list[IssueChangeHistory]: + """ + YC docs: https://cloud.yandex.com/en/docs/tracker/concepts/issues/get-changelog + """ + raw_response = await self._client.request( + method="GET", + uri=f"/issues/{issue_id}/changelog", + payload=IssueChangeHistoryParameters( + id=change_id, + per_page=per_page, + field=field, + type=change_type, + ).model_dump(exclude_none=True, by_alias=True), + ) + return self._decode(raw_response, IssueChangeHistory, plural=True) + + async def find_issues( + self, + issue_filter: dict[str, str] | None = None, + query: str | None = None, + order: str | None = None, + expand: str | None = None, + keys: str | None = None, + queue: str | None = None, + ) -> list[Issue]: + params = {} + if order: + params["order"] = order + if expand: + params["expand"] = expand + + raw_response = await self._client.request( + method="POST", + uri="/issues/_search", + params=params, + payload=IssueFindParameters( + filter=issue_filter, + query=query, + keys=keys, + queue=queue, + ).model_dump(exclude_none=True, by_alias=True), + ) + return self._decode(raw_response, Issue, plural=True)