From f899fe9affbbf3c7704e977873abcd6ccfee4f17 Mon Sep 17 00:00:00 2001 From: Maximiliano Greco Date: Sat, 28 Oct 2023 11:32:36 +0200 Subject: [PATCH 01/14] enh(refactor): Include all python standards --- .gitignore | 1 + README.md | 39 ++++++++++++++---- pyproject.toml | 33 +++++++++++++++ requirements.txt | 3 +- setup.cfg | 2 - setup.py | 41 ------------------- .../conftest.py => src/i3_agenda/__init__.py | 0 i3_agenda/API.py => src/i3_agenda/api.py | 4 +- {i3_agenda => src/i3_agenda}/cache_utils.py | 6 +-- {i3_agenda => src/i3_agenda}/config.py | 6 +-- src/i3_agenda/conftest.py | 0 {i3_agenda => src/i3_agenda}/const.py | 1 - {i3_agenda => src/i3_agenda}/event.py | 6 +-- {i3_agenda => src/i3_agenda}/helpers.py | 2 +- .../i3_agenda.py => src/i3_agenda/main.py | 14 +++---- .../i3_agenda}/tests/test_event.py | 0 .../i3_agenda}/tests/test_helpers.py | 2 +- 17 files changed, 84 insertions(+), 76 deletions(-) create mode 100644 pyproject.toml delete mode 100644 setup.cfg delete mode 100644 setup.py rename i3_agenda/conftest.py => src/i3_agenda/__init__.py (100%) rename i3_agenda/API.py => src/i3_agenda/api.py (97%) rename {i3_agenda => src/i3_agenda}/cache_utils.py (89%) rename {i3_agenda => src/i3_agenda}/config.py (96%) create mode 100644 src/i3_agenda/conftest.py rename {i3_agenda => src/i3_agenda}/const.py (99%) rename {i3_agenda => src/i3_agenda}/event.py (97%) rename {i3_agenda => src/i3_agenda}/helpers.py (98%) rename i3_agenda/i3_agenda.py => src/i3_agenda/main.py (88%) rename {i3_agenda => src/i3_agenda}/tests/test_event.py (100%) rename {i3_agenda => src/i3_agenda}/tests/test_helpers.py (93%) diff --git a/.gitignore b/.gitignore index c852e6d..aff0e3f 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ i3_agenda/__pycache__ .mypy_cache/ __pycache__ +.autoenv diff --git a/README.md b/README.md index 07f3d4b..8bdc806 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![AUR version](https://img.shields.io/aur/version/i3-agenda?style=flat-square&logo=arch-linux)](https://aur.archlinux.org/packages/i3-agenda/) -[![PyPI](https://img.shields.io/pypi/v/i3-agenda?style=flat-square&logo=python)](https://pypi.org/project/i3-agenda/) Version Badge +[![PyPI](https://img.shields.io/pypi/v/i3-agenda?style=flat-square&logo=python)](https://pypi.org/project/i3-agenda/) Version Badge # What is this? @@ -16,19 +16,33 @@ It will print the time and title of the closest event. # Setup ## Google API + https://developers.google.com/calendar/quickstart/python -1. You need to create a Google API project and download your OAuth 2.0 credentials json file.\ -You first need to create a project [here](https://console.developers.google.com/apis/credentials), then add Google Calendar support, then download the credentials.json file.\ -**Alternatively, you can just use [this link](https://developers.google.com/calendar/quickstart/python) and click "Enable the Google Calendar API". This will create a project, add Google Calendar support, and let you download the file in 1 click**.\ -If you're having trouble, you can use this tutorial for more information [https://developers.google.com/calendar/auth](https://developers.google.com/calendar/auth).\ -Another great guide can be found here if you're still having trouble: [https://github.com/jay0lee/GAM/wiki/CreatingClientSecretsFile](https://github.com/jay0lee/GAM/wiki/CreatingClientSecretsFile). +1. You need to create a Google API project and download your OAuth 2.0 + credentials json file.You first need to create a project [here][cred], then + add Google Calendar support, then download the credentials.json file. + **Alternatively, you can just use [this link][python] and click "Enable the + Google Calendar API". This will create a project, add Google Calendar + support, and let you download the file in 1 click**. If you're having + trouble, you can use this tutorial for more information + [https://developers.google.com/calendar/auth][auth]. Another great guide can + be found here if you're still having trouble: + [https://github.com/jay0lee/GAM/wiki/CreatingClientSecretsFile][secret]. 2. Download the credentials file to somewhere on your computer. 3. Proceed to installation phase. ## Installation After downloading the credentials file, install the package. +### Pipx + +Using [`pipx`][pipx] will save you time and it's crossplatform. + +```bash +pipx install https://github.com/rosenpin/i3-agenda +``` + ### Pip 1. `sudo pip install i3-agenda` 2. Try running `i3-agenda -c $CREDENTIALS_FILE_PATH` with "$CREDENTIALS_FILE_PATH" replaced with the path to the credentials.json file you downloaded in the previous step. @@ -90,7 +104,7 @@ Leaving the list empty will fetch all calendars (default behavior). It might not work properly if you have more than 10 all day events, this can be fixed by increasing the maxResults variable. ### RTL support -If you use RTL or some of your events contain RTL languages, you will need to pipe [pybidi](https://pypi.org/project/python-bidi/) with the script. Example: +If you use RTL or some of your events contain RTL languages, you will need to pipe [pybidi](https://pypi.org/project/python-bidi/) with the script. Example: `i3-agenda -c ~/.google_credentials.json -ttl 60 | pybidi` ### Caching @@ -120,7 +134,7 @@ interval = 60 ### Example [SwiftBar](https://github.com/swiftbar/SwiftBar) configuration ![example](https://raw.githubusercontent.com/rosenpin/i3-agenda/master/art/mac_screenshot.png) -This will show your next event as the menu bar title, when you press it you will see a dropdown with all your today events +This will show your next event as the menu bar title, when you press it you will see a dropdown with all your today events You can call the file `agenda.2m.sh` to make it refresh every 2 minutes ``` bash #!/bin/bash @@ -132,7 +146,7 @@ echo "---" href="href='https://calendar.google.com/calendar/u/0/r/'" i=1 -while :; do +while :; do event=$(i3-agenda -c ~/.google_credentials.json -ttl 60 --limchar 30 --skip $i --today) ((i++)) if [[ "$event" == "No events" ]];then @@ -205,3 +219,10 @@ if [ -n "${1}" ]; then echo $skip > $file fi ``` + + +[pipx]: https://pypa.github.io/pipx/installation/ +[cred]: https://console.developers.google.com/apis/credentials +[python]: https://developers.google.com/calendar/quickstart/python +[auth]: https://developers.google.com/calendar/auth +[secret]: https://github.com/jay0lee/GAM/wiki/CreatingClientSecretsFile diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..86e59fd --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,33 @@ +[build-system] +requires = ["setuptools", "wheel", "build"] +build-backend = "setuptools.build_meta" + +[project] +name = "i3-agenda" +version = "1.7" +description = "Show your next google calendar event in polybar or i3-bar" +readme = "README.md" +authors = [{ name = "Tomer Rosenfeld", email = "mail@tomerrosenfeld.com" }] +license = {text = "Unlicense"} +classifiers = ["Programming Language :: Python :: 3"] +requires-python = ">=3.3" +dependencies = [ + "aiohttp", + "google-api-python-client", + "google-auth-httplib2", + "google-auth-oauthlib", + "python-bidi", + "typing_extensions" +] + +[project.urls] +Download = "https://github.com/rosenpin/i3-agenda/archive/1.7.tar.gz" + +[project.scripts] +i3-agenda = "i3_agenda.main:main" + +[metadata] +url = "https://github.com/rosenpin/i3-agenda" +author = "Tomer Rosenfeld" +author_email = "mail@tomerrosenfeld.com" + diff --git a/requirements.txt b/requirements.txt index 362a55d..05eadac 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,5 +3,4 @@ google-api-python-client>=2.66 google-auth-httplib2>=0.1. google-auth-oauthlib>=0.7 aiohttp>=3.8 -typing_extensions - +typing_extensions>=4.8.0 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index b88034e..0000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[metadata] -description-file = README.md diff --git a/setup.py b/setup.py deleted file mode 100644 index 4f1f6f1..0000000 --- a/setup.py +++ /dev/null @@ -1,41 +0,0 @@ -import setuptools - -with open("README.md", "r") as fh: - long_description = fh.read() - -setuptools.setup( - name="i3-agenda", - version="1.7", - author="Tomer Rosenfeld", - author_email="mail@tomerrosenfeld.com", - description="Show your next google calendar event in polybar or i3-bar", - long_description=long_description, - long_description_content_type="text/markdown", - url="https://github.com/rosenpin/i3-agenda", - download_url="https://github.com/rosenpin/i3-agenda/archive/1.7.tar.gz", - packages=setuptools.find_packages(), - license="Unlicense", - classifiers=["Programming Language :: Python :: 3"], - install_requires=[ - "python-bidi", - "google-api-python-client", - "google-auth-httplib2", - "google-auth-oauthlib", - "aiohttp", - ], - scripts=[ - "i3_agenda/API.py", - "i3_agenda/cache_utils.py", - "i3_agenda/config.py", - "i3_agenda/const.py", - "i3_agenda/event.py", - "i3_agenda/helpers.py", - "i3_agenda/i3_agenda.py", - ], - entry_points={ - "console_scripts": [ - "i3-agenda = i3_agenda:main", - ], - }, - python_requires=">=3.3", -) diff --git a/i3_agenda/conftest.py b/src/i3_agenda/__init__.py similarity index 100% rename from i3_agenda/conftest.py rename to src/i3_agenda/__init__.py diff --git a/i3_agenda/API.py b/src/i3_agenda/api.py similarity index 97% rename from i3_agenda/API.py rename to src/i3_agenda/api.py index 88f43c3..ad82b04 100644 --- a/i3_agenda/API.py +++ b/src/i3_agenda/api.py @@ -8,8 +8,8 @@ from google_auth_oauthlib.flow import InstalledAppFlow from googleapiclient.discovery import build, Resource -from event import Event, from_json -from config import CONF_DIR +from i3_agenda.event import Event, from_json +from i3_agenda.config import CONF_DIR SCOPES = ["https://www.googleapis.com/auth/calendar.readonly"] TMP_TOKEN = f"{CONF_DIR}/i3agenda_google_token.pickle" diff --git a/i3_agenda/cache_utils.py b/src/i3_agenda/cache_utils.py similarity index 89% rename from i3_agenda/cache_utils.py rename to src/i3_agenda/cache_utils.py index 8263cb8..32fe9a7 100644 --- a/i3_agenda/cache_utils.py +++ b/src/i3_agenda/cache_utils.py @@ -1,12 +1,12 @@ -from config import CONF_DIR +from i3_agenda.config import CONF_DIR from typing import Optional, List, TextIO import os.path import time import json -from event import Event, EventEncoder -from const import SECONDS_PER_MINUTE +from i3_agenda.event import Event, EventEncoder +from i3_agenda.const import SECONDS_PER_MINUTE CACHE_PATH = f"{CONF_DIR}/i3agenda_cache.txt" diff --git a/i3_agenda/config.py b/src/i3_agenda/config.py similarity index 96% rename from i3_agenda/config.py rename to src/i3_agenda/config.py index aa7b1a9..a367c90 100644 --- a/i3_agenda/config.py +++ b/src/i3_agenda/config.py @@ -1,7 +1,7 @@ import os from os.path import expanduser import argparse -from const import * +from i3_agenda.const import * @@ -40,7 +40,7 @@ "-u", action="store_true", default=False, - help="""when using this flag it will not load previous results from cache, it will however save + help="""when using this flag it will not load previous results from cache, it will however save new results to cache. You can use this flag to refresh all the cache forcefully""", ) parser.add_argument( @@ -56,7 +56,7 @@ "-r", type=int, default=10, - help="""max number of events to query Google's API for each of your calendars. Increase this number if you + help="""max number of events to query Google's API for each of your calendars. Increase this number if you have lot of events in your google calendar""", ) parser.add_argument( diff --git a/src/i3_agenda/conftest.py b/src/i3_agenda/conftest.py new file mode 100644 index 0000000..e69de29 diff --git a/i3_agenda/const.py b/src/i3_agenda/const.py similarity index 99% rename from i3_agenda/const.py rename to src/i3_agenda/const.py index 6c067f5..f0b8b4f 100644 --- a/i3_agenda/const.py +++ b/src/i3_agenda/const.py @@ -1,4 +1,3 @@ - from typing_extensions import Final DAYS_PER_WEEK : Final = 7 diff --git a/i3_agenda/event.py b/src/i3_agenda/event.py similarity index 97% rename from i3_agenda/event.py rename to src/i3_agenda/event.py index d4d5f7f..7f8c269 100644 --- a/i3_agenda/event.py +++ b/src/i3_agenda/event.py @@ -7,9 +7,9 @@ from bidi.algorithm import get_display -from config import MIN_CHARS, MIN_DELAY, URL_REGEX -from const import * -from helpers import get_unix_time, human_delta +from i3_agenda.config import MIN_CHARS, MIN_DELAY, URL_REGEX +from i3_agenda.const import * +from i3_agenda.helpers import get_unix_time, human_delta from dataclasses import dataclass diff --git a/i3_agenda/helpers.py b/src/i3_agenda/helpers.py similarity index 98% rename from i3_agenda/helpers.py rename to src/i3_agenda/helpers.py index 07032eb..d54cfae 100644 --- a/i3_agenda/helpers.py +++ b/src/i3_agenda/helpers.py @@ -2,7 +2,7 @@ import time from typing_extensions import LiteralString -from const import * +from i3_agenda.const import * def human_delta(tdelta : dt.timedelta) -> str: diff --git a/i3_agenda/i3_agenda.py b/src/i3_agenda/main.py similarity index 88% rename from i3_agenda/i3_agenda.py rename to src/i3_agenda/main.py index 8eb080d..fbdc41b 100644 --- a/i3_agenda/i3_agenda.py +++ b/src/i3_agenda/main.py @@ -1,17 +1,15 @@ -#!/usr/bin/env python3 - from __future__ import print_function import subprocess -import config +from i3_agenda import config from typing import List, Optional import datetime -from event import Event, get_closest, sort_events, get_future_events +from i3_agenda.event import Event, get_closest, sort_events, get_future_events from typing import Union -from const import * +from i3_agenda.const import * DEFAULT_CAL_WEBPAGE = "https://calendar.google.com/calendar/r/day" @@ -22,7 +20,7 @@ def button_action(button_code: str, closest: Event): print("Opening calendar page...") subprocess.Popen(["xdg-open", DEFAULT_CAL_WEBPAGE]) elif button_code == RIGHT_MOUSE_BUTTON: - if closest.location: + if closest.location: print("Opening location link...") subprocess.Popen(["xdg-open", closest.location]) @@ -40,8 +38,8 @@ def filter_only_todays_events(events: List[Event]) -> Optional[List[Event]]: def load_events(args) -> List[Event]: - from API import get_events - from cache_utils import load_cache, save_cache + from i3_agenda.api import get_events + from i3_agenda.cache_utils import load_cache, save_cache events : Union[None,list[Event]] = None diff --git a/i3_agenda/tests/test_event.py b/src/i3_agenda/tests/test_event.py similarity index 100% rename from i3_agenda/tests/test_event.py rename to src/i3_agenda/tests/test_event.py diff --git a/i3_agenda/tests/test_helpers.py b/src/i3_agenda/tests/test_helpers.py similarity index 93% rename from i3_agenda/tests/test_helpers.py rename to src/i3_agenda/tests/test_helpers.py index 21882d0..c021d2a 100644 --- a/i3_agenda/tests/test_helpers.py +++ b/src/i3_agenda/tests/test_helpers.py @@ -5,7 +5,7 @@ from typing import Dict -from helpers import * +from i3_agenda.helpers import * @pytest.mark.parametrize("test_input,expected", From c7d7ede51ee403e408e5c9813b5c657c63626447 Mon Sep 17 00:00:00 2001 From: Maximiliano Greco Date: Sat, 28 Oct 2023 11:58:10 +0200 Subject: [PATCH 02/14] cln(lint): fix all lint complains --- src/i3_agenda/api.py | 16 +- src/i3_agenda/cache_utils.py | 5 +- src/i3_agenda/config.py | 3 +- src/i3_agenda/const.py | 21 +- src/i3_agenda/event.py | 68 ++++-- src/i3_agenda/helpers.py | 25 +- src/i3_agenda/main.py | 17 +- src/i3_agenda/tests/test_event.py | 356 ---------------------------- src/i3_agenda/tests/test_helpers.py | 23 -- 9 files changed, 100 insertions(+), 434 deletions(-) delete mode 100644 src/i3_agenda/tests/test_event.py delete mode 100644 src/i3_agenda/tests/test_helpers.py diff --git a/src/i3_agenda/api.py b/src/i3_agenda/api.py index ad82b04..227d61e 100644 --- a/src/i3_agenda/api.py +++ b/src/i3_agenda/api.py @@ -8,6 +8,7 @@ from google_auth_oauthlib.flow import InstalledAppFlow from googleapiclient.discovery import build, Resource +from textwrap import dedent from i3_agenda.event import Event, from_json from i3_agenda.config import CONF_DIR @@ -36,7 +37,12 @@ def get_credentials(credspath): if not creds or not creds.valid: if not Path(credspath).is_file(): print( - """You need to download your credentials json file from the Google API Console and pass its path to this script""" + dedent( + """ + You need to download your credentials json file from the Google + API Console and pass its path to this script + """ + ).replace("\n", " ") ) exit(1) if creds and creds.expired and creds.refresh_token: @@ -50,7 +56,9 @@ def get_credentials(credspath): return creds -def get_callendar_ids(allowed_calendars_ids: List[str], service: Resource) -> List: +def get_callendar_ids( + allowed_calendars_ids: List[str], service: Resource +) -> List: calendar_ids = [] while True: calendar_list = service.calendarList().list().execute() @@ -85,7 +93,9 @@ def get_result(service, calendar_id, max_results, time_max_rfc3339=None): def get_today_events(service, calendar_id, max_results): now = datetime.datetime.utcnow() - midnight_rfc3339 = now.replace(hour=23, minute=59, second=59).isoformat() + "Z" + midnight_rfc3339 = ( + now.replace(hour=23, minute=59, second=59).isoformat() + "Z" + ) return get_result(service, calendar_id, max_results, midnight_rfc3339).get( "items", [] ) diff --git a/src/i3_agenda/cache_utils.py b/src/i3_agenda/cache_utils.py index 32fe9a7..c611708 100644 --- a/src/i3_agenda/cache_utils.py +++ b/src/i3_agenda/cache_utils.py @@ -15,7 +15,10 @@ def load_cache(cachettl: int) -> Optional[List[Event]]: if not os.path.exists(CACHE_PATH): return None - if time.time() - os.path.getmtime(CACHE_PATH) > cachettl * SECONDS_PER_MINUTE: + if ( + time.time() - os.path.getmtime(CACHE_PATH) + > cachettl * SECONDS_PER_MINUTE + ): return None try: diff --git a/src/i3_agenda/config.py b/src/i3_agenda/config.py index a367c90..0aec251 100644 --- a/src/i3_agenda/config.py +++ b/src/i3_agenda/config.py @@ -1,8 +1,7 @@ import os from os.path import expanduser import argparse -from i3_agenda.const import * - +from i3_agenda.const import MIN_DELAY, MIN_CHARS CONF_DIR = expanduser("~") + os.path.sep + ".i3agenda" diff --git a/src/i3_agenda/const.py b/src/i3_agenda/const.py index f0b8b4f..fb53d74 100644 --- a/src/i3_agenda/const.py +++ b/src/i3_agenda/const.py @@ -1,16 +1,15 @@ from typing_extensions import Final -DAYS_PER_WEEK : Final = 7 -HOURS_PER_DAY : Final = 24 -SECONDS_PER_DAY : Final = 86400 -SECONDS_PER_HOUR : Final = 3600 -SECONDS_PER_MINUTE : Final = 60 +DAYS_PER_WEEK: Final = 7 +HOURS_PER_DAY: Final = 24 +SECONDS_PER_DAY: Final = 86400 +SECONDS_PER_HOUR: Final = 3600 +SECONDS_PER_MINUTE: Final = 60 -URGENT_DELAY_MN : Final = 5 +URGENT_DELAY_MN: Final = 5 -MIN_CHARS : Final = -1 -MIN_DELAY : Final = -1 - -LEFT_MOUSE_BUTTON : Final = "1" -RIGHT_MOUSE_BUTTON : Final = "3" +MIN_CHARS: Final = -1 +MIN_DELAY: Final = -1 +LEFT_MOUSE_BUTTON: Final = "1" +RIGHT_MOUSE_BUTTON: Final = "3" diff --git a/src/i3_agenda/event.py b/src/i3_agenda/event.py index 7f8c269..b512826 100644 --- a/src/i3_agenda/event.py +++ b/src/i3_agenda/event.py @@ -7,13 +7,22 @@ from bidi.algorithm import get_display -from i3_agenda.config import MIN_CHARS, MIN_DELAY, URL_REGEX -from i3_agenda.const import * +from i3_agenda.config import ( + MIN_CHARS, + MIN_DELAY, + URL_REGEX, +) + +from i3_agenda.const import ( + SECONDS_PER_MINUTE, + DAYS_PER_WEEK, + URGENT_DELAY_MN, + SECONDS_PER_DAY, +) from i3_agenda.helpers import get_unix_time, human_delta from dataclasses import dataclass - @dataclass class Event: summary: str @@ -21,7 +30,6 @@ class Event: end_time: int location: Union[str, None] - def get_datetime(self) -> dt.datetime: return dt.datetime.fromtimestamp(self.start_time) @@ -68,7 +76,9 @@ def get_string( def is_ongoing(self) -> bool: now = dt.datetime.now() - ongoing = now > self.get_datetime() and not now > self.get_end_datetime() + ongoing = ( + now > self.get_datetime() and not now > self.get_end_datetime() + ) return ongoing def is_today(self) -> bool: @@ -84,11 +94,12 @@ def is_this_week(self) -> bool: next_week = today + dt.timedelta(days=DAYS_PER_WEEK) return today.date() <= self.get_datetime().date() < next_week.date() - def is_urgent(self) -> bool: now = dt.datetime.now() urgent = now + dt.timedelta(minutes=URGENT_DELAY_MN) - five_minutes_started = self.get_datetime() + dt.timedelta(minutes=URGENT_DELAY_MN) + five_minutes_started = self.get_datetime() + dt.timedelta( + minutes=URGENT_DELAY_MN + ) # is urgent if it begins in URGENT_DELAY_MN minutes and if it hasn't # passed URGENT_DELAY_MN minutes it started return self.get_datetime() < urgent and not now > five_minutes_started @@ -97,10 +108,11 @@ def is_allday(self) -> bool: time_delta = self.end_time - self.start_time # event is considered all day if its start time and end time are both 00:00:00 # and the time difference between start and finish is divisible by 24 - return self.get_datetime().time() == dt.time(0) \ - and self.get_end_datetime().time() == dt.time(0) \ - and time_delta % SECONDS_PER_DAY == 0 - + return ( + self.get_datetime().time() == dt.time(0) + and self.get_end_datetime().time() == dt.time(0) + and time_delta % SECONDS_PER_DAY == 0 + ) class EventEncoder(json.JSONEncoder): @@ -111,13 +123,13 @@ def default(self, o): # pylint: disable=E0202 return json.JSONEncoder.default(self, o) - - def sort_events(events: List[Event]) -> List[Event]: return sorted(events, key=lambda e: e.start_time, reverse=False) -def get_future_events(events: List[Event], hide_event_after: int, show_event_before: int) -> List[Event]: +def get_future_events( + events: List[Event], hide_event_after: int, show_event_before: int +) -> List[Event]: future_events = [] now = time.time() @@ -132,11 +144,17 @@ def get_future_events(events: List[Event], hide_event_after: int, show_event_bef continue # Event won't start for more than show_event_before - if show_event_before > MIN_DELAY and now + SECONDS_PER_MINUTE * show_event_before < event.start_time: + if ( + show_event_before > MIN_DELAY + and now + SECONDS_PER_MINUTE * show_event_before < event.start_time + ): continue # If the event started more than hide_event_after ago - if hide_event_after > MIN_DELAY and event.start_time + SECONDS_PER_MINUTE * hide_event_after < now: + if ( + hide_event_after > MIN_DELAY + and event.start_time + SECONDS_PER_MINUTE * hide_event_after < now + ): continue future_events.append(event) @@ -153,13 +171,15 @@ def get_closest(events: List[Event]) -> Optional[Event]: return closest - - -def from_json(event_json : Dict[str,Any]) -> Event: - end_time = int(get_unix_time( - event_json["end"].get("dateTime", event_json["end"].get("date"))) +def from_json(event_json: Dict[str, Any]) -> Event: + end_time = int( + get_unix_time( + event_json["end"].get("dateTime", event_json["end"].get("date")) + ) + ) + start_time = event_json["start"].get( + "dateTime", event_json["start"].get("date") ) - start_time = event_json["start"].get("dateTime", event_json["start"].get("date")) start_time = int(get_unix_time(start_time)) location = None @@ -169,4 +189,6 @@ def from_json(event_json : Dict[str,Any]) -> Event: elif "description" in event_json: matches = re.findall(URL_REGEX, event_json["description"]) location = matches[0][0] if matches else None - return Event(event_json.get("summary", "(No title)"), start_time, end_time, location) + return Event( + event_json.get("summary", "(No title)"), start_time, end_time, location + ) diff --git a/src/i3_agenda/helpers.py b/src/i3_agenda/helpers.py index d54cfae..d3d4f8a 100644 --- a/src/i3_agenda/helpers.py +++ b/src/i3_agenda/helpers.py @@ -1,13 +1,16 @@ import datetime as dt import time -from typing_extensions import LiteralString -from i3_agenda.const import * +from i3_agenda.const import ( + SECONDS_PER_DAY, + SECONDS_PER_HOUR, + SECONDS_PER_MINUTE, +) -def human_delta(tdelta : dt.timedelta) -> str: - duration = [ 0 ] * 4 # will hold decomposition of tdelta in d, h, m, s - fmts = ['{d[0]} day(s)', '{d[1]}h', '{d[2]}m', '{d[3]}s'] +def human_delta(tdelta: dt.timedelta) -> str: + duration = [0] * 4 # will hold decomposition of tdelta in d, h, m, s + fmts = ["{d[0]} day(s)", "{d[1]}h", "{d[2]}m", "{d[3]}s"] total_seconds = int(tdelta.total_seconds()) @@ -17,15 +20,14 @@ def human_delta(tdelta : dt.timedelta) -> str: duration[2], duration[3] = divmod(rem, SECONDS_PER_MINUTE) # Keep only format for non null value - fmt = ' '.join([ fmts[i] for i in range(len(duration)) if duration[i] > 0]) + fmt = " ".join([fmts[i] for i in range(len(duration)) if duration[i] > 0]) if not fmt: return "0m" - return fmt.format(d = duration) + return fmt.format(d=duration) - -def make_tz_backward_compatible(full_time : str) -> str: +def make_tz_backward_compatible(full_time: str) -> str: # Python introduced the ability to parse ":" in the timezone format (in strptime()) only from version 3.7 and up. # We need to remove the : before the timezone to support older versions # See https://stackoverflow.com/questions/30999230/how-to-parse-timezone-with-colon for more information @@ -33,6 +35,7 @@ def make_tz_backward_compatible(full_time : str) -> str: full_time = full_time[:-3] + full_time[-2:] return full_time + def get_unix_time(full_time: str) -> float: if "T" in full_time: event_time_format = "%Y-%m-%dT%H:%M:%S%z" @@ -42,5 +45,7 @@ def get_unix_time(full_time: str) -> float: full_time = make_tz_backward_compatible(full_time) return time.mktime( - dt.datetime.strptime(full_time, event_time_format).astimezone().timetuple() + dt.datetime.strptime(full_time, event_time_format) + .astimezone() + .timetuple() ) diff --git a/src/i3_agenda/main.py b/src/i3_agenda/main.py index fbdc41b..f8a6344 100644 --- a/src/i3_agenda/main.py +++ b/src/i3_agenda/main.py @@ -9,7 +9,10 @@ from i3_agenda.event import Event, get_closest, sort_events, get_future_events from typing import Union -from i3_agenda.const import * +from i3_agenda.const import ( + LEFT_MOUSE_BUTTON, + RIGHT_MOUSE_BUTTON, +) DEFAULT_CAL_WEBPAGE = "https://calendar.google.com/calendar/r/day" @@ -41,7 +44,7 @@ def load_events(args) -> List[Event]: from i3_agenda.api import get_events from i3_agenda.cache_utils import load_cache, save_cache - events : Union[None,list[Event]] = None + events: Union[None, list[Event]] = None if not args.update: events = load_cache(args.cachettl) @@ -50,7 +53,9 @@ def load_events(args) -> List[Event]: events = filter_only_todays_events(events) if events is None or args.update: - events = get_events(args.credentials, args.ids, args.maxres, args.today) + events = get_events( + args.credentials, args.ids, args.maxres, args.today + ) save_cache(events) return events @@ -61,11 +66,13 @@ def main(): events = load_events(args) - events = get_future_events(events, args.hide_event_after, args.show_event_before) + events = get_future_events( + events, args.hide_event_after, args.show_event_before + ) if args.skip > 0: events = sort_events(events) - events = events[args.skip :] + events = events[args.skip:] closest = get_closest(events) if closest is None: diff --git a/src/i3_agenda/tests/test_event.py b/src/i3_agenda/tests/test_event.py deleted file mode 100644 index 006f0c2..0000000 --- a/src/i3_agenda/tests/test_event.py +++ /dev/null @@ -1,356 +0,0 @@ -import datetime as dt -import os -import time - -from freezegun import freeze_time -import pytest - -from event import * - - -os.environ['TZ'] = 'UTC' -time.tzset() - -def new_event(start_time: str, end_time: str, summary="summary", location=None): - start = dt.datetime.strptime(start_time, "%Y-%m-%d %H:%M:%S") - end = dt.datetime.strptime(end_time, "%Y-%m-%d %H:%M:%S") - return Event(summary, int(start.timestamp()), int(end.timestamp()), location) - - -@pytest.mark.parametrize( - "start,end,expected", - [ - ("2022-12-14 12:00:07", "2022-12-14 12:05:07", True), - ("2022-12-13 12:00:07", "2022-12-13 12:05:07", False), # Before - ("2022-12-15 12:00:07", "2022-12-15 12:05:07", False), # After - ], -) -@freeze_time("2022-12-14 12:03:07") -def test_is_ongoing(start, end, expected): - assert new_event(start, end).is_ongoing() == expected - - -@pytest.mark.parametrize( - "start,end,expected", - [ - ("2022-12-14 12:00:07", "2022-12-14 12:05:07", True), - ("2022-12-14 12:00:07", "2022-12-15 12:05:07", True), - ("2022-12-13 12:00:07", "2022-12-14 12:05:07", False), - ("2022-12-13 12:00:07", "2022-12-13 12:05:07", False), - ], -) -@freeze_time("2022-12-14 12:03:07") -def test_is_today(start, end, expected): - assert new_event(start, end).is_today() == expected - - -@pytest.mark.parametrize( - "start,end,expected", - [ - ("2022-12-15 12:00:07", "2022-12-15 12:05:07", True), - ("2022-12-15 12:00:07", "2022-12-17 12:05:07", True), - ("2022-12-14 12:00:07", "2022-12-14 12:05:07", False), - ("2022-12-14 12:00:07", "2022-12-15 12:05:07", False), - ("2022-12-13 12:00:07", "2022-12-14 12:05:07", False), - ("2022-12-13 12:00:07", "2022-12-13 12:05:07", False), - ], -) -@freeze_time("2022-12-14 12:03:07") -def test_is_tomorrow(start, end, expected): - assert new_event(start, end).is_tomorrow() == expected - - -@pytest.mark.parametrize( - "start,end,expected", - [ - ("2022-12-14 12:00:07", "2022-12-15 12:05:07", True), - ("2022-12-14 12:00:07", "2022-12-26 12:05:07", True), - ("2022-12-20 12:00:07", "2022-12-26 12:05:07", True), - ("2022-12-22 12:00:07", "2022-12-26 12:05:07", False), - ("2022-12-14 11:00:07", "2022-12-15 12:05:07", True), - ("2022-12-13 11:00:07", "2022-12-15 12:05:07", False), - ], -) -@freeze_time("2022-12-14 12:03:07") -def test_is_this_week(start, end, expected): - assert new_event(start, end).is_this_week() == expected - - -@pytest.mark.parametrize( - "start,end,expected", - [ - ("2022-12-14 12:09:07", "2022-12-15 12:35:07", True), - ("2022-12-14 12:05:06", "2022-12-15 12:35:07", False), # Just before now - 5m - ("2022-12-14 12:05:08", "2022-12-15 12:35:07", True), # Just after now - 5m - ("2022-12-14 12:15:06", "2022-12-15 12:35:07", True), # Just before now + 5m - ("2022-12-14 12:15:08", "2022-12-15 12:35:07", False), # After now + 5m - ], -) -@freeze_time("2022-12-14 12:10:07") -def test_is_urgent(start, end, expected): - assert new_event(start, end).is_urgent() == expected - - -@pytest.mark.parametrize( - "start,end,expected", - [ - ("2022-12-14 00:00:00", "2022-12-15 00:00:00", True), - ("2022-12-14 12:05:06", "2022-12-15 12:35:07", False), - ("2022-12-14 00:00:00", "2022-12-18 00:00:00", True), - ("2022-12-14 00:00:00", "2023-12-18 00:00:00", True), - ("2022-12-14 00:00:00", "2023-12-14 00:00:24", False), - ("2022-12-14 00:00:00", "2022-12-18 00:00:24", False), - ], -) -@freeze_time("2022-12-14 12:10:07") -def test_is_allday(start, end, expected): - assert new_event(start, end).is_allday() == expected - - -@freeze_time("2022-12-14 12:10:07") -def test_get_future_events_without_after_before(): - events = [ - new_event("2022-12-15 00:00:00", "2022-12-18 00:00:00"), # all day - new_event("2022-12-13 00:00:00", "2022-12-13 03:00:00"), # finished - new_event("2022-12-14 12:00:00", "2022-12-14 15:00:00"), # in middle - new_event("2022-12-14 15:00:00", "2022-12-14 17:00:00"), # future - ] - # It should only keep the two last - expected = events[-2:] - assert get_future_events(events, MIN_DELAY, MIN_DELAY) == expected - - -@freeze_time("2022-12-14 12:10:07") -def test_get_future_events_with_after(): - events = [ - new_event("2022-12-15 00:00:00", "2022-12-18 00:00:00"), # all day - new_event("2022-12-13 00:00:00", "2022-12-13 03:00:00"), # finished - new_event( - "2022-12-14 12:00:00", "2022-12-14 15:00:00" - ), # in middle more than 5m - new_event( - "2022-12-14 12:06:00", "2022-12-14 15:00:00" - ), # in middle less than 5m - new_event("2022-12-14 15:00:00", "2022-12-14 17:00:00"), # future - ] - # It should only keep the two last - expected = events[-2:] - assert get_future_events(events, 5, MIN_DELAY) == expected - - -@freeze_time("2022-12-14 12:10:07") -def test_get_future_events_with_before(): - events = [ - new_event("2022-12-15 00:00:00", "2022-12-18 00:00:00"), # all day - new_event("2022-12-13 00:00:00", "2022-12-13 03:00:00"), # finished - new_event("2022-12-15 15:00:00", "2022-12-15 17:00:00"), # distant future - new_event("2022-12-14 12:15:08", "2022-12-14 17:00:00"), # future after 5m - new_event("2022-12-14 12:00:00", "2022-12-14 15:00:00"), # in middle - new_event("2022-12-14 12:15:06", "2022-12-14 17:00:00"), # future before 5m - ] - # It should only keep the two last - expected = events[-2:] - assert get_future_events(events, MIN_DELAY, 5) == expected - - -def test_get_closest(): - events = [ - new_event("2022-12-15 00:00:00", "2022-12-18 00:00:00"), - new_event("2022-12-13 00:00:00", "2022-12-13 03:00:00"), - new_event("2022-12-15 15:00:00", "2022-12-15 17:00:00"), - new_event("2022-12-14 12:15:08", "2022-12-14 17:00:00"), - new_event("2022-12-14 12:00:00", "2022-12-14 15:00:00"), - new_event("2022-11-14 12:15:06", "2022-12-14 17:00:00"), # closest - ] - expected = events[-1] - assert get_closest(events) == expected - - -@pytest.mark.parametrize( - "json,expected", - [ - # Test with a basic input - ( - { - "summary": "Test event", - "start": {"dateTime": "2022-12-05T15:00:00+0000"}, - "end": {"dateTime": "2022-12-05T17:00:00+0000"}, - "location": "Test location", - }, - Event("Test event", 1670252400, 1670259600, "Test location"), - ), - # Test with a different time format - ( - { - "summary": "Test event", - "start": {"date": "2022-12-05"}, - "end": {"date": "2022-12-06"}, - "location": "Test location", - }, - Event("Test event", 1670198400, 1670284800, "Test location"), - ), - # Test with no title - ( - { - "start": {"dateTime": "2022-12-05T15:00:00+0100"}, - "end": {"dateTime": "2022-12-05T17:00:00+0100"}, - "location": "Test location", - }, - Event("(No title)", 1670248800, 1670256000, "Test location"), - ), - # Test with a description but no location - ( - { - "summary": "Test event", - "start": {"dateTime": "2022-12-05T15:00:00+0000"}, - "end": {"dateTime": "2022-12-05T17:00:00+0000"}, - "description": "This is a test description with no location.", - }, - Event("Test event", 1670252400, 1670259600, None), - ), - # Test with a location within description - ( - { - "summary": "Test event", - "start": {"dateTime": "2022-12-05T15:00:00+0000"}, - "end": {"dateTime": "2022-12-05T17:00:00+0000"}, - "description": "This is a test description with url : https://truc.com/calendar/ezfzerazdfz.", - }, - Event( - "Test event", - 1670252400, - 1670259600, - "https://truc.com/calendar/ezfzerazdfz.", - ), - ), - ], -) -def test_from_json(json, expected): - assert from_json(json) == expected - - -now = dt.datetime.strptime("2022-12-14 12:10:06+0000", "%Y-%m-%d %H:%M:%S%z") - - -@pytest.mark.parametrize( - "event_params,otl,netl,expected", - [ - # Test event that is ongoing as 1 hour left and ongoing_time_left is True - ( - dict( - summary="Event 1", - start_time=now.timestamp(), - end_time=(now + dt.timedelta(hours=1, seconds=1)).timestamp(), - location=None, - ), - True, - False, - "Event 1 (1h left)", - ), - # Test event that is not ongoing and is today and next_event_time_left is True - ( - dict( - summary="Event 1", - start_time=(now + dt.timedelta(minutes=30, seconds=1)).timestamp(), - end_time=(now + dt.timedelta(hours=1, seconds=1)).timestamp(), - location=None, - ), - False, - True, - "Event 1 in 30m", - ), - # Test event that is not ongoing and is today and next_event_time_left is True - ( - dict( - summary="Event 1", - start_time=(now + dt.timedelta(minutes=30, seconds=1)).timestamp(), - end_time=(now + dt.timedelta(hours=1, seconds=1)).timestamp(), - location=None, - ), - True, - False, - "12:40 Event 1", - ), - # Test event that is not ongoing and is today and next_event_time_left is True - ( - dict( - summary="Event 1", - start_time=(now + dt.timedelta(minutes=30, seconds=1)).timestamp(), - end_time=(now + dt.timedelta(hours=1, seconds=1)).timestamp(), - location=None, - ), - False, - False, - "12:40 Event 1", - ), - # Test event that is not ongoing and is tomorrow - ( - dict( - summary="Event 1", - start_time=(now + dt.timedelta(days=1, seconds=1)).timestamp(), - end_time=(now + dt.timedelta(hours=1, seconds=1)).timestamp(), - location=None, - ), - False, - False, - "Tomorrow at 12:10 Event 1", - ), - # Test event that is not ongoing, not tommorrow and this week - ( - dict( - summary="Event 1", - start_time=(now + dt.timedelta(days=2, seconds=1)).timestamp(), - end_time=(now + dt.timedelta(hours=1, seconds=1)).timestamp(), - location=None, - ), - False, - False, - "Fri at 12:10 Event 1", - ), - # Test event that is not ongoing, not tommorrow and after this week - ( - dict( - summary="Event 1", - start_time=(now + dt.timedelta(days=8, seconds=1)).timestamp(), - end_time=(now + dt.timedelta(hours=1, seconds=1)).timestamp(), - location=None, - ), - False, - False, - "2022-12-22 at 12:10 Event 1", - ), - ], -) -@freeze_time(now + dt.timedelta(seconds=1)) -def test_get_string(event_params, otl: bool, netl: bool, expected): - - event = Event(**event_params) - result = event.get_string( - limit_char=100, - date_format="%Y-%m-%d", - ongoing_time_left=otl, - next_event_time_left=netl, - ) - assert result == expected - -@pytest.mark.parametrize( - "description, expected", [ - ("A string that has more than twenty chars", "2022-12-22 at 12:10 A string that has mo..."), - ("اجراجوییِ تازه مغامرة جديدة", "2022-12-22 at 12:10 ...رماغم هزات ِییوجارجا"), # An RTL string - ("הרפתקה חדשה הרפתקה חדשה", "2022-12-22 at 12:10 ...ח הקתפרה השדח הקתפרה") # An RTL string - ]) -@freeze_time(now + dt.timedelta(seconds=1)) -def test_get_string_description(description, expected): - event = Event( - summary=description, - start_time=int((now + dt.timedelta(days=8, seconds=1)).timestamp()), - end_time=int((now + dt.timedelta(hours=1, seconds=1)).timestamp()), - location=None, - ) - result = event.get_string( - limit_char=20, - date_format="%Y-%m-%d", - ongoing_time_left=False, - next_event_time_left=False, - ) - assert result == expected diff --git a/src/i3_agenda/tests/test_helpers.py b/src/i3_agenda/tests/test_helpers.py deleted file mode 100644 index c021d2a..0000000 --- a/src/i3_agenda/tests/test_helpers.py +++ /dev/null @@ -1,23 +0,0 @@ - -import datetime as dt - -import pytest - -from typing import Dict - -from i3_agenda.helpers import * - - -@pytest.mark.parametrize("test_input,expected", -[ - ({"minutes":0}, "0m"), - ({"minutes":1}, "1m"), - ({"hours":1}, "1h"), - ({"days":1}, "1 day(s)"), - ({"days":1, "hours":1, "minutes":1}, "1 day(s) 1h 1m"), - ({"hours":23, "minutes":59}, "23h 59m"), -] -) -def test_human_delta(test_input:Dict[str,int],expected:str): - assert human_delta(dt.timedelta(**test_input)) == expected - From 5e7616bd952b6a6d5178434f41ec1b892c51cb32 Mon Sep 17 00:00:00 2001 From: Maximiliano Greco Date: Sat, 28 Oct 2023 11:58:42 +0200 Subject: [PATCH 03/14] enh(tests): move tests to root folder --- tests/test_event.py | 361 ++++++++++++++++++++++++++++++++++++++++++ tests/test_helpers.py | 23 +++ 2 files changed, 384 insertions(+) create mode 100644 tests/test_event.py create mode 100644 tests/test_helpers.py diff --git a/tests/test_event.py b/tests/test_event.py new file mode 100644 index 0000000..1ad10ce --- /dev/null +++ b/tests/test_event.py @@ -0,0 +1,361 @@ +import datetime as dt +import os +import time + +import pytest +from freezegun import freeze_time + +from i3_agenda.event import ( + MIN_DELAY, + Event, + from_json, + get_closest, + get_future_events, +) + +os.environ['TZ'] = 'UTC' +time.tzset() + +def new_event(start_time: str, end_time: str, summary="summary", location=None): + start = dt.datetime.strptime(start_time, "%Y-%m-%d %H:%M:%S") + end = dt.datetime.strptime(end_time, "%Y-%m-%d %H:%M:%S") + return Event(summary, int(start.timestamp()), int(end.timestamp()), location) + + +@pytest.mark.parametrize( + "start,end,expected", + [ + ("2022-12-14 12:00:07", "2022-12-14 12:05:07", True), + ("2022-12-13 12:00:07", "2022-12-13 12:05:07", False), # Before + ("2022-12-15 12:00:07", "2022-12-15 12:05:07", False), # After + ], +) +@freeze_time("2022-12-14 12:03:07") +def test_is_ongoing(start, end, expected): + assert new_event(start, end).is_ongoing() == expected + + +@pytest.mark.parametrize( + "start,end,expected", + [ + ("2022-12-14 12:00:07", "2022-12-14 12:05:07", True), + ("2022-12-14 12:00:07", "2022-12-15 12:05:07", True), + ("2022-12-13 12:00:07", "2022-12-14 12:05:07", False), + ("2022-12-13 12:00:07", "2022-12-13 12:05:07", False), + ], +) +@freeze_time("2022-12-14 12:03:07") +def test_is_today(start, end, expected): + assert new_event(start, end).is_today() == expected + + +@pytest.mark.parametrize( + "start,end,expected", + [ + ("2022-12-15 12:00:07", "2022-12-15 12:05:07", True), + ("2022-12-15 12:00:07", "2022-12-17 12:05:07", True), + ("2022-12-14 12:00:07", "2022-12-14 12:05:07", False), + ("2022-12-14 12:00:07", "2022-12-15 12:05:07", False), + ("2022-12-13 12:00:07", "2022-12-14 12:05:07", False), + ("2022-12-13 12:00:07", "2022-12-13 12:05:07", False), + ], +) +@freeze_time("2022-12-14 12:03:07") +def test_is_tomorrow(start, end, expected): + assert new_event(start, end).is_tomorrow() == expected + + +@pytest.mark.parametrize( + "start,end,expected", + [ + ("2022-12-14 12:00:07", "2022-12-15 12:05:07", True), + ("2022-12-14 12:00:07", "2022-12-26 12:05:07", True), + ("2022-12-20 12:00:07", "2022-12-26 12:05:07", True), + ("2022-12-22 12:00:07", "2022-12-26 12:05:07", False), + ("2022-12-14 11:00:07", "2022-12-15 12:05:07", True), + ("2022-12-13 11:00:07", "2022-12-15 12:05:07", False), + ], +) +@freeze_time("2022-12-14 12:03:07") +def test_is_this_week(start, end, expected): + assert new_event(start, end).is_this_week() == expected + + +@pytest.mark.parametrize( + "start,end,expected", + [ + ("2022-12-14 12:09:07", "2022-12-15 12:35:07", True), + ("2022-12-14 12:05:06", "2022-12-15 12:35:07", False), # Just before now - 5m + ("2022-12-14 12:05:08", "2022-12-15 12:35:07", True), # Just after now - 5m + ("2022-12-14 12:15:06", "2022-12-15 12:35:07", True), # Just before now + 5m + ("2022-12-14 12:15:08", "2022-12-15 12:35:07", False), # After now + 5m + ], +) +@freeze_time("2022-12-14 12:10:07") +def test_is_urgent(start, end, expected): + assert new_event(start, end).is_urgent() == expected + + +@pytest.mark.parametrize( + "start,end,expected", + [ + ("2022-12-14 00:00:00", "2022-12-15 00:00:00", True), + ("2022-12-14 12:05:06", "2022-12-15 12:35:07", False), + ("2022-12-14 00:00:00", "2022-12-18 00:00:00", True), + ("2022-12-14 00:00:00", "2023-12-18 00:00:00", True), + ("2022-12-14 00:00:00", "2023-12-14 00:00:24", False), + ("2022-12-14 00:00:00", "2022-12-18 00:00:24", False), + ], +) +@freeze_time("2022-12-14 12:10:07") +def test_is_allday(start, end, expected): + assert new_event(start, end).is_allday() == expected + + +@freeze_time("2022-12-14 12:10:07") +def test_get_future_events_without_after_before(): + events = [ + new_event("2022-12-15 00:00:00", "2022-12-18 00:00:00"), # all day + new_event("2022-12-13 00:00:00", "2022-12-13 03:00:00"), # finished + new_event("2022-12-14 12:00:00", "2022-12-14 15:00:00"), # in middle + new_event("2022-12-14 15:00:00", "2022-12-14 17:00:00"), # future + ] + # It should only keep the two last + expected = events[-2:] + assert get_future_events(events, MIN_DELAY, MIN_DELAY) == expected + + +@freeze_time("2022-12-14 12:10:07") +def test_get_future_events_with_after(): + events = [ + new_event("2022-12-15 00:00:00", "2022-12-18 00:00:00"), # all day + new_event("2022-12-13 00:00:00", "2022-12-13 03:00:00"), # finished + new_event( + "2022-12-14 12:00:00", "2022-12-14 15:00:00" + ), # in middle more than 5m + new_event( + "2022-12-14 12:06:00", "2022-12-14 15:00:00" + ), # in middle less than 5m + new_event("2022-12-14 15:00:00", "2022-12-14 17:00:00"), # future + ] + # It should only keep the two last + expected = events[-2:] + assert get_future_events(events, 5, MIN_DELAY) == expected + + +@freeze_time("2022-12-14 12:10:07") +def test_get_future_events_with_before(): + events = [ + new_event("2022-12-15 00:00:00", "2022-12-18 00:00:00"), # all day + new_event("2022-12-13 00:00:00", "2022-12-13 03:00:00"), # finished + new_event("2022-12-15 15:00:00", "2022-12-15 17:00:00"), # distant future + new_event("2022-12-14 12:15:08", "2022-12-14 17:00:00"), # future after 5m + new_event("2022-12-14 12:00:00", "2022-12-14 15:00:00"), # in middle + new_event("2022-12-14 12:15:06", "2022-12-14 17:00:00"), # future before 5m + ] + # It should only keep the two last + expected = events[-2:] + assert get_future_events(events, MIN_DELAY, 5) == expected + + +def test_get_closest(): + events = [ + new_event("2022-12-15 00:00:00", "2022-12-18 00:00:00"), + new_event("2022-12-13 00:00:00", "2022-12-13 03:00:00"), + new_event("2022-12-15 15:00:00", "2022-12-15 17:00:00"), + new_event("2022-12-14 12:15:08", "2022-12-14 17:00:00"), + new_event("2022-12-14 12:00:00", "2022-12-14 15:00:00"), + new_event("2022-11-14 12:15:06", "2022-12-14 17:00:00"), # closest + ] + expected = events[-1] + assert get_closest(events) == expected + + +@pytest.mark.parametrize( + "json,expected", + [ + # Test with a basic input + ( + { + "summary": "Test event", + "start": {"dateTime": "2022-12-05T15:00:00+0000"}, + "end": {"dateTime": "2022-12-05T17:00:00+0000"}, + "location": "Test location", + }, + Event("Test event", 1670252400, 1670259600, "Test location"), + ), + # Test with a different time format + ( + { + "summary": "Test event", + "start": {"date": "2022-12-05"}, + "end": {"date": "2022-12-06"}, + "location": "Test location", + }, + Event("Test event", 1670198400, 1670284800, "Test location"), + ), + # Test with no title + ( + { + "start": {"dateTime": "2022-12-05T15:00:00+0100"}, + "end": {"dateTime": "2022-12-05T17:00:00+0100"}, + "location": "Test location", + }, + Event("(No title)", 1670248800, 1670256000, "Test location"), + ), + # Test with a description but no location + ( + { + "summary": "Test event", + "start": {"dateTime": "2022-12-05T15:00:00+0000"}, + "end": {"dateTime": "2022-12-05T17:00:00+0000"}, + "description": "This is a test description with no location.", + }, + Event("Test event", 1670252400, 1670259600, None), + ), + # Test with a location within description + ( + { + "summary": "Test event", + "start": {"dateTime": "2022-12-05T15:00:00+0000"}, + "end": {"dateTime": "2022-12-05T17:00:00+0000"}, + "description": "This is a test description with url : https://truc.com/calendar/ezfzerazdfz.", + }, + Event( + "Test event", + 1670252400, + 1670259600, + "https://truc.com/calendar/ezfzerazdfz.", + ), + ), + ], +) +def test_from_json(json, expected): + assert from_json(json) == expected + + +now = dt.datetime.strptime("2022-12-14 12:10:06+0000", "%Y-%m-%d %H:%M:%S%z") + + +@pytest.mark.parametrize( + "event_params,otl,netl,expected", + [ + # Test event that is ongoing as 1 hour left and ongoing_time_left is True + ( + dict( + summary="Event 1", + start_time=now.timestamp(), + end_time=(now + dt.timedelta(hours=1, seconds=1)).timestamp(), + location=None, + ), + True, + False, + "Event 1 (1h left)", + ), + # Test event that is not ongoing and is today and next_event_time_left is True + ( + dict( + summary="Event 1", + start_time=(now + dt.timedelta(minutes=30, seconds=1)).timestamp(), + end_time=(now + dt.timedelta(hours=1, seconds=1)).timestamp(), + location=None, + ), + False, + True, + "Event 1 in 30m", + ), + # Test event that is not ongoing and is today and next_event_time_left is True + ( + dict( + summary="Event 1", + start_time=(now + dt.timedelta(minutes=30, seconds=1)).timestamp(), + end_time=(now + dt.timedelta(hours=1, seconds=1)).timestamp(), + location=None, + ), + True, + False, + "12:40 Event 1", + ), + # Test event that is not ongoing and is today and next_event_time_left is True + ( + dict( + summary="Event 1", + start_time=(now + dt.timedelta(minutes=30, seconds=1)).timestamp(), + end_time=(now + dt.timedelta(hours=1, seconds=1)).timestamp(), + location=None, + ), + False, + False, + "12:40 Event 1", + ), + # Test event that is not ongoing and is tomorrow + ( + dict( + summary="Event 1", + start_time=(now + dt.timedelta(days=1, seconds=1)).timestamp(), + end_time=(now + dt.timedelta(hours=1, seconds=1)).timestamp(), + location=None, + ), + False, + False, + "Tomorrow at 12:10 Event 1", + ), + # Test event that is not ongoing, not tommorrow and this week + ( + dict( + summary="Event 1", + start_time=(now + dt.timedelta(days=2, seconds=1)).timestamp(), + end_time=(now + dt.timedelta(hours=1, seconds=1)).timestamp(), + location=None, + ), + False, + False, + "Fri at 12:10 Event 1", + ), + # Test event that is not ongoing, not tommorrow and after this week + ( + dict( + summary="Event 1", + start_time=(now + dt.timedelta(days=8, seconds=1)).timestamp(), + end_time=(now + dt.timedelta(hours=1, seconds=1)).timestamp(), + location=None, + ), + False, + False, + "2022-12-22 at 12:10 Event 1", + ), + ], +) +@freeze_time(now + dt.timedelta(seconds=1)) +def test_get_string(event_params, otl: bool, netl: bool, expected): + + event = Event(**event_params) + result = event.get_string( + limit_char=100, + date_format="%Y-%m-%d", + ongoing_time_left=otl, + next_event_time_left=netl, + ) + assert result == expected + +@pytest.mark.parametrize( + "description, expected", [ + ("A string that has more than twenty chars", "2022-12-22 at 12:10 A string that has mo..."), + ("اجراجوییِ تازه مغامرة جديدة", "2022-12-22 at 12:10 ...رماغم هزات ِییوجارجا"), # An RTL string + ("הרפתקה חדשה הרפתקה חדשה", "2022-12-22 at 12:10 ...ח הקתפרה השדח הקתפרה") # An RTL string + ]) +@freeze_time(now + dt.timedelta(seconds=1)) +def test_get_string_description(description, expected): + event = Event( + summary=description, + start_time=int((now + dt.timedelta(days=8, seconds=1)).timestamp()), + end_time=int((now + dt.timedelta(hours=1, seconds=1)).timestamp()), + location=None, + ) + result = event.get_string( + limit_char=20, + date_format="%Y-%m-%d", + ongoing_time_left=False, + next_event_time_left=False, + ) + assert result == expected diff --git a/tests/test_helpers.py b/tests/test_helpers.py new file mode 100644 index 0000000..c021d2a --- /dev/null +++ b/tests/test_helpers.py @@ -0,0 +1,23 @@ + +import datetime as dt + +import pytest + +from typing import Dict + +from i3_agenda.helpers import * + + +@pytest.mark.parametrize("test_input,expected", +[ + ({"minutes":0}, "0m"), + ({"minutes":1}, "1m"), + ({"hours":1}, "1h"), + ({"days":1}, "1 day(s)"), + ({"days":1, "hours":1, "minutes":1}, "1 day(s) 1h 1m"), + ({"hours":23, "minutes":59}, "23h 59m"), +] +) +def test_human_delta(test_input:Dict[str,int],expected:str): + assert human_delta(dt.timedelta(**test_input)) == expected + From a6f4830755665262eaebf2a725a79073cb640c44 Mon Sep 17 00:00:00 2001 From: Maximiliano Greco Date: Sat, 28 Oct 2023 11:59:02 +0200 Subject: [PATCH 04/14] cln(coverage): ignore coverage file --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index aff0e3f..f7333ed 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ i3_agenda/__pycache__ __pycache__ .autoenv +.coverage From f348ff7f62d301994c34c29253ef8abbda22a5be Mon Sep 17 00:00:00 2001 From: Maximiliano Greco Date: Sat, 28 Oct 2023 11:59:20 +0200 Subject: [PATCH 05/14] enh(makefile): add makefile for common tasks --- Makefile | 17 +++++++++++++++++ requirements-dev.txt | 6 ++++++ 2 files changed, 23 insertions(+) create mode 100644 Makefile create mode 100644 requirements-dev.txt diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..528fa6e --- /dev/null +++ b/Makefile @@ -0,0 +1,17 @@ +install: + pip install . -r requirements.txt + +dev: + pip install -e . + pip install -r requirements-dev.txt + +test: + pytest tests --junitxml=../junit/test-results.xml + pytest tests --doctest-modules --cov=. --cov-report=xml:../coverage/cov.xml --cov-report=html:../coverage/ + +lint: + flake8 src --count --select=E9,F63,F7,F82 --show-source --statistics + flake8 src --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + +fix: + black -l79 src diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..d7d3694 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,6 @@ +flake8 +pytest +freezegun +pytest-cov +black +isort From 8a04c4d1d903dcad53fb3782960968dd30bde238 Mon Sep 17 00:00:00 2001 From: Maximiliano Greco Date: Sat, 28 Oct 2023 12:11:54 +0200 Subject: [PATCH 06/14] enh(src): add __version__ --- src/i3_agenda/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/i3_agenda/__init__.py b/src/i3_agenda/__init__.py index e69de29..cf376a4 100644 --- a/src/i3_agenda/__init__.py +++ b/src/i3_agenda/__init__.py @@ -0,0 +1,4 @@ +__version__ = "1.7" +__all__ = [ + "__version__", +] From 678b6279376ba688b19d1f282ca1b67ea5740bea Mon Sep 17 00:00:00 2001 From: Maximiliano Greco Date: Sat, 28 Oct 2023 12:21:42 +0200 Subject: [PATCH 07/14] enh(root): update makefil and version --- Makefile | 25 ++++++++++++++++++++----- pyproject.toml | 2 +- src/i3_agenda/__init__.py | 2 +- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 528fa6e..e43d106 100644 --- a/Makefile +++ b/Makefile @@ -1,17 +1,32 @@ -install: +help: + @cat ./Makefile | grep -E '^[a-zA-Z_-]+:.*?## .*$$' | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}' + +install: ## Install dependencies pip install . -r requirements.txt -dev: +dev: ## Install dependencies for development pip install -e . pip install -r requirements-dev.txt -test: +test: ## Run tests pytest tests --junitxml=../junit/test-results.xml pytest tests --doctest-modules --cov=. --cov-report=xml:../coverage/cov.xml --cov-report=html:../coverage/ -lint: +lint: ## Check code style flake8 src --count --select=E9,F63,F7,F82 --show-source --statistics flake8 src --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics -fix: +fix: ## Format code black -l79 src + +release: ## Create a release: make release v=0.1.0 + @if [ -z "$(v)" ]; then echo "Missing version number:\nUse: make release v=0.1"; exit 1; fi + @sed -i -e "s/version = \".*\"/version = \"$(v)\"/" pyproject.toml + @sed -i -e "s/__version__ = \".*\"/__version__ = \"$(v)\"/" src/i3_agenda/__init__.py + @git diff + @# ideally we would use the following + @# git add pyproject.toml src/i3_agenda/__init__.py + @# git commit -m "Release v$(v)" + @# git tag -a v$(v) -m "Release v$(v)" + @# git push + @# git push --tags diff --git a/pyproject.toml b/pyproject.toml index 86e59fd..db9e500 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "i3-agenda" -version = "1.7" +version = "1.8" description = "Show your next google calendar event in polybar or i3-bar" readme = "README.md" authors = [{ name = "Tomer Rosenfeld", email = "mail@tomerrosenfeld.com" }] diff --git a/src/i3_agenda/__init__.py b/src/i3_agenda/__init__.py index cf376a4..a6462e6 100644 --- a/src/i3_agenda/__init__.py +++ b/src/i3_agenda/__init__.py @@ -1,4 +1,4 @@ -__version__ = "1.7" +__version__ = "1.8" __all__ = [ "__version__", ] From 8e556a030fa07f89f2742437455702623bbd5c5a Mon Sep 17 00:00:00 2001 From: Maximiliano Greco Date: Sat, 28 Oct 2023 12:29:19 +0200 Subject: [PATCH 08/14] cln: cleanup --- .github/workflows/github-action.yml | 10 +++++----- Makefile | 3 +-- README.md | 7 +++++-- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/.github/workflows/github-action.yml b/.github/workflows/github-action.yml index e56332d..8467d39 100644 --- a/.github/workflows/github-action.yml +++ b/.github/workflows/github-action.yml @@ -22,19 +22,19 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install flake8 pytest freezegun pytest-cov + pip install -r requirements-dev.txt if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + flake8 src --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + flake8 src --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with pytest working-directory: ./i3_agenda run: | - pytest --junitxml=../junit/test-results.xml - pytest --doctest-modules --cov=. --cov-report=xml:../coverage/cov.xml --cov-report=html:../coverage/ + pytest tests --junitxml=../junit/test-results.xml + pytest tests --doctest-modules --cov=. --cov-report=xml:../coverage/cov.xml --cov-report=html:../coverage/ - name: Code Coverage Report uses: irongut/CodeCoverageSummary@51cc3a756ddcd398d447c044c02cb6aa83fdae95 diff --git a/Makefile b/Makefile index e43d106..3f1d332 100644 --- a/Makefile +++ b/Makefile @@ -9,8 +9,7 @@ dev: ## Install dependencies for development pip install -r requirements-dev.txt test: ## Run tests - pytest tests --junitxml=../junit/test-results.xml - pytest tests --doctest-modules --cov=. --cov-report=xml:../coverage/cov.xml --cov-report=html:../coverage/ + pytest tests --doctest-modules --cov=src lint: ## Check code style flake8 src --count --select=E9,F63,F7,F82 --show-source --statistics diff --git a/README.md b/README.md index 8bdc806..44033cc 100644 --- a/README.md +++ b/README.md @@ -37,14 +37,17 @@ After downloading the credentials file, install the package. ### Pipx -Using [`pipx`][pipx] will save you time and it's crossplatform. +Using [`pipx`][pipx] will save you time and it's cross-platform. It is a +package manager for Python that allows you to easily install and run Python +packages in isolated environments. ```bash pipx install https://github.com/rosenpin/i3-agenda ``` ### Pip -1. `sudo pip install i3-agenda` + +1. `sudo pip install .` 2. Try running `i3-agenda -c $CREDENTIALS_FILE_PATH` with "$CREDENTIALS_FILE_PATH" replaced with the path to the credentials.json file you downloaded in the previous step. 3. Add configuration to your bar (examples in the Examples section below). From 6a99b66fe08bdc17a4e22eeefb7dbb0b9ae4cd8a Mon Sep 17 00:00:00 2001 From: Maximiliano Greco Date: Mon, 30 Oct 2023 22:36:08 +0100 Subject: [PATCH 09/14] enh(makefile): replace version in download url --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 3f1d332..ffd41a4 100644 --- a/Makefile +++ b/Makefile @@ -22,6 +22,7 @@ release: ## Create a release: make release v=0.1.0 @if [ -z "$(v)" ]; then echo "Missing version number:\nUse: make release v=0.1"; exit 1; fi @sed -i -e "s/version = \".*\"/version = \"$(v)\"/" pyproject.toml @sed -i -e "s/__version__ = \".*\"/__version__ = \"$(v)\"/" src/i3_agenda/__init__.py + @sed -i -e "s/archive\/[0-9]\+\.[0-9]\+\(\.[0-9]\+\)\?\.tar\.gz/archive\/$(v).tar.gz/" pyproject.toml @git diff @# ideally we would use the following @# git add pyproject.toml src/i3_agenda/__init__.py From 746f62f915e739ecf7a9944d98c7805f418ae18f Mon Sep 17 00:00:00 2001 From: Maximiliano Greco Date: Wed, 1 Nov 2023 20:59:26 +0100 Subject: [PATCH 10/14] enh(requirements): makes python 3.7 compatible updating typing_extesion version --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 05eadac..666f6ae 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,4 @@ google-api-python-client>=2.66 google-auth-httplib2>=0.1. google-auth-oauthlib>=0.7 aiohttp>=3.8 -typing_extensions>=4.8.0 +typing_extensions>=4.7.1 From a623d9a1f251898e101203743814461c97cee1f8 Mon Sep 17 00:00:00 2001 From: Maximiliano Greco Date: Wed, 1 Nov 2023 21:00:49 +0100 Subject: [PATCH 11/14] enh(pyproject): bump version in archive url --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index db9e500..0f901a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ dependencies = [ ] [project.urls] -Download = "https://github.com/rosenpin/i3-agenda/archive/1.7.tar.gz" +Download = "https://github.com/rosenpin/i3-agenda/archive/1.8.tar.gz" [project.scripts] i3-agenda = "i3_agenda.main:main" From 5fe87b9ad55dfe3fc63abd6cf96d09b442b3b340 Mon Sep 17 00:00:00 2001 From: Maximiliano Greco Date: Wed, 1 Nov 2023 21:01:31 +0100 Subject: [PATCH 12/14] cln(makefile): minor change in comment --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index ffd41a4..d1ffce4 100644 --- a/Makefile +++ b/Makefile @@ -24,7 +24,7 @@ release: ## Create a release: make release v=0.1.0 @sed -i -e "s/__version__ = \".*\"/__version__ = \"$(v)\"/" src/i3_agenda/__init__.py @sed -i -e "s/archive\/[0-9]\+\.[0-9]\+\(\.[0-9]\+\)\?\.tar\.gz/archive\/$(v).tar.gz/" pyproject.toml @git diff - @# ideally we would use the following + @# Ideally, we would use the following @# git add pyproject.toml src/i3_agenda/__init__.py @# git commit -m "Release v$(v)" @# git tag -a v$(v) -m "Release v$(v)" From 6e08275d675bde7a37385dfcf31fe8dead452d60 Mon Sep 17 00:00:00 2001 From: Maximiliano Greco Date: Wed, 1 Nov 2023 21:01:57 +0100 Subject: [PATCH 13/14] enh(action): Remove working directory in test section --- .github/workflows/github-action.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/github-action.yml b/.github/workflows/github-action.yml index 8467d39..4eff34b 100644 --- a/.github/workflows/github-action.yml +++ b/.github/workflows/github-action.yml @@ -31,11 +31,9 @@ jobs: # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 src --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with pytest - working-directory: ./i3_agenda run: | - pytest tests --junitxml=../junit/test-results.xml - pytest tests --doctest-modules --cov=. --cov-report=xml:../coverage/cov.xml --cov-report=html:../coverage/ - + pytest tests --junitxml=junit/test-results.xml + pytest tests --doctest-modules --cov=. --cov-report=xml:coverage/cov.xml --cov-report=html:coverage/ - name: Code Coverage Report uses: irongut/CodeCoverageSummary@51cc3a756ddcd398d447c044c02cb6aa83fdae95 with: @@ -59,4 +57,3 @@ jobs: with: name: coverage path: pr/ - From 1043fabc56ef98dd9a61809dd0313d519b6408e0 Mon Sep 17 00:00:00 2001 From: Maximiliano Greco Date: Wed, 1 Nov 2023 21:17:03 +0100 Subject: [PATCH 14/14] enh(action): ensures i3_agenda is installed --- .github/workflows/github-action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/github-action.yml b/.github/workflows/github-action.yml index 4eff34b..078690c 100644 --- a/.github/workflows/github-action.yml +++ b/.github/workflows/github-action.yml @@ -23,7 +23,7 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements-dev.txt - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + pip install . -r requirements.txt - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names