ical.calendar_stream
The core, a collection of Calendar and Scheduling objects.
This is an example of parsing an ics file as a stream of calendar objects:
from pathlib import Path
from ical.calendar_stream import IcsCalendarStream
filename = Path("example/calendar.ics")
with filename.open() as ics_file:
stream = IcsCalendarStream.from_ics(ics_file.read())
print("File contains %s calendar(s)", len(stream.calendars))
You can encode a calendar stream as ics content calling the ics() method on
the IcsCalendarStream:
from pathlib import Path
filename = Path("/tmp/output.ics")
with filename.open(mode="w") as ics_file:
ics_file.write(stream.ics())
Fetching a remote ics file without blocking the event loop requires the
optional ical[async] extra (which installs aiohttp):
from ical.calendar_stream import IcsCalendarStream
stream = await IcsCalendarStream.from_url("https://example.com/calendar.ics")
1"""The core, a collection of Calendar and Scheduling objects. 2 3This is an example of parsing an ics file as a stream of calendar objects: 4```python 5from pathlib import Path 6from ical.calendar_stream import IcsCalendarStream 7 8filename = Path("example/calendar.ics") 9with filename.open() as ics_file: 10 stream = IcsCalendarStream.from_ics(ics_file.read()) 11 print("File contains %s calendar(s)", len(stream.calendars)) 12``` 13 14You can encode a calendar stream as ics content calling the `ics()` method on 15the `IcsCalendarStream`: 16 17```python 18from pathlib import Path 19 20filename = Path("/tmp/output.ics") 21with filename.open(mode="w") as ics_file: 22 ics_file.write(stream.ics()) 23``` 24 25Fetching a remote ics file without blocking the event loop requires the 26optional `ical[async]` extra (which installs `aiohttp`): 27 28```python 29from ical.calendar_stream import IcsCalendarStream 30 31stream = await IcsCalendarStream.from_url("https://example.com/calendar.ics") 32``` 33 34""" 35 36# ty: allow-any-generics 37 38from __future__ import annotations 39 40import logging 41from typing import TYPE_CHECKING 42 43from pydantic import Field, field_serializer 44 45from .calendar import Calendar 46from .component import ComponentModel 47from .parsing.component import encode_content, parse_content 48from .types.data_types import serialize_field 49from .exceptions import CalendarFetchError, CalendarParseError 50from pydantic import ConfigDict 51 52if TYPE_CHECKING: 53 import aiohttp 54 55_LOGGER = logging.getLogger(__name__) 56 57__all__ = ["CalendarStream", "IcsCalendarStream"] 58 59_ASYNC_EXTRA_ERROR = ( 60 "The aiohttp library is required for async URL fetching. Install it " 61 "with the optional extra: `pip install ical[async]`" 62) 63 64 65class CalendarStream(ComponentModel): 66 """A container that is a collection of calendaring information. 67 68 This object supports parsing an rfc5545 calendar file, but does not 69 support encoding. See `IcsCalendarStream` instead for encoding ics files. 70 """ 71 72 calendars: list[Calendar] = Field(alias="vcalendar", default_factory=list) 73 74 @classmethod 75 def from_ics(cls, content: str) -> "CalendarStream": 76 """Factory method to create a new instance from an rfc5545 iCalendar content.""" 77 components = parse_content(content) 78 result: dict[str, list] = {"vcalendar": []} 79 for component in components: 80 result.setdefault(component.name, []) 81 result[component.name].append(component.as_dict()) 82 _LOGGER.debug("Parsing object %s", result) 83 return cls(**result) 84 85 @classmethod 86 async def from_url( 87 cls, url: str, session: "aiohttp.ClientSession | None" = None 88 ) -> "CalendarStream": 89 """Async factory method to fetch and parse an rfc5545 iCalendar url. 90 91 This avoids blocking the event loop while fetching a remote ics file 92 (e.g. a calendar subscription url), which is useful for applications 93 such as Home Assistant that are built on asyncio. Parsing the fetched 94 content happens synchronously since it is fast and CPU-bound. 95 96 This requires the optional `aiohttp` dependency, installable with the 97 `ical[async]` extra. An existing `aiohttp.ClientSession` may be 98 passed in to reuse connection pooling; otherwise a session is created 99 and closed automatically for this single request. 100 """ 101 try: 102 import aiohttp # noqa: PLC0415 103 except ImportError as err: 104 raise ImportError(_ASYNC_EXTRA_ERROR) from err 105 106 async def _fetch(active_session: aiohttp.ClientSession) -> str: 107 try: 108 async with active_session.get(url) as response: 109 response.raise_for_status() 110 return await response.text() 111 except aiohttp.ClientError as err: 112 raise CalendarFetchError( 113 f"Failed to fetch calendar from url {url!r}: {err}" 114 ) from err 115 116 if session is not None: 117 content = await _fetch(session) 118 else: 119 async with aiohttp.ClientSession() as owned_session: 120 content = await _fetch(owned_session) 121 return cls.from_ics(content) 122 123 def ics(self) -> str: 124 """Encode the calendar stream as an rfc5545 iCalendar Stream content.""" 125 return encode_content(self.__encode_component_root__().components) 126 127 128class IcsCalendarStream(CalendarStream): 129 """A calendar stream that supports parsing and encoding ICS.""" 130 131 @classmethod 132 def calendar_from_ics(cls, content: str) -> Calendar: 133 """Load a single calendar from an ics string.""" 134 stream = cls.from_ics(content) 135 return cls._single_calendar(stream) 136 137 @classmethod 138 async def calendar_from_url( 139 cls, url: str, session: "aiohttp.ClientSession | None" = None 140 ) -> Calendar: 141 """Async convenience method to fetch and load a single calendar from a url. 142 143 See `from_url` for details on the optional `ical[async]` dependency 144 and the `session` argument. 145 """ 146 stream = await cls.from_url(url, session=session) 147 return cls._single_calendar(stream) 148 149 @staticmethod 150 def _single_calendar(stream: "CalendarStream") -> Calendar: 151 """Return the single calendar in the stream, or raise an error.""" 152 if len(stream.calendars) == 1: 153 return stream.calendars[0] 154 if len(stream.calendars) == 0: 155 return Calendar() 156 raise CalendarParseError("Calendar Stream had more than one calendar") 157 158 @classmethod 159 def calendar_to_ics(cls, calendar: Calendar) -> str: 160 """Serialize a calendar as an ICS stream.""" 161 stream = cls(vcalendar=[calendar]) 162 return stream.ics() 163 164 model_config = ConfigDict( 165 validate_assignment=True, 166 populate_by_name=True, 167 ) 168 serialize_fields = field_serializer("*")(serialize_field) # type: ignore[pydantic-field]
66class CalendarStream(ComponentModel): 67 """A container that is a collection of calendaring information. 68 69 This object supports parsing an rfc5545 calendar file, but does not 70 support encoding. See `IcsCalendarStream` instead for encoding ics files. 71 """ 72 73 calendars: list[Calendar] = Field(alias="vcalendar", default_factory=list) 74 75 @classmethod 76 def from_ics(cls, content: str) -> "CalendarStream": 77 """Factory method to create a new instance from an rfc5545 iCalendar content.""" 78 components = parse_content(content) 79 result: dict[str, list] = {"vcalendar": []} 80 for component in components: 81 result.setdefault(component.name, []) 82 result[component.name].append(component.as_dict()) 83 _LOGGER.debug("Parsing object %s", result) 84 return cls(**result) 85 86 @classmethod 87 async def from_url( 88 cls, url: str, session: "aiohttp.ClientSession | None" = None 89 ) -> "CalendarStream": 90 """Async factory method to fetch and parse an rfc5545 iCalendar url. 91 92 This avoids blocking the event loop while fetching a remote ics file 93 (e.g. a calendar subscription url), which is useful for applications 94 such as Home Assistant that are built on asyncio. Parsing the fetched 95 content happens synchronously since it is fast and CPU-bound. 96 97 This requires the optional `aiohttp` dependency, installable with the 98 `ical[async]` extra. An existing `aiohttp.ClientSession` may be 99 passed in to reuse connection pooling; otherwise a session is created 100 and closed automatically for this single request. 101 """ 102 try: 103 import aiohttp # noqa: PLC0415 104 except ImportError as err: 105 raise ImportError(_ASYNC_EXTRA_ERROR) from err 106 107 async def _fetch(active_session: aiohttp.ClientSession) -> str: 108 try: 109 async with active_session.get(url) as response: 110 response.raise_for_status() 111 return await response.text() 112 except aiohttp.ClientError as err: 113 raise CalendarFetchError( 114 f"Failed to fetch calendar from url {url!r}: {err}" 115 ) from err 116 117 if session is not None: 118 content = await _fetch(session) 119 else: 120 async with aiohttp.ClientSession() as owned_session: 121 content = await _fetch(owned_session) 122 return cls.from_ics(content) 123 124 def ics(self) -> str: 125 """Encode the calendar stream as an rfc5545 iCalendar Stream content.""" 126 return encode_content(self.__encode_component_root__().components)
A container that is a collection of calendaring information.
This object supports parsing an rfc5545 calendar file, but does not
support encoding. See IcsCalendarStream instead for encoding ics files.
75 @classmethod 76 def from_ics(cls, content: str) -> "CalendarStream": 77 """Factory method to create a new instance from an rfc5545 iCalendar content.""" 78 components = parse_content(content) 79 result: dict[str, list] = {"vcalendar": []} 80 for component in components: 81 result.setdefault(component.name, []) 82 result[component.name].append(component.as_dict()) 83 _LOGGER.debug("Parsing object %s", result) 84 return cls(**result)
Factory method to create a new instance from an rfc5545 iCalendar content.
86 @classmethod 87 async def from_url( 88 cls, url: str, session: "aiohttp.ClientSession | None" = None 89 ) -> "CalendarStream": 90 """Async factory method to fetch and parse an rfc5545 iCalendar url. 91 92 This avoids blocking the event loop while fetching a remote ics file 93 (e.g. a calendar subscription url), which is useful for applications 94 such as Home Assistant that are built on asyncio. Parsing the fetched 95 content happens synchronously since it is fast and CPU-bound. 96 97 This requires the optional `aiohttp` dependency, installable with the 98 `ical[async]` extra. An existing `aiohttp.ClientSession` may be 99 passed in to reuse connection pooling; otherwise a session is created 100 and closed automatically for this single request. 101 """ 102 try: 103 import aiohttp # noqa: PLC0415 104 except ImportError as err: 105 raise ImportError(_ASYNC_EXTRA_ERROR) from err 106 107 async def _fetch(active_session: aiohttp.ClientSession) -> str: 108 try: 109 async with active_session.get(url) as response: 110 response.raise_for_status() 111 return await response.text() 112 except aiohttp.ClientError as err: 113 raise CalendarFetchError( 114 f"Failed to fetch calendar from url {url!r}: {err}" 115 ) from err 116 117 if session is not None: 118 content = await _fetch(session) 119 else: 120 async with aiohttp.ClientSession() as owned_session: 121 content = await _fetch(owned_session) 122 return cls.from_ics(content)
Async factory method to fetch and parse an rfc5545 iCalendar url.
This avoids blocking the event loop while fetching a remote ics file (e.g. a calendar subscription url), which is useful for applications such as Home Assistant that are built on asyncio. Parsing the fetched content happens synchronously since it is fast and CPU-bound.
This requires the optional aiohttp dependency, installable with the
ical[async] extra. An existing aiohttp.ClientSession may be
passed in to reuse connection pooling; otherwise a session is created
and closed automatically for this single request.
129class IcsCalendarStream(CalendarStream): 130 """A calendar stream that supports parsing and encoding ICS.""" 131 132 @classmethod 133 def calendar_from_ics(cls, content: str) -> Calendar: 134 """Load a single calendar from an ics string.""" 135 stream = cls.from_ics(content) 136 return cls._single_calendar(stream) 137 138 @classmethod 139 async def calendar_from_url( 140 cls, url: str, session: "aiohttp.ClientSession | None" = None 141 ) -> Calendar: 142 """Async convenience method to fetch and load a single calendar from a url. 143 144 See `from_url` for details on the optional `ical[async]` dependency 145 and the `session` argument. 146 """ 147 stream = await cls.from_url(url, session=session) 148 return cls._single_calendar(stream) 149 150 @staticmethod 151 def _single_calendar(stream: "CalendarStream") -> Calendar: 152 """Return the single calendar in the stream, or raise an error.""" 153 if len(stream.calendars) == 1: 154 return stream.calendars[0] 155 if len(stream.calendars) == 0: 156 return Calendar() 157 raise CalendarParseError("Calendar Stream had more than one calendar") 158 159 @classmethod 160 def calendar_to_ics(cls, calendar: Calendar) -> str: 161 """Serialize a calendar as an ICS stream.""" 162 stream = cls(vcalendar=[calendar]) 163 return stream.ics() 164 165 model_config = ConfigDict( 166 validate_assignment=True, 167 populate_by_name=True, 168 ) 169 serialize_fields = field_serializer("*")(serialize_field) # type: ignore[pydantic-field]
A calendar stream that supports parsing and encoding ICS.
132 @classmethod 133 def calendar_from_ics(cls, content: str) -> Calendar: 134 """Load a single calendar from an ics string.""" 135 stream = cls.from_ics(content) 136 return cls._single_calendar(stream)
Load a single calendar from an ics string.
138 @classmethod 139 async def calendar_from_url( 140 cls, url: str, session: "aiohttp.ClientSession | None" = None 141 ) -> Calendar: 142 """Async convenience method to fetch and load a single calendar from a url. 143 144 See `from_url` for details on the optional `ical[async]` dependency 145 and the `session` argument. 146 """ 147 stream = await cls.from_url(url, session=session) 148 return cls._single_calendar(stream)
Async convenience method to fetch and load a single calendar from a url.
See from_url for details on the optional ical[async] dependency
and the session argument.
159 @classmethod 160 def calendar_to_ics(cls, calendar: Calendar) -> str: 161 """Serialize a calendar as an ICS stream.""" 162 stream = cls(vcalendar=[calendar]) 163 return stream.ics()
Serialize a calendar as an ICS stream.
293def serialize_field(self: BaseModel, value: Any, info: SerializationInfo) -> Any: 294 if not info.context or not info.context.get("ics"): 295 return value 296 if isinstance(value, list): 297 res = [] 298 for val in value: 299 for base in val.__class__.__mro__[:-1]: 300 if (func := DATA_TYPE.encode_property_json.get(base)) is not None: 301 res.append(func(val)) 302 break 303 else: 304 res.append(val) 305 return res 306 307 for base in value.__class__.__mro__[:-1]: 308 if (func := DATA_TYPE.encode_property_json.get(base)) is not None: 309 return func(value) 310 return value