From d302b0d14e9df9cc46e7e035a0d2be5290182b40 Mon Sep 17 00:00:00 2001 From: Aaron Godfrey <me@aarongodfrey.dev> Date: Tue, 8 Mar 2022 00:00:39 -0600 Subject: [PATCH] Fix todoist parsing due dates for calendar events (#65403) --- CODEOWNERS | 1 + homeassistant/components/todoist/calendar.py | 22 +++++----- homeassistant/components/todoist/types.py | 14 +++++++ requirements_test_all.txt | 3 ++ tests/components/todoist/__init__.py | 1 + tests/components/todoist/test_calendar.py | 44 ++++++++++++++++++++ 6 files changed, 75 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/todoist/types.py create mode 100644 tests/components/todoist/__init__.py create mode 100644 tests/components/todoist/test_calendar.py diff --git a/CODEOWNERS b/CODEOWNERS index cd946ea5382..6d145a064a1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1022,6 +1022,7 @@ homeassistant/components/time_date/* @fabaff tests/components/time_date/* @fabaff homeassistant/components/tmb/* @alemuro homeassistant/components/todoist/* @boralyl +tests/components/todoist/* @boralyl homeassistant/components/tolo/* @MatthiasLohr tests/components/tolo/* @MatthiasLohr homeassistant/components/totalconnect/* @austinmroczek diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index 560ad3efe1f..4582b1de0f0 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -1,7 +1,7 @@ """Support for Todoist task management (https://todoist.com).""" from __future__ import annotations -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone import logging from todoist.api import TodoistAPI @@ -55,6 +55,7 @@ from .const import ( SUMMARY, TASKS, ) +from .types import DueDate _LOGGER = logging.getLogger(__name__) @@ -219,7 +220,7 @@ def setup_platform( due_date = datetime(due.year, due.month, due.day) # Format it in the manner Todoist expects due_date = dt.as_utc(due_date) - date_format = "%Y-%m-%dT%H:%M%S" + date_format = "%Y-%m-%dT%H:%M:%S" _due["date"] = datetime.strftime(due_date, date_format) if _due: @@ -258,15 +259,15 @@ def setup_platform( ) -def _parse_due_date(data: dict, gmt_string) -> datetime | None: - """Parse the due date dict into a datetime object.""" - # Add time information to date only strings. - if len(data["date"]) == 10: - return datetime.fromisoformat(data["date"]).replace(tzinfo=dt.UTC) +def _parse_due_date(data: DueDate, timezone_offset: int) -> datetime | None: + """Parse the due date dict into a datetime object in UTC. + + This function will always return a timezone aware datetime if it can be parsed. + """ if not (nowtime := dt.parse_datetime(data["date"])): return None if nowtime.tzinfo is None: - data["date"] += gmt_string + nowtime = nowtime.replace(tzinfo=timezone(timedelta(hours=timezone_offset))) return dt.as_utc(nowtime) @@ -441,7 +442,7 @@ class TodoistProjectData: task[START] = dt.utcnow() if data[DUE] is not None: task[END] = _parse_due_date( - data[DUE], self._api.state["user"]["tz_info"]["gmt_string"] + data[DUE], self._api.state["user"]["tz_info"]["hours"] ) if self._due_date_days is not None and ( @@ -564,8 +565,9 @@ class TodoistProjectData: for task in project_task_data: if task["due"] is None: continue + # @NOTE: _parse_due_date always returns the date in UTC time. due_date = _parse_due_date( - task["due"], self._api.state["user"]["tz_info"]["gmt_string"] + task["due"], self._api.state["user"]["tz_info"]["hours"] ) if not due_date: continue diff --git a/homeassistant/components/todoist/types.py b/homeassistant/components/todoist/types.py new file mode 100644 index 00000000000..b9409e44daa --- /dev/null +++ b/homeassistant/components/todoist/types.py @@ -0,0 +1,14 @@ +"""Types for the Todoist component.""" +from __future__ import annotations + +from typing import TypedDict + + +class DueDate(TypedDict): + """Dict representing a due date in a todoist api response.""" + + date: str + is_recurring: bool + lang: str + string: str + timezone: str | None diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cfe57484344..f3bdb7d7549 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1446,6 +1446,9 @@ tesla-powerwall==0.3.17 # homeassistant.components.tesla_wall_connector tesla-wall-connector==1.0.1 +# homeassistant.components.todoist +todoist-python==8.0.0 + # homeassistant.components.tolo tololib==0.1.0b3 diff --git a/tests/components/todoist/__init__.py b/tests/components/todoist/__init__.py new file mode 100644 index 00000000000..e3b605f8f14 --- /dev/null +++ b/tests/components/todoist/__init__.py @@ -0,0 +1 @@ +"""Tests for the Todoist integration.""" diff --git a/tests/components/todoist/test_calendar.py b/tests/components/todoist/test_calendar.py new file mode 100644 index 00000000000..2f0832fe739 --- /dev/null +++ b/tests/components/todoist/test_calendar.py @@ -0,0 +1,44 @@ +"""Unit tests for the Todoist calendar platform.""" +from datetime import datetime + +from homeassistant.components.todoist.calendar import _parse_due_date +from homeassistant.components.todoist.types import DueDate +from homeassistant.util import dt + + +def test_parse_due_date_invalid(): + """Test None is returned if the due date can't be parsed.""" + data: DueDate = { + "date": "invalid", + "is_recurring": False, + "lang": "en", + "string": "", + "timezone": None, + } + assert _parse_due_date(data, timezone_offset=-8) is None + + +def test_parse_due_date_with_no_time_data(): + """Test due date is parsed correctly when it has no time data.""" + data: DueDate = { + "date": "2022-02-02", + "is_recurring": False, + "lang": "en", + "string": "Feb 2 2:00 PM", + "timezone": None, + } + actual = _parse_due_date(data, timezone_offset=-8) + assert datetime(2022, 2, 2, 8, 0, 0, tzinfo=dt.UTC) == actual + + +def test_parse_due_date_without_timezone_uses_offset(): + """Test due date uses user local timezone offset when it has no timezone.""" + data: DueDate = { + "date": "2022-02-02T14:00:00", + "is_recurring": False, + "lang": "en", + "string": "Feb 2 2:00 PM", + "timezone": None, + } + actual = _parse_due_date(data, timezone_offset=-8) + assert datetime(2022, 2, 2, 22, 0, 0, tzinfo=dt.UTC) == actual -- GitLab