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