Skip to content

Commit

Permalink
feat: time tracking support (#25)
Browse files Browse the repository at this point in the history
  • Loading branch information
danfimov committed Sep 28, 2023
2 parents 4f5042f + ca36794 commit 06fb175
Show file tree
Hide file tree
Showing 13 changed files with 255 additions and 38 deletions.
4 changes: 1 addition & 3 deletions ya_tracker_client/domain/client/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,14 @@ async def request(
params: dict[str, Any] | None = None,
payload: dict[str, Any] | None = None,
) -> bytes:
uri = f"{self._base_url}/{self._api_version}{uri}"

bytes_payload = BytesPayload(
value=bytes(serialize_entity(payload), encoding="utf-8"),
content_type="application/json",
)

status, body = await self._make_request(
method=method,
url=uri,
url=f"{self._base_url}/{self._api_version}{uri}",
params=params,
data=bytes_payload,
)
Expand Down
67 changes: 67 additions & 0 deletions ya_tracker_client/domain/entities/duration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from re import compile
from typing import Any

from pydantic import model_validator

from ya_tracker_client.domain.entities.base import AbstractEntity


PATTERN = compile(
r"^P(?=\d+[YMWD])"
r"((?P<years>\d+)Y)?"
r"((?P<months>\d+)M)?"
r"((?P<weeks>\d+)W)?"
r"((?P<days>\d+)D)?"
r"(?P<time>T(?=\d+[HMS])"
r"((?P<hours>\d+)H)?"
r"((?P<minutes>\d+)M)?"
r"((?P<seconds>\d+)S)?)?$",
)


class Duration(AbstractEntity):
years: int = 0
months: int = 0
days: int = 0
hours: int = 0
minutes: int = 0
seconds: int = 0

@model_validator(mode="before")
@classmethod
def validate(cls, model_value: Any) -> dict | None:
if not isinstance(model_value, str):
return

result = PATTERN.match(model_value)
if result is None:
msg = "Duration is not matched to ISO duration pattern."
raise ValueError(msg)

data: dict[str, int] = {}
for field in Duration.model_fields:
if value := result.group(field):
data[field] = int(value)
return data

def __str__(self) -> str:
time = ""
if self.hours:
time = f"{time}{self.hours}H"
if self.minutes:
time = f"{time}{self.minutes}M"
if self.seconds:
time = f"{time}{self.seconds}S"

duration = "P"
if self.years:
duration = f"{duration}{self.years}Y"
if self.months:
duration = f"{duration}{self.months}M"
if self.days:
duration = f"{duration}{self.days}D"
if time:
duration = f"{duration}T{time}"

return duration

45 changes: 45 additions & 0 deletions ya_tracker_client/domain/entities/worklog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from datetime import datetime

from pydantic import field_serializer

from ya_tracker_client.domain.entities.base import AbstractEntity
from ya_tracker_client.domain.entities.duration import Duration
from ya_tracker_client.domain.entities.issue import IssueShort
from ya_tracker_client.domain.entities.user import UserShort


class Worklog(AbstractEntity):
url: str
id: int
version: int
issue: IssueShort
comment: str | None = None
created_by: UserShort
updated_by: UserShort | None
created_at: datetime
updated_at: datetime | None
start: datetime
duration: Duration

@field_serializer("duration")
def serialize_duration(self, duration: Duration):
return str(duration)


class WorklogCreate(AbstractEntity):
start: datetime
duration: Duration
comment: str | None = None

@field_serializer("duration")
def serialize_duration(self, duration: Duration):
return str(duration)


class WorklogEdit(AbstractEntity):
duration: Duration | None = None
comment: str | None = None

@field_serializer("duration")
def serialize_duration(self, duration: Duration):
return str(duration)
2 changes: 2 additions & 0 deletions ya_tracker_client/domain/repositories/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from ya_tracker_client.domain.repositories.issue_relationship import IssueRelationshipRepository
from ya_tracker_client.domain.repositories.queue import QueueRepository
from ya_tracker_client.domain.repositories.user import UserRepository
from ya_tracker_client.domain.repositories.worklog import WorklogRepository


__all__ = [
Expand All @@ -12,5 +13,6 @@
"IssueRelationshipRepository",
"IssueRepository",
"QueueRepository",
"WorklogRepository",
"UserRepository",
]
21 changes: 19 additions & 2 deletions ya_tracker_client/domain/repositories/base.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,24 @@
from ya_tracker_client.domain.client import BaseClient
from json import loads
from typing import Any, Type

from pydantic import BaseModel

class EntityRepository:
from ya_tracker_client.infrastructure.client import BaseClient


class DeserializationMixin:
@staticmethod
def deserialize(
value: bytes,
return_type: Type[BaseModel] | None = None,
plural: bool = False,
) -> Any:
if plural:
return [return_type(**raw_item) for raw_item in loads(value)]
return return_type(**loads(value))


class EntityRepository(DeserializationMixin):
def __init__(self, client: BaseClient) -> None:
self._client = client
super().__init__()
Expand Down
12 changes: 5 additions & 7 deletions ya_tracker_client/domain/repositories/checklist.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from json import loads

from ya_tracker_client.domain.entities.checklist import ChecklistCreate, ChecklistItem, ChecklistItemEdit
from ya_tracker_client.domain.entities.deadline import Deadline
from ya_tracker_client.domain.entities.issue import IssueWithChecklist
Expand Down Expand Up @@ -30,7 +28,7 @@ async def create_checklist_item(
deadline=deadline,
).model_dump(exclude_none=True, by_alias=True),
)
return IssueWithChecklist(**loads(raw_response))
return self.deserialize(raw_response, IssueWithChecklist)

