Add more types to the todoist integration (#84210)

* Add more types to the todoist integration.

* Update tests.

* Update homeassistant/components/todoist/calendar.py

Pass f-string directly to strftime.

Co-authored-by: Allen Porter <allen.porter@gmail.com>

* Add back mistakenly removed local var.

Co-authored-by: Allen Porter <allen.porter@gmail.com>
This commit is contained in:
Aaron Godfrey 2022-12-19 20:29:57 -08:00 committed by GitHub
parent c212e317c3
commit 3405fa60ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 121 additions and 80 deletions

View File

@ -3,6 +3,7 @@ from __future__ import annotations
from datetime import date, datetime, timedelta, timezone
import logging
from typing import Any
from todoist.api import TodoistAPI
import voluptuous as vol
@ -57,7 +58,7 @@ from .const import (
SUMMARY,
TASKS,
)
from .types import DueDate
from .types import CalData, CustomProject, DueDate, ProjectData, TodoistEvent
_LOGGER = logging.getLogger(__name__)
@ -137,8 +138,8 @@ def setup_platform(
for project in projects:
# Project is an object, not a dict!
# Because of that, we convert what we need to a dict.
project_data = {CONF_NAME: project[NAME], CONF_ID: project[ID]}
project_devices.append(TodoistProjectEntity(hass, project_data, labels, api))
project_data: ProjectData = {CONF_NAME: project[NAME], CONF_ID: project[ID]}
project_devices.append(TodoistProjectEntity(project_data, labels, api))
# Cache the names so we can easily look up name->ID.
project_id_lookup[project[NAME].lower()] = project[ID]
@ -150,7 +151,7 @@ def setup_platform(
collaborator_id_lookup[collaborator[FULL_NAME].lower()] = collaborator[ID]
# Check config for more projects.
extra_projects = config[CONF_EXTRA_PROJECTS]
extra_projects: list[CustomProject] = config[CONF_EXTRA_PROJECTS]
for project in extra_projects:
# Special filter: By date
project_due_date = project.get(CONF_PROJECT_DUE_DATE)
@ -169,7 +170,6 @@ def setup_platform(
# Create the custom project and add it to the devices array.
project_devices.append(
TodoistProjectEntity(
hass,
project,
labels,
api,
@ -277,14 +277,13 @@ class TodoistProjectEntity(CalendarEntity):
def __init__(
self,
hass,
data,
labels,
token,
due_date_days=None,
whitelisted_labels=None,
whitelisted_projects=None,
):
data: ProjectData,
labels: list[str],
token: TodoistAPI,
due_date_days: int | None = None,
whitelisted_labels: list[str] | None = None,
whitelisted_projects: list[int] | None = None,
) -> None:
"""Create the Todoist Calendar Entity."""
self.data = TodoistProjectData(
data,
@ -294,17 +293,19 @@ class TodoistProjectEntity(CalendarEntity):
whitelisted_labels,
whitelisted_projects,
)
self._cal_data = {}
self._cal_data: CalData = {}
self._name = data[CONF_NAME]
self._attr_unique_id = data.get(CONF_ID)
self._attr_unique_id = (
str(data[CONF_ID]) if data.get(CONF_ID) is not None else None
)
@property
def event(self) -> CalendarEvent:
def event(self) -> CalendarEvent | None:
"""Return the next upcoming event."""
return self.data.calendar_event
@property
def name(self):
def name(self) -> str:
"""Return the name of the entity."""
return self._name
@ -326,7 +327,7 @@ class TodoistProjectEntity(CalendarEntity):
return await self.data.async_get_events(hass, start_date, end_date)
@property
def extra_state_attributes(self):
def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the device state attributes."""
if self.data.event is None:
# No tasks, we don't REALLY need to show anything.
@ -376,13 +377,13 @@ class TodoistProjectData:
def __init__(
self,
project_data,
labels,
api,
due_date_days=None,
whitelisted_labels=None,
whitelisted_projects=None,
):
project_data: ProjectData,
labels: list[str],
api: TodoistAPI,
due_date_days: int | None = None,
whitelisted_labels: list[str] | None = None,
whitelisted_projects: list[int] | None = None,
) -> None:
"""Initialize a Todoist Project."""
self.event = None
@ -395,26 +396,23 @@ class TodoistProjectData:
self._labels = labels
# Not tracked: order, indent, comment_count.
self.all_project_tasks = []
self.all_project_tasks: list[TodoistEvent] = []
# The days a task can be due (for making lists of everything
# due today, or everything due in the next week, for example).
self._due_date_days: timedelta | None = None
if due_date_days is not None:
self._due_date_days = timedelta(days=due_date_days)
else:
self._due_date_days = None
# Only tasks with one of these labels will be included.
self._label_whitelist: list[str] = []
if whitelisted_labels is not None:
self._label_whitelist = whitelisted_labels
else:
self._label_whitelist = []
# This project includes only projects with these names.
self._project_id_whitelist: list[int] = []
if whitelisted_projects is not None:
self._project_id_whitelist = whitelisted_projects
else:
self._project_id_whitelist = []
@property
def calendar_event(self) -> CalendarEvent | None:
@ -502,7 +500,7 @@ class TodoistProjectData:
return task
@staticmethod
def select_best_task(project_tasks):
def select_best_task(project_tasks: list[TodoistEvent]) -> TodoistEvent:
"""
Search through a list of events for the "best" event to select.
@ -562,14 +560,16 @@ class TodoistProjectData:
continue
if proposed_event[PRIORITY] == event[PRIORITY] and (
proposed_event[END] < event[END]
event[END] is not None and proposed_event[END] < event[END]
):
event = proposed_event
continue
return event
async def async_get_events(self, hass, start_date, end_date):
async def async_get_events(
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
) -> list[CalendarEvent]:
"""Get all tasks in a specific time frame."""
if self._id is None:
project_task_data = [
@ -594,13 +594,14 @@ class TodoistProjectData:
)
if not due_date:
continue
midnight = dt.as_utc(
dt.parse_datetime(
due_date.strftime("%Y-%m-%d")
+ "T00:00:00"
+ self._api.state["user"]["tz_info"]["gmt_string"]
)
gmt_string = self._api.state["user"]["tz_info"]["gmt_string"]
local_midnight = dt.parse_datetime(
due_date.strftime(f"%Y-%m-%dT00:00:00{gmt_string}")
)
if local_midnight is not None:
midnight = dt.as_utc(local_midnight)
else:
midnight = due_date.replace(hour=0, minute=0, second=0, microsecond=0)
if start_date < due_date < end_date:
due_date_value: datetime | date = due_date

View File

@ -1,35 +1,37 @@
"""Constants for the Todoist component."""
CONF_EXTRA_PROJECTS = "custom_projects"
CONF_PROJECT_DUE_DATE = "due_date_days"
CONF_PROJECT_LABEL_WHITELIST = "labels"
CONF_PROJECT_WHITELIST = "include_projects"
from typing import Final
CONF_EXTRA_PROJECTS: Final = "custom_projects"
CONF_PROJECT_DUE_DATE: Final = "due_date_days"
CONF_PROJECT_LABEL_WHITELIST: Final = "labels"
CONF_PROJECT_WHITELIST: Final = "include_projects"
# Calendar Platform: Does this calendar event last all day?
ALL_DAY = "all_day"
ALL_DAY: Final = "all_day"
# Attribute: All tasks in this project
ALL_TASKS = "all_tasks"
ALL_TASKS: Final = "all_tasks"
# Todoist API: "Completed" flag -- 1 if complete, else 0
CHECKED = "checked"
CHECKED: Final = "checked"
# Attribute: Is this task complete?
COMPLETED = "completed"
COMPLETED: Final = "completed"
# Todoist API: What is this task about?
# Service Call: What is this task about?
CONTENT = "content"
CONTENT: Final = "content"
# Calendar Platform: Get a calendar event's description
DESCRIPTION = "description"
DESCRIPTION: Final = "description"
# Calendar Platform: Used in the '_get_date()' method
DATETIME = "dateTime"
DUE = "due"
DATETIME: Final = "dateTime"
DUE: Final = "due"
# Service Call: When is this task due (in natural language)?
DUE_DATE_STRING = "due_date_string"
DUE_DATE_STRING: Final = "due_date_string"
# Service Call: The language of DUE_DATE_STRING
DUE_DATE_LANG = "due_date_lang"
DUE_DATE_LANG: Final = "due_date_lang"
# Service Call: When should user be reminded of this task (in natural language)?
REMINDER_DATE_STRING = "reminder_date_string"
REMINDER_DATE_STRING: Final = "reminder_date_string"
# Service Call: The language of REMINDER_DATE_STRING
REMINDER_DATE_LANG = "reminder_date_lang"
REMINDER_DATE_LANG: Final = "reminder_date_lang"
# Service Call: The available options of DUE_DATE_LANG
DUE_DATE_VALID_LANGS = [
DUE_DATE_VALID_LANGS: Final = [
"en",
"da",
"pl",
@ -47,45 +49,45 @@ DUE_DATE_VALID_LANGS = [
]
# Attribute: When is this task due?
# Service Call: When is this task due?
DUE_DATE = "due_date"
DUE_DATE: Final = "due_date"
# Service Call: When should user be reminded of this task?
REMINDER_DATE = "reminder_date"
REMINDER_DATE: Final = "reminder_date"
# Attribute: Is this task due today?
DUE_TODAY = "due_today"
DUE_TODAY: Final = "due_today"
# Calendar Platform: When a calendar event ends
END = "end"
END: Final = "end"
# Todoist API: Look up a Project/Label/Task ID
ID = "id"
ID: Final = "id"
# Todoist API: Fetch all labels
# Service Call: What are the labels attached to this task?
LABELS = "labels"
LABELS: Final = "labels"
# Todoist API: "Name" value
NAME = "name"
NAME: Final = "name"
# Todoist API: "Full Name" value
FULL_NAME = "full_name"
FULL_NAME: Final = "full_name"
# Attribute: Is this task overdue?
OVERDUE = "overdue"
OVERDUE: Final = "overdue"
# Attribute: What is this task's priority?
# Todoist API: Get a task's priority
# Service Call: What is this task's priority?
PRIORITY = "priority"
PRIORITY: Final = "priority"
# Todoist API: Look up the Project ID a Task belongs to
PROJECT_ID = "project_id"
PROJECT_ID: Final = "project_id"
# Service Call: What Project do you want a Task added to?
PROJECT_NAME = "project"
PROJECT_NAME: Final = "project"
# Todoist API: Fetch all Projects
PROJECTS = "projects"
PROJECTS: Final = "projects"
# Calendar Platform: When does a calendar event start?
START = "start"
START: Final = "start"
# Calendar Platform: What is the next calendar event about?
SUMMARY = "summary"
SUMMARY: Final = "summary"
# Todoist API: Fetch all Tasks
TASKS = "items"
TASKS: Final = "items"
# Todoist API: "responsible" for a Task
ASSIGNEE = "assignee"
ASSIGNEE: Final = "assignee"
# Todoist API: Collaborators in shared projects
COLLABORATORS = "collaborators"
COLLABORATORS: Final = "collaborators"
DOMAIN = "todoist"
DOMAIN: Final = "todoist"
SERVICE_NEW_TASK = "new_task"
SERVICE_NEW_TASK: Final = "new_task"

View File

@ -1,6 +1,7 @@
"""Types for the Todoist component."""
from __future__ import annotations
from datetime import datetime
from typing import TypedDict
@ -12,3 +13,40 @@ class DueDate(TypedDict):
lang: str
string: str
timezone: str | None
class ProjectData(TypedDict):
"""Dict representing project data."""
name: str
id: int | None
class CustomProject(TypedDict):
"""Dict representing a custom project."""
name: str
due_date_days: int | None
include_projects: list[str] | None
labels: list[str] | None
class CalData(TypedDict, total=False):
"""Dict representing calendar data in todoist."""
all_tasks: list[str]
class TodoistEvent(TypedDict):
"""Dict representing a todoist event."""
all_day: bool
completed: bool
description: str
due_today: bool
end: datetime | None
labels: list[str]
overdue: bool
priority: int
start: datetime
summary: str

View File

@ -57,7 +57,7 @@ def mock_state() -> dict[str, Any]:
return {
"collaborators": [],
"labels": [{"name": "label1", "id": 1}],
"projects": [{"id": 12345, "name": "Name"}],
"projects": [{"id": "12345", "name": "Name"}],
}
@ -80,7 +80,7 @@ async def test_calendar_entity_unique_id(todoist_api, hass, state):
registry = entity_registry.async_get(hass)
entity = registry.async_get("calendar.name")
assert 12345 == entity.unique_id
assert "12345" == entity.unique_id
@patch("homeassistant.components.todoist.calendar.TodoistAPI")