kopia lustrzana https://github.com/collective/icalendar
commit
4cb7b121a4
50
CHANGES.rst
50
CHANGES.rst
|
@ -1,14 +1,14 @@
|
|||
Changelog
|
||||
=========
|
||||
|
||||
5.0.8 (unreleased)
|
||||
------------------
|
||||
5.0.11 (unreleased)
|
||||
-------------------
|
||||
|
||||
Minor changes:
|
||||
|
||||
- Update build configuration to build readthedocs. #538
|
||||
- No longer run the ``plone.app.event`` tests.
|
||||
- Move pip caching into Python setup action.
|
||||
- The cli utility now displays start and end datetimes in the user's local timezone.
|
||||
Ref: #561
|
||||
[vimpostor]
|
||||
|
||||
Breaking changes:
|
||||
|
||||
|
@ -20,7 +20,45 @@ New features:
|
|||
|
||||
Bug fixes:
|
||||
|
||||
- ...
|
||||
- Multivalue FREEBUSY property is now parsed properly
|
||||
Ref: #27
|
||||
[jacadzaca]
|
||||
|
||||
5.0.10 (unreleased)
|
||||
-------------------
|
||||
|
||||
Bug fixes:
|
||||
|
||||
- Component._encode stops ignoring parameters argument on native values, now merges them
|
||||
Fixes: #557
|
||||
[zocker1999net]
|
||||
|
||||
5.0.9 (2023-09-24)
|
||||
------------------
|
||||
|
||||
Bug fixes:
|
||||
|
||||
- PERIOD values now set the timezone of their start and end. #556
|
||||
|
||||
5.0.8 (2023-09-18)
|
||||
------------------
|
||||
|
||||
Minor changes:
|
||||
|
||||
- Update build configuration to build readthedocs. #538
|
||||
- No longer run the ``plone.app.event`` tests.
|
||||
- Add documentation on how to parse ``.ics`` files. #152
|
||||
- Move pip caching into Python setup action.
|
||||
- Check that issue #165 can be closed.
|
||||
- Updated about.rst for issue #527
|
||||
- Avoid ``vText.__repr__`` BytesWarning.
|
||||
|
||||
Bug fixes:
|
||||
|
||||
- Calendar components are now properly compared
|
||||
Ref: #550
|
||||
Fixes: #526
|
||||
[jacadzaca]
|
||||
|
||||
5.0.7 (2023-05-29)
|
||||
------------------
|
||||
|
|
21
README.rst
21
README.rst
|
@ -42,6 +42,27 @@ files.
|
|||
.. _`pytz`: https://pypi.org/project/pytz/
|
||||
.. _`BSD`: https://github.com/collective/icalendar/issues/2
|
||||
|
||||
Quick Guide
|
||||
-----------
|
||||
|
||||
To **install** the package, run::
|
||||
|
||||
pip install icalendar
|
||||
|
||||
You can open an ``.ics`` file and see all the events::
|
||||
|
||||
>>> import icalendar
|
||||
>>> path_to_ics_file = "src/icalendar/tests/calendars/example.ics"
|
||||
>>> with open(path_to_ics_file) as f:
|
||||
... calendar = icalendar.Calendar.from_ical(f.read())
|
||||
>>> for event in calendar.walk('VEVENT'):
|
||||
... print(event.get("SUMMARY"))
|
||||
New Year's Day
|
||||
Orthodox Christmas
|
||||
International Women's Day
|
||||
|
||||
Using this package, you can also create calendars from scratch or edit existing ones.
|
||||
|
||||
Versions and Compatibility
|
||||
--------------------------
|
||||
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
About
|
||||
=====
|
||||
|
||||
`Max M`_ had often needed to parse and generate iCalendar files. Finally he got
|
||||
`Max M`_ had often needed to parse and generate iCalendar files. Finally, he got
|
||||
tired of writing ad-hoc tools. This package is his attempt at making an
|
||||
iCalendar package for Python. The inspiration has come from the email package
|
||||
in the standard lib, which he thinks is pretty simple, yet efficient and
|
||||
powerful.
|
||||
|
||||
At the time of writing this, last version was released more then 2 years ago.
|
||||
Since then many things have changes. For one, `RFC 2445`_ was updated by `RFC
|
||||
5545`_ which makes this package. So in some sense this package became outdated.
|
||||
The icalendar package is an RFC 5545-compatible parser/generator for iCalendar files.
|
||||
|
||||
.. _`Max M`: http://www.mxm.dk
|
||||
.. _`RFC 2445`: https://tools.ietf.org/html/rfc2445
|
||||
|
|
|
@ -43,6 +43,7 @@ icalendar contributors
|
|||
- Thomas Bruederli <thomas@roundcube.net>
|
||||
- Thomas Weißschuh <thomas@t-8ch.de>
|
||||
- Victor Varvaryuk <victor.varvariuc@gmail.com>
|
||||
- Ville Skyttä <ville.skytta@iki.fi>
|
||||
- Wichert Akkerman <wichert@wiggy.net>
|
||||
- cillianderoiste <cillian.deroiste@gmail.com>
|
||||
- fitnr <fitnr@fakeisthenewreal>
|
||||
|
@ -69,6 +70,7 @@ icalendar contributors
|
|||
- `Natasha Mattson <https://github.com/natashamm`_
|
||||
- `NikEasY <https://github.com/NikEasY>`_
|
||||
- Matt Lewis <git@semiprime.com>
|
||||
- Felix Stupp <felix.stupp@banananet.work>
|
||||
|
||||
Find out who contributed::
|
||||
|
||||
|
|
|
@ -112,7 +112,7 @@ Try it out:
|
|||
Type "help", "copyright", "credits" or "license" for more information.
|
||||
>>> import icalendar
|
||||
>>> icalendar.__version__
|
||||
'5.0.7'
|
||||
'5.0.10'
|
||||
|
||||
Building the documentation locally
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
|
|
@ -342,5 +342,5 @@ Print out the calendar::
|
|||
More documentation
|
||||
==================
|
||||
|
||||
Have a look at the tests of this package to get more examples.
|
||||
Have a look at the `tests <https://github.com/collective/icalendar/tree/master/src/icalendar/tests>`__ of this package to get more examples.
|
||||
All modules and classes docstrings, which document how they work.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
__version__ = '5.0.7'
|
||||
__version__ = '5.0.10'
|
||||
|
||||
from icalendar.cal import (
|
||||
Calendar,
|
||||
|
|
|
@ -113,7 +113,8 @@ class Component(CaselessDict):
|
|||
#############################
|
||||
# handling of property values
|
||||
|
||||
def _encode(self, name, value, parameters=None, encode=1):
|
||||
@staticmethod
|
||||
def _encode(name, value, parameters=None, encode=1):
|
||||
"""Encode values to icalendar property values.
|
||||
|
||||
:param name: Name of the property.
|
||||
|
@ -138,17 +139,19 @@ class Component(CaselessDict):
|
|||
return value
|
||||
if isinstance(value, types_factory.all_types):
|
||||
# Don't encode already encoded values.
|
||||
return value
|
||||
klass = types_factory.for_property(name)
|
||||
obj = klass(value)
|
||||
obj = value
|
||||
else:
|
||||
klass = types_factory.for_property(name)
|
||||
obj = klass(value)
|
||||
if parameters:
|
||||
if isinstance(parameters, dict):
|
||||
params = Parameters()
|
||||
for key, item in parameters.items():
|
||||
params[key] = item
|
||||
parameters = params
|
||||
assert isinstance(parameters, Parameters)
|
||||
obj.params = parameters
|
||||
if not hasattr(obj, "params"):
|
||||
obj.params = Parameters()
|
||||
for key, item in parameters.items():
|
||||
if item is None:
|
||||
if key in obj.params:
|
||||
del obj.params[key]
|
||||
else:
|
||||
obj.params[key] = item
|
||||
return obj
|
||||
|
||||
def add(self, name, value, parameters=None, encode=1):
|
||||
|
@ -372,19 +375,26 @@ class Component(CaselessDict):
|
|||
if not component:
|
||||
raise ValueError(f'Property "{name}" does not have a parent component.')
|
||||
datetime_names = ('DTSTART', 'DTEND', 'RECURRENCE-ID', 'DUE',
|
||||
'FREEBUSY', 'RDATE', 'EXDATE')
|
||||
'RDATE', 'EXDATE')
|
||||
try:
|
||||
if name in datetime_names and 'TZID' in params:
|
||||
vals = factory(factory.from_ical(vals, params['TZID']))
|
||||
if name == 'FREEBUSY':
|
||||
vals = vals.split(',')
|
||||
if 'TZID' in params:
|
||||
parsed_components = [factory(factory.from_ical(val, params['TZID'])) for val in vals]
|
||||
else:
|
||||
parsed_components = [factory(factory.from_ical(val)) for val in vals]
|
||||
elif name in datetime_names and 'TZID' in params:
|
||||
parsed_components = [factory(factory.from_ical(vals, params['TZID']))]
|
||||
else:
|
||||
vals = factory(factory.from_ical(vals))
|
||||
parsed_components = [factory(factory.from_ical(vals))]
|
||||
except ValueError as e:
|
||||
if not component.ignore_exceptions:
|
||||
raise
|
||||
component.errors.append((uname, str(e)))
|
||||
else:
|
||||
vals.params = params
|
||||
component.add(name, vals, encode=0)
|
||||
for parsed_component in parsed_components:
|
||||
parsed_component.params = params
|
||||
component.add(name, parsed_component, encode=0)
|
||||
|
||||
if multiple:
|
||||
return comps
|
||||
|
@ -436,6 +446,25 @@ class Component(CaselessDict):
|
|||
subs = ', '.join(str(it) for it in self.subcomponents)
|
||||
return f"{self.name or type(self).__name__}({dict(self)}{', ' + subs if subs else ''})"
|
||||
|
||||
def __eq__(self, other):
|
||||
if not len(self.subcomponents) == len(other.subcomponents):
|
||||
return False
|
||||
|
||||
properties_equal = super().__eq__(other)
|
||||
if not properties_equal:
|
||||
return False
|
||||
|
||||
# The subcomponents might not be in the same order,
|
||||
# neither there's a natural key we can sort the subcomponents by nor
|
||||
# are the subcomponent types hashable, so we cant put them in a set to
|
||||
# check for set equivalence. We have to iterate over the subcomponents
|
||||
# and look for each of them in the list.
|
||||
for subcomponent in self.subcomponents:
|
||||
if subcomponent not in other.subcomponents:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
#######################################
|
||||
# components defined in RFC 5545
|
||||
|
|
|
@ -52,10 +52,10 @@ def view(event):
|
|||
end = event.decoded('dtend', default=start)
|
||||
duration = event.decoded('duration', default=end - start)
|
||||
if isinstance(start, datetime):
|
||||
start = start.astimezone(start.tzinfo)
|
||||
start = start.astimezone()
|
||||
start = start.strftime('%c')
|
||||
if isinstance(end, datetime):
|
||||
end = end.astimezone(end.tzinfo)
|
||||
end = end.astimezone()
|
||||
end = end.strftime('%c')
|
||||
|
||||
return f""" Organizer: {organizer}
|
||||
|
|
|
@ -331,7 +331,7 @@ class vDDDTypes:
|
|||
if u.startswith(('P', '-P', '+P')):
|
||||
return vDuration.from_ical(ical)
|
||||
if '/' in u:
|
||||
return vPeriod.from_ical(ical)
|
||||
return vPeriod.from_ical(ical, timezone=timezone)
|
||||
|
||||
if len(ical) in (15, 16):
|
||||
return vDatetime.from_ical(ical, timezone=timezone)
|
||||
|
@ -533,6 +533,11 @@ class vPeriod:
|
|||
f'Cannot compare vPeriod with {other!r}')
|
||||
return cmp((self.start, self.end), (other.start, other.end))
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, vPeriod):
|
||||
return False
|
||||
return (self.start, self.end) == (other.start, other.end)
|
||||
|
||||
def overlaps(self, other):
|
||||
if self.start > other.start:
|
||||
return other.overlaps(self)
|
||||
|
@ -548,11 +553,11 @@ class vPeriod:
|
|||
+ vDatetime(self.end).to_ical())
|
||||
|
||||
@staticmethod
|
||||
def from_ical(ical):
|
||||
def from_ical(ical, timezone=None):
|
||||
try:
|
||||
start, end_or_duration = ical.split('/')
|
||||
start = vDDDTypes.from_ical(start)
|
||||
end_or_duration = vDDDTypes.from_ical(end_or_duration)
|
||||
start = vDDDTypes.from_ical(start, timezone=timezone)
|
||||
end_or_duration = vDDDTypes.from_ical(end_or_duration, timezone=timezone)
|
||||
return (start, end_or_duration)
|
||||
except Exception:
|
||||
raise ValueError(f'Expected period format, got: {ical}')
|
||||
|
@ -719,7 +724,7 @@ class vText(str):
|
|||
return self
|
||||
|
||||
def __repr__(self):
|
||||
return f"vText('{self.to_ical()}')"
|
||||
return f"vText('{self.to_ical()!r}')"
|
||||
|
||||
def to_ical(self):
|
||||
return escape_char(self).encode(self.encoding)
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:collective/icalendar
|
||||
CALSCALE:GREGORIAN
|
||||
METHOD:PUBLISH
|
||||
X-WR-CALNAME:Holidays
|
||||
X-WR-TIMEZONE:Etc/GMT
|
||||
BEGIN:VEVENT
|
||||
SUMMARY:New Year's Day
|
||||
DTSTART:20220101
|
||||
DTEND:20220101
|
||||
DESCRIPTION:Happy New Year!
|
||||
UID:636a0cc1dbd5a1667894465@icalendar
|
||||
DTSTAMP:20221108T080105Z
|
||||
STATUS:CONFIRMED
|
||||
TRANSP:TRANSPARENT
|
||||
SEQUENCE:0
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
SUMMARY:Orthodox Christmas
|
||||
DTSTART:20220107
|
||||
DTEND:20220107
|
||||
LOCATION:Russia
|
||||
DESCRIPTION:It is Christmas again!
|
||||
UID:636a0cc1dbfd91667894465@icalendar
|
||||
STATUS:CONFIRMED
|
||||
TRANSP:TRANSPARENT
|
||||
SEQUENCE:0
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
SUMMARY:International Women's Day
|
||||
DTSTART:20220308
|
||||
DTEND:20220308
|
||||
DESCRIPTION:May the feminine be honoured!
|
||||
UID:636a0cc1dc0f11667894465@icalendar
|
||||
STATUS:CONFIRMED
|
||||
TRANSP:TRANSPARENT
|
||||
SEQUENCE:0
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,28 @@
|
|||
BEGIN:VCALENDAR
|
||||
METHOD:REQUEST
|
||||
PRODID:Microsoft CDO for Microsoft Exchange
|
||||
VERSION:2.0
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:GMT +0100 (Standard) / GMT +0200 (Daylight)
|
||||
BEGIN:STANDARD
|
||||
DTSTART:16010101T030000
|
||||
TZOFFSETFROM:+0200
|
||||
TZOFFSETTO:+0100
|
||||
RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=10;BYDAY=-1SU
|
||||
END:STANDARD
|
||||
BEGIN:DAYLIGHT
|
||||
DTSTART:16010101T020000
|
||||
TZOFFSETFROM:+0100
|
||||
TZOFFSETTO:+0200
|
||||
RRULE:FREQ=YEARLY;WKST=MO;INTERVAL=1;BYMONTH=3;BYDAY=-1SU
|
||||
END:DAYLIGHT
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
DTSTAMP:20150703T071009Z
|
||||
DTSTART;TZID="GMT +0100 (Standard) / GMT +0200 (Daylight)":20150703T100000
|
||||
SUMMARY:Sprint 25 Daily Standup
|
||||
DTEND;TZID="GMT +0100 (Standard) / GMT +0200 (Daylight)":20150703T103000
|
||||
RRULE:FREQ=DAILY;UNTIL=20150722T080000Z;INTERVAL=1;BYDAY=MO, TU, WE, TH, FR
|
||||
;WKST=SU
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,21 @@
|
|||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:-//davmail.sf.net/NONSGML DavMail Calendar V1.1//EN
|
||||
METHOD:REPLY
|
||||
BEGIN:VFREEBUSY
|
||||
DTSTAMP:20120131T123000Z
|
||||
ORGANIZER:MAILTO:organizer@domain.tld
|
||||
DTSTART:20120101T000000Z
|
||||
DTEND:20120201T000000Z
|
||||
UID:null
|
||||
ATTENDEE:MAILTO:attendee@domain.tld
|
||||
FREEBUSY;FBTYPE=BUSY:20120103T091500Z/20120103T101500Z
|
||||
FREEBUSY;FBTYPE=BUSY:20120113T130000Z/20120113T150000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20120116T130000Z/20120116T150000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20120117T091500Z/20120117T101500Z
|
||||
FREEBUSY;FBTYPE=BUSY:20120118T160000Z/20120118T163000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20120124T083000Z/20120124T093000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20120124T123000Z/20120124T143000Z
|
||||
FREEBUSY;FBTYPE=BUSY:20120131T091500Z/20120131T101500Z
|
||||
END:VFREEBUSY
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,18 @@
|
|||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:icalendar-2023
|
||||
BEGIN:VEVENT
|
||||
UID:ical-jacadzaca-3
|
||||
SUMMARY: Some very different event ':'
|
||||
DTSTART;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T160000
|
||||
DTEND;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T163000
|
||||
DTSTAMP:20211004T150245Z
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:ical-jacadzaca-4
|
||||
SUMMARY: Some very different other event
|
||||
DTSTART;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T164000
|
||||
DTEND;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T165000
|
||||
DTSTAMP:20211004T150245Z
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,11 @@
|
|||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:icalendar-2023
|
||||
BEGIN:VEVENT
|
||||
UID:1
|
||||
SUMMARY: Some event ':'
|
||||
DTSTART;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T160000
|
||||
DTEND;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T163000
|
||||
DTSTAMP:20211004T150245Z
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,18 @@
|
|||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:icalendar-2023
|
||||
BEGIN:VEVENT
|
||||
UID:1
|
||||
SUMMARY: Some event ':'
|
||||
DTSTART;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T160000
|
||||
DTEND;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T163000
|
||||
DTSTAMP:20211004T150245Z
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:2
|
||||
SUMMARY: Some other event
|
||||
DTSTART;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T164000
|
||||
DTEND;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T165000
|
||||
DTSTAMP:20211004T150245Z
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -0,0 +1,18 @@
|
|||
BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
PRODID:icalendar-2023
|
||||
BEGIN:VEVENT
|
||||
UID:2
|
||||
SUMMARY: Some other event
|
||||
DTSTART;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T164000
|
||||
DTEND;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T165000
|
||||
DTSTAMP:20211004T150245Z
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
UID:1
|
||||
SUMMARY: Some event ':'
|
||||
DTSTART;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T160000
|
||||
DTEND;TZID="Western/Central Europe";VALUE=DATE-TIME:20211101T163000
|
||||
DTSTAMP:20211004T150245Z
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
|
@ -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
|
|
@ -1,6 +1,11 @@
|
|||
import unittest
|
||||
|
||||
from datetime import tzinfo, datetime
|
||||
from icalendar import Calendar, cli
|
||||
try:
|
||||
import zoneinfo
|
||||
except ModuleNotFoundError:
|
||||
from backports import zoneinfo
|
||||
|
||||
INPUT = '''
|
||||
BEGIN:VCALENDAR
|
||||
|
@ -21,7 +26,7 @@ BEGIN:VEVENT
|
|||
ORGANIZER:organizer@test.test
|
||||
ATTENDEE:attendee1@example.com
|
||||
ATTENDEE:attendee2@test.test
|
||||
SUMMARY:Test summury
|
||||
SUMMARY:Test summary
|
||||
DTSTART;TZID=Europe/Warsaw:20220820T200000
|
||||
DTEND;TZID=Europe/Warsaw:20220820T203000
|
||||
LOCATION:New Amsterdam, 1010 Test Street
|
||||
|
@ -36,13 +41,22 @@ END:VEVENT
|
|||
END:VCALENDAR
|
||||
'''
|
||||
|
||||
PROPER_OUTPUT = ''' Organizer: organizer <organizer@test.test>
|
||||
def local_datetime(dt):
|
||||
return datetime.strptime(dt, "%Y%m%dT%H%M%S").replace(tzinfo=zoneinfo.ZoneInfo("Europe/Warsaw")).astimezone().strftime('%c')
|
||||
|
||||
# datetimes are displayed in the local timezone, so we cannot just hardcode them
|
||||
firststart = local_datetime('20220820T103400')
|
||||
firstend = local_datetime('20220820T113400')
|
||||
secondstart = local_datetime('20220820T200000')
|
||||
secondend = local_datetime('20220820T203000')
|
||||
|
||||
PROPER_OUTPUT = f""" Organizer: organizer <organizer@test.test>
|
||||
Attendees:
|
||||
attendee1 <attendee1@example.com>
|
||||
attendee2 <attendee2@test.test>
|
||||
Summary : Test Summary
|
||||
Starts : Sat Aug 20 10:34:00 2022
|
||||
End : Sat Aug 20 11:34:00 2022
|
||||
Starts : {firststart}
|
||||
End : {firstend}
|
||||
Duration : 1:00:00
|
||||
Location : New Amsterdam, 1000 Sunrise Test Street
|
||||
Comment : Comment
|
||||
|
@ -53,9 +67,9 @@ PROPER_OUTPUT = ''' Organizer: organizer <organizer@test.test>
|
|||
Attendees:
|
||||
attendee1 <attendee1@example.com>
|
||||
attendee2 <attendee2@test.test>
|
||||
Summary : Test summury
|
||||
Starts : Sat Aug 20 20:00:00 2022
|
||||
End : Sat Aug 20 20:30:00 2022
|
||||
Summary : Test summary
|
||||
Starts : {secondstart}
|
||||
End : {secondend}
|
||||
Duration : 0:30:00
|
||||
Location : New Amsterdam, 1010 Test Street
|
||||
Comment :
|
||||
|
@ -75,7 +89,7 @@ PROPER_OUTPUT = ''' Organizer: organizer <organizer@test.test>
|
|||
Description:
|
||||
|
||||
|
||||
'''
|
||||
"""
|
||||
|
||||
class CLIToolTest(unittest.TestCase):
|
||||
def test_output_is_proper(self):
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
'''Issue #165 - Problem parsing a file with event recurring on weekdays
|
||||
|
||||
https://github.com/collective/icalendar/issues/165
|
||||
'''
|
||||
from icalendar import Calendar
|
||||
|
||||
def test_issue_165_missing_event(calendars):
|
||||
events = list(calendars.issue_165_missing_event.walk('VEVENT'))
|
||||
assert len(events) == 1, "There was an event missing from the parsed events' list."
|
|
@ -5,7 +5,10 @@
|
|||
from icalendar import Calendar
|
||||
|
||||
def test_issue_27_multiple_periods(calendars):
|
||||
free_busy = list(calendars.issue_27_multiple_periods.walk('VFREEBUSY'))
|
||||
assert len(free_busy) == 1
|
||||
|
||||
|
||||
free_busy = list(calendars.issue_27_multiple_periods_in_freebusy_multiple_freebusies.walk('VFREEBUSY'))[0]
|
||||
free_busy_period = free_busy['freebusy']
|
||||
print(free_busy['freebusy'])
|
||||
equivalent_way_of_defining_free_busy = list(calendars.issue_27_multiple_periods_in_freebusy_one_freebusy.walk('VFREEBUSY'))[0]
|
||||
free_busy_period_equivalent = equivalent_way_of_defining_free_busy['freebusy']
|
||||
assert free_busy_period == free_busy_period_equivalent
|
||||
|
||||
|
|
|
@ -0,0 +1,152 @@
|
|||
"""These are tests for Issue #557
|
||||
|
||||
TL;DR: Component._encode lost given parameters
|
||||
if the object to encode was already of native type,
|
||||
making its behavior unexpected.
|
||||
|
||||
see https://github.com/collective/icalendar/issues/557"""
|
||||
|
||||
|
||||
import unittest
|
||||
|
||||
from icalendar.cal import Component
|
||||
|
||||
|
||||
class TestComponentEncode(unittest.TestCase):
|
||||
def test_encode_non_native_parameters(self):
|
||||
"""Test _encode to add parameters to non-natives"""
|
||||
self.__assert_native_content(self.summary)
|
||||
self.__assert_native_kept_parameters(self.summary)
|
||||
|
||||
def test_encode_native_keep_params_None(self):
|
||||
"""_encode should keep parameters on natives
|
||||
if parameters=None
|
||||
"""
|
||||
new_sum = self.__add_params(
|
||||
self.summary,
|
||||
parameters=None,
|
||||
)
|
||||
self.__assert_native_content(new_sum)
|
||||
self.__assert_native_kept_parameters(new_sum)
|
||||
|
||||
def test_encode_native_keep_params_empty(self):
|
||||
"""_encode should keep paramters on natives
|
||||
if parameters={}
|
||||
"""
|
||||
new_sum = self.__add_params(
|
||||
self.summary,
|
||||
parameters={},
|
||||
)
|
||||
self.__assert_native_content(new_sum)
|
||||
self.__assert_native_kept_parameters(new_sum)
|
||||
|
||||
def test_encode_native_append_params(self):
|
||||
"""_encode should append paramters on natives
|
||||
keeping old parameters
|
||||
"""
|
||||
new_sum = self.__add_params(
|
||||
self.summary,
|
||||
parameters={"X-PARAM": "Test123"},
|
||||
)
|
||||
self.__assert_native_content(new_sum)
|
||||
self.__assert_native_kept_parameters(new_sum)
|
||||
self.assertParameter(new_sum, "X-PARAM", "Test123")
|
||||
|
||||
def test_encode_native_overwrite_params(self):
|
||||
"""_encode should overwrite single parameters
|
||||
if they have the same name as old ones"""
|
||||
new_sum = self.__add_params(
|
||||
self.summary,
|
||||
parameters={"LANGUAGE": "de"},
|
||||
)
|
||||
self.__assert_native_content(new_sum)
|
||||
self.assertParameter(new_sum, "LANGUAGE", "de")
|
||||
|
||||
def test_encode_native_remove_params(self):
|
||||
"""_encode should remove single parameters
|
||||
if they are explicitly set to None"""
|
||||
new_sum = self.__add_params(
|
||||
self.summary,
|
||||
parameters={"LANGUAGE": None},
|
||||
)
|
||||
self.__assert_native_content(new_sum)
|
||||
self.assertParameterMissing(new_sum, "LANGUAGE")
|
||||
|
||||
def test_encode_native_remove_already_missing(self):
|
||||
"""_encode should ignore removing a parameter
|
||||
that was already missing"""
|
||||
self.assertParameterMissing(self.summary, "X-MISSING")
|
||||
new_sum = self.__add_params(
|
||||
self.summary,
|
||||
parameters={"X-MISSING": None},
|
||||
)
|
||||
self.__assert_native_content(new_sum)
|
||||
self.__assert_native_kept_parameters(new_sum)
|
||||
self.assertParameterMissing(self.summary, "X-MISSING")
|
||||
|
||||
def test_encode_native_full_test(self):
|
||||
"""full test case with keeping, overwriting & removing properties"""
|
||||
# preperation
|
||||
orig_sum = self.__add_params(
|
||||
self.summary,
|
||||
parameters={
|
||||
"X-OVERWRITE": "overwrite me!",
|
||||
"X-REMOVE": "remove me!",
|
||||
"X-MISSING": None,
|
||||
},
|
||||
)
|
||||
# preperation check
|
||||
self.__assert_native_content(orig_sum)
|
||||
self.__assert_native_kept_parameters(orig_sum)
|
||||
self.assertParameter(orig_sum, "X-OVERWRITE", "overwrite me!")
|
||||
self.assertParameter(orig_sum, "X-REMOVE", "remove me!")
|
||||
self.assertParameterMissing(orig_sum, "X-MISSING")
|
||||
# modification
|
||||
new_sum = self.__add_params(
|
||||
orig_sum,
|
||||
parameters={
|
||||
"X-OVERWRITE": "overwritten",
|
||||
"X-REMOVE": None,
|
||||
"X-MISSING": None,
|
||||
},
|
||||
)
|
||||
# final asserts
|
||||
self.__assert_native_content(new_sum)
|
||||
self.__assert_native_kept_parameters(new_sum)
|
||||
self.assertParameter(new_sum, "X-OVERWRITE", "overwritten")
|
||||
self.assertParameterMissing(new_sum, "X-REMOVE")
|
||||
self.assertParameterMissing(new_sum, "X-MISSING")
|
||||
|
||||
def setUp(self):
|
||||
self.summary = self.__gen_native()
|
||||
|
||||
def __assert_native_kept_parameters(self, obj):
|
||||
self.assertParameter(obj, "LANGUAGE", "en")
|
||||
|
||||
def __assert_native_content(self, obj):
|
||||
self.assertEqual(obj, "English Summary")
|
||||
|
||||
def __add_params(self, obj, parameters):
|
||||
return Component._encode(
|
||||
"SUMMARY",
|
||||
obj,
|
||||
parameters=parameters,
|
||||
encode=True,
|
||||
)
|
||||
|
||||
def __gen_native(self):
|
||||
return Component._encode(
|
||||
"SUMMARY",
|
||||
"English Summary",
|
||||
parameters={
|
||||
"LANGUAGE": "en",
|
||||
},
|
||||
encode=True,
|
||||
)
|
||||
|
||||
def assertParameterMissing(self, obj, name):
|
||||
self.assertNotIn(name, obj.params)
|
||||
|
||||
def assertParameter(self, obj, name, val):
|
||||
self.assertIn(name, obj.params)
|
||||
self.assertEqual(obj.params[name], val)
|
|
@ -7,6 +7,7 @@ See
|
|||
import pytest
|
||||
import pytz
|
||||
from icalendar.prop import vDDDTypes
|
||||
import datetime
|
||||
|
||||
|
||||
@pytest.mark.parametrize("calname,tzname,index,period_string", [
|
||||
|
@ -31,7 +32,7 @@ def test_issue_156_period_list_in_rdate(calendars, calname, tzname, index, perio
|
|||
calendar = calendars[calname]
|
||||
rdate = calendar.walk("vevent")[0]["rdate"]
|
||||
period = rdate.dts[index]
|
||||
assert period.dt == vDDDTypes.from_ical(period_string, timezone=pytz.timezone(tzname))
|
||||
assert period.dt == vDDDTypes.from_ical(period_string, timezone=tzname)
|
||||
|
||||
|
||||
def test_duration_properly_parsed(events):
|
||||
|
@ -46,3 +47,17 @@ def test_duration_properly_parsed(events):
|
|||
assert period[1].days == 0
|
||||
assert period[1].seconds == (5 * 60 + 30) * 60
|
||||
assert period[1] == duration
|
||||
|
||||
|
||||
def test_tzid_is_part_of_the_parameters(calendars):
|
||||
"""The TZID should be mentioned in the parameters."""
|
||||
event = list(calendars.period_with_timezone.walk("VEVENT"))[0]
|
||||
assert event["RDATE"].params["TZID"] == "America/Vancouver"
|
||||
|
||||
|
||||
def test_tzid_is_part_of_the_period_values(calendars):
|
||||
"""The TZID should be set in the datetime."""
|
||||
event = list(calendars.period_with_timezone.walk("VEVENT"))[0]
|
||||
start, end = event["RDATE"].dts[0].dt
|
||||
assert start == pytz.timezone("America/Vancouver").localize(datetime.datetime(2023, 12, 13, 12))
|
||||
assert end == pytz.timezone("America/Vancouver").localize(datetime.datetime(2023, 12, 13, 15))
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import itertools
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
import unittest
|
||||
|
||||
import pytest
|
||||
|
||||
import icalendar
|
||||
import pytz
|
||||
import re
|
||||
|
@ -455,3 +458,33 @@ class TestCal(unittest.TestCase):
|
|||
icalendar.vUTCOffset.ignore_exceptions = True
|
||||
self.assertEqual(icalendar.Calendar.from_ical(cal_str).to_ical(), cal_str)
|
||||
icalendar.vUTCOffset.ignore_exceptions = False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'calendar, other_calendar',
|
||||
itertools.product([
|
||||
'issue_156_RDATE_with_PERIOD_TZID_khal',
|
||||
'issue_156_RDATE_with_PERIOD_TZID_khal_2',
|
||||
'issue_178_custom_component_contains_other',
|
||||
'issue_178_custom_component_inside_other',
|
||||
'issue_526_calendar_with_events',
|
||||
'issue_526_calendar_with_different_events',
|
||||
'issue_526_calendar_with_event_subset',
|
||||
], repeat=2)
|
||||
)
|
||||
def test_comparing_calendars(calendars, calendar, other_calendar):
|
||||
are_calendars_equal = calendars[calendar] == calendars[other_calendar]
|
||||
are_calendars_actually_equal = calendar == other_calendar
|
||||
assert are_calendars_equal == are_calendars_actually_equal
|
||||
|
||||
|
||||
@pytest.mark.parametrize('calendar, shuffeled_calendar', [
|
||||
(
|
||||
'issue_526_calendar_with_events',
|
||||
'issue_526_calendar_with_shuffeled_events',
|
||||
),
|
||||
])
|
||||
def test_calendars_with_same_subcomponents_in_different_order_are_equal(calendars, calendar, shuffeled_calendar):
|
||||
assert not calendars[calendar].subcomponents == calendars[shuffeled_calendar].subcomponents
|
||||
assert calendars[calendar] == calendars[shuffeled_calendar]
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue