From 47b8d842b9fb241b33e87c80e6ff47d160f91f6b Mon Sep 17 00:00:00 2001 From: Florian Wesch Date: Sun, 20 Jul 2014 15:50:45 +0200 Subject: [PATCH] initial commit --- COPYRIGHT | 19 + MANIFEST | 24 + README.creole | 95 ++++ dateutil/__init__.py | 9 + dateutil/parser.py | 886 ++++++++++++++++++++++++++++++ dateutil/relativedelta.py | 432 +++++++++++++++ dateutil/rrule.py | 1097 +++++++++++++++++++++++++++++++++++++ dateutil/tz.py | 951 ++++++++++++++++++++++++++++++++ defusedxml/ElementTree.py | 62 +++ defusedxml/__init__.py | 0 defusedxml/common.py | 163 ++++++ empty.png | Bin 0 -> 105 bytes hosted.lua | 198 +++++++ hosted.py | 213 +++++++ importer.py | 73 +++ node.json | 157 ++++++ node.lua | 410 ++++++++++++++ package.json | 5 + package.png | Bin 0 -> 2097 bytes progress.frag | 19 + screenshot.jpg | Bin 0 -> 18255 bytes service | 76 +++ silkscreen.ttf | Bin 0 -> 18336 bytes utils.py | 36 ++ 24 files changed, 4925 insertions(+) create mode 100644 COPYRIGHT create mode 100644 MANIFEST create mode 100644 README.creole create mode 100644 dateutil/__init__.py create mode 100644 dateutil/parser.py create mode 100644 dateutil/relativedelta.py create mode 100644 dateutil/rrule.py create mode 100644 dateutil/tz.py create mode 100644 defusedxml/ElementTree.py create mode 100644 defusedxml/__init__.py create mode 100644 defusedxml/common.py create mode 100644 empty.png create mode 100644 hosted.lua create mode 100644 hosted.py create mode 100644 importer.py create mode 100644 node.json create mode 100644 node.lua create mode 100644 package.json create mode 100644 package.png create mode 100644 progress.frag create mode 100644 screenshot.jpg create mode 100755 service create mode 100644 silkscreen.ttf create mode 100644 utils.py diff --git a/COPYRIGHT b/COPYRIGHT new file mode 100644 index 0000000..a60ed93 --- /dev/null +++ b/COPYRIGHT @@ -0,0 +1,19 @@ +Copyright (C) 2014 Florian Wesch + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/MANIFEST b/MANIFEST new file mode 100644 index 0000000..878f2d3 --- /dev/null +++ b/MANIFEST @@ -0,0 +1,24 @@ +COPYRIGHT +dateutil/__init__.py +dateutil/parser.py +dateutil/relativedelta.py +dateutil/rrule.py +dateutil/tz.py +defusedxml/common.py +defusedxml/ElementTree.py +defusedxml/__init__.py +empty.png +hosted.lua +hosted.py +importer.py +MANIFEST +node.json +node.lua +package.json +package.png +progress.frag +README.creole +screenshot.jpg +service +silkscreen.ttf +utils.py diff --git a/README.creole b/README.creole new file mode 100644 index 0000000..dd7345a --- /dev/null +++ b/README.creole @@ -0,0 +1,95 @@ += Conference Room Information + +{{screenshot.jpg|Screenshot}} + += Description + +Displays information about the next talk, talks in other rooms and generic room information. +Scheduling information is imported from a https://frab.cccv.de/ export. + += Configuration + +== Logo (logo) + +Sets a logo. Size should be 280x100 pixel. Otherwise the image will be scaled to that size which +might lead to a reduced quality. + +== Other room display (other_rooms) + +How long to show information about talks in other rooms (in seconds) + +== Current room (current_room) + +How long to show information about the (upcoming) talk in the current room. +Default is 15 seconds. + +The current talk is displayed for the first 25 minutes of that talk. So in +case of a delayed start the screen will stil show useful information. + +After 25 minutes into the talk this screen will show information about the +next talk. + +== Room info (room_info) + +Shows information about the current room (irc channel, recommended hashtag). + +== Schedule URL (schedule_url) + +Url where the running devices should fetch the schedule from. The schedule +is expected to be in the frabs export format. This format looks something +like this: + +{{{ + + + EuroPython 2014 + EP2014 + 2014-07-21 + 2014-07-27 + 7 + 00:15 + + + + + ... + +}}} + +== Rooms (rooms) + +Here you can defined your rooms. You can use the same Setup for multiple +rooms. + +==== Room name (name) + +Sets the room name. This name must match the room name in the Schedule. + +=== Short Room name (name_short) + +Sets the display name of the room. You can set a shorter name for a room +since space on the display is limited. + +=== Device Serial (serial) + +Enter the serial number of the device that is responsible for showing +room information here. + +=== Dect (dect) + +Information about what number to dial to live audio feed. + +=== Translation (translation) + +Information about what number to dial to a live audio translation. + +=== Hashtag (hashtag) + +Information about the hashtag recommended for discussing the content +of the current room. + +=== IRC (irc) + +Information about the recommended IRC channel to talk about the content +of the current room. + diff --git a/dateutil/__init__.py b/dateutil/__init__.py new file mode 100644 index 0000000..290814c --- /dev/null +++ b/dateutil/__init__.py @@ -0,0 +1,9 @@ +""" +Copyright (c) 2003-2010 Gustavo Niemeyer + +This module offers extensions to the standard python 2.3+ +datetime module. +""" +__author__ = "Gustavo Niemeyer " +__license__ = "PSF License" +__version__ = "1.5" diff --git a/dateutil/parser.py b/dateutil/parser.py new file mode 100644 index 0000000..5d824e4 --- /dev/null +++ b/dateutil/parser.py @@ -0,0 +1,886 @@ +# -*- coding:iso-8859-1 -*- +""" +Copyright (c) 2003-2007 Gustavo Niemeyer + +This module offers extensions to the standard python 2.3+ +datetime module. +""" +__author__ = "Gustavo Niemeyer " +__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 diff --git a/dateutil/relativedelta.py b/dateutil/relativedelta.py new file mode 100644 index 0000000..0c72a81 --- /dev/null +++ b/dateutil/relativedelta.py @@ -0,0 +1,432 @@ +""" +Copyright (c) 2003-2010 Gustavo Niemeyer + +This module offers extensions to the standard python 2.3+ +datetime module. +""" +__author__ = "Gustavo Niemeyer " +__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 diff --git a/dateutil/rrule.py b/dateutil/rrule.py new file mode 100644 index 0000000..6bd83ca --- /dev/null +++ b/dateutil/rrule.py @@ -0,0 +1,1097 @@ +""" +Copyright (c) 2003-2010 Gustavo Niemeyer + +This module offers extensions to the standard python 2.3+ +datetime module. +""" +__author__ = "Gustavo Niemeyer " +__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 + if month == 13: + month = 1 + year += 1 + if year > datetime.MAXYEAR: + self._len = total + return + daysinmonth = calendar.monthrange(year, month)[1] + ii.rebuild(year, month) + +class _iterinfo(object): + __slots__ = ["rrule", "lastyear", "lastmonth", + "yearlen", "nextyearlen", "yearordinal", "yearweekday", + "mmask", "mrange", "mdaymask", "nmdaymask", + "wdaymask", "wnomask", "nwdaymask", "eastermask"] + + def __init__(self, rrule): + for attr in self.__slots__: + setattr(self, attr, None) + self.rrule = rrule + + def rebuild(self, year, month): + # Every mask is 7 days longer to handle cross-year weekly periods. + rr = self.rrule + if year != self.lastyear: + self.yearlen = 365+calendar.isleap(year) + self.nextyearlen = 365+calendar.isleap(year+1) + firstyday = datetime.date(year, 1, 1) + self.yearordinal = firstyday.toordinal() + self.yearweekday = firstyday.weekday() + + wday = datetime.date(year, 1, 1).weekday() + if self.yearlen == 365: + self.mmask = M365MASK + self.mdaymask = MDAY365MASK + self.nmdaymask = NMDAY365MASK + self.wdaymask = WDAYMASK[wday:] + self.mrange = M365RANGE + else: + self.mmask = M366MASK + self.mdaymask = MDAY366MASK + self.nmdaymask = NMDAY366MASK + self.wdaymask = WDAYMASK[wday:] + self.mrange = M366RANGE + + if not rr._byweekno: + self.wnomask = None + else: + self.wnomask = [0]*(self.yearlen+7) + #no1wkst = firstwkst = self.wdaymask.index(rr._wkst) + no1wkst = firstwkst = (7-self.yearweekday+rr._wkst)%7 + if no1wkst >= 4: + no1wkst = 0 + # Number of days in the year, plus the days we got + # from last year. + wyearlen = self.yearlen+(self.yearweekday-rr._wkst)%7 + else: + # Number of days in the year, minus the days we + # left in last year. + wyearlen = self.yearlen-no1wkst + div, mod = divmod(wyearlen, 7) + numweeks = div+mod//4 + for n in rr._byweekno: + if n < 0: + n += numweeks+1 + if not (0 < n <= numweeks): + continue + if n > 1: + i = no1wkst+(n-1)*7 + if no1wkst != firstwkst: + i -= 7-firstwkst + else: + i = no1wkst + for j in range(7): + self.wnomask[i] = 1 + i += 1 + if self.wdaymask[i] == rr._wkst: + break + if 1 in rr._byweekno: + # Check week number 1 of next year as well + # TODO: Check -numweeks for next year. + i = no1wkst+numweeks*7 + if no1wkst != firstwkst: + i -= 7-firstwkst + if i < self.yearlen: + # If week starts in next year, we + # don't care about it. + for j in range(7): + self.wnomask[i] = 1 + i += 1 + if self.wdaymask[i] == rr._wkst: + break + if no1wkst: + # Check last week number of last year as + # well. If no1wkst is 0, either the year + # started on week start, or week number 1 + # got days from last year, so there are no + # days from last year's last week number in + # this year. + if -1 not in rr._byweekno: + lyearweekday = datetime.date(year-1,1,1).weekday() + lno1wkst = (7-lyearweekday+rr._wkst)%7 + lyearlen = 365+calendar.isleap(year-1) + if lno1wkst >= 4: + lno1wkst = 0 + lnumweeks = 52+(lyearlen+ + (lyearweekday-rr._wkst)%7)%7//4 + else: + lnumweeks = 52+(self.yearlen-no1wkst)%7//4 + else: + lnumweeks = -1 + if lnumweeks in rr._byweekno: + for i in range(no1wkst): + self.wnomask[i] = 1 + + if (rr._bynweekday and + (month != self.lastmonth or year != self.lastyear)): + ranges = [] + if rr._freq == YEARLY: + if rr._bymonth: + for month in rr._bymonth: + ranges.append(self.mrange[month-1:month+1]) + else: + ranges = [(0, self.yearlen)] + elif rr._freq == MONTHLY: + ranges = [self.mrange[month-1:month+1]] + if ranges: + # Weekly frequency won't get here, so we may not + # care about cross-year weekly periods. + self.nwdaymask = [0]*self.yearlen + for first, last in ranges: + last -= 1 + for wday, n in rr._bynweekday: + if n < 0: + i = last+(n+1)*7 + i -= (self.wdaymask[i]-wday)%7 + else: + i = first+(n-1)*7 + i += (7-self.wdaymask[i]+wday)%7 + if first <= i <= last: + self.nwdaymask[i] = 1 + + if rr._byeaster: + self.eastermask = [0]*(self.yearlen+7) + eyday = easter.easter(year).toordinal()-self.yearordinal + for offset in rr._byeaster: + self.eastermask[eyday+offset] = 1 + + self.lastyear = year + self.lastmonth = month + + def ydayset(self, year, month, day): + return range(self.yearlen), 0, self.yearlen + + def mdayset(self, year, month, day): + set = [None]*self.yearlen + start, end = self.mrange[month-1:month+1] + for i in range(start, end): + set[i] = i + return set, start, end + + def wdayset(self, year, month, day): + # We need to handle cross-year weeks here. + set = [None]*(self.yearlen+7) + i = datetime.date(year, month, day).toordinal()-self.yearordinal + start = i + for j in range(7): + set[i] = i + i += 1 + #if (not (0 <= i < self.yearlen) or + # self.wdaymask[i] == self.rrule._wkst): + # This will cross the year boundary, if necessary. + if self.wdaymask[i] == self.rrule._wkst: + break + return set, start, i + + def ddayset(self, year, month, day): + set = [None]*self.yearlen + i = datetime.date(year, month, day).toordinal()-self.yearordinal + set[i] = i + return set, i, i+1 + + def htimeset(self, hour, minute, second): + set = [] + rr = self.rrule + for minute in rr._byminute: + for second in rr._bysecond: + set.append(datetime.time(hour, minute, second, + tzinfo=rr._tzinfo)) + set.sort() + return set + + def mtimeset(self, hour, minute, second): + set = [] + rr = self.rrule + for second in rr._bysecond: + set.append(datetime.time(hour, minute, second, tzinfo=rr._tzinfo)) + set.sort() + return set + + def stimeset(self, hour, minute, second): + return (datetime.time(hour, minute, second, + tzinfo=self.rrule._tzinfo),) + + +class rruleset(rrulebase): + + class _genitem: + def __init__(self, genlist, gen): + try: + self.dt = gen() + genlist.append(self) + except StopIteration: + pass + self.genlist = genlist + self.gen = gen + + def next(self): + try: + self.dt = self.gen() + except StopIteration: + self.genlist.remove(self) + + def __cmp__(self, other): + return cmp(self.dt, other.dt) + + def __init__(self, cache=False): + rrulebase.__init__(self, cache) + self._rrule = [] + self._rdate = [] + self._exrule = [] + self._exdate = [] + + def rrule(self, rrule): + self._rrule.append(rrule) + + def rdate(self, rdate): + self._rdate.append(rdate) + + def exrule(self, exrule): + self._exrule.append(exrule) + + def exdate(self, exdate): + self._exdate.append(exdate) + + def _iter(self): + rlist = [] + self._rdate.sort() + self._genitem(rlist, iter(self._rdate).next) + for gen in [iter(x).next for x in self._rrule]: + self._genitem(rlist, gen) + rlist.sort() + exlist = [] + self._exdate.sort() + self._genitem(exlist, iter(self._exdate).next) + for gen in [iter(x).next for x in self._exrule]: + self._genitem(exlist, gen) + exlist.sort() + lastdt = None + total = 0 + while rlist: + ritem = rlist[0] + if not lastdt or lastdt != ritem.dt: + while exlist and exlist[0] < ritem: + exlist[0].next() + exlist.sort() + if not exlist or ritem != exlist[0]: + total += 1 + yield ritem.dt + lastdt = ritem.dt + ritem.next() + rlist.sort() + self._len = total + +class _rrulestr: + + _freq_map = {"YEARLY": YEARLY, + "MONTHLY": MONTHLY, + "WEEKLY": WEEKLY, + "DAILY": DAILY, + "HOURLY": HOURLY, + "MINUTELY": MINUTELY, + "SECONDLY": SECONDLY} + + _weekday_map = {"MO":0,"TU":1,"WE":2,"TH":3,"FR":4,"SA":5,"SU":6} + + def _handle_int(self, rrkwargs, name, value, **kwargs): + rrkwargs[name.lower()] = int(value) + + def _handle_int_list(self, rrkwargs, name, value, **kwargs): + rrkwargs[name.lower()] = [int(x) for x in value.split(',')] + + _handle_INTERVAL = _handle_int + _handle_COUNT = _handle_int + _handle_BYSETPOS = _handle_int_list + _handle_BYMONTH = _handle_int_list + _handle_BYMONTHDAY = _handle_int_list + _handle_BYYEARDAY = _handle_int_list + _handle_BYEASTER = _handle_int_list + _handle_BYWEEKNO = _handle_int_list + _handle_BYHOUR = _handle_int_list + _handle_BYMINUTE = _handle_int_list + _handle_BYSECOND = _handle_int_list + + def _handle_FREQ(self, rrkwargs, name, value, **kwargs): + rrkwargs["freq"] = self._freq_map[value] + + def _handle_UNTIL(self, rrkwargs, name, value, **kwargs): + global parser + if not parser: + from dateutil import parser + try: + rrkwargs["until"] = parser.parse(value, + ignoretz=kwargs.get("ignoretz"), + tzinfos=kwargs.get("tzinfos")) + except ValueError: + raise ValueError, "invalid until date" + + def _handle_WKST(self, rrkwargs, name, value, **kwargs): + rrkwargs["wkst"] = self._weekday_map[value] + + def _handle_BYWEEKDAY(self, rrkwargs, name, value, **kwarsg): + l = [] + for wday in value.split(','): + for i in range(len(wday)): + if wday[i] not in '+-0123456789': + break + n = wday[:i] or None + w = wday[i:] + if n: n = int(n) + l.append(weekdays[self._weekday_map[w]](n)) + rrkwargs["byweekday"] = l + + _handle_BYDAY = _handle_BYWEEKDAY + + def _parse_rfc_rrule(self, line, + dtstart=None, + cache=False, + ignoretz=False, + tzinfos=None): + if line.find(':') != -1: + name, value = line.split(':') + if name != "RRULE": + raise ValueError, "unknown parameter name" + else: + value = line + rrkwargs = {} + for pair in value.split(';'): + name, value = pair.split('=') + name = name.upper() + value = value.upper() + try: + getattr(self, "_handle_"+name)(rrkwargs, name, value, + ignoretz=ignoretz, + tzinfos=tzinfos) + except AttributeError: + raise ValueError, "unknown parameter '%s'" % name + except (KeyError, ValueError): + raise ValueError, "invalid '%s': %s" % (name, value) + return rrule(dtstart=dtstart, cache=cache, **rrkwargs) + + def _parse_rfc(self, s, + dtstart=None, + cache=False, + unfold=False, + forceset=False, + compatible=False, + ignoretz=False, + tzinfos=None): + global parser + if compatible: + forceset = True + unfold = True + s = s.upper() + if not s.strip(): + raise ValueError, "empty string" + if unfold: + lines = s.splitlines() + i = 0 + while i < len(lines): + line = lines[i].rstrip() + if not line: + del lines[i] + elif i > 0 and line[0] == " ": + lines[i-1] += line[1:] + del lines[i] + else: + i += 1 + else: + lines = s.split() + if (not forceset and len(lines) == 1 and + (s.find(':') == -1 or s.startswith('RRULE:'))): + return self._parse_rfc_rrule(lines[0], cache=cache, + dtstart=dtstart, ignoretz=ignoretz, + tzinfos=tzinfos) + else: + rrulevals = [] + rdatevals = [] + exrulevals = [] + exdatevals = [] + for line in lines: + if not line: + continue + if line.find(':') == -1: + name = "RRULE" + value = line + else: + name, value = line.split(':', 1) + parms = name.split(';') + if not parms: + raise ValueError, "empty property name" + name = parms[0] + parms = parms[1:] + if name == "RRULE": + for parm in parms: + raise ValueError, "unsupported RRULE parm: "+parm + rrulevals.append(value) + elif name == "RDATE": + for parm in parms: + if parm != "VALUE=DATE-TIME": + raise ValueError, "unsupported RDATE parm: "+parm + rdatevals.append(value) + elif name == "EXRULE": + for parm in parms: + raise ValueError, "unsupported EXRULE parm: "+parm + exrulevals.append(value) + elif name == "EXDATE": + for parm in parms: + if parm != "VALUE=DATE-TIME": + raise ValueError, "unsupported RDATE parm: "+parm + exdatevals.append(value) + elif name == "DTSTART": + for parm in parms: + raise ValueError, "unsupported DTSTART parm: "+parm + if not parser: + from dateutil import parser + dtstart = parser.parse(value, ignoretz=ignoretz, + tzinfos=tzinfos) + else: + raise ValueError, "unsupported property: "+name + if (forceset or len(rrulevals) > 1 or + rdatevals or exrulevals or exdatevals): + if not parser and (rdatevals or exdatevals): + from dateutil import parser + set = rruleset(cache=cache) + for value in rrulevals: + set.rrule(self._parse_rfc_rrule(value, dtstart=dtstart, + ignoretz=ignoretz, + tzinfos=tzinfos)) + for value in rdatevals: + for datestr in value.split(','): + set.rdate(parser.parse(datestr, + ignoretz=ignoretz, + tzinfos=tzinfos)) + for value in exrulevals: + set.exrule(self._parse_rfc_rrule(value, dtstart=dtstart, + ignoretz=ignoretz, + tzinfos=tzinfos)) + for value in exdatevals: + for datestr in value.split(','): + set.exdate(parser.parse(datestr, + ignoretz=ignoretz, + tzinfos=tzinfos)) + if compatible and dtstart: + set.rdate(dtstart) + return set + else: + return self._parse_rfc_rrule(rrulevals[0], + dtstart=dtstart, + cache=cache, + ignoretz=ignoretz, + tzinfos=tzinfos) + + def __call__(self, s, **kwargs): + return self._parse_rfc(s, **kwargs) + +rrulestr = _rrulestr() + +# vim:ts=4:sw=4:et diff --git a/dateutil/tz.py b/dateutil/tz.py new file mode 100644 index 0000000..0e28d6b --- /dev/null +++ b/dateutil/tz.py @@ -0,0 +1,951 @@ +""" +Copyright (c) 2003-2007 Gustavo Niemeyer + +This module offers extensions to the standard python 2.3+ +datetime module. +""" +__author__ = "Gustavo Niemeyer " +__license__ = "PSF License" + +import datetime +import struct +import time +import sys +import os + +relativedelta = None +parser = None +rrule = None + +__all__ = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange", + "tzstr", "tzical", "tzwin", "tzwinlocal", "gettz"] + +try: + from dateutil.tzwin import tzwin, tzwinlocal +except (ImportError, OSError): + tzwin, tzwinlocal = None, None + +ZERO = datetime.timedelta(0) +EPOCHORDINAL = datetime.datetime.utcfromtimestamp(0).toordinal() + +class tzutc(datetime.tzinfo): + + def utcoffset(self, dt): + return ZERO + + def dst(self, dt): + return ZERO + + def tzname(self, dt): + return "UTC" + + def __eq__(self, other): + return (isinstance(other, tzutc) or + (isinstance(other, tzoffset) and other._offset == ZERO)) + + def __ne__(self, other): + return not self.__eq__(other) + + def __repr__(self): + return "%s()" % self.__class__.__name__ + + __reduce__ = object.__reduce__ + +class tzoffset(datetime.tzinfo): + + def __init__(self, name, offset): + self._name = name + self._offset = datetime.timedelta(seconds=offset) + + def utcoffset(self, dt): + return self._offset + + def dst(self, dt): + return ZERO + + def tzname(self, dt): + return self._name + + def __eq__(self, other): + return (isinstance(other, tzoffset) and + self._offset == other._offset) + + def __ne__(self, other): + return not self.__eq__(other) + + def __repr__(self): + return "%s(%s, %s)" % (self.__class__.__name__, + `self._name`, + self._offset.days*86400+self._offset.seconds) + + __reduce__ = object.__reduce__ + +class tzlocal(datetime.tzinfo): + + _std_offset = datetime.timedelta(seconds=-time.timezone) + if time.daylight: + _dst_offset = datetime.timedelta(seconds=-time.altzone) + else: + _dst_offset = _std_offset + + def utcoffset(self, dt): + if self._isdst(dt): + return self._dst_offset + else: + return self._std_offset + + def dst(self, dt): + if self._isdst(dt): + return self._dst_offset-self._std_offset + else: + return ZERO + + def tzname(self, dt): + return time.tzname[self._isdst(dt)] + + def _isdst(self, dt): + # We can't use mktime here. It is unstable when deciding if + # the hour near to a change is DST or not. + # + # timestamp = time.mktime((dt.year, dt.month, dt.day, dt.hour, + # dt.minute, dt.second, dt.weekday(), 0, -1)) + # return time.localtime(timestamp).tm_isdst + # + # The code above yields the following result: + # + #>>> import tz, datetime + #>>> t = tz.tzlocal() + #>>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() + #'BRDT' + #>>> datetime.datetime(2003,2,16,0,tzinfo=t).tzname() + #'BRST' + #>>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() + #'BRST' + #>>> datetime.datetime(2003,2,15,22,tzinfo=t).tzname() + #'BRDT' + #>>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() + #'BRDT' + # + # Here is a more stable implementation: + # + timestamp = ((dt.toordinal() - EPOCHORDINAL) * 86400 + + dt.hour * 3600 + + dt.minute * 60 + + dt.second) + return time.localtime(timestamp+time.timezone).tm_isdst + + def __eq__(self, other): + if not isinstance(other, tzlocal): + return False + return (self._std_offset == other._std_offset and + self._dst_offset == other._dst_offset) + return True + + def __ne__(self, other): + return not self.__eq__(other) + + def __repr__(self): + return "%s()" % self.__class__.__name__ + + __reduce__ = object.__reduce__ + +class _ttinfo(object): + __slots__ = ["offset", "delta", "isdst", "abbr", "isstd", "isgmt"] + + def __init__(self): + for attr in self.__slots__: + setattr(self, attr, None) + + def __repr__(self): + l = [] + for attr in self.__slots__: + value = getattr(self, attr) + if value is not None: + l.append("%s=%s" % (attr, `value`)) + return "%s(%s)" % (self.__class__.__name__, ", ".join(l)) + + def __eq__(self, other): + if not isinstance(other, _ttinfo): + return False + return (self.offset == other.offset and + self.delta == other.delta and + self.isdst == other.isdst and + self.abbr == other.abbr and + self.isstd == other.isstd and + self.isgmt == other.isgmt) + + def __ne__(self, other): + return not self.__eq__(other) + + def __getstate__(self): + state = {} + for name in self.__slots__: + state[name] = getattr(self, name, None) + return state + + def __setstate__(self, state): + for name in self.__slots__: + if name in state: + setattr(self, name, state[name]) + +class tzfile(datetime.tzinfo): + + # http://www.twinsun.com/tz/tz-link.htm + # ftp://elsie.nci.nih.gov/pub/tz*.tar.gz + + def __init__(self, fileobj): + if isinstance(fileobj, basestring): + self._filename = fileobj + fileobj = open(fileobj) + elif hasattr(fileobj, "name"): + self._filename = fileobj.name + else: + self._filename = `fileobj` + + # From tzfile(5): + # + # The time zone information files used by tzset(3) + # begin with the magic characters "TZif" to identify + # them as time zone information files, followed by + # sixteen bytes reserved for future use, followed by + # six four-byte values of type long, written in a + # ``standard'' byte order (the high-order byte + # of the value is written first). + + if fileobj.read(4) != "TZif": + raise ValueError, "magic not found" + + fileobj.read(16) + + ( + # The number of UTC/local indicators stored in the file. + ttisgmtcnt, + + # The number of standard/wall indicators stored in the file. + ttisstdcnt, + + # The number of leap seconds for which data is + # stored in the file. + leapcnt, + + # The number of "transition times" for which data + # is stored in the file. + timecnt, + + # The number of "local time types" for which data + # is stored in the file (must not be zero). + typecnt, + + # The number of characters of "time zone + # abbreviation strings" stored in the file. + charcnt, + + ) = struct.unpack(">6l", fileobj.read(24)) + + # The above header is followed by tzh_timecnt four-byte + # values of type long, sorted in ascending order. + # These values are written in ``standard'' byte order. + # Each is used as a transition time (as returned by + # time(2)) at which the rules for computing local time + # change. + + if timecnt: + self._trans_list = struct.unpack(">%dl" % timecnt, + fileobj.read(timecnt*4)) + else: + self._trans_list = [] + + # Next come tzh_timecnt one-byte values of type unsigned + # char; each one tells which of the different types of + # ``local time'' types described in the file is associated + # with the same-indexed transition time. These values + # serve as indices into an array of ttinfo structures that + # appears next in the file. + + if timecnt: + self._trans_idx = struct.unpack(">%dB" % timecnt, + fileobj.read(timecnt)) + else: + self._trans_idx = [] + + # Each ttinfo structure is written as a four-byte value + # for tt_gmtoff of type long, in a standard byte + # order, followed by a one-byte value for tt_isdst + # and a one-byte value for tt_abbrind. In each + # structure, tt_gmtoff gives the number of + # seconds to be added to UTC, tt_isdst tells whether + # tm_isdst should be set by localtime(3), and + # tt_abbrind serves as an index into the array of + # time zone abbreviation characters that follow the + # ttinfo structure(s) in the file. + + ttinfo = [] + + for i in range(typecnt): + ttinfo.append(struct.unpack(">lbb", fileobj.read(6))) + + abbr = fileobj.read(charcnt) + + # Then there are tzh_leapcnt pairs of four-byte + # values, written in standard byte order; the + # first value of each pair gives the time (as + # returned by time(2)) at which a leap second + # occurs; the second gives the total number of + # leap seconds to be applied after the given time. + # The pairs of values are sorted in ascending order + # by time. + + # Not used, for now + if leapcnt: + leap = struct.unpack(">%dl" % (leapcnt*2), + fileobj.read(leapcnt*8)) + + # Then there are tzh_ttisstdcnt standard/wall + # indicators, each stored as a one-byte value; + # they tell whether the transition times associated + # with local time types were specified as standard + # time or wall clock time, and are used when + # a time zone file is used in handling POSIX-style + # time zone environment variables. + + if ttisstdcnt: + isstd = struct.unpack(">%db" % ttisstdcnt, + fileobj.read(ttisstdcnt)) + + # Finally, there are tzh_ttisgmtcnt UTC/local + # indicators, each stored as a one-byte value; + # they tell whether the transition times associated + # with local time types were specified as UTC or + # local time, and are used when a time zone file + # is used in handling POSIX-style time zone envi- + # ronment variables. + + if ttisgmtcnt: + isgmt = struct.unpack(">%db" % ttisgmtcnt, + fileobj.read(ttisgmtcnt)) + + # ** Everything has been read ** + + # Build ttinfo list + self._ttinfo_list = [] + for i in range(typecnt): + gmtoff, isdst, abbrind = ttinfo[i] + # Round to full-minutes if that's not the case. Python's + # datetime doesn't accept sub-minute timezones. Check + # http://python.org/sf/1447945 for some information. + gmtoff = (gmtoff+30)//60*60 + tti = _ttinfo() + tti.offset = gmtoff + tti.delta = datetime.timedelta(seconds=gmtoff) + tti.isdst = isdst + tti.abbr = abbr[abbrind:abbr.find('\x00', abbrind)] + tti.isstd = (ttisstdcnt > i and isstd[i] != 0) + tti.isgmt = (ttisgmtcnt > i and isgmt[i] != 0) + self._ttinfo_list.append(tti) + + # Replace ttinfo indexes for ttinfo objects. + trans_idx = [] + for idx in self._trans_idx: + trans_idx.append(self._ttinfo_list[idx]) + self._trans_idx = tuple(trans_idx) + + # Set standard, dst, and before ttinfos. before will be + # used when a given time is before any transitions, + # and will be set to the first non-dst ttinfo, or to + # the first dst, if all of them are dst. + self._ttinfo_std = None + self._ttinfo_dst = None + self._ttinfo_before = None + if self._ttinfo_list: + if not self._trans_list: + self._ttinfo_std = self._ttinfo_first = self._ttinfo_list[0] + else: + for i in range(timecnt-1,-1,-1): + tti = self._trans_idx[i] + if not self._ttinfo_std and not tti.isdst: + self._ttinfo_std = tti + elif not self._ttinfo_dst and tti.isdst: + self._ttinfo_dst = tti + if self._ttinfo_std and self._ttinfo_dst: + break + else: + if self._ttinfo_dst and not self._ttinfo_std: + self._ttinfo_std = self._ttinfo_dst + + for tti in self._ttinfo_list: + if not tti.isdst: + self._ttinfo_before = tti + break + else: + self._ttinfo_before = self._ttinfo_list[0] + + # Now fix transition times to become relative to wall time. + # + # I'm not sure about this. In my tests, the tz source file + # is setup to wall time, and in the binary file isstd and + # isgmt are off, so it should be in wall time. OTOH, it's + # always in gmt time. Let me know if you have comments + # about this. + laststdoffset = 0 + self._trans_list = list(self._trans_list) + for i in range(len(self._trans_list)): + tti = self._trans_idx[i] + if not tti.isdst: + # This is std time. + self._trans_list[i] += tti.offset + laststdoffset = tti.offset + else: + # This is dst time. Convert to std. + self._trans_list[i] += laststdoffset + self._trans_list = tuple(self._trans_list) + + def _find_ttinfo(self, dt, laststd=0): + timestamp = ((dt.toordinal() - EPOCHORDINAL) * 86400 + + dt.hour * 3600 + + dt.minute * 60 + + dt.second) + idx = 0 + for trans in self._trans_list: + if timestamp < trans: + break + idx += 1 + else: + return self._ttinfo_std + if idx == 0: + return self._ttinfo_before + if laststd: + while idx > 0: + tti = self._trans_idx[idx-1] + if not tti.isdst: + return tti + idx -= 1 + else: + return self._ttinfo_std + else: + return self._trans_idx[idx-1] + + def utcoffset(self, dt): + if not self._ttinfo_std: + return ZERO + return self._find_ttinfo(dt).delta + + def dst(self, dt): + if not self._ttinfo_dst: + return ZERO + tti = self._find_ttinfo(dt) + if not tti.isdst: + return ZERO + + # The documentation says that utcoffset()-dst() must + # be constant for every dt. + return tti.delta-self._find_ttinfo(dt, laststd=1).delta + + # An alternative for that would be: + # + # return self._ttinfo_dst.offset-self._ttinfo_std.offset + # + # However, this class stores historical changes in the + # dst offset, so I belive that this wouldn't be the right + # way to implement this. + + def tzname(self, dt): + if not self._ttinfo_std: + return None + return self._find_ttinfo(dt).abbr + + def __eq__(self, other): + if not isinstance(other, tzfile): + return False + return (self._trans_list == other._trans_list and + self._trans_idx == other._trans_idx and + self._ttinfo_list == other._ttinfo_list) + + def __ne__(self, other): + return not self.__eq__(other) + + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, `self._filename`) + + def __reduce__(self): + if not os.path.isfile(self._filename): + raise ValueError, "Unpickable %s class" % self.__class__.__name__ + return (self.__class__, (self._filename,)) + +class tzrange(datetime.tzinfo): + + def __init__(self, stdabbr, stdoffset=None, + dstabbr=None, dstoffset=None, + start=None, end=None): + global relativedelta + if not relativedelta: + from dateutil import relativedelta + self._std_abbr = stdabbr + self._dst_abbr = dstabbr + if stdoffset is not None: + self._std_offset = datetime.timedelta(seconds=stdoffset) + else: + self._std_offset = ZERO + if dstoffset is not None: + self._dst_offset = datetime.timedelta(seconds=dstoffset) + elif dstabbr and stdoffset is not None: + self._dst_offset = self._std_offset+datetime.timedelta(hours=+1) + else: + self._dst_offset = ZERO + if dstabbr and start is None: + self._start_delta = relativedelta.relativedelta( + hours=+2, month=4, day=1, weekday=relativedelta.SU(+1)) + else: + self._start_delta = start + if dstabbr and end is None: + self._end_delta = relativedelta.relativedelta( + hours=+1, month=10, day=31, weekday=relativedelta.SU(-1)) + else: + self._end_delta = end + + def utcoffset(self, dt): + if self._isdst(dt): + return self._dst_offset + else: + return self._std_offset + + def dst(self, dt): + if self._isdst(dt): + return self._dst_offset-self._std_offset + else: + return ZERO + + def tzname(self, dt): + if self._isdst(dt): + return self._dst_abbr + else: + return self._std_abbr + + def _isdst(self, dt): + if not self._start_delta: + return False + year = datetime.datetime(dt.year,1,1) + start = year+self._start_delta + end = year+self._end_delta + dt = dt.replace(tzinfo=None) + if start < end: + return dt >= start and dt < end + else: + return dt >= start or dt < end + + def __eq__(self, other): + if not isinstance(other, tzrange): + return False + return (self._std_abbr == other._std_abbr and + self._dst_abbr == other._dst_abbr and + self._std_offset == other._std_offset and + self._dst_offset == other._dst_offset and + self._start_delta == other._start_delta and + self._end_delta == other._end_delta) + + def __ne__(self, other): + return not self.__eq__(other) + + def __repr__(self): + return "%s(...)" % self.__class__.__name__ + + __reduce__ = object.__reduce__ + +class tzstr(tzrange): + + def __init__(self, s): + global parser + if not parser: + from dateutil import parser + self._s = s + + res = parser._parsetz(s) + if res is None: + raise ValueError, "unknown string format" + + # Here we break the compatibility with the TZ variable handling. + # GMT-3 actually *means* the timezone -3. + if res.stdabbr in ("GMT", "UTC"): + res.stdoffset *= -1 + + # We must initialize it first, since _delta() needs + # _std_offset and _dst_offset set. Use False in start/end + # to avoid building it two times. + tzrange.__init__(self, res.stdabbr, res.stdoffset, + res.dstabbr, res.dstoffset, + start=False, end=False) + + if not res.dstabbr: + self._start_delta = None + self._end_delta = None + else: + self._start_delta = self._delta(res.start) + if self._start_delta: + self._end_delta = self._delta(res.end, isend=1) + + def _delta(self, x, isend=0): + kwargs = {} + if x.month is not None: + kwargs["month"] = x.month + if x.weekday is not None: + kwargs["weekday"] = relativedelta.weekday(x.weekday, x.week) + if x.week > 0: + kwargs["day"] = 1 + else: + kwargs["day"] = 31 + elif x.day: + kwargs["day"] = x.day + elif x.yday is not None: + kwargs["yearday"] = x.yday + elif x.jyday is not None: + kwargs["nlyearday"] = x.jyday + if not kwargs: + # Default is to start on first sunday of april, and end + # on last sunday of october. + if not isend: + kwargs["month"] = 4 + kwargs["day"] = 1 + kwargs["weekday"] = relativedelta.SU(+1) + else: + kwargs["month"] = 10 + kwargs["day"] = 31 + kwargs["weekday"] = relativedelta.SU(-1) + if x.time is not None: + kwargs["seconds"] = x.time + else: + # Default is 2AM. + kwargs["seconds"] = 7200 + if isend: + # Convert to standard time, to follow the documented way + # of working with the extra hour. See the documentation + # of the tzinfo class. + delta = self._dst_offset-self._std_offset + kwargs["seconds"] -= delta.seconds+delta.days*86400 + return relativedelta.relativedelta(**kwargs) + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, `self._s`) + +class _tzicalvtzcomp: + def __init__(self, tzoffsetfrom, tzoffsetto, isdst, + tzname=None, rrule=None): + self.tzoffsetfrom = datetime.timedelta(seconds=tzoffsetfrom) + self.tzoffsetto = datetime.timedelta(seconds=tzoffsetto) + self.tzoffsetdiff = self.tzoffsetto-self.tzoffsetfrom + self.isdst = isdst + self.tzname = tzname + self.rrule = rrule + +class _tzicalvtz(datetime.tzinfo): + def __init__(self, tzid, comps=[]): + self._tzid = tzid + self._comps = comps + self._cachedate = [] + self._cachecomp = [] + + def _find_comp(self, dt): + if len(self._comps) == 1: + return self._comps[0] + dt = dt.replace(tzinfo=None) + try: + return self._cachecomp[self._cachedate.index(dt)] + except ValueError: + pass + lastcomp = None + lastcompdt = None + for comp in self._comps: + if not comp.isdst: + # Handle the extra hour in DST -> STD + compdt = comp.rrule.before(dt-comp.tzoffsetdiff, inc=True) + else: + compdt = comp.rrule.before(dt, inc=True) + if compdt and (not lastcompdt or lastcompdt < compdt): + lastcompdt = compdt + lastcomp = comp + if not lastcomp: + # RFC says nothing about what to do when a given + # time is before the first onset date. We'll look for the + # first standard component, or the first component, if + # none is found. + for comp in self._comps: + if not comp.isdst: + lastcomp = comp + break + else: + lastcomp = comp[0] + self._cachedate.insert(0, dt) + self._cachecomp.insert(0, lastcomp) + if len(self._cachedate) > 10: + self._cachedate.pop() + self._cachecomp.pop() + return lastcomp + + def utcoffset(self, dt): + return self._find_comp(dt).tzoffsetto + + def dst(self, dt): + comp = self._find_comp(dt) + if comp.isdst: + return comp.tzoffsetdiff + else: + return ZERO + + def tzname(self, dt): + return self._find_comp(dt).tzname + + def __repr__(self): + return "" % `self._tzid` + + __reduce__ = object.__reduce__ + +class tzical: + def __init__(self, fileobj): + global rrule + if not rrule: + from dateutil import rrule + + if isinstance(fileobj, basestring): + self._s = fileobj + fileobj = open(fileobj) + elif hasattr(fileobj, "name"): + self._s = fileobj.name + else: + self._s = `fileobj` + + self._vtz = {} + + self._parse_rfc(fileobj.read()) + + def keys(self): + return self._vtz.keys() + + def get(self, tzid=None): + if tzid is None: + keys = self._vtz.keys() + if len(keys) == 0: + raise ValueError, "no timezones defined" + elif len(keys) > 1: + raise ValueError, "more than one timezone available" + tzid = keys[0] + return self._vtz.get(tzid) + + def _parse_offset(self, s): + s = s.strip() + if not s: + raise ValueError, "empty offset" + if s[0] in ('+', '-'): + signal = (-1,+1)[s[0]=='+'] + s = s[1:] + else: + signal = +1 + if len(s) == 4: + return (int(s[:2])*3600+int(s[2:])*60)*signal + elif len(s) == 6: + return (int(s[:2])*3600+int(s[2:4])*60+int(s[4:]))*signal + else: + raise ValueError, "invalid offset: "+s + + def _parse_rfc(self, s): + lines = s.splitlines() + if not lines: + raise ValueError, "empty string" + + # Unfold + i = 0 + while i < len(lines): + line = lines[i].rstrip() + if not line: + del lines[i] + elif i > 0 and line[0] == " ": + lines[i-1] += line[1:] + del lines[i] + else: + i += 1 + + tzid = None + comps = [] + invtz = False + comptype = None + for line in lines: + if not line: + continue + name, value = line.split(':', 1) + parms = name.split(';') + if not parms: + raise ValueError, "empty property name" + name = parms[0].upper() + parms = parms[1:] + if invtz: + if name == "BEGIN": + if value in ("STANDARD", "DAYLIGHT"): + # Process component + pass + else: + raise ValueError, "unknown component: "+value + comptype = value + founddtstart = False + tzoffsetfrom = None + tzoffsetto = None + rrulelines = [] + tzname = None + elif name == "END": + if value == "VTIMEZONE": + if comptype: + raise ValueError, \ + "component not closed: "+comptype + if not tzid: + raise ValueError, \ + "mandatory TZID not found" + if not comps: + raise ValueError, \ + "at least one component is needed" + # Process vtimezone + self._vtz[tzid] = _tzicalvtz(tzid, comps) + invtz = False + elif value == comptype: + if not founddtstart: + raise ValueError, \ + "mandatory DTSTART not found" + if tzoffsetfrom is None: + raise ValueError, \ + "mandatory TZOFFSETFROM not found" + if tzoffsetto is None: + raise ValueError, \ + "mandatory TZOFFSETFROM not found" + # Process component + rr = None + if rrulelines: + rr = rrule.rrulestr("\n".join(rrulelines), + compatible=True, + ignoretz=True, + cache=True) + comp = _tzicalvtzcomp(tzoffsetfrom, tzoffsetto, + (comptype == "DAYLIGHT"), + tzname, rr) + comps.append(comp) + comptype = None + else: + raise ValueError, \ + "invalid component end: "+value + elif comptype: + if name == "DTSTART": + rrulelines.append(line) + founddtstart = True + elif name in ("RRULE", "RDATE", "EXRULE", "EXDATE"): + rrulelines.append(line) + elif name == "TZOFFSETFROM": + if parms: + raise ValueError, \ + "unsupported %s parm: %s "%(name, parms[0]) + tzoffsetfrom = self._parse_offset(value) + elif name == "TZOFFSETTO": + if parms: + raise ValueError, \ + "unsupported TZOFFSETTO parm: "+parms[0] + tzoffsetto = self._parse_offset(value) + elif name == "TZNAME": + if parms: + raise ValueError, \ + "unsupported TZNAME parm: "+parms[0] + tzname = value + elif name == "COMMENT": + pass + else: + raise ValueError, "unsupported property: "+name + else: + if name == "TZID": + if parms: + raise ValueError, \ + "unsupported TZID parm: "+parms[0] + tzid = value + elif name in ("TZURL", "LAST-MODIFIED", "COMMENT"): + pass + else: + raise ValueError, "unsupported property: "+name + elif name == "BEGIN" and value == "VTIMEZONE": + tzid = None + comps = [] + invtz = True + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, `self._s`) + +if sys.platform != "win32": + TZFILES = ["/etc/localtime", "localtime"] + TZPATHS = ["/usr/share/zoneinfo", "/usr/lib/zoneinfo", "/etc/zoneinfo"] +else: + TZFILES = [] + TZPATHS = [] + +def gettz(name=None): + tz = None + if not name: + try: + name = os.environ["TZ"] + except KeyError: + pass + if name is None or name == ":": + for filepath in TZFILES: + if not os.path.isabs(filepath): + filename = filepath + for path in TZPATHS: + filepath = os.path.join(path, filename) + if os.path.isfile(filepath): + break + else: + continue + if os.path.isfile(filepath): + try: + tz = tzfile(filepath) + break + except (IOError, OSError, ValueError): + pass + else: + tz = tzlocal() + else: + if name.startswith(":"): + name = name[:-1] + if os.path.isabs(name): + if os.path.isfile(name): + tz = tzfile(name) + else: + tz = None + else: + for path in TZPATHS: + filepath = os.path.join(path, name) + if not os.path.isfile(filepath): + filepath = filepath.replace(' ','_') + if not os.path.isfile(filepath): + continue + try: + tz = tzfile(filepath) + break + except (IOError, OSError, ValueError): + pass + else: + tz = None + if tzwin: + try: + tz = tzwin(name) + except OSError: + pass + if not tz: + from dateutil.zoneinfo import gettz + tz = gettz(name) + if not tz: + for c in name: + # name must have at least one offset to be a tzstr + if c in "0123456789": + try: + tz = tzstr(name) + except ValueError: + pass + break + else: + if name in ("GMT", "UTC"): + tz = tzutc() + elif name in time.tzname: + tz = tzlocal() + return tz + +# vim:ts=4:sw=4:et diff --git a/defusedxml/ElementTree.py b/defusedxml/ElementTree.py new file mode 100644 index 0000000..1258342 --- /dev/null +++ b/defusedxml/ElementTree.py @@ -0,0 +1,62 @@ +# defusedxml +# +# Copyright (c) 2013 by Christian Heimes +# Licensed to PSF under a Contributor Agreement. +# See http://www.python.org/psf/license for licensing details. +"""Defused xml.etree.ElementTree facade +""" +from __future__ import print_function, absolute_import + +from xml.etree.ElementTree import XMLParser as _XMLParser +from xml.etree.ElementTree import iterparse as _iterparse +_IterParseIterator = None +from xml.etree.ElementTree import TreeBuilder as _TreeBuilder +from xml.etree.ElementTree import parse as _parse + +from .common import (DTDForbidden, EntitiesForbidden, + ExternalReferenceForbidden, _generate_etree_functions) + +__origin__ = "xml.etree.ElementTree" + +class DefusedXMLParser(_XMLParser): + def __init__(self, html=0, target=None, encoding=None, + forbid_dtd=False, forbid_entities=True, + forbid_external=True): + # Python 2.x old style class + _XMLParser.__init__(self, html, target, encoding) + self.forbid_dtd = forbid_dtd + self.forbid_entities = forbid_entities + self.forbid_external = forbid_external + parser = self._parser + if self.forbid_dtd: + parser.StartDoctypeDeclHandler = self.defused_start_doctype_decl + if self.forbid_entities: + parser.EntityDeclHandler = self.defused_entity_decl + parser.UnparsedEntityDeclHandler = self.defused_unparsed_entity_decl + if self.forbid_external: + parser.ExternalEntityRefHandler = self.defused_external_entity_ref_handler + + def defused_start_doctype_decl(self, name, sysid, pubid, + has_internal_subset): + raise DTDForbidden(name, sysid, pubid) + + def defused_entity_decl(self, name, is_parameter_entity, value, base, + sysid, pubid, notation_name): + raise EntitiesForbidden(name, value, base, sysid, pubid, notation_name) + + def defused_unparsed_entity_decl(self, name, base, sysid, pubid, + notation_name): + # expat 1.2 + raise EntitiesForbidden(name, None, base, sysid, pubid, notation_name) + + def defused_external_entity_ref_handler(self, context, base, sysid, + pubid): + raise ExternalReferenceForbidden(context, base, sysid, pubid) + + +# aliases +XMLTreeBuilder = XMLParse = DefusedXMLParser + +parse, iterparse, fromstring = _generate_etree_functions(DefusedXMLParser, + _TreeBuilder, _IterParseIterator, _parse, _iterparse) +XML = fromstring diff --git a/defusedxml/__init__.py b/defusedxml/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/defusedxml/common.py b/defusedxml/common.py new file mode 100644 index 0000000..5e5f8a2 --- /dev/null +++ b/defusedxml/common.py @@ -0,0 +1,163 @@ +# defusedxml +# +# Copyright (c) 2013 by Christian Heimes +# Licensed to PSF under a Contributor Agreement. +# See http://www.python.org/psf/license for licensing details. +"""Common constants, exceptions and helpe functions +""" +import sys +from types import MethodType + +PY3 = sys.version_info[0] == 3 +PY26 = sys.version_info[:2] == (2, 6) +PY31 = sys.version_info[:2] == (3, 1) + + +class DefusedXmlException(ValueError): + """Base exception + """ + def __repr__(self): + return str(self) + + +class DTDForbidden(DefusedXmlException): + """Document type definition is forbidden + """ + def __init__(self, name, sysid, pubid): + super(DTDForbidden, self).__init__() + self.name = name + self.sysid = sysid + self.pubid = pubid + + def __str__(self): + tpl = "DTDForbidden(name='{}', system_id={!r}, public_id={!r})" + return tpl.format(self.name, self.sysid, self.pubid) + + +class EntitiesForbidden(DefusedXmlException): + """Entity definition is forbidden + """ + def __init__(self, name, value, base, sysid, pubid, notation_name): + super(EntitiesForbidden, self).__init__() + self.name = name + self.value = value + self.base = base + self.sysid = sysid + self.pubid = pubid + self.notation_name = notation_name + + def __str__(self): + tpl = "EntitiesForbidden(name='{}', system_id={!r}, public_id={!r})" + return tpl.format(self.name, self.sysid, self.pubid) + + +class ExternalReferenceForbidden(DefusedXmlException): + """Resolving an external reference is forbidden + """ + def __init__(self, context, base, sysid, pubid): + super(ExternalReferenceForbidden, self).__init__() + self.context = context + self.base = base + self.sysid = sysid + self.pubid = pubid + + def __str__(self): + tpl = "ExternalReferenceForbidden(system_id='{}', public_id={})" + return tpl.format(self.sysid, self.pubid) + + +class NotSupportedError(DefusedXmlException): + """The operation is not supported + """ + + +def _apply_defusing(defused_mod): + assert defused_mod is sys.modules[defused_mod.__name__] + stdlib_name = defused_mod.__origin__ + __import__(stdlib_name, {}, {}, ["*"]) + stdlib_mod = sys.modules[stdlib_name] + stdlib_names = set(dir(stdlib_mod)) + for name, obj in vars(defused_mod).items(): + if name.startswith("_") or name not in stdlib_names: + continue + setattr(stdlib_mod, name, obj) + return stdlib_mod + + +def _generate_etree_functions(DefusedXMLParser, _TreeBuilder, + _IterParseIterator, _parse, _iterparse): + """Factory for functions needed by etree, dependent on whether + cElementTree or ElementTree is used.""" + + def parse(source, parser=None, forbid_dtd=False, forbid_entities=True, + forbid_external=True): + if parser is None: + parser = DefusedXMLParser(target=_TreeBuilder(), + forbid_dtd=forbid_dtd, + forbid_entities=forbid_entities, + forbid_external=forbid_external) + return _parse(source, parser) + + if PY26 or PY31: + def bind(xmlparser, funcname, hookname): + func = getattr(DefusedXMLParser, funcname) + if PY26: + # unbound -> function + func = func.__func__ + method = MethodType(func, xmlparser, xmlparser.__class__) + else: + method = MethodType(func, xmlparser) + # set hook + setattr(xmlparser._parser, hookname, method) + + def iterparse(source, events=None, forbid_dtd=False, + forbid_entities=True, forbid_external=True): + it = _iterparse(source, events) + xmlparser = it._parser + if forbid_dtd: + bind(xmlparser, "defused_start_doctype_decl", + "StartDoctypeDeclHandler") + if forbid_entities: + bind(xmlparser, "defused_entity_decl", + "EntityDeclHandler") + bind(xmlparser, "defused_unparsed_entity_decl", + "UnparsedEntityDeclHandler") + if forbid_external: + bind(xmlparser, "defused_external_entity_ref_handler", + "ExternalEntityRefHandler") + return it + elif PY3: + def iterparse(source, events=None, parser=None, forbid_dtd=False, + forbid_entities=True, forbid_external=True): + close_source = False + if not hasattr(source, "read"): + source = open(source, "rb") + close_source = True + if not parser: + parser = DefusedXMLParser(target=_TreeBuilder(), + forbid_dtd=forbid_dtd, + forbid_entities=forbid_entities, + forbid_external=forbid_external) + return _IterParseIterator(source, events, parser, close_source) + else: + # Python 2.7 + def iterparse(source, events=None, parser=None, forbid_dtd=False, + forbid_entities=True, forbid_external=True): + if parser is None: + parser = DefusedXMLParser(target=_TreeBuilder(), + forbid_dtd=forbid_dtd, + forbid_entities=forbid_entities, + forbid_external=forbid_external) + return _iterparse(source, events, parser) + + def fromstring(text, forbid_dtd=False, forbid_entities=True, + forbid_external=True): + parser = DefusedXMLParser(target=_TreeBuilder(), + forbid_dtd=forbid_dtd, + forbid_entities=forbid_entities, + forbid_external=forbid_external) + parser.feed(text) + return parser.close() + + + return parse, iterparse, fromstring diff --git a/empty.png b/empty.png new file mode 100644 index 0000000000000000000000000000000000000000..a4882e3677f609871c5eb0fc0b518a1a71e6a396 GIT binary patch literal 105 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>$r9IylHmNblJdl&R0hYC{G?O` z&)mfH)S%SFl*+=BsWw1G+@3CuAr*|tKh85SFflSRu)Wtf4HRbZboFyt=akR{0Cq$f A5C8xG literal 0 HcmV?d00001 diff --git a/hosted.lua b/hosted.lua new file mode 100644 index 0000000..a63f926 --- /dev/null +++ b/hosted.lua @@ -0,0 +1,198 @@ +-- Part of info-beamer hosted +-- +-- Copyright (c) 2014, Florian Wesch +-- All rights reserved. +-- +-- Redistribution and use in source and binary forms, with or without +-- modification, are permitted provided that the following conditions are +-- met: +-- +-- Redistributions of source code must retain the above copyright +-- notice, this list of conditions and the following disclaimer. +-- +-- Redistributions in binary form must reproduce the above copyright +-- notice, this list of conditions and the following disclaimer in the +-- documentation and/or other materials provided with the +-- distribution. +-- +-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +-- IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +-- THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +-- PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +-- CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +-- EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +-- PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +-- PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +-- LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +-- NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +-- SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +local resource_types = { + ["image"] = function(value) + local surface + local image = { + asset_name = value.asset_name, + filename = value.filename, + type = value.type, + } + + function image.ensure_loaded() + if not surface then + surface = resource.load_image(value.asset_name) + end + return surface + end + function image.load() + if surface then + local state = surface:state() + return state ~= "loading" + else + surface = resource.load_image_async(value.asset_name) + return false + -- surface = resource.load_image(value.asset_name) + -- return true + end + end + function image.get_surface() + return image.ensure_loaded() + end + function image.draw(...) + image.ensure_loaded():draw(...) + end + function image.unload() + if surface then + surface:dispose() + surface = nil + end + end + return image + end; + ["video"] = function(value) + local surface + local video = { + asset_name = value.asset_name, + filename = value.filename, + type = value.type, + } + function video.ensure_loaded(opt) + if not surface then + surface = util.videoplayer(value.asset_name, opt) + end + return surface + end + function video.load(opt) + if surface then + local state = surface:state() + return state ~= "loading" + else + surface = util.videoplayer(value.asset_name, opt) + return false + end + end + function video.get_surface() + return video.ensure_loaded() + end + function video.draw(...) + video.ensure_loaded():draw(...) + end + function video.unload() + if surface then + surface:dispose() + surface = nil + end + end + return video + end; + ["child"] = function(value) + local child = { + asset_name = value.asset_name, + filename = value.filename, + type = value.type, + } + function child.ensure_loaded() + return resource.render_child(value.asset_name) + end + function child.load() + return true + end + function child.get_surface() + return resource.render_child(value.asset_name) + end + function child.draw(...) + resource.render_child(value.asset_name):draw(...) + end + function child.unload() + end + return child + end; +} + +local types = { + ["string"] = function(value) + return value + end; + ["integer"] = function(value) + return value + end; + ["select"] = function(value) + return value + end; + ["boolean"] = function(value) + return value + end; + ["duration"] = function(value) + return value + end; + ["color"] = function(value) + local color = {} + color.r = value.r + color.g = value.g + color.b = value.b + color.a = value.a + color.rgba_table = {color.r, color.g, color.b, color.a} + color.rgba = function() + return color.r, color.g, color.b, color.a + end + color.rgb_with_a = function(a) + return color.r, color.g, color.b, a + end + color.clear = function() + gl.clear(color.r, color.g, color.b, color.a) + end + return color + end; + ["resource"] = function(value) + return resource_types[value.type](value) + end; + ["font"] = function(value) + return resource.load_font(value.asset_name) + end; +} + +local function parse_config(options, config) + local function parse_recursive(options, config, target) + for _, option in ipairs(options) do + local name = option.name + if name then + if option.type == "list" then + local list = {} + for _, child_config in ipairs(config[name]) do + local child = {} + parse_recursive(option.items, child_config, child) + list[#list + 1] = child + end + target[name] = list + else + target[name] = types[option.type](config[name]) + end + end + end + end + local current_config = {} + parse_recursive(options, config, current_config) + return current_config +end + +return { + parse_config = parse_config; +} diff --git a/hosted.py b/hosted.py new file mode 100644 index 0000000..1bfd2a7 --- /dev/null +++ b/hosted.py @@ -0,0 +1,213 @@ +# Part of info-beamer hosted +# +# Copyright (c) 2014, Florian Wesch +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the +# distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import os +import sys +import json +import socket +import pyinotify + +types = {} + +def type(fn): + types[fn.__name__] = fn + return fn + +@type +def color(value): + return value + +@type +def string(value): + return value + +@type +def boolean(value): + return value + +@type +def select(value): + return value + +@type +def duration(value): + return value + +@type +def integer(value): + return value + +@type +def font(value): + return value + +@type +def resource(value): + return value + +class Configuration(object): + def __init__(self): + self._restart = False + self._options = [] + self._config = {} + self._parsed = {} + self.parse_node_json(do_update=False) + self.parse_config_json() + + def restart_on_update(self): + print >>sys.stderr, "[hosted.py] going to restart when config is updated" + self._restart = True + + def parse_node_json(self, do_update=True): + with file("node.json") as f: + self._options = json.load(f)['options'] + if do_update: + self.update_config() + + def parse_config_json(self, do_update=True): + with file("config.json") as f: + self._config = json.load(f) + if do_update: + self.update_config() + + def update_config(self): + if self._restart: + print >>sys.stderr, "[hosted.py] restarting service (restart_on_update set)" + import thread, time + thread.interrupt_main() + time.sleep(100) + return + + def parse_recursive(options, config, target): + # print 'parsing', config + for option in options: + if not 'name' in option: + continue + if option['type'] == 'list': + items = [] + for item in config[option['name']]: + parsed = {} + parse_recursive(option['items'], item, parsed) + items.append(parsed) + target[option['name']] = items + continue + target[option['name']] = types[option['type']](config[option['name']]) + + parsed = {} + parse_recursive(self._options, self._config, parsed) + print >>sys.stderr, "[hosted.py] updated config" + self._parsed = parsed + + def __getitem__(self, key): + return self._parsed[key] +Configuration = Configuration() + +class EventHandler(pyinotify.ProcessEvent): + def process_default(self, event): + print >>sys.stderr, event + basename = os.path.basename(event.pathname) + if basename == 'node.json': + Configuration.parse_node_json() + elif basename == 'config.json': + Configuration.parse_config_json() + elif basename == 'hosted.py': + print >>sys.stderr, "[hosted.py] restarting service since hosted.py changed" + import thread, time + thread.interrupt_main() + time.sleep(100) + + +wm = pyinotify.WatchManager() + +notifier = pyinotify.ThreadedNotifier(wm, EventHandler()) +notifier.daemon = True +notifier.start() + +wm.add_watch('.', pyinotify.IN_MOVED_TO) + +print >>sys.stderr, "initialized hosted.py" + +CONFIG = Configuration + +class Node(object): + def __init__(self): + self._node = os.environ['NODE'] + self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + + def send_raw(self, raw): + print >>sys.stderr, "sending %r" % (raw,) + self._sock.sendto(raw, ('127.0.0.1', 4444)) + + def send(self, data): + self.send_raw(self._node + data) + + class Sender(object): + def __init__(self, node, path): + self._node = node + self._path = path + + def __call__(self, data): + raw = "%s:%s" % (self._path, data) + self._node.send_raw(raw) + + def __getitem__(self, path): + return self.Sender(self, self._node + path) + + def __call__(self, data): + return self.Sender(self, self._node)(data) +NODE = Node() + +class Upstream(object): + def __init__(self): + self._socket = None + + def ensure_connected(self): + if self._socket: + return False + try: + print >>sys.stderr, "establishing upstream connection" + self._socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self._socket.connect(os.environ['SYNCER_SOCKET']) + return True + except Exception, err: + print >>sys.stderr, "cannot connect to upstream socket: %s" % (err,) + + def send_raw(self, raw): + try: + if self.ensure_connected(): + self._socket.send(raw) + except Exception, err: + print >>sys.stderr, "cannot send to upstream: %s" % (err,) + if self._socket: + self._socket.close() + self._socket = None + + def send(self, **data): + self.send_raw(json.dumps(data)) +UPSTREAM = Upstream() diff --git a/importer.py b/importer.py new file mode 100644 index 0000000..ebcfe8f --- /dev/null +++ b/importer.py @@ -0,0 +1,73 @@ +import traceback +import urllib2 +import calendar +import pytz +from operator import itemgetter +from datetime import timedelta + +import dateutil.parser +import defusedxml.ElementTree as ET + +def get_schedule(url): + def load_events(xml): + def to_unixtimestamp(dt): + dt = dt.astimezone(pytz.utc) + ts = int(calendar.timegm(dt.timetuple())) + return ts + def text_or_empty(node, child_name): + child = node.find(child_name) + if child is None: + return u"" + if child.text is None: + return u"" + return unicode(child.text) + def parse_duration(value): + h, m = map(int, value.split(':')) + return timedelta(hours=h, minutes=m) + + def all_events(): + schedule = ET.fromstring(xml) + for day in schedule.findall('day'): + for room in day.findall('room'): + for event in room.findall('event'): + yield event + + parsed_events = [] + for event in all_events(): + start = dateutil.parser.parse(event.find('date').text) + duration = parse_duration(event.find('duration').text) + end = start + duration + + persons = event.find('persons') + if persons is not None: + persons = persons.findall('person') + + parsed_events.append(dict( + start = start.astimezone(pytz.utc), + start_str = start.strftime('%H:%M'), + end_str = end.strftime('%H:%M'), + start_unix = to_unixtimestamp(start), + end_unix = to_unixtimestamp(end), + duration = int(duration.total_seconds() / 60), + title = text_or_empty(event, 'title'), + place = text_or_empty(event, 'room'), + speakers = [ + unicode(person.text.strip()) + for person in persons + ] if persons else [], + lang = text_or_empty(event, 'language') or "unk", + id = event.attrib["id"], + type = "talk", + )) + parsed_events.sort(key=itemgetter('start_unix')) + return parsed_events + + try: + resp = urllib2.urlopen(url) + schedule = resp.read() + events = load_events(schedule) + except Exception, err: + traceback.print_exc() + return False, None + + return True, events diff --git a/node.json b/node.json new file mode 100644 index 0000000..372b204 --- /dev/null +++ b/node.json @@ -0,0 +1,157 @@ +{ + "name": "Room Preview", + "permissions": { + "network": "Has to fetch schedule from remote site" + }, + "options": [{ + "title": "Appearance", + "type": "section" + }, { + "title": "Background color", + "ui_width": 4, + "name": "background_color", + "type": "color", + "default": [0,0,0,1] + }, { + "title": "Foreground color", + "ui_width": 4, + "name": "foreground_color", + "type": "color", + "default": [1,1,1,1] + }, { + "title": "Font", + "ui_width": 4, + "name": "font", + "type": "font", + "default": "silkscreen.ttf" + }, { + "title": "Background", + "ui_width": 4, + "name": "background", + "type": "resource", + "valid": ["image", "video", "child"], + "default": "empty.png" + }, { + "title": "Logo", + "ui_width": 4, + "name": "logo", + "doc_link": true, + "type": "resource", + "valid": ["image", "video", "child"], + "default": "empty.png" + }, + + { + "title": "Clock", + "type": "section" + }, { + "title": "Time Zone", + "ui_width": 4, + "name": "timezone", + "type": "select", + "hint": "The timezone", + "options": [ + ["UTC", "UTC"], + ["Europe/Berlin", "Europa / Berlin"] + ], + "default": "UTC" + }, + + { + "title": "Slides", + "type": "section" + }, { + "title": "Other Rooms (seconds)", + "ui_width": 4, + "name": "other_rooms", + "doc_link": true, + "type": "duration", + "hint": "How long to display other room information", + "default": 25 + }, { + "title": "Current Rooms (seconds)", + "ui_width": 4, + "name": "current_room", + "doc_link": true, + "type": "duration", + "hint": "How long to display current talk information", + "default": 15 + }, { + "title": "Room info (seconds)", + "ui_width": 4, + "name": "room_info", + "doc_link": true, + "type": "duration", + "hint": "How long to display room information", + "default": 10 + }, + + { + "title": "Scheduling", + "type": "section" + }, { + "title": "XML Fahrplan Url", + "ui_width": 10, + "name": "schedule_url", + "doc_link": true, + "type": "string", + "default": "http://localhost/" + }, { + "title": "Rooms", + "name": "rooms", + "doc_link": true, + "type": "list", + "itemname": "Room", + "hint": "Room definitions", + "items": [{ + "title": "Room Name", + "ui_width": 4, + "name": "name", + "doc_link": true, + "type": "string", + "default": "" + }, { + "title": "Room Short Name", + "ui_width": 4, + "name": "name_short", + "doc_link": true, + "type": "string", + "default": "" + }, { + "title": "Device Serial", + "ui_width": 4, + "name": "serial", + "doc_link": true, + "type": "string", + "default": "" + }, { + "title": "Dect", + "ui_width": 3, + "name": "dect", + "doc_link": true, + "type": "string", + "default": "" + }, { + "title": "Translation", + "ui_width": 3, + "name": "translation", + "doc_link": true, + "type": "string", + "default": "" + }, { + "title": "Hashtag", + "ui_width": 3, + "name": "hashtag", + "doc_link": true, + "type": "string", + "default": "" + }, { + "title": "IRC", + "ui_width": 3, + "name": "irc", + "doc_link": true, + "type": "string", + "default": "" + }] + }] +} diff --git a/node.lua b/node.lua new file mode 100644 index 0000000..75691d9 --- /dev/null +++ b/node.lua @@ -0,0 +1,410 @@ +gl.setup(1280, 720) + +sys.set_flag("slow_gc") + +local json = require "json" +local schedule +local current_room + +util.resource_loader{ + "progress.frag", +} + +local white = resource.create_colored_texture(1,1,1) + +util.file_watch("schedule.json", function(content) + print("reloading schedule") + schedule = json.decode(content) +end) + +local rooms +local spacer = white + +node.event("config_update", function(config) + rooms = {} + for idx, room in ipairs(config.rooms) do + if room.serial == sys.get_env("SERIAL") then + print("found my room") + current_room = room + end + rooms[room.name] = room + end + spacer = resource.create_colored_texture(CONFIG.foreground_color.rgba()) +end) + +hosted_init() + +local base_time = N.base_time or 0 +local current_talk +local all_talks = {} +local day = 0 + +function get_now() + return base_time + sys.now() +end + +function check_next_talk() + local now = get_now() + local room_next = {} + for idx, talk in ipairs(schedule) do + if rooms[talk.place] and not room_next[talk.place] and talk.start_unix + 25 * 60 > now then + room_next[talk.place] = talk + end + end + + for room, talk in pairs(room_next) do + talk.slide_lines = wrap(talk.title, 30) + + if #talk.title > 25 then + talk.lines = wrap(talk.title, 60) + if #talk.lines == 1 then + talk.lines[2] = table.concat(talk.speakers, ", ") + end + end + end + + if room_next[current_room.name] then + current_talk = room_next[current_room.name] + else + current_talk = nil + end + + all_talks = {} + for room, talk in pairs(room_next) do + if current_talk and room ~= current_talk.place then + all_talks[#all_talks + 1] = talk + end + end + table.sort(all_talks, function(a, b) + if a.start_unix < b.start_unix then + return true + elseif a.start_unix > b.start_unix then + return false + else + return a.place < b.place + end + end) +end + +function wrap(str, limit, indent, indent1) + limit = limit or 72 + local here = 1 + local wrapped = str:gsub("(%s+)()(%S+)()", function(sp, st, word, fi) + if fi-here > limit then + here = st + return "\n"..word + end + end) + local splitted = {} + for token in string.gmatch(wrapped, "[^\n]+") do + splitted[#splitted + 1] = token + end + return splitted +end + +local clock = (function() + local base_time = N.base_time or 0 + + local function set(time) + base_time = tonumber(time) - sys.now() + end + + util.data_mapper{ + ["clock/midnight"] = function(since_midnight) + print("NEW midnight", since_midnight) + set(since_midnight) + end; + } + + local function get() + local time = (base_time + sys.now()) % 86400 + return string.format("%d:%02d", math.floor(time / 3600), math.floor(time % 3600 / 60)) + end + + return { + get = get; + set = set; + } +end)() + +util.data_mapper{ + ["clock/set"] = function(time) + base_time = tonumber(time) - sys.now() + N.base_time = base_time + check_next_talk() + print("UPDATED TIME", base_time) + end; + ["clock/day"] = function(new_day) + day = new_day + print("UPDATED DAY", new_day) + end; +} + +function switcher(get_screens) + local current_idx = 0 + local current + local current_state + + local switch = sys.now() + local switched = sys.now() + + local blend = 0.8 + local mode = "switch" + + local old_screen + local current_screen + + local screens = get_screens() + + local function prepare() + local now = sys.now() + if now > switch and mode == "show" then + mode = "switch" + switched = now + + -- snapshot old screen + gl.clear(CONFIG.background_color.rgb_with_a(0.0)) + if current then + current.draw(current_state) + end + old_screen = resource.create_snapshot() + + -- find next screen + current_idx = current_idx + 1 + if current_idx > #screens then + screens = get_screens() + current_idx = 1 + end + current = screens[current_idx] + switch = now + current.time + current_state = current.prepare() + + -- snapshot next screen + gl.clear(CONFIG.background_color.rgb_with_a(0.0)) + current.draw(current_state) + current_screen = resource.create_snapshot() + elseif now - switched > blend and mode == "switch" then + if current_screen then + current_screen:dispose() + end + if old_screen then + old_screen:dispose() + end + current_screen = nil + old_screen = nil + mode = "show" + end + end + + local function draw() + local now = sys.now() + + local percent = ((now - switched) / (switch - switched)) * 3.14129 * 2 - 3.14129 + progress:use{percent = percent} + white:draw(WIDTH-50, HEIGHT-50, WIDTH-10, HEIGHT-10) + progress:deactivate() + + if mode == "switch" then + local progress = (now - switched) / blend + gl.pushMatrix() + gl.translate(WIDTH/2, 0) + if progress < 0.5 then + gl.rotate(180 * progress, 0, 1, 0) + gl.translate(-WIDTH/2, 0) + old_screen:draw(0, 0, WIDTH, HEIGHT) + else + gl.rotate(180 + 180 * progress, 0, 1, 0) + gl.translate(-WIDTH/2, 0) + current_screen:draw(0, 0, WIDTH, HEIGHT) + end + gl.popMatrix() + else + current.draw(current_state) + end + end + return { + prepare = prepare; + draw = draw; + } +end + +local content = switcher(function() + return {{ + time = CONFIG.other_rooms, + prepare = function() + local content = {} + + local function add_content(func) + content[#content+1] = func + end + + local function mk_spacer(y) + return function() + spacer:draw(0, y, WIDTH, y+2, 0.6) + end + end + + local function mk_talkmulti(y, talk, is_running) + local alpha + if is_running then + alpha = 0.5 + else + alpha = 1.0 + end + + local line_idx = 999999 + local top_line + local bottom_line + local function next_line() + line_idx = line_idx + 1 + if line_idx > #talk.lines then + line_idx = 2 + top_line = talk.lines[1] + bottom_line = talk.lines[2] or "" + else + top_line = bottom_line + bottom_line = talk.lines[line_idx] + end + end + + next_line() + + local switch = sys.now() + 3 + + return function() + CONFIG.font:write(30, y, talk.start_str, 50, CONFIG.foreground_color.rgb_with_a(alpha)) + CONFIG.font:write(190, y, rooms[talk.place].name_short, 50, CONFIG.foreground_color.rgb_with_a(alpha)) + CONFIG.font:write(400, y, top_line, 30, CONFIG.foreground_color.rgb_with_a(alpha)) + CONFIG.font:write(400, y+28, bottom_line, 30, CONFIG.foreground_color.rgb_with_a(alpha*0.6)) + + if sys.now() > switch then + next_line() + switch = sys.now() + 1 + end + end + end + + local function mk_talk(y, talk, is_running) + local alpha + if is_running then + alpha = 0.5 + else + alpha = 1.0 + end + + return function() + CONFIG.font:write(30, y, talk.start_str, 50, CONFIG.foreground_color.rgb_with_a(alpha)) + CONFIG.font:write(190, y, rooms[talk.place].name_short, 50, CONFIG.foreground_color.rgb_with_a(alpha)) + CONFIG.font:write(400, y, talk.title, 50, CONFIG.foreground_color.rgb_with_a(alpha)) + end + end + + local y = 300 + local time_sep = false + if #all_talks > 0 then + for idx, talk in ipairs(all_talks) do + if not time_sep and talk.start_unix > get_now() then + if idx > 1 then + y = y + 5 + add_content(mk_spacer(y)) + y = y + 20 + end + time_sep = true + end + if talk.lines then + add_content(mk_talkmulti(y, talk, not time_sep)) + else + add_content(mk_talk(y, talk, not time_sep)) + end + y = y + 62 + end + else + CONFIG.font:write(400, 330, "No other talks.", 50, CONFIG.foreground_color.rgba()) + end + + return content + end; + draw = function(content) + CONFIG.font:write(400, 180, "Other talks", 80, CONFIG.foreground_color.rgba()) + spacer:draw(0, 280, WIDTH, 282, 0.6) + for _, func in ipairs(content) do + func() + end + end + }, { + time = CONFIG.current_room, + prepare = function() + end; + draw = function() + if not current_talk then + CONFIG.font:write(400, 180, "Next talk", 80, CONFIG.foreground_color.rgba()) + spacer:draw(0, 300, WIDTH, 302, 0.6) + CONFIG.font:write(400, 310, "Nope. That's it.", 50, CONFIG.foreground_color.rgba()) + else + local delta = current_talk.start_unix - get_now() + if delta > 0 then + CONFIG.font:write(400, 180, "Next talk", 80, CONFIG.foreground_color.rgba()) + else + CONFIG.font:write(400, 180, "This talk", 80, CONFIG.foreground_color.rgba()) + end + spacer:draw(0, 280, WIDTH, 282, 0.6) + + CONFIG.font:write(130, 310, current_talk.start_str, 50, CONFIG.foreground_color.rgba()) + if delta > 180*60 then + CONFIG.font:write(130, 310 + 60, string.format("in %d h", math.floor(delta/3660)+1), 50, CONFIG.foreground_color.rgb_with_a(0.6)) + elseif delta > 0 then + CONFIG.font:write(130, 310 + 60, string.format("in %d min", math.floor(delta/60)+1), 50, CONFIG.foreground_color.rgb_with_a(0.6)) + end + for idx, line in ipairs(current_talk.slide_lines) do + if idx >= 5 then + break + end + CONFIG.font:write(400, 310 - 60 + 60 * idx, line, 50, CONFIG.foreground_color.rgba()) + end + for i, speaker in ipairs(current_talk.speakers) do + CONFIG.font:write(400, 490 + 50 * i, speaker, 50, CONFIG.foreground_color.rgb_with_a(0.6)) + end + end + end + }, { + time = CONFIG.room_info, + prepare = function() + end; + draw = function(t) + CONFIG.font:write(400, 180, "Room information", 80, CONFIG.foreground_color.rgba()) + spacer:draw(0, 280, WIDTH, 282, 0.6) + CONFIG.font:write(30, 300, "Audio", 50, CONFIG.foreground_color.rgba()) + CONFIG.font:write(400, 300, "Dial " .. current_room.dect, 50, CONFIG.foreground_color.rgba()) + + CONFIG.font:write(30, 360, "Translation", 50, CONFIG.foreground_color.rgba()) + CONFIG.font:write(400, 360, "Dial " .. current_room.translation, 50, CONFIG.foreground_color.rgba()) + + CONFIG.font:write(30, 460, "IRC", 50, CONFIG.foreground_color.rgba()) + CONFIG.font:write(400, 460, current_room.irc, 50, CONFIG.foreground_color.rgba()) + + CONFIG.font:write(30, 520, "Hashtag", 50, CONFIG.foreground_color.rgba()) + CONFIG.font:write(400, 520, current_room.hashtag, 50, CONFIG.foreground_color.rgba()) + end + }} +end) + +function node.render() + if base_time == 0 then + return + end + + content.prepare() + + CONFIG.background_color.clear() + CONFIG.background.ensure_loaded():draw(0, 0, WIDTH, HEIGHT) + + util.draw_correct(CONFIG.logo.ensure_loaded(), 20, 20, 300, 120) + CONFIG.font:write(400, 20, current_room.name_short, 100, CONFIG.foreground_color.rgba()) + CONFIG.font:write(850, 20, clock.get(), 100, CONFIG.foreground_color.rgba()) + -- font:write(WIDTH-300, 20, string.format("Day %d", day), 100, CONFIG.foreground_color.rgba()) + + local fov = math.atan2(HEIGHT, WIDTH*2) * 360 / math.pi + gl.perspective(fov, WIDTH/2, HEIGHT/2, -WIDTH, + WIDTH/2, HEIGHT/2, 0) + content.draw() +end diff --git a/package.json b/package.json new file mode 100644 index 0000000..c94eb6d --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "name": "Conference Room Announcement", + "author": "fw@dividuum.de", + "desc": "Show information about upcoming talks" +} diff --git a/package.png b/package.png new file mode 100644 index 0000000000000000000000000000000000000000..c69c168e5ddc0225248f9a920cbf5b2fc248d0a8 GIT binary patch literal 2097 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 literal 0 HcmV?d00001 diff --git a/service b/service new file mode 100755 index 0000000..882fe41 --- /dev/null +++ b/service @@ -0,0 +1,76 @@ +#!/usr/bin/python +import os +import sys +import time +import pytz +import json +from calendar import timegm +from datetime import timedelta, datetime +from operator import itemgetter + +from hosted import CONFIG, NODE, UPSTREAM +import importer + +CONFIG.restart_on_update() + +diff = datetime(2014,7,22,8,20,00) - datetime.utcnow() + +def current_time(): + now = datetime.utcnow() # + diff + timestamp = timegm(now.timetuple()) + now.microsecond / 1000000. + return now, timestamp + +def send_clock(now, ts): + now = now.replace(tzinfo=pytz.utc) + now = now.astimezone(pytz.timezone(CONFIG['timezone'])) + now = now.replace(tzinfo=None) + since_midnight = ( + now - + now.replace(hour=0, minute=0, second=0, microsecond=0) + ) + since_midnight = since_midnight.seconds + since_midnight.microseconds / 1000000. + NODE.send('/clock/set:%f' % ts) + NODE.send('/clock/midnight:%f' % since_midnight) + +def main(): + while 1: + now, ts = current_time() + if now.year < 2000: + print >>sys.stderr, "too soon" + time.sleep(1) + continue + + has_events, events = importer.get_schedule(CONFIG['schedule_url']) + + if not has_events: + print >>sys.stderr, "no events" + time.sleep(60) + continue + + with file("schedule.json.new", "wb") as f: + f.write(json.dumps([dict( + duration = event['duration'], + lang = event['lang'], + place = event['place'], + speakers = event['speakers'], + start_str = event['start_str'], + title = event['title'], + type = event['type'], + start_unix = event['start_unix'], + end_unix = event['end_unix'], + ) for event in sorted(events, key=itemgetter('start_unix'))], + ensure_ascii=False, + separators=(',',':') + ).encode('utf8')) + os.rename("schedule.json.new", "schedule.json") + + print >>sys.stderr, "updated schedule" + + for i in xrange(60): + now, ts = current_time() + print >>sys.stderr, "time is", now, ts + send_clock(now, ts) + time.sleep(10) + +if __name__ == "__main__": + main() diff --git a/silkscreen.ttf b/silkscreen.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e2dd974b1e31e1e50e0fa939fd0123bee9f95d80 GIT binary patch literal 18336 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 IJ^ax8FU~ke7ytkO literal 0 HcmV?d00001 diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..eaf9c0d --- /dev/null +++ b/utils.py @@ -0,0 +1,36 @@ +import re +import unicodedata +from itertools import tee, izip, cycle, islice + +def pairwise(iterable): + "s -> (s0,s1), (s1,s2), (s2, s3), ..." + a, b = tee(iterable) + next(b, None) + return izip(a, b) + +def roundrobin(*iterables): + "roundrobin('ABC', 'D', 'EF') --> A D E B F C" + # Recipe credited to George Sakkis + pending = len(iterables) + nexts = cycle(iter(it).next for it in iterables) + while pending: + try: + for next in nexts: + yield next() + except StopIteration: + pending -= 1 + nexts = cycle(islice(nexts, pending)) + +def slugify(s, l=80): + slug = unicodedata.normalize('NFKD', s) + slug = slug.encode('ascii', 'ignore').lower() + slug = re.sub(r'[^a-z0-9]+', '-', slug).strip('-') + slug = re.sub(r'[-]+', '-', slug) + return slug[:l] + +def separate(items, predicate): + a, b = [], [] + for item in items: + (a, b)[int(bool(predicate(item)))].append(item) + return a, b +