async def get_checklist_items(self, issue_id: str) -> list[ChecklistItem]:
"""
Expand All @@ -42,7 +40,7 @@ async def get_checklist_items(self, issue_id: str) -> list[ChecklistItem]:
method="GET",
uri=f"/issues/{issue_id}/checklistItems",
)
return [ChecklistItem(**raw_checklist_item) for raw_checklist_item in loads(raw_response)]
return self.deserialize(raw_response, ChecklistItem, plural=True)

async def edit_checklist_item(
self,
Expand All @@ -66,7 +64,7 @@ async def edit_checklist_item(
deadline=deadline,
).model_dump(exclude_none=True, by_alias=True),
)
return IssueWithChecklist(**loads(raw_response))
return self.deserialize(raw_response, IssueWithChecklist)

async def delete_checklist(self, issue_id: str) -> IssueWithChecklist:
"""
Expand All @@ -76,7 +74,7 @@ async def delete_checklist(self, issue_id: str) -> IssueWithChecklist:
method="DELETE",
uri=f"/issues/{issue_id}/checklistItems",
)
return IssueWithChecklist(**loads(raw_response))
return self.deserialize(raw_response, IssueWithChecklist)

async def delete_checklist_item(self, issue_id: str, checklist_item_id: str):
"""
Expand All @@ -86,4 +84,4 @@ async def delete_checklist_item(self, issue_id: str, checklist_item_id: str):
method="DELETE",
uri=f"/issues/{issue_id}/checklistItems/{checklist_item_id}",
)
return IssueWithChecklist(**loads(raw_response))
return self.deserialize(raw_response, IssueWithChecklist)
4 changes: 1 addition & 3 deletions ya_tracker_client/domain/repositories/component.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from json import loads

from ya_tracker_client.domain.entities.component import Component
from ya_tracker_client.domain.repositories.base import EntityRepository

Expand All @@ -13,4 +11,4 @@ async def get_components(self) -> list[Component]:
method="GET",
uri="/components",
)
return [Component(**raw_component) for raw_component in loads(raw_response)]
return self.deserialize(raw_response, Component, plural=True)
12 changes: 5 additions & 7 deletions ya_tracker_client/domain/repositories/issue.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from json import loads

from ya_tracker_client.domain.entities.issue import Issue, IssueCreate, IssueEdit, IssueShort
from ya_tracker_client.domain.entities.issue_type import IssueType
from ya_tracker_client.domain.entities.priority import Priority
Expand All @@ -19,7 +17,7 @@ async def get_issue(self, issue_id: str) -> Issue:
method="GET",
uri=f"/issues/{issue_id}",
)
return Issue(**loads(raw_response))
return self.deserialize(raw_response, Issue)

async def create_issue(
self,
Expand Down Expand Up @@ -55,7 +53,7 @@ async def create_issue(
attachment_ids=attachment_ids,
).model_dump(exclude_none=True, by_alias=True),
)
return Issue(**loads(raw_response))
return self.deserialize(raw_response, Issue)

async def edit_issue(
self,
Expand All @@ -72,7 +70,7 @@ async def edit_issue(
params={"version": version} if version is not None else None,
payload=IssueEdit(**kwargs).model_dump(exclude_unset=True),
)
return Issue(**loads(raw_response))
return self.deserialize(raw_response, Issue)

async def get_priorities(
self,
Expand All @@ -86,7 +84,7 @@ async def get_priorities(
uri="/priorities/",
params={"localized": str(localized)},
)
return [Priority(**raw_issue_priority) for raw_issue_priority in loads(raw_response)]
return self.deserialize(raw_response, Priority)

async def get_issue_transitions(
self,
Expand All @@ -99,4 +97,4 @@ async def get_issue_transitions(
method="GET",
uri=f"/issues/{issue_id}/transitions/",
)
return [Transition(**raw_issue_transition) for raw_issue_transition in loads(raw_response)]
return self.deserialize(raw_response, Transition, plural=True)
6 changes: 2 additions & 4 deletions ya_tracker_client/domain/repositories/issue_relationship.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from json import loads

from ya_tracker_client.domain.entities.issue_relationship import (
IssueRelationship,
IssueRelationshipCreate,
Expand All @@ -26,7 +24,7 @@ async def create_issue_relationship(
relationship=relationship,
).model_dump(exclude_none=True, by_alias=True),
)
return IssueRelationship(**loads(raw_response))
return self.deserialize(raw_response, IssueRelationship)

async def get_issue_relationships(
self,
Expand All @@ -39,7 +37,7 @@ async def get_issue_relationships(
method="GET",
uri=f"/issues/{issue_id}/links",
)
return [IssueRelationship(**raw_issue_relationship) for raw_issue_relationship in loads(raw_response)]
return self.deserialize(raw_response, IssueRelationship, plural=True)

async def delete_issue_relationships(
self,
Expand Down
14 changes: 6 additions & 8 deletions ya_tracker_client/domain/repositories/queue.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from json import loads

from ya_tracker_client.domain.entities.issue_type_config import IssueTypeConfig
from ya_tracker_client.domain.entities.queue import Queue, QueueCreate
from ya_tracker_client.domain.entities.queue_field import QueueField
Expand Down Expand Up @@ -32,7 +30,7 @@ async def create_queue(
issue_types_config=issue_types_config,
).model_dump(exclude_none=True, by_alias=True),
)
return Queue(**loads(raw_response))
return self.deserialize(raw_response, Queue)

async def get_queue(self, queue_id: str | int) -> Queue:
"""
Expand All @@ -42,7 +40,7 @@ async def get_queue(self, queue_id: str | int) -> Queue:
method="GET",
uri=f"/queues/{queue_id}",
)
return Queue(**loads(raw_response))
return self.deserialize(raw_response, Queue)

