├── .gitignore ├── BeautifulSoup.py ├── README.md ├── app.sample.yaml ├── dateutil ├── __init__.py ├── easter.py ├── parser.py ├── relativedelta.py ├── rrule.py ├── tz.py ├── tzwin.py └── zoneinfo │ ├── __init__.py │ └── zoneinfo-2010g.tar.gz ├── importers ├── __init__.py ├── delicious-html.py ├── delicious-v1.py └── delicious-xml.py ├── index.yaml ├── main.py ├── settings.sample.py ├── static ├── css │ └── style.css ├── favicon.ico ├── images │ ├── logo.png │ └── logo.svg └── js │ └── jquery-1.4.4.min.js ├── templates ├── archive.html ├── base.html ├── entry.html ├── form.html ├── home.html ├── import.html ├── modules │ └── entry.html └── tag.html └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | tornado 2 | app.yaml 3 | settings.py 4 | *.pyc 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | S E L F I C I O U S 2 | =================== 3 | 4 | This is a _quick n' dirty_ app written _very_ quickly to backup my delicious 5 | bookmarks and manage my future ones. 6 | 7 | More details, soon -I hope :) 8 | 9 | INSTALLATION 10 | ============ 11 | 12 | Super EASY! 13 | 14 | 0) Get your copy of tornado at: 15 | https://github.com/facebook/tornado 16 | And copy the folder tornado/todnado into the project's directory. 17 | 18 | 1) Copy app.sample.yaml to app.yaml 19 | 2) Copy settings.sample.py to settings.py 20 | 21 | 3) Edit the file app.yaml: 22 | Change the application name 23 | 24 | Example: 25 | 26 | application: mybookmarks (the one you reserved on appengine) 27 | version: 1 28 | runtime: python 29 | api_version: 1 30 | 31 | 32 | 4) Edit settings.py with your site title (Example: My bookmarks extraordinaires!) 33 | 34 | ATTENTION 35 | ========= 36 | 37 | This app was made in a hurry for fun. There are probably things to imporve 38 | and/or fix. Your feedback is welcome! 39 | 40 | Yuuta: http://twitter.com/initpy -- http://big.appspot.com 41 | 42 | Yes. You should follow me for updates and other cool stuff and geekeries :) 43 | -------------------------------------------------------------------------------- /app.sample.yaml: -------------------------------------------------------------------------------- 1 | application: selficious 2 | version: 1 3 | runtime: python 4 | api_version: 1 5 | 6 | handlers: 7 | - url: /static/ 8 | static_dir: static 9 | 10 | - url: /robots\.txt 11 | static_files: static/robots.txt 12 | upload: static/robots.txt 13 | 14 | - url: /favicon\.ico 15 | static_files: static/favicon.ico 16 | upload: static/favicon.ico 17 | 18 | - url: /.* 19 | script: main.py 20 | -------------------------------------------------------------------------------- /dateutil/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2003-2010 Gustavo Niemeyer 3 | 4 | This module offers extensions to the standard python 2.3+ 5 | datetime module. 6 | """ 7 | __author__ = "Gustavo Niemeyer " 8 | __license__ = "PSF License" 9 | __version__ = "1.5" 10 | -------------------------------------------------------------------------------- /dateutil/easter.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2003-2007 Gustavo Niemeyer 3 | 4 | This module offers extensions to the standard python 2.3+ 5 | datetime module. 6 | """ 7 | __author__ = "Gustavo Niemeyer " 8 | __license__ = "PSF License" 9 | 10 | import datetime 11 | 12 | __all__ = ["easter", "EASTER_JULIAN", "EASTER_ORTHODOX", "EASTER_WESTERN"] 13 | 14 | EASTER_JULIAN = 1 15 | EASTER_ORTHODOX = 2 16 | EASTER_WESTERN = 3 17 | 18 | def easter(year, method=EASTER_WESTERN): 19 | """ 20 | This method was ported from the work done by GM Arts, 21 | on top of the algorithm by Claus Tondering, which was 22 | based in part on the algorithm of Ouding (1940), as 23 | quoted in "Explanatory Supplement to the Astronomical 24 | Almanac", P. Kenneth Seidelmann, editor. 25 | 26 | This algorithm implements three different easter 27 | calculation methods: 28 | 29 | 1 - Original calculation in Julian calendar, valid in 30 | dates after 326 AD 31 | 2 - Original method, with date converted to Gregorian 32 | calendar, valid in years 1583 to 4099 33 | 3 - Revised method, in Gregorian calendar, valid in 34 | years 1583 to 4099 as well 35 | 36 | These methods are represented by the constants: 37 | 38 | EASTER_JULIAN = 1 39 | EASTER_ORTHODOX = 2 40 | EASTER_WESTERN = 3 41 | 42 | The default method is method 3. 43 | 44 | More about the algorithm may be found at: 45 | 46 | http://users.chariot.net.au/~gmarts/eastalg.htm 47 | 48 | and 49 | 50 | http://www.tondering.dk/claus/calendar.html 51 | 52 | """ 53 | 54 | if not (1 <= method <= 3): 55 | raise ValueError, "invalid method" 56 | 57 | # g - Golden year - 1 58 | # c - Century 59 | # h - (23 - Epact) mod 30 60 | # i - Number of days from March 21 to Paschal Full Moon 61 | # j - Weekday for PFM (0=Sunday, etc) 62 | # p - Number of days from March 21 to Sunday on or before PFM 63 | # (-6 to 28 methods 1 & 3, to 56 for method 2) 64 | # e - Extra days to add for method 2 (converting Julian 65 | # date to Gregorian date) 66 | 67 | y = year 68 | g = y % 19 69 | e = 0 70 | if method < 3: 71 | # Old method 72 | i = (19*g+15)%30 73 | j = (y+y//4+i)%7 74 | if method == 2: 75 | # Extra dates to convert Julian to Gregorian date 76 | e = 10 77 | if y > 1600: 78 | e = e+y//100-16-(y//100-16)//4 79 | else: 80 | # New method 81 | c = y//100 82 | h = (c-c//4-(8*c+13)//25+19*g+15)%30 83 | i = h-(h//28)*(1-(h//28)*(29//(h+1))*((21-g)//11)) 84 | j = (y+y//4+i+2-c+c//4)%7 85 | 86 | # p can be from -6 to 56 corresponding to dates 22 March to 23 May 87 | # (later dates apply to method 2, although 23 May never actually occurs) 88 | p = i-j+e 89 | d = 1+(p+27+(p+6)//40)%31 90 | m = 3+(p+26)//30 91 | return datetime.date(int(y),int(m),int(d)) 92 | 93 | -------------------------------------------------------------------------------- /dateutil/parser.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/initpy/selficious/4f57f00a36b40c71ffab031bfc41b2a17a7b939f/dateutil/parser.py -------------------------------------------------------------------------------- /dateutil/relativedelta.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2003-2010 Gustavo Niemeyer 3 | 4 | This module offers extensions to the standard python 2.3+ 5 | datetime module. 6 | """ 7 | __author__ = "Gustavo Niemeyer " 8 | __license__ = "PSF License" 9 | 10 | import datetime 11 | import calendar 12 | 13 | __all__ = ["relativedelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"] 14 | 15 | class weekday(object): 16 | __slots__ = ["weekday", "n"] 17 | 18 | def __init__(self, weekday, n=None): 19 | self.weekday = weekday 20 | self.n = n 21 | 22 | def __call__(self, n): 23 | if n == self.n: 24 | return self 25 | else: 26 | return self.__class__(self.weekday, n) 27 | 28 | def __eq__(self, other): 29 | try: 30 | if self.weekday != other.weekday or self.n != other.n: 31 | return False 32 | except AttributeError: 33 | return False 34 | return True 35 | 36 | def __repr__(self): 37 | s = ("MO", "TU", "WE", "TH", "FR", "SA", "SU")[self.weekday] 38 | if not self.n: 39 | return s 40 | else: 41 | return "%s(%+d)" % (s, self.n) 42 | 43 | MO, TU, WE, TH, FR, SA, SU = weekdays = tuple([weekday(x) for x in range(7)]) 44 | 45 | class relativedelta: 46 | """ 47 | The relativedelta type is based on the specification of the excelent 48 | work done by M.-A. Lemburg in his mx.DateTime extension. However, 49 | notice that this type does *NOT* implement the same algorithm as 50 | his work. Do *NOT* expect it to behave like mx.DateTime's counterpart. 51 | 52 | There's two different ways to build a relativedelta instance. The 53 | first one is passing it two date/datetime classes: 54 | 55 | relativedelta(datetime1, datetime2) 56 | 57 | And the other way is to use the following keyword arguments: 58 | 59 | year, month, day, hour, minute, second, microsecond: 60 | Absolute information. 61 | 62 | years, months, weeks, days, hours, minutes, seconds, microseconds: 63 | Relative information, may be negative. 64 | 65 | weekday: 66 | One of the weekday instances (MO, TU, etc). These instances may 67 | receive a parameter N, specifying the Nth weekday, which could 68 | be positive or negative (like MO(+1) or MO(-2). Not specifying 69 | it is the same as specifying +1. You can also use an integer, 70 | where 0=MO. 71 | 72 | leapdays: 73 | Will add given days to the date found, if year is a leap 74 | year, and the date found is post 28 of february. 75 | 76 | yearday, nlyearday: 77 | Set the yearday or the non-leap year day (jump leap days). 78 | These are converted to day/month/leapdays information. 79 | 80 | Here is the behavior of operations with relativedelta: 81 | 82 | 1) Calculate the absolute year, using the 'year' argument, or the 83 | original datetime year, if the argument is not present. 84 | 85 | 2) Add the relative 'years' argument to the absolute year. 86 | 87 | 3) Do steps 1 and 2 for month/months. 88 | 89 | 4) Calculate the absolute day, using the 'day' argument, or the 90 | original datetime day, if the argument is not present. Then, 91 | subtract from the day until it fits in the year and month 92 | found after their operations. 93 | 94 | 5) Add the relative 'days' argument to the absolute day. Notice 95 | that the 'weeks' argument is multiplied by 7 and added to 96 | 'days'. 97 | 98 | 6) Do steps 1 and 2 for hour/hours, minute/minutes, second/seconds, 99 | microsecond/microseconds. 100 | 101 | 7) If the 'weekday' argument is present, calculate the weekday, 102 | with the given (wday, nth) tuple. wday is the index of the 103 | weekday (0-6, 0=Mon), and nth is the number of weeks to add 104 | forward or backward, depending on its signal. Notice that if 105 | the calculated date is already Monday, for example, using 106 | (0, 1) or (0, -1) won't change the day. 107 | """ 108 | 109 | def __init__(self, dt1=None, dt2=None, 110 | years=0, months=0, days=0, leapdays=0, weeks=0, 111 | hours=0, minutes=0, seconds=0, microseconds=0, 112 | year=None, month=None, day=None, weekday=None, 113 | yearday=None, nlyearday=None, 114 | hour=None, minute=None, second=None, microsecond=None): 115 | if dt1 and dt2: 116 | if not isinstance(dt1, datetime.date) or \ 117 | not isinstance(dt2, datetime.date): 118 | raise TypeError, "relativedelta only diffs datetime/date" 119 | if type(dt1) is not type(dt2): 120 | if not isinstance(dt1, datetime.datetime): 121 | dt1 = datetime.datetime.fromordinal(dt1.toordinal()) 122 | elif not isinstance(dt2, datetime.datetime): 123 | dt2 = datetime.datetime.fromordinal(dt2.toordinal()) 124 | self.years = 0 125 | self.months = 0 126 | self.days = 0 127 | self.leapdays = 0 128 | self.hours = 0 129 | self.minutes = 0 130 | self.seconds = 0 131 | self.microseconds = 0 132 | self.year = None 133 | self.month = None 134 | self.day = None 135 | self.weekday = None 136 | self.hour = None 137 | self.minute = None 138 | self.second = None 139 | self.microsecond = None 140 | self._has_time = 0 141 | 142 | months = (dt1.year*12+dt1.month)-(dt2.year*12+dt2.month) 143 | self._set_months(months) 144 | dtm = self.__radd__(dt2) 145 | if dt1 < dt2: 146 | while dt1 > dtm: 147 | months += 1 148 | self._set_months(months) 149 | dtm = self.__radd__(dt2) 150 | else: 151 | while dt1 < dtm: 152 | months -= 1 153 | self._set_months(months) 154 | dtm = self.__radd__(dt2) 155 | delta = dt1 - dtm 156 | self.seconds = delta.seconds+delta.days*86400 157 | self.microseconds = delta.microseconds 158 | else: 159 | self.years = years 160 | self.months = months 161 | self.days = days+weeks*7 162 | self.leapdays = leapdays 163 | self.hours = hours 164 | self.minutes = minutes 165 | self.seconds = seconds 166 | self.microseconds = microseconds 167 | self.year = year 168 | self.month = month 169 | self.day = day 170 | self.hour = hour 171 | self.minute = minute 172 | self.second = second 173 | self.microsecond = microsecond 174 | 175 | if type(weekday) is int: 176 | self.weekday = weekdays[weekday] 177 | else: 178 | self.weekday = weekday 179 | 180 | yday = 0 181 | if nlyearday: 182 | yday = nlyearday 183 | elif yearday: 184 | yday = yearday 185 | if yearday > 59: 186 | self.leapdays = -1 187 | if yday: 188 | ydayidx = [31,59,90,120,151,181,212,243,273,304,334,366] 189 | for idx, ydays in enumerate(ydayidx): 190 | if yday <= ydays: 191 | self.month = idx+1 192 | if idx == 0: 193 | self.day = yday 194 | else: 195 | self.day = yday-ydayidx[idx-1] 196 | break 197 | else: 198 | raise ValueError, "invalid year day (%d)" % yday 199 | 200 | self._fix() 201 | 202 | def _fix(self): 203 | if abs(self.microseconds) > 999999: 204 | s = self.microseconds//abs(self.microseconds) 205 | div, mod = divmod(self.microseconds*s, 1000000) 206 | self.microseconds = mod*s 207 | self.seconds += div*s 208 | if abs(self.seconds) > 59: 209 | s = self.seconds//abs(self.seconds) 210 | div, mod = divmod(self.seconds*s, 60) 211 | self.seconds = mod*s 212 | self.minutes += div*s 213 | if abs(self.minutes) > 59: 214 | s = self.minutes//abs(self.minutes) 215 | div, mod = divmod(self.minutes*s, 60) 216 | self.minutes = mod*s 217 | self.hours += div*s 218 | if abs(self.hours) > 23: 219 | s = self.hours//abs(self.hours) 220 | div, mod = divmod(self.hours*s, 24) 221 | self.hours = mod*s 222 | self.days += div*s 223 | if abs(self.months) > 11: 224 | s = self.months//abs(self.months) 225 | div, mod = divmod(self.months*s, 12) 226 | self.months = mod*s 227 | self.years += div*s 228 | if (self.hours or self.minutes or self.seconds or self.microseconds or 229 | self.hour is not None or self.minute is not None or 230 | self.second is not None or self.microsecond is not None): 231 | self._has_time = 1 232 | else: 233 | self._has_time = 0 234 | 235 | def _set_months(self, months): 236 | self.months = months 237 | if abs(self.months) > 11: 238 | s = self.months//abs(self.months) 239 | div, mod = divmod(self.months*s, 12) 240 | self.months = mod*s 241 | self.years = div*s 242 | else: 243 | self.years = 0 244 | 245 | def __radd__(self, other): 246 | if not isinstance(other, datetime.date): 247 | raise TypeError, "unsupported type for add operation" 248 | elif self._has_time and not isinstance(other, datetime.datetime): 249 | other = datetime.datetime.fromordinal(other.toordinal()) 250 | year = (self.year or other.year)+self.years 251 | month = self.month or other.month 252 | if self.months: 253 | assert 1 <= abs(self.months) <= 12 254 | month += self.months 255 | if month > 12: 256 | year += 1 257 | month -= 12 258 | elif month < 1: 259 | year -= 1 260 | month += 12 261 | day = min(calendar.monthrange(year, month)[1], 262 | self.day or other.day) 263 | repl = {"year": year, "month": month, "day": day} 264 | for attr in ["hour", "minute", "second", "microsecond"]: 265 | value = getattr(self, attr) 266 | if value is not None: 267 | repl[attr] = value 268 | days = self.days 269 | if self.leapdays and month > 2 and calendar.isleap(year): 270 | days += self.leapdays 271 | ret = (other.replace(**repl) 272 | + datetime.timedelta(days=days, 273 | hours=self.hours, 274 | minutes=self.minutes, 275 | seconds=self.seconds, 276 | microseconds=self.microseconds)) 277 | if self.weekday: 278 | weekday, nth = self.weekday.weekday, self.weekday.n or 1 279 | jumpdays = (abs(nth)-1)*7 280 | if nth > 0: 281 | jumpdays += (7-ret.weekday()+weekday)%7 282 | else: 283 | jumpdays += (ret.weekday()-weekday)%7 284 | jumpdays *= -1 285 | ret += datetime.timedelta(days=jumpdays) 286 | return ret 287 | 288 | def __rsub__(self, other): 289 | return self.__neg__().__radd__(other) 290 | 291 | def __add__(self, other): 292 | if not isinstance(other, relativedelta): 293 | raise TypeError, "unsupported type for add operation" 294 | return relativedelta(years=other.years+self.years, 295 | months=other.months+self.months, 296 | days=other.days+self.days, 297 | hours=other.hours+self.hours, 298 | minutes=other.minutes+self.minutes, 299 | seconds=other.seconds+self.seconds, 300 | microseconds=other.microseconds+self.microseconds, 301 | leapdays=other.leapdays or self.leapdays, 302 | year=other.year or self.year, 303 | month=other.month or self.month, 304 | day=other.day or self.day, 305 | weekday=other.weekday or self.weekday, 306 | hour=other.hour or self.hour, 307 | minute=other.minute or self.minute, 308 | second=other.second or self.second, 309 | microsecond=other.second or self.microsecond) 310 | 311 | def __sub__(self, other): 312 | if not isinstance(other, relativedelta): 313 | raise TypeError, "unsupported type for sub operation" 314 | return relativedelta(years=other.years-self.years, 315 | months=other.months-self.months, 316 | days=other.days-self.days, 317 | hours=other.hours-self.hours, 318 | minutes=other.minutes-self.minutes, 319 | seconds=other.seconds-self.seconds, 320 | microseconds=other.microseconds-self.microseconds, 321 | leapdays=other.leapdays or self.leapdays, 322 | year=other.year or self.year, 323 | month=other.month or self.month, 324 | day=other.day or self.day, 325 | weekday=other.weekday or self.weekday, 326 | hour=other.hour or self.hour, 327 | minute=other.minute or self.minute, 328 | second=other.second or self.second, 329 | microsecond=other.second or self.microsecond) 330 | 331 | def __neg__(self): 332 | return relativedelta(years=-self.years, 333 | months=-self.months, 334 | days=-self.days, 335 | hours=-self.hours, 336 | minutes=-self.minutes, 337 | seconds=-self.seconds, 338 | microseconds=-self.microseconds, 339 | leapdays=self.leapdays, 340 | year=self.year, 341 | month=self.month, 342 | day=self.day, 343 | weekday=self.weekday, 344 | hour=self.hour, 345 | minute=self.minute, 346 | second=self.second, 347 | microsecond=self.microsecond) 348 | 349 | def __nonzero__(self): 350 | return not (not self.years and 351 | not self.months and 352 | not self.days and 353 | not self.hours and 354 | not self.minutes and 355 | not self.seconds and 356 | not self.microseconds and 357 | not self.leapdays and 358 | self.year is None and 359 | self.month is None and 360 | self.day is None and 361 | self.weekday is None and 362 | self.hour is None and 363 | self.minute is None and 364 | self.second is None and 365 | self.microsecond is None) 366 | 367 | def __mul__(self, other): 368 | f = float(other) 369 | return relativedelta(years=self.years*f, 370 | months=self.months*f, 371 | days=self.days*f, 372 | hours=self.hours*f, 373 | minutes=self.minutes*f, 374 | seconds=self.seconds*f, 375 | microseconds=self.microseconds*f, 376 | leapdays=self.leapdays, 377 | year=self.year, 378 | month=self.month, 379 | day=self.day, 380 | weekday=self.weekday, 381 | hour=self.hour, 382 | minute=self.minute, 383 | second=self.second, 384 | microsecond=self.microsecond) 385 | 386 | def __eq__(self, other): 387 | if not isinstance(other, relativedelta): 388 | return False 389 | if self.weekday or other.weekday: 390 | if not self.weekday or not other.weekday: 391 | return False 392 | if self.weekday.weekday != other.weekday.weekday: 393 | return False 394 | n1, n2 = self.weekday.n, other.weekday.n 395 | if n1 != n2 and not ((not n1 or n1 == 1) and (not n2 or n2 == 1)): 396 | return False 397 | return (self.years == other.years and 398 | self.months == other.months and 399 | self.days == other.days and 400 | self.hours == other.hours and 401 | self.minutes == other.minutes and 402 | self.seconds == other.seconds and 403 | self.leapdays == other.leapdays and 404 | self.year == other.year and 405 | self.month == other.month and 406 | self.day == other.day and 407 | self.hour == other.hour and 408 | self.minute == other.minute and 409 | self.second == other.second and 410 | self.microsecond == other.microsecond) 411 | 412 | def __ne__(self, other): 413 | return not self.__eq__(other) 414 | 415 | def __div__(self, other): 416 | return self.__mul__(1/float(other)) 417 | 418 | def __repr__(self): 419 | l = [] 420 | for attr in ["years", "months", "days", "leapdays", 421 | "hours", "minutes", "seconds", "microseconds"]: 422 | value = getattr(self, attr) 423 | if value: 424 | l.append("%s=%+d" % (attr, value)) 425 | for attr in ["year", "month", "day", "weekday", 426 | "hour", "minute", "second", "microsecond"]: 427 | value = getattr(self, attr) 428 | if value is not None: 429 | l.append("%s=%s" % (attr, `value`)) 430 | return "%s(%s)" % (self.__class__.__name__, ", ".join(l)) 431 | 432 | # vim:ts=4:sw=4:et 433 | -------------------------------------------------------------------------------- /dateutil/rrule.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2003-2010 Gustavo Niemeyer 3 | 4 | This module offers extensions to the standard python 2.3+ 5 | datetime module. 6 | """ 7 | __author__ = "Gustavo Niemeyer " 8 | __license__ = "PSF License" 9 | 10 | import itertools 11 | import datetime 12 | import calendar 13 | import thread 14 | import sys 15 | 16 | __all__ = ["rrule", "rruleset", "rrulestr", 17 | "YEARLY", "MONTHLY", "WEEKLY", "DAILY", 18 | "HOURLY", "MINUTELY", "SECONDLY", 19 | "MO", "TU", "WE", "TH", "FR", "SA", "SU"] 20 | 21 | # Every mask is 7 days longer to handle cross-year weekly periods. 22 | M366MASK = tuple([1]*31+[2]*29+[3]*31+[4]*30+[5]*31+[6]*30+ 23 | [7]*31+[8]*31+[9]*30+[10]*31+[11]*30+[12]*31+[1]*7) 24 | M365MASK = list(M366MASK) 25 | M29, M30, M31 = range(1,30), range(1,31), range(1,32) 26 | MDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7]) 27 | MDAY365MASK = list(MDAY366MASK) 28 | M29, M30, M31 = range(-29,0), range(-30,0), range(-31,0) 29 | NMDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7]) 30 | NMDAY365MASK = list(NMDAY366MASK) 31 | M366RANGE = (0,31,60,91,121,152,182,213,244,274,305,335,366) 32 | M365RANGE = (0,31,59,90,120,151,181,212,243,273,304,334,365) 33 | WDAYMASK = [0,1,2,3,4,5,6]*55 34 | del M29, M30, M31, M365MASK[59], MDAY365MASK[59], NMDAY365MASK[31] 35 | MDAY365MASK = tuple(MDAY365MASK) 36 | M365MASK = tuple(M365MASK) 37 | 38 | (YEARLY, 39 | MONTHLY, 40 | WEEKLY, 41 | DAILY, 42 | HOURLY, 43 | MINUTELY, 44 | SECONDLY) = range(7) 45 | 46 | # Imported on demand. 47 | easter = None 48 | parser = None 49 | 50 | class weekday(object): 51 | __slots__ = ["weekday", "n"] 52 | 53 | def __init__(self, weekday, n=None): 54 | if n == 0: 55 | raise ValueError, "Can't create weekday with n == 0" 56 | self.weekday = weekday 57 | self.n = n 58 | 59 | def __call__(self, n): 60 | if n == self.n: 61 | return self 62 | else: 63 | return self.__class__(self.weekday, n) 64 | 65 | def __eq__(self, other): 66 | try: 67 | if self.weekday != other.weekday or self.n != other.n: 68 | return False 69 | except AttributeError: 70 | return False 71 | return True 72 | 73 | def __repr__(self): 74 | s = ("MO", "TU", "WE", "TH", "FR", "SA", "SU")[self.weekday] 75 | if not self.n: 76 | return s 77 | else: 78 | return "%s(%+d)" % (s, self.n) 79 | 80 | MO, TU, WE, TH, FR, SA, SU = weekdays = tuple([weekday(x) for x in range(7)]) 81 | 82 | class rrulebase: 83 | def __init__(self, cache=False): 84 | if cache: 85 | self._cache = [] 86 | self._cache_lock = thread.allocate_lock() 87 | self._cache_gen = self._iter() 88 | self._cache_complete = False 89 | else: 90 | self._cache = None 91 | self._cache_complete = False 92 | self._len = None 93 | 94 | def __iter__(self): 95 | if self._cache_complete: 96 | return iter(self._cache) 97 | elif self._cache is None: 98 | return self._iter() 99 | else: 100 | return self._iter_cached() 101 | 102 | def _iter_cached(self): 103 | i = 0 104 | gen = self._cache_gen 105 | cache = self._cache 106 | acquire = self._cache_lock.acquire 107 | release = self._cache_lock.release 108 | while gen: 109 | if i == len(cache): 110 | acquire() 111 | if self._cache_complete: 112 | break 113 | try: 114 | for j in range(10): 115 | cache.append(gen.next()) 116 | except StopIteration: 117 | self._cache_gen = gen = None 118 | self._cache_complete = True 119 | break 120 | release() 121 | yield cache[i] 122 | i += 1 123 | while i < self._len: 124 | yield cache[i] 125 | i += 1 126 | 127 | def __getitem__(self, item): 128 | if self._cache_complete: 129 | return self._cache[item] 130 | elif isinstance(item, slice): 131 | if item.step and item.step < 0: 132 | return list(iter(self))[item] 133 | else: 134 | return list(itertools.islice(self, 135 | item.start or 0, 136 | item.stop or sys.maxint, 137 | item.step or 1)) 138 | elif item >= 0: 139 | gen = iter(self) 140 | try: 141 | for i in range(item+1): 142 | res = gen.next() 143 | except StopIteration: 144 | raise IndexError 145 | return res 146 | else: 147 | return list(iter(self))[item] 148 | 149 | def __contains__(self, item): 150 | if self._cache_complete: 151 | return item in self._cache 152 | else: 153 | for i in self: 154 | if i == item: 155 | return True 156 | elif i > item: 157 | return False 158 | return False 159 | 160 | # __len__() introduces a large performance penality. 161 | def count(self): 162 | if self._len is None: 163 | for x in self: pass 164 | return self._len 165 | 166 | def before(self, dt, inc=False): 167 | if self._cache_complete: 168 | gen = self._cache 169 | else: 170 | gen = self 171 | last = None 172 | if inc: 173 | for i in gen: 174 | if i > dt: 175 | break 176 | last = i 177 | else: 178 | for i in gen: 179 | if i >= dt: 180 | break 181 | last = i 182 | return last 183 | 184 | def after(self, dt, inc=False): 185 | if self._cache_complete: 186 | gen = self._cache 187 | else: 188 | gen = self 189 | if inc: 190 | for i in gen: 191 | if i >= dt: 192 | return i 193 | else: 194 | for i in gen: 195 | if i > dt: 196 | return i 197 | return None 198 | 199 | def between(self, after, before, inc=False): 200 | if self._cache_complete: 201 | gen = self._cache 202 | else: 203 | gen = self 204 | started = False 205 | l = [] 206 | if inc: 207 | for i in gen: 208 | if i > before: 209 | break 210 | elif not started: 211 | if i >= after: 212 | started = True 213 | l.append(i) 214 | else: 215 | l.append(i) 216 | else: 217 | for i in gen: 218 | if i >= before: 219 | break 220 | elif not started: 221 | if i > after: 222 | started = True 223 | l.append(i) 224 | else: 225 | l.append(i) 226 | return l 227 | 228 | class rrule(rrulebase): 229 | def __init__(self, freq, dtstart=None, 230 | interval=1, wkst=None, count=None, until=None, bysetpos=None, 231 | bymonth=None, bymonthday=None, byyearday=None, byeaster=None, 232 | byweekno=None, byweekday=None, 233 | byhour=None, byminute=None, bysecond=None, 234 | cache=False): 235 | rrulebase.__init__(self, cache) 236 | global easter 237 | if not dtstart: 238 | dtstart = datetime.datetime.now().replace(microsecond=0) 239 | elif not isinstance(dtstart, datetime.datetime): 240 | dtstart = datetime.datetime.fromordinal(dtstart.toordinal()) 241 | else: 242 | dtstart = dtstart.replace(microsecond=0) 243 | self._dtstart = dtstart 244 | self._tzinfo = dtstart.tzinfo 245 | self._freq = freq 246 | self._interval = interval 247 | self._count = count 248 | if until and not isinstance(until, datetime.datetime): 249 | until = datetime.datetime.fromordinal(until.toordinal()) 250 | self._until = until 251 | if wkst is None: 252 | self._wkst = calendar.firstweekday() 253 | elif type(wkst) is int: 254 | self._wkst = wkst 255 | else: 256 | self._wkst = wkst.weekday 257 | if bysetpos is None: 258 | self._bysetpos = None 259 | elif type(bysetpos) is int: 260 | if bysetpos == 0 or not (-366 <= bysetpos <= 366): 261 | raise ValueError("bysetpos must be between 1 and 366, " 262 | "or between -366 and -1") 263 | self._bysetpos = (bysetpos,) 264 | else: 265 | self._bysetpos = tuple(bysetpos) 266 | for pos in self._bysetpos: 267 | if pos == 0 or not (-366 <= pos <= 366): 268 | raise ValueError("bysetpos must be between 1 and 366, " 269 | "or between -366 and -1") 270 | if not (byweekno or byyearday or bymonthday or 271 | byweekday is not None or byeaster is not None): 272 | if freq == YEARLY: 273 | if not bymonth: 274 | bymonth = dtstart.month 275 | bymonthday = dtstart.day 276 | elif freq == MONTHLY: 277 | bymonthday = dtstart.day 278 | elif freq == WEEKLY: 279 | byweekday = dtstart.weekday() 280 | # bymonth 281 | if not bymonth: 282 | self._bymonth = None 283 | elif type(bymonth) is int: 284 | self._bymonth = (bymonth,) 285 | else: 286 | self._bymonth = tuple(bymonth) 287 | # byyearday 288 | if not byyearday: 289 | self._byyearday = None 290 | elif type(byyearday) is int: 291 | self._byyearday = (byyearday,) 292 | else: 293 | self._byyearday = tuple(byyearday) 294 | # byeaster 295 | if byeaster is not None: 296 | if not easter: 297 | from dateutil import easter 298 | if type(byeaster) is int: 299 | self._byeaster = (byeaster,) 300 | else: 301 | self._byeaster = tuple(byeaster) 302 | else: 303 | self._byeaster = None 304 | # bymonthay 305 | if not bymonthday: 306 | self._bymonthday = () 307 | self._bynmonthday = () 308 | elif type(bymonthday) is int: 309 | if bymonthday < 0: 310 | self._bynmonthday = (bymonthday,) 311 | self._bymonthday = () 312 | else: 313 | self._bymonthday = (bymonthday,) 314 | self._bynmonthday = () 315 | else: 316 | self._bymonthday = tuple([x for x in bymonthday if x > 0]) 317 | self._bynmonthday = tuple([x for x in bymonthday if x < 0]) 318 | # byweekno 319 | if byweekno is None: 320 | self._byweekno = None 321 | elif type(byweekno) is int: 322 | self._byweekno = (byweekno,) 323 | else: 324 | self._byweekno = tuple(byweekno) 325 | # byweekday / bynweekday 326 | if byweekday is None: 327 | self._byweekday = None 328 | self._bynweekday = None 329 | elif type(byweekday) is int: 330 | self._byweekday = (byweekday,) 331 | self._bynweekday = None 332 | elif hasattr(byweekday, "n"): 333 | if not byweekday.n or freq > MONTHLY: 334 | self._byweekday = (byweekday.weekday,) 335 | self._bynweekday = None 336 | else: 337 | self._bynweekday = ((byweekday.weekday, byweekday.n),) 338 | self._byweekday = None 339 | else: 340 | self._byweekday = [] 341 | self._bynweekday = [] 342 | for wday in byweekday: 343 | if type(wday) is int: 344 | self._byweekday.append(wday) 345 | elif not wday.n or freq > MONTHLY: 346 | self._byweekday.append(wday.weekday) 347 | else: 348 | self._bynweekday.append((wday.weekday, wday.n)) 349 | self._byweekday = tuple(self._byweekday) 350 | self._bynweekday = tuple(self._bynweekday) 351 | if not self._byweekday: 352 | self._byweekday = None 353 | elif not self._bynweekday: 354 | self._bynweekday = None 355 | # byhour 356 | if byhour is None: 357 | if freq < HOURLY: 358 | self._byhour = (dtstart.hour,) 359 | else: 360 | self._byhour = None 361 | elif type(byhour) is int: 362 | self._byhour = (byhour,) 363 | else: 364 | self._byhour = tuple(byhour) 365 | # byminute 366 | if byminute is None: 367 | if freq < MINUTELY: 368 | self._byminute = (dtstart.minute,) 369 | else: 370 | self._byminute = None 371 | elif type(byminute) is int: 372 | self._byminute = (byminute,) 373 | else: 374 | self._byminute = tuple(byminute) 375 | # bysecond 376 | if bysecond is None: 377 | if freq < SECONDLY: 378 | self._bysecond = (dtstart.second,) 379 | else: 380 | self._bysecond = None 381 | elif type(bysecond) is int: 382 | self._bysecond = (bysecond,) 383 | else: 384 | self._bysecond = tuple(bysecond) 385 | 386 | if self._freq >= HOURLY: 387 | self._timeset = None 388 | else: 389 | self._timeset = [] 390 | for hour in self._byhour: 391 | for minute in self._byminute: 392 | for second in self._bysecond: 393 | self._timeset.append( 394 | datetime.time(hour, minute, second, 395 | tzinfo=self._tzinfo)) 396 | self._timeset.sort() 397 | self._timeset = tuple(self._timeset) 398 | 399 | def _iter(self): 400 | year, month, day, hour, minute, second, weekday, yearday, _ = \ 401 | self._dtstart.timetuple() 402 | 403 | # Some local variables to speed things up a bit 404 | freq = self._freq 405 | interval = self._interval 406 | wkst = self._wkst 407 | until = self._until 408 | bymonth = self._bymonth 409 | byweekno = self._byweekno 410 | byyearday = self._byyearday 411 | byweekday = self._byweekday 412 | byeaster = self._byeaster 413 | bymonthday = self._bymonthday 414 | bynmonthday = self._bynmonthday 415 | bysetpos = self._bysetpos 416 | byhour = self._byhour 417 | byminute = self._byminute 418 | bysecond = self._bysecond 419 | 420 | ii = _iterinfo(self) 421 | ii.rebuild(year, month) 422 | 423 | getdayset = {YEARLY:ii.ydayset, 424 | MONTHLY:ii.mdayset, 425 | WEEKLY:ii.wdayset, 426 | DAILY:ii.ddayset, 427 | HOURLY:ii.ddayset, 428 | MINUTELY:ii.ddayset, 429 | SECONDLY:ii.ddayset}[freq] 430 | 431 | if freq < HOURLY: 432 | timeset = self._timeset 433 | else: 434 | gettimeset = {HOURLY:ii.htimeset, 435 | MINUTELY:ii.mtimeset, 436 | SECONDLY:ii.stimeset}[freq] 437 | if ((freq >= HOURLY and 438 | self._byhour and hour not in self._byhour) or 439 | (freq >= MINUTELY and 440 | self._byminute and minute not in self._byminute) or 441 | (freq >= SECONDLY and 442 | self._bysecond and second not in self._bysecond)): 443 | timeset = () 444 | else: 445 | timeset = gettimeset(hour, minute, second) 446 | 447 | total = 0 448 | count = self._count 449 | while True: 450 | # Get dayset with the right frequency 451 | dayset, start, end = getdayset(year, month, day) 452 | 453 | # Do the "hard" work ;-) 454 | filtered = False 455 | for i in dayset[start:end]: 456 | if ((bymonth and ii.mmask[i] not in bymonth) or 457 | (byweekno and not ii.wnomask[i]) or 458 | (byweekday and ii.wdaymask[i] not in byweekday) or 459 | (ii.nwdaymask and not ii.nwdaymask[i]) or 460 | (byeaster and not ii.eastermask[i]) or 461 | ((bymonthday or bynmonthday) and 462 | ii.mdaymask[i] not in bymonthday and 463 | ii.nmdaymask[i] not in bynmonthday) or 464 | (byyearday and 465 | ((i < ii.yearlen and i+1 not in byyearday 466 | and -ii.yearlen+i not in byyearday) or 467 | (i >= ii.yearlen and i+1-ii.yearlen not in byyearday 468 | and -ii.nextyearlen+i-ii.yearlen 469 | not in byyearday)))): 470 | dayset[i] = None 471 | filtered = True 472 | 473 | # Output results 474 | if bysetpos and timeset: 475 | poslist = [] 476 | for pos in bysetpos: 477 | if pos < 0: 478 | daypos, timepos = divmod(pos, len(timeset)) 479 | else: 480 | daypos, timepos = divmod(pos-1, len(timeset)) 481 | try: 482 | i = [x for x in dayset[start:end] 483 | if x is not None][daypos] 484 | time = timeset[timepos] 485 | except IndexError: 486 | pass 487 | else: 488 | date = datetime.date.fromordinal(ii.yearordinal+i) 489 | res = datetime.datetime.combine(date, time) 490 | if res not in poslist: 491 | poslist.append(res) 492 | poslist.sort() 493 | for res in poslist: 494 | if until and res > until: 495 | self._len = total 496 | return 497 | elif res >= self._dtstart: 498 | total += 1 499 | yield res 500 | if count: 501 | count -= 1 502 | if not count: 503 | self._len = total 504 | return 505 | else: 506 | for i in dayset[start:end]: 507 | if i is not None: 508 | date = datetime.date.fromordinal(ii.yearordinal+i) 509 | for time in timeset: 510 | res = datetime.datetime.combine(date, time) 511 | if until and res > until: 512 | self._len = total 513 | return 514 | elif res >= self._dtstart: 515 | total += 1 516 | yield res 517 | if count: 518 | count -= 1 519 | if not count: 520 | self._len = total 521 | return 522 | 523 | # Handle frequency and interval 524 | fixday = False 525 | if freq == YEARLY: 526 | year += interval 527 | if year > datetime.MAXYEAR: 528 | self._len = total 529 | return 530 | ii.rebuild(year, month) 531 | elif freq == MONTHLY: 532 | month += interval 533 | if month > 12: 534 | div, mod = divmod(month, 12) 535 | month = mod 536 | year += div 537 | if month == 0: 538 | month = 12 539 | year -= 1 540 | if year > datetime.MAXYEAR: 541 | self._len = total 542 | return 543 | ii.rebuild(year, month) 544 | elif freq == WEEKLY: 545 | if wkst > weekday: 546 | day += -(weekday+1+(6-wkst))+self._interval*7 547 | else: 548 | day += -(weekday-wkst)+self._interval*7 549 | weekday = wkst 550 | fixday = True 551 | elif freq == DAILY: 552 | day += interval 553 | fixday = True 554 | elif freq == HOURLY: 555 | if filtered: 556 | # Jump to one iteration before next day 557 | hour += ((23-hour)//interval)*interval 558 | while True: 559 | hour += interval 560 | div, mod = divmod(hour, 24) 561 | if div: 562 | hour = mod 563 | day += div 564 | fixday = True 565 | if not byhour or hour in byhour: 566 | break 567 | timeset = gettimeset(hour, minute, second) 568 | elif freq == MINUTELY: 569 | if filtered: 570 | # Jump to one iteration before next day 571 | minute += ((1439-(hour*60+minute))//interval)*interval 572 | while True: 573 | minute += interval 574 | div, mod = divmod(minute, 60) 575 | if div: 576 | minute = mod 577 | hour += div 578 | div, mod = divmod(hour, 24) 579 | if div: 580 | hour = mod 581 | day += div 582 | fixday = True 583 | filtered = False 584 | if ((not byhour or hour in byhour) and 585 | (not byminute or minute in byminute)): 586 | break 587 | timeset = gettimeset(hour, minute, second) 588 | elif freq == SECONDLY: 589 | if filtered: 590 | # Jump to one iteration before next day 591 | second += (((86399-(hour*3600+minute*60+second)) 592 | //interval)*interval) 593 | while True: 594 | second += self._interval 595 | div, mod = divmod(second, 60) 596 | if div: 597 | second = mod 598 | minute += div 599 | div, mod = divmod(minute, 60) 600 | if div: 601 | minute = mod 602 | hour += div 603 | div, mod = divmod(hour, 24) 604 | if div: 605 | hour = mod 606 | day += div 607 | fixday = True 608 | if ((not byhour or hour in byhour) and 609 | (not byminute or minute in byminute) and 610 | (not bysecond or second in bysecond)): 611 | break 612 | timeset = gettimeset(hour, minute, second) 613 | 614 | if fixday and day > 28: 615 | daysinmonth = calendar.monthrange(year, month)[1] 616 | if day > daysinmonth: 617 | while day > daysinmonth: 618 | day -= daysinmonth 619 | month += 1 620 | if month == 13: 621 | month = 1 622 | year += 1 623 | if year > datetime.MAXYEAR: 624 | self._len = total 625 | return 626 | daysinmonth = calendar.monthrange(year, month)[1] 627 | ii.rebuild(year, month) 628 | 629 | class _iterinfo(object): 630 | __slots__ = ["rrule", "lastyear", "lastmonth", 631 | "yearlen", "nextyearlen", "yearordinal", "yearweekday", 632 | "mmask", "mrange", "mdaymask", "nmdaymask", 633 | "wdaymask", "wnomask", "nwdaymask", "eastermask"] 634 | 635 | def __init__(self, rrule): 636 | for attr in self.__slots__: 637 | setattr(self, attr, None) 638 | self.rrule = rrule 639 | 640 | def rebuild(self, year, month): 641 | # Every mask is 7 days longer to handle cross-year weekly periods. 642 | rr = self.rrule 643 | if year != self.lastyear: 644 | self.yearlen = 365+calendar.isleap(year) 645 | self.nextyearlen = 365+calendar.isleap(year+1) 646 | firstyday = datetime.date(year, 1, 1) 647 | self.yearordinal = firstyday.toordinal() 648 | self.yearweekday = firstyday.weekday() 649 | 650 | wday = datetime.date(year, 1, 1).weekday() 651 | if self.yearlen == 365: 652 | self.mmask = M365MASK 653 | self.mdaymask = MDAY365MASK 654 | self.nmdaymask = NMDAY365MASK 655 | self.wdaymask = WDAYMASK[wday:] 656 | self.mrange = M365RANGE 657 | else: 658 | self.mmask = M366MASK 659 | self.mdaymask = MDAY366MASK 660 | self.nmdaymask = NMDAY366MASK 661 | self.wdaymask = WDAYMASK[wday:] 662 | self.mrange = M366RANGE 663 | 664 | if not rr._byweekno: 665 | self.wnomask = None 666 | else: 667 | self.wnomask = [0]*(self.yearlen+7) 668 | #no1wkst = firstwkst = self.wdaymask.index(rr._wkst) 669 | no1wkst = firstwkst = (7-self.yearweekday+rr._wkst)%7 670 | if no1wkst >= 4: 671 | no1wkst = 0 672 | # Number of days in the year, plus the days we got 673 | # from last year. 674 | wyearlen = self.yearlen+(self.yearweekday-rr._wkst)%7 675 | else: 676 | # Number of days in the year, minus the days we 677 | # left in last year. 678 | wyearlen = self.yearlen-no1wkst 679 | div, mod = divmod(wyearlen, 7) 680 | numweeks = div+mod//4 681 | for n in rr._byweekno: 682 | if n < 0: 683 | n += numweeks+1 684 | if not (0 < n <= numweeks): 685 | continue 686 | if n > 1: 687 | i = no1wkst+(n-1)*7 688 | if no1wkst != firstwkst: 689 | i -= 7-firstwkst 690 | else: 691 | i = no1wkst 692 | for j in range(7): 693 | self.wnomask[i] = 1 694 | i += 1 695 | if self.wdaymask[i] == rr._wkst: 696 | break 697 | if 1 in rr._byweekno: 698 | # Check week number 1 of next year as well 699 | # TODO: Check -numweeks for next year. 700 | i = no1wkst+numweeks*7 701 | if no1wkst != firstwkst: 702 | i -= 7-firstwkst 703 | if i < self.yearlen: 704 | # If week starts in next year, we 705 | # don't care about it. 706 | for j in range(7): 707 | self.wnomask[i] = 1 708 | i += 1 709 | if self.wdaymask[i] == rr._wkst: 710 | break 711 | if no1wkst: 712 | # Check last week number of last year as 713 | # well. If no1wkst is 0, either the year 714 | # started on week start, or week number 1 715 | # got days from last year, so there are no 716 | # days from last year's last week number in 717 | # this year. 718 | if -1 not in rr._byweekno: 719 | lyearweekday = datetime.date(year-1,1,1).weekday() 720 | lno1wkst = (7-lyearweekday+rr._wkst)%7 721 | lyearlen = 365+calendar.isleap(year-1) 722 | if lno1wkst >= 4: 723 | lno1wkst = 0 724 | lnumweeks = 52+(lyearlen+ 725 | (lyearweekday-rr._wkst)%7)%7//4 726 | else: 727 | lnumweeks = 52+(self.yearlen-no1wkst)%7//4 728 | else: 729 | lnumweeks = -1 730 | if lnumweeks in rr._byweekno: 731 | for i in range(no1wkst): 732 | self.wnomask[i] = 1 733 | 734 | if (rr._bynweekday and 735 | (month != self.lastmonth or year != self.lastyear)): 736 | ranges = [] 737 | if rr._freq == YEARLY: 738 | if rr._bymonth: 739 | for month in rr._bymonth: 740 | ranges.append(self.mrange[month-1:month+1]) 741 | else: 742 | ranges = [(0, self.yearlen)] 743 | elif rr._freq == MONTHLY: 744 | ranges = [self.mrange[month-1:month+1]] 745 | if ranges: 746 | # Weekly frequency won't get here, so we may not 747 | # care about cross-year weekly periods. 748 | self.nwdaymask = [0]*self.yearlen 749 | for first, last in ranges: 750 | last -= 1 751 | for wday, n in rr._bynweekday: 752 | if n < 0: 753 | i = last+(n+1)*7 754 | i -= (self.wdaymask[i]-wday)%7 755 | else: 756 | i = first+(n-1)*7 757 | i += (7-self.wdaymask[i]+wday)%7 758 | if first <= i <= last: 759 | self.nwdaymask[i] = 1 760 | 761 | if rr._byeaster: 762 | self.eastermask = [0]*(self.yearlen+7) 763 | eyday = easter.easter(year).toordinal()-self.yearordinal 764 | for offset in rr._byeaster: 765 | self.eastermask[eyday+offset] = 1 766 | 767 | self.lastyear = year 768 | self.lastmonth = month 769 | 770 | def ydayset(self, year, month, day): 771 | return range(self.yearlen), 0, self.yearlen 772 | 773 | def mdayset(self, year, month, day): 774 | set = [None]*self.yearlen 775 | start, end = self.mrange[month-1:month+1] 776 | for i in range(start, end): 777 | set[i] = i 778 | return set, start, end 779 | 780 | def wdayset(self, year, month, day): 781 | # We need to handle cross-year weeks here. 782 | set = [None]*(self.yearlen+7) 783 | i = datetime.date(year, month, day).toordinal()-self.yearordinal 784 | start = i 785 | for j in range(7): 786 | set[i] = i 787 | i += 1 788 | #if (not (0 <= i < self.yearlen) or 789 | # self.wdaymask[i] == self.rrule._wkst): 790 | # This will cross the year boundary, if necessary. 791 | if self.wdaymask[i] == self.rrule._wkst: 792 | break 793 | return set, start, i 794 | 795 | def ddayset(self, year, month, day): 796 | set = [None]*self.yearlen 797 | i = datetime.date(year, month, day).toordinal()-self.yearordinal 798 | set[i] = i 799 | return set, i, i+1 800 | 801 | def htimeset(self, hour, minute, second): 802 | set = [] 803 | rr = self.rrule 804 | for minute in rr._byminute: 805 | for second in rr._bysecond: 806 | set.append(datetime.time(hour, minute, second, 807 | tzinfo=rr._tzinfo)) 808 | set.sort() 809 | return set 810 | 811 | def mtimeset(self, hour, minute, second): 812 | set = [] 813 | rr = self.rrule 814 | for second in rr._bysecond: 815 | set.append(datetime.time(hour, minute, second, tzinfo=rr._tzinfo)) 816 | set.sort() 817 | return set 818 | 819 | def stimeset(self, hour, minute, second): 820 | return (datetime.time(hour, minute, second, 821 | tzinfo=self.rrule._tzinfo),) 822 | 823 | 824 | class rruleset(rrulebase): 825 | 826 | class _genitem: 827 | def __init__(self, genlist, gen): 828 | try: 829 | self.dt = gen() 830 | genlist.append(self) 831 | except StopIteration: 832 | pass 833 | self.genlist = genlist 834 | self.gen = gen 835 | 836 | def next(self): 837 | try: 838 | self.dt = self.gen() 839 | except StopIteration: 840 | self.genlist.remove(self) 841 | 842 | def __cmp__(self, other): 843 | return cmp(self.dt, other.dt) 844 | 845 | def __init__(self, cache=False): 846 | rrulebase.__init__(self, cache) 847 | self._rrule = [] 848 | self._rdate = [] 849 | self._exrule = [] 850 | self._exdate = [] 851 | 852 | def rrule(self, rrule): 853 | self._rrule.append(rrule) 854 | 855 | def rdate(self, rdate): 856 | self._rdate.append(rdate) 857 | 858 | def exrule(self, exrule): 859 | self._exrule.append(exrule) 860 | 861 | def exdate(self, exdate): 862 | self._exdate.append(exdate) 863 | 864 | def _iter(self): 865 | rlist = [] 866 | self._rdate.sort() 867 | self._genitem(rlist, iter(self._rdate).next) 868 | for gen in [iter(x).next for x in self._rrule]: 869 | self._genitem(rlist, gen) 870 | rlist.sort() 871 | exlist = [] 872 | self._exdate.sort() 873 | self._genitem(exlist, iter(self._exdate).next) 874 | for gen in [iter(x).next for x in self._exrule]: 875 | self._genitem(exlist, gen) 876 | exlist.sort() 877 | lastdt = None 878 | total = 0 879 | while rlist: 880 | ritem = rlist[0] 881 | if not lastdt or lastdt != ritem.dt: 882 | while exlist and exlist[0] < ritem: 883 | exlist[0].next() 884 | exlist.sort() 885 | if not exlist or ritem != exlist[0]: 886 | total += 1 887 | yield ritem.dt 888 | lastdt = ritem.dt 889 | ritem.next() 890 | rlist.sort() 891 | self._len = total 892 | 893 | class _rrulestr: 894 | 895 | _freq_map = {"YEARLY": YEARLY, 896 | "MONTHLY": MONTHLY, 897 | "WEEKLY": WEEKLY, 898 | "DAILY": DAILY, 899 | "HOURLY": HOURLY, 900 | "MINUTELY": MINUTELY, 901 | "SECONDLY": SECONDLY} 902 | 903 | _weekday_map = {"MO":0,"TU":1,"WE":2,"TH":3,"FR":4,"SA":5,"SU":6} 904 | 905 | def _handle_int(self, rrkwargs, name, value, **kwargs): 906 | rrkwargs[name.lower()] = int(value) 907 | 908 | def _handle_int_list(self, rrkwargs, name, value, **kwargs): 909 | rrkwargs[name.lower()] = [int(x) for x in value.split(',')] 910 | 911 | _handle_INTERVAL = _handle_int 912 | _handle_COUNT = _handle_int 913 | _handle_BYSETPOS = _handle_int_list 914 | _handle_BYMONTH = _handle_int_list 915 | _handle_BYMONTHDAY = _handle_int_list 916 | _handle_BYYEARDAY = _handle_int_list 917 | _handle_BYEASTER = _handle_int_list 918 | _handle_BYWEEKNO = _handle_int_list 919 | _handle_BYHOUR = _handle_int_list 920 | _handle_BYMINUTE = _handle_int_list 921 | _handle_BYSECOND = _handle_int_list 922 | 923 | def _handle_FREQ(self, rrkwargs, name, value, **kwargs): 924 | rrkwargs["freq"] = self._freq_map[value] 925 | 926 | def _handle_UNTIL(self, rrkwargs, name, value, **kwargs): 927 | global parser 928 | if not parser: 929 | from dateutil import parser 930 | try: 931 | rrkwargs["until"] = parser.parse(value, 932 | ignoretz=kwargs.get("ignoretz"), 933 | tzinfos=kwargs.get("tzinfos")) 934 | except ValueError: 935 | raise ValueError, "invalid until date" 936 | 937 | def _handle_WKST(self, rrkwargs, name, value, **kwargs): 938 | rrkwargs["wkst"] = self._weekday_map[value] 939 | 940 | def _handle_BYWEEKDAY(self, rrkwargs, name, value, **kwarsg): 941 | l = [] 942 | for wday in value.split(','): 943 | for i in range(len(wday)): 944 | if wday[i] not in '+-0123456789': 945 | break 946 | n = wday[:i] or None 947 | w = wday[i:] 948 | if n: n = int(n) 949 | l.append(weekdays[self._weekday_map[w]](n)) 950 | rrkwargs["byweekday"] = l 951 | 952 | _handle_BYDAY = _handle_BYWEEKDAY 953 | 954 | def _parse_rfc_rrule(self, line, 955 | dtstart=None, 956 | cache=False, 957 | ignoretz=False, 958 | tzinfos=None): 959 | if line.find(':') != -1: 960 | name, value = line.split(':') 961 | if name != "RRULE": 962 | raise ValueError, "unknown parameter name" 963 | else: 964 | value = line 965 | rrkwargs = {} 966 | for pair in value.split(';'): 967 | name, value = pair.split('=') 968 | name = name.upper() 969 | value = value.upper() 970 | try: 971 | getattr(self, "_handle_"+name)(rrkwargs, name, value, 972 | ignoretz=ignoretz, 973 | tzinfos=tzinfos) 974 | except AttributeError: 975 | raise ValueError, "unknown parameter '%s'" % name 976 | except (KeyError, ValueError): 977 | raise ValueError, "invalid '%s': %s" % (name, value) 978 | return rrule(dtstart=dtstart, cache=cache, **rrkwargs) 979 | 980 | def _parse_rfc(self, s, 981 | dtstart=None, 982 | cache=False, 983 | unfold=False, 984 | forceset=False, 985 | compatible=False, 986 | ignoretz=False, 987 | tzinfos=None): 988 | global parser 989 | if compatible: 990 | forceset = True 991 | unfold = True 992 | s = s.upper() 993 | if not s.strip(): 994 | raise ValueError, "empty string" 995 | if unfold: 996 | lines = s.splitlines() 997 | i = 0 998 | while i < len(lines): 999 | line = lines[i].rstrip() 1000 | if not line: 1001 | del lines[i] 1002 | elif i > 0 and line[0] == " ": 1003 | lines[i-1] += line[1:] 1004 | del lines[i] 1005 | else: 1006 | i += 1 1007 | else: 1008 | lines = s.split() 1009 | if (not forceset and len(lines) == 1 and 1010 | (s.find(':') == -1 or s.startswith('RRULE:'))): 1011 | return self._parse_rfc_rrule(lines[0], cache=cache, 1012 | dtstart=dtstart, ignoretz=ignoretz, 1013 | tzinfos=tzinfos) 1014 | else: 1015 | rrulevals = [] 1016 | rdatevals = [] 1017 | exrulevals = [] 1018 | exdatevals = [] 1019 | for line in lines: 1020 | if not line: 1021 | continue 1022 | if line.find(':') == -1: 1023 | name = "RRULE" 1024 | value = line 1025 | else: 1026 | name, value = line.split(':', 1) 1027 | parms = name.split(';') 1028 | if not parms: 1029 | raise ValueError, "empty property name" 1030 | name = parms[0] 1031 | parms = parms[1:] 1032 | if name == "RRULE": 1033 | for parm in parms: 1034 | raise ValueError, "unsupported RRULE parm: "+parm 1035 | rrulevals.append(value) 1036 | elif name == "RDATE": 1037 | for parm in parms: 1038 | if parm != "VALUE=DATE-TIME": 1039 | raise ValueError, "unsupported RDATE parm: "+parm 1040 | rdatevals.append(value) 1041 | elif name == "EXRULE": 1042 | for parm in parms: 1043 | raise ValueError, "unsupported EXRULE parm: "+parm 1044 | exrulevals.append(value) 1045 | elif name == "EXDATE": 1046 | for parm in parms: 1047 | if parm != "VALUE=DATE-TIME": 1048 | raise ValueError, "unsupported RDATE parm: "+parm 1049 | exdatevals.append(value) 1050 | elif name == "DTSTART": 1051 | for parm in parms: 1052 | raise ValueError, "unsupported DTSTART parm: "+parm 1053 | if not parser: 1054 | from dateutil import parser 1055 | dtstart = parser.parse(value, ignoretz=ignoretz, 1056 | tzinfos=tzinfos) 1057 | else: 1058 | raise ValueError, "unsupported property: "+name 1059 | if (forceset or len(rrulevals) > 1 or 1060 | rdatevals or exrulevals or exdatevals): 1061 | if not parser and (rdatevals or exdatevals): 1062 | from dateutil import parser 1063 | set = rruleset(cache=cache) 1064 | for value in rrulevals: 1065 | set.rrule(self._parse_rfc_rrule(value, dtstart=dtstart, 1066 | ignoretz=ignoretz, 1067 | tzinfos=tzinfos)) 1068 | for value in rdatevals: 1069 | for datestr in value.split(','): 1070 | set.rdate(parser.parse(datestr, 1071 | ignoretz=ignoretz, 1072 | tzinfos=tzinfos)) 1073 | for value in exrulevals: 1074 | set.exrule(self._parse_rfc_rrule(value, dtstart=dtstart, 1075 | ignoretz=ignoretz, 1076 | tzinfos=tzinfos)) 1077 | for value in exdatevals: 1078 | for datestr in value.split(','): 1079 | set.exdate(parser.parse(datestr, 1080 | ignoretz=ignoretz, 1081 | tzinfos=tzinfos)) 1082 | if compatible and dtstart: 1083 | set.rdate(dtstart) 1084 | return set 1085 | else: 1086 | return self._parse_rfc_rrule(rrulevals[0], 1087 | dtstart=dtstart, 1088 | cache=cache, 1089 | ignoretz=ignoretz, 1090 | tzinfos=tzinfos) 1091 | 1092 | def __call__(self, s, **kwargs): 1093 | return self._parse_rfc(s, **kwargs) 1094 | 1095 | rrulestr = _rrulestr() 1096 | 1097 | # vim:ts=4:sw=4:et 1098 | -------------------------------------------------------------------------------- /dateutil/tz.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2003-2007 Gustavo Niemeyer 3 | 4 | This module offers extensions to the standard python 2.3+ 5 | datetime module. 6 | """ 7 | __author__ = "Gustavo Niemeyer " 8 | __license__ = "PSF License" 9 | 10 | import datetime 11 | import struct 12 | import time 13 | import sys 14 | import os 15 | 16 | relativedelta = None 17 | parser = None 18 | rrule = None 19 | 20 | __all__ = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange", 21 | "tzstr", "tzical", "tzwin", "tzwinlocal", "gettz"] 22 | 23 | try: 24 | from dateutil.tzwin import tzwin, tzwinlocal 25 | except (ImportError, OSError): 26 | tzwin, tzwinlocal = None, None 27 | 28 | ZERO = datetime.timedelta(0) 29 | EPOCHORDINAL = datetime.datetime.utcfromtimestamp(0).toordinal() 30 | 31 | class tzutc(datetime.tzinfo): 32 | 33 | def utcoffset(self, dt): 34 | return ZERO 35 | 36 | def dst(self, dt): 37 | return ZERO 38 | 39 | def tzname(self, dt): 40 | return "UTC" 41 | 42 | def __eq__(self, other): 43 | return (isinstance(other, tzutc) or 44 | (isinstance(other, tzoffset) and other._offset == ZERO)) 45 | 46 | def __ne__(self, other): 47 | return not self.__eq__(other) 48 | 49 | def __repr__(self): 50 | return "%s()" % self.__class__.__name__ 51 | 52 | __reduce__ = object.__reduce__ 53 | 54 | class tzoffset(datetime.tzinfo): 55 | 56 | def __init__(self, name, offset): 57 | self._name = name 58 | self._offset = datetime.timedelta(seconds=offset) 59 | 60 | def utcoffset(self, dt): 61 | return self._offset 62 | 63 | def dst(self, dt): 64 | return ZERO 65 | 66 | def tzname(self, dt): 67 | return self._name 68 | 69 | def __eq__(self, other): 70 | return (isinstance(other, tzoffset) and 71 | self._offset == other._offset) 72 | 73 | def __ne__(self, other): 74 | return not self.__eq__(other) 75 | 76 | def __repr__(self): 77 | return "%s(%s, %s)" % (self.__class__.__name__, 78 | `self._name`, 79 | self._offset.days*86400+self._offset.seconds) 80 | 81 | __reduce__ = object.__reduce__ 82 | 83 | class tzlocal(datetime.tzinfo): 84 | 85 | _std_offset = datetime.timedelta(seconds=-time.timezone) 86 | if time.daylight: 87 | _dst_offset = datetime.timedelta(seconds=-time.altzone) 88 | else: 89 | _dst_offset = _std_offset 90 | 91 | def utcoffset(self, dt): 92 | if self._isdst(dt): 93 | return self._dst_offset 94 | else: 95 | return self._std_offset 96 | 97 | def dst(self, dt): 98 | if self._isdst(dt): 99 | return self._dst_offset-self._std_offset 100 | else: 101 | return ZERO 102 | 103 | def tzname(self, dt): 104 | return time.tzname[self._isdst(dt)] 105 | 106 | def _isdst(self, dt): 107 | # We can't use mktime here. It is unstable when deciding if 108 | # the hour near to a change is DST or not. 109 | # 110 | # timestamp = time.mktime((dt.year, dt.month, dt.day, dt.hour, 111 | # dt.minute, dt.second, dt.weekday(), 0, -1)) 112 | # return time.localtime(timestamp).tm_isdst 113 | # 114 | # The code above yields the following result: 115 | # 116 | #>>> import tz, datetime 117 | #>>> t = tz.tzlocal() 118 | #>>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() 119 | #'BRDT' 120 | #>>> datetime.datetime(2003,2,16,0,tzinfo=t).tzname() 121 | #'BRST' 122 | #>>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() 123 | #'BRST' 124 | #>>> datetime.datetime(2003,2,15,22,tzinfo=t).tzname() 125 | #'BRDT' 126 | #>>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() 127 | #'BRDT' 128 | # 129 | # Here is a more stable implementation: 130 | # 131 | timestamp = ((dt.toordinal() - EPOCHORDINAL) * 86400 132 | + dt.hour * 3600 133 | + dt.minute * 60 134 | + dt.second) 135 | return time.localtime(timestamp+time.timezone).tm_isdst 136 | 137 | def __eq__(self, other): 138 | if not isinstance(other, tzlocal): 139 | return False 140 | return (self._std_offset == other._std_offset and 141 | self._dst_offset == other._dst_offset) 142 | return True 143 | 144 | def __ne__(self, other): 145 | return not self.__eq__(other) 146 | 147 | def __repr__(self): 148 | return "%s()" % self.__class__.__name__ 149 | 150 | __reduce__ = object.__reduce__ 151 | 152 | class _ttinfo(object): 153 | __slots__ = ["offset", "delta", "isdst", "abbr", "isstd", "isgmt"] 154 | 155 | def __init__(self): 156 | for attr in self.__slots__: 157 | setattr(self, attr, None) 158 | 159 | def __repr__(self): 160 | l = [] 161 | for attr in self.__slots__: 162 | value = getattr(self, attr) 163 | if value is not None: 164 | l.append("%s=%s" % (attr, `value`)) 165 | return "%s(%s)" % (self.__class__.__name__, ", ".join(l)) 166 | 167 | def __eq__(self, other): 168 | if not isinstance(other, _ttinfo): 169 | return False 170 | return (self.offset == other.offset and 171 | self.delta == other.delta and 172 | self.isdst == other.isdst and 173 | self.abbr == other.abbr and 174 | self.isstd == other.isstd and 175 | self.isgmt == other.isgmt) 176 | 177 | def __ne__(self, other): 178 | return not self.__eq__(other) 179 | 180 | def __getstate__(self): 181 | state = {} 182 | for name in self.__slots__: 183 | state[name] = getattr(self, name, None) 184 | return state 185 | 186 | def __setstate__(self, state): 187 | for name in self.__slots__: 188 | if name in state: 189 | setattr(self, name, state[name]) 190 | 191 | class tzfile(datetime.tzinfo): 192 | 193 | # http://www.twinsun.com/tz/tz-link.htm 194 | # ftp://elsie.nci.nih.gov/pub/tz*.tar.gz 195 | 196 | def __init__(self, fileobj): 197 | if isinstance(fileobj, basestring): 198 | self._filename = fileobj 199 | fileobj = open(fileobj) 200 | elif hasattr(fileobj, "name"): 201 | self._filename = fileobj.name 202 | else: 203 | self._filename = `fileobj` 204 | 205 | # From tzfile(5): 206 | # 207 | # The time zone information files used by tzset(3) 208 | # begin with the magic characters "TZif" to identify 209 | # them as time zone information files, followed by 210 | # sixteen bytes reserved for future use, followed by 211 | # six four-byte values of type long, written in a 212 | # ``standard'' byte order (the high-order byte 213 | # of the value is written first). 214 | 215 | if fileobj.read(4) != "TZif": 216 | raise ValueError, "magic not found" 217 | 218 | fileobj.read(16) 219 | 220 | ( 221 | # The number of UTC/local indicators stored in the file. 222 | ttisgmtcnt, 223 | 224 | # The number of standard/wall indicators stored in the file. 225 | ttisstdcnt, 226 | 227 | # The number of leap seconds for which data is 228 | # stored in the file. 229 | leapcnt, 230 | 231 | # The number of "transition times" for which data 232 | # is stored in the file. 233 | timecnt, 234 | 235 | # The number of "local time types" for which data 236 | # is stored in the file (must not be zero). 237 | typecnt, 238 | 239 | # The number of characters of "time zone 240 | # abbreviation strings" stored in the file. 241 | charcnt, 242 | 243 | ) = struct.unpack(">6l", fileobj.read(24)) 244 | 245 | # The above header is followed by tzh_timecnt four-byte 246 | # values of type long, sorted in ascending order. 247 | # These values are written in ``standard'' byte order. 248 | # Each is used as a transition time (as returned by 249 | # time(2)) at which the rules for computing local time 250 | # change. 251 | 252 | if timecnt: 253 | self._trans_list = struct.unpack(">%dl" % timecnt, 254 | fileobj.read(timecnt*4)) 255 | else: 256 | self._trans_list = [] 257 | 258 | # Next come tzh_timecnt one-byte values of type unsigned 259 | # char; each one tells which of the different types of 260 | # ``local time'' types described in the file is associated 261 | # with the same-indexed transition time. These values 262 | # serve as indices into an array of ttinfo structures that 263 | # appears next in the file. 264 | 265 | if timecnt: 266 | self._trans_idx = struct.unpack(">%dB" % timecnt, 267 | fileobj.read(timecnt)) 268 | else: 269 | self._trans_idx = [] 270 | 271 | # Each ttinfo structure is written as a four-byte value 272 | # for tt_gmtoff of type long, in a standard byte 273 | # order, followed by a one-byte value for tt_isdst 274 | # and a one-byte value for tt_abbrind. In each 275 | # structure, tt_gmtoff gives the number of 276 | # seconds to be added to UTC, tt_isdst tells whether 277 | # tm_isdst should be set by localtime(3), and 278 | # tt_abbrind serves as an index into the array of 279 | # time zone abbreviation characters that follow the 280 | # ttinfo structure(s) in the file. 281 | 282 | ttinfo = [] 283 | 284 | for i in range(typecnt): 285 | ttinfo.append(struct.unpack(">lbb", fileobj.read(6))) 286 | 287 | abbr = fileobj.read(charcnt) 288 | 289 | # Then there are tzh_leapcnt pairs of four-byte 290 | # values, written in standard byte order; the 291 | # first value of each pair gives the time (as 292 | # returned by time(2)) at which a leap second 293 | # occurs; the second gives the total number of 294 | # leap seconds to be applied after the given time. 295 | # The pairs of values are sorted in ascending order 296 | # by time. 297 | 298 | # Not used, for now 299 | if leapcnt: 300 | leap = struct.unpack(">%dl" % (leapcnt*2), 301 | fileobj.read(leapcnt*8)) 302 | 303 | # Then there are tzh_ttisstdcnt standard/wall 304 | # indicators, each stored as a one-byte value; 305 | # they tell whether the transition times associated 306 | # with local time types were specified as standard 307 | # time or wall clock time, and are used when 308 | # a time zone file is used in handling POSIX-style 309 | # time zone environment variables. 310 | 311 | if ttisstdcnt: 312 | isstd = struct.unpack(">%db" % ttisstdcnt, 313 | fileobj.read(ttisstdcnt)) 314 | 315 | # Finally, there are tzh_ttisgmtcnt UTC/local 316 | # indicators, each stored as a one-byte value; 317 | # they tell whether the transition times associated 318 | # with local time types were specified as UTC or 319 | # local time, and are used when a time zone file 320 | # is used in handling POSIX-style time zone envi- 321 | # ronment variables. 322 | 323 | if ttisgmtcnt: 324 | isgmt = struct.unpack(">%db" % ttisgmtcnt, 325 | fileobj.read(ttisgmtcnt)) 326 | 327 | # ** Everything has been read ** 328 | 329 | # Build ttinfo list 330 | self._ttinfo_list = [] 331 | for i in range(typecnt): 332 | gmtoff, isdst, abbrind = ttinfo[i] 333 | # Round to full-minutes if that's not the case. Python's 334 | # datetime doesn't accept sub-minute timezones. Check 335 | # http://python.org/sf/1447945 for some information. 336 | gmtoff = (gmtoff+30)//60*60 337 | tti = _ttinfo() 338 | tti.offset = gmtoff 339 | tti.delta = datetime.timedelta(seconds=gmtoff) 340 | tti.isdst = isdst 341 | tti.abbr = abbr[abbrind:abbr.find('\x00', abbrind)] 342 | tti.isstd = (ttisstdcnt > i and isstd[i] != 0) 343 | tti.isgmt = (ttisgmtcnt > i and isgmt[i] != 0) 344 | self._ttinfo_list.append(tti) 345 | 346 | # Replace ttinfo indexes for ttinfo objects. 347 | trans_idx = [] 348 | for idx in self._trans_idx: 349 | trans_idx.append(self._ttinfo_list[idx]) 350 | self._trans_idx = tuple(trans_idx) 351 | 352 | # Set standard, dst, and before ttinfos. before will be 353 | # used when a given time is before any transitions, 354 | # and will be set to the first non-dst ttinfo, or to 355 | # the first dst, if all of them are dst. 356 | self._ttinfo_std = None 357 | self._ttinfo_dst = None 358 | self._ttinfo_before = None 359 | if self._ttinfo_list: 360 | if not self._trans_list: 361 | self._ttinfo_std = self._ttinfo_first = self._ttinfo_list[0] 362 | else: 363 | for i in range(timecnt-1,-1,-1): 364 | tti = self._trans_idx[i] 365 | if not self._ttinfo_std and not tti.isdst: 366 | self._ttinfo_std = tti 367 | elif not self._ttinfo_dst and tti.isdst: 368 | self._ttinfo_dst = tti 369 | if self._ttinfo_std and self._ttinfo_dst: 370 | break 371 | else: 372 | if self._ttinfo_dst and not self._ttinfo_std: 373 | self._ttinfo_std = self._ttinfo_dst 374 | 375 | for tti in self._ttinfo_list: 376 | if not tti.isdst: 377 | self._ttinfo_before = tti 378 | break 379 | else: 380 | self._ttinfo_before = self._ttinfo_list[0] 381 | 382 | # Now fix transition times to become relative to wall time. 383 | # 384 | # I'm not sure about this. In my tests, the tz source file 385 | # is setup to wall time, and in the binary file isstd and 386 | # isgmt are off, so it should be in wall time. OTOH, it's 387 | # always in gmt time. Let me know if you have comments 388 | # about this. 389 | laststdoffset = 0 390 | self._trans_list = list(self._trans_list) 391 | for i in range(len(self._trans_list)): 392 | tti = self._trans_idx[i] 393 | if not tti.isdst: 394 | # This is std time. 395 | self._trans_list[i] += tti.offset 396 | laststdoffset = tti.offset 397 | else: 398 | # This is dst time. Convert to std. 399 | self._trans_list[i] += laststdoffset 400 | self._trans_list = tuple(self._trans_list) 401 | 402 | def _find_ttinfo(self, dt, laststd=0): 403 | timestamp = ((dt.toordinal() - EPOCHORDINAL) * 86400 404 | + dt.hour * 3600 405 | + dt.minute * 60 406 | + dt.second) 407 | idx = 0 408 | for trans in self._trans_list: 409 | if timestamp < trans: 410 | break 411 | idx += 1 412 | else: 413 | return self._ttinfo_std 414 | if idx == 0: 415 | return self._ttinfo_before 416 | if laststd: 417 | while idx > 0: 418 | tti = self._trans_idx[idx-1] 419 | if not tti.isdst: 420 | return tti 421 | idx -= 1 422 | else: 423 | return self._ttinfo_std 424 | else: 425 | return self._trans_idx[idx-1] 426 | 427 | def utcoffset(self, dt): 428 | if not self._ttinfo_std: 429 | return ZERO 430 | return self._find_ttinfo(dt).delta 431 | 432 | def dst(self, dt): 433 | if not self._ttinfo_dst: 434 | return ZERO 435 | tti = self._find_ttinfo(dt) 436 | if not tti.isdst: 437 | return ZERO 438 | 439 | # The documentation says that utcoffset()-dst() must 440 | # be constant for every dt. 441 | return tti.delta-self._find_ttinfo(dt, laststd=1).delta 442 | 443 | # An alternative for that would be: 444 | # 445 | # return self._ttinfo_dst.offset-self._ttinfo_std.offset 446 | # 447 | # However, this class stores historical changes in the 448 | # dst offset, so I belive that this wouldn't be the right 449 | # way to implement this. 450 | 451 | def tzname(self, dt): 452 | if not self._ttinfo_std: 453 | return None 454 | return self._find_ttinfo(dt).abbr 455 | 456 | def __eq__(self, other): 457 | if not isinstance(other, tzfile): 458 | return False 459 | return (self._trans_list == other._trans_list and 460 | self._trans_idx == other._trans_idx and 461 | self._ttinfo_list == other._ttinfo_list) 462 | 463 | def __ne__(self, other): 464 | return not self.__eq__(other) 465 | 466 | 467 | def __repr__(self): 468 | return "%s(%s)" % (self.__class__.__name__, `self._filename`) 469 | 470 | def __reduce__(self): 471 | if not os.path.isfile(self._filename): 472 | raise ValueError, "Unpickable %s class" % self.__class__.__name__ 473 | return (self.__class__, (self._filename,)) 474 | 475 | class tzrange(datetime.tzinfo): 476 | 477 | def __init__(self, stdabbr, stdoffset=None, 478 | dstabbr=None, dstoffset=None, 479 | start=None, end=None): 480 | global relativedelta 481 | if not relativedelta: 482 | from dateutil import relativedelta 483 | self._std_abbr = stdabbr 484 | self._dst_abbr = dstabbr 485 | if stdoffset is not None: 486 | self._std_offset = datetime.timedelta(seconds=stdoffset) 487 | else: 488 | self._std_offset = ZERO 489 | if dstoffset is not None: 490 | self._dst_offset = datetime.timedelta(seconds=dstoffset) 491 | elif dstabbr and stdoffset is not None: 492 | self._dst_offset = self._std_offset+datetime.timedelta(hours=+1) 493 | else: 494 | self._dst_offset = ZERO 495 | if dstabbr and start is None: 496 | self._start_delta = relativedelta.relativedelta( 497 | hours=+2, month=4, day=1, weekday=relativedelta.SU(+1)) 498 | else: 499 | self._start_delta = start 500 | if dstabbr and end is None: 501 | self._end_delta = relativedelta.relativedelta( 502 | hours=+1, month=10, day=31, weekday=relativedelta.SU(-1)) 503 | else: 504 | self._end_delta = end 505 | 506 | def utcoffset(self, dt): 507 | if self._isdst(dt): 508 | return self._dst_offset 509 | else: 510 | return self._std_offset 511 | 512 | def dst(self, dt): 513 | if self._isdst(dt): 514 | return self._dst_offset-self._std_offset 515 | else: 516 | return ZERO 517 | 518 | def tzname(self, dt): 519 | if self._isdst(dt): 520 | return self._dst_abbr 521 | else: 522 | return self._std_abbr 523 | 524 | def _isdst(self, dt): 525 | if not self._start_delta: 526 | return False 527 | year = datetime.datetime(dt.year,1,1) 528 | start = year+self._start_delta 529 | end = year+self._end_delta 530 | dt = dt.replace(tzinfo=None) 531 | if start < end: 532 | return dt >= start and dt < end 533 | else: 534 | return dt >= start or dt < end 535 | 536 | def __eq__(self, other): 537 | if not isinstance(other, tzrange): 538 | return False 539 | return (self._std_abbr == other._std_abbr and 540 | self._dst_abbr == other._dst_abbr and 541 | self._std_offset == other._std_offset and 542 | self._dst_offset == other._dst_offset and 543 | self._start_delta == other._start_delta and 544 | self._end_delta == other._end_delta) 545 | 546 | def __ne__(self, other): 547 | return not self.__eq__(other) 548 | 549 | def __repr__(self): 550 | return "%s(...)" % self.__class__.__name__ 551 | 552 | __reduce__ = object.__reduce__ 553 | 554 | class tzstr(tzrange): 555 | 556 | def __init__(self, s): 557 | global parser 558 | if not parser: 559 | from dateutil import parser 560 | self._s = s 561 | 562 | res = parser._parsetz(s) 563 | if res is None: 564 | raise ValueError, "unknown string format" 565 | 566 | # Here we break the compatibility with the TZ variable handling. 567 | # GMT-3 actually *means* the timezone -3. 568 | if res.stdabbr in ("GMT", "UTC"): 569 | res.stdoffset *= -1 570 | 571 | # We must initialize it first, since _delta() needs 572 | # _std_offset and _dst_offset set. Use False in start/end 573 | # to avoid building it two times. 574 | tzrange.__init__(self, res.stdabbr, res.stdoffset, 575 | res.dstabbr, res.dstoffset, 576 | start=False, end=False) 577 | 578 | if not res.dstabbr: 579 | self._start_delta = None 580 | self._end_delta = None 581 | else: 582 | self._start_delta = self._delta(res.start) 583 | if self._start_delta: 584 | self._end_delta = self._delta(res.end, isend=1) 585 | 586 | def _delta(self, x, isend=0): 587 | kwargs = {} 588 | if x.month is not None: 589 | kwargs["month"] = x.month 590 | if x.weekday is not None: 591 | kwargs["weekday"] = relativedelta.weekday(x.weekday, x.week) 592 | if x.week > 0: 593 | kwargs["day"] = 1 594 | else: 595 | kwargs["day"] = 31 596 | elif x.day: 597 | kwargs["day"] = x.day 598 | elif x.yday is not None: 599 | kwargs["yearday"] = x.yday 600 | elif x.jyday is not None: 601 | kwargs["nlyearday"] = x.jyday 602 | if not kwargs: 603 | # Default is to start on first sunday of april, and end 604 | # on last sunday of october. 605 | if not isend: 606 | kwargs["month"] = 4 607 | kwargs["day"] = 1 608 | kwargs["weekday"] = relativedelta.SU(+1) 609 | else: 610 | kwargs["month"] = 10 611 | kwargs["day"] = 31 612 | kwargs["weekday"] = relativedelta.SU(-1) 613 | if x.time is not None: 614 | kwargs["seconds"] = x.time 615 | else: 616 | # Default is 2AM. 617 | kwargs["seconds"] = 7200 618 | if isend: 619 | # Convert to standard time, to follow the documented way 620 | # of working with the extra hour. See the documentation 621 | # of the tzinfo class. 622 | delta = self._dst_offset-self._std_offset 623 | kwargs["seconds"] -= delta.seconds+delta.days*86400 624 | return relativedelta.relativedelta(**kwargs) 625 | 626 | def __repr__(self): 627 | return "%s(%s)" % (self.__class__.__name__, `self._s`) 628 | 629 | class _tzicalvtzcomp: 630 | def __init__(self, tzoffsetfrom, tzoffsetto, isdst, 631 | tzname=None, rrule=None): 632 | self.tzoffsetfrom = datetime.timedelta(seconds=tzoffsetfrom) 633 | self.tzoffsetto = datetime.timedelta(seconds=tzoffsetto) 634 | self.tzoffsetdiff = self.tzoffsetto-self.tzoffsetfrom 635 | self.isdst = isdst 636 | self.tzname = tzname 637 | self.rrule = rrule 638 | 639 | class _tzicalvtz(datetime.tzinfo): 640 | def __init__(self, tzid, comps=[]): 641 | self._tzid = tzid 642 | self._comps = comps 643 | self._cachedate = [] 644 | self._cachecomp = [] 645 | 646 | def _find_comp(self, dt): 647 | if len(self._comps) == 1: 648 | return self._comps[0] 649 | dt = dt.replace(tzinfo=None) 650 | try: 651 | return self._cachecomp[self._cachedate.index(dt)] 652 | except ValueError: 653 | pass 654 | lastcomp = None 655 | lastcompdt = None 656 | for comp in self._comps: 657 | if not comp.isdst: 658 | # Handle the extra hour in DST -> STD 659 | compdt = comp.rrule.before(dt-comp.tzoffsetdiff, inc=True) 660 | else: 661 | compdt = comp.rrule.before(dt, inc=True) 662 | if compdt and (not lastcompdt or lastcompdt < compdt): 663 | lastcompdt = compdt 664 | lastcomp = comp 665 | if not lastcomp: 666 | # RFC says nothing about what to do when a given 667 | # time is before the first onset date. We'll look for the 668 | # first standard component, or the first component, if 669 | # none is found. 670 | for comp in self._comps: 671 | if not comp.isdst: 672 | lastcomp = comp 673 | break 674 | else: 675 | lastcomp = comp[0] 676 | self._cachedate.insert(0, dt) 677 | self._cachecomp.insert(0, lastcomp) 678 | if len(self._cachedate) > 10: 679 | self._cachedate.pop() 680 | self._cachecomp.pop() 681 | return lastcomp 682 | 683 | def utcoffset(self, dt): 684 | return self._find_comp(dt).tzoffsetto 685 | 686 | def dst(self, dt): 687 | comp = self._find_comp(dt) 688 | if comp.isdst: 689 | return comp.tzoffsetdiff 690 | else: 691 | return ZERO 692 | 693 | def tzname(self, dt): 694 | return self._find_comp(dt).tzname 695 | 696 | def __repr__(self): 697 | return "" % `self._tzid` 698 | 699 | __reduce__ = object.__reduce__ 700 | 701 | class tzical: 702 | def __init__(self, fileobj): 703 | global rrule 704 | if not rrule: 705 | from dateutil import rrule 706 | 707 | if isinstance(fileobj, basestring): 708 | self._s = fileobj 709 | fileobj = open(fileobj) 710 | elif hasattr(fileobj, "name"): 711 | self._s = fileobj.name 712 | else: 713 | self._s = `fileobj` 714 | 715 | self._vtz = {} 716 | 717 | self._parse_rfc(fileobj.read()) 718 | 719 | def keys(self): 720 | return self._vtz.keys() 721 | 722 | def get(self, tzid=None): 723 | if tzid is None: 724 | keys = self._vtz.keys() 725 | if len(keys) == 0: 726 | raise ValueError, "no timezones defined" 727 | elif len(keys) > 1: 728 | raise ValueError, "more than one timezone available" 729 | tzid = keys[0] 730 | return self._vtz.get(tzid) 731 | 732 | def _parse_offset(self, s): 733 | s = s.strip() 734 | if not s: 735 | raise ValueError, "empty offset" 736 | if s[0] in ('+', '-'): 737 | signal = (-1,+1)[s[0]=='+'] 738 | s = s[1:] 739 | else: 740 | signal = +1 741 | if len(s) == 4: 742 | return (int(s[:2])*3600+int(s[2:])*60)*signal 743 | elif len(s) == 6: 744 | return (int(s[:2])*3600+int(s[2:4])*60+int(s[4:]))*signal 745 | else: 746 | raise ValueError, "invalid offset: "+s 747 | 748 | def _parse_rfc(self, s): 749 | lines = s.splitlines() 750 | if not lines: 751 | raise ValueError, "empty string" 752 | 753 | # Unfold 754 | i = 0 755 | while i < len(lines): 756 | line = lines[i].rstrip() 757 | if not line: 758 | del lines[i] 759 | elif i > 0 and line[0] == " ": 760 | lines[i-1] += line[1:] 761 | del lines[i] 762 | else: 763 | i += 1 764 | 765 | tzid = None 766 | comps = [] 767 | invtz = False 768 | comptype = None 769 | for line in lines: 770 | if not line: 771 | continue 772 | name, value = line.split(':', 1) 773 | parms = name.split(';') 774 | if not parms: 775 | raise ValueError, "empty property name" 776 | name = parms[0].upper() 777 | parms = parms[1:] 778 | if invtz: 779 | if name == "BEGIN": 780 | if value in ("STANDARD", "DAYLIGHT"): 781 | # Process component 782 | pass 783 | else: 784 | raise ValueError, "unknown component: "+value 785 | comptype = value 786 | founddtstart = False 787 | tzoffsetfrom = None 788 | tzoffsetto = None 789 | rrulelines = [] 790 | tzname = None 791 | elif name == "END": 792 | if value == "VTIMEZONE": 793 | if comptype: 794 | raise ValueError, \ 795 | "component not closed: "+comptype 796 | if not tzid: 797 | raise ValueError, \ 798 | "mandatory TZID not found" 799 | if not comps: 800 | raise ValueError, \ 801 | "at least one component is needed" 802 | # Process vtimezone 803 | self._vtz[tzid] = _tzicalvtz(tzid, comps) 804 | invtz = False 805 | elif value == comptype: 806 | if not founddtstart: 807 | raise ValueError, \ 808 | "mandatory DTSTART not found" 809 | if tzoffsetfrom is None: 810 | raise ValueError, \ 811 | "mandatory TZOFFSETFROM not found" 812 | if tzoffsetto is None: 813 | raise ValueError, \ 814 | "mandatory TZOFFSETFROM not found" 815 | # Process component 816 | rr = None 817 | if rrulelines: 818 | rr = rrule.rrulestr("\n".join(rrulelines), 819 | compatible=True, 820 | ignoretz=True, 821 | cache=True) 822 | comp = _tzicalvtzcomp(tzoffsetfrom, tzoffsetto, 823 | (comptype == "DAYLIGHT"), 824 | tzname, rr) 825 | comps.append(comp) 826 | comptype = None 827 | else: 828 | raise ValueError, \ 829 | "invalid component end: "+value 830 | elif comptype: 831 | if name == "DTSTART": 832 | rrulelines.append(line) 833 | founddtstart = True 834 | elif name in ("RRULE", "RDATE", "EXRULE", "EXDATE"): 835 | rrulelines.append(line) 836 | elif name == "TZOFFSETFROM": 837 | if parms: 838 | raise ValueError, \ 839 | "unsupported %s parm: %s "%(name, parms[0]) 840 | tzoffsetfrom = self._parse_offset(value) 841 | elif name == "TZOFFSETTO": 842 | if parms: 843 | raise ValueError, \ 844 | "unsupported TZOFFSETTO parm: "+parms[0] 845 | tzoffsetto = self._parse_offset(value) 846 | elif name == "TZNAME": 847 | if parms: 848 | raise ValueError, \ 849 | "unsupported TZNAME parm: "+parms[0] 850 | tzname = value 851 | elif name == "COMMENT": 852 | pass 853 | else: 854 | raise ValueError, "unsupported property: "+name 855 | else: 856 | if name == "TZID": 857 | if parms: 858 | raise ValueError, \ 859 | "unsupported TZID parm: "+parms[0] 860 | tzid = value 861 | elif name in ("TZURL", "LAST-MODIFIED", "COMMENT"): 862 | pass 863 | else: 864 | raise ValueError, "unsupported property: "+name 865 | elif name == "BEGIN" and value == "VTIMEZONE": 866 | tzid = None 867 | comps = [] 868 | invtz = True 869 | 870 | def __repr__(self): 871 | return "%s(%s)" % (self.__class__.__name__, `self._s`) 872 | 873 | if sys.platform != "win32": 874 | TZFILES = ["/etc/localtime", "localtime"] 875 | TZPATHS = ["/usr/share/zoneinfo", "/usr/lib/zoneinfo", "/etc/zoneinfo"] 876 | else: 877 | TZFILES = [] 878 | TZPATHS = [] 879 | 880 | def gettz(name=None): 881 | tz = None 882 | if not name: 883 | try: 884 | name = os.environ["TZ"] 885 | except KeyError: 886 | pass 887 | if name is None or name == ":": 888 | for filepath in TZFILES: 889 | if not os.path.isabs(filepath): 890 | filename = filepath 891 | for path in TZPATHS: 892 | filepath = os.path.join(path, filename) 893 | if os.path.isfile(filepath): 894 | break 895 | else: 896 | continue 897 | if os.path.isfile(filepath): 898 | try: 899 | tz = tzfile(filepath) 900 | break 901 | except (IOError, OSError, ValueError): 902 | pass 903 | else: 904 | tz = tzlocal() 905 | else: 906 | if name.startswith(":"): 907 | name = name[:-1] 908 | if os.path.isabs(name): 909 | if os.path.isfile(name): 910 | tz = tzfile(name) 911 | else: 912 | tz = None 913 | else: 914 | for path in TZPATHS: 915 | filepath = os.path.join(path, name) 916 | if not os.path.isfile(filepath): 917 | filepath = filepath.replace(' ','_') 918 | if not os.path.isfile(filepath): 919 | continue 920 | try: 921 | tz = tzfile(filepath) 922 | break 923 | except (IOError, OSError, ValueError): 924 | pass 925 | else: 926 | tz = None 927 | if tzwin: 928 | try: 929 | tz = tzwin(name) 930 | except OSError: 931 | pass 932 | if not tz: 933 | from dateutil.zoneinfo import gettz 934 | tz = gettz(name) 935 | if not tz: 936 | for c in name: 937 | # name must have at least one offset to be a tzstr 938 | if c in "0123456789": 939 | try: 940 | tz = tzstr(name) 941 | except ValueError: 942 | pass 943 | break 944 | else: 945 | if name in ("GMT", "UTC"): 946 | tz = tzutc() 947 | elif name in time.tzname: 948 | tz = tzlocal() 949 | return tz 950 | 951 | # vim:ts=4:sw=4:et 952 | -------------------------------------------------------------------------------- /dateutil/tzwin.py: -------------------------------------------------------------------------------- 1 | # This code was originally contributed by Jeffrey Harris. 2 | import datetime 3 | import struct 4 | import _winreg 5 | 6 | __author__ = "Jeffrey Harris & Gustavo Niemeyer " 7 | 8 | __all__ = ["tzwin", "tzwinlocal"] 9 | 10 | ONEWEEK = datetime.timedelta(7) 11 | 12 | TZKEYNAMENT = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones" 13 | TZKEYNAME9X = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Time Zones" 14 | TZLOCALKEYNAME = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation" 15 | 16 | def _settzkeyname(): 17 | global TZKEYNAME 18 | handle = _winreg.ConnectRegistry(None, _winreg.HKEY_LOCAL_MACHINE) 19 | try: 20 | _winreg.OpenKey(handle, TZKEYNAMENT).Close() 21 | TZKEYNAME = TZKEYNAMENT 22 | except WindowsError: 23 | TZKEYNAME = TZKEYNAME9X 24 | handle.Close() 25 | 26 | _settzkeyname() 27 | 28 | class tzwinbase(datetime.tzinfo): 29 | """tzinfo class based on win32's timezones available in the registry.""" 30 | 31 | def utcoffset(self, dt): 32 | if self._isdst(dt): 33 | return datetime.timedelta(minutes=self._dstoffset) 34 | else: 35 | return datetime.timedelta(minutes=self._stdoffset) 36 | 37 | def dst(self, dt): 38 | if self._isdst(dt): 39 | minutes = self._dstoffset - self._stdoffset 40 | return datetime.timedelta(minutes=minutes) 41 | else: 42 | return datetime.timedelta(0) 43 | 44 | def tzname(self, dt): 45 | if self._isdst(dt): 46 | return self._dstname 47 | else: 48 | return self._stdname 49 | 50 | def list(): 51 | """Return a list of all time zones known to the system.""" 52 | handle = _winreg.ConnectRegistry(None, _winreg.HKEY_LOCAL_MACHINE) 53 | tzkey = _winreg.OpenKey(handle, TZKEYNAME) 54 | result = [_winreg.EnumKey(tzkey, i) 55 | for i in range(_winreg.QueryInfoKey(tzkey)[0])] 56 | tzkey.Close() 57 | handle.Close() 58 | return result 59 | list = staticmethod(list) 60 | 61 | def display(self): 62 | return self._display 63 | 64 | def _isdst(self, dt): 65 | dston = picknthweekday(dt.year, self._dstmonth, self._dstdayofweek, 66 | self._dsthour, self._dstminute, 67 | self._dstweeknumber) 68 | dstoff = picknthweekday(dt.year, self._stdmonth, self._stddayofweek, 69 | self._stdhour, self._stdminute, 70 | self._stdweeknumber) 71 | if dston < dstoff: 72 | return dston <= dt.replace(tzinfo=None) < dstoff 73 | else: 74 | return not dstoff <= dt.replace(tzinfo=None) < dston 75 | 76 | 77 | class tzwin(tzwinbase): 78 | 79 | def __init__(self, name): 80 | self._name = name 81 | 82 | handle = _winreg.ConnectRegistry(None, _winreg.HKEY_LOCAL_MACHINE) 83 | tzkey = _winreg.OpenKey(handle, "%s\%s" % (TZKEYNAME, name)) 84 | keydict = valuestodict(tzkey) 85 | tzkey.Close() 86 | handle.Close() 87 | 88 | self._stdname = keydict["Std"].encode("iso-8859-1") 89 | self._dstname = keydict["Dlt"].encode("iso-8859-1") 90 | 91 | self._display = keydict["Display"] 92 | 93 | # See http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm 94 | tup = struct.unpack("=3l16h", keydict["TZI"]) 95 | self._stdoffset = -tup[0]-tup[1] # Bias + StandardBias * -1 96 | self._dstoffset = self._stdoffset-tup[2] # + DaylightBias * -1 97 | 98 | (self._stdmonth, 99 | self._stddayofweek, # Sunday = 0 100 | self._stdweeknumber, # Last = 5 101 | self._stdhour, 102 | self._stdminute) = tup[4:9] 103 | 104 | (self._dstmonth, 105 | self._dstdayofweek, # Sunday = 0 106 | self._dstweeknumber, # Last = 5 107 | self._dsthour, 108 | self._dstminute) = tup[12:17] 109 | 110 | def __repr__(self): 111 | return "tzwin(%s)" % repr(self._name) 112 | 113 | def __reduce__(self): 114 | return (self.__class__, (self._name,)) 115 | 116 | 117 | class tzwinlocal(tzwinbase): 118 | 119 | def __init__(self): 120 | 121 | handle = _winreg.ConnectRegistry(None, _winreg.HKEY_LOCAL_MACHINE) 122 | 123 | tzlocalkey = _winreg.OpenKey(handle, TZLOCALKEYNAME) 124 | keydict = valuestodict(tzlocalkey) 125 | tzlocalkey.Close() 126 | 127 | self._stdname = keydict["StandardName"].encode("iso-8859-1") 128 | self._dstname = keydict["DaylightName"].encode("iso-8859-1") 129 | 130 | try: 131 | tzkey = _winreg.OpenKey(handle, "%s\%s"%(TZKEYNAME, self._stdname)) 132 | _keydict = valuestodict(tzkey) 133 | self._display = _keydict["Display"] 134 | tzkey.Close() 135 | except OSError: 136 | self._display = None 137 | 138 | handle.Close() 139 | 140 | self._stdoffset = -keydict["Bias"]-keydict["StandardBias"] 141 | self._dstoffset = self._stdoffset-keydict["DaylightBias"] 142 | 143 | 144 | # See http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm 145 | tup = struct.unpack("=8h", keydict["StandardStart"]) 146 | 147 | (self._stdmonth, 148 | self._stddayofweek, # Sunday = 0 149 | self._stdweeknumber, # Last = 5 150 | self._stdhour, 151 | self._stdminute) = tup[1:6] 152 | 153 | tup = struct.unpack("=8h", keydict["DaylightStart"]) 154 | 155 | (self._dstmonth, 156 | self._dstdayofweek, # Sunday = 0 157 | self._dstweeknumber, # Last = 5 158 | self._dsthour, 159 | self._dstminute) = tup[1:6] 160 | 161 | def __reduce__(self): 162 | return (self.__class__, ()) 163 | 164 | def picknthweekday(year, month, dayofweek, hour, minute, whichweek): 165 | """dayofweek == 0 means Sunday, whichweek 5 means last instance""" 166 | first = datetime.datetime(year, month, 1, hour, minute) 167 | weekdayone = first.replace(day=((dayofweek-first.isoweekday())%7+1)) 168 | for n in xrange(whichweek): 169 | dt = weekdayone+(whichweek-n)*ONEWEEK 170 | if dt.month == month: 171 | return dt 172 | 173 | def valuestodict(key): 174 | """Convert a registry key's values to a dictionary.""" 175 | dict = {} 176 | size = _winreg.QueryInfoKey(key)[1] 177 | for i in range(size): 178 | data = _winreg.EnumValue(key, i) 179 | dict[data[0]] = data[1] 180 | return dict 181 | -------------------------------------------------------------------------------- /dateutil/zoneinfo/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2003-2005 Gustavo Niemeyer 3 | 4 | This module offers extensions to the standard python 2.3+ 5 | datetime module. 6 | """ 7 | from dateutil.tz import tzfile 8 | from tarfile import TarFile 9 | import os 10 | 11 | __author__ = "Gustavo Niemeyer " 12 | __license__ = "PSF License" 13 | 14 | __all__ = ["setcachesize", "gettz", "rebuild"] 15 | 16 | CACHE = [] 17 | CACHESIZE = 10 18 | 19 | class tzfile(tzfile): 20 | def __reduce__(self): 21 | return (gettz, (self._filename,)) 22 | 23 | def getzoneinfofile(): 24 | filenames = os.listdir(os.path.join(os.path.dirname(__file__))) 25 | filenames.sort() 26 | filenames.reverse() 27 | for entry in filenames: 28 | if entry.startswith("zoneinfo") and ".tar." in entry: 29 | return os.path.join(os.path.dirname(__file__), entry) 30 | return None 31 | 32 | ZONEINFOFILE = getzoneinfofile() 33 | 34 | del getzoneinfofile 35 | 36 | def setcachesize(size): 37 | global CACHESIZE, CACHE 38 | CACHESIZE = size 39 | del CACHE[size:] 40 | 41 | def gettz(name): 42 | tzinfo = None 43 | if ZONEINFOFILE: 44 | for cachedname, tzinfo in CACHE: 45 | if cachedname == name: 46 | break 47 | else: 48 | tf = TarFile.open(ZONEINFOFILE) 49 | try: 50 | zonefile = tf.extractfile(name) 51 | except KeyError: 52 | tzinfo = None 53 | else: 54 | tzinfo = tzfile(zonefile) 55 | tf.close() 56 | CACHE.insert(0, (name, tzinfo)) 57 | del CACHE[CACHESIZE:] 58 | return tzinfo 59 | 60 | def rebuild(filename, tag=None, format="gz"): 61 | import tempfile, shutil 62 | tmpdir = tempfile.mkdtemp() 63 | zonedir = os.path.join(tmpdir, "zoneinfo") 64 | moduledir = os.path.dirname(__file__) 65 | if tag: tag = "-"+tag 66 | targetname = "zoneinfo%s.tar.%s" % (tag, format) 67 | try: 68 | tf = TarFile.open(filename) 69 | for name in tf.getnames(): 70 | if not (name.endswith(".sh") or 71 | name.endswith(".tab") or 72 | name == "leapseconds"): 73 | tf.extract(name, tmpdir) 74 | filepath = os.path.join(tmpdir, name) 75 | os.system("zic -d %s %s" % (zonedir, filepath)) 76 | tf.close() 77 | target = os.path.join(moduledir, targetname) 78 | for entry in os.listdir(moduledir): 79 | if entry.startswith("zoneinfo") and ".tar." in entry: 80 | os.unlink(os.path.join(moduledir, entry)) 81 | tf = TarFile.open(target, "w:%s" % format) 82 | for entry in os.listdir(zonedir): 83 | entrypath = os.path.join(zonedir, entry) 84 | tf.add(entrypath, entry) 85 | tf.close() 86 | finally: 87 | shutil.rmtree(tmpdir) 88 | -------------------------------------------------------------------------------- /dateutil/zoneinfo/zoneinfo-2010g.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/initpy/selficious/4f57f00a36b40c71ffab031bfc41b2a17a7b939f/dateutil/zoneinfo/zoneinfo-2010g.tar.gz -------------------------------------------------------------------------------- /importers/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # This is SELFICIOUS by Yuuta 5 | # UPDATED: 2010-12-23 20:35:08 6 | 7 | import os 8 | import sys 9 | 10 | messages = { 11 | "":"", 12 | "fetch": "Unable to fetch your posts", 13 | "parse": "Unable to parse your posts", 14 | "unknown_service": "Don't know how to import from this service", 15 | } 16 | 17 | IMPORTERS = {} 18 | 19 | class ImporterMeta(type): 20 | def __init__(cls, name, bases, dict): 21 | if not hasattr(cls, 'service_name'): 22 | cls.service_name = name 23 | if not hasattr(cls, 'verbose_service_name'): 24 | cls.verbose_service_name = name 25 | if not hasattr(cls, 'form'): 26 | cls.form = '' 27 | super(ImporterMeta, cls).__init__(name, bases, dict) 28 | if name != 'BaseImporter': 29 | IMPORTERS.update({cls.service_name:cls}) 30 | 31 | 32 | class BaseImporter(object): 33 | """ 34 | A base class for posts importers 35 | All derived classes MUST include a self.success property which tells if the 36 | import was successfull AND a method self.posts() which will return a list of 37 | dicts representing the fetched posts. 38 | Thses classes are __init__'iated using a tornado handler which will give 39 | them their attributes values using its (the handler's) method get_argument() 40 | """ 41 | __metaclass__ = ImporterMeta 42 | 43 | def __init__(self, tornado_handler): 44 | pass 45 | 46 | def fetch_posts(self): 47 | """Overrided in children classes to fetch the posts""" 48 | raise NotImplementedError 49 | 50 | def posts(self): 51 | """Overrided in children classes to return a list of posts""" 52 | raise NotImplementedError 53 | 54 | 55 | def find_importers(): 56 | '''find all files in the importer directory and imports them''' 57 | importer_dir = os.path.dirname(os.path.realpath(__file__)) 58 | importer_files = [x[:-3] for x in os.listdir(importer_dir) if 59 | x.endswith(".py")] 60 | sys.path.insert(0, importer_dir) 61 | for importer in importer_files: 62 | mod = __import__(importer) 63 | 64 | def new(service_name): 65 | find_importers() 66 | try: 67 | return IMPORTERS[service_name] 68 | except KeyError: 69 | raise NotImplementedError 70 | 71 | def list(): 72 | find_importers() 73 | return [ 74 | dict( 75 | name=i.service_name, 76 | verbose_name=i.service_verbose_name, 77 | form = i.form, 78 | description = i.__doc__, 79 | ) for i 80 | in IMPORTERS.values() 81 | ] 82 | -------------------------------------------------------------------------------- /importers/delicious-html.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # This is SELFICIOUS by Yuuta 5 | # UPDATED: 2010-12-24 15:06:39 6 | 7 | import hashlib 8 | import BeautifulSoup 9 | import datetime 10 | from importers import BaseImporter 11 | 12 | class DeliciousLocalHTMLImporter(BaseImporter): 13 | """ 14 | Imports bookmarks from an HTML file saved from delicious. To get this kind of 15 | files, visit 17 | https://secure.delicious.com/settings/bookmarks/export 18 | —make sure to check "include my tags" and "include my notes" 19 | """ 20 | service_name = 'delicious-html' 21 | service_verbose_name = "Local HTML bookmarks file saved from delicious" 22 | form = """ 23 |

