Skip to content

Commit

Permalink
Merge pull request #175 from niccokunzmann/issue-133-2
Browse files Browse the repository at this point in the history
test how to extend recurring ical events
  • Loading branch information
niccokunzmann authored Sep 10, 2024
2 parents af614ab + 0a3f5a9 commit 872d3c0
Show file tree
Hide file tree
Showing 4 changed files with 434 additions and 62 deletions.
159 changes: 137 additions & 22 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -305,9 +305,7 @@ Here is a template code for choosing the supported types of components:
If a type of component is not listed here, it can be added.
Please create an issue for this in the source code repository.

You can create subclasses and customizations.


For further customization, please refer to the section on how to extend the default functionality.

Speed
*****
Expand All @@ -332,19 +330,153 @@ Passing ``skip_bad_series=True`` as ``of()`` argument will totally skip theses e

.. code:: Python
# Create a calendar that contains broken events.
>>> calendar_file = CALENDARS / "bad_rrule_missing_until_event.ics"
>>> calendar_with_bad_event = icalendar.Calendar.from_ical(calendar_file.read_bytes())
# default: error
# By default, broken events result in errors.
>>> recurring_ical_events.of(calendar_with_bad_event, skip_bad_series=False).count()
Traceback (most recent call last):
...
recurring_ical_events.BadRuleStringFormat: UNTIL parameter is missing: FREQ=WEEKLY;BYDAY=TH;WKST=SU;UNTL=20191023
# skip the bad events
# With skip_bad_series=True we skip the series that we cannot handle.
>>> recurring_ical_events.of(calendar_with_bad_event, skip_bad_series=True).count()
0
Architecture
------------

.. image:: img/architecture.png
:alt: Architecture Diagram showing the components interacting

Each icalendar **Calendar** can contain Events, Journal entries,
TODOs and others, called **Components**.
Those entries are grouped by their ``UID``.
Such a ``UID`` defines a **Series** of **Occurrences** that take place at
a given time.
Since each **Component** is different, the **ComponentAdapter** offers a unified
interface to interact with them.
The **Calendar** gets filtered and for each ``UID``,
a **Series** can use one or more **ComponentAdapters** to create
**Occurrences** of what happens in a time span.
These **Occurrences** are used internally and convert to **Components** for further use.

Extending ``recurring-ical-events``
***********************************

All the functionality of ``recurring-ical-events`` can be extended and modified.
To understand where to extend, have a look at the `Architecture`_.

The first place for extending is the collection of components.
Components are collected into a ``Series``.
A series belongs together because all components have the same ``UID``.
In this example, we collect one VEVENT which matches a certain UID:

.. code:: Python
>>> from recurring_ical_events import SelectComponents, EventAdapter, Series
>>> from icalendar.cal import Component
>>> from typing import Sequence
# create the calendar
>>> calendar_file = CALENDARS / "machbar_16_feb_2019.ics"
>>> machbar_calendar = icalendar.Calendar.from_ical(calendar_file.read_bytes())
# Create a collector of components that searches for an event with a specific UID
>>> class CollectOneUIDEvent(SelectComponents):
... def __init__(self, uid:str) -> None:
... self.uid = uid
... def collect_series_from(self, source: Component, suppress_errors: tuple) -> Sequence[Series]:
... components : list[Component] = []
... for component in source.walk("VEVENT"):
... if component.get("UID") == self.uid:
... components.append(EventAdapter(component))
... return [Series(components)] if components else []
# collect only one UID: 4mm2ak3in2j3pllqdk1ubtbp9p@google.com
>>> one_uid = CollectOneUIDEvent("4mm2ak3in2j3pllqdk1ubtbp9p@google.com")
>>> uid_query = recurring_ical_events.of(machbar_calendar, components=[one_uid])
>>> uid_query.count() # the event has no recurrence and thus there is only one
1
Several ways of extending the functionality have been created to override internals.
These can be subclassed or composed.

