kopia lustrzana https://github.com/collective/icalendar
Merge pull request #789 from niccokunzmann/ruff-formmat
Format the source code with ruffpull/787/head^2
commit
4cbac76190
|
@ -6,7 +6,7 @@ Changelog
|
|||
|
||||
Minor changes:
|
||||
|
||||
- ...
|
||||
- Use ``ruff`` to format the source code.
|
||||
|
||||
Breaking changes:
|
||||
|
||||
|
|
110
bootstrap.py
110
bootstrap.py
|
@ -27,7 +27,7 @@ from optparse import OptionParser
|
|||
|
||||
tmpeggs = tempfile.mkdtemp()
|
||||
|
||||
usage = '''\
|
||||
usage = """\
|
||||
[DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options]
|
||||
|
||||
Bootstraps a buildout-based project.
|
||||
|
@ -37,28 +37,40 @@ Python that you want bin/buildout to use.
|
|||
|
||||
Note that by using --find-links to point to local resources, you can keep
|
||||
this script from going over the network.
|
||||
'''
|
||||
"""
|
||||
|
||||
parser = OptionParser(usage=usage)
|
||||
parser.add_option("-v", "--version", help="use a specific zc.buildout version")
|
||||
|
||||
parser.add_option("-t", "--accept-buildout-test-releases",
|
||||
dest='accept_buildout_test_releases',
|
||||
action="store_true", default=False,
|
||||
help=("Normally, if you do not specify a --version, the "
|
||||
"bootstrap script and buildout gets the newest "
|
||||
"*final* versions of zc.buildout and its recipes and "
|
||||
"extensions for you. If you use this flag, "
|
||||
"bootstrap and buildout will get the newest releases "
|
||||
"even if they are alphas or betas."))
|
||||
parser.add_option("-c", "--config-file",
|
||||
help=("Specify the path to the buildout configuration "
|
||||
"file to be used."))
|
||||
parser.add_option("-f", "--find-links",
|
||||
help=("Specify a URL to search for buildout releases"))
|
||||
parser.add_option("--allow-site-packages",
|
||||
action="store_true", default=False,
|
||||
help=("Let bootstrap.py use existing site packages"))
|
||||
parser.add_option(
|
||||
"-t",
|
||||
"--accept-buildout-test-releases",
|
||||
dest="accept_buildout_test_releases",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=(
|
||||
"Normally, if you do not specify a --version, the "
|
||||
"bootstrap script and buildout gets the newest "
|
||||
"*final* versions of zc.buildout and its recipes and "
|
||||
"extensions for you. If you use this flag, "
|
||||
"bootstrap and buildout will get the newest releases "
|
||||
"even if they are alphas or betas."
|
||||
),
|
||||
)
|
||||
parser.add_option(
|
||||
"-c",
|
||||
"--config-file",
|
||||
help=("Specify the path to the buildout configuration " "file to be used."),
|
||||
)
|
||||
parser.add_option(
|
||||
"-f", "--find-links", help=("Specify a URL to search for buildout releases")
|
||||
)
|
||||
parser.add_option(
|
||||
"--allow-site-packages",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help=("Let bootstrap.py use existing site packages"),
|
||||
)
|
||||
|
||||
|
||||
options, args = parser.parse_args()
|
||||
|
@ -75,21 +87,22 @@ except ImportError:
|
|||
from urllib2 import urlopen
|
||||
|
||||
ez = {}
|
||||
exec(urlopen('https://bootstrap.pypa.io/ez_setup.py').read(), ez)
|
||||
exec(urlopen("https://bootstrap.pypa.io/ez_setup.py").read(), ez)
|
||||
|
||||
if not options.allow_site_packages:
|
||||
# ez_setup imports site, which adds site packages
|
||||
# this will remove them from the path to ensure that incompatible versions
|
||||
# of setuptools are not in the path
|
||||
import site
|
||||
|
||||
# inside a virtualenv, there is no 'getsitepackages'.
|
||||
# We can't remove these reliably
|
||||
if hasattr(site, 'getsitepackages'):
|
||||
if hasattr(site, "getsitepackages"):
|
||||
for sitepackage_path in site.getsitepackages():
|
||||
sys.path[:] = [x for x in sys.path if sitepackage_path not in x]
|
||||
|
||||
setup_args = {'to_dir': tmpeggs, 'download_delay': 0}
|
||||
ez['use_setuptools'](**setup_args)
|
||||
setup_args = {"to_dir": tmpeggs, "download_delay": 0}
|
||||
ez["use_setuptools"](**setup_args)
|
||||
import setuptools
|
||||
import pkg_resources
|
||||
|
||||
|
@ -104,36 +117,43 @@ for path in sys.path:
|
|||
|
||||
ws = pkg_resources.working_set
|
||||
|
||||
cmd = [sys.executable, '-c',
|
||||
'from setuptools.command.easy_install import main; main()',
|
||||
'-mZqNxd', tmpeggs]
|
||||
cmd = [
|
||||
sys.executable,
|
||||
"-c",
|
||||
"from setuptools.command.easy_install import main; main()",
|
||||
"-mZqNxd",
|
||||
tmpeggs,
|
||||
]
|
||||
|
||||
find_links = os.environ.get(
|
||||
'bootstrap-testing-find-links',
|
||||
options.find_links or
|
||||
('https://downloads.buildout.org/'
|
||||
if options.accept_buildout_test_releases else None)
|
||||
)
|
||||
"bootstrap-testing-find-links",
|
||||
options.find_links
|
||||
or (
|
||||
"https://downloads.buildout.org/"
|
||||
if options.accept_buildout_test_releases
|
||||
else None
|
||||
),
|
||||
)
|
||||
if find_links:
|
||||
cmd.extend(['-f', find_links])
|
||||
cmd.extend(["-f", find_links])
|
||||
|
||||
setuptools_path = ws.find(
|
||||
pkg_resources.Requirement.parse('setuptools')).location
|
||||
setuptools_path = ws.find(pkg_resources.Requirement.parse("setuptools")).location
|
||||
|
||||
requirement = 'zc.buildout'
|
||||
requirement = "zc.buildout"
|
||||
version = options.version
|
||||
if version is None and not options.accept_buildout_test_releases:
|
||||
# Figure out the most recent final version of zc.buildout.
|
||||
import setuptools.package_index
|
||||
_final_parts = '*final-', '*final'
|
||||
|
||||
_final_parts = "*final-", "*final"
|
||||
|
||||
def _final_version(parsed_version):
|
||||
for part in parsed_version:
|
||||
if (part.startswith('*')) and (part not in _final_parts):
|
||||
if (part.startswith("*")) and (part not in _final_parts):
|
||||
return False
|
||||
return True
|
||||
index = setuptools.package_index.PackageIndex(
|
||||
search_path=[setuptools_path])
|
||||
|
||||
index = setuptools.package_index.PackageIndex(search_path=[setuptools_path])
|
||||
if find_links:
|
||||
index.add_find_links((find_links,))
|
||||
req = pkg_resources.Requirement.parse(requirement)
|
||||
|
@ -152,13 +172,13 @@ if version is None and not options.accept_buildout_test_releases:
|
|||
best.sort()
|
||||
version = best[-1].version
|
||||
if version:
|
||||
requirement = '=='.join((requirement, version))
|
||||
requirement = "==".join((requirement, version))
|
||||
cmd.append(requirement)
|
||||
|
||||
import subprocess
|
||||
|
||||
if subprocess.call(cmd, env=dict(os.environ, PYTHONPATH=setuptools_path)) != 0:
|
||||
raise Exception(
|
||||
"Failed to execute command:\n%s" % repr(cmd)[1:-1])
|
||||
raise Exception("Failed to execute command:\n%s" % repr(cmd)[1:-1])
|
||||
|
||||
######################################################################
|
||||
# Import and run buildout
|
||||
|
@ -167,12 +187,12 @@ ws.add_entry(tmpeggs)
|
|||
ws.require(requirement)
|
||||
import zc.buildout.buildout
|
||||
|
||||
if not [a for a in args if '=' not in a]:
|
||||
args.append('bootstrap')
|
||||
if not [a for a in args if "=" not in a]:
|
||||
args.append("bootstrap")
|
||||
|
||||
# if -c was provided, we push it back into args for buildout' main function
|
||||
if options.config_file is not None:
|
||||
args[0:0] = ['-c', options.config_file]
|
||||
args[0:0] = ["-c", options.config_file]
|
||||
|
||||
zc.buildout.buildout.main(args)
|
||||
shutil.rmtree(tmpeggs)
|
||||
|
|
49
docs/conf.py
49
docs/conf.py
|
@ -3,46 +3,45 @@ import importlib.metadata
|
|||
import datetime
|
||||
import os
|
||||
|
||||
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
|
||||
on_rtd = os.environ.get("READTHEDOCS", None) == "True"
|
||||
|
||||
try:
|
||||
import sphinx_rtd_theme
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
|
||||
html_theme = "sphinx_rtd_theme"
|
||||
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
||||
except ImportError:
|
||||
html_theme = 'default'
|
||||
html_theme = "default"
|
||||
if not on_rtd:
|
||||
print('-' * 74)
|
||||
print('Warning: sphinx-rtd-theme not installed, building with default '
|
||||
'theme.')
|
||||
print('-' * 74)
|
||||
print("-" * 74)
|
||||
print(
|
||||
"Warning: sphinx-rtd-theme not installed, building with default " "theme."
|
||||
)
|
||||
print("-" * 74)
|
||||
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.coverage',
|
||||
'sphinx.ext.viewcode',
|
||||
'sphinx_copybutton',
|
||||
'sphinx.ext.intersphinx',
|
||||
'sphinx.ext.autosectionlabel',
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinx.ext.coverage",
|
||||
"sphinx.ext.viewcode",
|
||||
"sphinx_copybutton",
|
||||
"sphinx.ext.intersphinx",
|
||||
"sphinx.ext.autosectionlabel",
|
||||
]
|
||||
source_suffix = '.rst'
|
||||
master_doc = 'index'
|
||||
source_suffix = ".rst"
|
||||
master_doc = "index"
|
||||
|
||||
project = 'icalendar'
|
||||
project = "icalendar"
|
||||
this_year = datetime.date.today().year
|
||||
copyright = f'{this_year}, Plone Foundation'
|
||||
version = importlib.metadata.version('icalendar')
|
||||
copyright = f"{this_year}, Plone Foundation"
|
||||
version = importlib.metadata.version("icalendar")
|
||||
release = version
|
||||
|
||||
exclude_patterns = ['_build', 'lib', 'bin', 'include', 'local']
|
||||
pygments_style = 'sphinx'
|
||||
exclude_patterns = ["_build", "lib", "bin", "include", "local"]
|
||||
pygments_style = "sphinx"
|
||||
|
||||
htmlhelp_basename = 'icalendardoc'
|
||||
htmlhelp_basename = "icalendardoc"
|
||||
|
||||
man_pages = [
|
||||
('index', 'icalendar', 'icalendar Documentation',
|
||||
['Plone Foundation'], 1)
|
||||
]
|
||||
man_pages = [("index", "icalendar", "icalendar Documentation", ["Plone Foundation"], 1)]
|
||||
|
||||
intersphinx_mapping = {
|
||||
"python": ("https://docs.python.org/3", None),
|
||||
|
|
|
@ -23,24 +23,27 @@ from icalendar.tools import is_date, normalize_pytz, to_datetime
|
|||
if TYPE_CHECKING:
|
||||
from datetime import datetime
|
||||
|
||||
Parent = Union[Event,Todo]
|
||||
Parent = Union[Event, Todo]
|
||||
|
||||
|
||||
class IncompleteAlarmInformation(ValueError):
|
||||
"""The alarms cannot be calculated yet because information is missing."""
|
||||
|
||||
|
||||
class ComponentStartMissing(IncompleteAlarmInformation):
|
||||
"""We are missing the start of a component that the alarm is for.
|
||||
|
||||
Use Alarms.set_start().
|
||||
"""
|
||||
|
||||
|
||||
class ComponentEndMissing(IncompleteAlarmInformation):
|
||||
"""We are missing the end of a component that the alarm is for.
|
||||
|
||||
Use Alarms.set_end().
|
||||
"""
|
||||
|
||||
|
||||
class LocalTimezoneMissing(IncompleteAlarmInformation):
|
||||
"""We are missing the local timezone to compute the value.
|
||||
|
||||
|
@ -52,13 +55,13 @@ class AlarmTime:
|
|||
"""An alarm time with all the information."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
alarm: Alarm,
|
||||
trigger : datetime,
|
||||
acknowledged_until:Optional[datetime]=None,
|
||||
snoozed_until:Optional[datetime]=None,
|
||||
parent: Optional[Parent]=None,
|
||||
):
|
||||
self,
|
||||
alarm: Alarm,
|
||||
trigger: datetime,
|
||||
acknowledged_until: Optional[datetime] = None,
|
||||
snoozed_until: Optional[datetime] = None,
|
||||
parent: Optional[Parent] = None,
|
||||
):
|
||||
"""Create a new AlarmTime.
|
||||
|
||||
alarm
|
||||
|
@ -191,22 +194,22 @@ class Alarms:
|
|||
This is not implemented yet.
|
||||
"""
|
||||
|
||||
def __init__(self, component:Optional[Alarm|Event|Todo]=None):
|
||||
def __init__(self, component: Optional[Alarm | Event | Todo] = None):
|
||||
"""Start computing alarm times."""
|
||||
self._absolute_alarms : list[Alarm] = []
|
||||
self._start_alarms : list[Alarm] = []
|
||||
self._end_alarms : list[Alarm] = []
|
||||
self._start : Optional[date] = None
|
||||
self._end : Optional[date] = None
|
||||
self._parent : Optional[Parent] = None
|
||||
self._last_ack : Optional[datetime] = None
|
||||
self._snooze_until : Optional[datetime] = None
|
||||
self._local_tzinfo : Optional[tzinfo] = None
|
||||
self._absolute_alarms: list[Alarm] = []
|
||||
self._start_alarms: list[Alarm] = []
|
||||
self._end_alarms: list[Alarm] = []
|
||||
self._start: Optional[date] = None
|
||||
self._end: Optional[date] = None
|
||||
self._parent: Optional[Parent] = None
|
||||
self._last_ack: Optional[datetime] = None
|
||||
self._snooze_until: Optional[datetime] = None
|
||||
self._local_tzinfo: Optional[tzinfo] = None
|
||||
|
||||
if component is not None:
|
||||
self.add_component(component)
|
||||
|
||||
def add_component(self, component:Alarm|Parent):
|
||||
def add_component(self, component: Alarm | Parent):
|
||||
"""Add a component.
|
||||
|
||||
If this is an alarm, it is added.
|
||||
|
@ -263,7 +266,7 @@ class Alarms:
|
|||
"""
|
||||
self._end = dt
|
||||
|
||||
def _add(self, dt: date, td:timedelta):
|
||||
def _add(self, dt: date, td: timedelta):
|
||||
"""Add a timedelta to a datetime."""
|
||||
if is_date(dt):
|
||||
if td.seconds == 0:
|
||||
|
@ -294,7 +297,7 @@ class Alarms:
|
|||
"""
|
||||
self._snooze_until = tzp.localize_utc(dt) if dt is not None else None
|
||||
|
||||
def set_local_timezone(self, tzinfo:Optional[tzinfo|str]):
|
||||
def set_local_timezone(self, tzinfo: Optional[tzinfo | str]):
|
||||
"""Set the local timezone.
|
||||
|
||||
Events are sometimes in local time.
|
||||
|
@ -318,30 +321,32 @@ class Alarms:
|
|||
If you forget to set the acknowledged times, that is not problem.
|
||||
"""
|
||||
return (
|
||||
self._get_end_alarm_times() +
|
||||
self._get_start_alarm_times() +
|
||||
self._get_absolute_alarm_times()
|
||||
self._get_end_alarm_times()
|
||||
+ self._get_start_alarm_times()
|
||||
+ self._get_absolute_alarm_times()
|
||||
)
|
||||
|
||||
def _repeat(self, first: datetime, alarm: Alarm) -> Generator[datetime]:
|
||||
"""The times when the alarm is triggered relative to start."""
|
||||
yield first # we trigger at the start
|
||||
yield first # we trigger at the start
|
||||
repeat = alarm.REPEAT
|
||||
duration = alarm.DURATION
|
||||
if repeat and duration:
|
||||
for i in range(1, repeat + 1):
|
||||
yield self._add(first, duration * i)
|
||||
|
||||
def _alarm_time(self, alarm: Alarm, trigger:date):
|
||||
def _alarm_time(self, alarm: Alarm, trigger: date):
|
||||
"""Create an alarm time with the additional attributes."""
|
||||
if getattr(trigger, "tzinfo", None) is None and self._local_tzinfo is not None:
|
||||
trigger = normalize_pytz(trigger.replace(tzinfo=self._local_tzinfo))
|
||||
return AlarmTime(alarm, trigger, self._last_ack, self._snooze_until, self._parent)
|
||||
return AlarmTime(
|
||||
alarm, trigger, self._last_ack, self._snooze_until, self._parent
|
||||
)
|
||||
|
||||
def _get_absolute_alarm_times(self) -> list[AlarmTime]:
|
||||
"""Return a list of absolute alarm times."""
|
||||
return [
|
||||
self._alarm_time(alarm , trigger)
|
||||
self._alarm_time(alarm, trigger)
|
||||
for alarm in self._absolute_alarms
|
||||
for trigger in self._repeat(alarm.TRIGGER, alarm)
|
||||
]
|
||||
|
@ -349,9 +354,11 @@ class Alarms:
|
|||
def _get_start_alarm_times(self) -> list[AlarmTime]:
|
||||
"""Return a list of alarm times relative to the start of the component."""
|
||||
if self._start is None and self._start_alarms:
|
||||
raise ComponentStartMissing("Use Alarms.set_start because at least one alarm is relative to the start of a component.")
|
||||
raise ComponentStartMissing(
|
||||
"Use Alarms.set_start because at least one alarm is relative to the start of a component."
|
||||
)
|
||||
return [
|
||||
self._alarm_time(alarm , trigger)
|
||||
self._alarm_time(alarm, trigger)
|
||||
for alarm in self._start_alarms
|
||||
for trigger in self._repeat(self._add(self._start, alarm.TRIGGER), alarm)
|
||||
]
|
||||
|
@ -359,9 +366,11 @@ class Alarms:
|
|||
def _get_end_alarm_times(self) -> list[AlarmTime]:
|
||||
"""Return a list of alarm times relative to the start of the component."""
|
||||
if self._end is None and self._end_alarms:
|
||||
raise ComponentEndMissing("Use Alarms.set_end because at least one alarm is relative to the end of a component.")
|
||||
raise ComponentEndMissing(
|
||||
"Use Alarms.set_end because at least one alarm is relative to the end of a component."
|
||||
)
|
||||
return [
|
||||
self._alarm_time(alarm , trigger)
|
||||
self._alarm_time(alarm, trigger)
|
||||
for alarm in self._end_alarms
|
||||
for trigger in self._repeat(self._add(self._end, alarm.TRIGGER), alarm)
|
||||
]
|
||||
|
@ -378,11 +387,12 @@ class Alarms:
|
|||
"""
|
||||
return [alarm_time for alarm_time in self.times if alarm_time.is_active()]
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Alarms",
|
||||
"AlarmTime",
|
||||
"IncompleteAlarmInformation",
|
||||
"ComponentEndMissing",
|
||||
"ComponentStartMissing",
|
||||
"LocalTimezoneMissing"
|
||||
"LocalTimezoneMissing",
|
||||
]
|
||||
|
|
Plik diff jest za duży
Load Diff
|
@ -14,10 +14,8 @@ def canonsort_keys(keys, canonical_order=None):
|
|||
|
||||
|
||||
def canonsort_items(dict1, canonical_order=None):
|
||||
"""Returns a list of items from dict1, sorted by canonical_order.
|
||||
"""
|
||||
return [(k, dict1[k]) for k
|
||||
in canonsort_keys(dict1.keys(), canonical_order)]
|
||||
"""Returns a list of items from dict1, sorted by canonical_order."""
|
||||
return [(k, dict1[k]) for k in canonsort_keys(dict1.keys(), canonical_order)]
|
||||
|
||||
|
||||
class CaselessDict(OrderedDict):
|
||||
|
@ -26,8 +24,7 @@ class CaselessDict(OrderedDict):
|
|||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Set keys to upper for initial dict.
|
||||
"""
|
||||
"""Set keys to upper for initial dict."""
|
||||
super().__init__(*args, **kwargs)
|
||||
for key, value in self.items():
|
||||
key_upper = to_unicode(key).upper()
|
||||
|
@ -74,7 +71,7 @@ class CaselessDict(OrderedDict):
|
|||
# Multiple keys where key1.upper() == key2.upper() will be lost.
|
||||
mappings = list(args) + [kwargs]
|
||||
for mapping in mappings:
|
||||
if hasattr(mapping, 'items'):
|
||||
if hasattr(mapping, "items"):
|
||||
mapping = iter(mapping.items())
|
||||
for key, value in mapping:
|
||||
self[key] = value
|
||||
|
@ -83,7 +80,7 @@ class CaselessDict(OrderedDict):
|
|||
return type(self)(super().copy())
|
||||
|
||||
def __repr__(self):
|
||||
return f'{type(self).__name__}({dict(self)})'
|
||||
return f"{type(self).__name__}({dict(self)})"
|
||||
|
||||
def __eq__(self, other):
|
||||
return self is other or dict(self.items()) == dict(other.items())
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
#!/usr/bin/env python3
|
||||
"""utility program that allows user to preview calendar's events"""
|
||||
|
||||
import sys
|
||||
import pathlib
|
||||
import argparse
|
||||
|
@ -7,6 +8,7 @@ from datetime import datetime
|
|||
|
||||
from icalendar import Calendar, __version__
|
||||
|
||||
|
||||
def _format_name(address):
|
||||
"""Retrieve the e-mail and the name from an address.
|
||||
|
||||
|
@ -14,10 +16,10 @@ def _format_name(address):
|
|||
|
||||
:returns str: The name and the e-mail address.
|
||||
"""
|
||||
email = address.split(':')[-1]
|
||||
name = email.split('@')[0]
|
||||
email = address.split(":")[-1]
|
||||
name = email.split("@")[0]
|
||||
if not email:
|
||||
return ''
|
||||
return ""
|
||||
return f"{name} <{email}>"
|
||||
|
||||
|
||||
|
@ -30,33 +32,34 @@ def _format_attendees(attendees):
|
|||
"""
|
||||
if isinstance(attendees, str):
|
||||
attendees = [attendees]
|
||||
return '\n'.join(map(lambda s: s.rjust(len(s) + 5), map(_format_name, attendees)))
|
||||
return "\n".join(map(lambda s: s.rjust(len(s) + 5), map(_format_name, attendees)))
|
||||
|
||||
|
||||
def view(event):
|
||||
"""Make a human readable summary of an iCalendar file.
|
||||
|
||||
:returns str: Human readable summary.
|
||||
"""
|
||||
summary = event.get('summary', default='')
|
||||
organizer = _format_name(event.get('organizer', default=''))
|
||||
attendees = _format_attendees(event.get('attendee', default=[]))
|
||||
location = event.get('location', default='')
|
||||
comment = event.get('comment', '')
|
||||
description = event.get('description', '').split('\n')
|
||||
description = '\n'.join(map(lambda s: s.rjust(len(s) + 5), description))
|
||||
summary = event.get("summary", default="")
|
||||
organizer = _format_name(event.get("organizer", default=""))
|
||||
attendees = _format_attendees(event.get("attendee", default=[]))
|
||||
location = event.get("location", default="")
|
||||
comment = event.get("comment", "")
|
||||
description = event.get("description", "").split("\n")
|
||||
description = "\n".join(map(lambda s: s.rjust(len(s) + 5), description))
|
||||
|
||||
start = event.decoded('dtstart')
|
||||
if 'duration' in event:
|
||||
end = event.decoded('dtend', default=start + event.decoded('duration'))
|
||||
start = event.decoded("dtstart")
|
||||
if "duration" in event:
|
||||
end = event.decoded("dtend", default=start + event.decoded("duration"))
|
||||
else:
|
||||
end = event.decoded('dtend', default=start)
|
||||
duration = event.decoded('duration', default=end - start)
|
||||
end = event.decoded("dtend", default=start)
|
||||
duration = event.decoded("duration", default=end - start)
|
||||
if isinstance(start, datetime):
|
||||
start = start.astimezone()
|
||||
start = start.strftime('%c')
|
||||
start = start.strftime("%c")
|
||||
if isinstance(end, datetime):
|
||||
end = end.astimezone()
|
||||
end = end.strftime('%c')
|
||||
end = end.strftime("%c")
|
||||
|
||||
return f""" Organizer: {organizer}
|
||||
Attendees:
|
||||
|
@ -70,21 +73,33 @@ def view(event):
|
|||
Description:
|
||||
{description}"""
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('calendar_files', nargs='+', type=pathlib.Path)
|
||||
parser.add_argument('--output', '-o', type=argparse.FileType('w'), default=sys.stdout, help='output file')
|
||||
parser.add_argument('-v', '--version', action='version', version=f'{parser.prog} version {__version__}')
|
||||
parser.add_argument("calendar_files", nargs="+", type=pathlib.Path)
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
"-o",
|
||||
type=argparse.FileType("w"),
|
||||
default=sys.stdout,
|
||||
help="output file",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-v",
|
||||
"--version",
|
||||
action="version",
|
||||
version=f"{parser.prog} version {__version__}",
|
||||
)
|
||||
argv = parser.parse_args()
|
||||
|
||||
for calendar_file in argv.calendar_files:
|
||||
with open(calendar_file, encoding='utf-8-sig') as f:
|
||||
with open(calendar_file, encoding="utf-8-sig") as f:
|
||||
calendar = Calendar.from_ical(f.read())
|
||||
for event in calendar.walk('vevent'):
|
||||
argv.output.write(view(event) + '\n\n')
|
||||
for event in calendar.walk("vevent"):
|
||||
argv.output.write(view(event) + "\n\n")
|
||||
|
||||
|
||||
__all__ = ["main", "view"]
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
@ -23,12 +23,30 @@ with atheris.instrument_imports():
|
|||
from icalendar.tests.fuzzed import fuzz_calendar_v1
|
||||
|
||||
_value_error_matches = [
|
||||
"component", "parse", "Expected", "Wrong date format", "END encountered",
|
||||
"vDDD", 'recurrence', 'Offset must', 'Invalid iCalendar',
|
||||
'alue MUST', 'Key name', 'Invalid content line', 'does not exist',
|
||||
'base 64', 'must use datetime', 'Unknown date type', 'Wrong',
|
||||
'Start time', 'iCalendar', 'recurrence', 'float, float', 'utc offset',
|
||||
'parent', 'MUST be a datetime'
|
||||
"component",
|
||||
"parse",
|
||||
"Expected",
|
||||
"Wrong date format",
|
||||
"END encountered",
|
||||
"vDDD",
|
||||
"recurrence",
|
||||
"Offset must",
|
||||
"Invalid iCalendar",
|
||||
"alue MUST",
|
||||
"Key name",
|
||||
"Invalid content line",
|
||||
"does not exist",
|
||||
"base 64",
|
||||
"must use datetime",
|
||||
"Unknown date type",
|
||||
"Wrong",
|
||||
"Start time",
|
||||
"iCalendar",
|
||||
"recurrence",
|
||||
"float, float",
|
||||
"utc offset",
|
||||
"parent",
|
||||
"MUST be a datetime",
|
||||
]
|
||||
|
||||
|
||||
|
@ -43,11 +61,18 @@ def TestOneInput(data):
|
|||
try:
|
||||
# print the ICS file for the test case extraction
|
||||
# see https://stackoverflow.com/a/27367173/1320237
|
||||
print(base64.b64encode(calendar_string.encode("UTF-8", "surrogateescape")).decode("ASCII"))
|
||||
except UnicodeEncodeError: pass
|
||||
print(
|
||||
base64.b64encode(
|
||||
calendar_string.encode("UTF-8", "surrogateescape")
|
||||
).decode("ASCII")
|
||||
)
|
||||
except UnicodeEncodeError:
|
||||
pass
|
||||
print("--- end calendar ---")
|
||||
|
||||
fuzz_calendar_v1(icalendar.Calendar.from_ical, calendar_string, multiple, should_walk)
|
||||
fuzz_calendar_v1(
|
||||
icalendar.Calendar.from_ical, calendar_string, multiple, should_walk
|
||||
)
|
||||
except ValueError as e:
|
||||
if any(m in str(e) for m in _value_error_matches):
|
||||
return -1
|
||||
|
|
|
@ -15,38 +15,43 @@ import re
|
|||
|
||||
|
||||
def escape_char(text):
|
||||
"""Format value according to iCalendar TEXT escaping rules.
|
||||
"""
|
||||
"""Format value according to iCalendar TEXT escaping rules."""
|
||||
assert isinstance(text, (str, bytes))
|
||||
# NOTE: ORDER MATTERS!
|
||||
return text.replace(r'\N', '\n')\
|
||||
.replace('\\', '\\\\')\
|
||||
.replace(';', r'\;')\
|
||||
.replace(',', r'\,')\
|
||||
.replace('\r\n', r'\n')\
|
||||
.replace('\n', r'\n')
|
||||
return (
|
||||
text.replace(r"\N", "\n")
|
||||
.replace("\\", "\\\\")
|
||||
.replace(";", r"\;")
|
||||
.replace(",", r"\,")
|
||||
.replace("\r\n", r"\n")
|
||||
.replace("\n", r"\n")
|
||||
)
|
||||
|
||||
|
||||
def unescape_char(text):
|
||||
assert isinstance(text, (str, bytes))
|
||||
# NOTE: ORDER MATTERS!
|
||||
if isinstance(text, str):
|
||||
return text.replace('\\N', '\\n')\
|
||||
.replace('\r\n', '\n')\
|
||||
.replace('\\n', '\n')\
|
||||
.replace('\\,', ',')\
|
||||
.replace('\\;', ';')\
|
||||
.replace('\\\\', '\\')
|
||||
return (
|
||||
text.replace("\\N", "\\n")
|
||||
.replace("\r\n", "\n")
|
||||
.replace("\\n", "\n")
|
||||
.replace("\\,", ",")
|
||||
.replace("\\;", ";")
|
||||
.replace("\\\\", "\\")
|
||||
)
|
||||
elif isinstance(text, bytes):
|
||||
return text.replace(b'\\N', b'\\n')\
|
||||
.replace(b'\r\n', b'\n')\
|
||||
.replace(b'\\n', b'\n')\
|
||||
.replace(b'\\,', b',')\
|
||||
.replace(b'\\;', b';')\
|
||||
.replace(b'\\\\', b'\\')
|
||||
return (
|
||||
text.replace(b"\\N", b"\\n")
|
||||
.replace(b"\r\n", b"\n")
|
||||
.replace(b"\\n", b"\n")
|
||||
.replace(b"\\,", b",")
|
||||
.replace(b"\\;", b";")
|
||||
.replace(b"\\\\", b"\\")
|
||||
)
|
||||
|
||||
|
||||
def foldline(line, limit=75, fold_sep='\r\n '):
|
||||
def foldline(line, limit=75, fold_sep="\r\n "):
|
||||
"""Make a string folded as defined in RFC5545
|
||||
Lines of text SHOULD NOT be longer than 75 octets, excluding the line
|
||||
break. Long content lines SHOULD be split into a multiple line
|
||||
|
@ -56,16 +61,16 @@ def foldline(line, limit=75, fold_sep='\r\n '):
|
|||
SPACE or HTAB).
|
||||
"""
|
||||
assert isinstance(line, str)
|
||||
assert '\n' not in line
|
||||
assert "\n" not in line
|
||||
|
||||
# Use a fast and simple variant for the common case that line is all ASCII.
|
||||
try:
|
||||
line.encode('ascii')
|
||||
line.encode("ascii")
|
||||
except (UnicodeEncodeError, UnicodeDecodeError):
|
||||
pass
|
||||
else:
|
||||
return fold_sep.join(
|
||||
line[i:i + limit - 1] for i in range(0, len(line), limit - 1)
|
||||
line[i : i + limit - 1] for i in range(0, len(line), limit - 1)
|
||||
)
|
||||
|
||||
ret_chars = []
|
||||
|
@ -78,15 +83,15 @@ def foldline(line, limit=75, fold_sep='\r\n '):
|
|||
byte_count = char_byte_len
|
||||
ret_chars.append(char)
|
||||
|
||||
return ''.join(ret_chars)
|
||||
return "".join(ret_chars)
|
||||
|
||||
|
||||
#################################################################
|
||||
# Property parameter stuff
|
||||
|
||||
|
||||
def param_value(value):
|
||||
"""Returns a parameter value.
|
||||
"""
|
||||
"""Returns a parameter value."""
|
||||
if isinstance(value, SEQUENCE_TYPES):
|
||||
return q_join(value)
|
||||
elif isinstance(value, str):
|
||||
|
@ -99,13 +104,13 @@ def param_value(value):
|
|||
|
||||
# [\w-] because of the iCalendar RFC
|
||||
# . because of the vCard RFC
|
||||
NAME = re.compile(r'[\w.-]+')
|
||||
NAME = re.compile(r"[\w.-]+")
|
||||
|
||||
UNSAFE_CHAR = re.compile('[\x00-\x08\x0a-\x1f\x7F",:;]')
|
||||
QUNSAFE_CHAR = re.compile('[\x00-\x08\x0a-\x1f\x7F"]')
|
||||
FOLD = re.compile(b'(\r?\n)+[ \t]')
|
||||
uFOLD = re.compile('(\r?\n)+[ \t]')
|
||||
NEWLINE = re.compile(r'\r?\n')
|
||||
UNSAFE_CHAR = re.compile('[\x00-\x08\x0a-\x1f\x7f",:;]')
|
||||
QUNSAFE_CHAR = re.compile('[\x00-\x08\x0a-\x1f\x7f"]')
|
||||
FOLD = re.compile(b"(\r?\n)+[ \t]")
|
||||
uFOLD = re.compile("(\r?\n)+[ \t]")
|
||||
NEWLINE = re.compile(r"\r?\n")
|
||||
|
||||
|
||||
def validate_token(name):
|
||||
|
@ -127,8 +132,7 @@ QUOTABLE = re.compile("[,;: ’']")
|
|||
|
||||
|
||||
def dquote(val):
|
||||
"""Enclose parameter values containing [,;:] in double quotes.
|
||||
"""
|
||||
"""Enclose parameter values containing [,;:] in double quotes."""
|
||||
# a double-quote character is forbidden to appear in a parameter value
|
||||
# so replace it with a single-quote character
|
||||
val = val.replace('"', "'")
|
||||
|
@ -138,9 +142,8 @@ def dquote(val):
|
|||
|
||||
|
||||
# parsing helper
|
||||
def q_split(st, sep=',', maxsplit=-1):
|
||||
"""Splits a string on char, taking double (q)uotes into considderation.
|
||||
"""
|
||||
def q_split(st, sep=",", maxsplit=-1):
|
||||
"""Splits a string on char, taking double (q)uotes into considderation."""
|
||||
if maxsplit == 0:
|
||||
return [st]
|
||||
|
||||
|
@ -162,9 +165,8 @@ def q_split(st, sep=',', maxsplit=-1):
|
|||
return result
|
||||
|
||||
|
||||
def q_join(lst, sep=','):
|
||||
"""Joins a list on sep, quoting strings with QUOTABLE chars.
|
||||
"""
|
||||
def q_join(lst, sep=","):
|
||||
"""Joins a list on sep, quoting strings with QUOTABLE chars."""
|
||||
return sep.join(dquote(itm) for itm in lst)
|
||||
|
||||
|
||||
|
@ -179,24 +181,24 @@ class Parameters(CaselessDict):
|
|||
"""
|
||||
return self.keys()
|
||||
|
||||
# TODO?
|
||||
# Later, when I get more time... need to finish this off now. The last major
|
||||
# thing missing.
|
||||
# def _encode(self, name, value, cond=1):
|
||||
# # internal, for conditional convertion of values.
|
||||
# if cond:
|
||||
# klass = types_factory.for_property(name)
|
||||
# return klass(value)
|
||||
# return value
|
||||
#
|
||||
# def add(self, name, value, encode=0):
|
||||
# "Add a parameter value and optionally encode it."
|
||||
# if encode:
|
||||
# value = self._encode(name, value, encode)
|
||||
# self[name] = value
|
||||
#
|
||||
# def decoded(self, name):
|
||||
# "returns a decoded value, or list of same"
|
||||
# TODO?
|
||||
# Later, when I get more time... need to finish this off now. The last major
|
||||
# thing missing.
|
||||
# def _encode(self, name, value, cond=1):
|
||||
# # internal, for conditional convertion of values.
|
||||
# if cond:
|
||||
# klass = types_factory.for_property(name)
|
||||
# return klass(value)
|
||||
# return value
|
||||
#
|
||||
# def add(self, name, value, encode=0):
|
||||
# "Add a parameter value and optionally encode it."
|
||||
# if encode:
|
||||
# value = self._encode(name, value, encode)
|
||||
# self[name] = value
|
||||
#
|
||||
# def decoded(self, name):
|
||||
# "returns a decoded value, or list of same"
|
||||
|
||||
def to_ical(self, sorted=True):
|
||||
result = []
|
||||
|
@ -210,8 +212,8 @@ class Parameters(CaselessDict):
|
|||
value = value.encode(DEFAULT_ENCODING)
|
||||
# CaselessDict keys are always unicode
|
||||
key = key.upper().encode(DEFAULT_ENCODING)
|
||||
result.append(key + b'=' + value)
|
||||
return b';'.join(result)
|
||||
result.append(key + b"=" + value)
|
||||
return b";".join(result)
|
||||
|
||||
@classmethod
|
||||
def from_ical(cls, st, strict=False):
|
||||
|
@ -219,14 +221,14 @@ class Parameters(CaselessDict):
|
|||
|
||||
# parse into strings
|
||||
result = cls()
|
||||
for param in q_split(st, ';'):
|
||||
for param in q_split(st, ";"):
|
||||
try:
|
||||
key, val = q_split(param, '=', maxsplit=1)
|
||||
key, val = q_split(param, "=", maxsplit=1)
|
||||
validate_token(key)
|
||||
# Property parameter values that are not in quoted
|
||||
# strings are case insensitive.
|
||||
vals = []
|
||||
for v in q_split(val, ','):
|
||||
for v in q_split(val, ","):
|
||||
if v.startswith('"') and v.endswith('"'):
|
||||
v = v.strip('"')
|
||||
validate_param_value(v, quoted=True)
|
||||
|
@ -245,20 +247,27 @@ class Parameters(CaselessDict):
|
|||
else:
|
||||
result[key] = vals
|
||||
except ValueError as exc:
|
||||
raise ValueError(
|
||||
f'{param!r} is not a valid parameter string: {exc}')
|
||||
raise ValueError(f"{param!r} is not a valid parameter string: {exc}")
|
||||
return result
|
||||
|
||||
|
||||
def escape_string(val):
|
||||
# f'{i:02X}'
|
||||
return val.replace(r'\,', '%2C').replace(r'\:', '%3A')\
|
||||
.replace(r'\;', '%3B').replace(r'\\', '%5C')
|
||||
return (
|
||||
val.replace(r"\,", "%2C")
|
||||
.replace(r"\:", "%3A")
|
||||
.replace(r"\;", "%3B")
|
||||
.replace(r"\\", "%5C")
|
||||
)
|
||||
|
||||
|
||||
def unescape_string(val):
|
||||
return val.replace('%2C', ',').replace('%3A', ':')\
|
||||
.replace('%3B', ';').replace('%5C', '\\')
|
||||
return (
|
||||
val.replace("%2C", ",")
|
||||
.replace("%3A", ":")
|
||||
.replace("%3B", ";")
|
||||
.replace("%5C", "\\")
|
||||
)
|
||||
|
||||
|
||||
def unescape_list_or_string(val):
|
||||
|
@ -271,24 +280,26 @@ def unescape_list_or_string(val):
|
|||
#########################################
|
||||
# parsing and generation of content lines
|
||||
|
||||
|
||||
class Contentline(str):
|
||||
"""A content line is basically a string that can be folded and parsed into
|
||||
parts.
|
||||
"""
|
||||
|
||||
def __new__(cls, value, strict=False, encoding=DEFAULT_ENCODING):
|
||||
value = to_unicode(value, encoding=encoding)
|
||||
assert '\n' not in value, ('Content line can not contain unescaped '
|
||||
'new line characters.')
|
||||
assert "\n" not in value, (
|
||||
"Content line can not contain unescaped " "new line characters."
|
||||
)
|
||||
self = super().__new__(cls, value)
|
||||
self.strict = strict
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def from_parts(cls, name, params, values, sorted=True):
|
||||
"""Turn a parts into a content line.
|
||||
"""
|
||||
"""Turn a parts into a content line."""
|
||||
assert isinstance(params, Parameters)
|
||||
if hasattr(values, 'to_ical'):
|
||||
if hasattr(values, "to_ical"):
|
||||
values = values.to_ical()
|
||||
else:
|
||||
values = vText(values).to_ical()
|
||||
|
@ -301,12 +312,11 @@ class Contentline(str):
|
|||
values = to_unicode(values)
|
||||
if params:
|
||||
params = to_unicode(params.to_ical(sorted=sorted))
|
||||
return cls(f'{name};{params}:{values}')
|
||||
return cls(f'{name}:{values}')
|
||||
return cls(f"{name};{params}:{values}")
|
||||
return cls(f"{name}:{values}")
|
||||
|
||||
def parts(self):
|
||||
"""Split the content line up into (name, parameters, values) parts.
|
||||
"""
|
||||
"""Split the content line up into (name, parameters, values) parts."""
|
||||
try:
|
||||
st = escape_string(self)
|
||||
name_split = None
|
||||
|
@ -314,39 +324,40 @@ class Contentline(str):
|
|||
in_quotes = False
|
||||
for i, ch in enumerate(st):
|
||||
if not in_quotes:
|
||||
if ch in ':;' and not name_split:
|
||||
if ch in ":;" and not name_split:
|
||||
name_split = i
|
||||
if ch == ':' and not value_split:
|
||||
if ch == ":" and not value_split:
|
||||
value_split = i
|
||||
if ch == '"':
|
||||
in_quotes = not in_quotes
|
||||
name = unescape_string(st[:name_split])
|
||||
if not name:
|
||||
raise ValueError('Key name is required')
|
||||
raise ValueError("Key name is required")
|
||||
validate_token(name)
|
||||
if not value_split:
|
||||
value_split = i + 1
|
||||
if not name_split or name_split + 1 == value_split:
|
||||
raise ValueError('Invalid content line')
|
||||
params = Parameters.from_ical(st[name_split + 1: value_split],
|
||||
strict=self.strict)
|
||||
raise ValueError("Invalid content line")
|
||||
params = Parameters.from_ical(
|
||||
st[name_split + 1 : value_split], strict=self.strict
|
||||
)
|
||||
params = Parameters(
|
||||
(unescape_string(key), unescape_list_or_string(value))
|
||||
for key, value in iter(params.items())
|
||||
)
|
||||
values = unescape_string(st[value_split + 1:])
|
||||
values = unescape_string(st[value_split + 1 :])
|
||||
return (name, params, values)
|
||||
except ValueError as exc:
|
||||
raise ValueError(
|
||||
f"Content line could not be parsed into parts: '{self}': {exc}")
|
||||
f"Content line could not be parsed into parts: '{self}': {exc}"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_ical(cls, ical, strict=False):
|
||||
"""Unfold the content lines in an iCalendar into long content lines.
|
||||
"""
|
||||
"""Unfold the content lines in an iCalendar into long content lines."""
|
||||
ical = to_unicode(ical)
|
||||
# a fold is carriage return followed by either a space or a tab
|
||||
return cls(uFOLD.sub('', ical), strict=strict)
|
||||
return cls(uFOLD.sub("", ical), strict=strict)
|
||||
|
||||
def to_ical(self):
|
||||
"""Long content lines are folded so they are less than 75 characters
|
||||
|
@ -362,33 +373,48 @@ class Contentlines(list):
|
|||
"""
|
||||
|
||||
def to_ical(self):
|
||||
"""Simply join self.
|
||||
"""
|
||||
return b'\r\n'.join(line.to_ical() for line in self if line) + b'\r\n'
|
||||
"""Simply join self."""
|
||||
return b"\r\n".join(line.to_ical() for line in self if line) + b"\r\n"
|
||||
|
||||
@classmethod
|
||||
def from_ical(cls, st):
|
||||
"""Parses a string into content lines.
|
||||
"""
|
||||
"""Parses a string into content lines."""
|
||||
st = to_unicode(st)
|
||||
try:
|
||||
# a fold is carriage return followed by either a space or a tab
|
||||
unfolded = uFOLD.sub('', st)
|
||||
lines = cls(Contentline(line) for
|
||||
line in NEWLINE.split(unfolded) if line)
|
||||
lines.append('') # '\r\n' at the end of every content line
|
||||
unfolded = uFOLD.sub("", st)
|
||||
lines = cls(Contentline(line) for line in NEWLINE.split(unfolded) if line)
|
||||
lines.append("") # '\r\n' at the end of every content line
|
||||
return lines
|
||||
except Exception:
|
||||
raise ValueError('Expected StringType with content lines')
|
||||
raise ValueError("Expected StringType with content lines")
|
||||
|
||||
|
||||
# XXX: what kind of hack is this? import depends to be at end
|
||||
from icalendar.prop import vText
|
||||
|
||||
|
||||
__all__ = ["Contentline", "Contentlines", "FOLD", "NAME", "NEWLINE",
|
||||
"Parameters", "QUNSAFE_CHAR", "QUOTABLE", "UNSAFE_CHAR", "dquote",
|
||||
"escape_char", "escape_string", "foldline", "param_value", "q_join",
|
||||
"q_split", "uFOLD", "unescape_char",
|
||||
"unescape_list_or_string", "unescape_string", "validate_param_value",
|
||||
"validate_token"]
|
||||
__all__ = [
|
||||
"Contentline",
|
||||
"Contentlines",
|
||||
"FOLD",
|
||||
"NAME",
|
||||
"NEWLINE",
|
||||
"Parameters",
|
||||
"QUNSAFE_CHAR",
|
||||
"QUOTABLE",
|
||||
"UNSAFE_CHAR",
|
||||
"dquote",
|
||||
"escape_char",
|
||||
"escape_string",
|
||||
"foldline",
|
||||
"param_value",
|
||||
"q_join",
|
||||
"q_split",
|
||||
"uFOLD",
|
||||
"unescape_char",
|
||||
"unescape_list_or_string",
|
||||
"unescape_string",
|
||||
"validate_param_value",
|
||||
"validate_token",
|
||||
]
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
from typing import List, Union
|
||||
|
||||
SEQUENCE_TYPES = (list, tuple)
|
||||
DEFAULT_ENCODING = 'utf-8'
|
||||
DEFAULT_ENCODING = "utf-8"
|
||||
ICAL_TYPE = Union[str, bytes]
|
||||
|
||||
|
||||
def from_unicode(value: ICAL_TYPE, encoding='utf-8') -> bytes:
|
||||
def from_unicode(value: ICAL_TYPE, encoding="utf-8") -> bytes:
|
||||
"""
|
||||
Converts a value to bytes, even if it already is bytes
|
||||
:param value: The value to convert
|
||||
|
@ -18,27 +18,26 @@ def from_unicode(value: ICAL_TYPE, encoding='utf-8') -> bytes:
|
|||
try:
|
||||
return value.encode(encoding)
|
||||
except UnicodeEncodeError:
|
||||
return value.encode('utf-8', 'replace')
|
||||
return value.encode("utf-8", "replace")
|
||||
else:
|
||||
return value
|
||||
|
||||
|
||||
def to_unicode(value: ICAL_TYPE, encoding='utf-8-sig') -> str:
|
||||
"""Converts a value to unicode, even if it is already a unicode string.
|
||||
"""
|
||||
def to_unicode(value: ICAL_TYPE, encoding="utf-8-sig") -> str:
|
||||
"""Converts a value to unicode, even if it is already a unicode string."""
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
elif isinstance(value, bytes):
|
||||
try:
|
||||
return value.decode(encoding)
|
||||
except UnicodeDecodeError:
|
||||
return value.decode('utf-8-sig', 'replace')
|
||||
return value.decode("utf-8-sig", "replace")
|
||||
else:
|
||||
return value
|
||||
|
||||
|
||||
def data_encode(
|
||||
data: Union[ICAL_TYPE, dict, list], encoding=DEFAULT_ENCODING
|
||||
data: Union[ICAL_TYPE, dict, list], encoding=DEFAULT_ENCODING
|
||||
) -> Union[bytes, List[bytes], dict]:
|
||||
"""Encode all datastructures to the given encoding.
|
||||
Currently unicode strings, dicts and lists are supported.
|
||||
|
@ -54,5 +53,11 @@ def data_encode(
|
|||
return data
|
||||
|
||||
|
||||
__all__ = ["DEFAULT_ENCODING", "SEQUENCE_TYPES", "ICAL_TYPE", "data_encode", "from_unicode",
|
||||
"to_unicode"]
|
||||
__all__ = [
|
||||
"DEFAULT_ENCODING",
|
||||
"SEQUENCE_TYPES",
|
||||
"ICAL_TYPE",
|
||||
"data_encode",
|
||||
"from_unicode",
|
||||
"to_unicode",
|
||||
]
|
||||
|
|
Plik diff jest za duży
Load Diff
|
@ -265,24 +265,28 @@ def pytz_only(tzp, tzp_name) -> str:
|
|||
assert tzp.uses_pytz()
|
||||
return tzp_name
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def zoneinfo_only(tzp, request, tzp_name) -> str:
|
||||
"""Skip tests that are not running under zoneinfo."""
|
||||
assert tzp.uses_zoneinfo()
|
||||
return tzp_name
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def no_pytz(tzp_name) -> str:
|
||||
"""Do not run tests with pytz."""
|
||||
assert tzp_name != "pytz"
|
||||
return tzp_name
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def no_zoneinfo(tzp_name) -> str:
|
||||
"""Do not run tests with zoneinfo."""
|
||||
assert tzp_name != "zoneinfo"
|
||||
return tzp_name
|
||||
|
||||
|
||||
def pytest_generate_tests(metafunc):
|
||||
"""Parametrize without skipping:
|
||||
|
||||
|
@ -343,7 +347,7 @@ def env_for_doctest(monkeypatch):
|
|||
|
||||
|
||||
@pytest.fixture(params=timezone_ids.TZIDS)
|
||||
def tzid(request:pytest.FixtureRequest) -> str:
|
||||
def tzid(request: pytest.FixtureRequest) -> str:
|
||||
"""Return a timezone id to be used with pytz or zoneinfo.
|
||||
|
||||
This goes through all the different timezones possible.
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
"""Test the properties of the alarm."""
|
||||
|
||||
|
||||
import pytest
|
||||
|
||||
from icalendar.cal import Alarm, InvalidCalendar
|
||||
|
@ -52,6 +51,3 @@ def test_alarm_to_string():
|
|||
a = Alarm()
|
||||
a.REPEAT = 11
|
||||
assert a.to_ical() == b"BEGIN:VALARM\r\nREPEAT:11\r\nEND:VALARM\r\n"
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -19,20 +19,21 @@ def test_no_dtstamp(dtstamp_comp):
|
|||
assert dtstamp_comp.DTSTAMP is None
|
||||
|
||||
|
||||
def set_dtstamp_attribute(component:Component, value:date):
|
||||
def set_dtstamp_attribute(component: Component, value: date):
|
||||
"""Use the setter."""
|
||||
component.DTSTAMP = value
|
||||
|
||||
def set_dtstamp_item(component: Component, value:date):
|
||||
|
||||
def set_dtstamp_item(component: Component, value: date):
|
||||
"""Use setitem."""
|
||||
component["DTSTAMP"] = vDDDTypes(value)
|
||||
|
||||
def set_dtstamp_add(component: Component, value:date):
|
||||
|
||||
def set_dtstamp_add(component: Component, value: date):
|
||||
"""Use add."""
|
||||
component.add("DTSTAMP", value)
|
||||
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("value", "timezone", "expected"),
|
||||
[
|
||||
|
@ -40,12 +41,14 @@ def set_dtstamp_add(component: Component, value:date):
|
|||
(datetime(2024, 10, 11, 23, 1), "Europe/Berlin", datetime(2024, 10, 11, 21, 1)),
|
||||
(datetime(2024, 10, 11, 22, 1), "UTC", datetime(2024, 10, 11, 22, 1)),
|
||||
(date(2024, 10, 10), None, datetime(2024, 10, 10)),
|
||||
]
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"set_dtstamp", [set_dtstamp_add, set_dtstamp_attribute, set_dtstamp_item]
|
||||
)
|
||||
def test_set_value_and_get_it(dtstamp_comp, value, timezone, expected, tzp, set_dtstamp):
|
||||
def test_set_value_and_get_it(
|
||||
dtstamp_comp, value, timezone, expected, tzp, set_dtstamp
|
||||
):
|
||||
"""Set and get the DTSTAMP value."""
|
||||
dtstamp = value if timezone is None else tzp.localize(value, timezone)
|
||||
set_dtstamp(dtstamp_comp, dtstamp)
|
||||
|
@ -55,13 +58,7 @@ def test_set_value_and_get_it(dtstamp_comp, value, timezone, expected, tzp, set_
|
|||
assert in_utc == dtstamp_comp.DTSTAMP
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"invalid_value",
|
||||
[
|
||||
None,
|
||||
timedelta()
|
||||
]
|
||||
)
|
||||
@pytest.mark.parametrize("invalid_value", [None, timedelta()])
|
||||
def test_set_invalid_value(invalid_value, dtstamp_comp):
|
||||
"""Check handling of invalid values."""
|
||||
with pytest.raises(TypeError) as e:
|
||||
|
@ -69,19 +66,16 @@ def test_set_invalid_value(invalid_value, dtstamp_comp):
|
|||
assert e.value.args[0] == f"DTSTAMP takes a datetime in UTC, not {invalid_value}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"invalid_value",
|
||||
[
|
||||
None,
|
||||
vDDDTypes(timedelta())
|
||||
]
|
||||
)
|
||||
@pytest.mark.parametrize("invalid_value", [None, vDDDTypes(timedelta())])
|
||||
def test_get_invalid_value(invalid_value, dtstamp_comp):
|
||||
"""Check handling of invalid values."""
|
||||
dtstamp_comp["DTSTAMP"] = invalid_value
|
||||
with pytest.raises(InvalidCalendar) as e:
|
||||
dtstamp_comp.DTSTAMP # noqa: B018
|
||||
assert e.value.args[0] == f"DTSTAMP must be a datetime in UTC, not {getattr(invalid_value, 'dt', invalid_value)}"
|
||||
assert (
|
||||
e.value.args[0]
|
||||
== f"DTSTAMP must be a datetime in UTC, not {getattr(invalid_value, 'dt', invalid_value)}"
|
||||
)
|
||||
|
||||
|
||||
def test_set_twice(dtstamp_comp, tzp):
|
||||
|
|
|
@ -1,87 +1,120 @@
|
|||
|
||||
from icalendar.prop import vBoolean,vInline , vUTCOffset, vCategory, vCalAddress, vWeekday, vDuration, vFloat, vGeo, vInt, vText, vMonth, vUTCOffset, vFrequency, vRecur, vDatetime, vUri
|
||||
from icalendar.prop import (
|
||||
vBoolean,
|
||||
vInline,
|
||||
vUTCOffset,
|
||||
vCategory,
|
||||
vCalAddress,
|
||||
vWeekday,
|
||||
vDuration,
|
||||
vFloat,
|
||||
vGeo,
|
||||
vInt,
|
||||
vText,
|
||||
vMonth,
|
||||
vUTCOffset,
|
||||
vFrequency,
|
||||
vRecur,
|
||||
vDatetime,
|
||||
vUri,
|
||||
)
|
||||
import datetime
|
||||
|
||||
|
||||
def test_param_vCategory():
|
||||
obj = vCategory(["Work", "Personal"], params={"SOME_PARAM":"VALUE"})
|
||||
assert isinstance(obj, vCategory)
|
||||
assert obj.params["SOME_PARAM"]=="VALUE"
|
||||
obj = vCategory(["Work", "Personal"], params={"SOME_PARAM": "VALUE"})
|
||||
assert isinstance(obj, vCategory)
|
||||
assert obj.params["SOME_PARAM"] == "VALUE"
|
||||
|
||||
|
||||
def test_param_vCalAddress():
|
||||
obj = vCalAddress('mailto:jane_doe@example.com',params={"SOME_PARAM":"VALUE"})
|
||||
assert isinstance(obj, vCalAddress)
|
||||
assert obj.params["SOME_PARAM"]=="VALUE"
|
||||
obj = vCalAddress("mailto:jane_doe@example.com", params={"SOME_PARAM": "VALUE"})
|
||||
assert isinstance(obj, vCalAddress)
|
||||
assert obj.params["SOME_PARAM"] == "VALUE"
|
||||
|
||||
|
||||
def test_param_vWeekday():
|
||||
obj = vWeekday("2FR",params={"SOME_PARAM":"VALUE"})
|
||||
assert isinstance(obj, vWeekday)
|
||||
assert obj.params["SOME_PARAM"]=="VALUE"
|
||||
obj = vWeekday("2FR", params={"SOME_PARAM": "VALUE"})
|
||||
assert isinstance(obj, vWeekday)
|
||||
assert obj.params["SOME_PARAM"] == "VALUE"
|
||||
|
||||
|
||||
def test_param_vBoolean():
|
||||
|
||||
obj = vBoolean(True, params={"SOME_PARAM":"VALUE"})
|
||||
assert isinstance(obj, vBoolean)
|
||||
assert obj.params["SOME_PARAM"]=="VALUE"
|
||||
obj = vBoolean(True, params={"SOME_PARAM": "VALUE"})
|
||||
assert isinstance(obj, vBoolean)
|
||||
assert obj.params["SOME_PARAM"] == "VALUE"
|
||||
|
||||
|
||||
def test_param_vDuration():
|
||||
td = datetime.timedelta(days=15, seconds=18020)
|
||||
obj = vDuration(td, params={"SOME_PARAM":"VALUE"})
|
||||
assert isinstance(obj, vDuration)
|
||||
assert obj.params["SOME_PARAM"]=="VALUE"
|
||||
obj = vDuration(td, params={"SOME_PARAM": "VALUE"})
|
||||
assert isinstance(obj, vDuration)
|
||||
assert obj.params["SOME_PARAM"] == "VALUE"
|
||||
|
||||
|
||||
def test_param_vFloat():
|
||||
obj = vFloat('1.333',params={"SOME_PARAM":"VALUE"})
|
||||
assert isinstance(obj, vFloat)
|
||||
assert obj.params["SOME_PARAM"]=="VALUE"
|
||||
obj = vFloat("1.333", params={"SOME_PARAM": "VALUE"})
|
||||
assert isinstance(obj, vFloat)
|
||||
assert obj.params["SOME_PARAM"] == "VALUE"
|
||||
|
||||
|
||||
def test_param_vGeo():
|
||||
obj = vGeo((37.386013, -122.082932),params={"SOME_PARAM":"VALUE"})
|
||||
assert isinstance(obj, vGeo)
|
||||
assert obj.params["SOME_PARAM"]=="VALUE"
|
||||
obj = vGeo((37.386013, -122.082932), params={"SOME_PARAM": "VALUE"})
|
||||
assert isinstance(obj, vGeo)
|
||||
assert obj.params["SOME_PARAM"] == "VALUE"
|
||||
|
||||
|
||||
def test_param_vInt():
|
||||
obj = vInt('87',params={"SOME_PARAM":"VALUE"})
|
||||
assert isinstance(obj, vInt)
|
||||
assert obj.params["SOME_PARAM"]=="VALUE"
|
||||
obj = vInt("87", params={"SOME_PARAM": "VALUE"})
|
||||
assert isinstance(obj, vInt)
|
||||
assert obj.params["SOME_PARAM"] == "VALUE"
|
||||
|
||||
|
||||
def test_param_vInline():
|
||||
obj = vInline("sometxt", params={"SOME_PARAM":"VALUE"})
|
||||
assert isinstance(obj, vInline)
|
||||
assert obj.params["SOME_PARAM"]=="VALUE"
|
||||
obj = vInline("sometxt", params={"SOME_PARAM": "VALUE"})
|
||||
assert isinstance(obj, vInline)
|
||||
assert obj.params["SOME_PARAM"] == "VALUE"
|
||||
|
||||
|
||||
def test_param_vText():
|
||||
obj = vText("sometxt", params={"SOME_PARAM":"VALUE"})
|
||||
assert isinstance(obj, vText)
|
||||
assert obj.params["SOME_PARAM"]=="VALUE"
|
||||
obj = vText("sometxt", params={"SOME_PARAM": "VALUE"})
|
||||
assert isinstance(obj, vText)
|
||||
assert obj.params["SOME_PARAM"] == "VALUE"
|
||||
|
||||
|
||||
def test_param_vMonth():
|
||||
obj = vMonth(1,params={"SOME_PARAM":"VALUE"})
|
||||
assert isinstance(obj, vMonth)
|
||||
assert obj.params["SOME_PARAM"]=="VALUE"
|
||||
obj = vMonth(1, params={"SOME_PARAM": "VALUE"})
|
||||
assert isinstance(obj, vMonth)
|
||||
assert obj.params["SOME_PARAM"] == "VALUE"
|
||||
|
||||
|
||||
def test_param_vUTCOffset():
|
||||
obj = vUTCOffset(datetime.timedelta(days=-1, seconds=68400),params={"SOME_PARAM":"VALUE"})
|
||||
assert isinstance(obj, vUTCOffset)
|
||||
assert obj.params["SOME_PARAM"]=="VALUE"
|
||||
obj = vUTCOffset(
|
||||
datetime.timedelta(days=-1, seconds=68400), params={"SOME_PARAM": "VALUE"}
|
||||
)
|
||||
assert isinstance(obj, vUTCOffset)
|
||||
assert obj.params["SOME_PARAM"] == "VALUE"
|
||||
|
||||
|
||||
def test_param_vFrequency():
|
||||
obj = vFrequency("DAILY",params={"SOME_PARAM":"VALUE"})
|
||||
assert isinstance(obj, vFrequency)
|
||||
assert obj.params["SOME_PARAM"]=="VALUE"
|
||||
obj = vFrequency("DAILY", params={"SOME_PARAM": "VALUE"})
|
||||
assert isinstance(obj, vFrequency)
|
||||
assert obj.params["SOME_PARAM"] == "VALUE"
|
||||
|
||||
|
||||
|
||||
def test_param_vRecur():
|
||||
obj = vRecur({'FREQ': ['DAILY'], 'COUNT': [10]}, params={"SOME_PARAM":"VALUE"})
|
||||
assert isinstance(obj,vRecur)
|
||||
assert obj.params["SOME_PARAM"]=="VALUE"
|
||||
obj = vRecur({"FREQ": ["DAILY"], "COUNT": [10]}, params={"SOME_PARAM": "VALUE"})
|
||||
assert isinstance(obj, vRecur)
|
||||
assert obj.params["SOME_PARAM"] == "VALUE"
|
||||
|
||||
|
||||
def test_param_vDatetime():
|
||||
dt = datetime.datetime(2025, 3, 16, 14, 30, 0, tzinfo=datetime.timezone.utc)
|
||||
obj = vDatetime(dt,params={"SOME_PARAM":"VALUE"})
|
||||
assert isinstance(obj, vDatetime)
|
||||
assert obj.params["SOME_PARAM"]=="VALUE"
|
||||
obj = vDatetime(dt, params={"SOME_PARAM": "VALUE"})
|
||||
assert isinstance(obj, vDatetime)
|
||||
assert obj.params["SOME_PARAM"] == "VALUE"
|
||||
|
||||
|
||||
def test_param_vUri():
|
||||
obj = vUri("WWW.WESBITE.COM",params={"SOME_PARAM":"VALUE"})
|
||||
assert isinstance(obj, vUri)
|
||||
assert obj.params["SOME_PARAM"]=="VALUE"
|
||||
obj = vUri("WWW.WESBITE.COM", params={"SOME_PARAM": "VALUE"})
|
||||
assert isinstance(obj, vUri)
|
||||
assert obj.params["SOME_PARAM"] == "VALUE"
|
||||
|
|
|
@ -6,8 +6,11 @@ import copy
|
|||
try:
|
||||
from pytz import UnknownTimeZoneError
|
||||
except ImportError:
|
||||
|
||||
class UnknownTimeZoneError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
from datetime import date, datetime, time, timedelta
|
||||
|
||||
import pytest
|
||||
|
@ -83,7 +86,10 @@ def test_deep_copies_are_equal(ics_file, tzp):
|
|||
Ignore errors when a custom time zone is used.
|
||||
This is still covered by the parsing test.
|
||||
"""
|
||||
if ics_file.source_file == "issue_722_timezone_transition_ambiguity.ics" and tzp.uses_zoneinfo():
|
||||
if (
|
||||
ics_file.source_file == "issue_722_timezone_transition_ambiguity.ics"
|
||||
and tzp.uses_zoneinfo()
|
||||
):
|
||||
pytest.skip("This test fails for now.")
|
||||
with contextlib.suppress(UnknownTimeZoneError):
|
||||
assert_equal(copy.deepcopy(ics_file), copy.deepcopy(ics_file))
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
"""This tests the properties of components and their types."""
|
||||
|
||||
from __future__ import annotations
|
||||
from datetime import date, datetime, timedelta
|
||||
|
||||
|
@ -23,7 +24,7 @@ from icalendar import (
|
|||
from icalendar.prop import vDuration
|
||||
|
||||
|
||||
def prop(component: Event|Todo, prop:str) -> str:
|
||||
def prop(component: Event | Todo, prop: str) -> str:
|
||||
"""Translate the end property.
|
||||
|
||||
This allows us to run the same tests on Event and Todo.
|
||||
|
@ -32,16 +33,20 @@ def prop(component: Event|Todo, prop:str) -> str:
|
|||
return "DUE"
|
||||
return prop
|
||||
|
||||
|
||||
@pytest.fixture(params=[Event, Todo])
|
||||
def start_end_component(request):
|
||||
"""The event to test."""
|
||||
return request.param()
|
||||
|
||||
@pytest.fixture(params=[
|
||||
|
||||
@pytest.fixture(
|
||||
params=[
|
||||
datetime(2022, 7, 22, 12, 7),
|
||||
date(2022, 7, 22),
|
||||
datetime(2022, 7, 22, 13, 7, tzinfo=ZoneInfo("Europe/Paris")),
|
||||
])
|
||||
]
|
||||
)
|
||||
def dtstart(request, set_component_start, start_end_component):
|
||||
"""Start of the event."""
|
||||
set_component_start(start_end_component, request.param)
|
||||
|
@ -55,14 +60,17 @@ def _set_component_start_init(component, start):
|
|||
component.clear()
|
||||
component.update(type(component)(d))
|
||||
|
||||
|
||||
def _set_component_dtstart(component, start):
|
||||
"""Create the event with the dtstart property."""
|
||||
component.DTSTART = start
|
||||
|
||||
|
||||
def _set_component_start_attr(component, start):
|
||||
"""Create the event with the dtstart property."""
|
||||
component.start = start
|
||||
|
||||
|
||||
def _set_component_start_ics(component, start):
|
||||
"""Create the event with the start property."""
|
||||
component.add("dtstart", start)
|
||||
|
@ -71,11 +79,20 @@ def _set_component_start_ics(component, start):
|
|||
component.clear()
|
||||
component.update(type(component).from_ical(ics))
|
||||
|
||||
@pytest.fixture(params=[_set_component_start_init, _set_component_start_ics, _set_component_dtstart, _set_component_start_attr])
|
||||
|
||||
@pytest.fixture(
|
||||
params=[
|
||||
_set_component_start_init,
|
||||
_set_component_start_ics,
|
||||
_set_component_dtstart,
|
||||
_set_component_start_attr,
|
||||
]
|
||||
)
|
||||
def set_component_start(request):
|
||||
"""Create a new event."""
|
||||
return request.param
|
||||
|
||||
|
||||
def test_component_dtstart(dtstart, start_end_component):
|
||||
"""Test the start of events."""
|
||||
assert start_end_component.DTSTART == dtstart
|
||||
|
@ -96,15 +113,17 @@ invalid_start_todo_1 = Todo(invalid_start_event_1)
|
|||
invalid_start_todo_2 = Todo(invalid_start_event_2)
|
||||
invalid_start_todo_3 = Todo(invalid_start_event_3)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"invalid_event", [
|
||||
"invalid_event",
|
||||
[
|
||||
invalid_start_event_1,
|
||||
invalid_start_event_2,
|
||||
invalid_start_event_3,
|
||||
invalid_start_todo_1,
|
||||
invalid_start_todo_2,
|
||||
invalid_start_todo_3,
|
||||
]
|
||||
],
|
||||
)
|
||||
def test_multiple_dtstart(invalid_event):
|
||||
"""Check that we get the right error."""
|
||||
|
@ -128,11 +147,13 @@ def test_no_dtstart(start_end_component):
|
|||
start_end_component.start # noqa: B018
|
||||
|
||||
|
||||
@pytest.fixture(params=[
|
||||
@pytest.fixture(
|
||||
params=[
|
||||
datetime(2022, 7, 22, 12, 8),
|
||||
date(2022, 7, 23),
|
||||
datetime(2022, 7, 22, 14, 7, tzinfo=ZoneInfo("Europe/Paris")),
|
||||
])
|
||||
]
|
||||
)
|
||||
def dtend(request, set_component_end, start_end_component):
|
||||
"""end of the event."""
|
||||
set_component_end(start_end_component, request.param)
|
||||
|
@ -146,14 +167,17 @@ def _set_component_end_init(component, end):
|
|||
component.clear()
|
||||
component.update(type(component)(d))
|
||||
|
||||
|
||||
def _set_component_end_property(component, end):
|
||||
"""Create the event with the dtend property."""
|
||||
setattr(component, prop(component, "DTEND"), end)
|
||||
|
||||
|
||||
def _set_component_end_attr(component, end):
|
||||
"""Create the event with the dtend property."""
|
||||
component.end = end
|
||||
|
||||
|
||||
def _set_component_end_ics(component, end):
|
||||
"""Create the event with the end property."""
|
||||
component.add(prop(component, "DTEND"), end)
|
||||
|
@ -162,11 +186,20 @@ def _set_component_end_ics(component, end):
|
|||
component.clear()
|
||||
component.update(type(component).from_ical(ics))
|
||||
|
||||
@pytest.fixture(params=[_set_component_end_init, _set_component_end_ics, _set_component_end_property, _set_component_end_attr])
|
||||
|
||||
@pytest.fixture(
|
||||
params=[
|
||||
_set_component_end_init,
|
||||
_set_component_end_ics,
|
||||
_set_component_end_property,
|
||||
_set_component_end_attr,
|
||||
]
|
||||
)
|
||||
def set_component_end(request):
|
||||
"""Create a new event."""
|
||||
return request.param
|
||||
|
||||
|
||||
def test_component_end_property(dtend, start_end_component):
|
||||
"""Test the end of events."""
|
||||
attr = prop(start_end_component, "DTEND")
|
||||
|
@ -186,34 +219,43 @@ def test_delete_attr(start_end_component, dtstart, dtend, attr):
|
|||
delattr(start_end_component, attr)
|
||||
|
||||
|
||||
def _set_duration_vdddtypes(event:Event, duration:timedelta):
|
||||
def _set_duration_vdddtypes(event: Event, duration: timedelta):
|
||||
"""Set the vDDDTypes value"""
|
||||
event["DURATION"] = vDDDTypes(duration)
|
||||
|
||||
def _set_duration_add(event:Event, duration:timedelta):
|
||||
|
||||
def _set_duration_add(event: Event, duration: timedelta):
|
||||
"""Set the vDDDTypes value"""
|
||||
event.add("DURATION", duration)
|
||||
|
||||
def _set_duration_vduration(event:Event, duration:timedelta):
|
||||
|
||||
def _set_duration_vduration(event: Event, duration: timedelta):
|
||||
"""Set the vDDDTypes value"""
|
||||
event["DURATION"] = vDuration(duration)
|
||||
|
||||
@pytest.fixture(params=[_set_duration_vdddtypes, _set_duration_add, _set_duration_vduration])
|
||||
|
||||
@pytest.fixture(
|
||||
params=[_set_duration_vdddtypes, _set_duration_add, _set_duration_vduration]
|
||||
)
|
||||
def duration(start_end_component, dtstart, request):
|
||||
"""... events have a DATE value type for the "DTSTART" property ...
|
||||
If such a "VEVENT" has a "DURATION"
|
||||
property, it MUST be specified as a "dur-day" or "dur-week" value.
|
||||
"""
|
||||
duration = timedelta(hours=1) if isinstance(dtstart, datetime) else timedelta(days=2)
|
||||
duration = (
|
||||
timedelta(hours=1) if isinstance(dtstart, datetime) else timedelta(days=2)
|
||||
)
|
||||
request.param(start_end_component, duration)
|
||||
return duration
|
||||
|
||||
|
||||
def test_start_and_duration(start_end_component, dtstart, duration):
|
||||
"""Check calculation of end with duration."""
|
||||
dur = start_end_component.end - start_end_component.start
|
||||
assert dur == duration
|
||||
assert start_end_component.duration == duration
|
||||
|
||||
|
||||
# The "VEVENT" is also the calendar component used to specify an
|
||||
# anniversary or daily reminder within a calendar. These events
|
||||
# have a DATE value type for the "DTSTART" property instead of the
|
||||
|
@ -247,18 +289,43 @@ invalid_todo_end_4 = Todo()
|
|||
invalid_todo_end_4.add("DTSTART", date(2024, 1, 1))
|
||||
invalid_todo_end_4.add("DURATION", timedelta(hours=1))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("invalid_component", "message"),
|
||||
[
|
||||
(invalid_event_end_1, "DTSTART and DTEND must be of the same type, either date or datetime."),
|
||||
(invalid_event_end_2, "DTSTART and DTEND must be of the same type, either date or datetime."),
|
||||
(invalid_event_end_3, "Only one of DTEND and DURATION may be in a VEVENT, not both."),
|
||||
(invalid_event_end_4, "When DTSTART is a date, DURATION must be of days or weeks."),
|
||||
(invalid_todo_end_1, "DTSTART and DUE must be of the same type, either date or datetime."),
|
||||
(invalid_todo_end_2, "DTSTART and DUE must be of the same type, either date or datetime."),
|
||||
(invalid_todo_end_3, "Only one of DUE and DURATION may be in a VTODO, not both."),
|
||||
(invalid_todo_end_4, "When DTSTART is a date, DURATION must be of days or weeks."),
|
||||
]
|
||||
(
|
||||
invalid_event_end_1,
|
||||
"DTSTART and DTEND must be of the same type, either date or datetime.",
|
||||
),
|
||||
(
|
||||
invalid_event_end_2,
|
||||
"DTSTART and DTEND must be of the same type, either date or datetime.",
|
||||
),
|
||||
(
|
||||
invalid_event_end_3,
|
||||
"Only one of DTEND and DURATION may be in a VEVENT, not both.",
|
||||
),
|
||||
(
|
||||
invalid_event_end_4,
|
||||
"When DTSTART is a date, DURATION must be of days or weeks.",
|
||||
),
|
||||
(
|
||||
invalid_todo_end_1,
|
||||
"DTSTART and DUE must be of the same type, either date or datetime.",
|
||||
),
|
||||
(
|
||||
invalid_todo_end_2,
|
||||
"DTSTART and DUE must be of the same type, either date or datetime.",
|
||||
),
|
||||
(
|
||||
invalid_todo_end_3,
|
||||
"Only one of DUE and DURATION may be in a VTODO, not both.",
|
||||
),
|
||||
(
|
||||
invalid_todo_end_4,
|
||||
"When DTSTART is a date, DURATION must be of days or weeks.",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("attr", ["start", "end"])
|
||||
def test_invalid_event(invalid_component, message, attr):
|
||||
|
@ -267,6 +334,7 @@ def test_invalid_event(invalid_component, message, attr):
|
|||
getattr(invalid_component, attr)
|
||||
assert e.value.args[0] == message
|
||||
|
||||
|
||||
def test_event_duration_zero():
|
||||
"""
|
||||
For cases where a "VEVENT" calendar component
|
||||
|
@ -300,8 +368,9 @@ def test_todo_duration_zero():
|
|||
assert todo.end == todo.start
|
||||
assert todo.duration == timedelta(days=0)
|
||||
|
||||
|
||||
def test_todo_duration_one_day():
|
||||
""" The end is at the end of the day, excluding midnight.
|
||||
"""The end is at the end of the day, excluding midnight.
|
||||
|
||||
RFC 5545:
|
||||
The following is an example of a "VTODO" calendar
|
||||
|
@ -314,7 +383,6 @@ def test_todo_duration_one_day():
|
|||
assert event.duration == timedelta(days=1)
|
||||
|
||||
|
||||
|
||||
incomplete_event_1 = Event()
|
||||
incomplete_event_2 = Event()
|
||||
incomplete_event_2.add("DURATION", timedelta(hours=1))
|
||||
|
@ -323,7 +391,6 @@ incomplete_todo_2 = Todo()
|
|||
incomplete_todo_2.add("DURATION", timedelta(hours=1))
|
||||
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"incomplete_event_end",
|
||||
[
|
||||
|
@ -331,7 +398,7 @@ incomplete_todo_2.add("DURATION", timedelta(hours=1))
|
|||
incomplete_event_2,
|
||||
incomplete_todo_1,
|
||||
incomplete_todo_2,
|
||||
]
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("attr", ["start", "end", "duration"])
|
||||
def test_incomplete_event(incomplete_event_end, attr):
|
||||
|
@ -346,23 +413,23 @@ def test_incomplete_event(incomplete_event_end, attr):
|
|||
object(),
|
||||
timedelta(days=1),
|
||||
(datetime(2024, 10, 11, 10, 20), timedelta(days=1)),
|
||||
]
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("Component", "attr"),
|
||||
[
|
||||
(Event,"start"),
|
||||
(Event,"end"),
|
||||
(Event,"DTSTART"),
|
||||
(Event,"DTEND"),
|
||||
(Journal,"start"),
|
||||
(Journal,"end"),
|
||||
(Journal,"DTSTART"),
|
||||
(Todo,"start"),
|
||||
(Todo,"end"),
|
||||
(Todo,"DTSTART"),
|
||||
(Todo,"DUE"),
|
||||
]
|
||||
(Event, "start"),
|
||||
(Event, "end"),
|
||||
(Event, "DTSTART"),
|
||||
(Event, "DTEND"),
|
||||
(Journal, "start"),
|
||||
(Journal, "end"),
|
||||
(Journal, "DTSTART"),
|
||||
(Todo, "start"),
|
||||
(Todo, "end"),
|
||||
(Todo, "DTSTART"),
|
||||
(Todo, "DUE"),
|
||||
],
|
||||
)
|
||||
def test_set_invalid_start(invalid_value, attr, Component):
|
||||
"""Check that we get the right error.
|
||||
|
@ -373,12 +440,15 @@ def test_set_invalid_start(invalid_value, attr, Component):
|
|||
component = Component()
|
||||
with pytest.raises(TypeError) as e:
|
||||
setattr(component, attr, invalid_value)
|
||||
assert e.value.args[0] == f"Use datetime or date, not {type(invalid_value).__name__}."
|
||||
assert (
|
||||
e.value.args[0] == f"Use datetime or date, not {type(invalid_value).__name__}."
|
||||
)
|
||||
|
||||
|
||||
def setitem(d:dict, key, value):
|
||||
def setitem(d: dict, key, value):
|
||||
d[key] = value
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"invalid_value",
|
||||
[
|
||||
|
@ -387,14 +457,17 @@ def setitem(d:dict, key, value):
|
|||
(datetime(2024, 10, 11, 10, 20), timedelta(days=1)),
|
||||
date(2012, 2, 2),
|
||||
datetime(2022, 2, 2),
|
||||
]
|
||||
],
|
||||
)
|
||||
def test_check_invalid_duration(start_end_component, invalid_value):
|
||||
"""Check that we get the right error."""
|
||||
start_end_component["DURATION"] = invalid_value
|
||||
with pytest.raises(InvalidCalendar) as e:
|
||||
start_end_component.DURATION # noqa: B018
|
||||
assert e.value.args[0] == f"DURATION must be a timedelta, not {type(invalid_value).__name__}."
|
||||
assert (
|
||||
e.value.args[0]
|
||||
== f"DURATION must be a timedelta, not {type(invalid_value).__name__}."
|
||||
)
|
||||
|
||||
|
||||
def test_setting_the_end_deletes_the_duration(start_end_component):
|
||||
|
@ -419,14 +492,17 @@ def test_setting_duration_deletes_the_end(start_end_component):
|
|||
assert getattr(start_end_component, DTEND) is None
|
||||
assert start_end_component.DURATION == timedelta(days=1)
|
||||
|
||||
|
||||
valid_values = pytest.mark.parametrize(
|
||||
("attr", "value"),
|
||||
[
|
||||
("DTSTART", datetime(2024, 10, 11, 10, 20)),
|
||||
("DTEND", datetime(2024, 10, 11, 10, 20)),
|
||||
("DURATION", timedelta(days=1)),
|
||||
]
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@valid_values
|
||||
def test_setting_to_none_deletes_value(start_end_component, attr, value):
|
||||
"""Setting attributes to None deletes them."""
|
||||
|
@ -462,12 +538,16 @@ def test_delete_duration(start_end_component):
|
|||
del start_end_component.DURATION
|
||||
assert start_end_component.DURATION is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("attr", ["DTSTART", "end", "start"])
|
||||
@pytest.mark.parametrize("start", [
|
||||
datetime(2024, 10, 11, 10, 20),
|
||||
date(2024, 10, 11),
|
||||
datetime(2024, 10, 11, 10, 20, tzinfo=ZoneInfo("Europe/Paris")),
|
||||
])
|
||||
@pytest.mark.parametrize(
|
||||
"start",
|
||||
[
|
||||
datetime(2024, 10, 11, 10, 20),
|
||||
date(2024, 10, 11),
|
||||
datetime(2024, 10, 11, 10, 20, tzinfo=ZoneInfo("Europe/Paris")),
|
||||
],
|
||||
)
|
||||
def test_journal_start(start, attr):
|
||||
"""Test that we can set the start of a journal."""
|
||||
j = Journal()
|
||||
|
@ -477,6 +557,7 @@ def test_journal_start(start, attr):
|
|||
assert j.end == start
|
||||
assert j.duration == timedelta(0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("attr", ["start", "end"])
|
||||
def test_delete_journal_start(attr):
|
||||
"""Delete the start of the journal."""
|
||||
|
@ -488,9 +569,10 @@ def test_delete_journal_start(attr):
|
|||
with pytest.raises(IncompleteComponent):
|
||||
getattr(j, attr)
|
||||
|
||||
|
||||
def setting_twice_does_not_duplicate_the_entry():
|
||||
j = Journal()
|
||||
j.DTSTART = date(2024, 1,1 )
|
||||
j.DTSTART = date(2024, 1, 1)
|
||||
j.DTSTART = date(2024, 1, 3)
|
||||
assert date(2024, 1, 3) == j.DTSTART
|
||||
assert j.start == date(2024, 1, 3)
|
||||
|
@ -500,10 +582,14 @@ def setting_twice_does_not_duplicate_the_entry():
|
|||
@pytest.mark.parametrize(
|
||||
("file", "trigger", "related"),
|
||||
[
|
||||
("rfc_5545_absolute_alarm_example", vDatetime.from_ical("19970317T133000Z"), "START"),
|
||||
(
|
||||
"rfc_5545_absolute_alarm_example",
|
||||
vDatetime.from_ical("19970317T133000Z"),
|
||||
"START",
|
||||
),
|
||||
("rfc_5545_end", timedelta(days=-2), "END"),
|
||||
("start_date", timedelta(days=-2), "START"),
|
||||
]
|
||||
],
|
||||
)
|
||||
def test_get_alarm_trigger_property(alarms, file, trigger, related):
|
||||
"""Get the trigger property."""
|
||||
|
@ -533,21 +619,38 @@ def test_get_related_without_trigger():
|
|||
"""The default is start"""
|
||||
assert Alarm().TRIGGER_RELATED == "START"
|
||||
|
||||
|
||||
def test_cannot_set_related_without_trigger():
|
||||
"""TRIGGER must be set to set the parameter."""
|
||||
with pytest.raises(ValueError) as e:
|
||||
a = Alarm()
|
||||
a.TRIGGER_RELATED = "END"
|
||||
assert e.value.args[0] == "You must set a TRIGGER before setting the RELATED parameter."
|
||||
assert (
|
||||
e.value.args[0]
|
||||
== "You must set a TRIGGER before setting the RELATED parameter."
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("file", "triggers"),
|
||||
[
|
||||
("rfc_5545_absolute_alarm_example", ((), (), (vDatetime.from_ical("19970317T133000Z"), vDatetime.from_ical("19970317T134500Z"),vDatetime.from_ical("19970317T140000Z"),vDatetime.from_ical("19970317T141500Z"),vDatetime.from_ical("19970317T143000Z"),))),
|
||||
(
|
||||
"rfc_5545_absolute_alarm_example",
|
||||
(
|
||||
(),
|
||||
(),
|
||||
(
|
||||
vDatetime.from_ical("19970317T133000Z"),
|
||||
vDatetime.from_ical("19970317T134500Z"),
|
||||
vDatetime.from_ical("19970317T140000Z"),
|
||||
vDatetime.from_ical("19970317T141500Z"),
|
||||
vDatetime.from_ical("19970317T143000Z"),
|
||||
),
|
||||
),
|
||||
),
|
||||
("rfc_5545_end", ((), (timedelta(days=-2),), ())),
|
||||
("start_date", ((timedelta(days=-2),), (), ())),
|
||||
]
|
||||
],
|
||||
)
|
||||
def test_get_alarm_triggers(alarms, file, triggers):
|
||||
"""Get the trigger property."""
|
||||
|
@ -561,8 +664,10 @@ def test_triggers_emtpy_alarm():
|
|||
"""An alarm with no trigger has no triggers."""
|
||||
assert Alarm().triggers == ((), (), ())
|
||||
|
||||
|
||||
h1 = timedelta(hours=1)
|
||||
|
||||
|
||||
def test_triggers_emtpy_with_no_repeat():
|
||||
"""Check incomplete values."""
|
||||
a = Alarm()
|
||||
|
@ -570,6 +675,7 @@ def test_triggers_emtpy_with_no_repeat():
|
|||
a.DURATION = h1
|
||||
assert a.triggers == ((h1,), (), ())
|
||||
|
||||
|
||||
def test_triggers_emtpy_with_no_duration():
|
||||
"""Check incomplete values."""
|
||||
a = Alarm()
|
||||
|
@ -581,10 +687,13 @@ def test_triggers_emtpy_with_no_duration():
|
|||
@pytest.mark.parametrize(
|
||||
("file", "triggers"),
|
||||
[
|
||||
("rfc_5545_absolute_alarm_example", ((), (), (vDatetime.from_ical("19970317T133000Z"),))),
|
||||
(
|
||||
"rfc_5545_absolute_alarm_example",
|
||||
((), (), (vDatetime.from_ical("19970317T133000Z"),)),
|
||||
),
|
||||
("rfc_5545_end", ((), (timedelta(days=-2),), ())),
|
||||
("start_date", ((timedelta(days=-2),), (), ())),
|
||||
]
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("duration", [timedelta(days=-1), h1])
|
||||
@pytest.mark.parametrize("repeat", [1, 3])
|
||||
|
|
|
@ -12,7 +12,6 @@ DTSTART, DTEND, and DURATION.
|
|||
|
||||
"""
|
||||
|
||||
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
|
||||
import pytest
|
||||
|
@ -46,12 +45,8 @@ alarm_1.add("TRIGGER", EXAMPLE_TRIGGER)
|
|||
alarm_2 = Alarm()
|
||||
alarm_2["TRIGGER"] = vDatetime(EXAMPLE_TRIGGER)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"alarm",
|
||||
[
|
||||
alarm_1, alarm_2
|
||||
]
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize("alarm", [alarm_1, alarm_2])
|
||||
def test_absolute_alarm_time_with_vDatetime(alarm):
|
||||
"""Check that the absolute alarm is recognized.
|
||||
|
||||
|
@ -72,6 +67,7 @@ alarm_incomplete_2 = Alarm()
|
|||
alarm_incomplete_2.TRIGGER = timedelta(hours=2)
|
||||
alarm_incomplete_2.REPEAT = 100
|
||||
|
||||
|
||||
@pytest.mark.parametrize("alarm", [alarm_incomplete_1, alarm_incomplete_2])
|
||||
def test_alarm_has_only_one_of_repeat_or_duration(alarm):
|
||||
"""This is an edge case and we should ignore the repetition."""
|
||||
|
@ -95,21 +91,34 @@ def test_cannot_compute_relative_alarm_without_start(alarm_before_start):
|
|||
"""We have an alarm without a start of a component."""
|
||||
with pytest.raises(IncompleteAlarmInformation) as e:
|
||||
Alarms(alarm_before_start).times # noqa: B018
|
||||
assert e.value.args[0] == f"Use {Alarms.__name__}.{Alarms.set_start.__name__} because at least one alarm is relative to the start of a component."
|
||||
assert (
|
||||
e.value.args[0]
|
||||
== f"Use {Alarms.__name__}.{Alarms.set_start.__name__} because at least one alarm is relative to the start of a component."
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("dtstart", "timezone", "trigger"),
|
||||
[
|
||||
(datetime(2024, 10, 29, 13, 10), "UTC", datetime(2024, 10, 29, 13, 10, tzinfo=UTC)),
|
||||
(
|
||||
datetime(2024, 10, 29, 13, 10),
|
||||
"UTC",
|
||||
datetime(2024, 10, 29, 13, 10, tzinfo=UTC),
|
||||
),
|
||||
(date(2024, 11, 16), None, datetime(2024, 11, 16, 0, 0)),
|
||||
(datetime(2024, 10, 29, 13, 10), "Asia/Singapore", datetime(2024, 10, 29, 5, 10, tzinfo=UTC)),
|
||||
(
|
||||
datetime(2024, 10, 29, 13, 10),
|
||||
"Asia/Singapore",
|
||||
datetime(2024, 10, 29, 5, 10, tzinfo=UTC),
|
||||
),
|
||||
(datetime(2024, 10, 29, 13, 20), None, datetime(2024, 10, 29, 13, 20)),
|
||||
]
|
||||
],
|
||||
)
|
||||
def test_can_complete_relative_calculation_if_a_start_is_given(alarm_before_start, dtstart, timezone, trigger, tzp):
|
||||
def test_can_complete_relative_calculation_if_a_start_is_given(
|
||||
alarm_before_start, dtstart, timezone, trigger, tzp
|
||||
):
|
||||
"""The start is given and required."""
|
||||
start = (dtstart if timezone is None else tzp.localize(dtstart, timezone))
|
||||
start = dtstart if timezone is None else tzp.localize(dtstart, timezone)
|
||||
alarms = Alarms(alarm_before_start)
|
||||
alarms.set_start(start)
|
||||
assert len(alarms.times) == 1
|
||||
|
@ -131,21 +140,32 @@ def test_cannot_compute_relative_alarm_without_end(alarms):
|
|||
"""We have an alarm without an end of a component."""
|
||||
with pytest.raises(IncompleteAlarmInformation) as e:
|
||||
Alarms(alarms.rfc_5545_end).times # noqa: B018
|
||||
assert e.value.args[0] == f"Use {Alarms.__name__}.{Alarms.set_end.__name__} because at least one alarm is relative to the end of a component."
|
||||
assert (
|
||||
e.value.args[0]
|
||||
== f"Use {Alarms.__name__}.{Alarms.set_end.__name__} because at least one alarm is relative to the end of a component."
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("dtend", "timezone", "trigger"),
|
||||
[
|
||||
(datetime(2024, 10, 29, 13, 10), "UTC", datetime(2024, 10, 29, 13, 10, tzinfo=UTC)),
|
||||
(
|
||||
datetime(2024, 10, 29, 13, 10),
|
||||
"UTC",
|
||||
datetime(2024, 10, 29, 13, 10, tzinfo=UTC),
|
||||
),
|
||||
(date(2024, 11, 16), None, date(2024, 11, 16)),
|
||||
(datetime(2024, 10, 29, 13, 10), "Asia/Singapore", datetime(2024, 10, 29, 5, 10, tzinfo=UTC)),
|
||||
(
|
||||
datetime(2024, 10, 29, 13, 10),
|
||||
"Asia/Singapore",
|
||||
datetime(2024, 10, 29, 5, 10, tzinfo=UTC),
|
||||
),
|
||||
(datetime(2024, 10, 29, 13, 20), None, datetime(2024, 10, 29, 13, 20)),
|
||||
]
|
||||
],
|
||||
)
|
||||
def test_can_complete_relative_calculation(alarms, dtend, timezone, trigger, tzp):
|
||||
"""The start is given and required."""
|
||||
start = (dtend if timezone is None else tzp.localize(dtend, timezone))
|
||||
start = dtend if timezone is None else tzp.localize(dtend, timezone)
|
||||
alarms = Alarms(alarms.rfc_5545_end)
|
||||
alarms.set_end(start)
|
||||
assert len(alarms.times) == 1
|
||||
|
@ -163,7 +183,6 @@ def test_end_as_date_with_delta_as_date_stays_date(alarms, dtend):
|
|||
assert a.times[0].trigger == dtend - timedelta(days=2)
|
||||
|
||||
|
||||
|
||||
def test_add_multiple_alarms(alarms):
|
||||
"""We can add multiple alarms."""
|
||||
a = Alarms()
|
||||
|
@ -202,31 +221,82 @@ def test_cannot_set_the_event_twice(calendars):
|
|||
[
|
||||
("alarm_etar_future", -1, 3, "Etar (1): we just created the alarm"),
|
||||
("alarm_etar_notification", -1, 2, "Etar (2): the notification popped up"),
|
||||
("alarm_etar_notification_clicked", -1, 0, "Etar (3): the notification was dismissed"), # TODO: check that that is really true
|
||||
("alarm_google_future", -1, 4, "Google (1): we just created the event with alarms"),
|
||||
("alarm_google_acknowledged", -1, 2, "Google (2): 2 alarms happened at the same time"),
|
||||
(
|
||||
"alarm_etar_notification_clicked",
|
||||
-1,
|
||||
0,
|
||||
"Etar (3): the notification was dismissed",
|
||||
), # TODO: check that that is really true
|
||||
(
|
||||
"alarm_google_future",
|
||||
-1,
|
||||
4,
|
||||
"Google (1): we just created the event with alarms",
|
||||
),
|
||||
(
|
||||
"alarm_google_acknowledged",
|
||||
-1,
|
||||
2,
|
||||
"Google (2): 2 alarms happened at the same time",
|
||||
),
|
||||
("alarm_thunderbird_future", -1, 2, "Thunderbird (1.1): 2 alarms are set"),
|
||||
("alarm_thunderbird_snoozed_until_1457", -1, 2, "Thunderbird (1.2): 2 alarms are snoozed to another time"),
|
||||
("alarm_thunderbird_closed", -1, 0, "Thunderbird (1.3): all alarms are dismissed (closed)"),
|
||||
(
|
||||
"alarm_thunderbird_snoozed_until_1457",
|
||||
-1,
|
||||
2,
|
||||
"Thunderbird (1.2): 2 alarms are snoozed to another time",
|
||||
),
|
||||
(
|
||||
"alarm_thunderbird_closed",
|
||||
-1,
|
||||
0,
|
||||
"Thunderbird (1.3): all alarms are dismissed (closed)",
|
||||
),
|
||||
("alarm_thunderbird_2_future", -1, 2, "Thunderbird (2.1): 2 alarms active"),
|
||||
("alarm_thunderbird_2_notification_popped_up", -1, 2, "Thunderbird (2.2): one alarm popped up as a notification"),
|
||||
("alarm_thunderbird_2_notification_5_min_postponed", -1, 2, "Thunderbird (2.3): 1 alarm active and one postponed by 5 minutes"),
|
||||
("alarm_thunderbird_2_notification_5_min_postponed_and_popped_up", -1, 2, "Thunderbird (2.4): 1 alarm active and one postponed by 5 minutes and now popped up"),
|
||||
("alarm_thunderbird_2_notification_5_min_postponed_and_closed", -1, 1, "Thunderbird (2.5): 1 alarm active and one postponed by 5 minutes and is now acknowledged"),
|
||||
]
|
||||
(
|
||||
"alarm_thunderbird_2_notification_popped_up",
|
||||
-1,
|
||||
2,
|
||||
"Thunderbird (2.2): one alarm popped up as a notification",
|
||||
),
|
||||
(
|
||||
"alarm_thunderbird_2_notification_5_min_postponed",
|
||||
-1,
|
||||
2,
|
||||
"Thunderbird (2.3): 1 alarm active and one postponed by 5 minutes",
|
||||
),
|
||||
(
|
||||
"alarm_thunderbird_2_notification_5_min_postponed_and_popped_up",
|
||||
-1,
|
||||
2,
|
||||
"Thunderbird (2.4): 1 alarm active and one postponed by 5 minutes and now popped up",
|
||||
),
|
||||
(
|
||||
"alarm_thunderbird_2_notification_5_min_postponed_and_closed",
|
||||
-1,
|
||||
1,
|
||||
"Thunderbird (2.5): 1 alarm active and one postponed by 5 minutes and is now acknowledged",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_number_of_active_alarms_from_calendar_software(calendars, calendar, index, count, message):
|
||||
def test_number_of_active_alarms_from_calendar_software(
|
||||
calendars, calendar, index, count, message
|
||||
):
|
||||
"""Check that we extract calculate the correct amount of active alarms."""
|
||||
event = calendars[calendar].subcomponents[index]
|
||||
a = Alarms(event)
|
||||
active_alarms = a.active # We do not need to pass a timezone because the events have a timezone
|
||||
assert len(active_alarms) == count, f"{message} - I expect {count} alarms active but got {len(active_alarms)}."
|
||||
active_alarms = (
|
||||
a.active
|
||||
) # We do not need to pass a timezone because the events have a timezone
|
||||
assert (
|
||||
len(active_alarms) == count
|
||||
), f"{message} - I expect {count} alarms active but got {len(active_alarms)}."
|
||||
|
||||
|
||||
three_alarms = Alarm()
|
||||
three_alarms.REPEAT = 2
|
||||
three_alarms.add("DURATION", timedelta(hours=1)) # 2 hours & 1 hour before
|
||||
three_alarms.add("TRIGGER", -timedelta(hours=3)) # 3 hours before
|
||||
three_alarms.add("DURATION", timedelta(hours=1)) # 2 hours & 1 hour before
|
||||
three_alarms.add("TRIGGER", -timedelta(hours=3)) # 3 hours before
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
@ -236,10 +306,17 @@ three_alarms.add("TRIGGER", -timedelta(hours=3)) # 3 hours before
|
|||
(datetime(2024, 10, 10, 12), datetime(2024, 10, 10, 9, 1), "UTC", 2),
|
||||
(datetime(2024, 10, 10, 12), datetime(2024, 10, 10, 10, 1), "UTC", 1),
|
||||
(datetime(2024, 10, 10, 12), datetime(2024, 10, 10, 11, 1), "UTC", 0),
|
||||
(datetime(2024, 10, 10, 12, tzinfo=timezone.utc), datetime(2024, 10, 10, 11, 1), None, 0),
|
||||
]
|
||||
(
|
||||
datetime(2024, 10, 10, 12, tzinfo=timezone.utc),
|
||||
datetime(2024, 10, 10, 11, 1),
|
||||
None,
|
||||
0,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_number_of_active_alarms_with_moving_time(start, acknowledged, count, tzp, timezone):
|
||||
def test_number_of_active_alarms_with_moving_time(
|
||||
start, acknowledged, count, tzp, timezone
|
||||
):
|
||||
"""Check how many alarms are active after a time they are acknowledged."""
|
||||
a = Alarms()
|
||||
a.add_alarm(three_alarms)
|
||||
|
@ -258,7 +335,10 @@ def test_incomplete_alarm_information_for_active_state(tzp):
|
|||
a.acknowledge_until(tzp.localize_utc(datetime(2012, 10, 10, 12)))
|
||||
with pytest.raises(IncompleteAlarmInformation) as e:
|
||||
a.active # noqa: B018
|
||||
assert e.value.args[0] == f"A local timezone is required to check if the alarm is still active. Use Alarms.{Alarms.set_local_timezone.__name__}()."
|
||||
assert (
|
||||
e.value.args[0]
|
||||
== f"A local timezone is required to check if the alarm is still active. Use Alarms.{Alarms.set_local_timezone.__name__}()."
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
@ -269,7 +349,7 @@ def test_incomplete_alarm_information_for_active_state(tzp):
|
|||
"alarm_thunderbird_closed",
|
||||
"alarm_thunderbird_future",
|
||||
"alarm_thunderbird_snoozed_until_1457",
|
||||
]
|
||||
],
|
||||
)
|
||||
def test_thunderbird_recognition(calendars, calendar_name):
|
||||
"""Check if we correctly discover Thunderbird's alarm algorithm."""
|
||||
|
@ -282,12 +362,12 @@ def test_thunderbird_recognition(calendars, calendar_name):
|
|||
@pytest.mark.parametrize(
|
||||
"snooze",
|
||||
[
|
||||
datetime(2012, 10, 10, 11, 1), # before everything
|
||||
datetime(2012, 10, 10, 11, 1), # before everything
|
||||
datetime(2017, 12, 1, 10, 1),
|
||||
datetime(2017, 12, 1, 11, 1),
|
||||
datetime(2017, 12, 1, 12, 1),
|
||||
datetime(2017, 12, 1, 13, 1), # snooze until after the start of the event
|
||||
]
|
||||
datetime(2017, 12, 1, 13, 1), # snooze until after the start of the event
|
||||
],
|
||||
)
|
||||
def test_snoozed_alarm_has_trigger_at_snooze_time(tzp, snooze):
|
||||
"""When an alarm is snoozed, it pops up after the snooze time."""
|
||||
|
@ -322,7 +402,7 @@ def test_snoozed_alarm_has_trigger_at_snooze_time(tzp, snooze):
|
|||
# When the second "snooze" alarm is triggered, the user decides to dismiss it.
|
||||
# The client acknowledges both the original alarm and the second "snooze" alarm:
|
||||
(4, ()),
|
||||
]
|
||||
],
|
||||
)
|
||||
def test_rfc_9074_alarm_times(events, event_index, alarm_times):
|
||||
"""Test the examples from the RFC and their timing.
|
||||
|
@ -331,7 +411,9 @@ def test_rfc_9074_alarm_times(events, event_index, alarm_times):
|
|||
"""
|
||||
a = events[f"rfc_9074_example_{event_index}"].alarms
|
||||
assert len(a.active) == len(alarm_times)
|
||||
expected_alarm_times = {vDatetime.from_ical(t, "America/New_York") for t in alarm_times}
|
||||
expected_alarm_times = {
|
||||
vDatetime.from_ical(t, "America/New_York") for t in alarm_times
|
||||
}
|
||||
computed_alarm_times = {alarm.trigger for alarm in a.active}
|
||||
assert expected_alarm_times == computed_alarm_times
|
||||
|
||||
|
@ -344,4 +426,4 @@ def test_set_to_None():
|
|||
a.set_local_timezone(None)
|
||||
a.acknowledge_until(None)
|
||||
a.snooze_until(None)
|
||||
assert vars(a) == vars(Alarms())
|
||||
assert vars(a) == vars(Alarms())
|
||||
|
|
|
@ -14,6 +14,7 @@ from re import findall
|
|||
|
||||
import pytest
|
||||
from dateutil.tz import gettz
|
||||
|
||||
try:
|
||||
from zoneinfo import available_timezones
|
||||
except ImportError:
|
||||
|
@ -22,25 +23,36 @@ except ImportError:
|
|||
from icalendar import Calendar, Component, Event, Timezone
|
||||
from icalendar.timezone import tzid_from_tzinfo, tzids_from_tzinfo
|
||||
|
||||
tzids = pytest.mark.parametrize("tzid", [
|
||||
"Europe/Berlin",
|
||||
"Asia/Singapore",
|
||||
"America/New_York",
|
||||
])
|
||||
tzids = pytest.mark.parametrize(
|
||||
"tzid",
|
||||
[
|
||||
"Europe/Berlin",
|
||||
"Asia/Singapore",
|
||||
"America/New_York",
|
||||
],
|
||||
)
|
||||
|
||||
def assert_components_equal(c1:Component, c2:Component):
|
||||
|
||||
def assert_components_equal(c1: Component, c2: Component):
|
||||
"""Print the diff of two components."""
|
||||
ML = 32
|
||||
ll1 = c1.to_ical().decode().splitlines()
|
||||
ll2 = c2.to_ical().decode().splitlines()
|
||||
pad = max(len(l) for l in ll1 if len(l) <=ML)
|
||||
pad = max(len(l) for l in ll1 if len(l) <= ML)
|
||||
diff = 0
|
||||
for l1, l2 in zip(ll1, ll2):
|
||||
a = len(l1) > 32 or len(l2) > 32
|
||||
print(a * " " + l1, " " * (pad - len(l1)), a* "\n->" + l2, " "*(pad - len(l2)), "\tdiff!" if l1 != l2 else "")
|
||||
print(
|
||||
a * " " + l1,
|
||||
" " * (pad - len(l1)),
|
||||
a * "\n->" + l2,
|
||||
" " * (pad - len(l2)),
|
||||
"\tdiff!" if l1 != l2 else "",
|
||||
)
|
||||
diff += l1 != l2
|
||||
assert not diff, f"{diff} lines differ"
|
||||
|
||||
|
||||
@tzids
|
||||
def test_conversion_converges(tzp, tzid):
|
||||
"""tzinfo -> VTIMEZONE -> tzinfo -> VTIMEZONE
|
||||
|
@ -48,11 +60,15 @@ def test_conversion_converges(tzp, tzid):
|
|||
We can assume that both generated VTIMEZONEs are equivalent.
|
||||
"""
|
||||
if tzp.uses_pytz():
|
||||
pytest.skip("pytz will not converge on the first run. This is problematic. PYTZ-TODO")
|
||||
pytest.skip(
|
||||
"pytz will not converge on the first run. This is problematic. PYTZ-TODO"
|
||||
)
|
||||
tzinfo1 = tzp.timezone(tzid)
|
||||
assert tzinfo1 is not None
|
||||
generated1 = Timezone.from_tzinfo(tzinfo1)
|
||||
generated1["TZID"] = "test-generated" # change the TZID so we do not use an existing one
|
||||
generated1["TZID"] = (
|
||||
"test-generated" # change the TZID so we do not use an existing one
|
||||
)
|
||||
tzinfo2 = generated1.to_tz()
|
||||
generated2 = Timezone.from_tzinfo(tzinfo2, "test-generated")
|
||||
tzinfo3 = generated2.to_tz()
|
||||
|
@ -70,7 +86,6 @@ def test_conversion_converges(tzp, tzid):
|
|||
assert generated1 == generated2
|
||||
|
||||
|
||||
|
||||
@tzids
|
||||
def both_tzps_generate_the_same_info(tzid, tzp):
|
||||
"""We want to make sure that we get the same info for all timezone implementations.
|
||||
|
@ -81,7 +96,7 @@ def both_tzps_generate_the_same_info(tzid, tzp):
|
|||
"""
|
||||
# default generation
|
||||
tz1 = Timezone.from_tzid(tzid, tzp, last_date=date(2024, 1, 1))
|
||||
tzp.use_zoneinfo() # we compare to zoneinfo
|
||||
tzp.use_zoneinfo() # we compare to zoneinfo
|
||||
tz2 = Timezone.from_tzid(tzid, tzp, last_date=date(2024, 1, 1))
|
||||
assert_components_equal(tz1, tz2)
|
||||
assert tz1 == tz2
|
||||
|
@ -90,7 +105,7 @@ def both_tzps_generate_the_same_info(tzid, tzp):
|
|||
@tzids
|
||||
def test_tzid_matches(tzid, tzp):
|
||||
"""Check the TZID."""
|
||||
tz = Timezone.from_tzinfo(tzp.timezone(tzid))
|
||||
tz = Timezone.from_tzinfo(tzp.timezone(tzid))
|
||||
assert tz["TZID"] == tzid
|
||||
|
||||
|
||||
|
@ -127,8 +142,10 @@ def test_berlin_time(tzp):
|
|||
def test_range_is_not_crossed():
|
||||
first_date = datetime(2023, 1, 1)
|
||||
last_date = datetime(2024, 1, 1)
|
||||
|
||||
def check(dt):
|
||||
assert first_date <= dt <= last_date
|
||||
|
||||
tz = Timezone.from_tzid("Europe/Berlin", last_date=last_date, first_date=first_date)
|
||||
for sub in tz.standard + tz.daylight:
|
||||
check(sub.DTSTART)
|
||||
|
@ -147,6 +164,7 @@ def test_use_the_original_timezone(tzid, tzp):
|
|||
assert type(tzinfo1) == type(tzinfo2)
|
||||
assert tzinfo1 == tzinfo2
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("tzid", "dt", "tzname"),
|
||||
[
|
||||
|
@ -155,19 +173,16 @@ def test_use_the_original_timezone(tzid, tzp):
|
|||
("Asia/Singapore", datetime(1981, 12, 31, 23, 10), "+0730"),
|
||||
("Asia/Singapore", datetime(1981, 12, 31, 23, 34), "+0730"),
|
||||
("Asia/Singapore", datetime(1981, 12, 31, 23, 59, 59), "+0730"),
|
||||
|
||||
("Asia/Singapore", datetime(1982, 1, 1), "+08"),
|
||||
("Asia/Singapore", datetime(1982, 1, 1, 0, 1), "+08"),
|
||||
("Asia/Singapore", datetime(1982, 1, 1, 0, 34), "+08"),
|
||||
("Asia/Singapore", datetime(1982, 1, 1, 1, 0), "+08"),
|
||||
("Asia/Singapore", datetime(1982, 1, 1, 1, 1), "+08"),
|
||||
|
||||
("Europe/Berlin", datetime(1970, 1, 1), "CET"),
|
||||
("Europe/Berlin", datetime(2024, 3, 31, 0, 0), "CET"),
|
||||
("Europe/Berlin", datetime(2024, 3, 31, 1, 0), "CET"),
|
||||
("Europe/Berlin", datetime(2024, 3, 31, 2, 0), "CET"),
|
||||
("Europe/Berlin", datetime(2024, 3, 31, 2, 59, 59), "CET"),
|
||||
|
||||
("Europe/Berlin", datetime(2024, 3, 31, 3, 0), "CEST"),
|
||||
("Europe/Berlin", datetime(2024, 3, 31, 3, 0, 1), "CEST"),
|
||||
("Europe/Berlin", datetime(2024, 3, 31, 4, 0), "CEST"),
|
||||
|
@ -176,11 +191,9 @@ def test_use_the_original_timezone(tzid, tzp):
|
|||
("Europe/Berlin", datetime(2024, 10, 27, 2, 0), "CEST"),
|
||||
("Europe/Berlin", datetime(2024, 10, 27, 2, 30), "CEST"),
|
||||
("Europe/Berlin", datetime(2024, 10, 27, 2, 59, 59), "CEST"),
|
||||
|
||||
("Europe/Berlin", datetime(2024, 10, 27, 3, 0), "CET"),
|
||||
("Europe/Berlin", datetime(2024, 10, 27, 3, 0, 1), "CET"),
|
||||
("Europe/Berlin", datetime(2024, 10, 27, 4, 0), "CET"),
|
||||
|
||||
# transition times from https://www.zeitverschiebung.net/de/timezone/america--new_york
|
||||
("America/New_York", datetime(1970, 1, 1), "EST"),
|
||||
# Daylight Saving Time
|
||||
|
@ -198,7 +211,7 @@ def test_use_the_original_timezone(tzid, tzp):
|
|||
("America/New_York", datetime(2025, 3, 9, 3, 0), "EDT"),
|
||||
("America/New_York", datetime(2025, 3, 9, 3, 1, 1), "EDT"),
|
||||
("America/New_York", datetime(2025, 3, 9, 4, 0), "EDT"),
|
||||
]
|
||||
],
|
||||
)
|
||||
def test_check_datetimes_around_transition_times(tzp, tzid, dt, tzname):
|
||||
"""We should make sure than the datetimes with the generated timezones
|
||||
|
@ -214,24 +227,22 @@ def test_check_datetimes_around_transition_times(tzp, tzid, dt, tzname):
|
|||
# generated_dt = generated_tzinfo.localize(dt)
|
||||
generated_dt = generated_tzinfo.normalize(generated_dt)
|
||||
if dt in (
|
||||
datetime(2024, 10, 27, 1, 0),
|
||||
datetime(2024, 11, 3, 1, 59, 59),
|
||||
datetime(2024, 11, 3, 1, 0),
|
||||
datetime(2024, 11, 3, 0, 0),
|
||||
datetime(2024, 10, 27, 2, 59, 59),
|
||||
datetime(2024, 10, 27, 2, 30),
|
||||
datetime(2024, 10, 27, 2, 0),
|
||||
datetime(2024, 10, 27, 1, 0)
|
||||
):
|
||||
datetime(2024, 10, 27, 1, 0),
|
||||
datetime(2024, 11, 3, 1, 59, 59),
|
||||
datetime(2024, 11, 3, 1, 0),
|
||||
datetime(2024, 11, 3, 0, 0),
|
||||
datetime(2024, 10, 27, 2, 59, 59),
|
||||
datetime(2024, 10, 27, 2, 30),
|
||||
datetime(2024, 10, 27, 2, 0),
|
||||
datetime(2024, 10, 27, 1, 0),
|
||||
):
|
||||
pytest.skip("We do not know how to do this. PYTZ-TODO")
|
||||
assert generated_dt.tzname() == expected_dt.tzname() == tzname, message
|
||||
assert generated_dt.dst() == expected_dt.dst(), message
|
||||
assert generated_dt.utcoffset() == expected_dt.utcoffset(), message
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"uid", [0, 1, 2, 3]
|
||||
)
|
||||
@pytest.mark.parametrize("uid", [0, 1, 2, 3])
|
||||
def test_dateutil_timezone_when_time_is_going_backwards(calendars, tzp, uid):
|
||||
"""When going from Daylight Saving Time to Standard Time, times can be ambiguous.
|
||||
For example, at 3:00 AM, the time falls back to 2:00 AM, repeating a full hour of times from 2:00 AM to 3:00 AM on the same date.
|
||||
|
@ -242,20 +253,21 @@ def test_dateutil_timezone_when_time_is_going_backwards(calendars, tzp, uid):
|
|||
|
||||
Each event has its timezone saved in it.
|
||||
"""
|
||||
cal : Calendar = calendars.issue_722_timezone_transition_ambiguity
|
||||
event : Event = cal.events[uid]
|
||||
cal: Calendar = calendars.issue_722_timezone_transition_ambiguity
|
||||
event: Event = cal.events[uid]
|
||||
expected_tzname = str(event["X-TZNAME"])
|
||||
actual_tzname = event.start.tzname()
|
||||
assert actual_tzname == expected_tzname, event["SUMMARY"]
|
||||
|
||||
|
||||
def query_tzid(query:str, cal:Calendar) -> str:
|
||||
def query_tzid(query: str, cal: Calendar) -> str:
|
||||
"""The tzid from the query."""
|
||||
try:
|
||||
tzinfo = eval(query, {"cal": cal}) # noqa: S307
|
||||
except Exception as e:
|
||||
raise ValueError(query) from e
|
||||
return tzid_from_tzinfo(tzinfo)
|
||||
return tzid_from_tzinfo(tzinfo)
|
||||
|
||||
|
||||
# these are queries for all the places that have a TZID
|
||||
# according to RFC 5545
|
||||
|
@ -270,6 +282,7 @@ queries = [
|
|||
"cal.events[2].get('RDATE')[1].dts[0].dt.tzinfo", # RDATE multiple
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("query", queries)
|
||||
def test_add_missing_timezones_to_example(calendars, query):
|
||||
"""Add the missing timezones to the calendar."""
|
||||
|
@ -278,6 +291,7 @@ def test_add_missing_timezones_to_example(calendars, query):
|
|||
tzs = cal.get_missing_tzids()
|
||||
assert tzid in tzs
|
||||
|
||||
|
||||
def test_queries_yield_unique_tzids(calendars):
|
||||
"""We make sure each query tests a unique place to find for the algorithm."""
|
||||
cal = calendars.issue_722_missing_timezones
|
||||
|
@ -288,6 +302,7 @@ def test_queries_yield_unique_tzids(calendars):
|
|||
tzids.add(tzid)
|
||||
assert len(tzids) == len(queries)
|
||||
|
||||
|
||||
def test_we_do_not_miss_to_add_a_query(calendars):
|
||||
"""Make sure all tzids are actually queried."""
|
||||
cal = calendars.issue_722_missing_timezones
|
||||
|
@ -296,35 +311,40 @@ def test_we_do_not_miss_to_add_a_query(calendars):
|
|||
assert cal.get_used_tzids() == ids, "We find all tzids and they are unique."
|
||||
assert len(ids) == len(queries), "We query all the tzids."
|
||||
|
||||
|
||||
def test_unknown_tzid(calendars):
|
||||
"""If we have an unknown tzid with no timezone component."""
|
||||
cal = calendars.issue_722_missing_VTIMEZONE_custom
|
||||
assert "CUSTOM_tzid" in cal.get_used_tzids()
|
||||
assert "CUSTOM_tzid" in cal.get_missing_tzids()
|
||||
|
||||
|
||||
def test_custom_timezone_is_found_and_used(calendars):
|
||||
"""Check the custom timezone component is not missing."""
|
||||
cal = calendars.america_new_york
|
||||
assert "custom_America/New_York" in cal.get_used_tzids()
|
||||
assert "custom_America/New_York" not in cal.get_missing_tzids()
|
||||
|
||||
|
||||
def test_not_missing_anything():
|
||||
"""Check that no timezone is missing."""
|
||||
cal = Calendar()
|
||||
assert cal.get_missing_tzids() == set()
|
||||
|
||||
|
||||
def test_utc_is_not_missing(calendars):
|
||||
"""UTC should not be found missing."""
|
||||
cal = calendars.issue_722_missing_timezones
|
||||
assert "UTC" not in cal.get_missing_tzids()
|
||||
assert "UTC" not in cal.get_used_tzids()
|
||||
|
||||
|
||||
def test_dateutil_timezone_is_not_found_with_tzname(calendars, no_pytz):
|
||||
"""dateutil is an example of a timezone that has no tzid.
|
||||
|
||||
In this test we make sure that the timezone is said to be missing.
|
||||
"""
|
||||
cal : Calendar = calendars.america_new_york
|
||||
cal: Calendar = calendars.america_new_york
|
||||
cal.subcomponents.remove(cal.timezones[0])
|
||||
assert cal.get_missing_tzids() == {"custom_America/New_York"}
|
||||
assert "dateutil" in repr(cal.events[0].start.tzinfo.__class__)
|
||||
|
@ -354,35 +374,38 @@ def test_dateutil_timezone_is_also_added(calendars):
|
|||
This is important as we use those in the zoneinfo implementation.
|
||||
"""
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"calendar",
|
||||
[
|
||||
"example",
|
||||
"america_new_york", # custom
|
||||
"timezone_same_start", # known tzid
|
||||
"period_with_timezone", # known tzid
|
||||
]
|
||||
"america_new_york", # custom
|
||||
"timezone_same_start", # known tzid
|
||||
"period_with_timezone", # known tzid
|
||||
],
|
||||
)
|
||||
def test_timezone_is_not_missing(calendars, calendar):
|
||||
"""Check that these calendars have no timezone missing."""
|
||||
cal :Calendar= calendars[calendar]
|
||||
cal: Calendar = calendars[calendar]
|
||||
timezones = cal.timezones[:]
|
||||
assert set() == cal.get_missing_tzids()
|
||||
cal.add_missing_timezones()
|
||||
assert set() == cal.get_missing_tzids()
|
||||
assert cal.timezones == timezones
|
||||
|
||||
|
||||
def test_add_missing_known_timezones(calendars):
|
||||
"""Add all timezones specified."""
|
||||
cal :Calendar= calendars.issue_722_missing_timezones
|
||||
cal: Calendar = calendars.issue_722_missing_timezones
|
||||
assert len(cal.timezones) == 0
|
||||
cal.add_missing_timezones()
|
||||
assert len(cal.timezones) == len(queries), "all timezones are known"
|
||||
assert len(cal.get_missing_tzids()) == 0
|
||||
|
||||
|
||||
def test_cannot_add_unknown_timezone(calendars):
|
||||
"""I cannot add a timezone that is unknown."""
|
||||
cal :Calendar= calendars.issue_722_missing_VTIMEZONE_custom
|
||||
cal: Calendar = calendars.issue_722_missing_VTIMEZONE_custom
|
||||
assert len(cal.timezones) == 0
|
||||
assert cal.get_missing_tzids() == {"CUSTOM_tzid"}
|
||||
cal.add_missing_timezones()
|
||||
|
@ -394,6 +417,7 @@ def test_cannot_create_a_timezone_from_an_invalid_tzid():
|
|||
with pytest.raises(ValueError):
|
||||
Timezone.from_tzid("invalid/tzid")
|
||||
|
||||
|
||||
def test_dates_before_and_after_are_considered():
|
||||
"""When we add the timezones, we should check the calendar to see
|
||||
if all dates really occur in the span we use.
|
||||
|
@ -403,7 +427,7 @@ def test_dates_before_and_after_are_considered():
|
|||
pytest.skip("todo")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tzid", available_timezones() - {"Factory", "localtime"})
|
||||
@pytest.mark.parametrize("tzid", available_timezones() - {"Factory", "localtime"})
|
||||
def test_we_can_identify_dateutil_timezones(tzid):
|
||||
"""dateutil and others were badly supported.
|
||||
|
||||
|
|
|
@ -100,12 +100,12 @@ def test_tzid_is_part_of_the_period_values(calendars, tzp):
|
|||
def test_period_overlaps():
|
||||
# 30 minute increments
|
||||
datetime_1 = datetime.datetime(2024, 11, 20, 12, 0) # 12:00
|
||||
datetime_2 = datetime.datetime(2024, 11, 20, 12, 30) # 12:30
|
||||
datetime_2 = datetime.datetime(2024, 11, 20, 12, 30) # 12:30
|
||||
datetime_3 = datetime.datetime(2024, 11, 20, 13, 0) # 13:00
|
||||
|
||||
period_1 = vPeriod((datetime_1,datetime_2))
|
||||
period_2 = vPeriod((datetime_1,datetime_3))
|
||||
period_3 = vPeriod((datetime_2,datetime_3))
|
||||
|
||||
period_1 = vPeriod((datetime_1, datetime_2))
|
||||
period_2 = vPeriod((datetime_1, datetime_3))
|
||||
period_3 = vPeriod((datetime_2, datetime_3))
|
||||
|
||||
assert period_1.overlaps(period_2)
|
||||
assert period_3.overlaps(period_2)
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
See https://www.rfc-editor.org/rfc/rfc9074.html
|
||||
and also https://github.com/collective/icalendar/issues/657
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from icalendar.prop import vDDDTypes, vText
|
||||
|
@ -15,7 +16,7 @@ from icalendar.prop import vDDDTypes, vText
|
|||
("RELATED-TO", vText, "rfc_9074_example_2", 1),
|
||||
("ACKNOWLEDGED", vDDDTypes, "rfc_9074_example_3", 0),
|
||||
("PROXIMITY", vText, "rfc_9074_example_proximity", 0),
|
||||
]
|
||||
],
|
||||
)
|
||||
def test_calendar_types(events, prop, cls, file, alarm_index):
|
||||
"""Check the types of the properties."""
|
||||
|
|
|
@ -510,7 +510,8 @@ TZIDS = [
|
|||
"Indian/Mauritius",
|
||||
"Indian/Mayotte",
|
||||
"Indian/Reunion",
|
||||
"Iran", "Asia/Tehran",
|
||||
"Iran",
|
||||
"Asia/Tehran",
|
||||
"Israel",
|
||||
"Jamaica",
|
||||
"Japan",
|
||||
|
@ -524,7 +525,8 @@ TZIDS = [
|
|||
"MST7MDT",
|
||||
"Navajo",
|
||||
"NZ",
|
||||
"NZ-CHAT", "Pacific/Chatham",
|
||||
"NZ-CHAT",
|
||||
"Pacific/Chatham",
|
||||
"Pacific/Apia",
|
||||
"Pacific/Auckland",
|
||||
"Pacific/Bougainville",
|
||||
|
@ -594,4 +596,4 @@ TZIDS = [
|
|||
"WET",
|
||||
"W-SU",
|
||||
"Zulu",
|
||||
]
|
||||
]
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
"""This package contains all functionality for timezones."""
|
||||
|
||||
from .tzid import tzid_from_dt, tzid_from_tzinfo, tzids_from_tzinfo
|
||||
from .tzp import TZP
|
||||
|
||||
tzp = TZP()
|
||||
|
||||
|
||||
def use_pytz():
|
||||
"""Use pytz as the implementation that looks up and creates timezones."""
|
||||
tzp.use_pytz()
|
||||
|
@ -13,6 +15,7 @@ def use_zoneinfo():
|
|||
"""Use zoneinfo as the implementation that looks up and creates timezones."""
|
||||
tzp.use_zoneinfo()
|
||||
|
||||
|
||||
__all__ = [
|
||||
"TZP",
|
||||
"tzp",
|
||||
|
@ -20,5 +23,5 @@ __all__ = [
|
|||
"use_zoneinfo",
|
||||
"tzid_from_tzinfo",
|
||||
"tzid_from_dt",
|
||||
"tzids_from_tzinfo"
|
||||
"tzids_from_tzinfo",
|
||||
]
|
||||
|
|
|
@ -15,6 +15,7 @@ Run this module:
|
|||
python -m icalendar.timezone.equivalent_timezone_ids
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
|
@ -34,13 +35,16 @@ DTS = []
|
|||
dt = START
|
||||
while dt <= END:
|
||||
DTS.append(dt)
|
||||
dt += timedelta(hours=25) # This must be big enough to be fast and small enough to identify the timeszones before it is the present year
|
||||
dt += timedelta(
|
||||
hours=25
|
||||
) # This must be big enough to be fast and small enough to identify the timeszones before it is the present year
|
||||
del dt
|
||||
|
||||
|
||||
def main(
|
||||
create_timezones:list[Callable[[str], tzinfo]],
|
||||
name:str,
|
||||
):
|
||||
create_timezones: list[Callable[[str], tzinfo]],
|
||||
name: str,
|
||||
):
|
||||
"""Generate a lookup table for timezone information if unknown timezones.
|
||||
|
||||
We cannot create one lookup for all because they seem to be all equivalent
|
||||
|
@ -50,24 +54,24 @@ def main(
|
|||
unsorted_tzids = available_timezones()
|
||||
unsorted_tzids.remove("localtime")
|
||||
unsorted_tzids.remove("Factory")
|
||||
|
||||
|
||||
class TZ(NamedTuple):
|
||||
tz: tzinfo
|
||||
id:str
|
||||
id: str
|
||||
|
||||
tzs = [
|
||||
TZ(create_timezone(tzid), tzid)
|
||||
for create_timezone in create_timezones
|
||||
for tzid in unsorted_tzids
|
||||
]
|
||||
|
||||
|
||||
def generate_tree(
|
||||
tzs: list[TZ],
|
||||
step:timedelta=timedelta(hours=1),
|
||||
start:datetime=START,
|
||||
end:datetime=END,
|
||||
todo:Optional[set[str]]=None
|
||||
) -> tuple[datetime, dict[timedelta, set[str]]] | set[str]: # should be recursive
|
||||
tzs: list[TZ],
|
||||
step: timedelta = timedelta(hours=1),
|
||||
start: datetime = START,
|
||||
end: datetime = END,
|
||||
todo: Optional[set[str]] = None,
|
||||
) -> tuple[datetime, dict[timedelta, set[str]]] | set[str]: # should be recursive
|
||||
"""Generate a lookup tree."""
|
||||
if todo is None:
|
||||
todo = [tz.id for tz in tzs]
|
||||
|
@ -79,14 +83,14 @@ def main(
|
|||
todo.remove(tzs[0].id)
|
||||
return {tzs[0].id}
|
||||
while start < end:
|
||||
offsets : dict[timedelta, list[TZ]] = defaultdict(list)
|
||||
offsets: dict[timedelta, list[TZ]] = defaultdict(list)
|
||||
try:
|
||||
# if we are around a timezone change, we must move on
|
||||
# see https://github.com/collective/icalendar/issues/776
|
||||
around_tz_change = not all(
|
||||
tz.tz.utcoffset(start) ==
|
||||
tz.tz.utcoffset(start - DISTANCE_FROM_TIMEZONE_CHANGE) ==
|
||||
tz.tz.utcoffset(start + DISTANCE_FROM_TIMEZONE_CHANGE)
|
||||
tz.tz.utcoffset(start)
|
||||
== tz.tz.utcoffset(start - DISTANCE_FROM_TIMEZONE_CHANGE)
|
||||
== tz.tz.utcoffset(start + DISTANCE_FROM_TIMEZONE_CHANGE)
|
||||
for tz in tzs
|
||||
)
|
||||
except (NonExistentTimeError, AmbiguousTimeError):
|
||||
|
@ -101,7 +105,9 @@ def main(
|
|||
continue
|
||||
lookup = {}
|
||||
for offset, tzs in offsets.items():
|
||||
lookup[offset] = generate_tree(tzs=tzs, step=step, start=start + step, end=end, todo=todo)
|
||||
lookup[offset] = generate_tree(
|
||||
tzs=tzs, step=step, start=start + step, end=end, todo=todo
|
||||
)
|
||||
return start, lookup
|
||||
print(f"reached end with {len(tzs)} timezones - assuming they are equivalent.")
|
||||
result = set()
|
||||
|
@ -110,7 +116,6 @@ def main(
|
|||
todo.remove(tz.id)
|
||||
return result
|
||||
|
||||
|
||||
lookup = generate_tree(tzs, step=timedelta(hours=33))
|
||||
|
||||
file = Path(__file__).parent / f"equivalent_timezone_ids_{name}.py"
|
||||
|
@ -118,7 +123,9 @@ def main(
|
|||
print("lookup = ", end="")
|
||||
pprint(lookup)
|
||||
with file.open("w") as f:
|
||||
f.write(f"'''This file is automatically generated by {Path(__file__).name}'''\n")
|
||||
f.write(
|
||||
f"'''This file is automatically generated by {Path(__file__).name}'''\n"
|
||||
)
|
||||
f.write("import datetime\n\n")
|
||||
f.write("\nlookup = ")
|
||||
pprint(lookup, stream=f)
|
||||
|
@ -126,12 +133,16 @@ def main(
|
|||
|
||||
return lookup
|
||||
|
||||
|
||||
__all__ = ["main"]
|
||||
|
||||
if __name__ == "__main__":
|
||||
from dateutil.tz import gettz
|
||||
from pytz import timezone
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
# add more timezone implementations if you like
|
||||
main([ZoneInfo, timezone, gettz], "result",
|
||||
main(
|
||||
[ZoneInfo, timezone, gettz],
|
||||
"result",
|
||||
)
|
||||
|
|
Plik diff jest za duży
Load Diff
|
@ -1,4 +1,5 @@
|
|||
"""The interface for timezone implementations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
|
@ -33,7 +34,7 @@ class TZProvider(ABC):
|
|||
"""Whether the timezone is already cached by the implementation."""
|
||||
|
||||
@abstractmethod
|
||||
def fix_rrule_until(self, rrule:rrule, ical_rrule:prop.vRecur) -> None:
|
||||
def fix_rrule_until(self, rrule: rrule, ical_rrule: prop.vRecur) -> None:
|
||||
"""Make sure the until value works for the rrule generated from the ical_rrule."""
|
||||
|
||||
@abstractmethod
|
||||
|
@ -52,4 +53,5 @@ class TZProvider(ABC):
|
|||
def uses_zoneinfo(self) -> bool:
|
||||
"""Whether we use zoneinfo."""
|
||||
|
||||
|
||||
__all__ = ["TZProvider"]
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
"""Use pytz timezones."""
|
||||
|
||||
from __future__ import annotations
|
||||
import pytz
|
||||
from .. import cal
|
||||
|
@ -10,7 +11,6 @@ from icalendar import prop
|
|||
from dateutil.rrule import rrule
|
||||
|
||||
|
||||
|
||||
class PYTZ(TZProvider):
|
||||
"""Provide icalendar with timezones from pytz."""
|
||||
|
||||
|
@ -18,7 +18,7 @@ class PYTZ(TZProvider):
|
|||
|
||||
def localize_utc(self, dt: datetime) -> datetime:
|
||||
"""Return the datetime in UTC."""
|
||||
if getattr(dt, 'tzinfo', False) and dt.tzinfo is not None:
|
||||
if getattr(dt, "tzinfo", False) and dt.tzinfo is not None:
|
||||
return dt.astimezone(pytz.utc)
|
||||
# assume UTC for naive datetime instances
|
||||
return pytz.utc.localize(dt)
|
||||
|
@ -31,9 +31,9 @@ class PYTZ(TZProvider):
|
|||
"""Whether the timezone is already cached by the implementation."""
|
||||
return id in pytz.all_timezones
|
||||
|
||||
def fix_rrule_until(self, rrule:rrule, ical_rrule:prop.vRecur) -> None:
|
||||
def fix_rrule_until(self, rrule: rrule, ical_rrule: prop.vRecur) -> None:
|
||||
"""Make sure the until value works for the rrule generated from the ical_rrule."""
|
||||
if not {'UNTIL', 'COUNT'}.intersection(ical_rrule.keys()):
|
||||
if not {"UNTIL", "COUNT"}.intersection(ical_rrule.keys()):
|
||||
# pytz.timezones don't know any transition dates after 2038
|
||||
# either
|
||||
rrule._until = datetime(2038, 12, 31, tzinfo=pytz.UTC)
|
||||
|
@ -42,11 +42,15 @@ class PYTZ(TZProvider):
|
|||
"""Create a pytz timezone from the given information."""
|
||||
transition_times, transition_info = tz.get_transitions()
|
||||
name = tz.tz_name
|
||||
cls = type(name, (DstTzInfo,), {
|
||||
'zone': name,
|
||||
'_utc_transition_times': transition_times,
|
||||
'_transition_info': transition_info
|
||||
})
|
||||
cls = type(
|
||||
name,
|
||||
(DstTzInfo,),
|
||||
{
|
||||
"zone": name,
|
||||
"_utc_transition_times": transition_times,
|
||||
"_transition_info": transition_info,
|
||||
},
|
||||
)
|
||||
return cls()
|
||||
|
||||
def timezone(self, name: str) -> Optional[tzinfo]:
|
||||
|
|
|
@ -4,6 +4,7 @@ Normally, timezones have ids.
|
|||
This is a way to access the ids if you have a
|
||||
datetime.tzinfo object.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
|
@ -34,7 +35,7 @@ def tzids_from_tzinfo(tzinfo: Optional[tzinfo]) -> tuple[str]:
|
|||
>>> tzids_from_tzinfo(gettz("Europe/Berlin"))
|
||||
('Europe/Berlin', 'Arctic/Longyearbyen', 'Atlantic/Jan_Mayen', 'Europe/Budapest', 'Europe/Copenhagen', 'Europe/Oslo', 'Europe/Stockholm', 'Europe/Vienna')
|
||||
|
||||
""" # The example might need to change if you recreate the lookup tree
|
||||
""" # The example might need to change if you recreate the lookup tree
|
||||
if tzinfo is None:
|
||||
return ()
|
||||
if hasattr(tzinfo, "zone"):
|
||||
|
@ -71,6 +72,7 @@ def tzid_from_tzinfo(tzinfo: Optional[tzinfo]) -> Optional[str]:
|
|||
return None
|
||||
return tzids[0]
|
||||
|
||||
|
||||
def tzid_from_dt(dt: datetime) -> Optional[str]:
|
||||
"""Retrieve the timezone id from the datetime object."""
|
||||
tzid = tzid_from_tzinfo(dt.tzinfo)
|
||||
|
@ -106,4 +108,5 @@ def get_equivalent_tzids(tzid: str) -> tuple[str]:
|
|||
ids = _EQUIVALENT_IDS.get(tzid, set())
|
||||
return (tzid,) + tuple(sorted(ids - {tzid}))
|
||||
|
||||
|
||||
__all__ = ["tzid_from_tzinfo", "tzid_from_dt", "tzids_from_tzinfo"]
|
||||
|
|
|
@ -26,31 +26,35 @@ class TZP:
|
|||
All of icalendar will then use this timezone implementation.
|
||||
"""
|
||||
|
||||
def __init__(self, provider:Union[str, TZProvider]=DEFAULT_TIMEZONE_PROVIDER):
|
||||
def __init__(self, provider: Union[str, TZProvider] = DEFAULT_TIMEZONE_PROVIDER):
|
||||
"""Create a new timezone implementation proxy."""
|
||||
self.use(provider)
|
||||
|
||||
def use_pytz(self) -> None:
|
||||
"""Use pytz as the timezone provider."""
|
||||
from .pytz import PYTZ
|
||||
|
||||
self._use(PYTZ())
|
||||
|
||||
def use_zoneinfo(self) -> None:
|
||||
"""Use zoneinfo as the timezone provider."""
|
||||
from .zoneinfo import ZONEINFO
|
||||
|
||||
self._use(ZONEINFO())
|
||||
|
||||
def _use(self, provider:TZProvider) -> None:
|
||||
def _use(self, provider: TZProvider) -> None:
|
||||
"""Use a timezone implementation."""
|
||||
self.__tz_cache = {}
|
||||
self.__provider = provider
|
||||
|
||||
def use(self, provider:Union[str, TZProvider]):
|
||||
def use(self, provider: Union[str, TZProvider]):
|
||||
"""Switch to a different timezone provider."""
|
||||
if isinstance(provider, str):
|
||||
use_provider = getattr(self, f"use_{provider}", None)
|
||||
if use_provider is None:
|
||||
raise ValueError(f"Unknown provider {provider}. Use 'pytz' or 'zoneinfo'.")
|
||||
raise ValueError(
|
||||
f"Unknown provider {provider}. Use 'pytz' or 'zoneinfo'."
|
||||
)
|
||||
use_provider()
|
||||
else:
|
||||
self._use(provider)
|
||||
|
@ -66,7 +70,9 @@ class TZP:
|
|||
"""
|
||||
return self.__provider.localize_utc(to_datetime(dt))
|
||||
|
||||
def localize(self, dt: datetime.date, tz: Union[datetime.tzinfo, str]) -> datetime.datetime:
|
||||
def localize(
|
||||
self, dt: datetime.date, tz: Union[datetime.tzinfo, str]
|
||||
) -> datetime.datetime:
|
||||
"""Localize a datetime to a timezone."""
|
||||
if isinstance(tz, str):
|
||||
tz = self.timezone(tz)
|
||||
|
@ -79,14 +85,16 @@ class TZP:
|
|||
This can influence the result from timezone(): Once cached, the
|
||||
custom timezone is returned from timezone().
|
||||
"""
|
||||
_unclean_id = timezone_component['TZID']
|
||||
_unclean_id = timezone_component["TZID"]
|
||||
_id = self.clean_timezone_id(_unclean_id)
|
||||
if not self.__provider.knows_timezone_id(_id) \
|
||||
and not self.__provider.knows_timezone_id(_unclean_id) \
|
||||
and _id not in self.__tz_cache:
|
||||
if (
|
||||
not self.__provider.knows_timezone_id(_id)
|
||||
and not self.__provider.knows_timezone_id(_unclean_id)
|
||||
and _id not in self.__tz_cache
|
||||
):
|
||||
self.__tz_cache[_id] = timezone_component.to_tz(self, lookup_tzid=False)
|
||||
|
||||
def fix_rrule_until(self, rrule:rrule, ical_rrule:prop.vRecur) -> None:
|
||||
def fix_rrule_until(self, rrule: rrule, ical_rrule: prop.vRecur) -> None:
|
||||
"""Make sure the until value works."""
|
||||
self.__provider.fix_rrule_until(rrule, ical_rrule)
|
||||
|
||||
|
@ -132,4 +140,5 @@ class TZP:
|
|||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({repr(self.name)})"
|
||||
|
||||
|
||||
__all__ = ["TZP"]
|
||||
|
|
|
@ -10,110 +10,110 @@ for this mapping is also available at the unicode consortium [1].
|
|||
"""
|
||||
|
||||
WINDOWS_TO_OLSON = {
|
||||
'AUS Central Standard Time': 'Australia/Darwin',
|
||||
'AUS Eastern Standard Time': 'Australia/Sydney',
|
||||
'Afghanistan Standard Time': 'Asia/Kabul',
|
||||
'Alaskan Standard Time': 'America/Anchorage',
|
||||
'Arab Standard Time': 'Asia/Riyadh',
|
||||
'Arabian Standard Time': 'Asia/Dubai',
|
||||
'Arabic Standard Time': 'Asia/Baghdad',
|
||||
'Argentina Standard Time': 'America/Argentina/Buenos_Aires',
|
||||
'Atlantic Standard Time': 'America/Halifax',
|
||||
'Azerbaijan Standard Time': 'Asia/Baku',
|
||||
'Azores Standard Time': 'Atlantic/Azores',
|
||||
'Bahia Standard Time': 'America/Bahia',
|
||||
'Bangladesh Standard Time': 'Asia/Dhaka',
|
||||
'Belarus Standard Time': 'Europe/Minsk',
|
||||
'Canada Central Standard Time': 'America/Regina',
|
||||
'Cape Verde Standard Time': 'Atlantic/Cape_Verde',
|
||||
'Caucasus Standard Time': 'Asia/Yerevan',
|
||||
'Cen. Australia Standard Time': 'Australia/Adelaide',
|
||||
'Central America Standard Time': 'America/Guatemala',
|
||||
'Central Asia Standard Time': 'Asia/Almaty',
|
||||
'Central Brazilian Standard Time': 'America/Cuiaba',
|
||||
'Central Europe Standard Time': 'Europe/Budapest',
|
||||
'Central European Standard Time': 'Europe/Warsaw',
|
||||
'Central Pacific Standard Time': 'Pacific/Guadalcanal',
|
||||
'Central Standard Time': 'America/Chicago',
|
||||
'Central Standard Time (Mexico)': 'America/Mexico_City',
|
||||
'China Standard Time': 'Asia/Shanghai',
|
||||
'Dateline Standard Time': 'Etc/GMT+12',
|
||||
'E. Africa Standard Time': 'Africa/Nairobi',
|
||||
'E. Australia Standard Time': 'Australia/Brisbane',
|
||||
'E. Europe Standard Time': 'Europe/Chisinau',
|
||||
'E. South America Standard Time': 'America/Sao_Paulo',
|
||||
'Eastern Standard Time': 'America/New_York',
|
||||
'Eastern Standard Time (Mexico)': 'America/Cancun',
|
||||
'Egypt Standard Time': 'Africa/Cairo',
|
||||
'Ekaterinburg Standard Time': 'Asia/Yekaterinburg',
|
||||
'FLE Standard Time': 'Europe/Kyiv',
|
||||
'Fiji Standard Time': 'Pacific/Fiji',
|
||||
'GMT Standard Time': 'Europe/London',
|
||||
'GTB Standard Time': 'Europe/Bucharest',
|
||||
'Georgian Standard Time': 'Asia/Tbilisi',
|
||||
'Greenland Standard Time': 'America/Nuuk',
|
||||
'Greenwich Standard Time': 'Atlantic/Reykjavik',
|
||||
'Hawaiian Standard Time': 'Pacific/Honolulu',
|
||||
'India Standard Time': 'Asia/Kolkata',
|
||||
'Iran Standard Time': 'Asia/Tehran',
|
||||
'Israel Standard Time': 'Asia/Jerusalem',
|
||||
'Jordan Standard Time': 'Asia/Amman',
|
||||
'Kaliningrad Standard Time': 'Europe/Kaliningrad',
|
||||
'Korea Standard Time': 'Asia/Seoul',
|
||||
'Libya Standard Time': 'Africa/Tripoli',
|
||||
'Line Islands Standard Time': 'Pacific/Kiritimati',
|
||||
'Magadan Standard Time': 'Asia/Magadan',
|
||||
'Mauritius Standard Time': 'Indian/Mauritius',
|
||||
'Middle East Standard Time': 'Asia/Beirut',
|
||||
'Montevideo Standard Time': 'America/Montevideo',
|
||||
'Morocco Standard Time': 'Africa/Casablanca',
|
||||
'Mountain Standard Time': 'America/Denver',
|
||||
'Mountain Standard Time (Mexico)': 'America/Chihuahua',
|
||||
'Myanmar Standard Time': 'Asia/Yangon',
|
||||
'N. Central Asia Standard Time': 'Asia/Novosibirsk',
|
||||
'Namibia Standard Time': 'Africa/Windhoek',
|
||||
'Nepal Standard Time': 'Asia/Kathmandu',
|
||||
'New Zealand Standard Time': 'Pacific/Auckland',
|
||||
'Newfoundland Standard Time': 'America/St_Johns',
|
||||
'North Asia East Standard Time': 'Asia/Irkutsk',
|
||||
'North Asia Standard Time': 'Asia/Krasnoyarsk',
|
||||
'North Korea Standard Time': 'Asia/Pyongyang',
|
||||
'Pacific SA Standard Time': 'America/Santiago',
|
||||
'Pacific Standard Time': 'America/Los_Angeles',
|
||||
'Pakistan Standard Time': 'Asia/Karachi',
|
||||
'Paraguay Standard Time': 'America/Asuncion',
|
||||
'Romance Standard Time': 'Europe/Paris',
|
||||
'Russia Time Zone 10': 'Asia/Srednekolymsk',
|
||||
'Russia Time Zone 11': 'Asia/Kamchatka',
|
||||
'Russia Time Zone 3': 'Europe/Samara',
|
||||
'Russian Standard Time': 'Europe/Moscow',
|
||||
'SA Eastern Standard Time': 'America/Cayenne',
|
||||
'SA Pacific Standard Time': 'America/Bogota',
|
||||
'SA Western Standard Time': 'America/La_Paz',
|
||||
'SE Asia Standard Time': 'Asia/Bangkok',
|
||||
'Samoa Standard Time': 'Pacific/Apia',
|
||||
'Singapore Standard Time': 'Asia/Singapore',
|
||||
'South Africa Standard Time': 'Africa/Johannesburg',
|
||||
'Sri Lanka Standard Time': 'Asia/Colombo',
|
||||
'Syria Standard Time': 'Asia/Damascus',
|
||||
'Taipei Standard Time': 'Asia/Taipei',
|
||||
'Tasmania Standard Time': 'Australia/Hobart',
|
||||
'Tokyo Standard Time': 'Asia/Tokyo',
|
||||
'Tonga Standard Time': 'Pacific/Tongatapu',
|
||||
'Turkey Standard Time': 'Europe/Istanbul',
|
||||
'US Eastern Standard Time': 'America/Indiana/Indianapolis',
|
||||
'US Mountain Standard Time': 'America/Phoenix',
|
||||
'UTC': 'Etc/GMT',
|
||||
'UTC+12': 'Etc/GMT-12',
|
||||
'UTC-02': 'Etc/GMT+2',
|
||||
'UTC-11': 'Etc/GMT+11',
|
||||
'Ulaanbaatar Standard Time': 'Asia/Ulaanbaatar',
|
||||
'Venezuela Standard Time': 'America/Caracas',
|
||||
'Vladivostok Standard Time': 'Asia/Vladivostok',
|
||||
'W. Australia Standard Time': 'Australia/Perth',
|
||||
'W. Central Africa Standard Time': 'Africa/Lagos',
|
||||
'W. Europe Standard Time': 'Europe/Berlin',
|
||||
'West Asia Standard Time': 'Asia/Tashkent',
|
||||
'West Pacific Standard Time': 'Pacific/Port_Moresby',
|
||||
'Yakutsk Standard Time': 'Asia/Yakutsk',
|
||||
"AUS Central Standard Time": "Australia/Darwin",
|
||||
"AUS Eastern Standard Time": "Australia/Sydney",
|
||||
"Afghanistan Standard Time": "Asia/Kabul",
|
||||
"Alaskan Standard Time": "America/Anchorage",
|
||||
"Arab Standard Time": "Asia/Riyadh",
|
||||
"Arabian Standard Time": "Asia/Dubai",
|
||||
"Arabic Standard Time": "Asia/Baghdad",
|
||||
"Argentina Standard Time": "America/Argentina/Buenos_Aires",
|
||||
"Atlantic Standard Time": "America/Halifax",
|
||||
"Azerbaijan Standard Time": "Asia/Baku",
|
||||
"Azores Standard Time": "Atlantic/Azores",
|
||||
"Bahia Standard Time": "America/Bahia",
|
||||
"Bangladesh Standard Time": "Asia/Dhaka",
|
||||
"Belarus Standard Time": "Europe/Minsk",
|
||||
"Canada Central Standard Time": "America/Regina",
|
||||
"Cape Verde Standard Time": "Atlantic/Cape_Verde",
|
||||
"Caucasus Standard Time": "Asia/Yerevan",
|
||||
"Cen. Australia Standard Time": "Australia/Adelaide",
|
||||
"Central America Standard Time": "America/Guatemala",
|
||||
"Central Asia Standard Time": "Asia/Almaty",
|
||||
"Central Brazilian Standard Time": "America/Cuiaba",
|
||||
"Central Europe Standard Time": "Europe/Budapest",
|
||||
"Central European Standard Time": "Europe/Warsaw",
|
||||
"Central Pacific Standard Time": "Pacific/Guadalcanal",
|
||||
"Central Standard Time": "America/Chicago",
|
||||
"Central Standard Time (Mexico)": "America/Mexico_City",
|
||||
"China Standard Time": "Asia/Shanghai",
|
||||
"Dateline Standard Time": "Etc/GMT+12",
|
||||
"E. Africa Standard Time": "Africa/Nairobi",
|
||||
"E. Australia Standard Time": "Australia/Brisbane",
|
||||
"E. Europe Standard Time": "Europe/Chisinau",
|
||||
"E. South America Standard Time": "America/Sao_Paulo",
|
||||
"Eastern Standard Time": "America/New_York",
|
||||
"Eastern Standard Time (Mexico)": "America/Cancun",
|
||||
"Egypt Standard Time": "Africa/Cairo",
|
||||
"Ekaterinburg Standard Time": "Asia/Yekaterinburg",
|
||||
"FLE Standard Time": "Europe/Kyiv",
|
||||
"Fiji Standard Time": "Pacific/Fiji",
|
||||
"GMT Standard Time": "Europe/London",
|
||||
"GTB Standard Time": "Europe/Bucharest",
|
||||
"Georgian Standard Time": "Asia/Tbilisi",
|
||||
"Greenland Standard Time": "America/Nuuk",
|
||||
"Greenwich Standard Time": "Atlantic/Reykjavik",
|
||||
"Hawaiian Standard Time": "Pacific/Honolulu",
|
||||
"India Standard Time": "Asia/Kolkata",
|
||||
"Iran Standard Time": "Asia/Tehran",
|
||||
"Israel Standard Time": "Asia/Jerusalem",
|
||||
"Jordan Standard Time": "Asia/Amman",
|
||||
"Kaliningrad Standard Time": "Europe/Kaliningrad",
|
||||
"Korea Standard Time": "Asia/Seoul",
|
||||
"Libya Standard Time": "Africa/Tripoli",
|
||||
"Line Islands Standard Time": "Pacific/Kiritimati",
|
||||
"Magadan Standard Time": "Asia/Magadan",
|
||||
"Mauritius Standard Time": "Indian/Mauritius",
|
||||
"Middle East Standard Time": "Asia/Beirut",
|
||||
"Montevideo Standard Time": "America/Montevideo",
|
||||
"Morocco Standard Time": "Africa/Casablanca",
|
||||
"Mountain Standard Time": "America/Denver",
|
||||
"Mountain Standard Time (Mexico)": "America/Chihuahua",
|
||||
"Myanmar Standard Time": "Asia/Yangon",
|
||||
"N. Central Asia Standard Time": "Asia/Novosibirsk",
|
||||
"Namibia Standard Time": "Africa/Windhoek",
|
||||
"Nepal Standard Time": "Asia/Kathmandu",
|
||||
"New Zealand Standard Time": "Pacific/Auckland",
|
||||
"Newfoundland Standard Time": "America/St_Johns",
|
||||
"North Asia East Standard Time": "Asia/Irkutsk",
|
||||
"North Asia Standard Time": "Asia/Krasnoyarsk",
|
||||
"North Korea Standard Time": "Asia/Pyongyang",
|
||||
"Pacific SA Standard Time": "America/Santiago",
|
||||
"Pacific Standard Time": "America/Los_Angeles",
|
||||
"Pakistan Standard Time": "Asia/Karachi",
|
||||
"Paraguay Standard Time": "America/Asuncion",
|
||||
"Romance Standard Time": "Europe/Paris",
|
||||
"Russia Time Zone 10": "Asia/Srednekolymsk",
|
||||
"Russia Time Zone 11": "Asia/Kamchatka",
|
||||
"Russia Time Zone 3": "Europe/Samara",
|
||||
"Russian Standard Time": "Europe/Moscow",
|
||||
"SA Eastern Standard Time": "America/Cayenne",
|
||||
"SA Pacific Standard Time": "America/Bogota",
|
||||
"SA Western Standard Time": "America/La_Paz",
|
||||
"SE Asia Standard Time": "Asia/Bangkok",
|
||||
"Samoa Standard Time": "Pacific/Apia",
|
||||
"Singapore Standard Time": "Asia/Singapore",
|
||||
"South Africa Standard Time": "Africa/Johannesburg",
|
||||
"Sri Lanka Standard Time": "Asia/Colombo",
|
||||
"Syria Standard Time": "Asia/Damascus",
|
||||
"Taipei Standard Time": "Asia/Taipei",
|
||||
"Tasmania Standard Time": "Australia/Hobart",
|
||||
"Tokyo Standard Time": "Asia/Tokyo",
|
||||
"Tonga Standard Time": "Pacific/Tongatapu",
|
||||
"Turkey Standard Time": "Europe/Istanbul",
|
||||
"US Eastern Standard Time": "America/Indiana/Indianapolis",
|
||||
"US Mountain Standard Time": "America/Phoenix",
|
||||
"UTC": "Etc/GMT",
|
||||
"UTC+12": "Etc/GMT-12",
|
||||
"UTC-02": "Etc/GMT+2",
|
||||
"UTC-11": "Etc/GMT+11",
|
||||
"Ulaanbaatar Standard Time": "Asia/Ulaanbaatar",
|
||||
"Venezuela Standard Time": "America/Caracas",
|
||||
"Vladivostok Standard Time": "Asia/Vladivostok",
|
||||
"W. Australia Standard Time": "Australia/Perth",
|
||||
"W. Central Africa Standard Time": "Africa/Lagos",
|
||||
"W. Europe Standard Time": "Europe/Berlin",
|
||||
"West Asia Standard Time": "Asia/Tashkent",
|
||||
"West Pacific Standard Time": "Pacific/Port_Moresby",
|
||||
"Yakutsk Standard Time": "Asia/Yakutsk",
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
"""Use zoneinfo timezones"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
try:
|
||||
|
@ -35,7 +36,7 @@ class ZONEINFO(TZProvider):
|
|||
|
||||
def localize_utc(self, dt: datetime) -> datetime:
|
||||
"""Return the datetime in UTC."""
|
||||
if getattr(dt, 'tzinfo', False) and dt.tzinfo is not None:
|
||||
if getattr(dt, "tzinfo", False) and dt.tzinfo is not None:
|
||||
return dt.astimezone(self.utc)
|
||||
return self.localize(dt, self.utc)
|
||||
|
||||
|
@ -53,9 +54,9 @@ class ZONEINFO(TZProvider):
|
|||
"""Whether the timezone is already cached by the implementation."""
|
||||
return id in self._available_timezones
|
||||
|
||||
def fix_rrule_until(self, rrule:rrule, ical_rrule:prop.vRecur) -> None:
|
||||
def fix_rrule_until(self, rrule: rrule, ical_rrule: prop.vRecur) -> None:
|
||||
"""Make sure the until value works for the rrule generated from the ical_rrule."""
|
||||
if not {'UNTIL', 'COUNT'}.intersection(ical_rrule.keys()):
|
||||
if not {"UNTIL", "COUNT"}.intersection(ical_rrule.keys()):
|
||||
# zoninfo does not know any transition dates after 2038
|
||||
rrule._until = datetime(2038, 12, 31, tzinfo=self.utc)
|
||||
|
||||
|
@ -87,10 +88,11 @@ class ZONEINFO(TZProvider):
|
|||
return True
|
||||
|
||||
|
||||
def pickle_tzicalvtz(tzicalvtz:tz._tzicalvtz):
|
||||
def pickle_tzicalvtz(tzicalvtz: tz._tzicalvtz):
|
||||
"""Because we use dateutil.tzical, we need to make it pickle-able."""
|
||||
return _tzicalvtz, (tzicalvtz._tzid, tzicalvtz._comps)
|
||||
|
||||
|
||||
copyreg.pickle(_tzicalvtz, pickle_tzicalvtz)
|
||||
|
||||
|
||||
|
@ -99,19 +101,23 @@ def pickle_rrule_with_cache(self: rrule):
|
|||
|
||||
This is mainly copied from rrule.replace.
|
||||
"""
|
||||
new_kwargs = {"interval": self._interval,
|
||||
"count": self._count,
|
||||
"dtstart": self._dtstart,
|
||||
"freq": self._freq,
|
||||
"until": self._until,
|
||||
"wkst": self._wkst,
|
||||
"cache": False if self._cache is None else True }
|
||||
new_kwargs = {
|
||||
"interval": self._interval,
|
||||
"count": self._count,
|
||||
"dtstart": self._dtstart,
|
||||
"freq": self._freq,
|
||||
"until": self._until,
|
||||
"wkst": self._wkst,
|
||||
"cache": False if self._cache is None else True,
|
||||
}
|
||||
new_kwargs.update(self._original_rule)
|
||||
# from https://stackoverflow.com/a/64915638/1320237
|
||||
return functools.partial(rrule, new_kwargs.pop("freq"), **new_kwargs), ()
|
||||
|
||||
|
||||
copyreg.pickle(rrule, pickle_rrule_with_cache)
|
||||
|
||||
|
||||
def pickle_rruleset_with_cache(rs: rruleset):
|
||||
"""Pickle an rruleset."""
|
||||
# self._rrule = []
|
||||
|
@ -119,19 +125,28 @@ def pickle_rruleset_with_cache(rs: rruleset):
|
|||
# self._exrule = []
|
||||
# self._exdate = []
|
||||
return unpickle_rruleset_with_cache, (
|
||||
rs._rrule, rs._rdate, rs._exrule,
|
||||
rs._exdate, False if rs._cache is None else True
|
||||
rs._rrule,
|
||||
rs._rdate,
|
||||
rs._exrule,
|
||||
rs._exdate,
|
||||
False if rs._cache is None else True,
|
||||
)
|
||||
|
||||
|
||||
def unpickle_rruleset_with_cache(rrule, rdate, exrule, exdate, cache):
|
||||
"""unpickling the rruleset."""
|
||||
rs = rruleset(cache)
|
||||
for o in rrule: rs.rrule(o)
|
||||
for o in rdate: rs.rdate(o)
|
||||
for o in exrule: rs.exrule(o)
|
||||
for o in exdate: rs.exdate(o)
|
||||
for o in rrule:
|
||||
rs.rrule(o)
|
||||
for o in rdate:
|
||||
rs.rdate(o)
|
||||
for o in exrule:
|
||||
rs.exrule(o)
|
||||
for o in exdate:
|
||||
rs.exdate(o)
|
||||
return rs
|
||||
|
||||
|
||||
copyreg.pickle(rruleset, pickle_rruleset_with_cache)
|
||||
|
||||
__all__ = ["ZONEINFO"]
|
||||
|
|
|
@ -7,15 +7,13 @@ from icalendar.parser_tools import to_unicode
|
|||
|
||||
|
||||
class UIDGenerator:
|
||||
"""If you are too lazy to create real uid's.
|
||||
"""If you are too lazy to create real uid's."""
|
||||
|
||||
"""
|
||||
chars = list(ascii_letters + digits)
|
||||
|
||||
@staticmethod
|
||||
def rnd_string(length=16):
|
||||
"""Generates a string with random characters of length.
|
||||
"""
|
||||
"""Generates a string with random characters of length."""
|
||||
return "".join([random.choice(UIDGenerator.chars) for _ in range(length)])
|
||||
|
||||
@staticmethod
|
||||
|
@ -26,6 +24,7 @@ class UIDGenerator:
|
|||
20050105T225746Z-HKtJMqUgdO0jDUwm@example.com
|
||||
"""
|
||||
from icalendar.prop import vDatetime, vText
|
||||
|
||||
host_name = to_unicode(host_name)
|
||||
unique = unique or UIDGenerator.rnd_string()
|
||||
today = to_unicode(vDatetime(datetime.today()).to_ical())
|
||||
|
@ -70,4 +69,12 @@ def normalize_pytz(dt: date):
|
|||
return dt
|
||||
|
||||
|
||||
__all__ = ["UIDGenerator", "is_date", "is_datetime", "to_datetime", "is_pytz", "is_pytz_dt", "normalize_pytz"]
|
||||
__all__ = [
|
||||
"UIDGenerator",
|
||||
"is_date",
|
||||
"is_datetime",
|
||||
"to_datetime",
|
||||
"is_pytz",
|
||||
"is_pytz_dt",
|
||||
"normalize_pytz",
|
||||
]
|
||||
|
|
Ładowanie…
Reference in New Issue