24 | 25 | 26 |

27 | """ 28 | def __init__(self, tornado_handler): 29 | try: 30 | uploaded_file = tornado_handler.request.files['htmlfile'][0] 31 | self.data = uploaded_file['body'] 32 | self.success = True 33 | except: 34 | self.success = False 35 | self.error = 'fetch' 36 | super(DeliciousLocalHTMLImporter, self).__init__(tornado_handler) 37 | 38 | def posts(self): 39 | if self.success: 40 | posts = [] 41 | soup = BeautifulSoup.BeautifulSoup(self.data) 42 | anchors = soup.findAll("a") 43 | h = hashlib.sha1() 44 | for a in anchors: 45 | h.update(a['href']) 46 | if a.parent.nextSibling and a.parent.nextSibling.name == 'dd': 47 | text = unicode(a.parent.nextSibling.string) 48 | else: 49 | text = '' 50 | posts.append({ 51 | 'hash':h.hexdigest(), 52 | 'url':a['href'], 53 | 'title':unicode(a.string), 54 | 'description':text, 55 | 'tags':unicode(a['tags']).split(','), 56 | 'time':datetime.datetime.fromtimestamp(float(a['add_date'])) 57 | }) 58 | return posts 59 | else: 60 | return [] 61 | -------------------------------------------------------------------------------- /importers/delicious-v1.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # This is SELFICIOUS by Yuuta 5 | # UPDATED: 2010-12-23 18:54:07 6 | 7 | import urllib2 8 | import base64 9 | import hashlib 10 | from xml.dom import minidom 11 | import dateutil.parser 12 | from importers import BaseImporter 13 | 14 | class DeliciousV1Importer(BaseImporter): 15 | """ 16 | A Delicious posts importer for old accounts i.e. Not tied with Yahoo 17 | accounts - Using the user's username and password. 18 | """ 19 | service_name = 'delicious-v1' 20 | service_verbose_name = "Old Delicious Account" 21 | form = """ 22 |

