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