def _serialize_attrs(self, component: Component, context: ContextDict, container: Container): assert isinstance(component, Calendar) context.setdefault(DatetimeConverterMixin.CONTEXT_KEY_AVAILABLE_TZ, {}) super()._serialize_attrs(component, context, container) # serialize all used timezones timezones = [tz.to_container() for tz in context[DatetimeConverterMixin.CONTEXT_KEY_AVAILABLE_TZ].values()] # insert them at the place where they usually would have been serialized split = context["VTIMEZONES_AFTER"] container.data = container.data[:split] + timezones + container.data[split:]
def post_populate(self, component: Component, context: ContextDict): lines_str = "".join( line.serialize(newline=True) for line in context.pop((self, "lines"))) # TODO only feed dateutil the params it likes, add the rest as extra tzinfos = context.get(DatetimeConverterMixin.CONTEXT_KEY_AVAILABLE_TZ, {}) rrule = dateutil.rrule.rrulestr(lines_str, tzinfos=tzinfos, compatible=True) rrule._rdate = list(unique_justseen(sorted(rrule._rdate))) rrule._exdate = list(unique_justseen(sorted(rrule._exdate))) self.set_or_append_value(component, rrule)
def post_populate(self, component: Component, context: ContextDict): timespan_type = getattr(component, "_TIMESPAN_TYPE", self.value_type) timespan = timespan_type(ensure_datetime(context[CONTEXT_BEGIN_TIME]), ensure_datetime(context[CONTEXT_END_TIME]), context[CONTEXT_DURATION], context[CONTEXT_PRECISION]) # missing values will be reported by the Timespan validator if context[CONTEXT_END_NAME] and context[ CONTEXT_END_NAME] != timespan._end_name(): raise ValueError("expected to get %s value, but got %s instead" % (timespan._end_name(), context[CONTEXT_END_NAME])) self.set_or_append_value(component, timespan) # we need to clear all values, otherwise they might not get overwritten by the next parsed Timespan for key in CONTEXT_KEYS: context.pop(key, None)
def finalize(self, component: "Component", context: ContextDict): self._check_component(component, context) # missing values will be reported by the Timespan validator timespan = self.value_type( ensure_datetime(context[CONTEXT_BEGIN_TIME]), ensure_datetime(context[CONTEXT_END_TIME]), context[CONTEXT_DURATION], context[CONTEXT_PRECISION]) if context[CONTEXT_END_NAME] and context[ CONTEXT_END_NAME] != timespan._end_name(): raise ValueError("expected to get %s value, but got %s instead" % (timespan._end_name(), context[CONTEXT_END_NAME])) self.set_or_append_value(component, timespan) super(TimespanConverter, self).finalize(component, context) # we need to clear all values, otherwise they might not get overwritten by the next parsed Timespan for key in CONTEXT_KEYS: context.pop(key, None)
def serialize_toplevel(self, component: "Component", context: Optional[ContextDict] = None): if not context: context = ContextDict(defaultdict(lambda: None)) container = Container(self.container_name) for conv in self.converters: conv.serialize(component, container, context) container.extend(component.extra) return container
def populate(self, component: Component, item: ContainerItem, context: ContextDict) -> bool: assert isinstance(item, ContentLine) seen_items = context.setdefault(CONTEXT_ITEMS, set()) if item.name in seen_items: raise ValueError("duplicate value for %s in %s" % (item.name, item)) seen_items.add(item.name) params = copy_extra_params(item.params) if item.name in ["DTSTART", "DTEND", "DUE"]: value_type = params.pop("VALUE", ["DATE-TIME"]) if value_type == ["DATE-TIME"]: precision = "second" value = DatetimeConverter.parse(item.value, params, context) elif value_type == ["DATE"]: precision = "day" value = DateConverter.parse(item.value, params, context) else: raise ValueError("can't handle %s with value type %s" % (item.name, value_type)) if context.setdefault(CONTEXT_PRECISION, precision) != precision: raise ValueError( "event with diverging begin and end time precision") if item.name == "DTSTART": self.set_or_append_extra_params(component, params, name="begin") context[CONTEXT_BEGIN_TIME] = value else: end_name = {"DTEND": "end", "DUE": "due"}[item.name] context[CONTEXT_END_NAME] = end_name self.set_or_append_extra_params(component, params, name=end_name) context[CONTEXT_END_TIME] = value else: assert item.name == "DURATION" self.set_or_append_extra_params(component, params, name="duration") context[CONTEXT_DURATION] = DurationConverter.parse( item.value, params, context) return True
def _populate_attrs(self, instance: Component, container: Container, context: ContextDict): assert isinstance(instance, Calendar) avail_tz: Dict[str, Timezone] = context.setdefault(DatetimeConverterMixin.CONTEXT_KEY_AVAILABLE_TZ, {}) for child in container: if child.name == Timezone.NAME and isinstance(child, Container): tz = Timezone.from_container(child) avail_tz.setdefault(tz.tzid, tz) super()._populate_attrs(instance, container, context)
def serialize_toplevel(self, component: Component, context: Optional[ContextDict] = None): check_is_instance("instance", component, self.component_type) if not context: context = ContextDict(defaultdict(lambda: None)) container = Container( component.extra.name ) # allow overwriting the name by setting the name of the extras self._serialize_attrs(component, context, container) return container
def populate_instance(self, instance: Component, container: Container, context: Optional[ContextDict] = None): if container.name != self.component_type.NAME: raise ValueError("container {} is no {}".format( container.name, self.component_type.NAME)) check_is_instance("instance", instance, (self.component_type, MutablePseudoComponent)) if not context: context = ContextDict(defaultdict(lambda: None)) self._populate_attrs(instance, container, context)
def _serialize_dt(self, value: datetime, params: ExtraParams, context: ContextDict, utc_fmt="%Y%m%dT%H%M%SZ", nonutc_fmt="%Y%m%dT%H%M%S") -> str: if is_utc(value): return value.strftime(utc_fmt) else: if value.tzinfo is not None: tzname = value.tzinfo.tzname(value) if not tzname: # TODO generate unique identifier as name raise ValueError("could not generate name for tzinfo %s" % value.tzinfo) params["TZID"] = [tzname] available_tz = context.setdefault(self.CONTEXT_KEY_AVAILABLE_TZ, {}) available_tz.setdefault(tzname, value.tzinfo) return value.strftime(nonutc_fmt)
def populate_instance(self, instance: "Component", container: Container, context: Optional[ContextDict] = None): if container.name != self.container_name: raise ValueError("container isn't an {}".format(self.container_name)) if not context: context = ContextDict(defaultdict(lambda: None)) for line in container: consumed = False for conv in self.converter_lookup[line.name]: if conv.populate(instance, line, context): consumed = True if not consumed: instance.extra.append(line) for conv in self.converters: conv.finalize(instance, context)
def _serialize_dt(self, value: datetime, params: ExtraParams, context: ContextDict, utc_fmt="%Y%m%dT%H%M%SZ", nonutc_fmt="%Y%m%dT%H%M%S") -> str: if is_utc(value): return value.strftime(utc_fmt) if value.tzinfo is not None: tz = Timezone.from_tzinfo(value.tzinfo, context) if tz is not None: params["TZID"] = [tz.tzid] available_tz = context.setdefault( self.CONTEXT_KEY_AVAILABLE_TZ, {}) available_tz.setdefault(tz.tzid, tz) return value.strftime(nonutc_fmt)
def _parse_dt(self, value: str, params: ExtraParams, context: ContextDict, warn_no_avail_tz=True) -> datetime: param_tz_list: Optional[List[str]] = params.pop( "TZID", None) # we remove the TZID from context if param_tz_list: if len(param_tz_list) > 1: raise ValueError("got multiple TZIDs") param_tz: Optional[str] = param_tz_list[0] else: param_tz = None available_tz = context.get(self.CONTEXT_KEY_AVAILABLE_TZ, None) if available_tz is None and warn_no_avail_tz: warnings.warn( "DatetimeConverterMixin.parse called without available_tz dict in context" ) fixed_utc = (value[-1].upper() == 'Z') value = value.translate({ ord("/"): "", ord("-"): "", ord("Z"): "", ord("z"): "" }) dt = datetime.strptime(value, self.FORMATS[len(value)]) if fixed_utc: if param_tz: raise ValueError( "can't specify UTC via appended 'Z' and TZID param '%s'" % param_tz) return dt.replace(tzinfo=dateutil_tzutc) elif param_tz: selected_tz = None if available_tz: selected_tz = available_tz.get(param_tz, None) if selected_tz is None: selected_tz = gettz( param_tz) # be lenient with missing vtimezone definitions return dt.replace(tzinfo=selected_tz) else: return dt
def post_serialize(self, component: Component, output: Container, context: ContextDict): context.pop("DTSTART", None)
def _parse_dt(self, value: str, params: ExtraParams, context: ContextDict, warn_no_avail_tz=True) -> datetime: param_tz_list: Optional[List[str]] = params.pop( "TZID", None) # we remove the TZID from context if param_tz_list: if len(param_tz_list) > 1: raise ValueError("got multiple TZIDs") param_tz: Optional[str] = str( param_tz_list[0]) # convert QuotedParamValues else: param_tz = None available_tz = context.setdefault(self.CONTEXT_KEY_AVAILABLE_TZ, {}) if available_tz is None and warn_no_avail_tz: warnings.warn( "DatetimeConverterMixin.parse called without available_tz dict in context" ) fixed_utc = (value[-1].upper() == 'Z') tr_value = value.translate({ ord("/"): "", ord("-"): "", ord("Z"): "", ord("z"): "" }) try: format = self.FORMATS[len(tr_value)] except KeyError: raise ValueError( "couldn't find format matching %r (%s chars), tried %s" % (tr_value, len(tr_value), self.FORMATS)) dt = datetime.strptime(tr_value, format) if fixed_utc: if param_tz: raise ValueError( "can't specify UTC via appended 'Z' and TZID param '%s'" % param_tz) return dt.replace(tzinfo=dateutil_tzutc) elif param_tz: selected_tz = None if available_tz: selected_tz = available_tz.get(param_tz, None) if selected_tz is None: try: selected_tz = Timezone.from_tzid( param_tz ) # be lenient with missing vtimezone definitions except ValueError: found_tz = gettz(param_tz) import platform import sys if not found_tz: raise ValueError( "timezone %s is unknown on this system (%s on %s)" % (param_tz, sys.version, platform.platform(terse=True))) else: warnings.warn( "no ics representation available for timezone %s, but known to system (%s on %s) as %s " % (param_tz, sys.version, platform.platform(terse=True), found_tz)) selected_tz = found_tz available_tz.setdefault(param_tz, selected_tz) return dt.replace(tzinfo=selected_tz) else: return dt