Add CONFERENCE property

pull/877/head
Nicco Kunzmann 2025-09-20 22:25:02 +01:00
rodzic 3786498d29
commit fc8872ddd8
10 zmienionych plików z 404 dodań i 8 usunięć

Wyświetl plik

@ -67,6 +67,9 @@ icalendar.prop
.. automodule:: icalendar.prop
:members:
.. automodule:: icalendar.prop.conference
:members:
.. automodule:: icalendar.prop.image
:members:

Wyświetl plik

@ -74,6 +74,7 @@ from icalendar.prop import (
vUTCOffset,
vWeekday,
)
from icalendar.prop.conference import Conference
from icalendar.prop.image import Image
# Switching the timezone provider
@ -104,6 +105,7 @@ __all__ = [
"ComponentEndMissing",
"ComponentFactory",
"ComponentStartMissing",
"Conference",
"Event",
"FeatureWillBeRemovedInFutureVersion",
"FreeBusy",

Wyświetl plik

@ -9,7 +9,15 @@ from typing import TYPE_CHECKING, Optional, Sequence, Union
from icalendar.enums import BUSYTYPE, CLASS, STATUS, TRANSP, StrEnum
from icalendar.error import IncompleteComponent, InvalidCalendar
from icalendar.parser_tools import SEQUENCE_TYPES
from icalendar.prop import vCalAddress, vCategory, vDDDTypes, vDuration, vRecur, vText
from icalendar.prop import (
vCalAddress,
vCategory,
vDDDTypes,
vDuration,
vRecur,
vText,
)
from icalendar.prop.conference import Conference
from icalendar.prop.image import Image
from icalendar.timezone import tzp
from icalendar.tools import is_date
@ -1617,6 +1625,113 @@ def _get_images(self: Component) -> list[Image]:
images_property = property(_get_images)
def _get_conferences(self: Component) -> list[Conference]:
"""Return the CONFERENCE properties as a list.
Purpose:
This property specifies information for accessing a conferencing system.
Conformance:
This property can be specified multiple times in a
"VEVENT" or "VTODO" calendar component.
Description:
This property specifies information for accessing a
conferencing system for attendees of a meeting or task. This
might be for a telephone-based conference number dial-in with
access codes included (such as a tel: URI :rfc:`3966` or a sip: or
sips: URI :rfc:`3261`), for a web-based video chat (such as an http:
or https: URI :rfc:`7230`), or for an instant messaging group chat
room (such as an xmpp: URI :rfc:`5122`). If a specific URI for a
conferencing system is not available, a data: URI :rfc:`2397`
containing a text description can be used.
A conference system can be a bidirectional communication channel
or a uni-directional "broadcast feed".
The "FEATURE" property parameter is used to describe the key
capabilities of the conference system to allow a client to choose
the ones that give the required level of interaction from a set of
multiple properties.
The "LABEL" property parameter is used to convey additional
details on the use of the URI. For example, the URIs or access
codes for the moderator and attendee of a teleconference system
could be different, and the "LABEL" property parameter could be
used to "tag" each "CONFERENCE" property to indicate which is
which.
The "LANGUAGE" property parameter can be used to specify the
language used for text values used with this property (as per
Section 3.2.10 of :rfc:`5545`).
Example:
The following are examples of this property:
.. code-block:: ical
CONFERENCE;VALUE=URI;FEATURE=PHONE,MODERATOR;
LABEL=Moderator dial-in:tel:+1-412-555-0123,,,654321
CONFERENCE;VALUE=URI;FEATURE=PHONE;
LABEL=Attendee dial-in:tel:+1-412-555-0123,,,555123
CONFERENCE;VALUE=URI;FEATURE=PHONE;
LABEL=Attendee dial-in:tel:+1-888-555-0456,,,555123
CONFERENCE;VALUE=URI;FEATURE=CHAT;
LABEL=Chat room:xmpp:chat-123@conference.example.com
CONFERENCE;VALUE=URI;FEATURE=AUDIO,VIDEO;
LABEL=Attendee dial-in:https://chat.example.com/audio?id=123456
Get all conferences:
.. code-block:: pycon
>>> from icalendar import Event
>>> event = Event()
>>> event.conferences
[]
Set a conference:
.. code-block:: pycon
>>> from icalendar import Event, Conference
>>> event = Event()
>>> event.conferences = [
... Conference(
... "tel:+1-412-555-0123,,,654321",
... feature="PHONE,MODERATOR",
... label="Moderator dial-in",
... language="EN",
... )
... ]
>>> print(event.to_ical())
BEGIN:VEVENT
CONFERENCE;FEATURE="PHONE,MODERATOR";LABEL=Moderator dial-in;LANGUAGE=EN:t
el:+1-412-555-0123,,,654321
END:VEVENT
"""
conferences = self.get("CONFERENCE", [])
if not isinstance(conferences, Sequence):
conferences = [conferences]
return [Conference.from_uri(conference) for conference in conferences]
def _set_conferences(self: Component, conferences: list[Conference] | None):
"""Set the conferences."""
_del_conferences(self)
for conference in conferences or []:
self.add("CONFERENCE", conference.to_uri())
def _del_conferences(self: Component):
"""Delete all conferences."""
self.pop("CONFERENCE")
conferences_property = property(_get_conferences, _set_conferences, _del_conferences)
__all__ = [
"attendees_property",
"busy_type_property",
@ -1624,6 +1739,7 @@ __all__ = [
"class_property",
"color_property",
"comments_property",
"conferences_property",
"contacts_property",
"create_single_property",
"description_property",

Wyświetl plik

@ -3,18 +3,20 @@
from __future__ import annotations
from datetime import date, datetime, timezone
from typing import ClassVar, Optional
from typing import TYPE_CHECKING, ClassVar, Optional
from icalendar.attr import comments_property, single_utc_property, uid_property
from icalendar.cal.component_factory import ComponentFactory
from icalendar.caselessdict import CaselessDict
from icalendar.compatibility import Self
from icalendar.error import InvalidCalendar
from icalendar.parser import Contentline, Contentlines, Parameters, q_join, q_split
from icalendar.parser_tools import DEFAULT_ENCODING
from icalendar.prop import TypesFactory, vDDDLists, vText
from icalendar.timezone import tzp
if TYPE_CHECKING:
from icalendar.compatibility import Self
_marker = []
@ -127,7 +129,13 @@ class Component(CaselessDict):
obj.params[key] = item
return obj
def add(self, name, value, parameters=None, encode=1):
def add(
self,
name: str,
value,
parameters: dict[str, str] | Parameters = None,
encode: bool = True, # noqa: FBT001
):
"""Add a property.
:param name: Name of the property.
@ -293,7 +301,7 @@ class Component(CaselessDict):
return properties
@classmethod
def from_ical(cls, st, multiple:bool=False) -> Self|list[Self]:
def from_ical(cls, st, multiple: bool = False) -> Self | list[Self]: # noqa: FBT001
"""Populates the component recursively from a string."""
stack = [] # a stack of components
comps = []
@ -560,7 +568,7 @@ class Component(CaselessDict):
The property can be specified once in "VEVENT",
"VTODO", or "VJOURNAL" calendar components. The value MUST be
specified as a date with UTC time.
""",
)

Wyświetl plik

@ -13,6 +13,7 @@ from icalendar.attr import (
categories_property,
class_property,
color_property,
conferences_property,
contacts_property,
create_single_property,
description_property,
@ -43,6 +44,7 @@ if TYPE_CHECKING:
from icalendar.alarms import Alarms
from icalendar.enums import CLASS, STATUS, TRANSP
from icalendar.prop import vCalAddress
from icalendar.prop.conference import Conference
class Event(Component):
@ -485,6 +487,7 @@ class Event(Component):
status = status_property
attendees = attendees_property
images = images_property
conferences = conferences_property
@classmethod
def new(
@ -495,6 +498,7 @@ class Event(Component):
classification: CLASS | None = None,
color: str | None = None,
comments: list[str] | str | None = None,
conferences: list[Conference] | None = None,
contacts: list[str] | str | None = None,
created: date | None = None,
description: str | None = None,
@ -522,6 +526,7 @@ class Event(Component):
classification: The :attr:`classification` of the event.
color: The :attr:`color` of the event.
comments: The :attr:`Component.comments` of the event.
conferences: The :attr:`conferences` of the event.
created: The :attr:`Component.created` of the event.
description: The :attr:`description` of the event.
end: The :attr:`end` of the event.
@ -571,6 +576,7 @@ class Event(Component):
event.contacts = contacts
event.status = status
event.attendees = attendees
event.conferences = conferences
if cls._validate_new:
cls._validate_start_and_end(start, end)
return event

Wyświetl plik

@ -13,6 +13,7 @@ from icalendar.attr import (
categories_property,
class_property,
color_property,
conferences_property,
contacts_property,
create_single_property,
description_property,
@ -41,6 +42,7 @@ if TYPE_CHECKING:
from icalendar.alarms import Alarms
from icalendar.enums import CLASS, STATUS
from icalendar.prop import vCalAddress
from icalendar.prop.conference import Conference
class Todo(Component):
@ -353,6 +355,7 @@ class Todo(Component):
status = status_property
attendees = attendees_property
images = images_property
conferences = conferences_property
@classmethod
def new(
@ -364,6 +367,7 @@ class Todo(Component):
color: str | None = None,
comments: list[str] | str | None = None,
contacts: list[str] | str | None = None,
conferences: list[Conference] | None = None,
created: date | None = None,
description: str | None = None,
end: date | datetime | None = None,
@ -389,6 +393,7 @@ class Todo(Component):
classification: The :attr:`classification` of the todo.
color: The :attr:`color` of the todo.
comments: The :attr:`Component.comments` of the todo.
conferences: The :attr:`conferences` if the todo.
created: The :attr:`Component.created` of the todo.
description: The :attr:`description` of the todo.
end: The :attr:`end` of the todo.
@ -435,6 +440,7 @@ class Todo(Component):
todo.contacts = contacts
todo.status = status
todo.attendees = attendees
todo.conferences = conferences
if cls._validate_new:
cls._validate_start_and_end(start, end)
return todo

Wyświetl plik

@ -1769,8 +1769,8 @@ class vUri(str):
def __new__(
cls,
value,
encoding=DEFAULT_ENCODING,
value: str,
encoding: str = DEFAULT_ENCODING,
/,
params: Optional[dict[str, Any]] = None,
):
@ -2145,6 +2145,7 @@ class TypesFactory(CaselessDict):
"recurrence-id": "date-time",
"related-to": "text",
"url": "uri",
"conference": "uri", # RFC 7986
"source": "uri",
"uid": "text",
# Recurrence Component Properties

Wyświetl plik

@ -0,0 +1,96 @@
"""Conferences according to Section 5.11 of :rfc:`7986`."""
from __future__ import annotations
from dataclasses import dataclass
from icalendar.prop import vUri
@dataclass
class Conference:
"""Conferences according to Section 5.11 of :rfc:`7986`.
Purpose:
Information for accessing a conferencing system.
Conformance:
This property can be specified multiple times in a
"VEVENT" or "VTODO" calendar component.
Description:
This property specifies information for accessing a
conferencing system for attendees of a meeting or task. This
might be for a telephone-based conference number dial-in with
access codes included (such as a tel: URI :rfc:`3966` or a sip: or
sips: URI :rfc:`3261`), for a web-based video chat (such as an http:
or https: URI :rfc:`7230`), or for an instant messaging group chat
room (such as an xmpp: URI :rfc:`5122`). If a specific URI for a
conferencing system is not available, a data: URI :rfc:`2397`
containing a text description can be used.
A conference system can be a bidirectional communication channel
or a uni-directional "broadcast feed".
The "FEATURE" property parameter is used to describe the key
capabilities of the conference system to allow a client to choose
the ones that give the required level of interaction from a set of
multiple properties.
The "LABEL" property parameter is used to convey additional
details on the use of the URI. For example, the URIs or access
codes for the moderator and attendee of a teleconference system
could be different, and the "LABEL" property parameter could be
used to "tag" each "CONFERENCE" property to indicate which is
which.
The "LANGUAGE" property parameter can be used to specify the
language used for text values used with this property (as per
Section 3.2.10 of :rfc:`5545`).
Example:
The following are examples of this property:
.. code-block:: ical
CONFERENCE;VALUE=URI;FEATURE=PHONE,MODERATOR;
LABEL=Moderator dial-in:tel:+1-412-555-0123,,,654321
CONFERENCE;VALUE=URI;FEATURE=PHONE;
LABEL=Attendee dial-in:tel:+1-412-555-0123,,,555123
CONFERENCE;VALUE=URI;FEATURE=PHONE;
LABEL=Attendee dial-in:tel:+1-888-555-0456,,,555123
CONFERENCE;VALUE=URI;FEATURE=CHAT;
LABEL=Chat room:xmpp:chat-123@conference.example.com
CONFERENCE;VALUE=URI;FEATURE=AUDIO,VIDEO;
LABEL=Attendee dial-in:https://chat.example.com/audio?id=123456
"""
# see https://stackoverflow.com/a/18348004/1320237
uri: str
feature: list[str] | str | None = None
label: list[str] | str | None = None
language: list[str] | str | None = None
@classmethod
def from_uri(cls, uri: vUri):
"""Create a Conference from a URI."""
return cls(
uri,
feature=uri.params.get("feature"),
label=uri.params.get("label"),
language=uri.params.get("language"),
)
def to_uri(self) -> vUri:
"""Convert the Conference to a vUri."""
params = {}
if self.feature:
params["FEATURE"] = self.feature
if self.label:
params["LABEL"] = self.label
if self.language:
params["LANGUAGE"] = self.language
return vUri(self.uri, params=params)
__all__ = ["Conference"]

Wyświetl plik

@ -0,0 +1,14 @@
BEGIN:VCALENDAR
BEGIN:VEVENT
CONFERENCE;VALUE=URI;FEATURE=PHONE,MODERATOR;
LABEL=Moderator dial-in:tel:+1-412-555-0123,,,654321
CONFERENCE;VALUE=URI;FEATURE=PHONE;
LABEL=Attendee dial-in:tel:+1-412-555-0123,,,555123
CONFERENCE;VALUE=URI;FEATURE=PHONE;
LABEL=Attendee dial-in:tel:+1-888-555-0456,,,555123
CONFERENCE;VALUE=URI;FEATURE=CHAT;
LABEL=Chat room:xmpp:chat-123@conference.example.com
CONFERENCE;VALUE=URI;FEATURE=AUDIO,VIDEO;
LABEL=Attendee dial-in:https://chat.example.com/audio?id=123456
END:VEVENT
END:VCALENDAR

Wyświetl plik

@ -0,0 +1,144 @@
"""Test conference properties."""
import pytest
from icalendar import Calendar, Event, Todo, vUri
from icalendar.prop.conference import Conference
def test_from_empty_uri():
"""Test creation from a URI."""
conference = Conference.from_uri(vUri("https://chat.example.com/audio?id=123456"))
assert conference.uri == "https://chat.example.com/audio?id=123456"
assert conference.feature is None
assert conference.label is None
assert conference.language is None
def test_from_example_uri():
"""Create a conference with lots of values."""
conference = Conference.from_uri(
vUri(
"https://chat.example.com/audio?id=123456",
params={
"FEATURE": "PHONE,MODERATOR",
"LABEL": "Moderator dial-in",
"LANGUAGE": "EN",
},
)
)
assert conference.uri == "https://chat.example.com/audio?id=123456"
assert conference.feature == "PHONE,MODERATOR"
assert conference.label == "Moderator dial-in"
assert conference.language == "EN"
def test_to_uri():
"""Test creating a vURI."""
uri = vUri(
"https://chat.example.com/audio?id=123456",
params={"FEATURE": "PHONE", "LABEL": "Moderator", "LANGUAGE": "DE"},
)
conference = Conference.from_uri(uri)
new_uri = conference.to_uri()
assert new_uri == uri
assert new_uri.params == uri.params
@pytest.fixture
def conference_1():
"""Fixture for a conference with a URI."""
return Conference.from_uri(vUri("https://chat.example.com/audio?id=123456"))
@pytest.fixture
def conference_2():
"""Fixture for a conference with a URI."""
return Conference(
vUri("https://chat.example.com/audio?id=123456"),
feature="PHONE",
label="Moderator dial-in",
language="EN",
)
@pytest.fixture(params=[Event, Todo])
def component_class(request):
"""Fixture to create a Conference component."""
return request.param
@pytest.fixture
def component(component_class):
"""Create a component."""
return component_class()
def test_no_conferenes(component):
"""No conferences by default."""
assert component.conferences == []
assert "CONFERENCE" not in component
def test_add_conference(component, conference_1):
"""Add a new conference."""
component.conferences = [conference_1]
assert component["CONFERENCE"] == conference_1.to_vUri()
def test_add_multiple_conferences(component, conference_1, conference_2):
"""Add a new conference."""
component.conferences = [conference_1, conference_2]
assert component["CONFERENCE"] == [conference_1.to_vUri(), conference_2.to_vUri()]
def test_new_component_with_conferences(component_class, conference_1, conference_2):
"""Create a new component with conferences."""
component = component_class.new(conferences=[conference_1, conference_2])
assert component.conferences == [conference_1, conference_2]
def test_uri_in_ical_(conference_2, component):
"""The URI start be in the ical string."""
component.conferences = [conference_2]
ical_str = component.to_ical().decode("utf-8")
assert "CONFERENCE;" in ical_str
assert "/audio?id=123456" in ical_str
assert "PHONE" in ical_str
def get_event(calendar: Calendar) -> Event:
"""Get the event directly"""
return calendar.events[0]
def get_serialized_event(calendar: Calendar) -> Event:
"""Get the event directly"""
cal = Calendar.from_ical(calendar.to_ical())
return cal.events[0]
@pytest.fixture(params=[get_event, get_serialized_event])
def event_with_conferences(request, calendars):
"""Return the event from the file."""
calendar = calendars.rfc_7986_conferences
return request.param(calendar)
def test_conferences_from_file(event_with_conferences):
"""The conferences should be in the calendar."""
assert len(event_with_conferences.conferences) == 5
assert event_with_conferences.conferences[0].uri == "tel:+1-412-555-0123,,,654321"
assert event_with_conferences.conferences[1].uri == "tel:+1-412-555-0123,,,555123"
assert event_with_conferences.conferences[2].uri == "tel:+1-888-555-0456,,,555123"
assert (
event_with_conferences.conferences[3].uri
== "xmpp:chat-123@conference.example.com"
)
assert (
event_with_conferences.conferences[4].uri
== "https://chat.example.com/audio?id=123456"
)
assert event_with_conferences.conferences[4].feature == ["AUDIO", "VIDEO"]
assert event_with_conferences.conferences[4].label == "Attendee dial-in"
assert event_with_conferences.conferences[4].language is None