[CalendarServer-changes] [2458] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Fri May 23 11:06:07 PDT 2008


Revision: 2458
          http://trac.macosforge.org/projects/calendarserver/changeset/2458
Author:   wsanchez at apple.com
Date:     2008-05-23 11:06:05 -0700 (Fri, 23 May 2008)

Log Message:
-----------
Add timezone service.
Merge source:branches/users/cdaboo/timezone-service-2436

Modified Paths:
--------------
    CalendarServer/trunk/conf/caldavd-test.plist
    CalendarServer/trunk/conf/caldavd.plist
    CalendarServer/trunk/twistedcaldav/config.py
    CalendarServer/trunk/twistedcaldav/customxml.py
    CalendarServer/trunk/twistedcaldav/ical.py
    CalendarServer/trunk/twistedcaldav/static.py
    CalendarServer/trunk/twistedcaldav/tap.py
    CalendarServer/trunk/twistedcaldav/test/test_timezones.py
    CalendarServer/trunk/twistedcaldav/timezones.py

Added Paths:
-----------
    CalendarServer/trunk/twistedcaldav/timezoneservice.py

Modified: CalendarServer/trunk/conf/caldavd-test.plist
===================================================================
--- CalendarServer/trunk/conf/caldavd-test.plist	2008-05-23 16:58:36 UTC (rev 2457)
+++ CalendarServer/trunk/conf/caldavd-test.plist	2008-05-23 18:06:05 UTC (rev 2458)
@@ -344,7 +344,11 @@
   <key>EnablePrivateEvents</key>
   <true/>
 
+  <!-- Timezone Service -->
+  <key>EnableTimezoneService</key>
+  <true/>
 
+
   <!--
     Twisted
   -->

Modified: CalendarServer/trunk/conf/caldavd.plist
===================================================================
--- CalendarServer/trunk/conf/caldavd.plist	2008-05-23 16:58:36 UTC (rev 2457)
+++ CalendarServer/trunk/conf/caldavd.plist	2008-05-23 18:06:05 UTC (rev 2458)
@@ -265,6 +265,10 @@
   <key>EnablePrivateEvents</key>
   <true/>
 
+  <!-- Timezone Service -->
+  <key>EnableTimezoneService</key>
+  <true/>
 
+
 </dict>
 </plist>

Modified: CalendarServer/trunk/twistedcaldav/config.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/config.py	2008-05-23 16:58:36 UTC (rev 2457)
+++ CalendarServer/trunk/twistedcaldav/config.py	2008-05-23 18:06:05 UTC (rev 2458)
@@ -154,8 +154,9 @@
     #
     # Non-standard CalDAV extensions
     #
-    "EnableDropBox"      : False, # Calendar Drop Box
-    "EnablePrivateEvents": False, # Private Events
+    "EnableDropBox"         : False, # Calendar Drop Box
+    "EnablePrivateEvents"   : False, # Private Events
+    "EnableTimezoneService" : False, # Timezone service
 
     #
     # Implementation details

Modified: CalendarServer/trunk/twistedcaldav/customxml.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/customxml.py	2008-05-23 16:58:36 UTC (rev 2457)
+++ CalendarServer/trunk/twistedcaldav/customxml.py	2008-05-23 18:06:05 UTC (rev 2458)
@@ -188,6 +188,62 @@
 
         return found
 
+class Timezones (davxml.WebDAVEmptyElement):
+    """
+    Denotes a timezone service resource.
+    (Apple Extension to CalDAV)
+    """
+    namespace = calendarserver_namespace
+    name = "timezones"
+
+class TZIDs (davxml.WebDAVElement):
+    """
+    Wraps a list of timezone ids.
+    """
+    namespace = calendarserver_namespace
+    name = "tzids"
+    allowed_children = { (calendarserver_namespace, "tzid" ): (0, None) }
+
+class TZID (davxml.WebDAVTextElement):
+    """
+    A timezone id.
+    """
+    namespace = calendarserver_namespace
+    name = "tzid"
+
+class TZData (davxml.WebDAVElement):
+    """
+    Wraps a list of timezone observances.
+    """
+    namespace = calendarserver_namespace
+    name = "tzdata"
+    allowed_children = { (calendarserver_namespace, "observance" ): (0, None) }
+
+class Observance (davxml.WebDAVElement):
+    """
+    A timezone observance.
+    """
+    namespace = calendarserver_namespace
+    name = "observance"
+    allowed_children = {
+        (calendarserver_namespace, "onset" )     : (1, 1),
+        (calendarserver_namespace, "utc-offset" ): (1, 1),
+    }
+
+class Onset (davxml.WebDAVTextElement):
+    """
+    The onset date-time for a DST transition.
+    """
+    namespace = calendarserver_namespace
+    name = "onset"
+
+class UTCOffset (davxml.WebDAVTextElement):
+    """
+    A UTC offset value for a timezone observance.
+    """
+    namespace = calendarserver_namespace
+    name = "utc-offset"
+
 ##
 # Extensions to davxml.ResourceType
 ##
@@ -196,3 +252,4 @@
 davxml.ResourceType.dropbox = davxml.ResourceType(davxml.Collection(), DropBox())
 davxml.ResourceType.calendarproxyread = davxml.ResourceType(davxml.Principal(), davxml.Collection(), CalendarProxyRead())
 davxml.ResourceType.calendarproxywrite = davxml.ResourceType(davxml.Principal(), davxml.Collection(), CalendarProxyWrite())
+davxml.ResourceType.timezones = davxml.ResourceType(Timezones())

Modified: CalendarServer/trunk/twistedcaldav/ical.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/ical.py	2008-05-23 16:58:36 UTC (rev 2457)
+++ CalendarServer/trunk/twistedcaldav/ical.py	2008-05-23 18:06:05 UTC (rev 2458)
@@ -37,6 +37,9 @@
 from vobject.base import ContentLine as vContentLine
 from vobject.base import ParseError as vParseError
 from vobject.icalendar import TimezoneComponent
+from vobject.icalendar import dateTimeToString
+from vobject.icalendar import deltaToOffset
+from vobject.icalendar import getTransition
 from vobject.icalendar import stringToDate, stringToDateTime, stringToDurations
 from vobject.icalendar import utc
 
@@ -1191,6 +1194,84 @@
 _regex_duration = None
 
 ##
+# Timezones
+##
+
+def tzexpand(tzdata, start, end):
+    """
+    Expand a timezone to get onset/utc-offset observance tuples withinthe specified
+    time range.
+
+    @param tzdata: the iCalendar data containing a VTIMEZONE.
+    @type tzdata: C{str}
+    @param start: date for the start of the expansion.
+    @type start: C{date}
+    @param end: date for the end of the expansion.
+    @type end: C{date}
+    
+    @return: a C{list} of tuples of (C{datetime}, C{str})
+    """
+    
+    start = datetime.datetime.fromordinal(start.toordinal())
+    end = datetime.datetime.fromordinal(end.toordinal())
+    icalobj = Component.fromString(tzdata)
+    tzcomp = None
+    for comp in icalobj.subcomponents():
+        if comp.name() == "VTIMEZONE":
+            tzcomp = comp
+            break
+    else:
+        raise ValueError("No VTIMEZONE component in %s" % (tzdata,))
+
+    tzinfo = tzcomp.gettzinfo()
+    
+    results = []
+    
+    # Get the start utc-offset - that is our first value
+    results.append((dateTimeToString(start), deltaToOffset(tzinfo.utcoffset(start)),))
+    last_dt = start
+    
+    while last_dt < end:
+        # Get the transitions for the current year
+        standard = getTransition("standard", last_dt.year, tzinfo)
+        daylight = getTransition("daylight", last_dt.year, tzinfo)
+        
+        # Order the transitions
+        if standard and daylight:
+            if standard < daylight:
+                first = standard
+                second = daylight
+            else:
+                first = daylight
+                second = standard
+        elif standard:
+            first = standard
+            second = None
+        else:
+            first = daylight
+            second = None
+        
+        for transition in (first, second):
+            # Terminate if the next transition is outside the time range
+            if transition and transition > end:
+                break
+            
+            # If the next transition is after the last one, then add its info if
+            # the utc-offset actually changed.
+            if transition and transition > last_dt:
+                utcoffset = deltaToOffset(tzinfo.utcoffset(transition + datetime.timedelta(days=1)))
+                if utcoffset != results[-1][1]:
+                    results.append((dateTimeToString(transition), utcoffset,))
+                last_dt = transition
+            
+        # Bump last transition up to the start of the next year
+        last_dt = datetime.datetime(last_dt.year + 1, 1, 1, 0, 0, 0)
+        if last_dt >= end:
+            break
+    
+    return results
+
+##
 # Utilities
 ##
 

