The calendar data model is described using pydantic models. Pydantic allows expressing effectively a dataclass with types and validation rules, which are reused across components. The data model mirrors the rfc5545 spec with a separate object for each type of component e.g. a calendar, event, todo, etc.
The rfc5545 spec defines a text file format, and the overall structure defines a series of components (e.g. a calendar, an event, a todo) and properties (e.g. a summary, start date and time, due date, category). Properties may additionally have parameters such as a timezone or alternative text display. Components have a hierarchy (e.g. a calendar component has an event sub-component).
The ical library uses pyparsing to
create a very simple gammar for rfc5545, converting the individual lines of an
ics file (called "contentlines") into a structured ParseResult
object which
has a dictionary of fields. The ical library then iterates through each
contentline and builds a stack to manage components and subcomponents, parses
individual properties and parameters associated with the active component, then
returns a ParsedComponent
which contains other components and properties. At
this point we have a tree of components and properties, but have not yet
interpreted the meaning.
The data model is parsed using pydantic
and has parsing and validation rules for each type of data. That is, the library
has a bridge between strongly typed rfc5545 properties (e.g. DATE
, DATE-TIME
) and
python types (e.g. datetime.date
, datetime.datetime
). Where possible, the
built in pydantic encoding and decoding is used, however ical makes heavy use of
custom root validators to perform a lot of the type mapping. Additionally, we want
to be able to support parsing the calendar data model from the output of the parser
as well as parsing values supplied by the user when creating objects manually.
Encoding is the opposite of parsing, converting the pydantic data model back
into an rfc5545 text file. The IcsCalendarStream
model uses both the
internal json encoding as well as custom encoding built on top to handle
everything. The custom logic is needed since a single field in a pydantic
model may be a complex value in the ics output (e.g. containing property
parameters). The json encoding using pydantic encoders could be removed
in the future, relying on entirely custom components, but for now its kind
of nice to reuse even if there are extera layers on top adding complexity.
The design for recurrence was based on the design guidance from Calendar Pro.
The motivation is to support most simple use cases (e.g. home and small business applications) that require repeating events such as daily, weekly, monthly. The other lesser used aspects of the rfc5545 recurrence format like secondly, minutely, hourly or yearly are not needed for these types of use cases.
There are performance implications based on possible event storage and event generation trade-offs. The initial approach will be to design for simplicity first, targeting smaller calendars (e.g. tens of recurring events, not thousands) and may layer in performance optimizations such as caching later, under the existing APIs.
Like other components in this library, the recurrence format is parsed into a data object using pydantic. This library has no additional internal storage. An rrule is stored as a column of an event.
The Timeline
interface is used to iterate over events, and can also work
to abstract away the details of recurring events. Recurring events may
repeat indefinitely, imply the existing iterator only based may need careful
consideration e.g. it can't serialize all events into the sorted heapq.
The python library dateutil
has an rrule
module with a lightweight and complete implementation of recurrence rules.
Events are generated using a timeline fed by bunch of iterators. There is one iterator for all non-recurring events, then a separate iterator for each recurring event. A merged iterator peeks into the input of each iterator to decide which one to pull from when determinig the next item in the iterator.
An individual instance of a recurring event is generated with the same UID
,
but with a different RECURRENCE_ID
based on the start time of that instance.
An entire series may be modified by modifying the original event without
referencing a RECURRENCE_ID
. A RECURRENCE_ID
can refer to a specific
instance in the series, or with THIS_AND_FUTURE
to apply to forward looking
events.
When modifying an instance, a new copy of the event is created for that instance and the original event representing the whole series is modified to exclude the edited instance.