Fix todoist parsing due dates for calendar events (#65403)

pull/67839/head
Aaron Godfrey 2022-03-08 00:00:39 -06:00 committed by GitHub
parent 00c84d8927
commit d302b0d14e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 75 additions and 10 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
"""Tests for the Todoist integration."""

View File

@ -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