Source code for icalendar.prop.dt.datetime

"""DATE-TIME property type from :rfc:`5545`."""

from datetime import datetime
from typing import Any, ClassVar
from xml.etree.ElementTree import Element, SubElement

from icalendar.compatibility import Self
from icalendar.error import ICalParsingError, JCalParsingError, XCalParsingError
from icalendar.parser import Parameters
from icalendar.parser.xcal.adapter import ElementAdapter
from icalendar.parser.xcal.base import XCalParser
from icalendar.parser.xcal.match import XCalRegexMatcher
from icalendar.parser.xcal.wrapper import from_xcal_wrapper
from icalendar.parser_tools import to_unicode
from icalendar.timezone import tzp
from icalendar.timezone.tzid import is_utc

from .base import TimeBase

XCAL_DATETIME_REGEX = XCalRegexMatcher(
    r"(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)(Z?)",
    "Expected date-time format YYYY-MM-DDTHH:MM:SS or YYYY-MM-DDTHH:MM:SSZ",
)


[docs] class vDatetime(TimeBase): """Date-Time Value Name: DATE-TIME Purpose: This value type is used to identify values that specify a precise calendar date and time of day. The format is based on the ISO.8601.2004 complete representation. Format Definition: This value type is defined by the following notation: .. code-block:: text date-time = date "T" time date = date-value date-value = date-fullyear date-month date-mday date-fullyear = 4DIGIT date-month = 2DIGIT ;01-12 date-mday = 2DIGIT ;01-28, 01-29, 01-30, 01-31 ;based on month/year time = time-hour time-minute time-second [time-utc] time-hour = 2DIGIT ;00-23 time-minute = 2DIGIT ;00-59 time-second = 2DIGIT ;00-60 time-utc = "Z" The following is the representation of the date-time format. .. code-block:: text YYYYMMDDTHHMMSS Description: vDatetime is timezone aware and uses a timezone library. When a vDatetime object is created from an ical string, you can pass a valid timezone identifier. When a vDatetime object is created from a Python :mod:`datetime` object, it uses the tzinfo component, if present. Otherwise a timezone-naive object is created. Be aware that there are certain limitations with timezone naive DATE-TIME components in the icalendar standard. Example: The following represents March 2, 2021 at 10:15 AM with local time: .. code-block:: pycon >>> from icalendar import vDatetime >>> datetime = vDatetime.from_ical("20210302T101500") >>> datetime.tzname() >>> datetime.year 2021 >>> datetime.minute 15 The following represents March 2, 2021 at 10:15 AM in New York: .. code-block:: pycon >>> datetime = vDatetime.from_ical("20210302T101500", 'America/New_York') >>> datetime.tzname() 'EST' The following represents March 2, 2021 at 10:15 AM in Berlin: .. code-block:: pycon >>> from zoneinfo import ZoneInfo >>> timezone = ZoneInfo("Europe/Berlin") >>> vDatetime.from_ical("20210302T101500", timezone) datetime.datetime(2021, 3, 2, 10, 15, tzinfo=ZoneInfo(key='Europe/Berlin')) """ default_value: ClassVar[str] = "DATE-TIME" params: Parameters def __init__(self, dt: datetime, /, params: dict[str, Any] | None = None) -> None: self.dt = dt self.params = Parameters(params) self.params.update_tzid_from(dt)
[docs] def to_ical(self) -> bytes: dt = self.dt s = ( f"{dt.year:04}{dt.month:02}{dt.day:02}" f"T{dt.hour:02}{dt.minute:02}{dt.second:02}" ) if self.is_utc(): s += "Z" return s.encode("utf-8")
[docs] @staticmethod def from_ical(ical, timezone=None): """Create a datetime from the RFC string. Parameters: ical (str | bytes): Required. The iCalendar DATE-TIME value to parse. timezone (str | datetime.tzinfo | None): The time zone name or object used to localize the result. If ``None``, a trailing ``Z`` selects UTC and other values remain floating. Raises: ~icalendar.error.ICalParsingError: If the value is not a valid DATE-TIME. """ ical = to_unicode(ical) tzinfo = None if isinstance(timezone, str): tzinfo = tzp.timezone(timezone) elif timezone is not None: tzinfo = timezone # Extract the value part if parameters are present per # https://datatracker.ietf.org/doc/html/rfc5545.html#section-3.3.5 # Form #3: TZID=America/New_York:19980119T020000 ical_value = ical.rpartition(":")[2] if len(ical_value) < 15 or ical_value[8] != "T": raise ICalParsingError( "Wrong datetime format", value=ical, ) try: timetuple = ( int(ical_value[:4]), # year int(ical_value[4:6]), # month int(ical_value[6:8]), # day int(ical_value[9:11]), # hour int(ical_value[11:13]), # minute int(ical_value[13:15]), # second ) if tzinfo: return tzp.localize(datetime(*timetuple), tzinfo) if not ical_value[15:]: return datetime(*timetuple) if ical_value[15:] == "Z": return tzp.localize_utc(datetime(*timetuple)) except Exception as e: raise ICalParsingError( "Wrong datetime format", value=ical, ) from e raise ICalParsingError( "Wrong datetime format", value=ical, )
[docs] @classmethod def examples(cls) -> list[Self]: """Examples of vDatetime.""" return [ cls(tzp.localize_utc(datetime(2025, 11, 10, 16, 52))), cls(tzp.localize(datetime(2025, 11, 10, 16, 52), "America/New_York")), cls(datetime(2025, 11, 10, 16, 52)), ]
from icalendar.param import VALUE
[docs] def to_jcal(self, name: str) -> list: """The jCal representation of this property according to :rfc:`7265`.""" value = self.dt.strftime("%Y-%m-%dT%H:%M:%S") if self.is_utc(): value += "Z" return [name, self.params.to_jcal(exclude_utc=True), self.VALUE.lower(), value]
[docs] def is_utc(self) -> bool: """Whether this datetime is UTC.""" return self.params.is_utc() or is_utc(self.dt)
[docs] @classmethod def parse_jcal_value(cls, jcal: str) -> datetime: """Parse a jCal string to a :class:`datetime.datetime`. Raises: ~icalendar.error.JCalParsingError: If it can't parse a date-time value. """ JCalParsingError.validate_value_type(jcal, str, cls) utc = jcal.endswith("Z") if utc: jcal = jcal[:-1] try: dt = datetime.strptime(jcal, "%Y-%m-%dT%H:%M:%S") except ValueError as e: raise JCalParsingError("Cannot parse date-time.", cls, value=jcal) from e if utc: return tzp.localize_utc(dt) return dt
[docs] @classmethod def from_jcal(cls, jcal_property: list) -> Self: """Parse jCal from :rfc:`7265`. Parameters: jcal_property: The jCal property to parse. Raises: ~icalendar.error.JCalParsingError: If the provided jCal is invalid. """ JCalParsingError.validate_property(jcal_property, cls) params = Parameters.from_jcal_property(jcal_property) with JCalParsingError.reraise_with_path_added(3): dt = cls.parse_jcal_value(jcal_property[3]) if params.tzid: dt = tzp.localize(dt, params.tzid) return cls( dt, params=params, )
[docs] def to_xcal(self, element: Element) -> None: """The xCal representation of this property according to :rfc:`6321`. Parameters: element: The element to add the property to. """ self.params.to_xcal(element) self.to_xcal_value_only(element, self.default_value)
[docs] def to_xcal_value_only(self, element: Element, tag: str): """The xCal representation of this property according to :rfc:`6321`. In contrast to :meth:`to_xcal`, this method does not add the parameters. Parameters: element: The element to add the property to. tag: The tag to use. """ element = SubElement(element, tag.lower()) text = self.dt.strftime("%Y-%m-%dT%H:%M:%S") if is_utc(self.dt): text += "Z" element.text = text
[docs] @classmethod def from_xcal_element(cls, element: ElementAdapter) -> datetime: """Parse xCal from :rfc:`6321`. Parameters: element: The xCal element to parse. Returns: The datetime object, possibly with a UTC timezone. Raises: ~error.XCalParsingError: If the provided xCal is invalid. """ year, month, day, hour, minute, second, utc = XCAL_DATETIME_REGEX.groups( element ) if second == "60": # This is wrong but we have no way to represent this second = 59 try: dt = datetime( int(year), int(month), int(day), int(hour), int(minute), int(second) ) except ValueError as e: raise XCalParsingError( "Date-time is out of range", element.get_xsd_token(), element ) from e if utc: dt = tzp.localize_utc(dt) return dt
[docs] @classmethod @from_xcal_wrapper def from_xcal(cls, parser: XCalParser, params: Parameters) -> Self: """Parse xCal from :rfc:`6321`. Parameters: xml: The XML to parse or a parser. Raises: ~error.XCalParsingError: If the provided xCal is invalid. """ element = parser.parse_tag(cls.default_value) dt = cls.from_xcal_element(element) tzid = params.tzid if tzid: if is_utc(dt): raise XCalParsingError(f"Cannot mix {tzid} with UTC", None, element) dt = tzp.localize(dt, tzid) return cls(dt, params=params)
__all__ = ["vDatetime"]