def initialize_converters(): # order is very important here: # 1) all simple value type converters import ics.valuetype.base import ics.valuetype.generic import ics.valuetype.text import ics.valuetype.datetime import ics.valuetype.special # 2) all relatively simple attribute converters and advanced component converters import ics.converter.base import ics.converter.value import ics.converter.types.timespan import ics.converter.types.various # 3) converters for all remaining component subclasses from ics.converter.component import ComponentMeta import ics.converter.types.timezone # vTimezone is a Component ComponentMeta.BY_TYPE[Event] = ComponentMeta(Event) ComponentMeta.BY_TYPE[Todo] = ComponentMeta(Todo) # 4) the converter for the calendar import ics.converter.types.calendar global initialize_converters initialize_converters = lambda: None
class Event(EventAttrs): """A calendar event. Can be full-day or between two instants. Can be defined by a beginning instant and a duration *or* end instant. Unsupported event attributes can be found in `event.extra`, a :class:`ics.parse.Container`. You may add some by appending a :class:`ics.parse.ContentLine` to `.extra` """ _timespan: EventTimespan = attr.ib(validator=instance_of(EventTimespan)) Meta = ComponentMeta("VEVENT") def __init__( self, summary: str = None, begin: DatetimeLike = None, end: DatetimeLike = None, duration: TimedeltaLike = None, *args, **kwargs ): """Initializes a new :class:`ics.event.Event`. Raises: ValueError: if `timespan` and any of `begin`, `end` or `duration` are specified at the same time, or if validation of the timespan fails (see :method:`ics.timespan.Timespan.validate`). """ if (begin is not None or end is not None or duration is not None) and "timespan" in kwargs: raise ValueError("can't specify explicit timespan together with any of begin, end or duration") kwargs.setdefault("timespan", EventTimespan(ensure_datetime(begin), ensure_datetime(end), ensure_timedelta(duration))) super(Event, self).__init__(kwargs.pop("timespan"), summary, *args, **kwargs)
class Attendee(Person): rsvp: Optional[bool] = attr.ib(default=None) role: Optional[str] = attr.ib(default=None) partstat: Optional[str] = attr.ib(default=None) cutype: Optional[str] = attr.ib(default=None) Meta = ComponentMeta("ATTENDEE")
class Person(object): email: str = attr.ib() common_name: str = attr.ib(default=None) dir: Optional[str] = attr.ib(default=None) sent_by: Optional[str] = attr.ib(default=None) extra: Dict[str, List[str]] = attr.ib(factory=dict) Meta = ComponentMeta("ABSTRACT-PERSON")
class Todo(TodoAttrs): """A todo list entry. Can have a start time and duration, or start and due time, or only start or due time. """ _timespan: TodoTimespan = attr.ib(validator=instance_of(TodoTimespan)) Meta = ComponentMeta("VTODO") def __init__(self, begin: DatetimeLike = None, due: DatetimeLike = None, duration: TimedeltaLike = None, *args, **kwargs): if (begin is not None or due is not None or duration is not None) and "timespan" in kwargs: raise ValueError( "can't specify explicit timespan together with any of begin, due or duration" ) kwargs.setdefault( "timespan", TodoTimespan(ensure_datetime(begin), ensure_datetime(due), ensure_timedelta(duration))) super(Todo, self).__init__(kwargs.pop("timespan"), *args, **kwargs) #################################################################################################################### def convert_due(self, representation): if representation == "due": representation = "end" super(Todo, self).convert_end(representation) due = property(TodoAttrs.end.fget, TodoAttrs.end.fset) # convert_due = TodoAttrs.convert_end # see above due_representation = property(TodoAttrs.end_representation.fget) has_explicit_due = property(TodoAttrs.has_explicit_end.fget) due_within = TodoAttrs.ends_within end = property(deprecated_due(TodoAttrs.end.fget), deprecated_due(TodoAttrs.end.fset)) convert_end = deprecated_due(TodoAttrs.convert_end) end_representation = property( deprecated_due(TodoAttrs.end_representation.fget)) has_explicit_end = property(deprecated_due( TodoAttrs.has_explicit_end.fget)) ends_within = deprecated_due(TodoAttrs.ends_within)
class BaseAlarm(Component, metaclass=ABCMeta): """ A calendar event VALARM base class """ Meta = ComponentMeta("VALARM", converter_class=AlarmConverter) trigger: Union[timedelta, datetime, None] = attr.ib( default=None, validator=v_optional(instance_of( (timedelta, datetime)))) # TODO is this relative to begin or end? repeat: int = attr.ib(default=None, validator=call_validate_on_inst) duration: timedelta = attr.ib( default=None, converter=c_optional(ensure_timedelta), validator=call_validate_on_inst) # type: ignore # FIXME: `attach` can be specified multiple times in a "VEVENT", "VTODO", "VJOURNAL", or "VALARM" calendar component # with the exception of AUDIO alarm that only allows this property to occur once. # (This property is used in "VALARM" calendar components to specify an audio sound resource or an email message attachment.) def validate(self, attr=None, value=None): if self.repeat is not None: if self.repeat < 0: raise ValueError("Repeat must be great than or equal to 0.") if self.duration is None: raise ValueError( "A definition of an alarm with a repeating trigger MUST include both the DURATION and REPEAT properties." ) if self.duration is not None and self.duration.total_seconds() < 0: raise ValueError("Alarm duration timespan must be positive.") @property @abstractmethod def action(self): """ VALARM action to be implemented by concrete classes """ ...
class Component(RuntimeAttrValidation): Meta: ClassVar[ComponentMeta] = ComponentMeta("ABSTRACT-COMPONENT") extra: Container = attr.ib(init=False, default=PLACEHOLDER_CONTAINER, validator=instance_of(Container), metadata={"ics_ignore": True}) extra_params: ComponentExtraParams = attr.ib(init=False, factory=dict, validator=instance_of(dict), metadata={"ics_ignore": True}) def __attrs_post_init__(self): super(Component, self).__attrs_post_init__() if self.extra is PLACEHOLDER_CONTAINER: self.extra = Container(self.Meta.container_name) def __init_subclass__(cls): super().__init_subclass__() cls.Meta.inflate(cls) @classmethod def from_container(cls: Type[ComponentType], container: Container) -> ComponentType: return cls.Meta.load_instance(container) def populate(self, container: Container): self.Meta.populate_instance(self, container) def to_container(self) -> Container: return self.Meta.serialize_toplevel(self) def serialize(self) -> str: return self.to_container().serialize() def strip_extras(self, all_extras=False, extra_properties=None, extra_params=None, property_merging=None): if extra_properties is None: extra_properties = all_extras if extra_params is None: extra_params = all_extras if property_merging is None: property_merging = all_extras if not any([extra_properties, extra_params, property_merging]): raise ValueError("need to strip at least one thing") if extra_properties: self.extra.clear() if extra_params: self.extra_params.clear() elif property_merging: for val in self.extra_params.values(): if not isinstance(val, list): continue for v in val: v.pop("__merge_next", None) def clone(self): """Returns an exact (shallow) copy of self""" # TODO deep copies? return attr.evolve(self)
class Calendar(CalendarAttrs): """ Represents an unique RFC 5545 iCalendar. Attributes: events: a list of `Event`s contained in the Calendar todos: a list of `Todo`s contained in the Calendar timeline: a `Timeline` instance for iterating this Calendar in chronological order """ Meta = ComponentMeta("VCALENDAR") DEFAULT_VERSION: ClassVar[str] = "2.0" DEFAULT_PRODID: ClassVar[str] = "ics.py - http://git.io/lLljaA" 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`s to be added to the calendar todos (**Iterable[Todo]**): `Todo`s 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(Calendar, self).__init__(events=events, todos=todos, **kwargs) # type: ignore self.timeline = Timeline(self, None) if imports is not None: if isinstance(imports, Container): self.populate(imports) else: containers = iter(string_to_containers(imports)) try: container = next(containers) if not isinstance(container, Container): raise ValueError("can't populate from %s" % 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 @classmethod def parse_multiple(cls, string): """" Parses an input string that may contain mutiple calendars and retruns a list of :class:`ics.event.Calendar` """ containers = string_to_containers(string) return [cls(imports=c) for c in containers] def __str__(self) -> str: return "<Calendar with {} event{} and {} todo{}>".format( len(self.events), "s" if len(self.events) > 1 else "", len(self.todos), "s" if len(self.todos) > 1 else "") 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))
class Organizer(Person): Meta = ComponentMeta("ORGANIZER")