async def get_queues(self) -> list[Queue]:
"""
Expand All @@ -52,7 +50,7 @@ async def get_queues(self) -> list[Queue]:
method="GET",
uri="/queues/",
)
return [Queue(**raw_queue) for raw_queue in loads(raw_response)]
return self.deserialize(raw_response, Queue, plural=True)

async def get_queue_versions(self, queue_id: str | int) -> list[QueueVersion]:
"""
Expand All @@ -62,7 +60,7 @@ async def get_queue_versions(self, queue_id: str | int) -> list[QueueVersion]:
method="GET",
uri=f"/queues/{queue_id}/versions",
)
return [QueueVersion(**raw_queue_version) for raw_queue_version in loads(raw_response)]
return self.deserialize(raw_response, QueueVersion, plural=True)

async def get_queue_fields(self, queue_id: str | int) -> list[QueueField]:
"""
Expand All @@ -72,7 +70,7 @@ async def get_queue_fields(self, queue_id: str | int) -> list[QueueField]:
method="GET",
uri=f"/queues/{queue_id}/fields",
)
return [QueueField(**raw_queue_field) for raw_queue_field in loads(raw_response)]
return self.deserialize(raw_response, QueueField, plural=True)

async def delete_queue(self, queue_id: str | int) -> None:
"""
Expand All @@ -91,7 +89,7 @@ async def restore_queue(self, queue_id: str | int) -> Queue:
method="POST",
uri=f"/queues/{queue_id}/_restore",
)
return Queue(**loads(raw_response))
return self.deserialize(raw_response, Queue)

async def delete_tag_in_queue(self, queue_id: str | int, tag_name: str) -> None:
"""
Expand Down
7 changes: 3 additions & 4 deletions ya_tracker_client/domain/repositories/user.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from json import loads
from logging import getLogger

from ya_tracker_client.domain.client.errors import ClientError
Expand All @@ -18,7 +17,7 @@ async def get_myself(self) -> User:
method="GET",
uri="/myself/",
)
return User(**loads(raw_response))
return self.deserialize(raw_response, User)

async def get_user(
self,
Expand All @@ -39,7 +38,7 @@ async def get_user(
method="GET",
uri=f"/users/{login or uid}",
)
return User(**loads(raw_response))
return self.deserialize(raw_response, User)

async def get_users(self) -> list[User]:
"""
Expand All @@ -49,4 +48,4 @@ async def get_users(self) -> list[User]:
method="GET",
uri="/users/",
)
return [User(**raw_user) for raw_user in loads(raw_response)]
return self.deserialize(raw_response, User, plural=True)
Loading

0 comments on commit 06fb175

Please sign in to comment.