Commits

David Warburton committed c00c60d

Initial check in. Basic functionality implemented.

Comments (0)

Files changed (42)

+syntax: glob
+*.pyc
+cache/*
+import sublime, sublime_plugin
+import dateutil, yaml, shopify
+import json, os, threading, re
+
+
+with open('Stores.json') as f:
+    stores = json.load(f)
+
+store_keys = [x for x in stores]
+current_store = None
+current_theme = None
+api_lock = threading.Lock()
+
+def connect(shop):
+    shopify.ShopifyResource.site = "https://%s:%s@%s.myshopify.com/admin" % (shop['API_KEY'], shop['API_PASSWORD'], shop['SHOP_NAME'])
+
+
+class ShopifyShowStoresCommand(sublime_plugin.WindowCommand):
+    def run(self):
+		commands = ["Shopify: Browse Themes - %s" % x for x in store_keys]
+		def on_shop_select(picked):
+			global current_store
+			if picked == -1:
+				return
+			current_store =  store_keys[picked] 
+
+			api_lock.acquire()
+			connect(stores[current_store])
+			stores[current_store]['themes'] = shopify.Theme.find()
+			api_lock.release()
+			
+			self.window.run_command('shopify_show_themes')
+		self.window.show_quick_panel(commands, on_shop_select)
+    	
+
+class ShopifyShowThemesCommand(sublime_plugin.WindowCommand):
+	def run(self):
+		if (current_store == None):
+			self.window.run_command('shopify_show_stores')
+			return
+
+		commands = ["Shopify: %s: %s (%s)" % (current_store, theme.name, theme.role) for theme in stores[current_store]['themes']]
+		def on_theme_select(picked):
+			if picked == -1:
+				return
+			global current_theme
+			current_theme = stores[current_store]['themes'][picked]
+			
+			api_lock.acquire()
+			all_assets = shopify.Asset.find(theme_id = current_theme.id)
+			keys = map(lambda a: a.key, all_assets)
+			current_theme.assets = [a for a in all_assets if "%s.liquid" % a.key not in keys and not re.search("(\.png|\.gif|\.jpg)$", a.key)]
+			api_lock.release()
+
+			self.window.run_command('shopify_show_assets')
+		self.window.show_quick_panel(commands, on_theme_select)
+
+class ShopifyShowAssetsCommand(sublime_plugin.WindowCommand):
+	def run(self):
+		if (current_store == None):
+			self.window.run_command('shopify_show_stores')
+			return
+		
+		commands = ["%s: %s" % (current_theme.name, a.key) for a in current_theme.assets]
+		def on_asset_select(picked):
+			if picked == -1:
+				return
+			key		     = current_theme.assets[picked].key
+			key1, key2      = key.split('/')
+			asset_file_name = os.path.join(current_store, str(current_theme.id), key1, key2)
+			root		    = os.path.join(sublime.packages_path(),'Shopify', 'cache')
+			asset_full_name = os.path.join(root,asset_file_name)
+
+			api_lock.acquire()
+			asset = shopify.Asset.find(key, theme_id = current_theme.id)
+			api_lock.release()
+
+			if ( not os.path.exists(os.path.dirname(asset_full_name))):
+				os.makedirs(os.path.dirname(asset_full_name))
+
+			with open(asset_full_name, 'wb') as f:
+				f.write(asset.value)
+			self.window.run_command('open_file', {"file":os.path.join('cache', asset_file_name)})
+
+		self.window.show_quick_panel(commands, on_asset_select)
+
+class ShopifyUploadOnSave(sublime_plugin.EventListener):
+    def on_post_save(self, view):
+		root = os.path.join(sublime.packages_path(),'Shopify','cache')
+		relpath = os.path.relpath(view.file_name(),root)
+		if relpath[0:2] != '..':
+		    store, theme_id, asset_type, asset_name = relpath.split(os.sep)
+		    key = "%s/%s" %(asset_type, asset_name)
+		    pusher = ShopifyPusher(store, theme_id, key, view.file_name())
+		    pusher.start()
+		    
+		    
+
+class ShopifyPusher(threading.Thread):
+	def __init__(self, store, theme_id, key, filename):
+		self.store = store
+		self.theme_id = theme_id
+		self.key = key
+		self.filename = filename
+		self.result = None
+		threading.Thread.__init__(self)
+
+	def run(self):
+		api_lock.acquire()
+		save_site = shopify.ShopifyResource.site
+		connect(stores[self.store])
+		asset = shopify.Asset.find(self.key, theme_id = self.theme_id)
+		with open(self.filename, 'rb') as f:
+			asset.value = f.read()
+		asset.save()
+		shopify.ShopifyResource.site = save_site
+		api_lock.release()
+		self.result = True
+

Shopify.sublime-commands

+[
+    {
+        "caption": "Shopify: Edit store information",
+		"command": "open_file", "args":
+        {
+            "file": "${packages}/Shopify/Stores.json"
+        }
+    },
+    {
+    	"caption": "Shopify: Choose store",
+    	"command": "shopify_show_stores"
+    },
+    {
+    	"caption": "Shopify: Switch theme",
+    	"command": "shopify_show_themes"
+    },
+    {
+        "caption": "Shopify: Edit asset",
+        "command": "shopify_show_assets"
+    }
+
+]
+{
+    "Human Readable Shop Name":{
+    	"API_KEY": "API-KEY-HERE",
+		"API_PASSWORD": "API-PASSWORD-HERE",
+		"SHOP_NAME": "SHOP-NAME-AS-IT-APPEARS-IN-URLS"
+    }
+}

dateutil/__init__.py

+"""
+Copyright (c) 2003-2010  Gustavo Niemeyer <gustavo@niemeyer.net>
+
+This module offers extensions to the standard python 2.3+
+datetime module.
+"""
+__author__ = "Gustavo Niemeyer <gustavo@niemeyer.net>"
+__license__ = "PSF License"
+__version__ = "1.5"

dateutil/easter.py

+"""
+Copyright (c) 2003-2007  Gustavo Niemeyer <gustavo@niemeyer.net>
+
+This module offers extensions to the standard python 2.3+
+datetime module.
+"""
+__author__ = "Gustavo Niemeyer <gustavo@niemeyer.net>"
+__license__ = "PSF License"
+
+import datetime
+
+__all__ = ["easter", "EASTER_JULIAN", "EASTER_ORTHODOX", "EASTER_WESTERN"]
+
+EASTER_JULIAN   = 1
+EASTER_ORTHODOX = 2
+EASTER_WESTERN  = 3
+
+def easter(year, method=EASTER_WESTERN):
+    """
+    This method was ported from the work done by GM Arts,
+    on top of the algorithm by Claus Tondering, which was
+    based in part on the algorithm of Ouding (1940), as
+    quoted in "Explanatory Supplement to the Astronomical
+    Almanac", P.  Kenneth Seidelmann, editor.
+
+    This algorithm implements three different easter
+    calculation methods:
+    
+    1 - Original calculation in Julian calendar, valid in
+        dates after 326 AD
+    2 - Original method, with date converted to Gregorian
+        calendar, valid in years 1583 to 4099
+    3 - Revised method, in Gregorian calendar, valid in
+        years 1583 to 4099 as well
+
+    These methods are represented by the constants:
+
+    EASTER_JULIAN   = 1
+    EASTER_ORTHODOX = 2
+    EASTER_WESTERN  = 3
+
+    The default method is method 3.
+    
+    More about the algorithm may be found at:
+
+    http://users.chariot.net.au/~gmarts/eastalg.htm
+
+    and
+
+    http://www.tondering.dk/claus/calendar.html
+
+    """
+
+    if not (1 <= method <= 3):
+        raise ValueError, "invalid method"
+
+    # g - Golden year - 1
+    # c - Century
+    # h - (23 - Epact) mod 30
+    # i - Number of days from March 21 to Paschal Full Moon
+    # j - Weekday for PFM (0=Sunday, etc)
+    # p - Number of days from March 21 to Sunday on or before PFM
+    #     (-6 to 28 methods 1 & 3, to 56 for method 2)
+    # e - Extra days to add for method 2 (converting Julian
+    #     date to Gregorian date)
+
+    y = year
+    g = y % 19
+    e = 0
+    if method < 3:
+        # Old method
+        i = (19*g+15)%30
+        j = (y+y//4+i)%7
+        if method == 2:
+            # Extra dates to convert Julian to Gregorian date
+            e = 10
+            if y > 1600:
+                e = e+y//100-16-(y//100-16)//4
+    else:
+        # New method
+        c = y//100
+        h = (c-c//4-(8*c+13)//25+19*g+15)%30
+        i = h-(h//28)*(1-(h//28)*(29//(h+1))*((21-g)//11))
+        j = (y+y//4+i+2-c+c//4)%7
+
+    # p can be from -6 to 56 corresponding to dates 22 March to 23 May
+    # (later dates apply to method 2, although 23 May never actually occurs)
+    p = i-j+e
+    d = 1+(p+27+(p+6)//40)%31
+    m = 3+(p+26)//30
+    return datetime.date(int(y),int(m),int(d))
+

dateutil/parser.py

+# -*- coding:iso-8859-1 -*-
+"""
+Copyright (c) 2003-2007  Gustavo Niemeyer <gustavo@niemeyer.net>
+
+This module offers extensions to the standard python 2.3+
+datetime module.
+"""
+__author__ = "Gustavo Niemeyer <gustavo@niemeyer.net>"
+__license__ = "PSF License"
+
+import datetime
+import string
+import time
+import sys
+import os
+
+try:
+    from cStringIO import StringIO
+except ImportError:
+    from StringIO import StringIO
+
+import relativedelta
+import tz
+
+
+__all__ = ["parse", "parserinfo"]
+
+
+# Some pointers:
+#
+# http://www.cl.cam.ac.uk/~mgk25/iso-time.html
+# http://www.iso.ch/iso/en/prods-services/popstds/datesandtime.html
+# http://www.w3.org/TR/NOTE-datetime
+# http://ringmaster.arc.nasa.gov/tools/time_formats.html
+# http://search.cpan.org/author/MUIR/Time-modules-2003.0211/lib/Time/ParseDate.pm
+# http://stein.cshl.org/jade/distrib/docs/java.text.SimpleDateFormat.html
+
+
+class _timelex(object):
+
+    def __init__(self, instream):
+        if isinstance(instream, basestring):
+            instream = StringIO(instream)
+        self.instream = instream
+        self.wordchars = ('abcdfeghijklmnopqrstuvwxyz'
+                          'ABCDEFGHIJKLMNOPQRSTUVWXYZ_'
+                          '��������������������������������'
+                          '������������������������������')
+        self.numchars = '0123456789'
+        self.whitespace = ' \t\r\n'
+        self.charstack = []
+        self.tokenstack = []
+        self.eof = False
+
+    def get_token(self):
+        if self.tokenstack:
+            return self.tokenstack.pop(0)
+        seenletters = False
+        token = None
+        state = None
+        wordchars = self.wordchars
+        numchars = self.numchars
+        whitespace = self.whitespace
+        while not self.eof:
+            if self.charstack:
+                nextchar = self.charstack.pop(0)
+            else:
+                nextchar = self.instream.read(1)
+                while nextchar == '\x00':
+                    nextchar = self.instream.read(1)
+            if not nextchar:
+                self.eof = True
+                break
+            elif not state:
+                token = nextchar
+                if nextchar in wordchars:
+                    state = 'a'
+                elif nextchar in numchars:
+                    state = '0'
+                elif nextchar in whitespace:
+                    token = ' '
+                    break # emit token
+                else:
+                    break # emit token
+            elif state == 'a':
+                seenletters = True
+                if nextchar in wordchars:
+                    token += nextchar
+                elif nextchar == '.':
+                    token += nextchar
+                    state = 'a.'
+                else:
+                    self.charstack.append(nextchar)
+                    break # emit token
+            elif state == '0':
+                if nextchar in numchars:
+                    token += nextchar
+                elif nextchar == '.':
+                    token += nextchar
+                    state = '0.'
+                else:
+                    self.charstack.append(nextchar)
+                    break # emit token
+            elif state == 'a.':
+                seenletters = True
+                if nextchar == '.' or nextchar in wordchars:
+                    token += nextchar
+                elif nextchar in numchars and token[-1] == '.':
+                    token += nextchar
+                    state = '0.'
+                else:
+                    self.charstack.append(nextchar)
+                    break # emit token
+            elif state == '0.':
+                if nextchar == '.' or nextchar in numchars:
+                    token += nextchar
+                elif nextchar in wordchars and token[-1] == '.':
+                    token += nextchar
+                    state = 'a.'
+                else:
+                    self.charstack.append(nextchar)
+                    break # emit token
+        if (state in ('a.', '0.') and
+            (seenletters or token.count('.') > 1 or token[-1] == '.')):
+            l = token.split('.')
+            token = l[0]
+            for tok in l[1:]:
+                self.tokenstack.append('.')
+                if tok:
+                    self.tokenstack.append(tok)
+        return token
+
+    def __iter__(self):
+        return self
+
+    def next(self):
+        token = self.get_token()
+        if token is None:
+            raise StopIteration
+        return token
+
+    def split(cls, s):
+        return list(cls(s))
+    split = classmethod(split)
+
+
+class _resultbase(object):
+
+    def __init__(self):
+        for attr in self.__slots__:
+            setattr(self, attr, None)
+
+    def _repr(self, classname):
+        l = []
+        for attr in self.__slots__:
+            value = getattr(self, attr)
+            if value is not None:
+                l.append("%s=%s" % (attr, `value`))
+        return "%s(%s)" % (classname, ", ".join(l))
+
+    def __repr__(self):
+        return self._repr(self.__class__.__name__)
+
+
+class parserinfo(object):
+
+    # m from a.m/p.m, t from ISO T separator
+    JUMP = [" ", ".", ",", ";", "-", "/", "'",
+            "at", "on", "and", "ad", "m", "t", "of",
+            "st", "nd", "rd", "th"] 
+
+    WEEKDAYS = [("Mon", "Monday"),
+                ("Tue", "Tuesday"),
+                ("Wed", "Wednesday"),
+                ("Thu", "Thursday"),
+                ("Fri", "Friday"),
+                ("Sat", "Saturday"),
+                ("Sun", "Sunday")]
+    MONTHS   = [("Jan", "January"),
+                ("Feb", "February"),
+                ("Mar", "March"),
+                ("Apr", "April"),
+                ("May", "May"),
+                ("Jun", "June"),
+                ("Jul", "July"),
+                ("Aug", "August"),
+                ("Sep", "September"),
+                ("Oct", "October"),
+                ("Nov", "November"),
+                ("Dec", "December")]
+    HMS = [("h", "hour", "hours"),
+           ("m", "minute", "minutes"),
+           ("s", "second", "seconds")]
+    AMPM = [("am", "a"),
+            ("pm", "p")]
+    UTCZONE = ["UTC", "GMT", "Z"]
+    PERTAIN = ["of"]
+    TZOFFSET = {}
+
+    def __init__(self, dayfirst=False, yearfirst=False):
+        self._jump = self._convert(self.JUMP)
+        self._weekdays = self._convert(self.WEEKDAYS)
+        self._months = self._convert(self.MONTHS)
+        self._hms = self._convert(self.HMS)
+        self._ampm = self._convert(self.AMPM)
+        self._utczone = self._convert(self.UTCZONE)
+        self._pertain = self._convert(self.PERTAIN)
+
+        self.dayfirst = dayfirst
+        self.yearfirst = yearfirst
+
+        self._year = time.localtime().tm_year
+        self._century = self._year//100*100
+
+    def _convert(self, lst):
+        dct = {}
+        for i in range(len(lst)):
+            v = lst[i]
+            if isinstance(v, tuple):
+                for v in v:
+                    dct[v.lower()] = i
+            else:
+                dct[v.lower()] = i
+        return dct
+
+    def jump(self, name):
+        return name.lower() in self._jump
+
+    def weekday(self, name):
+        if len(name) >= 3:
+            try:
+                return self._weekdays[name.lower()]
+            except KeyError:
+                pass
+        return None
+
+    def month(self, name):
+        if len(name) >= 3:
+            try:
+                return self._months[name.lower()]+1
+            except KeyError:
+                pass
+        return None
+
+    def hms(self, name):
+        try:
+            return self._hms[name.lower()]
+        except KeyError:
+            return None
+
+    def ampm(self, name):
+        try:
+            return self._ampm[name.lower()]
+        except KeyError:
+            return None
+
+    def pertain(self, name):
+        return name.lower() in self._pertain
+
+    def utczone(self, name):
+        return name.lower() in self._utczone
+
+    def tzoffset(self, name):
+        if name in self._utczone:
+            return 0
+        return self.TZOFFSET.get(name)
+
+    def convertyear(self, year):
+        if year < 100:
+            year += self._century
+            if abs(year-self._year) >= 50:
+                if year < self._year:
+                    year += 100
+                else:
+                    year -= 100
+        return year
+
+    def validate(self, res):
+        # move to info
+        if res.year is not None:
+            res.year = self.convertyear(res.year)
+        if res.tzoffset == 0 and not res.tzname or res.tzname == 'Z':
+            res.tzname = "UTC"
+            res.tzoffset = 0
+        elif res.tzoffset != 0 and res.tzname and self.utczone(res.tzname):
+            res.tzoffset = 0
+        return True
+
+
+class parser(object):
+
+    def __init__(self, info=None):
+        self.info = info or parserinfo()
+
+    def parse(self, timestr, default=None,
+                    ignoretz=False, tzinfos=None,
+                    **kwargs):
+        if not default:
+            default = datetime.datetime.now().replace(hour=0, minute=0,
+                                                      second=0, microsecond=0)
+        res = self._parse(timestr, **kwargs)
+        if res is None:
+            raise ValueError, "unknown string format"
+        repl = {}
+        for attr in ["year", "month", "day", "hour",
+                     "minute", "second", "microsecond"]:
+            value = getattr(res, attr)
+            if value is not None:
+                repl[attr] = value
+        ret = default.replace(**repl)
+        if res.weekday is not None and not res.day:
+            ret = ret+relativedelta.relativedelta(weekday=res.weekday)
+        if not ignoretz:
+            if callable(tzinfos) or tzinfos and res.tzname in tzinfos:
+                if callable(tzinfos):
+                    tzdata = tzinfos(res.tzname, res.tzoffset)
+                else:
+                    tzdata = tzinfos.get(res.tzname)
+                if isinstance(tzdata, datetime.tzinfo):
+                    tzinfo = tzdata
+                elif isinstance(tzdata, basestring):
+                    tzinfo = tz.tzstr(tzdata)
+                elif isinstance(tzdata, int):
+                    tzinfo = tz.tzoffset(res.tzname, tzdata)
+                else:
+                    raise ValueError, "offset must be tzinfo subclass, " \
+                                      "tz string, or int offset"
+                ret = ret.replace(tzinfo=tzinfo)
+            elif res.tzname and res.tzname in time.tzname:
+                ret = ret.replace(tzinfo=tz.tzlocal())
+            elif res.tzoffset == 0:
+                ret = ret.replace(tzinfo=tz.tzutc())
+            elif res.tzoffset:
+                ret = ret.replace(tzinfo=tz.tzoffset(res.tzname, res.tzoffset))
+        return ret
+
+    class _result(_resultbase):
+        __slots__ = ["year", "month", "day", "weekday",
+                     "hour", "minute", "second", "microsecond",
+                     "tzname", "tzoffset"]
+
+    def _parse(self, timestr, dayfirst=None, yearfirst=None, fuzzy=False):
+        info = self.info
+        if dayfirst is None:
+            dayfirst = info.dayfirst
+        if yearfirst is None:
+            yearfirst = info.yearfirst
+        res = self._result()
+        l = _timelex.split(timestr)
+        try:
+
+            # year/month/day list
+            ymd = []
+
+            # Index of the month string in ymd
+            mstridx = -1
+
+            len_l = len(l)
+            i = 0
+            while i < len_l:
+
+                # Check if it's a number
+                try:
+                    value_repr = l[i]
+                    value = float(value_repr)
+                except ValueError:
+                    value = None
+
+                if value is not None:
+                    # Token is a number
+                    len_li = len(l[i])
+                    i += 1
+                    if (len(ymd) == 3 and len_li in (2, 4)
+                        and (i >= len_l or (l[i] != ':' and
+                                            info.hms(l[i]) is None))):
+                        # 19990101T23[59]
+                        s = l[i-1]
+                        res.hour = int(s[:2])
+                        if len_li == 4:
+                            res.minute = int(s[2:])
+                    elif len_li == 6 or (len_li > 6 and l[i-1].find('.') == 6):
+                        # YYMMDD or HHMMSS[.ss]
+                        s = l[i-1] 
+                        if not ymd and l[i-1].find('.') == -1:
+                            ymd.append(info.convertyear(int(s[:2])))
+                            ymd.append(int(s[2:4]))
+                            ymd.append(int(s[4:]))
+                        else:
+                            # 19990101T235959[.59]
+                            res.hour = int(s[:2])
+                            res.minute = int(s[2:4])
+                            res.second, res.microsecond = _parsems(s[4:])
+                    elif len_li == 8:
+                        # YYYYMMDD
+                        s = l[i-1]
+                        ymd.append(int(s[:4]))
+                        ymd.append(int(s[4:6]))
+                        ymd.append(int(s[6:]))
+                    elif len_li in (12, 14):
+                        # YYYYMMDDhhmm[ss]
+                        s = l[i-1]
+                        ymd.append(int(s[:4]))
+                        ymd.append(int(s[4:6]))
+                        ymd.append(int(s[6:8]))
+                        res.hour = int(s[8:10])
+                        res.minute = int(s[10:12])
+                        if len_li == 14:
+                            res.second = int(s[12:])
+                    elif ((i < len_l and info.hms(l[i]) is not None) or
+                          (i+1 < len_l and l[i] == ' ' and
+                           info.hms(l[i+1]) is not None)):
+                        # HH[ ]h or MM[ ]m or SS[.ss][ ]s
+                        if l[i] == ' ':
+                            i += 1
+                        idx = info.hms(l[i])
+                        while True:
+                            if idx == 0:
+                                res.hour = int(value)
+                                if value%1:
+                                    res.minute = int(60*(value%1))
+                            elif idx == 1:
+                                res.minute = int(value)
+                                if value%1:
+                                    res.second = int(60*(value%1))
+                            elif idx == 2:
+                                res.second, res.microsecond = \
+                                    _parsems(value_repr)
+                            i += 1
+                            if i >= len_l or idx == 2:
+                                break
+                            # 12h00
+                            try:
+                                value_repr = l[i]
+                                value = float(value_repr)
+                            except ValueError:
+                                break
+                            else:
+                                i += 1
+                                idx += 1
+                                if i < len_l:
+                                    newidx = info.hms(l[i])
+                                    if newidx is not None:
+                                        idx = newidx
+                    elif i+1 < len_l and l[i] == ':':
+                        # HH:MM[:SS[.ss]]
+                        res.hour = int(value)
+                        i += 1
+                        value = float(l[i])
+                        res.minute = int(value)
+                        if value%1:
+                            res.second = int(60*(value%1))
+                        i += 1
+                        if i < len_l and l[i] == ':':
+                            res.second, res.microsecond = _parsems(l[i+1])
+                            i += 2
+                    elif i < len_l and l[i] in ('-', '/', '.'):
+                        sep = l[i]
+                        ymd.append(int(value))
+                        i += 1
+                        if i < len_l and not info.jump(l[i]):
+                            try:
+                                # 01-01[-01]
+                                ymd.append(int(l[i]))
+                            except ValueError:
+                                # 01-Jan[-01]
+                                value = info.month(l[i])
+                                if value is not None:
+                                    ymd.append(value)
+                                    assert mstridx == -1
+                                    mstridx = len(ymd)-1
+                                else:
+                                    return None
+                            i += 1
+                            if i < len_l and l[i] == sep:
+                                # We have three members
+                                i += 1
+                                value = info.month(l[i])
+                                if value is not None:
+                                    ymd.append(value)
+                                    mstridx = len(ymd)-1
+                                    assert mstridx == -1
+                                else:
+                                    ymd.append(int(l[i]))
+                                i += 1
+                    elif i >= len_l or info.jump(l[i]):
+                        if i+1 < len_l and info.ampm(l[i+1]) is not None:
+                            # 12 am
+                            res.hour = int(value)
+                            if res.hour < 12 and info.ampm(l[i+1]) == 1:
+                                res.hour += 12
+                            elif res.hour == 12 and info.ampm(l[i+1]) == 0:
+                                res.hour = 0
+                            i += 1
+                        else:
+                            # Year, month or day
+                            ymd.append(int(value))
+                        i += 1
+                    elif info.ampm(l[i]) is not None:
+                        # 12am
+                        res.hour = int(value)
+                        if res.hour < 12 and info.ampm(l[i]) == 1:
+                            res.hour += 12
+                        elif res.hour == 12 and info.ampm(l[i]) == 0:
+                            res.hour = 0
+                        i += 1
+                    elif not fuzzy:
+                        return None
+                    else:
+                        i += 1
+                    continue
+
+                # Check weekday
+                value = info.weekday(l[i])
+                if value is not None:
+                    res.weekday = value
+                    i += 1
+                    continue
+
+                # Check month name
+                value = info.month(l[i])
+                if value is not None:
+                    ymd.append(value)
+                    assert mstridx == -1
+                    mstridx = len(ymd)-1
+                    i += 1
+                    if i < len_l:
+                        if l[i] in ('-', '/'):
+                            # Jan-01[-99]
+                            sep = l[i]
+                            i += 1
+                            ymd.append(int(l[i]))
+                            i += 1
+                            if i < len_l and l[i] == sep:
+                                # Jan-01-99
+                                i += 1
+                                ymd.append(int(l[i]))
+                                i += 1
+                        elif (i+3 < len_l and l[i] == l[i+2] == ' '
+                              and info.pertain(l[i+1])):
+                            # Jan of 01
+                            # In this case, 01 is clearly year
+                            try:
+                                value = int(l[i+3])
+                            except ValueError:
+                                # Wrong guess
+                                pass
+                            else:
+                                # Convert it here to become unambiguous
+                                ymd.append(info.convertyear(value))
+                            i += 4
+                    continue
+
+                # Check am/pm
+                value = info.ampm(l[i])
+                if value is not None:
+                    if value == 1 and res.hour < 12:
+                        res.hour += 12
+                    elif value == 0 and res.hour == 12:
+                        res.hour = 0
+                    i += 1
+                    continue
+
+                # Check for a timezone name
+                if (res.hour is not None and len(l[i]) <= 5 and
+                    res.tzname is None and res.tzoffset is None and
+                    not [x for x in l[i] if x not in string.ascii_uppercase]):
+                    res.tzname = l[i]
+                    res.tzoffset = info.tzoffset(res.tzname)
+                    i += 1
+
+                    # Check for something like GMT+3, or BRST+3. Notice
+                    # that it doesn't mean "I am 3 hours after GMT", but
+                    # "my time +3 is GMT". If found, we reverse the
+                    # logic so that timezone parsing code will get it
+                    # right.
+                    if i < len_l and l[i] in ('+', '-'):
+                        l[i] = ('+', '-')[l[i] == '+']
+                        res.tzoffset = None
+                        if info.utczone(res.tzname):
+                            # With something like GMT+3, the timezone
+                            # is *not* GMT.
+                            res.tzname = None
+
+                    continue
+
+                # Check for a numbered timezone
+                if res.hour is not None and l[i] in ('+', '-'):
+                    signal = (-1,1)[l[i] == '+']
+                    i += 1
+                    len_li = len(l[i])
+                    if len_li == 4:
+                        # -0300
+                        res.tzoffset = int(l[i][:2])*3600+int(l[i][2:])*60
+                    elif i+1 < len_l and l[i+1] == ':':
+                        # -03:00
+                        res.tzoffset = int(l[i])*3600+int(l[i+2])*60
+                        i += 2
+                    elif len_li <= 2:
+                        # -[0]3
+                        res.tzoffset = int(l[i][:2])*3600
+                    else:
+                        return None
+                    i += 1
+                    res.tzoffset *= signal
+
+                    # Look for a timezone name between parenthesis
+                    if (i+3 < len_l and
+                        info.jump(l[i]) and l[i+1] == '(' and l[i+3] == ')' and
+                        3 <= len(l[i+2]) <= 5 and
+                        not [x for x in l[i+2]
+                                if x not in string.ascii_uppercase]):
+                        # -0300 (BRST)
+                        res.tzname = l[i+2]
+                        i += 4
+                    continue
+
+                # Check jumps
+                if not (info.jump(l[i]) or fuzzy):
+                    return None
+
+                i += 1
+
+            # Process year/month/day
+            len_ymd = len(ymd)
+            if len_ymd > 3:
+                # More than three members!?
+                return None
+            elif len_ymd == 1 or (mstridx != -1 and len_ymd == 2):
+                # One member, or two members with a month string
+                if mstridx != -1:
+                    res.month = ymd[mstridx]
+                    del ymd[mstridx]
+                if len_ymd > 1 or mstridx == -1:
+                    if ymd[0] > 31:
+                        res.year = ymd[0]
+                    else:
+                        res.day = ymd[0]
+            elif len_ymd == 2:
+                # Two members with numbers
+                if ymd[0] > 31:
+                    # 99-01
+                    res.year, res.month = ymd
+                elif ymd[1] > 31:
+                    # 01-99
+                    res.month, res.year = ymd
+                elif dayfirst and ymd[1] <= 12:
+                    # 13-01
+                    res.day, res.month = ymd
+                else:
+                    # 01-13
+                    res.month, res.day = ymd
+            if len_ymd == 3:
+                # Three members
+                if mstridx == 0:
+                    res.month, res.day, res.year = ymd
+                elif mstridx == 1:
+                    if ymd[0] > 31 or (yearfirst and ymd[2] <= 31):
+                        # 99-Jan-01
+                        res.year, res.month, res.day = ymd
+                    else:
+                        # 01-Jan-01
+                        # Give precendence to day-first, since
+                        # two-digit years is usually hand-written.
+                        res.day, res.month, res.year = ymd
+                elif mstridx == 2:
+                    # WTF!?
+                    if ymd[1] > 31:
+                        # 01-99-Jan
+                        res.day, res.year, res.month = ymd
+                    else:
+                        # 99-01-Jan
+                        res.year, res.day, res.month = ymd
+                else:
+                    if ymd[0] > 31 or \
+                       (yearfirst and ymd[1] <= 12 and ymd[2] <= 31):
+                        # 99-01-01
+                        res.year, res.month, res.day = ymd
+                    elif ymd[0] > 12 or (dayfirst and ymd[1] <= 12):
+                        # 13-01-01
+                        res.day, res.month, res.year = ymd
+                    else:
+                        # 01-13-01
+                        res.month, res.day, res.year = ymd
+
+        except (IndexError, ValueError, AssertionError):
+            return None
+
+        if not info.validate(res):
+            return None
+        return res
+
+DEFAULTPARSER = parser()
+def parse(timestr, parserinfo=None, **kwargs):
+    if parserinfo:
+        return parser(parserinfo).parse(timestr, **kwargs)
+    else:
+        return DEFAULTPARSER.parse(timestr, **kwargs)
+
+
+class _tzparser(object):
+
+    class _result(_resultbase):
+
+        __slots__ = ["stdabbr", "stdoffset", "dstabbr", "dstoffset",
+                     "start", "end"]
+
+        class _attr(_resultbase):
+            __slots__ = ["month", "week", "weekday",
+                         "yday", "jyday", "day", "time"]
+
+        def __repr__(self):
+            return self._repr("")
+
+        def __init__(self):
+            _resultbase.__init__(self)
+            self.start = self._attr()
+            self.end = self._attr()
+
+    def parse(self, tzstr):
+        res = self._result()
+        l = _timelex.split(tzstr)
+        try:
+
+            len_l = len(l)
+
+            i = 0
+            while i < len_l:
+                # BRST+3[BRDT[+2]]
+                j = i
+                while j < len_l and not [x for x in l[j]
+                                            if x in "0123456789:,-+"]:
+                    j += 1
+                if j != i:
+                    if not res.stdabbr:
+                        offattr = "stdoffset"
+                        res.stdabbr = "".join(l[i:j])
+                    else:
+                        offattr = "dstoffset"
+                        res.dstabbr = "".join(l[i:j])
+                    i = j
+                    if (i < len_l and
+                        (l[i] in ('+', '-') or l[i][0] in "0123456789")):
+                        if l[i] in ('+', '-'):
+                            # Yes, that's right.  See the TZ variable
+                            # documentation.
+                            signal = (1,-1)[l[i] == '+']
+                            i += 1
+                        else:
+                            signal = -1
+                        len_li = len(l[i])
+                        if len_li == 4:
+                            # -0300
+                            setattr(res, offattr,
+                                    (int(l[i][:2])*3600+int(l[i][2:])*60)*signal)
+                        elif i+1 < len_l and l[i+1] == ':':
+                            # -03:00
+                            setattr(res, offattr,
+                                    (int(l[i])*3600+int(l[i+2])*60)*signal)
+                            i += 2
+                        elif len_li <= 2:
+                            # -[0]3
+                            setattr(res, offattr,
+                                    int(l[i][:2])*3600*signal)
+                        else:
+                            return None
+                        i += 1
+                    if res.dstabbr:
+                        break
+                else:
+                    break
+
+            if i < len_l:
+                for j in range(i, len_l):
+                    if l[j] == ';': l[j] = ','
+
+                assert l[i] == ','
+
+                i += 1
+
+            if i >= len_l:
+                pass
+            elif (8 <= l.count(',') <= 9 and
+                not [y for x in l[i:] if x != ','
+                       for y in x if y not in "0123456789"]):
+                # GMT0BST,3,0,30,3600,10,0,26,7200[,3600]
+                for x in (res.start, res.end):
+                    x.month = int(l[i])
+                    i += 2
+                    if l[i] == '-':
+                        value = int(l[i+1])*-1
+                        i += 1
+                    else:
+                        value = int(l[i])
+                    i += 2
+                    if value:
+                        x.week = value
+                        x.weekday = (int(l[i])-1)%7
+                    else:
+                        x.day = int(l[i])
+                    i += 2
+                    x.time = int(l[i])
+                    i += 2
+                if i < len_l:
+                    if l[i] in ('-','+'):
+                        signal = (-1,1)[l[i] == "+"]
+                        i += 1
+                    else:
+                        signal = 1
+                    res.dstoffset = (res.stdoffset+int(l[i]))*signal
+            elif (l.count(',') == 2 and l[i:].count('/') <= 2 and
+                  not [y for x in l[i:] if x not in (',','/','J','M',
+                                                     '.','-',':')
+                         for y in x if y not in "0123456789"]):
+                for x in (res.start, res.end):
+                    if l[i] == 'J':
+                        # non-leap year day (1 based)
+                        i += 1
+                        x.jyday = int(l[i])
+                    elif l[i] == 'M':
+                        # month[-.]week[-.]weekday
+                        i += 1
+                        x.month = int(l[i])
+                        i += 1
+                        assert l[i] in ('-', '.')
+                        i += 1
+                        x.week = int(l[i])
+                        if x.week == 5:
+                            x.week = -1
+                        i += 1
+                        assert l[i] in ('-', '.')
+                        i += 1
+                        x.weekday = (int(l[i])-1)%7
+                    else:
+                        # year day (zero based)
+                        x.yday = int(l[i])+1
+
+                    i += 1
+
+                    if i < len_l and l[i] == '/':
+                        i += 1
+                        # start time
+                        len_li = len(l[i])
+                        if len_li == 4:
+                            # -0300
+                            x.time = (int(l[i][:2])*3600+int(l[i][2:])*60)
+                        elif i+1 < len_l and l[i+1] == ':':
+                            # -03:00
+                            x.time = int(l[i])*3600+int(l[i+2])*60
+                            i += 2
+                            if i+1 < len_l and l[i+1] == ':':
+                                i += 2
+                                x.time += int(l[i])
+                        elif len_li <= 2:
+                            # -[0]3
+                            x.time = (int(l[i][:2])*3600)
+                        else:
+                            return None
+                        i += 1
+
+                    assert i == len_l or l[i] == ','
+
+                    i += 1
+
+                assert i >= len_l
+
+        except (IndexError, ValueError, AssertionError):
+            return None
+        
+        return res
+
+
+DEFAULTTZPARSER = _tzparser()
+def _parsetz(tzstr):
+    return DEFAULTTZPARSER.parse(tzstr)
+
+
+def _parsems(value):
+    """Parse a I[.F] seconds value into (seconds, microseconds)."""
+    if "." not in value:
+        return int(value), 0
+    else:
+        i, f = value.split(".")
+        return int(i), int(f.ljust(6, "0")[:6])
+
+
+# vim:ts=4:sw=4:et

dateutil/relativedelta.py

+"""
+Copyright (c) 2003-2010  Gustavo Niemeyer <gustavo@niemeyer.net>
+
+This module offers extensions to the standard python 2.3+
+datetime module.
+"""
+__author__ = "Gustavo Niemeyer <gustavo@niemeyer.net>"
+__license__ = "PSF License"
+
+import datetime
+import calendar
+
+__all__ = ["relativedelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"]
+
+class weekday(object):
+    __slots__ = ["weekday", "n"]
+
+    def __init__(self, weekday, n=None):
+        self.weekday = weekday
+        self.n = n
+
+    def __call__(self, n):
+        if n == self.n:
+            return self
+        else:
+            return self.__class__(self.weekday, n)
+
+    def __eq__(self, other):
+        try:
+            if self.weekday != other.weekday or self.n != other.n:
+                return False
+        except AttributeError:
+            return False
+        return True
+
+    def __repr__(self):
+        s = ("MO", "TU", "WE", "TH", "FR", "SA", "SU")[self.weekday]
+        if not self.n:
+            return s
+        else:
+            return "%s(%+d)" % (s, self.n)
+
+MO, TU, WE, TH, FR, SA, SU = weekdays = tuple([weekday(x) for x in range(7)])
+
+class relativedelta:
+    """
+The relativedelta type is based on the specification of the excelent
+work done by M.-A. Lemburg in his mx.DateTime extension. However,
+notice that this type does *NOT* implement the same algorithm as
+his work. Do *NOT* expect it to behave like mx.DateTime's counterpart.
+
+There's two different ways to build a relativedelta instance. The
+first one is passing it two date/datetime classes:
+
+    relativedelta(datetime1, datetime2)
+
+And the other way is to use the following keyword arguments:
+
+    year, month, day, hour, minute, second, microsecond:
+        Absolute information.
+
+    years, months, weeks, days, hours, minutes, seconds, microseconds:
+        Relative information, may be negative.
+
+    weekday:
+        One of the weekday instances (MO, TU, etc). These instances may
+        receive a parameter N, specifying the Nth weekday, which could
+        be positive or negative (like MO(+1) or MO(-2). Not specifying
+        it is the same as specifying +1. You can also use an integer,
+        where 0=MO.
+
+    leapdays:
+        Will add given days to the date found, if year is a leap
+        year, and the date found is post 28 of february.
+
+    yearday, nlyearday:
+        Set the yearday or the non-leap year day (jump leap days).
+        These are converted to day/month/leapdays information.
+
+Here is the behavior of operations with relativedelta:
+
+1) Calculate the absolute year, using the 'year' argument, or the
+   original datetime year, if the argument is not present.
+
+2) Add the relative 'years' argument to the absolute year.
+
+3) Do steps 1 and 2 for month/months.
+
+4) Calculate the absolute day, using the 'day' argument, or the
+   original datetime day, if the argument is not present. Then,
+   subtract from the day until it fits in the year and month
+   found after their operations.
+
+5) Add the relative 'days' argument to the absolute day. Notice
+   that the 'weeks' argument is multiplied by 7 and added to
+   'days'.
+
+6) Do steps 1 and 2 for hour/hours, minute/minutes, second/seconds,
+   microsecond/microseconds.
+
+7) If the 'weekday' argument is present, calculate the weekday,
+   with the given (wday, nth) tuple. wday is the index of the
+   weekday (0-6, 0=Mon), and nth is the number of weeks to add
+   forward or backward, depending on its signal. Notice that if
+   the calculated date is already Monday, for example, using
+   (0, 1) or (0, -1) won't change the day.
+    """
+
+    def __init__(self, dt1=None, dt2=None,
+                 years=0, months=0, days=0, leapdays=0, weeks=0,
+                 hours=0, minutes=0, seconds=0, microseconds=0,
+                 year=None, month=None, day=None, weekday=None,
+                 yearday=None, nlyearday=None,
+                 hour=None, minute=None, second=None, microsecond=None):
+        if dt1 and dt2:
+            if not isinstance(dt1, datetime.date) or \
+               not isinstance(dt2, datetime.date):
+                raise TypeError, "relativedelta only diffs datetime/date"
+            if type(dt1) is not type(dt2):
+                if not isinstance(dt1, datetime.datetime):
+                    dt1 = datetime.datetime.fromordinal(dt1.toordinal())
+                elif not isinstance(dt2, datetime.datetime):
+                    dt2 = datetime.datetime.fromordinal(dt2.toordinal())
+            self.years = 0
+            self.months = 0
+            self.days = 0
+            self.leapdays = 0
+            self.hours = 0
+            self.minutes = 0
+            self.seconds = 0
+            self.microseconds = 0
+            self.year = None
+            self.month = None
+            self.day = None
+            self.weekday = None
+            self.hour = None
+            self.minute = None
+            self.second = None
+            self.microsecond = None
+            self._has_time = 0
+
+            months = (dt1.year*12+dt1.month)-(dt2.year*12+dt2.month)
+            self._set_months(months)
+            dtm = self.__radd__(dt2)
+            if dt1 < dt2:
+                while dt1 > dtm:
+                    months += 1
+                    self._set_months(months)
+                    dtm = self.__radd__(dt2)
+            else:
+                while dt1 < dtm:
+                    months -= 1
+                    self._set_months(months)
+                    dtm = self.__radd__(dt2)
+            delta = dt1 - dtm
+            self.seconds = delta.seconds+delta.days*86400
+            self.microseconds = delta.microseconds
+        else:
+            self.years = years
+            self.months = months
+            self.days = days+weeks*7
+            self.leapdays = leapdays
+            self.hours = hours
+            self.minutes = minutes
+            self.seconds = seconds
+            self.microseconds = microseconds
+            self.year = year
+            self.month = month
+            self.day = day
+            self.hour = hour
+            self.minute = minute
+            self.second = second
+            self.microsecond = microsecond
+
+            if type(weekday) is int:
+                self.weekday = weekdays[weekday]
+            else:
+                self.weekday = weekday
+
+            yday = 0
+            if nlyearday:
+                yday = nlyearday
+            elif yearday:
+                yday = yearday
+                if yearday > 59:
+                    self.leapdays = -1
+            if yday:
+                ydayidx = [31,59,90,120,151,181,212,243,273,304,334,366]
+                for idx, ydays in enumerate(ydayidx):
+                    if yday <= ydays:
+                        self.month = idx+1
+                        if idx == 0:
+                            self.day = yday
+                        else:
+                            self.day = yday-ydayidx[idx-1]
+                        break
+                else:
+                    raise ValueError, "invalid year day (%d)" % yday
+
+        self._fix()
+
+    def _fix(self):
+        if abs(self.microseconds) > 999999:
+            s = self.microseconds//abs(self.microseconds)
+            div, mod = divmod(self.microseconds*s, 1000000)
+            self.microseconds = mod*s
+            self.seconds += div*s
+        if abs(self.seconds) > 59:
+            s = self.seconds//abs(self.seconds)
+            div, mod = divmod(self.seconds*s, 60)
+            self.seconds = mod*s
+            self.minutes += div*s
+        if abs(self.minutes) > 59:
+            s = self.minutes//abs(self.minutes)
+            div, mod = divmod(self.minutes*s, 60)
+            self.minutes = mod*s
+            self.hours += div*s
+        if abs(self.hours) > 23:
+            s = self.hours//abs(self.hours)
+            div, mod = divmod(self.hours*s, 24)
+            self.hours = mod*s
+            self.days += div*s
+        if abs(self.months) > 11:
+            s = self.months//abs(self.months)
+            div, mod = divmod(self.months*s, 12)
+            self.months = mod*s
+            self.years += div*s
+        if (self.hours or self.minutes or self.seconds or self.microseconds or
+            self.hour is not None or self.minute is not None or
+            self.second is not None or self.microsecond is not None):
+            self._has_time = 1
+        else:
+            self._has_time = 0
+
+    def _set_months(self, months):
+        self.months = months
+        if abs(self.months) > 11:
+            s = self.months//abs(self.months)
+            div, mod = divmod(self.months*s, 12)
+            self.months = mod*s
+            self.years = div*s
+        else:
+            self.years = 0
+
+    def __radd__(self, other):
+        if not isinstance(other, datetime.date):
+            raise TypeError, "unsupported type for add operation"
+        elif self._has_time and not isinstance(other, datetime.datetime):
+            other = datetime.datetime.fromordinal(other.toordinal())
+        year = (self.year or other.year)+self.years
+        month = self.month or other.month
+        if self.months:
+            assert 1 <= abs(self.months) <= 12
+            month += self.months
+            if month > 12:
+                year += 1
+                month -= 12
+            elif month < 1:
+                year -= 1
+                month += 12
+        day = min(calendar.monthrange(year, month)[1],
+                  self.day or other.day)
+        repl = {"year": year, "month": month, "day": day}
+        for attr in ["hour", "minute", "second", "microsecond"]:
+            value = getattr(self, attr)
+            if value is not None:
+                repl[attr] = value
+        days = self.days
+        if self.leapdays and month > 2 and calendar.isleap(year):
+            days += self.leapdays
+        ret = (other.replace(**repl)
+               + datetime.timedelta(days=days,
+                                    hours=self.hours,
+                                    minutes=self.minutes,
+                                    seconds=self.seconds,
+                                    microseconds=self.microseconds))
+        if self.weekday:
+            weekday, nth = self.weekday.weekday, self.weekday.n or 1
+            jumpdays = (abs(nth)-1)*7
+            if nth > 0:
+                jumpdays += (7-ret.weekday()+weekday)%7
+            else:
+                jumpdays += (ret.weekday()-weekday)%7
+                jumpdays *= -1
+            ret += datetime.timedelta(days=jumpdays)
+        return ret
+
+    def __rsub__(self, other):
+        return self.__neg__().__radd__(other)
+
+    def __add__(self, other):
+        if not isinstance(other, relativedelta):
+            raise TypeError, "unsupported type for add operation"
+        return relativedelta(years=other.years+self.years,
+                             months=other.months+self.months,
+                             days=other.days+self.days,
+                             hours=other.hours+self.hours,
+                             minutes=other.minutes+self.minutes,
+                             seconds=other.seconds+self.seconds,
+                             microseconds=other.microseconds+self.microseconds,
+                             leapdays=other.leapdays or self.leapdays,
+                             year=other.year or self.year,
+                             month=other.month or self.month,
+                             day=other.day or self.day,
+                             weekday=other.weekday or self.weekday,
+                             hour=other.hour or self.hour,
+                             minute=other.minute or self.minute,
+                             second=other.second or self.second,
+                             microsecond=other.second or self.microsecond)
+
+    def __sub__(self, other):
+        if not isinstance(other, relativedelta):
+            raise TypeError, "unsupported type for sub operation"
+        return relativedelta(years=other.years-self.years,
+                             months=other.months-self.months,
+                             days=other.days-self.days,
+                             hours=other.hours-self.hours,
+                             minutes=other.minutes-self.minutes,
+                             seconds=other.seconds-self.seconds,
+                             microseconds=other.microseconds-self.microseconds,
+                             leapdays=other.leapdays or self.leapdays,
+                             year=other.year or self.year,
+                             month=other.month or self.month,
+                             day=other.day or self.day,
+                             weekday=other.weekday or self.weekday,
+                             hour=other.hour or self.hour,
+                             minute=other.minute or self.minute,
+                             second=other.second or self.second,
+                             microsecond=other.second or self.microsecond)
+
+    def __neg__(self):
+        return relativedelta(years=-self.years,
+                             months=-self.months,
+                             days=-self.days,
+                             hours=-self.hours,
+                             minutes=-self.minutes,
+                             seconds=-self.seconds,
+                             microseconds=-self.microseconds,
+                             leapdays=self.leapdays,
+                             year=self.year,
+                             month=self.month,
+                             day=self.day,
+                             weekday=self.weekday,
+                             hour=self.hour,
+                             minute=self.minute,
+                             second=self.second,
+                             microsecond=self.microsecond)
+
+    def __nonzero__(self):
+        return not (not self.years and
+                    not self.months and
+                    not self.days and
+                    not self.hours and
+                    not self.minutes and
+                    not self.seconds and
+                    not self.microseconds and
+                    not self.leapdays and
+                    self.year is None and
+                    self.month is None and
+                    self.day is None and
+                    self.weekday is None and
+                    self.hour is None and
+                    self.minute is None and
+                    self.second is None and
+                    self.microsecond is None)
+
+    def __mul__(self, other):
+        f = float(other)
+        return relativedelta(years=self.years*f,
+                             months=self.months*f,
+                             days=self.days*f,
+                             hours=self.hours*f,
+                             minutes=self.minutes*f,
+                             seconds=self.seconds*f,
+                             microseconds=self.microseconds*f,
+                             leapdays=self.leapdays,
+                             year=self.year,
+                             month=self.month,
+                             day=self.day,
+                             weekday=self.weekday,
+                             hour=self.hour,
+                             minute=self.minute,
+                             second=self.second,
+                             microsecond=self.microsecond)
+
+    def __eq__(self, other):
+        if not isinstance(other, relativedelta):
+            return False
+        if self.weekday or other.weekday:
+            if not self.weekday or not other.weekday:
+                return False
+            if self.weekday.weekday != other.weekday.weekday:
+                return False
+            n1, n2 = self.weekday.n, other.weekday.n
+            if n1 != n2 and not ((not n1 or n1 == 1) and (not n2 or n2 == 1)):
+                return False
+        return (self.years == other.years and
+                self.months == other.months and
+                self.days == other.days and
+                self.hours == other.hours and
+                self.minutes == other.minutes and
+                self.seconds == other.seconds and
+                self.leapdays == other.leapdays and
+                self.year == other.year and
+                self.month == other.month and
+                self.day == other.day and
+                self.hour == other.hour and
+                self.minute == other.minute and
+                self.second == other.second and
+                self.microsecond == other.microsecond)
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    def __div__(self, other):
+        return self.__mul__(1/float(other))
+
+    def __repr__(self):
+        l = []
+        for attr in ["years", "months", "days", "leapdays",
+                     "hours", "minutes", "seconds", "microseconds"]:
+            value = getattr(self, attr)
+            if value:
+                l.append("%s=%+d" % (attr, value))
+        for attr in ["year", "month", "day", "weekday",
+                     "hour", "minute", "second", "microsecond"]:
+            value = getattr(self, attr)
+            if value is not None:
+                l.append("%s=%s" % (attr, `value`))
+        return "%s(%s)" % (self.__class__.__name__, ", ".join(l))
+
+# vim:ts=4:sw=4:et

dateutil/rrule.py

+"""
+Copyright (c) 2003-2010  Gustavo Niemeyer <gustavo@niemeyer.net>
+
+This module offers extensions to the standard python 2.3+
+datetime module.
+"""
+__author__ = "Gustavo Niemeyer <gustavo@niemeyer.net>"
+__license__ = "PSF License"
+
+import itertools
+import datetime
+import calendar
+import thread
+import sys
+
+__all__ = ["rrule", "rruleset", "rrulestr",
+           "YEARLY", "MONTHLY", "WEEKLY", "DAILY",
+           "HOURLY", "MINUTELY", "SECONDLY",
+           "MO", "TU", "WE", "TH", "FR", "SA", "SU"]
+
+# Every mask is 7 days longer to handle cross-year weekly periods.
+M366MASK = tuple([1]*31+[2]*29+[3]*31+[4]*30+[5]*31+[6]*30+
+                 [7]*31+[8]*31+[9]*30+[10]*31+[11]*30+[12]*31+[1]*7)
+M365MASK = list(M366MASK)
+M29, M30, M31 = range(1,30), range(1,31), range(1,32)
+MDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7])
+MDAY365MASK = list(MDAY366MASK)
+M29, M30, M31 = range(-29,0), range(-30,0), range(-31,0)
+NMDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7])
+NMDAY365MASK = list(NMDAY366MASK)
+M366RANGE = (0,31,60,91,121,152,182,213,244,274,305,335,366)
+M365RANGE = (0,31,59,90,120,151,181,212,243,273,304,334,365)
+WDAYMASK = [0,1,2,3,4,5,6]*55
+del M29, M30, M31, M365MASK[59], MDAY365MASK[59], NMDAY365MASK[31]
+MDAY365MASK = tuple(MDAY365MASK)
+M365MASK = tuple(M365MASK)
+
+(YEARLY,
+ MONTHLY,
+ WEEKLY,
+ DAILY,
+ HOURLY,
+ MINUTELY,
+ SECONDLY) = range(7)
+
+# Imported on demand.
+easter = None
+parser = None
+
+class weekday(object):
+    __slots__ = ["weekday", "n"]
+
+    def __init__(self, weekday, n=None):
+        if n == 0:
+            raise ValueError, "Can't create weekday with n == 0"
+        self.weekday = weekday
+        self.n = n
+
+    def __call__(self, n):
+        if n == self.n:
+            return self
+        else:
+            return self.__class__(self.weekday, n)
+
+    def __eq__(self, other):
+        try:
+            if self.weekday != other.weekday or self.n != other.n:
+                return False
+        except AttributeError:
+            return False
+        return True
+
+    def __repr__(self):
+        s = ("MO", "TU", "WE", "TH", "FR", "SA", "SU")[self.weekday]
+        if not self.n:
+            return s
+        else:
+            return "%s(%+d)" % (s, self.n)
+
+MO, TU, WE, TH, FR, SA, SU = weekdays = tuple([weekday(x) for x in range(7)])
+
+class rrulebase:
+    def __init__(self, cache=False):
+        if cache:
+            self._cache = []
+            self._cache_lock = thread.allocate_lock()
+            self._cache_gen  = self._iter()
+            self._cache_complete = False
+        else:
+            self._cache = None
+            self._cache_complete = False
+        self._len = None
+
+    def __iter__(self):
+        if self._cache_complete:
+            return iter(self._cache)
+        elif self._cache is None:
+            return self._iter()
+        else:
+            return self._iter_cached()
+
+    def _iter_cached(self):
+        i = 0
+        gen = self._cache_gen
+        cache = self._cache
+        acquire = self._cache_lock.acquire
+        release = self._cache_lock.release
+        while gen:
+            if i == len(cache):
+                acquire()
+                if self._cache_complete:
+                    break
+                try:
+                    for j in range(10):
+                        cache.append(gen.next())
+                except StopIteration:
+                    self._cache_gen = gen = None
+                    self._cache_complete = True
+                    break
+                release()
+            yield cache[i]
+            i += 1
+        while i < self._len:
+            yield cache[i]
+            i += 1
+
+    def __getitem__(self, item):
+        if self._cache_complete:
+            return self._cache[item]
+        elif isinstance(item, slice):
+            if item.step and item.step < 0:
+                return list(iter(self))[item]
+            else:
+                return list(itertools.islice(self,
+                                             item.start or 0,
+                                             item.stop or sys.maxint,
+                                             item.step or 1))
+        elif item >= 0:
+            gen = iter(self)
+            try:
+                for i in range(item+1):
+                    res = gen.next()
+            except StopIteration:
+                raise IndexError
+            return res
+        else:
+            return list(iter(self))[item]
+
+    def __contains__(self, item):
+        if self._cache_complete:
+            return item in self._cache
+        else:
+            for i in self:
+                if i == item:
+                    return True
+                elif i > item:
+                    return False
+        return False
+
+    # __len__() introduces a large performance penality.
+    def count(self):
+        if self._len is None:
+            for x in self: pass
+        return self._len
+
+    def before(self, dt, inc=False):
+        if self._cache_complete:
+            gen = self._cache
+        else:
+            gen = self
+        last = None
+        if inc:
+            for i in gen:
+                if i > dt:
+                    break
+                last = i
+        else:
+            for i in gen:
+                if i >= dt:
+                    break
+                last = i
+        return last
+
+    def after(self, dt, inc=False):
+        if self._cache_complete:
+            gen = self._cache
+        else:
+            gen = self
+        if inc:
+            for i in gen:
+                if i >= dt:
+                    return i
+        else:
+            for i in gen:
+                if i > dt:
+                    return i
+        return None
+
+    def between(self, after, before, inc=False):
+        if self._cache_complete:
+            gen = self._cache
+        else:
+            gen = self
+        started = False
+        l = []
+        if inc:
+            for i in gen:
+                if i > before:
+                    break
+                elif not started:
+                    if i >= after:
+                        started = True
+                        l.append(i)
+                else:
+                    l.append(i)
+        else:
+            for i in gen:
+                if i >= before:
+                    break
+                elif not started:
+                    if i > after:
+                        started = True
+                        l.append(i)
+                else:
+                    l.append(i)
+        return l
+
+class rrule(rrulebase):
+    def __init__(self, freq, dtstart=None,
+                 interval=1, wkst=None, count=None, until=None, bysetpos=None,
+                 bymonth=None, bymonthday=None, byyearday=None, byeaster=None,
+                 byweekno=None, byweekday=None,
+                 byhour=None, byminute=None, bysecond=None,
+                 cache=False):
+        rrulebase.__init__(self, cache)
+        global easter
+        if not dtstart:
+            dtstart = datetime.datetime.now().replace(microsecond=0)
+        elif not isinstance(dtstart, datetime.datetime):
+            dtstart = datetime.datetime.fromordinal(dtstart.toordinal())
+        else:
+            dtstart = dtstart.replace(microsecond=0)
+        self._dtstart = dtstart
+        self._tzinfo = dtstart.tzinfo
+        self._freq = freq
+        self._interval = interval
+        self._count = count
+        if until and not isinstance(until, datetime.datetime):
+            until = datetime.datetime.fromordinal(until.toordinal())
+        self._until = until
+        if wkst is None:
+            self._wkst = calendar.firstweekday()
+        elif type(wkst) is int:
+            self._wkst = wkst
+        else:
+            self._wkst = wkst.weekday
+        if bysetpos is None:
+            self._bysetpos = None
+        elif type(bysetpos) is int:
+            if bysetpos == 0 or not (-366 <= bysetpos <= 366):
+                raise ValueError("bysetpos must be between 1 and 366, "
+                                 "or between -366 and -1")
+            self._bysetpos = (bysetpos,)
+        else:
+            self._bysetpos = tuple(bysetpos)
+            for pos in self._bysetpos:
+                if pos == 0 or not (-366 <= pos <= 366):
+                    raise ValueError("bysetpos must be between 1 and 366, "
+                                     "or between -366 and -1")
+        if not (byweekno or byyearday or bymonthday or
+                byweekday is not None or byeaster is not None):
+            if freq == YEARLY:
+                if not bymonth:
+                    bymonth = dtstart.month
+                bymonthday = dtstart.day
+            elif freq == MONTHLY:
+                bymonthday = dtstart.day
+            elif freq == WEEKLY:
+                byweekday = dtstart.weekday()
+        # bymonth
+        if not bymonth:
+            self._bymonth = None
+        elif type(bymonth) is int:
+            self._bymonth = (bymonth,)
+        else:
+            self._bymonth = tuple(bymonth)
+        # byyearday
+        if not byyearday:
+            self._byyearday = None
+        elif type(byyearday) is int:
+            self._byyearday = (byyearday,)
+        else:
+            self._byyearday = tuple(byyearday)
+        # byeaster
+        if byeaster is not None:
+            if not easter:
+                from dateutil import easter
+            if type(byeaster) is int:
+                self._byeaster = (byeaster,)
+            else:
+                self._byeaster = tuple(byeaster)
+        else:
+            self._byeaster = None
+        # bymonthay
+        if not bymonthday:
+            self._bymonthday = ()
+            self._bynmonthday = ()
+        elif type(bymonthday) is int:
+            if bymonthday < 0:
+                self._bynmonthday = (bymonthday,)
+                self._bymonthday = ()
+            else:
+                self._bymonthday = (bymonthday,)
+                self._bynmonthday = ()
+        else:
+            self._bymonthday = tuple([x for x in bymonthday if x > 0])
+            self._bynmonthday = tuple([x for x in bymonthday if x < 0])
+        # byweekno
+        if byweekno is None:
+            self._byweekno = None
+        elif type(byweekno) is int:
+            self._byweekno = (byweekno,)
+        else:
+            self._byweekno = tuple(byweekno)
+        # byweekday / bynweekday
+        if byweekday is None:
+            self._byweekday = None
+            self._bynweekday = None
+        elif type(byweekday) is int:
+            self._byweekday = (byweekday,)
+            self._bynweekday = None
+        elif hasattr(byweekday, "n"):
+            if not byweekday.n or freq > MONTHLY:
+                self._byweekday = (byweekday.weekday,)
+                self._bynweekday = None
+            else:
+                self._bynweekday = ((byweekday.weekday, byweekday.n),)
+                self._byweekday = None
+        else:
+            self._byweekday = []
+            self._bynweekday = []
+            for wday in byweekday:
+                if type(wday) is int:
+                    self._byweekday.append(wday)
+                elif not wday.n or freq > MONTHLY:
+                    self._byweekday.append(wday.weekday)
+                else:
+                    self._bynweekday.append((wday.weekday, wday.n))
+            self._byweekday = tuple(self._byweekday)
+            self._bynweekday = tuple(self._bynweekday)
+            if not self._byweekday:
+                self._byweekday = None
+            elif not self._bynweekday:
+                self._bynweekday = None
+        # byhour
+        if byhour is None:
+            if freq < HOURLY:
+                self._byhour = (dtstart.hour,)
+            else:
+                self._byhour = None
+        elif type(byhour) is int:
+            self._byhour = (byhour,)
+        else:
+            self._byhour = tuple(byhour)
+        # byminute
+        if byminute is None:
+            if freq < MINUTELY:
+                self._byminute = (dtstart.minute,)
+            else:
+                self._byminute = None
+        elif type(byminute) is int:
+            self._byminute = (byminute,)
+        else:
+            self._byminute = tuple(byminute)
+        # bysecond
+        if bysecond is None:
+            if freq < SECONDLY:
+                self._bysecond = (dtstart.second,)
+            else:
+                self._bysecond = None
+        elif type(bysecond) is int:
+            self._bysecond = (bysecond,)
+        else:
+            self._bysecond = tuple(bysecond)
+
+        if self._freq >= HOURLY:
+            self._timeset = None
+        else:
+            self._timeset = []
+            for hour in self._byhour:
+                for minute in self._byminute:
+                    for second in self._bysecond:
+                        self._timeset.append(
+                                datetime.time(hour, minute, second,
+                                                    tzinfo=self._tzinfo))
+            self._timeset.sort()
+            self._timeset = tuple(self._timeset)
+
+    def _iter(self):
+        year, month, day, hour, minute, second, weekday, yearday, _ = \
+            self._dtstart.timetuple()
+
+        # Some local variables to speed things up a bit
+        freq = self._freq
+        interval = self._interval
+        wkst = self._wkst
+        until = self._until
+        bymonth = self._bymonth
+        byweekno = self._byweekno
+        byyearday = self._byyearday
+        byweekday = self._byweekday
+        byeaster = self._byeaster
+        bymonthday = self._bymonthday
+        bynmonthday = self._bynmonthday
+        bysetpos = self._bysetpos
+        byhour = self._byhour
+        byminute = self._byminute
+        bysecond = self._bysecond
+
+        ii = _iterinfo(self)
+        ii.rebuild(year, month)
+
+        getdayset = {YEARLY:ii.ydayset,
+                     MONTHLY:ii.mdayset,
+                     WEEKLY:ii.wdayset,
+                     DAILY:ii.ddayset,
+                     HOURLY:ii.ddayset,
+                     MINUTELY:ii.ddayset,
+                     SECONDLY:ii.ddayset}[freq]
+        
+        if freq < HOURLY:
+            timeset = self._timeset
+        else:
+            gettimeset = {HOURLY:ii.htimeset,
+                          MINUTELY:ii.mtimeset,
+                          SECONDLY:ii.stimeset}[freq]
+            if ((freq >= HOURLY and
+                 self._byhour and hour not in self._byhour) or
+                (freq >= MINUTELY and
+                 self._byminute and minute not in self._byminute) or
+                (freq >= SECONDLY and
+                 self._bysecond and second not in self._bysecond)):
+                timeset = ()
+            else:
+                timeset = gettimeset(hour, minute, second)
+
+        total = 0
+        count = self._count
+        while True:
+            # Get dayset with the right frequency
+            dayset, start, end = getdayset(year, month, day)
+
+            # Do the "hard" work ;-)
+            filtered = False
+            for i in dayset[start:end]:
+                if ((bymonth and ii.mmask[i] not in bymonth) or
+                    (byweekno and not ii.wnomask[i]) or
+                    (byweekday and ii.wdaymask[i] not in byweekday) or
+                    (ii.nwdaymask and not ii.nwdaymask[i]) or
+                    (byeaster and not ii.eastermask[i]) or
+                    ((bymonthday or bynmonthday) and
+                     ii.mdaymask[i] not in bymonthday and
+                     ii.nmdaymask[i] not in bynmonthday) or
+                    (byyearday and
+                     ((i < ii.yearlen and i+1 not in byyearday
+                                      and -ii.yearlen+i not in byyearday) or
+                      (i >= ii.yearlen and i+1-ii.yearlen not in byyearday
+                                       and -ii.nextyearlen+i-ii.yearlen
+                                           not in byyearday)))):
+                    dayset[i] = None
+                    filtered = True
+
+            # Output results
+            if bysetpos and timeset:
+                poslist = []
+                for pos in bysetpos:
+                    if pos < 0:
+                        daypos, timepos = divmod(pos, len(timeset))
+                    else:
+                        daypos, timepos = divmod(pos-1, len(timeset))
+                    try:
+                        i = [x for x in dayset[start:end]
+                                if x is not None][daypos]
+                        time = timeset[timepos]
+                    except IndexError:
+                        pass
+                    else:
+                        date = datetime.date.fromordinal(ii.yearordinal+i)
+                        res = datetime.datetime.combine(date, time)
+                        if res not in poslist:
+                            poslist.append(res)
+                poslist.sort()
+                for res in poslist:
+                    if until and res > until:
+                        self._len = total
+                        return
+                    elif res >= self._dtstart:
+                        total += 1
+                        yield res
+                        if count:
+                            count -= 1
+                            if not count:
+                                self._len = total
+                                return
+            else:
+                for i in dayset[start:end]:
+                    if i is not None:
+                        date = datetime.date.fromordinal(ii.yearordinal+i)
+                        for time in timeset:
+                            res = datetime.datetime.combine(date, time)
+                            if until and res > until:
+                                self._len = total
+                                return
+                            elif res >= self._dtstart:
+                                total += 1
+                                yield res
+                                if count:
+                                    count -= 1
+                                    if not count:
+                                        self._len = total
+                                        return
+
+            # Handle frequency and interval
+            fixday = False
+            if freq == YEARLY:
+                year += interval
+                if year > datetime.MAXYEAR:
+                    self._len = total
+                    return
+                ii.rebuild(year, month)
+            elif freq == MONTHLY:
+                month += interval
+                if month > 12:
+                    div, mod = divmod(month, 12)
+                    month = mod
+                    year += div
+                    if month == 0:
+                        month = 12
+                        year -= 1
+                    if year > datetime.MAXYEAR:
+                        self._len = total
+                        return
+                ii.rebuild(year, month)
+            elif freq == WEEKLY:
+                if wkst > weekday:
+                    day += -(weekday+1+(6-wkst))+self._interval*7
+                else:
+                    day += -(weekday-wkst)+self._interval*7
+                weekday = wkst
+                fixday = True
+            elif freq == DAILY:
+                day += interval
+                fixday = True
+            elif freq == HOURLY:
+                if filtered:
+                    # Jump to one iteration before next day
+                    hour += ((23-hour)//interval)*interval
+                while True:
+                    hour += interval
+                    div, mod = divmod(hour, 24)
+                    if div:
+                        hour = mod
+                        day += div
+                        fixday = True
+                    if not byhour or hour in byhour:
+                        break
+                timeset = gettimeset(hour, minute, second)
+            elif freq == MINUTELY:
+                if filtered:
+                    # Jump to one iteration before next day
+                    minute += ((1439-(hour*60+minute))//interval)*interval
+                while True:
+                    minute += interval
+                    div, mod = divmod(minute, 60)
+                    if div:
+                        minute = mod
+                        hour += div
+                        div, mod = divmod(hour, 24)
+                        if div:
+                            hour = mod
+                            day += div
+                            fixday = True
+                            filtered = False
+                    if ((not byhour or hour in byhour) and
+                        (not byminute or minute in byminute)):
+                        break
+                timeset = gettimeset(hour, minute, second)
+            elif freq == SECONDLY:
+                if filtered:
+                    # Jump to one iteration before next day
+                    second += (((86399-(hour*3600+minute*60+second))
+                                //interval)*interval)
+                while True:
+                    second += self._interval
+                    div, mod = divmod(second, 60)
+                    if div:
+                        second = mod
+                        minute += div
+                        div, mod = divmod(minute, 60)
+                        if div:
+                            minute = mod
+                            hour += div
+                            div, mod = divmod(hour, 24)
+                            if div:
+                                hour = mod
+                                day += div
+                                fixday = True
+                    if ((not byhour or hour in byhour) and
+                        (not byminute or minute in byminute) and
+                        (not bysecond or second in bysecond)):
+                        break
+                timeset = gettimeset(hour, minute, second)
+
+            if fixday and day > 28:
+                daysinmonth = calendar.monthrange(year, month)[1]
+                if day > daysinmonth:
+                    while day > daysinmonth:
+                        day -= daysinmonth
+                        month += 1