diff --git a/.github/workflows/debs.yml b/.github/workflows/debs.yml new file mode 100644 index 0000000..8fe3a31 --- /dev/null +++ b/.github/workflows/debs.yml @@ -0,0 +1,91 @@ +name: Debian packages + +on: + workflow_dispatch: + #push: + #branches: [ main ] + pull_request: + +jobs: + get_version: + name: Get version info + runs-on: ubuntu-22.04 + defaults: + run: + shell: bash + outputs: + version: ${{ steps.git_ver.outputs.version }} + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Get package version + id: git_ver + run: | + version=$(git describe --tags | sed -e "s|-g|+g|") + echo "Version from git: ${version}" + echo "version=${version}" >> $GITHUB_OUTPUT + + build_debs: + name: ${{ matrix.name }} + runs-on: ubuntu-22.04 + needs: [get_version] + + strategy: + fail-fast: false + matrix: + name: [ + x64_bookworm, + x64_trixie, + x64_sid, + ] + + include: + - name: x64_bookworm + dist: bookworm + + - name: x64_trixie + dist: trixie + + - name: x64_sid + dist: sid + + steps: + - name: Check github variables + env: + VERSION: ${{ needs.get_version.outputs.version }} + run: | + echo "Package version from git: ${VERSION}" + + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Fetch debian files + run: | + wget https://launchpad.net/~nerdboy/+archive/ubuntu/embedded/+sourcefiles/timew-addons/0.1.1-1ubuntu1.22.04.1/timew-addons_0.1.1-1ubuntu1.22.04.1.debian.tar.xz + tar xf timew-addons_0.1.1-1ubuntu1.22.04.1.debian.tar.xz && rm *debian.tar.xz + ls + + - name: Install deps and update debian changelog + run: | + sudo apt-get update + sudo apt-get install devscripts + debchange -v ${{ needs.get_version.outputs.version }}-${{ matrix.dist }} -b -M --distribution ${{ matrix.dist }} "ci build" + + - name: Build deb packages + uses: jtdor/build-deb-action@v1 + env: + DEB_BUILD_OPTIONS: noautodbgsym + with: + docker-image: "debian:${{ matrix.dist }}-slim" + buildpackage-opts: --build=binary --no-sign + extra-build-deps: git + + - name: Upload deb files + uses: actions/upload-artifact@v4 + with: + name: "timew-addons_${{ needs.get_version.outputs.version }}-${{ matrix.dist }}" + path: ./debian/artifacts/*.deb diff --git a/.pep8speaks.yml b/.pep8speaks.yml index 04ed79e..d0287a9 100644 --- a/.pep8speaks.yml +++ b/.pep8speaks.yml @@ -11,7 +11,7 @@ exclude = build, dist -max-line-length = 99 +max-line-length = 100 ignore = # too many leading '#' for block comment diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b8f9a0a..4e2c8f1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ repos: - id: check-useless-excludes - id: check-hooks-apply - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.6.0 hooks: - id: check-added-large-files - id: check-case-conflict @@ -28,31 +28,37 @@ repos: - id: check-toml - repo: https://github.com/ambv/black - rev: 23.3.0 + rev: 24.8.0 hooks: - id: black name: "Format code" language_version: python3 - repo: https://github.com/pycqa/isort - rev: 5.12.0 + rev: 5.13.2 hooks: - id: isort args: [--settings-path=pyproject.toml] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.5.0 + rev: v1.11.2 hooks: - id: mypy args: - --follow-imports=normal - --install-types - --non-interactive - - --ignore-missing-imports - #files: src/ + - --implicit-reexport + additional_dependencies: + - "PyYAML" + - "munch" + - "munch-stubs" + - "PyGObject" + - "PyGObject-stubs" + files: src/ - repo: "https://github.com/asottile/blacken-docs" - rev: "1.15.0" + rev: "1.18.0" hooks: - id: "blacken-docs" name: "Format docs (blacken-docs)" @@ -76,7 +82,7 @@ repos: - id: rst-inline-touching-normal - repo: https://github.com/myint/autoflake - rev: v2.2.0 + rev: v2.3.1 hooks: - id: autoflake #files: src/ @@ -87,14 +93,14 @@ repos: - --remove-unused-variables - repo: https://github.com/PyCQA/flake8 - rev: 6.1.0 + rev: 7.1.1 hooks: - id: flake8 #files: src/ additional_dependencies: ["flake8-bugbear"] - repo: https://github.com/PyCQA/bandit - rev: 1.7.5 + rev: 1.7.9 hooks: - id: bandit args: ["-ll", "-q"] diff --git a/README.rst b/README.rst index d9bc9a3..d51ef5a 100644 --- a/README.rst +++ b/README.rst @@ -133,36 +133,42 @@ generated python byte-code. In the latter case, the list of installed files can be obtained with the following command:: $ python -m pip show -f timew-addons - Name: timew-addons - Version: 0.1.2.dev3+gda11428.d20240825 - Summary: A collection of timewarrior extensions and experiments - Home-page: https://github.com/sarnold/timew-addons - Author: Stephen L Arnold - Author-email: - License: - Location: /home/user/src/timew-addons/.tox/check/lib/python3.11/site-packages - Requires: munch, pycairo, PyGObject, timew-report - Required-by: - Files: - ../../../bin/timew-status-indicator - ../../../share/applications/timew-status-indicator.desktop - ../../../share/icons/hicolor/48x48/apps/timew.png - ../../../share/icons/hicolor/scalable/apps/timew.svg - ../../../share/icons/hicolor/scalable/status/timew_error.svg - ../../../share/icons/hicolor/scalable/status/timew_inactive.svg - ../../../share/icons/hicolor/scalable/status/timew_info.svg - ../../../share/icons/hicolor/scalable/status/timew_warning.svg - ../../../share/timew-addons/extensions/csv_rpt.py - ../../../share/timew-addons/extensions/onelineday.py - ../../../share/timew-addons/extensions/totals.py - timew_addons-0.1.2.dev3+gda11428.d20240825.dist-info/INSTALLER - timew_addons-0.1.2.dev3+gda11428.d20240825.dist-info/METADATA - timew_addons-0.1.2.dev3+gda11428.d20240825.dist-info/RECORD - timew_addons-0.1.2.dev3+gda11428.d20240825.dist-info/REQUESTED - timew_addons-0.1.2.dev3+gda11428.d20240825.dist-info/WHEEL - timew_addons-0.1.2.dev3+gda11428.d20240825.dist-info/top_level.txt - timew_status/__init__.py - timew_status/utils.py + Name: timew-addons + Version: 0.2.2.dev0+g4659e21.d20240901 + Summary: A collection of timewarrior extensions and experiments + Home-page: https://github.com/sarnold/timew-addons + Author: Stephen L Arnold + Author-email: nerdboy@gentoo.org + License: GPLv3+ + Location: /home/nerdboy/src/timew-addons/.tox/py/lib/python3.11/site-packages + Requires: munch, pycairo, PyGObject, timew-report + Required-by: + Files: + ../../../bin/timew-status-indicator + ../../../share/applications/timew-status-indicator.desktop + ../../../share/icons/hicolor/48x48/apps/timew.png + ../../../share/icons/hicolor/scalable/apps/timew.svg + ../../../share/icons/hicolor/scalable/status/timew_error.svg + ../../../share/icons/hicolor/scalable/status/timew_inactive.svg + ../../../share/icons/hicolor/scalable/status/timew_info.svg + ../../../share/icons/hicolor/scalable/status/timew_warning.svg + ../../../share/timew-addons/extensions/__pycache__/csv_rpt.cpython-311.pyc + ../../../share/timew-addons/extensions/__pycache__/onelineday.cpython-311.pyc + ../../../share/timew-addons/extensions/__pycache__/totals.cpython-311.pyc + ../../../share/timew-addons/extensions/csv_rpt.py + ../../../share/timew-addons/extensions/onelineday.py + ../../../share/timew-addons/extensions/totals.py + timew_addons-0.2.2.dev0+g4659e21.d20240901.dist-info/INSTALLER + timew_addons-0.2.2.dev0+g4659e21.d20240901.dist-info/METADATA + timew_addons-0.2.2.dev0+g4659e21.d20240901.dist-info/RECORD + timew_addons-0.2.2.dev0+g4659e21.d20240901.dist-info/REQUESTED + timew_addons-0.2.2.dev0+g4659e21.d20240901.dist-info/WHEEL + timew_addons-0.2.2.dev0+g4659e21.d20240901.dist-info/direct_url.json + timew_addons-0.2.2.dev0+g4659e21.d20240901.dist-info/top_level.txt + timew_status/__init__.py + timew_status/__pycache__/__init__.cpython-311.pyc + timew_status/__pycache__/utils.cpython-311.pyc + timew_status/utils.py Generated files --------------- @@ -199,18 +205,23 @@ Uninstalling Depending on how it was installed, use on or more of the following: -* delete the cloned directory, eg, ``rm -rf src/timew-addons`` +* delete the cloned directory, eg, ``rm -rf path/to/timew-addons`` * delete the virtual environment, eg, ``rm -rf ``.venv`` -* remove the OS package, eg, on Ubuntu: + +If you installed into a local env via ``pip`` then run:: + + $ pip uninstall timew-addons + +* or, remove the OS package, eg, on Ubuntu: :: $ sudo apt remove timew-addons $ sudo apt autoremove -Finally, delete the above configuration file:: +Finally, delete the configuration file:: - $ rm ~/.config/timew_status_indicator/config.yaml + $ rm $HOME/.config/timew_status_indicator/config.yaml Reporting examples diff --git a/data/timew-status-indicator.desktop b/data/timew-status-indicator.desktop index e03363d..e99f1ec 100644 --- a/data/timew-status-indicator.desktop +++ b/data/timew-status-indicator.desktop @@ -1,5 +1,5 @@ [Desktop Entry] -Name=Timew Status Tool +Name=Timew Status Indicator Comment=Control and status tool for timew Exec=timew-status-indicator Icon=timew diff --git a/docs/source/index.rst b/docs/source/index.rst index 367b54d..45a70cd 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -24,4 +24,3 @@ Indices and tables * :ref:`genindex` * :ref:`modindex` -* :ref:`search` diff --git a/scripts/timew-status-indicator b/scripts/timew-status-indicator index 217c47a..5b888e4 100755 --- a/scripts/timew-status-indicator +++ b/scripts/timew-status-indicator @@ -15,19 +15,14 @@ from threading import Thread from typing import cast import gi +from munch import Munch gi.require_version("Gtk", "3.0") gi.require_version('Notify', '0.7') +gi.require_version('AyatanaAppIndicator3', '0.1') -try: - gi.require_version('AyatanaAppIndicator3', '0.1') - from gi.repository import AyatanaAppIndicator3 as appindicator -except ValueError: - gi.require_version('AppIndicator3', '0.1') - from gi.repository import AppIndicator3 as appindicator - +from gi.repository import AyatanaAppIndicator3 as appindicator from gi.repository import Gdk, Gtk, Notify -from munch import Munch from timew_status import ( CFG, @@ -123,7 +118,7 @@ class Indicator: new_state = 'INACTIVE' print(f'{new_state} state msg: {result.stdout.decode().strip()}') else: - proc, _ = run_cmd() + proc, _ = run_cmd(CFG) msg, new_state = get_state_str(proc, current_tick_count, CFG) print(f'{new_state} state msg: {msg.strip()}') # if there is a change in state, update the icon @@ -238,18 +233,18 @@ class Indicator: def startd(self, source): my_tag = TAG["text"] - _, svc_msg = run_cmd(action='start', tag=my_tag) + _, svc_msg = run_cmd(CFG, action='start', tag=my_tag) self.indicator.set_icon_full(get_state_icon('ACTIVE', CFG), 'ACTIVE') Notify.Notification.new("Timew status", svc_msg, None).show() def statusd(self, source): - _, svc_msg = run_cmd() + _, svc_msg = run_cmd(CFG) seat_minutes = round(Decimal(COUNT["SeatTick"] / 60), 1) svc_msg = f'INFO: current seat time is {seat_minutes} mins \n' + svc_msg Notify.Notification.new("Timew status", svc_msg, None).show() def stopd(self, source): - _, svc_msg = run_cmd(action='stop') + _, svc_msg = run_cmd(CFG, action='stop') if svc_msg: self.last_tag = svc_msg if CFG['use_last_tag']: diff --git a/setup.cfg b/setup.cfg index 343dfcd..a343d1a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,9 +4,10 @@ version = attr: setuptools_scm.get_version description = A collection of timewarrior extensions and experiments url = https://github.com/sarnold/timew-addons author = Stephen L Arnold -email = nerdboy@gentoo.org +author_email = nerdboy@gentoo.org long_description = file: README.rst long_description_content_type = text/rst; charset=UTF-8 +license = GPLv3+ license_expression = GPL-3.0-or-later license_files = ["LICEN[CS]E*",] classifiers = @@ -24,6 +25,9 @@ classifiers = keywords = appindicator + extensions + timewarrior + timew-report timew [options] @@ -107,7 +111,7 @@ exclude = docs, tests -max-line-length = 99 +max-line-length = 100 max-complexity = 10 addons = file,open,basestring,xrange,unicode,long,cmp ignore = diff --git a/src/timew_status/__init__.py b/src/timew_status/__init__.py index 0cb3deb..5fbad2a 100644 --- a/src/timew_status/__init__.py +++ b/src/timew_status/__init__.py @@ -2,6 +2,7 @@ Monitoring and reporting tools for Timew tracking intervals with optional appindicator alerts for keyboard time and daily hours. """ + import sys from .utils import ( diff --git a/src/timew_status/utils.py b/src/timew_status/utils.py index 1afb845..a1d530f 100644 --- a/src/timew_status/utils.py +++ b/src/timew_status/utils.py @@ -1,13 +1,21 @@ """ Base configuration and app helper functions. """ + +from __future__ import annotations + +import datetime import os import subprocess +import sys from datetime import timedelta from pathlib import Path +from shutil import which +from typing import Dict, List, NewType, Optional, Tuple from munch import Munch +TimeDelta = NewType("TimeDelta", datetime.timedelta) APP_NAME = 'timew_status_indicator' CFG = { # time strings are HH:MM (no seconds) @@ -30,12 +38,29 @@ } -def do_install(cfg): +def check_for_timew() -> str: """ - Install report extensions to timew extensions directory. The default + Make sure we can find the ``timew`` binary in the user environment + and return a path string. + + :return timew_path: program path strings + :rtype str: path to program if found, else None + """ + timew_path = which('timew') + if not timew_path: + print('Cannot continue, no path found for timew') + sys.exit(1) + return timew_path + + +def do_install(cfg: Dict) -> List[str]: + """ + Install report extensions to timew extensions directory. The default src paths are preconfigured and should probably not be changed unless you know what you are doing, since *they are created during install or setup*. - Return a destination path string for each installed extension script. + You should, however, adjust the destination path in ``extensions_dir`` if + needed for your platform. Returns the destination path string for each + installed extension script. :param cfg: runtime CFG dict :return files: list of strings @@ -58,7 +83,7 @@ def do_install(cfg): return files -def get_config(file_encoding='utf-8'): +def get_config(file_encoding: str = 'utf-8') -> Tuple[Munch, Path]: """ Load configuration file and munchify the data. If local file is not found in config directory, the default will be loaded and saved to @@ -72,14 +97,14 @@ def get_config(file_encoding='utf-8'): cfgdir = get_userdirs() cfgfile = cfgdir.joinpath('config.yaml') if not cfgfile.exists(): - print(f"Saving initial config data to {cfgfile}") - cfgfile.write_text(Munch.toYAML(CFG), encoding=file_encoding) - cfgobj = Munch.fromYAML(cfgfile.read_text(encoding=file_encoding)) - + print(f"Saving initial config data to {cfgfile}") # fmt: off + cfgfile.write_text(Munch.toYAML(CFG), encoding=file_encoding) # type: ignore[attr-defined] + cfgobj = Munch.fromYAML(cfgfile.read_text(encoding=file_encoding)) # type: ignore[attr-defined] + # fmt: on return cfgobj, cfgfile -def get_delta_limits(ucfg): +def get_delta_limits(ucfg: Dict) -> Tuple[timedelta, timedelta, timedelta, timedelta]: """ Return config max/snooze limits as timedeltas. Everything comes from static config values and gets padded with seconds. @@ -99,7 +124,7 @@ def get_delta_limits(ucfg): return day_max, day_limit, seat_max, seat_limit -def get_state_icon(state, cfg): +def get_state_icon(state: str, cfg: Dict) -> str: """ Look up the state msg and return the icon name. Use builtin symbolic icons as fallback. @@ -137,7 +162,9 @@ def get_state_icon(state, cfg): return state_dict.get(state, state_dict['INACTIVE']) -def get_state_str(cmproc, count, cfg): +def get_state_str( + cmproc: subprocess.CompletedProcess[bytes], count: TimeDelta, cfg: Dict +) -> Tuple[str, str]: """ Return timew state message and tracking state, ie, the key for dict with icons. @@ -151,7 +178,7 @@ def get_state_str(cmproc, count, cfg): :return: tuple of state msg and state string """ - DAY_MAX, DAY_LIMIT, SEAT_MAX, SEAT_LIMIT = get_delta_limits(cfg) + (DAY_MAX, DAY_LIMIT, SEAT_MAX, SEAT_LIMIT) = get_delta_limits(cfg) state = 'INACTIVE' if cmproc.returncode == 1 else 'ACTIVE' msg = cmproc.stdout.decode('utf8') @@ -176,20 +203,22 @@ def get_state_str(cmproc, count, cfg): return msg, state -def get_status(): +def get_status() -> subprocess.CompletedProcess[bytes]: """ Return timew tracking status (output of ``timew`` with no arguments). :param None: :return: timew output str or None + :raises RuntimeError: for timew not found error """ try: return subprocess.run(["timew"], capture_output=True) except FileNotFoundError as exc: print(f'Timew status error: {exc}') + raise RuntimeError("Did you install timewarrior?") from exc -def get_userdirs(): +def get_userdirs() -> Path: """ Get XDG user configuration path defined as ``XDG_CONFIG_HOME`` plus application name. This may grow if needed. @@ -205,7 +234,7 @@ def get_userdirs(): return configdir -def parse_for_tag(text): +def parse_for_tag(text: str) -> str: """ Parse the output of timew start/stop commands for the tag string. @@ -215,25 +244,31 @@ def parse_for_tag(text): for line in text.splitlines(): if line.startswith(("Tracking", "Recorded")): return line.split('"')[1] + return "Tag extraction error" -def run_cmd(action='status', tag=None): +def run_cmd( + cfg: Dict, action: str = 'status', tag: Optional[str] = None +) -> Tuple[subprocess.CompletedProcess[bytes], str]: """ Run timew command subject to the given action. :param action: one of :return: completed proc obj and result msg + :raises RuntimeError: for timew action error """ - + timew_cmd = check_for_timew() + extension = cfg["extension_script"] actions = ['start', 'stop', 'status'] - svc_list = ['timew'] - sts_list = [CFG["extension_script"], "today"] + svc_list = [timew_cmd] + sts_list = [extension, "today"] cmd = svc_list act_list = [action] if action not in actions: - print(f'Invalid action: {action}') - return + msg = f'Invalid action: {action}' + print(msg) + raise RuntimeError(msg) if action == 'start' and tag: act_list.append(tag) if action != 'status': @@ -257,14 +292,15 @@ def run_cmd(action='status', tag=None): except Exception as exc: print(f'run_cmd exception: {exc}') + raise RuntimeError(f"Timew {action} error") from exc -def to_td(hms): +def to_td(hms: str) -> timedelta: """ Convert a time string in HH:MM:SS format to a timedelta object. :param hms: time string - :return: timedelta obj + :return: timedelta """ hrs, mins, secs = hms.split(':') return timedelta(hours=int(hrs), minutes=int(mins), seconds=int(secs)) diff --git a/tests/test_utils.py b/tests/test_utils.py index 6093eb9..2fe2135 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -3,12 +3,16 @@ from munch import Munch from timew_status.utils import ( + check_for_timew, do_install, get_config, get_delta_limits, get_state_icon, + get_state_str, + get_status, get_userdirs, parse_for_tag, + run_cmd, ) CFG = { @@ -17,6 +21,9 @@ "seat_max": "01:30", "seat_snooze": "00:40", "use_symbolic_icons": False, + "extension_script": "onelineday", + "default_jtag_str": "vct-sw,implement skeleton timew indicator", + "jtag_separator": ",", "extensions_dir": "~/.timewarrior/extensions", "install_dir": "lib/timew-addons/extensions", "install_prefix": "/usr", @@ -38,6 +45,14 @@ """ +def test_timew_check(): + timew = check_for_timew() + print(timew) + assert 'timew' in timew + assert 'bin' in timew + assert isinstance(timew, str) + + def test_do_install(script_loc, tmpdir_session): cfg = Munch.fromDict(CFG) # destination dir @@ -81,9 +96,10 @@ def test_get_state_icon(): def test_get_state_icon_fallback(): states = ['INACTIVE', 'ACTIVE', 'WARNING', 'ERROR', 'APP'] + other = ['DEAD', 'PARROTS', 'TINY', 'FISH'] cfg = Munch.fromDict(CFG) cfg.use_symbolic_icons = True - for state in states: + for state in states + other: icon = get_state_icon(state, cfg.toDict()) assert 'symbolic' in icon print(icon) @@ -91,11 +107,31 @@ def test_get_state_icon_fallback(): def test_get_userdirs(): udir = get_userdirs() + assert '.config' in str(udir) and 'timew_status_indicator' in str(udir) print(f'\nuserdir: {udir}') def test_parse_for_tag(): + expected = "vct-sw,refactor timew indicator config to yaml" ret = parse_for_tag(start_txt) + assert ret == expected print(f'\n{ret}') ret = parse_for_tag(stop_text) + assert ret == expected print(ret) + + +def test_run_cmd(): + proc, msg = run_cmd(CFG) + print(msg) + + +def test_get_state_str(): + proc, _ = run_cmd(CFG) + tick_count = timedelta(seconds=95) + msg, new_state = get_state_str(proc, tick_count, CFG) + + +def test_get_status(): + result = get_status() + print(result) diff --git a/tox.ini b/tox.ini index 13e43e1..1d78974 100644 --- a/tox.ini +++ b/tox.ini @@ -241,6 +241,9 @@ setenv = PYTHONPATH = {toxinidir} deps = pip>=23.1 + munch[yaml] + munch-stubs + PyYAML timew-report mypy diff --git a/toxfile.py b/toxfile.py index ae19a7b..79d59bc 100644 --- a/toxfile.py +++ b/toxfile.py @@ -4,6 +4,7 @@ MIT License Copyright (c) 2023 Masen Furer """ + from contextlib import contextmanager from typing import Any, Iterator, Optional, Sequence, Tuple