Modified: CalendarServer/trunk/twistedcaldav/static.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/static.py	2008-05-23 16:58:36 UTC (rev 2457)
+++ CalendarServer/trunk/twistedcaldav/static.py	2008-05-23 18:06:05 UTC (rev 2458)
@@ -30,6 +30,7 @@
     "DropBoxHomeFile",
     "DropBoxCollectionFile",
     "DropBoxChildFile",
+    "TimezoneServiceFile",
 ]
 
 import datetime
@@ -46,7 +47,7 @@
 from twisted.web2.dav.idav import IDAVResource
 from twisted.web2.dav.resource import AccessDeniedError
 from twisted.web2.dav.resource import davPrivilegeSet
-from twisted.web2.dav.util import parentForURL, bindMethods, allDataFromStream
+from twisted.web2.dav.util import parentForURL, bindMethods
 
 from twistedcaldav import caldavxml
 from twistedcaldav import customxml
@@ -66,6 +67,7 @@
 from twistedcaldav.directory.calendar import DirectoryCalendarHomeResource
 from twistedcaldav.directory.resource import AutoProvisioningResourceMixIn
 from twistedcaldav.log import Logger
+from twistedcaldav.timezoneservice import TimezoneServiceResource
 
 from twistedcaldav.cache import CacheChangeNotifier, PropfindCacheMixin
 
@@ -707,6 +709,31 @@
         else:
             return responsecode.NOT_FOUND
 
+class TimezoneServiceFile (TimezoneServiceResource, CalDAVFile):
+    def __init__(self, path, parent):
+        CalDAVFile.__init__(self, path, principalCollections=parent.principalCollections())
+        TimezoneServiceResource.__init__(self, parent)
+
+        assert self.fp.isfile() or not self.fp.exists()
+
+    def createSimilarFile(self, path):
+        if path == self.fp.path:
+            return self
+        else:
+            return responsecode.NOT_FOUND
+
+    def http_PUT        (self, request): return responsecode.FORBIDDEN
+    def http_COPY       (self, request): return responsecode.FORBIDDEN
+    def http_MOVE       (self, request): return responsecode.FORBIDDEN
+    def http_DELETE     (self, request): return responsecode.FORBIDDEN
+    def http_MKCOL      (self, request): return responsecode.FORBIDDEN
+
+    def http_MKCALENDAR(self, request):
+        return ErrorResponse(
+            responsecode.FORBIDDEN,
+            (caldav_namespace, "calendar-collection-location-ok")
+        )
+
 ##
 # Utilities
 ##

Modified: CalendarServer/trunk/twistedcaldav/tap.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/tap.py	2008-05-23 16:58:36 UTC (rev 2457)
+++ CalendarServer/trunk/twistedcaldav/tap.py	2008-05-23 18:06:05 UTC (rev 2458)
@@ -53,6 +53,7 @@
 from twistedcaldav.directory.aggregate import AggregateDirectoryService
 from twistedcaldav.directory.sudo import SudoDirectoryService
 from twistedcaldav.static import CalendarHomeProvisioningFile
+from twistedcaldav.static import TimezoneServiceFile
 from twistedcaldav.timezones import TimezoneCache
 from twistedcaldav import pdmonster
 
@@ -450,9 +451,10 @@
     # default resource classes
     #
 
-    rootResourceClass      = RootResource
-    principalResourceClass = DirectoryPrincipalProvisioningResource
-    calendarResourceClass  = CalendarHomeProvisioningFile
+    rootResourceClass            = RootResource
+    principalResourceClass       = DirectoryPrincipalProvisioningResource
+    calendarResourceClass        = CalendarHomeProvisioningFile
+    timezoneServiceResourceClass = TimezoneServiceFile
 
     def makeService_Slave(self, options):
         #
@@ -526,6 +528,14 @@
         root.putChild('principals', principalCollection)
         root.putChild('calendars', calendarCollection)
 
+		# Timezone service is optional
+        if config.EnableTimezoneService:
+            timezoneService = self.timezoneServiceResourceClass(
+                os.path.join(config.DocumentRoot, "timezones"),
+                root
+            )
+            root.putChild('timezones', timezoneService)
+
         # Configure default ACLs on the root resource
 
         log.info("Setting up default ACEs on root resource")

