diff --git a/bumblebee_status/modules/contrib/todoist.py b/bumblebee_status/modules/contrib/todoist.py new file mode 100644 index 0000000..5fae228 --- /dev/null +++ b/bumblebee_status/modules/contrib/todoist.py @@ -0,0 +1,76 @@ +# pylint: disable=C0111,R0903 + +""" +Displays the nº of Todoist tasks that are due: + + * https://developer.todoist.com/rest/v2/#get-active-tasks + +Uses `xdg-open` or `x-www-browser` to open web-pages. + +Requires the following library: + * requests + +Errors: + if the Todoist get active tasks query failed, the shown value is `n/a` + +Parameters: + * todoist.token: Todoist api token, you can get it in https://todoist.com/app/settings/integrations/developer. + * todoist.filter: a filter statement defined by Todoist (https://todoist.com/help/articles/introduction-to-filters), eg: "!assigned to: others & (Overdue | due: today)" +""" + +import shutil + +import requests + +import core.decorators +import core.input +import core.module +import core.widget + +HOST_API = "https://api.todoist.com" +HOST_WEBSITE = "https://todoist.com/app/today" + +TASKS_URL = f"{HOST_API}/rest/v2/tasks" + + +class Module(core.module.Module): + @core.decorators.every(minutes=5) + def __init__(self, config, theme): + super().__init__(config, theme, core.widget.Widget(self.todoist)) + + self.__user_id = None + self.background = True + self.__label = "" + + token = self.parameter("token", "") + self.__filter = self.parameter("filter", "") + + self.__requests = requests.Session() + self.__requests.headers.update({"Authorization": f"Bearer {token}"}) + + cmd = "xdg-open" + if not shutil.which(cmd): + cmd = "x-www-browser" + + core.input.register( + self, + button=core.input.LEFT_MOUSE, + cmd=f"{cmd} {HOST_WEBSITE}", + ) + + def todoist(self, _): + return self.__label + + def update(self): + try: + self.__label = self.__get_pending_tasks() + except Exception: + self.__label = "n/a" + + def __get_pending_tasks(self) -> str: + params = {"filter": self.__filter} if self.__filter else None + + response = self.__requests.get(TASKS_URL, params=params) + data = response.json() + + return str(len(data)) diff --git a/bumblebee_status/modules/contrib/wakatime.py b/bumblebee_status/modules/contrib/wakatime.py index 7e4e33e..1ac4461 100644 --- a/bumblebee_status/modules/contrib/wakatime.py +++ b/bumblebee_status/modules/contrib/wakatime.py @@ -28,7 +28,6 @@ Parameters: import base64 import shutil import time -from typing import Final, List import requests @@ -37,10 +36,10 @@ import core.input import core.module import core.widget -HOST_API: Final[str] = "https://wakatime.com" -SUMMARIES_URL: Final[str] = f"{HOST_API}/api/v1/users/current/summaries" -UTF8: Final[str] = "utf-8" -FORMAT_PARAMETERS: Final[List[str]] = ["decimal", "digital", "seconds", "text"] +HOST_API = "https://wakatime.com" +SUMMARIES_URL = f"{HOST_API}/api/v1/users/current/summaries" +UTF8 = "utf-8" +FORMAT_PARAMETERS = ["decimal", "digital", "seconds", "text"] class Module(core.module.Module): diff --git a/screenshots/todoist.png b/screenshots/todoist.png new file mode 100644 index 0000000..66e1bae Binary files /dev/null and b/screenshots/todoist.png differ diff --git a/tests/modules/contrib/test_todoist.py b/tests/modules/contrib/test_todoist.py new file mode 100644 index 0000000..75d42a7 --- /dev/null +++ b/tests/modules/contrib/test_todoist.py @@ -0,0 +1,58 @@ +from unittest import TestCase, mock + +import pytest +from requests import Session + +import core.config +import core.widget +import modules.contrib.todoist + +pytest.importorskip("requests") + + +def build_todoist_module(todoist_filter=None): + config = core.config.Config([ + "-p", + f"todoist.filter={todoist_filter}" if todoist_filter else "" + ]) + + return modules.contrib.todoist.Module(config=config, theme=None) + + +def mock_tasks_api_response(): + res = mock.Mock() + res.json = lambda: [ + { + "id": "-1", + "project_id": "-1" + }, + { + "id": "-2", + "project_id": "-2" + } + ] + + res.status_code = 200 + return res + + +class TestTodoistUnit(TestCase): + def test_load_module(self): + __import__("modules.contrib.todoist") + + @mock.patch.object(Session, "get", return_value=mock_tasks_api_response()) + def test_default_values(self, mock_get): + module = build_todoist_module() + module.update() + assert module.widgets()[0].full_text() == "2" + + mock_get.assert_called_with('https://api.todoist.com/rest/v2/tasks', params=None) + + @mock.patch.object(Session, "get", return_value=mock_tasks_api_response()) + def test_custom_filter(self, mock_get): + module = build_todoist_module(todoist_filter="!assigned to: others & (Overdue | due: today)") + module.update() + assert module.widgets()[0].full_text() == "2" + + mock_get.assert_called_with('https://api.todoist.com/rest/v2/tasks', + params={'filter': '!assigned to: others & (Overdue | due: today)'}) diff --git a/themes/icons/awesome-fonts.json b/themes/icons/awesome-fonts.json index e9ec1b8..60f8f51 100644 --- a/themes/icons/awesome-fonts.json +++ b/themes/icons/awesome-fonts.json @@ -587,6 +587,9 @@ "wakatime": { "prefix": "\uF017" }, + "todoist": { + "prefix": "\uF14A" + }, "deezer": { "prefix": "  " },