23 | 24 | 25 |

26 |

27 | 28 | 29 |

30 | """ 31 | 32 | def __init__(self, tornado_handler): 33 | self.url = "https://api.del.icio.us/v1/posts/all" 34 | self.domain = "https://api.del.icio.us/" 35 | self.user = tornado_handler.get_argument("username", "") 36 | self.password = tornado_handler.get_argument("password", "") 37 | super(DeliciousV1Importer, self).__init__(tornado_handler) 38 | 39 | def fetch_posts(self): 40 | try: 41 | passman = urllib2.HTTPPasswordMgrWithDefaultRealm() 42 | passman.add_password(None, self.domain, self.user, self.password) 43 | authhandler = urllib2.HTTPBasicAuthHandler(passman) 44 | opener = urllib2.build_opener(authhandler) 45 | urllib2.install_opener(opener) 46 | self.data = urllib2.urlopen(self.url).read() 47 | self.success = True 48 | except: 49 | self.success = False 50 | self.error = "fetch" 51 | self.data = None 52 | 53 | def posts(self): 54 | self.fetch_posts() 55 | if self.success: 56 | posts = [] 57 | dom = minidom.parseString(self.data) 58 | h = hashlib.sha1() 59 | for node in dom.getElementsByTagName('post'): 60 | h.update(node.getAttribute('href')) 61 | posts.append({ 62 | 'hash':h.hexdigest(), 63 | 'url':node.getAttribute('href'), 64 | 'title':node.getAttribute('description'), 65 | 'description':node.getAttribute('extended'), 66 | 'tags':node.getAttribute('tag').split(' '), 67 | 'time':dateutil.parser.parse(node.getAttribute('time')) 68 | }) 69 | return posts 70 | else: 71 | return [] 72 | -------------------------------------------------------------------------------- /importers/delicious-xml.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # This is SELFICIOUS by Yuuta 5 | # UPDATED: 2010-12-23 20:53:41 6 | 7 | import urllib2 8 | import base64 9 | import hashlib 10 | from xml.dom import minidom 11 | import dateutil.parser 12 | from importers import BaseImporter 13 | 14 | class DeliciousLocalXMLImporter(BaseImporter): 15 | """ 16 | Imports bookmarks from an XML file saved from delicious. To get this kind of 17 | files, visit http://api.del.icio.us/v1/posts/all 19 | (if you're an old delicious user) 20 | """ 21 | service_name = 'delicious-xml' 22 | service_verbose_name = "Local XML file saved from delicious" 23 | form = """ 24 |