Modified: CalendarServer/trunk/twistedcaldav/test/test_timezones.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_timezones.py	2008-05-23 16:58:36 UTC (rev 2457)
+++ CalendarServer/trunk/twistedcaldav/test/test_timezones.py	2008-05-23 18:06:05 UTC (rev 2458)
@@ -20,7 +20,8 @@
 from twistedcaldav.ical import Component
 from vobject.icalendar import utc
 from vobject.icalendar import registerTzid
-from twistedcaldav.timezones import TimezoneCache
+from twistedcaldav.timezones import TimezoneCache, TimezoneException
+from twistedcaldav.timezones import readTZ, listTZs
 import datetime
 import os
 
@@ -176,3 +177,36 @@
             self.assertEqual(end, datetime.datetime(2007, 12, 25, 06, 0, 0, tzinfo=utc))
             break;
         tzcache.unregister()
+
+class TimezonePackageTest (twistedcaldav.test.util.TestCase):
+    """
+    Timezone support tests
+    """
+
+    def test_ReadTZ(self):
+        
+        self.assertTrue(readTZ("America/New_York").find("TZID:America/New_York") != -1)
+        self.assertRaises(TimezoneException, readTZ, "America/Pittsburgh")
+
+    def test_ReadTZCached(self):
+        
+        self.assertTrue(readTZ("America/New_York").find("TZID:America/New_York") != -1)
+        self.assertTrue(readTZ("America/New_York").find("TZID:America/New_York") != -1)
+        self.assertRaises(TimezoneException, readTZ, "America/Pittsburgh")
+        self.assertRaises(TimezoneException, readTZ, "America/Pittsburgh")
+
+    def test_ListTZs(self):
+        
+        results = listTZs()
+        self.assertTrue("America/New_York" in results)
+        self.assertTrue("Europe/London" in results)
+        self.assertTrue("GB" in results)
+
+    def test_ListTZsCached(self):
+        
+        results = listTZs()
+        results = listTZs()
+        self.assertTrue("America/New_York" in results)
+        self.assertTrue("Europe/London" in results)
+        self.assertTrue("GB" in results)
+        

Modified: CalendarServer/trunk/twistedcaldav/timezones.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/timezones.py	2008-05-23 16:58:36 UTC (rev 2457)
+++ CalendarServer/trunk/twistedcaldav/timezones.py	2008-05-23 18:06:05 UTC (rev 2458)
@@ -73,18 +73,15 @@
         if getTzid(tzid) != None:
             return False
 
-        tzStream = openTZ(tzid)
-        try:
-            calendar = Component.fromStream(tzStream)
+        tzData = readTZ(tzid)
+        calendar = Component.fromString(tzData)
 
-            if calendar.name() != "VCALENDAR":
-                raise TimezoneException("%s does not contain valid iCalendar data." % (tzStream.name,))
+        if calendar.name() != "VCALENDAR":
+            raise TimezoneException("%s does not contain valid iCalendar data." % (tzStream.name,))
 
-            # Check that we now have it cached
-            if getTzid(tzid) == None:
-                raise TimezoneException("Could not read timezone %s from %s." % (tzid, tzStream.name))
-        finally:
-            tzStream.close()
+        # Check that we now have it cached
+        if getTzid(tzid) == None:
+            raise TimezoneException("Could not read timezone %s from %s." % (tzid, tzStream.name))
         
         return True
 
@@ -102,23 +99,65 @@
             self.vobjectRegisterTzid(tzid, tzinfo)
 
 try:
+    # zoneinfo never changes in a running instance so cache all this data as we use it
+    cachedTZs = {}
+    cachedTZIDs = []
+
     import pkg_resources
 except ImportError:
     #
     # We don't have pkg_resources, so assume file paths work, since that's all we have
     #
+    
     dirname = os.path.join(os.path.dirname(__file__), "zoneinfo")
