From de16d02c989c093578745f2227784293a8957ca2 Mon Sep 17 00:00:00 2001 From: Petar Andric Date: Mon, 13 Oct 2025 23:25:54 +0200 Subject: [PATCH 1/7] feat: add flags for get-status subcommand --- src/woffu_client/cli.py | 27 ++++++++++++++-- src/woffu_client/woffu_api_client.py | 47 +++++++++++++++++++++++++++- 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/src/woffu_client/cli.py b/src/woffu_client/cli.py index 4bba701..e18e8c5 100644 --- a/src/woffu_client/cli.py +++ b/src/woffu_client/cli.py @@ -68,11 +68,34 @@ def main() -> None: ) # ---- get_status ---- - subparsers.add_parser( + st_parser = subparsers.add_parser( "get-status", help="Get current status and current day's \ total amount of worked hours", ) + st_period = st_parser.add_mutually_exclusive_group() + st_period.add_argument( + "--week", + dest="period", + action="store_const", + const="week", + help="Get current week's total hours" + ) + st_period.add_argument( + "--month", + dest="period", + action="store_const", + const="month", + help="Get current month's total hours" + ) + st_period.add_argument( + "--year", + dest="period", + action="store_const", + const="year", + help="Get current year's total hours" + ) + st_parser.set_defaults(period="today") # ---- sign ---- sign_parser = subparsers.add_parser( @@ -144,7 +167,7 @@ def main() -> None: sys.exit(1) case "get-status": try: - client.get_status() + client.get_status(period=args.period) except Exception as e: print(f"❌ Error retrieving status: {e}", file=sys.stderr) case "sign": diff --git a/src/woffu_client/woffu_api_client.py b/src/woffu_client/woffu_api_client.py index f51c444..3b9174e 100644 --- a/src/woffu_client/woffu_api_client.py +++ b/src/woffu_client/woffu_api_client.py @@ -10,6 +10,8 @@ import os import sys import zoneinfo +import calendar +from datetime import date from datetime import datetime from datetime import timedelta from getpass import getpass @@ -417,9 +419,15 @@ def get_sign_requests(self, date: str) -> dict | list: return {} def get_status( - self, only_running_clock: bool = False, + self, only_running_clock: bool = False, period: str = "today" ) -> tuple[timedelta, bool]: """Return the total amount of worked hours and current sign status.""" + + # Retrieve info for the period flags, e.g., --week, --month, --year + if period != "today": + return self._get_status_period(period), None + + # Get current sign-in status and worked hours for today signs_in_day = self.get(url=f"{self._woffu_api_url}/api/signs").json() # Initialize a timer and the running clock boolean @@ -487,6 +495,43 @@ def get_status( ) return total_time, running_clock + def _get_status_period(self, period: str) -> timedelta: + match period: + case "week": + today = date.today() + from_date = today - timedelta(days=today.weekday()) + to_date = from_date + timedelta(days=6) + case "month": + today = date.today() + from_date = today.replace(day=1) + last_day = calendar.monthrange(today.year, today.month)[1] + to_date = today.replace(day=last_day) + case "year": + today = date.today() + from_date = date(today.year, 1, 1) + to_date = date(today.year, 12, 31) + + diary_json = self.get( + url=f"https://{self._domain}/api/svc/core/diariesquery/users/\ +{self._user_id}/diaries/summary/presence", + params={ + "fromDate": from_date, + "toDate": to_date, + "showHidden": False, + }, + ).json() + + h, m = map(int, diary_json["totalWorkedTimeFormatted"]["values"]) + logger.info( + "Hours worked this {}: {:02d}:{:02d}".format(period, h, m) + ) + total_time = timedelta(hours=h, minutes=m) + + h, m = diary_json["totalWorkingTimeFormatted"]["values"] + logger.info(f"Hours scheduled for this {period}: {h}:{m}") + + return total_time + def sign(self, type: str = "") -> HTTPResponse | None: """ Sign in/out on Woffu. From 99fc3260e462d0b633dd44ff2802c1a95534845e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 21:31:12 +0000 Subject: [PATCH 2/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/woffu_client/cli.py | 6 +++--- src/woffu_client/woffu_api_client.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/woffu_client/cli.py b/src/woffu_client/cli.py index e18e8c5..957410e 100644 --- a/src/woffu_client/cli.py +++ b/src/woffu_client/cli.py @@ -79,21 +79,21 @@ def main() -> None: dest="period", action="store_const", const="week", - help="Get current week's total hours" + help="Get current week's total hours", ) st_period.add_argument( "--month", dest="period", action="store_const", const="month", - help="Get current month's total hours" + help="Get current month's total hours", ) st_period.add_argument( "--year", dest="period", action="store_const", const="year", - help="Get current year's total hours" + help="Get current year's total hours", ) st_parser.set_defaults(period="today") diff --git a/src/woffu_client/woffu_api_client.py b/src/woffu_client/woffu_api_client.py index 3b9174e..ba4b55e 100644 --- a/src/woffu_client/woffu_api_client.py +++ b/src/woffu_client/woffu_api_client.py @@ -4,13 +4,13 @@ """ from __future__ import annotations +import calendar import csv import json import logging import os import sys import zoneinfo -import calendar from datetime import date from datetime import datetime from datetime import timedelta @@ -419,7 +419,7 @@ def get_sign_requests(self, date: str) -> dict | list: return {} def get_status( - self, only_running_clock: bool = False, period: str = "today" + self, only_running_clock: bool = False, period: str = "today", ) -> tuple[timedelta, bool]: """Return the total amount of worked hours and current sign status.""" @@ -523,7 +523,7 @@ def _get_status_period(self, period: str) -> timedelta: h, m = map(int, diary_json["totalWorkedTimeFormatted"]["values"]) logger.info( - "Hours worked this {}: {:02d}:{:02d}".format(period, h, m) + "Hours worked this {}: {:02d}:{:02d}".format(period, h, m), ) total_time = timedelta(hours=h, minutes=m) From 6d1a6e3f63ba4b7b9eb5cee4e8727a9012fff43c Mon Sep 17 00:00:00 2001 From: Petar Andric Date: Tue, 14 Oct 2025 00:31:13 +0200 Subject: [PATCH 3/7] fix: run pre-commit checks --- src/woffu_client/woffu_api_client.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/woffu_client/woffu_api_client.py b/src/woffu_client/woffu_api_client.py index ba4b55e..8d46b31 100644 --- a/src/woffu_client/woffu_api_client.py +++ b/src/woffu_client/woffu_api_client.py @@ -11,7 +11,7 @@ import os import sys import zoneinfo -from datetime import date +from datetime import date as dt_date from datetime import datetime from datetime import timedelta from getpass import getpass @@ -422,7 +422,6 @@ def get_status( self, only_running_clock: bool = False, period: str = "today", ) -> tuple[timedelta, bool]: """Return the total amount of worked hours and current sign status.""" - # Retrieve info for the period flags, e.g., --week, --month, --year if period != "today": return self._get_status_period(period), None @@ -496,20 +495,18 @@ def get_status( return total_time, running_clock def _get_status_period(self, period: str) -> timedelta: + today = dt_date.today() match period: case "week": - today = date.today() from_date = today - timedelta(days=today.weekday()) to_date = from_date + timedelta(days=6) case "month": - today = date.today() from_date = today.replace(day=1) last_day = calendar.monthrange(today.year, today.month)[1] to_date = today.replace(day=last_day) case "year": - today = date.today() - from_date = date(today.year, 1, 1) - to_date = date(today.year, 12, 31) + from_date = dt_date(today.year, 1, 1) + to_date = dt_date(today.year, 12, 31) diary_json = self.get( url=f"https://{self._domain}/api/svc/core/diariesquery/users/\ From 59443e8835cc4ae834eab2de9d50d232a4276b64 Mon Sep 17 00:00:00 2001 From: Petar Andric Date: Thu, 16 Oct 2025 00:12:42 +0200 Subject: [PATCH 4/7] upd: add tests --- tests/test_cli.py | 56 +++++++++++++++++++++++++++++----- tests/test_woffu_api_client.py | 33 ++++++++++++++++++++ 2 files changed, 82 insertions(+), 7 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 37de677..444795c 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -67,23 +67,65 @@ def test_download_all_documents_failure(self, mock_client_cls): self.assertIn("❌ Error downloading files: Boom!", error_output) @patch("src.woffu_client.cli.WoffuAPIClient") - def test_get_status_success(self, mock_client_cls): - """Test status retrieval success.""" + def test_get_status_today(self, mock_client_cls): + """Test get-status without a flag (today).""" + self._test_get_status(mock_client_cls, None) + + @patch("src.woffu_client.cli.WoffuAPIClient") + def test_get_status_week(self, mock_client_cls): + """Test get-status with --week flag.""" + self._test_get_status(mock_client_cls, "--week") + + @patch("src.woffu_client.cli.WoffuAPIClient") + def test_get_status_month(self, mock_client_cls): + """Test get-status with --month flag.""" + self._test_get_status(mock_client_cls, "--month") + + @patch("src.woffu_client.cli.WoffuAPIClient") + def test_get_status_year(self, mock_client_cls): + """Test get-status with --year flag.""" + self._test_get_status(mock_client_cls, "--year") + + def _test_get_status(self, mock_client_cls, flag): mock_client = mock_client_cls.return_value mock_client.get_status.return_value = None - with patch.object(sys, "argv", ["cli", "get-status"]): + argv = ["cli", "get-status"] + if flag: + argv.append(flag) + with patch.object(sys, "argv", argv): cli.main() - mock_client.get_status.assert_called_once() @patch("src.woffu_client.cli.WoffuAPIClient") - def test_get_status_failure(self, mock_client_cls): - """Test status retrieval failure.""" + def test_get_status_failure_today(self, mock_client_cls): + """Test failure for get-status without a flag (today).""" + self._test_get_status_failure(mock_client_cls, None) + + @patch("src.woffu_client.cli.WoffuAPIClient") + def test_get_status_failure_week(self, mock_client_cls): + """Test failure for get-status with --week flag.""" + self._test_get_status_failure(mock_client_cls, "--week") + + @patch("src.woffu_client.cli.WoffuAPIClient") + def test_get_status_failure_month(self, mock_client_cls): + """Test failure for get-status with --month flag.""" + self._test_get_status_failure(mock_client_cls, "--month") + + @patch("src.woffu_client.cli.WoffuAPIClient") + def test_get_status_failure_year(self, mock_client_cls): + """Test failure for get-status with --year flag.""" + self._test_get_status_failure(mock_client_cls, "--year") + + def _test_get_status_failure(self, mock_client_cls, flag): mock_client = mock_client_cls.return_value mock_client.get_status.side_effect = Exception("status failed") - with patch.object(sys, "argv", ["cli", "get-status"]): + argv = ["cli", "get-status"] + if flag: + argv.append(flag) + + with patch.object(sys, "argv", argv): cli.main() error_output = cast(StringIO, sys.stderr).getvalue() diff --git a/tests/test_woffu_api_client.py b/tests/test_woffu_api_client.py index d890751..2502d4c 100644 --- a/tests/test_woffu_api_client.py +++ b/tests/test_woffu_api_client.py @@ -405,6 +405,39 @@ def test_get_diary_hour_types_missing_key(self, mock_get): result = self.client._get_diary_hour_types("2025-09-12") self.assertEqual(result, {}) + @patch.object(WoffuAPIClient, "get") + def test_get_status_period_week(self, mock_get): + """Test get-status --week.""" + self._test_get_status_period(mock_get, "week") + + @patch.object(WoffuAPIClient, "get") + def test_get_status_period_month(self, mock_get): + """Test get-status --month.""" + self._test_get_status_period(mock_get, "month") + + @patch.object(WoffuAPIClient, "get") + def test_get_status_period_year(self, mock_get): + """Test get-status --year.""" + self._test_get_status_period(mock_get, "year") + + def _test_get_status_period(self, mock_get, period): + mock_get.return_value.status = 200 + mock_get.return_value.json.return_value = { + "totalWorkingTimeFormatted": { + "resource": "_HoursMinutesFormatted", + "values": ["225", "0"], + }, + "totalWorkedTimeFormatted": { + "resource": "_HoursMinutesFormatted", + "values": ["42", "24"], + }, + } + + total, running = self.client.get_status(period=period) + self.assertIsInstance(total, timedelta) + self.assertIsNone(running) + mock_get.assert_called_once() + @patch.object(WoffuAPIClient, "get") def test_download_document_file_exists(self, mock_get): """Test download_document skips download if file already exists.""" From 28a318f403975f51c9a2634f492280dd9a70b772 Mon Sep 17 00:00:00 2001 From: Petar Andric Date: Thu, 16 Oct 2025 00:47:00 +0200 Subject: [PATCH 5/7] upd: avoid importing date --- src/woffu_client/woffu_api_client.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/woffu_client/woffu_api_client.py b/src/woffu_client/woffu_api_client.py index 8d46b31..1fbd873 100644 --- a/src/woffu_client/woffu_api_client.py +++ b/src/woffu_client/woffu_api_client.py @@ -11,7 +11,6 @@ import os import sys import zoneinfo -from datetime import date as dt_date from datetime import datetime from datetime import timedelta from getpass import getpass @@ -495,7 +494,7 @@ def get_status( return total_time, running_clock def _get_status_period(self, period: str) -> timedelta: - today = dt_date.today() + today = datetime.now().date() match period: case "week": from_date = today - timedelta(days=today.weekday()) @@ -505,8 +504,8 @@ def _get_status_period(self, period: str) -> timedelta: last_day = calendar.monthrange(today.year, today.month)[1] to_date = today.replace(day=last_day) case "year": - from_date = dt_date(today.year, 1, 1) - to_date = dt_date(today.year, 12, 31) + from_date = today.replace(month=1, day=1) + to_date = today.replace(month=12, day=31) diary_json = self.get( url=f"https://{self._domain}/api/svc/core/diariesquery/users/\ From 466e06361d4c6c343422d25ff904e33115813030 Mon Sep 17 00:00:00 2001 From: Petar Andric Date: Thu, 16 Oct 2025 18:38:57 +0200 Subject: [PATCH 6/7] fix: make types consistent --- src/woffu_client/woffu_api_client.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/woffu_client/woffu_api_client.py b/src/woffu_client/woffu_api_client.py index 1fbd873..d786c42 100644 --- a/src/woffu_client/woffu_api_client.py +++ b/src/woffu_client/woffu_api_client.py @@ -16,6 +16,7 @@ from getpass import getpass from operator import itemgetter from pathlib import Path +from typing import Optional from tzlocal import get_localzone @@ -328,6 +329,12 @@ def _get_presence( from_date = current_date.strftime(DEFAULT_DATE_FORMAT) to_date = current_date.strftime(DEFAULT_DATE_FORMAT) + try: + datetime.strptime(from_date, DEFAULT_DATE_FORMAT) + datetime.strptime(to_date, DEFAULT_DATE_FORMAT) + except ValueError: + raise + hours_response = self.get( url=f"https://{self._domain}/api/svc/core/diariesquery/users/\ {self._user_id}/diaries/summary/presence", @@ -419,7 +426,7 @@ def get_sign_requests(self, date: str) -> dict | list: def get_status( self, only_running_clock: bool = False, period: str = "today", - ) -> tuple[timedelta, bool]: + ) -> tuple[timedelta, Optional[bool]]: """Return the total amount of worked hours and current sign status.""" # Retrieve info for the period flags, e.g., --week, --month, --year if period != "today": From a9c8ab70dac7ec2a216cd27963d88d55d6a873ca Mon Sep 17 00:00:00 2001 From: Petar Andric Date: Thu, 16 Oct 2025 20:52:18 +0200 Subject: [PATCH 7/7] rollback: remove code intended for future PR --- src/woffu_client/woffu_api_client.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/woffu_client/woffu_api_client.py b/src/woffu_client/woffu_api_client.py index d786c42..202d066 100644 --- a/src/woffu_client/woffu_api_client.py +++ b/src/woffu_client/woffu_api_client.py @@ -329,12 +329,6 @@ def _get_presence( from_date = current_date.strftime(DEFAULT_DATE_FORMAT) to_date = current_date.strftime(DEFAULT_DATE_FORMAT) - try: - datetime.strptime(from_date, DEFAULT_DATE_FORMAT) - datetime.strptime(to_date, DEFAULT_DATE_FORMAT) - except ValueError: - raise - hours_response = self.get( url=f"https://{self._domain}/api/svc/core/diariesquery/users/\ {self._user_id}/diaries/summary/presence",