Fix todoist parsing due dates for calendar events (#65403)
parent
00c84d8927
commit
d302b0d14e
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
"""Tests for the Todoist integration."""
|
|
@ -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
|
Loading…
Reference in New Issue