-    def openTZ(tzid):
-        tzpath = os.path.join(*tzid.split("/")) # Don't assume "/" from tzid is a path separator
-        tzpath = os.path.join(dirname, tzpath + ".ics")
-        try:
-            return file(tzpath)
-        except IOError:
-            raise TimezoneException("Unknown time zone: %s" % (tzid,))
+    def readTZ(tzid):
+
+        if tzid not in cachedTZs:
+            tzpath = os.path.join(*tzid.split("/")) # Don't assume "/" from tzid is a path separator
+            tzpath = os.path.join(dirname, tzpath + ".ics")
+            try:
+                cachedTZs[tzid] = file(tzpath).read()
+            except IOError:
+                raise TimezoneException("Unknown time zone: %s" % (tzid,))
+            
+        return cachedTZs[tzid]
+        
+    def listTZs(path=""):
+        if not path and cachedTZIDs:
+            return cachedTZIDs
+
+        result = []
+        for item in os.listdir(os.path.join(dirname, path)):
+            if item.find('.') == -1:
+                result.extend(listTZs(os.path.join(path, item)))
+            elif item.endswith(".ics"):
+                result.append(os.path.join(path, item[:-4]))
+                
+        if not path:
+            cachedTZIDs.extend(result)
+        return result
 else:
-    def openTZ(tzid):
-        # Here, "/" is always the path separator
-        try:
-            return pkg_resources.resource_stream("twistedcaldav", "zoneinfo/%s.ics" % (tzid,))
-        except IOError:
-            raise TimezoneException("Unknown time zone: %s" % (tzid,))
+    def readTZ(tzid):
+        if tzid not in cachedTZs:
+            # Here, "/" is always the path separator
+            try:
+                cachedTZs[tzid] = pkg_resources.resource_stream("twistedcaldav", "zoneinfo/%s.ics" % (tzid,)).read()
+            except IOError:
+                raise TimezoneException("Unknown time zone: %s" % (tzid,))
+            
+        return cachedTZs[tzid]
+
+    def listTZs(path=""):  
+        if not path and cachedTZIDs:
+            return cachedTZIDs
+
+        result = []
+        for item in pkg_resources.resource_listdir("twistedcaldav", os.path.join("zoneinfo", path)):
+            if item.find('.') == -1:
+                result.extend(listTZs(os.path.join(path, item)))
+            elif item.endswith(".ics"):
+                result.append(os.path.join(path, item[:-4]))
+                
+        if not path:
+            cachedTZIDs.extend(result)
+        return result

