diff --git a/bumblebee_status/modules/contrib/wakatime.py b/bumblebee_status/modules/contrib/wakatime.py new file mode 100644 index 0000000..7e4e33e --- /dev/null +++ b/bumblebee_status/modules/contrib/wakatime.py @@ -0,0 +1,95 @@ +# pylint: disable=C0111,R0903 + +""" +Displays the WakaTime daily/weekly/monthly times: + + * https://wakatime.com/developers#stats + +Uses `xdg-open` or `x-www-browser` to open web-pages. + +Requires the following library: + * requests + +Errors: + if the Wakatime status query failed, the shown value is `n/a` + +Parameters: + * wakatime.token: Wakatime secret api key, you can get it in https://wakatime.com/settings/account. + * wakatime.range: Range of the output, default is "Today". Can be one of “Today”, “Yesterday”, “Last 7 Days”, “Last 7 Days from Yesterday”, “Last 14 Days”, “Last 30 Days”, “This Week”, “Last Week”, “This Month”, or “Last Month”. + * wakatime.format: Format of the output, default is "digital" + Valid inputs are: + * "decimal" -> 1.37 + * "digital" -> 1:22 + * "seconds" -> 4931.29 + * "text" -> 1 hr 22 mins + * "%H:%M:%S" -> 01:22:31 (or any other valid format) +""" + +import base64 +import shutil +import time +from typing import Final, List + +import requests + +import core.decorators +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"] + + +class Module(core.module.Module): + @core.decorators.every(minutes=5) + def __init__(self, config, theme): + super().__init__(config, theme, core.widget.Widget(self.wakatime)) + + self.background = True + self.__label = "" + + self.__output_format = self.parameter("format", "digital") + self.__range = self.parameter("range", "Today") + + self.__requests = requests.Session() + + token = self.__encode_to_base_64(self.parameter("token", "")) + self.__requests.headers.update({"Authorization": f"Basic {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_API}/dashboard", + ) + + def wakatime(self, _): + return self.__label + + def update(self): + try: + self.__label = self.__get_waka_time(self.__range) + except Exception: + self.__label = "n/a" + + def __get_waka_time(self, since_date: str) -> str: + response = self.__requests.get(f"{SUMMARIES_URL}?range={since_date}") + + data = response.json() + grand_total = data["cumulative_total"] + + if self.__output_format in FORMAT_PARAMETERS: + return str(grand_total[self.__output_format]) + else: + total_seconds = int(grand_total["seconds"]) + return time.strftime(self.__output_format, time.gmtime(total_seconds)) + + @staticmethod + def __encode_to_base_64(s: str) -> str: + return base64.b64encode(s.encode(UTF8)).decode(UTF8) diff --git a/screenshots/wakatime.png b/screenshots/wakatime.png new file mode 100644 index 0000000..82c8c79 Binary files /dev/null and b/screenshots/wakatime.png differ diff --git a/tests/modules/contrib/test_wakatime.py b/tests/modules/contrib/test_wakatime.py new file mode 100644 index 0000000..d6091b4 --- /dev/null +++ b/tests/modules/contrib/test_wakatime.py @@ -0,0 +1,56 @@ +from unittest import TestCase, mock + +import pytest +from requests import Session + +import core.config +import core.widget +import modules.contrib.wakatime + +pytest.importorskip("requests") + + +def build_wakatime_module(waka_format=None, waka_range=None): + config = core.config.Config([ + "-p", + f"wakatime.format={waka_format}" if waka_format else "", + f"wakatime.range={waka_range}" if waka_range else "" + ]) + + return modules.contrib.wakatime.Module(config=config, theme=None) + + +def mock_todo_api_response(): + res = mock.Mock() + res.json = lambda: { + "cumulative_total": { + "text": "3 hrs 2 mins", + "seconds": 10996, + "digital": "3:02", + "decimal": "3.03" + }, + } + + res.status_code = 200 + return res + + +class TestWakatimeUnit(TestCase): + def test_load_module(self): + __import__("modules.contrib.wakatime") + + @mock.patch.object(Session, "get", return_value=mock_todo_api_response()) + def test_default_values(self, mock_get): + module = build_wakatime_module() + module.update() + assert module.widgets()[0].full_text() == "3:02" + + mock_get.assert_called_with('https://wakatime.com/api/v1/users/current/summaries?range=today') + + @mock.patch.object(Session, "get", return_value=mock_todo_api_response()) + def test_custom_configs(self, mock_get): + module = build_wakatime_module(waka_format="text", waka_range="last 7 days") + module.update() + assert module.widgets()[0].full_text() == "3 hrs 2 mins" + + mock_get.assert_called_with('https://wakatime.com/api/v1/users/current/summaries?range=last 7 days') diff --git a/themes/icons/awesome-fonts.json b/themes/icons/awesome-fonts.json index 4015e43..cd4191a 100644 --- a/themes/icons/awesome-fonts.json +++ b/themes/icons/awesome-fonts.json @@ -584,6 +584,9 @@ "gitlab": { "prefix": "" }, + "wakatime": { + "prefix": "W" + }, "deezer": { "prefix": "  " },