├── .gitignore ├── LICENSE ├── README.md ├── README.txt ├── build.py ├── gae-loader.py ├── setup.py ├── template ├── __init__.py ├── exceptions.py ├── lazy.py ├── reference.py ├── tests │ ├── test_docs.py │ ├── test_pytz_appengine.py │ └── test_tzinfo.py ├── tzfile.py ├── tzinfo.py └── zoneinfo.zip └── test_pytz_appengine.py /.gitignore: -------------------------------------------------------------------------------- 1 | pytz-* 2 | *.pyc 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2013 Brian M Hunt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PYTZ - Appengine 2 | ================ 3 | 4 | ### Note: As of Dec 1, 2016, AppEngine appears to natively include pytz 2016.4. 5 | 6 | 7 | Python Timezone for Google App Engine. 8 | 9 | There are some issues with pytz on Google App Engine, mostly performance related. 10 | The project [gae-pytz](https://code.google.com/p/gae-pytz/) addresses these 11 | performance issues but it has two issues itself: 12 | 13 | 1. it not been updated in some time, meaning the timezone data is out of date; 14 | and 15 | 2. it requires importing `pytz.gae` instead of just `pytz`, meaning that any 16 | existing code (e.g. `icalendar`) must be patched. 17 | 18 | This project aims to resolve these issues. It automatically builds a `pytz` by 19 | downloading the latest version from the 20 | [launchpad source](https://launchpad.net/pytz) and building a patched version. 21 | 22 | Also, instead of using the memcache approach of gae-pytz, pytz-appengine will 23 | put the timezone information into the datastore with `ndb`. This may or may not 24 | confer a performance advantage. 25 | 26 | ## Installation and usage 27 | 28 | To install: clone the repository, then from the command line run 29 | 30 | $ python build.py all 31 | 32 | This downloads the latest canonical `pytz` packages from PyPi and augments them 33 | by adding the code necessary to run on Google App Engine. 34 | 35 | The build process creates a directory `pytz`, which you can copy to your Google 36 | App Engine directory. This is the augmented pytz module, that ought to work by 37 | storing the timezone information in `ndb`. 38 | 39 | I have not created a PyPi package because it doesn't make sense in this 40 | context. One will never import anything directly from this package; it is 41 | essentially just a build script in python. 42 | 43 | Thoughts and feedback are welcome. 44 | 45 | ## License 46 | 47 | This project may be copied and otherwise used pursuant to the included MIT 48 | License. 49 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | 2 | `__` 3 | (°,°) Whoo! Whoo! 4 | /)_) Without me, a pytz unit test fails! 5 | ---”-“---------------------------------------------- 6 | -------------------------------------------------------------------------------- /build.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Download and patch the latest version of pytz 4 | """ 5 | import os 6 | import os.path 7 | import shutil 8 | import argparse 9 | 10 | PYTZ_OUTPUT = 'pytz' 11 | 12 | LATEST_OLSON = '2013.8' 13 | # 14 | # TODO: Slurp the latest URL from the pypi downloads page 15 | SRC_TEMPLATE = "https://pypi.python.org/packages/source/p/pytz/pytz-{}.zip" 16 | 17 | DONE_TEXT = """ 18 | 19 | The `pytz` for Google App Engine has been compiled, 20 | from: `{source}` 21 | 22 | Testing 23 | ~~~~~~~ 24 | 25 | You can test this with a test-runner such as `nosetests`. 26 | It is a good idea to set the log-level to INFO or higher 27 | and turning on color, for example by running: 28 | 29 | $ cd pytz 30 | $ nosetests --rednose --logging-level=INFO 31 | 32 | Installation 33 | ~~~~~~~~~~~~ 34 | 35 | The appengine-optimized version has been put into `{build_dir}`. 36 | 37 | You can install it by copying `{build_dir}` to your Google App Engine 38 | project. 39 | 40 | Usage 41 | ~~~~~ 42 | 43 | `pytz` should now work as it always has, but loading the timezones 44 | from the ndb datastore. 45 | 46 | If you update this package in your Google App Engine installation 47 | you can refresh the timezones by running `pytz.init_zoneinfo()` 48 | or alternatively by running in Google App Engine: 49 | 50 | ndb.Key('Zoneinfo', 'GMT', namespace='.pytz').delete() 51 | pytz.timezone('GMT') 52 | 53 | This will cause the zoneinfo to be refreshed. 54 | 55 | Note that deleted timezones will not be removed from the database 56 | (but they probably should). 57 | """ 58 | 59 | 60 | def download(args): 61 | """Get the latest pytz""" 62 | import urllib 63 | if args.release_url: 64 | source = args.release_url 65 | else: 66 | source = SRC_TEMPLATE.format(args.olson) 67 | dest = os.path.basename(source) 68 | print "Downloading %s" % source 69 | 70 | if os.path.exists(dest): 71 | print "File %s already exists." % dest 72 | else: 73 | urllib.urlretrieve(source, dest) 74 | print "Download complete." 75 | 76 | 77 | def compile(args): 78 | """Create a 'pytz' directory and create the appengine-compatible module. 79 | 80 | Copy over the bare minimum Python files (pytz/*.py) and put the zonefiles 81 | into a zip file. 82 | """ 83 | from zipfile import ZipFile, ZIP_DEFLATED 84 | 85 | if args.release_url: 86 | source = os.path.basename(args.release_url) 87 | else: 88 | source = os.path.basename(SRC_TEMPLATE.format(args.olson)) 89 | build_dir = args.build 90 | tests_dir = os.path.join(build_dir, 'tests') 91 | zone_file = os.path.join(build_dir, "zoneinfo.zip") 92 | 93 | print "Recreating pytz for appengine from %s into %s" % (source, build_dir) 94 | 95 | if not os.path.exists(build_dir): 96 | os.mkdir(build_dir) 97 | 98 | if not os.path.exists(tests_dir): 99 | os.mkdir(tests_dir) 100 | 101 | with ZipFile(source, 'r') as zf: 102 | 103 | # copy the source 104 | for zip_file_obj in ( 105 | zfi for zfi in zf.filelist 106 | if "/pytz/" in zfi.filename 107 | and zfi.filename.endswith(".py")): 108 | filename = zip_file_obj.filename # full path in the zip 109 | 110 | if 'test_' in filename: 111 | out_filename = '%s/%s' % ( 112 | tests_dir, os.path.basename(filename)) 113 | else: 114 | out_filename = "%s/%s" % ( 115 | build_dir, os.path.basename(filename)) 116 | 117 | if not os.path.exists(os.path.dirname(out_filename)): 118 | os.mkdir(os.path.dirname(out_filename)) 119 | 120 | with open(out_filename, 'w') as outfile: 121 | print "Copying %s" % out_filename 122 | outfile.write(zf.read(zip_file_obj)) 123 | 124 | # copy the zoneinfo to a new zip file 125 | with ZipFile(zone_file, "w", ZIP_DEFLATED) as out_zones: 126 | zonefiles = [ 127 | zfi for zfi in zf.filelist 128 | if "/pytz/zoneinfo" in zfi.filename] 129 | prefix = os.path.commonprefix([zfi.filename for zfi in zonefiles]) 130 | for zip_file_obj in zonefiles: 131 | # the destination zip will contain only the contents of the 132 | # zoneinfo directory e.g. 133 | # pytz-2013b/pytz/zoneinfo/America/Eastern 134 | # becoems America/Eastern in our zoneinfo.zip 135 | out_filename = os.path.relpath(zip_file_obj.filename, prefix) 136 | # print "Writing %s to %s" % (out_filename, zone_file) 137 | out_zones.writestr(out_filename, zf.read(zip_file_obj)) 138 | print "Created %s and added %s timezones" % ( 139 | zone_file, len(zonefiles)) 140 | 141 | print "Copying test file test_pytz_appengine.py to %s" % tests_dir 142 | shutil.copy("test_pytz_appengine.py", tests_dir) 143 | 144 | print "Files copied from %s to the %s directory" % ( 145 | source, build_dir) 146 | 147 | print "Augmenting %s/__init__.py with gae-loader.py" % build_dir 148 | 149 | init_file = os.path.join(build_dir, "__init__.py") 150 | loader_file = 'gae-loader.py' 151 | 152 | with file(init_file, 'r') as original: 153 | original_init = original.read() 154 | 155 | # rename open_resource and resource_exists, since we are hacking our own 156 | original_init = original_init.replace( 157 | "def open_resource(name):", "def __open_resource(name):") 158 | original_init = original_init.replace( 159 | "def resource_exists(name):", "def __resource_exists(name):") 160 | 161 | with open(init_file, "w") as init_out: 162 | with open(loader_file) as loader_in: 163 | init_out.write(loader_in.read()) 164 | 165 | # append the original __init__ 166 | init_out.write(original_init) 167 | 168 | if args.dir: 169 | print 'Automatically moving pytz...' 170 | 171 | destination_pytz = os.path.join(args.dir, 'pytz') 172 | 173 | if os.path.isdir(destination_pytz): 174 | shutil.rmtree(destination_pytz, ignore_errors=True) 175 | 176 | shutil.move('pytz', destination_pytz) 177 | 178 | print DONE_TEXT.format(source=source, build_dir=build_dir) 179 | 180 | 181 | def clean(args): 182 | """Erase all the compiled and downloaded documents, being 183 | pytz/* 184 | pytz-* 185 | """ 186 | from glob import glob 187 | 188 | print "Removing pytz- and pytz/*" 189 | for filename in glob("./pytz-*.zip"): 190 | print "unlink %s" % filename 191 | os.unlink(filename) 192 | 193 | for dirname in glob("./pytz-*"): 194 | print "rmtree %s" % dirname 195 | shutil.rmtree(dirname) 196 | 197 | print "rmtree %s" % args.build 198 | shutil.rmtree(args.build) 199 | 200 | 201 | def all(args): 202 | """Download and compile.""" 203 | download(args) 204 | compile(args) 205 | 206 | 207 | commands = dict(all=all, 208 | download=download, 209 | compile=compile, 210 | clean=clean, 211 | ) 212 | 213 | 214 | if __name__ == '__main__': 215 | parser = argparse.ArgumentParser(description='Update the pytz.') 216 | 217 | parser.add_argument( 218 | '--olson', dest='olson', default=LATEST_OLSON, 219 | help='The version of the pytz to use') 220 | 221 | parser.add_argument( 222 | '--release-url', dest='release_url', 223 | help='Explicit pytz release zip URL to download. Overrides --olson') 224 | 225 | parser.add_argument( 226 | '--dir', dest='dir', 227 | help='The directory to move the patched pytz to') 228 | 229 | parser.add_argument( 230 | '--build', dest='build', default=PYTZ_OUTPUT, 231 | help='The build directory where the updated pytz will be stored') 232 | 233 | parser.add_argument( 234 | 'command', help='Action to perform', 235 | choices=commands.keys()) 236 | 237 | args = parser.parse_args() 238 | 239 | command = commands[args.command] 240 | command(args) 241 | -------------------------------------------------------------------------------- /gae-loader.py: -------------------------------------------------------------------------------- 1 | """ 2 | The following is the Google App Engine loader for pytz. 3 | 4 | It is monkeypatched, prepending pytz/__init__.py 5 | 6 | Here are some helpful links discussing the problem: 7 | 8 | https://code.google.com/p/gae-pytz/source/browse/pytz/gae.py 9 | http://appengine-cookbook.appspot.com/recipe/caching-pytz-helper/ 10 | 11 | This is all based on the helpful gae-pytz project, here: 12 | 13 | https://code.google.com/p/gae-pytz/ 14 | """ 15 | 16 | # easy test to make sure we are running the appengine version 17 | APPENGINE_PYTZ = True 18 | 19 | # Put pytz into its own ndb namespace, so we avoid conflicts 20 | NDB_NAMESPACE = '.pytz' 21 | 22 | from google.appengine.ext import ndb 23 | 24 | 25 | class Zoneinfo(ndb.Model): 26 | """A model containing the zone info data 27 | """ 28 | data = ndb.BlobProperty(compressed=True) 29 | 30 | 31 | def init_zoneinfo(): 32 | """ 33 | Add each zone info to the datastore. This will overwrite existing zones. 34 | 35 | This must be called before the AppengineTimezoneLoader will work. 36 | """ 37 | import os 38 | import logging 39 | from zipfile import ZipFile 40 | zoneobjs = [] 41 | 42 | zoneinfo_path = os.path.abspath( 43 | os.path.join(os.path.dirname(__file__), 'zoneinfo.zip')) 44 | 45 | with ZipFile(zoneinfo_path) as zf: 46 | for zfi in zf.filelist: 47 | key = ndb.Key('Zoneinfo', zfi.filename, namespace=NDB_NAMESPACE) 48 | zobj = Zoneinfo(key=key, data=zf.read(zfi)) 49 | zoneobjs.append(zobj) 50 | 51 | logging.info( 52 | "Adding %d timezones to the pytz-appengine database" % 53 | len(zoneobjs)) 54 | 55 | ndb.put_multi(zoneobjs) 56 | 57 | 58 | def open_resource(name): 59 | """Load the object from the datastore""" 60 | import logging 61 | from cStringIO import StringIO 62 | try: 63 | data = ndb.Key('Zoneinfo', name, namespace=NDB_NAMESPACE).get().data 64 | except AttributeError: 65 | # Missing zone info; test for GMT 66 | # which would be there if the Zoneinfo has been initialized. 67 | if ndb.Key('Zoneinfo', 'GMT', namespace=NDB_NAMESPACE).get(): 68 | # the user asked for a zone that doesn't seem to exist. 69 | logging.exception( 70 | "Requested zone '%s' is not in the database." % name) 71 | raise 72 | 73 | # we need to initialize the database 74 | init_zoneinfo() 75 | return open_resource(name) 76 | 77 | return StringIO(data) 78 | 79 | 80 | def resource_exists(name): 81 | """Return true if the given timezone resource exists. 82 | Since we are loading the whole PyTZ database, this should always be true 83 | """ 84 | return True 85 | 86 | 87 | def setup_module(): 88 | """Set up tests (used by e.g. nosetests) for the module - loaded once""" 89 | from google.appengine.ext import testbed 90 | global _appengine_testbed 91 | tb = testbed.Testbed() 92 | tb.activate() 93 | tb.setup_env() 94 | tb.init_datastore_v3_stub() 95 | tb.init_memcache_stub() 96 | 97 | _appengine_testbed = tb 98 | 99 | 100 | def teardown_module(): 101 | """Any clean-up after each test""" 102 | global _appengine_testbed 103 | _appengine_testbed.deactivate() 104 | 105 | # 106 | # >>>>>>>>>>>>> 107 | # >>>>>>>>>>>>> end pytz-appengine augmentation 108 | # >>>>>>>>>>>>> 109 | # 110 | # The following shall be the canonical pytz/__init__.py 111 | # modified to remove open_resource and resource_exists 112 | # 113 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import json 4 | import sys 5 | import urllib2 6 | 7 | from setuptools import find_packages 8 | from setuptools import setup 9 | from setuptools.command.develop import develop 10 | from setuptools.command.install import install 11 | from subprocess import call 12 | 13 | 14 | def with_post_install(command): 15 | '''Runs the code in `modified_run` as a post-install step.''' 16 | original_run = command.run 17 | 18 | def modified_run(self): 19 | original_run(self) 20 | 21 | try: 22 | data = urllib2.urlopen('https://pypi.python.org/pypi/pytz/json').read() 23 | data = json.loads(data) 24 | except Exception: 25 | print 'Could not fetch latest pytz version! Falling back to default' 26 | else: 27 | latest_version = data['info']['version'] 28 | releases = data['releases'][latest_version] 29 | 30 | release_url = None 31 | 32 | for release in releases: 33 | if release['url'].endswith('.zip'): 34 | release_url = release['url'] 35 | 36 | if hasattr(self, 'install_libbase'): 37 | dir_ = self.install_libbase # setup.py install 38 | elif hasattr(self, 'install_dir'): 39 | dir_ = self.install_dir # setup.py develop 40 | 41 | call_args = [sys.executable, 'build.py', 'all', '--dir', dir_] 42 | 43 | if release_url: 44 | call_args.extend(['--release-url', release_url]) 45 | 46 | self.execute(lambda dir: call(call_args), (self.install_lib,), msg='Running post install task...') 47 | 48 | command.run = modified_run 49 | 50 | return command 51 | 52 | 53 | @with_post_install 54 | class Install(install): 55 | pass 56 | 57 | 58 | @with_post_install 59 | class Develop(develop): 60 | pass 61 | 62 | 63 | setup( 64 | name='pytz-appengine', 65 | packages=find_packages(), 66 | cmdclass={ 67 | 'install': Install, 68 | 'develop': Develop, 69 | }, 70 | ) 71 | -------------------------------------------------------------------------------- /template/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | The following is the Google App Engine loader for pytz. 3 | 4 | It is monkeypatched, prepending pytz/__init__.py 5 | 6 | Here are some helpful links discussing the problem: 7 | 8 | https://code.google.com/p/gae-pytz/source/browse/pytz/gae.py 9 | http://appengine-cookbook.appspot.com/recipe/caching-pytz-helper/ 10 | 11 | This is all based on the helpful gae-pytz project, here: 12 | 13 | https://code.google.com/p/gae-pytz/ 14 | """ 15 | 16 | # easy test to make sure we are running the appengine version 17 | APPENGINE_PYTZ = True 18 | 19 | # Put pytz into its own ndb namespace, so we avoid conflicts 20 | NDB_NAMESPACE = '.pytz' 21 | 22 | from google.appengine.ext import ndb 23 | 24 | 25 | class Zoneinfo(ndb.Model): 26 | """A model containing the zone info data 27 | """ 28 | data = ndb.BlobProperty(compressed=True) 29 | 30 | 31 | def init_zoneinfo(): 32 | """ 33 | Add each zone info to the datastore. This will overwrite existing zones. 34 | 35 | This must be called before the AppengineTimezoneLoader will work. 36 | """ 37 | import os 38 | import logging 39 | from zipfile import ZipFile 40 | zoneobjs = [] 41 | 42 | zoneinfo_path = os.path.abspath( 43 | os.path.join(os.path.dirname(__file__), 'zoneinfo.zip')) 44 | 45 | with ZipFile(zoneinfo_path) as zf: 46 | for zfi in zf.filelist: 47 | key = ndb.Key('Zoneinfo', zfi.filename, namespace=NDB_NAMESPACE) 48 | zobj = Zoneinfo(key=key, data=zf.read(zfi)) 49 | zoneobjs.append(zobj) 50 | 51 | logging.info( 52 | "Adding %d timezones to the pytz-appengine database" % 53 | len(zoneobjs)) 54 | 55 | ndb.put_multi(zoneobjs) 56 | 57 | 58 | def open_resource(name): 59 | """Load the object from the datastore""" 60 | import logging 61 | from cStringIO import StringIO 62 | try: 63 | data = ndb.Key('Zoneinfo', name, namespace=NDB_NAMESPACE).get().data 64 | except AttributeError: 65 | # Missing zone info; test for GMT 66 | # which would be there if the Zoneinfo has been initialized. 67 | if ndb.Key('Zoneinfo', 'GMT', namespace=NDB_NAMESPACE).get(): 68 | # the user asked for a zone that doesn't seem to exist. 69 | logging.exception( 70 | "Requested zone '%s' is not in the database." % name) 71 | raise 72 | 73 | # we need to initialize the database 74 | init_zoneinfo() 75 | return open_resource(name) 76 | 77 | return StringIO(data) 78 | 79 | 80 | def resource_exists(name): 81 | """Return true if the given timezone resource exists. 82 | Since we are loading the whole PyTZ database, this should always be true 83 | """ 84 | return True 85 | 86 | 87 | def setup_module(): 88 | """Set up tests (used by e.g. nosetests) for the module - loaded once""" 89 | from google.appengine.ext import testbed 90 | global _appengine_testbed 91 | tb = testbed.Testbed() 92 | tb.activate() 93 | tb.setup_env() 94 | tb.init_datastore_v3_stub() 95 | tb.init_memcache_stub() 96 | 97 | _appengine_testbed = tb 98 | 99 | 100 | def teardown_module(): 101 | """Any clean-up after each test""" 102 | global _appengine_testbed 103 | _appengine_testbed.deactivate() 104 | 105 | # 106 | # >>>>>>>>>>>>> 107 | # >>>>>>>>>>>>> end pytz-appengine augmentation 108 | # >>>>>>>>>>>>> 109 | # 110 | # The following shall be the canonical pytz/__init__.py 111 | # modified to remove open_resource and resource_exists 112 | # 113 | ''' 114 | datetime.tzinfo timezone definitions generated from the 115 | Olson timezone database: 116 | 117 | ftp://elsie.nci.nih.gov/pub/tz*.tar.gz 118 | 119 | See the datetime section of the Python Library Reference for information 120 | on how to use these modules. 121 | ''' 122 | 123 | # The Olson database is updated several times a year. 124 | OLSON_VERSION = '2013h' 125 | VERSION = '2013.8' # Switching to pip compatible version numbering. 126 | __version__ = VERSION 127 | 128 | OLSEN_VERSION = OLSON_VERSION # Old releases had this misspelling 129 | 130 | __all__ = [ 131 | 'timezone', 'utc', 'country_timezones', 'country_names', 132 | 'AmbiguousTimeError', 'InvalidTimeError', 133 | 'NonExistentTimeError', 'UnknownTimeZoneError', 134 | 'all_timezones', 'all_timezones_set', 135 | 'common_timezones', 'common_timezones_set', 136 | ] 137 | 138 | import sys, datetime, os.path, gettext 139 | 140 | try: 141 | from pkg_resources import resource_stream 142 | except ImportError: 143 | resource_stream = None 144 | 145 | from pytz.exceptions import AmbiguousTimeError 146 | from pytz.exceptions import InvalidTimeError 147 | from pytz.exceptions import NonExistentTimeError 148 | from pytz.exceptions import UnknownTimeZoneError 149 | from pytz.lazy import LazyDict, LazyList, LazySet 150 | from pytz.tzinfo import unpickler 151 | from pytz.tzfile import build_tzinfo, _byte_string 152 | 153 | 154 | try: 155 | unicode 156 | 157 | except NameError: # Python 3.x 158 | 159 | # Python 3.x doesn't have unicode(), making writing code 160 | # for Python 2.3 and Python 3.x a pain. 161 | unicode = str 162 | 163 | def ascii(s): 164 | r""" 165 | >>> ascii('Hello') 166 | 'Hello' 167 | >>> ascii('\N{TRADE MARK SIGN}') #doctest: +IGNORE_EXCEPTION_DETAIL 168 | Traceback (most recent call last): 169 | ... 170 | UnicodeEncodeError: ... 171 | """ 172 | s.encode('US-ASCII') # Raise an exception if not ASCII 173 | return s # But return the original string - not a byte string. 174 | 175 | else: # Python 2.x 176 | 177 | def ascii(s): 178 | r""" 179 | >>> ascii('Hello') 180 | 'Hello' 181 | >>> ascii(u'Hello') 182 | 'Hello' 183 | >>> ascii(u'\N{TRADE MARK SIGN}') #doctest: +IGNORE_EXCEPTION_DETAIL 184 | Traceback (most recent call last): 185 | ... 186 | UnicodeEncodeError: ... 187 | """ 188 | return s.encode('US-ASCII') 189 | 190 | 191 | def __open_resource(name): 192 | """Open a resource from the zoneinfo subdir for reading. 193 | 194 | Uses the pkg_resources module if available and no standard file 195 | found at the calculated location. 196 | """ 197 | name_parts = name.lstrip('/').split('/') 198 | for part in name_parts: 199 | if part == os.path.pardir or os.path.sep in part: 200 | raise ValueError('Bad path segment: %r' % part) 201 | filename = os.path.join(os.path.dirname(__file__), 202 | 'zoneinfo', *name_parts) 203 | if not os.path.exists(filename) and resource_stream is not None: 204 | # http://bugs.launchpad.net/bugs/383171 - we avoid using this 205 | # unless absolutely necessary to help when a broken version of 206 | # pkg_resources is installed. 207 | return resource_stream(__name__, 'zoneinfo/' + name) 208 | return open(filename, 'rb') 209 | 210 | 211 | def __resource_exists(name): 212 | """Return true if the given resource exists""" 213 | try: 214 | open_resource(name).close() 215 | return True 216 | except IOError: 217 | return False 218 | 219 | 220 | # Enable this when we get some translations? 221 | # We want an i18n API that is useful to programs using Python's gettext 222 | # module, as well as the Zope3 i18n package. Perhaps we should just provide 223 | # the POT file and translations, and leave it up to callers to make use 224 | # of them. 225 | # 226 | # t = gettext.translation( 227 | # 'pytz', os.path.join(os.path.dirname(__file__), 'locales'), 228 | # fallback=True 229 | # ) 230 | # def _(timezone_name): 231 | # """Translate a timezone name using the current locale, returning Unicode""" 232 | # return t.ugettext(timezone_name) 233 | 234 | 235 | _tzinfo_cache = {} 236 | 237 | def timezone(zone): 238 | r''' Return a datetime.tzinfo implementation for the given timezone 239 | 240 | >>> from datetime import datetime, timedelta 241 | >>> utc = timezone('UTC') 242 | >>> eastern = timezone('US/Eastern') 243 | >>> eastern.zone 244 | 'US/Eastern' 245 | >>> timezone(unicode('US/Eastern')) is eastern 246 | True 247 | >>> utc_dt = datetime(2002, 10, 27, 6, 0, 0, tzinfo=utc) 248 | >>> loc_dt = utc_dt.astimezone(eastern) 249 | >>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)' 250 | >>> loc_dt.strftime(fmt) 251 | '2002-10-27 01:00:00 EST (-0500)' 252 | >>> (loc_dt - timedelta(minutes=10)).strftime(fmt) 253 | '2002-10-27 00:50:00 EST (-0500)' 254 | >>> eastern.normalize(loc_dt - timedelta(minutes=10)).strftime(fmt) 255 | '2002-10-27 01:50:00 EDT (-0400)' 256 | >>> (loc_dt + timedelta(minutes=10)).strftime(fmt) 257 | '2002-10-27 01:10:00 EST (-0500)' 258 | 259 | Raises UnknownTimeZoneError if passed an unknown zone. 260 | 261 | >>> try: 262 | ... timezone('Asia/Shangri-La') 263 | ... except UnknownTimeZoneError: 264 | ... print('Unknown') 265 | Unknown 266 | 267 | >>> try: 268 | ... timezone(unicode('\N{TRADE MARK SIGN}')) 269 | ... except UnknownTimeZoneError: 270 | ... print('Unknown') 271 | Unknown 272 | 273 | ''' 274 | if zone.upper() == 'UTC': 275 | return utc 276 | 277 | try: 278 | zone = ascii(zone) 279 | except UnicodeEncodeError: 280 | # All valid timezones are ASCII 281 | raise UnknownTimeZoneError(zone) 282 | 283 | zone = _unmunge_zone(zone) 284 | if zone not in _tzinfo_cache: 285 | if zone in all_timezones_set: 286 | fp = open_resource(zone) 287 | try: 288 | _tzinfo_cache[zone] = build_tzinfo(zone, fp) 289 | finally: 290 | fp.close() 291 | else: 292 | raise UnknownTimeZoneError(zone) 293 | 294 | return _tzinfo_cache[zone] 295 | 296 | 297 | def _unmunge_zone(zone): 298 | """Undo the time zone name munging done by older versions of pytz.""" 299 | return zone.replace('_plus_', '+').replace('_minus_', '-') 300 | 301 | 302 | ZERO = datetime.timedelta(0) 303 | HOUR = datetime.timedelta(hours=1) 304 | 305 | 306 | class UTC(datetime.tzinfo): 307 | """UTC 308 | 309 | Optimized UTC implementation. It unpickles using the single module global 310 | instance defined beneath this class declaration. 311 | """ 312 | zone = "UTC" 313 | 314 | _utcoffset = ZERO 315 | _dst = ZERO 316 | _tzname = zone 317 | 318 | def fromutc(self, dt): 319 | if dt.tzinfo is None: 320 | return self.localize(dt) 321 | return super(utc.__class__, self).fromutc(dt) 322 | 323 | def utcoffset(self, dt): 324 | return ZERO 325 | 326 | def tzname(self, dt): 327 | return "UTC" 328 | 329 | def dst(self, dt): 330 | return ZERO 331 | 332 | def __reduce__(self): 333 | return _UTC, () 334 | 335 | def localize(self, dt, is_dst=False): 336 | '''Convert naive time to local time''' 337 | if dt.tzinfo is not None: 338 | raise ValueError('Not naive datetime (tzinfo is already set)') 339 | return dt.replace(tzinfo=self) 340 | 341 | def normalize(self, dt, is_dst=False): 342 | '''Correct the timezone information on the given datetime''' 343 | if dt.tzinfo is self: 344 | return dt 345 | if dt.tzinfo is None: 346 | raise ValueError('Naive time - no tzinfo set') 347 | return dt.astimezone(self) 348 | 349 | def __repr__(self): 350 | return "" 351 | 352 | def __str__(self): 353 | return "UTC" 354 | 355 | 356 | UTC = utc = UTC() # UTC is a singleton 357 | 358 | 359 | def _UTC(): 360 | """Factory function for utc unpickling. 361 | 362 | Makes sure that unpickling a utc instance always returns the same 363 | module global. 364 | 365 | These examples belong in the UTC class above, but it is obscured; or in 366 | the README.txt, but we are not depending on Python 2.4 so integrating 367 | the README.txt examples with the unit tests is not trivial. 368 | 369 | >>> import datetime, pickle 370 | >>> dt = datetime.datetime(2005, 3, 1, 14, 13, 21, tzinfo=utc) 371 | >>> naive = dt.replace(tzinfo=None) 372 | >>> p = pickle.dumps(dt, 1) 373 | >>> naive_p = pickle.dumps(naive, 1) 374 | >>> len(p) - len(naive_p) 375 | 17 376 | >>> new = pickle.loads(p) 377 | >>> new == dt 378 | True 379 | >>> new is dt 380 | False 381 | >>> new.tzinfo is dt.tzinfo 382 | True 383 | >>> utc is UTC is timezone('UTC') 384 | True 385 | >>> utc is timezone('GMT') 386 | False 387 | """ 388 | return utc 389 | _UTC.__safe_for_unpickling__ = True 390 | 391 | 392 | def _p(*args): 393 | """Factory function for unpickling pytz tzinfo instances. 394 | 395 | Just a wrapper around tzinfo.unpickler to save a few bytes in each pickle 396 | by shortening the path. 397 | """ 398 | return unpickler(*args) 399 | _p.__safe_for_unpickling__ = True 400 | 401 | 402 | 403 | class _CountryTimezoneDict(LazyDict): 404 | """Map ISO 3166 country code to a list of timezone names commonly used 405 | in that country. 406 | 407 | iso3166_code is the two letter code used to identify the country. 408 | 409 | >>> def print_list(list_of_strings): 410 | ... 'We use a helper so doctests work under Python 2.3 -> 3.x' 411 | ... for s in list_of_strings: 412 | ... print(s) 413 | 414 | >>> print_list(country_timezones['nz']) 415 | Pacific/Auckland 416 | Pacific/Chatham 417 | >>> print_list(country_timezones['ch']) 418 | Europe/Zurich 419 | >>> print_list(country_timezones['CH']) 420 | Europe/Zurich 421 | >>> print_list(country_timezones[unicode('ch')]) 422 | Europe/Zurich 423 | >>> print_list(country_timezones['XXX']) 424 | Traceback (most recent call last): 425 | ... 426 | KeyError: 'XXX' 427 | 428 | Previously, this information was exposed as a function rather than a 429 | dictionary. This is still supported:: 430 | 431 | >>> print_list(country_timezones('nz')) 432 | Pacific/Auckland 433 | Pacific/Chatham 434 | """ 435 | def __call__(self, iso3166_code): 436 | """Backwards compatibility.""" 437 | return self[iso3166_code] 438 | 439 | def _fill(self): 440 | data = {} 441 | zone_tab = open_resource('zone.tab') 442 | try: 443 | for line in zone_tab: 444 | line = line.decode('US-ASCII') 445 | if line.startswith('#'): 446 | continue 447 | code, coordinates, zone = line.split(None, 4)[:3] 448 | if zone not in all_timezones_set: 449 | continue 450 | try: 451 | data[code].append(zone) 452 | except KeyError: 453 | data[code] = [zone] 454 | self.data = data 455 | finally: 456 | zone_tab.close() 457 | 458 | country_timezones = _CountryTimezoneDict() 459 | 460 | 461 | class _CountryNameDict(LazyDict): 462 | '''Dictionary proving ISO3166 code -> English name. 463 | 464 | >>> print(country_names['au']) 465 | Australia 466 | ''' 467 | def _fill(self): 468 | data = {} 469 | zone_tab = open_resource('iso3166.tab') 470 | try: 471 | for line in zone_tab.readlines(): 472 | line = line.decode('US-ASCII') 473 | if line.startswith('#'): 474 | continue 475 | code, name = line.split(None, 1) 476 | data[code] = name.strip() 477 | self.data = data 478 | finally: 479 | zone_tab.close() 480 | 481 | country_names = _CountryNameDict() 482 | 483 | 484 | # Time-zone info based solely on fixed offsets 485 | 486 | class _FixedOffset(datetime.tzinfo): 487 | 488 | zone = None # to match the standard pytz API 489 | 490 | def __init__(self, minutes): 491 | if abs(minutes) >= 1440: 492 | raise ValueError("absolute offset is too large", minutes) 493 | self._minutes = minutes 494 | self._offset = datetime.timedelta(minutes=minutes) 495 | 496 | def utcoffset(self, dt): 497 | return self._offset 498 | 499 | def __reduce__(self): 500 | return FixedOffset, (self._minutes, ) 501 | 502 | def dst(self, dt): 503 | return ZERO 504 | 505 | def tzname(self, dt): 506 | return None 507 | 508 | def __repr__(self): 509 | return 'pytz.FixedOffset(%d)' % self._minutes 510 | 511 | def localize(self, dt, is_dst=False): 512 | '''Convert naive time to local time''' 513 | if dt.tzinfo is not None: 514 | raise ValueError('Not naive datetime (tzinfo is already set)') 515 | return dt.replace(tzinfo=self) 516 | 517 | def normalize(self, dt, is_dst=False): 518 | '''Correct the timezone information on the given datetime''' 519 | if dt.tzinfo is None: 520 | raise ValueError('Naive time - no tzinfo set') 521 | return dt.replace(tzinfo=self) 522 | 523 | 524 | def FixedOffset(offset, _tzinfos = {}): 525 | """return a fixed-offset timezone based off a number of minutes. 526 | 527 | >>> one = FixedOffset(-330) 528 | >>> one 529 | pytz.FixedOffset(-330) 530 | >>> one.utcoffset(datetime.datetime.now()) 531 | datetime.timedelta(-1, 66600) 532 | >>> one.dst(datetime.datetime.now()) 533 | datetime.timedelta(0) 534 | 535 | >>> two = FixedOffset(1380) 536 | >>> two 537 | pytz.FixedOffset(1380) 538 | >>> two.utcoffset(datetime.datetime.now()) 539 | datetime.timedelta(0, 82800) 540 | >>> two.dst(datetime.datetime.now()) 541 | datetime.timedelta(0) 542 | 543 | The datetime.timedelta must be between the range of -1 and 1 day, 544 | non-inclusive. 545 | 546 | >>> FixedOffset(1440) 547 | Traceback (most recent call last): 548 | ... 549 | ValueError: ('absolute offset is too large', 1440) 550 | 551 | >>> FixedOffset(-1440) 552 | Traceback (most recent call last): 553 | ... 554 | ValueError: ('absolute offset is too large', -1440) 555 | 556 | An offset of 0 is special-cased to return UTC. 557 | 558 | >>> FixedOffset(0) is UTC 559 | True 560 | 561 | There should always be only one instance of a FixedOffset per timedelta. 562 | This should be true for multiple creation calls. 563 | 564 | >>> FixedOffset(-330) is one 565 | True 566 | >>> FixedOffset(1380) is two 567 | True 568 | 569 | It should also be true for pickling. 570 | 571 | >>> import pickle 572 | >>> pickle.loads(pickle.dumps(one)) is one 573 | True 574 | >>> pickle.loads(pickle.dumps(two)) is two 575 | True 576 | """ 577 | if offset == 0: 578 | return UTC 579 | 580 | info = _tzinfos.get(offset) 581 | if info is None: 582 | # We haven't seen this one before. we need to save it. 583 | 584 | # Use setdefault to avoid a race condition and make sure we have 585 | # only one 586 | info = _tzinfos.setdefault(offset, _FixedOffset(offset)) 587 | 588 | return info 589 | 590 | FixedOffset.__safe_for_unpickling__ = True 591 | 592 | 593 | def _test(): 594 | import doctest, os, sys 595 | sys.path.insert(0, os.pardir) 596 | import pytz 597 | return doctest.testmod(pytz) 598 | 599 | if __name__ == '__main__': 600 | _test() 601 | 602 | all_timezones = \ 603 | ['Africa/Abidjan', 604 | 'Africa/Accra', 605 | 'Africa/Addis_Ababa', 606 | 'Africa/Algiers', 607 | 'Africa/Asmara', 608 | 'Africa/Asmera', 609 | 'Africa/Bamako', 610 | 'Africa/Bangui', 611 | 'Africa/Banjul', 612 | 'Africa/Bissau', 613 | 'Africa/Blantyre', 614 | 'Africa/Brazzaville', 615 | 'Africa/Bujumbura', 616 | 'Africa/Cairo', 617 | 'Africa/Casablanca', 618 | 'Africa/Ceuta', 619 | 'Africa/Conakry', 620 | 'Africa/Dakar', 621 | 'Africa/Dar_es_Salaam', 622 | 'Africa/Djibouti', 623 | 'Africa/Douala', 624 | 'Africa/El_Aaiun', 625 | 'Africa/Freetown', 626 | 'Africa/Gaborone', 627 | 'Africa/Harare', 628 | 'Africa/Johannesburg', 629 | 'Africa/Juba', 630 | 'Africa/Kampala', 631 | 'Africa/Khartoum', 632 | 'Africa/Kigali', 633 | 'Africa/Kinshasa', 634 | 'Africa/Lagos', 635 | 'Africa/Libreville', 636 | 'Africa/Lome', 637 | 'Africa/Luanda', 638 | 'Africa/Lubumbashi', 639 | 'Africa/Lusaka', 640 | 'Africa/Malabo', 641 | 'Africa/Maputo', 642 | 'Africa/Maseru', 643 | 'Africa/Mbabane', 644 | 'Africa/Mogadishu', 645 | 'Africa/Monrovia', 646 | 'Africa/Nairobi', 647 | 'Africa/Ndjamena', 648 | 'Africa/Niamey', 649 | 'Africa/Nouakchott', 650 | 'Africa/Ouagadougou', 651 | 'Africa/Porto-Novo', 652 | 'Africa/Sao_Tome', 653 | 'Africa/Timbuktu', 654 | 'Africa/Tripoli', 655 | 'Africa/Tunis', 656 | 'Africa/Windhoek', 657 | 'America/Adak', 658 | 'America/Anchorage', 659 | 'America/Anguilla', 660 | 'America/Antigua', 661 | 'America/Araguaina', 662 | 'America/Argentina/Buenos_Aires', 663 | 'America/Argentina/Catamarca', 664 | 'America/Argentina/ComodRivadavia', 665 | 'America/Argentina/Cordoba', 666 | 'America/Argentina/Jujuy', 667 | 'America/Argentina/La_Rioja', 668 | 'America/Argentina/Mendoza', 669 | 'America/Argentina/Rio_Gallegos', 670 | 'America/Argentina/Salta', 671 | 'America/Argentina/San_Juan', 672 | 'America/Argentina/San_Luis', 673 | 'America/Argentina/Tucuman', 674 | 'America/Argentina/Ushuaia', 675 | 'America/Aruba', 676 | 'America/Asuncion', 677 | 'America/Atikokan', 678 | 'America/Atka', 679 | 'America/Bahia', 680 | 'America/Bahia_Banderas', 681 | 'America/Barbados', 682 | 'America/Belem', 683 | 'America/Belize', 684 | 'America/Blanc-Sablon', 685 | 'America/Boa_Vista', 686 | 'America/Bogota', 687 | 'America/Boise', 688 | 'America/Buenos_Aires', 689 | 'America/Cambridge_Bay', 690 | 'America/Campo_Grande', 691 | 'America/Cancun', 692 | 'America/Caracas', 693 | 'America/Catamarca', 694 | 'America/Cayenne', 695 | 'America/Cayman', 696 | 'America/Chicago', 697 | 'America/Chihuahua', 698 | 'America/Coral_Harbour', 699 | 'America/Cordoba', 700 | 'America/Costa_Rica', 701 | 'America/Creston', 702 | 'America/Cuiaba', 703 | 'America/Curacao', 704 | 'America/Danmarkshavn', 705 | 'America/Dawson', 706 | 'America/Dawson_Creek', 707 | 'America/Denver', 708 | 'America/Detroit', 709 | 'America/Dominica', 710 | 'America/Edmonton', 711 | 'America/Eirunepe', 712 | 'America/El_Salvador', 713 | 'America/Ensenada', 714 | 'America/Fort_Wayne', 715 | 'America/Fortaleza', 716 | 'America/Glace_Bay', 717 | 'America/Godthab', 718 | 'America/Goose_Bay', 719 | 'America/Grand_Turk', 720 | 'America/Grenada', 721 | 'America/Guadeloupe', 722 | 'America/Guatemala', 723 | 'America/Guayaquil', 724 | 'America/Guyana', 725 | 'America/Halifax', 726 | 'America/Havana', 727 | 'America/Hermosillo', 728 | 'America/Indiana/Indianapolis', 729 | 'America/Indiana/Knox', 730 | 'America/Indiana/Marengo', 731 | 'America/Indiana/Petersburg', 732 | 'America/Indiana/Tell_City', 733 | 'America/Indiana/Vevay', 734 | 'America/Indiana/Vincennes', 735 | 'America/Indiana/Winamac', 736 | 'America/Indianapolis', 737 | 'America/Inuvik', 738 | 'America/Iqaluit', 739 | 'America/Jamaica', 740 | 'America/Jujuy', 741 | 'America/Juneau', 742 | 'America/Kentucky/Louisville', 743 | 'America/Kentucky/Monticello', 744 | 'America/Knox_IN', 745 | 'America/Kralendijk', 746 | 'America/La_Paz', 747 | 'America/Lima', 748 | 'America/Los_Angeles', 749 | 'America/Louisville', 750 | 'America/Lower_Princes', 751 | 'America/Maceio', 752 | 'America/Managua', 753 | 'America/Manaus', 754 | 'America/Marigot', 755 | 'America/Martinique', 756 | 'America/Matamoros', 757 | 'America/Mazatlan', 758 | 'America/Mendoza', 759 | 'America/Menominee', 760 | 'America/Merida', 761 | 'America/Metlakatla', 762 | 'America/Mexico_City', 763 | 'America/Miquelon', 764 | 'America/Moncton', 765 | 'America/Monterrey', 766 | 'America/Montevideo', 767 | 'America/Montreal', 768 | 'America/Montserrat', 769 | 'America/Nassau', 770 | 'America/New_York', 771 | 'America/Nipigon', 772 | 'America/Nome', 773 | 'America/Noronha', 774 | 'America/North_Dakota/Beulah', 775 | 'America/North_Dakota/Center', 776 | 'America/North_Dakota/New_Salem', 777 | 'America/Ojinaga', 778 | 'America/Panama', 779 | 'America/Pangnirtung', 780 | 'America/Paramaribo', 781 | 'America/Phoenix', 782 | 'America/Port-au-Prince', 783 | 'America/Port_of_Spain', 784 | 'America/Porto_Acre', 785 | 'America/Porto_Velho', 786 | 'America/Puerto_Rico', 787 | 'America/Rainy_River', 788 | 'America/Rankin_Inlet', 789 | 'America/Recife', 790 | 'America/Regina', 791 | 'America/Resolute', 792 | 'America/Rio_Branco', 793 | 'America/Rosario', 794 | 'America/Santa_Isabel', 795 | 'America/Santarem', 796 | 'America/Santiago', 797 | 'America/Santo_Domingo', 798 | 'America/Sao_Paulo', 799 | 'America/Scoresbysund', 800 | 'America/Shiprock', 801 | 'America/Sitka', 802 | 'America/St_Barthelemy', 803 | 'America/St_Johns', 804 | 'America/St_Kitts', 805 | 'America/St_Lucia', 806 | 'America/St_Thomas', 807 | 'America/St_Vincent', 808 | 'America/Swift_Current', 809 | 'America/Tegucigalpa', 810 | 'America/Thule', 811 | 'America/Thunder_Bay', 812 | 'America/Tijuana', 813 | 'America/Toronto', 814 | 'America/Tortola', 815 | 'America/Vancouver', 816 | 'America/Virgin', 817 | 'America/Whitehorse', 818 | 'America/Winnipeg', 819 | 'America/Yakutat', 820 | 'America/Yellowknife', 821 | 'Antarctica/Casey', 822 | 'Antarctica/Davis', 823 | 'Antarctica/DumontDUrville', 824 | 'Antarctica/Macquarie', 825 | 'Antarctica/Mawson', 826 | 'Antarctica/McMurdo', 827 | 'Antarctica/Palmer', 828 | 'Antarctica/Rothera', 829 | 'Antarctica/South_Pole', 830 | 'Antarctica/Syowa', 831 | 'Antarctica/Vostok', 832 | 'Arctic/Longyearbyen', 833 | 'Asia/Aden', 834 | 'Asia/Almaty', 835 | 'Asia/Amman', 836 | 'Asia/Anadyr', 837 | 'Asia/Aqtau', 838 | 'Asia/Aqtobe', 839 | 'Asia/Ashgabat', 840 | 'Asia/Ashkhabad', 841 | 'Asia/Baghdad', 842 | 'Asia/Bahrain', 843 | 'Asia/Baku', 844 | 'Asia/Bangkok', 845 | 'Asia/Beirut', 846 | 'Asia/Bishkek', 847 | 'Asia/Brunei', 848 | 'Asia/Calcutta', 849 | 'Asia/Choibalsan', 850 | 'Asia/Chongqing', 851 | 'Asia/Chungking', 852 | 'Asia/Colombo', 853 | 'Asia/Dacca', 854 | 'Asia/Damascus', 855 | 'Asia/Dhaka', 856 | 'Asia/Dili', 857 | 'Asia/Dubai', 858 | 'Asia/Dushanbe', 859 | 'Asia/Gaza', 860 | 'Asia/Harbin', 861 | 'Asia/Hebron', 862 | 'Asia/Ho_Chi_Minh', 863 | 'Asia/Hong_Kong', 864 | 'Asia/Hovd', 865 | 'Asia/Irkutsk', 866 | 'Asia/Istanbul', 867 | 'Asia/Jakarta', 868 | 'Asia/Jayapura', 869 | 'Asia/Jerusalem', 870 | 'Asia/Kabul', 871 | 'Asia/Kamchatka', 872 | 'Asia/Karachi', 873 | 'Asia/Kashgar', 874 | 'Asia/Kathmandu', 875 | 'Asia/Katmandu', 876 | 'Asia/Khandyga', 877 | 'Asia/Kolkata', 878 | 'Asia/Krasnoyarsk', 879 | 'Asia/Kuala_Lumpur', 880 | 'Asia/Kuching', 881 | 'Asia/Kuwait', 882 | 'Asia/Macao', 883 | 'Asia/Macau', 884 | 'Asia/Magadan', 885 | 'Asia/Makassar', 886 | 'Asia/Manila', 887 | 'Asia/Muscat', 888 | 'Asia/Nicosia', 889 | 'Asia/Novokuznetsk', 890 | 'Asia/Novosibirsk', 891 | 'Asia/Omsk', 892 | 'Asia/Oral', 893 | 'Asia/Phnom_Penh', 894 | 'Asia/Pontianak', 895 | 'Asia/Pyongyang', 896 | 'Asia/Qatar', 897 | 'Asia/Qyzylorda', 898 | 'Asia/Rangoon', 899 | 'Asia/Riyadh', 900 | 'Asia/Saigon', 901 | 'Asia/Sakhalin', 902 | 'Asia/Samarkand', 903 | 'Asia/Seoul', 904 | 'Asia/Shanghai', 905 | 'Asia/Singapore', 906 | 'Asia/Taipei', 907 | 'Asia/Tashkent', 908 | 'Asia/Tbilisi', 909 | 'Asia/Tehran', 910 | 'Asia/Tel_Aviv', 911 | 'Asia/Thimbu', 912 | 'Asia/Thimphu', 913 | 'Asia/Tokyo', 914 | 'Asia/Ujung_Pandang', 915 | 'Asia/Ulaanbaatar', 916 | 'Asia/Ulan_Bator', 917 | 'Asia/Urumqi', 918 | 'Asia/Ust-Nera', 919 | 'Asia/Vientiane', 920 | 'Asia/Vladivostok', 921 | 'Asia/Yakutsk', 922 | 'Asia/Yekaterinburg', 923 | 'Asia/Yerevan', 924 | 'Atlantic/Azores', 925 | 'Atlantic/Bermuda', 926 | 'Atlantic/Canary', 927 | 'Atlantic/Cape_Verde', 928 | 'Atlantic/Faeroe', 929 | 'Atlantic/Faroe', 930 | 'Atlantic/Jan_Mayen', 931 | 'Atlantic/Madeira', 932 | 'Atlantic/Reykjavik', 933 | 'Atlantic/South_Georgia', 934 | 'Atlantic/St_Helena', 935 | 'Atlantic/Stanley', 936 | 'Australia/ACT', 937 | 'Australia/Adelaide', 938 | 'Australia/Brisbane', 939 | 'Australia/Broken_Hill', 940 | 'Australia/Canberra', 941 | 'Australia/Currie', 942 | 'Australia/Darwin', 943 | 'Australia/Eucla', 944 | 'Australia/Hobart', 945 | 'Australia/LHI', 946 | 'Australia/Lindeman', 947 | 'Australia/Lord_Howe', 948 | 'Australia/Melbourne', 949 | 'Australia/NSW', 950 | 'Australia/North', 951 | 'Australia/Perth', 952 | 'Australia/Queensland', 953 | 'Australia/South', 954 | 'Australia/Sydney', 955 | 'Australia/Tasmania', 956 | 'Australia/Victoria', 957 | 'Australia/West', 958 | 'Australia/Yancowinna', 959 | 'Brazil/Acre', 960 | 'Brazil/DeNoronha', 961 | 'Brazil/East', 962 | 'Brazil/West', 963 | 'CET', 964 | 'CST6CDT', 965 | 'Canada/Atlantic', 966 | 'Canada/Central', 967 | 'Canada/East-Saskatchewan', 968 | 'Canada/Eastern', 969 | 'Canada/Mountain', 970 | 'Canada/Newfoundland', 971 | 'Canada/Pacific', 972 | 'Canada/Saskatchewan', 973 | 'Canada/Yukon', 974 | 'Chile/Continental', 975 | 'Chile/EasterIsland', 976 | 'Cuba', 977 | 'EET', 978 | 'EST', 979 | 'EST5EDT', 980 | 'Egypt', 981 | 'Eire', 982 | 'Etc/GMT', 983 | 'Etc/GMT+0', 984 | 'Etc/GMT+1', 985 | 'Etc/GMT+10', 986 | 'Etc/GMT+11', 987 | 'Etc/GMT+12', 988 | 'Etc/GMT+2', 989 | 'Etc/GMT+3', 990 | 'Etc/GMT+4', 991 | 'Etc/GMT+5', 992 | 'Etc/GMT+6', 993 | 'Etc/GMT+7', 994 | 'Etc/GMT+8', 995 | 'Etc/GMT+9', 996 | 'Etc/GMT-0', 997 | 'Etc/GMT-1', 998 | 'Etc/GMT-10', 999 | 'Etc/GMT-11', 1000 | 'Etc/GMT-12', 1001 | 'Etc/GMT-13', 1002 | 'Etc/GMT-14', 1003 | 'Etc/GMT-2', 1004 | 'Etc/GMT-3', 1005 | 'Etc/GMT-4', 1006 | 'Etc/GMT-5', 1007 | 'Etc/GMT-6', 1008 | 'Etc/GMT-7', 1009 | 'Etc/GMT-8', 1010 | 'Etc/GMT-9', 1011 | 'Etc/GMT0', 1012 | 'Etc/Greenwich', 1013 | 'Etc/UCT', 1014 | 'Etc/UTC', 1015 | 'Etc/Universal', 1016 | 'Etc/Zulu', 1017 | 'Europe/Amsterdam', 1018 | 'Europe/Andorra', 1019 | 'Europe/Athens', 1020 | 'Europe/Belfast', 1021 | 'Europe/Belgrade', 1022 | 'Europe/Berlin', 1023 | 'Europe/Bratislava', 1024 | 'Europe/Brussels', 1025 | 'Europe/Bucharest', 1026 | 'Europe/Budapest', 1027 | 'Europe/Busingen', 1028 | 'Europe/Chisinau', 1029 | 'Europe/Copenhagen', 1030 | 'Europe/Dublin', 1031 | 'Europe/Gibraltar', 1032 | 'Europe/Guernsey', 1033 | 'Europe/Helsinki', 1034 | 'Europe/Isle_of_Man', 1035 | 'Europe/Istanbul', 1036 | 'Europe/Jersey', 1037 | 'Europe/Kaliningrad', 1038 | 'Europe/Kiev', 1039 | 'Europe/Lisbon', 1040 | 'Europe/Ljubljana', 1041 | 'Europe/London', 1042 | 'Europe/Luxembourg', 1043 | 'Europe/Madrid', 1044 | 'Europe/Malta', 1045 | 'Europe/Mariehamn', 1046 | 'Europe/Minsk', 1047 | 'Europe/Monaco', 1048 | 'Europe/Moscow', 1049 | 'Europe/Nicosia', 1050 | 'Europe/Oslo', 1051 | 'Europe/Paris', 1052 | 'Europe/Podgorica', 1053 | 'Europe/Prague', 1054 | 'Europe/Riga', 1055 | 'Europe/Rome', 1056 | 'Europe/Samara', 1057 | 'Europe/San_Marino', 1058 | 'Europe/Sarajevo', 1059 | 'Europe/Simferopol', 1060 | 'Europe/Skopje', 1061 | 'Europe/Sofia', 1062 | 'Europe/Stockholm', 1063 | 'Europe/Tallinn', 1064 | 'Europe/Tirane', 1065 | 'Europe/Tiraspol', 1066 | 'Europe/Uzhgorod', 1067 | 'Europe/Vaduz', 1068 | 'Europe/Vatican', 1069 | 'Europe/Vienna', 1070 | 'Europe/Vilnius', 1071 | 'Europe/Volgograd', 1072 | 'Europe/Warsaw', 1073 | 'Europe/Zagreb', 1074 | 'Europe/Zaporozhye', 1075 | 'Europe/Zurich', 1076 | 'GB', 1077 | 'GB-Eire', 1078 | 'GMT', 1079 | 'GMT+0', 1080 | 'GMT-0', 1081 | 'GMT0', 1082 | 'Greenwich', 1083 | 'HST', 1084 | 'Hongkong', 1085 | 'Iceland', 1086 | 'Indian/Antananarivo', 1087 | 'Indian/Chagos', 1088 | 'Indian/Christmas', 1089 | 'Indian/Cocos', 1090 | 'Indian/Comoro', 1091 | 'Indian/Kerguelen', 1092 | 'Indian/Mahe', 1093 | 'Indian/Maldives', 1094 | 'Indian/Mauritius', 1095 | 'Indian/Mayotte', 1096 | 'Indian/Reunion', 1097 | 'Iran', 1098 | 'Israel', 1099 | 'Jamaica', 1100 | 'Japan', 1101 | 'Kwajalein', 1102 | 'Libya', 1103 | 'MET', 1104 | 'MST', 1105 | 'MST7MDT', 1106 | 'Mexico/BajaNorte', 1107 | 'Mexico/BajaSur', 1108 | 'Mexico/General', 1109 | 'NZ', 1110 | 'NZ-CHAT', 1111 | 'Navajo', 1112 | 'PRC', 1113 | 'PST8PDT', 1114 | 'Pacific/Apia', 1115 | 'Pacific/Auckland', 1116 | 'Pacific/Chatham', 1117 | 'Pacific/Chuuk', 1118 | 'Pacific/Easter', 1119 | 'Pacific/Efate', 1120 | 'Pacific/Enderbury', 1121 | 'Pacific/Fakaofo', 1122 | 'Pacific/Fiji', 1123 | 'Pacific/Funafuti', 1124 | 'Pacific/Galapagos', 1125 | 'Pacific/Gambier', 1126 | 'Pacific/Guadalcanal', 1127 | 'Pacific/Guam', 1128 | 'Pacific/Honolulu', 1129 | 'Pacific/Johnston', 1130 | 'Pacific/Kiritimati', 1131 | 'Pacific/Kosrae', 1132 | 'Pacific/Kwajalein', 1133 | 'Pacific/Majuro', 1134 | 'Pacific/Marquesas', 1135 | 'Pacific/Midway', 1136 | 'Pacific/Nauru', 1137 | 'Pacific/Niue', 1138 | 'Pacific/Norfolk', 1139 | 'Pacific/Noumea', 1140 | 'Pacific/Pago_Pago', 1141 | 'Pacific/Palau', 1142 | 'Pacific/Pitcairn', 1143 | 'Pacific/Pohnpei', 1144 | 'Pacific/Ponape', 1145 | 'Pacific/Port_Moresby', 1146 | 'Pacific/Rarotonga', 1147 | 'Pacific/Saipan', 1148 | 'Pacific/Samoa', 1149 | 'Pacific/Tahiti', 1150 | 'Pacific/Tarawa', 1151 | 'Pacific/Tongatapu', 1152 | 'Pacific/Truk', 1153 | 'Pacific/Wake', 1154 | 'Pacific/Wallis', 1155 | 'Pacific/Yap', 1156 | 'Poland', 1157 | 'Portugal', 1158 | 'ROC', 1159 | 'ROK', 1160 | 'Singapore', 1161 | 'Turkey', 1162 | 'UCT', 1163 | 'US/Alaska', 1164 | 'US/Aleutian', 1165 | 'US/Arizona', 1166 | 'US/Central', 1167 | 'US/East-Indiana', 1168 | 'US/Eastern', 1169 | 'US/Hawaii', 1170 | 'US/Indiana-Starke', 1171 | 'US/Michigan', 1172 | 'US/Mountain', 1173 | 'US/Pacific', 1174 | 'US/Pacific-New', 1175 | 'US/Samoa', 1176 | 'UTC', 1177 | 'Universal', 1178 | 'W-SU', 1179 | 'WET', 1180 | 'Zulu'] 1181 | all_timezones = LazyList( 1182 | tz for tz in all_timezones if resource_exists(tz)) 1183 | 1184 | all_timezones_set = LazySet(all_timezones) 1185 | common_timezones = \ 1186 | ['Africa/Abidjan', 1187 | 'Africa/Accra', 1188 | 'Africa/Addis_Ababa', 1189 | 'Africa/Algiers', 1190 | 'Africa/Asmara', 1191 | 'Africa/Bamako', 1192 | 'Africa/Bangui', 1193 | 'Africa/Banjul', 1194 | 'Africa/Bissau', 1195 | 'Africa/Blantyre', 1196 | 'Africa/Brazzaville', 1197 | 'Africa/Bujumbura', 1198 | 'Africa/Cairo', 1199 | 'Africa/Casablanca', 1200 | 'Africa/Ceuta', 1201 | 'Africa/Conakry', 1202 | 'Africa/Dakar', 1203 | 'Africa/Dar_es_Salaam', 1204 | 'Africa/Djibouti', 1205 | 'Africa/Douala', 1206 | 'Africa/El_Aaiun', 1207 | 'Africa/Freetown', 1208 | 'Africa/Gaborone', 1209 | 'Africa/Harare', 1210 | 'Africa/Johannesburg', 1211 | 'Africa/Juba', 1212 | 'Africa/Kampala', 1213 | 'Africa/Khartoum', 1214 | 'Africa/Kigali', 1215 | 'Africa/Kinshasa', 1216 | 'Africa/Lagos', 1217 | 'Africa/Libreville', 1218 | 'Africa/Lome', 1219 | 'Africa/Luanda', 1220 | 'Africa/Lubumbashi', 1221 | 'Africa/Lusaka', 1222 | 'Africa/Malabo', 1223 | 'Africa/Maputo', 1224 | 'Africa/Maseru', 1225 | 'Africa/Mbabane', 1226 | 'Africa/Mogadishu', 1227 | 'Africa/Monrovia', 1228 | 'Africa/Nairobi', 1229 | 'Africa/Ndjamena', 1230 | 'Africa/Niamey', 1231 | 'Africa/Nouakchott', 1232 | 'Africa/Ouagadougou', 1233 | 'Africa/Porto-Novo', 1234 | 'Africa/Sao_Tome', 1235 | 'Africa/Tripoli', 1236 | 'Africa/Tunis', 1237 | 'Africa/Windhoek', 1238 | 'America/Adak', 1239 | 'America/Anchorage', 1240 | 'America/Anguilla', 1241 | 'America/Antigua', 1242 | 'America/Araguaina', 1243 | 'America/Argentina/Buenos_Aires', 1244 | 'America/Argentina/Catamarca', 1245 | 'America/Argentina/Cordoba', 1246 | 'America/Argentina/Jujuy', 1247 | 'America/Argentina/La_Rioja', 1248 | 'America/Argentina/Mendoza', 1249 | 'America/Argentina/Rio_Gallegos', 1250 | 'America/Argentina/Salta', 1251 | 'America/Argentina/San_Juan', 1252 | 'America/Argentina/San_Luis', 1253 | 'America/Argentina/Tucuman', 1254 | 'America/Argentina/Ushuaia', 1255 | 'America/Aruba', 1256 | 'America/Asuncion', 1257 | 'America/Atikokan', 1258 | 'America/Bahia', 1259 | 'America/Bahia_Banderas', 1260 | 'America/Barbados', 1261 | 'America/Belem', 1262 | 'America/Belize', 1263 | 'America/Blanc-Sablon', 1264 | 'America/Boa_Vista', 1265 | 'America/Bogota', 1266 | 'America/Boise', 1267 | 'America/Cambridge_Bay', 1268 | 'America/Campo_Grande', 1269 | 'America/Cancun', 1270 | 'America/Caracas', 1271 | 'America/Cayenne', 1272 | 'America/Cayman', 1273 | 'America/Chicago', 1274 | 'America/Chihuahua', 1275 | 'America/Costa_Rica', 1276 | 'America/Creston', 1277 | 'America/Cuiaba', 1278 | 'America/Curacao', 1279 | 'America/Danmarkshavn', 1280 | 'America/Dawson', 1281 | 'America/Dawson_Creek', 1282 | 'America/Denver', 1283 | 'America/Detroit', 1284 | 'America/Dominica', 1285 | 'America/Edmonton', 1286 | 'America/Eirunepe', 1287 | 'America/El_Salvador', 1288 | 'America/Fortaleza', 1289 | 'America/Glace_Bay', 1290 | 'America/Godthab', 1291 | 'America/Goose_Bay', 1292 | 'America/Grand_Turk', 1293 | 'America/Grenada', 1294 | 'America/Guadeloupe', 1295 | 'America/Guatemala', 1296 | 'America/Guayaquil', 1297 | 'America/Guyana', 1298 | 'America/Halifax', 1299 | 'America/Havana', 1300 | 'America/Hermosillo', 1301 | 'America/Indiana/Indianapolis', 1302 | 'America/Indiana/Knox', 1303 | 'America/Indiana/Marengo', 1304 | 'America/Indiana/Petersburg', 1305 | 'America/Indiana/Tell_City', 1306 | 'America/Indiana/Vevay', 1307 | 'America/Indiana/Vincennes', 1308 | 'America/Indiana/Winamac', 1309 | 'America/Inuvik', 1310 | 'America/Iqaluit', 1311 | 'America/Jamaica', 1312 | 'America/Juneau', 1313 | 'America/Kentucky/Louisville', 1314 | 'America/Kentucky/Monticello', 1315 | 'America/Kralendijk', 1316 | 'America/La_Paz', 1317 | 'America/Lima', 1318 | 'America/Los_Angeles', 1319 | 'America/Lower_Princes', 1320 | 'America/Maceio', 1321 | 'America/Managua', 1322 | 'America/Manaus', 1323 | 'America/Marigot', 1324 | 'America/Martinique', 1325 | 'America/Matamoros', 1326 | 'America/Mazatlan', 1327 | 'America/Menominee', 1328 | 'America/Merida', 1329 | 'America/Metlakatla', 1330 | 'America/Mexico_City', 1331 | 'America/Miquelon', 1332 | 'America/Moncton', 1333 | 'America/Monterrey', 1334 | 'America/Montevideo', 1335 | 'America/Montreal', 1336 | 'America/Montserrat', 1337 | 'America/Nassau', 1338 | 'America/New_York', 1339 | 'America/Nipigon', 1340 | 'America/Nome', 1341 | 'America/Noronha', 1342 | 'America/North_Dakota/Beulah', 1343 | 'America/North_Dakota/Center', 1344 | 'America/North_Dakota/New_Salem', 1345 | 'America/Ojinaga', 1346 | 'America/Panama', 1347 | 'America/Pangnirtung', 1348 | 'America/Paramaribo', 1349 | 'America/Phoenix', 1350 | 'America/Port-au-Prince', 1351 | 'America/Port_of_Spain', 1352 | 'America/Porto_Velho', 1353 | 'America/Puerto_Rico', 1354 | 'America/Rainy_River', 1355 | 'America/Rankin_Inlet', 1356 | 'America/Recife', 1357 | 'America/Regina', 1358 | 'America/Resolute', 1359 | 'America/Rio_Branco', 1360 | 'America/Santa_Isabel', 1361 | 'America/Santarem', 1362 | 'America/Santiago', 1363 | 'America/Santo_Domingo', 1364 | 'America/Sao_Paulo', 1365 | 'America/Scoresbysund', 1366 | 'America/Sitka', 1367 | 'America/St_Barthelemy', 1368 | 'America/St_Johns', 1369 | 'America/St_Kitts', 1370 | 'America/St_Lucia', 1371 | 'America/St_Thomas', 1372 | 'America/St_Vincent', 1373 | 'America/Swift_Current', 1374 | 'America/Tegucigalpa', 1375 | 'America/Thule', 1376 | 'America/Thunder_Bay', 1377 | 'America/Tijuana', 1378 | 'America/Toronto', 1379 | 'America/Tortola', 1380 | 'America/Vancouver', 1381 | 'America/Whitehorse', 1382 | 'America/Winnipeg', 1383 | 'America/Yakutat', 1384 | 'America/Yellowknife', 1385 | 'Antarctica/Casey', 1386 | 'Antarctica/Davis', 1387 | 'Antarctica/DumontDUrville', 1388 | 'Antarctica/Macquarie', 1389 | 'Antarctica/Mawson', 1390 | 'Antarctica/McMurdo', 1391 | 'Antarctica/Palmer', 1392 | 'Antarctica/Rothera', 1393 | 'Antarctica/Syowa', 1394 | 'Antarctica/Vostok', 1395 | 'Arctic/Longyearbyen', 1396 | 'Asia/Aden', 1397 | 'Asia/Almaty', 1398 | 'Asia/Amman', 1399 | 'Asia/Anadyr', 1400 | 'Asia/Aqtau', 1401 | 'Asia/Aqtobe', 1402 | 'Asia/Ashgabat', 1403 | 'Asia/Baghdad', 1404 | 'Asia/Bahrain', 1405 | 'Asia/Baku', 1406 | 'Asia/Bangkok', 1407 | 'Asia/Beirut', 1408 | 'Asia/Bishkek', 1409 | 'Asia/Brunei', 1410 | 'Asia/Choibalsan', 1411 | 'Asia/Chongqing', 1412 | 'Asia/Colombo', 1413 | 'Asia/Damascus', 1414 | 'Asia/Dhaka', 1415 | 'Asia/Dili', 1416 | 'Asia/Dubai', 1417 | 'Asia/Dushanbe', 1418 | 'Asia/Gaza', 1419 | 'Asia/Harbin', 1420 | 'Asia/Hebron', 1421 | 'Asia/Ho_Chi_Minh', 1422 | 'Asia/Hong_Kong', 1423 | 'Asia/Hovd', 1424 | 'Asia/Irkutsk', 1425 | 'Asia/Jakarta', 1426 | 'Asia/Jayapura', 1427 | 'Asia/Jerusalem', 1428 | 'Asia/Kabul', 1429 | 'Asia/Kamchatka', 1430 | 'Asia/Karachi', 1431 | 'Asia/Kashgar', 1432 | 'Asia/Kathmandu', 1433 | 'Asia/Khandyga', 1434 | 'Asia/Kolkata', 1435 | 'Asia/Krasnoyarsk', 1436 | 'Asia/Kuala_Lumpur', 1437 | 'Asia/Kuching', 1438 | 'Asia/Kuwait', 1439 | 'Asia/Macau', 1440 | 'Asia/Magadan', 1441 | 'Asia/Makassar', 1442 | 'Asia/Manila', 1443 | 'Asia/Muscat', 1444 | 'Asia/Nicosia', 1445 | 'Asia/Novokuznetsk', 1446 | 'Asia/Novosibirsk', 1447 | 'Asia/Omsk', 1448 | 'Asia/Oral', 1449 | 'Asia/Phnom_Penh', 1450 | 'Asia/Pontianak', 1451 | 'Asia/Pyongyang', 1452 | 'Asia/Qatar', 1453 | 'Asia/Qyzylorda', 1454 | 'Asia/Rangoon', 1455 | 'Asia/Riyadh', 1456 | 'Asia/Sakhalin', 1457 | 'Asia/Samarkand', 1458 | 'Asia/Seoul', 1459 | 'Asia/Shanghai', 1460 | 'Asia/Singapore', 1461 | 'Asia/Taipei', 1462 | 'Asia/Tashkent', 1463 | 'Asia/Tbilisi', 1464 | 'Asia/Tehran', 1465 | 'Asia/Thimphu', 1466 | 'Asia/Tokyo', 1467 | 'Asia/Ulaanbaatar', 1468 | 'Asia/Urumqi', 1469 | 'Asia/Ust-Nera', 1470 | 'Asia/Vientiane', 1471 | 'Asia/Vladivostok', 1472 | 'Asia/Yakutsk', 1473 | 'Asia/Yekaterinburg', 1474 | 'Asia/Yerevan', 1475 | 'Atlantic/Azores', 1476 | 'Atlantic/Bermuda', 1477 | 'Atlantic/Canary', 1478 | 'Atlantic/Cape_Verde', 1479 | 'Atlantic/Faroe', 1480 | 'Atlantic/Madeira', 1481 | 'Atlantic/Reykjavik', 1482 | 'Atlantic/South_Georgia', 1483 | 'Atlantic/St_Helena', 1484 | 'Atlantic/Stanley', 1485 | 'Australia/Adelaide', 1486 | 'Australia/Brisbane', 1487 | 'Australia/Broken_Hill', 1488 | 'Australia/Currie', 1489 | 'Australia/Darwin', 1490 | 'Australia/Eucla', 1491 | 'Australia/Hobart', 1492 | 'Australia/Lindeman', 1493 | 'Australia/Lord_Howe', 1494 | 'Australia/Melbourne', 1495 | 'Australia/Perth', 1496 | 'Australia/Sydney', 1497 | 'Canada/Atlantic', 1498 | 'Canada/Central', 1499 | 'Canada/Eastern', 1500 | 'Canada/Mountain', 1501 | 'Canada/Newfoundland', 1502 | 'Canada/Pacific', 1503 | 'Europe/Amsterdam', 1504 | 'Europe/Andorra', 1505 | 'Europe/Athens', 1506 | 'Europe/Belgrade', 1507 | 'Europe/Berlin', 1508 | 'Europe/Bratislava', 1509 | 'Europe/Brussels', 1510 | 'Europe/Bucharest', 1511 | 'Europe/Budapest', 1512 | 'Europe/Busingen', 1513 | 'Europe/Chisinau', 1514 | 'Europe/Copenhagen', 1515 | 'Europe/Dublin', 1516 | 'Europe/Gibraltar', 1517 | 'Europe/Guernsey', 1518 | 'Europe/Helsinki', 1519 | 'Europe/Isle_of_Man', 1520 | 'Europe/Istanbul', 1521 | 'Europe/Jersey', 1522 | 'Europe/Kaliningrad', 1523 | 'Europe/Kiev', 1524 | 'Europe/Lisbon', 1525 | 'Europe/Ljubljana', 1526 | 'Europe/London', 1527 | 'Europe/Luxembourg', 1528 | 'Europe/Madrid', 1529 | 'Europe/Malta', 1530 | 'Europe/Mariehamn', 1531 | 'Europe/Minsk', 1532 | 'Europe/Monaco', 1533 | 'Europe/Moscow', 1534 | 'Europe/Oslo', 1535 | 'Europe/Paris', 1536 | 'Europe/Podgorica', 1537 | 'Europe/Prague', 1538 | 'Europe/Riga', 1539 | 'Europe/Rome', 1540 | 'Europe/Samara', 1541 | 'Europe/San_Marino', 1542 | 'Europe/Sarajevo', 1543 | 'Europe/Simferopol', 1544 | 'Europe/Skopje', 1545 | 'Europe/Sofia', 1546 | 'Europe/Stockholm', 1547 | 'Europe/Tallinn', 1548 | 'Europe/Tirane', 1549 | 'Europe/Uzhgorod', 1550 | 'Europe/Vaduz', 1551 | 'Europe/Vatican', 1552 | 'Europe/Vienna', 1553 | 'Europe/Vilnius', 1554 | 'Europe/Volgograd', 1555 | 'Europe/Warsaw', 1556 | 'Europe/Zagreb', 1557 | 'Europe/Zaporozhye', 1558 | 'Europe/Zurich', 1559 | 'GMT', 1560 | 'Indian/Antananarivo', 1561 | 'Indian/Chagos', 1562 | 'Indian/Christmas', 1563 | 'Indian/Cocos', 1564 | 'Indian/Comoro', 1565 | 'Indian/Kerguelen', 1566 | 'Indian/Mahe', 1567 | 'Indian/Maldives', 1568 | 'Indian/Mauritius', 1569 | 'Indian/Mayotte', 1570 | 'Indian/Reunion', 1571 | 'Pacific/Apia', 1572 | 'Pacific/Auckland', 1573 | 'Pacific/Chatham', 1574 | 'Pacific/Chuuk', 1575 | 'Pacific/Easter', 1576 | 'Pacific/Efate', 1577 | 'Pacific/Enderbury', 1578 | 'Pacific/Fakaofo', 1579 | 'Pacific/Fiji', 1580 | 'Pacific/Funafuti', 1581 | 'Pacific/Galapagos', 1582 | 'Pacific/Gambier', 1583 | 'Pacific/Guadalcanal', 1584 | 'Pacific/Guam', 1585 | 'Pacific/Honolulu', 1586 | 'Pacific/Johnston', 1587 | 'Pacific/Kiritimati', 1588 | 'Pacific/Kosrae', 1589 | 'Pacific/Kwajalein', 1590 | 'Pacific/Majuro', 1591 | 'Pacific/Marquesas', 1592 | 'Pacific/Midway', 1593 | 'Pacific/Nauru', 1594 | 'Pacific/Niue', 1595 | 'Pacific/Norfolk', 1596 | 'Pacific/Noumea', 1597 | 'Pacific/Pago_Pago', 1598 | 'Pacific/Palau', 1599 | 'Pacific/Pitcairn', 1600 | 'Pacific/Pohnpei', 1601 | 'Pacific/Port_Moresby', 1602 | 'Pacific/Rarotonga', 1603 | 'Pacific/Saipan', 1604 | 'Pacific/Tahiti', 1605 | 'Pacific/Tarawa', 1606 | 'Pacific/Tongatapu', 1607 | 'Pacific/Wake', 1608 | 'Pacific/Wallis', 1609 | 'US/Alaska', 1610 | 'US/Arizona', 1611 | 'US/Central', 1612 | 'US/Eastern', 1613 | 'US/Hawaii', 1614 | 'US/Mountain', 1615 | 'US/Pacific', 1616 | 'UTC'] 1617 | common_timezones = LazyList( 1618 | tz for tz in common_timezones if tz in all_timezones) 1619 | 1620 | common_timezones_set = LazySet(common_timezones) 1621 | -------------------------------------------------------------------------------- /template/exceptions.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Custom exceptions raised by pytz. 3 | ''' 4 | 5 | __all__ = [ 6 | 'UnknownTimeZoneError', 'InvalidTimeError', 'AmbiguousTimeError', 7 | 'NonExistentTimeError', 8 | ] 9 | 10 | 11 | class UnknownTimeZoneError(KeyError): 12 | '''Exception raised when pytz is passed an unknown timezone. 13 | 14 | >>> isinstance(UnknownTimeZoneError(), LookupError) 15 | True 16 | 17 | This class is actually a subclass of KeyError to provide backwards 18 | compatibility with code relying on the undocumented behavior of earlier 19 | pytz releases. 20 | 21 | >>> isinstance(UnknownTimeZoneError(), KeyError) 22 | True 23 | ''' 24 | pass 25 | 26 | 27 | class InvalidTimeError(Exception): 28 | '''Base class for invalid time exceptions.''' 29 | 30 | 31 | class AmbiguousTimeError(InvalidTimeError): 32 | '''Exception raised when attempting to create an ambiguous wallclock time. 33 | 34 | At the end of a DST transition period, a particular wallclock time will 35 | occur twice (once before the clocks are set back, once after). Both 36 | possibilities may be correct, unless further information is supplied. 37 | 38 | See DstTzInfo.normalize() for more info 39 | ''' 40 | 41 | 42 | class NonExistentTimeError(InvalidTimeError): 43 | '''Exception raised when attempting to create a wallclock time that 44 | cannot exist. 45 | 46 | At the start of a DST transition period, the wallclock time jumps forward. 47 | The instants jumped over never occur. 48 | ''' 49 | -------------------------------------------------------------------------------- /template/lazy.py: -------------------------------------------------------------------------------- 1 | from threading import RLock 2 | try: 3 | from UserDict import DictMixin 4 | except ImportError: 5 | from collections import Mapping as DictMixin 6 | 7 | 8 | # With lazy loading, we might end up with multiple threads triggering 9 | # it at the same time. We need a lock. 10 | _fill_lock = RLock() 11 | 12 | 13 | class LazyDict(DictMixin): 14 | """Dictionary populated on first use.""" 15 | data = None 16 | def __getitem__(self, key): 17 | if self.data is None: 18 | _fill_lock.acquire() 19 | try: 20 | if self.data is None: 21 | self._fill() 22 | finally: 23 | _fill_lock.release() 24 | return self.data[key.upper()] 25 | 26 | def __contains__(self, key): 27 | if self.data is None: 28 | _fill_lock.acquire() 29 | try: 30 | if self.data is None: 31 | self._fill() 32 | finally: 33 | _fill_lock.release() 34 | return key in self.data 35 | 36 | def __iter__(self): 37 | if self.data is None: 38 | _fill_lock.acquire() 39 | try: 40 | if self.data is None: 41 | self._fill() 42 | finally: 43 | _fill_lock.release() 44 | return iter(self.data) 45 | 46 | def __len__(self): 47 | if self.data is None: 48 | _fill_lock.acquire() 49 | try: 50 | if self.data is None: 51 | self._fill() 52 | finally: 53 | _fill_lock.release() 54 | return len(self.data) 55 | 56 | def keys(self): 57 | if self.data is None: 58 | _fill_lock.acquire() 59 | try: 60 | if self.data is None: 61 | self._fill() 62 | finally: 63 | _fill_lock.release() 64 | return self.data.keys() 65 | 66 | 67 | class LazyList(list): 68 | """List populated on first use.""" 69 | def __new__(cls, fill_iter=None): 70 | 71 | if fill_iter is None: 72 | return list() 73 | 74 | # We need a new class as we will be dynamically messing with its 75 | # methods. 76 | class LazyList(list): 77 | pass 78 | 79 | _props = ( 80 | '__str__', '__repr__', '__unicode__', 81 | '__hash__', '__sizeof__', '__cmp__', 82 | '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', 83 | 'append', 'count', 'index', 'extend', 'insert', 'pop', 'remove', 84 | 'reverse', 'sort', '__add__', '__radd__', '__iadd__', '__mul__', 85 | '__rmul__', '__imul__', '__contains__', '__len__', '__nonzero__', 86 | '__getitem__', '__setitem__', '__delitem__', '__iter__', 87 | '__reversed__', '__getslice__', '__setslice__', '__delslice__') 88 | 89 | fill_iter = [fill_iter] 90 | 91 | def lazy(name): 92 | def _lazy(self, *args, **kw): 93 | _fill_lock.acquire() 94 | try: 95 | if len(fill_iter) > 0: 96 | list.extend(self, fill_iter.pop()) 97 | for method_name in _props: 98 | delattr(LazyList, method_name) 99 | finally: 100 | _fill_lock.release() 101 | return getattr(list, name)(self, *args, **kw) 102 | return _lazy 103 | 104 | for name in _props: 105 | setattr(LazyList, name, lazy(name)) 106 | 107 | new_list = LazyList() 108 | return new_list 109 | 110 | 111 | class LazySet(set): 112 | """Set populated on first use.""" 113 | def __new__(cls, fill_iter=None): 114 | 115 | if fill_iter is None: 116 | return set() 117 | 118 | class LazySet(set): 119 | pass 120 | 121 | _props = ( 122 | '__str__', '__repr__', '__unicode__', 123 | '__hash__', '__sizeof__', '__cmp__', 124 | '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', 125 | '__contains__', '__len__', '__nonzero__', 126 | '__getitem__', '__setitem__', '__delitem__', '__iter__', 127 | '__sub__', '__and__', '__xor__', '__or__', 128 | '__rsub__', '__rand__', '__rxor__', '__ror__', 129 | '__isub__', '__iand__', '__ixor__', '__ior__', 130 | 'add', 'clear', 'copy', 'difference', 'difference_update', 131 | 'discard', 'intersection', 'intersection_update', 'isdisjoint', 132 | 'issubset', 'issuperset', 'pop', 'remove', 133 | 'symmetric_difference', 'symmetric_difference_update', 134 | 'union', 'update') 135 | 136 | fill_iter = [fill_iter] 137 | 138 | def lazy(name): 139 | def _lazy(self, *args, **kw): 140 | _fill_lock.acquire() 141 | try: 142 | if len(fill_iter) > 0: 143 | for i in fill_iter.pop(): 144 | set.add(self, i) 145 | for method_name in _props: 146 | delattr(LazySet, method_name) 147 | finally: 148 | _fill_lock.release() 149 | return getattr(set, name)(self, *args, **kw) 150 | return _lazy 151 | 152 | for name in _props: 153 | setattr(LazySet, name, lazy(name)) 154 | 155 | new_set = LazySet() 156 | return new_set 157 | -------------------------------------------------------------------------------- /template/reference.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Reference tzinfo implementations from the Python docs. 3 | Used for testing against as they are only correct for the years 4 | 1987 to 2006. Do not use these for real code. 5 | ''' 6 | 7 | from datetime import tzinfo, timedelta, datetime 8 | from pytz import utc, UTC, HOUR, ZERO 9 | 10 | # A class building tzinfo objects for fixed-offset time zones. 11 | # Note that FixedOffset(0, "UTC") is a different way to build a 12 | # UTC tzinfo object. 13 | 14 | class FixedOffset(tzinfo): 15 | """Fixed offset in minutes east from UTC.""" 16 | 17 | def __init__(self, offset, name): 18 | self.__offset = timedelta(minutes = offset) 19 | self.__name = name 20 | 21 | def utcoffset(self, dt): 22 | return self.__offset 23 | 24 | def tzname(self, dt): 25 | return self.__name 26 | 27 | def dst(self, dt): 28 | return ZERO 29 | 30 | # A class capturing the platform's idea of local time. 31 | 32 | import time as _time 33 | 34 | STDOFFSET = timedelta(seconds = -_time.timezone) 35 | if _time.daylight: 36 | DSTOFFSET = timedelta(seconds = -_time.altzone) 37 | else: 38 | DSTOFFSET = STDOFFSET 39 | 40 | DSTDIFF = DSTOFFSET - STDOFFSET 41 | 42 | class LocalTimezone(tzinfo): 43 | 44 | def utcoffset(self, dt): 45 | if self._isdst(dt): 46 | return DSTOFFSET 47 | else: 48 | return STDOFFSET 49 | 50 | def dst(self, dt): 51 | if self._isdst(dt): 52 | return DSTDIFF 53 | else: 54 | return ZERO 55 | 56 | def tzname(self, dt): 57 | return _time.tzname[self._isdst(dt)] 58 | 59 | def _isdst(self, dt): 60 | tt = (dt.year, dt.month, dt.day, 61 | dt.hour, dt.minute, dt.second, 62 | dt.weekday(), 0, -1) 63 | stamp = _time.mktime(tt) 64 | tt = _time.localtime(stamp) 65 | return tt.tm_isdst > 0 66 | 67 | Local = LocalTimezone() 68 | 69 | # A complete implementation of current DST rules for major US time zones. 70 | 71 | def first_sunday_on_or_after(dt): 72 | days_to_go = 6 - dt.weekday() 73 | if days_to_go: 74 | dt += timedelta(days_to_go) 75 | return dt 76 | 77 | # In the US, DST starts at 2am (standard time) on the first Sunday in April. 78 | DSTSTART = datetime(1, 4, 1, 2) 79 | # and ends at 2am (DST time; 1am standard time) on the last Sunday of Oct. 80 | # which is the first Sunday on or after Oct 25. 81 | DSTEND = datetime(1, 10, 25, 1) 82 | 83 | class USTimeZone(tzinfo): 84 | 85 | def __init__(self, hours, reprname, stdname, dstname): 86 | self.stdoffset = timedelta(hours=hours) 87 | self.reprname = reprname 88 | self.stdname = stdname 89 | self.dstname = dstname 90 | 91 | def __repr__(self): 92 | return self.reprname 93 | 94 | def tzname(self, dt): 95 | if self.dst(dt): 96 | return self.dstname 97 | else: 98 | return self.stdname 99 | 100 | def utcoffset(self, dt): 101 | return self.stdoffset + self.dst(dt) 102 | 103 | def dst(self, dt): 104 | if dt is None or dt.tzinfo is None: 105 | # An exception may be sensible here, in one or both cases. 106 | # It depends on how you want to treat them. The default 107 | # fromutc() implementation (called by the default astimezone() 108 | # implementation) passes a datetime with dt.tzinfo is self. 109 | return ZERO 110 | assert dt.tzinfo is self 111 | 112 | # Find first Sunday in April & the last in October. 113 | start = first_sunday_on_or_after(DSTSTART.replace(year=dt.year)) 114 | end = first_sunday_on_or_after(DSTEND.replace(year=dt.year)) 115 | 116 | # Can't compare naive to aware objects, so strip the timezone from 117 | # dt first. 118 | if start <= dt.replace(tzinfo=None) < end: 119 | return HOUR 120 | else: 121 | return ZERO 122 | 123 | Eastern = USTimeZone(-5, "Eastern", "EST", "EDT") 124 | Central = USTimeZone(-6, "Central", "CST", "CDT") 125 | Mountain = USTimeZone(-7, "Mountain", "MST", "MDT") 126 | Pacific = USTimeZone(-8, "Pacific", "PST", "PDT") 127 | 128 | -------------------------------------------------------------------------------- /template/tests/test_docs.py: -------------------------------------------------------------------------------- 1 | # -*- coding: ascii -*- 2 | 3 | from doctest import DocTestSuite 4 | import unittest, os, os.path, sys 5 | import warnings 6 | 7 | # We test the documentation this way instead of using DocFileSuite so 8 | # we can run the tests under Python 2.3 9 | def test_README(): 10 | pass 11 | 12 | this_dir = os.path.dirname(__file__) 13 | locs = [ 14 | os.path.join(this_dir, os.pardir, 'README.txt'), 15 | os.path.join(this_dir, os.pardir, os.pardir, 'README.txt'), 16 | ] 17 | for loc in locs: 18 | if os.path.exists(loc): 19 | test_README.__doc__ = open(loc).read() 20 | break 21 | if test_README.__doc__ is None: 22 | raise RuntimeError('README.txt not found') 23 | 24 | 25 | def test_suite(): 26 | "For the Z3 test runner" 27 | return DocTestSuite() 28 | 29 | 30 | if __name__ == '__main__': 31 | sys.path.insert(0, os.path.abspath(os.path.join( 32 | this_dir, os.pardir, os.pardir 33 | ))) 34 | unittest.main(defaultTest='test_suite') 35 | 36 | 37 | -------------------------------------------------------------------------------- /template/tests/test_pytz_appengine.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test the pytz-appengine specific components 3 | """ 4 | 5 | import pytz 6 | import logging 7 | import unittest 8 | 9 | 10 | class pytzAppengineTest(unittest.TestCase): 11 | """ 12 | Check that loading works as expected 13 | and that we see the appropriate model instances 14 | """ 15 | def test_pytz_appengine(self): 16 | """Check that pytz-appengine is used""" 17 | self.assertTrue(pytz.APPENGINE_PYTZ) 18 | 19 | def test_zones(self): 20 | """Check that the models do what we expect""" 21 | from pytz import NDB_NAMESPACE, Zoneinfo 22 | from google.appengine.ext import ndb 23 | 24 | est = pytz.timezone('Canada/Eastern') 25 | 26 | logging.error(est) 27 | 28 | EXPECT_ZONES = 589 # this may change with each iteration 29 | 30 | zones = Zoneinfo.query(namespace=NDB_NAMESPACE).count() 31 | 32 | self.assertEqual(zones, EXPECT_ZONES) 33 | -------------------------------------------------------------------------------- /template/tests/test_tzinfo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: ascii -*- 2 | 3 | import sys, os, os.path 4 | import unittest, doctest 5 | try: 6 | import cPickle as pickle 7 | except ImportError: 8 | import pickle 9 | from datetime import datetime, time, timedelta, tzinfo 10 | import warnings 11 | 12 | if __name__ == '__main__': 13 | # Only munge path if invoked as a script. Testrunners should have setup 14 | # the paths already 15 | sys.path.insert(0, os.path.abspath(os.path.join(os.pardir, os.pardir))) 16 | 17 | import pytz 18 | from pytz import reference 19 | from pytz.tzfile import _byte_string 20 | from pytz.tzinfo import DstTzInfo, StaticTzInfo 21 | 22 | # I test for expected version to ensure the correct version of pytz is 23 | # actually being tested. 24 | EXPECTED_VERSION='2013.8' 25 | 26 | fmt = '%Y-%m-%d %H:%M:%S %Z%z' 27 | 28 | NOTIME = timedelta(0) 29 | 30 | # GMT is a tzinfo.StaticTzInfo--the class we primarily want to test--while 31 | # UTC is reference implementation. They both have the same timezone meaning. 32 | UTC = pytz.timezone('UTC') 33 | GMT = pytz.timezone('GMT') 34 | assert isinstance(GMT, StaticTzInfo), 'GMT is no longer a StaticTzInfo' 35 | 36 | def prettydt(dt): 37 | """datetime as a string using a known format. 38 | 39 | We don't use strftime as it doesn't handle years earlier than 1900 40 | per http://bugs.python.org/issue1777412 41 | """ 42 | if dt.utcoffset() >= timedelta(0): 43 | offset = '+%s' % (dt.utcoffset(),) 44 | else: 45 | offset = '-%s' % (-1 * dt.utcoffset(),) 46 | return '%04d-%02d-%02d %02d:%02d:%02d %s %s' % ( 47 | dt.year, dt.month, dt.day, 48 | dt.hour, dt.minute, dt.second, 49 | dt.tzname(), offset) 50 | 51 | 52 | try: 53 | unicode 54 | except NameError: 55 | # Python 3.x doesn't have unicode(), making writing code 56 | # for Python 2.3 and Python 3.x a pain. 57 | unicode = str 58 | 59 | 60 | class BasicTest(unittest.TestCase): 61 | 62 | def testVersion(self): 63 | # Ensuring the correct version of pytz has been loaded 64 | self.assertEqual(EXPECTED_VERSION, pytz.__version__, 65 | 'Incorrect pytz version loaded. Import path is stuffed ' 66 | 'or this test needs updating. (Wanted %s, got %s)' 67 | % (EXPECTED_VERSION, pytz.__version__) 68 | ) 69 | 70 | def testGMT(self): 71 | now = datetime.now(tz=GMT) 72 | self.assertTrue(now.utcoffset() == NOTIME) 73 | self.assertTrue(now.dst() == NOTIME) 74 | self.assertTrue(now.timetuple() == now.utctimetuple()) 75 | self.assertTrue(now==now.replace(tzinfo=UTC)) 76 | 77 | def testReferenceUTC(self): 78 | now = datetime.now(tz=UTC) 79 | self.assertTrue(now.utcoffset() == NOTIME) 80 | self.assertTrue(now.dst() == NOTIME) 81 | self.assertTrue(now.timetuple() == now.utctimetuple()) 82 | 83 | def testUnknownOffsets(self): 84 | # This tzinfo behavior is required to make 85 | # datetime.time.{utcoffset, dst, tzname} work as documented. 86 | 87 | dst_tz = pytz.timezone('US/Eastern') 88 | 89 | # This information is not known when we don't have a date, 90 | # so return None per API. 91 | self.assertTrue(dst_tz.utcoffset(None) is None) 92 | self.assertTrue(dst_tz.dst(None) is None) 93 | # We don't know the abbreviation, but this is still a valid 94 | # tzname per the Python documentation. 95 | self.assertEqual(dst_tz.tzname(None), 'US/Eastern') 96 | 97 | def clearCache(self): 98 | pytz._tzinfo_cache.clear() 99 | 100 | def testUnicodeTimezone(self): 101 | # We need to ensure that cold lookups work for both Unicode 102 | # and traditional strings, and that the desired singleton is 103 | # returned. 104 | self.clearCache() 105 | eastern = pytz.timezone(unicode('US/Eastern')) 106 | self.assertTrue(eastern is pytz.timezone('US/Eastern')) 107 | 108 | self.clearCache() 109 | eastern = pytz.timezone('US/Eastern') 110 | self.assertTrue(eastern is pytz.timezone(unicode('US/Eastern'))) 111 | 112 | 113 | class PicklingTest(unittest.TestCase): 114 | 115 | def _roundtrip_tzinfo(self, tz): 116 | p = pickle.dumps(tz) 117 | unpickled_tz = pickle.loads(p) 118 | self.assertTrue(tz is unpickled_tz, '%s did not roundtrip' % tz.zone) 119 | 120 | def _roundtrip_datetime(self, dt): 121 | # Ensure that the tzinfo attached to a datetime instance 122 | # is identical to the one returned. This is important for 123 | # DST timezones, as some state is stored in the tzinfo. 124 | tz = dt.tzinfo 125 | p = pickle.dumps(dt) 126 | unpickled_dt = pickle.loads(p) 127 | unpickled_tz = unpickled_dt.tzinfo 128 | self.assertTrue(tz is unpickled_tz, '%s did not roundtrip' % tz.zone) 129 | 130 | def testDst(self): 131 | tz = pytz.timezone('Europe/Amsterdam') 132 | dt = datetime(2004, 2, 1, 0, 0, 0) 133 | 134 | for localized_tz in tz._tzinfos.values(): 135 | self._roundtrip_tzinfo(localized_tz) 136 | self._roundtrip_datetime(dt.replace(tzinfo=localized_tz)) 137 | 138 | def testRoundtrip(self): 139 | dt = datetime(2004, 2, 1, 0, 0, 0) 140 | for zone in pytz.all_timezones: 141 | tz = pytz.timezone(zone) 142 | self._roundtrip_tzinfo(tz) 143 | 144 | def testDatabaseFixes(self): 145 | # Hack the pickle to make it refer to a timezone abbreviation 146 | # that does not match anything. The unpickler should be able 147 | # to repair this case 148 | tz = pytz.timezone('Australia/Melbourne') 149 | p = pickle.dumps(tz) 150 | tzname = tz._tzname 151 | hacked_p = p.replace(_byte_string(tzname), _byte_string('???')) 152 | self.assertNotEqual(p, hacked_p) 153 | unpickled_tz = pickle.loads(hacked_p) 154 | self.assertTrue(tz is unpickled_tz) 155 | 156 | # Simulate a database correction. In this case, the incorrect 157 | # data will continue to be used. 158 | p = pickle.dumps(tz) 159 | new_utcoffset = tz._utcoffset.seconds + 42 160 | 161 | # Python 3 introduced a new pickle protocol where numbers are stored in 162 | # hexadecimal representation. Here we extract the pickle 163 | # representation of the number for the current Python version. 164 | old_pickle_pattern = pickle.dumps(tz._utcoffset.seconds)[3:-1] 165 | new_pickle_pattern = pickle.dumps(new_utcoffset)[3:-1] 166 | hacked_p = p.replace(old_pickle_pattern, new_pickle_pattern) 167 | 168 | self.assertNotEqual(p, hacked_p) 169 | unpickled_tz = pickle.loads(hacked_p) 170 | self.assertEqual(unpickled_tz._utcoffset.seconds, new_utcoffset) 171 | self.assertTrue(tz is not unpickled_tz) 172 | 173 | def testOldPickles(self): 174 | # Ensure that applications serializing pytz instances as pickles 175 | # have no troubles upgrading to a new pytz release. These pickles 176 | # where created with pytz2006j 177 | east1 = pickle.loads(_byte_string( 178 | "cpytz\n_p\np1\n(S'US/Eastern'\np2\nI-18000\n" 179 | "I0\nS'EST'\np3\ntRp4\n." 180 | )) 181 | east2 = pytz.timezone('US/Eastern') 182 | self.assertTrue(east1 is east2) 183 | 184 | # Confirm changes in name munging between 2006j and 2007c cause 185 | # no problems. 186 | pap1 = pickle.loads(_byte_string( 187 | "cpytz\n_p\np1\n(S'America/Port_minus_au_minus_Prince'" 188 | "\np2\nI-17340\nI0\nS'PPMT'\np3\ntRp4\n.")) 189 | pap2 = pytz.timezone('America/Port-au-Prince') 190 | self.assertTrue(pap1 is pap2) 191 | 192 | gmt1 = pickle.loads(_byte_string( 193 | "cpytz\n_p\np1\n(S'Etc/GMT_plus_10'\np2\ntRp3\n.")) 194 | gmt2 = pytz.timezone('Etc/GMT+10') 195 | self.assertTrue(gmt1 is gmt2) 196 | 197 | 198 | class USEasternDSTStartTestCase(unittest.TestCase): 199 | tzinfo = pytz.timezone('US/Eastern') 200 | 201 | # 24 hours before DST changeover 202 | transition_time = datetime(2002, 4, 7, 7, 0, 0, tzinfo=UTC) 203 | 204 | # Increase for 'flexible' DST transitions due to 1 minute granularity 205 | # of Python's datetime library 206 | instant = timedelta(seconds=1) 207 | 208 | # before transition 209 | before = { 210 | 'tzname': 'EST', 211 | 'utcoffset': timedelta(hours = -5), 212 | 'dst': timedelta(hours = 0), 213 | } 214 | 215 | # after transition 216 | after = { 217 | 'tzname': 'EDT', 218 | 'utcoffset': timedelta(hours = -4), 219 | 'dst': timedelta(hours = 1), 220 | } 221 | 222 | def _test_tzname(self, utc_dt, wanted): 223 | tzname = wanted['tzname'] 224 | dt = utc_dt.astimezone(self.tzinfo) 225 | self.assertEqual(dt.tzname(), tzname, 226 | 'Expected %s as tzname for %s. Got %s' % ( 227 | tzname, str(utc_dt), dt.tzname() 228 | ) 229 | ) 230 | 231 | def _test_utcoffset(self, utc_dt, wanted): 232 | utcoffset = wanted['utcoffset'] 233 | dt = utc_dt.astimezone(self.tzinfo) 234 | self.assertEqual( 235 | dt.utcoffset(), wanted['utcoffset'], 236 | 'Expected %s as utcoffset for %s. Got %s' % ( 237 | utcoffset, utc_dt, dt.utcoffset() 238 | ) 239 | ) 240 | 241 | def _test_dst(self, utc_dt, wanted): 242 | dst = wanted['dst'] 243 | dt = utc_dt.astimezone(self.tzinfo) 244 | self.assertEqual(dt.dst(),dst, 245 | 'Expected %s as dst for %s. Got %s' % ( 246 | dst, utc_dt, dt.dst() 247 | ) 248 | ) 249 | 250 | def test_arithmetic(self): 251 | utc_dt = self.transition_time 252 | 253 | for days in range(-420, 720, 20): 254 | delta = timedelta(days=days) 255 | 256 | # Make sure we can get back where we started 257 | dt = utc_dt.astimezone(self.tzinfo) 258 | dt2 = dt + delta 259 | dt2 = dt2 - delta 260 | self.assertEqual(dt, dt2) 261 | 262 | # Make sure arithmetic crossing DST boundaries ends 263 | # up in the correct timezone after normalization 264 | utc_plus_delta = (utc_dt + delta).astimezone(self.tzinfo) 265 | local_plus_delta = self.tzinfo.normalize(dt + delta) 266 | self.assertEqual( 267 | prettydt(utc_plus_delta), 268 | prettydt(local_plus_delta), 269 | 'Incorrect result for delta==%d days. Wanted %r. Got %r'%( 270 | days, 271 | prettydt(utc_plus_delta), 272 | prettydt(local_plus_delta), 273 | ) 274 | ) 275 | 276 | def _test_all(self, utc_dt, wanted): 277 | self._test_utcoffset(utc_dt, wanted) 278 | self._test_tzname(utc_dt, wanted) 279 | self._test_dst(utc_dt, wanted) 280 | 281 | def testDayBefore(self): 282 | self._test_all( 283 | self.transition_time - timedelta(days=1), self.before 284 | ) 285 | 286 | def testTwoHoursBefore(self): 287 | self._test_all( 288 | self.transition_time - timedelta(hours=2), self.before 289 | ) 290 | 291 | def testHourBefore(self): 292 | self._test_all( 293 | self.transition_time - timedelta(hours=1), self.before 294 | ) 295 | 296 | def testInstantBefore(self): 297 | self._test_all( 298 | self.transition_time - self.instant, self.before 299 | ) 300 | 301 | def testTransition(self): 302 | self._test_all( 303 | self.transition_time, self.after 304 | ) 305 | 306 | def testInstantAfter(self): 307 | self._test_all( 308 | self.transition_time + self.instant, self.after 309 | ) 310 | 311 | def testHourAfter(self): 312 | self._test_all( 313 | self.transition_time + timedelta(hours=1), self.after 314 | ) 315 | 316 | def testTwoHoursAfter(self): 317 | self._test_all( 318 | self.transition_time + timedelta(hours=1), self.after 319 | ) 320 | 321 | def testDayAfter(self): 322 | self._test_all( 323 | self.transition_time + timedelta(days=1), self.after 324 | ) 325 | 326 | 327 | class USEasternDSTEndTestCase(USEasternDSTStartTestCase): 328 | tzinfo = pytz.timezone('US/Eastern') 329 | transition_time = datetime(2002, 10, 27, 6, 0, 0, tzinfo=UTC) 330 | before = { 331 | 'tzname': 'EDT', 332 | 'utcoffset': timedelta(hours = -4), 333 | 'dst': timedelta(hours = 1), 334 | } 335 | after = { 336 | 'tzname': 'EST', 337 | 'utcoffset': timedelta(hours = -5), 338 | 'dst': timedelta(hours = 0), 339 | } 340 | 341 | 342 | class USEasternEPTStartTestCase(USEasternDSTStartTestCase): 343 | transition_time = datetime(1945, 8, 14, 23, 0, 0, tzinfo=UTC) 344 | before = { 345 | 'tzname': 'EWT', 346 | 'utcoffset': timedelta(hours = -4), 347 | 'dst': timedelta(hours = 1), 348 | } 349 | after = { 350 | 'tzname': 'EPT', 351 | 'utcoffset': timedelta(hours = -4), 352 | 'dst': timedelta(hours = 1), 353 | } 354 | 355 | 356 | class USEasternEPTEndTestCase(USEasternDSTStartTestCase): 357 | transition_time = datetime(1945, 9, 30, 6, 0, 0, tzinfo=UTC) 358 | before = { 359 | 'tzname': 'EPT', 360 | 'utcoffset': timedelta(hours = -4), 361 | 'dst': timedelta(hours = 1), 362 | } 363 | after = { 364 | 'tzname': 'EST', 365 | 'utcoffset': timedelta(hours = -5), 366 | 'dst': timedelta(hours = 0), 367 | } 368 | 369 | 370 | class WarsawWMTEndTestCase(USEasternDSTStartTestCase): 371 | # In 1915, Warsaw changed from Warsaw to Central European time. 372 | # This involved the clocks being set backwards, causing a end-of-DST 373 | # like situation without DST being involved. 374 | tzinfo = pytz.timezone('Europe/Warsaw') 375 | transition_time = datetime(1915, 8, 4, 22, 36, 0, tzinfo=UTC) 376 | before = { 377 | 'tzname': 'WMT', 378 | 'utcoffset': timedelta(hours=1, minutes=24), 379 | 'dst': timedelta(0), 380 | } 381 | after = { 382 | 'tzname': 'CET', 383 | 'utcoffset': timedelta(hours=1), 384 | 'dst': timedelta(0), 385 | } 386 | 387 | 388 | class VilniusWMTEndTestCase(USEasternDSTStartTestCase): 389 | # At the end of 1916, Vilnius changed timezones putting its clock 390 | # forward by 11 minutes 35 seconds. Neither timezone was in DST mode. 391 | tzinfo = pytz.timezone('Europe/Vilnius') 392 | instant = timedelta(seconds=31) 393 | transition_time = datetime(1916, 12, 31, 22, 36, 00, tzinfo=UTC) 394 | before = { 395 | 'tzname': 'WMT', 396 | 'utcoffset': timedelta(hours=1, minutes=24), 397 | 'dst': timedelta(0), 398 | } 399 | after = { 400 | 'tzname': 'KMT', 401 | 'utcoffset': timedelta(hours=1, minutes=36), # Really 1:35:36 402 | 'dst': timedelta(0), 403 | } 404 | 405 | 406 | class VilniusCESTStartTestCase(USEasternDSTStartTestCase): 407 | # In 1941, Vilnius changed from MSG to CEST, switching to summer 408 | # time while simultaneously reducing its UTC offset by two hours, 409 | # causing the clocks to go backwards for this summer time 410 | # switchover. 411 | tzinfo = pytz.timezone('Europe/Vilnius') 412 | transition_time = datetime(1941, 6, 23, 21, 00, 00, tzinfo=UTC) 413 | before = { 414 | 'tzname': 'MSK', 415 | 'utcoffset': timedelta(hours=3), 416 | 'dst': timedelta(0), 417 | } 418 | after = { 419 | 'tzname': 'CEST', 420 | 'utcoffset': timedelta(hours=2), 421 | 'dst': timedelta(hours=1), 422 | } 423 | 424 | 425 | class LondonHistoryStartTestCase(USEasternDSTStartTestCase): 426 | # The first known timezone transition in London was in 1847 when 427 | # clocks where synchronized to GMT. However, we currently only 428 | # understand v1 format tzfile(5) files which does handle years 429 | # this far in the past, so our earliest known transition is in 430 | # 1916. 431 | tzinfo = pytz.timezone('Europe/London') 432 | # transition_time = datetime(1847, 12, 1, 1, 15, 00, tzinfo=UTC) 433 | # before = { 434 | # 'tzname': 'LMT', 435 | # 'utcoffset': timedelta(minutes=-75), 436 | # 'dst': timedelta(0), 437 | # } 438 | # after = { 439 | # 'tzname': 'GMT', 440 | # 'utcoffset': timedelta(0), 441 | # 'dst': timedelta(0), 442 | # } 443 | transition_time = datetime(1916, 5, 21, 2, 00, 00, tzinfo=UTC) 444 | before = { 445 | 'tzname': 'GMT', 446 | 'utcoffset': timedelta(0), 447 | 'dst': timedelta(0), 448 | } 449 | after = { 450 | 'tzname': 'BST', 451 | 'utcoffset': timedelta(hours=1), 452 | 'dst': timedelta(hours=1), 453 | } 454 | 455 | 456 | class LondonHistoryEndTestCase(USEasternDSTStartTestCase): 457 | # Timezone switchovers are projected into the future, even 458 | # though no official statements exist or could be believed even 459 | # if they did exist. We currently only check the last known 460 | # transition in 2037, as we are still using v1 format tzfile(5) 461 | # files. 462 | tzinfo = pytz.timezone('Europe/London') 463 | # transition_time = datetime(2499, 10, 25, 1, 0, 0, tzinfo=UTC) 464 | transition_time = datetime(2037, 10, 25, 1, 0, 0, tzinfo=UTC) 465 | before = { 466 | 'tzname': 'BST', 467 | 'utcoffset': timedelta(hours=1), 468 | 'dst': timedelta(hours=1), 469 | } 470 | after = { 471 | 'tzname': 'GMT', 472 | 'utcoffset': timedelta(0), 473 | 'dst': timedelta(0), 474 | } 475 | 476 | 477 | class NoumeaHistoryStartTestCase(USEasternDSTStartTestCase): 478 | # Noumea adopted a whole hour offset in 1912. Previously 479 | # it was 11 hours, 5 minutes and 48 seconds off UTC. However, 480 | # due to limitations of the Python datetime library, we need 481 | # to round that to 11 hours 6 minutes. 482 | tzinfo = pytz.timezone('Pacific/Noumea') 483 | transition_time = datetime(1912, 1, 12, 12, 54, 12, tzinfo=UTC) 484 | before = { 485 | 'tzname': 'LMT', 486 | 'utcoffset': timedelta(hours=11, minutes=6), 487 | 'dst': timedelta(0), 488 | } 489 | after = { 490 | 'tzname': 'NCT', 491 | 'utcoffset': timedelta(hours=11), 492 | 'dst': timedelta(0), 493 | } 494 | 495 | 496 | class NoumeaDSTEndTestCase(USEasternDSTStartTestCase): 497 | # Noumea dropped DST in 1997. 498 | tzinfo = pytz.timezone('Pacific/Noumea') 499 | transition_time = datetime(1997, 3, 1, 15, 00, 00, tzinfo=UTC) 500 | before = { 501 | 'tzname': 'NCST', 502 | 'utcoffset': timedelta(hours=12), 503 | 'dst': timedelta(hours=1), 504 | } 505 | after = { 506 | 'tzname': 'NCT', 507 | 'utcoffset': timedelta(hours=11), 508 | 'dst': timedelta(0), 509 | } 510 | 511 | 512 | class NoumeaNoMoreDSTTestCase(NoumeaDSTEndTestCase): 513 | # Noumea dropped DST in 1997. Here we test that it stops occuring. 514 | transition_time = ( 515 | NoumeaDSTEndTestCase.transition_time + timedelta(days=365*10)) 516 | before = NoumeaDSTEndTestCase.after 517 | after = NoumeaDSTEndTestCase.after 518 | 519 | 520 | class TahitiTestCase(USEasternDSTStartTestCase): 521 | # Tahiti has had a single transition in its history. 522 | tzinfo = pytz.timezone('Pacific/Tahiti') 523 | transition_time = datetime(1912, 10, 1, 9, 58, 16, tzinfo=UTC) 524 | before = { 525 | 'tzname': 'LMT', 526 | 'utcoffset': timedelta(hours=-9, minutes=-58), 527 | 'dst': timedelta(0), 528 | } 529 | after = { 530 | 'tzname': 'TAHT', 531 | 'utcoffset': timedelta(hours=-10), 532 | 'dst': timedelta(0), 533 | } 534 | 535 | 536 | class SamoaInternationalDateLineChange(USEasternDSTStartTestCase): 537 | # At the end of 2011, Samoa will switch from being east of the 538 | # international dateline to the west. There will be no Dec 30th 539 | # 2011 and it will switch from UTC-10 to UTC+14. 540 | tzinfo = pytz.timezone('Pacific/Apia') 541 | transition_time = datetime(2011, 12, 30, 10, 0, 0, tzinfo=UTC) 542 | before = { 543 | 'tzname': 'WSDT', 544 | 'utcoffset': timedelta(hours=-10), 545 | 'dst': timedelta(hours=1), 546 | } 547 | after = { 548 | 'tzname': 'WSDT', 549 | 'utcoffset': timedelta(hours=14), 550 | 'dst': timedelta(hours=1), 551 | } 552 | 553 | 554 | class ReferenceUSEasternDSTStartTestCase(USEasternDSTStartTestCase): 555 | tzinfo = reference.Eastern 556 | def test_arithmetic(self): 557 | # Reference implementation cannot handle this 558 | pass 559 | 560 | 561 | class ReferenceUSEasternDSTEndTestCase(USEasternDSTEndTestCase): 562 | tzinfo = reference.Eastern 563 | 564 | def testHourBefore(self): 565 | # Python's datetime library has a bug, where the hour before 566 | # a daylight savings transition is one hour out. For example, 567 | # at the end of US/Eastern daylight savings time, 01:00 EST 568 | # occurs twice (once at 05:00 UTC and once at 06:00 UTC), 569 | # whereas the first should actually be 01:00 EDT. 570 | # Note that this bug is by design - by accepting this ambiguity 571 | # for one hour one hour per year, an is_dst flag on datetime.time 572 | # became unnecessary. 573 | self._test_all( 574 | self.transition_time - timedelta(hours=1), self.after 575 | ) 576 | 577 | def testInstantBefore(self): 578 | self._test_all( 579 | self.transition_time - timedelta(seconds=1), self.after 580 | ) 581 | 582 | def test_arithmetic(self): 583 | # Reference implementation cannot handle this 584 | pass 585 | 586 | 587 | class LocalTestCase(unittest.TestCase): 588 | def testLocalize(self): 589 | loc_tz = pytz.timezone('Europe/Amsterdam') 590 | 591 | loc_time = loc_tz.localize(datetime(1930, 5, 10, 0, 0, 0)) 592 | # Actually +00:19:32, but Python datetime rounds this 593 | self.assertEqual(loc_time.strftime('%Z%z'), 'AMT+0020') 594 | 595 | loc_time = loc_tz.localize(datetime(1930, 5, 20, 0, 0, 0)) 596 | # Actually +00:19:32, but Python datetime rounds this 597 | self.assertEqual(loc_time.strftime('%Z%z'), 'NST+0120') 598 | 599 | loc_time = loc_tz.localize(datetime(1940, 5, 10, 0, 0, 0)) 600 | self.assertEqual(loc_time.strftime('%Z%z'), 'NET+0020') 601 | 602 | loc_time = loc_tz.localize(datetime(1940, 5, 20, 0, 0, 0)) 603 | self.assertEqual(loc_time.strftime('%Z%z'), 'CEST+0200') 604 | 605 | loc_time = loc_tz.localize(datetime(2004, 2, 1, 0, 0, 0)) 606 | self.assertEqual(loc_time.strftime('%Z%z'), 'CET+0100') 607 | 608 | loc_time = loc_tz.localize(datetime(2004, 4, 1, 0, 0, 0)) 609 | self.assertEqual(loc_time.strftime('%Z%z'), 'CEST+0200') 610 | 611 | tz = pytz.timezone('Europe/Amsterdam') 612 | loc_time = loc_tz.localize(datetime(1943, 3, 29, 1, 59, 59)) 613 | self.assertEqual(loc_time.strftime('%Z%z'), 'CET+0100') 614 | 615 | 616 | # Switch to US 617 | loc_tz = pytz.timezone('US/Eastern') 618 | 619 | # End of DST ambiguity check 620 | loc_time = loc_tz.localize(datetime(1918, 10, 27, 1, 59, 59), is_dst=1) 621 | self.assertEqual(loc_time.strftime('%Z%z'), 'EDT-0400') 622 | 623 | loc_time = loc_tz.localize(datetime(1918, 10, 27, 1, 59, 59), is_dst=0) 624 | self.assertEqual(loc_time.strftime('%Z%z'), 'EST-0500') 625 | 626 | self.assertRaises(pytz.AmbiguousTimeError, 627 | loc_tz.localize, datetime(1918, 10, 27, 1, 59, 59), is_dst=None 628 | ) 629 | 630 | # Start of DST non-existent times 631 | loc_time = loc_tz.localize(datetime(1918, 3, 31, 2, 0, 0), is_dst=0) 632 | self.assertEqual(loc_time.strftime('%Z%z'), 'EST-0500') 633 | 634 | loc_time = loc_tz.localize(datetime(1918, 3, 31, 2, 0, 0), is_dst=1) 635 | self.assertEqual(loc_time.strftime('%Z%z'), 'EDT-0400') 636 | 637 | self.assertRaises(pytz.NonExistentTimeError, 638 | loc_tz.localize, datetime(1918, 3, 31, 2, 0, 0), is_dst=None 639 | ) 640 | 641 | # Weird changes - war time and peace time both is_dst==True 642 | 643 | loc_time = loc_tz.localize(datetime(1942, 2, 9, 3, 0, 0)) 644 | self.assertEqual(loc_time.strftime('%Z%z'), 'EWT-0400') 645 | 646 | loc_time = loc_tz.localize(datetime(1945, 8, 14, 19, 0, 0)) 647 | self.assertEqual(loc_time.strftime('%Z%z'), 'EPT-0400') 648 | 649 | loc_time = loc_tz.localize(datetime(1945, 9, 30, 1, 0, 0), is_dst=1) 650 | self.assertEqual(loc_time.strftime('%Z%z'), 'EPT-0400') 651 | 652 | loc_time = loc_tz.localize(datetime(1945, 9, 30, 1, 0, 0), is_dst=0) 653 | self.assertEqual(loc_time.strftime('%Z%z'), 'EST-0500') 654 | 655 | def testNormalize(self): 656 | tz = pytz.timezone('US/Eastern') 657 | dt = datetime(2004, 4, 4, 7, 0, 0, tzinfo=UTC).astimezone(tz) 658 | dt2 = dt - timedelta(minutes=10) 659 | self.assertEqual( 660 | dt2.strftime('%Y-%m-%d %H:%M:%S %Z%z'), 661 | '2004-04-04 02:50:00 EDT-0400' 662 | ) 663 | 664 | dt2 = tz.normalize(dt2) 665 | self.assertEqual( 666 | dt2.strftime('%Y-%m-%d %H:%M:%S %Z%z'), 667 | '2004-04-04 01:50:00 EST-0500' 668 | ) 669 | 670 | def testPartialMinuteOffsets(self): 671 | # utcoffset in Amsterdam was not a whole minute until 1937 672 | # However, we fudge this by rounding them, as the Python 673 | # datetime library 674 | tz = pytz.timezone('Europe/Amsterdam') 675 | utc_dt = datetime(1914, 1, 1, 13, 40, 28, tzinfo=UTC) # correct 676 | utc_dt = utc_dt.replace(second=0) # But we need to fudge it 677 | loc_dt = utc_dt.astimezone(tz) 678 | self.assertEqual( 679 | loc_dt.strftime('%Y-%m-%d %H:%M:%S %Z%z'), 680 | '1914-01-01 14:00:00 AMT+0020' 681 | ) 682 | 683 | # And get back... 684 | utc_dt = loc_dt.astimezone(UTC) 685 | self.assertEqual( 686 | utc_dt.strftime('%Y-%m-%d %H:%M:%S %Z%z'), 687 | '1914-01-01 13:40:00 UTC+0000' 688 | ) 689 | 690 | def no_testCreateLocaltime(self): 691 | # It would be nice if this worked, but it doesn't. 692 | tz = pytz.timezone('Europe/Amsterdam') 693 | dt = datetime(2004, 10, 31, 2, 0, 0, tzinfo=tz) 694 | self.assertEqual( 695 | dt.strftime(fmt), 696 | '2004-10-31 02:00:00 CET+0100' 697 | ) 698 | 699 | 700 | class CommonTimezonesTestCase(unittest.TestCase): 701 | def test_bratislava(self): 702 | # Bratislava is the default timezone for Slovakia, but our 703 | # heuristics where not adding it to common_timezones. Ideally, 704 | # common_timezones should be populated from zone.tab at runtime, 705 | # but I'm hesitant to pay the startup cost as loading the list 706 | # on demand whilst remaining backwards compatible seems 707 | # difficult. 708 | self.assertTrue('Europe/Bratislava' in pytz.common_timezones) 709 | self.assertTrue('Europe/Bratislava' in pytz.common_timezones_set) 710 | 711 | def test_us_eastern(self): 712 | self.assertTrue('US/Eastern' in pytz.common_timezones) 713 | self.assertTrue('US/Eastern' in pytz.common_timezones_set) 714 | 715 | def test_belfast(self): 716 | # Belfast uses London time. 717 | self.assertTrue('Europe/Belfast' in pytz.all_timezones_set) 718 | self.assertFalse('Europe/Belfast' in pytz.common_timezones) 719 | self.assertFalse('Europe/Belfast' in pytz.common_timezones_set) 720 | 721 | 722 | class BaseTzInfoTestCase: 723 | '''Ensure UTC, StaticTzInfo and DstTzInfo work consistently. 724 | 725 | These tests are run for each type of tzinfo. 726 | ''' 727 | tz = None # override 728 | tz_class = None # override 729 | 730 | def test_expectedclass(self): 731 | self.assertTrue(isinstance(self.tz, self.tz_class)) 732 | 733 | def test_fromutc(self): 734 | # naive datetime. 735 | dt1 = datetime(2011, 10, 31) 736 | 737 | # localized datetime, same timezone. 738 | dt2 = self.tz.localize(dt1) 739 | 740 | # Both should give the same results. Note that the standard 741 | # Python tzinfo.fromutc() only supports the second. 742 | for dt in [dt1, dt2]: 743 | loc_dt = self.tz.fromutc(dt) 744 | loc_dt2 = pytz.utc.localize(dt1).astimezone(self.tz) 745 | self.assertEqual(loc_dt, loc_dt2) 746 | 747 | # localized datetime, different timezone. 748 | new_tz = pytz.timezone('Europe/Paris') 749 | self.assertTrue(self.tz is not new_tz) 750 | dt3 = new_tz.localize(dt1) 751 | self.assertRaises(ValueError, self.tz.fromutc, dt3) 752 | 753 | def test_normalize(self): 754 | other_tz = pytz.timezone('Europe/Paris') 755 | self.assertTrue(self.tz is not other_tz) 756 | 757 | dt = datetime(2012, 3, 26, 12, 0) 758 | other_dt = other_tz.localize(dt) 759 | 760 | local_dt = self.tz.normalize(other_dt) 761 | 762 | self.assertTrue(local_dt.tzinfo is not other_dt.tzinfo) 763 | self.assertNotEqual( 764 | local_dt.replace(tzinfo=None), other_dt.replace(tzinfo=None)) 765 | 766 | def test_astimezone(self): 767 | other_tz = pytz.timezone('Europe/Paris') 768 | self.assertTrue(self.tz is not other_tz) 769 | 770 | dt = datetime(2012, 3, 26, 12, 0) 771 | other_dt = other_tz.localize(dt) 772 | 773 | local_dt = other_dt.astimezone(self.tz) 774 | 775 | self.assertTrue(local_dt.tzinfo is not other_dt.tzinfo) 776 | self.assertNotEqual( 777 | local_dt.replace(tzinfo=None), other_dt.replace(tzinfo=None)) 778 | 779 | 780 | class OptimizedUTCTestCase(unittest.TestCase, BaseTzInfoTestCase): 781 | tz = pytz.utc 782 | tz_class = tz.__class__ 783 | 784 | 785 | class LegacyUTCTestCase(unittest.TestCase, BaseTzInfoTestCase): 786 | # Deprecated timezone, but useful for comparison tests. 787 | tz = pytz.timezone('Etc/UTC') 788 | tz_class = StaticTzInfo 789 | 790 | 791 | class StaticTzInfoTestCase(unittest.TestCase, BaseTzInfoTestCase): 792 | tz = pytz.timezone('GMT') 793 | tz_class = StaticTzInfo 794 | 795 | 796 | class DstTzInfoTestCase(unittest.TestCase, BaseTzInfoTestCase): 797 | tz = pytz.timezone('Australia/Melbourne') 798 | tz_class = DstTzInfo 799 | 800 | 801 | def test_suite(): 802 | suite = unittest.TestSuite() 803 | suite.addTest(doctest.DocTestSuite('pytz')) 804 | suite.addTest(doctest.DocTestSuite('pytz.tzinfo')) 805 | import test_tzinfo 806 | suite.addTest(unittest.defaultTestLoader.loadTestsFromModule(test_tzinfo)) 807 | return suite 808 | 809 | 810 | if __name__ == '__main__': 811 | warnings.simplefilter("error") # Warnings should be fatal in tests. 812 | unittest.main(defaultTest='test_suite') 813 | 814 | -------------------------------------------------------------------------------- /template/tzfile.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ''' 3 | $Id: tzfile.py,v 1.8 2004/06/03 00:15:24 zenzen Exp $ 4 | ''' 5 | 6 | try: 7 | from cStringIO import StringIO 8 | except ImportError: 9 | from io import StringIO 10 | from datetime import datetime, timedelta 11 | from struct import unpack, calcsize 12 | 13 | from pytz.tzinfo import StaticTzInfo, DstTzInfo, memorized_ttinfo 14 | from pytz.tzinfo import memorized_datetime, memorized_timedelta 15 | 16 | def _byte_string(s): 17 | """Cast a string or byte string to an ASCII byte string.""" 18 | return s.encode('US-ASCII') 19 | 20 | _NULL = _byte_string('\0') 21 | 22 | def _std_string(s): 23 | """Cast a string or byte string to an ASCII string.""" 24 | return str(s.decode('US-ASCII')) 25 | 26 | def build_tzinfo(zone, fp): 27 | head_fmt = '>4s c 15x 6l' 28 | head_size = calcsize(head_fmt) 29 | (magic, format, ttisgmtcnt, ttisstdcnt,leapcnt, timecnt, 30 | typecnt, charcnt) = unpack(head_fmt, fp.read(head_size)) 31 | 32 | # Make sure it is a tzfile(5) file 33 | assert magic == _byte_string('TZif'), 'Got magic %s' % repr(magic) 34 | 35 | # Read out the transition times, localtime indices and ttinfo structures. 36 | data_fmt = '>%(timecnt)dl %(timecnt)dB %(ttinfo)s %(charcnt)ds' % dict( 37 | timecnt=timecnt, ttinfo='lBB'*typecnt, charcnt=charcnt) 38 | data_size = calcsize(data_fmt) 39 | data = unpack(data_fmt, fp.read(data_size)) 40 | 41 | # make sure we unpacked the right number of values 42 | assert len(data) == 2 * timecnt + 3 * typecnt + 1 43 | transitions = [memorized_datetime(trans) 44 | for trans in data[:timecnt]] 45 | lindexes = list(data[timecnt:2 * timecnt]) 46 | ttinfo_raw = data[2 * timecnt:-1] 47 | tznames_raw = data[-1] 48 | del data 49 | 50 | # Process ttinfo into separate structs 51 | ttinfo = [] 52 | tznames = {} 53 | i = 0 54 | while i < len(ttinfo_raw): 55 | # have we looked up this timezone name yet? 56 | tzname_offset = ttinfo_raw[i+2] 57 | if tzname_offset not in tznames: 58 | nul = tznames_raw.find(_NULL, tzname_offset) 59 | if nul < 0: 60 | nul = len(tznames_raw) 61 | tznames[tzname_offset] = _std_string( 62 | tznames_raw[tzname_offset:nul]) 63 | ttinfo.append((ttinfo_raw[i], 64 | bool(ttinfo_raw[i+1]), 65 | tznames[tzname_offset])) 66 | i += 3 67 | 68 | # Now build the timezone object 69 | if len(transitions) == 0: 70 | ttinfo[0][0], ttinfo[0][2] 71 | cls = type(zone, (StaticTzInfo,), dict( 72 | zone=zone, 73 | _utcoffset=memorized_timedelta(ttinfo[0][0]), 74 | _tzname=ttinfo[0][2])) 75 | else: 76 | # Early dates use the first standard time ttinfo 77 | i = 0 78 | while ttinfo[i][1]: 79 | i += 1 80 | if ttinfo[i] == ttinfo[lindexes[0]]: 81 | transitions[0] = datetime.min 82 | else: 83 | transitions.insert(0, datetime.min) 84 | lindexes.insert(0, i) 85 | 86 | # calculate transition info 87 | transition_info = [] 88 | for i in range(len(transitions)): 89 | inf = ttinfo[lindexes[i]] 90 | utcoffset = inf[0] 91 | if not inf[1]: 92 | dst = 0 93 | else: 94 | for j in range(i-1, -1, -1): 95 | prev_inf = ttinfo[lindexes[j]] 96 | if not prev_inf[1]: 97 | break 98 | dst = inf[0] - prev_inf[0] # dst offset 99 | 100 | # Bad dst? Look further. DST > 24 hours happens when 101 | # a timzone has moved across the international dateline. 102 | if dst <= 0 or dst > 3600*3: 103 | for j in range(i+1, len(transitions)): 104 | stdinf = ttinfo[lindexes[j]] 105 | if not stdinf[1]: 106 | dst = inf[0] - stdinf[0] 107 | if dst > 0: 108 | break # Found a useful std time. 109 | 110 | tzname = inf[2] 111 | 112 | # Round utcoffset and dst to the nearest minute or the 113 | # datetime library will complain. Conversions to these timezones 114 | # might be up to plus or minus 30 seconds out, but it is 115 | # the best we can do. 116 | utcoffset = int((utcoffset + 30) // 60) * 60 117 | dst = int((dst + 30) // 60) * 60 118 | transition_info.append(memorized_ttinfo(utcoffset, dst, tzname)) 119 | 120 | cls = type(zone, (DstTzInfo,), dict( 121 | zone=zone, 122 | _utc_transition_times=transitions, 123 | _transition_info=transition_info)) 124 | 125 | return cls() 126 | 127 | if __name__ == '__main__': 128 | import os.path 129 | from pprint import pprint 130 | base = os.path.join(os.path.dirname(__file__), 'zoneinfo') 131 | tz = build_tzinfo('Australia/Melbourne', 132 | open(os.path.join(base,'Australia','Melbourne'), 'rb')) 133 | tz = build_tzinfo('US/Eastern', 134 | open(os.path.join(base,'US','Eastern'), 'rb')) 135 | pprint(tz._utc_transition_times) 136 | #print tz.asPython(4) 137 | #print tz.transitions_mapping 138 | -------------------------------------------------------------------------------- /template/tzinfo.py: -------------------------------------------------------------------------------- 1 | '''Base classes and helpers for building zone specific tzinfo classes''' 2 | 3 | from datetime import datetime, timedelta, tzinfo 4 | from bisect import bisect_right 5 | try: 6 | set 7 | except NameError: 8 | from sets import Set as set 9 | 10 | import pytz 11 | from pytz.exceptions import AmbiguousTimeError, NonExistentTimeError 12 | 13 | __all__ = [] 14 | 15 | _timedelta_cache = {} 16 | def memorized_timedelta(seconds): 17 | '''Create only one instance of each distinct timedelta''' 18 | try: 19 | return _timedelta_cache[seconds] 20 | except KeyError: 21 | delta = timedelta(seconds=seconds) 22 | _timedelta_cache[seconds] = delta 23 | return delta 24 | 25 | _epoch = datetime.utcfromtimestamp(0) 26 | _datetime_cache = {0: _epoch} 27 | def memorized_datetime(seconds): 28 | '''Create only one instance of each distinct datetime''' 29 | try: 30 | return _datetime_cache[seconds] 31 | except KeyError: 32 | # NB. We can't just do datetime.utcfromtimestamp(seconds) as this 33 | # fails with negative values under Windows (Bug #90096) 34 | dt = _epoch + timedelta(seconds=seconds) 35 | _datetime_cache[seconds] = dt 36 | return dt 37 | 38 | _ttinfo_cache = {} 39 | def memorized_ttinfo(*args): 40 | '''Create only one instance of each distinct tuple''' 41 | try: 42 | return _ttinfo_cache[args] 43 | except KeyError: 44 | ttinfo = ( 45 | memorized_timedelta(args[0]), 46 | memorized_timedelta(args[1]), 47 | args[2] 48 | ) 49 | _ttinfo_cache[args] = ttinfo 50 | return ttinfo 51 | 52 | _notime = memorized_timedelta(0) 53 | 54 | def _to_seconds(td): 55 | '''Convert a timedelta to seconds''' 56 | return td.seconds + td.days * 24 * 60 * 60 57 | 58 | 59 | class BaseTzInfo(tzinfo): 60 | # Overridden in subclass 61 | _utcoffset = None 62 | _tzname = None 63 | zone = None 64 | 65 | def __str__(self): 66 | return self.zone 67 | 68 | 69 | class StaticTzInfo(BaseTzInfo): 70 | '''A timezone that has a constant offset from UTC 71 | 72 | These timezones are rare, as most locations have changed their 73 | offset at some point in their history 74 | ''' 75 | def fromutc(self, dt): 76 | '''See datetime.tzinfo.fromutc''' 77 | if dt.tzinfo is not None and dt.tzinfo is not self: 78 | raise ValueError('fromutc: dt.tzinfo is not self') 79 | return (dt + self._utcoffset).replace(tzinfo=self) 80 | 81 | def utcoffset(self, dt, is_dst=None): 82 | '''See datetime.tzinfo.utcoffset 83 | 84 | is_dst is ignored for StaticTzInfo, and exists only to 85 | retain compatibility with DstTzInfo. 86 | ''' 87 | return self._utcoffset 88 | 89 | def dst(self, dt, is_dst=None): 90 | '''See datetime.tzinfo.dst 91 | 92 | is_dst is ignored for StaticTzInfo, and exists only to 93 | retain compatibility with DstTzInfo. 94 | ''' 95 | return _notime 96 | 97 | def tzname(self, dt, is_dst=None): 98 | '''See datetime.tzinfo.tzname 99 | 100 | is_dst is ignored for StaticTzInfo, and exists only to 101 | retain compatibility with DstTzInfo. 102 | ''' 103 | return self._tzname 104 | 105 | def localize(self, dt, is_dst=False): 106 | '''Convert naive time to local time''' 107 | if dt.tzinfo is not None: 108 | raise ValueError('Not naive datetime (tzinfo is already set)') 109 | return dt.replace(tzinfo=self) 110 | 111 | def normalize(self, dt, is_dst=False): 112 | '''Correct the timezone information on the given datetime. 113 | 114 | This is normally a no-op, as StaticTzInfo timezones never have 115 | ambiguous cases to correct: 116 | 117 | >>> from pytz import timezone 118 | >>> gmt = timezone('GMT') 119 | >>> isinstance(gmt, StaticTzInfo) 120 | True 121 | >>> dt = datetime(2011, 5, 8, 1, 2, 3, tzinfo=gmt) 122 | >>> gmt.normalize(dt) is dt 123 | True 124 | 125 | The supported method of converting between timezones is to use 126 | datetime.astimezone(). Currently normalize() also works: 127 | 128 | >>> la = timezone('America/Los_Angeles') 129 | >>> dt = la.localize(datetime(2011, 5, 7, 1, 2, 3)) 130 | >>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)' 131 | >>> gmt.normalize(dt).strftime(fmt) 132 | '2011-05-07 08:02:03 GMT (+0000)' 133 | ''' 134 | if dt.tzinfo is self: 135 | return dt 136 | if dt.tzinfo is None: 137 | raise ValueError('Naive time - no tzinfo set') 138 | return dt.astimezone(self) 139 | 140 | def __repr__(self): 141 | return '' % (self.zone,) 142 | 143 | def __reduce__(self): 144 | # Special pickle to zone remains a singleton and to cope with 145 | # database changes. 146 | return pytz._p, (self.zone,) 147 | 148 | 149 | class DstTzInfo(BaseTzInfo): 150 | '''A timezone that has a variable offset from UTC 151 | 152 | The offset might change if daylight savings time comes into effect, 153 | or at a point in history when the region decides to change their 154 | timezone definition. 155 | ''' 156 | # Overridden in subclass 157 | _utc_transition_times = None # Sorted list of DST transition times in UTC 158 | _transition_info = None # [(utcoffset, dstoffset, tzname)] corresponding 159 | # to _utc_transition_times entries 160 | zone = None 161 | 162 | # Set in __init__ 163 | _tzinfos = None 164 | _dst = None # DST offset 165 | 166 | def __init__(self, _inf=None, _tzinfos=None): 167 | if _inf: 168 | self._tzinfos = _tzinfos 169 | self._utcoffset, self._dst, self._tzname = _inf 170 | else: 171 | _tzinfos = {} 172 | self._tzinfos = _tzinfos 173 | self._utcoffset, self._dst, self._tzname = self._transition_info[0] 174 | _tzinfos[self._transition_info[0]] = self 175 | for inf in self._transition_info[1:]: 176 | if inf not in _tzinfos: 177 | _tzinfos[inf] = self.__class__(inf, _tzinfos) 178 | 179 | def fromutc(self, dt): 180 | '''See datetime.tzinfo.fromutc''' 181 | if (dt.tzinfo is not None 182 | and getattr(dt.tzinfo, '_tzinfos', None) is not self._tzinfos): 183 | raise ValueError('fromutc: dt.tzinfo is not self') 184 | dt = dt.replace(tzinfo=None) 185 | idx = max(0, bisect_right(self._utc_transition_times, dt) - 1) 186 | inf = self._transition_info[idx] 187 | return (dt + inf[0]).replace(tzinfo=self._tzinfos[inf]) 188 | 189 | def normalize(self, dt): 190 | '''Correct the timezone information on the given datetime 191 | 192 | If date arithmetic crosses DST boundaries, the tzinfo 193 | is not magically adjusted. This method normalizes the 194 | tzinfo to the correct one. 195 | 196 | To test, first we need to do some setup 197 | 198 | >>> from pytz import timezone 199 | >>> utc = timezone('UTC') 200 | >>> eastern = timezone('US/Eastern') 201 | >>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)' 202 | 203 | We next create a datetime right on an end-of-DST transition point, 204 | the instant when the wallclocks are wound back one hour. 205 | 206 | >>> utc_dt = datetime(2002, 10, 27, 6, 0, 0, tzinfo=utc) 207 | >>> loc_dt = utc_dt.astimezone(eastern) 208 | >>> loc_dt.strftime(fmt) 209 | '2002-10-27 01:00:00 EST (-0500)' 210 | 211 | Now, if we subtract a few minutes from it, note that the timezone 212 | information has not changed. 213 | 214 | >>> before = loc_dt - timedelta(minutes=10) 215 | >>> before.strftime(fmt) 216 | '2002-10-27 00:50:00 EST (-0500)' 217 | 218 | But we can fix that by calling the normalize method 219 | 220 | >>> before = eastern.normalize(before) 221 | >>> before.strftime(fmt) 222 | '2002-10-27 01:50:00 EDT (-0400)' 223 | 224 | The supported method of converting between timezones is to use 225 | datetime.astimezone(). Currently, normalize() also works: 226 | 227 | >>> th = timezone('Asia/Bangkok') 228 | >>> am = timezone('Europe/Amsterdam') 229 | >>> dt = th.localize(datetime(2011, 5, 7, 1, 2, 3)) 230 | >>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)' 231 | >>> am.normalize(dt).strftime(fmt) 232 | '2011-05-06 20:02:03 CEST (+0200)' 233 | ''' 234 | if dt.tzinfo is None: 235 | raise ValueError('Naive time - no tzinfo set') 236 | 237 | # Convert dt in localtime to UTC 238 | offset = dt.tzinfo._utcoffset 239 | dt = dt.replace(tzinfo=None) 240 | dt = dt - offset 241 | # convert it back, and return it 242 | return self.fromutc(dt) 243 | 244 | def localize(self, dt, is_dst=False): 245 | '''Convert naive time to local time. 246 | 247 | This method should be used to construct localtimes, rather 248 | than passing a tzinfo argument to a datetime constructor. 249 | 250 | is_dst is used to determine the correct timezone in the ambigous 251 | period at the end of daylight savings time. 252 | 253 | >>> from pytz import timezone 254 | >>> fmt = '%Y-%m-%d %H:%M:%S %Z (%z)' 255 | >>> amdam = timezone('Europe/Amsterdam') 256 | >>> dt = datetime(2004, 10, 31, 2, 0, 0) 257 | >>> loc_dt1 = amdam.localize(dt, is_dst=True) 258 | >>> loc_dt2 = amdam.localize(dt, is_dst=False) 259 | >>> loc_dt1.strftime(fmt) 260 | '2004-10-31 02:00:00 CEST (+0200)' 261 | >>> loc_dt2.strftime(fmt) 262 | '2004-10-31 02:00:00 CET (+0100)' 263 | >>> str(loc_dt2 - loc_dt1) 264 | '1:00:00' 265 | 266 | Use is_dst=None to raise an AmbiguousTimeError for ambiguous 267 | times at the end of daylight savings 268 | 269 | >>> try: 270 | ... loc_dt1 = amdam.localize(dt, is_dst=None) 271 | ... except AmbiguousTimeError: 272 | ... print('Ambiguous') 273 | Ambiguous 274 | 275 | is_dst defaults to False 276 | 277 | >>> amdam.localize(dt) == amdam.localize(dt, False) 278 | True 279 | 280 | is_dst is also used to determine the correct timezone in the 281 | wallclock times jumped over at the start of daylight savings time. 282 | 283 | >>> pacific = timezone('US/Pacific') 284 | >>> dt = datetime(2008, 3, 9, 2, 0, 0) 285 | >>> ploc_dt1 = pacific.localize(dt, is_dst=True) 286 | >>> ploc_dt2 = pacific.localize(dt, is_dst=False) 287 | >>> ploc_dt1.strftime(fmt) 288 | '2008-03-09 02:00:00 PDT (-0700)' 289 | >>> ploc_dt2.strftime(fmt) 290 | '2008-03-09 02:00:00 PST (-0800)' 291 | >>> str(ploc_dt2 - ploc_dt1) 292 | '1:00:00' 293 | 294 | Use is_dst=None to raise a NonExistentTimeError for these skipped 295 | times. 296 | 297 | >>> try: 298 | ... loc_dt1 = pacific.localize(dt, is_dst=None) 299 | ... except NonExistentTimeError: 300 | ... print('Non-existent') 301 | Non-existent 302 | ''' 303 | if dt.tzinfo is not None: 304 | raise ValueError('Not naive datetime (tzinfo is already set)') 305 | 306 | # Find the two best possibilities. 307 | possible_loc_dt = set() 308 | for delta in [timedelta(days=-1), timedelta(days=1)]: 309 | loc_dt = dt + delta 310 | idx = max(0, bisect_right( 311 | self._utc_transition_times, loc_dt) - 1) 312 | inf = self._transition_info[idx] 313 | tzinfo = self._tzinfos[inf] 314 | loc_dt = tzinfo.normalize(dt.replace(tzinfo=tzinfo)) 315 | if loc_dt.replace(tzinfo=None) == dt: 316 | possible_loc_dt.add(loc_dt) 317 | 318 | if len(possible_loc_dt) == 1: 319 | return possible_loc_dt.pop() 320 | 321 | # If there are no possibly correct timezones, we are attempting 322 | # to convert a time that never happened - the time period jumped 323 | # during the start-of-DST transition period. 324 | if len(possible_loc_dt) == 0: 325 | # If we refuse to guess, raise an exception. 326 | if is_dst is None: 327 | raise NonExistentTimeError(dt) 328 | 329 | # If we are forcing the pre-DST side of the DST transition, we 330 | # obtain the correct timezone by winding the clock forward a few 331 | # hours. 332 | elif is_dst: 333 | return self.localize( 334 | dt + timedelta(hours=6), is_dst=True) - timedelta(hours=6) 335 | 336 | # If we are forcing the post-DST side of the DST transition, we 337 | # obtain the correct timezone by winding the clock back. 338 | else: 339 | return self.localize( 340 | dt - timedelta(hours=6), is_dst=False) + timedelta(hours=6) 341 | 342 | 343 | # If we get this far, we have multiple possible timezones - this 344 | # is an ambiguous case occuring during the end-of-DST transition. 345 | 346 | # If told to be strict, raise an exception since we have an 347 | # ambiguous case 348 | if is_dst is None: 349 | raise AmbiguousTimeError(dt) 350 | 351 | # Filter out the possiblilities that don't match the requested 352 | # is_dst 353 | filtered_possible_loc_dt = [ 354 | p for p in possible_loc_dt 355 | if bool(p.tzinfo._dst) == is_dst 356 | ] 357 | 358 | # Hopefully we only have one possibility left. Return it. 359 | if len(filtered_possible_loc_dt) == 1: 360 | return filtered_possible_loc_dt[0] 361 | 362 | if len(filtered_possible_loc_dt) == 0: 363 | filtered_possible_loc_dt = list(possible_loc_dt) 364 | 365 | # If we get this far, we have in a wierd timezone transition 366 | # where the clocks have been wound back but is_dst is the same 367 | # in both (eg. Europe/Warsaw 1915 when they switched to CET). 368 | # At this point, we just have to guess unless we allow more 369 | # hints to be passed in (such as the UTC offset or abbreviation), 370 | # but that is just getting silly. 371 | # 372 | # Choose the earliest (by UTC) applicable timezone. 373 | sorting_keys = {} 374 | for local_dt in filtered_possible_loc_dt: 375 | key = local_dt.replace(tzinfo=None) - local_dt.tzinfo._utcoffset 376 | sorting_keys[key] = local_dt 377 | first_key = sorted(sorting_keys)[0] 378 | return sorting_keys[first_key] 379 | 380 | def utcoffset(self, dt, is_dst=None): 381 | '''See datetime.tzinfo.utcoffset 382 | 383 | The is_dst parameter may be used to remove ambiguity during DST 384 | transitions. 385 | 386 | >>> from pytz import timezone 387 | >>> tz = timezone('America/St_Johns') 388 | >>> ambiguous = datetime(2009, 10, 31, 23, 30) 389 | 390 | >>> tz.utcoffset(ambiguous, is_dst=False) 391 | datetime.timedelta(-1, 73800) 392 | 393 | >>> tz.utcoffset(ambiguous, is_dst=True) 394 | datetime.timedelta(-1, 77400) 395 | 396 | >>> try: 397 | ... tz.utcoffset(ambiguous) 398 | ... except AmbiguousTimeError: 399 | ... print('Ambiguous') 400 | Ambiguous 401 | 402 | ''' 403 | if dt is None: 404 | return None 405 | elif dt.tzinfo is not self: 406 | dt = self.localize(dt, is_dst) 407 | return dt.tzinfo._utcoffset 408 | else: 409 | return self._utcoffset 410 | 411 | def dst(self, dt, is_dst=None): 412 | '''See datetime.tzinfo.dst 413 | 414 | The is_dst parameter may be used to remove ambiguity during DST 415 | transitions. 416 | 417 | >>> from pytz import timezone 418 | >>> tz = timezone('America/St_Johns') 419 | 420 | >>> normal = datetime(2009, 9, 1) 421 | 422 | >>> tz.dst(normal) 423 | datetime.timedelta(0, 3600) 424 | >>> tz.dst(normal, is_dst=False) 425 | datetime.timedelta(0, 3600) 426 | >>> tz.dst(normal, is_dst=True) 427 | datetime.timedelta(0, 3600) 428 | 429 | >>> ambiguous = datetime(2009, 10, 31, 23, 30) 430 | 431 | >>> tz.dst(ambiguous, is_dst=False) 432 | datetime.timedelta(0) 433 | >>> tz.dst(ambiguous, is_dst=True) 434 | datetime.timedelta(0, 3600) 435 | >>> try: 436 | ... tz.dst(ambiguous) 437 | ... except AmbiguousTimeError: 438 | ... print('Ambiguous') 439 | Ambiguous 440 | 441 | ''' 442 | if dt is None: 443 | return None 444 | elif dt.tzinfo is not self: 445 | dt = self.localize(dt, is_dst) 446 | return dt.tzinfo._dst 447 | else: 448 | return self._dst 449 | 450 | def tzname(self, dt, is_dst=None): 451 | '''See datetime.tzinfo.tzname 452 | 453 | The is_dst parameter may be used to remove ambiguity during DST 454 | transitions. 455 | 456 | >>> from pytz import timezone 457 | >>> tz = timezone('America/St_Johns') 458 | 459 | >>> normal = datetime(2009, 9, 1) 460 | 461 | >>> tz.tzname(normal) 462 | 'NDT' 463 | >>> tz.tzname(normal, is_dst=False) 464 | 'NDT' 465 | >>> tz.tzname(normal, is_dst=True) 466 | 'NDT' 467 | 468 | >>> ambiguous = datetime(2009, 10, 31, 23, 30) 469 | 470 | >>> tz.tzname(ambiguous, is_dst=False) 471 | 'NST' 472 | >>> tz.tzname(ambiguous, is_dst=True) 473 | 'NDT' 474 | >>> try: 475 | ... tz.tzname(ambiguous) 476 | ... except AmbiguousTimeError: 477 | ... print('Ambiguous') 478 | Ambiguous 479 | ''' 480 | if dt is None: 481 | return self.zone 482 | elif dt.tzinfo is not self: 483 | dt = self.localize(dt, is_dst) 484 | return dt.tzinfo._tzname 485 | else: 486 | return self._tzname 487 | 488 | def __repr__(self): 489 | if self._dst: 490 | dst = 'DST' 491 | else: 492 | dst = 'STD' 493 | if self._utcoffset > _notime: 494 | return '' % ( 495 | self.zone, self._tzname, self._utcoffset, dst 496 | ) 497 | else: 498 | return '' % ( 499 | self.zone, self._tzname, self._utcoffset, dst 500 | ) 501 | 502 | def __reduce__(self): 503 | # Special pickle to zone remains a singleton and to cope with 504 | # database changes. 505 | return pytz._p, ( 506 | self.zone, 507 | _to_seconds(self._utcoffset), 508 | _to_seconds(self._dst), 509 | self._tzname 510 | ) 511 | 512 | 513 | 514 | def unpickler(zone, utcoffset=None, dstoffset=None, tzname=None): 515 | """Factory function for unpickling pytz tzinfo instances. 516 | 517 | This is shared for both StaticTzInfo and DstTzInfo instances, because 518 | database changes could cause a zones implementation to switch between 519 | these two base classes and we can't break pickles on a pytz version 520 | upgrade. 521 | """ 522 | # Raises a KeyError if zone no longer exists, which should never happen 523 | # and would be a bug. 524 | tz = pytz.timezone(zone) 525 | 526 | # A StaticTzInfo - just return it 527 | if utcoffset is None: 528 | return tz 529 | 530 | # This pickle was created from a DstTzInfo. We need to 531 | # determine which of the list of tzinfo instances for this zone 532 | # to use in order to restore the state of any datetime instances using 533 | # it correctly. 534 | utcoffset = memorized_timedelta(utcoffset) 535 | dstoffset = memorized_timedelta(dstoffset) 536 | try: 537 | return tz._tzinfos[(utcoffset, dstoffset, tzname)] 538 | except KeyError: 539 | # The particular state requested in this timezone no longer exists. 540 | # This indicates a corrupt pickle, or the timezone database has been 541 | # corrected violently enough to make this particular 542 | # (utcoffset,dstoffset) no longer exist in the zone, or the 543 | # abbreviation has been changed. 544 | pass 545 | 546 | # See if we can find an entry differing only by tzname. Abbreviations 547 | # get changed from the initial guess by the database maintainers to 548 | # match reality when this information is discovered. 549 | for localized_tz in tz._tzinfos.values(): 550 | if (localized_tz._utcoffset == utcoffset 551 | and localized_tz._dst == dstoffset): 552 | return localized_tz 553 | 554 | # This (utcoffset, dstoffset) information has been removed from the 555 | # zone. Add it back. This might occur when the database maintainers have 556 | # corrected incorrect information. datetime instances using this 557 | # incorrect information will continue to do so, exactly as they were 558 | # before being pickled. This is purely an overly paranoid safety net - I 559 | # doubt this will ever been needed in real life. 560 | inf = (utcoffset, dstoffset, tzname) 561 | tz._tzinfos[inf] = tz.__class__(inf, tz._tzinfos) 562 | return tz._tzinfos[inf] 563 | 564 | -------------------------------------------------------------------------------- /template/zoneinfo.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brianmhunt/pytz-appengine/efea2138b6b6f5eddcfd142eeab018a411e46a71/template/zoneinfo.zip -------------------------------------------------------------------------------- /test_pytz_appengine.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test the pytz-appengine specific components 3 | """ 4 | 5 | import pytz 6 | import logging 7 | import unittest 8 | 9 | 10 | class pytzAppengineTest(unittest.TestCase): 11 | """ 12 | Check that loading works as expected 13 | and that we see the appropriate model instances 14 | """ 15 | def test_pytz_appengine(self): 16 | """Check that pytz-appengine is used""" 17 | self.assertTrue(pytz.APPENGINE_PYTZ) 18 | 19 | def test_zones(self): 20 | """Check that the models do what we expect""" 21 | from pytz import NDB_NAMESPACE, Zoneinfo 22 | from google.appengine.ext import ndb 23 | 24 | est = pytz.timezone('Canada/Eastern') 25 | 26 | logging.error(est) 27 | 28 | EXPECT_ZONES = 589 # this may change with each iteration 29 | 30 | zones = Zoneinfo.query(namespace=NDB_NAMESPACE).count() 31 | 32 | self.assertEqual(zones, EXPECT_ZONES) 33 | --------------------------------------------------------------------------------