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