25 | 26 | 27 |

28 | """ 29 | 30 | def __init__(self, tornado_handler): 31 | try: 32 | uploaded_file = tornado_handler.request.files['xmlfile'][0] 33 | self.data = uploaded_file['body'] 34 | self.success = True 35 | except: 36 | self.success = False 37 | self.error = 'fetch' 38 | super(DeliciousLocalXMLImporter, self).__init__(tornado_handler) 39 | 40 | def posts(self): 41 | if self.success: 42 | posts = [] 43 | dom = minidom.parseString(self.data) 44 | h = hashlib.sha1() 45 | for node in dom.getElementsByTagName('post'): 46 | h.update(node.getAttribute('href')) 47 | posts.append({ 48 | 'hash':h.hexdigest(), 49 | 'url':node.getAttribute('href'), 50 | 'title':node.getAttribute('description'), 51 | 'description':node.getAttribute('extended'), 52 | 'tags':node.getAttribute('tag').split(' '), 53 | 'time':dateutil.parser.parse(node.getAttribute('time')) 54 | }) 55 | return posts 56 | else: 57 | return [] 58 | -------------------------------------------------------------------------------- /index.yaml: -------------------------------------------------------------------------------- 1 | indexes: 2 | 3 | # AUTOGENERATED 4 | 5 | # This index.yaml is automatically updated whenever the dev_appserver 6 | # detects that a new type of query is run. If you want to manage the 7 | # index.yaml file manually, remove the above marker line (the line 8 | # saying "# AUTOGENERATED"). If you want to manage some indexes 9 | # manually, move them above the marker line. The index.yaml file is 10 | # automatically uploaded to the admin console when you next deploy 11 | # your application using appcfg.py. 12 | 13 | - kind: Entry 14 | properties: 15 | - name: tags 16 | - name: time 17 | direction: desc 18 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # This is SELFICIOUS by Yuuta 5 | # UPDATED: 2010-12-23 19:08:39 6 | 7 | import logging 8 | import hashlib 9 | import functools 10 | import os 11 | import os.path 12 | import re 13 | import tornado.web 14 | import tornado.wsgi 15 | import unicodedata 16 | import wsgiref.handlers 17 | 18 | from google.appengine.api import users 19 | from google.appengine.ext import db 20 | 21 | from utils import memoize, unmemoize 22 | import importers 23 | import settings 24 | 25 | 26 | class Entry(db.Model): 27 | """ 28 | A single entry. 29 | service: the service's name from which the entry comes -may be empty 30 | hash: a hash (sha1) of the url 31 | """ 32 | service = db.StringProperty(required=False) 33 | hash = db.StringProperty(required=True) 34 | title = db.StringProperty(required=True) 35 | description = db.TextProperty() 36 | url = db.LinkProperty(required=True) 37 | time = db.DateTimeProperty(auto_now_add=True) 38 | tags = db.ListProperty(db.Category) 39 | 40 | 41 | def administrator(method): 42 | """Decorate with this method to restrict to site admins.""" 43 | @functools.wraps(method) 44 | def wrapper(self, *args, **kwargs): 45 | if not self.current_user: 46 | if self.request.method == "GET": 47 | self.redirect(self.get_login_url()) 48 | return 49 | raise web.HTTPError(403) 50 | elif not self.current_user.administrator: 51 | if self.request.method == "GET": 52 | self.redirect("/") 53 | return 54 | raise web.HTTPError(403) 55 | else: 56 | return method(self, *args, **kwargs) 57 | return wrapper 58 | 59 | 60 | class BaseHandler(tornado.web.RequestHandler): 61 | """Implements Google Accounts authentication methods.""" 62 | def get_current_user(self): 63 | user = users.get_current_user() 64 | if user: user.administrator = users.is_current_user_admin() 65 | return user 66 | 67 | def get_login_url(self): 68 | return users.create_login_url(self.request.uri) 69 | 70 | def render_string(self, template_name, **kwargs): 71 | return tornado.web.RequestHandler.render_string( 72 | self, template_name, users=users, **kwargs) 73 | 74 | def slugify(self, title): 75 | slug = unicodedata.normalize("NFKD", title).encode("ascii", "ignore") 76 | slug = re.sub(r"[^\w]+", " ", slug) 77 | return "-".join(slug.lower().strip().split()) 78 | 79 | @memoize('/entries/recent') 80 | def get_recent_entries(self): 81 | entries = db.Query(Entry).order("-time").fetch(limit=5) 82 | return entries 83 | 84 | @memoize('/entries/home') 85 | def get_home_entries(self): 86 | entries = db.Query(Entry).order("-time").fetch(limit=10) 87 | return entries 88 | 89 | @memoize('/entries/archive') 90 | def get_archive_entries(self): 91 | entries = db.Query(Entry).order("-time") 92 | return entries 93 | 94 | @memoize('/entries/tag/%s') 95 | def get_tagged_entries(self, tag): 96 | entries = db.Query(Entry).filter("tags =", tag).order("-time") 97 | return entries 98 | 99 | def free_cache(self, tags=[]): 100 | """Use utils.unmemoize to delete stuff from memcache""" 101 | unmemoize([ "/entries/recent", "/entries/home", "/entries/archive"]) 102 | unmemoize(["/entries/tag/%s" % tag for tag in tags]) 103 | 104 | 105 | class HomeHandler(BaseHandler): 106 | def get(self): 107 | entries = self.get_home_entries() 108 | import_success = self.get_argument('imported', None) 109 | error = self.get_argument('error', "") 110 | self.render("home.html", entries=entries, import_success=import_success, 111 | error_message=importers.messages[error]) 112 | 113 | 114 | class ArchiveHandler(BaseHandler): 115 | def get(self): 116 | entries = self.get_archive_entries() 117 | self.render("archive.html", entries=entries) 118 | 119 | 120 | class TagHandler(BaseHandler): 121 | def get(self, tag): 122 | entries = self.get_tagged_entries(tag) 123 | self.render("tag.html", tag=tag, entries=entries) 124 | 125 | 126 | class BookmarkHandler(BaseHandler): 127 | @administrator 128 | def get(self): 129 | key = self.get_argument("key", None) 130 | entry = Entry.get(key) if key else None 131 | self.render("form.html", entry=entry) 132 | 133 | @administrator 134 | def post(self): 135 | key = self.get_argument("key", None) 136 | if key: 137 | entry = Entry.get(key) 138 | entry.title = self.get_argument("title", "") 139 | entry.description = self.get_argument("description", "") 140 | entry.url = self.get_argument("url", "") 141 | else: 142 | h = hashlib.sha1() 143 | h.update(self.get_argument("url", "")) 144 | entry = Entry( 145 | service="internal", 146 | title=self.get_argument("title", ""), 147 | description=self.get_argument("description", ""), 148 | url=self.get_argument("url", ""), 149 | hash=h.hexdigest(), 150 | ) 151 | tags = set([self.slugify(unicode(tag)) for tag in 152 | self.get_argument("tags", "").split(",")]) 153 | tags = [db.Category(tag) for tag in tags if tag] 154 | entry.tags = tags 155 | entry.put() 156 | self.free_cache(tags=entry.tags) 157 | self.redirect("/") 158 | 159 | 160 | class ImportHandler(BaseHandler): 161 | @administrator 162 | def get(self): 163 | services = importers.list() 164 | self.render("import.html", services=services) 165 | 166 | @administrator 167 | def post(self): 168 | service = self.get_argument("service", "") 169 | try: 170 | service_class = importers.new(service) 171 | importer = service_class(self) 172 | posts = importer.posts() 173 | if importer.success: 174 | self._save_posts(posts, service) 175 | self.redirect("/?imported=1") 176 | else: 177 | self.redirect("/?imported=0&error=%s"%importer.error) 178 | except NotImplementedError: 179 | self.redirect("/?imported=0&error=unknown_service") 180 | 181 | def _save_posts(self, posts, service): 182 | for post in posts: 183 | entry = Entry( 184 | service=service, 185 | hash=post['hash'], 186 | url=post['url'], 187 | title=post['title'], 188 | description=post['description'], 189 | time=post['time'], 190 | tags = [db.Category(tag) for tag in post['tags'] if tag ] 191 | ) 192 | entry.put() 193 | self.free_cache() 194 | 195 | 196 | class DeleteHandler(BaseHandler): 197 | @administrator 198 | def get(self): 199 | key = self.get_argument("key") 200 | try: 201 | entry = Entry.get(key) 202 | self.free_cache(tags=entry.tags) 203 | except db.BadKeyError: 204 | raise tornado.web.HTTPError(404) 205 | entry.delete() 206 | self.redirect("/") 207 | 208 | 209 | class EntryModule(tornado.web.UIModule): 210 | def render(self, entry): 211 | return self.render_string("modules/entry.html", entry=entry) 212 | 213 | 214 | settings = { 215 | "site_title": getattr(settings, 'SITE_TITLE', u'My Bookmarks'), 216 | "template_path": os.path.join(os.path.dirname(__file__), "templates"), 217 | "ui_modules": {"Entry": EntryModule,}, 218 | "xsrf_cookies": True, 219 | "debug": os.environ.get("SERVER_SOFTWARE", "").startswith("Development/"), 220 | } 221 | 222 | application = tornado.wsgi.WSGIApplication([ 223 | (r"/", HomeHandler), 224 | (r"/archive", ArchiveHandler), 225 | (r"/tag/([^/]+)/?", TagHandler), 226 | (r"/post", BookmarkHandler), 227 | (r"/import", ImportHandler), 228 | (r"/index", tornado.web.RedirectHandler, {"url": "/archive"}), 229 | (r"/delete", DeleteHandler), 230 | ], **settings) 231 | 232 | 233 | def main(): 234 | wsgiref.handlers.CGIHandler().run(application) 235 | 236 | 237 | if __name__ == "__main__": 238 | main() 239 | -------------------------------------------------------------------------------- /settings.sample.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Copy this file to settings.py and change the variable below to customize your 6 | bookmarking site. 7 | """ 8 | 9 | SITE_TITLE = "My SELFICIOUS!" 10 | 11 | -------------------------------------------------------------------------------- /static/css/style.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | margin:0; 3 | padding:0; 4 | color:#000; 5 | } 6 | 7 | body { 8 | min-width:800px; 9 | font-family:Arial, sans-serif; 10 | font-size: 62.5%; 11 | padding-top:1em; 12 | } 13 | 14 | #wrap { 15 | margin:0px auto; 16 | width:800px; 17 | font-size:1.2em; 18 | line-height:1.4em; 19 | } 20 | 21 | #header { 22 | font-size:1em; 23 | position:relative; 24 | margin:20px 0; 25 | margin-bottom:40px; 26 | } 27 | 28 | #header h1 { 29 | margin:0; 30 | } 31 | 32 | #main { 33 | float:left; 34 | width:550px; 35 | font-size:1em; 36 | margin-top:3em; 37 | } 38 | 39 | .entry{ 40 | font-size:1.1em; 41 | line-height:1.4em; 42 | border-bottom:1px solid #eee; 43 | margin-bottom:1em; 44 | } 45 | 46 | .meta , .footer{ 47 | font-size:.78em; 48 | margin-bottom:.714em; 49 | color:#444; 50 | } 51 | 52 | #sidebar { 53 | border-left:1px dotted #ccc; 54 | float:right; 55 | width:235px; 56 | font-size:1em; 57 | } 58 | 59 | .section{ 60 | padding: .125em 0; 61 | margin: 0 0 .125em 1.5em; 62 | } 63 | 64 | #sidebar ul { 65 | list-style:none; 66 | padding: .125em 0; 67 | margin: .125em 0; 68 | } 69 | 70 | #footer { 71 | font-size:.85em; 72 | color:#444; 73 | margin:2em 0 0 0; 74 | border-top:1px solid #ddd; 75 | clear:both; 76 | } 77 | #footer address { 78 | font-style:normal; 79 | padding:1em 0; 80 | } 81 | 82 | /*- typography -- */ 83 | 84 | 85 | h1,h2,h3,h4,h5,h6 { 86 | color: #111; 87 | } 88 | 89 | 90 | /* Typography 91 | -------------------------------------------------------------- */ 92 | 93 | h1,h2,h3,h4,h5,h6 { 94 | font-weight: normal; 95 | margin-bottom:.71em; 96 | letter-spacing:1px; 97 | } 98 | h1 {font-size:2em} 99 | h2 {font-size:1.4em; font-weight:bold;} 100 | h3 {font-size:1em} 101 | h4 {font-size:1em} 102 | 103 | #sidebar h2 { 104 | font-size:1.1em; 105 | font-weight:normal; 106 | text-transform:uppercase; 107 | color: #111; 108 | } 109 | 110 | p{ 111 | margin: 0 0 1em; 112 | } 113 | 114 | em{ 115 | font-family:georgia, "Times New Roman", serif; 116 | font-style:italic; 117 | text-transform:lowercase; 118 | background-color:#ff0; 119 | padding:.125em; 120 | } 121 | 122 | p em{ 123 | background-color:#fff; 124 | } 125 | 126 | a:focus, 127 | a:hover { color: #f20; text-decoration: underline;} 128 | a { color: #00d; text-decoration:none;} 129 | 130 | /* forms 131 | -------------------------------------------------------------- */ 132 | 133 | label { font-weight: bold;} 134 | 135 | 136 | /* Text fields */ 137 | input.text { width: 40%; margin:0.5em 0.5em 0.5em 0; } 138 | input.text { border:1px solid #bbb; background:#f6f6f6; padding:5px; } 139 | input.text:focus { border:1px solid #999; background:#fff; } 140 | 141 | /* Textareas */ 142 | textarea { width: 400px; margin:0.5em 0.5em 0.5em 0; } 143 | textarea { border:1px solid #bbb; background:#eee; padding:5px; } 144 | textarea:focus { border:1px solid #999; background:#fff; } 145 | 146 | 147 | /* misc. 148 | -------------------------------------------------------------- */ 149 | 150 | a img{ 151 | border:0; 152 | } 153 | .right{ 154 | float:right; 155 | margin: 0 20px; 156 | } 157 | .left{ 158 | float:left; 159 | margin: 0 10px 5px 0; 160 | } 161 | 162 | .clearboth{ 163 | clear:both; 164 | } 165 | .formfooter{ 166 | text-align:right; 167 | border-top:1px solid #ddd; 168 | padding-top:15px; 169 | } 170 | .two3rds{ 171 | width:75%; 172 | } 173 | .control{ 174 | background-color:#f8f8f8; 175 | border:1px solid #ccc; 176 | padding:.25em; 177 | } 178 | ul.entries { 179 | list-style-type: none; 180 | margin: 0; 181 | padding: 0; 182 | } 183 | ul.entries li { 184 | margin-bottom: 1em; 185 | } 186 | ul.entries .title { 187 | font-size:.85em; 188 | } 189 | 190 | .tags a{ 191 | color:#03a; 192 | } 193 | .form{ 194 | width:90%; 195 | border:1px solid #ddd; 196 | padding:1.5em; 197 | } 198 | .message{ 199 | padding:4px; 200 | margin:0 0 2em 0; 201 | } 202 | .ok{ 203 | border:1px solid #9cc857; 204 | background:#d0fd89; 205 | } 206 | .error{ 207 | border:1px solid #f47dc9; 208 | background:#fbdcf0; 209 | } 210 | .hidden{ 211 | display: none; 212 | } 213 | .description{ 214 | background:lightyellow; 215 | border:1px solid #fe0; 216 | padding:.75em; 217 | } 218 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/initpy/selficious/4f57f00a36b40c71ffab031bfc41b2a17a7b939f/static/favicon.ico -------------------------------------------------------------------------------- /static/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/initpy/selficious/4f57f00a36b40c71ffab031bfc41b2a17a7b939f/static/images/logo.png -------------------------------------------------------------------------------- /static/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 24 | 28 | 32 | 33 | 40 | 47 | 55 | 59 | 60 | 71 | 73 | 77 | 81 | 82 | 91 | 94 | 98 | 102 | 103 | 106 | 111 | 112 | 120 | 124 | 125 | 134 | 137 | 141 | 145 | 146 | 154 | 158 | 159 | 168 | 171 | 175 | 179 | 180 | 188 | 192 | 193 | 202 | 205 | 209 | 213 | 214 | 218 | 222 | 223 | 227 | 231 | 232 | 239 | 242 | 246 | 247 | 254 | 262 | 266 | 267 | 269 | 273 | 277 | 278 | 281 | 286 | 287 | 295 | 299 | 300 | 308 | 312 | 313 | 321 | 325 | 326 | 330 | 334 | 335 | 339 | 343 | 344 | 347 | 351 | 352 | 362 | 373 | 382 | 391 | 400 | 409 | 420 | 429 | 438 | 447 | 456 | 467 | 476 | 485 | 494 | 503 | 504 | 522 | 524 | 525 | 527 | image/svg+xml 528 | 530 | 531 | 532 | 533 | 534 | 539 | SELFICIOUS 551 | 555 | 559 | 564 | 568 | 571 | 577 | 581 | 586 | 590 | 596 | 600 | 605 | 609 | 613 | 618 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 645 | SELFICIOUS 656 | 659 | 665 | 668 | 673 | 677 | 683 | 687 | 692 | 696 | 700 | 705 | 721 | 722 | 723 | 724 | 725 | -------------------------------------------------------------------------------- /templates/archive.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}{{ escape(handler.settings["site_title"]) }} - Everything{% end %} 4 | 5 | {% block content %} 6 |

