Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add missing mandatory fields to user generated components #390

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,72 @@ Related projects
* `icalevents <https://github.com/irgangla/icalevents>`_. It is built on top of icalendar and allows you to query iCal files and get the events happening on specific dates. It manages recurrent events as well.
* `recurring-ical-events <https://pypi.org/project/recurring-ical-events/>`_. Library to query an ``ICalendar`` object for events happening at a certain date or within a certain time.
* `x-wr-timezone <https://pypi.org/project/x-wr-timezone/>`_. Library to make ``ICalendar`` objects and files using the non-standard ``X-WR-TIMEZONE`` compliant with the standard (RFC 5545).


Examples
================
Create a calendear and add an event to it (master branch)

.. code-block:: python

#!/usr/bin/env python3
from dateutil import tz
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are already using pytz and hopefully soon using tzinfo, let's not start using dateutil's tz implementation as well.

Also, at least in this snippet, we don't really need it, or am I missing something?

from datetime import datetime

from icalendar import ComponentWithRequiredFieldsFactory, vDatetime

def main():
component_factory = ComponentWithRequiredFieldsFactory(tzid='Europe/Warsaw')

calendar = component_factory['VCALENDAR']()
event = component_factory['VEVENT'](
DTSTART=vDDDTypes(datetime.now()),
DTEND=vDDDTypes(datetime(year=2050, month=7, day=22, hour=12)),
SUMMARY='A sentence succinctly describing the event',
LOCATION='Where the event will take place',
ORGANIZER='[email protected]',
DESCRIPTION='Longer and more detailed version of the summary\nIt can also be multi-line',
COMMENT='A comment')
event.add('attendee', '[email protected]')
event.add('attendee', '[email protected]')
timezone = component_factory['VTIMEZONE']()
calendar.add_component(timezone)
calendar.add_component(event)
print(calendar.to_ical().decode('utf-8'))


if __name__ == '__main__':
main()


You can view it with the handy CLI tool by:

.. code-block:: text

python SCRIPT-NAME.py > sample.ics && pythom -m icalendar.cli sample.ics

Create a ics file with 5 alarms, each 10 minutes apart

.. code-block:: python

#!/usr/bin/env python3
from datetime import datetime, timedelta

from icalendar import ComponentWithRequiredFieldsFactory, vDatetime

def main():
now = datetime.now()
alarms_triggers = [now + timedelta(minutes=10 * i) for i in range(5)]
component_factory = ComponentWithRequiredFieldsFactory(tzid='Europe/Warsaw', alarm_trigger_supplier=lambda: alarms_triggers.pop())

calendar = component_factory['VCALENDAR']()
calendar.add_component(component_factory['VTIMEZONE']())

for _ in alarms_triggers:
calendar.add_component(component_factory['VALARM']())

print(calendar.to_ical().decode('utf-8'))

if __name__ == '__main__':
main()

