from datetime import tzinfo
from typing import ClassVar, Iterable, Iterator, List, Optional, Union, overload
import attr
from attr.validators import instance_of
from ics.component import Component
from ics.contentline import Container, lines_to_containers, string_to_containers
from ics.event import Event
from ics.timeline import Timeline
from ics.timespan import Normalization, NormalizationAction
from ics.todo import Todo
@attr.s
class CalendarAttrs(Component):
version: str = attr.ib(
validator=instance_of(str), metadata={"ics_priority": 1000}
) # default set by Calendar.DEFAULT_VERSION
prodid: str = attr.ib(
validator=instance_of(str), metadata={"ics_priority": 900}
) # default set by Calendar.DEFAULT_PRODID
scale: Optional[str] = attr.ib(default=None, metadata={"ics_priority": 800})
method: Optional[str] = attr.ib(default=None, metadata={"ics_priority": 700})
# CalendarTimezoneConverter has priority 600
events: List[Event] = attr.ib(
factory=list, converter=list, metadata={"ics_priority": -100}
)
todos: List[Todo] = attr.ib(
factory=list, converter=list, metadata={"ics_priority": -200}
)
[docs]
class Calendar(CalendarAttrs):
"""
Represents a unique RFC 5545 iCalendar.
Attributes:
events: a list of `Event` contained in the Calendar
todos: a list of `Todo` contained in the Calendar
timeline: a `Timeline` instance for iterating this Calendar in chronological order
"""
NAME = "VCALENDAR"
DEFAULT_VERSION: ClassVar[str] = "2.0"
DEFAULT_PRODID: ClassVar[str] = "ics.py 0.8.0.dev0 - http://git.io/lLljaA"
[docs]
def __init__(
self,
imports: Union[str, Container, None] = None,
events: Optional[Iterable[Event]] = None,
todos: Optional[Iterable[Todo]] = None,
creator: str = None,
**kwargs,
):
"""Initializes a new Calendar.
Args:
imports (**str**): data to be imported into the Calendar,
events (**Iterable[Event]**): `Event` to be added to the calendar
todos (**Iterable[Todo]**): `Todo` to be added to the calendar
creator (**string**): uid of the creator program.
"""
if events is None:
events = tuple()
if todos is None:
todos = tuple()
kwargs.setdefault("version", self.DEFAULT_VERSION)
kwargs.setdefault(
"prodid", creator if creator is not None else self.DEFAULT_PRODID
)
super().__init__(events=events, todos=todos, **kwargs) # type: ignore[arg-type]
self.timeline = Timeline(self, None)
if imports is not None:
if isinstance(imports, Container):
self.populate(imports)
else:
if isinstance(imports, str):
containers = iter(string_to_containers(imports))
else:
containers = iter(lines_to_containers(imports))
try:
container = next(containers)
if not isinstance(container, Container):
raise ValueError(f"can't populate from {type(container)}")
self.populate(container)
except StopIteration:
raise ValueError("string didn't contain any ics data")
try:
next(containers)
raise ValueError(
"Multiple calendars in one file are not supported by this method."
"Use ics.Calendar.parse_multiple()"
)
except StopIteration:
pass
@property
def creator(self) -> str:
return self.prodid
@creator.setter
def creator(self, value: str):
self.prodid = value
[docs]
@classmethod
def parse_multiple(cls, string):
""" "
Parses an input string that may contain multiple calendars
and returns a list of :class:`ics.event.Calendar`
"""
containers = string_to_containers(string)
return [cls(imports=c) for c in containers]
@overload
def normalize(self, normalization: Normalization):
...
@overload
def normalize(
self,
value: tzinfo,
normalize_floating: NormalizationAction,
normalize_with_tz: NormalizationAction,
):
...
def normalize(self, normalization, *args, **kwargs):
if isinstance(normalization, Normalization):
if args or kwargs:
raise ValueError(
"can't pass args or kwargs when a complete Normalization is given"
)
else:
normalization = Normalization(normalization, *args, **kwargs)
self.events = [
e if e.all_day else normalization.normalize(e) for e in self.events
]
self.todos = [
e if e.all_day else normalization.normalize(e) for e in self.todos
]
[docs]
def __str__(self) -> str:
return "<Calendar with {} event{} and {} todo{}>".format(
len(self.events),
"" if len(self.events) == 1 else "s",
len(self.todos),
"" if len(self.todos) == 1 else "s",
)
[docs]
def __iter__(self) -> Iterator[str]:
"""Returns:
iterable: an iterable version of __str__, line per line
(with line-endings).
Example:
Can be used to write calendar to a file:
>>> c = Calendar(); c.events.append(Event(summary="My cool event"))
>>> open('my.ics', 'w').writelines(c)
"""
return iter(self.serialize().splitlines(keepends=True))