Below, you can choose to collect all components. Subclasses can be created for the
``Series`` and the ``Occurrence``.

.. code:: Python
>>> from recurring_ical_events import AllKnownComponents, Series, Occurrence
# we create a calendar with one event
>>> calendar_file = CALENDARS / "one_event.ics"
>>> one_event = icalendar.Calendar.from_ical(calendar_file.read_bytes())
# You can override the Occurrence and Series classes for all computable components
>>> select_all_known = AllKnownComponents(series=Series, occurrence=Occurrence)
>>> select_all_known.names # these are the supported types of components
['VEVENT', 'VTODO', 'VJOURNAL']
>>> query_all_known = recurring_ical_events.of(one_event, components=[select_all_known])
# There should be exactly one event.
>>> query_all_known.count()
1
This example shows that the behavior for specific types of components can be extended.
Additional to the series, you can change the ``ComponentAdapter`` that provides
a unified interface for all the components with the same name (``VEVENT`` for example).

.. code:: Python
>>> from recurring_ical_events import ComponentsWithName, EventAdapter, JournalAdapter, TodoAdapter
# You can also choose to select only specific subcomponents by their name.
# The default arguments are added to show the extensibility.
>>> select_events = ComponentsWithName("VEVENT", adapter=EventAdapter, series=Series, occurrence=Occurrence)
>>> select_todos = ComponentsWithName("VTODO", adapter=TodoAdapter, series=Series, occurrence=Occurrence)
>>> select_journals = ComponentsWithName("VJOURNAL", adapter=JournalAdapter, series=Series, occurrence=Occurrence)
# There should be one event happening and nothing else
>>> recurring_ical_events.of(one_event, components=[select_events]).count()
1
>>> recurring_ical_events.of(one_event, components=[select_todos]).count()
0
>>> recurring_ical_events.of(one_event, components=[select_journals]).count()
0
So, if you would like to modify all events that are returned by the query,
you can do that subclassing the ``Occurrence`` class.


.. code:: Python
# This occurence changes adds a new attribute to the resulting events
>>> class MyOccurrence(Occurrence):
... """An occurrence that modifies the component."""
... def as_component(self, keep_recurrence_attributes: bool) -> Component:
... """Return a shallow copy of the source component and modify some attributes."""
... component = super().as_component(keep_recurrence_attributes)
... component["X-MY-ATTRIBUTE"] = "my occurrence"
... return component
>>> query = recurring_ical_events.of(one_event, components=[ComponentsWithName("VEVENT", occurrence=MyOccurrence)])
>>> event = next(query.all())
>>> event["X-MY-ATTRIBUTE"]
'my occurrence'
This library allows extension of functionality during the selection of components to calculate using these classes:

* ``ComponentsWithName`` - for components of a certain name
* ``AllKnownComponents`` - for all components known
* ``SelectComponents`` - the interface to provide

You can further customize behaviour by subclassing these:

* ``ComponentAdapter`` such as ``EventAdapter``, ``JournalAdapter`` or ``TodoAdapter``.
* ``Series``
* ``Occurrence``
* ``CalendarQuery``

Version Fixing
**************
Expand Down Expand Up @@ -429,23 +561,6 @@ To release new versions,
6. notify the issues about their release

Architecture
------------

.. image:: img/architecture.png
:alt: Architecture Diagram showing the components interacting

Each icalendar **Calendar** can contain Events, Journal entries,
TODOs and others, called **Components**.
Those entries are grouped by their ``UID``.
Such a ``UID`` defines a **Series** of **Occurrences** that take place at
a given time.
Since each **Component** is different, the **ComponentAdapter** offers a unified
interface to interact with them.
The **Calendar** gets filtered and for each ``UID``,
a **Series** can use one or more **ComponentAdapters** to create
**Occurrences** of what happens in a time span.
These **Occurrences** are used internally and convert to **Components** for further use.

Changelog
---------
Expand Down
Loading

0 comments on commit 872d3c0

Please sign in to comment.