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