Copied: CalendarServer/trunk/twistedcaldav/timezoneservice.py (from rev 2457, CalendarServer/branches/users/cdaboo/timezone-service-2436/twistedcaldav/timezoneservice.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/timezoneservice.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/timezoneservice.py	2008-05-23 18:06:05 UTC (rev 2458)
@@ -0,0 +1,212 @@
+##
+# Copyright (c) 2008 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+from twistedcaldav.ical import tzexpand
+
+"""
+Timezone service resource and operations.
+"""
+
+__all__ = [
+    "TimezoneServiceResource",
+]
+
+from twisted.internet.defer import deferredGenerator, waitForDeferred
+from twisted.web2 import responsecode
+from twisted.web2.dav import davxml
+from twisted.web2.dav.http import ErrorResponse
+from twisted.web2.http import HTTPError
+from twisted.web2.http import Response
+from twisted.web2.http_headers import MimeType
+from twisted.web2.stream import MemoryStream
+
+from twistedcaldav import customxml
+from twistedcaldav.customxml import calendarserver_namespace
+from twistedcaldav.extensions import XMLResponse
+from twistedcaldav.ical import parse_date_or_datetime
+from twistedcaldav.resource import CalDAVResource
+from twistedcaldav.timezones import TimezoneException
+from twistedcaldav.timezones import listTZs
+from twistedcaldav.timezones import readTZ
+
+class TimezoneServiceResource (CalDAVResource):
+    """
+    Timezone Service resource.
+
+    Extends L{DAVResource} to provide timezone service functionality.
+    """
+
+    def __init__(self, parent):
+        """
+        @param parent: the parent resource of this one.
+        """
+        assert parent is not None
+
+        CalDAVResource.__init__(self, principalCollections=parent.principalCollections())
+
+        self.parent = parent
+        self.cache = {}
+
+    def defaultAccessControlList(self):
+        return davxml.ACL(
+            # DAV:Read for all principals (includes anonymous)
+            davxml.ACE(
+                davxml.Principal(davxml.All()),
+                davxml.Grant(
+                    davxml.Privilege(davxml.Read()),
+                ),
+                davxml.Protected(),
+            ),
+        )
+
+    def resourceType(self):
+        return davxml.ResourceType.timezones
+
+    def isCollection(self):
+        return False
+
+    def isCalendarCollection(self):
+        return False
+
+    def isPseudoCalendarCollection(self):
+        return False
+
+    def render(self, request):
+        output = """<html>
+<head>
+<title>Timezone Service Resource</title>
+</head>
+<body>
+<h1>Timezone Service Resource.</h1>
+</body
+</html>"""
+
+        response = Response(200, {}, output)
+        response.headers.setHeader("content-type", MimeType("text", "html"))
+        return response
+
+    def http_GET(self, request):
+        """
+        The timezone service POST method.
+        """
+        
+        # GET and POST do the same thing
+        return self.http_POST(request)
+
+    @deferredGenerator
+    def http_POST(self, request):
+        """
+        The timezone service POST method.
+        """
+
+        # Check authentication and access controls
+        x = waitForDeferred(self.authorize(request, (davxml.Read(),)))
+        yield x
+        x.getResult()
+        
+        if not request.args:
+            # Do normal GET behavior
+            yield self.render(request)
+            return
+
+        method = request.args.get("method", ("",))
+        if len(method) != 1:
+            raise HTTPError(ErrorResponse(responsecode.BAD_REQUEST, (calendarserver_namespace, "valid-method")))
+        method = method[0]
+            
+        action = {
+            "list"   : self.doPOSTList,
+            "get"    : self.doPOSTGet,
+            "expand" : self.doPOSTExpand,
+        }.get(method, None)
+        
+        if action is None:
+            raise HTTPError(ErrorResponse(responsecode.BAD_REQUEST, (calendarserver_namespace, "supported-method")))
+
+        yield action(request)
+
+    def doPOSTList(self, request):
+        """
+        Return a list of all timezones known to the server.
+        """
+        
+        tzids = listTZs()
+        tzids.sort()
+        result = customxml.TZIDs(*[customxml.TZID(tzid) for tzid in tzids])
+        return XMLResponse(responsecode.OK, result)
+
+    def doPOSTGet(self, request):
+        """
+        Return the specified timezone data.
+        """
+        
+        tzid = request.args.get("tzid", ())
+        if len(tzid) != 1:
+            raise HTTPError(ErrorResponse(responsecode.BAD_REQUEST, (calendarserver_namespace, "valid-timezone")))
+        tzid = tzid[0]
+
+        try:
+            tzdata = readTZ(tzid)
+        except TimezoneException:
+            raise HTTPError(ErrorResponse(responsecode.NOT_FOUND, (calendarserver_namespace, "timezone-available")))
+
+        response = Response()
+        response.stream = MemoryStream(tzdata)
+        response.headers.setHeader("content-type", MimeType.fromString("text/calendar; charset=utf-8"))
+        return response
+
+    def doPOSTExpand(self, request):
+        """
+        Expand a timezone within specified start/end dates.
+        """
+
+        tzid = request.args.get("tzid", ())
+        if len(tzid) != 1:
+            raise HTTPError(ErrorResponse(responsecode.BAD_REQUEST, (calendarserver_namespace, "valid-timezone")))
+        tzid = tzid[0]
+        try:
+            tzdata = readTZ(tzid)
+        except TimezoneException:
+            raise HTTPError(ErrorResponse(responsecode.NOT_FOUND, (calendarserver_namespace, "timezone-available")))
+
+        try:
+            start = request.args.get("start", ())
+            if len(start) != 1:
+                raise ValueError()
+            start = parse_date_or_datetime(start[0])
+        except ValueError:
+            raise HTTPError(ErrorResponse(responsecode.BAD_REQUEST, (calendarserver_namespace, "valid-start-date")))
+
+        try:
+            end = request.args.get("end", ())
+            if len(end) != 1:
+                raise ValueError()
+            end = parse_date_or_datetime(end[0])
+            if end <= start:
+                raise ValueError()
+        except ValueError:
+            raise HTTPError(ErrorResponse(responsecode.BAD_REQUEST, (calendarserver_namespace, "valid-end-date")))
+
+        # Now do the expansion (but use a cache to avoid re-calculating TZs)
+        observances = self.cache.get((tzid, start, end), None)
+        if observances is None:
+            observances = tzexpand(tzdata, start, end)
+            self.cache[(tzid, start, end)] = observances
+
+        # Turn into XML
+        result = customxml.TZData(
+            *[customxml.Observance(customxml.Onset(onset), customxml.UTCOffset(utc_offset)) for onset, utc_offset in observances]
+        )
+        return XMLResponse(responsecode.OK, result)

-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20080523/d6684a44/attachment-0001.htm 


More information about the calendarserver-changes mailing list