Skip to content

vobjectx

Main package documentation for VObjectX.

Module: vobjectx

vobjectx

VObjectx Overview

vobjectx parses vCard or vCalendar files, returning a tree of Python objects.
It also provids an API to create vCard or vCalendar data structures which
can then be serialized.

Parsing existing streams
------------------------
Streams containing one or many L{Component<base.Component>}s can be
parsed using L{read_components<base.read_components>}.  As each Component
is parsed, vobjectx will attempt to give it a L{Behavior<behavior.Behavior>}.
If an appropriate Behavior is found, any base64, quoted-printable, or
backslash escaped data will automatically be decoded.  Dates and datetimes
will be transformed to datetime.date or datetime.datetime instances.
Components containing recurrence information will have a special rruleset
attribute (a dateutil.rrule.rruleset instance).

Validation
----------
L{Behavior<behavior.Behavior>} classes implement validation for
L{Component<base.Component>}s.  To validate, an object must have all
required children.  There (TODO: will be) a toggle to raise an exception or
just log unrecognized, non-experimental children and parameters.

Creating objects programatically
--------------------------------
A L{Component<base.Component>} can be created from scratch.  No encoding
is necessary, serialization will encode data automatically.  Factory
functions (TODO: will be) available to create standard objects.

Serializing objects
-------------------
Serialization:
  - Looks for missing required children that can be automatically generated,
    like a UID or a PRODID, and adds them
  - Encodes all values that can be automatically encoded
  - Checks to make sure the object is valid (unless this behavior is
    explicitly disabled)
  - Appends the serialized object to a buffer, or fills a new
    buffer and returns it

Examples
--------

>>> import datetime
>>> import dateutil.rrule as rrule
>>> x = iCalendar()
>>> x.add('vevent')
<VEVENT| []>
>>> x
<VCALENDAR| [<VEVENT| []>]>
>>> v = x.vevent
>>> utc = icalendar.utc
>>> v.add('dtstart').value = datetime.datetime(2004, 12, 15, 14, tzinfo = utc)
>>> v
<VEVENT| [<DTSTART{}2004-12-15 14:00:00+00:00>]>
>>> x
<VCALENDAR| [<VEVENT| [<DTSTART{}2004-12-15 14:00:00+00:00>]>]>
>>> newrule = rrule.rruleset()
>>> newrule.rrule(rrule.rrule(rrule.WEEKLY, count=2, dtstart=v.dtstart.value))
>>> v.rruleset = newrule
>>> list(v.rruleset)
[datetime.datetime(2004, 12, 15, 14, 0, tzinfo=tzutc()), datetime.datetime(2004, 12, 22, 14, 0, tzinfo=tzutc())]
>>> v.add('uid').value = "randomuid@MYHOSTNAME"
>>> print(x.serialize())
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//VOBJECTX//NONSGML Version 1//EN
BEGIN:VEVENT
UID:randomuid@MYHOSTNAME
DTSTART:20041215T140000Z
RRULE:FREQ=WEEKLY;COUNT=2
END:VEVENT
END:VCALENDAR

Functions

read_components

read_components(stream_or_string, validate=False, transform=True, ignore_unreadable=False, allow_qp=False)

Generate one Component at a time from a stream.

Source code in vobjectx/base.py
def read_components(
    stream_or_string, validate=False, transform=True, ignore_unreadable=False, allow_qp=False
) -> Iterator[Component]:
    """Generate one Component at a time from a stream."""

    def raise_parse_error(msg):
        raise ParseError(msg, n, inputs=stream_or_string)

    def _handle_end():
        if not stack:
            raise raise_parse_error(f"Attempted to end the {vline.value} component but it was never opened")
        if vline.value.upper() != stack.top_name():
            raise raise_parse_error(f"{stack.top_name()} component wasn't closed")

        # START matches END
        if len(stack) == 1:
            component: Component = stack.pop()
            component.set_behavior_from_version_line(version_line)

            if validate:
                component.validate(raise_exception=True)
            if transform:
                component.transform_children_to_native()
            return component  # EXIT POINT

        stack.modify_top(stack.pop())
        return None

    stream = get_buffer(stream_or_string)
    stack = ComponentStack()
    n, version_line = 0, None

    for line, n in get_logical_lines(stream, allow_qp):
        # 1. Get vline
        try:
            vline = text_line_to_content_line(line, n)
        except VObjectError as e:
            if ignore_unreadable:
                logger.error(f"Skipped line: {e.line_number or '?'}, message: {str(e)}")
                continue
            raise e

        # 2. Parse vline
        if vline.name == "VERSION":
            version_line = vline
            stack.modify_top(vline)
        elif vline.name == "BEGIN":
            stack.push(Component(vline.value, group=vline.group))
        elif vline.name == "PROFILE":
            if not stack.top():
                stack.push(Component())
            stack.top().set_profile(vline.value)
        elif vline.name == "END":
            _component = _handle_end()
            if _component:
                yield _component
        else:
            stack.modify_top(vline)  # not a START or END line

    if stack.top():
        if stack.top_name() is None:
            logger.warning("Top level component was never named")
        elif stack.top().use_begin:
            raise raise_parse_error(f"Component {(stack.top_name())!s} was never closed")
        yield stack.pop()

read_one

read_one(stream, validate=False, transform=True, ignore_unreadable=False, allow_qp=False)

Return the first component from stream.

Source code in vobjectx/base.py
def read_one(stream, validate=False, transform=True, ignore_unreadable=False, allow_qp=False):
    """
    Return the first component from stream.
    """
    return next(read_components(stream, validate, transform, ignore_unreadable, allow_qp))

new_from_behavior

new_from_behavior(name, id_=None)

Given a name, return a behaviored ContentLine or Component.

Source code in vobjectx/behavior.py
def new_from_behavior(name, id_=None):
    """
    Given a name, return a behaviored ContentLine or Component.
    """
    name = name.upper()
    behavior = BehaviorRegistry.get(name, id_)
    if behavior is None:
        raise VObjectError(f"No behavior found named {name!s}")
    obj = Component(name) if behavior.is_component else ContentLine(name, [], "")
    obj.behavior = behavior
    obj.is_native = False
    return obj

Modules

base

vobjectx module for reading vCard and vCalendar files.

Classes
VBase

Base class for ContentLine and Component.

@ivar behavior: The Behavior class associated with this object, which controls validation, transformations, and encoding. @ivar parent_behavior: The object's parent's behavior, or None if no behaviored parent exists. @ivar is_native: Boolean describing whether this component is a Native instance. @ivar group: An optional group prefix, should be used only to indicate sort order in vCards, according to spec.

