summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJochen Sprickerhof <jspricke@debian.org>2023-09-27 09:36:23 +0200
committerJochen Sprickerhof <jspricke@debian.org>2023-09-27 09:36:23 +0200
commitf95de8455543b71d84c06c9bc757fcf3206febae (patch)
tree7e7b91fb8c93bf9e55da4f50a473a52eb02a81d1
parent5777b0ef7a80ea731b02b26477476acce19c497e (diff)
New upstream version 2.1.0
-rw-r--r--README.rst108
-rw-r--r--recurring_ical_events.py15
-rw-r--r--requirements.txt5
-rw-r--r--setup.py3
-rw-r--r--test/calendars/issue-62-moved-event-2.ics79
-rw-r--r--test/calendars/issue_113_period_in_rdate.ics32
-rw-r--r--test/test_issue_113_period_in_rdate.py28
-rw-r--r--test/test_issue_62_moved_event.py30
-rw-r--r--test/test_rdate.py13
9 files changed, 241 insertions, 72 deletions
diff --git a/README.rst b/README.rst
index 27509bf..98d78a6 100644
--- a/README.rst
+++ b/README.rst
@@ -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
diff --git a/setup.py b/setup.py
index e673f10..6ef2146 100644
--- a/setup.py
+++ b/setup.py
@@ -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()