Archive

7 |
    8 | {% for entry in entries %} 9 |
  • 10 | 11 | {{ locale.format_date(entry.time, full_format=True, shorter=True) }} 12 | → 13 | {{ escape(entry.title) }} 14 |
  • 15 | {% end %} 16 |
17 | {% end %} 18 | -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | {% set site_title = escape(handler.settings["site_title"]) %} 2 | {% set debug = handler.settings["debug"] %} 3 | 5 | 6 | 7 | 8 | {% block title %}{{site_title}}{% end %} 9 | 10 | 11 | {% block extrameta %}{% end %} 12 | {% block extracss %}{% end %} 13 | 14 | {% block pagejs %} 15 | {% if debug %} 16 | 17 | {% else %} 18 | 19 | {% end %} 20 | {% end %} 21 | {% block extrajs %}{% end %} 22 | 23 | 24 | 25 |
26 | 27 | 34 | 35 |
36 | {% block content %}{% end %} 37 | 43 |
44 | 45 | {% block side %} 46 | 71 | {% end %} 72 |
73 | 74 | 75 | -------------------------------------------------------------------------------- /templates/entry.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}{{ escape(entry.title) }} - {{ 4 | escape(handler.settings["site_title"]) }}{% end %} 5 | 6 | {% block content %} 7 | {{ modules.Entry(entry) }} 8 | {% end %} 9 | 10 | {% block extrajs %} 11 | {% if current_user and current_user.administrator %} 12 | 22 | {% end %} 23 | {% end %} 24 | -------------------------------------------------------------------------------- /templates/form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 5 |

{{ _("Edit this post") if entry else _("Post Something New ") }}

6 | 7 |
8 |
9 |

10 | 11 | 12 |

13 |

14 | 15 | 16 |

17 |

18 |
19 | 22 |

23 |

24 | 25 | 26 | (Comma separated) 27 |

28 | 29 |

30 | 31 |  {{ _("Cancel") }} 32 |

33 | {% if entry %} 34 | 35 | {% end %} 36 | {{ xsrf_form_html() }} 37 |
38 |
39 | {% end %} 40 | -------------------------------------------------------------------------------- /templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | {% if import_success == "1" %} 5 |

Yay! Your posts were successfully imported!

6 | {% else %} 7 | {% if import_success == "0" %} 8 |

Oh No! Your posts coulden't be imported! {{ _(error_message) }}

9 | {% end %} 10 | {% end %} 11 | {% for entry in entries %} 12 | {{ modules.Entry(entry) }} 13 | {% end %} 14 | 15 | {% end %} 16 | 17 | {% block extrajs %} 18 | {% if current_user and current_user.administrator %} 19 | 29 | {% end %} 30 | {% end %} 31 | -------------------------------------------------------------------------------- /templates/import.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | {% block extrajs %} 5 | 17 | {% end %} 18 | 19 | 20 | {% block content %} 21 |

{{ _("Import your stuff") }}

22 |
23 |
24 |

25 | 26 | 31 |

32 | 33 | {% for service in services %} 34 | 38 | {% end %} 39 | 40 |

41 | 42 | {{ xsrf_form_html() }} 43 |

44 |
45 |
46 | {% end %} 47 | -------------------------------------------------------------------------------- /templates/modules/entry.html: -------------------------------------------------------------------------------- 1 |
2 |

{{ escape(entry.title) }}

3 |
4 | {{ locale.format_date(entry.time, full_format=True, shorter=True) }} 5 | {% if entry.tags %} 6 | in 7 | 8 | {{ locale.list(['%(escaped_tag)s' % {"tag": tag, "escaped_tag": escape(tag)} for tag in sorted(entry.tags)]) }} 9 | 10 | {% end %} 11 | {% if current_user and current_user.administrator %} 12 | 13 | {{ _("Edit") }} 14 | — 15 | {{ _("Delete") }} 16 | 17 | {% end %} 18 |
19 |

20 | {{ escape(entry.description) }} 21 |

22 |
23 | -------------------------------------------------------------------------------- /templates/tag.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}{{ escape(handler.settings["site_title"]) }} - Entries tagged: {{ tag }}{% end %} 4 | 5 | {% block content %} 6 |

Entries Tagged: {{ tag }}

7 |
    8 | {% for entry in entries %} 9 |
  • 10 | 11 | {{ locale.format_date(entry.time, full_format=True, shorter=True) }} 12 | → 13 | {{ escape(entry.title) }} 14 |
  • 15 | {% end %} 16 |
17 | {% end %} 18 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # This is SELFICIOUS by Yuuta 5 | # UPDATED: 2010-12-22 22:28:40 6 | 7 | import logging 8 | from google.appengine.api import memcache 9 | 10 | def keygen(format, *args, **kwargs): 11 | """generates a key from args and kwargs using format""" 12 | allargs = args+tuple(kwargs[key] for key in sorted(kwargs.keys())) 13 | key = format % allargs[0:format.count('%')] 14 | return key 15 | 16 | def memoize(keyformat, time=600, cache_null=False): 17 | """Decorator to memoize functions using memcache.""" 18 | def decorator(fxn): 19 | def wrapper(self, *args, **kwargs): 20 | key = keygen(keyformat, *args, **kwargs) 21 | data = memcache.get(key) 22 | if data is not None: 23 | logging.info('From memcache: %s' % key) 24 | return data 25 | data = fxn(self, *args, **kwargs) 26 | if data or cache_null: 27 | memcache.set(key, data, time) 28 | return data 29 | return wrapper 30 | return decorator 31 | 32 | def unmemoize(keys_list): 33 | memcache.delete_multi(keys_list) 34 | --------------------------------------------------------------------------------