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
50import uuid
51from datetime import date, datetime, time, timedelta, timezone
52from typing import Any, ClassVar, Optional, Tuple, Union
53
54from icalendar.caselessdict import CaselessDict
55from icalendar.enums import Enum
56from icalendar.error import InvalidCalendar, JCalParsingError
57from icalendar.parser import Parameters, escape_char
58from icalendar.parser_tools import (
59 DEFAULT_ENCODING,
60 ICAL_TYPE,
61 SEQUENCE_TYPES,
62 from_unicode,
63 to_unicode,
64)
65from icalendar.timezone import tzid_from_dt, tzid_from_tzinfo, tzp
66from icalendar.timezone.tzid import is_utc
67from icalendar.tools import is_date, is_datetime, normalize_pytz, to_datetime
68
69try:
70 from typing import TypeAlias
71except ImportError:
72 from typing_extensions import TypeAlias
73try:
74 from typing import Self
75except ImportError:
76 from typing_extensions import Self
77
78DURATION_REGEX = re.compile(
79 r"([-+]?)P(?:(\d+)W)?(?:(\d+)D)?" r"(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$"
80)
81
82WEEKDAY_RULE = re.compile(
83 r"(?P<signal>[+-]?)(?P<relative>[\d]{0,2})" r"(?P<weekday>[\w]{2})$"
84)
85
86
87class vBinary:
88 """Binary property values are base 64 encoded."""
89
90 default_value: ClassVar[str] = "BINARY"
91 params: Parameters
92 obj: str
93
94 def __init__(self, obj, params: dict[str, str] | None = None):
95 self.obj = to_unicode(obj)
96 self.params = Parameters(encoding="BASE64", value="BINARY")
97 if params:
98 self.params.update(params)
99
100 def __repr__(self):
101 return f"vBinary({self.to_ical()})"
102
103 def to_ical(self):
104 return binascii.b2a_base64(self.obj.encode("utf-8"))[:-1]
105
106 @staticmethod
107 def from_ical(ical):
108 try:
109 return base64.b64decode(ical)
110 except ValueError as e:
111 raise ValueError("Not valid base 64 encoding.") from e
112
113 def __eq__(self, other):
114 """self == other"""
115 return isinstance(other, vBinary) and self.obj == other.obj
116
117 @classmethod
118 def examples(cls) -> list[vBinary]:
119 """Examples of vBinary."""
120 return [cls("VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZy4")]
121
122 from icalendar.param import VALUE
123
124 def to_jcal(self, name: str) -> list:
125 """The jCal representation of this property according to :rfc:`7265`."""
126 params = self.params.to_jcal()
127 if params.get("encoding") == "BASE64":
128 # BASE64 is the only allowed encoding
129 del params["encoding"]
130 return [name, params, self.VALUE.lower(), self.obj]
131
132 @classmethod
133 def from_jcal(cls, jcal_property: list) -> vBinary:
134 """Parse jCal from :rfc:`7265` to a vBinary.
135
136 Args:
137 jcal_property: The jCal property to parse.
138
139 Raises:
140 ~error.JCalParsingError: If the provided jCal is invalid.
141 """
142 JCalParsingError.validate_property(jcal_property, cls)
143 JCalParsingError.validate_value_type(jcal_property[3], str, cls, 3)
144 return cls(
145 jcal_property[3],
146 params=Parameters.from_jcal_property(jcal_property),
147 )
148
149
150class vBoolean(int):
151 """Boolean
152
153 Value Name: BOOLEAN
154
155 Purpose: This value type is used to identify properties that contain
156 either a "TRUE" or "FALSE" Boolean value.
157
158 Format Definition: This value type is defined by the following
159 notation:
160
161 .. code-block:: text
162
163 boolean = "TRUE" / "FALSE"
164
165 Description: These values are case-insensitive text. No additional
166 content value encoding is defined for this value type.
167
168 Example: The following is an example of a hypothetical property that
169 has a BOOLEAN value type:
170
171 .. code-block:: python
172
173 TRUE
174
175 .. code-block:: pycon
176
177 >>> from icalendar.prop import vBoolean
178 >>> boolean = vBoolean.from_ical('TRUE')
179 >>> boolean
180 True
181 >>> boolean = vBoolean.from_ical('FALSE')
182 >>> boolean
183 False
184 >>> boolean = vBoolean.from_ical('True')
185 >>> boolean
186 True
187 """
188
189 default_value: ClassVar[str] = "BOOLEAN"
190 params: Parameters
191
192 BOOL_MAP = CaselessDict({"true": True, "false": False})
193
194 def __new__(cls, *args, params: dict[str, Any] | None = None, **kwargs):
195 self = super().__new__(cls, *args, **kwargs)
196 self.params = Parameters(params)
197 return self
198
199 def to_ical(self):
200 return b"TRUE" if self else b"FALSE"
201
202 @classmethod
203 def from_ical(cls, ical):
204 try:
205 return cls.BOOL_MAP[ical]
206 except Exception as e:
207 raise ValueError(f"Expected 'TRUE' or 'FALSE'. Got {ical}") from e
208
209 @classmethod
210 def examples(cls) -> list[vBoolean]:
211 """Examples of vBoolean."""
212 return [
213 cls(True), # noqa: FBT003
214 cls(False), # noqa: FBT003
215 ]
216
217 from icalendar.param import VALUE
218
219 def to_jcal(self, name: str) -> list:
220 """The jCal representation of this property according to :rfc:`7265`."""
221 return [name, self.params.to_jcal(), self.VALUE.lower(), bool(self)]
222
223 @classmethod
224 def from_jcal(cls, jcal_property: list) -> vBoolean:
225 """Parse jCal from :rfc:`7265` to a vBoolean.
226
227 Args:
228 jcal_property: The jCal property to parse.
229
230 Raises:
231 ~error.JCalParsingError: If the provided jCal is invalid.
232 """
233 JCalParsingError.validate_property(jcal_property, cls)
234 JCalParsingError.validate_value_type(jcal_property[3], bool, cls, 3)
235 return cls(
236 jcal_property[3],
237 params=Parameters.from_jcal_property(jcal_property),
238 )
239
240
241class vText(str):
242 """Simple text."""
243
244 default_value: ClassVar[str] = "TEXT"
245 params: Parameters
246 __slots__ = ("encoding", "params")
247
248 def __new__(
249 cls,
250 value,
251 encoding=DEFAULT_ENCODING,
252 /,
253 params: dict[str, Any] | None = None,
254 ):
255 value = to_unicode(value, encoding=encoding)
256 self = super().__new__(cls, value)
257 self.encoding = encoding
258 self.params = Parameters(params)
259 return self
260
261 def __repr__(self) -> str:
262 return f"vText({self.to_ical()!r})"
263
264 def to_ical(self) -> bytes:
265 return escape_char(self).encode(self.encoding)
266
267 @classmethod
268 def from_ical(cls, ical: ICAL_TYPE):
269 return cls(ical)
270
271 @property
272 def ical_value(self) -> str:
273 """The string value of the text."""
274 return str(self)
275
276 from icalendar.param import ALTREP, GAP, LANGUAGE, RELTYPE, VALUE
277
278 def to_jcal(self, name: str) -> list:
279 """The jCal representation of this property according to :rfc:`7265`."""
280 if name == "request-status": # TODO: maybe add a vRequestStatus class?
281 return [name, {}, "text", self.split(";", 2)]
282 return [name, self.params.to_jcal(), self.VALUE.lower(), str(self)]
283
284 @classmethod
285 def examples(cls):
286 """Examples of vText."""
287 return [cls("Hello World!")]
288
289 @classmethod
290 def from_jcal(cls, jcal_property: list) -> Self:
291 """Parse jCal from :rfc:`7265`.
292
293 Args:
294 jcal_property: The jCal property to parse.
295
296 Raises:
297 ~error.JCalParsingError: If the provided jCal is invalid.
298 """
299 JCalParsingError.validate_property(jcal_property, cls)
300 name = jcal_property[0]
301 if name == "categories":
302 return vCategory.from_jcal(jcal_property)
303 string = jcal_property[3] # TODO: accept list or string but join with ;
304 if name == "request-status": # TODO: maybe add a vRequestStatus class?
305 JCalParsingError.validate_list_type(jcal_property[3], str, cls, 3)
306 string = ";".join(jcal_property[3])
307 JCalParsingError.validate_value_type(string, str, cls, 3)
308 return cls(
309 string,
310 params=Parameters.from_jcal_property(jcal_property),
311 )
312
313 @classmethod
314 def parse_jcal_value(cls, jcal_value: Any) -> vText:
315 """Parse a jCal value into a vText."""
316 JCalParsingError.validate_value_type(jcal_value, (str, int, float), cls)
317 return cls(str(jcal_value))
318
319
320class vCalAddress(str):
321 r"""Calendar User Address
322
323 Value Name:
324 CAL-ADDRESS
325
326 Purpose:
327 This value type is used to identify properties that contain a
328 calendar user address.
329
330 Description:
331 The value is a URI as defined by [RFC3986] or any other
332 IANA-registered form for a URI. When used to address an Internet
333 email transport address for a calendar user, the value MUST be a
334 mailto URI, as defined by [RFC2368].
335
336 Example:
337 ``mailto:`` is in front of the address.
338
339 .. code-block:: text
340
341 mailto:jane_doe@example.com
342
343 Parsing:
344
345 .. code-block:: pycon
346
347 >>> from icalendar import vCalAddress
348 >>> cal_address = vCalAddress.from_ical('mailto:jane_doe@example.com')
349 >>> cal_address
350 vCalAddress('mailto:jane_doe@example.com')
351
352 Encoding:
353
354 .. code-block:: pycon
355
356 >>> from icalendar import vCalAddress, Event
357 >>> event = Event()
358 >>> jane = vCalAddress("mailto:jane_doe@example.com")
359 >>> jane.name = "Jane"
360 >>> event["organizer"] = jane
361 >>> print(event.to_ical().decode().replace('\\r\\n', '\\n').strip())
362 BEGIN:VEVENT
363 ORGANIZER;CN=Jane:mailto:jane_doe@example.com
364 END:VEVENT
365 """
366
367 default_value: ClassVar[str] = "CAL-ADDRESS"
368 params: Parameters
369 __slots__ = ("params",)
370
371 def __new__(
372 cls,
373 value,
374 encoding=DEFAULT_ENCODING,
375 /,
376 params: dict[str, Any] | None = None,
377 ):
378 value = to_unicode(value, encoding=encoding)
379 self = super().__new__(cls, value)
380 self.params = Parameters(params)
381 return self
382
383 def __repr__(self):
384 return f"vCalAddress('{self}')"
385
386 def to_ical(self):
387 return self.encode(DEFAULT_ENCODING)
388
389 @classmethod
390 def from_ical(cls, ical):
391 return cls(ical)
392
393 @property
394 def ical_value(self):
395 """The ``mailto:`` part of the address."""
396 return str(self)
397
398 @property
399 def email(self) -> str:
400 """The email address without ``mailto:`` at the start."""
401 if self.lower().startswith("mailto:"):
402 return self[7:]
403 return str(self)
404
405 from icalendar.param import (
406 CN,
407 CUTYPE,
408 DELEGATED_FROM,
409 DELEGATED_TO,
410 DIR,
411 LANGUAGE,
412 PARTSTAT,
413 ROLE,
414 RSVP,
415 SENT_BY,
416 VALUE,
417 )
418
419 name = CN
420
421 @staticmethod
422 def _get_email(email: str) -> str:
423 """Extract email and add mailto: prefix if needed.
424
425 Handles case-insensitive mailto: prefix checking.
426
427 Args:
428 email: Email string that may or may not have mailto: prefix
429
430 Returns:
431 Email string with mailto: prefix
432 """
433 if not email.lower().startswith("mailto:"):
434 return f"mailto:{email}"
435 return email
436
437 @classmethod
438 def new(
439 cls,
440 email: str,
441 /,
442 cn: str | None = None,
443 cutype: str | None = None,
444 delegated_from: str | None = None,
445 delegated_to: str | None = None,
446 directory: str | None = None,
447 language: str | None = None,
448 partstat: str | None = None,
449 role: str | None = None,
450 rsvp: bool | None = None,
451 sent_by: str | None = None,
452 ):
453 """Create a new vCalAddress with RFC 5545 parameters.
454
455 Creates a vCalAddress instance with automatic mailto: prefix handling
456 and support for all standard RFC 5545 parameters.
457
458 Args:
459 email: The email address (mailto: prefix added automatically if missing)
460 cn: Common Name parameter
461 cutype: Calendar user type (INDIVIDUAL, GROUP, RESOURCE, ROOM)
462 delegated_from: Email of the calendar user that delegated
463 delegated_to: Email of the calendar user that was delegated to
464 directory: Reference to directory information
465 language: Language for text values
466 partstat: Participation status (NEEDS-ACTION, ACCEPTED, DECLINED, etc.)
467 role: Role (REQ-PARTICIPANT, OPT-PARTICIPANT, NON-PARTICIPANT, CHAIR)
468 rsvp: Whether RSVP is requested
469 sent_by: Email of the calendar user acting on behalf of this user
470
471 Returns:
472 vCalAddress: A new calendar address with specified parameters
473
474 Raises:
475 TypeError: If email is not a string
476
477 Examples:
478 Basic usage:
479
480 >>> from icalendar.prop import vCalAddress
481 >>> addr = vCalAddress.new("test@test.com")
482 >>> str(addr)
483 'mailto:test@test.com'
484
485 With parameters:
486
487 >>> addr = vCalAddress.new("test@test.com", cn="Test User", role="CHAIR")
488 >>> addr.params["CN"]
489 'Test User'
490 >>> addr.params["ROLE"]
491 'CHAIR'
492 """
493 if not isinstance(email, str):
494 raise TypeError(f"Email must be a string, not {type(email).__name__}")
495
496 # Handle mailto: prefix (case-insensitive)
497 email_with_prefix = cls._get_email(email)
498
499 # Create the address
500 addr = cls(email_with_prefix)
501
502 # Set parameters if provided
503 if cn is not None:
504 addr.params["CN"] = cn
505 if cutype is not None:
506 addr.params["CUTYPE"] = cutype
507 if delegated_from is not None:
508 addr.params["DELEGATED-FROM"] = cls._get_email(delegated_from)
509 if delegated_to is not None:
510 addr.params["DELEGATED-TO"] = cls._get_email(delegated_to)
511 if directory is not None:
512 addr.params["DIR"] = directory
513 if language is not None:
514 addr.params["LANGUAGE"] = language
515 if partstat is not None:
516 addr.params["PARTSTAT"] = partstat
517 if role is not None:
518 addr.params["ROLE"] = role
519 if rsvp is not None:
520 addr.params["RSVP"] = "TRUE" if rsvp else "FALSE"
521 if sent_by is not None:
522 addr.params["SENT-BY"] = cls._get_email(sent_by)
523
524 return addr
525
526 def to_jcal(self, name: str) -> list:
527 """Return this property in jCal format."""
528 return [name, self.params.to_jcal(), self.VALUE.lower(), self.ical_value]
529
530 @classmethod
531 def examples(cls) -> list[vCalAddress]:
532 """Examples of vCalAddress."""
533 return [cls.new("you@example.org", cn="You There")]
534
535 @classmethod
536 def from_jcal(cls, jcal_property: list) -> Self:
537 """Parse jCal from :rfc:`7265`.
538
539 Args:
540 jcal_property: The jCal property to parse.
541
542 Raises:
543 ~error.JCalParsingError: If the provided jCal is invalid.
544 """
545 JCalParsingError.validate_property(jcal_property, cls)
546 JCalParsingError.validate_value_type(jcal_property[3], str, cls, 3)
547 return cls(
548 jcal_property[3],
549 params=Parameters.from_jcal_property(jcal_property),
550 )
551
552
553class vFloat(float):
554 """Float
555
556 Value Name:
557 FLOAT
558
559 Purpose:
560 This value type is used to identify properties that contain
561 a real-number value.
562
563 Format Definition:
564 This value type is defined by the following notation:
565
566 .. code-block:: text
567
568 float = (["+"] / "-") 1*DIGIT ["." 1*DIGIT]
569
570 Description:
571 If the property permits, multiple "float" values are
572 specified by a COMMA-separated list of values.
573
574 Example:
575
576 .. code-block:: text
577
578 1000000.0000001
579 1.333
580 -3.14
581
582 .. code-block:: pycon
583
584 >>> from icalendar.prop import vFloat
585 >>> float = vFloat.from_ical('1000000.0000001')
586 >>> float
587 1000000.0000001
588 >>> float = vFloat.from_ical('1.333')
589 >>> float
590 1.333
591 >>> float = vFloat.from_ical('+1.333')
592 >>> float
593 1.333
594 >>> float = vFloat.from_ical('-3.14')
595 >>> float
596 -3.14
597 """
598
599 default_value: ClassVar[str] = "FLOAT"
600 params: Parameters
601
602 def __new__(cls, *args, params: dict[str, Any] | None = None, **kwargs):
603 self = super().__new__(cls, *args, **kwargs)
604 self.params = Parameters(params)
605 return self
606
607 def to_ical(self):
608 return str(self).encode("utf-8")
609
610 @classmethod
611 def from_ical(cls, ical):
612 try:
613 return cls(ical)
614 except Exception as e:
615 raise ValueError(f"Expected float value, got: {ical}") from e
616
617 @classmethod
618 def examples(cls) -> list[vFloat]:
619 """Examples of vFloat."""
620 return [vFloat(3.1415)]
621
622 from icalendar.param import VALUE
623
624 def to_jcal(self, name: str) -> list:
625 """The jCal representation of this property according to :rfc:`7265`."""
626 return [name, self.params.to_jcal(), self.VALUE.lower(), float(self)]
627
628 @classmethod
629 def from_jcal(cls, jcal_property: list) -> Self:
630 """Parse jCal from :rfc:`7265`.
631
632 Args:
633 jcal_property: The jCal property to parse.
634
635 Raises:
636 ~error.JCalParsingError: If the jCal provided is invalid.
637 """
638 JCalParsingError.validate_property(jcal_property, cls)
639 if jcal_property[0].upper() == "GEO":
640 return vGeo.from_jcal(jcal_property)
641 JCalParsingError.validate_value_type(jcal_property[3], float, cls, 3)
642 return cls(
643 jcal_property[3],
644 params=Parameters.from_jcal_property(jcal_property),
645 )
646
647
648class vInt(int):
649 """Integer
650
651 Value Name:
652 INTEGER
653
654 Purpose:
655 This value type is used to identify properties that contain a
656 signed integer value.
657
658 Format Definition:
659 This value type is defined by the following notation:
660
661 .. code-block:: text
662
663 integer = (["+"] / "-") 1*DIGIT
664
665 Description:
666 If the property permits, multiple "integer" values are
667 specified by a COMMA-separated list of values. The valid range
668 for "integer" is -2147483648 to 2147483647. If the sign is not
669 specified, then the value is assumed to be positive.
670
671 Example:
672
673 .. code-block:: text
674
675 1234567890
676 -1234567890
677 +1234567890
678 432109876
679
680 .. code-block:: pycon
681
682 >>> from icalendar.prop import vInt
683 >>> integer = vInt.from_ical('1234567890')
684 >>> integer
685 1234567890
686 >>> integer = vInt.from_ical('-1234567890')
687 >>> integer
688 -1234567890
689 >>> integer = vInt.from_ical('+1234567890')
690 >>> integer
691 1234567890
692 >>> integer = vInt.from_ical('432109876')
693 >>> integer
694 432109876
695 """
696
697 default_value: ClassVar[str] = "INTEGER"
698 params: Parameters
699
700 def __new__(cls, *args, params: dict[str, Any] | None = None, **kwargs):
701 self = super().__new__(cls, *args, **kwargs)
702 self.params = Parameters(params)
703 return self
704
705 def to_ical(self) -> bytes:
706 return str(self).encode("utf-8")
707
708 @classmethod
709 def from_ical(cls, ical: ICAL_TYPE):
710 try:
711 return cls(ical)
712 except Exception as e:
713 raise ValueError(f"Expected int, got: {ical}") from e
714
715 @classmethod
716 def examples(cls) -> list[vInt]:
717 """Examples of vInt."""
718 return [vInt(1000), vInt(-42)]
719
720 from icalendar.param import VALUE
721
722 def to_jcal(self, name: str) -> list:
723 """The jCal representation of this property according to :rfc:`7265`."""
724 return [name, self.params.to_jcal(), self.VALUE.lower(), int(self)]
725
726 @classmethod
727 def from_jcal(cls, jcal_property: list) -> Self:
728 """Parse jCal from :rfc:`7265`.
729
730 Args:
731 jcal_property: The jCal property to parse.
732
733 Raises:
734 ~error.JCalParsingError: If the provided jCal is invalid.
735 """
736 JCalParsingError.validate_property(jcal_property, cls)
737 JCalParsingError.validate_value_type(jcal_property[3], int, cls, 3)
738 return cls(
739 jcal_property[3],
740 params=Parameters.from_jcal_property(jcal_property),
741 )
742
743 @classmethod
744 def parse_jcal_value(cls, value: Any) -> int:
745 """Parse a jCal value for vInt.
746
747 Raises:
748 ~error.JCalParsingError: If the value is not an int.
749 """
750 JCalParsingError.validate_value_type(value, int, cls)
751 return cls(value)
752
753
754class vDDDLists:
755 """A list of vDDDTypes values."""
756
757 default_value: ClassVar[str] = "DATE-TIME"
758 params: Parameters
759 dts: list[vDDDTypes]
760
761 def __init__(self, dt_list, params: dict[str, Any] | None = None):
762 if params is None:
763 params = {}
764 if not hasattr(dt_list, "__iter__"):
765 dt_list = [dt_list]
766 vddd = []
767 tzid = None
768 for dt_l in dt_list:
769 dt = vDDDTypes(dt_l) if not isinstance(dt_l, vDDDTypes) else dt_l
770 vddd.append(dt)
771 if "TZID" in dt.params:
772 tzid = dt.params["TZID"]
773
774 if tzid:
775 # NOTE: no support for multiple timezones here!
776 params["TZID"] = tzid
777 self.params = Parameters(params)
778 self.dts = vddd
779
780 def to_ical(self):
781 dts_ical = (from_unicode(dt.to_ical()) for dt in self.dts)
782 return b",".join(dts_ical)
783
784 @staticmethod
785 def from_ical(ical, timezone=None):
786 out = []
787 ical_dates = ical.split(",")
788 for ical_dt in ical_dates:
789 out.append(vDDDTypes.from_ical(ical_dt, timezone=timezone))
790 return out
791
792 def __eq__(self, other):
793 if isinstance(other, vDDDLists):
794 return self.dts == other.dts
795 if isinstance(other, (TimeBase, date)):
796 return self.dts == [other]
797 return False
798
799 def __repr__(self):
800 """String representation."""
801 return f"{self.__class__.__name__}({self.dts})"
802
803 @classmethod
804 def examples(cls) -> list[vDDDLists]:
805 """Examples of vDDDLists."""
806 return [vDDDLists([datetime(2025, 11, 10, 16, 50)])] # noqa: DTZ001
807
808 def to_jcal(self, name: str) -> list:
809 """The jCal representation of this property according to :rfc:`7265`."""
810 return [
811 name,
812 self.params.to_jcal(),
813 self.VALUE.lower(),
814 *[dt.to_jcal(name)[3] for dt in self.dts],
815 ]
816
817 def _get_value(self) -> str | None:
818 return None if not self.dts else self.dts[0].VALUE
819
820 from icalendar.param import VALUE
821
822 @classmethod
823 def from_jcal(cls, jcal_property: list) -> Self:
824 """Parse jCal from :rfc:`7265`.
825
826 Args:
827 jcal_property: The jCal property to parse.
828
829 Raises:
830 ~error.JCalParsingError: If the jCal provided is invalid.
831 """
832 JCalParsingError.validate_property(jcal_property, cls)
833 values = jcal_property[3:]
834 prop = jcal_property[:3]
835 dts = []
836 for value in values:
837 dts.append(vDDDTypes.from_jcal(prop + [value]))
838 return cls(
839 dts,
840 params=Parameters.from_jcal_property(jcal_property),
841 )
842
843
844class vCategory:
845 default_value: ClassVar[str] = "TEXT"
846 params: Parameters
847
848 def __init__(
849 self, c_list: list[str] | str, /, params: dict[str, Any] | None = None
850 ):
851 if not hasattr(c_list, "__iter__") or isinstance(c_list, str):
852 c_list = [c_list]
853 self.cats: list[vText | str] = [vText(c) for c in c_list]
854 self.params = Parameters(params)
855
856 def __iter__(self):
857 return iter(vCategory.from_ical(self.to_ical()))
858
859 def to_ical(self):
860 return b",".join(
861 [
862 c.to_ical() if hasattr(c, "to_ical") else vText(c).to_ical()
863 for c in self.cats
864 ]
865 )
866
867 @staticmethod
868 def from_ical(ical):
869 ical = to_unicode(ical)
870 return ical.split(",")
871
872 def __eq__(self, other):
873 """self == other"""
874 return isinstance(other, vCategory) and self.cats == other.cats
875
876 def __repr__(self):
877 """String representation."""
878 return f"{self.__class__.__name__}({self.cats}, params={self.params})"
879
880 def to_jcal(self, name: str) -> list:
881 """The jCal representation for categories."""
882 result = [name, self.params.to_jcal(), self.VALUE.lower()]
883 result.extend(map(str, self.cats))
884 if not self.cats:
885 result.append("")
886 return result
887
888 @classmethod
889 def examples(cls) -> list[vCategory]:
890 """Examples of vCategory."""
891 return [cls(["HOME", "COSY"])]
892
893 from icalendar.param import VALUE
894
895 @classmethod
896 def from_jcal(cls, jcal_property: list) -> Self:
897 """Parse jCal from :rfc:`7265`.
898
899 Args:
900 jcal_property: The jCal property to parse.
901
902 Raises:
903 ~error.JCalParsingError: If the provided jCal is invalid.
904 """
905 JCalParsingError.validate_property(jcal_property, cls)
906 for i, category in enumerate(jcal_property[3:], start=3):
907 JCalParsingError.validate_value_type(category, str, cls, i)
908 return cls(
909 jcal_property[3:],
910 Parameters.from_jcal_property(jcal_property),
911 )
912
913 @property
914 def ical_value(self) -> list[str]:
915 """The list of categories as strings."""
916 return [str(cat) for cat in self.cats]
917
918
919class vAdr:
920 """vCard ADR (Address) structured property per :rfc:`6350` `Section 6.3.1 <https://datatracker.ietf.org/doc/html/rfc6350.html#section-6.3.1>`_.
921
922 The ADR property represents a delivery address as a single text value.
923 The structured type value consists of a sequence of seven address components.
924 The component values must be specified in their corresponding position.
925
926 - post office box
927 - extended address (e.g., apartment or suite number)
928 - street address
929 - locality (e.g., city)
930 - region (e.g., state or province)
931 - postal code
932 - country name (full name)
933
934 When a component value is missing, the associated component separator MUST still be specified.
935
936 Semicolons are field separators and are NOT escaped.
937 Commas and backslashes within field values ARE escaped per :rfc:`6350`.
938
939 Examples:
940 .. code-block:: pycon
941
942 >>> from icalendar.prop import vAdr
943 >>> adr = vAdr(("", "", "123 Main St", "Springfield", "IL", "62701", "USA"))
944 >>> adr.to_ical()
945 b';;123 Main St;Springfield;IL;62701;USA'
946 >>> vAdr.from_ical(";;123 Main St;Springfield;IL;62701;USA")
947 ('', '', '123 Main St', 'Springfield', 'IL', '62701', 'USA')
948 """
949
950 default_value: ClassVar[str] = "TEXT"
951 params: Parameters
952
953 # 7 ADR fields per RFC 6350
954 FIELDS = [
955 "po_box",
956 "extended",
957 "street",
958 "locality",
959 "region",
960 "postal_code",
961 "country",
962 ]
963
964 def __init__(
965 self,
966 fields: tuple[str, ...] | list[str] | str,
967 /,
968 params: dict[str, Any] | None = None,
969 ):
970 """Initialize ADR with seven fields or parse from vCard format string.
971
972 Args:
973 fields: Either a tuple or list of seven strings, one per field, or a
974 vCard format string with semicolon-separated fields
975 params: Optional property parameters
976 """
977 if isinstance(fields, str):
978 fields = self.from_ical(fields)
979 if len(fields) != 7:
980 raise ValueError(f"ADR must have exactly 7 fields, got {len(fields)}")
981 self.fields = tuple(str(f) for f in fields)
982 self.params = Parameters(params)
983
984 def to_ical(self) -> bytes:
985 """Generate vCard format with semicolon-separated fields."""
986 # Each field is vText (handles comma/backslash escaping)
987 # but we join with unescaped semicolons (field separators)
988 parts = [vText(f).to_ical().decode(DEFAULT_ENCODING) for f in self.fields]
989 return ";".join(parts).encode(DEFAULT_ENCODING)
990
991 @staticmethod
992 def from_ical(ical: str | bytes) -> tuple[str, ...]:
993 """Parse vCard ADR format into a tuple of seven fields.
994
995 Args:
996 ical: vCard format string with semicolon-separated fields
997
998 Returns:
999 Tuple of seven field values, or the empty string if the field is empty.
1000 """
1001 from icalendar.parser import split_on_unescaped_semicolon
1002
1003 ical = to_unicode(ical)
1004 fields = split_on_unescaped_semicolon(ical)
1005 if len(fields) != 7:
1006 raise ValueError(
1007 f"ADR must have exactly 7 fields, got {len(fields)}: {ical}"
1008 )
1009 return tuple(fields)
1010
1011 def __eq__(self, other):
1012 """self == other"""
1013 return isinstance(other, vAdr) and self.fields == other.fields
1014
1015 def __repr__(self):
1016 """String representation."""
1017 return f"{self.__class__.__name__}({self.fields}, params={self.params})"
1018
1019 from icalendar.param import VALUE
1020
1021 def to_jcal(self, name: str) -> list:
1022 """The jCal representation of this property according to :rfc:`7265`."""
1023 result = [name, self.params.to_jcal(), self.VALUE.lower()]
1024 result.extend(self.fields)
1025 return result
1026
1027 @classmethod
1028 def from_jcal(cls, jcal_property: list) -> Self:
1029 """Parse jCal from :rfc:`7265`.
1030
1031 Args:
1032 jcal_property: The jCal property to parse.
1033
1034 Raises:
1035 ~error.JCalParsingError: If the provided jCal is invalid.
1036 """
1037 JCalParsingError.validate_property(jcal_property, cls)
1038 if len(jcal_property) != 10: # name, params, value_type, 7 fields
1039 raise JCalParsingError(
1040 f"ADR must have 10 elements (name, params, value_type, 7 fields), "
1041 f"got {len(jcal_property)}"
1042 )
1043 for i, field in enumerate(jcal_property[3:], start=3):
1044 JCalParsingError.validate_value_type(field, str, cls, i)
1045 return cls(
1046 tuple(jcal_property[3:]),
1047 Parameters.from_jcal_property(jcal_property),
1048 )
1049
1050 @classmethod
1051 def examples(cls) -> list[vAdr]:
1052 """Examples of vAdr."""
1053 return [cls(("", "", "123 Main St", "Springfield", "IL", "62701", "USA"))]
1054
1055
1056class vN:
1057 """vCard N (Name) structured property per :rfc:`6350` `Section 6.2.2 <https://datatracker.ietf.org/doc/html/rfc6350.html#section-6.2.2>`_.
1058
1059 The N property represents a person's name.
1060 It consists of a single structured text value.
1061 Each component in the structure may have multiple values, separated by commas.
1062
1063 The structured property value corresponds, in sequence, to the following fields:
1064
1065 - family names (also known as surnames)
1066 - given names
1067 - additional names
1068 - honorific prefixes
1069 - honorific suffixes
1070
1071 Semicolons are field separators and are NOT escaped.
1072 Commas and backslashes within field values ARE escaped per :rfc:`6350`.
1073
1074 Examples:
1075 .. code-block:: pycon
1076
1077 >>> from icalendar.prop import vN
1078 >>> n = vN(("Doe", "John", "M.", "Dr.", "Jr.,M.D.,A.C.P."))
1079 >>> n.to_ical()
1080 b'Doe;John;M.;Dr.;Jr.\\\\,M.D.\\\\,A.C.P.'
1081 >>> vN.from_ical(r"Doe;John;M.;Dr.;Jr.\,M.D.\,A.C.P.")
1082 ('Doe', 'John', 'M.', 'Dr.', 'Jr.,M.D.,A.C.P.')
1083 """
1084
1085 default_value: ClassVar[str] = "TEXT"
1086 params: Parameters
1087
1088 # 5 N fields per RFC 6350
1089 FIELDS = ["family", "given", "additional", "prefix", "suffix"]
1090
1091 def __init__(
1092 self,
1093 fields: tuple[str, ...] | list[str] | str,
1094 /,
1095 params: dict[str, Any] | None = None,
1096 ):
1097 """Initialize N with five fields or parse from vCard format string.
1098
1099 Args:
1100 fields: Either a tuple or list of five strings, one per field, or a
1101 vCard format string with semicolon-separated fields
1102 params: Optional property parameters
1103 """
1104 if isinstance(fields, str):
1105 fields = self.from_ical(fields)
1106 if len(fields) != 5:
1107 raise ValueError(f"N must have exactly 5 fields, got {len(fields)}")
1108 self.fields = tuple(str(f) for f in fields)
1109 self.params = Parameters(params)
1110
1111 def to_ical(self) -> bytes:
1112 """Generate vCard format with semicolon-separated fields."""
1113 parts = [vText(f).to_ical().decode(DEFAULT_ENCODING) for f in self.fields]
1114 return ";".join(parts).encode(DEFAULT_ENCODING)
1115
1116 @staticmethod
1117 def from_ical(ical: str | bytes) -> tuple[str, ...]:
1118 """Parse vCard N format into a tuple of five fields.
1119
1120 Args:
1121 ical: vCard format string with semicolon-separated fields
1122
1123 Returns:
1124 Tuple of five field values, or the empty string if the field is empty
1125 """
1126 from icalendar.parser import split_on_unescaped_semicolon
1127
1128 ical = to_unicode(ical)
1129 fields = split_on_unescaped_semicolon(ical)
1130 if len(fields) != 5:
1131 raise ValueError(f"N must have exactly 5 fields, got {len(fields)}: {ical}")
1132 return tuple(fields)
1133
1134 def __eq__(self, other):
1135 """self == other"""
1136 return isinstance(other, vN) and self.fields == other.fields
1137
1138 def __repr__(self):
1139 """String representation."""
1140 return f"{self.__class__.__name__}({self.fields}, params={self.params})"
1141
1142 from icalendar.param import VALUE
1143
1144 def to_jcal(self, name: str) -> list:
1145 """The jCal representation of this property according to :rfc:`7265`."""
1146 result = [name, self.params.to_jcal(), self.VALUE.lower()]
1147 result.extend(self.fields)
1148 return result
1149
1150 @classmethod
1151 def from_jcal(cls, jcal_property: list) -> Self:
1152 """Parse jCal from :rfc:`7265`.
1153
1154 Args:
1155 jcal_property: The jCal property to parse.
1156
1157 Raises:
1158 ~error.JCalParsingError: If the provided jCal is invalid.
1159 """
1160 JCalParsingError.validate_property(jcal_property, cls)
1161 if len(jcal_property) != 8: # name, params, value_type, 5 fields
1162 raise JCalParsingError(
1163 f"N must have 8 elements (name, params, value_type, 5 fields), "
1164 f"got {len(jcal_property)}"
1165 )
1166 for i, field in enumerate(jcal_property[3:], start=3):
1167 JCalParsingError.validate_value_type(field, str, cls, i)
1168 return cls(
1169 tuple(jcal_property[3:]),
1170 Parameters.from_jcal_property(jcal_property),
1171 )
1172
1173 @classmethod
1174 def examples(cls) -> list[vN]:
1175 """Examples of vN."""
1176 return [cls(("Doe", "John", "M.", "Dr.", "Jr."))]
1177
1178
1179class vOrg:
1180 r"""vCard ORG (Organization) structured property per :rfc:`6350` `Section 6.6.4 <https://datatracker.ietf.org/doc/html/rfc6350.html#section-6.6.4>`_.
1181
1182 The ORG property specifies the organizational name and units associated with the vCard.
1183
1184 Its value is a structured type consisting of components separated by semicolons.
1185 The components are the organization name, followed by zero or more levels of organizational unit names:
1186
1187 .. code-block:: text
1188
1189 organization-name; organizational-unit-1; organizational-unit-2; ...
1190
1191 Semicolons are field separators and are NOT escaped.
1192 Commas and backslashes within field values ARE escaped per :rfc:`6350`.
1193
1194 Examples:
1195 A property value consisting of an organizational name,
1196 organizational unit #1 name, and organizational unit #2 name.
1197
1198 .. code-block:: text
1199
1200 ORG:ABC\, Inc.;North American Division;Marketing
1201
1202 The same example in icalendar.
1203
1204 .. code-block:: pycon
1205
1206 >>> from icalendar.prop import vOrg
1207 >>> org = vOrg(("ABC, Inc.", "North American Division", "Marketing"))
1208 >>> org.to_ical()
1209 b'ABC\\, Inc.;North American Division;Marketing'
1210 >>> vOrg.from_ical(r"ABC\, Inc.;North American Division;Marketing")
1211 ('ABC, Inc.', 'North American Division', 'Marketing')
1212 """
1213
1214 default_value: ClassVar[str] = "TEXT"
1215 params: Parameters
1216
1217 def __init__(
1218 self,
1219 fields: tuple[str, ...] | list[str] | str,
1220 /,
1221 params: dict[str, Any] | None = None,
1222 ):
1223 """Initialize ORG with variable fields or parse from vCard format string.
1224
1225 Args:
1226 fields: Either a tuple or list of one or more strings, or a
1227 vCard format string with semicolon-separated fields
1228 params: Optional property parameters
1229 """
1230 if isinstance(fields, str):
1231 fields = self.from_ical(fields)
1232 if len(fields) < 1:
1233 raise ValueError("ORG must have at least 1 field (organization name)")
1234 self.fields = tuple(str(f) for f in fields)
1235 self.params = Parameters(params)
1236
1237 def to_ical(self) -> bytes:
1238 """Generate vCard format with semicolon-separated fields."""
1239 parts = [vText(f).to_ical().decode(DEFAULT_ENCODING) for f in self.fields]
1240 return ";".join(parts).encode(DEFAULT_ENCODING)
1241
1242 @staticmethod
1243 def from_ical(ical: str | bytes) -> tuple[str, ...]:
1244 """Parse vCard ORG format into a tuple of fields.
1245
1246 Args:
1247 ical: vCard format string with semicolon-separated fields
1248
1249 Returns:
1250 Tuple of field values with one or more fields
1251 """
1252 from icalendar.parser import split_on_unescaped_semicolon
1253
1254 ical = to_unicode(ical)
1255 fields = split_on_unescaped_semicolon(ical)
1256 if len(fields) < 1:
1257 raise ValueError(f"ORG must have at least 1 field: {ical}")
1258 return tuple(fields)
1259
1260 def __eq__(self, other):
1261 """self == other"""
1262 return isinstance(other, vOrg) and self.fields == other.fields
1263
1264 def __repr__(self):
1265 """String representation."""
1266 return f"{self.__class__.__name__}({self.fields}, params={self.params})"
1267
1268 from icalendar.param import VALUE
1269
1270 def to_jcal(self, name: str) -> list:
1271 """The jCal representation of this property according to :rfc:`7265`."""
1272 result = [name, self.params.to_jcal(), self.VALUE.lower()]
1273 result.extend(self.fields)
1274 return result
1275
1276 @classmethod
1277 def from_jcal(cls, jcal_property: list) -> Self:
1278 """Parse jCal from :rfc:`7265`.
1279
1280 Args:
1281 jcal_property: The jCal property to parse.
1282
1283 Raises:
1284 ~error.JCalParsingError: If the provided jCal is invalid.
1285 """
1286 JCalParsingError.validate_property(jcal_property, cls)
1287 if len(jcal_property) < 4: # name, params, value_type, at least 1 field
1288 raise JCalParsingError(
1289 f"ORG must have at least 4 elements (name, params, value_type, org name), "
1290 f"got {len(jcal_property)}"
1291 )
1292 for i, field in enumerate(jcal_property[3:], start=3):
1293 JCalParsingError.validate_value_type(field, str, cls, i)
1294 return cls(
1295 tuple(jcal_property[3:]),
1296 Parameters.from_jcal_property(jcal_property),
1297 )
1298
1299 @classmethod
1300 def examples(cls) -> list[vOrg]:
1301 """Examples of vOrg."""
1302 return [cls(("ABC Inc.", "North American Division", "Marketing"))]
1303
1304
1305class TimeBase:
1306 """Make classes with a datetime/date comparable."""
1307
1308 default_value: ClassVar[str]
1309 params: Parameters
1310 ignore_for_equality = {"TZID", "VALUE"}
1311
1312 def __eq__(self, other):
1313 """self == other"""
1314 if isinstance(other, date):
1315 return self.dt == other
1316 if isinstance(other, TimeBase):
1317 default = object()
1318 for key in (
1319 set(self.params) | set(other.params)
1320 ) - self.ignore_for_equality:
1321 if key[:2].lower() != "x-" and self.params.get(
1322 key, default
1323 ) != other.params.get(key, default):
1324 return False
1325 return self.dt == other.dt
1326 if isinstance(other, vDDDLists):
1327 return other == self
1328 return False
1329
1330 def __hash__(self):
1331 return hash(self.dt)
1332
1333 from icalendar.param import RANGE, RELATED, TZID
1334
1335 def __repr__(self):
1336 """String representation."""
1337 return f"{self.__class__.__name__}({self.dt}, {self.params})"
1338
1339
1340DT_TYPE: TypeAlias = Union[
1341 datetime,
1342 date,
1343 timedelta,
1344 time,
1345 Tuple[datetime, datetime],
1346 Tuple[datetime, timedelta],
1347]
1348
1349
1350class vDDDTypes(TimeBase):
1351 """A combined Datetime, Date or Duration parser/generator. Their format
1352 cannot be confused, and often values can be of either types.
1353 So this is practical.
1354 """
1355
1356 default_value: ClassVar[str] = "DATE-TIME"
1357 params: Parameters
1358 dt: DT_TYPE
1359
1360 def __init__(self, dt, params: dict[str, Any] | None = None):
1361 if params is None:
1362 params = {}
1363 if not isinstance(dt, (datetime, date, timedelta, time, tuple)):
1364 raise TypeError(
1365 "You must use datetime, date, timedelta, time or tuple (for periods)"
1366 )
1367 self.dt = dt
1368 # if isinstance(dt, (datetime, timedelta)): pass
1369 if is_date(dt):
1370 params.update({"value": "DATE"})
1371 elif isinstance(dt, time):
1372 params.update({"value": "TIME"})
1373 elif isinstance(dt, tuple):
1374 params.update({"value": "PERIOD"})
1375 self.params = Parameters(params)
1376 self.params.update_tzid_from(dt)
1377
1378 def to_property_type(self) -> vDatetime | vDate | vDuration | vTime | vPeriod:
1379 """Convert to a property type.
1380
1381 Raises:
1382 ValueError: If the type is unknown.
1383 """
1384 dt = self.dt
1385 if isinstance(dt, datetime):
1386 result = vDatetime(dt)
1387 elif isinstance(dt, date):
1388 result = vDate(dt)
1389 elif isinstance(dt, timedelta):
1390 result = vDuration(dt)
1391 elif isinstance(dt, time):
1392 result = vTime(dt)
1393 elif isinstance(dt, tuple) and len(dt) == 2:
1394 result = vPeriod(dt)
1395 else:
1396 raise ValueError(f"Unknown date type: {type(dt)}")
1397 result.params = self.params
1398 return result
1399
1400 def to_ical(self) -> str:
1401 """Return the ical representation."""
1402 return self.to_property_type().to_ical()
1403
1404 @classmethod
1405 def from_ical(cls, ical, timezone=None):
1406 if isinstance(ical, cls):
1407 return ical.dt
1408 u = ical.upper()
1409 if u.startswith(("P", "-P", "+P")):
1410 return vDuration.from_ical(ical)
1411 if "/" in u:
1412 return vPeriod.from_ical(ical, timezone=timezone)
1413
1414 if len(ical) in (15, 16):
1415 return vDatetime.from_ical(ical, timezone=timezone)
1416 if len(ical) == 8:
1417 if timezone:
1418 tzinfo = tzp.timezone(timezone)
1419 if tzinfo is not None:
1420 return to_datetime(vDate.from_ical(ical)).replace(tzinfo=tzinfo)
1421 return vDate.from_ical(ical)
1422 if len(ical) in (6, 7):
1423 return vTime.from_ical(ical)
1424 raise ValueError(f"Expected datetime, date, or time. Got: '{ical}'")
1425
1426 @property
1427 def td(self) -> timedelta:
1428 """Compatibility property returning ``self.dt``.
1429
1430 This class is used to replace different time components.
1431 Some of them contain a datetime or date (``.dt``).
1432 Some of them contain a timedelta (``.td``).
1433 This property allows interoperability.
1434 """
1435 return self.dt
1436
1437 @property
1438 def dts(self) -> list:
1439 """Compatibility method to return a list of datetimes."""
1440 return [self]
1441
1442 @classmethod
1443 def examples(cls) -> list[vDDDTypes]:
1444 """Examples of vDDDTypes."""
1445 return [cls(date(2025, 11, 10))]
1446
1447 def _get_value(self) -> str | None:
1448 """Determine the VALUE parameter."""
1449 return self.to_property_type().VALUE
1450
1451 from icalendar.param import VALUE
1452
1453 def to_jcal(self, name: str) -> list:
1454 """The jCal representation of this property according to :rfc:`7265`."""
1455 return self.to_property_type().to_jcal(name)
1456
1457 @classmethod
1458 def parse_jcal_value(cls, jcal: str | list) -> timedelta:
1459 """Parse a jCal value.
1460
1461 Raises:
1462 ~error.JCalParsingError: If the value can't be parsed as either a date, time,
1463 date-time, duration, or period.
1464 """
1465 if isinstance(jcal, list):
1466 return vPeriod.parse_jcal_value(jcal)
1467 JCalParsingError.validate_value_type(jcal, str, cls)
1468 if "/" in jcal:
1469 return vPeriod.parse_jcal_value(jcal)
1470 for jcal_type in (vDatetime, vDate, vTime, vDuration):
1471 try:
1472 return jcal_type.parse_jcal_value(jcal)
1473 except JCalParsingError: # noqa: PERF203
1474 pass
1475 raise JCalParsingError(
1476 "Cannot parse date, time, date-time, duration, or period.", cls, value=jcal
1477 )
1478
1479 @classmethod
1480 def from_jcal(cls, jcal_property: list) -> Self:
1481 """Parse jCal from :rfc:`7265`.
1482
1483 Args:
1484 jcal_property: The jCal property to parse.
1485
1486 Raises:
1487 ~error.JCalParsingError: If the provided jCal is invalid.
1488 """
1489 JCalParsingError.validate_property(jcal_property, cls)
1490 with JCalParsingError.reraise_with_path_added(3):
1491 dt = cls.parse_jcal_value(jcal_property[3])
1492 params = Parameters.from_jcal_property(jcal_property)
1493 if params.tzid:
1494 if isinstance(dt, tuple):
1495 # period
1496 start = tzp.localize(dt[0], params.tzid)
1497 end = tzp.localize(dt[1], params.tzid) if is_datetime(dt[1]) else dt[1]
1498 dt = (start, end)
1499 else:
1500 dt = tzp.localize(dt, params.tzid)
1501 return cls(
1502 dt,
1503 params=params,
1504 )
1505
1506
1507class vDate(TimeBase):
1508 """Date
1509
1510 Value Name:
1511 DATE
1512
1513 Purpose:
1514 This value type is used to identify values that contain a
1515 calendar date.
1516
1517 Format Definition:
1518 This value type is defined by the following notation:
1519
1520 .. code-block:: text
1521
1522 date = date-value
1523
1524 date-value = date-fullyear date-month date-mday
1525 date-fullyear = 4DIGIT
1526 date-month = 2DIGIT ;01-12
1527 date-mday = 2DIGIT ;01-28, 01-29, 01-30, 01-31
1528 ;based on month/year
1529
1530 Description:
1531 If the property permits, multiple "date" values are
1532 specified as a COMMA-separated list of values. The format for the
1533 value type is based on the [ISO.8601.2004] complete
1534 representation, basic format for a calendar date. The textual
1535 format specifies a four-digit year, two-digit month, and two-digit
1536 day of the month. There are no separator characters between the
1537 year, month, and day component text.
1538
1539 Example:
1540 The following represents July 14, 1997:
1541
1542 .. code-block:: text
1543
1544 19970714
1545
1546 .. code-block:: pycon
1547
1548 >>> from icalendar.prop import vDate
1549 >>> date = vDate.from_ical('19970714')
1550 >>> date.year
1551 1997
1552 >>> date.month
1553 7
1554 >>> date.day
1555 14
1556 """
1557
1558 default_value: ClassVar[str] = "DATE"
1559 params: Parameters
1560
1561 def __init__(self, dt, params: dict[str, Any] | None = None):
1562 if not isinstance(dt, date):
1563 raise TypeError("Value MUST be a date instance")
1564 self.dt = dt
1565 self.params = Parameters(params or {})
1566
1567 def to_ical(self):
1568 s = f"{self.dt.year:04}{self.dt.month:02}{self.dt.day:02}"
1569 return s.encode("utf-8")
1570
1571 @staticmethod
1572 def from_ical(ical):
1573 try:
1574 timetuple = (
1575 int(ical[:4]), # year
1576 int(ical[4:6]), # month
1577 int(ical[6:8]), # day
1578 )
1579 return date(*timetuple)
1580 except Exception as e:
1581 raise ValueError(f"Wrong date format {ical}") from e
1582
1583 @classmethod
1584 def examples(cls) -> list[vDate]:
1585 """Examples of vDate."""
1586 return [cls(date(2025, 11, 10))]
1587
1588 from icalendar.param import VALUE
1589
1590 def to_jcal(self, name: str) -> list:
1591 """The jCal representation of this property according to :rfc:`7265`."""
1592 return [
1593 name,
1594 self.params.to_jcal(),
1595 self.VALUE.lower(),
1596 self.dt.strftime("%Y-%m-%d"),
1597 ]
1598
1599 @classmethod
1600 def parse_jcal_value(cls, jcal: str) -> datetime:
1601 """Parse a jCal string to a :py:class:`datetime.datetime`.
1602
1603 Raises:
1604 ~error.JCalParsingError: If it can't parse a date.
1605 """
1606 JCalParsingError.validate_value_type(jcal, str, cls)
1607 try:
1608 return datetime.strptime(jcal, "%Y-%m-%d").date() # noqa: DTZ007
1609 except ValueError as e:
1610 raise JCalParsingError("Cannot parse date.", cls, value=jcal) from e
1611
1612 @classmethod
1613 def from_jcal(cls, jcal_property: list) -> Self:
1614 """Parse jCal from :rfc:`7265`.
1615
1616 Args:
1617 jcal_property: The jCal property to parse.
1618
1619 Raises:
1620 ~error.JCalParsingError: If the provided jCal is invalid.
1621 """
1622 JCalParsingError.validate_property(jcal_property, cls)
1623 with JCalParsingError.reraise_with_path_added(3):
1624 value = cls.parse_jcal_value(jcal_property[3])
1625 return cls(
1626 value,
1627 params=Parameters.from_jcal_property(jcal_property),
1628 )
1629
1630
1631class vDatetime(TimeBase):
1632 """Date-Time
1633
1634 Value Name:
1635 DATE-TIME
1636
1637 Purpose:
1638 This value type is used to identify values that specify a
1639 precise calendar date and time of day. The format is based on
1640 the ISO.8601.2004 complete representation.
1641
1642 Format Definition:
1643 This value type is defined by the following notation:
1644
1645 .. code-block:: text
1646
1647 date-time = date "T" time
1648
1649 date = date-value
1650 date-value = date-fullyear date-month date-mday
1651 date-fullyear = 4DIGIT
1652 date-month = 2DIGIT ;01-12
1653 date-mday = 2DIGIT ;01-28, 01-29, 01-30, 01-31
1654 ;based on month/year
1655 time = time-hour time-minute time-second [time-utc]
1656 time-hour = 2DIGIT ;00-23
1657 time-minute = 2DIGIT ;00-59
1658 time-second = 2DIGIT ;00-60
1659 time-utc = "Z"
1660
1661 The following is the representation of the date-time format.
1662
1663 .. code-block:: text
1664
1665 YYYYMMDDTHHMMSS
1666
1667 Description:
1668 vDatetime is timezone aware and uses a timezone library.
1669 When a vDatetime object is created from an
1670 ical string, you can pass a valid timezone identifier. When a
1671 vDatetime object is created from a Python :py:mod:`datetime` object, it uses the
1672 tzinfo component, if present. Otherwise a timezone-naive object is
1673 created. Be aware that there are certain limitations with timezone naive
1674 DATE-TIME components in the icalendar standard.
1675
1676 Example:
1677 The following represents March 2, 2021 at 10:15 AM with local time:
1678
1679 .. code-block:: pycon
1680
1681 >>> from icalendar import vDatetime
1682 >>> datetime = vDatetime.from_ical("20210302T101500")
1683 >>> datetime.tzname()
1684 >>> datetime.year
1685 2021
1686 >>> datetime.minute
1687 15
1688
1689 The following represents March 2, 2021 at 10:15 AM in New York:
1690
1691 .. code-block:: pycon
1692
1693 >>> datetime = vDatetime.from_ical("20210302T101500", 'America/New_York')
1694 >>> datetime.tzname()
1695 'EST'
1696
1697 The following represents March 2, 2021 at 10:15 AM in Berlin:
1698
1699 .. code-block:: pycon
1700
1701 >>> from zoneinfo import ZoneInfo
1702 >>> timezone = ZoneInfo("Europe/Berlin")
1703 >>> vDatetime.from_ical("20210302T101500", timezone)
1704 datetime.datetime(2021, 3, 2, 10, 15, tzinfo=ZoneInfo(key='Europe/Berlin'))
1705 """
1706
1707 default_value: ClassVar[str] = "DATE-TIME"
1708 params: Parameters
1709
1710 def __init__(self, dt, /, params: dict[str, Any] | None = None):
1711 self.dt = dt
1712 self.params = Parameters(params)
1713 self.params.update_tzid_from(dt)
1714
1715 def to_ical(self):
1716 dt = self.dt
1717
1718 s = (
1719 f"{dt.year:04}{dt.month:02}{dt.day:02}"
1720 f"T{dt.hour:02}{dt.minute:02}{dt.second:02}"
1721 )
1722 if self.is_utc():
1723 s += "Z"
1724 return s.encode("utf-8")
1725
1726 @staticmethod
1727 def from_ical(ical, timezone=None):
1728 """Create a datetime from the RFC string."""
1729 tzinfo = None
1730 if isinstance(timezone, str):
1731 tzinfo = tzp.timezone(timezone)
1732 elif timezone is not None:
1733 tzinfo = timezone
1734
1735 try:
1736 timetuple = (
1737 int(ical[:4]), # year
1738 int(ical[4:6]), # month
1739 int(ical[6:8]), # day
1740 int(ical[9:11]), # hour
1741 int(ical[11:13]), # minute
1742 int(ical[13:15]), # second
1743 )
1744 if tzinfo:
1745 return tzp.localize(datetime(*timetuple), tzinfo) # noqa: DTZ001
1746 if not ical[15:]:
1747 return datetime(*timetuple) # noqa: DTZ001
1748 if ical[15:16] == "Z":
1749 return tzp.localize_utc(datetime(*timetuple)) # noqa: DTZ001
1750 except Exception as e:
1751 raise ValueError(f"Wrong datetime format: {ical}") from e
1752 raise ValueError(f"Wrong datetime format: {ical}")
1753
1754 @classmethod
1755 def examples(cls) -> list[vDatetime]:
1756 """Examples of vDatetime."""
1757 return [cls(datetime(2025, 11, 10, 16, 52))] # noqa: DTZ001
1758
1759 from icalendar.param import VALUE
1760
1761 def to_jcal(self, name: str) -> list:
1762 """The jCal representation of this property according to :rfc:`7265`."""
1763 value = self.dt.strftime("%Y-%m-%dT%H:%M:%S")
1764 if self.is_utc():
1765 value += "Z"
1766 return [name, self.params.to_jcal(exclude_utc=True), self.VALUE.lower(), value]
1767
1768 def is_utc(self) -> bool:
1769 """Whether this datetime is UTC."""
1770 return self.params.is_utc() or is_utc(self.dt)
1771
1772 @classmethod
1773 def parse_jcal_value(cls, jcal: str) -> datetime:
1774 """Parse a jCal string to a :py:class:`datetime.datetime`.
1775
1776 Raises:
1777 ~error.JCalParsingError: If it can't parse a date-time value.
1778 """
1779 JCalParsingError.validate_value_type(jcal, str, cls)
1780 utc = jcal.endswith("Z")
1781 if utc:
1782 jcal = jcal[:-1]
1783 try:
1784 dt = datetime.strptime(jcal, "%Y-%m-%dT%H:%M:%S") # noqa: DTZ007
1785 except ValueError as e:
1786 raise JCalParsingError("Cannot parse date-time.", cls, value=jcal) from e
1787 if utc:
1788 return tzp.localize_utc(dt)
1789 return dt
1790
1791 @classmethod
1792 def from_jcal(cls, jcal_property: list) -> Self:
1793 """Parse jCal from :rfc:`7265`.
1794
1795 Args:
1796 jcal_property: The jCal property to parse.
1797
1798 Raises:
1799 ~error.JCalParsingError: If the provided jCal is invalid.
1800 """
1801 JCalParsingError.validate_property(jcal_property, cls)
1802 params = Parameters.from_jcal_property(jcal_property)
1803 with JCalParsingError.reraise_with_path_added(3):
1804 dt = cls.parse_jcal_value(jcal_property[3])
1805 if params.tzid:
1806 dt = tzp.localize(dt, params.tzid)
1807 return cls(
1808 dt,
1809 params=params,
1810 )
1811
1812
1813class vDuration(TimeBase):
1814 """Duration
1815
1816 Value Name:
1817 DURATION
1818
1819 Purpose:
1820 This value type is used to identify properties that contain
1821 a duration of time.
1822
1823 Format Definition:
1824 This value type is defined by the following notation:
1825
1826 .. code-block:: text
1827
1828 dur-value = (["+"] / "-") "P" (dur-date / dur-time / dur-week)
1829
1830 dur-date = dur-day [dur-time]
1831 dur-time = "T" (dur-hour / dur-minute / dur-second)
1832 dur-week = 1*DIGIT "W"
1833 dur-hour = 1*DIGIT "H" [dur-minute]
1834 dur-minute = 1*DIGIT "M" [dur-second]
1835 dur-second = 1*DIGIT "S"
1836 dur-day = 1*DIGIT "D"
1837
1838 Description:
1839 If the property permits, multiple "duration" values are
1840 specified by a COMMA-separated list of values. The format is
1841 based on the [ISO.8601.2004] complete representation basic format
1842 with designators for the duration of time. The format can
1843 represent nominal durations (weeks and days) and accurate
1844 durations (hours, minutes, and seconds). Note that unlike
1845 [ISO.8601.2004], this value type doesn't support the "Y" and "M"
1846 designators to specify durations in terms of years and months.
1847 The duration of a week or a day depends on its position in the
1848 calendar. In the case of discontinuities in the time scale, such
1849 as the change from standard time to daylight time and back, the
1850 computation of the exact duration requires the subtraction or
1851 addition of the change of duration of the discontinuity. Leap
1852 seconds MUST NOT be considered when computing an exact duration.
1853 When computing an exact duration, the greatest order time
1854 components MUST be added first, that is, the number of days MUST
1855 be added first, followed by the number of hours, number of
1856 minutes, and number of seconds.
1857
1858 Example:
1859 A duration of 15 days, 5 hours, and 20 seconds would be:
1860
1861 .. code-block:: text
1862
1863 P15DT5H0M20S
1864
1865 A duration of 7 weeks would be:
1866
1867 .. code-block:: text
1868
1869 P7W
1870
1871 .. code-block:: pycon
1872
1873 >>> from icalendar.prop import vDuration
1874 >>> duration = vDuration.from_ical('P15DT5H0M20S')
1875 >>> duration
1876 datetime.timedelta(days=15, seconds=18020)
1877 >>> duration = vDuration.from_ical('P7W')
1878 >>> duration
1879 datetime.timedelta(days=49)
1880 """
1881
1882 default_value: ClassVar[str] = "DURATION"
1883 params: Parameters
1884
1885 def __init__(self, td: timedelta | str, /, params: dict[str, Any] | None = None):
1886 if isinstance(td, str):
1887 td = vDuration.from_ical(td)
1888 if not isinstance(td, timedelta):
1889 raise TypeError("Value MUST be a timedelta instance")
1890 self.td = td
1891 self.params = Parameters(params)
1892
1893 def to_ical(self):
1894 sign = ""
1895 td = self.td
1896 if td.days < 0:
1897 sign = "-"
1898 td = -td
1899 timepart = ""
1900 if td.seconds:
1901 timepart = "T"
1902 hours = td.seconds // 3600
1903 minutes = td.seconds % 3600 // 60
1904 seconds = td.seconds % 60
1905 if hours:
1906 timepart += f"{hours}H"
1907 if minutes or (hours and seconds):
1908 timepart += f"{minutes}M"
1909 if seconds:
1910 timepart += f"{seconds}S"
1911 if td.days == 0 and timepart:
1912 return str(sign).encode("utf-8") + b"P" + str(timepart).encode("utf-8")
1913 return (
1914 str(sign).encode("utf-8")
1915 + b"P"
1916 + str(abs(td.days)).encode("utf-8")
1917 + b"D"
1918 + str(timepart).encode("utf-8")
1919 )
1920
1921 @staticmethod
1922 def from_ical(ical):
1923 match = DURATION_REGEX.match(ical)
1924 if not match:
1925 raise InvalidCalendar(f"Invalid iCalendar duration: {ical}")
1926
1927 sign, weeks, days, hours, minutes, seconds = match.groups()
1928 value = timedelta(
1929 weeks=int(weeks or 0),
1930 days=int(days or 0),
1931 hours=int(hours or 0),
1932 minutes=int(minutes or 0),
1933 seconds=int(seconds or 0),
1934 )
1935
1936 if sign == "-":
1937 value = -value
1938
1939 return value
1940
1941 @property
1942 def dt(self) -> timedelta:
1943 """The time delta for compatibility."""
1944 return self.td
1945
1946 @classmethod
1947 def examples(cls) -> list[vDuration]:
1948 """Examples of vDuration."""
1949 return [cls(timedelta(1, 99))]
1950
1951 from icalendar.param import VALUE
1952
1953 def to_jcal(self, name: str) -> list:
1954 """The jCal representation of this property according to :rfc:`7265`."""
1955 return [
1956 name,
1957 self.params.to_jcal(),
1958 self.VALUE.lower(),
1959 self.to_ical().decode(),
1960 ]
1961
1962 @classmethod
1963 def parse_jcal_value(cls, jcal: str) -> timedelta | None:
1964 """Parse a jCal string to a :py:class:`datetime.timedelta`.
1965
1966 Raises:
1967 ~error.JCalParsingError: If it can't parse a duration."""
1968 JCalParsingError.validate_value_type(jcal, str, cls)
1969 try:
1970 return cls.from_ical(jcal)
1971 except ValueError as e:
1972 raise JCalParsingError("Cannot parse duration.", cls, value=jcal) from e
1973
1974 @classmethod
1975 def from_jcal(cls, jcal_property: list) -> Self:
1976 """Parse jCal from :rfc:`7265`.
1977
1978 Args:
1979 jcal_property: The jCal property to parse.
1980
1981 Raises:
1982 ~error.JCalParsingError: If the provided jCal is invalid.
1983 """
1984 JCalParsingError.validate_property(jcal_property, cls)
1985 with JCalParsingError.reraise_with_path_added(3):
1986 duration = cls.parse_jcal_value(jcal_property[3])
1987 return cls(
1988 duration,
1989 Parameters.from_jcal_property(jcal_property),
1990 )
1991
1992
1993class vPeriod(TimeBase):
1994 """Period of Time
1995
1996 Value Name:
1997 PERIOD
1998
1999 Purpose:
2000 This value type is used to identify values that contain a
2001 precise period of time.
2002
2003 Format Definition:
2004 This value type is defined by the following notation:
2005
2006 .. code-block:: text
2007
2008 period = period-explicit / period-start
2009
2010 period-explicit = date-time "/" date-time
2011 ; [ISO.8601.2004] complete representation basic format for a
2012 ; period of time consisting of a start and end. The start MUST
2013 ; be before the end.
2014
2015 period-start = date-time "/" dur-value
2016 ; [ISO.8601.2004] complete representation basic format for a
2017 ; period of time consisting of a start and positive duration
2018 ; of time.
2019
2020 Description:
2021 If the property permits, multiple "period" values are
2022 specified by a COMMA-separated list of values. There are two
2023 forms of a period of time. First, a period of time is identified
2024 by its start and its end. This format is based on the
2025 [ISO.8601.2004] complete representation, basic format for "DATE-
2026 TIME" start of the period, followed by a SOLIDUS character
2027 followed by the "DATE-TIME" of the end of the period. The start
2028 of the period MUST be before the end of the period. Second, a
2029 period of time can also be defined by a start and a positive
2030 duration of time. The format is based on the [ISO.8601.2004]
2031 complete representation, basic format for the "DATE-TIME" start of
2032 the period, followed by a SOLIDUS character, followed by the
2033 [ISO.8601.2004] basic format for "DURATION" of the period.
2034
2035 Example:
2036 The period starting at 18:00:00 UTC, on January 1, 1997 and
2037 ending at 07:00:00 UTC on January 2, 1997 would be:
2038
2039 .. code-block:: text
2040
2041 19970101T180000Z/19970102T070000Z
2042
2043 The period start at 18:00:00 on January 1, 1997 and lasting 5 hours
2044 and 30 minutes would be:
2045
2046 .. code-block:: text
2047
2048 19970101T180000Z/PT5H30M
2049
2050 .. code-block:: pycon
2051
2052 >>> from icalendar.prop import vPeriod
2053 >>> period = vPeriod.from_ical('19970101T180000Z/19970102T070000Z')
2054 >>> period = vPeriod.from_ical('19970101T180000Z/PT5H30M')
2055 """
2056
2057 default_value: ClassVar[str] = "PERIOD"
2058 params: Parameters
2059 by_duration: bool
2060 start: datetime
2061 end: datetime
2062 duration: timedelta
2063
2064 def __init__(
2065 self,
2066 per: tuple[datetime, Union[datetime, timedelta]],
2067 params: dict[str, Any] | None = None,
2068 ):
2069 start, end_or_duration = per
2070 if not (isinstance(start, (datetime, date))):
2071 raise TypeError("Start value MUST be a datetime or date instance")
2072 if not (isinstance(end_or_duration, (datetime, date, timedelta))):
2073 raise TypeError(
2074 "end_or_duration MUST be a datetime, date or timedelta instance"
2075 )
2076 by_duration = isinstance(end_or_duration, timedelta)
2077 if by_duration:
2078 duration = end_or_duration
2079 end = normalize_pytz(start + duration)
2080 else:
2081 end = end_or_duration
2082 duration = normalize_pytz(end - start)
2083 if start > end:
2084 raise ValueError("Start time is greater than end time")
2085
2086 self.params = Parameters(params or {"value": "PERIOD"})
2087 # set the timezone identifier
2088 # does not support different timezones for start and end
2089 self.params.update_tzid_from(start)
2090
2091 self.start = start
2092 self.end = end
2093 self.by_duration = by_duration
2094 self.duration = duration
2095
2096 def overlaps(self, other):
2097 if self.start > other.start:
2098 return other.overlaps(self)
2099 return self.start <= other.start < self.end
2100
2101 def to_ical(self):
2102 if self.by_duration:
2103 return (
2104 vDatetime(self.start).to_ical()
2105 + b"/"
2106 + vDuration(self.duration).to_ical()
2107 )
2108 return vDatetime(self.start).to_ical() + b"/" + vDatetime(self.end).to_ical()
2109
2110 @staticmethod
2111 def from_ical(ical, timezone=None):
2112 try:
2113 start, end_or_duration = ical.split("/")
2114 start = vDDDTypes.from_ical(start, timezone=timezone)
2115 end_or_duration = vDDDTypes.from_ical(end_or_duration, timezone=timezone)
2116 except Exception as e:
2117 raise ValueError(f"Expected period format, got: {ical}") from e
2118 return (start, end_or_duration)
2119
2120 def __repr__(self):
2121 p = (self.start, self.duration) if self.by_duration else (self.start, self.end)
2122 return f"vPeriod({p!r})"
2123
2124 @property
2125 def dt(self):
2126 """Make this cooperate with the other vDDDTypes."""
2127 return (self.start, (self.duration if self.by_duration else self.end))
2128
2129 from icalendar.param import FBTYPE
2130
2131 @classmethod
2132 def examples(cls) -> list[vPeriod]:
2133 """Examples of vPeriod."""
2134 return [
2135 vPeriod((datetime(2025, 11, 10, 16, 35), timedelta(hours=1, minutes=30))), # noqa: DTZ001
2136 vPeriod((datetime(2025, 11, 10, 16, 35), datetime(2025, 11, 10, 18, 5))), # noqa: DTZ001
2137 ]
2138
2139 from icalendar.param import VALUE
2140
2141 def to_jcal(self, name: str) -> list:
2142 """The jCal representation of this property according to :rfc:`7265`."""
2143 value = [vDatetime(self.start).to_jcal(name)[-1]]
2144 if self.by_duration:
2145 value.append(vDuration(self.duration).to_jcal(name)[-1])
2146 else:
2147 value.append(vDatetime(self.end).to_jcal(name)[-1])
2148 return [name, self.params.to_jcal(exclude_utc=True), self.VALUE.lower(), value]
2149
2150 @classmethod
2151 def parse_jcal_value(
2152 cls, jcal: str | list
2153 ) -> tuple[datetime, datetime] | tuple[datetime, timedelta]:
2154 """Parse a jCal value.
2155
2156 Raises:
2157 ~error.JCalParsingError: If the period is not a list with exactly two items,
2158 or it can't parse a date-time or duration.
2159 """
2160 if isinstance(jcal, str) and "/" in jcal:
2161 # only occurs in the example of RFC7265, Section B.2.2.
2162 jcal = jcal.split("/")
2163 if not isinstance(jcal, list) or len(jcal) != 2:
2164 raise JCalParsingError(
2165 "A period must be a list with exactly 2 items.", cls, value=jcal
2166 )
2167 with JCalParsingError.reraise_with_path_added(0):
2168 start = vDatetime.parse_jcal_value(jcal[0])
2169 with JCalParsingError.reraise_with_path_added(1):
2170 JCalParsingError.validate_value_type(jcal[1], str, cls)
2171 if jcal[1].startswith(("P", "-P", "+P")):
2172 end_or_duration = vDuration.parse_jcal_value(jcal[1])
2173 else:
2174 try:
2175 end_or_duration = vDatetime.parse_jcal_value(jcal[1])
2176 except JCalParsingError as e:
2177 raise JCalParsingError(
2178 "Cannot parse date-time or duration.",
2179 cls,
2180 value=jcal[1],
2181 ) from e
2182 return start, end_or_duration
2183
2184 @classmethod
2185 def from_jcal(cls, jcal_property: list) -> Self:
2186 """Parse jCal from :rfc:`7265`.
2187
2188 Args:
2189 jcal_property: The jCal property to parse.
2190
2191 Raises:
2192 ~error.JCalParsingError: If the provided jCal is invalid.
2193 """
2194 JCalParsingError.validate_property(jcal_property, cls)
2195 with JCalParsingError.reraise_with_path_added(3):
2196 start, end_or_duration = cls.parse_jcal_value(jcal_property[3])
2197 params = Parameters.from_jcal_property(jcal_property)
2198 tzid = params.tzid
2199
2200 if tzid:
2201 start = tzp.localize(start, tzid)
2202 if is_datetime(end_or_duration):
2203 end_or_duration = tzp.localize(end_or_duration, tzid)
2204
2205 return cls((start, end_or_duration), params=params)
2206
2207
2208class vWeekday(str):
2209 """Either a ``weekday`` or a ``weekdaynum``.
2210
2211 .. code-block:: pycon
2212
2213 >>> from icalendar import vWeekday
2214 >>> vWeekday("MO") # Simple weekday
2215 'MO'
2216 >>> vWeekday("2FR").relative # Second friday
2217 2
2218 >>> vWeekday("2FR").weekday
2219 'FR'
2220 >>> vWeekday("-1SU").relative # Last Sunday
2221 -1
2222
2223 Definition from `RFC 5545, Section 3.3.10 <https://www.rfc-editor.org/rfc/rfc5545#section-3.3.10>`_:
2224
2225 .. code-block:: text
2226
2227 weekdaynum = [[plus / minus] ordwk] weekday
2228 plus = "+"
2229 minus = "-"
2230 ordwk = 1*2DIGIT ;1 to 53
2231 weekday = "SU" / "MO" / "TU" / "WE" / "TH" / "FR" / "SA"
2232 ;Corresponding to SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY,
2233 ;FRIDAY, and SATURDAY days of the week.
2234
2235 """
2236
2237 params: Parameters
2238 __slots__ = ("params", "relative", "weekday")
2239
2240 week_days = CaselessDict(
2241 {
2242 "SU": 0,
2243 "MO": 1,
2244 "TU": 2,
2245 "WE": 3,
2246 "TH": 4,
2247 "FR": 5,
2248 "SA": 6,
2249 }
2250 )
2251
2252 def __new__(
2253 cls,
2254 value,
2255 encoding=DEFAULT_ENCODING,
2256 /,
2257 params: dict[str, Any] | None = None,
2258 ):
2259 value = to_unicode(value, encoding=encoding)
2260 self = super().__new__(cls, value)
2261 match = WEEKDAY_RULE.match(self)
2262 if match is None:
2263 raise ValueError(f"Expected weekday abbrevation, got: {self}")
2264 match = match.groupdict()
2265 sign = match["signal"]
2266 weekday = match["weekday"]
2267 relative = match["relative"]
2268 if weekday not in vWeekday.week_days or sign not in "+-":
2269 raise ValueError(f"Expected weekday abbrevation, got: {self}")
2270 self.weekday = weekday or None
2271 self.relative = (relative and int(relative)) or None
2272 if sign == "-" and self.relative:
2273 self.relative *= -1
2274 self.params = Parameters(params)
2275 return self
2276
2277 def to_ical(self):
2278 return self.encode(DEFAULT_ENCODING).upper()
2279
2280 @classmethod
2281 def from_ical(cls, ical):
2282 try:
2283 return cls(ical.upper())
2284 except Exception as e:
2285 raise ValueError(f"Expected weekday abbrevation, got: {ical}") from e
2286
2287 @classmethod
2288 def parse_jcal_value(cls, value: Any) -> vWeekday:
2289 """Parse a jCal value for vWeekday.
2290
2291 Raises:
2292 ~error.JCalParsingError: If the value is not a valid weekday.
2293 """
2294 JCalParsingError.validate_value_type(value, str, cls)
2295 try:
2296 return cls(value)
2297 except ValueError as e:
2298 raise JCalParsingError(
2299 "The value must be a valid weekday.", cls, value=value
2300 ) from e
2301
2302
2303class vFrequency(str):
2304 """A simple class that catches illegal values."""
2305
2306 params: Parameters
2307 __slots__ = ("params",)
2308
2309 frequencies = CaselessDict(
2310 {
2311 "SECONDLY": "SECONDLY",
2312 "MINUTELY": "MINUTELY",
2313 "HOURLY": "HOURLY",
2314 "DAILY": "DAILY",
2315 "WEEKLY": "WEEKLY",
2316 "MONTHLY": "MONTHLY",
2317 "YEARLY": "YEARLY",
2318 }
2319 )
2320
2321 def __new__(
2322 cls,
2323 value,
2324 encoding=DEFAULT_ENCODING,
2325 /,
2326 params: dict[str, Any] | None = None,
2327 ):
2328 value = to_unicode(value, encoding=encoding)
2329 self = super().__new__(cls, value)
2330 if self not in vFrequency.frequencies:
2331 raise ValueError(f"Expected frequency, got: {self}")
2332 self.params = Parameters(params)
2333 return self
2334
2335 def to_ical(self):
2336 return self.encode(DEFAULT_ENCODING).upper()
2337
2338 @classmethod
2339 def from_ical(cls, ical):
2340 try:
2341 return cls(ical.upper())
2342 except Exception as e:
2343 raise ValueError(f"Expected frequency, got: {ical}") from e
2344
2345 @classmethod
2346 def parse_jcal_value(cls, value: Any) -> vFrequency:
2347 """Parse a jCal value for vFrequency.
2348
2349 Raises:
2350 ~error.JCalParsingError: If the value is not a valid frequency.
2351 """
2352 JCalParsingError.validate_value_type(value, str, cls)
2353 try:
2354 return cls(value)
2355 except ValueError as e:
2356 raise JCalParsingError(
2357 "The value must be a valid frequency.", cls, value=value
2358 ) from e
2359
2360
2361class vMonth(int):
2362 """The number of the month for recurrence.
2363
2364 In :rfc:`5545`, this is just an int.
2365 In :rfc:`7529`, this can be followed by `L` to indicate a leap month.
2366
2367 .. code-block:: pycon
2368
2369 >>> from icalendar import vMonth
2370 >>> vMonth(1) # first month January
2371 vMonth('1')
2372 >>> vMonth("5L") # leap month in Hebrew calendar
2373 vMonth('5L')
2374 >>> vMonth(1).leap
2375 False
2376 >>> vMonth("5L").leap
2377 True
2378
2379 Definition from RFC:
2380
2381 .. code-block:: text
2382
2383 type-bymonth = element bymonth {
2384 xsd:positiveInteger |
2385 xsd:string
2386 }
2387 """
2388
2389 params: Parameters
2390
2391 def __new__(cls, month: Union[str, int], /, params: dict[str, Any] | None = None):
2392 if isinstance(month, vMonth):
2393 return cls(month.to_ical().decode())
2394 if isinstance(month, str):
2395 if month.isdigit():
2396 month_index = int(month)
2397 leap = False
2398 else:
2399 if not month or (month[-1] != "L" and month[:-1].isdigit()):
2400 raise ValueError(f"Invalid month: {month!r}")
2401 month_index = int(month[:-1])
2402 leap = True
2403 else:
2404 leap = False
2405 month_index = int(month)
2406 self = super().__new__(cls, month_index)
2407 self.leap = leap
2408 self.params = Parameters(params)
2409 return self
2410
2411 def to_ical(self) -> bytes:
2412 """The ical representation."""
2413 return str(self).encode("utf-8")
2414
2415 @classmethod
2416 def from_ical(cls, ical: str):
2417 return cls(ical)
2418
2419 @property
2420 def leap(self) -> bool:
2421 """Whether this is a leap month."""
2422 return self._leap
2423
2424 @leap.setter
2425 def leap(self, value: bool) -> None:
2426 self._leap = value
2427
2428 def __repr__(self) -> str:
2429 """repr(self)"""
2430 return f"{self.__class__.__name__}({str(self)!r})"
2431
2432 def __str__(self) -> str:
2433 """str(self)"""
2434 return f"{int(self)}{'L' if self.leap else ''}"
2435
2436 @classmethod
2437 def parse_jcal_value(cls, value: Any) -> vMonth:
2438 """Parse a jCal value for vMonth.
2439
2440 Raises:
2441 ~error.JCalParsingError: If the value is not a valid month.
2442 """
2443 JCalParsingError.validate_value_type(value, (str, int), cls)
2444 try:
2445 return cls(value)
2446 except ValueError as e:
2447 raise JCalParsingError(
2448 "The value must be a string or an integer.", cls, value=value
2449 ) from e
2450
2451
2452class vSkip(vText, Enum):
2453 """Skip values for RRULE.
2454
2455 These are defined in :rfc:`7529`.
2456
2457 OMIT is the default value.
2458
2459 Examples:
2460
2461 .. code-block:: pycon
2462
2463 >>> from icalendar import vSkip
2464 >>> vSkip.OMIT
2465 vSkip('OMIT')
2466 >>> vSkip.FORWARD
2467 vSkip('FORWARD')
2468 >>> vSkip.BACKWARD
2469 vSkip('BACKWARD')
2470 """
2471
2472 OMIT = "OMIT"
2473 FORWARD = "FORWARD"
2474 BACKWARD = "BACKWARD"
2475
2476 __reduce_ex__ = Enum.__reduce_ex__
2477
2478 def __repr__(self):
2479 return f"{self.__class__.__name__}({self._name_!r})"
2480
2481 @classmethod
2482 def parse_jcal_value(cls, value: Any) -> vSkip:
2483 """Parse a jCal value for vSkip.
2484
2485 Raises:
2486 ~error.JCalParsingError: If the value is not a valid skip value.
2487 """
2488 JCalParsingError.validate_value_type(value, str, cls)
2489 try:
2490 return cls[value.upper()]
2491 except KeyError as e:
2492 raise JCalParsingError(
2493 "The value must be a valid skip value.", cls, value=value
2494 ) from e
2495
2496
2497class vRecur(CaselessDict):
2498 """Recurrence definition.
2499
2500 Property Name:
2501 RRULE
2502
2503 Purpose:
2504 This property defines a rule or repeating pattern for recurring events, to-dos,
2505 journal entries, or time zone definitions.
2506
2507 Value Type:
2508 RECUR
2509
2510 Property Parameters:
2511 IANA and non-standard property parameters can be specified on this property.
2512
2513 Conformance:
2514 This property can be specified in recurring "VEVENT", "VTODO", and "VJOURNAL"
2515 calendar components as well as in the "STANDARD" and "DAYLIGHT" sub-components
2516 of the "VTIMEZONE" calendar component, but it SHOULD NOT be specified more than once.
2517 The recurrence set generated with multiple "RRULE" properties is undefined.
2518
2519 Description:
2520 The recurrence rule, if specified, is used in computing the recurrence set.
2521 The recurrence set is the complete set of recurrence instances for a calendar component.
2522 The recurrence set is generated by considering the initial "DTSTART" property along
2523 with the "RRULE", "RDATE", and "EXDATE" properties contained within the
2524 recurring component. The "DTSTART" property defines the first instance in the
2525 recurrence set. The "DTSTART" property value SHOULD be synchronized with the
2526 recurrence rule, if specified. The recurrence set generated with a "DTSTART" property
2527 value not synchronized with the recurrence rule is undefined.
2528 The final recurrence set is generated by gathering all of the start DATE-TIME
2529 values generated by any of the specified "RRULE" and "RDATE" properties, and then
2530 excluding any start DATE-TIME values specified by "EXDATE" properties.
2531 This implies that start DATE- TIME values specified by "EXDATE" properties take
2532 precedence over those specified by inclusion properties (i.e., "RDATE" and "RRULE").
2533 Where duplicate instances are generated by the "RRULE" and "RDATE" properties,
2534 only one recurrence is considered. Duplicate instances are ignored.
2535
2536 The "DTSTART" property specified within the iCalendar object defines the first
2537 instance of the recurrence. In most cases, a "DTSTART" property of DATE-TIME value
2538 type used with a recurrence rule, should be specified as a date with local time
2539 and time zone reference to make sure all the recurrence instances start at the
2540 same local time regardless of time zone changes.
2541
2542 If the duration of the recurring component is specified with the "DTEND" or
2543 "DUE" property, then the same exact duration will apply to all the members of the
2544 generated recurrence set. Else, if the duration of the recurring component is
2545 specified with the "DURATION" property, then the same nominal duration will apply
2546 to all the members of the generated recurrence set and the exact duration of each
2547 recurrence instance will depend on its specific start time. For example, recurrence
2548 instances of a nominal duration of one day will have an exact duration of more or less
2549 than 24 hours on a day where a time zone shift occurs. The duration of a specific
2550 recurrence may be modified in an exception component or simply by using an
2551 "RDATE" property of PERIOD value type.
2552
2553 Examples:
2554 The following RRULE specifies daily events for 10 occurrences.
2555
2556 .. code-block:: text
2557
2558 RRULE:FREQ=DAILY;COUNT=10
2559
2560 Below, we parse the RRULE ical string.
2561
2562 .. code-block:: pycon
2563
2564 >>> from icalendar.prop import vRecur
2565 >>> rrule = vRecur.from_ical('FREQ=DAILY;COUNT=10')
2566 >>> rrule
2567 vRecur({'FREQ': ['DAILY'], 'COUNT': [10]})
2568
2569 You can choose to add an rrule to an :class:`icalendar.cal.Event` or
2570 :class:`icalendar.cal.Todo`.
2571
2572 .. code-block:: pycon
2573
2574 >>> from icalendar import Event
2575 >>> event = Event()
2576 >>> event.add('RRULE', 'FREQ=DAILY;COUNT=10')
2577 >>> event.rrules
2578 [vRecur({'FREQ': ['DAILY'], 'COUNT': [10]})]
2579 """ # noqa: E501
2580
2581 default_value: ClassVar[str] = "RECUR"
2582 params: Parameters
2583
2584 frequencies = [
2585 "SECONDLY",
2586 "MINUTELY",
2587 "HOURLY",
2588 "DAILY",
2589 "WEEKLY",
2590 "MONTHLY",
2591 "YEARLY",
2592 ]
2593
2594 # Mac iCal ignores RRULEs where FREQ is not the first rule part.
2595 # Sorts parts according to the order listed in RFC 5545, section 3.3.10.
2596 canonical_order = (
2597 "RSCALE",
2598 "FREQ",
2599 "UNTIL",
2600 "COUNT",
2601 "INTERVAL",
2602 "BYSECOND",
2603 "BYMINUTE",
2604 "BYHOUR",
2605 "BYDAY",
2606 "BYWEEKDAY",
2607 "BYMONTHDAY",
2608 "BYYEARDAY",
2609 "BYWEEKNO",
2610 "BYMONTH",
2611 "BYSETPOS",
2612 "WKST",
2613 "SKIP",
2614 )
2615
2616 types = CaselessDict(
2617 {
2618 "COUNT": vInt,
2619 "INTERVAL": vInt,
2620 "BYSECOND": vInt,
2621 "BYMINUTE": vInt,
2622 "BYHOUR": vInt,
2623 "BYWEEKNO": vInt,
2624 "BYMONTHDAY": vInt,
2625 "BYYEARDAY": vInt,
2626 "BYMONTH": vMonth,
2627 "UNTIL": vDDDTypes,
2628 "BYSETPOS": vInt,
2629 "WKST": vWeekday,
2630 "BYDAY": vWeekday,
2631 "FREQ": vFrequency,
2632 "BYWEEKDAY": vWeekday,
2633 "SKIP": vSkip, # RFC 7529
2634 "RSCALE": vText, # RFC 7529
2635 }
2636 )
2637
2638 # for reproducible serialization:
2639 # RULE: if and only if it can be a list it will be a list
2640 # look up in RFC
2641 jcal_not_a_list = {"FREQ", "UNTIL", "COUNT", "INTERVAL", "WKST", "SKIP", "RSCALE"}
2642
2643 def __init__(self, *args, params: dict[str, Any] | None = None, **kwargs):
2644 if args and isinstance(args[0], str):
2645 # we have a string as an argument.
2646 args = (self.from_ical(args[0]),) + args[1:]
2647 for k, v in kwargs.items():
2648 if not isinstance(v, SEQUENCE_TYPES):
2649 kwargs[k] = [v]
2650 super().__init__(*args, **kwargs)
2651 self.params = Parameters(params)
2652
2653 def to_ical(self):
2654 result = []
2655 for key, vals in self.sorted_items():
2656 typ = self.types.get(key, vText)
2657 if not isinstance(vals, SEQUENCE_TYPES):
2658 vals = [vals] # noqa: PLW2901
2659 param_vals = b",".join(typ(val).to_ical() for val in vals)
2660
2661 # CaselessDict keys are always unicode
2662 param_key = key.encode(DEFAULT_ENCODING)
2663 result.append(param_key + b"=" + param_vals)
2664
2665 return b";".join(result)
2666
2667 @classmethod
2668 def parse_type(cls, key, values):
2669 # integers
2670 parser = cls.types.get(key, vText)
2671 return [parser.from_ical(v) for v in values.split(",")]
2672
2673 @classmethod
2674 def from_ical(cls, ical: str):
2675 if isinstance(ical, cls):
2676 return ical
2677 try:
2678 recur = cls()
2679 for pairs in ical.split(";"):
2680 try:
2681 key, vals = pairs.split("=")
2682 except ValueError:
2683 # E.g. incorrect trailing semicolon, like (issue #157):
2684 # FREQ=YEARLY;BYMONTH=11;BYDAY=1SU;
2685 continue
2686 recur[key] = cls.parse_type(key, vals)
2687 return cls(recur)
2688 except ValueError:
2689 raise
2690 except Exception as e:
2691 raise ValueError(f"Error in recurrence rule: {ical}") from e
2692
2693 @classmethod
2694 def examples(cls) -> list[vRecur]:
2695 """Examples of vRecur."""
2696 return [cls.from_ical("FREQ=DAILY;COUNT=10")]
2697
2698 from icalendar.param import VALUE
2699
2700 def to_jcal(self, name: str) -> list:
2701 """The jCal representation of this property according to :rfc:`7265`."""
2702 recur = {}
2703 for k, v in self.items():
2704 key = k.lower()
2705 if key.upper() in self.jcal_not_a_list:
2706 value = v[0] if isinstance(v, list) and len(v) == 1 else v
2707 elif not isinstance(v, list):
2708 value = [v]
2709 else:
2710 value = v
2711 recur[key] = value
2712 if "until" in recur:
2713 until = recur["until"]
2714 until_jcal = vDDDTypes(until).to_jcal("until")
2715 recur["until"] = until_jcal[-1]
2716 return [name, self.params.to_jcal(), self.VALUE.lower(), recur]
2717
2718 @classmethod
2719 def from_jcal(cls, jcal_property: list) -> Self:
2720 """Parse jCal from :rfc:`7265`.
2721
2722 Args:
2723 jcal_property: The jCal property to parse.
2724
2725 Raises:
2726 ~error.JCalParsingError: If the provided jCal is invalid.
2727 """
2728 JCalParsingError.validate_property(jcal_property, cls)
2729 params = Parameters.from_jcal_property(jcal_property)
2730 if not isinstance(jcal_property[3], dict) or not all(
2731 isinstance(k, str) for k in jcal_property[3]
2732 ):
2733 raise JCalParsingError(
2734 "The recurrence rule must be a mapping with string keys.",
2735 cls,
2736 3,
2737 value=jcal_property[3],
2738 )
2739 recur = {}
2740 for key, value in jcal_property[3].items():
2741 value_type = cls.types.get(key, vText)
2742 with JCalParsingError.reraise_with_path_added(3, key):
2743 if isinstance(value, list):
2744 recur[key.lower()] = values = []
2745 for i, v in enumerate(value):
2746 with JCalParsingError.reraise_with_path_added(i):
2747 values.append(value_type.parse_jcal_value(v))
2748 else:
2749 recur[key] = value_type.parse_jcal_value(value)
2750 until = recur.get("until")
2751 if until is not None and not isinstance(until, list):
2752 recur["until"] = [until]
2753 return cls(recur, params=params)
2754
2755 def __eq__(self, other: object) -> bool:
2756 """self == other"""
2757 if not isinstance(other, vRecur):
2758 return super().__eq__(other)
2759 if self.keys() != other.keys():
2760 return False
2761 for key in self.keys():
2762 v1 = self[key]
2763 v2 = other[key]
2764 if not isinstance(v1, SEQUENCE_TYPES):
2765 v1 = [v1]
2766 if not isinstance(v2, SEQUENCE_TYPES):
2767 v2 = [v2]
2768 if v1 != v2:
2769 return False
2770 return True
2771
2772
2773TIME_JCAL_REGEX = re.compile(
2774 r"^(?P<hour>[0-9]{2}):(?P<minute>[0-9]{2}):(?P<second>[0-9]{2})(?P<utc>Z)?$"
2775)
2776
2777
2778class vTime(TimeBase):
2779 """Time
2780
2781 Value Name:
2782 TIME
2783
2784 Purpose:
2785 This value type is used to identify values that contain a
2786 time of day.
2787
2788 Format Definition:
2789 This value type is defined by the following notation:
2790
2791 .. code-block:: text
2792
2793 time = time-hour time-minute time-second [time-utc]
2794
2795 time-hour = 2DIGIT ;00-23
2796 time-minute = 2DIGIT ;00-59
2797 time-second = 2DIGIT ;00-60
2798 ;The "60" value is used to account for positive "leap" seconds.
2799
2800 time-utc = "Z"
2801
2802 Description:
2803 If the property permits, multiple "time" values are
2804 specified by a COMMA-separated list of values. No additional
2805 content value encoding (i.e., BACKSLASH character encoding, see
2806 vText) is defined for this value type.
2807
2808 The "TIME" value type is used to identify values that contain a
2809 time of day. The format is based on the [ISO.8601.2004] complete
2810 representation, basic format for a time of day. The text format
2811 consists of a two-digit, 24-hour of the day (i.e., values 00-23),
2812 two-digit minute in the hour (i.e., values 00-59), and two-digit
2813 seconds in the minute (i.e., values 00-60). The seconds value of
2814 60 MUST only be used to account for positive "leap" seconds.
2815 Fractions of a second are not supported by this format.
2816
2817 In parallel to the "DATE-TIME" definition above, the "TIME" value
2818 type expresses time values in three forms:
2819
2820 The form of time with UTC offset MUST NOT be used. For example,
2821 the following is not valid for a time value:
2822
2823 .. code-block:: text
2824
2825 230000-0800 ;Invalid time format
2826
2827 **FORM #1 LOCAL TIME**
2828
2829 The local time form is simply a time value that does not contain
2830 the UTC designator nor does it reference a time zone. For
2831 example, 11:00 PM:
2832
2833 .. code-block:: text
2834
2835 230000
2836
2837 Time values of this type are said to be "floating" and are not
2838 bound to any time zone in particular. They are used to represent
2839 the same hour, minute, and second value regardless of which time
2840 zone is currently being observed. For example, an event can be
2841 defined that indicates that an individual will be busy from 11:00
2842 AM to 1:00 PM every day, no matter which time zone the person is
2843 in. In these cases, a local time can be specified. The recipient
2844 of an iCalendar object with a property value consisting of a local
2845 time, without any relative time zone information, SHOULD interpret
2846 the value as being fixed to whatever time zone the "ATTENDEE" is
2847 in at any given moment. This means that two "Attendees", may
2848 participate in the same event at different UTC times; floating
2849 time SHOULD only be used where that is reasonable behavior.
2850
2851 In most cases, a fixed time is desired. To properly communicate a
2852 fixed time in a property value, either UTC time or local time with
2853 time zone reference MUST be specified.
2854
2855 The use of local time in a TIME value without the "TZID" property
2856 parameter is to be interpreted as floating time, regardless of the
2857 existence of "VTIMEZONE" calendar components in the iCalendar
2858 object.
2859
2860 **FORM #2: UTC TIME**
2861
2862 UTC time, or absolute time, is identified by a LATIN CAPITAL
2863 LETTER Z suffix character, the UTC designator, appended to the
2864 time value. For example, the following represents 07:00 AM UTC:
2865
2866 .. code-block:: text
2867
2868 070000Z
2869
2870 The "TZID" property parameter MUST NOT be applied to TIME
2871 properties whose time values are specified in UTC.
2872
2873 **FORM #3: LOCAL TIME AND TIME ZONE REFERENCE**
2874
2875 The local time with reference to time zone information form is
2876 identified by the use the "TZID" property parameter to reference
2877 the appropriate time zone definition.
2878
2879 Example:
2880 The following represents 8:30 AM in New York in winter,
2881 five hours behind UTC, in each of the three formats:
2882
2883 .. code-block:: text
2884
2885 083000
2886 133000Z
2887 TZID=America/New_York:083000
2888 """
2889
2890 default_value: ClassVar[str] = "TIME"
2891 params: Parameters
2892
2893 def __init__(self, *args, params: dict[str, Any] | None = None):
2894 if len(args) == 1:
2895 if not isinstance(args[0], (time, datetime)):
2896 raise ValueError(f"Expected a datetime.time, got: {args[0]}")
2897 self.dt = args[0]
2898 else:
2899 self.dt = time(*args)
2900 self.params = Parameters(params or {})
2901 self.params.update_tzid_from(self.dt)
2902
2903 def to_ical(self):
2904 value = self.dt.strftime("%H%M%S")
2905 if self.is_utc():
2906 value += "Z"
2907 return value
2908
2909 def is_utc(self) -> bool:
2910 """Whether this time is UTC."""
2911 return self.params.is_utc() or is_utc(self.dt)
2912
2913 @staticmethod
2914 def from_ical(ical):
2915 # TODO: timezone support
2916 try:
2917 timetuple = (int(ical[:2]), int(ical[2:4]), int(ical[4:6]))
2918 return time(*timetuple)
2919 except Exception as e:
2920 raise ValueError(f"Expected time, got: {ical}") from e
2921
2922 @classmethod
2923 def examples(cls) -> list[vTime]:
2924 """Examples of vTime."""
2925 return [cls(time(12, 30))]
2926
2927 from icalendar.param import VALUE
2928
2929 def to_jcal(self, name: str) -> list:
2930 """The jCal representation of this property according to :rfc:`7265`."""
2931 value = self.dt.strftime("%H:%M:%S")
2932 if self.is_utc():
2933 value += "Z"
2934 return [name, self.params.to_jcal(exclude_utc=True), self.VALUE.lower(), value]
2935
2936 @classmethod
2937 def parse_jcal_value(cls, jcal: str) -> time:
2938 """Parse a jCal string to a :py:class:`datetime.time`.
2939
2940 Raises:
2941 ~error.JCalParsingError: If it can't parse a time.
2942 """
2943 JCalParsingError.validate_value_type(jcal, str, cls)
2944 match = TIME_JCAL_REGEX.match(jcal)
2945 if match is None:
2946 raise JCalParsingError("Cannot parse time.", cls, value=jcal)
2947 hour = int(match.group("hour"))
2948 minute = int(match.group("minute"))
2949 second = int(match.group("second"))
2950 utc = bool(match.group("utc"))
2951 return time(hour, minute, second, tzinfo=timezone.utc if utc else None)
2952
2953 @classmethod
2954 def from_jcal(cls, jcal_property: list) -> Self:
2955 """Parse jCal from :rfc:`7265`.
2956
2957 Args:
2958 jcal_property: The jCal property to parse.
2959
2960 Raises:
2961 ~error.JCalParsingError: If the provided jCal is invalid.
2962 """
2963 JCalParsingError.validate_property(jcal_property, cls)
2964 with JCalParsingError.reraise_with_path_added(3):
2965 value = cls.parse_jcal_value(jcal_property[3])
2966 return cls(
2967 value,
2968 params=Parameters.from_jcal_property(jcal_property),
2969 )
2970
2971
2972class vUri(str):
2973 """URI
2974
2975 Value Name:
2976 URI
2977
2978 Purpose:
2979 This value type is used to identify values that contain a
2980 uniform resource identifier (URI) type of reference to the
2981 property value.
2982
2983 Format Definition:
2984 This value type is defined by the following notation:
2985
2986 .. code-block:: text
2987
2988 uri = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
2989
2990 Description:
2991 This value type might be used to reference binary
2992 information, for values that are large, or otherwise undesirable
2993 to include directly in the iCalendar object.
2994
2995 Property values with this value type MUST follow the generic URI
2996 syntax defined in [RFC3986].
2997
2998 When a property parameter value is a URI value type, the URI MUST
2999 be specified as a quoted-string value.
3000
3001 Examples:
3002 The following is a URI for a network file:
3003
3004 .. code-block:: text
3005
3006 http://example.com/my-report.txt
3007
3008 .. code-block:: pycon
3009
3010 >>> from icalendar.prop import vUri
3011 >>> uri = vUri.from_ical('http://example.com/my-report.txt')
3012 >>> uri
3013 vUri('http://example.com/my-report.txt')
3014 >>> uri.uri
3015 'http://example.com/my-report.txt'
3016 """
3017
3018 default_value: ClassVar[str] = "URI"
3019 params: Parameters
3020 __slots__ = ("params",)
3021
3022 def __new__(
3023 cls,
3024 value: str,
3025 encoding: str = DEFAULT_ENCODING,
3026 /,
3027 params: dict[str, Any] | None = None,
3028 ):
3029 value = to_unicode(value, encoding=encoding)
3030 self = super().__new__(cls, value)
3031 self.params = Parameters(params)
3032 return self
3033
3034 def to_ical(self):
3035 return self.encode(DEFAULT_ENCODING)
3036
3037 @classmethod
3038 def from_ical(cls, ical):
3039 try:
3040 return cls(ical)
3041 except Exception as e:
3042 raise ValueError(f"Expected , got: {ical}") from e
3043
3044 @classmethod
3045 def examples(cls) -> list[vUri]:
3046 """Examples of vUri."""
3047 return [cls("http://example.com/my-report.txt")]
3048
3049 def to_jcal(self, name: str) -> list:
3050 """The jCal representation of this property according to :rfc:`7265`."""
3051 return [name, self.params.to_jcal(), self.VALUE.lower(), str(self)]
3052
3053 @classmethod
3054 def from_jcal(cls, jcal_property: list) -> Self:
3055 """Parse jCal from :rfc:`7265`.
3056
3057 Args:
3058 jcal_property: The jCal property to parse.
3059
3060 Raises:
3061 ~error.JCalParsingError: If the provided jCal is invalid.
3062 """
3063 JCalParsingError.validate_property(jcal_property, cls)
3064 return cls(
3065 jcal_property[3],
3066 Parameters.from_jcal_property(jcal_property),
3067 )
3068
3069 @property
3070 def ical_value(self) -> str:
3071 """The URI."""
3072 return self.uri
3073
3074 @property
3075 def uri(self) -> str:
3076 """The URI."""
3077 return str(self)
3078
3079 def __repr__(self) -> str:
3080 """repr(self)"""
3081 return f"{self.__class__.__name__}({self.uri!r})"
3082
3083 from icalendar.param import FMTTYPE, GAP, LABEL, LANGUAGE, LINKREL, RELTYPE, VALUE
3084
3085
3086class vUid(vText):
3087 """A UID of a component.
3088
3089 This is defined in :rfc:`9253`, Section 7.
3090 """
3091
3092 default_value: ClassVar[str] = "UID"
3093
3094 @classmethod
3095 def new(cls):
3096 """Create a new UID for convenience.
3097
3098 .. code-block:: pycon
3099
3100 >>> from icalendar import vUid
3101 >>> vUid.new()
3102 vUid('d755cef5-2311-46ed-a0e1-6733c9e15c63')
3103
3104 """
3105 return vUid(uuid.uuid4())
3106
3107 @property
3108 def uid(self) -> str:
3109 """The UID of this property."""
3110 return str(self)
3111
3112 @property
3113 def ical_value(self) -> str:
3114 """The UID of this property."""
3115 return self.uid
3116
3117 def __repr__(self) -> str:
3118 """repr(self)"""
3119 return f"{self.__class__.__name__}({self.uid!r})"
3120
3121 from icalendar.param import FMTTYPE, LABEL, LINKREL
3122
3123 @classmethod
3124 def examples(cls) -> list[vUid]:
3125 """Examples of vUid."""
3126 return [cls("d755cef5-2311-46ed-a0e1-6733c9e15c63")]
3127
3128
3129class vXmlReference(vUri):
3130 """An XML-REFERENCE.
3131
3132 The associated value references an associated XML artifact and
3133 is a URI with an XPointer anchor value.
3134
3135 This is defined in :rfc:`9253`, Section 7.
3136 """
3137
3138 default_value: ClassVar[str] = "XML-REFERENCE"
3139
3140 @property
3141 def xml_reference(self) -> str:
3142 """The XML reference URI of this property."""
3143 return self.uri
3144
3145 @property
3146 def x_pointer(self) -> str | None:
3147 """The XPointer of the URI.
3148
3149 The XPointer is defined in `W3C.WD-xptr-xpointer-20021219
3150 <https://www.rfc-editor.org/rfc/rfc9253.html#W3C.WD-xptr-xpointer-20021219>`_,
3151 and its use as an anchor is defined in `W3C.REC-xptr-framework-20030325
3152 <https://www.rfc-editor.org/rfc/rfc9253.html#W3C.REC-xptr-framework-20030325>`_.
3153
3154 Returns:
3155 The decoded x-pointer or ``None`` if no valid x-pointer is found.
3156 """
3157 from urllib.parse import unquote, urlparse
3158
3159 parsed = urlparse(self.xml_reference)
3160 fragment = unquote(parsed.fragment)
3161 if not fragment.startswith("xpointer(") or not fragment.endswith(")"):
3162 return None
3163 return fragment[9:-1]
3164
3165 @classmethod
3166 def examples(cls) -> list[vXmlReference]:
3167 """Examples of vXmlReference."""
3168 return [cls("http://example.com/doc.xml#xpointer(/doc/element)")]
3169
3170
3171class vGeo:
3172 """Geographic Position
3173
3174 Property Name:
3175 GEO
3176
3177 Purpose:
3178 This property specifies information related to the global
3179 position for the activity specified by a calendar component.
3180
3181 Value Type:
3182 FLOAT. The value MUST be two SEMICOLON-separated FLOAT values.
3183
3184 Property Parameters:
3185 IANA and non-standard property parameters can be specified on
3186 this property.
3187
3188 Conformance:
3189 This property can be specified in "VEVENT" or "VTODO"
3190 calendar components.
3191
3192 Description:
3193 This property value specifies latitude and longitude,
3194 in that order (i.e., "LAT LON" ordering). The longitude
3195 represents the location east or west of the prime meridian as a
3196 positive or negative real number, respectively. The longitude and
3197 latitude values MAY be specified up to six decimal places, which
3198 will allow for accuracy to within one meter of geographical
3199 position. Receiving applications MUST accept values of this
3200 precision and MAY truncate values of greater precision.
3201
3202 Example:
3203
3204 .. code-block:: text
3205
3206 GEO:37.386013;-122.082932
3207
3208 Parse vGeo:
3209
3210 .. code-block:: pycon
3211
3212 >>> from icalendar.prop import vGeo
3213 >>> geo = vGeo.from_ical('37.386013;-122.082932')
3214 >>> geo
3215 (37.386013, -122.082932)
3216
3217 Add a geo location to an event:
3218
3219 .. code-block:: pycon
3220
3221 >>> from icalendar import Event
3222 >>> event = Event()
3223 >>> latitude = 37.386013
3224 >>> longitude = -122.082932
3225 >>> event.add('GEO', (latitude, longitude))
3226 >>> event['GEO']
3227 vGeo((37.386013, -122.082932))
3228 """
3229
3230 default_value: ClassVar[str] = "FLOAT"
3231 params: Parameters
3232
3233 def __init__(
3234 self,
3235 geo: tuple[float | str | int, float | str | int],
3236 /,
3237 params: dict[str, Any] | None = None,
3238 ):
3239 """Create a new vGeo from a tuple of (latitude, longitude).
3240
3241 Raises:
3242 ValueError: if geo is not a tuple of (latitude, longitude)
3243 """
3244 try:
3245 latitude, longitude = (geo[0], geo[1])
3246 latitude = float(latitude)
3247 longitude = float(longitude)
3248 except Exception as e:
3249 raise ValueError(
3250 "Input must be (float, float) for latitude and longitude"
3251 ) from e
3252 self.latitude = latitude
3253 self.longitude = longitude
3254 self.params = Parameters(params)
3255
3256 def to_ical(self):
3257 return f"{self.latitude};{self.longitude}"
3258
3259 @staticmethod
3260 def from_ical(ical):
3261 try:
3262 latitude, longitude = ical.split(";")
3263 return (float(latitude), float(longitude))
3264 except Exception as e:
3265 raise ValueError(f"Expected 'float;float' , got: {ical}") from e
3266
3267 def __eq__(self, other):
3268 return self.to_ical() == other.to_ical()
3269
3270 def __repr__(self):
3271 """repr(self)"""
3272 return f"{self.__class__.__name__}(({self.latitude}, {self.longitude}))"
3273
3274 def to_jcal(self, name: str) -> list:
3275 """Convert to jCal object."""
3276 return [
3277 name,
3278 self.params.to_jcal(),
3279 self.VALUE.lower(),
3280 [self.latitude, self.longitude],
3281 ]
3282
3283 @classmethod
3284 def examples(cls) -> list[vGeo]:
3285 """Examples of vGeo."""
3286 return [cls((37.386013, -122.082932))]
3287
3288 from icalendar.param import VALUE
3289
3290 @classmethod
3291 def from_jcal(cls, jcal_property: list) -> Self:
3292 """Parse jCal from :rfc:`7265`.
3293
3294 Args:
3295 jcal_property: The jCal property to parse.
3296
3297 Raises:
3298 ~error.JCalParsingError: If the provided jCal is invalid.
3299 """
3300 JCalParsingError.validate_property(jcal_property, cls)
3301 return cls(
3302 jcal_property[3],
3303 Parameters.from_jcal_property(jcal_property),
3304 )
3305
3306
3307UTC_OFFSET_JCAL_REGEX = re.compile(
3308 r"^(?P<sign>[+-])?(?P<hours>\d\d):(?P<minutes>\d\d)(?::(?P<seconds>\d\d))?$"
3309)
3310
3311
3312class vUTCOffset:
3313 """UTC Offset
3314
3315 Value Name:
3316 UTC-OFFSET
3317
3318 Purpose:
3319 This value type is used to identify properties that contain
3320 an offset from UTC to local time.
3321
3322 Format Definition:
3323 This value type is defined by the following notation:
3324
3325 .. code-block:: text
3326
3327 utc-offset = time-numzone
3328
3329 time-numzone = ("+" / "-") time-hour time-minute [time-second]
3330
3331 Description:
3332 The PLUS SIGN character MUST be specified for positive
3333 UTC offsets (i.e., ahead of UTC). The HYPHEN-MINUS character MUST
3334 be specified for negative UTC offsets (i.e., behind of UTC). The
3335 value of "-0000" and "-000000" are not allowed. The time-second,
3336 if present, MUST NOT be 60; if absent, it defaults to zero.
3337
3338 Example:
3339 The following UTC offsets are given for standard time for
3340 New York (five hours behind UTC) and Geneva (one hour ahead of
3341 UTC):
3342
3343 .. code-block:: text
3344
3345 -0500
3346
3347 +0100
3348
3349 .. code-block:: pycon
3350
3351 >>> from icalendar.prop import vUTCOffset
3352 >>> utc_offset = vUTCOffset.from_ical('-0500')
3353 >>> utc_offset
3354 datetime.timedelta(days=-1, seconds=68400)
3355 >>> utc_offset = vUTCOffset.from_ical('+0100')
3356 >>> utc_offset
3357 datetime.timedelta(seconds=3600)
3358 """
3359
3360 default_value: ClassVar[str] = "UTC-OFFSET"
3361 params: Parameters
3362
3363 ignore_exceptions = False # if True, and we cannot parse this
3364
3365 # component, we will silently ignore
3366 # it, rather than let the exception
3367 # propagate upwards
3368
3369 def __init__(self, td: timedelta, /, params: dict[str, Any] | None = None):
3370 if not isinstance(td, timedelta):
3371 raise TypeError("Offset value MUST be a timedelta instance")
3372 self.td = td
3373 self.params = Parameters(params)
3374
3375 def to_ical(self) -> str:
3376 """Return the ical representation."""
3377 return self.format("")
3378
3379 def format(self, divider: str = "") -> str:
3380 """Represent the value with a possible divider.
3381
3382 .. code-block:: pycon
3383
3384 >>> from icalendar import vUTCOffset
3385 >>> from datetime import timedelta
3386 >>> utc_offset = vUTCOffset(timedelta(hours=-5))
3387 >>> utc_offset.format()
3388 '-0500'
3389 >>> utc_offset.format(divider=':')
3390 '-05:00'
3391 """
3392 if self.td < timedelta(0):
3393 sign = "-%s"
3394 td = timedelta(0) - self.td # get timedelta relative to 0
3395 else:
3396 # Google Calendar rejects '0000' but accepts '+0000'
3397 sign = "+%s"
3398 td = self.td
3399
3400 days, seconds = td.days, td.seconds
3401
3402 hours = abs(days * 24 + seconds // 3600)
3403 minutes = abs((seconds % 3600) // 60)
3404 seconds = abs(seconds % 60)
3405 if seconds:
3406 duration = f"{hours:02}{divider}{minutes:02}{divider}{seconds:02}"
3407 else:
3408 duration = f"{hours:02}{divider}{minutes:02}"
3409 return sign % duration
3410
3411 @classmethod
3412 def from_ical(cls, ical):
3413 if isinstance(ical, cls):
3414 return ical.td
3415 try:
3416 sign, hours, minutes, seconds = (
3417 ical[0:1],
3418 int(ical[1:3]),
3419 int(ical[3:5]),
3420 int(ical[5:7] or 0),
3421 )
3422 offset = timedelta(hours=hours, minutes=minutes, seconds=seconds)
3423 except Exception as e:
3424 raise ValueError(f"Expected UTC offset, got: {ical}") from e
3425 if not cls.ignore_exceptions and offset >= timedelta(hours=24):
3426 raise ValueError(f"Offset must be less than 24 hours, was {ical}")
3427 if sign == "-":
3428 return -offset
3429 return offset
3430
3431 def __eq__(self, other):
3432 if not isinstance(other, vUTCOffset):
3433 return False
3434 return self.td == other.td
3435
3436 def __hash__(self):
3437 return hash(self.td)
3438
3439 def __repr__(self):
3440 return f"vUTCOffset({self.td!r})"
3441
3442 @classmethod
3443 def examples(cls) -> list[vUTCOffset]:
3444 """Examples of vUTCOffset."""
3445 return [
3446 cls(timedelta(hours=3)),
3447 cls(timedelta(0)),
3448 ]
3449
3450 from icalendar.param import VALUE
3451
3452 def to_jcal(self, name: str) -> list:
3453 """The jCal representation of this property according to :rfc:`7265`."""
3454 return [name, self.params.to_jcal(), self.VALUE.lower(), self.format(":")]
3455
3456 @classmethod
3457 def from_jcal(cls, jcal_property: list) -> Self:
3458 """Parse jCal from :rfc:`7265`.
3459
3460 Args:
3461 jcal_property: The jCal property to parse.
3462
3463 Raises:
3464 ~error.JCalParsingError: If the provided jCal is invalid.
3465 """
3466 JCalParsingError.validate_property(jcal_property, cls)
3467 match = UTC_OFFSET_JCAL_REGEX.match(jcal_property[3])
3468 if match is None:
3469 raise JCalParsingError(f"Cannot parse {jcal_property!r} as UTC-OFFSET.")
3470 negative = match.group("sign") == "-"
3471 hours = int(match.group("hours"))
3472 minutes = int(match.group("minutes"))
3473 seconds = int(match.group("seconds") or 0)
3474 t = timedelta(hours=hours, minutes=minutes, seconds=seconds)
3475 if negative:
3476 t = -t
3477 return cls(t, Parameters.from_jcal_property(jcal_property))
3478
3479
3480class vInline(str):
3481 """This is an especially dumb class that just holds raw unparsed text and
3482 has parameters. Conversion of inline values are handled by the Component
3483 class, so no further processing is needed.
3484 """
3485
3486 params: Parameters
3487 __slots__ = ("params",)
3488
3489 def __new__(
3490 cls,
3491 value,
3492 encoding=DEFAULT_ENCODING,
3493 /,
3494 params: dict[str, Any] | None = None,
3495 ):
3496 value = to_unicode(value, encoding=encoding)
3497 self = super().__new__(cls, value)
3498 self.params = Parameters(params)
3499 return self
3500
3501 def to_ical(self):
3502 return self.encode(DEFAULT_ENCODING)
3503
3504 @classmethod
3505 def from_ical(cls, ical):
3506 return cls(ical)
3507
3508
3509class vUnknown(vText):
3510 """This is text but the VALUE parameter is unknown.
3511
3512 Since :rfc:`7265`, it is important to record if values are unknown.
3513 For :rfc:`5545`, we could just assume TEXT.
3514 """
3515
3516 default_value: ClassVar[str] = "UNKNOWN"
3517
3518 @classmethod
3519 def examples(cls) -> list[vUnknown]:
3520 """Examples of vUnknown."""
3521 return [vUnknown("Some property text.")]
3522
3523 from icalendar.param import VALUE
3524
3525
3526class TypesFactory(CaselessDict):
3527 """All Value types defined in RFC 5545 are registered in this factory
3528 class.
3529
3530 The value and parameter names don't overlap. So one factory is enough for
3531 both kinds.
3532 """
3533
3534 _instance: ClassVar[TypesFactory] = None
3535
3536 def instance() -> TypesFactory:
3537 """Return a singleton instance of this class."""
3538 if TypesFactory._instance is None:
3539 TypesFactory._instance = TypesFactory()
3540 return TypesFactory._instance
3541
3542 def __init__(self, *args, **kwargs):
3543 """Set keys to upper for initial dict"""
3544 super().__init__(*args, **kwargs)
3545 self.all_types = (
3546 vBinary,
3547 vBoolean,
3548 vCalAddress,
3549 vDDDLists,
3550 vDDDTypes,
3551 vDate,
3552 vDatetime,
3553 vDuration,
3554 vFloat,
3555 vFrequency,
3556 vGeo,
3557 vInline,
3558 vInt,
3559 vPeriod,
3560 vRecur,
3561 vText,
3562 vTime,
3563 vUTCOffset,
3564 vUri,
3565 vWeekday,
3566 vCategory,
3567 vAdr,
3568 vN,
3569 vOrg,
3570 vUid,
3571 vXmlReference,
3572 vUnknown,
3573 )
3574 self["binary"] = vBinary
3575 self["boolean"] = vBoolean
3576 self["cal-address"] = vCalAddress
3577 self["date"] = vDDDTypes
3578 self["date-time"] = vDDDTypes
3579 self["duration"] = vDDDTypes
3580 self["float"] = vFloat
3581 self["integer"] = vInt
3582 self["period"] = vPeriod
3583 self["recur"] = vRecur
3584 self["text"] = vText
3585 self["time"] = vTime
3586 self["uri"] = vUri
3587 self["utc-offset"] = vUTCOffset
3588 self["geo"] = vGeo
3589 self["inline"] = vInline
3590 self["date-time-list"] = vDDDLists
3591 self["categories"] = vCategory
3592 self["adr"] = vAdr # RFC 6350 vCard
3593 self["n"] = vN # RFC 6350 vCard
3594 self["org"] = vOrg # RFC 6350 vCard
3595 self["unknown"] = vUnknown # RFC 7265
3596 self["uid"] = vUid # RFC 9253
3597 self["xml-reference"] = vXmlReference # RFC 9253
3598
3599 #################################################
3600 # Property types
3601
3602 # These are the default types
3603 types_map = CaselessDict(
3604 {
3605 ####################################
3606 # Property value types
3607 # Calendar Properties
3608 "calscale": "text",
3609 "method": "text",
3610 "prodid": "text",
3611 "version": "text",
3612 # Descriptive Component Properties
3613 "attach": "uri",
3614 "categories": "categories",
3615 "class": "text",
3616 # vCard Properties (RFC 6350)
3617 "adr": "adr",
3618 "n": "n",
3619 "org": "org",
3620 "comment": "text",
3621 "description": "text",
3622 "geo": "geo",
3623 "location": "text",
3624 "percent-complete": "integer",
3625 "priority": "integer",
3626 "resources": "text",
3627 "status": "text",
3628 "summary": "text",
3629 # RFC 9253
3630 # link should be uri, xml-reference or uid
3631 # uri is likely most helpful if people forget to set VALUE
3632 "link": "uri",
3633 "concept": "uri",
3634 "refid": "text",
3635 # Date and Time Component Properties
3636 "completed": "date-time",
3637 "dtend": "date-time",
3638 "due": "date-time",
3639 "dtstart": "date-time",
3640 "duration": "duration",
3641 "freebusy": "period",
3642 "transp": "text",
3643 "refresh-interval": "duration", # RFC 7986
3644 # Time Zone Component Properties
3645 "tzid": "text",
3646 "tzname": "text",
3647 "tzoffsetfrom": "utc-offset",
3648 "tzoffsetto": "utc-offset",
3649 "tzurl": "uri",
3650 # Relationship Component Properties
3651 "attendee": "cal-address",
3652 "contact": "text",
3653 "organizer": "cal-address",
3654 "recurrence-id": "date-time",
3655 "related-to": "text",
3656 "url": "uri",
3657 "conference": "uri", # RFC 7986
3658 "source": "uri",
3659 "uid": "text",
3660 # Recurrence Component Properties
3661 "exdate": "date-time-list",
3662 "exrule": "recur",
3663 "rdate": "date-time-list",
3664 "rrule": "recur",
3665 # Alarm Component Properties
3666 "action": "text",
3667 "repeat": "integer",
3668 "trigger": "duration",
3669 "acknowledged": "date-time",
3670 # Change Management Component Properties
3671 "created": "date-time",
3672 "dtstamp": "date-time",
3673 "last-modified": "date-time",
3674 "sequence": "integer",
3675 # Miscellaneous Component Properties
3676 "request-status": "text",
3677 ####################################
3678 # parameter types (luckily there is no name overlap)
3679 "altrep": "uri",
3680 "cn": "text",
3681 "cutype": "text",
3682 "delegated-from": "cal-address",
3683 "delegated-to": "cal-address",
3684 "dir": "uri",
3685 "encoding": "text",
3686 "fmttype": "text",
3687 "fbtype": "text",
3688 "language": "text",
3689 "member": "cal-address",
3690 "partstat": "text",
3691 "range": "text",
3692 "related": "text",
3693 "reltype": "text",
3694 "role": "text",
3695 "rsvp": "boolean",
3696 "sent-by": "cal-address",
3697 "value": "text",
3698 # rfc 9253 parameters
3699 "label": "text",
3700 "linkrel": "text",
3701 "gap": "duration",
3702 }
3703 )
3704
3705 def for_property(self, name, value_param: str | None = None) -> type:
3706 """Returns the type class for a property or parameter.
3707
3708 Args:
3709 name: Property or parameter name
3710 value_param: Optional ``VALUE`` parameter, for example,
3711 "DATE", "DATE-TIME", or other string.
3712
3713 Returns:
3714 The appropriate value type class.
3715 """
3716 # Special case: RDATE and EXDATE always use vDDDLists to support list values
3717 # regardless of the VALUE parameter
3718 if name.upper() in ("RDATE", "EXDATE"): # and value_param is None:
3719 return self["date-time-list"]
3720
3721 # Only use VALUE parameter for known properties that support multiple value
3722 # types (like DTSTART, DTEND, etc. which can be DATE or DATE-TIME)
3723 # For unknown/custom properties, always use the default type from types_map
3724 if value_param and name in self.types_map and value_param in self:
3725 return self[value_param]
3726 return self[self.types_map.get(name, "unknown")]
3727
3728 def to_ical(self, name, value):
3729 """Encodes a named value from a primitive python type to an icalendar
3730 encoded string.
3731 """
3732 type_class = self.for_property(name)
3733 return type_class(value).to_ical()
3734
3735 def from_ical(self, name, value):
3736 """Decodes a named property or parameter value from an icalendar
3737 encoded string to a primitive python type.
3738 """
3739 type_class = self.for_property(name)
3740 return type_class.from_ical(value)
3741
3742
3743VPROPERTY: TypeAlias = Union[
3744 vAdr,
3745 vBoolean,
3746 vCalAddress,
3747 vCategory,
3748 vDDDLists,
3749 vDDDTypes,
3750 vDate,
3751 vDatetime,
3752 vDuration,
3753 vFloat,
3754 vFrequency,
3755 vInt,
3756 vMonth,
3757 vN,
3758 vOrg,
3759 vPeriod,
3760 vRecur,
3761 vSkip,
3762 vText,
3763 vTime,
3764 vUTCOffset,
3765 vUri,
3766 vWeekday,
3767 vInline,
3768 vBinary,
3769 vGeo,
3770 vUnknown,
3771 vXmlReference,
3772 vUid,
3773]
3774
3775__all__ = [
3776 "DURATION_REGEX",
3777 "VPROPERTY",
3778 "WEEKDAY_RULE",
3779 "TimeBase",
3780 "TypesFactory",
3781 "tzid_from_dt",
3782 "tzid_from_tzinfo",
3783 "vAdr",
3784 "vBinary",
3785 "vBoolean",
3786 "vCalAddress",
3787 "vCategory",
3788 "vDDDLists",
3789 "vDDDTypes",
3790 "vDate",
3791 "vDatetime",
3792 "vDuration",
3793 "vFloat",
3794 "vFrequency",
3795 "vGeo",
3796 "vInline",
3797 "vInt",
3798 "vMonth",
3799 "vN",
3800 "vOrg",
3801 "vPeriod",
3802 "vRecur",
3803 "vSkip",
3804 "vText",
3805 "vTime",
3806 "vUTCOffset",
3807 "vUid",
3808 "vUnknown",
3809 "vUri",
3810 "vWeekday",
3811 "vXmlReference",
3812]