diff options
author | Jochen Sprickerhof <jspricke@debian.org> | 2023-09-27 09:36:23 +0200 |
---|---|---|
committer | Jochen Sprickerhof <jspricke@debian.org> | 2023-09-27 09:36:23 +0200 |
commit | f95de8455543b71d84c06c9bc757fcf3206febae (patch) | |
tree | 7e7b91fb8c93bf9e55da4f50a473a52eb02a81d1 | |
parent | 5777b0ef7a80ea731b02b26477476acce19c497e (diff) |
New upstream version 2.1.0
-rw-r--r-- | README.rst | 108 | ||||
-rw-r--r-- | recurring_ical_events.py | 15 | ||||
-rw-r--r-- | requirements.txt | 5 | ||||
-rw-r--r-- | setup.py | 3 | ||||
-rw-r--r-- | test/calendars/issue-62-moved-event-2.ics | 79 | ||||
-rw-r--r-- | test/calendars/issue_113_period_in_rdate.ics | 32 | ||||
-rw-r--r-- | test/test_issue_113_period_in_rdate.py | 28 | ||||
-rw-r--r-- | test/test_issue_62_moved_event.py | 30 | ||||
-rw-r--r-- | test/test_rdate.py | 13 |
9 files changed, 241 insertions, 72 deletions
@@ -17,6 +17,11 @@ Recurring ICal events for Python :target: https://opencollective.com/open-web-calendar/ :alt: Support on Open Collective +.. image:: https://img.shields.io/github/issues/niccokunzmann/python-recurring-ical-events/polar?label=issues%20seek%20funding&color=%23374e96 + :target: https://polar.sh/niccokunzmann/python-recurring-ical-events + :alt: issues seek funding + + ICal has some complexity to it: Events, TODOs and Journal entries can be repeated, removed from the feed and edited later on. @@ -53,6 +58,17 @@ Installation pip install recurring-ical-events +Support +------- + +- `Support using GitHub Sponsors <https://github.com/sponsors/niccokunzmann>`_ +- `Fund specific issues using Polar <https://polar.sh/niccokunzmann/python-recurring-ical-events>`_ +- `Support using Open Collective <https://opencollective.com/open-web-calendar/>`_ +- `Support using thanks.dev <https://thanks.dev>`_ + +We accept donations to sustain our work, once or regular. +Consider donating money to open-source as everyone benefits. + Example ------- @@ -91,7 +107,7 @@ Usage ----- The `icalendar <https://pypi.org/project/icalendar/>`_ module is responsible for parsing and converting calendars. -The `recurring_ical_events <https://pypi.org/project/recurring-ical-events/>`_ module uses such a calendar and creates all repetitions of its events within a time span. +The `recurring_ical_events <https://pypi.org/project/recurring-ical-events/>`_ module uses such a `calendar`_ and creates all repetitions of its events within a time span. To import this module, write @@ -124,7 +140,7 @@ The start and end are inclusive. As an example: if an event is longer than one d events = recurring_ical_events.of(an_icalendar_object).at(a_date) -The resulting ``events`` are a list of icalendar events, see below. +The resulting ``events`` are a list of `icalendar events <https://icalendar.readthedocs.io/en/latest/api.html#icalendar.cal.Event>`_, see below. ``between(start, end)`` *********************** @@ -137,18 +153,18 @@ For examples, see ``at(a_date)`` above. events = recurring_ical_events.of(an_icalendar_object).between(start, end) -The resulting ``events`` are in a list, see below. +The resulting ``events`` are in a list of `icalendar events`_, see below. ``events`` as list ****************** -The result of both ``between(start, end)`` and ``at(a_date)`` is a list of `icalendar`_ events. -By default, all attributes of the event with repetitions are copied, like UID and SUMMARY. +The result of both ``between(start, end)`` and ``at(a_date)`` is a list of `icalendar events`_. +By default, all attributes of the event with repetitions are copied, like ``UID`` and ``SUMMARY``. However, these attributes may differ from the source event: -* **DTSTART** which is the start of the event instance. (always present) -* **DTEND** which is the end of the event instance. (always present) -* **RDATE**, **EXDATE**, **RRULE** are the rules to create event repetitions. +* ``DTSTART`` which is the start of the event instance. (always present) +* ``DTEND`` which is the end of the event instance. (always present) +* ``RDATE``, ``EXDATE``, ``RRULE`` are the rules to create event repetitions. They are **not** included in repeated events, see `Issue 23 <https://github.com/niccokunzmann/python-recurring-ical-events/issues/23>`_. To change this, use ``of(calendar, keep_recurrence_attributes=True)``. @@ -156,7 +172,7 @@ Different Components, not just Events ************************************* By default the ``recurring_ical_events`` only selects events as the name already implies. -However, there are different components available in a calendar. +However, there are different `components <https://icalendar.readthedocs.io/en/latest/api.html#icalendar.cal.Component>`_ available in a `calendar <https://icalendar.readthedocs.io/en/latest/api.html#icalendar.cal.Calendar>`_. You can select which components you like to have returned by passing ``components`` to the ``of`` function: .. code:: Python @@ -203,58 +219,27 @@ The version numbers are handeled this way: ``a.b.c`` example: ``0.1.12`` So, I recommend to version-fix this library to stay with the same ``a`` while ``b`` and ``c`` can change. -Support -------- - -.. image:: https://img.shields.io/opencollective/all/open-web-calendar?label=support%20on%20open%20collective - :target: https://opencollective.com/open-web-calendar/ - :alt: Support on Open Collective - -This library is part of the -`Open Web Calendar Collective <https://opencollective.com/open-web-calendar/>`_. -We accept donations to sustain our work, once or regular. -Open Collective makes it easy for you to give and -for us to receive. -Consider donating money to open-source as everyone benefits. - Development ----------- -1. Optional: Install virtualenv and Python3 and create a virtual environment. - - .. code-block:: shell - - virtualenv -p python3 ENV - source ENV/bin/activate - -2. Install the packages. - - .. code-block:: shell +To run the tests, we use ``tox``. +``tox`` tests all different Python versions which we want to be compatible to. - pip install -r requirements.txt -r test-requirements.txt - -3. Run the tests - - .. code-block:: shell +.. code-block:: shell - pytest + pip3 install tox -New Releases ------------- +To run all the tests: -To release new versions, +.. code-block:: shell -1. edit the Changelog Section -2. edit setup.py, the ``__version__`` variable -3. create a commit and push it -4. Wait for `GitHub Actions <https://github.com/niccokunzmann/python-recurring-ical-events/actions>`_ to finish the build. -5. run + tox - .. code-block:: shell +To run the tests in a specific Python version: - python3 setup.py tag_and_deploy +.. code-block:: shell -6. notify the issues about their release + tox -e py39 Testing ******* @@ -273,9 +258,32 @@ folder and write tests for what you expect. If you like, `open an issue <https://github.com/niccokunzmann/python-recurring-ical-events/issues>`_ first, e.g. to discuss the changes and how to go about it. +New Releases +------------ + +To release new versions, + +1. edit the Changelog Section +2. edit setup.py, the ``__version__`` variable +3. create a commit and push it +4. wait for `GitHub Actions <https://github.com/niccokunzmann/python-recurring-ical-events/actions>`_ to finish the build +5. run + + .. code-block:: shell + + python3 setup.py tag_and_deploy + +6. notify the issues about their release + Changelog --------- +- v2.1.0 + + - Added support for PERIOD values in RDATE. See `Issue 113 <https://github.com/niccokunzmann/python-recurring-ical-events/issues/113>`_. + - Fixed ``icalendar>=5.0.9`` to support ``RDATE`` of type ``PERIOD`` with a time zone. + - Fixed ``pytz>=2023.3`` to assure compatibility. + - v2.0.2 - Fixed omitting last event of ``RRULE`` with ``UNTIL`` when using ``pytz``, the event starting in winter time and ending in summer time. See `Issue 107 <https://github.com/niccokunzmann/python-recurring-ical-events/issues/107>`_. diff --git a/recurring_ical_events.py b/recurring_ical_events.py index eb9cda3..4f47b49 100644 --- a/recurring_ical_events.py +++ b/recurring_ical_events.py @@ -169,6 +169,7 @@ class RepeatedComponent: self.keep_recurrence_attributes = keep_recurrence_attributes self.exdates = [] self.exdates_utc = set() + self.replace_ends = {} # DTSTART -> DTEND # for periods exdates = component.get("EXDATE", []) for exdates in ((exdates,) if not isinstance(exdates, list) else exdates): for exdate in exdates.dts: @@ -178,7 +179,13 @@ class RepeatedComponent: rdates = component.get("RDATE", []) for rdates in ((rdates,) if not isinstance(rdates, list) else rdates): for rdate in rdates.dts: - self.rdates.append(rdate.dt) + if isinstance(rdate.dt, tuple): + # we have a period as rdate + self.rdates.append(rdate.dt[0]) + self.replace_ends[timestamp(rdate.dt[0])] = rdate.dt[1] + else: + # we have a date/datetime + self.rdates.append(rdate.dt) self.make_all_dates_comparable() @@ -242,7 +249,7 @@ class RepeatedComponent: rule.until = until = self._get_rrule_until(rule) if is_pytz(self.start.tzinfo) and rule.until: # when starting in a time zone that is one hour off to the end, - # we might miss the last occurence + # we might miss the last occurrence # see issue 107 and test/test_issue_107_omitting_last_event.py rule = rule.replace(until=rule.until + datetime.timedelta(hours=1)) rule.until = until @@ -266,7 +273,7 @@ class RepeatedComponent: Dates may be mixed and we have many of them. - date - - datetime without timezome + - datetime without timezone - datetime with timezone These three are not comparable but can be converted. """ @@ -311,7 +318,7 @@ class RepeatedComponent: continue if self._unify_exdate(start) in self.exdates_utc: continue - stop = start + self.duration + stop = self.replace_ends.get(timestamp(start), start + self.duration) yield Repetition( self.component, self.convert_to_original_type(start), diff --git a/requirements.txt b/requirements.txt index f8dc157..25fa70a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ -icalendar -pytz +icalendar>=5.0.9 +icalendar<6.0.0 +pytz>=2023.3 python-dateutil>=2.8.1 x-wr-timezone < 1.0.0 x-wr-timezone >= 0.0.5 @@ -11,7 +11,7 @@ PACKAGE_NAME = "recurring_ical_events" HERE = os.path.abspath(os.path.dirname(__file__)) sys.path.insert(0, HERE) # for package import -__version__ = "2.0.2" +__version__ = "2.1.0" __author__ = 'Nicco Kunzmann' @@ -124,7 +124,6 @@ SETUPTOOLS_METADATA = dict( 'License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)', 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Utilities', - 'Intended Audience :: Developers', 'Natural Language :: English', 'Operating System :: OS Independent', 'Programming Language :: Python :: 3', diff --git a/test/calendars/issue-62-moved-event-2.ics b/test/calendars/issue-62-moved-event-2.ics new file mode 100644 index 0000000..cb6ed23 --- /dev/null +++ b/test/calendars/issue-62-moved-event-2.ics @@ -0,0 +1,79 @@ +BEGIN:VCALENDAR +PRODID:-//Google Inc//Google Calendar 70.9054//EN +VERSION:2.0 +CALSCALE:GREGORIAN +METHOD:PUBLISH +X-WR-CALNAME:Test Calendar +X-WR-TIMEZONE:Australia/Sydney +BEGIN:VTIMEZONE +TZID:Australia/Sydney +X-LIC-LOCATION:Australia/Sydney +BEGIN:STANDARD +TZOFFSETFROM:+1100 +TZOFFSETTO:+1000 +TZNAME:AEST +DTSTART:19700405T030000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETFROM:+1000 +TZOFFSETTO:+1100 +TZNAME:AEDT +DTSTART:19701004T020000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VEVENT +DTSTART;TZID=Australia/Sydney:20230808T140000 +DTEND;TZID=Australia/Sydney:20230808T150000 +RRULE:FREQ=WEEKLY;WKST=SU;UNTIL=20230828T135959Z;BYDAY=TU +DTSTAMP:20230629T040023Z +UID:7v3ju5ft4je5iq18nfdk2s3spk@google.com +CREATED:20230629T035454Z +LAST-MODIFIED:20230629T035851Z +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:Datetime +TRANSP:OPAQUE +END:VEVENT +BEGIN:VEVENT +DTSTART;TZID=Australia/Sydney:20230814T140000 +DTEND;TZID=Australia/Sydney:20230814T150000 +DTSTAMP:20230629T040023Z +UID:7v3ju5ft4je5iq18nfdk2s3spk@google.com +RECURRENCE-ID;TZID=Australia/Sydney:20230815T140000 +CREATED:20230629T035454Z +LAST-MODIFIED:20230629T035851Z +SEQUENCE:1 +STATUS:CONFIRMED +SUMMARY:Datetime +TRANSP:OPAQUE +END:VEVENT +BEGIN:VEVENT +DTSTART;VALUE=DATE:20230810 +DTEND;VALUE=DATE:20230811 +RRULE:FREQ=WEEKLY;WKST=SU;UNTIL=20230830;BYDAY=TH +DTSTAMP:20230629T040023Z +UID:6ep37v20d728v14rcgn17v9is6@google.com +CREATED:20230629T035522Z +LAST-MODIFIED:20230629T035854Z +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:All Day +TRANSP:TRANSPARENT +END:VEVENT +BEGIN:VEVENT +DTSTART;VALUE=DATE:20230816 +DTEND;VALUE=DATE:20230817 +DTSTAMP:20230629T040023Z +UID:6ep37v20d728v14rcgn17v9is6@google.com +RECURRENCE-ID;VALUE=DATE:20230817 +CREATED:20230629T035522Z +LAST-MODIFIED:20230629T035854Z +SEQUENCE:1 +STATUS:CONFIRMED +SUMMARY:All Day +TRANSP:TRANSPARENT +END:VEVENT +END:VCALENDAR + diff --git a/test/calendars/issue_113_period_in_rdate.ics b/test/calendars/issue_113_period_in_rdate.ics new file mode 100644 index 0000000..e82c10f --- /dev/null +++ b/test/calendars/issue_113_period_in_rdate.ics @@ -0,0 +1,32 @@ +BEGIN:VCALENDAR +VERSION:2.0 +X-WR-CALNAME;VALUE=TEXT:Test RDATE +BEGIN:VTIMEZONE +TZID:America/Vancouver +BEGIN:STANDARD +DTSTART:20221106T020000 +TZOFFSETFROM:-0700 +TZOFFSETTO:-0800 +RDATE:20231105T020000 +TZNAME:PST +END:STANDARD +BEGIN:DAYLIGHT +DTSTART:20230312T020000 +TZOFFSETFROM:-0800 +TZOFFSETTO:-0700 +RDATE:20240310T020000 +TZNAME:PDT +END:DAYLIGHT +END:VTIMEZONE +BEGIN:VEVENT +UID:1 +DESCRIPTION:Test RDATE +DTSTART;TZID=America/Vancouver:20230920T120000 +DTEND;TZID=America/Vancouver:20230920T140000 +EXDATE;TZID=America/Vancouver:20231220T120000 +RDATE;VALUE=PERIOD;TZID=America/Vancouver:20231213T120000/20231213T150000 +RRULE:FREQ=MONTHLY;COUNT=9;INTERVAL=1;BYDAY=+3WE;BYMONTH=1,2,3,4,5,9,10,11, + 12;WKST=MO +SUMMARY:Test RDATE +END:VEVENT +END:VCALENDAR diff --git a/test/test_issue_113_period_in_rdate.py b/test/test_issue_113_period_in_rdate.py new file mode 100644 index 0000000..db3c861 --- /dev/null +++ b/test/test_issue_113_period_in_rdate.py @@ -0,0 +1,28 @@ +"""This tests that RDATE can be a PERIOD. + +See https://github.com/niccokunzmann/python-recurring-ical-events/issues/113 + + Value Type: The default value type for this property is DATE-TIME. + The value type can be set to DATE or PERIOD. + + If the "RDATE" property is + specified as a PERIOD value the duration of the recurrence + instance will be the one specified by the "RDATE" property, and + not the duration of the recurrence instance defined by the + "DTSTART" property. + +""" +from datetime import datetime +import pytz + +def test_start_of_rdate(calendars): + """The event starts on that time.""" + event = calendars.issue_113_period_in_rdate.at("20231213")[0] + expected_start = pytz.timezone("America/Vancouver").localize(datetime(2023, 12, 13, 12, 0)) + start = event["DTSTART"].dt + assert start == expected_start + +def test_end_of_rdate(calendars): + """The event starts on that time.""" + event = calendars.issue_113_period_in_rdate.at("20231213")[0] + assert event["DTEND"].dt == pytz.timezone("America/Vancouver").localize(datetime(2023, 12, 13, 15, 0))
\ No newline at end of file diff --git a/test/test_issue_62_moved_event.py b/test/test_issue_62_moved_event.py index 835cf43..298010c 100644 --- a/test/test_issue_62_moved_event.py +++ b/test/test_issue_62_moved_event.py @@ -3,6 +3,8 @@ This tests the move of a december event. Issue: https://github.com/niccokunzmann/python-recurring-ical-events/issues/62 ''' +import pytest + def test_event_is_absent(calendars): '''RRULE:FREQ=MONTHLY;BYDAY=-1FR''' @@ -19,6 +21,32 @@ def test_there_is_only_one_event_in_december(calendars): events = calendars.issue_62_moved_event.at((2021, 12)) assert len(events) == 1 - +@pytest.mark.parametrize( + "date,summary", + [ + ("20230810", "All Day"), + ("20230816", "All Day"), + ("20230824", "All Day"), + ("20230808", "Datetime"), + ("20230814", "Datetime"), + ("20230822", "Datetime"), + ] +) +def test_event_is_present(calendars, date, summary): + """Test that the middle event has moved""" + events = calendars.issue_62_moved_event_2.at(date) + assert len(events) == 1 + event = events[0] + assert event["SUMMARY"] == summary +@pytest.mark.parametrize("date", ["20230815", "20230817"]) +def test_event_is_absent(calendars, date): + """We make sure that the moved event is not there.""" + events = calendars.issue_62_moved_event_2.at(date) + assert len(events) == 0 +def test_total_amount_of_events(calendars): + """There are only 6 events!""" + events = calendars.issue_62_moved_event_2.at((2023, 8)) + assert len(events) == 6 + diff --git a/test/test_rdate.py b/test/test_rdate.py index 93ad902..6558dbc 100644 --- a/test/test_rdate.py +++ b/test/test_rdate.py @@ -70,19 +70,6 @@ def test_rdate_and_rrule_can_be_excluded_by_exdate(calendars): events = calendars.rdate.at("20150705") assert len(events) == 0 -def test_period_as_rdate(todo): - """Test the PERIOD type. - - Value Type: The default value type for this property is DATE-TIME. - The value type can be set to DATE or PERIOD. - - If the "RDATE" property is - specified as a PERIOD value the duration of the recurrence - instance will be the one specified by the "RDATE" property, and - not the duration of the recurrence instance defined by the - "DTSTART" property. - """ - def test_rdate_occurs_multiple_times(calendars): """An event can not only have an RDATE once but also many of them.""" events = calendars.rdate_hackerpublicradio.all() |