1 change: 1 addition & 0 deletions src/icalendar/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
FreeBusy,
Alarm,
ComponentFactory,
ComponentWithRequiredFieldsFactory,
)
# Property Data Value Types
from icalendar.prop import (
Expand Down
57 changes: 54 additions & 3 deletions src/icalendar/cal.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
These are the defined components.
"""
from datetime import datetime, timedelta

from icalendar import __version__
from icalendar.caselessdict import CaselessDict
from icalendar.parser import Contentline
from icalendar.parser import Contentlines
Expand All @@ -12,8 +14,9 @@
from icalendar.parser import q_split
from icalendar.parser_tools import DEFAULT_ENCODING
from icalendar.prop import TypesFactory
from icalendar.prop import vText, vDDDLists
from icalendar.prop import vText, vDDDLists, vDDDTypes
from icalendar.timezone_cache import _timezone_cache
from icalendar.tools import UIDGenerator

import pytz
import dateutil.rrule, dateutil.tz
Expand All @@ -24,11 +27,60 @@
######################################
# The component factory

class ComponentWithRequiredFieldsFactory(CaselessDict):
"""All components defined in rfc 5545 in the section 3.6 are registered in this factory class.
All components returned by this factory should have all the required fields
as per each component's definition.
"""

def __init__(self, host_name='example.com', tzid='Europe/London', daylight_saving=False, alarm_action='AUDIO', alarm_trigger_supplier=None, *args, **kwargs):
"""Set keys to upper for initial dict.
:param host_name is the suffix to be appended after components uid, e.g
host_name='example.com' would result in [email protected]

:param tzid is the the timezone id you'd like your event to be in, e.g Europe/Warsaw

:param alarm_trigger_supplier is a callable that gives you the the date
when the alarm is to go off, e.g lambda: datetime.datime(year=2002, month=7, day=22).
Bother with it, if and only if, you plan on creating alarms with this factory
"""
super().__init__(*args, **kwargs,)
self['VEVENT'] = lambda **kwargs: Event(**kwargs, UID=UIDGenerator.uid(host_name), DTSTAMP=vDDDTypes(datetime.now(pytz.timezone(tzid))))
self['VTODO'] = lambda **kwargs: Todo(**kwargs, UID=UIDGenerator.uid(host_name), DTSTAMP=vDDDTypes(datetime.now(pytz.timezone(tzid))))
self['VJOURNAL'] = lambda **kwargs: Journal(**kwargs, UID=UIDGenerator.uid(host_name), DTSTAMP=vDDDTypes(datetime.now(pytz.timezone(tzid))))
self['VFREEBUSY'] = lambda **kwargs: FreeBusy(**kwargs, UID=UIDGenerator.uid(host_name), DTSTAMP=vDDDTypes(datetime.now(pytz.timezone(tzid))))
self['VTIMEZONE'] = lambda **kwargs: self._create_timezone(tzid, daylight_saving, **kwargs)
self['VALARM'] = lambda **kwargs: self.create_alarm(alarm_action, alarm_trigger_supplier(), **kwargs)
self['VCALENDAR'] = lambda **kwargs: Calendar(**kwargs, PRODID='icalendar', VERSION='2')
self._host_name = host_name

def _create_timezone(self, tzid, daylight_saving, **kwargs):
now = datetime.now(dateutil.tz.gettz(tzid))
offset = now.strftime('%z')
timezone = Timezone(**kwargs, tzid=now.tzname())
timezone_type = TimezoneDaylight if daylight_saving else TimezoneStandard
# DTSTART is probably very wrong. I have no idea what it should be set to
timezone_type = timezone_type(DTSTART=vDDDTypes(now), TZOFFSETTO=offset, TZOFFSETFROM=offset)
timezone.add_component(timezone_type)
return timezone



def create_alarm(self, alarm_action, alarm_trigger, **kwargs):
"""Create a VALARM
:param alarm_action: what is supposed to happen on alarm trigger, e.g use AUDIO to play a sound
on trigger
:type alarm_action: string

:param alarm_trigger: at what time is the alarm supposed to trigger
:type alarm_trigger: :class `datetime.datetime`
"""
return Alarm(**kwargs, action=alarm_action, trigger=vDDDTypes(alarm_trigger))

class ComponentFactory(CaselessDict):
"""All components defined in rfc 2445 are registered in this factory class.
To get a component you can use it like this.
"""

def __init__(self, *args, **kwargs):
"""Set keys to upper for initial dict.
"""
Expand All @@ -43,7 +95,6 @@ def __init__(self, *args, **kwargs):
self['VALARM'] = Alarm
self['VCALENDAR'] = Calendar


# These Properties have multiple property values inlined in one propertyline
# seperated by comma. Use CaselessDict as simple caseless set.
INLINE = CaselessDict({
Expand Down
14 changes: 12 additions & 2 deletions src/icalendar/tests/test_unit_cal.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import pytz
import re


class TestCalComponent(unittest.TestCase):

def test_cal_Component(self):
Expand Down Expand Up @@ -356,7 +355,6 @@ def test_repr(self):


class TestCal(unittest.TestCase):

def test_cal_ComponentFactory(self):
ComponentFactory = icalendar.cal.ComponentFactory
factory = ComponentFactory()
Expand All @@ -371,6 +369,18 @@ def test_cal_ComponentFactory(self):
factory.get('VCALENDAR', icalendar.cal.Component),
icalendar.cal.Calendar)

def test_cal_ComponentWithRequiredFieldsFactory_properly_creates_components(self):
from icalendar.cal import Event, Journal, Todo, FreeBusy, Timezone, TimezoneStandard, TimezoneDaylight, Alarm, Calendar
names = ['VEVENT', 'VTODO', 'VJOURNAL', 'VFREEBUSY', 'VTIMEZONE', 'VALARM', 'VCALENDAR']
component_types = [Event, Todo, Journal, FreeBusy, Timezone, Alarm, Calendar]

component_factory = icalendar.cal.ComponentWithRequiredFieldsFactory(alarm_trigger_supplier=lambda: datetime.now())
for name, component_type in zip(names, component_types):
component = component_factory[name]()
self.assertIsInstance(component, component_type)
for required_field in component.required:
self.assertIn(required_field, component)

def test_cal_Calendar(self):
# Setting up a minimal calendar component looks like this
cal = icalendar.cal.Calendar()
Expand Down