Skip to content

Commit

Permalink
feat: add tasks model foundation (#62)
Browse files Browse the repository at this point in the history
  • Loading branch information
MartinHjelmare committed Jan 5, 2023
1 parent e3c0de9 commit a469860
Show file tree
Hide file tree
Showing 11 changed files with 579 additions and 3 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ repos:
rev: v3.3.1
hooks:
- id: pyupgrade
args: [--py37-plus]
args: [--py39-plus, --keep-runtime-typing]
- repo: https://github.com/PyCQA/isort
rev: 5.11.4
hooks:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ max-line-length-suggestions = 88
runtime-typing = false

[tool.pytest.ini_options]
addopts = "-v -Wdefault --cov=aiortm --cov-report=term-missing:skip-covered"
addopts = "-Wdefault --cov=aiortm --cov-report=term-missing:skip-covered"
asyncio_mode = "auto"
pythonpath = ["src"]

Expand Down
7 changes: 6 additions & 1 deletion src/aiortm/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ async def call_api_auth(self, api_method: str, **params: Any) -> dict[str, Any]:

async def call_api(self, api_method: str, **params: Any) -> dict[str, Any]:
"""Call an api method."""
# Remove empty values.
params = {key: value for key, value in params.items() if value is not None}
all_params = {"method": api_method} | params | {"format": "json"}
all_params |= {"api_sig": self._sign_request(all_params)}
response = await self.request(REST_URL, params=all_params)
Expand All @@ -111,7 +113,10 @@ async def call_api(self, api_method: str, **params: Any) -> dict[str, Any]:
response_text = await response.text()

if "rtm.auth" not in api_method:
_LOGGER.debug("Response text: %s", response_text)
logged_response_text = response_text
if self.api_key in response_text:
logged_response_text = response_text.replace(self.api_key, "API_KEY")
_LOGGER.debug("Response text: %s", logged_response_text)

# API doesn't return a JSON encoded response.
# It's text/javascript mimetype but with a JSON string in the text.
Expand Down
3 changes: 3 additions & 0 deletions src/aiortm/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from typing import TYPE_CHECKING

from .contacts import Contacts
from .tasks import Tasks
from .timelines import Timelines

if TYPE_CHECKING:
Expand All @@ -15,9 +16,11 @@ class RTM:

api: "Auth"
contacts: "Contacts" = field(init=False)
tasks: "Tasks" = field(init=False)
timelines: "Timelines" = field(init=False)

def __post_init__(self) -> None:
"""Set up the instance."""
self.contacts = Contacts(self.api)
self.tasks = Tasks(self.api)
self.timelines = Timelines(self.api)
153 changes: 153 additions & 0 deletions src/aiortm/model/tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
"""Provide a model for tasks."""
from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime
from typing import TYPE_CHECKING, Any, Literal, Optional

from pydantic import BaseModel as PydanticBaseModel, Field, validator

from .response import BaseResponse, TransactionResponse

if TYPE_CHECKING:
from ..client import Auth

# pylint: disable=consider-alternative-union-syntax


class BaseModel(PydanticBaseModel):
"""Represent a base model that turns empty string to None."""

@validator("*", pre=True)
def empty_str_to_none(cls, value: Any) -> Any: # pylint: disable=no-self-argument
"""Turn empty string to None."""
if value == "":
return None
return value


class TaskResponse(BaseModel):
"""Represent a response for a task."""

id: int
due: Optional[datetime]
has_due_time: bool
added: datetime
completed: Optional[datetime]
deleted: Optional[datetime]
priority: Literal["N", "1", "2", "3"]
postponed: bool
estimate: Optional[str]


class TaskSeriesResponse(BaseModel):
"""Represent a response for a task series."""

id: int
created: datetime
modified: datetime
name: str
source: str
location_id: Optional[str]
url: Optional[str]
tags: list[str]
participants: list[str]
notes: list[str]
task: list[TaskResponse]


class TaskListResponse(BaseModel):
"""Represent a response for a task list."""

id: int
taskseries: list[TaskSeriesResponse]
current: Optional[datetime]


class RootTaskResponse(BaseModel):
"""Represent a response for the root tasks object."""

rev: str
task_list: list[TaskListResponse] = Field(..., alias="list")

@validator("task_list", pre=True)
def ensure_taskseries( # pylint: disable=no-self-argument
cls, value: list[dict[str, Any]]
) -> list[dict[str, Any]]:
"""Ensure that taskseries exist."""
for item in value:
if "taskseries" not in item:
item["taskseries"] = []
return value


class TaskModifiedResponse(BaseResponse):
"""Represent a response for a modified task."""

transaction: TransactionResponse
task_list: TaskListResponse = Field(..., alias="list")


class TasksResponse(BaseResponse):
"""Represent a response for a list of tasks."""

tasks: RootTaskResponse


@dataclass
class Tasks:
"""Represent the tasks model."""

api: Auth

async def add(
self,
timeline: int,
name: str,
list_id: int | None = None,
parse: bool | None = None,
) -> TaskModifiedResponse:
"""Add a task."""
result = await self.api.call_api_auth(
"rtm.tasks.add", timeline=timeline, name=name, list_id=list_id, parse=parse
)
return TaskModifiedResponse(**result)

async def complete(
self, timeline: int, list_id: int, taskseries_id: int, task_id: int
) -> TaskModifiedResponse:
"""Complete a task."""
result = await self.api.call_api_auth(
"rtm.tasks.complete",
timeline=timeline,
list_id=list_id,
taskseries_id=taskseries_id,
task_id=task_id,
)
return TaskModifiedResponse(**result)

async def delete(
self, timeline: int, list_id: int, taskseries_id: int, task_id: int
) -> TaskModifiedResponse:
"""Delete a task."""
result = await self.api.call_api_auth(
"rtm.tasks.delete",
timeline=timeline,
list_id=list_id,
taskseries_id=taskseries_id,
task_id=task_id,
)
return TaskModifiedResponse(**result)

async def get_list(
self, list_id: int | None = None, last_sync: datetime | None = None
) -> TasksResponse:
"""Get a list of tasks."""
last_sync_string: str | None = None
if last_sync is not None:
last_sync_string = last_sync.isoformat()

result = await self.api.call_api_auth(
"rtm.tasks.getList", list_id=list_id, last_sync=last_sync_string
)
return TasksResponse(**result)
36 changes: 36 additions & 0 deletions tests/fixtures/tasks/add.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"rsp": {
"stat": "ok",
"transaction": { "id": "12451745176", "undoable": "0" },
"list": {
"id": "48730705",
"taskseries": [
{
"id": "493137362",
"created": "2023-01-02T01:55:25Z",
"modified": "2023-01-02T01:55:25Z",
"name": "Test task",
"source": "api:test-api-key",
"url": "",
"location_id": "",
"tags": [],
"participants": [],
"notes": [],
"task": [
{
"id": "924832826",
"due": "",
"has_due_time": "0",
"added": "2023-01-02T01:55:25Z",
"completed": "",
"deleted": "",
"priority": "N",
"postponed": "0",
"estimate": ""
}
]
}
]
}
}
}
36 changes: 36 additions & 0 deletions tests/fixtures/tasks/complete.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"rsp": {
"stat": "ok",
"transaction": { "id": "12475899884", "undoable": "1" },
"list": {
"id": "48730705",
"taskseries": [
{
"id": "493137362",
"created": "2023-01-02T01:55:25Z",
"modified": "2023-01-05T00:04:52Z",
"name": "Test task",
"source": "api:test-api-key",
"url": "",
"location_id": "",
"tags": [],
"participants": [],
"notes": [],
"task": [
{
"id": "924832826",
"due": "",
"has_due_time": "0",
"added": "2023-01-02T01:55:25Z",
"completed": "2023-01-05T00:04:52Z",
"deleted": "",
"priority": "N",
"postponed": "0",
"estimate": ""
}
]
}
]
}
}
}
36 changes: 36 additions & 0 deletions tests/fixtures/tasks/delete.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"rsp": {
"stat": "ok",
"transaction": { "id": "12476056249", "undoable": "1" },
"list": {
"id": "48730705",
"taskseries": [
{
"id": "493137362",
"created": "2023-01-02T01:55:25Z",
"modified": "2023-01-05T00:34:45Z",
"name": "Test task",
"source": "api:test-api-key",
"url": "",
"location_id": "",
"tags": [],
"participants": [],
"notes": [],
"task": [
{
"id": "924832826",
"due": "",
"has_due_time": "0",
"added": "2023-01-02T01:55:25Z",
"completed": "2023-01-05T00:04:52Z",
"deleted": "2023-01-05T00:34:45Z",
"priority": "N",
"postponed": "0",
"estimate": ""
}
]
}
]
}
}
}
25 changes: 25 additions & 0 deletions tests/fixtures/tasks/get_list.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,31 @@
{
"id": "48730705",
"taskseries": [
{
"id": "493137362",
"created": "2023-01-02T01:55:25Z",
"modified": "2023-01-02T01:55:25Z",
"name": "Test task",
"source": "api:test-api-key",
"url": "",
"location_id": "",
"tags": [],
"participants": [],
"notes": [],
"task": [
{
"id": "924832826",
"due": "",
"has_due_time": "0",
"added": "2023-01-02T01:55:25Z",
"completed": "",
"deleted": "",
"priority": "N",
"postponed": "0",
"estimate": ""
}
]
},
{
"id": "475830808",
"created": "2022-05-18T14:27:08Z",
Expand Down
Loading

0 comments on commit a469860

Please sign in to comment.