Current spec: 4.0 (http://tools.ietf.org/html/rfc6350)

Source code in vobjectx/base.py
class VBase:
    """
    Base class for ContentLine and Component.

    @ivar behavior:
        The Behavior class associated with this object, which controls
        validation, transformations, and encoding.
    @ivar parent_behavior:
        The object's parent's behavior, or None if no behaviored parent exists.
    @ivar is_native:
        Boolean describing whether this component is a Native instance.
    @ivar group:
        An optional group prefix, should be used only to indicate sort order in
        vCards, according to spec.

    Current spec: 4.0 (http://tools.ietf.org/html/rfc6350)
    """

    def __init__(self, group=None, *args, **kwds):
        super().__init__(*args, **kwds)
        self.name = None
        self.group = group
        self.behavior = None
        self.parent_behavior = None
        self.is_native = False
        self.is_encoded = False

    def copy(self) -> Self:
        newcopy = type(self)()
        newcopy.upgrade_from(self)
        return newcopy  # type: ignore

    def upgrade_from(self, copyit: Self):
        self.group = copyit.group
        self.behavior = copyit.behavior
        self.parent_behavior = copyit.parent_behavior
        self.is_native = copyit.is_native

    def validate(self, *args, **kwds):
        """
        Call the behavior's validate method, or return True.
        """
        return self.behavior.validate(self, *args, **kwds) if self.behavior else True

    def get_children(self):
        """
        Return an iterable containing the contents of the object.
        """
        return []

    def clear_behavior(self, cascade=True):
        """
        Set behavior to None. Do for all descendants if cascading.
        """
        self.behavior = None
        if cascade:
            self.transform_children_from_native()

    def auto_behavior(self, cascade=False):
        """
        Set behavior if name is in self.parent_behavior.known_children.

        If cascade is True, unset behavior and parent_behavior for all
        descendants, then recalculate behavior and parent_behavior.
        """
        parent_behavior = self.parent_behavior
        if parent_behavior is not None:
            known_child_tup = parent_behavior.known_children.get(self.name)
            if known_child_tup is not None:
                behavior = BehaviorRegistry.get(self.name, known_child_tup[2])
                if behavior is not None:
                    self.set_behavior(behavior, cascade)
                    if isinstance(self, ContentLine) and self.is_encoded:
                        self.behavior.decode(self)
            elif isinstance(self, ContentLine):
                self.behavior = parent_behavior.default_behavior
                if self.is_encoded and self.behavior:
                    self.behavior.decode(self)

    def set_behavior(self, behavior, cascade=True):
        """
        Set behavior. If cascade is True, auto_behavior all descendants.
        """
        self.behavior = behavior
        if cascade:
            for obj in self.get_children():
                obj.parent_behavior = behavior
                obj.auto_behavior(True)

    def transform_to_native(self):
        """
        Transform this object into a custom VBase subclass.

        transform_to_native should always return a representation of this object.
        It may do so by modifying self in place then returning self, or by
        creating a new object.
        """
        if self.is_native or not self.behavior or not self.behavior.has_native:
            return self

        self_orig = self.copy()
        try:
            return self.behavior.transform_to_native(self)
        except ParseError as e:
            e.line_number = getattr(self, "line_number", None)
            raise
        except VObjectError as e:
            e.line_number = getattr(self, "line_number", None)

            # wrap errors in transformation in a ParseError
            msg = "In transform_to_native, unhandled exception on line {0}: {1}: {2}"
            msg = msg.format(e.line_number, sys.exc_info()[0], sys.exc_info()[1])
            msg = f"{msg} ({str(self_orig)})"
            raise ParseError(msg, e.line_number) from e

    def transform_from_native(self):
        """
        Return self transformed into a ContentLine or Component if needed.

        May have side effects.  If it does, transform_from_native and
        transform_to_native MUST have perfectly inverse side effects. Allowing
        such side effects is convenient for objects whose transformations only
        change a few attributes.

        Note that it isn't always possible for transform_from_native to be a
        perfect inverse of transform_to_native, in such cases transform_from_native
        should return a new object, not self after modifications.
        """
        if not self.is_native or not self.behavior or not self.behavior.has_native:
            return self

        try:
            return self.behavior.transform_from_native(self)
        except VObjectError as e:
            # wrap errors in transformation in a NativeError
            line_number = getattr(self, "line_number", None)
            if isinstance(e, NativeError):
                e.line_number = line_number
                raise

            msg = "In transform_from_native, unhandled exception on line {0} {1}: {2}"
            msg = msg.format(line_number, sys.exc_info()[0], sys.exc_info()[1])
            raise NativeError(msg, line_number) from e

    def transform_children_to_native(self):
        """
        Recursively replace children with their native representation.
        """

    def transform_children_from_native(self, clear_behavior=True):
        """
        Recursively transform native children to vanilla representations.
        """

    def serialize(self, buf=None, line_length=75, validate=True, behavior=None, *args, **kwargs):
        """
        Serialize to buf if it exists, otherwise return a string.

        Use self.behavior.serialize if behavior exists.
        """
        if not behavior:
            behavior = self.behavior
        if behavior:
            logger.debug(f"serializing {self.name!s} with behavior {behavior!s}")
            return behavior.serialize(self, buf, line_length, validate, *args, **kwargs)

        logger.debug(f"serializing {self.name!s} without behavior")
        return default_serialize(self, buf, line_length)
Functions
validate
validate(*args, **kwds)

Call the behavior's validate method, or return True.

Source code in vobjectx/base.py
def validate(self, *args, **kwds):
    """
    Call the behavior's validate method, or return True.
    """
    return self.behavior.validate(self, *args, **kwds) if self.behavior else True
get_children
get_children()

Return an iterable containing the contents of the object.

Source code in vobjectx/base.py
def get_children(self):
    """
    Return an iterable containing the contents of the object.
    """
    return []
clear_behavior
clear_behavior(cascade=True)

Set behavior to None. Do for all descendants if cascading.

Source code in vobjectx/base.py
def clear_behavior(self, cascade=True):
    """
    Set behavior to None. Do for all descendants if cascading.
    """
    self.behavior = None
    if cascade:
        self.transform_children_from_native()
auto_behavior
auto_behavior(cascade=False)

Set behavior if name is in self.parent_behavior.known_children.

If cascade is True, unset behavior and parent_behavior for all descendants, then recalculate behavior and parent_behavior.

Source code in vobjectx/base.py
def auto_behavior(self, cascade=False):
    """
    Set behavior if name is in self.parent_behavior.known_children.

    If cascade is True, unset behavior and parent_behavior for all
    descendants, then recalculate behavior and parent_behavior.
    """
    parent_behavior = self.parent_behavior
    if parent_behavior is not None:
        known_child_tup = parent_behavior.known_children.get(self.name)
        if known_child_tup is not None:
            behavior = BehaviorRegistry.get(self.name, known_child_tup[2])
            if behavior is not None:
                self.set_behavior(behavior, cascade)
                if isinstance(self, ContentLine) and self.is_encoded:
                    self.behavior.decode(self)
        elif isinstance(self, ContentLine):
            self.behavior = parent_behavior.default_behavior
            if self.is_encoded and self.behavior:
                self.behavior.decode(self)
set_behavior
set_behavior(behavior, cascade=True)

Set behavior. If cascade is True, auto_behavior all descendants.

Source code in vobjectx/base.py
def set_behavior(self, behavior, cascade=True):
    """
    Set behavior. If cascade is True, auto_behavior all descendants.
    """
    self.behavior = behavior
    if cascade:
        for obj in self.get_children():
            obj.parent_behavior = behavior
            obj.auto_behavior(True)
transform_to_native
transform_to_native()

Transform this object into a custom VBase subclass.

transform_to_native should always return a representation of this object. It may do so by modifying self in place then returning self, or by creating a new object.

Source code in vobjectx/base.py
def transform_to_native(self):
    """
    Transform this object into a custom VBase subclass.

    transform_to_native should always return a representation of this object.
    It may do so by modifying self in place then returning self, or by
    creating a new object.
    """
    if self.is_native or not self.behavior or not self.behavior.has_native:
        return self

    self_orig = self.copy()
    try:
        return self.behavior.transform_to_native(self)
    except ParseError as e:
        e.line_number = getattr(self, "line_number", None)
        raise
    except VObjectError as e:
        e.line_number = getattr(self, "line_number", None)

        # wrap errors in transformation in a ParseError
        msg = "In transform_to_native, unhandled exception on line {0}: {1}: {2}"
        msg = msg.format(e.line_number, sys.exc_info()[0], sys.exc_info()[1])
        msg = f"{msg} ({str(self_orig)})"
        raise ParseError(msg, e.line_number) from e
transform_from_native
transform_from_native()

Return self transformed into a ContentLine or Component if needed.

May have side effects. If it does, transform_from_native and transform_to_native MUST have perfectly inverse side effects. Allowing such side effects is convenient for objects whose transformations only change a few attributes.

Note that it isn't always possible for transform_from_native to be a perfect inverse of transform_to_native, in such cases transform_from_native should return a new object, not self after modifications.

Source code in vobjectx/base.py
def transform_from_native(self):
    """
    Return self transformed into a ContentLine or Component if needed.

    May have side effects.  If it does, transform_from_native and
    transform_to_native MUST have perfectly inverse side effects. Allowing
    such side effects is convenient for objects whose transformations only
    change a few attributes.

    Note that it isn't always possible for transform_from_native to be a
    perfect inverse of transform_to_native, in such cases transform_from_native
    should return a new object, not self after modifications.
    """
    if not self.is_native or not self.behavior or not self.behavior.has_native:
        return self

    try:
        return self.behavior.transform_from_native(self)
    except VObjectError as e:
        # wrap errors in transformation in a NativeError
        line_number = getattr(self, "line_number", None)
        if isinstance(e, NativeError):
            e.line_number = line_number
            raise

        msg = "In transform_from_native, unhandled exception on line {0} {1}: {2}"
        msg = msg.format(line_number, sys.exc_info()[0], sys.exc_info()[1])
        raise NativeError(msg, line_number) from e
transform_children_to_native
transform_children_to_native()

Recursively replace children with their native representation.

Source code in vobjectx/base.py
def transform_children_to_native(self):
    """
    Recursively replace children with their native representation.
    """
transform_children_from_native
transform_children_from_native(clear_behavior=True)

Recursively transform native children to vanilla representations.

Source code in vobjectx/base.py
def transform_children_from_native(self, clear_behavior=True):
    """
    Recursively transform native children to vanilla representations.
    """
serialize
serialize(buf=None, line_length=75, validate=True, behavior=None, *args, **kwargs)

Serialize to buf if it exists, otherwise return a string.

Use self.behavior.serialize if behavior exists.

Source code in vobjectx/base.py
def serialize(self, buf=None, line_length=75, validate=True, behavior=None, *args, **kwargs):
    """
    Serialize to buf if it exists, otherwise return a string.

    Use self.behavior.serialize if behavior exists.
    """
    if not behavior:
        behavior = self.behavior
    if behavior:
        logger.debug(f"serializing {self.name!s} with behavior {behavior!s}")
        return behavior.serialize(self, buf, line_length, validate, *args, **kwargs)

    logger.debug(f"serializing {self.name!s} without behavior")
    return default_serialize(self, buf, line_length)
ContentLine

Bases: VBase

Holds one content line for formats like vCard and vCalendar.

For example::

@ivar name: The uppercased name of the contentline. @ivar params: A dictionary of parameters and associated lists of values. Singleton params (e.g., WORK, CELL in vCard 2.1) are stored with an empty list as the value. @ivar value: The value of the contentline. @ivar encoded: A boolean describing whether the data in the content line is encoded. Generally, text read from a serialized vCard or vCalendar should be considered encoded. Data added programmatically should not be encoded. @ivar line_number: An optional line number associated with the contentline.

Source code in vobjectx/base.py
class ContentLine(VBase):
    """
    Holds one content line for formats like vCard and vCalendar.

    For example::
      <SUMMARY{u'param1' : [u'val1'], u'param2' : [u'val2']}Bastille Day Party>

    @ivar name:
        The uppercased name of the contentline.
    @ivar params:
        A dictionary of parameters and associated lists of values.
        Singleton params (e.g., WORK, CELL in vCard 2.1) are stored with an
        empty list as the value.
    @ivar value:
        The value of the contentline.
    @ivar encoded:
        A boolean describing whether the data in the content line is encoded.
        Generally, text read from a serialized vCard or vCalendar should be
        considered encoded.  Data added programmatically should not be encoded.
    @ivar line_number:
        An optional line number associated with the contentline.
    """

    # pylint: disable=r0902,r0917
    def __init__(
        self,
        name: str,
        params: list,
        value: str,
        group=None,
        is_encoded: bool = False,
        is_native: bool = False,
        line_number: int = None,
        *args,
        **kwds,
    ):
        """
        Take output from parse_line, convert params list to dictionary.

        Group is used as a positional argument to match parse_line's return
        """
        super().__init__(group, *args, **kwds)

        self.name = name.upper()
        self.is_encoded = is_encoded
        self.params = ContentDict()
        self.is_native = is_native
        self.line_number = line_number
        self.value: Any = value  # depends on Behavior

        def update_table(x):
            # All params stored uniformly: singleton params get empty list
            paramlist = self.params.setdefault(x[0], [])
            if len(x) > 1:
                paramlist.extend(x[1:])

        list(map(update_table, params))

        qp = False
        if "ENCODING" in self.params and "QUOTED-PRINTABLE" in self.params["ENCODING"]:
            qp = True
            self.params["ENCODING"].remove("QUOTED-PRINTABLE")
            if not self.params["ENCODING"]:
                del self.params["ENCODING"]
        if "QUOTED-PRINTABLE" in self.params:
            qp = True
            del self.params["QUOTED-PRINTABLE"]
        if qp:
            if "ENCODING" in self.params:
                _encoding = self.params["ENCODING"]
            elif "CHARSET" in self.params:
                _encoding = self.params["CHARSET"][0]
            else:
                _encoding = "utf-8"

            _value = byte_decoder(self.value, "quoted-printable")
            try:
                self.value = _value.decode(_encoding)
            except UnicodeDecodeError:
                self.value = _value.decode("latin-1")

    def copy(self) -> Self:
        newcopy = ContentLine("", [], "")
        newcopy.update_from(self)
        return newcopy  # type: ignore

    def update_from(self, copyit: Self):
        super().upgrade_from(copyit)
        self.name = copyit.name
        self.value = copy.copy(copyit.value)
        self.is_encoded = copyit.is_encoded

        for k, v in copyit.params.items():
            self.params[k] = copy.copy(v)
        self.line_number = copyit.line_number

    def __eq__(self, other):
        return (self.name == other.name) and (self.params == other.params) and (self.value == other.value)

    def __getattr__(self, name):
        """
        Make params accessible via self.foo_param or self.foo_paramlist.

        Underscores, legal in python variable names, are converted to dashes,
        which are legal in IANA tokens.
        """
        try:
            if name.endswith("_param"):
                return self.params[name][0]
            if name.endswith("_paramlist"):
                return self.params[name]
            raise AttributeError(name)
        except KeyError as e:
            raise AttributeError(name) from e

    def __setattr__(self, name, value):
        """
        Make params accessible via self.foo_param or self.foo_paramlist.

        Underscores, legal in python variable names, are converted to dashes,
        which are legal in IANA tokens.
        """
        if name.endswith("_param"):
            if isinstance(value, list):
                self.params[name] = value
            else:
                self.params[name] = [value]
        elif name.endswith("_paramlist"):
            if isinstance(value, list):
                self.params[name] = value
            else:
                raise VObjectError("Parameter list set to a non-list")
        else:
            prop = getattr(self.__class__, name, None)
            if isinstance(prop, property):
                prop.fset(self, value)
            else:
                object.__setattr__(self, name, value)

    def __delattr__(self, name):
        try:
            if name.endswith("_param") or name.endswith("_paramlist"):
                del self.params[name]
            else:
                object.__delattr__(self, name)
        except KeyError as e:
            raise AttributeError(name) from e

    def value_repr(self):
        """Transform the representation of the value according to the behavior, if any."""
        return self.behavior.value_repr(self) if self.behavior else self.value

    @property
    def display_params(self):
        return {k: v for k, v in self.params.items() if v}

    def __repr__(self):
        try:
            value_repr = self.value_repr()
        except UnicodeEncodeError:
            value_repr = self.value_repr().encode("utf-8")

        # Filter out singleton params (empty lists) for display
        return f"<{self.name}{self.display_params}{value_repr}>"

    def __unicode__(self):
        # Filter out singleton params (empty lists) for display
        return f"<{self.name}{self.display_params}{self.value_repr()}>"

    def pretty_print(self, level=0, tabwidth=3):
        pre = " " * level * tabwidth
        print(pre, f"{self.name}:", self.value_repr())
        if self.params:
            print(pre, "params for ", f"{self.name}:")
            for k, v in self.params.items():
                print(pre + " " * tabwidth, k, v)

    def default_serialize(self, outbuf, line_length):
        started_encoded = self.is_encoded
        if self.behavior and not started_encoded:
            self.behavior.encode(self)

        s = get_buffer()

        if self.group is not None:
            s.write(f"{self.group}.")
        s.write(self.name.upper())
        keys = sorted(self.params.keys())
        for key in keys:
            paramstr = ",".join(dquote_escape(p) for p in self.params[key])
            try:
                if paramstr:
                    s.write(f";{key}={paramstr}")
                else:
                    s.write(f";{key}")
            except (UnicodeDecodeError, UnicodeEncodeError):
                s.write(f";{key}={paramstr.encode('utf-8')}")
        try:
            s.write(f":{self.value}")
        except (UnicodeDecodeError, UnicodeEncodeError):
            s.write(f":{self.value.encode('utf-8')}")
        if self.behavior and not started_encoded:
            self.behavior.decode(self)
        fold_one_line(outbuf, s.getvalue(), line_length)

    # pylint: disable=w0613
    @classmethod
    def line_validate(cls, line, raise_exception=True, complain_unrecognized=False):
        """Examine a line's parameters and values, return True if valid."""
        return True
Functions
value_repr
value_repr()

Transform the representation of the value according to the behavior, if any.

Source code in vobjectx/base.py
def value_repr(self):
    """Transform the representation of the value according to the behavior, if any."""
    return self.behavior.value_repr(self) if self.behavior else self.value
line_validate classmethod
line_validate(line, raise_exception=True, complain_unrecognized=False)

Examine a line's parameters and values, return True if valid.

Source code in vobjectx/base.py
@classmethod
def line_validate(cls, line, raise_exception=True, complain_unrecognized=False):
    """Examine a line's parameters and values, return True if valid."""
    return True
Component

Bases: VBase

A complex property that can contain multiple ContentLines.

For our purposes, a component must start with a BEGIN:xxxx line and end with END:xxxx, or have a PROFILE:xxx line if a top-level component.

@ivar contents: A dictionary of lists of Component or ContentLine instances. The keys are the lowercased names of child ContentLines or Components. Note that BEGIN and END ContentLines are not included in contents. @ivar name: Uppercase string used to represent this Component, i.e VCARD if the serialized object starts with BEGIN:VCARD. @ivar use_begin: A boolean flag determining whether BEGIN: and END: lines should be serialized.

Source code in vobjectx/base.py
class Component(VBase):
    """
    A complex property that can contain multiple ContentLines.

    For our purposes, a component must start with a BEGIN:xxxx line and end with
    END:xxxx, or have a PROFILE:xxx line if a top-level component.

    @ivar contents:
        A dictionary of lists of Component or ContentLine instances. The keys
        are the lowercased names of child ContentLines or Components.
        Note that BEGIN and END ContentLines are not included in contents.
    @ivar name:
        Uppercase string used to represent this Component, i.e VCARD if the
        serialized object starts with BEGIN:VCARD.
    @ivar use_begin:
        A boolean flag determining whether BEGIN: and END: lines should
        be serialized.
    """

    def __init__(self, name="", *args, **kwds):
        super().__init__(*args, **kwds)
        self.contents = ContentDict()
        self.name = name.upper()
        self.use_begin = bool(name)
        self.auto_behavior()

    def upgrade_from(self, copyit: Self):
        super().upgrade_from(copyit)

        # deep copy of contents
        self.contents = ContentDict()
        for key, lvalue in copyit.contents.items():
            newvalue = []
            for value in lvalue:
                newitem = value.copy()
                newvalue.append(newitem)
            self.contents[key] = newvalue

        self.name = copyit.name
        self.use_begin = copyit.use_begin

    def set_profile(self, name):
        """
        Assign a PROFILE to this unnamed component.

        Used by vCard, not by vCalendar.
        """
        if self.name or self.use_begin:
            if self.name == name:
                return
            raise VObjectError("This component already has a PROFILE or uses BEGIN.")
        self.name = name.upper()

    def __getattr__(self, name):
        """
        For convenience, make self.contents directly accessible.

        Underscores, legal in python variable names, are converted to dashes,
        which are legal in IANA tokens.
        """
        # if the object is being re-created by pickle, self.contents may not
        # be set, don't get into an infinite loop over the issue
        if name == "contents":
            return object.__getattribute__(self, name)
        try:
            if name.endswith("_list"):
                return self.contents[name]
            return self.contents[name][0]
        except KeyError as e:
            raise AttributeError(name) from e

    def __setattr__(self, name, value):
        """
        For convenience, make self.contents directly accessible.

        Underscores, legal in python variable names, are converted to dashes,
        which are legal in IANA tokens.
        """
        prop = getattr(self.__class__, name, None)
        if isinstance(prop, property):
            prop.fset(self, value)
        else:
            object.__setattr__(self, name, value)

    def get_child_value(self, child_name, default=None, child_number=0):
        """
        Return a child's value (the first, by default), or None.
        """
        child = self.contents.get(child_name)
        return default if child is None else child[child_number].value

    def add(self, obj_or_name, group=None):
        """
        Add obj_or_name to contents, set behavior if it can be inferred.

        If obj_or_name is a string, create an empty component or line based on
        behavior. If no behavior is found for the object, add a ContentLine.

        group is an optional prefix to the name of the object (see RFC 2425).
        """
        if isinstance(obj_or_name, VBase):
            obj = obj_or_name
            if self.behavior:
                obj.parent_behavior = self.behavior
                obj.auto_behavior(True)
        else:
            name = obj_or_name.upper()
            try:
                _id = self.behavior.known_children[name][2]
                behavior = BehaviorRegistry.get(name, _id)
                if behavior.is_component:
                    obj = Component(name)
                else:
                    obj = ContentLine(name, [], "", group)
                obj.parent_behavior = self.behavior
                obj.behavior = behavior
                obj = obj.transform_to_native()
            except (KeyError, AttributeError):
                obj = ContentLine(obj_or_name, [], "", group)
            if obj.behavior is None and self.behavior is not None and isinstance(obj, ContentLine):
                obj.behavior = self.behavior.default_behavior

        self.contents.setdefault(obj.name, []).append(obj)
        return obj

    def remove(self, obj):
        """
        Remove obj from contents.
        """
        named = self.contents.get(obj.name.lower())
        if named:
            with contextlib.suppress(ValueError):
                named.remove(obj)
                if not named:
                    del self.contents[obj.name.lower()]

    def get_children(self):
        """
        Return an iterable of all children.
        """
        for obj_list in self.contents.values():
            yield from obj_list

    def components(self):
        """
        Return an iterable of all Component children.
        """
        return (i for i in self.get_children() if isinstance(i, Component))

    def lines(self):
        """
        Return an iterable of all ContentLine children.
        """
        return (i for i in self.get_children() if isinstance(i, ContentLine))

    def sort_child_keys(self):
        if self.behavior:
            first = [s for s in self.behavior.sort_first if s in self.contents]
        else:
            first = []
        return first + sorted(k for k in self.contents.keys() if k not in first)

    def get_sorted_children(self):
        return [obj for k in self.sort_child_keys() for obj in self.contents[k]]

    def set_behavior_from_version_line(self, version_line):
        """
        Set behavior if one matches name, version_line.value.
        """
        _id = None if version_line is None else version_line.value
        v = BehaviorRegistry.get(self.name, id_=_id)
        if v:
            self.set_behavior(v)

    def transform_children_to_native(self):
        """
        Recursively replace children with their native representation.

        Sort to get dependency order right, like vtimezone before vevent.
        """
        for child_array in (self.contents[k] for k in self.sort_child_keys()):
            for child in child_array:
                child = child.transform_to_native()
                child.transform_children_to_native()

    def transform_children_from_native(self, clear_behavior=True):
        """
        Recursively transform native children to vanilla representations.
        """
        for child_array in self.contents.values():
            for child in child_array:
                child = child.transform_from_native()
                child.transform_children_from_native(clear_behavior)
                if clear_behavior:
                    child.behavior = None
                    child.parent_behavior = None

    def __repr__(self):
        return f"<{self.name or '*unnamed*'}| {self.get_sorted_children()}>"

    def pretty_print(self, level=0, tabwidth=3):
        pre = " " * level * tabwidth
        print(pre, self.name)
        if isinstance(self, Component):
            for line in self.get_children():
                line.pretty_print(level + 1, tabwidth)

    def default_serialize(self, output_buffer, line_length):
        group_string = "" if self.group is None else f"{self.group}."
        if self.use_begin:
            fold_one_line(output_buffer, f"{group_string}BEGIN:{self.name}", line_length)
        for child in self.get_sorted_children():
            # validate is recursive, we only need to validate once
            child.serialize(output_buffer, line_length, validate=False)
        if self.use_begin:
            fold_one_line(output_buffer, f"{group_string}END:{self.name}", line_length)
Functions
set_profile
set_profile(name)

Assign a PROFILE to this unnamed component.

Used by vCard, not by vCalendar.

Source code in vobjectx/base.py
def set_profile(self, name):
    """
    Assign a PROFILE to this unnamed component.

    Used by vCard, not by vCalendar.
    """
    if self.name or self.use_begin:
        if self.name == name:
            return
        raise VObjectError("This component already has a PROFILE or uses BEGIN.")
    self.name = name.upper()
get_child_value
get_child_value(child_name, default=None, child_number=0)

Return a child's value (the first, by default), or None.

Source code in vobjectx/base.py
def get_child_value(self, child_name, default=None, child_number=0):
    """
    Return a child's value (the first, by default), or None.
    """
    child = self.contents.get(child_name)
    return default if child is None else child[child_number].value
add
add(obj_or_name, group=None)

Add obj_or_name to contents, set behavior if it can be inferred.

If obj_or_name is a string, create an empty component or line based on behavior. If no behavior is found for the object, add a ContentLine.

group is an optional prefix to the name of the object (see RFC 2425).

Source code in vobjectx/base.py
def add(self, obj_or_name, group=None):
    """
    Add obj_or_name to contents, set behavior if it can be inferred.

    If obj_or_name is a string, create an empty component or line based on
    behavior. If no behavior is found for the object, add a ContentLine.

    group is an optional prefix to the name of the object (see RFC 2425).
    """
    if isinstance(obj_or_name, VBase):
        obj = obj_or_name
        if self.behavior:
            obj.parent_behavior = self.behavior
            obj.auto_behavior(True)
    else:
        name = obj_or_name.upper()
        try:
            _id = self.behavior.known_children[name][2]
            behavior = BehaviorRegistry.get(name, _id)
            if behavior.is_component:
                obj = Component(name)
            else:
                obj = ContentLine(name, [], "", group)
            obj.parent_behavior = self.behavior
            obj.behavior = behavior
            obj = obj.transform_to_native()
        except (KeyError, AttributeError):
            obj = ContentLine(obj_or_name, [], "", group)
        if obj.behavior is None and self.behavior is not None and isinstance(obj, ContentLine):
            obj.behavior = self.behavior.default_behavior

    self.contents.setdefault(obj.name, []).append(obj)
    return obj
remove
remove(obj)

Remove obj from contents.

Source code in vobjectx/base.py
def remove(self, obj):
    """
    Remove obj from contents.
    """
    named = self.contents.get(obj.name.lower())
    if named:
        with contextlib.suppress(ValueError):
            named.remove(obj)
            if not named:
                del self.contents[obj.name.lower()]
get_children
get_children()

Return an iterable of all children.

Source code in vobjectx/base.py
def get_children(self):
    """
    Return an iterable of all children.
    """
    for obj_list in self.contents.values():
        yield from obj_list
components
components()

Return an iterable of all Component children.

Source code in vobjectx/base.py
def components(self):
    """
    Return an iterable of all Component children.
    """
    return (i for i in self.get_children() if isinstance(i, Component))
lines
lines()

Return an iterable of all ContentLine children.

Source code in vobjectx/base.py
def lines(self):
    """
    Return an iterable of all ContentLine children.
    """
    return (i for i in self.get_children() if isinstance(i, ContentLine))
set_behavior_from_version_line
set_behavior_from_version_line(version_line)

Set behavior if one matches name, version_line.value.

Source code in vobjectx/base.py
def set_behavior_from_version_line(self, version_line):
    """
    Set behavior if one matches name, version_line.value.
    """
    _id = None if version_line is None else version_line.value
    v = BehaviorRegistry.get(self.name, id_=_id)
    if v:
        self.set_behavior(v)
transform_children_to_native
transform_children_to_native()

Recursively replace children with their native representation.

Sort to get dependency order right, like vtimezone before vevent.

Source code in vobjectx/base.py
def transform_children_to_native(self):
    """
    Recursively replace children with their native representation.

    Sort to get dependency order right, like vtimezone before vevent.
    """
    for child_array in (self.contents[k] for k in self.sort_child_keys()):
        for child in child_array:
            child = child.transform_to_native()
            child.transform_children_to_native()
transform_children_from_native
transform_children_from_native(clear_behavior=True)

Recursively transform native children to vanilla representations.

Source code in vobjectx/base.py
def transform_children_from_native(self, clear_behavior=True):
    """
    Recursively transform native children to vanilla representations.
    """
    for child_array in self.contents.values():
        for child in child_array:
            child = child.transform_from_native()
            child.transform_children_from_native(clear_behavior)
            if clear_behavior:
                child.behavior = None
                child.parent_behavior = None
Functions
parse_params
parse_params(string)

Parse parameters

Source code in vobjectx/base.py
def parse_params(string):
    """
    Parse parameters
    """
    _all = params_re.findall(string)
    all_parameters = []
    for param in _all:
        name, values_string = param
        param_list = [name]
        for pair in param_values_re.findall(values_string):
            # pair looks like ('', value) or (value, '')
            param_list.append(pair[0] or pair[1])

        all_parameters.append(param_list)
    return all_parameters
parse_line
parse_line(line, line_number=None)

Parse line

Source code in vobjectx/base.py
def parse_line(line, line_number=None):
    """
    Parse line
    """
    match = line_re.match(line)
    if match is None:
        raise ParseError(f"Failed to parse line: {line!s}", line_number)
    # Underscores are replaced with dash to work around Lotus Notes
    return (
        match.group("name").replace("_", "-"),
        parse_params(match.group("params")),
        match.group("value"),
        match.group("group"),
    )
get_logical_lines
get_logical_lines(fp, allow_qp=True)

Iterate through a stream, yielding one logical line at a time.

Because many applications still use vCard 2.1, we have to deal with the quoted-printable encoding for long lines, as well as the vCard 3.0 and vCalendar line folding technique, a whitespace character at the start of the line.

Quoted-printable data will be decoded in the Behavior decoding phase.

Source code in vobjectx/base.py
def get_logical_lines(fp: TextIO, allow_qp: bool = True) -> Iterator:
    """
    Iterate through a stream, yielding one logical line at a time.

    Because many applications still use vCard 2.1, we have to deal with the
    quoted-printable encoding for long lines, as well as the vCard 3.0 and
    vCalendar line folding technique, a whitespace character at the start
    of the line.

    Quoted-printable data will be decoded in the Behavior decoding phase.
    """

    def get_value(lines: list[str]):
        return "".join(lines)

    if not allow_qp:
        val = fp.read(-1)

        line_number = 1
        for match in logical_lines_re.finditer(val):
            line, n = wrap_re.subn("", match.group())
            if line:
                yield line, line_number
            line_number += n
        return

    quoted_printable = False
    logical_line: list[str] = []
    line_start_number = 0

    for n, line in enumerate(fp, start=1):
        line = line.rstrip(Char.CRLF)

        if line.rstrip() == "":
            if logical_line:
                yield get_value(logical_line), line_start_number
            line_start_number = n
            logical_line = []
            quoted_printable = False
            continue

        if quoted_printable and allow_qp:
            logical_line.append("\n")
            quoted_printable = False
        elif line[0] in Char.SPACEORTAB:
            line = line[1:]
        elif logical_line:
            yield get_value(logical_line), line_start_number
            line_start_number = n
            logical_line = []
        else:
            logical_line = []
        logical_line.append(line)

        # vCard 2.1 allows parameters to be encoded without a parameter name
        # False positives are unlikely, but possible.
        if line[-1] == "=" and "quoted-printable" in get_value(logical_line).lower():
            quoted_printable = True

    if logical_line:
        yield get_value(logical_line), line_start_number
dquote_escape
dquote_escape(param)

Return param, or "param" if ',' or ';' or ':' is in param.

Source code in vobjectx/base.py
def dquote_escape(param: str) -> str:
    """Return param, or "param" if ',' or ';' or ':' is in param."""

    if '"' in param:
        raise VObjectError("Double quotes aren't allowed in parameter values.")
    for char in ",;:":  # sourcery skip # temp
        if char in param:
            return f'"{param}"'
    return param
fold_one_line
fold_one_line(outbuf, input_, line_length=75)

Folding line procedure that ensures multi-byte utf-8 sequences are not broken across lines

Source code in vobjectx/base.py
def fold_one_line(outbuf: TextIO, input_: str, line_length=75):
    """
    Folding line procedure that ensures multi-byte utf-8 sequences are not broken across lines
    """
    chunks = split_by_size(input_, byte_size=line_length)
    for chunk in chunks:
        outbuf.write(chunk)
    outbuf.write(Char.CRLF)
default_serialize
default_serialize(obj, buf, line_length)

Encode and fold obj and its children, write to buf or return a string.

Source code in vobjectx/base.py
def default_serialize(obj, buf, line_length):
    """
    Encode and fold obj and its children, write to buf or return a string.
    """
    outbuf = buf or get_buffer()
    if isinstance(obj, (Component, ContentLine)):
        obj.default_serialize(outbuf, line_length)
    return buf or outbuf.getvalue()
read_components
read_components(stream_or_string, validate=False, transform=True, ignore_unreadable=False, allow_qp=False)

Generate one Component at a time from a stream.

Source code in vobjectx/base.py
def read_components(
    stream_or_string, validate=False, transform=True, ignore_unreadable=False, allow_qp=False
) -> Iterator[Component]:
    """Generate one Component at a time from a stream."""

    def raise_parse_error(msg):
        raise ParseError(msg, n, inputs=stream_or_string)

    def _handle_end():
        if not stack:
            raise raise_parse_error(f"Attempted to end the {vline.value} component but it was never opened")
        if vline.value.upper() != stack.top_name():
            raise raise_parse_error(f"{stack.top_name()} component wasn't closed")

        # START matches END
        if len(stack) == 1:
            component: Component = stack.pop()
            component.set_behavior_from_version_line(version_line)

            if validate:
                component.validate(raise_exception=True)
            if transform:
                component.transform_children_to_native()
            return component  # EXIT POINT

        stack.modify_top(stack.pop())
        return None

    stream = get_buffer(stream_or_string)
    stack = ComponentStack()
    n, version_line = 0, None

    for line, n in get_logical_lines(stream, allow_qp):
        # 1. Get vline
        try:
            vline = text_line_to_content_line(line, n)
        except VObjectError as e:
            if ignore_unreadable:
                logger.error(f"Skipped line: {e.line_number or '?'}, message: {str(e)}")
                continue
            raise e

        # 2. Parse vline
        if vline.name == "VERSION":
            version_line = vline
            stack.modify_top(vline)
        elif vline.name == "BEGIN":
            stack.push(Component(vline.value, group=vline.group))
        elif vline.name == "PROFILE":
            if not stack.top():
                stack.push(Component())
            stack.top().set_profile(vline.value)
        elif vline.name == "END":
            _component = _handle_end()
            if _component:
                yield _component
        else:
            stack.modify_top(vline)  # not a START or END line

    if stack.top():
        if stack.top_name() is None:
            logger.warning("Top level component was never named")
        elif stack.top().use_begin:
            raise raise_parse_error(f"Component {(stack.top_name())!s} was never closed")
        yield stack.pop()
read_one
read_one(stream, validate=False, transform=True, ignore_unreadable=False, allow_qp=False)

Return the first component from stream.

Source code in vobjectx/base.py
def read_one(stream, validate=False, transform=True, ignore_unreadable=False, allow_qp=False):
    """
    Return the first component from stream.
    """
    return next(read_components(stream, validate, transform, ignore_unreadable, allow_qp))

behavior

Classes
Behavior

Behavior (validation, encoding, and transformations) for vobjects.

Abstract class to describe vobjectx options, requirements and encodings.

Behaviors are used for root components like VCALENDAR, for subcomponents like VEVENT, and for individual lines in components.

Behavior subclasses are not meant to be instantiated, all methods should be classmethods.

@cvar name: The uppercase name of the object described by the class, or a generic name if the class defines behavior for many objects. @cvar description: A brief excerpt from the RFC explaining the function of the component or line. @cvar version_string: The string associated with the component, for instance, 2.0 if there's a line like VERSION:2.0, an empty string otherwise. @cvar known_children: A dictionary with uppercased component/property names as keys and a tuple (min, max, id) as value, where id is the id used by L{register_behavior}, min and max are the limits on how many of this child must occur. None is used to denote no max or no id. @cvar quoted_printable: A boolean describing whether the object should be encoded and decoded using quoted printable line folding and character escaping. @cvar default_behavior: Behavior to apply to ContentLine children when no behavior is found. @cvar has_native: A boolean describing whether the object can be transformed into a more Pythonic object. @cvar is_component: A boolean, True if the object should be a Component. @cvar sort_first: The lower-case list of children which should come first when sorting. @cvar allow_group: Whether or not vCard style group prefixes are allowed.

Source code in vobjectx/behavior.py
class Behavior:
    """
    Behavior (validation, encoding, and transformations) for vobjects.

    Abstract class to describe vobjectx options, requirements and encodings.

    Behaviors are used for root components like VCALENDAR, for subcomponents
    like VEVENT, and for individual lines in components.

    Behavior subclasses are not meant to be instantiated, all methods should
    be classmethods.

    @cvar name:
        The uppercase name of the object described by the class, or a generic
        name if the class defines behavior for many objects.
    @cvar description:
        A brief excerpt from the RFC explaining the function of the component or
        line.
    @cvar version_string:
        The string associated with the component, for instance, 2.0 if there's a
        line like VERSION:2.0, an empty string otherwise.
    @cvar known_children:
        A dictionary with uppercased component/property names as keys and a
        tuple (min, max, id) as value, where id is the id used by
        L{register_behavior}, min and max are the limits on how many of this child
        must occur.  None is used to denote no max or no id.
    @cvar quoted_printable:
        A boolean describing whether the object should be encoded and decoded
        using quoted printable line folding and character escaping.
    @cvar default_behavior:
        Behavior to apply to ContentLine children when no behavior is found.
    @cvar has_native:
        A boolean describing whether the object can be transformed into a more
        Pythonic object.
    @cvar is_component:
        A boolean, True if the object should be a Component.
    @cvar sort_first:
        The lower-case list of children which should come first when sorting.
    @cvar allow_group:
        Whether or not vCard style group prefixes are allowed.
    """

    name = ""
    description = ""
    version_string = ""
    known_children = {}
    quoted_printable = False
    default_behavior = None
    has_native = False
    is_component = False
    allow_group = False
    force_utc = False
    sort_first = []

    def __init__(self):
        raise VObjectError("Behavior subclasses are not meant to be instantiated")

    @classmethod
    def validate(cls, obj, raise_exception=False, complain_unrecognized=False):
        """Check if the object satisfies this behavior's requirements.

        @param obj:
            The L{ContentLine<base.ContentLine>} or
            L{Component<base.Component>} to be validated.
        @param raise_exception:
            If True, raise a L{base.ValidateError} on validation failure.
            Otherwise return a boolean.
        @param complain_unrecognized:
            If True, fail to validate if an uncrecognized parameter or child is
            found.  Otherwise log the lack of recognition.

        """
        if not cls.allow_group and obj.group is not None:
            raise VObjectError(f"{obj} has a group, but this object doesn't support groups")

        if isinstance(obj, ContentLine):
            return obj.line_validate(obj, raise_exception, complain_unrecognized)

        if isinstance(obj, Component):
            count = {}
            for child in obj.get_children():
                if not child.validate(raise_exception, complain_unrecognized):
                    return False
                name = child.name.upper()
                count[name] = count.get(name, 0) + 1
            for key, val in cls.known_children.items():
                if count.get(key, 0) < val[0]:
                    if raise_exception:
                        m = "{0} components must contain at least {1} {2}"
                        raise ValidateError(m.format(cls.name, val[0], key))
                    return False
                if val[1] and count.get(key, 0) > val[1]:
                    if raise_exception:
                        m = "{0} components cannot contain more than {1} {2}"
                        raise ValidateError(m.format(cls.name, val[1], key))
                    return False
            return True
        raise VObjectError(f"{obj} is not a Component or Contentline")

    @classmethod
    def decode(cls, line):
        line.is_encoded = False

    @classmethod
    def encode(cls, line):
        line.is_encoded = True

    @staticmethod
    def transform_to_native(obj):
        """
        Turn a ContentLine or Component into a Python-native representation.

        If appropriate, turn dates or datetime strings into Python objects.
        Components containing VTIMEZONEs turn into VtimezoneComponents.

        """
        return obj

    @staticmethod
    def transform_from_native(obj):
        """
        Inverse of transform_to_native.
        """
        raise NativeError("No transform_from_native defined")

    @staticmethod
    def generate_implicit_parameters(obj):
        """Generate any required information that don't yet exist."""

    @classmethod
    def serialize(cls, obj, buf, line_length, validate=True, *args, **kwargs):  # pylint:disable=unused-argument
        """
        Set implicit parameters, do encoding, return unicode string.

        If validate is True, raise VObjectError if the line doesn't validate
        after implicit parameters are generated.

        Default is to call base.default_serialize.

        """

        cls.generate_implicit_parameters(obj)
        if validate:
            cls.validate(obj, raise_exception=True)

        if obj.is_native:
            transformed = obj.transform_from_native()
            undo_transform = True
        else:
            transformed = obj
            undo_transform = False

        out = default_serialize(transformed, buf, line_length)
        if undo_transform:
            obj.transform_to_native()
        return out

    @classmethod
    def value_repr(cls, line):
        """return the representation of the given content line value"""
        return line.value
Functions
validate classmethod
validate(obj, raise_exception=False, complain_unrecognized=False)

Check if the object satisfies this behavior's requirements.

@param obj: The L{ContentLine} or L{Component} to be validated. @param raise_exception: If True, raise a L{base.ValidateError} on validation failure. Otherwise return a boolean. @param complain_unrecognized: If True, fail to validate if an uncrecognized parameter or child is found. Otherwise log the lack of recognition.

Source code in vobjectx/behavior.py
@classmethod
def validate(cls, obj, raise_exception=False, complain_unrecognized=False):
    """Check if the object satisfies this behavior's requirements.

    @param obj:
        The L{ContentLine<base.ContentLine>} or
        L{Component<base.Component>} to be validated.
    @param raise_exception:
        If True, raise a L{base.ValidateError} on validation failure.
        Otherwise return a boolean.
    @param complain_unrecognized:
        If True, fail to validate if an uncrecognized parameter or child is
        found.  Otherwise log the lack of recognition.

    """
    if not cls.allow_group and obj.group is not None:
        raise VObjectError(f"{obj} has a group, but this object doesn't support groups")

    if isinstance(obj, ContentLine):
        return obj.line_validate(obj, raise_exception, complain_unrecognized)

    if isinstance(obj, Component):
        count = {}
        for child in obj.get_children():
            if not child.validate(raise_exception, complain_unrecognized):
                return False
            name = child.name.upper()
            count[name] = count.get(name, 0) + 1
        for key, val in cls.known_children.items():
            if count.get(key, 0) < val[0]:
                if raise_exception:
                    m = "{0} components must contain at least {1} {2}"
                    raise ValidateError(m.format(cls.name, val[0], key))
                return False
            if val[1] and count.get(key, 0) > val[1]:
                if raise_exception:
                    m = "{0} components cannot contain more than {1} {2}"
                    raise ValidateError(m.format(cls.name, val[1], key))
                return False
        return True
    raise VObjectError(f"{obj} is not a Component or Contentline")
transform_to_native staticmethod
transform_to_native(obj)

Turn a ContentLine or Component into a Python-native representation.

If appropriate, turn dates or datetime strings into Python objects. Components containing VTIMEZONEs turn into VtimezoneComponents.

Source code in vobjectx/behavior.py
@staticmethod
def transform_to_native(obj):
    """
    Turn a ContentLine or Component into a Python-native representation.

    If appropriate, turn dates or datetime strings into Python objects.
    Components containing VTIMEZONEs turn into VtimezoneComponents.

    """
    return obj
transform_from_native staticmethod
transform_from_native(obj)

Inverse of transform_to_native.

Source code in vobjectx/behavior.py
@staticmethod
def transform_from_native(obj):
    """
    Inverse of transform_to_native.
    """
    raise NativeError("No transform_from_native defined")
generate_implicit_parameters staticmethod
generate_implicit_parameters(obj)

Generate any required information that don't yet exist.

Source code in vobjectx/behavior.py
@staticmethod
def generate_implicit_parameters(obj):
    """Generate any required information that don't yet exist."""
serialize classmethod
serialize(obj, buf, line_length, validate=True, *args, **kwargs)

Set implicit parameters, do encoding, return unicode string.

If validate is True, raise VObjectError if the line doesn't validate after implicit parameters are generated.

Default is to call base.default_serialize.

Source code in vobjectx/behavior.py
@classmethod
def serialize(cls, obj, buf, line_length, validate=True, *args, **kwargs):  # pylint:disable=unused-argument
    """
    Set implicit parameters, do encoding, return unicode string.

    If validate is True, raise VObjectError if the line doesn't validate
    after implicit parameters are generated.

    Default is to call base.default_serialize.

    """

    cls.generate_implicit_parameters(obj)
    if validate:
        cls.validate(obj, raise_exception=True)

    if obj.is_native:
        transformed = obj.transform_from_native()
        undo_transform = True
    else:
        transformed = obj
        undo_transform = False

    out = default_serialize(transformed, buf, line_length)
    if undo_transform:
        obj.transform_to_native()
    return out
value_repr classmethod
value_repr(line)

return the representation of the given content line value

Source code in vobjectx/behavior.py
@classmethod
def value_repr(cls, line):
    """return the representation of the given content line value"""
    return line.value
Functions
new_from_behavior
new_from_behavior(name, id_=None)

Given a name, return a behaviored ContentLine or Component.

Source code in vobjectx/behavior.py
def new_from_behavior(name, id_=None):
    """
    Given a name, return a behaviored ContentLine or Component.
    """
    name = name.upper()
    behavior = BehaviorRegistry.get(name, id_)
    if behavior is None:
        raise VObjectError(f"No behavior found named {name!s}")
    obj = Component(name) if behavior.is_component else ContentLine(name, [], "")
    obj.behavior = behavior
    obj.is_native = False
    return obj

change_tz

Translate an ics file's events to a different timezone.

Functions
change_tz
change_tz(cal, new_timezone, default, utc_only=False, utc_tz=tz.tzutc())

Change the timezone of the specified component.

Parameters:

Name Type Description Default
cal Component

the component to change

required
new_timezone tzinfo

the timezone to change to

required
default tzinfo

a timezone to assume if the dtstart or dtend in cal doesn't have an existing timezone

required
utc_only bool

only convert dates that are in utc

False
utc_tz tzinfo

the tzinfo to compare to for UTC when processing utc_only=True

tzutc()
Source code in vobjectx/change_tz.py
def change_tz(cal, new_timezone, default, utc_only=False, utc_tz=tz.tzutc()):
    """
    Change the timezone of the specified component.

    Args:
        cal (Component): the component to change
        new_timezone (tzinfo): the timezone to change to
        default (tzinfo): a timezone to assume if the dtstart or dtend in cal doesn't have an existing timezone
        utc_only (bool): only convert dates that are in utc
        utc_tz (tzinfo): the tzinfo to compare to for UTC when processing utc_only=True
    """

    for vevent in getattr(cal, "vevent_list", []):
        start = getattr(vevent, "dtstart", None)
        end = getattr(vevent, "dtend", None)
        for node in (start, end):
            if node:
                dt = node.value
                if isinstance(dt, datetime) and (not utc_only or dt.tzinfo == utc_tz):
                    if dt.tzinfo is None:
                        dt = dt.replace(tzinfo=default)
                    node.value = dt.astimezone(new_timezone)

exceptions

Functions
warn_if_true
warn_if_true(cond=True, raise_error=True)

Warns if unexpected code excecuttion is encountered.

Source code in vobjectx/exceptions.py
def warn_if_true(cond: bool = True, raise_error: bool = True):
    """Warns if unexpected code excecuttion is encountered."""
    if not cond:
        return

    warnings.warn("Unexpected code execution", UserWarning, stacklevel=2)
    if raise_error:
        raise UnusedBranchError()

hcalendar

A microformat for serializing iCalendar data

(http://microformats.org/wiki/hcalendar)

Here is a sample event in an iCalendar:

BEGIN:VCALENDAR PRODID:-//XYZproduct//EN VERSION:2.0 BEGIN:VEVENT URL:http://www.web2con.com/ DTSTART:20051005 DTEND:20051008 SUMMARY:Web 2.0 Conference LOCATION:Argent Hotel\, San Francisco\, CA END:VEVENT END:VCALENDAR

and an equivalent event in hCalendar format with various elements optimized appropriately.

Web 2.0 Conference: October 5- 7, at the Argent Hotel, San Francisco, CA

Classes
HCalendar

Bases: VCalendar2_0

Source code in vobjectx/hcalendar.py
class HCalendar(VCalendar2_0):
    name = "HCALENDAR"
    indent_width = 3

    @classmethod
    def serialize(cls, obj, buf=None, line_length=None, validate=True, *args, **kwargs):
        """
        Serialize iCalendar to HTML using the hCalendar microformat (http://microformats.org/wiki/hcalendar)
        """

        outbuf = buf or get_buffer()

        def get_xml(event_child: str, value, *, tag="span", prefix="") -> str:
            if value:
                return f'{prefix}<{tag} class="{event_child}">{value}</{tag}>:'
            return ""

        # not serializing optional vcalendar wrapper

        vevents = obj.vevent_list

        for event in vevents:
            _event = Event(event)
            _event_data = [get_xml("summary", _event.summary, tag="span")]  # SUMMARY

            # DTSTART
            if _event.dtstart:
                # TODO: Handle non-datetime formats? Spec says we should handle when dtstart isn't included

                _event_data.append(
                    f'<abbr class="dtstart", title="{_event.machine_date(_event.dtstart)}"'
                    f">{_event.human_date(_event.dtstart)}</abbr>"
                )

                # DTEND
                if not _event.dtend and _event.duration:
                    _event.dtend = _event.duration + _event.dtstart
                # TODO: If lacking dtend & duration?

                if _event.dtend:
                    human = _event.dtend
                    # TODO: Human readable part could be smarter, excluding repeated data
                    if type(_event.dtend) is date:
                        human = _event.dtend - timedelta(days=1)

                    _event_data.append(
                        f'- <abbr class="dtend", title="{_event.machine_date(_event.dtend)}"'
                        f">{_event.human_date(human)}</abbr>"
                    )

            # LOCATION
            _event_data.append(get_xml("location", _event.location, tag="span", prefix="at "))
            _event_data.append(get_xml("description", _event.description, tag="div"))

            _event_str = Character.CRLF.join(_event_data)
            if _event.url:
                _event_str = f'<a class="url" href="{_event.url}">{_event_str}</a>'
            _event_str = f'<span class="vevent">{_event_str}</span>'

            outbuf.write(pretty_xml(_event_str, indent=cls.indent_width))

        return outbuf.getvalue()
Functions
serialize classmethod
serialize(obj, buf=None, line_length=None, validate=True, *args, **kwargs)

Serialize iCalendar to HTML using the hCalendar microformat (http://microformats.org/wiki/hcalendar)

Source code in vobjectx/hcalendar.py
@classmethod
def serialize(cls, obj, buf=None, line_length=None, validate=True, *args, **kwargs):
    """
    Serialize iCalendar to HTML using the hCalendar microformat (http://microformats.org/wiki/hcalendar)
    """

    outbuf = buf or get_buffer()

    def get_xml(event_child: str, value, *, tag="span", prefix="") -> str:
        if value:
            return f'{prefix}<{tag} class="{event_child}">{value}</{tag}>:'
        return ""

    # not serializing optional vcalendar wrapper

    vevents = obj.vevent_list

    for event in vevents:
        _event = Event(event)
        _event_data = [get_xml("summary", _event.summary, tag="span")]  # SUMMARY

        # DTSTART
        if _event.dtstart:
            # TODO: Handle non-datetime formats? Spec says we should handle when dtstart isn't included

            _event_data.append(
                f'<abbr class="dtstart", title="{_event.machine_date(_event.dtstart)}"'
                f">{_event.human_date(_event.dtstart)}</abbr>"
            )

            # DTEND
            if not _event.dtend and _event.duration:
                _event.dtend = _event.duration + _event.dtstart
            # TODO: If lacking dtend & duration?

            if _event.dtend:
                human = _event.dtend
                # TODO: Human readable part could be smarter, excluding repeated data
                if type(_event.dtend) is date:
                    human = _event.dtend - timedelta(days=1)

                _event_data.append(
                    f'- <abbr class="dtend", title="{_event.machine_date(_event.dtend)}"'
                    f">{_event.human_date(human)}</abbr>"
                )

        # LOCATION
        _event_data.append(get_xml("location", _event.location, tag="span", prefix="at "))
        _event_data.append(get_xml("description", _event.description, tag="div"))

        _event_str = Character.CRLF.join(_event_data)
        if _event.url:
            _event_str = f'<a class="url" href="{_event.url}">{_event_str}</a>'
        _event_str = f'<span class="vevent">{_event_str}</span>'

        outbuf.write(pretty_xml(_event_str, indent=cls.indent_width))

    return outbuf.getvalue()

helper

Classes
Functions
Modules
constants
Classes
Character dataclass

Space and Line-break characters

Source code in vobjectx/helper/constants.py
@dataclass(frozen=True, slots=True)
class Character:
    """Space and Line-break characters"""

    CR = "\r"
    LF = "\n"
    CRLF = CR + LF
    SPACE = " "
    TAB = "\t"
    SPACEORTAB = SPACE + TAB
funcs
Classes Functions
to_list
to_list(string_or_list)

Turn a string or array value into a list

Source code in vobjectx/helper/funcs.py
def to_list(string_or_list) -> list:
    """Turn a string or array value into a list"""
    return [string_or_list] if isinstance(string_or_list, str) else string_or_list
to_string
to_string(value, sep=' ')

Turn a string or array value into a string

Source code in vobjectx/helper/funcs.py
def to_string(value, sep=" ") -> str:
    """Turn a string or array value into a string"""
    return sep.join(value) if type(value) in (list, tuple) else value
imports_

List of all common imports except future and aliases

parser
Functions
get_transition
get_transition(transition_to, year, tzinfo)

Return the datetime of the transition to/from DST, or None.

Returns the transition time as a naive datetime in local wall-clock time, typically at 2:00 AM for most timezones.

Source code in vobjectx/helper/parser.py
def get_transition(transition_to: str, year: int, tzinfo: dt.tzinfo) -> dt.datetime | None:
    """
    Return the datetime of the transition to/from DST, or None.

    Returns the transition time as a naive datetime in local wall-clock time,
    typically at 2:00 AM for most timezones.
    """
    assert transition_to in TRANSITIONS

    # Get transitions for the specified year
    transitions = get_transistions(tzinfo, start_year=year, end_year=year)

    # Filter transitions based on the requested type
    for trans in transitions:
        is_standard = trans.is_standard
        if (transition_to == "standard" and is_standard) or (transition_to == "daylight" and not is_standard):
            # Use the UTC transition date directly, since transitions occur at
            # a specific instant that corresponds to 2:00 AM local time
            utc_dt = trans.transition_dt
            return dt.datetime(utc_dt.year, utc_dt.month, utc_dt.day, 2, 0, 0)

    # No transition found
    return None
tzinfo_eq
tzinfo_eq(tzinfo1, tzinfo2, start_year=1950, end_year=2030)

Compare offsets and DST transitions from start_year to end_year.

Source code in vobjectx/helper/parser.py
def tzinfo_eq(tzinfo1: dt.tzinfo, tzinfo2: dt.tzinfo, start_year: int = 1950, end_year: int = 2030) -> bool:
    """Compare offsets and DST transitions from start_year to end_year."""
    if tzinfo1 == tzinfo2:
        return True
    if tzinfo1 is None or tzinfo2 is None:
        return False

    t1_transitions = get_transistions(tzinfo1, start_year, end_year)
    t2_transitions = get_transistions(tzinfo2, start_year, end_year)

    for t1, t2 in zip(t1_transitions, t2_transitions):
        if t1 != t2:
            return False

    return True
serializer
Functions
timedelta_to_string
timedelta_to_string(delta)

Convert timedelta to an ical DURATION format: PnYnMnDTnHnMnS

Source code in vobjectx/helper/serializer.py
def timedelta_to_string(delta: dt.timedelta) -> str:
    """Convert timedelta to an ical DURATION format: PnYnMnDTnHnMnS"""
    sign = "-" if delta.days < 0 else ""
    days, hours, minutes, seconds = split_delta(abs(delta))

    output = f"{sign}P"
    if days:
        output += f"{days}D"
    if hours or minutes or seconds:
        output += "T"
    elif not days:  # Deal with zero duration
        output += "T0S"
    if hours:
        output += f"{hours}H"
    if minutes:
        output += f"{minutes}M"
    if seconds:
        output += f"{seconds}S"
    return output
time_to_string
time_to_string(date_or_date_time)

overloading function for date_to_string and datetime_to_string

Source code in vobjectx/helper/serializer.py
def time_to_string(date_or_date_time) -> str:
    """overloading function for date_to_string and datetime_to_string"""
    if hasattr(date_or_date_time, "hour"):
        return datetime_to_string(date_or_date_time)
    return date_to_string(date_or_date_time)
datetime_to_string
datetime_to_string(date_time, convert_to_utc=False)

Ignore tzinfo unless convert_to_utc. Output string.

Source code in vobjectx/helper/serializer.py
def datetime_to_string(date_time, convert_to_utc=False) -> str:
    """Ignore tzinfo unless convert_to_utc. Output string."""
    if date_time.tzinfo and convert_to_utc:
        date_time = date_time.astimezone(UTC_TZ)

    datestr = date_time.strftime("%Y%m%dT%H%M%S")
    if tzinfo_eq(date_time.tzinfo, UTC_TZ):
        datestr += "Z"
    return datestr
delta_to_offset
delta_to_offset(delta)

Returns offset in format : ±HHMM

Source code in vobjectx/helper/serializer.py
def delta_to_offset(delta: dt.timedelta) -> str:
    """Returns offset in format : ±HHMM"""
    # Remark : This code assumes day difference = 0
    abs_delta = split_delta(abs(delta))
    assert abs_delta.days == 0, "rethink this function uses"
    sign_string = "-" if delta.days == -1 else "+"
    return f"{sign_string}{abs_delta.hours:02}{abs_delta.minutes:02}"
wrappers
Functions
deprecated
deprecated(func=None)

This is a decorator which can be used to mark functions as deprecated. It will result in a warning being emitted when the function is used.

Source code in vobjectx/helper/wrappers.py
def deprecated(func=None):
    """This is a decorator which can be used to mark functions as deprecated.
    It will result in a warning being emitted when the function is used."""

    def camel_to_snake(name):
        s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
        x = re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
        return x.replace("date_time", "datetime")

    @wraps(func)
    def wrapper(*args, **kwargs):
        func_name = func.__name__
        warnings.simplefilter("always", DeprecationWarning)  # turn off filter
        new_func = camel_to_snake(func_name)
        warnings.warn(
            f"{func_name}() is deprecated, use {new_func}() instead", category=DeprecationWarning, stacklevel=2
        )
        warnings.simplefilter("default", DeprecationWarning)  # reset filter
        return func(*args, **kwargs)

    return wrapper
grab_testcase
grab_testcase(func)

This is a decorator logs inputs and outputs of a func

Source code in vobjectx/helper/wrappers.py
def grab_testcase(func):
    """This is a decorator logs inputs and outputs of a func"""

    @wraps(func)
    def wrapper(*args, **kwargs):
        logger.warning(func.__name__)
        logger.warning(f"{args=}, {kwargs}")
        result = func(*args, **kwargs)
        logger.warning(f"{result=}")
        return result

    return wrapper

ical

Functions
Modules
ical_helper
Classes Functions
from_last_week_
from_last_week_(dt_)

How many weeks from the end of the month dt is, starting from 1.

Source code in vobjectx/ical/ical_helper.py
def from_last_week_(dt_: dt.datetime) -> int:
    """How many weeks from the end of the month dt is, starting from 1."""

    next_month = dt.datetime(dt_.year, dt_.month, 1) + relativedelta(months=1)
    time_diff = next_month - dt_
    days_gap = time_diff.days + bool(time_diff.seconds)
    return math.ceil(days_gap / 7)
parse_dtstart
parse_dtstart(contentline, allow_signature_mismatch=False)

Convert a contentline's value into a date or date-time.

A variety of clients don't serialize dates with the appropriate VALUE parameter, so rather than failing on these (technically invalid) lines, if allow_signature_mismatch is True, try to parse both varieties.

Source code in vobjectx/ical/ical_helper.py
def parse_dtstart(contentline, allow_signature_mismatch: bool = False) -> dt.datetime | dt.date | None:
    """
    Convert a contentline's value into a date or date-time.

    A variety of clients don't serialize dates with the appropriate VALUE parameter, so rather than failing on these
    (technically invalid) lines, if allow_signature_mismatch is True, try to parse both varieties.
    """
    tzinfo = TzidRegistry.get(getattr(contentline, "tzid_param", None))
    value_param = getattr(contentline, "value_param", "DATE-TIME").upper()
    parsed_dtstart = None
    if value_param == "DATE":
        parsed_dtstart = vtypes.Date(contentline.value).value
    elif value_param == "DATE-TIME":
        try:
            parsed_dtstart = vtypes.DateTime(contentline.value, tzinfo).value
        except ParseError as e:
            if not allow_signature_mismatch:
                raise e
            parsed_dtstart = vtypes.Date(contentline.value).value
    return parsed_dtstart

icalendar

Definitions and behavior for iCalendar, also known as vCalendar 2.0

Classes
TimezoneComponent

Bases: Component

A VTIMEZONE object.

VTIMEZONEs are parsed by tz.tzical, the resulting dt.tzinfo subclass is stored in self.tzinfo, self.tzid stores the TZID associated with this timezone.

@ivar name: The uppercased name of the object, in this case always 'VTIMEZONE'. @ivar tzinfo: A dt.tzinfo subclass representing this timezone. @ivar tzid: The string used to refer to this timezone.

Source code in vobjectx/icalendar.py
class TimezoneComponent(Component):
    """
    A VTIMEZONE object.

    VTIMEZONEs are parsed by tz.tzical, the resulting dt.tzinfo subclass is stored in self.tzinfo, self.tzid stores
    the TZID associated with this timezone.

    @ivar name:
        The uppercased name of the object, in this case always 'VTIMEZONE'.
    @ivar tzinfo:
        A dt.tzinfo subclass representing this timezone.
    @ivar tzid:
        The string used to refer to this timezone.
    """

    def __init__(self, tzinfo=None, *args, **kwds):
        """
        Accept an existing Component or a tzinfo class.
        """
        super().__init__(*args, **kwds)
        self.is_native = True
        # hack to make sure a behavior is assigned
        if self.behavior is None:
            self.behavior = VTimezone
        if tzinfo is not None:
            self.tzinfo = tzinfo
        if not hasattr(self, "name") or self.name == "":
            self.name = "VTIMEZONE"
            self.use_begin = True

    @classmethod
    def register_tzinfo(cls, tzinfo):
        """
        Register tzinfo if it's not already registered, return its tzid.
        """
        tzid = cls.pick_tzid(tzinfo)
        if tzid:
            TzidRegistry.register(tzid, tzinfo, exist_ok=True)
        return tzid

    @property
    def tzinfo(self):
        # workaround for dateutil failing to parse some experimental properties
        good_lines = ("rdate", "rrule", "dtstart", "tzname", "tzoffsetfrom", "tzoffsetto", "tzid")
        # serialize encodes as utf-8, cStringIO will leave utf-8 alone
        buffer = get_buffer()
        # allow empty VTIMEZONEs
        if len(self.contents) == 0:
            return None

        def custom_serialize(obj):
            if isinstance(obj, Component):
                fold_one_line(buffer, f"BEGIN:{obj.name}")
                for child in obj.lines():
                    if child.name.lower() in good_lines:
                        child.serialize(buffer, 75, validate=False)
                for comp in obj.components():
                    custom_serialize(comp)
                fold_one_line(buffer, f"END:{obj.name}")

        custom_serialize(self)
        buffer.seek(0)  # tzical wants to read a stream
        return tz.tzical(buffer).get()

    @tzinfo.setter
    def tzinfo(self, tzinfo, start=2000, end=2030):
        # pylint: disable=r0914
        """
        Create appropriate objects in self to represent tzinfo.

        Collapse DST transitions to rrules as much as possible.

        Assumptions:
        - DST <-> Standard transitions occur on the hour
        - never within a month of one another
        - twice or fewer times a year
        - never in the month of December
        - DST always moves offset exactly one hour later
        - tzinfo classes dst method always treats times that could be in either offset as being in the later regime
        """

        def _handle_else():
            two_hours = dt.timedelta(hours=2)
            # Use fold=1 to get the state after the transition
            # For overlaps, fold=1 is the second instance (Standard time)
            # For gaps, fold=1 is the instance after the gap (Daylight time)

            old_offset = tzinfo.utcoffset((transition - two_hours).replace(fold=0))
            name = tzinfo.tzname(transition.replace(fold=1))
            offset = tzinfo.utcoffset(transition.replace(fold=1))

            rule = {
                "end": None,  # None, or an integer year
                "start": transition,  # the datetime of transition
                "month": transition.month,
                "weekday": transition.weekday(),
                "hour": transition.hour,
                "name": name,
                "plus": int((transition.day - 1) / 7 + 1),  # nth week of the month
                "minus": from_last_week_(transition),  # nth from last week
                "offset": offset,
                "offsetfrom": old_offset,
            }

            if oldrule is None:
                working[transition_to] = rule
            else:
                plus_match = rule["plus"] == oldrule["plus"]
                minus_match = rule["minus"] == oldrule["minus"]
                truth = plus_match or minus_match
                truth = truth and all(rule[key] == oldrule[key] for key in ("month", "weekday", "hour", "offset"))
                if truth:
                    # the old rule is still true, limit to plus or minus
                    oldrule["plus"] = oldrule["plus"] if plus_match else None
                    oldrule["minus"] = oldrule["minus"] if minus_match else None
                else:
                    # the new rule did not match the old
                    oldrule["end"] = year - 1
                    completed[transition_to].append(oldrule)
                    working[transition_to] = rule

        # lists of dictionaries defining rules which are no longer in effect
        completed = {"daylight": [], "standard": []}

        # dictionary defining rules which are currently in effect
        working: dict[str, dict | None] = {"daylight": None, "standard": None}

        # rule may be based on nth week of the month or the nth from the last
        for year in range(start, end + 1):
            newyear = dt.datetime(year, 1, 1)
            for transition_to in TRANSITIONS:
                transition = get_transition(transition_to, year, tzinfo)
                oldrule = working[transition_to]

                if transition == newyear:
                    # transition_to is in effect for the whole year
                    rule = {
                        "end": None,
                        "start": newyear,
                        "month": 1,
                        "weekday": None,
                        "hour": None,
                        "plus": None,
                        "minus": None,
                        "name": tzinfo.tzname(newyear),
                        "offset": tzinfo.utcoffset(newyear),
                        "offsetfrom": tzinfo.utcoffset(newyear),
                    }
                    if oldrule is None:
                        # transition_to was not yet in effect
                        working[transition_to] = rule
                    elif oldrule["offset"] != tzinfo.utcoffset(newyear):
                        # transition_to was already in effect.
                        # old rule was different, it shouldn't continue
                        oldrule["end"] = year - 1
                        completed[transition_to].append(oldrule)
                        working[transition_to] = rule
                elif transition is None:
                    # transition_to is not in effect
                    if oldrule is not None:
                        # transition_to used to be in effect
                        oldrule["end"] = year - 1
                        completed[transition_to].append(oldrule)
                        working[transition_to] = None
                else:
                    # an offset transition was found
                    _handle_else()

        for transition_to, rule in working.items():
            if rule is not None:
                completed[transition_to].append(rule)

        self.contents.tzid = []
        self.contents.daylight = []
        self.contents.standard = []

        self.add("tzid").value = self.pick_tzid(tzinfo, True)

        # old = None # unused?
        for transition_to, rules in completed.items():
            for rule in rules:
                comp = self.add(transition_to)
                dtstart = comp.add("dtstart")
                dtstart.value = rule["start"]
                if rule["name"] is not None:
                    comp.add("tzname").value = rule["name"]
                line = comp.add("tzoffsetto")
                line.value = delta_to_offset(rule["offset"])
                line = comp.add("tzoffsetfrom")
                line.value = delta_to_offset(rule["offsetfrom"])

                num = rule["plus"] or -1 * (rule["minus"] or 0)
                day_string = f"BYDAY={num}{WEEKDAYS[rule['weekday']]}" if num else ""

                end_string = ""
                if rule["end"] is not None:
                    # all year offset, with no rule
                    end_date = dt.datetime(rule["end"], 1, 1)
                    if rule["hour"] is not None:
                        du_rule = rrule.rrule(
                            rrule.YEARLY,
                            bymonth=rule["month"],
                            byweekday=rrule.weekday(rule["weekday"], num),
                            dtstart=dt.datetime(rule["end"], 1, 1, rule["hour"]),
                        )
                        end_date = du_rule[0]
                    end_date = end_date.replace(tzinfo=UTC_TZ) - rule["offsetfrom"]
                    end_string = f"UNTIL={datetime_to_string(end_date)}"

                new_rule = ";".join(["FREQ=YEARLY", day_string, f"BYMONTH={rule['month']}", end_string])
                comp.add("rrule").value = new_rule.strip(";")

    @staticmethod
    def pick_tzid(tzinfo, allow_utc=False):
        """
        Given a tzinfo class, use known APIs to determine TZID, or use tzname.
        """
        if tzinfo is None or (not allow_utc and tzinfo_eq(tzinfo, UTC_TZ)):
            # If tzinfo is UTC, we don't need a TZID
            return None

        for attr in ("key", "tzid", "zone", "_tzid"):
            tzid_ = getattr(tzinfo, attr, None)
            if tzid_:
                return tzid_

        # return tzname for standard (non-DST) time
        not_dst = dt.timedelta(0)
        for month in range(1, 13):
            _dt = dt.datetime(2000, month, 1)
            if tzinfo.dst(_dt) == not_dst:
                return tzinfo.tzname(_dt)

        # there was no standard time in 2000!
        raise VObjectError(f"Unable to guess TZID for tzinfo {tzinfo!s}")

    def __repr__(self):
        return f'<VTIMEZONE | {getattr(self, "tzid", "No TZID")}>'

    def pretty_print(self, level=0, tabwidth=3):
        pre = " " * level * tabwidth
        print(pre, self.name)
        print(pre, "TZID:", self.tzid)
        print("")
Functions
register_tzinfo classmethod
register_tzinfo(tzinfo)

Register tzinfo if it's not already registered, return its tzid.

Source code in vobjectx/icalendar.py
@classmethod
def register_tzinfo(cls, tzinfo):
    """
    Register tzinfo if it's not already registered, return its tzid.
    """
    tzid = cls.pick_tzid(tzinfo)
    if tzid:
        TzidRegistry.register(tzid, tzinfo, exist_ok=True)
    return tzid
pick_tzid staticmethod
pick_tzid(tzinfo, allow_utc=False)

Given a tzinfo class, use known APIs to determine TZID, or use tzname.

Source code in vobjectx/icalendar.py
@staticmethod
def pick_tzid(tzinfo, allow_utc=False):
    """
    Given a tzinfo class, use known APIs to determine TZID, or use tzname.
    """
    if tzinfo is None or (not allow_utc and tzinfo_eq(tzinfo, UTC_TZ)):
        # If tzinfo is UTC, we don't need a TZID
        return None

    for attr in ("key", "tzid", "zone", "_tzid"):
        tzid_ = getattr(tzinfo, attr, None)
        if tzid_:
            return tzid_

    # return tzname for standard (non-DST) time
    not_dst = dt.timedelta(0)
    for month in range(1, 13):
        _dt = dt.datetime(2000, month, 1)
        if tzinfo.dst(_dt) == not_dst:
            return tzinfo.tzname(_dt)

    # there was no standard time in 2000!
    raise VObjectError(f"Unable to guess TZID for tzinfo {tzinfo!s}")
RecurringComponent

Bases: Component

A vCalendar component like VEVENT or VTODO which may recur.

Any recurring component can have one or multiple RRULE, RDATE, EXRULE, or EXDATE lines, and one or zero DTSTART lines. It can also have a variety of children that don't have any recurrence information.

In the example below, note that dtstart is included in the rruleset. This is not the default behavior for dateutil's rrule implementation unless dtstart would already have been a member of the recurrence rule, and as a result, COUNT is wrong. This can be worked around when getting rruleset by adjusting count down by one if an rrule has a count and dtstart isn't in its result set, but by default, the rruleset property doesn't do this work around, to access it getrruleset must be called with addRDate set True.

@property rruleset: A U{rrulesethttps://moin.conectiva.com.br/DateUtil}.

Source code in vobjectx/icalendar.py
class RecurringComponent(Component):
    """
    A vCalendar component like VEVENT or VTODO which may recur.

    Any recurring component can have one or multiple RRULE, RDATE, EXRULE, or EXDATE lines, and one or zero DTSTART
    lines. It can also have a variety of children that don't have any recurrence information.

    In the example below, note that dtstart is included in the rruleset. This is not the default behavior for
    dateutil's rrule implementation unless dtstart would already have been a member of the recurrence rule,
    and as a result, COUNT is wrong. This can be worked around when getting rruleset by adjusting count down by one
    if an rrule has a count and dtstart isn't in its result set, but by default, the rruleset property doesn't do
    this work around, to access it getrruleset must be called with addRDate set True.

    @property rruleset:
        A U{rruleset<https://moin.conectiva.com.br/DateUtil>}.
    """

    def __init__(self, *args, **kwds):
        super().__init__(*args, **kwds)
        self.is_native = True

    @property
    def rruleset(self):
        return self.getrruleset()

    def getrruleset(self, add_rdate=False):
        """
        Get an rruleset created from self.

        If addRDate is True, add an RDATE for dtstart if it's not included in an RRULE or RDATE, and count is
        decremented if it exists.

        Note that for rules which don't match DTSTART, DTSTART may not appear in list(rruleset), although it should.
        By default, an RDATE is not created in these cases, and count isn't updated, so dateutil may list a spurious
        occurrence.
        """

        def _handle_rulenames(add_func_):
            # a Ruby iCalendar library escapes semi-colons in rrules, so also remove any backslashes
            value = line.value.replace("\\", "")
            # If dtstart has no time zone, `until` shouldn't get one, either:
            ignoretz = not isinstance(dtstart, dt.datetime) or dtstart.tzinfo is None
            try:
                until = rrule.rrulestr(value, ignoretz=ignoretz)._until
            except ValueError:
                # WORKAROUND: dateutil<=2.7.2 doesn't set the time zone of dtstart
                if ignoretz:
                    raise
                utc_now = dt.datetime.now(dt.timezone.utc)
                until = rrule.rrulestr(value, dtstart=utc_now)._until

            if until is not None and isinstance(dtstart, dt.datetime) and (until.tzinfo != dtstart.tzinfo):
                # dateutil converts the UNTIL date to a datetime,
                # check to see if the UNTIL parameter value was a date
                vals = dict(pair.split("=") for pair in value.upper().split(";"))
                if len(vals.get("UNTIL", "")) == 8:
                    until = dt.datetime.combine(until.date(), dtstart.time())
                # While RFC2445 says UNTIL MUST be UTC, Chandler allows floating recurring events, and uses
                # floating UNTIL values. Also, some odd floating UNTIL but timezoned DTSTART values have
                # shown up in the wild, so put floating UNTIL values DTSTART's timezone
                if until.tzinfo is None:
                    until = until.replace(tzinfo=dtstart.tzinfo)

                # RFC2445 actually states that UNTIL must be a UTC value. Whilst the changes above work OK,
                # one problem case is if DTSTART is floating but UNTIL is properly specified as UTC (or with
                # a TZID). In that case dateutil will fail datetime comparisons. There is no easy solution to
                # this as there is no obvious timezone (at this point) to do proper floating time offset
                # comparisons. The best we can do is treat the UNTIL value as floating. This could mean
                # incorrect determination of the last instance. The better solution here is to encourage
                # clients to use COUNT rather than UNTIL when DTSTART is floating.

                until = until.replace(tzinfo=None) if dtstart.tzinfo is None else until.astimezone(dtstart.tzinfo)

            value_without_until = ";".join(pair for pair in value.split(";") if pair.split("=")[0].upper() != "UNTIL")
            rule = rrule.rrulestr(value_without_until, dtstart=dtstart, ignoretz=ignoretz)
            rule._until = until

            # add the rrule or exrule to the rruleset
            add_func_(rule)

        rruleset = None
        for name in DATESANDRULES:
            addfunc = None
            for line in self.contents.get(name, ()):
                # don't bother creating a rruleset unless there's a rule
                rruleset = rruleset or rrule.rruleset()
                addfunc = addfunc or getattr(rruleset, name)

                try:
                    dtstart = self.dtstart.value
                except (AttributeError, KeyError):
                    # Special for VTODO - try DUE property instead
                    if self.name != "VTODO":
                        # if there's no dtstart, just return None
                        logger.error("failed to get dtstart with VTODO")
                        return None

                    try:
                        dtstart = self.due.value
                    except (AttributeError, KeyError):
                        # if there's no due, just return None
                        logger.error("failed to find DUE at all.")
                        return None

                if name in DATENAMES:
                    # ignoring RDATEs with PERIOD values for now
                    for _dt in line.value:
                        addfunc(date_to_datetime_(_dt))
                elif name in RULENAMES:
                    _handle_rulenames(add_func_=addfunc)

                if name in ["rrule", "rdate"] and add_rdate:
                    # rlist = rruleset._rrule if name == 'rrule' else rruleset._rdate

                    # dateutils does not work with all-day (dt.date) items so we need to convert to a
                    # dt.datetime (which is what dateutils does internally)
                    adddtstart = date_to_datetime_(dtstart)

                    try:  # sourcery skip
                        if name == "rdate" and rruleset._rdate[0] != adddtstart:
                            rruleset.rdate(adddtstart)

                        elif name == "rrule" and rruleset._rrule[-1][0] != adddtstart:
                            rruleset.rdate(adddtstart)

                            if rruleset._rrule[-1]._count is not None:
                                rruleset._rrule[-1]._count -= 1
                    except IndexError:
                        # it's conceivable that an rrule has 0 datetimes
                        pass

        return rruleset

    @rruleset.setter
    def rruleset(self, rruleset):
        def _parse_values_from_rule(rule) -> dict:
            _value_map = {"BYYEARDAY": rule._byyearday, "BYWEEKNO": rule._byweekno, "BYSETPOS": rule._bysetpos}
            values_ = {}
            for k, v in _value_map.items():
                if v is not None:
                    values_[k] = [str(n) for n in v]

            if rule._interval != 1:
                values_["INTERVAL"] = [str(rule._interval)]
            if rule._wkst != 0:  # wkst defaults to Monday
                values_["WKST"] = [WEEKDAYS[rule._wkst]]

            if rule._count is not None:
                values_["COUNT"] = [str(rule._count)]
            elif rule._until is not None:
                values_["UNTIL"] = [until_serialize(rule._until)]

            days = []
            if rule._byweekday is not None and (
                rrule.WEEKLY != rule._freq or len(rule._byweekday) != 1 or rule._dtstart.weekday() != rule._byweekday[0]
            ):
                # ignore byweekday if freq is WEEKLY and day correlates with dtstart because
                # it was automatically set by dateutil
                days.extend(WEEKDAYS[n] for n in rule._byweekday)

            if rule._bynweekday is not None:
                days.extend(n + WEEKDAYS[day] for day, n in rule._bynweekday)

            if days:
                values_["BYDAY"] = days

            if rule._bymonthday and not (
                rule._freq <= rrule.MONTHLY and len(rule._bymonthday) == 1 and rule._bymonthday[0] == rule._dtstart.day
            ):
                # ignore bymonthday if it's generated by dateutil
                values_["BYMONTHDAY"] = [str(n) for n in rule._bymonthday]

            if rule._bynmonthday:
                values_.setdefault("BYMONTHDAY", []).extend(str(n) for n in rule._bynmonthday)

            if rule._bymonth and (
                rule._byweekday
                or not (
                    rule._freq == rrule.YEARLY and len(rule._bymonth) == 1 and rule._bymonth[0] == rule._dtstart.month
                )
            ):
                # ignore bymonth if it's generated by dateutil
                values_["BYMONTH"] = [str(n) for n in rule._bymonth]

            # byhour, byminute, bysecond are always ignored for now
            return values_

        # Get DTSTART from component (or DUE if no DTSTART in a VTODO)
        try:
            dtstart = self.dtstart.value
        except (AttributeError, KeyError):
            if self.name != "VTODO":
                raise
            dtstart = self.due.value

        is_date = type(dtstart) is dt.date

        dtstart = date_to_datetime_(dtstart)
        # make sure to convert time zones to UTC
        until_serialize = date_to_string if is_date else partial(datetime_to_string, convert_to_utc=True)

        for name in DATESANDRULES:
            if name in self.contents:
                del self.contents[name]
            setlist = getattr(rruleset, f"_{name}")
            if name in DATENAMES:
                setlist = list(setlist)  # make a copy of the list
                if name == "rdate" and dtstart in setlist:
                    setlist.remove(dtstart)
                if is_date:
                    setlist = [_dt.date() for _dt in setlist]
                if setlist:
                    self.add(name).value = setlist
            elif name in RULENAMES:
                for rule_item in setlist:
                    buf = get_buffer()
                    buf.write(f"FREQ={rrule.FREQNAMES[rule_item._freq]}")

                    values = _parse_values_from_rule(rule_item)
                    for key, paramvals in values.items():
                        buf.write(f";{key}={','.join(paramvals)}")

                    self.add(name).value = buf.getvalue()
Functions
getrruleset
getrruleset(add_rdate=False)

Get an rruleset created from self.

If addRDate is True, add an RDATE for dtstart if it's not included in an RRULE or RDATE, and count is decremented if it exists.

Note that for rules which don't match DTSTART, DTSTART may not appear in list(rruleset), although it should. By default, an RDATE is not created in these cases, and count isn't updated, so dateutil may list a spurious occurrence.

Source code in vobjectx/icalendar.py
def getrruleset(self, add_rdate=False):
    """
    Get an rruleset created from self.

    If addRDate is True, add an RDATE for dtstart if it's not included in an RRULE or RDATE, and count is
    decremented if it exists.

    Note that for rules which don't match DTSTART, DTSTART may not appear in list(rruleset), although it should.
    By default, an RDATE is not created in these cases, and count isn't updated, so dateutil may list a spurious
    occurrence.
    """

    def _handle_rulenames(add_func_):
        # a Ruby iCalendar library escapes semi-colons in rrules, so also remove any backslashes
        value = line.value.replace("\\", "")
        # If dtstart has no time zone, `until` shouldn't get one, either:
        ignoretz = not isinstance(dtstart, dt.datetime) or dtstart.tzinfo is None
        try:
            until = rrule.rrulestr(value, ignoretz=ignoretz)._until
        except ValueError:
            # WORKAROUND: dateutil<=2.7.2 doesn't set the time zone of dtstart
            if ignoretz:
                raise
            utc_now = dt.datetime.now(dt.timezone.utc)
            until = rrule.rrulestr(value, dtstart=utc_now)._until

        if until is not None and isinstance(dtstart, dt.datetime) and (until.tzinfo != dtstart.tzinfo):
            # dateutil converts the UNTIL date to a datetime,
            # check to see if the UNTIL parameter value was a date
            vals = dict(pair.split("=") for pair in value.upper().split(";"))
            if len(vals.get("UNTIL", "")) == 8:
                until = dt.datetime.combine(until.date(), dtstart.time())
            # While RFC2445 says UNTIL MUST be UTC, Chandler allows floating recurring events, and uses
            # floating UNTIL values. Also, some odd floating UNTIL but timezoned DTSTART values have
            # shown up in the wild, so put floating UNTIL values DTSTART's timezone
            if until.tzinfo is None:
                until = until.replace(tzinfo=dtstart.tzinfo)

            # RFC2445 actually states that UNTIL must be a UTC value. Whilst the changes above work OK,
            # one problem case is if DTSTART is floating but UNTIL is properly specified as UTC (or with
            # a TZID). In that case dateutil will fail datetime comparisons. There is no easy solution to
            # this as there is no obvious timezone (at this point) to do proper floating time offset
            # comparisons. The best we can do is treat the UNTIL value as floating. This could mean
            # incorrect determination of the last instance. The better solution here is to encourage
            # clients to use COUNT rather than UNTIL when DTSTART is floating.

            until = until.replace(tzinfo=None) if dtstart.tzinfo is None else until.astimezone(dtstart.tzinfo)

        value_without_until = ";".join(pair for pair in value.split(";") if pair.split("=")[0].upper() != "UNTIL")
        rule = rrule.rrulestr(value_without_until, dtstart=dtstart, ignoretz=ignoretz)
        rule._until = until

        # add the rrule or exrule to the rruleset
        add_func_(rule)

    rruleset = None
    for name in DATESANDRULES:
        addfunc = None
        for line in self.contents.get(name, ()):
            # don't bother creating a rruleset unless there's a rule
            rruleset = rruleset or rrule.rruleset()
            addfunc = addfunc or getattr(rruleset, name)

            try:
                dtstart = self.dtstart.value
            except (AttributeError, KeyError):
                # Special for VTODO - try DUE property instead
                if self.name != "VTODO":
                    # if there's no dtstart, just return None
                    logger.error("failed to get dtstart with VTODO")
                    return None

                try:
                    dtstart = self.due.value
                except (AttributeError, KeyError):
                    # if there's no due, just return None
                    logger.error("failed to find DUE at all.")
                    return None

            if name in DATENAMES:
                # ignoring RDATEs with PERIOD values for now
                for _dt in line.value:
                    addfunc(date_to_datetime_(_dt))
            elif name in RULENAMES:
                _handle_rulenames(add_func_=addfunc)

            if name in ["rrule", "rdate"] and add_rdate:
                # rlist = rruleset._rrule if name == 'rrule' else rruleset._rdate

                # dateutils does not work with all-day (dt.date) items so we need to convert to a
                # dt.datetime (which is what dateutils does internally)
                adddtstart = date_to_datetime_(dtstart)

                try:  # sourcery skip
                    if name == "rdate" and rruleset._rdate[0] != adddtstart:
                        rruleset.rdate(adddtstart)

                    elif name == "rrule" and rruleset._rrule[-1][0] != adddtstart:
                        rruleset.rdate(adddtstart)

                        if rruleset._rrule[-1]._count is not None:
                            rruleset._rrule[-1]._count -= 1
                except IndexError:
                    # it's conceivable that an rrule has 0 datetimes
                    pass

    return rruleset
TextBehavior

Bases: Behavior

Provide backslash escape encoding/decoding for single valued properties.

TextBehavior also deals with base64 encoding if the ENCODING parameter is explicitly set to BASE64.

Source code in vobjectx/icalendar.py
class TextBehavior(Behavior):
    """
    Provide backslash escape encoding/decoding for single valued properties.

    TextBehavior also deals with base64 encoding if the ENCODING parameter is explicitly set to BASE64.
    """

    base64string = "BASE64"  # vCard uses B

    @classmethod
    def decode(cls, line):
        """Remove backslash escaping from line.value."""
        if line.is_encoded:
            encoding = getattr(line, "encoding_param", None)
            if encoding and encoding.upper() == cls.base64string:
                line.value = base64.b64decode(line.value)
            else:
                line.value = string_to_text_values(line.value)[0]
            line.is_encoded = False

    @classmethod
    def encode(cls, line: VBase):
        """Backslash escape line.value."""
        if not line.is_encoded:
            encoding = getattr(line, "encoding_param", None)
            if encoding and encoding.upper() == cls.base64string:
                line.value = base64.b64encode(line.value.encode("utf-8")).decode("utf-8").replace("\n", "")
            else:
                line.value = backslash_escape(line.value)
            line.is_encoded = True
Functions
decode classmethod
decode(line)

Remove backslash escaping from line.value.

Source code in vobjectx/icalendar.py
@classmethod
def decode(cls, line):
    """Remove backslash escaping from line.value."""
    if line.is_encoded:
        encoding = getattr(line, "encoding_param", None)
        if encoding and encoding.upper() == cls.base64string:
            line.value = base64.b64decode(line.value)
        else:
            line.value = string_to_text_values(line.value)[0]
        line.is_encoded = False
encode classmethod
encode(line)

Backslash escape line.value.

Source code in vobjectx/icalendar.py
@classmethod
def encode(cls, line: VBase):
    """Backslash escape line.value."""
    if not line.is_encoded:
        encoding = getattr(line, "encoding_param", None)
        if encoding and encoding.upper() == cls.base64string:
            line.value = base64.b64encode(line.value.encode("utf-8")).decode("utf-8").replace("\n", "")
        else:
            line.value = backslash_escape(line.value)
        line.is_encoded = True
RecurringBehavior

Bases: VCalendarComponentBehavior

Parent Behavior for components which should be RecurringComponents.

Source code in vobjectx/icalendar.py
class RecurringBehavior(VCalendarComponentBehavior):
    """
    Parent Behavior for components which should be RecurringComponents.
    """

    has_native = True

    @staticmethod
    def transform_to_native(obj):
        """
        Turn a recurring Component into a RecurringComponent.
        """
        if not obj.is_native:
            object.__setattr__(obj, "__class__", RecurringComponent)
            obj.is_native = True
        return obj

    @staticmethod
    def transform_from_native(obj):
        if obj.is_native:
            object.__setattr__(obj, "__class__", Component)
            obj.is_native = False
        return obj

    @staticmethod
    def generate_implicit_parameters(obj):
        """
        Generate a UID and DTSTAMP if one does not exist.

        This is just a dummy implementation, for now.
        """
        if not hasattr(obj, "uid"):
            now = dt.datetime.now(UTC_TZ)
            now = datetime_to_string(now)
            host = socket.gethostname()
            obj.add(ContentLine("UID", [], f"{now} - {get_random_int()}@{host}"))

        if not hasattr(obj, "dtstamp"):
            now = dt.datetime.now(UTC_TZ)
            obj.add("dtstamp").value = now

    @classmethod
    def validate(cls, obj, raise_exception=True, complain_unrecognized=False):
        if hasattr(obj, "recurrence_id") and hasattr(obj, "dtstart"):
            if type(obj.dtstart.value) is not type(obj.recurrence_id.value):
                raise ValidateError("RECURRENCE-ID and DTSTART must be of same type")
        return super().validate(obj, raise_exception, complain_unrecognized)
Functions
transform_to_native staticmethod
transform_to_native(obj)

Turn a recurring Component into a RecurringComponent.

Source code in vobjectx/icalendar.py
@staticmethod
def transform_to_native(obj):
    """
    Turn a recurring Component into a RecurringComponent.
    """
    if not obj.is_native:
        object.__setattr__(obj, "__class__", RecurringComponent)
        obj.is_native = True
    return obj
generate_implicit_parameters staticmethod
generate_implicit_parameters(obj)

Generate a UID and DTSTAMP if one does not exist.

This is just a dummy implementation, for now.

Source code in vobjectx/icalendar.py
@staticmethod
def generate_implicit_parameters(obj):
    """
    Generate a UID and DTSTAMP if one does not exist.

    This is just a dummy implementation, for now.
    """
    if not hasattr(obj, "uid"):
        now = dt.datetime.now(UTC_TZ)
        now = datetime_to_string(now)
        host = socket.gethostname()
        obj.add(ContentLine("UID", [], f"{now} - {get_random_int()}@{host}"))

    if not hasattr(obj, "dtstamp"):
        now = dt.datetime.now(UTC_TZ)
        obj.add("dtstamp").value = now
DateTimeBehavior

Bases: Behavior

Parent Behavior for ContentLines containing one DATE-TIME.

Source code in vobjectx/icalendar.py
class DateTimeBehavior(Behavior):
    """
    Parent Behavior for ContentLines containing one DATE-TIME.
    """

    has_native = True

    @staticmethod
    def transform_to_native(obj):
        """
        Turn obj.value into a dt.

        RFC2445 allows times without time zone information, "floating times" in some properties. Mostly, this isn't
        what you want, but when parsing a file, real floating times are noted by setting to 'TRUE' the
        X-VOBJ-FLOATINGTIME-ALLOWED parameter.
        """
        if obj.is_native:
            return obj
        obj.is_native = True
        if obj.value == "":
            return obj

        # we're cheating a little here, parse_dtstart allows DATE
        obj.value = parse_dtstart(obj)
        if obj.value.tzinfo is None:
            obj.params["X-VOBJ-FLOATINGTIME-ALLOWED"] = ["TRUE"]
        if obj.params.get("TZID"):
            # Keep a copy of the original TZID around
            obj.params["X-VOBJ-ORIGINAL-TZID"] = [obj.params.pop("TZID")]
        return obj

    @classmethod
    def transform_from_native(cls, obj):
        """
        Replace the datetime in obj.value with an ISO 8601 string.
        """
        if obj.is_native:
            obj.is_native = False
            tzid = TimezoneComponent.register_tzinfo(obj.value.tzinfo)
            obj_value: str | dt.datetime = datetime_to_string(obj.value, cls.force_utc)
            if not cls.force_utc and tzid is not None:
                obj.tzid_param = tzid
            if obj.params.get("X-VOBJ-ORIGINAL-TZID"):
                if not hasattr(obj, "tzid_param"):
                    obj.tzid_param = obj.x_vobj_original_tzid_param
                del obj.params["X-VOBJ-ORIGINAL-TZID"]
            obj.value = obj_value
        return obj
Functions
transform_to_native staticmethod
transform_to_native(obj)

Turn obj.value into a dt.

RFC2445 allows times without time zone information, "floating times" in some properties. Mostly, this isn't what you want, but when parsing a file, real floating times are noted by setting to 'TRUE' the X-VOBJ-FLOATINGTIME-ALLOWED parameter.

Source code in vobjectx/icalendar.py
@staticmethod
def transform_to_native(obj):
    """
    Turn obj.value into a dt.

    RFC2445 allows times without time zone information, "floating times" in some properties. Mostly, this isn't
    what you want, but when parsing a file, real floating times are noted by setting to 'TRUE' the
    X-VOBJ-FLOATINGTIME-ALLOWED parameter.
    """
    if obj.is_native:
        return obj
    obj.is_native = True
    if obj.value == "":
        return obj

    # we're cheating a little here, parse_dtstart allows DATE
    obj.value = parse_dtstart(obj)
    if obj.value.tzinfo is None:
        obj.params["X-VOBJ-FLOATINGTIME-ALLOWED"] = ["TRUE"]
    if obj.params.get("TZID"):
        # Keep a copy of the original TZID around
        obj.params["X-VOBJ-ORIGINAL-TZID"] = [obj.params.pop("TZID")]
    return obj
transform_from_native classmethod
transform_from_native(obj)

Replace the datetime in obj.value with an ISO 8601 string.

Source code in vobjectx/icalendar.py
@classmethod
def transform_from_native(cls, obj):
    """
    Replace the datetime in obj.value with an ISO 8601 string.
    """
    if obj.is_native:
        obj.is_native = False
        tzid = TimezoneComponent.register_tzinfo(obj.value.tzinfo)
        obj_value: str | dt.datetime = datetime_to_string(obj.value, cls.force_utc)
        if not cls.force_utc and tzid is not None:
            obj.tzid_param = tzid
        if obj.params.get("X-VOBJ-ORIGINAL-TZID"):
            if not hasattr(obj, "tzid_param"):
                obj.tzid_param = obj.x_vobj_original_tzid_param
            del obj.params["X-VOBJ-ORIGINAL-TZID"]
        obj.value = obj_value
    return obj
UTCDateTimeBehavior

Bases: DateTimeBehavior

A value which must be specified in UTC.

Source code in vobjectx/icalendar.py
class UTCDateTimeBehavior(DateTimeBehavior):
    """
    A value which must be specified in UTC.
    """

    force_utc = True
DateOrDateTimeBehavior

Bases: Behavior

Parent Behavior for ContentLines containing one DATE or DATE-TIME.

Source code in vobjectx/icalendar.py
class DateOrDateTimeBehavior(Behavior):
    """
    Parent Behavior for ContentLines containing one DATE or DATE-TIME.
    """

    has_native = True

    @staticmethod
    def transform_to_native(obj):
        """Turn obj.value into a date or dt."""
        if obj.is_native:
            return obj
        obj.is_native = True
        if obj.value == "":
            return obj

        obj.value = parse_dtstart(obj, allow_signature_mismatch=True)
        if getattr(obj, "value_param", "DATE-TIME").upper() == "DATE-TIME" and hasattr(obj, "tzid_param"):
            # Keep a copy of the original TZID around
            obj.params["X-VOBJ-ORIGINAL-TZID"] = [obj.tzid_param]
            del obj.tzid_param
        return obj

    @staticmethod
    def transform_from_native(obj):
        """
        Replace the date or datetime in obj.value with an ISO 8601 string.
        """
        if type(obj.value) is not dt.date:
            return DateTimeBehavior.transform_from_native(obj)
        obj.is_native = False
        obj.value_param = "DATE"
        obj.value = date_to_string(obj.value)
        return obj
Functions
transform_to_native staticmethod
transform_to_native(obj)

Turn obj.value into a date or dt.

Source code in vobjectx/icalendar.py
@staticmethod
def transform_to_native(obj):
    """Turn obj.value into a date or dt."""
    if obj.is_native:
        return obj
    obj.is_native = True
    if obj.value == "":
        return obj

    obj.value = parse_dtstart(obj, allow_signature_mismatch=True)
    if getattr(obj, "value_param", "DATE-TIME").upper() == "DATE-TIME" and hasattr(obj, "tzid_param"):
        # Keep a copy of the original TZID around
        obj.params["X-VOBJ-ORIGINAL-TZID"] = [obj.tzid_param]
        del obj.tzid_param
    return obj
transform_from_native staticmethod
transform_from_native(obj)

Replace the date or datetime in obj.value with an ISO 8601 string.

Source code in vobjectx/icalendar.py
@staticmethod
def transform_from_native(obj):
    """
    Replace the date or datetime in obj.value with an ISO 8601 string.
    """
    if type(obj.value) is not dt.date:
        return DateTimeBehavior.transform_from_native(obj)
    obj.is_native = False
    obj.value_param = "DATE"
    obj.value = date_to_string(obj.value)
    return obj
MultiDateBehavior

Bases: Behavior

Parent Behavior for ContentLines containing one or more DATE, DATE-TIME, or PERIOD.

Source code in vobjectx/icalendar.py
class MultiDateBehavior(Behavior):
    """
    Parent Behavior for ContentLines containing one or more DATE, DATE-TIME, or PERIOD.
    """

    has_native = True

    @staticmethod
    def transform_to_native(obj):
        """
        Turn obj.value into a list of dates, datetimes, or (datetime, timedelta) tuples.
        """
        if obj.is_native:
            return obj
        obj.is_native = True
        if obj.value == "":
            obj.value = []
            return obj
        tzinfo = TzidRegistry.get(getattr(obj, "tzid_param", None))
        value_param = getattr(obj, "value_param", "DATE-TIME").upper()
        val_texts = obj.value.split(",")
        if value_param == "DATE":
            obj.value = [vtypes.Date(x).value for x in val_texts]
        elif value_param == "DATE-TIME":
            obj.value = [vtypes.DateTime(x, tzinfo).value for x in val_texts]
        elif value_param == "PERIOD":
            obj.value = [vtypes.Period(x, tzinfo).value for x in val_texts]
        return obj

    @staticmethod
    def transform_from_native(obj):
        """
        Replace the date, datetime or period tuples in obj.value with appropriate strings.
        """
        if obj.value and type(obj.value[0]) is dt.date:
            obj.is_native = False
            obj.value_param = "DATE"
            obj.value = ",".join([date_to_string(val) for val in obj.value])

        # Fixme: handle PERIOD case
        elif obj.is_native:
            obj.is_native = False
            transformed = []
            tzid = None
            for val in obj.value:
                if tzid is None and type(val) is dt.datetime:
                    tzid = TimezoneComponent.register_tzinfo(val.tzinfo)
                    if tzid is not None:
                        obj.tzid_param = tzid
                transformed.append(datetime_to_string(val))
            obj.value = ",".join(transformed)
        return obj
Functions
transform_to_native staticmethod
transform_to_native(obj)

Turn obj.value into a list of dates, datetimes, or (datetime, timedelta) tuples.

Source code in vobjectx/icalendar.py
@staticmethod
def transform_to_native(obj):
    """
    Turn obj.value into a list of dates, datetimes, or (datetime, timedelta) tuples.
    """
    if obj.is_native:
        return obj
    obj.is_native = True
    if obj.value == "":
        obj.value = []
        return obj
    tzinfo = TzidRegistry.get(getattr(obj, "tzid_param", None))
    value_param = getattr(obj, "value_param", "DATE-TIME").upper()
    val_texts = obj.value.split(",")
    if value_param == "DATE":
        obj.value = [vtypes.Date(x).value for x in val_texts]
    elif value_param == "DATE-TIME":
        obj.value = [vtypes.DateTime(x, tzinfo).value for x in val_texts]
    elif value_param == "PERIOD":
        obj.value = [vtypes.Period(x, tzinfo).value for x in val_texts]
    return obj
transform_from_native staticmethod
transform_from_native(obj)

Replace the date, datetime or period tuples in obj.value with appropriate strings.

Source code in vobjectx/icalendar.py
@staticmethod
def transform_from_native(obj):
    """
    Replace the date, datetime or period tuples in obj.value with appropriate strings.
    """
    if obj.value and type(obj.value[0]) is dt.date:
        obj.is_native = False
        obj.value_param = "DATE"
        obj.value = ",".join([date_to_string(val) for val in obj.value])

    # Fixme: handle PERIOD case
    elif obj.is_native:
        obj.is_native = False
        transformed = []
        tzid = None
        for val in obj.value:
            if tzid is None and type(val) is dt.datetime:
                tzid = TimezoneComponent.register_tzinfo(val.tzinfo)
                if tzid is not None:
                    obj.tzid_param = tzid
            transformed.append(datetime_to_string(val))
        obj.value = ",".join(transformed)
    return obj
MultiTextBehavior

Bases: Behavior

Provide backslash escape encoding/decoding of each of several values.

After transformation, value is a list of strings.

Source code in vobjectx/icalendar.py
class MultiTextBehavior(Behavior):
    """
    Provide backslash escape encoding/decoding of each of several values.

    After transformation, value is a list of strings.
    """

    list_separator = ","

    @classmethod
    def decode(cls, line):
        """
        Remove backslash escaping from line.value, then split on commas.
        """
        if line.is_encoded:
            line.value = string_to_text_values(line.value, list_separator=cls.list_separator)
            line.is_encoded = False

    @classmethod
    def encode(cls, line):
        """
        Backslash escape line.value.
        """
        if not line.is_encoded:
            line.value = cls.list_separator.join(backslash_escape(val) for val in line.value)
            line.is_encoded = True
Functions
decode classmethod
decode(line)

Remove backslash escaping from line.value, then split on commas.

Source code in vobjectx/icalendar.py
@classmethod
def decode(cls, line):
    """
    Remove backslash escaping from line.value, then split on commas.
    """
    if line.is_encoded:
        line.value = string_to_text_values(line.value, list_separator=cls.list_separator)
        line.is_encoded = False
encode classmethod
encode(line)

Backslash escape line.value.

Source code in vobjectx/icalendar.py
@classmethod
def encode(cls, line):
    """
    Backslash escape line.value.
    """
    if not line.is_encoded:
        line.value = cls.list_separator.join(backslash_escape(val) for val in line.value)
        line.is_encoded = True
VCalendar2

Bases: VCalendarComponentBehavior

vCalendar 2.0 behavior. With added VAVAILABILITY support.

Source code in vobjectx/icalendar.py
class VCalendar2(VCalendarComponentBehavior):
    """
    vCalendar 2.0 behavior. With added VAVAILABILITY support.
    """

    name = "VCALENDAR"
    description = "vCalendar 2.0, also known as iCalendar."
    version_string = "2.0"
    sort_first = ("VERSION", "CALSCALE", "METHOD", "PRODID", "VTIMEZONE")
    known_children = {
        "CALSCALE": (0, 1, None),  # min, max, behavior_registry id
        "METHOD": (0, 1, None),
        "VERSION": (0, 1, None),  # required, but auto-generated
        "PRODID": (1, 1, None),
        "VTIMEZONE": (0, None, None),
        "VEVENT": (0, None, None),
        "VTODO": (0, None, None),
        "VJOURNAL": (0, None, None),
        "VFREEBUSY": (0, None, None),
        "VAVAILABILITY": (0, None, None),
    }

    @classmethod
    def generate_implicit_parameters(cls, obj):
        """
        Create PRODID, VERSION and VTIMEZONEs if needed.

        VTIMEZONEs will need to exist whenever TZID parameters exist or when datetimes with tzinfo exist.
        """

        def find_tzids(obj_, table: set):
            if isinstance(obj_, ContentLine) and (obj_.behavior is None or not obj_.behavior.force_utc):
                if getattr(obj_, "tzid_param", None):
                    warn_if_true()
                    table.add(obj_.tzid_param)
                else:
                    if type(obj_.value) is list:
                        for _ in obj_.value:
                            tzinfo = getattr(obj_.value, "tzinfo", None)
                            warn_if_true(tzinfo is not None)
                            tzid_ = TimezoneComponent.register_tzinfo(tzinfo)
                            if tzid_:
                                table.add(tzid_)
                    else:
                        tzinfo = getattr(obj_.value, "tzinfo", None)
                        tzid_ = TimezoneComponent.register_tzinfo(tzinfo)
                        if tzid_:
                            table.add(tzid_)
            for child in obj_.get_children():
                if obj_.name != "VTIMEZONE":
                    find_tzids(child, table)

        for comp in obj.components():
            if comp.behavior is not None:
                comp.behavior.generate_implicit_parameters(comp)
        if not hasattr(obj, "prodid"):
            obj.add(ContentLine("PRODID", [], PRODID))
        if not hasattr(obj, "version"):
            obj.add(ContentLine("VERSION", [], cls.version_string))

        tzids_used = set()
        find_tzids(obj, tzids_used)
        oldtzids = [x.tzid.value for x in getattr(obj, "vtimezone_list", [])]
        for tzid in tzids_used:
            if tzid != "UTC" and tzid not in oldtzids:
                obj.add(TimezoneComponent(tzinfo=TzidRegistry.get(tzid)))

    @classmethod
    def serialize(cls, obj, buf, line_length, validate=True, *args, **kwargs):
        """
        Set implicit parameters, do encoding, return unicode string.

        If validate is True, raise VObjectError if the line doesn't validate after implicit parameters are generated.

        Default is to call base.default_serialize.
        """

        cls.generate_implicit_parameters(obj)
        if validate:
            cls.validate(obj, raise_exception=True)

        outbuf = buf or get_buffer()
        group_string = "" if obj.group is None else f"{obj.group}."
        if obj.use_begin:
            fold_one_line(outbuf, f"{group_string}BEGIN:{obj.name}", line_length)

        props, comps = set(), set()
        for key in obj.contents.keys():
            if isinstance(obj.contents[key][0], Component):
                comps.add(key)
            else:
                props.add(key)

        first_props, first_components = [], []
        for key in cls.sort_first:
            if key in props:
                first_props.append(key)
                props.remove(key)
            if key in comps:
                first_components.append(key)
                comps.remove(key)

        sorted_keys = first_props + sorted(props) + first_components + sorted(comps)

        for child in chain.from_iterable(obj.contents[key] for key in sorted_keys):
            # validate is recursive, we only need to validate once
            child.serialize(outbuf, line_length, validate=False)

        if obj.use_begin:
            fold_one_line(outbuf, f"{group_string}END:{obj.name}", line_length)
        if obj.is_native:
            obj.transform_to_native()
        return outbuf.getvalue()
Functions
generate_implicit_parameters classmethod
generate_implicit_parameters(obj)

Create PRODID, VERSION and VTIMEZONEs if needed.

VTIMEZONEs will need to exist whenever TZID parameters exist or when datetimes with tzinfo exist.

Source code in vobjectx/icalendar.py
@classmethod
def generate_implicit_parameters(cls, obj):
    """
    Create PRODID, VERSION and VTIMEZONEs if needed.

    VTIMEZONEs will need to exist whenever TZID parameters exist or when datetimes with tzinfo exist.
    """

    def find_tzids(obj_, table: set):
        if isinstance(obj_, ContentLine) and (obj_.behavior is None or not obj_.behavior.force_utc):
            if getattr(obj_, "tzid_param", None):
                warn_if_true()
                table.add(obj_.tzid_param)
            else:
                if type(obj_.value) is list:
                    for _ in obj_.value:
                        tzinfo = getattr(obj_.value, "tzinfo", None)
                        warn_if_true(tzinfo is not None)
                        tzid_ = TimezoneComponent.register_tzinfo(tzinfo)
                        if tzid_:
                            table.add(tzid_)
                else:
                    tzinfo = getattr(obj_.value, "tzinfo", None)
                    tzid_ = TimezoneComponent.register_tzinfo(tzinfo)
                    if tzid_:
                        table.add(tzid_)
        for child in obj_.get_children():
            if obj_.name != "VTIMEZONE":
                find_tzids(child, table)

    for comp in obj.components():
        if comp.behavior is not None:
            comp.behavior.generate_implicit_parameters(comp)
    if not hasattr(obj, "prodid"):
        obj.add(ContentLine("PRODID", [], PRODID))
    if not hasattr(obj, "version"):
        obj.add(ContentLine("VERSION", [], cls.version_string))

    tzids_used = set()
    find_tzids(obj, tzids_used)
    oldtzids = [x.tzid.value for x in getattr(obj, "vtimezone_list", [])]
    for tzid in tzids_used:
        if tzid != "UTC" and tzid not in oldtzids:
            obj.add(TimezoneComponent(tzinfo=TzidRegistry.get(tzid)))
serialize classmethod
serialize(obj, buf, line_length, validate=True, *args, **kwargs)

Set implicit parameters, do encoding, return unicode string.

If validate is True, raise VObjectError if the line doesn't validate after implicit parameters are generated.

Default is to call base.default_serialize.

Source code in vobjectx/icalendar.py
@classmethod
def serialize(cls, obj, buf, line_length, validate=True, *args, **kwargs):
    """
    Set implicit parameters, do encoding, return unicode string.

    If validate is True, raise VObjectError if the line doesn't validate after implicit parameters are generated.

    Default is to call base.default_serialize.
    """

    cls.generate_implicit_parameters(obj)
    if validate:
        cls.validate(obj, raise_exception=True)

    outbuf = buf or get_buffer()
    group_string = "" if obj.group is None else f"{obj.group}."
    if obj.use_begin:
        fold_one_line(outbuf, f"{group_string}BEGIN:{obj.name}", line_length)

    props, comps = set(), set()
    for key in obj.contents.keys():
        if isinstance(obj.contents[key][0], Component):
            comps.add(key)
        else:
            props.add(key)

    first_props, first_components = [], []
    for key in cls.sort_first:
        if key in props:
            first_props.append(key)
            props.remove(key)
        if key in comps:
            first_components.append(key)
            comps.remove(key)

    sorted_keys = first_props + sorted(props) + first_components + sorted(comps)

    for child in chain.from_iterable(obj.contents[key] for key in sorted_keys):
        # validate is recursive, we only need to validate once
        child.serialize(outbuf, line_length, validate=False)

    if obj.use_begin:
        fold_one_line(outbuf, f"{group_string}END:{obj.name}", line_length)
    if obj.is_native:
        obj.transform_to_native()
    return outbuf.getvalue()
VTimezone

Bases: VCalendarComponentBehavior

Timezone behavior.

Source code in vobjectx/icalendar.py
class VTimezone(VCalendarComponentBehavior):
    """
    Timezone behavior.
    """

    name = "VTIMEZONE"
    has_native = True
    description = "A grouping of component properties that defines a time zone."
    sort_first = ("TZID", "LAST-MODIFIED", "TZURL", "STANDARD", "DAYLIGHT")
    known_children = {
        "TZID": (1, 1, None),  # min, max, behavior_registry id
        "LAST-MODIFIED": (0, 1, None),
        "TZURL": (0, 1, None),
        "STANDARD": (0, None, None),  # NOTE: One of Standard or
        "DAYLIGHT": (0, None, None),  # Daylight must appear
    }

    @classmethod
    def validate(cls, obj, raise_exception=False, complain_unrecognized=False):
        if not hasattr(obj, "tzid") or obj.tzid.value is None:
            if raise_exception:
                raise ValidateError("VTIMEZONE components must contain a valid TZID")
            return False
        if "standard" in obj.contents or "daylight" in obj.contents:
            return super().validate(obj, raise_exception, complain_unrecognized)

        if raise_exception:
            raise ValidateError("VTIMEZONE components must contain a STANDARD or a DAYLIGHT component")
        return False

    @staticmethod
    def transform_to_native(obj):
        if not obj.is_native:
            object.__setattr__(obj, "__class__", TimezoneComponent)
            obj.is_native = True
            obj.register_tzinfo(obj.tzinfo)
        return obj

    @staticmethod
    def transform_from_native(obj):
        return obj
TZID

Bases: Behavior

Don't use TextBehavior for TZID.

RFC2445 only allows TZID lines to be paramtext, so they shouldn't need any encoding or decoding. Unfortunately, some Microsoft products use commas in TZIDs which should NOT be treated as a multi-valued text property, nor do we want to escape them. Leaving them alone works for Microsoft's breakage, and doesn't affect compliant iCalendar streams.

Source code in vobjectx/icalendar.py
class TZID(Behavior):
    """
    Don't use TextBehavior for TZID.

    RFC2445 only allows TZID lines to be paramtext, so they shouldn't need any encoding or decoding.  Unfortunately,
    some Microsoft products use commas in TZIDs which should NOT be treated as a multi-valued text property,
    nor do we want to escape them.  Leaving them alone works for Microsoft's breakage, and doesn't affect compliant
    iCalendar streams.
    """
VEvent

Bases: RecurringBehavior

Event behavior.

Source code in vobjectx/icalendar.py
class VEvent(RecurringBehavior):
    """Event behavior."""

    name = "VEVENT"
    sort_first = ("UID", "RECURRENCE-ID", "DTSTART", "DURATION", "DTEND")

    description = 'A grouping of component properties, and possibly including \
                   "VALARM" calendar components, that represents a scheduled \
                   amount of time on a calendar.'
    known_children = {
        "DTSTART": (0, 1, None),  # min, max, behavior_registry id
        "CLASS": (0, 1, None),
        "CREATED": (0, 1, None),
        "DESCRIPTION": (0, 1, None),
        "GEO": (0, 1, None),
        "LAST-MODIFIED": (0, 1, None),
        "LOCATION": (0, 1, None),
        "ORGANIZER": (0, 1, None),
        "PRIORITY": (0, 1, None),
        "DTSTAMP": (1, 1, None),  # required
        "SEQUENCE": (0, 1, None),
        "STATUS": (0, 1, None),
        "SUMMARY": (0, 1, None),
        "TRANSP": (0, 1, None),
        "UID": (1, 1, None),
        "URL": (0, 1, None),
        "RECURRENCE-ID": (0, 1, None),
        "DTEND": (0, 1, None),  # NOTE: Only one of DtEnd or
        "DURATION": (0, 1, None),  # Duration can appear
        "ATTACH": (0, None, None),
        "ATTENDEE": (0, None, None),
        "CATEGORIES": (0, None, None),
        "COMMENT": (0, None, None),
        "CONTACT": (0, None, None),
        "EXDATE": (0, None, None),
        "EXRULE": (0, None, None),
        "REQUEST-STATUS": (0, None, None),
        "RELATED-TO": (0, None, None),
        "RESOURCES": (0, None, None),
        "RDATE": (0, None, None),
        "RRULE": (0, None, None),
        "VALARM": (0, None, None),
    }

    @classmethod
    def validate(cls, obj, raise_exception=False, complain_unrecognized=False):
        if "dtend" in obj.contents and "duration" in obj.contents:
            if raise_exception:
                raise ValidateError("VEVENT components cannot contain both DTEND and DURATION components")
            return False
        return super().validate(obj, raise_exception, complain_unrecognized)
VTodo

Bases: RecurringBehavior

To-do behavior.

Source code in vobjectx/icalendar.py
class VTodo(RecurringBehavior):
    """To-do behavior."""

    name = "VTODO"
    description = 'A grouping of component properties and possibly "VALARM" \
                   calendar components that represent an action-item or \
                   assignment.'
    known_children = {
        "DTSTART": (0, 1, None),  # min, max, behavior_registry id
        "CLASS": (0, 1, None),
        "COMPLETED": (0, 1, None),
        "CREATED": (0, 1, None),
        "DESCRIPTION": (0, 1, None),
        "GEO": (0, 1, None),
        "LAST-MODIFIED": (0, 1, None),
        "LOCATION": (0, 1, None),
        "ORGANIZER": (0, 1, None),
        "PERCENT": (0, 1, None),
        "PRIORITY": (0, 1, None),
        "DTSTAMP": (1, 1, None),
        "SEQUENCE": (0, 1, None),
        "STATUS": (0, 1, None),
        "SUMMARY": (0, 1, None),
        "UID": (0, 1, None),
        "URL": (0, 1, None),
        "RECURRENCE-ID": (0, 1, None),
        "DUE": (0, 1, None),  # NOTE: Only one of Due or
        "DURATION": (0, 1, None),  # Duration can appear
        "ATTACH": (0, None, None),
        "ATTENDEE": (0, None, None),
        "CATEGORIES": (0, None, None),
        "COMMENT": (0, None, None),
        "CONTACT": (0, None, None),
        "EXDATE": (0, None, None),
        "EXRULE": (0, None, None),
        "REQUEST-STATUS": (0, None, None),
        "RELATED-TO": (0, None, None),
        "RESOURCES": (0, None, None),
        "RDATE": (0, None, None),
        "RRULE": (0, None, None),
        "VALARM": (0, None, None),
    }

    @classmethod
    def validate(cls, obj, raise_exception=False, complain_unrecognized=False):
        if "due" in obj.contents and "duration" in obj.contents:
            if raise_exception:
                raise ValidateError("VTODO components cannot contain both DUE and DURATION components")
            return False
        return super().validate(obj, raise_exception, complain_unrecognized)
VJournal

Bases: RecurringBehavior

Journal entry behavior.

Source code in vobjectx/icalendar.py
class VJournal(RecurringBehavior):
    """
    Journal entry behavior.
    """

    name = "VJOURNAL"
    known_children = {
        "DTSTART": (0, 1, None),  # min, max, behavior_registry id
        "CLASS": (0, 1, None),
        "CREATED": (0, 1, None),
        "DESCRIPTION": (0, 1, None),
        "LAST-MODIFIED": (0, 1, None),
        "ORGANIZER": (0, 1, None),
        "DTSTAMP": (1, 1, None),
        "SEQUENCE": (0, 1, None),
        "STATUS": (0, 1, None),
        "SUMMARY": (0, 1, None),
        "UID": (0, 1, None),
        "URL": (0, 1, None),
        "RECURRENCE-ID": (0, 1, None),
        "ATTACH": (0, None, None),
        "ATTENDEE": (0, None, None),
        "CATEGORIES": (0, None, None),
        "COMMENT": (0, None, None),
        "CONTACT": (0, None, None),
        "EXDATE": (0, None, None),
        "EXRULE": (0, None, None),
        "REQUEST-STATUS": (0, None, None),
        "RELATED-TO": (0, None, None),
        "RDATE": (0, None, None),
        "RRULE": (0, None, None),
    }
VFreeBusy

Bases: VCalendarComponentBehavior

Free/busy state behavior.

Source code in vobjectx/icalendar.py
class VFreeBusy(VCalendarComponentBehavior):
    """
    Free/busy state behavior.
    """

    name = "VFREEBUSY"
    description = "A grouping of component properties that describe either a \
                   request for free/busy time, describe a response to a request \
                   for free/busy time or describe a published set of busy time."
    sort_first = ("UID", "DTSTART", "DURATION", "DTEND")
    known_children = {
        "DTSTART": (0, 1, None),  # min, max, behavior_registry id
        "CONTACT": (0, 1, None),
        "DTEND": (0, 1, None),
        "DURATION": (0, 1, None),
        "ORGANIZER": (0, 1, None),
        "DTSTAMP": (1, 1, None),
        "UID": (0, 1, None),
        "URL": (0, 1, None),
        "ATTENDEE": (0, None, None),
        "COMMENT": (0, None, None),
        "FREEBUSY": (0, None, None),
        "REQUEST-STATUS": (0, None, None),
    }
VAlarm

Bases: VCalendarComponentBehavior

Alarm behavior

Source code in vobjectx/icalendar.py
class VAlarm(VCalendarComponentBehavior):
    """Alarm behavior"""

    name = "VALARM"
    description = "Alarms describe when and how to provide alerts about events and to-dos."
    known_children = {
        "ACTION": (1, 1, None),  # min, max, behavior_registry id
        "TRIGGER": (1, 1, None),
        "DURATION": (0, 1, None),
        "REPEAT": (0, 1, None),
        "DESCRIPTION": (0, 1, None),
    }

    @staticmethod
    def generate_implicit_parameters(obj):
        """Create default ACTION and TRIGGER if they're not set."""
        if not hasattr(obj, "action"):
            obj.add("action").value = "AUDIO"

        if not hasattr(obj, "trigger"):
            obj.add("trigger").value = dt.timedelta(0)

    @classmethod
    def validate(cls, obj, raise_exception: bool = True, complain_unrecognized: bool = False) -> bool:
        contents = obj.contents

        def fail(msg):
            if raise_exception:
                raise ValidateError(msg)
            return False

        action = contents.action[0].value

        # REPEAT and DURATION must appear together
        if ("duration" in contents) ^ ("repeat" in contents):
            return fail("VALARM DURATION and REPEAT must both be present/absent.")

        if action == "DISPLAY":
            if "description" not in contents:
                return fail("DISPLAY VALARM missing DESCRIPTION")

        elif action == "EMAIL":
            for prop in ("description", "summary", "attendee"):
                if prop not in contents:
                    return fail(f"EMAIL VALARM missing {prop.upper()}")

        elif action == "AUDIO":
            if len(contents.get("attach", [])) > 1:
                return fail("AUDIO VALARM can contain only one ATTACH")

        return super().validate(obj, raise_exception, complain_unrecognized)
Functions
generate_implicit_parameters staticmethod
generate_implicit_parameters(obj)

Create default ACTION and TRIGGER if they're not set.

Source code in vobjectx/icalendar.py
@staticmethod
def generate_implicit_parameters(obj):
    """Create default ACTION and TRIGGER if they're not set."""
    if not hasattr(obj, "action"):
        obj.add("action").value = "AUDIO"

    if not hasattr(obj, "trigger"):
        obj.add("trigger").value = dt.timedelta(0)
VAvailability

Bases: VCalendarComponentBehavior

Availability state behavior.

Used to represent user's available time slots.

Source code in vobjectx/icalendar.py
class VAvailability(VCalendarComponentBehavior):
    """
    Availability state behavior.

    Used to represent user's available time slots.
    """

    name = "VAVAILABILITY"
    description = "A component used to represent a user's available time slots."
    sort_first = ("UID", "DTSTART", "DURATION", "DTEND")
    known_children = {
        "UID": (1, 1, None),  # min, max, behavior_registry id
        "DTSTAMP": (1, 1, None),
        "BUSYTYPE": (0, 1, None),
        "CREATED": (0, 1, None),
        "DTSTART": (0, 1, None),
        "LAST-MODIFIED": (0, 1, None),
        "ORGANIZER": (0, 1, None),
        "SEQUENCE": (0, 1, None),
        "SUMMARY": (0, 1, None),
        "URL": (0, 1, None),
        "DTEND": (0, 1, None),
        "DURATION": (0, 1, None),
        "CATEGORIES": (0, None, None),
        "COMMENT": (0, None, None),
        "CONTACT": (0, None, None),
        "AVAILABLE": (0, None, None),
    }

    @classmethod
    def validate(cls, obj, raise_exception=False, complain_unrecognized=False):
        if "dtend" in obj.contents and "duration" in obj.contents:
            if raise_exception:
                raise ValidateError("VAVAILABILITY components cannot contain both DTEND and DURATION components")
            return False
        return super().validate(obj, raise_exception, complain_unrecognized)
Available

Bases: RecurringBehavior

Event behavior.

Source code in vobjectx/icalendar.py
class Available(RecurringBehavior):
    """
    Event behavior.
    """

    name = "AVAILABLE"
    sort_first = ("UID", "RECURRENCE-ID", "DTSTART", "DURATION", "DTEND")
    description = "Defines a period of time in which a user is normally available."
    known_children = {
        "DTSTAMP": (1, 1, None),  # min, max, behavior_registry id
        "DTSTART": (1, 1, None),
        "UID": (1, 1, None),
        "DTEND": (0, 1, None),  # NOTE: One of DtEnd or
        "DURATION": (0, 1, None),  # Duration must appear, but not both
        "CREATED": (0, 1, None),
        "LAST-MODIFIED": (0, 1, None),
        "RECURRENCE-ID": (0, 1, None),
        "RRULE": (0, 1, None),
        "SUMMARY": (0, 1, None),
        "CATEGORIES": (0, None, None),
        "COMMENT": (0, None, None),
        "CONTACT": (0, None, None),
        "EXDATE": (0, None, None),
        "RDATE": (0, None, None),
    }

    @classmethod
    def validate(cls, obj, raise_exception=False, complain_unrecognized=False):
        if ("dtend" in obj.contents) ^ ("duration" in obj.contents):
            return super().validate(obj, raise_exception, complain_unrecognized)
        if raise_exception:
            raise ValidateError("AVAILABLE components must have either DTEND or DURATION properties, but not both")
        return False
Duration

Bases: Behavior

Behavior for Duration ContentLines. Transform to dt.timedelta.

Source code in vobjectx/icalendar.py
class Duration(Behavior):
    """
    Behavior for Duration ContentLines.  Transform to dt.timedelta.
    """

    name = "DURATION"
    has_native = True

    @staticmethod
    def transform_to_native(obj):
        """Turn obj.value into a dt.timedelta."""
        if obj.is_native:
            return obj
        obj.is_native = True

        if obj.value == "":
            return obj

        obj.value = vtypes.Duration(obj.value).value
        return obj

    @staticmethod
    def transform_from_native(obj):
        """
        Replace the dt.timedelta in obj.value with an RFC2445 string.
        """
        if not obj.is_native:
            return obj
        obj.is_native = False
        obj.value = timedelta_to_string(obj.value)
        return obj
Functions
transform_to_native staticmethod
transform_to_native(obj)

Turn obj.value into a dt.timedelta.

Source code in vobjectx/icalendar.py
@staticmethod
def transform_to_native(obj):
    """Turn obj.value into a dt.timedelta."""
    if obj.is_native:
        return obj
    obj.is_native = True

    if obj.value == "":
        return obj

    obj.value = vtypes.Duration(obj.value).value
    return obj
transform_from_native staticmethod
transform_from_native(obj)

Replace the dt.timedelta in obj.value with an RFC2445 string.

Source code in vobjectx/icalendar.py
@staticmethod
def transform_from_native(obj):
    """
    Replace the dt.timedelta in obj.value with an RFC2445 string.
    """
    if not obj.is_native:
        return obj
    obj.is_native = False
    obj.value = timedelta_to_string(obj.value)
    return obj
Trigger

Bases: Behavior

DATE-TIME or DURATION

Source code in vobjectx/icalendar.py
class Trigger(Behavior):
    """
    DATE-TIME or DURATION
    """

    name = "TRIGGER"
    description = "This property specifies when an alarm will trigger."
    has_native = True
    force_utc = True

    @staticmethod
    def transform_to_native(obj):
        """
        Turn obj.value into a timedelta or dt.
        """
        if obj.is_native:
            return obj
        value = getattr(obj, "value_param", "DURATION").upper()
        if hasattr(obj, "value_param"):
            del obj.value_param
        if obj.value == "":
            obj.is_native = True
            return obj
        if value == "DURATION":
            try:
                return Duration.transform_to_native(obj)
            except ParseError:
                logger.warning(
                    "TRIGGER not recognized as DURATION, trying DATE-TIME, because iCal sometimes exports DATE-TIMEs "
                    "without setting VALUE=DATE-TIME"
                )
                try:
                    obj.is_native = False
                    return DateTimeBehavior.transform_to_native(obj)
                except AllException as e:
                    raise ParseError("TRIGGER with no VALUE not recognized as DURATION or as DATE-TIME") from e
        elif value == "DATE-TIME":
            # TRIGGERs with DATE-TIME values must be in UTC, we could validate that fact, for now we take it on faith.
            return DateTimeBehavior.transform_to_native(obj)
        else:
            raise ParseError("VALUE must be DURATION or DATE-TIME")

    @staticmethod
    def transform_from_native(obj):
        if type(obj.value) is dt.datetime:
            obj.value_param = "DATE-TIME"
            return UTCDateTimeBehavior.transform_from_native(obj)
        if type(obj.value) is dt.timedelta:
            return Duration.transform_from_native(obj)

        raise NativeError("Native TRIGGER values must be timedelta or datetime")
Functions
transform_to_native staticmethod
transform_to_native(obj)

Turn obj.value into a timedelta or dt.

Source code in vobjectx/icalendar.py
@staticmethod
def transform_to_native(obj):
    """
    Turn obj.value into a timedelta or dt.
    """
    if obj.is_native:
        return obj
    value = getattr(obj, "value_param", "DURATION").upper()
    if hasattr(obj, "value_param"):
        del obj.value_param
    if obj.value == "":
        obj.is_native = True
        return obj
    if value == "DURATION":
        try:
            return Duration.transform_to_native(obj)
        except ParseError:
            logger.warning(
                "TRIGGER not recognized as DURATION, trying DATE-TIME, because iCal sometimes exports DATE-TIMEs "
                "without setting VALUE=DATE-TIME"
            )
            try:
                obj.is_native = False
                return DateTimeBehavior.transform_to_native(obj)
            except AllException as e:
                raise ParseError("TRIGGER with no VALUE not recognized as DURATION or as DATE-TIME") from e
    elif value == "DATE-TIME":
        # TRIGGERs with DATE-TIME values must be in UTC, we could validate that fact, for now we take it on faith.
        return DateTimeBehavior.transform_to_native(obj)
    else:
        raise ParseError("VALUE must be DURATION or DATE-TIME")
PeriodBehavior

Bases: Behavior

A list of (date-time, timedelta) tuples.

Source code in vobjectx/icalendar.py
class PeriodBehavior(Behavior):
    """A list of (date-time, timedelta) tuples."""

    has_native = True

    @staticmethod
    def transform_to_native(obj):
        """
        Convert comma separated periods into tuples.
        """
        if obj.is_native:
            return obj
        obj.is_native = True
        if obj.value == "":
            obj.value = []
            return obj
        tzinfo = TzidRegistry.get(getattr(obj, "tzid_param", None))
        obj.value = [vtypes.Period(x, tzinfo).value for x in obj.value.split(",")]
        return obj

    @classmethod
    def transform_from_native(cls, obj):
        """
        Convert the list of tuples in obj.value to strings.
        """
        if obj.is_native:
            obj.is_native = False
            transformed = [period_to_string(tup, cls.force_utc) for tup in obj.value]
            if transformed:
                tzid = TimezoneComponent.register_tzinfo(obj.value[-1][0].tzinfo)
                if not cls.force_utc and tzid is not None:
                    obj.tzid_param = tzid

            obj.value = ",".join(transformed)

        return obj
Functions
transform_to_native staticmethod
transform_to_native(obj)

Convert comma separated periods into tuples.

Source code in vobjectx/icalendar.py
@staticmethod
def transform_to_native(obj):
    """
    Convert comma separated periods into tuples.
    """
    if obj.is_native:
        return obj
    obj.is_native = True
    if obj.value == "":
        obj.value = []
        return obj
    tzinfo = TzidRegistry.get(getattr(obj, "tzid_param", None))
    obj.value = [vtypes.Period(x, tzinfo).value for x in obj.value.split(",")]
    return obj
transform_from_native classmethod
transform_from_native(obj)

Convert the list of tuples in obj.value to strings.

Source code in vobjectx/icalendar.py
@classmethod
def transform_from_native(cls, obj):
    """
    Convert the list of tuples in obj.value to strings.
    """
    if obj.is_native:
        obj.is_native = False
        transformed = [period_to_string(tup, cls.force_utc) for tup in obj.value]
        if transformed:
            tzid = TimezoneComponent.register_tzinfo(obj.value[-1][0].tzinfo)
            if not cls.force_utc and tzid is not None:
                obj.tzid_param = tzid

        obj.value = ",".join(transformed)

    return obj
FreeBusy

Bases: PeriodBehavior

Free or busy period of time, must be specified in UTC.

Source code in vobjectx/icalendar.py
class FreeBusy(PeriodBehavior):
    """Free or busy period of time, must be specified in UTC."""

    name = "FREEBUSY"
    force_utc = True
RRule

Bases: Behavior

Dummy behavior to avoid having RRULEs being treated as text lines (and thus having semi-colons inaccurately escaped).

Source code in vobjectx/icalendar.py
class RRule(Behavior):
    """
    Dummy behavior to avoid having RRULEs being treated as text lines
    (and thus having semi-colons inaccurately escaped).
    """
Functions

ics_diff

Compares VTODOs and VEVENTs in two iCalendar sources.

Classes
Functions
delete_extraneous
delete_extraneous(component, ignore_dtstamp=False)

Recursively walk the component's children, deleting extraneous details like X-VOBJ-ORIGINAL-TZID.

Source code in vobjectx/ics_diff.py
def delete_extraneous(component, ignore_dtstamp=False):
    """Recursively walk the component's children, deleting extraneous details like X-VOBJ-ORIGINAL-TZID."""
    for comp in component.components():
        delete_extraneous(comp, ignore_dtstamp)
    for line in component.lines():
        if "X-VOBJ-ORIGINAL-TZID" in line.params:
            del line.params["X-VOBJ-ORIGINAL-TZID"]
    if ignore_dtstamp and hasattr(component, "dtstamp_list"):
        del component.dtstamp_list
diff
diff(left, right)

Take two VCALENDAR components, compare VEVENTs and VTODOs in them, return a list of object pairs containing just UID and the bits that didn't match, using None for objects that weren't present in one version or the other.

When there are multiple ContentLines in one VEVENT, for instance many DESCRIPTION lines, such lines original order is assumed to be meaningful. Order is also preserved when comparing (the unlikely case of) multiple parameters of the same type in a ContentLine

Source code in vobjectx/ics_diff.py
def diff(left, right):
    """
    Take two VCALENDAR components, compare VEVENTs and VTODOs in them, return a list of object pairs containing just
    UID and the bits that didn't match, using None for objects that weren't present in one version or the other.

    When there are multiple ContentLines in one VEVENT, for instance many DESCRIPTION lines, such lines original
    order is assumed to be meaningful.  Order is also preserved when comparing (the unlikely case of) multiple
    parameters of the same type in a ContentLine
    """

    vevents = _process_component_lists(
        sort_by_uid(getattr(left, "vevent_list", [])), sort_by_uid(getattr(right, "vevent_list", []))
    )

    vtodos = _process_component_lists(
        sort_by_uid(getattr(left, "vtodo_list", [])), sort_by_uid(getattr(right, "vtodo_list", []))
    )

    return vevents + vtodos

registry

Classes
BehaviorRegistry
Source code in vobjectx/registry.py
class BehaviorRegistry:
    __registry = {}

    @classmethod
    def keys(cls) -> list[str]:
        return list(cls.__registry.keys())

    @classmethod
    def get(cls, name: str, id_=None) -> BehaviorProtocol | None:
        """
        Return a matching behavior if it exists, or None.

        If id is None, return the default for name.
        """
        name = name.upper()
        if name in cls.__registry:
            named_registry = cls.__registry[name]
            return named_registry.get(id_) or named_registry["default_"]
        return None

    @classmethod
    def register(cls, behavior: BehaviorProtocol, name=None, default=False, id_=None):
        """
        Register the given behavior.

        If default is True (or if this is the first version registered with this
        name), the version will be the default if no id is given.
        """
        if not name:
            name = behavior.name.upper()
        if id_ is None:
            id_ = behavior.version_string
        if name in cls.__registry:
            cls.__registry[name][id_] = behavior
            if default:
                cls.__registry[name]["default_"] = behavior
        else:
            cls.__registry[name] = {id_: behavior, "default_": behavior}
Functions
get classmethod
get(name, id_=None)

Return a matching behavior if it exists, or None.

If id is None, return the default for name.

Source code in vobjectx/registry.py
@classmethod
def get(cls, name: str, id_=None) -> BehaviorProtocol | None:
    """
    Return a matching behavior if it exists, or None.

    If id is None, return the default for name.
    """
    name = name.upper()
    if name in cls.__registry:
        named_registry = cls.__registry[name]
        return named_registry.get(id_) or named_registry["default_"]
    return None
register classmethod
register(behavior, name=None, default=False, id_=None)

Register the given behavior.

If default is True (or if this is the first version registered with this name), the version will be the default if no id is given.

Source code in vobjectx/registry.py
@classmethod
def register(cls, behavior: BehaviorProtocol, name=None, default=False, id_=None):
    """
    Register the given behavior.

    If default is True (or if this is the first version registered with this
    name), the version will be the default if no id is given.
    """
    if not name:
        name = behavior.name.upper()
    if id_ is None:
        id_ = behavior.version_string
    if name in cls.__registry:
        cls.__registry[name][id_] = behavior
        if default:
            cls.__registry[name]["default_"] = behavior
    else:
        cls.__registry[name] = {id_: behavior, "default_": behavior}
TzidRegistry

TZID registry for iCalendar timezone handling.

A registry for mapping timezone identifiers (TZIDs) to tzinfo objects, with automatic fallback to zoneinfo for unknown timezones.

Source code in vobjectx/registry.py
class TzidRegistry:
    """TZID registry for iCalendar timezone handling.

    A registry for mapping timezone identifiers (TZIDs) to tzinfo objects,
    with automatic fallback to zoneinfo for unknown timezones.
    """

    __tzid_map: dict[str, dt.tzinfo | None] = {}

    @classmethod
    def get(cls, tzid) -> dt.tzinfo | None:
        """Return the tzid if it exists, or None."""
        return cls.__tzid_map.get(to_unicode(tzid))

    @classmethod
    def register(cls, tzid, tzinfo: dt.tzinfo, *, exist_ok: bool = False) -> None:
        """Register a new tzid to tzinfo mapping."""

        _key = to_unicode(tzid)
        if _key in cls.__tzid_map:
            if exist_ok:
                return
            raise KeyError(f"Tzid {_key} already registered")

        try:
            tzinfo = zoneinfo.ZoneInfo(tzid)
        except zoneinfo.ZoneInfoNotFoundError as e:
            logger.error(f"Unknown timezone: {tzid} - {e}")

        cls.__tzid_map[_key] = tzinfo

    @classmethod
    def unregister(cls, tzid) -> None:
        """Unregister a tzid from tzinfo mapping."""
        cls.__tzid_map.pop(to_unicode(tzid), None)

    @classmethod
    def reset(cls) -> None:
        """Resets tzinfo mapping to initial state."""
        cls.__tzid_map.clear()
        cls.register("UTC", UTC_TZ)
Functions
get classmethod
get(tzid)

Return the tzid if it exists, or None.

Source code in vobjectx/registry.py
@classmethod
def get(cls, tzid) -> dt.tzinfo | None:
    """Return the tzid if it exists, or None."""
    return cls.__tzid_map.get(to_unicode(tzid))
register classmethod
register(tzid, tzinfo, *, exist_ok=False)

Register a new tzid to tzinfo mapping.

Source code in vobjectx/registry.py
@classmethod
def register(cls, tzid, tzinfo: dt.tzinfo, *, exist_ok: bool = False) -> None:
    """Register a new tzid to tzinfo mapping."""

    _key = to_unicode(tzid)
    if _key in cls.__tzid_map:
        if exist_ok:
            return
        raise KeyError(f"Tzid {_key} already registered")

    try:
        tzinfo = zoneinfo.ZoneInfo(tzid)
    except zoneinfo.ZoneInfoNotFoundError as e:
        logger.error(f"Unknown timezone: {tzid} - {e}")

    cls.__tzid_map[_key] = tzinfo
unregister classmethod
unregister(tzid)

Unregister a tzid from tzinfo mapping.

Source code in vobjectx/registry.py
@classmethod
def unregister(cls, tzid) -> None:
    """Unregister a tzid from tzinfo mapping."""
    cls.__tzid_map.pop(to_unicode(tzid), None)
reset classmethod
reset()

Resets tzinfo mapping to initial state.

Source code in vobjectx/registry.py
@classmethod
def reset(cls) -> None:
    """Resets tzinfo mapping to initial state."""
    cls.__tzid_map.clear()
    cls.register("UTC", UTC_TZ)
Functions
to_unicode
to_unicode(value)

Converts a string argument to a unicode string.

If the argument is already a unicode string, it is returned unchanged. Otherwise it must be a byte string and is decoded as utf8.

Source code in vobjectx/registry.py
def to_unicode(value: str | bytes):
    """Converts a string argument to a unicode string.

    If the argument is already a unicode string, it is returned
    unchanged.  Otherwise it must be a byte string and is decoded as utf8.
    """
    return value.decode() if isinstance(value, bytes) else value

vcard

Definitions and behavior for vCard 3.0

Classes
Name
Source code in vobjectx/vcard.py
class Name:
    def __init__(self, family="", given="", additional="", *, prefix="", suffix=""):
        """
        Each name attribute can be a string or a list of strings.
        """
        self.family = family
        self.given = given
        self.additional = additional
        self.prefix = prefix
        self.suffix = suffix

    def __str__(self):
        eng_order = ("prefix", "given", "additional", "family", "suffix")
        return " ".join(to_string(getattr(self, val)) for val in eng_order)

    def __repr__(self):
        return f"<Name: {self!s}>"

    def __eq__(self, other: Self) -> bool:
        return (
            self.family == other.family
            and self.given == other.given
            and self.additional == other.additional
            and self.prefix == other.prefix
            and self.suffix == other.suffix
        )
Address
Source code in vobjectx/vcard.py
class Address:
    lines = ("box", "extended", "street")
    one_line = ("city", "region", "code")

    def __init__(self, street="", city="", region="", country="", *, code="", box="", extended=""):
        """
        Each name attribute can be a string or a list of strings.
        """
        self.box = box
        self.extended = extended
        self.street = street
        self.city = city
        self.region = region
        self.code = code
        self.country = country

    def __str__(self):
        lines = "\n".join(to_string(getattr(self, val), "\n") for val in self.lines if getattr(self, val))
        one_line = tuple(to_string(getattr(self, val)) for val in self.one_line)
        lines += "\n{0!s}, {1!s} {2!s}".format(*one_line)  # pylint:disable=c0209
        if self.country:
            lines += "\n" + to_string(self.country, "\n")
        return lines

    def __repr__(self):
        return f"<Address: {self!s}>"

    def __eq__(self, other: Self) -> bool:
        return (
            self.box == other.box
            and self.extended == other.extended
            and self.street == other.street
            and self.city == other.city
            and self.region == other.region
            and self.code == other.code
            and self.country == other.country
        )
VCardTextBehavior

Bases: Behavior

Provide backslash escape encoding/decoding for single valued properties.

TextBehavior also deals with base64 encoding if the ENCODING parameter is explicitly set to BASE64.

Source code in vobjectx/vcard.py
class VCardTextBehavior(Behavior):
    """
    Provide backslash escape encoding/decoding for single valued properties.

    TextBehavior also deals with base64 encoding if the ENCODING parameter is
    explicitly set to BASE64.
    """

    allow_group = True
    base64string = "B"

    @classmethod
    def decode(cls, line):
        """
        Remove backslash escaping from line.value_decode line, either to remove
        backslash escaping, or to decode base64 encoding. The content line should
        contain a ENCODING=b for base64 encoding, but Apple Addressbook seems to
        export a singleton parameter of 'BASE64', which does not match the 3.0
        vCard spec. If we encounter that, then we transform the parameter to
        ENCODING=b
        """
        if line.is_encoded:
            if "BASE64" in line.params:
                del line.params["BASE64"]
                line.encoding_param = cls.base64string
            encoding = getattr(line, "encoding_param", None)
            if encoding:
                line.value = byte_decoder(line.value)
            else:
                line.value = string_to_text_values(line.value)[0]
            line.is_encoded = False

    @classmethod
    def encode(cls, line):
        """Backslash escape line.value."""
        if not line.is_encoded:
            encoding = getattr(line, "encoding_param", "")
            if encoding and encoding.upper() == cls.base64string:
                if isinstance(line.value, bytes):
                    line.value = byte_encoder(line.value).decode("utf-8").replace("\n", "")
                else:
                    line.value = byte_encoder(line.value.encode(encoding)).decode("utf-8")
            else:
                line.value = backslash_escape(line.value)
            line.is_encoded = True
Functions
decode classmethod
decode(line)

Remove backslash escaping from line.value_decode line, either to remove backslash escaping, or to decode base64 encoding. The content line should contain a ENCODING=b for base64 encoding, but Apple Addressbook seems to export a singleton parameter of 'BASE64', which does not match the 3.0 vCard spec. If we encounter that, then we transform the parameter to ENCODING=b

Source code in vobjectx/vcard.py
@classmethod
def decode(cls, line):
    """
    Remove backslash escaping from line.value_decode line, either to remove
    backslash escaping, or to decode base64 encoding. The content line should
    contain a ENCODING=b for base64 encoding, but Apple Addressbook seems to
    export a singleton parameter of 'BASE64', which does not match the 3.0
    vCard spec. If we encounter that, then we transform the parameter to
    ENCODING=b
    """
    if line.is_encoded:
        if "BASE64" in line.params:
            del line.params["BASE64"]
            line.encoding_param = cls.base64string
        encoding = getattr(line, "encoding_param", None)
        if encoding:
            line.value = byte_decoder(line.value)
        else:
            line.value = string_to_text_values(line.value)[0]
        line.is_encoded = False
encode classmethod
encode(line)

Backslash escape line.value.

Source code in vobjectx/vcard.py
@classmethod
def encode(cls, line):
    """Backslash escape line.value."""
    if not line.is_encoded:
        encoding = getattr(line, "encoding_param", "")
        if encoding and encoding.upper() == cls.base64string:
            if isinstance(line.value, bytes):
                line.value = byte_encoder(line.value).decode("utf-8").replace("\n", "")
            else:
                line.value = byte_encoder(line.value.encode(encoding)).decode("utf-8")
        else:
            line.value = backslash_escape(line.value)
        line.is_encoded = True
VCard3

Bases: VCardBehavior

vCard 3.0 behavior.

Source code in vobjectx/vcard.py
class VCard3(VCardBehavior):
    """
    vCard 3.0 behavior.
    """

    name = "VCARD"
    description = "vCard 3.0, defined in rfc2426"
    version_string = "3.0"
    is_component = True
    sort_first = ("VERSION", "PRODID", "UID")
    known_children = {
        "N": (0, 1, None),  # min, max, behavior_registry id
        "FN": (1, None, None),
        "VERSION": (1, 1, None),  # required, auto-generated
        "PRODID": (0, 1, None),
        "LABEL": (0, None, None),
        "UID": (0, None, None),
        "ADR": (0, None, None),
        "ORG": (0, None, None),
        "PHOTO": (0, None, None),
        "CATEGORIES": (0, None, None),
        "GEO": (0, None, None),
    }

    @classmethod
    def generate_implicit_parameters(cls, obj):
        """
        Create PRODID, VERSION, and VTIMEZONEs if needed.

        VTIMEZONEs will need to exist whenever TZID parameters exist or when
        datetimes with tzinfo exist.
        """
        if not hasattr(obj, "version"):
            obj.add(ContentLine("VERSION", [], cls.version_string))
Functions
generate_implicit_parameters classmethod
generate_implicit_parameters(obj)

Create PRODID, VERSION, and VTIMEZONEs if needed.

VTIMEZONEs will need to exist whenever TZID parameters exist or when datetimes with tzinfo exist.

Source code in vobjectx/vcard.py
@classmethod
def generate_implicit_parameters(cls, obj):
    """
    Create PRODID, VERSION, and VTIMEZONEs if needed.

    VTIMEZONEs will need to exist whenever TZID parameters exist or when
    datetimes with tzinfo exist.
    """
    if not hasattr(obj, "version"):
        obj.add(ContentLine("VERSION", [], cls.version_string))
Photo

Bases: VCardTextBehavior

Source code in vobjectx/vcard.py
class Photo(VCardTextBehavior):
    name = "Photo"
    description = "Photograph"
    WACKY_APPLE_PHOTO_SERIALIZE = True
    REALLY_LARGE = 1e50

    @classmethod
    def value_repr(cls, line):
        return f" (BINARY PHOTO DATA at 0x{id(line.value)!s}) "

    @classmethod
    def serialize(cls, obj, buf, line_length, validate=True, *args, **kwargs):
        """
        Apple's Address Book is *really* weird with images, it expects
        base64 data to have very specific whitespace.  It seems Address Book
        can handle PHOTO if it's not wrapped, so don't wrap it.
        """
        if cls.WACKY_APPLE_PHOTO_SERIALIZE:
            line_length = cls.REALLY_LARGE
        VCardTextBehavior.serialize(obj, buf, line_length, validate, *args, **kwargs)
Functions
serialize classmethod
serialize(obj, buf, line_length, validate=True, *args, **kwargs)

Apple's Address Book is really weird with images, it expects base64 data to have very specific whitespace. It seems Address Book can handle PHOTO if it's not wrapped, so don't wrap it.

Source code in vobjectx/vcard.py
@classmethod
def serialize(cls, obj, buf, line_length, validate=True, *args, **kwargs):
    """
    Apple's Address Book is *really* weird with images, it expects
    base64 data to have very specific whitespace.  It seems Address Book
    can handle PHOTO if it's not wrapped, so don't wrap it.
    """
    if cls.WACKY_APPLE_PHOTO_SERIALIZE:
        line_length = cls.REALLY_LARGE
    VCardTextBehavior.serialize(obj, buf, line_length, validate, *args, **kwargs)
NameBehavior

Bases: VCardBehavior

A structured name.

Source code in vobjectx/vcard.py
class NameBehavior(VCardBehavior):
    """
    A structured name.
    """

    has_native = True
    field_order = "family", "given", "additional", "prefix", "suffix"

    @classmethod
    def transform_to_native(cls, obj):
        """
        Turn obj.value into a Name.
        """
        if obj.is_native:
            return obj
        obj.is_native = True
        obj.value = Name(**dict(zip(cls.field_order, split_fields(obj.value))))
        return obj

    @classmethod
    def transform_from_native(cls, obj):
        """
        Replace the Name in obj.value with a string.
        """
        obj.is_native = False
        obj.value = serialize_fields(obj.value, cls.field_order)
        return obj
Functions
transform_to_native classmethod
transform_to_native(obj)

Turn obj.value into a Name.

Source code in vobjectx/vcard.py
@classmethod
def transform_to_native(cls, obj):
    """
    Turn obj.value into a Name.
    """
    if obj.is_native:
        return obj
    obj.is_native = True
    obj.value = Name(**dict(zip(cls.field_order, split_fields(obj.value))))
    return obj
transform_from_native classmethod
transform_from_native(obj)

Replace the Name in obj.value with a string.

Source code in vobjectx/vcard.py
@classmethod
def transform_from_native(cls, obj):
    """
    Replace the Name in obj.value with a string.
    """
    obj.is_native = False
    obj.value = serialize_fields(obj.value, cls.field_order)
    return obj
AddressBehavior

Bases: VCardBehavior

A structured address.

Source code in vobjectx/vcard.py
class AddressBehavior(VCardBehavior):
    """
    A structured address.
    """

    has_native = True
    field_order = "box", "extended", "street", "city", "region", "code", "country"

    @classmethod
    def transform_to_native(cls, obj):
        """
        Turn obj.value into an Address.
        """
        if obj.is_native:
            return obj
        obj.is_native = True
        obj.value = Address(**dict(zip(cls.field_order, split_fields(obj.value))))
        return obj

    @classmethod
    def transform_from_native(cls, obj):
        """
        Replace the Address in obj.value with a string.
        """
        obj.is_native = False
        obj.value = serialize_fields(obj.value, cls.field_order)
        return obj
Functions
transform_to_native classmethod
transform_to_native(obj)

Turn obj.value into an Address.

Source code in vobjectx/vcard.py
@classmethod
def transform_to_native(cls, obj):
    """
    Turn obj.value into an Address.
    """
    if obj.is_native:
        return obj
    obj.is_native = True
    obj.value = Address(**dict(zip(cls.field_order, split_fields(obj.value))))
    return obj
transform_from_native classmethod
transform_from_native(obj)

Replace the Address in obj.value with a string.

Source code in vobjectx/vcard.py
@classmethod
def transform_from_native(cls, obj):
    """
    Replace the Address in obj.value with a string.
    """
    obj.is_native = False
    obj.value = serialize_fields(obj.value, cls.field_order)
    return obj
OrgBehavior

Bases: VCardBehavior

A list of organization values and sub-organization values.

Source code in vobjectx/vcard.py
class OrgBehavior(VCardBehavior):
    """
    A list of organization values and sub-organization values.
    """

    has_native = True

    @staticmethod
    def transform_to_native(obj):
        """
        Turn obj.value into a list.
        """
        if obj.is_native:
            return obj
        obj.is_native = True
        obj.value = split_fields(obj.value)
        return obj

    @staticmethod
    def transform_from_native(obj):
        """
        Replace the list in obj.value with a string.
        """
        if not obj.is_native:
            return obj
        obj.is_native = False
        obj.value = serialize_fields(obj.value)
        return obj
Functions
transform_to_native staticmethod
transform_to_native(obj)

Turn obj.value into a list.

Source code in vobjectx/vcard.py
@staticmethod
def transform_to_native(obj):
    """
    Turn obj.value into a list.
    """
    if obj.is_native:
        return obj
    obj.is_native = True
    obj.value = split_fields(obj.value)
    return obj
transform_from_native staticmethod
transform_from_native(obj)

Replace the list in obj.value with a string.

Source code in vobjectx/vcard.py
@staticmethod
def transform_from_native(obj):
    """
    Replace the list in obj.value with a string.
    """
    if not obj.is_native:
        return obj
    obj.is_native = False
    obj.value = serialize_fields(obj.value)
    return obj
Functions
split_fields
split_fields(string)

Return a list of strings or lists from a Name or Address.

Source code in vobjectx/vcard.py
def split_fields(string):
    """
    Return a list of strings or lists from a Name or Address.
    """

    def to_list_or_string(x) -> str | list:
        string_list = string_to_text_values(x)
        return string_list[0] if len(string_list) == 1 else string_list

    return [to_list_or_string(i) for i in string_to_text_values(string, list_separator=";", char_list=";")]
serialize_fields
serialize_fields(obj, order=None)

Turn an object's fields into a ';' and ',' separated string.

If order is None, obj should be a list, backslash escape each field and return a ';' separated string.

Source code in vobjectx/vcard.py
def serialize_fields(obj, order=None):
    """
    Turn an object's fields into a ';' and ',' separated string.

    If order is None, obj should be a list, backslash escape each field and
    return a ';' separated string.
    """
    fields = []
    if order is None:
        fields = [backslash_escape(val) for val in obj]
    else:
        for field in order:
            escaped_value_list = [backslash_escape(val) for val in to_list(getattr(obj, field))]
            fields.append(",".join(escaped_value_list))
    return ";".join(fields)

Module: vobjectx.base

vobjectx.base

vobjectx module for reading vCard and vCalendar files.

Classes

VBase

Base class for ContentLine and Component.

@ivar behavior: The Behavior class associated with this object, which controls validation, transformations, and encoding. @ivar parent_behavior: The object's parent's behavior, or None if no behaviored parent exists. @ivar is_native: Boolean describing whether this component is a Native instance. @ivar group: An optional group prefix, should be used only to indicate sort order in vCards, according to spec.

Current spec: 4.0 (http://tools.ietf.org/html/rfc6350)

Source code in vobjectx/base.py
class VBase:
    """
    Base class for ContentLine and Component.

    @ivar behavior:
        The Behavior class associated with this object, which controls
        validation, transformations, and encoding.
    @ivar parent_behavior:
        The object's parent's behavior, or None if no behaviored parent exists.
    @ivar is_native:
        Boolean describing whether this component is a Native instance.
    @ivar group:
        An optional group prefix, should be used only to indicate sort order in
        vCards, according to spec.

    Current spec: 4.0 (http://tools.ietf.org/html/rfc6350)
    """

    def __init__(self, group=None, *args, **kwds):
        super().__init__(*args, **kwds)
        self.name = None
        self.group = group
        self.behavior = None
        self.parent_behavior = None
        self.is_native = False
        self.is_encoded = False

    def copy(self) -> Self:
        newcopy = type(self)()
        newcopy.upgrade_from(self)
        return newcopy  # type: ignore

    def upgrade_from(self, copyit: Self):
        self.group = copyit.group
        self.behavior = copyit.behavior
        self.parent_behavior = copyit.parent_behavior
        self.is_native = copyit.is_native

    def validate(self, *args, **kwds):
        """
        Call the behavior's validate method, or return True.
        """
        return self.behavior.validate(self, *args, **kwds) if self.behavior else True

    def get_children(self):
        """
        Return an iterable containing the contents of the object.
        """
        return []

    def clear_behavior(self, cascade=True):
        """
        Set behavior to None. Do for all descendants if cascading.
        """
        self.behavior = None
        if cascade:
            self.transform_children_from_native()

    def auto_behavior(self, cascade=False):
        """
        Set behavior if name is in self.parent_behavior.known_children.

        If cascade is True, unset behavior and parent_behavior for all
        descendants, then recalculate behavior and parent_behavior.
        """
        parent_behavior = self.parent_behavior
        if parent_behavior is not None:
            known_child_tup = parent_behavior.known_children.get(self.name)
            if known_child_tup is not None:
                behavior = BehaviorRegistry.get(self.name, known_child_tup[2])
                if behavior is not None:
                    self.set_behavior(behavior, cascade)
                    if isinstance(self, ContentLine) and self.is_encoded:
                        self.behavior.decode(self)
            elif isinstance(self, ContentLine):
                self.behavior = parent_behavior.default_behavior
                if self.is_encoded and self.behavior:
                    self.behavior.decode(self)

    def set_behavior(self, behavior, cascade=True):
        """
        Set behavior. If cascade is True, auto_behavior all descendants.
        """
        self.behavior = behavior
        if cascade:
            for obj in self.get_children():
                obj.parent_behavior = behavior
                obj.auto_behavior(True)

    def transform_to_native(self):
        """
        Transform this object into a custom VBase subclass.

        transform_to_native should always return a representation of this object.
        It may do so by modifying self in place then returning self, or by
        creating a new object.
        """
        if self.is_native or not self.behavior or not self.behavior.has_native:
            return self

        self_orig = self.copy()
        try:
            return self.behavior.transform_to_native(self)
        except ParseError as e:
            e.line_number = getattr(self, "line_number", None)
            raise
        except VObjectError as e:
            e.line_number = getattr(self, "line_number", None)

            # wrap errors in transformation in a ParseError
            msg = "In transform_to_native, unhandled exception on line {0}: {1}: {2}"
            msg = msg.format(e.line_number, sys.exc_info()[0], sys.exc_info()[1])
            msg = f"{msg} ({str(self_orig)})"
            raise ParseError(msg, e.line_number) from e

    def transform_from_native(self):
        """
        Return self transformed into a ContentLine or Component if needed.

        May have side effects.  If it does, transform_from_native and
        transform_to_native MUST have perfectly inverse side effects. Allowing
        such side effects is convenient for objects whose transformations only
        change a few attributes.

        Note that it isn't always possible for transform_from_native to be a
        perfect inverse of transform_to_native, in such cases transform_from_native
        should return a new object, not self after modifications.
        """
        if not self.is_native or not self.behavior or not self.behavior.has_native:
            return self

        try:
            return self.behavior.transform_from_native(self)
        except VObjectError as e:
            # wrap errors in transformation in a NativeError
            line_number = getattr(self, "line_number", None)
            if isinstance(e, NativeError):
                e.line_number = line_number
                raise

            msg = "In transform_from_native, unhandled exception on line {0} {1}: {2}"
            msg = msg.format(line_number, sys.exc_info()[0], sys.exc_info()[1])
            raise NativeError(msg, line_number) from e

    def transform_children_to_native(self):
        """
        Recursively replace children with their native representation.
        """

    def transform_children_from_native(self, clear_behavior=True):
        """
        Recursively transform native children to vanilla representations.
        """

    def serialize(self, buf=None, line_length=75, validate=True, behavior=None, *args, **kwargs):
        """
        Serialize to buf if it exists, otherwise return a string.

        Use self.behavior.serialize if behavior exists.
        """
        if not behavior:
            behavior = self.behavior
        if behavior:
            logger.debug(f"serializing {self.name!s} with behavior {behavior!s}")
            return behavior.serialize(self, buf, line_length, validate, *args, **kwargs)

        logger.debug(f"serializing {self.name!s} without behavior")
        return default_serialize(self, buf, line_length)
Functions
validate
validate(*args, **kwds)

Call the behavior's validate method, or return True.

Source code in vobjectx/base.py
def validate(self, *args, **kwds):
    """
    Call the behavior's validate method, or return True.
    """
    return self.behavior.validate(self, *args, **kwds) if self.behavior else True
get_children
get_children()

Return an iterable containing the contents of the object.

Source code in vobjectx/base.py
def get_children(self):
    """
    Return an iterable containing the contents of the object.
    """
    return []
clear_behavior
clear_behavior(cascade=True)

Set behavior to None. Do for all descendants if cascading.

Source code in vobjectx/base.py
def clear_behavior(self, cascade=True):
    """
    Set behavior to None. Do for all descendants if cascading.
    """
    self.behavior = None
    if cascade:
        self.transform_children_from_native()
auto_behavior
auto_behavior(cascade=False)

Set behavior if name is in self.parent_behavior.known_children.

If cascade is True, unset behavior and parent_behavior for all descendants, then recalculate behavior and parent_behavior.

Source code in vobjectx/base.py
def auto_behavior(self, cascade=False):
    """
    Set behavior if name is in self.parent_behavior.known_children.

    If cascade is True, unset behavior and parent_behavior for all
    descendants, then recalculate behavior and parent_behavior.
    """
    parent_behavior = self.parent_behavior
    if parent_behavior is not None:
        known_child_tup = parent_behavior.known_children.get(self.name)
        if known_child_tup is not None:
            behavior = BehaviorRegistry.get(self.name, known_child_tup[2])
            if behavior is not None:
                self.set_behavior(behavior, cascade)
                if isinstance(self, ContentLine) and self.is_encoded:
                    self.behavior.decode(self)
        elif isinstance(self, ContentLine):
            self.behavior = parent_behavior.default_behavior
            if self.is_encoded and self.behavior:
                self.behavior.decode(self)
set_behavior
set_behavior(behavior, cascade=True)

Set behavior. If cascade is True, auto_behavior all descendants.

Source code in vobjectx/base.py
def set_behavior(self, behavior, cascade=True):
    """
    Set behavior. If cascade is True, auto_behavior all descendants.
    """
    self.behavior = behavior
    if cascade:
        for obj in self.get_children():
            obj.parent_behavior = behavior
            obj.auto_behavior(True)
transform_to_native
transform_to_native()

Transform this object into a custom VBase subclass.

transform_to_native should always return a representation of this object. It may do so by modifying self in place then returning self, or by creating a new object.

Source code in vobjectx/base.py
def transform_to_native(self):
    """
    Transform this object into a custom VBase subclass.

    transform_to_native should always return a representation of this object.
    It may do so by modifying self in place then returning self, or by
    creating a new object.
    """
    if self.is_native or not self.behavior or not self.behavior.has_native:
        return self

    self_orig = self.copy()
    try:
        return self.behavior.transform_to_native(self)
    except ParseError as e:
        e.line_number = getattr(self, "line_number", None)
        raise
    except VObjectError as e:
        e.line_number = getattr(self, "line_number", None)

        # wrap errors in transformation in a ParseError
        msg = "In transform_to_native, unhandled exception on line {0}: {1}: {2}"
        msg = msg.format(e.line_number, sys.exc_info()[0], sys.exc_info()[1])
        msg = f"{msg} ({str(self_orig)})"
        raise ParseError(msg, e.line_number) from e
transform_from_native
transform_from_native()

Return self transformed into a ContentLine or Component if needed.

May have side effects. If it does, transform_from_native and transform_to_native MUST have perfectly inverse side effects. Allowing such side effects is convenient for objects whose transformations only change a few attributes.

Note that it isn't always possible for transform_from_native to be a perfect inverse of transform_to_native, in such cases transform_from_native should return a new object, not self after modifications.

Source code in vobjectx/base.py
def transform_from_native(self):
    """
    Return self transformed into a ContentLine or Component if needed.

    May have side effects.  If it does, transform_from_native and
    transform_to_native MUST have perfectly inverse side effects. Allowing
    such side effects is convenient for objects whose transformations only
    change a few attributes.

    Note that it isn't always possible for transform_from_native to be a
    perfect inverse of transform_to_native, in such cases transform_from_native
    should return a new object, not self after modifications.
    """
    if not self.is_native or not self.behavior or not self.behavior.has_native:
        return self

    try:
        return self.behavior.transform_from_native(self)
    except VObjectError as e:
        # wrap errors in transformation in a NativeError
        line_number = getattr(self, "line_number", None)
        if isinstance(e, NativeError):
            e.line_number = line_number
            raise

        msg = "In transform_from_native, unhandled exception on line {0} {1}: {2}"
        msg = msg.format(line_number, sys.exc_info()[0], sys.exc_info()[1])
        raise NativeError(msg, line_number) from e
transform_children_to_native
transform_children_to_native()

Recursively replace children with their native representation.

Source code in vobjectx/base.py
def transform_children_to_native(self):
    """
    Recursively replace children with their native representation.
    """
transform_children_from_native
transform_children_from_native(clear_behavior=True)

Recursively transform native children to vanilla representations.

Source code in vobjectx/base.py
def transform_children_from_native(self, clear_behavior=True):
    """
    Recursively transform native children to vanilla representations.
    """
serialize
serialize(buf=None, line_length=75, validate=True, behavior=None, *args, **kwargs)

Serialize to buf if it exists, otherwise return a string.

Use self.behavior.serialize if behavior exists.

Source code in vobjectx/base.py
def serialize(self, buf=None, line_length=75, validate=True, behavior=None, *args, **kwargs):
    """
    Serialize to buf if it exists, otherwise return a string.

    Use self.behavior.serialize if behavior exists.
    """
    if not behavior:
        behavior = self.behavior
    if behavior:
        logger.debug(f"serializing {self.name!s} with behavior {behavior!s}")
        return behavior.serialize(self, buf, line_length, validate, *args, **kwargs)

    logger.debug(f"serializing {self.name!s} without behavior")
    return default_serialize(self, buf, line_length)

ContentLine

Bases: VBase

Holds one content line for formats like vCard and vCalendar.

For example::

@ivar name: The uppercased name of the contentline. @ivar params: A dictionary of parameters and associated lists of values. Singleton params (e.g., WORK, CELL in vCard 2.1) are stored with an empty list as the value. @ivar value: The value of the contentline. @ivar encoded: A boolean describing whether the data in the content line is encoded. Generally, text read from a serialized vCard or vCalendar should be considered encoded. Data added programmatically should not be encoded. @ivar line_number: An optional line number associated with the contentline.

Source code in vobjectx/base.py
class ContentLine(VBase):
    """
    Holds one content line for formats like vCard and vCalendar.

    For example::
      <SUMMARY{u'param1' : [u'val1'], u'param2' : [u'val2']}Bastille Day Party>

    @ivar name:
        The uppercased name of the contentline.
    @ivar params:
        A dictionary of parameters and associated lists of values.
        Singleton params (e.g., WORK, CELL in vCard 2.1) are stored with an
        empty list as the value.
    @ivar value:
        The value of the contentline.
    @ivar encoded:
        A boolean describing whether the data in the content line is encoded.
        Generally, text read from a serialized vCard or vCalendar should be
        considered encoded.  Data added programmatically should not be encoded.
    @ivar line_number:
        An optional line number associated with the contentline.
    """

    # pylint: disable=r0902,r0917
    def __init__(
        self,
        name: str,
        params: list,
        value: str,
        group=None,
        is_encoded: bool = False,
        is_native: bool = False,
        line_number: int = None,
        *args,
        **kwds,
    ):
        """
        Take output from parse_line, convert params list to dictionary.

        Group is used as a positional argument to match parse_line's return
        """
        super().__init__(group, *args, **kwds)

        self.name = name.upper()
        self.is_encoded = is_encoded
        self.params = ContentDict()
        self.is_native = is_native
        self.line_number = line_number
        self.value: Any = value  # depends on Behavior

        def update_table(x):
            # All params stored uniformly: singleton params get empty list
            paramlist = self.params.setdefault(x[0], [])
            if len(x) > 1:
                paramlist.extend(x[1:])

        list(map(update_table, params))

        qp = False
        if "ENCODING" in self.params and "QUOTED-PRINTABLE" in self.params["ENCODING"]:
            qp = True
            self.params["ENCODING"].remove("QUOTED-PRINTABLE")
            if not self.params["ENCODING"]:
                del self.params["ENCODING"]
        if "QUOTED-PRINTABLE" in self.params:
            qp = True
            del self.params["QUOTED-PRINTABLE"]
        if qp:
            if "ENCODING" in self.params:
                _encoding = self.params["ENCODING"]
            elif "CHARSET" in self.params:
                _encoding = self.params["CHARSET"][0]
            else:
                _encoding = "utf-8"

            _value = byte_decoder(self.value, "quoted-printable")
            try:
                self.value = _value.decode(_encoding)
            except UnicodeDecodeError:
                self.value = _value.decode("latin-1")

    def copy(self) -> Self:
        newcopy = ContentLine("", [], "")
        newcopy.update_from(self)
        return newcopy  # type: ignore

    def update_from(self, copyit: Self):
        super().upgrade_from(copyit)
        self.name = copyit.name
        self.value = copy.copy(copyit.value)
        self.is_encoded = copyit.is_encoded

        for k, v in copyit.params.items():
            self.params[k] = copy.copy(v)
        self.line_number = copyit.line_number

    def __eq__(self, other):
        return (self.name == other.name) and (self.params == other.params) and (self.value == other.value)

    def __getattr__(self, name):
        """
        Make params accessible via self.foo_param or self.foo_paramlist.

        Underscores, legal in python variable names, are converted to dashes,
        which are legal in IANA tokens.
        """
        try:
            if name.endswith("_param"):
                return self.params[name][0]
            if name.endswith("_paramlist"):
                return self.params[name]
            raise AttributeError(name)
        except KeyError as e:
            raise AttributeError(name) from e

    def __setattr__(self, name, value):
        """
        Make params accessible via self.foo_param or self.foo_paramlist.

        Underscores, legal in python variable names, are converted to dashes,
        which are legal in IANA tokens.
        """
        if name.endswith("_param"):
            if isinstance(value, list):
                self.params[name] = value
            else:
                self.params[name] = [value]
        elif name.endswith("_paramlist"):
            if isinstance(value, list):
                self.params[name] = value
            else:
                raise VObjectError("Parameter list set to a non-list")
        else:
            prop = getattr(self.__class__, name, None)
            if isinstance(prop, property):
                prop.fset(self, value)
            else:
                object.__setattr__(self, name, value)

    def __delattr__(self, name):
        try:
            if name.endswith("_param") or name.endswith("_paramlist"):
                del self.params[name]
            else:
                object.__delattr__(self, name)
        except KeyError as e:
            raise AttributeError(name) from e

    def value_repr(self):
        """Transform the representation of the value according to the behavior, if any."""
        return self.behavior.value_repr(self) if self.behavior else self.value

    @property
    def display_params(self):
        return {k: v for k, v in self.params.items() if v}

    def __repr__(self):
        try:
            value_repr = self.value_repr()
        except UnicodeEncodeError:
            value_repr = self.value_repr().encode("utf-8")

        # Filter out singleton params (empty lists) for display
        return f"<{self.name}{self.display_params}{value_repr}>"

    def __unicode__(self):
        # Filter out singleton params (empty lists) for display
        return f"<{self.name}{self.display_params}{self.value_repr()}>"

    def pretty_print(self, level=0, tabwidth=3):
        pre = " " * level * tabwidth
        print(pre, f"{self.name}:", self.value_repr())
        if self.params:
            print(pre, "params for ", f"{self.name}:")
            for k, v in self.params.items():
                print(pre + " " * tabwidth, k, v)

    def default_serialize(self, outbuf, line_length):
        started_encoded = self.is_encoded
        if self.behavior and not started_encoded:
            self.behavior.encode(self)

        s = get_buffer()

        if self.group is not None:
            s.write(f"{self.group}.")
        s.write(self.name.upper())
        keys = sorted(self.params.keys())
        for key in keys:
            paramstr = ",".join(dquote_escape(p) for p in self.params[key])
            try:
                if paramstr:
                    s.write(f";{key}={paramstr}")
                else:
                    s.write(f";{key}")
            except (UnicodeDecodeError, UnicodeEncodeError):
                s.write(f";{key}={paramstr.encode('utf-8')}")
        try:
            s.write(f":{self.value}")
        except (UnicodeDecodeError, UnicodeEncodeError):
            s.write(f":{self.value.encode('utf-8')}")
        if self.behavior and not started_encoded:
            self.behavior.decode(self)
        fold_one_line(outbuf, s.getvalue(), line_length)

    # pylint: disable=w0613
    @classmethod
    def line_validate(cls, line, raise_exception=True, complain_unrecognized=False):
        """Examine a line's parameters and values, return True if valid."""
        return True
Functions
value_repr
value_repr()

Transform the representation of the value according to the behavior, if any.

Source code in vobjectx/base.py
def value_repr(self):
    """Transform the representation of the value according to the behavior, if any."""
    return self.behavior.value_repr(self) if self.behavior else self.value
line_validate classmethod
line_validate(line, raise_exception=True, complain_unrecognized=False)

Examine a line's parameters and values, return True if valid.

Source code in vobjectx/base.py
@classmethod
def line_validate(cls, line, raise_exception=True, complain_unrecognized=False):
    """Examine a line's parameters and values, return True if valid."""
    return True

Component

Bases: VBase

A complex property that can contain multiple ContentLines.

For our purposes, a component must start with a BEGIN:xxxx line and end with END:xxxx, or have a PROFILE:xxx line if a top-level component.

@ivar contents: A dictionary of lists of Component or ContentLine instances. The keys are the lowercased names of child ContentLines or Components. Note that BEGIN and END ContentLines are not included in contents. @ivar name: Uppercase string used to represent this Component, i.e VCARD if the serialized object starts with BEGIN:VCARD. @ivar use_begin: A boolean flag determining whether BEGIN: and END: lines should be serialized.

Source code in vobjectx/base.py
class Component(VBase):
    """
    A complex property that can contain multiple ContentLines.

    For our purposes, a component must start with a BEGIN:xxxx line and end with
    END:xxxx, or have a PROFILE:xxx line if a top-level component.

    @ivar contents:
        A dictionary of lists of Component or ContentLine instances. The keys
        are the lowercased names of child ContentLines or Components.
        Note that BEGIN and END ContentLines are not included in contents.
    @ivar name:
        Uppercase string used to represent this Component, i.e VCARD if the
        serialized object starts with BEGIN:VCARD.
    @ivar use_begin:
        A boolean flag determining whether BEGIN: and END: lines should
        be serialized.
    """

    def __init__(self, name="", *args, **kwds):
        super().__init__(*args, **kwds)
        self.contents = ContentDict()
        self.name = name.upper()
        self.use_begin = bool(name)
        self.auto_behavior()

    def upgrade_from(self, copyit: Self):
        super().upgrade_from(copyit)

        # deep copy of contents
        self.contents = ContentDict()
        for key, lvalue in copyit.contents.items():
            newvalue = []
            for value in lvalue:
                newitem = value.copy()
                newvalue.append(newitem)
            self.contents[key] = newvalue

        self.name = copyit.name
        self.use_begin = copyit.use_begin

    def set_profile(self, name):
        """
        Assign a PROFILE to this unnamed component.

        Used by vCard, not by vCalendar.
        """
        if self.name or self.use_begin:
            if self.name == name:
                return
            raise VObjectError("This component already has a PROFILE or uses BEGIN.")
        self.name = name.upper()

    def __getattr__(self, name):
        """
        For convenience, make self.contents directly accessible.

        Underscores, legal in python variable names, are converted to dashes,
        which are legal in IANA tokens.
        """
        # if the object is being re-created by pickle, self.contents may not
        # be set, don't get into an infinite loop over the issue
        if name == "contents":
            return object.__getattribute__(self, name)
        try:
            if name.endswith("_list"):
                return self.contents[name]
            return self.contents[name][0]
        except KeyError as e:
            raise AttributeError(name) from e

    def __setattr__(self, name, value):
        """
        For convenience, make self.contents directly accessible.

        Underscores, legal in python variable names, are converted to dashes,
        which are legal in IANA tokens.
        """
        prop = getattr(self.__class__, name, None)
        if isinstance(prop, property):
            prop.fset(self, value)
        else:
            object.__setattr__(self, name, value)

    def get_child_value(self, child_name, default=None, child_number=0):
        """
        Return a child's value (the first, by default), or None.
        """
        child = self.contents.get(child_name)
        return default if child is None else child[child_number].value

    def add(self, obj_or_name, group=None):
        """
        Add obj_or_name to contents, set behavior if it can be inferred.

        If obj_or_name is a string, create an empty component or line based on
        behavior. If no behavior is found for the object, add a ContentLine.

        group is an optional prefix to the name of the object (see RFC 2425).
        """
        if isinstance(obj_or_name, VBase):
            obj = obj_or_name
            if self.behavior:
                obj.parent_behavior = self.behavior
                obj.auto_behavior(True)
        else:
            name = obj_or_name.upper()
            try:
                _id = self.behavior.known_children[name][2]
                behavior = BehaviorRegistry.get(name, _id)
                if behavior.is_component:
                    obj = Component(name)
                else:
                    obj = ContentLine(name, [], "", group)
                obj.parent_behavior = self.behavior
                obj.behavior = behavior
                obj = obj.transform_to_native()
            except (KeyError, AttributeError):
                obj = ContentLine(obj_or_name, [], "", group)
            if obj.behavior is None and self.behavior is not None and isinstance(obj, ContentLine):
                obj.behavior = self.behavior.default_behavior

        self.contents.setdefault(obj.name, []).append(obj)
        return obj

    def remove(self, obj):
        """
        Remove obj from contents.
        """
        named = self.contents.get(obj.name.lower())
        if named:
            with contextlib.suppress(ValueError):
                named.remove(obj)
                if not named:
                    del self.contents[obj.name.lower()]

    def get_children(self):
        """
        Return an iterable of all children.
        """
        for obj_list in self.contents.values():
            yield from obj_list

    def components(self):
        """
        Return an iterable of all Component children.
        """
        return (i for i in self.get_children() if isinstance(i, Component))

    def lines(self):
        """
        Return an iterable of all ContentLine children.
        """
        return (i for i in self.get_children() if isinstance(i, ContentLine))

    def sort_child_keys(self):
        if self.behavior:
            first = [s for s in self.behavior.sort_first if s in self.contents]
        else:
            first = []
        return first + sorted(k for k in self.contents.keys() if k not in first)

    def get_sorted_children(self):
        return [obj for k in self.sort_child_keys() for obj in self.contents[k]]

    def set_behavior_from_version_line(self, version_line):
        """
        Set behavior if one matches name, version_line.value.
        """
        _id = None if version_line is None else version_line.value
        v = BehaviorRegistry.get(self.name, id_=_id)
        if v:
            self.set_behavior(v)

    def transform_children_to_native(self):
        """
        Recursively replace children with their native representation.

        Sort to get dependency order right, like vtimezone before vevent.
        """
        for child_array in (self.contents[k] for k in self.sort_child_keys()):
            for child in child_array:
                child = child.transform_to_native()
                child.transform_children_to_native()

    def transform_children_from_native(self, clear_behavior=True):
        """
        Recursively transform native children to vanilla representations.
        """
        for child_array in self.contents.values():
            for child in child_array:
                child = child.transform_from_native()
                child.transform_children_from_native(clear_behavior)
                if clear_behavior:
                    child.behavior = None
                    child.parent_behavior = None

    def __repr__(self):
        return f"<{self.name or '*unnamed*'}| {self.get_sorted_children()}>"

    def pretty_print(self, level=0, tabwidth=3):
        pre = " " * level * tabwidth
        print(pre, self.name)
        if isinstance(self, Component):
            for line in self.get_children():
                line.pretty_print(level + 1, tabwidth)

    def default_serialize(self, output_buffer, line_length):
        group_string = "" if self.group is None else f"{self.group}."
        if self.use_begin:
            fold_one_line(output_buffer, f"{group_string}BEGIN:{self.name}", line_length)
        for child in self.get_sorted_children():
            # validate is recursive, we only need to validate once
            child.serialize(output_buffer, line_length, validate=False)
        if self.use_begin:
            fold_one_line(output_buffer, f"{group_string}END:{self.name}", line_length)
Functions
set_profile
set_profile(name)

Assign a PROFILE to this unnamed component.

Used by vCard, not by vCalendar.

Source code in vobjectx/base.py
def set_profile(self, name):
    """
    Assign a PROFILE to this unnamed component.

    Used by vCard, not by vCalendar.
    """
    if self.name or self.use_begin:
        if self.name == name:
            return
        raise VObjectError("This component already has a PROFILE or uses BEGIN.")
    self.name = name.upper()
get_child_value
get_child_value(child_name, default=None, child_number=0)

Return a child's value (the first, by default), or None.

Source code in vobjectx/base.py
def get_child_value(self, child_name, default=None, child_number=0):
    """
    Return a child's value (the first, by default), or None.
    """
    child = self.contents.get(child_name)
    return default if child is None else child[child_number].value
add
add(obj_or_name, group=None)

Add obj_or_name to contents, set behavior if it can be inferred.

If obj_or_name is a string, create an empty component or line based on behavior. If no behavior is found for the object, add a ContentLine.

group is an optional prefix to the name of the object (see RFC 2425).

Source code in vobjectx/base.py
def add(self, obj_or_name, group=None):
    """
    Add obj_or_name to contents, set behavior if it can be inferred.

    If obj_or_name is a string, create an empty component or line based on
    behavior. If no behavior is found for the object, add a ContentLine.

    group is an optional prefix to the name of the object (see RFC 2425).
    """
    if isinstance(obj_or_name, VBase):
        obj = obj_or_name
        if self.behavior:
            obj.parent_behavior = self.behavior
            obj.auto_behavior(True)
    else:
        name = obj_or_name.upper()
        try:
            _id = self.behavior.known_children[name][2]
            behavior = BehaviorRegistry.get(name, _id)
            if behavior.is_component:
                obj = Component(name)
            else:
                obj = ContentLine(name, [], "", group)
            obj.parent_behavior = self.behavior
            obj.behavior = behavior
            obj = obj.transform_to_native()
        except (KeyError, AttributeError):
            obj = ContentLine(obj_or_name, [], "", group)
        if obj.behavior is None and self.behavior is not None and isinstance(obj, ContentLine):
            obj.behavior = self.behavior.default_behavior

    self.contents.setdefault(obj.name, []).append(obj)
    return obj
remove
remove(obj)

Remove obj from contents.

Source code in vobjectx/base.py
def remove(self, obj):
    """
    Remove obj from contents.
    """
    named = self.contents.get(obj.name.lower())
    if named:
        with contextlib.suppress(ValueError):
            named.remove(obj)
            if not named:
                del self.contents[obj.name.lower()]
get_children
get_children()

Return an iterable of all children.

Source code in vobjectx/base.py
def get_children(self):
    """
    Return an iterable of all children.
    """
    for obj_list in self.contents.values():
        yield from obj_list
components
components()

Return an iterable of all Component children.

Source code in vobjectx/base.py
def components(self):
    """
    Return an iterable of all Component children.
    """
    return (i for i in self.get_children() if isinstance(i, Component))
lines
lines()

Return an iterable of all ContentLine children.

Source code in vobjectx/base.py
def lines(self):
    """
    Return an iterable of all ContentLine children.
    """
    return (i for i in self.get_children() if isinstance(i, ContentLine))
set_behavior_from_version_line
set_behavior_from_version_line(version_line)

Set behavior if one matches name, version_line.value.

Source code in vobjectx/base.py
def set_behavior_from_version_line(self, version_line):
    """
    Set behavior if one matches name, version_line.value.
    """
    _id = None if version_line is None else version_line.value
    v = BehaviorRegistry.get(self.name, id_=_id)
    if v:
        self.set_behavior(v)
transform_children_to_native
transform_children_to_native()

Recursively replace children with their native representation.

Sort to get dependency order right, like vtimezone before vevent.

Source code in vobjectx/base.py
def transform_children_to_native(self):
    """
    Recursively replace children with their native representation.

    Sort to get dependency order right, like vtimezone before vevent.
    """
    for child_array in (self.contents[k] for k in self.sort_child_keys()):
        for child in child_array:
            child = child.transform_to_native()
            child.transform_children_to_native()
transform_children_from_native
transform_children_from_native(clear_behavior=True)

Recursively transform native children to vanilla representations.

Source code in vobjectx/base.py
def transform_children_from_native(self, clear_behavior=True):
    """
    Recursively transform native children to vanilla representations.
    """
    for child_array in self.contents.values():
        for child in child_array:
            child = child.transform_from_native()
            child.transform_children_from_native(clear_behavior)
            if clear_behavior:
                child.behavior = None
                child.parent_behavior = None

Functions

parse_params

parse_params(string)

Parse parameters

Source code in vobjectx/base.py
def parse_params(string):
    """
    Parse parameters
    """
    _all = params_re.findall(string)
    all_parameters = []
    for param in _all:
        name, values_string = param
        param_list = [name]
        for pair in param_values_re.findall(values_string):
            # pair looks like ('', value) or (value, '')
            param_list.append(pair[0] or pair[1])

        all_parameters.append(param_list)
    return all_parameters

parse_line

parse_line(line, line_number=None)

Parse line

Source code in vobjectx/base.py
def parse_line(line, line_number=None):
    """
    Parse line
    """
    match = line_re.match(line)
    if match is None:
        raise ParseError(f"Failed to parse line: {line!s}", line_number)
    # Underscores are replaced with dash to work around Lotus Notes
    return (
        match.group("name").replace("_", "-"),
        parse_params(match.group("params")),
        match.group("value"),
        match.group("group"),
    )

get_logical_lines

get_logical_lines(fp, allow_qp=True)

Iterate through a stream, yielding one logical line at a time.

Because many applications still use vCard 2.1, we have to deal with the quoted-printable encoding for long lines, as well as the vCard 3.0 and vCalendar line folding technique, a whitespace character at the start of the line.

Quoted-printable data will be decoded in the Behavior decoding phase.

Source code in vobjectx/base.py
def get_logical_lines(fp: TextIO, allow_qp: bool = True) -> Iterator:
    """
    Iterate through a stream, yielding one logical line at a time.

    Because many applications still use vCard 2.1, we have to deal with the
    quoted-printable encoding for long lines, as well as the vCard 3.0 and
    vCalendar line folding technique, a whitespace character at the start
    of the line.

    Quoted-printable data will be decoded in the Behavior decoding phase.
    """

    def get_value(lines: list[str]):
        return "".join(lines)

    if not allow_qp:
        val = fp.read(-1)

        line_number = 1
        for match in logical_lines_re.finditer(val):
            line, n = wrap_re.subn("", match.group())
            if line:
                yield line, line_number
            line_number += n
        return

    quoted_printable = False
    logical_line: list[str] = []
    line_start_number = 0

    for n, line in enumerate(fp, start=1):
        line = line.rstrip(Char.CRLF)

        if line.rstrip() == "":
            if logical_line:
                yield get_value(logical_line), line_start_number
            line_start_number = n
            logical_line = []
            quoted_printable = False
            continue

        if quoted_printable and allow_qp:
            logical_line.append("\n")
            quoted_printable = False
        elif line[0] in Char.SPACEORTAB:
            line = line[1:]
        elif logical_line:
            yield get_value(logical_line), line_start_number
            line_start_number = n
            logical_line = []
        else:
            logical_line = []
        logical_line.append(line)

        # vCard 2.1 allows parameters to be encoded without a parameter name
        # False positives are unlikely, but possible.
        if line[-1] == "=" and "quoted-printable" in get_value(logical_line).lower():
            quoted_printable = True

    if logical_line:
        yield get_value(logical_line), line_start_number

dquote_escape

dquote_escape(param)

Return param, or "param" if ',' or ';' or ':' is in param.

Source code in vobjectx/base.py
def dquote_escape(param: str) -> str:
    """Return param, or "param" if ',' or ';' or ':' is in param."""

    if '"' in param:
        raise VObjectError("Double quotes aren't allowed in parameter values.")
    for char in ",;:":  # sourcery skip # temp
        if char in param:
            return f'"{param}"'
    return param

fold_one_line

fold_one_line(outbuf, input_, line_length=75)

Folding line procedure that ensures multi-byte utf-8 sequences are not broken across lines

Source code in vobjectx/base.py
def fold_one_line(outbuf: TextIO, input_: str, line_length=75):
    """
    Folding line procedure that ensures multi-byte utf-8 sequences are not broken across lines
    """
    chunks = split_by_size(input_, byte_size=line_length)
    for chunk in chunks:
        outbuf.write(chunk)
    outbuf.write(Char.CRLF)

default_serialize

default_serialize(obj, buf, line_length)

Encode and fold obj and its children, write to buf or return a string.

Source code in vobjectx/base.py
def default_serialize(obj, buf, line_length):
    """
    Encode and fold obj and its children, write to buf or return a string.
    """
    outbuf = buf or get_buffer()
    if isinstance(obj, (Component, ContentLine)):
        obj.default_serialize(outbuf, line_length)
    return buf or outbuf.getvalue()

read_components

read_components(stream_or_string, validate=False, transform=True, ignore_unreadable=False, allow_qp=False)

Generate one Component at a time from a stream.

Source code in vobjectx/base.py
def read_components(
    stream_or_string, validate=False, transform=True, ignore_unreadable=False, allow_qp=False
) -> Iterator[Component]:
    """Generate one Component at a time from a stream."""

    def raise_parse_error(msg):
        raise ParseError(msg, n, inputs=stream_or_string)

    def _handle_end():
        if not stack:
            raise raise_parse_error(f"Attempted to end the {vline.value} component but it was never opened")
        if vline.value.upper() != stack.top_name():
            raise raise_parse_error(f"{stack.top_name()} component wasn't closed")

        # START matches END
        if len(stack) == 1:
            component: Component = stack.pop()
            component.set_behavior_from_version_line(version_line)

            if validate:
                component.validate(raise_exception=True)
            if transform:
                component.transform_children_to_native()
            return component  # EXIT POINT

        stack.modify_top(stack.pop())
        return None

    stream = get_buffer(stream_or_string)
    stack = ComponentStack()
    n, version_line = 0, None

    for line, n in get_logical_lines(stream, allow_qp):
        # 1. Get vline
        try:
            vline = text_line_to_content_line(line, n)
        except VObjectError as e:
            if ignore_unreadable:
                logger.error(f"Skipped line: {e.line_number or '?'}, message: {str(e)}")
                continue
            raise e

        # 2. Parse vline
        if vline.name == "VERSION":
            version_line = vline
            stack.modify_top(vline)
        elif vline.name == "BEGIN":
            stack.push(Component(vline.value, group=vline.group))
        elif vline.name == "PROFILE":
            if not stack.top():
                stack.push(Component())
            stack.top().set_profile(vline.value)
        elif vline.name == "END":
            _component = _handle_end()
            if _component:
                yield _component
        else:
            stack.modify_top(vline)  # not a START or END line

    if stack.top():
        if stack.top_name() is None:
            logger.warning("Top level component was never named")
        elif stack.top().use_begin:
            raise raise_parse_error(f"Component {(stack.top_name())!s} was never closed")
        yield stack.pop()

read_one

read_one(stream, validate=False, transform=True, ignore_unreadable=False, allow_qp=False)

Return the first component from stream.

Source code in vobjectx/base.py
def read_one(stream, validate=False, transform=True, ignore_unreadable=False, allow_qp=False):
    """
    Return the first component from stream.
    """
    return next(read_components(stream, validate, transform, ignore_unreadable, allow_qp))

Module: vobjectx.behavior

vobjectx.behavior

Classes

Behavior

Behavior (validation, encoding, and transformations) for vobjects.

Abstract class to describe vobjectx options, requirements and encodings.

Behaviors are used for root components like VCALENDAR, for subcomponents like VEVENT, and for individual lines in components.

Behavior subclasses are not meant to be instantiated, all methods should be classmethods.

@cvar name: The uppercase name of the object described by the class, or a generic name if the class defines behavior for many objects. @cvar description: A brief excerpt from the RFC explaining the function of the component or line. @cvar version_string: The string associated with the component, for instance, 2.0 if there's a line like VERSION:2.0, an empty string otherwise. @cvar known_children: A dictionary with uppercased component/property names as keys and a tuple (min, max, id) as value, where id is the id used by L{register_behavior}, min and max are the limits on how many of this child must occur. None is used to denote no max or no id. @cvar quoted_printable: A boolean describing whether the object should be encoded and decoded using quoted printable line folding and character escaping. @cvar default_behavior: Behavior to apply to ContentLine children when no behavior is found. @cvar has_native: A boolean describing whether the object can be transformed into a more Pythonic object. @cvar is_component: A boolean, True if the object should be a Component. @cvar sort_first: The lower-case list of children which should come first when sorting. @cvar allow_group: Whether or not vCard style group prefixes are allowed.

Source code in vobjectx/behavior.py
class Behavior:
    """
    Behavior (validation, encoding, and transformations) for vobjects.

    Abstract class to describe vobjectx options, requirements and encodings.

    Behaviors are used for root components like VCALENDAR, for subcomponents
    like VEVENT, and for individual lines in components.

    Behavior subclasses are not meant to be instantiated, all methods should
    be classmethods.

    @cvar name:
        The uppercase name of the object described by the class, or a generic
        name if the class defines behavior for many objects.
    @cvar description:
        A brief excerpt from the RFC explaining the function of the component or
        line.
    @cvar version_string:
        The string associated with the component, for instance, 2.0 if there's a
        line like VERSION:2.0, an empty string otherwise.
    @cvar known_children:
        A dictionary with uppercased component/property names as keys and a
        tuple (min, max, id) as value, where id is the id used by
        L{register_behavior}, min and max are the limits on how many of this child
        must occur.  None is used to denote no max or no id.
    @cvar quoted_printable:
        A boolean describing whether the object should be encoded and decoded
        using quoted printable line folding and character escaping.
    @cvar default_behavior:
        Behavior to apply to ContentLine children when no behavior is found.
    @cvar has_native:
        A boolean describing whether the object can be transformed into a more
        Pythonic object.
    @cvar is_component:
        A boolean, True if the object should be a Component.
    @cvar sort_first:
        The lower-case list of children which should come first when sorting.
    @cvar allow_group:
        Whether or not vCard style group prefixes are allowed.
    """

    name = ""
    description = ""
    version_string = ""
    known_children = {}
    quoted_printable = False
    default_behavior = None
    has_native = False
    is_component = False
    allow_group = False
    force_utc = False
    sort_first = []

    def __init__(self):
        raise VObjectError("Behavior subclasses are not meant to be instantiated")

    @classmethod
    def validate(cls, obj, raise_exception=False, complain_unrecognized=False):
        """Check if the object satisfies this behavior's requirements.

        @param obj:
            The L{ContentLine<base.ContentLine>} or
            L{Component<base.Component>} to be validated.
        @param raise_exception:
            If True, raise a L{base.ValidateError} on validation failure.
            Otherwise return a boolean.
        @param complain_unrecognized:
            If True, fail to validate if an uncrecognized parameter or child is
            found.  Otherwise log the lack of recognition.

        """
        if not cls.allow_group and obj.group is not None:
            raise VObjectError(f"{obj} has a group, but this object doesn't support groups")

        if isinstance(obj, ContentLine):
            return obj.line_validate(obj, raise_exception, complain_unrecognized)

        if isinstance(obj, Component):
            count = {}
            for child in obj.get_children():
                if not child.validate(raise_exception, complain_unrecognized):
                    return False
                name = child.name.upper()
                count[name] = count.get(name, 0) + 1
            for key, val in cls.known_children.items():
                if count.get(key, 0) < val[0]:
                    if raise_exception:
                        m = "{0} components must contain at least {1} {2}"
                        raise ValidateError(m.format(cls.name, val[0], key))
                    return False
                if val[1] and count.get(key, 0) > val[1]:
                    if raise_exception:
                        m = "{0} components cannot contain more than {1} {2}"
                        raise ValidateError(m.format(cls.name, val[1], key))
                    return False
            return True
        raise VObjectError(f"{obj} is not a Component or Contentline")

    @classmethod
    def decode(cls, line):
        line.is_encoded = False

    @classmethod
    def encode(cls, line):
        line.is_encoded = True

    @staticmethod
    def transform_to_native(obj):
        """
        Turn a ContentLine or Component into a Python-native representation.

        If appropriate, turn dates or datetime strings into Python objects.
        Components containing VTIMEZONEs turn into VtimezoneComponents.

        """
        return obj

    @staticmethod
    def transform_from_native(obj):
        """
        Inverse of transform_to_native.
        """
        raise NativeError("No transform_from_native defined")

    @staticmethod
    def generate_implicit_parameters(obj):
        """Generate any required information that don't yet exist."""

    @classmethod
    def serialize(cls, obj, buf, line_length, validate=True, *args, **kwargs):  # pylint:disable=unused-argument
        """
        Set implicit parameters, do encoding, return unicode string.

        If validate is True, raise VObjectError if the line doesn't validate
        after implicit parameters are generated.

        Default is to call base.default_serialize.

        """

        cls.generate_implicit_parameters(obj)
        if validate:
            cls.validate(obj, raise_exception=True)

        if obj.is_native:
            transformed = obj.transform_from_native()
            undo_transform = True
        else:
            transformed = obj
            undo_transform = False

        out = default_serialize(transformed, buf, line_length)
        if undo_transform:
            obj.transform_to_native()
        return out

    @classmethod
    def value_repr(cls, line):
        """return the representation of the given content line value"""
        return line.value
Functions
validate classmethod
validate(obj, raise_exception=False, complain_unrecognized=False)

Check if the object satisfies this behavior's requirements.

@param obj: The L{ContentLine} or L{Component} to be validated. @param raise_exception: If True, raise a L{base.ValidateError} on validation failure. Otherwise return a boolean. @param complain_unrecognized: If True, fail to validate if an uncrecognized parameter or child is found. Otherwise log the lack of recognition.

Source code in vobjectx/behavior.py
@classmethod
def validate(cls, obj, raise_exception=False, complain_unrecognized=False):
    """Check if the object satisfies this behavior's requirements.

    @param obj:
        The L{ContentLine<base.ContentLine>} or
        L{Component<base.Component>} to be validated.
    @param raise_exception:
        If True, raise a L{base.ValidateError} on validation failure.
        Otherwise return a boolean.
    @param complain_unrecognized:
        If True, fail to validate if an uncrecognized parameter or child is
        found.  Otherwise log the lack of recognition.

    """
    if not cls.allow_group and obj.group is not None:
        raise VObjectError(f"{obj} has a group, but this object doesn't support groups")

    if isinstance(obj, ContentLine):
        return obj.line_validate(obj, raise_exception, complain_unrecognized)

    if isinstance(obj, Component):
        count = {}
        for child in obj.get_children():
            if not child.validate(raise_exception, complain_unrecognized):
                return False
            name = child.name.upper()
            count[name] = count.get(name, 0) + 1
        for key, val in cls.known_children.items():
            if count.get(key, 0) < val[0]:
                if raise_exception:
                    m = "{0} components must contain at least {1} {2}"
                    raise ValidateError(m.format(cls.name, val[0], key))
                return False
            if val[1] and count.get(key, 0) > val[1]:
                if raise_exception:
                    m = "{0} components cannot contain more than {1} {2}"
                    raise ValidateError(m.format(cls.name, val[1], key))
                return False
        return True
    raise VObjectError(f"{obj} is not a Component or Contentline")
transform_to_native staticmethod
transform_to_native(obj)

Turn a ContentLine or Component into a Python-native representation.

If appropriate, turn dates or datetime strings into Python objects. Components containing VTIMEZONEs turn into VtimezoneComponents.

Source code in vobjectx/behavior.py
@staticmethod
def transform_to_native(obj):
    """
    Turn a ContentLine or Component into a Python-native representation.

    If appropriate, turn dates or datetime strings into Python objects.
    Components containing VTIMEZONEs turn into VtimezoneComponents.

    """
    return obj
transform_from_native staticmethod
transform_from_native(obj)

Inverse of transform_to_native.

Source code in vobjectx/behavior.py
@staticmethod
def transform_from_native(obj):
    """
    Inverse of transform_to_native.
    """
    raise NativeError("No transform_from_native defined")
generate_implicit_parameters staticmethod
generate_implicit_parameters(obj)

Generate any required information that don't yet exist.

Source code in vobjectx/behavior.py
@staticmethod
def generate_implicit_parameters(obj):
    """Generate any required information that don't yet exist."""
serialize classmethod
serialize(obj, buf, line_length, validate=True, *args, **kwargs)

Set implicit parameters, do encoding, return unicode string.

If validate is True, raise VObjectError if the line doesn't validate after implicit parameters are generated.

Default is to call base.default_serialize.

Source code in vobjectx/behavior.py
@classmethod
def serialize(cls, obj, buf, line_length, validate=True, *args, **kwargs):  # pylint:disable=unused-argument
    """
    Set implicit parameters, do encoding, return unicode string.

    If validate is True, raise VObjectError if the line doesn't validate
    after implicit parameters are generated.

    Default is to call base.default_serialize.

    """

    cls.generate_implicit_parameters(obj)
    if validate:
        cls.validate(obj, raise_exception=True)

    if obj.is_native:
        transformed = obj.transform_from_native()
        undo_transform = True
    else:
        transformed = obj
        undo_transform = False

    out = default_serialize(transformed, buf, line_length)
    if undo_transform:
        obj.transform_to_native()
    return out
value_repr classmethod
value_repr(line)

return the representation of the given content line value

Source code in vobjectx/behavior.py
@classmethod
def value_repr(cls, line):
    """return the representation of the given content line value"""
    return line.value

Functions

new_from_behavior

new_from_behavior(name, id_=None)

Given a name, return a behaviored ContentLine or Component.

Source code in vobjectx/behavior.py
def new_from_behavior(name, id_=None):
    """
    Given a name, return a behaviored ContentLine or Component.
    """
    name = name.upper()
    behavior = BehaviorRegistry.get(name, id_)
    if behavior is None:
        raise VObjectError(f"No behavior found named {name!s}")
    obj = Component(name) if behavior.is_component else ContentLine(name, [], "")
    obj.behavior = behavior
    obj.is_native = False
    return obj

Module: vobjectx.custom_class

vobjectx.custom_class

Module: vobjectx.exceptions

vobjectx.exceptions

Functions

warn_if_true

warn_if_true(cond=True, raise_error=True)

Warns if unexpected code excecuttion is encountered.

Source code in vobjectx/exceptions.py
def warn_if_true(cond: bool = True, raise_error: bool = True):
    """Warns if unexpected code excecuttion is encountered."""
    if not cond:
        return

    warnings.warn("Unexpected code execution", UserWarning, stacklevel=2)
    if raise_error:
        raise UnusedBranchError()

Module: vobjectx.patterns

vobjectx.patterns

Module: vobjectx.registry

vobjectx.registry

Classes

BehaviorRegistry

Source code in vobjectx/registry.py
class BehaviorRegistry:
    __registry = {}

    @classmethod
    def keys(cls) -> list[str]:
        return list(cls.__registry.keys())

    @classmethod
    def get(cls, name: str, id_=None) -> BehaviorProtocol | None:
        """
        Return a matching behavior if it exists, or None.

        If id is None, return the default for name.
        """
        name = name.upper()
        if name in cls.__registry:
            named_registry = cls.__registry[name]
            return named_registry.get(id_) or named_registry["default_"]
        return None

    @classmethod
    def register(cls, behavior: BehaviorProtocol, name=None, default=False, id_=None):
        """
        Register the given behavior.

        If default is True (or if this is the first version registered with this
        name), the version will be the default if no id is given.
        """
        if not name:
            name = behavior.name.upper()
        if id_ is None:
            id_ = behavior.version_string
        if name in cls.__registry:
            cls.__registry[name][id_] = behavior
            if default:
                cls.__registry[name]["default_"] = behavior
        else:
            cls.__registry[name] = {id_: behavior, "default_": behavior}
Functions
get classmethod
get(name, id_=None)

Return a matching behavior if it exists, or None.

If id is None, return the default for name.

Source code in vobjectx/registry.py
@classmethod
def get(cls, name: str, id_=None) -> BehaviorProtocol | None:
    """
    Return a matching behavior if it exists, or None.

    If id is None, return the default for name.
    """
    name = name.upper()
    if name in cls.__registry:
        named_registry = cls.__registry[name]
        return named_registry.get(id_) or named_registry["default_"]
    return None
register classmethod
register(behavior, name=None, default=False, id_=None)

Register the given behavior.

If default is True (or if this is the first version registered with this name), the version will be the default if no id is given.

Source code in vobjectx/registry.py
@classmethod
def register(cls, behavior: BehaviorProtocol, name=None, default=False, id_=None):
    """
    Register the given behavior.

    If default is True (or if this is the first version registered with this
    name), the version will be the default if no id is given.
    """
    if not name:
        name = behavior.name.upper()
    if id_ is None:
        id_ = behavior.version_string
    if name in cls.__registry:
        cls.__registry[name][id_] = behavior
        if default:
            cls.__registry[name]["default_"] = behavior
    else:
        cls.__registry[name] = {id_: behavior, "default_": behavior}

TzidRegistry

TZID registry for iCalendar timezone handling.

A registry for mapping timezone identifiers (TZIDs) to tzinfo objects, with automatic fallback to zoneinfo for unknown timezones.

Source code in vobjectx/registry.py
class TzidRegistry:
    """TZID registry for iCalendar timezone handling.

    A registry for mapping timezone identifiers (TZIDs) to tzinfo objects,
    with automatic fallback to zoneinfo for unknown timezones.
    """

    __tzid_map: dict[str, dt.tzinfo | None] = {}

    @classmethod
    def get(cls, tzid) -> dt.tzinfo | None:
        """Return the tzid if it exists, or None."""
        return cls.__tzid_map.get(to_unicode(tzid))

    @classmethod
    def register(cls, tzid, tzinfo: dt.tzinfo, *, exist_ok: bool = False) -> None:
        """Register a new tzid to tzinfo mapping."""

        _key = to_unicode(tzid)
        if _key in cls.__tzid_map:
            if exist_ok:
                return
            raise KeyError(f"Tzid {_key} already registered")

        try:
            tzinfo = zoneinfo.ZoneInfo(tzid)
        except zoneinfo.ZoneInfoNotFoundError as e:
            logger.error(f"Unknown timezone: {tzid} - {e}")

        cls.__tzid_map[_key] = tzinfo

    @classmethod
    def unregister(cls, tzid) -> None:
        """Unregister a tzid from tzinfo mapping."""
        cls.__tzid_map.pop(to_unicode(tzid), None)

    @classmethod
    def reset(cls) -> None:
        """Resets tzinfo mapping to initial state."""
        cls.__tzid_map.clear()
        cls.register("UTC", UTC_TZ)
Functions
get classmethod
get(tzid)

Return the tzid if it exists, or None.

Source code in vobjectx/registry.py
@classmethod
def get(cls, tzid) -> dt.tzinfo | None:
    """Return the tzid if it exists, or None."""
    return cls.__tzid_map.get(to_unicode(tzid))
register classmethod
register(tzid, tzinfo, *, exist_ok=False)

Register a new tzid to tzinfo mapping.

Source code in vobjectx/registry.py
@classmethod
def register(cls, tzid, tzinfo: dt.tzinfo, *, exist_ok: bool = False) -> None:
    """Register a new tzid to tzinfo mapping."""

    _key = to_unicode(tzid)
    if _key in cls.__tzid_map:
        if exist_ok:
            return
        raise KeyError(f"Tzid {_key} already registered")

    try:
        tzinfo = zoneinfo.ZoneInfo(tzid)
    except zoneinfo.ZoneInfoNotFoundError as e:
        logger.error(f"Unknown timezone: {tzid} - {e}")

    cls.__tzid_map[_key] = tzinfo
unregister classmethod
unregister(tzid)

Unregister a tzid from tzinfo mapping.

Source code in vobjectx/registry.py
@classmethod
def unregister(cls, tzid) -> None:
    """Unregister a tzid from tzinfo mapping."""
    cls.__tzid_map.pop(to_unicode(tzid), None)
reset classmethod
reset()

Resets tzinfo mapping to initial state.

Source code in vobjectx/registry.py
@classmethod
def reset(cls) -> None:
    """Resets tzinfo mapping to initial state."""
    cls.__tzid_map.clear()
    cls.register("UTC", UTC_TZ)

Functions

to_unicode

to_unicode(value)

Converts a string argument to a unicode string.

If the argument is already a unicode string, it is returned unchanged. Otherwise it must be a byte string and is decoded as utf8.

Source code in vobjectx/registry.py
def to_unicode(value: str | bytes):
    """Converts a string argument to a unicode string.

    If the argument is already a unicode string, it is returned
    unchanged.  Otherwise it must be a byte string and is decoded as utf8.
    """
    return value.decode() if isinstance(value, bytes) else value

Module: vobjectx.hcalendar

vobjectx.hcalendar

A microformat for serializing iCalendar data

(http://microformats.org/wiki/hcalendar)

Here is a sample event in an iCalendar:

BEGIN:VCALENDAR PRODID:-//XYZproduct//EN VERSION:2.0 BEGIN:VEVENT URL:http://www.web2con.com/ DTSTART:20051005 DTEND:20051008 SUMMARY:Web 2.0 Conference LOCATION:Argent Hotel\, San Francisco\, CA END:VEVENT END:VCALENDAR

and an equivalent event in hCalendar format with various elements optimized appropriately.

Web 2.0 Conference: October 5- 7, at the Argent Hotel, San Francisco, CA

Classes

HCalendar

Bases: VCalendar2_0

Source code in vobjectx/hcalendar.py
class HCalendar(VCalendar2_0):
    name = "HCALENDAR"
    indent_width = 3

    @classmethod
    def serialize(cls, obj, buf=None, line_length=None, validate=True, *args, **kwargs):
        """
        Serialize iCalendar to HTML using the hCalendar microformat (http://microformats.org/wiki/hcalendar)
        """

        outbuf = buf or get_buffer()

        def get_xml(event_child: str, value, *, tag="span", prefix="") -> str:
            if value:
                return f'{prefix}<{tag} class="{event_child}">{value}</{tag}>:'
            return ""

        # not serializing optional vcalendar wrapper

        vevents = obj.vevent_list

        for event in vevents:
            _event = Event(event)
            _event_data = [get_xml("summary", _event.summary, tag="span")]  # SUMMARY

            # DTSTART
            if _event.dtstart:
                # TODO: Handle non-datetime formats? Spec says we should handle when dtstart isn't included

                _event_data.append(
                    f'<abbr class="dtstart", title="{_event.machine_date(_event.dtstart)}"'
                    f">{_event.human_date(_event.dtstart)}</abbr>"
                )

                # DTEND
                if not _event.dtend and _event.duration:
                    _event.dtend = _event.duration + _event.dtstart
                # TODO: If lacking dtend & duration?

                if _event.dtend:
                    human = _event.dtend
                    # TODO: Human readable part could be smarter, excluding repeated data
                    if type(_event.dtend) is date:
                        human = _event.dtend - timedelta(days=1)

                    _event_data.append(
                        f'- <abbr class="dtend", title="{_event.machine_date(_event.dtend)}"'
                        f">{_event.human_date(human)}</abbr>"
                    )

            # LOCATION
            _event_data.append(get_xml("location", _event.location, tag="span", prefix="at "))
            _event_data.append(get_xml("description", _event.description, tag="div"))

            _event_str = Character.CRLF.join(_event_data)
            if _event.url:
                _event_str = f'<a class="url" href="{_event.url}">{_event_str}</a>'
            _event_str = f'<span class="vevent">{_event_str}</span>'

            outbuf.write(pretty_xml(_event_str, indent=cls.indent_width))

        return outbuf.getvalue()
Functions
serialize classmethod
serialize(obj, buf=None, line_length=None, validate=True, *args, **kwargs)

Serialize iCalendar to HTML using the hCalendar microformat (http://microformats.org/wiki/hcalendar)

Source code in vobjectx/hcalendar.py
@classmethod
def serialize(cls, obj, buf=None, line_length=None, validate=True, *args, **kwargs):
    """
    Serialize iCalendar to HTML using the hCalendar microformat (http://microformats.org/wiki/hcalendar)
    """

    outbuf = buf or get_buffer()

    def get_xml(event_child: str, value, *, tag="span", prefix="") -> str:
        if value:
            return f'{prefix}<{tag} class="{event_child}">{value}</{tag}>:'
        return ""

    # not serializing optional vcalendar wrapper

    vevents = obj.vevent_list

    for event in vevents:
        _event = Event(event)
        _event_data = [get_xml("summary", _event.summary, tag="span")]  # SUMMARY

        # DTSTART
        if _event.dtstart:
            # TODO: Handle non-datetime formats? Spec says we should handle when dtstart isn't included

            _event_data.append(
                f'<abbr class="dtstart", title="{_event.machine_date(_event.dtstart)}"'
                f">{_event.human_date(_event.dtstart)}</abbr>"
            )

            # DTEND
            if not _event.dtend and _event.duration:
                _event.dtend = _event.duration + _event.dtstart
            # TODO: If lacking dtend & duration?

            if _event.dtend:
                human = _event.dtend
                # TODO: Human readable part could be smarter, excluding repeated data
                if type(_event.dtend) is date:
                    human = _event.dtend - timedelta(days=1)

                _event_data.append(
                    f'- <abbr class="dtend", title="{_event.machine_date(_event.dtend)}"'
                    f">{_event.human_date(human)}</abbr>"
                )

        # LOCATION
        _event_data.append(get_xml("location", _event.location, tag="span", prefix="at "))
        _event_data.append(get_xml("description", _event.description, tag="div"))

        _event_str = Character.CRLF.join(_event_data)
        if _event.url:
            _event_str = f'<a class="url" href="{_event.url}">{_event_str}</a>'
        _event_str = f'<span class="vevent">{_event_str}</span>'

        outbuf.write(pretty_xml(_event_str, indent=cls.indent_width))

    return outbuf.getvalue()

Module: vobjectx.icalendar

vobjectx.icalendar

Definitions and behavior for iCalendar, also known as vCalendar 2.0

Classes

TimezoneComponent

Bases: Component

A VTIMEZONE object.

VTIMEZONEs are parsed by tz.tzical, the resulting dt.tzinfo subclass is stored in self.tzinfo, self.tzid stores the TZID associated with this timezone.

@ivar name: The uppercased name of the object, in this case always 'VTIMEZONE'. @ivar tzinfo: A dt.tzinfo subclass representing this timezone. @ivar tzid: The string used to refer to this timezone.

Source code in vobjectx/icalendar.py
class TimezoneComponent(Component):
    """
    A VTIMEZONE object.

    VTIMEZONEs are parsed by tz.tzical, the resulting dt.tzinfo subclass is stored in self.tzinfo, self.tzid stores
    the TZID associated with this timezone.

    @ivar name:
        The uppercased name of the object, in this case always 'VTIMEZONE'.
    @ivar tzinfo:
        A dt.tzinfo subclass representing this timezone.
    @ivar tzid:
        The string used to refer to this timezone.
    """

    def __init__(self, tzinfo=None, *args, **kwds):
        """
        Accept an existing Component or a tzinfo class.
        """
        super().__init__(*args, **kwds)
        self.is_native = True
        # hack to make sure a behavior is assigned
        if self.behavior is None:
            self.behavior = VTimezone
        if tzinfo is not None:
            self.tzinfo = tzinfo
        if not hasattr(self, "name") or self.name == "":
            self.name = "VTIMEZONE"
            self.use_begin = True

    @classmethod
    def register_tzinfo(cls, tzinfo):
        """
        Register tzinfo if it's not already registered, return its tzid.
        """
        tzid = cls.pick_tzid(tzinfo)
        if tzid:
            TzidRegistry.register(tzid, tzinfo, exist_ok=True)
        return tzid

    @property
    def tzinfo(self):
        # workaround for dateutil failing to parse some experimental properties
        good_lines = ("rdate", "rrule", "dtstart", "tzname", "tzoffsetfrom", "tzoffsetto", "tzid")
        # serialize encodes as utf-8, cStringIO will leave utf-8 alone
        buffer = get_buffer()
        # allow empty VTIMEZONEs
        if len(self.contents) == 0:
            return None

        def custom_serialize(obj):
            if isinstance(obj, Component):
                fold_one_line(buffer, f"BEGIN:{obj.name}")
                for child in obj.lines():
                    if child.name.lower() in good_lines:
                        child.serialize(buffer, 75, validate=False)
                for comp in obj.components():
                    custom_serialize(comp)
                fold_one_line(buffer, f"END:{obj.name}")

        custom_serialize(self)
        buffer.seek(0)  # tzical wants to read a stream
        return tz.tzical(buffer).get()

    @tzinfo.setter
    def tzinfo(self, tzinfo, start=2000, end=2030):
        # pylint: disable=r0914
        """
        Create appropriate objects in self to represent tzinfo.

        Collapse DST transitions to rrules as much as possible.

        Assumptions:
        - DST <-> Standard transitions occur on the hour
        - never within a month of one another
        - twice or fewer times a year
        - never in the month of December
        - DST always moves offset exactly one hour later
        - tzinfo classes dst method always treats times that could be in either offset as being in the later regime
        """

        def _handle_else():
            two_hours = dt.timedelta(hours=2)
            # Use fold=1 to get the state after the transition
            # For overlaps, fold=1 is the second instance (Standard time)
            # For gaps, fold=1 is the instance after the gap (Daylight time)

            old_offset = tzinfo.utcoffset((transition - two_hours).replace(fold=0))
            name = tzinfo.tzname(transition.replace(fold=1))
            offset = tzinfo.utcoffset(transition.replace(fold=1))

            rule = {
                "end": None,  # None, or an integer year
                "start": transition,  # the datetime of transition
                "month": transition.month,
                "weekday": transition.weekday(),
                "hour": transition.hour,
                "name": name,
                "plus": int((transition.day - 1) / 7 + 1),  # nth week of the month
                "minus": from_last_week_(transition),  # nth from last week
                "offset": offset,
                "offsetfrom": old_offset,
            }

            if oldrule is None:
                working[transition_to] = rule
            else:
                plus_match = rule["plus"] == oldrule["plus"]
                minus_match = rule["minus"] == oldrule["minus"]
                truth = plus_match or minus_match
                truth = truth and all(rule[key] == oldrule[key] for key in ("month", "weekday", "hour", "offset"))
                if truth:
                    # the old rule is still true, limit to plus or minus
                    oldrule["plus"] = oldrule["plus"] if plus_match else None
                    oldrule["minus"] = oldrule["minus"] if minus_match else None
                else:
                    # the new rule did not match the old
                    oldrule["end"] = year - 1
                    completed[transition_to].append(oldrule)
                    working[transition_to] = rule

        # lists of dictionaries defining rules which are no longer in effect
        completed = {"daylight": [], "standard": []}

        # dictionary defining rules which are currently in effect
        working: dict[str, dict | None] = {"daylight": None, "standard": None}

        # rule may be based on nth week of the month or the nth from the last
        for year in range(start, end + 1):
            newyear = dt.datetime(year, 1, 1)
            for transition_to in TRANSITIONS:
                transition = get_transition(transition_to, year, tzinfo)
                oldrule = working[transition_to]

                if transition == newyear:
                    # transition_to is in effect for the whole year
                    rule = {
                        "end": None,
                        "start": newyear,
                        "month": 1,
                        "weekday": None,
                        "hour": None,
                        "plus": None,
                        "minus": None,
                        "name": tzinfo.tzname(newyear),
                        "offset": tzinfo.utcoffset(newyear),
                        "offsetfrom": tzinfo.utcoffset(newyear),
                    }
                    if oldrule is None:
                        # transition_to was not yet in effect
                        working[transition_to] = rule
                    elif oldrule["offset"] != tzinfo.utcoffset(newyear):
                        # transition_to was already in effect.
                        # old rule was different, it shouldn't continue
                        oldrule["end"] = year - 1
                        completed[transition_to].append(oldrule)
                        working[transition_to] = rule
                elif transition is None:
                    # transition_to is not in effect
                    if oldrule is not None:
                        # transition_to used to be in effect
                        oldrule["end"] = year - 1
                        completed[transition_to].append(oldrule)
                        working[transition_to] = None
                else:
                    # an offset transition was found
                    _handle_else()

        for transition_to, rule in working.items():
            if rule is not None:
                completed[transition_to].append(rule)

        self.contents.tzid = []
        self.contents.daylight = []
        self.contents.standard = []

        self.add("tzid").value = self.pick_tzid(tzinfo, True)

        # old = None # unused?
        for transition_to, rules in completed.items():
            for rule in rules:
                comp = self.add(transition_to)
                dtstart = comp.add("dtstart")
                dtstart.value = rule["start"]
                if rule["name"] is not None:
                    comp.add("tzname").value = rule["name"]
                line = comp.add("tzoffsetto")
                line.value = delta_to_offset(rule["offset"])
                line = comp.add("tzoffsetfrom")
                line.value = delta_to_offset(rule["offsetfrom"])

                num = rule["plus"] or -1 * (rule["minus"] or 0)
                day_string = f"BYDAY={num}{WEEKDAYS[rule['weekday']]}" if num else ""

                end_string = ""
                if rule["end"] is not None:
                    # all year offset, with no rule
                    end_date = dt.datetime(rule["end"], 1, 1)
                    if rule["hour"] is not None:
                        du_rule = rrule.rrule(
                            rrule.YEARLY,
                            bymonth=rule["month"],
                            byweekday=rrule.weekday(rule["weekday"], num),
                            dtstart=dt.datetime(rule["end"], 1, 1, rule["hour"]),
                        )
                        end_date = du_rule[0]
                    end_date = end_date.replace(tzinfo=UTC_TZ) - rule["offsetfrom"]
                    end_string = f"UNTIL={datetime_to_string(end_date)}"

                new_rule = ";".join(["FREQ=YEARLY", day_string, f"BYMONTH={rule['month']}", end_string])
                comp.add("rrule").value = new_rule.strip(";")

    @staticmethod
    def pick_tzid(tzinfo, allow_utc=False):
        """
        Given a tzinfo class, use known APIs to determine TZID, or use tzname.
        """
        if tzinfo is None or (not allow_utc and tzinfo_eq(tzinfo, UTC_TZ)):
            # If tzinfo is UTC, we don't need a TZID
            return None

        for attr in ("key", "tzid", "zone", "_tzid"):
            tzid_ = getattr(tzinfo, attr, None)
            if tzid_:
                return tzid_

        # return tzname for standard (non-DST) time
        not_dst = dt.timedelta(0)
        for month in range(1, 13):
            _dt = dt.datetime(2000, month, 1)
            if tzinfo.dst(_dt) == not_dst:
                return tzinfo.tzname(_dt)

        # there was no standard time in 2000!
        raise VObjectError(f"Unable to guess TZID for tzinfo {tzinfo!s}")

    def __repr__(self):
        return f'<VTIMEZONE | {getattr(self, "tzid", "No TZID")}>'

    def pretty_print(self, level=0, tabwidth=3):
        pre = " " * level * tabwidth
        print(pre, self.name)
        print(pre, "TZID:", self.tzid)
        print("")
Functions
register_tzinfo classmethod
register_tzinfo(tzinfo)

Register tzinfo if it's not already registered, return its tzid.

Source code in vobjectx/icalendar.py
@classmethod
def register_tzinfo(cls, tzinfo):
    """
    Register tzinfo if it's not already registered, return its tzid.
    """
    tzid = cls.pick_tzid(tzinfo)
    if tzid:
        TzidRegistry.register(tzid, tzinfo, exist_ok=True)
    return tzid
pick_tzid staticmethod
pick_tzid(tzinfo, allow_utc=False)

Given a tzinfo class, use known APIs to determine TZID, or use tzname.

Source code in vobjectx/icalendar.py
@staticmethod
def pick_tzid(tzinfo, allow_utc=False):
    """
    Given a tzinfo class, use known APIs to determine TZID, or use tzname.
    """
    if tzinfo is None or (not allow_utc and tzinfo_eq(tzinfo, UTC_TZ)):
        # If tzinfo is UTC, we don't need a TZID
        return None

    for attr in ("key", "tzid", "zone", "_tzid"):
        tzid_ = getattr(tzinfo, attr, None)
        if tzid_:
            return tzid_

    # return tzname for standard (non-DST) time
    not_dst = dt.timedelta(0)
    for month in range(1, 13):
        _dt = dt.datetime(2000, month, 1)
        if tzinfo.dst(_dt) == not_dst:
            return tzinfo.tzname(_dt)

    # there was no standard time in 2000!
    raise VObjectError(f"Unable to guess TZID for tzinfo {tzinfo!s}")

RecurringComponent

Bases: Component

A vCalendar component like VEVENT or VTODO which may recur.

Any recurring component can have one or multiple RRULE, RDATE, EXRULE, or EXDATE lines, and one or zero DTSTART lines. It can also have a variety of children that don't have any recurrence information.

In the example below, note that dtstart is included in the rruleset. This is not the default behavior for dateutil's rrule implementation unless dtstart would already have been a member of the recurrence rule, and as a result, COUNT is wrong. This can be worked around when getting rruleset by adjusting count down by one if an rrule has a count and dtstart isn't in its result set, but by default, the rruleset property doesn't do this work around, to access it getrruleset must be called with addRDate set True.

@property rruleset: A U{rrulesethttps://moin.conectiva.com.br/DateUtil}.

Source code in vobjectx/icalendar.py
class RecurringComponent(Component):
    """
    A vCalendar component like VEVENT or VTODO which may recur.

    Any recurring component can have one or multiple RRULE, RDATE, EXRULE, or EXDATE lines, and one or zero DTSTART
    lines. It can also have a variety of children that don't have any recurrence information.

    In the example below, note that dtstart is included in the rruleset. This is not the default behavior for
    dateutil's rrule implementation unless dtstart would already have been a member of the recurrence rule,
    and as a result, COUNT is wrong. This can be worked around when getting rruleset by adjusting count down by one
    if an rrule has a count and dtstart isn't in its result set, but by default, the rruleset property doesn't do
    this work around, to access it getrruleset must be called with addRDate set True.

    @property rruleset:
        A U{rruleset<https://moin.conectiva.com.br/DateUtil>}.
    """

    def __init__(self, *args, **kwds):
        super().__init__(*args, **kwds)
        self.is_native = True

    @property
    def rruleset(self):
        return self.getrruleset()

    def getrruleset(self, add_rdate=False):
        """
        Get an rruleset created from self.

        If addRDate is True, add an RDATE for dtstart if it's not included in an RRULE or RDATE, and count is
        decremented if it exists.

        Note that for rules which don't match DTSTART, DTSTART may not appear in list(rruleset), although it should.
        By default, an RDATE is not created in these cases, and count isn't updated, so dateutil may list a spurious
        occurrence.
        """

        def _handle_rulenames(add_func_):
            # a Ruby iCalendar library escapes semi-colons in rrules, so also remove any backslashes
            value = line.value.replace("\\", "")
            # If dtstart has no time zone, `until` shouldn't get one, either:
            ignoretz = not isinstance(dtstart, dt.datetime) or dtstart.tzinfo is None
            try:
                until = rrule.rrulestr(value, ignoretz=ignoretz)._until
            except ValueError:
                # WORKAROUND: dateutil<=2.7.2 doesn't set the time zone of dtstart
                if ignoretz:
                    raise
                utc_now = dt.datetime.now(dt.timezone.utc)
                until = rrule.rrulestr(value, dtstart=utc_now)._until

            if until is not None and isinstance(dtstart, dt.datetime) and (until.tzinfo != dtstart.tzinfo):
                # dateutil converts the UNTIL date to a datetime,
                # check to see if the UNTIL parameter value was a date
                vals = dict(pair.split("=") for pair in value.upper().split(";"))
                if len(vals.get("UNTIL", "")) == 8:
                    until = dt.datetime.combine(until.date(), dtstart.time())
                # While RFC2445 says UNTIL MUST be UTC, Chandler allows floating recurring events, and uses
                # floating UNTIL values. Also, some odd floating UNTIL but timezoned DTSTART values have
                # shown up in the wild, so put floating UNTIL values DTSTART's timezone
                if until.tzinfo is None:
                    until = until.replace(tzinfo=dtstart.tzinfo)

                # RFC2445 actually states that UNTIL must be a UTC value. Whilst the changes above work OK,
                # one problem case is if DTSTART is floating but UNTIL is properly specified as UTC (or with
                # a TZID). In that case dateutil will fail datetime comparisons. There is no easy solution to
                # this as there is no obvious timezone (at this point) to do proper floating time offset
                # comparisons. The best we can do is treat the UNTIL value as floating. This could mean
                # incorrect determination of the last instance. The better solution here is to encourage
                # clients to use COUNT rather than UNTIL when DTSTART is floating.

                until = until.replace(tzinfo=None) if dtstart.tzinfo is None else until.astimezone(dtstart.tzinfo)

            value_without_until = ";".join(pair for pair in value.split(";") if pair.split("=")[0].upper() != "UNTIL")
            rule = rrule.rrulestr(value_without_until, dtstart=dtstart, ignoretz=ignoretz)
            rule._until = until

            # add the rrule or exrule to the rruleset
            add_func_(rule)

        rruleset = None
        for name in DATESANDRULES:
            addfunc = None
            for line in self.contents.get(name, ()):
                # don't bother creating a rruleset unless there's a rule
                rruleset = rruleset or rrule.rruleset()
                addfunc = addfunc or getattr(rruleset, name)

                try:
                    dtstart = self.dtstart.value
                except (AttributeError, KeyError):
                    # Special for VTODO - try DUE property instead
                    if self.name != "VTODO":
                        # if there's no dtstart, just return None
                        logger.error("failed to get dtstart with VTODO")
                        return None

                    try:
                        dtstart = self.due.value
                    except (AttributeError, KeyError):
                        # if there's no due, just return None
                        logger.error("failed to find DUE at all.")
                        return None

                if name in DATENAMES:
                    # ignoring RDATEs with PERIOD values for now
                    for _dt in line.value:
                        addfunc(date_to_datetime_(_dt))
                elif name in RULENAMES:
                    _handle_rulenames(add_func_=addfunc)

                if name in ["rrule", "rdate"] and add_rdate:
                    # rlist = rruleset._rrule if name == 'rrule' else rruleset._rdate

                    # dateutils does not work with all-day (dt.date) items so we need to convert to a
                    # dt.datetime (which is what dateutils does internally)
                    adddtstart = date_to_datetime_(dtstart)

                    try:  # sourcery skip
                        if name == "rdate" and rruleset._rdate[0] != adddtstart:
                            rruleset.rdate(adddtstart)

                        elif name == "rrule" and rruleset._rrule[-1][0] != adddtstart:
                            rruleset.rdate(adddtstart)

                            if rruleset._rrule[-1]._count is not None:
                                rruleset._rrule[-1]._count -= 1
                    except IndexError:
                        # it's conceivable that an rrule has 0 datetimes
                        pass

        return rruleset

    @rruleset.setter
    def rruleset(self, rruleset):
        def _parse_values_from_rule(rule) -> dict:
            _value_map = {"BYYEARDAY": rule._byyearday, "BYWEEKNO": rule._byweekno, "BYSETPOS": rule._bysetpos}
            values_ = {}
            for k, v in _value_map.items():
                if v is not None:
                    values_[k] = [str(n) for n in v]

            if rule._interval != 1:
                values_["INTERVAL"] = [str(rule._interval)]
            if rule._wkst != 0:  # wkst defaults to Monday
                values_["WKST"] = [WEEKDAYS[rule._wkst]]

            if rule._count is not None:
                values_["COUNT"] = [str(rule._count)]
            elif rule._until is not None:
                values_["UNTIL"] = [until_serialize(rule._until)]

            days = []
            if rule._byweekday is not None and (
                rrule.WEEKLY != rule._freq or len(rule._byweekday) != 1 or rule._dtstart.weekday() != rule._byweekday[0]
            ):
                # ignore byweekday if freq is WEEKLY and day correlates with dtstart because
                # it was automatically set by dateutil
                days.extend(WEEKDAYS[n] for n in rule._byweekday)

            if rule._bynweekday is not None:
                days.extend(n + WEEKDAYS[day] for day, n in rule._bynweekday)

            if days:
                values_["BYDAY"] = days

            if rule._bymonthday and not (
                rule._freq <= rrule.MONTHLY and len(rule._bymonthday) == 1 and rule._bymonthday[0] == rule._dtstart.day
            ):
                # ignore bymonthday if it's generated by dateutil
                values_["BYMONTHDAY"] = [str(n) for n in rule._bymonthday]

            if rule._bynmonthday:
                values_.setdefault("BYMONTHDAY", []).extend(str(n) for n in rule._bynmonthday)

            if rule._bymonth and (
                rule._byweekday
                or not (
                    rule._freq == rrule.YEARLY and len(rule._bymonth) == 1 and rule._bymonth[0] == rule._dtstart.month
                )
            ):
                # ignore bymonth if it's generated by dateutil
                values_["BYMONTH"] = [str(n) for n in rule._bymonth]

            # byhour, byminute, bysecond are always ignored for now
            return values_

        # Get DTSTART from component (or DUE if no DTSTART in a VTODO)
        try:
            dtstart = self.dtstart.value
        except (AttributeError, KeyError):
            if self.name != "VTODO":
                raise
            dtstart = self.due.value

        is_date = type(dtstart) is dt.date

        dtstart = date_to_datetime_(dtstart)
        # make sure to convert time zones to UTC
        until_serialize = date_to_string if is_date else partial(datetime_to_string, convert_to_utc=True)

        for name in DATESANDRULES:
            if name in self.contents:
                del self.contents[name]
            setlist = getattr(rruleset, f"_{name}")
            if name in DATENAMES:
                setlist = list(setlist)  # make a copy of the list
                if name == "rdate" and dtstart in setlist:
                    setlist.remove(dtstart)
                if is_date:
                    setlist = [_dt.date() for _dt in setlist]
                if setlist:
                    self.add(name).value = setlist
            elif name in RULENAMES:
                for rule_item in setlist:
                    buf = get_buffer()
                    buf.write(f"FREQ={rrule.FREQNAMES[rule_item._freq]}")

                    values = _parse_values_from_rule(rule_item)
                    for key, paramvals in values.items():
                        buf.write(f";{key}={','.join(paramvals)}")

                    self.add(name).value = buf.getvalue()
Functions
getrruleset
getrruleset(add_rdate=False)

Get an rruleset created from self.

If addRDate is True, add an RDATE for dtstart if it's not included in an RRULE or RDATE, and count is decremented if it exists.

Note that for rules which don't match DTSTART, DTSTART may not appear in list(rruleset), although it should. By default, an RDATE is not created in these cases, and count isn't updated, so dateutil may list a spurious occurrence.

Source code in vobjectx/icalendar.py
def getrruleset(self, add_rdate=False):
    """
    Get an rruleset created from self.

    If addRDate is True, add an RDATE for dtstart if it's not included in an RRULE or RDATE, and count is
    decremented if it exists.

    Note that for rules which don't match DTSTART, DTSTART may not appear in list(rruleset), although it should.
    By default, an RDATE is not created in these cases, and count isn't updated, so dateutil may list a spurious
    occurrence.
    """

    def _handle_rulenames(add_func_):
        # a Ruby iCalendar library escapes semi-colons in rrules, so also remove any backslashes
        value = line.value.replace("\\", "")
        # If dtstart has no time zone, `until` shouldn't get one, either:
        ignoretz = not isinstance(dtstart, dt.datetime) or dtstart.tzinfo is None
        try:
            until = rrule.rrulestr(value, ignoretz=ignoretz)._until
        except ValueError:
            # WORKAROUND: dateutil<=2.7.2 doesn't set the time zone of dtstart
            if ignoretz:
                raise
            utc_now = dt.datetime.now(dt.timezone.utc)
            until = rrule.rrulestr(value, dtstart=utc_now)._until

        if until is not None and isinstance(dtstart, dt.datetime) and (until.tzinfo != dtstart.tzinfo):
            # dateutil converts the UNTIL date to a datetime,
            # check to see if the UNTIL parameter value was a date
            vals = dict(pair.split("=") for pair in value.upper().split(";"))
            if len(vals.get("UNTIL", "")) == 8:
                until = dt.datetime.combine(until.date(), dtstart.time())
            # While RFC2445 says UNTIL MUST be UTC, Chandler allows floating recurring events, and uses
            # floating UNTIL values. Also, some odd floating UNTIL but timezoned DTSTART values have
            # shown up in the wild, so put floating UNTIL values DTSTART's timezone
            if until.tzinfo is None:
                until = until.replace(tzinfo=dtstart.tzinfo)

            # RFC2445 actually states that UNTIL must be a UTC value. Whilst the changes above work OK,
            # one problem case is if DTSTART is floating but UNTIL is properly specified as UTC (or with
            # a TZID). In that case dateutil will fail datetime comparisons. There is no easy solution to
            # this as there is no obvious timezone (at this point) to do proper floating time offset
            # comparisons. The best we can do is treat the UNTIL value as floating. This could mean
            # incorrect determination of the last instance. The better solution here is to encourage
            # clients to use COUNT rather than UNTIL when DTSTART is floating.

            until = until.replace(tzinfo=None) if dtstart.tzinfo is None else until.astimezone(dtstart.tzinfo)

        value_without_until = ";".join(pair for pair in value.split(";") if pair.split("=")[0].upper() != "UNTIL")
        rule = rrule.rrulestr(value_without_until, dtstart=dtstart, ignoretz=ignoretz)
        rule._until = until

        # add the rrule or exrule to the rruleset
        add_func_(rule)

    rruleset = None
    for name in DATESANDRULES:
        addfunc = None
        for line in self.contents.get(name, ()):
            # don't bother creating a rruleset unless there's a rule
            rruleset = rruleset or rrule.rruleset()
            addfunc = addfunc or getattr(rruleset, name)

            try:
                dtstart = self.dtstart.value
            except (AttributeError, KeyError):
                # Special for VTODO - try DUE property instead
                if self.name != "VTODO":
                    # if there's no dtstart, just return None
                    logger.error("failed to get dtstart with VTODO")
                    return None

                try:
                    dtstart = self.due.value
                except (AttributeError, KeyError):
                    # if there's no due, just return None
                    logger.error("failed to find DUE at all.")
                    return None

            if name in DATENAMES:
                # ignoring RDATEs with PERIOD values for now
                for _dt in line.value:
                    addfunc(date_to_datetime_(_dt))
            elif name in RULENAMES:
                _handle_rulenames(add_func_=addfunc)

            if name in ["rrule", "rdate"] and add_rdate:
                # rlist = rruleset._rrule if name == 'rrule' else rruleset._rdate

                # dateutils does not work with all-day (dt.date) items so we need to convert to a
                # dt.datetime (which is what dateutils does internally)
                adddtstart = date_to_datetime_(dtstart)

                try:  # sourcery skip
                    if name == "rdate" and rruleset._rdate[0] != adddtstart:
                        rruleset.rdate(adddtstart)

                    elif name == "rrule" and rruleset._rrule[-1][0] != adddtstart:
                        rruleset.rdate(adddtstart)

                        if rruleset._rrule[-1]._count is not None:
                            rruleset._rrule[-1]._count -= 1
                except IndexError:
                    # it's conceivable that an rrule has 0 datetimes
                    pass

    return rruleset

TextBehavior

Bases: Behavior

Provide backslash escape encoding/decoding for single valued properties.

TextBehavior also deals with base64 encoding if the ENCODING parameter is explicitly set to BASE64.

Source code in vobjectx/icalendar.py
class TextBehavior(Behavior):
    """
    Provide backslash escape encoding/decoding for single valued properties.

    TextBehavior also deals with base64 encoding if the ENCODING parameter is explicitly set to BASE64.
    """

    base64string = "BASE64"  # vCard uses B

    @classmethod
    def decode(cls, line):
        """Remove backslash escaping from line.value."""
        if line.is_encoded:
            encoding = getattr(line, "encoding_param", None)
            if encoding and encoding.upper() == cls.base64string:
                line.value = base64.b64decode(line.value)
            else:
                line.value = string_to_text_values(line.value)[0]
            line.is_encoded = False

    @classmethod
    def encode(cls, line: VBase):
        """Backslash escape line.value."""
        if not line.is_encoded:
            encoding = getattr(line, "encoding_param", None)
            if encoding and encoding.upper() == cls.base64string:
                line.value = base64.b64encode(line.value.encode("utf-8")).decode("utf-8").replace("\n", "")
            else:
                line.value = backslash_escape(line.value)
            line.is_encoded = True
Functions
decode classmethod
decode(line)

Remove backslash escaping from line.value.

Source code in vobjectx/icalendar.py
@classmethod
def decode(cls, line):
    """Remove backslash escaping from line.value."""
    if line.is_encoded:
        encoding = getattr(line, "encoding_param", None)
        if encoding and encoding.upper() == cls.base64string:
            line.value = base64.b64decode(line.value)
        else:
            line.value = string_to_text_values(line.value)[0]
        line.is_encoded = False
encode classmethod
encode(line)

Backslash escape line.value.

Source code in vobjectx/icalendar.py
@classmethod
def encode(cls, line: VBase):
    """Backslash escape line.value."""
    if not line.is_encoded:
        encoding = getattr(line, "encoding_param", None)
        if encoding and encoding.upper() == cls.base64string:
            line.value = base64.b64encode(line.value.encode("utf-8")).decode("utf-8").replace("\n", "")
        else:
            line.value = backslash_escape(line.value)
        line.is_encoded = True

RecurringBehavior

Bases: VCalendarComponentBehavior

Parent Behavior for components which should be RecurringComponents.

Source code in vobjectx/icalendar.py
class RecurringBehavior(VCalendarComponentBehavior):
    """
    Parent Behavior for components which should be RecurringComponents.
    """

    has_native = True

    @staticmethod
    def transform_to_native(obj):
        """
        Turn a recurring Component into a RecurringComponent.
        """
        if not obj.is_native:
            object.__setattr__(obj, "__class__", RecurringComponent)
            obj.is_native = True
        return obj

    @staticmethod
    def transform_from_native(obj):
        if obj.is_native:
            object.__setattr__(obj, "__class__", Component)
            obj.is_native = False
        return obj

    @staticmethod
    def generate_implicit_parameters(obj):
        """
        Generate a UID and DTSTAMP if one does not exist.

        This is just a dummy implementation, for now.
        """
        if not hasattr(obj, "uid"):
            now = dt.datetime.now(UTC_TZ)
            now = datetime_to_string(now)
            host = socket.gethostname()
            obj.add(ContentLine("UID", [], f"{now} - {get_random_int()}@{host}"))

        if not hasattr(obj, "dtstamp"):
            now = dt.datetime.now(UTC_TZ)
            obj.add("dtstamp").value = now

    @classmethod
    def validate(cls, obj, raise_exception=True, complain_unrecognized=False):
        if hasattr(obj, "recurrence_id") and hasattr(obj, "dtstart"):
            if type(obj.dtstart.value) is not type(obj.recurrence_id.value):
                raise ValidateError("RECURRENCE-ID and DTSTART must be of same type")
        return super().validate(obj, raise_exception, complain_unrecognized)
Functions
transform_to_native staticmethod
transform_to_native(obj)

Turn a recurring Component into a RecurringComponent.

Source code in vobjectx/icalendar.py
@staticmethod
def transform_to_native(obj):
    """
    Turn a recurring Component into a RecurringComponent.
    """
    if not obj.is_native:
        object.__setattr__(obj, "__class__", RecurringComponent)
        obj.is_native = True
    return obj
generate_implicit_parameters staticmethod
generate_implicit_parameters(obj)

Generate a UID and DTSTAMP if one does not exist.

This is just a dummy implementation, for now.

Source code in vobjectx/icalendar.py
@staticmethod
def generate_implicit_parameters(obj):
    """
    Generate a UID and DTSTAMP if one does not exist.

    This is just a dummy implementation, for now.
    """
    if not hasattr(obj, "uid"):
        now = dt.datetime.now(UTC_TZ)
        now = datetime_to_string(now)
        host = socket.gethostname()
        obj.add(ContentLine("UID", [], f"{now} - {get_random_int()}@{host}"))

    if not hasattr(obj, "dtstamp"):
        now = dt.datetime.now(UTC_TZ)
        obj.add("dtstamp").value = now

DateTimeBehavior

Bases: Behavior

Parent Behavior for ContentLines containing one DATE-TIME.

Source code in vobjectx/icalendar.py
class DateTimeBehavior(Behavior):
    """
    Parent Behavior for ContentLines containing one DATE-TIME.
    """

    has_native = True

    @staticmethod
    def transform_to_native(obj):
        """
        Turn obj.value into a dt.

        RFC2445 allows times without time zone information, "floating times" in some properties. Mostly, this isn't
        what you want, but when parsing a file, real floating times are noted by setting to 'TRUE' the
        X-VOBJ-FLOATINGTIME-ALLOWED parameter.
        """
        if obj.is_native:
            return obj
        obj.is_native = True
        if obj.value == "":
            return obj

        # we're cheating a little here, parse_dtstart allows DATE
        obj.value = parse_dtstart(obj)
        if obj.value.tzinfo is None:
            obj.params["X-VOBJ-FLOATINGTIME-ALLOWED"] = ["TRUE"]
        if obj.params.get("TZID"):
            # Keep a copy of the original TZID around
            obj.params["X-VOBJ-ORIGINAL-TZID"] = [obj.params.pop("TZID")]
        return obj

    @classmethod
    def transform_from_native(cls, obj):
        """
        Replace the datetime in obj.value with an ISO 8601 string.
        """
        if obj.is_native:
            obj.is_native = False
            tzid = TimezoneComponent.register_tzinfo(obj.value.tzinfo)
            obj_value: str | dt.datetime = datetime_to_string(obj.value, cls.force_utc)
            if not cls.force_utc and tzid is not None:
                obj.tzid_param = tzid
            if obj.params.get("X-VOBJ-ORIGINAL-TZID"):
                if not hasattr(obj, "tzid_param"):
                    obj.tzid_param = obj.x_vobj_original_tzid_param
                del obj.params["X-VOBJ-ORIGINAL-TZID"]
            obj.value = obj_value
        return obj
Functions
transform_to_native staticmethod
transform_to_native(obj)

Turn obj.value into a dt.

RFC2445 allows times without time zone information, "floating times" in some properties. Mostly, this isn't what you want, but when parsing a file, real floating times are noted by setting to 'TRUE' the X-VOBJ-FLOATINGTIME-ALLOWED parameter.

Source code in vobjectx/icalendar.py
@staticmethod
def transform_to_native(obj):
    """
    Turn obj.value into a dt.

    RFC2445 allows times without time zone information, "floating times" in some properties. Mostly, this isn't
    what you want, but when parsing a file, real floating times are noted by setting to 'TRUE' the
    X-VOBJ-FLOATINGTIME-ALLOWED parameter.
    """
    if obj.is_native:
        return obj
    obj.is_native = True
    if obj.value == "":
        return obj

    # we're cheating a little here, parse_dtstart allows DATE
    obj.value = parse_dtstart(obj)
    if obj.value.tzinfo is None:
        obj.params["X-VOBJ-FLOATINGTIME-ALLOWED"] = ["TRUE"]
    if obj.params.get("TZID"):
        # Keep a copy of the original TZID around
        obj.params["X-VOBJ-ORIGINAL-TZID"] = [obj.params.pop("TZID")]
    return obj
transform_from_native classmethod
transform_from_native(obj)

Replace the datetime in obj.value with an ISO 8601 string.

Source code in vobjectx/icalendar.py
@classmethod
def transform_from_native(cls, obj):
    """
    Replace the datetime in obj.value with an ISO 8601 string.
    """
    if obj.is_native:
        obj.is_native = False
        tzid = TimezoneComponent.register_tzinfo(obj.value.tzinfo)
        obj_value: str | dt.datetime = datetime_to_string(obj.value, cls.force_utc)
        if not cls.force_utc and tzid is not None:
            obj.tzid_param = tzid
        if obj.params.get("X-VOBJ-ORIGINAL-TZID"):
            if not hasattr(obj, "tzid_param"):
                obj.tzid_param = obj.x_vobj_original_tzid_param
            del obj.params["X-VOBJ-ORIGINAL-TZID"]
        obj.value = obj_value
    return obj

UTCDateTimeBehavior

Bases: DateTimeBehavior

A value which must be specified in UTC.

Source code in vobjectx/icalendar.py
class UTCDateTimeBehavior(DateTimeBehavior):
    """
    A value which must be specified in UTC.
    """

    force_utc = True

DateOrDateTimeBehavior

Bases: Behavior

Parent Behavior for ContentLines containing one DATE or DATE-TIME.

Source code in vobjectx/icalendar.py
class DateOrDateTimeBehavior(Behavior):
    """
    Parent Behavior for ContentLines containing one DATE or DATE-TIME.
    """

    has_native = True

    @staticmethod
    def transform_to_native(obj):
        """Turn obj.value into a date or dt."""
        if obj.is_native:
            return obj
        obj.is_native = True
        if obj.value == "":
            return obj

        obj.value = parse_dtstart(obj, allow_signature_mismatch=True)
        if getattr(obj, "value_param", "DATE-TIME").upper() == "DATE-TIME" and hasattr(obj, "tzid_param"):
            # Keep a copy of the original TZID around
            obj.params["X-VOBJ-ORIGINAL-TZID"] = [obj.tzid_param]
            del obj.tzid_param
        return obj

    @staticmethod
    def transform_from_native(obj):
        """
        Replace the date or datetime in obj.value with an ISO 8601 string.
        """
        if type(obj.value) is not dt.date:
            return DateTimeBehavior.transform_from_native(obj)
        obj.is_native = False
        obj.value_param = "DATE"
        obj.value = date_to_string(obj.value)
        return obj
Functions
transform_to_native staticmethod
transform_to_native(obj)

Turn obj.value into a date or dt.

Source code in vobjectx/icalendar.py
@staticmethod
def transform_to_native(obj):
    """Turn obj.value into a date or dt."""
    if obj.is_native:
        return obj
    obj.is_native = True
    if obj.value == "":
        return obj

    obj.value = parse_dtstart(obj, allow_signature_mismatch=True)
    if getattr(obj, "value_param", "DATE-TIME").upper() == "DATE-TIME" and hasattr(obj, "tzid_param"):
        # Keep a copy of the original TZID around
        obj.params["X-VOBJ-ORIGINAL-TZID"] = [obj.tzid_param]
        del obj.tzid_param
    return obj
transform_from_native staticmethod
transform_from_native(obj)

Replace the date or datetime in obj.value with an ISO 8601 string.

Source code in vobjectx/icalendar.py
@staticmethod
def transform_from_native(obj):
    """
    Replace the date or datetime in obj.value with an ISO 8601 string.
    """
    if type(obj.value) is not dt.date:
        return DateTimeBehavior.transform_from_native(obj)
    obj.is_native = False
    obj.value_param = "DATE"
    obj.value = date_to_string(obj.value)
    return obj

MultiDateBehavior

Bases: Behavior

Parent Behavior for ContentLines containing one or more DATE, DATE-TIME, or PERIOD.

Source code in vobjectx/icalendar.py
class MultiDateBehavior(Behavior):
    """
    Parent Behavior for ContentLines containing one or more DATE, DATE-TIME, or PERIOD.
    """

    has_native = True

    @staticmethod
    def transform_to_native(obj):
        """
        Turn obj.value into a list of dates, datetimes, or (datetime, timedelta) tuples.
        """
        if obj.is_native:
            return obj
        obj.is_native = True
        if obj.value == "":
            obj.value = []
            return obj
        tzinfo = TzidRegistry.get(getattr(obj, "tzid_param", None))
        value_param = getattr(obj, "value_param", "DATE-TIME").upper()
        val_texts = obj.value.split(",")
        if value_param == "DATE":
            obj.value = [vtypes.Date(x).value for x in val_texts]
        elif value_param == "DATE-TIME":
            obj.value = [vtypes.DateTime(x, tzinfo).value for x in val_texts]
        elif value_param == "PERIOD":
            obj.value = [vtypes.Period(x, tzinfo).value for x in val_texts]
        return obj

    @staticmethod
    def transform_from_native(obj):
        """
        Replace the date, datetime or period tuples in obj.value with appropriate strings.
        """
        if obj.value and type(obj.value[0]) is dt.date:
            obj.is_native = False
            obj.value_param = "DATE"
            obj.value = ",".join([date_to_string(val) for val in obj.value])

        # Fixme: handle PERIOD case
        elif obj.is_native:
            obj.is_native = False
            transformed = []
            tzid = None
            for val in obj.value:
                if tzid is None and type(val) is dt.datetime:
                    tzid = TimezoneComponent.register_tzinfo(val.tzinfo)
                    if tzid is not None:
                        obj.tzid_param = tzid
                transformed.append(datetime_to_string(val))
            obj.value = ",".join(transformed)
        return obj
Functions
transform_to_native staticmethod
transform_to_native(obj)

Turn obj.value into a list of dates, datetimes, or (datetime, timedelta) tuples.

Source code in vobjectx/icalendar.py
@staticmethod
def transform_to_native(obj):
    """
    Turn obj.value into a list of dates, datetimes, or (datetime, timedelta) tuples.
    """
    if obj.is_native:
        return obj
    obj.is_native = True
    if obj.value == "":
        obj.value = []
        return obj
    tzinfo = TzidRegistry.get(getattr(obj, "tzid_param", None))
    value_param = getattr(obj, "value_param", "DATE-TIME").upper()
    val_texts = obj.value.split(",")
    if value_param == "DATE":
        obj.value = [vtypes.Date(x).value for x in val_texts]
    elif value_param == "DATE-TIME":
        obj.value = [vtypes.DateTime(x, tzinfo).value for x in val_texts]
    elif value_param == "PERIOD":
        obj.value = [vtypes.Period(x, tzinfo).value for x in val_texts]
    return obj
transform_from_native staticmethod
transform_from_native(obj)

Replace the date, datetime or period tuples in obj.value with appropriate strings.

Source code in vobjectx/icalendar.py
@staticmethod
def transform_from_native(obj):
    """
    Replace the date, datetime or period tuples in obj.value with appropriate strings.
    """
    if obj.value and type(obj.value[0]) is dt.date:
        obj.is_native = False
        obj.value_param = "DATE"
        obj.value = ",".join([date_to_string(val) for val in obj.value])

    # Fixme: handle PERIOD case
    elif obj.is_native:
        obj.is_native = False
        transformed = []
        tzid = None
        for val in obj.value:
            if tzid is None and type(val) is dt.datetime:
                tzid = TimezoneComponent.register_tzinfo(val.tzinfo)
                if tzid is not None:
                    obj.tzid_param = tzid
            transformed.append(datetime_to_string(val))
        obj.value = ",".join(transformed)
    return obj

MultiTextBehavior

Bases: Behavior

Provide backslash escape encoding/decoding of each of several values.

After transformation, value is a list of strings.

Source code in vobjectx/icalendar.py
class MultiTextBehavior(Behavior):
    """
    Provide backslash escape encoding/decoding of each of several values.

    After transformation, value is a list of strings.
    """

    list_separator = ","

    @classmethod
    def decode(cls, line):
        """
        Remove backslash escaping from line.value, then split on commas.
        """
        if line.is_encoded:
            line.value = string_to_text_values(line.value, list_separator=cls.list_separator)
            line.is_encoded = False

    @classmethod
    def encode(cls, line):
        """
        Backslash escape line.value.
        """
        if not line.is_encoded:
            line.value = cls.list_separator.join(backslash_escape(val) for val in line.value)
            line.is_encoded = True
Functions
decode classmethod
decode(line)

Remove backslash escaping from line.value, then split on commas.

Source code in vobjectx/icalendar.py
@classmethod
def decode(cls, line):
    """
    Remove backslash escaping from line.value, then split on commas.
    """
    if line.is_encoded:
        line.value = string_to_text_values(line.value, list_separator=cls.list_separator)
        line.is_encoded = False
encode classmethod
encode(line)

Backslash escape line.value.

Source code in vobjectx/icalendar.py
@classmethod
def encode(cls, line):
    """
    Backslash escape line.value.
    """
    if not line.is_encoded:
        line.value = cls.list_separator.join(backslash_escape(val) for val in line.value)
        line.is_encoded = True

VCalendar2

Bases: VCalendarComponentBehavior

vCalendar 2.0 behavior. With added VAVAILABILITY support.

Source code in vobjectx/icalendar.py
class VCalendar2(VCalendarComponentBehavior):
    """
    vCalendar 2.0 behavior. With added VAVAILABILITY support.
    """

    name = "VCALENDAR"
    description = "vCalendar 2.0, also known as iCalendar."
    version_string = "2.0"
    sort_first = ("VERSION", "CALSCALE", "METHOD", "PRODID", "VTIMEZONE")
    known_children = {
        "CALSCALE": (0, 1, None),  # min, max, behavior_registry id
        "METHOD": (0, 1, None),
        "VERSION": (0, 1, None),  # required, but auto-generated
        "PRODID": (1, 1, None),
        "VTIMEZONE": (0, None, None),
        "VEVENT": (0, None, None),
        "VTODO": (0, None, None),
        "VJOURNAL": (0, None, None),
        "VFREEBUSY": (0, None, None),
        "VAVAILABILITY": (0, None, None),
    }

    @classmethod
    def generate_implicit_parameters(cls, obj):
        """
        Create PRODID, VERSION and VTIMEZONEs if needed.

        VTIMEZONEs will need to exist whenever TZID parameters exist or when datetimes with tzinfo exist.
        """

        def find_tzids(obj_, table: set):
            if isinstance(obj_, ContentLine) and (obj_.behavior is None or not obj_.behavior.force_utc):
                if getattr(obj_, "tzid_param", None):
                    warn_if_true()
                    table.add(obj_.tzid_param)
                else:
                    if type(obj_.value) is list:
                        for _ in obj_.value:
                            tzinfo = getattr(obj_.value, "tzinfo", None)
                            warn_if_true(tzinfo is not None)
                            tzid_ = TimezoneComponent.register_tzinfo(tzinfo)
                            if tzid_:
                                table.add(tzid_)
                    else:
                        tzinfo = getattr(obj_.value, "tzinfo", None)
                        tzid_ = TimezoneComponent.register_tzinfo(tzinfo)
                        if tzid_:
                            table.add(tzid_)
            for child in obj_.get_children():
                if obj_.name != "VTIMEZONE":
                    find_tzids(child, table)

        for comp in obj.components():
            if comp.behavior is not None:
                comp.behavior.generate_implicit_parameters(comp)
        if not hasattr(obj, "prodid"):
            obj.add(ContentLine("PRODID", [], PRODID))
        if not hasattr(obj, "version"):
            obj.add(ContentLine("VERSION", [], cls.version_string))

        tzids_used = set()
        find_tzids(obj, tzids_used)
        oldtzids = [x.tzid.value for x in getattr(obj, "vtimezone_list", [])]
        for tzid in tzids_used:
            if tzid != "UTC" and tzid not in oldtzids:
                obj.add(TimezoneComponent(tzinfo=TzidRegistry.get(tzid)))

    @classmethod
    def serialize(cls, obj, buf, line_length, validate=True, *args, **kwargs):
        """
        Set implicit parameters, do encoding, return unicode string.

        If validate is True, raise VObjectError if the line doesn't validate after implicit parameters are generated.

        Default is to call base.default_serialize.
        """

        cls.generate_implicit_parameters(obj)
        if validate:
            cls.validate(obj, raise_exception=True)

        outbuf = buf or get_buffer()
        group_string = "" if obj.group is None else f"{obj.group}."
        if obj.use_begin:
            fold_one_line(outbuf, f"{group_string}BEGIN:{obj.name}", line_length)

        props, comps = set(), set()
        for key in obj.contents.keys():
            if isinstance(obj.contents[key][0], Component):
                comps.add(key)
            else:
                props.add(key)

        first_props, first_components = [], []
        for key in cls.sort_first:
            if key in props:
                first_props.append(key)
                props.remove(key)
            if key in comps:
                first_components.append(key)
                comps.remove(key)

        sorted_keys = first_props + sorted(props) + first_components + sorted(comps)

        for child in chain.from_iterable(obj.contents[key] for key in sorted_keys):
            # validate is recursive, we only need to validate once
            child.serialize(outbuf, line_length, validate=False)

        if obj.use_begin:
            fold_one_line(outbuf, f"{group_string}END:{obj.name}", line_length)
        if obj.is_native:
            obj.transform_to_native()
        return outbuf.getvalue()
Functions
generate_implicit_parameters classmethod
generate_implicit_parameters(obj)

Create PRODID, VERSION and VTIMEZONEs if needed.

VTIMEZONEs will need to exist whenever TZID parameters exist or when datetimes with tzinfo exist.

Source code in vobjectx/icalendar.py
@classmethod
def generate_implicit_parameters(cls, obj):
    """
    Create PRODID, VERSION and VTIMEZONEs if needed.

    VTIMEZONEs will need to exist whenever TZID parameters exist or when datetimes with tzinfo exist.
    """

    def find_tzids(obj_, table: set):
        if isinstance(obj_, ContentLine) and (obj_.behavior is None or not obj_.behavior.force_utc):
            if getattr(obj_, "tzid_param", None):
                warn_if_true()
                table.add(obj_.tzid_param)
            else:
                if type(obj_.value) is list:
                    for _ in obj_.value:
                        tzinfo = getattr(obj_.value, "tzinfo", None)
                        warn_if_true(tzinfo is not None)
                        tzid_ = TimezoneComponent.register_tzinfo(tzinfo)
                        if tzid_:
                            table.add(tzid_)
                else:
                    tzinfo = getattr(obj_.value, "tzinfo", None)
                    tzid_ = TimezoneComponent.register_tzinfo(tzinfo)
                    if tzid_:
                        table.add(tzid_)
        for child in obj_.get_children():
            if obj_.name != "VTIMEZONE":
                find_tzids(child, table)

    for comp in obj.components():
        if comp.behavior is not None:
            comp.behavior.generate_implicit_parameters(comp)
    if not hasattr(obj, "prodid"):
        obj.add(ContentLine("PRODID", [], PRODID))
    if not hasattr(obj, "version"):
        obj.add(ContentLine("VERSION", [], cls.version_string))

    tzids_used = set()
    find_tzids(obj, tzids_used)
    oldtzids = [x.tzid.value for x in getattr(obj, "vtimezone_list", [])]
    for tzid in tzids_used:
        if tzid != "UTC" and tzid not in oldtzids:
            obj.add(TimezoneComponent(tzinfo=TzidRegistry.get(tzid)))
serialize classmethod
serialize(obj, buf, line_length, validate=True, *args, **kwargs)

Set implicit parameters, do encoding, return unicode string.

If validate is True, raise VObjectError if the line doesn't validate after implicit parameters are generated.

Default is to call base.default_serialize.

Source code in vobjectx/icalendar.py
@classmethod
def serialize(cls, obj, buf, line_length, validate=True, *args, **kwargs):
    """
    Set implicit parameters, do encoding, return unicode string.

    If validate is True, raise VObjectError if the line doesn't validate after implicit parameters are generated.

    Default is to call base.default_serialize.
    """

    cls.generate_implicit_parameters(obj)
    if validate:
        cls.validate(obj, raise_exception=True)

    outbuf = buf or get_buffer()
    group_string = "" if obj.group is None else f"{obj.group}."
    if obj.use_begin:
        fold_one_line(outbuf, f"{group_string}BEGIN:{obj.name}", line_length)

    props, comps = set(), set()
    for key in obj.contents.keys():
        if isinstance(obj.contents[key][0], Component):
            comps.add(key)
        else:
            props.add(key)

    first_props, first_components = [], []
    for key in cls.sort_first:
        if key in props:
            first_props.append(key)
            props.remove(key)
        if key in comps:
            first_components.append(key)
            comps.remove(key)

    sorted_keys = first_props + sorted(props) + first_components + sorted(comps)

    for child in chain.from_iterable(obj.contents[key] for key in sorted_keys):
        # validate is recursive, we only need to validate once
        child.serialize(outbuf, line_length, validate=False)

    if obj.use_begin:
        fold_one_line(outbuf, f"{group_string}END:{obj.name}", line_length)
    if obj.is_native:
        obj.transform_to_native()
    return outbuf.getvalue()

VTimezone

Bases: VCalendarComponentBehavior

Timezone behavior.

Source code in vobjectx/icalendar.py
class VTimezone(VCalendarComponentBehavior):
    """
    Timezone behavior.
    """

    name = "VTIMEZONE"
    has_native = True
    description = "A grouping of component properties that defines a time zone."
    sort_first = ("TZID", "LAST-MODIFIED", "TZURL", "STANDARD", "DAYLIGHT")
    known_children = {
        "TZID": (1, 1, None),  # min, max, behavior_registry id
        "LAST-MODIFIED": (0, 1, None),
        "TZURL": (0, 1, None),
        "STANDARD": (0, None, None),  # NOTE: One of Standard or
        "DAYLIGHT": (0, None, None),  # Daylight must appear
    }

    @classmethod
    def validate(cls, obj, raise_exception=False, complain_unrecognized=False):
        if not hasattr(obj, "tzid") or obj.tzid.value is None:
            if raise_exception:
                raise ValidateError("VTIMEZONE components must contain a valid TZID")
            return False
        if "standard" in obj.contents or "daylight" in obj.contents:
            return super().validate(obj, raise_exception, complain_unrecognized)

        if raise_exception:
            raise ValidateError("VTIMEZONE components must contain a STANDARD or a DAYLIGHT component")
        return False

    @staticmethod
    def transform_to_native(obj):
        if not obj.is_native:
            object.__setattr__(obj, "__class__", TimezoneComponent)
            obj.is_native = True
            obj.register_tzinfo(obj.tzinfo)
        return obj

    @staticmethod
    def transform_from_native(obj):
        return obj

TZID

Bases: Behavior

Don't use TextBehavior for TZID.

RFC2445 only allows TZID lines to be paramtext, so they shouldn't need any encoding or decoding. Unfortunately, some Microsoft products use commas in TZIDs which should NOT be treated as a multi-valued text property, nor do we want to escape them. Leaving them alone works for Microsoft's breakage, and doesn't affect compliant iCalendar streams.

Source code in vobjectx/icalendar.py
class TZID(Behavior):
    """
    Don't use TextBehavior for TZID.

    RFC2445 only allows TZID lines to be paramtext, so they shouldn't need any encoding or decoding.  Unfortunately,
    some Microsoft products use commas in TZIDs which should NOT be treated as a multi-valued text property,
    nor do we want to escape them.  Leaving them alone works for Microsoft's breakage, and doesn't affect compliant
    iCalendar streams.
    """

VEvent

Bases: RecurringBehavior

Event behavior.

Source code in vobjectx/icalendar.py
class VEvent(RecurringBehavior):
    """Event behavior."""

    name = "VEVENT"
    sort_first = ("UID", "RECURRENCE-ID", "DTSTART", "DURATION", "DTEND")

    description = 'A grouping of component properties, and possibly including \
                   "VALARM" calendar components, that represents a scheduled \
                   amount of time on a calendar.'
    known_children = {
        "DTSTART": (0, 1, None),  # min, max, behavior_registry id
        "CLASS": (0, 1, None),
        "CREATED": (0, 1, None),
        "DESCRIPTION": (0, 1, None),
        "GEO": (0, 1, None),
        "LAST-MODIFIED": (0, 1, None),
        "LOCATION": (0, 1, None),
        "ORGANIZER": (0, 1, None),
        "PRIORITY": (0, 1, None),
        "DTSTAMP": (1, 1, None),  # required
        "SEQUENCE": (0, 1, None),
        "STATUS": (0, 1, None),
        "SUMMARY": (0, 1, None),
        "TRANSP": (0, 1, None),
        "UID": (1, 1, None),
        "URL": (0, 1, None),
        "RECURRENCE-ID": (0, 1, None),
        "DTEND": (0, 1, None),  # NOTE: Only one of DtEnd or
        "DURATION": (0, 1, None),  # Duration can appear
        "ATTACH": (0, None, None),
        "ATTENDEE": (0, None, None),
        "CATEGORIES": (0, None, None),
        "COMMENT": (0, None, None),
        "CONTACT": (0, None, None),
        "EXDATE": (0, None, None),
        "EXRULE": (0, None, None),
        "REQUEST-STATUS": (0, None, None),
        "RELATED-TO": (0, None, None),
        "RESOURCES": (0, None, None),
        "RDATE": (0, None, None),
        "RRULE": (0, None, None),
        "VALARM": (0, None, None),
    }

    @classmethod
    def validate(cls, obj, raise_exception=False, complain_unrecognized=False):
        if "dtend" in obj.contents and "duration" in obj.contents:
            if raise_exception:
                raise ValidateError("VEVENT components cannot contain both DTEND and DURATION components")
            return False
        return super().validate(obj, raise_exception, complain_unrecognized)

VTodo

Bases: RecurringBehavior

To-do behavior.

Source code in vobjectx/icalendar.py
class VTodo(RecurringBehavior):
    """To-do behavior."""

    name = "VTODO"
    description = 'A grouping of component properties and possibly "VALARM" \
                   calendar components that represent an action-item or \
                   assignment.'
    known_children = {
        "DTSTART": (0, 1, None),  # min, max, behavior_registry id
        "CLASS": (0, 1, None),
        "COMPLETED": (0, 1, None),
        "CREATED": (0, 1, None),
        "DESCRIPTION": (0, 1, None),
        "GEO": (0, 1, None),
        "LAST-MODIFIED": (0, 1, None),
        "LOCATION": (0, 1, None),
        "ORGANIZER": (0, 1, None),
        "PERCENT": (0, 1, None),
        "PRIORITY": (0, 1, None),
        "DTSTAMP": (1, 1, None),
        "SEQUENCE": (0, 1, None),
        "STATUS": (0, 1, None),
        "SUMMARY": (0, 1, None),
        "UID": (0, 1, None),
        "URL": (0, 1, None),
        "RECURRENCE-ID": (0, 1, None),
        "DUE": (0, 1, None),  # NOTE: Only one of Due or
        "DURATION": (0, 1, None),  # Duration can appear
        "ATTACH": (0, None, None),
        "ATTENDEE": (0, None, None),
        "CATEGORIES": (0, None, None),
        "COMMENT": (0, None, None),
        "CONTACT": (0, None, None),
        "EXDATE": (0, None, None),
        "EXRULE": (0, None, None),
        "REQUEST-STATUS": (0, None, None),
        "RELATED-TO": (0, None, None),
        "RESOURCES": (0, None, None),
        "RDATE": (0, None, None),
        "RRULE": (0, None, None),
        "VALARM": (0, None, None),
    }

    @classmethod
    def validate(cls, obj, raise_exception=False, complain_unrecognized=False):
        if "due" in obj.contents and "duration" in obj.contents:
            if raise_exception:
                raise ValidateError("VTODO components cannot contain both DUE and DURATION components")
            return False
        return super().validate(obj, raise_exception, complain_unrecognized)

VJournal

Bases: RecurringBehavior

Journal entry behavior.

Source code in vobjectx/icalendar.py
class VJournal(RecurringBehavior):
    """
    Journal entry behavior.
    """

    name = "VJOURNAL"
    known_children = {
        "DTSTART": (0, 1, None),  # min, max, behavior_registry id
        "CLASS": (0, 1, None),
        "CREATED": (0, 1, None),
        "DESCRIPTION": (0, 1, None),
        "LAST-MODIFIED": (0, 1, None),
        "ORGANIZER": (0, 1, None),
        "DTSTAMP": (1, 1, None),
        "SEQUENCE": (0, 1, None),
        "STATUS": (0, 1, None),
        "SUMMARY": (0, 1, None),
        "UID": (0, 1, None),
        "URL": (0, 1, None),
        "RECURRENCE-ID": (0, 1, None),
        "ATTACH": (0, None, None),
        "ATTENDEE": (0, None, None),
        "CATEGORIES": (0, None, None),
        "COMMENT": (0, None, None),
        "CONTACT": (0, None, None),
        "EXDATE": (0, None, None),
        "EXRULE": (0, None, None),
        "REQUEST-STATUS": (0, None, None),
        "RELATED-TO": (0, None, None),
        "RDATE": (0, None, None),
        "RRULE": (0, None, None),
    }

VFreeBusy

Bases: VCalendarComponentBehavior

Free/busy state behavior.

Source code in vobjectx/icalendar.py
class VFreeBusy(VCalendarComponentBehavior):
    """
    Free/busy state behavior.
    """

    name = "VFREEBUSY"
    description = "A grouping of component properties that describe either a \
                   request for free/busy time, describe a response to a request \
                   for free/busy time or describe a published set of busy time."
    sort_first = ("UID", "DTSTART", "DURATION", "DTEND")
    known_children = {
        "DTSTART": (0, 1, None),  # min, max, behavior_registry id
        "CONTACT": (0, 1, None),
        "DTEND": (0, 1, None),
        "DURATION": (0, 1, None),
        "ORGANIZER": (0, 1, None),
        "DTSTAMP": (1, 1, None),
        "UID": (0, 1, None),
        "URL": (0, 1, None),
        "ATTENDEE": (0, None, None),
        "COMMENT": (0, None, None),
        "FREEBUSY": (0, None, None),
        "REQUEST-STATUS": (0, None, None),
    }

VAlarm

Bases: VCalendarComponentBehavior

Alarm behavior

Source code in vobjectx/icalendar.py
class VAlarm(VCalendarComponentBehavior):
    """Alarm behavior"""

    name = "VALARM"
    description = "Alarms describe when and how to provide alerts about events and to-dos."
    known_children = {
        "ACTION": (1, 1, None),  # min, max, behavior_registry id
        "TRIGGER": (1, 1, None),
        "DURATION": (0, 1, None),
        "REPEAT": (0, 1, None),
        "DESCRIPTION": (0, 1, None),
    }

    @staticmethod
    def generate_implicit_parameters(obj):
        """Create default ACTION and TRIGGER if they're not set."""
        if not hasattr(obj, "action"):
            obj.add("action").value = "AUDIO"

        if not hasattr(obj, "trigger"):
            obj.add("trigger").value = dt.timedelta(0)

    @classmethod
    def validate(cls, obj, raise_exception: bool = True, complain_unrecognized: bool = False) -> bool:
        contents = obj.contents

        def fail(msg):
            if raise_exception:
                raise ValidateError(msg)
            return False

        action = contents.action[0].value

        # REPEAT and DURATION must appear together
        if ("duration" in contents) ^ ("repeat" in contents):
            return fail("VALARM DURATION and REPEAT must both be present/absent.")

        if action == "DISPLAY":
            if "description" not in contents:
                return fail("DISPLAY VALARM missing DESCRIPTION")

        elif action == "EMAIL":
            for prop in ("description", "summary", "attendee"):
                if prop not in contents:
                    return fail(f"EMAIL VALARM missing {prop.upper()}")

        elif action == "AUDIO":
            if len(contents.get("attach", [])) > 1:
                return fail("AUDIO VALARM can contain only one ATTACH")

        return super().validate(obj, raise_exception, complain_unrecognized)
Functions
generate_implicit_parameters staticmethod
generate_implicit_parameters(obj)

Create default ACTION and TRIGGER if they're not set.

Source code in vobjectx/icalendar.py
@staticmethod
def generate_implicit_parameters(obj):
    """Create default ACTION and TRIGGER if they're not set."""
    if not hasattr(obj, "action"):
        obj.add("action").value = "AUDIO"

    if not hasattr(obj, "trigger"):
        obj.add("trigger").value = dt.timedelta(0)

VAvailability

Bases: VCalendarComponentBehavior

Availability state behavior.

Used to represent user's available time slots.

Source code in vobjectx/icalendar.py
class VAvailability(VCalendarComponentBehavior):
    """
    Availability state behavior.

    Used to represent user's available time slots.
    """

    name = "VAVAILABILITY"
    description = "A component used to represent a user's available time slots."
    sort_first = ("UID", "DTSTART", "DURATION", "DTEND")
    known_children = {
        "UID": (1, 1, None),  # min, max, behavior_registry id
        "DTSTAMP": (1, 1, None),
        "BUSYTYPE": (0, 1, None),
        "CREATED": (0, 1, None),
        "DTSTART": (0, 1, None),
        "LAST-MODIFIED": (0, 1, None),
        "ORGANIZER": (0, 1, None),
        "SEQUENCE": (0, 1, None),
        "SUMMARY": (0, 1, None),
        "URL": (0, 1, None),
        "DTEND": (0, 1, None),
        "DURATION": (0, 1, None),
        "CATEGORIES": (0, None, None),
        "COMMENT": (0, None, None),
        "CONTACT": (0, None, None),
        "AVAILABLE": (0, None, None),
    }

    @classmethod
    def validate(cls, obj, raise_exception=False, complain_unrecognized=False):
        if "dtend" in obj.contents and "duration" in obj.contents:
            if raise_exception:
                raise ValidateError("VAVAILABILITY components cannot contain both DTEND and DURATION components")
            return False
        return super().validate(obj, raise_exception, complain_unrecognized)

Available

Bases: RecurringBehavior

Event behavior.

Source code in vobjectx/icalendar.py
class Available(RecurringBehavior):
    """
    Event behavior.
    """

    name = "AVAILABLE"
    sort_first = ("UID", "RECURRENCE-ID", "DTSTART", "DURATION", "DTEND")
    description = "Defines a period of time in which a user is normally available."
    known_children = {
        "DTSTAMP": (1, 1, None),  # min, max, behavior_registry id
        "DTSTART": (1, 1, None),
        "UID": (1, 1, None),
        "DTEND": (0, 1, None),  # NOTE: One of DtEnd or
        "DURATION": (0, 1, None),  # Duration must appear, but not both
        "CREATED": (0, 1, None),
        "LAST-MODIFIED": (0, 1, None),
        "RECURRENCE-ID": (0, 1, None),
        "RRULE": (0, 1, None),
        "SUMMARY": (0, 1, None),
        "CATEGORIES": (0, None, None),
        "COMMENT": (0, None, None),
        "CONTACT": (0, None, None),
        "EXDATE": (0, None, None),
        "RDATE": (0, None, None),
    }

    @classmethod
    def validate(cls, obj, raise_exception=False, complain_unrecognized=False):
        if ("dtend" in obj.contents) ^ ("duration" in obj.contents):
            return super().validate(obj, raise_exception, complain_unrecognized)
        if raise_exception:
            raise ValidateError("AVAILABLE components must have either DTEND or DURATION properties, but not both")
        return False

Duration

Bases: Behavior

Behavior for Duration ContentLines. Transform to dt.timedelta.

Source code in vobjectx/icalendar.py
class Duration(Behavior):
    """
    Behavior for Duration ContentLines.  Transform to dt.timedelta.
    """

    name = "DURATION"
    has_native = True

    @staticmethod
    def transform_to_native(obj):
        """Turn obj.value into a dt.timedelta."""
        if obj.is_native:
            return obj
        obj.is_native = True

        if obj.value == "":
            return obj

        obj.value = vtypes.Duration(obj.value).value
        return obj

    @staticmethod
    def transform_from_native(obj):
        """
        Replace the dt.timedelta in obj.value with an RFC2445 string.
        """
        if not obj.is_native:
            return obj
        obj.is_native = False
        obj.value = timedelta_to_string(obj.value)
        return obj
Functions
transform_to_native staticmethod
transform_to_native(obj)

Turn obj.value into a dt.timedelta.

Source code in vobjectx/icalendar.py
@staticmethod
def transform_to_native(obj):
    """Turn obj.value into a dt.timedelta."""
    if obj.is_native:
        return obj
    obj.is_native = True

    if obj.value == "":
        return obj

    obj.value = vtypes.Duration(obj.value).value
    return obj
transform_from_native staticmethod
transform_from_native(obj)

Replace the dt.timedelta in obj.value with an RFC2445 string.

Source code in vobjectx/icalendar.py
@staticmethod
def transform_from_native(obj):
    """
    Replace the dt.timedelta in obj.value with an RFC2445 string.
    """
    if not obj.is_native:
        return obj
    obj.is_native = False
    obj.value = timedelta_to_string(obj.value)
    return obj

Trigger

Bases: Behavior

DATE-TIME or DURATION

Source code in vobjectx/icalendar.py
class Trigger(Behavior):
    """
    DATE-TIME or DURATION
    """

    name = "TRIGGER"
    description = "This property specifies when an alarm will trigger."
    has_native = True
    force_utc = True

    @staticmethod
    def transform_to_native(obj):
        """
        Turn obj.value into a timedelta or dt.
        """
        if obj.is_native:
            return obj
        value = getattr(obj, "value_param", "DURATION").upper()
        if hasattr(obj, "value_param"):
            del obj.value_param
        if obj.value == "":
            obj.is_native = True
            return obj
        if value == "DURATION":
            try:
                return Duration.transform_to_native(obj)
            except ParseError:
                logger.warning(
                    "TRIGGER not recognized as DURATION, trying DATE-TIME, because iCal sometimes exports DATE-TIMEs "
                    "without setting VALUE=DATE-TIME"
                )
                try:
                    obj.is_native = False
                    return DateTimeBehavior.transform_to_native(obj)
                except AllException as e:
                    raise ParseError("TRIGGER with no VALUE not recognized as DURATION or as DATE-TIME") from e
        elif value == "DATE-TIME":
            # TRIGGERs with DATE-TIME values must be in UTC, we could validate that fact, for now we take it on faith.
            return DateTimeBehavior.transform_to_native(obj)
        else:
            raise ParseError("VALUE must be DURATION or DATE-TIME")

    @staticmethod
    def transform_from_native(obj):
        if type(obj.value) is dt.datetime:
            obj.value_param = "DATE-TIME"
            return UTCDateTimeBehavior.transform_from_native(obj)
        if type(obj.value) is dt.timedelta:
            return Duration.transform_from_native(obj)

        raise NativeError("Native TRIGGER values must be timedelta or datetime")
Functions
transform_to_native staticmethod
transform_to_native(obj)

Turn obj.value into a timedelta or dt.

Source code in vobjectx/icalendar.py
@staticmethod
def transform_to_native(obj):
    """
    Turn obj.value into a timedelta or dt.
    """
    if obj.is_native:
        return obj
    value = getattr(obj, "value_param", "DURATION").upper()
    if hasattr(obj, "value_param"):
        del obj.value_param
    if obj.value == "":
        obj.is_native = True
        return obj
    if value == "DURATION":
        try:
            return Duration.transform_to_native(obj)
        except ParseError:
            logger.warning(
                "TRIGGER not recognized as DURATION, trying DATE-TIME, because iCal sometimes exports DATE-TIMEs "
                "without setting VALUE=DATE-TIME"
            )
            try:
                obj.is_native = False
                return DateTimeBehavior.transform_to_native(obj)
            except AllException as e:
                raise ParseError("TRIGGER with no VALUE not recognized as DURATION or as DATE-TIME") from e
    elif value == "DATE-TIME":
        # TRIGGERs with DATE-TIME values must be in UTC, we could validate that fact, for now we take it on faith.
        return DateTimeBehavior.transform_to_native(obj)
    else:
        raise ParseError("VALUE must be DURATION or DATE-TIME")

PeriodBehavior

Bases: Behavior

A list of (date-time, timedelta) tuples.

Source code in vobjectx/icalendar.py
class PeriodBehavior(Behavior):
    """A list of (date-time, timedelta) tuples."""

    has_native = True

    @staticmethod
    def transform_to_native(obj):
        """
        Convert comma separated periods into tuples.
        """
        if obj.is_native:
            return obj
        obj.is_native = True
        if obj.value == "":
            obj.value = []
            return obj
        tzinfo = TzidRegistry.get(getattr(obj, "tzid_param", None))
        obj.value = [vtypes.Period(x, tzinfo).value for x in obj.value.split(",")]
        return obj

    @classmethod
    def transform_from_native(cls, obj):
        """
        Convert the list of tuples in obj.value to strings.
        """
        if obj.is_native:
            obj.is_native = False
            transformed = [period_to_string(tup, cls.force_utc) for tup in obj.value]
            if transformed:
                tzid = TimezoneComponent.register_tzinfo(obj.value[-1][0].tzinfo)
                if not cls.force_utc and tzid is not None:
                    obj.tzid_param = tzid

            obj.value = ",".join(transformed)

        return obj
Functions
transform_to_native staticmethod
transform_to_native(obj)

Convert comma separated periods into tuples.

Source code in vobjectx/icalendar.py
@staticmethod
def transform_to_native(obj):
    """
    Convert comma separated periods into tuples.
    """
    if obj.is_native:
        return obj
    obj.is_native = True
    if obj.value == "":
        obj.value = []
        return obj
    tzinfo = TzidRegistry.get(getattr(obj, "tzid_param", None))
    obj.value = [vtypes.Period(x, tzinfo).value for x in obj.value.split(",")]
    return obj
transform_from_native classmethod
transform_from_native(obj)

Convert the list of tuples in obj.value to strings.

Source code in vobjectx/icalendar.py
@classmethod
def transform_from_native(cls, obj):
    """
    Convert the list of tuples in obj.value to strings.
    """
    if obj.is_native:
        obj.is_native = False
        transformed = [period_to_string(tup, cls.force_utc) for tup in obj.value]
        if transformed:
            tzid = TimezoneComponent.register_tzinfo(obj.value[-1][0].tzinfo)
            if not cls.force_utc and tzid is not None:
                obj.tzid_param = tzid

        obj.value = ",".join(transformed)

    return obj

FreeBusy

Bases: PeriodBehavior

Free or busy period of time, must be specified in UTC.

Source code in vobjectx/icalendar.py
class FreeBusy(PeriodBehavior):
    """Free or busy period of time, must be specified in UTC."""

    name = "FREEBUSY"
    force_utc = True

RRule

Bases: Behavior

Dummy behavior to avoid having RRULEs being treated as text lines (and thus having semi-colons inaccurately escaped).

Source code in vobjectx/icalendar.py
class RRule(Behavior):
    """
    Dummy behavior to avoid having RRULEs being treated as text lines
    (and thus having semi-colons inaccurately escaped).
    """

Functions

Module: vobjectx.vcard

vobjectx.vcard

Definitions and behavior for vCard 3.0

Classes

Name

Source code in vobjectx/vcard.py
class Name:
    def __init__(self, family="", given="", additional="", *, prefix="", suffix=""):
        """
        Each name attribute can be a string or a list of strings.
        """
        self.family = family
        self.given = given
        self.additional = additional
        self.prefix = prefix
        self.suffix = suffix

    def __str__(self):
        eng_order = ("prefix", "given", "additional", "family", "suffix")
        return " ".join(to_string(getattr(self, val)) for val in eng_order)

    def __repr__(self):
        return f"<Name: {self!s}>"

    def __eq__(self, other: Self) -> bool:
        return (
            self.family == other.family
            and self.given == other.given
            and self.additional == other.additional
            and self.prefix == other.prefix
            and self.suffix == other.suffix
        )

Address

Source code in vobjectx/vcard.py
class Address:
    lines = ("box", "extended", "street")
    one_line = ("city", "region", "code")

    def __init__(self, street="", city="", region="", country="", *, code="", box="", extended=""):
        """
        Each name attribute can be a string or a list of strings.
        """
        self.box = box
        self.extended = extended
        self.street = street
        self.city = city
        self.region = region
        self.code = code
        self.country = country

    def __str__(self):
        lines = "\n".join(to_string(getattr(self, val), "\n") for val in self.lines if getattr(self, val))
        one_line = tuple(to_string(getattr(self, val)) for val in self.one_line)
        lines += "\n{0!s}, {1!s} {2!s}".format(*one_line)  # pylint:disable=c0209
        if self.country:
            lines += "\n" + to_string(self.country, "\n")
        return lines

    def __repr__(self):
        return f"<Address: {self!s}>"

    def __eq__(self, other: Self) -> bool:
        return (
            self.box == other.box
            and self.extended == other.extended
            and self.street == other.street
            and self.city == other.city
            and self.region == other.region
            and self.code == other.code
            and self.country == other.country
        )

VCardTextBehavior

Bases: Behavior

Provide backslash escape encoding/decoding for single valued properties.

TextBehavior also deals with base64 encoding if the ENCODING parameter is explicitly set to BASE64.

Source code in vobjectx/vcard.py
class VCardTextBehavior(Behavior):
    """
    Provide backslash escape encoding/decoding for single valued properties.

    TextBehavior also deals with base64 encoding if the ENCODING parameter is
    explicitly set to BASE64.
    """

    allow_group = True
    base64string = "B"

    @classmethod
    def decode(cls, line):
        """
        Remove backslash escaping from line.value_decode line, either to remove
        backslash escaping, or to decode base64 encoding. The content line should
        contain a ENCODING=b for base64 encoding, but Apple Addressbook seems to
        export a singleton parameter of 'BASE64', which does not match the 3.0
        vCard spec. If we encounter that, then we transform the parameter to
        ENCODING=b
        """
        if line.is_encoded:
            if "BASE64" in line.params:
                del line.params["BASE64"]
                line.encoding_param = cls.base64string
            encoding = getattr(line, "encoding_param", None)
            if encoding:
                line.value = byte_decoder(line.value)
            else:
                line.value = string_to_text_values(line.value)[0]
            line.is_encoded = False

    @classmethod
    def encode(cls, line):
        """Backslash escape line.value."""
        if not line.is_encoded:
            encoding = getattr(line, "encoding_param", "")
            if encoding and encoding.upper() == cls.base64string:
                if isinstance(line.value, bytes):
                    line.value = byte_encoder(line.value).decode("utf-8").replace("\n", "")
                else:
                    line.value = byte_encoder(line.value.encode(encoding)).decode("utf-8")
            else:
                line.value = backslash_escape(line.value)
            line.is_encoded = True
Functions
decode classmethod
decode(line)

Remove backslash escaping from line.value_decode line, either to remove backslash escaping, or to decode base64 encoding. The content line should contain a ENCODING=b for base64 encoding, but Apple Addressbook seems to export a singleton parameter of 'BASE64', which does not match the 3.0 vCard spec. If we encounter that, then we transform the parameter to ENCODING=b

Source code in vobjectx/vcard.py
@classmethod
def decode(cls, line):
    """
    Remove backslash escaping from line.value_decode line, either to remove
    backslash escaping, or to decode base64 encoding. The content line should
    contain a ENCODING=b for base64 encoding, but Apple Addressbook seems to
    export a singleton parameter of 'BASE64', which does not match the 3.0
    vCard spec. If we encounter that, then we transform the parameter to
    ENCODING=b
    """
    if line.is_encoded:
        if "BASE64" in line.params:
            del line.params["BASE64"]
            line.encoding_param = cls.base64string
        encoding = getattr(line, "encoding_param", None)
        if encoding:
            line.value = byte_decoder(line.value)
        else:
            line.value = string_to_text_values(line.value)[0]
        line.is_encoded = False
encode classmethod
encode(line)

Backslash escape line.value.

Source code in vobjectx/vcard.py
@classmethod
def encode(cls, line):
    """Backslash escape line.value."""
    if not line.is_encoded:
        encoding = getattr(line, "encoding_param", "")
        if encoding and encoding.upper() == cls.base64string:
            if isinstance(line.value, bytes):
                line.value = byte_encoder(line.value).decode("utf-8").replace("\n", "")
            else:
                line.value = byte_encoder(line.value.encode(encoding)).decode("utf-8")
        else:
            line.value = backslash_escape(line.value)
        line.is_encoded = True

VCard3

Bases: VCardBehavior

vCard 3.0 behavior.

Source code in vobjectx/vcard.py
class VCard3(VCardBehavior):
    """
    vCard 3.0 behavior.
    """

    name = "VCARD"
    description = "vCard 3.0, defined in rfc2426"
    version_string = "3.0"
    is_component = True
    sort_first = ("VERSION", "PRODID", "UID")
    known_children = {
        "N": (0, 1, None),  # min, max, behavior_registry id
        "FN": (1, None, None),
        "VERSION": (1, 1, None),  # required, auto-generated
        "PRODID": (0, 1, None),
        "LABEL": (0, None, None),
        "UID": (0, None, None),
        "ADR": (0, None, None),
        "ORG": (0, None, None),
        "PHOTO": (0, None, None),
        "CATEGORIES": (0, None, None),
        "GEO": (0, None, None),
    }

    @classmethod
    def generate_implicit_parameters(cls, obj):
        """
        Create PRODID, VERSION, and VTIMEZONEs if needed.

        VTIMEZONEs will need to exist whenever TZID parameters exist or when
        datetimes with tzinfo exist.
        """
        if not hasattr(obj, "version"):
            obj.add(ContentLine("VERSION", [], cls.version_string))
Functions
generate_implicit_parameters classmethod
generate_implicit_parameters(obj)

Create PRODID, VERSION, and VTIMEZONEs if needed.

VTIMEZONEs will need to exist whenever TZID parameters exist or when datetimes with tzinfo exist.

Source code in vobjectx/vcard.py
@classmethod
def generate_implicit_parameters(cls, obj):
    """
    Create PRODID, VERSION, and VTIMEZONEs if needed.

    VTIMEZONEs will need to exist whenever TZID parameters exist or when
    datetimes with tzinfo exist.
    """
    if not hasattr(obj, "version"):
        obj.add(ContentLine("VERSION", [], cls.version_string))

Photo

Bases: VCardTextBehavior

Source code in vobjectx/vcard.py
class Photo(VCardTextBehavior):
    name = "Photo"
    description = "Photograph"
    WACKY_APPLE_PHOTO_SERIALIZE = True
    REALLY_LARGE = 1e50

    @classmethod
    def value_repr(cls, line):
        return f" (BINARY PHOTO DATA at 0x{id(line.value)!s}) "

    @classmethod
    def serialize(cls, obj, buf, line_length, validate=True, *args, **kwargs):
        """
        Apple's Address Book is *really* weird with images, it expects
        base64 data to have very specific whitespace.  It seems Address Book
        can handle PHOTO if it's not wrapped, so don't wrap it.
        """
        if cls.WACKY_APPLE_PHOTO_SERIALIZE:
            line_length = cls.REALLY_LARGE
        VCardTextBehavior.serialize(obj, buf, line_length, validate, *args, **kwargs)
Functions
serialize classmethod
serialize(obj, buf, line_length, validate=True, *args, **kwargs)

Apple's Address Book is really weird with images, it expects base64 data to have very specific whitespace. It seems Address Book can handle PHOTO if it's not wrapped, so don't wrap it.

Source code in vobjectx/vcard.py
@classmethod
def serialize(cls, obj, buf, line_length, validate=True, *args, **kwargs):
    """
    Apple's Address Book is *really* weird with images, it expects
    base64 data to have very specific whitespace.  It seems Address Book
    can handle PHOTO if it's not wrapped, so don't wrap it.
    """
    if cls.WACKY_APPLE_PHOTO_SERIALIZE:
        line_length = cls.REALLY_LARGE
    VCardTextBehavior.serialize(obj, buf, line_length, validate, *args, **kwargs)

NameBehavior

Bases: VCardBehavior

A structured name.

Source code in vobjectx/vcard.py
class NameBehavior(VCardBehavior):
    """
    A structured name.
    """

    has_native = True
    field_order = "family", "given", "additional", "prefix", "suffix"

    @classmethod
    def transform_to_native(cls, obj):
        """
        Turn obj.value into a Name.
        """
        if obj.is_native:
            return obj
        obj.is_native = True
        obj.value = Name(**dict(zip(cls.field_order, split_fields(obj.value))))
        return obj

    @classmethod
    def transform_from_native(cls, obj):
        """
        Replace the Name in obj.value with a string.
        """
        obj.is_native = False
        obj.value = serialize_fields(obj.value, cls.field_order)
        return obj
Functions
transform_to_native classmethod
transform_to_native(obj)

Turn obj.value into a Name.

Source code in vobjectx/vcard.py
@classmethod
def transform_to_native(cls, obj):
    """
    Turn obj.value into a Name.
    """
    if obj.is_native:
        return obj
    obj.is_native = True
    obj.value = Name(**dict(zip(cls.field_order, split_fields(obj.value))))
    return obj
transform_from_native classmethod
transform_from_native(obj)

Replace the Name in obj.value with a string.

Source code in vobjectx/vcard.py
@classmethod
def transform_from_native(cls, obj):
    """
    Replace the Name in obj.value with a string.
    """
    obj.is_native = False
    obj.value = serialize_fields(obj.value, cls.field_order)
    return obj

AddressBehavior

Bases: VCardBehavior

A structured address.

Source code in vobjectx/vcard.py
class AddressBehavior(VCardBehavior):
    """
    A structured address.
    """

    has_native = True
    field_order = "box", "extended", "street", "city", "region", "code", "country"

    @classmethod
    def transform_to_native(cls, obj):
        """
        Turn obj.value into an Address.
        """
        if obj.is_native:
            return obj
        obj.is_native = True
        obj.value = Address(**dict(zip(cls.field_order, split_fields(obj.value))))
        return obj

    @classmethod
    def transform_from_native(cls, obj):
        """
        Replace the Address in obj.value with a string.
        """
        obj.is_native = False
        obj.value = serialize_fields(obj.value, cls.field_order)
        return obj
Functions
transform_to_native classmethod
transform_to_native(obj)

Turn obj.value into an Address.

Source code in vobjectx/vcard.py
@classmethod
def transform_to_native(cls, obj):
    """
    Turn obj.value into an Address.
    """
    if obj.is_native:
        return obj
    obj.is_native = True
    obj.value = Address(**dict(zip(cls.field_order, split_fields(obj.value))))
    return obj
transform_from_native classmethod
transform_from_native(obj)

Replace the Address in obj.value with a string.

Source code in vobjectx/vcard.py
@classmethod
def transform_from_native(cls, obj):
    """
    Replace the Address in obj.value with a string.
    """
    obj.is_native = False
    obj.value = serialize_fields(obj.value, cls.field_order)
    return obj

OrgBehavior

Bases: VCardBehavior

A list of organization values and sub-organization values.

Source code in vobjectx/vcard.py
class OrgBehavior(VCardBehavior):
    """
    A list of organization values and sub-organization values.
    """

    has_native = True

    @staticmethod
    def transform_to_native(obj):
        """
        Turn obj.value into a list.
        """
        if obj.is_native:
            return obj
        obj.is_native = True
        obj.value = split_fields(obj.value)
        return obj

    @staticmethod
    def transform_from_native(obj):
        """
        Replace the list in obj.value with a string.
        """
        if not obj.is_native:
            return obj
        obj.is_native = False
        obj.value = serialize_fields(obj.value)
        return obj
Functions
transform_to_native staticmethod
transform_to_native(obj)

Turn obj.value into a list.

Source code in vobjectx/vcard.py
@staticmethod
def transform_to_native(obj):
    """
    Turn obj.value into a list.
    """
    if obj.is_native:
        return obj
    obj.is_native = True
    obj.value = split_fields(obj.value)
    return obj
transform_from_native staticmethod
transform_from_native(obj)

Replace the list in obj.value with a string.

Source code in vobjectx/vcard.py
@staticmethod
def transform_from_native(obj):
    """
    Replace the list in obj.value with a string.
    """
    if not obj.is_native:
        return obj
    obj.is_native = False
    obj.value = serialize_fields(obj.value)
    return obj

Functions

split_fields

split_fields(string)

Return a list of strings or lists from a Name or Address.

Source code in vobjectx/vcard.py
def split_fields(string):
    """
    Return a list of strings or lists from a Name or Address.
    """

    def to_list_or_string(x) -> str | list:
        string_list = string_to_text_values(x)
        return string_list[0] if len(string_list) == 1 else string_list

    return [to_list_or_string(i) for i in string_to_text_values(string, list_separator=";", char_list=";")]

serialize_fields

serialize_fields(obj, order=None)

Turn an object's fields into a ';' and ',' separated string.

If order is None, obj should be a list, backslash escape each field and return a ';' separated string.

Source code in vobjectx/vcard.py
def serialize_fields(obj, order=None):
    """
    Turn an object's fields into a ';' and ',' separated string.

    If order is None, obj should be a list, backslash escape each field and
    return a ';' separated string.
    """
    fields = []
    if order is None:
        fields = [backslash_escape(val) for val in obj]
    else:
        for field in order:
            escaped_value_list = [backslash_escape(val) for val in to_list(getattr(obj, field))]
            fields.append(",".join(escaped_value_list))
    return ";".join(fields)