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, 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: dict[str, Any] | None = 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: dict[str, Any] | None = 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 r"""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: dict[str, Any] | None = 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: dict[str, Any] | None = 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: dict[str, Any] | None = 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: dict[str, Any] | None = 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 """Date-Time
812
813 Value Name: DATE-TIME
814
815 Purpose: This value type is used to identify values that specify a
816 precise calendar date and time of day. The format is based on
817 the ISO.8601.2004 complete representation.
818
819 Format:
820 YYYYMMDDTHHMMSS
821
822 Descripiton: vDatetime is timezone aware and uses a timezone library.
823 When a vDatetime object is created from an
824 ical string, you can pass a valid timezone identifier. When a
825 vDatetime object is created from a python datetime object, it uses the
826 tzinfo component, if present. Otherwise a timezone-naive object is
827 created. Be aware that there are certain limitations with timezone naive
828 DATE-TIME components in the icalendar standard.
829
830 Example 1: The following represents March 2, 2021 at 10:15 with local time:
831
832 .. code-block:: pycon
833
834 >>> from icalendar import vDatetime
835 >>> datetime = vDatetime.from_ical("20210302T101500")
836 >>> datetime.tzname()
837 >>> datetime.year
838 2021
839 >>> datetime.minute
840 15
841
842 Example 2: The following represents March 2, 2021 at 10:15 in New York:
843
844 .. code-block:: pycon
845
846 >>> datetime = vDatetime.from_ical("20210302T101500", 'America/New_York')
847 >>> datetime.tzname()
848 'EST'
849
850 Example 3: The following represents March 2, 2021 at 10:15 in Berlin:
851
852 .. code-block:: pycon
853
854 >>> from zoneinfo import ZoneInfo
855 >>> timezone = ZoneInfo("Europe/Berlin")
856 >>> vDatetime.from_ical("20210302T101500", timezone)
857 datetime.datetime(2021, 3, 2, 10, 15, tzinfo=ZoneInfo(key='Europe/Berlin'))
858 """
859
860 params: Parameters
861
862 def __init__(self, dt, /, params: dict[str, Any] | None = None):
863 if params is None:
864 params = {}
865 self.dt = dt
866 self.params = Parameters(params)
867
868 def to_ical(self):
869 dt = self.dt
870 tzid = tzid_from_dt(dt)
871
872 s = (
873 f"{dt.year:04}{dt.month:02}{dt.day:02}"
874 f"T{dt.hour:02}{dt.minute:02}{dt.second:02}"
875 )
876 if tzid == "UTC":
877 s += "Z"
878 elif tzid:
879 self.params.update({"TZID": tzid})
880 return s.encode("utf-8")
881
882 @staticmethod
883 def from_ical(ical, timezone=None):
884 """Create a datetime from the RFC string."""
885 tzinfo = None
886 if isinstance(timezone, str):
887 tzinfo = tzp.timezone(timezone)
888 elif timezone is not None:
889 tzinfo = timezone
890
891 try:
892 timetuple = (
893 int(ical[:4]), # year
894 int(ical[4:6]), # month
895 int(ical[6:8]), # day
896 int(ical[9:11]), # hour
897 int(ical[11:13]), # minute
898 int(ical[13:15]), # second
899 )
900 if tzinfo:
901 return tzp.localize(datetime(*timetuple), tzinfo) # noqa: DTZ001
902 if not ical[15:]:
903 return datetime(*timetuple) # noqa: DTZ001
904 if ical[15:16] == "Z":
905 return tzp.localize_utc(datetime(*timetuple)) # noqa: DTZ001
906 except Exception as e:
907 raise ValueError(f"Wrong datetime format: {ical}") from e
908 raise ValueError(f"Wrong datetime format: {ical}")
909
910
911class vDuration(TimeBase):
912 """Duration
913
914 Value Name:
915 DURATION
916
917 Purpose:
918 This value type is used to identify properties that contain
919 a duration of time.
920
921 Format Definition:
922 This value type is defined by the following notation:
923
924 .. code-block:: text
925
926 dur-value = (["+"] / "-") "P" (dur-date / dur-time / dur-week)
927
928 dur-date = dur-day [dur-time]
929 dur-time = "T" (dur-hour / dur-minute / dur-second)
930 dur-week = 1*DIGIT "W"
931 dur-hour = 1*DIGIT "H" [dur-minute]
932 dur-minute = 1*DIGIT "M" [dur-second]
933 dur-second = 1*DIGIT "S"
934 dur-day = 1*DIGIT "D"
935
936 Description:
937 If the property permits, multiple "duration" values are
938 specified by a COMMA-separated list of values. The format is
939 based on the [ISO.8601.2004] complete representation basic format
940 with designators for the duration of time. The format can
941 represent nominal durations (weeks and days) and accurate
942 durations (hours, minutes, and seconds). Note that unlike
943 [ISO.8601.2004], this value type doesn't support the "Y" and "M"
944 designators to specify durations in terms of years and months.
945 The duration of a week or a day depends on its position in the
946 calendar. In the case of discontinuities in the time scale, such
947 as the change from standard time to daylight time and back, the
948 computation of the exact duration requires the subtraction or
949 addition of the change of duration of the discontinuity. Leap
950 seconds MUST NOT be considered when computing an exact duration.
951 When computing an exact duration, the greatest order time
952 components MUST be added first, that is, the number of days MUST
953 be added first, followed by the number of hours, number of
954 minutes, and number of seconds.
955
956 Example:
957 A duration of 15 days, 5 hours, and 20 seconds would be:
958
959 .. code-block:: text
960
961 P15DT5H0M20S
962
963 A duration of 7 weeks would be:
964
965 .. code-block:: text
966
967 P7W
968
969 .. code-block:: pycon
970
971 >>> from icalendar.prop import vDuration
972 >>> duration = vDuration.from_ical('P15DT5H0M20S')
973 >>> duration
974 datetime.timedelta(days=15, seconds=18020)
975 >>> duration = vDuration.from_ical('P7W')
976 >>> duration
977 datetime.timedelta(days=49)
978 """
979
980 params: Parameters
981
982 def __init__(self, td, /, params: dict[str, Any] | None = None):
983 if params is None:
984 params = {}
985 if not isinstance(td, timedelta):
986 raise TypeError("Value MUST be a timedelta instance")
987 self.td = td
988 self.params = Parameters(params)
989
990 def to_ical(self):
991 sign = ""
992 td = self.td
993 if td.days < 0:
994 sign = "-"
995 td = -td
996 timepart = ""
997 if td.seconds:
998 timepart = "T"
999 hours = td.seconds // 3600
1000 minutes = td.seconds % 3600 // 60
1001 seconds = td.seconds % 60
1002 if hours:
1003 timepart += f"{hours}H"
1004 if minutes or (hours and seconds):
1005 timepart += f"{minutes}M"
1006 if seconds:
1007 timepart += f"{seconds}S"
1008 if td.days == 0 and timepart:
1009 return str(sign).encode("utf-8") + b"P" + str(timepart).encode("utf-8")
1010 return (
1011 str(sign).encode("utf-8")
1012 + b"P"
1013 + str(abs(td.days)).encode("utf-8")
1014 + b"D"
1015 + str(timepart).encode("utf-8")
1016 )
1017
1018 @staticmethod
1019 def from_ical(ical):
1020 match = DURATION_REGEX.match(ical)
1021 if not match:
1022 raise ValueError(f"Invalid iCalendar duration: {ical}")
1023
1024 sign, weeks, days, hours, minutes, seconds = match.groups()
1025 value = timedelta(
1026 weeks=int(weeks or 0),
1027 days=int(days or 0),
1028 hours=int(hours or 0),
1029 minutes=int(minutes or 0),
1030 seconds=int(seconds or 0),
1031 )
1032
1033 if sign == "-":
1034 value = -value
1035
1036 return value
1037
1038 @property
1039 def dt(self) -> timedelta:
1040 """The time delta for compatibility."""
1041 return self.td
1042
1043
1044class vPeriod(TimeBase):
1045 """Period of Time
1046
1047 Value Name:
1048 PERIOD
1049
1050 Purpose:
1051 This value type is used to identify values that contain a
1052 precise period of time.
1053
1054 Format Definition:
1055 This value type is defined by the following notation:
1056
1057 .. code-block:: text
1058
1059 period = period-explicit / period-start
1060
1061 period-explicit = date-time "/" date-time
1062 ; [ISO.8601.2004] complete representation basic format for a
1063 ; period of time consisting of a start and end. The start MUST
1064 ; be before the end.
1065
1066 period-start = date-time "/" dur-value
1067 ; [ISO.8601.2004] complete representation basic format for a
1068 ; period of time consisting of a start and positive duration
1069 ; of time.
1070
1071 Description:
1072 If the property permits, multiple "period" values are
1073 specified by a COMMA-separated list of values. There are two
1074 forms of a period of time. First, a period of time is identified
1075 by its start and its end. This format is based on the
1076 [ISO.8601.2004] complete representation, basic format for "DATE-
1077 TIME" start of the period, followed by a SOLIDUS character
1078 followed by the "DATE-TIME" of the end of the period. The start
1079 of the period MUST be before the end of the period. Second, a
1080 period of time can also be defined by a start and a positive
1081 duration of time. The format is based on the [ISO.8601.2004]
1082 complete representation, basic format for the "DATE-TIME" start of
1083 the period, followed by a SOLIDUS character, followed by the
1084 [ISO.8601.2004] basic format for "DURATION" of the period.
1085
1086 Example:
1087 The period starting at 18:00:00 UTC, on January 1, 1997 and
1088 ending at 07:00:00 UTC on January 2, 1997 would be:
1089
1090 .. code-block:: text
1091
1092 19970101T180000Z/19970102T070000Z
1093
1094 The period start at 18:00:00 on January 1, 1997 and lasting 5 hours
1095 and 30 minutes would be:
1096
1097 .. code-block:: text
1098
1099 19970101T180000Z/PT5H30M
1100
1101 .. code-block:: pycon
1102
1103 >>> from icalendar.prop import vPeriod
1104 >>> period = vPeriod.from_ical('19970101T180000Z/19970102T070000Z')
1105 >>> period = vPeriod.from_ical('19970101T180000Z/PT5H30M')
1106 """
1107
1108 params: Parameters
1109
1110 def __init__(self, per: tuple[datetime, Union[datetime, timedelta]]):
1111 start, end_or_duration = per
1112 if not (isinstance(start, (datetime, date))):
1113 raise TypeError("Start value MUST be a datetime or date instance")
1114 if not (isinstance(end_or_duration, (datetime, date, timedelta))):
1115 raise TypeError(
1116 "end_or_duration MUST be a datetime, date or timedelta instance"
1117 )
1118 by_duration = 0
1119 if isinstance(end_or_duration, timedelta):
1120 by_duration = 1
1121 duration = end_or_duration
1122 end = start + duration
1123 else:
1124 end = end_or_duration
1125 duration = end - start
1126 if start > end:
1127 raise ValueError("Start time is greater than end time")
1128
1129 self.params = Parameters({"value": "PERIOD"})
1130 # set the timezone identifier
1131 # does not support different timezones for start and end
1132 tzid = tzid_from_dt(start)
1133 if tzid:
1134 self.params["TZID"] = tzid
1135
1136 self.start = start
1137 self.end = end
1138 self.by_duration = by_duration
1139 self.duration = duration
1140
1141 def overlaps(self, other):
1142 if self.start > other.start:
1143 return other.overlaps(self)
1144 return self.start <= other.start < self.end
1145
1146 def to_ical(self):
1147 if self.by_duration:
1148 return (
1149 vDatetime(self.start).to_ical()
1150 + b"/"
1151 + vDuration(self.duration).to_ical()
1152 )
1153 return vDatetime(self.start).to_ical() + b"/" + vDatetime(self.end).to_ical()
1154
1155 @staticmethod
1156 def from_ical(ical, timezone=None):
1157 try:
1158 start, end_or_duration = ical.split("/")
1159 start = vDDDTypes.from_ical(start, timezone=timezone)
1160 end_or_duration = vDDDTypes.from_ical(end_or_duration, timezone=timezone)
1161 except Exception as e:
1162 raise ValueError(f"Expected period format, got: {ical}") from e
1163 return (start, end_or_duration)
1164
1165 def __repr__(self):
1166 p = (self.start, self.duration) if self.by_duration else (self.start, self.end)
1167 return f"vPeriod({p!r})"
1168
1169 @property
1170 def dt(self):
1171 """Make this cooperate with the other vDDDTypes."""
1172 return (self.start, (self.duration if self.by_duration else self.end))
1173
1174 from icalendar.param import FBTYPE
1175
1176
1177class vWeekday(str):
1178 """Either a ``weekday`` or a ``weekdaynum``
1179
1180 .. code-block:: pycon
1181
1182 >>> from icalendar import vWeekday
1183 >>> vWeekday("MO") # Simple weekday
1184 'MO'
1185 >>> vWeekday("2FR").relative # Second friday
1186 2
1187 >>> vWeekday("2FR").weekday
1188 'FR'
1189 >>> vWeekday("-1SU").relative # Last Sunday
1190 -1
1191
1192 Definition from `RFC 5545, Section 3.3.10 <https://www.rfc-editor.org/rfc/rfc5545#section-3.3.10>`_:
1193
1194 .. code-block:: text
1195
1196 weekdaynum = [[plus / minus] ordwk] weekday
1197 plus = "+"
1198 minus = "-"
1199 ordwk = 1*2DIGIT ;1 to 53
1200 weekday = "SU" / "MO" / "TU" / "WE" / "TH" / "FR" / "SA"
1201 ;Corresponding to SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY,
1202 ;FRIDAY, and SATURDAY days of the week.
1203
1204 """
1205
1206 params: Parameters
1207 __slots__ = ("params", "relative", "weekday")
1208
1209 week_days = CaselessDict(
1210 {
1211 "SU": 0,
1212 "MO": 1,
1213 "TU": 2,
1214 "WE": 3,
1215 "TH": 4,
1216 "FR": 5,
1217 "SA": 6,
1218 }
1219 )
1220
1221 def __new__(
1222 cls,
1223 value,
1224 encoding=DEFAULT_ENCODING,
1225 /,
1226 params: dict[str, Any] | None = None,
1227 ):
1228 if params is None:
1229 params = {}
1230 value = to_unicode(value, encoding=encoding)
1231 self = super().__new__(cls, value)
1232 match = WEEKDAY_RULE.match(self)
1233 if match is None:
1234 raise ValueError(f"Expected weekday abbrevation, got: {self}")
1235 match = match.groupdict()
1236 sign = match["signal"]
1237 weekday = match["weekday"]
1238 relative = match["relative"]
1239 if weekday not in vWeekday.week_days or sign not in "+-":
1240 raise ValueError(f"Expected weekday abbrevation, got: {self}")
1241 self.weekday = weekday or None
1242 self.relative = (relative and int(relative)) or None
1243 if sign == "-" and self.relative:
1244 self.relative *= -1
1245 self.params = Parameters(params)
1246 return self
1247
1248 def to_ical(self):
1249 return self.encode(DEFAULT_ENCODING).upper()
1250
1251 @classmethod
1252 def from_ical(cls, ical):
1253 try:
1254 return cls(ical.upper())
1255 except Exception as e:
1256 raise ValueError(f"Expected weekday abbrevation, got: {ical}") from e
1257
1258
1259class vFrequency(str):
1260 """A simple class that catches illegal values."""
1261
1262 params: Parameters
1263 __slots__ = ("params",)
1264
1265 frequencies = CaselessDict(
1266 {
1267 "SECONDLY": "SECONDLY",
1268 "MINUTELY": "MINUTELY",
1269 "HOURLY": "HOURLY",
1270 "DAILY": "DAILY",
1271 "WEEKLY": "WEEKLY",
1272 "MONTHLY": "MONTHLY",
1273 "YEARLY": "YEARLY",
1274 }
1275 )
1276
1277 def __new__(
1278 cls,
1279 value,
1280 encoding=DEFAULT_ENCODING,
1281 /,
1282 params: dict[str, Any] | None = None,
1283 ):
1284 if params is None:
1285 params = {}
1286 value = to_unicode(value, encoding=encoding)
1287 self = super().__new__(cls, value)
1288 if self not in vFrequency.frequencies:
1289 raise ValueError(f"Expected frequency, got: {self}")
1290 self.params = Parameters(params)
1291 return self
1292
1293 def to_ical(self):
1294 return self.encode(DEFAULT_ENCODING).upper()
1295
1296 @classmethod
1297 def from_ical(cls, ical):
1298 try:
1299 return cls(ical.upper())
1300 except Exception as e:
1301 raise ValueError(f"Expected frequency, got: {ical}") from e
1302
1303
1304class vMonth(int):
1305 """The number of the month for recurrence.
1306
1307 In :rfc:`5545`, this is just an int.
1308 In :rfc:`7529`, this can be followed by `L` to indicate a leap month.
1309
1310 .. code-block:: pycon
1311
1312 >>> from icalendar import vMonth
1313 >>> vMonth(1) # first month January
1314 vMonth('1')
1315 >>> vMonth("5L") # leap month in Hebrew calendar
1316 vMonth('5L')
1317 >>> vMonth(1).leap
1318 False
1319 >>> vMonth("5L").leap
1320 True
1321
1322 Definition from RFC:
1323
1324 .. code-block:: text
1325
1326 type-bymonth = element bymonth {
1327 xsd:positiveInteger |
1328 xsd:string
1329 }
1330 """
1331
1332 params: Parameters
1333
1334 def __new__(
1335 cls, month: Union[str, int], /, params: dict[str, Any] | None = None
1336 ):
1337 if params is None:
1338 params = {}
1339 if isinstance(month, vMonth):
1340 return cls(month.to_ical().decode())
1341 if isinstance(month, str):
1342 if month.isdigit():
1343 month_index = int(month)
1344 leap = False
1345 else:
1346 if month[-1] != "L" and month[:-1].isdigit():
1347 raise ValueError(f"Invalid month: {month!r}")
1348 month_index = int(month[:-1])
1349 leap = True
1350 else:
1351 leap = False
1352 month_index = int(month)
1353 self = super().__new__(cls, month_index)
1354 self.leap = leap
1355 self.params = Parameters(params)
1356 return self
1357
1358 def to_ical(self) -> bytes:
1359 """The ical representation."""
1360 return str(self).encode("utf-8")
1361
1362 @classmethod
1363 def from_ical(cls, ical: str):
1364 return cls(ical)
1365
1366 @property
1367 def leap(self) -> bool:
1368 """Whether this is a leap month."""
1369 return self._leap
1370
1371 @leap.setter
1372 def leap(self, value: bool) -> None:
1373 self._leap = value
1374
1375 def __repr__(self) -> str:
1376 """repr(self)"""
1377 return f"{self.__class__.__name__}({str(self)!r})"
1378
1379 def __str__(self) -> str:
1380 """str(self)"""
1381 return f"{int(self)}{'L' if self.leap else ''}"
1382
1383
1384class vSkip(vText, Enum):
1385 """Skip values for RRULE.
1386
1387 These are defined in :rfc:`7529`.
1388
1389 OMIT is the default value.
1390
1391 Examples:
1392
1393 .. code-block:: pycon
1394
1395 >>> from icalendar import vSkip
1396 >>> vSkip.OMIT
1397 vSkip('OMIT')
1398 >>> vSkip.FORWARD
1399 vSkip('FORWARD')
1400 >>> vSkip.BACKWARD
1401 vSkip('BACKWARD')
1402 """
1403
1404 OMIT = "OMIT"
1405 FORWARD = "FORWARD"
1406 BACKWARD = "BACKWARD"
1407
1408 __reduce_ex__ = Enum.__reduce_ex__
1409
1410 def __repr__(self):
1411 return f"{self.__class__.__name__}({self._name_!r})"
1412
1413
1414class vRecur(CaselessDict):
1415 """Recurrence definition.
1416
1417 Property Name:
1418 RRULE
1419
1420 Purpose:
1421 This property defines a rule or repeating pattern for recurring events, to-dos,
1422 journal entries, or time zone definitions.
1423
1424 Value Type:
1425 RECUR
1426
1427 Property Parameters:
1428 IANA and non-standard property parameters can be specified on this property.
1429
1430 Conformance:
1431 This property can be specified in recurring "VEVENT", "VTODO", and "VJOURNAL"
1432 calendar components as well as in the "STANDARD" and "DAYLIGHT" sub-components
1433 of the "VTIMEZONE" calendar component, but it SHOULD NOT be specified more than once.
1434 The recurrence set generated with multiple "RRULE" properties is undefined.
1435
1436 Description:
1437 The recurrence rule, if specified, is used in computing the recurrence set.
1438 The recurrence set is the complete set of recurrence instances for a calendar component.
1439 The recurrence set is generated by considering the initial "DTSTART" property along
1440 with the "RRULE", "RDATE", and "EXDATE" properties contained within the
1441 recurring component. The "DTSTART" property defines the first instance in the
1442 recurrence set. The "DTSTART" property value SHOULD be synchronized with the
1443 recurrence rule, if specified. The recurrence set generated with a "DTSTART" property
1444 value not synchronized with the recurrence rule is undefined.
1445 The final recurrence set is generated by gathering all of the start DATE-TIME
1446 values generated by any of the specified "RRULE" and "RDATE" properties, and then
1447 excluding any start DATE-TIME values specified by "EXDATE" properties.
1448 This implies that start DATE- TIME values specified by "EXDATE" properties take
1449 precedence over those specified by inclusion properties (i.e., "RDATE" and "RRULE").
1450 Where duplicate instances are generated by the "RRULE" and "RDATE" properties,
1451 only one recurrence is considered. Duplicate instances are ignored.
1452
1453 The "DTSTART" property specified within the iCalendar object defines the first
1454 instance of the recurrence. In most cases, a "DTSTART" property of DATE-TIME value
1455 type used with a recurrence rule, should be specified as a date with local time
1456 and time zone reference to make sure all the recurrence instances start at the
1457 same local time regardless of time zone changes.
1458
1459 If the duration of the recurring component is specified with the "DTEND" or
1460 "DUE" property, then the same exact duration will apply to all the members of the
1461 generated recurrence set. Else, if the duration of the recurring component is
1462 specified with the "DURATION" property, then the same nominal duration will apply
1463 to all the members of the generated recurrence set and the exact duration of each
1464 recurrence instance will depend on its specific start time. For example, recurrence
1465 instances of a nominal duration of one day will have an exact duration of more or less
1466 than 24 hours on a day where a time zone shift occurs. The duration of a specific
1467 recurrence may be modified in an exception component or simply by using an
1468 "RDATE" property of PERIOD value type.
1469
1470 Examples:
1471 The following RRULE specifies daily events for 10 occurrences.
1472
1473 .. code-block:: text
1474
1475 RRULE:FREQ=DAILY;COUNT=10
1476
1477 Below, we parse the RRULE ical string.
1478
1479 .. code-block:: pycon
1480
1481 >>> from icalendar.prop import vRecur
1482 >>> rrule = vRecur.from_ical('FREQ=DAILY;COUNT=10')
1483 >>> rrule
1484 vRecur({'FREQ': ['DAILY'], 'COUNT': [10]})
1485
1486 You can choose to add an rrule to an :class:`icalendar.cal.Event` or
1487 :class:`icalendar.cal.Todo`.
1488
1489 .. code-block:: pycon
1490
1491 >>> from icalendar import Event
1492 >>> event = Event()
1493 >>> event.add('RRULE', 'FREQ=DAILY;COUNT=10')
1494 >>> event.rrules
1495 [vRecur({'FREQ': ['DAILY'], 'COUNT': [10]})]
1496 """ # noqa: E501
1497
1498 params: Parameters
1499
1500 frequencies = [
1501 "SECONDLY",
1502 "MINUTELY",
1503 "HOURLY",
1504 "DAILY",
1505 "WEEKLY",
1506 "MONTHLY",
1507 "YEARLY",
1508 ]
1509
1510 # Mac iCal ignores RRULEs where FREQ is not the first rule part.
1511 # Sorts parts according to the order listed in RFC 5545, section 3.3.10.
1512 canonical_order = (
1513 "RSCALE",
1514 "FREQ",
1515 "UNTIL",
1516 "COUNT",
1517 "INTERVAL",
1518 "BYSECOND",
1519 "BYMINUTE",
1520 "BYHOUR",
1521 "BYDAY",
1522 "BYWEEKDAY",
1523 "BYMONTHDAY",
1524 "BYYEARDAY",
1525 "BYWEEKNO",
1526 "BYMONTH",
1527 "BYSETPOS",
1528 "WKST",
1529 "SKIP",
1530 )
1531
1532 types = CaselessDict(
1533 {
1534 "COUNT": vInt,
1535 "INTERVAL": vInt,
1536 "BYSECOND": vInt,
1537 "BYMINUTE": vInt,
1538 "BYHOUR": vInt,
1539 "BYWEEKNO": vInt,
1540 "BYMONTHDAY": vInt,
1541 "BYYEARDAY": vInt,
1542 "BYMONTH": vMonth,
1543 "UNTIL": vDDDTypes,
1544 "BYSETPOS": vInt,
1545 "WKST": vWeekday,
1546 "BYDAY": vWeekday,
1547 "FREQ": vFrequency,
1548 "BYWEEKDAY": vWeekday,
1549 "SKIP": vSkip,
1550 }
1551 )
1552
1553 def __init__(self, *args, params: dict[str, Any] | None = None, **kwargs):
1554 if params is None:
1555 params = {}
1556 if args and isinstance(args[0], str):
1557 # we have a string as an argument.
1558 args = (self.from_ical(args[0]),) + args[1:]
1559 for k, v in kwargs.items():
1560 if not isinstance(v, SEQUENCE_TYPES):
1561 kwargs[k] = [v]
1562 super().__init__(*args, **kwargs)
1563 self.params = Parameters(params)
1564
1565 def to_ical(self):
1566 result = []
1567 for key, vals in self.sorted_items():
1568 typ = self.types.get(key, vText)
1569 if not isinstance(vals, SEQUENCE_TYPES):
1570 vals = [vals] # noqa: PLW2901
1571 param_vals = b",".join(typ(val).to_ical() for val in vals)
1572
1573 # CaselessDict keys are always unicode
1574 param_key = key.encode(DEFAULT_ENCODING)
1575 result.append(param_key + b"=" + param_vals)
1576
1577 return b";".join(result)
1578
1579 @classmethod
1580 def parse_type(cls, key, values):
1581 # integers
1582 parser = cls.types.get(key, vText)
1583 return [parser.from_ical(v) for v in values.split(",")]
1584
1585 @classmethod
1586 def from_ical(cls, ical: str):
1587 if isinstance(ical, cls):
1588 return ical
1589 try:
1590 recur = cls()
1591 for pairs in ical.split(";"):
1592 try:
1593 key, vals = pairs.split("=")
1594 except ValueError:
1595 # E.g. incorrect trailing semicolon, like (issue #157):
1596 # FREQ=YEARLY;BYMONTH=11;BYDAY=1SU;
1597 continue
1598 recur[key] = cls.parse_type(key, vals)
1599 return cls(recur)
1600 except ValueError:
1601 raise
1602 except Exception as e:
1603 raise ValueError(f"Error in recurrence rule: {ical}") from e
1604
1605
1606class vTime(TimeBase):
1607 """Time
1608
1609 Value Name:
1610 TIME
1611
1612 Purpose:
1613 This value type is used to identify values that contain a
1614 time of day.
1615
1616 Format Definition:
1617 This value type is defined by the following notation:
1618
1619 .. code-block:: text
1620
1621 time = time-hour time-minute time-second [time-utc]
1622
1623 time-hour = 2DIGIT ;00-23
1624 time-minute = 2DIGIT ;00-59
1625 time-second = 2DIGIT ;00-60
1626 ;The "60" value is used to account for positive "leap" seconds.
1627
1628 time-utc = "Z"
1629
1630 Description:
1631 If the property permits, multiple "time" values are
1632 specified by a COMMA-separated list of values. No additional
1633 content value encoding (i.e., BACKSLASH character encoding, see
1634 vText) is defined for this value type.
1635
1636 The "TIME" value type is used to identify values that contain a
1637 time of day. The format is based on the [ISO.8601.2004] complete
1638 representation, basic format for a time of day. The text format
1639 consists of a two-digit, 24-hour of the day (i.e., values 00-23),
1640 two-digit minute in the hour (i.e., values 00-59), and two-digit
1641 seconds in the minute (i.e., values 00-60). The seconds value of
1642 60 MUST only be used to account for positive "leap" seconds.
1643 Fractions of a second are not supported by this format.
1644
1645 In parallel to the "DATE-TIME" definition above, the "TIME" value
1646 type expresses time values in three forms:
1647
1648 The form of time with UTC offset MUST NOT be used. For example,
1649 the following is not valid for a time value:
1650
1651 .. code-block:: text
1652
1653 230000-0800 ;Invalid time format
1654
1655 **FORM #1 LOCAL TIME**
1656
1657 The local time form is simply a time value that does not contain
1658 the UTC designator nor does it reference a time zone. For
1659 example, 11:00 PM:
1660
1661 .. code-block:: text
1662
1663 230000
1664
1665 Time values of this type are said to be "floating" and are not
1666 bound to any time zone in particular. They are used to represent
1667 the same hour, minute, and second value regardless of which time
1668 zone is currently being observed. For example, an event can be
1669 defined that indicates that an individual will be busy from 11:00
1670 AM to 1:00 PM every day, no matter which time zone the person is
1671 in. In these cases, a local time can be specified. The recipient
1672 of an iCalendar object with a property value consisting of a local
1673 time, without any relative time zone information, SHOULD interpret
1674 the value as being fixed to whatever time zone the "ATTENDEE" is
1675 in at any given moment. This means that two "Attendees", may
1676 participate in the same event at different UTC times; floating
1677 time SHOULD only be used where that is reasonable behavior.
1678
1679 In most cases, a fixed time is desired. To properly communicate a
1680 fixed time in a property value, either UTC time or local time with
1681 time zone reference MUST be specified.
1682
1683 The use of local time in a TIME value without the "TZID" property
1684 parameter is to be interpreted as floating time, regardless of the
1685 existence of "VTIMEZONE" calendar components in the iCalendar
1686 object.
1687
1688 **FORM #2: UTC TIME**
1689
1690 UTC time, or absolute time, is identified by a LATIN CAPITAL
1691 LETTER Z suffix character, the UTC designator, appended to the
1692 time value. For example, the following represents 07:00 AM UTC:
1693
1694 .. code-block:: text
1695
1696 070000Z
1697
1698 The "TZID" property parameter MUST NOT be applied to TIME
1699 properties whose time values are specified in UTC.
1700
1701 **FORM #3: LOCAL TIME AND TIME ZONE REFERENCE**
1702
1703 The local time with reference to time zone information form is
1704 identified by the use the "TZID" property parameter to reference
1705 the appropriate time zone definition.
1706
1707 Example:
1708 The following represents 8:30 AM in New York in winter,
1709 five hours behind UTC, in each of the three formats:
1710
1711 .. code-block:: text
1712
1713 083000
1714 133000Z
1715 TZID=America/New_York:083000
1716 """
1717
1718 def __init__(self, *args):
1719 if len(args) == 1:
1720 if not isinstance(args[0], (time, datetime)):
1721 raise ValueError(f"Expected a datetime.time, got: {args[0]}")
1722 self.dt = args[0]
1723 else:
1724 self.dt = time(*args)
1725 self.params = Parameters({"value": "TIME"})
1726
1727 def to_ical(self):
1728 return self.dt.strftime("%H%M%S")
1729
1730 @staticmethod
1731 def from_ical(ical):
1732 # TODO: timezone support
1733 try:
1734 timetuple = (int(ical[:2]), int(ical[2:4]), int(ical[4:6]))
1735 return time(*timetuple)
1736 except Exception as e:
1737 raise ValueError(f"Expected time, got: {ical}") from e
1738
1739
1740class vUri(str):
1741 """URI
1742
1743 Value Name:
1744 URI
1745
1746 Purpose:
1747 This value type is used to identify values that contain a
1748 uniform resource identifier (URI) type of reference to the
1749 property value.
1750
1751 Format Definition:
1752 This value type is defined by the following notation:
1753
1754 .. code-block:: text
1755
1756 uri = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
1757
1758 Description:
1759 This value type might be used to reference binary
1760 information, for values that are large, or otherwise undesirable
1761 to include directly in the iCalendar object.
1762
1763 Property values with this value type MUST follow the generic URI
1764 syntax defined in [RFC3986].
1765
1766 When a property parameter value is a URI value type, the URI MUST
1767 be specified as a quoted-string value.
1768
1769 Example:
1770 The following is a URI for a network file:
1771
1772 .. code-block:: text
1773
1774 http://example.com/my-report.txt
1775
1776 .. code-block:: pycon
1777
1778 >>> from icalendar.prop import vUri
1779 >>> uri = vUri.from_ical('http://example.com/my-report.txt')
1780 >>> uri
1781 'http://example.com/my-report.txt'
1782 """
1783
1784 params: Parameters
1785 __slots__ = ("params",)
1786
1787 def __new__(
1788 cls,
1789 value: str,
1790 encoding: str = DEFAULT_ENCODING,
1791 /,
1792 params: dict[str, Any] | None = None,
1793 ):
1794 if params is None:
1795 params = {}
1796 value = to_unicode(value, encoding=encoding)
1797 self = super().__new__(cls, value)
1798 self.params = Parameters(params)
1799 return self
1800
1801 def to_ical(self):
1802 return self.encode(DEFAULT_ENCODING)
1803
1804 @classmethod
1805 def from_ical(cls, ical):
1806 try:
1807 return cls(ical)
1808 except Exception as e:
1809 raise ValueError(f"Expected , got: {ical}") from e
1810
1811
1812class vGeo:
1813 """Geographic Position
1814
1815 Property Name:
1816 GEO
1817
1818 Purpose:
1819 This property specifies information related to the global
1820 position for the activity specified by a calendar component.
1821
1822 Value Type:
1823 FLOAT. The value MUST be two SEMICOLON-separated FLOAT values.
1824
1825 Property Parameters:
1826 IANA and non-standard property parameters can be specified on
1827 this property.
1828
1829 Conformance:
1830 This property can be specified in "VEVENT" or "VTODO"
1831 calendar components.
1832
1833 Description:
1834 This property value specifies latitude and longitude,
1835 in that order (i.e., "LAT LON" ordering). The longitude
1836 represents the location east or west of the prime meridian as a
1837 positive or negative real number, respectively. The longitude and
1838 latitude values MAY be specified up to six decimal places, which
1839 will allow for accuracy to within one meter of geographical
1840 position. Receiving applications MUST accept values of this
1841 precision and MAY truncate values of greater precision.
1842
1843 Example:
1844
1845 .. code-block:: text
1846
1847 GEO:37.386013;-122.082932
1848
1849 Parse vGeo:
1850
1851 .. code-block:: pycon
1852
1853 >>> from icalendar.prop import vGeo
1854 >>> geo = vGeo.from_ical('37.386013;-122.082932')
1855 >>> geo
1856 (37.386013, -122.082932)
1857
1858 Add a geo location to an event:
1859
1860 .. code-block:: pycon
1861
1862 >>> from icalendar import Event
1863 >>> event = Event()
1864 >>> latitude = 37.386013
1865 >>> longitude = -122.082932
1866 >>> event.add('GEO', (latitude, longitude))
1867 >>> event['GEO']
1868 vGeo((37.386013, -122.082932))
1869 """
1870
1871 params: Parameters
1872
1873 def __init__(
1874 self,
1875 geo: tuple[float | str | int, float | str | int],
1876 /,
1877 params: dict[str, Any] | None = None,
1878 ):
1879 """Create a new vGeo from a tuple of (latitude, longitude).
1880
1881 Raises:
1882 ValueError: if geo is not a tuple of (latitude, longitude)
1883 """
1884 if params is None:
1885 params = {}
1886 try:
1887 latitude, longitude = (geo[0], geo[1])
1888 latitude = float(latitude)
1889 longitude = float(longitude)
1890 except Exception as e:
1891 raise ValueError(
1892 "Input must be (float, float) for latitude and longitude"
1893 ) from e
1894 self.latitude = latitude
1895 self.longitude = longitude
1896 self.params = Parameters(params)
1897
1898 def to_ical(self):
1899 return f"{self.latitude};{self.longitude}"
1900
1901 @staticmethod
1902 def from_ical(ical):
1903 try:
1904 latitude, longitude = ical.split(";")
1905 return (float(latitude), float(longitude))
1906 except Exception as e:
1907 raise ValueError(f"Expected 'float;float' , got: {ical}") from e
1908
1909 def __eq__(self, other):
1910 return self.to_ical() == other.to_ical()
1911
1912 def __repr__(self):
1913 """repr(self)"""
1914 return f"{self.__class__.__name__}(({self.latitude}, {self.longitude}))"
1915
1916
1917class vUTCOffset:
1918 """UTC Offset
1919
1920 Value Name:
1921 UTC-OFFSET
1922
1923 Purpose:
1924 This value type is used to identify properties that contain
1925 an offset from UTC to local time.
1926
1927 Format Definition:
1928 This value type is defined by the following notation:
1929
1930 .. code-block:: text
1931
1932 utc-offset = time-numzone
1933
1934 time-numzone = ("+" / "-") time-hour time-minute [time-second]
1935
1936 Description:
1937 The PLUS SIGN character MUST be specified for positive
1938 UTC offsets (i.e., ahead of UTC). The HYPHEN-MINUS character MUST
1939 be specified for negative UTC offsets (i.e., behind of UTC). The
1940 value of "-0000" and "-000000" are not allowed. The time-second,
1941 if present, MUST NOT be 60; if absent, it defaults to zero.
1942
1943 Example:
1944 The following UTC offsets are given for standard time for
1945 New York (five hours behind UTC) and Geneva (one hour ahead of
1946 UTC):
1947
1948 .. code-block:: text
1949
1950 -0500
1951
1952 +0100
1953
1954 .. code-block:: pycon
1955
1956 >>> from icalendar.prop import vUTCOffset
1957 >>> utc_offset = vUTCOffset.from_ical('-0500')
1958 >>> utc_offset
1959 datetime.timedelta(days=-1, seconds=68400)
1960 >>> utc_offset = vUTCOffset.from_ical('+0100')
1961 >>> utc_offset
1962 datetime.timedelta(seconds=3600)
1963 """
1964
1965 params: Parameters
1966
1967 ignore_exceptions = False # if True, and we cannot parse this
1968
1969 # component, we will silently ignore
1970 # it, rather than let the exception
1971 # propagate upwards
1972
1973 def __init__(self, td, /, params: dict[str, Any] | None = None):
1974 if params is None:
1975 params = {}
1976 if not isinstance(td, timedelta):
1977 raise TypeError("Offset value MUST be a timedelta instance")
1978 self.td = td
1979 self.params = Parameters(params)
1980
1981 def to_ical(self):
1982 if self.td < timedelta(0):
1983 sign = "-%s"
1984 td = timedelta(0) - self.td # get timedelta relative to 0
1985 else:
1986 # Google Calendar rejects '0000' but accepts '+0000'
1987 sign = "+%s"
1988 td = self.td
1989
1990 days, seconds = td.days, td.seconds
1991
1992 hours = abs(days * 24 + seconds // 3600)
1993 minutes = abs((seconds % 3600) // 60)
1994 seconds = abs(seconds % 60)
1995 if seconds:
1996 duration = f"{hours:02}{minutes:02}{seconds:02}"
1997 else:
1998 duration = f"{hours:02}{minutes:02}"
1999 return sign % duration
2000
2001 @classmethod
2002 def from_ical(cls, ical):
2003 if isinstance(ical, cls):
2004 return ical.td
2005 try:
2006 sign, hours, minutes, seconds = (
2007 ical[0:1],
2008 int(ical[1:3]),
2009 int(ical[3:5]),
2010 int(ical[5:7] or 0),
2011 )
2012 offset = timedelta(hours=hours, minutes=minutes, seconds=seconds)
2013 except Exception as e:
2014 raise ValueError(f"Expected utc offset, got: {ical}") from e
2015 if not cls.ignore_exceptions and offset >= timedelta(hours=24):
2016 raise ValueError(f"Offset must be less than 24 hours, was {ical}")
2017 if sign == "-":
2018 return -offset
2019 return offset
2020
2021 def __eq__(self, other):
2022 if not isinstance(other, vUTCOffset):
2023 return False
2024 return self.td == other.td
2025
2026 def __hash__(self):
2027 return hash(self.td)
2028
2029 def __repr__(self):
2030 return f"vUTCOffset({self.td!r})"
2031
2032
2033class vInline(str):
2034 """This is an especially dumb class that just holds raw unparsed text and
2035 has parameters. Conversion of inline values are handled by the Component
2036 class, so no further processing is needed.
2037 """
2038
2039 params: Parameters
2040 __slots__ = ("params",)
2041
2042 def __new__(
2043 cls,
2044 value,
2045 encoding=DEFAULT_ENCODING,
2046 /,
2047 params: dict[str, Any] | None = None,
2048 ):
2049 if params is None:
2050 params = {}
2051 value = to_unicode(value, encoding=encoding)
2052 self = super().__new__(cls, value)
2053 self.params = Parameters(params)
2054 return self
2055
2056 def to_ical(self):
2057 return self.encode(DEFAULT_ENCODING)
2058
2059 @classmethod
2060 def from_ical(cls, ical):
2061 return cls(ical)
2062
2063
2064class TypesFactory(CaselessDict):
2065 """All Value types defined in RFC 5545 are registered in this factory
2066 class.
2067
2068 The value and parameter names don't overlap. So one factory is enough for
2069 both kinds.
2070 """
2071
2072 def __init__(self, *args, **kwargs):
2073 """Set keys to upper for initial dict"""
2074 super().__init__(*args, **kwargs)
2075 self.all_types = (
2076 vBinary,
2077 vBoolean,
2078 vCalAddress,
2079 vDDDLists,
2080 vDDDTypes,
2081 vDate,
2082 vDatetime,
2083 vDuration,
2084 vFloat,
2085 vFrequency,
2086 vGeo,
2087 vInline,
2088 vInt,
2089 vPeriod,
2090 vRecur,
2091 vText,
2092 vTime,
2093 vUTCOffset,
2094 vUri,
2095 vWeekday,
2096 vCategory,
2097 )
2098 self["binary"] = vBinary
2099 self["boolean"] = vBoolean
2100 self["cal-address"] = vCalAddress
2101 self["date"] = vDDDTypes
2102 self["date-time"] = vDDDTypes
2103 self["duration"] = vDDDTypes
2104 self["float"] = vFloat
2105 self["integer"] = vInt
2106 self["period"] = vPeriod
2107 self["recur"] = vRecur
2108 self["text"] = vText
2109 self["time"] = vTime
2110 self["uri"] = vUri
2111 self["utc-offset"] = vUTCOffset
2112 self["geo"] = vGeo
2113 self["inline"] = vInline
2114 self["date-time-list"] = vDDDLists
2115 self["categories"] = vCategory
2116
2117 #################################################
2118 # Property types
2119
2120 # These are the default types
2121 types_map = CaselessDict(
2122 {
2123 ####################################
2124 # Property value types
2125 # Calendar Properties
2126 "calscale": "text",
2127 "method": "text",
2128 "prodid": "text",
2129 "version": "text",
2130 # Descriptive Component Properties
2131 "attach": "uri",
2132 "categories": "categories",
2133 "class": "text",
2134 "comment": "text",
2135 "description": "text",
2136 "geo": "geo",
2137 "location": "text",
2138 "percent-complete": "integer",
2139 "priority": "integer",
2140 "resources": "text",
2141 "status": "text",
2142 "summary": "text",
2143 # Date and Time Component Properties
2144 "completed": "date-time",
2145 "dtend": "date-time",
2146 "due": "date-time",
2147 "dtstart": "date-time",
2148 "duration": "duration",
2149 "freebusy": "period",
2150 "transp": "text",
2151 "refresh-interval": "duration", # RFC 7986
2152 # Time Zone Component Properties
2153 "tzid": "text",
2154 "tzname": "text",
2155 "tzoffsetfrom": "utc-offset",
2156 "tzoffsetto": "utc-offset",
2157 "tzurl": "uri",
2158 # Relationship Component Properties
2159 "attendee": "cal-address",
2160 "contact": "text",
2161 "organizer": "cal-address",
2162 "recurrence-id": "date-time",
2163 "related-to": "text",
2164 "url": "uri",
2165 "conference": "uri", # RFC 7986
2166 "source": "uri",
2167 "uid": "text",
2168 # Recurrence Component Properties
2169 "exdate": "date-time-list",
2170 "exrule": "recur",
2171 "rdate": "date-time-list",
2172 "rrule": "recur",
2173 # Alarm Component Properties
2174 "action": "text",
2175 "repeat": "integer",
2176 "trigger": "duration",
2177 "acknowledged": "date-time",
2178 # Change Management Component Properties
2179 "created": "date-time",
2180 "dtstamp": "date-time",
2181 "last-modified": "date-time",
2182 "sequence": "integer",
2183 # Miscellaneous Component Properties
2184 "request-status": "text",
2185 ####################################
2186 # parameter types (luckily there is no name overlap)
2187 "altrep": "uri",
2188 "cn": "text",
2189 "cutype": "text",
2190 "delegated-from": "cal-address",
2191 "delegated-to": "cal-address",
2192 "dir": "uri",
2193 "encoding": "text",
2194 "fmttype": "text",
2195 "fbtype": "text",
2196 "language": "text",
2197 "member": "cal-address",
2198 "partstat": "text",
2199 "range": "text",
2200 "related": "text",
2201 "reltype": "text",
2202 "role": "text",
2203 "rsvp": "boolean",
2204 "sent-by": "cal-address",
2205 "value": "text",
2206 }
2207 )
2208
2209 def for_property(self, name, value_param: str | None = None) -> type:
2210 """Returns the type class for a property or parameter.
2211
2212 Args:
2213 name: Property or parameter name
2214 value_param: Optional ``VALUE`` parameter, for example, "DATE", "DATE-TIME", or other string.
2215
2216 Returns:
2217 The appropriate value type class
2218 """
2219 # Special case: RDATE and EXDATE always use vDDDLists to support list values
2220 # regardless of the VALUE parameter
2221 if name.upper() in ("RDATE", "EXDATE"):
2222 return self["date-time-list"]
2223
2224 # Only use VALUE parameter for known properties that support multiple value types
2225 # (like DTSTART, DTEND, etc. which can be DATE or DATE-TIME)
2226 # For unknown/custom properties, always use the default type from types_map
2227 if value_param and name in self.types_map:
2228 if value_param in self:
2229 return self[value_param]
2230 return self[self.types_map.get(name, "text")]
2231
2232 def to_ical(self, name, value):
2233 """Encodes a named value from a primitive python type to an icalendar
2234 encoded string.
2235 """
2236 type_class = self.for_property(name)
2237 return type_class(value).to_ical()
2238
2239 def from_ical(self, name, value):
2240 """Decodes a named property or parameter value from an icalendar
2241 encoded string to a primitive python type.
2242 """
2243 type_class = self.for_property(name)
2244 return type_class.from_ical(value)
2245
2246
2247__all__ = [
2248 "DURATION_REGEX",
2249 "WEEKDAY_RULE",
2250 "TimeBase",
2251 "TypesFactory",
2252 "tzid_from_dt",
2253 "tzid_from_tzinfo",
2254 "vBinary",
2255 "vBoolean",
2256 "vCalAddress",
2257 "vCategory",
2258 "vDDDLists",
2259 "vDDDTypes",
2260 "vDate",
2261 "vDatetime",
2262 "vDuration",
2263 "vFloat",
2264 "vFrequency",
2265 "vGeo",
2266 "vInline",
2267 "vInt",
2268 "vMonth",
2269 "vPeriod",
2270 "vRecur",
2271 "vSkip",
2272 "vText",
2273 "vTime",
2274 "vUTCOffset",
2275 "vUri",
2276 "vWeekday",
2277]