From 3405fa60ec2a1b2cc31da8152022ff2598e006bc Mon Sep 17 00:00:00 2001 From: Aaron Godfrey Date: Mon, 19 Dec 2022 20:29:57 -0800 Subject: [PATCH] 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 * Add back mistakenly removed local var. Co-authored-by: Allen Porter --- homeassistant/components/todoist/calendar.py | 83 ++++++++++---------- homeassistant/components/todoist/const.py | 76 +++++++++--------- homeassistant/components/todoist/types.py | 38 +++++++++ tests/components/todoist/test_calendar.py | 4 +- 4 files changed, 121 insertions(+), 80 deletions(-) diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index 30391f90d03..9e14a87c82d 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -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 diff --git a/homeassistant/components/todoist/const.py b/homeassistant/components/todoist/const.py index 26108128ca0..021111b48d7 100644 --- a/homeassistant/components/todoist/const.py +++ b/homeassistant/components/todoist/const.py @@ -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" diff --git a/homeassistant/components/todoist/types.py b/homeassistant/components/todoist/types.py index b9409e44daa..fd3b2e889ca 100644 --- a/homeassistant/components/todoist/types.py +++ b/homeassistant/components/todoist/types.py @@ -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 diff --git a/tests/components/todoist/test_calendar.py b/tests/components/todoist/test_calendar.py index 3458a7bf5d3..b08d96a6b92 100644 --- a/tests/components/todoist/test_calendar.py +++ b/tests/components/todoist/test_calendar.py @@ -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")