Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/icalendar/prop.py: 84%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""This module contains the parser/generators (or coders/encoders if you
2prefer) for the classes/datatypes that are used in iCalendar:
4###########################################################################
6# This module defines these property value data types and property parameters
84.2 Defined property parameters are:
10.. code-block:: text
12 ALTREP, CN, CUTYPE, DELEGATED-FROM, DELEGATED-TO, DIR, ENCODING, FMTTYPE,
13 FBTYPE, LANGUAGE, MEMBER, PARTSTAT, RANGE, RELATED, RELTYPE, ROLE, RSVP,
14 SENT-BY, TZID, VALUE
164.3 Defined value data types are:
18.. code-block:: text
20 BINARY, BOOLEAN, CAL-ADDRESS, DATE, DATE-TIME, DURATION, FLOAT, INTEGER,
21 PERIOD, RECUR, TEXT, TIME, URI, UTC-OFFSET
23###########################################################################
25iCalendar properties have values. The values are strongly typed. This module
26defines these types, calling val.to_ical() on them will render them as defined
27in rfc5545.
29If you pass any of these classes a Python primitive, you will have an object
30that can render itself as iCalendar formatted date.
32Property Value Data Types start with a 'v'. they all have an to_ical() and
33from_ical() method. The to_ical() method generates a text string in the
34iCalendar format. The from_ical() method can parse this format and return a
35primitive Python datatype. So it should always be true that:
37.. code-block:: python
39 x == vDataType.from_ical(VDataType(x).to_ical())
41These types are mainly used for parsing and file generation. But you can set
42them directly.
43"""
45from __future__ import annotations
47import base64
48import binascii
49import re
50from datetime import date, datetime, time, timedelta
51from typing import Any, Optional, Union
53from icalendar.caselessdict import CaselessDict
54from icalendar.enums import VALUE, Enum
55from icalendar.parser import Parameters, escape_char, unescape_char
56from icalendar.parser_tools import (
57 DEFAULT_ENCODING,
58 ICAL_TYPE,
59 SEQUENCE_TYPES,
60 from_unicode,
61 to_unicode,
62)
63from icalendar.tools import to_datetime
65from .timezone import tzid_from_dt, tzid_from_tzinfo, tzp
67DURATION_REGEX = re.compile(
68 r"([-+]?)P(?:(\d+)W)?(?:(\d+)D)?" r"(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$"
69)
71WEEKDAY_RULE = re.compile(
72 r"(?P<signal>[+-]?)(?P<relative>[\d]{0,2})" r"(?P<weekday>[\w]{2})$"
73)
76class vBinary:
77 """Binary property values are base 64 encoded."""
79 params: Parameters
81 def __init__(self, obj):
82 self.obj = to_unicode(obj)
83 self.params = Parameters(encoding="BASE64", value="BINARY")
85 def __repr__(self):
86 return f"vBinary({self.to_ical()})"
88 def to_ical(self):
89 return binascii.b2a_base64(self.obj.encode("utf-8"))[:-1]
91 @staticmethod
92 def from_ical(ical):
93 try:
94 return base64.b64decode(ical)
95 except ValueError as e:
96 raise ValueError("Not valid base 64 encoding.") from e
98 def __eq__(self, other):
99 """self == other"""
100 return isinstance(other, vBinary) and self.obj == other.obj
103class vBoolean(int):
104 """Boolean
106 Value Name: BOOLEAN
108 Purpose: This value type is used to identify properties that contain
109 either a "TRUE" or "FALSE" Boolean value.
111 Format Definition: This value type is defined by the following
112 notation:
114 .. code-block:: text
116 boolean = "TRUE" / "FALSE"
118 Description: These values are case-insensitive text. No additional
119 content value encoding is defined for this value type.
121 Example: The following is an example of a hypothetical property that
122 has a BOOLEAN value type:
124 .. code-block:: python
126 TRUE
128 .. code-block:: pycon
130 >>> from icalendar.prop import vBoolean
131 >>> boolean = vBoolean.from_ical('TRUE')
132 >>> boolean
133 True
134 >>> boolean = vBoolean.from_ical('FALSE')
135 >>> boolean
136 False
137 >>> boolean = vBoolean.from_ical('True')
138 >>> boolean
139 True
140 """
142 params: Parameters
144 BOOL_MAP = CaselessDict({"true": True, "false": False})
146 def __new__(cls, *args, params: Optional[dict[str, Any]] = None, **kwargs):
147 if params is None:
148 params = {}
149 self = super().__new__(cls, *args, **kwargs)
150 self.params = Parameters(params)
151 return self
153 def to_ical(self):
154 return b"TRUE" if self else b"FALSE"
156 @classmethod
157 def from_ical(cls, ical):
158 try:
159 return cls.BOOL_MAP[ical]
160 except Exception as e:
161 raise ValueError(f"Expected 'TRUE' or 'FALSE'. Got {ical}") from e
164class vText(str):
165 """Simple text."""
167 params: Parameters
168 __slots__ = ("encoding", "params")
170 def __new__(
171 cls,
172 value,
173 encoding=DEFAULT_ENCODING,
174 /,
175 params: Optional[dict[str, Any]] = None,
176 ):
177 if params is None:
178 params = {}
179 value = to_unicode(value, encoding=encoding)
180 self = super().__new__(cls, value)
181 self.encoding = encoding
182 self.params = Parameters(params)
183 return self
185 def __repr__(self) -> str:
186 return f"vText({self.to_ical()!r})"
188 def to_ical(self) -> bytes:
189 return escape_char(self).encode(self.encoding)
191 @classmethod
192 def from_ical(cls, ical: ICAL_TYPE):
193 ical_unesc = unescape_char(ical)
194 return cls(ical_unesc)
196 from icalendar.param import ALTREP, LANGUAGE, RELTYPE
199class vCalAddress(str):
200 """Calendar User Address
202 Value Name:
203 CAL-ADDRESS
205 Purpose:
206 This value type is used to identify properties that contain a
207 calendar user address.
209 Description:
210 The value is a URI as defined by [RFC3986] or any other
211 IANA-registered form for a URI. When used to address an Internet
212 email transport address for a calendar user, the value MUST be a
213 mailto URI, as defined by [RFC2368].
215 Example:
216 ``mailto:`` is in front of the address.
218 .. code-block:: text
220 mailto:jane_doe@example.com
222 Parsing:
224 .. code-block:: pycon
226 >>> from icalendar import vCalAddress
227 >>> cal_address = vCalAddress.from_ical('mailto:jane_doe@example.com')
228 >>> cal_address
229 vCalAddress('mailto:jane_doe@example.com')
231 Encoding:
233 .. code-block:: pycon
235 >>> from icalendar import vCalAddress, Event
236 >>> event = Event()
237 >>> jane = vCalAddress("mailto:jane_doe@example.com")
238 >>> jane.name = "Jane"
239 >>> event["organizer"] = jane
240 >>> print(event.to_ical())
241 BEGIN:VEVENT
242 ORGANIZER;CN=Jane:mailto:jane_doe@example.com
243 END:VEVENT
244 """
246 params: Parameters
247 __slots__ = ("params",)
249 def __new__(
250 cls,
251 value,
252 encoding=DEFAULT_ENCODING,
253 /,
254 params: Optional[dict[str, Any]] = None,
255 ):
256 if params is None:
257 params = {}
258 value = to_unicode(value, encoding=encoding)
259 self = super().__new__(cls, value)
260 self.params = Parameters(params)
261 return self
263 def __repr__(self):
264 return f"vCalAddress('{self}')"
266 def to_ical(self):
267 return self.encode(DEFAULT_ENCODING)
269 @classmethod
270 def from_ical(cls, ical):
271 return cls(ical)
273 @property
274 def email(self) -> str:
275 """The email address without mailto: at the start."""
276 if self.lower().startswith("mailto:"):
277 return self[7:]
278 return str(self)
280 from icalendar.param import (
281 CN,
282 CUTYPE,
283 DELEGATED_FROM,
284 DELEGATED_TO,
285 DIR,
286 LANGUAGE,
287 PARTSTAT,
288 ROLE,
289 RSVP,
290 SENT_BY,
291 )
293 name = CN
296class vFloat(float):
297 """Float
299 Value Name:
300 FLOAT
302 Purpose:
303 This value type is used to identify properties that contain
304 a real-number value.
306 Format Definition:
307 This value type is defined by the following notation:
309 .. code-block:: text
311 float = (["+"] / "-") 1*DIGIT ["." 1*DIGIT]
313 Description:
314 If the property permits, multiple "float" values are
315 specified by a COMMA-separated list of values.
317 Example:
319 .. code-block:: text
321 1000000.0000001
322 1.333
323 -3.14
325 .. code-block:: pycon
327 >>> from icalendar.prop import vFloat
328 >>> float = vFloat.from_ical('1000000.0000001')
329 >>> float
330 1000000.0000001
331 >>> float = vFloat.from_ical('1.333')
332 >>> float
333 1.333
334 >>> float = vFloat.from_ical('+1.333')
335 >>> float
336 1.333
337 >>> float = vFloat.from_ical('-3.14')
338 >>> float
339 -3.14
340 """
342 params: Parameters
344 def __new__(cls, *args, params: Optional[dict[str, Any]] = None, **kwargs):
345 if params is None:
346 params = {}
347 self = super().__new__(cls, *args, **kwargs)
348 self.params = Parameters(params)
349 return self
351 def to_ical(self):
352 return str(self).encode("utf-8")
354 @classmethod
355 def from_ical(cls, ical):
356 try:
357 return cls(ical)
358 except Exception as e:
359 raise ValueError(f"Expected float value, got: {ical}") from e
362class vInt(int):
363 """Integer
365 Value Name:
366 INTEGER
368 Purpose:
369 This value type is used to identify properties that contain a
370 signed integer value.
372 Format Definition:
373 This value type is defined by the following notation:
375 .. code-block:: text
377 integer = (["+"] / "-") 1*DIGIT
379 Description:
380 If the property permits, multiple "integer" values are
381 specified by a COMMA-separated list of values. The valid range
382 for "integer" is -2147483648 to 2147483647. If the sign is not
383 specified, then the value is assumed to be positive.
385 Example:
387 .. code-block:: text
389 1234567890
390 -1234567890
391 +1234567890
392 432109876
394 .. code-block:: pycon
396 >>> from icalendar.prop import vInt
397 >>> integer = vInt.from_ical('1234567890')
398 >>> integer
399 1234567890
400 >>> integer = vInt.from_ical('-1234567890')
401 >>> integer
402 -1234567890
403 >>> integer = vInt.from_ical('+1234567890')
404 >>> integer
405 1234567890
406 >>> integer = vInt.from_ical('432109876')
407 >>> integer
408 432109876
409 """
411 params: Parameters
413 def __new__(cls, *args, params: Optional[dict[str, Any]] = None, **kwargs):
414 if params is None:
415 params = {}
416 self = super().__new__(cls, *args, **kwargs)
417 self.params = Parameters(params)
418 return self
420 def to_ical(self) -> bytes:
421 return str(self).encode("utf-8")
423 @classmethod
424 def from_ical(cls, ical: ICAL_TYPE):
425 try:
426 return cls(ical)
427 except Exception as e:
428 raise ValueError(f"Expected int, got: {ical}") from e
431class vDDDLists:
432 """A list of vDDDTypes values."""
434 params: Parameters
435 dts: list
437 def __init__(self, dt_list):
438 if not hasattr(dt_list, "__iter__"):
439 dt_list = [dt_list]
440 vddd = []
441 tzid = None
442 for dt_l in dt_list:
443 dt = vDDDTypes(dt_l)
444 vddd.append(dt)
445 if "TZID" in dt.params:
446 tzid = dt.params["TZID"]
448 params = {}
449 if tzid:
450 # NOTE: no support for multiple timezones here!
451 params["TZID"] = tzid
452 self.params = Parameters(params)
453 self.dts = vddd
455 def to_ical(self):
456 dts_ical = (from_unicode(dt.to_ical()) for dt in self.dts)
457 return b",".join(dts_ical)
459 @staticmethod
460 def from_ical(ical, timezone=None):
461 out = []
462 ical_dates = ical.split(",")
463 for ical_dt in ical_dates:
464 out.append(vDDDTypes.from_ical(ical_dt, timezone=timezone))
465 return out
467 def __eq__(self, other):
468 if isinstance(other, vDDDLists):
469 return self.dts == other.dts
470 if isinstance(other, (TimeBase, date)):
471 return self.dts == [other]
472 return False
474 def __repr__(self):
475 """String representation."""
476 return f"{self.__class__.__name__}({self.dts})"
479class vCategory:
480 params: Parameters
482 def __init__(
483 self, c_list: list[str] | str, /, params: Optional[dict[str, Any]] = None
484 ):
485 if params is None:
486 params = {}
487 if not hasattr(c_list, "__iter__") or isinstance(c_list, str):
488 c_list = [c_list]
489 self.cats: list[vText | str] = [vText(c) for c in c_list]
490 self.params = Parameters(params)
492 def __iter__(self):
493 return iter(vCategory.from_ical(self.to_ical()))
495 def to_ical(self):
496 return b",".join(
497 [
498 c.to_ical() if hasattr(c, "to_ical") else vText(c).to_ical()
499 for c in self.cats
500 ]
501 )
503 @staticmethod
504 def from_ical(ical):
505 ical = to_unicode(ical)
506 return unescape_char(ical).split(",")
508 def __eq__(self, other):
509 """self == other"""
510 return isinstance(other, vCategory) and self.cats == other.cats
512 def __repr__(self):
513 """String representation."""
514 return f"{self.__class__.__name__}({self.cats}, params={self.params})"
517class TimeBase:
518 """Make classes with a datetime/date comparable."""
520 params: Parameters
521 ignore_for_equality = {"TZID", "VALUE"}
523 def __eq__(self, other):
524 """self == other"""
525 if isinstance(other, date):
526 return self.dt == other
527 if isinstance(other, TimeBase):
528 default = object()
529 for key in (
530 set(self.params) | set(other.params)
531 ) - self.ignore_for_equality:
532 if key[:2].lower() != "x-" and self.params.get(
533 key, default
534 ) != other.params.get(key, default):
535 return False
536 return self.dt == other.dt
537 if isinstance(other, vDDDLists):
538 return other == self
539 return False
541 def __hash__(self):
542 return hash(self.dt)
544 from icalendar.param import RANGE, RELATED, TZID
546 def __repr__(self):
547 """String representation."""
548 return f"{self.__class__.__name__}({self.dt}, {self.params})"
551class vDDDTypes(TimeBase):
552 """A combined Datetime, Date or Duration parser/generator. Their format
553 cannot be confused, and often values can be of either types.
554 So this is practical.
555 """
557 params: Parameters
559 def __init__(self, dt):
560 if not isinstance(dt, (datetime, date, timedelta, time, tuple)):
561 raise TypeError(
562 "You must use datetime, date, timedelta, time or tuple (for periods)"
563 )
564 if isinstance(dt, (datetime, timedelta)):
565 self.params = Parameters()
566 elif isinstance(dt, date):
567 self.params = Parameters({"value": "DATE"})
568 elif isinstance(dt, time):
569 self.params = Parameters({"value": "TIME"})
570 else: # isinstance(dt, tuple)
571 self.params = Parameters({"value": "PERIOD"})
573 tzid = tzid_from_dt(dt) if isinstance(dt, (datetime, time)) else None
574 if tzid is not None and tzid != "UTC":
575 self.params.update({"TZID": tzid})
577 self.dt = dt
579 def to_ical(self):
580 dt = self.dt
581 if isinstance(dt, datetime):
582 return vDatetime(dt).to_ical()
583 if isinstance(dt, date):
584 return vDate(dt).to_ical()
585 if isinstance(dt, timedelta):
586 return vDuration(dt).to_ical()
587 if isinstance(dt, time):
588 return vTime(dt).to_ical()
589 if isinstance(dt, tuple) and len(dt) == 2:
590 return vPeriod(dt).to_ical()
591 raise ValueError(f"Unknown date type: {type(dt)}")
593 @classmethod
594 def from_ical(cls, ical, timezone=None):
595 if isinstance(ical, cls):
596 return ical.dt
597 u = ical.upper()
598 if u.startswith(("P", "-P", "+P")):
599 return vDuration.from_ical(ical)
600 if "/" in u:
601 return vPeriod.from_ical(ical, timezone=timezone)
603 if len(ical) in (15, 16):
604 return vDatetime.from_ical(ical, timezone=timezone)
605 if len(ical) == 8:
606 if timezone:
607 tzinfo = tzp.timezone(timezone)
608 if tzinfo is not None:
609 return to_datetime(vDate.from_ical(ical)).replace(tzinfo=tzinfo)
610 return vDate.from_ical(ical)
611 if len(ical) in (6, 7):
612 return vTime.from_ical(ical)
613 raise ValueError(f"Expected datetime, date, or time. Got: '{ical}'")
616class vDate(TimeBase):
617 """Date
619 Value Name:
620 DATE
622 Purpose:
623 This value type is used to identify values that contain a
624 calendar date.
626 Format Definition:
627 This value type is defined by the following notation:
629 .. code-block:: text
631 date = date-value
633 date-value = date-fullyear date-month date-mday
634 date-fullyear = 4DIGIT
635 date-month = 2DIGIT ;01-12
636 date-mday = 2DIGIT ;01-28, 01-29, 01-30, 01-31
637 ;based on month/year
639 Description:
640 If the property permits, multiple "date" values are
641 specified as a COMMA-separated list of values. The format for the
642 value type is based on the [ISO.8601.2004] complete
643 representation, basic format for a calendar date. The textual
644 format specifies a four-digit year, two-digit month, and two-digit
645 day of the month. There are no separator characters between the
646 year, month, and day component text.
648 Example:
649 The following represents July 14, 1997:
651 .. code-block:: text
653 19970714
655 .. code-block:: pycon
657 >>> from icalendar.prop import vDate
658 >>> date = vDate.from_ical('19970714')
659 >>> date.year
660 1997
661 >>> date.month
662 7
663 >>> date.day
664 14
665 """
667 params: Parameters
669 def __init__(self, dt):
670 if not isinstance(dt, date):
671 raise TypeError("Value MUST be a date instance")
672 self.dt = dt
673 self.params = Parameters({"value": "DATE"})
675 def to_ical(self):
676 s = f"{self.dt.year:04}{self.dt.month:02}{self.dt.day:02}"
677 return s.encode("utf-8")
679 @staticmethod
680 def from_ical(ical):
681 try:
682 timetuple = (
683 int(ical[:4]), # year
684 int(ical[4:6]), # month
685 int(ical[6:8]), # day
686 )
687 return date(*timetuple)
688 except Exception as e:
689 raise ValueError(f"Wrong date format {ical}") from e
692class vDatetime(TimeBase):
693 """Render and generates icalendar datetime format.
695 vDatetime is timezone aware and uses a timezone library.
696 When a vDatetime object is created from an
697 ical string, you can pass a valid timezone identifier. When a
698 vDatetime object is created from a python datetime object, it uses the
699 tzinfo component, if present. Otherwise a timezone-naive object is
700 created. Be aware that there are certain limitations with timezone naive
701 DATE-TIME components in the icalendar standard.
702 """
704 params: Parameters
706 def __init__(self, dt, /, params: Optional[dict[str, Any]] = None):
707 if params is None:
708 params = {}
709 self.dt = dt
710 self.params = Parameters(params)
712 def to_ical(self):
713 dt = self.dt
714 tzid = tzid_from_dt(dt)
716 s = (
717 f"{dt.year:04}{dt.month:02}{dt.day:02}"
718 f"T{dt.hour:02}{dt.minute:02}{dt.second:02}"
719 )
720 if tzid == "UTC":
721 s += "Z"
722 elif tzid:
723 self.params.update({"TZID": tzid})
724 return s.encode("utf-8")
726 @staticmethod
727 def from_ical(ical, timezone=None):
728 """Create a datetime from the RFC string.
730 Format:
732 .. code-block:: text
734 YYYYMMDDTHHMMSS
736 .. code-block:: pycon
738 >>> from icalendar import vDatetime
739 >>> vDatetime.from_ical("20210302T101500")
740 datetime.datetime(2021, 3, 2, 10, 15)
742 >>> vDatetime.from_ical("20210302T101500", "America/New_York")
743 datetime.datetime(2021, 3, 2, 10, 15, tzinfo=ZoneInfo(key='America/New_York'))
745 >>> from zoneinfo import ZoneInfo
746 >>> timezone = ZoneInfo("Europe/Berlin")
747 >>> vDatetime.from_ical("20210302T101500", timezone)
748 datetime.datetime(2021, 3, 2, 10, 15, tzinfo=ZoneInfo(key='Europe/Berlin'))
749 """ # noqa: E501
750 tzinfo = None
751 if isinstance(timezone, str):
752 tzinfo = tzp.timezone(timezone)
753 elif timezone is not None:
754 tzinfo = timezone
756 try:
757 timetuple = (
758 int(ical[:4]), # year
759 int(ical[4:6]), # month
760 int(ical[6:8]), # day
761 int(ical[9:11]), # hour
762 int(ical[11:13]), # minute
763 int(ical[13:15]), # second
764 )
765 if tzinfo:
766 return tzp.localize(datetime(*timetuple), tzinfo) # noqa: DTZ001
767 if not ical[15:]:
768 return datetime(*timetuple) # noqa: DTZ001
769 if ical[15:16] == "Z":
770 return tzp.localize_utc(datetime(*timetuple)) # noqa: DTZ001
771 except Exception as e:
772 raise ValueError(f"Wrong datetime format: {ical}") from e
773 raise ValueError(f"Wrong datetime format: {ical}")
776class vDuration(TimeBase):
777 """Duration
779 Value Name:
780 DURATION
782 Purpose:
783 This value type is used to identify properties that contain
784 a duration of time.
786 Format Definition:
787 This value type is defined by the following notation:
789 .. code-block:: text
791 dur-value = (["+"] / "-") "P" (dur-date / dur-time / dur-week)
793 dur-date = dur-day [dur-time]
794 dur-time = "T" (dur-hour / dur-minute / dur-second)
795 dur-week = 1*DIGIT "W"
796 dur-hour = 1*DIGIT "H" [dur-minute]
797 dur-minute = 1*DIGIT "M" [dur-second]
798 dur-second = 1*DIGIT "S"
799 dur-day = 1*DIGIT "D"
801 Description:
802 If the property permits, multiple "duration" values are
803 specified by a COMMA-separated list of values. The format is
804 based on the [ISO.8601.2004] complete representation basic format
805 with designators for the duration of time. The format can
806 represent nominal durations (weeks and days) and accurate
807 durations (hours, minutes, and seconds). Note that unlike
808 [ISO.8601.2004], this value type doesn't support the "Y" and "M"
809 designators to specify durations in terms of years and months.
810 The duration of a week or a day depends on its position in the
811 calendar. In the case of discontinuities in the time scale, such
812 as the change from standard time to daylight time and back, the
813 computation of the exact duration requires the subtraction or
814 addition of the change of duration of the discontinuity. Leap
815 seconds MUST NOT be considered when computing an exact duration.
816 When computing an exact duration, the greatest order time
817 components MUST be added first, that is, the number of days MUST
818 be added first, followed by the number of hours, number of
819 minutes, and number of seconds.
821 Example:
822 A duration of 15 days, 5 hours, and 20 seconds would be:
824 .. code-block:: text
826 P15DT5H0M20S
828 A duration of 7 weeks would be:
830 .. code-block:: text
832 P7W
834 .. code-block:: pycon
836 >>> from icalendar.prop import vDuration
837 >>> duration = vDuration.from_ical('P15DT5H0M20S')
838 >>> duration
839 datetime.timedelta(days=15, seconds=18020)
840 >>> duration = vDuration.from_ical('P7W')
841 >>> duration
842 datetime.timedelta(days=49)
843 """
845 params: Parameters
847 def __init__(self, td, /, params: Optional[dict[str, Any]] = None):
848 if params is None:
849 params = {}
850 if not isinstance(td, timedelta):
851 raise TypeError("Value MUST be a timedelta instance")
852 self.td = td
853 self.params = Parameters(params)
855 def to_ical(self):
856 sign = ""
857 td = self.td
858 if td.days < 0:
859 sign = "-"
860 td = -td
861 timepart = ""
862 if td.seconds:
863 timepart = "T"
864 hours = td.seconds // 3600
865 minutes = td.seconds % 3600 // 60
866 seconds = td.seconds % 60
867 if hours:
868 timepart += f"{hours}H"
869 if minutes or (hours and seconds):
870 timepart += f"{minutes}M"
871 if seconds:
872 timepart += f"{seconds}S"
873 if td.days == 0 and timepart:
874 return str(sign).encode("utf-8") + b"P" + str(timepart).encode("utf-8")
875 return (
876 str(sign).encode("utf-8")
877 + b"P"
878 + str(abs(td.days)).encode("utf-8")
879 + b"D"
880 + str(timepart).encode("utf-8")
881 )
883 @staticmethod
884 def from_ical(ical):
885 match = DURATION_REGEX.match(ical)
886 if not match:
887 raise ValueError(f"Invalid iCalendar duration: {ical}")
889 sign, weeks, days, hours, minutes, seconds = match.groups()
890 value = timedelta(
891 weeks=int(weeks or 0),
892 days=int(days or 0),
893 hours=int(hours or 0),
894 minutes=int(minutes or 0),
895 seconds=int(seconds or 0),
896 )
898 if sign == "-":
899 value = -value
901 return value
903 @property
904 def dt(self) -> timedelta:
905 """The time delta for compatibility."""
906 return self.td
909class vPeriod(TimeBase):
910 """Period of Time
912 Value Name:
913 PERIOD
915 Purpose:
916 This value type is used to identify values that contain a
917 precise period of time.
919 Format Definition:
920 This value type is defined by the following notation:
922 .. code-block:: text
924 period = period-explicit / period-start
926 period-explicit = date-time "/" date-time
927 ; [ISO.8601.2004] complete representation basic format for a
928 ; period of time consisting of a start and end. The start MUST
929 ; be before the end.
931 period-start = date-time "/" dur-value
932 ; [ISO.8601.2004] complete representation basic format for a
933 ; period of time consisting of a start and positive duration
934 ; of time.
936 Description:
937 If the property permits, multiple "period" values are
938 specified by a COMMA-separated list of values. There are two
939 forms of a period of time. First, a period of time is identified
940 by its start and its end. This format is based on the
941 [ISO.8601.2004] complete representation, basic format for "DATE-
942 TIME" start of the period, followed by a SOLIDUS character
943 followed by the "DATE-TIME" of the end of the period. The start
944 of the period MUST be before the end of the period. Second, a
945 period of time can also be defined by a start and a positive
946 duration of time. The format is based on the [ISO.8601.2004]
947 complete representation, basic format for the "DATE-TIME" start of
948 the period, followed by a SOLIDUS character, followed by the
949 [ISO.8601.2004] basic format for "DURATION" of the period.
951 Example:
952 The period starting at 18:00:00 UTC, on January 1, 1997 and
953 ending at 07:00:00 UTC on January 2, 1997 would be:
955 .. code-block:: text
957 19970101T180000Z/19970102T070000Z
959 The period start at 18:00:00 on January 1, 1997 and lasting 5 hours
960 and 30 minutes would be:
962 .. code-block:: text
964 19970101T180000Z/PT5H30M
966 .. code-block:: pycon
968 >>> from icalendar.prop import vPeriod
969 >>> period = vPeriod.from_ical('19970101T180000Z/19970102T070000Z')
970 >>> period = vPeriod.from_ical('19970101T180000Z/PT5H30M')
971 """
973 params: Parameters
975 def __init__(self, per: tuple[datetime, Union[datetime, timedelta]]):
976 start, end_or_duration = per
977 if not (isinstance(start, (datetime, date))):
978 raise TypeError("Start value MUST be a datetime or date instance")
979 if not (isinstance(end_or_duration, (datetime, date, timedelta))):
980 raise TypeError(
981 "end_or_duration MUST be a datetime, date or timedelta instance"
982 )
983 by_duration = 0
984 if isinstance(end_or_duration, timedelta):
985 by_duration = 1
986 duration = end_or_duration
987 end = start + duration
988 else:
989 end = end_or_duration
990 duration = end - start
991 if start > end:
992 raise ValueError("Start time is greater than end time")
994 self.params = Parameters({"value": "PERIOD"})
995 # set the timezone identifier
996 # does not support different timezones for start and end
997 tzid = tzid_from_dt(start)
998 if tzid:
999 self.params["TZID"] = tzid
1001 self.start = start
1002 self.end = end
1003 self.by_duration = by_duration
1004 self.duration = duration
1006 def overlaps(self, other):
1007 if self.start > other.start:
1008 return other.overlaps(self)
1009 return self.start <= other.start < self.end
1011 def to_ical(self):
1012 if self.by_duration:
1013 return (
1014 vDatetime(self.start).to_ical()
1015 + b"/"
1016 + vDuration(self.duration).to_ical()
1017 )
1018 return vDatetime(self.start).to_ical() + b"/" + vDatetime(self.end).to_ical()
1020 @staticmethod
1021 def from_ical(ical, timezone=None):
1022 try:
1023 start, end_or_duration = ical.split("/")
1024 start = vDDDTypes.from_ical(start, timezone=timezone)
1025 end_or_duration = vDDDTypes.from_ical(end_or_duration, timezone=timezone)
1026 except Exception as e:
1027 raise ValueError(f"Expected period format, got: {ical}") from e
1028 return (start, end_or_duration)
1030 def __repr__(self):
1031 p = (self.start, self.duration) if self.by_duration else (self.start, self.end)
1032 return f"vPeriod({p!r})"
1034 @property
1035 def dt(self):
1036 """Make this cooperate with the other vDDDTypes."""
1037 return (self.start, (self.duration if self.by_duration else self.end))
1039 from icalendar.param import FBTYPE
1042class vWeekday(str):
1043 """Either a ``weekday`` or a ``weekdaynum``
1045 .. code-block:: pycon
1047 >>> from icalendar import vWeekday
1048 >>> vWeekday("MO") # Simple weekday
1049 'MO'
1050 >>> vWeekday("2FR").relative # Second friday
1051 2
1052 >>> vWeekday("2FR").weekday
1053 'FR'
1054 >>> vWeekday("-1SU").relative # Last Sunday
1055 -1
1057 Definition from `RFC 5545, Section 3.3.10 <https://www.rfc-editor.org/rfc/rfc5545#section-3.3.10>`_:
1059 .. code-block:: text
1061 weekdaynum = [[plus / minus] ordwk] weekday
1062 plus = "+"
1063 minus = "-"
1064 ordwk = 1*2DIGIT ;1 to 53
1065 weekday = "SU" / "MO" / "TU" / "WE" / "TH" / "FR" / "SA"
1066 ;Corresponding to SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY,
1067 ;FRIDAY, and SATURDAY days of the week.
1069 """
1071 params: Parameters
1072 __slots__ = ("params", "relative", "weekday")
1074 week_days = CaselessDict(
1075 {
1076 "SU": 0,
1077 "MO": 1,
1078 "TU": 2,
1079 "WE": 3,
1080 "TH": 4,
1081 "FR": 5,
1082 "SA": 6,
1083 }
1084 )
1086 def __new__(
1087 cls,
1088 value,
1089 encoding=DEFAULT_ENCODING,
1090 /,
1091 params: Optional[dict[str, Any]] = None,
1092 ):
1093 if params is None:
1094 params = {}
1095 value = to_unicode(value, encoding=encoding)
1096 self = super().__new__(cls, value)
1097 match = WEEKDAY_RULE.match(self)
1098 if match is None:
1099 raise ValueError(f"Expected weekday abbrevation, got: {self}")
1100 match = match.groupdict()
1101 sign = match["signal"]
1102 weekday = match["weekday"]
1103 relative = match["relative"]
1104 if weekday not in vWeekday.week_days or sign not in "+-":
1105 raise ValueError(f"Expected weekday abbrevation, got: {self}")
1106 self.weekday = weekday or None
1107 self.relative = (relative and int(relative)) or None
1108 if sign == "-" and self.relative:
1109 self.relative *= -1
1110 self.params = Parameters(params)
1111 return self
1113 def to_ical(self):
1114 return self.encode(DEFAULT_ENCODING).upper()
1116 @classmethod
1117 def from_ical(cls, ical):
1118 try:
1119 return cls(ical.upper())
1120 except Exception as e:
1121 raise ValueError(f"Expected weekday abbrevation, got: {ical}") from e
1124class vFrequency(str):
1125 """A simple class that catches illegal values."""
1127 params: Parameters
1128 __slots__ = ("params",)
1130 frequencies = CaselessDict(
1131 {
1132 "SECONDLY": "SECONDLY",
1133 "MINUTELY": "MINUTELY",
1134 "HOURLY": "HOURLY",
1135 "DAILY": "DAILY",
1136 "WEEKLY": "WEEKLY",
1137 "MONTHLY": "MONTHLY",
1138 "YEARLY": "YEARLY",
1139 }
1140 )
1142 def __new__(
1143 cls,
1144 value,
1145 encoding=DEFAULT_ENCODING,
1146 /,
1147 params: Optional[dict[str, Any]] = None,
1148 ):
1149 if params is None:
1150 params = {}
1151 value = to_unicode(value, encoding=encoding)
1152 self = super().__new__(cls, value)
1153 if self not in vFrequency.frequencies:
1154 raise ValueError(f"Expected frequency, got: {self}")
1155 self.params = Parameters(params)
1156 return self
1158 def to_ical(self):
1159 return self.encode(DEFAULT_ENCODING).upper()
1161 @classmethod
1162 def from_ical(cls, ical):
1163 try:
1164 return cls(ical.upper())
1165 except Exception as e:
1166 raise ValueError(f"Expected frequency, got: {ical}") from e
1169class vMonth(int):
1170 """The number of the month for recurrence.
1172 In :rfc:`5545`, this is just an int.
1173 In :rfc:`7529`, this can be followed by `L` to indicate a leap month.
1175 .. code-block:: pycon
1177 >>> from icalendar import vMonth
1178 >>> vMonth(1) # first month January
1179 vMonth('1')
1180 >>> vMonth("5L") # leap month in Hebrew calendar
1181 vMonth('5L')
1182 >>> vMonth(1).leap
1183 False
1184 >>> vMonth("5L").leap
1185 True
1187 Definition from RFC:
1189 .. code-block:: text
1191 type-bymonth = element bymonth {
1192 xsd:positiveInteger |
1193 xsd:string
1194 }
1195 """
1197 params: Parameters
1199 def __new__(
1200 cls, month: Union[str, int], /, params: Optional[dict[str, Any]] = None
1201 ):
1202 if params is None:
1203 params = {}
1204 if isinstance(month, vMonth):
1205 return cls(month.to_ical().decode())
1206 if isinstance(month, str):
1207 if month.isdigit():
1208 month_index = int(month)
1209 leap = False
1210 else:
1211 if month[-1] != "L" and month[:-1].isdigit():
1212 raise ValueError(f"Invalid month: {month!r}")
1213 month_index = int(month[:-1])
1214 leap = True
1215 else:
1216 leap = False
1217 month_index = int(month)
1218 self = super().__new__(cls, month_index)
1219 self.leap = leap
1220 self.params = Parameters(params)
1221 return self
1223 def to_ical(self) -> bytes:
1224 """The ical representation."""
1225 return str(self).encode("utf-8")
1227 @classmethod
1228 def from_ical(cls, ical: str):
1229 return cls(ical)
1231 @property
1232 def leap(self) -> bool:
1233 """Whether this is a leap month."""
1234 return self._leap
1236 @leap.setter
1237 def leap(self, value: bool) -> None:
1238 self._leap = value
1240 def __repr__(self) -> str:
1241 """repr(self)"""
1242 return f"{self.__class__.__name__}({str(self)!r})"
1244 def __str__(self) -> str:
1245 """str(self)"""
1246 return f"{int(self)}{'L' if self.leap else ''}"
1249class vSkip(vText, Enum):
1250 """Skip values for RRULE.
1252 These are defined in :rfc:`7529`.
1254 OMIT is the default value.
1256 Examples:
1258 .. code-block:: pycon
1260 >>> from icalendar import vSkip
1261 >>> vSkip.OMIT
1262 vSkip('OMIT')
1263 >>> vSkip.FORWARD
1264 vSkip('FORWARD')
1265 >>> vSkip.BACKWARD
1266 vSkip('BACKWARD')
1267 """
1269 OMIT = "OMIT"
1270 FORWARD = "FORWARD"
1271 BACKWARD = "BACKWARD"
1273 __reduce_ex__ = Enum.__reduce_ex__
1275 def __repr__(self):
1276 return f"{self.__class__.__name__}({self._name_!r})"
1279class vRecur(CaselessDict):
1280 """Recurrence definition.
1282 Property Name:
1283 RRULE
1285 Purpose:
1286 This property defines a rule or repeating pattern for recurring events, to-dos,
1287 journal entries, or time zone definitions.
1289 Value Type:
1290 RECUR
1292 Property Parameters:
1293 IANA and non-standard property parameters can be specified on this property.
1295 Conformance:
1296 This property can be specified in recurring "VEVENT", "VTODO", and "VJOURNAL"
1297 calendar components as well as in the "STANDARD" and "DAYLIGHT" sub-components
1298 of the "VTIMEZONE" calendar component, but it SHOULD NOT be specified more than once.
1299 The recurrence set generated with multiple "RRULE" properties is undefined.
1301 Description:
1302 The recurrence rule, if specified, is used in computing the recurrence set.
1303 The recurrence set is the complete set of recurrence instances for a calendar component.
1304 The recurrence set is generated by considering the initial "DTSTART" property along
1305 with the "RRULE", "RDATE", and "EXDATE" properties contained within the
1306 recurring component. The "DTSTART" property defines the first instance in the
1307 recurrence set. The "DTSTART" property value SHOULD be synchronized with the
1308 recurrence rule, if specified. The recurrence set generated with a "DTSTART" property
1309 value not synchronized with the recurrence rule is undefined.
1310 The final recurrence set is generated by gathering all of the start DATE-TIME
1311 values generated by any of the specified "RRULE" and "RDATE" properties, and then
1312 excluding any start DATE-TIME values specified by "EXDATE" properties.
1313 This implies that start DATE- TIME values specified by "EXDATE" properties take
1314 precedence over those specified by inclusion properties (i.e., "RDATE" and "RRULE").
1315 Where duplicate instances are generated by the "RRULE" and "RDATE" properties,
1316 only one recurrence is considered. Duplicate instances are ignored.
1318 The "DTSTART" property specified within the iCalendar object defines the first
1319 instance of the recurrence. In most cases, a "DTSTART" property of DATE-TIME value
1320 type used with a recurrence rule, should be specified as a date with local time
1321 and time zone reference to make sure all the recurrence instances start at the
1322 same local time regardless of time zone changes.
1324 If the duration of the recurring component is specified with the "DTEND" or
1325 "DUE" property, then the same exact duration will apply to all the members of the
1326 generated recurrence set. Else, if the duration of the recurring component is
1327 specified with the "DURATION" property, then the same nominal duration will apply
1328 to all the members of the generated recurrence set and the exact duration of each
1329 recurrence instance will depend on its specific start time. For example, recurrence
1330 instances of a nominal duration of one day will have an exact duration of more or less
1331 than 24 hours on a day where a time zone shift occurs. The duration of a specific
1332 recurrence may be modified in an exception component or simply by using an
1333 "RDATE" property of PERIOD value type.
1335 Examples:
1336 The following RRULE specifies daily events for 10 occurrences.
1338 .. code-block:: text
1340 RRULE:FREQ=DAILY;COUNT=10
1342 Below, we parse the RRULE ical string.
1344 .. code-block:: pycon
1346 >>> from icalendar.prop import vRecur
1347 >>> rrule = vRecur.from_ical('FREQ=DAILY;COUNT=10')
1348 >>> rrule
1349 vRecur({'FREQ': ['DAILY'], 'COUNT': [10]})
1351 You can choose to add an rrule to an :class:`icalendar.cal.Event` or
1352 :class:`icalendar.cal.Todo`.
1354 .. code-block:: pycon
1356 >>> from icalendar import Event
1357 >>> event = Event()
1358 >>> event.add('RRULE', 'FREQ=DAILY;COUNT=10')
1359 >>> event.rrules
1360 [vRecur({'FREQ': ['DAILY'], 'COUNT': [10]})]
1361 """ # noqa: E501
1363 params: Parameters
1365 frequencies = [
1366 "SECONDLY",
1367 "MINUTELY",
1368 "HOURLY",
1369 "DAILY",
1370 "WEEKLY",
1371 "MONTHLY",
1372 "YEARLY",
1373 ]
1375 # Mac iCal ignores RRULEs where FREQ is not the first rule part.
1376 # Sorts parts according to the order listed in RFC 5545, section 3.3.10.
1377 canonical_order = (
1378 "RSCALE",
1379 "FREQ",
1380 "UNTIL",
1381 "COUNT",
1382 "INTERVAL",
1383 "BYSECOND",
1384 "BYMINUTE",
1385 "BYHOUR",
1386 "BYDAY",
1387 "BYWEEKDAY",
1388 "BYMONTHDAY",
1389 "BYYEARDAY",
1390 "BYWEEKNO",
1391 "BYMONTH",
1392 "BYSETPOS",
1393 "WKST",
1394 "SKIP",
1395 )
1397 types = CaselessDict(
1398 {
1399 "COUNT": vInt,
1400 "INTERVAL": vInt,
1401 "BYSECOND": vInt,
1402 "BYMINUTE": vInt,
1403 "BYHOUR": vInt,
1404 "BYWEEKNO": vInt,
1405 "BYMONTHDAY": vInt,
1406 "BYYEARDAY": vInt,
1407 "BYMONTH": vMonth,
1408 "UNTIL": vDDDTypes,
1409 "BYSETPOS": vInt,
1410 "WKST": vWeekday,
1411 "BYDAY": vWeekday,
1412 "FREQ": vFrequency,
1413 "BYWEEKDAY": vWeekday,
1414 "SKIP": vSkip,
1415 }
1416 )
1418 def __init__(self, *args, params: Optional[dict[str, Any]] = None, **kwargs):
1419 if params is None:
1420 params = {}
1421 if args and isinstance(args[0], str):
1422 # we have a string as an argument.
1423 args = (self.from_ical(args[0]),) + args[1:]
1424 for k, v in kwargs.items():
1425 if not isinstance(v, SEQUENCE_TYPES):
1426 kwargs[k] = [v]
1427 super().__init__(*args, **kwargs)
1428 self.params = Parameters(params)
1430 def to_ical(self):
1431 result = []
1432 for key, vals in self.sorted_items():
1433 typ = self.types.get(key, vText)
1434 if not isinstance(vals, SEQUENCE_TYPES):
1435 vals = [vals] # noqa: PLW2901
1436 param_vals = b",".join(typ(val).to_ical() for val in vals)
1438 # CaselessDict keys are always unicode
1439 param_key = key.encode(DEFAULT_ENCODING)
1440 result.append(param_key + b"=" + param_vals)
1442 return b";".join(result)
1444 @classmethod
1445 def parse_type(cls, key, values):
1446 # integers
1447 parser = cls.types.get(key, vText)
1448 return [parser.from_ical(v) for v in values.split(",")]
1450 @classmethod
1451 def from_ical(cls, ical: str):
1452 if isinstance(ical, cls):
1453 return ical
1454 try:
1455 recur = cls()
1456 for pairs in ical.split(";"):
1457 try:
1458 key, vals = pairs.split("=")
1459 except ValueError:
1460 # E.g. incorrect trailing semicolon, like (issue #157):
1461 # FREQ=YEARLY;BYMONTH=11;BYDAY=1SU;
1462 continue
1463 recur[key] = cls.parse_type(key, vals)
1464 return cls(recur)
1465 except ValueError:
1466 raise
1467 except Exception as e:
1468 raise ValueError(f"Error in recurrence rule: {ical}") from e
1471class vTime(TimeBase):
1472 """Time
1474 Value Name:
1475 TIME
1477 Purpose:
1478 This value type is used to identify values that contain a
1479 time of day.
1481 Format Definition:
1482 This value type is defined by the following notation:
1484 .. code-block:: text
1486 time = time-hour time-minute time-second [time-utc]
1488 time-hour = 2DIGIT ;00-23
1489 time-minute = 2DIGIT ;00-59
1490 time-second = 2DIGIT ;00-60
1491 ;The "60" value is used to account for positive "leap" seconds.
1493 time-utc = "Z"
1495 Description:
1496 If the property permits, multiple "time" values are
1497 specified by a COMMA-separated list of values. No additional
1498 content value encoding (i.e., BACKSLASH character encoding, see
1499 vText) is defined for this value type.
1501 The "TIME" value type is used to identify values that contain a
1502 time of day. The format is based on the [ISO.8601.2004] complete
1503 representation, basic format for a time of day. The text format
1504 consists of a two-digit, 24-hour of the day (i.e., values 00-23),
1505 two-digit minute in the hour (i.e., values 00-59), and two-digit
1506 seconds in the minute (i.e., values 00-60). The seconds value of
1507 60 MUST only be used to account for positive "leap" seconds.
1508 Fractions of a second are not supported by this format.
1510 In parallel to the "DATE-TIME" definition above, the "TIME" value
1511 type expresses time values in three forms:
1513 The form of time with UTC offset MUST NOT be used. For example,
1514 the following is not valid for a time value:
1516 .. code-block:: text
1518 230000-0800 ;Invalid time format
1520 **FORM #1 LOCAL TIME**
1522 The local time form is simply a time value that does not contain
1523 the UTC designator nor does it reference a time zone. For
1524 example, 11:00 PM:
1526 .. code-block:: text
1528 230000
1530 Time values of this type are said to be "floating" and are not
1531 bound to any time zone in particular. They are used to represent
1532 the same hour, minute, and second value regardless of which time
1533 zone is currently being observed. For example, an event can be
1534 defined that indicates that an individual will be busy from 11:00
1535 AM to 1:00 PM every day, no matter which time zone the person is
1536 in. In these cases, a local time can be specified. The recipient
1537 of an iCalendar object with a property value consisting of a local
1538 time, without any relative time zone information, SHOULD interpret
1539 the value as being fixed to whatever time zone the "ATTENDEE" is
1540 in at any given moment. This means that two "Attendees", may
1541 participate in the same event at different UTC times; floating
1542 time SHOULD only be used where that is reasonable behavior.
1544 In most cases, a fixed time is desired. To properly communicate a
1545 fixed time in a property value, either UTC time or local time with
1546 time zone reference MUST be specified.
1548 The use of local time in a TIME value without the "TZID" property
1549 parameter is to be interpreted as floating time, regardless of the
1550 existence of "VTIMEZONE" calendar components in the iCalendar
1551 object.
1553 **FORM #2: UTC TIME**
1555 UTC time, or absolute time, is identified by a LATIN CAPITAL
1556 LETTER Z suffix character, the UTC designator, appended to the
1557 time value. For example, the following represents 07:00 AM UTC:
1559 .. code-block:: text
1561 070000Z
1563 The "TZID" property parameter MUST NOT be applied to TIME
1564 properties whose time values are specified in UTC.
1566 **FORM #3: LOCAL TIME AND TIME ZONE REFERENCE**
1568 The local time with reference to time zone information form is
1569 identified by the use the "TZID" property parameter to reference
1570 the appropriate time zone definition.
1572 Example:
1573 The following represents 8:30 AM in New York in winter,
1574 five hours behind UTC, in each of the three formats:
1576 .. code-block:: text
1578 083000
1579 133000Z
1580 TZID=America/New_York:083000
1581 """
1583 def __init__(self, *args):
1584 if len(args) == 1:
1585 if not isinstance(args[0], (time, datetime)):
1586 raise ValueError(f"Expected a datetime.time, got: {args[0]}")
1587 self.dt = args[0]
1588 else:
1589 self.dt = time(*args)
1590 self.params = Parameters({"value": "TIME"})
1592 def to_ical(self):
1593 return self.dt.strftime("%H%M%S")
1595 @staticmethod
1596 def from_ical(ical):
1597 # TODO: timezone support
1598 try:
1599 timetuple = (int(ical[:2]), int(ical[2:4]), int(ical[4:6]))
1600 return time(*timetuple)
1601 except Exception as e:
1602 raise ValueError(f"Expected time, got: {ical}") from e
1605class vUri(str):
1606 """URI
1608 Value Name:
1609 URI
1611 Purpose:
1612 This value type is used to identify values that contain a
1613 uniform resource identifier (URI) type of reference to the
1614 property value.
1616 Format Definition:
1617 This value type is defined by the following notation:
1619 .. code-block:: text
1621 uri = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
1623 Description:
1624 This value type might be used to reference binary
1625 information, for values that are large, or otherwise undesirable
1626 to include directly in the iCalendar object.
1628 Property values with this value type MUST follow the generic URI
1629 syntax defined in [RFC3986].
1631 When a property parameter value is a URI value type, the URI MUST
1632 be specified as a quoted-string value.
1634 Example:
1635 The following is a URI for a network file:
1637 .. code-block:: text
1639 http://example.com/my-report.txt
1641 .. code-block:: pycon
1643 >>> from icalendar.prop import vUri
1644 >>> uri = vUri.from_ical('http://example.com/my-report.txt')
1645 >>> uri
1646 'http://example.com/my-report.txt'
1647 """
1649 params: Parameters
1650 __slots__ = ("params",)
1652 def __new__(
1653 cls,
1654 value,
1655 encoding=DEFAULT_ENCODING,
1656 /,
1657 params: Optional[dict[str, Any]] = None,
1658 ):
1659 if params is None:
1660 params = {}
1661 value = to_unicode(value, encoding=encoding)
1662 self = super().__new__(cls, value)
1663 self.params = Parameters(params)
1664 return self
1666 def to_ical(self):
1667 return self.encode(DEFAULT_ENCODING)
1669 @classmethod
1670 def from_ical(cls, ical):
1671 try:
1672 return cls(ical)
1673 except Exception as e:
1674 raise ValueError(f"Expected , got: {ical}") from e
1677class vGeo:
1678 """Geographic Position
1680 Property Name:
1681 GEO
1683 Purpose:
1684 This property specifies information related to the global
1685 position for the activity specified by a calendar component.
1687 Value Type:
1688 FLOAT. The value MUST be two SEMICOLON-separated FLOAT values.
1690 Property Parameters:
1691 IANA and non-standard property parameters can be specified on
1692 this property.
1694 Conformance:
1695 This property can be specified in "VEVENT" or "VTODO"
1696 calendar components.
1698 Description:
1699 This property value specifies latitude and longitude,
1700 in that order (i.e., "LAT LON" ordering). The longitude
1701 represents the location east or west of the prime meridian as a
1702 positive or negative real number, respectively. The longitude and
1703 latitude values MAY be specified up to six decimal places, which
1704 will allow for accuracy to within one meter of geographical
1705 position. Receiving applications MUST accept values of this
1706 precision and MAY truncate values of greater precision.
1708 Example:
1710 .. code-block:: text
1712 GEO:37.386013;-122.082932
1714 Parse vGeo:
1716 .. code-block:: pycon
1718 >>> from icalendar.prop import vGeo
1719 >>> geo = vGeo.from_ical('37.386013;-122.082932')
1720 >>> geo
1721 (37.386013, -122.082932)
1723 Add a geo location to an event:
1725 .. code-block:: pycon
1727 >>> from icalendar import Event
1728 >>> event = Event()
1729 >>> latitude = 37.386013
1730 >>> longitude = -122.082932
1731 >>> event.add('GEO', (latitude, longitude))
1732 >>> event['GEO']
1733 vGeo((37.386013, -122.082932))
1734 """
1736 params: Parameters
1738 def __init__(
1739 self,
1740 geo: tuple[float | str | int, float | str | int],
1741 /,
1742 params: Optional[dict[str, Any]] = None,
1743 ):
1744 """Create a new vGeo from a tuple of (latitude, longitude).
1746 Raises:
1747 ValueError: if geo is not a tuple of (latitude, longitude)
1748 """
1749 if params is None:
1750 params = {}
1751 try:
1752 latitude, longitude = (geo[0], geo[1])
1753 latitude = float(latitude)
1754 longitude = float(longitude)
1755 except Exception as e:
1756 raise ValueError(
1757 "Input must be (float, float) for latitude and longitude"
1758 ) from e
1759 self.latitude = latitude
1760 self.longitude = longitude
1761 self.params = Parameters(params)
1763 def to_ical(self):
1764 return f"{self.latitude};{self.longitude}"
1766 @staticmethod
1767 def from_ical(ical):
1768 try:
1769 latitude, longitude = ical.split(";")
1770 return (float(latitude), float(longitude))
1771 except Exception as e:
1772 raise ValueError(f"Expected 'float;float' , got: {ical}") from e
1774 def __eq__(self, other):
1775 return self.to_ical() == other.to_ical()
1777 def __repr__(self):
1778 """repr(self)"""
1779 return f"{self.__class__.__name__}(({self.latitude}, {self.longitude}))"
1782class vUTCOffset:
1783 """UTC Offset
1785 Value Name:
1786 UTC-OFFSET
1788 Purpose:
1789 This value type is used to identify properties that contain
1790 an offset from UTC to local time.
1792 Format Definition:
1793 This value type is defined by the following notation:
1795 .. code-block:: text
1797 utc-offset = time-numzone
1799 time-numzone = ("+" / "-") time-hour time-minute [time-second]
1801 Description:
1802 The PLUS SIGN character MUST be specified for positive
1803 UTC offsets (i.e., ahead of UTC). The HYPHEN-MINUS character MUST
1804 be specified for negative UTC offsets (i.e., behind of UTC). The
1805 value of "-0000" and "-000000" are not allowed. The time-second,
1806 if present, MUST NOT be 60; if absent, it defaults to zero.
1808 Example:
1809 The following UTC offsets are given for standard time for
1810 New York (five hours behind UTC) and Geneva (one hour ahead of
1811 UTC):
1813 .. code-block:: text
1815 -0500
1817 +0100
1819 .. code-block:: pycon
1821 >>> from icalendar.prop import vUTCOffset
1822 >>> utc_offset = vUTCOffset.from_ical('-0500')
1823 >>> utc_offset
1824 datetime.timedelta(days=-1, seconds=68400)
1825 >>> utc_offset = vUTCOffset.from_ical('+0100')
1826 >>> utc_offset
1827 datetime.timedelta(seconds=3600)
1828 """
1830 params: Parameters
1832 ignore_exceptions = False # if True, and we cannot parse this
1834 # component, we will silently ignore
1835 # it, rather than let the exception
1836 # propagate upwards
1838 def __init__(self, td, /, params: Optional[dict[str, Any]] = None):
1839 if params is None:
1840 params = {}
1841 if not isinstance(td, timedelta):
1842 raise TypeError("Offset value MUST be a timedelta instance")
1843 self.td = td
1844 self.params = Parameters(params)
1846 def to_ical(self):
1847 if self.td < timedelta(0):
1848 sign = "-%s"
1849 td = timedelta(0) - self.td # get timedelta relative to 0
1850 else:
1851 # Google Calendar rejects '0000' but accepts '+0000'
1852 sign = "+%s"
1853 td = self.td
1855 days, seconds = td.days, td.seconds
1857 hours = abs(days * 24 + seconds // 3600)
1858 minutes = abs((seconds % 3600) // 60)
1859 seconds = abs(seconds % 60)
1860 if seconds:
1861 duration = f"{hours:02}{minutes:02}{seconds:02}"
1862 else:
1863 duration = f"{hours:02}{minutes:02}"
1864 return sign % duration
1866 @classmethod
1867 def from_ical(cls, ical):
1868 if isinstance(ical, cls):
1869 return ical.td
1870 try:
1871 sign, hours, minutes, seconds = (
1872 ical[0:1],
1873 int(ical[1:3]),
1874 int(ical[3:5]),
1875 int(ical[5:7] or 0),
1876 )
1877 offset = timedelta(hours=hours, minutes=minutes, seconds=seconds)
1878 except Exception as e:
1879 raise ValueError(f"Expected utc offset, got: {ical}") from e
1880 if not cls.ignore_exceptions and offset >= timedelta(hours=24):
1881 raise ValueError(f"Offset must be less than 24 hours, was {ical}")
1882 if sign == "-":
1883 return -offset
1884 return offset
1886 def __eq__(self, other):
1887 if not isinstance(other, vUTCOffset):
1888 return False
1889 return self.td == other.td
1891 def __hash__(self):
1892 return hash(self.td)
1894 def __repr__(self):
1895 return f"vUTCOffset({self.td!r})"
1898class vInline(str):
1899 """This is an especially dumb class that just holds raw unparsed text and
1900 has parameters. Conversion of inline values are handled by the Component
1901 class, so no further processing is needed.
1902 """
1904 params: Parameters
1905 __slots__ = ("params",)
1907 def __new__(
1908 cls,
1909 value,
1910 encoding=DEFAULT_ENCODING,
1911 /,
1912 params: Optional[dict[str, Any]] = None,
1913 ):
1914 if params is None:
1915 params = {}
1916 value = to_unicode(value, encoding=encoding)
1917 self = super().__new__(cls, value)
1918 self.params = Parameters(params)
1919 return self
1921 def to_ical(self):
1922 return self.encode(DEFAULT_ENCODING)
1924 @classmethod
1925 def from_ical(cls, ical):
1926 return cls(ical)
1929class TypesFactory(CaselessDict):
1930 """All Value types defined in RFC 5545 are registered in this factory
1931 class.
1933 The value and parameter names don't overlap. So one factory is enough for
1934 both kinds.
1935 """
1937 def __init__(self, *args, **kwargs):
1938 """Set keys to upper for initial dict"""
1939 super().__init__(*args, **kwargs)
1940 self.all_types = (
1941 vBinary,
1942 vBoolean,
1943 vCalAddress,
1944 vDDDLists,
1945 vDDDTypes,
1946 vDate,
1947 vDatetime,
1948 vDuration,
1949 vFloat,
1950 vFrequency,
1951 vGeo,
1952 vInline,
1953 vInt,
1954 vPeriod,
1955 vRecur,
1956 vText,
1957 vTime,
1958 vUTCOffset,
1959 vUri,
1960 vWeekday,
1961 vCategory,
1962 )
1963 self["binary"] = vBinary
1964 self["boolean"] = vBoolean
1965 self["cal-address"] = vCalAddress
1966 self["date"] = vDDDTypes
1967 self["date-time"] = vDDDTypes
1968 self["duration"] = vDDDTypes
1969 self["float"] = vFloat
1970 self["integer"] = vInt
1971 self["period"] = vPeriod
1972 self["recur"] = vRecur
1973 self["text"] = vText
1974 self["time"] = vTime
1975 self["uri"] = vUri
1976 self["utc-offset"] = vUTCOffset
1977 self["geo"] = vGeo
1978 self["inline"] = vInline
1979 self["date-time-list"] = vDDDLists
1980 self["categories"] = vCategory
1982 #################################################
1983 # Property types
1985 # These are the default types
1986 types_map = CaselessDict(
1987 {
1988 ####################################
1989 # Property value types
1990 # Calendar Properties
1991 "calscale": "text",
1992 "method": "text",
1993 "prodid": "text",
1994 "version": "text",
1995 # Descriptive Component Properties
1996 "attach": "uri",
1997 "categories": "categories",
1998 "class": "text",
1999 "comment": "text",
2000 "description": "text",
2001 "geo": "geo",
2002 "location": "text",
2003 "percent-complete": "integer",
2004 "priority": "integer",
2005 "resources": "text",
2006 "status": "text",
2007 "summary": "text",
2008 # Date and Time Component Properties
2009 "completed": "date-time",
2010 "dtend": "date-time",
2011 "due": "date-time",
2012 "dtstart": "date-time",
2013 "duration": "duration",
2014 "freebusy": "period",
2015 "transp": "text",
2016 # Time Zone Component Properties
2017 "tzid": "text",
2018 "tzname": "text",
2019 "tzoffsetfrom": "utc-offset",
2020 "tzoffsetto": "utc-offset",
2021 "tzurl": "uri",
2022 # Relationship Component Properties
2023 "attendee": "cal-address",
2024 "contact": "text",
2025 "organizer": "cal-address",
2026 "recurrence-id": "date-time",
2027 "related-to": "text",
2028 "url": "uri",
2029 "uid": "text",
2030 # Recurrence Component Properties
2031 "exdate": "date-time-list",
2032 "exrule": "recur",
2033 "rdate": "date-time-list",
2034 "rrule": "recur",
2035 # Alarm Component Properties
2036 "action": "text",
2037 "repeat": "integer",
2038 "trigger": "duration",
2039 "acknowledged": "date-time",
2040 # Change Management Component Properties
2041 "created": "date-time",
2042 "dtstamp": "date-time",
2043 "last-modified": "date-time",
2044 "sequence": "integer",
2045 # Miscellaneous Component Properties
2046 "request-status": "text",
2047 ####################################
2048 # parameter types (luckily there is no name overlap)
2049 "altrep": "uri",
2050 "cn": "text",
2051 "cutype": "text",
2052 "delegated-from": "cal-address",
2053 "delegated-to": "cal-address",
2054 "dir": "uri",
2055 "encoding": "text",
2056 "fmttype": "text",
2057 "fbtype": "text",
2058 "language": "text",
2059 "member": "cal-address",
2060 "partstat": "text",
2061 "range": "text",
2062 "related": "text",
2063 "reltype": "text",
2064 "role": "text",
2065 "rsvp": "boolean",
2066 "sent-by": "cal-address",
2067 "value": "text",
2068 }
2069 )
2071 def for_property(self, name):
2072 """Returns a the default type for a property or parameter"""
2073 return self[self.types_map.get(name, "text")]
2075 def to_ical(self, name, value):
2076 """Encodes a named value from a primitive python type to an icalendar
2077 encoded string.
2078 """
2079 type_class = self.for_property(name)
2080 return type_class(value).to_ical()
2082 def from_ical(self, name, value):
2083 """Decodes a named property or parameter value from an icalendar
2084 encoded string to a primitive python type.
2085 """
2086 type_class = self.for_property(name)
2087 return type_class.from_ical(value)
2090__all__ = [
2091 "DURATION_REGEX",
2092 "WEEKDAY_RULE",
2093 "TimeBase",
2094 "TypesFactory",
2095 "tzid_from_dt",
2096 "tzid_from_tzinfo",
2097 "vBinary",
2098 "vBoolean",
2099 "vCalAddress",
2100 "vCategory",
2101 "vDDDLists",
2102 "vDDDTypes",
2103 "vDate",
2104 "vDatetime",
2105 "vDuration",
2106 "vFloat",
2107 "vFrequency",
2108 "vGeo",
2109 "vInline",
2110 "vInt",
2111 "vMonth",
2112 "vPeriod",
2113 "vRecur",
2114 "vSkip",
2115 "vText",
2116 "vTime",
2117 "vUTCOffset",
2118 "vUri",
2119 "vWeekday",
2120]