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

View File

@ -1,35 +1,37 @@
"""Constants for the Todoist component.""" """Constants for the Todoist component."""
CONF_EXTRA_PROJECTS = "custom_projects" from typing import Final
CONF_PROJECT_DUE_DATE = "due_date_days"
CONF_PROJECT_LABEL_WHITELIST = "labels" CONF_EXTRA_PROJECTS: Final = "custom_projects"
CONF_PROJECT_WHITELIST = "include_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? # Calendar Platform: Does this calendar event last all day?
ALL_DAY = "all_day" ALL_DAY: Final = "all_day"
# Attribute: All tasks in this project # Attribute: All tasks in this project
ALL_TASKS = "all_tasks" ALL_TASKS: Final = "all_tasks"
# Todoist API: "Completed" flag -- 1 if complete, else 0 # Todoist API: "Completed" flag -- 1 if complete, else 0
CHECKED = "checked" CHECKED: Final = "checked"
# Attribute: Is this task complete? # Attribute: Is this task complete?
COMPLETED = "completed" COMPLETED: Final = "completed"
# Todoist API: What is this task about? # Todoist API: What is this task about?
# Service Call: 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 # Calendar Platform: Get a calendar event's description
DESCRIPTION = "description" DESCRIPTION: Final = "description"
# Calendar Platform: Used in the '_get_date()' method # Calendar Platform: Used in the '_get_date()' method
DATETIME = "dateTime" DATETIME: Final = "dateTime"
DUE = "due" DUE: Final = "due"
# Service Call: When is this task due (in natural language)? # 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 # 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)? # 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 # 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 # Service Call: The available options of DUE_DATE_LANG
DUE_DATE_VALID_LANGS = [ DUE_DATE_VALID_LANGS: Final = [
"en", "en",
"da", "da",
"pl", "pl",
@ -47,45 +49,45 @@ DUE_DATE_VALID_LANGS = [
] ]
# Attribute: When is this task due? # Attribute: When is this task due?
# Service Call: 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? # 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? # Attribute: Is this task due today?
DUE_TODAY = "due_today" DUE_TODAY: Final = "due_today"
# Calendar Platform: When a calendar event ends # Calendar Platform: When a calendar event ends
END = "end" END: Final = "end"
# Todoist API: Look up a Project/Label/Task ID # Todoist API: Look up a Project/Label/Task ID
ID = "id" ID: Final = "id"
# Todoist API: Fetch all labels # Todoist API: Fetch all labels
# Service Call: What are the labels attached to this task? # Service Call: What are the labels attached to this task?
LABELS = "labels" LABELS: Final = "labels"
# Todoist API: "Name" value # Todoist API: "Name" value
NAME = "name" NAME: Final = "name"
# Todoist API: "Full Name" value # Todoist API: "Full Name" value
FULL_NAME = "full_name" FULL_NAME: Final = "full_name"
# Attribute: Is this task overdue? # Attribute: Is this task overdue?
OVERDUE = "overdue" OVERDUE: Final = "overdue"
# Attribute: What is this task's priority? # Attribute: What is this task's priority?
# Todoist API: Get a task's priority # Todoist API: Get a task's priority
# Service Call: What is this 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 # 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? # Service Call: What Project do you want a Task added to?
PROJECT_NAME = "project" PROJECT_NAME: Final = "project"
# Todoist API: Fetch all Projects # Todoist API: Fetch all Projects
PROJECTS = "projects" PROJECTS: Final = "projects"
# Calendar Platform: When does a calendar event start? # Calendar Platform: When does a calendar event start?
START = "start" START: Final = "start"
# Calendar Platform: What is the next calendar event about? # Calendar Platform: What is the next calendar event about?
SUMMARY = "summary" SUMMARY: Final = "summary"
# Todoist API: Fetch all Tasks # Todoist API: Fetch all Tasks
TASKS = "items" TASKS: Final = "items"
# Todoist API: "responsible" for a Task # Todoist API: "responsible" for a Task
ASSIGNEE = "assignee" ASSIGNEE: Final = "assignee"
# Todoist API: Collaborators in shared projects # 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.""" """Types for the Todoist component."""
from __future__ import annotations from __future__ import annotations
from datetime import datetime
from typing import TypedDict from typing import TypedDict
@ -12,3 +13,40 @@ class DueDate(TypedDict):
lang: str lang: str
string: str string: str
timezone: str | None 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 { return {
"collaborators": [], "collaborators": [],
"labels": [{"name": "label1", "id": 1}], "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) registry = entity_registry.async_get(hass)
entity = registry.async_get("calendar.name") entity = registry.async_get("calendar.name")
assert 12345 == entity.unique_id assert "12345" == entity.unique_id
@patch("homeassistant.components.todoist.calendar.TodoistAPI") @patch("homeassistant.components.todoist.calendar.TodoistAPI")