ICal has some complexity to it: Events, TODOs and Journal entries can be repeated, removed from the feed and edited later on. This tool takes care of these circumstances.
Let's put our expertise together and build a tool that can solve this!
Not included:
pip install recurring-ical-events
import icalendar
import recurring_ical_events
import urllib.request
start_date = (2019, 3, 5)
end_date = (2019, 4, 1)
url = "http://tinyurl.com/y24m3r8f"
ical_string = urllib.request.urlopen(url).read()
calendar = icalendar.Calendar.from_ical(ical_string)
events = recurring_ical_events.of(calendar).between(start_date, end_date)
for event in events:
start = event["DTSTART"].dt
duration = event["DTEND"].dt - event["DTSTART"].dt
print("start {} duration {}".format(start, duration))
Output:
start 2019-03-18 04:00:00+01:00 duration 1:00:00
start 2019-03-20 04:00:00+01:00 duration 1:00:00
start 2019-03-19 04:00:00+01:00 duration 1:00:00
start 2019-03-07 02:00:00+01:00 duration 1:00:00
start 2019-03-08 01:00:00+01:00 duration 2:00:00
start 2019-03-09 03:00:00+01:00 duration 0:30:00
start 2019-03-10 duration 1 day, 0:00:00
The icalendar module is responsible for parsing and converting calendars. The recurring_ical_events module uses such a calendar and creates all repetitions of its events within a time span.
To import this module, write
import recurring_ical_events
There are several methods you can use to unfold repeating events, such as at(a_time)
and between(a_start, an_end)
.
at(a_date)
You can get all events which take place at a_date
.
A date can be a year, e.g. 2023
, a month of a year e.g. January in 2023 (2023, 1)
, a day of a certain month e.g. (2023, 1, 1)
, an hour e.g. (2023, 1, 1, 0)
, a minute e.g. (2023, 1, 1, 0, 0)
, or second as well as a datetime.date object and datetime.datetime.
The start and end are inclusive. As an example: if an event is longer than one day it is still included if it takes place at a_date
.
a_date = 2023 # a year
a_date = (2023,) # a year
a_date = (2023, 1) # January in 2023
a_date = (2023, 1, 1) # the 1st of January in 2023
a_date = "20230101" # the 1st of January in 2023
a_date = (2023, 1, 1, 0) # the first hour of the year 2023
a_date = (2023, 1, 1, 0, 0) # the first minute in 2023
a_date = datetime.date(2023) # the first day in 2023
a_date = datetime.date(2023, 1, 1) # the first day in 2023
a_date = datetime.datetime.now() # this exact second
events = recurring_ical_events.of(an_icalendar_object).at(a_date)
The resulting events
are a list of icalendar events, see below.
between(start, end)
between(start, end)
returns all events happening between a start and an end time. Both arguments can be datetime.datetime, datetime.date, tuples of numbers passed as arguments to datetime.datetime or strings in the form of
%Y%m%d
(yyyymmdd
) and %Y%m%dT%H%M%SZ
(yyyymmddThhmmssZ
).
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.
events
as listThe 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:
of(calendar, keep_recurrence_attributes=True)
.By default the recurring_ical_events
only selects events as the name already implies.
However, there are different components available in a calendar.
You can select which components you like to have returned by passing components
to the of
function:
of(a_calendar, components=["VEVENT"])
Here is a template code for choosing the supported types of components:
events = recurring_ical_events.of(calendar).between(...)
journals = recurring_ical_events.of(calendar, components=["VJOURNAL"]).between(...)
todos = recurring_ical_events.of(calendar, components=["VTODO"]).between(...)
all = recurring_ical_events.of(calendar, components=["VTODO", "VEVENT", "VJOURNAL"]).between(...)
If a type of component is not listed here, it can be added. Please create an issue for this in the source code repository.
If you use between()
or at()
several times, it is faster to re-use the object coming from of()
.
rcalendar = recurring_ical_events.of(an_icalendar_object)
events_of_day_1 = rcalendar.at(day_1)
events_of_day_2 = rcalendar.at(day_2)
events_of_day_3 = rcalendar.at(day_3)
# ...
If you use this library in your code, you may want to make sure that
updates can be received but they do not break your code.
The version numbers are handeled this way: a.b.c
example: 0.1.12
c
is changed for each minor bug fix.b
is changed whenever new features are added.a
is changed when the interface or major assumptions change that may break your code.So, I recommend to version-fix this library to stay with the same a
while b
and c
can change.
This library is part of the Open Web Calendar Collective. 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.
Optional: Install virtualenv and Python3 and create a virtual environment.
virtualenv -p python3 ENV
source ENV/bin/activate
Install the packages.
pip install -r requirements.txt -r test-requirements.txt
Run the tests
pytest
To release new versions,
edit the Changelog Section
edit setup.py, the __version__
variable
create a commit and push it
Wait for GitHub Actions to finish the build.
run
python3 setup.py tag_and_deploy
notify the issues about their release
This project's development is driven by tests. Tests assure a consistent interface and less knowledge lost over time. If you like to change the code, tests help that nothing breaks in the future. They are required in that sense. Example code and ics files can be transferred into tests and speed up fixing bugs.
You can view the tests in the test folder.
If you have a calendar ICS file for which this library does not
generate the desired output, you can add it to the test/calendars
folder and write tests for what you expect.
If you like, open an issue first, e.g. to discuss the changes and
how to go about it.
VEVENT
by default. Add of(... ,components=...)
parameter to select which kinds of components should be returned. See Issue 101.beta
indicator. This library works okay: Feature requests come in, not so much bug reports.X-WR-TIMEZONE
calendars which contain events without an explicit time zone, see Issue 86.zoneinfo.ZoneInfo
time zones, see Issue 57.between()
and at()
take the same kind of arguments. These arguments are documented.at()
does not return an event starting at the next day, see Issue 44.rrule
X-WR-TIMEZONE
property.Nicco Kunzmann talked about this library at the FOSSASIA 2022 Summit: