From d860f1db10d8c7a8978b9d5f2c0181941e645c14 Mon Sep 17 00:00:00 2001 From: Kfir Hadas <sharkykh@gmail.com> Date: Sun, 2 Jul 2017 12:13:05 +0300 Subject: [PATCH] Update Python dependencies - 2nd batch (#3877) * Update Unidecode to 0.04.20 (was 0.04.12) * Vanilla babelfish @ 0.5.5 Most recent version. * Update markdown2 to 2.3.4 (was 596d48b ~ 2.3.1) * List versions of decorator & validators Upgrading these breaks url validation. validators.url() was changed in newer version, and it fails on urls with spaces in them AFAICT. * Update xmltodict to 0.11.0 (was 0.9.2) * Update python-dateutil to 2.6.0 (was d05b837 ~ 2.5.0) * Replace unmaintained SocksiPy, with maintained PySocks PySocks==1.6.7 win-inet-pton==1.0.1 # Required on Windows systems * Remove simplejson (unused) * Update tzlocal to 1.4, remove tests Dependencies: - pytz [required: Any] (already installed) * Update FeedParser to f1dd1bb923ebfe6482fc2521c1f150b4032289ec For now we'll use the latest commit available * Update PyGithub to v1.34 vanilla (latest) Dependencies: - pyjwt @ 1.5.0 (new) * Fix Github Exceptions imports * Revert changes in IMDB lib * Update imdb to 5.1.1 Install arguments: `pip install imdbpy --no-deps --global-option="--without-sqlobject" --global-option="--without-sqlalchemy"` --- lib/README.md | 17 + lib/babelfish/data/get_files.py | 45 - lib/dateutil/__init__.py | 2 +- lib/dateutil/_common.py | 33 + lib/dateutil/easter.py | 6 +- lib/dateutil/parser.py | 9 +- lib/dateutil/relativedelta.py | 79 +- lib/dateutil/rrule.py | 57 +- lib/dateutil/test/_common.py | 102 - lib/dateutil/test/test_easter.py | 99 - lib/dateutil/test/test_imports.py | 149 - lib/dateutil/test/test_parser.py | 779 --- lib/dateutil/test/test_relativedelta.py | 484 -- lib/dateutil/test/test_rrule.py | 4676 ----------------- lib/dateutil/test/test_tz.py | 525 -- lib/dateutil/tz/_common.py | 366 +- lib/dateutil/tz/tz.py | 1057 +++- lib/dateutil/tz/win.py | 131 +- lib/dateutil/zoneinfo/__init__.py | 93 +- .../zoneinfo/dateutil-zoneinfo.tar.gz | Bin 154021 -> 139671 bytes lib/feedparser/api.py | 1 - lib/feedparser/util.py | 20 +- lib/github/AuthenticatedUser.py | 8 +- lib/github/Authorization.py | 6 +- lib/github/AuthorizationApplication.py | 6 +- lib/github/Branch.py | 31 +- lib/github/Commit.py | 18 +- lib/github/CommitCombinedStatus.py | 114 + lib/github/CommitComment.py | 6 +- lib/github/CommitStats.py | 3 +- lib/github/CommitStatus.py | 20 +- lib/github/Comparison.py | 3 +- lib/github/Consts.py | 5 +- lib/github/ContentFile.py | 6 +- lib/github/Download.py | 6 +- lib/github/Event.py | 6 +- lib/github/File.py | 16 +- lib/github/Gist.py | 6 +- lib/github/GistComment.py | 6 +- lib/github/GistFile.py | 6 +- lib/github/GistHistoryState.py | 3 +- lib/github/GitAuthor.py | 6 +- lib/github/GitBlob.py | 6 +- lib/github/GitCommit.py | 6 +- lib/github/GitObject.py | 6 +- lib/github/GitRef.py | 22 +- lib/github/GitRelease.py | 17 +- lib/github/GitTag.py | 6 +- lib/github/GitTree.py | 6 +- lib/github/GitTreeElement.py | 6 +- lib/github/GithubException.py | 4 +- lib/github/GithubObject.py | 29 +- lib/github/GitignoreTemplate.py | 6 +- lib/github/Hook.py | 6 +- lib/github/HookDescription.py | 6 +- lib/github/HookResponse.py | 6 +- lib/github/InputFileContent.py | 3 +- lib/github/InputGitAuthor.py | 6 +- lib/github/InputGitTreeElement.py | 3 +- lib/github/Installation.py | 75 + lib/github/InstallationAuthorization.py | 72 + lib/github/Issue.py | 68 +- lib/github/IssueComment.py | 6 +- lib/github/IssueEvent.py | 6 +- lib/github/IssuePullRequest.py | 3 +- lib/github/Label.py | 6 +- lib/github/Legacy.py | 3 +- lib/github/MainClass.py | 104 +- lib/github/Milestone.py | 6 +- lib/github/NamedUser.py | 6 +- lib/github/Notification.py | 6 +- lib/github/NotificationSubject.py | 6 +- lib/github/Organization.py | 13 +- lib/github/PaginatedList.py | 14 +- lib/github/Permissions.py | 10 +- lib/github/Plan.py | 6 +- lib/github/PullRequest.py | 64 +- lib/github/PullRequestComment.py | 6 +- lib/github/PullRequestMergeStatus.py | 6 +- lib/github/PullRequestPart.py | 6 +- lib/github/PullRequestReview.py | 121 + lib/github/PullRequestReviewerRequest.py | 66 + lib/github/Rate.py | 6 +- lib/github/RateLimit.py | 6 +- lib/github/Repository.py | 262 +- lib/github/RepositoryKey.py | 6 +- lib/github/Requester.py | 14 +- lib/github/Stargazer.py | 7 +- lib/github/StatsCodeFrequency.py | 3 +- lib/github/StatsCommitActivity.py | 3 +- lib/github/StatsContributor.py | 3 +- lib/github/StatsParticipation.py | 3 +- lib/github/StatsPunchCard.py | 3 +- lib/github/Status.py | 6 +- lib/github/StatusMessage.py | 6 +- lib/github/Tag.py | 9 +- lib/github/Team.py | 25 +- lib/github/UserKey.py | 6 +- lib/github/__init__.py | 7 +- lib/imdb/Character.py | 2 +- lib/imdb/Company.py | 2 +- lib/imdb/Movie.py | 2 +- lib/imdb/Person.py | 2 +- lib/imdb/__init__.py | 6 +- lib/imdb/_compat.py | 2 +- lib/imdb/_exceptions.py | 2 +- lib/imdb/_logging.py | 2 +- lib/imdb/helpers.py | 2 +- lib/imdb/linguistics.py | 2 +- lib/imdb/locale/__init__.py | 2 +- lib/imdb/locale/ar/LC_MESSAGES/imdbpy.mo | Bin 0 -> 3037 bytes lib/imdb/locale/bg/LC_MESSAGES/imdbpy.mo | Bin 0 -> 5655 bytes lib/imdb/locale/de/LC_MESSAGES/imdbpy.mo | Bin 0 -> 4596 bytes lib/imdb/locale/en/LC_MESSAGES/imdbpy.mo | Bin 0 -> 13723 bytes lib/imdb/locale/es/LC_MESSAGES/imdbpy.mo | Bin 0 -> 15620 bytes lib/imdb/locale/fr/LC_MESSAGES/imdbpy.mo | Bin 0 -> 3210 bytes lib/imdb/locale/generatepot.py | 2 +- lib/imdb/locale/imdbpy-ar.po | 8 +- lib/imdb/locale/imdbpy-bg.po | 8 +- lib/imdb/locale/imdbpy-de.po | 4 +- lib/imdb/locale/imdbpy-es.po | 9 +- lib/imdb/locale/imdbpy-fr.po | 22 +- lib/imdb/locale/imdbpy-pt_BR.po | 1303 +++++ lib/imdb/locale/it/LC_MESSAGES/imdbpy.mo | Bin 0 -> 13779 bytes lib/imdb/locale/pt_BR/LC_MESSAGES/imdbpy.mo | Bin 0 -> 1004 bytes lib/imdb/locale/rebuildmo.py | 2 +- lib/imdb/locale/tr/LC_MESSAGES/imdbpy.mo | Bin 0 -> 11191 bytes lib/imdb/parser/__init__.py | 2 +- lib/imdb/parser/http/__init__.py | 2 +- lib/imdb/parser/http/bsouplxml/bsoupxpath.py | 2 +- lib/imdb/parser/http/bsouplxml/etree.py | 2 +- lib/imdb/parser/http/bsouplxml/html.py | 2 +- lib/imdb/parser/http/characterParser.py | 2 +- lib/imdb/parser/http/companyParser.py | 2 +- lib/imdb/parser/http/movieParser.py | 58 +- lib/imdb/parser/http/personParser.py | 8 +- lib/imdb/parser/http/searchCharacterParser.py | 2 +- lib/imdb/parser/http/searchCompanyParser.py | 2 +- lib/imdb/parser/http/searchKeywordParser.py | 10 +- lib/imdb/parser/http/searchMovieParser.py | 3 +- lib/imdb/parser/http/searchPersonParser.py | 2 +- lib/imdb/parser/http/topBottomParser.py | 27 +- lib/imdb/parser/http/utils.py | 11 +- lib/imdb/parser/mobile/__init__.py | 2 +- lib/imdb/parser/sql/__init__.py | 1595 ------ lib/imdb/parser/sql/alchemyadapter.py | 513 -- lib/imdb/parser/sql/cutils.c | 269 - lib/imdb/parser/sql/dbschema.py | 476 -- lib/imdb/parser/sql/objectadapter.py | 211 - lib/imdb/utils.py | 8 +- lib/jwt/__init__.py | 29 + lib/jwt/__main__.py | 136 + lib/jwt/algorithms.py | 428 ++ lib/jwt/api_jws.py | 215 + lib/jwt/api_jwt.py | 183 + lib/jwt/compat.py | 76 + .../test => jwt/contrib}/__init__.py | 0 lib/jwt/contrib/algorithms/__init__.py | 0 lib/jwt/contrib/algorithms/py_ecdsa.py | 60 + lib/jwt/contrib/algorithms/pycrypto.py | 47 + lib/jwt/exceptions.py | 48 + lib/jwt/utils.py | 113 + lib/markdown2.py | 443 +- lib/simplejson/__init__.py | 318 -- lib/simplejson/_speedups.c | 2329 -------- lib/simplejson/decoder.py | 354 -- lib/simplejson/encoder.py | 440 -- lib/simplejson/scanner.py | 65 - lib/socks.py | 831 +++ lib/socks/__init__.py | 393 -- lib/sockshandler.py | 79 + lib/tzlocal/darwin.py | 34 +- lib/tzlocal/test_data/Harare | Bin 157 -> 0 bytes lib/tzlocal/test_data/localtime/etc/localtime | Bin 157 -> 0 bytes .../test_data/symlink_localtime/etc/localtime | 1 - .../usr/share/zoneinfo/Africa/Harare | Bin 157 -> 0 bytes lib/tzlocal/test_data/timezone/etc/timezone | 1 - .../timezone_setting/etc/conf.d/clock | 1 - .../test_data/vardbzoneinfo/var/db/zoneinfo | 1 - .../zone_setting/etc/sysconfig/clock | 1 - lib/tzlocal/tests.py | 79 - lib/tzlocal/windows_tz.py | 4 +- lib/unidecode/__init__.py | 54 +- lib/unidecode/util.py | 58 + lib/unidecode/x000.py | 163 +- lib/unidecode/x002.py | 2 +- lib/unidecode/x005.py | 4 +- lib/unidecode/x020.py | 44 +- lib/unidecode/x021.py | 96 +- lib/unidecode/x022.py | 22 +- lib/unidecode/x023.py | 6 +- lib/unidecode/x024.py | 265 +- lib/unidecode/x026.py | 2 +- lib/unidecode/x027.py | 12 +- lib/unidecode/x029.py | 257 + lib/unidecode/x02a.py | 257 + lib/unidecode/x032.py | 68 +- lib/unidecode/x033.py | 13 +- lib/unidecode/x04e.py | 2 +- lib/unidecode/x1f1.py | 258 + lib/win_inet_pton.py | 84 + lib/xmltodict.py | 167 +- patches/feedparser.diff | 43 - sickbeard/logger.py | 3 +- sickrage/helper/common.py | 3 +- 205 files changed, 8241 insertions(+), 15155 deletions(-) delete mode 100644 lib/babelfish/data/get_files.py create mode 100644 lib/dateutil/_common.py delete mode 100644 lib/dateutil/test/_common.py delete mode 100644 lib/dateutil/test/test_easter.py delete mode 100644 lib/dateutil/test/test_imports.py delete mode 100644 lib/dateutil/test/test_parser.py delete mode 100644 lib/dateutil/test/test_relativedelta.py delete mode 100644 lib/dateutil/test/test_rrule.py delete mode 100644 lib/dateutil/test/test_tz.py create mode 100644 lib/github/CommitCombinedStatus.py create mode 100644 lib/github/Installation.py create mode 100644 lib/github/InstallationAuthorization.py create mode 100644 lib/github/PullRequestReview.py create mode 100644 lib/github/PullRequestReviewerRequest.py create mode 100644 lib/imdb/locale/ar/LC_MESSAGES/imdbpy.mo create mode 100644 lib/imdb/locale/bg/LC_MESSAGES/imdbpy.mo create mode 100644 lib/imdb/locale/de/LC_MESSAGES/imdbpy.mo create mode 100644 lib/imdb/locale/en/LC_MESSAGES/imdbpy.mo create mode 100644 lib/imdb/locale/es/LC_MESSAGES/imdbpy.mo create mode 100644 lib/imdb/locale/fr/LC_MESSAGES/imdbpy.mo create mode 100644 lib/imdb/locale/imdbpy-pt_BR.po create mode 100644 lib/imdb/locale/it/LC_MESSAGES/imdbpy.mo create mode 100644 lib/imdb/locale/pt_BR/LC_MESSAGES/imdbpy.mo create mode 100644 lib/imdb/locale/tr/LC_MESSAGES/imdbpy.mo delete mode 100644 lib/imdb/parser/sql/__init__.py delete mode 100644 lib/imdb/parser/sql/alchemyadapter.py delete mode 100644 lib/imdb/parser/sql/cutils.c delete mode 100644 lib/imdb/parser/sql/dbschema.py delete mode 100644 lib/imdb/parser/sql/objectadapter.py create mode 100644 lib/jwt/__init__.py create mode 100644 lib/jwt/__main__.py create mode 100644 lib/jwt/algorithms.py create mode 100644 lib/jwt/api_jws.py create mode 100644 lib/jwt/api_jwt.py create mode 100644 lib/jwt/compat.py rename lib/{dateutil/test => jwt/contrib}/__init__.py (100%) create mode 100644 lib/jwt/contrib/algorithms/__init__.py create mode 100644 lib/jwt/contrib/algorithms/py_ecdsa.py create mode 100644 lib/jwt/contrib/algorithms/pycrypto.py create mode 100644 lib/jwt/exceptions.py create mode 100644 lib/jwt/utils.py delete mode 100644 lib/simplejson/__init__.py delete mode 100644 lib/simplejson/_speedups.c delete mode 100644 lib/simplejson/decoder.py delete mode 100644 lib/simplejson/encoder.py delete mode 100644 lib/simplejson/scanner.py create mode 100644 lib/socks.py delete mode 100644 lib/socks/__init__.py create mode 100644 lib/sockshandler.py delete mode 100644 lib/tzlocal/test_data/Harare delete mode 100644 lib/tzlocal/test_data/localtime/etc/localtime delete mode 120000 lib/tzlocal/test_data/symlink_localtime/etc/localtime delete mode 100644 lib/tzlocal/test_data/symlink_localtime/usr/share/zoneinfo/Africa/Harare delete mode 100644 lib/tzlocal/test_data/timezone/etc/timezone delete mode 100644 lib/tzlocal/test_data/timezone_setting/etc/conf.d/clock delete mode 100644 lib/tzlocal/test_data/vardbzoneinfo/var/db/zoneinfo delete mode 100644 lib/tzlocal/test_data/zone_setting/etc/sysconfig/clock delete mode 100644 lib/tzlocal/tests.py create mode 100644 lib/unidecode/util.py create mode 100644 lib/unidecode/x029.py create mode 100644 lib/unidecode/x02a.py create mode 100644 lib/unidecode/x1f1.py create mode 100644 lib/win_inet_pton.py delete mode 100644 patches/feedparser.diff diff --git a/lib/README.md b/lib/README.md index cf73cb021..bd4daba31 100644 --- a/lib/README.md +++ b/lib/README.md @@ -17,14 +17,24 @@ Add the output to the list below to the appropriate location (based on the top-l Packages List ========= ``` +babelfish=0.5.5 beautifulsoup4==4.5.3 bencode==1.0 # certgen.py==d52975c # source: https://github.com/pyca/pyopenssl/blob/d52975cef3a36e18552aeb23de7c06aa73d76454/examples/certgen.py +git+https://github.com/kurtmckee/feedparser.git@f1dd1bb923ebfe6482fc2521c1f150b4032289ec#egg=feedparser html5lib==0.999999999 - six [required: Any, installed: 1.10.0] - webencodings [required: Any, installed: 0.5.1] +IMDbPY==5.1.1 Mako==1.0.6 - MarkupSafe [required: >=0.9.2, installed: 1.0] +markdown2==2.3.4 +PyGithub==1.34 + - pyjwt [required: Any, installed: 1.5.0] +PySocks==1.6.7 + - win-inet-pton==1.0.1 +python-dateutil==2.6.0 + - six [required: >=1.5, installed: 1.10.0] python-twitter==3.3 - future [required: Any, installed: 0.16.0] # <-- Not really needed, so not installed - requests [required: Any, installed: 2.18.1] @@ -43,4 +53,11 @@ tornado==4.5.1 - certifi [required: Any, installed: 2017.4.17] - singledispatch [required: Any, installed: 3.4.0.3] - six [required: Any, installed: 1.10.0] +tzlocal==1.4 + - pytz [required: Any, installed: 2016.4] +Unidecode==0.04.20 +validators==0.10 + - decorator [required: >=3.4.0, installed: 4.0.10] + - six [required: >=1.4.0, installed: 1.10.0] +xmltodict==0.11.0 ``` diff --git a/lib/babelfish/data/get_files.py b/lib/babelfish/data/get_files.py deleted file mode 100644 index aaa090ccc..000000000 --- a/lib/babelfish/data/get_files.py +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# -# Copyright (c) 2013 the BabelFish authors. All rights reserved. -# Use of this source code is governed by the 3-clause BSD license -# that can be found in the LICENSE file. -# -from __future__ import unicode_literals -import os.path -import tempfile -import zipfile -import requests - - -DATA_DIR = os.path.dirname(__file__) - -# iso-3166-1.txt -print('Downloading ISO-3166-1 standard (ISO country codes)...') -with open(os.path.join(DATA_DIR, 'iso-3166-1.txt'), 'w') as f: - r = requests.get('http://www.iso.org/iso/home/standards/country_codes/country_names_and_code_elements_txt.htm') - f.write(r.content.strip()) - -# iso-639-3.tab -print('Downloading ISO-639-3 standard (ISO language codes)...') -with tempfile.TemporaryFile() as f: - r = requests.get('http://www-01.sil.org/iso639-3/iso-639-3_Code_Tables_20130531.zip') - f.write(r.content) - with zipfile.ZipFile(f) as z: - z.extract('iso-639-3.tab', DATA_DIR) - -# iso-15924 -print('Downloading ISO-15924 standard (ISO script codes)...') -with tempfile.TemporaryFile() as f: - r = requests.get('http://www.unicode.org/iso15924/iso15924.txt.zip') - f.write(r.content) - with zipfile.ZipFile(f) as z: - z.extract('iso15924-utf8-20131012.txt', DATA_DIR) - -# opensubtitles supported languages -print('Downloading OpenSubtitles supported languages...') -with open(os.path.join(DATA_DIR, 'opensubtitles_languages.txt'), 'w') as f: - r = requests.get('http://www.opensubtitles.org/addons/export_languages.php') - f.write(r.content) - -print('Done!') diff --git a/lib/dateutil/__init__.py b/lib/dateutil/__init__.py index 1f160ea5a..ba89aa70b 100644 --- a/lib/dateutil/__init__.py +++ b/lib/dateutil/__init__.py @@ -1,2 +1,2 @@ # -*- coding: utf-8 -*- -__version__ = "2.5.0" +__version__ = "2.6.0" diff --git a/lib/dateutil/_common.py b/lib/dateutil/_common.py new file mode 100644 index 000000000..cd2a33860 --- /dev/null +++ b/lib/dateutil/_common.py @@ -0,0 +1,33 @@ +""" +Common code used in multiple modules. +""" + +class weekday(object): + __slots__ = ["weekday", "n"] + + def __init__(self, weekday, n=None): + self.weekday = weekday + self.n = n + + def __call__(self, n): + if n == self.n: + return self + else: + return self.__class__(self.weekday, n) + + def __eq__(self, other): + try: + if self.weekday != other.weekday or self.n != other.n: + return False + except AttributeError: + return False + return True + + __hash__ = None + + def __repr__(self): + s = ("MO", "TU", "WE", "TH", "FR", "SA", "SU")[self.weekday] + if not self.n: + return s + else: + return "%s(%+d)" % (s, self.n) diff --git a/lib/dateutil/easter.py b/lib/dateutil/easter.py index 8d30c4ebd..e4def97f9 100644 --- a/lib/dateutil/easter.py +++ b/lib/dateutil/easter.py @@ -33,9 +33,9 @@ def easter(year, method=EASTER_WESTERN): These methods are represented by the constants: - EASTER_JULIAN = 1 - EASTER_ORTHODOX = 2 - EASTER_WESTERN = 3 + * ``EASTER_JULIAN = 1`` + * ``EASTER_ORTHODOX = 2`` + * ``EASTER_WESTERN = 3`` The default method is method 3. diff --git a/lib/dateutil/parser.py b/lib/dateutil/parser.py index 759094089..147b3f2ca 100644 --- a/lib/dateutil/parser.py +++ b/lib/dateutil/parser.py @@ -56,6 +56,10 @@ class _timelex(object): if isinstance(instream, text_type): instream = StringIO(instream) + if getattr(instream, 'read', None) is None: + raise TypeError('Parser must be a string or character stream, not ' + '{itype}'.format(itype=instream.__class__.__name__)) + self.instream = instream self.charstack = [] self.tokenstack = [] @@ -464,7 +468,10 @@ class _ymd(list): self.find_probable_year_index(_timelex.split(self.tzstr)) == 0 or \ (yearfirst and self[1] <= 12 and self[2] <= 31): # 99-01-01 - year, month, day = self + if dayfirst and self[2] <= 12: + year, day, month = self + else: + year, month, day = self elif self[0] > 12 or (dayfirst and self[1] <= 12): # 13-01-01 day, month, year = self diff --git a/lib/dateutil/relativedelta.py b/lib/dateutil/relativedelta.py index 0217d57f5..7e3bd12ac 100644 --- a/lib/dateutil/relativedelta.py +++ b/lib/dateutil/relativedelta.py @@ -8,39 +8,12 @@ from math import copysign from six import integer_types from warnings import warn -__all__ = ["relativedelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"] - - -class weekday(object): - __slots__ = ["weekday", "n"] - - def __init__(self, weekday, n=None): - self.weekday = weekday - self.n = n - - def __call__(self, n): - if n == self.n: - return self - else: - return self.__class__(self.weekday, n) - - def __eq__(self, other): - try: - if self.weekday != other.weekday or self.n != other.n: - return False - except AttributeError: - return False - return True - - def __repr__(self): - s = ("MO", "TU", "WE", "TH", "FR", "SA", "SU")[self.weekday] - if not self.n: - return s - else: - return "%s(%+d)" % (s, self.n) +from ._common import weekday MO, TU, WE, TH, FR, SA, SU = weekdays = tuple([weekday(x) for x in range(7)]) +__all__ = ["relativedelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"] + class relativedelta(object): """ @@ -69,7 +42,7 @@ class relativedelta(object): Relative information, may be negative (argument is plural); adding or subtracting a relativedelta with relative information performs the corresponding aritmetic operation on the original datetime value - with the information in the relativedelta. + with the information in the relativedelta. weekday: One of the weekday instances (MO, TU, etc). These instances may @@ -299,16 +272,16 @@ class relativedelta(object): >>> relativedelta(days=1.5, hours=2).normalized() relativedelta(days=1, hours=14) - + :return: Returns a :class:`dateutil.relativedelta.relativedelta` object. """ # Cascade remainders down (rounding each to roughly nearest microsecond) days = int(self.days) - + hours_f = round(self.hours + 24 * (self.days - days), 11) hours = int(hours_f) - + minutes_f = round(self.minutes + 60 * (hours_f - hours), 10) minutes = int(minutes_f) @@ -347,8 +320,25 @@ class relativedelta(object): second=other.second or self.second, microsecond=(other.microsecond or self.microsecond)) + if isinstance(other, datetime.timedelta): + return self.__class__(years=self.years, + months=self.months, + days=self.days + other.days, + hours=self.hours, + minutes=self.minutes, + seconds=self.seconds + other.seconds, + microseconds=self.microseconds + other.microseconds, + leapdays=self.leapdays, + year=self.year, + month=self.month, + day=self.day, + weekday=self.weekday, + hour=self.hour, + minute=self.minute, + second=self.second, + microsecond=self.microsecond) if not isinstance(other, datetime.date): - raise TypeError("unsupported type for add operation") + return NotImplemented elif self._has_time and not isinstance(other, datetime.datetime): other = datetime.datetime.fromordinal(other.toordinal()) year = (self.year or other.year)+self.years @@ -397,7 +387,7 @@ class relativedelta(object): def __sub__(self, other): if not isinstance(other, relativedelta): - raise TypeError("unsupported type for sub operation") + return NotImplemented # In case the other object defines __rsub__ return self.__class__(years=self.years - other.years, months=self.months - other.months, days=self.days - other.days, @@ -454,7 +444,11 @@ class relativedelta(object): __nonzero__ = __bool__ def __mul__(self, other): - f = float(other) + try: + f = float(other) + except TypeError: + return NotImplemented + return self.__class__(years=int(self.years * f), months=int(self.months * f), days=int(self.days * f), @@ -476,7 +470,7 @@ class relativedelta(object): def __eq__(self, other): if not isinstance(other, relativedelta): - return False + return NotImplemented if self.weekday or other.weekday: if not self.weekday or not other.weekday: return False @@ -501,11 +495,18 @@ class relativedelta(object): self.second == other.second and self.microsecond == other.microsecond) + __hash__ = None + def __ne__(self, other): return not self.__eq__(other) def __div__(self, other): - return self.__mul__(1/float(other)) + try: + reciprocal = 1 / float(other) + except TypeError: + return NotImplemented + + return self.__mul__(reciprocal) __truediv__ = __div__ diff --git a/lib/dateutil/rrule.py b/lib/dateutil/rrule.py index 22d6dfca1..da94351b9 100644 --- a/lib/dateutil/rrule.py +++ b/lib/dateutil/rrule.py @@ -16,9 +16,11 @@ except ImportError: from fractions import gcd from six import advance_iterator, integer_types -from six.moves import _thread +from six.moves import _thread, range import heapq +from ._common import weekday as weekdaybase + # For warning about deprecation of until and count from warnings import warn @@ -58,37 +60,15 @@ FREQNAMES = ['YEARLY','MONTHLY','WEEKLY','DAILY','HOURLY','MINUTELY','SECONDLY'] easter = None parser = None - -class weekday(object): - __slots__ = ["weekday", "n"] - - def __init__(self, weekday, n=None): +class weekday(weekdaybase): + """ + This version of weekday does not allow n = 0. + """ + def __init__(self, wkday, n=None): if n == 0: - raise ValueError("Can't create weekday with n == 0") - - self.weekday = weekday - self.n = n + raise ValueError("Can't create weekday with n==0") - def __call__(self, n): - if n == self.n: - return self - else: - return self.__class__(self.weekday, n) - - def __eq__(self, other): - try: - if self.weekday != other.weekday or self.n != other.n: - return False - except AttributeError: - return False - return True - - def __repr__(self): - s = ("MO", "TU", "WE", "TH", "FR", "SA", "SU")[self.weekday] - if not self.n: - return s - else: - return "%s(%+d)" % (s, self.n) + super(weekday, self).__init__(wkday, n) MO, TU, WE, TH, FR, SA, SU = weekdays = tuple([weekday(x) for x in range(7)]) @@ -707,7 +687,7 @@ class rrule(rrulebase): parts.append('INTERVAL=' + str(self._interval)) if self._wkst: - parts.append('WKST=' + str(self._wkst)) + parts.append('WKST=' + repr(weekday(self._wkst))[0:2]) if self._count: parts.append('COUNT=' + str(self._count)) @@ -751,6 +731,21 @@ class rrule(rrulebase): output.append(';'.join(parts)) return '\n'.join(output) + def replace(self, **kwargs): + """Return new rrule with same attributes except for those attributes given new + values by whichever keyword arguments are specified.""" + new_kwargs = {"interval": self._interval, + "count": self._count, + "dtstart": self._dtstart, + "freq": self._freq, + "until": self._until, + "wkst": self._wkst, + "cache": False if self._cache is None else True } + new_kwargs.update(self._original_rule) + new_kwargs.update(kwargs) + return rrule(**new_kwargs) + + def _iter(self): year, month, day, hour, minute, second, weekday, yearday, _ = \ self._dtstart.timetuple() diff --git a/lib/dateutil/test/_common.py b/lib/dateutil/test/_common.py deleted file mode 100644 index 775738c69..000000000 --- a/lib/dateutil/test/_common.py +++ /dev/null @@ -1,102 +0,0 @@ -from __future__ import unicode_literals -try: - import unittest2 as unittest -except ImportError: - import unittest - -import os -import subprocess -import warnings - - -class WarningTestMixin(object): - # Based on https://stackoverflow.com/a/12935176/467366 - class _AssertWarnsContext(warnings.catch_warnings): - def __init__(self, expected_warnings, parent, **kwargs): - super(WarningTestMixin._AssertWarnsContext, self).__init__(**kwargs) - - self.parent = parent - try: - self.expected_warnings = list(expected_warnings) - except TypeError: - self.expected_warnings = [expected_warnings] - - self._warning_log = [] - - def __enter__(self, *args, **kwargs): - rv = super(WarningTestMixin._AssertWarnsContext, self).__enter__(*args, **kwargs) - - if self._showwarning is not self._module.showwarning: - super_showwarning = self._module.showwarning - else: - super_showwarning = None - - def showwarning(*args, **kwargs): - if super_showwarning is not None: - super_showwarning(*args, **kwargs) - - self._warning_log.append(warnings.WarningMessage(*args, **kwargs)) - - self._module.showwarning = showwarning - return rv - - def __exit__(self, *args, **kwargs): - super(WarningTestMixin._AssertWarnsContext, self).__exit__(self, *args, **kwargs) - - self.parent.assertTrue(any(issubclass(item.category, warning) - for warning in self.expected_warnings - for item in self._warning_log)) - - def assertWarns(self, warning, callable=None, *args, **kwargs): - warnings.simplefilter('always') - context = self.__class__._AssertWarnsContext(warning, self) - if callable is None: - return context - else: - with context: - callable(*args, **kwargs) - - -class TZWinContext(object): - """ Context manager for changing local time zone on Windows """ - @classmethod - def tz_change_allowed(cls): - # Allowing dateutil to change the local TZ is set as a local environment - # flag. - return bool(os.environ.get('DATEUTIL_MAY_CHANGE_TZ', False)) - - def __init__(self, tzname): - self.tzname = tzname - self._old_tz = None - - def __enter__(self): - if not self.tz_change_allowed(): - raise ValueError('Environment variable DATEUTIL_MAY_CHANGE_TZ ' + - 'must be true.') - - self._old_tz = self.get_current_tz() - self.set_current_tz(self.tzname) - - def __exit__(self, type, value, traceback): - if self._old_tz is not None: - self.set_current_tz(self._old_tz) - - def get_current_tz(self): - p = subprocess.Popen(['tzutil', '/g'], stdout=subprocess.PIPE) - - ctzname, err = p.communicate() - ctzname = ctzname.decode() # Popen returns - - if p.returncode: - raise OSError('Failed to get current time zone: ' + err) - - return ctzname - - def set_current_tz(self, tzname): - p = subprocess.Popen('tzutil /s "' + tzname + '"') - - out, err = p.communicate() - - if p.returncode: - raise OSError('Failed to set current time zone: ' + - (err or 'Unknown error.')) \ No newline at end of file diff --git a/lib/dateutil/test/test_easter.py b/lib/dateutil/test/test_easter.py deleted file mode 100644 index 6897e88be..000000000 --- a/lib/dateutil/test/test_easter.py +++ /dev/null @@ -1,99 +0,0 @@ -from dateutil.easter import easter -from dateutil.easter import EASTER_WESTERN, EASTER_ORTHODOX, EASTER_JULIAN - -from datetime import date - -try: - import unittest2 as unittest -except ImportError: - import unittest - -# List of easters between 1990 and 2050 -western_easter_dates = [ - date(1990, 4, 15), date(1991, 3, 31), date(1992, 4, 19), date(1993, 4, 11), - date(1994, 4, 3), date(1995, 4, 16), date(1996, 4, 7), date(1997, 3, 30), - date(1998, 4, 12), date(1999, 4, 4), - - date(2000, 4, 23), date(2001, 4, 15), date(2002, 3, 31), date(2003, 4, 20), - date(2004, 4, 11), date(2005, 3, 27), date(2006, 4, 16), date(2007, 4, 8), - date(2008, 3, 23), date(2009, 4, 12), - - date(2010, 4, 4), date(2011, 4, 24), date(2012, 4, 8), date(2013, 3, 31), - date(2014, 4, 20), date(2015, 4, 5), date(2016, 3, 27), date(2017, 4, 16), - date(2018, 4, 1), date(2019, 4, 21), - - date(2020, 4, 12), date(2021, 4, 4), date(2022, 4, 17), date(2023, 4, 9), - date(2024, 3, 31), date(2025, 4, 20), date(2026, 4, 5), date(2027, 3, 28), - date(2028, 4, 16), date(2029, 4, 1), - - date(2030, 4, 21), date(2031, 4, 13), date(2032, 3, 28), date(2033, 4, 17), - date(2034, 4, 9), date(2035, 3, 25), date(2036, 4, 13), date(2037, 4, 5), - date(2038, 4, 25), date(2039, 4, 10), - - date(2040, 4, 1), date(2041, 4, 21), date(2042, 4, 6), date(2043, 3, 29), - date(2044, 4, 17), date(2045, 4, 9), date(2046, 3, 25), date(2047, 4, 14), - date(2048, 4, 5), date(2049, 4, 18), date(2050, 4, 10) - ] - -orthodox_easter_dates = [ - date(1990, 4, 15), date(1991, 4, 7), date(1992, 4, 26), date(1993, 4, 18), - date(1994, 5, 1), date(1995, 4, 23), date(1996, 4, 14), date(1997, 4, 27), - date(1998, 4, 19), date(1999, 4, 11), - - date(2000, 4, 30), date(2001, 4, 15), date(2002, 5, 5), date(2003, 4, 27), - date(2004, 4, 11), date(2005, 5, 1), date(2006, 4, 23), date(2007, 4, 8), - date(2008, 4, 27), date(2009, 4, 19), - - date(2010, 4, 4), date(2011, 4, 24), date(2012, 4, 15), date(2013, 5, 5), - date(2014, 4, 20), date(2015, 4, 12), date(2016, 5, 1), date(2017, 4, 16), - date(2018, 4, 8), date(2019, 4, 28), - - date(2020, 4, 19), date(2021, 5, 2), date(2022, 4, 24), date(2023, 4, 16), - date(2024, 5, 5), date(2025, 4, 20), date(2026, 4, 12), date(2027, 5, 2), - date(2028, 4, 16), date(2029, 4, 8), - - date(2030, 4, 28), date(2031, 4, 13), date(2032, 5, 2), date(2033, 4, 24), - date(2034, 4, 9), date(2035, 4, 29), date(2036, 4, 20), date(2037, 4, 5), - date(2038, 4, 25), date(2039, 4, 17), - - date(2040, 5, 6), date(2041, 4, 21), date(2042, 4, 13), date(2043, 5, 3), - date(2044, 4, 24), date(2045, 4, 9), date(2046, 4, 29), date(2047, 4, 21), - date(2048, 4, 5), date(2049, 4, 25), date(2050, 4, 17) -] - -# A random smattering of Julian dates. -# Pulled values from http://www.kevinlaughery.com/east4099.html -julian_easter_dates = [ - date( 326, 4, 3), date( 375, 4, 5), date( 492, 4, 5), date( 552, 3, 31), - date( 562, 4, 9), date( 569, 4, 21), date( 597, 4, 14), date( 621, 4, 19), - date( 636, 3, 31), date( 655, 3, 29), date( 700, 4, 11), date( 725, 4, 8), - date( 750, 3, 29), date( 782, 4, 7), date( 835, 4, 18), date( 849, 4, 14), - date( 867, 3, 30), date( 890, 4, 12), date( 922, 4, 21), date( 934, 4, 6), - date(1049, 3, 26), date(1058, 4, 19), date(1113, 4, 6), date(1119, 3, 30), - date(1242, 4, 20), date(1255, 3, 28), date(1257, 4, 8), date(1258, 3, 24), - date(1261, 4, 24), date(1278, 4, 17), date(1333, 4, 4), date(1351, 4, 17), - date(1371, 4, 6), date(1391, 3, 26), date(1402, 3, 26), date(1412, 4, 3), - date(1439, 4, 5), date(1445, 3, 28), date(1531, 4, 9), date(1555, 4, 14) -] - - -class EasterTest(unittest.TestCase): - def testEasterWestern(self): - for easter_date in western_easter_dates: - self.assertEqual(easter_date, - easter(easter_date.year, EASTER_WESTERN)) - - def testEasterOrthodox(self): - for easter_date in orthodox_easter_dates: - self.assertEqual(easter_date, - easter(easter_date.year, EASTER_ORTHODOX)) - - def testEasterJulian(self): - for easter_date in julian_easter_dates: - self.assertEqual(easter_date, - easter(easter_date.year, EASTER_JULIAN)) - - def testEasterBadMethod(self): - # Invalid methods raise ValueError - with self.assertRaises(ValueError): - easter(1975, 4) diff --git a/lib/dateutil/test/test_imports.py b/lib/dateutil/test/test_imports.py deleted file mode 100644 index 1d8ac171e..000000000 --- a/lib/dateutil/test/test_imports.py +++ /dev/null @@ -1,149 +0,0 @@ -import sys - -try: - import unittest2 as unittest -except ImportError: - import unittest - - -class ImportEasterTest(unittest.TestCase): - """ Test that dateutil.easter-related imports work properly """ - - def testEasterDirect(self): - import dateutil.easter - - def testEasterFrom(self): - from dateutil import easter - - def testEasterStar(self): - from dateutil.easter import easter - - -class ImportParserTest(unittest.TestCase): - """ Test that dateutil.parser-related imports work properly """ - def testParserDirect(self): - import dateutil.parser - - def testParserFrom(self): - from dateutil import parser - - def testParserAll(self): - # All interface - from dateutil.parser import parse - from dateutil.parser import parserinfo - - # Other public classes - from dateutil.parser import parser - - for var in (parse, parserinfo, parser): - self.assertIsNot(var, None) - - -class ImportRelativeDeltaTest(unittest.TestCase): - """ Test that dateutil.relativedelta-related imports work properly """ - def testRelativeDeltaDirect(self): - import dateutil.relativedelta - - def testRelativeDeltaFrom(self): - from dateutil import relativedelta - - def testRelativeDeltaAll(self): - from dateutil.relativedelta import relativedelta - from dateutil.relativedelta import MO, TU, WE, TH, FR, SA, SU - - for var in (relativedelta, MO, TU, WE, TH, FR, SA, SU): - self.assertIsNot(var, None) - - # In the public interface but not in all - from dateutil.relativedelta import weekday - self.assertIsNot(weekday, None) - - -class ImportRRuleTest(unittest.TestCase): - """ Test that dateutil.rrule related imports work properly """ - def testRRuleDirect(self): - import dateutil.rrule - - def testRRuleFrom(self): - from dateutil import rrule - - def testRRuleAll(self): - from dateutil.rrule import rrule - from dateutil.rrule import rruleset - from dateutil.rrule import rrulestr - from dateutil.rrule import YEARLY, MONTHLY, WEEKLY, DAILY - from dateutil.rrule import HOURLY, MINUTELY, SECONDLY - from dateutil.rrule import MO, TU, WE, TH, FR, SA, SU - - rr_all = (rrule, rruleset, rrulestr, - YEARLY, MONTHLY, WEEKLY, DAILY, - HOURLY, MINUTELY, SECONDLY, - MO, TU, WE, TH, FR, SA, SU) - - for var in rr_all: - self.assertIsNot(var, None) - - # In the public interface but not in all - from dateutil.rrule import weekday - self.assertIsNot(weekday, None) - - -class ImportTZTest(unittest.TestCase): - """ Test that dateutil.tz related imports work properly """ - def testTzDirect(self): - import dateutil.tz - - def testTzFrom(self): - from dateutil import tz - - def testTzAll(self): - from dateutil.tz import tzutc - from dateutil.tz import tzoffset - from dateutil.tz import tzlocal - from dateutil.tz import tzfile - from dateutil.tz import tzrange - from dateutil.tz import tzstr - from dateutil.tz import tzical - from dateutil.tz import gettz - from dateutil.tz import tzwin - from dateutil.tz import tzwinlocal - - tz_all = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange", - "tzstr", "tzical", "gettz"] - - tz_all += ["tzwin", "tzwinlocal"] if sys.platform.startswith("win") else [] - lvars = locals() - - for var in tz_all: - self.assertIsNot(lvars[var], None) - - -@unittest.skipUnless(sys.platform.startswith('win'), "Requires Windows") -class ImportTZWinTest(unittest.TestCase): - """ Test that dateutil.tzwin related imports work properly """ - def testTzwinDirect(self): - import dateutil.tzwin - - def testTzwinFrom(self): - from dateutil import tzwin - - def testTzwinStar(self): - tzwin_all = ["tzwin", "tzwinlocal"] - - -class ImportZoneInfoTest(unittest.TestCase): - def testZoneinfoDirect(self): - import dateutil.zoneinfo - - def testZoneinfoFrom(self): - from dateutil import zoneinfo - - def testZoneinfoStar(self): - from dateutil.zoneinfo import gettz - from dateutil.zoneinfo import gettz_db_metadata - from dateutil.zoneinfo import rebuild - - zi_all = (gettz, gettz_db_metadata, rebuild) - - for var in zi_all: - self.assertIsNot(var, None) diff --git a/lib/dateutil/test/test_parser.py b/lib/dateutil/test/test_parser.py deleted file mode 100644 index 572e58905..000000000 --- a/lib/dateutil/test/test_parser.py +++ /dev/null @@ -1,779 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals -from ._common import unittest - -from datetime import datetime, timedelta, date - -from dateutil.tz import tzoffset -from dateutil.parser import * - -from six import assertRaisesRegex, PY3 - -class ParserTest(unittest.TestCase): - - def setUp(self): - self.tzinfos = {"BRST": -10800} - self.brsttz = tzoffset("BRST", -10800) - self.default = datetime(2003, 9, 25) - - # Parser should be able to handle bytestring and unicode - base_str = '2014-05-01 08:00:00' - try: - # Python 2.x - self.uni_str = unicode(base_str) - self.str_str = str(base_str) - except NameError: - self.uni_str = str(base_str) - self.str_str = bytes(base_str.encode()) - - def testEmptyString(self): - with self.assertRaises(ValueError): - parse('') - - def testDateCommandFormat(self): - self.assertEqual(parse("Thu Sep 25 10:36:28 BRST 2003", - tzinfos=self.tzinfos), - datetime(2003, 9, 25, 10, 36, 28, - tzinfo=self.brsttz)) - - def testDateCommandFormatUnicode(self): - self.assertEqual(parse("Thu Sep 25 10:36:28 BRST 2003", - tzinfos=self.tzinfos), - datetime(2003, 9, 25, 10, 36, 28, - tzinfo=self.brsttz)) - - - def testDateCommandFormatReversed(self): - self.assertEqual(parse("2003 10:36:28 BRST 25 Sep Thu", - tzinfos=self.tzinfos), - datetime(2003, 9, 25, 10, 36, 28, - tzinfo=self.brsttz)) - - def testDateCommandFormatWithLong(self): - if not PY3: - self.assertEqual(parse("Thu Sep 25 10:36:28 BRST 2003", - tzinfos={"BRST": long(-10800)}), - datetime(2003, 9, 25, 10, 36, 28, - tzinfo=self.brsttz)) - def testDateCommandFormatIgnoreTz(self): - self.assertEqual(parse("Thu Sep 25 10:36:28 BRST 2003", - ignoretz=True), - datetime(2003, 9, 25, 10, 36, 28)) - - def testDateCommandFormatStrip1(self): - self.assertEqual(parse("Thu Sep 25 10:36:28 2003"), - datetime(2003, 9, 25, 10, 36, 28)) - - def testDateCommandFormatStrip2(self): - self.assertEqual(parse("Thu Sep 25 10:36:28", default=self.default), - datetime(2003, 9, 25, 10, 36, 28)) - - def testDateCommandFormatStrip3(self): - self.assertEqual(parse("Thu Sep 10:36:28", default=self.default), - datetime(2003, 9, 25, 10, 36, 28)) - - def testDateCommandFormatStrip4(self): - self.assertEqual(parse("Thu 10:36:28", default=self.default), - datetime(2003, 9, 25, 10, 36, 28)) - - def testDateCommandFormatStrip5(self): - self.assertEqual(parse("Sep 10:36:28", default=self.default), - datetime(2003, 9, 25, 10, 36, 28)) - - def testDateCommandFormatStrip6(self): - self.assertEqual(parse("10:36:28", default=self.default), - datetime(2003, 9, 25, 10, 36, 28)) - - def testDateCommandFormatStrip7(self): - self.assertEqual(parse("10:36", default=self.default), - datetime(2003, 9, 25, 10, 36)) - - def testDateCommandFormatStrip8(self): - self.assertEqual(parse("Thu Sep 25 2003"), - datetime(2003, 9, 25)) - - def testDateCommandFormatStrip9(self): - self.assertEqual(parse("Sep 25 2003"), - datetime(2003, 9, 25)) - - def testDateCommandFormatStrip10(self): - self.assertEqual(parse("Sep 2003", default=self.default), - datetime(2003, 9, 25)) - - def testDateCommandFormatStrip11(self): - self.assertEqual(parse("Sep", default=self.default), - datetime(2003, 9, 25)) - - def testDateCommandFormatStrip12(self): - self.assertEqual(parse("2003", default=self.default), - datetime(2003, 9, 25)) - - def testDateRCommandFormat(self): - self.assertEqual(parse("Thu, 25 Sep 2003 10:49:41 -0300"), - datetime(2003, 9, 25, 10, 49, 41, - tzinfo=self.brsttz)) - - def testISOFormat(self): - self.assertEqual(parse("2003-09-25T10:49:41.5-03:00"), - datetime(2003, 9, 25, 10, 49, 41, 500000, - tzinfo=self.brsttz)) - - def testISOFormatStrip1(self): - self.assertEqual(parse("2003-09-25T10:49:41-03:00"), - datetime(2003, 9, 25, 10, 49, 41, - tzinfo=self.brsttz)) - - def testISOFormatStrip2(self): - self.assertEqual(parse("2003-09-25T10:49:41"), - datetime(2003, 9, 25, 10, 49, 41)) - - def testISOFormatStrip3(self): - self.assertEqual(parse("2003-09-25T10:49"), - datetime(2003, 9, 25, 10, 49)) - - def testISOFormatStrip4(self): - self.assertEqual(parse("2003-09-25T10"), - datetime(2003, 9, 25, 10)) - - def testISOFormatStrip5(self): - self.assertEqual(parse("2003-09-25"), - datetime(2003, 9, 25)) - - def testISOStrippedFormat(self): - self.assertEqual(parse("20030925T104941.5-0300"), - datetime(2003, 9, 25, 10, 49, 41, 500000, - tzinfo=self.brsttz)) - - def testISOStrippedFormatStrip1(self): - self.assertEqual(parse("20030925T104941-0300"), - datetime(2003, 9, 25, 10, 49, 41, - tzinfo=self.brsttz)) - - def testISOStrippedFormatStrip2(self): - self.assertEqual(parse("20030925T104941"), - datetime(2003, 9, 25, 10, 49, 41)) - - def testISOStrippedFormatStrip3(self): - self.assertEqual(parse("20030925T1049"), - datetime(2003, 9, 25, 10, 49, 0)) - - def testISOStrippedFormatStrip4(self): - self.assertEqual(parse("20030925T10"), - datetime(2003, 9, 25, 10)) - - def testISOStrippedFormatStrip5(self): - self.assertEqual(parse("20030925"), - datetime(2003, 9, 25)) - - def testPythonLoggerFormat(self): - self.assertEqual(parse("2003-09-25 10:49:41,502"), - datetime(2003, 9, 25, 10, 49, 41, 502000)) - - def testNoSeparator1(self): - self.assertEqual(parse("199709020908"), - datetime(1997, 9, 2, 9, 8)) - - def testNoSeparator2(self): - self.assertEqual(parse("19970902090807"), - datetime(1997, 9, 2, 9, 8, 7)) - - def testDateWithDash1(self): - self.assertEqual(parse("2003-09-25"), - datetime(2003, 9, 25)) - - def testDateWithDash2(self): - self.assertEqual(parse("2003-Sep-25"), - datetime(2003, 9, 25)) - - def testDateWithDash3(self): - self.assertEqual(parse("25-Sep-2003"), - datetime(2003, 9, 25)) - - def testDateWithDash4(self): - self.assertEqual(parse("25-Sep-2003"), - datetime(2003, 9, 25)) - - def testDateWithDash5(self): - self.assertEqual(parse("Sep-25-2003"), - datetime(2003, 9, 25)) - - def testDateWithDash6(self): - self.assertEqual(parse("09-25-2003"), - datetime(2003, 9, 25)) - - def testDateWithDash7(self): - self.assertEqual(parse("25-09-2003"), - datetime(2003, 9, 25)) - - def testDateWithDash8(self): - self.assertEqual(parse("10-09-2003", dayfirst=True), - datetime(2003, 9, 10)) - - def testDateWithDash9(self): - self.assertEqual(parse("10-09-2003"), - datetime(2003, 10, 9)) - - def testDateWithDash10(self): - self.assertEqual(parse("10-09-03"), - datetime(2003, 10, 9)) - - def testDateWithDash11(self): - self.assertEqual(parse("10-09-03", yearfirst=True), - datetime(2010, 9, 3)) - - def testDateWithDot1(self): - self.assertEqual(parse("2003.09.25"), - datetime(2003, 9, 25)) - - def testDateWithDot2(self): - self.assertEqual(parse("2003.Sep.25"), - datetime(2003, 9, 25)) - - def testDateWithDot3(self): - self.assertEqual(parse("25.Sep.2003"), - datetime(2003, 9, 25)) - - def testDateWithDot4(self): - self.assertEqual(parse("25.Sep.2003"), - datetime(2003, 9, 25)) - - def testDateWithDot5(self): - self.assertEqual(parse("Sep.25.2003"), - datetime(2003, 9, 25)) - - def testDateWithDot6(self): - self.assertEqual(parse("09.25.2003"), - datetime(2003, 9, 25)) - - def testDateWithDot7(self): - self.assertEqual(parse("25.09.2003"), - datetime(2003, 9, 25)) - - def testDateWithDot8(self): - self.assertEqual(parse("10.09.2003", dayfirst=True), - datetime(2003, 9, 10)) - - def testDateWithDot9(self): - self.assertEqual(parse("10.09.2003"), - datetime(2003, 10, 9)) - - def testDateWithDot10(self): - self.assertEqual(parse("10.09.03"), - datetime(2003, 10, 9)) - - def testDateWithDot11(self): - self.assertEqual(parse("10.09.03", yearfirst=True), - datetime(2010, 9, 3)) - - def testDateWithSlash1(self): - self.assertEqual(parse("2003/09/25"), - datetime(2003, 9, 25)) - - def testDateWithSlash2(self): - self.assertEqual(parse("2003/Sep/25"), - datetime(2003, 9, 25)) - - def testDateWithSlash3(self): - self.assertEqual(parse("25/Sep/2003"), - datetime(2003, 9, 25)) - - def testDateWithSlash4(self): - self.assertEqual(parse("25/Sep/2003"), - datetime(2003, 9, 25)) - - def testDateWithSlash5(self): - self.assertEqual(parse("Sep/25/2003"), - datetime(2003, 9, 25)) - - def testDateWithSlash6(self): - self.assertEqual(parse("09/25/2003"), - datetime(2003, 9, 25)) - - def testDateWithSlash7(self): - self.assertEqual(parse("25/09/2003"), - datetime(2003, 9, 25)) - - def testDateWithSlash8(self): - self.assertEqual(parse("10/09/2003", dayfirst=True), - datetime(2003, 9, 10)) - - def testDateWithSlash9(self): - self.assertEqual(parse("10/09/2003"), - datetime(2003, 10, 9)) - - def testDateWithSlash10(self): - self.assertEqual(parse("10/09/03"), - datetime(2003, 10, 9)) - - def testDateWithSlash11(self): - self.assertEqual(parse("10/09/03", yearfirst=True), - datetime(2010, 9, 3)) - - def testDateWithSpace1(self): - self.assertEqual(parse("2003 09 25"), - datetime(2003, 9, 25)) - - def testDateWithSpace2(self): - self.assertEqual(parse("2003 Sep 25"), - datetime(2003, 9, 25)) - - def testDateWithSpace3(self): - self.assertEqual(parse("25 Sep 2003"), - datetime(2003, 9, 25)) - - def testDateWithSpace4(self): - self.assertEqual(parse("25 Sep 2003"), - datetime(2003, 9, 25)) - - def testDateWithSpace5(self): - self.assertEqual(parse("Sep 25 2003"), - datetime(2003, 9, 25)) - - def testDateWithSpace6(self): - self.assertEqual(parse("09 25 2003"), - datetime(2003, 9, 25)) - - def testDateWithSpace7(self): - self.assertEqual(parse("25 09 2003"), - datetime(2003, 9, 25)) - - def testDateWithSpace8(self): - self.assertEqual(parse("10 09 2003", dayfirst=True), - datetime(2003, 9, 10)) - - def testDateWithSpace9(self): - self.assertEqual(parse("10 09 2003"), - datetime(2003, 10, 9)) - - def testDateWithSpace10(self): - self.assertEqual(parse("10 09 03"), - datetime(2003, 10, 9)) - - def testDateWithSpace11(self): - self.assertEqual(parse("10 09 03", yearfirst=True), - datetime(2010, 9, 3)) - - def testDateWithSpace12(self): - self.assertEqual(parse("25 09 03"), - datetime(2003, 9, 25)) - - def testStrangelyOrderedDate1(self): - self.assertEqual(parse("03 25 Sep"), - datetime(2003, 9, 25)) - - def testStrangelyOrderedDate2(self): - self.assertEqual(parse("2003 25 Sep"), - datetime(2003, 9, 25)) - - def testStrangelyOrderedDate3(self): - self.assertEqual(parse("25 03 Sep"), - datetime(2025, 9, 3)) - - def testHourWithLetters(self): - self.assertEqual(parse("10h36m28.5s", default=self.default), - datetime(2003, 9, 25, 10, 36, 28, 500000)) - - def testHourWithLettersStrip1(self): - self.assertEqual(parse("10h36m28s", default=self.default), - datetime(2003, 9, 25, 10, 36, 28)) - - def testHourWithLettersStrip2(self): - self.assertEqual(parse("10h36m", default=self.default), - datetime(2003, 9, 25, 10, 36)) - - def testHourWithLettersStrip3(self): - self.assertEqual(parse("10h", default=self.default), - datetime(2003, 9, 25, 10)) - - def testHourWithLettersStrip4(self): - self.assertEqual(parse("10 h 36", default=self.default), - datetime(2003, 9, 25, 10, 36)) - - def testAMPMNoHour(self): - with self.assertRaises(ValueError): - parse("AM") - - with self.assertRaises(ValueError): - parse("Jan 20, 2015 PM") - - def testHourAmPm1(self): - self.assertEqual(parse("10h am", default=self.default), - datetime(2003, 9, 25, 10)) - - def testHourAmPm2(self): - self.assertEqual(parse("10h pm", default=self.default), - datetime(2003, 9, 25, 22)) - - def testHourAmPm3(self): - self.assertEqual(parse("10am", default=self.default), - datetime(2003, 9, 25, 10)) - - def testHourAmPm4(self): - self.assertEqual(parse("10pm", default=self.default), - datetime(2003, 9, 25, 22)) - - def testHourAmPm5(self): - self.assertEqual(parse("10:00 am", default=self.default), - datetime(2003, 9, 25, 10)) - - def testHourAmPm6(self): - self.assertEqual(parse("10:00 pm", default=self.default), - datetime(2003, 9, 25, 22)) - - def testHourAmPm7(self): - self.assertEqual(parse("10:00am", default=self.default), - datetime(2003, 9, 25, 10)) - - def testHourAmPm8(self): - self.assertEqual(parse("10:00pm", default=self.default), - datetime(2003, 9, 25, 22)) - - def testHourAmPm9(self): - self.assertEqual(parse("10:00a.m", default=self.default), - datetime(2003, 9, 25, 10)) - - def testHourAmPm10(self): - self.assertEqual(parse("10:00p.m", default=self.default), - datetime(2003, 9, 25, 22)) - - def testHourAmPm11(self): - self.assertEqual(parse("10:00a.m.", default=self.default), - datetime(2003, 9, 25, 10)) - - def testHourAmPm12(self): - self.assertEqual(parse("10:00p.m.", default=self.default), - datetime(2003, 9, 25, 22)) - - def testAMPMRange(self): - with self.assertRaises(ValueError): - parse("13:44 AM") - - with self.assertRaises(ValueError): - parse("January 25, 1921 23:13 PM") - - def testPertain(self): - self.assertEqual(parse("Sep 03", default=self.default), - datetime(2003, 9, 3)) - self.assertEqual(parse("Sep of 03", default=self.default), - datetime(2003, 9, 25)) - - def testWeekdayAlone(self): - self.assertEqual(parse("Wed", default=self.default), - datetime(2003, 10, 1)) - - def testLongWeekday(self): - self.assertEqual(parse("Wednesday", default=self.default), - datetime(2003, 10, 1)) - - def testLongMonth(self): - self.assertEqual(parse("October", default=self.default), - datetime(2003, 10, 25)) - - def testZeroYear(self): - self.assertEqual(parse("31-Dec-00", default=self.default), - datetime(2000, 12, 31)) - - def testFuzzy(self): - s = "Today is 25 of September of 2003, exactly " \ - "at 10:49:41 with timezone -03:00." - self.assertEqual(parse(s, fuzzy=True), - datetime(2003, 9, 25, 10, 49, 41, - tzinfo=self.brsttz)) - - def testFuzzyWithTokens(self): - s = "Today is 25 of September of 2003, exactly " \ - "at 10:49:41 with timezone -03:00." - self.assertEqual(parse(s, fuzzy_with_tokens=True), - (datetime(2003, 9, 25, 10, 49, 41, - tzinfo=self.brsttz), - ('Today is ', 'of ', ', exactly at ', - ' with timezone ', '.'))) - - def testFuzzyAMPMProblem(self): - # Sometimes fuzzy parsing results in AM/PM flag being set without - # hours - if it's fuzzy it should ignore that. - s1 = "I have a meeting on March 1, 1974." - s2 = "On June 8th, 2020, I am going to be the first man on Mars" - - # Also don't want any erroneous AM or PMs changing the parsed time - s3 = "Meet me at the AM/PM on Sunset at 3:00 AM on December 3rd, 2003" - s4 = "Meet me at 3:00AM on December 3rd, 2003 at the AM/PM on Sunset" - - self.assertEqual(parse(s1, fuzzy=True), datetime(1974, 3, 1)) - self.assertEqual(parse(s2, fuzzy=True), datetime(2020, 6, 8)) - self.assertEqual(parse(s3, fuzzy=True), datetime(2003, 12, 3, 3)) - self.assertEqual(parse(s4, fuzzy=True), datetime(2003, 12, 3, 3)) - - def testFuzzyIgnoreAMPM(self): - s1 = "Jan 29, 1945 14:45 AM I going to see you there?" - - self.assertEqual(parse(s1, fuzzy=True), datetime(1945, 1, 29, 14, 45)) - - def testExtraSpace(self): - self.assertEqual(parse(" July 4 , 1976 12:01:02 am "), - datetime(1976, 7, 4, 0, 1, 2)) - - def testRandomFormat1(self): - self.assertEqual(parse("Wed, July 10, '96"), - datetime(1996, 7, 10, 0, 0)) - - def testRandomFormat2(self): - self.assertEqual(parse("1996.07.10 AD at 15:08:56 PDT", - ignoretz=True), - datetime(1996, 7, 10, 15, 8, 56)) - - def testRandomFormat3(self): - self.assertEqual(parse("1996.July.10 AD 12:08 PM"), - datetime(1996, 7, 10, 12, 8)) - - def testRandomFormat4(self): - self.assertEqual(parse("Tuesday, April 12, 1952 AD 3:30:42pm PST", - ignoretz=True), - datetime(1952, 4, 12, 15, 30, 42)) - - def testRandomFormat5(self): - self.assertEqual(parse("November 5, 1994, 8:15:30 am EST", - ignoretz=True), - datetime(1994, 11, 5, 8, 15, 30)) - - def testRandomFormat6(self): - self.assertEqual(parse("1994-11-05T08:15:30-05:00", - ignoretz=True), - datetime(1994, 11, 5, 8, 15, 30)) - - def testRandomFormat7(self): - self.assertEqual(parse("1994-11-05T08:15:30Z", - ignoretz=True), - datetime(1994, 11, 5, 8, 15, 30)) - - def testRandomFormat8(self): - self.assertEqual(parse("July 4, 1976"), datetime(1976, 7, 4)) - - def testRandomFormat9(self): - self.assertEqual(parse("7 4 1976"), datetime(1976, 7, 4)) - - def testRandomFormat10(self): - self.assertEqual(parse("4 jul 1976"), datetime(1976, 7, 4)) - - def testRandomFormat11(self): - self.assertEqual(parse("7-4-76"), datetime(1976, 7, 4)) - - def testRandomFormat12(self): - self.assertEqual(parse("19760704"), datetime(1976, 7, 4)) - - def testRandomFormat13(self): - self.assertEqual(parse("0:01:02", default=self.default), - datetime(2003, 9, 25, 0, 1, 2)) - - def testRandomFormat14(self): - self.assertEqual(parse("12h 01m02s am", default=self.default), - datetime(2003, 9, 25, 0, 1, 2)) - - def testRandomFormat15(self): - self.assertEqual(parse("0:01:02 on July 4, 1976"), - datetime(1976, 7, 4, 0, 1, 2)) - - def testRandomFormat16(self): - self.assertEqual(parse("0:01:02 on July 4, 1976"), - datetime(1976, 7, 4, 0, 1, 2)) - - def testRandomFormat17(self): - self.assertEqual(parse("1976-07-04T00:01:02Z", ignoretz=True), - datetime(1976, 7, 4, 0, 1, 2)) - - def testRandomFormat18(self): - self.assertEqual(parse("July 4, 1976 12:01:02 am"), - datetime(1976, 7, 4, 0, 1, 2)) - - def testRandomFormat19(self): - self.assertEqual(parse("Mon Jan 2 04:24:27 1995"), - datetime(1995, 1, 2, 4, 24, 27)) - - def testRandomFormat20(self): - self.assertEqual(parse("Tue Apr 4 00:22:12 PDT 1995", ignoretz=True), - datetime(1995, 4, 4, 0, 22, 12)) - - def testRandomFormat21(self): - self.assertEqual(parse("04.04.95 00:22"), - datetime(1995, 4, 4, 0, 22)) - - def testRandomFormat22(self): - self.assertEqual(parse("Jan 1 1999 11:23:34.578"), - datetime(1999, 1, 1, 11, 23, 34, 578000)) - - def testRandomFormat23(self): - self.assertEqual(parse("950404 122212"), - datetime(1995, 4, 4, 12, 22, 12)) - - def testRandomFormat24(self): - self.assertEqual(parse("0:00 PM, PST", default=self.default, - ignoretz=True), - datetime(2003, 9, 25, 12, 0)) - - def testRandomFormat25(self): - self.assertEqual(parse("12:08 PM", default=self.default), - datetime(2003, 9, 25, 12, 8)) - - def testRandomFormat26(self): - self.assertEqual(parse("5:50 A.M. on June 13, 1990"), - datetime(1990, 6, 13, 5, 50)) - - def testRandomFormat27(self): - self.assertEqual(parse("3rd of May 2001"), datetime(2001, 5, 3)) - - def testRandomFormat28(self): - self.assertEqual(parse("5th of March 2001"), datetime(2001, 3, 5)) - - def testRandomFormat29(self): - self.assertEqual(parse("1st of May 2003"), datetime(2003, 5, 1)) - - def testRandomFormat30(self): - self.assertEqual(parse("01h02m03", default=self.default), - datetime(2003, 9, 25, 1, 2, 3)) - - def testRandomFormat31(self): - self.assertEqual(parse("01h02", default=self.default), - datetime(2003, 9, 25, 1, 2)) - - def testRandomFormat32(self): - self.assertEqual(parse("01h02s", default=self.default), - datetime(2003, 9, 25, 1, 0, 2)) - - def testRandomFormat33(self): - self.assertEqual(parse("01m02", default=self.default), - datetime(2003, 9, 25, 0, 1, 2)) - - def testRandomFormat34(self): - self.assertEqual(parse("01m02h", default=self.default), - datetime(2003, 9, 25, 2, 1)) - - def testRandomFormat35(self): - self.assertEqual(parse("2004 10 Apr 11h30m", default=self.default), - datetime(2004, 4, 10, 11, 30)) - - def test_99_ad(self): - self.assertEqual(parse('0099-01-01T00:00:00'), - datetime(99, 1, 1, 0, 0)) - - def test_31_ad(self): - self.assertEqual(parse('0031-01-01T00:00:00'), - datetime(31, 1, 1, 0, 0)) - - def testInvalidDay(self): - with self.assertRaises(ValueError): - parse("Feb 30, 2007") - - def testUnspecifiedDayFallback(self): - # Test that for an unspecified day, the fallback behavior is correct. - self.assertEqual(parse("April 2009", default=datetime(2010, 1, 31)), - datetime(2009, 4, 30)) - - def testUnspecifiedDayFallbackFebNoLeapYear(self): - self.assertEqual(parse("Feb 2007", default=datetime(2010, 1, 31)), - datetime(2007, 2, 28)) - - def testUnspecifiedDayFallbackFebLeapYear(self): - self.assertEqual(parse("Feb 2008", default=datetime(2010, 1, 31)), - datetime(2008, 2, 29)) - - def testErrorType01(self): - self.assertRaises(ValueError, - parse, 'shouldfail') - - def testCorrectErrorOnFuzzyWithTokens(self): - assertRaisesRegex(self, ValueError, 'Unknown string format', - parse, '04/04/32/423', fuzzy_with_tokens=True) - assertRaisesRegex(self, ValueError, 'Unknown string format', - parse, '04/04/04 +32423', fuzzy_with_tokens=True) - assertRaisesRegex(self, ValueError, 'Unknown string format', - parse, '04/04/0d4', fuzzy_with_tokens=True) - - def testIncreasingCTime(self): - # This test will check 200 different years, every month, every day, - # every hour, every minute, every second, and every weekday, using - # a delta of more or less 1 year, 1 month, 1 day, 1 minute and - # 1 second. - delta = timedelta(days=365+31+1, seconds=1+60+60*60) - dt = datetime(1900, 1, 1, 0, 0, 0, 0) - for i in range(200): - self.assertEqual(parse(dt.ctime()), dt) - dt += delta - - def testIncreasingISOFormat(self): - delta = timedelta(days=365+31+1, seconds=1+60+60*60) - dt = datetime(1900, 1, 1, 0, 0, 0, 0) - for i in range(200): - self.assertEqual(parse(dt.isoformat()), dt) - dt += delta - - def testMicrosecondsPrecisionError(self): - # Skip found out that sad precision problem. :-( - dt1 = parse("00:11:25.01") - dt2 = parse("00:12:10.01") - self.assertEqual(dt1.microsecond, 10000) - self.assertEqual(dt2.microsecond, 10000) - - def testMicrosecondPrecisionErrorReturns(self): - # One more precision issue, discovered by Eric Brown. This should - # be the last one, as we're no longer using floating points. - for ms in [100001, 100000, 99999, 99998, - 10001, 10000, 9999, 9998, - 1001, 1000, 999, 998, - 101, 100, 99, 98]: - dt = datetime(2008, 2, 27, 21, 26, 1, ms) - self.assertEqual(parse(dt.isoformat()), dt) - - def testHighPrecisionSeconds(self): - self.assertEqual(parse("20080227T21:26:01.123456789"), - datetime(2008, 2, 27, 21, 26, 1, 123456)) - - def testCustomParserInfo(self): - # Custom parser info wasn't working, as Michael Elsdörfer discovered. - from dateutil.parser import parserinfo, parser - - class myparserinfo(parserinfo): - MONTHS = parserinfo.MONTHS[:] - MONTHS[0] = ("Foo", "Foo") - myparser = parser(myparserinfo()) - dt = myparser.parse("01/Foo/2007") - self.assertEqual(dt, datetime(2007, 1, 1)) - - def testParseStr(self): - self.assertEqual(parse(self.str_str), - parse(self.uni_str)) - - def testParserParseStr(self): - from dateutil.parser import parser - - self.assertEqual(parser().parse(self.str_str), - parser().parse(self.uni_str)) - - def testParseUnicodeWords(self): - - class rus_parserinfo(parserinfo): - MONTHS = [("янв", "Январь"), - ("фев", "Февраль"), - ("мар", "Март"), - ("апр", "Апрель"), - ("май", "Май"), - ("июн", "Июнь"), - ("июл", "Июль"), - ("авг", "Август"), - ("сен", "Сентябрь"), - ("окт", "Октябрь"), - ("ноя", "Ноябрь"), - ("дек", "Декабрь")] - - self.assertEqual(parse('10 Сентябрь 2015 10:20', - parserinfo=rus_parserinfo()), - datetime(2015, 9, 10, 10, 20)) - - def testParseWithNulls(self): - # This relies on the from __future__ import unicode_literals, because - # explicitly specifying a unicode literal is a syntax error in Py 3.2 - # May want to switch to u'...' if we ever drop Python 3.2 support. - pstring = '\x00\x00August 29, 1924' - - self.assertEqual(parse(pstring), - datetime(1924, 8, 29)) - diff --git a/lib/dateutil/test/test_relativedelta.py b/lib/dateutil/test/test_relativedelta.py deleted file mode 100644 index 122776ac6..000000000 --- a/lib/dateutil/test/test_relativedelta.py +++ /dev/null @@ -1,484 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals -from ._common import unittest, WarningTestMixin - -import calendar -from datetime import datetime, date - -from dateutil.relativedelta import * - -class RelativeDeltaTest(WarningTestMixin, unittest.TestCase): - now = datetime(2003, 9, 17, 20, 54, 47, 282310) - today = date(2003, 9, 17) - - def testInheritance(self): - # Ensure that relativedelta is inheritance-friendly. - class rdChildClass(relativedelta): - pass - - ccRD = rdChildClass(years=1, months=1, days=1, leapdays=1, weeks=1, - hours=1, minutes=1, seconds=1, microseconds=1) - - rd = relativedelta(years=1, months=1, days=1, leapdays=1, weeks=1, - hours=1, minutes=1, seconds=1, microseconds=1) - - self.assertEqual(type(ccRD + rd), type(ccRD), - msg='Addition does not inherit type.') - - self.assertEqual(type(ccRD - rd), type(ccRD), - msg='Subtraction does not inherit type.') - - self.assertEqual(type(-ccRD), type(ccRD), - msg='Negation does not inherit type.') - - self.assertEqual(type(ccRD * 5.0), type(ccRD), - msg='Multiplication does not inherit type.') - - self.assertEqual(type(ccRD / 5.0), type(ccRD), - msg='Division does not inherit type.') - - def testMonthEndMonthBeginning(self): - self.assertEqual(relativedelta(datetime(2003, 1, 31, 23, 59, 59), - datetime(2003, 3, 1, 0, 0, 0)), - relativedelta(months=-1, seconds=-1)) - - self.assertEqual(relativedelta(datetime(2003, 3, 1, 0, 0, 0), - datetime(2003, 1, 31, 23, 59, 59)), - relativedelta(months=1, seconds=1)) - - def testMonthEndMonthBeginningLeapYear(self): - self.assertEqual(relativedelta(datetime(2012, 1, 31, 23, 59, 59), - datetime(2012, 3, 1, 0, 0, 0)), - relativedelta(months=-1, seconds=-1)) - - self.assertEqual(relativedelta(datetime(2003, 3, 1, 0, 0, 0), - datetime(2003, 1, 31, 23, 59, 59)), - relativedelta(months=1, seconds=1)) - - def testNextMonth(self): - self.assertEqual(self.now+relativedelta(months=+1), - datetime(2003, 10, 17, 20, 54, 47, 282310)) - - def testNextMonthPlusOneWeek(self): - self.assertEqual(self.now+relativedelta(months=+1, weeks=+1), - datetime(2003, 10, 24, 20, 54, 47, 282310)) - - def testNextMonthPlusOneWeek10am(self): - self.assertEqual(self.today + - relativedelta(months=+1, weeks=+1, hour=10), - datetime(2003, 10, 24, 10, 0)) - - def testNextMonthPlusOneWeek10amDiff(self): - self.assertEqual(relativedelta(datetime(2003, 10, 24, 10, 0), - self.today), - relativedelta(months=+1, days=+7, hours=+10)) - - def testOneMonthBeforeOneYear(self): - self.assertEqual(self.now+relativedelta(years=+1, months=-1), - datetime(2004, 8, 17, 20, 54, 47, 282310)) - - def testMonthsOfDiffNumOfDays(self): - self.assertEqual(date(2003, 1, 27)+relativedelta(months=+1), - date(2003, 2, 27)) - self.assertEqual(date(2003, 1, 31)+relativedelta(months=+1), - date(2003, 2, 28)) - self.assertEqual(date(2003, 1, 31)+relativedelta(months=+2), - date(2003, 3, 31)) - - def testMonthsOfDiffNumOfDaysWithYears(self): - self.assertEqual(date(2000, 2, 28)+relativedelta(years=+1), - date(2001, 2, 28)) - self.assertEqual(date(2000, 2, 29)+relativedelta(years=+1), - date(2001, 2, 28)) - - self.assertEqual(date(1999, 2, 28)+relativedelta(years=+1), - date(2000, 2, 28)) - self.assertEqual(date(1999, 3, 1)+relativedelta(years=+1), - date(2000, 3, 1)) - self.assertEqual(date(1999, 3, 1)+relativedelta(years=+1), - date(2000, 3, 1)) - - self.assertEqual(date(2001, 2, 28)+relativedelta(years=-1), - date(2000, 2, 28)) - self.assertEqual(date(2001, 3, 1)+relativedelta(years=-1), - date(2000, 3, 1)) - - def testNextFriday(self): - self.assertEqual(self.today+relativedelta(weekday=FR), - date(2003, 9, 19)) - - def testNextFridayInt(self): - self.assertEqual(self.today+relativedelta(weekday=calendar.FRIDAY), - date(2003, 9, 19)) - - def testLastFridayInThisMonth(self): - self.assertEqual(self.today+relativedelta(day=31, weekday=FR(-1)), - date(2003, 9, 26)) - - def testNextWednesdayIsToday(self): - self.assertEqual(self.today+relativedelta(weekday=WE), - date(2003, 9, 17)) - - def testNextWenesdayNotToday(self): - self.assertEqual(self.today+relativedelta(days=+1, weekday=WE), - date(2003, 9, 24)) - - def test15thISOYearWeek(self): - self.assertEqual(date(2003, 1, 1) + - relativedelta(day=4, weeks=+14, weekday=MO(-1)), - date(2003, 4, 7)) - - def testMillenniumAge(self): - self.assertEqual(relativedelta(self.now, date(2001, 1, 1)), - relativedelta(years=+2, months=+8, days=+16, - hours=+20, minutes=+54, seconds=+47, - microseconds=+282310)) - - def testJohnAge(self): - self.assertEqual(relativedelta(self.now, - datetime(1978, 4, 5, 12, 0)), - relativedelta(years=+25, months=+5, days=+12, - hours=+8, minutes=+54, seconds=+47, - microseconds=+282310)) - - def testJohnAgeWithDate(self): - self.assertEqual(relativedelta(self.today, - datetime(1978, 4, 5, 12, 0)), - relativedelta(years=+25, months=+5, days=+11, - hours=+12)) - - def testYearDay(self): - self.assertEqual(date(2003, 1, 1)+relativedelta(yearday=260), - date(2003, 9, 17)) - self.assertEqual(date(2002, 1, 1)+relativedelta(yearday=260), - date(2002, 9, 17)) - self.assertEqual(date(2000, 1, 1)+relativedelta(yearday=260), - date(2000, 9, 16)) - self.assertEqual(self.today+relativedelta(yearday=261), - date(2003, 9, 18)) - - def testYearDayBug(self): - # Tests a problem reported by Adam Ryan. - self.assertEqual(date(2010, 1, 1)+relativedelta(yearday=15), - date(2010, 1, 15)) - - def testNonLeapYearDay(self): - self.assertEqual(date(2003, 1, 1)+relativedelta(nlyearday=260), - date(2003, 9, 17)) - self.assertEqual(date(2002, 1, 1)+relativedelta(nlyearday=260), - date(2002, 9, 17)) - self.assertEqual(date(2000, 1, 1)+relativedelta(nlyearday=260), - date(2000, 9, 17)) - self.assertEqual(self.today+relativedelta(yearday=261), - date(2003, 9, 18)) - - def testAddition(self): - self.assertEqual(relativedelta(days=10) + - relativedelta(years=1, months=2, days=3, hours=4, - minutes=5, microseconds=6), - relativedelta(years=1, months=2, days=13, hours=4, - minutes=5, microseconds=6)) - - def testAdditionToDatetime(self): - self.assertEqual(datetime(2000, 1, 1) + relativedelta(days=1), - datetime(2000, 1, 2)) - - def testRightAdditionToDatetime(self): - self.assertEqual(relativedelta(days=1) + datetime(2000, 1, 1), - datetime(2000, 1, 2)) - - def testAdditionInvalidType(self): - with self.assertRaises(TypeError): - relativedelta(days=3) + 9 - - def testSubtraction(self): - self.assertEqual(relativedelta(days=10) - - relativedelta(years=1, months=2, days=3, hours=4, - minutes=5, microseconds=6), - relativedelta(years=-1, months=-2, days=7, hours=-4, - minutes=-5, microseconds=-6)) - - def testRightSubtractionFromDatetime(self): - self.assertEqual(datetime(2000, 1, 2) - relativedelta(days=1), - datetime(2000, 1, 1)) - - def testSubractionWithDatetime(self): - self.assertRaises(TypeError, lambda x, y: x - y, - (relativedelta(days=1), datetime(2000, 1, 1))) - - def testSubtractionInvalidType(self): - with self.assertRaises(TypeError): - relativedelta(hours=12) - 14 - - def testMultiplication(self): - self.assertEqual(datetime(2000, 1, 1) + relativedelta(days=1) * 28, - datetime(2000, 1, 29)) - self.assertEqual(datetime(2000, 1, 1) + 28 * relativedelta(days=1), - datetime(2000, 1, 29)) - - def testDivision(self): - self.assertEqual(datetime(2000, 1, 1) + relativedelta(days=28) / 28, - datetime(2000, 1, 2)) - - def testBoolean(self): - self.assertFalse(relativedelta(days=0)) - self.assertTrue(relativedelta(days=1)) - - def testComparison(self): - d1 = relativedelta(years=1, months=1, days=1, leapdays=0, hours=1, - minutes=1, seconds=1, microseconds=1) - d2 = relativedelta(years=1, months=1, days=1, leapdays=0, hours=1, - minutes=1, seconds=1, microseconds=1) - d3 = relativedelta(years=1, months=1, days=1, leapdays=0, hours=1, - minutes=1, seconds=1, microseconds=2) - - self.assertEqual(d1, d2) - self.assertNotEqual(d1, d3) - - def testInequalityTypeMismatch(self): - # Different type - self.assertFalse(relativedelta(year=1) == 19) - - def testInequalityWeekdays(self): - # Different weekdays - no_wday = relativedelta(year=1997, month=4) - wday_mo_1 = relativedelta(year=1997, month=4, weekday=MO(+1)) - wday_mo_2 = relativedelta(year=1997, month=4, weekday=MO(+2)) - wday_tu = relativedelta(year=1997, month=4, weekday=TU) - - self.assertTrue(wday_mo_1 == wday_mo_1) - - self.assertFalse(no_wday == wday_mo_1) - self.assertFalse(wday_mo_1 == no_wday) - - self.assertFalse(wday_mo_1 == wday_mo_2) - self.assertFalse(wday_mo_2 == wday_mo_1) - - self.assertFalse(wday_mo_1 == wday_tu) - self.assertFalse(wday_tu == wday_mo_1) - - def testMonthOverflow(self): - self.assertEqual(relativedelta(months=273), - relativedelta(years=22, months=9)) - - def testWeeks(self): - # Test that the weeks property is working properly. - rd = relativedelta(years=4, months=2, weeks=8, days=6) - self.assertEqual((rd.weeks, rd.days), (8, 8 * 7 + 6)) - - rd.weeks = 3 - self.assertEqual((rd.weeks, rd.days), (3, 3 * 7 + 6)) - - def testRelativeDeltaRepr(self): - self.assertEqual(repr(relativedelta(years=1, months=-1, days=15)), - 'relativedelta(years=+1, months=-1, days=+15)') - - self.assertEqual(repr(relativedelta(months=14, seconds=-25)), - 'relativedelta(years=+1, months=+2, seconds=-25)') - - self.assertEqual(repr(relativedelta(month=3, hour=3, weekday=SU(3))), - 'relativedelta(month=3, weekday=SU(+3), hour=3)') - - def testRelativeDeltaFractionalYear(self): - with self.assertRaises(ValueError): - relativedelta(years=1.5) - - def testRelativeDeltaFractionalMonth(self): - with self.assertRaises(ValueError): - relativedelta(months=1.5) - - def testRelativeDeltaFractionalAbsolutes(self): - # Fractional absolute values will soon be unsupported, - # check for the deprecation warning. - with self.assertWarns(DeprecationWarning): - relativedelta(year=2.86) - - with self.assertWarns(DeprecationWarning): - relativedelta(month=1.29) - - with self.assertWarns(DeprecationWarning): - relativedelta(day=0.44) - - with self.assertWarns(DeprecationWarning): - relativedelta(hour=23.98) - - with self.assertWarns(DeprecationWarning): - relativedelta(minute=45.21) - - with self.assertWarns(DeprecationWarning): - relativedelta(second=13.2) - - with self.assertWarns(DeprecationWarning): - relativedelta(microsecond=157221.93) - - def testRelativeDeltaFractionalRepr(self): - rd = relativedelta(years=3, months=-2, days=1.25) - - self.assertEqual(repr(rd), - 'relativedelta(years=+3, months=-2, days=+1.25)') - - rd = relativedelta(hours=0.5, seconds=9.22) - self.assertEqual(repr(rd), - 'relativedelta(hours=+0.5, seconds=+9.22)') - - def testRelativeDeltaFractionalWeeks(self): - # Equivalent to days=8, hours=18 - rd = relativedelta(weeks=1.25) - d1 = datetime(2009, 9, 3, 0, 0) - self.assertEqual(d1 + rd, - datetime(2009, 9, 11, 18)) - - def testRelativeDeltaFractionalDays(self): - rd1 = relativedelta(days=1.48) - - d1 = datetime(2009, 9, 3, 0, 0) - self.assertEqual(d1 + rd1, - datetime(2009, 9, 4, 11, 31, 12)) - - rd2 = relativedelta(days=1.5) - self.assertEqual(d1 + rd2, - datetime(2009, 9, 4, 12, 0, 0)) - - def testRelativeDeltaFractionalHours(self): - rd = relativedelta(days=1, hours=12.5) - d1 = datetime(2009, 9, 3, 0, 0) - self.assertEqual(d1 + rd, - datetime(2009, 9, 4, 12, 30, 0)) - - def testRelativeDeltaFractionalMinutes(self): - rd = relativedelta(hours=1, minutes=30.5) - d1 = datetime(2009, 9, 3, 0, 0) - self.assertEqual(d1 + rd, - datetime(2009, 9, 3, 1, 30, 30)) - - def testRelativeDeltaFractionalSeconds(self): - rd = relativedelta(hours=5, minutes=30, seconds=30.5) - d1 = datetime(2009, 9, 3, 0, 0) - self.assertEqual(d1 + rd, - datetime(2009, 9, 3, 5, 30, 30, 500000)) - - def testRelativeDeltaFractionalPositiveOverflow(self): - # Equivalent to (days=1, hours=14) - rd1 = relativedelta(days=1.5, hours=2) - d1 = datetime(2009, 9, 3, 0, 0) - self.assertEqual(d1 + rd1, - datetime(2009, 9, 4, 14, 0, 0)) - - # Equivalent to (days=1, hours=14, minutes=45) - rd2 = relativedelta(days=1.5, hours=2.5, minutes=15) - d1 = datetime(2009, 9, 3, 0, 0) - self.assertEqual(d1 + rd2, - datetime(2009, 9, 4, 14, 45)) - - # Carry back up - equivalent to (days=2, hours=2, minutes=0, seconds=1) - rd3 = relativedelta(days=1.5, hours=13, minutes=59.5, seconds=31) - self.assertEqual(d1 + rd3, - datetime(2009, 9, 5, 2, 0, 1)) - - def testRelativeDeltaFractionalNegativeDays(self): - # Equivalent to (days=-1, hours=-1) - rd1 = relativedelta(days=-1.5, hours=11) - d1 = datetime(2009, 9, 3, 12, 0) - self.assertEqual(d1 + rd1, - datetime(2009, 9, 2, 11, 0, 0)) - - # Equivalent to (days=-1, hours=-9) - rd2 = relativedelta(days=-1.25, hours=-3) - self.assertEqual(d1 + rd2, - datetime(2009, 9, 2, 3)) - - def testRelativeDeltaNormalizeFractionalDays(self): - # Equivalent to (days=2, hours=18) - rd1 = relativedelta(days=2.75) - - self.assertEqual(rd1.normalized(), relativedelta(days=2, hours=18)) - - # Equvalent to (days=1, hours=11, minutes=31, seconds=12) - rd2 = relativedelta(days=1.48) - - self.assertEqual(rd2.normalized(), - relativedelta(days=1, hours=11, minutes=31, seconds=12)) - - def testRelativeDeltaNormalizeFractionalDays(self): - # Equivalent to (hours=1, minutes=30) - rd1 = relativedelta(hours=1.5) - - self.assertEqual(rd1.normalized(), relativedelta(hours=1, minutes=30)) - - # Equivalent to (hours=3, minutes=17, seconds=5, microseconds=100) - rd2 = relativedelta(hours=3.28472225) - - self.assertEqual(rd2.normalized(), - relativedelta(hours=3, minutes=17, seconds=5, microseconds=100)) - - def testRelativeDeltaNormalizeFractionalMinutes(self): - # Equivalent to (minutes=15, seconds=36) - rd1 = relativedelta(minutes=15.6) - - self.assertEqual(rd1.normalized(), - relativedelta(minutes=15, seconds=36)) - - # Equivalent to (minutes=25, seconds=20, microseconds=25000) - rd2 = relativedelta(minutes=25.33375) - - self.assertEqual(rd2.normalized(), - relativedelta(minutes=25, seconds=20, microseconds=25000)) - - def testRelativeDeltaNormalizeFractionalSeconds(self): - # Equivalent to (seconds=45, microseconds=25000) - rd1 = relativedelta(seconds=45.025) - self.assertEqual(rd1.normalized(), - relativedelta(seconds=45, microseconds=25000)) - - def testRelativeDeltaFractionalPositiveOverflow(self): - # Equivalent to (days=1, hours=14) - rd1 = relativedelta(days=1.5, hours=2) - self.assertEqual(rd1.normalized(), - relativedelta(days=1, hours=14)) - - # Equivalent to (days=1, hours=14, minutes=45) - rd2 = relativedelta(days=1.5, hours=2.5, minutes=15) - self.assertEqual(rd2.normalized(), - relativedelta(days=1, hours=14, minutes=45)) - - # Carry back up - equivalent to: - # (days=2, hours=2, minutes=0, seconds=2, microseconds=3) - rd3 = relativedelta(days=1.5, hours=13, minutes=59.50045, - seconds=31.473, microseconds=500003) - self.assertEqual(rd3.normalized(), - relativedelta(days=2, hours=2, minutes=0, - seconds=2, microseconds=3)) - - def testRelativeDeltaFractionalNegativeOverflow(self): - # Equivalent to (days=-1) - rd1 = relativedelta(days=-0.5, hours=-12) - self.assertEqual(rd1.normalized(), - relativedelta(days=-1)) - - # Equivalent to (days=-1) - rd2 = relativedelta(days=-1.5, hours=12) - self.assertEqual(rd2.normalized(), - relativedelta(days=-1)) - - # Equivalent to (days=-1, hours=-14, minutes=-45) - rd3 = relativedelta(days=-1.5, hours=-2.5, minutes=-15) - self.assertEqual(rd3.normalized(), - relativedelta(days=-1, hours=-14, minutes=-45)) - - # Equivalent to (days=-1, hours=-14, minutes=+15) - rd4 = relativedelta(days=-1.5, hours=-2.5, minutes=45) - self.assertEqual(rd4.normalized(), - relativedelta(days=-1, hours=-14, minutes=+15)) - - # Carry back up - equivalent to: - # (days=-2, hours=-2, minutes=0, seconds=-2, microseconds=-3) - rd3 = relativedelta(days=-1.5, hours=-13, minutes=-59.50045, - seconds=-31.473, microseconds=-500003) - self.assertEqual(rd3.normalized(), - relativedelta(days=-2, hours=-2, minutes=0, - seconds=-2, microseconds=-3)) - - def testInvalidYearDay(self): - with self.assertRaises(ValueError): - relativedelta(yearday=367) - diff --git a/lib/dateutil/test/test_rrule.py b/lib/dateutil/test/test_rrule.py deleted file mode 100644 index acaa6e972..000000000 --- a/lib/dateutil/test/test_rrule.py +++ /dev/null @@ -1,4676 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals -from ._common import WarningTestMixin, unittest - -import calendar -from datetime import datetime, date -from six import PY3 - -from dateutil.rrule import * - - -class RRuleTest(WarningTestMixin, unittest.TestCase): - def _rrulestr_reverse_test(self, rule): - """ - Call with an `rrule` and it will test that `str(rrule)` generates a - string which generates the same `rrule` as the input when passed to - `rrulestr()` - """ - rr_str = str(rule) - rrulestr_rrule = rrulestr(rr_str) - - self.assertEqual(list(rule), list(rrulestr_rrule)) - - def testYearly(self): - self.assertEqual(list(rrule(YEARLY, - count=3, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0), - datetime(1998, 9, 2, 9, 0), - datetime(1999, 9, 2, 9, 0)]) - - def testYearlyInterval(self): - self.assertEqual(list(rrule(YEARLY, - count=3, - interval=2, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0), - datetime(1999, 9, 2, 9, 0), - datetime(2001, 9, 2, 9, 0)]) - - def testYearlyIntervalLarge(self): - self.assertEqual(list(rrule(YEARLY, - count=3, - interval=100, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0), - datetime(2097, 9, 2, 9, 0), - datetime(2197, 9, 2, 9, 0)]) - - def testYearlyByMonth(self): - self.assertEqual(list(rrule(YEARLY, - count=3, - bymonth=(1, 3), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 2, 9, 0), - datetime(1998, 3, 2, 9, 0), - datetime(1999, 1, 2, 9, 0)]) - - def testYearlyByMonthDay(self): - self.assertEqual(list(rrule(YEARLY, - count=3, - bymonthday=(1, 3), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 3, 9, 0), - datetime(1997, 10, 1, 9, 0), - datetime(1997, 10, 3, 9, 0)]) - - def testYearlyByMonthAndMonthDay(self): - self.assertEqual(list(rrule(YEARLY, - count=3, - bymonth=(1, 3), - bymonthday=(5, 7), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 5, 9, 0), - datetime(1998, 1, 7, 9, 0), - datetime(1998, 3, 5, 9, 0)]) - - def testYearlyByWeekDay(self): - self.assertEqual(list(rrule(YEARLY, - count=3, - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 4, 9, 0), - datetime(1997, 9, 9, 9, 0)]) - - def testYearlyByNWeekDay(self): - self.assertEqual(list(rrule(YEARLY, - count=3, - byweekday=(TU(1), TH(-1)), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 12, 25, 9, 0), - datetime(1998, 1, 6, 9, 0), - datetime(1998, 12, 31, 9, 0)]) - - def testYearlyByNWeekDayLarge(self): - self.assertEqual(list(rrule(YEARLY, - count=3, - byweekday=(TU(3), TH(-3)), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 12, 11, 9, 0), - datetime(1998, 1, 20, 9, 0), - datetime(1998, 12, 17, 9, 0)]) - - def testYearlyByMonthAndWeekDay(self): - self.assertEqual(list(rrule(YEARLY, - count=3, - bymonth=(1, 3), - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 1, 9, 0), - datetime(1998, 1, 6, 9, 0), - datetime(1998, 1, 8, 9, 0)]) - - def testYearlyByMonthAndNWeekDay(self): - self.assertEqual(list(rrule(YEARLY, - count=3, - bymonth=(1, 3), - byweekday=(TU(1), TH(-1)), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 6, 9, 0), - datetime(1998, 1, 29, 9, 0), - datetime(1998, 3, 3, 9, 0)]) - - def testYearlyByMonthAndNWeekDayLarge(self): - # This is interesting because the TH(-3) ends up before - # the TU(3). - self.assertEqual(list(rrule(YEARLY, - count=3, - bymonth=(1, 3), - byweekday=(TU(3), TH(-3)), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 15, 9, 0), - datetime(1998, 1, 20, 9, 0), - datetime(1998, 3, 12, 9, 0)]) - - def testYearlyByMonthDayAndWeekDay(self): - self.assertEqual(list(rrule(YEARLY, - count=3, - bymonthday=(1, 3), - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 1, 9, 0), - datetime(1998, 2, 3, 9, 0), - datetime(1998, 3, 3, 9, 0)]) - - def testYearlyByMonthAndMonthDayAndWeekDay(self): - self.assertEqual(list(rrule(YEARLY, - count=3, - bymonth=(1, 3), - bymonthday=(1, 3), - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 1, 9, 0), - datetime(1998, 3, 3, 9, 0), - datetime(2001, 3, 1, 9, 0)]) - - def testYearlyByYearDay(self): - self.assertEqual(list(rrule(YEARLY, - count=4, - byyearday=(1, 100, 200, 365), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 12, 31, 9, 0), - datetime(1998, 1, 1, 9, 0), - datetime(1998, 4, 10, 9, 0), - datetime(1998, 7, 19, 9, 0)]) - - def testYearlyByYearDayNeg(self): - self.assertEqual(list(rrule(YEARLY, - count=4, - byyearday=(-365, -266, -166, -1), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 12, 31, 9, 0), - datetime(1998, 1, 1, 9, 0), - datetime(1998, 4, 10, 9, 0), - datetime(1998, 7, 19, 9, 0)]) - - def testYearlyByMonthAndYearDay(self): - self.assertEqual(list(rrule(YEARLY, - count=4, - bymonth=(4, 7), - byyearday=(1, 100, 200, 365), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 4, 10, 9, 0), - datetime(1998, 7, 19, 9, 0), - datetime(1999, 4, 10, 9, 0), - datetime(1999, 7, 19, 9, 0)]) - - def testYearlyByMonthAndYearDayNeg(self): - self.assertEqual(list(rrule(YEARLY, - count=4, - bymonth=(4, 7), - byyearday=(-365, -266, -166, -1), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 4, 10, 9, 0), - datetime(1998, 7, 19, 9, 0), - datetime(1999, 4, 10, 9, 0), - datetime(1999, 7, 19, 9, 0)]) - - def testYearlyByWeekNo(self): - self.assertEqual(list(rrule(YEARLY, - count=3, - byweekno=20, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 5, 11, 9, 0), - datetime(1998, 5, 12, 9, 0), - datetime(1998, 5, 13, 9, 0)]) - - def testYearlyByWeekNoAndWeekDay(self): - # That's a nice one. The first days of week number one - # may be in the last year. - self.assertEqual(list(rrule(YEARLY, - count=3, - byweekno=1, - byweekday=MO, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 12, 29, 9, 0), - datetime(1999, 1, 4, 9, 0), - datetime(2000, 1, 3, 9, 0)]) - - def testYearlyByWeekNoAndWeekDayLarge(self): - # Another nice test. The last days of week number 52/53 - # may be in the next year. - self.assertEqual(list(rrule(YEARLY, - count=3, - byweekno=52, - byweekday=SU, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 12, 28, 9, 0), - datetime(1998, 12, 27, 9, 0), - datetime(2000, 1, 2, 9, 0)]) - - def testYearlyByWeekNoAndWeekDayLast(self): - self.assertEqual(list(rrule(YEARLY, - count=3, - byweekno=-1, - byweekday=SU, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 12, 28, 9, 0), - datetime(1999, 1, 3, 9, 0), - datetime(2000, 1, 2, 9, 0)]) - - def testYearlyByEaster(self): - self.assertEqual(list(rrule(YEARLY, - count=3, - byeaster=0, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 4, 12, 9, 0), - datetime(1999, 4, 4, 9, 0), - datetime(2000, 4, 23, 9, 0)]) - - def testYearlyByEasterPos(self): - self.assertEqual(list(rrule(YEARLY, - count=3, - byeaster=1, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 4, 13, 9, 0), - datetime(1999, 4, 5, 9, 0), - datetime(2000, 4, 24, 9, 0)]) - - def testYearlyByEasterNeg(self): - self.assertEqual(list(rrule(YEARLY, - count=3, - byeaster=-1, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 4, 11, 9, 0), - datetime(1999, 4, 3, 9, 0), - datetime(2000, 4, 22, 9, 0)]) - - def testYearlyByWeekNoAndWeekDay53(self): - self.assertEqual(list(rrule(YEARLY, - count=3, - byweekno=53, - byweekday=MO, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 12, 28, 9, 0), - datetime(2004, 12, 27, 9, 0), - datetime(2009, 12, 28, 9, 0)]) - - def testYearlyByHour(self): - self.assertEqual(list(rrule(YEARLY, - count=3, - byhour=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 18, 0), - datetime(1998, 9, 2, 6, 0), - datetime(1998, 9, 2, 18, 0)]) - - def testYearlyByMinute(self): - self.assertEqual(list(rrule(YEARLY, - count=3, - byminute=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 6), - datetime(1997, 9, 2, 9, 18), - datetime(1998, 9, 2, 9, 6)]) - - def testYearlyBySecond(self): - self.assertEqual(list(rrule(YEARLY, - count=3, - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0, 6), - datetime(1997, 9, 2, 9, 0, 18), - datetime(1998, 9, 2, 9, 0, 6)]) - - def testYearlyByHourAndMinute(self): - self.assertEqual(list(rrule(YEARLY, - count=3, - byhour=(6, 18), - byminute=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 18, 6), - datetime(1997, 9, 2, 18, 18), - datetime(1998, 9, 2, 6, 6)]) - - def testYearlyByHourAndSecond(self): - self.assertEqual(list(rrule(YEARLY, - count=3, - byhour=(6, 18), - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 18, 0, 6), - datetime(1997, 9, 2, 18, 0, 18), - datetime(1998, 9, 2, 6, 0, 6)]) - - def testYearlyByMinuteAndSecond(self): - self.assertEqual(list(rrule(YEARLY, - count=3, - byminute=(6, 18), - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 6, 6), - datetime(1997, 9, 2, 9, 6, 18), - datetime(1997, 9, 2, 9, 18, 6)]) - - def testYearlyByHourAndMinuteAndSecond(self): - self.assertEqual(list(rrule(YEARLY, - count=3, - byhour=(6, 18), - byminute=(6, 18), - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 18, 6, 6), - datetime(1997, 9, 2, 18, 6, 18), - datetime(1997, 9, 2, 18, 18, 6)]) - - def testYearlyBySetPos(self): - self.assertEqual(list(rrule(YEARLY, - count=3, - bymonthday=15, - byhour=(6, 18), - bysetpos=(3, -3), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 11, 15, 18, 0), - datetime(1998, 2, 15, 6, 0), - datetime(1998, 11, 15, 18, 0)]) - - def testMonthly(self): - self.assertEqual(list(rrule(MONTHLY, - count=3, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 10, 2, 9, 0), - datetime(1997, 11, 2, 9, 0)]) - - def testMonthlyInterval(self): - self.assertEqual(list(rrule(MONTHLY, - count=3, - interval=2, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 11, 2, 9, 0), - datetime(1998, 1, 2, 9, 0)]) - - def testMonthlyIntervalLarge(self): - self.assertEqual(list(rrule(MONTHLY, - count=3, - interval=18, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0), - datetime(1999, 3, 2, 9, 0), - datetime(2000, 9, 2, 9, 0)]) - - def testMonthlyByMonth(self): - self.assertEqual(list(rrule(MONTHLY, - count=3, - bymonth=(1, 3), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 2, 9, 0), - datetime(1998, 3, 2, 9, 0), - datetime(1999, 1, 2, 9, 0)]) - - def testMonthlyByMonthDay(self): - self.assertEqual(list(rrule(MONTHLY, - count=3, - bymonthday=(1, 3), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 3, 9, 0), - datetime(1997, 10, 1, 9, 0), - datetime(1997, 10, 3, 9, 0)]) - - def testMonthlyByMonthAndMonthDay(self): - self.assertEqual(list(rrule(MONTHLY, - count=3, - bymonth=(1, 3), - bymonthday=(5, 7), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 5, 9, 0), - datetime(1998, 1, 7, 9, 0), - datetime(1998, 3, 5, 9, 0)]) - - def testMonthlyByWeekDay(self): - self.assertEqual(list(rrule(MONTHLY, - count=3, - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 4, 9, 0), - datetime(1997, 9, 9, 9, 0)]) - - # Third Monday of the month - self.assertEqual(rrule(MONTHLY, - byweekday=(MO(+3)), - dtstart=datetime(1997, 9, 1)).between(datetime(1997, 9, 1), - datetime(1997, 12, 1)), - [datetime(1997, 9, 15, 0, 0), - datetime(1997, 10, 20, 0, 0), - datetime(1997, 11, 17, 0, 0)]) - - def testMonthlyByNWeekDay(self): - self.assertEqual(list(rrule(MONTHLY, - count=3, - byweekday=(TU(1), TH(-1)), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 25, 9, 0), - datetime(1997, 10, 7, 9, 0)]) - - def testMonthlyByNWeekDayLarge(self): - self.assertEqual(list(rrule(MONTHLY, - count=3, - byweekday=(TU(3), TH(-3)), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 11, 9, 0), - datetime(1997, 9, 16, 9, 0), - datetime(1997, 10, 16, 9, 0)]) - - def testMonthlyByMonthAndWeekDay(self): - self.assertEqual(list(rrule(MONTHLY, - count=3, - bymonth=(1, 3), - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 1, 9, 0), - datetime(1998, 1, 6, 9, 0), - datetime(1998, 1, 8, 9, 0)]) - - def testMonthlyByMonthAndNWeekDay(self): - self.assertEqual(list(rrule(MONTHLY, - count=3, - bymonth=(1, 3), - byweekday=(TU(1), TH(-1)), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 6, 9, 0), - datetime(1998, 1, 29, 9, 0), - datetime(1998, 3, 3, 9, 0)]) - - def testMonthlyByMonthAndNWeekDayLarge(self): - self.assertEqual(list(rrule(MONTHLY, - count=3, - bymonth=(1, 3), - byweekday=(TU(3), TH(-3)), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 15, 9, 0), - datetime(1998, 1, 20, 9, 0), - datetime(1998, 3, 12, 9, 0)]) - - def testMonthlyByMonthDayAndWeekDay(self): - self.assertEqual(list(rrule(MONTHLY, - count=3, - bymonthday=(1, 3), - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 1, 9, 0), - datetime(1998, 2, 3, 9, 0), - datetime(1998, 3, 3, 9, 0)]) - - def testMonthlyByMonthAndMonthDayAndWeekDay(self): - self.assertEqual(list(rrule(MONTHLY, - count=3, - bymonth=(1, 3), - bymonthday=(1, 3), - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 1, 9, 0), - datetime(1998, 3, 3, 9, 0), - datetime(2001, 3, 1, 9, 0)]) - - def testMonthlyByYearDay(self): - self.assertEqual(list(rrule(MONTHLY, - count=4, - byyearday=(1, 100, 200, 365), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 12, 31, 9, 0), - datetime(1998, 1, 1, 9, 0), - datetime(1998, 4, 10, 9, 0), - datetime(1998, 7, 19, 9, 0)]) - - def testMonthlyByYearDayNeg(self): - self.assertEqual(list(rrule(MONTHLY, - count=4, - byyearday=(-365, -266, -166, -1), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 12, 31, 9, 0), - datetime(1998, 1, 1, 9, 0), - datetime(1998, 4, 10, 9, 0), - datetime(1998, 7, 19, 9, 0)]) - - def testMonthlyByMonthAndYearDay(self): - self.assertEqual(list(rrule(MONTHLY, - count=4, - bymonth=(4, 7), - byyearday=(1, 100, 200, 365), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 4, 10, 9, 0), - datetime(1998, 7, 19, 9, 0), - datetime(1999, 4, 10, 9, 0), - datetime(1999, 7, 19, 9, 0)]) - - def testMonthlyByMonthAndYearDayNeg(self): - self.assertEqual(list(rrule(MONTHLY, - count=4, - bymonth=(4, 7), - byyearday=(-365, -266, -166, -1), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 4, 10, 9, 0), - datetime(1998, 7, 19, 9, 0), - datetime(1999, 4, 10, 9, 0), - datetime(1999, 7, 19, 9, 0)]) - - def testMonthlyByWeekNo(self): - self.assertEqual(list(rrule(MONTHLY, - count=3, - byweekno=20, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 5, 11, 9, 0), - datetime(1998, 5, 12, 9, 0), - datetime(1998, 5, 13, 9, 0)]) - - def testMonthlyByWeekNoAndWeekDay(self): - # That's a nice one. The first days of week number one - # may be in the last year. - self.assertEqual(list(rrule(MONTHLY, - count=3, - byweekno=1, - byweekday=MO, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 12, 29, 9, 0), - datetime(1999, 1, 4, 9, 0), - datetime(2000, 1, 3, 9, 0)]) - - def testMonthlyByWeekNoAndWeekDayLarge(self): - # Another nice test. The last days of week number 52/53 - # may be in the next year. - self.assertEqual(list(rrule(MONTHLY, - count=3, - byweekno=52, - byweekday=SU, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 12, 28, 9, 0), - datetime(1998, 12, 27, 9, 0), - datetime(2000, 1, 2, 9, 0)]) - - def testMonthlyByWeekNoAndWeekDayLast(self): - self.assertEqual(list(rrule(MONTHLY, - count=3, - byweekno=-1, - byweekday=SU, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 12, 28, 9, 0), - datetime(1999, 1, 3, 9, 0), - datetime(2000, 1, 2, 9, 0)]) - - def testMonthlyByWeekNoAndWeekDay53(self): - self.assertEqual(list(rrule(MONTHLY, - count=3, - byweekno=53, - byweekday=MO, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 12, 28, 9, 0), - datetime(2004, 12, 27, 9, 0), - datetime(2009, 12, 28, 9, 0)]) - - def testMonthlyByEaster(self): - self.assertEqual(list(rrule(MONTHLY, - count=3, - byeaster=0, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 4, 12, 9, 0), - datetime(1999, 4, 4, 9, 0), - datetime(2000, 4, 23, 9, 0)]) - - def testMonthlyByEasterPos(self): - self.assertEqual(list(rrule(MONTHLY, - count=3, - byeaster=1, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 4, 13, 9, 0), - datetime(1999, 4, 5, 9, 0), - datetime(2000, 4, 24, 9, 0)]) - - def testMonthlyByEasterNeg(self): - self.assertEqual(list(rrule(MONTHLY, - count=3, - byeaster=-1, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 4, 11, 9, 0), - datetime(1999, 4, 3, 9, 0), - datetime(2000, 4, 22, 9, 0)]) - - def testMonthlyByHour(self): - self.assertEqual(list(rrule(MONTHLY, - count=3, - byhour=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 18, 0), - datetime(1997, 10, 2, 6, 0), - datetime(1997, 10, 2, 18, 0)]) - - def testMonthlyByMinute(self): - self.assertEqual(list(rrule(MONTHLY, - count=3, - byminute=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 6), - datetime(1997, 9, 2, 9, 18), - datetime(1997, 10, 2, 9, 6)]) - - def testMonthlyBySecond(self): - self.assertEqual(list(rrule(MONTHLY, - count=3, - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0, 6), - datetime(1997, 9, 2, 9, 0, 18), - datetime(1997, 10, 2, 9, 0, 6)]) - - def testMonthlyByHourAndMinute(self): - self.assertEqual(list(rrule(MONTHLY, - count=3, - byhour=(6, 18), - byminute=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 18, 6), - datetime(1997, 9, 2, 18, 18), - datetime(1997, 10, 2, 6, 6)]) - - def testMonthlyByHourAndSecond(self): - self.assertEqual(list(rrule(MONTHLY, - count=3, - byhour=(6, 18), - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 18, 0, 6), - datetime(1997, 9, 2, 18, 0, 18), - datetime(1997, 10, 2, 6, 0, 6)]) - - def testMonthlyByMinuteAndSecond(self): - self.assertEqual(list(rrule(MONTHLY, - count=3, - byminute=(6, 18), - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 6, 6), - datetime(1997, 9, 2, 9, 6, 18), - datetime(1997, 9, 2, 9, 18, 6)]) - - def testMonthlyByHourAndMinuteAndSecond(self): - self.assertEqual(list(rrule(MONTHLY, - count=3, - byhour=(6, 18), - byminute=(6, 18), - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 18, 6, 6), - datetime(1997, 9, 2, 18, 6, 18), - datetime(1997, 9, 2, 18, 18, 6)]) - - def testMonthlyBySetPos(self): - self.assertEqual(list(rrule(MONTHLY, - count=3, - bymonthday=(13, 17), - byhour=(6, 18), - bysetpos=(3, -3), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 13, 18, 0), - datetime(1997, 9, 17, 6, 0), - datetime(1997, 10, 13, 18, 0)]) - - def testWeekly(self): - self.assertEqual(list(rrule(WEEKLY, - count=3, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 9, 9, 0), - datetime(1997, 9, 16, 9, 0)]) - - def testWeeklyInterval(self): - self.assertEqual(list(rrule(WEEKLY, - count=3, - interval=2, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 16, 9, 0), - datetime(1997, 9, 30, 9, 0)]) - - def testWeeklyIntervalLarge(self): - self.assertEqual(list(rrule(WEEKLY, - count=3, - interval=20, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0), - datetime(1998, 1, 20, 9, 0), - datetime(1998, 6, 9, 9, 0)]) - - def testWeeklyByMonth(self): - self.assertEqual(list(rrule(WEEKLY, - count=3, - bymonth=(1, 3), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 6, 9, 0), - datetime(1998, 1, 13, 9, 0), - datetime(1998, 1, 20, 9, 0)]) - - def testWeeklyByMonthDay(self): - self.assertEqual(list(rrule(WEEKLY, - count=3, - bymonthday=(1, 3), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 3, 9, 0), - datetime(1997, 10, 1, 9, 0), - datetime(1997, 10, 3, 9, 0)]) - - def testWeeklyByMonthAndMonthDay(self): - self.assertEqual(list(rrule(WEEKLY, - count=3, - bymonth=(1, 3), - bymonthday=(5, 7), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 5, 9, 0), - datetime(1998, 1, 7, 9, 0), - datetime(1998, 3, 5, 9, 0)]) - - def testWeeklyByWeekDay(self): - self.assertEqual(list(rrule(WEEKLY, - count=3, - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 4, 9, 0), - datetime(1997, 9, 9, 9, 0)]) - - def testWeeklyByNWeekDay(self): - self.assertEqual(list(rrule(WEEKLY, - count=3, - byweekday=(TU(1), TH(-1)), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 4, 9, 0), - datetime(1997, 9, 9, 9, 0)]) - - def testWeeklyByMonthAndWeekDay(self): - # This test is interesting, because it crosses the year - # boundary in a weekly period to find day '1' as a - # valid recurrence. - self.assertEqual(list(rrule(WEEKLY, - count=3, - bymonth=(1, 3), - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 1, 9, 0), - datetime(1998, 1, 6, 9, 0), - datetime(1998, 1, 8, 9, 0)]) - - def testWeeklyByMonthAndNWeekDay(self): - self.assertEqual(list(rrule(WEEKLY, - count=3, - bymonth=(1, 3), - byweekday=(TU(1), TH(-1)), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 1, 9, 0), - datetime(1998, 1, 6, 9, 0), - datetime(1998, 1, 8, 9, 0)]) - - def testWeeklyByMonthDayAndWeekDay(self): - self.assertEqual(list(rrule(WEEKLY, - count=3, - bymonthday=(1, 3), - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 1, 9, 0), - datetime(1998, 2, 3, 9, 0), - datetime(1998, 3, 3, 9, 0)]) - - def testWeeklyByMonthAndMonthDayAndWeekDay(self): - self.assertEqual(list(rrule(WEEKLY, - count=3, - bymonth=(1, 3), - bymonthday=(1, 3), - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 1, 9, 0), - datetime(1998, 3, 3, 9, 0), - datetime(2001, 3, 1, 9, 0)]) - - def testWeeklyByYearDay(self): - self.assertEqual(list(rrule(WEEKLY, - count=4, - byyearday=(1, 100, 200, 365), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 12, 31, 9, 0), - datetime(1998, 1, 1, 9, 0), - datetime(1998, 4, 10, 9, 0), - datetime(1998, 7, 19, 9, 0)]) - - def testWeeklyByYearDayNeg(self): - self.assertEqual(list(rrule(WEEKLY, - count=4, - byyearday=(-365, -266, -166, -1), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 12, 31, 9, 0), - datetime(1998, 1, 1, 9, 0), - datetime(1998, 4, 10, 9, 0), - datetime(1998, 7, 19, 9, 0)]) - - def testWeeklyByMonthAndYearDay(self): - self.assertEqual(list(rrule(WEEKLY, - count=4, - bymonth=(1, 7), - byyearday=(1, 100, 200, 365), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 1, 9, 0), - datetime(1998, 7, 19, 9, 0), - datetime(1999, 1, 1, 9, 0), - datetime(1999, 7, 19, 9, 0)]) - - def testWeeklyByMonthAndYearDayNeg(self): - self.assertEqual(list(rrule(WEEKLY, - count=4, - bymonth=(1, 7), - byyearday=(-365, -266, -166, -1), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 1, 9, 0), - datetime(1998, 7, 19, 9, 0), - datetime(1999, 1, 1, 9, 0), - datetime(1999, 7, 19, 9, 0)]) - - def testWeeklyByWeekNo(self): - self.assertEqual(list(rrule(WEEKLY, - count=3, - byweekno=20, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 5, 11, 9, 0), - datetime(1998, 5, 12, 9, 0), - datetime(1998, 5, 13, 9, 0)]) - - def testWeeklyByWeekNoAndWeekDay(self): - # That's a nice one. The first days of week number one - # may be in the last year. - self.assertEqual(list(rrule(WEEKLY, - count=3, - byweekno=1, - byweekday=MO, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 12, 29, 9, 0), - datetime(1999, 1, 4, 9, 0), - datetime(2000, 1, 3, 9, 0)]) - - def testWeeklyByWeekNoAndWeekDayLarge(self): - # Another nice test. The last days of week number 52/53 - # may be in the next year. - self.assertEqual(list(rrule(WEEKLY, - count=3, - byweekno=52, - byweekday=SU, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 12, 28, 9, 0), - datetime(1998, 12, 27, 9, 0), - datetime(2000, 1, 2, 9, 0)]) - - def testWeeklyByWeekNoAndWeekDayLast(self): - self.assertEqual(list(rrule(WEEKLY, - count=3, - byweekno=-1, - byweekday=SU, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 12, 28, 9, 0), - datetime(1999, 1, 3, 9, 0), - datetime(2000, 1, 2, 9, 0)]) - - def testWeeklyByWeekNoAndWeekDay53(self): - self.assertEqual(list(rrule(WEEKLY, - count=3, - byweekno=53, - byweekday=MO, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 12, 28, 9, 0), - datetime(2004, 12, 27, 9, 0), - datetime(2009, 12, 28, 9, 0)]) - - def testWeeklyByEaster(self): - self.assertEqual(list(rrule(WEEKLY, - count=3, - byeaster=0, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 4, 12, 9, 0), - datetime(1999, 4, 4, 9, 0), - datetime(2000, 4, 23, 9, 0)]) - - def testWeeklyByEasterPos(self): - self.assertEqual(list(rrule(WEEKLY, - count=3, - byeaster=1, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 4, 13, 9, 0), - datetime(1999, 4, 5, 9, 0), - datetime(2000, 4, 24, 9, 0)]) - - def testWeeklyByEasterNeg(self): - self.assertEqual(list(rrule(WEEKLY, - count=3, - byeaster=-1, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 4, 11, 9, 0), - datetime(1999, 4, 3, 9, 0), - datetime(2000, 4, 22, 9, 0)]) - - def testWeeklyByHour(self): - self.assertEqual(list(rrule(WEEKLY, - count=3, - byhour=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 18, 0), - datetime(1997, 9, 9, 6, 0), - datetime(1997, 9, 9, 18, 0)]) - - def testWeeklyByMinute(self): - self.assertEqual(list(rrule(WEEKLY, - count=3, - byminute=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 6), - datetime(1997, 9, 2, 9, 18), - datetime(1997, 9, 9, 9, 6)]) - - def testWeeklyBySecond(self): - self.assertEqual(list(rrule(WEEKLY, - count=3, - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0, 6), - datetime(1997, 9, 2, 9, 0, 18), - datetime(1997, 9, 9, 9, 0, 6)]) - - def testWeeklyByHourAndMinute(self): - self.assertEqual(list(rrule(WEEKLY, - count=3, - byhour=(6, 18), - byminute=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 18, 6), - datetime(1997, 9, 2, 18, 18), - datetime(1997, 9, 9, 6, 6)]) - - def testWeeklyByHourAndSecond(self): - self.assertEqual(list(rrule(WEEKLY, - count=3, - byhour=(6, 18), - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 18, 0, 6), - datetime(1997, 9, 2, 18, 0, 18), - datetime(1997, 9, 9, 6, 0, 6)]) - - def testWeeklyByMinuteAndSecond(self): - self.assertEqual(list(rrule(WEEKLY, - count=3, - byminute=(6, 18), - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 6, 6), - datetime(1997, 9, 2, 9, 6, 18), - datetime(1997, 9, 2, 9, 18, 6)]) - - def testWeeklyByHourAndMinuteAndSecond(self): - self.assertEqual(list(rrule(WEEKLY, - count=3, - byhour=(6, 18), - byminute=(6, 18), - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 18, 6, 6), - datetime(1997, 9, 2, 18, 6, 18), - datetime(1997, 9, 2, 18, 18, 6)]) - - def testWeeklyBySetPos(self): - self.assertEqual(list(rrule(WEEKLY, - count=3, - byweekday=(TU, TH), - byhour=(6, 18), - bysetpos=(3, -3), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 18, 0), - datetime(1997, 9, 4, 6, 0), - datetime(1997, 9, 9, 18, 0)]) - - def testDaily(self): - self.assertEqual(list(rrule(DAILY, - count=3, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 3, 9, 0), - datetime(1997, 9, 4, 9, 0)]) - - def testDailyInterval(self): - self.assertEqual(list(rrule(DAILY, - count=3, - interval=2, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 4, 9, 0), - datetime(1997, 9, 6, 9, 0)]) - - def testDailyIntervalLarge(self): - self.assertEqual(list(rrule(DAILY, - count=3, - interval=92, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 12, 3, 9, 0), - datetime(1998, 3, 5, 9, 0)]) - - def testDailyByMonth(self): - self.assertEqual(list(rrule(DAILY, - count=3, - bymonth=(1, 3), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 1, 9, 0), - datetime(1998, 1, 2, 9, 0), - datetime(1998, 1, 3, 9, 0)]) - - def testDailyByMonthDay(self): - self.assertEqual(list(rrule(DAILY, - count=3, - bymonthday=(1, 3), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 3, 9, 0), - datetime(1997, 10, 1, 9, 0), - datetime(1997, 10, 3, 9, 0)]) - - def testDailyByMonthAndMonthDay(self): - self.assertEqual(list(rrule(DAILY, - count=3, - bymonth=(1, 3), - bymonthday=(5, 7), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 5, 9, 0), - datetime(1998, 1, 7, 9, 0), - datetime(1998, 3, 5, 9, 0)]) - - def testDailyByWeekDay(self): - self.assertEqual(list(rrule(DAILY, - count=3, - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 4, 9, 0), - datetime(1997, 9, 9, 9, 0)]) - - def testDailyByNWeekDay(self): - self.assertEqual(list(rrule(DAILY, - count=3, - byweekday=(TU(1), TH(-1)), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 4, 9, 0), - datetime(1997, 9, 9, 9, 0)]) - - def testDailyByMonthAndWeekDay(self): - self.assertEqual(list(rrule(DAILY, - count=3, - bymonth=(1, 3), - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 1, 9, 0), - datetime(1998, 1, 6, 9, 0), - datetime(1998, 1, 8, 9, 0)]) - - def testDailyByMonthAndNWeekDay(self): - self.assertEqual(list(rrule(DAILY, - count=3, - bymonth=(1, 3), - byweekday=(TU(1), TH(-1)), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 1, 9, 0), - datetime(1998, 1, 6, 9, 0), - datetime(1998, 1, 8, 9, 0)]) - - def testDailyByMonthDayAndWeekDay(self): - self.assertEqual(list(rrule(DAILY, - count=3, - bymonthday=(1, 3), - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 1, 9, 0), - datetime(1998, 2, 3, 9, 0), - datetime(1998, 3, 3, 9, 0)]) - - def testDailyByMonthAndMonthDayAndWeekDay(self): - self.assertEqual(list(rrule(DAILY, - count=3, - bymonth=(1, 3), - bymonthday=(1, 3), - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 1, 9, 0), - datetime(1998, 3, 3, 9, 0), - datetime(2001, 3, 1, 9, 0)]) - - def testDailyByYearDay(self): - self.assertEqual(list(rrule(DAILY, - count=4, - byyearday=(1, 100, 200, 365), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 12, 31, 9, 0), - datetime(1998, 1, 1, 9, 0), - datetime(1998, 4, 10, 9, 0), - datetime(1998, 7, 19, 9, 0)]) - - def testDailyByYearDayNeg(self): - self.assertEqual(list(rrule(DAILY, - count=4, - byyearday=(-365, -266, -166, -1), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 12, 31, 9, 0), - datetime(1998, 1, 1, 9, 0), - datetime(1998, 4, 10, 9, 0), - datetime(1998, 7, 19, 9, 0)]) - - def testDailyByMonthAndYearDay(self): - self.assertEqual(list(rrule(DAILY, - count=4, - bymonth=(1, 7), - byyearday=(1, 100, 200, 365), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 1, 9, 0), - datetime(1998, 7, 19, 9, 0), - datetime(1999, 1, 1, 9, 0), - datetime(1999, 7, 19, 9, 0)]) - - def testDailyByMonthAndYearDayNeg(self): - self.assertEqual(list(rrule(DAILY, - count=4, - bymonth=(1, 7), - byyearday=(-365, -266, -166, -1), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 1, 9, 0), - datetime(1998, 7, 19, 9, 0), - datetime(1999, 1, 1, 9, 0), - datetime(1999, 7, 19, 9, 0)]) - - def testDailyByWeekNo(self): - self.assertEqual(list(rrule(DAILY, - count=3, - byweekno=20, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 5, 11, 9, 0), - datetime(1998, 5, 12, 9, 0), - datetime(1998, 5, 13, 9, 0)]) - - def testDailyByWeekNoAndWeekDay(self): - # That's a nice one. The first days of week number one - # may be in the last year. - self.assertEqual(list(rrule(DAILY, - count=3, - byweekno=1, - byweekday=MO, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 12, 29, 9, 0), - datetime(1999, 1, 4, 9, 0), - datetime(2000, 1, 3, 9, 0)]) - - def testDailyByWeekNoAndWeekDayLarge(self): - # Another nice test. The last days of week number 52/53 - # may be in the next year. - self.assertEqual(list(rrule(DAILY, - count=3, - byweekno=52, - byweekday=SU, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 12, 28, 9, 0), - datetime(1998, 12, 27, 9, 0), - datetime(2000, 1, 2, 9, 0)]) - - def testDailyByWeekNoAndWeekDayLast(self): - self.assertEqual(list(rrule(DAILY, - count=3, - byweekno=-1, - byweekday=SU, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 12, 28, 9, 0), - datetime(1999, 1, 3, 9, 0), - datetime(2000, 1, 2, 9, 0)]) - - def testDailyByWeekNoAndWeekDay53(self): - self.assertEqual(list(rrule(DAILY, - count=3, - byweekno=53, - byweekday=MO, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 12, 28, 9, 0), - datetime(2004, 12, 27, 9, 0), - datetime(2009, 12, 28, 9, 0)]) - - def testDailyByEaster(self): - self.assertEqual(list(rrule(DAILY, - count=3, - byeaster=0, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 4, 12, 9, 0), - datetime(1999, 4, 4, 9, 0), - datetime(2000, 4, 23, 9, 0)]) - - def testDailyByEasterPos(self): - self.assertEqual(list(rrule(DAILY, - count=3, - byeaster=1, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 4, 13, 9, 0), - datetime(1999, 4, 5, 9, 0), - datetime(2000, 4, 24, 9, 0)]) - - def testDailyByEasterNeg(self): - self.assertEqual(list(rrule(DAILY, - count=3, - byeaster=-1, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 4, 11, 9, 0), - datetime(1999, 4, 3, 9, 0), - datetime(2000, 4, 22, 9, 0)]) - - def testDailyByHour(self): - self.assertEqual(list(rrule(DAILY, - count=3, - byhour=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 18, 0), - datetime(1997, 9, 3, 6, 0), - datetime(1997, 9, 3, 18, 0)]) - - def testDailyByMinute(self): - self.assertEqual(list(rrule(DAILY, - count=3, - byminute=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 6), - datetime(1997, 9, 2, 9, 18), - datetime(1997, 9, 3, 9, 6)]) - - def testDailyBySecond(self): - self.assertEqual(list(rrule(DAILY, - count=3, - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0, 6), - datetime(1997, 9, 2, 9, 0, 18), - datetime(1997, 9, 3, 9, 0, 6)]) - - def testDailyByHourAndMinute(self): - self.assertEqual(list(rrule(DAILY, - count=3, - byhour=(6, 18), - byminute=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 18, 6), - datetime(1997, 9, 2, 18, 18), - datetime(1997, 9, 3, 6, 6)]) - - def testDailyByHourAndSecond(self): - self.assertEqual(list(rrule(DAILY, - count=3, - byhour=(6, 18), - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 18, 0, 6), - datetime(1997, 9, 2, 18, 0, 18), - datetime(1997, 9, 3, 6, 0, 6)]) - - def testDailyByMinuteAndSecond(self): - self.assertEqual(list(rrule(DAILY, - count=3, - byminute=(6, 18), - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 6, 6), - datetime(1997, 9, 2, 9, 6, 18), - datetime(1997, 9, 2, 9, 18, 6)]) - - def testDailyByHourAndMinuteAndSecond(self): - self.assertEqual(list(rrule(DAILY, - count=3, - byhour=(6, 18), - byminute=(6, 18), - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 18, 6, 6), - datetime(1997, 9, 2, 18, 6, 18), - datetime(1997, 9, 2, 18, 18, 6)]) - - def testDailyBySetPos(self): - self.assertEqual(list(rrule(DAILY, - count=3, - byhour=(6, 18), - byminute=(15, 45), - bysetpos=(3, -3), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 18, 15), - datetime(1997, 9, 3, 6, 45), - datetime(1997, 9, 3, 18, 15)]) - - def testHourly(self): - self.assertEqual(list(rrule(HOURLY, - count=3, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 2, 10, 0), - datetime(1997, 9, 2, 11, 0)]) - - def testHourlyInterval(self): - self.assertEqual(list(rrule(HOURLY, - count=3, - interval=2, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 2, 11, 0), - datetime(1997, 9, 2, 13, 0)]) - - def testHourlyIntervalLarge(self): - self.assertEqual(list(rrule(HOURLY, - count=3, - interval=769, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 10, 4, 10, 0), - datetime(1997, 11, 5, 11, 0)]) - - def testHourlyByMonth(self): - self.assertEqual(list(rrule(HOURLY, - count=3, - bymonth=(1, 3), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 1, 0, 0), - datetime(1998, 1, 1, 1, 0), - datetime(1998, 1, 1, 2, 0)]) - - def testHourlyByMonthDay(self): - self.assertEqual(list(rrule(HOURLY, - count=3, - bymonthday=(1, 3), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 3, 0, 0), - datetime(1997, 9, 3, 1, 0), - datetime(1997, 9, 3, 2, 0)]) - - def testHourlyByMonthAndMonthDay(self): - self.assertEqual(list(rrule(HOURLY, - count=3, - bymonth=(1, 3), - bymonthday=(5, 7), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 5, 0, 0), - datetime(1998, 1, 5, 1, 0), - datetime(1998, 1, 5, 2, 0)]) - - def testHourlyByWeekDay(self): - self.assertEqual(list(rrule(HOURLY, - count=3, - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 2, 10, 0), - datetime(1997, 9, 2, 11, 0)]) - - def testHourlyByNWeekDay(self): - self.assertEqual(list(rrule(HOURLY, - count=3, - byweekday=(TU(1), TH(-1)), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 2, 10, 0), - datetime(1997, 9, 2, 11, 0)]) - - def testHourlyByMonthAndWeekDay(self): - self.assertEqual(list(rrule(HOURLY, - count=3, - bymonth=(1, 3), - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 1, 0, 0), - datetime(1998, 1, 1, 1, 0), - datetime(1998, 1, 1, 2, 0)]) - - def testHourlyByMonthAndNWeekDay(self): - self.assertEqual(list(rrule(HOURLY, - count=3, - bymonth=(1, 3), - byweekday=(TU(1), TH(-1)), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 1, 0, 0), - datetime(1998, 1, 1, 1, 0), - datetime(1998, 1, 1, 2, 0)]) - - def testHourlyByMonthDayAndWeekDay(self): - self.assertEqual(list(rrule(HOURLY, - count=3, - bymonthday=(1, 3), - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 1, 0, 0), - datetime(1998, 1, 1, 1, 0), - datetime(1998, 1, 1, 2, 0)]) - - def testHourlyByMonthAndMonthDayAndWeekDay(self): - self.assertEqual(list(rrule(HOURLY, - count=3, - bymonth=(1, 3), - bymonthday=(1, 3), - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 1, 0, 0), - datetime(1998, 1, 1, 1, 0), - datetime(1998, 1, 1, 2, 0)]) - - def testHourlyByYearDay(self): - self.assertEqual(list(rrule(HOURLY, - count=4, - byyearday=(1, 100, 200, 365), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 12, 31, 0, 0), - datetime(1997, 12, 31, 1, 0), - datetime(1997, 12, 31, 2, 0), - datetime(1997, 12, 31, 3, 0)]) - - def testHourlyByYearDayNeg(self): - self.assertEqual(list(rrule(HOURLY, - count=4, - byyearday=(-365, -266, -166, -1), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 12, 31, 0, 0), - datetime(1997, 12, 31, 1, 0), - datetime(1997, 12, 31, 2, 0), - datetime(1997, 12, 31, 3, 0)]) - - def testHourlyByMonthAndYearDay(self): - self.assertEqual(list(rrule(HOURLY, - count=4, - bymonth=(4, 7), - byyearday=(1, 100, 200, 365), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 4, 10, 0, 0), - datetime(1998, 4, 10, 1, 0), - datetime(1998, 4, 10, 2, 0), - datetime(1998, 4, 10, 3, 0)]) - - def testHourlyByMonthAndYearDayNeg(self): - self.assertEqual(list(rrule(HOURLY, - count=4, - bymonth=(4, 7), - byyearday=(-365, -266, -166, -1), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 4, 10, 0, 0), - datetime(1998, 4, 10, 1, 0), - datetime(1998, 4, 10, 2, 0), - datetime(1998, 4, 10, 3, 0)]) - - def testHourlyByWeekNo(self): - self.assertEqual(list(rrule(HOURLY, - count=3, - byweekno=20, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 5, 11, 0, 0), - datetime(1998, 5, 11, 1, 0), - datetime(1998, 5, 11, 2, 0)]) - - def testHourlyByWeekNoAndWeekDay(self): - self.assertEqual(list(rrule(HOURLY, - count=3, - byweekno=1, - byweekday=MO, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 12, 29, 0, 0), - datetime(1997, 12, 29, 1, 0), - datetime(1997, 12, 29, 2, 0)]) - - def testHourlyByWeekNoAndWeekDayLarge(self): - self.assertEqual(list(rrule(HOURLY, - count=3, - byweekno=52, - byweekday=SU, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 12, 28, 0, 0), - datetime(1997, 12, 28, 1, 0), - datetime(1997, 12, 28, 2, 0)]) - - def testHourlyByWeekNoAndWeekDayLast(self): - self.assertEqual(list(rrule(HOURLY, - count=3, - byweekno=-1, - byweekday=SU, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 12, 28, 0, 0), - datetime(1997, 12, 28, 1, 0), - datetime(1997, 12, 28, 2, 0)]) - - def testHourlyByWeekNoAndWeekDay53(self): - self.assertEqual(list(rrule(HOURLY, - count=3, - byweekno=53, - byweekday=MO, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 12, 28, 0, 0), - datetime(1998, 12, 28, 1, 0), - datetime(1998, 12, 28, 2, 0)]) - - def testHourlyByEaster(self): - self.assertEqual(list(rrule(HOURLY, - count=3, - byeaster=0, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 4, 12, 0, 0), - datetime(1998, 4, 12, 1, 0), - datetime(1998, 4, 12, 2, 0)]) - - def testHourlyByEasterPos(self): - self.assertEqual(list(rrule(HOURLY, - count=3, - byeaster=1, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 4, 13, 0, 0), - datetime(1998, 4, 13, 1, 0), - datetime(1998, 4, 13, 2, 0)]) - - def testHourlyByEasterNeg(self): - self.assertEqual(list(rrule(HOURLY, - count=3, - byeaster=-1, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 4, 11, 0, 0), - datetime(1998, 4, 11, 1, 0), - datetime(1998, 4, 11, 2, 0)]) - - def testHourlyByHour(self): - self.assertEqual(list(rrule(HOURLY, - count=3, - byhour=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 18, 0), - datetime(1997, 9, 3, 6, 0), - datetime(1997, 9, 3, 18, 0)]) - - def testHourlyByMinute(self): - self.assertEqual(list(rrule(HOURLY, - count=3, - byminute=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 6), - datetime(1997, 9, 2, 9, 18), - datetime(1997, 9, 2, 10, 6)]) - - def testHourlyBySecond(self): - self.assertEqual(list(rrule(HOURLY, - count=3, - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0, 6), - datetime(1997, 9, 2, 9, 0, 18), - datetime(1997, 9, 2, 10, 0, 6)]) - - def testHourlyByHourAndMinute(self): - self.assertEqual(list(rrule(HOURLY, - count=3, - byhour=(6, 18), - byminute=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 18, 6), - datetime(1997, 9, 2, 18, 18), - datetime(1997, 9, 3, 6, 6)]) - - def testHourlyByHourAndSecond(self): - self.assertEqual(list(rrule(HOURLY, - count=3, - byhour=(6, 18), - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 18, 0, 6), - datetime(1997, 9, 2, 18, 0, 18), - datetime(1997, 9, 3, 6, 0, 6)]) - - def testHourlyByMinuteAndSecond(self): - self.assertEqual(list(rrule(HOURLY, - count=3, - byminute=(6, 18), - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 6, 6), - datetime(1997, 9, 2, 9, 6, 18), - datetime(1997, 9, 2, 9, 18, 6)]) - - def testHourlyByHourAndMinuteAndSecond(self): - self.assertEqual(list(rrule(HOURLY, - count=3, - byhour=(6, 18), - byminute=(6, 18), - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 18, 6, 6), - datetime(1997, 9, 2, 18, 6, 18), - datetime(1997, 9, 2, 18, 18, 6)]) - - def testHourlyBySetPos(self): - self.assertEqual(list(rrule(HOURLY, - count=3, - byminute=(15, 45), - bysecond=(15, 45), - bysetpos=(3, -3), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 15, 45), - datetime(1997, 9, 2, 9, 45, 15), - datetime(1997, 9, 2, 10, 15, 45)]) - - def testMinutely(self): - self.assertEqual(list(rrule(MINUTELY, - count=3, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 2, 9, 1), - datetime(1997, 9, 2, 9, 2)]) - - def testMinutelyInterval(self): - self.assertEqual(list(rrule(MINUTELY, - count=3, - interval=2, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 2, 9, 2), - datetime(1997, 9, 2, 9, 4)]) - - def testMinutelyIntervalLarge(self): - self.assertEqual(list(rrule(MINUTELY, - count=3, - interval=1501, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 3, 10, 1), - datetime(1997, 9, 4, 11, 2)]) - - def testMinutelyByMonth(self): - self.assertEqual(list(rrule(MINUTELY, - count=3, - bymonth=(1, 3), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 1, 0, 0), - datetime(1998, 1, 1, 0, 1), - datetime(1998, 1, 1, 0, 2)]) - - def testMinutelyByMonthDay(self): - self.assertEqual(list(rrule(MINUTELY, - count=3, - bymonthday=(1, 3), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 3, 0, 0), - datetime(1997, 9, 3, 0, 1), - datetime(1997, 9, 3, 0, 2)]) - - def testMinutelyByMonthAndMonthDay(self): - self.assertEqual(list(rrule(MINUTELY, - count=3, - bymonth=(1, 3), - bymonthday=(5, 7), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 5, 0, 0), - datetime(1998, 1, 5, 0, 1), - datetime(1998, 1, 5, 0, 2)]) - - def testMinutelyByWeekDay(self): - self.assertEqual(list(rrule(MINUTELY, - count=3, - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 2, 9, 1), - datetime(1997, 9, 2, 9, 2)]) - - def testMinutelyByNWeekDay(self): - self.assertEqual(list(rrule(MINUTELY, - count=3, - byweekday=(TU(1), TH(-1)), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 2, 9, 1), - datetime(1997, 9, 2, 9, 2)]) - - def testMinutelyByMonthAndWeekDay(self): - self.assertEqual(list(rrule(MINUTELY, - count=3, - bymonth=(1, 3), - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 1, 0, 0), - datetime(1998, 1, 1, 0, 1), - datetime(1998, 1, 1, 0, 2)]) - - def testMinutelyByMonthAndNWeekDay(self): - self.assertEqual(list(rrule(MINUTELY, - count=3, - bymonth=(1, 3), - byweekday=(TU(1), TH(-1)), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 1, 0, 0), - datetime(1998, 1, 1, 0, 1), - datetime(1998, 1, 1, 0, 2)]) - - def testMinutelyByMonthDayAndWeekDay(self): - self.assertEqual(list(rrule(MINUTELY, - count=3, - bymonthday=(1, 3), - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 1, 0, 0), - datetime(1998, 1, 1, 0, 1), - datetime(1998, 1, 1, 0, 2)]) - - def testMinutelyByMonthAndMonthDayAndWeekDay(self): - self.assertEqual(list(rrule(MINUTELY, - count=3, - bymonth=(1, 3), - bymonthday=(1, 3), - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 1, 0, 0), - datetime(1998, 1, 1, 0, 1), - datetime(1998, 1, 1, 0, 2)]) - - def testMinutelyByYearDay(self): - self.assertEqual(list(rrule(MINUTELY, - count=4, - byyearday=(1, 100, 200, 365), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 12, 31, 0, 0), - datetime(1997, 12, 31, 0, 1), - datetime(1997, 12, 31, 0, 2), - datetime(1997, 12, 31, 0, 3)]) - - def testMinutelyByYearDayNeg(self): - self.assertEqual(list(rrule(MINUTELY, - count=4, - byyearday=(-365, -266, -166, -1), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 12, 31, 0, 0), - datetime(1997, 12, 31, 0, 1), - datetime(1997, 12, 31, 0, 2), - datetime(1997, 12, 31, 0, 3)]) - - def testMinutelyByMonthAndYearDay(self): - self.assertEqual(list(rrule(MINUTELY, - count=4, - bymonth=(4, 7), - byyearday=(1, 100, 200, 365), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 4, 10, 0, 0), - datetime(1998, 4, 10, 0, 1), - datetime(1998, 4, 10, 0, 2), - datetime(1998, 4, 10, 0, 3)]) - - def testMinutelyByMonthAndYearDayNeg(self): - self.assertEqual(list(rrule(MINUTELY, - count=4, - bymonth=(4, 7), - byyearday=(-365, -266, -166, -1), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 4, 10, 0, 0), - datetime(1998, 4, 10, 0, 1), - datetime(1998, 4, 10, 0, 2), - datetime(1998, 4, 10, 0, 3)]) - - def testMinutelyByWeekNo(self): - self.assertEqual(list(rrule(MINUTELY, - count=3, - byweekno=20, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 5, 11, 0, 0), - datetime(1998, 5, 11, 0, 1), - datetime(1998, 5, 11, 0, 2)]) - - def testMinutelyByWeekNoAndWeekDay(self): - self.assertEqual(list(rrule(MINUTELY, - count=3, - byweekno=1, - byweekday=MO, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 12, 29, 0, 0), - datetime(1997, 12, 29, 0, 1), - datetime(1997, 12, 29, 0, 2)]) - - def testMinutelyByWeekNoAndWeekDayLarge(self): - self.assertEqual(list(rrule(MINUTELY, - count=3, - byweekno=52, - byweekday=SU, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 12, 28, 0, 0), - datetime(1997, 12, 28, 0, 1), - datetime(1997, 12, 28, 0, 2)]) - - def testMinutelyByWeekNoAndWeekDayLast(self): - self.assertEqual(list(rrule(MINUTELY, - count=3, - byweekno=-1, - byweekday=SU, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 12, 28, 0, 0), - datetime(1997, 12, 28, 0, 1), - datetime(1997, 12, 28, 0, 2)]) - - def testMinutelyByWeekNoAndWeekDay53(self): - self.assertEqual(list(rrule(MINUTELY, - count=3, - byweekno=53, - byweekday=MO, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 12, 28, 0, 0), - datetime(1998, 12, 28, 0, 1), - datetime(1998, 12, 28, 0, 2)]) - - def testMinutelyByEaster(self): - self.assertEqual(list(rrule(MINUTELY, - count=3, - byeaster=0, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 4, 12, 0, 0), - datetime(1998, 4, 12, 0, 1), - datetime(1998, 4, 12, 0, 2)]) - - def testMinutelyByEasterPos(self): - self.assertEqual(list(rrule(MINUTELY, - count=3, - byeaster=1, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 4, 13, 0, 0), - datetime(1998, 4, 13, 0, 1), - datetime(1998, 4, 13, 0, 2)]) - - def testMinutelyByEasterNeg(self): - self.assertEqual(list(rrule(MINUTELY, - count=3, - byeaster=-1, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 4, 11, 0, 0), - datetime(1998, 4, 11, 0, 1), - datetime(1998, 4, 11, 0, 2)]) - - def testMinutelyByHour(self): - self.assertEqual(list(rrule(MINUTELY, - count=3, - byhour=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 18, 0), - datetime(1997, 9, 2, 18, 1), - datetime(1997, 9, 2, 18, 2)]) - - def testMinutelyByMinute(self): - self.assertEqual(list(rrule(MINUTELY, - count=3, - byminute=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 6), - datetime(1997, 9, 2, 9, 18), - datetime(1997, 9, 2, 10, 6)]) - - def testMinutelyBySecond(self): - self.assertEqual(list(rrule(MINUTELY, - count=3, - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0, 6), - datetime(1997, 9, 2, 9, 0, 18), - datetime(1997, 9, 2, 9, 1, 6)]) - - def testMinutelyByHourAndMinute(self): - self.assertEqual(list(rrule(MINUTELY, - count=3, - byhour=(6, 18), - byminute=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 18, 6), - datetime(1997, 9, 2, 18, 18), - datetime(1997, 9, 3, 6, 6)]) - - def testMinutelyByHourAndSecond(self): - self.assertEqual(list(rrule(MINUTELY, - count=3, - byhour=(6, 18), - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 18, 0, 6), - datetime(1997, 9, 2, 18, 0, 18), - datetime(1997, 9, 2, 18, 1, 6)]) - - def testMinutelyByMinuteAndSecond(self): - self.assertEqual(list(rrule(MINUTELY, - count=3, - byminute=(6, 18), - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 6, 6), - datetime(1997, 9, 2, 9, 6, 18), - datetime(1997, 9, 2, 9, 18, 6)]) - - def testMinutelyByHourAndMinuteAndSecond(self): - self.assertEqual(list(rrule(MINUTELY, - count=3, - byhour=(6, 18), - byminute=(6, 18), - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 18, 6, 6), - datetime(1997, 9, 2, 18, 6, 18), - datetime(1997, 9, 2, 18, 18, 6)]) - - def testMinutelyBySetPos(self): - self.assertEqual(list(rrule(MINUTELY, - count=3, - bysecond=(15, 30, 45), - bysetpos=(3, -3), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0, 15), - datetime(1997, 9, 2, 9, 0, 45), - datetime(1997, 9, 2, 9, 1, 15)]) - - def testSecondly(self): - self.assertEqual(list(rrule(SECONDLY, - count=3, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0, 0), - datetime(1997, 9, 2, 9, 0, 1), - datetime(1997, 9, 2, 9, 0, 2)]) - - def testSecondlyInterval(self): - self.assertEqual(list(rrule(SECONDLY, - count=3, - interval=2, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0, 0), - datetime(1997, 9, 2, 9, 0, 2), - datetime(1997, 9, 2, 9, 0, 4)]) - - def testSecondlyIntervalLarge(self): - self.assertEqual(list(rrule(SECONDLY, - count=3, - interval=90061, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0, 0), - datetime(1997, 9, 3, 10, 1, 1), - datetime(1997, 9, 4, 11, 2, 2)]) - - def testSecondlyByMonth(self): - self.assertEqual(list(rrule(SECONDLY, - count=3, - bymonth=(1, 3), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 1, 0, 0, 0), - datetime(1998, 1, 1, 0, 0, 1), - datetime(1998, 1, 1, 0, 0, 2)]) - - def testSecondlyByMonthDay(self): - self.assertEqual(list(rrule(SECONDLY, - count=3, - bymonthday=(1, 3), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 3, 0, 0, 0), - datetime(1997, 9, 3, 0, 0, 1), - datetime(1997, 9, 3, 0, 0, 2)]) - - def testSecondlyByMonthAndMonthDay(self): - self.assertEqual(list(rrule(SECONDLY, - count=3, - bymonth=(1, 3), - bymonthday=(5, 7), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 5, 0, 0, 0), - datetime(1998, 1, 5, 0, 0, 1), - datetime(1998, 1, 5, 0, 0, 2)]) - - def testSecondlyByWeekDay(self): - self.assertEqual(list(rrule(SECONDLY, - count=3, - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0, 0), - datetime(1997, 9, 2, 9, 0, 1), - datetime(1997, 9, 2, 9, 0, 2)]) - - def testSecondlyByNWeekDay(self): - self.assertEqual(list(rrule(SECONDLY, - count=3, - byweekday=(TU(1), TH(-1)), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0, 0), - datetime(1997, 9, 2, 9, 0, 1), - datetime(1997, 9, 2, 9, 0, 2)]) - - def testSecondlyByMonthAndWeekDay(self): - self.assertEqual(list(rrule(SECONDLY, - count=3, - bymonth=(1, 3), - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 1, 0, 0, 0), - datetime(1998, 1, 1, 0, 0, 1), - datetime(1998, 1, 1, 0, 0, 2)]) - - def testSecondlyByMonthAndNWeekDay(self): - self.assertEqual(list(rrule(SECONDLY, - count=3, - bymonth=(1, 3), - byweekday=(TU(1), TH(-1)), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 1, 0, 0, 0), - datetime(1998, 1, 1, 0, 0, 1), - datetime(1998, 1, 1, 0, 0, 2)]) - - def testSecondlyByMonthDayAndWeekDay(self): - self.assertEqual(list(rrule(SECONDLY, - count=3, - bymonthday=(1, 3), - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 1, 0, 0, 0), - datetime(1998, 1, 1, 0, 0, 1), - datetime(1998, 1, 1, 0, 0, 2)]) - - def testSecondlyByMonthAndMonthDayAndWeekDay(self): - self.assertEqual(list(rrule(SECONDLY, - count=3, - bymonth=(1, 3), - bymonthday=(1, 3), - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 1, 0, 0, 0), - datetime(1998, 1, 1, 0, 0, 1), - datetime(1998, 1, 1, 0, 0, 2)]) - - def testSecondlyByYearDay(self): - self.assertEqual(list(rrule(SECONDLY, - count=4, - byyearday=(1, 100, 200, 365), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 12, 31, 0, 0, 0), - datetime(1997, 12, 31, 0, 0, 1), - datetime(1997, 12, 31, 0, 0, 2), - datetime(1997, 12, 31, 0, 0, 3)]) - - def testSecondlyByYearDayNeg(self): - self.assertEqual(list(rrule(SECONDLY, - count=4, - byyearday=(-365, -266, -166, -1), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 12, 31, 0, 0, 0), - datetime(1997, 12, 31, 0, 0, 1), - datetime(1997, 12, 31, 0, 0, 2), - datetime(1997, 12, 31, 0, 0, 3)]) - - def testSecondlyByMonthAndYearDay(self): - self.assertEqual(list(rrule(SECONDLY, - count=4, - bymonth=(4, 7), - byyearday=(1, 100, 200, 365), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 4, 10, 0, 0, 0), - datetime(1998, 4, 10, 0, 0, 1), - datetime(1998, 4, 10, 0, 0, 2), - datetime(1998, 4, 10, 0, 0, 3)]) - - def testSecondlyByMonthAndYearDayNeg(self): - self.assertEqual(list(rrule(SECONDLY, - count=4, - bymonth=(4, 7), - byyearday=(-365, -266, -166, -1), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 4, 10, 0, 0, 0), - datetime(1998, 4, 10, 0, 0, 1), - datetime(1998, 4, 10, 0, 0, 2), - datetime(1998, 4, 10, 0, 0, 3)]) - - def testSecondlyByWeekNo(self): - self.assertEqual(list(rrule(SECONDLY, - count=3, - byweekno=20, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 5, 11, 0, 0, 0), - datetime(1998, 5, 11, 0, 0, 1), - datetime(1998, 5, 11, 0, 0, 2)]) - - def testSecondlyByWeekNoAndWeekDay(self): - self.assertEqual(list(rrule(SECONDLY, - count=3, - byweekno=1, - byweekday=MO, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 12, 29, 0, 0, 0), - datetime(1997, 12, 29, 0, 0, 1), - datetime(1997, 12, 29, 0, 0, 2)]) - - def testSecondlyByWeekNoAndWeekDayLarge(self): - self.assertEqual(list(rrule(SECONDLY, - count=3, - byweekno=52, - byweekday=SU, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 12, 28, 0, 0, 0), - datetime(1997, 12, 28, 0, 0, 1), - datetime(1997, 12, 28, 0, 0, 2)]) - - def testSecondlyByWeekNoAndWeekDayLast(self): - self.assertEqual(list(rrule(SECONDLY, - count=3, - byweekno=-1, - byweekday=SU, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 12, 28, 0, 0, 0), - datetime(1997, 12, 28, 0, 0, 1), - datetime(1997, 12, 28, 0, 0, 2)]) - - def testSecondlyByWeekNoAndWeekDay53(self): - self.assertEqual(list(rrule(SECONDLY, - count=3, - byweekno=53, - byweekday=MO, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 12, 28, 0, 0, 0), - datetime(1998, 12, 28, 0, 0, 1), - datetime(1998, 12, 28, 0, 0, 2)]) - - def testSecondlyByEaster(self): - self.assertEqual(list(rrule(SECONDLY, - count=3, - byeaster=0, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 4, 12, 0, 0, 0), - datetime(1998, 4, 12, 0, 0, 1), - datetime(1998, 4, 12, 0, 0, 2)]) - - def testSecondlyByEasterPos(self): - self.assertEqual(list(rrule(SECONDLY, - count=3, - byeaster=1, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 4, 13, 0, 0, 0), - datetime(1998, 4, 13, 0, 0, 1), - datetime(1998, 4, 13, 0, 0, 2)]) - - def testSecondlyByEasterNeg(self): - self.assertEqual(list(rrule(SECONDLY, - count=3, - byeaster=-1, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 4, 11, 0, 0, 0), - datetime(1998, 4, 11, 0, 0, 1), - datetime(1998, 4, 11, 0, 0, 2)]) - - def testSecondlyByHour(self): - self.assertEqual(list(rrule(SECONDLY, - count=3, - byhour=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 18, 0, 0), - datetime(1997, 9, 2, 18, 0, 1), - datetime(1997, 9, 2, 18, 0, 2)]) - - def testSecondlyByMinute(self): - self.assertEqual(list(rrule(SECONDLY, - count=3, - byminute=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 6, 0), - datetime(1997, 9, 2, 9, 6, 1), - datetime(1997, 9, 2, 9, 6, 2)]) - - def testSecondlyBySecond(self): - self.assertEqual(list(rrule(SECONDLY, - count=3, - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0, 6), - datetime(1997, 9, 2, 9, 0, 18), - datetime(1997, 9, 2, 9, 1, 6)]) - - def testSecondlyByHourAndMinute(self): - self.assertEqual(list(rrule(SECONDLY, - count=3, - byhour=(6, 18), - byminute=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 18, 6, 0), - datetime(1997, 9, 2, 18, 6, 1), - datetime(1997, 9, 2, 18, 6, 2)]) - - def testSecondlyByHourAndSecond(self): - self.assertEqual(list(rrule(SECONDLY, - count=3, - byhour=(6, 18), - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 18, 0, 6), - datetime(1997, 9, 2, 18, 0, 18), - datetime(1997, 9, 2, 18, 1, 6)]) - - def testSecondlyByMinuteAndSecond(self): - self.assertEqual(list(rrule(SECONDLY, - count=3, - byminute=(6, 18), - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 6, 6), - datetime(1997, 9, 2, 9, 6, 18), - datetime(1997, 9, 2, 9, 18, 6)]) - - def testSecondlyByHourAndMinuteAndSecond(self): - self.assertEqual(list(rrule(SECONDLY, - count=3, - byhour=(6, 18), - byminute=(6, 18), - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 18, 6, 6), - datetime(1997, 9, 2, 18, 6, 18), - datetime(1997, 9, 2, 18, 18, 6)]) - - def testSecondlyByHourAndMinuteAndSecondBug(self): - # This explores a bug found by Mathieu Bridon. - self.assertEqual(list(rrule(SECONDLY, - count=3, - bysecond=(0,), - byminute=(1,), - dtstart=datetime(2010, 3, 22, 12, 1))), - [datetime(2010, 3, 22, 12, 1), - datetime(2010, 3, 22, 13, 1), - datetime(2010, 3, 22, 14, 1)]) - - def testLongIntegers(self): - if not PY3: # There is no longs in python3 - self.assertEqual(list(rrule(MINUTELY, - count=long(2), - interval=long(2), - bymonth=long(2), - byweekday=long(3), - byhour=long(6), - byminute=long(6), - bysecond=long(6), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 2, 5, 6, 6, 6), - datetime(1998, 2, 12, 6, 6, 6)]) - self.assertEqual(list(rrule(YEARLY, - count=long(2), - bymonthday=long(5), - byweekno=long(2), - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1998, 1, 5, 9, 0), - datetime(2004, 1, 5, 9, 0)]) - - def testHourlyBadRRule(self): - """ - When `byhour` is specified with `freq=HOURLY`, there are certain - combinations of `dtstart` and `byhour` which result in an rrule with no - valid values. - - See https://github.com/dateutil/dateutil/issues/4 - """ - - self.assertRaises(ValueError, rrule, HOURLY, - **dict(interval=4, byhour=(7, 11, 15, 19), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testMinutelyBadRRule(self): - """ - See :func:`testHourlyBadRRule` for details. - """ - - self.assertRaises(ValueError, rrule, MINUTELY, - **dict(interval=12, byminute=(10, 11, 25, 39, 50), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testSecondlyBadRRule(self): - """ - See :func:`testHourlyBadRRule` for details. - """ - - self.assertRaises(ValueError, rrule, SECONDLY, - **dict(interval=10, bysecond=(2, 15, 37, 42, 59), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testMinutelyBadComboRRule(self): - """ - Certain values of :param:`interval` in :class:`rrule`, when combined - with certain values of :param:`byhour` create rules which apply to no - valid dates. The library should detect this case in the iterator and - raise a :exception:`ValueError`. - """ - - # In Python 2.7 you can use a context manager for this. - def make_bad_rrule(): - list(rrule(MINUTELY, interval=120, byhour=(10, 12, 14, 16), - count=2, dtstart=datetime(1997, 9, 2, 9, 0))) - - self.assertRaises(ValueError, make_bad_rrule) - - def testSecondlyBadComboRRule(self): - """ - See :func:`testMinutelyBadComboRRule' for details. - """ - - # In Python 2.7 you can use a context manager for this. - def make_bad_minute_rrule(): - list(rrule(SECONDLY, interval=360, byminute=(10, 28, 49), - count=4, dtstart=datetime(1997, 9, 2, 9, 0))) - - def make_bad_hour_rrule(): - list(rrule(SECONDLY, interval=43200, byhour=(2, 10, 18, 23), - count=4, dtstart=datetime(1997, 9, 2, 9, 0))) - - self.assertRaises(ValueError, make_bad_minute_rrule) - self.assertRaises(ValueError, make_bad_hour_rrule) - - def testBadUntilCountRRule(self): - """ - See rfc-2445 4.3.10 - This checks for the deprecation warning, and will - eventually check for an error. - """ - with self.assertWarns(DeprecationWarning): - rrule(DAILY, dtstart=datetime(1997, 9, 2, 9, 0), - count=3, until=datetime(1997, 9, 4, 9, 0)) - - def testUntilNotMatching(self): - self.assertEqual(list(rrule(DAILY, - dtstart=datetime(1997, 9, 2, 9, 0), - until=datetime(1997, 9, 5, 8, 0))), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 3, 9, 0), - datetime(1997, 9, 4, 9, 0)]) - - def testUntilMatching(self): - self.assertEqual(list(rrule(DAILY, - dtstart=datetime(1997, 9, 2, 9, 0), - until=datetime(1997, 9, 4, 9, 0))), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 3, 9, 0), - datetime(1997, 9, 4, 9, 0)]) - - def testUntilSingle(self): - self.assertEqual(list(rrule(DAILY, - dtstart=datetime(1997, 9, 2, 9, 0), - until=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0)]) - - def testUntilEmpty(self): - self.assertEqual(list(rrule(DAILY, - dtstart=datetime(1997, 9, 2, 9, 0), - until=datetime(1997, 9, 1, 9, 0))), - []) - - def testUntilWithDate(self): - self.assertEqual(list(rrule(DAILY, - dtstart=datetime(1997, 9, 2, 9, 0), - until=date(1997, 9, 5))), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 3, 9, 0), - datetime(1997, 9, 4, 9, 0)]) - - def testWkStIntervalMO(self): - self.assertEqual(list(rrule(WEEKLY, - count=3, - interval=2, - byweekday=(TU, SU), - wkst=MO, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 7, 9, 0), - datetime(1997, 9, 16, 9, 0)]) - - def testWkStIntervalSU(self): - self.assertEqual(list(rrule(WEEKLY, - count=3, - interval=2, - byweekday=(TU, SU), - wkst=SU, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 14, 9, 0), - datetime(1997, 9, 16, 9, 0)]) - - def testDTStartIsDate(self): - self.assertEqual(list(rrule(DAILY, - count=3, - dtstart=date(1997, 9, 2))), - [datetime(1997, 9, 2, 0, 0), - datetime(1997, 9, 3, 0, 0), - datetime(1997, 9, 4, 0, 0)]) - - def testDTStartWithMicroseconds(self): - self.assertEqual(list(rrule(DAILY, - count=3, - dtstart=datetime(1997, 9, 2, 9, 0, 0, 500000))), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 3, 9, 0), - datetime(1997, 9, 4, 9, 0)]) - - def testMaxYear(self): - self.assertEqual(list(rrule(YEARLY, - count=3, - bymonth=2, - bymonthday=31, - dtstart=datetime(9997, 9, 2, 9, 0, 0))), - []) - - def testGetItem(self): - self.assertEqual(rrule(DAILY, - count=3, - dtstart=datetime(1997, 9, 2, 9, 0))[0], - datetime(1997, 9, 2, 9, 0)) - - def testGetItemNeg(self): - self.assertEqual(rrule(DAILY, - count=3, - dtstart=datetime(1997, 9, 2, 9, 0))[-1], - datetime(1997, 9, 4, 9, 0)) - - def testGetItemSlice(self): - self.assertEqual(rrule(DAILY, - # count=3, - dtstart=datetime(1997, 9, 2, 9, 0))[1:2], - [datetime(1997, 9, 3, 9, 0)]) - - def testGetItemSliceEmpty(self): - self.assertEqual(rrule(DAILY, - count=3, - dtstart=datetime(1997, 9, 2, 9, 0))[:], - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 3, 9, 0), - datetime(1997, 9, 4, 9, 0)]) - - def testGetItemSliceStep(self): - self.assertEqual(rrule(DAILY, - count=3, - dtstart=datetime(1997, 9, 2, 9, 0))[::-2], - [datetime(1997, 9, 4, 9, 0), - datetime(1997, 9, 2, 9, 0)]) - - def testCount(self): - self.assertEqual(rrule(DAILY, - count=3, - dtstart=datetime(1997, 9, 2, 9, 0)).count(), - 3) - - def testContains(self): - rr = rrule(DAILY, count=3, dtstart=datetime(1997, 9, 2, 9, 0)) - self.assertEqual(datetime(1997, 9, 3, 9, 0) in rr, True) - - def testContainsNot(self): - rr = rrule(DAILY, count=3, dtstart=datetime(1997, 9, 2, 9, 0)) - self.assertEqual(datetime(1997, 9, 3, 9, 0) not in rr, False) - - def testBefore(self): - self.assertEqual(rrule(DAILY, # count=5 - dtstart=datetime(1997, 9, 2, 9, 0)).before(datetime(1997, 9, 5, 9, 0)), - datetime(1997, 9, 4, 9, 0)) - - def testBeforeInc(self): - self.assertEqual(rrule(DAILY, - #count=5, - dtstart=datetime(1997, 9, 2, 9, 0)) - .before(datetime(1997, 9, 5, 9, 0), inc=True), - datetime(1997, 9, 5, 9, 0)) - - def testAfter(self): - self.assertEqual(rrule(DAILY, - #count=5, - dtstart=datetime(1997, 9, 2, 9, 0)) - .after(datetime(1997, 9, 4, 9, 0)), - datetime(1997, 9, 5, 9, 0)) - - def testAfterInc(self): - self.assertEqual(rrule(DAILY, - #count=5, - dtstart=datetime(1997, 9, 2, 9, 0)) - .after(datetime(1997, 9, 4, 9, 0), inc=True), - datetime(1997, 9, 4, 9, 0)) - - def testXAfter(self): - self.assertEqual(list(rrule(DAILY, - dtstart=datetime(1997, 9, 2, 9, 0)) - .xafter(datetime(1997, 9, 8, 9, 0), count=12)), - [datetime(1997, 9, 9, 9, 0), - datetime(1997, 9, 10, 9, 0), - datetime(1997, 9, 11, 9, 0), - datetime(1997, 9, 12, 9, 0), - datetime(1997, 9, 13, 9, 0), - datetime(1997, 9, 14, 9, 0), - datetime(1997, 9, 15, 9, 0), - datetime(1997, 9, 16, 9, 0), - datetime(1997, 9, 17, 9, 0), - datetime(1997, 9, 18, 9, 0), - datetime(1997, 9, 19, 9, 0), - datetime(1997, 9, 20, 9, 0)]) - - def testXAfterInc(self): - self.assertEqual(list(rrule(DAILY, - dtstart=datetime(1997, 9, 2, 9, 0)) - .xafter(datetime(1997, 9, 8, 9, 0), count=12, inc=True)), - [datetime(1997, 9, 8, 9, 0), - datetime(1997, 9, 9, 9, 0), - datetime(1997, 9, 10, 9, 0), - datetime(1997, 9, 11, 9, 0), - datetime(1997, 9, 12, 9, 0), - datetime(1997, 9, 13, 9, 0), - datetime(1997, 9, 14, 9, 0), - datetime(1997, 9, 15, 9, 0), - datetime(1997, 9, 16, 9, 0), - datetime(1997, 9, 17, 9, 0), - datetime(1997, 9, 18, 9, 0), - datetime(1997, 9, 19, 9, 0)]) - - def testBetween(self): - self.assertEqual(rrule(DAILY, - #count=5, - dtstart=datetime(1997, 9, 2, 9, 0)) - .between(datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 6, 9, 0)), - [datetime(1997, 9, 3, 9, 0), - datetime(1997, 9, 4, 9, 0), - datetime(1997, 9, 5, 9, 0)]) - - def testBetweenInc(self): - self.assertEqual(rrule(DAILY, - #count=5, - dtstart=datetime(1997, 9, 2, 9, 0)) - .between(datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 6, 9, 0), inc=True), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 3, 9, 0), - datetime(1997, 9, 4, 9, 0), - datetime(1997, 9, 5, 9, 0), - datetime(1997, 9, 6, 9, 0)]) - - def testCachePre(self): - rr = rrule(DAILY, count=15, cache=True, - dtstart=datetime(1997, 9, 2, 9, 0)) - self.assertEqual(list(rr), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 3, 9, 0), - datetime(1997, 9, 4, 9, 0), - datetime(1997, 9, 5, 9, 0), - datetime(1997, 9, 6, 9, 0), - datetime(1997, 9, 7, 9, 0), - datetime(1997, 9, 8, 9, 0), - datetime(1997, 9, 9, 9, 0), - datetime(1997, 9, 10, 9, 0), - datetime(1997, 9, 11, 9, 0), - datetime(1997, 9, 12, 9, 0), - datetime(1997, 9, 13, 9, 0), - datetime(1997, 9, 14, 9, 0), - datetime(1997, 9, 15, 9, 0), - datetime(1997, 9, 16, 9, 0)]) - - def testCachePost(self): - rr = rrule(DAILY, count=15, cache=True, - dtstart=datetime(1997, 9, 2, 9, 0)) - for x in rr: pass - self.assertEqual(list(rr), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 3, 9, 0), - datetime(1997, 9, 4, 9, 0), - datetime(1997, 9, 5, 9, 0), - datetime(1997, 9, 6, 9, 0), - datetime(1997, 9, 7, 9, 0), - datetime(1997, 9, 8, 9, 0), - datetime(1997, 9, 9, 9, 0), - datetime(1997, 9, 10, 9, 0), - datetime(1997, 9, 11, 9, 0), - datetime(1997, 9, 12, 9, 0), - datetime(1997, 9, 13, 9, 0), - datetime(1997, 9, 14, 9, 0), - datetime(1997, 9, 15, 9, 0), - datetime(1997, 9, 16, 9, 0)]) - - def testCachePostInternal(self): - rr = rrule(DAILY, count=15, cache=True, - dtstart=datetime(1997, 9, 2, 9, 0)) - for x in rr: pass - self.assertEqual(rr._cache, - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 3, 9, 0), - datetime(1997, 9, 4, 9, 0), - datetime(1997, 9, 5, 9, 0), - datetime(1997, 9, 6, 9, 0), - datetime(1997, 9, 7, 9, 0), - datetime(1997, 9, 8, 9, 0), - datetime(1997, 9, 9, 9, 0), - datetime(1997, 9, 10, 9, 0), - datetime(1997, 9, 11, 9, 0), - datetime(1997, 9, 12, 9, 0), - datetime(1997, 9, 13, 9, 0), - datetime(1997, 9, 14, 9, 0), - datetime(1997, 9, 15, 9, 0), - datetime(1997, 9, 16, 9, 0)]) - - def testCachePreContains(self): - rr = rrule(DAILY, count=3, cache=True, - dtstart=datetime(1997, 9, 2, 9, 0)) - self.assertEqual(datetime(1997, 9, 3, 9, 0) in rr, True) - - def testCachePostContains(self): - rr = rrule(DAILY, count=3, cache=True, - dtstart=datetime(1997, 9, 2, 9, 0)) - for x in rr: pass - self.assertEqual(datetime(1997, 9, 3, 9, 0) in rr, True) - - def testStr(self): - self.assertEqual(list(rrulestr( - "DTSTART:19970902T090000\n" - "RRULE:FREQ=YEARLY;COUNT=3\n" - )), - [datetime(1997, 9, 2, 9, 0), - datetime(1998, 9, 2, 9, 0), - datetime(1999, 9, 2, 9, 0)]) - - def testStrType(self): - self.assertEqual(isinstance(rrulestr( - "DTSTART:19970902T090000\n" - "RRULE:FREQ=YEARLY;COUNT=3\n" - ), rrule), True) - - def testStrForceSetType(self): - self.assertEqual(isinstance(rrulestr( - "DTSTART:19970902T090000\n" - "RRULE:FREQ=YEARLY;COUNT=3\n" - , forceset=True), rruleset), True) - - def testStrSetType(self): - self.assertEqual(isinstance(rrulestr( - "DTSTART:19970902T090000\n" - "RRULE:FREQ=YEARLY;COUNT=2;BYDAY=TU\n" - "RRULE:FREQ=YEARLY;COUNT=1;BYDAY=TH\n" - ), rruleset), True) - - def testStrCase(self): - self.assertEqual(list(rrulestr( - "dtstart:19970902T090000\n" - "rrule:freq=yearly;count=3\n" - )), - [datetime(1997, 9, 2, 9, 0), - datetime(1998, 9, 2, 9, 0), - datetime(1999, 9, 2, 9, 0)]) - - def testStrSpaces(self): - self.assertEqual(list(rrulestr( - " DTSTART:19970902T090000 " - " RRULE:FREQ=YEARLY;COUNT=3 " - )), - [datetime(1997, 9, 2, 9, 0), - datetime(1998, 9, 2, 9, 0), - datetime(1999, 9, 2, 9, 0)]) - - def testStrSpacesAndLines(self): - self.assertEqual(list(rrulestr( - " DTSTART:19970902T090000 \n" - " \n" - " RRULE:FREQ=YEARLY;COUNT=3 \n" - )), - [datetime(1997, 9, 2, 9, 0), - datetime(1998, 9, 2, 9, 0), - datetime(1999, 9, 2, 9, 0)]) - - def testStrNoDTStart(self): - self.assertEqual(list(rrulestr( - "RRULE:FREQ=YEARLY;COUNT=3\n" - , dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0), - datetime(1998, 9, 2, 9, 0), - datetime(1999, 9, 2, 9, 0)]) - - def testStrValueOnly(self): - self.assertEqual(list(rrulestr( - "FREQ=YEARLY;COUNT=3\n" - , dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0), - datetime(1998, 9, 2, 9, 0), - datetime(1999, 9, 2, 9, 0)]) - - def testStrUnfold(self): - self.assertEqual(list(rrulestr( - "FREQ=YEA\n RLY;COUNT=3\n", unfold=True, - dtstart=datetime(1997, 9, 2, 9, 0))), - [datetime(1997, 9, 2, 9, 0), - datetime(1998, 9, 2, 9, 0), - datetime(1999, 9, 2, 9, 0)]) - - def testStrSet(self): - self.assertEqual(list(rrulestr( - "DTSTART:19970902T090000\n" - "RRULE:FREQ=YEARLY;COUNT=2;BYDAY=TU\n" - "RRULE:FREQ=YEARLY;COUNT=1;BYDAY=TH\n" - )), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 4, 9, 0), - datetime(1997, 9, 9, 9, 0)]) - - def testStrSetDate(self): - self.assertEqual(list(rrulestr( - "DTSTART:19970902T090000\n" - "RRULE:FREQ=YEARLY;COUNT=1;BYDAY=TU\n" - "RDATE:19970904T090000\n" - "RDATE:19970909T090000\n" - )), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 4, 9, 0), - datetime(1997, 9, 9, 9, 0)]) - - def testStrSetExRule(self): - self.assertEqual(list(rrulestr( - "DTSTART:19970902T090000\n" - "RRULE:FREQ=YEARLY;COUNT=6;BYDAY=TU,TH\n" - "EXRULE:FREQ=YEARLY;COUNT=3;BYDAY=TH\n" - )), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 9, 9, 0), - datetime(1997, 9, 16, 9, 0)]) - - def testStrSetExDate(self): - self.assertEqual(list(rrulestr( - "DTSTART:19970902T090000\n" - "RRULE:FREQ=YEARLY;COUNT=6;BYDAY=TU,TH\n" - "EXDATE:19970904T090000\n" - "EXDATE:19970911T090000\n" - "EXDATE:19970918T090000\n" - )), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 9, 9, 0), - datetime(1997, 9, 16, 9, 0)]) - - def testStrSetDateAndExDate(self): - self.assertEqual(list(rrulestr( - "DTSTART:19970902T090000\n" - "RDATE:19970902T090000\n" - "RDATE:19970904T090000\n" - "RDATE:19970909T090000\n" - "RDATE:19970911T090000\n" - "RDATE:19970916T090000\n" - "RDATE:19970918T090000\n" - "EXDATE:19970904T090000\n" - "EXDATE:19970911T090000\n" - "EXDATE:19970918T090000\n" - )), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 9, 9, 0), - datetime(1997, 9, 16, 9, 0)]) - - def testStrSetDateAndExRule(self): - self.assertEqual(list(rrulestr( - "DTSTART:19970902T090000\n" - "RDATE:19970902T090000\n" - "RDATE:19970904T090000\n" - "RDATE:19970909T090000\n" - "RDATE:19970911T090000\n" - "RDATE:19970916T090000\n" - "RDATE:19970918T090000\n" - "EXRULE:FREQ=YEARLY;COUNT=3;BYDAY=TH\n" - )), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 9, 9, 0), - datetime(1997, 9, 16, 9, 0)]) - - def testStrKeywords(self): - self.assertEqual(list(rrulestr( - "DTSTART:19970902T090000\n" - "RRULE:FREQ=YEARLY;COUNT=3;INTERVAL=3;" - "BYMONTH=3;BYWEEKDAY=TH;BYMONTHDAY=3;" - "BYHOUR=3;BYMINUTE=3;BYSECOND=3\n" - )), - [datetime(2033, 3, 3, 3, 3, 3), - datetime(2039, 3, 3, 3, 3, 3), - datetime(2072, 3, 3, 3, 3, 3)]) - - def testStrNWeekDay(self): - self.assertEqual(list(rrulestr( - "DTSTART:19970902T090000\n" - "RRULE:FREQ=YEARLY;COUNT=3;BYDAY=1TU,-1TH\n" - )), - [datetime(1997, 12, 25, 9, 0), - datetime(1998, 1, 6, 9, 0), - datetime(1998, 12, 31, 9, 0)]) - - def testStrUntil(self): - self.assertEqual(list(rrulestr( - "DTSTART:19970902T090000\n" - "RRULE:FREQ=YEARLY;" - "UNTIL=19990101T000000;BYDAY=1TU,-1TH\n" - )), - [datetime(1997, 12, 25, 9, 0), - datetime(1998, 1, 6, 9, 0), - datetime(1998, 12, 31, 9, 0)]) - - def testStrInvalidUntil(self): - with self.assertRaises(ValueError): - list(rrulestr("DTSTART:19970902T090000\n" - "RRULE:FREQ=YEARLY;" - "UNTIL=TheCowsComeHome;BYDAY=1TU,-1TH\n")) - - def testStrEmptyByDay(self): - with self.assertRaises(ValueError): - list(rrulestr("DTSTART:19970902T090000\n" - "FREQ=WEEKLY;" - "BYDAY=;" # This part is invalid - "WKST=SU")) - - def testStrInvalidByDay(self): - with self.assertRaises(ValueError): - list(rrulestr("DTSTART:19970902T090000\n" - "FREQ=WEEKLY;" - "BYDAY=-1OK;" # This part is invalid - "WKST=SU")) - - def testBadBySetPos(self): - self.assertRaises(ValueError, - rrule, MONTHLY, - count=1, - bysetpos=0, - dtstart=datetime(1997, 9, 2, 9, 0)) - - def testBadBySetPosMany(self): - self.assertRaises(ValueError, - rrule, MONTHLY, - count=1, - bysetpos=(-1, 0, 1), - dtstart=datetime(1997, 9, 2, 9, 0)) - - # Tests to ensure that str(rrule) works - def testToStrYearly(self): - rule = rrule(YEARLY, count=3, dtstart=datetime(1997, 9, 2, 9, 0)) - self._rrulestr_reverse_test(rule) - - def testToStrYearlyInterval(self): - rule = rrule(YEARLY, count=3, interval=2, - dtstart=datetime(1997, 9, 2, 9, 0)) - self._rrulestr_reverse_test(rule) - - def testToStrYearlyByMonth(self): - rule = rrule(YEARLY, count=3, bymonth=(1, 3), - dtstart=datetime(1997, 9, 2, 9, 0)) - - self._rrulestr_reverse_test(rule) - - def testToStrYearlyByMonth(self): - self._rrulestr_reverse_test(rrule(YEARLY, - count=3, - bymonth=(1, 3), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrYearlyByMonthDay(self): - self._rrulestr_reverse_test(rrule(YEARLY, - count=3, - bymonthday=(1, 3), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrYearlyByMonthAndMonthDay(self): - self._rrulestr_reverse_test(rrule(YEARLY, - count=3, - bymonth=(1, 3), - bymonthday=(5, 7), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrYearlyByWeekDay(self): - self._rrulestr_reverse_test(rrule(YEARLY, - count=3, - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrYearlyByNWeekDay(self): - self._rrulestr_reverse_test(rrule(YEARLY, - count=3, - byweekday=(TU(1), TH(-1)), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrYearlyByNWeekDayLarge(self): - self._rrulestr_reverse_test(rrule(YEARLY, - count=3, - byweekday=(TU(3), TH(-3)), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrYearlyByMonthAndWeekDay(self): - self._rrulestr_reverse_test(rrule(YEARLY, - count=3, - bymonth=(1, 3), - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrYearlyByMonthAndNWeekDay(self): - self._rrulestr_reverse_test(rrule(YEARLY, - count=3, - bymonth=(1, 3), - byweekday=(TU(1), TH(-1)), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrYearlyByMonthAndNWeekDayLarge(self): - # This is interesting because the TH(-3) ends up before - # the TU(3). - self._rrulestr_reverse_test(rrule(YEARLY, - count=3, - bymonth=(1, 3), - byweekday=(TU(3), TH(-3)), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrYearlyByMonthDayAndWeekDay(self): - self._rrulestr_reverse_test(rrule(YEARLY, - count=3, - bymonthday=(1, 3), - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrYearlyByMonthAndMonthDayAndWeekDay(self): - self._rrulestr_reverse_test(rrule(YEARLY, - count=3, - bymonth=(1, 3), - bymonthday=(1, 3), - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrYearlyByYearDay(self): - self._rrulestr_reverse_test(rrule(YEARLY, - count=4, - byyearday=(1, 100, 200, 365), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrYearlyByYearDayNeg(self): - self._rrulestr_reverse_test(rrule(YEARLY, - count=4, - byyearday=(-365, -266, -166, -1), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrYearlyByMonthAndYearDay(self): - self._rrulestr_reverse_test(rrule(YEARLY, - count=4, - bymonth=(4, 7), - byyearday=(1, 100, 200, 365), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrYearlyByMonthAndYearDayNeg(self): - self._rrulestr_reverse_test(rrule(YEARLY, - count=4, - bymonth=(4, 7), - byyearday=(-365, -266, -166, -1), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrYearlyByWeekNo(self): - self._rrulestr_reverse_test(rrule(YEARLY, - count=3, - byweekno=20, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrYearlyByWeekNoAndWeekDay(self): - # That's a nice one. The first days of week number one - # may be in the last year. - self._rrulestr_reverse_test(rrule(YEARLY, - count=3, - byweekno=1, - byweekday=MO, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrYearlyByWeekNoAndWeekDayLarge(self): - # Another nice test. The last days of week number 52/53 - # may be in the next year. - self._rrulestr_reverse_test(rrule(YEARLY, - count=3, - byweekno=52, - byweekday=SU, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrYearlyByWeekNoAndWeekDayLast(self): - self._rrulestr_reverse_test(rrule(YEARLY, - count=3, - byweekno=-1, - byweekday=SU, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrYearlyByEaster(self): - self._rrulestr_reverse_test(rrule(YEARLY, - count=3, - byeaster=0, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrYearlyByEasterPos(self): - self._rrulestr_reverse_test(rrule(YEARLY, - count=3, - byeaster=1, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrYearlyByEasterNeg(self): - self._rrulestr_reverse_test(rrule(YEARLY, - count=3, - byeaster=-1, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrYearlyByWeekNoAndWeekDay53(self): - self._rrulestr_reverse_test(rrule(YEARLY, - count=3, - byweekno=53, - byweekday=MO, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrYearlyByHour(self): - self._rrulestr_reverse_test(rrule(YEARLY, - count=3, - byhour=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrYearlyByMinute(self): - self._rrulestr_reverse_test(rrule(YEARLY, - count=3, - byminute=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrYearlyBySecond(self): - self._rrulestr_reverse_test(rrule(YEARLY, - count=3, - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrYearlyByHourAndMinute(self): - self._rrulestr_reverse_test(rrule(YEARLY, - count=3, - byhour=(6, 18), - byminute=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrYearlyByHourAndSecond(self): - self._rrulestr_reverse_test(rrule(YEARLY, - count=3, - byhour=(6, 18), - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrYearlyByMinuteAndSecond(self): - self._rrulestr_reverse_test(rrule(YEARLY, - count=3, - byminute=(6, 18), - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrYearlyByHourAndMinuteAndSecond(self): - self._rrulestr_reverse_test(rrule(YEARLY, - count=3, - byhour=(6, 18), - byminute=(6, 18), - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrYearlyBySetPos(self): - self._rrulestr_reverse_test(rrule(YEARLY, - count=3, - bymonthday=15, - byhour=(6, 18), - bysetpos=(3, -3), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMonthly(self): - self._rrulestr_reverse_test(rrule(MONTHLY, - count=3, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMonthlyInterval(self): - self._rrulestr_reverse_test(rrule(MONTHLY, - count=3, - interval=2, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMonthlyIntervalLarge(self): - self._rrulestr_reverse_test(rrule(MONTHLY, - count=3, - interval=18, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMonthlyByMonth(self): - self._rrulestr_reverse_test(rrule(MONTHLY, - count=3, - bymonth=(1, 3), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMonthlyByMonthDay(self): - self._rrulestr_reverse_test(rrule(MONTHLY, - count=3, - bymonthday=(1, 3), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMonthlyByMonthAndMonthDay(self): - self._rrulestr_reverse_test(rrule(MONTHLY, - count=3, - bymonth=(1, 3), - bymonthday=(5, 7), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMonthlyByWeekDay(self): - self._rrulestr_reverse_test(rrule(MONTHLY, - count=3, - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))) - - # Third Monday of the month - self.assertEqual(rrule(MONTHLY, - byweekday=(MO(+3)), - dtstart=datetime(1997, 9, 1)).between(datetime(1997, - 9, - 1), - datetime(1997, - 12, - 1)), - [datetime(1997, 9, 15, 0, 0), - datetime(1997, 10, 20, 0, 0), - datetime(1997, 11, 17, 0, 0)]) - - def testToStrMonthlyByNWeekDay(self): - self._rrulestr_reverse_test(rrule(MONTHLY, - count=3, - byweekday=(TU(1), TH(-1)), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMonthlyByNWeekDayLarge(self): - self._rrulestr_reverse_test(rrule(MONTHLY, - count=3, - byweekday=(TU(3), TH(-3)), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMonthlyByMonthAndWeekDay(self): - self._rrulestr_reverse_test(rrule(MONTHLY, - count=3, - bymonth=(1, 3), - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMonthlyByMonthAndNWeekDay(self): - self._rrulestr_reverse_test(rrule(MONTHLY, - count=3, - bymonth=(1, 3), - byweekday=(TU(1), TH(-1)), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMonthlyByMonthAndNWeekDayLarge(self): - self._rrulestr_reverse_test(rrule(MONTHLY, - count=3, - bymonth=(1, 3), - byweekday=(TU(3), TH(-3)), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMonthlyByMonthDayAndWeekDay(self): - self._rrulestr_reverse_test(rrule(MONTHLY, - count=3, - bymonthday=(1, 3), - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMonthlyByMonthAndMonthDayAndWeekDay(self): - self._rrulestr_reverse_test(rrule(MONTHLY, - count=3, - bymonth=(1, 3), - bymonthday=(1, 3), - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMonthlyByYearDay(self): - self._rrulestr_reverse_test(rrule(MONTHLY, - count=4, - byyearday=(1, 100, 200, 365), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMonthlyByYearDayNeg(self): - self._rrulestr_reverse_test(rrule(MONTHLY, - count=4, - byyearday=(-365, -266, -166, -1), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMonthlyByMonthAndYearDay(self): - self._rrulestr_reverse_test(rrule(MONTHLY, - count=4, - bymonth=(4, 7), - byyearday=(1, 100, 200, 365), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMonthlyByMonthAndYearDayNeg(self): - self._rrulestr_reverse_test(rrule(MONTHLY, - count=4, - bymonth=(4, 7), - byyearday=(-365, -266, -166, -1), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMonthlyByWeekNo(self): - self._rrulestr_reverse_test(rrule(MONTHLY, - count=3, - byweekno=20, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMonthlyByWeekNoAndWeekDay(self): - # That's a nice one. The first days of week number one - # may be in the last year. - self._rrulestr_reverse_test(rrule(MONTHLY, - count=3, - byweekno=1, - byweekday=MO, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMonthlyByWeekNoAndWeekDayLarge(self): - # Another nice test. The last days of week number 52/53 - # may be in the next year. - self._rrulestr_reverse_test(rrule(MONTHLY, - count=3, - byweekno=52, - byweekday=SU, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMonthlyByWeekNoAndWeekDayLast(self): - self._rrulestr_reverse_test(rrule(MONTHLY, - count=3, - byweekno=-1, - byweekday=SU, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMonthlyByWeekNoAndWeekDay53(self): - self._rrulestr_reverse_test(rrule(MONTHLY, - count=3, - byweekno=53, - byweekday=MO, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMonthlyByEaster(self): - self._rrulestr_reverse_test(rrule(MONTHLY, - count=3, - byeaster=0, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMonthlyByEasterPos(self): - self._rrulestr_reverse_test(rrule(MONTHLY, - count=3, - byeaster=1, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMonthlyByEasterNeg(self): - self._rrulestr_reverse_test(rrule(MONTHLY, - count=3, - byeaster=-1, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMonthlyByHour(self): - self._rrulestr_reverse_test(rrule(MONTHLY, - count=3, - byhour=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMonthlyByMinute(self): - self._rrulestr_reverse_test(rrule(MONTHLY, - count=3, - byminute=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMonthlyBySecond(self): - self._rrulestr_reverse_test(rrule(MONTHLY, - count=3, - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMonthlyByHourAndMinute(self): - self._rrulestr_reverse_test(rrule(MONTHLY, - count=3, - byhour=(6, 18), - byminute=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMonthlyByHourAndSecond(self): - self._rrulestr_reverse_test(rrule(MONTHLY, - count=3, - byhour=(6, 18), - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMonthlyByMinuteAndSecond(self): - self._rrulestr_reverse_test(rrule(MONTHLY, - count=3, - byminute=(6, 18), - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMonthlyByHourAndMinuteAndSecond(self): - self._rrulestr_reverse_test(rrule(MONTHLY, - count=3, - byhour=(6, 18), - byminute=(6, 18), - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMonthlyBySetPos(self): - self._rrulestr_reverse_test(rrule(MONTHLY, - count=3, - bymonthday=(13, 17), - byhour=(6, 18), - bysetpos=(3, -3), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrWeekly(self): - self._rrulestr_reverse_test(rrule(WEEKLY, - count=3, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrWeeklyInterval(self): - self._rrulestr_reverse_test(rrule(WEEKLY, - count=3, - interval=2, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrWeeklyIntervalLarge(self): - self._rrulestr_reverse_test(rrule(WEEKLY, - count=3, - interval=20, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrWeeklyByMonth(self): - self._rrulestr_reverse_test(rrule(WEEKLY, - count=3, - bymonth=(1, 3), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrWeeklyByMonthDay(self): - self._rrulestr_reverse_test(rrule(WEEKLY, - count=3, - bymonthday=(1, 3), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrWeeklyByMonthAndMonthDay(self): - self._rrulestr_reverse_test(rrule(WEEKLY, - count=3, - bymonth=(1, 3), - bymonthday=(5, 7), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrWeeklyByWeekDay(self): - self._rrulestr_reverse_test(rrule(WEEKLY, - count=3, - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrWeeklyByNWeekDay(self): - self._rrulestr_reverse_test(rrule(WEEKLY, - count=3, - byweekday=(TU(1), TH(-1)), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrWeeklyByMonthAndWeekDay(self): - # This test is interesting, because it crosses the year - # boundary in a weekly period to find day '1' as a - # valid recurrence. - self._rrulestr_reverse_test(rrule(WEEKLY, - count=3, - bymonth=(1, 3), - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrWeeklyByMonthAndNWeekDay(self): - self._rrulestr_reverse_test(rrule(WEEKLY, - count=3, - bymonth=(1, 3), - byweekday=(TU(1), TH(-1)), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrWeeklyByMonthDayAndWeekDay(self): - self._rrulestr_reverse_test(rrule(WEEKLY, - count=3, - bymonthday=(1, 3), - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrWeeklyByMonthAndMonthDayAndWeekDay(self): - self._rrulestr_reverse_test(rrule(WEEKLY, - count=3, - bymonth=(1, 3), - bymonthday=(1, 3), - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrWeeklyByYearDay(self): - self._rrulestr_reverse_test(rrule(WEEKLY, - count=4, - byyearday=(1, 100, 200, 365), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrWeeklyByYearDayNeg(self): - self._rrulestr_reverse_test(rrule(WEEKLY, - count=4, - byyearday=(-365, -266, -166, -1), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrWeeklyByMonthAndYearDay(self): - self._rrulestr_reverse_test(rrule(WEEKLY, - count=4, - bymonth=(1, 7), - byyearday=(1, 100, 200, 365), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrWeeklyByMonthAndYearDayNeg(self): - self._rrulestr_reverse_test(rrule(WEEKLY, - count=4, - bymonth=(1, 7), - byyearday=(-365, -266, -166, -1), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrWeeklyByWeekNo(self): - self._rrulestr_reverse_test(rrule(WEEKLY, - count=3, - byweekno=20, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrWeeklyByWeekNoAndWeekDay(self): - # That's a nice one. The first days of week number one - # may be in the last year. - self._rrulestr_reverse_test(rrule(WEEKLY, - count=3, - byweekno=1, - byweekday=MO, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrWeeklyByWeekNoAndWeekDayLarge(self): - # Another nice test. The last days of week number 52/53 - # may be in the next year. - self._rrulestr_reverse_test(rrule(WEEKLY, - count=3, - byweekno=52, - byweekday=SU, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrWeeklyByWeekNoAndWeekDayLast(self): - self._rrulestr_reverse_test(rrule(WEEKLY, - count=3, - byweekno=-1, - byweekday=SU, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrWeeklyByWeekNoAndWeekDay53(self): - self._rrulestr_reverse_test(rrule(WEEKLY, - count=3, - byweekno=53, - byweekday=MO, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrWeeklyByEaster(self): - self._rrulestr_reverse_test(rrule(WEEKLY, - count=3, - byeaster=0, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrWeeklyByEasterPos(self): - self._rrulestr_reverse_test(rrule(WEEKLY, - count=3, - byeaster=1, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrWeeklyByEasterNeg(self): - self._rrulestr_reverse_test(rrule(WEEKLY, - count=3, - byeaster=-1, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrWeeklyByHour(self): - self._rrulestr_reverse_test(rrule(WEEKLY, - count=3, - byhour=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrWeeklyByMinute(self): - self._rrulestr_reverse_test(rrule(WEEKLY, - count=3, - byminute=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrWeeklyBySecond(self): - self._rrulestr_reverse_test(rrule(WEEKLY, - count=3, - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrWeeklyByHourAndMinute(self): - self._rrulestr_reverse_test(rrule(WEEKLY, - count=3, - byhour=(6, 18), - byminute=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrWeeklyByHourAndSecond(self): - self._rrulestr_reverse_test(rrule(WEEKLY, - count=3, - byhour=(6, 18), - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrWeeklyByMinuteAndSecond(self): - self._rrulestr_reverse_test(rrule(WEEKLY, - count=3, - byminute=(6, 18), - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrWeeklyByHourAndMinuteAndSecond(self): - self._rrulestr_reverse_test(rrule(WEEKLY, - count=3, - byhour=(6, 18), - byminute=(6, 18), - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrWeeklyBySetPos(self): - self._rrulestr_reverse_test(rrule(WEEKLY, - count=3, - byweekday=(TU, TH), - byhour=(6, 18), - bysetpos=(3, -3), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrDaily(self): - self._rrulestr_reverse_test(rrule(DAILY, - count=3, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrDailyInterval(self): - self._rrulestr_reverse_test(rrule(DAILY, - count=3, - interval=2, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrDailyIntervalLarge(self): - self._rrulestr_reverse_test(rrule(DAILY, - count=3, - interval=92, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrDailyByMonth(self): - self._rrulestr_reverse_test(rrule(DAILY, - count=3, - bymonth=(1, 3), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrDailyByMonthDay(self): - self._rrulestr_reverse_test(rrule(DAILY, - count=3, - bymonthday=(1, 3), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrDailyByMonthAndMonthDay(self): - self._rrulestr_reverse_test(rrule(DAILY, - count=3, - bymonth=(1, 3), - bymonthday=(5, 7), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrDailyByWeekDay(self): - self._rrulestr_reverse_test(rrule(DAILY, - count=3, - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrDailyByNWeekDay(self): - self._rrulestr_reverse_test(rrule(DAILY, - count=3, - byweekday=(TU(1), TH(-1)), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrDailyByMonthAndWeekDay(self): - self._rrulestr_reverse_test(rrule(DAILY, - count=3, - bymonth=(1, 3), - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrDailyByMonthAndNWeekDay(self): - self._rrulestr_reverse_test(rrule(DAILY, - count=3, - bymonth=(1, 3), - byweekday=(TU(1), TH(-1)), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrDailyByMonthDayAndWeekDay(self): - self._rrulestr_reverse_test(rrule(DAILY, - count=3, - bymonthday=(1, 3), - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrDailyByMonthAndMonthDayAndWeekDay(self): - self._rrulestr_reverse_test(rrule(DAILY, - count=3, - bymonth=(1, 3), - bymonthday=(1, 3), - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrDailyByYearDay(self): - self._rrulestr_reverse_test(rrule(DAILY, - count=4, - byyearday=(1, 100, 200, 365), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrDailyByYearDayNeg(self): - self._rrulestr_reverse_test(rrule(DAILY, - count=4, - byyearday=(-365, -266, -166, -1), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrDailyByMonthAndYearDay(self): - self._rrulestr_reverse_test(rrule(DAILY, - count=4, - bymonth=(1, 7), - byyearday=(1, 100, 200, 365), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrDailyByMonthAndYearDayNeg(self): - self._rrulestr_reverse_test(rrule(DAILY, - count=4, - bymonth=(1, 7), - byyearday=(-365, -266, -166, -1), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrDailyByWeekNo(self): - self._rrulestr_reverse_test(rrule(DAILY, - count=3, - byweekno=20, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrDailyByWeekNoAndWeekDay(self): - # That's a nice one. The first days of week number one - # may be in the last year. - self._rrulestr_reverse_test(rrule(DAILY, - count=3, - byweekno=1, - byweekday=MO, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrDailyByWeekNoAndWeekDayLarge(self): - # Another nice test. The last days of week number 52/53 - # may be in the next year. - self._rrulestr_reverse_test(rrule(DAILY, - count=3, - byweekno=52, - byweekday=SU, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrDailyByWeekNoAndWeekDayLast(self): - self._rrulestr_reverse_test(rrule(DAILY, - count=3, - byweekno=-1, - byweekday=SU, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrDailyByWeekNoAndWeekDay53(self): - self._rrulestr_reverse_test(rrule(DAILY, - count=3, - byweekno=53, - byweekday=MO, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrDailyByEaster(self): - self._rrulestr_reverse_test(rrule(DAILY, - count=3, - byeaster=0, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrDailyByEasterPos(self): - self._rrulestr_reverse_test(rrule(DAILY, - count=3, - byeaster=1, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrDailyByEasterNeg(self): - self._rrulestr_reverse_test(rrule(DAILY, - count=3, - byeaster=-1, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrDailyByHour(self): - self._rrulestr_reverse_test(rrule(DAILY, - count=3, - byhour=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrDailyByMinute(self): - self._rrulestr_reverse_test(rrule(DAILY, - count=3, - byminute=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrDailyBySecond(self): - self._rrulestr_reverse_test(rrule(DAILY, - count=3, - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrDailyByHourAndMinute(self): - self._rrulestr_reverse_test(rrule(DAILY, - count=3, - byhour=(6, 18), - byminute=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrDailyByHourAndSecond(self): - self._rrulestr_reverse_test(rrule(DAILY, - count=3, - byhour=(6, 18), - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrDailyByMinuteAndSecond(self): - self._rrulestr_reverse_test(rrule(DAILY, - count=3, - byminute=(6, 18), - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrDailyByHourAndMinuteAndSecond(self): - self._rrulestr_reverse_test(rrule(DAILY, - count=3, - byhour=(6, 18), - byminute=(6, 18), - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrDailyBySetPos(self): - self._rrulestr_reverse_test(rrule(DAILY, - count=3, - byhour=(6, 18), - byminute=(15, 45), - bysetpos=(3, -3), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrHourly(self): - self._rrulestr_reverse_test(rrule(HOURLY, - count=3, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrHourlyInterval(self): - self._rrulestr_reverse_test(rrule(HOURLY, - count=3, - interval=2, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrHourlyIntervalLarge(self): - self._rrulestr_reverse_test(rrule(HOURLY, - count=3, - interval=769, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrHourlyByMonth(self): - self._rrulestr_reverse_test(rrule(HOURLY, - count=3, - bymonth=(1, 3), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrHourlyByMonthDay(self): - self._rrulestr_reverse_test(rrule(HOURLY, - count=3, - bymonthday=(1, 3), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrHourlyByMonthAndMonthDay(self): - self._rrulestr_reverse_test(rrule(HOURLY, - count=3, - bymonth=(1, 3), - bymonthday=(5, 7), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrHourlyByWeekDay(self): - self._rrulestr_reverse_test(rrule(HOURLY, - count=3, - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrHourlyByNWeekDay(self): - self._rrulestr_reverse_test(rrule(HOURLY, - count=3, - byweekday=(TU(1), TH(-1)), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrHourlyByMonthAndWeekDay(self): - self._rrulestr_reverse_test(rrule(HOURLY, - count=3, - bymonth=(1, 3), - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrHourlyByMonthAndNWeekDay(self): - self._rrulestr_reverse_test(rrule(HOURLY, - count=3, - bymonth=(1, 3), - byweekday=(TU(1), TH(-1)), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrHourlyByMonthDayAndWeekDay(self): - self._rrulestr_reverse_test(rrule(HOURLY, - count=3, - bymonthday=(1, 3), - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrHourlyByMonthAndMonthDayAndWeekDay(self): - self._rrulestr_reverse_test(rrule(HOURLY, - count=3, - bymonth=(1, 3), - bymonthday=(1, 3), - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrHourlyByYearDay(self): - self._rrulestr_reverse_test(rrule(HOURLY, - count=4, - byyearday=(1, 100, 200, 365), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrHourlyByYearDayNeg(self): - self._rrulestr_reverse_test(rrule(HOURLY, - count=4, - byyearday=(-365, -266, -166, -1), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrHourlyByMonthAndYearDay(self): - self._rrulestr_reverse_test(rrule(HOURLY, - count=4, - bymonth=(4, 7), - byyearday=(1, 100, 200, 365), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrHourlyByMonthAndYearDayNeg(self): - self._rrulestr_reverse_test(rrule(HOURLY, - count=4, - bymonth=(4, 7), - byyearday=(-365, -266, -166, -1), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrHourlyByWeekNo(self): - self._rrulestr_reverse_test(rrule(HOURLY, - count=3, - byweekno=20, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrHourlyByWeekNoAndWeekDay(self): - self._rrulestr_reverse_test(rrule(HOURLY, - count=3, - byweekno=1, - byweekday=MO, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrHourlyByWeekNoAndWeekDayLarge(self): - self._rrulestr_reverse_test(rrule(HOURLY, - count=3, - byweekno=52, - byweekday=SU, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrHourlyByWeekNoAndWeekDayLast(self): - self._rrulestr_reverse_test(rrule(HOURLY, - count=3, - byweekno=-1, - byweekday=SU, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrHourlyByWeekNoAndWeekDay53(self): - self._rrulestr_reverse_test(rrule(HOURLY, - count=3, - byweekno=53, - byweekday=MO, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrHourlyByEaster(self): - self._rrulestr_reverse_test(rrule(HOURLY, - count=3, - byeaster=0, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrHourlyByEasterPos(self): - self._rrulestr_reverse_test(rrule(HOURLY, - count=3, - byeaster=1, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrHourlyByEasterNeg(self): - self._rrulestr_reverse_test(rrule(HOURLY, - count=3, - byeaster=-1, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrHourlyByHour(self): - self._rrulestr_reverse_test(rrule(HOURLY, - count=3, - byhour=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrHourlyByMinute(self): - self._rrulestr_reverse_test(rrule(HOURLY, - count=3, - byminute=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrHourlyBySecond(self): - self._rrulestr_reverse_test(rrule(HOURLY, - count=3, - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrHourlyByHourAndMinute(self): - self._rrulestr_reverse_test(rrule(HOURLY, - count=3, - byhour=(6, 18), - byminute=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrHourlyByHourAndSecond(self): - self._rrulestr_reverse_test(rrule(HOURLY, - count=3, - byhour=(6, 18), - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrHourlyByMinuteAndSecond(self): - self._rrulestr_reverse_test(rrule(HOURLY, - count=3, - byminute=(6, 18), - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrHourlyByHourAndMinuteAndSecond(self): - self._rrulestr_reverse_test(rrule(HOURLY, - count=3, - byhour=(6, 18), - byminute=(6, 18), - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrHourlyBySetPos(self): - self._rrulestr_reverse_test(rrule(HOURLY, - count=3, - byminute=(15, 45), - bysecond=(15, 45), - bysetpos=(3, -3), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMinutely(self): - self._rrulestr_reverse_test(rrule(MINUTELY, - count=3, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMinutelyInterval(self): - self._rrulestr_reverse_test(rrule(MINUTELY, - count=3, - interval=2, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMinutelyIntervalLarge(self): - self._rrulestr_reverse_test(rrule(MINUTELY, - count=3, - interval=1501, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMinutelyByMonth(self): - self._rrulestr_reverse_test(rrule(MINUTELY, - count=3, - bymonth=(1, 3), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMinutelyByMonthDay(self): - self._rrulestr_reverse_test(rrule(MINUTELY, - count=3, - bymonthday=(1, 3), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMinutelyByMonthAndMonthDay(self): - self._rrulestr_reverse_test(rrule(MINUTELY, - count=3, - bymonth=(1, 3), - bymonthday=(5, 7), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMinutelyByWeekDay(self): - self._rrulestr_reverse_test(rrule(MINUTELY, - count=3, - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMinutelyByNWeekDay(self): - self._rrulestr_reverse_test(rrule(MINUTELY, - count=3, - byweekday=(TU(1), TH(-1)), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMinutelyByMonthAndWeekDay(self): - self._rrulestr_reverse_test(rrule(MINUTELY, - count=3, - bymonth=(1, 3), - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMinutelyByMonthAndNWeekDay(self): - self._rrulestr_reverse_test(rrule(MINUTELY, - count=3, - bymonth=(1, 3), - byweekday=(TU(1), TH(-1)), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMinutelyByMonthDayAndWeekDay(self): - self._rrulestr_reverse_test(rrule(MINUTELY, - count=3, - bymonthday=(1, 3), - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMinutelyByMonthAndMonthDayAndWeekDay(self): - self._rrulestr_reverse_test(rrule(MINUTELY, - count=3, - bymonth=(1, 3), - bymonthday=(1, 3), - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMinutelyByYearDay(self): - self._rrulestr_reverse_test(rrule(MINUTELY, - count=4, - byyearday=(1, 100, 200, 365), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMinutelyByYearDayNeg(self): - self._rrulestr_reverse_test(rrule(MINUTELY, - count=4, - byyearday=(-365, -266, -166, -1), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMinutelyByMonthAndYearDay(self): - self._rrulestr_reverse_test(rrule(MINUTELY, - count=4, - bymonth=(4, 7), - byyearday=(1, 100, 200, 365), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMinutelyByMonthAndYearDayNeg(self): - self._rrulestr_reverse_test(rrule(MINUTELY, - count=4, - bymonth=(4, 7), - byyearday=(-365, -266, -166, -1), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMinutelyByWeekNo(self): - self._rrulestr_reverse_test(rrule(MINUTELY, - count=3, - byweekno=20, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMinutelyByWeekNoAndWeekDay(self): - self._rrulestr_reverse_test(rrule(MINUTELY, - count=3, - byweekno=1, - byweekday=MO, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMinutelyByWeekNoAndWeekDayLarge(self): - self._rrulestr_reverse_test(rrule(MINUTELY, - count=3, - byweekno=52, - byweekday=SU, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMinutelyByWeekNoAndWeekDayLast(self): - self._rrulestr_reverse_test(rrule(MINUTELY, - count=3, - byweekno=-1, - byweekday=SU, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMinutelyByWeekNoAndWeekDay53(self): - self._rrulestr_reverse_test(rrule(MINUTELY, - count=3, - byweekno=53, - byweekday=MO, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMinutelyByEaster(self): - self._rrulestr_reverse_test(rrule(MINUTELY, - count=3, - byeaster=0, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMinutelyByEasterPos(self): - self._rrulestr_reverse_test(rrule(MINUTELY, - count=3, - byeaster=1, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMinutelyByEasterNeg(self): - self._rrulestr_reverse_test(rrule(MINUTELY, - count=3, - byeaster=-1, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMinutelyByHour(self): - self._rrulestr_reverse_test(rrule(MINUTELY, - count=3, - byhour=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMinutelyByMinute(self): - self._rrulestr_reverse_test(rrule(MINUTELY, - count=3, - byminute=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMinutelyBySecond(self): - self._rrulestr_reverse_test(rrule(MINUTELY, - count=3, - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMinutelyByHourAndMinute(self): - self._rrulestr_reverse_test(rrule(MINUTELY, - count=3, - byhour=(6, 18), - byminute=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMinutelyByHourAndSecond(self): - self._rrulestr_reverse_test(rrule(MINUTELY, - count=3, - byhour=(6, 18), - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMinutelyByMinuteAndSecond(self): - self._rrulestr_reverse_test(rrule(MINUTELY, - count=3, - byminute=(6, 18), - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMinutelyByHourAndMinuteAndSecond(self): - self._rrulestr_reverse_test(rrule(MINUTELY, - count=3, - byhour=(6, 18), - byminute=(6, 18), - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrMinutelyBySetPos(self): - self._rrulestr_reverse_test(rrule(MINUTELY, - count=3, - bysecond=(15, 30, 45), - bysetpos=(3, -3), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrSecondly(self): - self._rrulestr_reverse_test(rrule(SECONDLY, - count=3, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrSecondlyInterval(self): - self._rrulestr_reverse_test(rrule(SECONDLY, - count=3, - interval=2, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrSecondlyIntervalLarge(self): - self._rrulestr_reverse_test(rrule(SECONDLY, - count=3, - interval=90061, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrSecondlyByMonth(self): - self._rrulestr_reverse_test(rrule(SECONDLY, - count=3, - bymonth=(1, 3), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrSecondlyByMonthDay(self): - self._rrulestr_reverse_test(rrule(SECONDLY, - count=3, - bymonthday=(1, 3), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrSecondlyByMonthAndMonthDay(self): - self._rrulestr_reverse_test(rrule(SECONDLY, - count=3, - bymonth=(1, 3), - bymonthday=(5, 7), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrSecondlyByWeekDay(self): - self._rrulestr_reverse_test(rrule(SECONDLY, - count=3, - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrSecondlyByNWeekDay(self): - self._rrulestr_reverse_test(rrule(SECONDLY, - count=3, - byweekday=(TU(1), TH(-1)), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrSecondlyByMonthAndWeekDay(self): - self._rrulestr_reverse_test(rrule(SECONDLY, - count=3, - bymonth=(1, 3), - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrSecondlyByMonthAndNWeekDay(self): - self._rrulestr_reverse_test(rrule(SECONDLY, - count=3, - bymonth=(1, 3), - byweekday=(TU(1), TH(-1)), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrSecondlyByMonthDayAndWeekDay(self): - self._rrulestr_reverse_test(rrule(SECONDLY, - count=3, - bymonthday=(1, 3), - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrSecondlyByMonthAndMonthDayAndWeekDay(self): - self._rrulestr_reverse_test(rrule(SECONDLY, - count=3, - bymonth=(1, 3), - bymonthday=(1, 3), - byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrSecondlyByYearDay(self): - self._rrulestr_reverse_test(rrule(SECONDLY, - count=4, - byyearday=(1, 100, 200, 365), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrSecondlyByYearDayNeg(self): - self._rrulestr_reverse_test(rrule(SECONDLY, - count=4, - byyearday=(-365, -266, -166, -1), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrSecondlyByMonthAndYearDay(self): - self._rrulestr_reverse_test(rrule(SECONDLY, - count=4, - bymonth=(4, 7), - byyearday=(1, 100, 200, 365), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrSecondlyByMonthAndYearDayNeg(self): - self._rrulestr_reverse_test(rrule(SECONDLY, - count=4, - bymonth=(4, 7), - byyearday=(-365, -266, -166, -1), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrSecondlyByWeekNo(self): - self._rrulestr_reverse_test(rrule(SECONDLY, - count=3, - byweekno=20, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrSecondlyByWeekNoAndWeekDay(self): - self._rrulestr_reverse_test(rrule(SECONDLY, - count=3, - byweekno=1, - byweekday=MO, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrSecondlyByWeekNoAndWeekDayLarge(self): - self._rrulestr_reverse_test(rrule(SECONDLY, - count=3, - byweekno=52, - byweekday=SU, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrSecondlyByWeekNoAndWeekDayLast(self): - self._rrulestr_reverse_test(rrule(SECONDLY, - count=3, - byweekno=-1, - byweekday=SU, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrSecondlyByWeekNoAndWeekDay53(self): - self._rrulestr_reverse_test(rrule(SECONDLY, - count=3, - byweekno=53, - byweekday=MO, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrSecondlyByEaster(self): - self._rrulestr_reverse_test(rrule(SECONDLY, - count=3, - byeaster=0, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrSecondlyByEasterPos(self): - self._rrulestr_reverse_test(rrule(SECONDLY, - count=3, - byeaster=1, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrSecondlyByEasterNeg(self): - self._rrulestr_reverse_test(rrule(SECONDLY, - count=3, - byeaster=-1, - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrSecondlyByHour(self): - self._rrulestr_reverse_test(rrule(SECONDLY, - count=3, - byhour=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrSecondlyByMinute(self): - self._rrulestr_reverse_test(rrule(SECONDLY, - count=3, - byminute=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrSecondlyBySecond(self): - self._rrulestr_reverse_test(rrule(SECONDLY, - count=3, - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrSecondlyByHourAndMinute(self): - self._rrulestr_reverse_test(rrule(SECONDLY, - count=3, - byhour=(6, 18), - byminute=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrSecondlyByHourAndSecond(self): - self._rrulestr_reverse_test(rrule(SECONDLY, - count=3, - byhour=(6, 18), - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrSecondlyByMinuteAndSecond(self): - self._rrulestr_reverse_test(rrule(SECONDLY, - count=3, - byminute=(6, 18), - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrSecondlyByHourAndMinuteAndSecond(self): - self._rrulestr_reverse_test(rrule(SECONDLY, - count=3, - byhour=(6, 18), - byminute=(6, 18), - bysecond=(6, 18), - dtstart=datetime(1997, 9, 2, 9, 0))) - - def testToStrSecondlyByHourAndMinuteAndSecondBug(self): - # This explores a bug found by Mathieu Bridon. - self._rrulestr_reverse_test(rrule(SECONDLY, - count=3, - bysecond=(0,), - byminute=(1,), - dtstart=datetime(2010, 3, 22, 12, 1))) - - def testToStrLongIntegers(self): - if not PY3: # There is no longs in python3 - self._rrulestr_reverse_test(rrule(MINUTELY, - count=long(2), - interval=long(2), - bymonth=long(2), - byweekday=long(3), - byhour=long(6), - byminute=long(6), - bysecond=long(6), - dtstart=datetime(1997, 9, 2, 9, 0))) - - self._rrulestr_reverse_test(rrule(YEARLY, - count=long(2), - bymonthday=long(5), - byweekno=long(2), - dtstart=datetime(1997, 9, 2, 9, 0))) - - -class RRuleSetTest(unittest.TestCase): - def testSet(self): - rrset = rruleset() - rrset.rrule(rrule(YEARLY, count=2, byweekday=TU, - dtstart=datetime(1997, 9, 2, 9, 0))) - rrset.rrule(rrule(YEARLY, count=1, byweekday=TH, - dtstart=datetime(1997, 9, 2, 9, 0))) - self.assertEqual(list(rrset), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 4, 9, 0), - datetime(1997, 9, 9, 9, 0)]) - - def testSetDate(self): - rrset = rruleset() - rrset.rrule(rrule(YEARLY, count=1, byweekday=TU, - dtstart=datetime(1997, 9, 2, 9, 0))) - rrset.rdate(datetime(1997, 9, 4, 9)) - rrset.rdate(datetime(1997, 9, 9, 9)) - self.assertEqual(list(rrset), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 4, 9, 0), - datetime(1997, 9, 9, 9, 0)]) - - def testSetExRule(self): - rrset = rruleset() - rrset.rrule(rrule(YEARLY, count=6, byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))) - rrset.exrule(rrule(YEARLY, count=3, byweekday=TH, - dtstart=datetime(1997, 9, 2, 9, 0))) - self.assertEqual(list(rrset), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 9, 9, 0), - datetime(1997, 9, 16, 9, 0)]) - - def testSetExDate(self): - rrset = rruleset() - rrset.rrule(rrule(YEARLY, count=6, byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))) - rrset.exdate(datetime(1997, 9, 4, 9)) - rrset.exdate(datetime(1997, 9, 11, 9)) - rrset.exdate(datetime(1997, 9, 18, 9)) - self.assertEqual(list(rrset), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 9, 9, 0), - datetime(1997, 9, 16, 9, 0)]) - - def testSetExDateRevOrder(self): - rrset = rruleset() - rrset.rrule(rrule(MONTHLY, count=5, bymonthday=10, - dtstart=datetime(2004, 1, 1, 9, 0))) - rrset.exdate(datetime(2004, 4, 10, 9, 0)) - rrset.exdate(datetime(2004, 2, 10, 9, 0)) - self.assertEqual(list(rrset), - [datetime(2004, 1, 10, 9, 0), - datetime(2004, 3, 10, 9, 0), - datetime(2004, 5, 10, 9, 0)]) - - def testSetDateAndExDate(self): - rrset = rruleset() - rrset.rdate(datetime(1997, 9, 2, 9)) - rrset.rdate(datetime(1997, 9, 4, 9)) - rrset.rdate(datetime(1997, 9, 9, 9)) - rrset.rdate(datetime(1997, 9, 11, 9)) - rrset.rdate(datetime(1997, 9, 16, 9)) - rrset.rdate(datetime(1997, 9, 18, 9)) - rrset.exdate(datetime(1997, 9, 4, 9)) - rrset.exdate(datetime(1997, 9, 11, 9)) - rrset.exdate(datetime(1997, 9, 18, 9)) - self.assertEqual(list(rrset), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 9, 9, 0), - datetime(1997, 9, 16, 9, 0)]) - - def testSetDateAndExRule(self): - rrset = rruleset() - rrset.rdate(datetime(1997, 9, 2, 9)) - rrset.rdate(datetime(1997, 9, 4, 9)) - rrset.rdate(datetime(1997, 9, 9, 9)) - rrset.rdate(datetime(1997, 9, 11, 9)) - rrset.rdate(datetime(1997, 9, 16, 9)) - rrset.rdate(datetime(1997, 9, 18, 9)) - rrset.exrule(rrule(YEARLY, count=3, byweekday=TH, - dtstart=datetime(1997, 9, 2, 9, 0))) - self.assertEqual(list(rrset), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 9, 9, 0), - datetime(1997, 9, 16, 9, 0)]) - - def testSetCount(self): - rrset = rruleset() - rrset.rrule(rrule(YEARLY, count=6, byweekday=(TU, TH), - dtstart=datetime(1997, 9, 2, 9, 0))) - rrset.exrule(rrule(YEARLY, count=3, byweekday=TH, - dtstart=datetime(1997, 9, 2, 9, 0))) - self.assertEqual(rrset.count(), 3) - - def testSetCachePre(self): - rrset = rruleset() - rrset.rrule(rrule(YEARLY, count=2, byweekday=TU, - dtstart=datetime(1997, 9, 2, 9, 0))) - rrset.rrule(rrule(YEARLY, count=1, byweekday=TH, - dtstart=datetime(1997, 9, 2, 9, 0))) - self.assertEqual(list(rrset), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 4, 9, 0), - datetime(1997, 9, 9, 9, 0)]) - - def testSetCachePost(self): - rrset = rruleset(cache=True) - rrset.rrule(rrule(YEARLY, count=2, byweekday=TU, - dtstart=datetime(1997, 9, 2, 9, 0))) - rrset.rrule(rrule(YEARLY, count=1, byweekday=TH, - dtstart=datetime(1997, 9, 2, 9, 0))) - for x in rrset: pass - self.assertEqual(list(rrset), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 4, 9, 0), - datetime(1997, 9, 9, 9, 0)]) - - def testSetCachePostInternal(self): - rrset = rruleset(cache=True) - rrset.rrule(rrule(YEARLY, count=2, byweekday=TU, - dtstart=datetime(1997, 9, 2, 9, 0))) - rrset.rrule(rrule(YEARLY, count=1, byweekday=TH, - dtstart=datetime(1997, 9, 2, 9, 0))) - for x in rrset: pass - self.assertEqual(list(rrset._cache), - [datetime(1997, 9, 2, 9, 0), - datetime(1997, 9, 4, 9, 0), - datetime(1997, 9, 9, 9, 0)]) - - def testSetRRuleCount(self): - # Test that the count is updated when an rrule is added - rrset = rruleset(cache=False) - for cache in (True, False): - rrset = rruleset(cache=cache) - rrset.rrule(rrule(YEARLY, count=2, byweekday=TH, - dtstart=datetime(1983, 4, 1))) - rrset.rrule(rrule(WEEKLY, count=4, byweekday=FR, - dtstart=datetime(1991, 6, 3))) - - # Check the length twice - first one sets a cache, second reads it - self.assertEqual(rrset.count(), 6) - self.assertEqual(rrset.count(), 6) - - # This should invalidate the cache and force an update - rrset.rrule(rrule(MONTHLY, count=3, dtstart=datetime(1994, 1, 3))) - - self.assertEqual(rrset.count(), 9) - self.assertEqual(rrset.count(), 9) - - def testSetRDateCount(self): - # Test that the count is updated when an rdate is added - rrset = rruleset(cache=False) - for cache in (True, False): - rrset = rruleset(cache=cache) - rrset.rrule(rrule(YEARLY, count=2, byweekday=TH, - dtstart=datetime(1983, 4, 1))) - rrset.rrule(rrule(WEEKLY, count=4, byweekday=FR, - dtstart=datetime(1991, 6, 3))) - - # Check the length twice - first one sets a cache, second reads it - self.assertEqual(rrset.count(), 6) - self.assertEqual(rrset.count(), 6) - - # This should invalidate the cache and force an update - rrset.rdate(datetime(1993, 2, 14)) - - self.assertEqual(rrset.count(), 7) - self.assertEqual(rrset.count(), 7) - - def testSetExRuleCount(self): - # Test that the count is updated when an exrule is added - rrset = rruleset(cache=False) - for cache in (True, False): - rrset = rruleset(cache=cache) - rrset.rrule(rrule(YEARLY, count=2, byweekday=TH, - dtstart=datetime(1983, 4, 1))) - rrset.rrule(rrule(WEEKLY, count=4, byweekday=FR, - dtstart=datetime(1991, 6, 3))) - - # Check the length twice - first one sets a cache, second reads it - self.assertEqual(rrset.count(), 6) - self.assertEqual(rrset.count(), 6) - - # This should invalidate the cache and force an update - rrset.exrule(rrule(WEEKLY, count=2, interval=2, - dtstart=datetime(1991, 6, 14))) - - self.assertEqual(rrset.count(), 4) - self.assertEqual(rrset.count(), 4) - - def testSetExDateCount(self): - # Test that the count is updated when an rdate is added - for cache in (True, False): - rrset = rruleset(cache=cache) - rrset.rrule(rrule(YEARLY, count=2, byweekday=TH, - dtstart=datetime(1983, 4, 1))) - rrset.rrule(rrule(WEEKLY, count=4, byweekday=FR, - dtstart=datetime(1991, 6, 3))) - - # Check the length twice - first one sets a cache, second reads it - self.assertEqual(rrset.count(), 6) - self.assertEqual(rrset.count(), 6) - - # This should invalidate the cache and force an update - rrset.exdate(datetime(1991, 6, 28)) - - self.assertEqual(rrset.count(), 5) - self.assertEqual(rrset.count(), 5) - - -class WeekdayTest(unittest.TestCase): - def testInvalidNthWeekday(self): - with self.assertRaises(ValueError): - zeroth_friday = FR(0) - - def testWeekdayCallable(self): - # Calling a weekday instance generates a new weekday instance with the - # value of n changed. - from dateutil.rrule import weekday - self.assertEqual(MO(1), weekday(0, 1)) - - # Calling a weekday instance with the identical n returns the original - # object - FR_3 = weekday(4, 3) - self.assertIs(FR_3(3), FR_3) - - def testWeekdayEquality(self): - # Two weekday objects are not equal if they have different values for n - self.assertNotEqual(TH, TH(-1)) - self.assertNotEqual(SA(3), SA(2)) - - def testWeekdayEqualitySubclass(self): - # Two weekday objects equal if their "weekday" and "n" attributes are - # available and the same - class BasicWeekday(object): - def __init__(self, weekday): - self.weekday = weekday - - class BasicNWeekday(BasicWeekday): - def __init__(self, weekday, n=None): - super(BasicNWeekday, self).__init__(weekday) - self.n = n - - MO_Basic = BasicWeekday(0) - - self.assertNotEqual(MO, MO_Basic) - self.assertNotEqual(MO(1), MO_Basic) - - TU_BasicN = BasicNWeekday(1) - - self.assertEqual(TU, TU_BasicN) - self.assertNotEqual(TU(3), TU_BasicN) - - WE_Basic3 = BasicNWeekday(2, 3) - self.assertEqual(WE(3), WE_Basic3) - self.assertNotEqual(WE(2), WE_Basic3) - - def testWeekdayReprNoN(self): - no_n_reprs = ('MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU') - no_n_wdays = (MO, TU, WE, TH, FR, SA, SU) - - for repstr, wday in zip(no_n_reprs, no_n_wdays): - self.assertEqual(repr(wday), repstr) - - def testWeekdayReprWithN(self): - with_n_reprs = ('WE(+1)', 'TH(-2)', 'SU(+3)') - with_n_wdays = (WE(1), TH(-2), SU(+3)) - - for repstr, wday in zip(with_n_reprs, with_n_wdays): - self.assertEqual(repr(wday), repstr) - diff --git a/lib/dateutil/test/test_tz.py b/lib/dateutil/test/test_tz.py deleted file mode 100644 index 7a7ea78ed..000000000 --- a/lib/dateutil/test/test_tz.py +++ /dev/null @@ -1,525 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals -from ._common import unittest, TZWinContext - -from datetime import datetime, timedelta -from datetime import time as dt_time -from six import BytesIO, StringIO - -import os -import subprocess -import sys -import time as _time -import base64 -IS_WIN = sys.platform.startswith('win') - -# dateutil imports -from dateutil.relativedelta import relativedelta -from dateutil.parser import parse -from dateutil import tz as tz -from dateutil import zoneinfo - -try: - from dateutil import tzwin -except ImportError as e: - if IS_WIN: - raise e - else: - pass - -MISSING_TARBALL = ("This test fails if you don't have the dateutil " - "timezone file installed. Please read the README") - -TZFILE_EST5EDT = b""" -VFppZgAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAAAAAAADrAAAABAAAABCeph5wn7rrYKCGAHCh -ms1gomXicKOD6eCkaq5wpTWnYKZTyvCnFYlgqDOs8Kj+peCqE47wqt6H4KvzcPCsvmngrdNS8K6e -S+CvszTwsH4t4LGcUXCyZ0pgs3wzcLRHLGC1XBVwticOYLc793C4BvBguRvZcLnm0mC7BPXwu8a0 -YLzk1/C9r9DgvsS58L+PsuDApJvwwW+U4MKEffDDT3bgxGRf8MUvWODGTXxwxw864MgtXnDI+Fdg -yg1AcMrYOWDLiPBw0iP0cNJg++DTdeTw1EDd4NVVxvDWIL/g1zWo8NgAoeDZFYrw2eCD4Nr+p3Db -wGXg3N6JcN2pgmDevmtw34lkYOCeTXDhaUZg4n4vcONJKGDkXhFw5Vcu4OZHLfDnNxDg6CcP8OkW -8uDqBvHw6vbU4Ovm0/Ds1rbg7ca18O6/02Dvr9Jw8J+1YPGPtHDyf5dg82+WcPRfeWD1T3hw9j9b -YPcvWnD4KHfg+Q88cPoIWeD6+Fjw++g74PzYOvD9yB3g/rgc8P+n/+AAl/7wAYfh4AJ34PADcP5g -BGD9cAVQ4GAGQN9wBzDCYAeNGXAJEKRgCa2U8ArwhmAL4IVwDNmi4A3AZ3AOuYTgD6mD8BCZZuAR -iWXwEnlI4BNpR/AUWSrgFUkp8BY5DOAXKQvwGCIpYBkI7fAaAgtgGvIKcBvh7WAc0exwHcHPYB6x -znAfobFgIHYA8CGBk2AiVeLwI2qv4CQ1xPAlSpHgJhWm8Ccqc+An/sNwKQpV4CnepXAq6jfgK76H -cCzTVGAtnmlwLrM2YC9+S3AwkxhgMWdn8DJy+mAzR0nwNFLcYDUnK/A2Mr5gNwcN8Dgb2uA45u/w -Ofu84DrG0fA7257gPK/ucD27gOA+j9BwP5ti4EBvsnBBhH9gQk+UcENkYWBEL3ZwRURDYEYPWHBH -JCVgR/h08EkEB2BJ2FbwSuPpYEu4OPBMzQXgTZga8E6s5+BPd/zwUIzJ4FFhGXBSbKvgU0D7cFRM -jeBVIN1wVixv4FcAv3BYFYxgWOChcFn1bmBawINwW9VQYFypn/BdtTJgXomB8F+VFGBgaWPwYX4w -4GJJRfBjXhLgZCkn8GU99OBmEkRwZx3W4GfyJnBo/bjgadIIcGrdmuBrsepwbMa3YG2RzHBupplg -b3GucHCGe2BxWsrwcmZdYHM6rPB0Rj9gdRqO8HYvW+B2+nDweA894HjaUvB57x/gero08HvPAeB8 -o1Fwfa7j4H6DM3B/jsXgAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAB -AAEAAQABAgMBAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAB -AAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEA -AQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAB -AAEAAQABAAEAAQABAAEAAQABAAEAAf//x8ABAP//ubAABP//x8ABCP//x8ABDEVEVABFU1QARVdU -AEVQVAAAAAABAAAAAQ== -""" - -EUROPE_HELSINKI = b""" -VFppZgAAAAAAAAAAAAAAAAAAAAAAAAAFAAAABQAAAAAAAAB1AAAABQAAAA2kc28Yy85RYMy/hdAV -I+uQFhPckBcDzZAX876QGOOvkBnToJAaw5GQG7y9EBysrhAdnJ8QHoyQEB98gRAgbHIQIVxjECJM -VBAjPEUQJCw2ECUcJxAmDBgQJwVDkCf1NJAo5SWQKdUWkCrFB5ArtPiQLKTpkC2U2pAuhMuQL3S8 -kDBkrZAxXdkQMnK0EDM9uxA0UpYQNR2dEDYyeBA2/X8QOBuUkDjdYRA5+3aQOr1DEDvbWJA8pl+Q -Pbs6kD6GQZA/mxyQQGYjkEGEORBCRgWQQ2QbEEQl55BFQ/0QRgXJkEcj3xBH7uYQSQPBEEnOyBBK -46MQS66qEEzMv5BNjowQTqyhkE9ubhBQjIOQUVeKkFJsZZBTN2yQVExHkFUXTpBWLCmQVvcwkFgV -RhBY1xKQWfUoEFq29JBb1QoQXKAREF207BBef/MQX5TOEGBf1RBhfeqQYj+3EGNdzJBkH5kQZT2u -kGYItZBnHZCQZ+iXkGj9cpBpyHmQat1UkGuoW5BsxnEQbYg9kG6mUxBvaB+QcIY1EHFRPBByZhcQ -czEeEHRF+RB1EQAQdi8VkHbw4hB4DveQeNDEEHnu2ZB6sKYQe867kHyZwpB9rp2QfnmkkH+Of5AC -AQIDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQD -BAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAMEAwQDBAME -AwQAABdoAAAAACowAQQAABwgAAkAACowAQQAABwgAAlITVQARUVTVABFRVQAAAAAAQEAAAABAQ== -""" - -NEW_YORK = b""" -VFppZgAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAAAABcAAADrAAAABAAAABCeph5wn7rrYKCGAHCh -ms1gomXicKOD6eCkaq5wpTWnYKZTyvCnFYlgqDOs8Kj+peCqE47wqt6H4KvzcPCsvmngrdNS8K6e -S+CvszTwsH4t4LGcUXCyZ0pgs3wzcLRHLGC1XBVwticOYLc793C4BvBguRvZcLnm0mC7BPXwu8a0 -YLzk1/C9r9DgvsS58L+PsuDApJvwwW+U4MKEffDDT3bgxGRf8MUvWODGTXxwxw864MgtXnDI+Fdg -yg1AcMrYOWDLiPBw0iP0cNJg++DTdeTw1EDd4NVVxvDWIL/g1zWo8NgAoeDZFYrw2eCD4Nr+p3Db -wGXg3N6JcN2pgmDevmtw34lkYOCeTXDhaUZg4n4vcONJKGDkXhFw5Vcu4OZHLfDnNxDg6CcP8OkW -8uDqBvHw6vbU4Ovm0/Ds1rbg7ca18O6/02Dvr9Jw8J+1YPGPtHDyf5dg82+WcPRfeWD1T3hw9j9b -YPcvWnD4KHfg+Q88cPoIWeD6+Fjw++g74PzYOvD9yB3g/rgc8P+n/+AAl/7wAYfh4AJ34PADcP5g -BGD9cAVQ4GEGQN9yBzDCYgeNGXMJEKRjCa2U9ArwhmQL4IV1DNmi5Q3AZ3YOuYTmD6mD9xCZZucR -iWX4EnlI6BNpR/kUWSrpFUkp+RY5DOoXKQv6GCIpaxkI7fsaAgtsGvIKfBvh7Wwc0ex8HcHPbR6x -zn0fobFtIHYA/SGBk20iVeL+I2qv7iQ1xP4lSpHuJhWm/ycqc+8n/sOAKQpV8CnepYAq6jfxK76H -gSzTVHItnmmCLrM2cy9+S4MwkxhzMWdoBDJy+nQzR0oENFLcdTUnLAU2Mr51NwcOBjgb2vY45vAG -Ofu89jrG0gY72572PK/uhj27gPY+j9CGP5ti9kBvsoZBhH92Qk+UhkNkYXZEL3aHRURDd0XzqQdH -LV/3R9OLB0kNQfdJs20HSu0j90uciYdM1kB3TXxrh062IndPXE2HUJYEd1E8L4dSdeZ3UxwRh1RV -yHdU+/OHVjWqd1blEAdYHsb3WMTyB1n+qPdapNQHW96K91yEtgddvmz3XmSYB1+eTvdgTbSHYYdr -d2ItlodjZ013ZA14h2VHL3dl7VqHZycRd2fNPIdpBvN3aa0eh2rm1XdrljsHbM/x9212HQdur9P3 -b1X/B3CPtfdxNeEHcm+X93MVwwd0T3n3dP7fh3Y4lnd23sGHeBh4d3i+o4d5+Fp3ep6Fh3vYPHd8 -fmeHfbged35eSYd/mAB3AAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAB -AAEAAQABAgMBAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAB -AAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEA -AQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAB -AAEAAQABAAEAAQABAAEAAQABAAEAAf//x8ABAP//ubAABP//x8ABCP//x8ABDEVEVABFU1QARVdU -AEVQVAAEslgAAAAAAQWk7AEAAAACB4YfggAAAAMJZ1MDAAAABAtIhoQAAAAFDSsLhQAAAAYPDD8G -AAAABxDtcocAAAAIEs6mCAAAAAkVn8qJAAAACheA/goAAAALGWIxiwAAAAwdJeoMAAAADSHa5Q0A -AAAOJZ6djgAAAA8nf9EPAAAAECpQ9ZAAAAARLDIpEQAAABIuE1ySAAAAEzDnJBMAAAAUM7hIlAAA -ABU2jBAVAAAAFkO3G5YAAAAXAAAAAQAAAAE= -""" - -TZICAL_EST5EDT = """ -BEGIN:VTIMEZONE -TZID:US-Eastern -LAST-MODIFIED:19870101T000000Z -TZURL:http://zones.stds_r_us.net/tz/US-Eastern -BEGIN:STANDARD -DTSTART:19671029T020000 -RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 -TZOFFSETFROM:-0400 -TZOFFSETTO:-0500 -TZNAME:EST -END:STANDARD -BEGIN:DAYLIGHT -DTSTART:19870405T020000 -RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 -TZOFFSETFROM:-0500 -TZOFFSETTO:-0400 -TZNAME:EDT -END:DAYLIGHT -END:VTIMEZONE -""" - - -class TZTest(unittest.TestCase): - def testStrStart1(self): - self.assertEqual(datetime(2003, 4, 6, 1, 59, - tzinfo=tz.tzstr("EST5EDT")).tzname(), "EST") - self.assertEqual(datetime(2003, 4, 6, 2, 00, - tzinfo=tz.tzstr("EST5EDT")).tzname(), "EDT") - - def testStrEnd1(self): - self.assertEqual(datetime(2003, 10, 26, 0, 59, - tzinfo=tz.tzstr("EST5EDT")).tzname(), "EDT") - self.assertEqual(datetime(2003, 10, 26, 1, 00, - tzinfo=tz.tzstr("EST5EDT")).tzname(), "EST") - - def testStrStart2(self): - s = "EST5EDT,4,0,6,7200,10,0,26,7200,3600" - self.assertEqual(datetime(2003, 4, 6, 1, 59, - tzinfo=tz.tzstr(s)).tzname(), "EST") - self.assertEqual(datetime(2003, 4, 6, 2, 00, - tzinfo=tz.tzstr(s)).tzname(), "EDT") - - def testStrEnd2(self): - s = "EST5EDT,4,0,6,7200,10,0,26,7200,3600" - self.assertEqual(datetime(2003, 10, 26, 0, 59, - tzinfo=tz.tzstr(s)).tzname(), "EDT") - self.assertEqual(datetime(2003, 10, 26, 1, 00, - tzinfo=tz.tzstr(s)).tzname(), "EST") - - def testStrStart3(self): - s = "EST5EDT,4,1,0,7200,10,-1,0,7200,3600" - self.assertEqual(datetime(2003, 4, 6, 1, 59, - tzinfo=tz.tzstr(s)).tzname(), "EST") - self.assertEqual(datetime(2003, 4, 6, 2, 00, - tzinfo=tz.tzstr(s)).tzname(), "EDT") - - def testStrEnd3(self): - s = "EST5EDT,4,1,0,7200,10,-1,0,7200,3600" - self.assertEqual(datetime(2003, 10, 26, 0, 59, - tzinfo=tz.tzstr(s)).tzname(), "EDT") - self.assertEqual(datetime(2003, 10, 26, 1, 00, - tzinfo=tz.tzstr(s)).tzname(), "EST") - - def testStrStart4(self): - s = "EST5EDT4,M4.1.0/02:00:00,M10-5-0/02:00" - self.assertEqual(datetime(2003, 4, 6, 1, 59, - tzinfo=tz.tzstr(s)).tzname(), "EST") - self.assertEqual(datetime(2003, 4, 6, 2, 00, - tzinfo=tz.tzstr(s)).tzname(), "EDT") - - def testStrEnd4(self): - s = "EST5EDT4,M4.1.0/02:00:00,M10-5-0/02:00" - self.assertEqual(datetime(2003, 10, 26, 0, 59, - tzinfo=tz.tzstr(s)).tzname(), "EDT") - self.assertEqual(datetime(2003, 10, 26, 1, 00, - tzinfo=tz.tzstr(s)).tzname(), "EST") - - def testStrStart5(self): - s = "EST5EDT4,95/02:00:00,298/02:00" - self.assertEqual(datetime(2003, 4, 6, 1, 59, - tzinfo=tz.tzstr(s)).tzname(), "EST") - self.assertEqual(datetime(2003, 4, 6, 2, 00, - tzinfo=tz.tzstr(s)).tzname(), "EDT") - - def testStrEnd5(self): - s = "EST5EDT4,95/02:00:00,298/02" - self.assertEqual(datetime(2003, 10, 26, 0, 59, - tzinfo=tz.tzstr(s)).tzname(), "EDT") - self.assertEqual(datetime(2003, 10, 26, 1, 00, - tzinfo=tz.tzstr(s)).tzname(), "EST") - - def testStrStart6(self): - s = "EST5EDT4,J96/02:00:00,J299/02:00" - self.assertEqual(datetime(2003, 4, 6, 1, 59, - tzinfo=tz.tzstr(s)).tzname(), "EST") - self.assertEqual(datetime(2003, 4, 6, 2, 00, - tzinfo=tz.tzstr(s)).tzname(), "EDT") - - def testStrEnd6(self): - s = "EST5EDT4,J96/02:00:00,J299/02" - self.assertEqual(datetime(2003, 10, 26, 0, 59, - tzinfo=tz.tzstr(s)).tzname(), "EDT") - self.assertEqual(datetime(2003, 10, 26, 1, 00, - tzinfo=tz.tzstr(s)).tzname(), "EST") - - def testStrStr(self): - # Test that tz.tzstr() won't throw an error if given a str instead - # of a unicode literal. - self.assertEqual(datetime(2003, 4, 6, 1, 59, - tzinfo=tz.tzstr(str("EST5EDT"))).tzname(), "EST") - self.assertEqual(datetime(2003, 4, 6, 2, 00, - tzinfo=tz.tzstr(str("EST5EDT"))).tzname(), "EDT") - - def testStrCmp1(self): - self.assertEqual(tz.tzstr("EST5EDT"), - tz.tzstr("EST5EDT4,M4.1.0/02:00:00,M10-5-0/02:00")) - - def testStrCmp2(self): - self.assertEqual(tz.tzstr("EST5EDT"), - tz.tzstr("EST5EDT,4,1,0,7200,10,-1,0,7200,3600")) - - def testRangeCmp1(self): - from dateutil.relativedelta import SU - self.assertEqual(tz.tzstr("EST5EDT"), - tz.tzrange("EST", -18000, "EDT", -14400, - relativedelta(hours=+2, - month=4, day=1, - weekday=SU(+1)), - relativedelta(hours=+1, - month=10, day=31, - weekday=SU(-1)))) - - def testRangeCmp2(self): - self.assertEqual(tz.tzstr("EST5EDT"), - tz.tzrange("EST", -18000, "EDT")) - - def testFileStart1(self): - tzc = tz.tzfile(BytesIO(base64.b64decode(TZFILE_EST5EDT))) - self.assertEqual(datetime(2003, 4, 6, 1, 59, tzinfo=tzc).tzname(), "EST") - self.assertEqual(datetime(2003, 4, 6, 2, 00, tzinfo=tzc).tzname(), "EDT") - - def testFileEnd1(self): - tzc = tz.tzfile(BytesIO(base64.b64decode(TZFILE_EST5EDT))) - self.assertEqual(datetime(2003, 10, 26, 0, 59, tzinfo=tzc).tzname(), - "EDT") - self.assertEqual(datetime(2003, 10, 26, 1, 00, tzinfo=tzc).tzname(), - "EST") - - def testZoneInfoFileStart1(self): - tz = zoneinfo.gettz("EST5EDT") - self.assertEqual(datetime(2003, 4, 6, 1, 59, tzinfo=tz).tzname(), "EST", - MISSING_TARBALL) - self.assertEqual(datetime(2003, 4, 6, 2, 00, tzinfo=tz).tzname(), "EDT") - - def testZoneInfoFileEnd1(self): - tzc = zoneinfo.gettz("EST5EDT") - self.assertEqual(datetime(2003, 10, 26, 0, 59, tzinfo=tzc).tzname(), - "EDT", MISSING_TARBALL) - self.assertEqual(datetime(2003, 10, 26, 1, 00, tzinfo=tzc).tzname(), - "EST") - - def testZoneInfoOffsetSignal(self): - utc = zoneinfo.gettz("UTC") - nyc = zoneinfo.gettz("America/New_York") - self.assertNotEqual(utc, None, MISSING_TARBALL) - self.assertNotEqual(nyc, None) - t0 = datetime(2007, 11, 4, 0, 30, tzinfo=nyc) - t1 = t0.astimezone(utc) - t2 = t1.astimezone(nyc) - self.assertEqual(t0, t2) - self.assertEqual(nyc.dst(t0), timedelta(hours=1)) - - def testTzNameNone(self): - gmt5 = tz.tzoffset(None, -18000) # -5:00 - self.assertIs(datetime(2003, 10, 26, 0, 0, tzinfo=gmt5).tzname(), - None) - - def testICalStart1(self): - tzc = tz.tzical(StringIO(TZICAL_EST5EDT)).get() - self.assertEqual(datetime(2003, 4, 6, 1, 59, tzinfo=tzc).tzname(), "EST") - self.assertEqual(datetime(2003, 4, 6, 2, 00, tzinfo=tzc).tzname(), "EDT") - - def testICalEnd1(self): - tzc = tz.tzical(StringIO(TZICAL_EST5EDT)).get() - self.assertEqual(datetime(2003, 10, 26, 0, 59, tzinfo=tzc).tzname(), "EDT") - self.assertEqual(datetime(2003, 10, 26, 1, 00, tzinfo=tzc).tzname(), "EST") - - def testRoundNonFullMinutes(self): - # This timezone has an offset of 5992 seconds in 1900-01-01. - tzc = tz.tzfile(BytesIO(base64.b64decode(EUROPE_HELSINKI))) - self.assertEqual(str(datetime(1900, 1, 1, 0, 0, tzinfo=tzc)), - "1900-01-01 00:00:00+01:40") - - def testLeapCountDecodesProperly(self): - # This timezone has leapcnt, and failed to decode until - # Eugene Oden notified about the issue. - tzc = tz.tzfile(BytesIO(base64.b64decode(NEW_YORK))) - self.assertEqual(datetime(2007, 3, 31, 20, 12).tzname(), None) # What is the point of this? - - def testGettz(self): - # bug 892569 - str(tz.gettz('UTC')) - - def testGetTzEquality(self): - self.assertEqual(tz.gettz('UTC'), tz.gettz('UTC')) - - def testBrokenIsDstHandling(self): - # tzrange._isdst() was using a date() rather than a datetime(). - # Issue reported by Lennart Regebro. - dt = datetime(2007, 8, 6, 4, 10, tzinfo=tz.tzutc()) - self.assertEqual(dt.astimezone(tz=tz.gettz("GMT+2")), - datetime(2007, 8, 6, 6, 10, tzinfo=tz.tzstr("GMT+2"))) - - def testGMTHasNoDaylight(self): - # tz.tzstr("GMT+2") improperly considered daylight saving time. - # Issue reported by Lennart Regebro. - dt = datetime(2007, 8, 6, 4, 10) - self.assertEqual(tz.gettz("GMT+2").dst(dt), timedelta(0)) - - def testGMTOffset(self): - # GMT and UTC offsets have inverted signal when compared to the - # usual TZ variable handling. - dt = datetime(2007, 8, 6, 4, 10, tzinfo=tz.tzutc()) - self.assertEqual(dt.astimezone(tz=tz.tzstr("GMT+2")), - datetime(2007, 8, 6, 6, 10, tzinfo=tz.tzstr("GMT+2"))) - self.assertEqual(dt.astimezone(tz=tz.gettz("UTC-2")), - datetime(2007, 8, 6, 2, 10, tzinfo=tz.tzstr("UTC-2"))) - - def testTimeOnlyUTC(self): - # https://github.com/dateutil/dateutil/issues/132 - # tzutc doesn't care - tz_utc = tz.tzutc() - self.assertEqual(dt_time(13, 20, tzinfo=tz_utc).utcoffset(), - timedelta(0)) - - def testTimeOnlyOffset(self): - # tzoffset doesn't care - tz_offset = tz.tzoffset('+3', 3600) - self.assertEqual(dt_time(13, 20, tzinfo=tz_offset).utcoffset(), - timedelta(seconds=3600)) - - def testTimeOnlyLocal(self): - # tzlocal returns None - tz_local = tz.tzlocal() - self.assertIs(dt_time(13, 20, tzinfo=tz_local).utcoffset(), None) - - def testTimeOnlyRange(self): - # tzrange returns None - tz_range = tz.tzrange('dflt') - self.assertIs(dt_time(13, 20, tzinfo=tz_range).utcoffset(), None) - - def testTimeOnlyGettz(self): - # gettz returns None - tz_get = tz.gettz('Europe/Minsk') - self.assertIs(dt_time(13, 20, tzinfo=tz_get).utcoffset(), None) - - @unittest.skipIf(IS_WIN, "requires Unix") - def testTZSetDoesntCorrupt(self): - # if we start in non-UTC then tzset UTC make sure parse doesn't get - # confused - os.environ['TZ'] = 'UTC' - _time.tzset() - # this should parse to UTC timezone not the original timezone - dt = parse('2014-07-20T12:34:56+00:00') - self.assertEqual(str(dt), '2014-07-20 12:34:56+00:00') - -@unittest.skipUnless(IS_WIN, "Requires Windows") -class TzWinTest(unittest.TestCase): - def testTzResLoadName(self): - # This may not work right on non-US locales. - tzr = tzwin.tzres() - self.assertEqual(tzr.load_name(112), "Eastern Standard Time") - - def testTzResNameFromString(self): - tzr = tzwin.tzres() - self.assertEqual(tzr.name_from_string('@tzres.dll,-221'), - 'Alaskan Daylight Time') - - self.assertEqual(tzr.name_from_string('Samoa Daylight Time'), - 'Samoa Daylight Time') - - with self.assertRaises(ValueError): - tzr.name_from_string('@tzres.dll,100') - - def testIsdstZoneWithNoDaylightSaving(self): - tz = tzwin.tzwin("UTC") - dt = parse("2013-03-06 19:08:15") - self.assertFalse(tz._isdst(dt)) - - def testOffset(self): - tz = tzwin.tzwin("Cape Verde Standard Time") - self.assertEqual(tz.utcoffset(datetime(1995, 5, 21, 12, 9, 13)), - timedelta(-1, 82800)) - - def testLocal(self): - # Not sure how to pin a local time zone, so for now we're just going - # to run this and make sure it doesn't raise an error - # See Github Issue #135: https://github.com/dateutil/dateutil/issues/135 - datetime.now(tzwin.tzwinlocal()) - - datetime(2014, 3, 11, tzinfo=tzwin.tzwinlocal()).utcoffset() - - def testTzwinName(self): - # https://github.com/dateutil/dateutil/issues/143 - tw = tz.tzwin('Eastern Standard Time') - - # Cover the transitions for at least two years. - ESTs = 'Eastern Standard Time' - EDTs = 'Eastern Daylight Time' - transition_dates = [(datetime(2015, 3, 8, 0, 59), ESTs), - (datetime(2015, 3, 8, 2, 1), EDTs), - (datetime(2015, 11, 1, 1, 59), EDTs), - (datetime(2015, 11, 1, 3, 1), ESTs), - (datetime(2016, 3, 13, 0, 59), ESTs), - (datetime(2016, 3, 13, 2, 1), EDTs), - (datetime(2016, 11, 6, 1, 59), EDTs), - (datetime(2016, 11, 6, 3, 1), ESTs)] - - for t_date, expected in transition_dates: - self.assertEqual(t_date.replace(tzinfo=tw).tzname(), expected) - - def testTzwinRepr(self): - tw = tz.tzwin('Yakutsk Standard Time') - self.assertEqual(repr(tw), 'tzwin(' + - repr('Yakutsk Standard Time') + ')') - - def testTzWinEquality(self): - # https://github.com/dateutil/dateutil/issues/151 - tzwin_names = ('Eastern Standard Time', - 'West Pacific Standard Time', - 'Yakutsk Standard Time', - 'Iran Standard Time', - 'UTC') - - for tzwin_name in tzwin_names: - # Get two different instances to compare - tw1 = tz.tzwin(tzwin_name) - tw2 = tz.tzwin(tzwin_name) - - self.assertEqual(tw1, tw2) - - def testTzWinInequality(self): - # https://github.com/dateutil/dateutil/issues/151 - # Note these last two currently differ only in their name. - tzwin_names = (('Eastern Standard Time', 'Yakutsk Standard Time'), - ('Greenwich Standard Time', 'GMT Standard Time'), - ('GMT Standard Time', 'UTC'), - ('E. South America Standard Time', - 'Argentina Standard Time')) - - for tzwn1, tzwn2 in tzwin_names: - # Get two different instances to compare - tw1 = tz.tzwin(tzwn1) - tw2 = tz.tzwin(tzwn2) - - self.assertNotEqual(tw1, tw2) - - @unittest.skipUnless(TZWinContext.tz_change_allowed(), - 'Skipping unless tz changes are allowed.') - def testTzwinLocalName(self): - # https://github.com/dateutil/dateutil/issues/143 - ESTs = 'Eastern Standard Time' - EDTs = 'Eastern Daylight Time' - transition_dates = [(datetime(2015, 3, 8, 0, 59), ESTs), - (datetime(2015, 3, 8, 2, 1), EDTs), - (datetime(2015, 11, 1, 1, 59), EDTs), - (datetime(2015, 11, 1, 3, 1), ESTs), - (datetime(2016, 3, 13, 0, 59), ESTs), - (datetime(2016, 3, 13, 2, 1), EDTs), - (datetime(2016, 11, 6, 1, 59), EDTs), - (datetime(2016, 11, 6, 3, 1), ESTs)] - - with TZWinContext('Eastern Standard Time'): - tw = tz.tzwinlocal() - - for t_date, expected in transition_dates: - self.assertEqual(t_date.replace(tzinfo=tw).tzname(), expected) - - def testTzWinLocalRepr(self): - tw = tz.tzwinlocal() - self.assertEqual(repr(tw), 'tzwinlocal()') - - @unittest.skipUnless(TZWinContext.tz_change_allowed(), - 'Skipping unless tz changes are allowed.') - def testTzwinLocalRepr(self): - # https://github.com/dateutil/dateutil/issues/143 - with TZWinContext('Eastern Standard Time'): - tw = tz.tzwinlocal() - - self.assertEqual(str(tw), 'tzwinlocal(' + - repr('Eastern Standard Time') + ')') - - with TZWinContext('Pacific Standard Time'): - tw = tz.tzwinlocal() - - self.assertEqual(str(tw), 'tzwinlocal(' + - repr('Pacific Standard Time') + ')') - - @unittest.skipUnless(TZWinContext.tz_change_allowed(), - 'Skipping unless tz changes are allowed.') - def testTzwinLocalEquality(self): - tw_est = tz.tzwin('Eastern Standard Time') - tw_pst = tz.tzwin('Pacific Standard Time') - - with TZWinContext('Eastern Standard Time'): - twl1 = tz.tzwinlocal() - twl2 = tz.tzwinlocal() - - self.assertEqual(twl1, twl2) - self.assertEqual(twl1, tw_est) - self.assertNotEqual(twl1, tw_pst) - - with TZWinContext('Pacific Standard Time'): - twl1 = tz.tzwinlocal() - twl2 = tz.tzwinlocal() - tw = tz.tzwin('Pacific Standard Time') - - self.assertEqual(twl1, twl2) - self.assertEqual(twl1, tw) - self.assertEqual(twl1, tw_pst) - self.assertNotEqual(twl1, tw_est) - diff --git a/lib/dateutil/tz/_common.py b/lib/dateutil/tz/_common.py index bbce0fb99..212e8ce95 100644 --- a/lib/dateutil/tz/_common.py +++ b/lib/dateutil/tz/_common.py @@ -1,6 +1,13 @@ from six import PY3 +from six.moves import _thread + +from datetime import datetime, timedelta, tzinfo +import copy + +ZERO = timedelta(0) + +__all__ = ['tzname_in_python2', 'enfold'] -__all__ = ['tzname_in_python2'] def tzname_in_python2(namefunc): """Change unicode output into bytestrings in Python 2 @@ -15,4 +22,359 @@ def tzname_in_python2(namefunc): return name - return adjust_encoding \ No newline at end of file + return adjust_encoding + + +# The following is adapted from Alexander Belopolsky's tz library +# https://github.com/abalkin/tz +if hasattr(datetime, 'fold'): + # This is the pre-python 3.6 fold situation + def enfold(dt, fold=1): + """ + Provides a unified interface for assigning the ``fold`` attribute to + datetimes both before and after the implementation of PEP-495. + + :param fold: + The value for the ``fold`` attribute in the returned datetime. This + should be either 0 or 1. + + :return: + Returns an object for which ``getattr(dt, 'fold', 0)`` returns + ``fold`` for all versions of Python. In versions prior to + Python 3.6, this is a ``_DatetimeWithFold`` object, which is a + subclass of :py:class:`datetime.datetime` with the ``fold`` + attribute added, if ``fold`` is 1. + + ..versionadded:: 2.6.0 + """ + return dt.replace(fold=fold) + +else: + class _DatetimeWithFold(datetime): + """ + This is a class designed to provide a PEP 495-compliant interface for + Python versions before 3.6. It is used only for dates in a fold, so + the ``fold`` attribute is fixed at ``1``. + + ..versionadded:: 2.6.0 + """ + __slots__ = () + + @property + def fold(self): + return 1 + + def enfold(dt, fold=1): + """ + Provides a unified interface for assigning the ``fold`` attribute to + datetimes both before and after the implementation of PEP-495. + + :param fold: + The value for the ``fold`` attribute in the returned datetime. This + should be either 0 or 1. + + :return: + Returns an object for which ``getattr(dt, 'fold', 0)`` returns + ``fold`` for all versions of Python. In versions prior to + Python 3.6, this is a ``_DatetimeWithFold`` object, which is a + subclass of :py:class:`datetime.datetime` with the ``fold`` + attribute added, if ``fold`` is 1. + + ..versionadded:: 2.6.0 + """ + if getattr(dt, 'fold', 0) == fold: + return dt + + args = dt.timetuple()[:6] + args += (dt.microsecond, dt.tzinfo) + + if fold: + return _DatetimeWithFold(*args) + else: + return datetime(*args) + + +class _tzinfo(tzinfo): + """ + Base class for all ``dateutil`` ``tzinfo`` objects. + """ + + def is_ambiguous(self, dt): + """ + Whether or not the "wall time" of a given datetime is ambiguous in this + zone. + + :param dt: + A :py:class:`datetime.datetime`, naive or time zone aware. + + + :return: + Returns ``True`` if ambiguous, ``False`` otherwise. + + ..versionadded:: 2.6.0 + """ + + dt = dt.replace(tzinfo=self) + + wall_0 = enfold(dt, fold=0) + wall_1 = enfold(dt, fold=1) + + same_offset = wall_0.utcoffset() == wall_1.utcoffset() + same_dt = wall_0.replace(tzinfo=None) == wall_1.replace(tzinfo=None) + + return same_dt and not same_offset + + def _fold_status(self, dt_utc, dt_wall): + """ + Determine the fold status of a "wall" datetime, given a representation + of the same datetime as a (naive) UTC datetime. This is calculated based + on the assumption that ``dt.utcoffset() - dt.dst()`` is constant for all + datetimes, and that this offset is the actual number of hours separating + ``dt_utc`` and ``dt_wall``. + + :param dt_utc: + Representation of the datetime as UTC + + :param dt_wall: + Representation of the datetime as "wall time". This parameter must + either have a `fold` attribute or have a fold-naive + :class:`datetime.tzinfo` attached, otherwise the calculation may + fail. + """ + if self.is_ambiguous(dt_wall): + delta_wall = dt_wall - dt_utc + _fold = int(delta_wall == (dt_utc.utcoffset() - dt_utc.dst())) + else: + _fold = 0 + + return _fold + + def _fold(self, dt): + return getattr(dt, 'fold', 0) + + def _fromutc(self, dt): + """ + Given a timezone-aware datetime in a given timezone, calculates a + timezone-aware datetime in a new timezone. + + Since this is the one time that we *know* we have an unambiguous + datetime object, we take this opportunity to determine whether the + datetime is ambiguous and in a "fold" state (e.g. if it's the first + occurence, chronologically, of the ambiguous datetime). + + :param dt: + A timezone-aware :class:`datetime.dateime` object. + """ + + # Re-implement the algorithm from Python's datetime.py + if not isinstance(dt, datetime): + raise TypeError("fromutc() requires a datetime argument") + if dt.tzinfo is not self: + raise ValueError("dt.tzinfo is not self") + + dtoff = dt.utcoffset() + if dtoff is None: + raise ValueError("fromutc() requires a non-None utcoffset() " + "result") + + # The original datetime.py code assumes that `dst()` defaults to + # zero during ambiguous times. PEP 495 inverts this presumption, so + # for pre-PEP 495 versions of python, we need to tweak the algorithm. + dtdst = dt.dst() + if dtdst is None: + raise ValueError("fromutc() requires a non-None dst() result") + delta = dtoff - dtdst + if delta: + dt += delta + # Set fold=1 so we can default to being in the fold for + # ambiguous dates. + dtdst = enfold(dt, fold=1).dst() + if dtdst is None: + raise ValueError("fromutc(): dt.dst gave inconsistent " + "results; cannot convert") + return dt + dtdst + + def fromutc(self, dt): + """ + Given a timezone-aware datetime in a given timezone, calculates a + timezone-aware datetime in a new timezone. + + Since this is the one time that we *know* we have an unambiguous + datetime object, we take this opportunity to determine whether the + datetime is ambiguous and in a "fold" state (e.g. if it's the first + occurance, chronologically, of the ambiguous datetime). + + :param dt: + A timezone-aware :class:`datetime.dateime` object. + """ + dt_wall = self._fromutc(dt) + + # Calculate the fold status given the two datetimes. + _fold = self._fold_status(dt, dt_wall) + + # Set the default fold value for ambiguous dates + return enfold(dt_wall, fold=_fold) + + +class tzrangebase(_tzinfo): + """ + This is an abstract base class for time zones represented by an annual + transition into and out of DST. Child classes should implement the following + methods: + + * ``__init__(self, *args, **kwargs)`` + * ``transitions(self, year)`` - this is expected to return a tuple of + datetimes representing the DST on and off transitions in standard + time. + + A fully initialized ``tzrangebase`` subclass should also provide the + following attributes: + * ``hasdst``: Boolean whether or not the zone uses DST. + * ``_dst_offset`` / ``_std_offset``: :class:`datetime.timedelta` objects + representing the respective UTC offsets. + * ``_dst_abbr`` / ``_std_abbr``: Strings representing the timezone short + abbreviations in DST and STD, respectively. + * ``_hasdst``: Whether or not the zone has DST. + + ..versionadded:: 2.6.0 + """ + def __init__(self): + raise NotImplementedError('tzrangebase is an abstract base class') + + def utcoffset(self, dt): + isdst = self._isdst(dt) + + if isdst is None: + return None + elif isdst: + return self._dst_offset + else: + return self._std_offset + + def dst(self, dt): + isdst = self._isdst(dt) + + if isdst is None: + return None + elif isdst: + return self._dst_base_offset + else: + return ZERO + + @tzname_in_python2 + def tzname(self, dt): + if self._isdst(dt): + return self._dst_abbr + else: + return self._std_abbr + + def fromutc(self, dt): + """ Given a datetime in UTC, return local time """ + if not isinstance(dt, datetime): + raise TypeError("fromutc() requires a datetime argument") + + if dt.tzinfo is not self: + raise ValueError("dt.tzinfo is not self") + + # Get transitions - if there are none, fixed offset + transitions = self.transitions(dt.year) + if transitions is None: + return dt + self.utcoffset(dt) + + # Get the transition times in UTC + dston, dstoff = transitions + + dston -= self._std_offset + dstoff -= self._std_offset + + utc_transitions = (dston, dstoff) + dt_utc = dt.replace(tzinfo=None) + + + isdst = self._naive_isdst(dt_utc, utc_transitions) + + if isdst: + dt_wall = dt + self._dst_offset + else: + dt_wall = dt + self._std_offset + + _fold = int(not isdst and self.is_ambiguous(dt_wall)) + + return enfold(dt_wall, fold=_fold) + + def is_ambiguous(self, dt): + """ + Whether or not the "wall time" of a given datetime is ambiguous in this + zone. + + :param dt: + A :py:class:`datetime.datetime`, naive or time zone aware. + + + :return: + Returns ``True`` if ambiguous, ``False`` otherwise. + + .. versionadded:: 2.6.0 + """ + if not self.hasdst: + return False + + start, end = self.transitions(dt.year) + + dt = dt.replace(tzinfo=None) + return (end <= dt < end + self._dst_base_offset) + + def _isdst(self, dt): + if not self.hasdst: + return False + elif dt is None: + return None + + transitions = self.transitions(dt.year) + + if transitions is None: + return False + + dt = dt.replace(tzinfo=None) + + isdst = self._naive_isdst(dt, transitions) + + # Handle ambiguous dates + if not isdst and self.is_ambiguous(dt): + return not self._fold(dt) + else: + return isdst + + def _naive_isdst(self, dt, transitions): + dston, dstoff = transitions + + dt = dt.replace(tzinfo=None) + + if dston < dstoff: + isdst = dston <= dt < dstoff + else: + isdst = not dstoff <= dt < dston + + return isdst + + @property + def _dst_base_offset(self): + return self._dst_offset - self._std_offset + + __hash__ = None + + def __ne__(self, other): + return not (self == other) + + def __repr__(self): + return "%s(...)" % self.__class__.__name__ + + __reduce__ = object.__reduce__ + + +def _total_seconds(td): + # Python 2.6 doesn't have a total_seconds() method on timedelta objects + return ((td.seconds + td.days * 86400) * 1000000 + + td.microseconds) // 1000000 + +_total_seconds = getattr(timedelta, 'total_seconds', _total_seconds) diff --git a/lib/dateutil/tz/tz.py b/lib/dateutil/tz/tz.py index 56421fea8..6bee29168 100644 --- a/lib/dateutil/tz/tz.py +++ b/lib/dateutil/tz/tz.py @@ -12,24 +12,30 @@ import struct import time import sys import os +import bisect +import copy + +from operator import itemgetter + +from contextlib import contextmanager from six import string_types, PY3 -from ._common import tzname_in_python2 +from ._common import tzname_in_python2, _tzinfo, _total_seconds +from ._common import tzrangebase, enfold try: from .win import tzwin, tzwinlocal except ImportError: tzwin = tzwinlocal = None -relativedelta = None -parser = None -rrule = None - ZERO = datetime.timedelta(0) -EPOCHORDINAL = datetime.datetime.utcfromtimestamp(0).toordinal() +EPOCH = datetime.datetime.utcfromtimestamp(0) +EPOCHORDINAL = EPOCH.toordinal() class tzutc(datetime.tzinfo): - + """ + This is a tzinfo object that represents the UTC time zone. + """ def utcoffset(self, dt): return ZERO @@ -40,12 +46,33 @@ class tzutc(datetime.tzinfo): def tzname(self, dt): return "UTC" + def is_ambiguous(self, dt): + """ + Whether or not the "wall time" of a given datetime is ambiguous in this + zone. + + :param dt: + A :py:class:`datetime.datetime`, naive or time zone aware. + + + :return: + Returns ``True`` if ambiguous, ``False`` otherwise. + + .. versionadded:: 2.6.0 + """ + return False + def __eq__(self, other): + if not isinstance(other, (tzutc, tzoffset)): + return NotImplemented + return (isinstance(other, tzutc) or (isinstance(other, tzoffset) and other._offset == ZERO)) + __hash__ = None + def __ne__(self, other): - return not self.__eq__(other) + return not (self == other) def __repr__(self): return "%s()" % self.__class__.__name__ @@ -54,9 +81,24 @@ class tzutc(datetime.tzinfo): class tzoffset(datetime.tzinfo): + """ + A simple class for representing a fixed offset from UTC. + + :param name: + The timezone name, to be returned when ``tzname()`` is called. + :param offset: + The time zone offset in seconds, or (since version 2.6.0, represented + as a :py:class:`datetime.timedelta` object. + """ def __init__(self, name, offset): self._name = name + + try: + # Allow a timedelta + offset = _total_seconds(offset) + except (TypeError, AttributeError): + pass self._offset = datetime.timedelta(seconds=offset) def utcoffset(self, dt): @@ -65,36 +107,64 @@ class tzoffset(datetime.tzinfo): def dst(self, dt): return ZERO + def is_ambiguous(self, dt): + """ + Whether or not the "wall time" of a given datetime is ambiguous in this + zone. + + :param dt: + A :py:class:`datetime.datetime`, naive or time zone aware. + + + :return: + Returns ``True`` if ambiguous, ``False`` otherwise. + + .. versionadded:: 2.6.0 + """ + return False + @tzname_in_python2 def tzname(self, dt): return self._name def __eq__(self, other): - return (isinstance(other, tzoffset) and - self._offset == other._offset) + if not isinstance(other, tzoffset): + return NotImplemented + + return self._offset == other._offset + + __hash__ = None def __ne__(self, other): - return not self.__eq__(other) + return not (self == other) def __repr__(self): return "%s(%s, %s)" % (self.__class__.__name__, repr(self._name), - self._offset.days*86400+self._offset.seconds) + int(_total_seconds(self._offset))) __reduce__ = object.__reduce__ -class tzlocal(datetime.tzinfo): +class tzlocal(_tzinfo): + """ + A :class:`tzinfo` subclass built around the ``time`` timezone functions. + """ def __init__(self): + super(tzlocal, self).__init__() + self._std_offset = datetime.timedelta(seconds=-time.timezone) if time.daylight: self._dst_offset = datetime.timedelta(seconds=-time.altzone) else: self._dst_offset = self._std_offset + self._dst_saved = self._dst_offset - self._std_offset + self._hasdst = bool(self._dst_saved) + def utcoffset(self, dt): - if dt is None: - return dt + if dt is None and self._hasdst: + return None if self._isdst(dt): return self._dst_offset @@ -102,8 +172,11 @@ class tzlocal(datetime.tzinfo): return self._std_offset def dst(self, dt): + if dt is None and self._hasdst: + return None + if self._isdst(dt): - return self._dst_offset-self._std_offset + return self._dst_offset - self._std_offset else: return ZERO @@ -111,7 +184,29 @@ class tzlocal(datetime.tzinfo): def tzname(self, dt): return time.tzname[self._isdst(dt)] - def _isdst(self, dt): + def is_ambiguous(self, dt): + """ + Whether or not the "wall time" of a given datetime is ambiguous in this + zone. + + :param dt: + A :py:class:`datetime.datetime`, naive or time zone aware. + + + :return: + Returns ``True`` if ambiguous, ``False`` otherwise. + + .. versionadded:: 2.6.0 + """ + naive_dst = self._naive_is_dst(dt) + return (not naive_dst and + (naive_dst != self._naive_is_dst(dt - self._dst_saved))) + + def _naive_is_dst(self, dt): + timestamp = _datetime_to_timestamp(dt) + return time.localtime(timestamp + time.timezone).tm_isdst + + def _isdst(self, dt, fold_naive=True): # We can't use mktime here. It is unstable when deciding if # the hour near to a change is DST or not. # @@ -136,19 +231,32 @@ class tzlocal(datetime.tzinfo): # # Here is a more stable implementation: # - timestamp = ((dt.toordinal() - EPOCHORDINAL) * 86400 - + dt.hour * 3600 - + dt.minute * 60 - + dt.second) - return time.localtime(timestamp+time.timezone).tm_isdst + if not self._hasdst: + return False + + # Check for ambiguous times: + dstval = self._naive_is_dst(dt) + fold = getattr(dt, 'fold', None) + + if self.is_ambiguous(dt): + if fold is not None: + return not self._fold(dt) + else: + return True + + return dstval def __eq__(self, other): - return (isinstance(other, tzlocal) and - (self._std_offset == other._std_offset and - self._dst_offset == other._dst_offset)) + if not isinstance(other, tzlocal): + return NotImplemented + + return (self._std_offset == other._std_offset and + self._dst_offset == other._dst_offset) + + __hash__ = None def __ne__(self, other): - return not self.__eq__(other) + return not (self == other) def __repr__(self): return "%s()" % self.__class__.__name__ @@ -157,7 +265,8 @@ class tzlocal(datetime.tzinfo): class _ttinfo(object): - __slots__ = ["offset", "delta", "isdst", "abbr", "isstd", "isgmt"] + __slots__ = ["offset", "delta", "isdst", "abbr", + "isstd", "isgmt", "dstoffset"] def __init__(self): for attr in self.__slots__: @@ -173,16 +282,20 @@ class _ttinfo(object): def __eq__(self, other): if not isinstance(other, _ttinfo): - return False + return NotImplemented + return (self.offset == other.offset and self.delta == other.delta and self.isdst == other.isdst and self.abbr == other.abbr and self.isstd == other.isstd and - self.isgmt == other.isgmt) + self.isgmt == other.isgmt and + self.dstoffset == other.dstoffset) + + __hash__ = None def __ne__(self, other): - return not self.__eq__(other) + return not (self == other) def __getstate__(self): state = {} @@ -196,12 +309,44 @@ class _ttinfo(object): setattr(self, name, state[name]) -class tzfile(datetime.tzinfo): +class _tzfile(object): + """ + Lightweight class for holding the relevant transition and time zone + information read from binary tzfiles. + """ + attrs = ['trans_list', 'trans_idx', 'ttinfo_list', + 'ttinfo_std', 'ttinfo_dst', 'ttinfo_before', 'ttinfo_first'] + + def __init__(self, **kwargs): + for attr in self.attrs: + setattr(self, attr, kwargs.get(attr, None)) + + +class tzfile(_tzinfo): + """ + This is a ``tzinfo`` subclass that allows one to use the ``tzfile(5)`` + format timezone files to extract current and historical zone information. - # http://www.twinsun.com/tz/tz-link.htm - # ftp://ftp.iana.org/tz/tz*.tar.gz + :param fileobj: + This can be an opened file stream or a file name that the time zone + information can be read from. + + :param filename: + This is an optional parameter specifying the source of the time zone + information in the event that ``fileobj`` is a file object. If omitted + and ``fileobj`` is a file stream, this parameter will be set either to + ``fileobj``'s ``name`` attribute or to ``repr(fileobj)``. + + See `Sources for Time Zone and Daylight Saving Time Data + <http://www.twinsun.com/tz/tz-link.htm>`_ for more information. Time zone + files can be compiled from the `IANA Time Zone database files + <https://www.iana.org/time-zones>`_ with the `zic time zone compiler + <https://www.freebsd.org/cgi/man.cgi?query=zic&sektion=8>`_ + """ def __init__(self, fileobj, filename=None): + super(tzfile, self).__init__() + file_opened_here = False if isinstance(fileobj, string_types): self._filename = fileobj @@ -214,6 +359,24 @@ class tzfile(datetime.tzinfo): else: self._filename = repr(fileobj) + if fileobj is not None: + if not file_opened_here: + fileobj = _ContextWrapper(fileobj) + + with fileobj as file_stream: + tzobj = self._read_tzfile(file_stream) + + self._set_tzdata(tzobj) + + def _set_tzdata(self, tzobj): + """ Set the time zone data of this object from a _tzfile object """ + # Copy the relevant attributes over as private attributes + for attr in _tzfile.attrs: + setattr(self, '_' + attr, getattr(tzobj, attr)) + + def _read_tzfile(self, fileobj): + out = _tzfile() + # From tzfile(5): # # The time zone information files used by tzset(3) @@ -223,176 +386,169 @@ class tzfile(datetime.tzinfo): # six four-byte values of type long, written in a # ``standard'' byte order (the high-order byte # of the value is written first). - try: - if fileobj.read(4).decode() != "TZif": - raise ValueError("magic not found") + if fileobj.read(4).decode() != "TZif": + raise ValueError("magic not found") - fileobj.read(16) + fileobj.read(16) - ( - # The number of UTC/local indicators stored in the file. - ttisgmtcnt, + ( + # The number of UTC/local indicators stored in the file. + ttisgmtcnt, - # The number of standard/wall indicators stored in the file. - ttisstdcnt, + # The number of standard/wall indicators stored in the file. + ttisstdcnt, - # The number of leap seconds for which data is - # stored in the file. - leapcnt, + # The number of leap seconds for which data is + # stored in the file. + leapcnt, - # The number of "transition times" for which data - # is stored in the file. - timecnt, + # The number of "transition times" for which data + # is stored in the file. + timecnt, - # The number of "local time types" for which data - # is stored in the file (must not be zero). - typecnt, + # The number of "local time types" for which data + # is stored in the file (must not be zero). + typecnt, - # The number of characters of "time zone - # abbreviation strings" stored in the file. - charcnt, + # The number of characters of "time zone + # abbreviation strings" stored in the file. + charcnt, - ) = struct.unpack(">6l", fileobj.read(24)) + ) = struct.unpack(">6l", fileobj.read(24)) - # The above header is followed by tzh_timecnt four-byte - # values of type long, sorted in ascending order. - # These values are written in ``standard'' byte order. - # Each is used as a transition time (as returned by - # time(2)) at which the rules for computing local time - # change. + # The above header is followed by tzh_timecnt four-byte + # values of type long, sorted in ascending order. + # These values are written in ``standard'' byte order. + # Each is used as a transition time (as returned by + # time(2)) at which the rules for computing local time + # change. - if timecnt: - self._trans_list = struct.unpack(">%dl" % timecnt, - fileobj.read(timecnt*4)) - else: - self._trans_list = [] - - # Next come tzh_timecnt one-byte values of type unsigned - # char; each one tells which of the different types of - # ``local time'' types described in the file is associated - # with the same-indexed transition time. These values - # serve as indices into an array of ttinfo structures that - # appears next in the file. - - if timecnt: - self._trans_idx = struct.unpack(">%dB" % timecnt, - fileobj.read(timecnt)) - else: - self._trans_idx = [] - - # Each ttinfo structure is written as a four-byte value - # for tt_gmtoff of type long, in a standard byte - # order, followed by a one-byte value for tt_isdst - # and a one-byte value for tt_abbrind. In each - # structure, tt_gmtoff gives the number of - # seconds to be added to UTC, tt_isdst tells whether - # tm_isdst should be set by localtime(3), and - # tt_abbrind serves as an index into the array of - # time zone abbreviation characters that follow the - # ttinfo structure(s) in the file. - - ttinfo = [] - - for i in range(typecnt): - ttinfo.append(struct.unpack(">lbb", fileobj.read(6))) - - abbr = fileobj.read(charcnt).decode() - - # Then there are tzh_leapcnt pairs of four-byte - # values, written in standard byte order; the - # first value of each pair gives the time (as - # returned by time(2)) at which a leap second - # occurs; the second gives the total number of - # leap seconds to be applied after the given time. - # The pairs of values are sorted in ascending order - # by time. - - # Not used, for now - # if leapcnt: - # leap = struct.unpack(">%dl" % (leapcnt*2), - # fileobj.read(leapcnt*8)) - - # Then there are tzh_ttisstdcnt standard/wall - # indicators, each stored as a one-byte value; - # they tell whether the transition times associated - # with local time types were specified as standard - # time or wall clock time, and are used when - # a time zone file is used in handling POSIX-style - # time zone environment variables. - - if ttisstdcnt: - isstd = struct.unpack(">%db" % ttisstdcnt, - fileobj.read(ttisstdcnt)) - - # Finally, there are tzh_ttisgmtcnt UTC/local - # indicators, each stored as a one-byte value; - # they tell whether the transition times associated - # with local time types were specified as UTC or - # local time, and are used when a time zone file - # is used in handling POSIX-style time zone envi- - # ronment variables. - - if ttisgmtcnt: - isgmt = struct.unpack(">%db" % ttisgmtcnt, - fileobj.read(ttisgmtcnt)) - - # ** Everything has been read ** - finally: - if file_opened_here: - fileobj.close() + if timecnt: + out.trans_list = list(struct.unpack(">%dl" % timecnt, + fileobj.read(timecnt*4))) + else: + out.trans_list = [] + + # Next come tzh_timecnt one-byte values of type unsigned + # char; each one tells which of the different types of + # ``local time'' types described in the file is associated + # with the same-indexed transition time. These values + # serve as indices into an array of ttinfo structures that + # appears next in the file. + + if timecnt: + out.trans_idx = struct.unpack(">%dB" % timecnt, + fileobj.read(timecnt)) + else: + out.trans_idx = [] + + # Each ttinfo structure is written as a four-byte value + # for tt_gmtoff of type long, in a standard byte + # order, followed by a one-byte value for tt_isdst + # and a one-byte value for tt_abbrind. In each + # structure, tt_gmtoff gives the number of + # seconds to be added to UTC, tt_isdst tells whether + # tm_isdst should be set by localtime(3), and + # tt_abbrind serves as an index into the array of + # time zone abbreviation characters that follow the + # ttinfo structure(s) in the file. + + ttinfo = [] + + for i in range(typecnt): + ttinfo.append(struct.unpack(">lbb", fileobj.read(6))) + + abbr = fileobj.read(charcnt).decode() + + # Then there are tzh_leapcnt pairs of four-byte + # values, written in standard byte order; the + # first value of each pair gives the time (as + # returned by time(2)) at which a leap second + # occurs; the second gives the total number of + # leap seconds to be applied after the given time. + # The pairs of values are sorted in ascending order + # by time. + + # Not used, for now (but read anyway for correct file position) + if leapcnt: + leap = struct.unpack(">%dl" % (leapcnt*2), + fileobj.read(leapcnt*8)) + + # Then there are tzh_ttisstdcnt standard/wall + # indicators, each stored as a one-byte value; + # they tell whether the transition times associated + # with local time types were specified as standard + # time or wall clock time, and are used when + # a time zone file is used in handling POSIX-style + # time zone environment variables. + + if ttisstdcnt: + isstd = struct.unpack(">%db" % ttisstdcnt, + fileobj.read(ttisstdcnt)) + + # Finally, there are tzh_ttisgmtcnt UTC/local + # indicators, each stored as a one-byte value; + # they tell whether the transition times associated + # with local time types were specified as UTC or + # local time, and are used when a time zone file + # is used in handling POSIX-style time zone envi- + # ronment variables. + + if ttisgmtcnt: + isgmt = struct.unpack(">%db" % ttisgmtcnt, + fileobj.read(ttisgmtcnt)) # Build ttinfo list - self._ttinfo_list = [] + out.ttinfo_list = [] for i in range(typecnt): gmtoff, isdst, abbrind = ttinfo[i] # Round to full-minutes if that's not the case. Python's # datetime doesn't accept sub-minute timezones. Check # http://python.org/sf/1447945 for some information. - gmtoff = (gmtoff+30)//60*60 + gmtoff = 60 * ((gmtoff + 30) // 60) tti = _ttinfo() tti.offset = gmtoff + tti.dstoffset = datetime.timedelta(0) tti.delta = datetime.timedelta(seconds=gmtoff) tti.isdst = isdst tti.abbr = abbr[abbrind:abbr.find('\x00', abbrind)] tti.isstd = (ttisstdcnt > i and isstd[i] != 0) tti.isgmt = (ttisgmtcnt > i and isgmt[i] != 0) - self._ttinfo_list.append(tti) + out.ttinfo_list.append(tti) # Replace ttinfo indexes for ttinfo objects. - trans_idx = [] - for idx in self._trans_idx: - trans_idx.append(self._ttinfo_list[idx]) - self._trans_idx = tuple(trans_idx) + out.trans_idx = [out.ttinfo_list[idx] for idx in out.trans_idx] # Set standard, dst, and before ttinfos. before will be # used when a given time is before any transitions, # and will be set to the first non-dst ttinfo, or to # the first dst, if all of them are dst. - self._ttinfo_std = None - self._ttinfo_dst = None - self._ttinfo_before = None - if self._ttinfo_list: - if not self._trans_list: - self._ttinfo_std = self._ttinfo_first = self._ttinfo_list[0] + out.ttinfo_std = None + out.ttinfo_dst = None + out.ttinfo_before = None + if out.ttinfo_list: + if not out.trans_list: + out.ttinfo_std = out.ttinfo_first = out.ttinfo_list[0] else: for i in range(timecnt-1, -1, -1): - tti = self._trans_idx[i] - if not self._ttinfo_std and not tti.isdst: - self._ttinfo_std = tti - elif not self._ttinfo_dst and tti.isdst: - self._ttinfo_dst = tti - if self._ttinfo_std and self._ttinfo_dst: + tti = out.trans_idx[i] + if not out.ttinfo_std and not tti.isdst: + out.ttinfo_std = tti + elif not out.ttinfo_dst and tti.isdst: + out.ttinfo_dst = tti + + if out.ttinfo_std and out.ttinfo_dst: break else: - if self._ttinfo_dst and not self._ttinfo_std: - self._ttinfo_std = self._ttinfo_dst + if out.ttinfo_dst and not out.ttinfo_std: + out.ttinfo_std = out.ttinfo_dst - for tti in self._ttinfo_list: + for tti in out.ttinfo_list: if not tti.isdst: - self._ttinfo_before = tti + out.ttinfo_before = tti break else: - self._ttinfo_before = self._ttinfo_list[0] + out.ttinfo_before = out.ttinfo_list[0] # Now fix transition times to become relative to wall time. # @@ -401,43 +557,113 @@ class tzfile(datetime.tzinfo): # isgmt are off, so it should be in wall time. OTOH, it's # always in gmt time. Let me know if you have comments # about this. - laststdoffset = 0 - self._trans_list = list(self._trans_list) - for i in range(len(self._trans_list)): - tti = self._trans_idx[i] + laststdoffset = None + for i, tti in enumerate(out.trans_idx): if not tti.isdst: - # This is std time. - self._trans_list[i] += tti.offset - laststdoffset = tti.offset + offset = tti.offset + laststdoffset = offset else: - # This is dst time. Convert to std. - self._trans_list[i] += laststdoffset - self._trans_list = tuple(self._trans_list) - - def _find_ttinfo(self, dt, laststd=0): - timestamp = ((dt.toordinal() - EPOCHORDINAL) * 86400 - + dt.hour * 3600 - + dt.minute * 60 - + dt.second) - idx = 0 - for trans in self._trans_list: - if timestamp < trans: - break - idx += 1 - else: + if laststdoffset is not None: + # Store the DST offset as well and update it in the list + tti.dstoffset = tti.offset - laststdoffset + out.trans_idx[i] = tti + + offset = laststdoffset or 0 + + out.trans_list[i] += offset + + # In case we missed any DST offsets on the way in for some reason, make + # a second pass over the list, looking for the /next/ DST offset. + laststdoffset = None + for i in reversed(range(len(out.trans_idx))): + tti = out.trans_idx[i] + if tti.isdst: + if not (tti.dstoffset or laststdoffset is None): + tti.dstoffset = tti.offset - laststdoffset + else: + laststdoffset = tti.offset + + if not isinstance(tti.dstoffset, datetime.timedelta): + tti.dstoffset = datetime.timedelta(seconds=tti.dstoffset) + + out.trans_idx[i] = tti + + out.trans_idx = tuple(out.trans_idx) + out.trans_list = tuple(out.trans_list) + + return out + + def _find_last_transition(self, dt): + # If there's no list, there are no transitions to find + if not self._trans_list: + return None + + timestamp = _datetime_to_timestamp(dt) + + # Find where the timestamp fits in the transition list - if the + # timestamp is a transition time, it's part of the "after" period. + idx = bisect.bisect_right(self._trans_list, timestamp) + + # We want to know when the previous transition was, so subtract off 1 + return idx - 1 + + def _get_ttinfo(self, idx): + # For no list or after the last transition, default to _ttinfo_std + if idx is None or (idx + 1) == len(self._trans_list): return self._ttinfo_std - if idx == 0: + + # If there is a list and the time is before it, return _ttinfo_before + if idx < 0: return self._ttinfo_before - if laststd: - while idx > 0: - tti = self._trans_idx[idx-1] - if not tti.isdst: - return tti - idx -= 1 - else: - return self._ttinfo_std - else: - return self._trans_idx[idx-1] + + return self._trans_idx[idx] + + def _find_ttinfo(self, dt): + idx = self._resolve_ambiguous_time(dt) + + return self._get_ttinfo(idx) + + def is_ambiguous(self, dt, idx=None): + """ + Whether or not the "wall time" of a given datetime is ambiguous in this + zone. + + :param dt: + A :py:class:`datetime.datetime`, naive or time zone aware. + + + :return: + Returns ``True`` if ambiguous, ``False`` otherwise. + + .. versionadded:: 2.6.0 + """ + if idx is None: + idx = self._find_last_transition(dt) + + # Calculate the difference in offsets from current to previous + timestamp = _datetime_to_timestamp(dt) + tti = self._get_ttinfo(idx) + + if idx is None or idx <= 0: + return False + + od = self._get_ttinfo(idx - 1).offset - tti.offset + tt = self._trans_list[idx] # Transition time + + return timestamp < tt + od + + def _resolve_ambiguous_time(self, dt): + idx = self._find_last_transition(dt) + + # If we have no transitions, return the index + _fold = self._fold(dt) + if idx is None or idx == 0: + return idx + + # Get the current datetime as a timestamp + idx_offset = int(not _fold and self.is_ambiguous(dt, idx)) + + return idx - idx_offset def utcoffset(self, dt): if dt is None: @@ -445,119 +671,202 @@ class tzfile(datetime.tzinfo): if not self._ttinfo_std: return ZERO + return self._find_ttinfo(dt).delta def dst(self, dt): + if dt is None: + return None + if not self._ttinfo_dst: return ZERO + tti = self._find_ttinfo(dt) + if not tti.isdst: return ZERO # The documentation says that utcoffset()-dst() must # be constant for every dt. - return tti.delta-self._find_ttinfo(dt, laststd=1).delta - - # An alternative for that would be: - # - # return self._ttinfo_dst.offset-self._ttinfo_std.offset - # - # However, this class stores historical changes in the - # dst offset, so I belive that this wouldn't be the right - # way to implement this. + return tti.dstoffset @tzname_in_python2 def tzname(self, dt): - if not self._ttinfo_std: + if not self._ttinfo_std or dt is None: return None return self._find_ttinfo(dt).abbr def __eq__(self, other): if not isinstance(other, tzfile): - return False + return NotImplemented return (self._trans_list == other._trans_list and self._trans_idx == other._trans_idx and self._ttinfo_list == other._ttinfo_list) + __hash__ = None + def __ne__(self, other): - return not self.__eq__(other) + return not (self == other) def __repr__(self): return "%s(%s)" % (self.__class__.__name__, repr(self._filename)) def __reduce__(self): - if not os.path.isfile(self._filename): - raise ValueError("Unpickable %s class" % self.__class__.__name__) - return (self.__class__, (self._filename,)) + return self.__reduce_ex__(None) + + def __reduce_ex__(self, protocol): + return (self.__class__, (None, self._filename), self.__dict__) + + +class tzrange(tzrangebase): + """ + The ``tzrange`` object is a time zone specified by a set of offsets and + abbreviations, equivalent to the way the ``TZ`` variable can be specified + in POSIX-like systems, but using Python delta objects to specify DST + start, end and offsets. + + :param stdabbr: + The abbreviation for standard time (e.g. ``'EST'``). + + :param stdoffset: + An integer or :class:`datetime.timedelta` object or equivalent + specifying the base offset from UTC. + + If unspecified, +00:00 is used. + + :param dstabbr: + The abbreviation for DST / "Summer" time (e.g. ``'EDT'``). + + If specified, with no other DST information, DST is assumed to occur + and the default behavior or ``dstoffset``, ``start`` and ``end`` is + used. If unspecified and no other DST information is specified, it + is assumed that this zone has no DST. + + If this is unspecified and other DST information is *is* specified, + DST occurs in the zone but the time zone abbreviation is left + unchanged. + :param dstoffset: + A an integer or :class:`datetime.timedelta` object or equivalent + specifying the UTC offset during DST. If unspecified and any other DST + information is specified, it is assumed to be the STD offset +1 hour. -class tzrange(datetime.tzinfo): + :param start: + A :class:`relativedelta.relativedelta` object or equivalent specifying + the time and time of year that daylight savings time starts. To specify, + for example, that DST starts at 2AM on the 2nd Sunday in March, pass: + + ``relativedelta(hours=2, month=3, day=1, weekday=SU(+2))`` + + If unspecified and any other DST information is specified, the default + value is 2 AM on the first Sunday in April. + + :param end: + A :class:`relativedelta.relativedelta` object or equivalent representing + the time and time of year that daylight savings time ends, with the + same specification method as in ``start``. One note is that this should + point to the first time in the *standard* zone, so if a transition + occurs at 2AM in the DST zone and the clocks are set back 1 hour to 1AM, + set the `hours` parameter to +1. + + + **Examples:** + + .. testsetup:: tzrange + + from dateutil.tz import tzrange, tzstr + + .. doctest:: tzrange + + >>> tzstr('EST5EDT') == tzrange("EST", -18000, "EDT") + True + + >>> from dateutil.relativedelta import * + >>> range1 = tzrange("EST", -18000, "EDT") + >>> range2 = tzrange("EST", -18000, "EDT", -14400, + ... relativedelta(hours=+2, month=4, day=1, + ... weekday=SU(+1)), + ... relativedelta(hours=+1, month=10, day=31, + ... weekday=SU(-1))) + >>> tzstr('EST5EDT') == range1 == range2 + True + + """ def __init__(self, stdabbr, stdoffset=None, dstabbr=None, dstoffset=None, start=None, end=None): + global relativedelta - if not relativedelta: - from dateutil import relativedelta + from dateutil import relativedelta + self._std_abbr = stdabbr self._dst_abbr = dstabbr + + try: + stdoffset = _total_seconds(stdoffset) + except (TypeError, AttributeError): + pass + + try: + dstoffset = _total_seconds(dstoffset) + except (TypeError, AttributeError): + pass + if stdoffset is not None: self._std_offset = datetime.timedelta(seconds=stdoffset) else: self._std_offset = ZERO + if dstoffset is not None: self._dst_offset = datetime.timedelta(seconds=dstoffset) elif dstabbr and stdoffset is not None: - self._dst_offset = self._std_offset+datetime.timedelta(hours=+1) + self._dst_offset = self._std_offset + datetime.timedelta(hours=+1) else: self._dst_offset = ZERO + if dstabbr and start is None: self._start_delta = relativedelta.relativedelta( hours=+2, month=4, day=1, weekday=relativedelta.SU(+1)) else: self._start_delta = start + if dstabbr and end is None: self._end_delta = relativedelta.relativedelta( hours=+1, month=10, day=31, weekday=relativedelta.SU(-1)) else: self._end_delta = end - def utcoffset(self, dt): - if dt is None: - return None + self._dst_base_offset_ = self._dst_offset - self._std_offset + self.hasdst = bool(self._start_delta) - if self._isdst(dt): - return self._dst_offset - else: - return self._std_offset + def transitions(self, year): + """ + For a given year, get the DST on and off transition times, expressed + always on the standard time side. For zones with no transitions, this + function returns ``None``. - def dst(self, dt): - if self._isdst(dt): - return self._dst_offset-self._std_offset - else: - return ZERO + :param year: + The year whose transitions you would like to query. - @tzname_in_python2 - def tzname(self, dt): - if self._isdst(dt): - return self._dst_abbr - else: - return self._std_abbr + :return: + Returns a :class:`tuple` of :class:`datetime.datetime` objects, + ``(dston, dstoff)`` for zones with an annual DST transition, or + ``None`` for fixed offset zones. + """ + if not self.hasdst: + return None - def _isdst(self, dt): - if not self._start_delta: - return False - year = datetime.datetime(dt.year, 1, 1) - start = year+self._start_delta - end = year+self._end_delta - dt = dt.replace(tzinfo=None) - if start < end: - return dt >= start and dt < end - else: - return dt >= start or dt < end + base_year = datetime.datetime(year, 1, 1) + + start = base_year + self._start_delta + end = base_year + self._end_delta + + return (start, end) def __eq__(self, other): if not isinstance(other, tzrange): - return False + return NotImplemented + return (self._std_abbr == other._std_abbr and self._dst_abbr == other._dst_abbr and self._std_offset == other._std_offset and @@ -565,21 +874,44 @@ class tzrange(datetime.tzinfo): self._start_delta == other._start_delta and self._end_delta == other._end_delta) - def __ne__(self, other): - return not self.__eq__(other) - - def __repr__(self): - return "%s(...)" % self.__class__.__name__ - - __reduce__ = object.__reduce__ + @property + def _dst_base_offset(self): + return self._dst_base_offset_ class tzstr(tzrange): - - def __init__(self, s): + """ + ``tzstr`` objects are time zone objects specified by a time-zone string as + it would be passed to a ``TZ`` variable on POSIX-style systems (see + the `GNU C Library: TZ Variable`_ for more details). + + There is one notable exception, which is that POSIX-style time zones use an + inverted offset format, so normally ``GMT+3`` would be parsed as an offset + 3 hours *behind* GMT. The ``tzstr`` time zone object will parse this as an + offset 3 hours *ahead* of GMT. If you would like to maintain the POSIX + behavior, pass a ``True`` value to ``posix_offset``. + + The :class:`tzrange` object provides the same functionality, but is + specified using :class:`relativedelta.relativedelta` objects. rather than + strings. + + :param s: + A time zone string in ``TZ`` variable format. This can be a + :class:`bytes` (2.x: :class:`str`), :class:`str` (2.x: :class:`unicode`) + or a stream emitting unicode characters (e.g. :class:`StringIO`). + + :param posix_offset: + Optional. If set to ``True``, interpret strings such as ``GMT+3`` or + ``UTC+3`` as being 3 hours *behind* UTC rather than ahead, per the + POSIX standard. + + .. _`GNU C Library: TZ Variable`: + https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html + """ + def __init__(self, s, posix_offset=False): global parser - if not parser: - from dateutil import parser + from dateutil import parser + self._s = s res = parser._parsetz(s) @@ -588,7 +920,7 @@ class tzstr(tzrange): # Here we break the compatibility with the TZ variable handling. # GMT-3 actually *means* the timezone -3. - if res.stdabbr in ("GMT", "UTC"): + if res.stdabbr in ("GMT", "UTC") and not posix_offset: res.stdoffset *= -1 # We must initialize it first, since _delta() needs @@ -606,7 +938,10 @@ class tzstr(tzrange): if self._start_delta: self._end_delta = self._delta(res.end, isend=1) + self.hasdst = bool(self._start_delta) + def _delta(self, x, isend=0): + from dateutil import relativedelta kwargs = {} if x.month is not None: kwargs["month"] = x.month @@ -642,8 +977,8 @@ class tzstr(tzrange): # Convert to standard time, to follow the documented way # of working with the extra hour. See the documentation # of the tzinfo class. - delta = self._dst_offset-self._std_offset - kwargs["seconds"] -= delta.seconds+delta.days*86400 + delta = self._dst_offset - self._std_offset + kwargs["seconds"] -= delta.seconds + delta.days * 86400 return relativedelta.relativedelta(**kwargs) def __repr__(self): @@ -655,14 +990,16 @@ class _tzicalvtzcomp(object): tzname=None, rrule=None): self.tzoffsetfrom = datetime.timedelta(seconds=tzoffsetfrom) self.tzoffsetto = datetime.timedelta(seconds=tzoffsetto) - self.tzoffsetdiff = self.tzoffsetto-self.tzoffsetfrom + self.tzoffsetdiff = self.tzoffsetto - self.tzoffsetfrom self.isdst = isdst self.tzname = tzname self.rrule = rrule -class _tzicalvtz(datetime.tzinfo): +class _tzicalvtz(_tzinfo): def __init__(self, tzid, comps=[]): + super(_tzicalvtz, self).__init__() + self._tzid = tzid self._comps = comps self._cachedate = [] @@ -671,22 +1008,25 @@ class _tzicalvtz(datetime.tzinfo): def _find_comp(self, dt): if len(self._comps) == 1: return self._comps[0] + dt = dt.replace(tzinfo=None) + try: - return self._cachecomp[self._cachedate.index(dt)] + return self._cachecomp[self._cachedate.index((dt, self._fold(dt)))] except ValueError: pass - lastcomp = None + + lastcompdt = None + lastcomp = None + for comp in self._comps: - if not comp.isdst: - # Handle the extra hour in DST -> STD - compdt = comp.rrule.before(dt-comp.tzoffsetdiff, inc=True) - else: - compdt = comp.rrule.before(dt, inc=True) + compdt = self._find_compdt(comp, dt) + if compdt and (not lastcompdt or lastcompdt < compdt): lastcompdt = compdt lastcomp = comp + if not lastcomp: # RFC says nothing about what to do when a given # time is before the first onset date. We'll look for the @@ -698,13 +1038,24 @@ class _tzicalvtz(datetime.tzinfo): break else: lastcomp = comp[0] - self._cachedate.insert(0, dt) + + self._cachedate.insert(0, (dt, self._fold(dt))) self._cachecomp.insert(0, lastcomp) + if len(self._cachedate) > 10: self._cachedate.pop() self._cachecomp.pop() + return lastcomp + def _find_compdt(self, comp, dt): + if comp.tzoffsetdiff < ZERO and self._fold(dt): + dt -= comp.tzoffsetdiff + + compdt = comp.rrule.before(dt, inc=True) + + return compdt + def utcoffset(self, dt): if dt is None: return None @@ -729,35 +1080,65 @@ class _tzicalvtz(datetime.tzinfo): class tzical(object): + """ + This object is designed to parse an iCalendar-style ``VTIMEZONE`` structure + as set out in `RFC 2445`_ Section 4.6.5 into one or more `tzinfo` objects. + + :param `fileobj`: + A file or stream in iCalendar format, which should be UTF-8 encoded + with CRLF endings. + + .. _`RFC 2445`: https://www.ietf.org/rfc/rfc2445.txt + """ def __init__(self, fileobj): global rrule - if not rrule: - from dateutil import rrule + from dateutil import rrule if isinstance(fileobj, string_types): self._s = fileobj # ical should be encoded in UTF-8 with CRLF fileobj = open(fileobj, 'r') - elif hasattr(fileobj, "name"): - self._s = fileobj.name + file_opened_here = True else: - self._s = repr(fileobj) + self._s = getattr(fileobj, 'name', repr(fileobj)) + fileobj = _ContextWrapper(fileobj) self._vtz = {} - self._parse_rfc(fileobj.read()) + with fileobj as fobj: + self._parse_rfc(fobj.read()) def keys(self): + """ + Retrieves the available time zones as a list. + """ return list(self._vtz.keys()) def get(self, tzid=None): + """ + Retrieve a :py:class:`datetime.tzinfo` object by its ``tzid``. + + :param tzid: + If there is exactly one time zone available, omitting ``tzid`` + or passing :py:const:`None` value returns it. Otherwise a valid + key (which can be retrieved from :func:`keys`) is required. + + :raises ValueError: + Raised if ``tzid`` is not specified but there are either more + or fewer than 1 zone defined. + + :returns: + Returns either a :py:class:`datetime.tzinfo` object representing + the relevant time zone or :py:const:`None` if the ``tzid`` was + not found. + """ if tzid is None: - keys = list(self._vtz.keys()) - if len(keys) == 0: + if len(self._vtz) == 0: raise ValueError("no timezones defined") - elif len(keys) > 1: + elif len(self._vtz) > 1: raise ValueError("more than one timezone available") - tzid = keys[0] + tzid = next(iter(self._vtz)) + return self._vtz.get(tzid) def _parse_offset(self, s): @@ -770,11 +1151,11 @@ class tzical(object): else: signal = +1 if len(s) == 4: - return (int(s[:2])*3600+int(s[2:])*60)*signal + return (int(s[:2]) * 3600 + int(s[2:]) * 60) * signal elif len(s) == 6: - return (int(s[:2])*3600+int(s[2:4])*60+int(s[4:]))*signal + return (int(s[:2]) * 3600 + int(s[2:4]) * 60 + int(s[4:])) * signal else: - raise ValueError("invalid offset: "+s) + raise ValueError("invalid offset: " + s) def _parse_rfc(self, s): lines = s.splitlines() @@ -899,7 +1280,10 @@ class tzical(object): if sys.platform != "win32": TZFILES = ["/etc/localtime", "localtime"] - TZPATHS = ["/usr/share/zoneinfo", "/usr/lib/zoneinfo", "/etc/zoneinfo"] + TZPATHS = ["/usr/share/zoneinfo", + "/usr/lib/zoneinfo", + "/usr/share/lib/zoneinfo", + "/etc/zoneinfo"] else: TZFILES = [] TZPATHS = [] @@ -957,9 +1341,11 @@ def gettz(name=None): tz = tzwin(name) except WindowsError: tz = None + if not tz: - from dateutil.zoneinfo import gettz - tz = gettz(name) + from dateutil.zoneinfo import get_zonefile_instance + tz = get_zonefile_instance().get(name) + if not tz: for c in name: # name must have at least one offset to be a tzstr @@ -976,4 +1362,103 @@ def gettz(name=None): tz = tzlocal() return tz + +def datetime_exists(dt, tz=None): + """ + Given a datetime and a time zone, determine whether or not a given datetime + would fall in a gap. + + :param dt: + A :class:`datetime.datetime` (whose time zone will be ignored if ``tz`` + is provided.) + + :param tz: + A :class:`datetime.tzinfo` with support for the ``fold`` attribute. If + ``None`` or not provided, the datetime's own time zone will be used. + + :return: + Returns a boolean value whether or not the "wall time" exists in ``tz``. + """ + if tz is None: + if dt.tzinfo is None: + raise ValueError('Datetime is naive and no time zone provided.') + tz = dt.tzinfo + + dt = dt.replace(tzinfo=None) + + # This is essentially a test of whether or not the datetime can survive + # a round trip to UTC. + dt_rt = dt.replace(tzinfo=tz).astimezone(tzutc()).astimezone(tz) + dt_rt = dt_rt.replace(tzinfo=None) + + return dt == dt_rt + + +def datetime_ambiguous(dt, tz=None): + """ + Given a datetime and a time zone, determine whether or not a given datetime + is ambiguous (i.e if there are two times differentiated only by their DST + status). + + :param dt: + A :class:`datetime.datetime` (whose time zone will be ignored if ``tz`` + is provided.) + + :param tz: + A :class:`datetime.tzinfo` with support for the ``fold`` attribute. If + ``None`` or not provided, the datetime's own time zone will be used. + + :return: + Returns a boolean value whether or not the "wall time" is ambiguous in + ``tz``. + + .. versionadded:: 2.6.0 + """ + if tz is None: + if dt.tzinfo is None: + raise ValueError('Datetime is naive and no time zone provided.') + + tz = dt.tzinfo + + # If a time zone defines its own "is_ambiguous" function, we'll use that. + is_ambiguous_fn = getattr(tz, 'is_ambiguous', None) + if is_ambiguous_fn is not None: + try: + return tz.is_ambiguous(dt) + except: + pass + + # If it doesn't come out and tell us it's ambiguous, we'll just check if + # the fold attribute has any effect on this particular date and time. + dt = dt.replace(tzinfo=tz) + wall_0 = enfold(dt, fold=0) + wall_1 = enfold(dt, fold=1) + + same_offset = wall_0.utcoffset() == wall_1.utcoffset() + same_dst = wall_0.dst() == wall_1.dst() + + return not (same_offset and same_dst) + + +def _datetime_to_timestamp(dt): + """ + Convert a :class:`datetime.datetime` object to an epoch timestamp in seconds + since January 1, 1970, ignoring the time zone. + """ + return _total_seconds((dt.replace(tzinfo=None) - EPOCH)) + +class _ContextWrapper(object): + """ + Class for wrapping contexts so that they are passed through in a + with statement. + """ + def __init__(self, context): + self.context = context + + def __enter__(self): + return self.context + + def __exit__(*args, **kwargs): + pass + # vim:ts=4:sw=4:et diff --git a/lib/dateutil/tz/win.py b/lib/dateutil/tz/win.py index 321602052..9f4e5519f 100644 --- a/lib/dateutil/tz/win.py +++ b/lib/dateutil/tz/win.py @@ -12,7 +12,8 @@ except ValueError: # ValueError is raised on non-Windows systems for some horrible reason. raise ImportError("Running tzwin on non-Windows system") -from ._common import tzname_in_python2 +from ._common import tzname_in_python2, _tzinfo +from ._common import tzrangebase __all__ = ["tzwin", "tzwinlocal", "tzres"] @@ -41,7 +42,7 @@ class tzres(object): Class for accessing `tzres.dll`, which contains timezone name related resources. - ..versionadded:: 2.5.0 + .. versionadded:: 2.5.0 """ p_wchar = ctypes.POINTER(wintypes.WCHAR) # Pointer to a wide char @@ -112,13 +113,18 @@ class tzres(object): return self.load_name(offset) -class tzwinbase(datetime.tzinfo): +class tzwinbase(tzrangebase): """tzinfo class based on win32's timezones available in the registry.""" + def __init__(self): + raise NotImplementedError('tzwinbase is an abstract base class') + def __eq__(self, other): # Compare on all relevant dimensions, including name. - return (isinstance(other, tzwinbase) and - (self._stdoffset == other._stdoffset and - self._dstoffset == other._dstoffset and + if not isinstance(other, tzwinbase): + return NotImplemented + + return (self._std_offset == other._std_offset and + self._dst_offset == other._dst_offset and self._stddayofweek == other._stddayofweek and self._dstdayofweek == other._dstdayofweek and self._stdweeknumber == other._stdweeknumber and @@ -127,60 +133,58 @@ class tzwinbase(datetime.tzinfo): self._dsthour == other._dsthour and self._stdminute == other._stdminute and self._dstminute == other._dstminute and - self._stdname == other._stdname and - self._dstname == other._dstname)) - - def __ne__(self, other): - return not self.__eq__(other) - - def utcoffset(self, dt): - if self._isdst(dt): - return datetime.timedelta(minutes=self._dstoffset) - else: - return datetime.timedelta(minutes=self._stdoffset) - - def dst(self, dt): - if self._isdst(dt): - minutes = self._dstoffset - self._stdoffset - return datetime.timedelta(minutes=minutes) - else: - return datetime.timedelta(0) - - @tzname_in_python2 - def tzname(self, dt): - if self._isdst(dt): - return self._dstname - else: - return self._stdname + self._std_abbr == other._std_abbr and + self._dst_abbr == other._dst_abbr) @staticmethod def list(): """Return a list of all time zones known to the system.""" - handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) - tzkey = winreg.OpenKey(handle, TZKEYNAME) - result = [winreg.EnumKey(tzkey, i) - for i in range(winreg.QueryInfoKey(tzkey)[0])] - tzkey.Close() - handle.Close() + with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle: + with winreg.OpenKey(handle, TZKEYNAME) as tzkey: + result = [winreg.EnumKey(tzkey, i) + for i in range(winreg.QueryInfoKey(tzkey)[0])] return result def display(self): return self._display - def _isdst(self, dt): - if not self._dstmonth: - # dstmonth == 0 signals the zone has no daylight saving time - return False - dston = picknthweekday(dt.year, self._dstmonth, self._dstdayofweek, + def transitions(self, year): + """ + For a given year, get the DST on and off transition times, expressed + always on the standard time side. For zones with no transitions, this + function returns ``None``. + + :param year: + The year whose transitions you would like to query. + + :return: + Returns a :class:`tuple` of :class:`datetime.datetime` objects, + ``(dston, dstoff)`` for zones with an annual DST transition, or + ``None`` for fixed offset zones. + """ + + if not self.hasdst: + return None + + dston = picknthweekday(year, self._dstmonth, self._dstdayofweek, self._dsthour, self._dstminute, self._dstweeknumber) - dstoff = picknthweekday(dt.year, self._stdmonth, self._stddayofweek, + + dstoff = picknthweekday(year, self._stdmonth, self._stddayofweek, self._stdhour, self._stdminute, self._stdweeknumber) - if dston < dstoff: - return dston <= dt.replace(tzinfo=None) < dstoff - else: - return not dstoff <= dt.replace(tzinfo=None) < dston + + # Ambiguous dates default to the STD side + dstoff -= self._dst_base_offset + + return dston, dstoff + + def _get_hasdst(self): + return self._dstmonth != 0 + + @property + def _dst_base_offset(self): + return self._dst_base_offset_ class tzwin(tzwinbase): @@ -194,15 +198,17 @@ class tzwin(tzwinbase): with winreg.OpenKey(handle, tzkeyname) as tzkey: keydict = valuestodict(tzkey) - self._stdname = keydict["Std"] - self._dstname = keydict["Dlt"] + self._std_abbr = keydict["Std"] + self._dst_abbr = keydict["Dlt"] self._display = keydict["Display"] # See http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm tup = struct.unpack("=3l16h", keydict["TZI"]) - self._stdoffset = -tup[0]-tup[1] # Bias + StandardBias * -1 - self._dstoffset = self._stdoffset-tup[2] # + DaylightBias * -1 + stdoffset = -tup[0]-tup[1] # Bias + StandardBias * -1 + dstoffset = stdoffset-tup[2] # + DaylightBias * -1 + self._std_offset = datetime.timedelta(minutes=stdoffset) + self._dst_offset = datetime.timedelta(minutes=dstoffset) # for the meaning see the win32 TIME_ZONE_INFORMATION structure docs # http://msdn.microsoft.com/en-us/library/windows/desktop/ms725481(v=vs.85).aspx @@ -218,6 +224,9 @@ class tzwin(tzwinbase): self._dsthour, self._dstminute) = tup[12:17] + self._dst_base_offset_ = self._dst_offset - self._std_offset + self.hasdst = self._get_hasdst() + def __repr__(self): return "tzwin(%s)" % repr(self._name) @@ -226,27 +235,28 @@ class tzwin(tzwinbase): class tzwinlocal(tzwinbase): - def __init__(self): with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle: - with winreg.OpenKey(handle, TZLOCALKEYNAME) as tzlocalkey: keydict = valuestodict(tzlocalkey) - self._stdname = keydict["StandardName"] - self._dstname = keydict["DaylightName"] + self._std_abbr = keydict["StandardName"] + self._dst_abbr = keydict["DaylightName"] try: tzkeyname = text_type('{kn}\{sn}').format(kn=TZKEYNAME, - sn=self._stdname) + sn=self._std_abbr) with winreg.OpenKey(handle, tzkeyname) as tzkey: _keydict = valuestodict(tzkey) self._display = _keydict["Display"] except OSError: self._display = None - self._stdoffset = -keydict["Bias"]-keydict["StandardBias"] - self._dstoffset = self._stdoffset-keydict["DaylightBias"] + stdoffset = -keydict["Bias"]-keydict["StandardBias"] + dstoffset = stdoffset-keydict["DaylightBias"] + + self._std_offset = datetime.timedelta(minutes=stdoffset) + self._dst_offset = datetime.timedelta(minutes=dstoffset) # For reasons unclear, in this particular key, the day of week has been # moved to the END of the SYSTEMTIME structure. @@ -268,12 +278,15 @@ class tzwinlocal(tzwinbase): self._dstdayofweek = tup[7] + self._dst_base_offset_ = self._dst_offset - self._std_offset + self.hasdst = self._get_hasdst() + def __repr__(self): return "tzwinlocal()" def __str__(self): # str will return the standard name, not the daylight name. - return "tzwinlocal(%s)" % repr(self._stdname) + return "tzwinlocal(%s)" % repr(self._std_abbr) def __reduce__(self): return (self.__class__, ()) diff --git a/lib/dateutil/zoneinfo/__init__.py b/lib/dateutil/zoneinfo/__init__.py index 8156092ef..7145e05cf 100644 --- a/lib/dateutil/zoneinfo/__init__.py +++ b/lib/dateutil/zoneinfo/__init__.py @@ -6,7 +6,6 @@ import tempfile import shutil import json -from subprocess import check_call from tarfile import TarFile from pkgutil import get_data from io import BytesIO @@ -14,7 +13,7 @@ from contextlib import closing from dateutil.tz import tzfile -__all__ = ["gettz", "gettz_db_metadata", "rebuild"] +__all__ = ["get_zonefile_instance", "gettz", "gettz_db_metadata", "rebuild"] ZONEFILENAME = "dateutil-zoneinfo.tar.gz" METADATA_FN = 'METADATA' @@ -72,17 +71,92 @@ class ZoneInfoFile(object): self.zones = dict() self.metadata = None + def get(self, name, default=None): + """ + Wrapper for :func:`ZoneInfoFile.zones.get`. This is a convenience method + for retrieving zones from the zone dictionary. + + :param name: + The name of the zone to retrieve. (Generally IANA zone names) + + :param default: + The value to return in the event of a missing key. + + .. versionadded:: 2.6.0 + + """ + return self.zones.get(name, default) + # The current API has gettz as a module function, although in fact it taps into # a stateful class. So as a workaround for now, without changing the API, we # will create a new "global" class instance the first time a user requests a # timezone. Ugly, but adheres to the api. # -# TODO: deprecate this. +# TODO: Remove after deprecation period. _CLASS_ZONE_INSTANCE = list() +def get_zonefile_instance(new_instance=False): + """ + This is a convenience function which provides a :class:`ZoneInfoFile` + instance using the data provided by the ``dateutil`` package. By default, it + caches a single instance of the ZoneInfoFile object and returns that. + + :param new_instance: + If ``True``, a new instance of :class:`ZoneInfoFile` is instantiated and + used as the cached instance for the next call. Otherwise, new instances + are created only as necessary. + + :return: + Returns a :class:`ZoneInfoFile` object. + + .. versionadded:: 2.6 + """ + if new_instance: + zif = None + else: + zif = getattr(get_zonefile_instance, '_cached_instance', None) + + if zif is None: + zif = ZoneInfoFile(getzoneinfofile_stream()) + + get_zonefile_instance._cached_instance = zif + + return zif def gettz(name): + """ + This retrieves a time zone from the local zoneinfo tarball that is packaged + with dateutil. + + :param name: + An IANA-style time zone name, as found in the zoneinfo file. + + :return: + Returns a :class:`dateutil.tz.tzfile` time zone object. + + .. warning:: + It is generally inadvisable to use this function, and it is only + provided for API compatibility with earlier versions. This is *not* + equivalent to ``dateutil.tz.gettz()``, which selects an appropriate + time zone based on the inputs, favoring system zoneinfo. This is ONLY + for accessing the dateutil-specific zoneinfo (which may be out of + date compared to the system zoneinfo). + + .. deprecated:: 2.6 + If you need to use a specific zoneinfofile over the system zoneinfo, + instantiate a :class:`dateutil.zoneinfo.ZoneInfoFile` object and call + :func:`dateutil.zoneinfo.ZoneInfoFile.get(name)` instead. + + Use :func:`get_zonefile_instance` to retrieve an instance of the + dateutil-provided zoneinfo. + """ + warnings.warn("zoneinfo.gettz() will be removed in future versions, " + "to use the dateutil-provided zoneinfo files, instantiate a " + "ZoneInfoFile object and use ZoneInfoFile.zones.get() " + "instead. See the documentation for details.", + DeprecationWarning) + if len(_CLASS_ZONE_INSTANCE) == 0: _CLASS_ZONE_INSTANCE.append(ZoneInfoFile(getzoneinfofile_stream())) return _CLASS_ZONE_INSTANCE[0].zones.get(name) @@ -93,8 +167,19 @@ def gettz_db_metadata(): See `zonefile_metadata`_ - :returns: A dictionary with the database metadata + :returns: + A dictionary with the database metadata + + .. deprecated:: 2.6 + See deprecation warning in :func:`zoneinfo.gettz`. To get metadata, + query the attribute ``zoneinfo.ZoneInfoFile.metadata``. """ + warnings.warn("zoneinfo.gettz_db_metadata() will be removed in future " + "versions, to use the dateutil-provided zoneinfo files, " + "ZoneInfoFile object and query the 'metadata' attribute " + "instead. See the documentation for details.", + DeprecationWarning) + if len(_CLASS_ZONE_INSTANCE) == 0: _CLASS_ZONE_INSTANCE.append(ZoneInfoFile(getzoneinfofile_stream())) return _CLASS_ZONE_INSTANCE[0].metadata diff --git a/lib/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz b/lib/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz index 83b2e05188e7f27231a5cec913b921b6912af3e5..1d15597b4630ade143d8408477858d1ba18dc707 100644 GIT binary patch literal 139671 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∈v?h+!|q+%B@ju)y80Yets|8{bQh<OdTb znXvI?dL}<u6q*qfJ!zexYv+#olfR1$Lpygk?-D9(pQ8GRFVp*oY|wP2CHjID9*IgH zCLD9Qp*Xd3gR|9T-^M9whxjsmTV#V~b677i7cBEARC+Vvnad5utDPI1cL^u9Pf<U_ zm+9Xk8#KSeYKgsT149o7v&#)eu8s}NG6IS09twx}HaN7%E@<SiKENzk*1+LZ-N32z zhC$Tj2BTib2IdrjMD`mBhxjfWY^h~(ZxB#=#h@(0=rv=*@=K;pT*77SvC3Nv67xL; z727y>%e%HnslCyC@!$DG;-R?ab%IqLE13TzxNs;f;xlj&T+q>BbwJRyK}G2egSX2K z##tR3m~8|T**laD@fo<b$S&yOus$FvSk|DU^oAkY<p$%f6=xV~1n)9%C@<nOa1&h6 z(_wW$(zU@v=?z1+%MHd`9UGYUfb=UL;xlk>kzLToVSPYWu&lvG=?z1*%MHd~9UGYc z2qv;~s4U_$=nz~up~LEcqHBXo4@bAl4MwTX4a_z|iR>LJhxiOUTVxkZ;;=rTDp=Ow zqx6Phw#yAhtIiF~HA0E(9IA`>47^rzH3TWWVpuN17}U9fc});YhUy`{4_+;@3ez~O z59kP%HAE@BVc70+gR!V{1M?fMdgnV|Tk~029>ptt<Wqg5o17!|W^T&9x&7w~or5MF z_u2FK&-?2?>MyMQz2Dg><^8Y!dh7moiwf@jQTXsfF2mo)b;tjg|1JEuXVHIN`^Nn2 zj@-S!`QLqVcR3|1R-fnnpOc4s+TLI1<4gZ5C;vK8{_Ed=$N%pgEJG$RDIZr+>5-am zk}~7_zxxk=-;emV;kUe6_51(v75|xS-u+{*eE&bb?Em`@r)#AD?tgH=;r-wLtKa=U z{MfDU^O|4sGBR!7|NH$<y7$}v`~LSUmW2E`es`ntgFNQ{&4vHhfB*Nt^!5G&3wE#k z&vJLsf0nB6_0@C!1qWO>ug1anC2Xhe-{;TNKjiXm%uSdO&hV+{5C4}9f4={+uS)o* ze#5YqIp6+&z76BE`h?pD9P<BFyZt*YH|_tufBo#w{ztH04&V3p&;E}rGyWIdcF1Qw zVf<--Yk~Ra|I_~E7i_rx|0?t6`mV0Gx5c*4|8G8z;rD*sTiZS?+n@fVe#i2s^)t-> z+v@*M&z!OEAw>SO{hr(%=KpP#|Ie<D`d9te-p1g6;RzFmyt>~%>mPF({GYu&_ubk0 z^C$ir_@AzS|4*O)%>Qy8pZgnbpZ_oI-|$oZn%SAz|L0Bozi{5P|Mxz}+Zz18Ze>{O z4D&+9?EtV3+AE^|Rsa4kH{<{OT?Vzzc~BpE+!g>!vx6)LOW)sNSlbNKT@d|#oiG2% z{|f%6>+}ELXa4+uL+8ui>d*e?fr2L>`u&gj?N90#v_Gw%u>Jr0#-H`~4t@D6{JGwI z9>Zt*4QtE)oHzbt|KQ!T`U%tj|L1>VKlhxH2`Kb_a@IsU=SV+bWjKEVZ20<r?;C%< zS5N%KcDL@|{R*Zt{}*gyjAMEr`BVN)=Fj(U|5qpcdz|^JH$VRW?w_0n|C?{KRj@jo zKk;7x6e@q_vp=)H^~!YL-+%Ydf&%4|*@4>(UwZ!Vf8Fxu`#F&2@@>h#+U`NkzOaq) z9-{&@Ou<&)cR%=rcQ-U%6LXpOFdvvV@&8#{o&S5E+gB(3;}0{eoqZpw{aG&a9_a=f z<^P9mb^q`ETrW4{|Eji^(y$mie4DMpoT1w7-}Vn%{(S%T-`3#&<r1I!&;;>$zw2#= z_lyejCjKw~{%8Gz==1+Sfz@0*x~K5L{15dzKe9~t-&h1vlpr_he<L{Q?Pq`XU)#^; zzP;i9a!{%${CD1(;f3T+`6r3c(9thW{PnN%=YDXaVEOr87VQ6T8=x+*FGxm6^T4D( z!lcb-&)8R=^zZ#K;e>zf4*#mz8Qxd@`~LRE^S*yQ_V50Sh)8w)i!c8r{`r~x{r`5d zwr~EipI*2y?*IDz8~#<__{%@BdC$N7^(Wi+md`J!UcS5PzjM>!Lv?-s8yDXDdjD?B ze^LEENBS*W`;UHE8hGqb>D1EXD&^#h+S9+}8n2eNejOK5e(b8a{&QR1SWCl@J+me& z#GdXF`fRMUv?;aMC9%$V<qhN1qt$9tCccxs>!2_9Jb%iIm#e4e#NYZb(f_sG`BD?F z?)X<rr`Id}jPl#tv-6V1?&{qU=e{-t*1h%Gcqm$1<>OQFBVER&FZXpOAN@V~n8(y# z-$k|EzfY9QxEQnMl7Y{vsO8(^)|P~>{AIha>g#*w9;x<KFY2u24YKlnc&^{7b$-R( zjP-g;&8ErC*%Y^4INEQObxr?|fESi4<EHl?{WZ1R<6rM3*Z+Tv-JIinb7uUWdhx}R zgpGyIv=)1o&61m7cROQO)Wz9LdySvml>Ktxi1^Y48~I-zoTwS(@qSX(iuyGoVkJ)b z?}YVQe@_)S+#fzYD#lW8+nuV=tgYXTH*Vd$>SEem>!jO}(|gOf=LzSY{N3{{`2Hp9 zFDFc_j{T2*f8~sUl9lZ;_m~sYl$igix}~k3xaf4R$l~zxlXuN|saIB8y7gl2^#a?p zOEQapv)}z9$9zos+fLuF^Va^B-W%k)cYD0{V*mT^zuJXoOKn^j|9Yc)v{zr$bJycy z_ABe_w*C0<sOIIwJEo5sRxbOJ@yw<$;g4Hpz%Q4;&XcB((#~9DXW!AI*!lSDkD~R` zoD=_kZ7~k+b@?Aw_2}C|w@LoKwwaYv4F5zl+9l;RnOu6SZZ}Ozvo~s{zS?oKDOVzw zSy-*<F;S~rY_aW=q~_A&Nrp=oY?&za?ox2EzohW0ee3*B+{yDfGkcZ&ynUA{l+st& zX})EBe6mNwHuph9@~1y;Rt7Ce-|s~Gd{|*$;a6!`roF)VeD~Bv>r(|a7!6pf?SE|f z{*(XYPi4;#2KgBt5{5GklAIEcEIHKC*(x}>TS-O2&Bv%`MuvppO@pLEiA$Co=J&Dr z)9_?*ypWK9vYMiskIOGsMM=XXr=%lGj&yW-3QiJMR?~3x2<n-!Lc%c8FzHazktIhv zI=clYiz}-cxcMaY%s3%oIMXo6Df!5fV;!Bnf|I3{)hyh6ih5?e5K+8om~<%l$dcn7 zo&AE7<&~Kj+*lZUI9MbEc?^^k5?lffbR1|9Jjkfb#Nx`p*~1|s!l+}Ql#t+Z;XucM zM!|#3%1j(?EP|^<8B`<$eGHTqB)9||>^RUQc#u_@iN}pavWLS&gfYiJ=|O_ag@YXj zngtKCD>DhWu_*R%xJU^07$_+ux&$2RIM5<^uv4s)AwYz2iGfl;Aj^hB9SyF64&2I2 z;%+SZJsb%Vf=3LLCM3ETEOzB!kaA-&?cvCg5WKU(gYiHh%Y?%n4(@^n_>`ICj_THx z^d4vzJjk!iq~ONl*u&8xA;@E>q>$tiaHQivhu}d$WhNCj7SA4zDI$zIhDr%REEkS+ z9Pku8Ags)!;l>i!!?8p{(8o||L6S?r(T)RMf(J#FnRMJ(B6~Qth;UoP3l?ZHy-0F- zu$1FLw;-d0a+84@OHvQVF$uv5hDsXAe{M5u&VTr6o2O5~<ps-%CeJu<U|FN@US)N2 zyQHe`nR1UiU)R6?$Il=0fBO~jb)PTxysZ1RD16`LZD(~>fBv<9RRQR*rl&2=J7!(> z-!tuM(24Uu?7yvJ(d*jvmp@J;E_+#}LSB}jmXQp1g&9Z7iW6NcDpWQf>RNrGFQ(~v z$g=f&_wHP}a_`i2X{pPz<q~JDOD~(DaWngwVOd1|X(?-8-hbymS35ZBh_=S&-Z=lR zT<p}`x%WT+wY&ek+_yaTq*ne0h1^oP;_jVM!T+vYop;A=U2<0L>g+uV*M-mBw)vU7 z>3KcRHa4r}cOLYbTIrXz-U*1z%l>jRY=cn$jafM{d}4>zUAFzq5z*dWTK;7AL8B$F z-pL3uhdpGBJ^ijOLGaz>t*-=hx92@rnjL&PJn!@6z1z0Mn@rfte`WjD+u!EC+5BwE z+P|OnZnE1x_uaepu+y?_(_UEl{on}t%X!!K@BJ2${m#$1vz|xS+<EqVDyL@c?6sd) zx_CU*zp+cqbJPDz?zwlbshwanzMS)?Nx;$|cblHZ>ZLL=#(v_H3c}2Oe7oGrCOhr0 zjrHq8xBu~J?wq*p_s{bRqW>4X{`phQ<6zvjd$Z@YU-`ZF?Z5PN$5VfAh#CHl+?l`U zu-Hs1=RU5AQ)04Hb=?2&@fLq<FB|mg^sXDnk`<<&yZqH&*MY0{qS=Sb7mMzE*}2V8 z`qzn%i*?N&EA6$}ah)OBDNnbuWBY#H!WZF1k6M>)OKiLQX~X*6uO`*(YYDmGV!o(C ze2GJ-`KueJC;Z}jA$P}!Kj@xS?59he6JBxUF56lBbmPDLn?aX!in1QJniZd6xLIR$ zecH3v`pe(?Zi=*-eQKI{)eW^dHp>1nCNb0FZm<M}mB${-5)BHCo@?@-!Tjcj<$9NQ z@daHjj4k`4q__LW9$oW4x|ZK>1V+cLZY{buZSA%{n|FS?vh;NBWj@uDm(%T%W(Qty zo@aS1XZF@3(PfuSKF_+_{4VTBUR6^jTb%U5fTo0^!U<`Q@=pB!zG343?^90xufBca z|Nr?u|MpK@yXb%Z(TV@hU!3^g|Ivy6{MRS`e}8b||M%;Z|G)R0&-El(os%WypS*|1 zzx};F|Mr`n{_WxM?0@5wC;$6TKKXw#UD@&E|MeFq{*PCj{69WV`Tzc@%K!g&^#1u@ z)c0q9toy(Jf_;DfAGZ4RKdtXi{i?n{^*rwX{xiz{thev`^S{>W)BlS`KkM~nf7Zv( z{PTaW*{A=!DWCp-&3*EJ|KyYZ=Wm+$|GxI*|M|<6|L>o**WFOz|9?UEfB#$h{`{XU z^Yg##li${x>)Se6e*X9A`}5yS`e(h@0u8Ar|5a!H`M=og(|=B*pZ`syfBqMk`R9L* z?9czxjDG$vd}GVS{qujF)u;dMsh|FbTwi!$!vE#F_*mQj{O7m&^nbPmSdd{NNU*iz zkNwqB4UZ@PSFUqTRQms4<Y%3L#;5<gPke6<nE1bbW{o|E*w6o!R-gVex&Qm`D*Y4W zq5nJ0KK-vv`Sf2*`e*%KpMUiYy?^R;Xa3o5|FcfO;?sZMli!;!O#J`eYo9zz|DSqc z*`NPoWq#I&y8rv1+V=+(JpU_Ye*Q1D`t)De^3(rTvrqqvQ$PLBzj@++bjqjy+$X;` z2Tb~ZUiChIljlGC&Ki3T^`HOc%s%~3Px<u!aOso(vkRX5-~IZ;|J5=->mO-zX@CCD z_DGvc=jZ>&PkuXQNQw!^2Ns<CFRyx^|B&~;`qjOE>eu=FoByNB2~0Nm{JWo{{J(xS z$L8>rd$0caAOF{d&y%OO#QDGclC}Q}od4esjr(>^ep0QUaOKZ>`{{On!Z-T-+uvCA zE=Q{T-|dL6hZaV>J8v6%<x!Jf%*j>9w6-rXywNXzH7dA{<@QC7RmoWz*G>N^Tov{^ zJ+Efrlb`bOI~(8Wt~@BM->^19!!$(fM6X@(ir%@FMU#8|SNg_Ra=mfhb}J{&>_%H+ zR`F?r4crg2x^EP0;LN?U`_HWgTaL+5*Y`7QTd?)Yv*lq$w}qdpzW@9DN!jf_FT3o% z6K|*IPyMpfSMja>bSoZP=9m9sw@zNjap{lI*Lz~G75CaN`6c##`rkc4A^VfFpPpO3 z=&D}yylK0|dZ+%G?l*Dw+TKY_X?+uZvVQk!%-lI)-D~fIhMp6a`Fpb6mzyxpe$pYE zy^6Z^tJ~)5w3Ym6yp_D2Df36^*6Xjhi{AVCuc&<{Kly!qSP<*#;Jz7u<yYqa;j%Qk zs^!XhX4>M#Y~B~`_w#0!PLc}#!n5RZg=yJa6OYA>uNt0<oo2R|RXOT=emZ;ZgQY(% zE>3y8$a=}ay;Y|<E%$pk@40=t)7gHikbcF9!#u2)Hmt8zkgZ#!_V$p;4oBIYpSV5h zy{}x{T<&yh_NqS5TFy&%GeUcA>&<@l%scsS_$kfI#Y?UnD^4l)$v;_BwP((=+i_mE zZ^v(|sPdR*zU!2*U!A6JjL7Mw@3jNfpO!V2lsawY%jU_<YPY)iL3zn?@vCdZek<yp z4c*-Be|qOb?WK2;e?Qq1R=i}7ZuXNun;$P-=`DWZGT+zf%g&#fm@ZN>HLXul?|jrH zm!iuin=d|eEZ0skw>@mayuLR;!_i=;hllY*^C$mz8_n-eQTl)1MeYB8%j*6kt6uz< zUh(ArWs6V$H){R-AJO-xUVQ4GdhxzL^~pve4tAgZ+pl`^zy8UI|K^iU{5PL+^8bH@ ziT~|=_sMs7{+nN8CN$;Ge{Pwd|67HA{%1A%S>NvU&)!|_|Nkjo|Nd+C{i)yP_0N8` z@_&0D)&KTA%KzopP5Qq)mt(?>C;x93Jo$h4#&gA!C;xAscH;l`r1|YF>Hp+C{Qv!5 z;qvc)htI$I)h_?`Yr6ld-|6!2Kd3y5o%*L<(EVTiOs{|Swm<6xI6nRVo%-p&rq<8@ zTaAAH&zSn>|7D|}|2GQ#{I6;B^S?&tpZ^@7;!f!2|Cqi%^|hUU>V@3@{h#9UZ$G>H zzyHde;6lfQ$3G~xy~+7s{KFIf_n*7B_{W6*^-ik){Yz!J1b^1AdL1y~#Q)&f_CqfJ z>dhd<QvT%k><g9t|L6HxC!q5Q9D0pfKmQ*!`uX2c=;!~MnSbo{C;pdLo&0~lv(o?j zv-i3uD*wMfug0E3=4U<sr@sm%PyW}Q_}=_r;(zr`3qMT!|G!E3|9%(M|NAE^|F;iS z{r_Lc{a^hSuYdKseg4_UEB)Uer1sz5OX<ITdyPGZ>d*flpZs<-IQjp(`hEUGp8xi* z_WAeUyYo-I&Tku$pilo}Ek6BM5Bc=}NVrHCn2h-J|2L>Cob+FP-=YVT{@-`sC*Lyt z&wr_zfBsi={;~fk+ByS7vd;YTzhLH{|G7$M;+B33`~1J&@_)9+BE?&>fA??ucbNC@ z{{Ag*=Dv6NE#nvU@BZFL?f-*RpZ!1j`P9zMp09r9S8Iv$eGT}3=dz!s-iI*nIK99f zM~|*Len;4At!7tLw3gWGj(V@s-mVCL3%$xuw_VTw-!e_=MOYW_y@(Z6D~pe`omw2U z_<Wc~@7&Cw$-SGWE?aN2=+!*Q<tDS!!``2GzuaW|krm~AFV<}7_!=BtmcQiA)9V4b zR}cE<#QxuMYgw4{#<Tu$LAQmMyT1Q>-}~*hx5Bm8YIL^7?{mBIyy(?keq8};p1)3S zEfTk0I=LZu@AChXPwmx>3#zqF*W7p1TE%|-!mj%p#h0F2erSqb^fRy6ZXwtIOT|{+ zz7=4Y@Qk4|{(={ek9Tmh(fQ7Uh8wdy4hdd-aNCua_m)yyrCHeF+j3!t{Qs>ev)>>2 za%1)WTc1BK^;&<`-ea%%Nw4`oTQB`R-xtdAIy&gco5-a5KU#xQR(bYtotV04vC`2O z^TSsf{yXXAb1^w9=P`$BS?;b||4ZCc4-|i1(m0<h>CTtVOAB~qkNmh;IH@pDwy<D5 zgQnv1B^F0^&zxdW(m$i(UwY<@hoOZxuKQk9nSa;o(mCT-9gjj!otWzSYDIFt=Hm4A z;qrGTt?TiOU8ANiyQHJ=waVo?osS~^y9Rkh&6#n_!r$mPOUJXz%1ce|rzPFG7dL6? zz4MEn^2%o(I@$c_h^y_DFSY4YJa^v9I`&HGsMVH;sn6Djt4WKPUfvaPGHYY(*}n7P zo4WhGcRuVsawqxrl09LSN%y9SFZr`s`RGb-`KilfU#a@dpQ(0M<AY#&U+2^`-G?sb zzQ*q@ZC~UaeY0%&jI;+^1-Vo?)Wmr+)qb`8+28N-@4xlLKlbXNhRLM=;Y+#x$#b~< zv-ec}e?M69|9xlG|M&eA|Gz(b&-l?p{t5km{&Tke*?;JsvfxclCdrfkwUxkiaN`VE z+ets<<p1-6C;!`PP5d9faKeB8$tV7w4?FSSe$k}=^2;auH(&ka|8LDF|C2*L{hurP z>3^Np&;M>*KmWIB{j9g2_~(D-)Iask6aVa=?(*;d7LR}bt2_Vvx10Fq|8b$8|4o&C z*88dc-|wLE|9`93zxu6C|LT`}{rm6P`scs*li!XCC;q>maN<9I=!yUPH75S&4?gk# z{L~Zw)w`bj*A@EtKVagY{|udf>a{2Ssh9fwJuu+J|KqEk{O|90@_+Y=C;zv1fEz^% z6~P4(-_QSlwSN9z$^|Z%8o&jUlgq#PudGB=!0jo6iQonclk2~F(Vuk!TA%*+PkhgQ zQ0f2tb(8+TUoqkTeD#U{#a%xA7gqiBU)JT*|3aak|E-jMf}HT*bK;+R<4=DT3V0l! zocO<AYtsLGuYLF1x#UG`KmA`V^69^;*3bXPMLzw19s242ORk^)k81t=@5uG@f2q*V z|D8%d|GNtP{BJSwPrcmFIsvaw|CLXEZ%&x>|Gn!z`IgC$SY6=pZ-2YjzyCc>|L#Yt z{*OO$Z}Eyr|KrzB02NIKCjGzfw@<!h8n`f;{^!5hr@sn&p8SuWaN>Wz$jSf0@qry+ za>B{~$Gx8X&lgVornPnZtpER4&#!Y{Ro{8_>h=1#egBjVR6;)gU-#vIec}J(IvaJ8 zR#p5z?)|?(ykXnV$<}B77hC>kK72^;`F_QK+5exn{i(kfCT1A=<p0w82HvyTU&@>R z*>8Vhzy9NW-k<#YcAF%vI`QB8Kf~r{QvYNB3LBoBlj&*58e;zcFi+d-oBtJ$F#Nl} z;N$*dx9Vr88lRo?aM7>x1wa4YKe+JL-~BPG{%`e~|Fr-0=?|L}bbge+X}>wQqv+qf z$*YYG^d@#|lnP$|yngQ986sX@|Ng~)ZDP)yz2Mc;{}VonX<nXXbW!xr`ZfRC9yr|j zpFTnJy2-&+ANQ->x<6%Nu2<h#qh8bHyZ8M)e5mQrg1?W?R8AAgc8I^sa#<leZChma z|E%o)EZNgEF9+Gi?DqQ2&ML*E{r3OjxBp*W`V+tZOQ-!_@BW)oOGW+9J~#LA7Py!9 zdEep#>H7jcbROAXe4XdH_3QY89IM@KH}<@!|Fe63?bADxo<F^@Yvt4jGM4u@zWw>U z^{s9B2kSI3tMaP<&u*w*6D$)ge|Eo{qj=x)HP61=Yzg?Gkhk-3v-~p)`#$Z5_GNR* z#C}T3%y(I4RsS+}{{PgQ`RC$aFZ;Zo^<~R_KfU5PxpFUUe#Z&^Kdv9#QtZ00@3S?} zIquo*AL{qbQxLIFf8Z_0FK840;{A&IC$rw~DA>Dra^Awf^ES+v`MzRj+5O$KKkaMv zEVEzd@}c<4hVrnkAA09`uDAc)c3W`Y*3HUkUd--iCPx0Mdmd(yWIp?g+}5o4$BXl1 ztF9cre(TMSB`NNTQc@MEheKojPMXx*D%b1fwJ+qu*967mY#Vl@mGmrA7wYwAvOD;f z=lEhRlgRm-)U@+A8O^Ug+}PpU(Q&q;!?&~J>`8%HB7(E0Dw^mjndvK;87i3>E18)p znVBn@St^-XE1B6Unb|9~F(|b$I&-kP@UXWD2z5w^bx26@DB1`Zx=1+9P)J&#khDV~ z>4ZVc2FD{89FKfR<WOwsNIW9o#3^{V^X#UE2}zrE6xQ6(;pWgaJN>A1SDem~5A26{ z*6X%OWixP?yYN`L@L0R>*tQAObx1VvDDnsziby(HC?$C)B}FJDWhf<8C?$0mwHUY_ z32;49@br#B+};P%Z|UvNoA<x>!TL2P1vO_rlRE!D{6qbRc=qQ1uV-h@%JSr2F>mvw zPxU{)^>0o~NuTBW^6E#)(2V~9d;Ui!EYUnXC5x-WEH!nuvGJB~>(6~NPhS$Xd{)e( z|IJN2)4%_3ZO!~`zw6Tf>Zx|8|Jch_-xvG0zqI4Q?(+7>`-(ID?A`6%)1S5k-22bn z_pGlYv35>s>4C4mR#wgX;9wux|8wQq%j@s@S8mby|699y^}{zCx_ECcU!$lU#+rZi z;wmQd6<kr5|F<9P>|pc?b+l&>WmRw4znaUSVCDkn4`Hi>@@8sW%VHIO`oHm-jCa^N zRolJK{_a<_`0?qFeA0zu|6^wTxAmU?<8#<c!_)t#zh~TN^t$}r(P?Lw@JsPevbPWk zPW10!p06*~9cDIxkI(i*?YlEU{W8%y^0i0RWN%N>m!CgfQ`6i1q`FS$C2`~G)A7gJ zyyAN^_3PhW_p*|_-2GeajQihf*V}*Z-)-?#q~_Pk`}+d+TmNMJ{&%_c)yuofi|li3 z760ytniwiSJ7lYA`i|!<+n&9dzvJ1OY3?r<m(A^NpDH)um!7TGzSTe7Jx{*+f9Z=R z|F-vn;k(|=-23XrmQ~g_y*k2l9>#5xnzgn^BldB$mF4Njvu5ny*7Nfz@8cP_j(*F& zd4y>yqu|v>Nz(w1wgp1<+fTBtVlysSAk(ozu49El#|ov66)GJo)H+sZbga<oSfSIg zLa$?mLB|TCjuj>yE6h4pSahtg>R4gZvBHkWu~8tfRUoiaAh1^;aH2rqRDr;mS!EOB zZmP-NQnF2(v(>fg&Hw9SFYg}f{<0;cZGrCR-4B1|JG7bVTb51G{#C#J%71yWp3n7d ze4HI;J+{s;^7-eyz-xAd>|%$g|MuDc|5*wbeOmp}c%9XM^*5h>+8@02OaJ@J|L-fK zzy2?md-vt<qvGSW_BP+Pf39D3Avwydf6<bZ-*V=!zP>Mg8u#CvdrMrs`HQdZ1t}bg zC#(J{>a4aAe<9Wxxwij4@8|lJ=O?XsIZfyKZ~GtXz5dFd`};xP)>8L>c>HhsgtKS- z<nm6PFX;XmpV-)@u=a2EroW4gH=VJ#dCK<H{tX8d;-dctPJSz&dDdrZU+Kv|>$T7S zKm0Y`bfbl9_Ww=a%$KTvH%{%_`Yh#I!Efm%{q_HO8yy{^{=Pr-Yk%C!%{eXt{Ws%Y zdvj0Q{PDkSdVBZN<G(h}-)VMDaD7$$+Vbz~f0gQgeOVQAI`gW&^;)jdxBQ!fZ$40& zllJqVr_J5{QYBCFKJC08WE~yzWWVNZlao_sCatRcyZ_R!{R++<$LhsD{g>bU&;LeA zn>e4@nooc3cfVel@$AVrm-49p+jp(L_PqS{`u~RoQaKW<4%arA3apQ+zpp#t|L<or zjMqe-6rAQ$?EC#c>&L0z>bW_&O*4Mp_x)4<{?g;L?cx9I4~L)s^?%pX>%sfwi%f0T zE?Ic}rd92!sO3qk;!Oj*zrLICO0#r##>)R^d3<$O-w*p~uT;Hg<u1F$T)*pCTc`E? zcUdyKc$4lj9^L=p`M=d4{o{X`clm!l2cPEk|M7EY2WP$Y|Ghs|oqu!6k{QOE?k4}y z56S*NEARi2kJmhx>1=;cFCj0nVrxC`_WuXwh;+~H`QP5g#+Lmy{_tD-1C!>?eX931 z|H7C3%Wm&quW4Mp$MUX?=KZH{<;6}ttIvEJpZvD}cl|;0GbTIR|8sM3UcI&7?e_gC znVy@|!cV7%Ed9rQ<wyMDxAK#dy-b%!7pr`q@n8B#_S^W)Z|6tmUanug<^OWW)&H}$ z{9U|evX105k?kx0H*Nj3-tD%%lJf0I(;nOX-CyzF9Gp}x7wNownRocVz}kQ7FMgAs zkm&^~2GxAl{QSnxw`<Q|XD6OpfBsAT{Mjw3w$*$7(|+lbA2uCW9)0}&w3_?<pI3wi zZTjjs$LWEN#%!kpdACeoT<YDu?*07r1<nRPB+MEg-?I1G?R}&7L;Zr%E=%K^GdhzS zB2CZy_xdG&MEb|4f9tJ|dS0$S^?#;i^6G3RkI--bIe9PL+OKxo{?ZitlTQ}?u4iLs z3*Yr$ef``2ul0WJ5BvG&|ND5K`$=|j`iXXN_g8&ccR~C)m)re<SKH6*4R7z5e<b#{ zd*=5^`#ArcH(45T`c41p&-N|rYyS5q<o?^Q{WE^~mrnaq?|$#U_1EJvP9?k9s`BoC zF52nqq^EgPO~HM;tD^hCCD-?-T$;G@@FL;$&zWCOtxa%^%uo(Uy|DHA(Nhd{A6x`J zrtElaC#_%cxnED}RR8~3#kwogvU@+<Yv?Sw^h@4(!nS|cr~b(g_?2=-%XT04&;5^9 z?p(kBQKA1q?etkzaetFseq^&N{k&do6ZhfRt(fD>;;)J4+}m~U<2&_k?rl2JZ*J_; z+ke1P8`NTPeRzAayJgv#6WsSJ>+_GVV|Q<RpLhS>$GG=#5365&e{?_px$wV#+wK0a z|Bn;<_wa(n@woYWA6C!zyH<byL*Vi6_NABS_q}^>@%P2@U-xIHTh-ZL53B$7+~xRA zw#ce&-=1Ib{>FW@_J@7F{O(mhex$gcT<H3}@SJ#MfA9PCt0tGUtS|m$yZiY^d0Ae! zV|T7+oDHm=oS*j5SAONv{mCz6_Eoy|{Cy|eS<^nR_KocH`lm0hRL%c4?_Z|qb<WDt z!k8HKl+WATdH(di|LxFVyy-r7Yv9@W#ctc>6MFttE;C+M7nSHU@x`0(xjBh%ZtXkw zuX>jVUxi+z#Ew;kiFdwmBwn~9&;8iFT%>2Rv?TlYRoW^N^#**Izk4T~5SJ1@zhA#% z%G#?d4$D0a%b#-J=K5q?hV`}E@ABX5@~c>TSgfK!tZq+xcF?z~4Fd7|Lu}q@Xj&F= z{O>xxVoKo$8LLJ21!|6_xBBv}Q?8nHYSHpNGoGKf2;U`A`R;XxL?DmiDgnby0*1Q; z3=atyp1Sc%rNd;K9p|Fnwf;>V$*(6XyaEZ?fQ0(5sp}^Fmvd8$p3gGz#5HwClTD?b z0uwtVrsl*7xu``?XSvwfA$Uq)(Ui6RO&+PYJsq|vB^@zpS&>?O*!hW3%Zt?NBhD<w zEgEh|JkmHWp1G##=(72%hrm_QYpRYVn@c?f9)b+AjGo>yvCm%O?GI+b^HJ=J=A{OA z)U*lw?Kr`3&ENo&OA3?HCPqQY#*Q@(E@=u%)&dh1J5C6>q;M&1YJC{Lww&pk!JeAE z!UmnG@(IF9MqGlCtsQ5aT*?et+BT-}>m+mdIJ<09RC+8hv9#lajZ2EWl97O5WJiaO zi;J0(lCt2$sU0VL#O@#PYda9|?%I+S;(y-eU(1}j|9|?X(wI*ICrd87M{W68|L*<& zo`C<pMyXN9uI>5%*m=QOefj^KSC7@ly!?M^y@8%}+Ux!E*Zg)lIPDyBnViJ!(%8dk zo~*gsS;P`<wrYFXziPMo7N~z^V?Wy?Z4HT)k;%)Fdjq%je2@{5oqU4V(__u2l~c@x zE4*}m9?sloJNLnr5BU{U9p)cJ_O{*0>yFd!`yZjUGv}{*-na6||Na3h!v62{{@=gu z%MQa&|3BtDoGknMmvp&Xz2+OP7x6oG@7@1-@3L6#``VeWzGQB_{KELZ<HEo4`^8_W z&i*`aX}s9I+ey1F|DSiu?SK0G-64;s*m-%~yVqG#^SiXB_WVmn**&}xtQ*U(SJeJ> zJA2A+OVa+p<7abN?dMo*{#Wqn%&)`Z3D@NJu|(F_-<rg*kH`6k(~tZI(~~&jc?9>% zDAhTp)jsrAdL-_8ME|tFK7(@S`!~4ng;cNqcJ$-^HFJ7Ix=jzW{^5Vf?5uzOf8)YN zssDFX{@aJx{TILUb@kf3>4%%1vtE6;RsZ(Qf{*NdZrT3|%+6^)o2~Ofd{!S%$%{>{ z>6@>39G7(#=Q=BY<M9sTJzFmv`p$HJ(^rS*vAj$&<#Q*uhAj{Jac%zk(rrIqMDKe$ ztzh~0*}1ZBr!D2S`~3Llw@*gP<Em~HZeQu~!r;i|i-uaBFGMa)-mkuBc|m?RPka8} zwN3BywJUbDJKMWIuYGT|J16HrdF@-%gYj8UUVQsunC|e0pU>_wpQ1sDjl9X8*~U9w zM)yrHm}~sxjh)?=r~c<I2;a9nRQLOc<i6bXi+?Ydo?|<0ec6{gQ7T`e_BOt^z4`2o zaXn}K{}bjc2d$>|&-l}P!Td+5`;!MwimQGK_q{l<z2(JxRd=7+{c{anIs59K%bdHh zRk811`}`ul8`mbw|0wD-&a1SpyKqPNT*b~7bFZU^9yq?NX$ll<E0I2;{^@q>Lf?wI z8F@<$-h6NDd-zI5@n!v88D*~H+{*W_S-R!Uc`LK`h5GU4^F76VC;uLqIOqO(O||(C zeU+`OkKc<wvb-e!iBInSdFI=H9n92=JhS|2mc|m<^UsgZx6qB)*7ry~=eD{}jOo#d z`)Ya(UdAiSNb>6nta)5*wS;r^+{wSU7N>lxzclk{^rI8+<~%)<`RwfTD&Ep(tB(Ge z5}bH$k5lXU{|mMjuS&MI&&^$7aCh6J`5PBZH3*({`1~mw^@^A^^|4<xG%Mp>Kek*i zlAa!OYu}ScPZXwH5&W^|pKYn!zDZ}doXJf~u71YAd}9O0hK8?e8H8uVFiX#P%i<Ow ze_H;Htx@yWtqj%?@!SS2ORwg<I_$74;Qnc+Z{;aGN}JdOXEt}dadcrbWD!$Tnk+EU zx8sDyro9i}*cx#Q-gNEMnfF&V(yjAN;<xe?>DR9rk8Rv1mmvH4HRH35F6?g>yG%d$ zuTLk1!^dS$$?L95(qYWnk@5Tn!h$zX3Mm_i2u5~w_@pv%<+!>WQ&P$noVc^&L|of} z1eX*;=L93AO_G9=y&Y@ZT+)=4b_-5?*m0uBCB;f<ldPcRc_ZZw^S=5revZ5^vSI$O zW%Cl=2ePin_gZ9I<-oflU#oGid+36FD+RvEU2Xj8o4fdRr`Ew=%N#UU<iBF6?(YJz zO(X7$Xt00v)qG>C#qrfQH~oE}r$C^uMAM=TdDQ`;*BH4^HFdmd>fmbb&}#1RYVOEt z?&xanSk>Hds=4D;a|c&ThgM66S4&6M5djfS!O%`c4Pm8K!b(~qN~=Vav_zFwi7IJ{ zDXkJy(h^r%C9b3;p|nauNlQ{`m8AET1sNSHvUoxt$*+=D(vneHC8MMztF%g1NlQ*? zm7J26ywWQ9^4F*5i~liWslRQiH{tm1C#)YTZtoGWxv(tYPxA5l<f2cjU-L<azyH7b z$^T~O<}E+t*M0e43Tm50{@wp%^Pl>^7x$h|TQB*#>gPGD^PBrjCoHxxYdM$}b0C{p z&5L)2%*ID%%kp{l^mwd!q(1$7Vg*|czr6*wT1J7|nf5yuZdM$;UH`~sLeXdYvyzf& z(?8f-OjvL7|MY^h|BF=ruQ&1DZ1?*A|E1l$<)>4OjqSr$N_VsA?X>EN<_}3^%J|Fm z_vZ(})Y(OGhmThrT)0i}Y47v;;C-1zeX4g=|Lm{%SMFhO?|=B?{m~!xANup2SNr$2 zq{*A!JNxBH#McFVOPDi7wo>?6z4}Jm?uV0iWdGLCx481G{(i*2^9O(4zgpbAR_n}v zj%U2Le$Ahi^MBtplk9D)U)kGj`1DKO_kZll|4-XPPxsyYaemU0g+b46MIO_OmP>iO zQ&si-jevO%r+#&>+%P-sqDj|2i-&e{@27~?$3EVX%FU-$vcJDl&n5M#{2YeYacz?m z?w&4CikW)u2=~1|k2fDX`uD$&<V~^QSyx5>?4R_n{n0=9iH~((K5OvMo)gt(seF6N zx{v#J-LgNJ<oj-R<|&VjyPSWwx3N6@m2a_d<+Lvi9aDUYx5hH8{rmFY{%L3C@N7;g zWBkwjF|B_3`TqtPKkrZdxxYp~r~Oj$wEyCJf9daB@X})4gYy^9^``e6EAPA6a`>UV z1b_YgOBGAL&Tn-$d&c2@o?WWnw%X64uB188vq!ux^3t{s*LMZC-k-DS<99XNIqB@* zjpx<AZz(wUy8Te``a3Ue{@1cUZmqX}ob&zPYPb1&=Fffmwwe3u!TXl)>s|yVx3|Sx z?zZ|a{r9-0lYQYd!^ALI_1%fj@7rWKU)<rk&f;*_S9#NYCbzYi-an8O^khyA-7WL< zE@zyWnI(fBuk#L@j*8A3y`>L~6dx^3;+W1Ys4wZfBd4R{rNGBeijP!Xj+{P`SHe7f zQeKJi)Vu=b=~Hx78c&}Rn{;65Qt7*M7D(SYpqj=pedgDd2bZRCdp#(6yz{ZD`;pUq z9n<9m_3f3+7=K^7z|(O<q~nH6#|@Q^t9Nwb1fv?`_x_SkKV|SV<ou`itM2?)?Ron@ za@PO$*_$_+{$2m>*ZnCrXC_U3+w}WC8{5aJpXviT%>F&r`@3J(t6w_!-~6Q_CbH|q z{#c&a_|Yf+;oM~Y&+UiJx!d=z*O$rsyZG~oV<!Jz{OmilF@68@e)(8cjZVQo`rcBP z|Nfpc^U!<WT~_D#-YGtqd*fWC=DFhaSBmHS|L6Dc(Ek5#Grs>TZt}l(z3<yMkL7kh zRy=;!zKlaws?O`P3AfmXyZ!S8cm0pIK7Lh*w@hmP^5ZVW8!jw9w>0s7#}37(!l%F1 zeC^s<H_d5h-43svbrYJ4YU3M={vNh@`s?hJ&krvJtowh_bEjSJ@|gR93u6A;`tPip z>af$UDKCqC`r1RjZy$ULT=(9~d~5URh=0q<Dl9dxUtfH5aY3Z!_5ZWg7!EyM8uNc^ zb5ZS*$whwyXEAV0pI!8Kxv2V;gG=3yoId~InQ5cAOjh-_oz4Fq|MJiNWKjI8BD?o> zeL{ljUw+<e&;IY|{`WiN%K!HLYcu~oU#DbZ9etN)?f0FVuG#;2o4fw2d0nNc?Eb4? z?TwBnNxh!+asLB@eV6{#2QXy)daw4&e)Xb%{?QSaf894cdHm+b?wNmVAI|wa&!6wp zzVhwDHTP{Fe|YT0vcL8Ghq?Y+OUq_zSnRSY=lJ)S@59|U`dp0sYaUltt`oSCH>G*) z|5ekEx3~0*-uR`zcfY_c{q0V%7nAGw-OqoQwyuucE4#N`Pxu#)R+-D0wd!B?zV??3 zuXFiVH>bbP*mCjI&wVq}xLapzkAHA;*ZQOVpK|U_-*d<C!`z(oIX^m*FVweNK2CnS z=i@}3#0_@GCQdL4kvR0zQb_r5%aQ-p4f5AEZZ&xG_xFz_QO{eKgzxzw7%wC0_V0Cd z-Iexw3-|v2GwMHVJwG@3m*tNq_4{9oFxwZ+P}3{!%l)=YIrV_^xhci+pPuPy`hEWA z<9=)IrU~2BE*k!uzOm`){>eW%=euwUFXwxB`TAXLAF0d-#$wmR*Vw*$v{Z8LoXOGu z52o8ioS*sg`16{w_4#ve&fk0bc~$*`Gk5>ZOg`0meQxFYzsH`-S01wb`tZ5z<+8nM zKJwP54Idx<&tc4NbNqbO6|I=NAHrWRUh?Gdm)R4a$nPqeG=JNN`2iOc`z4sB3rzIq zX$<Z-q1|>s*X4=0^8*Q`PdtJ<-R2&&W_iV9ci>;&9%q+#hMp|{6qVQo_1C>%(o<5B zmz=it#ih$!mQSAD7hP~7)a8k`QjwToWp~G&)SbLPx;tdtbfOQo#<;oI8MF8)E3KDg z+8+75>?W7q1B*vTMPm+>x;(K~Dv}ed?C-ea?($E05!(j8q6>^JSD3`sFJR|!6cz{+ zmuNCmSY&6w((B+d*TH42gUenAm$MEocO6{bI=K9GaN%`yk#%&@b#$>!<e1gevFnh) z7FNNpt%?~uO0Rg7vUrtV@hWBUDZS!T%Hmgg#jlhlp!7;WDN9i4m7wzl6_+cjZ3}!m zR`~NcE*1z}E)cj{AaK1v;AVlq?E-<j1p@aA1Rjo1a+YnD-^qRSzw@($>#l9h4l#SS z{$qL0#V`ZiTU?8n*Ux_Z-)FK)c9G9AP35aGkN@fKxb&|-@!+k0_U|73ms|DX|FX6} z!GG@m^*0r2$?JN?eD^r}qPV&*7ta<n^~c=g^y9ra=fT_OEE7+5oJcz>nRV{Nw1T$l zD@x6u@#>%bzg11z*Yf36L&hCkpY5BOPoMqYVV3#XepT(i_RKv;UBCb5e5Bp`pSAtN ztpDZW|L-qf^nd<~*b63Mnm>X!<l6`z+Fz^_U2pNn_uq%|=RcI!`djqn$Skz4+Q(Y| z=4SG*$L{}6Yj+;}6)!g3qHe9V+yB|dQ~&Sn?Em}b!MZ;;zn8r{UnyHKC%W`n-M`;e ze_HF$Z`fY_pYxvj`}%L+%9@?!_nfc$?ku*?`LOM`K2Iz6<N7LpZ4>P+-~D`IYkPb4 z-@E#6>+VS2%YV54>s)`+vXYq+Deqs!od4Z_d*%DI_%rM_A12M4{`u#Dvchi*9#*** z?B)D5;e(6alQ(9yEB4vS@5<j}`)2#yz7I|AU+?zUpZq@K{?&WM{+)}bZ|&Us{hazw zyXKJJ3;usAK62b{>HdvA0YRUBhHE^E*OJ>(H?y<iev9IdNfE7SD>5R~t0pJ*F16IW zppz2eTw%24W|h&J{7HqaX-bEy4mq}!w#?(n=AOs%+R#?=+W$`}5zRNNbDWIUB!5YX z2(C0*V^?OhCbrUO&F&25;*6-a3wu%`UVlu9NWXD%gWbW&iGSzM<yn2uv8@zDx0=bd zKT|*KHCrrE!eFtK+ibB!iwTLkcPnHLHSuWYMzuV2Y}>lAnJ0U~TnN{xZK|2>p$&>) zEk7nCUbQ~4!SBN4#HGC#4d<NLuuc6jSE6fM>Go!x*Ndx+*2I5GiP*hqfAWfniMdJY zhkvQ~w}oz<oVdhyOP0o;J-^g4vS0ka_;`QeGLg+otET;rEB)tRu=MZ#X6LPc-kbg0 zUwrhBeci6rpX(#z^yly2{r^gQ{IvZ$Uke;h-kk52_pavY9h<sc(`IWwzqJ2U?~RtJ zMUT(sdg-43JN2J?%B)L8KGS%@&-~|K8}h%Mr8Vob{ax4p&RVbP&ED(%iod$++>ef# z@@wAbJztfwoL}T#Pmf^H)1HZEvn3CIy(-SeQF=Z+dBP5Ny<I#0-Jg>r8ScCK&H7jW zn}44AY~O79w_Nz|{`HIgEf?Jp^<VqPzyH};`{VBOH{E!jmhq!D{Mv^tOumQzAA9QD z9D4O>eDL8+x&2zd{x_H!-=2~;FRO_AFFz;WrEC9pt^DVo^EB*Nd~4nONt4d+U+eaF z{td6o|6jd)x^r3E<uCiSe)a#}Khx;!<d}#5nVp-p-v3|nvp)P){m-`&w~L>CtB?8e z>Pnrd?f>obEq-ck-k3FgN=-;W;j$}BcUQ*E(Z6c0xY6tW?@f}6{{G`Mc3J&+R`#hk z`+nWOJ}1uh(fQES|LqSZDCGXR9{cBi>Tmm$^y^#x@4v;y$}YEGGVh=MxB3(3_PxFP zr#C*%_hx?SU-^`?The;WcRKxFzvaKNe8vuw|JJ8sdefJm{lDz@{@Igym#x%0`M;(h z<LO^}o?CzC-(C7|b?m->+8-jngdXH)w@>_c|7Gt?#yCaenmeNAcaGnUF|>C{>N~*u z{de5`YyV5vg%n3V+vb~kQrvE~VvSw+RN)Uz=C{mT!=Kc<q|K7te$$*ag#Z7?w!G`> z{wp4|_<wxC$NQ{r<um7|`1)>mGAqb;r+K>Ph0^E$7c5BF^FRH;(zBeKd?)+N)Kpvh zclG|i?P<N|4ewsrnh+lSUo?Yb&7S|8xBcI*{ki_{CG~$_^lbAwf7Txj{``~ovQW$R z+k4(?e3+>(Qls?o{T<W8KhOOVmzjNTouHZiFZuuP(&H_+KdH3bmY1b!;eM_>r}|%I zV)c31KYcS>&&|EJ-?FT;v?ccF+@If-4$XP5x%#IqzpjW~NA$ZN585ry^*{Fd(J#Av zu4^6JarKES&&5|=e_kK?P5P@mJ7S*pNZon+x|vFIw7E(j^eXA+$Q}Csv#KfJ&zwg6 zN*k9CH4FDIu2WyV_wk{xeSW)I%goAFJiB%C*2Y6BQP$61B)(rG@-cRvq+fDZw6%3h zZ*#@N1n2ZKQ`q`uC~y9<BlquC+gH~#zpsehF1KgelpNb`wp91TpR-FJ=k8iKD=(z% zao&$wV=FylJweVNET4^RloU5=`CePT=H#i{E6zN>wn6%NlJF+!H;%?iNx_r$&N_cD zdGe-xycuC_7q)cV*wb<2NXLyc9XGCY+_=+m<4MPjHyt;=blmvUA;Huk!N#K~B48*Z z;pCx^6rqrmVbJoy;fO#YM`BaQ!lsUghkgYqq)0g32zlYu4w{!e@+(N8=T!N^WQ&b< zq8uwX{qEmnC(5Dg!lUmZmh^);vF*B;bFV2wi?mXkj8dEIqoWf8IqZ*gXJ1v|5!7E7 z+FamrWP;0)4K8~uCUHIN>`*)<a7aYZd8%TI_Vc!nuS6G?^4)d%DgT~DG}+7Fe0k*N zn5qAroq2Elsn`9#Tj_tkRdMj={oAdd{@)(7^-sO@lmEWytMva@XaD>!KktA045LZx z&m!_3?Om{L+W+Pzz4!n5pKkrRU+?UHUa$G7Ct3RU=KuWj|9N-W{cDO%@9)X>{Z6`N zrT^?t`7}$Vtj|oRe{$>HNbVAn`FrjO*T!sTnZMuFIj^r?`PJt2)vqp&Uymqzw8+Qq zUQxB>k9DT(Ilk3by1q}}n&j%ep=xd#XG{jCg3?UU4B!9qZfZ(y&qagnU9^I8JFao9 zlwUMW(M3<mO<&2)K*`PU`M<<9maOI{{ue&$33+`*>dpWChko$q8?;Q3{=f6*@+D9I z@0a_(`(<<P_51%CxBOph_V0PqxA*gn71jSQt8){5R^Qq>RsR3ppUanQ{eS;j{oi6A zNmGk|(o$h7|8ZY=WncdC|J&l*U;n?0&ENn2UUlpKH_UqR{9!Bkcf=j_+d5b8CCk45 z`fQEn*`o8MOPQQ?Hg&y^yQTPdmv!_1RZ453(hI(_uM2y;v+|#OhL_)l2=y5;Gyn4@ zFl_y^{%O7V)Bn<P%<D83-xj*7`QLh1*3bWDr~aFl8|?VS9De$L-n{?9#X&`Cch7wK z|LXDnJqv^W@pE1Lw14)K|G#(MF8UYV`C<Cb|A+sIXa9$;N?7%XQBPAd>*Y6PJwwJr zQ+*H2);1M>&}V$mD}QtO-jni7+k!qFPfXy5Gvnc5JhL;EB|Gl`@9+PYH|M`|xE=cX z{{gR`_BR$htuH_M-}b-f&;M*$bwS?CrmWwsa{c?S?wJ3b(_ZKQjj3PUTKm;)`LyHt zv;RQnGJpPAf1v2Iz0jJ-WoNQ>S<f{4|9I24_p_7FZAepfk1#qLxi0I!f^)*ppZ4be zr)>GQf6d=yBVWmtCp#@?3g`bzk2(9l{loPB|E&l5pZ$-0@&ENSyN~G!FF($G@n75| zHvCe~yvV)E`{!S}BeeLQ<)Y1(l&<;b-rPH}XnMT!)=+l)jirAtXU&e{J!)oO>6z<4 zM{Mu+di|}pxA}jG{Jil>-A}2&`KMQw+i!7rWAKH0>*JQIUh0SazRvsSul0Rfh3D6| zbC&)Pdq3gNo;Izz<8f20=5qz_%9*KMs_P~4cY4&4ylm&A4KtZl>KwHV4Rn2ZPuZxm z9Y3;4k<;C!IbYn=B%@p6+06*v{OSl!`=5(%{r!`+^!U5KS?AAB*V-HQU*YRL746z* z;$GkFt-}P@<a-KkPVcOJEbcXNrJA9ZLU>nSm&oG!xTVK_sB0U#_OIO7t32h#)|XlT zFUwA4Jb!jFTb))%;bnENX0sB3$L_~HkGYl}Ik=Z;Nz0tcj|ASQ1wFc8oAl`C{H2L) z;YYK}Gs9~eLaVQLY+HMPIrp|3`%Dv)oP?_-8JE}&7)$+$ED@2|%60km`h~rZPu<$_ zXkWD5nNLZxAHUylS8(H7!Hs_f4cP<?`2-Bb1PtW_4Ale-^#lyfB%Ep#l6n-9<|rhs zQApaOkaR{N>5f9u8-=7l2CsPykE}62BDndEfOm(4|Cu6ZAH$X%Nz<g0x)qOzZ@wpx ze5T5Ij$+ap#iTuoNoN$3?ij9eexsQ5M=^;<DM>~tNk=KkMk&e1$eX|Lq>k+oHRle0 zm!3eEp5U+o=QW$E_#TN0CQsu%;(TWlUu8$^wYTgaPn*ajNeDic5KNX7d@L!LJWKJ2 zskWlRHc6+N&F>sL3flypcSyYMka+LIpzthmg2Nr<q&Lb*f0UKj1tzk0oM3f%!rX2D zfW`R%t5Q*Ov3L<^i0#9?0`ch)5o*t8TwW`A^`m6?$^Qo(9Tlg3`+xo2|EHJ!#Mk+| zdB6N9JLCWSI|BPwRl0Qr&;J~%czb^Ri+6|mgvBhZCBsDTUA8}7ncsb!tJa#o{Fm7N zoG+hWz58NqcK*iCkm&U3y28D&-G^BJFD*QlmwlUq(U`gCPq28)fh~-!jx&$9JdpT* zXadXfgb%q<5>i{7I98W0GuZQ1Ed9sHMTg!!e|t`D_jU1afBqz<ufNLPv)6p7)$cR$ z>3ja)WS`SiI?*txtm}fg-Fb<bQo1gzJtl7MKe;a2+s8>fmVeo!c}k-vx2E5?Z0*Jk z|1<rcH#}F9WUssB*)dx#UuMg~c>?xU7cy#oMJE=le|D^84u6YcZoR1*Uu~?1&42sH z28V?YZCGEY=kuxZ>dY;l6CQRvIzCr9d|#}^tBAJZFK^Ph<nJVv^m)z=o3Y2_gyu0_ z!&%RcaVP&>E){%<srAaS#l6pFJZt({BA<L~?|!b#>aT9zd;fDzGtZJ#pL0jUC+11; zjD7DH6bI>r&)E1YcX{ax@zzU!(&tR(wdOWndwEWzZk*PczWNI@WA_}MA#E3W!uH!D z!}V7)6!TBVB>%f<cj(GevBz7Ss}g^mVLT?uy0v9y@}J(#V)d$bd}9CJcK1Bl?zv*; zf(@mos(SZ@*kpXK4~R&XsqXYz6SQLLc14MzGkFd-45b+sx|=QdGL2QBDTP_g;w0M| zEfwhrk5!~UL@3^Dc<8r@QD6EbTS@sO-WlmC(hl8vh9M`}TDVh~AN8a#-{MGNX1Y<$ z6TgX(nd>B*jPMDzAL5gE3%F94XNgVXjW~EOIb$;KjYlfd0U;h{2i~bjZ`d~>=fGjV zO^ob4Da=80Q<$$wo@BFmpd!8Dgol}etAg}|bQNg<-3h!BnJUr`v?uUZ?3s|maF>Zm z)5+|Cocc|MWW}2d{^~ayJl!@i&Slxe_|a_><5!kVjE!$>FLmTN6f56s;J;D5MC4|} zb=FOc|BWZ{Ry6H(?^0AcE5USCU}9c}ho#FBS*0L;!I$kFA}%gcN=l-F6Z<+mJYANk zDg_A(zC0<Q(bZv+$`O)!kfS7(V~VTGEhVK=!HN4iJQ7`&7%Bxx3cl>=nBwLVrL447 zaN<3l#z!3<#cd9yE=#PP7uYCWk`r9n-|@xWh10=B(!oX3p`_UI!=ZTA$Ln_;V=d=? zb*#LXo9B(#jo$0KCdNMa-(&ggOip%Dlj_rF<(uP5|G(eh_1Au(;;p~-caHv_D*O7v z|JonX6Fxf}k6*(4=%rKtt*tg*UfZgtJ>Tp8vLHzEy>ib7nZ7mO6V7M<*t08je)v5W zAKRZ_++6<DxY)3_70AVe`aO}4`yTpezvf|+l~rLk!V~}IXG~CBbLiLmKeMfsLm$^K z{Uv|PUiY}^_x~K+dt&}KALhCBOFsNn{oYIJ|6X|YNB;l6``_PvYsF{knnm0^ek12Y z<kFk!VacBl9QOKL+T2^WO#SQJ?YstMoi?jj_WobIF@I8U=brmXJzi%fEWX{J@b_Wq zgTUP)-d<sw-#!c5?sT&5Y~Q@X#m(6*>*cI|@t!J|@pF`LlC78+*m=GB-`P{=_9xvh z3*RQGR44z<{y@3Yk@rXcssG61dH8$dBjX9baz0jXS|qqnTU%=T#Gl9BpXqY=ac>Pr z^ZRpM8-AZ$^Vl@usCW$fIxD>v@jr@JUbFp}KQs5_0lx#Yp4W5K|NOUJvCZM^|DCh{ zCx5T6{M2e2c=oYTi?wr<l}*Im?bXI@dV8kr=2W>_%Wo6#Z11}D;w#QD#FYupurA+p zPr-uG&BcP#><5ebq0pB;ChQS?|NkER8yR%x>A(240~ZoL)L*R(lKQW`rGAm6uKLkQ zX<pNPOz-_ubX+*~)&GRJ%k|=~>VHZnU(>hEYx%Xm=s|<V-}-)y^8UJOHxgfoElpl8 zd29u{eE1@}yTK=7_oP0Vy*Hrq;GQcxQ{pacaN+Y@bLjarf94-Qa|9U9HS#hYsNCSl zU?FbC!SMX>AF(;YhhN*Q*SfVLdjG%eX=%S_7yap-Iju*exc8I&1%Z&4|Lh+ynEu=C z`S<$PFEf7be{Q4}pK<qF;~_ISd#>Dbe-d)4llDK@yZ^$CyxUO+)6Yg9`*wQ1*cX32 zlS?-~sB@ite`~>oKkHPv&c2V*zw~F_QKhr*Pl;#NUDufSto*B3X5Dp;)@S8kLkj%P zK26>eKDR!!uFvD{ss2yQ5~`}RE3dSj4SXVJE44vrP2tbv5bni>eQ6V2rhB!uA2*Hn z_`iue^5XSv=1PxRkKTWH)b$AW838>brML{2JvTZk4hnu;r2MEg{Sb%ve9p&5FJAQb zkkI#=bnC!}ODZc>dGviJMV;q*=skIf*yiuk)jXeq$=;-j%t;~nZPV2}dlww~@knLm zyf?KsRaUO<4ZC+kW$~;Y2K^tc2VaX_{Xat{cw6Np&AnD<zJ>mc&j?uh+y2w7Kl0@t z>*JpOy}#+XzwAeuU%OuHEDub4y{<Cpb#1(4t^eNzcebaOB!(n4N!+b#n5bR;`NMUq zyDaK2N>$Z6(wmM~nDIR9nbB}4(y+xixrr^ky(8Vjwvbcl>yg;^Gw=1?+tBm>;>(lH zJ}-AJ3&~FF{cLZsM(96p;;p~)@3j8^dg}B4)@J>e`s@Gi2x3#@&!6%7>$)SQf3B<! ztG!+$_J51i`=}rGN2W~7_3hi_w<%>wWcG~ziEY|f>Tms$FJJlp)Uoro6#n10yccd0 zUhnX5=k#(8RsH#zPn4Fw{Wv2!<V9g#*#29SvewjY@!P-a81u1R(a*nresW#X_U-KN zCKq&#HkDe`sYm#RJ%6+~@9nM~lVmt|yY9JDX3cFgYh&M^X&w(11E1!Um+bfY{A2yg z)pr8Fg=&Q*ynVG<^!<mcH@pgdRPlV&esun{NxblWt<S%UGxw?0r7ny6uvPL<c=};+ zw^{2ur#DCn&wG~rXsN@I(~T8{H`!yv)=8~DeZQ}G%E$flHLp*3X!hCOsmZzUZ+^<T zh{?zPOI`f$p&i3M=U@7&pWn<`B(>K5zjCZTcGdr<?a6z06g}RrUvt}NU1Y|Z8*kq7 zB!8Y3d*oSnuiO2jT@~@i+MM=l-ge#pcD`uUl-I$L!aYiC>vIytx+E%$uJs!&saH)d z^Id*<!tO^e3Z7e}i9Gxgajq#ig=2YV$MRDGe$z!;3r?-kXt|uWH>Thf&%-Y}r7iwP zpNoo%Ny_)z?qeE$K0Rjbe|w!5MSt0UUjJ2pr}h8Cr$6^6oJsFXJzb^s?Zv_NY3H++ zuX!<T;+I1+gi=$5lP`7G`f2F@6PWU4Q~t&upN^NS?R~VeWBs0w(U(@=&k>K0t~tK& z`u%9leG`A5UT7XaPkEi(x0i~??|ghT>q3)rzUSttTXp|eXUr{}v+3RTm+fXB{!ZaN zKHpR8LTMP+Hb3uME6jrKMSe@TwCwz!BWE|i^RFsTb}sb(bk{R_`W&&j->>`Knzr5l zPF?(OXX{DVI;>w;%iI#(=Wyri+3D@~c2{rxbgz0h-`@8hmwftnT3`Ox1D9!^7Urg3 za=+cCRlRijB%9-PdR2KkCfkgB#iI9$g{?DxaZ7&7w}-sj{(M;*y3|yCbC$_xl}+(0 zom}GX%gp$oy;3Hw=Gs?Q-iD6eh7R6?-zu7pC1`Q*9%A!e7$~qYQNro5fipMn5dkSq zLFq0<6EP(-2_-WrB{LZ%GdU$Q1tl{jB{LNzGc_eM4J9)zB{Lo8jF@fgq9z8;87bSo zi<p==XXLo#6g)ez@ZwFmkP9Dj__Z=>%rg0z8s0jxIv)78pvfVjI>6CjgY60hg&T4q z0wOv5S{yvv*hN{`Z&!0MwJ5T1g6LL77H%ar9%l|IkQCqS8oq<g9gHm<jz{L$vIrgS zH8E>uJi0-xc~#cR=ZpT$Hab2lXl<n4h5CB}F(?1Cvt@nVzxze~%!f&@?c+|q|5AEz zv1Yva!@8hZIyHMXyiD@Dn3^tPcKWZ=PUVXcH*9vg?3w&Nc}>EiZ(ELfv|aDkHeOJb zs%`x1*u^<}I#yL4;PQ6)aAii<ftL^8e-M$>Tk^H9VpT`Qt~1hyG&lP-x<tM2Sl(CB zyu3c!S6k=te}&+%X`07X|LL=s|K4-we=AGYpYqB7{<}W?{a?K0YwersKWtuZ|Lxwn zpS@?wxqIfWKNn}}zl`}SNZ!V?qrZOT1~K3Dy4YRq_$lal2k%t(rEc@r21r?7UZN(Q zuI(FN>2lT9*ze*u*<IUe+uLgIhWLKExmM+^&Eh_dxTQTYkxTllzRfVI`d0Pzpzh04 zGp-!X`gF;$OJcj9(b>5geP82MmM%Hkxw7}OO0iEvhMjGY*XLZdWpCfAM!x#16Ls&y zM723K8h$ZXoPF<?yUm>PdTrVoy`VMD-JzNPs!}&O1{$yJp1MZt_SH8(YE*as*fB+V zpT`;7ZN8_nr>Z~8t#mwPZyhve*O$P|TMOeJeVLhgsfS6v@Y#%r%xgk^Yr0%l{d4y` zd3)!U1s4{V1kY@o?)ztl-M0{fPY0B?d1@MT=K68vIJvlOJnPvQno}H<AXe(xpYVQS zV$S4C`70S5mTmO)`X0b|K;rnET2AlW6E?3l*&V#6%)IjSt{F?MQj|&~gUdOLChc0Y z(wb$aXY4g?*H>IsjeFgvZL;C<adp|Iq?8<aiuKy2DcomLHN|h74qCUZdCInCk@Cr9 z6P#F<I=gI5=ICngSk>Nfs=eb?dk0rXhgL_2S4T%yM@Lsj$EuEwQym?zIy$&IJG4#; zOc4^iI!RGOO=*>ybAX>qh`&onfJ;cAOGuDQNU%#th)YPQOGubYNVrQ#giA<d+tDN8 zA<=EeJJzq*#j{-?KCo6Wuu(9uRWPtqFtArJaH3#-ZfKhD$3<IfRo5N=uO6lLB;-0n zg6OsS_Et%Y+Jrfl=O-oczQ~@HAeD0V%(`jsGIRf|-||0w&(SG&u1}emb<QO9YQ}H& z!}9v`{|g?;e!V|7@_*$tyU0m8wt1~z?{`b@n;Q0i%EBB8bDtXPPYn?lG(vQK6!-ml zxbozqh3eNG%O*WJu;u2i1#6}i6`Zk%^*9uH^TBO@ajB9BzRz#gaOeNF7XRm~9Q*zA z{6gEhy@#(AAFf+B{qX5))t5g%@Y}cj?*BJB1*-D5mD23BzxS5kPzp{z5y<F&bJ5~S zPuXXs-A=M{Jz<zW|H{#8FEx(Kox1e(|EhI13ldi3J-GKQ>yD6p-i6ouSADGHwS4^e ze=z&(nx`#adS6ZGU@LlbApPpXU+<qJ#-^`0ymN0+Q1^c?&r?~IN7i3nI`N$P(?Is) z9*g5ECs!BjRp_0W{&>>-cM6@;e9oC{?zb{Co9HdMI6U~TlV$Pzs8>6zg`R9Fi(i>( z-+JQl|2u1=__j<@mx*2K7gHSOcYm+<tS?UzEgvtPuKN1P*-a+t^GqtAq{lql(HRuh z?;G=txiH9Y_Vb;07`kUw=ZQ_fUeYkD{Kk=||6i>4t5u&gd)xYzR)1SlDv!-hzcRDm z<;3QB4D7+P{$y2O`8nJDZd7RXlqojn^FDvy^Y+rC&p(ay?CQ_<b)Pr4vU#Voou!uJ ztBv%-&fS)=`*{BTQd(8x+cNPeo2bIB8xI&<Bz6a~x=6mh*6fkYaq*bdW5q`Tny+SL zPdXWV{+glx+@pCBhZ035Ew|I2y!_pAW&hd;HUGPj?(=??OI0?$Q1SmOS=}Fa%Er`1 z>+=<XwNF;ge|{pkK4012wp-c%@5Wx6ubYi3v)X%YzVghp`5J8Y>B`ABwr-}MFA3b_ zQQS3idK*VNN8mXdQx~hxR|MXk33fhW?9Ur`-p16$?(>yO-ka4a44<zYV0yN)k@HM2 zvvh<tqq4w6(KFu6#sU*%&v-LCOE7uQSkAW0z@IlKah{aKVH?v8ZIxLGeA#y@-@G<p zGxX10D0%dR%CnWpnn@D^&jd5Co-v&*O;Kt0jOlD`hAh(*l`hX%Zs+i}t0#G074KGU zqyEZQ>MLU>Hl7J)=8h~mvoMO~<_Vjx7OPw*Og-am@356))8xv`1dYSRSJs^fX=^{J zs+c@;IomZO|GoWLf+_l+ujB{mHtD3z`!y}uNII~~MP9b)pJa{6{j&>xzPNqq|M8j2 zW^KCcyY9yS?_U4xO)PdS`Y%6ea-^-e!{`4d|1Zz6JUJ`X(|`N5n&0gor(XO2cv8aI z|GUrq|Nr!#zxkv8`seFZRlCpsi`{qVe!OOI=+{57@61*|{9S!e{_vi>IsYd<)_q*; z^SC%>!#`t<H(sypxBj{x`@{ZK)DQU?)3{|;=(8SIaeOm>Qr5C(?-{Q8&&azVUAo{^ z=2~OBI-ymv&AN)U{2`X*H-*^uaa}ztzb!FVzJ1lR;^?66(4tA7*5w{q-+xkh?&+Mx ze9KNRj=#C4y5O%u<(WN8cRg8N8M)<z)#Wo5D}s)_3OdH(P%5<4_<JT>XlI6Z_IKkD z<=WiG|7|!zJ=AZvEY2?$zP~En`+ls7f7Pex=wsfmQ(r&f&$%?`=%*(W@4qlQ{`7RD z?Y--%_6A<(GC$AKGgshW)%pL$r&)b1T8CDh{=ZQ?gul7;^0%K3R}D^@C7XtEf7}-7 zUvsc<dRo)fk2`kuPnz^xZhuVK0+S!!HxC|Kz_rQxm0RlcC#KVtE7$5z+_Q<ZT<1vM z`lNl#?ni4Rs{0jvkJe<%ihfqN)OyBiruFP*kdoN5kTd&F1seWu@9p||+*0eA{q{h^ z{asyE9Q9hy<R`D35udg=$u4$TQr+ghuAh!ZqMyA>L(Y6Q68*fXudCAR?7WLDT|XK6 zx_)ZNihe$8q4msFTJ*DhambnPTLTU2CoE3dH@Ua#=k}Y`ON2!~yBdjpzJF`QjClQE z!~c!FT|bY41YWF|aen&B8TIk*NB?Nd?D~1VB;?HVz(D>li<9b9M87&XYdxzj3^~Jp zEzt1&;Xp(C&RV&k$z4Ayq(wjH`3FAZd9*HU@ARkte{fFY37+MBtbW~u|ILS;w*K9} z<<I}}pZ|A?=Q7*N>K^x<pDn#*VZV(1tt4Z;OQ)Nwc{iv&xi+c2;`eQ#_BvJdxQs06 zv>Uqozjv+HJNevOFEd>DZpWk8J13^D%Db|0`p%1+`9WvY^h;0P<u`p#iIVy;(OW*n z&rc=)y}xPclE^1lj&bKco58s0XURI<v)AMOZqH8HTwyj<H~-59UyDk&c{?(8=<VIW zX|B0_w_ftrQ<3dkuX=vwFPO|*|H^3X>m6&vj+fs2*uCS;qtDy!eAxcvPO^J$<yx^* zmC@U7eLDZ_RPc1Y?Y`DiRQ%`v?8tR<<ga`tu{rmeQ2x%Sqp2&mPXjG?c9z)uhkxOj zPxdXwr~eCF()sTk0TMX-$-c$>^nZazI{%$#Z2r~|kk0Y&+$Z}M8)uH{Y2OvUNIL!4 z{D)s8;ir7Y0V(bk2R_+fXnb0~faUam2kvSAr(dddoG7RBpV>9yAN#BgfA~WZf6Bi& z^vOP;P4Pn8)7k}m&I|ZY|8`Jpb5Nf4ztKzQKl9v(f9zfx|L|`~`dM!pde=jL+W*ER zo&U^dBmS|k+W3dRB>AU(onYxJ=C={|*k2j4{MxvOUnGUYB;}|4iW8sgFHC$|zd+^m ze+Tbr{~KrN{Aad}{8wJz6?f`?y*c-@r2qP_vwZuuq&(m4_&-0QAmGK)fA=Md+bl0n zS@-n+t2h6*r=`p`Ha`3F>Icd2&-Du%!z2E$U;Dps>3?neX3I`(^Z);k^0pu4b3R`n ztG=h2_55{ap^xrCGnSjHJjlQLVe-)l74`wuf1=#q?M&Q#EBh<!&hN|Auh@O=Kl$BW zdu7vAP4_eZ`mdPRwaU!Qn!1Q7?exXK=B4bj`X(n^v3eV(%Qqgm>C|PL^6Xdr&)p^q z6xx2PpZUDwX8)u~PxmL7HQG3>zLDv_gzGi?dUZZ6sa=Pa<dS_J@E<$6RdTzWjO6xy z6{Ea+DK`}IB%Iz%DE#f9`Z&y8?XjD2g5k6iJ8TzDDzsLcTsS$zD9>+Z&+U2@qde^t zqr9-rp4&Mydu}Im_T08G$`L3}xg+=R#Ez{KPVBh&#<|D&)sD`GTOuX4f79U!|8VB+ zvyd36|L4toH>HK0W_qydN&VY@`BDGYPx(JTT8IBn$pjH|+jzMxjnf~w_fP8Wem8;b z`zu}bCx4W9Kj!JH^ZgC++Ven>-&n-s@V;-2hrSD&W{QjLk-jofdw+vdd8bv?H$RPn z<*Sl|;+qbYZP7ZGJKeYV^U{s~w4YAd5*U>A*m9HQvh68$wsC%+bM=?LeXAdBW2<sn zI`%22`u-_udR<R1eV@8wa?x&w#40DyaVe}X`ARoEbk}f+57`{`rk$x|-K^M}sB^sW znos3!t3Tg5OZjPTr1JCmviiQUyBy|~t=Ml;o7QKuc)@Cu;xh%6*Cwe2eSLX}%klJo zbIbqs?#u!b{Z|T|nFS=KL#X*z3NO$8WPe1n^v;&$fA~Lo$(qYe`LCF_@^KrRf<fKN z$8GB@j~SNUsd3z!BXIso;pWMo?2nk1-r3{wQ~uFxtGsQeuM|$!4?KR>Yo=A6o9C56 z<94eYg?B3-r`bHM@2G#Z<E6i>*#YKPJ6?MIl>gx%YrgT#=LzCh3NN=V?wc*FReHx{ zS)}j*;a59;PW)v5q17sHg)?8Jv#j~@m514WILn&<UU8VM!bR3RJLT05Nsv8EGp+J| zd0#1<?EF*yhpVjlVa@-$XQWU6w7+_d@2)HVCue$=p8Z$vxNzar@AZ$i{;iLl`TzAa zyVE7|{iUft_dlJ}oV2=;xhLvx{pZ*JZ(aSs;JPayZu+V3YQgrOrsiu^*lsLT^=~L_ z>e#sGD`!S6)4O}>5B%<|URl@Vy*=Wb$H|igp8xoHb~^pzzw)epxA(vIK~**BKmXJ( zwJqSWw!A3&ss7Qu#W6S8;=<*#*Tx2YdbY}2r1t#Fz=!X4i)%`S9u=wl_~~zK*Pd&= z4j+WFH9rM)tvR@CpKAX5lC!_$Q)f*|E$#i+{(;T;>EHWXe%7zM^8eRT|FnLu{`YRr zbaj7R*q9USzI*S6iOD~^m_N0Depf8>;zxJM0g*-jZ56JUecLB$<^Qiby4ZW!zjK1l zrMgMNUv+o4zkK(J`|9>r21l;#a@4M!9mN(sd%n|~-2FRFKdW7^bno45_BnRh=WgBB z|9@xM`L~Pq|6W+0E8Avzt$a>F#PyRO7XO>~Z+7YTZ8z3`f4e8-d34RKch6N{|LJ?X zx!le)?0V{Hsp{*CW}5w9%Khf<wXSUqGcC*N9OI)CZUz5Zu*PrOgBdxsCni*xHNQ8g z&A70Ao5A^l+8yPwu|LdnzuY}vzxegW-%b0TKY#za$};L^b$*ra_4I$1zyF^9=dK>n ze?vKKf6{B|{YT|y{>qOIX}BvVYkE@bZ}CLCWB(0feJ)nj<~%NmeRJ#RuYb$gU$WV3 zzh?L0GUv7(Fa2+`=SpOB%iA8!i@9>vY{zwuYNvJQ%#Q3{Zd>@mf7=o6w{H&f%)ilA zd)ImXlVZ2&TTVyYFWTW@UX;4A?#jlmyDn}P{o5QZ_WIb8yzA=vXRmh_9)GiJr~mC^ zzpUSCW(Mc1><zzh)=)X;+2ze|Gxyo%W>z1+sXOm&&NTC1H`V9pZ1jyuxwc%q>b2U_ zV=s)u#9yyD{O8N18dr16Eeq`~?~0v%_0bkP>$;_SyO%WDn%~JhZToGm?fR=+(f6k9 z{q|?`=G@@v>$dq@o18PNeKz@<B<tTC<Krne({*3wEMDnXnss$;==sa6{gWn1G6(K> zetO5-^UgxLQ=O!F*XKBi=;hASFJ61I(0a-zL52Iu4#(Vi+LsI7cfTm5mArD@^n2U; zADVt}(0lCo)5R|NobYRbiQ+lgX%E<L&yxIca9bK%--fTr2ZUXoa5+EVe*0ru_F~!l zVH-NLPsqAF;a4i+5Uf1H<6Iu4&`^`K)wF<1u(Gw|P7=o-Czn1&rSk$4vpY_hyF8Im z`ot%=^Z2jNjrCXN*UzYx`y?Q^v!kQN#l_EvB~D4nT$0INaN^=Ktc?DG6IXYf2zPm+ zqx5Oo$rnsf3Jrgj?gLRaX>A!Cx&jV%lLZ&CGM&gyYiPL5(|A9nC8Lqy(;Uf#=QA4_ zKF#eB&=qjF2T@XQ<h1_A369*2lLdcm@9mjz*Uzm(jAMbG0W-_30|H+fB^54kv@H-m z!_gRa;m5%!>t1=E4HthLyj&(_FOjkMW3y(Nl>Lje#UGmm-`Hkx_R4?Ra{0$We-lQL zlGbDVb_+QdEYLTpD`-<|XgYV{hvUS)WBgrTP3ktpxj$y_ns<y}>~+SUgx&Lw@qa2c zsVg|+_n6)PcE+Crnae*mYnn;fM<g!)*eqzmDDtKA7=P6zmVgVIQuZ0{!VZq>GyW_% z;`W$5?OWxFSd%(|#mhf7C&~88|9F}C$06~3;CYa_eaHCyUT6MEQ1xYCtzG<)S>2a` zb?)Mi&7vR^`;PIa-Ol)vuyyV+{$n>Y{ygxs?v?+t`Qi`9X>*V9TixDqJC<kf1OA6@ zxy)PZ5BSggFK>5mQm$P7*)yx!e6OwA^k2H~Ka1NeBcGMa8U!tcSFbj?`0W3*ZS^^B ze(O>fdB58K;MC{(1^+G|`+sNYfBpSmcCbn%EWM}j^LxX`Pd9&y6`g%{^UGP?mAkCn zk2KiVI$YnhTmHwT-z96UTu!f*u6TLt%BQKf|CFp>`&OJc<vsJ)+_&3m)qk(COwHb3 zvwqF_+~vD}FT7gRzEE{npRgTMeWg>m|9|s?;qNZ|bn5!5YnS)=^C=ejooBE83{m=V zTs$YXTjgf`%f;LBvW1^C%*-^ab3C#2z3KMf?6K$1o5z0MwfV*T@|Q_}mN)lTM~dIB z;Q6t=a@W7*tnaqPmmizc%Pp6xdNsSu#55sJYD21C@6G&KKbaTzq)t;^cgarTy71Ab ztId1TRvkKaI;2%MrT*}VWm-C+?x7N;(MeN+McwXBlXx4_>Uuh)wesZ(uElxYJ$0)N zt+*`|=@2T>`$kcE1=r$<wkfQki6)!c-z?C2qujS@gRs@ALm4UjH=V#rvLQ<NLlaB3 zPH+urz4%V`gc?{uq$^0l-7kB;G_;1aUOaK{Y+POYnx$GfR_>uUPKVT*$~oV?bRcQf zA(p;X2Dv|FLld_@__RAZXyv-x(73a|_X|GzQu2CAr{uK4XS2T7Cr*F=Us~u`_V@qA zOaE){pHLn3vM_tegK6j1?%;oJx+!pey>{O_#?LEO{XB5{4EwdTS5enl`@4Q|t^Yc6 z-!#5Uo9=IMnA$7N_b5w8_-42E0^4=hme(J>q4%E4@Yu@p`zEeUns58}-=)|nxlq&T zD|Wh^Ro%7Gdk=5P8Y|n!OMli+K9&|0botE5THl|hum3y!@cL~pS<(FLf6UAOQq$}t z_j>iecP!HVpSixq``7+WJ3C7MonKLT;MIqQd0$^uUt3eL>G8E3vv_N>{`c=T-G5yF zbNVGI$<oYAUzZl1|I~inx$Dz0!IKLmk4u^KO+R18T9nNeWISP=@r3OYId(<+FZ~ug z_wRnrWj&qKdY;_qDJd>H+y6iO*8kYs|F72ms{icOfAhzG^ZxXP=kgklWV>cZw>6e> zhDRT1YfjBveDCRXwl!WeuI&29x<O;@QZxIllWxzm(64-NufF|uS;(izVwb%AHLI7L z&*c(J*%`8}-$UJ5FrqK@)n46e%~vZtzUto7|6RFsMcwghr{?;*u8!Siqg$>krTtTX zu2Ollv?}Wv8O6`s@20M+{QEJFb<Kg-*Z2I>`1O3d;#U8l<gR&*Q{>hkTQ`?&rA@%z zx8LP8@1FfMORhBQ`mV_SO(yQ&OIls7HK=YqrZd-c@%;p)?w+n)Tjm~GdV6nNy~6bG zZzL`}Im_Oay8WJ7zFm)UzTCP==KRl3%-$Y&a`yIzCuX;Aoa!s_<;mG-?<Z$({uQ!R zVfg&af$7sTchyhN+*3Y1i&p*g%-8bMv(u`do{5@=I8K^i9=~jodA$ET84l-rHT!+; z*|@6a?+a7Qms>l@ygo`T-!4ok-_BJnUru$RxxC_J^ZnsU`E`@UvoFiek`$b&zW=25 z#R*%UoK(N4yQ*=|lar5Mie1fKUijoBbLgihx7hTaoXkJS8<Us1uxPz(g5EFt;%TO~ z%d7G>#<2buKO$}N|9RiP_YZ!yr=0zt`EPyGnrk~-|H`v%dG_Dr|MH*v>l^;BUi@?a z{txvVC;Ps%oR)J=^q;=YnoED<e=6ku?brT0Km6rn!_8l2y#4<n@K@2y-oM??r`~7& zC2{^<x@JyZXW)C8i;<avua|FobI)Vv>Gcb5t!kcsW7ogTt-fp9l4j4}vt*mPU-!M= z`={T^y{-Oh&CiIe{WZNS&Og1fTRuvmOyXDTt-?cDOD88Tuk!z|KD9b}&yrtd=Yndw z%RTDioJIF1>wC@hZ`F+5c4q1>G1abr-nv2Cw=PUNaE4K6|AMQc-<7zKPFK_Wd0;@+ zWhKU&hfc03)AqDJze2eyTvIt*Y~|uU+hslT_Qur~c<(Y&E8RG2Pss$OV{0e-6svb7 z{}uPt%q&dGdTeu2Mp$8%fB%$AHM2W2tE)YyRoQn>Gp|{~sm|XeEdDj%^7pxcYEP?H zORnw~d3_-%bk_d3nK=Phd|oH1u0C33YHb&PQqNBBX`8-KaewaQPf<@hE2HBkd^+DJ zvRtn=WLDXV=@zwVvrHBraGztT7B*{Zi}21}8tY5fy$wA7IqLuE?1~VPh+DdE6BO6w zwB0vJUavdj%(k>cx7WS-{v&?T2RCWY{KbI{H_hx0pUyq-s_?Ax-*BG)pVoc+e{;9c zff?u4)ha6qa$cA7E)D6tn=(^nikE<=$Bu6+9`SW@OpLi1u=3uvYjsi6SKT_LvGt4o zvZcCPkG@;IRD5a3%iO8kvUc9`&t&~yeb4xC4-2Q7@uPD!#?Q}PSQh`TdfsQ7=X1XA zdB9dCTK9<Wgi`#I39f3VPbe)8NtxIynliCBBxRx<*QOIiTANO|*=#z&t+nYyk<BKL ze8$Kpta6c0Dj6f6nA>bR!OXPjgnwnq#GWplPqA{5PtI#?I&qb2(+OFvO($M*Z91{^ zgk0xt#qK+L3f*s(Ds=zxa1fJRrO-V`MWOr6N#$-Iu?eniA5JJ)bDvQ9Tahxcb#^xE zyDpte;y0BGZ#P9gaTJYw!o(W+#Fa5}$qI$;DXb|In?+J4&gVR#6#sal>p77Lu2PjL z6K{fSQSLr+vGu{X6H51;zIoh{k4)+6(%CexNk_<CoJmFjB+0;iLaDGs@c{Ft6Jku8 zPHeW>bfW8o9A`{`w*qtIljAm<JY-@yJ~+PRlZoXpXwvz#p-bnJE^FkI=USUih_FUJ z`K`6-gdW$X6I+=!op@-o=|m*crV|!&kxw#G`dP{*xbACtGw}fbrW4mr$Z_&0cCS%Z z=sseZ#^8BENniVfQugW-O8l44GX$PcvJao&s-?B*#COq@i5AOw61XFu^q-L9tWoT~ z)AeTJ0f|i~jxudJF})&XqA*L!#I=<v6Fr$Wov^ambV7nL@<}jjaY<QZisPKir~X?c zX9Z;yRi%Yx1r>hYpHS=gyMD`O`8g;5*XZcK@}BnE?ZAIA`Ima{=KeA6m-*0Cv$fUk zPt@VMol_@AYlZ&0Hf{B>D?zJT?@n25zGvYN+wkty55F!By8M<c{Bx09{OjfNJ8!Z3 ze>|Ua_te#&Ce!Q7E++X+|GGTr`&Z68#e4UKzT%BmvCseEwDSH--<^(EZ+6u`cDecg z+4CCZ_ZmAduKfP<*mJ3qzGnRoXUn}iVH$m*KY!mc#p_8i(s7R)_#QO0W>)XAop@~Z zo8&q9$*%5A9ZO&C7JndLVmmi%!RyYN;HgSVuI?|YlsqD*E2Y-ne$S|>%wgaru%$=g zfdpfQ<i&$&`yRiQ>^J-CZ+6}FBX4vOhn>@HhtByoCLiH8n&eVzG3iL%P7k4IEf1mW z(;h<E7tcR%pQ0iZ?=&f*+j-It<-a_2uDTy}RXXj9JcRr|dI)VV@(?PnRO#$~nf}32 zrBm2KrSra)hmiP5525`56IAwTDtpf9eKO&P@}wi@D^)rJjV2xWUa8XAo1)U0T%pn_ zYpK%tw?d_p*HWc3x1v`iOXXvyLZ=kpy>{EI{z;eq-?XzeRKDyOyiLyJzxD}%3Htw~ z7hL^6_vC-?=(rCB+hZ<&zW+@2t&Yx*=d5jV<tE9;=Q^)i-q|z#@4+i`d><|rus>Fn zTfXN;gV`as4Ih$U&ItXqxck@UOy%8dVc)<1@5_8SlX<twGK(aRAI)Ft103xB^K)MM zwLf<0|MXc;XMHo)iP(N}OM!D}=KCqL)b23fYOuZZdegfV?__?@eYoMB#O8k6xUa=` z3sXgZ|2^BhOn9fz@53uimrd5To9LHYcmB)cDdj4u2Q@DHpKSW_dB?tGmksYOjc41F zV0Bp|!b)0n{qL<o=HJZ!hwZxmk0<GT<+W>btwW~%ef-Av?oa>qo5agBcXeOx-#k6+ z`OGU5rEi?R>^JjYP44P1;Zf^e)-#KHhwrM(__g~j%ge0pk7_fP|M72CTvosL*yjIn z)xqU|w=3q{+vsw)*Y@I@v!#K%qEAb2KJ&)SYWqpMoRt@!mb_Dz_FCHePpv=iUeL^r zO=>d^zghbGaL6=Ug-YY!zV}u|YAs)5_EYBm`Crfc%%{fhpS5;F($v1W#&cUU6Q>?a z*>LO3=j!t|#~XL%>3#aLqcr=Ky@dUpoJZ^_2`8;(7oHAjj5rjz{E5)NGm+l{?kXP7 zYb}&N`O5BFp>DNMAMgE?LmMB4s@}1Tx}37VcnhQV?kOGJjjZO(-0RrH?(pjDklA|V z?SZD(jjZ39x%D{2?g;4YP>LvUx8msEp%PKxz2U*EgoCyRo4T7=%~`qEdF_7Qxx0yV z`qF7TbR!C4vpRn_v5K>C=kbWeNb2k`jws07@Zeg)!L0|Is+(EG*}3!h#A0N1c33Y9 zIW~Ju@|A;4ubWxFFQ4YX5YfQ6fq^xFk?%l|N5k==JBeHu9NvFOk?~~`aNH@-$mtcv z5z@fAV}ZF1({bPE<J*}_8d!fEXj1B%%_hOf9pF^#$>Mi_X-NZX1S9taCb0*sYf2Rq z5AZDO+4Awjjk(Ku+mBBPR6KTgdH~P0AAge{{IB?T{=m1Fot=lfXZ~jwVx6%5pZmXv z$N$$I{2y%_`scpyR5>Yi&LzIxdTLQWoepj>GWJ<KRka~(Qs@bbddaMbUsrbi-`l&g z`SPYs!k6bx_^<!sPrtzI&-NCNA3RY!*}V0~`vbq_XBsH!UUg($u;~8(fZ7QU&;9N9 zKJ9hZL~VK2_xpw4_a9&JL~$nb?ewL~)t9A-z5mO9;rZ?Vi5#F|FrNS4H$VL^oPXw1 z^oG2xTMC+&-<3IW_-5Xl?Y5U@DDU;#>|Sbba9^@))A1Q+&wN~U`o`m~&Yx%ZYyPhO z%5_FQJ@nh3i-*s&#<t(+zWm*2dRY9FD-*RjKd&s*Kj!@6%$@u1dJZZ*Iocg}+4xE! zkJu&28QHGwF_CO>TlKA0f79*=J^VjC<?3qHl&kZ;%YJ6x&%NQpZ-;5Mvs|bBWp37~ zR`1rSHg49bmfV#4soLq<?J0F__r*8GOs#9XFTLrGv(BbFdwDk9=@!~_XKI9}Zrgp; zyGNG=9u_+Mf0w$N>T)UB&&F~}|JYwLzdQ0@?pV*mV`mOJ3%g4OoBk<3^Y{JaIkRTz zsHp8c_LqO**ZS9g^ZhkEcNw~N|L5EDUvTEh!<XBnW1Ii8zhc(-zyI!>X>NJ{4`d|& zTCinJ$=iR{EdM+ANf~db-lS~A!2jw$CkM}|`=Apn3)TK_x3$cl^mG^3v;X3q-1~pI z-zZ+ozrXyG?;Y+uoz+z@_X(cfG@tX{ndOVNTLo(@-=qCY#(ed@osmgDzZzfR(oQpY zGskD^qbWYiF3q^N%FKF2%2__9(uYh(pGI$=cC>j<Wd1gv7atZ~pVoO&A!ki<|Lw!m zJkPmZ*&%vJR4qF8V3^#p_&HYuo+`fMWdF{<t=DwBA@A0EQT2CB|8;ioL=;GGcwlv9 z)sDX`F&sKO1S1MmH$3o5IJooyV;-wm47c2axP+~oZns#JH{4=TzCI`6%aX@Svw4j! z$@L%kzgva-r02Pt|APhotKZN1bKmsm|J{cUt^fDndrbGkZ+>B{uXQx^?}jbZ+1>Iz zO^o~AE0=w9j2u5${#*EH_H30Y@%C{mH$3v4I@j1`rG;wXS`)RX!oM>Er(c<N-CXO| zo`yqbxeI1iY%}`v?xfZeDfQJCD$?$L;ZhD(k{AA8Z!7cj|8AyF|LrTD{5O>OR3Eyd zI!xqK{hehzdCh<1w`(^4R``EB_tXQo6aNcm8|@JO^xr)6$$w{&C;vOOKK;*N{kh+) zy6e;bb~)_=PuG9@10DaZ?-g}`5dS%nS-lwIvXU9S1pdFbG|@IyG`!2We81g{|Lo7+ z|L5nt@a@0V<o~z79r@F~@#X)S-~L1#o%Y@M{r!-;YJZnZky^Uq-67w7CiTuA9*D$U zX7=AMx8Sb3oZiPhrLQN?yYcCWh`{3)*146N<k~ZOOD4*8-LW&9s=1(6=kFtF--AuB z@A&gKXkVP~a)(oAhe$+$_J#*hArl0?U;5I#Oy1=02a(t9LLKX8%_~s*@9*9ISN`0I z|KeBvr*mw%`Cl;e%n2W#a-UqM*Z&XwzOQ$5hSiQp{>KZZ9FJ$Iy6~5uPjkV4?U2j$ zdO`m+ljHx-JfQ#Ql}frbv($E}JG;J?KAT%Ob$9W59#i%ACR16A)n77roi;e7wDg`W zx7S|&jLVnUgWn5R`0Snk<eh2d^vW;)eeyq*7M$#I5BuNm?JfIe$<Ka8M#rD8_ct=m z`oG)b-*f5PfBGwaa-ZvBl#XgYDSho4_x+IF%l2e0bN}(|b>*jNJ@JQ&{pU>D{_RKp zk8Qj=KYd&9pZmVyd*{mZr`r#gb6$UPci->B-(BLTeUFKd{a3n`@zZtp@6T=}oqV`v z@u}?(@1Jn5S{vN=%5Lg@<1?XshVM@uU;Rwu_*9*rzh@W!5Z+n)MBcCWssFkYPyBM< zo&RsK)O@*4#_9i4WRCr{S*KTTwM^~%X@>HDK8J1I<y_f(YW>tdPwxM@JLUfBze@A9 zi=!;Rf7kojzu9`~XZPu+#mjVq_400>jE&mc94@Z+SkFKD)c>EiMBc66a`MlAHooWU zN@S1yziZZa*jRt^(|RqF=l{RH()nM#c5>0*?P*2#HZD8WYb*L>_UlvH+o#1&oB2KT zXzr!NomW;kckNzKwDhO<9`^E%&zIIdJiTeNMgB9}d(pCIrv|%yv^$=DYTwS<pPycD ze#)i2Nu<ngPfVeg{L)J^!iqkgz5IUu<u3(~r=)S;dS(*;c=DyBn7cB6d%86lJ##-X zU4A-c!HV?HO}p!-C4DGA&CNMyrcl(glDG)n&p$V8w*DLyH@|S-)pe<r`g2a$+^_oD zH^)r6YJPp;txI#ZeLi%qeYx+sPKo>4OJlDmTD}!_3bxwPakleKfU@eR^*g;nHs=*x zc_^cL%-o|d(%9i;$--54D*r{;ealb~pMH7%`%kW->t3JEt$eq4|IWO%uRq=0+*fFu zwfnPeb?wowd+OTW=gv6))$Z}KBiAF<kH5EA`(>|XcwOd6zoqwA%KUu#@${5`v)NBD z3oX-N7H>ISb0_GY#Y%hg6*p7r3WB2b4wg&Imk6Eiy+`1*`p@0Pp1)^*KAC#1;G}kB z-Hn@f?|g{#ojN7o@3iu!)7mRnm!Hbq8{#@+n~z+k#gZk<eJ2{PS{G`xn{T>tzTA_| zht-O2F2D9+@!z(>#pe5u_wN)ty?0ZBi2b*SV!ia3_<vVRb<Fb)aR14E|NrDp(HgV4 z#~)kI*%2{+dtUDQhT7yav71*ac3mo8^;4-@M8<#ZqV)nFl*Ok6O9wn>n#;{y5F)_H zkg;gm0pW-SjVw-|M%EQf+y$&+3fwve#3CB>HZa5|FdjO<)YHhS!OUIYmi?FI(hAiL zY+?&~qnPr)TXQiy=*tsTozNcbdR=oz=$By0?Oo?W3s%3_ZImZ^Hn3oI#<U*h$Y&oc zCM@mmVU9fZ$zp=)<im|Re=B89T$+8jQ75-j=7i9(hYr(r+3=;TJo(T;to)bc0%7(| zHme^vh}lbSU$*Pwd4{DW0^E(kS)5BAg<YM?l@}VT_c-io>K9`NoxISyj{pBMY_*t> zs@cyRxvaoqLaI?eb7Whw#e`Iee&)zW7Qcg|Ca21rxHRW@qt09q!}DOH&e;^16PH2` zHtL9`$eak8SGO#{w|L#ECd=!Gjh@BuYtASRwLG&k?vdg1s*b~RUQ5XpuiI>KX6L^4 zo#9L`XB5jGO+Ht;tjF^DKAUGTn^Vv1Ow*iE+&kUy`7QT@bF#HhS0<{y)#&(VIiomt zPLt*JZI&~Nzj_{=lO6j@(uwa`%<U(M=R*As&UwALaz^oA9oFJ?d6hGY=X%;ZTO69R z`lrpanDS?d=Sq)Np4q8)`tY3WXO(AmzB_SnPWJoHGm2wpHCtZa@Oeh@+&Rsb*W)y2 z6w6L)w!D5p<jl@v&l1jUb>n&#^E_$Y)E1L7JD(-2o7!S_W+z{O!SkrlV{=|d$Q7?^ zlzA5O`}2(AwTBYVZPoqDvyi`dU1{Z+opO&2p1<nb879PK_`Hg(c&&;`(z&a=#cNee zlFohI(qwskr_r;Ra;+J~XD1py-{o^~&T9tA;&s6yXLh!oo>3foD*4=2`*zFgCvBd^ z+%B3?EZep-Oeo0kxm5SgFrg^J=eJ~v*Qy*zIyd#H;qzUy56{UCW-VT~JoU`ZY2gOX zcLg7tqy0<0CE?7@MfFY^$L73_I<s}sgdWT5%`>u97fKedd$C?%k>T^Me_RWtz?@AG zj&$)l{R`&>4oT{^iAx+_snvJk(>DJt|N6ZQ`TR;ezPl^_vTyn{_rJ8ng0J<vz5cz= zz5Qo?MP+4yqou>@f7@Q&HG0Ti>HjxM{MugDxysMavZ{OyWx9O6*fCIF<KSKX0=EC@ zQzkeVi$^`1CKQ%EU#9Gv`qHOgLyK0&-kvJ%=TY?fD%brq=UsmuZON^<U1|ID!xq(= zBR?j_>|Q7Q^Bbq_4nEzsPZMQ%?p$6~9h>#Spfmrs%#1~KyBCO<TKFUeUn$q#(j>xR zA$GQ1`1vJ~PG$b(pPvOx{1O^4anAFAiF+0-P_jI*P^r?*(KYE=z{ER;-tjYut?<|| zo85s~^kf;UXs60zrGt5L41yslw<|(aY*Ry2Zro*F(A34UyP6>;U?R`acl=C7vy>`N ziFkT4hWk2M%>M5#9XzRB!hN^TfA@?FbN}^soDoURTGq^VAnULEivO1`8O>Sp--+q- z`~SQq8K3RvuKa&0GVa5T^quRv|H?mJruXMn#<^##g_=8GGacI2w{uI)?Kh_qZ~vBy z*_V7}>V~<x*LCivuGx`S-ur#pd(Qvack8`tuYJhAJFztWzs=@{cF#_it`I-}&eCS* z;f?v$JKg0aZ#}qi(Jg(^{_by9WozENd-3?+^FG=CwXR<}yXJjV=X-nS^Wl|eeoWSO zbD4kZj`hFenz4(gvL=4CS7mqkZ?i7#&&wI7F7E$wzT5ut2d^(THTEa(4_me4`F6Jd za=+hximyI5_2<JIJDc6V{V6Q_yz%q=x}DDt>0RE~9c|_wAGKFAE;gfUpG|J!pZV+e z3jPhh_4?y~=@|mGOJ6_uvHx~i&%RqqM*pG?J65l&wYT|~n7#Mt`r7_yS8Hu8pYDu% z{pl62^w-0m+jdrdwN{;*A7H9}et+Sa%Kx*Ai=-;eE$_d$a%0Z1=QZcW>(7?Q#Dy3i z{<-3CO<Yg@*UyI8x1N?-{XhTn+-LS@N95K{6PhkOF<2>k--e^-pG6)!{O<AJd2#bx zE2imQx?i_LJ+VZedAGHJ@T8;mI*lnG8*RV*l8BjHF8?dfQzn@0g?qo&x?1(AcUm{> z*K1)dQ+%`DTVC4j#=_=UznjlC2{3HE&lT`(ZQ|Pb?sskTv|hbiY`Nu=cSCnr4QIv0 zE`3IWsE?`&Zrb};PGlUN-XOX5hmgZ!(Rik;Eswk#dc&$XcQmp#ES?(2RCGmX0W)_( z`ug|h-z^l4XR6uq$ooKlSPdt`r7nF&$Ec604j$V3Sa>p*PG?Zs$|3@ymAA6UY<x6Z zvS0MR;KhiKZ)N*M@2|T!!=I!3c#V^TNmxzxvE-xMjsEg5bcx>Az8LZGt96^`{jM+f z`5U}+8AI3n_~&5Hyd&vo`Da6$rpKpkBR-nS9`1VoEMv`&ysqP2>oujc_i;bo_~^Hr zNmxyM|M9MTCzG%m_T$M%w<nu~)hIvS_{h5B-*3hla|3_s{;Zt7xN=6hr{BRnr+3+$ z+1>Mt@v~m>XM^{XO5{H4Sys*{@3ic%oNj43qdc=n?z3KUvBCRC)&E|uVCg@7cu)63 zn`f~<Y|iWsvG1>(UiQV9#qP{*o-+sccyARsv)koKM&)$%r9x|5zq$9niMn*W^-F#K znZ~)7+Lop5aQmljqH;p*|7F#}D-*V?xzZw%oYl1KzijdU$3nrE-BgyVsaY-jC!ca+ z`hQNYE6?n$jsHK+tUQ=J>#5ZF`j}f&&efaw>gemus&?8im$BC3wf?i)AMCHxYn2H7 z|LPT#zb@M;<8y!0@;EV@``?y-UU+(4%$bbO3+JEvdEr{r)OKx=)nW7Ye7_TXOE{c$ zzkUW=){?u4771Jr-6mb_&Dkt+EYPqyb6V4msc+w86zFewko0|)V(;I_^@ca>|Fw6m z+A+Cfm4&D4)o$a0gS>%zAN4R6xQDE_NM^Vrbak~^wG<MO%{kjPwrOL;Z12AUfA%{k zrBy{0Epx7OyZCj!qKU}4|F5ntc8&S;VBty6V<GK-nN0q=zpsCO<jR>f2M-_LQgGof zzo|r5!T*i_@-KXe|9JoZ4wH<no<5$Q+Is)_`P9zUh5z>tXxR4ue(0C~KBr6HvcLSl zC)w~!udVNjcl)>A2~SP?ZIm}zZ{<?A*rYq|zc`lmK6T-Lw<tjEPgBIEVy0U?Ck~zK zsn_|tI@Lz*_UCK5#n1kp(BLs%Jmt!I2cugL6(=*lkE}hjuJz#GWiJCKF+b>!V9GDD zmUQrbaJn!+@rU}f10YIMw^2BLscONa^$|?R9a|r4j}m(5>8F&ztYO%>fOo+Wl|#yW z8|tMF>~8U2dgl25<jb2T8E={#93<4;{a?J`U%o@eg10~S^Zm%b^ke_uzx|%R9%jp= zWEKC*yZyiXGk?}U{eQpjTO0n~_@1${;XivDOUu2v|1Yy=|NbBA`oCOz&A<PZG14EO z?Yi}9ZR+-Sm3l49DkqxRnq6|X@c+@gTD|4xkGrd@lI+`ePiWe@TbV^$M?qxO*3dUs zkL|tUYAfj|tTxM_FqgkrGPJP#&-=-9vSbovV&;6b*U<R#?w`GLz=Uc4%cuUZ-?QQ4 z{agRbms}Towjyu;uelGspGj<vvW+X{D~_4DuCD%b@kf`uwqJ)=em^={Tjk-EXNHgZ zgD<hBI@*6+{$w80f&VXO2t1t5c)lzB*OsvJZd=ppc-Joe_bk5s=iBpHuPi^s7ruL7 z`n7+vz43GVzj1pvWV06+{&M?zDN9xUy~Di3x7#lly=Jx7+_!UTRY*<D*Ir=>k1l12 z6L<fXY?x9g_qtB>`Nq1F4{WYo)jF3v_wWOoYcI9VCI5{%Qn^M_>)pBP_#<~KCpGN; ze5_-KwSMe@yOn=k?##U&ydl-$0h>kt*T`RzcNoi`Z(U~3e~0P$#%l}bAGCe3&c^?b zZNuyb@Ak}>yfgRv-mcxB%PyRMV79~BUsL4quEQ(dy%WDIF?HIj(p;U3=R~!b{=c3x z>)Va?12#wg%@3IIpW|=*wEyeUY&pE8=dJi??|#2t&v5de|1wji&I_sebAQ7A|L*du zp8k(%{huy2?f?HtSKKTwpYUI>XKzr!!iwFy3U(J%-ZJ}m{<@veKd)$|fB6qo9(rBx zd~`o|a_ef{woA1x|Kq%q*8LazeB*cb8&jq?Coh^DdYchfFr~fhqdV`1vvU;0HpwW8 zCF~6@^1HT2)>1wDf8nI!4}vmtH~JpAC@IfrVU^t3vDjD*#8}j^*mx4F%v{4OdBy@i z1p7{OR4V;yey3}-W_F0l#g8Ffed?TZVq$9Voc&*=wrmMc&ipw)EwT^rpZWjl^VRyT zpXKMC{(mYeP9#m|dh5CW&rUxVYZcG?>A7G}!4syf3UL+xZQftryZ)D5<V^Rw3%6&v zJ}kKS?Y)SQvaWb*M2GVl?u!#8@3W_+$h!VIacH}8?GeT<r(be*&8y%3a5efm<;{=U zMS)JYZ@+%=Z~v1k2Rm1;?36ru;y+XK=a2gX_L}`a9{Q=i({=5C>*@b9E%!7{_$yfY z@ziNQk6XN&eO&)9pZ>n($<5&CjpdOc7qY(kJ*zJ~_T|WxC0maFGM1b3Uwgs-VC(;f zLx02vCH|bhb^Yaa{hK?!K45<<f3R}W?TYi;vpFJe&s|xyT6k-%>UW14={;rDI(t8? zuAO^;rQNED^WNRrmFMqyvrjLVFWr86?#;f6B844owKq$bb)|;eSO(mdU2J$*T~OU5 zFM{XK`J~n@Q=arBJ*sDY`~SK$)5ibCmu5`7dFGS+(vSAX@BS~#JSo}9_e+w0*8en) zeY5|wvrgW>>|_1)2mjyS{`^1V@*&<()yqeYEpshi?{(Vx*sE`TA14Md|1kU?cRFeP zf3fpVWM3b?cjI!`&YkO2tFtR}+h)dgm(DJkJYlBvn%^g8C1ob_o@_oXe7Yrmy3N<+ zF6YnBn|f-~&ZL`D=Wn|Asy)dvb>fra8t%ule=iHE`>V9<5SQ@jbCr`<T0G_5c4>yo zvZ5lU=%x80!KTZUqt~V?FRNPIw&s5E#H8L$_j1k#{YyG)FYU3lPEY%q-4j*Y&0qO9 zo?HHWNxJx3@zR^;H<ZT5>)Ks^Ef$@B!|qw-VgH=JeyeA&g>K*cuUGZh|7ZO{%I`~- zU0k^~=y9>_OtsnK-oE_YNt;=Pmpsef)vbF>)$><*^U;PSA9E7iObs=%IHd3Vot5#S zKh2qGzqqrl^yb8^>x>fj|JtCtUOqkQ?XDC13X+&F-+H6F_2vi0&yTiz4!T{r-tb{2 z`;v~2pO2Kqo(=ll5`L6VH_c<BEL+#a`nM^IXFU}V+Sb$iQP$zab&+kCTML|a*-c97 z-=}lzsn4WcH6It<`1&-ZbM{i<-CN)3l%Cg9{kGz+&#RlS#W$wMv98>nIQ_<NpJ20n z=Q7pm(kr*UKRdU@WM$6&;L{wDR!6?ww`rZQWb2y)tcN)*C#gPO;`wMn+I-LE$&-$< z+@Beg6q0WE(P`V1C-;4PZprJbR)+~p4&OGZZSGzjzWG<%eJ7q4o^*MVZ|qv5%)Qwg z4{dR7xpF7r^5ttnMxT{u-z*hVG`zm_&4%+w-UO>Jy;x^_^y2L89?9!cyVvVPsPx*3 z=|AhK4z4PV)h@i#;PP_o&8@B~itm=%p4xHc^R>0*+6?_a9hl^deOcr_*Q-4+d@{H7 zeW97*CyBlSvA%-o7Kf4_arAdKziz*1y_x%p_0sHTi|%zU+r@rI;HzJ<@T<A2zZmaO zxoUev_ibsvd~@!CNS$qs$Bb{V-D*E-b|H-C)x>E!+ZwNJ`*ZzDQegiB3&VoM9Oku} zNzxg-oL@Q4sjyFXvu!dyd2nlB{{x?*g2F4x7L#RSWN$lcH@Ly}YgtR~g6<dT1+^O< z{QJPb!N6_6z~8{Mfr0%4g9iim1_u5HwhavY35-eyn0x}f7cl7@V2x<t*}x#3z-V=V zsiuLIgOS^SNo)a&&H+vvhM)sXYYs3kVAVOmy_F^D0MnTO?*(i+2Y785iViTnX<&W9 z$o+v?OrgU~hRHdBvFiYnOe3oY6ZZxdu?3tu;8Bqc48d1~z@sAk4LTbbqCYUaVB-G3 z%HLqHfg$+=gT~^JrUQ}@4JI2HvR9a=d#o^L?^<DAzG;Pd@dhQw1$<whF}!h5oX}on zJjZeG&xtKnpHl_DK2zcR`m9^&)!7uzugm;g_THRX#}?tT_ok22-k;oM3j{h`_U`1i z4&Vq`bkC+&R)fVTFh7Z#%R&9s*;JlaXPtFkoz>)db#|-HtFsSzUin5k?X~oD+52;5 zOO<h?%U;W1r@c2Ntphl&EV`G|Cab|xwDI#2rm_VBFI@LlM!M|1+5XR_A@Ry=-N-Am zZ*1kA&{OqUpt(x;ujm8Guh05*UY(7+(O3lHN_~Ci$@%q}&l?BD3%ymw(Jp&C|8W`g zftAX9O<UAbHQAtTImh+AC$BFo^q*n3^;3ObMpRPPrqy%)=W!TH1pIehqP~C9GliGS zKmND=Zm)FE;!dFbW5sv%2^(DhyDw2mOI6GM{9m^A|L!Hd9wnFl@|&^Tsr}!~<h=D? zefq2V+uO2F7Tsl7^!uKsY|52iF+cXVSIx{{Cw=m6)IH-lGw(JR&z2F2%i7XdEPvB> z`<9Kf-(1wS*zV&p;f-W~?)A@a1&boTMdjU`y1)3|tM5CXPI<iP{KwS~_wAH^^67Pd zdw4`)PxA8|j`bE#wbxym!GB0w{rvM)Uut#tWSy~C7v-H?@zu@d`S&}kcbcBs_Bd;r z)QsrOo}$~H&3}7lYu&p1RkbfFKO5h+@h!E}POdAv_+Blx%)9pP_3yLhy^i&c&%cqs z>p8Fam#@~fYqJBsy6>%j>|gTFeEn?c%KeM8OI|J6`O(txddg?9;>(AFeb0*N7@5aU z&dM@;=ezk|>^YGMDVNyOSY|aXx_5#7`-&u^AL=ihnf{x#R+U|8zq)P>_wTI>_dWfp zy(?_@-mm&|e)wI#b?3mHw8K?*yC+TG`F2m`t`A#^pLKkUpLMJBZhGE)gX32g9@Q#N zY2Uc)#QwTV?xwTXc3bU<m4E!ZOE2xL`!7H7xND{J_Ft8$F4>}~m%ef5X>t9TyY|@} zJaaZH_tiD4lXq-)_pNk)D^Yy@UeC((IN{>_!{?XXOJDT$V`<*YN9AVMzVH1x`Q6g~ znlFF-a<A{T`L}4_|7ZOr%t9%D53o)ysku|Rx$sib<ppW%XPPIg%@C?N`|QXHmEcFM zlFwBBO9Wr~=Tp2XT{!sDse8{7_y3yJcK+0b<tKVel{Zzt3_feOYs<ZbRf)G;@1B?- z_3MjC<n+L=yX-c<PTzGV`s&%6iR?Z%m;W>D|7JYfE#EFtt~j^t<y6+$XOjC<XB8dO zHQB#w-S*5o4cE`EKg(ylb81n!_S?e!=`)io-e3Fn<aSH91Cy|&#apxH_RD!Eck};k zTY9~PWuM-G%<Y$5EZ$CwxxTmdR<69w+ikKt^W0A)#9O@GW-L>ETe6+wcDdgD)0g+? z>Q|iE5M#b!>fhXA{e9fuu6oAuFI@9p;r8h&y*a0<9`$P6?{p9Pc~AJO@JF9t@s4{Y zpDKT}P~-mY=Zk9IyYc(5<uCm-w?b#?+auW<=U6zo{jAd!fBO4<#RJQaNmJe*Po45U z{BYpD{0V{k<oVry_OXXGmoKh4&b<2I>A-!D8?Qcit8xE$p~n5*hZ^@Q`JVn>y;I}< z?T3udSy)%7dNdgAn=6@GQ!9Mx$Mcuz4s2SFIjt7-{J+~`v~Q_g>Yun@k{6g#|KwXu z`Dp${<MIA$3w!<t_8a{ZmOb_3b=s7V?_UHQn{TqXN4!Mi@%9;k$NWtf_Qa>SNzTu5 zlKj6+GSw#CNizS^;vRmJg+2Anb!<-@CFd`2m;Aq-GquM3m*fPF)EZ~oQ$Mu77%vby z_2Yes#$)LjfydS_pU=27;Mn=g3w!L--6a1n<xKr!XMF01rP`E_tIudWK0Yhp*#9N& zlJ?Q<Ms>U8QvYaMP5CJQGVs{`HO`Xzul5+#ZQ)G)lVp7A$8;Z!$Lcczk8L+w*yC@s zxMzK`lcc<}Ju}myp8O;?$^V-<Q){-!rT)>CJ@w;Q)s&C+7tc5JX*}NkI^bCRmBl^( zJ=%@pt}X7Fzr<Ox{$7t!-A2yTKa9qwesHQy`Iu&W>W7N(sUMPQQ$8B1P5C%8ZOTVq zAC1S=m(MqB(0FY8MdPvb>wsh57tLoZ3_RBUD)3nRlKG6afyYi;P5F5JtHxvd#q$|^ z1CPlYEbRGzyvOKXj+12lp&p~UHqO*PYR0F2_z0i+(V8~p<6d8l$MNQid-#nFTW73z z5qWL3#C)F5M>!6-w=3;9JM-x^<<lqUZ)w>d9MpAT``7qa4;E&=Jf@;P=YPzDpZSfB zVSm%lNaS7lv48RV`uN8m*7z(AKKGxSkB{Sw{r}k6{~v38jeq^~erdpeQ<KI2xUb!f zx$a$`T{)v~rtGTn*)hT4StY!Cf1eNt&Rl%(&Be3lL(~@Sx*7a`{mIX7^PX*b!@q2k z_V?F&uby32bV_aW`H#X@_wTem`}ErUrPi7oE7G6qTnU}C$ss=T%<EmJPA=N#b^qY5 zRff%9u12}NT4nxf!T0KO8LI-fHSSAW`AyCDx9!4P@AsWg(q#~^e9CsD@dD$l|Ll(( z?$y4q_P#&S;tInih9e8~6}f9UBVXKJm@xBsS$_JbO?w^~-Ytqf9`{JLmRB|Ih1iq| z-m~9?^ds6Y^;yVFElaeTQsyCe@Mpj+MOPMem&|m7#alibFWlnX#o^a^Y29qerFZ$& zrj-5us`1urHds-*Yi7HM;2g1~cin8gcDGthDdYa4@iy-I!Y#sIHQutl4!Fg8Y4Mis zD+{;0Uf`aYZnAJo_5%0J>-Uy;TKnG0ytMAdlDoC@=cRUiJZZol#~x9W!%!Q2bmvqL z*6$TR>SL19x(atp+#_oJpP%RC`Txz%TYmmucj*7_Tc7{WQhV~B_r~nki%PanGf$g& zv$rH~a_x<v#iw81aW|XdCH>b|?(UwYMuMrn^L~Av^!)Z)-_)DTK{uyHm*83Xvev@& zQmEAAgJFJt*F#_4?p`Z(IXHZ6sz&tQz>ah8cgh9zZmQcgYbj6A$tU|=-^5Gq7kN-P zi{rzOMwSmA;E-?J$i?c()qi34A;F3#7H?7)p71c-bmGs}=!1X09{7GD{;TQ4{nt!x ze!kA8^Xa;V?5FTI4$ZC2e?+Y>Brq!0=V~a{e>GREKgwqk5pW`YBQvj<7+48w8(7Ig z#rnJFmFnl_IsMx;%k|$bul7HsR^5L>FDcfaO_;cU+3N}Wza>rF|LuxmJ#Xs7{cSHM z?Ef}l!v0?`C+wF}>D|ABuU=D#F>%IX-h@bdGxp-le~G>SZ6_JaPD`sz_;+4lQvQGU z4#)2Qem(!UO|U;(^moqcBlWuf!<l~eyKn6BVS4bdwW-{`>aUNZLjCzR{_RfNs(ti# z+<hDn|0Kw<*hH_i-8*xqgRc0B!p!BxFO81gd0Lnud6r2`YR%i-He5|_s-Lra=S@G} z{Ih=fnkz2J(<c9@e{uKceudnByNiGB&;9@1<k-6ZyLbGszw+PoU;L+^``K?j%KKaQ z_t#&iQ*S?AYIZ*LIP>qmb}<VJ#;pzy&P`g$x~7(Kme_?Y4a->PiRcEf<rbFL$jf<e zZBTCfz49_s*gyH%(+mxiOf?OkZuI-7FY;f1!T;G8{xiJ!|E#BZa=QxuwIBPN-`BSv zx$@>n$?_NbncmeGXw3b8?dXhLnQ6~%#5a`v*ZsZ!vT-}#n*^D8(=u}&HvE>)G7w4q z-!AveUfSgE`#Hbw>ltbvwb*jwlYN`R;k!Th-A@1C;r9Q!&au3I@!FXW+59h>Y}~db z<K6p5$us|c-{v^`Z}^Iv_5RN#+xmahywFuz_T3@#&-|0UTD`m7UDlfKz9<;TBP+=I zc)_C_H<w6vsoUi@L{o1q`}@LP-nj2yw(7$d_WpwI|F2Ff_;UaKBJbWm)wdV){z=|4 zvA#e5$^5b#D!wvPTMnyv^Uaywa9M-<$^Td(ciuONKAtiUP5#LXi7fh9-}`@h$iMm8 zZawwU8P~r$Z>)8ibz$Mria_hbd$wG5cG=fZ==k`*)c@;0>K*^czt>(M@u_~#`i8U< z|Boxbzs@)N`j7gPt)C~I`hQz}S(@FkqzBF){yXfQ_W$~i{g;3KuTHvrxVa^b+pY8L z^%8-op4Zy1?&m77sH9Fl@miI|_2}e|YCnS?+m$Ak%@=qTIosvN{VN+bn|r4Gjmq8X zttj|ac<a|!HXF~jm0uBGK3C}WIpL7r|Mq78-d_+5?A~MYZ~e#rj~@Pi-+AIcJLif2 z?wu$8TXUTF|GVqNe`$^r|2IoM`5(vh>3;>=&;OQ;KkK#Ge*U*({AqtL+(A9-{G(dd zDNg^^8=UxWzi>X|#tHxTFIM`0{5RK&rJyje?)q<^SP<9rqyG28C$=XI_m=S+{=Y7e zWci<ciN`KO*T?_+uGM!gX8Y#rE&opGyS<6YlXE}!cfMSC>u>pozo4ZfM?IhaXKQPF zX8m8?BE<Ut^_jo!-~F`c$+4oh%!+mMEVR!5Kc($;*!b^LH}|+OsrNbB<~MEH@~d_z zU(a#0-}iG?TTFLhZpGi-_hKA^;#Cd)+K7vMZ4$ckmm&Q_y&ET|N%6weJk7+d-%ci` z*9S$##j9?=wKcZ#VT;Q4xV0a0&6b?j_q%;;``qineE!jg+<W9tZhdVvF@OGr=GPaJ zG~{m{JmC7zG`utGns1-goW)0eu$?r_X49Cf7c$lQy!`3oa=cF-75(}gyKlvz(%PN6 z=l6a$zrMCKc3<^1!*%Yt<@?gtuir86`uPugc|Y&Gmwso;53AQte#kDLy3hRiDSh3W zE5BGzRo-t?H}g{cXNF3LLn5gUnhIw2N1u{b|N5NGboLp3zpT@$KlmOCFMd_jDW3FG zrM_G*v%c7F=NU=8O4ZZ+*1Km*+pS(X>C@TFN|&-q)zYtimTRZ4;<!BRq%v>&lrLK} zi`=em^-8<`u;|&@=9;n_DSE%JNY!n*Z;<!p+4moY*OPXJi}gG@E|-1oY0vA^`*-y{ zJ;qznSpW8~z2&h$UD0XJ*G5MlV|KMtVs^bX_jCoLXjj8qL$O!ETfQc(PDs2Ppz3ms zFCxM|gma>)%T$3#_Z2P2KCEc5Dq7L<>*I<RsiGAvw?3?B*<%^vRQuAN|25Cl3*1~< z3z=oK7N(1YIHmoPye6l$u+~Ux;e3%0r`0JTPPauvoF?^nc{~i{TzYy%%eIdzTB=U1 zXqon5MT=X}iWap`D_TBWk&W04p>{*4!w~B9ik4NUR<s=Zw4#N{GQ{a`Wr$O#MTk@G z%k+rnD_UMbwSI;u{tc!+Forn2esP{T-a*wQ+)Z^$u%qe}<;6l#%N7VpDJ&M6wP=A5 z*YrS6-KT+^U!MnX`aTQf-1;<tllO5T=hdeHoJT*cXt`q<;&k?9`Umw8C!Kz;4D)uc z6)T&(UTo>{TCu0eD?|M+&%QRVjF~-NFD5j3Wpwp;Wvp%T%GlB4^+KWFE5oSWD}%S+ zD?_5)>xHe{)Bv4!uMClXuN9W<UMuwby*i!-aPD=m-`%ypYVzp^9o60<*?;=Iyidqv z$vo-#AwR|bzr0A+g<thuy8qap{=aqj(;~gZo!^fBu%EJ0X8Pe!&R^lHqTchbjCv}+ z<kh?1OJCLJPW$7xep;4{nWyP?|LIw|caB{9mm>Q6%%9erxhsW!=bU@JMo)F_+TWiN zzU{m*FK6$yiElppI^X!U_3uqKySg>|-K$Uj_u6pfgHP?sixabg9?zW@G<oawCCj|) zrq4S2CoapbZv7Rd;>An+BQ#mR*q>utwm!A_R!&0m?(<2!=W7nMvGk=m=j}Pxc6F9! z>s*sfy6e-u*P3ljkGBxi-x_BXkhOW?)Gv>YaIAd0=T^{<2~|NyeoV{R-6t3HyCo{8 z{nvi4iEr~a$lCtXcD?$0V(+esX}yK_PaTuFak$KC?~J#5UL~8A-51k*H+N;ubbm3= z*x=yEyjQb>w%g@<-VGL7Dz{KG===?hmEm9Yg5EFdU2-q{Yf#PAzMMbhQ<v-um)fLX z`_X>oeZl;1sll$>{@?oirs4X(B=&PQ2R<?PZ#Wiy-n02u-ATT8`~AHt%J^pANSkVM z<aM#;OG!_!UvFzOU-Xt}R-Tv0ym4~gr4LX4E}0Umd{c1pZI6}S)f+OM=TB|vWGhSA zel6$MgjYFk4zgy(pXAL}D;-YTzCtT?wf*MBr{kx(owmMVDBkbAe#Z1o$^1DJik{_6 zj*I=;Xp(nE=;hY(xXBXRFD<>({kQV9*poR7dlS_4_stJWu-D(iJn3s^lX$C<xp|HD zsiLjR<#){GoTle1Gx@2v@bMJR%SQ|&kC_zuOf%Y=VtJXxDDsj?qt7%`Uzy2EeT9#w zRD$Fmf#fYgLVm)>Q+9%cK7oX6K|=n*$5Vb@K4MtNILksU&q4V35m60arFRV_nh{RB zLga-{95J1sR~f{u=k!vjP}pmF$LfGxE_YflsXj6incHzYV3$h_`%9sDk$ieBC7EJd zA9#68cU!Eg*IL4PUWfg<)2^2EY0ce=v4@OLAG|t2@8Qg+4~jgdX9aVwYX_?kciGj# z%3k2=HThQZLB^O#mzGpjq*?AzTk3hlcBQJG*Gr+oV34IjySDJ3*I}Q&wB)6*jzz=k zpj{@S+<XrXP0?HWVCK^Yr$od$t(#e=FEi=fVY>CmR!!ekU99W{g<jLwB^+donR{ue zazp{c^yMX;J8YMFANi^otMtzQrBEY-*YvofP0S9AOTAh6G-8z|IAjVnvTk7DouVdK zv0$15OGE>w*L04MfL$&Rnl7m_Nlwudys@C9(}8`dH;bJ{tdfA!OQA+Vujw39KuTIJ zsWNF!Q4@?<7&yU!yVQtD|B6t@LX*w|JX=|^G-8zsoL>qx%6LuZcoVS8WkLHTRVMo> zdV(B_N?x99Qq2DpD^SC}<<y0k`->tK62q$Y@KgwW-fzpce5raGkJRLeQ)dO-`J?ZW ze`5Rp@YDZy`22U@_H>ujr~MK8PZh3w7}_-d{MxAZ@{^DK_hS05{WR{`KXzm52bHg{ z$vZPv_-ixloF3W|vnMUX^72}xeasx&`<uiWAD+;th`exvH@i?QDdI+Hjz3TG-w6`S z+Jciju9*rQuJ@5Lywb7IjVHEI!tUpfC%*gmJGLbD{JPz8ab?ZNZ-3;IUvVV=`p~oR z*qbX!&*~W+f1ay1nxJp`@43sr@ME7ADV{8f75o|BbNO@qulU_B<aNYs^X~4dkWX!| zZ7=(!r}b{zvBmFQf8M=O+$EuVlV#hCcazP3KFYM+|2|ND;zV27f5vh#|2g+@|7YIv z`Tv~m|4ViBy{_t9PpzHKGwsy><!aH1HA{}9wRwmN|5@r~`*+EyLfMG5av_sGG5S>- z^?qe|y|uK*Jw;_hCX>Iz;_gj8nFkcSMRjz-H*82bcxV$Vx3{>CPCU<5*@(4zOHS7N z{@cCiPr0SVCpGc2Ob@kJ&*9&t`@DI+hMLs#nt$SJ_&HA=|L<h__r1@*@LQjnj;EY1 zy;*5@^=GB~zVPSr@v5~Oo)l*utn!Sq_*a|he{<rceeu?te}{)m=c~Cf?{Au$f%g8u z`L;9SdTp<$#$2j+d^y;?s8yZ8>_Azim{jAtnB$Bs$`5w#6fOOD`3PSjLsqA71FJg& z_c@2{&xP6-+%E16ZeTsmz<rKUOom0rf-|B(WWxiUgoBAMw#7a&k?Ou0vF2EkcbMSo zPA2#KPYWX*?#T94&wj|3x$~OP=ZyD@{=}dDpY<mm)RwKEqqxqtWyd#At9Jjf|E8dJ z<nQLi<<$@Vzcu|Azev6|X3>A;2YLVUFJ%9=-|~;Y_qcuOU-_qzrz=eR?F1(L%J?fU zReWv6=4<~SMio5q)%#MLbIYD-&66LVi;qo=e3|K##NGaR|02-ps1%NC=l<&+`+eWo z^6m`9B}*;d{JXFJUtXx-`@i=O7-|<B`#<-@|H#waa_=M7Nq0QSJ@}#3E`An&<=Xm> z`gMPo{`?bezwd1Rk2IP6h3C0aFYnrU>e<u(f!ofU3oKIa57^cxw;)IQR_IMR`E_qP z_yWq>Y?r^uiio`VHazO)=8v)eUI(r(`uU12R%PF+|3*7!-RFOJb+2E+)&FZJye?n2 zC+4xs?+>$n-<z79YqD~y{mgZC#{YS@p1Z>H_2wLw(CL@F*ZwYY|GM)=z}C3eZLdE2 zX0P~~UA>z3RBX8Pmj2K`wmggePi0FwY<%^o+n@ZJ#rfwy_m<ZjHjS)q$lvuOc->7O z<NGy#cE@y|k=$da*v<W7zZa*Eyf*vRNKcLnDNc3E1RojqOqe&Z<%5sNVZUz*F6k?7 zWKC`qnB$l9dg<LGX412|^!<2*ZlBZEZ{uI7<<^sbXiN7Vt!@4vRG&QEA#wAckfN^i zkrh?!oROAp9jBz6g7zt~JhgT5dnTOpX{E&Xa~4h>_e6Iu`_$5r(s}pSla9N=zdCH4 z*gI^Kqr3L5YVY1V`<hU7e63&4dMPo<2r_O4LnIrnUcDB1mtF@xYa*ml6&K)j# zih{d28-F;;Tq$E!<ePo8CH=oMN3*BQqpdzZRnK@0UvAfYbU~Uk>C?Q2k2Xw}J^JwU z<0DgIn>i&j6qo7ss;Dejq~Y^L{osB}&OfQ17t%cs{BJ&)^k1d%UhtW){i=-r$_uyr zVdZ>NeKh>cW>1HB&1YvKwfF7Hnq`r9M(5+!a<{I$r8alGZ`+@q^7YH7FIvxg{pIgl zpEr8qZ!2TLeye#x$*z?vp8Z$Y_}+Zh$3?mG?6#cUyy&a!e$Denp_!A`w0*VJxwDq< zkLKq&U+@0v{d(V_ym?aiZi^S<TTObmE!SVM;rk1RxVe(vv(8!tJW2dhH0!E<?)Q0L z|0SI_jp|aU;}WY8)%l^l^+@``ru$8-_gT6Baf{VR==?B@_>j8c!TE%P%MUiiH?!Vn z<Nn7hRwJYH!!qJSX%v^@KR&UVeF3V1P0S%3KOf83A7Iuxz@Zl4UC_v7*u)#sAy~7Z zd};y<m*M0tr<`3~PH%WcTO-=KoOB#r6|Z@_Dqah8RTNvbNFaN~B7xNl7YTe`CT7qY z)ugR)f=7wVh^wC`fk!ljC4GfzgmbIpL1~dR_6m`-`HyP6n`|}C+&lVn8cTD)jMRrU z-c7C=XO5lHI1^^1aYoHp<IFNcjWY&SjBX1J4tIWT2-P@K@W>`XjLUeA1^<oKLp<u| znA;W_XzMRBSgo}PtRxenL>jC_q8+T{+Cqcq=4lCXT${g3h@^d%6iKrvlrs=$mGqm# zuH%x}Cj6XHIB>?pBb5i-MACF!MbaKBacz#u;0#-2z?~d0W2#u-jH@XDGv+Rx&uAYw z<Do?Gfk2V8bP18PlAVD*u8H58I}YWrHXq8_+;QlQJr9@h<c>o*Gdm8w;oZVg95_R@ zL*q<ax5gQ(E{!vfTtw2EB}LM7T}09*@^BgN;+@VI<Cb`Bj$@+RXE~9@fiwO#ZfQ+0 z<udjcY`wXihimhFSCO=M1C2A^x-`zD8Ec#g>(V%LtxMy~vQCXNX`LEp+`4+$-ng=V ztBGcx6CJkk!~e2QP3IoTnSLk#i!b=!<lM|G5$h1LJ8k00mz`4o%m4iKH&0VbUm~%& z*Y85T<H4Wr>L-}}KfUtT`LeuA*3+`*{EwO<x8VxorGtjsZ#Z}F+SGpZ)}`%+?KhM+ zCce4omVNX6tZOO#Q|BI)D;K`;cz=J|O>WMnFn_I?UsbAP0{*3Iwr^^zwma#x?8?lR zzn7+POxb(gR6M=&?KHK>*_C$R3#9c=bIVP7dMnSwW9|&eka<gMir>w0wm#MI`eL8a zqq;4Ndv+*96gY2q(3Nmd_F$8D6YF*s?sc4EcP8JuzwsS&-m0tTxBQwJW?wz&Rr))Y zdTxorXK}|aOZsawO#XgJByX?AonQA}OO;%|8&JdEeRArxmETVM`LCKBRP{)K{cjKZ zx&OT9%KtHMY03O^-}J|SiRgd-OEsH=F2wQwj5mMyVA0Bw{q6Bj92_6N@>dJr-jl7Y zczErO%*xbR3spPfZ~f{sJn&6+-||X}>|4ERH|1PT=Z1gQzIHXId|y$v>8vpI6R-46 zwed!Losu?rYX0s*W*djaJEhKE&B*&GFaNQ{Y5HW3HLd@@O3YK^DZcb`KI@bJ&P)ma z@*N5%1nd6yf4|@Gu7!@_?a*`ee4Kn-SKied-mBNsRn+8VYutU}uX#&L*7yAq@9W=e zSmUU9xM$_M;{T@q_0Rpje|yG+uU(7ljxm4#-`e)X`hWELv;T|J{vH43a5S*z?9ch} z;qu37{(kmIdw(?|?fy2op0LHbhNicgR&VL6HS(ER|30=SclvsU>is(XZ+G$}&tN*2 zS`lZf-X!}ocV?%_ZRcx7TT^VCTTOj8+uz=h9;BU=byHS%_hTp5Gfk&w^wma5TXFbJ z-51}wDfS=N)5!sI_pJRN%zjMr^5o#lUGx7-GJQSx^Vff-t)K75F8%ktGxu+P+zr#j zdlPF<{h3>Rm49E#57yv3E^gngqQd_t?#j61_3!X>=hmYeO@G>IvVAR^`=Qt8Xq5J! zXJ7PMzp)<5*>S`9)kAUS4f-Euu0C|p?fq)^{{pHRcE8kL6n~xn*m}zAt^AzVudfgN z{OKB(`Li=Y`ST`Tjm}>qIAiYom3htgguTwzdluxIduiAzMt{xtv@S~RYwW?zG1Fd& z-t_wVYjOXpyL<W{?A^ZfRq(#04?fp#4Y?mLvh`22`4z6;`5ZZEz6&3nl5h30{nX;c zelL9MzQaN%GIjmGU;0v?w{*?f_U~#-m(IC7H6rirU6&iv`0oGQ_glGCbzKtI3$L8b zJI~o|ICSiKrls&4ncYI)FBWN7SikjB;q%`u)Oq>xI+sU35)S@6$i&dVD!{<);ILba zQ$i#nO!YuOK&7a|p>^FCt;G8o*xn|oEJ(b5)Z+m2Zi)NL>-iLtZXe}HaMEQI*}$N6 zMJR!h`vH>}1Dg)3WCg>81I-P*e|d!uFmf+o7HeSFY2Xh%%hI3`uAtx&IE6uQFC$CO z3e^S{F^A45CZ7bxIV)5dT&)*#cwB4}V$|Be5SGBW;Q&)cBkO}j(;CDh)^Z068_a4b z%w$Zioc3+Of9<DvxkvYGXDTfB>sM=2{!};p)yAuK-?&!nzP9-16WhG!|BWZJO9#t- zHtvf1&w2ImpZ5)q1eQnrZGZRw^eGQNUoW51iNEEAL=uYs%SO$)^v?cI*TKC)?NaVb ztRDVjmvs7c_CF`jg<t#U&iub|?a~xuv*-2tCp}hg{qA617ynLjU)J8qNn1~`A9?jI z`pB#LnXbD2pIW}vL`P&6s$W}uxy|`g+^oLH1ua4+p3co)F!kcL$6`6cuUjK_E#}f< z{`O<lL7NR5rexmi*GZo>SHW8<^s}d6=x0X3(9dF9QaZC&MFyXpl~vZ1Rd;B^%T7y$ zYlr?P83hTkFXL4-(%yRGXFTJd`w1Le*PiVcdHlEkl$VeHSr3`VO25tvIQ+KX`a3@6 z`+nh{_RkmW{vYspz7x~d-|=(4@Av-nU`tQR;^24n0U7&#`a3G#`lG)5RsHRM@e?<` z{Qoy2WNvAy{+h5o{^ur~{yTfYkDPl)f9hqPoL>K0ednPcQ8u%$hOV>t!4|)={ZMG% z>V+Ykw~mFb-9B}FSS4rp-CD!=zw0~a+<(bg(|g!?=d9b4e?&<iKfTqyGPpABpTN#@ zS6rUnoRbkbU1;f*+MOH!#N;S1jmwt*`E%LRDZjSro#H(e8}P^WRN=qyJwMy_$2{ON zJ#L=2OXiAOP}A(x6K(02M0Nc?a6E6iVHP6t?B|T#t(`4>%bdddWd(1@GQR)d`zAK# z&%d*WSoI2S>oDq+>}Wgcm;E*6;N>of$S+&M{Kf3nUY&fjf9tRLFXlH3Ux>0TWQkg> zey(9_RqJ7?T3*)dFSjkunB_b*yuE>i{kD+$!JNY`e4BZNE6$klJT<sI;Yjkf#63MX z+ssAJnDI<Byggw>!ZyXIV>!-Knr^loGfI<|(wvd|VWQ#ffWA65_Qf~Z9;M94)i|EG z?cnTxHU$>5aw}q(b32ohwmB}Ef6&qJ_JnmAH`#P=KbVqpcrr&p|Fo<fdyo2gc>7nU zzL;_3(ewI)zb;(Z{d0fWzs<!z=dXY2YI~x{mp!vSCVAt(&#I<B*4zIJ-8PrGB;j)4 z!@_yDAH094|0DjR@VoP$w^U7Ve&x9I&S{6o6V>hZp1NhT*Zbk-IsMA>TGj>dEt|aY zEr)xBvBiejOL^Tx*St@BXs}KC|COq@-+I2v{)@EAT`c}cwddKk*;i#dl`Zo8UvHaz zwbuVu#%uB9wRMU0{|=Q^-DCQ}8Sz1M*YTbor~j&PH~tT>da>xaY=ga$>=$9VUcRiO z>b-n+mhNWuUmboN3E8yexbl<w>b*uLog57;=}+pLzbY>LdA^?e>W}^1AOBk&_|HA( zv;MjVPhGjEG^gjS==Wy65<lUfX~piHuj-f1^?dyQ=(+!i-A%Jgi>2ngNUoe#sQub^ zejitLXx-wIA=mP5AAFlFbT5OSP3{Tv`yafVMWLAoRwvfB=f7F@HuC)QMhS_^>Ob?& zCff2J{rmbx(f>s8vvsagm;C><MLn`EnSERG&by~sQVcZ{vlt)suVB12)8yUy!vB-b z{hgmBaHZn^<s<*C)z#Ci-=y2j_|Jal?tejnRcHSfi2k!bX7_);PUExx-_5t5|NlPz zU&QDCy!WnYe%X`O`+K+7yZaX<*Wdc067lKZ45PTYk7cI)s$crx%KvvPK7aMSH+cUQ z4&RyoYW|Ks?dAUtie{d=U$teU+i%lb|3#MlnJ@O|@{IqNkN;2R;P#W9GW8M9zxWIP z`W=|({J&tVUAyCOnQF$L``i9&Yui?S2zdTtf73Jj7hfH3{b^6y^WWC!G8@1AtZ7Dv zIKS_2`1$@|BU|&b|8rOTe{cD}Hg4|9FMslL!_qu2|IIsU|5x_e{^{$B%h&z(JGR?> z$=$-peyjg`pHFNxKKSg*DG8V3(UZ33*w#q@>oc<p6n-|#p!{J~%t!zF_CSF*m0ba) zI_<v|oqk5#e<&SryFB^oWYhn)=VnZpH|N2E`5zbjeE;jhf|}>{ubUwQ9oIfJ9Tz%X zs{5e+^Z%e$ezSN{v6`*Hu^CxwX5IT474hX4!^7l$??cD;^M!=}E1$V5E7G=f>&n#$ z^PViMvr66ZYvzYJ8`NqyZJ4I5n-g6ZdPDBM>)ne*-5ZR3*1yX#^}ZWx?sud1sch-{ zslk8tMb7{Hdwcma{+budYc}tY)Zbs*WB((K^`C&fyM@gQqyM=_(w<h#?K|~x^Xm}( z`+vKx|4-vc{cm@?x_-9il-mK(3ytr+ta~Dz)yk>KHTUDZ2l}C}UaDu#+4|&M`S#mW z+s|KmS0;QdSIVO6W993xdm$=2HZ0}0?PTGLe{KF`mk_Vg)!g-p3%)cuTv8W`D{C&j zn^&H5XMbGk>qGL9u{Wmi^>ar~_gubPc>1!-cg-J*losU9d3SF{R6(KD-MKp~*X9d` zFQ2x<I-;O*!-H=L2frR{`rXV5UWq5RfI;T~V?+bX1_tf~MyUf#HVv#D4BQ5ex{X{` z+j|eNu3as7VnL}XlXOJ*=f!VJ805F??Q`JBU9W85wETpy#=_tyoO3Q{eqsTw%M0*c zz*^_d03LnYAHZasz?gM_=}yC5+pT}U`4ybLQpRMTknf+XZone8z+<xXtUDK;924Fl z{717}_tuSX%NPH<u4kyKd3VNx8G+IK#{aF4ESUR0y65};@KX}WSy5%j3e@lXKd@lJ zf|{@LYyV$f`Ro4iPm5|!6veWB`LDEW>5re2ief_lTkgF2hwsJe>bLys|E-^W^S#XE z=*KON@x}LB?nFtaYgk;?4n8<DZm;N^m!iua_^hABHTPxd_ciP<zRT=&&H3_nztdyc zdJf-f{}fDOH^pa|6>q*!^CxSe>oU&^>OSI3zw$$vFW(PuzNHh;yt_Y;_k2f|M}ocS zlVb{|Yp!J|zDZ%<`t<Acmay4UEA;1N=x&L-6_>GjVc&;GM}9fouIv%8I}+R>5g(l^ z{&V+*x(~ivo&WB2N~v~z`(k5I$ExkVy}N$In;xs}lq!Dk|BByo^;L7j_pUwmE?Vcv z-pR|p_0+b#uie^J@Z4hh?Uhaz=0{iODDp>WzV&1(F<vDu^;&WBjfscv)xCMySpM34 zLh6zI#npN|*?;B!sj+-f`=#*7XhOI_<wWUMZO7MNP>#4G_pa=Amg|nUH!gPsFMRMV zc&*bkhN~JWk^$b+n67H2SO%<hn#OWfD<yJ4*vy3s!Z!M?JrxqLI%0X)*RX)qE}APs z4~bmSy0$XZRP>70w3VU!Qo#vtBV~Gx_atUsyl)v9w<5AmAYk(w@qo=aVgZ}ihy`q( z!E#0CGV2u`>Ch`W6_3Pp9Ixmkhg{KF)HQ3VeAFhDz|B2k0h>z#0yjV4OxoZQxOt0M zz~(LD0h_M~25i0}9<bSfDND?s(bPNl*ZG4&%glRqlCS7gvtH4uXp+#$yrOeE<ciM! z(7??)j^5KOvZi%vWlh^2dPS$3^@>iWR@Q{{iYqeyZ!Ue$^Zj?M`|K9JZA`!Idz5=# z@=5*w`QV4V#4P*&>)-w_oBBgOaLvd0H_twp*FV|j-On3`9?$D{wK?~-X7ci4+sn%} zO4g-+f5>(2YfrFK-j|R=S-g)A`?YE4u0Q^1t=Uga{W%%uw#Q{XxSf;mQ(Qgl`R#Lm zC-eLNY44YakINLVY|6Kf2(`L$|A_lG`z0k`B2Ktpjb7~%duPFs9p{6R_-YoESlLFs z-1ExxsMLSm-%Ppcmx4ch)(M@Fl_c4)@7e#|%(W~>_GsMsd8hX6vr_&S&MUs`Vy-<X z_M>{q3i&{##deWC5?|L^i}`&5Q`<g(DY1_dU)LII-+yBGDmpD}l52b5eWT5jbnJS5 zU7I<zFyo}tkrnnm$5+TNO1RSh)bQ1BO^L6wKTCY&wQ;MRc6>$r+^C2(Ca)&5b_ed? z(-iohr9Ut})ZkVABg0qnK?bk%pBTPc58?+HygKjXEoSES_ac|u-|JIq0{?r)yol<l zKk?(frKRoJL}>?kk^jtJe}1cHYt8stpMI?Vw({b`y4Tk%er$iN_WISo-v#WJa%)#} zD4o;mdbKn9Zbabut_s(k<*JHn?sGn#RiVGBqI~04?uk|}j;2M4o-}<_|9;DqB_=Bv zcDnuH=i|BX>Hj*(|LaTsJiPe0{@~_$vG)J&t9I%<h^XMn-LgcZ>qXwvX4Rk8yG!M* z{!OlaA@y;8hH?AWAOAHz-rss{o!I|at$*$*Da)kW`AwHuss3Yceb)a!>fimDAMbCg zv^Z4c`}qIyx`{LWSO44Z|8a9kpx1mmSM6G{kQZAHt-S4g*CX19!92v9En{lU^r$mY za{G6@{(n-rO=Z&REh6ei{$Dt7pys(fYu0mnOXGjx8}?`vMXb~PEI<84!iKx|F6Eyt zKi~L2Epf)D9nbfRoBZE+b6afD=a0gNqEq^wf8q3e$oSW{DB)L-+K2s*Q?B2PHg{lI zvGt7bXN`mfGpx@EH?Ke0&hF3gSvBC74)5LbTK&mxGenm>e1AdqjOz^XG<(A}0jF1V zmrC9|?{g>7QZKXfTv?iu&iX?=0qv^oYmRPpUnUL`$W74|J~VM_&+`deRa%v^4Nsnk zns({FqE2Os{;hkO8_K`=@e1Sy%lkL;S52;8ae`Cy^}qjHdJbkTo)Zi@73TW^2BH7V zzf4|V^zQzD^wR%CA5TADzfvF87ma_;Pssl_UD4w;sHt=P>a_o7Kd<zAUVqkppVzO1 zlg}?TeVw$W;Lq=%#^+3X1LsfuUn<PE&wQT!mup_{E_AZ>cgkN&kor+!{7;VU;(ssZ ziQjtVJ_<>{|KXmq>D!zeB_}yI-aJ#A^!elLBVsdGyVM?XGF<RKPRgYD)*2PZA|uWT zmpuw(+?XDF2#Kh7C8}3XNU#XW*?Q8UDM5YX*GqXxvw3}wimS7A=02Y)ez-kERQZ^> zqS4<~H|!2hXW8*MDdo4f%7$El#jj*l7~e{8re%95{O)4e_}znHwtv(0<w<v}AA1O7 zDNRvweDUqdY9qPSHM1>r)jdVt>Tt#j8EUl$-dj|Bc%@0|+x5Br0{gC43O;!svb27= z+TVSAamR1vaqKOQYk7Jjal+d(b8niJE!um5yGy%+b6M>T{aJjEmAKDXP3iViSK#jT zQ&;E)5!`(s);f>&w-b(f=tfQ`kz$CP0wQKynRN7Jl+Px%zjqHf9se`k@yMTW=B)hh zJ0`XEpZO#&IP<Bc;LN9-f-|4m2}(Xy6O??aB`EpSNl@}>+_7naP0b3E=bTZPJm;9g zWS0t-c1JbKKo_-}&5ml8Auej2N978XQYPJXNtxv9nli~z$>`)KC8Lw8l#EWQ@jN@Y zuIq$K;SDPbDWj9#JVqyZm5fd{^BA2><}o^Hn$gLuG`XhVQO)zXR&ctDnkC<L%|fqd zD?fV8dHJUH{Ez#3*&hOutCHN73IC9n{Z?uIpZm(Q`dwcCk4H~?-SxnpIujEscYIQS z^fk5B!#OAYT=mtj>#CN#!0O%kf~)_hiv9J|7s{5Ir+Dqozhx2GkF%Iosxd9U%sFMB zV}ePuLk3gh`8XCUYoQxFsuONb*!Y-b{o2q!>nlF<*6%#`{I>U}J)S#@+c@5+pL|qc ze=;I+_USJ((mJams(Ev&`QH8Gn>S5g_h`G(y#C*whrVV=ZE=tOCVt^zZO{9?>lo8+ zAN{(gdCfQX<h#Y!CavB5=lCnVZEw_`O1D1Sd7$<{6L+BZ9S)rxLJ<Y(8y*BD99(;# zDZ7#NI}^7ao7kP6a{)^0+Ki_hTFE;7u%y@dK<;(!>FR>toByj7IBs~*mT*w?V3T!_ zm`lv0O$v{uP5Hay@xRCd|6kmydR~zNg~6wqI#w%jN1avRHZnZXw9NQK)4hZVtcq<- z+{)Zejr{9+bTp<oid~toQ=hwvw|rZ}>|isOn7el8%lhZq9r>I4Ik)_r#>RI)PCl9- z8d?6Q=jx~I9<$jewz@5x7E^QeoLr&bKYcUH<bbGE&2`5P&-pJXa^eNkjxAr#tWins zQm$u=`X~ObUOu_1?PBYv|B4H%s{RM}tmN$b%bM|B{$2g`o+&2ne|y?z{1=rHa(Y+q z^6SBwCkJ(tE(c9MrXqEjz1P#{>bw2U@9K|q^qe{Jg=Mkvxq3qhr_bO2GyTi{DZl(_ z{i*1<4?pe{#j?M)KYRYXb<vlnKaXGkS+gOf`+QUC*IKcC@n?^#xcbCSsXhDLdEvb) zNB8dk9(kzJ=dVV};`5h&{avQ_`My=@&xXsv;T3xGc0GFJZ72WVD)E*2Kl|S`o2)Zm zo#J+`3DfPKx$D<~IV+!e`Yx;cl{t6ie9nt@*=Z%;BTt)t`)QMW=!*UqJG=OAG4D68 zJLEe5>%+y%*X>vNo%2E3vh?uA&%x&ZK2JQqtdxCO-oJl0uIO$)8(eJub^fl>FFh-7 zPkTFe*W31g`uY9mMauSUbG{*YE&9gM_|Wp}zTw|Xe)MRs2rD(%D^<P1d&iU8KiVGb zPT3Ih&))PV*JEW>Gh>%aX&uvVNIq({TYGFWU-j2*Tl#)jJ(D?cXs&2vmD<^RI*nCK zJHKt(ub104Z<5!}yft6<+MK?yifJBK{d3v<?d22FVmq|D8KgPa%azOTOU|_3xLS9D ze`~`8{WStfvxT)oS4MM%{h2lYc-Q@l(>FfaevVzcE_wxP$3jtiCgT!0%SNePy}bu` zBN}8jFjyxrRvlpa)4<BX#0}~LaOfNm+{!YkVBSxbUy=(tqnP|l_E|P+%*mH?wr37` zSo2x!3RB9%n$H=(zBBA9*jJgH1wOjuqKrbL(FTTe6Lt>;-r`21zdRS*bQ{fQajOWl z?>xZ##n?gi8PnM-OivQ^3fQ(X)Ls$#ad=LG+S&>xo+Q5w=35!sEy8W*Ml~E=6cWU! zm-%#xgW8?o2Hz~sl1r=(4<=8m<7{bSl~}sdJ(q8n8GGz$Q#-@r3tNBu$oP11Y5I!$ z@BaIk@|A_KJ#hS`ucC1Ok9xD&|AQ-kt)KIhwg2n1*XfS`eV^}K_j!@q>d*VKwa(nP zi2b2p9u;fwbeY5XrbqTc;+=M{+RtjQ?2hjYoh}g<ZZ8$!9}#tbf!^o&%k}#671n&H z35_i@oH0-IYyJJgSL(Z!<toyyJU7(6q8~L!bRSQT=(W%r%4>FSjf<#%nf!3|;yt?_ z-q0(3JWv0s@9Sx+XN&WRY&%oueRl5ZRnhLtSKMy<7XC6aIM?>w98=N%og04t-n4A} z>hHn(|Lt79sXTSlEPrmlook}DChX+Zm9z==y4>vV`rc_~)2oc{)%Rvry?FYvba&yJ z8}oUB+vWvFXfd4EEIC}q693w?GhKA;{RywLOjcdpw6~4p_}%9P0@*2>ISan$bZj`< zzkbb+MD2fV?>m>IZ)BPm@X;MSi59{ll(SH2!UJy6Mwa9^d`p!goLv<TN^3E3J_``^ zC}{6;XtE7q5&GjOm=O@ja&EEM1`gK;tF^igB<yxx%P>2D#m#1#^E!sZD>zgv+*}n7 z8i_PkUl8(f7W`qjwINr7seI)Pflo`jlnN}jHk4{G-Cs1H@pAynvK2Q3F2!%P4(U!v zXl&0Cv{^DOK`G*ZYZj;3rQQe@d2UrM&NML<E=K!@h0iDG_{wBG3J`8rT`cIzJ<XMC zn%)W)-CyhfUU<^;BqVuS_mz6*llBGA|16((^5rp}<2`B@UhTj3d48~wZ0VoF%wOvp z?oG)5S3F_a|G6vw+v@69tl1Fr`_iv`)QN(MEC05NuKIg??GyVN%a90}=*jmdCvCWX z_0suCbF3T|?Xvndq3Xv`hL^WzIKP>gzK+XXdhdF<zs^(orp`V3Px#BuU*|u{p89&L zz2)`m>#Hg&vs<N$(^i_FKk;g<`8AF+=gx<0cYM#e^x1xu9owZ<r_7tMw#uY3TsPR* z<HMJ_xDTu1d_VlMi_oav+Vy1b*>IimFZn0aFREVr-kluu^E!W*Xj-l6#$%>QN8kJm zTH5jb$f=2cZ)o+}C!73@Gy1Z7_uI(16Q-1Dr+WHN-(?+}TfJ_%`^-mmLiLNJFXw!3 zzxhsL_vhL*wi|aCyM3JZ>h`s2`)yV0*ZvYSll5DWHM_!iTHN=*t?Y5Wq2?cRs;|aI zZw$VB<nqc_YWMs?%|BK|Ir$jgiPsepS&%hRp>b}MN{2&#irNYgae_hY0;7%s%T^ZE z157~;tXmdLQ(%j5;LYMJYGD1sz|FxlZ4rkJgXaUs3oZZDI2Q%jS;)6nGHz;MEn(#5 zU=|B#``5&IseyIN!f6V85e~8&8f-%T3Vm7lYPy2J(#b5XD}JeZxLnnDR9;obsTBCt zJD_t_e3MVeU!jskucj+ZSypHIb@j@4@u2<w;VUlsHR(;{fBmn2&Bab-wxy{{)h!qQ zlb(|Q&%0p5wg0cR{_cORd;My|q@Mo;PmfiWE_=i?qxkd`{%L0f*BB>=X<4&eX-xNt zVD?Mf$aW{}kkp6cXFeUvd&PBrr_Qy8;F<rezb*W5GOzETRDnXX=>sjEYYktOt~Jb_ zZP#Dv`Mq_XX|3dp+^Fz}{+D+}FZX?|Uz`2w)r<NP>Au)&xpzKq&bz$p@UFAm%V%sm z^Dkz)_4;XfuU~vGz1`+_Ew^8O@$HM-S~Jrl?C-7%i!R;$xPR7ydmER}jhnvy+w8P` zciC$ysw{rzKCg<h{}8-HUQ%lDOM?ka^XA2|+Magwnz_w$>ZgyT*CuyNku7Jh{N3=r zjX6{+^8QEZPs^vixc_BqrG5wBopU9|*L-KQnplgn`sJ`{9^evP#1i#p*?)nKgK-Zm zog6RMGZejPeJMG)*3x3;R(<yEQ{S9ED1UiA|Av;Q4|cApwX~S}&+>rCv}Vg=`($KJ z+?d|H+Th#MguNN@e0h0?iavN-z2Qvrjo>?HmT39Ie+$dK4Kjb$a-Kf;|6-!$k4=21 z58A)pAX5|3{`7&SnhtxtpFQ)RM9UwwveTN?UvH4Hach72Kuk@C{rlxa%Zk-}rw_XO zZhM;0aKdS7J!j6n*ndl#<~eAHewq6^)ArCTfom-VoNooMIX!c3_j%5h{rYGANzIif zBPIP-{%2)>X8+&T#AEjVr#^qfUw&Jq9+9wfSuy+b^4e+9CJQgW;n#nC^{BJk=W4s3 zZa;SAKd#yQAhlL{`pmDAe+u>Lx9e4H3`&i=?C||XBWuF94^52va;YC*#-96hrhn1W z)8QKDr>#3-*Qc&nAgy`V*g4*B>%t9@%?nobe+~I?tZLb*x%qegfX3V{!e*~5dVXu| zy07yO?N9#yzHl|S$KCclR>u`1?n~~j{eI%D^@F+fo0MvTIpn3XzbgKI`2Dw4z(nVo zt}gLM;URlsj>-9c(Dypj*?)1K&!hBKrN>9UYj|FBn`z`Rsc?$s1Bb*DMhVSsO{PLI zTjD%;O}bfT-CCj_zUqI8zh6<r@B7U^>oYFg{bR4-V*UT(%s=<bVs>ZjTz0VP;4e>e z@xJ=!%QB0O7cSeIe<$YW>!-ohVd@{^`Mq=smUZd)3eH)@{K(9xart@8?`!&NN<A-T zDYsr_-Ex_6d*$?Rho0NIFWvonTKn>&Hht=xdGbMazZF?_J-6TNGxf;h;sW9FsnsnO zX7L@LH|}lQaBc3Mq-vWNKV)aGI3}{{OX6$K_0hH`<5=IwH!SV-G~c-8Bj45siC2X5 z7pt;A$_(KZ$X}e*P!Thc&rV_9kC4?(J9;xXrEEgD<|?#Kv$`H~I5~`0YRNt6Ak*G} zoIm!tE{it*sCWEq-|Uq8t^TXQGQJ+;W$MeVk{mj>{pUVk?`gorXZ7!C<FE4qbKd_E z-|KiAlqk}7H>dC=@BaV#-RJvXJ|1dH?e)1ZA$eA{$99eK*<bGO-^TsjhCA<E#?uSY z(mxL03A*$DYLRvImWxr}c1y``+_3s_#mcppKHhiVd_O)zajwPf{=YvDKYjHwum02i zJwKZZ)K=^(GW)Fl^?FzQhy7n|j{GUBoE>7R*K+aKd#@Wdes886539QUGkN>$7v1~Y z?=>fC{VV#P#`xO2e_MREUhsjswTa@_ujpNQSaC$UFeOcSa_(%I3bnNcg6~<l<$eXU zKNEa^)cLd7h5vjXl6L=%Ix@R(|Fx*BgR67DXE&Ayd)vt9R9HrQDB1AfZNkCF2b-=p zvz}+?zQ->nz@W2$Ve9nmk++UGNv!qt$rir6W!>t&Nje)Z{A+pFzv<ln^D?t$yjt*W zJ^QEs3l`M;jc-%f3tp49S69DcPwLKfo<H|TgvZSPyn9pK^{k)!>-oa=rA<%1`}}dl z*8}Z!r_A50aodN*9r*uasd@OfKMeck-2cD1T=j!*=*{1vQ68toCM?~elc*cObu;># zL$udRr<P6m`dgOr>b_OZRn&PD9J$CqLU4-Xl@oUV0+pGRnw>*KL|Pga@V$^=DZL=5 zv`CEc=lx=TC*hz4y1v3sRQErr=2We${66P>&2zoyaY@oOpR_K2cq=~BP1^l)?{xoX zvvs<j9yMOqH9PHWxry}8O^+KF_HQ~X`aX-5_rJKLdQ*^pz?04e#mfz%{+x`I*dg}b z<>%Mszt;ah`+KsWU%r>+-lK27X-U7?7e2FkV|@9${c|^!e_x!cXI~Wl?P&A03zi8F z!b=Xaf7I<dvVTR8x}BSQb;tZljc?O8o>TlG6Z+=JdxnGGHH7{*zh{tVVX<H0Q6rf5 zpud25J`2nKC5J<gm@^)<XFTZsGR5rC@{mN~c-Pjy>%szVG#wHXDO2cGyW4(5yGQV9 z%Rm21{>BcR7R~Zs_G^X39{l&7%ke+E$N&HA=aR4dkB#4G^F6X7<*wk$r$2k&N5(~b z-}Cfny0Xr+qtz<<XZK}A&fn{|De)iU>0@8+PV;SE=V4Z!s2aV<ZbHu6i@}?|9ydCD z`poia)BU$h_;cT~J>_=$&q?8SR%(@{-n*Wdo{s4*T^aJ}m)Go6>*-mQhd0~}y148A z_F27$OaDKYi1}WhrL*pL{a^p{rT;&#d${XAyLr{x-~V@{hJQYOzRz6V=GM8I3_ttN z>g$c49e!icFIDQk{>0JaU+TT=&c4aBoR>3wPR8|vMM)=2KX<Nw`{?|o?VfhOH?Moz z9jC1l9}}l*d2`0(Ie$LZPYS-jZBtF<hYe3w|DCS;|K`W$r?11Kz22<mi}aIji`;io zd)geyn}>_c4(I#~yLaK!-G*?L`|f4a|7@>pb3IZ1?x$h5*1gyFc069q^-pcbw%rHU z#s7b-b(ZDGh0jMX_`dO+k@rG!)596>9{C(96iR41Zl;mPEu(FE>*K=i=sU%WHo6~f z%Jg&D(WdlJ+Ul001-rZTy~P}Nnx&gv16cSD^&VW8B*<{cwM3Tpp>h;ULxGSklS2h( zH^T-Y9i|VBEZq!WTvTja#s3ShTjcShON>!rPOB@!C)EfR2KUg0{TnzA#Bhr-9&r79 zAT@$T;cAmB!?x&EK`abA&MoF(cxcnF#L)8Fri(#<Z%?iu!=X+&R|XFAuw@(!A5>ir z&D^49^U!xzx6r#KJ2f8q&*~Q9+Z#L8{y~UUl}p9cKDpFK8loSxWo?yYyeEFpaXqxP z#H90suIr(lAX?A$(B2Y}&JX&ohYo`H1_}j+3Lgv=3XIP7a_TcPnKLulU%se*=wiv2 z&JQNCwMsF*?(;d~K|(h{LT0k1N_TXh>~T7${bbKSJv#-K`1KE>Pkz+6{%3uT^S*rt z+%1pIJyt(OwCDec)?fMm&ir?p%fG|+sZCzk!$11zZ?`iq+<a#Ht>B+8)}_x#e*EvK ze065aj~kjxA74J>VJyO8b@BO%Cobo{KNF7bmat-9liqS%W<y&0{8EmuSO1;*t=rDR zdiZqJIZj5=>s=q;&P|k>m1q9rzO&So6JKk(jr2cm_{x4RS^xd4J*mGQzl+k2UH5d} zKaq9Ur-o*2y6(I6!U5LRo9p#=s84-*#8p$se5pr_Xlm7xo1urf)hlIPclyOlS1OF} z_l!T%n{WEEn@?46`>~TzPQLn%VceU3$=&$=UH|Om@1?2P%l}<}#ZwjU`+t?o-}Log z|8p@l{rf)uU;dMt1G(Ej-`M*9{igkg{_SV~wD?Wj&r=<j%M#z;|6Oq}%46zw<0W6~ zoSK~vyCnX*^KM@skIdJw-IBZdPFqjPvvCi(bMVQ=%@2F|mTi=<FWjkjxFT%Rp45Gh z{Z#XJt~gSx{ovv0Inj*z!rPem=B#aKRMMX6FLX;}8-v&<7e)U)^2=qo3_s^3ExTXr zpZ?EW$9lg<{DkG--T#GO{I}m|=Bhpan6K2GICu4|j=(4T_^tJAdiL@04;C{Xu)g+x za^=(i8-o)6PPhMDzwxu<&-;~k1V8;3KXzQEsQ2gIBQC|#-CV)^5#_h+|2>F&|EvBe zJM%KO9jo3>?%8#3|CV<@H%{QcuPD#AF4$aSNn9<*?XNBs`%`nZit;b5DD?0RU8YnN z=jpn4(m`3SjVp4MuNO;mx;&5zTGskH{;`SU+SvCy`WD)V{#*L`{an`7x15fgD|jzc zc6CLOwr^>&o~x^9j`m|q{+%|@m#w<ro+0IJvvN_Q%?GVn+l0<9%9Qx1XZ5?UB4X(! z!$+kizCx?&jc2GOe$7u{m?bADwj*|z$`WTMtB}7X9t+Og4p_D!OgrFfy-lUoiK|aj zek-x=nEY@0%KUTxr*JI|_-`F>tzI|r&g=ab_s0KEwHC0Sw)<+${^YqwpZP!66O-?+ zIa-r+N;_%B@&A>V+@GEQzE}Iq%irDW>Ob9hcZa`v&SAahynJ_U9G5(bJ@<HZ%mUvh zH3!Z<vB`U-_v)*6-y4QGS3fjeDwe-of6<8Vf!@`bv)R|VJbxfqzNq*^hxbLx@*`{S zOh3_39jKzGc4njSF~63ShjE(M1!NwTm51i8xVpRB;^b%DPi$+>JgAy~ZsqPyr@LK7 zcb*HsdUx?>JzK0_=X$M%@WubkEj8r-OD0_qeck=3baU-S*NF!IoW0kG`(6K?!@R=! zS$)yY>wo-PC;b0u{J(g8^tTP4cc?z8-@fK`?hBpFS(T2rbG|72X3pfzIU6ebMz`u; zO7>3&&FiZk3P!JvUsLmA=JX%e=C6CAe6PBv|ITkI!O3DuD)W_;J_RZ%bazyoQ94v; zBG~KPnb4z}v{Cx8;kVBp_^Pk0Pt4iA**<f({D1k|$^Qj&zw{l>Ip4cJJ2@=!W?c8Y zYv1(ZPI_(j+Op~7`dd%iewryBU%k;b(Qyq+>w~-At~>rU-m4At*D?~HY&~b)!;N-f zF2B`h@A@a&fBL?s-|m$?Q=;Qjmv;RyEwFp=t>d%Bp<<8c1?diltJ4py4h@@a!9Vw> zn@mSq{*g|D>THMhlmjy}zPZV8ZQdTVYpSq?SXzFV+~)1mE6&^t`!r*(?Xl!<-=vyv z&Tc*U=B*~%=I!p1XYNh7d4SQdTKa6lH?t`R-{?*|_-3ti@oh!fM+;^gZQW?nt=#9e z+2YX}i!H@2-H~?^#WwG_zIDdY$jCE~=3Pq?+dk>2&N`69>asJBp51QI@jLN|DgW{H z2fGwYUp>5={c_T4`4@kWB?R=|<<zb5oNFU~+5G$4_~s+)lAYRhWBzv?fBN3Rsa$)~ zmn6@HPwvGV{+CYD(9}A0?)zDx4c6=b?~44dof_o$|99~J-QM$T&n2(=e_ci8^Lf)Z zheAV`A00iM8nlZo=b`=1=yi;*1)r?=Befy=I^&(%zdY9%|KvzsW3>6edyTQ?W8rzL zhMj6_m@QUp50^c_d^p;B&!?H4zazqV9U=~Vco{5Zs8f@F<*}6d8s^>cMRNtDF1v=l zaCPNvIK4RA?pSynH;cQ)-ff3WPL}el6#U0)H~s(F2`1|~-z>f(=DR!Hd%M?kDJ%Jc z+plf*X~zEgWqn}g^H1i?Ov@7vcI(B2a>X&+uh3yo-V?l*HKAp4&1Qx*Ri*zMZri;2 zC)oFS#c%f==N7+{f4JxNwZF<TF9N^!7bK>>`p0M=rZrb}PGsO-`v?A36`mGXLLFFg z7<Noqv_Zf};*7|p2m5`be3}wG3Oq7IS5;kbJYQAvW^-NybN$`ez{xiEHTFH9>$tm5 zF?*k(tjm*s=37keuYI&UoS`-RpS)(N?#4^vB2Ry2{&8P7NptV(dXBIE|IGZqQ~#EK ziQMIvzx21)S>`=IcJ0maV2jz|??l4d*UtQEm)NJLFOs>qj`#Vhz1Q6Bnvd*{RQaa+ z@bNw=*Q7YT``>d#J4+A${5M<v3HzVT>h=YlP4Zu2lH-i`vf5V!ni#8mEVr{;E!dg- z|C#V({Ruz5`rEC%94NGXpMv3QUZM5hCPZwVb!26!ire0E%hyJDWcFVQuidkoH!5V- zMUI_Y{xxw;Wqul4VY`IKtH^NajA!efOBZjGt~z`3*#E1785+88%XY7G_ZID`z8@D5 z>YKFUw2DpY-{W^z<fy4RrRo_9?<#1W;k7hH#ZX@BHB;I{F9{_>*D1vhWlrskirb*% zp2Sq_X1Tq^-O{|v(Q^9f#dCrUHJ-P9)_5-WB=FqVCxPcQE2b3pR!%9_t(a1L@yq|l zpNr=lPGd87uq<bkp1Xtj)Xv2`r!2F!Trmwe_ck%$+}gB&b8C|W&h0(7cuw?*KcatC z*VzC3-e~y!*&Vg?Kk|#u>uFs0ss2y<=|-*hzxF%*UvK|-|L;Hl_Zx1%{r~P(u^*4@ z@@&Nw?@ciH_-$*0yr0f`#y9)^@810FJ#%JY-!m?0?JrCJEq(QOm+0g*k8gdrF7M4U z%cf(qht@j9%I;kgeL}w%#2*aV&|dxU*UG)CgRhC(bbre#iU<?8d9&`+Um^ZA8}__k z`>mi{`+4`?f<V4Et(!MG>Cd^qt^VZ@YuwFy`vvZDb#rDp|L4~`p=BabQg`t`SL>bZ z|NNzEF8qJ)|5yG|O~9jKIdj&(^)(+f-7ehwerC?w2e)<!uR6E?>zU>&v)x^1NI(6c zmDpe$%l1NVx%jI45w5A)h8~V?Tt-0?CYstWx7;w(IiSsS`G2D?Y_W~=uhnZU&DvT1 zfBlC4<qKZyS3Q?}>;LaFr%&BsHlBWO#otdS-xa3?ZnxvR*xHyA(Q#2QZFXvYK_1KX zg5w6;E^f4Oe;jIiL+#q?IXiYJ-oElje%;L#pWiUYXkOoQKRTy7F~3k({>Hg&ljas| z*;RGp-mJ)@TkTAz_F8<I<^1CD-trQ);@4-*vh{neq|Qjoev#v}(Or`Hx#2g15ck@9 z2KD7%IrAK>6Fap-u5fbaIX-6>yuQHWPII5AkoZE6J1yrntFLo<enzN#Ve^|KsW18! z@|>5u2wgv@Ig$5ec7dXm>ZBclZ+o5=s4DCbe%qs}*E)CO?U&gFx?M;1W~l2F8p>GD zKlSLw&uqSh#$f>$o4USrcI^w#44iQ7`^<m-8Z)15|2toM#s5ViCzb^Kcg;WX?JKLm ztNjkYmy3y|h_VIzUY%NW>7S=_QcWJy_sZYdr89rM{B`EuzCV0B<Th%Bir$J}7c%ir zyy}zxTd)0lTsp`9_W!bf`r#+?-u<7*`PaVg?SIZ9yF2ldRVsG2E-^domVW1#{k1~7 zJN?eK$8&eRYv_-j$n|~#KhH8L5&J1B#|lK0CvzEXF8gbLIN;IxulAxJ_IoL||A?1< zqVmCh?~%nN#@wZhm;arLV14u7v}%g~dGCz>pT1oe6ML~E=l_;^){pVJ!r%V$9r&^T znwL(R%dfjf>ra~f*b!=SZ!-UqkN1BrRQY!}u*XO3$p2ek|37Tq$?AG^-%F+yZhykH zQ&0RqTvA+8wfDjQeR=;)YOl%Mz0+{9-YDX~>XHAquGClgJ)i&Aeks?|@;~CQ_Wv~f zcfHguCQ+vE<)8k2AA@fwd^&eYcHNh`c9$EMsLVJp|Fuqe;pu+9w3ycAr}}M=U7J0> z?!SOmtHdpLhImfnOY7ppFaJHO_3`S3eup^DzZ_G){a1<I*LV4;h}h4=|DT$_|G#<) z_vio2$Ns<G_51(2-jy}GKm3h)G~xRC{Kay=#V&rhzW)FG%fiBsJI(q0&aOE2eD`FA z8h)AZr@Ie5h|sO5Dg9#~7#=bwmqX;QxOT8c$G`6LSD&X_`|Ix1h!3Bj`eT3pt@?;W z$HY3%I?n@7>b(^IuRi-te)Z||*TsJx|NpdT>-2x&Ct7~UC;Y8<$;;PGow3n*^^L#k zpWgfv&TaL-ZN_&`alZTR{~wG!S=8ALjdltg%C`UKe_ix<_W$(}t<(Q&iv}J1&+PJl zd&%|x+V!>N?yr;g-&VS38!1&;v}^6zUwfD4&j0Ou``txxuiNMUJ87T%y?oi>Beu8t zG|y*SeT`oJR_5=9W!_Jma<8*l`CivvWA^HJh41#w+&OpUD+4Y+zGD!*#cyGv)*R*2 z2Iu9S`Wmztv)4wbCY*T@_+d(5@8$C4ia#U^c<fmP>i87?s5tzHZ1`|FL&~X6PT`M> z!;hH_AA%VlK4&`Q&(<=Zm&0CGpw7;<h?C)=5yPRS3@xV_Ii|CG(9`$6G2`M?85hm| z|CM&L)5Ci|+V70&_}?h@-+9i*`hrt;d;hn}H}yv^|02NfW8ngZ6B88}vuywI>uO%` z)z`fC`{@6@U-kzr6Z-X^IplwO&ra*V_djUW+`5)f{{8rQm3#htEYH8U|4$bYd-9^I zG{*Do@BKPqVz0OUb6zR-=YIdc`13y-W*@4&!}I08yQ}k=jp37W3g_FUyq4Cv^v(3m zwJ+A6o45Txv2|8quKo7j+3Fj6_kWv~>-3D_?UD8KHv4wvub=zpoAWl!xlLPw=g-ed zem&>nt7$qHUhS)lpKV#&W%l{5?yWZqrE<Uh-<y8=<*mTkn{y`n#_LC{J|CSm`{Ud4 zTYE(I>i+kTy?y3|Nc6$g>A9_Ei?`nC;?KFW^kJ0q!LZp6EBtc{e+h4UB-^`nbMp3# zbo;j#q@r!tnTp9?KKIww-#Y(N<?TP`FK^-gbvuWD@6G7l8_t&9K78Kn>LIB;6Rbja z8K2QU{Ge*jAx6=6n+`79@%yb~@7?~!GmBD-c22R;KGEDYNBV1%cHzdU>$=~1&iY!; zeCTNV_XUg$3k4Y#Dr!tR#?ZpX*y6_6(zeh;id{g8LqLjCK#EI1id#U6M?i{KK#EU5 zieEr#f>Nf7LxxA!5<|v?j+&F)SX<hbdPs>2NX<~nOmWD_aLCAU$S82gC~?TBaLA}} zFkt99!ohG*Ktm{*fu(tYM-P(#PqUICpM!xw*AWkfg8>W&BNz@QFdWQaI9R}Nu!7-W zgT(%bfQb4}?+!@JY+6v1aNcC*n(003&b?W>;@+b((!cztFV~(E_hR}Vf5oq>{>!Wu zIabfS^#5_w|IJ~${r7(>*XB9yXUb%nnmxCzM@w(1)Z%^fUKx~`tgYq0f9TH7>_bKh z;Ud#it~dPi*1DMb@{@5X>#jey&i@ZFfB%0`$WgA>`~STBFZ!(T!T;568q=KrZTltk zXL9zhZz~ifL;snS@8AD-dD}$hM6VtBf9nmFf4^yR+xqQ)mC2R8|4)P-`q6Ly_x|2l zwf4^q&;Fmswe!@RIbt%?e<e*ZeS0&b`j_+l4KJ@xi*!4anR4Cl|K)!Qx0_`xujI)u z%{-A-QhVKQ*Wb!ApS```46o<Dx$%D5jpMa@=gR(fTvvN&U+9}7*TptZ-WK(C^87Q2 z%V&h$S}vxxIVW#pthv&xxn?;%wwue9%~nUxvEBWVwXEv$#W!Czv3;8}Kl$dJ^Yxov z_isx}-&1*HUi#ic&+LEis$KR|dt1V}%io;x<WFx5E4%h1xU4x%HTuf)-0F=>X6R=A zT~upk+%~uNr{HgyYMJam<;!aGGFhzOuXXwTr+&p-?rZ+Z@>Mo>_I<d>^6lXr`!|lI z3o5)!Psc@9F4($q%AI9A&63%!IRP414HZ`;ueg@)QoYZ;SIK8O13RZ=X#Sjk>y8yJ zzkHE>-%{ySPRWc;%?I~8E@pH(95~ZeivQ^~BW?u~B~{i-6B;heXt=PT;lhe9dpr&^ zIXz}_Vh-NZ7tQ40e7WXx_8KQvUcb-2mtL6MSK;;l?0e~l$$b?zhtIxAEGG9=+CU5* zlk+NUj2v!`pM8=f8WLncTqX`Tr->3=XU#h|GBh2$P%~MA_pEv6LlA@ita&Hn#hS?y zf@jS;6+sN)v&Nl{j7@<TZ6-^IDM*N)HSTN->{D=a`|OjHA=0^#vFRX4nJmZz5JMhh z!ljzY5{e)bKn&%x=ADj4jENHyKlHm49k&1K^SW8Jv1*y7BKO<1B1&IurT=f;HRsUE z4^P_v>~H#6uhr@k@jrO=+x7bAuDs-}i~PTQLgm^2?e$_`;uF69H(~j^+)F~Ksc)t# zui}&$Jpx|yo)lW`Gn;GU7P^>!#lm#?0R364hs+J9{$6EJB%#hSfA`1#imj@aR<#Fn zYU}FCa(~<VeB0gm-;+z@$A0<0`xm)ZmHh8_{+RVGqGX@)<JR*(7g#wSsg_^Pnle4w z!9&mF@IJfjwLZ$aZ&s}IJ(bWVtS|d$o56Y^(el@p{m~Wg^sFbn)64#mshn5s=GT&! z?B>^2r)Q}sF?pr4-&f6D)*6<EF^Upi{%ZtnH!hoEn^#)Y;?>r(YyGcTr(V}fbS+;d zDz&M5+1a(Z|G&TcAL`lqE57dQ|L8OS8-K+=Fuxjq-8}6&%f!>Yk~7Z!s+hTaebtZU z+I!SFy(??y{eSw}tLBSr`Mm$jKh68Ef95~8!+-U}DHr?p?*3W->i@yzx;cK<JI<W@ zWS<;+wf<PzkuQQWd%yj)-+RGeuYvt}{eSDftopxj3fEWrKd=73E4owa^Y#Dq=F>ke zsL4NXd-v!24nyC4r%ScoJ@5T6{rqPKA^o>)x^v>J8KNx&kNST8T2jH&+TMO^os&_) z<!AY8eHsePKFG}xbS(;NC`dbVcWzX~uM58w1$R9B|4V59+W&zXd%x9BI=+hKKmUY3 z``3M*aqj;Qqqw78-S&NpzgFCkebBdtwdz?x2xnSW;Bu+Cfm7EP$lf=R)KQ*lW%<*q zf`8w%m508!O%y-0W}?@x{~mJ}ZjiXfaAn{B(;bhi&;AeR`23&w=zrz8$9oU{KYCkf zx2fzF<2^U;xU2AQsW;p6^M{Y;=hwxDqrZ7h+4AT8E7OOvrPDqz7hiL^(z)FwM|Qv3 zr`dYb%$C(<-`W>_|B~MJx3j+K&)w`XY2EslA5J)WyGPFc;Ai|#`=!o5<2|Q8^sC0o zeEYCM?zT$cw(`TxudeO9`uEpY$-)Q!e?Bgdk)HSbsnz#Qo{h&h1coNfH(MwF;-uYS zeuH!JJRJ5?0(BM&e<~byFo>LVWoY4coEXF7utP!Qq$^_!xAVjpnX1y0tc%ng^@J2+ zG#qwBG!z_Uc({o1P%CpwH#>*8fPkKoLX1b(lO(1?tV=!gw1ShvO?&pJemU`{^@qJ( zc=(*vvtNq;4G;XAFL~v^&wK%4`<#Nd1ONBs|39cYvvQy2hS0r#=ZD<*=k#a&)_?OS z&Ae$hE2H@MzxfgC|M=^Qf7<Wx^1q;A-tYhG{*=}iK7Ri#V|LHa^p!uFOK&aPC~i{Z z-Q_Ge?||XCy}XiXvCcpGpQ?K0>4i^{v*((3YP-yuTb&hQMK|Mlju!+q6eNXguWgtb z@qmT#p$g;sUoMAFoZjzu`~vU)>r*BEnEqSb@pavQ5$z}Sjg$VjTJ*pCv!3_9JG=ay zjrlWjJEmpt()q#AJlWxzZo~eX%<aV!JVN4bDb}j^ZC-IseAjQw8bgH|ONBWMU*Er) z<04V%yu$e5yZ@T4u55GXf2(gy;&=MP|Ks2NfDiv)dn;{ym1-&VsE}uKTbNSw)2bI) zPrbhfx%I3HWB=g0bkQvh#)sAqZGXfq(~6g2Ti-IZdFsdd+{8S_>1)4t|F3>tpOw1i zO74I2_&h(=C-vw4%@>-V%Mj7~Ro-iA(2f7=!yf;axyut<x6S_>!}_-Wr=L9dzt2W< z;a}TZ_P*wej|9As|G$30zwUtlxA*^gKkaM0L-)ZgvE1)h9&o5$%DemZFa1}~{_q|x zf4-hmx!p-B-1yIW^B3J;jZ62fV2Jwq$A6{Jv;VAV|E}kJaa~rvw_T+~&d-iN&s%SH z@ezq@6BhR`Q(J!B;h(s5&o28K^*ax$XRfL#l-ScXk@NMX_iz4pvPRx)%T#5H&tZ(@ z{U5H;`b)p@<^SX+@9OK(YD@DhN@aIEjOfi~i>vH4d%Ho<^x^--FDp!H)PMaq(hQ!e z&(|F8)VSl+kNUQA3uZrF{_#H7_y1yF_S^scZ}#>7<VnHr*Zr$Ej{0A}*Zpt(^Y%k> zbx%AleA)W$zo)9*tUsZ9|Jnc8{OO;4e4d}qm;V+&?Ze~OeE0u-I#jghKj-WHe^34w zy=3uXf9S1?yJsp)DwSohYQNJx?TM_-np3O}wu=tQ+L`h6wfsD0_vOFI+{7@?vWe&9 zf7_qFdhfHn=%F9=4ZrGN85CS;f05K!x$~Uf&4bGCul>*ey)m)i%JTJ22@LaJ+TT5s zStXy#Q2YExyyM^c#DDP(zy5DXD7fM-*09*_kmcp=2Q$lG|KI-ihJl69E2ke9{<ptM z|B*BOt@YLa(^l>MULV5w@4f&3a`~0AM|QmZzw2A$b?>Wxqi$ZE|Nr2z(y&d1`E@6k znY_*PuiVNx``0`EoLPw*Y-|52mbo0M+x_igc;s98*>9CU+imN&u{=L*;z!%r8`Ilr zdp;?bow*hAd&jo>w~n8Eoho1d*7<kK8IiI_;r}=Icl9T#@85gIZfUS^*{jdSZ*%6( zkGwlcJ>stU{7o}2f4+I9ynNG5=gT+W{I||J|57Wpc-r2?>({f7z4{z>JH4j5^Q`yG z<n#7Q+s^mM<?B~`U9@+@&zxz|g?G-yN*$NtD!a*h{P(7#FSFkqwZ64sb8`8n&GY|F zHr-Y=%~b5>&7J$tr#|=Hl{NqWPnR;s|Nn2uR~0kt-r&3P@5A)ln?>`FMZL5>oTJ&A zz4NqKi}KBq(t`mrCfk%$xS8HMG}mUK?*oZjtxB2F4jJ-YOL`d=&efQ-jj81uQ;QjM zOBr*^w#9cGZuKi=Iy+>zcP-I<`E$~@B_6j#1a3`L%8Yi%i0@j`%eZhZ<HEI!3->ZE zJj=N7F5|+tj0^uV9%N%U$fqF`&A?K<z$1rA;7ya#MqY;v{9Q-f7!LYr2t_lpR5P;7 zW@OpT$a0&J<u@aXG!u*UB99zSfj6B>8#Ns^Xm=gi#&B?-hLAKfi}hlU9A1GpyaG9V z0&n;Pa`*+_@C)P!Y}oe3phxD|i6<ZOc;6VP-eLM_HsgqWvtIoA*Kc>fUr@bAxHtai zzxY;>Z~uchKkgU1UB73Y!sdd$eG43(cYoX8`!Bfa`u{-B*01q(SL@%M)|+1<*WUOq zdiuXp$4aXgpW;h<BPjc)pzGs>*^_<e=UkNFkABwKr}H*r&FM^^gS}RrPFrMs75DQR zU7gN0`&Iq5gCe({SO>pYw`<u3ZgwBuxG5b{bsyKRxOlkKrMJ9ZBg^RN8jn5C<QFYt zwihrp))Th9khglp&mA)tJ*&4<%#t*By0U8FO_6(w=SBW4JSieLC9Ug*(P5FwUE5cl zy%Z9ByL{HHTW9YkWc_~=t#tCo(PeypID?NXzIL7aMcj3+wVqI^r=8xbqKD~$A>U&c ztx%rdwX1!dU}=5s@)gH-RxWyB>h5dXZ=2j@xG(Y5*1H+6Z1c8y?kZ~XG>c51B>ii) z2=gP~Qxo6%tlAdqlVzD46u&yZt2;-#>va9w?j8E)Xa4$A7<;Asy<_nH$;(~;n*R+q zNjP7<sIl8#j5lhx=aQ56)TI90uwJIvsqOM{rnC>|-Z(SWWBO*iypoz;ucjn>{rW4v zDDnP!QPui=J|=$-vo4xqp57I--A8b#t+msXeId$HhVE*YiYEK;Z^~RY@mAxqz*+Z} z@vJ;z5_Rc{iCfHZlV`v5mu(b3H|tn!?IDwYC)k&9{=R48d#BdTB=dFQ#V4lkyO+%< z>s%(az2n9mQ>D8*j=NrNJ1d-e>}ltwUA{u8?aMotnVZfkYWlKl&V2P5MgGfYd~yFh zJ8h3}@~P^*b2a}jRa-XG-*6ezzw}_m@4cO~FGiST|0$iQcvDg9RdH0voxd|yEcsf! z=;lIem(BY5LT4?%_ni3ge(9o}@{+Ef?_KJCmLH!QwV`g~%Y{=n`*O>lHMGr<NZ+OC z_=+w3vz$oG_Gj-RPEAUjcTdX2r~0GR`Ns!0yecoKaZ<gezWe>r&6g_wH0-v}&&oW- zG)H#NNj{7Hk+FONGM!3=YP;4=v=DK96tZQXX`zlofu2KwfkT1OEKvb>W|s8Da^5n1 zN`+QcNk=|v2t8+JVPE1gXF`|FqmG6LJq-^gG(4En@L)#6gE<Wk7BoCq((qtK!-F*q z4>pK&US?`~%+&OmsfjsQ#gB)h&u5~Av4Vw(f`zGqg_(kdxq^j-f`z4mg_VMZwSt9> zf`zSug`Ml8KMf8HBAlEIOo9O_DGVGf3>-@sIF2xIJYnEqVdPM8{5em6Rbc|Ff&iPs z1U3Z$c7+M-zNa`E15{L;CL1}ah;YsfP-$V}Si;0{go)z`69)@3hYB-?i?gwllUaj< zg~<ZpON=a!7+F3svM@2R2r;oJF|ime`eD6Ie}a&LfUv>@VPC2HN4~L6TIBh7^NoED zVJ4X>Ev}PYouV2XVoV}cp0IMTxc%*zAfX^2=_|%LGf<_4jbn-1<YVhM);8YBxa7gn zr`Fr@B;(Q!(c9uD`qX;Wa^e+iXPG=p`pxu{k%{qA%ESo@3IdA0Vw{ppOo||`($V=F zYa3-UFI9-#7WbH>*1JCWH&f)LB_B0&;uU;nnFu{va#Bgr-$5Z@mX44RD~r<7lS+!g ztMsFicQdUF+SBuv>xtK7H>V9InJOyYlii%Qm}IJ?cuzj|^~O4fT_%||BDY0PT;EwP zvbX)p>SufN_D-0!+xX0)(t@(ny62AGS~#U%I{4pLi?%-{1xino&4bGB3rYS{WMgZ8 zAn&<I>u8JU_a#C?*G|3qzwxH)(YlxarykQv+Iy@%rRg8P$lw1BpLZC(`d|Ki-EC`! z8iU6jPad?pK6B2O(wUzlnah`3bW~`U_8lQXle0n_pC)>L*PE<!H~x3TB#Z3%DH$Gp zDl0EDd0+Z@T<2uz@`y?N>CsvN%`M)V>N+OQ_eG9B-Y2^*`4n@q@Po_{(bvaJ%KtKy z$A<0M`84uM;`?{5vStoFR&P33N+Sf{89UV}yxMRv?Mh&{&Wgo{bXO!VQ=BC4`<3PA z!xh>a_~c_4dbV2%^=#*UvH$SV1%=aM1iGFqGB7O*J@lZ|NA;VF*7yI0zs@{zYSu4n z+#CE)Km2Ib+W&!^pY55B{tvJC_5WGv>u+8%?8f&J`06aCS$}yZ@o%zj?T*9Kl%Acq z<grfgdOLsKm9{0y6KhW77uNi}cm2n&`K})p`tEz?TpsuIu-=}-<`o-FJ<b0eHi@ce zT_U+u<>-&DCE~9=vo`iG5$*Qy`Y7b7zHD;HBg08<JjqdY|CQwLDR%S!{~&0ml%XNN zQFU^^&E4Mo%_bA`fBQ+?&wFRJ@9*Z@o>?0vyf;V}_2itV{PIA*ifBd062|!kv9Enp zKm9jV`pYkP^S?;p+3?Og`bt6nllMK}sQy#pvDv=s-y&<?KWcQj*8eDVvigUj=B{v_ z7w5Izw>=8Yz5OxkW@=n*@59GR(LyW#@!n5AZMpCLy}vd4C#!dw*!!mZj8i&!U%BWH ztI|<(LCJmX@{@K<Iq|-<#%VooP0}Byx(pAl!uo0cziMqK-t-sl+}m;DwUE-<<-(D+ z(>z`!y7+xp4BEv$@#5m2>^B$w@yOM&_qyDBe!{kA?%s!f82YaJki71HzkKyW?YcYH z+kbv^|MpS%TjbBq-TPH0{Sa1a|L?lwL$(WBbO%da*NOY?onLg7Hko&O+`H)Vj!!kH zTx`CuN$>fel_K-2ts1Y(hki7^_wv;9z4yA#*Lxm0Zxa8E`-l7wr*|Uf&#v%ak}WxL z?emTlThAx29F;=93(fq+HF1T$K;<u$iOXyzF5TRrGS}_Ni~B!VUyFa@U#s&(-0svT zerdTU@9n;?zj>wpW&Oc5*Q1*Aa`;2*dWwt>uC!b&k~VLR#u<^VF*1FLX`iiljD^_S zW|~Xz7$;lu7$;Zo7}r|(eD9X<`L1l~^ZoaS8Sl0q)K~l0`m#>O^@Fj`cjXG6;CN-B z`G-sX%gSB;dyQw=@Ba%q>^ffB$%-%9*YWaS72D;%wLHsycL(qY+&@xMZ>97x(C2%* zqEP(N5_|56KXlqoF6=l`Zg}_ovE*IzPbBa1-z_frA=T&mcH<fE^p~tZxZdz?ec!>7 z|L$y;|F%o`e79f7v9Istzw8e8jFl3vzDrDtPc^vfZp6-apya=&+~vQyHp_l*)?iXk z*k#{!yriC~|K+}}<0bp;`(M^+%U%9kCeo;Ha5ufe=X<o#jA!-E@yzuJv-ZzFZt`EH zIrCp8+vPudd6xZr|L}nRLs6g4=81RI{xoI&3u3!mvnTI>>5ONOOPS9(-aPne#<S$z zO#c&R^}pq^Y00$Pru{*1+0Ps64tURa_WeMma?5;!v&YL==N&SUzb*QM>vBy__yb*^ z&;O4n&-yRgpZPCQ?(&~nn`J-4<Szfwm-P9(F}CqKSV!Gqkd6vIunudV&+f)Ep7E!G zrT#&r1i(_xU_IiQv+|FXvDO_i`7hF+Y4?qZDLiRbecwT_8Ny(N{$PdfhG&<{yZ;oq z{3nxV+0XjNh9K9~9R;hK_ETFRS7J}!M9+u!TT=Fkeg4lAd(kJh(f-T+poeS!ixxTl zXYcq|e=PRDefgmi_x=9ISANewbM-Cv%xw9-^nY`5pRUh;>F!alD!*lmZgBdA>>mA3 z&0(eP0ec1O+>V9JkbmMb^*Q^W#DCL%9$Xshvwgnb=ezyu6RT=I$;t0cmb?6Us&CDi zRh~ZU_v;@0ldw$gbF-m$#+&d}#hGWqx6Ak?*S^WOwf)>&Yg;i(e$R~w%YPm`^x5Vt z|FRnf_7Xc}KR1`HHn3mu->P{3Q+D$-byk-fTKw_;TQuJ<Qft({@|EZPlyYx%wGc!9 zifxD9t%&1!+sE{ORo*-Xj(_YNb{rga93HixwUr?qYnSX|{3De2VD|T*d5m`NC#DPB zUmrBj(PH5tn`U?0sHZl~1?+bk>@(&S*6==)Y;n(+=eVQgnPiK9#ym%h)@PC};WnRm z4(ZEPvOSdSw{iA0x6(du%<+E5bIAkt+s_(v$X7m*)W5W8-_uIAhwA+{%^#+?&vX27 zO8LCPo~IMj1^x+r>e*j0?_;>qo8LC~HLm|(%;nux{MYc`@+sBt|Eo^s`dZJl_W%9- z&;O(9_WxY>?0@N6<GaQG?jO{deEiS;)Vl@$;tLn82>SfLWoP|XoA3Gx%l}<1wtJ9P zCU^hJ_x)>r{<V0@|L5xb;C~11=UYGR|M5R}`5!^M4>LZwS1fc}{)aR8Z_CO$yV<YG zlf92Wn82{Z_>A)Z!!NZJPH~=Z3!3-UV%eoT$z}7ii{=|_U;pjP|7O*jH(6cx-C+LE z_xb-4uk~N;vl9RLFL?2P+0~i<ng6E5>iv0O`u><Vx6zr&Zu5PPXM_csuYR~(+;!iK zzK5^GTISarf5~oI!W&~No@n~`|I1dk|4;r0O${k5{--bc?ElZJ|L<n=wOI9QPyK&? zL+*Q%*ULEnOx5JR)37oj{oSHz+Q+X(M1|^2?>KiWYGTmGF!gz-p4<~t%=>4bwWfdH z;#Z&RMZG?qZ~D(3_J98UXaBcl{it~Q{C}9izW9lJD+~U;-yyok;Q#ma_W!@<|E^WC z$T$4|{o%jwm;c0n2<<QYIcwJc=#zy%y*LY&aec1mR8P1+-{60^#B}jz|9{QNX#Kgr z?$ZC2hbR4A{9p9m&;3rCqR;-bb^X&XlCwW>xx)VLJtM#1YoC6r7ag|tF>9-zF<)BH z<b=dVRa^5<CG5$wmtMNF?fn(gZL@bbW&eM3I(5=Vnd^=JFWe|RQRJ@vcjl&ZvfMJU zlkV%kE_ygcdDZV}n^q|2TkmSOi@WxBtGKM~xu;WI*WWz-)h5m8m_+=8s@(aBZ*2RX zFVPO}-E?y0oI4jzuaLOV9o#1(9h!S|PE^^&75l0tc6aB*h<$%rxbFP4{GQtH?;ib{ zYZG>@{<7im23zY>Y<l+|N^JQy)5!Omao@rIy@@iDGD?o5uQOBo5_MMf*m*-f-k0w^ zUYXpR@#XjBzQp_b+TQ>695MWJcxI|*eqZU5JjuXm-*V*Le+5O}?6923r#n5bW<u{{ zx2&bd0%vjPIo~>CvFmSWp3h|OV}{rNAMV}zVP@f->r3zaP~P1u{B7-XwRd)LW|^<| z%S^uhMr(G`CW~cK*QadU*>6?y_}Nrn>1j_-^vn*Kb~Sf}vUN?u%`eMl#`s-uNj`VJ zGv?h(Mz_BgHS&&GzwNDD-(K`npH(t_R{ayMAI8U=-U;_zj@O@U`ftwR2{pUctT^xJ zUikg{DX*ygk7mg3TAB0hsk&_OIbB`jf7R2H-?{(VRGHsfbmsoPBRdjokK1lBy1wJX z#ff(gw>@;*p1A1NF5@$=&L#L;*uP$S*Kx}^=F1Z4nRjQ*pK7JUIPcE3kL#vP72Cb$ zN^tG@pOXrsbXSG=O7;cVPwdtz-6R;ARj0S=ilbY|%9lG=1X)W2F7+x7TzbuPkyq@& zg<iEgc|x_0FZB9*p+l>5Cv)hlnMNTiS@}X%zMsC(%l4_m)Llz?LSNnH3t8DeeW6$F z#YQdD^*o`k;`l;Wo$S#1%E}k|ibr`>$UNt%v$9;K&N{Ww>usvb)LY%oQ?<+%d5IRU z2-3fF{ekO>Aj3+=e5a|udIdvY>G6fW+GrHA^0>^3pz<jTz3x7AnEJ~{a@Cb|nH53z z4HkKsS_Ld!XST>I_UuBhxyde5Z{2UyD%~R(`sy-Y$jZ~F0+vp*2v~Yf$9byNwMMN{ z7s*vu?D#@fZZZm4Igu}9<x97am4z~^LRJcfX0fzuZI!I$jqB0+I!Q3}RlP;X%K4Ik zOQ#jB2r8EjT)OUgz*0GV=c&K+*;ieOO<fVB%NMdT`&7VEyH^fVt=98|W=&%beYM0W zWTmt0iXirB3%$-h30Uf9vdGK#Nx)J*gGFAwj{=r%GhgJz`!rxFn`Ge9e<v1t*(SS8 z{l!_!d!|wA>m26LS1*l1R<g^k2)eKBJoQ%>bLgvOMj<Qxtpb+n={irfI^C!h8sDZ> zI)yp(m5}nPkT2{BG9fFKEkjlwzPmf-J?ksZ=Ibu~-$Fee&QDI+G&|&F{L=sCap4BK zw-Z+U{TeTN<$sUr|Hg?YvKALdt@!_P&ynWG#(x(7*S_<g``Lf4xACig#xGRNezoC{ z<;}(i|BZI0|NgCKDSGREWoNqt*XK>#%lIxofBt*--=~{q-@addw6pbF`P%<8W`|CA z+x>R_>WBZcM0@^o{fVFbcK)=v_G>pB2@qiTqd#3M;qU%6W?hpx-@d=}UzM%d?l%9w z_`tvBJ7ZS-m(cjTU+%O0yO95mE8Z(yDd%|ei;rjXk+^%}A<d<-T66vf-qDUI+FmOA zU~|hZ9;aBgH<9A6j1tc8YCSlwH1Qw%w>9iBv2Cve*Bbmk?EkAi;-SghgX*FWR$u(T zyzA?m|KDQ^trY(kfBm1H`uz2m$eWB8|9Q<!|KI<<Ycj{%_pg$!i>IUttFQk0ZMtV_ z(fVKgL5lzRr~KKk8r=NazB;4$t@--BA7*WfVn6<7dT#Uj^&cyLt)0Oo-nP#E-S^go z6Q)mi?fkLq^X2SMO;=B71wAZ#|9Zb7TkS=YSVx5;^*cjWez{V=Puox7Nd0qFKb@0j zuK#>u{Vw9a<IAi6^+kXEXIA^S`&+$G@}K*<vD?G<y+}CrFO=Q>3cGxj@%EqU(Lqhl zv;W+&DG7i4OwjT1EcYDE%zJxJpLljR{`PjQ!fSn@&dzFUy<Vkc%qywO`}|~=<k`vm z{yA|E{oLJlt^M;jS0U|e{)v{I@{f&jc3lmu*;?KpRQ--YH0|hw{n1^D@%uQO|NRS< z`1JLl#HZrf$tV0dkFC2pe|E5nz4&gk<BF5!`#M|xd?|71`$b#r2SN$qcT~CxG&Lrj zcHI5ogz7>MJ?`M<>#9Bb1ck4MujE?Y@;`g?-_ZXHn?BZWy)*fL?RLw#2mYV)p0mLC z%tdh)x8KJnaXhg6e>nZ9=ezIU^p(P|AHVOIyvHlF_1pBL$4*IhdUgEYdimet&JFXd zT*6~AKCu5kog8Yk{;z$g)8F{|SN7kPY$ne=?(6Vh_ve?ItjTQgvksq^eGol=&#MlL zDaYL3O)bjZ_VwBAb=k+ZM&Fx~Z~tFN{p9b~_XZ!0*Cn6g+!wt6Zp`x+=0Ei&X760b z`|q4P>-(Y$J0;SuTfB(r`TJR>MDN%!Pd+R0<ckjMbGs+17GBsXk;!afC9b^8*8O>s z3D=`v^QsSD%0F&a{J-~qy5xqe*2)Kq_D8ab7N_}!Ele#q=J9z#3%l+|ucej?Hy@gG z<D28h7h>7E$xbDAX2gp}9IKN)-)n7n)ac)bkmD`&XQ%trlx|4A#64Mca(GFRPo#y1 z(N={c2mM|uTwGfFeumkGbt=Zsl-<K_=d0e--T!#mb^c90|KBo9|9n@{`(3(z@h|>+ z+t1wCI_LE5Jx3?~_hB?IKKXcBQ~Ue^5wW~!1;L+XH@nSHX>)wpx9Fa+%F(w=`zEQa zoHXh6S((lHUnZUTd0w^sV@HJ1cZcA-d++8=`x#yLP;5<RPG!bR&b)^*?N1-hF}K`) z)yT9yeTI(emJcR;<%w+@E&Gmd+<9?j^UKM%lV+^C(Y5;m+w8{V4bl!9WF0ohYt8B8 zWo(jVY|>?HvSn=YWo(LNY|3S9s%32IWo(+u*tGVdoq1AzLqhSE^Sw7@U9Zn!NU9H6 zvHKZ|fc28vymgk0N8~c+&(8K^*yw#gx|pe>mg(LMVTU&&ACApoNLt@u{)|OnH!I6+ zR+itaEYfT&)@&@%Y%JAmEVGxz_`Z=6$dML!BQ20KM@{O;ImUz67!Tgl{MQwIuIVmw z(_7}Izsybi3{CP3P5Kv(sr+N$uw&$?W8~P!$nnqdZ2J!mg&&*>Ke!Zra4Y=aaeZXo z@W8s^fqlaR=Y|LF4G+8<9{4vr2yS=~-tZv0;X!=EgXD$>=?xFE8y@6O+0}VIP^FHY zV;?)bozAh3gX13uhaD$J9Vf>=PKk_vl5Fpf9=T{<y-c%eW!1{+WnURz)SiA9+w<Sp zi>qi^Rw--buBJJ+uK)KsdiIz9N|A5>xn}&6ziz1daJrtK+561OLp2w!OtUw8X0x$A zX~hxMi-xj1@4R&vrGAlmXc&<#RoUZvPT~yD`d7Q&N#B1b7x+Mj;h|4PIIpGjf!+3t z4Jy_fNvHo0{q_Igf8(OB#>=PvF!{fG?tkqkalhi1P2v3a-u-|1`u{JjmI({_*IZw; zqggLkn_1M(SnAOJ<-N@ErxPx&pVVp1d-BAJW5;(}Th#DQt(f{BG~K^gtj%!s<Nr5* z2D6{PcK+Ms|JC3BYlH^;51$s3av}9O_rLeAzsWDX{9Hur#~V<&7rFM+v-;Kp|LdOA zf8U#XXwGrnhX1~&AOBo#=F(of=vSY;)jxysHTNBkbN;kHnqEHlY5l9cZJg2{6q&hn zW~3RYG%l2%_*43WnP;*=W2Vc|h~OIb88O~3ZRXhs8RZG3gj&0Pmtb1X#j)J``1=!@ z%!e|AH+uNZJtkH3tKow%!^89o{A+*C7jpZjsAi^f;ZMDa=WniBcZTwF|M&cfR|L(* zp8Vf5_qeXXe{HGq509>XSe#uf^FW%%c%k%L=T~g?=7!sCE-D@OJt*!r>G*_;$`>US zUat!K_59(@<TBfTtjii*w+PwH5%`)@6m%wbeyBvH<l=qLwReXf{C~M*<}RoEwa4c^ z`>!Qxe!YImajll0_KimWgx}gM`)jYg|Fuj_;<<}=`_{g^TPOGX*t>4E%<b_>n}2xk zJiF_!)=jzZ(U+Fy?Kht36M4n$_)`J1cc0dOy!-D*na7=}|9$ROef==y?)87B6|d*7 z&nk<rUwAnz&Gq<QM{ehB*EBiq#g>2i9^HJo?2{t%wSuon^Pavgzxz;N>z#uw*Q_t- ze%a3d_|U%Uozczb{|R3I(ERkS)Y~Yr>lMY0+ttp0Nk8I!%W~GayyGU?Z;sm<Y}LPC z;}WfxS2W?ans)iF$#;s6G2L35&39;@QhLhry)McPa&9RGA48?tcNU(#lgYhKRw2gT zVMkj-fh^<0D5gVSSz5BWIIfEdtkYG9iFep>tf648=A>+1j_b4EvHGpEyvsc&rhJ#U z#m;Xf`UN}w6*QYQNN2J$WN<rV@Xwk#*=(VG&lc{dNtet*DkpwpY%ybMDQlA6BI<f6 zZi<KJY(}Tuj83;1oqjVqNna{dywq3Fyp5&hTZ{A-O@%Gm3R`p+tiNr}v+dgs|6L54 z|Chh$N`J+*$?MPI|3R%WFGK%_>-PL-Px*h}TU<Wsie60D+#=<5)5=d}?$>!g#ryib z-2d`5yfWbrS06f&*z;=JWZBl+Yya%mFn3Q+`19m{(bVw2`IT!n{GV?BcYojKhjl*U z2crMqnS6A@2kS|C`KJ;J4XW;Z=+Ua5wR`vdf5C@OG+x;J-ub`$GGm|1zt{i2zWaat z!jOZ%;@_UAIQTz%;a}mBnKP}9<-c7&ZPngS^)o?htN*QkUvuHhxyn0oKmI=s_FmKO zl-4RArF;L^)oaPwuX-xtjP~5GEA;xWf5zaWlEsoZj-MgBKA!f7u6!>YnAfYZ*?j4F z-Gzs3G?*glW~^4{dlJJGq9`|sp+8VE)c$I~^_9EW#s5xHt!8|9<z<yz(Jzsc{tPYi z9hHTDYBqc*W_YM>X!p-I@k#CeHnUg%KkigzwXCd)Kl68$+<)!VtgQ$BZ@uxamDO@{ z%eVdcx8tYJj|_@=qs>`x{G<KS@BjR@7XH6K|KtB@pY88H{=e~2TK0>eP$9AXD=aMh z&hTbE5fxbd<?YuQT+izJZhj5fe!I2A!uZ)cKi!@^J%N9px-+i)&#!mt?CGn%ms5Wp z{h#{e?CihsOZ!AYOD*MJ)f~`0ROxg5|Nc9_@;R-|V=iy$h)T<qjMK_gy``R8`cvHd z$Ik9+*Oo6*5o~`Jq5NHM)7G0|N!h#ecCEepY2&tU+-)~ie%9v5s(sy1>eIWW^ws?f z*DQa{TI>C!GWt4O{Ov_fQW=Z&Vy~82SGT^+yK`XL`js4fg}bk>_TLlfds_WiW`SIW zz{h6`qn0gvee<hdfb;eH2Yx?zdND|~a>YsJ@TqL9mf<JEe`@XcED*!%utVvCT#Ts0 z4x@&GQjJO4EF9sS0_#Nn1{7^&c-X~wD3rNHo1G(^UtpbrLX4}!j!6v#ri>4}m=1-q zwrF#6go_KTGjJ`M%J|TW`PcKT_OI&i`FjMdUNN`+pUlZF=A-@bs?Y7L7xkrUdS3kR z5ptfFzio-YkNbQ7UiRuTDm`lVyDzKf|A~-8zxeBq{g3YZzo+Em`{=|aZXa8<jTr8$ z_wL&LasKW4ED6hwK(332(*N1J&5sRO^WF39_ESIhJ3TY~@4QyvSpDCZ|KAClyfVo9 zY=7<iiJSbN4VPWejD8}ie)#fF3FkS+_qG3?6!rTnu{rx=ox=~cWkvC#6_;PV*ZqI} znV87AIjhd<am@G??-i<ZrG6D_k<gd<>&m@O%FMOf@MV6~g?h%7|NUH6l{W-+FMHZu z?J-x{^3KwKyFm*fn<Wz8v&(<`pOz~A<Ae38$^TD>=>7X{Xvz6!{cBs7>Dd=v{@wO( zebX=gqNxl1GrRu3Z}R_lW#$KAZSR#v)3ah<TB+tv)Xvn`_X_#@^ZxnE-Vv?qg%-JN z{aZg{-GYGk_W!~)Q{q1V4>|P5zW&$#-;b&UA8-6;uefm0*Z=94*58THId3r2B=!5( zRsW^cpY42QueeZa^}p;tN8YbJ$8n{eb=R7mZ1q3)$H=`l{U5z7fD2Ty@)z5!=(Omc z-TZ(5on3Et&6S=ueOpJ@lx;lniJ7Wi+qdodGsWc3oXE?+p44R@StvH^#9rB(X1C=M zZm;7`yRG|sYOei%>##}REpI12V%(N|DmrI*o&M&zi{~frcF{}UZC26I%XKfSf7h3C z>$U9JS1fkP&(D9q(f+>N-sDYtpC2o!KC{a4TU_^&Z}*PMzPYp}t?cHL#x-{?%nIe1 zu;CQ@`VFhLl}2oRqa9ZJRYzL9@TT1Nr|dcN?fq{4es}lOH|w9<_Wob?_Zwq%+!p?r zJvZ$#-YS>*ZaZoBW25*D#ZKclA7|RkzMy(_o`ij`tkk8`^R8$%`dZCDq;~!7DK+`N z#P<1u8#ULPB?j}Kl+CQ)-S<_lx%q_cl6klH?mO}L>DqTs&P${j&rRFBRm37~ziiuu z@|y`3wbOaoE`6-MUMO(aa-!hl-z6EbYU=!|l^Q3Q&BK>Ie(iWQJLLRJp4{sXOWK|; z{UB#zq+nvAU}ENa=|RJV7Y!FaG+g-6kigK8z|xSw(U8E?kRZ^IAkvT^(U2h1kf6|z zpwf__(U73ikYFIvd6D6s#uKleb|piFTN536I0Se&1$sCIc(??5xCD5*e;F9+IT#o? z7#KMim^c`iIT%<t7+7^3`LM!9^_cra2?YfSMFj~Z1qo#Z2^9qiRRsw(1qpQp35{Hp zq$LdrD;g5kG$d?jNZ8Vlu%jVik4PsYbCY1ON*fQyF&+*!UJf_jit`d>5%r(KnUhnB z9G_j!_`g0cEp6R83;*ZKf2{w~^?y}F;=lQz?LTL>AMCmPXTPrU{G0oW4@X6JuXs0W z+QZ3f*SbvhdcXTe{KXv0{^{FyO}0;-5n2)?t7yN(S#arrwsq;(tsN%+F#ljDwB(Fu z-zTme8?|^}%6(qwX!vaX*2FXK*BzW;FPUtcnV|UWe`|7`g4AY#=h>$J`t`K>JkOi` z$o$(MBlz0%zq?}3|6iv6%uDPvm@WGcXa4`ca{aeAcRtv03NPL<?P2B8wJydvYxVZ8 z;I#kVwC(H3<fVe`R+)wNGx%iQu?J5-{5N6W(*pY${FZn4pUKO-W8d6!_;12Ci(PZ? zI=!f2S@`L=><)FGdCcEiJc}R9GuR8_X{npL6m~K0wRn0W!0&3<nU$&bMXy$i@7!_X zSKki({^Hty7SCVoE*HQ4{)waef8XOW%-{ArlGstvo_yh!y6mhs=`zjow~z69npizD z4lm%0RkIL!Y?+tgQSTnKd}7R{pC4n2VpXTt?2tGA<KwOO^y;=b+W(fH-?QYT$4%x` zwz;=IhpNY}5*Lzk>L@%Juw&nb#msLLMFJ-s`QlOY*)ZTm5%-%E4~>vFRr2=^a(*v- za`Cw5>Ioj6cZD>*i>%7>eP^iVJWFosq2D}Ot${LHtu`SXM??ZN_pESS`k_f|>Y<H6 zAzX8}tPqL0y{F_sfaZ>El`Bs#3@XU|{7JXn>)_V2r`u(;Sg)Ju|A$CM9s^5mI@;)! z$ldIf$f9JZAHucnZQLgrkQsZISc1%`e)-m6>4RJERHr&G6?l5*-BNK^1J@9)B)+MK z`a6#3taMUX*6H=oqTTD^9OkKq^!TP8a%9(PP38;XT5l1cnK0ATAU1^SyhVU!&f@@0 z3E34QVFfEh)GPut3uLrf|M7)zDO-kcJ+=(t@;Y)jB8}Hab}G}f$j<>>PuQ0EzkPns ztp0-7rN%>j*YiHsPf@g9tzf<0p#I&NMK<<F>Sx~C{cpFx|G$#|r7h&A#Lqc??rVM5 zW^41Dt=l`guHNhaXs?_)`|rC;%O)fUKD0JoTjKho`<aWQ`HuH&uRHHV@PAgD_&Yv{ zPqlQ@+&5w=;Zv`DmEj1Ob2Ome(|pS|fmiS62h`_BTDp`-cD^uDTokVVZ~4ojl1Qui zv;QY+i^TohAEWu@KfBkT{_j2ivrhk&`+wYRUGlqGvI{?d$<Jx%X!z0P+Hgy$qv3Z9 z%Y@suY44h(R#g34fBN**r@wO;56C{=e`WW--9an<i)uXn|M6V1)&Km?$CK|KUvgWv zQTTuK#}fYv#Ki8Qzisb7yGttG`U{%Z<vch~?a8j`0<WuELgE!#c6Cm8DOcz5D9CBi z>mMz8RZjD`e=bziY%z(nds)LbKc<{xevQRU`MPDs^FL*o%&*EYn7nO~AphL?$#u8m zG@aIj|G)M<LE*?z_6NW8Ov1Yaj^?R&?6!J(?ww!e*EHrk8DafvZ+U*c6nSY$XX}jk zz%%(rlh2grz4SZNr(|f`^HW!{=jZ(o5}!Z5YSK(PV}CN?jQ!b!Gs_=8zhYweZ2y6T zGxuA*_?!_*{uh*ElC)0ZbKzdL8M?>*h_EHwolZV;{-NPBen*vMJ%;;@`wi`99iL%8 z<Db33Tj|>0Ps;db>=1aKt@CgBlUH1uo;~}K{U`qA-hcipMXvq-Vf@d$$WG(BMgQ!_ zKmOm8KYjfF^jp!L(=4K5|EWvY<?&zt^`x@;$U?`MC-*{%XXi4X$-Vy7XnXX%dw1#@ zZ|I!lH!7b|aq*7f)1IBd`yW3$ck%t~-7fEp%5ygyl@a~3(&E*ny29<7-+S(o{;YDh zKVR(ojsI&bs@gs86hDqBJhMvjQCzoLSGD-^uBDO8M>d_@nYL5$-WmymJ8NWa9$k}l z`|_G;*UeY2H#Ix%S|)b9_NBr1ok9M0%HJCn@1Oi%^!xo1N1KH6#~(Fz_jj?cy{jfy z@=>SUTuNc@f<80lr;D>xToV@se6MZ}IMl_zT|=HVHjnerTrTNd^J3pKI6t~Cu<gi1 z|B0_9p9dbke_w*TRp+FoT+}>O!+FdLL$j*ScQT~cuc=Tv5M<reqRJ=2`9CF4<;}wY zl{woNdq^Bv=;2|y*h3=0ZQ_N~JxU90T{lQ;33a?(;ISjaZDPT(9;JsnBwY{vP7~qW zeMUoQ+l)Y!8ZEbp4Xk$$?h8<Pq9|nTJn_RC3D+ji9;L)}60V1M47*x7lSDXICW&w| zAJGshI;$b{=&XiN+w4G<62rwFFFXZvgk7DIjk{XTC5dqI{gTQNbv<Ng+|}~GD^Mjy zV6n%Kh#sYfD<oVGnYkn#|0lxzfL%lCU2S~BtSPl@$G&jxmVf+zMosLry0XN;!o+#@ zkLp<;|1a9P;h(<3@&DU<{AcYcKCCzCzxpQK!_RL;b@qG<n;w6$ZT{_J+w3y8yUI-3 zII)`j6J#K7@`tz6%%9%YUvb#`Q{6*rADh$VK9&c|Uwvo1Z~4o2pZAlnec8?XKL;Ls zFWh&}^+)CM9lHxR?fCuCv}LlqdW}tgL808khn_9XmpT6Psoc3e`@=hyxV(UmU5tM# z>!18LH8irScdm?ciQxIU8q#b({As^qS@GZ0%D$4|x)9~Z|1X@9cvx<^n^7pT=R>jK zU-h+pg8xKy-G1cixK-@tRo>Hcl*4|)agO`5k8|ihG(2=U=@U-`TN(48L&+`nr;}Uk z4<@(#Z#d5JU#(xTPP<?5-+LRkiV%eaTZxC|U51DJ-E8`zcQ%_Qw#+}C(Bi&``Sc-< z`NtAk<U7v!tl?4q!y>1=r|US!{rUUzcQL+`)D}<sayRvl{QA?{ntG=`UoF35`LFsa zcqKlQ@xT2gui7=gSN&Umvd-D&$42I7ik+*Eew?X4TX@$QLG@$$=KQ>p`hKrM68x;< zll<1LHdU9G%a*Hi%k!(;+Hd*w0!QJkE|21`MY8#6b9V2`n%Sp)yzuYSvp0Ru9;?qt zRXqH?zKwrc*T?@2l8^sq86NvT{j9|Q=N*Rs^v#q1$m<{b5ub4EM|{$;AM0IJ#Cku* z=N<cT|9H>G`WA`D^-FgClH9w0&7Zonce8c=9e@6+`|tMu%VQP>R9yWpbnHK~+P~w~ z$Nrz5_S^Pf|IfD@p1f0cxtcmda?Sb|8+ZA>(PYw#W)Uoy-_2kp{$QR_xg&3g<v(@l zKePX@zH;y9euMw_=YPCkYxqOpxuNX;`1@;4?wc!ZC%V1k(nsCH;nq=|VUrm?2#VLe zW4Qn8NvQjgg(h}I?^(QO=W0&d_WD=!E#3Z&TjDRAQ#zTS^wOYnV~Nkto0nGJH=lOy z;{2#xF7LLN<ZieuBw7<CRQhtplG`^IUwU_J=cRYY%RQ>z+*j$m_pxWm^J7aV%?Ne# zT<==t`Th~7P-a+{i&^D1*Sv}gizE!rEs|NeVa2U)TUR{Q)p?bjt5Z0!$n(zh`lw$q znR+jGU(b74{>j|)zwdPw=5IBMk~@B6pIorDT4mOoa+PNJ-)D?e3Y%5zinpsSJe}m3 z^e5`{g$XjALCaaEE&1FZIq9|EMiu#U+j#63Z(OoxR*I*sN#vw$i*~CjKP~>)yY+nO z>fJwPCYHQEuvGQ@IbYv9GyLCFmF;Ctp0jSh<Gzx=s%xFSA`QiUdvaReQ}SGOYSaH` z^L}r%vFdnt+$l=#DMMTI28Jojs{`2tyd5k*2(}kJVCXM;u!{MqgH(S}LKEv#hc4Eq z4)xrp8YQ?-HI|7@VfNK%y1GE-$IYgq2h!Z98vS)O*uO;w@V(ixK*mDX(Xzm&sp!E9 zk*5b@T#{ln*v&ix_-vFNEjNTT6&=uUv|KQKfy@jAN6QIHj+O?un~EMda-V7}6q~}# z#eJ%ApV$;;U4hQfMKTk-1Ni=AI$B<cX(~F9=V<w1Hp|lkrktl5^;I<3_f1<Mb3@tD z@<SiX(*x;ZQ<$YyG}z0c1Nd~NEs%-GcC?%TvhYAt(E~&7Q;nJ(M@|LuW$0+IPtws~ zSMv_wE7`t4=10t#JF}PZPg2NB@R_5)snz#ilPxyT@nOu|g(v(U|JQo|v3}vd`h;)) z<MT=ipSN9}Bo^5EY}!L<&e!<@+jsr3l&^bdT>Ltim*cd*iTk^=TesabyLYSXZbA0$ zr+?SH75}ou>(lKhYoDx~w=;_F-dZ{T@GZ+<Tef;XG0eWs_C9iPlfK7dy_-w#o~v8? zGB0M;%ep_Ynf-tBJr<YeO!;<aN0ij&tj;ah7g=7qcXVe^=Cz8IIiHkP-!ZtlO2Xjo zDjCZgtFo-mteRH7Y1R6#8;-eVEh&sQ-STbcrNCR|?<=$S2OqrjZ??1t`<tJBRs|pZ zFF9uOdzjvIU(zgZoh>`*$1W4Q&&=6Q)0Z#p{20B~+1YD}=iGT4)aKToniXSye^$+o z6IOqGc9}lCdhyy7=gLc4L^odRnUb$+dePWwo8jJHIujHYS_&@R3GR0|@}B<B=CR?A zzM*)Kwf6iY*Gm4|XQuXimGEA;)$m&W5efe&+2p^^ZI1mq|6+eLYr>lO8OI{#XCIBQ zzt^>~Zii@ct-bcKU-QCxzUHo#cr6`mcrAW=(wg!J!)x=mC9SbfI~wtSo@nx4R?TBo zyG4@!UYF|m>Mhu*nz*LkxoczHVv*#(S(?XwsRs3YEnXt=x_GI?>*L;r*Yr0ft(l*7 zG{XLF*T%XHqRD>^w2%Gr>o~$>crE+Ie|GO95%&YTHvY4z<qh*pw%zE{^Yy-`;kExN zM<ePTx;Fmn5J~<UseSC%^`#Q8?KdQ?sgLT~Shrd<xwcaz`LA$T&sYCQ!)wo#g}RSL z^taAAxXJL^b5Fx-$D<6d-S-aTduUs>U;|^q*2nc#58oWxGpUmG=DwFKJCgs#yS_R5 zF@B}UF;ELRKl1<grv;ub|4ROUo^>fqW!g1|lULZiSG#p>S3Q(vy5zW4{iOv?EB4QK z`WwUB_KU$RO>xontUu>9=Dgf}@BgWu|J7^%Pd?T1<Nf&`@ArlJG<}x%{6GKl|62WH z|Nph0k7R$dbBW}RA6;n|Zk;!o^=7?Ev;6J|E0x0eYIdJ1GM%PRH|hMi+pE3JW|`-! zEz3+^{^?$Nvh?{>HTk+FB|m0*F4bJ0Hf@)QgWD<nOB+w7hA#CFJgU9KJ!hp>Gn?ri z+kPX*tJ(4WatxQwHR-Idjb!-N`H7+XP1D>(Ha~otKRwXqJ=NU3RD=IpW`LZGzH8-& zNo-FKt_zyN-k%vDw?@;o@<Wp5spiU#BkKa?9(?0VxF7ea;Ty{zr}OXQHkLbDh8X|X zzvO@U_v-)mwf}Ly{?BCgZ+_O@oo0{gzs!Hxvf4Z&DQxYfFHu_}F7K|`<~D2U;)%X- zky_?wbV7MF^g{b&0#{{OFALF~K6Talqf?K$F3WzjuS?@?vFoaryT4~;?hig|^4EO( zwJRBa>VI8$`!gV9|2^6FQ}{1`V@vs?{`OCJ-~WX070hNC0qkooE#Qk-<0xwo-E5`c z-fWeSs`;v6=F%0+UEYE0JsPgE2FpZB8+az(P<+);!uzVBBj^fa5bvvo)k{|}&&mv7 z-*Szm<j;QhH?bZ6uXg-*E@@+m^{iuf`v1vPf&a`#|K?Y`SY`kD|2_F-pBe@JAOG{M z`B~r32`h|hX6(~Ycx<2f{I}(Qc5C5B^_742heHifuDSI$zV~N+(bkw5|KFegQUCg9 zeSSvS+dBo0x0GHfdY}IA_EFfP>#A$@>a+Rm%dcJf6_Q<eL~*Zb(fc=-%x)KX-Cidh zek-rCDDyXKt+(pWi%VtIwrakeacjkv;QjXA*6#5dZ-p}T-`;wVks0#A)c4BL&$IH< z>o1j=ue(-epLhAojsJaWRmaPG6`uVwH8j?Gn0)F|l=|ULjoYRrw!7~!J}dsqt6cQo z`-#)k;$NG0)$nXhd-i`~*s&M=7e%9_7ymEo`L*P#;jjAaV=wBjHC^1lRbuh}drcSX z%QW5o&k5@J_t?wum-_UiFYlwfF52%BSzIsOm7pr|_rK237xfE87VqCHviSeSpq_u% zO(g!F&o=n=f637o`=usE2)X?)H<9?eJ-cPi{z-xV=WQ(N`1z{QFiY`SxbeUK^XD#$ zrb~Zez3jzfS=aGDtM&T7dM@x__5MSli30!U&z6$<8npSkXUdLcnSxu-+%nn|a>;9p z|E*V_^rw9&7GJy8-_=uz@9ajmcd;qio7Wz>R(7`}diT@h+_&O9n>|1MzG3C_D)IFU zsm#|`_Agvp{KYhS`ialm*75G!=Gtm<!7X;zs<&m|xNhIw(fGFRkNqvV-~Abj%co>j z=hehYZ_e7g?fRnSTlaR0XD*FYjk=lA?VC3vKbWWD$}$<to6E9hrv{&9zj@_)+pR}R z>Y0!Cy-IssoVsA!``d}z>XxhA`Zs&=7UqAE?$!mKE6Oh1+8pKkt^d}+Zi(9yj`(l+ zIP?Aup<R2H`S#q;mzMf8#cbu}wCGDe!*foS`fhu2*#B1B{F#Z~m8H{kzg`H+z10=C zt>=n@Z||CxuHWXoY9*>6bJ_~koj1O|`IT=?OVMB6pcSw68&$SW(U^Mfi^tJBO`5Cs zatO?t%f)=@iw2LuhbGPi1{$u84nB;n2ld57*u>Xr@GMf%;CU7iD50}yp@GK6g$6V7 zoD&VMwH->B!Q1L6FU{2~XC}fnQRHAvfJDsZg$5S+&WRsPT@BPl*uuAJ@T}XQ!DFT% z^w}w~z@_`pf{^Y*4@z~pn!iSgu=!^PO6Xi#WT0_zk->%=0TO4nFEpsQ(sn3e6K|{I zG~U*O3NA_0G<ei910_l}FEo(wh;N>`$Y4T7poB{x%jBhp8fNHnHE)g*VY3fn5q3-b z&?(k>(A-RfZGC2-#2b)VUfqWhHuJU~)HV}gQ*%jrromHC&u&mI!sfkIgU9XS_3s;A zHhTr|_5^Zj9n}8EuRTX<{_6R&mg%4P>ip0D@Y3`@;oeWP+?0y*{Fa>8)Dd{JpZ~VK zbNH0m(_;hP%>O4p)8}dE|3KBA|35<ich2FL+C2C8xxe!A++Q>Q^Z#BSoj$MSyy^aM z)x~>4|3%D=f4|f8QS!p?CI3Cv#-%ELVzc<)f9dQM$xkJ$k#&#iTgCqii~P9X|0n+b z&jQ1QzeWGzWnRzx&HVp*$h-HC{#&VRXUN*6S-8SkNHJ=1PYUzFDb;x&1dM`zTCX(U z#B`O{PwW2Y#ajDsP5v%^{GQ$4(vNiumhOMI=l$!|+8_Sf9{bO!dby<Pey#h}&-STn zqTZeQe+fL*!1Vw3hQI9MPoKS5_Ogq4+25;|K7CVHpFZ9DoSt_^ef-w{0<n&Mafx!m z4sZE?+Hd<WQCsG?F)n<9?w0-Uf9%%?2Q6B%f9wCHe$DlN^R*WKR}c7AA9erA|HHr6 z{D1yfee3o&Qxhb3<4?!hug$;s_3-`4vG!Xl3tM>}+<R1i*C#eG&2wV?kNA~Sy!ZZ$ zU-AEi|G)LGYYyc0ex8xX`~A|<|HkX;mzeKQJoWjGTWoOM<pb?I&(4bfq5HPvPS{D# zfAU}Di~jt1;_>X!k<vT*u6KWU{}n6R-&fcAe{F5s<MOYW&%VFTI{x&S`pnbI|G)h4 z_>;l2Q@h1?tX^)HxGq=g*}t0E57+D8e3TFuVB~1Ie1{eP^ADwyrLCJv|37wKG{5Xh z_jH4=(sd^~t@<Ci+W&pX!5w+3MfT-U(;xZH%O1YYFX*+CI%0lK`hCWyB0*)pztZA$ zFKeB48}IBm-)=lHKmN(B-gif~)IW;$n`F}e@<T<o`|*EAeU|SLmzn$Y_Yb{Tp`Gh= zg?_A<prQ9_d*`>j`?I&+-QSr~U314tdav!Qw>uM8=|0=3Qoa5u_m;ilXVRA%$8SEl z%xF!_>MfdVb%(W5cSeQo{<~(^&Ejo0tg_#3+%I$SSnch5_nvR&$-U3-w(;-1n5@_P zQ#)%9%&N~~pY?4+^Rs!|Ge5CyY*rV&*|tRL%}3^K8Nx|sn>)|0%WO9bxjngx=Ze~2 zt1jO>gVWP$KJfqkApa^OxNKWeu)Wo4-{{hrzW<N2-x55!JmT%+=&k%|d(QS<v0o^= zv}WR2iJdpicKuX-BeC<O*|Q)u>vcDIZtqwxx2=EwjI7xoww<&47@>cA$@9l=CvWf0 z4VHVAH2+sj&$PKQ_t(w7?_$07zh(8amib#FV_$E)S!;jQkN>CsY~hW;vp>H*zGmOM zz}f%)=Dz7M&cE@=Y;D5l`_Z|xcRot3KG<ft?NHOxvUeLlZi{@$TIQT)A04Q_cCSV0 zoNY@s-m%-A9{={u=d*8*wdHO(c0TpZ9r^!P&Q*VAEc@rUyX>Ix{!RREcVtCd{4W0X zF!cXM!_p?EPkv2$Q(0`6II37}n`p35;J=Wt`@vv0Q<;;=t8@!BqT*&-h$>i!DOiXr zSV$;XNZy?_*+NReLR!H>M!`Z>!9q^KLSDf_LBT>%!9q#FLfQ2ZXn&YUXEIY$F;i1B zQ`2Ooro~J_>8;WhI(IrQ2T3}}hzrb_9>?(LK*NJ04G&H<JUFxE{8S4Q(YPZkHHD5Z z6H;Q?zKeyOokhJ}+5)jr?9PFYE8p#E**%ScG2pJNN5d;UCzg<OL8Ap$-2zNkBRzMR zY6vi2jr_aOnuVk3m5!58NV?#n1y<b?Sj)D$UA*of6_PIaXhH1S7Mm3p1(+CFgcw<r z7+H)MS)3SIf)*CG2yiP*;8qaeQJBD^Ai%3IfmcC*PhkSz(f9&4;R*Z-0s;yX1QY}W z6($HO2nZ>__Kk^hv@Bp~T*A<}f}wE@L*s@OB`Ql;IgYq~4F6-NV9?cagrV^SL*p5S z#tRIMR~Q;^Ff`s_XnerX_=KVH1w-ST7596ONcuWdH8|9WaDsM<1*xcTaJX=Aq;PPw zaBwW);5fp;@q~keg_A>tlf#9RBZZTrg_C0mC&v*^jwhTPEM5~QXebD19{pZ;Iz}PH zK_S#ZA<RJ`+(9A2K_SvXA<97^+Cd@4K_S*bA#PPT$4@3E#$Xi{9u60ukHvQuiE~H_ zct{F#ND7>g6yT5&@R0g2PyLlak%V%)Idnx(m3Q^BlS?W}?yKtT_#d5E_wY<+>%Uk3 zgHP}IRWD-Hy5K+alK;m`W%xekz5JZN-O^&A?{??ai*=8C{jj*1wrqBdn6jtD7iV*c zh&94N{W~6h(AT^=zx7_rmcny^hmW=76gSU0XneWnn~uq`dRBGc!*Z;xS1kL3*G>p~ z9^)MQ{MF9pllL=qg)Pb*Eb1M8@HTv~&N!jEzqKvqQ*}m%^8U_`U;7uus&c(NAQ2kh zD*uB0dHoCSqLP?}ins0my}$c^eUNA8|I;-!aZUfNZ`UuWTc@Wz{iFE*@C*NpzV`my zuc=yGQkD4U`t-N|7xcw2t1Y+{U$*#f`->u8tJ=m3|Mq47H;HYui+{fTTlmU7ul}D% zIrOW({?&eg=ZBZBe6U0F#s2bZv5)TOYCh$Q`!jFj-^JYDee2|(%@%$w>+99_>Cwy2 zGAi7wU!OUecCl_^>obKrH<YKmdw7+j|K*{%iHjd-HcQ%RJwBHna{lLnTE}ybm%Y<k z|MR8((T2~r`Hh$l&15=sGw7=OISGMtQUd4ZC|OqDYILb%eCwdJzlm?kLzSJcuiw60 zzwX(3MW+5ek~^aNAO4#ECX-EGZ{yC#Kl+0nKK;*i@IPoe+&FQ5ncQ}U?-mAiyZ>I; z{yxM0<~Q}?AI;v|l>Pm--~MNKy{tqq+y2`T_dCmPhW)FUclTR-soL-R-iEj2rS=^C zSJ1Te?~ncVGXHN|PX77w#Nocr=i^fJBJMwsDc=3T-u~;{$B$Y*p3!#|ytB0K^v`(r z{U`ojTc`Pd_pcd`zrXH0^Yla2ou`-Ojq79HJ>DT?>Y^RB;f&s{w5_fGzA;O0i%iV^ zyxyq#_VM~{Tdhmx=AQo-t6lk}ber=hzv$zd`ZugS)v~SqCv1wH;dpD^|NHl*9&imU zd*9u`owxYezTV$mF=+`7uCgqwtR*5LF*}<T6mDBI{4UyWa`{tyxV^UaWzFknO0=}K zPaA%ZnPqai=;gGPZ+$aY&-`C}&RV%Sfy<2N>)o01pYJfH>V3C8Uwp3mT=n_r*U>vK zvx~o-yT2~{UD9XMlG^{$3vX}EeRKD|$K-0AkBM97K1t_o+`Ri#%^5rE`jxw6rdWAg zaM`1qv3M89!QAVdin4!L=bl$`{Qe--a?8d*j@4qR9J}qp-+kSi@cysoraRB?GJg9I zD_C?tSN6@{Yg?xOwvRcxZvK09_n!a#HVKbZEsY+99`BX%zuW)BG5*QSMf)?gj_(OP z>tHJ9y+h#l9ZAb`KbG0n9nD|e*0v{C!fM|%o05l;VXq~(`z(v{pVE4@iY;B*&CE)# z|CQAhgICM9%X;$O7o6m`uDECC9#@Iq*YYm!5a#E69KPPvx8kIV&z$o4nw3|BB4^CV zGdg#2YVa>!GheCv3Bl3j=1W_%pPan9SoWpv`-zwJOG{H?{zjg&2{>)|)X7;f`|$R@ zH+$_lExYfPK3VrW;Kb?Q$5Ntx%rYpwVK~|HtEZy<g)FU(S=u7kqVr~neb{01_BYez z9;?kyCExcMo&8hRWRu-~)9`mjedW1RF30aYzkTX++6$S_@^965q`s~3uKwqm-l?_k z>W9c*|95w*A7802dRi`Q>0_xcQSJMJ=hx0p6`ect=)u4brOuyEZ#Y!b+8zEbF6Z5B z=UIwM=QZ3JW1L;~DJne|m{@*h_Sq%hPuQPIWndSa*xqr%)#Zt@Vu6rgr6<Q77neFE zC3nGz%R5elx;)WVDw=l6_dv9kIb(U`&Xqemr_P?%wdT&2wtLFcBX_>E(6(k=zVZyq zp0uaS$}Ph3Ub))4_XP*BSNL(nELULp;df8OqQ7I0yUV*^OZFfBPh~DJXgzP>3$SG0 z(s0Uh0ei?fM&AWRd@q=$7CT6<n8W<bfkVpismujVt>+D40ha7vntGT`non6SkPbP= z_*NtA93$_-BEA<KJ6SIX9r9jN(=f})Qtg7M*6W6C0g>!qT2EOmFbuiII9HKn?ZQob zFL<YxIuvs_mao{ue9QT%>;-wP_YG`;mFy)Qr>qyahTLPcUG#}BLuhKb!}N&C=k_ft z{6p?B_G)e2!(8RcG0XL->;+wyE9;V;%3d(gdf(6&SjoQSOys>tkc5fT1yiRB+mfEj zUDyF4c7X^Rt?v!*0%x*s=@<CYf69h8awco(gj2TDBWJRHop8!lJz^$n>BLjE3wlG| zG1e|AI<FO$<~Vmr(RrD$G{?P5iq2Prr8(YRQsi%;y?H@h(9Gqk+M5^jDJ_~7w3GeI zlvB3iApKKM*;a$hn|jLjHHbg$l<k78B6Usof_Ac-Oh0A2V0Xwn#<xp9uzZ<*%4WgA zmIa4Ho-yjGvDhvv;?FReTJ11><sIf*K2PN?q-%X|a0_0*ZRPt^?n2hP`+C>Rr?-?e zI-Z*(AoF*(x0A?!cj<IV+aHqKd;kBuAoTZt#nS)&sh$6)Yku6{{`i0R>)ZK9{-6II z^HlD?cH#c^SWXKeXIHnsr}G8BMo(Q4-}&pmVW7Q6*vdfr71!fk|9VYX@A5Nl(zE{$ zCH`;wSik0C&YUR|UoQPI|9Ym^lmCfK3y%Dc>-m3ob?(-=$9oO`#T&ivFLjr@m}3=p zA>rvw-%rc54$pghcsuWwZ?Tt;3JE7ZWWV0EjpyGV9s4J<Z)Wi=IkHshL3hT_kQ$*) z6)|o7)$N^DC3hGidye$>N=!T<nY3}9*zAaRn@juGe^_&~OaH7|*YpQ(1V8E@sBf-k z{ptV1`$YYrc;`C4e@dT<KZFY&ig){WPPu-rea+%U?`IZ&+OME|e8%jkh{$X840m?? zm-jo@{CwG({|CPAm#mzrdF)M1@;dWl@uy#Vt@+Op`epy6C-rJp{VV@}UL-ty*Ok?! zZdt#cI&Qpq{Qk+a{a?(t-ni+Pv2*F#l~-4-a(caV-!ik!`dqKpa4*~Tn2+mi690@_ z-*4)ay!f|fWpejYr{z)nv#i4Js+8~Bbn)WT&+Jduz1J@+xjkFA{QkVBFE-|_klCPg z?_#l{KD!u;)sHJ|MHgBuCac*6T=@1jlZ$VKe#@4inI&8Gx%cjm(f@jLUldRL_bFG( zTQ$B#Tg1PLpP=+AxHrfpX^Mq{R!|quI~BLm7aF`dq9<MJ_un+!bWwO(V4d6L74x4( zxW#vvsLq{}Az7?*)9~Bco39F^m1Q1>`v>?}tk|3UXwSb_Ph#xb4K}X5B~gEJ;-&N9 zwweznC~v*WS{n7Zd)djWwK|jQ?J~UfSKOQQ@nBl;vwyYIG}r5xrsm$rI9X?MeahyH zPSx^%%zb<I@MZ-UZA*T&;b>Oz|Al>j4rsn=GTXCwL)@HQAF7y-Z+Tz6YWe)#k7j&a z=Vkjjq4x3`SzDdW=5c0cBO|XHeg8LkS><KfX+K}CpLIv--My2WXT{H)XW;F*XlH-t z+Y`;}KdZz%*mR}o^z7%JKVC&WGcwXsn%LbV>85YKwnTHb>CWHjTePB@nWwdOTx;zR zYwHMW>zL+N$*{rf6m!GTKskmreV-T-DyOiYNHQ;7$QmT4Fzs$i+cb|#h7E3~m>XO- z*&Jwoqo%jP=aBZBWoBCfrm;sjy}uZw$<J`jtCC?u#3|;6t3h%MI@3QfB<xk;N;qip zpIv*|Px}a?v;Ti@ntkiSqRfB%VZNX1Hss8@-duWlkDSh&M?xEly1Wlu%8cW`=J&a3 zL&dD?%~vPSYJPfokKCI1&%PdDn01|*J0tGC)}(con;OrqpTA%STjarK*Nb1+ZfZWe zUj0&a3g_&2=R3`xvlF;y$NMk%enQG@|2zNp6K!+q?oPX>+<oKEt?BoaXJ^#$_AL5o zx}odr`q~$tC&ZfUXKTCoM=HYYb9RE%+4YUNIdyv{-cx>k<4+amUiWVse|}~A_4kC6 z+5R}6_Y-?_>TFx@DQn;SW7T(0+4|<6UFv(?r)~PVwd<a8^vyrJ*lXoBO*^~(^`+_* z{j=*E51Q=%=f3fVe7%r>n|+brwCW?@7tJ(UUsCcz{M>){H0j=*nr{AUne^TN$V;s$ zJ^H_(Xy-}h9@ffvkqg%y`G4boZPY4u`^RO!_qR-o_`mjIris^|_eX#1U-h|r&hF1U z<huX=j|g0UEqvOe-+R9aN9VoN-Ws)3ELuNTdF#wz*O2tz9=XSB^|p1}>Ti9&IsVOs z+WA*D9#z{IytX&!`KsRvhi<KY_35Wu#~QO0yWYk!30_SK?JtcK@bnG*8Y96k7&52J zpSR_Nrt_sE$GdLrb6)bGfx&P|D8Fa;@_9FYpT2v|?sok4mi5(VZ<Qa_^4{O-AN5=O z%;p!t&!d!uSWh>sNXbt5z~CLy&hh#{*vVHDd(9%<e?IM8Tvv6#>erzH!<Q-BO3!SJ z>ByM&;qf*pg-ItLsoN}R<WQUEe{L_k(sYM=690`|biV%dWL&NgwDZhz(|t;Q>*CH^ z{d~@U<V&}Ggt6TX#k6<!iE}@l5>C3-Hht~Q{&RxGVzy~#=l-{Fk-J?P{3)lyLhk9= zzv>Q444d^cF3T(RY^-_sb(=tB%je7XA6q*Xnc9C&kS@H|ckkNG*)>NjwWFh+->Z+C zWB%9w^`9@bNr9);_-zy!{<QTR*eaRsE9Q{UAz+*%vUi6od+Nrik1qa|*N!OpKeg5M z`}SMf+vcBG^n0IB_q(e~-S2K+sNSO8_3q%dh?4(1Pb~WVS*ZKn_imkC_tiJPsGEFZ z(eGZN?st*i99wL~?&d#<DB16LbkT2BneKPut0PLzM=34x-Son4Uh>7-IbE*b8)drR z8LNohJ>I3WOI~T?3p<CDi?#e!{bi>Y{k|I}c6WbX;>Ew&I<Gm(&Mf+^zE)?~{Pe_& ze`B<}-?4Iczni>PXP10J;>FsQsjlBoW{F+R-?V9koqtN;U*<5etNh}TA@g%n18q-# zS746_@qa4zh^hP4%?xGrh>&{z<BNVd%ya!}U#zog{rSx+{)rr4RJAQ#=Toce*RN}I zR{eKK3H-|+CU*6`Kx9b$p;L=~sk?|>-Jg>h_*Z74>(~Cs$dLaQ#~1y2Yt#KIy;x^e z{gm{;zbt*OUyUoou6|Fy#5*TBur|8S_3OhgazBqR`gP5w`_=6ZomKCB=eO6S1lD?M zcfUH{t+VR?<x`7(ojU4Y|0m8tqxQ4k<Fe<v(<U1k6<^-_<bQL;z9)bG?<x9UpStq@ zoyRuIe*d3ezhKsvs;V!KVmDiN@7CLH@Mhiy*E_YBl5Sk~fA(!p(V>0*cLVNh-?Zyt zgTG06LR$XsgP+}ZI6p4CdH-g0tc^_F&4P+Zp>i9w;}<J#md9|u3;Ft$xirD}uVluj zR-0LWdT*}j>U;0_S>7OrrP?#0JmzS>r_T5LPv5Qn@ZR~zenG+ieojB4U#>IyQO)@< z-|fi$*#iH{T>kv#d?=;xs8pdP+flpbL6yQIQ^zB(oywOV+^X2}+F4X<hg*f;ynTNE zKJ=cdE}!*j#f)F^3k`42kO`lWtYs3u=ilcYH;?=Ny}xyqg6sdiEB-J2TA%6oc|Ys- zwY9djD^foHkEwXRl>5-b)RU$&=KT+0j$Zn}u0cSvZ_$qer;GFUz06wEynOXX36s@q zFGHL!tyEhP%y~mkRjDs7PcgK-V%It`1Km5Rwe#|xq|NF&@Y1+a-pi=Jp6#T;A5EXS z$=M>)@6XRF5dBbI>Dw0B-MYo6c;WM?oI81c>jN@6f9QLx{9iTo|GQl6|NX_U|Nrv2 zrBeC!O*QZ9wys0J{@s2nZB;hSylh_n{VQKL?RoiHoHxE?x4+)LcmJh-&JGWg*jHP= z>d)QBXL;^NU+3<NEc~`gT8*pj?(*BKU4Narnr)xH;-X^6ijQwCckjP1a`XRPwzjD0 z`=)2_|1e|WxrN`H?mm5g<F)N;^%ZBL-<)0lWlHp(?WgX2O6j`r%60d`uiE`5a>IVy zy4o)*bW5yF>ekw~ireM)UcR7c8~H-w+upswKTbP6fA*F)HmX*!e5ci^d%dS8?v`Dh z`F`Ku83(?JZsW>{fB2N?c6i0=HJ$Nk*S}4D@}Qxn+CzKun$^aa*6p6AvAMRj)aZ8j zj+JM=iSh~F*mua6W!w4>`AoONe{4DMO|&jJ;WyWMka+%yocM>!Q?{-DP&4zbcFvUc zyptw8r+cUHdMEQ9cIQ6S@rJ?K=>>zL;ktQ=DU<mQt$1fI;r2Jk*CQ+B_`EL%#ShKU zzJL4pzE6Dy@BV&&^Z0tq6!o0?lXivw94#x4Dwio%ZqAwWdy(SO|3Q_PT14BfnC&cz zx}Vv0(o|05Qnae%rTr%ajn;iNOOcaN^Rc{rX~I@bj}!Og)6PfU2{>{0!ku~3|4mNV z^`8Itm4{zbi*H$0+to+vNn~smU*&h|a@9OjpW;1Q=U41LZE*PC4x7)rm-B93v-#=G z<6;ueZ1x4s*ll{&Ape<8;y>q$ZC=;pH{|lDR=wLa@wIb|W^au9-cR57vYv49EOc9U zKq%`8mk)>`mi2_I2gH!ddct)^k>%TksM>@J@#}B?Z>#HDla&2!`K9U<@#xL_=l!$Y z)N`r$fQd_rv~OcWVaOErYZKqIJQHO4eL|FxnX|FA<AjS#in5ZCkYJ=|q-}zl(x%Cq zDh~vlddBFjIh);QX^z~6shcVfM4WoYn5{XR{R~K8`liYQ38$Vhc7s$b%aPkKb5rGk zj8l&oS8H~&pHX8eyZnIZFlXaao!R9+2{&!7Upp7pnEE*;+FD;c!tDIkt)IhYrhcAg zxxH*!%Cxhm%k$ozky^M&OQ6X}K`a-<29ej!i08hQahe~#Wvg*^OB9@pHjdx2)fkEV zEd8a@HQL(V^?moYvb5d+2pL!_5ZEXX*eVd%DG=Bz5IC_zy>N}7mX>g;<nGOGD^>(d zNoAR;^rykp$#v_ZoX6TZZ|zozrAj;Q)VRH4uBnqN>*AcpT-P{6r}iw;3ot%q_ANd8 zd*D@#+c(<Q-gPfstfQ<UlPVd#(QU<wk}0`+j!7+C^eAxKN1g1Ju2b$e-2&`Sndzlv ze-C8UyglRGwRel9g0=~YOx)C)*`vRrXG$(h=@JJ<kx83+7EEecFl9<A%T|>wo45X6 zF>6ZhmQ7oKub4L_mxXm{j`9kPRB1;~&D$JVr~ZC^^5t3S_dmU5tM_c0wP4bj|GdVZ zi!P^2*-mBLclVFI#g-%g_;&@Q{)><Nw0~XltN&jgwA-(Vs@wQ+eYM*+Ls@>4S#ENx zlNNDq;*VK$e&wo7H)gi3d6+ekb$#f~?}73KPk&E1(K4xb)2Sk@ca7rngp}rqElqlO zC4*1?gxJ=<y~|(l+jDY!_F5d~|H)oacfG;w;`xE!zS>9S7HBNzlQPr^dsy%Kq~7SW z@uo|Aw*H^LY|5|sY9TNGKk)tgy}a&X%kj!LcmC(jJC*!<|K6?Fu6+&CxS#vJs`NYa z*&lMR--Wa1>^Sv3X2wdsn38GFgOjg1PA>mA>qklVw)c~Q4=TIK@?=ge^;*tpy<)dm zz1EteCuVHe+_=0dmbdri-?YEMwSMN^ocAvnuYCVHJo%Sykm}d^M>#!qZ;vnOVBIe( zWANec62|c9tMj+2&ho#w`Pf<i8Nc|Ky!kLUZ~C)+y`SE0^V5F4K;-`Hym_zWcbd!c z*eLPY`Dz?s$aux)c3|1N@0Y7h*vv}LFZrG!lJiQ=`~5|W8&_=a9$NO!JHwSrifi_+ z^o!Ld5@x0S_D7e!Q%=5d#rA{9>|O20Hmt1v8=Uj%o=%(Z-Ret9*(MrhrQxa@SN^^! zVOIKo;=RR<8(04R{bk=JDJir0*S~UK(EPmN|I45E4K`nFet19k`u~=;X`B9qulye( z_Fq2c&j0`4&z#O)|5CcN?!v7dD}B%Y`Jd{2S-*aZnf2YsyMIoUWOvWyeyh7(<W}_B zSvmQ8OSbGiJ4a%-)$Zwg-~Pz^R#vF{=I!C^+twxf)85~B#4l;Rq2yb(TFJkY@h?98 z3|;uTXRFio@8>0Vy|vZeAaz!+@oL6Av54NZMh8==&q6b^Oq4gJth$)-C^c;HOr;G{ zzH<&_nb@+1Ee_;d<Mw&+fh?2K)L9~%u8FPtow4frf#t$pmA?);lq-ZUe17Ns$9)S= zyt^-%;!vvhYr4<G=DDlRh|E;ExxD6a;GM7kzw;FTJ+t-E4}<IZ!Q$nAr|X{i`adIe z-Tuw9cK&-A_UxDVrZu)b^2L8AM=qC@aDVnIIpUh!vB^9C<*m8Q*Q33&P9pl*uk|-) zzW(o=wr+mut%DoY{&be^IcTVUo&T{PNAh{6hRseN-~82ESn=&&=E90^b&UZ!)%@QA zb*lNxf<UYwn`(Pkjd^?fg}c(<S92ZR{9OyA)_q~cw|zyouAjC3Jn5gikFJ4ORp5W; zym$W>a`Il){1xx{B|bXlfB1<NhP|`p9>4edy?=B0s;`}UH^=KbKi968=*s@TyY1cW zT(+?J^~VB2=Kr6)bw8I)_gy#6D@&TA1B?S=C(Y84_`Xu&<@ITLf3;J}Umjohaov;6 zvMPlYGoM^H+HmupeB1tMHQS0+`D#Bly)|yT#Jz25$S3XTk$=C&z0v!7-rMu@w13$@ z7EJoRU*r4#%FzGWmiuEqrma=_Td(o>efR&<ub9Q8Zb<2N$0pCqz5Z?Qbvc`)Cfg;U zt74bG%)cHhosyotW!H_HQo8ll(TlI%Ja8@Js>ue^sCem&QrD9|&dgoA)Xvc_Y?`{A z_M=a0Enf$pE7W|)BX?@WoX-sY)~&5<&qQKYSLl>I6G^iU|9B@m-7m4JSkzxDdM|U? z;W>vCtc$tUh1$fvIitBF?BkEyGlL7(K6;sx;CFCq*YahJmaNyc=B?*FqZyKC{X6^p z0-Jy8pXv{%pPMyvZo1#s0O21+KmG?8L>>Jf?)88FiARzD|932W{kv?h`QQEVXQN*g z`3BT1tl9OZ>xJ#x|KgGVZvOi(nEpI>_rLq^{%beS)f9C&`k&n`<+%iRTG*mr^UX9| zX8sSq^y~f`W4Y-6pMP#|Q~JL@`hM;2?q71Ruf1XpTXW%iOvQY=4GFX6`TJgH_l?NA zJfk@0);`v44+IbAD6Vz8Su#su_PeMVx@(RG)ZV!HY+GD-W$pX9^*4Ua$y>mG|ANo` z?_afl)qRh>`n~=U^Ip5RYwVH^_HT>UJ-MRpwcGav8w^9{@BAz(>H7a!n6}Zsc_63! zKe5!_;MvmOfA3a3`yPMAc>n&e^MC$v*?#{0{pHO2^OvR9*~gy$^RGzy^KVPDXWzMV z&b;T}Y`EY4)UiKx0?r#GpM7tCHS>OcUUJ?1<=-=$&%8IkV7&i+kHFi`KXx}nKL3_~ zG4sB?cSPjd*J<_Vzu2oEf3fpzeg5tLmM+3Ui~fl(`cnUK*}vWI|J$Ce^!fgO-==TO z)ux}%MBm^1yKv#(XVqoVcJ)CSTbC`{`}I?{)9mWh=39R{FPJ^eb>1zwyY<_~=Wk?# z%DH5B-+nXK>2u4ut@AdPubir1{xhdz;p^x5oOk*B)2d5s#SedbG3(<d+k(VZ2h`W@ zfBG+G;pTtZEp=aeWWL>$*?GM)*6ICr^;5NRRZ$%AyJKGbzR&8kpOw|=|J1`Qf0qP0 zxx737slj5ijHI1s->Zol{5>2~*soqYc`^6(=^eMvCj6hr^!Du6sa<c+R=;pptUH>2 z=tBB^@dESNg8IHG)kVEG^!oQaw?CwDcJZeb+5RUzdDBjKrawQOTqmR5Uwc`j-&#(u zzg{b7-iM^@%13wliat%QWl3}@y=k#~>I9<|%?6ooN)@*Tn<}pTzWh+ie*Km&&#GI# zaKGo6xo`f7nZLJj1fP39ZSysIm6Z39+RtsaM4pSSo<C>$?hrFwkLk<#&Q3i$neFsX zTiznaJ1Gk{taF=pP1$Y6x6=lRZg2Ar_3$^h%)Ea^EN$<tfEl$vtP{^Iw&yI4|2F-( z@9%&a`yWaF<a?*}Io1E#tZU1+q>IYm-(-8R{DRNs7w-(OYu7!0W_@ywRG3(CX?cIf zwu+u*-4V}b$J?h}esKHlpRQ$odBwcDe~S3dyQ4P4cBijFZP9Ga9A}j!3_H(8eEaFa zS~UCa&9f~YoJG?exQnJc@H#DE-|4-8BlgJQ$=&$@Ut@ee@~%qTz`JpR)sip=`J(Bm zcV=*{bmjP>RL&`)Twd9vd){%?#N9QWy5}8FP2BxQfAv+RuP0*HrI;rwv7Y38x`}tn zgsu(`cb6q9N|z?xNy-gd?OG*dWxm8|me4M<CBM3^X65HNS9e^^nz*aOBi`kDTZl*U z5ozfF#<_wMFLiiiAIg4{7PZ9uVPu)H?j;$)m3<v0?k=a4mEH<Y{MF%6?=pqKWeTIq z6egD`%q~+{T&A$POks1G!hVSRO<L3x&O^01$~sfHU8eB3OyPBz!sjxD-(`w`%M?ME zDMBt&gk7eHxJ(iK)Kq%YEkZ-TW&egkmzj!xCfxPuE^6+W)ZDSDx#LoE$EW5Fp_U)~ zlNZj;&*J5~!eaeCi}z>xTQ%2@@kw#l&e*I!TEFz}f6-Nk|DAX4QS(2x)a%c4ng5EJ z&kofjmqq!dO8$RvAwcH;!Jlc#QQM6E&6E4jc<f70Nb;)bKjcML@#+=+pC7jK|M}UU z?Z3}nS9&h_)c>{54nI|^X<K;m{ns4d{S}ulyeV0-@8`t-x1#q{2HgMjlesNyHP@4C z-j|=46}z2$^(JHI+aCryV`IX1)>ahnbncHT+GzUU{``YoF;%f|s$NN5e>?YI()a6? z8;=+N6xgz?$aZ?30P}MjwNIAv#f~R+U3(R0UVg<JQ*fckaLHSy!0QS2;!#gS^}c+{ zPrA`su<knh=Og!ie?DTje&31K{Fu{UV=EosZ2!T$@&B5%Id<7|Bn<0JS*G(BPprK! zyqM{p82h%5kJ+yNo!R~D&=Kk1vHx7-mTmFzy<RHP6n$gg6q&0D2KvDzKC%`MwV9MU zI##%x({0%?SzG?qu21iRXP$A@4_L<U-ego|Ak|`^H05S<VB*ZZ9=507{GIMpFQaJY zYMGEQb1&1()NePYFuM0ZsJ_&1H|A}#Xfkna3{081H+s#R_Q0i@Ts=Cm@)EIJ8xmSF zyO<WAS*FqBXK~BKwNY@nCRdM@AeY;`Wf}&*Ok5ec7if0PD_!a4y+D&If_bK91WT;R z1}>)!JhK)xlwJ^F)oEF#@jEg+XN$<JMGad)0&UAQx+AA&ZIPL^sNw5`peELhnx_u8 zWO$mmGG;H*^pZAXc3r(_C1-@#bJ@$E+Vj~m{=Sd+FMQePj2zFFQ^Nl<KmPlFFs;A6 zzU%+-ALlJDXK0y7?=|^%YtrxktlEF$_q_Ul_j^f!Z1BJDUytn$zco3oIHvOFOrAT8 zQy+`fI0{#0EhxJ0mF_J%%c5V7=aNX|HeQvyni;!KALd`0`~G2b=I)=LYIfMys4tkl z>D)q*f5)Xa_x$`h@8+LQ?fd`y8P@%szf9!WGIi;moh#)s1D3wnZ+KzC^ymqhXH3&7 z)BiY^sQ%jj!b#-scTmM}Bl2?P9geU655DGa<p0rPZBrcE`*`s_<64P*`;Q&JZe182 zSC?IQyPrGaNc2{&JP(VRhaR2WqIt0Q+KsI%P0M(zLN}`2T5*jn=ndnQ0`3y4M|$Tr zMPwUTL_c$|Q#`VH*|mg%XB`#aI(~>79RGAJY1Z$GH~NhFCJG8y_7{Du=XkMy=Yimd z_hmoC2XFd6dDFk$n?CGsv;IGSZsGhtRga4gJ&ffRyMLxW>e;L3+%2A=kLz2mugcr| zF>?0aUoJDOTxM9U^L#U*BhYot&*~NT>pvxAHTS5WQF$Qq`UAguT{P#16Y)VeSHvIr ze`C)ZoyFgO#2fwNzL;4V{zp9X%)F3@2OBpBKeA6w2uUkj#%}+3+u!(7)ob-Hr~KWo z<@;}c#Q*D0#b>{>{j=j(<EO~i+V`$xpMNYf*?WhJ^~d@5Z?vvz%TD6EzcSXu(7(r9 z$JRnC|9`lRYEYSl?(|KM&wu!$@$vWfxr^-oJh?yBIpODh5rKw3KKdWy54TV0_`k)G z>;K-9_1}M=ahh{n<bQ77x0dru`?IB`O6%?%t2!Wa)%@q(<F~$OpS$*^gya676SdpK ztf!yW-L5m?bO`s6fAQj3vvzUGOW*kWf5$)h_x}%n{a^8ae)ix05^~oovVZ$+W9#qO zuxac6w`+6W?w^;Q{r~pIoVWF%^Va^|e`532|Lm19^Vix}ACTI(VQKP#1Ny#pp{uN> z3*RltFPl2+#}WA-VY=VeL^8iNzW(vl0qxc2uPm9p;nfA-h_A8h*S-I7rTnjSS&zNy zR7r=|TeqGwy?yXa*8Mw+KKi+NJeOMd==a1u?+(A?sb~^#=dEv3ZjZR4_H4yG0XIQG zcOgM{VL^8hL3dF>cd?7fU!B9#4K}dtI*}mQVs6E>;~h`IUBMT-j1QE$KR8>?*0XT` z;nH-5C_beq{+Zu<t(k6J{=LYW>C2b@UGF$D+~0S<<5=MZB6oOzNDGhmvE?iau1<W^ z^i*-t*TSjy|9og`d{D=D(Cg8k`?LScr<^fdwpjT~-~a2GUeD^Mezl*f_D}w)-2e6S z@9lCwo4@_!zx3(1v;WT1x?Y#Pf8n`ZaZY#H?Y3TzzTp%g%D4IY{1V@c2@29u$IK_c zzql_q<4WE33p*ZKISb$4>NNRuH;?c6`|2jv6B9G`smCVlWikjm5XUH?ov@yvM|49t z!!fR{Z*?F-SBy5qExhkE=R@y-Uo2A+_07-nOnrB&CN!d3H0Va;;#JqLW(K`CR}QW9 zQ}=u*J!$E_{4djf>2^){Wq&j1l7F7QtInIHQU)jdyL@sNcD<_bl#;0QH9hfug2>%} z!l7Tv#Y3BG%U83>Td$BRs#~j7_Bt)}&cxWDKhEBv&f9Htc~74Wm7JzGp{>8pL2BY$ zHPx0h@6N)DmrkfTFAcQa<+Zr(;nIuWett80|EhShx5n3(>~cSAKTTh%m!md6VEK(t zX<u09WD9ccnRe9V4l}=rpx5cfnG@I<gBNzNo?#XAWfMHhCg{s9c$QtzmqYL@hoCQ~ z;8{*V-!7(^-`?EgG|}ZsIIxG~;1P`pjK+%{p0PwLv_3n=BpIH~9Io&<EB66cO3Yf` zn8u#fTN||}6mB!yA$Vd(l=KcJ$@J`IX<fg$+bx=QZelog_0j=n>B)_qn;p{jb+H<0 ztB9SD2-TS|)9wAmH<1dHRW>@P%@ojdXym!jz$CeViPgY?UF^V@MgQBkIJjgfDA@{3 zRO;{$c3Hxuc!5<=)0tz7qsuD=rCNcBP8}Y~E=z=zE^!KKwsvfBa>-Iu+Us$tA*sW| z*eGa$l;Q>6jY7eE&APe!tz&k5{r>a+!kLplF=+e$3qPJ+I>#y_VWH*uDbH?R4*s-% z$G_xB;=lh-+xE|1<%!!rdH-|G=F2r6=kxfOq|Lfyq<!&6cJs@B^92tpP5rT->6pEm zzPYWX+4<<-|EC81zbx?o<DN}6Wl^Vo>^@e%WT!!TR_der4znM}e(jG)u5;UU{F<89 z<Nn|OQzeQo8~4ndn)YYwn}7C)Ib8p1C;d5o;KTE$VS4}Wcl>vIaI*5<n<*d7+4u~^ zpH&(DyFBgE|JalNt4#mDe*LG&eqH0w`_JW92z{-pGSQp(c!y4u??lVPJDZxW`SD21 zis*}fJJ0?|{{qqK@^6O!3M2%UO<Ddf?cel?Gm}qgd=dNaefj^QpZ4=&ycQm+=kKcz zX)`?iV&{1c|9|Yi>o<SwH}aeH<lyAsWAeZ4Qz{Mqmf!xRpZ6^`>F@GQ|L)KJcYp8w z7Y^$p{$F=rWV7qv>%aGX?|!SxeWYKP%RTGEM6cv~>n_O{-P}?;|9O+U&NHVd+cPgs zf-UZDT^SS6?V5DsCd<0(S9vympRE`(|FZLu54?wu?#uro@LTs%*WdYTv|p*43n`10 zJ!j+j<J#l1_ONT|9}gLcpSI>F-t!vn`dc0IrF{90gZuOgoBQ{k<F)+HA-=m*RJ{BN ztKq+em3J=a{ugb3$~7l3bD>4e7nwqrZyAP)C5vWAT=$l_>7w8F>Y~%0)c*5Xp|#mp zO8&kv^Y44@spw}u&-8cMw$JO{R+ZKszsb1Z#{N$2R8jwr%jVnJrAj_|<eMtr`O95j zmN|LRYc4id8P>&4E{C059xJghJG&?!W|xw(yKv(}Bbz&iTRXprjgx_0%>qWHMS_o* zc?AV~1qFG91bc-9d4&agC%Z`Sh{$Iw>Bv~|!y$R;LB{5z>{7Z)QhE=|x7eCW8YoH_ z+Fh9NvGG_Bze$FRNmk8*MCC<=kC}O81bbx!d1VEA=ejWQ$O-n!eUwlBaq7`l>(l?2 z%)Gp;a^2NqZ~yF9+i~Pi{OZ6z^{F5K|9bTAf9H&@5C89OpKtgzqWp@;hDW-==UZ?4 zG_r~b&);?CNK)hO+rNts9JuWCM5SHk>PO$igeQfS6YfhX?)qD1`D?fNlY{&EPc`@3 zs`6XfJNMnSitn@j)FJSHLZIBP)LjMHCzH>w+P6LT_2$FHS>f9@>b!e4zvHL*&dM#a zE?eYXwkW!6QFhs)x=80naN+m&M|+)5{@t9mC1duPzxy=}zSqa3y_;{k+%-D&-GAPh zJL|8_5qmogLQUWK-<{>zzsTZS^-a3k^L8`+tzTof^g?s4S^n(*&z-0K`#;tEfBaF~ z|M~ylI@>;b^3{hk^Zd$xhC1r<hA;Ah|Ih56x8UyS55YTb*Z0`Vu(5o<_NmKj%IyC; z6aG!+GE$dhwmtR#!h{K%uKaH<vGS8$^kKi!aeKw}8x)jpPMALTS>s3j<iF|>(nnS{ zxAlb{+yCg8eO*w^LQB*0#((s)|G!t5YxGal<{w*fncpn+Yd7!a{a=1T==*>6%fIAR z|E-?!!$11dZPPjNKW03($?N%e|Gf76=XLvb9Q&1h*yHwkcE1n%V`r}W|F`8_RjGf+ z+y1$nf41r>f2m)`^>DpD@3ruF>myN%o#!p(+<*DjmRl^*m%qD3&W?SPpZ(8i#@3^| zAAi-IZn}Njt!w`GZ+&UsdiHSE-l~22_s!OK?TcN%y5{%7`LC@_tWTHoJkF7=zkRg) z*T?Ya=l%1Z?f$dr`n9ib&DY+wyOZ$x-x;aw{&4ZzOz-~oZ+)H|oYSZ|dCi`R{dO4# z99FxXxRU+5?gabm<gbcb7gfa0mHl-gOMPu(jBV|Whf+1`^>^Q{%Kz8(zIa8S?w>~G zn)|-@Z~yYWdFp%opLxFXKlGU@JyyNV@K!84<&5&S6>a>H>}-2i$XDD<>0P(qN&auG zsNJ*n%wGqsKmSmCulh2%yx`gn@v7I4zlT3>mwLCAH+t*UEQt>jbAQO3v|9B0{I8ae zY1v6@gMVFIDE{-}?R^`2Gm9q3fBs;2`$oLk=f|JcZ2Ks$_f(xHI&b!mEWP<M5edR_ z#vhI?JN-p7X`AWO>ov8j46e+o%34ylY2J3fdxERZ2bgb2Ta&Zw{GPg_R;j;2Yp<!x zM{Mr(&9P+H|Ge|p{hZ3#exKC7X5PMXbp5BacfQee#~#=0+jA^T!g#j6&6Z$$+ZTqf zzodu1PQNxkbM8);E9ZYD&M7TR>#93fUAwlJ-TmeBy%&yI?ax-<d)qfCw03pa|L(i5 z{{LH^?N@N%Sm@rmzf0~eKlb(Y^VAyK%<{VBe1EqvR`W8Pa_wn2d9b6!MbD@&PAE3D zK{`S#yL|ri1y_Zeo+>Z;D$K+zFp;(61dB@wvyu^$U}U4mnFH(_nCs1rTKpIyoFvT> zxKeL4=(Ze?IJJc_TjMg@nT0;m8`_O>4=9}4!q}~GnayXBkMxF4quc`;r?xPz2B`oE zbQ|R!FgUfPbz_g5QLj<n0kczE7$0j~W?Qq^N7|r&lidNkQ(G9BH7~R2Eb);xm}r!H z!0pr)MrBW##;G9{%xOW#cyF9E%9FqJ-hEooG2R(oGtCmTm5ipTH5`a?O3+KavoB@0 zzf!a2<FbyocAY-UdTy(2`>kR$%P6n==DW!!az0fZ$UC)z@v!D&Hl5`?(gt&l@*H!n zd#2c=-g>n;R(;~njuUmKb~G{t9ODgXFv?rNGNq8&bU}}_22<)C2hkM|*iJdHng_6J z?PzogILI5)#Fe)|WXY)oVjU|aS{6uktdMD0AlI=%p<{(o#|o8>6>1$TG&)vjb*#|o zSfSUk!k}e=QO63CmIY=VD=b<TSaqzhY0-{6&mE%R5~Ao5qT~{y>=L5l5~Au7qUI8! z?h>Nm5~Ar6qU98zU9@XKM9YGxjukO23*tIfB(y9@>R6G|u_DbSEx^Pj#MC9k>`49c zT|W<-H-B7i`{;00R@JMF;8g<8C;or?xc>2ym91{YX;G}l>t|iKF#GX-fw<L2>hGQT zzjOO_nddfn9Ut#cR!{F(UA9Vg+VAb$^4HEiShw$Q{T+GzbC1`_N*w-cc~&d#{*Cs> zZo1;N@A8j^zTDsY+4QT>>T8)Ne@aGm)Y-RpUS(e2dDT8c@7kL0oe`g(+PnOJU*5g- zn#tp=`BulT&Mcq0diUXDS7%pD3tn5(w)%d)`1OQiPU{k1FW*@`Nv-(%pPdcaGGd2K zKgu36&7G}fW-EWh^t+^r*?kMooR68}8=wAVJ{q?B@6l^7=N>id-+N-^@iW4=yN(|z zW1jcqR(jpp(hT$IuPuIPTwm#BbbaS*vvr@RhVRh3U#C@8Td#Wk?F+e$zrRJs^gqAe z&8U5>*!KVXI>QYM<c~j3>2ef4)-@}z`#^MS_rYB}dP_pH1Fu(bEZ=>sbD{nB2Zzqp zsC~QjW9g0R=W!d)uggpN@ymbbzU-{-zwd4z{yP8r+!g-O=UsiabThNvJMa3fI<V)} z&M=u9Rjn2$^rv;N`^O~y_q1`$nWg`Wn(bpw@LC3(IJ(bj=HlKrr?zCCm}?iiS9-_N zExS($hs~e0?)`@3>)&_oQn9XBUv=cb3KzY)f~I1AoB2i&=PRav-Elm)_>YwEB2&dh zu8NC71B1gh=yWYUuuG$liF>gH*P5P38e2?i7HoZ(EOMKhZEauBwS>wkDP|j6wKE!j zIB2V|crOuV%~oOQc5zv)!g6{Ei*bayQj~_>1+R~d)4KS#gw-rK`iOb0n0&_09}cgN zvPT)%UFhOGIBDe}u4_Jp@)0ISs}ruaB-|8v-&iW#)Eaz;J<P9AKEmo~ZNgiT?~O-; z?|hfIeVI*n{v*2$bzQ$37)8D_a%;R}Rtwm}p0l80J=g7M_DDyLGYfX`-#DnXM*F4h zrk2o%^o!Lg+^aYAE&qN(DlGBsaTmcG&V}*@9gpl11iOD9dTG0<Gc+pta&?O6>MccI z<Zika${X}NvP+OWT6@5x>vu!0$alua8t<5|1>Rvlvv>#pjpJHtU(ejmEVjw(_1`aY zH$4jF4JJLZO;BCE<=Cb36X$|P`CO+3-TAI49iF0nwD!P_dXCh;AFCN1*ZFGQ{qw(L zPg9SHe-2ae?mzR@RCXNsm4D{H=D+*d|Mqu%KK*ybtNPMaEDe@Br^qZ0-Liw>+VLem z``?Cdum5m;|5baLzF(PBmFA~^-oN<MC+<%+8(B}h{aIi9|M`Q2xX%CDs{i-<L@5=y zew=?`#;v*6r_TN_aqREc{G(ePcGWRjxiZ+C`tMR4x6M6_@ow4Q{D971{cYDZ{(M*Z ze|LLIcadChhV24BljkSvIy`&XR!*|ZQRw}-vODXIcKDJ)^}nHyb-#uRZ>wKC%i{kg zt;(Z@Qzz5}rOx?1b;_mxp;wo1x-Yq6*1!7Fue_;h*DE5u@>M-w#@nc+#u$6gWKZ`B zKBwjtIs2IB&ELyc6^VVHT4b)W;*-%Wk7cfkt5zO99Qy2}Z^%!_wV|xc^}(Xoo`+m! zlJ>kTGk;~;mA|gD67R2`_4BU#C7su7SKlP(dlbvnN6*^*RWtM6nbJ#t+MjvPS?@n} zckM2{3h%A{SEaXdRHXh|-l#wC>5sg-6E<GiSk%9+;0(wAg}lEnsBs+B)A-H1&GBab z27a%A#@>KcjBi>t-A~Y-@|Uf8;T!WCJgNH+=7E$rN9Jz;wfjz0HOne4(ha;O{f1L8 zw@ctn*QWak(Nq4i{a*CO{Dws8{)6X0%H3|}Z!kYq*L*hcoAjH$P4^S3K}r_CF~6aZ zy7wTT;6*XdqF2Q&8IxNQrgvn_Zb_Iw<u9A`(l_QeG*kB<4AcD0`_1cS{)Wg?b<KB! zzTM{D)@?WE{qe)Am&G?MbKkQq-NWC`V1;{)fWNy}$@|j(^9_vicU|QEw|~ozc;O{? z{s(J5{_m}?Qs_F-YTcXk9R2@)=D)M~dHR0T$JoIC>ObW}XH1?REns@dM%Vavek^m< z?|=5Qg#N~xdi{xy{1Jb3?*I4i=cvu&7Hjc-7+P@b-Q#&P)y}bfO#Qd_omaztc|rT; zDG$x&Gv5+8@|~L_D35VR_P^uO%b!;-Ie#nn=lYA&BL1(u^Z#+^zv*Sm`Of~g56(WK zD8D_q{JF@|*B`#wg+ExoSo-&=f3NSRGT65*n^pcVP(rok3pYn#%Yr@s?px>@-<SSa zUvT2=_y6qdtu0!A)~o#7FZJ%{e(%*^?wr3kf5pF^I=&N?{PMqVy#KZEeBQHm*MCXQ z_GfohZE){8c{Hr-jD_^8M>@Ys=5MO0+bGFAhkfmd*5ZYtiUoE)2JaST_dj>txgz~0 z*U3-w<X3rR?DyK9yI%c=eC8Sd*)L`PO<(c<FptB-{o+60KmT*cz4W=^$N%TAxgYo+ z9(>&HPhawj&8x2*R_gci{8#G|^OxyP(ck)QhhGVYEeX45s2m}|^4?Lesn9OfinW~c zKyc!e^~=;w9o95FwLZxBlzi&T$Gfd<=OnUx)s8a!{Jx6kobOqaXKYnpj8E15U+*R@ zvg*$N=*GwL`EzE52TXXj^W%T-zt^+8-~3-5{L4OE>~Fd3<zMf`_x=9S|2E$~(SC!@ zuPfUd=kMzm6y->@j^aNlwRg+=rcJjpxx?FQ+spQJc|2jWh<8()_WASOqw?pO1<$hz zo@f8~-a@_p$Fu)l+isrxe`3!Zo6XD1BDIeHi<f%yUoJ%O(UKSTaY=L7o*cS7$5MB_ zP)~h^!LRz%KiU)iPyY1DQvYN8V%>}XT~BNOjL-j7FMn>W+-n;)b1S{($4bh=J3ob2 zJ@0w5g|Ghh4dWlOKWd+J-F(k-o|~P~YU#q@qo3^$&zTuNA^D5fzwpJH8~-V5gzWsk zSm=}eQI<dcyQKd;H?R9v{3myE)Gnh9NB*mb|DP=MfA7v~Ubg?w`<<OL?b%F$2j5=X zdp)VIjGOttdNOEP-I71=w>MUYCE1$2;{GGJ$4Xn{#G{HoQlWiv6OLZmwKy*F@|3er zzOQ-m?#<#q4+Av++?)H~d|7o4Q$XL}`%TTwBLBjL{)ylIpL=poeAq|v^HH5MC(eI- zzHxHs%}YEf&mJU}-)0fBZ(8?aZf%dfjMn$IyQ=?_FQ0kYnt3yG=e-B@ny>damz?^Q zpa1FqwAcIX&6e%ByRvgmO6cVYM*q#qPyPRz9~xE`>hNd&#Ah4N@38&vedT{s)07|k zgFe+4zB<}^BIK?759ORQefKO5&fKr9|NsB5?Y|B;ZW3;B|7owZ*1-I7@a&xF&Ht_D z3jGiF`loMwsoTc%_SU0EuLv@~@B3f>H~Q~Ghll&y|JE<G+^n$qg7(k)+>DdI?YDCM z*FODc|E$kD_|E=+{(EiCtM6;~z05Q||IMrV#yQLPZ2Ui)T;EriTNLb2Q+?&8_P{&I z(r#UZAA_A(|Hs9g4<Dbb$<L_`U3U3HyR&QH2f@Gzxo31w%z1KBbivbz|0}sU!>-kb zz4*az_)P7%bb6H%bI7x=_EK@eOOF1%|MFbRfgOA9e3_Z)R@|k2&QG%5>|b+F=CeH} z{U86Y|Ms7Qk54o5fAFjS_rCmp_dQ<JqW|Q7`x%?{RtElFzw{XIc@N*3elK6W{i>$6 z`+c*@4k?-5-|UmM@w0E}Y2E11wP9-mx5jC$ymjVhzt`N{u&uK8o>%qs0<YTDELi2t zKX+B~>2s_6w$5H<_1o2JZavquT`?~-N^NgOyxO*Y*Ol7e>x24B<GmQQ?WR@f+32u} zy_xdk%5&B0bsui+%U{vtZnKIj^Xn9^<ueyvik-clD|@38mvwf?*Iib=ul}YjS#|!U zie`=TrYqlj?*;956%6{X9_{t(<lChkt$Rbf{%fWG*P5@jPxSAey1(0fUCqM6pB}Da zyI7O|#o^DQzO8Gr10s7a+eW!4A5At1aWM&VF^O<8i0V>0SRi<@LhxdP;KdHXixUJd z&Jet~K=9%U!HXLNFYXY$ctG&t3BijO1TWqYy!b%y;tS4$9|SM{5S+-+;lbph!lX2b zQINB-V~K-H(u8jemIC?ruFG{cb)-1D98yrq<Y=@odC2xjfT>YnqDzN|ii?V{k`R}m zXKTk1Czm2crGqC{GMp5c+Lr8f*zibw%Z(q0%Pao)bv{e^|E%&+?+^Wj3YKi#2jX7- zk6rXXeZ!{z(NE9(*W16trtS02y1)Gf;u|OUT>Mx*{r}(B_h0d|pXw^`|2)5}zSr-x z@$z&j?HT`P&AwjWma*0A@AcV#^h^JmzYAn_j5>68&Hr;TC1=}w?6VJl|L|MlvT%%# zzsD5~pYMJD&7Y-JFa6){w)*PS75_E<{@-(|{=2LpyJi3D#sB~3hZOyomzbPeUAphj zz5@lf|GA0(VD>%!f<NZ_M!7#*`~O|o`T1J8-_}d=b=mX3o>+bTt9$AHhqJH#55E8A zh+p<+`ERSDbNrr0@7wNp_f?}~&1>JsYjT&p`10y#?b_USldIcLXC&v||5{$#E+L<N zd{+9GxlK`@=kBZVzQ5G&c)8`jjcZpwHr{u;+dB8|+v=Zkr^9bEt&#pQFYTh3hVHYU zf|}bF%zV9Omfh#Cw+y@29MtXkH6c#&+Jw~;e_cp(UbAqy^{c~muM^*Xbt|oX-@ohT zZ=Gs)zIy9h7s_9W?YwUs%lr4ZoY0r=*Nj}g)cHvy{JAD@^Uwd56&>Zgf%~6xHs9Kp zAo1m6f#R|MD{RyJ`)q!!&i$sC-F|Z8d!5b4bvY*0cKKRmoSwJoeCVp!{dF1oSxS5E z-4OoJ(7XEaHsb`@s<j8}ytGpuWC!Fh?NgbSp0POAIofB@`}O;U1>2<r+oc8DWdz%0 z1>5K4?QMUk&LZsKV(j1&?BG)D;4<04WwV3J<ppoJXRx0>wBfA(MIQ}r=4DQi5(eU@ z4mF$%5K9xj{Y22WeUm}=O|||Erzx!`6K2~i(hL-1JJY$zAR%Z<E8}J*mdlIYaL*7| zoFS2#c;F1k1h+^D19OlGAccLK3=*oQv@(8HW@QjwqQf&oF*Wgkh$c7lGml6K1J6^3 z8X|?7GL;rp3N>{GiLsrTvdJJ})s$Ao;-xw~GjviD59DZaGcWUwlrV@tb*O<eSd8t= zj3u%@GdCF|oSV|hIC+^4&kR$=8D<})&&(F^kr6yQN5E(9CWC}`Aj#!AJTq)l6AygZ z>6P{S-w)xXi3O5>{x_Hu`z5Y2|5cyuY`XNz|DZ?p`M>JFvr2w1%KKiQd;jvQB;K#y zPQF(cv(>*7Jb2&maDewby(|ARTKD$l_5^(2n)$8jyx6H#$0xt}I=yF_^qDug`7>^M z?@Rabu3mgl_sOxu`dhP0cB+}@UO#kU%`x?&;`!Na&+PvwZu#*=*!lGLbDV3--kWWy zJE*p$vTChk?2pBFzS)15ywzJ1pJ)Fxv$}mk_>(?ou^ZN=QJUrNrlr0A^Zc|^`2URR z>&5+?clW)@djId5&%)!Kj%^KBAGgfYFEwO2_geeI|F7mOOZz99KFjC1^}?>`Lq~Gk zgmY_C<{r!9SX&m$mtA;7uIyv*&g)m#e!KVk`-xgV?{6P+H(mJ7yLs>Z!};8Q=lg7V z{rMWFAop@9HlAn|mt|@ai<pdC8Uq#E8mG^nu$Z~+_G14t6WOm{yv4rXBv9^Cu5$I8 zRX3gPuuD1rnZtSLLijsF9_jn>1~#8ICstHMC%Qb;bawKNS1f$;y6wnn@s>cv)jW&! zew^@JTYh8mw|Uc#|M}Tc`({_|rya$`Zyt;1O;O)tdeP3Xv~$j$sC@B9<{Za<t4jni ziE&T9^jkXUwU6MXMYp9c?dui3Wc^#}QvWv*qt&qnDR%3Hmc`g4oT%E;KIiYd%Ib4l zb``zbR{L$wM82C}KDhn9GU4$i%luB;{TIu2CPYlz`{4ZdmdwO%-pl9qs+`HNI%@EE zt$WMN{l_HJ*3Q#AV|VqkLHa$3#CLpC6Ym|dJM`yxok>CY{?a3{c8VG=V{87WeBlt> z@is9e$x`RG?H@<(<rbW4WCV5R#&AVgD@NETMcFDv*(pWYD;Y5eMmBW#IJlT8C@Bj} z6zw>{qZP(zy&{5@XQ7hlhL*0710r1g4Z0$7jL8u*zOdKI8MU{1K7V0r)Nx29L9o^H zfC|^-244|1#>E;wOksgDSk5dm;Jo3QC}hxcNI5}LbY(+k%K<a4{)WPq16Ev<8!AQA z7=LT{FtG*AV9{A(z&XPsQE0=&79k1GM4=6nL|5-y`2B=m*cvOh_Y;?{iAY`g{X|&U z8Y`#y?Q2qxu3Gy=?&j&PP@%o<Vw;nu&b+5QJ94Ae#CyuCBR6Jg{jEze*A7$N@PQ-3 z@2FOSmEwlENoff-iW}w~Qckc_+%W%;N&o}bWCvCaA12iWNunzlMOQlru83e=<#1Fh zfK4l`(JEjgmq=4r=mMdX;|oMu7Knu`VLGb7a&>`{s0RDPd!N6^iMDiwE>H?t!=$<} zNpuCL=xT>RkO59dwF1Pn!Wwsh3~1{LU0@WlhG{8C39smChe8g=(iI-8r<^%nIlF2F zC^!WuYK1il1#aZJ($N*Vz$0W0Q|O{3Q4L|y)edu4M6hzX9@Pra)Cy}10x9Y03I&gd zg)Cuus$_HgM;Djd53$GET9Trx!;JSWS`@f3OYX6@mbB<<hr25xzV-)N)K^IJ<Znz^ z`ts!W4t}mzk|D>6uJ-=W7us!Ksq(k@_78oPEiV7lQ~$HSKURMgG@|~_Ca>Y+{kBz) zW#7C1S(GQS{_(qu|MWDo8~@p6H`;6}{~B+<KK{R>)6CxoAI4k^p8V=d>7D+Sjy^{& zB&k+k@4L1=Mmb~^<L>s#j1yZPa!44rCV$M?9B7iob$rTJ=To;7{UWVHBCqlNRX)1% z(|XNag8z?72VXWid+)^mmnBlMC;!?{`PYB1=YN*<{tt!MD(~<;-v3lQT-{dEI=bf5 z`N@}7g@zwp&ZjFJeqQ{j>Fn=?)1HWYe92xfI=^#=%DS_2wby0eZ;G~m!<x7GzG>m- zsmpgf=ihrM_wI^^+alFEuKj-hFfPKVyZYjhZvC0^qT8jVI@V_$>#}}TtMG0|okPvr z-?ER+HUHjeUcTql-mPlxceZ{iJooKLY2Npqn|E$Y-n=t!3Fn=A|9FqqCjQ;|cklIL z_DON#{54<JR-9PAzT!puyhqmaeV_8}&w0XE|GDXLhsCkNCug|tTy$63xw!h+jJ}y` z`;ve6b-foWdvfn?(2EWI5$`svzpu8>e3RApdzUWuJ)XsM{NwLwo$ANtJh&&nXS(&x z*TVmr*OmP{oVNRCuxb1g|G(R&{eN*<^v}%emws;6*V{g?I<4{R<fp7*Kku2mUj8ib z+WDYGYoEKfMB2+=xLW0S`A*fwSl!o4#TNb%-@5BVd`#}K`k!0v|6e^G{&W5PpKbr2 zeck+L`|SRY_iZ<J|NqCR{eH$*lmCaOJpI!@`QDZ|zZ+S5g?1kMH92h4zdQfzuf<Ev zTD$+3-==?6mKUR*pA?I^%bs2QRjjRRRg+%Q-z#ZJ%}1669X_!->1E;1*<z}C|DPXy z8WF!cxjpplvh~(g*Ve=xe)}?hHPig->#cV!Xl@NJ+7R)IIX!y&55pZd+rHfDd9tDu zbSjF~L&u8g=`JBmdoOwKxvbb<cF*lyGyl2cxsjK3)A`?hDnF-M|88@8XRWl_#@6oD zoPk1|j$(p=Qi6eUf`Lkcfog(*T7rRkf`LYYfo6h%R)T?cf`Lw)j&6d1UV?#sf`LJT zfnh6c?L4%VPH8K7>8NgZOcM;u5)91abSx4KEE5c@5)7;p3~Uk%Y!eLZ5)AAU44jm? zmf?iaT2Y3~Wm}mP&Tk20P?>d&qrpeGDde&g<AiLd2{}>P44ZtjSsiw9I_}Fn%iv-0 zP_oQ6;1s9hIl;h7oQ~H718;FU-V+Rb#Oe4<Fz^+p<2%8?Pn_?QAGcoe-|Rf0-lc@G z;{}_*MNx%C)(R}s8#?kB1!I|&cCp_&Clte?w2RZFM6~0DUdDdw0?CdSW&#&W6&C$= za1nR99oH!B=o0S4@s33>mP=`us7r}%$BVcXGnIBoxRe-oyvP!`cvoT3X-Ajs%^mC5 z1@H1J?NYdv$9+dYX_t~qiF?a~Nh>&*@;bW|_>F!os9vPRu|q?vsbMyVp*5?tdAHW> zHNSnkjb4YP-95Etw^G_&uQv}*-dG%@!k%~LzjE5snR_q)k6h0E%==2c)0E%!do2I2 zfBdKD?EjBDL}&lE=l=X(_0r_}dE3`ity=cw%0kmiuYU4-W}D3qh@9km>9Y4~&+{&S zRMwuY4T!qFZ*|t+4Y8}vUd{5Hz20Vd)xQgISFiO}T{ZKbma=#M+*hrt-J$xketGK8 zyzY6MH|u4nZ`+o|_nu$sd#%3o`o8=6Z*6|;dv#X!cS!!)nYwSIb_S+aZ|ckX-lMu| zyK&meZz`!PzkSQse#_k*{`URfNv|9(IlesL9=_DsZsVne@`sj|+s$5Y^`~gws}JU8 z?>c52l(pRKUHWC>!&Qr`FC9&O7JFx7?eeAN+*?<bZ`XP9MtR<|H?QwcxhH+o<LkXk zC+`%VWqS9cHhYP1vR&w2`yV@ArL52YDqmu|{QriVyMG0n=Kc!azjwj^7pK#|q{nGr znO}P`$@=>lquc7I-)}wn{n>3h)8)79yd^WO?UN?F{mi&<8}}@|ZQXBY-PGGEvEu)t zX@38YUOo3qe}CSp|F24X|6YIpXWjp2U)O)VpJ%uD|GUn`zuwDNc)y>q)%X8lk?w!T zJ==4q3zywm=VF)t+jIA=Z^ggcbN8Ew<nI6FxAkw8<<YR`C)cgo#cvjVHQFK|Nc6>$ zUn}P>W$T=)!@D@k>pRQ!4JKLtXD=;0yk5t*#y@&ij9pS@)xQl}Z+5Po_N@9^MARFb zHF-Nzm+!i|>iN~R)7OP<pZR%1)Lh+FxBKR=$`hZyC{KRgf_E2`z3y&I?bsFk*6-E1 zy2HA2>!lK27CC0WtlHGP^!7eCvDAP6-_Q9!z2?{TRc}|SpGlW_(X3x`(3Q{L;==oR z(|7KAcl?RA^sefsO7>?>i~rkIJ(7q~F)WJ!l?^qX>M?JQsm-?zI8dy|sxyx%Lvf?X zfz9Vc4hVHFWjGhD!OHOJL=Z!^$~2}5k0i|oVXbLQ1{bSCe5Nt|@J!NdU{{^S^kP+# zW<zYtibXoCALeu|W%#x#g3Dojl4gTW=Te4SD<ilPHg+v#_~og?y1_3=lVR(TAO=Yx zZ^oK)AFRA%7kt_za^SbtG^QKNlQbJ{3VAc;Ecw3Tm=5a(m!m-pn?rP1CoE0UY}h8` z&3I&W1lI#qQEx_>DG^)`Rvigq*vR9!HIge~U)NHGPZCW{n?)Sh4jaFzS}<#~h{LWE zK@7VsrZHLQZWL(<I~v5mZ8?ppVwR$Ytv91d&_<C1DSb;B#0>WI_lmEw_hys{-zaim zd(|{1i{<H>4dq{TSQC;t;u@DS%)7W*#9`sQr3^Y|)0iUCk~JHub9GoJ<R@!3gh+cc z_H5ZGazH`ao3Z6q1lNM@-lYt)Zbon=ESR^Hp@iq);te7W!A@UvbXY&^p0|`?-?hym z2PAF18D*|+7C8`IHI3=UvUE*`?s-cY_-;pVB`oh<$}n&HMv((q(%y_VImwy~7mo!o z1WU%HZMR>Lo}}6EOlumGgvLgZ2EQcDhI2yRj3tX!+EnRyF*aOJ(rma7vS(`RQU*U` z7X4#E4DD-mST|ff6vU9Nqr+MdaW;s-`sQ1u2X`a561L7-${-iES)^e_nr6ewtZ7Uh zfty7fnr1C!Sad~)RbWw?W`keWG$x6>M9l_9v1v>fq9V8!G`B8gkcyll(lCiBAc(>1 zM_}8B!}9-A1^%js%QcigWn{A5|NKYq&;K1;W=010TsGRQRr4?Y*1z>O@=d?%{k)nt zdxt*zFKY0wxo2M1|L}Pq`5&(QpUi5a9&+sd)_=FBOtaBCJ7-qd!^i7+bvOQVKd$|H z|K6GZi}N==Kfd#8eT-54k4wt^Yl7tUXDnWI^@+IcJ<W&Qll!&TmC5Nn@9ch-QFuq= zkC%teb#<|PM%x<Av;QAl&YZ`V-e9kNKk~=n@}?=jn|F4ce0Ik3m_vV^o8>S2?1}%q z{y*oE{(t1moGFLaG#&DK^xt{wL8<cp?+-pJIrZc55BDd}msF;G%>Vgl|AGq&J^y28 z2cJ2kt>F>;<NeN$@<B6_zT8N^llG_C>-PU{{?JGJy@dX6Etd=azdZa}-I>DGvrpJ< zZsHAn_D}71pZE4~(Pt;?YWHspzsz0q)hx@I_bbCrwdo1ppIe>yH~(J#3f1t*+3(lp zuGsvVd*{1f|Grh7|HZk=^=I3!ZhcPPOXfGE(`1C+NnW~pPve>8!ou$<&Xe{2tDCId zd*|L7w|N)d-2U74%-%+0`-bCEQTwJVZ?8(6XX_)g;#pob_gekqTg5aDPe1lk(7jr5 z+Rc1k@x4tG8}vKx9OyVw6wCen#FTZMb`N$11hKE<Rf>~$+2i)5`+-$p1^0C>!S&)w zamFrt@;WNsYPhw&cIUW0K`C}eUq^+m=4HO?la1aM_<|TyjoudIf*8|{-WK#~UY>VQ zisj+fWj@tB*#a%s=NP?-jc8Wb^KHt#RUiq8fMv4B8jao>h)v0??>FT*WVq00OZ%~- z^7mLY&eqth{Qpt&m`L)v>7VQmIxaMSUGLFx>HqRgFX~O77y72W-P?Tcti`6%l@%qW z^IqO?-uY<v=eF4q{00};ZC82)|G92{;%r$_k47}pf?cnb@1~lim$8Uyd=$|TWLWOV znqVQS_2<ybfP><Go!XBre?H#b@c-bo#d`mw5ANZ%eE(tpmLKz<)@wV=neszkf1~30 zm<i$rCizkG#E<Py`6IuEGwe}){;U7GzxUhQ{@KL1+wu3M_=xbQcYG8dz5BKQ|JVGl z?ad6Tk*_`f@t-w$R<Z17@y{>zF3Ytq)%SNzUiAOW|8(i#Pg)m$WIIp&xc<P$er=Oy z%fc4@$>+HDe_>Dk(cb#BF3a4gw6e0Or4s)yB;<A&{S&qLw{hmqpOVLFl2sqpX9%qC zsdw$G4~bKBVVLUqzj+GC9Qg#ZbF)&vOkmbv|6dxF`bWFuPw&o`tuwj)|1W?1zb1Lw z_AmRV{IbvET=(R^R^q?+_Z)w(OIWt<YtWyGtEUN7PXD=o|G%~W3IrD9a6gayw_N1e z5{b{oMtcwZ5Bd`S-0uJ96&9Mm&OfVvdF;)p$N!z3ZvOI5{iB`nC%GqWvE^E$-uew- zXI?xu#in>(d)Aw$7wUai_(}fnn)6@OkMV!}i>d$4FNLVG{#UX1s88|Ihx@}8|M=e} z^Z&=5lH{zkI=4hs$^QXyJO3|sdR(8}cQTo^q3EN1h~$4Rng1&^XKGfqvd)~E`p0>T zRO%n?qCdUQo^1K^gyjN5a^Lsk_9hY1YkKNg`|4fl`ZfmL{lAhs@EB;tl<1=UPdH*S z;w6vSPdWa#e#*!78$S9QJX2MF_B3SL&-fK>PybK({a-udPrIm8BGZiU8S}y;IFH3M z+w%t=+s|~oJ|k|SVRjJPiJYeo_UpMwf}CsNpILADcz?p$OaGTD9j~8Ysk>GozbY^F z<A0t1i+k#ip8dbM=b6`${{ph>!7fUPTW5IMX7i?=`iu!e|HEJW;5T^Yr+H?b%^&{M z*Y#6DE;?_R7P{!Sz4O+tf8i_sEf*E5Si^WH_n7^R>(e6suRZvC|J1Md=hfdyI$Fm2 zT-@%Nb-#4?hfTkloi5K>Ufg=N?*44Ec&{>>`i#ZNuaAA}%bk{}_WEyKpZK&C%Q%y* z#(RIfH2Rq3zWLv)+xKT@rRa)ezd2uc<<}y!@Y@n^R}`<<AMt&myzf^(9`Vq**JsVz zzpF9$+ro8Eeg}kp{J8eiyu7n_{;UdJKjZn=*Ws~y7p}UruiG?c{kr!L*nD-b+I_uQ zIH&kN>#Y{UB89q*dH#934ek}aRxf`n9((M4-R?s+ceCp+O!ISKyLE|^<6v3lzj^nR z4R46~O3UVSFZ;%~&GoW@@a;t>&mY{DajIy8?yOrEzfUyj@Q`s*;GZiX&>`T_(&2K5 zeG;c2XKTk2Czm8erJVv36Mh7g9S+@g$T4KwA<gq)7XRH?zqHS1VR049b*fH&ExGjm zv;Rj9U;V$=z})zL@chEv|ISDKd-m6U%as42kLuSS{eN{%yojg;=p-ehg9bVE?~i_J zTyphL<K?$c%I&<T@Y~OSX;GvZR{G_p+NF!zrHkGBBSMeQ<9v88R&LEEeM|lQ>3M6c z?6zzScz!^=Z<qJB8Lx8vN>^~rntf~S+qW6=_l{m%=x*$OeA>T0zlYU6bN^b+W)rzm z=+f4GXKhm6{8gbft9QIr3_JS$nfmdoORPOtvlL$WIJr}m>k047HH}P}9p$f*RNh_; z-C}xOHfGwlb*CT6N%l!z`M{+Vr?&0o)1;ZXRXb-Xx$H^FnS9#CX-~<O(;|BEUyWM5 zJvgRM6wp)sYSwz%bFc5iNte{76+~u!_S&&!izHV}To=njp<oq`=`)oacU%$SJoI$g z#uo2$N_+J9#2%U!Z{RpTU!c9gwPG%3p@s$%*Ybu<If4PKJ;I;kyH@Xt{@Z`nU>5f$ zuHrol|1Z7wU;I_s|Ml+X>H(kXOFBOr_3Yf4`BSs?*_TrR|1Y+%Y5%R)F8K4f=Vj%+ zuK!-K{{@$Psb6{Gqx{^Nvu915KJ97m$LpUIRlogz{PCZ@>c8(%f8y`{biJwgyik~j z@$T>c<*T*@@>RUj-kF}}9s1yozNOtW|ImP4^H;o7TD_%Nuk+y7{}Y_7WQCmTEYw@W zs{MFs&#Se@{;}V9o%_7U-tW&lYp3Z>Ika8(=`X*zI$x_TroXl?YxJ6*{y?c}`ARmP z%LNn9E)ETB{9X{azdV$8)o)|-7muS>y@;3YFs!!nZnoc}#1|PI+*SLP+3?-H^-F&o zFY9m)?(;gl%wAE7EoX8Y_xd8`Te_2lzVsJ&Zr!%ZZB=@)^8RTdiyxksti7?uYtrxH z<ayp9y<4|FJ!ZQixVL`WdJDzXE3+O=GD|8-+Mts1tK-;%V?kbLDhq=S$1lC2_xs3| z)AHVvU)LY=wc8l%^*>R6`o_=7q0g$NcWAD@7m&Jl!;I7OK2Om~zbT?u{--AN-W7{; zB6%Ucva=OuMZ2x~RLw8Q{b$nBL(goF9O4$dwB`QmuFK!IPEV_OB9OlB`{cB}WifO1 zewbwPeCGR|J9mbLetuWmKU=;&@M*JRiK+d0o5VPWpUT^8o3ho#=FcjLnSSU!n@HF1 zi*1}Y`mGOcbv&K%{jKo38`IQw<*fX1jBlfH-qwxGzFr5;Y1Hy;b9<S*!Q4x}`K;jA z1Mffr?k|%!*n7!0^9KErcr)SB@r3S{g#Hfj3W+IpY`2$uF}$I^wEv)+W-U*d*UQJb zCXtHOOTT<PoU>QvmhRI2&pbZC2kSCFne@c+Zb_bH)2J)lWPAAuXU;5vH?!3I6K;28 z+&AfDl3xDB@P_5m{)5*vYk9u;y-eOv=_TKMH~5!C&iqTq6aG)JW8(|>B~jt<GWh{h zukeS)OUDmz9$Ft6nP3t)SGg|WmxP7W%g4_nA{ll%y?o3b8OdPj%rx=nDW(&$Uh?Xj zHZc@+TsnSmjZQ<%qA!KcVPXz`f^li7j8DWKa)yaH^acK^P}k;GxUNyVr)w(fgyJc7 z^A4Y4I$`T2-=Ch!_(Wl8|KZ3;hMgWSA5UAO(=cbrmyPn;+zRHL`WrSe6p08{dc913 zptZFB`sPgxMbj=F=f1f_VyE}Z<Oljo`x_5Wv15}f{;#66x$XClX1R}htJo65EF>If zrvHzA{{N#}aH6AX)c?bu7kd3~=aTu~c=M#-u`^EV*ZjZsqD1xD{}wjQkNeFZ*MC2| z<5)r7Z~J#Wg(tS&+w8SH>a?(MVPH??;<=d~zn|Ti@ZUPK%Qji~v098lV2#|KD~35! zk0!17*mhLlOUtbZ`z{*QoZCC?;c~ucw;yY_^_)1jU8UvRCY7ArY)=bgkGT@&my9-@ ztT=tba#Pil;!xeiRV`6&e=nUkvU*r|`o;g8Ky_2E5cS#>+j;`yH}-gXZRq*wxm@md zmgwml@nI{Emu`tXHaBOLTz=+axq6*TM*sSGzP7yGzgoS^?p3FMl&&!U+z|Ek7K_Cu zonOsbJSl3!nT-<9|7Cg3yys|}dFc4GD=Si8ZP{n7J2|=~@>2BLZA<<utTtZPIqgjI z_vmGFd^e`#p4yV~e*XG%kuupQ?<RyiPi2Yl=~lWL+|D03TfIEwIs4ZDah_G{^wz|O z+v;xEn>y?8!g+z`&l?@Ci?RK@+k0B%wYRTs9_NdEW|O!5OssvVZT_?FX+NJ8XFWfb zTL1s^udk=Yc(xb1mux>(D0|(_Iz(O0)?j1Iw1;wM7q>nzjN(j`6kol7Rj^4k@C)}e z7mjN#fd&yGt_KrBk1k5spx4!O@Q?-<o3=8Gw=zri;uR7)eFAIx7A0@6aoS+lHR&J| z=Rq#cgF-K>8cc(>@Q8VGgn4pI>+HDZ$sy(yXcD2R7@_5QIbm%}!p4%12eLR17I7Y| z(mch+t;V9gEJIpnroft+i}E&PIBm#s+K|(A=-@6*E_QBp7HxGF@8v6G*34a0w4uUj zLzUBpnyyO+e`$KLvjt4y(^>FBW=2Ct&Vm;*5{?%uwae~KkQ7<UY^~vCf8t+W3a9JK z10r3Q8g)gMGAC<zv0n?A!gpq&hU^XJKr4feMI{M>t}hR$bX{unRXK7%qw8E_Xv+be zu5*pCEe8y`E;Xi#EM>l|;l;i!a0*|~Vhz~{_dqLy{zYHBj@s2%9Pa=2XBW@)U-8NN z3{`K>us#{_@n5|35?;-X|7VA-{9k?f|8L9v9t?AiKlv{&`|tLuzt`Uj@~h+;%Kmr} z{qe_TspBsvl-_yy-pcyamV4VX=Q($ZzB3Bb*tz{vZh5@k>(9Tw#@nmsul<x5w}07c zv;N~b6K4kRQesz|c6q~{!|WyoQI?Eu$HM$_7H-{`k+bCzBM+ZoZ=V3q`Ni332Lq;V zaN+dcBQ-4~ZBcm0l0`N)GyZ#;q|f?fbCqG=o1gm=Vg;W4e`FM#CS_a0IOUK0>;JQ| z&&{6pa_-;zTXroF`ubl%_1}7(pZ4+3H*!6e^e+0gYSpp%`#&GFeCt0y`3&3o(1)cv zKhIlv>xK5ChuV2AO5(Ed%UUMAj(933*f)1;qmt8~j9=~_KE_zu-Iu?op}OYMPx~t~ zGS7LhH2JmuGMn~ad+nk>>?zNlOLJSk@7W)Ac>10H-aG%_p8e}RcoR<Vul;`>1?y%n zlP<J9{Pnf{60xTyg{$74N)ua|7g@J=_p`^xFC2M%&faU{_uUWbk1X*_%l}%>!NsX{ zu)a5J^@shFKFlwdyYBdJ{WJCacxjvHnCeIThkv=<-tkqR{g(NkKQniHyy)5HyIiXH zykGs5eLugd?|go2+lKtgPcweh%uj4vTfDm9cGSJ>?S4FhYp=%{?paYETNC&$`Nq9( z)_K3*#r)RZmwPmPUgfb5ubwOKl;!rj6@UA`;rCxpHhwcVyDC`jW}i|~?8)*yZ2H}A zvB6v}(P!uL?0#G)TwZT{dc*Q`hV9Q^eEwGZm8J0hHGkgUvf4YCFWG);R-aYaey0ES z{hM;?a()(B7N0s!$nrjX6Eibd>3V;_fy~mSE&-*Bl7&vKC@|Msu=~)%R+l5)o*d#L zg8EvDF%d3%c63xU3Vu{le$=`wj=5l?;KwHAN3P57F&8{s`JExA=9Mi2v%_BY1Objj zg%!^kc-RzqIF$If6nS`*`1llg1eExMl=wuH_{5a>B$W81rZy%%5Ged0P{<%yC?Hs< zAXsR?ndl%`7{HmBAXr$ynb@GI&(rMlGeIK5MJC5ZreN1*KDN2<)-*RRKf1)=$Cepa zg4sTt|7LCes$SrKwda5D)BiI{Zu~D!_~jGaxahy=!~elX!OJCzwNL+O|711y(*M;; z&;J|zvXtAouiWTAH=owVfBG}R!Q&I5rTzb>#ft<>`^?MAy76*OgKnV3Iz8VjuU5sB z7u=7y6trdC9v%6h8ZnKe-WiAaU34zDZ*qNes^!DEFZR~9pVz;=-Tc`;H>30Cd_9c_ z@bbEQOl#+U-eLJkUOL)F()RGjC+)UAtIPC^ZBx4o*Tj8Vp4q!}_F<(p<u<z4%4{xg zZcKi=#^U&{3I8pwy<L%ZtA6jSmkyU+KWgmHj#{C9f?4KxRa1$<Uis*|B@b_KzWY?M zdhw~AyL|UgH1}D(uRC$I*6vhb{;>$ZsfKP>6QY)^eE5X%k?qOiklr@c&HIF0{ma6A zuV#I?`>G;fZ<cA_>LND(xnHMhMCCoN+IvxLrQz?DtB<!tckZ5&CR|>z_tx%p(|1ov zm6g%iZO(Qu+k@k}px}B{r8xgta)(TpUMnhyUHOCYx|ra4eWkeMS!Rby-Hv4Ya$KLS zJ3q1G`doo^wHw)#_&0pk%2K%0_|LYFU8#>#sgFC>Q$qSuj)eTK4g)upg9d&b7GXaQ zd{jEwW~h9!t<d?%M&~0RKfhwol69Wa{wYB2lyjv9|I>vwNSu%M-+t7)Yb*<6DBxuP E0Ar9k!~g&Q literal 154021 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�`dg|iv<JA1p})E1M6G& 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 diff --git a/lib/feedparser/api.py b/lib/feedparser/api.py index 12eafd2a8..614bd2d26 100644 --- a/lib/feedparser/api.py +++ b/lib/feedparser/api.py @@ -60,7 +60,6 @@ from .sanitizer import replace_doctype from .sgml import * from .urls import _convert_to_idn, _makeSafeAbsoluteURI from .util import FeedParserDict -from . import USER_AGENT bytes_ = type(b'') unicode_ = type('') diff --git a/lib/feedparser/util.py b/lib/feedparser/util.py index df36b3eb3..f7c02c01e 100644 --- a/lib/feedparser/util.py +++ b/lib/feedparser/util.py @@ -122,23 +122,9 @@ class FeedParserDict(dict): def __setitem__(self, key, value): key = self.keymap.get(key, key) - if key == 'newznab_attr': - if isinstance(value, dict) and value.keys() == ['name', 'value']: - key = value['name'] - value = value['value'] - - if not dict.__contains__(self, 'categories'): - dict.__setitem__(self, 'categories', []) - - if key == 'category': - self['categories'].append(value) - else: - dict.__setitem__(self, key, value) - else: - if isinstance(key, list): - key = key[0] - - return dict.__setitem__(self, key, value) + if isinstance(key, list): + key = key[0] + return dict.__setitem__(self, key, value) def setdefault(self, key, value): if key not in self: diff --git a/lib/github/AuthenticatedUser.py b/lib/github/AuthenticatedUser.py index 27cef266c..ad1523c42 100644 --- a/lib/github/AuthenticatedUser.py +++ b/lib/github/AuthenticatedUser.py @@ -8,7 +8,8 @@ # Copyright 2013 AKFish <akfish@gmail.com> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -45,8 +46,13 @@ import github.Notification class AuthenticatedUser(github.GithubObject.CompletableGithubObject): """ This class represents AuthenticatedUsers as returned for example by http://developer.github.com/v3/todo + + An AuthenticatedUser object can be created by calling ``get_user()`` on a Github object. """ + def __repr__(self): + return self.get__repr__({"login": self._login.value}) + @property def avatar_url(self): """ diff --git a/lib/github/Authorization.py b/lib/github/Authorization.py index b1ff7314d..11b4d9b4d 100644 --- a/lib/github/Authorization.py +++ b/lib/github/Authorization.py @@ -7,7 +7,8 @@ # Copyright 2013 AKFish <akfish@gmail.com> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -34,6 +35,9 @@ class Authorization(github.GithubObject.CompletableGithubObject): This class represents Authorizations as returned for example by http://developer.github.com/v3/todo """ + def __repr__(self): + return self.get__repr__({"scopes": self._scopes.value}) + @property def app(self): """ diff --git a/lib/github/AuthorizationApplication.py b/lib/github/AuthorizationApplication.py index a072798b4..63445fbf2 100644 --- a/lib/github/AuthorizationApplication.py +++ b/lib/github/AuthorizationApplication.py @@ -7,7 +7,8 @@ # Copyright 2013 AKFish <akfish@gmail.com> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -32,6 +33,9 @@ class AuthorizationApplication(github.GithubObject.CompletableGithubObject): This class represents AuthorizationApplications as returned for example by http://developer.github.com/v3/todo """ + def __repr__(self): + return self.get__repr__({"name": self._name.value}) + @property def name(self): """ diff --git a/lib/github/Branch.py b/lib/github/Branch.py index 88ffb7b32..effc5bf69 100644 --- a/lib/github/Branch.py +++ b/lib/github/Branch.py @@ -8,7 +8,8 @@ # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # Copyright 2013 martinqt <m.ki2@laposte.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -35,6 +36,9 @@ class Branch(github.GithubObject.NonCompletableGithubObject): This class represents Branchs. The reference can be found here http://developer.github.com/v3/repos/#list-branches """ + def __repr__(self): + return self.get__repr__({"name": self._name.value}) + @property def commit(self): """ @@ -49,6 +53,27 @@ class Branch(github.GithubObject.NonCompletableGithubObject): """ return self._name.value + @property + def protected(self): + """ + :type: bool + """ + return self._protected.value + + @property + def enforcement_level(self): + """ + :type: string + """ + return self._enforcement_level.value + + @property + def contexts(self): + """ + :type: list of strings + """ + return self._contexts.value + def _initAttributes(self): self._commit = github.GithubObject.NotSet self._name = github.GithubObject.NotSet @@ -58,3 +83,7 @@ class Branch(github.GithubObject.NonCompletableGithubObject): self._commit = self._makeClassAttribute(github.Commit.Commit, attributes["commit"]) if "name" in attributes: # pragma no branch self._name = self._makeStringAttribute(attributes["name"]) + if "protection" in attributes: + self._protected = self._makeBoolAttribute(attributes["protection"]["enabled"]) + self._enforcement_level = self._makeStringAttribute(attributes["protection"]["required_status_checks"]["enforcement_level"]) + self._contexts = self._makeListOfStringsAttribute(attributes["protection"]["required_status_checks"]["contexts"]) diff --git a/lib/github/Commit.py b/lib/github/Commit.py index a53b5e447..b84dfab70 100644 --- a/lib/github/Commit.py +++ b/lib/github/Commit.py @@ -8,7 +8,8 @@ # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # Copyright 2013 martinqt <m.ki2@laposte.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -31,6 +32,7 @@ import github.PaginatedList import github.GitCommit import github.NamedUser import github.CommitStatus +import github.CommitCombinedStatus import github.File import github.CommitStats import github.CommitComment @@ -41,6 +43,9 @@ class Commit(github.GithubObject.CompletableGithubObject): This class represents Commits. The reference can be found here http://developer.github.com/v3/git/commits/ """ + def __repr__(self): + return self.get__repr__({"sha": self._sha.value}) + @property def author(self): """ @@ -203,6 +208,17 @@ class Commit(github.GithubObject.CompletableGithubObject): None ) + def get_combined_status(self): + """ + :calls: `GET /repos/:owner/:repo/commits/:ref/status/ <http://developer.github.com/v3/repos/statuses>`_ + :rtype: :class:`github.CommitCombinedStatus.CommitCombinedStatus` + """ + headers, data = self._requester.requestJsonAndCheck( + "GET", + self.url + "/status" + ) + return github.CommitCombinedStatus.CommitCombinedStatus(self._requester, headers, data, completed=True) + @property def _identity(self): return self.sha diff --git a/lib/github/CommitCombinedStatus.py b/lib/github/CommitCombinedStatus.py new file mode 100644 index 000000000..c2ef2967a --- /dev/null +++ b/lib/github/CommitCombinedStatus.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- + +# ########################## Copyrights and license ############################ +# # +# Copyright 2012 Vincent Jacques <vincent@vincent-jacques.net> # +# Copyright 2012 Zearin <zearin@gonk.net> # +# Copyright 2013 AKFish <akfish@gmail.com> # +# Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # +# # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # +# # +# PyGithub is free software: you can redistribute it and/or modify it under # +# the terms of the GNU Lesser General Public License as published by the Free # +# Software Foundation, either version 3 of the License, or (at your option) # +# any later version. # +# # +# PyGithub is distributed in the hope that it will be useful, but WITHOUT ANY # +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # +# details. # +# # +# You should have received a copy of the GNU Lesser General Public License # +# along with PyGithub. If not, see <http://www.gnu.org/licenses/>. # +# # +# ############################################################################## + +import github.GithubObject + +import github.CommitStatus +import github.Repository + + +class CommitCombinedStatus(github.GithubObject.NonCompletableGithubObject): + """ + This class represents CommitCombinedStatus as returned for example by https://developer.github.com/v3/repos/statuses/ + """ + + def __repr__(self): + return self.get__repr__({"sha": self._sha.value, "state": self._state.value}) + + @property + def state(self): + """ + :type: string + """ + return self._state.value + + @property + def sha(self): + """ + :type: string + """ + return self._sha.value + + @property + def total_count(self): + """ + :type: integer + """ + return self._total_count.value + + @property + def commit_url(self): + """ + :type: string + """ + return self._commit_url.value + + @property + def url(self): + """ + :type: string + """ + return self._url.value + + @property + def repository(self): + """ + :type: :class:`github.Repository.Repository` + """ + return self._repository.value + + @property + def statuses(self): + """ + :type: list of :class:`CommitStatus` + """ + return self._statuses.value + + def _initAttributes(self): + self._state = github.GithubObject.NotSet + self._sha = github.GithubObject.NotSet + self._total_count = github.GithubObject.NotSet + self._commit_url = github.GithubObject.NotSet + self._url = github.GithubObject.NotSet + self._repository = github.GithubObject.NotSet + self._statuses = github.GithubObject.NotSet + + def _useAttributes(self, attributes): + if "state" in attributes: # pragma no branch + self._state = self._makeStringAttribute(attributes["state"]) + if "sha" in attributes: # pragma no branch + self._sha = self._makeStringAttribute(attributes["sha"]) + if "total_count" in attributes: # pragma no branch + self._total_count = self._makeIntAttribute(attributes["total_count"]) + if "commit_url" in attributes: # pragma no branch + self._commit_url = self._makeStringAttribute(attributes["commit_url"]) + if "url" in attributes: # pragma no branch + self._url = self._makeStringAttribute(attributes["url"]) + if "repository" in attributes: # pragma no branch + self._repository = self._makeClassAttribute(github.Repository.Repository, attributes["repository"]) + if "statuses" in attributes: # pragma no branch + self._statuses = self._makeListOfClassesAttribute(github.CommitStatus.CommitStatus, attributes["statuses"]) diff --git a/lib/github/CommitComment.py b/lib/github/CommitComment.py index b5f4577d8..c338100ef 100644 --- a/lib/github/CommitComment.py +++ b/lib/github/CommitComment.py @@ -7,7 +7,8 @@ # Copyright 2013 AKFish <akfish@gmail.com> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -34,6 +35,9 @@ class CommitComment(github.GithubObject.CompletableGithubObject): This class represents CommitComments as returned for example by http://developer.github.com/v3/todo """ + def __repr__(self): + return self.get__repr__({"id": self._id.value, "user": self.user}) + @property def body(self): """ diff --git a/lib/github/CommitStats.py b/lib/github/CommitStats.py index af6ecba36..d8a2a3cef 100644 --- a/lib/github/CommitStats.py +++ b/lib/github/CommitStats.py @@ -7,7 +7,8 @@ # Copyright 2013 AKFish <akfish@gmail.com> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # diff --git a/lib/github/CommitStatus.py b/lib/github/CommitStatus.py index d6074ea85..ff43d695f 100644 --- a/lib/github/CommitStatus.py +++ b/lib/github/CommitStatus.py @@ -7,7 +7,8 @@ # Copyright 2013 AKFish <akfish@gmail.com> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -34,6 +35,13 @@ class CommitStatus(github.GithubObject.NonCompletableGithubObject): This class represents CommitStatuss as returned for example by https://developer.github.com/v3/repos/statuses/ """ + def __repr__(self): + return self.get__repr__({ + "id": self._id.value, + "state": self._state.value, + "context": self._context.value + }) + @property def created_at(self): """ @@ -69,6 +77,13 @@ class CommitStatus(github.GithubObject.NonCompletableGithubObject): """ return self._state.value + @property + def context(self): + """ + :type: string + """ + return self._context.value + @property def target_url(self): """ @@ -96,6 +111,7 @@ class CommitStatus(github.GithubObject.NonCompletableGithubObject): self._description = github.GithubObject.NotSet self._id = github.GithubObject.NotSet self._state = github.GithubObject.NotSet + self._context = github.GithubObject.NotSet self._target_url = github.GithubObject.NotSet self._updated_at = github.GithubObject.NotSet self._url = github.GithubObject.NotSet @@ -111,6 +127,8 @@ class CommitStatus(github.GithubObject.NonCompletableGithubObject): self._id = self._makeIntAttribute(attributes["id"]) if "state" in attributes: # pragma no branch self._state = self._makeStringAttribute(attributes["state"]) + if "context" in attributes: # pragma no branch + self._context = self._makeStringAttribute(attributes["context"]) if "target_url" in attributes: # pragma no branch self._target_url = self._makeStringAttribute(attributes["target_url"]) if "updated_at" in attributes: # pragma no branch diff --git a/lib/github/Comparison.py b/lib/github/Comparison.py index 9111a25ba..bee42c73b 100644 --- a/lib/github/Comparison.py +++ b/lib/github/Comparison.py @@ -7,7 +7,8 @@ # Copyright 2013 AKFish <akfish@gmail.com> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # diff --git a/lib/github/Consts.py b/lib/github/Consts.py index 62a106450..2c1a169a1 100644 --- a/lib/github/Consts.py +++ b/lib/github/Consts.py @@ -5,7 +5,8 @@ # Copyright 2013 AKFish <akfish@gmail.com> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -41,4 +42,4 @@ REQ_IF_MODIFIED_SINCE = "If-Modified-Since" # (Lower Case) # # ############################################################################## RES_ETAG = "etag" -RES_LAST_MODIFED = "last-modified" +RES_LAST_MODIFIED = "last-modified" diff --git a/lib/github/ContentFile.py b/lib/github/ContentFile.py index e2b0993bb..2f69946be 100644 --- a/lib/github/ContentFile.py +++ b/lib/github/ContentFile.py @@ -7,7 +7,8 @@ # Copyright 2013 AKFish <akfish@gmail.com> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -39,6 +40,9 @@ class ContentFile(github.GithubObject.CompletableGithubObject): This class represents ContentFiles as returned for example by http://developer.github.com/v3/todo """ + def __repr__(self): + return self.get__repr__({"path": self._path.value}) + @property def content(self): """ diff --git a/lib/github/Download.py b/lib/github/Download.py index af73f73ee..49acc4897 100644 --- a/lib/github/Download.py +++ b/lib/github/Download.py @@ -7,7 +7,8 @@ # Copyright 2013 AKFish <akfish@gmail.com> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -32,6 +33,9 @@ class Download(github.GithubObject.CompletableGithubObject): This class represents Downloads as returned for example by http://developer.github.com/v3/todo """ + def __repr__(self): + return self.get__repr__({"id": self._id.value}) + @property def accesskeyid(self): """ diff --git a/lib/github/Event.py b/lib/github/Event.py index 87af16236..a769a230e 100644 --- a/lib/github/Event.py +++ b/lib/github/Event.py @@ -8,7 +8,8 @@ # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # Copyright 2013 martinqt <m.ki2@laposte.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -37,6 +38,9 @@ class Event(github.GithubObject.NonCompletableGithubObject): This class represents Events. The reference can be found here http://developer.github.com/v3/activity/events/ """ + def __repr__(self): + return self.get__repr__({"id": self._id.value, "type": self._type.value}) + @property def actor(self): """ diff --git a/lib/github/File.py b/lib/github/File.py index d411b3b54..d03efd3ba 100644 --- a/lib/github/File.py +++ b/lib/github/File.py @@ -7,7 +7,8 @@ # Copyright 2013 AKFish <akfish@gmail.com> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -32,6 +33,9 @@ class File(github.GithubObject.NonCompletableGithubObject): This class represents Files as returned for example by http://developer.github.com/v3/todo """ + def __repr__(self): + return self.get__repr__({"sha": self._sha.value, "filename": self._filename.value}) + @property def additions(self): """ @@ -81,6 +85,13 @@ class File(github.GithubObject.NonCompletableGithubObject): """ return self._patch.value + @property + def previous_filename(self): + """ + :type: string + """ + return self._previous_filename.value + @property def raw_url(self): """ @@ -110,6 +121,7 @@ class File(github.GithubObject.NonCompletableGithubObject): self._deletions = github.GithubObject.NotSet self._filename = github.GithubObject.NotSet self._patch = github.GithubObject.NotSet + self._previous_filename = github.GithubObject.NotSet self._raw_url = github.GithubObject.NotSet self._sha = github.GithubObject.NotSet self._status = github.GithubObject.NotSet @@ -129,6 +141,8 @@ class File(github.GithubObject.NonCompletableGithubObject): self._filename = self._makeStringAttribute(attributes["filename"]) if "patch" in attributes: # pragma no branch self._patch = self._makeStringAttribute(attributes["patch"]) + if "previous_filename" in attributes: # pragma no branch + self._previous_filename = self._makeStringAttribute(attributes["previous_filename"]) if "raw_url" in attributes: # pragma no branch self._raw_url = self._makeStringAttribute(attributes["raw_url"]) if "sha" in attributes: # pragma no branch diff --git a/lib/github/Gist.py b/lib/github/Gist.py index f93b79898..8d75e1e10 100644 --- a/lib/github/Gist.py +++ b/lib/github/Gist.py @@ -8,7 +8,8 @@ # Copyright 2013 AKFish <akfish@gmail.com> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -39,6 +40,9 @@ class Gist(github.GithubObject.CompletableGithubObject): This class represents Gists as returned for example by http://developer.github.com/v3/todo """ + def __repr__(self): + return self.get__repr__({"id": self._id.value}) + @property def comments(self): """ diff --git a/lib/github/GistComment.py b/lib/github/GistComment.py index 2e07b7a39..be1d08f74 100644 --- a/lib/github/GistComment.py +++ b/lib/github/GistComment.py @@ -7,7 +7,8 @@ # Copyright 2013 AKFish <akfish@gmail.com> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -34,6 +35,9 @@ class GistComment(github.GithubObject.CompletableGithubObject): This class represents GistComments as returned for example by http://developer.github.com/v3/todo """ + def __repr__(self): + return self.get__repr__({"id": self._id.value, "user": self._user.value}) + @property def body(self): """ diff --git a/lib/github/GistFile.py b/lib/github/GistFile.py index a90df90f0..5778acfce 100644 --- a/lib/github/GistFile.py +++ b/lib/github/GistFile.py @@ -7,7 +7,8 @@ # Copyright 2013 AKFish <akfish@gmail.com> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -32,6 +33,9 @@ class GistFile(github.GithubObject.NonCompletableGithubObject): This class represents GistFiles as returned for example by http://developer.github.com/v3/todo """ + def __repr__(self): + return self.get__repr__({"filename": self._filename.value}) + @property def content(self): """ diff --git a/lib/github/GistHistoryState.py b/lib/github/GistHistoryState.py index 3fcaf48ab..137462a67 100644 --- a/lib/github/GistHistoryState.py +++ b/lib/github/GistHistoryState.py @@ -7,7 +7,8 @@ # Copyright 2013 AKFish <akfish@gmail.com> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # diff --git a/lib/github/GitAuthor.py b/lib/github/GitAuthor.py index 552f19ef3..324dcc14f 100644 --- a/lib/github/GitAuthor.py +++ b/lib/github/GitAuthor.py @@ -7,7 +7,8 @@ # Copyright 2013 AKFish <akfish@gmail.com> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -32,6 +33,9 @@ class GitAuthor(github.GithubObject.NonCompletableGithubObject): This class represents GitAuthors as returned for example by http://developer.github.com/v3/todo """ + def __repr__(self): + return self.get__repr__({"name": self._name.value}) + @property def date(self): """ diff --git a/lib/github/GitBlob.py b/lib/github/GitBlob.py index 9c59b94bf..3de1c6fb7 100644 --- a/lib/github/GitBlob.py +++ b/lib/github/GitBlob.py @@ -7,7 +7,8 @@ # Copyright 2013 AKFish <akfish@gmail.com> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -32,6 +33,9 @@ class GitBlob(github.GithubObject.CompletableGithubObject): This class represents GitBlobs as returned for example by http://developer.github.com/v3/todo """ + def __repr__(self): + return self.get__repr__({"sha": self._sha.value}) + @property def content(self): """ diff --git a/lib/github/GitCommit.py b/lib/github/GitCommit.py index 3467eab5a..9d43d51b6 100644 --- a/lib/github/GitCommit.py +++ b/lib/github/GitCommit.py @@ -7,7 +7,8 @@ # Copyright 2013 AKFish <akfish@gmail.com> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -35,6 +36,9 @@ class GitCommit(github.GithubObject.CompletableGithubObject): This class represents GitCommits as returned for example by http://developer.github.com/v3/todo """ + def __repr__(self): + return self.get__repr__({"sha": self._sha.value}) + @property def author(self): """ diff --git a/lib/github/GitObject.py b/lib/github/GitObject.py index 1a169952b..0850061be 100644 --- a/lib/github/GitObject.py +++ b/lib/github/GitObject.py @@ -7,7 +7,8 @@ # Copyright 2013 AKFish <akfish@gmail.com> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -32,6 +33,9 @@ class GitObject(github.GithubObject.NonCompletableGithubObject): This class represents GitObjects as returned for example by http://developer.github.com/v3/todo """ + def __repr__(self): + return self.get__repr__({"sha": self._sha.value}) + @property def sha(self): """ diff --git a/lib/github/GitRef.py b/lib/github/GitRef.py index 0938fdb12..417982fe7 100644 --- a/lib/github/GitRef.py +++ b/lib/github/GitRef.py @@ -7,7 +7,8 @@ # Copyright 2013 AKFish <akfish@gmail.com> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -34,6 +35,9 @@ class GitRef(github.GithubObject.CompletableGithubObject): This class represents GitRefs as returned for example by http://developer.github.com/v3/todo """ + def __repr__(self): + return self.get__repr__({"ref": self._ref.value}) + @property def object(self): """ @@ -89,6 +93,22 @@ class GitRef(github.GithubObject.CompletableGithubObject): ) self._useAttributes(data) + def get_statuses(self): + """ + https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref + :calls: `GET /repos/:owner/:repo/commits/:ref/statuses` + :return: + """ + pass + + def get_status(self): + """ + https://developer.github.com/v3/repos/statuses/#get-the-combined-status-for-a-specific-ref + :calls: `GET /repos/:owner/:repo/commits/:ref/status` + :return: + """ + pass + def _initAttributes(self): self._object = github.GithubObject.NotSet self._ref = github.GithubObject.NotSet diff --git a/lib/github/GitRelease.py b/lib/github/GitRelease.py index 9e068020a..2c1ae524c 100644 --- a/lib/github/GitRelease.py +++ b/lib/github/GitRelease.py @@ -4,7 +4,8 @@ # # # Copyright 2015 Ed Holland <eholland@alertlogic.com> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -30,6 +31,9 @@ class GitRelease(github.GithubObject.CompletableGithubObject): This class represents GitRelease as returned for example by https://developer.github.com/v3/repos/releases """ + def __repr__(self): + return self.get__repr__({"title": self._title.value}) + @property def body(self): """ @@ -78,6 +82,14 @@ class GitRelease(github.GithubObject.CompletableGithubObject): self._completeIfNotSet(self._upload_url) return self._upload_url.value + @property + def html_url(self): + """ + :type: string + """ + self._completeIfNotSet(self._html_url) + return self._html_url.value + def delete_release(self): headers, data = self._requester.requestJsonAndCheck( "DELETE", @@ -111,6 +123,7 @@ class GitRelease(github.GithubObject.CompletableGithubObject): self._author = github.GithubObject.NotSet self._url = github.GithubObject.NotSet self._upload_url = github.GithubObject.NotSet + self._html_url = github.GithubObject.NotSet def _useAttributes(self, attributes): if "body" in attributes: @@ -125,3 +138,5 @@ class GitRelease(github.GithubObject.CompletableGithubObject): self._url = self._makeStringAttribute(attributes["url"]) if "upload_url" in attributes: self._upload_url = self._makeStringAttribute(attributes["upload_url"]) + if "html_url" in attributes: + self._html_url = self._makeStringAttribute(attributes["html_url"]) diff --git a/lib/github/GitTag.py b/lib/github/GitTag.py index 554dd2a74..347fd2474 100644 --- a/lib/github/GitTag.py +++ b/lib/github/GitTag.py @@ -7,7 +7,8 @@ # Copyright 2013 AKFish <akfish@gmail.com> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -35,6 +36,9 @@ class GitTag(github.GithubObject.CompletableGithubObject): This class represents GitTags as returned for example by http://developer.github.com/v3/todo """ + def __repr__(self): + return self.get__repr__({"sha": self._sha.value, "tag": self._tag.value}) + @property def message(self): """ diff --git a/lib/github/GitTree.py b/lib/github/GitTree.py index 9c78986dd..8012bb77c 100644 --- a/lib/github/GitTree.py +++ b/lib/github/GitTree.py @@ -7,7 +7,8 @@ # Copyright 2013 AKFish <akfish@gmail.com> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -34,6 +35,9 @@ class GitTree(github.GithubObject.CompletableGithubObject): This class represents GitTrees as returned for example by http://developer.github.com/v3/todo """ + def __repr__(self): + return self.get__repr__({"sha": self._sha.value}) + @property def sha(self): """ diff --git a/lib/github/GitTreeElement.py b/lib/github/GitTreeElement.py index a9b4615de..907fb58ff 100644 --- a/lib/github/GitTreeElement.py +++ b/lib/github/GitTreeElement.py @@ -7,7 +7,8 @@ # Copyright 2013 AKFish <akfish@gmail.com> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -32,6 +33,9 @@ class GitTreeElement(github.GithubObject.NonCompletableGithubObject): This class represents GitTreeElements as returned for example by http://developer.github.com/v3/todo """ + def __repr__(self): + return self.get__repr__({"sha": self._sha.value, "path": self._path.value}) + @property def mode(self): """ diff --git a/lib/github/GithubException.py b/lib/github/GithubException.py index 594f20131..3b724f1fa 100644 --- a/lib/github/GithubException.py +++ b/lib/github/GithubException.py @@ -7,7 +7,8 @@ # Copyright 2013 AKFish <akfish@gmail.com> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -36,6 +37,7 @@ class GithubException(Exception): Exception.__init__(self) self.__status = status self.__data = data + self.args = [status, data] @property def status(self): diff --git a/lib/github/GithubObject.py b/lib/github/GithubObject.py index 35e4b5b57..cd8b7d02e 100644 --- a/lib/github/GithubObject.py +++ b/lib/github/GithubObject.py @@ -7,7 +7,8 @@ # Copyright 2013 AKFish <akfish@gmail.com> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -23,12 +24,15 @@ # along with PyGithub. If not, see <http://www.gnu.org/licenses/>. # # # # ############################################################################## - +import sys import datetime +from operator import itemgetter import GithubException import Consts +atLeastPython3 = sys.hexversion >= 0x03000000 + class _NotSetType: def __repr__(self): @@ -204,7 +208,26 @@ class GithubObject(object): ''' :type: str ''' - return self._headers.get(Consts.RES_LAST_MODIFED) + return self._headers.get(Consts.RES_LAST_MODIFIED) + + def get__repr__(self, params): + """ + Converts the object to a nicely printable string. + """ + def format_params(params): + if atLeastPython3: + items = params.items() + else: + items = list(params.items()) + for k, v in sorted(items, key=itemgetter(0), reverse=True): + isText = isinstance(v, (str, unicode)) + if isText and not atLeastPython3: + v = v.encode('utf-8') + yield '{k}="{v}"'.format(k=k, v=v) if isText else '{k}={v}'.format(k=k, v=v) + return '{class_name}({params})'.format( + class_name=self.__class__.__name__, + params=", ".join(list(format_params(params))) + ) class NonCompletableGithubObject(GithubObject): diff --git a/lib/github/GitignoreTemplate.py b/lib/github/GitignoreTemplate.py index 75b96b1bd..55353dc35 100644 --- a/lib/github/GitignoreTemplate.py +++ b/lib/github/GitignoreTemplate.py @@ -6,7 +6,8 @@ # Copyright 2013 AKFish <akfish@gmail.com> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -31,6 +32,9 @@ class GitignoreTemplate(github.GithubObject.NonCompletableGithubObject): This class represents GitignoreTemplates as returned for example by http://developer.github.com/v3/todo """ + def __repr__(self): + return self.get__repr__({"name": self._name.value}) + @property def source(self): """ diff --git a/lib/github/Hook.py b/lib/github/Hook.py index b212b646d..8e0900592 100644 --- a/lib/github/Hook.py +++ b/lib/github/Hook.py @@ -7,7 +7,8 @@ # Copyright 2013 AKFish <akfish@gmail.com> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -34,6 +35,9 @@ class Hook(github.GithubObject.CompletableGithubObject): This class represents Hooks as returned for example by http://developer.github.com/v3/repos/hooks """ + def __repr__(self): + return self.get__repr__({"id": self._id.value, "url": self._url.value}) + @property def active(self): """ diff --git a/lib/github/HookDescription.py b/lib/github/HookDescription.py index 749aed154..d1058307d 100644 --- a/lib/github/HookDescription.py +++ b/lib/github/HookDescription.py @@ -7,7 +7,8 @@ # Copyright 2013 AKFish <akfish@gmail.com> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -32,6 +33,9 @@ class HookDescription(github.GithubObject.NonCompletableGithubObject): This class represents HookDescriptions as returned for example by http://developer.github.com/v3/todo """ + def __repr__(self): + return self.get__repr__({"name": self._name.value}) + @property def events(self): """ diff --git a/lib/github/HookResponse.py b/lib/github/HookResponse.py index e0945735f..bce05e398 100644 --- a/lib/github/HookResponse.py +++ b/lib/github/HookResponse.py @@ -7,7 +7,8 @@ # Copyright 2013 AKFish <akfish@gmail.com> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -32,6 +33,9 @@ class HookResponse(github.GithubObject.NonCompletableGithubObject): This class represents HookResponses as returned for example by http://developer.github.com/v3/todo """ + def __repr__(self): + return self.get__repr__({"status": self._status.value}) + @property def code(self): """ diff --git a/lib/github/InputFileContent.py b/lib/github/InputFileContent.py index d16556f10..a133eb1e3 100644 --- a/lib/github/InputFileContent.py +++ b/lib/github/InputFileContent.py @@ -6,7 +6,8 @@ # Copyright 2012 Zearin <zearin@gonk.net> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # diff --git a/lib/github/InputGitAuthor.py b/lib/github/InputGitAuthor.py index 4567f4177..f7a9ff142 100644 --- a/lib/github/InputGitAuthor.py +++ b/lib/github/InputGitAuthor.py @@ -6,7 +6,8 @@ # Copyright 2012 Zearin <zearin@gonk.net> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -45,6 +46,9 @@ class InputGitAuthor(object): self.__email = email self.__date = date + def __repr__(self): + return 'InputGitAuthor(name="{}")'.format(self.__name) + @property def _identity(self): identity = { diff --git a/lib/github/InputGitTreeElement.py b/lib/github/InputGitTreeElement.py index 94685c135..ee55452c6 100644 --- a/lib/github/InputGitTreeElement.py +++ b/lib/github/InputGitTreeElement.py @@ -6,7 +6,8 @@ # Copyright 2012 Zearin <zearin@gonk.net> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # diff --git a/lib/github/Installation.py b/lib/github/Installation.py new file mode 100644 index 000000000..67a086b1e --- /dev/null +++ b/lib/github/Installation.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- + +# ########################## Copyrights and license ############################ +# # +# Copyright 2017 Jannis Gebauer <ja.geb@me.com> # +# # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # +# # +# PyGithub is free software: you can redistribute it and/or modify it under # +# the terms of the GNU Lesser General Public License as published by the Free # +# Software Foundation, either version 3 of the License, or (at your option) # +# any later version. # +# # +# PyGithub is distributed in the hope that it will be useful, but WITHOUT ANY # +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # +# details. # +# # +# You should have received a copy of the GNU Lesser General Public License # +# along with PyGithub. If not, see <http://www.gnu.org/licenses/>. # +# # +# ############################################################################## + +import github.GithubObject +import github.PaginatedList + +import github.Gist +import github.Repository +import github.NamedUser +import github.Plan +import github.Organization +import github.UserKey +import github.Issue +import github.Event +import github.Authorization +import github.Notification + +INTEGRATION_PREVIEW_HEADERS = {"Accept": "application/vnd.github.machine-man-preview+json"} + + +class Installation(github.GithubObject.NonCompletableGithubObject): + """ + This class represents Installations as in https://developer.github.com/v3/integrations/installations + """ + + def __repr__(self): + return self.get__repr__({"id": self._id.value}) + + @property + def id(self): + return self._id + + def get_repos(self): + """ + :calls: `GET /installation/repositories <https://developer.github.com/v3/integrations/installations/#list-repositories>`_ + :rtype: :class:`github.PaginatedList.PaginatedList` of :class:`github.Repository.Repository` + """ + url_parameters = dict() + + return github.PaginatedList.PaginatedList( + contentClass=github.Repository.Repository, + requester=self._requester, + firstUrl="/installation/repositories", + firstParams=url_parameters, + headers=INTEGRATION_PREVIEW_HEADERS, + list_item='repositories' + ) + + def _initAttributes(self): + self._id = github.GithubObject.NotSet + + def _useAttributes(self, attributes): + if "id" in attributes: # pragma no branch + self._id = self._makeIntAttribute(attributes["id"]) \ No newline at end of file diff --git a/lib/github/InstallationAuthorization.py b/lib/github/InstallationAuthorization.py new file mode 100644 index 000000000..5dfaf8cbf --- /dev/null +++ b/lib/github/InstallationAuthorization.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- + +# ########################## Copyrights and license ############################ +# # +# Copyright 2016 Jannis gebauier <ja.geb@me.com> # +# # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # +# # +# PyGithub is free software: you can redistribute it and/or modify it under # +# the terms of the GNU Lesser General Public License as published by the Free # +# Software Foundation, either version 3 of the License, or (at your option) # +# any later version. # +# # +# PyGithub is distributed in the hope that it will be useful, but WITHOUT ANY # +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # +# details. # +# # +# You should have received a copy of the GNU Lesser General Public License # +# along with PyGithub. If not, see <http://www.gnu.org/licenses/>. # +# # +# ############################################################################## + +import datetime + +import github.GithubObject +import github.PaginatedList +import github.NamedUser + + +class InstallationAuthorization(github.GithubObject.NonCompletableGithubObject): + """ + InstallationAuthorization as obtained from a GitHub integration. + """ + + def __repr__(self): + return self.get__repr__({"expires_at": self._expires_at.value}) + + @property + def token(self): + """ + :type: string + """ + return self._token.value + + @property + def expires_at(self): + """ + :type: datetime + """ + return self._expires_at.value + + @property + def on_behalf_of(self): + """ + :type: :class:`github.NamedUser.NamedUser` + """ + return self._on_behalf_of.value + + def _initAttributes(self): + self._token = github.GithubObject.NotSet + self._expires_at = github.GithubObject.NotSet + self._on_behalf_of = github.GithubObject.NotSet + + def _useAttributes(self, attributes): + if "token" in attributes: # pragma no branch + self._token = self._makeStringAttribute(attributes["token"]) + if "expires_at" in attributes: # pragma no branch + self._expires_at = self._makeDatetimeAttribute(attributes["expires_at"]) + if "on_behalf_of" in attributes: # pragma no branch + self._on_behalf_of = self._makeClassAttribute(github.NamedUser.NamedUser, attributes["on_behalf_of"]) \ No newline at end of file diff --git a/lib/github/Issue.py b/lib/github/Issue.py index f88e9519b..9ef4df28e 100644 --- a/lib/github/Issue.py +++ b/lib/github/Issue.py @@ -10,7 +10,8 @@ # Copyright 2013 Stuart Glaser <stuglaser@gmail.com> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -28,6 +29,7 @@ # ############################################################################## import urllib +import datetime import github.GithubObject import github.PaginatedList @@ -45,6 +47,9 @@ class Issue(github.GithubObject.CompletableGithubObject): This class represents Issues as returned for example by http://developer.github.com/v3/todo """ + def __repr__(self): + return self.get__repr__({"number": self._number.value, "title": self._title.value}) + @property def assignee(self): """ @@ -53,6 +58,14 @@ class Issue(github.GithubObject.CompletableGithubObject): self._completeIfNotSet(self._assignee) return self._assignee.value + @property + def assignees(self): + """ + :type: list of :class:`github.NamedUser.NamedUser` + """ + self._completeIfNotSet(self._assignees) + return self._assignees.value + @property def body(self): """ @@ -217,6 +230,21 @@ class Issue(github.GithubObject.CompletableGithubObject): self._completeIfNotSet(self._user) return self._user.value + def add_to_assignees(self, *assignees): + """ + :calls: `POST /repos/:owner/:repo/issues/:number/assignees <https://developer.github.com/v3/issues/assignees>`_ + :param assignee: :class:`github.NamedUser.NamedUser` or string + :rtype: None + """ + assert all(isinstance(element, (github.NamedUser.NamedUser, str, unicode)) for element in assignees), assignees + post_parameters = {"assignees": [assignee.login if isinstance(assignee, github.NamedUser.NamedUser) else assignee for assignee in assignees]} + headers, data = self._requester.requestJsonAndCheck( + "POST", + self.url + "/assignees", + input=post_parameters + ) + self._useAttributes(data) + def add_to_labels(self, *labels): """ :calls: `POST /repos/:owner/:repo/issues/:number/labels <http://developer.github.com/v3/issues/labels>`_ @@ -258,12 +286,13 @@ class Issue(github.GithubObject.CompletableGithubObject): self.url + "/labels" ) - def edit(self, title=github.GithubObject.NotSet, body=github.GithubObject.NotSet, assignee=github.GithubObject.NotSet, state=github.GithubObject.NotSet, milestone=github.GithubObject.NotSet, labels=github.GithubObject.NotSet): + def edit(self, title=github.GithubObject.NotSet, body=github.GithubObject.NotSet, assignee=github.GithubObject.NotSet, state=github.GithubObject.NotSet, milestone=github.GithubObject.NotSet, labels=github.GithubObject.NotSet, assignees=github.GithubObject.NotSet): """ :calls: `PATCH /repos/:owner/:repo/issues/:number <http://developer.github.com/v3/issues>`_ :param title: string :param body: string :param assignee: string or :class:`github.NamedUser.NamedUser` or None + :param assignees: list (of string or :class:`github.NamedUser.NamedUser`) :param state: string :param milestone: :class:`github.Milestone.Milestone` or None :param labels: list of string @@ -272,6 +301,7 @@ class Issue(github.GithubObject.CompletableGithubObject): assert title is github.GithubObject.NotSet or isinstance(title, (str, unicode)), title assert body is github.GithubObject.NotSet or isinstance(body, (str, unicode)), body assert assignee is github.GithubObject.NotSet or assignee is None or isinstance(assignee, github.NamedUser.NamedUser) or isinstance(assignee, (str, unicode)), assignee + assert assignees is github.GithubObject.NotSet or all(isinstance(element, github.NamedUser.NamedUser) or isinstance(element, (str, unicode)) for element in assignees), assignees assert state is github.GithubObject.NotSet or isinstance(state, (str, unicode)), state assert milestone is github.GithubObject.NotSet or milestone is None or isinstance(milestone, github.Milestone.Milestone), milestone assert labels is github.GithubObject.NotSet or all(isinstance(element, (str, unicode)) for element in labels), labels @@ -285,6 +315,8 @@ class Issue(github.GithubObject.CompletableGithubObject): post_parameters["assignee"] = assignee else: post_parameters["assignee"] = assignee._identity if assignee else '' + if assignees is not github.GithubObject.NotSet: + post_parameters["assignees"] = [element._identity if isinstance(element, github.NamedUser.NamedUser) else element for element in assignees] if state is not github.GithubObject.NotSet: post_parameters["state"] = state if milestone is not github.GithubObject.NotSet: @@ -311,16 +343,21 @@ class Issue(github.GithubObject.CompletableGithubObject): ) return github.IssueComment.IssueComment(self._requester, headers, data, completed=True) - def get_comments(self): + def get_comments(self, since=github.GithubObject.NotSet): """ :calls: `GET /repos/:owner/:repo/issues/:number/comments <http://developer.github.com/v3/issues/comments>`_ + :param since: datetime.datetime format YYYY-MM-DDTHH:MM:SSZ :rtype: :class:`github.PaginatedList.PaginatedList` of :class:`github.IssueComment.IssueComment` """ + assert since is github.GithubObject.NotSet or isinstance(since, datetime.datetime), since + url_parameters = dict() + if since is not github.GithubObject.NotSet: + url_parameters["since"] = since.strftime("%Y-%m-%dT%H:%M:%SZ") return github.PaginatedList.PaginatedList( github.IssueComment.IssueComment, self._requester, self.url + "/comments", - None + url_parameters ) def get_events(self): @@ -347,6 +384,21 @@ class Issue(github.GithubObject.CompletableGithubObject): None ) + def remove_from_assignees(self, *assignees): + """ + :calls: `DELETE /repos/:owner/:repo/issues/:number/assignees <https://developer.github.com/v3/issues/assignees>`_ + :param assignee: :class:`github.NamedUser.NamedUser` or string + :rtype: None + """ + assert all(isinstance(element, (github.NamedUser.NamedUser, str, unicode)) for element in assignees), assignees + post_parameters = {"assignees": [assignee.login if isinstance(assignee, github.NamedUser.NamedUser) else assignee for assignee in assignees]} + headers, data = self._requester.requestJsonAndCheck( + "DELETE", + self.url + "/assignees", + input=post_parameters + ) + self._useAttributes(data) + def remove_from_labels(self, label): """ :calls: `DELETE /repos/:owner/:repo/issues/:number/labels/:name <http://developer.github.com/v3/issues/labels>`_ @@ -383,6 +435,7 @@ class Issue(github.GithubObject.CompletableGithubObject): def _initAttributes(self): self._assignee = github.GithubObject.NotSet + self._assignees = github.GithubObject.NotSet self._body = github.GithubObject.NotSet self._closed_at = github.GithubObject.NotSet self._closed_by = github.GithubObject.NotSet @@ -407,6 +460,13 @@ class Issue(github.GithubObject.CompletableGithubObject): def _useAttributes(self, attributes): if "assignee" in attributes: # pragma no branch self._assignee = self._makeClassAttribute(github.NamedUser.NamedUser, attributes["assignee"]) + if "assignees" in attributes: # pragma no branch + self._assignees = self._makeListOfClassesAttribute(github.NamedUser.NamedUser, attributes["assignees"]) + elif "assignee" in attributes: + if attributes["assignee"] is not None: + self._assignees = self._makeListOfClassesAttribute(github.NamedUser.NamedUser, [attributes["assignee"]]) + else: + self._assignees = self._makeListOfClassesAttribute(github.NamedUser.NamedUser, []) if "body" in attributes: # pragma no branch self._body = self._makeStringAttribute(attributes["body"]) if "closed_at" in attributes: # pragma no branch diff --git a/lib/github/IssueComment.py b/lib/github/IssueComment.py index 0b0fc5a90..8593a8cd5 100644 --- a/lib/github/IssueComment.py +++ b/lib/github/IssueComment.py @@ -8,7 +8,8 @@ # Copyright 2013 Michael Stead <michael.stead@gmail.com> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -35,6 +36,9 @@ class IssueComment(github.GithubObject.CompletableGithubObject): This class represents IssueComments as returned for example by http://developer.github.com/v3/todo """ + def __repr__(self): + return self.get__repr__({"id": self._id.value, "user": self._user.value}) + @property def body(self): """ diff --git a/lib/github/IssueEvent.py b/lib/github/IssueEvent.py index ea372978d..d55cd3534 100644 --- a/lib/github/IssueEvent.py +++ b/lib/github/IssueEvent.py @@ -7,7 +7,8 @@ # Copyright 2013 AKFish <akfish@gmail.com> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -35,6 +36,9 @@ class IssueEvent(github.GithubObject.CompletableGithubObject): This class represents IssueEvents as returned for example by http://developer.github.com/v3/todo """ + def __repr__(self): + return self.get__repr__({"id": self._id.value}) + @property def actor(self): """ diff --git a/lib/github/IssuePullRequest.py b/lib/github/IssuePullRequest.py index 6e5eec444..18243d11d 100644 --- a/lib/github/IssuePullRequest.py +++ b/lib/github/IssuePullRequest.py @@ -7,7 +7,8 @@ # Copyright 2013 AKFish <akfish@gmail.com> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # diff --git a/lib/github/Label.py b/lib/github/Label.py index 6fd413049..56e6d083a 100644 --- a/lib/github/Label.py +++ b/lib/github/Label.py @@ -8,7 +8,8 @@ # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # Copyright 2013 martinqt <m.ki2@laposte.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -35,6 +36,9 @@ class Label(github.GithubObject.CompletableGithubObject): This class represents Labels. The reference can be found here http://developer.github.com/v3/issues/labels/ """ + def __repr__(self): + return self.get__repr__({"name": self._name.value}) + @property def color(self): """ diff --git a/lib/github/Legacy.py b/lib/github/Legacy.py index 0cd5bc6c8..7c8a7995a 100644 --- a/lib/github/Legacy.py +++ b/lib/github/Legacy.py @@ -8,7 +8,8 @@ # Copyright 2013 AKFish <akfish@gmail.com> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # diff --git a/lib/github/MainClass.py b/lib/github/MainClass.py index f55be934c..2184547e5 100644 --- a/lib/github/MainClass.py +++ b/lib/github/MainClass.py @@ -8,7 +8,8 @@ # Copyright 2013 Peter Golm <golm.peter@gmail.com> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -27,14 +28,19 @@ import urllib import pickle +import time +import sys +from httplib import HTTPSConnection +import jwt -from Requester import Requester +from Requester import Requester, json import AuthenticatedUser import NamedUser import Organization import Gist import github.PaginatedList import Repository +import Installation import Legacy import github.GithubObject import HookDescription @@ -42,7 +48,10 @@ import GitignoreTemplate import Status import StatusMessage import RateLimit +import InstallationAuthorization +import GithubException +atLeastPython3 = sys.hexversion >= 0x03000000 DEFAULT_BASE_URL = "https://api.github.com" DEFAULT_TIMEOUT = 10 @@ -589,3 +598,94 @@ class Github(object): cnx="status" ) return [StatusMessage.StatusMessage(self.__requester, headers, attributes, completed=True) for attributes in data] + + def get_installation(self, id): + """ + + :param id: + :return: + """ + return Installation.Installation(self.__requester, headers={}, attributes={"id": id}, completed=True) + + +class GithubIntegration(object): + """ + Main class to obtain tokens for a GitHub integration. + """ + + def __init__(self, integration_id, private_key): + """ + :param integration_id: int + :param private_key: string + """ + self.integration_id = integration_id + self.private_key = private_key + + def create_jwt(self): + """ + Creates a signed JWT, valid for 60 seconds. + :return: + """ + now = int(time.time()) + payload = { + "iat": now, + "exp": now + 60, + "iss": self.integration_id + } + return jwt.encode( + payload, + key=self.private_key, + algorithm="RS256" + ) + + def get_access_token(self, installation_id, user_id=None): + """ + Get an access token for the given installation id. + POSTs https://api.github.com/installations/<installation_id>/access_tokens + :param user_id: int + :param installation_id: int + :return: :class:`github.InstallationAuthorization.InstallationAuthorization` + """ + body = None + if user_id: + body = json.dumps({"user_id": user_id}) + conn = HTTPSConnection("api.github.com") + conn.request( + method="POST", + url="/installations/{}/access_tokens".format(installation_id), + headers={ + "Authorization": "Bearer {}".format(self.create_jwt()), + "Accept": "application/vnd.github.machine-man-preview+json", + "User-Agent": "PyGithub/Python" + }, + body=body + ) + response = conn.getresponse() + response_text = response.read() + + if atLeastPython3: + response_text = response_text.decode('utf-8') + + conn.close() + if response.status == 201: + data = json.loads(response_text) + return InstallationAuthorization.InstallationAuthorization( + requester=None, # not required, this is a NonCompletableGithubObject + headers={}, # not required, this is a NonCompletableGithubObject + attributes=data, + completed=True + ) + elif response.status == 403: + raise GithubException.BadCredentialsException( + status=response.status, + data=response_text + ) + elif response.status == 404: + raise GithubException.UnknownObjectException( + status=response.status, + data=response_text + ) + raise GithubException.GithubException( + status=response.status, + data=response_text + ) \ No newline at end of file diff --git a/lib/github/Milestone.py b/lib/github/Milestone.py index 7875ebccd..ebe59df14 100644 --- a/lib/github/Milestone.py +++ b/lib/github/Milestone.py @@ -8,7 +8,8 @@ # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # Copyright 2013 martinqt <m.ki2@laposte.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -39,6 +40,9 @@ class Milestone(github.GithubObject.CompletableGithubObject): This class represents Milestones. The reference can be found here http://developer.github.com/v3/issues/milestones/ """ + def __repr__(self): + return self.get__repr__({"number": self._number.value}) + @property def closed_issues(self): """ diff --git a/lib/github/NamedUser.py b/lib/github/NamedUser.py index 1b4175465..2549e6750 100644 --- a/lib/github/NamedUser.py +++ b/lib/github/NamedUser.py @@ -8,7 +8,8 @@ # Copyright 2013 AKFish <akfish@gmail.com> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -41,6 +42,9 @@ class NamedUser(github.GithubObject.CompletableGithubObject): This class represents NamedUsers as returned for example by http://developer.github.com/v3/todo """ + def __repr__(self): + return self.get__repr__({"login": self._login.value}) + @property def avatar_url(self): """ diff --git a/lib/github/Notification.py b/lib/github/Notification.py index 84533fbea..b4b0205a9 100644 --- a/lib/github/Notification.py +++ b/lib/github/Notification.py @@ -7,7 +7,8 @@ # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # Copyright 2013 martinqt <m.ki2@laposte.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -35,6 +36,9 @@ class Notification(github.GithubObject.CompletableGithubObject): This class represents Notifications. The reference can be found here http://developer.github.com/v3/activity/notifications/ """ + def __repr__(self): + return self.get__repr__({"id": self._id.value, "subject": self._subject.value}) + @property def id(self): """ diff --git a/lib/github/NotificationSubject.py b/lib/github/NotificationSubject.py index 62744a311..c4289424e 100644 --- a/lib/github/NotificationSubject.py +++ b/lib/github/NotificationSubject.py @@ -5,7 +5,8 @@ # Copyright 2013 AKFish <akfish@gmail.com> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -30,6 +31,9 @@ class NotificationSubject(github.GithubObject.NonCompletableGithubObject): This class represents Subjects of Notifications as returned for example by http://developer.github.com/v3/activity/notifications/#list-your-notifications """ + def __repr__(self): + return self.get__repr__({"title": self._title.value}) + @property def title(self): """ diff --git a/lib/github/Organization.py b/lib/github/Organization.py index e043cac17..50e796f9c 100644 --- a/lib/github/Organization.py +++ b/lib/github/Organization.py @@ -9,7 +9,8 @@ # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # Copyright 2013 martinqt <m.ki2@laposte.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -43,6 +44,9 @@ class Organization(github.GithubObject.CompletableGithubObject): This class represents Organizations. The reference can be found here http://developer.github.com/v3/orgs/ """ + def __repr__(self): + return self.get__repr__({"id": self._id.value, "name": self._name.value}) + @property def avatar_url(self): """ @@ -522,7 +526,7 @@ class Organization(github.GithubObject.CompletableGithubObject): def get_repos(self, type=github.GithubObject.NotSet): """ :calls: `GET /orgs/:org/repos <http://developer.github.com/v3/repos>`_ - :param type: string + :param type: string ('all', 'public', 'private', 'forks', 'sources', 'member') :rtype: :class:`github.PaginatedList.PaginatedList` of :class:`github.Repository.Repository` """ assert type is github.GithubObject.NotSet or isinstance(type, (str, unicode)), type @@ -572,6 +576,11 @@ class Organization(github.GithubObject.CompletableGithubObject): "GET", self.url + "/members/" + member._identity ) + if status == 302: + status, headers, data = self._requester.requestJson( + "GET", + headers['location'] + ) return status == 204 def has_in_public_members(self, public_member): diff --git a/lib/github/PaginatedList.py b/lib/github/PaginatedList.py index 12e1acc7e..81e21220a 100644 --- a/lib/github/PaginatedList.py +++ b/lib/github/PaginatedList.py @@ -9,7 +9,8 @@ # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # Copyright 2013 davidbrai <davidbrai@gmail.com> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -106,7 +107,7 @@ class PaginatedList(PaginatedListBase): some_other_repos = user.get_repos().get_page(3) """ - def __init__(self, contentClass, requester, firstUrl, firstParams, headers=None): + def __init__(self, contentClass, requester, firstUrl, firstParams, headers=None, list_item="items"): PaginatedListBase.__init__(self) self.__requester = requester self.__contentClass = contentClass @@ -115,6 +116,7 @@ class PaginatedList(PaginatedListBase): self.__nextUrl = firstUrl self.__nextParams = firstParams or {} self.__headers = headers + self.__list_item = list_item if self.__requester.per_page != 30: self.__nextParams["per_page"] = self.__requester.per_page self._reversed = False @@ -172,9 +174,9 @@ class PaginatedList(PaginatedListBase): self.__nextUrl = links["next"] self.__nextParams = None - if 'items' in data: + if self.__list_item in data: self.__totalCount = data['total_count'] - data = data["items"] + data = data[self.__list_item] content = [ self.__contentClass(self.__requester, headers, element, completed=False) @@ -208,9 +210,9 @@ class PaginatedList(PaginatedListBase): headers=self.__headers ) - if 'items' in data: + if self.__list_item in data: self.__totalCount = data['total_count'] - data = data["items"] + data = data[self.__list_item] return [ self.__contentClass(self.__requester, headers, element, completed=False) diff --git a/lib/github/Permissions.py b/lib/github/Permissions.py index 7fdff591a..3bb6faf86 100644 --- a/lib/github/Permissions.py +++ b/lib/github/Permissions.py @@ -7,7 +7,8 @@ # Copyright 2013 AKFish <akfish@gmail.com> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -32,6 +33,13 @@ class Permissions(github.GithubObject.NonCompletableGithubObject): This class represents Permissionss as returned for example by http://developer.github.com/v3/todo """ + def __repr__(self): + return self.get__repr__({ + "admin": self._admin.value, + "pull": self._pull.value, + "push": self._push.value + }) + @property def admin(self): """ diff --git a/lib/github/Plan.py b/lib/github/Plan.py index 7a3781ded..0b6473ae6 100644 --- a/lib/github/Plan.py +++ b/lib/github/Plan.py @@ -7,7 +7,8 @@ # Copyright 2013 AKFish <akfish@gmail.com> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -32,6 +33,9 @@ class Plan(github.GithubObject.NonCompletableGithubObject): This class represents Plans as returned for example by http://developer.github.com/v3/todo """ + def __repr__(self): + return self.get__repr__({"name": self._name.value}) + @property def collaborators(self): """ diff --git a/lib/github/PullRequest.py b/lib/github/PullRequest.py index ffd528845..7cee6f539 100644 --- a/lib/github/PullRequest.py +++ b/lib/github/PullRequest.py @@ -9,7 +9,8 @@ # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # Copyright 2013 martinqt <m.ki2@laposte.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -36,6 +37,8 @@ import github.PullRequestComment import github.File import github.IssueComment import github.Commit +import github.PullRequestReview +import github.PullRequestReviewerRequest class PullRequest(github.GithubObject.CompletableGithubObject): @@ -43,6 +46,9 @@ class PullRequest(github.GithubObject.CompletableGithubObject): This class represents PullRequests. The reference can be found here http://developer.github.com/v3/pulls/ """ + def __repr__(self): + return self.get__repr__({"number": self._number.value, "title": self._title.value}) + @property def additions(self): """ @@ -59,6 +65,14 @@ class PullRequest(github.GithubObject.CompletableGithubObject): self._completeIfNotSet(self._assignee) return self._assignee.value + @property + def assignees(self): + """ + :type: list of :class:`github.NamedUser.NamedUser` + """ + self._completeIfNotSet(self._assignees) + return self._assignees.value + @property def base(self): """ @@ -483,6 +497,46 @@ class PullRequest(github.GithubObject.CompletableGithubObject): None ) + def get_review(self, id): + """ + :calls: `GET /repos/:owner/:repo/pulls/:number/reviews/:id <https://developer.github.com/v3/pulls/reviews>`_ + :param id: integer + :rtype: :class:`github.PullRequestReview.PullRequestReview` + """ + assert isinstance(id, (int, long)), id + headers, data = self._requester.requestJsonAndCheck( + "GET", + self.url + "/reviews/" + str(id), + headers={'Accept': 'application/vnd.github.black-cat-preview+json'} + ) + return github.PullRequestReview.PullRequestReview(self._requester, headers, data, completed=True) + + def get_reviews(self): + """ + :calls: `GET /repos/:owner/:repo/pulls/:number/reviews <https://developer.github.com/v3/pulls/reviews/>`_ + :rtype: :class:`github.PaginatedList.PaginatedList` of :class:`github.PullRequestReview.PullRequestReview` + """ + return github.PaginatedList.PaginatedList( + github.PullRequestReview.PullRequestReview, + self._requester, + self.url + "/reviews", + None, + headers={'Accept': 'application/vnd.github.black-cat-preview+json'} + ) + + def get_reviewer_requests(self): + """ + :calls: `GET /repos/:owner/:repo/pulls/:number/requested_reviewers <https://developer.github.com/v3/pulls/review_requests/>`_ + :rtype: :class:`github.PaginatedList.PaginatedList` of :class:`github.InspectionReviewers.InspectionReviewers` + """ + return github.PaginatedList.PaginatedList( + github.PullRequestReviewerRequest.PullRequestReviewerRequest, + self._requester, + self.url + "/requested_reviewers", + None, + headers={'Accept': 'application/vnd.github.black-cat-preview+json'} + ) + def is_merged(self): """ :calls: `GET /repos/:owner/:repo/pulls/:number/merge <http://developer.github.com/v3/pulls>`_ @@ -514,6 +568,7 @@ class PullRequest(github.GithubObject.CompletableGithubObject): def _initAttributes(self): self._additions = github.GithubObject.NotSet self._assignee = github.GithubObject.NotSet + self._assignees = github.GithubObject.NotSet self._base = github.GithubObject.NotSet self._body = github.GithubObject.NotSet self._changed_files = github.GithubObject.NotSet @@ -552,6 +607,13 @@ class PullRequest(github.GithubObject.CompletableGithubObject): self._additions = self._makeIntAttribute(attributes["additions"]) if "assignee" in attributes: # pragma no branch self._assignee = self._makeClassAttribute(github.NamedUser.NamedUser, attributes["assignee"]) + if "assignees" in attributes: # pragma no branch + self._assignees = self._makeListOfClassesAttribute(github.NamedUser.NamedUser, attributes["assignees"]) + elif "assignee" in attributes: + if attributes["assignee"] is not None: + self._assignees = self._makeListOfClassesAttribute(github.NamedUser.NamedUser, [attributes["assignee"]]) + else: + self._assignees = self._makeListOfClassesAttribute(github.NamedUser.NamedUser, []) if "base" in attributes: # pragma no branch self._base = self._makeClassAttribute(github.PullRequestPart.PullRequestPart, attributes["base"]) if "body" in attributes: # pragma no branch diff --git a/lib/github/PullRequestComment.py b/lib/github/PullRequestComment.py index 4169afa27..1decabad9 100644 --- a/lib/github/PullRequestComment.py +++ b/lib/github/PullRequestComment.py @@ -9,7 +9,8 @@ # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # Copyright 2013 martinqt <m.ki2@laposte.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -36,6 +37,9 @@ class PullRequestComment(github.GithubObject.CompletableGithubObject): This class represents PullRequestComments. The reference can be found here http://developer.github.com/v3/pulls/comments/ """ + def __repr__(self): + return self.get__repr__({"id": self._id.value, "user": self._user.value}) + @property def body(self): """ diff --git a/lib/github/PullRequestMergeStatus.py b/lib/github/PullRequestMergeStatus.py index 682472c58..a59adfa9c 100644 --- a/lib/github/PullRequestMergeStatus.py +++ b/lib/github/PullRequestMergeStatus.py @@ -8,7 +8,8 @@ # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # Copyright 2013 martinqt <m.ki2@laposte.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -33,6 +34,9 @@ class PullRequestMergeStatus(github.GithubObject.NonCompletableGithubObject): This class represents PullRequestMergeStatuss. The reference can be found here http://developer.github.com/v3/pulls/#get-if-a-pull-request-has-been-merged """ + def __repr__(self): + return self.get__repr__({"sha": self._sha.value, "merged": self._merged.value}) + @property def merged(self): """ diff --git a/lib/github/PullRequestPart.py b/lib/github/PullRequestPart.py index d05948926..8f03d6f10 100644 --- a/lib/github/PullRequestPart.py +++ b/lib/github/PullRequestPart.py @@ -7,7 +7,8 @@ # Copyright 2013 AKFish <akfish@gmail.com> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -35,6 +36,9 @@ class PullRequestPart(github.GithubObject.NonCompletableGithubObject): This class represents PullRequestParts as returned for example by http://developer.github.com/v3/todo """ + def __repr__(self): + return self.get__repr__({"sha": self._sha.value}) + @property def label(self): """ diff --git a/lib/github/PullRequestReview.py b/lib/github/PullRequestReview.py new file mode 100644 index 000000000..1c2189ee8 --- /dev/null +++ b/lib/github/PullRequestReview.py @@ -0,0 +1,121 @@ +# -*- coding: utf-8 -*- + +# ########################## Copyrights and license ############################ +# # +# Copyright 2012 Vincent Jacques <vincent@vincent-jacques.net> # +# Copyright 2012 Zearin <zearin@gonk.net> # +# Copyright 2013 AKFish <akfish@gmail.com> # +# Copyright 2013 Michael Stead <michael.stead@gmail.com> # +# Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # +# # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # +# # +# PyGithub is free software: you can redistribute it and/or modify it under # +# the terms of the GNU Lesser General Public License as published by the Free # +# Software Foundation, either version 3 of the License, or (at your option) # +# any later version. # +# # +# PyGithub is distributed in the hope that it will be useful, but WITHOUT ANY # +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # +# details. # +# # +# You should have received a copy of the GNU Lesser General Public License # +# along with PyGithub. If not, see <http://www.gnu.org/licenses/>. # +# # +# ############################################################################## + +import github.GithubObject + +import github.NamedUser + + +class PullRequestReview(github.GithubObject.CompletableGithubObject): + """ + This class represents Pull Request Reviews as returned for example by https://developer.github.com/v3/pulls/reviews/ + """ + + def __repr__(self): + return self.get__repr__({"id": self._id.value, "user": self._user.value}) + + @property + def id(self): + """ + :type: integer + """ + self._completeIfNotSet(self._id) + return self._id.value + + @property + def user(self): + """ + :type: :class:`github.NamedUser.NamedUser` + """ + self._completeIfNotSet(self._user) + return self._user.value + + @property + def body(self): + """ + :type: string + """ + self._completeIfNotSet(self._body) + return self._body.value + + @property + def commit_id(self): + """ + :type: string + """ + self._completeIfNotSet(self._commit_id) + return self._commit_id.value + + @property + def state(self): + """ + :type: string + """ + self._completeIfNotSet(self._state) + return self._state.value + + @property + def html_url(self): + """ + :type: string + """ + self._completeIfNotSet(self._html_url) + return self._html_url.value + + @property + def pull_request_url(self): + """ + :type: string + """ + self._completeIfNotSet(self._pull_request_url) + return self._pull_request_url.value + + def _initAttributes(self): + self._id = github.GithubObject.NotSet + self._user = github.GithubObject.NotSet + self._body = github.GithubObject.NotSet + self._commit_id = github.GithubObject.NotSet + self._state = github.GithubObject.NotSet + self._html_url = github.GithubObject.NotSet + self._pull_request_url = github.GithubObject.NotSet + + def _useAttributes(self, attributes): + if "id" in attributes: # pragma no branch + self._id = self._makeIntAttribute(attributes["id"]) + if "user" in attributes: # pragma no branch + self._user = self._makeClassAttribute(github.NamedUser.NamedUser, attributes["user"]) + if "body" in attributes: # pragma no branch + self._body = self._makeStringAttribute(attributes["body"]) + if "commit_id" in attributes: # pragma no branch + self._commit_id = self._makeStringAttribute(attributes["commit_id"]) + if "state" in attributes: # pragma no branch + self._state = self._makeStringAttribute(attributes["state"]) + if "html_url" in attributes: # pragma no branch + self._html_url = self._makeStringAttribute(attributes["html_url"]) + if "pull_request_url" in attributes: # pragma no branch + self._pull_request_url = self._makeStringAttribute(attributes["pull_request_url"]) diff --git a/lib/github/PullRequestReviewerRequest.py b/lib/github/PullRequestReviewerRequest.py new file mode 100644 index 000000000..6f3ea688a --- /dev/null +++ b/lib/github/PullRequestReviewerRequest.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- + +# ########################## Copyrights and license ############################ +# # +# Copyright 2012 Vincent Jacques <vincent@vincent-jacques.net> # +# Copyright 2012 Zearin <zearin@gonk.net> # +# Copyright 2013 AKFish <akfish@gmail.com> # +# Copyright 2013 Michael Stead <michael.stead@gmail.com> # +# Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # +# # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # +# # +# PyGithub is free software: you can redistribute it and/or modify it under # +# the terms of the GNU Lesser General Public License as published by the Free # +# Software Foundation, either version 3 of the License, or (at your option) # +# any later version. # +# # +# PyGithub is distributed in the hope that it will be useful, but WITHOUT ANY # +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more # +# details. # +# # +# You should have received a copy of the GNU Lesser General Public License # +# along with PyGithub. If not, see <http://www.gnu.org/licenses/>. # +# # +# ############################################################################## + +import github.GithubObject + +import github.NamedUser + + +class PullRequestReviewerRequest(github.GithubObject.CompletableGithubObject): + """ + This class represents Pull Request Reviewer Requests as returned for example by https://developer.github.com/v3/pulls/review_requests/ + """ + + def __repr__(self): + return self.get__repr__({"id": self._id.value, "login": self._login.value}) + + @property + def login(self): + """ + :type: string + """ + self._completeIfNotSet(self._login) + return self._login.value + + @property + def id(self): + """ + :type: integer + """ + self._completeIfNotSet(self._id) + return self._id.value + + def _initAttributes(self): + self._login = github.GithubObject.NotSet + self._id = github.GithubObject.NotSet + + def _useAttributes(self, attributes): + if "login" in attributes: # pragma no branch + self._login = self._makeStringAttribute(attributes["login"]) + if "id" in attributes: # pragma no branch + self._id = self._makeIntAttribute(attributes["id"]) diff --git a/lib/github/Rate.py b/lib/github/Rate.py index 60f2f2b3a..5c7e4f5d1 100644 --- a/lib/github/Rate.py +++ b/lib/github/Rate.py @@ -4,7 +4,8 @@ # # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -30,6 +31,9 @@ class Rate(github.GithubObject.NonCompletableGithubObject): This class represents rate limits as defined in http://developer.github.com/v3/rate_limit """ + def __repr__(self): + return self.get__repr__({"limit": self._limit.value, "remaining": self._remaining.value}) + @property def limit(self): """ diff --git a/lib/github/RateLimit.py b/lib/github/RateLimit.py index f0cffc65c..9ff0f08cd 100644 --- a/lib/github/RateLimit.py +++ b/lib/github/RateLimit.py @@ -4,7 +4,8 @@ # # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -30,6 +31,9 @@ class RateLimit(github.GithubObject.NonCompletableGithubObject): This class represents rate limits as defined in http://developer.github.com/v3/rate_limit """ + def __repr__(self): + return self.get__repr__({"rate": self._rate.value}) + @property def rate(self): """ diff --git a/lib/github/Repository.py b/lib/github/Repository.py index 64c96e3ea..b079bc881 100644 --- a/lib/github/Repository.py +++ b/lib/github/Repository.py @@ -13,7 +13,8 @@ # Copyright 2013 martinqt <m.ki2@laposte.net> # # Copyright 2015 Jannis Gebauer <ja.geb@me.com> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -29,9 +30,10 @@ # along with PyGithub. If not, see <http://www.gnu.org/licenses/>. # # # # ############################################################################## - +import sys import urllib import datetime +from base64 import b64encode import github.GithubObject import github.PaginatedList @@ -70,12 +72,18 @@ import github.StatsParticipation import github.StatsPunchCard import github.Stargazer +atLeastPython26 = sys.hexversion >= 0x02060000 +atLeastPython3 = sys.hexversion >= 0x03000000 + class Repository(github.GithubObject.CompletableGithubObject): """ - This class represents Repositorys. The reference can be found here http://developer.github.com/v3/repos/ + This class represents Repositories. The reference can be found here http://developer.github.com/v3/repos/ """ + def __repr__(self): + return self.get__repr__({"full_name": self._full_name.value}) + @property def archive_url(self): """ @@ -571,6 +579,14 @@ class Repository(github.GithubObject.CompletableGithubObject): """ self._completeIfNotSet(self._subscribers_url) return self._subscribers_url.value + + @property + def subscribers_count(self): + """ + :type: integer + """ + self._completeIfNotSet(self._subscribers_count) + return self._subscribers_count.value @property def subscription_url(self): @@ -849,12 +865,13 @@ class Repository(github.GithubObject.CompletableGithubObject): ) return github.Hook.Hook(self._requester, headers, data, completed=True) - def create_issue(self, title, body=github.GithubObject.NotSet, assignee=github.GithubObject.NotSet, milestone=github.GithubObject.NotSet, labels=github.GithubObject.NotSet): + def create_issue(self, title, body=github.GithubObject.NotSet, assignee=github.GithubObject.NotSet, milestone=github.GithubObject.NotSet, labels=github.GithubObject.NotSet, assignees=github.GithubObject.NotSet): """ :calls: `POST /repos/:owner/:repo/issues <http://developer.github.com/v3/issues>`_ :param title: string :param body: string :param assignee: string or :class:`github.NamedUser.NamedUser` + :param assignees: list (of string or :class:`github.NamedUser.NamedUser`) :param milestone: :class:`github.Milestone.Milestone` :param labels: list of :class:`github.Label.Label` :rtype: :class:`github.Issue.Issue` @@ -862,8 +879,9 @@ class Repository(github.GithubObject.CompletableGithubObject): assert isinstance(title, (str, unicode)), title assert body is github.GithubObject.NotSet or isinstance(body, (str, unicode)), body assert assignee is github.GithubObject.NotSet or isinstance(assignee, github.NamedUser.NamedUser) or isinstance(assignee, (str, unicode)), assignee + assert assignees is github.GithubObject.NotSet or all(isinstance(element, github.NamedUser.NamedUser) or isinstance(element, (str, unicode)) for element in assignees), assignees assert milestone is github.GithubObject.NotSet or isinstance(milestone, github.Milestone.Milestone), milestone - assert labels is github.GithubObject.NotSet or all(isinstance(element, github.Label.Label) or isinstance(element, str) for element in labels), labels + assert labels is github.GithubObject.NotSet or all(isinstance(element, github.Label.Label) or isinstance(element, (str, unicode)) for element in labels), labels post_parameters = { "title": title, @@ -875,6 +893,8 @@ class Repository(github.GithubObject.CompletableGithubObject): post_parameters["assignee"] = assignee else: post_parameters["assignee"] = assignee._identity + if assignees is not github.GithubObject.NotSet: + post_parameters["assignees"] = [element._identity if isinstance(element, github.NamedUser.NamedUser) else element for element in assignees] if milestone is not github.GithubObject.NotSet: post_parameters["milestone"] = milestone._identity if labels is not github.GithubObject.NotSet: @@ -1002,7 +1022,7 @@ class Repository(github.GithubObject.CompletableGithubObject): self.url ) - def edit(self, name, description=github.GithubObject.NotSet, homepage=github.GithubObject.NotSet, private=github.GithubObject.NotSet, has_issues=github.GithubObject.NotSet, has_wiki=github.GithubObject.NotSet, has_downloads=github.GithubObject.NotSet, default_branch=github.GithubObject.NotSet): + def edit(self, name=None, description=github.GithubObject.NotSet, homepage=github.GithubObject.NotSet, private=github.GithubObject.NotSet, has_issues=github.GithubObject.NotSet, has_wiki=github.GithubObject.NotSet, has_downloads=github.GithubObject.NotSet, default_branch=github.GithubObject.NotSet): """ :calls: `PATCH /repos/:owner/:repo <http://developer.github.com/v3/repos>`_ :param name: string @@ -1015,6 +1035,8 @@ class Repository(github.GithubObject.CompletableGithubObject): :param default_branch: string :rtype: None """ + if name is None: + name = self.name assert isinstance(name, (str, unicode)), name assert description is github.GithubObject.NotSet or isinstance(description, (str, unicode)), description assert homepage is github.GithubObject.NotSet or isinstance(homepage, (str, unicode)), homepage @@ -1090,6 +1112,20 @@ class Repository(github.GithubObject.CompletableGithubObject): ) return github.Branch.Branch(self._requester, headers, data, completed=True) + def get_protected_branch(self, branch): + """ + :calls: `GET /repos/:owner/:repo/branches/:branch <https://developer.github.com/v3/repos/#response-10>`_ + :param branch: string + :rtype: :class:`github.Branch.Branch` + """ + assert isinstance(branch, (str, unicode)), branch + headers, data = self._requester.requestJsonAndCheck( + "GET", + self.url + "/branches/" + branch, + headers={'Accept': 'application/vnd.github.loki-preview+json'} + ) + return github.Branch.Branch(self._requester, headers, data, completed=True) + def get_branches(self): """ :calls: `GET /repos/:owner/:repo/branches <http://developer.github.com/v3/repos>`_ @@ -1214,8 +1250,163 @@ class Repository(github.GithubObject.CompletableGithubObject): self.url + "/contents" + path, parameters=url_parameters ) + if isinstance(data, list): + return [ + github.ContentFile.ContentFile(self._requester, headers, item, completed=False) + for item in data + ] return github.ContentFile.ContentFile(self._requester, headers, data, completed=True) + def create_file(self, path, message, content, + branch=github.GithubObject.NotSet, + committer=github.GithubObject.NotSet, + author=github.GithubObject.NotSet): + """Create a file in this repository. + :calls: `PUT /repos/:owner/:repo/contents/:path <http://developer.github.com/v3/repos/contents#create-a-file>`_ + :param path: string, (required), path of the file in the repository + :param message: string, (required), commit message + :param content: string, (required), the actual data in the file + :param branch: string, (optional), branch to create the commit on. Defaults to the default branch of the repository + :param committer: dict, (optional), if no information is given the authenticated user's information will be used. You must specify both a name and email. + :param author: dict, (optional), if omitted this will be filled in with committer information. If passed, you must specify both a name and email. + :rtype: { + 'content': :class:`ContentFile <github.ContentFile.ContentFile>`:, + 'commit': :class:`Commit <github.Commit.Commit>`} + """ + assert isinstance(path, (str, unicode)), \ + 'path must be str/unicode object' + assert isinstance(message, (str, unicode)), \ + 'message must be str/unicode object' + assert isinstance(content, (str, unicode)), \ + 'content must be a str/unicode object' + assert branch is github.GithubObject.NotSet \ + or isinstance(branch, (str, unicode)), \ + 'branch must be a str/unicode object' + assert author is github.GithubObject.NotSet \ + or isinstance(author, github.InputGitAuthor), \ + 'author must be a github.InputGitAuthor object' + assert committer is github.GithubObject.NotSet \ + or isinstance(committer, github.InputGitAuthor), \ + 'committer must be a github.InputGitAuthor object' + + if atLeastPython3: + content = b64encode(content.encode('utf-8')).decode('utf-8') + else: + if isinstance(content, unicode): + content = content.encode('utf-8') + content = b64encode(content) + put_parameters = {'message': message, 'content': content} + + if branch is not github.GithubObject.NotSet: + put_parameters['branch'] = branch + if author is not github.GithubObject.NotSet: + put_parameters["author"] = author._identity + if committer is not github.GithubObject.NotSet: + put_parameters["committer"] = committer._identity + + headers, data = self._requester.requestJsonAndCheck( + "PUT", + self.url + "/contents" + path, + input=put_parameters + ) + + return {'content': github.ContentFile.ContentFile(self._requester, headers, data["content"], completed=False), + 'commit': github.Commit.Commit(self._requester, headers, data["commit"], completed=True)} + + def update_file(self, path, message, content, sha, + branch=github.GithubObject.NotSet, + committer=github.GithubObject.NotSet, + author=github.GithubObject.NotSet): + """This method updates a file in a repository + :calls: `PUT /repos/:owner/:repo/contents/:path <http://developer.github.com/v3/repos/contents#update-a-file>`_ + :param path: string, Required. The content path. + :param message: string, Required. The commit message. + :param content: string, Required. The updated file content, Base64 encoded. + :param sha: string, Required. The blob SHA of the file being replaced. + :param branch: string. The branch name. Default: the repository’s default branch (usually master) + :rtype: { + 'content': :class:`ContentFile <github.ContentFile.ContentFile>`:, + 'commit': :class:`Commit <github.Commit.Commit>`} + """ + assert isinstance(path, (str, unicode)), \ + 'path must be str/unicode object' + assert isinstance(message, (str, unicode)), \ + 'message must be str/unicode object' + assert isinstance(content, (str, unicode)), \ + 'content must be a str/unicode object' + assert isinstance(sha, (str, unicode)), \ + 'sha must be a str/unicode object' + assert branch is github.GithubObject.NotSet \ + or isinstance(branch, (str, unicode)), \ + 'branch must be a str/unicode object' + assert author is github.GithubObject.NotSet \ + or isinstance(author, github.InputGitAuthor), \ + 'author must be a github.InputGitAuthor object' + assert committer is github.GithubObject.NotSet \ + or isinstance(committer, github.InputGitAuthor), \ + 'committer must be a github.InputGitAuthor object' + + if atLeastPython3: + content = b64encode(content.encode('utf-8')).decode('utf-8') + else: + if isinstance(content, unicode): + content = content.encode('utf-8') + content = b64encode(content) + + put_parameters = {'message': message, 'content': content, + 'sha': sha} + + if branch is not github.GithubObject.NotSet: + put_parameters['branch'] = branch + if author is not github.GithubObject.NotSet: + put_parameters["author"] = author._identity + if committer is not github.GithubObject.NotSet: + put_parameters["committer"] = committer._identity + + headers, data = self._requester.requestJsonAndCheck( + "PUT", + self.url + "/contents" + path, + input=put_parameters + ) + + return {'commit': github.Commit.Commit(self._requester, headers, data["commit"], completed=True), + 'content': github.ContentFile.ContentFile(self._requester, headers, data["content"], completed=False)} + + def delete_file(self, path, message, sha, + branch=github.GithubObject.NotSet): + """This method delete a file in a repository + :calls: `DELETE /repos/:owner/:repo/contents/:path <https://developer.github.com/v3/repos/contents/#delete-a-file>`_ + :param path: string, Required. The content path. + :param message: string, Required. The commit message. + :param sha: string, Required. The blob SHA of the file being replaced. + :param branch: string. The branch name. Default: the repository’s default branch (usually master) + :rtype: { + 'content': :class:`null <github.GithubObject.NotSet>`:, + 'commit': :class:`Commit <github.Commit.Commit>`} + """ + assert isinstance(path, (str, unicode)), \ + 'path must be str/unicode object' + assert isinstance(message, (str, unicode)), \ + 'message must be str/unicode object' + assert isinstance(sha, (str, unicode)), \ + 'sha must be a str/unicode object' + assert branch is github.GithubObject.NotSet \ + or isinstance(branch, (str, unicode)), \ + 'branch must be a str/unicode object' + + url_parameters = {'message': message, 'sha': sha} + if branch is not github.GithubObject.NotSet: + url_parameters['branch'] = branch + + headers, data = self._requester.requestJsonAndCheck( + "DELETE", + self.url + "/contents" + path, + input=url_parameters + ) + + return {'commit': github.Commit.Commit(self._requester, headers, data["commit"], completed=True), + 'content': github.GithubObject.NotSet} + def get_dir_contents(self, path, ref=github.GithubObject.NotSet): """ :calls: `GET /repos/:owner/:repo/contents/:path <http://developer.github.com/v3/repos/contents>`_ @@ -1457,14 +1648,14 @@ class Repository(github.GithubObject.CompletableGithubObject): assert creator is github.GithubObject.NotSet or isinstance(creator, github.NamedUser.NamedUser) or isinstance(creator, (str, unicode)), creator url_parameters = dict() if milestone is not github.GithubObject.NotSet: - if isinstance(milestone, str): + if isinstance(milestone, (str, unicode)): url_parameters["milestone"] = milestone else: url_parameters["milestone"] = milestone._identity if state is not github.GithubObject.NotSet: url_parameters["state"] = state if assignee is not github.GithubObject.NotSet: - if isinstance(assignee, str): + if isinstance(assignee, (str, unicode)): url_parameters["assignee"] = assignee else: url_parameters["assignee"] = assignee._identity @@ -1479,7 +1670,7 @@ class Repository(github.GithubObject.CompletableGithubObject): if since is not github.GithubObject.NotSet: url_parameters["since"] = since.strftime("%Y-%m-%dT%H:%M:%SZ") if creator is not github.GithubObject.NotSet: - if isinstance(creator, str): + if isinstance(creator, (str, unicode)): url_parameters["creator"] = creator else: url_parameters["creator"] = creator._identity @@ -1664,19 +1855,21 @@ class Repository(github.GithubObject.CompletableGithubObject): ) return github.PullRequest.PullRequest(self._requester, headers, data, completed=True) - def get_pulls(self, state=github.GithubObject.NotSet, sort=github.GithubObject.NotSet, direction=github.GithubObject.NotSet, base=github.GithubObject.NotSet): + def get_pulls(self, state=github.GithubObject.NotSet, sort=github.GithubObject.NotSet, direction=github.GithubObject.NotSet, base=github.GithubObject.NotSet, head=github.GithubObject.NotSet): """ :calls: `GET /repos/:owner/:repo/pulls <http://developer.github.com/v3/pulls>`_ :param state: string :param sort: string :param direction: string :param base: string + :param head: string :rtype: :class:`github.PaginatedList.PaginatedList` of :class:`github.PullRequest.PullRequest` """ assert state is github.GithubObject.NotSet or isinstance(state, (str, unicode)), state assert sort is github.GithubObject.NotSet or isinstance(sort, (str, unicode)), sort assert direction is github.GithubObject.NotSet or isinstance(direction, (str, unicode)), direction assert base is github.GithubObject.NotSet or isinstance(base, (str, unicode)), base + assert head is github.GithubObject.NotSet or isinstance(head, (str, unicode)), head url_parameters = dict() if state is not github.GithubObject.NotSet: url_parameters["state"] = state @@ -1686,6 +1879,8 @@ class Repository(github.GithubObject.CompletableGithubObject): url_parameters["direction"] = direction if base is not github.GithubObject.NotSet: url_parameters["base"] = base + if head is not github.GithubObject.NotSet: + url_parameters["head"] = head return github.PaginatedList.PaginatedList( github.PullRequest.PullRequest, self._requester, @@ -1779,7 +1974,7 @@ class Repository(github.GithubObject.CompletableGithubObject): "GET", self.url + "/stats/contributors" ) - if data == {}: + if not data: return None else: return [ @@ -1796,7 +1991,7 @@ class Repository(github.GithubObject.CompletableGithubObject): "GET", self.url + "/stats/commit_activity" ) - if data == {}: + if not data: return None else: return [ @@ -1813,7 +2008,7 @@ class Repository(github.GithubObject.CompletableGithubObject): "GET", self.url + "/stats/code_frequency" ) - if data == {}: + if not data: return None else: return [ @@ -1830,7 +2025,7 @@ class Repository(github.GithubObject.CompletableGithubObject): "GET", self.url + "/stats/participation" ) - if data == {}: + if not data: return None else: return github.StatsParticipation.StatsParticipation(self._requester, headers, data, completed=True) @@ -1844,7 +2039,7 @@ class Repository(github.GithubObject.CompletableGithubObject): "GET", self.url + "/stats/punch_card" ) - if data == {}: + if not data: return None else: return github.StatsPunchCard.StatsPunchCard(self._requester, headers, data, completed=True) @@ -1897,7 +2092,7 @@ class Repository(github.GithubObject.CompletableGithubObject): self.url + "/releases/" + str(id) ) return github.GitRelease.GitRelease(self._requester, headers, data, completed=True) - elif isinstance(id, str): + elif isinstance(id, (str, unicode)): headers, data = self._requester.requestJsonAndCheck( "GET", self.url + "/releases/tags/" + id @@ -2007,6 +2202,38 @@ class Repository(github.GithubObject.CompletableGithubObject): else: return github.Commit.Commit(self._requester, headers, data, completed=True) + def protect_branch(self, branch, enabled, enforcement_level=github.GithubObject.NotSet, contexts=github.GithubObject.NotSet): + """ + :calls: `PATCH /repos/:owner/:repo/branches/:branch <https://developer.github.com/v3/repos/#enabling-and-disabling-branch-protection>`_ + :param branch: string + :param enabled: boolean + :param enforcement_level: string + :param contexts: list of strings + :rtype: None + """ + + assert isinstance(branch, (str, unicode)) + assert isinstance(enabled, bool) + assert enforcement_level is github.GithubObject.NotSet or isinstance(enforcement_level, (str, unicode)), enforcement_level + assert contexts is github.GithubObject.NotSet or all(isinstance(element, (str, unicode)) or isinstance(element, (str, unicode)) for element in contexts), contexts + + post_parameters = { + "protection": {} + } + if enabled is not github.GithubObject.NotSet: + post_parameters["protection"]["enabled"] = enabled + if enforcement_level is not github.GithubObject.NotSet: + post_parameters["protection"]["required_status_checks"] = {} + post_parameters["protection"]["required_status_checks"]["enforcement_level"] = enforcement_level + if contexts is not github.GithubObject.NotSet: + post_parameters["protection"]["required_status_checks"]["contexts"] = contexts + headers, data = self._requester.requestJsonAndCheck( + "PATCH", + self.url + "/branches/" + branch, + input=post_parameters, + headers={'Accept': 'application/vnd.github.loki-preview+json'} + ) + def remove_from_collaborators(self, collaborator): """ :calls: `DELETE /repos/:owner/:repo/collaborators/:user <http://developer.github.com/v3/repos/collaborators>`_ @@ -2130,6 +2357,7 @@ class Repository(github.GithubObject.CompletableGithubObject): self._stargazers_url = github.GithubObject.NotSet self._statuses_url = github.GithubObject.NotSet self._subscribers_url = github.GithubObject.NotSet + self._subscribers_count = github.GithubObject.NotSet self._subscription_url = github.GithubObject.NotSet self._svn_url = github.GithubObject.NotSet self._tags_url = github.GithubObject.NotSet @@ -2265,6 +2493,8 @@ class Repository(github.GithubObject.CompletableGithubObject): self._statuses_url = self._makeStringAttribute(attributes["statuses_url"]) if "subscribers_url" in attributes: # pragma no branch self._subscribers_url = self._makeStringAttribute(attributes["subscribers_url"]) + if "subscribers_count" in attributes: # pragma no branch + self._subscribers_count = self._makeIntAttribute(attributes["subscribers_count"]) if "subscription_url" in attributes: # pragma no branch self._subscription_url = self._makeStringAttribute(attributes["subscription_url"]) if "svn_url" in attributes: # pragma no branch diff --git a/lib/github/RepositoryKey.py b/lib/github/RepositoryKey.py index 261bbfd3b..847b7f82b 100644 --- a/lib/github/RepositoryKey.py +++ b/lib/github/RepositoryKey.py @@ -9,7 +9,8 @@ # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # Copyright 2013 martinqt <m.ki2@laposte.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -38,6 +39,9 @@ class RepositoryKey(github.GithubObject.CompletableGithubObject): github.GithubObject.CompletableGithubObject.__init__(self, requester, headers, attributes, completed) self.__repoUrl = repoUrl + def __repr__(self): + return self.get__repr__({"id": self._id.value}) + @property def __customUrl(self): return self.__repoUrl + "/keys/" + str(self.id) diff --git a/lib/github/Requester.py b/lib/github/Requester.py index 720d9d653..9fd77b163 100644 --- a/lib/github/Requester.py +++ b/lib/github/Requester.py @@ -15,7 +15,8 @@ # Copyright 2013 Mark Roddy <markroddy@gmail.com> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -186,7 +187,7 @@ class Requester: cls = GithubException.TwoFactorException # pragma no cover (Should be covered) elif status == 403 and output.get("message").startswith("Missing or invalid User Agent string"): cls = GithubException.BadUserAgentException - elif status == 403 and output.get("message").startswith("API Rate Limit Exceeded"): + elif status == 403 and output.get("message").lower().startswith("api rate limit exceeded"): cls = GithubException.RateLimitExceededException elif status == 404 and output.get("message") == "Not Found": cls = GithubException.UnknownObjectException @@ -263,6 +264,7 @@ class Requester: return status, responseHeaders, output def __requestRaw(self, cnx, verb, url, requestHeaders, input): + original_cnx = cnx if cnx is None: cnx = self.__createConnection() else: @@ -284,6 +286,9 @@ class Requester: self.__log(verb, url, requestHeaders, input, status, responseHeaders, output) + if status == 301 and 'location' in responseHeaders: + return self.__requestRaw(original_cnx, verb, responseHeaders['location'], requestHeaders, input) + return status, responseHeaders, output def __authenticate(self, url, requestHeaders, parameters): @@ -333,7 +338,10 @@ class Requester: headers = {} if url.username and url.password: auth = '%s:%s' % (url.username, url.password) - headers['Proxy-Authorization'] = 'Basic ' + base64.b64encode(auth) + if atLeastPython3 and isinstance(auth, str): + headers['Proxy-Authorization'] = 'Basic ' + base64.b64encode(auth.encode()).decode() + else: + headers['Proxy-Authorization'] = 'Basic ' + base64.b64encode(auth) conn.set_tunnel(self.__hostname, self.__port, headers) else: conn = self.__connectionClass(self.__hostname, self.__port, **kwds) diff --git a/lib/github/Stargazer.py b/lib/github/Stargazer.py index 4f261c64c..6fca6a291 100644 --- a/lib/github/Stargazer.py +++ b/lib/github/Stargazer.py @@ -13,7 +13,8 @@ # Copyright 2013 martinqt <m.ki2@laposte.net> # # Copyright 2015 Dan Vanderkam <danvdk@gmail.com> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -38,6 +39,10 @@ class Stargazer(github.GithubObject.NonCompletableGithubObject): This class represents Stargazers with the date of starring as returned by https://developer.github.com/v3/activity/starring/#alternative-response-with-star-creation-timestamps """ + + def __repr__(self): + return self.get__repr__({"user": self._user.value._login.value}) + @property def starred_at(self): """ diff --git a/lib/github/StatsCodeFrequency.py b/lib/github/StatsCodeFrequency.py index 0a1981040..0635c0720 100755 --- a/lib/github/StatsCodeFrequency.py +++ b/lib/github/StatsCodeFrequency.py @@ -4,7 +4,8 @@ # # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # diff --git a/lib/github/StatsCommitActivity.py b/lib/github/StatsCommitActivity.py index 14bd75376..ded0b93c7 100755 --- a/lib/github/StatsCommitActivity.py +++ b/lib/github/StatsCommitActivity.py @@ -4,7 +4,8 @@ # # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # diff --git a/lib/github/StatsContributor.py b/lib/github/StatsContributor.py index b869b0504..fda45b03b 100755 --- a/lib/github/StatsContributor.py +++ b/lib/github/StatsContributor.py @@ -4,7 +4,8 @@ # # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # diff --git a/lib/github/StatsParticipation.py b/lib/github/StatsParticipation.py index 356974eed..b4acd4498 100755 --- a/lib/github/StatsParticipation.py +++ b/lib/github/StatsParticipation.py @@ -4,7 +4,8 @@ # # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # diff --git a/lib/github/StatsPunchCard.py b/lib/github/StatsPunchCard.py index 4c5de41e2..733d4a81b 100755 --- a/lib/github/StatsPunchCard.py +++ b/lib/github/StatsPunchCard.py @@ -4,7 +4,8 @@ # # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # diff --git a/lib/github/Status.py b/lib/github/Status.py index 46e392126..67a6b8048 100644 --- a/lib/github/Status.py +++ b/lib/github/Status.py @@ -4,7 +4,8 @@ # # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -29,6 +30,9 @@ class Status(github.GithubObject.NonCompletableGithubObject): This class represents status as defined in https://status.github.com/api """ + def __repr__(self): + return self.get__repr__({"status": self._status.value}) + @property def status(self): """ diff --git a/lib/github/StatusMessage.py b/lib/github/StatusMessage.py index c0c99903c..ec081e393 100644 --- a/lib/github/StatusMessage.py +++ b/lib/github/StatusMessage.py @@ -4,7 +4,8 @@ # # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -29,6 +30,9 @@ class StatusMessage(github.GithubObject.NonCompletableGithubObject): This class represents status messages as defined in https://status.github.com/api """ + def __repr__(self): + return self.get__repr__({"body": self._body.value}) + @property def body(self): """ diff --git a/lib/github/Tag.py b/lib/github/Tag.py index 5f19363fd..c837c74e2 100644 --- a/lib/github/Tag.py +++ b/lib/github/Tag.py @@ -8,7 +8,8 @@ # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # Copyright 2013 martinqt <m.ki2@laposte.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -35,6 +36,12 @@ class Tag(github.GithubObject.NonCompletableGithubObject): This class represents Tags. The reference can be found here http://developer.github.com/v3/git/tags/ """ + def __repr__(self): + return self.get__repr__({ + "name": self._name.value, + "commit": self._commit.value + }) + @property def commit(self): """ diff --git a/lib/github/Team.py b/lib/github/Team.py index e1971f5f2..9491020ba 100644 --- a/lib/github/Team.py +++ b/lib/github/Team.py @@ -8,7 +8,8 @@ # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # Copyright 2013 martinqt <m.ki2@laposte.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -37,6 +38,9 @@ class Team(github.GithubObject.CompletableGithubObject): This class represents Teams. The reference can be found here http://developer.github.com/v3/orgs/teams/ """ + def __repr__(self): + return self.get__repr__({"id": self._id.value, "name": self._name.value}) + @property def id(self): """ @@ -128,7 +132,7 @@ class Team(github.GithubObject.CompletableGithubObject): :rtype: None """ assert isinstance(member, github.NamedUser.NamedUser), member - headers, data = self._requester.requestjsonandcheck( + headers, data = self._requester.requestJsonAndCheck( "PUT", self.url + "/memberships/" + member._identity ) @@ -145,6 +149,23 @@ class Team(github.GithubObject.CompletableGithubObject): self.url + "/repos/" + repo._identity ) + def set_repo_permission(self, repo, permission): + """ + :calls: `PUT /teams/:id/repos/:org/:repo <http://developer.github.com/v3/orgs/teams>`_ + :param repo: :class:`github.Repository.Repository` + :param permission: string + :rtype: None + """ + assert isinstance(repo, github.Repository.Repository), repo + put_parameters = { + "permission": permission, + } + headers, data = self._requester.requestJsonAndCheck( + "PUT", + self.url + "/repos/" + repo._identity, + input=put_parameters + ) + def delete(self): """ :calls: `DELETE /teams/:id <http://developer.github.com/v3/orgs/teams>`_ diff --git a/lib/github/UserKey.py b/lib/github/UserKey.py index cfc45823a..0f47cf086 100644 --- a/lib/github/UserKey.py +++ b/lib/github/UserKey.py @@ -8,7 +8,8 @@ # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # Copyright 2013 martinqt <m.ki2@laposte.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -33,6 +34,9 @@ class UserKey(github.GithubObject.CompletableGithubObject): This class represents UserKeys. The reference can be found here http://developer.github.com/v3/users/keys/ """ + def __repr__(self): + return self.get__repr__({"id": self._id.value, "title": self._title.value}) + @property def id(self): """ diff --git a/lib/github/__init__.py b/lib/github/__init__.py index 18a0e6127..550c9afbf 100644 --- a/lib/github/__init__.py +++ b/lib/github/__init__.py @@ -6,7 +6,8 @@ # Copyright 2012 Zearin <zearin@gonk.net> # # Copyright 2013 Vincent Jacques <vincent@vincent-jacques.net> # # # -# This file is part of PyGithub. http://jacquev6.github.com/PyGithub/ # +# This file is part of PyGithub. # +# http://pygithub.github.io/PyGithub/v1/index.html # # # # PyGithub is free software: you can redistribute it and/or modify it under # # the terms of the GNU Lesser General Public License as published by the Free # @@ -33,8 +34,8 @@ All classes inherit from :class:`github.GithubObject.GithubObject`. import logging -from MainClass import Github -from GithubException import GithubException, BadCredentialsException, UnknownObjectException, BadUserAgentException, RateLimitExceededException, BadAttributeException, TwoFactorException +from MainClass import Github, GithubIntegration +from GithubException import GithubException, BadCredentialsException, UnknownObjectException, BadUserAgentException, RateLimitExceededException, BadAttributeException from InputFileContent import InputFileContent from InputGitAuthor import InputGitAuthor from InputGitTreeElement import InputGitTreeElement diff --git a/lib/imdb/Character.py b/lib/imdb/Character.py index bc225d6c4..5a5239af7 100644 --- a/lib/imdb/Character.py +++ b/lib/imdb/Character.py @@ -18,7 +18,7 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ from copy import deepcopy diff --git a/lib/imdb/Company.py b/lib/imdb/Company.py index 64c434f7a..5e05c840d 100644 --- a/lib/imdb/Company.py +++ b/lib/imdb/Company.py @@ -18,7 +18,7 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ from copy import deepcopy diff --git a/lib/imdb/Movie.py b/lib/imdb/Movie.py index b248bf8c2..5cdcde650 100644 --- a/lib/imdb/Movie.py +++ b/lib/imdb/Movie.py @@ -18,7 +18,7 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ from copy import deepcopy diff --git a/lib/imdb/Person.py b/lib/imdb/Person.py index 0dfdf20c1..6e3e46231 100644 --- a/lib/imdb/Person.py +++ b/lib/imdb/Person.py @@ -18,7 +18,7 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ from copy import deepcopy diff --git a/lib/imdb/__init__.py b/lib/imdb/__init__.py index fd63921f1..92c40ec3c 100644 --- a/lib/imdb/__init__.py +++ b/lib/imdb/__init__.py @@ -6,7 +6,7 @@ a person from the IMDb database. It can fetch data through different media (e.g.: the IMDb web pages, a SQL database, etc.) -Copyright 2004-2014 Davide Alberani <da@erlug.linux.it> +Copyright 2004-2016 Davide Alberani <da@erlug.linux.it> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -20,12 +20,12 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ __all__ = ['IMDb', 'IMDbError', 'Movie', 'Person', 'Character', 'Company', 'available_access_systems'] -__version__ = VERSION = '5.1dev20150705' +__version__ = VERSION = '5.2dev20161118' # Import compatibility module (importing it is enough). import _compat diff --git a/lib/imdb/_compat.py b/lib/imdb/_compat.py index 4625ec0c4..03b11564e 100644 --- a/lib/imdb/_compat.py +++ b/lib/imdb/_compat.py @@ -18,7 +18,7 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ # TODO: now we're heavily using the 'logging' module, which was not diff --git a/lib/imdb/_exceptions.py b/lib/imdb/_exceptions.py index 92f09245c..55788a207 100644 --- a/lib/imdb/_exceptions.py +++ b/lib/imdb/_exceptions.py @@ -17,7 +17,7 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ import logging diff --git a/lib/imdb/_logging.py b/lib/imdb/_logging.py index 5c9b1957d..2b8a286a0 100644 --- a/lib/imdb/_logging.py +++ b/lib/imdb/_logging.py @@ -17,7 +17,7 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ import logging diff --git a/lib/imdb/helpers.py b/lib/imdb/helpers.py index 44b454ef2..f22061429 100644 --- a/lib/imdb/helpers.py +++ b/lib/imdb/helpers.py @@ -19,7 +19,7 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ # XXX: find better names for the functions in this modules. diff --git a/lib/imdb/linguistics.py b/lib/imdb/linguistics.py index 9e39cc326..d53597b2e 100644 --- a/lib/imdb/linguistics.py +++ b/lib/imdb/linguistics.py @@ -20,7 +20,7 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ # List of generic articles used when the language of the title is unknown (or diff --git a/lib/imdb/locale/__init__.py b/lib/imdb/locale/__init__.py index 6c83abc22..9bc2e4668 100644 --- a/lib/imdb/locale/__init__.py +++ b/lib/imdb/locale/__init__.py @@ -18,7 +18,7 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ import gettext diff --git a/lib/imdb/locale/ar/LC_MESSAGES/imdbpy.mo b/lib/imdb/locale/ar/LC_MESSAGES/imdbpy.mo new file mode 100644 index 0000000000000000000000000000000000000000..c3687cfaacbea039fd89fa4d096562df7a070a97 GIT binary patch literal 3037 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 Ke6#JwN(KP?zqN?~ literal 0 HcmV?d00001 diff --git a/lib/imdb/locale/bg/LC_MESSAGES/imdbpy.mo b/lib/imdb/locale/bg/LC_MESSAGES/imdbpy.mo new file mode 100644 index 0000000000000000000000000000000000000000..07eef0df19533305eba19066af159daf88022775 GIT binary patch literal 5655 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 literal 0 HcmV?d00001 diff --git a/lib/imdb/locale/de/LC_MESSAGES/imdbpy.mo b/lib/imdb/locale/de/LC_MESSAGES/imdbpy.mo new file mode 100644 index 0000000000000000000000000000000000000000..879ff6efaf37bdf00d4d2923a3113e7d63d9376b GIT binary patch literal 4596 zcmca7#4?qEfq|iffq_AWfq`KUGl+-4G3*QstPBhcvFr>C><kPH$?OaaVhjun+3XAq zEDQ_`P3#N|JPZsB?d%K;d<+Z>Q=t6CQ1u(2{2lBJ47>~s3`e2nU1n!sU}IolxXI4I zz{$YC@PM6xfs28G;T4qr3N@dB17bfb2ShzL2LnimK?us1=76|E2`aA6!N9=Hz`$U@ z0dbcN2gF`a4v721IUx4ML;0Cd^9!N;a;Q1=P;)z==Ji6&o5{hz01BdIQ2A9*ee0q2 zY=`PU%E7?E&%nTN4yx`R)SSmq`aM+NZ>V|9oDhGob3**d!wK=HFqALD330bFCnTJ7 zIU)YDfU0wa(tc2N;ZQmesy`cQei2lEIaI!h6B52XQ29wvdOB3y9H{(qs5xt)@>`(x z?S#r7g36zO(&wS{HK;r9azf(a71Z7zoRIin;)3{>3rY)eLDWfbLE=%33!=`53lbhy zToC^|L-~GC_2E$QRH!)xP`aFpfkB*sfuRn{p9VE=E*B(zmO{n1azVm(57eAvQ2Q@J z?YRr3pF!2X=Yqu7H!eu{{pNzi2O~GceSF*ye@SvPFfcJNFeq>{FbFX)Fer0F(vKN8 zM4uHm#J>*Qko@BZrDLJ;IouF?ilF9|L-}>w3=I1j85o+mA@(}+Li7jlGB8LmFfhdP zLc*_t7h--3FU0;yyb$$Ec_HDtiI;&vkb!|=7cV3{&OqhwK+Su}3-Qkns5mnp#9RSB zh`VI?Am%9YLHw)92MI?5DBlXocZAa3d=T?Oq2?s;LEM+a2MLcVsCf-g^E;s8y-@v= zq4Zp+_;Nmof7bFr(#I}7NcbLx>OT%O?+nzQD^PWh_#ol&8mjIe)IH4n5dZP>L);_H z5AmlWKg3)$euzJ{_#yTgLe*LEL;UT)5AmND)SOU$Ncbl5L&B*Ts;?TVz6EMtAJm?y zQ1Mw%dI?l~9hBY-HFpQpo+D8Hd8qttsQC||`d>ob_Ze!>PpCM90K^|$0+9S6D8RrV z$-uy%DFAW5GnDodfcP^Ksy;~o65g3m`7)?{Bb46-rKdvGEf8P;S;epx%HJmd35U~A z^DYZ8Fo-fRFx-OjKS1TbL-qZKiVFxr+$9dBl>{N~&=rKFTPs0`f4l@C@fIiuiMMbl zKSq#&L70JoAqlFlOc0Xpngt>GbD|(5|4b2t#PedPzU5H!*Fxp@3PSvIMiAnUyHNE{ zq3T~k?R^i*REfzY`9%zg$t6Xp#l;MXDJhvHnfZB%Il7s7Y57IDi69<BVs>INLt;*j zZb4CgN@;RQF+*Z*VpV>gZc%DkW@>pcLt;^hZc1iRD#Wm&lFa0sREEUjg4E;^-6D_~ z42h*FnfbbfrHMJ2C6x?`<%vZp5VDvdDKkI4D6t@;k|8Oxs3b!-C9xzG%FIj5O=U>R z&(8)E#SBTMDe0*t3`wQMnRy`hCMV{m79}zyCl;45Bqx@nrso$`G9;%Km1L%6g80bn zVkAyxejY<|Mq*xGY7RqkeraAwQD$l}lvT-)oL`n&#E@K+ng|M;l*E$6q{QM>2C%zQ zQj3#|G7G@Ll9E|mQk0ogT9RK>%#c!+0>Z@%sRfzE`6;O|TDK&#BnQreOBaK~B`=*J zEwQkeAuTf}7ZwO<`8hfH<*6yUNtFx`b}>U*ei1`jQGPB%dTL%#YB57bYGO)pMt%uH zMrvkyMhQb^UP)>ZC@K)FVh|G&T_E3Pr&gBd7o`+4<Rs>$mnNpC!Wkf;;?$y)%;IE* zoYcJZk_?8N%)D%doXnEcqQsKYqEv>Q{A5rh<`pyK=9gurGJvBc4;1lvnaSC@AZ{^3 zUVcexF+*NyZc=IyLqSe{2?Hdxq!uw0mgbkFLdaqUP<qHqXDCWd2f3>#H77N(I297< z$edz^qSV~P>{Q+SG=`$oypqHmkVtW9P6-1f)j=uU#L|+C{2~}HJ2NkZp{O*kBr`V^ zMi(;_=a=TCloTZ<XEPKRWajDSr=`Iey5N8<F38VMOHI+u%wqsEi$PRraVkS`Nn%N9 zv2K2vZem$tW=>*KP9``B7nguiGy{Yx2GMyXx&^64pj?w$1mPDm6qhD};~&H-D9B08 zP0cG|C@zIDiy4Y5^YROdGm9BY64P@)iJGA#BQY<#m;vkouwI6e`~qDgQv=<i#Jp^V zlA^?vRNdUfA`mmPEHjazq>Q07FD11|HzzYWHLo}o;-RvPVurHJl+=6(3D0_9G2P;l z#Jm(x9xTf&E=|nQO-)Nn1?B59a3q#zmSixL7l9H0LuG1W5ko*xeikS%d#31yr4|)u z=I2={c>1~|1w?WM_=o5^gR)9yex9yNVo9o%f{}rtfv$nEuAzm3p^25TskVWEfdN;5 zziv=!Stdv`LY)~%osortk%5(o0YsfoVsVLXNKs;5aSkZ|St$f17Gxx*=5YByQ?71E zYGSUHf_rLFZepH-Mn*|Vft9{~d3m{B2}oCFT55$}a(=FUN+QUw@rgM}sYQu-nfjTz zDM<yD`mhY3pOUJt$>r<m>x%G`p`HPkbADb)YF>$MNM%8)l|o5sMTvevPGV-BwL)@6 zVo`BwiEU_zo2~^y9mvwO)FNHiyyX0p%)E3f1&gH25-ykg+{Da0D+P$PaMxQYq@=<* zy3YA2sa6W9c?c1|#N1RX1=qavoXp}3u7I4<qQo3sxBQ~qVk?Eb0x-AO*2r1`!nD=M zQ&6;3Fx0f>3MfiVOD!r&O@TY4*h-<aBu&?Xiy=5UBeAr&ATu>5wFrfina5C7T6}m( zNmWi}az?6xCsL^&P@GztlAm{YPi`uMV@^)0LI9{Z$u3D{a0HkB3PGt=sd=FIOwD6( zEGkiOfz<$xMJ1WpsW}Y6shK6Id1a|Z8HbnTl;mX=GdO}WjY1%}8aTY9gu$`2xGFU> zIU}z$FCB%Gn#bS-trs#=8QfEoN{dR0QxZ!`b734%sl(t@nw-Jlba+p4Mrsj*cYbk6 zY951oYH@PL;U#G$4Bn9H!6~&kwWJDUgEyp($V_F3f>aXOi6uzPR0b!I-qg&z%yfwI zyu=&^pTkQ)*^9v^F)xL|IUiIvxE2+cq~?GE6O`ifl0X^L1?)Jm0Y#~qNg(|mAe~@U zVJ-|VVJ^iCuF%Q>O6QeimZavuMDySb1*g>V)S?oQ8E%e&#SCuH`XMbdl_5PfEk7r{ zM4>D{4^%&-moTIq-c!T?;<`g>hk*Q|lA^@S5(baM+cHuaJmHlCoK?&amJjg)$co_P zjGV)Jii=Zo$`5ZVDgh;tf}+Iaj8ukT2%X2^lL)IKK%tSI%HRX4B78t41gMH&2nLlW z`9&qEV5c&G5;22cVsb`NW^x87PJ#<EOR7LImdD_jmz!FYU7DAkn#T~8n32Qak(if~ z19AsAeN}-<$f(Q`P{e>pP@KTRA_!7vgryc8-j<e@npXl!w556J(1d|0UXogro0*r& z5CpDDz(MAdnwg%Oba+oj5rb<{dTJ6VReBd?mSko#Kqv)AP=yZX6qPXeB$lRCrDh@+ zsd)^+h#JK&H7PX@T-RlnFd%cl_5~ys<)>t(7L_Pu<}pAy4DN{~C7F2)VW~xFhxeo> zB^G6;W|kB~sv3r1NX-JFib34G5?z->NPK}Jr=&Emq?jQT6s^$oQ<Q#qNgg<byTX~E z;4Ljq%*{>BOG_*+1_f|%PJVh~9)n9_UN$HPWR@_5fYV<{eu09KsewX4PGU(_W@-^g zZFpi)Y92UmrsgsDfub=p9puW?Jcf`khMY`r)(3eDl<>klf*HcVl?|9g%HR+_IC&%% nl`w>57MFrDj)H4iS}Le02`<S5xsV|@6I2+0+FM?U8AS{LcSU3k literal 0 HcmV?d00001 diff --git a/lib/imdb/locale/en/LC_MESSAGES/imdbpy.mo b/lib/imdb/locale/en/LC_MESSAGES/imdbpy.mo new file mode 100644 index 0000000000000000000000000000000000000000..be2d4a29a3b50cfe4f3b5ddab221823bdc558d11 GIT binary patch literal 13723 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 I1sTi%0F*!^>;M1& literal 0 HcmV?d00001 diff --git a/lib/imdb/locale/es/LC_MESSAGES/imdbpy.mo b/lib/imdb/locale/es/LC_MESSAGES/imdbpy.mo new file mode 100644 index 0000000000000000000000000000000000000000..814ffc5def499463296e86de27d542f1b7655d70 GIT binary patch literal 15620 zcmca7#4?qEfq}t^k%2*mfq_AX3&cZU8xaNuRt5$JTM-5Zb_NCpXAuSl4h9AWZxIFt zF$M;P5D^9jkd|x_1_oXR28J>b1_m|;28I?91_l-e28M2^x(Ol-3_J`B49i6r7-Sh3 z7`BKoFbFX)FkBL0VBlk5V0bFRz`)JG!0-jCjzN@xL4<*Ufmf7)fuDhaK?zD5i9*yn zi9+lP6or@@FA8yAjwr<55~z5kD8#*Oq7eJ~q5MUn5c^g`&Dkyr@#j%di2F~AGB5~( z+$YMwz{$YC@C<7I2T_QBL7oTsn_CQGj+hw4UL`24E(S4AM-1YABQc1%PGS)Ec#1*H z4}yxPi7_xJFfcIWiZL*VGcYjpi9!6iObimvTcPHhftr6u4C0@MVhjudpm2k#XBCH- zFCq?cm#jD>d^E)&;bkTc2`3kEh`)TqA?^zkhuE7Y4oL@f;t+q%7H41(WME)e235BM zs{aJkybDnJ9@O49P<y{Z`HT_}_i;%;+$#;`>q$WDx0isp$5R60-arXRxWz){lO-VT z&V{P0mVo%DSpwqE2@;TSpD6)x?>wkEE1~pOD1A_ZfkA?Sf#EFFyk}7TU!d}Tq4se~ zLd+MIWME)sU|>*y@>L`u<{L^v?6Z-CxXV!z;%<K^9W4oo=X6O(`puJs_^%nNzZ<G= znk2;iOC%xTzZR-*uOuYAjzI0X3e|rLYW^drzIRabzeCOAk%G8G3QDU<LF_e>g80Kq z3KITyQV{nCN<rc^ObU_?GobP{QV{p|OF`mc8kC*`HFuE|#NF$l>b6Tk;&Go8B)y)1 z@~=xl!t0?FBs||h?fnVW{}0M%mxkEGBMnh6Bn>fNRvKci8k9DWhJ=%qG{k*g(olau z=`g6eXs9`<P=0|lB;Ko}85krP7#Jo?L(=(LX^4A|Le0G*4T-NO(vWcbDGiBVMj42G z{4$Vm5|e@G(~^O>*HQ*zo{J15d}CxF{>hhtxUW<O5}#F2em9h!AOi{isZev4L-lQf z(mSB`?t_|t6l(rmsJ;&}knmuVh0sE>44@*DK}i<kJ`1S0yDY?>aHx1Dl&+A4xT{?j zl5S?mLj1c#77|~ZWFh|E50yVA3yF_QQ2lpgA^v*}RsR7>|AoqP$w9(PQV!xST{%d+ zI?F-A*+&i%UWsxLca+FM+}R4%HytX!T8@E1nt_2~A5{L99K^qG<RIbvM-JjnCV2?W zDG!kskcaq3K^_uMdQf>wsJw$b#Ge6Bb+Pgg|7AnzQh7)^sE6unhthpe^;6{`;kFp6 zZoNFjf7|6D;eS9L;{WqdeUIfC7_=A|7+%Rk!c9~G;x8!$Ncfm3Ffd3lFfjNiK+<cv z0wldQC_vKJEGT~yRNol|h&?Z$;@=e@;VP&IQLm;530FHsh&}#_5c9L4@@<L?42ld4 z4D%Er;dDR|5+3IiA@$I0DE&tfVvnp6L|#V;!nag{gqMdBBz%LEAoY5z5~Tj9R)Y9z zh7tpVJOcy6IweSWor9{s1vTd>)SNF+@qbX7QyCJEBFd0(P*H}s-%c44&XLLxcPA-B z>@QG;#D6uEZc>JXLnoBKP#F@RE1>3XfQlbbhWO(+)SQdT5O>~z+VcWRe}t<01=Yu* z0*Oa%6-YW1P=VCbVk!&_ObiSRx+;+HGEjlI&sGKE4-XZHJCmU5GF2e?AP1_xUIkKK z_dxYcfbwTU?U}Cv374fR5O;2c(g#%_;c-?45<j=0^ed>opHO@LL)8hXLj0|u3h{@Q zD#Ts-su1^@LB$=R;%=%84Eq=v7<^SB{^e1Js25U)xI<DM;(t|jh`(*rA@T324)I^S zIz(TQIs=0e0|P^aIwXJ0R)?gI^-%tKbx1mX45fdlL((y?1|<IFG$4FS4M=(6r2&cO z7!8PhB^nTOyEGvFo2LQs-wF+gxob5b>E{qs-)SiSu?EB+Z=mM>(17@lOB0fA1T`V{ zNNPgD$50dEFB?sWeg2w|a8HKvi!>qn+cY8m?uGIvYeMqlEKP`g+o0wj(uBn8DJcJz zCIf>C0|Ub&O-R0C*Mj(4RSOahW?B&c2WmmwlMEHl(}Ki9rxwJ&3!w5_wIKE#(1OIz z87+u=Z)!o@_d*L2UhlLZ{`mnlpGO-~PRMCP(wBoaB%J-UA^uO%hS*=O4bj)64e`e` zsQ5A{y-^!t{vK_J`;KWt^21$iNVtC2hJ-V}4#Yj;IuLit>OkU4O9x_~u@1z1Hyw!j zFsQmX9f<p~q53OzAokQl>1jF;|F4Cr->Cx$pJO_ZaJUV%=amj5oIgWp7F~!tM0Fwg zQb`x$UNv2aK094V`tsF<xGP8(;*KO;h<^)oA?ct>7vi2qU5I~X=tA7HP#5BkwNUkk zbRp&c8C{6~p6Wv4`K>M_-Tc>ugtxRF#9eB75Oa+6Amz5b9>lx^sC+3@zDW;aUY{Ps zz0>p{{+O)?3GdB%kaByc9s`3a0|Ub~J&1jO^dRn)(1++()Q7}_rar`7Hc)XteTYB9 z^datw)rW*bmOjM275Wf&Hbcex^&#fYg{ohs4|NArd?!@>A*i|Mp!QtVhlJ~0eMmZd z4(0!as$(~RxP#9C5|5$=5PQ@NAn~Pdz`!8Kz`$T<04aB34It^e)<2^9>;8ZZUv_ z$0-Ad`=1*?;`fsQBpjFwA@Rgz2oYB{gqUw^2nio2C_mH?;@%WPNVw-2LhLCvgt)WA z5aPamLx{U(K*hHkLc(Jolz+q!V()1~h`TRC&G~EyaW97v#J_S-THlBP+~0A4@*|BP z@ttJ^DL<N^;&Y+&IwOet_e1G(Q2H)Z{*4jD{=Y^L`&f-3=~)^|8yQ2~>uC%r2mFm8 z;gW0&36El9NO-mxL(<h^V@SB{H-?mNCygQQdu|Ld|3B1RQ4>fxp<@D(cQavNP-b9Y zh%<qNW0MKQzUd~A@Lg>JamNM|NP6350&&M7D18Yk{>TL4pLZsZaQg<O*-atoS^`RI znL^^<#uVaicT<SFlT9J|b4?-kmzqNC>4eJ9gz8&t3i1CsQw9b#1_p+MQ27_83=Ad= z3=E%4A?{W;gUIWcLHrkN1}Xo`q4XT6{6RBFK7U~b$)}v=kor{L91<Qu<`8qz%^~@( z&m5wDjyWX!_L?&=s53AyTs4R2{|nWxY{9^w$H2hgW&w%UQVWRxdMqI3O|f8L&|zR; z*bS9`VF3xJ{}vGc3tB?dt6D<BN7oY4-Zi&mV9;b>U`Vor#B+}&1A{gL1H&pyNc>#0 zgoN88ONjeFLHX=fka9#EN~>5w!qLzQl3!h{ApTCXg4o+>1&ODLQ1x@HAo*&g6(pZ+ zvV!D?oly0cpz^n@Ao<}jRR2FKh`TtgA?Ar%L--2T5c71bA@-SCL(-`&lpkyjiN|Ot zKfxN3ZZoVI7#JBC7@Djh?ryP$q_<_(5PP><L;QKk8q#jO50!sp4e{@1Ye>DrW&?2t zmkoq3W&`n;lno>v6m1~p7(;0XsJO2U#GH5=h`mKNko?hX1BvHO8%R1^09AJsYW@uy zh=1=u?R{zk@!v-qNV@uF!@$6hm|T)y1R{%4i;Ec&QxXeGGV}8o5>rw#L5##4-ORkS z{G!}M5RV}-B{#FUI5R)5m?1GeHLpZBF(n0}F|#N$FP$MVJFyr{=%y9r=js-v=A<ST zr-F@ONX*I6Ehx%QDNQaZW=PB_NiE7tEJ@YPNz6+xO-xVKElVu|nXH?ih9nCWEM`c| zO{~h#(=AFZ%S<gVW=PCS%*jvJ%}Grwfw7A+(=$MvOo&@`Q&I~Oi%N1+^GX;Ji;^=k z%TjgI^7Bg)(^DA|i!w_xa#Kq(lXY`b6Z04ni%L+Hm*}Qs7NsUb{0x!=^FRSrRFavT zlgf}-oSDavSX_{rT%ub93LA#R;^NHoywoBvqqrn74`v2PS1H)2Vur-hl+1kHy!_1K zR4BW!G%+W$q>>@AJh3PRLKZV5W#*?BB^G34CMV|T=H{1WrXs{D;j)#w1x1;8B@9WK zMI{-!DTyViP-b3YE}WTP0@9nGQpu2%pPvmTiW!pfOG@%{bqx&+bc+)6vKf*}Q_@pQ z7?MhhGxI>nAgNL}v8X69FFh3$e7fbCB^eCKiMgpoiBL*6F)u|oH77N>qzLRrq_9p- z%*)RM3#Jxi7U!p=>Xu}d<fJkn#KF!(aEnumGE<8&RKau=moOwJ7MEn^r6WfXTnH91 z$%!T46p>e&o0M9_kepbOnx0=&$&j2{RFavN3F0HOi;*~>^q!oNn3tED!;qYjSd^Gt z0x}^pFEux@1QHSG{LExfQcebiRdP;#ajI@IB;OS4mZavSmZVmc=;oy8rZXhx=j0bb zl0Sk8%S;fFqSQP{R%1xc&n-yIs|3>^Lv%r&MB){tmV<fu#USV8=jA~XXfZ=_eqKpY zA}FBp3o60cKsO^lCk3pkxTG{U6(!{IVM5UKnw(#nR|1Lx1_%q}fYPFp)Z$7|R!q(> z1DjKpTBKWAl*5o*lvq`%n_QHdl37yBkX)3S2r564ON)v?#X(ViPAWr6Vo73BVlkMJ z%8-(p2uqz1W*#_6rljUVf;KamAtkjqxhNA-@T6pdqOlZIW|XAn<fNu&CS`&%NJ?gU zW=Ud>ZgGBTUJ3&wd*&B0q+}K+>p}`B2)j75Diy>kDauSLEy*t`W=JVZ0b$*=)Wnj~ zqExT|TuWI>F+)maUSe)$GAJ*kr!s(imYM=Cz+mjmypnt{JHH4@WhUkz2P!nFGeBuj zz`>Z1bPVMc!&DT5OO3pAhScKX#L8lZ)CzEUm;*21(h>`c8K5qP(J8u_c`$Y{Lt17| zu5MmxdSVHrXaglHupl_AB!coiNC_mAD;d)Ab8_;_Q&V)4VC-UswEQB5w4&6)($u`< zO5LK=;)49V;#3Au+0Bqvnv<iO2r2G#b29U?AtIpQ)deRCaB={v0*hs&CZ-f;<d-m{ zr{;kIIXyo=t(YObD8Cp?mKNxOB^co9GE%_>c4lr$Qf6LCY6U}Po^DENS!zyx0jShr z$jmEAEdrJH2v#wO38_LrnKrW|HJ2efwX!_FD5aPoJ2NkZAtx~@H3vlI7l9IHB?GK_ zfisF3auSPEi$LiH!Ge}PIjMQ+B^eAknR%%oA{$KTg0eG1PG(7J5jg!Y<m4xVO8mTH zhMfGobX|}KbP>fFxOBrN0Z9l58EBG#Sq(P|%>anD+{E<6s?5ApXwVmA<d@_#<R)gP z>XsHT<R%uEq!z)FbZ%-&W?~*gZfX&zP)o@y2Bim31Qs*oX69w;!YY?ahFnnIfyopn zr{?4&=B4JB7VAPvB2cXZQi3E7DuTfE5kqc4Vj@FsX>n#Uay|gNJr7htgNXE^{L%t= ziq6YS&ejDfC}w~Z0eSglsW}YL0vkf<=BMc<XCxMYyi;6~Ur-E{28lz%3t6Z*HL)1f z7C~q(&P+)~R#65nZ1U66GLu1BwkWkEF%#4b$xKdVfQuGqf-*~fL24c-sLE4Qvs3d@ z81joU(=+oDb956kiy*ZtTm<ZIgaSwu<(Fin7U?3T6No^0eo=NYLqQ^_d@R;YFU?Fz zWhh830u@=1%vg|GRGgp30FF{!h*JtOL9MdP#2f|?tF#Ezasg-Wf}H#k1`wf}Us?iA zrUm&$B}Ivqso=7q0Mr%(`Jo^uF|ULH+&lmkWuTG*WMLvg44g{faRX_Wr4}(j7@&3! zQX)VR1XUK5$m&21k75*oR8Y-=DS~QzZem_yI;v1XQ9h_j1y!vj`N{b?4A90ADE5o< zON)|I;UWb^pmHsjp|CW+1SATMW>7;cFP)(%H90>wH#IK>noEmP(^894^O7N91m_hq z6s4wviV;{V6;}Ph_#lrlAag)EauZWhbrT`1>{L)e4~p2t9FRzHX-)|Pq=gElbQ4QU zGV+UHJa7?JRGL?knVZT0rHdJg6LS)aDs?k5i%UQ)mg3~1)YQC!oWx3SV*<orfFyK= z;?(5)ycFHiyi7#nQa8Uy7u5DdYjPH+=A<z|n^uW=SlYxeIZ*2s(Nczq78hrt8dwZ7 z4AKrn&h$_{pw<c~?IE=VVKS&qDTGn*rU<;H4%GpwgY%0(ITq72h#cGyXd#HjK`3Px z%=zG~i$yUcji(mD&4mbnb3zI@L1UMLCS$lZaPtI<`Jk=>$UmuRX{pI2#n@Dol;)Md z&4H9D(BLR3O3W(;<#ce0#-g_@v$!-dM;Eu@<wc<S3vPO4Y9iRDpl(kxsHz0zgW`<D zq5^RHzc@2DGbgb~w<MpTI2Y78LR4?iQW#omq07SD3vThFOG3Q_HV{z_B8-O0gW7kx z$vK$?#SD<ZOa|3D5N2v#a()V^qY2^WW>!GdloTZ<XEPKRB!WtLP<IL~xEav+NQE1^ z403`i$jsBtPfKGchB6?H!s3Gb{4_{m0%jI76c^-|7N;^4mn4>y7K2Kw#InT9oW!IY zNYk~r1k~+gfKbICI<G_*RtFS8_{9vxrAgor0I>=Ra==|LhT>8fvzVc{GB3ZNIJ20c zBrzRS_7^jhq$X!TdKV=biFw(@4B&zVYzhOIEY{7;(=Ex!EC!XSpq@8FNj^hKeu1u$ zDYWZXQUof{aubWP8E~hvlA_GA%tVHgGKSK;6mTV%oSIjh3JH$VyzIRE@;pczv9utW z0orE=m5pT?#SCScDXIAo65O6lO9Tx7fJLFv3}Hih#Sk{M;D_o0l|(6^?lEdIXMj}h zU{W_Fvl!Ci2c=|a6|M_ueU@jW7NzRu<(D&nJF<|hzyP93Qu9E~VNh;hs4C6?wKoEa z^0Pplde0QyFvx&}m4c_QOHx22SAc(rt~02^lbN5V>ylWKYNcRgU}&IgV61Csp<rlY zWo)W#U|?Xt72vNMlv<Vv9?gKOGXtqJvQRKGure`#sPjoIF3}AE<?S3$W7SHbxTGjG zhsy^xA`+6Cm}{jFT#%TTS)8Gukx^1oV5P5LUS6&Tj;hSG)C#@i{9OH%L{O;3C*~xj z7A59o>VsOO1(o`+rmuc#vA!mkucxmo!b^sF23*ehc_pAuTS#R=s+9t$m8)NnlbD%j ztpMr;7pIokhK9K5S|HScT$+|zr0WXJg;oj{Ntq>FF8R5MnR!+U5NqMiw^B$ghI4eC z^HWl-6jJjLB7TXvsa6WEdFeTs#Ti@yIi*F3Il6B7MY+XR3V8)!Zn3SAwE~1`tC6Rm zXsckTY0VV?uEL8_Q{WCMwo)i9Nz=990u5w=##=z+5>*V~(X7Pe%)^`W7@(tAV2*BL zN@j9qeqLe@11K?;7D3g3JCzU>DXBTS;1RA=hE!0j<mH1aV^E})<ufGaWP--IK%{PJ zo^DQlF{ptEB2#q>N|SOjlc6S-<fr5pgXN*sNq#XTdmP@Jr<<Ig2Tr!2-lcA4N@jj; zqHanmL_R++wHPJ|Zf$^Uh6twWLdL=vGOG$pGgFIF5_LhNVTU)TXC~(}B<3Ann3JEL zubYxul$xB8$N(wN5<y)mkf1KOF#z@?G<Sj8GvM*Dd<ICP5HvcLdw69@W-^-Y#G;bJ zYl}-j)*uBHTr?5hD+lFsm`bp8a%oOJWX!B6H3t$lx*&T&g>OkdTpvhDF(^%fMl2vh za1fUkmnJ0^mw>_?RFdizm1d^oGbAVGWTqsh=z`-4B+gJ!l$x8F52cD33Q}_puT3t^ zNi2qRdJZoH4Fti4($WsEO@xl6ftb3PxuE8AB10M|*g&q!OH9tp%}fRL{qyp3lZwEO zD#*-91odAMK~=ael9J@o)S`lXhMdf#A}CqR02*9_G>O1z;P9@TlFZ!vV$i@F$dSdR z1*NIQCHV|_sp<L2Acuq6av+0J@<FMCA^Gsa+{7ZZSV&3Lg^KG|>Zax#UI|L<$@vT= zhu4;r=H!FAqK7x<WhR4T8&X|o<}*NjkerwYk;JYXTz_LqF%*Nw`XEt(o}Q4Upo4Ls zz(W$wJG?75wFvB-<irw4@`Mf0f#Lu@L<fpf*l=A+KGF~!#O7pBG6(gYiu3alvr-vK z4zEmx`4bu<piW0}YBAWZqWlz)s^rw7qQoLyQ0)o{AjBA5K4g#%R5XB`3?7d|Ehs=? z4I08@NXtw~g{3xKh(d5D`S3=N4-lROH!N~eOA;AU3rjN#K+%|6kds=H4;{J71do>G z=cQI;f^su_=q{C^BsDL!C?&rLoSc(k19=$Pw=A`|q%^T86O?I+Qx9)UglN%4h!-Rt zUR#_B>T(`l3mO{)4doqP3ocrcOOrBFi%Jq1N{c|lSGqZgx^SW7qQfgwGE2b4RWc~> zi&8;mGZZ8ifXc$;lG4N+2GFn`D5z2rOY%X(Q=kF{o?p@ub8=FVvP=mikDv-e$NfM# z4mR!wiQSY;NXpO!jr-(ere`MQq%sue=Vhkk>q5u=5ak4<GYgJp&_GT;LrG$8;^B=* z0<iW#W(sJKuq=fk89J~FDVHEl1C0zK8wD9C1m{~&37VOAcwsK6v;%cH5<%`rElz<9 z5Q2D#(6K<&I0C5!cU_=EeVO@i2B>t0<x)_g1<#x?MlnNKVopA^Is%0=LLxP<II%Jx z+{*;Tda*8a#ITqFG*YOmQBahcUYZ9A^0cDV<kHl<lGI{NhQxxzqD)Bmf|#kvpwLXs zgNcKaAW{H8M-#!p2roP#1tPe(1C1v_N^kH$Bd7}wj>zIv(6~mvZb5z#Lvdz$X{v4_ zXv8Ct0W_otD&jz?PZ#R2<V;Y31`f!)oW$f*UC<B<sAd6G60rP}4^E<q;1rq&8h6V9 z8v%=EP~HRQs-n~q&_Ew3tT5!#53kHiEy^!uNKdRvEC7dHa$;!;!~?oXMWrQ)3{W-T zOqX6%T9D5GH4!uiRa%tDaCmKAa%M^@c$AMJ6*NLwoLE!@8cStJ%_{+Q_KQK89nM4y zQ9^<}71EFg<$v%XJvj0hl0jpT45=lVg{7dfA}28k)CtQ6Wot-7fB`}w4^|?l9FTU% z5M~Oz^^$XVZC+|3Ln=5>89>cBP(0@7LIuEM?4Vo=8otcQtV#s6fIwqqkg5${G3e$b z7J(8!)XOki3CY8C!ICT}N<b|_Sj_`h02YK9iz=FyUjlM7yifv}0UP;b$W1IP&CJOG zmFch~o0|w~oI%?yc`(0&lPb#aC-T52IKe`~q*%8MRFZ-U$=rfOa8V8sN(8kXK%;Vr zIS?g?rUYc*6QvRa4=E((Ks^d-9DoKJk+Nz^sxCNffgP5YUjmU%EC4kQLCO<BBcf%g zpoRscs$@W_G7Az*i@~K;4rr1IQiUPQ!@|25S+FEEw;;bLF$H8Y#7Iz3f{H*CWo7xG z1W{0wnw*)ho12-JUj!QU(and9mNFEBrhq~HS&)NsQbAJ=pbVf}oC+HC(9O(CEKLWs z+94$b$cEJ1%wkY;TNgG|%8+|_S8--?B0>Nb^KcdUB}Mtgy71%);g%F7CM9Np@@^@p z?t%=TCgy;~|5J+!K*P8xkm3e1dYZ_PQ<|Pwlv=C{N<!&HiD{XMpcX_?DtOc+3p9WV zn#u#W0P{if9-yf`kc(mJLDhIkQDQE*%qq<V=X6l%0}}(a4O8<<K;?dF9z)vUg-M{9 zPml@8`30cf04z<Wr0T+pPI#NLC>2~+7lV2Wphh;bhhaT~B5)G{mJCxsGvm-+JESB5 z*Xtk)a}rU+!SydVX+cbas7}cQ*$B=}n8Kin9b^)!3~0g|-2Ol@zMv={JQxcz3f8<r z){EQ{hsP^)h!$LKLlrP2XO<*_36z=+GGYrFu!WXM;0|7DUNR^WbyM>g5PVRsN(ZGd z26%9Qih$I-e56DO>99crzySkOQj`iEn*c>)a(*tTv`;I_1Q$9WM<8m40#Hjl2UOss zf+n+yQbA+8i76lrpm0rONCeF-g2FH-5j1fID%KfFKn=c>R5%||HbYq8>J~E03mOUp zkNbiK`O{0mjX~XH$T%rzXd9AHbs<B*puS&OJ_9^cGl1&bJWzWCsli>E2ks2z>n4JR zZb9`w$aRQ*8ra3CO%+fN3^X(hPB9?WIf=TE9so-D2JXRuyL~8X;P!xn7-12LA{2u_ zt!YS)2CN0UZ7Hd`NX=<THh?$^*4_tehBo-|S_W?u<5mT4t)VD^h=O|H;9Ll?9^EU5 z`UpiYSTVdBLD2-QC{UC`vQbGssO&=aEF}MeGAhIw5Or8$3Yr86r~;4hBL)Fr?P@&U zO9hQngUhLcRM4m@!~%GnB_<c;CxM1r!Lb1shL-La=?pwz1uBcdooKjnl;BB8)rDCK z8R$c^5}Zvy6%?wKpmsm1NJ=VbOdA$fIl9HEB@Bt_r75XZi43rz?L^F8F0AJP%{w`| zu&R}yQcy(!j(7Yj@ynt$ydX=3AmiZBz9cBWg2y>P&EKL_NKFeD%uTIIhP0BwS{afO z^HLIZi}UmHixL@9i=kuVDTyfzuux6~4Zmb2f@*e<iAYrs78SUn3dxXSP{$)D5j0Iz znhJIbxEGKDE5-{Fi}F)IbtPz_K{3ct;QBNr6*SUSke>%$>H&5jWT+f8^ak>8G06Gg zL<ov~Q1(HjJ_aNvQXd~Qa1LvE7nedu)ANfNiZh{O=&8jy`RR#h<ATr<CXpc>)CWr} zX2<}A9V|^lX>cW;T3nI}ttLzI84^KLt)MZEV(`E^Bp-v?_~4aC;DpGKn^=?#sw&b! z1J#Kspgs_2)(ep*5m|&GxwI%VzZg`Wrxt@o-9b$u-JHy1aFqz^MMB!Ix}b$K`FZ)E zUL6B`<Q+7$2pUEN4ZR;;3mSWe#b!#XZW%-bLcxaL!D1MLYB05_(BXGbxI^^86A&z$ zf=1%=i{Yzoz#axG2e&VaQqj9z;O<shCQ@UPA?5JqJWv5pTyl7!ZX!6`lk@W!pv}S} R7z5nJ1m$?xf<Wl7Jphb?RxSVl literal 0 HcmV?d00001 diff --git a/lib/imdb/locale/fr/LC_MESSAGES/imdbpy.mo b/lib/imdb/locale/fr/LC_MESSAGES/imdbpy.mo new file mode 100644 index 0000000000000000000000000000000000000000..800543323911b2c8d03bcc7e2d0d1f4126ffa1cc GIT binary patch literal 3210 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 literal 0 HcmV?d00001 diff --git a/lib/imdb/locale/generatepot.py b/lib/imdb/locale/generatepot.py index 52ea2d785..282f7d41e 100755 --- a/lib/imdb/locale/generatepot.py +++ b/lib/imdb/locale/generatepot.py @@ -18,7 +18,7 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ import re diff --git a/lib/imdb/locale/imdbpy-ar.po b/lib/imdb/locale/imdbpy-ar.po index 31489dbc4..a9ed06e25 100644 --- a/lib/imdb/locale/imdbpy-ar.po +++ b/lib/imdb/locale/imdbpy-ar.po @@ -1,13 +1,13 @@ # Gettext message file for imdbpy # Translators: -# RainDropR <rajaa@hilltx.com>, 2013 +# Rajaa Jalil <rajaa@hilltx.com>, 2013 msgid "" msgstr "" "Project-Id-Version: IMDbPY\n" "POT-Creation-Date: 2010-03-18 14:35+0000\n" -"PO-Revision-Date: 2013-11-20 11:07+0000\n" -"Last-Translator: RainDropR <rajaa@hilltx.com>\n" -"Language-Team: Arabic (http://www.transifex.com/projects/p/imdbpy/language/ar/)\n" +"PO-Revision-Date: 2016-03-28 20:40+0000\n" +"Last-Translator: Rajaa Jalil <rajaa@hilltx.com>\n" +"Language-Team: Arabic (http://www.transifex.com/davide_alberani/imdbpy/language/ar/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" diff --git a/lib/imdb/locale/imdbpy-bg.po b/lib/imdb/locale/imdbpy-bg.po index 41c4ce6ad..9cbf2a5a7 100644 --- a/lib/imdb/locale/imdbpy-bg.po +++ b/lib/imdb/locale/imdbpy-bg.po @@ -1,13 +1,13 @@ # Gettext message file for imdbpy # Translators: -# Niko Kovach <crashdeburn@gmail.com>, 2014 +# Atanas Kovachki <crashdeburn@gmail.com>, 2014 msgid "" msgstr "" "Project-Id-Version: IMDbPY\n" "POT-Creation-Date: 2010-03-18 14:35+0000\n" -"PO-Revision-Date: 2014-03-16 10:46+0000\n" -"Last-Translator: Niko Kovach <crashdeburn@gmail.com>\n" -"Language-Team: Bulgarian (http://www.transifex.com/projects/p/imdbpy/language/bg/)\n" +"PO-Revision-Date: 2016-03-28 20:40+0000\n" +"Last-Translator: Atanas Kovachki <crashdeburn@gmail.com>\n" +"Language-Team: Bulgarian (http://www.transifex.com/davide_alberani/imdbpy/language/bg/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" diff --git a/lib/imdb/locale/imdbpy-de.po b/lib/imdb/locale/imdbpy-de.po index ee3112b10..aa8b6ef2a 100644 --- a/lib/imdb/locale/imdbpy-de.po +++ b/lib/imdb/locale/imdbpy-de.po @@ -6,9 +6,9 @@ msgid "" msgstr "" "Project-Id-Version: IMDbPY\n" "POT-Creation-Date: 2010-03-18 14:35+0000\n" -"PO-Revision-Date: 2014-10-21 15:24+0000\n" +"PO-Revision-Date: 2016-03-28 20:40+0000\n" "Last-Translator: Raphael\n" -"Language-Team: German (http://www.transifex.com/projects/p/imdbpy/language/de/)\n" +"Language-Team: German (http://www.transifex.com/davide_alberani/imdbpy/language/de/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" diff --git a/lib/imdb/locale/imdbpy-es.po b/lib/imdb/locale/imdbpy-es.po index 729903df0..a7ad01a10 100644 --- a/lib/imdb/locale/imdbpy-es.po +++ b/lib/imdb/locale/imdbpy-es.po @@ -1,14 +1,13 @@ # Gettext message file for imdbpy # Translators: -# strel <strelnic@gmail.com>, 2013 +# strel, 2013 msgid "" msgstr "" "Project-Id-Version: IMDbPY\n" -"Report-Msgid-Bugs-To: http://sourceforge.net/tracker/?group_id=105998&atid=642794\n" "POT-Creation-Date: 2010-03-18 14:35+0000\n" -"PO-Revision-Date: 2013-03-11 17:18+0000\n" -"Last-Translator: strel <strelnic@gmail.com>\n" -"Language-Team: Spanish (http://www.transifex.com/projects/p/imdbpy/language/es/)\n" +"PO-Revision-Date: 2016-03-28 20:40+0000\n" +"Last-Translator: strel\n" +"Language-Team: Spanish (http://www.transifex.com/davide_alberani/imdbpy/language/es/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" diff --git a/lib/imdb/locale/imdbpy-fr.po b/lib/imdb/locale/imdbpy-fr.po index f40012c12..0fbed611d 100644 --- a/lib/imdb/locale/imdbpy-fr.po +++ b/lib/imdb/locale/imdbpy-fr.po @@ -1,15 +1,15 @@ # Gettext message file for imdbpy # Translators: -# lukophron, 2014 -# Rajaa Gutknecht <rajaa@hilltx.com>, 2013 +# lukophron, 2014-2016 +# Rajaa Jalil <rajaa@hilltx.com>, 2013 # lkppo, 2012 msgid "" msgstr "" "Project-Id-Version: IMDbPY\n" "POT-Creation-Date: 2010-03-18 14:35+0000\n" -"PO-Revision-Date: 2014-10-08 02:52+0000\n" +"PO-Revision-Date: 2016-03-20 05:27+0000\n" "Last-Translator: lukophron\n" -"Language-Team: French (http://www.transifex.com/projects/p/imdbpy/language/fr/)\n" +"Language-Team: French (http://www.transifex.com/davide_alberani/imdbpy/language/fr/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -38,7 +38,7 @@ msgstr "information-additionnelle" # Default: Admissions msgid "admissions" -msgstr "admissions" +msgstr "entrées" # Default: Agent address msgid "agent-address" @@ -46,15 +46,15 @@ msgstr "" # Default: Airing msgid "airing" -msgstr "" +msgstr "en-diffusion" # Default: Akas msgid "akas" -msgstr "" +msgstr "alias" # Default: Akas from release info msgid "akas-from-release-info" -msgstr "" +msgstr "alias-depuis-info-sortie" # Default: All products msgid "all-products" @@ -70,7 +70,7 @@ msgstr "" # Default: Amazon reviews msgid "amazon-reviews" -msgstr "" +msgstr "critiques-amazon" # Default: Analog left msgid "analog-left" @@ -82,7 +82,7 @@ msgstr "" # Default: Animation department msgid "animation-department" -msgstr "" +msgstr "département-animation" # Default: Archive footage msgid "archive-footage" @@ -178,7 +178,7 @@ msgstr "livre" # Default: Books msgid "books" -msgstr "vres" +msgstr "livres" # Default: Bottom 100 rank msgid "bottom-100-rank" diff --git a/lib/imdb/locale/imdbpy-pt_BR.po b/lib/imdb/locale/imdbpy-pt_BR.po new file mode 100644 index 000000000..51a7a82e7 --- /dev/null +++ b/lib/imdb/locale/imdbpy-pt_BR.po @@ -0,0 +1,1303 @@ +# Gettext message file for imdbpy +# Translators: +# Wagner Marques Oliveira <wagnermarques00@hotmail.com>, 2015 +msgid "" +msgstr "" +"Project-Id-Version: IMDbPY\n" +"POT-Creation-Date: 2010-03-18 14:35+0000\n" +"PO-Revision-Date: 2016-03-28 20:40+0000\n" +"Last-Translator: Wagner Marques Oliveira <wagnermarques00@hotmail.com>\n" +"Language-Team: Portuguese (Brazil) (http://www.transifex.com/davide_alberani/imdbpy/language/pt_BR/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Domain: imdbpy\n" +"Language: pt_BR\n" +"Language-Code: en\n" +"Language-Name: English\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"Preferred-Encodings: utf-8\n" + +# Default: Actor +msgid "actor" +msgstr "ator" + +# Default: Actress +msgid "actress" +msgstr "atriz" + +# Default: Adaption +msgid "adaption" +msgstr "adaptação" + +# Default: Additional information +msgid "additional-information" +msgstr "informação-adicional" + +# Default: Admissions +msgid "admissions" +msgstr "admissões" + +# Default: Agent address +msgid "agent-address" +msgstr "endereço-de-agente" + +# Default: Airing +msgid "airing" +msgstr "no ar" + +# Default: Akas +msgid "akas" +msgstr "mais conhecido como" + +# Default: Akas from release info +msgid "akas-from-release-info" +msgstr "mais conhecido como-para-lançamento-informação" + +# Default: All products +msgid "all-products" +msgstr "todos-produtos" + +# Default: Alternate language version of +msgid "alternate-language-version-of" +msgstr "" + +# Default: Alternate versions +msgid "alternate-versions" +msgstr "" + +# Default: Amazon reviews +msgid "amazon-reviews" +msgstr "" + +# Default: Analog left +msgid "analog-left" +msgstr "" + +# Default: Analog right +msgid "analog-right" +msgstr "" + +# Default: Animation department +msgid "animation-department" +msgstr "" + +# Default: Archive footage +msgid "archive-footage" +msgstr "" + +# Default: Arithmetic mean +msgid "arithmetic-mean" +msgstr "" + +# Default: Art department +msgid "art-department" +msgstr "" + +# Default: Art direction +msgid "art-direction" +msgstr "" + +# Default: Art director +msgid "art-director" +msgstr "" + +# Default: Article +msgid "article" +msgstr "" + +# Default: Asin +msgid "asin" +msgstr "" + +# Default: Aspect ratio +msgid "aspect-ratio" +msgstr "" + +# Default: Assigner +msgid "assigner" +msgstr "" + +# Default: Assistant director +msgid "assistant-director" +msgstr "" + +# Default: Auctions +msgid "auctions" +msgstr "" + +# Default: Audio noise +msgid "audio-noise" +msgstr "" + +# Default: Audio quality +msgid "audio-quality" +msgstr "" + +# Default: Award +msgid "award" +msgstr "" + +# Default: Awards +msgid "awards" +msgstr "" + +# Default: Biographical movies +msgid "biographical-movies" +msgstr "" + +# Default: Biography +msgid "biography" +msgstr "" + +# Default: Biography print +msgid "biography-print" +msgstr "" + +# Default: Birth date +msgid "birth-date" +msgstr "" + +# Default: Birth name +msgid "birth-name" +msgstr "" + +# Default: Birth notes +msgid "birth-notes" +msgstr "" + +# Default: Body +msgid "body" +msgstr "" + +# Default: Book +msgid "book" +msgstr "" + +# Default: Books +msgid "books" +msgstr "" + +# Default: Bottom 100 rank +msgid "bottom-100-rank" +msgstr "" + +# Default: Budget +msgid "budget" +msgstr "" + +# Default: Business +msgid "business" +msgstr "" + +# Default: By arrangement with +msgid "by-arrangement-with" +msgstr "" + +# Default: Camera +msgid "camera" +msgstr "" + +# Default: Camera and electrical department +msgid "camera-and-electrical-department" +msgstr "" + +# Default: Canonical episode title +msgid "canonical-episode-title" +msgstr "" + +# Default: Canonical name +msgid "canonical-name" +msgstr "" + +# Default: Canonical series title +msgid "canonical-series-title" +msgstr "" + +# Default: Canonical title +msgid "canonical-title" +msgstr "" + +# Default: Cast +msgid "cast" +msgstr "" + +# Default: Casting department +msgid "casting-department" +msgstr "" + +# Default: Casting director +msgid "casting-director" +msgstr "" + +# Default: Catalog number +msgid "catalog-number" +msgstr "" + +# Default: Category +msgid "category" +msgstr "" + +# Default: Certificate +msgid "certificate" +msgstr "" + +# Default: Certificates +msgid "certificates" +msgstr "" + +# Default: Certification +msgid "certification" +msgstr "" + +# Default: Channel +msgid "channel" +msgstr "" + +# Default: Character +msgid "character" +msgstr "" + +# Default: Cinematographer +msgid "cinematographer" +msgstr "" + +# Default: Cinematographic process +msgid "cinematographic-process" +msgstr "" + +# Default: Close captions teletext ld g +msgid "close-captions-teletext-ld-g" +msgstr "" + +# Default: Color info +msgid "color-info" +msgstr "" + +# Default: Color information +msgid "color-information" +msgstr "" + +# Default: Color rendition +msgid "color-rendition" +msgstr "" + +# Default: Company +msgid "company" +msgstr "" + +# Default: Complete cast +msgid "complete-cast" +msgstr "" + +# Default: Complete crew +msgid "complete-crew" +msgstr "" + +# Default: Composer +msgid "composer" +msgstr "" + +# Default: Connections +msgid "connections" +msgstr "" + +# Default: Contrast +msgid "contrast" +msgstr "" + +# Default: Copyright holder +msgid "copyright-holder" +msgstr "" + +# Default: Costume department +msgid "costume-department" +msgstr "" + +# Default: Costume designer +msgid "costume-designer" +msgstr "" + +# Default: Countries +msgid "countries" +msgstr "" + +# Default: Country +msgid "country" +msgstr "" + +# Default: Courtesy of +msgid "courtesy-of" +msgstr "" + +# Default: Cover +msgid "cover" +msgstr "" + +# Default: Cover url +msgid "cover-url" +msgstr "" + +# Default: Crazy credits +msgid "crazy-credits" +msgstr "" + +# Default: Creator +msgid "creator" +msgstr "" + +# Default: Current role +msgid "current-role" +msgstr "" + +# Default: Database +msgid "database" +msgstr "" + +# Default: Date +msgid "date" +msgstr "" + +# Default: Death date +msgid "death-date" +msgstr "" + +# Default: Death notes +msgid "death-notes" +msgstr "" + +# Default: Demographic +msgid "demographic" +msgstr "" + +# Default: Description +msgid "description" +msgstr "" + +# Default: Dialogue intellegibility +msgid "dialogue-intellegibility" +msgstr "" + +# Default: Digital sound +msgid "digital-sound" +msgstr "" + +# Default: Director +msgid "director" +msgstr "" + +# Default: Disc format +msgid "disc-format" +msgstr "" + +# Default: Disc size +msgid "disc-size" +msgstr "" + +# Default: Distributors +msgid "distributors" +msgstr "" + +# Default: Dvd +msgid "dvd" +msgstr "" + +# Default: Dvd features +msgid "dvd-features" +msgstr "" + +# Default: Dvd format +msgid "dvd-format" +msgstr "" + +# Default: Dvds +msgid "dvds" +msgstr "" + +# Default: Dynamic range +msgid "dynamic-range" +msgstr "" + +# Default: Edited from +msgid "edited-from" +msgstr "" + +# Default: Edited into +msgid "edited-into" +msgstr "" + +# Default: Editor +msgid "editor" +msgstr "" + +# Default: Editorial department +msgid "editorial-department" +msgstr "" + +# Default: Episode +msgid "episode" +msgstr "" + +# Default: Episode of +msgid "episode-of" +msgstr "" + +# Default: Episode title +msgid "episode-title" +msgstr "" + +# Default: Episodes +msgid "episodes" +msgstr "" + +# Default: Episodes rating +msgid "episodes-rating" +msgstr "" + +# Default: Essays +msgid "essays" +msgstr "" + +# Default: External reviews +msgid "external-reviews" +msgstr "" + +# Default: Faqs +msgid "faqs" +msgstr "" + +# Default: Feature +msgid "feature" +msgstr "" + +# Default: Featured in +msgid "featured-in" +msgstr "" + +# Default: Features +msgid "features" +msgstr "" + +# Default: Film negative format +msgid "film-negative-format" +msgstr "" + +# Default: Filming dates +msgid "filming-dates" +msgstr "" + +# Default: Filmography +msgid "filmography" +msgstr "" + +# Default: Followed by +msgid "followed-by" +msgstr "" + +# Default: Follows +msgid "follows" +msgstr "" + +# Default: For +msgid "for" +msgstr "" + +# Default: Frequency response +msgid "frequency-response" +msgstr "" + +# Default: From +msgid "from" +msgstr "" + +# Default: Full article link +msgid "full-article-link" +msgstr "" + +# Default: Full size cover url +msgid "full-size-cover-url" +msgstr "" + +# Default: Full size headshot +msgid "full-size-headshot" +msgstr "" + +# Default: Genres +msgid "genres" +msgstr "" + +# Default: Goofs +msgid "goofs" +msgstr "" + +# Default: Gross +msgid "gross" +msgstr "" + +# Default: Group genre +msgid "group-genre" +msgstr "" + +# Default: Headshot +msgid "headshot" +msgstr "" + +# Default: Height +msgid "height" +msgstr "" + +# Default: Imdbindex +msgid "imdbindex" +msgstr "" + +# Default: In development +msgid "in-development" +msgstr "" + +# Default: Interview +msgid "interview" +msgstr "" + +# Default: Interviews +msgid "interviews" +msgstr "" + +# Default: Introduction +msgid "introduction" +msgstr "" + +# Default: Item +msgid "item" +msgstr "" + +# Default: Keywords +msgid "keywords" +msgstr "" + +# Default: Kind +msgid "kind" +msgstr "" + +# Default: Label +msgid "label" +msgstr "" + +# Default: Laboratory +msgid "laboratory" +msgstr "" + +# Default: Language +msgid "language" +msgstr "" + +# Default: Languages +msgid "languages" +msgstr "" + +# Default: Laserdisc +msgid "laserdisc" +msgstr "" + +# Default: Laserdisc title +msgid "laserdisc-title" +msgstr "" + +# Default: Length +msgid "length" +msgstr "" + +# Default: Line +msgid "line" +msgstr "" + +# Default: Link +msgid "link" +msgstr "" + +# Default: Link text +msgid "link-text" +msgstr "" + +# Default: Literature +msgid "literature" +msgstr "" + +# Default: Locations +msgid "locations" +msgstr "" + +# Default: Long imdb canonical name +msgid "long-imdb-canonical-name" +msgstr "" + +# Default: Long imdb canonical title +msgid "long-imdb-canonical-title" +msgstr "" + +# Default: Long imdb episode title +msgid "long-imdb-episode-title" +msgstr "" + +# Default: Long imdb name +msgid "long-imdb-name" +msgstr "" + +# Default: Long imdb title +msgid "long-imdb-title" +msgstr "" + +# Default: Magazine cover photo +msgid "magazine-cover-photo" +msgstr "" + +# Default: Make up +msgid "make-up" +msgstr "" + +# Default: Master format +msgid "master-format" +msgstr "" + +# Default: Median +msgid "median" +msgstr "" + +# Default: Merchandising links +msgid "merchandising-links" +msgstr "" + +# Default: Mini biography +msgid "mini-biography" +msgstr "" + +# Default: Misc links +msgid "misc-links" +msgstr "" + +# Default: Miscellaneous companies +msgid "miscellaneous-companies" +msgstr "" + +# Default: Miscellaneous crew +msgid "miscellaneous-crew" +msgstr "" + +# Default: Movie +msgid "movie" +msgstr "" + +# Default: Mpaa +msgid "mpaa" +msgstr "" + +# Default: Music department +msgid "music-department" +msgstr "" + +# Default: Name +msgid "name" +msgstr "" + +# Default: News +msgid "news" +msgstr "" + +# Default: Newsgroup reviews +msgid "newsgroup-reviews" +msgstr "" + +# Default: Nick names +msgid "nick-names" +msgstr "" + +# Default: Notes +msgid "notes" +msgstr "" + +# Default: Novel +msgid "novel" +msgstr "" + +# Default: Number +msgid "number" +msgstr "" + +# Default: Number of chapter stops +msgid "number-of-chapter-stops" +msgstr "" + +# Default: Number of episodes +msgid "number-of-episodes" +msgstr "" + +# Default: Number of seasons +msgid "number-of-seasons" +msgstr "" + +# Default: Number of sides +msgid "number-of-sides" +msgstr "" + +# Default: Number of votes +msgid "number-of-votes" +msgstr "" + +# Default: Official retail price +msgid "official-retail-price" +msgstr "" + +# Default: Official sites +msgid "official-sites" +msgstr "" + +# Default: Opening weekend +msgid "opening-weekend" +msgstr "" + +# Default: Original air date +msgid "original-air-date" +msgstr "" + +# Default: Original music +msgid "original-music" +msgstr "" + +# Default: Original title +msgid "original-title" +msgstr "" + +# Default: Other literature +msgid "other-literature" +msgstr "" + +# Default: Other works +msgid "other-works" +msgstr "" + +# Default: Parents guide +msgid "parents-guide" +msgstr "" + +# Default: Performed by +msgid "performed-by" +msgstr "" + +# Default: Person +msgid "person" +msgstr "" + +# Default: Photo sites +msgid "photo-sites" +msgstr "" + +# Default: Pictorial +msgid "pictorial" +msgstr "" + +# Default: Picture format +msgid "picture-format" +msgstr "" + +# Default: Plot +msgid "plot" +msgstr "" + +# Default: Plot outline +msgid "plot-outline" +msgstr "" + +# Default: Portrayed in +msgid "portrayed-in" +msgstr "" + +# Default: Pressing plant +msgid "pressing-plant" +msgstr "" + +# Default: Printed film format +msgid "printed-film-format" +msgstr "" + +# Default: Printed media reviews +msgid "printed-media-reviews" +msgstr "" + +# Default: Producer +msgid "producer" +msgstr "" + +# Default: Production companies +msgid "production-companies" +msgstr "" + +# Default: Production country +msgid "production-country" +msgstr "" + +# Default: Production dates +msgid "production-dates" +msgstr "" + +# Default: Production design +msgid "production-design" +msgstr "" + +# Default: Production designer +msgid "production-designer" +msgstr "" + +# Default: Production manager +msgid "production-manager" +msgstr "" + +# Default: Production process protocol +msgid "production-process-protocol" +msgstr "" + +# Default: Quality of source +msgid "quality-of-source" +msgstr "" + +# Default: Quality program +msgid "quality-program" +msgstr "" + +# Default: Quote +msgid "quote" +msgstr "" + +# Default: Quotes +msgid "quotes" +msgstr "" + +# Default: Rating +msgid "rating" +msgstr "" + +# Default: Recommendations +msgid "recommendations" +msgstr "" + +# Default: Referenced in +msgid "referenced-in" +msgstr "" + +# Default: References +msgid "references" +msgstr "" + +# Default: Region +msgid "region" +msgstr "" + +# Default: Release country +msgid "release-country" +msgstr "" + +# Default: Release date +msgid "release-date" +msgstr "" + +# Default: Release dates +msgid "release-dates" +msgstr "" + +# Default: Remade as +msgid "remade-as" +msgstr "" + +# Default: Remake of +msgid "remake-of" +msgstr "" + +# Default: Rentals +msgid "rentals" +msgstr "" + +# Default: Result +msgid "result" +msgstr "" + +# Default: Review +msgid "review" +msgstr "" + +# Default: Review author +msgid "review-author" +msgstr "" + +# Default: Review kind +msgid "review-kind" +msgstr "" + +# Default: Runtime +msgid "runtime" +msgstr "" + +# Default: Runtimes +msgid "runtimes" +msgstr "" + +# Default: Salary history +msgid "salary-history" +msgstr "" + +# Default: Screenplay teleplay +msgid "screenplay-teleplay" +msgstr "" + +# Default: Season +msgid "season" +msgstr "" + +# Default: Second unit director or assistant director +msgid "second-unit-director-or-assistant-director" +msgstr "" + +# Default: Self +msgid "self" +msgstr "" + +# Default: Series animation department +msgid "series-animation-department" +msgstr "" + +# Default: Series art department +msgid "series-art-department" +msgstr "" + +# Default: Series assistant directors +msgid "series-assistant-directors" +msgstr "" + +# Default: Series camera department +msgid "series-camera-department" +msgstr "" + +# Default: Series casting department +msgid "series-casting-department" +msgstr "" + +# Default: Series cinematographers +msgid "series-cinematographers" +msgstr "" + +# Default: Series costume department +msgid "series-costume-department" +msgstr "" + +# Default: Series editorial department +msgid "series-editorial-department" +msgstr "" + +# Default: Series editors +msgid "series-editors" +msgstr "" + +# Default: Series make up department +msgid "series-make-up-department" +msgstr "" + +# Default: Series miscellaneous +msgid "series-miscellaneous" +msgstr "" + +# Default: Series music department +msgid "series-music-department" +msgstr "" + +# Default: Series producers +msgid "series-producers" +msgstr "" + +# Default: Series production designers +msgid "series-production-designers" +msgstr "" + +# Default: Series production managers +msgid "series-production-managers" +msgstr "" + +# Default: Series sound department +msgid "series-sound-department" +msgstr "" + +# Default: Series special effects department +msgid "series-special-effects-department" +msgstr "" + +# Default: Series stunts +msgid "series-stunts" +msgstr "" + +# Default: Series title +msgid "series-title" +msgstr "" + +# Default: Series transportation department +msgid "series-transportation-department" +msgstr "" + +# Default: Series visual effects department +msgid "series-visual-effects-department" +msgstr "" + +# Default: Series writers +msgid "series-writers" +msgstr "" + +# Default: Series years +msgid "series-years" +msgstr "" + +# Default: Set decoration +msgid "set-decoration" +msgstr "" + +# Default: Sharpness +msgid "sharpness" +msgstr "" + +# Default: Similar to +msgid "similar-to" +msgstr "" + +# Default: Smart canonical episode title +msgid "smart-canonical-episode-title" +msgstr "" + +# Default: Smart canonical series title +msgid "smart-canonical-series-title" +msgstr "" + +# Default: Smart canonical title +msgid "smart-canonical-title" +msgstr "" + +# Default: Smart long imdb canonical title +msgid "smart-long-imdb-canonical-title" +msgstr "" + +# Default: Sound clips +msgid "sound-clips" +msgstr "" + +# Default: Sound crew +msgid "sound-crew" +msgstr "" + +# Default: Sound encoding +msgid "sound-encoding" +msgstr "" + +# Default: Sound mix +msgid "sound-mix" +msgstr "" + +# Default: Soundtrack +msgid "soundtrack" +msgstr "" + +# Default: Spaciality +msgid "spaciality" +msgstr "" + +# Default: Special effects +msgid "special-effects" +msgstr "" + +# Default: Special effects companies +msgid "special-effects-companies" +msgstr "" + +# Default: Special effects department +msgid "special-effects-department" +msgstr "" + +# Default: Spin off +msgid "spin-off" +msgstr "" + +# Default: Spin off from +msgid "spin-off-from" +msgstr "" + +# Default: Spoofed in +msgid "spoofed-in" +msgstr "" + +# Default: Spoofs +msgid "spoofs" +msgstr "" + +# Default: Spouse +msgid "spouse" +msgstr "" + +# Default: Status of availablility +msgid "status-of-availablility" +msgstr "" + +# Default: Studio +msgid "studio" +msgstr "" + +# Default: Studios +msgid "studios" +msgstr "" + +# Default: Stunt performer +msgid "stunt-performer" +msgstr "" + +# Default: Stunts +msgid "stunts" +msgstr "" + +# Default: Subtitles +msgid "subtitles" +msgstr "" + +# Default: Supplement +msgid "supplement" +msgstr "" + +# Default: Supplements +msgid "supplements" +msgstr "" + +# Default: Synopsis +msgid "synopsis" +msgstr "" + +# Default: Taglines +msgid "taglines" +msgstr "" + +# Default: Tech info +msgid "tech-info" +msgstr "" + +# Default: Thanks +msgid "thanks" +msgstr "" + +# Default: Time +msgid "time" +msgstr "" + +# Default: Title +msgid "title" +msgstr "" + +# Default: Titles in this product +msgid "titles-in-this-product" +msgstr "" + +# Default: To +msgid "to" +msgstr "" + +# Default: Top 250 rank +msgid "top-250-rank" +msgstr "" + +# Default: Trade mark +msgid "trade-mark" +msgstr "" + +# Default: Transportation department +msgid "transportation-department" +msgstr "" + +# Default: Trivia +msgid "trivia" +msgstr "" + +# Default: Tv +msgid "tv" +msgstr "" + +# Default: Under license from +msgid "under-license-from" +msgstr "" + +# Default: Unknown link +msgid "unknown-link" +msgstr "" + +# Default: Upc +msgid "upc" +msgstr "" + +# Default: Version of +msgid "version-of" +msgstr "" + +# Default: Vhs +msgid "vhs" +msgstr "" + +# Default: Video +msgid "video" +msgstr "" + +# Default: Video artifacts +msgid "video-artifacts" +msgstr "" + +# Default: Video clips +msgid "video-clips" +msgstr "" + +# Default: Video noise +msgid "video-noise" +msgstr "" + +# Default: Video quality +msgid "video-quality" +msgstr "" + +# Default: Video standard +msgid "video-standard" +msgstr "" + +# Default: Visual effects +msgid "visual-effects" +msgstr "" + +# Default: Votes +msgid "votes" +msgstr "" + +# Default: Votes distribution +msgid "votes-distribution" +msgstr "" + +# Default: Weekend gross +msgid "weekend-gross" +msgstr "" + +# Default: Where now +msgid "where-now" +msgstr "" + +# Default: With +msgid "with" +msgstr "" + +# Default: Writer +msgid "writer" +msgstr "" + +# Default: Written by +msgid "written-by" +msgstr "" + +# Default: Year +msgid "year" +msgstr "" + +# Default: Zshops +msgid "zshops" +msgstr "" diff --git a/lib/imdb/locale/it/LC_MESSAGES/imdbpy.mo b/lib/imdb/locale/it/LC_MESSAGES/imdbpy.mo new file mode 100644 index 0000000000000000000000000000000000000000..4d06b657bd2aa4ca9fc1e7c7d73876e59a6bd8f7 GIT binary patch literal 13779 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 Bch3L- literal 0 HcmV?d00001 diff --git a/lib/imdb/locale/pt_BR/LC_MESSAGES/imdbpy.mo b/lib/imdb/locale/pt_BR/LC_MESSAGES/imdbpy.mo new file mode 100644 index 0000000000000000000000000000000000000000..73549cef0ec3a3270be38ed77feb71906c015a3c GIT binary patch literal 1004 zcmca7#4?qEfq{XWfq_AWfq|g}#6iMm7#J8>85kJOGB7Z(GcYh*W?*38U|?Xl&A`AQ z#=yYvgn@y9i-CdRHv<C$F9QPu8zTb)8v_G_2qOan3j+g#G$TZv0wV(h4+8^(IU@tZ ze?|rd7pS}oGsJvPW{4a7nIY<<nIYyDF*7g-GcYjJLe))#@~1O1Fc>m0Fsx=~VBlk5 zV7SZ7z`&50T#{b|B8yUsiy0DA5(`Q)^Ya)IQ&KWPjKmz>%)GSxqTECfk0CK7H?z1n zGe57GAu&BQuS7R7B?Y1}vnVq!ogpzhu^3F~rWNJq>K3Kuq$U=pf{kEE%*oL$D9TSM zO)e>B2q?<WN=+`&^-R$XODzJKXr<uk>yi`@$ra!qqU&6g3bs(!C9x#cO2Np$&_LI~ zSl7@(!O+CY*i_rVz`%ejz+X2gwJZ~)8KKS$q|V4f!N|bM!~mksC$YFhH>4;ruQ(^M zB)`Z?Av`fXFSSU)H?gR&G__d4KPR&+HM1yD!KNH6kqZ$wFmTAoFUd{J%+X8E&$Z+7 zNz6+xO-xVK4M|PRwNePkFDfZbFHJ2@RnTxMO03Gv(NxgLC@Cqh($_C9FV`ypS(urY zS^?6npORRXnUWfxn3I%Rl$e*PpP8GIR8Xm(12t2>pd{WYNMDo7*VES(5h8|q23*eh zc_pBb4yi0iwNfZattini$Vtr1vsOsXNGvK&EwK#^anrRxr~}!XmRh9inwOlPl9`uo zrC^biS;FO#pPQJOXQcqK8SWb^1+dj{zOHkAN~)DYY92zwFEKaOO2IWRJtwm`gDW7X zv?wu0*Db#&x7bP{uK>&~wl%U=fG}+}@)Yb83^lE}0*X@8Qj3aGQ{YZ1wo)i9Nz=99 zVgN-1Lt;r$W)&pOB_3XWcyT@hG_8R-x``>7$(i|ii8;_@cX(@RF+*xzN@`K+;pO?d zDXF^PG?>bem#>gm#E_eqS*(zppO=xEoSBlZker{Jk4;RsAh9S>HzzUg@bbjm)Vz{> WaOy0|g}R_5KPA5yl1@wVix~hh4k@w# literal 0 HcmV?d00001 diff --git a/lib/imdb/locale/rebuildmo.py b/lib/imdb/locale/rebuildmo.py index 14fe17f1f..b72a74c36 100755 --- a/lib/imdb/locale/rebuildmo.py +++ b/lib/imdb/locale/rebuildmo.py @@ -18,7 +18,7 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ import glob diff --git a/lib/imdb/locale/tr/LC_MESSAGES/imdbpy.mo b/lib/imdb/locale/tr/LC_MESSAGES/imdbpy.mo new file mode 100644 index 0000000000000000000000000000000000000000..8ce4e3acad9ad9594e24949db3cd39bc3d965d6a GIT binary patch literal 11191 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 literal 0 HcmV?d00001 diff --git a/lib/imdb/parser/__init__.py b/lib/imdb/parser/__init__.py index 7126af5f2..4c3c90a85 100644 --- a/lib/imdb/parser/__init__.py +++ b/lib/imdb/parser/__init__.py @@ -20,7 +20,7 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ __all__ = ['http', 'mobile', 'sql'] diff --git a/lib/imdb/parser/http/__init__.py b/lib/imdb/parser/http/__init__.py index 2bfeeb747..a3001a08d 100644 --- a/lib/imdb/parser/http/__init__.py +++ b/lib/imdb/parser/http/__init__.py @@ -22,7 +22,7 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ import sys diff --git a/lib/imdb/parser/http/bsouplxml/bsoupxpath.py b/lib/imdb/parser/http/bsouplxml/bsoupxpath.py index 4b7930746..c5c489db7 100644 --- a/lib/imdb/parser/http/bsouplxml/bsoupxpath.py +++ b/lib/imdb/parser/http/bsouplxml/bsoupxpath.py @@ -17,7 +17,7 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ __author__ = 'H. Turgut Uyar <uyar@tekir.org>' diff --git a/lib/imdb/parser/http/bsouplxml/etree.py b/lib/imdb/parser/http/bsouplxml/etree.py index 72a8ba1cf..28465f5c2 100644 --- a/lib/imdb/parser/http/bsouplxml/etree.py +++ b/lib/imdb/parser/http/bsouplxml/etree.py @@ -18,7 +18,7 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ import _bsoup as BeautifulSoup diff --git a/lib/imdb/parser/http/bsouplxml/html.py b/lib/imdb/parser/http/bsouplxml/html.py index dca08fe3c..bbf13bd64 100644 --- a/lib/imdb/parser/http/bsouplxml/html.py +++ b/lib/imdb/parser/http/bsouplxml/html.py @@ -18,7 +18,7 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ import _bsoup as BeautifulSoup diff --git a/lib/imdb/parser/http/characterParser.py b/lib/imdb/parser/http/characterParser.py index dcbaa9c5e..ff5ea09bc 100644 --- a/lib/imdb/parser/http/characterParser.py +++ b/lib/imdb/parser/http/characterParser.py @@ -23,7 +23,7 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ import re diff --git a/lib/imdb/parser/http/companyParser.py b/lib/imdb/parser/http/companyParser.py index e058e25ae..843379169 100644 --- a/lib/imdb/parser/http/companyParser.py +++ b/lib/imdb/parser/http/companyParser.py @@ -21,7 +21,7 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ import re diff --git a/lib/imdb/parser/http/movieParser.py b/lib/imdb/parser/http/movieParser.py index 790e07d3c..f4589d7a0 100644 --- a/lib/imdb/parser/http/movieParser.py +++ b/lib/imdb/parser/http/movieParser.py @@ -9,7 +9,7 @@ pages would be: plot summary: http://akas.imdb.com/title/tt0094226/plotsummary ...and so on... -Copyright 2004-2013 Davide Alberani <da@erlug.linux.it> +Copyright 2004-2016 Davide Alberani <da@erlug.linux.it> 2008 H. Turgut Uyar <uyar@tekir.org> This program is free software; you can redistribute it and/or modify @@ -24,7 +24,7 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ import re @@ -207,6 +207,11 @@ class DOMHTMLMovieParser(DOMParserBase): multi=True, path="./text()")), + Extractor(label='myrating', + path="//span[@id='voteuser']", + attrs=Attribute(key='myrating', + path=".//text()")), + Extractor(label='h5sections', path="//div[@class='info']/h5/..", attrs=[ @@ -226,7 +231,7 @@ class DOMHTMLMovieParser(DOMParserBase): Attribute(key="countries", path="./h5[starts-with(text(), " \ "'Countr')]/../div[@class='info-content']//text()", - postprocess=makeSplitter('|')), + postprocess=makeSplitter('|')), Attribute(key="language", path="./h5[starts-with(text(), " \ "'Language')]/..//text()", @@ -234,7 +239,7 @@ class DOMHTMLMovieParser(DOMParserBase): Attribute(key='color info', path="./h5[starts-with(text(), " \ "'Color')]/..//text()", - postprocess=makeSplitter('Color:')), + postprocess=makeSplitter('|')), Attribute(key='sound mix', path="./h5[starts-with(text(), " \ "'Sound Mix')]/..//text()", @@ -462,6 +467,8 @@ class DOMHTMLMovieParser(DOMParserBase): del data['other akas'] if nakas: data['akas'] = nakas + if 'color info' in data: + data['color info'] = [x.replace('Color:', '', 1) for x in data['color info']] if 'runtimes' in data: data['runtimes'] = [x.replace(' min', u'') for x in data['runtimes']] @@ -552,10 +559,10 @@ class DOMHTMLPlotParser(DOMParserBase): # Notice that recently IMDb started to put the email of the # author only in the link, that we're not collecting, here. extractors = [Extractor(label='plot', - path="//ul[@class='zebraList']//p", + path="//p[@class='plotSummary']", attrs=Attribute(key='plot', multi=True, - path={'plot': './text()[1]', + path={'plot': './/text()', 'author': './span/em/a/text()'}, postprocess=_process_plotsummary))] @@ -783,17 +790,20 @@ class DOMHTMLTriviaParser(DOMParserBase): -class DOMHTMLSoundtrackParser(DOMHTMLAlternateVersionsParser): - kind = 'soundtrack' - - preprocessors = [ - ('<br>', '\n') - ] +class DOMHTMLSoundtrackParser(DOMParserBase): + _defGetRefs = True + preprocessors = [('<br />', '\n'), ('<br>', '\n')] + extractors = [Extractor(label='soundtrack', + path="//div[@class='list']//div", + attrs=Attribute(key='soundtrack', + multi=True, + path=".//text()", + postprocess=lambda x: x.strip()))] def postprocess_data(self, data): - if 'alternate versions' in data: + if 'soundtrack' in data: nd = [] - for x in data['alternate versions']: + for x in data['soundtrack']: ds = x.split('\n') title = ds[0] if title[0] == '"' and title[-1] == '"': @@ -1177,7 +1187,7 @@ class DOMHTMLCriticReviewsParser(DOMParserBase): path="//div[@class='article']/div[@class='see-more']/a", attrs=Attribute(key='metacritic url', path="./@href")) ] - + class DOMHTMLOfficialsitesParser(DOMParserBase): """Parser for the "official sites", "external reviews", "newsgroup reviews", "miscellaneous links", "sound clips", "video clips" and @@ -1289,16 +1299,17 @@ class DOMHTMLTechParser(DOMParserBase): result = tparser.parse(technical_html_string) """ kind = 'tech' + re_space = re.compile(r'\s+') extractors = [Extractor(label='tech', - group="//h5", + group="//table//tr/td[@class='label']", group_key="./text()", - group_key_normalize=lambda x: x.lower(), - path="./following-sibling::div[1]", + group_key_normalize=lambda x: x.lower().strip(), + path=".", attrs=Attribute(key=None, - path=".//text()", + path="..//td[2]//text()", postprocess=lambda x: [t.strip() - for t in x.split('\n') if t.strip()]))] + for t in x.split(':::') if t.strip()]))] preprocessors = [ (re.compile('(<h5>.*?</h5>)', re.I), r'</div>\1<div class="_imdbpy">'), @@ -1308,12 +1319,15 @@ class DOMHTMLTechParser(DOMParserBase): (re.compile('<p>(.*?)</p>', re.I), r'\1<br/>'), (re.compile('(</td><td valign="top">)', re.I), r'\1::'), (re.compile('(</tr><tr>)', re.I), r'\n\1'), + (re.compile('<span class="ghost">\|</span>', re.I), r':::'), + (re.compile('<br/?>', re.I), r':::'), # this is for splitting individual entries - (re.compile('<br/>', re.I), r'\n'), ] def postprocess_data(self, data): for key in data: + data[key] = filter(lambda x: x != '|', data[key]) + data[key] = [self.re_space.sub(' ', x).strip() for x in data[key]] data[key] = filter(None, data[key]) if self.kind in ('literature', 'business', 'contacts') and data: if 'screenplay/teleplay' in data: @@ -1905,7 +1919,7 @@ _OBJECTS = { 'goofs_parser': ((DOMHTMLGoofsParser,), None), 'alternateversions_parser': ((DOMHTMLAlternateVersionsParser,), None), 'trivia_parser': ((DOMHTMLTriviaParser,), None), - 'soundtrack_parser': ((DOMHTMLSoundtrackParser,), {'kind': 'soundtrack'}), + 'soundtrack_parser': ((DOMHTMLSoundtrackParser,), None), 'quotes_parser': ((DOMHTMLQuotesParser,), None), 'releasedates_parser': ((DOMHTMLReleaseinfoParser,), None), 'ratings_parser': ((DOMHTMLRatingsParser,), None), diff --git a/lib/imdb/parser/http/personParser.py b/lib/imdb/parser/http/personParser.py index fbaf5571d..caf8b2ef8 100644 --- a/lib/imdb/parser/http/personParser.py +++ b/lib/imdb/parser/http/personParser.py @@ -23,7 +23,7 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ import re @@ -204,7 +204,7 @@ class DOMHTMLBioParser(DOMParserBase): _birth_attrs = [Attribute(key='birth date', path={ 'day': "./a[starts-with(@href, " \ - "'/date/')]/text()", + "'/search/name?birth_monthday=')]/text()", 'year': "./a[starts-with(@href, " \ "'/search/name?birth_year=')]/text()" }, @@ -215,7 +215,7 @@ class DOMHTMLBioParser(DOMParserBase): _death_attrs = [Attribute(key='death date', path={ 'day': "./a[starts-with(@href, " \ - "'/date/')]/text()", + "'/search/name?death_monthday=')]/text()", 'year': "./a[starts-with(@href, " \ "'/search/name?death_date=')]/text()" }, @@ -396,7 +396,7 @@ class DOMHTMLResumeParser(DOMParserBase): ] def postprocess_data(self, data): - + for key in data.keys(): if data[key] == '': del data[key] diff --git a/lib/imdb/parser/http/searchCharacterParser.py b/lib/imdb/parser/http/searchCharacterParser.py index 038e6b443..5f281fa0c 100644 --- a/lib/imdb/parser/http/searchCharacterParser.py +++ b/lib/imdb/parser/http/searchCharacterParser.py @@ -22,7 +22,7 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ from imdb.utils import analyze_name, build_name diff --git a/lib/imdb/parser/http/searchCompanyParser.py b/lib/imdb/parser/http/searchCompanyParser.py index ffd5bb6bb..40ea8a722 100644 --- a/lib/imdb/parser/http/searchCompanyParser.py +++ b/lib/imdb/parser/http/searchCompanyParser.py @@ -22,7 +22,7 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ from imdb.utils import analyze_company_name, build_company_name diff --git a/lib/imdb/parser/http/searchKeywordParser.py b/lib/imdb/parser/http/searchKeywordParser.py index c0343365a..4161fa484 100644 --- a/lib/imdb/parser/http/searchKeywordParser.py +++ b/lib/imdb/parser/http/searchKeywordParser.py @@ -21,7 +21,7 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ from utils import Extractor, Attribute, analyze_imdbid @@ -47,7 +47,7 @@ class DOMHTMLSearchKeywordParser(DOMHTMLSearchMovieParser): the one given.""" _BaseParser = DOMBasicKeywordParser - _notDirectHitTitle = '<title>imdb keyword' + _notDirectHitTitle = '<title>find - imdb' _titleBuilder = lambda self, x: x _linkPrefix = '/keyword/' @@ -56,7 +56,7 @@ class DOMHTMLSearchKeywordParser(DOMHTMLSearchMovieParser): path="./a[1]/text()" )] extractors = [Extractor(label='search', - path="//td[3]/a[starts-with(@href, " \ + path="//a[starts-with(@href, " \ "'/keyword/')]/..", attrs=_attrs)] @@ -80,7 +80,7 @@ class DOMHTMLSearchMovieKeywordParser(DOMHTMLSearchMovieParser): "new search system" is used, searching for movies with the given keyword.""" - _notDirectHitTitle = '<title>best' + _notDirectHitTitle = '<title>most' _attrs = [Attribute(key='data', multi=True, @@ -98,7 +98,7 @@ class DOMHTMLSearchMovieKeywordParser(DOMHTMLSearchMovieParser): ))] extractors = [Extractor(label='search', - path="//td[3]/a[starts-with(@href, " \ + path="//div[@class='lister-list']//h3//a[starts-with(@href, " \ "'/title/tt')]/..", attrs=_attrs)] diff --git a/lib/imdb/parser/http/searchMovieParser.py b/lib/imdb/parser/http/searchMovieParser.py index 5d5331809..781610cf7 100644 --- a/lib/imdb/parser/http/searchMovieParser.py +++ b/lib/imdb/parser/http/searchMovieParser.py @@ -23,7 +23,7 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ import re @@ -118,6 +118,7 @@ class DOMHTMLSearchMovieParser(DOMParserBase): self.url = u'' def preprocess_string(self, html_string): + if self._notDirectHitTitle in html_string[:10240].lower(): if self._linkPrefix == '/title/tt': # Only for movies. diff --git a/lib/imdb/parser/http/searchPersonParser.py b/lib/imdb/parser/http/searchPersonParser.py index 1e5f7f375..2dd269410 100644 --- a/lib/imdb/parser/http/searchPersonParser.py +++ b/lib/imdb/parser/http/searchPersonParser.py @@ -22,7 +22,7 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ import re diff --git a/lib/imdb/parser/http/topBottomParser.py b/lib/imdb/parser/http/topBottomParser.py index 75d06f57c..1b8bb9f0b 100644 --- a/lib/imdb/parser/http/topBottomParser.py +++ b/lib/imdb/parser/http/topBottomParser.py @@ -7,7 +7,7 @@ E.g.: http://akas.imdb.com/chart/top http://akas.imdb.com/chart/bottom -Copyright 2009 Davide Alberani <da@erlug.linux.it> +Copyright 2009-2015 Davide Alberani <da@erlug.linux.it> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -21,7 +21,7 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ from imdb.utils import analyze_title @@ -43,14 +43,15 @@ class DOMHTMLTop250Parser(DOMParserBase): def _init(self): self.extractors = [Extractor(label=self.label, - path="//div[@id='main']//table//tr", + path="//div[@id='main']//div[1]//div//table//tbody//tr", attrs=Attribute(key=None, multi=True, - path={self.ranktext: "./td[1]//text()", - 'rating': "./td[2]//text()", - 'title': "./td[3]//text()", - 'movieID': "./td[3]//a/@href", - 'votes': "./td[4]//text()" + path={self.ranktext: "./td[2]//text()", + 'rating': "./td[3]//strong//text()", + 'title': "./td[2]//a//text()", + 'year': "./td[2]//span//text()", + 'movieID': "./td[2]//a/@href", + 'votes': "./td[3]//strong/@title" }))] def postprocess_data(self, data): @@ -72,12 +73,16 @@ class DOMHTMLTop250Parser(DOMParserBase): if theID in seenIDs: continue seenIDs.append(theID) - minfo = analyze_title(d['title']) + minfo = analyze_title(d['title']+" "+d['year']) try: minfo[self.ranktext] = int(d[self.ranktext].replace('.', '')) except: pass if 'votes' in d: - try: minfo['votes'] = int(d['votes'].replace(',', '')) - except: pass + try: + votes = d['votes'].replace(' votes','') + votes = votes.split(' based on ')[1] + minfo['votes'] = int(votes.replace(',', '')) + except: + pass if 'rating' in d: try: minfo['rating'] = float(d['rating']) except: pass diff --git a/lib/imdb/parser/http/utils.py b/lib/imdb/parser/http/utils.py index 9b059ebfd..5aefb3ce3 100644 --- a/lib/imdb/parser/http/utils.py +++ b/lib/imdb/parser/http/utils.py @@ -19,7 +19,7 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ import re @@ -404,6 +404,15 @@ def build_movie(txt, movieID=None, roleID=None, status=None, m = Movie(title=title, movieID=movieID, notes=notes, currentRole=role, roleID=roleID, roleIsPerson=_parsingCharacter, modFunct=modFunct, accessSystem=accessSystem) + if additionalNotes: + if '(TV Series)' in additionalNotes: + m['kind'] = u'tv series' + elif '(Video Game)' in additionalNotes: + m['kind'] = u'video game' + elif '(TV Movie)' in additionalNotes: + m['kind'] = u'tv movie' + elif '(TV Short)' in additionalNotes: + m['kind'] = u'tv short' if roleNotes and len(roleNotes) == len(roleID): for idx, role in enumerate(m.currentRole): try: diff --git a/lib/imdb/parser/mobile/__init__.py b/lib/imdb/parser/mobile/__init__.py index 371c809cc..e7ab589d3 100644 --- a/lib/imdb/parser/mobile/__init__.py +++ b/lib/imdb/parser/mobile/__init__.py @@ -20,7 +20,7 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ import re diff --git a/lib/imdb/parser/sql/__init__.py b/lib/imdb/parser/sql/__init__.py deleted file mode 100644 index bc0319d82..000000000 --- a/lib/imdb/parser/sql/__init__.py +++ /dev/null @@ -1,1595 +0,0 @@ -""" -parser.sql package (imdb package). - -This package provides the IMDbSqlAccessSystem class used to access -IMDb's data through a SQL database. Every database supported by -the SQLObject _AND_ SQLAlchemy Object Relational Managers is available. -the imdb.IMDb function will return an instance of this class when -called with the 'accessSystem' argument set to "sql", "database" or "db". - -Copyright 2005-2012 Davide Alberani <da@erlug.linux.it> - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation; either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA -""" - -# FIXME: this whole module was written in a veeery short amount of time. -# The code should be commented, rewritten and cleaned. :-) - -import re -import logging -from difflib import SequenceMatcher -from codecs import lookup - -from imdb import IMDbBase -from imdb.utils import normalizeName, normalizeTitle, build_title, \ - build_name, analyze_name, analyze_title, \ - canonicalTitle, canonicalName, re_titleRef, \ - build_company_name, re_episodes, _unicodeArticles, \ - analyze_company_name, re_year_index, re_nameRef -from imdb.Person import Person -from imdb.Movie import Movie -from imdb.Company import Company -from imdb._exceptions import IMDbDataAccessError, IMDbError - - -# Logger for miscellaneous functions. -_aux_logger = logging.getLogger('imdbpy.parser.sql.aux') - -# ============================= -# Things that once upon a time were in imdb.parser.common.locsql. - -def titleVariations(title, fromPtdf=0): - """Build title variations useful for searches; if fromPtdf is true, - the input is assumed to be in the plain text data files format.""" - if fromPtdf: title1 = u'' - else: title1 = title - title2 = title3 = u'' - if fromPtdf or re_year_index.search(title): - # If it appears to have a (year[/imdbIndex]) indication, - # assume that a long imdb canonical name was provided. - titldict = analyze_title(title, canonical=1) - # title1: the canonical name. - title1 = titldict['title'] - if titldict['kind'] != 'episode': - # title3: the long imdb canonical name. - if fromPtdf: title3 = title - else: title3 = build_title(titldict, canonical=1, ptdf=1) - else: - title1 = normalizeTitle(title1) - title3 = build_title(titldict, canonical=1, ptdf=1) - else: - # Just a title. - # title1: the canonical title. - title1 = canonicalTitle(title) - title3 = u'' - # title2 is title1 without the article, or title1 unchanged. - if title1: - title2 = title1 - t2s = title2.split(u', ') - if t2s[-1].lower() in _unicodeArticles: - title2 = u', '.join(t2s[:-1]) - _aux_logger.debug('title variations: 1:[%s] 2:[%s] 3:[%s]', - title1, title2, title3) - return title1, title2, title3 - - -re_nameIndex = re.compile(r'\(([IVXLCDM]+)\)') - -def nameVariations(name, fromPtdf=0): - """Build name variations useful for searches; if fromPtdf is true, - the input is assumed to be in the plain text data files format.""" - name1 = name2 = name3 = u'' - if fromPtdf or re_nameIndex.search(name): - # We've a name with an (imdbIndex) - namedict = analyze_name(name, canonical=1) - # name1 is the name in the canonical format. - name1 = namedict['name'] - # name3 is the canonical name with the imdbIndex. - if fromPtdf: - if namedict.has_key('imdbIndex'): - name3 = name - else: - name3 = build_name(namedict, canonical=1) - else: - # name1 is the name in the canonical format. - name1 = canonicalName(name) - name3 = u'' - # name2 is the name in the normal format, if it differs from name1. - name2 = normalizeName(name1) - if name1 == name2: name2 = u'' - _aux_logger.debug('name variations: 1:[%s] 2:[%s] 3:[%s]', - name1, name2, name3) - return name1, name2, name3 - - -try: - from cutils import ratcliff as _ratcliff - def ratcliff(s1, s2, sm): - """Return the Ratcliff-Obershelp value between the two strings, - using the C implementation.""" - return _ratcliff(s1.encode('latin_1', 'replace'), - s2.encode('latin_1', 'replace')) -except ImportError: - _aux_logger.warn('Unable to import the cutils.ratcliff function.' - ' Searching names and titles using the "sql"' - ' data access system will be slower.') - - def ratcliff(s1, s2, sm): - """Ratcliff-Obershelp similarity.""" - STRING_MAXLENDIFFER = 0.7 - s1len = len(s1) - s2len = len(s2) - if s1len < s2len: - threshold = float(s1len) / s2len - else: - threshold = float(s2len) / s1len - if threshold < STRING_MAXLENDIFFER: - return 0.0 - sm.set_seq2(s2.lower()) - return sm.ratio() - - -def merge_roles(mop): - """Merge multiple roles.""" - new_list = [] - for m in mop: - if m in new_list: - keep_this = new_list[new_list.index(m)] - if not isinstance(keep_this.currentRole, list): - keep_this.currentRole = [keep_this.currentRole] - keep_this.currentRole.append(m.currentRole) - else: - new_list.append(m) - return new_list - - -def scan_names(name_list, name1, name2, name3, results=0, ro_thresold=None, - _scan_character=False): - """Scan a list of names, searching for best matches against - the given variations.""" - if ro_thresold is not None: RO_THRESHOLD = ro_thresold - else: RO_THRESHOLD = 0.6 - sm1 = SequenceMatcher() - sm2 = SequenceMatcher() - sm3 = SequenceMatcher() - sm1.set_seq1(name1.lower()) - if name2: sm2.set_seq1(name2.lower()) - if name3: sm3.set_seq1(name3.lower()) - resd = {} - for i, n_data in name_list: - nil = n_data['name'] - # XXX: on Symbian, here we get a str; not sure this is the - # right place to fix it. - if isinstance(nil, str): - nil = unicode(nil, 'latin1', 'ignore') - # Distance with the canonical name. - ratios = [ratcliff(name1, nil, sm1) + 0.05] - namesurname = u'' - if not _scan_character: - nils = nil.split(', ', 1) - surname = nils[0] - if len(nils) == 2: namesurname = '%s %s' % (nils[1], surname) - else: - nils = nil.split(' ', 1) - surname = nils[-1] - namesurname = nil - if surname != nil: - # Distance with the "Surname" in the database. - ratios.append(ratcliff(name1, surname, sm1)) - if not _scan_character: - ratios.append(ratcliff(name1, namesurname, sm1)) - if name2: - ratios.append(ratcliff(name2, surname, sm2)) - # Distance with the "Name Surname" in the database. - if namesurname: - ratios.append(ratcliff(name2, namesurname, sm2)) - if name3: - # Distance with the long imdb canonical name. - ratios.append(ratcliff(name3, - build_name(n_data, canonical=1), sm3) + 0.1) - ratio = max(ratios) - if ratio >= RO_THRESHOLD: - if resd.has_key(i): - if ratio > resd[i][0]: resd[i] = (ratio, (i, n_data)) - else: resd[i] = (ratio, (i, n_data)) - res = resd.values() - res.sort() - res.reverse() - if results > 0: res[:] = res[:results] - return res - - -def scan_titles(titles_list, title1, title2, title3, results=0, - searchingEpisode=0, onlyEpisodes=0, ro_thresold=None): - """Scan a list of titles, searching for best matches against - the given variations.""" - if ro_thresold is not None: RO_THRESHOLD = ro_thresold - else: RO_THRESHOLD = 0.6 - sm1 = SequenceMatcher() - sm2 = SequenceMatcher() - sm3 = SequenceMatcher() - sm1.set_seq1(title1.lower()) - sm2.set_seq2(title2.lower()) - if title3: - sm3.set_seq1(title3.lower()) - if title3[-1] == '}': searchingEpisode = 1 - hasArt = 0 - if title2 != title1: hasArt = 1 - resd = {} - for i, t_data in titles_list: - if onlyEpisodes: - if t_data.get('kind') != 'episode': - continue - til = t_data['title'] - if til[-1] == ')': - dateIdx = til.rfind('(') - if dateIdx != -1: - til = til[:dateIdx].rstrip() - if not til: - continue - ratio = ratcliff(title1, til, sm1) - if ratio >= RO_THRESHOLD: - resd[i] = (ratio, (i, t_data)) - continue - if searchingEpisode: - if t_data.get('kind') != 'episode': continue - elif t_data.get('kind') == 'episode': continue - til = t_data['title'] - # XXX: on Symbian, here we get a str; not sure this is the - # right place to fix it. - if isinstance(til, str): - til = unicode(til, 'latin1', 'ignore') - # Distance with the canonical title (with or without article). - # titleS -> titleR - # titleS, the -> titleR, the - if not searchingEpisode: - til = canonicalTitle(til) - ratios = [ratcliff(title1, til, sm1) + 0.05] - # til2 is til without the article, if present. - til2 = til - tils = til2.split(', ') - matchHasArt = 0 - if tils[-1].lower() in _unicodeArticles: - til2 = ', '.join(tils[:-1]) - matchHasArt = 1 - if hasArt and not matchHasArt: - # titleS[, the] -> titleR - ratios.append(ratcliff(title2, til, sm2)) - elif matchHasArt and not hasArt: - # titleS -> titleR[, the] - ratios.append(ratcliff(title1, til2, sm1)) - else: - ratios = [0.0] - if title3: - # Distance with the long imdb canonical title. - ratios.append(ratcliff(title3, - build_title(t_data, canonical=1, ptdf=1), sm3) + 0.1) - ratio = max(ratios) - if ratio >= RO_THRESHOLD: - if resd.has_key(i): - if ratio > resd[i][0]: - resd[i] = (ratio, (i, t_data)) - else: resd[i] = (ratio, (i, t_data)) - res = resd.values() - res.sort() - res.reverse() - if results > 0: res[:] = res[:results] - return res - - -def scan_company_names(name_list, name1, results=0, ro_thresold=None): - """Scan a list of company names, searching for best matches against - the given name. Notice that this function takes a list of - strings, and not a list of dictionaries.""" - if ro_thresold is not None: RO_THRESHOLD = ro_thresold - else: RO_THRESHOLD = 0.6 - sm1 = SequenceMatcher() - sm1.set_seq1(name1.lower()) - resd = {} - withoutCountry = not name1.endswith(']') - for i, n in name_list: - # XXX: on Symbian, here we get a str; not sure this is the - # right place to fix it. - if isinstance(n, str): - n = unicode(n, 'latin1', 'ignore') - o_name = n - var = 0.0 - if withoutCountry and n.endswith(']'): - cidx = n.rfind('[') - if cidx != -1: - n = n[:cidx].rstrip() - var = -0.05 - # Distance with the company name. - ratio = ratcliff(name1, n, sm1) + var - if ratio >= RO_THRESHOLD: - if resd.has_key(i): - if ratio > resd[i][0]: resd[i] = (ratio, - (i, analyze_company_name(o_name))) - else: - resd[i] = (ratio, (i, analyze_company_name(o_name))) - res = resd.values() - res.sort() - res.reverse() - if results > 0: res[:] = res[:results] - return res - - -try: - from cutils import soundex -except ImportError: - _aux_logger.warn('Unable to import the cutils.soundex function.' - ' Searches of movie titles and person names will be' - ' a bit slower.') - - _translate = dict(B='1', C='2', D='3', F='1', G='2', J='2', K='2', L='4', - M='5', N='5', P='1', Q='2', R='6', S='2', T='3', V='1', - X='2', Z='2') - _translateget = _translate.get - _re_non_ascii = re.compile(r'^[^a-z]*', re.I) - SOUNDEX_LEN = 5 - - def soundex(s): - """Return the soundex code for the given string.""" - # Maximum length of the soundex code. - s = _re_non_ascii.sub('', s) - if not s: return None - s = s.upper() - soundCode = s[0] - for c in s[1:]: - cw = _translateget(c, '0') - if cw != '0' and soundCode[-1] != cw: - soundCode += cw - return soundCode[:SOUNDEX_LEN] or None - - -def _sortKeywords(keyword, kwds): - """Sort a list of keywords, based on the searched one.""" - sm = SequenceMatcher() - sm.set_seq1(keyword.lower()) - ratios = [(ratcliff(keyword, k, sm), k) for k in kwds] - checkContained = False - if len(keyword) > 4: - checkContained = True - for idx, data in enumerate(ratios): - ratio, key = data - if key.startswith(keyword): - ratios[idx] = (ratio+0.5, key) - elif checkContained and keyword in key: - ratios[idx] = (ratio+0.3, key) - ratios.sort() - ratios.reverse() - return [r[1] for r in ratios] - - -def filterSimilarKeywords(keyword, kwdsIterator): - """Return a sorted list of keywords similar to the one given.""" - seenDict = {} - kwdSndx = soundex(keyword.encode('ascii', 'ignore')) - matches = [] - matchesappend = matches.append - checkContained = False - if len(keyword) > 4: - checkContained = True - for movieID, key in kwdsIterator: - if key in seenDict: - continue - seenDict[key] = None - if checkContained and keyword in key: - matchesappend(key) - continue - if kwdSndx == soundex(key.encode('ascii', 'ignore')): - matchesappend(key) - return _sortKeywords(keyword, matches) - - - -# ============================= - -_litlist = ['screenplay/teleplay', 'novel', 'adaption', 'book', - 'production process protocol', 'interviews', - 'printed media reviews', 'essays', 'other literature'] -_litd = dict([(x, ('literature', x)) for x in _litlist]) - -_buslist = ['budget', 'weekend gross', 'gross', 'opening weekend', 'rentals', - 'admissions', 'filming dates', 'production dates', 'studios', - 'copyright holder'] -_busd = dict([(x, ('business', x)) for x in _buslist]) - - -def _reGroupDict(d, newgr): - """Regroup keys in the d dictionary in subdictionaries, based on - the scheme in the newgr dictionary. - E.g.: in the newgr, an entry 'LD label': ('laserdisc', 'label') - tells the _reGroupDict() function to take the entry with - label 'LD label' (as received from the sql database) - and put it in the subsection (another dictionary) named - 'laserdisc', using the key 'label'.""" - r = {} - newgrks = newgr.keys() - for k, v in d.items(): - if k in newgrks: - r.setdefault(newgr[k][0], {})[newgr[k][1]] = v - # A not-so-clearer version: - ##r.setdefault(newgr[k][0], {}) - ##r[newgr[k][0]][newgr[k][1]] = v - else: r[k] = v - return r - - -def _groupListBy(l, index): - """Regroup items in a list in a list of lists, grouped by - the value at the given index.""" - tmpd = {} - for item in l: - tmpd.setdefault(item[index], []).append(item) - res = tmpd.values() - return res - - -def sub_dict(d, keys): - """Return the subdictionary of 'd', with just the keys listed in 'keys'.""" - return dict([(k, d[k]) for k in keys if k in d]) - - -def get_movie_data(movieID, kindDict, fromAka=0, _table=None): - """Return a dictionary containing data about the given movieID; - if fromAka is true, the AkaTitle table is searched; _table is - reserved for the imdbpy2sql.py script.""" - if _table is not None: - Table = _table - else: - if not fromAka: Table = Title - else: Table = AkaTitle - try: - m = Table.get(movieID) - except Exception, e: - _aux_logger.warn('Unable to fetch information for movieID %s: %s', movieID, e) - mdict = {} - return mdict - mdict = {'title': m.title, 'kind': kindDict[m.kindID], - 'year': m.productionYear, 'imdbIndex': m.imdbIndex, - 'season': m.seasonNr, 'episode': m.episodeNr} - if not fromAka: - if m.seriesYears is not None: - mdict['series years'] = unicode(m.seriesYears) - if mdict['imdbIndex'] is None: del mdict['imdbIndex'] - if mdict['year'] is None: del mdict['year'] - else: - try: - mdict['year'] = int(mdict['year']) - except (TypeError, ValueError): - del mdict['year'] - if mdict['season'] is None: del mdict['season'] - else: - try: mdict['season'] = int(mdict['season']) - except: pass - if mdict['episode'] is None: del mdict['episode'] - else: - try: mdict['episode'] = int(mdict['episode']) - except: pass - episodeOfID = m.episodeOfID - if episodeOfID is not None: - ser_dict = get_movie_data(episodeOfID, kindDict, fromAka) - mdict['episode of'] = Movie(data=ser_dict, movieID=episodeOfID, - accessSystem='sql') - if fromAka: - ser_note = AkaTitle.get(episodeOfID).note - if ser_note: - mdict['episode of'].notes = ser_note - return mdict - - -def _iterKeywords(results): - """Iterate over (key.id, key.keyword) columns of a selection of - the Keyword table.""" - for key in results: - yield key.id, key.keyword - - -def getSingleInfo(table, movieID, infoType, notAList=False): - """Return a dictionary in the form {infoType: infoListOrString}, - retrieving a single set of information about a given movie, from - the specified table.""" - infoTypeID = InfoType.select(InfoType.q.info == infoType) - if infoTypeID.count() == 0: - return {} - res = table.select(AND(table.q.movieID == movieID, - table.q.infoTypeID == infoTypeID[0].id)) - retList = [] - for r in res: - info = r.info - note = r.note - if note: - info += u'::%s' % note - retList.append(info) - if not retList: - return {} - if not notAList: return {infoType: retList} - else: return {infoType: retList[0]} - - -def _cmpTop(a, b, what='top 250 rank'): - """Compare function used to sort top 250/bottom 10 rank.""" - av = int(a[1].get(what)) - bv = int(b[1].get(what)) - if av == bv: - return 0 - return (-1, 1)[av > bv] - -def _cmpBottom(a, b): - """Compare function used to sort top 250/bottom 10 rank.""" - return _cmpTop(a, b, what='bottom 10 rank') - - -class IMDbSqlAccessSystem(IMDbBase): - """The class used to access IMDb's data through a SQL database.""" - - accessSystem = 'sql' - _sql_logger = logging.getLogger('imdbpy.parser.sql') - - def __init__(self, uri, adultSearch=1, useORM=None, *arguments, **keywords): - """Initialize the access system.""" - IMDbBase.__init__(self, *arguments, **keywords) - if useORM is None: - useORM = ('sqlobject', 'sqlalchemy') - if not isinstance(useORM, (tuple, list)): - if ',' in useORM: - useORM = useORM.split(',') - else: - useORM = [useORM] - self.useORM = useORM - nrMods = len(useORM) - _gotError = False - DB_TABLES = [] - for idx, mod in enumerate(useORM): - mod = mod.strip().lower() - try: - if mod == 'sqlalchemy': - from alchemyadapter import getDBTables, NotFoundError, \ - setConnection, AND, OR, IN, \ - ISNULL, CONTAINSSTRING, toUTF8 - elif mod == 'sqlobject': - from objectadapter import getDBTables, NotFoundError, \ - setConnection, AND, OR, IN, \ - ISNULL, CONTAINSSTRING, toUTF8 - else: - self._sql_logger.warn('unknown module "%s"' % mod) - continue - self._sql_logger.info('using %s ORM', mod) - # XXX: look ma'... black magic! It's used to make - # TableClasses and some functions accessible - # through the whole module. - for k, v in [('NotFoundError', NotFoundError), - ('AND', AND), ('OR', OR), ('IN', IN), - ('ISNULL', ISNULL), - ('CONTAINSSTRING', CONTAINSSTRING)]: - globals()[k] = v - self.toUTF8 = toUTF8 - DB_TABLES = getDBTables(uri) - for t in DB_TABLES: - globals()[t._imdbpyName] = t - if _gotError: - self._sql_logger.warn('falling back to "%s"' % mod) - break - except ImportError, e: - if idx+1 >= nrMods: - raise IMDbError('unable to use any ORM in %s: %s' % ( - str(useORM), str(e))) - else: - self._sql_logger.warn('unable to use "%s": %s' % (mod, - str(e))) - _gotError = True - continue - else: - raise IMDbError('unable to use any ORM in %s' % str(useORM)) - # Set the connection to the database. - self._sql_logger.debug('connecting to %s', uri) - try: - self._connection = setConnection(uri, DB_TABLES) - except AssertionError, e: - raise IMDbDataAccessError( \ - 'unable to connect to the database server; ' + \ - 'complete message: "%s"' % str(e)) - self.Error = self._connection.module.Error - # Maps some IDs to the corresponding strings. - self._kind = {} - self._kindRev = {} - self._sql_logger.debug('reading constants from the database') - try: - for kt in KindType.select(): - self._kind[kt.id] = kt.kind - self._kindRev[str(kt.kind)] = kt.id - except self.Error: - # NOTE: you can also get the error, but - at least with - # MySQL - it also contains the password, and I don't - # like the idea to print it out. - raise IMDbDataAccessError( \ - 'unable to connect to the database server') - self._role = {} - for rl in RoleType.select(): - self._role[rl.id] = str(rl.role) - self._info = {} - self._infoRev = {} - for inf in InfoType.select(): - self._info[inf.id] = str(inf.info) - self._infoRev[str(inf.info)] = inf.id - self._compType = {} - for cType in CompanyType.select(): - self._compType[cType.id] = cType.kind - info = [(it.id, it.info) for it in InfoType.select()] - self._compcast = {} - for cc in CompCastType.select(): - self._compcast[cc.id] = str(cc.kind) - self._link = {} - for lt in LinkType.select(): - self._link[lt.id] = str(lt.link) - self._moviesubs = {} - # Build self._moviesubs, a dictionary used to rearrange - # the data structure for a movie object. - for vid, vinfo in info: - if not vinfo.startswith('LD '): continue - self._moviesubs[vinfo] = ('laserdisc', vinfo[3:]) - self._moviesubs.update(_litd) - self._moviesubs.update(_busd) - self.do_adult_search(adultSearch) - - def _findRefs(self, o, trefs, nrefs): - """Find titles or names references in strings.""" - if isinstance(o, (unicode, str)): - for title in re_titleRef.findall(o): - a_title = analyze_title(title, canonical=0) - rtitle = build_title(a_title, ptdf=1) - if trefs.has_key(rtitle): continue - movieID = self._getTitleID(rtitle) - if movieID is None: - movieID = self._getTitleID(title) - if movieID is None: - continue - m = Movie(title=rtitle, movieID=movieID, - accessSystem=self.accessSystem) - trefs[rtitle] = m - rtitle2 = canonicalTitle(a_title.get('title', u'')) - if rtitle2 and rtitle2 != rtitle and rtitle2 != title: - trefs[rtitle2] = m - if title != rtitle: - trefs[title] = m - for name in re_nameRef.findall(o): - a_name = analyze_name(name, canonical=1) - rname = build_name(a_name, canonical=1) - if nrefs.has_key(rname): continue - personID = self._getNameID(rname) - if personID is None: - personID = self._getNameID(name) - if personID is None: continue - p = Person(name=rname, personID=personID, - accessSystem=self.accessSystem) - nrefs[rname] = p - rname2 = normalizeName(a_name.get('name', u'')) - if rname2 and rname2 != rname: - nrefs[rname2] = p - if name != rname and name != rname2: - nrefs[name] = p - elif isinstance(o, (list, tuple)): - for item in o: - self._findRefs(item, trefs, nrefs) - elif isinstance(o, dict): - for value in o.values(): - self._findRefs(value, trefs, nrefs) - return (trefs, nrefs) - - def _extractRefs(self, o): - """Scan for titles or names references in strings.""" - trefs = {} - nrefs = {} - try: - return self._findRefs(o, trefs, nrefs) - except RuntimeError, e: - # Symbian/python 2.2 has a poor regexp implementation. - import warnings - warnings.warn('RuntimeError in ' - "imdb.parser.sql.IMDbSqlAccessSystem; " - "if it's not a recursion limit exceeded and we're not " - "running in a Symbian environment, it's a bug:\n%s" % e) - return (trefs, nrefs) - - def _changeAKAencoding(self, akanotes, akatitle): - """Return akatitle in the correct charset, as specified in - the akanotes field; if akatitle doesn't need to be modified, - return None.""" - oti = akanotes.find('(original ') - if oti == -1: return None - ote = akanotes[oti+10:].find(' title)') - if ote != -1: - cs_info = akanotes[oti+10:oti+10+ote].lower().split() - for e in cs_info: - # excludes some strings that clearly are not encoding. - if e in ('script', '', 'cyrillic', 'greek'): continue - if e.startswith('iso-') and e.find('latin') != -1: - e = e[4:].replace('-', '') - try: - lookup(e) - lat1 = akatitle.encode('latin_1', 'replace') - return unicode(lat1, e, 'replace') - except (LookupError, ValueError, TypeError): - continue - return None - - def _buildNULLCondition(self, col, val): - """Build a comparison for columns where values can be NULL.""" - if val is None: - return ISNULL(col) - else: - if isinstance(val, (int, long)): - return col == val - else: - return col == self.toUTF8(val) - - def _getTitleID(self, title): - """Given a long imdb canonical title, returns a movieID or - None if not found.""" - td = analyze_title(title) - condition = None - if td['kind'] == 'episode': - epof = td['episode of'] - seriesID = [s.id for s in Title.select( - AND(Title.q.title == self.toUTF8(epof['title']), - self._buildNULLCondition(Title.q.imdbIndex, - epof.get('imdbIndex')), - Title.q.kindID == self._kindRev[epof['kind']], - self._buildNULLCondition(Title.q.productionYear, - epof.get('year'))))] - if seriesID: - condition = AND(IN(Title.q.episodeOfID, seriesID), - Title.q.title == self.toUTF8(td['title']), - self._buildNULLCondition(Title.q.imdbIndex, - td.get('imdbIndex')), - Title.q.kindID == self._kindRev[td['kind']], - self._buildNULLCondition(Title.q.productionYear, - td.get('year'))) - if condition is None: - condition = AND(Title.q.title == self.toUTF8(td['title']), - self._buildNULLCondition(Title.q.imdbIndex, - td.get('imdbIndex')), - Title.q.kindID == self._kindRev[td['kind']], - self._buildNULLCondition(Title.q.productionYear, - td.get('year'))) - res = Title.select(condition) - try: - if res.count() != 1: - return None - except (UnicodeDecodeError, TypeError): - return None - return res[0].id - - def _getNameID(self, name): - """Given a long imdb canonical name, returns a personID or - None if not found.""" - nd = analyze_name(name) - res = Name.select(AND(Name.q.name == self.toUTF8(nd['name']), - self._buildNULLCondition(Name.q.imdbIndex, - nd.get('imdbIndex')))) - try: - c = res.count() - if res.count() != 1: - return None - except (UnicodeDecodeError, TypeError): - return None - return res[0].id - - def _normalize_movieID(self, movieID): - """Normalize the given movieID.""" - try: - return int(movieID) - except (ValueError, OverflowError): - raise IMDbError('movieID "%s" can\'t be converted to integer' % \ - movieID) - - def _normalize_personID(self, personID): - """Normalize the given personID.""" - try: - return int(personID) - except (ValueError, OverflowError): - raise IMDbError('personID "%s" can\'t be converted to integer' % \ - personID) - - def _normalize_characterID(self, characterID): - """Normalize the given characterID.""" - try: - return int(characterID) - except (ValueError, OverflowError): - raise IMDbError('characterID "%s" can\'t be converted to integer' \ - % characterID) - - def _normalize_companyID(self, companyID): - """Normalize the given companyID.""" - try: - return int(companyID) - except (ValueError, OverflowError): - raise IMDbError('companyID "%s" can\'t be converted to integer' \ - % companyID) - - def get_imdbMovieID(self, movieID): - """Translate a movieID in an imdbID. - If not in the database, try an Exact Primary Title search on IMDb; - return None if it's unable to get the imdbID. - """ - try: movie = Title.get(movieID) - except NotFoundError: return None - imdbID = movie.imdbID - if imdbID is not None: return '%07d' % imdbID - m_dict = get_movie_data(movie.id, self._kind) - titline = build_title(m_dict, ptdf=0) - imdbID = self.title2imdbID(titline, m_dict['kind']) - # If the imdbID was retrieved from the web and was not in the - # database, update the database (ignoring errors, because it's - # possibile that the current user has not update privileges). - # There're times when I think I'm a genius; this one of - # those times... <g> - if imdbID is not None and not isinstance(imdbID, list): - try: movie.imdbID = int(imdbID) - except: pass - return imdbID - - def get_imdbPersonID(self, personID): - """Translate a personID in an imdbID. - If not in the database, try an Exact Primary Name search on IMDb; - return None if it's unable to get the imdbID. - """ - try: person = Name.get(personID) - except NotFoundError: return None - imdbID = person.imdbID - if imdbID is not None: return '%07d' % imdbID - n_dict = {'name': person.name, 'imdbIndex': person.imdbIndex} - namline = build_name(n_dict, canonical=False) - imdbID = self.name2imdbID(namline) - if imdbID is not None and not isinstance(imdbID, list): - try: person.imdbID = int(imdbID) - except: pass - return imdbID - - def get_imdbCharacterID(self, characterID): - """Translate a characterID in an imdbID. - If not in the database, try an Exact Primary Name search on IMDb; - return None if it's unable to get the imdbID. - """ - try: character = CharName.get(characterID) - except NotFoundError: return None - imdbID = character.imdbID - if imdbID is not None: return '%07d' % imdbID - n_dict = {'name': character.name, 'imdbIndex': character.imdbIndex} - namline = build_name(n_dict, canonical=False) - imdbID = self.character2imdbID(namline) - if imdbID is not None and not isinstance(imdbID, list): - try: character.imdbID = int(imdbID) - except: pass - return imdbID - - def get_imdbCompanyID(self, companyID): - """Translate a companyID in an imdbID. - If not in the database, try an Exact Primary Name search on IMDb; - return None if it's unable to get the imdbID. - """ - try: company = CompanyName.get(companyID) - except NotFoundError: return None - imdbID = company.imdbID - if imdbID is not None: return '%07d' % imdbID - n_dict = {'name': company.name, 'country': company.countryCode} - namline = build_company_name(n_dict) - imdbID = self.company2imdbID(namline) - if imdbID is not None and not isinstance(imdbID, list): - try: company.imdbID = int(imdbID) - except: pass - return imdbID - - def do_adult_search(self, doAdult): - """If set to 0 or False, movies in the Adult category are not - episodeOf = title_dict.get('episode of') - shown in the results of a search.""" - self.doAdult = doAdult - - def _search_movie(self, title, results, _episodes=False): - title = title.strip() - if not title: return [] - title_dict = analyze_title(title, canonical=1) - s_title = title_dict['title'] - if not s_title: return [] - episodeOf = title_dict.get('episode of') - if episodeOf: - _episodes = False - s_title_split = s_title.split(', ') - if len(s_title_split) > 1 and \ - s_title_split[-1].lower() in _unicodeArticles: - s_title_rebuilt = ', '.join(s_title_split[:-1]) - if s_title_rebuilt: - s_title = s_title_rebuilt - #if not episodeOf: - # if not _episodes: - # s_title_split = s_title.split(', ') - # if len(s_title_split) > 1 and \ - # s_title_split[-1].lower() in _articles: - # s_title_rebuilt = ', '.join(s_title_split[:-1]) - # if s_title_rebuilt: - # s_title = s_title_rebuilt - #else: - # _episodes = False - if isinstance(s_title, unicode): - s_title = s_title.encode('ascii', 'ignore') - - soundexCode = soundex(s_title) - - # XXX: improve the search restricting the kindID if the - # "kind" of the input differs from "movie"? - condition = conditionAka = None - if _episodes: - condition = AND(Title.q.phoneticCode == soundexCode, - Title.q.kindID == self._kindRev['episode']) - conditionAka = AND(AkaTitle.q.phoneticCode == soundexCode, - AkaTitle.q.kindID == self._kindRev['episode']) - elif title_dict['kind'] == 'episode' and episodeOf is not None: - # set canonical=0 ? Should not make much difference. - series_title = build_title(episodeOf, canonical=1) - # XXX: is it safe to get "results" results? - # Too many? Too few? - serRes = results - if serRes < 3 or serRes > 10: - serRes = 10 - searchSeries = self._search_movie(series_title, serRes) - seriesIDs = [result[0] for result in searchSeries] - if seriesIDs: - condition = AND(Title.q.phoneticCode == soundexCode, - IN(Title.q.episodeOfID, seriesIDs), - Title.q.kindID == self._kindRev['episode']) - conditionAka = AND(AkaTitle.q.phoneticCode == soundexCode, - IN(AkaTitle.q.episodeOfID, seriesIDs), - AkaTitle.q.kindID == self._kindRev['episode']) - else: - # XXX: bad situation: we have found no matching series; - # try searching everything (both episodes and - # non-episodes) for the title. - condition = AND(Title.q.phoneticCode == soundexCode, - IN(Title.q.episodeOfID, seriesIDs)) - conditionAka = AND(AkaTitle.q.phoneticCode == soundexCode, - IN(AkaTitle.q.episodeOfID, seriesIDs)) - if condition is None: - # XXX: excludes episodes? - condition = AND(Title.q.kindID != self._kindRev['episode'], - Title.q.phoneticCode == soundexCode) - conditionAka = AND(AkaTitle.q.kindID != self._kindRev['episode'], - AkaTitle.q.phoneticCode == soundexCode) - - # Up to 3 variations of the title are searched, plus the - # long imdb canonical title, if provided. - if not _episodes: - title1, title2, title3 = titleVariations(title) - else: - title1 = title - title2 = '' - title3 = '' - try: - qr = [(q.id, get_movie_data(q.id, self._kind)) - for q in Title.select(condition)] - q2 = [(q.movieID, get_movie_data(q.id, self._kind, fromAka=1)) - for q in AkaTitle.select(conditionAka)] - qr += q2 - except NotFoundError, e: - raise IMDbDataAccessError( \ - 'unable to search the database: "%s"' % str(e)) - - resultsST = results * 3 - res = scan_titles(qr, title1, title2, title3, resultsST, - searchingEpisode=episodeOf is not None, - onlyEpisodes=_episodes, - ro_thresold=0.0) - res[:] = [x[1] for x in res] - - if res and not self.doAdult: - mids = [x[0] for x in res] - genreID = self._infoRev['genres'] - adultlist = [al.movieID for al - in MovieInfo.select( - AND(MovieInfo.q.infoTypeID == genreID, - MovieInfo.q.info == 'Adult', - IN(MovieInfo.q.movieID, mids)))] - res[:] = [x for x in res if x[0] not in adultlist] - - new_res = [] - # XXX: can there be duplicates? - for r in res: - if r not in q2: - new_res.append(r) - continue - mdict = r[1] - aka_title = build_title(mdict, ptdf=1) - orig_dict = get_movie_data(r[0], self._kind) - orig_title = build_title(orig_dict, ptdf=1) - if aka_title == orig_title: - new_res.append(r) - continue - orig_dict['akas'] = [aka_title] - new_res.append((r[0], orig_dict)) - if results > 0: new_res[:] = new_res[:results] - return new_res - - def _search_episode(self, title, results): - return self._search_movie(title, results, _episodes=True) - - def get_movie_main(self, movieID): - # Every movie information is retrieved from here. - infosets = self.get_movie_infoset() - try: - res = get_movie_data(movieID, self._kind) - except NotFoundError, e: - raise IMDbDataAccessError( \ - 'unable to get movieID "%s": "%s"' % (movieID, str(e))) - if not res: - raise IMDbDataAccessError('unable to get movieID "%s"' % movieID) - # Collect cast information. - castdata = [[cd.personID, cd.personRoleID, cd.note, cd.nrOrder, - self._role[cd.roleID]] - for cd in CastInfo.select(CastInfo.q.movieID == movieID)] - for p in castdata: - person = Name.get(p[0]) - p += [person.name, person.imdbIndex] - if p[4] in ('actor', 'actress'): - p[4] = 'cast' - # Regroup by role/duty (cast, writer, director, ...) - castdata[:] = _groupListBy(castdata, 4) - for group in castdata: - duty = group[0][4] - for pdata in group: - curRole = pdata[1] - curRoleID = None - if curRole is not None: - robj = CharName.get(curRole) - curRole = robj.name - curRoleID = robj.id - p = Person(personID=pdata[0], name=pdata[5], - currentRole=curRole or u'', - roleID=curRoleID, - notes=pdata[2] or u'', - accessSystem='sql') - if pdata[6]: p['imdbIndex'] = pdata[6] - p.billingPos = pdata[3] - res.setdefault(duty, []).append(p) - if duty == 'cast': - res[duty] = merge_roles(res[duty]) - res[duty].sort() - # Info about the movie. - minfo = [(self._info[m.infoTypeID], m.info, m.note) - for m in MovieInfo.select(MovieInfo.q.movieID == movieID)] - minfo += [(self._info[m.infoTypeID], m.info, m.note) - for m in MovieInfoIdx.select(MovieInfoIdx.q.movieID == movieID)] - minfo += [('keywords', Keyword.get(m.keywordID).keyword, None) - for m in MovieKeyword.select(MovieKeyword.q.movieID == movieID)] - minfo = _groupListBy(minfo, 0) - for group in minfo: - sect = group[0][0] - for mdata in group: - data = mdata[1] - if mdata[2]: data += '::%s' % mdata[2] - res.setdefault(sect, []).append(data) - # Companies info about a movie. - cinfo = [(self._compType[m.companyTypeID], m.companyID, m.note) for m - in MovieCompanies.select(MovieCompanies.q.movieID == movieID)] - cinfo = _groupListBy(cinfo, 0) - for group in cinfo: - sect = group[0][0] - for mdata in group: - cDb = CompanyName.get(mdata[1]) - cDbTxt = cDb.name - if cDb.countryCode: - cDbTxt += ' %s' % cDb.countryCode - company = Company(name=cDbTxt, - companyID=mdata[1], - notes=mdata[2] or u'', - accessSystem=self.accessSystem) - res.setdefault(sect, []).append(company) - # AKA titles. - akat = [(get_movie_data(at.id, self._kind, fromAka=1), at.note) - for at in AkaTitle.select(AkaTitle.q.movieID == movieID)] - if akat: - res['akas'] = [] - for td, note in akat: - nt = build_title(td, ptdf=1) - if note: - net = self._changeAKAencoding(note, nt) - if net is not None: nt = net - nt += '::%s' % note - if nt not in res['akas']: res['akas'].append(nt) - # Complete cast/crew. - compcast = [(self._compcast[cc.subjectID], self._compcast[cc.statusID]) - for cc in CompleteCast.select(CompleteCast.q.movieID == movieID)] - if compcast: - for entry in compcast: - val = unicode(entry[1]) - res[u'complete %s' % entry[0]] = val - # Movie connections. - mlinks = [[ml.linkedMovieID, self._link[ml.linkTypeID]] - for ml in MovieLink.select(MovieLink.q.movieID == movieID)] - if mlinks: - for ml in mlinks: - lmovieData = get_movie_data(ml[0], self._kind) - if lmovieData: - m = Movie(movieID=ml[0], data=lmovieData, accessSystem='sql') - ml[0] = m - res['connections'] = {} - mlinks[:] = _groupListBy(mlinks, 1) - for group in mlinks: - lt = group[0][1] - res['connections'][lt] = [i[0] for i in group] - # Episodes. - episodes = {} - eps_list = list(Title.select(Title.q.episodeOfID == movieID)) - eps_list.sort() - if eps_list: - ps_data = {'title': res['title'], 'kind': res['kind'], - 'year': res.get('year'), - 'imdbIndex': res.get('imdbIndex')} - parentSeries = Movie(movieID=movieID, data=ps_data, - accessSystem='sql') - for episode in eps_list: - episodeID = episode.id - episode_data = get_movie_data(episodeID, self._kind) - m = Movie(movieID=episodeID, data=episode_data, - accessSystem='sql') - m['episode of'] = parentSeries - season = episode_data.get('season', 'UNKNOWN') - if season not in episodes: episodes[season] = {} - ep_number = episode_data.get('episode') - if ep_number is None: - ep_number = max((episodes[season].keys() or [0])) + 1 - episodes[season][ep_number] = m - res['episodes'] = episodes - res['number of episodes'] = sum([len(x) for x in episodes.values()]) - res['number of seasons'] = len(episodes.keys()) - # Regroup laserdisc information. - res = _reGroupDict(res, self._moviesubs) - # Do some transformation to preserve consistency with other - # data access systems. - if 'quotes' in res: - for idx, quote in enumerate(res['quotes']): - res['quotes'][idx] = quote.split('::') - if 'runtimes' in res and len(res['runtimes']) > 0: - rt = res['runtimes'][0] - episodes = re_episodes.findall(rt) - if episodes: - res['runtimes'][0] = re_episodes.sub('', rt) - if res['runtimes'][0][-2:] == '::': - res['runtimes'][0] = res['runtimes'][0][:-2] - if 'votes' in res: - res['votes'] = int(res['votes'][0]) - if 'rating' in res: - res['rating'] = float(res['rating'][0]) - if 'votes distribution' in res: - res['votes distribution'] = res['votes distribution'][0] - if 'mpaa' in res: - res['mpaa'] = res['mpaa'][0] - if 'top 250 rank' in res: - try: res['top 250 rank'] = int(res['top 250 rank']) - except: pass - if 'bottom 10 rank' in res: - try: res['bottom 100 rank'] = int(res['bottom 10 rank']) - except: pass - del res['bottom 10 rank'] - for old, new in [('guest', 'guests'), ('trademarks', 'trade-mark'), - ('articles', 'article'), ('pictorials', 'pictorial'), - ('magazine-covers', 'magazine-cover-photo')]: - if old in res: - res[new] = res[old] - del res[old] - trefs,nrefs = {}, {} - trefs,nrefs = self._extractRefs(sub_dict(res,Movie.keys_tomodify_list)) - return {'data': res, 'titlesRefs': trefs, 'namesRefs': nrefs, - 'info sets': infosets} - - # Just to know what kind of information are available. - get_movie_alternate_versions = get_movie_main - get_movie_business = get_movie_main - get_movie_connections = get_movie_main - get_movie_crazy_credits = get_movie_main - get_movie_goofs = get_movie_main - get_movie_keywords = get_movie_main - get_movie_literature = get_movie_main - get_movie_locations = get_movie_main - get_movie_plot = get_movie_main - get_movie_quotes = get_movie_main - get_movie_release_dates = get_movie_main - get_movie_soundtrack = get_movie_main - get_movie_taglines = get_movie_main - get_movie_technical = get_movie_main - get_movie_trivia = get_movie_main - get_movie_vote_details = get_movie_main - get_movie_episodes = get_movie_main - - def _search_person(self, name, results): - name = name.strip() - if not name: return [] - s_name = analyze_name(name)['name'] - if not s_name: return [] - if isinstance(s_name, unicode): - s_name = s_name.encode('ascii', 'ignore') - soundexCode = soundex(s_name) - name1, name2, name3 = nameVariations(name) - - # If the soundex is None, compare only with the first - # phoneticCode column. - if soundexCode is not None: - condition = IN(soundexCode, [Name.q.namePcodeCf, - Name.q.namePcodeNf, - Name.q.surnamePcode]) - conditionAka = IN(soundexCode, [AkaName.q.namePcodeCf, - AkaName.q.namePcodeNf, - AkaName.q.surnamePcode]) - else: - condition = ISNULL(Name.q.namePcodeCf) - conditionAka = ISNULL(AkaName.q.namePcodeCf) - - try: - qr = [(q.id, {'name': q.name, 'imdbIndex': q.imdbIndex}) - for q in Name.select(condition)] - - q2 = [(q.personID, {'name': q.name, 'imdbIndex': q.imdbIndex}) - for q in AkaName.select(conditionAka)] - qr += q2 - except NotFoundError, e: - raise IMDbDataAccessError( \ - 'unable to search the database: "%s"' % str(e)) - - res = scan_names(qr, name1, name2, name3, results) - res[:] = [x[1] for x in res] - # Purge empty imdbIndex. - returnl = [] - for x in res: - tmpd = x[1] - if tmpd['imdbIndex'] is None: - del tmpd['imdbIndex'] - returnl.append((x[0], tmpd)) - - new_res = [] - # XXX: can there be duplicates? - for r in returnl: - if r not in q2: - new_res.append(r) - continue - pdict = r[1] - aka_name = build_name(pdict, canonical=1) - p = Name.get(r[0]) - orig_dict = {'name': p.name, 'imdbIndex': p.imdbIndex} - if orig_dict['imdbIndex'] is None: - del orig_dict['imdbIndex'] - orig_name = build_name(orig_dict, canonical=1) - if aka_name == orig_name: - new_res.append(r) - continue - orig_dict['akas'] = [aka_name] - new_res.append((r[0], orig_dict)) - if results > 0: new_res[:] = new_res[:results] - - return new_res - - def get_person_main(self, personID): - # Every person information is retrieved from here. - infosets = self.get_person_infoset() - try: - p = Name.get(personID) - except NotFoundError, e: - raise IMDbDataAccessError( \ - 'unable to get personID "%s": "%s"' % (personID, str(e))) - res = {'name': p.name, 'imdbIndex': p.imdbIndex} - if res['imdbIndex'] is None: del res['imdbIndex'] - if not res: - raise IMDbDataAccessError('unable to get personID "%s"' % personID) - # Collect cast information. - castdata = [(cd.movieID, cd.personRoleID, cd.note, - self._role[cd.roleID], - get_movie_data(cd.movieID, self._kind)) - for cd in CastInfo.select(CastInfo.q.personID == personID)] - # Regroup by role/duty (cast, writer, director, ...) - castdata[:] = _groupListBy(castdata, 3) - episodes = {} - seenDuties = [] - for group in castdata: - for mdata in group: - duty = orig_duty = group[0][3] - if duty not in seenDuties: seenDuties.append(orig_duty) - note = mdata[2] or u'' - if 'episode of' in mdata[4]: - duty = 'episodes' - if orig_duty not in ('actor', 'actress'): - if note: note = ' %s' % note - note = '[%s]%s' % (orig_duty, note) - curRole = mdata[1] - curRoleID = None - if curRole is not None: - robj = CharName.get(curRole) - curRole = robj.name - curRoleID = robj.id - m = Movie(movieID=mdata[0], data=mdata[4], - currentRole=curRole or u'', - roleID=curRoleID, - notes=note, accessSystem='sql') - if duty != 'episodes': - res.setdefault(duty, []).append(m) - else: - episodes.setdefault(m['episode of'], []).append(m) - if episodes: - for k in episodes: - episodes[k].sort() - episodes[k].reverse() - res['episodes'] = episodes - for duty in seenDuties: - if duty in res: - if duty in ('actor', 'actress', 'himself', 'herself', - 'themselves'): - res[duty] = merge_roles(res[duty]) - res[duty].sort() - # Info about the person. - pinfo = [(self._info[pi.infoTypeID], pi.info, pi.note) - for pi in PersonInfo.select(PersonInfo.q.personID == personID)] - # Regroup by duty. - pinfo = _groupListBy(pinfo, 0) - for group in pinfo: - sect = group[0][0] - for pdata in group: - data = pdata[1] - if pdata[2]: data += '::%s' % pdata[2] - res.setdefault(sect, []).append(data) - # AKA names. - akan = [(an.name, an.imdbIndex) - for an in AkaName.select(AkaName.q.personID == personID)] - if akan: - res['akas'] = [] - for n in akan: - nd = {'name': n[0]} - if n[1]: nd['imdbIndex'] = n[1] - nt = build_name(nd, canonical=1) - res['akas'].append(nt) - # Do some transformation to preserve consistency with other - # data access systems. - for key in ('birth date', 'birth notes', 'death date', 'death notes', - 'birth name', 'height'): - if key in res: - res[key] = res[key][0] - if 'guest' in res: - res['notable tv guest appearances'] = res['guest'] - del res['guest'] - miscnames = res.get('nick names', []) - if 'birth name' in res: miscnames.append(res['birth name']) - if 'akas' in res: - for mname in miscnames: - if mname in res['akas']: res['akas'].remove(mname) - if not res['akas']: del res['akas'] - trefs,nrefs = self._extractRefs(sub_dict(res,Person.keys_tomodify_list)) - return {'data': res, 'titlesRefs': trefs, 'namesRefs': nrefs, - 'info sets': infosets} - - # Just to know what kind of information are available. - get_person_filmography = get_person_main - get_person_biography = get_person_main - get_person_other_works = get_person_main - get_person_episodes = get_person_main - - def _search_character(self, name, results): - name = name.strip() - if not name: return [] - s_name = analyze_name(name)['name'] - if not s_name: return [] - if isinstance(s_name, unicode): - s_name = s_name.encode('ascii', 'ignore') - s_name = normalizeName(s_name) - soundexCode = soundex(s_name) - surname = s_name.split(' ')[-1] - surnameSoundex = soundex(surname) - name2 = '' - soundexName2 = None - nsplit = s_name.split() - if len(nsplit) > 1: - name2 = '%s %s' % (nsplit[-1], ' '.join(nsplit[:-1])) - if s_name == name2: - name2 = '' - else: - soundexName2 = soundex(name2) - # If the soundex is None, compare only with the first - # phoneticCode column. - if soundexCode is not None: - if soundexName2 is not None: - condition = OR(surnameSoundex == CharName.q.surnamePcode, - IN(CharName.q.namePcodeNf, [soundexCode, - soundexName2]), - IN(CharName.q.surnamePcode, [soundexCode, - soundexName2])) - else: - condition = OR(surnameSoundex == CharName.q.surnamePcode, - IN(soundexCode, [CharName.q.namePcodeNf, - CharName.q.surnamePcode])) - else: - condition = ISNULL(Name.q.namePcodeNf) - try: - qr = [(q.id, {'name': q.name, 'imdbIndex': q.imdbIndex}) - for q in CharName.select(condition)] - except NotFoundError, e: - raise IMDbDataAccessError( \ - 'unable to search the database: "%s"' % str(e)) - res = scan_names(qr, s_name, name2, '', results, - _scan_character=True) - res[:] = [x[1] for x in res] - # Purge empty imdbIndex. - returnl = [] - for x in res: - tmpd = x[1] - if tmpd['imdbIndex'] is None: - del tmpd['imdbIndex'] - returnl.append((x[0], tmpd)) - return returnl - - def get_character_main(self, characterID, results=1000): - # Every character information is retrieved from here. - infosets = self.get_character_infoset() - try: - c = CharName.get(characterID) - except NotFoundError, e: - raise IMDbDataAccessError( \ - 'unable to get characterID "%s": "%s"' % (characterID, e)) - res = {'name': c.name, 'imdbIndex': c.imdbIndex} - if res['imdbIndex'] is None: del res['imdbIndex'] - if not res: - raise IMDbDataAccessError('unable to get characterID "%s"' % \ - characterID) - # Collect filmography information. - items = CastInfo.select(CastInfo.q.personRoleID == characterID) - if results > 0: - items = items[:results] - filmodata = [(cd.movieID, cd.personID, cd.note, - get_movie_data(cd.movieID, self._kind)) for cd in items - if self._role[cd.roleID] in ('actor', 'actress')] - fdata = [] - for f in filmodata: - curRole = None - curRoleID = f[1] - note = f[2] or u'' - if curRoleID is not None: - robj = Name.get(curRoleID) - curRole = robj.name - m = Movie(movieID=f[0], data=f[3], - currentRole=curRole or u'', - roleID=curRoleID, roleIsPerson=True, - notes=note, accessSystem='sql') - fdata.append(m) - fdata = merge_roles(fdata) - fdata.sort() - if fdata: - res['filmography'] = fdata - return {'data': res, 'info sets': infosets} - - get_character_filmography = get_character_main - get_character_biography = get_character_main - - def _search_company(self, name, results): - name = name.strip() - if not name: return [] - if isinstance(name, unicode): - name = name.encode('ascii', 'ignore') - soundexCode = soundex(name) - # If the soundex is None, compare only with the first - # phoneticCode column. - if soundexCode is None: - condition = ISNULL(CompanyName.q.namePcodeNf) - else: - if name.endswith(']'): - condition = CompanyName.q.namePcodeSf == soundexCode - else: - condition = CompanyName.q.namePcodeNf == soundexCode - try: - qr = [(q.id, {'name': q.name, 'country': q.countryCode}) - for q in CompanyName.select(condition)] - except NotFoundError, e: - raise IMDbDataAccessError( \ - 'unable to search the database: "%s"' % str(e)) - qr[:] = [(x[0], build_company_name(x[1])) for x in qr] - res = scan_company_names(qr, name, results) - res[:] = [x[1] for x in res] - # Purge empty country keys. - returnl = [] - for x in res: - tmpd = x[1] - country = tmpd.get('country') - if country is None and 'country' in tmpd: - del tmpd['country'] - returnl.append((x[0], tmpd)) - return returnl - - def get_company_main(self, companyID, results=0): - # Every company information is retrieved from here. - infosets = self.get_company_infoset() - try: - c = CompanyName.get(companyID) - except NotFoundError, e: - raise IMDbDataAccessError( \ - 'unable to get companyID "%s": "%s"' % (companyID, e)) - res = {'name': c.name, 'country': c.countryCode} - if res['country'] is None: del res['country'] - if not res: - raise IMDbDataAccessError('unable to get companyID "%s"' % \ - companyID) - # Collect filmography information. - items = MovieCompanies.select(MovieCompanies.q.companyID == companyID) - if results > 0: - items = items[:results] - filmodata = [(cd.movieID, cd.companyID, - self._compType[cd.companyTypeID], cd.note, - get_movie_data(cd.movieID, self._kind)) for cd in items] - filmodata = _groupListBy(filmodata, 2) - for group in filmodata: - ctype = group[0][2] - for movieID, companyID, ctype, note, movieData in group: - movie = Movie(data=movieData, movieID=movieID, - notes=note or u'', accessSystem=self.accessSystem) - res.setdefault(ctype, []).append(movie) - res.get(ctype, []).sort() - return {'data': res, 'info sets': infosets} - - def _search_keyword(self, keyword, results): - constr = OR(Keyword.q.phoneticCode == - soundex(keyword.encode('ascii', 'ignore')), - CONTAINSSTRING(Keyword.q.keyword, self.toUTF8(keyword))) - return filterSimilarKeywords(keyword, - _iterKeywords(Keyword.select(constr)))[:results] - - def _get_keyword(self, keyword, results): - keyID = Keyword.select(Keyword.q.keyword == keyword) - if keyID.count() == 0: - return [] - keyID = keyID[0].id - movies = MovieKeyword.select(MovieKeyword.q.keywordID == - keyID)[:results] - return [(m.movieID, get_movie_data(m.movieID, self._kind)) - for m in movies] - - def _get_top_bottom_movies(self, kind): - if kind == 'top': - kind = 'top 250 rank' - elif kind == 'bottom': - # Not a refuse: the plain text data files contains only - # the bottom 10 movies. - kind = 'bottom 10 rank' - else: - return [] - infoID = InfoType.select(InfoType.q.info == kind) - if infoID.count() == 0: - return [] - infoID = infoID[0].id - movies = MovieInfoIdx.select(MovieInfoIdx.q.infoTypeID == infoID) - ml = [] - for m in movies: - minfo = get_movie_data(m.movieID, self._kind) - for k in kind, 'votes', 'rating', 'votes distribution': - valueDict = getSingleInfo(MovieInfoIdx, m.movieID, - k, notAList=True) - if k in (kind, 'votes') and k in valueDict: - valueDict[k] = int(valueDict[k]) - elif k == 'rating' and k in valueDict: - valueDict[k] = float(valueDict[k]) - minfo.update(valueDict) - ml.append((m.movieID, minfo)) - sorter = (_cmpBottom, _cmpTop)[kind == 'top 250 rank'] - ml.sort(sorter) - return ml - - def __del__(self): - """Ensure that the connection is closed.""" - if not hasattr(self, '_connection'): return - self._sql_logger.debug('closing connection to the database') - self._connection.close() - diff --git a/lib/imdb/parser/sql/alchemyadapter.py b/lib/imdb/parser/sql/alchemyadapter.py deleted file mode 100644 index 3872dcb77..000000000 --- a/lib/imdb/parser/sql/alchemyadapter.py +++ /dev/null @@ -1,513 +0,0 @@ -""" -parser.sql.alchemyadapter module (imdb.parser.sql package). - -This module adapts the SQLAlchemy ORM to the internal mechanism. - -Copyright 2008-2010 Davide Alberani <da@erlug.linux.it> - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation; either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA -""" - -import re -import sys -import logging -from sqlalchemy import * -from sqlalchemy import schema -try: from sqlalchemy import exc # 0.5 -except ImportError: from sqlalchemy import exceptions as exc # 0.4 - -_alchemy_logger = logging.getLogger('imdbpy.parser.sql.alchemy') - -try: - import migrate.changeset - HAS_MC = True -except ImportError: - HAS_MC = False - _alchemy_logger.warn('Unable to import migrate.changeset: Foreign ' \ - 'Keys will not be created.') - -from imdb._exceptions import IMDbDataAccessError -from dbschema import * - -# Used to convert table and column names. -re_upper = re.compile(r'([A-Z])') - -# XXX: I'm not sure at all that this is the best method to connect -# to the database and bind that connection to every table. -metadata = MetaData() - -# Maps our placeholders to SQLAlchemy's column types. -MAP_COLS = { - INTCOL: Integer, - UNICODECOL: UnicodeText, - STRINGCOL: String -} - - -class NotFoundError(IMDbDataAccessError): - """Exception raised when Table.get(id) returns no value.""" - pass - - -def _renameTable(tname): - """Build the name of a table, as done by SQLObject.""" - tname = re_upper.sub(r'_\1', tname) - if tname.startswith('_'): - tname = tname[1:] - return tname.lower() - -def _renameColumn(cname): - """Build the name of a column, as done by SQLObject.""" - cname = cname.replace('ID', 'Id') - return _renameTable(cname) - - -class DNNameObj(object): - """Used to access table.sqlmeta.columns[column].dbName (a string).""" - def __init__(self, dbName): - self.dbName = dbName - - def __repr__(self): - return '<DNNameObj(dbName=%s) [id=%s]>' % (self.dbName, id(self)) - - -class DNNameDict(object): - """Used to access table.sqlmeta.columns (a dictionary).""" - def __init__(self, colMap): - self.colMap = colMap - - def __getitem__(self, key): - return DNNameObj(self.colMap[key]) - - def __repr__(self): - return '<DNNameDict(colMap=%s) [id=%s]>' % (self.colMap, id(self)) - - -class SQLMetaAdapter(object): - """Used to access table.sqlmeta (an object with .table, .columns and - .idName attributes).""" - def __init__(self, table, colMap=None): - self.table = table - if colMap is None: - colMap = {} - self.colMap = colMap - - def __getattr__(self, name): - if name == 'table': - return getattr(self.table, name) - if name == 'columns': - return DNNameDict(self.colMap) - if name == 'idName': - return self.colMap.get('id', 'id') - return None - - def __repr__(self): - return '<SQLMetaAdapter(table=%s, colMap=%s) [id=%s]>' % \ - (repr(self.table), repr(self.colMap), id(self)) - - -class QAdapter(object): - """Used to access table.q attribute (remapped to SQLAlchemy table.c).""" - def __init__(self, table, colMap=None): - self.table = table - if colMap is None: - colMap = {} - self.colMap = colMap - - def __getattr__(self, name): - try: return getattr(self.table.c, self.colMap[name]) - except KeyError, e: raise AttributeError("unable to get '%s'" % name) - - def __repr__(self): - return '<QAdapter(table=%s, colMap=%s) [id=%s]>' % \ - (repr(self.table), repr(self.colMap), id(self)) - - -class RowAdapter(object): - """Adapter for a SQLAlchemy RowProxy object.""" - def __init__(self, row, table, colMap=None): - self.row = row - # FIXME: it's OBSCENE that 'table' should be passed from - # TableAdapter through ResultAdapter only to land here, - # where it's used to directly update a row item. - self.table = table - if colMap is None: - colMap = {} - self.colMap = colMap - self.colMapKeys = colMap.keys() - - def __getattr__(self, name): - try: return getattr(self.row, self.colMap[name]) - except KeyError, e: raise AttributeError("unable to get '%s'" % name) - - def __setattr__(self, name, value): - # FIXME: I can't even think about how much performances suffer, - # for this horrible hack (and it's used so rarely...) - # For sure something like a "property" to map column names - # to getter/setter functions would be much better, but it's - # not possible (or at least not easy) to build them for a - # single instance. - if name in self.__dict__.get('colMapKeys', ()): - # Trying to update a value in the database. - row = self.__dict__['row'] - table = self.__dict__['table'] - colMap = self.__dict__['colMap'] - params = {colMap[name]: value} - table.update(table.c.id==row.id).execute(**params) - # XXX: minor bug: after a value is assigned with the - # 'rowAdapterInstance.colName = value' syntax, for some - # reason rowAdapterInstance.colName still returns the - # previous value (even if the database is updated). - # Fix it? I'm not even sure it's ever used. - return - # For every other attribute. - object.__setattr__(self, name, value) - - def __repr__(self): - return '<RowAdapter(row=%s, table=%s, colMap=%s) [id=%s]>' % \ - (repr(self.row), repr(self.table), repr(self.colMap), id(self)) - - -class ResultAdapter(object): - """Adapter for a SQLAlchemy ResultProxy object.""" - def __init__(self, result, table, colMap=None): - self.result = result - self.table = table - if colMap is None: - colMap = {} - self.colMap = colMap - - def count(self): - return len(self) - - def __len__(self): - # FIXME: why sqlite returns -1? (that's wrooong!) - if self.result.rowcount == -1: - return 0 - return self.result.rowcount - - def __getitem__(self, key): - res = list(self.result)[key] - if not isinstance(key, slice): - # A single item. - return RowAdapter(res, self.table, colMap=self.colMap) - else: - # A (possible empty) list of items. - return [RowAdapter(x, self.table, colMap=self.colMap) - for x in res] - - def __iter__(self): - for item in self.result: - yield RowAdapter(item, self.table, colMap=self.colMap) - - def __repr__(self): - return '<ResultAdapter(result=%s, table=%s, colMap=%s) [id=%s]>' % \ - (repr(self.result), repr(self.table), - repr(self.colMap), id(self)) - - -class TableAdapter(object): - """Adapter for a SQLAlchemy Table object, to mimic a SQLObject class.""" - def __init__(self, table, uri=None): - """Initialize a TableAdapter object.""" - self._imdbpySchema = table - self._imdbpyName = table.name - self.connectionURI = uri - self.colMap = {} - columns = [] - for col in table.cols: - # Column's paramters. - params = {'nullable': True} - params.update(col.params) - if col.name == 'id': - params['primary_key'] = True - if 'notNone' in params: - params['nullable'] = not params['notNone'] - del params['notNone'] - cname = _renameColumn(col.name) - self.colMap[col.name] = cname - colClass = MAP_COLS[col.kind] - colKindParams = {} - if 'length' in params: - colKindParams['length'] = params['length'] - del params['length'] - elif colClass is UnicodeText and col.index: - # XXX: limit length for UNICODECOLs that will have an index. - # this can result in name.name and title.title truncations! - colClass = Unicode - # Should work for most of the database servers. - length = 511 - if self.connectionURI: - if self.connectionURI.startswith('mysql'): - # To stay compatible with MySQL 4.x. - length = 255 - colKindParams['length'] = length - elif self._imdbpyName == 'PersonInfo' and col.name == 'info': - if self.connectionURI: - if self.connectionURI.startswith('ibm'): - # There are some entries longer than 32KB. - colClass = CLOB - # I really do hope that this space isn't wasted - # for each other shorter entry... <g> - colKindParams['length'] = 68*1024 - colKind = colClass(**colKindParams) - if 'alternateID' in params: - # There's no need to handle them here. - del params['alternateID'] - # Create a column. - colObj = Column(cname, colKind, **params) - columns.append(colObj) - self.tableName = _renameTable(table.name) - # Create the table. - self.table = Table(self.tableName, metadata, *columns) - self._ta_insert = self.table.insert() - self._ta_select = self.table.select - # Adapters for special attributes. - self.q = QAdapter(self.table, colMap=self.colMap) - self.sqlmeta = SQLMetaAdapter(self.table, colMap=self.colMap) - - def select(self, conditions=None): - """Return a list of results.""" - result = self._ta_select(conditions).execute() - return ResultAdapter(result, self.table, colMap=self.colMap) - - def get(self, theID): - """Get an object given its ID.""" - result = self.select(self.table.c.id == theID) - #if not result: - # raise NotFoundError, 'no data for ID %s' % theID - # FIXME: isn't this a bit risky? We can't check len(result), - # because sqlite returns -1... - # What about converting it to a list and getting the first item? - try: - return result[0] - except KeyError: - raise NotFoundError('no data for ID %s' % theID) - - def dropTable(self, checkfirst=True): - """Drop the table.""" - dropParams = {'checkfirst': checkfirst} - # Guess what? Another work-around for a ibm_db bug. - if self.table.bind.engine.url.drivername.startswith('ibm_db'): - del dropParams['checkfirst'] - try: - self.table.drop(**dropParams) - except exc.ProgrammingError: - # As above: re-raise the exception, but only if it's not ibm_db. - if not self.table.bind.engine.url.drivername.startswith('ibm_db'): - raise - - def createTable(self, checkfirst=True): - """Create the table.""" - self.table.create(checkfirst=checkfirst) - # Create indexes for alternateID columns (other indexes will be - # created later, at explicit request for performances reasons). - for col in self._imdbpySchema.cols: - if col.name == 'id': - continue - if col.params.get('alternateID', False): - self._createIndex(col, checkfirst=checkfirst) - - def _createIndex(self, col, checkfirst=True): - """Create an index for a given (schema) column.""" - # XXX: indexLen is ignored in SQLAlchemy, and that means that - # indexes will be over the whole 255 chars strings... - # NOTE: don't use a dot as a separator, or DB2 will do - # nasty things. - idx_name = '%s_%s' % (self.table.name, col.index or col.name) - if checkfirst: - for index in self.table.indexes: - if index.name == idx_name: - return - idx = Index(idx_name, getattr(self.table.c, self.colMap[col.name])) - # XXX: beware that exc.OperationalError can be raised, is some - # strange circumstances; that's why the index name doesn't - # follow the SQLObject convention, but includes the table name: - # sqlite, for example, expects index names to be unique at - # db-level. - try: - idx.create() - except exc.OperationalError, e: - _alchemy_logger.warn('Skipping creation of the %s.%s index: %s' % - (self.sqlmeta.table, col.name, e)) - - def addIndexes(self, ifNotExists=True): - """Create all required indexes.""" - for col in self._imdbpySchema.cols: - if col.index: - self._createIndex(col, checkfirst=ifNotExists) - - def addForeignKeys(self, mapTables, ifNotExists=True): - """Create all required foreign keys.""" - if not HAS_MC: - return - # It seems that there's no reason to prevent the creation of - # indexes for columns with FK constrains: if there's already - # an index, the FK index is not created. - countCols = 0 - for col in self._imdbpySchema.cols: - countCols += 1 - if not col.foreignKey: - continue - fks = col.foreignKey.split('.', 1) - foreignTableName = fks[0] - if len(fks) == 2: - foreignColName = fks[1] - else: - foreignColName = 'id' - foreignColName = mapTables[foreignTableName].colMap.get( - foreignColName, foreignColName) - thisColName = self.colMap.get(col.name, col.name) - thisCol = self.table.columns[thisColName] - foreignTable = mapTables[foreignTableName].table - foreignCol = getattr(foreignTable.c, foreignColName) - # Need to explicitly set an unique name, otherwise it will - # explode, if two cols points to the same table. - fkName = 'fk_%s_%s_%d' % (foreignTable.name, foreignColName, - countCols) - constrain = migrate.changeset.ForeignKeyConstraint([thisCol], - [foreignCol], - name=fkName) - try: - constrain.create() - except exc.OperationalError: - continue - - def __call__(self, *args, **kwds): - """To insert a new row with the syntax: TableClass(key=value, ...)""" - taArgs = {} - for key, value in kwds.items(): - taArgs[self.colMap.get(key, key)] = value - self._ta_insert.execute(*args, **taArgs) - - def __repr__(self): - return '<TableAdapter(table=%s) [id=%s]>' % (repr(self.table), id(self)) - - -# Module-level "cache" for SQLObject classes, to prevent -# "Table 'tableName' is already defined for this MetaData instance" errors, -# when two or more connections to the database are made. -# XXX: is this the best way to act? -TABLES_REPOSITORY = {} - -def getDBTables(uri=None): - """Return a list of TableAdapter objects to be used to access the - database through the SQLAlchemy ORM. The connection uri is optional, and - can be used to tailor the db schema to specific needs.""" - DB_TABLES = [] - for table in DB_SCHEMA: - if table.name in TABLES_REPOSITORY: - DB_TABLES.append(TABLES_REPOSITORY[table.name]) - continue - tableAdapter = TableAdapter(table, uri) - DB_TABLES.append(tableAdapter) - TABLES_REPOSITORY[table.name] = tableAdapter - return DB_TABLES - - -# Functions used to emulate SQLObject's logical operators. -def AND(*params): - """Emulate SQLObject's AND.""" - return and_(*params) - -def OR(*params): - """Emulate SQLObject's OR.""" - return or_(*params) - -def IN(item, inList): - """Emulate SQLObject's IN.""" - if not isinstance(item, schema.Column): - return OR(*[x == item for x in inList]) - else: - return item.in_(inList) - -def ISNULL(x): - """Emulate SQLObject's ISNULL.""" - # XXX: Should we use null()? Can null() be a global instance? - # XXX: Is it safe to test None with the == operator, in this case? - return x == None - -def ISNOTNULL(x): - """Emulate SQLObject's ISNOTNULL.""" - return x != None - -def CONTAINSSTRING(expr, pattern): - """Emulate SQLObject's CONTAINSSTRING.""" - return expr.like('%%%s%%' % pattern) - - -def toUTF8(s): - """For some strange reason, sometimes SQLObject wants utf8 strings - instead of unicode; with SQLAlchemy we just return the unicode text.""" - return s - - -class _AlchemyConnection(object): - """A proxy for the connection object, required since _ConnectionFairy - uses __slots__.""" - def __init__(self, conn): - self.conn = conn - - def __getattr__(self, name): - return getattr(self.conn, name) - - -def setConnection(uri, tables, encoding='utf8', debug=False): - """Set connection for every table.""" - params = {'encoding': encoding} - # FIXME: why on earth MySQL requires an additional parameter, - # is well beyond my understanding... - if uri.startswith('mysql'): - if '?' in uri: - uri += '&' - else: - uri += '?' - uri += 'charset=%s' % encoding - - # On some server configurations, we will need to explictly enable - # loading data from local files - params['local_infile'] = 1 - - if debug: - params['echo'] = True - if uri.startswith('ibm_db'): - # Try to work-around a possible bug of the ibm_db DB2 driver. - params['convert_unicode'] = True - # XXX: is this the best way to connect? - engine = create_engine(uri, **params) - metadata.bind = engine - eng_conn = engine.connect() - if uri.startswith('sqlite'): - major = sys.version_info[0] - minor = sys.version_info[1] - if major > 2 or (major == 2 and minor > 5): - eng_conn.connection.connection.text_factory = str - # XXX: OH MY, THAT'S A MESS! - # We need to return a "connection" object, with the .dbName - # attribute set to the db engine name (e.g. "mysql"), .paramstyle - # set to the style of the paramters for query() calls, and the - # .module attribute set to a module (?) with .OperationalError and - # .IntegrityError attributes. - # Another attribute of "connection" is the getConnection() function, - # used to return an object with a .cursor() method. - connection = _AlchemyConnection(eng_conn.connection) - paramstyle = eng_conn.dialect.paramstyle - connection.module = eng_conn.dialect.dbapi - connection.paramstyle = paramstyle - connection.getConnection = lambda: connection.connection - connection.dbName = engine.url.drivername - return connection - - diff --git a/lib/imdb/parser/sql/cutils.c b/lib/imdb/parser/sql/cutils.c deleted file mode 100644 index 677c1b1e0..000000000 --- a/lib/imdb/parser/sql/cutils.c +++ /dev/null @@ -1,269 +0,0 @@ -/* - * cutils.c module. - * - * Miscellaneous functions to speed up the IMDbPY package. - * - * Contents: - * - pyratcliff(): - * Function that implements the Ratcliff-Obershelp comparison - * amongst Python strings. - * - * - pysoundex(): - * Return a soundex code string, for the given string. - * - * Copyright 2004-2009 Davide Alberani <da@erlug.linux.it> - * Released under the GPL license. - * - * NOTE: The Ratcliff-Obershelp part was heavily based on code from the - * "simil" Python module. - * The "simil" module is copyright of Luca Montecchiani <cbm64 _at_ inwind.it> - * and can be found here: http://spazioinwind.libero.it/montecchiani/ - * It was released under the GPL license; original comments are leaved - * below. - * - */ - - -/*========== Ratcliff-Obershelp ==========*/ -/***************************************************************************** - * - * Stolen code from : - * - * [Python-Dev] Why is soundex marked obsolete? - * by Eric S. Raymond [4]esr@thyrsus.com - * on Sun, 14 Jan 2001 14:09:01 -0500 - * - *****************************************************************************/ - -/***************************************************************************** - * - * Ratcliff-Obershelp common-subpattern similarity. - * - * This code first appeared in a letter to the editor in Doctor - * Dobbs's Journal, 11/1988. The original article on the algorithm, - * "Pattern Matching by Gestalt" by John Ratcliff, had appeared in the - * July 1988 issue (#181) but the algorithm was presented in assembly. - * The main drawback of the Ratcliff-Obershelp algorithm is the cost - * of the pairwise comparisons. It is significantly more expensive - * than stemming, Hamming distance, soundex, and the like. - * - * Running time quadratic in the data size, memory usage constant. - * - *****************************************************************************/ - -#include <Python.h> - -#define DONTCOMPARE_NULL 0.0 -#define DONTCOMPARE_SAME 1.0 -#define COMPARE 2.0 -#define STRING_MAXLENDIFFER 0.7 - -/* As of 05 Mar 2008, the longest title is ~600 chars. */ -#define MXLINELEN 1023 - -#define MAX(a,b) ((a) > (b) ? (a) : (b)) - - -//***************************************** -// preliminary check.... -//***************************************** -static float -strings_check(char const *s, char const *t) -{ - float threshold; // lenght difference - int s_len = strlen(s); // length of s - int t_len = strlen(t); // length of t - - // NULL strings ? - if ((t_len * s_len) == 0) - return (DONTCOMPARE_NULL); - - // the same ? - if (strcmp(s, t) == 0) - return (DONTCOMPARE_SAME); - - // string lenght difference threshold - // we don't want to compare too different lenght strings ;) - if (s_len < t_len) - threshold = (float) s_len / (float) t_len; - else - threshold = (float) t_len / (float) s_len; - if (threshold < STRING_MAXLENDIFFER) - return (DONTCOMPARE_NULL); - - // proceed - return (COMPARE); -} - - -static int -RatcliffObershelp(char *st1, char *end1, char *st2, char *end2) -{ - register char *a1, *a2; - char *b1, *b2; - char *s1 = st1, *s2 = st2; /* initializations are just to pacify GCC */ - short max, i; - - if (end1 <= st1 || end2 <= st2) - return (0); - if (end1 == st1 + 1 && end2 == st2 + 1) - return (0); - - max = 0; - b1 = end1; - b2 = end2; - - for (a1 = st1; a1 < b1; a1++) { - for (a2 = st2; a2 < b2; a2++) { - if (*a1 == *a2) { - /* determine length of common substring */ - for (i = 1; a1[i] && (a1[i] == a2[i]); i++) - continue; - if (i > max) { - max = i; - s1 = a1; - s2 = a2; - b1 = end1 - max; - b2 = end2 - max; - } - } - } - } - if (!max) - return (0); - max += RatcliffObershelp(s1 + max, end1, s2 + max, end2); /* rhs */ - max += RatcliffObershelp(st1, s1, st2, s2); /* lhs */ - return max; -} - - -static float -ratcliff(char *s1, char *s2) -/* compute Ratcliff-Obershelp similarity of two strings */ -{ - int l1, l2; - float res; - - // preliminary tests - res = strings_check(s1, s2); - if (res != COMPARE) - return(res); - - l1 = strlen(s1); - l2 = strlen(s2); - - return 2.0 * RatcliffObershelp(s1, s1 + l1, s2, s2 + l2) / (l1 + l2); -} - - -/* Change a string to lowercase. */ -static void -strtolower(char *s1) -{ - int i; - for (i=0; i < strlen(s1); i++) s1[i] = tolower(s1[i]); -} - - -/* Ratcliff-Obershelp for two python strings; returns a python float. */ -static PyObject* -pyratcliff(PyObject *self, PyObject *pArgs) -{ - char *s1 = NULL; - char *s2 = NULL; - PyObject *discard = NULL; - char s1copy[MXLINELEN+1]; - char s2copy[MXLINELEN+1]; - - /* The optional PyObject parameter is here to be compatible - * with the pure python implementation, which uses a - * difflib.SequenceMatcher object. */ - if (!PyArg_ParseTuple(pArgs, "ss|O", &s1, &s2, &discard)) - return NULL; - - strncpy(s1copy, s1, MXLINELEN); - strncpy(s2copy, s2, MXLINELEN); - /* Work on copies. */ - strtolower(s1copy); - strtolower(s2copy); - - return Py_BuildValue("f", ratcliff(s1copy, s2copy)); -} - - -/*========== soundex ==========*/ -/* Max length of the soundex code to output (an uppercase char and - * _at most_ 4 digits). */ -#define SOUNDEX_LEN 5 - -/* Group Number Lookup Table */ -static char soundTable[26] = -{ 0 /* A */, '1' /* B */, '2' /* C */, '3' /* D */, 0 /* E */, '1' /* F */, - '2' /* G */, 0 /* H */, 0 /* I */, '2' /* J */, '2' /* K */, '4' /* L */, - '5' /* M */, '5' /* N */, 0 /* O */, '1' /* P */, '2' /* Q */, '6' /* R */, - '2' /* S */, '3' /* T */, 0 /* U */, '1' /* V */, 0 /* W */, '2' /* X */, - 0 /* Y */, '2' /* Z */}; - -static PyObject* -pysoundex(PyObject *self, PyObject *pArgs) -{ - int i, j, n; - char *s = NULL; - char word[MXLINELEN+1]; - char soundCode[SOUNDEX_LEN+1]; - char c; - - if (!PyArg_ParseTuple(pArgs, "s", &s)) - return NULL; - - j = 0; - n = strlen(s); - - /* Convert to uppercase and exclude non-ascii chars. */ - for (i = 0; i < n; i++) { - c = toupper(s[i]); - if (c < 91 && c > 64) { - word[j] = c; - j++; - } - } - word[j] = '\0'; - - n = strlen(word); - if (n == 0) { - /* If the string is empty, returns None. */ - return Py_BuildValue(""); - } - soundCode[0] = word[0]; - - /* Build the soundCode string. */ - j = 1; - for (i = 1; j < SOUNDEX_LEN && i < n; i++) { - c = soundTable[(word[i]-65)]; - /* Compact zeroes and equal consecutive digits ("12234112"->"123412") */ - if (c != 0 && c != soundCode[j-1]) { - soundCode[j++] = c; - } - } - soundCode[j] = '\0'; - - return Py_BuildValue("s", soundCode); -} - - -static PyMethodDef cutils_methods[] = { - {"ratcliff", pyratcliff, - METH_VARARGS, "Ratcliff-Obershelp similarity."}, - {"soundex", pysoundex, - METH_VARARGS, "Soundex code for strings."}, - {NULL} -}; - - -void -initcutils(void) -{ - Py_InitModule("cutils", cutils_methods); -} - - diff --git a/lib/imdb/parser/sql/dbschema.py b/lib/imdb/parser/sql/dbschema.py deleted file mode 100644 index 9bb855d33..000000000 --- a/lib/imdb/parser/sql/dbschema.py +++ /dev/null @@ -1,476 +0,0 @@ -#-*- encoding: utf-8 -*- -""" -parser.sql.dbschema module (imdb.parser.sql package). - -This module provides the schema used to describe the layout of the -database used by the imdb.parser.sql package; functions to create/drop -tables and indexes are also provided. - -Copyright 2005-2012 Davide Alberani <da@erlug.linux.it> - 2006 Giuseppe "Cowo" Corbelli <cowo --> lugbs.linux.it> - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation; either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA -""" - -import logging - -_dbschema_logger = logging.getLogger('imdbpy.parser.sql.dbschema') - - -# Placeholders for column types. -INTCOL = 1 -UNICODECOL = 2 -STRINGCOL = 3 -_strMap = {1: 'INTCOL', 2: 'UNICODECOL', 3: 'STRINGCOL'} - -class DBCol(object): - """Define column objects.""" - def __init__(self, name, kind, **params): - self.name = name - self.kind = kind - self.index = None - self.indexLen = None - # If not None, two notations are accepted: 'TableName' - # and 'TableName.ColName'; in the first case, 'id' is assumed - # as the name of the pointed column. - self.foreignKey = None - if 'index' in params: - self.index = params['index'] - del params['index'] - if 'indexLen' in params: - self.indexLen = params['indexLen'] - del params['indexLen'] - if 'foreignKey' in params: - self.foreignKey = params['foreignKey'] - del params['foreignKey'] - self.params = params - - def __str__(self): - """Class representation.""" - s = '<DBCol %s %s' % (self.name, _strMap[self.kind]) - if self.index: - s += ' INDEX' - if self.indexLen: - s += '[:%d]' % self.indexLen - if self.foreignKey: - s += ' FOREIGN' - if 'default' in self.params: - val = self.params['default'] - if val is not None: - val = '"%s"' % val - s += ' DEFAULT=%s' % val - for param in self.params: - if param == 'default': continue - s += ' %s' % param.upper() - s += '>' - return s - - def __repr__(self): - """Class representation.""" - s = '<DBCol(name="%s", %s' % (self.name, _strMap[self.kind]) - if self.index: - s += ', index="%s"' % self.index - if self.indexLen: - s += ', indexLen=%d' % self.indexLen - if self.foreignKey: - s += ', foreignKey="%s"' % self.foreignKey - for param in self.params: - val = self.params[param] - if isinstance(val, (unicode, str)): - val = u'"%s"' % val - s += ', %s=%s' % (param, val) - s += ')>' - return s - - -class DBTable(object): - """Define table objects.""" - def __init__(self, name, *cols, **kwds): - self.name = name - self.cols = cols - # Default values. - self.values = kwds.get('values', {}) - - def __str__(self): - """Class representation.""" - return '<DBTable %s (%d cols, %d values)>' % (self.name, - len(self.cols), sum([len(v) for v in self.values.values()])) - - def __repr__(self): - """Class representation.""" - s = '<DBTable(name="%s"' % self.name - col_s = ', '.join([repr(col).rstrip('>').lstrip('<') - for col in self.cols]) - if col_s: - s += ', %s' % col_s - if self.values: - s += ', values=%s' % self.values - s += ')>' - return s - - -# Default values to insert in some tables: {'column': (list, of, values, ...)} -kindTypeDefs = {'kind': ('movie', 'tv series', 'tv movie', 'video movie', - 'tv mini series', 'video game', 'episode')} -companyTypeDefs = {'kind': ('distributors', 'production companies', - 'special effects companies', 'miscellaneous companies')} -infoTypeDefs = {'info': ('runtimes', 'color info', 'genres', 'languages', - 'certificates', 'sound mix', 'tech info', 'countries', 'taglines', - 'keywords', 'alternate versions', 'crazy credits', 'goofs', - 'soundtrack', 'quotes', 'release dates', 'trivia', 'locations', - 'mini biography', 'birth notes', 'birth date', 'height', - 'death date', 'spouse', 'other works', 'birth name', - 'salary history', 'nick names', 'books', 'agent address', - 'biographical movies', 'portrayed in', 'where now', 'trade mark', - 'interviews', 'article', 'magazine cover photo', 'pictorial', - 'death notes', 'LD disc format', 'LD year', 'LD digital sound', - 'LD official retail price', 'LD frequency response', 'LD pressing plant', - 'LD length', 'LD language', 'LD review', 'LD spaciality', 'LD release date', - 'LD production country', 'LD contrast', 'LD color rendition', - 'LD picture format', 'LD video noise', 'LD video artifacts', - 'LD release country', 'LD sharpness', 'LD dynamic range', - 'LD audio noise', 'LD color information', 'LD group genre', - 'LD quality program', 'LD close captions-teletext-ld-g', - 'LD category', 'LD analog left', 'LD certification', - 'LD audio quality', 'LD video quality', 'LD aspect ratio', - 'LD analog right', 'LD additional information', - 'LD number of chapter stops', 'LD dialogue intellegibility', - 'LD disc size', 'LD master format', 'LD subtitles', - 'LD status of availablility', 'LD quality of source', - 'LD number of sides', 'LD video standard', 'LD supplement', - 'LD original title', 'LD sound encoding', 'LD number', 'LD label', - 'LD catalog number', 'LD laserdisc title', 'screenplay-teleplay', - 'novel', 'adaption', 'book', 'production process protocol', - 'printed media reviews', 'essays', 'other literature', 'mpaa', - 'plot', 'votes distribution', 'votes', 'rating', - 'production dates', 'copyright holder', 'filming dates', 'budget', - 'weekend gross', 'gross', 'opening weekend', 'rentals', - 'admissions', 'studios', 'top 250 rank', 'bottom 10 rank')} -compCastTypeDefs = {'kind': ('cast', 'crew', 'complete', 'complete+verified')} -linkTypeDefs = {'link': ('follows', 'followed by', 'remake of', 'remade as', - 'references', 'referenced in', 'spoofs', 'spoofed in', - 'features', 'featured in', 'spin off from', 'spin off', - 'version of', 'similar to', 'edited into', - 'edited from', 'alternate language version of', - 'unknown link')} -roleTypeDefs = {'role': ('actor', 'actress', 'producer', 'writer', - 'cinematographer', 'composer', 'costume designer', - 'director', 'editor', 'miscellaneous crew', - 'production designer', 'guest')} - -# Schema of tables in our database. -# XXX: Foreign keys can be used to create constrains between tables, -# but they create indexes in the database, and this -# means poor performances at insert-time. -DB_SCHEMA = [ - DBTable('Name', - # namePcodeCf is the soundex of the name in the canonical format. - # namePcodeNf is the soundex of the name in the normal format, if - # different from namePcodeCf. - # surnamePcode is the soundex of the surname, if different from the - # other two values. - - # The 'id' column is simply skipped by SQLObject (it's a default); - # the alternateID attribute here will be ignored by SQLAlchemy. - DBCol('id', INTCOL, notNone=True, alternateID=True), - DBCol('name', UNICODECOL, notNone=True, index='idx_name', indexLen=6), - DBCol('imdbIndex', UNICODECOL, length=12, default=None), - DBCol('imdbID', INTCOL, default=None, index='idx_imdb_id'), - DBCol('gender', STRINGCOL, length=1, default=None), - DBCol('namePcodeCf', STRINGCOL, length=5, default=None, - index='idx_pcodecf'), - DBCol('namePcodeNf', STRINGCOL, length=5, default=None, - index='idx_pcodenf'), - DBCol('surnamePcode', STRINGCOL, length=5, default=None, - index='idx_pcode'), - DBCol('md5sum', STRINGCOL, length=32, default=None, index='idx_md5') - ), - - DBTable('CharName', - # namePcodeNf is the soundex of the name in the normal format. - # surnamePcode is the soundex of the surname, if different - # from namePcodeNf. - DBCol('id', INTCOL, notNone=True, alternateID=True), - DBCol('name', UNICODECOL, notNone=True, index='idx_name', indexLen=6), - DBCol('imdbIndex', UNICODECOL, length=12, default=None), - DBCol('imdbID', INTCOL, default=None), - DBCol('namePcodeNf', STRINGCOL, length=5, default=None, - index='idx_pcodenf'), - DBCol('surnamePcode', STRINGCOL, length=5, default=None, - index='idx_pcode'), - DBCol('md5sum', STRINGCOL, length=32, default=None, index='idx_md5') - ), - - DBTable('CompanyName', - # namePcodeNf is the soundex of the name in the normal format. - # namePcodeSf is the soundex of the name plus the country code. - DBCol('id', INTCOL, notNone=True, alternateID=True), - DBCol('name', UNICODECOL, notNone=True, index='idx_name', indexLen=6), - DBCol('countryCode', UNICODECOL, length=255, default=None), - DBCol('imdbID', INTCOL, default=None), - DBCol('namePcodeNf', STRINGCOL, length=5, default=None, - index='idx_pcodenf'), - DBCol('namePcodeSf', STRINGCOL, length=5, default=None, - index='idx_pcodesf'), - DBCol('md5sum', STRINGCOL, length=32, default=None, index='idx_md5') - ), - - DBTable('KindType', - DBCol('id', INTCOL, notNone=True, alternateID=True), - DBCol('kind', STRINGCOL, length=15, default=None, alternateID=True), - values=kindTypeDefs - ), - - DBTable('Title', - DBCol('id', INTCOL, notNone=True, alternateID=True), - DBCol('title', UNICODECOL, notNone=True, - index='idx_title', indexLen=10), - DBCol('imdbIndex', UNICODECOL, length=12, default=None), - DBCol('kindID', INTCOL, notNone=True, foreignKey='KindType'), - DBCol('productionYear', INTCOL, default=None), - DBCol('imdbID', INTCOL, default=None, index="idx_imdb_id"), - DBCol('phoneticCode', STRINGCOL, length=5, default=None, - index='idx_pcode'), - DBCol('episodeOfID', INTCOL, default=None, index='idx_epof', - foreignKey='Title'), - DBCol('seasonNr', INTCOL, default=None, index="idx_season_nr"), - DBCol('episodeNr', INTCOL, default=None, index="idx_episode_nr"), - # Maximum observed length is 44; 49 can store 5 comma-separated - # year-year pairs. - DBCol('seriesYears', STRINGCOL, length=49, default=None), - DBCol('md5sum', STRINGCOL, length=32, default=None, index='idx_md5') - ), - - DBTable('CompanyType', - DBCol('id', INTCOL, notNone=True, alternateID=True), - DBCol('kind', STRINGCOL, length=32, default=None, alternateID=True), - values=companyTypeDefs - ), - - DBTable('AkaName', - DBCol('id', INTCOL, notNone=True, alternateID=True), - DBCol('personID', INTCOL, notNone=True, index='idx_person', - foreignKey='Name'), - DBCol('name', UNICODECOL, notNone=True), - DBCol('imdbIndex', UNICODECOL, length=12, default=None), - DBCol('namePcodeCf', STRINGCOL, length=5, default=None, - index='idx_pcodecf'), - DBCol('namePcodeNf', STRINGCOL, length=5, default=None, - index='idx_pcodenf'), - DBCol('surnamePcode', STRINGCOL, length=5, default=None, - index='idx_pcode'), - DBCol('md5sum', STRINGCOL, length=32, default=None, index='idx_md5') - ), - - DBTable('AkaTitle', - # XXX: It's safer to set notNone to False, here. - # alias for akas are stored completely in the AkaTitle table; - # this means that episodes will set also a "tv series" alias name. - # Reading the aka-title.list file it looks like there are - # episode titles with aliases to different titles for both - # the episode and the series title, while for just the series - # there are no aliases. - # E.g.: - # aka title original title - # "Series, The" (2005) {The Episode} "Other Title" (2005) {Other Title} - # But there is no: - # "Series, The" (2005) "Other Title" (2005) - DBCol('id', INTCOL, notNone=True, alternateID=True), - DBCol('movieID', INTCOL, notNone=True, index='idx_movieid', - foreignKey='Title'), - DBCol('title', UNICODECOL, notNone=True), - DBCol('imdbIndex', UNICODECOL, length=12, default=None), - DBCol('kindID', INTCOL, notNone=True, foreignKey='KindType'), - DBCol('productionYear', INTCOL, default=None), - DBCol('phoneticCode', STRINGCOL, length=5, default=None, - index='idx_pcode'), - DBCol('episodeOfID', INTCOL, default=None, index='idx_epof', - foreignKey='AkaTitle'), - DBCol('seasonNr', INTCOL, default=None), - DBCol('episodeNr', INTCOL, default=None), - DBCol('note', UNICODECOL, default=None), - DBCol('md5sum', STRINGCOL, length=32, default=None, index='idx_md5') - ), - - DBTable('RoleType', - DBCol('id', INTCOL, notNone=True, alternateID=True), - DBCol('role', STRINGCOL, length=32, notNone=True, alternateID=True), - values=roleTypeDefs - ), - - DBTable('CastInfo', - DBCol('id', INTCOL, notNone=True, alternateID=True), - DBCol('personID', INTCOL, notNone=True, index='idx_pid', - foreignKey='Name'), - DBCol('movieID', INTCOL, notNone=True, index='idx_mid', - foreignKey='Title'), - DBCol('personRoleID', INTCOL, default=None, index='idx_cid', - foreignKey='CharName'), - DBCol('note', UNICODECOL, default=None), - DBCol('nrOrder', INTCOL, default=None), - DBCol('roleID', INTCOL, notNone=True, foreignKey='RoleType') - ), - - DBTable('CompCastType', - DBCol('id', INTCOL, notNone=True, alternateID=True), - DBCol('kind', STRINGCOL, length=32, notNone=True, alternateID=True), - values=compCastTypeDefs - ), - - DBTable('CompleteCast', - DBCol('id', INTCOL, notNone=True, alternateID=True), - DBCol('movieID', INTCOL, index='idx_mid', foreignKey='Title'), - DBCol('subjectID', INTCOL, notNone=True, foreignKey='CompCastType'), - DBCol('statusID', INTCOL, notNone=True, foreignKey='CompCastType') - ), - - DBTable('InfoType', - DBCol('id', INTCOL, notNone=True, alternateID=True), - DBCol('info', STRINGCOL, length=32, notNone=True, alternateID=True), - values=infoTypeDefs - ), - - DBTable('LinkType', - DBCol('id', INTCOL, notNone=True, alternateID=True), - DBCol('link', STRINGCOL, length=32, notNone=True, alternateID=True), - values=linkTypeDefs - ), - - DBTable('Keyword', - DBCol('id', INTCOL, notNone=True, alternateID=True), - # XXX: can't use alternateID=True, because it would create - # a UNIQUE index; unfortunately (at least with a common - # collation like utf8_unicode_ci) MySQL will consider - # some different keywords identical - like - # "fiancée" and "fiancee". - DBCol('keyword', UNICODECOL, notNone=True, - index='idx_keyword', indexLen=5), - DBCol('phoneticCode', STRINGCOL, length=5, default=None, - index='idx_pcode') - ), - - DBTable('MovieKeyword', - DBCol('id', INTCOL, notNone=True, alternateID=True), - DBCol('movieID', INTCOL, notNone=True, index='idx_mid', - foreignKey='Title'), - DBCol('keywordID', INTCOL, notNone=True, index='idx_keywordid', - foreignKey='Keyword') - ), - - DBTable('MovieLink', - DBCol('id', INTCOL, notNone=True, alternateID=True), - DBCol('movieID', INTCOL, notNone=True, index='idx_mid', - foreignKey='Title'), - DBCol('linkedMovieID', INTCOL, notNone=True, foreignKey='Title'), - DBCol('linkTypeID', INTCOL, notNone=True, foreignKey='LinkType') - ), - - DBTable('MovieInfo', - DBCol('id', INTCOL, notNone=True, alternateID=True), - DBCol('movieID', INTCOL, notNone=True, index='idx_mid', - foreignKey='Title'), - DBCol('infoTypeID', INTCOL, notNone=True, foreignKey='InfoType'), - DBCol('info', UNICODECOL, notNone=True), - DBCol('note', UNICODECOL, default=None) - ), - - # This table is identical to MovieInfo, except that both 'infoTypeID' - # and 'info' are indexed. - DBTable('MovieInfoIdx', - DBCol('id', INTCOL, notNone=True, alternateID=True), - DBCol('movieID', INTCOL, notNone=True, index='idx_mid', - foreignKey='Title'), - DBCol('infoTypeID', INTCOL, notNone=True, index='idx_infotypeid', - foreignKey='InfoType'), - DBCol('info', UNICODECOL, notNone=True, index='idx_info', indexLen=10), - DBCol('note', UNICODECOL, default=None) - ), - - DBTable('MovieCompanies', - DBCol('id', INTCOL, notNone=True, alternateID=True), - DBCol('movieID', INTCOL, notNone=True, index='idx_mid', - foreignKey='Title'), - DBCol('companyID', INTCOL, notNone=True, index='idx_cid', - foreignKey='CompanyName'), - DBCol('companyTypeID', INTCOL, notNone=True, foreignKey='CompanyType'), - DBCol('note', UNICODECOL, default=None) - ), - - DBTable('PersonInfo', - DBCol('id', INTCOL, notNone=True, alternateID=True), - DBCol('personID', INTCOL, notNone=True, index='idx_pid', - foreignKey='Name'), - DBCol('infoTypeID', INTCOL, notNone=True, foreignKey='InfoType'), - DBCol('info', UNICODECOL, notNone=True), - DBCol('note', UNICODECOL, default=None) - ) -] - - -# Functions to manage tables. -def dropTables(tables, ifExists=True): - """Drop the tables.""" - # In reverse order (useful to avoid errors about foreign keys). - DB_TABLES_DROP = list(tables) - DB_TABLES_DROP.reverse() - for table in DB_TABLES_DROP: - _dbschema_logger.info('dropping table %s', table._imdbpyName) - table.dropTable(ifExists) - -def createTables(tables, ifNotExists=True): - """Create the tables and insert default values.""" - for table in tables: - # Create the table. - _dbschema_logger.info('creating table %s', table._imdbpyName) - table.createTable(ifNotExists) - # Insert default values, if any. - if table._imdbpySchema.values: - _dbschema_logger.info('inserting values into table %s', - table._imdbpyName) - for key in table._imdbpySchema.values: - for value in table._imdbpySchema.values[key]: - table(**{key: unicode(value)}) - -def createIndexes(tables, ifNotExists=True): - """Create the indexes in the database. - Return a list of errors, if any.""" - errors = [] - for table in tables: - _dbschema_logger.info('creating indexes for table %s', - table._imdbpyName) - try: - table.addIndexes(ifNotExists) - except Exception, e: - errors.append(e) - continue - return errors - -def createForeignKeys(tables, ifNotExists=True): - """Create Foreign Keys. - Return a list of errors, if any.""" - errors = [] - mapTables = {} - for table in tables: - mapTables[table._imdbpyName] = table - for table in tables: - _dbschema_logger.info('creating foreign keys for table %s', - table._imdbpyName) - try: - table.addForeignKeys(mapTables, ifNotExists) - except Exception, e: - errors.append(e) - continue - return errors - diff --git a/lib/imdb/parser/sql/objectadapter.py b/lib/imdb/parser/sql/objectadapter.py deleted file mode 100644 index 170e5164c..000000000 --- a/lib/imdb/parser/sql/objectadapter.py +++ /dev/null @@ -1,211 +0,0 @@ -""" -parser.sql.objectadapter module (imdb.parser.sql package). - -This module adapts the SQLObject ORM to the internal mechanism. - -Copyright 2008-2010 Davide Alberani <da@erlug.linux.it> - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation; either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA -""" - -import sys -import logging - -from sqlobject import * -from sqlobject.sqlbuilder import ISNULL, ISNOTNULL, AND, OR, IN, CONTAINSSTRING - -from dbschema import * - -_object_logger = logging.getLogger('imdbpy.parser.sql.object') - - -# Maps our placeholders to SQLAlchemy's column types. -MAP_COLS = { - INTCOL: IntCol, - UNICODECOL: UnicodeCol, - STRINGCOL: StringCol -} - - -# Exception raised when Table.get(id) returns no value. -NotFoundError = SQLObjectNotFound - - -# class method to be added to the SQLObject class. -def addIndexes(cls, ifNotExists=True): - """Create all required indexes.""" - for col in cls._imdbpySchema.cols: - if col.index: - idxName = col.index - colToIdx = col.name - if col.indexLen: - colToIdx = {'column': col.name, 'length': col.indexLen} - if idxName in [i.name for i in cls.sqlmeta.indexes]: - # Check if the index is already present. - continue - idx = DatabaseIndex(colToIdx, name=idxName) - cls.sqlmeta.addIndex(idx) - try: - cls.createIndexes(ifNotExists) - except dberrors.OperationalError, e: - _object_logger.warn('Skipping creation of the %s.%s index: %s' % - (cls.sqlmeta.table, col.name, e)) -addIndexes = classmethod(addIndexes) - - -# Global repository for "fake" tables with Foreign Keys - need to -# prevent troubles if addForeignKeys is called more than one time. -FAKE_TABLES_REPOSITORY = {} - -def _buildFakeFKTable(cls, fakeTableName): - """Return a "fake" table, with foreign keys where needed.""" - countCols = 0 - attrs = {} - for col in cls._imdbpySchema.cols: - countCols += 1 - if col.name == 'id': - continue - if not col.foreignKey: - # A non-foreign key column - add it as usual. - attrs[col.name] = MAP_COLS[col.kind](**col.params) - continue - # XXX: Foreign Keys pointing to TableName.ColName not yet supported. - thisColName = col.name - if thisColName.endswith('ID'): - thisColName = thisColName[:-2] - - fks = col.foreignKey.split('.', 1) - foreignTableName = fks[0] - if len(fks) == 2: - foreignColName = fks[1] - else: - foreignColName = 'id' - # Unused... - #fkName = 'fk_%s_%s_%d' % (foreignTableName, foreignColName, - # countCols) - # Create a Foreign Key column, with the correct references. - fk = ForeignKey(foreignTableName, name=thisColName, default=None) - attrs[thisColName] = fk - # Build a _NEW_ SQLObject subclass, with foreign keys, if needed. - newcls = type(fakeTableName, (SQLObject,), attrs) - return newcls - -def addForeignKeys(cls, mapTables, ifNotExists=True): - """Create all required foreign keys.""" - # Do not even try, if there are no FK, in this table. - if not filter(None, [col.foreignKey for col in cls._imdbpySchema.cols]): - return - fakeTableName = 'myfaketable%s' % cls.sqlmeta.table - if fakeTableName in FAKE_TABLES_REPOSITORY: - newcls = FAKE_TABLES_REPOSITORY[fakeTableName] - else: - newcls = _buildFakeFKTable(cls, fakeTableName) - FAKE_TABLES_REPOSITORY[fakeTableName] = newcls - # Connect the class with foreign keys. - newcls.setConnection(cls._connection) - for col in cls._imdbpySchema.cols: - if col.name == 'id': - continue - if not col.foreignKey: - continue - # Get the SQL that _WOULD BE_ run, if we had to create - # this "fake" table. - fkQuery = newcls._connection.createReferenceConstraint(newcls, - newcls.sqlmeta.columns[col.name]) - if not fkQuery: - # Probably the db doesn't support foreign keys (SQLite). - continue - # Remove "myfaketable" to get references to _real_ tables. - fkQuery = fkQuery.replace('myfaketable', '') - # Execute the query. - newcls._connection.query(fkQuery) - # Disconnect it. - newcls._connection.close() -addForeignKeys = classmethod(addForeignKeys) - - -# Module-level "cache" for SQLObject classes, to prevent -# "class TheClass is already in the registry" errors, when -# two or more connections to the database are made. -# XXX: is this the best way to act? -TABLES_REPOSITORY = {} - -def getDBTables(uri=None): - """Return a list of classes to be used to access the database - through the SQLObject ORM. The connection uri is optional, and - can be used to tailor the db schema to specific needs.""" - DB_TABLES = [] - for table in DB_SCHEMA: - if table.name in TABLES_REPOSITORY: - DB_TABLES.append(TABLES_REPOSITORY[table.name]) - continue - attrs = {'_imdbpyName': table.name, '_imdbpySchema': table, - 'addIndexes': addIndexes, 'addForeignKeys': addForeignKeys} - for col in table.cols: - if col.name == 'id': - continue - attrs[col.name] = MAP_COLS[col.kind](**col.params) - # Create a subclass of SQLObject. - # XXX: use a metaclass? I can't see any advantage. - cls = type(table.name, (SQLObject,), attrs) - DB_TABLES.append(cls) - TABLES_REPOSITORY[table.name] = cls - return DB_TABLES - - -def toUTF8(s): - """For some strange reason, sometimes SQLObject wants utf8 strings - instead of unicode.""" - return s.encode('utf_8') - - -def setConnection(uri, tables, encoding='utf8', debug=False): - """Set connection for every table.""" - kw = {} - # FIXME: it's absolutely unclear what we should do to correctly - # support unicode in MySQL; with some versions of SQLObject, - # it seems that setting use_unicode=1 is the _wrong_ thing to do. - _uriLower = uri.lower() - if _uriLower.startswith('mysql'): - kw['use_unicode'] = 1 - #kw['sqlobject_encoding'] = encoding - kw['charset'] = encoding - - # On some server configurations, we will need to explictly enable - # loading data from local files - kw['local_infile'] = 1 - conn = connectionForURI(uri, **kw) - conn.debug = debug - # XXX: doesn't work and a work-around was put in imdbpy2sql.py; - # is there any way to modify the text_factory parameter of - # a SQLite connection? - #if uri.startswith('sqlite'): - # major = sys.version_info[0] - # minor = sys.version_info[1] - # if major > 2 or (major == 2 and minor > 5): - # sqliteConn = conn.getConnection() - # sqliteConn.text_factory = str - for table in tables: - table.setConnection(conn) - #table.sqlmeta.cacheValues = False - # FIXME: is it safe to set table._cacheValue to False? Looks like - # we can't retrieve correct values after an update (I think - # it's never needed, but...) Anyway, these are set to False - # for performance reason at insert time (see imdbpy2sql.py). - table._cacheValue = False - # Required by imdbpy2sql.py. - conn.paramstyle = conn.module.paramstyle - return conn - diff --git a/lib/imdb/utils.py b/lib/imdb/utils.py index 0ffb58a84..284edae0d 100644 --- a/lib/imdb/utils.py +++ b/lib/imdb/utils.py @@ -18,7 +18,7 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """ from __future__ import generators @@ -431,13 +431,13 @@ def analyze_title(title, canonical=None, canonicalSeries=None, yi = [(yiy, yii)] if yk == 'TV episode': kind = u'episode' - elif yk == 'TV': + elif yk in ('TV', 'TV Movie'): kind = u'tv movie' elif yk == 'TV Series': kind = u'tv series' elif yk == 'Video': kind = u'video movie' - elif yk == 'TV mini-series': + elif yk in ('TV mini-series', 'TV Mini-Series'): kind = u'tv mini series' elif yk == 'Video Game': kind = u'video game' @@ -960,7 +960,7 @@ def _tag4TON(ton, addAccessSystem=False, _containerOnly=False): crl = [crl] for cr in crl: crTag = cr.__class__.__name__.lower() - crValue = cr['long imdb name'] + crValue = cr.get('long imdb name') or u'' crValue = _normalizeValue(crValue) crID = cr.getID() if crID is not None: diff --git a/lib/jwt/__init__.py b/lib/jwt/__init__.py new file mode 100644 index 000000000..398d8540d --- /dev/null +++ b/lib/jwt/__init__.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# flake8: noqa + +""" +JSON Web Token implementation + +Minimum implementation based on this spec: +http://self-issued.info/docs/draft-jones-json-web-token-01.html +""" + + +__title__ = 'pyjwt' +__version__ = '1.5.0' +__author__ = 'José Padilla' +__license__ = 'MIT' +__copyright__ = 'Copyright 2015 José Padilla' + + +from .api_jwt import ( + encode, decode, register_algorithm, unregister_algorithm, + get_unverified_header, PyJWT +) +from .api_jws import PyJWS +from .exceptions import ( + InvalidTokenError, DecodeError, InvalidAudienceError, + ExpiredSignatureError, ImmatureSignatureError, InvalidIssuedAtError, + InvalidIssuerError, ExpiredSignature, InvalidAudience, InvalidIssuer, + MissingRequiredClaimError +) diff --git a/lib/jwt/__main__.py b/lib/jwt/__main__.py new file mode 100644 index 000000000..3ccc38c5f --- /dev/null +++ b/lib/jwt/__main__.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python + +from __future__ import absolute_import, print_function + +import json +import optparse +import sys +import time + +from . import DecodeError, __package__, __version__, decode, encode + + +def main(): + + usage = '''Encodes or decodes JSON Web Tokens based on input. + + %prog [options] input + +Decoding examples: + + %prog --key=secret json.web.token + %prog --no-verify json.web.token + +Encoding requires the key option and takes space separated key/value pairs +separated by equals (=) as input. Examples: + + %prog --key=secret iss=me exp=1302049071 + %prog --key=secret foo=bar exp=+10 + +The exp key is special and can take an offset to current Unix time.\ +''' + p = optparse.OptionParser( + usage=usage, + prog='pyjwt', + version='%s %s' % (__package__, __version__), + ) + + p.add_option( + '-n', '--no-verify', + action='store_false', + dest='verify', + default=True, + help='ignore signature and claims verification on decode' + ) + + p.add_option( + '--key', + dest='key', + metavar='KEY', + default=None, + help='set the secret key to sign with' + ) + + p.add_option( + '--alg', + dest='algorithm', + metavar='ALG', + default='HS256', + help='set crypto algorithm to sign with. default=HS256' + ) + + options, arguments = p.parse_args() + + if len(arguments) > 0 or not sys.stdin.isatty(): + if len(arguments) == 1 and (not options.verify or options.key): + # Try to decode + try: + if not sys.stdin.isatty(): + token = sys.stdin.read() + else: + token = arguments[0] + + token = token.encode('utf-8') + data = decode(token, key=options.key, verify=options.verify) + + print(json.dumps(data)) + sys.exit(0) + except DecodeError as e: + print(e) + sys.exit(1) + + # Try to encode + if options.key is None: + print('Key is required when encoding. See --help for usage.') + sys.exit(1) + + # Build payload object to encode + payload = {} + + for arg in arguments: + try: + k, v = arg.split('=', 1) + + # exp +offset special case? + if k == 'exp' and v[0] == '+' and len(v) > 1: + v = str(int(time.time()+int(v[1:]))) + + # Cast to integer? + if v.isdigit(): + v = int(v) + else: + # Cast to float? + try: + v = float(v) + except ValueError: + pass + + # Cast to true, false, or null? + constants = {'true': True, 'false': False, 'null': None} + + if v in constants: + v = constants[v] + + payload[k] = v + except ValueError: + print('Invalid encoding input at {}'.format(arg)) + sys.exit(1) + + try: + token = encode( + payload, + key=options.key, + algorithm=options.algorithm + ) + + print(token) + sys.exit(0) + except Exception as e: + print(e) + sys.exit(1) + else: + p.print_help() + + +if __name__ == '__main__': + main() diff --git a/lib/jwt/algorithms.py b/lib/jwt/algorithms.py new file mode 100644 index 000000000..f6d990adc --- /dev/null +++ b/lib/jwt/algorithms.py @@ -0,0 +1,428 @@ +import hashlib +import hmac +import json + + +from .compat import constant_time_compare, string_types +from .exceptions import InvalidKeyError +from .utils import ( + base64url_decode, base64url_encode, der_to_raw_signature, + force_bytes, force_unicode, from_base64url_uint, raw_to_der_signature, + to_base64url_uint +) + +try: + from cryptography.hazmat.primitives import hashes + from cryptography.hazmat.primitives.serialization import ( + load_pem_private_key, load_pem_public_key, load_ssh_public_key + ) + from cryptography.hazmat.primitives.asymmetric.rsa import ( + RSAPrivateKey, RSAPublicKey, RSAPrivateNumbers, RSAPublicNumbers, + rsa_recover_prime_factors, rsa_crt_dmp1, rsa_crt_dmq1, rsa_crt_iqmp + ) + from cryptography.hazmat.primitives.asymmetric.ec import ( + EllipticCurvePrivateKey, EllipticCurvePublicKey + ) + from cryptography.hazmat.primitives.asymmetric import ec, padding + from cryptography.hazmat.backends import default_backend + from cryptography.exceptions import InvalidSignature + + has_crypto = True +except ImportError: + has_crypto = False + +requires_cryptography = set(['RS256', 'RS384', 'RS512', 'ES256', 'ES384', + 'ES521', 'ES512', 'PS256', 'PS384', 'PS512']) + + +def get_default_algorithms(): + """ + Returns the algorithms that are implemented by the library. + """ + default_algorithms = { + 'none': NoneAlgorithm(), + 'HS256': HMACAlgorithm(HMACAlgorithm.SHA256), + 'HS384': HMACAlgorithm(HMACAlgorithm.SHA384), + 'HS512': HMACAlgorithm(HMACAlgorithm.SHA512) + } + + if has_crypto: + default_algorithms.update({ + 'RS256': RSAAlgorithm(RSAAlgorithm.SHA256), + 'RS384': RSAAlgorithm(RSAAlgorithm.SHA384), + 'RS512': RSAAlgorithm(RSAAlgorithm.SHA512), + 'ES256': ECAlgorithm(ECAlgorithm.SHA256), + 'ES384': ECAlgorithm(ECAlgorithm.SHA384), + 'ES521': ECAlgorithm(ECAlgorithm.SHA512), + 'ES512': ECAlgorithm(ECAlgorithm.SHA512), # Backward compat for #219 fix + 'PS256': RSAPSSAlgorithm(RSAPSSAlgorithm.SHA256), + 'PS384': RSAPSSAlgorithm(RSAPSSAlgorithm.SHA384), + 'PS512': RSAPSSAlgorithm(RSAPSSAlgorithm.SHA512) + }) + + return default_algorithms + + +class Algorithm(object): + """ + The interface for an algorithm used to sign and verify tokens. + """ + def prepare_key(self, key): + """ + Performs necessary validation and conversions on the key and returns + the key value in the proper format for sign() and verify(). + """ + raise NotImplementedError + + def sign(self, msg, key): + """ + Returns a digital signature for the specified message + using the specified key value. + """ + raise NotImplementedError + + def verify(self, msg, key, sig): + """ + Verifies that the specified digital signature is valid + for the specified message and key values. + """ + raise NotImplementedError + + @staticmethod + def to_jwk(key_obj): + """ + Serializes a given RSA key into a JWK + """ + raise NotImplementedError + + @staticmethod + def from_jwk(jwk): + """ + Deserializes a given RSA key from JWK back into a PublicKey or PrivateKey object + """ + raise NotImplementedError + + +class NoneAlgorithm(Algorithm): + """ + Placeholder for use when no signing or verification + operations are required. + """ + def prepare_key(self, key): + if key == '': + key = None + + if key is not None: + raise InvalidKeyError('When alg = "none", key value must be None.') + + return key + + def sign(self, msg, key): + return b'' + + def verify(self, msg, key, sig): + return False + + +class HMACAlgorithm(Algorithm): + """ + Performs signing and verification operations using HMAC + and the specified hash function. + """ + SHA256 = hashlib.sha256 + SHA384 = hashlib.sha384 + SHA512 = hashlib.sha512 + + def __init__(self, hash_alg): + self.hash_alg = hash_alg + + def prepare_key(self, key): + key = force_bytes(key) + + invalid_strings = [ + b'-----BEGIN PUBLIC KEY-----', + b'-----BEGIN CERTIFICATE-----', + b'ssh-rsa' + ] + + if any([string_value in key for string_value in invalid_strings]): + raise InvalidKeyError( + 'The specified key is an asymmetric key or x509 certificate and' + ' should not be used as an HMAC secret.') + + return key + + @staticmethod + def to_jwk(key_obj): + return json.dumps({ + 'k': force_unicode(base64url_encode(force_bytes(key_obj))), + 'kty': 'oct' + }) + + @staticmethod + def from_jwk(jwk): + obj = json.loads(jwk) + + if obj.get('kty') != 'oct': + raise InvalidKeyError('Not an HMAC key') + + return base64url_decode(obj['k']) + + def sign(self, msg, key): + return hmac.new(key, msg, self.hash_alg).digest() + + def verify(self, msg, key, sig): + return constant_time_compare(sig, self.sign(msg, key)) + + +if has_crypto: + + class RSAAlgorithm(Algorithm): + """ + Performs signing and verification operations using + RSASSA-PKCS-v1_5 and the specified hash function. + """ + SHA256 = hashes.SHA256 + SHA384 = hashes.SHA384 + SHA512 = hashes.SHA512 + + def __init__(self, hash_alg): + self.hash_alg = hash_alg + + def prepare_key(self, key): + if isinstance(key, RSAPrivateKey) or \ + isinstance(key, RSAPublicKey): + return key + + if isinstance(key, string_types): + key = force_bytes(key) + + try: + if key.startswith(b'ssh-rsa'): + key = load_ssh_public_key(key, backend=default_backend()) + else: + key = load_pem_private_key(key, password=None, backend=default_backend()) + except ValueError: + key = load_pem_public_key(key, backend=default_backend()) + else: + raise TypeError('Expecting a PEM-formatted key.') + + return key + + @staticmethod + def to_jwk(key_obj): + obj = None + + if getattr(key_obj, 'private_numbers', None): + # Private key + numbers = key_obj.private_numbers() + + obj = { + 'kty': 'RSA', + 'key_ops': ['sign'], + 'n': force_unicode(to_base64url_uint(numbers.public_numbers.n)), + 'e': force_unicode(to_base64url_uint(numbers.public_numbers.e)), + 'd': force_unicode(to_base64url_uint(numbers.d)), + 'p': force_unicode(to_base64url_uint(numbers.p)), + 'q': force_unicode(to_base64url_uint(numbers.q)), + 'dp': force_unicode(to_base64url_uint(numbers.dmp1)), + 'dq': force_unicode(to_base64url_uint(numbers.dmq1)), + 'qi': force_unicode(to_base64url_uint(numbers.iqmp)) + } + + elif getattr(key_obj, 'verifier', None): + # Public key + numbers = key_obj.public_numbers() + + obj = { + 'kty': 'RSA', + 'key_ops': ['verify'], + 'n': force_unicode(to_base64url_uint(numbers.n)), + 'e': force_unicode(to_base64url_uint(numbers.e)) + } + else: + raise InvalidKeyError('Not a public or private key') + + return json.dumps(obj) + + @staticmethod + def from_jwk(jwk): + try: + obj = json.loads(jwk) + except ValueError: + raise InvalidKeyError('Key is not valid JSON') + + if obj.get('kty') != 'RSA': + raise InvalidKeyError('Not an RSA key') + + if 'd' in obj and 'e' in obj and 'n' in obj: + # Private key + if 'oth' in obj: + raise InvalidKeyError('Unsupported RSA private key: > 2 primes not supported') + + other_props = ['p', 'q', 'dp', 'dq', 'qi'] + props_found = [prop in obj for prop in other_props] + any_props_found = any(props_found) + + if any_props_found and not all(props_found): + raise InvalidKeyError('RSA key must include all parameters if any are present besides d') + + public_numbers = RSAPublicNumbers( + from_base64url_uint(obj['e']), from_base64url_uint(obj['n']) + ) + + if any_props_found: + numbers = RSAPrivateNumbers( + d=from_base64url_uint(obj['d']), + p=from_base64url_uint(obj['p']), + q=from_base64url_uint(obj['q']), + dmp1=from_base64url_uint(obj['dp']), + dmq1=from_base64url_uint(obj['dq']), + iqmp=from_base64url_uint(obj['qi']), + public_numbers=public_numbers + ) + else: + d = from_base64url_uint(obj['d']) + p, q = rsa_recover_prime_factors( + public_numbers.n, d, public_numbers.e + ) + + numbers = RSAPrivateNumbers( + d=d, + p=p, + q=q, + dmp1=rsa_crt_dmp1(d, p), + dmq1=rsa_crt_dmq1(d, q), + iqmp=rsa_crt_iqmp(p, q), + public_numbers=public_numbers + ) + + return numbers.private_key(default_backend()) + elif 'n' in obj and 'e' in obj: + # Public key + numbers = RSAPublicNumbers( + from_base64url_uint(obj['e']), from_base64url_uint(obj['n']) + ) + + return numbers.public_key(default_backend()) + else: + raise InvalidKeyError('Not a public or private key') + + def sign(self, msg, key): + signer = key.signer( + padding.PKCS1v15(), + self.hash_alg() + ) + + signer.update(msg) + return signer.finalize() + + def verify(self, msg, key, sig): + verifier = key.verifier( + sig, + padding.PKCS1v15(), + self.hash_alg() + ) + + verifier.update(msg) + + try: + verifier.verify() + return True + except InvalidSignature: + return False + + class ECAlgorithm(Algorithm): + """ + Performs signing and verification operations using + ECDSA and the specified hash function + """ + SHA256 = hashes.SHA256 + SHA384 = hashes.SHA384 + SHA512 = hashes.SHA512 + + def __init__(self, hash_alg): + self.hash_alg = hash_alg + + def prepare_key(self, key): + if isinstance(key, EllipticCurvePrivateKey) or \ + isinstance(key, EllipticCurvePublicKey): + return key + + if isinstance(key, string_types): + key = force_bytes(key) + + # Attempt to load key. We don't know if it's + # a Signing Key or a Verifying Key, so we try + # the Verifying Key first. + try: + if key.startswith(b'ecdsa-sha2-'): + key = load_ssh_public_key(key, backend=default_backend()) + else: + key = load_pem_public_key(key, backend=default_backend()) + except ValueError: + key = load_pem_private_key(key, password=None, backend=default_backend()) + + else: + raise TypeError('Expecting a PEM-formatted key.') + + return key + + def sign(self, msg, key): + signer = key.signer(ec.ECDSA(self.hash_alg())) + + signer.update(msg) + der_sig = signer.finalize() + + return der_to_raw_signature(der_sig, key.curve) + + def verify(self, msg, key, sig): + try: + der_sig = raw_to_der_signature(sig, key.curve) + except ValueError: + return False + + verifier = key.verifier(der_sig, ec.ECDSA(self.hash_alg())) + + verifier.update(msg) + + try: + verifier.verify() + return True + except InvalidSignature: + return False + + class RSAPSSAlgorithm(RSAAlgorithm): + """ + Performs a signature using RSASSA-PSS with MGF1 + """ + + def sign(self, msg, key): + signer = key.signer( + padding.PSS( + mgf=padding.MGF1(self.hash_alg()), + salt_length=self.hash_alg.digest_size + ), + self.hash_alg() + ) + + signer.update(msg) + return signer.finalize() + + def verify(self, msg, key, sig): + verifier = key.verifier( + sig, + padding.PSS( + mgf=padding.MGF1(self.hash_alg()), + salt_length=self.hash_alg.digest_size + ), + self.hash_alg() + ) + + verifier.update(msg) + + try: + verifier.verify() + return True + except InvalidSignature: + return False diff --git a/lib/jwt/api_jws.py b/lib/jwt/api_jws.py new file mode 100644 index 000000000..66b6a675e --- /dev/null +++ b/lib/jwt/api_jws.py @@ -0,0 +1,215 @@ +import binascii +import json +import warnings + +from collections import Mapping + +from .algorithms import ( + Algorithm, get_default_algorithms, has_crypto, requires_cryptography # NOQA +) +from .compat import binary_type, string_types, text_type +from .exceptions import DecodeError, InvalidAlgorithmError, InvalidTokenError +from .utils import base64url_decode, base64url_encode, force_bytes, merge_dict + + +class PyJWS(object): + header_typ = 'JWT' + + def __init__(self, algorithms=None, options=None): + self._algorithms = get_default_algorithms() + self._valid_algs = (set(algorithms) if algorithms is not None + else set(self._algorithms)) + + # Remove algorithms that aren't on the whitelist + for key in list(self._algorithms.keys()): + if key not in self._valid_algs: + del self._algorithms[key] + + if not options: + options = {} + + self.options = merge_dict(self._get_default_options(), options) + + @staticmethod + def _get_default_options(): + return { + 'verify_signature': True + } + + def register_algorithm(self, alg_id, alg_obj): + """ + Registers a new Algorithm for use when creating and verifying tokens. + """ + if alg_id in self._algorithms: + raise ValueError('Algorithm already has a handler.') + + if not isinstance(alg_obj, Algorithm): + raise TypeError('Object is not of type `Algorithm`') + + self._algorithms[alg_id] = alg_obj + self._valid_algs.add(alg_id) + + def unregister_algorithm(self, alg_id): + """ + Unregisters an Algorithm for use when creating and verifying tokens + Throws KeyError if algorithm is not registered. + """ + if alg_id not in self._algorithms: + raise KeyError('The specified algorithm could not be removed' + ' because it is not registered.') + + del self._algorithms[alg_id] + self._valid_algs.remove(alg_id) + + def get_algorithms(self): + """ + Returns a list of supported values for the 'alg' parameter. + """ + return list(self._valid_algs) + + def encode(self, payload, key, algorithm='HS256', headers=None, + json_encoder=None): + segments = [] + + if algorithm is None: + algorithm = 'none' + + if algorithm not in self._valid_algs: + pass + + # Header + header = {'typ': self.header_typ, 'alg': algorithm} + + if headers: + self._validate_headers(headers) + header.update(headers) + + json_header = force_bytes( + json.dumps( + header, + separators=(',', ':'), + cls=json_encoder + ) + ) + + segments.append(base64url_encode(json_header)) + segments.append(base64url_encode(payload)) + + # Segments + signing_input = b'.'.join(segments) + try: + alg_obj = self._algorithms[algorithm] + key = alg_obj.prepare_key(key) + signature = alg_obj.sign(signing_input, key) + + except KeyError: + if not has_crypto and algorithm in requires_cryptography: + raise NotImplementedError( + "Algorithm '%s' could not be found. Do you have cryptography " + "installed?" % algorithm + ) + else: + raise NotImplementedError('Algorithm not supported') + + segments.append(base64url_encode(signature)) + + return b'.'.join(segments) + + def decode(self, jws, key='', verify=True, algorithms=None, options=None, + **kwargs): + payload, signing_input, header, signature = self._load(jws) + + if verify: + merged_options = merge_dict(self.options, options) + if merged_options.get('verify_signature'): + self._verify_signature(payload, signing_input, header, signature, + key, algorithms) + else: + warnings.warn('The verify parameter is deprecated. ' + 'Please use options instead.', DeprecationWarning) + + return payload + + def get_unverified_header(self, jwt): + """Returns back the JWT header parameters as a dict() + + Note: The signature is not verified so the header parameters + should not be fully trusted until signature verification is complete + """ + headers = self._load(jwt)[2] + self._validate_headers(headers) + + return headers + + def _load(self, jwt): + if isinstance(jwt, text_type): + jwt = jwt.encode('utf-8') + + if not issubclass(type(jwt), binary_type): + raise DecodeError("Invalid token type. Token must be a {0}".format( + binary_type)) + + try: + signing_input, crypto_segment = jwt.rsplit(b'.', 1) + header_segment, payload_segment = signing_input.split(b'.', 1) + except ValueError: + raise DecodeError('Not enough segments') + + try: + header_data = base64url_decode(header_segment) + except (TypeError, binascii.Error): + raise DecodeError('Invalid header padding') + + try: + header = json.loads(header_data.decode('utf-8')) + except ValueError as e: + raise DecodeError('Invalid header string: %s' % e) + + if not isinstance(header, Mapping): + raise DecodeError('Invalid header string: must be a json object') + + try: + payload = base64url_decode(payload_segment) + except (TypeError, binascii.Error): + raise DecodeError('Invalid payload padding') + + try: + signature = base64url_decode(crypto_segment) + except (TypeError, binascii.Error): + raise DecodeError('Invalid crypto padding') + + return (payload, signing_input, header, signature) + + def _verify_signature(self, payload, signing_input, header, signature, + key='', algorithms=None): + + alg = header.get('alg') + + if algorithms is not None and alg not in algorithms: + raise InvalidAlgorithmError('The specified alg value is not allowed') + + try: + alg_obj = self._algorithms[alg] + key = alg_obj.prepare_key(key) + + if not alg_obj.verify(signing_input, key, signature): + raise DecodeError('Signature verification failed') + + except KeyError: + raise InvalidAlgorithmError('Algorithm not supported') + + def _validate_headers(self, headers): + if 'kid' in headers: + self._validate_kid(headers['kid']) + + def _validate_kid(self, kid): + if not isinstance(kid, string_types): + raise InvalidTokenError('Key ID header parameter must be a string') + + +_jws_global_obj = PyJWS() +encode = _jws_global_obj.encode +decode = _jws_global_obj.decode +register_algorithm = _jws_global_obj.register_algorithm +unregister_algorithm = _jws_global_obj.unregister_algorithm +get_unverified_header = _jws_global_obj.get_unverified_header diff --git a/lib/jwt/api_jwt.py b/lib/jwt/api_jwt.py new file mode 100644 index 000000000..bca682311 --- /dev/null +++ b/lib/jwt/api_jwt.py @@ -0,0 +1,183 @@ +import json +import warnings + +from calendar import timegm +from collections import Mapping +from datetime import datetime, timedelta + +from .api_jws import PyJWS +from .algorithms import Algorithm, get_default_algorithms # NOQA +from .compat import string_types, timedelta_total_seconds +from .exceptions import ( + DecodeError, ExpiredSignatureError, ImmatureSignatureError, + InvalidAudienceError, InvalidIssuedAtError, + InvalidIssuerError, MissingRequiredClaimError +) +from .utils import merge_dict + + +class PyJWT(PyJWS): + header_type = 'JWT' + + @staticmethod + def _get_default_options(): + return { + 'verify_signature': True, + 'verify_exp': True, + 'verify_nbf': True, + 'verify_iat': True, + 'verify_aud': True, + 'verify_iss': True, + 'require_exp': False, + 'require_iat': False, + 'require_nbf': False + } + + def encode(self, payload, key, algorithm='HS256', headers=None, + json_encoder=None): + # Check that we get a mapping + if not isinstance(payload, Mapping): + raise TypeError('Expecting a mapping object, as JWT only supports ' + 'JSON objects as payloads.') + + # Payload + for time_claim in ['exp', 'iat', 'nbf']: + # Convert datetime to a intDate value in known time-format claims + if isinstance(payload.get(time_claim), datetime): + payload[time_claim] = timegm(payload[time_claim].utctimetuple()) + + json_payload = json.dumps( + payload, + separators=(',', ':'), + cls=json_encoder + ).encode('utf-8') + + return super(PyJWT, self).encode( + json_payload, key, algorithm, headers, json_encoder + ) + + def decode(self, jwt, key='', verify=True, algorithms=None, options=None, + **kwargs): + payload, signing_input, header, signature = self._load(jwt) + + decoded = super(PyJWT, self).decode(jwt, key, verify, algorithms, + options, **kwargs) + + try: + payload = json.loads(decoded.decode('utf-8')) + except ValueError as e: + raise DecodeError('Invalid payload string: %s' % e) + if not isinstance(payload, Mapping): + raise DecodeError('Invalid payload string: must be a json object') + + if verify: + merged_options = merge_dict(self.options, options) + self._validate_claims(payload, merged_options, **kwargs) + + return payload + + def _validate_claims(self, payload, options, audience=None, issuer=None, + leeway=0, **kwargs): + + if 'verify_expiration' in kwargs: + options['verify_exp'] = kwargs.get('verify_expiration', True) + warnings.warn('The verify_expiration parameter is deprecated. ' + 'Please use options instead.', DeprecationWarning) + + if isinstance(leeway, timedelta): + leeway = timedelta_total_seconds(leeway) + + if not isinstance(audience, (string_types, type(None))): + raise TypeError('audience must be a string or None') + + self._validate_required_claims(payload, options) + + now = timegm(datetime.utcnow().utctimetuple()) + + if 'iat' in payload and options.get('verify_iat'): + self._validate_iat(payload, now, leeway) + + if 'nbf' in payload and options.get('verify_nbf'): + self._validate_nbf(payload, now, leeway) + + if 'exp' in payload and options.get('verify_exp'): + self._validate_exp(payload, now, leeway) + + if options.get('verify_iss'): + self._validate_iss(payload, issuer) + + if options.get('verify_aud'): + self._validate_aud(payload, audience) + + def _validate_required_claims(self, payload, options): + if options.get('require_exp') and payload.get('exp') is None: + raise MissingRequiredClaimError('exp') + + if options.get('require_iat') and payload.get('iat') is None: + raise MissingRequiredClaimError('iat') + + if options.get('require_nbf') and payload.get('nbf') is None: + raise MissingRequiredClaimError('nbf') + + def _validate_iat(self, payload, now, leeway): + try: + int(payload['iat']) + except ValueError: + raise InvalidIssuedAtError('Issued At claim (iat) must be an integer.') + + def _validate_nbf(self, payload, now, leeway): + try: + nbf = int(payload['nbf']) + except ValueError: + raise DecodeError('Not Before claim (nbf) must be an integer.') + + if nbf > (now + leeway): + raise ImmatureSignatureError('The token is not yet valid (nbf)') + + def _validate_exp(self, payload, now, leeway): + try: + exp = int(payload['exp']) + except ValueError: + raise DecodeError('Expiration Time claim (exp) must be an' + ' integer.') + + if exp < (now - leeway): + raise ExpiredSignatureError('Signature has expired') + + def _validate_aud(self, payload, audience): + if audience is None and 'aud' not in payload: + return + + if audience is not None and 'aud' not in payload: + # Application specified an audience, but it could not be + # verified since the token does not contain a claim. + raise MissingRequiredClaimError('aud') + + audience_claims = payload['aud'] + + if isinstance(audience_claims, string_types): + audience_claims = [audience_claims] + if not isinstance(audience_claims, list): + raise InvalidAudienceError('Invalid claim format in token') + if any(not isinstance(c, string_types) for c in audience_claims): + raise InvalidAudienceError('Invalid claim format in token') + if audience not in audience_claims: + raise InvalidAudienceError('Invalid audience') + + def _validate_iss(self, payload, issuer): + if issuer is None: + return + + if 'iss' not in payload: + raise MissingRequiredClaimError('iss') + + if payload['iss'] != issuer: + raise InvalidIssuerError('Invalid issuer') + + +_jwt_global_obj = PyJWT() +encode = _jwt_global_obj.encode +decode = _jwt_global_obj.decode +register_algorithm = _jwt_global_obj.register_algorithm +unregister_algorithm = _jwt_global_obj.unregister_algorithm +get_unverified_header = _jwt_global_obj.get_unverified_header diff --git a/lib/jwt/compat.py b/lib/jwt/compat.py new file mode 100644 index 000000000..b928c7d2e --- /dev/null +++ b/lib/jwt/compat.py @@ -0,0 +1,76 @@ +""" +The `compat` module provides support for backwards compatibility with older +versions of python, and compatibility wrappers around optional packages. +""" +# flake8: noqa +import hmac +import struct +import sys + + +PY3 = sys.version_info[0] == 3 + + +if PY3: + text_type = str + binary_type = bytes +else: + text_type = unicode + binary_type = str + +string_types = (text_type, binary_type) + + +def timedelta_total_seconds(delta): + try: + delta.total_seconds + except AttributeError: + # On Python 2.6, timedelta instances do not have + # a .total_seconds() method. + total_seconds = delta.days * 24 * 60 * 60 + delta.seconds + else: + total_seconds = delta.total_seconds() + + return total_seconds + + +try: + constant_time_compare = hmac.compare_digest +except AttributeError: + # Fallback for Python < 2.7 + def constant_time_compare(val1, val2): + """ + Returns True if the two strings are equal, False otherwise. + + The time taken is independent of the number of characters that match. + """ + if len(val1) != len(val2): + return False + + result = 0 + + for x, y in zip(val1, val2): + result |= ord(x) ^ ord(y) + + return result == 0 + +# Use int.to_bytes if it exists (Python 3) +if getattr(int, 'to_bytes', None): + def bytes_from_int(val): + remaining = val + byte_length = 0 + + while remaining != 0: + remaining = remaining >> 8 + byte_length += 1 + + return val.to_bytes(byte_length, 'big', signed=False) +else: + def bytes_from_int(val): + buf = [] + while val: + val, remainder = divmod(val, 256) + buf.append(remainder) + + buf.reverse() + return struct.pack('%sB' % len(buf), *buf) diff --git a/lib/dateutil/test/__init__.py b/lib/jwt/contrib/__init__.py similarity index 100% rename from lib/dateutil/test/__init__.py rename to lib/jwt/contrib/__init__.py diff --git a/lib/jwt/contrib/algorithms/__init__.py b/lib/jwt/contrib/algorithms/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/lib/jwt/contrib/algorithms/py_ecdsa.py b/lib/jwt/contrib/algorithms/py_ecdsa.py new file mode 100644 index 000000000..bf0dea5ae --- /dev/null +++ b/lib/jwt/contrib/algorithms/py_ecdsa.py @@ -0,0 +1,60 @@ +# Note: This file is named py_ecdsa.py because import behavior in Python 2 +# would cause ecdsa.py to squash the ecdsa library that it depends upon. + +import hashlib + +import ecdsa + +from jwt.algorithms import Algorithm +from jwt.compat import string_types, text_type + + +class ECAlgorithm(Algorithm): + """ + Performs signing and verification operations using + ECDSA and the specified hash function + + This class requires the ecdsa package to be installed. + + This is based off of the implementation in PyJWT 0.3.2 + """ + SHA256 = hashlib.sha256 + SHA384 = hashlib.sha384 + SHA512 = hashlib.sha512 + + def __init__(self, hash_alg): + self.hash_alg = hash_alg + + def prepare_key(self, key): + + if isinstance(key, ecdsa.SigningKey) or \ + isinstance(key, ecdsa.VerifyingKey): + return key + + if isinstance(key, string_types): + if isinstance(key, text_type): + key = key.encode('utf-8') + + # Attempt to load key. We don't know if it's + # a Signing Key or a Verifying Key, so we try + # the Verifying Key first. + try: + key = ecdsa.VerifyingKey.from_pem(key) + except ecdsa.der.UnexpectedDER: + key = ecdsa.SigningKey.from_pem(key) + + else: + raise TypeError('Expecting a PEM-formatted key.') + + return key + + def sign(self, msg, key): + return key.sign(msg, hashfunc=self.hash_alg, + sigencode=ecdsa.util.sigencode_string) + + def verify(self, msg, key, sig): + try: + return key.verify(sig, msg, hashfunc=self.hash_alg, + sigdecode=ecdsa.util.sigdecode_string) + except AssertionError: + return False diff --git a/lib/jwt/contrib/algorithms/pycrypto.py b/lib/jwt/contrib/algorithms/pycrypto.py new file mode 100644 index 000000000..e6afaa59d --- /dev/null +++ b/lib/jwt/contrib/algorithms/pycrypto.py @@ -0,0 +1,47 @@ +import Crypto.Hash.SHA256 +import Crypto.Hash.SHA384 +import Crypto.Hash.SHA512 + +from Crypto.PublicKey import RSA +from Crypto.Signature import PKCS1_v1_5 + +from jwt.algorithms import Algorithm +from jwt.compat import string_types, text_type + + +class RSAAlgorithm(Algorithm): + """ + Performs signing and verification operations using + RSASSA-PKCS-v1_5 and the specified hash function. + + This class requires PyCrypto package to be installed. + + This is based off of the implementation in PyJWT 0.3.2 + """ + SHA256 = Crypto.Hash.SHA256 + SHA384 = Crypto.Hash.SHA384 + SHA512 = Crypto.Hash.SHA512 + + def __init__(self, hash_alg): + self.hash_alg = hash_alg + + def prepare_key(self, key): + + if isinstance(key, RSA._RSAobj): + return key + + if isinstance(key, string_types): + if isinstance(key, text_type): + key = key.encode('utf-8') + + key = RSA.importKey(key) + else: + raise TypeError('Expecting a PEM- or RSA-formatted key.') + + return key + + def sign(self, msg, key): + return PKCS1_v1_5.new(key).sign(self.hash_alg.new(msg)) + + def verify(self, msg, key, sig): + return PKCS1_v1_5.new(key).verify(self.hash_alg.new(msg), sig) diff --git a/lib/jwt/exceptions.py b/lib/jwt/exceptions.py new file mode 100644 index 000000000..31177a0a2 --- /dev/null +++ b/lib/jwt/exceptions.py @@ -0,0 +1,48 @@ +class InvalidTokenError(Exception): + pass + + +class DecodeError(InvalidTokenError): + pass + + +class ExpiredSignatureError(InvalidTokenError): + pass + + +class InvalidAudienceError(InvalidTokenError): + pass + + +class InvalidIssuerError(InvalidTokenError): + pass + + +class InvalidIssuedAtError(InvalidTokenError): + pass + + +class ImmatureSignatureError(InvalidTokenError): + pass + + +class InvalidKeyError(Exception): + pass + + +class InvalidAlgorithmError(InvalidTokenError): + pass + + +class MissingRequiredClaimError(InvalidTokenError): + def __init__(self, claim): + self.claim = claim + + def __str__(self): + return 'Token is missing the "%s" claim' % self.claim + + +# Compatibility aliases (deprecated) +ExpiredSignature = ExpiredSignatureError +InvalidAudience = InvalidAudienceError +InvalidIssuer = InvalidIssuerError diff --git a/lib/jwt/utils.py b/lib/jwt/utils.py new file mode 100644 index 000000000..b33c7a2d4 --- /dev/null +++ b/lib/jwt/utils.py @@ -0,0 +1,113 @@ +import base64 +import binascii +import struct + +from .compat import binary_type, bytes_from_int, text_type + +try: + from cryptography.hazmat.primitives.asymmetric.utils import ( + decode_dss_signature, encode_dss_signature + ) +except ImportError: + pass + + +def force_unicode(value): + if isinstance(value, binary_type): + return value.decode('utf-8') + elif isinstance(value, text_type): + return value + else: + raise TypeError('Expected a string value') + + +def force_bytes(value): + if isinstance(value, text_type): + return value.encode('utf-8') + elif isinstance(value, binary_type): + return value + else: + raise TypeError('Expected a string value') + + +def base64url_decode(input): + if isinstance(input, text_type): + input = input.encode('ascii') + + rem = len(input) % 4 + + if rem > 0: + input += b'=' * (4 - rem) + + return base64.urlsafe_b64decode(input) + + +def base64url_encode(input): + return base64.urlsafe_b64encode(input).replace(b'=', b'') + + +def to_base64url_uint(val): + if val < 0: + raise ValueError('Must be a positive integer') + + int_bytes = bytes_from_int(val) + + if len(int_bytes) == 0: + int_bytes = b'\x00' + + return base64url_encode(int_bytes) + + +def from_base64url_uint(val): + if isinstance(val, text_type): + val = val.encode('ascii') + + data = base64url_decode(val) + + buf = struct.unpack('%sB' % len(data), data) + return int(''.join(["%02x" % byte for byte in buf]), 16) + + +def merge_dict(original, updates): + if not updates: + return original + + try: + merged_options = original.copy() + merged_options.update(updates) + except (AttributeError, ValueError) as e: + raise TypeError('original and updates must be a dictionary: %s' % e) + + return merged_options + + +def number_to_bytes(num, num_bytes): + padded_hex = '%0*x' % (2 * num_bytes, num) + big_endian = binascii.a2b_hex(padded_hex.encode('ascii')) + return big_endian + + +def bytes_to_number(string): + return int(binascii.b2a_hex(string), 16) + + +def der_to_raw_signature(der_sig, curve): + num_bits = curve.key_size + num_bytes = (num_bits + 7) // 8 + + r, s = decode_dss_signature(der_sig) + + return number_to_bytes(r, num_bytes) + number_to_bytes(s, num_bytes) + + +def raw_to_der_signature(raw_sig, curve): + num_bits = curve.key_size + num_bytes = (num_bits + 7) // 8 + + if len(raw_sig) != 2 * num_bytes: + raise ValueError('Invalid signature') + + r = bytes_to_number(raw_sig[:num_bytes]) + s = bytes_to_number(raw_sig[num_bytes:]) + + return encode_dss_signature(r, s) diff --git a/lib/markdown2.py b/lib/markdown2.py index 41c8f19dd..0c77519a8 100644 --- a/lib/markdown2.py +++ b/lib/markdown2.py @@ -56,6 +56,8 @@ see <https://github.com/trentm/python-markdown2/wiki/Extras> for details): string to use for a "class" tag attribute. Currently only supports "img", "table", "pre" and "code" tags. Add an issue if you require this for other tags. +* link-patterns: Auto-link given regex patterns in text (e.g. bug number + references, revision number references). * markdown-in-html: Allow the use of `markdown="1"` in a block HTML tag to have markdown processing be done on its contents. Similar to <http://michelf.com/projects/php-markdown/extra/#markdown-attr> but with @@ -64,23 +66,28 @@ see <https://github.com/trentm/python-markdown2/wiki/Extras> for details): See <https://github.com/trentm/python-markdown2/issues/77> for details. * nofollow: Add `rel="nofollow"` to add `<a>` tags with an href. See <http://en.wikipedia.org/wiki/Nofollow>. +* numbering: Support of generic counters. Non standard extension to + allow sequential numbering of figures, tables, equations, exhibits etc. * pyshell: Treats unindented Python interactive shell sessions as <code> blocks. -* link-patterns: Auto-link given regex patterns in text (e.g. bug number - references, revision number references). * smarty-pants: Replaces ' and " with curly quotation marks or curly apostrophes. Replaces --, ---, ..., and . . . with en dashes, em dashes, and ellipses. * spoiler: A special kind of blockquote commonly hidden behind a click on SO. Syntax per <http://meta.stackexchange.com/a/72878>. -* toc: The returned HTML string gets a new "toc_html" attribute which is - a Table of Contents for the document. (experimental) -* xml: Passes one-liner processing instructions and namespaced XML tags. +* tag-friendly: Requires atx style headers to have a space between the # and + the header text. Useful for applications that require twitter style tags to + pass through the parser. * tables: Tables using the same format as GFM <https://help.github.com/articles/github-flavored-markdown#tables> and PHP-Markdown Extra <https://michelf.ca/projects/php-markdown/extra/#table>. +* toc: The returned HTML string gets a new "toc_html" attribute which is + a Table of Contents for the document. (experimental) +* use-file-vars: Look for an Emacs-style markdown-extras file variable to turn + on Extras. * wiki-tables: Google Code Wiki-style tables. See <http://code.google.com/p/support/wiki/WikiSyntax#Tables>. +* xml: Passes one-liner processing instructions and namespaced XML tags. """ # Dev Notes: @@ -88,13 +95,11 @@ see <https://github.com/trentm/python-markdown2/wiki/Extras> for details): # not yet sure if there implications with this. Compare 'pydoc sre' # and 'perldoc perlre'. -__version_info__ = (2, 3, 1) +__version_info__ = (2, 3, 4) __version__ = '.'.join(map(str, __version_info__)) __author__ = "Trent Mick" -import os import sys -from pprint import pprint, pformat import re import logging try: @@ -104,17 +109,15 @@ except ImportError: import optparse from random import random, randint import codecs - - -#---- Python version compat - try: - from urllib.parse import quote # python3 + from urllib import quote_plus except ImportError: - from urllib import quote # python2 + from urllib.parse import quote_plus + -if sys.version_info[:2] < (2,4): - from sets import Set as set +# ---- Python version compat + +if sys.version_info[:2] < (2, 4): def reversed(sequence): for i in sequence[::-1]: yield i @@ -132,9 +135,7 @@ elif sys.version_info[0] >= 3: unicode = str base_string_type = str - - -#---- globals +# ---- globals DEBUG = False log = logging.getLogger("markdown") @@ -151,19 +152,17 @@ g_escape_table = dict([(ch, _hash_text(ch)) for ch in '\\`*_{}[]()>#+-.!']) - -#---- exceptions - +# ---- exceptions class MarkdownError(Exception): pass - -#---- public api +# ---- public api def markdown_path(path, encoding="utf-8", html4tags=False, tab_width=DEFAULT_TAB_WIDTH, safe_mode=None, extras=None, link_patterns=None, + footnote_title=None, footnote_return_symbol=None, use_file_vars=False): fp = codecs.open(path, 'r', encoding) text = fp.read() @@ -171,16 +170,23 @@ def markdown_path(path, encoding="utf-8", return Markdown(html4tags=html4tags, tab_width=tab_width, safe_mode=safe_mode, extras=extras, link_patterns=link_patterns, + footnote_title=footnote_title, + footnote_return_symbol=footnote_return_symbol, use_file_vars=use_file_vars).convert(text) + def markdown(text, html4tags=False, tab_width=DEFAULT_TAB_WIDTH, safe_mode=None, extras=None, link_patterns=None, + footnote_title=None, footnote_return_symbol=None, use_file_vars=False): return Markdown(html4tags=html4tags, tab_width=tab_width, safe_mode=safe_mode, extras=extras, link_patterns=link_patterns, + footnote_title=footnote_title, + footnote_return_symbol=footnote_return_symbol, use_file_vars=use_file_vars).convert(text) + class Markdown(object): # The dict of "extras" to enable in processing -- a mapping of # extra name to argument for the extra. Most extras do not have an @@ -203,7 +209,9 @@ class Markdown(object): _ws_only_line_re = re.compile(r"^[ \t]+$", re.M) def __init__(self, html4tags=False, tab_width=4, safe_mode=None, - extras=None, link_patterns=None, use_file_vars=False): + extras=None, link_patterns=None, + footnote_title=None, footnote_return_symbol=None, + use_file_vars=False): if html4tags: self.empty_element_suffix = ">" else: @@ -228,11 +236,13 @@ class Markdown(object): extras = dict([(e, None) for e in extras]) self.extras.update(extras) assert isinstance(self.extras, dict) - if "toc" in self.extras and not "header-ids" in self.extras: + if "toc" in self.extras and "header-ids" not in self.extras: self.extras["header-ids"] = None # "toc" implies "header-ids" self._instance_extras = self.extras.copy() self.link_patterns = link_patterns + self.footnote_title = footnote_title + self.footnote_return_symbol = footnote_return_symbol self.use_file_vars = use_file_vars self._outdent_re = re.compile(r'^(\t|[ ]{1,%d})' % tab_width, re.M) @@ -252,13 +262,28 @@ class Markdown(object): self.footnotes = {} self.footnote_ids = [] if "header-ids" in self.extras: - self._count_from_header_id = {} # no `defaultdict` in Python 2.4 + self._count_from_header_id = {} # no `defaultdict` in Python 2.4 if "metadata" in self.extras: self.metadata = {} # Per <https://developer.mozilla.org/en-US/docs/HTML/Element/a> "rel" # should only be used in <a> tags with an "href" attribute. - _a_nofollow = re.compile(r"<(a)([^>]*href=)", re.IGNORECASE) + _a_nofollow = re.compile(r""" + <(a) + ( + [^>]* + href= # href is required + ['"]? # HTML5 attribute values do not have to be quoted + [^#'"] # We don't want to match href values that start with # (like footnotes) + ) + """, + re.IGNORECASE | re.VERBOSE + ) + + # Opens the linked document in a new window or tab + # should only used in <a> tags with an "href" attribute. + # same with _a_nofollow + _a_blank = _a_nofollow def convert(self, text): """Convert the given text.""" @@ -274,7 +299,7 @@ class Markdown(object): self.reset() if not isinstance(text, unicode): - #TODO: perhaps shouldn't presume UTF-8 for string input? + # TODO: perhaps shouldn't presume UTF-8 for string input? text = unicode(text, 'utf-8') if self.use_file_vars: @@ -294,7 +319,8 @@ class Markdown(object): self.extras[ename] = earg # Standardize line endings: - text = re.sub("\r\n|\r", "\n", text) + text = text.replace("\r\n", "\n") + text = text.replace("\r", "\n") # Make sure $text ends with a couple of newlines: text += "\n\n" @@ -326,6 +352,11 @@ class Markdown(object): if "fenced-code-blocks" in self.extras and self.safe_mode: text = self._do_fenced_code_blocks(text) + # Because numbering references aren't links (yet?) then we can do everything associated with counters + # before we get started + if "numbering" in self.extras: + text = self._do_numbering(text) + # Strip link definitions, store in hashes. if "footnotes" in self.extras: # Must do footnotes first because an unlucky footnote defn @@ -349,6 +380,9 @@ class Markdown(object): if "nofollow" in self.extras: text = self._a_nofollow.sub(r'<\1 rel="nofollow"\2', text) + if "target-blank-links" in self.extras: + text = self._a_blank.sub(r'<\1 target="_blank"\2', text) + text += "\n" rv = UnicodeWithAttrs(text) @@ -372,30 +406,53 @@ class Markdown(object): """ return text - # Is metadata if the content starts with '---'-fenced `key: value` + # Is metadata if the content starts with optional '---'-fenced `key: value` # pairs. E.g. (indented for presentation): # --- # foo: bar # another-var: blah blah # --- - _metadata_pat = re.compile("""^---[ \t]*\n((?:[ \t]*[^ \t:]+[ \t]*:[^\n]*\n)+)---[ \t]*\n""") + # # header + # or: + # foo: bar + # another-var: blah blah + # + # # header + _meta_data_pattern = re.compile(r'^(?:---[\ \t]*\n)?(.*:\s+>\n\s+[\S\s]+?)(?=\n\w+\s*:\s*\w+\n|\Z)|([\S\w]+\s*:(?! >)[ \t]*.*\n?)(?:---[\ \t]*\n)?', re.MULTILINE) + _key_val_pat = re.compile("[\S\w]+\s*:(?! >)[ \t]*.*\n?", re.MULTILINE) + # this allows key: > + # value + # conutiues over multiple lines + _key_val_block_pat = re.compile( + "(.*:\s+>\n\s+[\S\s]+?)(?=\n\w+\s*:\s*\w+\n|\Z)", re.MULTILINE) + _meta_data_fence_pattern = re.compile(r'^---[\ \t]*\n', re.MULTILINE) + _meta_data_newline = re.compile("^\n", re.MULTILINE) def _extract_metadata(self, text): - # fast test - if not text.startswith("---"): - return text - match = self._metadata_pat.match(text) - if not match: - return text + if text.startswith("---"): + fence_splits = re.split(self._meta_data_fence_pattern, text, maxsplit=2) + metadata_content = fence_splits[1] + match = re.findall(self._meta_data_pattern, metadata_content) + if not match: + return text + tail = fence_splits[2] + else: + metadata_split = re.split(self._meta_data_newline, text, maxsplit=1) + metadata_content = metadata_split[0] + match = re.findall(self._meta_data_pattern, metadata_content) + if not match: + return text + tail = metadata_split[1] - tail = text[len(match.group(0)):] - metadata_str = match.group(1).strip() - for line in metadata_str.split('\n'): - key, value = line.split(':', 1) - self.metadata[key.strip()] = value.strip() + kv = re.findall(self._key_val_pat, text) + kvm = re.findall(self._key_val_block_pat, text) + kvm = [item.replace(": >\n", ":", 1) for item in kvm] - return tail + for item in kv + kvm: + k, v = item.split(":", 1) + self.metadata[k.strip()] = v.strip() + return tail _emacs_oneliner_vars_pat = re.compile(r"-\*-\s*([^\r\n]*?)\s*-\*-", re.UNICODE) # This regular expression is intended to match blocks like this: @@ -421,7 +478,7 @@ class Markdown(object): http://www.gnu.org/software/emacs/manual/html_node/emacs/Specifying-File-Variables.html#Specifying-File-Variables """ emacs_vars = {} - SIZE = pow(2, 13) # 8kB + SIZE = pow(2, 13) # 8kB # Search near the start for a '-*-'-style one-liner of variables. head = text[:SIZE] @@ -457,7 +514,7 @@ class Markdown(object): prefix = match.group("prefix") suffix = match.group("suffix") lines = match.group("content").splitlines(0) - #print "prefix=%r, suffix=%r, content=%r, lines: %s"\ + # print "prefix=%r, suffix=%r, content=%r, lines: %s"\ # % (prefix, suffix, match.group("content"), lines) # Validate the Local Variables block: proper prefix and suffix @@ -478,9 +535,9 @@ class Markdown(object): # Parse out one emacs var per line. continued_for = None - for line in lines[:-1]: # no var on the last line ("PREFIX End:") - if prefix: line = line[len(prefix):] # strip prefix - if suffix: line = line[:-len(suffix)] # strip suffix + for line in lines[:-1]: # no var on the last line ("PREFIX End:") + if prefix: line = line[len(prefix):] # strip prefix + if suffix: line = line[:-len(suffix)] # strip suffix line = line.strip() if continued_for: variable = continued_for @@ -514,14 +571,19 @@ class Markdown(object): return emacs_vars - # Cribbed from a post by Bart Lateur: - # <http://www.nntp.perl.org/group/perl.macperl.anyperl/154> - _detab_re = re.compile(r'(.*?)\t', re.M) - def _detab_sub(self, match): - g1 = match.group(1) - return g1 + (' ' * (self.tab_width - len(g1) % self.tab_width)) + def _detab_line(self, line): + r"""Recusively convert tabs to spaces in a single line. + + Called from _detab().""" + if '\t' not in line: + return line + chunk1, chunk2 = line.split('\t', 1) + chunk1 += (' ' * (self.tab_width - len(chunk1) % self.tab_width)) + output = chunk1 + chunk2 + return self._detab_line(output) + def _detab(self, text): - r"""Remove (leading?) tabs from a file. + r"""Iterate text line by line and convert tabs to spaces. >>> m = Markdown() >>> m._detab("\tfoo") @@ -537,7 +599,10 @@ class Markdown(object): """ if '\t' not in text: return text - return self._detab_re.subn(self._detab_sub, text)[0] + output = [] + for line in text.splitlines(): + output.append(self._detab_line(line)) + return '\n'.join(output) # I broke out the html5 tags here and add them to _block_tags_a and # _block_tags_b. This way html5 tags are easy to keep track of. @@ -743,6 +808,64 @@ class Markdown(object): self.titles[key] = title return "" + def _do_numbering(self, text): + ''' We handle the special extension for generic numbering for + tables, figures etc. + ''' + # First pass to define all the references + self.regex_defns = re.compile(r''' + \[\#(\w+)\s* # the counter. Open square plus hash plus a word \1 + ([^@]*)\s* # Some optional characters, that aren't an @. \2 + @(\w+) # the id. Should this be normed? \3 + ([^\]]*)\] # The rest of the text up to the terminating ] \4 + ''', re.VERBOSE) + self.regex_subs = re.compile(r"\[@(\w+)\s*\]") # [@ref_id] + counters = {} + references = {} + replacements = [] + definition_html = '<figcaption class="{}" id="counter-ref-{}">{}{}{}</figcaption>' + reference_html = '<a class="{}" href="#counter-ref-{}">{}</a>' + for match in self.regex_defns.finditer(text): + # We must have four match groups otherwise this isn't a numbering reference + if len(match.groups()) != 4: + continue + counter = match.group(1) + text_before = match.group(2) + ref_id = match.group(3) + text_after = match.group(4) + number = counters.get(counter, 1) + references[ref_id] = (number, counter) + replacements.append((match.start(0), + definition_html.format(counter, + ref_id, + text_before, + number, + text_after), + match.end(0))) + counters[counter] = number + 1 + for repl in reversed(replacements): + text = text[:repl[0]] + repl[1] + text[repl[2]:] + + # Second pass to replace the references with the right + # value of the counter + # Fwiw, it's vaguely annoying to have to turn the iterator into + # a list and then reverse it but I can't think of a better thing to do. + for match in reversed(list(self.regex_subs.finditer(text))): + number, counter = references.get(match.group(1), (None, None)) + if number is not None: + repl = reference_html.format(counter, + match.group(1), + number) + else: + repl = reference_html.format(match.group(1), + 'countererror', + '?' + match.group(1) + '?') + if "smarty-pants" in self.extras: + repl = repl.replace('"', self._escape_table['"']) + + text = text[:match.start()] + repl + text[match.end():] + return text + def _extract_footnote_def_sub(self, match): id, text = match.groups() text = _dedent(text, skip_first_line=not text.startswith('\n')).strip() @@ -831,7 +954,7 @@ class Markdown(object): lines = match.group(0).splitlines(0) _dedentlines(lines) indent = ' ' * self.tab_width - s = ('\n' # separate from possible cuddled paragraph + s = ('\n' # separate from possible cuddled paragraph + indent + ('\n'+indent).join(lines) + '\n\n') return s @@ -853,10 +976,15 @@ class Markdown(object): return _pyshell_block_re.sub(self._pyshell_block_sub, text) def _table_sub(self, match): + trim_space_re = '^[ \t\n]+|[ \t\n]+$' + trim_bar_re = '^\||\|$' + split_bar_re = '^\||(?<!\\\\)\|' + escape_bar_re = '\\\\\|' + head, underline, body = match.groups() # Determine aligns for columns. - cols = [cell.strip() for cell in underline.strip('| \t\n').split('|')] + cols = [re.sub(escape_bar_re, '|', cell.strip()) for cell in re.split(split_bar_re, re.sub(trim_bar_re, "", re.sub(trim_space_re, "", underline)))] align_from_col_idx = {} for col_idx, col in enumerate(cols): if col[0] == ':' and col[-1] == ':': @@ -868,7 +996,7 @@ class Markdown(object): # thead hlines = ['<table%s>' % self._html_class_str_from_tag('table'), '<thead>', '<tr>'] - cols = [cell.strip() for cell in head.strip('| \t\n').split('|')] + cols = [re.sub(escape_bar_re, '|', cell.strip()) for cell in re.split(split_bar_re, re.sub(trim_bar_re, "", re.sub(trim_space_re, "", head)))] for col_idx, col in enumerate(cols): hlines.append(' <th%s>%s</th>' % ( align_from_col_idx.get(col_idx, ''), @@ -881,7 +1009,7 @@ class Markdown(object): hlines.append('<tbody>') for line in body.strip('\n').split('\n'): hlines.append('<tr>') - cols = [cell.strip() for cell in line.strip('| \t\n').split('|')] + cols = [re.sub(escape_bar_re, '|', cell.strip()) for cell in re.split(split_bar_re, re.sub(trim_bar_re, "", re.sub(trim_space_re, "", line)))] for col_idx, col in enumerate(cols): hlines.append(' <td%s>%s</td>' % ( align_from_col_idx.get(col_idx, ''), @@ -924,13 +1052,13 @@ class Markdown(object): def _wiki_table_sub(self, match): ttext = match.group(0).strip() - #print 'wiki table: %r' % match.group(0) + # print 'wiki table: %r' % match.group(0) rows = [] for line in ttext.splitlines(0): line = line.strip()[2:-2].strip() row = [c.strip() for c in re.split(r'(?<!\\)\|\|', line)] rows.append(row) - #pprint(rows) + # pprint(rows) hlines = ['<table%s>' % self._html_class_str_from_tag('table'), '<tbody>'] for row in rows: hrow = ['<tr>'] @@ -977,6 +1105,9 @@ class Markdown(object): text = self._encode_amps_and_angles(text) + if "strike" in self.extras: + text = self._do_strike(text) + text = self._do_italics_and_bold(text) if "smarty-pants" in self.extras: @@ -1140,6 +1271,7 @@ class Markdown(object): url = self._strip_anglebrackets.sub(r'\1', url) return url, title, end_idx + _safe_protocols = re.compile(r'(https?|ftp):', re.I) def _do_links(self, text): """Turn Markdown link shortcuts into XHTML <a> and <img> tags. @@ -1157,7 +1289,7 @@ class Markdown(object): anchor_allowed_pos = 0 curr_pos = 0 - while True: # Handle the next link. + while True: # Handle the next link. # The next '[' is the start of: # - an inline anchor: [text](url "title") # - a reference anchor: [text][id] @@ -1221,7 +1353,7 @@ class Markdown(object): return text # Inline anchor or img? - if text[p] == '(': # attempt at perf improvement + if text[p] == '(': # attempt at perf improvement url, title, url_end_idx = self._extract_url_and_title(text, p) if url is not None: # Handle an inline anchor or img. @@ -1243,16 +1375,21 @@ class Markdown(object): if is_img: img_class_str = self._html_class_str_from_tag("img") result = '<img src="%s" alt="%s"%s%s%s' \ - % (url.replace('"', '"'), + % (_html_escape_url(url, safe_mode=self.safe_mode), _xml_escape_attr(link_text), - title_str, img_class_str, self.empty_element_suffix) + title_str, + img_class_str, + self.empty_element_suffix) if "smarty-pants" in self.extras: result = result.replace('"', self._escape_table['"']) curr_pos = start_idx + len(result) text = text[:start_idx] + result + text[url_end_idx:] elif start_idx >= anchor_allowed_pos: - result_head = '<a href="%s"%s>' % (url, title_str) - result = '%s%s</a>' % (result_head, link_text) + if self.safe_mode and not self._safe_protocols.match(url): + result_head = '<a href="#"%s>' % (title_str) + else: + result_head = '<a href="%s"%s>' % (_html_escape_url(url, safe_mode=self.safe_mode), title_str) + result = '%s%s</a>' % (result_head, _xml_escape_attr(link_text)) if "smarty-pants" in self.extras: result = result.replace('"', self._escape_table['"']) # <img> allowed from curr_pos on, <a> from @@ -1284,7 +1421,6 @@ class Markdown(object): .replace('_', self._escape_table['_']) title = self.titles.get(link_id) if title: - before = title title = _xml_escape_attr(title) \ .replace('*', self._escape_table['*']) \ .replace('_', self._escape_table['_']) @@ -1294,17 +1430,20 @@ class Markdown(object): if is_img: img_class_str = self._html_class_str_from_tag("img") result = '<img src="%s" alt="%s"%s%s%s' \ - % (url.replace('"', '"'), - link_text.replace('"', '"'), - title_str, img_class_str, self.empty_element_suffix) + % (_html_escape_url(url, safe_mode=self.safe_mode), + _xml_escape_attr(link_text), + title_str, + img_class_str, + self.empty_element_suffix) if "smarty-pants" in self.extras: result = result.replace('"', self._escape_table['"']) curr_pos = start_idx + len(result) text = text[:start_idx] + result + text[match.end():] elif start_idx >= anchor_allowed_pos: - result = '<a href="%s"%s>%s</a>' \ - % (url, title_str, link_text) - result_head = '<a href="%s"%s>' % (url, title_str) + if self.safe_mode and not self._safe_protocols.match(url): + result_head = '<a href="#"%s>' % (title_str) + else: + result_head = '<a href="%s"%s>' % (_html_escape_url(url, safe_mode=self.safe_mode), title_str) result = '%s%s</a>' % (result_head, link_text) if "smarty-pants" in self.extras: result = result.replace('"', self._escape_table['"']) @@ -1349,6 +1488,9 @@ class Markdown(object): header_id += '-%s' % self._count_from_header_id[header_id] else: self._count_from_header_id[header_id] = 1 + if 0 == len(header_id): + header_id += '-%s' % self._count_from_header_id[header_id] + return header_id _toc = None @@ -1416,7 +1558,7 @@ class Markdown(object): return self._h_re_tag_friendly.sub(self._h_sub, text) return self._h_re.sub(self._h_sub, text) - _marker_ul_chars = '*+-' + _marker_ul_chars = '*+-' _marker_any = r'(?:[%s]|\d+\.)' % _marker_ul_chars _marker_ul = '(?:[%s])' % _marker_ul_chars _marker_ol = r'(?:\d+\.)' @@ -1478,7 +1620,7 @@ class Markdown(object): start, end = match.span() middle = self._list_sub(match) text = text[:start] + middle + text[end:] - pos = start + len(middle) # start pos for next attempted match + pos = start + len(middle) # start pos for next attempted match return text @@ -1487,16 +1629,30 @@ class Markdown(object): (^[ \t]*) # leading whitespace = \2 (?P<marker>%s) [ \t]+ # list marker = \3 ((?:.+?) # list item text = \4 - (\n{1,2})) # eols = \5 + (\n{1,2})) # eols = \5 (?= \n* (\Z | \2 (?P<next_marker>%s) [ \t]+)) ''' % (_marker_any, _marker_any), re.M | re.X | re.S) + _task_list_item_re = re.compile(r''' + (\[[\ x]\])[ \t]+ # tasklist marker = \1 + (.*) # list item text = \2 + ''', re.M | re.X | re.S) + + _task_list_warpper_str = r'<input type="checkbox" class="task-list-item-checkbox" %sdisabled> %s' + + def _task_list_item_sub(self, match): + marker = match.group(1) + item_text = match.group(2) + if marker == '[x]': + return self._task_list_warpper_str % ('checked ', item_text) + elif marker == '[ ]': + return self._task_list_warpper_str % ('', item_text) + _last_li_endswith_two_eols = False def _list_item_sub(self, match): item = match.group(4) leading_line = match.group(1) - leading_space = match.group(2) if leading_line or "\n\n" in item or self._last_li_endswith_two_eols: item = self._run_block_gamut(self._outdent(item)) else: @@ -1506,6 +1662,10 @@ class Markdown(object): item = item[:-1] item = self._run_span_gamut(item) self._last_li_endswith_two_eols = (len(match.group(5)) == 2) + + if "task_list" in self.extras: + item = self._task_list_item_re.sub(self._task_list_item_sub, item) + return "<li>%s</li>\n" % item def _process_list_items(self, list_str): @@ -1594,7 +1754,7 @@ class Markdown(object): formatter_opts = self.extras['code-color'] or {} if lexer_name: - def unhash_code( codeblock ): + def unhash_code(codeblock): for key, sanitized in list(self.html_spans.items()): codeblock = codeblock.replace(key, sanitized) replacements = [ @@ -1652,14 +1812,14 @@ class Markdown(object): return code_block_re.sub(self._code_block_sub, text) _fenced_code_block_re = re.compile(r''' - (?:\n\n|\A\n?) + (?:\n+|\A\n?) ^```([\w+-]+)?[ \t]*\n # opening fence, $1 = optional lang (.*?) # $2 = code block content ^```[ \t]*\n # closing fence ''', re.M | re.X | re.S) def _fenced_code_block_sub(self, match): - return self._code_block_sub(match, is_fenced_code_block=True); + return self._code_block_sub(match, is_fenced_code_block=True) def _do_fenced_code_blocks(self, text): """Process ```-fenced unindented code blocks ('fenced-code-blocks' extra).""" @@ -1732,6 +1892,11 @@ class Markdown(object): self._escape_table[text] = hashed return hashed + _strike_re = re.compile(r"~~(?=\S)(.+?)(?<=\S)~~", re.S) + def _do_strike(self, text): + text = self._strike_re.sub(r"<strike>\1</strike>", text) + return text + _strong_re = re.compile(r"(\*\*|__)(?=\S)(.+?[*_]*)(?<=\S)\1", re.S) _em_re = re.compile(r"(\*|_)(?=\S)(.+?)(?<=\S)\1", re.S) _code_friendly_strong_re = re.compile(r"\*\*(?=\S)(.+?[*_]*)(?<=\S)\*\*", re.S) @@ -1776,12 +1941,12 @@ class Markdown(object): <http://code.google.com/p/python-markdown2/issues/detail?id=42> for a discussion of some diversion from the original SmartyPants. """ - if "'" in text: # guard for perf + if "'" in text: # guard for perf text = self._do_smart_contractions(text) text = self._opening_single_quote_re.sub("‘", text) text = self._closing_single_quote_re.sub("’", text) - if '"' in text: # guard for perf + if '"' in text: # guard for perf text = self._opening_double_quote_re.sub("“", text) text = self._closing_double_quote_re.sub("”", text) @@ -1804,8 +1969,8 @@ class Markdown(object): ''' _block_quote_re = re.compile(_block_quote_base % '', re.M | re.X) _block_quote_re_spoiler = re.compile(_block_quote_base % '[ \t]*?!?', re.M | re.X) - _bq_one_level_re = re.compile('^[ \t]*>[ \t]?', re.M); - _bq_one_level_re_spoiler = re.compile('^[ \t]*>[ \t]*?![ \t]?', re.M); + _bq_one_level_re = re.compile('^[ \t]*>[ \t]?', re.M) + _bq_one_level_re_spoiler = re.compile('^[ \t]*>[ \t]*?![ \t]?', re.M) _bq_all_lines_spoilers = re.compile(r'\A(?:^[ \t]*>[ \t]*?!.*[\n\r]*)+\Z', re.M) _html_pre_block_re = re.compile(r'(\s*<pre>.+?</pre>)', re.S) def _dedent_two_spaces_sub(self, match): @@ -1884,15 +2049,31 @@ class Markdown(object): '<hr' + self.empty_element_suffix, '<ol>', ] + + if not self.footnote_title: + self.footnote_title = "Jump back to footnote %d in the text." + if not self.footnote_return_symbol: + self.footnote_return_symbol = "↩" + for i, id in enumerate(self.footnote_ids): if i != 0: footer.append('') footer.append('<li id="fn-%s">' % id) footer.append(self._run_block_gamut(self.footnotes[id])) - backlink = ('<a href="#fnref-%s" ' - 'class="footnoteBackLink" ' - 'title="Jump back to footnote %d in the text.">' - '↩</a>' % (id, i+1)) + try: + backlink = ('<a href="#fnref-%s" ' + + 'class="footnoteBackLink" ' + + 'title="' + self.footnote_title + '">' + + self.footnote_return_symbol + + '</a>') % (id, i+1) + except TypeError: + log.debug("Footnote error. `footnote_title` " + "must include parameter. Using defaults.") + backlink = ('<a href="#fnref-%s" ' + 'class="footnoteBackLink" ' + 'title="Jump back to footnote %d in the text.">' + '↩</a>' % (id, i+1)) + if footer[-1].endswith("</p>"): footer[-1] = footer[-1][:-len("</p>")] \ + ' ' + backlink + "</p>" @@ -2031,7 +2212,7 @@ class MarkdownWithExtras(Markdown): extras = ["footnotes", "code-color"] -#---- internal support functions +# ---- internal support functions class UnicodeWithAttrs(unicode): """A subclass of unicode used for the return value of conversion to @@ -2100,6 +2281,7 @@ def _curry(*args, **kwargs): return function(*args + rest, **combined) return result + # Recipe: regex_from_encoded_pattern (1.0) def _regex_from_encoded_pattern(s): """'foo' -> re.compile(re.escape('foo')) @@ -2126,9 +2308,10 @@ def _regex_from_encoded_pattern(s): "(must be one of '%s')" % (char, s, ''.join(list(flag_from_char.keys())))) return re.compile(s[1:idx], flags) - else: # not an encoded regex + else: # not an encoded regex return re.compile(re.escape(s)) + # Recipe: dedent (0.1.2) def _dedentlines(lines, tabsize=8, skip_first_line=False): """_dedentlines(lines, tabsize=8, skip_first_line=False) -> dedented lines @@ -2146,7 +2329,6 @@ def _dedentlines(lines, tabsize=8, skip_first_line=False): if DEBUG: print("dedent: dedent(..., tabsize=%d, skip_first_line=%r)"\ % (tabsize, skip_first_line)) - indents = [] margin = None for i, line in enumerate(lines): if i == 0 and skip_first_line: continue @@ -2157,11 +2339,11 @@ def _dedentlines(lines, tabsize=8, skip_first_line=False): elif ch == '\t': indent += tabsize - (indent % tabsize) elif ch in '\r\n': - continue # skip all-whitespace lines + continue # skip all-whitespace lines else: break else: - continue # skip all-whitespace lines + continue # skip all-whitespace lines if DEBUG: print("dedent: indent=%d: %r" % (indent, line)) if margin is None: margin = indent @@ -2200,6 +2382,7 @@ def _dedentlines(lines, tabsize=8, skip_first_line=False): lines[i] = lines[i][removed:] return lines + def _dedent(text, tabsize=8, skip_first_line=False): """_dedent(text, tabsize=8, skip_first_line=False) -> dedented text @@ -2217,28 +2400,30 @@ def _dedent(text, tabsize=8, skip_first_line=False): class _memoized(object): - """Decorator that caches a function's return value each time it is called. - If called later with the same arguments, the cached value is returned, and - not re-evaluated. - - http://wiki.python.org/moin/PythonDecoratorLibrary - """ - def __init__(self, func): - self.func = func - self.cache = {} - def __call__(self, *args): - try: - return self.cache[args] - except KeyError: - self.cache[args] = value = self.func(*args) - return value - except TypeError: - # uncachable -- for instance, passing a list as an argument. - # Better to not cache than to blow up entirely. - return self.func(*args) - def __repr__(self): - """Return the function's docstring.""" - return self.func.__doc__ + """Decorator that caches a function's return value each time it is called. + If called later with the same arguments, the cached value is returned, and + not re-evaluated. + + http://wiki.python.org/moin/PythonDecoratorLibrary + """ + def __init__(self, func): + self.func = func + self.cache = {} + + def __call__(self, *args): + try: + return self.cache[args] + except KeyError: + self.cache[args] = value = self.func(*args) + return value + except TypeError: + # uncachable -- for instance, passing a list as an argument. + # Better to not cache than to blow up entirely. + return self.func(*args) + + def __repr__(self): + """Return the function's docstring.""" + return self.func.__doc__ def _xml_oneliner_re_from_tab_width(tab_width): @@ -2262,8 +2447,9 @@ def _xml_oneliner_re_from_tab_width(tab_width): """ % (tab_width - 1), re.X) _xml_oneliner_re_from_tab_width = _memoized(_xml_oneliner_re_from_tab_width) + def _hr_tag_re_from_tab_width(tab_width): - return re.compile(r""" + return re.compile(r""" (?: (?<=\n\n) # Starting after a blank line | # or @@ -2312,18 +2498,31 @@ def _xml_encode_email_char_at_random(ch): return '&#%s;' % ord(ch) +def _html_escape_url(attr, safe_mode=False): + """Replace special characters that are potentially malicious in url string.""" + escaped = (attr + .replace('"', '"') + .replace('<', '<') + .replace('>', '>')) + if safe_mode: + escaped = escaped.replace('+', ' ') + escaped = escaped.replace("'", "'") + return escaped + -#---- mainline +# ---- mainline class _NoReflowFormatter(optparse.IndentedHelpFormatter): """An optparse formatter that does NOT reflow the description.""" def format_description(self, description): return description or "" + def _test(): import doctest doctest.testmod() + def main(argv=None): if argv is None: argv = sys.argv @@ -2440,7 +2639,7 @@ def main(argv=None): sys.stdout.encoding or "utf-8", 'xmlcharrefreplace')) if extras and "toc" in extras: log.debug("toc_html: " + - html.toc_html.encode(sys.stdout.encoding or "utf-8", 'xmlcharrefreplace')) + str(html.toc_html.encode(sys.stdout.encoding or "utf-8", 'xmlcharrefreplace'))) if opts.compare: test_dir = join(dirname(dirname(abspath(__file__))), "test") if exists(join(test_dir, "test_markdown2.py")): @@ -2455,4 +2654,4 @@ def main(argv=None): if __name__ == "__main__": - sys.exit( main(sys.argv) ) + sys.exit(main(sys.argv)) diff --git a/lib/simplejson/__init__.py b/lib/simplejson/__init__.py deleted file mode 100644 index d5b4d3991..000000000 --- a/lib/simplejson/__init__.py +++ /dev/null @@ -1,318 +0,0 @@ -r"""JSON (JavaScript Object Notation) <http://json.org> is a subset of -JavaScript syntax (ECMA-262 3rd edition) used as a lightweight data -interchange format. - -:mod:`simplejson` exposes an API familiar to users of the standard library -:mod:`marshal` and :mod:`pickle` modules. It is the externally maintained -version of the :mod:`json` library contained in Python 2.6, but maintains -compatibility with Python 2.4 and Python 2.5 and (currently) has -significant performance advantages, even without using the optional C -extension for speedups. - -Encoding basic Python object hierarchies:: - - >>> import simplejson as json - >>> json.dumps(['foo', {'bar': ('baz', None, 1.0, 2)}]) - '["foo", {"bar": ["baz", null, 1.0, 2]}]' - >>> print json.dumps("\"foo\bar") - "\"foo\bar" - >>> print json.dumps(u'\u1234') - "\u1234" - >>> print json.dumps('\\') - "\\" - >>> print json.dumps({"c": 0, "b": 0, "a": 0}, sort_keys=True) - {"a": 0, "b": 0, "c": 0} - >>> from StringIO import StringIO - >>> io = StringIO() - >>> json.dump(['streaming API'], io) - >>> io.getvalue() - '["streaming API"]' - -Compact encoding:: - - >>> import simplejson as json - >>> json.dumps([1,2,3,{'4': 5, '6': 7}], separators=(',',':')) - '[1,2,3,{"4":5,"6":7}]' - -Pretty printing:: - - >>> import simplejson as json - >>> s = json.dumps({'4': 5, '6': 7}, sort_keys=True, indent=4) - >>> print '\n'.join([l.rstrip() for l in s.splitlines()]) - { - "4": 5, - "6": 7 - } - -Decoding JSON:: - - >>> import simplejson as json - >>> obj = [u'foo', {u'bar': [u'baz', None, 1.0, 2]}] - >>> json.loads('["foo", {"bar":["baz", null, 1.0, 2]}]') == obj - True - >>> json.loads('"\\"foo\\bar"') == u'"foo\x08ar' - True - >>> from StringIO import StringIO - >>> io = StringIO('["streaming API"]') - >>> json.load(io)[0] == 'streaming API' - True - -Specializing JSON object decoding:: - - >>> import simplejson as json - >>> def as_complex(dct): - ... if '__complex__' in dct: - ... return complex(dct['real'], dct['imag']) - ... return dct - ... - >>> json.loads('{"__complex__": true, "real": 1, "imag": 2}', - ... object_hook=as_complex) - (1+2j) - >>> import decimal - >>> json.loads('1.1', parse_float=decimal.Decimal) == decimal.Decimal('1.1') - True - -Specializing JSON object encoding:: - - >>> import simplejson as json - >>> def encode_complex(obj): - ... if isinstance(obj, complex): - ... return [obj.real, obj.imag] - ... raise TypeError(repr(o) + " is not JSON serializable") - ... - >>> json.dumps(2 + 1j, default=encode_complex) - '[2.0, 1.0]' - >>> json.JSONEncoder(default=encode_complex).encode(2 + 1j) - '[2.0, 1.0]' - >>> ''.join(json.JSONEncoder(default=encode_complex).iterencode(2 + 1j)) - '[2.0, 1.0]' - - -Using simplejson.tool from the shell to validate and pretty-print:: - - $ echo '{"json":"obj"}' | python -m simplejson.tool - { - "json": "obj" - } - $ echo '{ 1.2:3.4}' | python -m simplejson.tool - Expecting property name: line 1 column 2 (char 2) -""" -__version__ = '2.0.9' -__all__ = [ - 'dump', 'dumps', 'load', 'loads', - 'JSONDecoder', 'JSONEncoder', -] - -__author__ = 'Bob Ippolito <bob@redivi.com>' - -from decoder import JSONDecoder -from encoder import JSONEncoder - -_default_encoder = JSONEncoder( - skipkeys=False, - ensure_ascii=True, - check_circular=True, - allow_nan=True, - indent=None, - separators=None, - encoding='utf-8', - default=None, -) - -def dump(obj, fp, skipkeys=False, ensure_ascii=True, check_circular=True, - allow_nan=True, cls=None, indent=None, separators=None, - encoding='utf-8', default=None, **kw): - """Serialize ``obj`` as a JSON formatted stream to ``fp`` (a - ``.write()``-supporting file-like object). - - If ``skipkeys`` is true then ``dict`` keys that are not basic types - (``str``, ``unicode``, ``int``, ``long``, ``float``, ``bool``, ``None``) - will be skipped instead of raising a ``TypeError``. - - If ``ensure_ascii`` is false, then the some chunks written to ``fp`` - may be ``unicode`` instances, subject to normal Python ``str`` to - ``unicode`` coercion rules. Unless ``fp.write()`` explicitly - understands ``unicode`` (as in ``codecs.getwriter()``) this is likely - to cause an error. - - If ``check_circular`` is false, then the circular reference check - for container types will be skipped and a circular reference will - result in an ``OverflowError`` (or worse). - - If ``allow_nan`` is false, then it will be a ``ValueError`` to - serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``) - in strict compliance of the JSON specification, instead of using the - JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``). - - If ``indent`` is a non-negative integer, then JSON array elements and object - members will be pretty-printed with that indent level. An indent level - of 0 will only insert newlines. ``None`` is the most compact representation. - - If ``separators`` is an ``(item_separator, dict_separator)`` tuple - then it will be used instead of the default ``(', ', ': ')`` separators. - ``(',', ':')`` is the most compact JSON representation. - - ``encoding`` is the character encoding for str instances, default is UTF-8. - - ``default(obj)`` is a function that should return a serializable version - of obj or raise TypeError. The default simply raises TypeError. - - To use a custom ``JSONEncoder`` subclass (e.g. one that overrides the - ``.default()`` method to serialize additional types), specify it with - the ``cls`` kwarg. - - """ - # cached encoder - if (not skipkeys and ensure_ascii and - check_circular and allow_nan and - cls is None and indent is None and separators is None and - encoding == 'utf-8' and default is None and not kw): - iterable = _default_encoder.iterencode(obj) - else: - if cls is None: - cls = JSONEncoder - iterable = cls(skipkeys=skipkeys, ensure_ascii=ensure_ascii, - check_circular=check_circular, allow_nan=allow_nan, indent=indent, - separators=separators, encoding=encoding, - default=default, **kw).iterencode(obj) - # could accelerate with writelines in some versions of Python, at - # a debuggability cost - for chunk in iterable: - fp.write(chunk) - - -def dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True, - allow_nan=True, cls=None, indent=None, separators=None, - encoding='utf-8', default=None, **kw): - """Serialize ``obj`` to a JSON formatted ``str``. - - If ``skipkeys`` is false then ``dict`` keys that are not basic types - (``str``, ``unicode``, ``int``, ``long``, ``float``, ``bool``, ``None``) - will be skipped instead of raising a ``TypeError``. - - If ``ensure_ascii`` is false, then the return value will be a - ``unicode`` instance subject to normal Python ``str`` to ``unicode`` - coercion rules instead of being escaped to an ASCII ``str``. - - If ``check_circular`` is false, then the circular reference check - for container types will be skipped and a circular reference will - result in an ``OverflowError`` (or worse). - - If ``allow_nan`` is false, then it will be a ``ValueError`` to - serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``) in - strict compliance of the JSON specification, instead of using the - JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``). - - If ``indent`` is a non-negative integer, then JSON array elements and - object members will be pretty-printed with that indent level. An indent - level of 0 will only insert newlines. ``None`` is the most compact - representation. - - If ``separators`` is an ``(item_separator, dict_separator)`` tuple - then it will be used instead of the default ``(', ', ': ')`` separators. - ``(',', ':')`` is the most compact JSON representation. - - ``encoding`` is the character encoding for str instances, default is UTF-8. - - ``default(obj)`` is a function that should return a serializable version - of obj or raise TypeError. The default simply raises TypeError. - - To use a custom ``JSONEncoder`` subclass (e.g. one that overrides the - ``.default()`` method to serialize additional types), specify it with - the ``cls`` kwarg. - - """ - # cached encoder - if (not skipkeys and ensure_ascii and - check_circular and allow_nan and - cls is None and indent is None and separators is None and - encoding == 'utf-8' and default is None and not kw): - return _default_encoder.encode(obj) - if cls is None: - cls = JSONEncoder - return cls( - skipkeys=skipkeys, ensure_ascii=ensure_ascii, - check_circular=check_circular, allow_nan=allow_nan, indent=indent, - separators=separators, encoding=encoding, default=default, - **kw).encode(obj) - - -_default_decoder = JSONDecoder(encoding=None, object_hook=None) - - -def load(fp, encoding=None, cls=None, object_hook=None, parse_float=None, - parse_int=None, parse_constant=None, **kw): - """Deserialize ``fp`` (a ``.read()``-supporting file-like object containing - a JSON document) to a Python object. - - If the contents of ``fp`` is encoded with an ASCII based encoding other - than utf-8 (e.g. latin-1), then an appropriate ``encoding`` name must - be specified. Encodings that are not ASCII based (such as UCS-2) are - not allowed, and should be wrapped with - ``codecs.getreader(fp)(encoding)``, or simply decoded to a ``unicode`` - object and passed to ``loads()`` - - ``object_hook`` is an optional function that will be called with the - result of any object literal decode (a ``dict``). The return value of - ``object_hook`` will be used instead of the ``dict``. This feature - can be used to implement custom decoders (e.g. JSON-RPC class hinting). - - To use a custom ``JSONDecoder`` subclass, specify it with the ``cls`` - kwarg. - - """ - return loads(fp.read(), - encoding=encoding, cls=cls, object_hook=object_hook, - parse_float=parse_float, parse_int=parse_int, - parse_constant=parse_constant, **kw) - - -def loads(s, encoding=None, cls=None, object_hook=None, parse_float=None, - parse_int=None, parse_constant=None, **kw): - """Deserialize ``s`` (a ``str`` or ``unicode`` instance containing a JSON - document) to a Python object. - - If ``s`` is a ``str`` instance and is encoded with an ASCII based encoding - other than utf-8 (e.g. latin-1) then an appropriate ``encoding`` name - must be specified. Encodings that are not ASCII based (such as UCS-2) - are not allowed and should be decoded to ``unicode`` first. - - ``object_hook`` is an optional function that will be called with the - result of any object literal decode (a ``dict``). The return value of - ``object_hook`` will be used instead of the ``dict``. This feature - can be used to implement custom decoders (e.g. JSON-RPC class hinting). - - ``parse_float``, if specified, will be called with the string - of every JSON float to be decoded. By default this is equivalent to - float(num_str). This can be used to use another datatype or parser - for JSON floats (e.g. decimal.Decimal). - - ``parse_int``, if specified, will be called with the string - of every JSON int to be decoded. By default this is equivalent to - int(num_str). This can be used to use another datatype or parser - for JSON integers (e.g. float). - - ``parse_constant``, if specified, will be called with one of the - following strings: -Infinity, Infinity, NaN, null, true, false. - This can be used to raise an exception if invalid JSON numbers - are encountered. - - To use a custom ``JSONDecoder`` subclass, specify it with the ``cls`` - kwarg. - - """ - if (cls is None and encoding is None and object_hook is None and - parse_int is None and parse_float is None and - parse_constant is None and not kw): - return _default_decoder.decode(s) - if cls is None: - cls = JSONDecoder - if object_hook is not None: - kw['object_hook'] = object_hook - if parse_float is not None: - kw['parse_float'] = parse_float - if parse_int is not None: - kw['parse_int'] = parse_int - if parse_constant is not None: - kw['parse_constant'] = parse_constant - return cls(encoding=encoding, **kw).decode(s) diff --git a/lib/simplejson/_speedups.c b/lib/simplejson/_speedups.c deleted file mode 100644 index 23b5f4a6e..000000000 --- a/lib/simplejson/_speedups.c +++ /dev/null @@ -1,2329 +0,0 @@ -#include "Python.h" -#include "structmember.h" -#if PY_VERSION_HEX < 0x02060000 && !defined(Py_TYPE) -#define Py_TYPE(ob) (((PyObject*)(ob))->ob_type) -#endif -#if PY_VERSION_HEX < 0x02050000 && !defined(PY_SSIZE_T_MIN) -typedef int Py_ssize_t; -#define PY_SSIZE_T_MAX INT_MAX -#define PY_SSIZE_T_MIN INT_MIN -#define PyInt_FromSsize_t PyInt_FromLong -#define PyInt_AsSsize_t PyInt_AsLong -#endif -#ifndef Py_IS_FINITE -#define Py_IS_FINITE(X) (!Py_IS_INFINITY(X) && !Py_IS_NAN(X)) -#endif - -#ifdef __GNUC__ -#define UNUSED __attribute__((__unused__)) -#else -#define UNUSED -#endif - -#define DEFAULT_ENCODING "utf-8" - -#define PyScanner_Check(op) PyObject_TypeCheck(op, &PyScannerType) -#define PyScanner_CheckExact(op) (Py_TYPE(op) == &PyScannerType) -#define PyEncoder_Check(op) PyObject_TypeCheck(op, &PyEncoderType) -#define PyEncoder_CheckExact(op) (Py_TYPE(op) == &PyEncoderType) - -static PyTypeObject PyScannerType; -static PyTypeObject PyEncoderType; - -typedef struct _PyScannerObject { - PyObject_HEAD - PyObject *encoding; - PyObject *strict; - PyObject *object_hook; - PyObject *parse_float; - PyObject *parse_int; - PyObject *parse_constant; -} PyScannerObject; - -static PyMemberDef scanner_members[] = { - {"encoding", T_OBJECT, offsetof(PyScannerObject, encoding), READONLY, "encoding"}, - {"strict", T_OBJECT, offsetof(PyScannerObject, strict), READONLY, "strict"}, - {"object_hook", T_OBJECT, offsetof(PyScannerObject, object_hook), READONLY, "object_hook"}, - {"parse_float", T_OBJECT, offsetof(PyScannerObject, parse_float), READONLY, "parse_float"}, - {"parse_int", T_OBJECT, offsetof(PyScannerObject, parse_int), READONLY, "parse_int"}, - {"parse_constant", T_OBJECT, offsetof(PyScannerObject, parse_constant), READONLY, "parse_constant"}, - {NULL} -}; - -typedef struct _PyEncoderObject { - PyObject_HEAD - PyObject *markers; - PyObject *defaultfn; - PyObject *encoder; - PyObject *indent; - PyObject *key_separator; - PyObject *item_separator; - PyObject *sort_keys; - PyObject *skipkeys; - int fast_encode; - int allow_nan; -} PyEncoderObject; - -static PyMemberDef encoder_members[] = { - {"markers", T_OBJECT, offsetof(PyEncoderObject, markers), READONLY, "markers"}, - {"default", T_OBJECT, offsetof(PyEncoderObject, defaultfn), READONLY, "default"}, - {"encoder", T_OBJECT, offsetof(PyEncoderObject, encoder), READONLY, "encoder"}, - {"indent", T_OBJECT, offsetof(PyEncoderObject, indent), READONLY, "indent"}, - {"key_separator", T_OBJECT, offsetof(PyEncoderObject, key_separator), READONLY, "key_separator"}, - {"item_separator", T_OBJECT, offsetof(PyEncoderObject, item_separator), READONLY, "item_separator"}, - {"sort_keys", T_OBJECT, offsetof(PyEncoderObject, sort_keys), READONLY, "sort_keys"}, - {"skipkeys", T_OBJECT, offsetof(PyEncoderObject, skipkeys), READONLY, "skipkeys"}, - {NULL} -}; - -static Py_ssize_t -ascii_escape_char(Py_UNICODE c, char *output, Py_ssize_t chars); -static PyObject * -ascii_escape_unicode(PyObject *pystr); -static PyObject * -ascii_escape_str(PyObject *pystr); -static PyObject * -py_encode_basestring_ascii(PyObject* self UNUSED, PyObject *pystr); -void init_speedups(void); -static PyObject * -scan_once_str(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *next_idx_ptr); -static PyObject * -scan_once_unicode(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *next_idx_ptr); -static PyObject * -_build_rval_index_tuple(PyObject *rval, Py_ssize_t idx); -static PyObject * -scanner_new(PyTypeObject *type, PyObject *args, PyObject *kwds); -static int -scanner_init(PyObject *self, PyObject *args, PyObject *kwds); -static void -scanner_dealloc(PyObject *self); -static int -scanner_clear(PyObject *self); -static PyObject * -encoder_new(PyTypeObject *type, PyObject *args, PyObject *kwds); -static int -encoder_init(PyObject *self, PyObject *args, PyObject *kwds); -static void -encoder_dealloc(PyObject *self); -static int -encoder_clear(PyObject *self); -static int -encoder_listencode_list(PyEncoderObject *s, PyObject *rval, PyObject *seq, Py_ssize_t indent_level); -static int -encoder_listencode_obj(PyEncoderObject *s, PyObject *rval, PyObject *obj, Py_ssize_t indent_level); -static int -encoder_listencode_dict(PyEncoderObject *s, PyObject *rval, PyObject *dct, Py_ssize_t indent_level); -static PyObject * -_encoded_const(PyObject *const); -static void -raise_errmsg(char *msg, PyObject *s, Py_ssize_t end); -static PyObject * -encoder_encode_string(PyEncoderObject *s, PyObject *obj); -static int -_convertPyInt_AsSsize_t(PyObject *o, Py_ssize_t *size_ptr); -static PyObject * -_convertPyInt_FromSsize_t(Py_ssize_t *size_ptr); -static PyObject * -encoder_encode_float(PyEncoderObject *s, PyObject *obj); - -#define S_CHAR(c) (c >= ' ' && c <= '~' && c != '\\' && c != '"') -#define IS_WHITESPACE(c) (((c) == ' ') || ((c) == '\t') || ((c) == '\n') || ((c) == '\r')) - -#define MIN_EXPANSION 6 -#ifdef Py_UNICODE_WIDE -#define MAX_EXPANSION (2 * MIN_EXPANSION) -#else -#define MAX_EXPANSION MIN_EXPANSION -#endif - -static int -_convertPyInt_AsSsize_t(PyObject *o, Py_ssize_t *size_ptr) -{ - /* PyObject to Py_ssize_t converter */ - *size_ptr = PyInt_AsSsize_t(o); - if (*size_ptr == -1 && PyErr_Occurred()); - return 1; - return 0; -} - -static PyObject * -_convertPyInt_FromSsize_t(Py_ssize_t *size_ptr) -{ - /* Py_ssize_t to PyObject converter */ - return PyInt_FromSsize_t(*size_ptr); -} - -static Py_ssize_t -ascii_escape_char(Py_UNICODE c, char *output, Py_ssize_t chars) -{ - /* Escape unicode code point c to ASCII escape sequences - in char *output. output must have at least 12 bytes unused to - accommodate an escaped surrogate pair "\uXXXX\uXXXX" */ - output[chars++] = '\\'; - switch (c) { - case '\\': output[chars++] = (char)c; break; - case '"': output[chars++] = (char)c; break; - case '\b': output[chars++] = 'b'; break; - case '\f': output[chars++] = 'f'; break; - case '\n': output[chars++] = 'n'; break; - case '\r': output[chars++] = 'r'; break; - case '\t': output[chars++] = 't'; break; - default: -#ifdef Py_UNICODE_WIDE - if (c >= 0x10000) { - /* UTF-16 surrogate pair */ - Py_UNICODE v = c - 0x10000; - c = 0xd800 | ((v >> 10) & 0x3ff); - output[chars++] = 'u'; - output[chars++] = "0123456789abcdef"[(c >> 12) & 0xf]; - output[chars++] = "0123456789abcdef"[(c >> 8) & 0xf]; - output[chars++] = "0123456789abcdef"[(c >> 4) & 0xf]; - output[chars++] = "0123456789abcdef"[(c ) & 0xf]; - c = 0xdc00 | (v & 0x3ff); - output[chars++] = '\\'; - } -#endif - output[chars++] = 'u'; - output[chars++] = "0123456789abcdef"[(c >> 12) & 0xf]; - output[chars++] = "0123456789abcdef"[(c >> 8) & 0xf]; - output[chars++] = "0123456789abcdef"[(c >> 4) & 0xf]; - output[chars++] = "0123456789abcdef"[(c ) & 0xf]; - } - return chars; -} - -static PyObject * -ascii_escape_unicode(PyObject *pystr) -{ - /* Take a PyUnicode pystr and return a new ASCII-only escaped PyString */ - Py_ssize_t i; - Py_ssize_t input_chars; - Py_ssize_t output_size; - Py_ssize_t max_output_size; - Py_ssize_t chars; - PyObject *rval; - char *output; - Py_UNICODE *input_unicode; - - input_chars = PyUnicode_GET_SIZE(pystr); - input_unicode = PyUnicode_AS_UNICODE(pystr); - - /* One char input can be up to 6 chars output, estimate 4 of these */ - output_size = 2 + (MIN_EXPANSION * 4) + input_chars; - max_output_size = 2 + (input_chars * MAX_EXPANSION); - rval = PyString_FromStringAndSize(NULL, output_size); - if (rval == NULL) { - return NULL; - } - output = PyString_AS_STRING(rval); - chars = 0; - output[chars++] = '"'; - for (i = 0; i < input_chars; i++) { - Py_UNICODE c = input_unicode[i]; - if (S_CHAR(c)) { - output[chars++] = (char)c; - } - else { - chars = ascii_escape_char(c, output, chars); - } - if (output_size - chars < (1 + MAX_EXPANSION)) { - /* There's more than four, so let's resize by a lot */ - Py_ssize_t new_output_size = output_size * 2; - /* This is an upper bound */ - if (new_output_size > max_output_size) { - new_output_size = max_output_size; - } - /* Make sure that the output size changed before resizing */ - if (new_output_size != output_size) { - output_size = new_output_size; - if (_PyString_Resize(&rval, output_size) == -1) { - return NULL; - } - output = PyString_AS_STRING(rval); - } - } - } - output[chars++] = '"'; - if (_PyString_Resize(&rval, chars) == -1) { - return NULL; - } - return rval; -} - -static PyObject * -ascii_escape_str(PyObject *pystr) -{ - /* Take a PyString pystr and return a new ASCII-only escaped PyString */ - Py_ssize_t i; - Py_ssize_t input_chars; - Py_ssize_t output_size; - Py_ssize_t chars; - PyObject *rval; - char *output; - char *input_str; - - input_chars = PyString_GET_SIZE(pystr); - input_str = PyString_AS_STRING(pystr); - - /* Fast path for a string that's already ASCII */ - for (i = 0; i < input_chars; i++) { - Py_UNICODE c = (Py_UNICODE)(unsigned char)input_str[i]; - if (!S_CHAR(c)) { - /* If we have to escape something, scan the string for unicode */ - Py_ssize_t j; - for (j = i; j < input_chars; j++) { - c = (Py_UNICODE)(unsigned char)input_str[j]; - if (c > 0x7f) { - /* We hit a non-ASCII character, bail to unicode mode */ - PyObject *uni; - uni = PyUnicode_DecodeUTF8(input_str, input_chars, "strict"); - if (uni == NULL) { - return NULL; - } - rval = ascii_escape_unicode(uni); - Py_DECREF(uni); - return rval; - } - } - break; - } - } - - if (i == input_chars) { - /* Input is already ASCII */ - output_size = 2 + input_chars; - } - else { - /* One char input can be up to 6 chars output, estimate 4 of these */ - output_size = 2 + (MIN_EXPANSION * 4) + input_chars; - } - rval = PyString_FromStringAndSize(NULL, output_size); - if (rval == NULL) { - return NULL; - } - output = PyString_AS_STRING(rval); - output[0] = '"'; - - /* We know that everything up to i is ASCII already */ - chars = i + 1; - memcpy(&output[1], input_str, i); - - for (; i < input_chars; i++) { - Py_UNICODE c = (Py_UNICODE)(unsigned char)input_str[i]; - if (S_CHAR(c)) { - output[chars++] = (char)c; - } - else { - chars = ascii_escape_char(c, output, chars); - } - /* An ASCII char can't possibly expand to a surrogate! */ - if (output_size - chars < (1 + MIN_EXPANSION)) { - /* There's more than four, so let's resize by a lot */ - output_size *= 2; - if (output_size > 2 + (input_chars * MIN_EXPANSION)) { - output_size = 2 + (input_chars * MIN_EXPANSION); - } - if (_PyString_Resize(&rval, output_size) == -1) { - return NULL; - } - output = PyString_AS_STRING(rval); - } - } - output[chars++] = '"'; - if (_PyString_Resize(&rval, chars) == -1) { - return NULL; - } - return rval; -} - -static void -raise_errmsg(char *msg, PyObject *s, Py_ssize_t end) -{ - /* Use the Python function simplejson.decoder.errmsg to raise a nice - looking ValueError exception */ - static PyObject *errmsg_fn = NULL; - PyObject *pymsg; - if (errmsg_fn == NULL) { - PyObject *decoder = PyImport_ImportModule("simplejson.decoder"); - if (decoder == NULL) - return; - errmsg_fn = PyObject_GetAttrString(decoder, "errmsg"); - Py_DECREF(decoder); - if (errmsg_fn == NULL) - return; - } - pymsg = PyObject_CallFunction(errmsg_fn, "(zOO&)", msg, s, _convertPyInt_FromSsize_t, &end); - if (pymsg) { - PyErr_SetObject(PyExc_ValueError, pymsg); - Py_DECREF(pymsg); - } -} - -static PyObject * -join_list_unicode(PyObject *lst) -{ - /* return u''.join(lst) */ - static PyObject *joinfn = NULL; - if (joinfn == NULL) { - PyObject *ustr = PyUnicode_FromUnicode(NULL, 0); - if (ustr == NULL) - return NULL; - - joinfn = PyObject_GetAttrString(ustr, "join"); - Py_DECREF(ustr); - if (joinfn == NULL) - return NULL; - } - return PyObject_CallFunctionObjArgs(joinfn, lst, NULL); -} - -static PyObject * -join_list_string(PyObject *lst) -{ - /* return ''.join(lst) */ - static PyObject *joinfn = NULL; - if (joinfn == NULL) { - PyObject *ustr = PyString_FromStringAndSize(NULL, 0); - if (ustr == NULL) - return NULL; - - joinfn = PyObject_GetAttrString(ustr, "join"); - Py_DECREF(ustr); - if (joinfn == NULL) - return NULL; - } - return PyObject_CallFunctionObjArgs(joinfn, lst, NULL); -} - -static PyObject * -_build_rval_index_tuple(PyObject *rval, Py_ssize_t idx) { - /* return (rval, idx) tuple, stealing reference to rval */ - PyObject *tpl; - PyObject *pyidx; - /* - steal a reference to rval, returns (rval, idx) - */ - if (rval == NULL) { - return NULL; - } - pyidx = PyInt_FromSsize_t(idx); - if (pyidx == NULL) { - Py_DECREF(rval); - return NULL; - } - tpl = PyTuple_New(2); - if (tpl == NULL) { - Py_DECREF(pyidx); - Py_DECREF(rval); - return NULL; - } - PyTuple_SET_ITEM(tpl, 0, rval); - PyTuple_SET_ITEM(tpl, 1, pyidx); - return tpl; -} - -static PyObject * -scanstring_str(PyObject *pystr, Py_ssize_t end, char *encoding, int strict, Py_ssize_t *next_end_ptr) -{ - /* Read the JSON string from PyString pystr. - end is the index of the first character after the quote. - encoding is the encoding of pystr (must be an ASCII superset) - if strict is zero then literal control characters are allowed - *next_end_ptr is a return-by-reference index of the character - after the end quote - - Return value is a new PyString (if ASCII-only) or PyUnicode - */ - PyObject *rval; - Py_ssize_t len = PyString_GET_SIZE(pystr); - Py_ssize_t begin = end - 1; - Py_ssize_t next = begin; - int has_unicode = 0; - char *buf = PyString_AS_STRING(pystr); - PyObject *chunks = PyList_New(0); - if (chunks == NULL) { - goto bail; - } - if (end < 0 || len <= end) { - PyErr_SetString(PyExc_ValueError, "end is out of bounds"); - goto bail; - } - while (1) { - /* Find the end of the string or the next escape */ - Py_UNICODE c = 0; - PyObject *chunk = NULL; - for (next = end; next < len; next++) { - c = (unsigned char)buf[next]; - if (c == '"' || c == '\\') { - break; - } - else if (strict && c <= 0x1f) { - raise_errmsg("Invalid control character at", pystr, next); - goto bail; - } - else if (c > 0x7f) { - has_unicode = 1; - } - } - if (!(c == '"' || c == '\\')) { - raise_errmsg("Unterminated string starting at", pystr, begin); - goto bail; - } - /* Pick up this chunk if it's not zero length */ - if (next != end) { - PyObject *strchunk = PyString_FromStringAndSize(&buf[end], next - end); - if (strchunk == NULL) { - goto bail; - } - if (has_unicode) { - chunk = PyUnicode_FromEncodedObject(strchunk, encoding, NULL); - Py_DECREF(strchunk); - if (chunk == NULL) { - goto bail; - } - } - else { - chunk = strchunk; - } - if (PyList_Append(chunks, chunk)) { - Py_DECREF(chunk); - goto bail; - } - Py_DECREF(chunk); - } - next++; - if (c == '"') { - end = next; - break; - } - if (next == len) { - raise_errmsg("Unterminated string starting at", pystr, begin); - goto bail; - } - c = buf[next]; - if (c != 'u') { - /* Non-unicode backslash escapes */ - end = next + 1; - switch (c) { - case '"': break; - case '\\': break; - case '/': break; - case 'b': c = '\b'; break; - case 'f': c = '\f'; break; - case 'n': c = '\n'; break; - case 'r': c = '\r'; break; - case 't': c = '\t'; break; - default: c = 0; - } - if (c == 0) { - raise_errmsg("Invalid \\escape", pystr, end - 2); - goto bail; - } - } - else { - c = 0; - next++; - end = next + 4; - if (end >= len) { - raise_errmsg("Invalid \\uXXXX escape", pystr, next - 1); - goto bail; - } - /* Decode 4 hex digits */ - for (; next < end; next++) { - Py_UNICODE digit = buf[next]; - c <<= 4; - switch (digit) { - case '0': case '1': case '2': case '3': case '4': - case '5': case '6': case '7': case '8': case '9': - c |= (digit - '0'); break; - case 'a': case 'b': case 'c': case 'd': case 'e': - case 'f': - c |= (digit - 'a' + 10); break; - case 'A': case 'B': case 'C': case 'D': case 'E': - case 'F': - c |= (digit - 'A' + 10); break; - default: - raise_errmsg("Invalid \\uXXXX escape", pystr, end - 5); - goto bail; - } - } -#ifdef Py_UNICODE_WIDE - /* Surrogate pair */ - if ((c & 0xfc00) == 0xd800) { - Py_UNICODE c2 = 0; - if (end + 6 >= len) { - raise_errmsg("Unpaired high surrogate", pystr, end - 5); - goto bail; - } - if (buf[next++] != '\\' || buf[next++] != 'u') { - raise_errmsg("Unpaired high surrogate", pystr, end - 5); - goto bail; - } - end += 6; - /* Decode 4 hex digits */ - for (; next < end; next++) { - c2 <<= 4; - Py_UNICODE digit = buf[next]; - switch (digit) { - case '0': case '1': case '2': case '3': case '4': - case '5': case '6': case '7': case '8': case '9': - c2 |= (digit - '0'); break; - case 'a': case 'b': case 'c': case 'd': case 'e': - case 'f': - c2 |= (digit - 'a' + 10); break; - case 'A': case 'B': case 'C': case 'D': case 'E': - case 'F': - c2 |= (digit - 'A' + 10); break; - default: - raise_errmsg("Invalid \\uXXXX escape", pystr, end - 5); - goto bail; - } - } - if ((c2 & 0xfc00) != 0xdc00) { - raise_errmsg("Unpaired high surrogate", pystr, end - 5); - goto bail; - } - c = 0x10000 + (((c - 0xd800) << 10) | (c2 - 0xdc00)); - } - else if ((c & 0xfc00) == 0xdc00) { - raise_errmsg("Unpaired low surrogate", pystr, end - 5); - goto bail; - } -#endif - } - if (c > 0x7f) { - has_unicode = 1; - } - if (has_unicode) { - chunk = PyUnicode_FromUnicode(&c, 1); - if (chunk == NULL) { - goto bail; - } - } - else { - char c_char = Py_CHARMASK(c); - chunk = PyString_FromStringAndSize(&c_char, 1); - if (chunk == NULL) { - goto bail; - } - } - if (PyList_Append(chunks, chunk)) { - Py_DECREF(chunk); - goto bail; - } - Py_DECREF(chunk); - } - - rval = join_list_string(chunks); - if (rval == NULL) { - goto bail; - } - Py_CLEAR(chunks); - *next_end_ptr = end; - return rval; -bail: - *next_end_ptr = -1; - Py_XDECREF(chunks); - return NULL; -} - - -static PyObject * -scanstring_unicode(PyObject *pystr, Py_ssize_t end, int strict, Py_ssize_t *next_end_ptr) -{ - /* Read the JSON string from PyUnicode pystr. - end is the index of the first character after the quote. - if strict is zero then literal control characters are allowed - *next_end_ptr is a return-by-reference index of the character - after the end quote - - Return value is a new PyUnicode - */ - PyObject *rval; - Py_ssize_t len = PyUnicode_GET_SIZE(pystr); - Py_ssize_t begin = end - 1; - Py_ssize_t next = begin; - const Py_UNICODE *buf = PyUnicode_AS_UNICODE(pystr); - PyObject *chunks = PyList_New(0); - if (chunks == NULL) { - goto bail; - } - if (end < 0 || len <= end) { - PyErr_SetString(PyExc_ValueError, "end is out of bounds"); - goto bail; - } - while (1) { - /* Find the end of the string or the next escape */ - Py_UNICODE c = 0; - PyObject *chunk = NULL; - for (next = end; next < len; next++) { - c = buf[next]; - if (c == '"' || c == '\\') { - break; - } - else if (strict && c <= 0x1f) { - raise_errmsg("Invalid control character at", pystr, next); - goto bail; - } - } - if (!(c == '"' || c == '\\')) { - raise_errmsg("Unterminated string starting at", pystr, begin); - goto bail; - } - /* Pick up this chunk if it's not zero length */ - if (next != end) { - chunk = PyUnicode_FromUnicode(&buf[end], next - end); - if (chunk == NULL) { - goto bail; - } - if (PyList_Append(chunks, chunk)) { - Py_DECREF(chunk); - goto bail; - } - Py_DECREF(chunk); - } - next++; - if (c == '"') { - end = next; - break; - } - if (next == len) { - raise_errmsg("Unterminated string starting at", pystr, begin); - goto bail; - } - c = buf[next]; - if (c != 'u') { - /* Non-unicode backslash escapes */ - end = next + 1; - switch (c) { - case '"': break; - case '\\': break; - case '/': break; - case 'b': c = '\b'; break; - case 'f': c = '\f'; break; - case 'n': c = '\n'; break; - case 'r': c = '\r'; break; - case 't': c = '\t'; break; - default: c = 0; - } - if (c == 0) { - raise_errmsg("Invalid \\escape", pystr, end - 2); - goto bail; - } - } - else { - c = 0; - next++; - end = next + 4; - if (end >= len) { - raise_errmsg("Invalid \\uXXXX escape", pystr, next - 1); - goto bail; - } - /* Decode 4 hex digits */ - for (; next < end; next++) { - Py_UNICODE digit = buf[next]; - c <<= 4; - switch (digit) { - case '0': case '1': case '2': case '3': case '4': - case '5': case '6': case '7': case '8': case '9': - c |= (digit - '0'); break; - case 'a': case 'b': case 'c': case 'd': case 'e': - case 'f': - c |= (digit - 'a' + 10); break; - case 'A': case 'B': case 'C': case 'D': case 'E': - case 'F': - c |= (digit - 'A' + 10); break; - default: - raise_errmsg("Invalid \\uXXXX escape", pystr, end - 5); - goto bail; - } - } -#ifdef Py_UNICODE_WIDE - /* Surrogate pair */ - if ((c & 0xfc00) == 0xd800) { - Py_UNICODE c2 = 0; - if (end + 6 >= len) { - raise_errmsg("Unpaired high surrogate", pystr, end - 5); - goto bail; - } - if (buf[next++] != '\\' || buf[next++] != 'u') { - raise_errmsg("Unpaired high surrogate", pystr, end - 5); - goto bail; - } - end += 6; - /* Decode 4 hex digits */ - for (; next < end; next++) { - c2 <<= 4; - Py_UNICODE digit = buf[next]; - switch (digit) { - case '0': case '1': case '2': case '3': case '4': - case '5': case '6': case '7': case '8': case '9': - c2 |= (digit - '0'); break; - case 'a': case 'b': case 'c': case 'd': case 'e': - case 'f': - c2 |= (digit - 'a' + 10); break; - case 'A': case 'B': case 'C': case 'D': case 'E': - case 'F': - c2 |= (digit - 'A' + 10); break; - default: - raise_errmsg("Invalid \\uXXXX escape", pystr, end - 5); - goto bail; - } - } - if ((c2 & 0xfc00) != 0xdc00) { - raise_errmsg("Unpaired high surrogate", pystr, end - 5); - goto bail; - } - c = 0x10000 + (((c - 0xd800) << 10) | (c2 - 0xdc00)); - } - else if ((c & 0xfc00) == 0xdc00) { - raise_errmsg("Unpaired low surrogate", pystr, end - 5); - goto bail; - } -#endif - } - chunk = PyUnicode_FromUnicode(&c, 1); - if (chunk == NULL) { - goto bail; - } - if (PyList_Append(chunks, chunk)) { - Py_DECREF(chunk); - goto bail; - } - Py_DECREF(chunk); - } - - rval = join_list_unicode(chunks); - if (rval == NULL) { - goto bail; - } - Py_DECREF(chunks); - *next_end_ptr = end; - return rval; -bail: - *next_end_ptr = -1; - Py_XDECREF(chunks); - return NULL; -} - -PyDoc_STRVAR(pydoc_scanstring, - "scanstring(basestring, end, encoding, strict=True) -> (str, end)\n" - "\n" - "Scan the string s for a JSON string. End is the index of the\n" - "character in s after the quote that started the JSON string.\n" - "Unescapes all valid JSON string escape sequences and raises ValueError\n" - "on attempt to decode an invalid string. If strict is False then literal\n" - "control characters are allowed in the string.\n" - "\n" - "Returns a tuple of the decoded string and the index of the character in s\n" - "after the end quote." -); - -static PyObject * -py_scanstring(PyObject* self UNUSED, PyObject *args) -{ - PyObject *pystr; - PyObject *rval; - Py_ssize_t end; - Py_ssize_t next_end = -1; - char *encoding = NULL; - int strict = 1; - if (!PyArg_ParseTuple(args, "OO&|zi:scanstring", &pystr, _convertPyInt_AsSsize_t, &end, &encoding, &strict)) { - return NULL; - } - if (encoding == NULL) { - encoding = DEFAULT_ENCODING; - } - if (PyString_Check(pystr)) { - rval = scanstring_str(pystr, end, encoding, strict, &next_end); - } - else if (PyUnicode_Check(pystr)) { - rval = scanstring_unicode(pystr, end, strict, &next_end); - } - else { - PyErr_Format(PyExc_TypeError, - "first argument must be a string, not %.80s", - Py_TYPE(pystr)->tp_name); - return NULL; - } - return _build_rval_index_tuple(rval, next_end); -} - -PyDoc_STRVAR(pydoc_encode_basestring_ascii, - "encode_basestring_ascii(basestring) -> str\n" - "\n" - "Return an ASCII-only JSON representation of a Python string" -); - -static PyObject * -py_encode_basestring_ascii(PyObject* self UNUSED, PyObject *pystr) -{ - /* Return an ASCII-only JSON representation of a Python string */ - /* METH_O */ - if (PyString_Check(pystr)) { - return ascii_escape_str(pystr); - } - else if (PyUnicode_Check(pystr)) { - return ascii_escape_unicode(pystr); - } - else { - PyErr_Format(PyExc_TypeError, - "first argument must be a string, not %.80s", - Py_TYPE(pystr)->tp_name); - return NULL; - } -} - -static void -scanner_dealloc(PyObject *self) -{ - /* Deallocate scanner object */ - scanner_clear(self); - Py_TYPE(self)->tp_free(self); -} - -static int -scanner_traverse(PyObject *self, visitproc visit, void *arg) -{ - PyScannerObject *s; - assert(PyScanner_Check(self)); - s = (PyScannerObject *)self; - Py_VISIT(s->encoding); - Py_VISIT(s->strict); - Py_VISIT(s->object_hook); - Py_VISIT(s->parse_float); - Py_VISIT(s->parse_int); - Py_VISIT(s->parse_constant); - return 0; -} - -static int -scanner_clear(PyObject *self) -{ - PyScannerObject *s; - assert(PyScanner_Check(self)); - s = (PyScannerObject *)self; - Py_CLEAR(s->encoding); - Py_CLEAR(s->strict); - Py_CLEAR(s->object_hook); - Py_CLEAR(s->parse_float); - Py_CLEAR(s->parse_int); - Py_CLEAR(s->parse_constant); - return 0; -} - -static PyObject * -_parse_object_str(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *next_idx_ptr) { - /* Read a JSON object from PyString pystr. - idx is the index of the first character after the opening curly brace. - *next_idx_ptr is a return-by-reference index to the first character after - the closing curly brace. - - Returns a new PyObject (usually a dict, but object_hook can change that) - */ - char *str = PyString_AS_STRING(pystr); - Py_ssize_t end_idx = PyString_GET_SIZE(pystr) - 1; - PyObject *rval = PyDict_New(); - PyObject *key = NULL; - PyObject *val = NULL; - char *encoding = PyString_AS_STRING(s->encoding); - int strict = PyObject_IsTrue(s->strict); - Py_ssize_t next_idx; - if (rval == NULL) - return NULL; - - /* skip whitespace after { */ - while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; - - /* only loop if the object is non-empty */ - if (idx <= end_idx && str[idx] != '}') { - while (idx <= end_idx) { - /* read key */ - if (str[idx] != '"') { - raise_errmsg("Expecting property name", pystr, idx); - goto bail; - } - key = scanstring_str(pystr, idx + 1, encoding, strict, &next_idx); - if (key == NULL) - goto bail; - idx = next_idx; - - /* skip whitespace between key and : delimiter, read :, skip whitespace */ - while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; - if (idx > end_idx || str[idx] != ':') { - raise_errmsg("Expecting : delimiter", pystr, idx); - goto bail; - } - idx++; - while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; - - /* read any JSON data type */ - val = scan_once_str(s, pystr, idx, &next_idx); - if (val == NULL) - goto bail; - - if (PyDict_SetItem(rval, key, val) == -1) - goto bail; - - Py_CLEAR(key); - Py_CLEAR(val); - idx = next_idx; - - /* skip whitespace before } or , */ - while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; - - /* bail if the object is closed or we didn't get the , delimiter */ - if (idx > end_idx) break; - if (str[idx] == '}') { - break; - } - else if (str[idx] != ',') { - raise_errmsg("Expecting , delimiter", pystr, idx); - goto bail; - } - idx++; - - /* skip whitespace after , delimiter */ - while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; - } - } - /* verify that idx < end_idx, str[idx] should be '}' */ - if (idx > end_idx || str[idx] != '}') { - raise_errmsg("Expecting object", pystr, end_idx); - goto bail; - } - /* if object_hook is not None: rval = object_hook(rval) */ - if (s->object_hook != Py_None) { - val = PyObject_CallFunctionObjArgs(s->object_hook, rval, NULL); - if (val == NULL) - goto bail; - Py_DECREF(rval); - rval = val; - val = NULL; - } - *next_idx_ptr = idx + 1; - return rval; -bail: - Py_XDECREF(key); - Py_XDECREF(val); - Py_DECREF(rval); - return NULL; -} - -static PyObject * -_parse_object_unicode(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *next_idx_ptr) { - /* Read a JSON object from PyUnicode pystr. - idx is the index of the first character after the opening curly brace. - *next_idx_ptr is a return-by-reference index to the first character after - the closing curly brace. - - Returns a new PyObject (usually a dict, but object_hook can change that) - */ - Py_UNICODE *str = PyUnicode_AS_UNICODE(pystr); - Py_ssize_t end_idx = PyUnicode_GET_SIZE(pystr) - 1; - PyObject *val = NULL; - PyObject *rval = PyDict_New(); - PyObject *key = NULL; - int strict = PyObject_IsTrue(s->strict); - Py_ssize_t next_idx; - if (rval == NULL) - return NULL; - - /* skip whitespace after { */ - while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; - - /* only loop if the object is non-empty */ - if (idx <= end_idx && str[idx] != '}') { - while (idx <= end_idx) { - /* read key */ - if (str[idx] != '"') { - raise_errmsg("Expecting property name", pystr, idx); - goto bail; - } - key = scanstring_unicode(pystr, idx + 1, strict, &next_idx); - if (key == NULL) - goto bail; - idx = next_idx; - - /* skip whitespace between key and : delimiter, read :, skip whitespace */ - while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; - if (idx > end_idx || str[idx] != ':') { - raise_errmsg("Expecting : delimiter", pystr, idx); - goto bail; - } - idx++; - while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; - - /* read any JSON term */ - val = scan_once_unicode(s, pystr, idx, &next_idx); - if (val == NULL) - goto bail; - - if (PyDict_SetItem(rval, key, val) == -1) - goto bail; - - Py_CLEAR(key); - Py_CLEAR(val); - idx = next_idx; - - /* skip whitespace before } or , */ - while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; - - /* bail if the object is closed or we didn't get the , delimiter */ - if (idx > end_idx) break; - if (str[idx] == '}') { - break; - } - else if (str[idx] != ',') { - raise_errmsg("Expecting , delimiter", pystr, idx); - goto bail; - } - idx++; - - /* skip whitespace after , delimiter */ - while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; - } - } - - /* verify that idx < end_idx, str[idx] should be '}' */ - if (idx > end_idx || str[idx] != '}') { - raise_errmsg("Expecting object", pystr, end_idx); - goto bail; - } - - /* if object_hook is not None: rval = object_hook(rval) */ - if (s->object_hook != Py_None) { - val = PyObject_CallFunctionObjArgs(s->object_hook, rval, NULL); - if (val == NULL) - goto bail; - Py_DECREF(rval); - rval = val; - val = NULL; - } - *next_idx_ptr = idx + 1; - return rval; -bail: - Py_XDECREF(key); - Py_XDECREF(val); - Py_DECREF(rval); - return NULL; -} - -static PyObject * -_parse_array_str(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *next_idx_ptr) { - /* Read a JSON array from PyString pystr. - idx is the index of the first character after the opening brace. - *next_idx_ptr is a return-by-reference index to the first character after - the closing brace. - - Returns a new PyList - */ - char *str = PyString_AS_STRING(pystr); - Py_ssize_t end_idx = PyString_GET_SIZE(pystr) - 1; - PyObject *val = NULL; - PyObject *rval = PyList_New(0); - Py_ssize_t next_idx; - if (rval == NULL) - return NULL; - - /* skip whitespace after [ */ - while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; - - /* only loop if the array is non-empty */ - if (idx <= end_idx && str[idx] != ']') { - while (idx <= end_idx) { - - /* read any JSON term and de-tuplefy the (rval, idx) */ - val = scan_once_str(s, pystr, idx, &next_idx); - if (val == NULL) - goto bail; - - if (PyList_Append(rval, val) == -1) - goto bail; - - Py_CLEAR(val); - idx = next_idx; - - /* skip whitespace between term and , */ - while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; - - /* bail if the array is closed or we didn't get the , delimiter */ - if (idx > end_idx) break; - if (str[idx] == ']') { - break; - } - else if (str[idx] != ',') { - raise_errmsg("Expecting , delimiter", pystr, idx); - goto bail; - } - idx++; - - /* skip whitespace after , */ - while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; - } - } - - /* verify that idx < end_idx, str[idx] should be ']' */ - if (idx > end_idx || str[idx] != ']') { - raise_errmsg("Expecting object", pystr, end_idx); - goto bail; - } - *next_idx_ptr = idx + 1; - return rval; -bail: - Py_XDECREF(val); - Py_DECREF(rval); - return NULL; -} - -static PyObject * -_parse_array_unicode(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *next_idx_ptr) { - /* Read a JSON array from PyString pystr. - idx is the index of the first character after the opening brace. - *next_idx_ptr is a return-by-reference index to the first character after - the closing brace. - - Returns a new PyList - */ - Py_UNICODE *str = PyUnicode_AS_UNICODE(pystr); - Py_ssize_t end_idx = PyUnicode_GET_SIZE(pystr) - 1; - PyObject *val = NULL; - PyObject *rval = PyList_New(0); - Py_ssize_t next_idx; - if (rval == NULL) - return NULL; - - /* skip whitespace after [ */ - while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; - - /* only loop if the array is non-empty */ - if (idx <= end_idx && str[idx] != ']') { - while (idx <= end_idx) { - - /* read any JSON term */ - val = scan_once_unicode(s, pystr, idx, &next_idx); - if (val == NULL) - goto bail; - - if (PyList_Append(rval, val) == -1) - goto bail; - - Py_CLEAR(val); - idx = next_idx; - - /* skip whitespace between term and , */ - while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; - - /* bail if the array is closed or we didn't get the , delimiter */ - if (idx > end_idx) break; - if (str[idx] == ']') { - break; - } - else if (str[idx] != ',') { - raise_errmsg("Expecting , delimiter", pystr, idx); - goto bail; - } - idx++; - - /* skip whitespace after , */ - while (idx <= end_idx && IS_WHITESPACE(str[idx])) idx++; - } - } - - /* verify that idx < end_idx, str[idx] should be ']' */ - if (idx > end_idx || str[idx] != ']') { - raise_errmsg("Expecting object", pystr, end_idx); - goto bail; - } - *next_idx_ptr = idx + 1; - return rval; -bail: - Py_XDECREF(val); - Py_DECREF(rval); - return NULL; -} - -static PyObject * -_parse_constant(PyScannerObject *s, char *constant, Py_ssize_t idx, Py_ssize_t *next_idx_ptr) { - /* Read a JSON constant from PyString pystr. - constant is the constant string that was found - ("NaN", "Infinity", "-Infinity"). - idx is the index of the first character of the constant - *next_idx_ptr is a return-by-reference index to the first character after - the constant. - - Returns the result of parse_constant - */ - PyObject *cstr; - PyObject *rval; - /* constant is "NaN", "Infinity", or "-Infinity" */ - cstr = PyString_InternFromString(constant); - if (cstr == NULL) - return NULL; - - /* rval = parse_constant(constant) */ - rval = PyObject_CallFunctionObjArgs(s->parse_constant, cstr, NULL); - idx += PyString_GET_SIZE(cstr); - Py_DECREF(cstr); - *next_idx_ptr = idx; - return rval; -} - -static PyObject * -_match_number_str(PyScannerObject *s, PyObject *pystr, Py_ssize_t start, Py_ssize_t *next_idx_ptr) { - /* Read a JSON number from PyString pystr. - idx is the index of the first character of the number - *next_idx_ptr is a return-by-reference index to the first character after - the number. - - Returns a new PyObject representation of that number: - PyInt, PyLong, or PyFloat. - May return other types if parse_int or parse_float are set - */ - char *str = PyString_AS_STRING(pystr); - Py_ssize_t end_idx = PyString_GET_SIZE(pystr) - 1; - Py_ssize_t idx = start; - int is_float = 0; - PyObject *rval; - PyObject *numstr; - - /* read a sign if it's there, make sure it's not the end of the string */ - if (str[idx] == '-') { - idx++; - if (idx > end_idx) { - PyErr_SetNone(PyExc_StopIteration); - return NULL; - } - } - - /* read as many integer digits as we find as long as it doesn't start with 0 */ - if (str[idx] >= '1' && str[idx] <= '9') { - idx++; - while (idx <= end_idx && str[idx] >= '0' && str[idx] <= '9') idx++; - } - /* if it starts with 0 we only expect one integer digit */ - else if (str[idx] == '0') { - idx++; - } - /* no integer digits, error */ - else { - PyErr_SetNone(PyExc_StopIteration); - return NULL; - } - - /* if the next char is '.' followed by a digit then read all float digits */ - if (idx < end_idx && str[idx] == '.' && str[idx + 1] >= '0' && str[idx + 1] <= '9') { - is_float = 1; - idx += 2; - while (idx <= end_idx && str[idx] >= '0' && str[idx] <= '9') idx++; - } - - /* if the next char is 'e' or 'E' then maybe read the exponent (or backtrack) */ - if (idx < end_idx && (str[idx] == 'e' || str[idx] == 'E')) { - - /* save the index of the 'e' or 'E' just in case we need to backtrack */ - Py_ssize_t e_start = idx; - idx++; - - /* read an exponent sign if present */ - if (idx < end_idx && (str[idx] == '-' || str[idx] == '+')) idx++; - - /* read all digits */ - while (idx <= end_idx && str[idx] >= '0' && str[idx] <= '9') idx++; - - /* if we got a digit, then parse as float. if not, backtrack */ - if (str[idx - 1] >= '0' && str[idx - 1] <= '9') { - is_float = 1; - } - else { - idx = e_start; - } - } - - /* copy the section we determined to be a number */ - numstr = PyString_FromStringAndSize(&str[start], idx - start); - if (numstr == NULL) - return NULL; - if (is_float) { - /* parse as a float using a fast path if available, otherwise call user defined method */ - if (s->parse_float != (PyObject *)&PyFloat_Type) { - rval = PyObject_CallFunctionObjArgs(s->parse_float, numstr, NULL); - } - else { - rval = PyFloat_FromDouble(PyOS_ascii_atof(PyString_AS_STRING(numstr))); - } - } - else { - /* parse as an int using a fast path if available, otherwise call user defined method */ - if (s->parse_int != (PyObject *)&PyInt_Type) { - rval = PyObject_CallFunctionObjArgs(s->parse_int, numstr, NULL); - } - else { - rval = PyInt_FromString(PyString_AS_STRING(numstr), NULL, 10); - } - } - Py_DECREF(numstr); - *next_idx_ptr = idx; - return rval; -} - -static PyObject * -_match_number_unicode(PyScannerObject *s, PyObject *pystr, Py_ssize_t start, Py_ssize_t *next_idx_ptr) { - /* Read a JSON number from PyUnicode pystr. - idx is the index of the first character of the number - *next_idx_ptr is a return-by-reference index to the first character after - the number. - - Returns a new PyObject representation of that number: - PyInt, PyLong, or PyFloat. - May return other types if parse_int or parse_float are set - */ - Py_UNICODE *str = PyUnicode_AS_UNICODE(pystr); - Py_ssize_t end_idx = PyUnicode_GET_SIZE(pystr) - 1; - Py_ssize_t idx = start; - int is_float = 0; - PyObject *rval; - PyObject *numstr; - - /* read a sign if it's there, make sure it's not the end of the string */ - if (str[idx] == '-') { - idx++; - if (idx > end_idx) { - PyErr_SetNone(PyExc_StopIteration); - return NULL; - } - } - - /* read as many integer digits as we find as long as it doesn't start with 0 */ - if (str[idx] >= '1' && str[idx] <= '9') { - idx++; - while (idx <= end_idx && str[idx] >= '0' && str[idx] <= '9') idx++; - } - /* if it starts with 0 we only expect one integer digit */ - else if (str[idx] == '0') { - idx++; - } - /* no integer digits, error */ - else { - PyErr_SetNone(PyExc_StopIteration); - return NULL; - } - - /* if the next char is '.' followed by a digit then read all float digits */ - if (idx < end_idx && str[idx] == '.' && str[idx + 1] >= '0' && str[idx + 1] <= '9') { - is_float = 1; - idx += 2; - while (idx < end_idx && str[idx] >= '0' && str[idx] <= '9') idx++; - } - - /* if the next char is 'e' or 'E' then maybe read the exponent (or backtrack) */ - if (idx < end_idx && (str[idx] == 'e' || str[idx] == 'E')) { - Py_ssize_t e_start = idx; - idx++; - - /* read an exponent sign if present */ - if (idx < end_idx && (str[idx] == '-' || str[idx] == '+')) idx++; - - /* read all digits */ - while (idx <= end_idx && str[idx] >= '0' && str[idx] <= '9') idx++; - - /* if we got a digit, then parse as float. if not, backtrack */ - if (str[idx - 1] >= '0' && str[idx - 1] <= '9') { - is_float = 1; - } - else { - idx = e_start; - } - } - - /* copy the section we determined to be a number */ - numstr = PyUnicode_FromUnicode(&str[start], idx - start); - if (numstr == NULL) - return NULL; - if (is_float) { - /* parse as a float using a fast path if available, otherwise call user defined method */ - if (s->parse_float != (PyObject *)&PyFloat_Type) { - rval = PyObject_CallFunctionObjArgs(s->parse_float, numstr, NULL); - } - else { - rval = PyFloat_FromString(numstr, NULL); - } - } - else { - /* no fast path for unicode -> int, just call */ - rval = PyObject_CallFunctionObjArgs(s->parse_int, numstr, NULL); - } - Py_DECREF(numstr); - *next_idx_ptr = idx; - return rval; -} - -static PyObject * -scan_once_str(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *next_idx_ptr) -{ - /* Read one JSON term (of any kind) from PyString pystr. - idx is the index of the first character of the term - *next_idx_ptr is a return-by-reference index to the first character after - the number. - - Returns a new PyObject representation of the term. - */ - char *str = PyString_AS_STRING(pystr); - Py_ssize_t length = PyString_GET_SIZE(pystr); - if (idx >= length) { - PyErr_SetNone(PyExc_StopIteration); - return NULL; - } - switch (str[idx]) { - case '"': - /* string */ - return scanstring_str(pystr, idx + 1, - PyString_AS_STRING(s->encoding), - PyObject_IsTrue(s->strict), - next_idx_ptr); - case '{': - /* object */ - return _parse_object_str(s, pystr, idx + 1, next_idx_ptr); - case '[': - /* array */ - return _parse_array_str(s, pystr, idx + 1, next_idx_ptr); - case 'n': - /* null */ - if ((idx + 3 < length) && str[idx + 1] == 'u' && str[idx + 2] == 'l' && str[idx + 3] == 'l') { - Py_INCREF(Py_None); - *next_idx_ptr = idx + 4; - return Py_None; - } - break; - case 't': - /* true */ - if ((idx + 3 < length) && str[idx + 1] == 'r' && str[idx + 2] == 'u' && str[idx + 3] == 'e') { - Py_INCREF(Py_True); - *next_idx_ptr = idx + 4; - return Py_True; - } - break; - case 'f': - /* false */ - if ((idx + 4 < length) && str[idx + 1] == 'a' && str[idx + 2] == 'l' && str[idx + 3] == 's' && str[idx + 4] == 'e') { - Py_INCREF(Py_False); - *next_idx_ptr = idx + 5; - return Py_False; - } - break; - case 'N': - /* NaN */ - if ((idx + 2 < length) && str[idx + 1] == 'a' && str[idx + 2] == 'N') { - return _parse_constant(s, "NaN", idx, next_idx_ptr); - } - break; - case 'I': - /* Infinity */ - if ((idx + 7 < length) && str[idx + 1] == 'n' && str[idx + 2] == 'f' && str[idx + 3] == 'i' && str[idx + 4] == 'n' && str[idx + 5] == 'i' && str[idx + 6] == 't' && str[idx + 7] == 'y') { - return _parse_constant(s, "Infinity", idx, next_idx_ptr); - } - break; - case '-': - /* -Infinity */ - if ((idx + 8 < length) && str[idx + 1] == 'I' && str[idx + 2] == 'n' && str[idx + 3] == 'f' && str[idx + 4] == 'i' && str[idx + 5] == 'n' && str[idx + 6] == 'i' && str[idx + 7] == 't' && str[idx + 8] == 'y') { - return _parse_constant(s, "-Infinity", idx, next_idx_ptr); - } - break; - } - /* Didn't find a string, object, array, or named constant. Look for a number. */ - return _match_number_str(s, pystr, idx, next_idx_ptr); -} - -static PyObject * -scan_once_unicode(PyScannerObject *s, PyObject *pystr, Py_ssize_t idx, Py_ssize_t *next_idx_ptr) -{ - /* Read one JSON term (of any kind) from PyUnicode pystr. - idx is the index of the first character of the term - *next_idx_ptr is a return-by-reference index to the first character after - the number. - - Returns a new PyObject representation of the term. - */ - Py_UNICODE *str = PyUnicode_AS_UNICODE(pystr); - Py_ssize_t length = PyUnicode_GET_SIZE(pystr); - if (idx >= length) { - PyErr_SetNone(PyExc_StopIteration); - return NULL; - } - switch (str[idx]) { - case '"': - /* string */ - return scanstring_unicode(pystr, idx + 1, - PyObject_IsTrue(s->strict), - next_idx_ptr); - case '{': - /* object */ - return _parse_object_unicode(s, pystr, idx + 1, next_idx_ptr); - case '[': - /* array */ - return _parse_array_unicode(s, pystr, idx + 1, next_idx_ptr); - case 'n': - /* null */ - if ((idx + 3 < length) && str[idx + 1] == 'u' && str[idx + 2] == 'l' && str[idx + 3] == 'l') { - Py_INCREF(Py_None); - *next_idx_ptr = idx + 4; - return Py_None; - } - break; - case 't': - /* true */ - if ((idx + 3 < length) && str[idx + 1] == 'r' && str[idx + 2] == 'u' && str[idx + 3] == 'e') { - Py_INCREF(Py_True); - *next_idx_ptr = idx + 4; - return Py_True; - } - break; - case 'f': - /* false */ - if ((idx + 4 < length) && str[idx + 1] == 'a' && str[idx + 2] == 'l' && str[idx + 3] == 's' && str[idx + 4] == 'e') { - Py_INCREF(Py_False); - *next_idx_ptr = idx + 5; - return Py_False; - } - break; - case 'N': - /* NaN */ - if ((idx + 2 < length) && str[idx + 1] == 'a' && str[idx + 2] == 'N') { - return _parse_constant(s, "NaN", idx, next_idx_ptr); - } - break; - case 'I': - /* Infinity */ - if ((idx + 7 < length) && str[idx + 1] == 'n' && str[idx + 2] == 'f' && str[idx + 3] == 'i' && str[idx + 4] == 'n' && str[idx + 5] == 'i' && str[idx + 6] == 't' && str[idx + 7] == 'y') { - return _parse_constant(s, "Infinity", idx, next_idx_ptr); - } - break; - case '-': - /* -Infinity */ - if ((idx + 8 < length) && str[idx + 1] == 'I' && str[idx + 2] == 'n' && str[idx + 3] == 'f' && str[idx + 4] == 'i' && str[idx + 5] == 'n' && str[idx + 6] == 'i' && str[idx + 7] == 't' && str[idx + 8] == 'y') { - return _parse_constant(s, "-Infinity", idx, next_idx_ptr); - } - break; - } - /* Didn't find a string, object, array, or named constant. Look for a number. */ - return _match_number_unicode(s, pystr, idx, next_idx_ptr); -} - -static PyObject * -scanner_call(PyObject *self, PyObject *args, PyObject *kwds) -{ - /* Python callable interface to scan_once_{str,unicode} */ - PyObject *pystr; - PyObject *rval; - Py_ssize_t idx; - Py_ssize_t next_idx = -1; - static char *kwlist[] = {"string", "idx", NULL}; - PyScannerObject *s; - assert(PyScanner_Check(self)); - s = (PyScannerObject *)self; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO&:scan_once", kwlist, &pystr, _convertPyInt_AsSsize_t, &idx)) - return NULL; - - if (PyString_Check(pystr)) { - rval = scan_once_str(s, pystr, idx, &next_idx); - } - else if (PyUnicode_Check(pystr)) { - rval = scan_once_unicode(s, pystr, idx, &next_idx); - } - else { - PyErr_Format(PyExc_TypeError, - "first argument must be a string, not %.80s", - Py_TYPE(pystr)->tp_name); - return NULL; - } - return _build_rval_index_tuple(rval, next_idx); -} - -static PyObject * -scanner_new(PyTypeObject *type, PyObject *args, PyObject *kwds) -{ - PyScannerObject *s; - s = (PyScannerObject *)type->tp_alloc(type, 0); - if (s != NULL) { - s->encoding = NULL; - s->strict = NULL; - s->object_hook = NULL; - s->parse_float = NULL; - s->parse_int = NULL; - s->parse_constant = NULL; - } - return (PyObject *)s; -} - -static int -scanner_init(PyObject *self, PyObject *args, PyObject *kwds) -{ - /* Initialize Scanner object */ - PyObject *ctx; - static char *kwlist[] = {"context", NULL}; - PyScannerObject *s; - - assert(PyScanner_Check(self)); - s = (PyScannerObject *)self; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O:make_scanner", kwlist, &ctx)) - return -1; - - /* PyString_AS_STRING is used on encoding */ - s->encoding = PyObject_GetAttrString(ctx, "encoding"); - if (s->encoding == Py_None) { - Py_DECREF(Py_None); - s->encoding = PyString_InternFromString(DEFAULT_ENCODING); - } - else if (PyUnicode_Check(s->encoding)) { - PyObject *tmp = PyUnicode_AsEncodedString(s->encoding, NULL, NULL); - Py_DECREF(s->encoding); - s->encoding = tmp; - } - if (s->encoding == NULL || !PyString_Check(s->encoding)) - goto bail; - - /* All of these will fail "gracefully" so we don't need to verify them */ - s->strict = PyObject_GetAttrString(ctx, "strict"); - if (s->strict == NULL) - goto bail; - s->object_hook = PyObject_GetAttrString(ctx, "object_hook"); - if (s->object_hook == NULL) - goto bail; - s->parse_float = PyObject_GetAttrString(ctx, "parse_float"); - if (s->parse_float == NULL) - goto bail; - s->parse_int = PyObject_GetAttrString(ctx, "parse_int"); - if (s->parse_int == NULL) - goto bail; - s->parse_constant = PyObject_GetAttrString(ctx, "parse_constant"); - if (s->parse_constant == NULL) - goto bail; - - return 0; - -bail: - Py_CLEAR(s->encoding); - Py_CLEAR(s->strict); - Py_CLEAR(s->object_hook); - Py_CLEAR(s->parse_float); - Py_CLEAR(s->parse_int); - Py_CLEAR(s->parse_constant); - return -1; -} - -PyDoc_STRVAR(scanner_doc, "JSON scanner object"); - -static -PyTypeObject PyScannerType = { - PyObject_HEAD_INIT(NULL) - 0, /* tp_internal */ - "simplejson._speedups.Scanner", /* tp_name */ - sizeof(PyScannerObject), /* tp_basicsize */ - 0, /* tp_itemsize */ - scanner_dealloc, /* tp_dealloc */ - 0, /* tp_print */ - 0, /* tp_getattr */ - 0, /* tp_setattr */ - 0, /* tp_compare */ - 0, /* tp_repr */ - 0, /* tp_as_number */ - 0, /* tp_as_sequence */ - 0, /* tp_as_mapping */ - 0, /* tp_hash */ - scanner_call, /* tp_call */ - 0, /* tp_str */ - 0,/* PyObject_GenericGetAttr, */ /* tp_getattro */ - 0,/* PyObject_GenericSetAttr, */ /* tp_setattro */ - 0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, /* tp_flags */ - scanner_doc, /* tp_doc */ - scanner_traverse, /* tp_traverse */ - scanner_clear, /* tp_clear */ - 0, /* tp_richcompare */ - 0, /* tp_weaklistoffset */ - 0, /* tp_iter */ - 0, /* tp_iternext */ - 0, /* tp_methods */ - scanner_members, /* tp_members */ - 0, /* tp_getset */ - 0, /* tp_base */ - 0, /* tp_dict */ - 0, /* tp_descr_get */ - 0, /* tp_descr_set */ - 0, /* tp_dictoffset */ - scanner_init, /* tp_init */ - 0,/* PyType_GenericAlloc, */ /* tp_alloc */ - scanner_new, /* tp_new */ - 0,/* PyObject_GC_Del, */ /* tp_free */ -}; - -static PyObject * -encoder_new(PyTypeObject *type, PyObject *args, PyObject *kwds) -{ - PyEncoderObject *s; - s = (PyEncoderObject *)type->tp_alloc(type, 0); - if (s != NULL) { - s->markers = NULL; - s->defaultfn = NULL; - s->encoder = NULL; - s->indent = NULL; - s->key_separator = NULL; - s->item_separator = NULL; - s->sort_keys = NULL; - s->skipkeys = NULL; - } - return (PyObject *)s; -} - -static int -encoder_init(PyObject *self, PyObject *args, PyObject *kwds) -{ - /* initialize Encoder object */ - static char *kwlist[] = {"markers", "default", "encoder", "indent", "key_separator", "item_separator", "sort_keys", "skipkeys", "allow_nan", NULL}; - - PyEncoderObject *s; - PyObject *allow_nan; - - assert(PyEncoder_Check(self)); - s = (PyEncoderObject *)self; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOOOOOOOO:make_encoder", kwlist, - &s->markers, &s->defaultfn, &s->encoder, &s->indent, &s->key_separator, &s->item_separator, &s->sort_keys, &s->skipkeys, &allow_nan)) - return -1; - - Py_INCREF(s->markers); - Py_INCREF(s->defaultfn); - Py_INCREF(s->encoder); - Py_INCREF(s->indent); - Py_INCREF(s->key_separator); - Py_INCREF(s->item_separator); - Py_INCREF(s->sort_keys); - Py_INCREF(s->skipkeys); - s->fast_encode = (PyCFunction_Check(s->encoder) && PyCFunction_GetFunction(s->encoder) == (PyCFunction)py_encode_basestring_ascii); - s->allow_nan = PyObject_IsTrue(allow_nan); - return 0; -} - -static PyObject * -encoder_call(PyObject *self, PyObject *args, PyObject *kwds) -{ - /* Python callable interface to encode_listencode_obj */ - static char *kwlist[] = {"obj", "_current_indent_level", NULL}; - PyObject *obj; - PyObject *rval; - Py_ssize_t indent_level; - PyEncoderObject *s; - assert(PyEncoder_Check(self)); - s = (PyEncoderObject *)self; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO&:_iterencode", kwlist, - &obj, _convertPyInt_AsSsize_t, &indent_level)) - return NULL; - rval = PyList_New(0); - if (rval == NULL) - return NULL; - if (encoder_listencode_obj(s, rval, obj, indent_level)) { - Py_DECREF(rval); - return NULL; - } - return rval; -} - -static PyObject * -_encoded_const(PyObject *obj) -{ - /* Return the JSON string representation of None, True, False */ - if (obj == Py_None) { - static PyObject *s_null = NULL; - if (s_null == NULL) { - s_null = PyString_InternFromString("null"); - } - Py_INCREF(s_null); - return s_null; - } - else if (obj == Py_True) { - static PyObject *s_true = NULL; - if (s_true == NULL) { - s_true = PyString_InternFromString("true"); - } - Py_INCREF(s_true); - return s_true; - } - else if (obj == Py_False) { - static PyObject *s_false = NULL; - if (s_false == NULL) { - s_false = PyString_InternFromString("false"); - } - Py_INCREF(s_false); - return s_false; - } - else { - PyErr_SetString(PyExc_ValueError, "not a const"); - return NULL; - } -} - -static PyObject * -encoder_encode_float(PyEncoderObject *s, PyObject *obj) -{ - /* Return the JSON representation of a PyFloat */ - double i = PyFloat_AS_DOUBLE(obj); - if (!Py_IS_FINITE(i)) { - if (!s->allow_nan) { - PyErr_SetString(PyExc_ValueError, "Out of range float values are not JSON compliant"); - return NULL; - } - if (i > 0) { - return PyString_FromString("Infinity"); - } - else if (i < 0) { - return PyString_FromString("-Infinity"); - } - else { - return PyString_FromString("NaN"); - } - } - /* Use a better float format here? */ - return PyObject_Repr(obj); -} - -static PyObject * -encoder_encode_string(PyEncoderObject *s, PyObject *obj) -{ - /* Return the JSON representation of a string */ - if (s->fast_encode) - return py_encode_basestring_ascii(NULL, obj); - else - return PyObject_CallFunctionObjArgs(s->encoder, obj, NULL); -} - -static int -_steal_list_append(PyObject *lst, PyObject *stolen) -{ - /* Append stolen and then decrement its reference count */ - int rval = PyList_Append(lst, stolen); - Py_DECREF(stolen); - return rval; -} - -static int -encoder_listencode_obj(PyEncoderObject *s, PyObject *rval, PyObject *obj, Py_ssize_t indent_level) -{ - /* Encode Python object obj to a JSON term, rval is a PyList */ - PyObject *newobj; - int rv; - - if (obj == Py_None || obj == Py_True || obj == Py_False) { - PyObject *cstr = _encoded_const(obj); - if (cstr == NULL) - return -1; - return _steal_list_append(rval, cstr); - } - else if (PyString_Check(obj) || PyUnicode_Check(obj)) - { - PyObject *encoded = encoder_encode_string(s, obj); - if (encoded == NULL) - return -1; - return _steal_list_append(rval, encoded); - } - else if (PyInt_Check(obj) || PyLong_Check(obj)) { - PyObject *encoded = PyObject_Str(obj); - if (encoded == NULL) - return -1; - return _steal_list_append(rval, encoded); - } - else if (PyFloat_Check(obj)) { - PyObject *encoded = encoder_encode_float(s, obj); - if (encoded == NULL) - return -1; - return _steal_list_append(rval, encoded); - } - else if (PyList_Check(obj) || PyTuple_Check(obj)) { - return encoder_listencode_list(s, rval, obj, indent_level); - } - else if (PyDict_Check(obj)) { - return encoder_listencode_dict(s, rval, obj, indent_level); - } - else { - PyObject *ident = NULL; - if (s->markers != Py_None) { - int has_key; - ident = PyLong_FromVoidPtr(obj); - if (ident == NULL) - return -1; - has_key = PyDict_Contains(s->markers, ident); - if (has_key) { - if (has_key != -1) - PyErr_SetString(PyExc_ValueError, "Circular reference detected"); - Py_DECREF(ident); - return -1; - } - if (PyDict_SetItem(s->markers, ident, obj)) { - Py_DECREF(ident); - return -1; - } - } - newobj = PyObject_CallFunctionObjArgs(s->defaultfn, obj, NULL); - if (newobj == NULL) { - Py_XDECREF(ident); - return -1; - } - rv = encoder_listencode_obj(s, rval, newobj, indent_level); - Py_DECREF(newobj); - if (rv) { - Py_XDECREF(ident); - return -1; - } - if (ident != NULL) { - if (PyDict_DelItem(s->markers, ident)) { - Py_XDECREF(ident); - return -1; - } - Py_XDECREF(ident); - } - return rv; - } -} - -static int -encoder_listencode_dict(PyEncoderObject *s, PyObject *rval, PyObject *dct, Py_ssize_t indent_level) -{ - /* Encode Python dict dct a JSON term, rval is a PyList */ - static PyObject *open_dict = NULL; - static PyObject *close_dict = NULL; - static PyObject *empty_dict = NULL; - PyObject *kstr = NULL; - PyObject *ident = NULL; - PyObject *key, *value; - Py_ssize_t pos; - int skipkeys; - Py_ssize_t idx; - - if (open_dict == NULL || close_dict == NULL || empty_dict == NULL) { - open_dict = PyString_InternFromString("{"); - close_dict = PyString_InternFromString("}"); - empty_dict = PyString_InternFromString("{}"); - if (open_dict == NULL || close_dict == NULL || empty_dict == NULL) - return -1; - } - if (PyDict_Size(dct) == 0) - return PyList_Append(rval, empty_dict); - - if (s->markers != Py_None) { - int has_key; - ident = PyLong_FromVoidPtr(dct); - if (ident == NULL) - goto bail; - has_key = PyDict_Contains(s->markers, ident); - if (has_key) { - if (has_key != -1) - PyErr_SetString(PyExc_ValueError, "Circular reference detected"); - goto bail; - } - if (PyDict_SetItem(s->markers, ident, dct)) { - goto bail; - } - } - - if (PyList_Append(rval, open_dict)) - goto bail; - - if (s->indent != Py_None) { - /* TODO: DOES NOT RUN */ - indent_level += 1; - /* - newline_indent = '\n' + (' ' * (_indent * _current_indent_level)) - separator = _item_separator + newline_indent - buf += newline_indent - */ - } - - /* TODO: C speedup not implemented for sort_keys */ - - pos = 0; - skipkeys = PyObject_IsTrue(s->skipkeys); - idx = 0; - while (PyDict_Next(dct, &pos, &key, &value)) { - PyObject *encoded; - - if (PyString_Check(key) || PyUnicode_Check(key)) { - Py_INCREF(key); - kstr = key; - } - else if (PyFloat_Check(key)) { - kstr = encoder_encode_float(s, key); - if (kstr == NULL) - goto bail; - } - else if (PyInt_Check(key) || PyLong_Check(key)) { - kstr = PyObject_Str(key); - if (kstr == NULL) - goto bail; - } - else if (key == Py_True || key == Py_False || key == Py_None) { - kstr = _encoded_const(key); - if (kstr == NULL) - goto bail; - } - else if (skipkeys) { - continue; - } - else { - /* TODO: include repr of key */ - PyErr_SetString(PyExc_ValueError, "keys must be a string"); - goto bail; - } - - if (idx) { - if (PyList_Append(rval, s->item_separator)) - goto bail; - } - - encoded = encoder_encode_string(s, kstr); - Py_CLEAR(kstr); - if (encoded == NULL) - goto bail; - if (PyList_Append(rval, encoded)) { - Py_DECREF(encoded); - goto bail; - } - Py_DECREF(encoded); - if (PyList_Append(rval, s->key_separator)) - goto bail; - if (encoder_listencode_obj(s, rval, value, indent_level)) - goto bail; - idx += 1; - } - if (ident != NULL) { - if (PyDict_DelItem(s->markers, ident)) - goto bail; - Py_CLEAR(ident); - } - if (s->indent != Py_None) { - /* TODO: DOES NOT RUN */ - indent_level -= 1; - /* - yield '\n' + (' ' * (_indent * _current_indent_level)) - */ - } - if (PyList_Append(rval, close_dict)) - goto bail; - return 0; - -bail: - Py_XDECREF(kstr); - Py_XDECREF(ident); - return -1; -} - - -static int -encoder_listencode_list(PyEncoderObject *s, PyObject *rval, PyObject *seq, Py_ssize_t indent_level) -{ - /* Encode Python list seq to a JSON term, rval is a PyList */ - static PyObject *open_array = NULL; - static PyObject *close_array = NULL; - static PyObject *empty_array = NULL; - PyObject *ident = NULL; - PyObject *s_fast = NULL; - Py_ssize_t num_items; - PyObject **seq_items; - Py_ssize_t i; - - if (open_array == NULL || close_array == NULL || empty_array == NULL) { - open_array = PyString_InternFromString("["); - close_array = PyString_InternFromString("]"); - empty_array = PyString_InternFromString("[]"); - if (open_array == NULL || close_array == NULL || empty_array == NULL) - return -1; - } - ident = NULL; - s_fast = PySequence_Fast(seq, "_iterencode_list needs a sequence"); - if (s_fast == NULL) - return -1; - num_items = PySequence_Fast_GET_SIZE(s_fast); - if (num_items == 0) { - Py_DECREF(s_fast); - return PyList_Append(rval, empty_array); - } - - if (s->markers != Py_None) { - int has_key; - ident = PyLong_FromVoidPtr(seq); - if (ident == NULL) - goto bail; - has_key = PyDict_Contains(s->markers, ident); - if (has_key) { - if (has_key != -1) - PyErr_SetString(PyExc_ValueError, "Circular reference detected"); - goto bail; - } - if (PyDict_SetItem(s->markers, ident, seq)) { - goto bail; - } - } - - seq_items = PySequence_Fast_ITEMS(s_fast); - if (PyList_Append(rval, open_array)) - goto bail; - if (s->indent != Py_None) { - /* TODO: DOES NOT RUN */ - indent_level += 1; - /* - newline_indent = '\n' + (' ' * (_indent * _current_indent_level)) - separator = _item_separator + newline_indent - buf += newline_indent - */ - } - for (i = 0; i < num_items; i++) { - PyObject *obj = seq_items[i]; - if (i) { - if (PyList_Append(rval, s->item_separator)) - goto bail; - } - if (encoder_listencode_obj(s, rval, obj, indent_level)) - goto bail; - } - if (ident != NULL) { - if (PyDict_DelItem(s->markers, ident)) - goto bail; - Py_CLEAR(ident); - } - if (s->indent != Py_None) { - /* TODO: DOES NOT RUN */ - indent_level -= 1; - /* - yield '\n' + (' ' * (_indent * _current_indent_level)) - */ - } - if (PyList_Append(rval, close_array)) - goto bail; - Py_DECREF(s_fast); - return 0; - -bail: - Py_XDECREF(ident); - Py_DECREF(s_fast); - return -1; -} - -static void -encoder_dealloc(PyObject *self) -{ - /* Deallocate Encoder */ - encoder_clear(self); - Py_TYPE(self)->tp_free(self); -} - -static int -encoder_traverse(PyObject *self, visitproc visit, void *arg) -{ - PyEncoderObject *s; - assert(PyEncoder_Check(self)); - s = (PyEncoderObject *)self; - Py_VISIT(s->markers); - Py_VISIT(s->defaultfn); - Py_VISIT(s->encoder); - Py_VISIT(s->indent); - Py_VISIT(s->key_separator); - Py_VISIT(s->item_separator); - Py_VISIT(s->sort_keys); - Py_VISIT(s->skipkeys); - return 0; -} - -static int -encoder_clear(PyObject *self) -{ - /* Deallocate Encoder */ - PyEncoderObject *s; - assert(PyEncoder_Check(self)); - s = (PyEncoderObject *)self; - Py_CLEAR(s->markers); - Py_CLEAR(s->defaultfn); - Py_CLEAR(s->encoder); - Py_CLEAR(s->indent); - Py_CLEAR(s->key_separator); - Py_CLEAR(s->item_separator); - Py_CLEAR(s->sort_keys); - Py_CLEAR(s->skipkeys); - return 0; -} - -PyDoc_STRVAR(encoder_doc, "_iterencode(obj, _current_indent_level) -> iterable"); - -static -PyTypeObject PyEncoderType = { - PyObject_HEAD_INIT(NULL) - 0, /* tp_internal */ - "simplejson._speedups.Encoder", /* tp_name */ - sizeof(PyEncoderObject), /* tp_basicsize */ - 0, /* tp_itemsize */ - encoder_dealloc, /* tp_dealloc */ - 0, /* tp_print */ - 0, /* tp_getattr */ - 0, /* tp_setattr */ - 0, /* tp_compare */ - 0, /* tp_repr */ - 0, /* tp_as_number */ - 0, /* tp_as_sequence */ - 0, /* tp_as_mapping */ - 0, /* tp_hash */ - encoder_call, /* tp_call */ - 0, /* tp_str */ - 0, /* tp_getattro */ - 0, /* tp_setattro */ - 0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, /* tp_flags */ - encoder_doc, /* tp_doc */ - encoder_traverse, /* tp_traverse */ - encoder_clear, /* tp_clear */ - 0, /* tp_richcompare */ - 0, /* tp_weaklistoffset */ - 0, /* tp_iter */ - 0, /* tp_iternext */ - 0, /* tp_methods */ - encoder_members, /* tp_members */ - 0, /* tp_getset */ - 0, /* tp_base */ - 0, /* tp_dict */ - 0, /* tp_descr_get */ - 0, /* tp_descr_set */ - 0, /* tp_dictoffset */ - encoder_init, /* tp_init */ - 0, /* tp_alloc */ - encoder_new, /* tp_new */ - 0, /* tp_free */ -}; - -static PyMethodDef speedups_methods[] = { - {"encode_basestring_ascii", - (PyCFunction)py_encode_basestring_ascii, - METH_O, - pydoc_encode_basestring_ascii}, - {"scanstring", - (PyCFunction)py_scanstring, - METH_VARARGS, - pydoc_scanstring}, - {NULL, NULL, 0, NULL} -}; - -PyDoc_STRVAR(module_doc, -"simplejson speedups\n"); - -void -init_speedups(void) -{ - PyObject *m; - PyScannerType.tp_new = PyType_GenericNew; - if (PyType_Ready(&PyScannerType) < 0) - return; - PyEncoderType.tp_new = PyType_GenericNew; - if (PyType_Ready(&PyEncoderType) < 0) - return; - m = Py_InitModule3("_speedups", speedups_methods, module_doc); - Py_INCREF((PyObject*)&PyScannerType); - PyModule_AddObject(m, "make_scanner", (PyObject*)&PyScannerType); - Py_INCREF((PyObject*)&PyEncoderType); - PyModule_AddObject(m, "make_encoder", (PyObject*)&PyEncoderType); -} diff --git a/lib/simplejson/decoder.py b/lib/simplejson/decoder.py deleted file mode 100644 index b769ea486..000000000 --- a/lib/simplejson/decoder.py +++ /dev/null @@ -1,354 +0,0 @@ -"""Implementation of JSONDecoder -""" -import re -import sys -import struct - -from simplejson.scanner import make_scanner -try: - from simplejson._speedups import scanstring as c_scanstring -except ImportError: - c_scanstring = None - -__all__ = ['JSONDecoder'] - -FLAGS = re.VERBOSE | re.MULTILINE | re.DOTALL - -def _floatconstants(): - _BYTES = '7FF80000000000007FF0000000000000'.decode('hex') - if sys.byteorder != 'big': - _BYTES = _BYTES[:8][::-1] + _BYTES[8:][::-1] - nan, inf = struct.unpack('dd', _BYTES) - return nan, inf, -inf - -NaN, PosInf, NegInf = _floatconstants() - - -def linecol(doc, pos): - lineno = doc.count('\n', 0, pos) + 1 - if lineno == 1: - colno = pos - else: - colno = pos - doc.rindex('\n', 0, pos) - return lineno, colno - - -def errmsg(msg, doc, pos, end=None): - # Note that this function is called from _speedups - lineno, colno = linecol(doc, pos) - if end is None: - #fmt = '{0}: line {1} column {2} (char {3})' - #return fmt.format(msg, lineno, colno, pos) - fmt = '%s: line %d column %d (char %d)' - return fmt % (msg, lineno, colno, pos) - endlineno, endcolno = linecol(doc, end) - #fmt = '{0}: line {1} column {2} - line {3} column {4} (char {5} - {6})' - #return fmt.format(msg, lineno, colno, endlineno, endcolno, pos, end) - fmt = '%s: line %d column %d - line %d column %d (char %d - %d)' - return fmt % (msg, lineno, colno, endlineno, endcolno, pos, end) - - -_CONSTANTS = { - '-Infinity': NegInf, - 'Infinity': PosInf, - 'NaN': NaN, -} - -STRINGCHUNK = re.compile(r'(.*?)(["\\\x00-\x1f])', FLAGS) -BACKSLASH = { - '"': u'"', '\\': u'\\', '/': u'/', - 'b': u'\b', 'f': u'\f', 'n': u'\n', 'r': u'\r', 't': u'\t', -} - -DEFAULT_ENCODING = "utf-8" - -def py_scanstring(s, end, encoding=None, strict=True, _b=BACKSLASH, _m=STRINGCHUNK.match): - """Scan the string s for a JSON string. End is the index of the - character in s after the quote that started the JSON string. - Unescapes all valid JSON string escape sequences and raises ValueError - on attempt to decode an invalid string. If strict is False then literal - control characters are allowed in the string. - - Returns a tuple of the decoded string and the index of the character in s - after the end quote.""" - if encoding is None: - encoding = DEFAULT_ENCODING - chunks = [] - _append = chunks.append - begin = end - 1 - while 1: - chunk = _m(s, end) - if chunk is None: - raise ValueError( - errmsg("Unterminated string starting at", s, begin)) - end = chunk.end() - content, terminator = chunk.groups() - # Content is contains zero or more unescaped string characters - if content: - if not isinstance(content, unicode): - content = unicode(content, encoding) - _append(content) - # Terminator is the end of string, a literal control character, - # or a backslash denoting that an escape sequence follows - if terminator == '"': - break - elif terminator != '\\': - if strict: - msg = "Invalid control character %r at" % (terminator,) - #msg = "Invalid control character {0!r} at".format(terminator) - raise ValueError(errmsg(msg, s, end)) - else: - _append(terminator) - continue - try: - esc = s[end] - except IndexError: - raise ValueError( - errmsg("Unterminated string starting at", s, begin)) - # If not a unicode escape sequence, must be in the lookup table - if esc != 'u': - try: - char = _b[esc] - except KeyError: - msg = "Invalid \\escape: " + repr(esc) - raise ValueError(errmsg(msg, s, end)) - end += 1 - else: - # Unicode escape sequence - esc = s[end + 1:end + 5] - next_end = end + 5 - if len(esc) != 4: - msg = "Invalid \\uXXXX escape" - raise ValueError(errmsg(msg, s, end)) - uni = int(esc, 16) - # Check for surrogate pair on UCS-4 systems - if 0xd800 <= uni <= 0xdbff and sys.maxunicode > 65535: - msg = "Invalid \\uXXXX\\uXXXX surrogate pair" - if not s[end + 5:end + 7] == '\\u': - raise ValueError(errmsg(msg, s, end)) - esc2 = s[end + 7:end + 11] - if len(esc2) != 4: - raise ValueError(errmsg(msg, s, end)) - uni2 = int(esc2, 16) - uni = 0x10000 + (((uni - 0xd800) << 10) | (uni2 - 0xdc00)) - next_end += 6 - char = unichr(uni) - end = next_end - # Append the unescaped character - _append(char) - return u''.join(chunks), end - - -# Use speedup if available -scanstring = c_scanstring or py_scanstring - -WHITESPACE = re.compile(r'[ \t\n\r]*', FLAGS) -WHITESPACE_STR = ' \t\n\r' - -def JSONObject((s, end), encoding, strict, scan_once, object_hook, _w=WHITESPACE.match, _ws=WHITESPACE_STR): - pairs = {} - # Use a slice to prevent IndexError from being raised, the following - # check will raise a more specific ValueError if the string is empty - nextchar = s[end:end + 1] - # Normally we expect nextchar == '"' - if nextchar != '"': - if nextchar in _ws: - end = _w(s, end).end() - nextchar = s[end:end + 1] - # Trivial empty object - if nextchar == '}': - return pairs, end + 1 - elif nextchar != '"': - raise ValueError(errmsg("Expecting property name", s, end)) - end += 1 - while True: - key, end = scanstring(s, end, encoding, strict) - - # To skip some function call overhead we optimize the fast paths where - # the JSON key separator is ": " or just ":". - if s[end:end + 1] != ':': - end = _w(s, end).end() - if s[end:end + 1] != ':': - raise ValueError(errmsg("Expecting : delimiter", s, end)) - - end += 1 - - try: - if s[end] in _ws: - end += 1 - if s[end] in _ws: - end = _w(s, end + 1).end() - except IndexError: - pass - - try: - value, end = scan_once(s, end) - except StopIteration: - raise ValueError(errmsg("Expecting object", s, end)) - pairs[key] = value - - try: - nextchar = s[end] - if nextchar in _ws: - end = _w(s, end + 1).end() - nextchar = s[end] - except IndexError: - nextchar = '' - end += 1 - - if nextchar == '}': - break - elif nextchar != ',': - raise ValueError(errmsg("Expecting , delimiter", s, end - 1)) - - try: - nextchar = s[end] - if nextchar in _ws: - end += 1 - nextchar = s[end] - if nextchar in _ws: - end = _w(s, end + 1).end() - nextchar = s[end] - except IndexError: - nextchar = '' - - end += 1 - if nextchar != '"': - raise ValueError(errmsg("Expecting property name", s, end - 1)) - - if object_hook is not None: - pairs = object_hook(pairs) - return pairs, end - -def JSONArray((s, end), scan_once, _w=WHITESPACE.match, _ws=WHITESPACE_STR): - values = [] - nextchar = s[end:end + 1] - if nextchar in _ws: - end = _w(s, end + 1).end() - nextchar = s[end:end + 1] - # Look-ahead for trivial empty array - if nextchar == ']': - return values, end + 1 - _append = values.append - while True: - try: - value, end = scan_once(s, end) - except StopIteration: - raise ValueError(errmsg("Expecting object", s, end)) - _append(value) - nextchar = s[end:end + 1] - if nextchar in _ws: - end = _w(s, end + 1).end() - nextchar = s[end:end + 1] - end += 1 - if nextchar == ']': - break - elif nextchar != ',': - raise ValueError(errmsg("Expecting , delimiter", s, end)) - - try: - if s[end] in _ws: - end += 1 - if s[end] in _ws: - end = _w(s, end + 1).end() - except IndexError: - pass - - return values, end - -class JSONDecoder(object): - """Simple JSON <http://json.org> decoder - - Performs the following translations in decoding by default: - - +---------------+-------------------+ - | JSON | Python | - +===============+===================+ - | object | dict | - +---------------+-------------------+ - | array | list | - +---------------+-------------------+ - | string | unicode | - +---------------+-------------------+ - | number (int) | int, long | - +---------------+-------------------+ - | number (real) | float | - +---------------+-------------------+ - | true | True | - +---------------+-------------------+ - | false | False | - +---------------+-------------------+ - | null | None | - +---------------+-------------------+ - - It also understands ``NaN``, ``Infinity``, and ``-Infinity`` as - their corresponding ``float`` values, which is outside the JSON spec. - - """ - - def __init__(self, encoding=None, object_hook=None, parse_float=None, - parse_int=None, parse_constant=None, strict=True): - """``encoding`` determines the encoding used to interpret any ``str`` - objects decoded by this instance (utf-8 by default). It has no - effect when decoding ``unicode`` objects. - - Note that currently only encodings that are a superset of ASCII work, - strings of other encodings should be passed in as ``unicode``. - - ``object_hook``, if specified, will be called with the result - of every JSON object decoded and its return value will be used in - place of the given ``dict``. This can be used to provide custom - deserializations (e.g. to support JSON-RPC class hinting). - - ``parse_float``, if specified, will be called with the string - of every JSON float to be decoded. By default this is equivalent to - float(num_str). This can be used to use another datatype or parser - for JSON floats (e.g. decimal.Decimal). - - ``parse_int``, if specified, will be called with the string - of every JSON int to be decoded. By default this is equivalent to - int(num_str). This can be used to use another datatype or parser - for JSON integers (e.g. float). - - ``parse_constant``, if specified, will be called with one of the - following strings: -Infinity, Infinity, NaN. - This can be used to raise an exception if invalid JSON numbers - are encountered. - - """ - self.encoding = encoding - self.object_hook = object_hook - self.parse_float = parse_float or float - self.parse_int = parse_int or int - self.parse_constant = parse_constant or _CONSTANTS.__getitem__ - self.strict = strict - self.parse_object = JSONObject - self.parse_array = JSONArray - self.parse_string = scanstring - self.scan_once = make_scanner(self) - - def decode(self, s, _w=WHITESPACE.match): - """Return the Python representation of ``s`` (a ``str`` or ``unicode`` - instance containing a JSON document) - - """ - obj, end = self.raw_decode(s, idx=_w(s, 0).end()) - end = _w(s, end).end() - if end != len(s): - raise ValueError(errmsg("Extra data", s, end, len(s))) - return obj - - def raw_decode(self, s, idx=0): - """Decode a JSON document from ``s`` (a ``str`` or ``unicode`` beginning - with a JSON document) and return a 2-tuple of the Python - representation and the index in ``s`` where the document ended. - - This can be used to decode a JSON document from a string that may - have extraneous data at the end. - - """ - try: - obj, end = self.scan_once(s, idx) - except StopIteration: - raise ValueError("No JSON object could be decoded") - return obj, end diff --git a/lib/simplejson/encoder.py b/lib/simplejson/encoder.py deleted file mode 100644 index cf5829036..000000000 --- a/lib/simplejson/encoder.py +++ /dev/null @@ -1,440 +0,0 @@ -"""Implementation of JSONEncoder -""" -import re - -try: - from simplejson._speedups import encode_basestring_ascii as c_encode_basestring_ascii -except ImportError: - c_encode_basestring_ascii = None -try: - from simplejson._speedups import make_encoder as c_make_encoder -except ImportError: - c_make_encoder = None - -ESCAPE = re.compile(r'[\x00-\x1f\\"\b\f\n\r\t]') -ESCAPE_ASCII = re.compile(r'([\\"]|[^\ -~])') -HAS_UTF8 = re.compile(r'[\x80-\xff]') -ESCAPE_DCT = { - '\\': '\\\\', - '"': '\\"', - '\b': '\\b', - '\f': '\\f', - '\n': '\\n', - '\r': '\\r', - '\t': '\\t', -} -for i in range(0x20): - #ESCAPE_DCT.setdefault(chr(i), '\\u{0:04x}'.format(i)) - ESCAPE_DCT.setdefault(chr(i), '\\u%04x' % (i,)) - -# Assume this produces an infinity on all machines (probably not guaranteed) -INFINITY = float('1e66666') -FLOAT_REPR = repr - -def encode_basestring(s): - """Return a JSON representation of a Python string - - """ - def replace(match): - return ESCAPE_DCT[match.group(0)] - return '"' + ESCAPE.sub(replace, s) + '"' - - -def py_encode_basestring_ascii(s): - """Return an ASCII-only JSON representation of a Python string - - """ - if isinstance(s, str) and HAS_UTF8.search(s) is not None: - s = s.decode('utf-8') - def replace(match): - s = match.group(0) - try: - return ESCAPE_DCT[s] - except KeyError: - n = ord(s) - if n < 0x10000: - #return '\\u{0:04x}'.format(n) - return '\\u%04x' % (n,) - else: - # surrogate pair - n -= 0x10000 - s1 = 0xd800 | ((n >> 10) & 0x3ff) - s2 = 0xdc00 | (n & 0x3ff) - #return '\\u{0:04x}\\u{1:04x}'.format(s1, s2) - return '\\u%04x\\u%04x' % (s1, s2) - return '"' + str(ESCAPE_ASCII.sub(replace, s)) + '"' - - -encode_basestring_ascii = c_encode_basestring_ascii or py_encode_basestring_ascii - -class JSONEncoder(object): - """Extensible JSON <http://json.org> encoder for Python data structures. - - Supports the following objects and types by default: - - +-------------------+---------------+ - | Python | JSON | - +===================+===============+ - | dict | object | - +-------------------+---------------+ - | list, tuple | array | - +-------------------+---------------+ - | str, unicode | string | - +-------------------+---------------+ - | int, long, float | number | - +-------------------+---------------+ - | True | true | - +-------------------+---------------+ - | False | false | - +-------------------+---------------+ - | None | null | - +-------------------+---------------+ - - To extend this to recognize other objects, subclass and implement a - ``.default()`` method with another method that returns a serializable - object for ``o`` if possible, otherwise it should call the superclass - implementation (to raise ``TypeError``). - - """ - item_separator = ', ' - key_separator = ': ' - def __init__(self, skipkeys=False, ensure_ascii=True, - check_circular=True, allow_nan=True, sort_keys=False, - indent=None, separators=None, encoding='utf-8', default=None): - """Constructor for JSONEncoder, with sensible defaults. - - If skipkeys is false, then it is a TypeError to attempt - encoding of keys that are not str, int, long, float or None. If - skipkeys is True, such items are simply skipped. - - If ensure_ascii is true, the output is guaranteed to be str - objects with all incoming unicode characters escaped. If - ensure_ascii is false, the output will be unicode object. - - If check_circular is true, then lists, dicts, and custom encoded - objects will be checked for circular references during encoding to - prevent an infinite recursion (which would cause an OverflowError). - Otherwise, no such check takes place. - - If allow_nan is true, then NaN, Infinity, and -Infinity will be - encoded as such. This behavior is not JSON specification compliant, - but is consistent with most JavaScript based encoders and decoders. - Otherwise, it will be a ValueError to encode such floats. - - If sort_keys is true, then the output of dictionaries will be - sorted by key; this is useful for regression tests to ensure - that JSON serializations can be compared on a day-to-day basis. - - If indent is a non-negative integer, then JSON array - elements and object members will be pretty-printed with that - indent level. An indent level of 0 will only insert newlines. - None is the most compact representation. - - If specified, separators should be a (item_separator, key_separator) - tuple. The default is (', ', ': '). To get the most compact JSON - representation you should specify (',', ':') to eliminate whitespace. - - If specified, default is a function that gets called for objects - that can't otherwise be serialized. It should return a JSON encodable - version of the object or raise a ``TypeError``. - - If encoding is not None, then all input strings will be - transformed into unicode using that encoding prior to JSON-encoding. - The default is UTF-8. - - """ - - self.skipkeys = skipkeys - self.ensure_ascii = ensure_ascii - self.check_circular = check_circular - self.allow_nan = allow_nan - self.sort_keys = sort_keys - self.indent = indent - if separators is not None: - self.item_separator, self.key_separator = separators - if default is not None: - self.default = default - self.encoding = encoding - - def default(self, o): - """Implement this method in a subclass such that it returns - a serializable object for ``o``, or calls the base implementation - (to raise a ``TypeError``). - - For example, to support arbitrary iterators, you could - implement default like this:: - - def default(self, o): - try: - iterable = iter(o) - except TypeError: - pass - else: - return list(iterable) - return JSONEncoder.default(self, o) - - """ - raise TypeError(repr(o) + " is not JSON serializable") - - def encode(self, o): - """Return a JSON string representation of a Python data structure. - - >>> JSONEncoder().encode({"foo": ["bar", "baz"]}) - '{"foo": ["bar", "baz"]}' - - """ - # This is for extremely simple cases and benchmarks. - if isinstance(o, basestring): - if isinstance(o, str): - _encoding = self.encoding - if (_encoding is not None - and not (_encoding == 'utf-8')): - o = o.decode(_encoding) - if self.ensure_ascii: - return encode_basestring_ascii(o) - else: - return encode_basestring(o) - # This doesn't pass the iterator directly to ''.join() because the - # exceptions aren't as detailed. The list call should be roughly - # equivalent to the PySequence_Fast that ''.join() would do. - chunks = self.iterencode(o, _one_shot=True) - if not isinstance(chunks, (list, tuple)): - chunks = list(chunks) - return ''.join(chunks) - - def iterencode(self, o, _one_shot=False): - """Encode the given object and yield each string - representation as available. - - For example:: - - for chunk in JSONEncoder().iterencode(bigobject): - mysocket.write(chunk) - - """ - if self.check_circular: - markers = {} - else: - markers = None - if self.ensure_ascii: - _encoder = encode_basestring_ascii - else: - _encoder = encode_basestring - if self.encoding != 'utf-8': - def _encoder(o, _orig_encoder=_encoder, _encoding=self.encoding): - if isinstance(o, str): - o = o.decode(_encoding) - return _orig_encoder(o) - - def floatstr(o, allow_nan=self.allow_nan, _repr=FLOAT_REPR, _inf=INFINITY, _neginf=-INFINITY): - # Check for specials. Note that this type of test is processor- and/or - # platform-specific, so do tests which don't depend on the internals. - - if o != o: - text = 'NaN' - elif o == _inf: - text = 'Infinity' - elif o == _neginf: - text = '-Infinity' - else: - return _repr(o) - - if not allow_nan: - raise ValueError( - "Out of range float values are not JSON compliant: " + - repr(o)) - - return text - - - if _one_shot and c_make_encoder is not None and not self.indent and not self.sort_keys: - _iterencode = c_make_encoder( - markers, self.default, _encoder, self.indent, - self.key_separator, self.item_separator, self.sort_keys, - self.skipkeys, self.allow_nan) - else: - _iterencode = _make_iterencode( - markers, self.default, _encoder, self.indent, floatstr, - self.key_separator, self.item_separator, self.sort_keys, - self.skipkeys, _one_shot) - return _iterencode(o, 0) - -def _make_iterencode(markers, _default, _encoder, _indent, _floatstr, _key_separator, _item_separator, _sort_keys, _skipkeys, _one_shot, - ## HACK: hand-optimized bytecode; turn globals into locals - False=False, - True=True, - ValueError=ValueError, - basestring=basestring, - dict=dict, - float=float, - id=id, - int=int, - isinstance=isinstance, - list=list, - long=long, - str=str, - tuple=tuple, - ): - - def _iterencode_list(lst, _current_indent_level): - if not lst: - yield '[]' - return - if markers is not None: - markerid = id(lst) - if markerid in markers: - raise ValueError("Circular reference detected") - markers[markerid] = lst - buf = '[' - if _indent is not None: - _current_indent_level += 1 - newline_indent = '\n' + (' ' * (_indent * _current_indent_level)) - separator = _item_separator + newline_indent - buf += newline_indent - else: - newline_indent = None - separator = _item_separator - first = True - for value in lst: - if first: - first = False - else: - buf = separator - if isinstance(value, basestring): - yield buf + _encoder(value) - elif value is None: - yield buf + 'null' - elif value is True: - yield buf + 'true' - elif value is False: - yield buf + 'false' - elif isinstance(value, (int, long)): - yield buf + str(value) - elif isinstance(value, float): - yield buf + _floatstr(value) - else: - yield buf - if isinstance(value, (list, tuple)): - chunks = _iterencode_list(value, _current_indent_level) - elif isinstance(value, dict): - chunks = _iterencode_dict(value, _current_indent_level) - else: - chunks = _iterencode(value, _current_indent_level) - for chunk in chunks: - yield chunk - if newline_indent is not None: - _current_indent_level -= 1 - yield '\n' + (' ' * (_indent * _current_indent_level)) - yield ']' - if markers is not None: - del markers[markerid] - - def _iterencode_dict(dct, _current_indent_level): - if not dct: - yield '{}' - return - if markers is not None: - markerid = id(dct) - if markerid in markers: - raise ValueError("Circular reference detected") - markers[markerid] = dct - yield '{' - if _indent is not None: - _current_indent_level += 1 - newline_indent = '\n' + (' ' * (_indent * _current_indent_level)) - item_separator = _item_separator + newline_indent - yield newline_indent - else: - newline_indent = None - item_separator = _item_separator - first = True - if _sort_keys: - items = dct.items() - items.sort(key=lambda kv: kv[0]) - else: - items = dct.iteritems() - for key, value in items: - if isinstance(key, basestring): - pass - # JavaScript is weakly typed for these, so it makes sense to - # also allow them. Many encoders seem to do something like this. - elif isinstance(key, float): - key = _floatstr(key) - elif key is True: - key = 'true' - elif key is False: - key = 'false' - elif key is None: - key = 'null' - elif isinstance(key, (int, long)): - key = str(key) - elif _skipkeys: - continue - else: - raise TypeError("key " + repr(key) + " is not a string") - if first: - first = False - else: - yield item_separator - yield _encoder(key) - yield _key_separator - if isinstance(value, basestring): - yield _encoder(value) - elif value is None: - yield 'null' - elif value is True: - yield 'true' - elif value is False: - yield 'false' - elif isinstance(value, (int, long)): - yield str(value) - elif isinstance(value, float): - yield _floatstr(value) - else: - if isinstance(value, (list, tuple)): - chunks = _iterencode_list(value, _current_indent_level) - elif isinstance(value, dict): - chunks = _iterencode_dict(value, _current_indent_level) - else: - chunks = _iterencode(value, _current_indent_level) - for chunk in chunks: - yield chunk - if newline_indent is not None: - _current_indent_level -= 1 - yield '\n' + (' ' * (_indent * _current_indent_level)) - yield '}' - if markers is not None: - del markers[markerid] - - def _iterencode(o, _current_indent_level): - if isinstance(o, basestring): - yield _encoder(o) - elif o is None: - yield 'null' - elif o is True: - yield 'true' - elif o is False: - yield 'false' - elif isinstance(o, (int, long)): - yield str(o) - elif isinstance(o, float): - yield _floatstr(o) - elif isinstance(o, (list, tuple)): - for chunk in _iterencode_list(o, _current_indent_level): - yield chunk - elif isinstance(o, dict): - for chunk in _iterencode_dict(o, _current_indent_level): - yield chunk - else: - if markers is not None: - markerid = id(o) - if markerid in markers: - raise ValueError("Circular reference detected") - markers[markerid] = o - o = _default(o) - for chunk in _iterencode(o, _current_indent_level): - yield chunk - if markers is not None: - del markers[markerid] - - return _iterencode diff --git a/lib/simplejson/scanner.py b/lib/simplejson/scanner.py deleted file mode 100644 index adbc6ec97..000000000 --- a/lib/simplejson/scanner.py +++ /dev/null @@ -1,65 +0,0 @@ -"""JSON token scanner -""" -import re -try: - from simplejson._speedups import make_scanner as c_make_scanner -except ImportError: - c_make_scanner = None - -__all__ = ['make_scanner'] - -NUMBER_RE = re.compile( - r'(-?(?:0|[1-9]\d*))(\.\d+)?([eE][-+]?\d+)?', - (re.VERBOSE | re.MULTILINE | re.DOTALL)) - -def py_make_scanner(context): - parse_object = context.parse_object - parse_array = context.parse_array - parse_string = context.parse_string - match_number = NUMBER_RE.match - encoding = context.encoding - strict = context.strict - parse_float = context.parse_float - parse_int = context.parse_int - parse_constant = context.parse_constant - object_hook = context.object_hook - - def _scan_once(string, idx): - try: - nextchar = string[idx] - except IndexError: - raise StopIteration - - if nextchar == '"': - return parse_string(string, idx + 1, encoding, strict) - elif nextchar == '{': - return parse_object((string, idx + 1), encoding, strict, _scan_once, object_hook) - elif nextchar == '[': - return parse_array((string, idx + 1), _scan_once) - elif nextchar == 'n' and string[idx:idx + 4] == 'null': - return None, idx + 4 - elif nextchar == 't' and string[idx:idx + 4] == 'true': - return True, idx + 4 - elif nextchar == 'f' and string[idx:idx + 5] == 'false': - return False, idx + 5 - - m = match_number(string, idx) - if m is not None: - integer, frac, exp = m.groups() - if frac or exp: - res = parse_float(integer + (frac or '') + (exp or '')) - else: - res = parse_int(integer) - return res, m.end() - elif nextchar == 'N' and string[idx:idx + 3] == 'NaN': - return parse_constant('NaN'), idx + 3 - elif nextchar == 'I' and string[idx:idx + 8] == 'Infinity': - return parse_constant('Infinity'), idx + 8 - elif nextchar == '-' and string[idx:idx + 9] == '-Infinity': - return parse_constant('-Infinity'), idx + 9 - else: - raise StopIteration - - return _scan_once - -make_scanner = c_make_scanner or py_make_scanner diff --git a/lib/socks.py b/lib/socks.py new file mode 100644 index 000000000..0c91ea6a8 --- /dev/null +++ b/lib/socks.py @@ -0,0 +1,831 @@ +""" +SocksiPy - Python SOCKS module. + +Copyright 2006 Dan-Haim. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +3. Neither the name of Dan Haim nor the names of his contributors may be used + to endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY DAN HAIM "AS IS" AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +EVENT SHALL DAN HAIM OR HIS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMANGE. + + +This module provides a standard socket-like interface for Python +for tunneling connections through SOCKS proxies. + +=============================================================================== + +Minor modifications made by Christopher Gilbert (http://motomastyle.com/) +for use in PyLoris (http://pyloris.sourceforge.net/) + +Minor modifications made by Mario Vilas (http://breakingcode.wordpress.com/) +mainly to merge bug fixes found in Sourceforge + +Modifications made by Anorov (https://github.com/Anorov) +-Forked and renamed to PySocks +-Fixed issue with HTTP proxy failure checking (same bug that was in the old ___recvall() method) +-Included SocksiPyHandler (sockshandler.py), to be used as a urllib2 handler, + courtesy of e000 (https://github.com/e000): https://gist.github.com/869791#file_socksipyhandler.py +-Re-styled code to make it readable + -Aliased PROXY_TYPE_SOCKS5 -> SOCKS5 etc. + -Improved exception handling and output + -Removed irritating use of sequence indexes, replaced with tuple unpacked variables + -Fixed up Python 3 bytestring handling - chr(0x03).encode() -> b"\x03" + -Other general fixes +-Added clarification that the HTTP proxy connection method only supports CONNECT-style tunneling HTTP proxies +-Various small bug fixes +""" + +__version__ = "1.6.7" + +import socket +import struct +from errno import EOPNOTSUPP, EINVAL, EAGAIN +from io import BytesIO +from os import SEEK_CUR +import os +import sys +import functools +import logging +from collections import Callable +from base64 import b64encode + + +if os.name == "nt" and sys.version_info < (3, 0): + try: + import win_inet_pton + except ImportError: + raise ImportError("To run PySocks on Windows you must install win_inet_pton") + +log = logging.getLogger(__name__) + +PROXY_TYPE_SOCKS4 = SOCKS4 = 1 +PROXY_TYPE_SOCKS5 = SOCKS5 = 2 +PROXY_TYPE_HTTP = HTTP = 3 + +PROXY_TYPES = {"SOCKS4": SOCKS4, "SOCKS5": SOCKS5, "HTTP": HTTP} +PRINTABLE_PROXY_TYPES = dict(zip(PROXY_TYPES.values(), PROXY_TYPES.keys())) + +_orgsocket = _orig_socket = socket.socket + + +def set_self_blocking(function): + + @functools.wraps(function) + def wrapper(*args, **kwargs): + self = args[0] + try: + _is_blocking = self.gettimeout() + if _is_blocking == 0: + self.setblocking(True) + return function(*args, **kwargs) + except Exception as e: + raise + finally: + # set orgin blcoking + if _is_blocking == 0: + self.setblocking(False) + return wrapper + +class ProxyError(IOError): + """ + socket_err contains original socket.error exception. + """ + def __init__(self, msg, socket_err=None): + self.msg = msg + self.socket_err = socket_err + + if socket_err: + self.msg += ": {0}".format(socket_err) + + def __str__(self): + return self.msg + +class GeneralProxyError(ProxyError): pass +class ProxyConnectionError(ProxyError): pass +class SOCKS5AuthError(ProxyError): pass +class SOCKS5Error(ProxyError): pass +class SOCKS4Error(ProxyError): pass +class HTTPError(ProxyError): pass + +SOCKS4_ERRORS = { 0x5B: "Request rejected or failed", + 0x5C: "Request rejected because SOCKS server cannot connect to identd on the client", + 0x5D: "Request rejected because the client program and identd report different user-ids" + } + +SOCKS5_ERRORS = { 0x01: "General SOCKS server failure", + 0x02: "Connection not allowed by ruleset", + 0x03: "Network unreachable", + 0x04: "Host unreachable", + 0x05: "Connection refused", + 0x06: "TTL expired", + 0x07: "Command not supported, or protocol error", + 0x08: "Address type not supported" + } + +DEFAULT_PORTS = { SOCKS4: 1080, + SOCKS5: 1080, + HTTP: 8080 + } + +def set_default_proxy(proxy_type=None, addr=None, port=None, rdns=True, username=None, password=None): + """ + set_default_proxy(proxy_type, addr[, port[, rdns[, username, password]]]) + + Sets a default proxy which all further socksocket objects will use, + unless explicitly changed. All parameters are as for socket.set_proxy(). + """ + socksocket.default_proxy = (proxy_type, addr, port, rdns, + username.encode() if username else None, + password.encode() if password else None) + +def setdefaultproxy(*args, **kwargs): + if 'proxytype' in kwargs: + kwargs['proxy_type'] = kwargs.pop('proxytype') + return set_default_proxy(*args, **kwargs) + +def get_default_proxy(): + """ + Returns the default proxy, set by set_default_proxy. + """ + return socksocket.default_proxy + +getdefaultproxy = get_default_proxy + +def wrap_module(module): + """ + Attempts to replace a module's socket library with a SOCKS socket. Must set + a default proxy using set_default_proxy(...) first. + This will only work on modules that import socket directly into the namespace; + most of the Python Standard Library falls into this category. + """ + if socksocket.default_proxy: + module.socket.socket = socksocket + else: + raise GeneralProxyError("No default proxy specified") + +wrapmodule = wrap_module + +def create_connection(dest_pair, proxy_type=None, proxy_addr=None, + proxy_port=None, proxy_rdns=True, + proxy_username=None, proxy_password=None, + timeout=None, source_address=None, + socket_options=None): + """create_connection(dest_pair, *[, timeout], **proxy_args) -> socket object + + Like socket.create_connection(), but connects to proxy + before returning the socket object. + + dest_pair - 2-tuple of (IP/hostname, port). + **proxy_args - Same args passed to socksocket.set_proxy() if present. + timeout - Optional socket timeout value, in seconds. + source_address - tuple (host, port) for the socket to bind to as its source + address before connecting (only for compatibility) + """ + # Remove IPv6 brackets on the remote address and proxy address. + remote_host, remote_port = dest_pair + if remote_host.startswith('['): + remote_host = remote_host.strip('[]') + if proxy_addr and proxy_addr.startswith('['): + proxy_addr = proxy_addr.strip('[]') + + err = None + + # Allow the SOCKS proxy to be on IPv4 or IPv6 addresses. + for r in socket.getaddrinfo(proxy_addr, proxy_port, 0, socket.SOCK_STREAM): + family, socket_type, proto, canonname, sa = r + sock = None + try: + sock = socksocket(family, socket_type, proto) + + if socket_options: + for opt in socket_options: + sock.setsockopt(*opt) + + if isinstance(timeout, (int, float)): + sock.settimeout(timeout) + + if proxy_type: + sock.set_proxy(proxy_type, proxy_addr, proxy_port, proxy_rdns, + proxy_username, proxy_password) + if source_address: + sock.bind(source_address) + + sock.connect((remote_host, remote_port)) + return sock + + except (socket.error, ProxyConnectionError) as e: + err = e + if sock: + sock.close() + sock = None + + if err: + raise err + + raise socket.error("gai returned empty list.") + +class _BaseSocket(socket.socket): + """Allows Python 2's "delegated" methods such as send() to be overridden + """ + def __init__(self, *pos, **kw): + _orig_socket.__init__(self, *pos, **kw) + + self._savedmethods = dict() + for name in self._savenames: + self._savedmethods[name] = getattr(self, name) + delattr(self, name) # Allows normal overriding mechanism to work + + _savenames = list() + +def _makemethod(name): + return lambda self, *pos, **kw: self._savedmethods[name](*pos, **kw) +for name in ("sendto", "send", "recvfrom", "recv"): + method = getattr(_BaseSocket, name, None) + + # Determine if the method is not defined the usual way + # as a function in the class. + # Python 2 uses __slots__, so there are descriptors for each method, + # but they are not functions. + if not isinstance(method, Callable): + _BaseSocket._savenames.append(name) + setattr(_BaseSocket, name, _makemethod(name)) + +class socksocket(_BaseSocket): + """socksocket([family[, type[, proto]]]) -> socket object + + Open a SOCKS enabled socket. The parameters are the same as + those of the standard socket init. In order for SOCKS to work, + you must specify family=AF_INET and proto=0. + The "type" argument must be either SOCK_STREAM or SOCK_DGRAM. + """ + + default_proxy = None + + def __init__(self, family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0, *args, **kwargs): + if type not in (socket.SOCK_STREAM, socket.SOCK_DGRAM): + msg = "Socket type must be stream or datagram, not {!r}" + raise ValueError(msg.format(type)) + + super(socksocket, self).__init__(family, type, proto, *args, **kwargs) + self._proxyconn = None # TCP connection to keep UDP relay alive + + if self.default_proxy: + self.proxy = self.default_proxy + else: + self.proxy = (None, None, None, None, None, None) + self.proxy_sockname = None + self.proxy_peername = None + + self._timeout = None + + def _readall(self, file, count): + """ + Receive EXACTLY the number of bytes requested from the file object. + Blocks until the required number of bytes have been received. + """ + data = b"" + while len(data) < count: + d = file.read(count - len(data)) + if not d: + raise GeneralProxyError("Connection closed unexpectedly") + data += d + return data + + def settimeout(self, timeout): + self._timeout = timeout + try: + # test if we're connected, if so apply timeout + peer = self.get_proxy_peername() + super(socksocket, self).settimeout(self._timeout) + except socket.error: + pass + + def gettimeout(self): + return self._timeout + + def setblocking(self, v): + if v: + self.settimeout(None) + else: + self.settimeout(0.0) + + def set_proxy(self, proxy_type=None, addr=None, port=None, rdns=True, username=None, password=None): + """set_proxy(proxy_type, addr[, port[, rdns[, username[, password]]]]) + Sets the proxy to be used. + + proxy_type - The type of the proxy to be used. Three types + are supported: PROXY_TYPE_SOCKS4 (including socks4a), + PROXY_TYPE_SOCKS5 and PROXY_TYPE_HTTP + addr - The address of the server (IP or DNS). + port - The port of the server. Defaults to 1080 for SOCKS + servers and 8080 for HTTP proxy servers. + rdns - Should DNS queries be performed on the remote side + (rather than the local side). The default is True. + Note: This has no effect with SOCKS4 servers. + username - Username to authenticate with to the server. + The default is no authentication. + password - Password to authenticate with to the server. + Only relevant when username is also provided. + """ + self.proxy = (proxy_type, addr, port, rdns, + username.encode() if username else None, + password.encode() if password else None) + + def setproxy(self, *args, **kwargs): + if 'proxytype' in kwargs: + kwargs['proxy_type'] = kwargs.pop('proxytype') + return self.set_proxy(*args, **kwargs) + + def bind(self, *pos, **kw): + """ + Implements proxy connection for UDP sockets, + which happens during the bind() phase. + """ + proxy_type, proxy_addr, proxy_port, rdns, username, password = self.proxy + if not proxy_type or self.type != socket.SOCK_DGRAM: + return _orig_socket.bind(self, *pos, **kw) + + if self._proxyconn: + raise socket.error(EINVAL, "Socket already bound to an address") + if proxy_type != SOCKS5: + msg = "UDP only supported by SOCKS5 proxy type" + raise socket.error(EOPNOTSUPP, msg) + super(socksocket, self).bind(*pos, **kw) + + # Need to specify actual local port because + # some relays drop packets if a port of zero is specified. + # Avoid specifying host address in case of NAT though. + _, port = self.getsockname() + dst = ("0", port) + + self._proxyconn = _orig_socket() + proxy = self._proxy_addr() + self._proxyconn.connect(proxy) + + UDP_ASSOCIATE = b"\x03" + _, relay = self._SOCKS5_request(self._proxyconn, UDP_ASSOCIATE, dst) + + # The relay is most likely on the same host as the SOCKS proxy, + # but some proxies return a private IP address (10.x.y.z) + host, _ = proxy + _, port = relay + super(socksocket, self).connect((host, port)) + super(socksocket, self).settimeout(self._timeout) + self.proxy_sockname = ("0.0.0.0", 0) # Unknown + + def sendto(self, bytes, *args, **kwargs): + if self.type != socket.SOCK_DGRAM: + return super(socksocket, self).sendto(bytes, *args, **kwargs) + if not self._proxyconn: + self.bind(("", 0)) + + address = args[-1] + flags = args[:-1] + + header = BytesIO() + RSV = b"\x00\x00" + header.write(RSV) + STANDALONE = b"\x00" + header.write(STANDALONE) + self._write_SOCKS5_address(address, header) + + sent = super(socksocket, self).send(header.getvalue() + bytes, *flags, **kwargs) + return sent - header.tell() + + def send(self, bytes, flags=0, **kwargs): + if self.type == socket.SOCK_DGRAM: + return self.sendto(bytes, flags, self.proxy_peername, **kwargs) + else: + return super(socksocket, self).send(bytes, flags, **kwargs) + + def recvfrom(self, bufsize, flags=0): + if self.type != socket.SOCK_DGRAM: + return super(socksocket, self).recvfrom(bufsize, flags) + if not self._proxyconn: + self.bind(("", 0)) + + buf = BytesIO(super(socksocket, self).recv(bufsize + 1024, flags)) + buf.seek(2, SEEK_CUR) + frag = buf.read(1) + if ord(frag): + raise NotImplementedError("Received UDP packet fragment") + fromhost, fromport = self._read_SOCKS5_address(buf) + + if self.proxy_peername: + peerhost, peerport = self.proxy_peername + if fromhost != peerhost or peerport not in (0, fromport): + raise socket.error(EAGAIN, "Packet filtered") + + return (buf.read(bufsize), (fromhost, fromport)) + + def recv(self, *pos, **kw): + bytes, _ = self.recvfrom(*pos, **kw) + return bytes + + def close(self): + if self._proxyconn: + self._proxyconn.close() + return super(socksocket, self).close() + + def get_proxy_sockname(self): + """ + Returns the bound IP address and port number at the proxy. + """ + return self.proxy_sockname + + getproxysockname = get_proxy_sockname + + def get_proxy_peername(self): + """ + Returns the IP and port number of the proxy. + """ + return super(socksocket, self).getpeername() + + getproxypeername = get_proxy_peername + + def get_peername(self): + """ + Returns the IP address and port number of the destination + machine (note: get_proxy_peername returns the proxy) + """ + return self.proxy_peername + + getpeername = get_peername + + def _negotiate_SOCKS5(self, *dest_addr): + """ + Negotiates a stream connection through a SOCKS5 server. + """ + CONNECT = b"\x01" + self.proxy_peername, self.proxy_sockname = self._SOCKS5_request(self, + CONNECT, dest_addr) + + def _SOCKS5_request(self, conn, cmd, dst): + """ + Send SOCKS5 request with given command (CMD field) and + address (DST field). Returns resolved DST address that was used. + """ + proxy_type, addr, port, rdns, username, password = self.proxy + + writer = conn.makefile("wb") + reader = conn.makefile("rb", 0) # buffering=0 renamed in Python 3 + try: + # First we'll send the authentication packages we support. + if username and password: + # The username/password details were supplied to the + # set_proxy method so we support the USERNAME/PASSWORD + # authentication (in addition to the standard none). + writer.write(b"\x05\x02\x00\x02") + else: + # No username/password were entered, therefore we + # only support connections with no authentication. + writer.write(b"\x05\x01\x00") + + # We'll receive the server's response to determine which + # method was selected + writer.flush() + chosen_auth = self._readall(reader, 2) + + if chosen_auth[0:1] != b"\x05": + # Note: string[i:i+1] is used because indexing of a bytestring + # via bytestring[i] yields an integer in Python 3 + raise GeneralProxyError("SOCKS5 proxy server sent invalid data") + + # Check the chosen authentication method + + if chosen_auth[1:2] == b"\x02": + # Okay, we need to perform a basic username/password + # authentication. + writer.write(b"\x01" + chr(len(username)).encode() + + username + + chr(len(password)).encode() + + password) + writer.flush() + auth_status = self._readall(reader, 2) + if auth_status[0:1] != b"\x01": + # Bad response + raise GeneralProxyError("SOCKS5 proxy server sent invalid data") + if auth_status[1:2] != b"\x00": + # Authentication failed + raise SOCKS5AuthError("SOCKS5 authentication failed") + + # Otherwise, authentication succeeded + + # No authentication is required if 0x00 + elif chosen_auth[1:2] != b"\x00": + # Reaching here is always bad + if chosen_auth[1:2] == b"\xFF": + raise SOCKS5AuthError("All offered SOCKS5 authentication methods were rejected") + else: + raise GeneralProxyError("SOCKS5 proxy server sent invalid data") + + # Now we can request the actual connection + writer.write(b"\x05" + cmd + b"\x00") + resolved = self._write_SOCKS5_address(dst, writer) + writer.flush() + + # Get the response + resp = self._readall(reader, 3) + if resp[0:1] != b"\x05": + raise GeneralProxyError("SOCKS5 proxy server sent invalid data") + + status = ord(resp[1:2]) + if status != 0x00: + # Connection failed: server returned an error + error = SOCKS5_ERRORS.get(status, "Unknown error") + raise SOCKS5Error("{0:#04x}: {1}".format(status, error)) + + # Get the bound address/port + bnd = self._read_SOCKS5_address(reader) + + super(socksocket, self).settimeout(self._timeout) + return (resolved, bnd) + finally: + reader.close() + writer.close() + + def _write_SOCKS5_address(self, addr, file): + """ + Return the host and port packed for the SOCKS5 protocol, + and the resolved address as a tuple object. + """ + host, port = addr + proxy_type, _, _, rdns, username, password = self.proxy + family_to_byte = {socket.AF_INET: b"\x01", socket.AF_INET6: b"\x04"} + + # If the given destination address is an IP address, we'll + # use the IP address request even if remote resolving was specified. + # Detect whether the address is IPv4/6 directly. + for family in (socket.AF_INET, socket.AF_INET6): + try: + addr_bytes = socket.inet_pton(family, host) + file.write(family_to_byte[family] + addr_bytes) + host = socket.inet_ntop(family, addr_bytes) + file.write(struct.pack(">H", port)) + return host, port + except socket.error: + continue + + # Well it's not an IP number, so it's probably a DNS name. + if rdns: + # Resolve remotely + host_bytes = host.encode('idna') + file.write(b"\x03" + chr(len(host_bytes)).encode() + host_bytes) + else: + # Resolve locally + addresses = socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM, socket.IPPROTO_TCP, socket.AI_ADDRCONFIG) + # We can't really work out what IP is reachable, so just pick the + # first. + target_addr = addresses[0] + family = target_addr[0] + host = target_addr[4][0] + + addr_bytes = socket.inet_pton(family, host) + file.write(family_to_byte[family] + addr_bytes) + host = socket.inet_ntop(family, addr_bytes) + file.write(struct.pack(">H", port)) + return host, port + + def _read_SOCKS5_address(self, file): + atyp = self._readall(file, 1) + if atyp == b"\x01": + addr = socket.inet_ntoa(self._readall(file, 4)) + elif atyp == b"\x03": + length = self._readall(file, 1) + addr = self._readall(file, ord(length)) + elif atyp == b"\x04": + addr = socket.inet_ntop(socket.AF_INET6, self._readall(file, 16)) + else: + raise GeneralProxyError("SOCKS5 proxy server sent invalid data") + + port = struct.unpack(">H", self._readall(file, 2))[0] + return addr, port + + def _negotiate_SOCKS4(self, dest_addr, dest_port): + """ + Negotiates a connection through a SOCKS4 server. + """ + proxy_type, addr, port, rdns, username, password = self.proxy + + writer = self.makefile("wb") + reader = self.makefile("rb", 0) # buffering=0 renamed in Python 3 + try: + # Check if the destination address provided is an IP address + remote_resolve = False + try: + addr_bytes = socket.inet_aton(dest_addr) + except socket.error: + # It's a DNS name. Check where it should be resolved. + if rdns: + addr_bytes = b"\x00\x00\x00\x01" + remote_resolve = True + else: + addr_bytes = socket.inet_aton(socket.gethostbyname(dest_addr)) + + # Construct the request packet + writer.write(struct.pack(">BBH", 0x04, 0x01, dest_port)) + writer.write(addr_bytes) + + # The username parameter is considered userid for SOCKS4 + if username: + writer.write(username) + writer.write(b"\x00") + + # DNS name if remote resolving is required + # NOTE: This is actually an extension to the SOCKS4 protocol + # called SOCKS4A and may not be supported in all cases. + if remote_resolve: + writer.write(dest_addr.encode('idna') + b"\x00") + writer.flush() + + # Get the response from the server + resp = self._readall(reader, 8) + if resp[0:1] != b"\x00": + # Bad data + raise GeneralProxyError("SOCKS4 proxy server sent invalid data") + + status = ord(resp[1:2]) + if status != 0x5A: + # Connection failed: server returned an error + error = SOCKS4_ERRORS.get(status, "Unknown error") + raise SOCKS4Error("{0:#04x}: {1}".format(status, error)) + + # Get the bound address/port + self.proxy_sockname = (socket.inet_ntoa(resp[4:]), struct.unpack(">H", resp[2:4])[0]) + if remote_resolve: + self.proxy_peername = socket.inet_ntoa(addr_bytes), dest_port + else: + self.proxy_peername = dest_addr, dest_port + finally: + reader.close() + writer.close() + + def _negotiate_HTTP(self, dest_addr, dest_port): + """ + Negotiates a connection through an HTTP server. + NOTE: This currently only supports HTTP CONNECT-style proxies. + """ + proxy_type, addr, port, rdns, username, password = self.proxy + + # If we need to resolve locally, we do this now + addr = dest_addr if rdns else socket.gethostbyname(dest_addr) + + http_headers = [ + b"CONNECT " + addr.encode('idna') + b":" + str(dest_port).encode() + b" HTTP/1.1", + b"Host: " + dest_addr.encode('idna') + ] + + if username and password: + http_headers.append(b"Proxy-Authorization: basic " + b64encode(username + b":" + password)) + + http_headers.append(b"\r\n") + + self.sendall(b"\r\n".join(http_headers)) + + # We just need the first line to check if the connection was successful + fobj = self.makefile() + status_line = fobj.readline() + fobj.close() + + if not status_line: + raise GeneralProxyError("Connection closed unexpectedly") + + try: + proto, status_code, status_msg = status_line.split(" ", 2) + except ValueError: + raise GeneralProxyError("HTTP proxy server sent invalid response") + + if not proto.startswith("HTTP/"): + raise GeneralProxyError("Proxy server does not appear to be an HTTP proxy") + + try: + status_code = int(status_code) + except ValueError: + raise HTTPError("HTTP proxy server did not return a valid HTTP status") + + if status_code != 200: + error = "{0}: {1}".format(status_code, status_msg) + if status_code in (400, 403, 405): + # It's likely that the HTTP proxy server does not support the CONNECT tunneling method + error += ("\n[*] Note: The HTTP proxy server may not be supported by PySocks" + " (must be a CONNECT tunnel proxy)") + raise HTTPError(error) + + self.proxy_sockname = (b"0.0.0.0", 0) + self.proxy_peername = addr, dest_port + + _proxy_negotiators = { + SOCKS4: _negotiate_SOCKS4, + SOCKS5: _negotiate_SOCKS5, + HTTP: _negotiate_HTTP + } + + @set_self_blocking + def connect(self, dest_pair): + """ + Connects to the specified destination through a proxy. + Uses the same API as socket's connect(). + To select the proxy server, use set_proxy(). + + dest_pair - 2-tuple of (IP/hostname, port). + """ + if len(dest_pair) != 2 or dest_pair[0].startswith("["): + # Probably IPv6, not supported -- raise an error, and hope + # Happy Eyeballs (RFC6555) makes sure at least the IPv4 + # connection works... + raise socket.error("PySocks doesn't support IPv6: %s" % str(dest_pair)) + + dest_addr, dest_port = dest_pair + + if self.type == socket.SOCK_DGRAM: + if not self._proxyconn: + self.bind(("", 0)) + dest_addr = socket.gethostbyname(dest_addr) + + # If the host address is INADDR_ANY or similar, reset the peer + # address so that packets are received from any peer + if dest_addr == "0.0.0.0" and not dest_port: + self.proxy_peername = None + else: + self.proxy_peername = (dest_addr, dest_port) + return + + proxy_type, proxy_addr, proxy_port, rdns, username, password = self.proxy + + # Do a minimal input check first + if (not isinstance(dest_pair, (list, tuple)) + or len(dest_pair) != 2 + or not dest_addr + or not isinstance(dest_port, int)): + raise GeneralProxyError("Invalid destination-connection (host, port) pair") + + + # We set the timeout here so that we don't hang in connection or during + # negotiation. + super(socksocket, self).settimeout(self._timeout) + + if proxy_type is None: + # Treat like regular socket object + self.proxy_peername = dest_pair + super(socksocket, self).settimeout(self._timeout) + super(socksocket, self).connect((dest_addr, dest_port)) + return + + proxy_addr = self._proxy_addr() + + try: + # Initial connection to proxy server. + super(socksocket, self).connect(proxy_addr) + + except socket.error as error: + # Error while connecting to proxy + self.close() + proxy_addr, proxy_port = proxy_addr + proxy_server = "{0}:{1}".format(proxy_addr, proxy_port) + printable_type = PRINTABLE_PROXY_TYPES[proxy_type] + + msg = "Error connecting to {0} proxy {1}".format(printable_type, + proxy_server) + log.debug("%s due to: %s", msg, error) + raise ProxyConnectionError(msg, error) + + else: + # Connected to proxy server, now negotiate + try: + # Calls negotiate_{SOCKS4, SOCKS5, HTTP} + negotiate = self._proxy_negotiators[proxy_type] + negotiate(self, dest_addr, dest_port) + except socket.error as error: + # Wrap socket errors + self.close() + raise GeneralProxyError("Socket error", error) + except ProxyError: + # Protocol error while negotiating with proxy + self.close() + raise + + def _proxy_addr(self): + """ + Return proxy address to connect to as tuple object + """ + proxy_type, proxy_addr, proxy_port, rdns, username, password = self.proxy + proxy_port = proxy_port or DEFAULT_PORTS.get(proxy_type) + if not proxy_port: + raise GeneralProxyError("Invalid proxy type") + return proxy_addr, proxy_port diff --git a/lib/socks/__init__.py b/lib/socks/__init__.py deleted file mode 100644 index 628d37c50..000000000 --- a/lib/socks/__init__.py +++ /dev/null @@ -1,393 +0,0 @@ -"""SocksiPy - Python SOCKS module. -Version 1.00 - -Copyright 2006 Dan-Haim. All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. -3. Neither the name of Dan Haim nor the names of his contributors may be used - to endorse or promote products derived from this software without specific - prior written permission. - -THIS SOFTWARE IS PROVIDED BY DAN HAIM "AS IS" AND ANY EXPRESS OR IMPLIED -WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO -EVENT SHALL DAN HAIM OR HIS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA -OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT -OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMANGE. - - -This module provides a standard socket-like interface for Python -for tunneling connections through SOCKS proxies. - -""" - -""" - -Minor modifications made by Christopher Gilbert (http://motomastyle.com/) -for use in PyLoris (http://pyloris.sourceforge.net/) - -Minor modifications made by Mario Vilas (http://breakingcode.wordpress.com/) -mainly to merge bug fixes found in Sourceforge - -""" - -import re -import socket -import struct -import sys - -PROXY_TYPE_SOCKS4 = 1 -PROXY_TYPE_SOCKS5 = 2 -PROXY_TYPE_HTTP = 3 - -PROXY_REGEX = re.compile(r"^(([^:/?#]+):)?(//([^/?#]*):([^/?#]*))?") - -_defaultproxy = None -_orgsocket = socket.socket - -class ProxyError(Exception): pass -class GeneralProxyError(ProxyError): pass -class Socks5AuthError(ProxyError): pass -class Socks5Error(ProxyError): pass -class Socks4Error(ProxyError): pass -class HTTPError(ProxyError): pass - -_generalerrors = ("success", - "invalid data", - "not connected", - "not available", - "bad proxy type", - "bad input") - -_socks5errors = ("succeeded", - "general SOCKS server failure", - "connection not allowed by ruleset", - "Network unreachable", - "Host unreachable", - "Connection refused", - "TTL expired", - "Command not supported", - "Address type not supported", - "Unknown error") - -_socks5autherrors = ("succeeded", - "authentication is required", - "all offered authentication methods were rejected", - "unknown username or invalid password", - "unknown error") - -_socks4errors = ("request granted", - "request rejected or failed", - "request rejected because SOCKS server cannot connect to identd on the client", - "request rejected because the client program and identd report different user-ids", - "unknown error") - -def parseproxyuri(proxyurl): - """Parses a http proxy uri in the format x://a.b.c.d:port - - (protocol, addr, port) = parseproxyuri(uri) - """ - groups = PROXY_REGEX.match(proxyurl).groups() - return (groups[1], groups[3], groups[4]) - -def setdefaultproxy(proxytype=None, addr=None, port=None, rdns=True, username=None, password=None): - """setdefaultproxy(proxytype, addr[, port[, rdns[, username[, password]]]]) - Sets a default proxy which all further socksocket objects will use, - unless explicitly changed. - """ - global _defaultproxy - _defaultproxy = (proxytype, addr, port, rdns, username, password) - -def wrapmodule(module): - """wrapmodule(module) - Attempts to replace a module's socket library with a SOCKS socket. Must set - a default proxy using setdefaultproxy(...) first. - This will only work on modules that import socket directly into the namespace; - most of the Python Standard Library falls into this category. - """ - if _defaultproxy != None: - module.socket.socket = socksocket - else: - raise GeneralProxyError((4, "no proxy specified")) - -class socksocket(socket.socket): - """socksocket([family[, type[, proto]]]) -> socket object - Open a SOCKS enabled socket. The parameters are the same as - those of the standard socket init. In order for SOCKS to work, - you must specify family=AF_INET, type=SOCK_STREAM and proto=0. - """ - - def __init__(self, family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0, _sock=None): - _orgsocket.__init__(self, family, type, proto, _sock) - if _defaultproxy != None: - self.__proxy = _defaultproxy - else: - self.__proxy = (None, None, None, None, None, None) - self.__proxysockname = None - self.__proxypeername = None - - def __recvall(self, count): - """__recvall(count) -> data - Receive EXACTLY the number of bytes requested from the socket. - Blocks until the required number of bytes have been received. - """ - data = self.recv(count) - while len(data) < count: - d = self.recv(count-len(data)) - if not d: raise GeneralProxyError((0, "connection closed unexpectedly")) - data = data + d - return data - - def setproxy(self, proxytype=None, addr=None, port=None, rdns=True, username=None, password=None): - """setproxy(proxytype, addr[, port[, rdns[, username[, password]]]]) - Sets the proxy to be used. - proxytype - The type of the proxy to be used. Three types - are supported: PROXY_TYPE_SOCKS4 (including socks4a), - PROXY_TYPE_SOCKS5 and PROXY_TYPE_HTTP - addr - The address of the server (IP or DNS). - port - The port of the server. Defaults to 1080 for SOCKS - servers and 8080 for HTTP proxy servers. - rdns - Should DNS queries be preformed on the remote side - (rather than the local side). The default is True. - Note: This has no effect with SOCKS4 servers. - username - Username to authenticate with to the server. - The default is no authentication. - password - Password to authenticate with to the server. - Only relevant when username is also provided. - """ - self.__proxy = (proxytype, addr, port, rdns, username, password) - - def __negotiatesocks5(self, destaddr, destport): - """__negotiatesocks5(self,destaddr,destport) - Negotiates a connection through a SOCKS5 server. - """ - # First we'll send the authentication packages we support. - if (self.__proxy[4]!=None) and (self.__proxy[5]!=None): - # The username/password details were supplied to the - # setproxy method so we support the USERNAME/PASSWORD - # authentication (in addition to the standard none). - self.sendall(struct.pack('BBBB', 0x05, 0x02, 0x00, 0x02)) - else: - # No username/password were entered, therefore we - # only support connections with no authentication. - self.sendall(struct.pack('BBB', 0x05, 0x01, 0x00)) - # We'll receive the server's response to determine which - # method was selected - chosenauth = self.__recvall(2) - if chosenauth[0:1] != chr(0x05).encode(): - self.close() - raise GeneralProxyError((1, _generalerrors[1])) - # Check the chosen authentication method - if chosenauth[1:2] == chr(0x00).encode(): - # No authentication is required - pass - elif chosenauth[1:2] == chr(0x02).encode(): - # Okay, we need to perform a basic username/password - # authentication. - self.sendall(chr(0x01).encode() + chr(len(self.__proxy[4])) + self.__proxy[4] + chr(len(self.__proxy[5])) + self.__proxy[5]) - authstat = self.__recvall(2) - if authstat[0:1] != chr(0x01).encode(): - # Bad response - self.close() - raise GeneralProxyError((1, _generalerrors[1])) - if authstat[1:2] != chr(0x00).encode(): - # Authentication failed - self.close() - raise Socks5AuthError((3, _socks5autherrors[3])) - # Authentication succeeded - else: - # Reaching here is always bad - self.close() - if chosenauth[1] == chr(0xFF).encode(): - raise Socks5AuthError((2, _socks5autherrors[2])) - else: - raise GeneralProxyError((1, _generalerrors[1])) - # Now we can request the actual connection - req = struct.pack('BBB', 0x05, 0x01, 0x00) - # If the given destination address is an IP address, we'll - # use the IPv4 address request even if remote resolving was specified. - try: - ipaddr = socket.inet_aton(destaddr) - req = req + chr(0x01).encode() + ipaddr - except socket.error: - # Well it's not an IP number, so it's probably a DNS name. - if self.__proxy[3]: - # Resolve remotely - ipaddr = None - req = req + chr(0x03).encode() + chr(len(destaddr)).encode() + destaddr - else: - # Resolve locally - ipaddr = socket.inet_aton(socket.gethostbyname(destaddr)) - req = req + chr(0x01).encode() + ipaddr - req = req + struct.pack(">H", destport) - self.sendall(req) - # Get the response - resp = self.__recvall(4) - if resp[0:1] != chr(0x05).encode(): - self.close() - raise GeneralProxyError((1, _generalerrors[1])) - elif resp[1:2] != chr(0x00).encode(): - # Connection failed - self.close() - if ord(resp[1:2])<=8: - raise Socks5Error((ord(resp[1:2]), _socks5errors[ord(resp[1:2])])) - else: - raise Socks5Error((9, _socks5errors[9])) - # Get the bound address/port - elif resp[3:4] == chr(0x01).encode(): - boundaddr = self.__recvall(4) - elif resp[3:4] == chr(0x03).encode(): - resp = resp + self.recv(1) - boundaddr = self.__recvall(ord(resp[4:5])) - else: - self.close() - raise GeneralProxyError((1,_generalerrors[1])) - boundport = struct.unpack(">H", self.__recvall(2))[0] - self.__proxysockname = (boundaddr, boundport) - if ipaddr != None: - self.__proxypeername = (socket.inet_ntoa(ipaddr), destport) - else: - self.__proxypeername = (destaddr, destport) - - def getproxysockname(self): - """getsockname() -> address info - Returns the bound IP address and port number at the proxy. - """ - return self.__proxysockname - - def getproxypeername(self): - """getproxypeername() -> address info - Returns the IP and port number of the proxy. - """ - return _orgsocket.getpeername(self) - - def getpeername(self): - """getpeername() -> address info - Returns the IP address and port number of the destination - machine (note: getproxypeername returns the proxy) - """ - return self.__proxypeername - - def __negotiatesocks4(self,destaddr,destport): - """__negotiatesocks4(self,destaddr,destport) - Negotiates a connection through a SOCKS4 server. - """ - # Check if the destination address provided is an IP address - rmtrslv = False - try: - ipaddr = socket.inet_aton(destaddr) - except socket.error: - # It's a DNS name. Check where it should be resolved. - if self.__proxy[3]: - ipaddr = struct.pack("BBBB", 0x00, 0x00, 0x00, 0x01) - rmtrslv = True - else: - ipaddr = socket.inet_aton(socket.gethostbyname(destaddr)) - # Construct the request packet - req = struct.pack(">BBH", 0x04, 0x01, destport) + ipaddr - # The username parameter is considered userid for SOCKS4 - if self.__proxy[4] != None: - req = req + self.__proxy[4] - req = req + chr(0x00).encode() - # DNS name if remote resolving is required - # NOTE: This is actually an extension to the SOCKS4 protocol - # called SOCKS4A and may not be supported in all cases. - if rmtrslv: - req = req + destaddr + chr(0x00).encode() - self.sendall(req) - # Get the response from the server - resp = self.__recvall(8) - if resp[0:1] != chr(0x00).encode(): - # Bad data - self.close() - raise GeneralProxyError((1,_generalerrors[1])) - if resp[1:2] != chr(0x5A).encode(): - # Server returned an error - self.close() - if ord(resp[1:2]) in (91, 92, 93): - self.close() - raise Socks4Error((ord(resp[1:2]), _socks4errors[ord(resp[1:2]) - 90])) - else: - raise Socks4Error((94, _socks4errors[4])) - # Get the bound address/port - self.__proxysockname = (socket.inet_ntoa(resp[4:]), struct.unpack(">H", resp[2:4])[0]) - if rmtrslv != None: - self.__proxypeername = (socket.inet_ntoa(ipaddr), destport) - else: - self.__proxypeername = (destaddr, destport) - - def __negotiatehttp(self, destaddr, destport): - """__negotiatehttp(self,destaddr,destport) - Negotiates a connection through an HTTP server. - """ - # If we need to resolve locally, we do this now - if not self.__proxy[3]: - addr = socket.gethostbyname(destaddr) - else: - addr = destaddr - self.sendall(("CONNECT " + addr + ":" + str(destport) + " HTTP/1.1\r\n" + "Host: " + destaddr + "\r\n\r\n").encode()) - # We read the response until we get the string "\r\n\r\n" - resp = self.recv(1) - while resp.find("\r\n\r\n".encode()) == -1: - resp = resp + self.recv(1) - # We just need the first line to check if the connection - # was successful - statusline = resp.splitlines()[0].split(" ".encode(), 2) - if statusline[0] not in ("HTTP/1.0".encode(), "HTTP/1.1".encode()): - self.close() - raise GeneralProxyError((1, _generalerrors[1])) - try: - statuscode = int(statusline[1]) - except ValueError: - self.close() - raise GeneralProxyError((1, _generalerrors[1])) - if statuscode != 200: - self.close() - raise HTTPError((statuscode, statusline[2])) - self.__proxysockname = ("0.0.0.0", 0) - self.__proxypeername = (addr, destport) - - def connect(self, destpair): - """connect(self, despair) - Connects to the specified destination through a proxy. - destpar - A tuple of the IP/DNS address and the port number. - (identical to socket's connect). - To select the proxy server use setproxy(). - """ - # Do a minimal input check first - if (not type(destpair) in (list,tuple)) or (len(destpair) < 2) or (type(destpair[0]) != type('')) or (type(destpair[1]) != int): - raise GeneralProxyError((5, _generalerrors[5])) - if self.__proxy[0] == PROXY_TYPE_SOCKS5: - if self.__proxy[2] != None: - portnum = self.__proxy[2] - else: - portnum = 1080 - _orgsocket.connect(self, (self.__proxy[1], portnum)) - self.__negotiatesocks5(destpair[0], destpair[1]) - elif self.__proxy[0] == PROXY_TYPE_SOCKS4: - if self.__proxy[2] != None: - portnum = self.__proxy[2] - else: - portnum = 1080 - _orgsocket.connect(self,(self.__proxy[1], portnum)) - self.__negotiatesocks4(destpair[0], destpair[1]) - elif self.__proxy[0] == PROXY_TYPE_HTTP: - if self.__proxy[2] != None: - portnum = self.__proxy[2] - else: - portnum = 8080 - _orgsocket.connect(self,(self.__proxy[1], portnum)) - self.__negotiatehttp(destpair[0], destpair[1]) - elif self.__proxy[0] == None: - _orgsocket.connect(self, (destpair[0], destpair[1])) - else: - raise GeneralProxyError((4, _generalerrors[4])) diff --git a/lib/sockshandler.py b/lib/sockshandler.py new file mode 100644 index 000000000..26c83439c --- /dev/null +++ b/lib/sockshandler.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python +""" +SocksiPy + urllib2 handler + +version: 0.3 +author: e<e@tr0ll.in> + +This module provides a Handler which you can use with urllib2 to allow it to tunnel your connection through a socks.sockssocket socket, with out monkey patching the original socket... +""" +import ssl + +try: + import urllib2 + import httplib +except ImportError: # Python 3 + import urllib.request as urllib2 + import http.client as httplib + +import socks # $ pip install PySocks + +def merge_dict(a, b): + d = a.copy() + d.update(b) + return d + +class SocksiPyConnection(httplib.HTTPConnection): + def __init__(self, proxytype, proxyaddr, proxyport=None, rdns=True, username=None, password=None, *args, **kwargs): + self.proxyargs = (proxytype, proxyaddr, proxyport, rdns, username, password) + httplib.HTTPConnection.__init__(self, *args, **kwargs) + + def connect(self): + self.sock = socks.socksocket() + self.sock.setproxy(*self.proxyargs) + if type(self.timeout) in (int, float): + self.sock.settimeout(self.timeout) + self.sock.connect((self.host, self.port)) + +class SocksiPyConnectionS(httplib.HTTPSConnection): + def __init__(self, proxytype, proxyaddr, proxyport=None, rdns=True, username=None, password=None, *args, **kwargs): + self.proxyargs = (proxytype, proxyaddr, proxyport, rdns, username, password) + httplib.HTTPSConnection.__init__(self, *args, **kwargs) + + def connect(self): + sock = socks.socksocket() + sock.setproxy(*self.proxyargs) + if type(self.timeout) in (int, float): + sock.settimeout(self.timeout) + sock.connect((self.host, self.port)) + self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file) + +class SocksiPyHandler(urllib2.HTTPHandler, urllib2.HTTPSHandler): + def __init__(self, *args, **kwargs): + self.args = args + self.kw = kwargs + urllib2.HTTPHandler.__init__(self) + + def http_open(self, req): + def build(host, port=None, timeout=0, **kwargs): + kw = merge_dict(self.kw, kwargs) + conn = SocksiPyConnection(*self.args, host=host, port=port, timeout=timeout, **kw) + return conn + return self.do_open(build, req) + + def https_open(self, req): + def build(host, port=None, timeout=0, **kwargs): + kw = merge_dict(self.kw, kwargs) + conn = SocksiPyConnectionS(*self.args, host=host, port=port, timeout=timeout, **kw) + return conn + return self.do_open(build, req) + +if __name__ == "__main__": + import sys + try: + port = int(sys.argv[1]) + except (ValueError, IndexError): + port = 9050 + opener = urllib2.build_opener(SocksiPyHandler(socks.PROXY_TYPE_SOCKS5, "localhost", port)) + print("HTTP: " + opener.open("http://httpbin.org/ip").read().decode()) + print("HTTPS: " + opener.open("https://httpbin.org/ip").read().decode()) diff --git a/lib/tzlocal/darwin.py b/lib/tzlocal/darwin.py index 0485fb720..4e8540bc0 100644 --- a/lib/tzlocal/darwin.py +++ b/lib/tzlocal/darwin.py @@ -2,25 +2,45 @@ from __future__ import with_statement import os import pytz import subprocess +import sys _cache_tz = None +if sys.version_info[0] == 2: -def _get_localzone(): - pipe = subprocess.Popen( + class Popen(subprocess.Popen): + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + if self.stdout: + self.stdout.close() + if self.stderr: + self.stderr.close() + if self.stdin: + self.stdin.close() + # Wait for the process to terminate, to avoid zombies. + self.wait() + +else: + from subprocess import Popen + + +def _get_localzone(_root='/'): + with Popen( "systemsetup -gettimezone", shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE - ) - tzname = pipe.stdout.read().replace(b'Time Zone: ', b'').strip() + ) as pipe: + tzname = pipe.stdout.read().replace(b'Time Zone: ', b'').strip() if not tzname or tzname not in pytz.all_timezones_set: # link will be something like /usr/share/zoneinfo/America/Los_Angeles. - link = os.readlink("/etc/localtime") + link = os.readlink(os.path.join(_root, "etc/localtime")) tzname = link[link.rfind("zoneinfo/") + 9:] - pipe.stdout.close() - pipe.stderr.close() + return pytz.timezone(tzname) diff --git a/lib/tzlocal/test_data/Harare b/lib/tzlocal/test_data/Harare deleted file mode 100644 index 258b393637294912a6d6c78973c09424136ed50e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 157 zcmWHE%1kq2zyM4P3=AL)l3;|gIhx##r!X=w$jX33WfT}#e0)O~oE<|zTn17M`v3p` Pf4qiqIXi~v8gT&tQx_2^ diff --git a/lib/tzlocal/test_data/localtime/etc/localtime b/lib/tzlocal/test_data/localtime/etc/localtime deleted file mode 100644 index 258b393637294912a6d6c78973c09424136ed50e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 157 zcmWHE%1kq2zyM4P3=AL)l3;|gIhx##r!X=w$jX33WfT}#e0)O~oE<|zTn17M`v3p` Pf4qiqIXi~v8gT&tQx_2^ diff --git a/lib/tzlocal/test_data/symlink_localtime/etc/localtime b/lib/tzlocal/test_data/symlink_localtime/etc/localtime deleted file mode 120000 index 2f01cab10..000000000 --- a/lib/tzlocal/test_data/symlink_localtime/etc/localtime +++ /dev/null @@ -1 +0,0 @@ -../usr/share/zoneinfo/Africa/Harare \ No newline at end of file diff --git a/lib/tzlocal/test_data/symlink_localtime/usr/share/zoneinfo/Africa/Harare b/lib/tzlocal/test_data/symlink_localtime/usr/share/zoneinfo/Africa/Harare deleted file mode 100644 index 258b393637294912a6d6c78973c09424136ed50e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 157 zcmWHE%1kq2zyM4P3=AL)l3;|gIhx##r!X=w$jX33WfT}#e0)O~oE<|zTn17M`v3p` Pf4qiqIXi~v8gT&tQx_2^ diff --git a/lib/tzlocal/test_data/timezone/etc/timezone b/lib/tzlocal/test_data/timezone/etc/timezone deleted file mode 100644 index 28b3372d2..000000000 --- a/lib/tzlocal/test_data/timezone/etc/timezone +++ /dev/null @@ -1 +0,0 @@ -Africa/Harare diff --git a/lib/tzlocal/test_data/timezone_setting/etc/conf.d/clock b/lib/tzlocal/test_data/timezone_setting/etc/conf.d/clock deleted file mode 100644 index 95032934d..000000000 --- a/lib/tzlocal/test_data/timezone_setting/etc/conf.d/clock +++ /dev/null @@ -1 +0,0 @@ -TIMEZONE = "Africa/Harare" diff --git a/lib/tzlocal/test_data/vardbzoneinfo/var/db/zoneinfo b/lib/tzlocal/test_data/vardbzoneinfo/var/db/zoneinfo deleted file mode 100644 index 28b3372d2..000000000 --- a/lib/tzlocal/test_data/vardbzoneinfo/var/db/zoneinfo +++ /dev/null @@ -1 +0,0 @@ -Africa/Harare diff --git a/lib/tzlocal/test_data/zone_setting/etc/sysconfig/clock b/lib/tzlocal/test_data/zone_setting/etc/sysconfig/clock deleted file mode 100644 index e1ddbfd6e..000000000 --- a/lib/tzlocal/test_data/zone_setting/etc/sysconfig/clock +++ /dev/null @@ -1 +0,0 @@ -ZONE="Africa/Harare" diff --git a/lib/tzlocal/tests.py b/lib/tzlocal/tests.py deleted file mode 100644 index e736eb408..000000000 --- a/lib/tzlocal/tests.py +++ /dev/null @@ -1,79 +0,0 @@ -import sys -import os -from datetime import datetime -import unittest -import pytz -import tzlocal.unix - -class TzLocalTests(unittest.TestCase): - def setUp(self): - if 'TZ' in os.environ: - del os.environ['TZ'] - - def test_env(self): - tz_harare = tzlocal.unix._tz_from_env(':Africa/Harare') - self.assertEqual(tz_harare.zone, 'Africa/Harare') - - # Some Unices allow this as well, so we must allow it: - tz_harare = tzlocal.unix._tz_from_env('Africa/Harare') - self.assertEqual(tz_harare.zone, 'Africa/Harare') - - local_path = os.path.split(__file__)[0] - tz_local = tzlocal.unix._tz_from_env(':' + os.path.join(local_path, 'test_data', 'Harare')) - self.assertEqual(tz_local.zone, 'local') - # Make sure the local timezone is the same as the Harare one above. - # We test this with a past date, so that we don't run into future changes - # of the Harare timezone. - dt = datetime(2012, 1, 1, 5) - self.assertEqual(tz_harare.localize(dt), tz_local.localize(dt)) - - # Non-zoneinfo timezones are not supported in the TZ environment. - self.assertRaises(pytz.UnknownTimeZoneError, tzlocal.unix._tz_from_env, 'GMT+03:00') - - def test_timezone(self): - # Most versions of Ubuntu - local_path = os.path.split(__file__)[0] - tz = tzlocal.unix._get_localzone(_root=os.path.join(local_path, 'test_data', 'timezone')) - self.assertEqual(tz.zone, 'Africa/Harare') - - def test_zone_setting(self): - # A ZONE setting in /etc/sysconfig/clock, f ex CentOS - local_path = os.path.split(__file__)[0] - tz = tzlocal.unix._get_localzone(_root=os.path.join(local_path, 'test_data', 'zone_setting')) - self.assertEqual(tz.zone, 'Africa/Harare') - - def test_timezone_setting(self): - # A ZONE setting in /etc/conf.d/clock, f ex Gentoo - local_path = os.path.split(__file__)[0] - tz = tzlocal.unix._get_localzone(_root=os.path.join(local_path, 'test_data', 'timezone_setting')) - self.assertEqual(tz.zone, 'Africa/Harare') - - def test_symlink_localtime(self): - # A ZONE setting in the target path of a symbolic linked localtime, f ex systemd distributions - local_path = os.path.split(__file__)[0] - tz = tzlocal.unix._get_localzone(_root=os.path.join(local_path, 'test_data', 'symlink_localtime')) - self.assertEqual(tz.zone, 'Africa/Harare') - - def test_vardbzoneinfo_setting(self): - # A ZONE setting in /etc/conf.d/clock, f ex Gentoo - local_path = os.path.split(__file__)[0] - tz = tzlocal.unix._get_localzone(_root=os.path.join(local_path, 'test_data', 'vardbzoneinfo')) - self.assertEqual(tz.zone, 'Africa/Harare') - - def test_only_localtime(self): - local_path = os.path.split(__file__)[0] - tz = tzlocal.unix._get_localzone(_root=os.path.join(local_path, 'test_data', 'localtime')) - self.assertEqual(tz.zone, 'local') - dt = datetime(2012, 1, 1, 5) - self.assertEqual(pytz.timezone('Africa/Harare').localize(dt), tz.localize(dt)) - -if sys.platform == 'win32': - - import tzlocal.win32 - class TzWin32Tests(unittest.TestCase): - - def test_win32(self): - tzlocal.win32.get_localzone() - -if __name__ == '__main__': - unittest.main() diff --git a/lib/tzlocal/windows_tz.py b/lib/tzlocal/windows_tz.py index ee4be8a68..de89c8567 100644 --- a/lib/tzlocal/windows_tz.py +++ b/lib/tzlocal/windows_tz.py @@ -303,6 +303,7 @@ tz_win = {'Africa/Abidjan': 'Greenwich Standard Time', 'America/Port_of_Spain': 'SA Western Standard Time', 'America/Porto_Velho': 'SA Western Standard Time', 'America/Puerto_Rico': 'SA Western Standard Time', + 'America/Punta_Arenas': 'SA Eastern Standard Time', 'America/Rainy_River': 'Central Standard Time', 'America/Rankin_Inlet': 'Central Standard Time', 'America/Recife': 'SA Eastern Standard Time', @@ -340,7 +341,7 @@ tz_win = {'Africa/Abidjan': 'Greenwich Standard Time', 'Antarctica/Macquarie': 'Central Pacific Standard Time', 'Antarctica/Mawson': 'West Asia Standard Time', 'Antarctica/McMurdo': 'New Zealand Standard Time', - 'Antarctica/Palmer': 'Pacific SA Standard Time', + 'Antarctica/Palmer': 'SA Eastern Standard Time', 'Antarctica/Rothera': 'SA Eastern Standard Time', 'Antarctica/Syowa': 'E. Africa Standard Time', 'Antarctica/Vostok': 'Central Asia Standard Time', @@ -370,6 +371,7 @@ tz_win = {'Africa/Abidjan': 'Greenwich Standard Time', 'Asia/Dili': 'Tokyo Standard Time', 'Asia/Dubai': 'Arabian Standard Time', 'Asia/Dushanbe': 'West Asia Standard Time', + 'Asia/Famagusta': 'Turkey Standard Time', 'Asia/Gaza': 'West Bank Standard Time', 'Asia/Hebron': 'West Bank Standard Time', 'Asia/Hong_Kong': 'China Standard Time', diff --git a/lib/unidecode/__init__.py b/lib/unidecode/__init__.py index 82eb5a3fc..3b68de4c9 100644 --- a/lib/unidecode/__init__.py +++ b/lib/unidecode/__init__.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +# vi:tabstop=4:expandtab:sw=4 """Transliterate Unicode text into plain 7-bit ASCII. Example usage: @@ -18,19 +19,53 @@ from sys import version_info Cache = {} -def unidecode(string): + +def _warn_if_not_unicode(string): + if version_info[0] < 3 and not isinstance(string, unicode): + warnings.warn( "Argument %r is not an unicode object. " + "Passing an encoded string will likely have " + "unexpected results." % (type(string),), + RuntimeWarning, 2) + + +def unidecode_expect_ascii(string): """Transliterate an Unicode object into an ASCII string >>> unidecode(u"\u5317\u4EB0") "Bei Jing " + + This function first tries to convert the string using ASCII codec. + If it fails (because of non-ASCII characters), it falls back to + transliteration using the character tables. + + This is approx. five times faster if the string only contains ASCII + characters, but slightly slower than using unidecode directly if non-ASCII + chars are present. """ - if version_info[0] < 3 and not isinstance(string, unicode): - warnings.warn( "Argument %r is not an unicode object. " - "Passing an encoded string will likely have " - "unexpected results." % (type(string),), - RuntimeWarning, 2) + _warn_if_not_unicode(string) + try: + bytestring = string.encode('ASCII') + except UnicodeEncodeError: + return _unidecode(string) + if version_info[0] >= 3: + return string + else: + return bytestring + +def unidecode_expect_nonascii(string): + """Transliterate an Unicode object into an ASCII string + >>> unidecode(u"\u5317\u4EB0") + "Bei Jing " + """ + + _warn_if_not_unicode(string) + return _unidecode(string) + +unidecode = unidecode_expect_ascii + +def _unidecode(string): retval = [] for char in string: @@ -43,6 +78,11 @@ def unidecode(string): if codepoint > 0xeffff: continue # Characters in Private Use Area and above are ignored + if 0xd800 <= codepoint <= 0xdfff: + warnings.warn( "Surrogate character %r will be ignored. " + "You might be using a narrow Python build." % (char,), + RuntimeWarning, 2) + section = codepoint >> 8 # Chop off the last two hex digits position = codepoint % 256 # Last two hex digits @@ -50,7 +90,7 @@ def unidecode(string): table = Cache[section] except KeyError: try: - mod = __import__('unidecode.x%03x'%(section), [], [], ['data']) + mod = __import__('unidecode.x%03x'%(section), globals(), locals(), ['data']) except ImportError: Cache[section] = None continue # No match: ignore this character and carry on. diff --git a/lib/unidecode/util.py b/lib/unidecode/util.py new file mode 100644 index 000000000..477280d18 --- /dev/null +++ b/lib/unidecode/util.py @@ -0,0 +1,58 @@ +# vim:ts=4 sw=4 expandtab softtabstop=4 +from __future__ import print_function +import optparse +import locale +import os +import sys +import warnings + +from unidecode import unidecode + +PY3 = sys.version_info[0] >= 3 + +def fatal(msg): + sys.stderr.write(msg + "\n") + sys.exit(1) + +def main(): + default_encoding = locale.getpreferredencoding() + + parser = optparse.OptionParser('%prog [options] [FILE]', + description="Transliterate Unicode text into ASCII. FILE is path to file to transliterate. " + "Standard input is used if FILE is omitted and -c is not specified.") + parser.add_option('-e', '--encoding', metavar='ENCODING', default=default_encoding, + help='Specify an encoding (default is %s)' % (default_encoding,)) + parser.add_option('-c', metavar='TEXT', dest='text', + help='Transliterate TEXT instead of FILE') + + options, args = parser.parse_args() + + encoding = options.encoding + + if args: + if options.text: + fatal("Can't use both FILE and -c option") + else: + with open(args[0], 'rb') as f: + stream = f.read() + elif options.text: + if PY3: + stream = os.fsencode(options.text) + else: + stream = options.text + # add a newline to the string if it comes from the + # command line so that the result is printed nicely + # on the console. + stream += '\n'.encode('ascii') + else: + if PY3: + stream = sys.stdin.buffer.read() + else: + stream = sys.stdin.read() + + try: + stream = stream.decode(encoding) + except UnicodeDecodeError as e: + fatal('Unable to decode input: %s, start: %d, end: %d' % (e.reason, e.start, e.end)) + + sys.stdout.write(unidecode(stream)) diff --git a/lib/unidecode/x000.py b/lib/unidecode/x000.py index 6821df47d..c3f8f5157 100644 --- a/lib/unidecode/x000.py +++ b/lib/unidecode/x000.py @@ -1,132 +1,15 @@ data = ( -'\x00', # 0x00 -'\x01', # 0x01 -'\x02', # 0x02 -'\x03', # 0x03 -'\x04', # 0x04 -'\x05', # 0x05 -'\x06', # 0x06 -'\x07', # 0x07 -'\x08', # 0x08 -'\x09', # 0x09 -'\x0a', # 0x0a -'\x0b', # 0x0b -'\x0c', # 0x0c -'\x0d', # 0x0d -'\x0e', # 0x0e -'\x0f', # 0x0f -'\x10', # 0x10 -'\x11', # 0x11 -'\x12', # 0x12 -'\x13', # 0x13 -'\x14', # 0x14 -'\x15', # 0x15 -'\x16', # 0x16 -'\x17', # 0x17 -'\x18', # 0x18 -'\x19', # 0x19 -'\x1a', # 0x1a -'\x1b', # 0x1b -'\x1c', # 0x1c -'\x1d', # 0x1d -'\x1e', # 0x1e -'\x1f', # 0x1f -' ', # 0x20 -'!', # 0x21 -'"', # 0x22 -'#', # 0x23 -'$', # 0x24 -'%', # 0x25 -'&', # 0x26 -'\'', # 0x27 -'(', # 0x28 -')', # 0x29 -'*', # 0x2a -'+', # 0x2b -',', # 0x2c -'-', # 0x2d -'.', # 0x2e -'/', # 0x2f -'0', # 0x30 -'1', # 0x31 -'2', # 0x32 -'3', # 0x33 -'4', # 0x34 -'5', # 0x35 -'6', # 0x36 -'7', # 0x37 -'8', # 0x38 -'9', # 0x39 -':', # 0x3a -';', # 0x3b -'<', # 0x3c -'=', # 0x3d -'>', # 0x3e -'?', # 0x3f -'@', # 0x40 -'A', # 0x41 -'B', # 0x42 -'C', # 0x43 -'D', # 0x44 -'E', # 0x45 -'F', # 0x46 -'G', # 0x47 -'H', # 0x48 -'I', # 0x49 -'J', # 0x4a -'K', # 0x4b -'L', # 0x4c -'M', # 0x4d -'N', # 0x4e -'O', # 0x4f -'P', # 0x50 -'Q', # 0x51 -'R', # 0x52 -'S', # 0x53 -'T', # 0x54 -'U', # 0x55 -'V', # 0x56 -'W', # 0x57 -'X', # 0x58 -'Y', # 0x59 -'Z', # 0x5a -']', # 0x5b -'\\', # 0x5c -']', # 0x5d -'^', # 0x5e -'_', # 0x5f -'`', # 0x60 -'a', # 0x61 -'b', # 0x62 -'c', # 0x63 -'d', # 0x64 -'e', # 0x65 -'f', # 0x66 -'g', # 0x67 -'h', # 0x68 -'i', # 0x69 -'j', # 0x6a -'k', # 0x6b -'l', # 0x6c -'m', # 0x6d -'n', # 0x6e -'o', # 0x6f -'p', # 0x70 -'q', # 0x71 -'r', # 0x72 -'s', # 0x73 -'t', # 0x74 -'u', # 0x75 -'v', # 0x76 -'w', # 0x77 -'x', # 0x78 -'y', # 0x79 -'z', # 0x7a -'{', # 0x7b -'|', # 0x7c -'}', # 0x7d -'~', # 0x7e -'', # 0x7f +# Code points u+007f and below are equivalent to ASCII and are handled by a +# special case in the code. Hence they are not present in this table. +'', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', +'', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', +'', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', +'', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', +'', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', +'', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', +'', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', +'', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', + '', # 0x80 '', # 0x81 '', # 0x82 @@ -162,7 +45,10 @@ data = ( ' ', # 0xa0 '!', # 0xa1 'C/', # 0xa2 + +# Not "GBP" - Pound Sign is used for more than just British Pounds. 'PS', # 0xa3 + '$?', # 0xa4 'Y=', # 0xa5 '|', # 0xa6 @@ -177,8 +63,11 @@ data = ( '-', # 0xaf 'deg', # 0xb0 '+-', # 0xb1 + +# These might be combined with other superscript digits (u+2070 - u+2079) '2', # 0xb2 '3', # 0xb3 + '\'', # 0xb4 'u', # 0xb5 'P', # 0xb6 @@ -195,7 +84,10 @@ data = ( 'A', # 0xc1 'A', # 0xc2 'A', # 0xc3 + +# Not "AE" - used in languages other than German 'A', # 0xc4 + 'A', # 0xc5 'AE', # 0xc6 'C', # 0xc7 @@ -213,13 +105,19 @@ data = ( 'O', # 0xd3 'O', # 0xd4 'O', # 0xd5 + +# Not "OE" - used in languages other than German 'O', # 0xd6 + 'x', # 0xd7 'O', # 0xd8 'U', # 0xd9 'U', # 0xda 'U', # 0xdb + +# Not "UE" - used in languages other than German 'U', # 0xdc + 'Y', # 0xdd 'Th', # 0xde 'ss', # 0xdf @@ -227,7 +125,10 @@ data = ( 'a', # 0xe1 'a', # 0xe2 'a', # 0xe3 + +# Not "ae" - used in languages other than German 'a', # 0xe4 + 'a', # 0xe5 'ae', # 0xe6 'c', # 0xe7 @@ -245,13 +146,19 @@ data = ( 'o', # 0xf3 'o', # 0xf4 'o', # 0xf5 + +# Not "oe" - used in languages other than German 'o', # 0xf6 + '/', # 0xf7 'o', # 0xf8 'u', # 0xf9 'u', # 0xfa 'u', # 0xfb + +# Not "ue" - used in languages other than German 'u', # 0xfc + 'y', # 0xfd 'th', # 0xfe 'y', # 0xff diff --git a/lib/unidecode/x002.py b/lib/unidecode/x002.py index ea45441e1..d7028cdf3 100644 --- a/lib/unidecode/x002.py +++ b/lib/unidecode/x002.py @@ -175,7 +175,7 @@ data = ( ']]', # 0xad 'h', # 0xae 'h', # 0xaf -'k', # 0xb0 +'h', # 0xb0 'h', # 0xb1 'j', # 0xb2 'r', # 0xb3 diff --git a/lib/unidecode/x005.py b/lib/unidecode/x005.py index 738a99623..2913ffff0 100644 --- a/lib/unidecode/x005.py +++ b/lib/unidecode/x005.py @@ -136,7 +136,7 @@ data = ( 'f', # 0x86 'ew', # 0x87 '[?]', # 0x88 -'.', # 0x89 +':', # 0x89 '-', # 0x8a '[?]', # 0x8b '[?]', # 0x8c @@ -191,7 +191,7 @@ data = ( '', # 0xbd '', # 0xbe '', # 0xbf -'', # 0xc0 +'|', # 0xc0 '', # 0xc1 '', # 0xc2 ':', # 0xc3 diff --git a/lib/unidecode/x020.py b/lib/unidecode/x020.py index cb6963e2d..bee561b03 100644 --- a/lib/unidecode/x020.py +++ b/lib/unidecode/x020.py @@ -70,23 +70,23 @@ data = ( '/', # 0x44 '-[', # 0x45 ']-', # 0x46 -'[?]', # 0x47 +'??', # 0x47 '?!', # 0x48 '!?', # 0x49 '7', # 0x4a 'PP', # 0x4b '(]', # 0x4c '[)', # 0x4d -'[?]', # 0x4e +'*', # 0x4e '[?]', # 0x4f '[?]', # 0x50 '[?]', # 0x51 -'[?]', # 0x52 -'[?]', # 0x53 +'%', # 0x52 +'~', # 0x53 '[?]', # 0x54 '[?]', # 0x55 '[?]', # 0x56 -'[?]', # 0x57 +"''''", # 0x57 '[?]', # 0x58 '[?]', # 0x59 '[?]', # 0x5a @@ -94,8 +94,8 @@ data = ( '[?]', # 0x5c '[?]', # 0x5d '[?]', # 0x5e -'[?]', # 0x5f -'[?]', # 0x60 +' ', # 0x5f +'', # 0x60 '[?]', # 0x61 '[?]', # 0x62 '[?]', # 0x63 @@ -112,7 +112,7 @@ data = ( '', # 0x6e '', # 0x6f '0', # 0x70 -'', # 0x71 +'i', # 0x71 '', # 0x72 '', # 0x73 '4', # 0x74 @@ -143,19 +143,19 @@ data = ( '(', # 0x8d ')', # 0x8e '[?]', # 0x8f -'[?]', # 0x90 -'[?]', # 0x91 -'[?]', # 0x92 -'[?]', # 0x93 +'a', # 0x90 +'e', # 0x91 +'o', # 0x92 +'x', # 0x93 '[?]', # 0x94 -'[?]', # 0x95 -'[?]', # 0x96 -'[?]', # 0x97 -'[?]', # 0x98 -'[?]', # 0x99 -'[?]', # 0x9a -'[?]', # 0x9b -'[?]', # 0x9c +'h', # 0x95 +'k', # 0x96 +'l', # 0x97 +'m', # 0x98 +'n', # 0x99 +'p', # 0x9a +'s', # 0x9b +'t', # 0x9c '[?]', # 0x9d '[?]', # 0x9e '[?]', # 0x9f @@ -171,7 +171,7 @@ data = ( 'W', # 0xa9 'NS', # 0xaa 'D', # 0xab -'EU', # 0xac +'EUR', # 0xac 'K', # 0xad 'T', # 0xae 'Dr', # 0xaf @@ -228,7 +228,7 @@ data = ( '', # 0xe2 '', # 0xe3 '[?]', # 0xe4 -'[?]', # 0xe5 +'', # 0xe5 '[?]', # 0xe6 '[?]', # 0xe7 '[?]', # 0xe8 diff --git a/lib/unidecode/x021.py b/lib/unidecode/x021.py index 0164cdb5b..cc74bc65f 100644 --- a/lib/unidecode/x021.py +++ b/lib/unidecode/x021.py @@ -1,87 +1,87 @@ data = ( -'', # 0x00 -'', # 0x01 -'', # 0x02 +' a/c ', # 0x00 +' a/s ', # 0x01 +'C', # 0x02 '', # 0x03 '', # 0x04 -'', # 0x05 -'', # 0x06 +' c/o ', # 0x05 +' c/u ', # 0x06 '', # 0x07 '', # 0x08 '', # 0x09 -'', # 0x0a -'', # 0x0b -'', # 0x0c -'', # 0x0d -'', # 0x0e +'g', # 0x0a +'H', # 0x0b +'H', # 0x0c +'H', # 0x0d +'h', # 0x0e '', # 0x0f -'', # 0x10 -'', # 0x11 -'', # 0x12 -'', # 0x13 +'I', # 0x10 +'I', # 0x11 +'L', # 0x12 +'l', # 0x13 '', # 0x14 -'', # 0x15 +'N', # 0x15 '', # 0x16 '', # 0x17 '', # 0x18 -'', # 0x19 -'', # 0x1a -'', # 0x1b -'', # 0x1c -'', # 0x1d +'P', # 0x19 +'Q', # 0x1a +'R', # 0x1b +'R', # 0x1c +'R', # 0x1d '', # 0x1e '', # 0x1f -'', # 0x20 -'', # 0x21 -'', # 0x22 +'(sm)', # 0x20 +'TEL', # 0x21 +'(tm)', # 0x22 '', # 0x23 -'', # 0x24 +'Z', # 0x24 '', # 0x25 '', # 0x26 '', # 0x27 -'', # 0x28 +'Z', # 0x28 '', # 0x29 'K', # 0x2a 'A', # 0x2b -'', # 0x2c -'', # 0x2d -'', # 0x2e -'', # 0x2f -'', # 0x30 -'', # 0x31 +'B', # 0x2c +'C', # 0x2d +'e', # 0x2e +'e', # 0x2f +'E', # 0x30 +'F', # 0x31 'F', # 0x32 -'', # 0x33 -'', # 0x34 +'M', # 0x33 +'o', # 0x34 '', # 0x35 '', # 0x36 '', # 0x37 '', # 0x38 -'', # 0x39 +'i', # 0x39 '', # 0x3a -'[?]', # 0x3b -'[?]', # 0x3c -'[?]', # 0x3d -'[?]', # 0x3e -'[?]', # 0x3f +'FAX', # 0x3b +'', # 0x3c +'', # 0x3d +'', # 0x3e +'', # 0x3f '[?]', # 0x40 '[?]', # 0x41 '[?]', # 0x42 '[?]', # 0x43 '[?]', # 0x44 -'[?]', # 0x45 -'[?]', # 0x46 -'[?]', # 0x47 -'[?]', # 0x48 -'[?]', # 0x49 +'D', # 0x45 +'d', # 0x46 +'e', # 0x47 +'i', # 0x48 +'j', # 0x49 '[?]', # 0x4a '[?]', # 0x4b '[?]', # 0x4c '[?]', # 0x4d 'F', # 0x4e '[?]', # 0x4f -'[?]', # 0x50 -'[?]', # 0x51 -'[?]', # 0x52 +' 1/7 ', # 0x50 +' 1/9 ', # 0x51 +' 1/10 ', # 0x52 ' 1/3 ', # 0x53 ' 2/3 ', # 0x54 ' 1/5 ', # 0x55 @@ -136,7 +136,7 @@ data = ( '[?]', # 0x86 '[?]', # 0x87 '[?]', # 0x88 -'[?]', # 0x89 +' 0/3 ', # 0x89 '[?]', # 0x8a '[?]', # 0x8b '[?]', # 0x8c diff --git a/lib/unidecode/x022.py b/lib/unidecode/x022.py index 2046a9b4e..e38fb5cca 100644 --- a/lib/unidecode/x022.py +++ b/lib/unidecode/x022.py @@ -17,12 +17,12 @@ data = ( '[?]', # 0x0f '[?]', # 0x10 '[?]', # 0x11 -'[?]', # 0x12 +'-', # 0x12 '[?]', # 0x13 '[?]', # 0x14 -'[?]', # 0x15 -'[?]', # 0x16 -'[?]', # 0x17 +'/', # 0x15 +'\\', # 0x16 +'*', # 0x17 '[?]', # 0x18 '[?]', # 0x19 '[?]', # 0x1a @@ -34,7 +34,7 @@ data = ( '[?]', # 0x20 '[?]', # 0x21 '[?]', # 0x22 -'[?]', # 0x23 +'|', # 0x23 '[?]', # 0x24 '[?]', # 0x25 '[?]', # 0x26 @@ -53,13 +53,13 @@ data = ( '[?]', # 0x33 '[?]', # 0x34 '[?]', # 0x35 -'[?]', # 0x36 +':', # 0x36 '[?]', # 0x37 '[?]', # 0x38 '[?]', # 0x39 '[?]', # 0x3a '[?]', # 0x3b -'[?]', # 0x3c +'~', # 0x3c '[?]', # 0x3d '[?]', # 0x3e '[?]', # 0x3f @@ -99,10 +99,10 @@ data = ( '[?]', # 0x61 '[?]', # 0x62 '[?]', # 0x63 -'[?]', # 0x64 -'[?]', # 0x65 -'[?]', # 0x66 -'[?]', # 0x67 +'<=', # 0x64 +'>=', # 0x65 +'<=', # 0x66 +'>=', # 0x67 '[?]', # 0x68 '[?]', # 0x69 '[?]', # 0x6a diff --git a/lib/unidecode/x023.py b/lib/unidecode/x023.py index 2046a9b4e..3c4462e29 100644 --- a/lib/unidecode/x023.py +++ b/lib/unidecode/x023.py @@ -2,7 +2,7 @@ data = ( '[?]', # 0x00 '[?]', # 0x01 '[?]', # 0x02 -'[?]', # 0x03 +'^', # 0x03 '[?]', # 0x04 '[?]', # 0x05 '[?]', # 0x06 @@ -40,8 +40,8 @@ data = ( '[?]', # 0x26 '[?]', # 0x27 '[?]', # 0x28 -'[?]', # 0x29 -'[?]', # 0x2a +'<', # 0x29 +'> ', # 0x2a '[?]', # 0x2b '[?]', # 0x2c '[?]', # 0x2d diff --git a/lib/unidecode/x024.py b/lib/unidecode/x024.py index 76d8c8cd8..231b0ca14 100644 --- a/lib/unidecode/x024.py +++ b/lib/unidecode/x024.py @@ -95,118 +95,118 @@ data = ( '[?]', # 0x5d '[?]', # 0x5e '[?]', # 0x5f -'', # 0x60 -'', # 0x61 -'', # 0x62 -'', # 0x63 -'', # 0x64 -'', # 0x65 -'', # 0x66 -'', # 0x67 -'', # 0x68 -'', # 0x69 -'', # 0x6a -'', # 0x6b -'', # 0x6c -'', # 0x6d -'', # 0x6e -'', # 0x6f -'', # 0x70 -'', # 0x71 -'', # 0x72 -'', # 0x73 -'', # 0x74 -'', # 0x75 -'', # 0x76 -'', # 0x77 -'', # 0x78 -'', # 0x79 -'', # 0x7a -'', # 0x7b -'', # 0x7c -'', # 0x7d -'', # 0x7e -'', # 0x7f -'', # 0x80 -'', # 0x81 -'', # 0x82 -'', # 0x83 -'', # 0x84 -'', # 0x85 -'', # 0x86 -'', # 0x87 -'', # 0x88 -'', # 0x89 -'', # 0x8a -'', # 0x8b -'', # 0x8c -'', # 0x8d -'', # 0x8e -'', # 0x8f -'', # 0x90 -'', # 0x91 -'', # 0x92 -'', # 0x93 -'', # 0x94 -'', # 0x95 -'', # 0x96 -'', # 0x97 -'', # 0x98 -'', # 0x99 -'', # 0x9a -'', # 0x9b -'', # 0x9c -'', # 0x9d -'', # 0x9e -'', # 0x9f -'', # 0xa0 -'', # 0xa1 -'', # 0xa2 -'', # 0xa3 -'', # 0xa4 -'', # 0xa5 -'', # 0xa6 -'', # 0xa7 -'', # 0xa8 -'', # 0xa9 -'', # 0xaa -'', # 0xab -'', # 0xac -'', # 0xad -'', # 0xae -'', # 0xaf -'', # 0xb0 -'', # 0xb1 -'', # 0xb2 -'', # 0xb3 -'', # 0xb4 -'', # 0xb5 -'', # 0xb6 -'', # 0xb7 -'', # 0xb8 -'', # 0xb9 -'', # 0xba -'', # 0xbb -'', # 0xbc -'', # 0xbd -'', # 0xbe -'', # 0xbf -'', # 0xc0 -'', # 0xc1 -'', # 0xc2 -'', # 0xc3 -'', # 0xc4 -'', # 0xc5 -'', # 0xc6 -'', # 0xc7 -'', # 0xc8 -'', # 0xc9 -'', # 0xca -'', # 0xcb -'', # 0xcc -'', # 0xcd -'', # 0xce -'', # 0xcf +'1', # 0x60 +'2', # 0x61 +'3', # 0x62 +'4', # 0x63 +'5', # 0x64 +'6', # 0x65 +'7', # 0x66 +'8', # 0x67 +'9', # 0x68 +'10', # 0x69 +'11', # 0x6a +'12', # 0x6b +'13', # 0x6c +'14', # 0x6d +'15', # 0x6e +'16', # 0x6f +'17', # 0x70 +'18', # 0x71 +'19', # 0x72 +'20', # 0x73 +'(1)', # 0x74 +'(2)', # 0x75 +'(3)', # 0x76 +'(4)', # 0x77 +'(5)', # 0x78 +'(6)', # 0x79 +'(7)', # 0x7a +'(8)', # 0x7b +'(9)', # 0x7c +'(10)', # 0x7d +'(11)', # 0x7e +'(12)', # 0x7f +'(13)', # 0x80 +'(14)', # 0x81 +'(15)', # 0x82 +'(16)', # 0x83 +'(17)', # 0x84 +'(18)', # 0x85 +'(19)', # 0x86 +'(20)', # 0x87 +'1.', # 0x88 +'2.', # 0x89 +'3.', # 0x8a +'4.', # 0x8b +'5.', # 0x8c +'6.', # 0x8d +'7.', # 0x8e +'8.', # 0x8f +'9.', # 0x90 +'10.', # 0x91 +'11.', # 0x92 +'12.', # 0x93 +'13.', # 0x94 +'14.', # 0x95 +'15.', # 0x96 +'16.', # 0x97 +'17.', # 0x98 +'18.', # 0x99 +'19.', # 0x9a +'20.', # 0x9b +'(a)', # 0x9c +'(b)', # 0x9d +'(c)', # 0x9e +'(d)', # 0x9f +'(e)', # 0xa0 +'(f)', # 0xa1 +'(g)', # 0xa2 +'(h)', # 0xa3 +'(i)', # 0xa4 +'(j)', # 0xa5 +'(k)', # 0xa6 +'(l)', # 0xa7 +'(m)', # 0xa8 +'(n)', # 0xa9 +'(o)', # 0xaa +'(p)', # 0xab +'(q)', # 0xac +'(r)', # 0xad +'(s)', # 0xae +'(t)', # 0xaf +'(u)', # 0xb0 +'(v)', # 0xb1 +'(w)', # 0xb2 +'(x)', # 0xb3 +'(y)', # 0xb4 +'(z)', # 0xb5 +'A', # 0xb6 +'B', # 0xb7 +'C', # 0xb8 +'D', # 0xb9 +'E', # 0xba +'F', # 0xbb +'G', # 0xbc +'H', # 0xbd +'I', # 0xbe +'J', # 0xbf +'K', # 0xc0 +'L', # 0xc1 +'M', # 0xc2 +'N', # 0xc3 +'O', # 0xc4 +'P', # 0xc5 +'Q', # 0xc6 +'R', # 0xc7 +'S', # 0xc8 +'T', # 0xc9 +'U', # 0xca +'V', # 0xcb +'W', # 0xcc +'X', # 0xcd +'Y', # 0xce +'Z', # 0xcf 'a', # 0xd0 'b', # 0xd1 'c', # 0xd2 @@ -234,24 +234,25 @@ data = ( 'y', # 0xe8 'z', # 0xe9 '0', # 0xea -'[?]', # 0xeb -'[?]', # 0xec -'[?]', # 0xed -'[?]', # 0xee -'[?]', # 0xef -'[?]', # 0xf0 -'[?]', # 0xf1 -'[?]', # 0xf2 -'[?]', # 0xf3 -'[?]', # 0xf4 -'[?]', # 0xf5 -'[?]', # 0xf6 -'[?]', # 0xf7 -'[?]', # 0xf8 -'[?]', # 0xf9 -'[?]', # 0xfa -'[?]', # 0xfb -'[?]', # 0xfc -'[?]', # 0xfd -'[?]', # 0xfe +'11', # 0xeb +'12', # 0xec +'13', # 0xed +'14', # 0xee +'15', # 0xef +'16', # 0xf0 +'17', # 0xf1 +'18', # 0xf2 +'19', # 0xf3 +'20', # 0xf4 +'1', # 0xf5 +'2', # 0xf6 +'3', # 0xf7 +'4', # 0xf8 +'5', # 0xf9 +'6', # 0xfa +'7', # 0xfb +'8', # 0xfc +'9', # 0xfd +'10', # 0xfe +'0', # 0xff ) diff --git a/lib/unidecode/x026.py b/lib/unidecode/x026.py index bfb03a9af..c575472c7 100644 --- a/lib/unidecode/x026.py +++ b/lib/unidecode/x026.py @@ -110,7 +110,7 @@ data = ( '', # 0x6c '', # 0x6d '', # 0x6e -'', # 0x6f +'#', # 0x6f '', # 0x70 '', # 0x71 '[?]', # 0x72 diff --git a/lib/unidecode/x027.py b/lib/unidecode/x027.py index 473cfc772..3c74c0738 100644 --- a/lib/unidecode/x027.py +++ b/lib/unidecode/x027.py @@ -48,7 +48,7 @@ data = ( '', # 0x2e '', # 0x2f '', # 0x30 -'', # 0x31 +'*', # 0x31 '', # 0x32 '', # 0x33 '', # 0x34 @@ -87,7 +87,7 @@ data = ( '', # 0x55 '', # 0x56 '', # 0x57 -'', # 0x58 +'|', # 0x58 '', # 0x59 '', # 0x5a '', # 0x5b @@ -97,7 +97,7 @@ data = ( '[?]', # 0x5f '[?]', # 0x60 '', # 0x61 -'', # 0x62 +'!', # 0x62 '', # 0x63 '', # 0x64 '', # 0x65 @@ -229,10 +229,10 @@ data = ( '[?]', # 0xe3 '[?]', # 0xe4 '[?]', # 0xe5 -'[?]', # 0xe6 +'[', # 0xe6 '[?]', # 0xe7 -'[?]', # 0xe8 -'[?]', # 0xe9 +'<', # 0xe8 +'> ', # 0xe9 '[?]', # 0xea '[?]', # 0xeb '[?]', # 0xec diff --git a/lib/unidecode/x029.py b/lib/unidecode/x029.py new file mode 100644 index 000000000..c2df25484 --- /dev/null +++ b/lib/unidecode/x029.py @@ -0,0 +1,257 @@ +data = ( +'', # 0x00 +'', # 0x01 +'', # 0x02 +'', # 0x03 +'', # 0x04 +'', # 0x05 +'', # 0x06 +'', # 0x07 +'', # 0x08 +'', # 0x09 +'', # 0x0a +'', # 0x0b +'', # 0x0c +'', # 0x0d +'', # 0x0e +'', # 0x0f +'', # 0x10 +'', # 0x11 +'', # 0x12 +'', # 0x13 +'', # 0x14 +'', # 0x15 +'', # 0x16 +'', # 0x17 +'', # 0x18 +'', # 0x19 +'', # 0x1a +'', # 0x1b +'', # 0x1c +'', # 0x1d +'', # 0x1e +'', # 0x1f +'', # 0x20 +'', # 0x21 +'', # 0x22 +'', # 0x23 +'', # 0x24 +'', # 0x25 +'', # 0x26 +'', # 0x27 +'', # 0x28 +'', # 0x29 +'', # 0x2a +'', # 0x2b +'', # 0x2c +'', # 0x2d +'', # 0x2e +'', # 0x2f +'', # 0x30 +'', # 0x31 +'', # 0x32 +'', # 0x33 +'', # 0x34 +'', # 0x35 +'', # 0x36 +'', # 0x37 +'', # 0x38 +'', # 0x39 +'', # 0x3a +'', # 0x3b +'', # 0x3c +'', # 0x3d +'', # 0x3e +'', # 0x3f +'', # 0x40 +'', # 0x41 +'', # 0x42 +'', # 0x43 +'', # 0x44 +'', # 0x45 +'', # 0x46 +'', # 0x47 +'', # 0x48 +'', # 0x49 +'', # 0x4a +'', # 0x4b +'', # 0x4c +'', # 0x4d +'', # 0x4e +'', # 0x4f +'', # 0x50 +'', # 0x51 +'', # 0x52 +'', # 0x53 +'', # 0x54 +'', # 0x55 +'', # 0x56 +'', # 0x57 +'', # 0x58 +'', # 0x59 +'', # 0x5a +'', # 0x5b +'', # 0x5c +'', # 0x5d +'', # 0x5e +'', # 0x5f +'', # 0x60 +'', # 0x61 +'', # 0x62 +'', # 0x63 +'', # 0x64 +'', # 0x65 +'', # 0x66 +'', # 0x67 +'', # 0x68 +'', # 0x69 +'', # 0x6a +'', # 0x6b +'', # 0x6c +'', # 0x6d +'', # 0x6e +'', # 0x6f +'', # 0x70 +'', # 0x71 +'', # 0x72 +'', # 0x73 +'', # 0x74 +'', # 0x75 +'', # 0x76 +'', # 0x77 +'', # 0x78 +'', # 0x79 +'', # 0x7a +'', # 0x7b +'', # 0x7c +'', # 0x7d +'', # 0x7e +'', # 0x7f +'', # 0x80 +'', # 0x81 +'', # 0x82 +'{', # 0x83 +'} ', # 0x84 +'', # 0x85 +'', # 0x86 +'', # 0x87 +'', # 0x88 +'', # 0x89 +'', # 0x8a +'', # 0x8b +'', # 0x8c +'', # 0x8d +'', # 0x8e +'', # 0x8f +'', # 0x90 +'', # 0x91 +'', # 0x92 +'', # 0x93 +'', # 0x94 +'', # 0x95 +'', # 0x96 +'', # 0x97 +'', # 0x98 +'', # 0x99 +'', # 0x9a +'', # 0x9b +'', # 0x9c +'', # 0x9d +'', # 0x9e +'', # 0x9f +'', # 0xa0 +'', # 0xa1 +'', # 0xa2 +'', # 0xa3 +'', # 0xa4 +'', # 0xa5 +'', # 0xa6 +'', # 0xa7 +'', # 0xa8 +'', # 0xa9 +'', # 0xaa +'', # 0xab +'', # 0xac +'', # 0xad +'', # 0xae +'', # 0xaf +'', # 0xb0 +'', # 0xb1 +'', # 0xb2 +'', # 0xb3 +'', # 0xb4 +'', # 0xb5 +'', # 0xb6 +'', # 0xb7 +'', # 0xb8 +'', # 0xb9 +'', # 0xba +'', # 0xbb +'', # 0xbc +'', # 0xbd +'', # 0xbe +'', # 0xbf +'', # 0xc0 +'', # 0xc1 +'', # 0xc2 +'', # 0xc3 +'', # 0xc4 +'', # 0xc5 +'', # 0xc6 +'', # 0xc7 +'', # 0xc8 +'', # 0xc9 +'', # 0xca +'', # 0xcb +'', # 0xcc +'', # 0xcd +'', # 0xce +'', # 0xcf +'', # 0xd0 +'', # 0xd1 +'', # 0xd2 +'', # 0xd3 +'', # 0xd4 +'', # 0xd5 +'', # 0xd6 +'', # 0xd7 +'', # 0xd8 +'', # 0xd9 +'', # 0xda +'', # 0xdb +'', # 0xdc +'', # 0xdd +'', # 0xde +'', # 0xdf +'', # 0xe0 +'', # 0xe1 +'', # 0xe2 +'', # 0xe3 +'', # 0xe4 +'', # 0xe5 +'', # 0xe6 +'', # 0xe7 +'', # 0xe8 +'', # 0xe9 +'', # 0xea +'', # 0xeb +'', # 0xec +'', # 0xed +'', # 0xee +'', # 0xef +'', # 0xf0 +'', # 0xf1 +'', # 0xf2 +'', # 0xf3 +'', # 0xf4 +'', # 0xf5 +'', # 0xf6 +'', # 0xf7 +'', # 0xf8 +'', # 0xf9 +'', # 0xfa +'', # 0xfb +'', # 0xfc +'', # 0xfd +'', # 0xfe +) diff --git a/lib/unidecode/x02a.py b/lib/unidecode/x02a.py new file mode 100644 index 000000000..b832ef352 --- /dev/null +++ b/lib/unidecode/x02a.py @@ -0,0 +1,257 @@ +data = ( +'', # 0x00 +'', # 0x01 +'', # 0x02 +'', # 0x03 +'', # 0x04 +'', # 0x05 +'', # 0x06 +'', # 0x07 +'', # 0x08 +'', # 0x09 +'', # 0x0a +'', # 0x0b +'', # 0x0c +'', # 0x0d +'', # 0x0e +'', # 0x0f +'', # 0x10 +'', # 0x11 +'', # 0x12 +'', # 0x13 +'', # 0x14 +'', # 0x15 +'', # 0x16 +'', # 0x17 +'', # 0x18 +'', # 0x19 +'', # 0x1a +'', # 0x1b +'', # 0x1c +'', # 0x1d +'', # 0x1e +'', # 0x1f +'', # 0x20 +'', # 0x21 +'', # 0x22 +'', # 0x23 +'', # 0x24 +'', # 0x25 +'', # 0x26 +'', # 0x27 +'', # 0x28 +'', # 0x29 +'', # 0x2a +'', # 0x2b +'', # 0x2c +'', # 0x2d +'', # 0x2e +'', # 0x2f +'', # 0x30 +'', # 0x31 +'', # 0x32 +'', # 0x33 +'', # 0x34 +'', # 0x35 +'', # 0x36 +'', # 0x37 +'', # 0x38 +'', # 0x39 +'', # 0x3a +'', # 0x3b +'', # 0x3c +'', # 0x3d +'', # 0x3e +'', # 0x3f +'', # 0x40 +'', # 0x41 +'', # 0x42 +'', # 0x43 +'', # 0x44 +'', # 0x45 +'', # 0x46 +'', # 0x47 +'', # 0x48 +'', # 0x49 +'', # 0x4a +'', # 0x4b +'', # 0x4c +'', # 0x4d +'', # 0x4e +'', # 0x4f +'', # 0x50 +'', # 0x51 +'', # 0x52 +'', # 0x53 +'', # 0x54 +'', # 0x55 +'', # 0x56 +'', # 0x57 +'', # 0x58 +'', # 0x59 +'', # 0x5a +'', # 0x5b +'', # 0x5c +'', # 0x5d +'', # 0x5e +'', # 0x5f +'', # 0x60 +'', # 0x61 +'', # 0x62 +'', # 0x63 +'', # 0x64 +'', # 0x65 +'', # 0x66 +'', # 0x67 +'', # 0x68 +'', # 0x69 +'', # 0x6a +'', # 0x6b +'', # 0x6c +'', # 0x6d +'', # 0x6e +'', # 0x6f +'', # 0x70 +'', # 0x71 +'', # 0x72 +'', # 0x73 +'::=', # 0x74 +'==', # 0x75 +'===', # 0x76 +'', # 0x77 +'', # 0x78 +'', # 0x79 +'', # 0x7a +'', # 0x7b +'', # 0x7c +'', # 0x7d +'', # 0x7e +'', # 0x7f +'', # 0x80 +'', # 0x81 +'', # 0x82 +'', # 0x83 +'', # 0x84 +'', # 0x85 +'', # 0x86 +'', # 0x87 +'', # 0x88 +'', # 0x89 +'', # 0x8a +'', # 0x8b +'', # 0x8c +'', # 0x8d +'', # 0x8e +'', # 0x8f +'', # 0x90 +'', # 0x91 +'', # 0x92 +'', # 0x93 +'', # 0x94 +'', # 0x95 +'', # 0x96 +'', # 0x97 +'', # 0x98 +'', # 0x99 +'', # 0x9a +'', # 0x9b +'', # 0x9c +'', # 0x9d +'', # 0x9e +'', # 0x9f +'', # 0xa0 +'', # 0xa1 +'', # 0xa2 +'', # 0xa3 +'', # 0xa4 +'', # 0xa5 +'', # 0xa6 +'', # 0xa7 +'', # 0xa8 +'', # 0xa9 +'', # 0xaa +'', # 0xab +'', # 0xac +'', # 0xad +'', # 0xae +'', # 0xaf +'', # 0xb0 +'', # 0xb1 +'', # 0xb2 +'', # 0xb3 +'', # 0xb4 +'', # 0xb5 +'', # 0xb6 +'', # 0xb7 +'', # 0xb8 +'', # 0xb9 +'', # 0xba +'', # 0xbb +'', # 0xbc +'', # 0xbd +'', # 0xbe +'', # 0xbf +'', # 0xc0 +'', # 0xc1 +'', # 0xc2 +'', # 0xc3 +'', # 0xc4 +'', # 0xc5 +'', # 0xc6 +'', # 0xc7 +'', # 0xc8 +'', # 0xc9 +'', # 0xca +'', # 0xcb +'', # 0xcc +'', # 0xcd +'', # 0xce +'', # 0xcf +'', # 0xd0 +'', # 0xd1 +'', # 0xd2 +'', # 0xd3 +'', # 0xd4 +'', # 0xd5 +'', # 0xd6 +'', # 0xd7 +'', # 0xd8 +'', # 0xd9 +'', # 0xda +'', # 0xdb +'', # 0xdc +'', # 0xdd +'', # 0xde +'', # 0xdf +'', # 0xe0 +'', # 0xe1 +'', # 0xe2 +'', # 0xe3 +'', # 0xe4 +'', # 0xe5 +'', # 0xe6 +'', # 0xe7 +'', # 0xe8 +'', # 0xe9 +'', # 0xea +'', # 0xeb +'', # 0xec +'', # 0xed +'', # 0xee +'', # 0xef +'', # 0xf0 +'', # 0xf1 +'', # 0xf2 +'', # 0xf3 +'', # 0xf4 +'', # 0xf5 +'', # 0xf6 +'', # 0xf7 +'', # 0xf8 +'', # 0xf9 +'', # 0xfa +'', # 0xfb +'', # 0xfc +'', # 0xfd +'', # 0xfe +) diff --git a/lib/unidecode/x032.py b/lib/unidecode/x032.py index 3295a25ca..a0c21d11e 100644 --- a/lib/unidecode/x032.py +++ b/lib/unidecode/x032.py @@ -80,21 +80,21 @@ data = ( '[?]', # 0x4e '[?]', # 0x4f '[?]', # 0x50 -'[?]', # 0x51 -'[?]', # 0x52 -'[?]', # 0x53 -'[?]', # 0x54 -'[?]', # 0x55 -'[?]', # 0x56 -'[?]', # 0x57 -'[?]', # 0x58 -'[?]', # 0x59 -'[?]', # 0x5a -'[?]', # 0x5b -'[?]', # 0x5c -'[?]', # 0x5d -'[?]', # 0x5e -'[?]', # 0x5f +'21', # 0x51 +'22', # 0x52 +'23', # 0x53 +'24', # 0x54 +'25', # 0x55 +'26', # 0x56 +'27', # 0x57 +'28', # 0x58 +'29', # 0x59 +'30', # 0x5a +'31', # 0x5b +'32', # 0x5c +'33', # 0x5d +'34', # 0x5e +'35', # 0x5f '(g)', # 0x60 '(n)', # 0x61 '(d)', # 0x62 @@ -176,21 +176,21 @@ data = ( '(Zi) ', # 0xae '(Xie) ', # 0xaf '(Ye) ', # 0xb0 -'[?]', # 0xb1 -'[?]', # 0xb2 -'[?]', # 0xb3 -'[?]', # 0xb4 -'[?]', # 0xb5 -'[?]', # 0xb6 -'[?]', # 0xb7 -'[?]', # 0xb8 -'[?]', # 0xb9 -'[?]', # 0xba -'[?]', # 0xbb -'[?]', # 0xbc -'[?]', # 0xbd -'[?]', # 0xbe -'[?]', # 0xbf +'36', # 0xb1 +'37', # 0xb2 +'38', # 0xb3 +'39', # 0xb4 +'40', # 0xb5 +'41', # 0xb6 +'42', # 0xb7 +'43', # 0xb8 +'44', # 0xb9 +'45', # 0xba +'46', # 0xbb +'47', # 0xbc +'48', # 0xbd +'49', # 0xbe +'50', # 0xbf '1M', # 0xc0 '2M', # 0xc1 '3M', # 0xc2 @@ -203,10 +203,10 @@ data = ( '10M', # 0xc9 '11M', # 0xca '12M', # 0xcb -'[?]', # 0xcc -'[?]', # 0xcd -'[?]', # 0xce -'[?]', # 0xcf +'Hg', # 0xcc +'erg', # 0xcd +'eV', # 0xce +'LTD', # 0xcf 'a', # 0xd0 'i', # 0xd1 'u', # 0xd2 diff --git a/lib/unidecode/x033.py b/lib/unidecode/x033.py index 64eb651a7..b9536832d 100644 --- a/lib/unidecode/x033.py +++ b/lib/unidecode/x033.py @@ -112,16 +112,16 @@ data = ( '22h', # 0x6e '23h', # 0x6f '24h', # 0x70 -'HPA', # 0x71 +'hPa', # 0x71 'da', # 0x72 'AU', # 0x73 'bar', # 0x74 'oV', # 0x75 'pc', # 0x76 -'[?]', # 0x77 -'[?]', # 0x78 -'[?]', # 0x79 -'[?]', # 0x7a +'dm', # 0x77 +'dm^2', # 0x78 +'dm^3', # 0x79 +'IU', # 0x7a 'Heisei', # 0x7b 'Syouwa', # 0x7c 'Taisyou', # 0x7d @@ -162,7 +162,7 @@ data = ( 'cm^2', # 0xa0 'm^2', # 0xa1 'km^2', # 0xa2 -'mm^4', # 0xa3 +'mm^3', # 0xa3 'cm^3', # 0xa4 'm^3', # 0xa5 'km^3', # 0xa6 @@ -254,4 +254,5 @@ data = ( '29d', # 0xfc '30d', # 0xfd '31d', # 0xfe +'gal', # 0xff ) diff --git a/lib/unidecode/x04e.py b/lib/unidecode/x04e.py index e346f67bf..b472b8559 100644 --- a/lib/unidecode/x04e.py +++ b/lib/unidecode/x04e.py @@ -1,5 +1,5 @@ data = ( -'[?] ', # 0x00 +'Yi ', # 0x00 'Ding ', # 0x01 'Kao ', # 0x02 'Qi ', # 0x03 diff --git a/lib/unidecode/x1f1.py b/lib/unidecode/x1f1.py new file mode 100644 index 000000000..ba0481fcf --- /dev/null +++ b/lib/unidecode/x1f1.py @@ -0,0 +1,258 @@ +data = ( +'0.', # 0x00 +'0,', # 0x01 +'1,', # 0x02 +'2,', # 0x03 +'3,', # 0x04 +'4,', # 0x05 +'5,', # 0x06 +'6,', # 0x07 +'7,', # 0x08 +'8,', # 0x09 +'9,', # 0x0a +'', # 0x0b +'', # 0x0c +'', # 0x0d +'', # 0x0e +'', # 0x0f +'(A)', # 0x10 +'(B)', # 0x11 +'(C)', # 0x12 +'(D)', # 0x13 +'(E)', # 0x14 +'(F)', # 0x15 +'(G)', # 0x16 +'(H)', # 0x17 +'(I)', # 0x18 +'(J)', # 0x19 +'(K)', # 0x1a +'(L)', # 0x1b +'(M)', # 0x1c +'(N)', # 0x1d +'(O)', # 0x1e +'(P)', # 0x1f +'(Q)', # 0x20 +'(R)', # 0x21 +'(S)', # 0x22 +'(T)', # 0x23 +'(U)', # 0x24 +'(V)', # 0x25 +'(W)', # 0x26 +'(X)', # 0x27 +'(Y)', # 0x28 +'(Z)', # 0x29 +'', # 0x2a +'', # 0x2b +'', # 0x2c +'', # 0x2d +'', # 0x2e +'', # 0x2f +'', # 0x30 +'', # 0x31 +'', # 0x32 +'', # 0x33 +'', # 0x34 +'', # 0x35 +'', # 0x36 +'', # 0x37 +'', # 0x38 +'', # 0x39 +'', # 0x3a +'', # 0x3b +'', # 0x3c +'', # 0x3d +'', # 0x3e +'', # 0x3f +'', # 0x40 +'', # 0x41 +'', # 0x42 +'', # 0x43 +'', # 0x44 +'', # 0x45 +'', # 0x46 +'', # 0x47 +'', # 0x48 +'', # 0x49 +'', # 0x4a +'', # 0x4b +'', # 0x4c +'', # 0x4d +'', # 0x4e +'', # 0x4f +'', # 0x50 +'', # 0x51 +'', # 0x52 +'', # 0x53 +'', # 0x54 +'', # 0x55 +'', # 0x56 +'', # 0x57 +'', # 0x58 +'', # 0x59 +'', # 0x5a +'', # 0x5b +'', # 0x5c +'', # 0x5d +'', # 0x5e +'', # 0x5f +'', # 0x60 +'', # 0x61 +'', # 0x62 +'', # 0x63 +'', # 0x64 +'', # 0x65 +'', # 0x66 +'', # 0x67 +'', # 0x68 +'', # 0x69 +'', # 0x6a +'', # 0x6b +'', # 0x6c +'', # 0x6d +'', # 0x6e +'', # 0x6f +'', # 0x70 +'', # 0x71 +'', # 0x72 +'', # 0x73 +'', # 0x74 +'', # 0x75 +'', # 0x76 +'', # 0x77 +'', # 0x78 +'', # 0x79 +'', # 0x7a +'', # 0x7b +'', # 0x7c +'', # 0x7d +'', # 0x7e +'', # 0x7f +'', # 0x80 +'', # 0x81 +'', # 0x82 +'', # 0x83 +'', # 0x84 +'', # 0x85 +'', # 0x86 +'', # 0x87 +'', # 0x88 +'', # 0x89 +'', # 0x8a +'', # 0x8b +'', # 0x8c +'', # 0x8d +'', # 0x8e +'', # 0x8f +'', # 0x90 +'', # 0x91 +'', # 0x92 +'', # 0x93 +'', # 0x94 +'', # 0x95 +'', # 0x96 +'', # 0x97 +'', # 0x98 +'', # 0x99 +'', # 0x9a +'', # 0x9b +'', # 0x9c +'', # 0x9d +'', # 0x9e +'', # 0x9f +'', # 0xa0 +'', # 0xa1 +'', # 0xa2 +'', # 0xa3 +'', # 0xa4 +'', # 0xa5 +'', # 0xa6 +'', # 0xa7 +'', # 0xa8 +'', # 0xa9 +'', # 0xaa +'', # 0xab +'', # 0xac +'', # 0xad +'', # 0xae +'', # 0xaf +'', # 0xb0 +'', # 0xb1 +'', # 0xb2 +'', # 0xb3 +'', # 0xb4 +'', # 0xb5 +'', # 0xb6 +'', # 0xb7 +'', # 0xb8 +'', # 0xb9 +'', # 0xba +'', # 0xbb +'', # 0xbc +'', # 0xbd +'', # 0xbe +'', # 0xbf +'', # 0xc0 +'', # 0xc1 +'', # 0xc2 +'', # 0xc3 +'', # 0xc4 +'', # 0xc5 +'', # 0xc6 +'', # 0xc7 +'', # 0xc8 +'', # 0xc9 +'', # 0xca +'', # 0xcb +'', # 0xcc +'', # 0xcd +'', # 0xce +'', # 0xcf +'', # 0xd0 +'', # 0xd1 +'', # 0xd2 +'', # 0xd3 +'', # 0xd4 +'', # 0xd5 +'', # 0xd6 +'', # 0xd7 +'', # 0xd8 +'', # 0xd9 +'', # 0xda +'', # 0xdb +'', # 0xdc +'', # 0xdd +'', # 0xde +'', # 0xdf +'', # 0xe0 +'', # 0xe1 +'', # 0xe2 +'', # 0xe3 +'', # 0xe4 +'', # 0xe5 +'', # 0xe6 +'', # 0xe7 +'', # 0xe8 +'', # 0xe9 +'', # 0xea +'', # 0xeb +'', # 0xec +'', # 0xed +'', # 0xee +'', # 0xef +'', # 0xf0 +'', # 0xf1 +'', # 0xf2 +'', # 0xf3 +'', # 0xf4 +'', # 0xf5 +'', # 0xf6 +'', # 0xf7 +'', # 0xf8 +'', # 0xf9 +'', # 0xfa +'', # 0xfb +'', # 0xfc +'', # 0xfd +'', # 0xfe +'', # 0xff +) diff --git a/lib/win_inet_pton.py b/lib/win_inet_pton.py new file mode 100644 index 000000000..12aaf46c4 --- /dev/null +++ b/lib/win_inet_pton.py @@ -0,0 +1,84 @@ +# This software released into the public domain. Anyone is free to copy, +# modify, publish, use, compile, sell, or distribute this software, +# either in source code form or as a compiled binary, for any purpose, +# commercial or non-commercial, and by any means. + +import socket +import ctypes +import os + + +class sockaddr(ctypes.Structure): + _fields_ = [("sa_family", ctypes.c_short), + ("__pad1", ctypes.c_ushort), + ("ipv4_addr", ctypes.c_byte * 4), + ("ipv6_addr", ctypes.c_byte * 16), + ("__pad2", ctypes.c_ulong)] + +if hasattr(ctypes, 'windll'): + WSAStringToAddressA = ctypes.windll.ws2_32.WSAStringToAddressA + WSAAddressToStringA = ctypes.windll.ws2_32.WSAAddressToStringA +else: + def not_windows(): + raise SystemError( + "Invalid platform. ctypes.windll must be available." + ) + WSAStringToAddressA = not_windows + WSAAddressToStringA = not_windows + + +def inet_pton(address_family, ip_string): + addr = sockaddr() + addr.sa_family = address_family + addr_size = ctypes.c_int(ctypes.sizeof(addr)) + + if WSAStringToAddressA( + ip_string, + address_family, + None, + ctypes.byref(addr), + ctypes.byref(addr_size) + ) != 0: + raise socket.error(ctypes.FormatError()) + + if address_family == socket.AF_INET: + return ctypes.string_at(addr.ipv4_addr, 4) + if address_family == socket.AF_INET6: + return ctypes.string_at(addr.ipv6_addr, 16) + + raise socket.error('unknown address family') + + +def inet_ntop(address_family, packed_ip): + addr = sockaddr() + addr.sa_family = address_family + addr_size = ctypes.c_int(ctypes.sizeof(addr)) + ip_string = ctypes.create_string_buffer(128) + ip_string_size = ctypes.c_int(ctypes.sizeof(ip_string)) + + if address_family == socket.AF_INET: + if len(packed_ip) != ctypes.sizeof(addr.ipv4_addr): + raise socket.error('packed IP wrong length for inet_ntoa') + ctypes.memmove(addr.ipv4_addr, packed_ip, 4) + elif address_family == socket.AF_INET6: + if len(packed_ip) != ctypes.sizeof(addr.ipv6_addr): + raise socket.error('packed IP wrong length for inet_ntoa') + ctypes.memmove(addr.ipv6_addr, packed_ip, 16) + else: + raise socket.error('unknown address family') + + if WSAAddressToStringA( + ctypes.byref(addr), + addr_size, + None, + ip_string, + ctypes.byref(ip_string_size) + ) != 0: + raise socket.error(ctypes.FormatError()) + + return ip_string[:ip_string_size.value - 1] + +# Adding our two functions to the socket library +if os.name == 'nt': + socket.inet_pton = inet_pton + socket.inet_ntop = inet_ntop diff --git a/lib/xmltodict.py b/lib/xmltodict.py index 746a4bcd7..ce532d91d 100644 --- a/lib/xmltodict.py +++ b/lib/xmltodict.py @@ -1,7 +1,10 @@ #!/usr/bin/env python "Makes working with XML feel like you are working with JSON" -from xml.parsers import expat +try: + from defusedexpat import pyexpat as expat +except ImportError: + from xml.parsers import expat from xml.sax.saxutils import XMLGenerator from xml.sax.xmlreader import AttributesImpl try: # pragma no cover @@ -29,7 +32,7 @@ except NameError: # pragma no cover _unicode = str __author__ = 'Martin Blech' -__version__ = '0.9.2' +__version__ = '0.11.0' __license__ = 'MIT' @@ -50,10 +53,11 @@ class _DictSAXHandler(object): dict_constructor=OrderedDict, strip_whitespace=True, namespace_separator=':', - namespaces=None): + namespaces=None, + force_list=None): self.path = [] self.stack = [] - self.data = None + self.data = [] self.item = None self.item_depth = item_depth self.xml_attribs = xml_attribs @@ -67,6 +71,8 @@ class _DictSAXHandler(object): self.strip_whitespace = strip_whitespace self.namespace_separator = namespace_separator self.namespaces = namespaces + self.namespace_declarations = OrderedDict() + self.force_list = force_list def _build_name(self, full_name): if not self.namespaces: @@ -86,34 +92,51 @@ class _DictSAXHandler(object): return attrs return self.dict_constructor(zip(attrs[0::2], attrs[1::2])) + def startNamespaceDecl(self, prefix, uri): + self.namespace_declarations[prefix or ''] = uri + def startElement(self, full_name, attrs): name = self._build_name(full_name) attrs = self._attrs_to_dict(attrs) + if attrs and self.namespace_declarations: + attrs['xmlns'] = self.namespace_declarations + self.namespace_declarations = OrderedDict() self.path.append((name, attrs or None)) if len(self.path) > self.item_depth: self.stack.append((self.item, self.data)) if self.xml_attribs: - attrs = self.dict_constructor( - (self.attr_prefix+self._build_name(key), value) - for (key, value) in attrs.items()) + attr_entries = [] + for key, value in attrs.items(): + key = self.attr_prefix+self._build_name(key) + if self.postprocessor: + entry = self.postprocessor(self.path, key, value) + else: + entry = (key, value) + if entry: + attr_entries.append(entry) + attrs = self.dict_constructor(attr_entries) else: attrs = None self.item = attrs or None - self.data = None + self.data = [] def endElement(self, full_name): name = self._build_name(full_name) if len(self.path) == self.item_depth: item = self.item if item is None: - item = self.data + item = (None if not self.data + else self.cdata_separator.join(self.data)) + should_continue = self.item_callback(self.path, item) if not should_continue: raise ParsingInterrupted() if len(self.stack): - item, data = self.item, self.data + data = (None if not self.data + else self.cdata_separator.join(self.data)) + item = self.item self.item, self.data = self.stack.pop() - if self.strip_whitespace and data is not None: + if self.strip_whitespace and data: data = data.strip() or None if data and self.force_cdata and item is None: item = self.dict_constructor() @@ -124,14 +147,15 @@ class _DictSAXHandler(object): else: self.item = self.push_data(self.item, name, data) else: - self.item = self.data = None + self.item = None + self.data = [] self.path.pop() def characters(self, data): if not self.data: - self.data = data + self.data = [data] else: - self.data += self.cdata_separator + data + self.data.append(data) def push_data(self, item, key, data): if self.postprocessor is not None: @@ -148,12 +172,23 @@ class _DictSAXHandler(object): else: item[key] = [value, data] except KeyError: - item[key] = data + if self._should_force_list(key, data): + item[key] = [data] + else: + item[key] = data return item + def _should_force_list(self, key, value): + if not self.force_list: + return False + try: + return key in self.force_list + except TypeError: + return self.force_list(self.path[:-1], key, value) + def parse(xml_input, encoding=None, expat=expat, process_namespaces=False, - namespace_separator=':', **kwargs): + namespace_separator=':', disable_entities=True, **kwargs): """Parse the given XML input and convert it into a dictionary. `xml_input` can either be a `string` or a file-like object. @@ -189,7 +224,7 @@ def parse(xml_input, encoding=None, expat=expat, process_namespaces=False, Streaming example:: >>> def handle(path, item): - ... print 'path:%s item:%s' % (path, item) + ... print('path:%s item:%s' % (path, item)) ... return True ... >>> xmltodict.parse(\"\"\" @@ -220,6 +255,41 @@ def parse(xml_input, encoding=None, expat=expat, process_namespaces=False, >>> xmltodict.parse('<a>hello</a>', expat=defusedexpat.pyexpat) OrderedDict([(u'a', u'hello')]) + You can use the force_list argument to force lists to be created even + when there is only a single child of a given level of hierarchy. The + force_list argument is a tuple of keys. If the key for a given level + of hierarchy is in the force_list argument, that level of hierarchy + will have a list as a child (even if there is only one sub-element). + The index_keys operation takes precendence over this. This is applied + after any user-supplied postprocessor has already run. + + For example, given this input: + <servers> + <server> + <name>host1</name> + <os>Linux</os> + <interfaces> + <interface> + <name>em0</name> + <ip_address>10.0.0.1</ip_address> + </interface> + </interfaces> + </server> + </servers> + + If called with force_list=('interface',), it will produce + this dictionary: + {'servers': + {'server': + {'name': 'host1', + 'os': 'Linux'}, + 'interfaces': + {'interface': + [ {'name': 'em0', 'ip_address': '10.0.0.1' } ] } } } + + `force_list` can also be a callable that receives `path`, `key` and + `value`. This is helpful in cases where the logic that decides whether + a list should be forced is more complex. """ handler = _DictSAXHandler(namespace_separator=namespace_separator, **kwargs) @@ -238,17 +308,44 @@ def parse(xml_input, encoding=None, expat=expat, process_namespaces=False, except AttributeError: # Jython's expat does not support ordered_attributes pass + parser.StartNamespaceDeclHandler = handler.startNamespaceDecl parser.StartElementHandler = handler.startElement parser.EndElementHandler = handler.endElement parser.CharacterDataHandler = handler.characters parser.buffer_text = True - try: + if disable_entities: + try: + # Attempt to disable DTD in Jython's expat parser (Xerces-J). + feature = "http://apache.org/xml/features/disallow-doctype-decl" + parser._reader.setFeature(feature, True) + except AttributeError: + # For CPython / expat parser. + # Anything not handled ends up here and entities aren't expanded. + parser.DefaultHandler = lambda x: None + # Expects an integer return; zero means failure -> expat.ExpatError. + parser.ExternalEntityRefHandler = lambda *x: 1 + if hasattr(xml_input, 'read'): parser.ParseFile(xml_input) - except (TypeError, AttributeError): + else: parser.Parse(xml_input, True) return handler.item +def _process_namespace(name, namespaces, ns_sep=':', attr_prefix='@'): + if not namespaces: + return name + try: + ns, name = name.rsplit(ns_sep, 1) + except ValueError: + pass + else: + ns_res = namespaces.get(ns.strip(attr_prefix)) + name = '{0}{1}{2}{3}'.format( + attr_prefix if ns.startswith(attr_prefix) else '', + ns_res, ns_sep, name) if ns_res else name + return name + + def _emit(key, value, content_handler, attr_prefix='@', cdata_key='#text', @@ -257,7 +354,10 @@ def _emit(key, value, content_handler, pretty=False, newl='\n', indent='\t', + namespace_separator=':', + namespaces=None, full_document=True): + key = _process_namespace(key, namespaces, namespace_separator, attr_prefix) if preprocessor is not None: result = preprocessor(key, value) if result is None: @@ -284,6 +384,15 @@ def _emit(key, value, content_handler, cdata = iv continue if ik.startswith(attr_prefix): + ik = _process_namespace(ik, namespaces, namespace_separator, + attr_prefix) + if ik == '@xmlns' and isinstance(iv, dict): + for k, v in iv.items(): + attr = 'xmlns{0}'.format(':{0}'.format(k) if k else '') + attrs[attr] = _unicode(v) + continue + if not isinstance(iv, _unicode): + iv = _unicode(iv) attrs[ik[len(attr_prefix):]] = iv continue children.append((ik, iv)) @@ -295,7 +404,8 @@ def _emit(key, value, content_handler, for child_key, child_value in children: _emit(child_key, child_value, content_handler, attr_prefix, cdata_key, depth+1, preprocessor, - pretty, newl, indent) + pretty, newl, indent, namespaces=namespaces, + namespace_separator=namespace_separator) if cdata is not None: content_handler.characters(cdata) if pretty and children: @@ -306,6 +416,7 @@ def _emit(key, value, content_handler, def unparse(input_dict, output=None, encoding='utf-8', full_document=True, + short_empty_elements=False, **kwargs): """Emit an XML document for the given `input_dict` (reverse of `parse`). @@ -327,7 +438,10 @@ def unparse(input_dict, output=None, encoding='utf-8', full_document=True, if output is None: output = StringIO() must_return = True - content_handler = XMLGenerator(output, encoding) + if short_empty_elements: + content_handler = XMLGenerator(output, encoding, True) + else: + content_handler = XMLGenerator(output, encoding) if full_document: content_handler.startDocument() for key, value in input_dict.items(): @@ -346,16 +460,23 @@ def unparse(input_dict, output=None, encoding='utf-8', full_document=True, if __name__ == '__main__': # pragma: no cover import sys import marshal + try: + stdin = sys.stdin.buffer + stdout = sys.stdout.buffer + except AttributeError: + stdin = sys.stdin + stdout = sys.stdout (item_depth,) = sys.argv[1:] item_depth = int(item_depth) + def handle_item(path, item): - marshal.dump((path, item), sys.stdout) + marshal.dump((path, item), stdout) return True try: - root = parse(sys.stdin, + root = parse(stdin, item_depth=item_depth, item_callback=handle_item, dict_constructor=dict) diff --git a/patches/feedparser.diff b/patches/feedparser.diff deleted file mode 100644 index 1fd3637bb..000000000 --- a/patches/feedparser.diff +++ /dev/null @@ -1,43 +0,0 @@ -diff --git a/lib/feedparser/api.py b/lib/feedparser/api.py -index 614bd2d..12eafd2 100644 ---- a/lib/feedparser/api.py -+++ b/lib/feedparser/api.py -@@ -60,6 +60,7 @@ from .sanitizer import replace_doctype - from .sgml import * - from .urls import _convert_to_idn, _makeSafeAbsoluteURI - from .util import FeedParserDict -+from . import USER_AGENT - - bytes_ = type(b'') - unicode_ = type('') -diff --git a/lib/feedparser/util.py b/lib/feedparser/util.py -index f7c02c0..df36b3e 100644 ---- a/lib/feedparser/util.py -+++ b/lib/feedparser/util.py -@@ -122,9 +122,23 @@ class FeedParserDict(dict): - - def __setitem__(self, key, value): - key = self.keymap.get(key, key) -- if isinstance(key, list): -- key = key[0] -- return dict.__setitem__(self, key, value) -+ if key == 'newznab_attr': -+ if isinstance(value, dict) and value.keys() == ['name', 'value']: -+ key = value['name'] -+ value = value['value'] -+ -+ if not dict.__contains__(self, 'categories'): -+ dict.__setitem__(self, 'categories', []) -+ -+ if key == 'category': -+ self['categories'].append(value) -+ else: -+ dict.__setitem__(self, key, value) -+ else: -+ if isinstance(key, list): -+ key = key[0] -+ -+ return dict.__setitem__(self, key, value) - - def setdefault(self, key, value): - if key not in self: diff --git a/sickbeard/logger.py b/sickbeard/logger.py index 38151be23..f3ac40cbc 100644 --- a/sickbeard/logger.py +++ b/sickbeard/logger.py @@ -38,7 +38,8 @@ from logging import NullHandler import sickbeard import six -from github import InputFileContent, RateLimitExceededException, TwoFactorException +from github import InputFileContent +from github.GithubException import RateLimitExceededException, TwoFactorException from sickbeard import classes from sickrage.helper.common import dateTimeFormat from sickrage.helper.encoding import ek, ss diff --git a/sickrage/helper/common.py b/sickrage/helper/common.py index 9d61f69e0..b53478c66 100644 --- a/sickrage/helper/common.py +++ b/sickrage/helper/common.py @@ -30,7 +30,8 @@ from fnmatch import fnmatch import sickbeard import six -from github import BadCredentialsException, Github, TwoFactorException +from github import Github +from github.GithubException import BadCredentialsException, TwoFactorException dateFormat = '%Y-%m-%d' dateTimeFormat = '%Y-%m-%d %H:%M:%S' -- GitLab