Conversation
I explained my initial thoughts here. The README of the ics_vtimezones project also has some relevant info on what's packaged there. Basically, to get really portable standards-compliant ics files, you need to include the full vTimezone (i.e. usually the recurring changes of UTC-offset when changing between daylight-saving and normal time for that region, but also any political decisions changing the offset at specific points in time) specification of any timezone used within the file. There are two common sources for this information: the IANA time zone database (also informally known as Olson database) or the time zone database shipped by windows. Please note that both databases change over time - e.g. there have been 4 updated versions of the IANA time zone database released in 2020 alone. The IANA time zone database is shipped on most unix systems at different paths and also used in many programming languages (e.g. the
So you are right, most UNIX systems already have (some revision of) that database installed, there are at least two python modules that package this information and starting from python 3.9, the database will also be used by the standard library. The issue is that just having the database files available doesn't help us, as those files are usually in a specific binary format, which you first need to parse and then convert into an ics-compatible vTimezone representation. IANA has a long list of tools that can be useful for this. Here's a list of stuff that might be useful for us:
The issue with |
|
I created an overview how different other tools handle the inclusion of vTimezone definitions when writing ics files: There seem to be three different broad categories
Using some minor introspection and the publicly available API, I was able to generate all the data for case 2. from To summarize, none of the three different behaviours seem perfect. Maybe we should select a sensible default for ics.py and provide opt-in configurations for the alternatives:
Note this only applies to writing ics files, interpreting and also serializing vTimezone definitions when they were already included in a parsed ics file is pretty straight-forward. |
|
I think a graceful upgrade is a good approach - IIUC, the vtimezone needs to specify RRULE and whatnot? Though, if you can specify e.g. 'America/Chicago' then that should be the default. If not, dumping as UTC with a warning about losing daylight info would be appropriate, and gracefully upgrading to use a library that can add the vtimezone info |
Codecov Report
@@ Coverage Diff @@
## main #249 +/- ##
=======================================
Coverage ? 75.80%
=======================================
Files ? 30
Lines ? 2703
Branches ? 0
=======================================
Hits ? 2049
Misses ? 654
Partials ? 0 Continue to review full report at Codecov.
|
|
Hmm, that was closer to a usable state than I thought. I kept the code using The code could still use some more testing and the component system would largely benefit from a self-contained documentation, but the core of this PR should be good to go. So I'd merge this soonish so that other work can easily build on this, meaning that we can also switch to working on the remaining points in #245 (i.e. mostly testing and documentation) more step-by-step in more fine grained PRs. @C4ptainCrunch due to the size of this PR I'm against again squashing into one uber-commit, I'd prefer fast-forwarding master to contain all the commits separately, but unfortunately I can't do that due to missing permissions. Could you make me co-owner of the organisation/repo so that I can merge this PR properly and also continue working on things like your PR #274? |
|
I agree with not squashing. |
|
When serializing a timezone, the following is raising an ics-py/src/ics/timezone/converters.py Line 93 in b551ca5 Minimal example: import datetime
import ics
# make a datetime with an "anonymous" timezone
dt = datetime.datetime.fromisoformat("2017-10-01T12:00:00+09:00")
print(dt.tzinfo) # datetime.timezone(datetime.timedelta(seconds=32400))
print(hasattr(dt.tzinfo, "_name")) # False
print(hasattr(dt.tzinfo, "_offset")) # False
event = ics.Event(begin=dt, duration=datetime.timedelta(minutes=30))
event.serialize() # AttributeErrorI'm using Python 3.9. I would suggest staying away from using @attr.s(frozen=True, repr=False)
class Timezone(Component, _tzinfo):
...
@classmethod
def from_datetime(dt: datetime.datetime):
return Timezone_from_offset(dt.tzname(), dt.utcoffset())By the way, if |
|
Unfortunately, >>> from dateutil.tz import gettz
>>> from datetime import datetime, timedelta
>>> now = datetime.now(tz=gettz("Europe/Berlin"))
>>> later = now + timedelta(days=183)
>>> now.tzname(), now.utcoffset(), now.tzinfo
('CEST', datetime.timedelta(seconds=7200), tzfile('/usr/share/zoneinfo/Europe/Berlin'))
>>> later.tzname(), later.utcoffset(), later.tzinfo
('CET', datetime.timedelta(seconds=3600), tzfile('/usr/share/zoneinfo/Europe/Berlin'))So for more complex timezone objects, we get 'CEST' or 'CET' depending on the timestamp in this case, while the user would want and expect 'Europe/Berlin' - working with 'CEST' or 'CET' might even yield wrong results once you start modifying datetimes based on that fixed timezone. So to get the information we actually want, we unfortunately need to touch some internals as there is no suitable API to get this information. See here and here for more background information on why there seems to be no other way how to do this. It seems that in your specific case (maybe starting with Python 3.9), the class was replaced with a built-in / compiled C++ implementation that replaces the python implementation for efficiency reasons: >>> dt = datetime.fromisoformat("2017-10-01T12:00:00+09:00")
>>> dt.tzname(), dt.utcoffset(), dt.tzinfo
('UTC+09:00', datetime.timedelta(seconds=32400), datetime.timezone(datetime.timedelta(seconds=32400)))
>>> print(inspect.getsource(type(dt.tzinfo)))
class timezone(tzinfo):
__slots__ = '_offset', '_name'
...
>>> dt.tzinfo.utcoffset
<built-in method utcoffset of datetime.timezone object at 0x7f917c2a9750>Fortunately, in this case we can use the class methods of the >>> dt.tzinfo.tzname(None)
'UTC+09:00'
>>> dt.tzinfo.utcoffset(None)
datetime.timedelta(seconds=32400)I'll push a small fix for this, could you then try again? Thanks for the hint with |
Components are now fully decoupled from their Converters, they only save their ics NAME and never directly reference their converters. The only exception to this rule are the convenience methods Component.from_container, populate, to_container and serialize and Timezone.from_tzid and from_tzinfo, but all of them import their Converters locally. When loading the converters package, a registry mapping Component Classes to their respective Converters is set up. This mapping can easily be changed later to use customized converters for certain Components.
…etation and conversion
…post_populate/serialize hooks
- The calendar.timezones field didn't really do anything except for capturing / providing defaults for the avail_tz context variable, which can also be done directly without having a potentially out-of-date and confusing long-lived cache variable. - We now correctly insert vTimezone definitions after the iCalendar header.
This PR contains two big things:
initialize_converters; and the customizations of Alarm and Calendar for examples)I also started to include tests against regressions of prior github issues (see e8e13e4, where mostly Timezone-related issues are covered) and some more general polish.