diff -Nru astral-1.4/debian/changelog astral-1.6.1/debian/changelog --- astral-1.4/debian/changelog 2017-08-02 21:00:58.000000000 +0000 +++ astral-1.6.1/debian/changelog 2018-07-07 04:53:43.000000000 +0000 @@ -1,3 +1,19 @@ +astral (1.6.1-1) unstable; urgency=medium + + * Team upload. + [ Ondřej Nový ] + * d/control: Set Vcs-* to salsa.debian.org + * d/copyright: Use https protocol in Format field + + [ Diane Trout ] + * New upstream version 1.6.1 + * Add python{,3}-requests to Build-Depends + * README renamed to README.rst + * Clean up .pytest_cache directory in case of rebuild + * egg-info directory is now in src/ + + -- Diane Trout Fri, 06 Jul 2018 21:53:43 -0700 + astral (1.4-1) unstable; urgency=medium * Initial Debian release (closes: #869101) diff -Nru astral-1.4/debian/control astral-1.6.1/debian/control --- astral-1.4/debian/control 2017-08-02 21:00:58.000000000 +0000 +++ astral-1.6.1/debian/control 2018-07-07 04:48:13.000000000 +0000 @@ -7,16 +7,18 @@ dh-python, python-all, python-pytest, + python-requests, python-setuptools, python-tz, python3-all, python3-pytest, + python3-requests, python3-setuptools, python3-tz Standards-Version: 4.0.0 Homepage: https://pypi.python.org/pypi/astral -Vcs-Git: https://anonscm.debian.org/git/python-modules/packages/astral.git -Vcs-Browser: https://anonscm.debian.org/git/python-modules/packages/astral.git +Vcs-Git: https://salsa.debian.org/python-team/modules/astral.git +Vcs-Browser: https://salsa.debian.org/python-team/modules/astral Package: python-astral Architecture: all diff -Nru astral-1.4/debian/copyright astral-1.6.1/debian/copyright --- astral-1.4/debian/copyright 2017-08-02 21:00:58.000000000 +0000 +++ astral-1.6.1/debian/copyright 2018-07-07 04:35:36.000000000 +0000 @@ -1,4 +1,4 @@ -Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: astral Source: https://pypi.python.org/pypi/astral diff -Nru astral-1.4/debian/docs astral-1.6.1/debian/docs --- astral-1.4/debian/docs 2017-08-02 21:00:58.000000000 +0000 +++ astral-1.6.1/debian/docs 2018-07-07 04:44:54.000000000 +0000 @@ -1,2 +1,2 @@ -README +README.rst notes.txt diff -Nru astral-1.4/debian/rules astral-1.6.1/debian/rules --- astral-1.4/debian/rules 2017-08-02 21:00:58.000000000 +0000 +++ astral-1.6.1/debian/rules 2018-07-07 04:43:02.000000000 +0000 @@ -7,3 +7,7 @@ %: dh $@ --with python2,python3 --buildsystem=pybuild --test-pytest + +override_dh_auto_clean: + rm -rf .pytests_cache + dh_auto_clean diff -Nru astral-1.4/debian/source/options astral-1.6.1/debian/source/options --- astral-1.4/debian/source/options 2017-08-02 21:00:58.000000000 +0000 +++ astral-1.6.1/debian/source/options 2018-07-07 04:46:35.000000000 +0000 @@ -1 +1 @@ -extend-diff-ignore = "^[^/]*[.]egg-info/" +extend-diff-ignore = "^src/[^/]*[.]egg-info/" diff -Nru astral-1.4/MANIFEST.in astral-1.6.1/MANIFEST.in --- astral-1.4/MANIFEST.in 2017-01-13 09:21:56.000000000 +0000 +++ astral-1.6.1/MANIFEST.in 2018-03-15 12:15:13.000000000 +0000 @@ -1,5 +1,5 @@ -include LICENSE -include notes.txt -include src/test/*.py -global-exclude *.pyc -exclude setup.cfg +include notes.txt +include src/test/*.py +global-exclude *.pyc +include monkeypatch.py +include LICENSE diff -Nru astral-1.4/monkeypatch.py astral-1.6.1/monkeypatch.py --- astral-1.4/monkeypatch.py 1970-01-01 00:00:00.000000000 +0000 +++ astral-1.6.1/monkeypatch.py 2018-02-23 12:33:58.000000000 +0000 @@ -0,0 +1,77 @@ +"""Monkey patch distutils to also use setup-dev.cfg and setuptools to add README.md to the default files""" + +import os +import sys +import distutils.dist +from distutils.util import (check_environ, convert_path) +from distutils.debug import DEBUG +import setuptools.command.sdist + + +def find_config_files(self): + """Find as many configuration files as should be processed for this + platform, and return a list of filenames in the order in which they + should be parsed. The filenames returned are guaranteed to exist + (modulo nasty race conditions). + + There are three possible config files: distutils.cfg in the + Distutils installation directory (ie. where the top-level + Distutils __inst__.py file lives), a file in the user's home + directory named .pydistutils.cfg on Unix and pydistutils.cfg + on Windows/Mac; and setup.cfg in the current directory. + + The file in the user's home directory can be disabled with the + --no-user-cfg option. + """ + + files = [] + check_environ() + + # Where to look for the system-wide Distutils config file + sys_dir = os.path.dirname(sys.modules['distutils'].__file__) + + # Look for the system config file + sys_file = os.path.join(sys_dir, "distutils.cfg") + if os.path.isfile(sys_file): + files.append(sys_file) + + # What to call the per-user config file + if os.name == 'posix': + user_filename = ".pydistutils.cfg" + else: + user_filename = "pydistutils.cfg" + + # And look for the user config file + if self.want_user_cfg: + user_file = os.path.join(os.path.expanduser('~'), user_filename) + if os.path.isfile(user_file): + files.append(user_file) + + # All platforms support local setup.cfg + local_file = "setup.cfg" + if os.path.isfile(local_file): + files.append(local_file) + + local_dev_file = "setup-dev.cfg" + if os.path.isfile(local_dev_file): + files.append(local_dev_file) + + if DEBUG: + self.announce("using config files: %s" % ', '.join(files)) + + return files + + +def check_readme(self): + for f in self.READMES + ('README.md',): + if os.path.exists(f): + return + else: + self.warn( + "standard file not found: should have one of " + + ', '.join(self.READMES) + ) + + +distutils.dist.Distribution.find_config_files = find_config_files +setuptools.command.sdist.sdist.check_readme = check_readme diff -Nru astral-1.4/notes.txt astral-1.6.1/notes.txt --- astral-1.4/notes.txt 2017-01-13 09:21:56.000000000 +0000 +++ astral-1.6.1/notes.txt 2018-02-23 12:33:58.000000000 +0000 @@ -1,6 +1,6 @@ -Written Consent from Google has been obtained by following the steps outlined at -the following location - - http://support.google.com/maps/bin/static.py?hl=en&ts=1342531&page=ts.cs - -Refer to the README file for Attribution. +Written Consent from Google has been obtained by following the steps outlined at +the following location + + http://support.google.com/maps/bin/static.py?hl=en&ts=1342531&page=ts.cs + +Refer to the README.md file for Attribution. diff -Nru astral-1.4/PKG-INFO astral-1.6.1/PKG-INFO --- astral-1.4/PKG-INFO 2017-02-24 09:23:20.000000000 +0000 +++ astral-1.6.1/PKG-INFO 2018-05-02 12:53:34.000000000 +0000 @@ -1,12 +1,47 @@ -Metadata-Version: 1.1 +Metadata-Version: 1.2 Name: astral -Version: 1.4 +Version: 1.6.1 Summary: Calculations for the position of the sun and moon. Home-page: https://github.com/sffjunkie/astral Author: Simon Kennedy Author-email: sffjunkie+code@gmail.com License: Apache-2.0 -Description: Calculations for the position of the sun and moon. +Project-URL: Documentation, https://astral.readthedocs.io/en/stable/index.html +Project-URL: Source, https://github.com/sffjunkie/astral +Project-URL: Issues, https://github.com/sffjunkie/astral/issues +Description-Content-Type: UNKNOWN +Description: Astral + ====== + + |travis_status| |pypi_ver| + + .. |travis_status| image:: https://travis-ci.org/sffjunkie/astral.svg?branch=master + :target: https://travis-ci.org/sffjunkie/astral + + .. |pypi_ver| image:: https://img.shields.io/pypi/v/astral.svg + :target: https://pypi.org/project/astral/ + + This is 'astral' a Python module which calculates + + * Times for various positions of the sun: dawn, sunrise, solar noon, + sunset, dusk, solar elevation, solar azimuth and rahukaalam. + * The phase of the moon. + + For documentation see the https://astral.readthedocs.io/en/stable/index.html + + GoogleGeocoder + ~~~~~~~~~~~~~~ + + `GoogleGeocoder` uses the mapping services provided by Google + + Access to the `GoogleGeocoder` requires you to agree to be bound by + Google Maps/Google Earth APIs Terms of Service found at + https://developers.google.com/maps/terms which includes but is not limited to + having a Google Account. + + More information on Google's maps service can be found at + https://developers.google.com/maps/documentation/ + Platform: UNKNOWN Classifier: Intended Audience :: Developers Classifier: Programming Language :: Python diff -Nru astral-1.4/README astral-1.6.1/README --- astral-1.4/README 2017-01-13 09:21:56.000000000 +0000 +++ astral-1.6.1/README 1970-01-01 00:00:00.000000000 +0000 @@ -1,20 +0,0 @@ -This is 'astral' a Python module which calculates - - * Times for various positions of the sun: dawn, sunrise, solar noon, - sunset, dusk, solar elevation, solar azimuth and rahukaalam. - * The phase of the moon. - -For documentation see the http://astral.readthedocs.io/en/latest/index.html - -GoogleGeocoder --------------- - -`GoogleGeocoder` uses the mapping services provided by Google - -Access to the `GoogleGeocoder` requires you to agree to be bound by -Google Maps/Google Earth APIs Terms of Service found at -https://developers.google.com/maps/terms which includes but is not limited to -having a Google Account. - -More information on Google's maps service can be found at -https://developers.google.com/maps/documentation/ diff -Nru astral-1.4/README.rst astral-1.6.1/README.rst --- astral-1.4/README.rst 1970-01-01 00:00:00.000000000 +0000 +++ astral-1.6.1/README.rst 2018-03-20 13:22:25.000000000 +0000 @@ -0,0 +1,31 @@ +Astral +====== + +|travis_status| |pypi_ver| + +.. |travis_status| image:: https://travis-ci.org/sffjunkie/astral.svg?branch=master + :target: https://travis-ci.org/sffjunkie/astral + +.. |pypi_ver| image:: https://img.shields.io/pypi/v/astral.svg + :target: https://pypi.org/project/astral/ + +This is 'astral' a Python module which calculates + + * Times for various positions of the sun: dawn, sunrise, solar noon, + sunset, dusk, solar elevation, solar azimuth and rahukaalam. + * The phase of the moon. + +For documentation see the https://astral.readthedocs.io/en/stable/index.html + +GoogleGeocoder +~~~~~~~~~~~~~~ + +`GoogleGeocoder` uses the mapping services provided by Google + +Access to the `GoogleGeocoder` requires you to agree to be bound by +Google Maps/Google Earth APIs Terms of Service found at +https://developers.google.com/maps/terms which includes but is not limited to +having a Google Account. + +More information on Google's maps service can be found at +https://developers.google.com/maps/documentation/ diff -Nru astral-1.4/setup.cfg astral-1.6.1/setup.cfg --- astral-1.4/setup.cfg 2017-02-24 09:23:20.000000000 +0000 +++ astral-1.6.1/setup.cfg 2018-05-02 12:53:34.000000000 +0000 @@ -1,5 +1,46 @@ +[metadata] +name = astral +version = 1.5 +description = Calculations for the position of the sun and moon. +description-file = README.rst +author = Simon Kennedy +author_email = sffjunkie+code@gmail.com +home-page = https://github.com/sffjunkie/astral +license = Apache-2.0 +classifiers = + Intended Audience :: Developers + Programming Language :: Python + Programming Language :: Python :: 2.7 + Programming Language :: Python :: 3 +requires_dist = pytz + +[files] +packages_root = src +modules = astral + +[aliases] +test = pytest +build_docs = build_sphinx + +[bdist_wheel] +universal = 1 + +[tool:pytest] +norecursedirs = + .eggs + .git + .bzr + .hg + .tox + .cache + .settings + dist + build + +[pycodestyle] +max_line_length = 100 + [egg_info] tag_build = tag_date = 0 -tag_svn_revision = 0 diff -Nru astral-1.4/setup.py astral-1.6.1/setup.py --- astral-1.4/setup.py 2017-01-13 09:21:56.000000000 +0000 +++ astral-1.6.1/setup.py 2018-05-02 12:00:57.000000000 +0000 @@ -1,68 +1,44 @@ -# Copyright 2009-2017, Simon Kennedy, sffjunkie+code@gmail.com - -import os -import sys -from setuptools import setup - -PACKAGE_DIR = 'src' - -sys.path.insert(0, PACKAGE_DIR) - -from astral import __version__ as astral_version - -def dev_dir(): - dev_home = os.environ.get('DEV_HOME', None) - if not dev_home: - return None - - return os.path.join(dev_home, 'projects') - -cmd_class = {} -tests_require = [] - -dd = dev_dir() -if dd: - sys.path.insert(0, dd) - -try: - import common.test - cmd_class['test'] = common.test.TestCommand - tests_require.extend(common.test.requires) -except ImportError: - print('Unable to import test command. Skipping. Try running tool from command line e.g. tox') - pass - -#try: -# import common.clean -# cmd_class['clean'] = common.clean.CleanCommand -#except ImportError: -# pass - -description = 'Calculations for the position of the sun and moon.' -try: - from common.setup_funcs import read_contents - long_description = read_contents(os.path.dirname(__file__), 'README') -except ImportError: - long_description = description - -setup(name='astral', - version=astral_version, - description=description, - long_description=long_description, - author='Simon Kennedy', - author_email='sffjunkie+code@gmail.com', - url="https://github.com/sffjunkie/astral", - license='Apache-2.0', - classifiers=[ - "Intended Audience :: Developers", - "Programming Language :: Python", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3", - ], - package_dir={'': PACKAGE_DIR}, - py_modules=['astral'], - install_requires=['pytz'], - - cmdclass = cmd_class, - tests_require=tests_require, -) +# Copyright 2009-2018, Simon Kennedy, sffjunkie+code@gmail.com + +import io +import os +from setuptools import setup + +import monkeypatch # pylint: disable=W0611 + +def read_contents(*names, **kwargs): + return io.open( + os.path.join(*names), + encoding=kwargs.get("encoding", "utf8") + ).read() + +description = 'Calculations for the position of the sun and moon.' +try: + long_description = read_contents(os.path.dirname(__file__), 'README.rst') +except: + long_description = description + +setup(name='astral', + version='1.6.1', + description=description, + long_description=long_description, + author='Simon Kennedy', + author_email='sffjunkie+code@gmail.com', + url="https://github.com/sffjunkie/astral", + project_urls={ + 'Documentation': 'https://astral.readthedocs.io/en/stable/index.html', + 'Source': 'https://github.com/sffjunkie/astral', + 'Issues': 'https://github.com/sffjunkie/astral/issues', + }, + license='Apache-2.0', + classifiers=[ + "Intended Audience :: Developers", + "Programming Language :: Python", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + ], + package_dir={'': 'src'}, + py_modules=['astral'], + install_requires=['pytz', 'requests'], + tests_require=['pytest-runner'], +) diff -Nru astral-1.4/src/astral.py astral-1.6.1/src/astral.py --- astral-1.4/src/astral.py 2017-02-24 09:22:24.000000000 +0000 +++ astral-1.6.1/src/astral.py 2018-05-02 12:00:57.000000000 +0000 @@ -1,2461 +1,2501 @@ -# -*- coding: utf-8 -*- - -# Copyright 2009-2017, Simon Kennedy, sffjunkie+code@gmail.com - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""The :mod:`astral` module provides the means to calculate dawn, sunrise, -solar noon, sunset, dusk and rahukaalam times, plus solar azimuth and -elevation, for specific locations or at a specific latitude/longitude. It can -also calculate the moon phase for a specific date. - -The module provides 2 main classes :class:`Astral` and :class:`Location`. - -:class:`Astral` - Has 2 main responsibilities - - * Calculates the events in the UTC timezone. - * Provides access to location data - -:class:`Location` - Holds information about a location and provides functions to calculate - the event times for the location in the correct time zone. - -For example :: - - >>> from astral import * - >>> a = Astral() - >>> location = a['London'] - >>> print('Information for %s' % location.name) - Information for London - >>> timezone = location.timezone - >>> print('Timezone: %s' % timezone) - Timezone: Europe/London - >>> print('Latitude: %.02f; Longitude: %.02f' % (location.latitude, - ... location.longitude)) - Latitude: 51.60; Longitude: 0.05 - >>> from datetime import date - >>> d = date(2009,4,22) - >>> sun = location.sun(local=True, date=d) - >>> print('Dawn: %s' % str(sun['dawn'])) - Dawn: 2009-04-22 05:12:56+01:00 - -The module currently provides 2 methods of obtaining location information; -:class:`AstralGeocoder` (the default, which uses information from within the -module) and :class:`GoogleGeocoder` (which obtains information from Google's -Map Service.) - -To use the :class:`GoogleGeocoder` pass the class as the `geocoder` parameter -to :meth:`Astral.__init__` or by setting the `geocoder` property to an -instance of :class:`GoogleGeocoder`:: - - >>> from astral import GoogleGeocoder - >>> a = Astral(GoogleGeocoder) - -or :: - - >>> from astral import GoogleGeocoder - >>> a = Astral() - >>> a.geocoder = GoogleGeocoder() -""" - -from __future__ import unicode_literals, division - -try: - import pytz -except ImportError: - raise ImportError(('The astral module requires the ' - 'pytz module to be available.')) - -import datetime -from time import time -from math import cos, sin, tan, acos, asin, atan2, floor, ceil -from math import radians, degrees, pow -import sys - -try: - from urllib import quote_plus -except ImportError: - from urllib.parse import quote_plus - -try: - from urllib2 import urlopen, URLError -except ImportError: - from urllib.request import urlopen, URLError - -try: - import simplejson as json -except ImportError: - import json - -if sys.version_info[0] >= 3: - ustr = str -else: - ustr = unicode - -__all__ = ['Astral', 'Location', - 'AstralGeocoder', 'GoogleGeocoder', - 'AstralError'] - -__version__ = "1.4" -__author__ = "Simon Kennedy " - -SUN_RISING = 1 -SUN_SETTING = -1 - -# name,region,latitude,longitude,timezone,elevation -_LOCATION_INFO = """Abu Dhabi,UAE,24°28'N,54°22'E,Asia/Dubai,5 -Abu Dhabi,United Arab Emirates,24°28'N,54°22'E,Asia/Dubai,5 -Abuja,Nigeria,09°05'N,07°32'E,Africa/Lagos,342 -Accra,Ghana,05°35'N,00°06'W,Africa/Accra,61 -Addis Ababa,Ethiopia,09°02'N,38°42'E,Africa/Addis_Ababa,2355 -Adelaide,Australia,34°56'S,138°36'E,Australia/Adelaide,50 -Al Jubail,Saudi Arabia,25°24'N,49°39'W,Asia/Riyadh,8 -Algiers,Algeria,36°42'N,03°08'E,Africa/Algiers,224 -Amman,Jordan,31°57'N,35°52'E,Asia/Amman,1100 -Amsterdam,Netherlands,52°23'N,04°54'E,Europe/Amsterdam,2 -Andorra la Vella,Andorra,42°31'N,01°32'E,Europe/Andorra,1023 -Ankara,Turkey,39°57'N,32°54'E,Europe/Istanbul,938 -Antananarivo,Madagascar,18°55'S,47°31'E,Indian/Antananarivo,1276 -Apia,Samoa,13°50'S,171°50'W,Pacific/Apia,2 -Ashgabat,Turkmenistan,38°00'N,57°50'E,Asia/Ashgabat,219 -Asmara,Eritrea,15°19'N,38°55'E,Africa/Asmara,2325 -Astana,Kazakhstan,51°10'N,71°30'E,Asia/Qyzylorda,347 -Asuncion,Paraguay,25°10'S,57°30'W,America/Asuncion,124 -Athens,Greece,37°58'N,23°46'E,Europe/Athens,338 -Avarua,Cook Islands,21°12'N,159°46'W,Etc/GMT-10,208 -Baghdad,Iraq,33°20'N,44°30'E,Asia/Baghdad,41 -Baku,Azerbaijan,40°29'N,49°56'E,Asia/Baku,30 -Bamako,Mali,12°34'N,07°55'W,Africa/Bamako,350 -Bandar Seri Begawan,Brunei Darussalam,04°52'N,115°00'E,Asia/Brunei,1 -Bangkok,Thailand,13°45'N,100°35'E,Asia/Bangkok,2 -Bangui,Central African Republic,04°23'N,18°35'E,Africa/Bangui,373 -Banjul,Gambia,13°28'N,16°40'W,Africa/Banjul,5 -Basse-Terre,Guadeloupe,16°00'N,61°44'W,America/Guadeloupe,1 -Basseterre,Saint Kitts and Nevis,17°17'N,62°43'W,America/St_Kitts,50 -Beijing,China,39°55'N,116°20'E,Asia/Harbin,59 -Beirut,Lebanon,33°53'N,35°31'E,Asia/Beirut,56 -Belfast,Northern Ireland,54°36'N,5°56'W,Europe/Belfast,9 -Belgrade,Yugoslavia,44°50'N,20°37'E,Europe/Belgrade,90 -Belmopan,Belize,17°18'N,88°30'W,America/Belize,63 -Berlin,Germany,52°30'N,13°25'E,Europe/Berlin,35 -Bern,Switzerland,46°57'N,07°28'E,Europe/Zurich,510 -Bishkek,Kyrgyzstan,42°54'N,74°46'E,Asia/Bishkek,772 -Bissau,Guinea-Bissau,11°45'N,15°45'W,Africa/Bissau,0 -Bloemfontein,South Africa,29°12'S,26°07'E,Africa/Johannesburg,1398 -Bogota,Colombia,04°34'N,74°00'W,America/Bogota,2620 -Brasilia,Brazil,15°47'S,47°55'W,Brazil/East,1087 -Bratislava,Slovakia,48°10'N,17°07'E,Europe/Bratislava,132 -Brazzaville,Congo,04°09'S,15°12'E,Africa/Brazzaville,156 -Bridgetown,Barbados,13°05'N,59°30'W,America/Barbados,1 -Brisbane,Australia,27°30'S,153°01'E,Australia/Brisbane,25 -Brussels,Belgium,50°51'N,04°21'E,Europe/Brussels,62 -Bucharest,Romania,44°27'N,26°10'E,Europe/Bucharest,71 -Bucuresti,Romania,44°27'N,26°10'E,Europe/Bucharest,71 -Budapest,Hungary,47°29'N,19°05'E,Europe/Budapest,120 -Buenos Aires,Argentina,34°62'S,58°44'W,America/Buenos_Aires,6 -Bujumbura,Burundi,03°16'S,29°18'E,Africa/Bujumbura,782 -Cairo,Egypt,30°01'N,31°14'E,Africa/Cairo,74 -Canberra,Australia,35°15'S,149°08'E,Australia/Canberra,575 -Cape Town,South Africa,33°55'S,18°22'E,Africa/Johannesburg,1700 -Caracas,Venezuela,10°30'N,66°55'W,America/Caracas,885 -Castries,Saint Lucia,14°02'N,60°58'W,America/St_Lucia,125 -Cayenne,French Guiana,05°05'N,52°18'W,America/Cayenne,9 -Charlotte Amalie,United States of Virgin Islands,18°21'N,64°56'W,America/Virgin,0 -Chisinau,Moldova,47°02'N,28°50'E,Europe/Chisinau,122 -Conakry,Guinea,09°29'N,13°49'W,Africa/Conakry,26 -Copenhagen,Denmark,55°41'N,12°34'E,Europe/Copenhagen,5 -Cotonou,Benin,06°23'N,02°42'E,Africa/Porto-Novo,5 -Dakar,Senegal,14°34'N,17°29'W,Africa/Dakar,24 -Damascus,Syrian Arab Republic,33°30'N,36°18'E,Asia/Damascus,609 -Dammam,Saudi Arabia,26°30'N,50°12'E,Asia/Riyadh,1 -Dhaka,Bangladesh,23°43'N,90°26'E,Asia/Dhaka,8 -Dili,East Timor,08°29'S,125°34'E,Asia/Dili,11 -Djibouti,Djibouti,11°08'N,42°20'E,Africa/Djibouti,19 -Dodoma,United Republic of Tanzania,06°08'S,35°45'E,Africa/Dar_es_Salaam,1119 -Doha,Qatar,25°15'N,51°35'E,Asia/Qatar,10 -Douglas,Isle Of Man,54°9'N,4°29'W,Europe/London,35 -Dublin,Ireland,53°21'N,06°15'W,Europe/Dublin,85 -Dushanbe,Tajikistan,38°33'N,68°48'E,Asia/Dushanbe,803 -El Aaiun,Morocco,27°9'N,13°12'W,UTC,64 -Fort-de-France,Martinique,14°36'N,61°02'W,America/Martinique,9 -Freetown,Sierra Leone,08°30'N,13°17'W,Africa/Freetown,26 -Funafuti,Tuvalu,08°31'S,179°13'E,Pacific/Funafuti,2 -Gaborone,Botswana,24°45'S,25°57'E,Africa/Gaborone,1005 -George Town,Cayman Islands,19°20'N,81°24'W,America/Cayman,3 -Georgetown,Guyana,06°50'N,58°12'W,America/Guyana,30 -Gibraltar,Gibraltar,36°9'N,5°21'W,Europe/Gibraltar,3 -Guatemala,Guatemala,14°40'N,90°22'W,America/Guatemala,1500 -Hanoi,Viet Nam,21°05'N,105°55'E,Asia/Saigon,6 -Harare,Zimbabwe,17°43'S,31°02'E,Africa/Harare,1503 -Havana,Cuba,23°08'N,82°22'W,America/Havana,59 -Helsinki,Finland,60°15'N,25°03'E,Europe/Helsinki,56 -Hobart,Tasmania,42°53'S,147°19'E,Australia/Hobart,4 -Hong Kong,China,22°16'N,114°09'E,Asia/Hong_Kong,8 -Honiara,Solomon Islands,09°27'S,159°57'E,Pacific/Guadalcanal,8 -Islamabad,Pakistan,33°40'N,73°10'E,Asia/Karachi,508 -Jakarta,Indonesia,06°09'S,106°49'E,Asia/Jakarta,6 -Jerusalem,Israel,31°47'N,35°12'E,Asia/Jerusalem,775 -Juba,South Sudan,4°51'N,31°36'E,Africa/Juba,550 -Jubail,Saudi Arabia,27°02'N,49°39'E,Asia/Riyadh,2 -Kabul,Afghanistan,34°28'N,69°11'E,Asia/Kabul,1791 -Kampala,Uganda,00°20'N,32°30'E,Africa/Kampala,1155 -Kathmandu,Nepal,27°45'N,85°20'E,Asia/Kathmandu,1337 -Khartoum,Sudan,15°31'N,32°35'E,Africa/Khartoum,380 -Kiev,Ukraine,50°30'N,30°28'E,Europe/Kiev,153 -Kigali,Rwanda,01°59'S,30°04'E,Africa/Kigali,1497 -Kingston,Jamaica,18°00'N,76°50'W,America/Jamaica,9 -Kingston,Norfolk Island,45°20'S,168°43'E,Pacific/Norfolk,113 -Kingstown,Saint Vincent and the Grenadines,13°10'N,61°10'W,America/St_Vincent,1 -Kinshasa,Democratic Republic of the Congo,04°20'S,15°15'E,Africa/Kinshasa,312 -Koror,Palau,07°20'N,134°28'E,Pacific/Palau,33 -Kuala Lumpur,Malaysia,03°09'N,101°41'E,Asia/Kuala_Lumpur,22 -Kuwait,Kuwait,29°30'N,48°00'E,Asia/Kuwait,55 -La Paz,Bolivia,16°20'S,68°10'W,America/La_Paz,4014 -Libreville,Gabon,00°25'N,09°26'E,Africa/Libreville,15 -Lilongwe,Malawi,14°00'S,33°48'E,Africa/Blantyre,1229 -Lima,Peru,12°00'S,77°00'W,America/Lima,13 -Lisbon,Portugal,38°42'N,09°10'W,Europe/Lisbon,123 -Ljubljana,Slovenia,46°04'N,14°33'E,Europe/Ljubljana,385 -Lome,Togo,06°09'N,01°20'E,Africa/Lome,25 -London,England,51°30'N,00°07'W,Europe/London,24 -Luanda,Angola,08°50'S,13°15'E,Africa/Luanda,6 -Lusaka,Zambia,15°28'S,28°16'E,Africa/Lusaka,1154 -Luxembourg,Luxembourg,49°37'N,06°09'E,Europe/Luxembourg,232 -Macau,Macao,22°12'N,113°33'E,Asia/Macau,6 -Madinah,Saudi Arabia,24°28'N,39°36'E,Asia/Riyadh,631 -Madrid,Spain,40°25'N,03°45'W,Europe/Madrid,582 -Majuro,Marshall Islands,7°4'N,171°16'E,Pacific/Majuro,65 -Makkah,Saudi Arabia,21°26'N,39°49'E,Asia/Riyadh,240 -Malabo,Equatorial Guinea,03°45'N,08°50'E,Africa/Malabo,56 -Male,Maldives,04°00'N,73°28'E,Indian/Maldives,2 -Mamoudzou,Mayotte,12°48'S,45°14'E,Indian/Mayotte,420 -Managua,Nicaragua,12°06'N,86°20'W,America/Managua,50 -Manama,Bahrain,26°10'N,50°30'E,Asia/Bahrain,2 -Manila,Philippines,14°40'N,121°03'E,Asia/Manila,21 -Maputo,Mozambique,25°58'S,32°32'E,Africa/Maputo,44 -Maseru,Lesotho,29°18'S,27°30'E,Africa/Maseru,1628 -Masqat,Oman,23°37'N,58°36'E,Asia/Muscat,8 -Mbabane,Swaziland,26°18'S,31°06'E,Africa/Mbabane,1243 -Mecca,Saudi Arabia,21°26'N,39°49'E,Asia/Riyadh,240 -Medina,Saudi Arabia,24°28'N,39°36'E,Asia/Riyadh,631 -Mexico,Mexico,19°20'N,99°10'W,America/Mexico_City,2254 -Minsk,Belarus,53°52'N,27°30'E,Europe/Minsk,231 -Mogadishu,Somalia,02°02'N,45°25'E,Africa/Mogadishu,9 -Monaco,Priciplality Of Monaco,43°43'N,7°25'E,Europe/Monaco,206 -Monrovia,Liberia,06°18'N,10°47'W,Africa/Monrovia,9 -Montevideo,Uruguay,34°50'S,56°11'W,America/Montevideo,32 -Moroni,Comoros,11°40'S,43°16'E,Indian/Comoro,29 -Moscow,Russian Federation,55°45'N,37°35'E,Europe/Moscow,247 -Moskva,Russian Federation,55°45'N,37°35'E,Europe/Moscow,247 -Mumbai,India,18°58'N,72°49'E,Asia/Kolkata,14 -Muscat,Oman,23°37'N,58°32'E,Asia/Muscat,8 -N'Djamena,Chad,12°10'N,14°59'E,Africa/Ndjamena,295 -Nairobi,Kenya,01°17'S,36°48'E,Africa/Nairobi,1624 -Nassau,Bahamas,25°05'N,77°20'W,America/Nassau,7 -Naypyidaw,Myanmar,19°45'N,96°6'E,Asia/Rangoon,104 -New Delhi,India,28°37'N,77°13'E,Asia/Kolkata,233 -Ngerulmud,Palau,7°30'N,134°37'E,Pacific/Palau,3 -Niamey,Niger,13°27'N,02°06'E,Africa/Niamey,223 -Nicosia,Cyprus,35°10'N,33°25'E,Asia/Nicosia,162 -Nouakchott,Mauritania,20°10'S,57°30'E,Africa/Nouakchott,3 -Noumea,New Caledonia,22°17'S,166°30'E,Pacific/Noumea,69 -Nuku'alofa,Tonga,21°10'S,174°00'W,Pacific/Tongatapu,6 -Nuuk,Greenland,64°10'N,51°35'W,America/Godthab,70 -Oranjestad,Aruba,12°32'N,70°02'W,America/Aruba,33 -Oslo,Norway,59°55'N,10°45'E,Europe/Oslo,170 -Ottawa,Canada,45°27'N,75°42'W,US/Eastern,79 -Ouagadougou,Burkina Faso,12°15'N,01°30'W,Africa/Ouagadougou,316 -P'yongyang,Democratic People's Republic of Korea,39°09'N,125°30'E,Asia/Pyongyang,21 -Pago Pago,American Samoa,14°16'S,170°43'W,Pacific/Pago_Pago,0 -Palikir,Micronesia,06°55'N,158°09'E,Pacific/Ponape,71 -Panama,Panama,09°00'N,79°25'W,America/Panama,2 -Papeete,French Polynesia,17°32'S,149°34'W,Pacific/Tahiti,7 -Paramaribo,Suriname,05°50'N,55°10'W,America/Paramaribo,7 -Paris,France,48°50'N,02°20'E,Europe/Paris,109 -Perth,Australia,31°56'S,115°50'E,Australia/Perth,20 -Phnom Penh,Cambodia,11°33'N,104°55'E,Asia/Phnom_Penh,10 -Podgorica,Montenegro,42°28'N,19°16'E,Europe/Podgorica,53 -Port Louis,Mauritius,20°9'S,57°30'E,Indian/Mauritius,5 -Port Moresby,Papua New Guinea,09°24'S,147°08'E,Pacific/Port_Moresby,44 -Port-Vila,Vanuatu,17°45'S,168°18'E,Pacific/Efate,1 -Port-au-Prince,Haiti,18°40'N,72°20'W,America/Port-au-Prince,34 -Port of Spain,Trinidad and Tobago,10°40'N,61°31'W,America/Port_of_Spain,66 -Porto-Novo,Benin,06°23'N,02°42'E,Africa/Porto-Novo,38 -Prague,Czech Republic,50°05'N,14°22'E,Europe/Prague,365 -Praia,Cape Verde,15°02'N,23°34'W,Atlantic/Cape_Verde,35 -Pretoria,South Africa,25°44'S,28°12'E,Africa/Johannesburg,1322 -Pristina,Albania,42°40'N,21°10'E,Europe/Tirane,576 -Quito,Ecuador,00°15'S,78°35'W,America/Guayaquil,2812 -Rabat,Morocco,34°1'N,6°50'W,Africa/Casablanca,75 -Reykjavik,Iceland,64°10'N,21°57'W,Atlantic/Reykjavik,61 -Riga,Latvia,56°53'N,24°08'E,Europe/Riga,7 -Riyadh,Saudi Arabia,24°41'N,46°42'E,Asia/Riyadh,612 -Road Town,British Virgin Islands,18°27'N,64°37'W,America/Virgin,1 -Rome,Italy,41°54'N,12°29'E,Europe/Rome,95 -Roseau,Dominica,15°20'N,61°24'W,America/Dominica,72 -Saint Helier,Jersey,49°11'N,2°6'W,Etc/GMT,54 -Saint Pierre,Saint Pierre and Miquelon,46°46'N,56°12'W,America/Miquelon,5 -Saipan,Northern Mariana Islands,15°12'N,145°45'E,Pacific/Saipan,200 -Sana,Yemen,15°20'N,44°12'W,Asia/Aden,2199 -Sana'a,Yemen,15°20'N,44°12'W,Asia/Aden,2199 -San Jose,Costa Rica,09°55'N,84°02'W,America/Costa_Rica,931 -San Juan,Puerto Rico,18°28'N,66°07'W,America/Puerto_Rico,21 -San Marino,San Marino,43°55'N,12°30'E,Europe/San_Marino,749 -San Salvador,El Salvador,13°40'N,89°10'W,America/El_Salvador,621 -Santiago,Chile,33°24'S,70°40'W,America/Santiago,476 -Santo Domingo,Dominica Republic,18°30'N,69°59'W,America/Santo_Domingo,14 -Sao Tome,Sao Tome and Principe,00°10'N,06°39'E,Africa/Sao_Tome,13 -Sarajevo,Bosnia and Herzegovina,43°52'N,18°26'E,Europe/Sarajevo,511 -Seoul,Republic of Korea,37°31'N,126°58'E,Asia/Seoul,49 -Singapore,Republic of Singapore,1°18'N,103°48'E,Asia/Singapore,16 -Skopje,The Former Yugoslav Republic of Macedonia,42°01'N,21°26'E,Europe/Skopje,238 -Sofia,Bulgaria,42°45'N,23°20'E,Europe/Sofia,531 -Sri Jayawardenapura Kotte,Sri Lanka,6°54'N,79°53'E,Asia/Colombo,7 -St. George's,Grenada,32°22'N,64°40'W,America/Grenada,7 -St. John's,Antigua and Barbuda,17°7'N,61°51'W,America/Antigua,1 -St. Peter Port,Guernsey,49°26'N,02°33'W,Europe/Guernsey,1 -Stanley,Falkland Islands,51°40'S,59°51'W,Atlantic/Stanley,23 -Stockholm,Sweden,59°20'N,18°05'E,Europe/Stockholm,52 -Sucre,Bolivia,16°20'S,68°10'W,America/La_Paz,2903 -Suva,Fiji,18°06'S,178°30'E,Pacific/Fiji,0 -Sydney,Australia,33°53'S,151°13'E,Australia/Sydney,3 -Taipei,Republic of China (Taiwan),25°02'N,121°38'E,Asia/Taipei,9 -T'bilisi,Georgia,41°43'N,44°50'E,Asia/Tbilisi,467 -Tbilisi,Georgia,41°43'N,44°50'E,Asia/Tbilisi,467 -Tallinn,Estonia,59°22'N,24°48'E,Europe/Tallinn,39 -Tarawa,Kiribati,01°30'N,173°00'E,Pacific/Tarawa,2 -Tashkent,Uzbekistan,41°20'N,69°10'E,Asia/Tashkent,489 -Tegucigalpa,Honduras,14°05'N,87°14'W,America/Tegucigalpa,994 -Tehran,Iran,35°44'N,51°30'E,Asia/Tehran,1191 -Thimphu,Bhutan,27°31'N,89°45'E,Asia/Thimphu,2300 -Tirana,Albania,41°18'N,19°49'E,Europe/Tirane,90 -Tirane,Albania,41°18'N,19°49'E,Europe/Tirane,90 -Torshavn,Faroe Islands,62°05'N,06°56'W,Atlantic/Faroe,39 -Tokyo,Japan,35°41'N,139°41'E,Asia/Tokyo,8 -Tripoli,Libyan Arab Jamahiriya,32°49'N,13°07'E,Africa/Tripoli,81 -Tunis,Tunisia,36°50'N,10°11'E,Africa/Tunis,4 -Ulan Bator,Mongolia,47°55'N,106°55'E,Asia/Ulaanbaatar,1330 -Ulaanbaatar,Mongolia,47°55'N,106°55'E,Asia/Ulaanbaatar,1330 -Vaduz,Liechtenstein,47°08'N,09°31'E,Europe/Vaduz,463 -Valletta,Malta,35°54'N,14°31'E,Europe/Malta,48 -Vienna,Austria,48°12'N,16°22'E,Europe/Vienna,171 -Vientiane,Lao People's Democratic Republic,17°58'N,102°36'E,Asia/Vientiane,171 -Vilnius,Lithuania,54°38'N,25°19'E,Europe/Vilnius,156 -W. Indies,Antigua and Barbuda,17°20'N,61°48'W,America/Antigua,0 -Warsaw,Poland,52°13'N,21°00'E,Europe/Warsaw,107 -Washington DC,USA,39°91'N,77°02'W,US/Eastern,23 -Wellington,New Zealand,41°19'S,174°46'E,Pacific/Auckland,7 -Willemstad,Netherlands Antilles,12°05'N,69°00'W,America/Curacao,1 -Windhoek,Namibia,22°35'S,17°04'E,Africa/Windhoek,1725 -Yamoussoukro,Cote d'Ivoire,06°49'N,05°17'W,Africa/Abidjan,213 -Yangon,Myanmar,16°45'N,96°20'E,Asia/Rangoon,33 -Yaounde,Cameroon,03°50'N,11°35'E,Africa/Douala,760 -Yaren,Nauru,0°32'S,166°55'E,Pacific/Nauru,0 -Yerevan,Armenia,40°10'N,44°31'E,Asia/Yerevan,890 -Zagreb,Croatia,45°50'N,15°58'E,Europe/Zagreb,123 - -# UK Cities -Aberdeen,Scotland,57°08'N,02°06'W,Europe/London,65 -Birmingham,England,52°30'N,01°50'W,Europe/London,99 -Bolton,England,53°35'N,02°15'W,Europe/London,105 -Bradford,England,53°47'N,01°45'W,Europe/London,127 -Bristol,England,51°28'N,02°35'W,Europe/London,11 -Cardiff,Wales,51°29'N,03°13'W,Europe/London,9 -Crawley,England,51°8'N,00°10'W,Europe/London,77 -Edinburgh,Scotland,55°57'N,03°13'W,Europe/London,61 -Glasgow,Scotland,55°50'N,04°15'W,Europe/London,8 -Greenwich,England,51°28'N,00°00'W,Europe/London,24 -Leeds,England,53°48'N,01°35'W,Europe/London,47 -Leicester,England,52°38'N,01°08'W,Europe/London,138 -Liverpool,England,53°25'N,03°00'W,Europe/London,25 -Manchester,England,53°30'N,02°15'W,Europe/London,78 -Newcastle Upon Time,England,54°59'N,01°36'W,Europe/London,47 -Newcastle,England,54°59'N,01°36'W,Europe/London,47 -Norwich,England,52°38'N,01°18'E,Europe/London,18 -Oxford,England,51°45'N,01°15'W,Europe/London,72 -Plymouth,England,50°25'N,04°15'W,Europe/London,50 -Portsmouth,England,50°48'N,01°05'W,Europe/London,9 -Reading,England,51°27'N,0°58'W,Europe/London,84 -Sheffield,England,53°23'N,01°28'W,Europe/London,105 -Southampton,England,50°55'N,01°25'W,Europe/London,9 -Swansea,England,51°37'N,03°57'W,Europe/London,91 -Swindon,England,51°34'N,01°47'W,Europe/London,112 -Wolverhampton,England,52°35'N,2°08'W,Europe/London,89 -Barrow-In-Furness,England,54°06'N,3°13'W,Europe/London,20 - -# US State Capitals -Montgomery,USA,32°21'N,86°16'W,US/Central,42 -Juneau,USA,58°23'N,134°11'W,US/Alaska,29 -Phoenix,USA,33°26'N,112°04'W,America/Phoenix,331 -Little Rock,USA,34°44'N,92°19'W,US/Central,95 -Sacramento,USA,38°33'N,121°28'W,US/Pacific,15 -Denver,USA,39°44'N,104°59'W,US/Mountain,1600 -Hartford,USA,41°45'N,72°41'W,US/Eastern,9 -Dover,USA,39°09'N,75°31'W,US/Eastern,8 -Tallahassee,USA,30°27'N,84°16'W,US/Eastern,59 -Atlanta,USA,33°45'N,84°23'W,US/Eastern,267 -Honolulu,USA,21°18'N,157°49'W,US/Hawaii,229 -Boise,USA,43°36'N,116°12'W,US/Mountain,808 -Springfield,USA,39°47'N,89°39'W,US/Central,190 -Indianapolis,USA,39°46'N,86°9'W,US/Eastern,238 -Des Moines,USA,41°35'N,93°37'W,US/Central,276 -Topeka,USA,39°03'N,95°41'W,US/Central,289 -Frankfort,USA,38°11'N,84°51'W,US/Eastern,243 -Baton Rouge,USA,30°27'N,91°8'W,US/Central,15 -Augusta,USA,44°18'N,69°46'W,US/Eastern,41 -Annapolis,USA,38°58'N,76°30'W,US/Eastern,0 -Boston,USA,42°21'N,71°03'W,US/Eastern,6 -Lansing,USA,42°44'N,84°32'W,US/Eastern,271 -Saint Paul,USA,44°56'N,93°05'W,US/Central,256 -Jackson,USA,32°17'N,90°11'W,US/Central,90 -Jefferson City,USA,38°34'N,92°10'W,US/Central,167 -Helena,USA,46°35'N,112°1'W,US/Mountain,1150 -Lincoln,USA,40°48'N,96°40'W,US/Central,384 -Carson City,USA,39°9'N,119°45'W,US/Pacific,1432 -Concord,USA,43°12'N,71°32'W,US/Eastern,117 -Trenton,USA,40°13'N,74°45'W,US/Eastern,28 -Santa Fe,USA,35°40'N,105°57'W,US/Mountain,2151 -Albany,USA,42°39'N,73°46'W,US/Eastern,17 -Raleigh,USA,35°49'N,78°38'W,US/Eastern,90 -Bismarck,USA,46°48'N,100°46'W,US/Central,541 -Columbus,USA,39°59'N,82°59'W,US/Eastern,271 -Oklahoma City,USA,35°28'N,97°32'W,US/Central,384 -Salem,USA,44°55'N,123°1'W,US/Pacific,70 -Harrisburg,USA,40°16'N,76°52'W,US/Eastern,112 -Providence,USA,41°49'N,71°25'W,US/Eastern,2 -Columbia,USA,34°00'N,81°02'W,US/Eastern,96 -Pierre,USA,44°22'N,100°20'W,US/Central,543 -Nashville,USA,36°10'N,86°47'W,US/Central,149 -Austin,USA,30°16'N,97°45'W,US/Central,167 -Salt Lake City,USA,40°45'N,111°53'W,US/Mountain,1294 -Montpelier,USA,44°15'N,72°34'W,US/Eastern,325 -Richmond,USA,37°32'N,77°25'W,US/Eastern,68 -Olympia,USA,47°2'N,122°53'W,US/Pacific,35 -Charleston,USA,38°20'N,81°38'W,US/Eastern,11 -Madison,USA,43°4'N,89°24'W,US/Central,281 -Cheyenne,USA,41°8'N,104°48'W,US/Mountain,1860 - -# Major US Cities -Birmingham,USA,33°39'N,86°48'W,US/Central,197 -Anchorage,USA,61°13'N,149°53'W,US/Alaska,30 -Los Angeles,USA,34°03'N,118°15'W,US/Pacific,50 -San Francisco,USA,37°46'N,122°25'W,US/Pacific,47 -Bridgeport,USA,41°11'N,73°11'W,US/Eastern,13 -Wilmington,USA,39°44'N,75°32'W,US/Eastern,15 -Jacksonville,USA,30°19'N,81°39'W,US/Eastern,13 -Miami,USA,26°8'N,80°12'W,US/Eastern,10 -Chicago,USA,41°50'N,87°41'W,US/Central,189 -Wichita,USA,37°41'N,97°20'W,US/Central,399 -Louisville,USA,38°15'N,85°45'W,US/Eastern,142 -New Orleans,USA,29°57'N,90°4'W,US/Central,10 -Portland,USA,43°39'N,70°16'W,US/Eastern,6 -Baltimore,USA,39°17'N,76°37'W,US/Eastern,31 -Detroit,USA,42°19'N,83°2'W,US/Eastern,189 -Minneapolis,USA,44°58'N,93°15'W,US/Central,260 -Kansas City,USA,39°06'N,94°35'W,US/Central,256 -Billings,USA,45°47'N,108°32'W,US/Mountain,946 -Omaha,USA,41°15'N,96°0'W,US/Central,299 -Las Vegas,USA,36°10'N,115°08'W,US/Pacific,720 -Manchester,USA,42°59'N,71°27'W,US/Eastern,56 -Newark,USA,40°44'N,74°11'W,US/Eastern,4 -Albuquerque,USA,35°06'N,106°36'W,US/Mountain,1523 -New York,USA,40°43'N,74°0'W,US/Eastern,17 -Charlotte,USA,35°13'N,80°50'W,US/Eastern,217 -Fargo,USA,46°52'N,96°47'W,US/Central,271 -Cleveland,USA,41°28'N,81°40'W,US/Eastern,210 -Philadelphia,USA,39°57'N,75°10'W,US/Eastern,62 -Sioux Falls,USA,43°32'N,96°43'W,US/Central,443 -Memphis,USA,35°07'N,89°58'W,US/Central,84 -Houston,USA,29°45'N,95°22'W,US/Central,8 -Dallas,USA,32°47'N,96°48'W,US/Central,137 -Burlington,USA,44°28'N,73°9'W,US/Eastern,35 -Virginia Beach,USA,36°50'N,76°05'W,US/Eastern,9 -Seattle,USA,47°36'N,122°19'W,US/Pacific,63 -Milwaukee,USA,43°03'N,87°57'W,US/Central,188 -San Diego,USA,32°42'N,117°09'W,US/Pacific,16 -Orlando,USA,28°32'N,81°22'W,US/Eastern,35 -Buffalo,USA,42°54'N,78°50'W,US/Eastern,188 -Toledo,USA,41°39'N,83°34'W,US/Eastern,180 - -# Canadian cities -Vancouver,Canada,49°15'N,123°6'W,America/Vancouver,55 -Calgary,Canada,51°2'N,114°3'W,America/Edmonton,1040 -Edmonton,Canada,53°32'N,113°29'W,America/Edmonton,664 -Saskatoon,Canada,52°8'N,106°40'W,America/Regina,480 -Regina,Canada,50°27'N,104°36'W,America/Regina,577 -Winnipeg,Canada,49°53'N,97°8'W,America/Winnipeg,229 -Toronto,Canada,43°39'N,79°22'W,America/Toronto,77 -Montreal,Canada,45°30'N,73°33'W,America/Montreal,23 -Quebec,Canada,46°48'N,71°14'W,America/Toronto,87 -Fredericton,Canada,45°57'N,66°38'W,America/Halifax,8 -Halifax,Canada,44°38'N,63°34'W,America/Halifax,36 -Charlottetown,Canada,46°14'N,63°7'W,America/Halifax,2 -St. John's,Canada,47°33'N,52°42'W,America/Halifax,116 -Whitehorse,Canada,60°43'N,135°3'W,America/Whitehorse,696 -Yellowknife,Canada,62°27'N,114°22'W,America/Yellowknife,191 -Iqaluit,Canada,63°44'N,68°31'W,America/Iqaluit,3 -""" - -class AstralError(Exception): - """Astral base exception class""" - - -def excel_datediff(start_date, end_date): - """Return the same number of days between 2 dates as Excel does""" - return end_date.toordinal() - start_date.toordinal() + 2 - - -class Location(object): - """Provides access to information for single location.""" - - def __init__(self, info=None): - """Initializes the object with a tuple of information. - - :param info: A tuple of information to fill in the location info. - - The tuple should contain items in the following order - - ================ ============= - Field Default - ================ ============= - name Greenwich - region England - latitude 51.168 - longitude 0 - time zone name Europe/London - elevation 24 - ================ ============= - - See :attr:`timezone` property for a method of obtaining time zone - names - """ - - self.astral = None - if info is None: - self.name = 'Greenwich' - self.region = 'England' - self._latitude = 51.168 - self._longitude = 0.0 - self._timezone_group = 'Europe' - self._timezone_location = 'London' - self._elevation = 24 - else: - self.name = '' - self.region = '' - self._latitude = 0.0 - self._longitude = 0.0 - self._timezone_group = '' - self._timezone_location = '' - self._elevation = 0 - - try: - self.name = info[0] - self.region = info[1] - self.latitude = info[2] - self.longitude = info[3] - self.timezone = info[4] - self.elevation = info[5] - except IndexError: - pass - - self.url = '' - - def __repr__(self): - repr_format = '%s/%s, tz=%s, lat=%0.02f, lon=%0.02f' - return repr_format % (self.name, self.region, - self.timezone, - self.latitude, self.longitude) - - @property - def latitude(self): - """The location's latitude - - ``latitude`` can be set either as a string or as a number - - For strings they must be of the form - - degrees°minutes'[N|S] e.g. 51°31'N - - For numbers, positive numbers signify latitudes to the North. - """ - - return self._latitude - - @latitude.setter - def latitude(self, latitude): - if isinstance(latitude, str) or isinstance(latitude, ustr): - (deg, rest) = latitude.split("°", 1) - (minute, rest) = rest.split("'", 1) - - self._latitude = float(deg) + (float(minute) / 60) - - if latitude.endswith("S"): - self._latitude = -self._latitude - else: - self._latitude = float(latitude) - - @property - def longitude(self): - """The location's longitude. - - ``longitude`` can be set either as a string or as a number - - For strings they must be of the form - - degrees°minutes'[E|W] e.g. 51°31'W - - For numbers, positive numbers signify longitudes to the East. - """ - - return self._longitude - - @longitude.setter - def longitude(self, longitude): - if isinstance(longitude, str) or isinstance(longitude, ustr): - (deg, rest) = longitude.split("°", 1) - (minute, rest) = rest.split("'", 1) - - self._longitude = float(deg) + (float(minute) / 60) - - if longitude.endswith("W"): - self._longitude = -self._longitude - else: - self._longitude = float(longitude) - - @property - def elevation(self): - """The elevation in metres above sea level.""" - - return self._elevation - - @elevation.setter - def elevation(self, elevation): - self._elevation = int(elevation) - - @property - def timezone(self): - """The name of the time zone for the location. - - A list of time zone names can be obtained from pytz. For example. - - >>> from pytz import all_timezones - >>> for timezone in all_timezones: - ... print(timezone) - """ - - if self._timezone_location != '': - return '%s/%s' % (self._timezone_group, - self._timezone_location) - else: - return self._timezone_group - - @timezone.setter - def timezone(self, name): - if name not in pytz.all_timezones: - raise ValueError('Timezone \'%s\' not recognized' % name) - - try: - self._timezone_group, self._timezone_location = \ - name.split('/', 1) - except ValueError: - self._timezone_group = name - self._timezone_location = '' - - @property - def tz(self): - """Time zone information.""" - - try: - tz = pytz.timezone(self.timezone) - return tz - except pytz.UnknownTimeZoneError: - raise AstralError('Unknown timezone \'%s\'' % self.timezone) - - tzinfo = tz - - @property - def solar_depression(self): - """The number of degrees the sun must be below the horizon for the - dawn/dusk calculation. - - Can either be set as a number of degrees below the horizon or as - one of the following strings - - ============= ======= - String Degrees - ============= ======= - civil 6.0 - nautical 12.0 - astronomical 18.0 - ============= ======= - """ - - return self.astral.solar_depression - - @solar_depression.setter - def solar_depression(self, depression): - if self.astral is None: - self.astral = Astral() - - self.astral.solar_depression = depression - - def sun(self, date=None, local=True): - """Returns dawn, sunrise, noon, sunset and dusk as a dictionary. - - :param date: The date for which to calculate the times. - If no date is specified then the current date will be used. - - :param local: True = Time to be returned in location's time zone; - False = Time to be returned in UTC. - If not specified then the time will be returned in local time - - :returns: Dictionary with keys ``dawn``, ``sunrise``, ``noon``, - ``sunset`` and ``dusk`` whose values are the results of the - corresponding methods. - :rtype: dict - """ - - if self.astral is None: - self.astral = Astral() - - if date is None: - date = datetime.date.today() - - sun = self.astral.sun_utc(date, self.latitude, self.longitude) - - if local: - for key, dt in sun.items(): - sun[key] = dt.astimezone(self.tz) - - return sun - - def dawn(self, date=None, local=True): - """Calculates the time in the morning when the sun is a certain number - of degrees below the horizon. By default this is 6 degrees but can be - changed by setting the :attr:`Astral.solar_depression` property. - - :param date: The date for which to calculate the dawn time. - If no date is specified then the current date will be used. - - :param local: True = Time to be returned in location's time zone; - False = Time to be returned in UTC. - If not specified then the time will be returned in local time - - :returns: The date and time at which dawn occurs. - :rtype: :class:`~datetime.datetime` - """ - - if self.astral is None: - self.astral = Astral() - - if date is None: - date = datetime.date.today() - - dawn = self.astral.dawn_utc(date, self.latitude, self.longitude) - - if local: - return dawn.astimezone(self.tz) - else: - return dawn - - def sunrise(self, date=None, local=True): - """Return sunrise time. - - Calculates the time in the morning when the sun is a 0.833 degrees - below the horizon. This is to account for refraction. - - :param date: The date for which to calculate the sunrise time. - If no date is specified then the current date will be used. - - :param local: True = Time to be returned in location's time zone; - False = Time to be returned in UTC. - If not specified then the time will be returned in local time - - :returns: The date and time at which sunrise occurs. - :rtype: :class:`~datetime.datetime` - """ - - if self.astral is None: - self.astral = Astral() - - if date is None: - date = datetime.date.today() - - sunrise = self.astral.sunrise_utc(date, self.latitude, self.longitude) - - if local: - return sunrise.astimezone(self.tz) - else: - return sunrise - - def solar_noon(self, date=None, local=True): - """Calculates the solar noon (the time when the sun is at its highest - point.) - - :param date: The date for which to calculate the noon time. - If no date is specified then the current date will be used. - - :param local: True = Time to be returned in location's time zone; - False = Time to be returned in UTC. - If not specified then the time will be returned in local time - - :returns: The date and time at which the solar noon occurs. - :rtype: :class:`~datetime.datetime` - """ - - if self.astral is None: - self.astral = Astral() - - if date is None: - date = datetime.date.today() - - noon = self.astral.solar_noon_utc(date, self.longitude) - - if local: - return noon.astimezone(self.tz) - else: - return noon - - def sunset(self, date=None, local=True): - """Calculates sunset time (the time in the evening when the sun is a - 0.833 degrees below the horizon. This is to account for refraction.) - - :param date: The date for which to calculate the sunset time. - If no date is specified then the current date will be used. - - :param local: True = Time to be returned in location's time zone; - False = Time to be returned in UTC. - If not specified then the time will be returned in local time - - :returns: The date and time at which sunset occurs. - :rtype: :class:`~datetime.datetime` - """ - - if self.astral is None: - self.astral = Astral() - - if date is None: - date = datetime.date.today() - - sunset = self.astral.sunset_utc(date, self.latitude, self.longitude) - - if local: - return sunset.astimezone(self.tz) - else: - return sunset - - def dusk(self, date=None, local=True): - """Calculates the dusk time (the time in the evening when the sun is a - certain number of degrees below the horizon. By default this is 6 - degrees but can be changed by setting the - :attr:`solar_depression` property.) - - :param date: The date for which to calculate the dusk time. - If no date is specified then the current date will be used. - - :param local: True = Time to be returned in location's time zone; - False = Time to be returned in UTC. - If not specified then the time will be returned in local time - - :returns: The date and time at which dusk occurs. - :rtype: :class:`~datetime.datetime` - """ - - if self.astral is None: - self.astral = Astral() - - if date is None: - date = datetime.date.today() - - dusk = self.astral.dusk_utc(date, self.latitude, self.longitude) - - if local: - return dusk.astimezone(self.tz) - else: - return dusk - - def solar_midnight(self, date=None, local=True): - """Calculates the solar midnight (the time when the sun is at its lowest - point.) - - :param date: The date for which to calculate the midnight time. - If no date is specified then the current date will be used. - - :param local: True = Time to be returned in location's time zone; - False = Time to be returned in UTC. - If not specified then the time will be returned in local time - - :returns: The date and time at which the solar midnight occurs. - :rtype: :class:`~datetime.datetime` - """ - - if self.astral is None: - self.astral = Astral() - - if date is None: - date = datetime.date.today() - - midnight = self.astral.solar_midnight_utc(date, self.longitude) - - if local: - return midnight.astimezone(self.tz) - else: - return midnight - - def daylight(self, date=None, local=True): - """Calculates the daylight time (the time between sunrise and sunset) - - :param date: The date for which to calculate daylight. - If no date is specified then the current date will be used. - - :param local: True = Time to be returned in location's time zone; - False = Time to be returned in UTC. - If not specified then the time will be returned in local time - - :returns: A tuple containing the start and end times - :rtype: tuple(:class:`~datetime.datetime`, :class:`~datetime.datetime`) - """ - - if self.astral is None: - self.astral = Astral() - - if date is None: - date = datetime.date.today() - - start, end = self.astral.daylight_utc(date, self.latitude, self.longitude) - - if local: - return start.astimezone(self.tz), end.astimezone(self.tz) - else: - return start, end - - def night(self, date=None, local=True): - """Calculates the night time (the time between astronomical dusk and - astronomical dawn of the next day) - - :param date: The date for which to calculate the start of the night time. - If no date is specified then the current date will be used. - - :param local: True = Time to be returned in location's time zone; - False = Time to be returned in UTC. - If not specified then the time will be returned in local time - - :returns: A tuple containing the start and end times - :rtype: tuple(:class:`~datetime.datetime`, :class:`~datetime.datetime`) - """ - - if self.astral is None: - self.astral = Astral() - - if date is None: - date = datetime.date.today() - - start, end = self.astral.night_utc(date, self.latitude, self.longitude) - - if local: - return start.astimezone(self.tz), end.astimezone(self.tz) - else: - return start, end - - def twilight(self, direction=SUN_RISING, date=None, local=True): - """Returns the start and end times of Twilight in the UTC timezone when - the sun is traversing in the specified direction. - - This method defines twilight as being between the time - when the sun is at -6 degrees and sunrise/sunset. - - :param direction: Determines whether the time is for the sun rising or setting. - Use ``astral.SUN_RISING`` or ``astral.SUN_SETTING``. - :type direction: int - :param date: The date for which to calculate the times. - :type date: :class:`datetime.date` - :param local: True = Time to be returned in location's time zone; - False = Time to be returned in UTC. - If not specified then the time will be returned in local time - - :return: A tuple of the UTC date and time at which twilight starts and ends. - :rtype: (:class:`~datetime.datetime`, :class:`~datetime.datetime`) - """ - - if date is None: - date = datetime.date.today() - - start, end = self.astral.twilight_utc(direction, date, - self.latitude, self.longitude) - - if local: - return start.astimezone(self.tz), end.astimezone(self.tz) - else: - return start, end - - def time_at_elevation(self, elevation, direction=SUN_RISING, date=None, local=True): - """Calculate the time when the sun is at the specified elevation. - - Note: - This method uses positive elevations for those above the horizon. - - Elevations greater than 90 degrees are converted to a setting sun - i.e. an elevation of 110 will calculate a setting sun at 70 degrees. - - :param elevation: Elevation in degrees above the horizon to calculate for. - :type elevation: float - :param direction: Determines whether the time is for the sun rising or setting. - Use ``astral.SUN_RISING`` or ``astral.SUN_SETTING``. Default is rising. - :type direction: int - :param date: The date for which to calculate the elevation time. - If no date is specified then the current date will be used. - - :param local: True = Time to be returned in location's time zone; - False = Time to be returned in UTC. - If not specified then the time will be returned in local time - - :returns: The date and time at which dusk occurs. - :rtype: :class:`~datetime.datetime` - """ - - if self.astral is None: - self.astral = Astral() - - if date is None: - date = datetime.date.today() - - if elevation > 90.0: - elevation = 180.0 - elevation - direction = SUN_SETTING - - time_ = self.astral.time_at_elevation_utc(elevation, direction, - date, self.latitude, self.longitude) - - if local: - return time_.astimezone(self.tz) - else: - return time_ - - def rahukaalam(self, date=None, local=True): - """Calculates the period of rahukaalam. - - :param date: The date for which to calculate the rahukaalam period. - A value of ``None`` uses the current date. - - :param local: True = Time to be returned in location's time zone; - False = Time to be returned in UTC. - - :return: Tuple containing the start and end times for Rahukaalam. - :rtype: tuple - """ - - if self.astral is None: - self.astral = Astral() - - if date is None: - date = datetime.date.today() - - rahukaalam = self.astral.rahukaalam_utc(date, - self.latitude, self.longitude) - - if local: - rahukaalam = (rahukaalam[0].astimezone(self.tz), - rahukaalam[1].astimezone(self.tz)) - - return rahukaalam - - def golden_hour(self, direction=SUN_RISING, date=None, local=True): - """Returns the start and end times of the Golden Hour when the sun is traversing - in the specified direction. - - This method uses the definition from PhotoPills i.e. the - golden hour is when the sun is between 4 degrees below the horizon - and 6 degrees above. - - :param direction: Determines whether the time is for the sun rising or setting. - Use ``astral.SUN_RISING`` or ``astral.SUN_SETTING``. Default is rising. - :type direction: int - :param date: The date for which to calculate the times. - :type date: :class:`datetime.date` - :param local: True = Times to be returned in location's time zone; - False = Times to be returned in UTC. - If not specified then the time will be returned in local time - - :return: A tuple of the date and time at which the Golden Hour starts and ends. - :rtype: (:class:`~datetime.datetime`, :class:`~datetime.datetime`) - """ - - if self.astral is None: - self.astral = Astral() - - if date is None: - date = datetime.date.today() - - start, end = self.astral.golden_hour_utc(direction, date, - self.latitude, self.longitude) - - if local: - start = start.astimezone(self.tz) - end = end.astimezone(self.tz) - - return start, end - - def blue_hour(self, direction=SUN_RISING, date=None, local=True): - """Returns the start and end times of the Blue Hour when the sun is traversing - in the specified direction. - - This method uses the definition from PhotoPills i.e. the - blue hour is when the sun is between 6 and 4 degrees below the horizon. - - :param direction: Determines whether the time is for the sun rising or setting. - Use ``astral.SUN_RISING`` or ``astral.SUN_SETTING``. Default is rising. - :type direction: int - :param date: The date for which to calculate the times. - If no date is specified then the current date will be used. - - :param local: True = Times to be returned in location's time zone; - False = Times to be returned in UTC. - If not specified then the time will be returned in local time - - :return: A tuple of the date and time at which the Blue Hour starts and ends. - :rtype: (:class:`~datetime.datetime`, :class:`~datetime.datetime`) - """ - - if self.astral is None: - self.astral = Astral() - - if date is None: - date = datetime.date.today() - - start, end = self.astral.blue_hour_utc(direction, date, - self.latitude, self.longitude) - - if local: - start = start.astimezone(self.tz) - end = end.astimezone(self.tz) - - return start, end - - def solar_azimuth(self, dateandtime=None): - """Calculates the solar azimuth angle for a specific date/time. - - :param dateandtime: The date and time for which to calculate the angle. - :type dateandtime: :class:`~datetime.datetime` - - :returns: The azimuth angle in degrees clockwise from North. - :rtype: float - """ - - if self.astral is None: - self.astral = Astral() - - if dateandtime is None: - dateandtime = datetime.datetime.now(self.tz) - elif not dateandtime.tzinfo: - dateandtime = self.tz.localize(dateandtime) - - dateandtime = dateandtime.astimezone(pytz.UTC) - - return self.astral.solar_azimuth(dateandtime, - self.latitude, self.longitude) - - def solar_elevation(self, dateandtime=None): - """Calculates the solar elevation angle for a specific time. - - :param dateandtime: The date and time for which to calculate the angle. - :type dateandtime: :class:`~datetime.datetime` - - :returns: The elevation angle in degrees above the horizon. - :rtype: float - """ - - if self.astral is None: - self.astral = Astral() - - if dateandtime is None: - dateandtime = datetime.datetime.now(self.tz) - elif not dateandtime.tzinfo: - dateandtime = self.tz.localize(dateandtime) - - dateandtime = dateandtime.astimezone(pytz.UTC) - - return self.astral.solar_elevation(dateandtime, - self.latitude, self.longitude) - - def solar_zenith(self, dateandtime=None): - """Calculates the solar zenith angle for a specific time. - - :param dateandtime: The date and time for which to calculate the angle. - :type dateandtime: :class:`~datetime.datetime` - - :returns: The zenith angle in degrees from vertical. - :rtype: float - """ - - return 90.0 - self.solar_elevation(dateandtime) - - def moon_phase(self, date=None): - """Calculates the moon phase for a specific date. - - :param date: The date to calculate the phase for. - If ommitted the current date is used. - :type date: :class:`datetime.date` - - :returns: - A number designating the phase - - | 0 = New moon - | 7 = First quarter - | 14 = Full moon - | 21 = Last quarter - :rtype: int - """ - - if self.astral is None: - self.astral = Astral() - - if date is None: - date = datetime.date.today() - - return self.astral.moon_phase(date) - - -class LocationGroup(object): - """Groups a set of timezones by the timezone group""" - - def __init__(self, name): - self.name = name - self._locations = {} - - def __getitem__(self, key): - """Returns a Location object for the specified `key`. - - group = astral.europe - location = group['London'] - - You can supply an optional region name by adding a comma - followed by the region name. Where multiple locations have the - same name you may need to supply the region name otherwise - the first result will be returned which may not be the one - you're looking for. - - location = group['Abu Dhabi,United Arab Emirates'] - - Handles location names with spaces and mixed case. - """ - - key = self._sanitize_key(key) - - try: - lookup_name, lookup_region = key.split(',', 1) - except ValueError: - lookup_name = key - lookup_region = '' - - lookup_name = lookup_name.strip('"\'') - lookup_region = lookup_region.strip('"\'') - - for (location_name, location_list) in self._locations.items(): - if location_name == lookup_name: - if lookup_region == '': - return location_list[0] - - for location in location_list: - if self._sanitize_key(location.region) == lookup_region: - return location - - raise KeyError('Unrecognised location name - %s' % key) - - def __setitem__(self, key, value): - key = self._sanitize_key(key) - if key not in self._locations: - self._locations[key] = [value] - else: - self._locations[key].append(value) - - def __contains__(self, key): - key = self._sanitize_key(key) - for name in self._locations.keys(): - if name == key: - return True - - return False - - def __iter__(self): - for location_list in self._locations.values(): - for location in location_list: - yield location - - def keys(self): - return self._locations.keys() - - def values(self): - return self._locations.values() - - def items(self): - return self._locations.items() - - @property - def locations(self): - k = [] - for location_list in self._locations.values(): - for location in location_list: - k.append(location.name) - - return k - - def _sanitize_key(self, key): - return str(key).lower().replace(' ', '_') - - -class AstralGeocoder(object): - """Looks up geographic information from the locations stored within the - module - """ - - def __init__(self): - self._groups = {} - - locations = _LOCATION_INFO.split('\n') - for line in locations: - line = line.strip() - if line != '' and line[0] != '#': - if line[-1] == '\n': - line = line[:-1] - - info = line.split(',') - - l = Location(info) - - key = l._timezone_group.lower() - try: - group = self.__getattr__(key) - except AttributeError: - group = LocationGroup(l._timezone_group) - self._groups[key] = group - - group[info[0].lower()] = l - - def __getattr__(self, key): - """Access to each timezone group. For example London is in timezone - group Europe. - - Attribute lookup is case insensitive""" - - key = str(key).lower() - for name, value in self._groups.items(): - if name == key: - return value - - raise AttributeError('Group \'%s\' not found' % key) - - def __getitem__(self, key): - """Lookup a location within all timezone groups. - - Item lookup is case insensitive.""" - - key = str(key).lower() - for group in self._groups.values(): - try: - return group[key] - except KeyError: - pass - - raise KeyError('Unrecognised location name - %s' % key) - - def __iter__(self): - return self._groups.__iter__() - - def __contains__(self, key): - key = str(key).lower() - for name, group in self._groups.items(): - if name == key: - return True - - if key in group: - return True - - return False - - @property - def locations(self): - k = [] - for group in self._groups.values(): - k.extend(group.locations) - - return k - - @property - def groups(self): - return self._groups - - -class GoogleGeocoder(object): - """Use Google Maps API Web Service to lookup GPS co-ordinates, timezone and - elevation. - - See the following for more info. - https://developers.google.com/maps/documentation/ - """ - - def __init__(self, cache=False): - self.cache = cache - self.geocache = {} - self._location_query_base = 'http://maps.googleapis.com/maps/api/geocode/json?address=%s&sensor=false' - self._timezone_query_base = 'https://maps.googleapis.com/maps/api/timezone/json?location=%f,%f×tamp=%d&sensor=false' - self._elevation_query_base = 'http://maps.googleapis.com/maps/api/elevation/json?locations=%f,%f&sensor=false' - - def __getitem__(self, key): - if self.cache and key in self.geocache: - return self.geocache[key] - - location = Location() - try: - self._get_geocoding(key, location) - self._get_timezone(location) - self._get_elevation(location) - except URLError: - raise AstralError(('GoogleGeocoder: Unable to contact ' - 'Google maps API')) - - url = 'http://maps.google.com/maps?q=loc:%f,%f' - location.url = url % (location.latitude, location.longitude) - - if self.cache: - self.geocache[key] = location - - return location - - def _get_geocoding(self, key, location): - """Lookup the Google geocoding API information for `key`""" - - url = self._location_query_base % quote_plus(key) - data = self._read_from_url(url) - response = json.loads(data) - if response['status'] == 'OK': - formatted_address = response['results'][0]['formatted_address'] - pos = formatted_address.find(',') - if pos == -1: - location.name = formatted_address - location.region = '' - else: - location.name = formatted_address[:pos].strip() - location.region = formatted_address[pos + 1:].strip() - - l = response['results'][0]['geometry']['location'] - location.latitude = float(l['lat']) - location.longitude = float(l['lng']) - else: - raise AstralError('GoogleGeocoder: Unable to locate %s' % key) - - def _get_timezone(self, location): - """Query the timezone information with the latitude and longitude of - the specified `location`. - - This function assumes the timezone of the location has always been - the same as it is now by using time() in the query string. - """ - - url = self._timezone_query_base % (location.latitude, - location.longitude, - int(time())) - data = self._read_from_url(url) - response = json.loads(data) - if response['status'] == 'OK': - location.timezone = response['timeZoneId'] - else: - location.timezone = 'UTC' - - def _get_elevation(self, location): - """Query the elevation information with the latitude and longitude of - the specified `location`. - """ - - url = self._elevation_query_base % (location.latitude, - location.longitude) - data = self._read_from_url(url) - response = json.loads(data) - if response['status'] == 'OK': - location.elevation = int(float(response['results'][0]['elevation'])) - else: - location.elevation = 0 - - def _read_from_url(self, url): - ds = urlopen(url) - content_types = ds.headers.get('Content-Type').split(';') - - charset = 'UTF-8' - for ct in content_types: - if ct.strip().startswith('charset'): - charset = ct.split('=')[1] - - data = ds.read().decode(charset) - ds.close() - - return data - - -class Astral(object): - def __init__(self, geocoder=AstralGeocoder): - """Initialise the geocoder and set the default depression.""" - - self.geocoder = geocoder() - self._depression = 6 # Set default depression in degrees - - def __getitem__(self, key): - """Returns the Location instance specified by ``key``.""" - - location = self.geocoder[key] - location.astral = self - return location - - @property - def solar_depression(self): - """The number of degrees the sun must be below the horizon for the - dawn/dusk calculation. - - Can either be set as a number of degrees below the horizon or as - one of the following strings - - ============= ======= - String Degrees - ============= ======= - civil 6.0 - nautical 12.0 - astronomical 18.0 - ============= ======= - """ - - return self._depression - - @solar_depression.setter - def solar_depression(self, depression): - if isinstance(depression, str) or isinstance(depression, ustr): - try: - self._depression = { - 'civil': 6, - 'nautical': 12, - 'astronomical': 18}[depression] - except: - raise KeyError(("solar_depression must be either a number " - "or one of 'civil', 'nautical' or " - "'astronomical'")) - else: - self._depression = float(depression) - - def sun_utc(self, date, latitude, longitude): - """Calculate all the info for the sun at once. - All times are returned in the UTC timezone. - - :param date: Date to calculate for. - :type date: :class:`datetime.date` - :param latitude: Latitude - Northern latitudes should be positive - :type latitude: float - :param longitude: Longitude - Eastern longitudes should be positive - :type longitude: float - - :returns: Dictionary with keys ``dawn``, ``sunrise``, ``noon``, - ``sunset`` and ``dusk`` whose values are the results of the - corresponding `_utc` methods. - :rtype: dict - """ - - dawn = self.dawn_utc(date, latitude, longitude) - sunrise = self.sunrise_utc(date, latitude, longitude) - noon = self.solar_noon_utc(date, longitude) - sunset = self.sunset_utc(date, latitude, longitude) - dusk = self.dusk_utc(date, latitude, longitude) - - return { - 'dawn': dawn, - 'sunrise': sunrise, - 'noon': noon, - 'sunset': sunset, - 'dusk': dusk - } - - def dawn_utc(self, date, latitude, longitude, depression=0): - """Calculate dawn time in the UTC timezone. - - :param date: Date to calculate for. - :type date: :class:`datetime.date` - :param latitude: Latitude - Northern latitudes should be positive - :type latitude: float - :param longitude: Longitude - Eastern longitudes should be positive - :type longitude: float - :param depression: Override the depression used - :type depression: float - - :return: The UTC date and time at which dawn occurs. - :rtype: :class:`~datetime.datetime` - """ - - if depression == 0: - depression = self._depression - depression += 90 - - try: - return self._calc_time(depression, SUN_RISING, date, latitude, longitude) - except: - raise AstralError(('Sun never reaches %d degrees below the horizon, ' - 'at this location.') % (depression - 90)) - - def sunrise_utc(self, date, latitude, longitude): - """Calculate sunrise time in the UTC timezone. - - :param date: Date to calculate for. - :type date: :class:`datetime.date` - :param latitude: Latitude - Northern latitudes should be positive - :type latitude: float - :param longitude: Longitude - Eastern longitudes should be positive - :type longitude: float - - :return: The UTC date and time at which sunrise occurs. - :rtype: :class:`~datetime.datetime` - """ - - try: - return self._calc_time(90 + 0.833, SUN_RISING, date, latitude, longitude) - except: - raise AstralError(('Sun never reaches the horizon on this day, ' - 'at this location.')) - - def solar_noon_utc(self, date, longitude): - """Calculate solar noon time in the UTC timezone. - - :param date: Date to calculate for. - :type date: :class:`datetime.date` - :param longitude: Longitude - Eastern longitudes should be positive - :type longitude: float - - :return: The UTC date and time at which noon occurs. - :rtype: :class:`~datetime.datetime` - """ - - jc = self._jday_to_jcentury(self._julianday(date)) - eqtime = self._eq_of_time(jc) - timeUTC = (720.0 - (4 * longitude) - eqtime) / 60.0 - - hour = int(timeUTC) - minute = int((timeUTC - hour) * 60) - second = int((((timeUTC - hour) * 60) - minute) * 60) - - if second > 59: - second -= 60 - minute += 1 - elif second < 0: - second += 60 - minute -= 1 - - if minute > 59: - minute -= 60 - hour += 1 - elif minute < 0: - minute += 60 - hour -= 1 - - if hour > 23: - hour -= 24 - date += datetime.timedelta(days=1) - elif hour < 0: - hour += 24 - date -= datetime.timedelta(days=1) - - noon = datetime.datetime(date.year, date.month, date.day, - hour, minute, second) - noon = pytz.UTC.localize(noon) - - return noon - - def sunset_utc(self, date, latitude, longitude): - """Calculate sunset time in the UTC timezone. - - :param date: Date to calculate for. - :type date: :class:`datetime.date` - :param latitude: Latitude - Northern latitudes should be positive - :type latitude: float - :param longitude: Longitude - Eastern longitudes should be positive - :type longitude: float - - :return: The UTC date and time at which sunset occurs. - :rtype: :class:`~datetime.datetime` - """ - - try: - return self._calc_time(90 + 0.833, SUN_SETTING, date, latitude, longitude) - except: - raise AstralError(('Sun never reaches the horizon on this day, ' - 'at this location.')) - - def dusk_utc(self, date, latitude, longitude, depression=0): - """Calculate dusk time in the UTC timezone. - - :param date: Date to calculate for. - :type date: :class:`datetime.date` - :param latitude: Latitude - Northern latitudes should be positive - :type latitude: float - :param longitude: Longitude - Eastern longitudes should be positive - :type longitude: float - :param depression: Override the depression used - :type depression: float - - :return: The UTC date and time at which dusk occurs. - :rtype: :class:`~datetime.datetime` - """ - - if depression == 0: - depression = self._depression - depression += 90 - - try: - return self._calc_time(depression, SUN_SETTING, date, latitude, longitude) - except: - raise AstralError(('Sun never reaches %d degrees below the horizon, ' - 'at this location.') % (depression - 90)) - - def solar_midnight_utc(self, date, longitude): - """Calculate solar midnight time in the UTC timezone. - - Note that this claculates the solar midgnight that is closest - to 00:00:00 of the specified date i.e. it may return a time that is on - the previous day. - - :param date: Date to calculate for. - :type date: :class:`datetime.date` - :param longitude: Longitude - Eastern longitudes should be positive - :type longitude: float - - :return: The UTC date and time at which midnight occurs. - :rtype: :class:`~datetime.datetime` - """ - - julianday = self._julianday(date) - - newt = self._jday_to_jcentury(julianday + 0.5 + -longitude / 360.0) - - eqtime = self._eq_of_time(newt) - timeUTC = (-longitude * 4.0) - eqtime - - timeUTC = timeUTC / 60.0 - hour = int(timeUTC) - minute = int((timeUTC - hour) * 60) - second = int((((timeUTC - hour) * 60) - minute) * 60) - - if second > 59: - second -= 60 - minute += 1 - elif second < 0: - second += 60 - minute -= 1 - - if minute > 59: - minute -= 60 - hour += 1 - elif minute < 0: - minute += 60 - hour -= 1 - - if hour < 0: - hour += 24 - date -= datetime.timedelta(days=1) - - midnight = datetime.datetime(date.year, date.month, date.day, - hour, minute, second) - midnight = pytz.UTC.localize(midnight) - - return midnight - - def daylight_utc(self, date, latitude, longitude): - """Calculate daylight start and end times in the UTC timezone. - - :param date: Date to calculate for. - :type date: :class:`datetime.date` - :param latitude: Latitude - Northern latitudes should be positive - :type latitude: float - :param longitude: Longitude - Eastern longitudes should be positive - :type longitude: float - - :return: A tuple of the UTC date and time at which daylight starts and ends. - :rtype: (:class:`~datetime.datetime`, :class:`~datetime.datetime`) - """ - - start = self.sunrise_utc(date, latitude, longitude) - end = self.sunset_utc(date, latitude, longitude) - - return start, end - - def night_utc(self, date, latitude, longitude): - """Calculate night start and end times in the UTC timezone. - - Night is calculated to be between astronomical dusk on the - date specified and astronomical dawn of the next day. - - :param date: Date to calculate for. - :type date: :class:`datetime.date` - :param latitude: Latitude - Northern latitudes should be positive - :type latitude: float - :param longitude: Longitude - Eastern longitudes should be positive - :type longitude: float - - :return: A tuple of the UTC date and time at which night starts and ends. - :rtype: (:class:`~datetime.datetime`, :class:`~datetime.datetime`) - """ - - start = self.dusk_utc(date, latitude, longitude, 18) - tomorrow = date + datetime.timedelta(days=1) - end = self.dawn_utc(tomorrow, latitude, longitude, 18) - - return start, end - - def twilight_utc(self, direction, date, latitude, longitude): - """Returns the start and end times of Twilight in the UTC timezone when - the sun is traversing in the specified direction. - - This method defines twilight as being between the time - when the sun is at -6 degrees and sunrise/sunset. - - :param direction: Determines whether the time is for the sun rising or setting. - Use ``astral.SUN_RISING`` or ``astral.SUN_SETTING``. - :type direction: int - :param date: The date for which to calculate the times. - :type date: :class:`datetime.date` - :param latitude: Latitude - Northern latitudes should be positive - :type latitude: float - :param longitude: Longitude - Eastern longitudes should be positive - :type longitude: float - - :return: A tuple of the UTC date and time at which twilight starts and ends. - :rtype: (:class:`~datetime.datetime`, :class:`~datetime.datetime`) - """ - - if date is None: - date = datetime.date.today() - - start = self.time_at_elevation_utc(-6, direction, date, latitude, longitude) - if direction == SUN_RISING: - end = self.sunrise_utc(date, latitude, longitude) - else: - end = self.sunset_utc(date, latitude, longitude) - - if direction == SUN_RISING: - return start, end - else: - return end, start - - def golden_hour_utc(self, direction, date, latitude, longitude): - """Returns the start and end times of the Golden Hour in the UTC timezone - when the sun is traversing in the specified direction. - - This method uses the definition from PhotoPills i.e. the - golden hour is when the sun is between 4 degrees below the horizon - and 6 degrees above. - - :param direction: Determines whether the time is for the sun rising or setting. - Use ``astral.SUN_RISING`` or ``astral.SUN_SETTING``. - :type direction: int - :param date: The date for which to calculate the times. - :type date: :class:`datetime.date` - :param latitude: Latitude - Northern latitudes should be positive - :type latitude: float - :param longitude: Longitude - Eastern longitudes should be positive - :type longitude: float - - :return: A tuple of the UTC date and time at which the Golden Hour starts and ends. - :rtype: (:class:`~datetime.datetime`, :class:`~datetime.datetime`) - """ - - if date is None: - date = datetime.date.today() - - start = self.time_at_elevation_utc(-4, direction, date, - latitude, longitude) - end = self.time_at_elevation_utc(6, direction, date, - latitude, longitude) - - if direction == SUN_RISING: - return start, end - else: - return end, start - - def blue_hour_utc(self, direction, date, latitude, longitude): - """Returns the start and end times of the Blue Hour in the UTC timezone - when the sun is traversing in the specified direction. - - This method uses the definition from PhotoPills i.e. the - blue hour is when the sun is between 6 and 4 degrees below the horizon. - - :param direction: Determines whether the time is for the sun rising or setting. - Use ``astral.SUN_RISING`` or ``astral.SUN_SETTING``. - :type direction: int - :param date: The date for which to calculate the times. - :type date: :class:`datetime.date` - :param latitude: Latitude - Northern latitudes should be positive - :type latitude: float - :param longitude: Longitude - Eastern longitudes should be positive - :type longitude: float - - :return: A tuple of the UTC date and time at which the Blue Hour starts and ends. - :rtype: (:class:`~datetime.datetime`, :class:`~datetime.datetime`) - """ - - if date is None: - date = datetime.date.today() - - start = self.time_at_elevation_utc(-6, direction, date, - latitude, longitude) - end = self.time_at_elevation_utc(-4, direction, date, - latitude, longitude) - - if direction == SUN_RISING: - return start, end - else: - return end, start - - def time_at_elevation_utc(self, elevation, direction, date, latitude, longitude): - """Calculate the time in the UTC timezone when the sun is at - the specified elevation on the specified date. - - Note: This method uses positive elevations for those above the horizon. - - :param elevation: Elevation in degrees above the horizon to calculate for. - :type elevation: float - :param direction: Determines whether the calculated time is for the sun rising or setting. - Use ``astral.SUN_RISING`` or ``astral.SUN_SETTING``. Default is rising. - :type direction: int - :param date: Date to calculate for. - :type date: :class:`datetime.date` - :param latitude: Latitude - Northern latitudes should be positive - :type latitude: float - :param longitude: Longitude - Eastern longitudes should be positive - :type longitude: float - - :return: The UTC date and time at which the sun is at the required - elevation. - :rtype: :class:`~datetime.datetime` - """ - - if elevation > 90.0: - elevation = 180.0 - elevation - direction = SUN_SETTING - - depression = 90 - elevation - try: - return self._calc_time(depression, direction, date, latitude, longitude) - except Exception: - raise AstralError(('Sun never reaches an elevation of %d degrees' - 'at this location.') % elevation) - - def solar_azimuth(self, dateandtime, latitude, longitude): - """Calculate the azimuth angle of the sun. - - :param dateandtime: The date and time for which to calculate - the angle. - :type dateandtime: :class:`~datetime.datetime` - :param latitude: Latitude - Northern latitudes should be positive - :type latitude: float - :param longitude: Longitude - Eastern longitudes should be positive - :type longitude: float - - :return: The azimuth angle in degrees clockwise from North. - :rtype: float - - If `dateandtime` is a naive Python datetime then it is assumed to be - in the UTC timezone. - """ - - if latitude > 89.8: - latitude = 89.8 - - if latitude < -89.8: - latitude = -89.8 - - if dateandtime.tzinfo is None: - zone = 0 - utc_datetime = dateandtime - else: - zone = -dateandtime.utcoffset().total_seconds() / 3600.0 - utc_datetime = dateandtime.astimezone(pytz.utc) - - timenow = utc_datetime.hour + (utc_datetime.minute / 60.0) + \ - (utc_datetime.second / 3600.0) - - JD = self._julianday(dateandtime) - t = self._jday_to_jcentury(JD + timenow / 24.0) - theta = self._sun_declination(t) - eqtime = self._eq_of_time(t) - solarDec = theta # in degrees - - solarTimeFix = eqtime - (4.0 * -longitude) + (60 * zone) - trueSolarTime = dateandtime.hour * 60.0 + dateandtime.minute + \ - dateandtime.second / 60.0 + solarTimeFix - # in minutes - - while trueSolarTime > 1440: - trueSolarTime = trueSolarTime - 1440 - - hourangle = trueSolarTime / 4.0 - 180.0 - # Thanks to Louis Schwarzmayr for the next line: - if hourangle < -180: - hourangle = hourangle + 360.0 - - harad = radians(hourangle) - - csz = sin(radians(latitude)) * sin(radians(solarDec)) + \ - cos(radians(latitude)) * cos(radians(solarDec)) * cos(harad) - - if csz > 1.0: - csz = 1.0 - elif csz < -1.0: - csz = -1.0 - - zenith = degrees(acos(csz)) - - azDenom = (cos(radians(latitude)) * sin(radians(zenith))) - - if abs(azDenom) > 0.001: - azRad = ((sin(radians(latitude)) * cos(radians(zenith))) - - sin(radians(solarDec))) / azDenom - - if abs(azRad) > 1.0: - if azRad < 0: - azRad = -1.0 - else: - azRad = 1.0 - - azimuth = 180.0 - degrees(acos(azRad)) - - if hourangle > 0.0: - azimuth = -azimuth - else: - if latitude > 0.0: - azimuth = 180.0 - else: - azimuth = 0.0 - - if azimuth < 0.0: - azimuth = azimuth + 360.0 - - return azimuth - - def solar_elevation(self, dateandtime, latitude, longitude): - """Calculate the elevation angle of the sun. - - :param dateandtime: The date and time for which to calculate - the angle. - :type dateandtime: :class:`~datetime.datetime` - :param latitude: Latitude - Northern latitudes should be positive - :type latitude: float - :param longitude: Longitude - Eastern longitudes should be positive - :type longitude: float - - :return: The elevation angle in degrees above the horizon. - :rtype: float - - If `dateandtime` is a naive Python datetime then it is assumed to be - in the UTC timezone. - """ - - if latitude > 89.8: - latitude = 89.8 - - if latitude < -89.8: - latitude = -89.8 - - if dateandtime.tzinfo is None: - zone = 0 - utc_datetime = dateandtime - else: - zone = -dateandtime.utcoffset().total_seconds() / 3600.0 - utc_datetime = dateandtime.astimezone(pytz.utc) - - timenow = utc_datetime.hour + (utc_datetime.minute / 60.0) + \ - (utc_datetime.second / 3600) - - JD = self._julianday(dateandtime) - t = self._jday_to_jcentury(JD + timenow / 24.0) - theta = self._sun_declination(t) - eqtime = self._eq_of_time(t) - solarDec = theta # in degrees - - solarTimeFix = eqtime - (4.0 * -longitude) + (60 * zone) - trueSolarTime = dateandtime.hour * 60.0 + dateandtime.minute + \ - dateandtime.second / 60.0 + solarTimeFix - # in minutes - - while trueSolarTime > 1440: - trueSolarTime = trueSolarTime - 1440 - - hourangle = trueSolarTime / 4.0 - 180.0 - # Thanks to Louis Schwarzmayr for the next line: - if hourangle < -180: - hourangle = hourangle + 360.0 - - harad = radians(hourangle) - - csz = sin(radians(latitude)) * sin(radians(solarDec)) + \ - cos(radians(latitude)) * cos(radians(solarDec)) * cos(harad) - - if csz > 1.0: - csz = 1.0 - elif csz < -1.0: - csz = -1.0 - - zenith = degrees(acos(csz)) - - azDenom = (cos(radians(latitude)) * sin(radians(zenith))) - - if abs(azDenom) > 0.001: - azRad = ((sin(radians(latitude)) * cos(radians(zenith))) - - sin(radians(solarDec))) / azDenom - - if abs(azRad) > 1.0: - if azRad < 0: - azRad = -1.0 - else: - azRad = 1.0 - - azimuth = 180.0 - degrees(acos(azRad)) - - if hourangle > 0.0: - azimuth = -azimuth - else: - if latitude > 0.0: - azimuth = 180.0 - else: - azimuth = 0.0 - - if azimuth < 0.0: - azimuth = azimuth + 360.0 - - exoatmElevation = 90.0 - zenith - - if exoatmElevation > 85.0: - refractionCorrection = 0.0 - else: - te = tan(radians(exoatmElevation)) - if exoatmElevation > 5.0: - refractionCorrection = 58.1 / te - 0.07 / (te * te * te) + \ - 0.000086 / (te * te * te * te * te) - elif exoatmElevation > -0.575: - step1 = (-12.79 + exoatmElevation * 0.711) - step2 = (103.4 + exoatmElevation * (step1)) - step3 = (-518.2 + exoatmElevation * (step2)) - refractionCorrection = 1735.0 + exoatmElevation * (step3) - else: - refractionCorrection = -20.774 / te - - refractionCorrection = refractionCorrection / 3600.0 - - solarzen = zenith - refractionCorrection - - solarelevation = 90.0 - solarzen - - return solarelevation - - def solar_zenith(self, dateandtime, latitude, longitude): - """Calculates the solar zenith angle. - - :param dateandtime: The date and time for which to calculate - the angle. - :type dateandtime: :class:`~datetime.datetime` - :param latitude: Latitude - Northern latitudes should be positive - :type latitude: float - :param longitude: Longitude - Eastern longitudes should be positive - :type longitude: float - - :return: The zenith angle in degrees from vertical. - :rtype: float - - If `dateandtime` is a naive Python datetime then it is assumed to be - in the UTC timezone. - """ - - return 90.0 - self.solar_elevation(dateandtime, latitude, longitude) - - def moon_phase(self, date): - """Calculates the phase of the moon on the specified date. - - :param date: The date to calculate the phase for. - :type date: :class:`datetime.date` - - :return: - A number designating the phase - - | 0 = New moon - | 7 = First quarter - | 14 = Full moon - | 21 = Last quarter - :rtype: int - """ - - jd = self._julianday(date) - DT = pow((jd - 2382148), 2) / (41048480 * 86400) - T = (jd + DT - 2451545.0) / 36525 - T2 = pow(T, 2) - T3 = pow(T, 3) - D = 297.85 + (445267.1115 * T) - (0.0016300 * T2) + (T3 / 545868) - D = radians(self._proper_angle(D)) - M = 357.53 + (35999.0503 * T) - M = radians(self._proper_angle(M)) - M1 = 134.96 + (477198.8676 * T) + (0.0089970 * T2) + (T3 / 69699) - M1 = radians(self._proper_angle(M1)) - elong = degrees(D) + 6.29 * sin(M1) - elong -= 2.10 * sin(M) - elong += 1.27 * sin(2 * D - M1) - elong += 0.66 * sin(2 * D) - elong = self._proper_angle(elong) - elong = round(elong) - moon = ((elong + 6.43) / 360) * 28 - moon = floor(moon) - if moon == 28: - moon = 0 - - return moon - - def rahukaalam_utc(self, date, latitude, longitude): - """Calculate ruhakaalam times in the UTC timezone. - - :param date: Date to calculate for. - :type date: :class:`datetime.date` - :param latitude: Latitude - Northern latitudes should be positive - :type latitude: float - :param longitude: Longitude - Eastern longitudes should be positive - :type longitude: float - - :return: Tuple containing the start and end times for Rahukaalam. - :rtype: tuple - """ - - if date is None: - date = datetime.date.today() - - sunrise = self.sunrise_utc(date, latitude, longitude) - sunset = self.sunset_utc(date, latitude, longitude) - - octant_duration = datetime.timedelta(seconds=(sunset - sunrise).seconds / 8) - - # Mo,Sa,Fr,We,Th,Tu,Su - octant_index = [1, 6, 4, 5, 3, 2, 7] - - weekday = date.weekday() - octant = octant_index[weekday] - - start = sunrise + (octant_duration * octant) - end = start + octant_duration - - return start, end - - def _proper_angle(self, value): - if value > 0.0: - value /= 360.0 - return (value - floor(value)) * 360.0 - else: - tmp = ceil(abs(value / 360.0)) - return value + tmp * 360.0 - - def _julianday(self, utcdatetime, timezone=None): - if isinstance(utcdatetime, datetime.datetime): - end_date = utcdatetime.date() - hour = utcdatetime.hour - minute = utcdatetime.minute - second = utcdatetime.second - else: - end_date = utcdatetime - hour = 0 - minute = 0 - second = 0 - - if timezone: - if isinstance(timezone, int): - hour_offset = timezone - else: - offset = timezone.localize(utcdatetime).utcoffset() - hour_offset = offset.total_seconds() / 3600.0 - else: - hour_offset = 0 - - start_date = datetime.date(1900, 1, 1) - time_fraction = (hour * 3600.0 + minute * 60.0 + second) / (24.0 * 3600.0) - date_diff = excel_datediff(start_date, end_date) - jd = date_diff + 2415018.5 + time_fraction - (hour_offset / 24) - - return jd - - def _jday_to_jcentury(self, julianday): - return (julianday - 2451545.0) / 36525.0 - - def _jcentury_to_jday(self, juliancentury): - return (juliancentury * 36525.0) + 2451545.0 - - def _geom_mean_long_sun(self, juliancentury): - l0 = 280.46646 + \ - juliancentury * (36000.76983 + 0.0003032 * juliancentury) - return l0 % 360.0 - - def _geom_mean_anomaly_sun(self, juliancentury): - return 357.52911 + \ - juliancentury * (35999.05029 - 0.0001537 * juliancentury) - - def _eccentrilocation_earth_orbit(self, juliancentury): - return 0.016708634 - \ - juliancentury * (0.000042037 + 0.0000001267 * juliancentury) - - def _sun_eq_of_center(self, juliancentury): - m = self._geom_mean_anomaly_sun(juliancentury) - - mrad = radians(m) - sinm = sin(mrad) - sin2m = sin(mrad + mrad) - sin3m = sin(mrad + mrad + mrad) - - c = sinm * (1.914602 - juliancentury * \ - (0.004817 + 0.000014 * juliancentury)) + \ - sin2m * (0.019993 - 0.000101 * juliancentury) + sin3m * 0.000289 - - return c - - def _sun_true_long(self, juliancentury): - l0 = self._geom_mean_long_sun(juliancentury) - c = self._sun_eq_of_center(juliancentury) - - return l0 + c - - def _sun_true_anomoly(self, juliancentury): - m = self._geom_mean_anomaly_sun(juliancentury) - c = self._sun_eq_of_center(juliancentury) - - return m + c - - def _sun_rad_vector(self, juliancentury): - v = self._sun_true_anomoly(juliancentury) - e = self._eccentrilocation_earth_orbit(juliancentury) - - return (1.000001018 * (1 - e * e)) / (1 + e * cos(radians(v))) - - def _sun_apparent_long(self, juliancentury): - O = self._sun_true_long(juliancentury) - - omega = 125.04 - 1934.136 * juliancentury - return O - 0.00569 - 0.00478 * sin(radians(omega)) - - def _mean_obliquity_of_ecliptic(self, juliancentury): - seconds = 21.448 - juliancentury * \ - (46.815 + juliancentury * (0.00059 - juliancentury * (0.001813))) - return 23.0 + (26.0 + (seconds / 60.0)) / 60.0 - - def _obliquity_correction(self, juliancentury): - e0 = self._mean_obliquity_of_ecliptic(juliancentury) - - omega = 125.04 - 1934.136 * juliancentury - return e0 + 0.00256 * cos(radians(omega)) - - def _sun_rt_ascension(self, juliancentury): - oc = self._obliquity_correction(juliancentury) - al = self._sun_apparent_long(juliancentury) - - tananum = (cos(radians(oc)) * sin(radians(al))) - tanadenom = cos(radians(al)) - - return degrees(atan2(tananum, tanadenom)) - - def _sun_declination(self, juliancentury): - e = self._obliquity_correction(juliancentury) - lambd = self._sun_apparent_long(juliancentury) - - sint = sin(radians(e)) * sin(radians(lambd)) - return degrees(asin(sint)) - - def _var_y(self, juliancentury): - epsilon = self._obliquity_correction(juliancentury) - y = tan(radians(epsilon) / 2.0) - return y * y - - def _eq_of_time(self, juliancentury): - l0 = self._geom_mean_long_sun(juliancentury) - e = self._eccentrilocation_earth_orbit(juliancentury) - m = self._geom_mean_anomaly_sun(juliancentury) - - y = self._var_y(juliancentury) - - sin2l0 = sin(2.0 * radians(l0)) - sinm = sin(radians(m)) - cos2l0 = cos(2.0 * radians(l0)) - sin4l0 = sin(4.0 * radians(l0)) - sin2m = sin(2.0 * radians(m)) - - Etime = y * sin2l0 - 2.0 * e * sinm + 4.0 * e * y * sinm * cos2l0 - \ - 0.5 * y * y * sin4l0 - 1.25 * e * e * sin2m - - return degrees(Etime) * 4.0 - - def _hour_angle(self, latitude, declination, depression): - latitude_rad = radians(latitude) - declination_rad = radians(declination) - depression_rad = radians(depression) - - n = cos(depression_rad) - d = cos(latitude_rad) * cos(declination_rad) - t = tan(latitude_rad) * tan(declination_rad) - h = (n / d) - t - - HA = acos(h) - return HA - - def _calc_time(self, depression, direction, date, latitude, longitude): - julianday = self._julianday(date) - - if latitude > 89.8: - latitude = 89.8 - - if latitude < -89.8: - latitude = -89.8 - - t = self._jday_to_jcentury(julianday) - eqtime = self._eq_of_time(t) - solarDec = self._sun_declination(t) - - hourangle = self._hour_angle(latitude, solarDec, depression) - if direction == SUN_SETTING: - hourangle = -hourangle - - delta = -longitude - degrees(hourangle) - timeDiff = 4.0 * delta - timeUTC = 720.0 + timeDiff - eqtime - - timeUTC = timeUTC / 60.0 - hour = int(timeUTC) - minute = int((timeUTC - hour) * 60) - second = int((((timeUTC - hour) * 60) - minute) * 60) - - if second > 59: - second -= 60 - minute += 1 - elif second < 0: - second += 60 - minute -= 1 - - if minute > 59: - minute -= 60 - hour += 1 - elif minute < 0: - minute += 60 - hour -= 1 - - if hour > 23: - hour -= 24 - date += datetime.timedelta(days=1) - elif hour < 0: - hour += 24 - date -= datetime.timedelta(days=1) - - dt = datetime.datetime(date.year, date.month, date.day, - hour, minute, second) - dt = pytz.UTC.localize(dt) - - return dt +# -*- coding: utf-8 -*- + +# Copyright 2009-2018, Simon Kennedy, sffjunkie+code@gmail.com + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Calculations for the position of the sun and moon. + +The :mod:`astral` module provides the means to calculate dawn, sunrise, +solar noon, sunset, dusk and rahukaalam times, plus solar azimuth and +elevation, for specific locations or at a specific latitude/longitude. It can +also calculate the moon phase for a specific date. + +The module provides 2 main classes :class:`Astral` and :class:`Location`. + +:class:`Astral` + Has 2 main responsibilities + + * Calculates the events in the UTC timezone. + * Provides access to location data + +:class:`Location` + Holds information about a location and provides functions to calculate + the event times for the location in the correct time zone. + +For example :: + + >>> from astral import * + >>> a = Astral() + >>> location = a['London'] + >>> print('Information for %s' % location.name) + Information for London + >>> timezone = location.timezone + >>> print('Timezone: %s' % timezone) + Timezone: Europe/London + >>> print('Latitude: %.02f; Longitude: %.02f' % (location.latitude, + ... location.longitude)) + Latitude: 51.60; Longitude: 0.05 + >>> from datetime import date + >>> d = date(2009,4,22) + >>> sun = location.sun(local=True, date=d) + >>> print('Dawn: %s' % str(sun['dawn'])) + Dawn: 2009-04-22 05:12:56+01:00 + +The module currently provides 2 methods of obtaining location information; +:class:`AstralGeocoder` (the default, which uses information from within the +module) and :class:`GoogleGeocoder` (which obtains information from Google's +Map Service.) + +To use the :class:`GoogleGeocoder` pass the class as the `geocoder` parameter +to :meth:`Astral.__init__` or by setting the `geocoder` property to an +instance of :class:`GoogleGeocoder`:: + + >>> from astral import GoogleGeocoder + >>> a = Astral(GoogleGeocoder) + +or :: + + >>> from astral import GoogleGeocoder + >>> a = Astral() + >>> a.geocoder = GoogleGeocoder() +""" + +from __future__ import unicode_literals, division + +try: + import pytz +except ImportError: + raise ImportError(('The astral module requires the ' + 'pytz module to be available.')) + +try: + import requests +except ImportError: + raise ImportError(('The astral module requires the ' + 'requests module to be available.')) + +import datetime +from time import time +from math import cos, sin, tan, acos, asin, atan2, floor, ceil +from math import radians, degrees, pow +from numbers import Number +import sys + +try: + from urllib import quote_plus +except ImportError: + from urllib.parse import quote_plus + +try: + from urllib2 import URLError +except ImportError: + from urllib.request import URLError + +try: + import simplejson as json +except ImportError: + import json + +if sys.version_info[0] >= 3: + ustr = str +else: + ustr = unicode # pylint: disable=E0602 + +__all__ = ['Astral', 'Location', + 'AstralGeocoder', 'GoogleGeocoder', + 'AstralError'] + +__version__ = "1.6.1" +__author__ = "Simon Kennedy " + +SUN_RISING = 1 +SUN_SETTING = -1 + +# name,region,latitude,longitude,timezone,elevation +_LOCATION_INFO = """Abu Dhabi,UAE,24°28'N,54°22'E,Asia/Dubai,5 +Abu Dhabi,United Arab Emirates,24°28'N,54°22'E,Asia/Dubai,5 +Abuja,Nigeria,09°05'N,07°32'E,Africa/Lagos,342 +Accra,Ghana,05°35'N,00°06'W,Africa/Accra,61 +Addis Ababa,Ethiopia,09°02'N,38°42'E,Africa/Addis_Ababa,2355 +Adelaide,Australia,34°56'S,138°36'E,Australia/Adelaide,50 +Al Jubail,Saudi Arabia,25°24'N,49°39'W,Asia/Riyadh,8 +Algiers,Algeria,36°42'N,03°08'E,Africa/Algiers,224 +Amman,Jordan,31°57'N,35°52'E,Asia/Amman,1100 +Amsterdam,Netherlands,52°23'N,04°54'E,Europe/Amsterdam,2 +Andorra la Vella,Andorra,42°31'N,01°32'E,Europe/Andorra,1023 +Ankara,Turkey,39°57'N,32°54'E,Europe/Istanbul,938 +Antananarivo,Madagascar,18°55'S,47°31'E,Indian/Antananarivo,1276 +Apia,Samoa,13°50'S,171°50'W,Pacific/Apia,2 +Ashgabat,Turkmenistan,38°00'N,57°50'E,Asia/Ashgabat,219 +Asmara,Eritrea,15°19'N,38°55'E,Africa/Asmara,2325 +Astana,Kazakhstan,51°10'N,71°30'E,Asia/Qyzylorda,347 +Asuncion,Paraguay,25°10'S,57°30'W,America/Asuncion,124 +Athens,Greece,37°58'N,23°46'E,Europe/Athens,338 +Avarua,Cook Islands,21°12'N,159°46'W,Etc/GMT-10,208 +Baghdad,Iraq,33°20'N,44°30'E,Asia/Baghdad,41 +Baku,Azerbaijan,40°29'N,49°56'E,Asia/Baku,30 +Bamako,Mali,12°34'N,07°55'W,Africa/Bamako,350 +Bandar Seri Begawan,Brunei Darussalam,04°52'N,115°00'E,Asia/Brunei,1 +Bangkok,Thailand,13°45'N,100°35'E,Asia/Bangkok,2 +Bangui,Central African Republic,04°23'N,18°35'E,Africa/Bangui,373 +Banjul,Gambia,13°28'N,16°40'W,Africa/Banjul,5 +Basse-Terre,Guadeloupe,16°00'N,61°44'W,America/Guadeloupe,1 +Basseterre,Saint Kitts and Nevis,17°17'N,62°43'W,America/St_Kitts,50 +Beijing,China,39°55'N,116°20'E,Asia/Harbin,59 +Beirut,Lebanon,33°53'N,35°31'E,Asia/Beirut,56 +Belfast,Northern Ireland,54°36'N,5°56'W,Europe/Belfast,9 +Belgrade,Yugoslavia,44°50'N,20°37'E,Europe/Belgrade,90 +Belmopan,Belize,17°18'N,88°30'W,America/Belize,63 +Berlin,Germany,52°30'N,13°25'E,Europe/Berlin,35 +Bern,Switzerland,46°57'N,07°28'E,Europe/Zurich,510 +Bishkek,Kyrgyzstan,42°54'N,74°46'E,Asia/Bishkek,772 +Bissau,Guinea-Bissau,11°45'N,15°45'W,Africa/Bissau,0 +Bloemfontein,South Africa,29°12'S,26°07'E,Africa/Johannesburg,1398 +Bogota,Colombia,04°34'N,74°00'W,America/Bogota,2620 +Brasilia,Brazil,15°47'S,47°55'W,Brazil/East,1087 +Bratislava,Slovakia,48°10'N,17°07'E,Europe/Bratislava,132 +Brazzaville,Congo,04°09'S,15°12'E,Africa/Brazzaville,156 +Bridgetown,Barbados,13°05'N,59°30'W,America/Barbados,1 +Brisbane,Australia,27°30'S,153°01'E,Australia/Brisbane,25 +Brussels,Belgium,50°51'N,04°21'E,Europe/Brussels,62 +Bucharest,Romania,44°27'N,26°10'E,Europe/Bucharest,71 +Bucuresti,Romania,44°27'N,26°10'E,Europe/Bucharest,71 +Budapest,Hungary,47°29'N,19°05'E,Europe/Budapest,120 +Buenos Aires,Argentina,34°62'S,58°44'W,America/Buenos_Aires,6 +Bujumbura,Burundi,03°16'S,29°18'E,Africa/Bujumbura,782 +Cairo,Egypt,30°01'N,31°14'E,Africa/Cairo,74 +Canberra,Australia,35°15'S,149°08'E,Australia/Canberra,575 +Cape Town,South Africa,33°55'S,18°22'E,Africa/Johannesburg,1700 +Caracas,Venezuela,10°30'N,66°55'W,America/Caracas,885 +Castries,Saint Lucia,14°02'N,60°58'W,America/St_Lucia,125 +Cayenne,French Guiana,05°05'N,52°18'W,America/Cayenne,9 +Charlotte Amalie,United States of Virgin Islands,18°21'N,64°56'W,America/Virgin,0 +Chisinau,Moldova,47°02'N,28°50'E,Europe/Chisinau,122 +Conakry,Guinea,09°29'N,13°49'W,Africa/Conakry,26 +Copenhagen,Denmark,55°41'N,12°34'E,Europe/Copenhagen,5 +Cotonou,Benin,06°23'N,02°42'E,Africa/Porto-Novo,5 +Dakar,Senegal,14°34'N,17°29'W,Africa/Dakar,24 +Damascus,Syrian Arab Republic,33°30'N,36°18'E,Asia/Damascus,609 +Dammam,Saudi Arabia,26°30'N,50°12'E,Asia/Riyadh,1 +Dhaka,Bangladesh,23°43'N,90°26'E,Asia/Dhaka,8 +Dili,East Timor,08°29'S,125°34'E,Asia/Dili,11 +Djibouti,Djibouti,11°08'N,42°20'E,Africa/Djibouti,19 +Dodoma,United Republic of Tanzania,06°08'S,35°45'E,Africa/Dar_es_Salaam,1119 +Doha,Qatar,25°15'N,51°35'E,Asia/Qatar,10 +Douglas,Isle Of Man,54°9'N,4°29'W,Europe/London,35 +Dublin,Ireland,53°21'N,06°15'W,Europe/Dublin,85 +Dushanbe,Tajikistan,38°33'N,68°48'E,Asia/Dushanbe,803 +El Aaiun,Morocco,27°9'N,13°12'W,UTC,64 +Fort-de-France,Martinique,14°36'N,61°02'W,America/Martinique,9 +Freetown,Sierra Leone,08°30'N,13°17'W,Africa/Freetown,26 +Funafuti,Tuvalu,08°31'S,179°13'E,Pacific/Funafuti,2 +Gaborone,Botswana,24°45'S,25°57'E,Africa/Gaborone,1005 +George Town,Cayman Islands,19°20'N,81°24'W,America/Cayman,3 +Georgetown,Guyana,06°50'N,58°12'W,America/Guyana,30 +Gibraltar,Gibraltar,36°9'N,5°21'W,Europe/Gibraltar,3 +Guatemala,Guatemala,14°40'N,90°22'W,America/Guatemala,1500 +Hanoi,Viet Nam,21°05'N,105°55'E,Asia/Saigon,6 +Harare,Zimbabwe,17°43'S,31°02'E,Africa/Harare,1503 +Havana,Cuba,23°08'N,82°22'W,America/Havana,59 +Helsinki,Finland,60°15'N,25°03'E,Europe/Helsinki,56 +Hobart,Tasmania,42°53'S,147°19'E,Australia/Hobart,4 +Hong Kong,China,22°16'N,114°09'E,Asia/Hong_Kong,8 +Honiara,Solomon Islands,09°27'S,159°57'E,Pacific/Guadalcanal,8 +Islamabad,Pakistan,33°40'N,73°10'E,Asia/Karachi,508 +Jakarta,Indonesia,06°09'S,106°49'E,Asia/Jakarta,6 +Jerusalem,Israel,31°47'N,35°12'E,Asia/Jerusalem,775 +Juba,South Sudan,4°51'N,31°36'E,Africa/Juba,550 +Jubail,Saudi Arabia,27°02'N,49°39'E,Asia/Riyadh,2 +Kabul,Afghanistan,34°28'N,69°11'E,Asia/Kabul,1791 +Kampala,Uganda,00°20'N,32°30'E,Africa/Kampala,1155 +Kathmandu,Nepal,27°45'N,85°20'E,Asia/Kathmandu,1337 +Khartoum,Sudan,15°31'N,32°35'E,Africa/Khartoum,380 +Kiev,Ukraine,50°30'N,30°28'E,Europe/Kiev,153 +Kigali,Rwanda,01°59'S,30°04'E,Africa/Kigali,1497 +Kingston,Jamaica,18°00'N,76°50'W,America/Jamaica,9 +Kingston,Norfolk Island,45°20'S,168°43'E,Pacific/Norfolk,113 +Kingstown,Saint Vincent and the Grenadines,13°10'N,61°10'W,America/St_Vincent,1 +Kinshasa,Democratic Republic of the Congo,04°20'S,15°15'E,Africa/Kinshasa,312 +Koror,Palau,07°20'N,134°28'E,Pacific/Palau,33 +Kuala Lumpur,Malaysia,03°09'N,101°41'E,Asia/Kuala_Lumpur,22 +Kuwait,Kuwait,29°30'N,48°00'E,Asia/Kuwait,55 +La Paz,Bolivia,16°20'S,68°10'W,America/La_Paz,4014 +Libreville,Gabon,00°25'N,09°26'E,Africa/Libreville,15 +Lilongwe,Malawi,14°00'S,33°48'E,Africa/Blantyre,1229 +Lima,Peru,12°00'S,77°00'W,America/Lima,13 +Lisbon,Portugal,38°42'N,09°10'W,Europe/Lisbon,123 +Ljubljana,Slovenia,46°04'N,14°33'E,Europe/Ljubljana,385 +Lome,Togo,06°09'N,01°20'E,Africa/Lome,25 +London,England,51°30'N,00°07'W,Europe/London,24 +Luanda,Angola,08°50'S,13°15'E,Africa/Luanda,6 +Lusaka,Zambia,15°28'S,28°16'E,Africa/Lusaka,1154 +Luxembourg,Luxembourg,49°37'N,06°09'E,Europe/Luxembourg,232 +Macau,Macao,22°12'N,113°33'E,Asia/Macau,6 +Madinah,Saudi Arabia,24°28'N,39°36'E,Asia/Riyadh,631 +Madrid,Spain,40°25'N,03°45'W,Europe/Madrid,582 +Majuro,Marshall Islands,7°4'N,171°16'E,Pacific/Majuro,65 +Makkah,Saudi Arabia,21°26'N,39°49'E,Asia/Riyadh,240 +Malabo,Equatorial Guinea,03°45'N,08°50'E,Africa/Malabo,56 +Male,Maldives,04°00'N,73°28'E,Indian/Maldives,2 +Mamoudzou,Mayotte,12°48'S,45°14'E,Indian/Mayotte,420 +Managua,Nicaragua,12°06'N,86°20'W,America/Managua,50 +Manama,Bahrain,26°10'N,50°30'E,Asia/Bahrain,2 +Manila,Philippines,14°40'N,121°03'E,Asia/Manila,21 +Maputo,Mozambique,25°58'S,32°32'E,Africa/Maputo,44 +Maseru,Lesotho,29°18'S,27°30'E,Africa/Maseru,1628 +Masqat,Oman,23°37'N,58°36'E,Asia/Muscat,8 +Mbabane,Swaziland,26°18'S,31°06'E,Africa/Mbabane,1243 +Mecca,Saudi Arabia,21°26'N,39°49'E,Asia/Riyadh,240 +Medina,Saudi Arabia,24°28'N,39°36'E,Asia/Riyadh,631 +Mexico,Mexico,19°20'N,99°10'W,America/Mexico_City,2254 +Minsk,Belarus,53°52'N,27°30'E,Europe/Minsk,231 +Mogadishu,Somalia,02°02'N,45°25'E,Africa/Mogadishu,9 +Monaco,Priciplality Of Monaco,43°43'N,7°25'E,Europe/Monaco,206 +Monrovia,Liberia,06°18'N,10°47'W,Africa/Monrovia,9 +Montevideo,Uruguay,34°50'S,56°11'W,America/Montevideo,32 +Moroni,Comoros,11°40'S,43°16'E,Indian/Comoro,29 +Moscow,Russian Federation,55°45'N,37°35'E,Europe/Moscow,247 +Moskva,Russian Federation,55°45'N,37°35'E,Europe/Moscow,247 +Mumbai,India,18°58'N,72°49'E,Asia/Kolkata,14 +Muscat,Oman,23°37'N,58°32'E,Asia/Muscat,8 +N'Djamena,Chad,12°10'N,14°59'E,Africa/Ndjamena,295 +Nairobi,Kenya,01°17'S,36°48'E,Africa/Nairobi,1624 +Nassau,Bahamas,25°05'N,77°20'W,America/Nassau,7 +Naypyidaw,Myanmar,19°45'N,96°6'E,Asia/Rangoon,104 +New Delhi,India,28°37'N,77°13'E,Asia/Kolkata,233 +Ngerulmud,Palau,7°30'N,134°37'E,Pacific/Palau,3 +Niamey,Niger,13°27'N,02°06'E,Africa/Niamey,223 +Nicosia,Cyprus,35°10'N,33°25'E,Asia/Nicosia,162 +Nouakchott,Mauritania,20°10'S,57°30'E,Africa/Nouakchott,3 +Noumea,New Caledonia,22°17'S,166°30'E,Pacific/Noumea,69 +Nuku'alofa,Tonga,21°10'S,174°00'W,Pacific/Tongatapu,6 +Nuuk,Greenland,64°10'N,51°35'W,America/Godthab,70 +Oranjestad,Aruba,12°32'N,70°02'W,America/Aruba,33 +Oslo,Norway,59°55'N,10°45'E,Europe/Oslo,170 +Ottawa,Canada,45°27'N,75°42'W,US/Eastern,79 +Ouagadougou,Burkina Faso,12°15'N,01°30'W,Africa/Ouagadougou,316 +P'yongyang,Democratic People's Republic of Korea,39°09'N,125°30'E,Asia/Pyongyang,21 +Pago Pago,American Samoa,14°16'S,170°43'W,Pacific/Pago_Pago,0 +Palikir,Micronesia,06°55'N,158°09'E,Pacific/Ponape,71 +Panama,Panama,09°00'N,79°25'W,America/Panama,2 +Papeete,French Polynesia,17°32'S,149°34'W,Pacific/Tahiti,7 +Paramaribo,Suriname,05°50'N,55°10'W,America/Paramaribo,7 +Paris,France,48°50'N,02°20'E,Europe/Paris,109 +Perth,Australia,31°56'S,115°50'E,Australia/Perth,20 +Phnom Penh,Cambodia,11°33'N,104°55'E,Asia/Phnom_Penh,10 +Podgorica,Montenegro,42°28'N,19°16'E,Europe/Podgorica,53 +Port Louis,Mauritius,20°9'S,57°30'E,Indian/Mauritius,5 +Port Moresby,Papua New Guinea,09°24'S,147°08'E,Pacific/Port_Moresby,44 +Port-Vila,Vanuatu,17°45'S,168°18'E,Pacific/Efate,1 +Port-au-Prince,Haiti,18°40'N,72°20'W,America/Port-au-Prince,34 +Port of Spain,Trinidad and Tobago,10°40'N,61°31'W,America/Port_of_Spain,66 +Porto-Novo,Benin,06°23'N,02°42'E,Africa/Porto-Novo,38 +Prague,Czech Republic,50°05'N,14°22'E,Europe/Prague,365 +Praia,Cape Verde,15°02'N,23°34'W,Atlantic/Cape_Verde,35 +Pretoria,South Africa,25°44'S,28°12'E,Africa/Johannesburg,1322 +Pristina,Albania,42°40'N,21°10'E,Europe/Tirane,576 +Quito,Ecuador,00°15'S,78°35'W,America/Guayaquil,2812 +Rabat,Morocco,34°1'N,6°50'W,Africa/Casablanca,75 +Reykjavik,Iceland,64°10'N,21°57'W,Atlantic/Reykjavik,61 +Riga,Latvia,56°53'N,24°08'E,Europe/Riga,7 +Riyadh,Saudi Arabia,24°41'N,46°42'E,Asia/Riyadh,612 +Road Town,British Virgin Islands,18°27'N,64°37'W,America/Virgin,1 +Rome,Italy,41°54'N,12°29'E,Europe/Rome,95 +Roseau,Dominica,15°20'N,61°24'W,America/Dominica,72 +Saint Helier,Jersey,49°11'N,2°6'W,Etc/GMT,54 +Saint Pierre,Saint Pierre and Miquelon,46°46'N,56°12'W,America/Miquelon,5 +Saipan,Northern Mariana Islands,15°12'N,145°45'E,Pacific/Saipan,200 +Sana,Yemen,15°20'N,44°12'W,Asia/Aden,2199 +Sana'a,Yemen,15°20'N,44°12'W,Asia/Aden,2199 +San Jose,Costa Rica,09°55'N,84°02'W,America/Costa_Rica,931 +San Juan,Puerto Rico,18°28'N,66°07'W,America/Puerto_Rico,21 +San Marino,San Marino,43°55'N,12°30'E,Europe/San_Marino,749 +San Salvador,El Salvador,13°40'N,89°10'W,America/El_Salvador,621 +Santiago,Chile,33°24'S,70°40'W,America/Santiago,476 +Santo Domingo,Dominica Republic,18°30'N,69°59'W,America/Santo_Domingo,14 +Sao Tome,Sao Tome and Principe,00°10'N,06°39'E,Africa/Sao_Tome,13 +Sarajevo,Bosnia and Herzegovina,43°52'N,18°26'E,Europe/Sarajevo,511 +Seoul,Republic of Korea,37°31'N,126°58'E,Asia/Seoul,49 +Singapore,Republic of Singapore,1°18'N,103°48'E,Asia/Singapore,16 +Skopje,The Former Yugoslav Republic of Macedonia,42°01'N,21°26'E,Europe/Skopje,238 +Sofia,Bulgaria,42°45'N,23°20'E,Europe/Sofia,531 +Sri Jayawardenapura Kotte,Sri Lanka,6°54'N,79°53'E,Asia/Colombo,7 +St. George's,Grenada,32°22'N,64°40'W,America/Grenada,7 +St. John's,Antigua and Barbuda,17°7'N,61°51'W,America/Antigua,1 +St. Peter Port,Guernsey,49°26'N,02°33'W,Europe/Guernsey,1 +Stanley,Falkland Islands,51°40'S,59°51'W,Atlantic/Stanley,23 +Stockholm,Sweden,59°20'N,18°05'E,Europe/Stockholm,52 +Sucre,Bolivia,16°20'S,68°10'W,America/La_Paz,2903 +Suva,Fiji,18°06'S,178°30'E,Pacific/Fiji,0 +Sydney,Australia,33°53'S,151°13'E,Australia/Sydney,3 +Taipei,Republic of China (Taiwan),25°02'N,121°38'E,Asia/Taipei,9 +T'bilisi,Georgia,41°43'N,44°50'E,Asia/Tbilisi,467 +Tbilisi,Georgia,41°43'N,44°50'E,Asia/Tbilisi,467 +Tallinn,Estonia,59°22'N,24°48'E,Europe/Tallinn,39 +Tarawa,Kiribati,01°30'N,173°00'E,Pacific/Tarawa,2 +Tashkent,Uzbekistan,41°20'N,69°10'E,Asia/Tashkent,489 +Tegucigalpa,Honduras,14°05'N,87°14'W,America/Tegucigalpa,994 +Tehran,Iran,35°44'N,51°30'E,Asia/Tehran,1191 +Thimphu,Bhutan,27°31'N,89°45'E,Asia/Thimphu,2300 +Tirana,Albania,41°18'N,19°49'E,Europe/Tirane,90 +Tirane,Albania,41°18'N,19°49'E,Europe/Tirane,90 +Torshavn,Faroe Islands,62°05'N,06°56'W,Atlantic/Faroe,39 +Tokyo,Japan,35°41'N,139°41'E,Asia/Tokyo,8 +Tripoli,Libyan Arab Jamahiriya,32°49'N,13°07'E,Africa/Tripoli,81 +Tunis,Tunisia,36°50'N,10°11'E,Africa/Tunis,4 +Ulan Bator,Mongolia,47°55'N,106°55'E,Asia/Ulaanbaatar,1330 +Ulaanbaatar,Mongolia,47°55'N,106°55'E,Asia/Ulaanbaatar,1330 +Vaduz,Liechtenstein,47°08'N,09°31'E,Europe/Vaduz,463 +Valletta,Malta,35°54'N,14°31'E,Europe/Malta,48 +Vienna,Austria,48°12'N,16°22'E,Europe/Vienna,171 +Vientiane,Lao People's Democratic Republic,17°58'N,102°36'E,Asia/Vientiane,171 +Vilnius,Lithuania,54°38'N,25°19'E,Europe/Vilnius,156 +W. Indies,Antigua and Barbuda,17°20'N,61°48'W,America/Antigua,0 +Warsaw,Poland,52°13'N,21°00'E,Europe/Warsaw,107 +Washington DC,USA,39°91'N,77°02'W,US/Eastern,23 +Wellington,New Zealand,41°19'S,174°46'E,Pacific/Auckland,7 +Willemstad,Netherlands Antilles,12°05'N,69°00'W,America/Curacao,1 +Windhoek,Namibia,22°35'S,17°04'E,Africa/Windhoek,1725 +Yamoussoukro,Cote d'Ivoire,06°49'N,05°17'W,Africa/Abidjan,213 +Yangon,Myanmar,16°45'N,96°20'E,Asia/Rangoon,33 +Yaounde,Cameroon,03°50'N,11°35'E,Africa/Douala,760 +Yaren,Nauru,0°32'S,166°55'E,Pacific/Nauru,0 +Yerevan,Armenia,40°10'N,44°31'E,Asia/Yerevan,890 +Zagreb,Croatia,45°50'N,15°58'E,Europe/Zagreb,123 + +# UK Cities +Aberdeen,Scotland,57°08'N,02°06'W,Europe/London,65 +Birmingham,England,52°30'N,01°50'W,Europe/London,99 +Bolton,England,53°35'N,02°15'W,Europe/London,105 +Bradford,England,53°47'N,01°45'W,Europe/London,127 +Bristol,England,51°28'N,02°35'W,Europe/London,11 +Cardiff,Wales,51°29'N,03°13'W,Europe/London,9 +Crawley,England,51°8'N,00°10'W,Europe/London,77 +Edinburgh,Scotland,55°57'N,03°13'W,Europe/London,61 +Glasgow,Scotland,55°50'N,04°15'W,Europe/London,8 +Greenwich,England,51°28'N,00°00'W,Europe/London,24 +Leeds,England,53°48'N,01°35'W,Europe/London,47 +Leicester,England,52°38'N,01°08'W,Europe/London,138 +Liverpool,England,53°25'N,03°00'W,Europe/London,25 +Manchester,England,53°30'N,02°15'W,Europe/London,78 +Newcastle Upon Time,England,54°59'N,01°36'W,Europe/London,47 +Newcastle,England,54°59'N,01°36'W,Europe/London,47 +Norwich,England,52°38'N,01°18'E,Europe/London,18 +Oxford,England,51°45'N,01°15'W,Europe/London,72 +Plymouth,England,50°25'N,04°15'W,Europe/London,50 +Portsmouth,England,50°48'N,01°05'W,Europe/London,9 +Reading,England,51°27'N,0°58'W,Europe/London,84 +Sheffield,England,53°23'N,01°28'W,Europe/London,105 +Southampton,England,50°55'N,01°25'W,Europe/London,9 +Swansea,England,51°37'N,03°57'W,Europe/London,91 +Swindon,England,51°34'N,01°47'W,Europe/London,112 +Wolverhampton,England,52°35'N,2°08'W,Europe/London,89 +Barrow-In-Furness,England,54°06'N,3°13'W,Europe/London,20 + +# US State Capitals +Montgomery,USA,32°21'N,86°16'W,US/Central,42 +Juneau,USA,58°23'N,134°11'W,US/Alaska,29 +Phoenix,USA,33°26'N,112°04'W,America/Phoenix,331 +Little Rock,USA,34°44'N,92°19'W,US/Central,95 +Sacramento,USA,38°33'N,121°28'W,US/Pacific,15 +Denver,USA,39°44'N,104°59'W,US/Mountain,1600 +Hartford,USA,41°45'N,72°41'W,US/Eastern,9 +Dover,USA,39°09'N,75°31'W,US/Eastern,8 +Tallahassee,USA,30°27'N,84°16'W,US/Eastern,59 +Atlanta,USA,33°45'N,84°23'W,US/Eastern,267 +Honolulu,USA,21°18'N,157°49'W,US/Hawaii,229 +Boise,USA,43°36'N,116°12'W,US/Mountain,808 +Springfield,USA,39°47'N,89°39'W,US/Central,190 +Indianapolis,USA,39°46'N,86°9'W,US/Eastern,238 +Des Moines,USA,41°35'N,93°37'W,US/Central,276 +Topeka,USA,39°03'N,95°41'W,US/Central,289 +Frankfort,USA,38°11'N,84°51'W,US/Eastern,243 +Baton Rouge,USA,30°27'N,91°8'W,US/Central,15 +Augusta,USA,44°18'N,69°46'W,US/Eastern,41 +Annapolis,USA,38°58'N,76°30'W,US/Eastern,0 +Boston,USA,42°21'N,71°03'W,US/Eastern,6 +Lansing,USA,42°44'N,84°32'W,US/Eastern,271 +Saint Paul,USA,44°56'N,93°05'W,US/Central,256 +Jackson,USA,32°17'N,90°11'W,US/Central,90 +Jefferson City,USA,38°34'N,92°10'W,US/Central,167 +Helena,USA,46°35'N,112°1'W,US/Mountain,1150 +Lincoln,USA,40°48'N,96°40'W,US/Central,384 +Carson City,USA,39°9'N,119°45'W,US/Pacific,1432 +Concord,USA,43°12'N,71°32'W,US/Eastern,117 +Trenton,USA,40°13'N,74°45'W,US/Eastern,28 +Santa Fe,USA,35°40'N,105°57'W,US/Mountain,2151 +Albany,USA,42°39'N,73°46'W,US/Eastern,17 +Raleigh,USA,35°49'N,78°38'W,US/Eastern,90 +Bismarck,USA,46°48'N,100°46'W,US/Central,541 +Columbus,USA,39°59'N,82°59'W,US/Eastern,271 +Oklahoma City,USA,35°28'N,97°32'W,US/Central,384 +Salem,USA,44°55'N,123°1'W,US/Pacific,70 +Harrisburg,USA,40°16'N,76°52'W,US/Eastern,112 +Providence,USA,41°49'N,71°25'W,US/Eastern,2 +Columbia,USA,34°00'N,81°02'W,US/Eastern,96 +Pierre,USA,44°22'N,100°20'W,US/Central,543 +Nashville,USA,36°10'N,86°47'W,US/Central,149 +Austin,USA,30°16'N,97°45'W,US/Central,167 +Salt Lake City,USA,40°45'N,111°53'W,US/Mountain,1294 +Montpelier,USA,44°15'N,72°34'W,US/Eastern,325 +Richmond,USA,37°32'N,77°25'W,US/Eastern,68 +Olympia,USA,47°2'N,122°53'W,US/Pacific,35 +Charleston,USA,38°20'N,81°38'W,US/Eastern,11 +Madison,USA,43°4'N,89°24'W,US/Central,281 +Cheyenne,USA,41°8'N,104°48'W,US/Mountain,1860 + +# Major US Cities +Birmingham,USA,33°39'N,86°48'W,US/Central,197 +Anchorage,USA,61°13'N,149°53'W,US/Alaska,30 +Los Angeles,USA,34°03'N,118°15'W,US/Pacific,50 +San Francisco,USA,37°46'N,122°25'W,US/Pacific,47 +Bridgeport,USA,41°11'N,73°11'W,US/Eastern,13 +Wilmington,USA,39°44'N,75°32'W,US/Eastern,15 +Jacksonville,USA,30°19'N,81°39'W,US/Eastern,13 +Miami,USA,26°8'N,80°12'W,US/Eastern,10 +Chicago,USA,41°50'N,87°41'W,US/Central,189 +Wichita,USA,37°41'N,97°20'W,US/Central,399 +Louisville,USA,38°15'N,85°45'W,US/Eastern,142 +New Orleans,USA,29°57'N,90°4'W,US/Central,10 +Portland,USA,43°39'N,70°16'W,US/Eastern,6 +Baltimore,USA,39°17'N,76°37'W,US/Eastern,31 +Detroit,USA,42°19'N,83°2'W,US/Eastern,189 +Minneapolis,USA,44°58'N,93°15'W,US/Central,260 +Kansas City,USA,39°06'N,94°35'W,US/Central,256 +Billings,USA,45°47'N,108°32'W,US/Mountain,946 +Omaha,USA,41°15'N,96°0'W,US/Central,299 +Las Vegas,USA,36°10'N,115°08'W,US/Pacific,720 +Manchester,USA,42°59'N,71°27'W,US/Eastern,56 +Newark,USA,40°44'N,74°11'W,US/Eastern,4 +Albuquerque,USA,35°06'N,106°36'W,US/Mountain,1523 +New York,USA,40°43'N,74°0'W,US/Eastern,17 +Charlotte,USA,35°13'N,80°50'W,US/Eastern,217 +Fargo,USA,46°52'N,96°47'W,US/Central,271 +Cleveland,USA,41°28'N,81°40'W,US/Eastern,210 +Philadelphia,USA,39°57'N,75°10'W,US/Eastern,62 +Sioux Falls,USA,43°32'N,96°43'W,US/Central,443 +Memphis,USA,35°07'N,89°58'W,US/Central,84 +Houston,USA,29°45'N,95°22'W,US/Central,8 +Dallas,USA,32°47'N,96°48'W,US/Central,137 +Burlington,USA,44°28'N,73°9'W,US/Eastern,35 +Virginia Beach,USA,36°50'N,76°05'W,US/Eastern,9 +Seattle,USA,47°36'N,122°19'W,US/Pacific,63 +Milwaukee,USA,43°03'N,87°57'W,US/Central,188 +San Diego,USA,32°42'N,117°09'W,US/Pacific,16 +Orlando,USA,28°32'N,81°22'W,US/Eastern,35 +Buffalo,USA,42°54'N,78°50'W,US/Eastern,188 +Toledo,USA,41°39'N,83°34'W,US/Eastern,180 + +# Canadian cities +Vancouver,Canada,49°15'N,123°6'W,America/Vancouver,55 +Calgary,Canada,51°2'N,114°3'W,America/Edmonton,1040 +Edmonton,Canada,53°32'N,113°29'W,America/Edmonton,664 +Saskatoon,Canada,52°8'N,106°40'W,America/Regina,480 +Regina,Canada,50°27'N,104°36'W,America/Regina,577 +Winnipeg,Canada,49°53'N,97°8'W,America/Winnipeg,229 +Toronto,Canada,43°39'N,79°22'W,America/Toronto,77 +Montreal,Canada,45°30'N,73°33'W,America/Montreal,23 +Quebec,Canada,46°48'N,71°14'W,America/Toronto,87 +Fredericton,Canada,45°57'N,66°38'W,America/Halifax,8 +Halifax,Canada,44°38'N,63°34'W,America/Halifax,36 +Charlottetown,Canada,46°14'N,63°7'W,America/Halifax,2 +St. John's,Canada,47°33'N,52°42'W,America/Halifax,116 +Whitehorse,Canada,60°43'N,135°3'W,America/Whitehorse,696 +Yellowknife,Canada,62°27'N,114°22'W,America/Yellowknife,191 +Iqaluit,Canada,63°44'N,68°31'W,America/Iqaluit,3 +""" + + +class AstralError(Exception): + """Astral base exception class""" + + +def excel_datediff(start_date, end_date): + """Return the same number of days between 2 dates as Excel does""" + return end_date.toordinal() - start_date.toordinal() + 2 + + +class Location(object): + """Provides access to information for single location.""" + + def __init__(self, info=None): + """Initializes the object with a tuple of information. + + :param info: A tuple of information to fill in the location info. + + The tuple should contain items in the following order + + ================ ============= + Field Default + ================ ============= + name Greenwich + region England + latitude 51.168 + longitude 0 + time zone name Europe/London + elevation 24 + ================ ============= + + See :attr:`timezone` property for a method of obtaining time zone + names + """ + + self.astral = None + if info is None: + self.name = 'Greenwich' + self.region = 'England' + self._latitude = 51.168 + self._longitude = 0.0 + self._timezone_group = 'Europe' + self._timezone_location = 'London' + self._elevation = 24 + else: + self.name = '' + self.region = '' + self._latitude = 0.0 + self._longitude = 0.0 + self._timezone_group = '' + self._timezone_location = '' + self._elevation = 0 + + try: + self.name = info[0] + self.region = info[1] + self.latitude = info[2] + self.longitude = info[3] + self.timezone = info[4] + self.elevation = info[5] + except IndexError: + pass + + self.url = '' + + def __repr__(self): + if self.region: + _repr = '%s/%s' % (self.name, self.region) + else: + _repr = self.name + repr_format = '%s, tz=%s, lat=%0.02f, lon=%0.02f' + return repr_format % (_repr, + self.timezone, + self.latitude, self.longitude) + + @property + def latitude(self): + """The location's latitude + + ``latitude`` can be set either as a string or as a number + + For strings they must be of the form + + degrees°minutes'[N|S] e.g. 51°31'N + + For numbers, positive numbers signify latitudes to the North. + """ + + return self._latitude + + @latitude.setter + def latitude(self, latitude): + if isinstance(latitude, str) or isinstance(latitude, ustr): + (deg, rest) = latitude.split("°", 1) + (minute, rest) = rest.split("'", 1) + + self._latitude = float(deg) + (float(minute) / 60) + + if latitude.endswith("S"): + self._latitude = -self._latitude + else: + self._latitude = float(latitude) + + @property + def longitude(self): + """The location's longitude. + + ``longitude`` can be set either as a string or as a number + + For strings they must be of the form + + degrees°minutes'[E|W] e.g. 51°31'W + + For numbers, positive numbers signify longitudes to the East. + """ + + return self._longitude + + @longitude.setter + def longitude(self, longitude): + if isinstance(longitude, str) or isinstance(longitude, ustr): + (deg, rest) = longitude.split("°", 1) + (minute, rest) = rest.split("'", 1) + + self._longitude = float(deg) + (float(minute) / 60) + + if longitude.endswith("W"): + self._longitude = -self._longitude + else: + self._longitude = float(longitude) + + @property + def elevation(self): + """The elevation in metres above sea level.""" + + return self._elevation + + @elevation.setter + def elevation(self, elevation): + self._elevation = int(elevation) + + @property + def timezone(self): + """The name of the time zone for the location. + + A list of time zone names can be obtained from pytz. For example. + + >>> from pytz import all_timezones + >>> for timezone in all_timezones: + ... print(timezone) + """ + + if self._timezone_location != '': + return '%s/%s' % (self._timezone_group, + self._timezone_location) + else: + return self._timezone_group + + @timezone.setter + def timezone(self, name): + if name not in pytz.all_timezones: + raise ValueError('Timezone \'%s\' not recognized' % name) + + try: + self._timezone_group, self._timezone_location = \ + name.split('/', 1) + except ValueError: + self._timezone_group = name + self._timezone_location = '' + + @property + def tz(self): + """Time zone information.""" + + try: + tz = pytz.timezone(self.timezone) + return tz + except pytz.UnknownTimeZoneError: + raise AstralError('Unknown timezone \'%s\'' % self.timezone) + + tzinfo = tz + + @property + def solar_depression(self): + """The number of degrees the sun must be below the horizon for the + dawn/dusk calculation. + + Can either be set as a number of degrees below the horizon or as + one of the following strings + + ============= ======= + String Degrees + ============= ======= + civil 6.0 + nautical 12.0 + astronomical 18.0 + ============= ======= + """ + + return self.astral.solar_depression + + @solar_depression.setter + def solar_depression(self, depression): + if self.astral is None: + self.astral = Astral() + + self.astral.solar_depression = depression + + def sun(self, date=None, local=True): + """Returns dawn, sunrise, noon, sunset and dusk as a dictionary. + + :param date: The date for which to calculate the times. + If no date is specified then the current date will be used. + + :param local: True = Time to be returned in location's time zone; + False = Time to be returned in UTC. + If not specified then the time will be returned in local time + + :returns: Dictionary with keys ``dawn``, ``sunrise``, ``noon``, + ``sunset`` and ``dusk`` whose values are the results of the + corresponding methods. + :rtype: dict + """ + + if self.astral is None: + self.astral = Astral() + + if date is None: + date = datetime.date.today() + + sun = self.astral.sun_utc(date, self.latitude, self.longitude) + + if local: + for key, dt in sun.items(): + sun[key] = dt.astimezone(self.tz) + + return sun + + def dawn(self, date=None, local=True): + """Calculates the time in the morning when the sun is a certain number + of degrees below the horizon. By default this is 6 degrees but can be + changed by setting the :attr:`Astral.solar_depression` property. + + :param date: The date for which to calculate the dawn time. + If no date is specified then the current date will be used. + + :param local: True = Time to be returned in location's time zone; + False = Time to be returned in UTC. + If not specified then the time will be returned in local time + + :returns: The date and time at which dawn occurs. + :rtype: :class:`~datetime.datetime` + """ + + if self.astral is None: + self.astral = Astral() + + if date is None: + date = datetime.date.today() + + dawn = self.astral.dawn_utc(date, self.latitude, self.longitude) + + if local: + return dawn.astimezone(self.tz) + else: + return dawn + + def sunrise(self, date=None, local=True): + """Return sunrise time. + + Calculates the time in the morning when the sun is a 0.833 degrees + below the horizon. This is to account for refraction. + + :param date: The date for which to calculate the sunrise time. + If no date is specified then the current date will be used. + + :param local: True = Time to be returned in location's time zone; + False = Time to be returned in UTC. + If not specified then the time will be returned in local time + + :returns: The date and time at which sunrise occurs. + :rtype: :class:`~datetime.datetime` + """ + + if self.astral is None: + self.astral = Astral() + + if date is None: + date = datetime.date.today() + + sunrise = self.astral.sunrise_utc(date, self.latitude, self.longitude) + + if local: + return sunrise.astimezone(self.tz) + else: + return sunrise + + def solar_noon(self, date=None, local=True): + """Calculates the solar noon (the time when the sun is at its highest + point.) + + :param date: The date for which to calculate the noon time. + If no date is specified then the current date will be used. + + :param local: True = Time to be returned in location's time zone; + False = Time to be returned in UTC. + If not specified then the time will be returned in local time + + :returns: The date and time at which the solar noon occurs. + :rtype: :class:`~datetime.datetime` + """ + + if self.astral is None: + self.astral = Astral() + + if date is None: + date = datetime.date.today() + + noon = self.astral.solar_noon_utc(date, self.longitude) + + if local: + return noon.astimezone(self.tz) + else: + return noon + + def sunset(self, date=None, local=True): + """Calculates sunset time (the time in the evening when the sun is a + 0.833 degrees below the horizon. This is to account for refraction.) + + :param date: The date for which to calculate the sunset time. + If no date is specified then the current date will be used. + + :param local: True = Time to be returned in location's time zone; + False = Time to be returned in UTC. + If not specified then the time will be returned in local time + + :returns: The date and time at which sunset occurs. + :rtype: :class:`~datetime.datetime` + """ + + if self.astral is None: + self.astral = Astral() + + if date is None: + date = datetime.date.today() + + sunset = self.astral.sunset_utc(date, self.latitude, self.longitude) + + if local: + return sunset.astimezone(self.tz) + else: + return sunset + + def dusk(self, date=None, local=True): + """Calculates the dusk time (the time in the evening when the sun is a + certain number of degrees below the horizon. By default this is 6 + degrees but can be changed by setting the + :attr:`solar_depression` property.) + + :param date: The date for which to calculate the dusk time. + If no date is specified then the current date will be used. + + :param local: True = Time to be returned in location's time zone; + False = Time to be returned in UTC. + If not specified then the time will be returned in local time + + :returns: The date and time at which dusk occurs. + :rtype: :class:`~datetime.datetime` + """ + + if self.astral is None: + self.astral = Astral() + + if date is None: + date = datetime.date.today() + + dusk = self.astral.dusk_utc(date, self.latitude, self.longitude) + + if local: + return dusk.astimezone(self.tz) + else: + return dusk + + def solar_midnight(self, date=None, local=True): + """Calculates the solar midnight (the time when the sun is at its lowest + point.) + + :param date: The date for which to calculate the midnight time. + If no date is specified then the current date will be used. + + :param local: True = Time to be returned in location's time zone; + False = Time to be returned in UTC. + If not specified then the time will be returned in local time + + :returns: The date and time at which the solar midnight occurs. + :rtype: :class:`~datetime.datetime` + """ + + if self.astral is None: + self.astral = Astral() + + if date is None: + date = datetime.date.today() + + midnight = self.astral.solar_midnight_utc(date, self.longitude) + + if local: + return midnight.astimezone(self.tz) + else: + return midnight + + def daylight(self, date=None, local=True): + """Calculates the daylight time (the time between sunrise and sunset) + + :param date: The date for which to calculate daylight. + If no date is specified then the current date will be used. + + :param local: True = Time to be returned in location's time zone; + False = Time to be returned in UTC. + If not specified then the time will be returned in local time + + :returns: A tuple containing the start and end times + :rtype: tuple(:class:`~datetime.datetime`, :class:`~datetime.datetime`) + """ + + if self.astral is None: + self.astral = Astral() + + if date is None: + date = datetime.date.today() + + start, end = self.astral.daylight_utc(date, self.latitude, self.longitude) + + if local: + return start.astimezone(self.tz), end.astimezone(self.tz) + else: + return start, end + + def night(self, date=None, local=True): + """Calculates the night time (the time between astronomical dusk and + astronomical dawn of the next day) + + :param date: The date for which to calculate the start of the night time. + If no date is specified then the current date will be used. + + :param local: True = Time to be returned in location's time zone; + False = Time to be returned in UTC. + If not specified then the time will be returned in local time + + :returns: A tuple containing the start and end times + :rtype: tuple(:class:`~datetime.datetime`, :class:`~datetime.datetime`) + """ + + if self.astral is None: + self.astral = Astral() + + if date is None: + date = datetime.date.today() + + start, end = self.astral.night_utc(date, self.latitude, self.longitude) + + if local: + return start.astimezone(self.tz), end.astimezone(self.tz) + else: + return start, end + + def twilight(self, direction=SUN_RISING, date=None, local=True): + """Returns the start and end times of Twilight in the UTC timezone when + the sun is traversing in the specified direction. + + This method defines twilight as being between the time + when the sun is at -6 degrees and sunrise/sunset. + + :param direction: Determines whether the time is for the sun rising or setting. + Use ``astral.SUN_RISING`` or ``astral.SUN_SETTING``. + :type direction: int + :param date: The date for which to calculate the times. + :type date: :class:`datetime.date` + :param local: True = Time to be returned in location's time zone; + False = Time to be returned in UTC. + If not specified then the time will be returned in local time + + :return: A tuple of the UTC date and time at which twilight starts and ends. + :rtype: (:class:`~datetime.datetime`, :class:`~datetime.datetime`) + """ + + if date is None: + date = datetime.date.today() + + start, end = self.astral.twilight_utc(direction, date, + self.latitude, self.longitude) + + if local: + return start.astimezone(self.tz), end.astimezone(self.tz) + else: + return start, end + + def time_at_elevation(self, elevation, direction=SUN_RISING, date=None, local=True): + """Calculate the time when the sun is at the specified elevation. + + Note: + This method uses positive elevations for those above the horizon. + + Elevations greater than 90 degrees are converted to a setting sun + i.e. an elevation of 110 will calculate a setting sun at 70 degrees. + + :param elevation: Elevation in degrees above the horizon to calculate for. + :type elevation: float + :param direction: Determines whether the time is for the sun rising or setting. + Use ``astral.SUN_RISING`` or ``astral.SUN_SETTING``. Default is rising. + :type direction: int + :param date: The date for which to calculate the elevation time. + If no date is specified then the current date will be used. + + :param local: True = Time to be returned in location's time zone; + False = Time to be returned in UTC. + If not specified then the time will be returned in local time + + :returns: The date and time at which dusk occurs. + :rtype: :class:`~datetime.datetime` + """ + + if self.astral is None: + self.astral = Astral() + + if date is None: + date = datetime.date.today() + + if elevation > 90.0: + elevation = 180.0 - elevation + direction = SUN_SETTING + + time_ = self.astral.time_at_elevation_utc(elevation, direction, + date, self.latitude, self.longitude) + + if local: + return time_.astimezone(self.tz) + else: + return time_ + + def rahukaalam(self, date=None, local=True): + """Calculates the period of rahukaalam. + + :param date: The date for which to calculate the rahukaalam period. + A value of ``None`` uses the current date. + + :param local: True = Time to be returned in location's time zone; + False = Time to be returned in UTC. + + :return: Tuple containing the start and end times for Rahukaalam. + :rtype: tuple + """ + + if self.astral is None: + self.astral = Astral() + + if date is None: + date = datetime.date.today() + + rahukaalam = self.astral.rahukaalam_utc(date, + self.latitude, self.longitude) + + if local: + rahukaalam = (rahukaalam[0].astimezone(self.tz), + rahukaalam[1].astimezone(self.tz)) + + return rahukaalam + + def golden_hour(self, direction=SUN_RISING, date=None, local=True): + """Returns the start and end times of the Golden Hour when the sun is traversing + in the specified direction. + + This method uses the definition from PhotoPills i.e. the + golden hour is when the sun is between 4 degrees below the horizon + and 6 degrees above. + + :param direction: Determines whether the time is for the sun rising or setting. + Use ``astral.SUN_RISING`` or ``astral.SUN_SETTING``. Default is rising. + :type direction: int + :param date: The date for which to calculate the times. + :type date: :class:`datetime.date` + :param local: True = Times to be returned in location's time zone; + False = Times to be returned in UTC. + If not specified then the time will be returned in local time + + :return: A tuple of the date and time at which the Golden Hour starts and ends. + :rtype: (:class:`~datetime.datetime`, :class:`~datetime.datetime`) + """ + + if self.astral is None: + self.astral = Astral() + + if date is None: + date = datetime.date.today() + + start, end = self.astral.golden_hour_utc(direction, date, + self.latitude, self.longitude) + + if local: + start = start.astimezone(self.tz) + end = end.astimezone(self.tz) + + return start, end + + def blue_hour(self, direction=SUN_RISING, date=None, local=True): + """Returns the start and end times of the Blue Hour when the sun is traversing + in the specified direction. + + This method uses the definition from PhotoPills i.e. the + blue hour is when the sun is between 6 and 4 degrees below the horizon. + + :param direction: Determines whether the time is for the sun rising or setting. + Use ``astral.SUN_RISING`` or ``astral.SUN_SETTING``. Default is rising. + :type direction: int + :param date: The date for which to calculate the times. + If no date is specified then the current date will be used. + + :param local: True = Times to be returned in location's time zone; + False = Times to be returned in UTC. + If not specified then the time will be returned in local time + + :return: A tuple of the date and time at which the Blue Hour starts and ends. + :rtype: (:class:`~datetime.datetime`, :class:`~datetime.datetime`) + """ + + if self.astral is None: + self.astral = Astral() + + if date is None: + date = datetime.date.today() + + start, end = self.astral.blue_hour_utc(direction, date, + self.latitude, self.longitude) + + if local: + start = start.astimezone(self.tz) + end = end.astimezone(self.tz) + + return start, end + + def solar_azimuth(self, dateandtime=None): + """Calculates the solar azimuth angle for a specific date/time. + + :param dateandtime: The date and time for which to calculate the angle. + :type dateandtime: :class:`~datetime.datetime` + + :returns: The azimuth angle in degrees clockwise from North. + :rtype: float + """ + + if self.astral is None: + self.astral = Astral() + + if dateandtime is None: + dateandtime = datetime.datetime.now(self.tz) + elif not dateandtime.tzinfo: + dateandtime = self.tz.localize(dateandtime) + + dateandtime = dateandtime.astimezone(pytz.UTC) + + return self.astral.solar_azimuth(dateandtime, + self.latitude, self.longitude) + + def solar_elevation(self, dateandtime=None): + """Calculates the solar elevation angle for a specific time. + + :param dateandtime: The date and time for which to calculate the angle. + :type dateandtime: :class:`~datetime.datetime` + + :returns: The elevation angle in degrees above the horizon. + :rtype: float + """ + + if self.astral is None: + self.astral = Astral() + + if dateandtime is None: + dateandtime = datetime.datetime.now(self.tz) + elif not dateandtime.tzinfo: + dateandtime = self.tz.localize(dateandtime) + + dateandtime = dateandtime.astimezone(pytz.UTC) + + return self.astral.solar_elevation(dateandtime, + self.latitude, self.longitude) + + def solar_zenith(self, dateandtime=None): + """Calculates the solar zenith angle for a specific time. + + :param dateandtime: The date and time for which to calculate the angle. + :type dateandtime: :class:`~datetime.datetime` + + :returns: The zenith angle in degrees from vertical. + :rtype: float + """ + + return 90.0 - self.solar_elevation(dateandtime) + + def moon_phase(self, date=None, rtype=int): + """Calculates the moon phase for a specific date. + + :param date: The date to calculate the phase for. + If ommitted the current date is used. + :type date: :class:`datetime.date` + + :returns: + A number designating the phase + + | 0 = New moon + | 7 = First quarter + | 14 = Full moon + | 21 = Last quarter + """ + + if self.astral is None: + self.astral = Astral() + + if date is None: + date = datetime.date.today() + + return self.astral.moon_phase(date, rtype) + + +class LocationGroup(object): + """Groups a set of timezones by the timezone group""" + + def __init__(self, name): + self.name = name + self._locations = {} + + def __getitem__(self, key): + """Returns a Location object for the specified `key`. + + group = astral.europe + location = group['London'] + + You can supply an optional region name by adding a comma + followed by the region name. Where multiple locations have the + same name you may need to supply the region name otherwise + the first result will be returned which may not be the one + you're looking for. + + location = group['Abu Dhabi,United Arab Emirates'] + + Handles location names with spaces and mixed case. + """ + + key = self._sanitize_key(key) + + try: + lookup_name, lookup_region = key.split(',', 1) + except ValueError: + lookup_name = key + lookup_region = '' + + lookup_name = lookup_name.strip('"\'') + lookup_region = lookup_region.strip('"\'') + + for (location_name, location_list) in self._locations.items(): + if location_name == lookup_name: + if lookup_region == '': + return location_list[0] + + for location in location_list: + if self._sanitize_key(location.region) == lookup_region: + return location + + raise KeyError('Unrecognised location name - %s' % key) + + def __setitem__(self, key, value): + key = self._sanitize_key(key) + if key not in self._locations: + self._locations[key] = [value] + else: + self._locations[key].append(value) + + def __contains__(self, key): + key = self._sanitize_key(key) + for name in self._locations.keys(): + if name == key: + return True + + return False + + def __iter__(self): + for location_list in self._locations.values(): + for location in location_list: + yield location + + def keys(self): + return self._locations.keys() + + def values(self): + return self._locations.values() + + def items(self): + return self._locations.items() + + @property + def locations(self): + k = [] + for location_list in self._locations.values(): + for location in location_list: + k.append(location.name) + + return k + + def _sanitize_key(self, key): + return str(key).lower().replace(' ', '_') + + +class AstralGeocoder(object): + """Looks up geographic information from the locations stored within the + module + """ + + def __init__(self): + self._groups = {} + + locations = _LOCATION_INFO.split('\n') + for line in locations: + line = line.strip() + if line != '' and line[0] != '#': + if line[-1] == '\n': + line = line[:-1] + + info = line.split(',') + + location = Location(info) + + key = location._timezone_group.lower() + try: + group = self.__getattr__(key) + except AttributeError: + group = LocationGroup(location._timezone_group) + self._groups[key] = group + + group[info[0].lower()] = location + + def __getattr__(self, key): + """Access to each timezone group. For example London is in timezone + group Europe. + + Attribute lookup is case insensitive""" + + key = str(key).lower() + for name, value in self._groups.items(): + if name == key: + return value + + raise AttributeError('Group \'%s\' not found' % key) + + def __getitem__(self, key): + """Lookup a location within all timezone groups. + + Item lookup is case insensitive.""" + + key = str(key).lower() + for group in self._groups.values(): + try: + return group[key] + except KeyError: + pass + + raise KeyError('Unrecognised location name - %s' % key) + + def __iter__(self): + return self._groups.__iter__() + + def __contains__(self, key): + key = str(key).lower() + for name, group in self._groups.items(): + if name == key: + return True + + if key in group: + return True + + return False + + @property + def locations(self): + k = [] + for group in self._groups.values(): + k.extend(group.locations) + + return k + + @property + def groups(self): + return self._groups + + +class GoogleGeocoder(object): + """Use Google Maps API Web Service to lookup GPS co-ordinates, timezone and + elevation. + + See the following for more info. + https://developers.google.com/maps/documentation/ + """ + + def __init__(self, cache=False, api_key=''): + self.cache = cache + self.api_key = api_key + self.geocache = {} + self._location_query_base = ('https://maps.googleapis.com/maps/api/geocode/json' + '?address=%s&sensor=false') + self._timezone_query_base = ('https://maps.googleapis.com/maps/api/timezone/json?' + 'location=%f,%f×tamp=%d&sensor=false') + self._elevation_query_base = ('https://maps.googleapis.com/maps/api/elevation/json?' + 'locations=%f,%f&sensor=false') + + def __getitem__(self, key): + if self.cache and key in self.geocache: + return self.geocache[key] + + location = Location() + try: + self._get_geocoding(key, location) + self._get_timezone(location) + self._get_elevation(location) + except URLError: + raise AstralError(('GoogleGeocoder: Unable to contact ' + 'Google maps API')) + + url = 'https://maps.google.com/maps?q=loc:%f,%f' + location.url = url % (location.latitude, location.longitude) + + if self.cache: + self.geocache[key] = location + + return location + + def _get_geocoding(self, key, location): + """Lookup the Google geocoding API information for `key`""" + + url = self._location_query_base % quote_plus(key) + if self.api_key: + url += '&key=%s' % self.api_key + data = self._read_from_url(url) + response = json.loads(data) + if response['status'] == 'OK': + formatted_address = response['results'][0]['formatted_address'] + pos = formatted_address.find(',') + if pos == -1: + location.name = formatted_address + location.region = '' + else: + location.name = formatted_address[:pos].strip() + location.region = formatted_address[pos + 1:].strip() + + geo_location = response['results'][0]['geometry']['location'] + location.latitude = float(geo_location['lat']) + location.longitude = float(geo_location['lng']) + else: + raise AstralError('GoogleGeocoder: Unable to locate %s' % key) + + def _get_timezone(self, location): + """Query the timezone information with the latitude and longitude of + the specified `location`. + + This function assumes the timezone of the location has always been + the same as it is now by using time() in the query string. + """ + + url = self._timezone_query_base % (location.latitude, + location.longitude, + int(time())) + if self.api_key != '': + url += '&key=%s' % self.api_key + data = self._read_from_url(url) + response = json.loads(data) + if response['status'] == 'OK': + location.timezone = response['timeZoneId'] + else: + location.timezone = 'UTC' + + def _get_elevation(self, location): + """Query the elevation information with the latitude and longitude of + the specified `location`. + """ + + url = self._elevation_query_base % (location.latitude, + location.longitude) + if self.api_key != '': + url += '&key=%s' % self.api_key + data = self._read_from_url(url) + response = json.loads(data) + if response['status'] == 'OK': + location.elevation = int(float(response['results'][0]['elevation'])) + else: + location.elevation = 0 + + def _read_from_url(self, url): + ds = requests.get(url) + + return ds.text + + +class Astral(object): + def __init__(self, geocoder=AstralGeocoder): + """Initialise the geocoder and set the default depression.""" + + self.geocoder = geocoder() + self._depression = 6 # Set default depression in degrees + + def __getitem__(self, key): + """Returns the Location instance specified by ``key``.""" + + location = self.geocoder[key] + location.astral = self + return location + + @property + def solar_depression(self): + """The number of degrees the sun must be below the horizon for the + dawn/dusk calculation. + + Can either be set as a number of degrees below the horizon or as + one of the following strings + + ============= ======= + String Degrees + ============= ======= + civil 6.0 + nautical 12.0 + astronomical 18.0 + ============= ======= + """ + + return self._depression + + @solar_depression.setter + def solar_depression(self, depression): + if isinstance(depression, str) or isinstance(depression, ustr): + try: + self._depression = { + 'civil': 6, + 'nautical': 12, + 'astronomical': 18}[depression] + except KeyError: + raise KeyError(("solar_depression must be either a number " + "or one of 'civil', 'nautical' or " + "'astronomical'")) + else: + self._depression = float(depression) + + def sun_utc(self, date, latitude, longitude): + """Calculate all the info for the sun at once. + All times are returned in the UTC timezone. + + :param date: Date to calculate for. + :type date: :class:`datetime.date` + :param latitude: Latitude - Northern latitudes should be positive + :type latitude: float + :param longitude: Longitude - Eastern longitudes should be positive + :type longitude: float + + :returns: Dictionary with keys ``dawn``, ``sunrise``, ``noon``, + ``sunset`` and ``dusk`` whose values are the results of the + corresponding `_utc` methods. + :rtype: dict + """ + + dawn = self.dawn_utc(date, latitude, longitude) + sunrise = self.sunrise_utc(date, latitude, longitude) + noon = self.solar_noon_utc(date, longitude) + sunset = self.sunset_utc(date, latitude, longitude) + dusk = self.dusk_utc(date, latitude, longitude) + + return { + 'dawn': dawn, + 'sunrise': sunrise, + 'noon': noon, + 'sunset': sunset, + 'dusk': dusk + } + + def dawn_utc(self, date, latitude, longitude, depression=0): + """Calculate dawn time in the UTC timezone. + + :param date: Date to calculate for. + :type date: :class:`datetime.date` + :param latitude: Latitude - Northern latitudes should be positive + :type latitude: float + :param longitude: Longitude - Eastern longitudes should be positive + :type longitude: float + :param depression: Override the depression used + :type depression: float + + :return: The UTC date and time at which dawn occurs. + :rtype: :class:`~datetime.datetime` + """ + + if depression == 0: + depression = self._depression + depression += 90 + + try: + return self._calc_time(depression, SUN_RISING, date, latitude, longitude) + except ValueError as exc: + if exc.args[0] == 'math domain error': + raise AstralError(('Sun never reaches %d degrees below the horizon, ' + 'at this location.') % (depression - 90)) + else: + raise + + def sunrise_utc(self, date, latitude, longitude): + """Calculate sunrise time in the UTC timezone. + + :param date: Date to calculate for. + :type date: :class:`datetime.date` + :param latitude: Latitude - Northern latitudes should be positive + :type latitude: float + :param longitude: Longitude - Eastern longitudes should be positive + :type longitude: float + + :return: The UTC date and time at which sunrise occurs. + :rtype: :class:`~datetime.datetime` + """ + + try: + return self._calc_time(90 + 0.833, SUN_RISING, date, latitude, longitude) + except ValueError as exc: + if exc.args[0] == 'math domain error': + raise AstralError(('Sun never reaches the horizon on this day, ' + 'at this location.')) + else: + raise + + def solar_noon_utc(self, date, longitude): + """Calculate solar noon time in the UTC timezone. + + :param date: Date to calculate for. + :type date: :class:`datetime.date` + :param longitude: Longitude - Eastern longitudes should be positive + :type longitude: float + + :return: The UTC date and time at which noon occurs. + :rtype: :class:`~datetime.datetime` + """ + + jc = self._jday_to_jcentury(self._julianday(date)) + eqtime = self._eq_of_time(jc) + timeUTC = (720.0 - (4 * longitude) - eqtime) / 60.0 + + hour = int(timeUTC) + minute = int((timeUTC - hour) * 60) + second = int((((timeUTC - hour) * 60) - minute) * 60) + + if second > 59: + second -= 60 + minute += 1 + elif second < 0: + second += 60 + minute -= 1 + + if minute > 59: + minute -= 60 + hour += 1 + elif minute < 0: + minute += 60 + hour -= 1 + + if hour > 23: + hour -= 24 + date += datetime.timedelta(days=1) + elif hour < 0: + hour += 24 + date -= datetime.timedelta(days=1) + + noon = datetime.datetime(date.year, date.month, date.day, + hour, minute, second) + noon = pytz.UTC.localize(noon) # pylint: disable=E1120 + + return noon + + def sunset_utc(self, date, latitude, longitude): + """Calculate sunset time in the UTC timezone. + + :param date: Date to calculate for. + :type date: :class:`datetime.date` + :param latitude: Latitude - Northern latitudes should be positive + :type latitude: float + :param longitude: Longitude - Eastern longitudes should be positive + :type longitude: float + + :return: The UTC date and time at which sunset occurs. + :rtype: :class:`~datetime.datetime` + """ + + try: + return self._calc_time(90 + 0.833, SUN_SETTING, date, latitude, longitude) + except ValueError as exc: + if exc.args[0] == 'math domain error': + raise AstralError(('Sun never reaches the horizon on this day, ' + 'at this location.')) + else: + raise + + def dusk_utc(self, date, latitude, longitude, depression=0): + """Calculate dusk time in the UTC timezone. + + :param date: Date to calculate for. + :type date: :class:`datetime.date` + :param latitude: Latitude - Northern latitudes should be positive + :type latitude: float + :param longitude: Longitude - Eastern longitudes should be positive + :type longitude: float + :param depression: Override the depression used + :type depression: float + + :return: The UTC date and time at which dusk occurs. + :rtype: :class:`~datetime.datetime` + """ + + if depression == 0: + depression = self._depression + depression += 90 + + try: + return self._calc_time(depression, SUN_SETTING, date, latitude, longitude) + except ValueError as exc: + if exc.args[0] == 'math domain error': + raise AstralError(('Sun never reaches %d degrees below the horizon, ' + 'at this location.') % (depression - 90)) + else: + raise + + def solar_midnight_utc(self, date, longitude): + """Calculate solar midnight time in the UTC timezone. + + Note that this claculates the solar midgnight that is closest + to 00:00:00 of the specified date i.e. it may return a time that is on + the previous day. + + :param date: Date to calculate for. + :type date: :class:`datetime.date` + :param longitude: Longitude - Eastern longitudes should be positive + :type longitude: float + + :return: The UTC date and time at which midnight occurs. + :rtype: :class:`~datetime.datetime` + """ + + julianday = self._julianday(date) + + newt = self._jday_to_jcentury(julianday + 0.5 + -longitude / 360.0) + + eqtime = self._eq_of_time(newt) + timeUTC = (-longitude * 4.0) - eqtime + + timeUTC = timeUTC / 60.0 + hour = int(timeUTC) + minute = int((timeUTC - hour) * 60) + second = int((((timeUTC - hour) * 60) - minute) * 60) + + if second > 59: + second -= 60 + minute += 1 + elif second < 0: + second += 60 + minute -= 1 + + if minute > 59: + minute -= 60 + hour += 1 + elif minute < 0: + minute += 60 + hour -= 1 + + if hour < 0: + hour += 24 + date -= datetime.timedelta(days=1) + + midnight = datetime.datetime(date.year, date.month, date.day, + hour, minute, second) + midnight = pytz.UTC.localize(midnight) # pylint: disable=E1120 + + return midnight + + def daylight_utc(self, date, latitude, longitude): + """Calculate daylight start and end times in the UTC timezone. + + :param date: Date to calculate for. + :type date: :class:`datetime.date` + :param latitude: Latitude - Northern latitudes should be positive + :type latitude: float + :param longitude: Longitude - Eastern longitudes should be positive + :type longitude: float + + :return: A tuple of the UTC date and time at which daylight starts and ends. + :rtype: (:class:`~datetime.datetime`, :class:`~datetime.datetime`) + """ + + start = self.sunrise_utc(date, latitude, longitude) + end = self.sunset_utc(date, latitude, longitude) + + return start, end + + def night_utc(self, date, latitude, longitude): + """Calculate night start and end times in the UTC timezone. + + Night is calculated to be between astronomical dusk on the + date specified and astronomical dawn of the next day. + + :param date: Date to calculate for. + :type date: :class:`datetime.date` + :param latitude: Latitude - Northern latitudes should be positive + :type latitude: float + :param longitude: Longitude - Eastern longitudes should be positive + :type longitude: float + + :return: A tuple of the UTC date and time at which night starts and ends. + :rtype: (:class:`~datetime.datetime`, :class:`~datetime.datetime`) + """ + + start = self.dusk_utc(date, latitude, longitude, 18) + tomorrow = date + datetime.timedelta(days=1) + end = self.dawn_utc(tomorrow, latitude, longitude, 18) + + return start, end + + def twilight_utc(self, direction, date, latitude, longitude): + """Returns the start and end times of Twilight in the UTC timezone when + the sun is traversing in the specified direction. + + This method defines twilight as being between the time + when the sun is at -6 degrees and sunrise/sunset. + + :param direction: Determines whether the time is for the sun rising or setting. + Use ``astral.SUN_RISING`` or ``astral.SUN_SETTING``. + :type direction: int + :param date: The date for which to calculate the times. + :type date: :class:`datetime.date` + :param latitude: Latitude - Northern latitudes should be positive + :type latitude: float + :param longitude: Longitude - Eastern longitudes should be positive + :type longitude: float + + :return: A tuple of the UTC date and time at which twilight starts and ends. + :rtype: (:class:`~datetime.datetime`, :class:`~datetime.datetime`) + """ + + if date is None: + date = datetime.date.today() + + start = self.time_at_elevation_utc(-6, direction, date, latitude, longitude) + if direction == SUN_RISING: + end = self.sunrise_utc(date, latitude, longitude) + else: + end = self.sunset_utc(date, latitude, longitude) + + if direction == SUN_RISING: + return start, end + else: + return end, start + + def golden_hour_utc(self, direction, date, latitude, longitude): + """Returns the start and end times of the Golden Hour in the UTC timezone + when the sun is traversing in the specified direction. + + This method uses the definition from PhotoPills i.e. the + golden hour is when the sun is between 4 degrees below the horizon + and 6 degrees above. + + :param direction: Determines whether the time is for the sun rising or setting. + Use ``astral.SUN_RISING`` or ``astral.SUN_SETTING``. + :type direction: int + :param date: The date for which to calculate the times. + :type date: :class:`datetime.date` + :param latitude: Latitude - Northern latitudes should be positive + :type latitude: float + :param longitude: Longitude - Eastern longitudes should be positive + :type longitude: float + + :return: A tuple of the UTC date and time at which the Golden Hour starts and ends. + :rtype: (:class:`~datetime.datetime`, :class:`~datetime.datetime`) + """ + + if date is None: + date = datetime.date.today() + + start = self.time_at_elevation_utc(-4, direction, date, + latitude, longitude) + end = self.time_at_elevation_utc(6, direction, date, + latitude, longitude) + + if direction == SUN_RISING: + return start, end + else: + return end, start + + def blue_hour_utc(self, direction, date, latitude, longitude): + """Returns the start and end times of the Blue Hour in the UTC timezone + when the sun is traversing in the specified direction. + + This method uses the definition from PhotoPills i.e. the + blue hour is when the sun is between 6 and 4 degrees below the horizon. + + :param direction: Determines whether the time is for the sun rising or setting. + Use ``astral.SUN_RISING`` or ``astral.SUN_SETTING``. + :type direction: int + :param date: The date for which to calculate the times. + :type date: :class:`datetime.date` + :param latitude: Latitude - Northern latitudes should be positive + :type latitude: float + :param longitude: Longitude - Eastern longitudes should be positive + :type longitude: float + + :return: A tuple of the UTC date and time at which the Blue Hour starts and ends. + :rtype: (:class:`~datetime.datetime`, :class:`~datetime.datetime`) + """ + + if date is None: + date = datetime.date.today() + + start = self.time_at_elevation_utc(-6, direction, date, + latitude, longitude) + end = self.time_at_elevation_utc(-4, direction, date, + latitude, longitude) + + if direction == SUN_RISING: + return start, end + else: + return end, start + + def time_at_elevation_utc(self, elevation, direction, date, latitude, longitude): + """Calculate the time in the UTC timezone when the sun is at + the specified elevation on the specified date. + + Note: This method uses positive elevations for those above the horizon. + + :param elevation: Elevation in degrees above the horizon to calculate for. + :type elevation: float + :param direction: Determines whether the calculated time is for the sun rising or setting. + Use ``astral.SUN_RISING`` or ``astral.SUN_SETTING``. Default is rising. + :type direction: int + :param date: Date to calculate for. + :type date: :class:`datetime.date` + :param latitude: Latitude - Northern latitudes should be positive + :type latitude: float + :param longitude: Longitude - Eastern longitudes should be positive + :type longitude: float + + :return: The UTC date and time at which the sun is at the required + elevation. + :rtype: :class:`~datetime.datetime` + """ + + if elevation > 90.0: + elevation = 180.0 - elevation + direction = SUN_SETTING + + depression = 90 - elevation + try: + return self._calc_time(depression, direction, date, latitude, longitude) + except ValueError as exc: + if exc.args[0] == 'math domain error': + raise AstralError(('Sun never reaches an elevation of %d degrees' + 'at this location.') % elevation) + else: + raise + + def solar_azimuth(self, dateandtime, latitude, longitude): + """Calculate the azimuth angle of the sun. + + :param dateandtime: The date and time for which to calculate + the angle. + :type dateandtime: :class:`~datetime.datetime` + :param latitude: Latitude - Northern latitudes should be positive + :type latitude: float + :param longitude: Longitude - Eastern longitudes should be positive + :type longitude: float + + :return: The azimuth angle in degrees clockwise from North. + :rtype: float + + If `dateandtime` is a naive Python datetime then it is assumed to be + in the UTC timezone. + """ + + if latitude > 89.8: + latitude = 89.8 + + if latitude < -89.8: + latitude = -89.8 + + if dateandtime.tzinfo is None: + zone = 0 + utc_datetime = dateandtime + else: + zone = -dateandtime.utcoffset().total_seconds() / 3600.0 + utc_datetime = dateandtime.astimezone(pytz.utc) + + timenow = utc_datetime.hour + (utc_datetime.minute / 60.0) + \ + (utc_datetime.second / 3600.0) + + JD = self._julianday(dateandtime) + t = self._jday_to_jcentury(JD + timenow / 24.0) + theta = self._sun_declination(t) + eqtime = self._eq_of_time(t) + solarDec = theta # in degrees + + solarTimeFix = eqtime - (4.0 * -longitude) + (60 * zone) + trueSolarTime = dateandtime.hour * 60.0 + dateandtime.minute + \ + dateandtime.second / 60.0 + solarTimeFix + # in minutes + + while trueSolarTime > 1440: + trueSolarTime = trueSolarTime - 1440 + + hourangle = trueSolarTime / 4.0 - 180.0 + # Thanks to Louis Schwarzmayr for the next line: + if hourangle < -180: + hourangle = hourangle + 360.0 + + harad = radians(hourangle) + + csz = sin(radians(latitude)) * sin(radians(solarDec)) + \ + cos(radians(latitude)) * cos(radians(solarDec)) * cos(harad) + + if csz > 1.0: + csz = 1.0 + elif csz < -1.0: + csz = -1.0 + + zenith = degrees(acos(csz)) + + azDenom = (cos(radians(latitude)) * sin(radians(zenith))) + + if abs(azDenom) > 0.001: + azRad = ((sin(radians(latitude)) * cos(radians(zenith))) - + sin(radians(solarDec))) / azDenom + + if abs(azRad) > 1.0: + if azRad < 0: + azRad = -1.0 + else: + azRad = 1.0 + + azimuth = 180.0 - degrees(acos(azRad)) + + if hourangle > 0.0: + azimuth = -azimuth + else: + if latitude > 0.0: + azimuth = 180.0 + else: + azimuth = 0.0 + + if azimuth < 0.0: + azimuth = azimuth + 360.0 + + return azimuth + + def solar_elevation(self, dateandtime, latitude, longitude): + """Calculate the elevation angle of the sun. + + :param dateandtime: The date and time for which to calculate + the angle. + :type dateandtime: :class:`~datetime.datetime` + :param latitude: Latitude - Northern latitudes should be positive + :type latitude: float + :param longitude: Longitude - Eastern longitudes should be positive + :type longitude: float + + :return: The elevation angle in degrees above the horizon. + :rtype: float + + If `dateandtime` is a naive Python datetime then it is assumed to be + in the UTC timezone. + """ + + if latitude > 89.8: + latitude = 89.8 + + if latitude < -89.8: + latitude = -89.8 + + if dateandtime.tzinfo is None: + zone = 0 + utc_datetime = dateandtime + else: + zone = -dateandtime.utcoffset().total_seconds() / 3600.0 + utc_datetime = dateandtime.astimezone(pytz.utc) + + timenow = utc_datetime.hour + (utc_datetime.minute / 60.0) + \ + (utc_datetime.second / 3600) + + JD = self._julianday(dateandtime) + t = self._jday_to_jcentury(JD + timenow / 24.0) + theta = self._sun_declination(t) + eqtime = self._eq_of_time(t) + solarDec = theta # in degrees + + solarTimeFix = eqtime - (4.0 * -longitude) + (60 * zone) + trueSolarTime = dateandtime.hour * 60.0 + dateandtime.minute + \ + dateandtime.second / 60.0 + solarTimeFix + # in minutes + + while trueSolarTime > 1440: + trueSolarTime = trueSolarTime - 1440 + + hourangle = trueSolarTime / 4.0 - 180.0 + # Thanks to Louis Schwarzmayr for the next line: + if hourangle < -180: + hourangle = hourangle + 360.0 + + harad = radians(hourangle) + + csz = sin(radians(latitude)) * sin(radians(solarDec)) + \ + cos(radians(latitude)) * cos(radians(solarDec)) * cos(harad) + + if csz > 1.0: + csz = 1.0 + elif csz < -1.0: + csz = -1.0 + + zenith = degrees(acos(csz)) + + azDenom = (cos(radians(latitude)) * sin(radians(zenith))) + + if abs(azDenom) > 0.001: + azRad = ((sin(radians(latitude)) * cos(radians(zenith))) - + sin(radians(solarDec))) / azDenom + + if abs(azRad) > 1.0: + if azRad < 0: + azRad = -1.0 + else: + azRad = 1.0 + + azimuth = 180.0 - degrees(acos(azRad)) + + if hourangle > 0.0: + azimuth = -azimuth + else: + if latitude > 0.0: + azimuth = 180.0 + else: + azimuth = 0.0 + + if azimuth < 0.0: + azimuth = azimuth + 360.0 + + exoatmElevation = 90.0 - zenith + + if exoatmElevation > 85.0: + refractionCorrection = 0.0 + else: + te = tan(radians(exoatmElevation)) + if exoatmElevation > 5.0: + refractionCorrection = 58.1 / te - 0.07 / (te * te * te) + \ + 0.000086 / (te * te * te * te * te) + elif exoatmElevation > -0.575: + step1 = (-12.79 + exoatmElevation * 0.711) + step2 = (103.4 + exoatmElevation * (step1)) + step3 = (-518.2 + exoatmElevation * (step2)) + refractionCorrection = 1735.0 + exoatmElevation * (step3) + else: + refractionCorrection = -20.774 / te + + refractionCorrection = refractionCorrection / 3600.0 + + solarzen = zenith - refractionCorrection + + solarelevation = 90.0 - solarzen + + return solarelevation + + def solar_zenith(self, dateandtime, latitude, longitude): + """Calculates the solar zenith angle. + + :param dateandtime: The date and time for which to calculate + the angle. + :type dateandtime: :class:`~datetime.datetime` + :param latitude: Latitude - Northern latitudes should be positive + :type latitude: float + :param longitude: Longitude - Eastern longitudes should be positive + :type longitude: float + + :return: The zenith angle in degrees from vertical. + :rtype: float + + If `dateandtime` is a naive Python datetime then it is assumed to be + in the UTC timezone. + """ + + return 90.0 - self.solar_elevation(dateandtime, latitude, longitude) + + def moon_phase(self, date, rtype=int): + """Calculates the phase of the moon on the specified date. + + :param date: The date to calculate the phase for. + :type date: :class:`datetime.date` + :param rtype: The type to return either int (default) or float. + + :return: + A number designating the phase. + + | 0 = New moon + | 7 = First quarter + | 14 = Full moon + | 21 = Last quarter + """ + + if rtype != float and rtype != int: + rtype = int + + moon = self._moon_phase_asfloat(date) + if moon >= 28.0: + moon -= 28.0 + moon = rtype(moon) + + return moon + + def rahukaalam_utc(self, date, latitude, longitude): + """Calculate ruhakaalam times in the UTC timezone. + + :param date: Date to calculate for. + :type date: :class:`datetime.date` + :param latitude: Latitude - Northern latitudes should be positive + :type latitude: float + :param longitude: Longitude - Eastern longitudes should be positive + :type longitude: float + + :return: Tuple containing the start and end times for Rahukaalam. + :rtype: tuple + """ + + if date is None: + date = datetime.date.today() + + sunrise = self.sunrise_utc(date, latitude, longitude) + sunset = self.sunset_utc(date, latitude, longitude) + + octant_duration = datetime.timedelta(seconds=(sunset - sunrise).seconds / 8) + + # Mo,Sa,Fr,We,Th,Tu,Su + octant_index = [1, 6, 4, 5, 3, 2, 7] + + weekday = date.weekday() + octant = octant_index[weekday] + + start = sunrise + (octant_duration * octant) + end = start + octant_duration + + return start, end + + def _proper_angle(self, value): + if value > 0.0: + value /= 360.0 + return (value - floor(value)) * 360.0 + else: + tmp = ceil(abs(value / 360.0)) + return value + tmp * 360.0 + + def _julianday(self, utcdatetime, timezone=None): + if isinstance(utcdatetime, datetime.datetime): + end_date = utcdatetime.date() + hour = utcdatetime.hour + minute = utcdatetime.minute + second = utcdatetime.second + else: + end_date = utcdatetime + hour = 0 + minute = 0 + second = 0 + + if timezone: + if isinstance(timezone, int): + hour_offset = timezone + else: + offset = timezone.localize(utcdatetime).utcoffset() + hour_offset = offset.total_seconds() / 3600.0 + else: + hour_offset = 0 + + start_date = datetime.date(1900, 1, 1) + time_fraction = (hour * 3600.0 + minute * 60.0 + second) / (24.0 * 3600.0) + date_diff = excel_datediff(start_date, end_date) + jd = date_diff + 2415018.5 + time_fraction - (hour_offset / 24) + + return jd + + def _jday_to_jcentury(self, julianday): + return (julianday - 2451545.0) / 36525.0 + + def _jcentury_to_jday(self, juliancentury): + return (juliancentury * 36525.0) + 2451545.0 + + def _geom_mean_long_sun(self, juliancentury): + l0 = 280.46646 + \ + juliancentury * (36000.76983 + 0.0003032 * juliancentury) + return l0 % 360.0 + + def _geom_mean_anomaly_sun(self, juliancentury): + return 357.52911 + \ + juliancentury * (35999.05029 - 0.0001537 * juliancentury) + + def _eccentrilocation_earth_orbit(self, juliancentury): + return 0.016708634 - \ + juliancentury * (0.000042037 + 0.0000001267 * juliancentury) + + def _sun_eq_of_center(self, juliancentury): + m = self._geom_mean_anomaly_sun(juliancentury) + + mrad = radians(m) + sinm = sin(mrad) + sin2m = sin(mrad + mrad) + sin3m = sin(mrad + mrad + mrad) + + c = sinm * (1.914602 - juliancentury * + (0.004817 + 0.000014 * juliancentury)) + \ + sin2m * (0.019993 - 0.000101 * juliancentury) + \ + sin3m * 0.000289 + + return c + + def _sun_true_long(self, juliancentury): + l0 = self._geom_mean_long_sun(juliancentury) + c = self._sun_eq_of_center(juliancentury) + + return l0 + c + + def _sun_true_anomoly(self, juliancentury): + m = self._geom_mean_anomaly_sun(juliancentury) + c = self._sun_eq_of_center(juliancentury) + + return m + c + + def _sun_rad_vector(self, juliancentury): + v = self._sun_true_anomoly(juliancentury) + e = self._eccentrilocation_earth_orbit(juliancentury) + + return (1.000001018 * (1 - e * e)) / (1 + e * cos(radians(v))) + + def _sun_apparent_long(self, juliancentury): + true_long = self._sun_true_long(juliancentury) + + omega = 125.04 - 1934.136 * juliancentury + return true_long - 0.00569 - 0.00478 * sin(radians(omega)) + + def _mean_obliquity_of_ecliptic(self, juliancentury): + seconds = 21.448 - juliancentury * \ + (46.815 + juliancentury * (0.00059 - juliancentury * (0.001813))) + return 23.0 + (26.0 + (seconds / 60.0)) / 60.0 + + def _obliquity_correction(self, juliancentury): + e0 = self._mean_obliquity_of_ecliptic(juliancentury) + + omega = 125.04 - 1934.136 * juliancentury + return e0 + 0.00256 * cos(radians(omega)) + + def _sun_rt_ascension(self, juliancentury): + oc = self._obliquity_correction(juliancentury) + al = self._sun_apparent_long(juliancentury) + + tananum = (cos(radians(oc)) * sin(radians(al))) + tanadenom = cos(radians(al)) + + return degrees(atan2(tananum, tanadenom)) + + def _sun_declination(self, juliancentury): + e = self._obliquity_correction(juliancentury) + lambd = self._sun_apparent_long(juliancentury) + + sint = sin(radians(e)) * sin(radians(lambd)) + return degrees(asin(sint)) + + def _var_y(self, juliancentury): + epsilon = self._obliquity_correction(juliancentury) + y = tan(radians(epsilon) / 2.0) + return y * y + + def _eq_of_time(self, juliancentury): + l0 = self._geom_mean_long_sun(juliancentury) + e = self._eccentrilocation_earth_orbit(juliancentury) + m = self._geom_mean_anomaly_sun(juliancentury) + + y = self._var_y(juliancentury) + + sin2l0 = sin(2.0 * radians(l0)) + sinm = sin(radians(m)) + cos2l0 = cos(2.0 * radians(l0)) + sin4l0 = sin(4.0 * radians(l0)) + sin2m = sin(2.0 * radians(m)) + + Etime = y * sin2l0 - 2.0 * e * sinm + 4.0 * e * y * sinm * cos2l0 - \ + 0.5 * y * y * sin4l0 - 1.25 * e * e * sin2m + + return degrees(Etime) * 4.0 + + def _hour_angle(self, latitude, declination, depression): + latitude_rad = radians(latitude) + declination_rad = radians(declination) + depression_rad = radians(depression) + + n = cos(depression_rad) + d = cos(latitude_rad) * cos(declination_rad) + t = tan(latitude_rad) * tan(declination_rad) + h = (n / d) - t + + HA = acos(h) + return HA + + def _calc_time(self, depression, direction, date, latitude, longitude): + if not isinstance(latitude, Number) or not isinstance(longitude, Number): + raise TypeError('Latitude and longitude must be a numbers') + + julianday = self._julianday(date) + + if latitude > 89.8: + latitude = 89.8 + + if latitude < -89.8: + latitude = -89.8 + + t = self._jday_to_jcentury(julianday) + eqtime = self._eq_of_time(t) + solarDec = self._sun_declination(t) + + hourangle = self._hour_angle(latitude, solarDec, depression) + if direction == SUN_SETTING: + hourangle = -hourangle + + delta = -longitude - degrees(hourangle) + timeDiff = 4.0 * delta + timeUTC = 720.0 + timeDiff - eqtime + + timeUTC = timeUTC / 60.0 + hour = int(timeUTC) + minute = int((timeUTC - hour) * 60) + second = int((((timeUTC - hour) * 60) - minute) * 60) + + if second > 59: + second -= 60 + minute += 1 + elif second < 0: + second += 60 + minute -= 1 + + if minute > 59: + minute -= 60 + hour += 1 + elif minute < 0: + minute += 60 + hour -= 1 + + if hour > 23: + hour -= 24 + date += datetime.timedelta(days=1) + elif hour < 0: + hour += 24 + date -= datetime.timedelta(days=1) + + dt = datetime.datetime(date.year, date.month, date.day, + hour, minute, second) + dt = pytz.UTC.localize(dt) # pylint: disable=E1120 + + return dt + + def _moon_phase_asfloat(self, date): + jd = self._julianday(date) + DT = pow((jd - 2382148), 2) / (41048480 * 86400) + T = (jd + DT - 2451545.0) / 36525 + T2 = pow(T, 2) + T3 = pow(T, 3) + D = 297.85 + (445267.1115 * T) - (0.0016300 * T2) + (T3 / 545868) + D = radians(self._proper_angle(D)) + M = 357.53 + (35999.0503 * T) + M = radians(self._proper_angle(M)) + M1 = 134.96 + (477198.8676 * T) + (0.0089970 * T2) + (T3 / 69699) + M1 = radians(self._proper_angle(M1)) + elong = degrees(D) + 6.29 * sin(M1) + elong -= 2.10 * sin(M) + elong += 1.27 * sin(2 * D - M1) + elong += 0.66 * sin(2 * D) + elong = self._proper_angle(elong) + elong = round(elong) + moon = ((elong + 6.43) / 360) * 28 + return moon diff -Nru astral-1.4/src/test/conftest.py astral-1.6.1/src/test/conftest.py --- astral-1.4/src/test/conftest.py 1970-01-01 00:00:00.000000000 +0000 +++ astral-1.6.1/src/test/conftest.py 2018-02-23 12:33:58.000000000 +0000 @@ -0,0 +1,4 @@ +import os +import sys +sys.path.insert(0, + os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) diff -Nru astral-1.4/src/test/test_AstralGeocoder.py astral-1.6.1/src/test/test_AstralGeocoder.py --- astral-1.4/src/test/test_AstralGeocoder.py 2017-01-13 09:21:56.000000000 +0000 +++ astral-1.6.1/src/test/test_AstralGeocoder.py 2018-02-23 12:33:58.000000000 +0000 @@ -1,68 +1,64 @@ -# -*- coding: utf-8 -*- -import os -import sys -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) - -from pytest import raises - -from astral import AstralGeocoder - -def test_Group(): - db = AstralGeocoder() - _e = db.europe - -def test_UnknownGroup(): - with raises(AttributeError): - db = AstralGeocoder() - _e = db.wallyland - -def test_CityContainment(): - db = AstralGeocoder() - assert 'london' in db - -def test_GroupContainment(): - db = AstralGeocoder() - assert 'africa' in db - -def test_CityCountry(): - city_name = 'Birmingham,England' - - db = AstralGeocoder() - city = db[city_name] - assert city.name == 'Birmingham' - assert city.region == 'England' - -def test_MultiCountry(): - db = AstralGeocoder() - city = db['Abu Dhabi'] - assert city.name == 'Abu Dhabi' - -def test_MultiCountryWithCountry(): - """Test for fix made due to bug report from Klaus Alexander Seistrup""" - - db = AstralGeocoder() - city = db['Abu Dhabi,United Arab Emirates'] - assert city.name == 'Abu Dhabi' - - city = db['Abu Dhabi,UAE'] - assert city.name == 'Abu Dhabi' - -def test_Adelaide(): - """Test for fix made due to bug report from Klaus Alexander Seistrup""" - - db = AstralGeocoder() - _city = db['Adelaide'] - -def test_CandianCities(): - db = AstralGeocoder() - - city = db['Fredericton'] - assert city.elevation == 8 - -def test_AllCities(): - db = AstralGeocoder() - locations = db.locations - locations.sort() - - for city_name in locations: - _city = db[city_name] +# -*- coding: utf-8 -*- +from pytest import raises + +from astral import AstralGeocoder + +def test_Group(): + db = AstralGeocoder() + _e = db.europe + +def test_UnknownGroup(): + with raises(AttributeError): + db = AstralGeocoder() + _e = db.wallyland + +def test_CityContainment(): + db = AstralGeocoder() + assert 'london' in db + +def test_GroupContainment(): + db = AstralGeocoder() + assert 'africa' in db + +def test_CityCountry(): + city_name = 'Birmingham,England' + + db = AstralGeocoder() + city = db[city_name] + assert city.name == 'Birmingham' + assert city.region == 'England' + +def test_MultiCountry(): + db = AstralGeocoder() + city = db['Abu Dhabi'] + assert city.name == 'Abu Dhabi' + +def test_MultiCountryWithCountry(): + """Test for fix made due to bug report from Klaus Alexander Seistrup""" + + db = AstralGeocoder() + city = db['Abu Dhabi,United Arab Emirates'] + assert city.name == 'Abu Dhabi' + + city = db['Abu Dhabi,UAE'] + assert city.name == 'Abu Dhabi' + +def test_Adelaide(): + """Test for fix made due to bug report from Klaus Alexander Seistrup""" + + db = AstralGeocoder() + _city = db['Adelaide'] + +def test_CandianCities(): + db = AstralGeocoder() + + city = db['Fredericton'] + assert city.elevation == 8 + +def test_AllCities(): + db = AstralGeocoder() + locations = db.locations + locations.sort() + + for city_name in locations: + _city = db[city_name] diff -Nru astral-1.4/src/test/test_Astral.py astral-1.6.1/src/test/test_Astral.py --- astral-1.4/src/test/test_Astral.py 2017-02-23 16:38:06.000000000 +0000 +++ astral-1.6.1/src/test/test_Astral.py 2018-02-23 12:33:58.000000000 +0000 @@ -1,290 +1,313 @@ -# -*- coding: utf-8 -*- -import os -import sys -sys.path.insert(0, - os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) - -from pytest import raises - -import pytz -import datetime -import math -from astral import Astral - - -def float_almost_equal(value1, value2, diff=0.5): - return abs(value1 - value2) <= diff - - -def test_AstralBadLocationName(): - with raises(KeyError): - dd = Astral() - _c = dd['wally'] - - -def test_AstralLocationName(): - dd = Astral() - c = dd['London'] - assert c.name == 'London' - - -def test_AstralAssign(): - with raises(TypeError): - dd = Astral() - dd['London'] = 'wally' - - -def test_Astral(): - location_name = 'Jubail' - - dd = Astral() - dd.solar_depression = 'civil' - - location = dd[location_name] - assert location.timezone == 'Asia/Riyadh' - - sun = location.sun() - sunrise = location.sunrise(local=True) - assert sunrise == sun['sunrise'] - - -def test_Astral_SolarNoon(): - dd = Astral() - dt = datetime.datetime(2017, 2, 10) - - noon = dd.solar_noon_utc(dt, 49.65) - assert noon.hour == 8 - assert noon.minute == 55 - assert noon.second == 37 - - -def test_Astral_SolarElevation(): - dd = Astral() - dt = datetime.datetime(2015, 2, 3, 9, 0, 0, tzinfo=pytz.UTC) - - elevation = dd.solar_elevation(dt, 51.5, -0.12) - assert float_almost_equal(elevation, 9.97, 0.1) - - -def test_Astral_SolarAzimuth(): - dd = Astral() - dt = datetime.datetime(2015, 2, 3, 9, 0, 0, tzinfo=pytz.UTC) - - azimuth = dd.solar_azimuth(dt, 51.5, -0.12) - assert float_almost_equal(azimuth, 133.162, 0.1) - - -def test_Astral_SolarZenith(): - dd = Astral() - dt = datetime.datetime(2015, 2, 3, 9, 0, 0, tzinfo=pytz.UTC) - - zenith = dd.solar_zenith(dt, 51.5, -0.12) - assert float_almost_equal(zenith, 90.0 - 9.97, 0.1) - - -def test_Astral_SolarElevationWithTimezone(): - dd = Astral() - location = dd['Jubail'] - - dt = datetime.datetime(2015, 2, 4, 9, 0, 0, tzinfo=location.tz) - elevation = dd.solar_elevation(dt, location.latitude, location.longitude) - assert float_almost_equal(elevation, 28.118, 0.1) - - -def test_Astral_SolarAzimuthWithTimezone(): - dd = Astral() - location = dd['Jubail'] - - dt = datetime.datetime(2015, 2, 4, 9, 0, 0, tzinfo=location.tz) - azimuth = dd.solar_azimuth(dt, location.latitude, location.longitude) - assert float_almost_equal(azimuth, 129.02, 0.1) - - -def test_Astral_JulianDay_Date(): - a = Astral() - - dt = datetime.date(2015, 1, 1) - jd = a._julianday(dt) - assert float_almost_equal(jd, 2457023.5, 0.1) - - dt = datetime.date(2015, 2, 9) - jd = a._julianday(dt) - assert float_almost_equal(jd, 2457062.5, 0.1) - - dt = datetime.date(2000, 8, 12) - jd = a._julianday(dt) - assert float_almost_equal(jd, 2451768.5, 0.1) - - dt = datetime.date(1632, 8, 12) - jd = a._julianday(dt) - assert float_almost_equal(jd, 2317359.5, 0.1) - - -def test_Astral_JulianDay_DateTime(): - a = Astral() - - dt = datetime.datetime(2015, 1, 1, 1, 36, 0) - jd = a._julianday(dt) - assert float_almost_equal(jd, 2457023.57, 0.1) - - dt = datetime.datetime(2015, 1, 1, 15, 12, 0) - jd = a._julianday(dt) - assert float_almost_equal(jd, 2457024.13, 0.1) - - -def test_Astral_JulianDay_DateTimeZone(): - a = Astral() - - dt = datetime.datetime(2015, 1, 1, 1, 36, 0) - jd = a._julianday(dt, 1) - assert float_almost_equal(jd, 2457023.51, 0.1) - - dt = datetime.datetime(2015, 10, 10, 1, 36, 0) - jd = a._julianday(dt, 5) - assert float_almost_equal(jd, 2457305.36, 0.1) - - -def test_GeomMeanLongSun(): - a = Astral() - - dt = datetime.datetime(2015, 10, 10, 1, 36, 0) - jc = a._jday_to_jcentury(a._julianday(dt, 5)) - geom = a._geom_mean_long_sun(jc) - assert float_almost_equal(geom, 198.1484524, 0.1) - - dt = datetime.datetime(2015, 1, 1, 1, 36, 0) - jc = a._jday_to_jcentury(a._julianday(dt, -4)) - geom = a._geom_mean_long_sun(jc) - assert float_almost_equal(geom, 280.5655139, 0.1) - - -def test_GeomMeanAnomalySun(): - a = Astral() - - dt = datetime.date(2015, 10, 10) - jc = a._jday_to_jcentury(a._julianday(dt, -4)) - geom = a._geom_mean_anomaly_sun(jc) - assert float_almost_equal(geom, 6035.243796, 0.1) - - -def test_EccentrilocationEarthOrbit(): - a = Astral() - - dt = datetime.date(2015, 10, 10) - jc = a._jday_to_jcentury(a._julianday(dt, -4)) - ecc = a._eccentrilocation_earth_orbit(jc) - assert float_almost_equal(ecc, 0.016702001, 0.1) - - -def test_SunEqOfCenter(): - a = Astral() - - dt = datetime.date(2015, 10, 10) - jc = a._jday_to_jcentury(a._julianday(dt, -4)) - eoc = a._sun_eq_of_center(jc) - assert float_almost_equal(eoc, -1.909033734, 0.1) - - -def test_SunTrueLong(): - a = Astral() - - dt = datetime.date(2015, 10, 10) - jc = a._jday_to_jcentury(a._julianday(dt, -4)) - true_long = a._sun_true_long(jc) - assert float_almost_equal(true_long, 196.6090364, 0.1) - - -def test_SunTrueAnomoly(): - a = Astral() - - dt = datetime.date(2015, 10, 10) - jc = a._jday_to_jcentury(a._julianday(dt, -4)) - true_long = a._sun_true_anomoly(jc) - assert float_almost_equal(true_long, 6033.400469, 0.1) - - -def test_SunRadVector(): - a = Astral() - - dt = datetime.date(2015, 10, 10) - jc = a._jday_to_jcentury(a._julianday(dt, -4)) - rad_vector = a._sun_rad_vector(jc) - assert float_almost_equal(rad_vector, 0.998732645, 0.1) - - -def test_SunApparentLong(): - a = Astral() - - dt = datetime.date(2015, 10, 10) - jc = a._jday_to_jcentury(a._julianday(dt, -4)) - apparent_long = a._sun_apparent_long(jc) - assert float_almost_equal(apparent_long, 196.6033454, 0.1) - - -def test_MeanObliquityOfEcliptic(): - a = Astral() - - dt = datetime.date(2015, 10, 10) - jc = a._jday_to_jcentury(a._julianday(dt, -4)) - mean_obliquity = a._mean_obliquity_of_ecliptic(jc) - assert float_almost_equal(mean_obliquity, 23.43724009, 0.1) - - -def test_ObliquityCorrection(): - a = Astral() - - dt = datetime.datetime(2015, 10, 10, 1, 36, 0) - jc = a._jday_to_jcentury(a._julianday(dt, -4)) - obliquity_correction = a._obliquity_correction(jc) - assert float_almost_equal(obliquity_correction, 23.43468009, 0.0001) - - -def test_SunRightAscension(): - a = Astral() - - dt = datetime.date(2015, 10, 10) - jc = a._jday_to_jcentury(a._julianday(dt, -4)) - rt_ascension = a._sun_rt_ascension(jc) - assert float_almost_equal(rt_ascension, -164.7605748, 0.001) - - -def test_SunDeclination(): - a = Astral() - - dt = datetime.datetime(2015, 10, 10, 1, 36, 0) - jc = a._jday_to_jcentury(a._julianday(dt, -4)) - declination = a._sun_declination(jc) - assert float_almost_equal(declination, -6.525273018, 0.0001) - - -def test_VarY(): - a = Astral() - - dt = datetime.datetime(2015, 10, 10, 0, 54, 0) - jc = a._jday_to_jcentury(a._julianday(dt, -4)) - y = a._var_y(jc) - assert float_almost_equal(y, 0.043017118, 0.0001) - - -def test_EquationOfTime(): - a = Astral() - - dt = datetime.datetime(2015, 10, 10, 0, 54, 0) - jc = a._jday_to_jcentury(a._julianday(dt, -4)) - etime = a._eq_of_time(jc) - assert float_almost_equal(etime, 12.84030157, 0.0001) - - dt = datetime.datetime(2017, 1, 1, 2, 0, 0) - jc = a._jday_to_jcentury(a._julianday(dt, 2)) - etime = a._eq_of_time(jc) - assert float_almost_equal(etime, -3.438084536, 0.0001) - - -def test_SunriseHourAngle(): - a = Astral() - - dt = datetime.date(2015, 1, 1) - jc = a._jday_to_jcentury(a._julianday(dt, 1)) - declination = a._sun_declination(jc) - ha = math.degrees(a._hour_angle(52.169, declination, 90.833)) - assert float_almost_equal(ha, 58.6102679, 0.1) +# -*- coding: utf-8 -*- +import pytest + +import pytz +import datetime +import math +from astral import Astral + + +def float_almost_equal(value1, value2, diff=0.5): + return abs(value1 - value2) <= diff + + +def test_AstralBadLocationName(): + with pytest.raises(KeyError): + dd = Astral() + _c = dd['wally'] + + +def test_AstralLocationName(): + dd = Astral() + c = dd['London'] + assert c.name == 'London' + + +def test_AstralAssign(): + with pytest.raises(TypeError): + dd = Astral() + dd['London'] = 'wally' + + +def test_Astral(): + location_name = 'Jubail' + + dd = Astral() + dd.solar_depression = 'civil' + + location = dd[location_name] + assert location.timezone == 'Asia/Riyadh' + + sun = location.sun() + sunrise = location.sunrise(local=True) + assert sunrise == sun['sunrise'] + + +def test_Astral_SolarNoon(): + dd = Astral() + dt = datetime.datetime(2017, 2, 10) + + noon = dd.solar_noon_utc(dt, 49.65) + assert noon.hour == 8 + assert noon.minute == 55 + assert noon.second == 37 + + +def test_Astral_SolarElevation(): + dd = Astral() + dt = datetime.datetime(2015, 2, 3, 9, 0, 0, tzinfo=pytz.UTC) + + elevation = dd.solar_elevation(dt, 51.5, -0.12) + assert float_almost_equal(elevation, 9.97, 0.1) + + +def test_Astral_SolarAzimuth(): + dd = Astral() + dt = datetime.datetime(2015, 2, 3, 9, 0, 0, tzinfo=pytz.UTC) + + azimuth = dd.solar_azimuth(dt, 51.5, -0.12) + assert float_almost_equal(azimuth, 133.162, 0.1) + + +def test_Astral_SolarZenith(): + dd = Astral() + dt = datetime.datetime(2015, 2, 3, 9, 0, 0, tzinfo=pytz.UTC) + + zenith = dd.solar_zenith(dt, 51.5, -0.12) + assert float_almost_equal(zenith, 90.0 - 9.97, 0.1) + + +def test_Astral_SolarElevationWithTimezone(): + dd = Astral() + location = dd['Jubail'] + + dt = datetime.datetime(2015, 2, 4, 9, 0, 0, tzinfo=location.tz) + elevation = dd.solar_elevation(dt, location.latitude, location.longitude) + assert float_almost_equal(elevation, 28.118, 0.1) + + +def test_Astral_SolarAzimuthWithTimezone(): + dd = Astral() + location = dd['Jubail'] + + dt = datetime.datetime(2015, 2, 4, 9, 0, 0, tzinfo=location.tz) + azimuth = dd.solar_azimuth(dt, location.latitude, location.longitude) + assert float_almost_equal(azimuth, 129.02, 0.1) + + +def test_Astral_JulianDay_Date(): + a = Astral() + + dt = datetime.date(2015, 1, 1) + jd = a._julianday(dt) + assert float_almost_equal(jd, 2457023.5, 0.1) + + dt = datetime.date(2015, 2, 9) + jd = a._julianday(dt) + assert float_almost_equal(jd, 2457062.5, 0.1) + + dt = datetime.date(2000, 8, 12) + jd = a._julianday(dt) + assert float_almost_equal(jd, 2451768.5, 0.1) + + dt = datetime.date(1632, 8, 12) + jd = a._julianday(dt) + assert float_almost_equal(jd, 2317359.5, 0.1) + + +def test_Astral_JulianDay_DateTime(): + a = Astral() + + dt = datetime.datetime(2015, 1, 1, 1, 36, 0) + jd = a._julianday(dt) + assert float_almost_equal(jd, 2457023.57, 0.1) + + dt = datetime.datetime(2015, 1, 1, 15, 12, 0) + jd = a._julianday(dt) + assert float_almost_equal(jd, 2457024.13, 0.1) + + +def test_Astral_JulianDay_DateTimeZone(): + a = Astral() + + dt = datetime.datetime(2015, 1, 1, 1, 36, 0) + jd = a._julianday(dt, 1) + assert float_almost_equal(jd, 2457023.51, 0.1) + + dt = datetime.datetime(2015, 10, 10, 1, 36, 0) + jd = a._julianday(dt, 5) + assert float_almost_equal(jd, 2457305.36, 0.1) + + +def test_GeomMeanLongSun(): + a = Astral() + + dt = datetime.datetime(2015, 10, 10, 1, 36, 0) + jc = a._jday_to_jcentury(a._julianday(dt, 5)) + geom = a._geom_mean_long_sun(jc) + assert float_almost_equal(geom, 198.1484524, 0.1) + + dt = datetime.datetime(2015, 1, 1, 1, 36, 0) + jc = a._jday_to_jcentury(a._julianday(dt, -4)) + geom = a._geom_mean_long_sun(jc) + assert float_almost_equal(geom, 280.5655139, 0.1) + + +def test_GeomMeanAnomalySun(): + a = Astral() + + dt = datetime.date(2015, 10, 10) + jc = a._jday_to_jcentury(a._julianday(dt, -4)) + geom = a._geom_mean_anomaly_sun(jc) + assert float_almost_equal(geom, 6035.243796, 0.1) + + +def test_EccentrilocationEarthOrbit(): + a = Astral() + + dt = datetime.date(2015, 10, 10) + jc = a._jday_to_jcentury(a._julianday(dt, -4)) + ecc = a._eccentrilocation_earth_orbit(jc) + assert float_almost_equal(ecc, 0.016702001, 0.1) + + +def test_SunEqOfCenter(): + a = Astral() + + dt = datetime.date(2015, 10, 10) + jc = a._jday_to_jcentury(a._julianday(dt, -4)) + eoc = a._sun_eq_of_center(jc) + assert float_almost_equal(eoc, -1.909033734, 0.1) + + +def test_SunTrueLong(): + a = Astral() + + dt = datetime.date(2015, 10, 10) + jc = a._jday_to_jcentury(a._julianday(dt, -4)) + true_long = a._sun_true_long(jc) + assert float_almost_equal(true_long, 196.6090364, 0.1) + + +def test_SunTrueAnomoly(): + a = Astral() + + dt = datetime.date(2015, 10, 10) + jc = a._jday_to_jcentury(a._julianday(dt, -4)) + true_long = a._sun_true_anomoly(jc) + assert float_almost_equal(true_long, 6033.400469, 0.1) + + +def test_SunRadVector(): + a = Astral() + + dt = datetime.date(2015, 10, 10) + jc = a._jday_to_jcentury(a._julianday(dt, -4)) + rad_vector = a._sun_rad_vector(jc) + assert float_almost_equal(rad_vector, 0.998732645, 0.1) + + +def test_SunApparentLong(): + a = Astral() + + dt = datetime.date(2015, 10, 10) + jc = a._jday_to_jcentury(a._julianday(dt, -4)) + apparent_long = a._sun_apparent_long(jc) + assert float_almost_equal(apparent_long, 196.6033454, 0.1) + + +def test_MeanObliquityOfEcliptic(): + a = Astral() + + dt = datetime.date(2015, 10, 10) + jc = a._jday_to_jcentury(a._julianday(dt, -4)) + mean_obliquity = a._mean_obliquity_of_ecliptic(jc) + assert float_almost_equal(mean_obliquity, 23.43724009, 0.1) + + +def test_ObliquityCorrection(): + a = Astral() + + dt = datetime.datetime(2015, 10, 10, 1, 36, 0) + jc = a._jday_to_jcentury(a._julianday(dt, -4)) + obliquity_correction = a._obliquity_correction(jc) + assert float_almost_equal(obliquity_correction, 23.43468009, 0.0001) + + +def test_SunRightAscension(): + a = Astral() + + dt = datetime.date(2015, 10, 10) + jc = a._jday_to_jcentury(a._julianday(dt, -4)) + rt_ascension = a._sun_rt_ascension(jc) + assert float_almost_equal(rt_ascension, -164.7605748, 0.001) + + +def test_SunDeclination(): + a = Astral() + + dt = datetime.datetime(2015, 10, 10, 1, 36, 0) + jc = a._jday_to_jcentury(a._julianday(dt, -4)) + declination = a._sun_declination(jc) + assert float_almost_equal(declination, -6.525273018, 0.0001) + + +def test_VarY(): + a = Astral() + + dt = datetime.datetime(2015, 10, 10, 0, 54, 0) + jc = a._jday_to_jcentury(a._julianday(dt, -4)) + y = a._var_y(jc) + assert float_almost_equal(y, 0.043017118, 0.0001) + + +def test_EquationOfTime(): + a = Astral() + + dt = datetime.datetime(2015, 10, 10, 0, 54, 0) + jc = a._jday_to_jcentury(a._julianday(dt, -4)) + etime = a._eq_of_time(jc) + assert float_almost_equal(etime, 12.84030157, 0.0001) + + dt = datetime.datetime(2017, 1, 1, 2, 0, 0) + jc = a._jday_to_jcentury(a._julianday(dt, 2)) + etime = a._eq_of_time(jc) + assert float_almost_equal(etime, -3.438084536, 0.0001) + + +def test_SunriseHourAngle(): + a = Astral() + + dt = datetime.date(2015, 1, 1) + jc = a._jday_to_jcentury(a._julianday(dt, 1)) + declination = a._sun_declination(jc) + ha = math.degrees(a._hour_angle(52.169, declination, 90.833)) + assert float_almost_equal(ha, 58.6102679, 0.1) + +@pytest.mark.parametrize("test", ["dawn_utc", "sunrise_utc", "sunset_utc", "dusk_utc"]) +def test_UTC_BadDate(test): + a = Astral() + func = getattr(a, test) + + with pytest.raises(AttributeError): + l = func('s', 1, -0.5) + + +@pytest.mark.parametrize("test", ["dawn_utc", "sunrise_utc", "sunset_utc", "dusk_utc"]) +def test_UTC_BadLatitude(test): + a = Astral() + func = getattr(a, test) + dt = datetime.date(2015, 1, 1) + + with pytest.raises(TypeError): + l = func(dt, 1, 'a') + + +@pytest.mark.parametrize("test", ["dawn_utc", "sunrise_utc", "sunset_utc", "dusk_utc"]) +def test_UTC_BadLongitude(test): + a = Astral() + func = getattr(a, test) + dt = datetime.date(2015, 1, 1) + + with pytest.raises(TypeError): + l = func(dt, 'a', 1) diff -Nru astral-1.4/src/test/test_BuenosAries.py astral-1.6.1/src/test/test_BuenosAries.py --- astral-1.4/src/test/test_BuenosAries.py 2017-01-13 09:21:56.000000000 +0000 +++ astral-1.6.1/src/test/test_BuenosAries.py 2018-02-23 12:33:58.000000000 +0000 @@ -1,12 +1,7 @@ -# -*- coding: utf-8 -*- -import os -import sys -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) - -from astral import Astral, Location - -def test_BuenosAries(): - a = Astral() - b = a['Buenos Aires'] - assert b.timezone == 'America/Buenos_Aires' - \ No newline at end of file +# -*- coding: utf-8 -*- +from astral import Astral, Location + +def test_BuenosAries(): + a = Astral() + b = a['Buenos Aires'] + assert b.timezone == 'America/Buenos_Aires' diff -Nru astral-1.4/src/test/test_DepressionNotReached.py astral-1.6.1/src/test/test_DepressionNotReached.py --- astral-1.4/src/test/test_DepressionNotReached.py 2017-01-13 09:21:56.000000000 +0000 +++ astral-1.6.1/src/test/test_DepressionNotReached.py 2018-02-23 12:33:58.000000000 +0000 @@ -1,19 +1,15 @@ -# -*- coding: utf-8 -*- - -from __future__ import unicode_literals -import os -import sys -sys.path.insert(0, - os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) - -import pytest -import astral -import datetime - - -def test_Dawn_NeverReachesDepression(): - d = datetime.date(2016, 5, 29) - with pytest.raises(astral.AstralError): - l = astral.Location(("Ghent", "Belgium", "51°3'N", "3°44'W", "Europe/Brussels")) - l.solar_depression = 18 - l.dawn(date=d, local=True) +# -*- coding: utf-8 -*- + +from __future__ import unicode_literals + +import pytest +import astral +import datetime + + +def test_Dawn_NeverReachesDepression(): + d = datetime.date(2016, 5, 29) + with pytest.raises(astral.AstralError): + l = astral.Location(("Ghent", "Belgium", "51°3'N", "3°44'W", "Europe/Brussels")) + l.solar_depression = 18 + l.dawn(date=d, local=True) diff -Nru astral-1.4/src/test/test_GoldenBlue.py astral-1.6.1/src/test/test_GoldenBlue.py --- astral-1.4/src/test/test_GoldenBlue.py 2017-02-23 16:38:06.000000000 +0000 +++ astral-1.6.1/src/test/test_GoldenBlue.py 2018-02-23 12:33:58.000000000 +0000 @@ -1,91 +1,86 @@ -# -*- coding: utf-8 -*- -# Test data taken from http://www.timeanddate.com/sun/uk/london - -import os -import sys -sys.path.insert(0, - os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) - -import pytz -import datetime -from astral import Astral, SUN_RISING, SUN_SETTING - - -def datetime_almost_equal(datetime1, datetime2, seconds=60): - dd = datetime1 - datetime2 - sd = (dd.days * 24 * 60 * 60) + dd.seconds - return abs(sd) <= seconds - - -def test_Location_GoldenHour_Morning(): - a = Astral() - l = a['London'] - - test_data = { - datetime.date(2016, 5, 18): (datetime.datetime(2016, 5, 18, 3, 38), - datetime.datetime(2016, 5, 18, 4, 53)), - datetime.date(2016, 1, 1): (datetime.datetime(2016, 1, 1, 7, 41), - datetime.datetime(2016, 1, 1, 9, 7)), - } - - for day, golden_hour in test_data.items(): - start1 = pytz.UTC.localize(golden_hour[0]) - end1 = pytz.UTC.localize(golden_hour[1]) - - start2, end2 = l.golden_hour(SUN_RISING, day) - assert datetime_almost_equal(end1, end2, seconds=90) - assert datetime_almost_equal(start1, start2, seconds=90) - - -def test_Location_GoldenHour_Evening(): - a = Astral() - l = a['London'] - - test_data = { - datetime.date(2016, 5, 18): (datetime.datetime(2016, 5, 18, 19, 0), - datetime.datetime(2016, 5, 18, 20, 16)), - } - - for day, golden_hour in test_data.items(): - start1 = pytz.UTC.localize(golden_hour[0]) - end1 = pytz.UTC.localize(golden_hour[1]) - - start2, end2 = l.golden_hour(SUN_SETTING, day) - assert datetime_almost_equal(end1, end2, seconds=90) - assert datetime_almost_equal(start1, start2, seconds=90) - - -def test_Location_BlueHour_Morning(): - a = Astral() - l = a['London'] - - test_data = { - datetime.date(2016, 5, 19): (datetime.datetime(2016, 5, 19, 3, 19), - datetime.datetime(2016, 5, 19, 3, 36)), - } - - for day, blue_hour in test_data.items(): - start1 = pytz.UTC.localize(blue_hour[0]) - end1 = pytz.UTC.localize(blue_hour[1]) - - start2, end2 = l.blue_hour(SUN_RISING, day) - assert datetime_almost_equal(end1, end2, seconds=90) - assert datetime_almost_equal(start1, start2, seconds=90) - - -def test_Location_BlueHour_Evening(): - a = Astral() - l = a['London'] - - test_data = { - datetime.date(2016, 5, 19): (datetime.datetime(2016, 5, 19, 20, 17), - datetime.datetime(2016, 5, 19, 20, 34)), - } - - for day, blue_hour in test_data.items(): - start1 = pytz.UTC.localize(blue_hour[0]) - end1 = pytz.UTC.localize(blue_hour[1]) - - start2, end2 = l.blue_hour(SUN_SETTING, day) - assert datetime_almost_equal(end1, end2, seconds=90) - assert datetime_almost_equal(start1, start2, seconds=90) +# -*- coding: utf-8 -*- +# Test data taken from http://www.timeanddate.com/sun/uk/london + +import pytz +import datetime +from astral import Astral, SUN_RISING, SUN_SETTING + + +def datetime_almost_equal(datetime1, datetime2, seconds=60): + dd = datetime1 - datetime2 + sd = (dd.days * 24 * 60 * 60) + dd.seconds + return abs(sd) <= seconds + + +def test_Location_GoldenHour_Morning(): + a = Astral() + l = a['London'] + + test_data = { + datetime.date(2016, 5, 18): (datetime.datetime(2016, 5, 18, 3, 38), + datetime.datetime(2016, 5, 18, 4, 53)), + datetime.date(2016, 1, 1): (datetime.datetime(2016, 1, 1, 7, 41), + datetime.datetime(2016, 1, 1, 9, 7)), + } + + for day, golden_hour in test_data.items(): + start1 = pytz.UTC.localize(golden_hour[0]) + end1 = pytz.UTC.localize(golden_hour[1]) + + start2, end2 = l.golden_hour(SUN_RISING, day) + assert datetime_almost_equal(end1, end2, seconds=90) + assert datetime_almost_equal(start1, start2, seconds=90) + + +def test_Location_GoldenHour_Evening(): + a = Astral() + l = a['London'] + + test_data = { + datetime.date(2016, 5, 18): (datetime.datetime(2016, 5, 18, 19, 0), + datetime.datetime(2016, 5, 18, 20, 16)), + } + + for day, golden_hour in test_data.items(): + start1 = pytz.UTC.localize(golden_hour[0]) + end1 = pytz.UTC.localize(golden_hour[1]) + + start2, end2 = l.golden_hour(SUN_SETTING, day) + assert datetime_almost_equal(end1, end2, seconds=90) + assert datetime_almost_equal(start1, start2, seconds=90) + + +def test_Location_BlueHour_Morning(): + a = Astral() + l = a['London'] + + test_data = { + datetime.date(2016, 5, 19): (datetime.datetime(2016, 5, 19, 3, 19), + datetime.datetime(2016, 5, 19, 3, 36)), + } + + for day, blue_hour in test_data.items(): + start1 = pytz.UTC.localize(blue_hour[0]) + end1 = pytz.UTC.localize(blue_hour[1]) + + start2, end2 = l.blue_hour(SUN_RISING, day) + assert datetime_almost_equal(end1, end2, seconds=90) + assert datetime_almost_equal(start1, start2, seconds=90) + + +def test_Location_BlueHour_Evening(): + a = Astral() + l = a['London'] + + test_data = { + datetime.date(2016, 5, 19): (datetime.datetime(2016, 5, 19, 20, 17), + datetime.datetime(2016, 5, 19, 20, 34)), + } + + for day, blue_hour in test_data.items(): + start1 = pytz.UTC.localize(blue_hour[0]) + end1 = pytz.UTC.localize(blue_hour[1]) + + start2, end2 = l.blue_hour(SUN_SETTING, day) + assert datetime_almost_equal(end1, end2, seconds=90) + assert datetime_almost_equal(start1, start2, seconds=90) diff -Nru astral-1.4/src/test/test_GoogleGeocoder.py astral-1.6.1/src/test/test_GoogleGeocoder.py --- astral-1.4/src/test/test_GoogleGeocoder.py 2017-01-13 09:21:56.000000000 +0000 +++ astral-1.6.1/src/test/test_GoogleGeocoder.py 2018-02-27 10:32:37.000000000 +0000 @@ -1,16 +1,30 @@ -import os -import sys -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) - -import pytest - -from astral import GoogleGeocoder - -@pytest.mark.webtest -def test_GoogleLocator(): - locator = GoogleGeocoder() - l = locator['Eiffel Tower'] - assert l is not None - -if __name__ == '__main__': - test_GoogleLocator() +import io +import os.path +import pytest + +from astral import GoogleGeocoder + +def read_contents(*names, **kwargs): + return io.open( + os.path.join(*names), + encoding=kwargs.get("encoding", "utf8") + ).read() + +try: + api_key = read_contents(os.path.dirname(__file__), '.api_key').strip() +except: + api_key = '' + +@pytest.mark.webtest +@pytest.mark.skipif(api_key != '', reason="api key file found") +def test_GoogleLocator(): + locator = GoogleGeocoder() + l = locator['Eiffel Tower'] + assert l is not None + +@pytest.mark.webtest +@pytest.mark.skipif(api_key == '', reason="api key file not found") +def test_GoogleLocator_WithAPIKey(): + locator = GoogleGeocoder(api_key=api_key) + l = locator['Eiffel Tower'] + assert l is not None diff -Nru astral-1.4/src/test/test_Location.py astral-1.6.1/src/test/test_Location.py --- astral-1.4/src/test/test_Location.py 2017-02-23 16:38:06.000000000 +0000 +++ astral-1.6.1/src/test/test_Location.py 2018-02-23 12:33:58.000000000 +0000 @@ -1,181 +1,178 @@ -# -*- coding: utf-8 -*- -import os -import sys -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) - -from pytest import raises - -from astral import Astral, AstralError, Location -import datetime -import pytz - - -def datetime_almost_equal(datetime1, datetime2, seconds=60): - dd = datetime1 - datetime2 - sd = (dd.days * 24 * 60 * 60) + dd.seconds - return abs(sd) <= seconds - - -def test_Location_Name(): - c = Location() - assert c.name == 'Greenwich' - c.name = 'London' - assert c.name == 'London' - c.name = 'Köln' - assert c.name == 'Köln' - - -def test_Location_Country(): - c = Location() - assert c.region == 'England' - c.region = 'Australia' - assert c.region == 'Australia' - - -def test_Location_Elevation(): - dd = Astral() - c = dd['London'] - - assert c.elevation == 24 - - -def test_Location_TimezoneName(): - c = Location() - assert c.timezone == 'Europe/London' - c.name = 'Asia/Riyadh' - assert c.name == 'Asia/Riyadh' - - -def test_Location_TimezoneNameNoLocation(): - c = Location() - c._timezone_group = 'Europe' - c._timezone_location = '' - assert c.timezone == 'Europe' - - -def test_Location_TimezoneNameBad(): - c = Location() - with raises(ValueError): - c.timezone = 'bad/timezone' - - -def test_Location_TimezoneLookup(): - c = Location() - assert c.tz == pytz.timezone('Europe/London') - c.timezone='Europe/Stockholm' - assert c.tz == pytz.timezone('Europe/Stockholm') - - -def test_Location_TimezoneLookupBad(): - c = Location() - c._timezone_group = 'bad' - c._timezone_location = 'timezone' - with raises(AstralError): - c.tz - - -def test_Location_Sun(): - c = Location() - c.sun() - - -def test_Location_Dawn(): - c = Location() - c.dawn() - - -def test_Location_DawnUTC(): - c = Location() - c.dawn(local=False) - - -def test_Location_Sunrise(): - c = Location() - c.sunrise() - - -def test_Location_SunriseUTC(): - c = Location() - c.sunrise(local=False) - - -def test_Location_SolarNoon(): - c = Location() - c.solar_noon() - - -def test_Location_SolarNoonUTC(): - c = Location() - c.solar_noon(local=False) - - -def test_Location_Dusk(): - c = Location() - c.dusk() - - -def test_Location_DuskUTC(): - c = Location() - c.dusk(local=False) - - -def test_Location_Sunset(): - c = Location() - c.sunset() - - -def test_Location_SunsetUTC(): - c = Location() - c.sunset(local=False) - - -def test_Location_SolarElevation(): - dd = Astral() - location = dd['Riyadh'] - dt = datetime.datetime(2015, 12, 14, 8, 0, 0) - dt = location.tz.localize(dt) - elevation = location.solar_elevation(dt) - assert abs(elevation - 17) < 0.5 - - -def test_Location_SolarAzimuth(): - dd = Astral() - location = dd['Riyadh'] - dt = datetime.datetime(2015, 12, 14, 8, 0, 0) - dt = location.tz.localize(dt) - azimuth = location.solar_azimuth(dt) - assert abs(azimuth - 126) < 0.5 - - -def test_Location_TimeAtElevation(): - dd = Astral() - location = dd['New Delhi'] - - test_data = { - datetime.date(2016, 1, 5): datetime.datetime(2016, 1, 5, 10, 0), - } - - for day, cdt in test_data.items(): - cdt = location.tz.localize(cdt) - dt = location.time_at_elevation(28, date=day) - assert datetime_almost_equal(dt, cdt, seconds=600) - - -def test_Location_SolarDepression(): - c = Location(("Heidelberg", "Germany", 49.412, -8.71, "Europe/Berlin")) - c.solar_depression = 'nautical' - assert c.solar_depression == 12 - - c.solar_depression = 18 - assert c.solar_depression == 18 - - -def test_Location_Moon(): - c=Location() - c.moon_phase() - - -def test_Location_TzError(): - with raises(AttributeError): - c = Location() - c.tz = 1 +# -*- coding: utf-8 -*- +from pytest import raises + +from astral import Astral, AstralError, Location +import datetime +import pytz + + +def datetime_almost_equal(datetime1, datetime2, seconds=60): + dd = datetime1 - datetime2 + sd = (dd.days * 24 * 60 * 60) + dd.seconds + return abs(sd) <= seconds + + +def test_Location_Name(): + c = Location() + assert c.name == 'Greenwich' + c.name = 'London' + assert c.name == 'London' + c.name = 'Köln' + assert c.name == 'Köln' + + +def test_Location_Country(): + c = Location() + assert c.region == 'England' + c.region = 'Australia' + assert c.region == 'Australia' + + +def test_Location_Elevation(): + dd = Astral() + c = dd['London'] + + assert c.elevation == 24 + + +def test_Location_TimezoneName(): + c = Location() + assert c.timezone == 'Europe/London' + c.name = 'Asia/Riyadh' + assert c.name == 'Asia/Riyadh' + + +def test_Location_TimezoneNameNoLocation(): + c = Location() + c._timezone_group = 'Europe' + c._timezone_location = '' + assert c.timezone == 'Europe' + + +def test_Location_TimezoneNameBad(): + c = Location() + with raises(ValueError): + c.timezone = 'bad/timezone' + + +def test_Location_TimezoneLookup(): + c = Location() + assert c.tz == pytz.timezone('Europe/London') + c.timezone='Europe/Stockholm' + assert c.tz == pytz.timezone('Europe/Stockholm') + + +def test_Location_TimezoneLookupBad(): + c = Location() + c._timezone_group = 'bad' + c._timezone_location = 'timezone' + with raises(AstralError): + c.tz + + +def test_Location_Sun(): + c = Location() + c.sun() + + +def test_Location_Dawn(): + c = Location() + c.dawn() + + +def test_Location_DawnUTC(): + c = Location() + c.dawn(local=False) + + +def test_Location_Sunrise(): + c = Location() + c.sunrise() + + +def test_Location_SunriseUTC(): + c = Location() + c.sunrise(local=False) + + +def test_Location_SolarNoon(): + c = Location() + c.solar_noon() + + +def test_Location_SolarNoonUTC(): + c = Location() + c.solar_noon(local=False) + + +def test_Location_Dusk(): + c = Location() + c.dusk() + + +def test_Location_DuskUTC(): + c = Location() + c.dusk(local=False) + + +def test_Location_Sunset(): + c = Location() + c.sunset() + + +def test_Location_SunsetUTC(): + c = Location() + c.sunset(local=False) + + +def test_Location_SolarElevation(): + dd = Astral() + location = dd['Riyadh'] + dt = datetime.datetime(2015, 12, 14, 8, 0, 0) + dt = location.tz.localize(dt) + elevation = location.solar_elevation(dt) + assert abs(elevation - 17) < 0.5 + + +def test_Location_SolarAzimuth(): + dd = Astral() + location = dd['Riyadh'] + dt = datetime.datetime(2015, 12, 14, 8, 0, 0) + dt = location.tz.localize(dt) + azimuth = location.solar_azimuth(dt) + assert abs(azimuth - 126) < 0.5 + + +def test_Location_TimeAtElevation(): + dd = Astral() + location = dd['New Delhi'] + + test_data = { + datetime.date(2016, 1, 5): datetime.datetime(2016, 1, 5, 10, 0), + } + + for day, cdt in test_data.items(): + cdt = location.tz.localize(cdt) + dt = location.time_at_elevation(28, date=day) + assert datetime_almost_equal(dt, cdt, seconds=600) + + +def test_Location_SolarDepression(): + c = Location(("Heidelberg", "Germany", 49.412, -8.71, "Europe/Berlin")) + c.solar_depression = 'nautical' + assert c.solar_depression == 12 + + c.solar_depression = 18 + assert c.solar_depression == 18 + + +def test_Location_Moon(): + d = datetime.date(2017, 12, 1) + c=Location() + assert c.moon_phase(date=d) == 11 + + +def test_Location_TzError(): + with raises(AttributeError): + c = Location() + c.tz = 1 diff -Nru astral-1.4/src/test/test_Moon.py astral-1.6.1/src/test/test_Moon.py --- astral-1.4/src/test/test_Moon.py 2017-01-13 09:21:56.000000000 +0000 +++ astral-1.6.1/src/test/test_Moon.py 2018-02-23 12:33:58.000000000 +0000 @@ -1,33 +1,46 @@ -# -*- coding: utf-8 -*- -import os -import sys -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) - -from astral import Astral - -import datetime - -def test_Astral_Moon_PhaseNumber(): - dates = { - datetime.date(2015, 12, 1): 19, - datetime.date(2015, 12, 2): 20, - datetime.date(2015, 12, 3): 21, - - datetime.date(2014, 12, 1): 9, - datetime.date(2014, 12, 2): 10, - datetime.date(2014, 1, 1): 0, - } - - a = Astral() - - for date_, moon in dates.items(): - assert a.moon_phase(date_) == moon - - -def test_Location_Moon_PhaseNumber(): - a = Astral() - d = datetime.date(2011, 1, 1) - - l = a['London'] - assert l.moon_phase(d) == 25 - \ No newline at end of file +# -*- coding: utf-8 -*- +import pytest + +from astral import Astral + +import datetime + +def test_Astral_Moon_PhaseNumber(): + dates = { + datetime.date(2015, 12, 1): 19, + datetime.date(2015, 12, 2): 20, + datetime.date(2015, 12, 3): 21, + + datetime.date(2014, 12, 1): 9, + datetime.date(2014, 12, 2): 10, + datetime.date(2014, 1, 1): 0, + } + + a = Astral() + + for date_, moon in dates.items(): + assert a.moon_phase(date_) == moon + + +def test_Location_Moon_PhaseNumber(): + a = Astral() + d = datetime.date(2011, 1, 1) + + l = a['London'] + assert l.moon_phase(d) == 25 + + +def test_Location_Moon_PhaseNumberAsFloat(): + a = Astral() + d = datetime.date(2011, 1, 1) + + l = a['London'] + assert l.moon_phase(d, float) == pytest.approx(25.3, abs=0.1) + + +def test_Location_Moon_Phase_BadRType(): + a = Astral() + d = datetime.date(2011, 1, 1) + + l = a['London'] + assert l.moon_phase(d, str) == 25 diff -Nru astral-1.4/src/test/test_Repr.py astral-1.6.1/src/test/test_Repr.py --- astral-1.4/src/test/test_Repr.py 1970-01-01 00:00:00.000000000 +0000 +++ astral-1.6.1/src/test/test_Repr.py 2018-02-23 12:33:58.000000000 +0000 @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +from astral import Astral, Location + +def test_LocationReprDefault(): + l = Location() + assert l.__repr__() == 'Greenwich/England, tz=Europe/London, lat=51.17, lon=0.00' + + +def test_LocationReprFull(): + l = Location(('London', 'England', 51.68, -0.05, 'Europe/London', 3)) + assert l.__repr__() == 'London/England, tz=Europe/London, lat=51.68, lon=-0.05' + + +def test_LocationReprNoRegion(): + l = Location(('London', None, 51.68, -0.05, 'Europe/London', 3)) + assert l.__repr__() == 'London, tz=Europe/London, lat=51.68, lon=-0.05' diff -Nru astral-1.4/src/test/test_Sun.py astral-1.6.1/src/test/test_Sun.py --- astral-1.4/src/test/test_Sun.py 2017-01-13 09:21:56.000000000 +0000 +++ astral-1.6.1/src/test/test_Sun.py 2018-02-23 12:33:58.000000000 +0000 @@ -1,300 +1,295 @@ -# -*- coding: utf-8 -*- -# Test data taken from http://www.timeanddate.com/sun/uk/london - -import os -import sys -sys.path.insert(0, - os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) - -import pytest -import pytz -import datetime -from astral import Astral, AstralError, SUN_RISING, SUN_SETTING - - -def float_almost_equal(value1, value2, diff=0.5): - return abs(value1 - value2) <= diff - - -def datetime_almost_equal(datetime1, datetime2, seconds=60): - dd = datetime1 - datetime2 - sd = (dd.days * 24 * 60 * 60) + dd.seconds - return abs(sd) <= seconds - - -def test_Astral_Dawn_Civil(): - a = Astral() - l = a['London'] - - test_data = { - datetime.date(2015, 12, 1): datetime.datetime(2015, 12, 1, 7, 4), - datetime.date(2015, 12, 2): datetime.datetime(2015, 12, 2, 7, 6), - datetime.date(2015, 12, 3): datetime.datetime(2015, 12, 3, 7, 7), - datetime.date(2015, 12, 12): datetime.datetime(2015, 12, 12, 7, 17), - datetime.date(2015, 12, 25): datetime.datetime(2015, 12, 25, 7, 25), - } - - for day, dawn in test_data.items(): - dawn = pytz.UTC.localize(dawn) - dawn_utc = a.dawn_utc(day, l.latitude, l.longitude) - assert datetime_almost_equal(dawn, dawn_utc) - - -def test_Astral_Dawn_Nautical(): - a = Astral() - a.solar_depression = 'nautical' - l = a['London'] - - test_data = { - datetime.date(2015, 12, 1): datetime.datetime(2015, 12, 1, 6, 22), - datetime.date(2015, 12, 2): datetime.datetime(2015, 12, 2, 6, 23), - datetime.date(2015, 12, 3): datetime.datetime(2015, 12, 3, 6, 24), - datetime.date(2015, 12, 12): datetime.datetime(2015, 12, 12, 6, 34), - datetime.date(2015, 12, 25): datetime.datetime(2015, 12, 25, 6, 42), - } - - for day, dawn in test_data.items(): - dawn = pytz.UTC.localize(dawn) - dawn_utc = a.dawn_utc(day, l.latitude, l.longitude) - assert datetime_almost_equal(dawn, dawn_utc) - - -def test_Astral_Sunrise(): - a = Astral() - l = a['London'] - - test_data = { - datetime.date(2015, 1, 1): datetime.datetime(2015, 1, 1, 8, 6), - datetime.date(2015, 12, 1): datetime.datetime(2015, 12, 1, 7, 43), - datetime.date(2015, 12, 2): datetime.datetime(2015, 12, 2, 7, 45), - datetime.date(2015, 12, 3): datetime.datetime(2015, 12, 3, 7, 46), - datetime.date(2015, 12, 12): datetime.datetime(2015, 12, 12, 7, 57), - datetime.date(2015, 12, 25): datetime.datetime(2015, 12, 25, 8, 5), - } - - for day, sunrise in test_data.items(): - sunrise = pytz.UTC.localize(sunrise) - sunrise_utc = a.sunrise_utc(day, l.latitude, l.longitude) - assert datetime_almost_equal(sunrise, sunrise_utc) - - -def test_Astral_Sunset(): - a = Astral() - l = a['London'] - - test_data = { - datetime.date(2015, 1, 1): datetime.datetime(2015, 1, 1, 16, 2), - datetime.date(2015, 12, 1): datetime.datetime(2015, 12, 1, 15, 55), - datetime.date(2015, 12, 2): datetime.datetime(2015, 12, 2, 15, 55), - datetime.date(2015, 12, 3): datetime.datetime(2015, 12, 3, 15, 54), - datetime.date(2015, 12, 12): datetime.datetime(2015, 12, 12, 15, 51), - datetime.date(2015, 12, 25): datetime.datetime(2015, 12, 25, 15, 56), - } - - for day, sunset in test_data.items(): - sunset = pytz.UTC.localize(sunset) - sunset_utc = a.sunset_utc(day, l.latitude, l.longitude) - assert datetime_almost_equal(sunset, sunset_utc) - - -def test_Astral_Dusk_Civil(): - a = Astral() - l = a['London'] - - test_data = { - datetime.date(2015, 12, 1): datetime.datetime(2015, 12, 1, 16, 34), - datetime.date(2015, 12, 2): datetime.datetime(2015, 12, 2, 16, 34), - datetime.date(2015, 12, 3): datetime.datetime(2015, 12, 3, 16, 33), - datetime.date(2015, 12, 12): datetime.datetime(2015, 12, 12, 16, 31), - datetime.date(2015, 12, 25): datetime.datetime(2015, 12, 25, 16, 36), - } - - for day, dusk in test_data.items(): - dusk = pytz.UTC.localize(dusk) - dusk_utc = a.dusk_utc(day, l.latitude, l.longitude) - assert datetime_almost_equal(dusk, dusk_utc) - - -def test_Astral_Dusk_Nautical(): - a = Astral() - a.solar_depression = 'nautical' - l = a['London'] - - test_data = { - datetime.date(2015, 12, 1): datetime.datetime(2015, 12, 1, 17, 16), - datetime.date(2015, 12, 2): datetime.datetime(2015, 12, 2, 17, 16), - datetime.date(2015, 12, 3): datetime.datetime(2015, 12, 3, 17, 16), - datetime.date(2015, 12, 12): datetime.datetime(2015, 12, 12, 17, 14), - datetime.date(2015, 12, 25): datetime.datetime(2015, 12, 25, 17, 19), - } - - for day, dusk in test_data.items(): - dusk = pytz.UTC.localize(dusk) - dusk_utc = a.dusk_utc(day, l.latitude, l.longitude) - assert datetime_almost_equal(dusk, dusk_utc) - - -def test_Astral_SolarNoon(): - a = Astral() - l = a['London'] - - test_data = { - datetime.date(2015, 12, 1): datetime.datetime(2015, 12, 1, 11, 49), - datetime.date(2015, 12, 2): datetime.datetime(2015, 12, 2, 11, 50), - datetime.date(2015, 12, 3): datetime.datetime(2015, 12, 3, 11, 50), - datetime.date(2015, 12, 12): datetime.datetime(2015, 12, 12, 11, 54), - datetime.date(2015, 12, 25): datetime.datetime(2015, 12, 25, 12, 00), - } - - for day, solar_noon in test_data.items(): - solar_noon = pytz.UTC.localize(solar_noon) - solar_noon_utc = a.solar_noon_utc(day, l.longitude) - assert datetime_almost_equal(solar_noon, solar_noon_utc) - - -def test_Astral_SolarMidnight(): - a = Astral() - l = a['London'] - - test_data = { - datetime.date(2016, 2, 18): datetime.datetime(2016, 2, 18, 0, 14), - datetime.date(2016, 10, 26): datetime.datetime(2016, 10, 25, 23, 44), - } - - for day, solar_midnight in test_data.items(): - solar_midnight = pytz.UTC.localize(solar_midnight) - solar_midnight_utc = a.solar_midnight_utc(day, l.longitude) - assert datetime_almost_equal(solar_midnight, solar_midnight_utc) - - -# Test data from http://www.astroloka.com/rahukaal.aspx?City=Delhi -def test_Astral_Rahukaalam(): - a = Astral() - l = a['New Delhi'] - - test_data = { - datetime.date(2015, 12, 1): (datetime.datetime(2015, 12, 1, 9, 17), datetime.datetime(2015, 12, 1, 10, 35)), - datetime.date(2015, 12, 2): (datetime.datetime(2015, 12, 2, 6, 40), datetime.datetime(2015, 12, 2, 7, 58)), - } - - for day, (start, end) in test_data.items(): - start = pytz.UTC.localize(start) - end = pytz.UTC.localize(end) - - info = a.rahukaalam_utc(day, l.latitude, l.longitude) - start_utc = info[0] - end_utc = info[1] - assert datetime_almost_equal(start, start_utc) - assert datetime_almost_equal(end, end_utc) - - -def test_Astral_SolarElevation(): - a = Astral() - l = a['London'] - - test_data = { - datetime.datetime(2015, 12, 14, 11, 0, 0): 14, - datetime.datetime(2015, 12, 14, 20, 1, 0): -37, - } - - for dt, angle1 in test_data.items(): - angle2 = a.solar_elevation(dt, l.latitude, l.longitude) - assert float_almost_equal(angle1, angle2) - - -def test_Astral_SolarAzimuth(): - a = Astral() - l = a['London'] - - test_data = { - datetime.datetime(2015, 12, 14, 11, 0, 0, tzinfo=pytz.UTC): 167, - datetime.datetime(2015, 12, 14, 20, 1, 0): 279, - } - - for dt, angle1 in test_data.items(): - angle2 = a.solar_azimuth(dt, l.latitude, l.longitude) - assert float_almost_equal(angle1, angle2) - - -def test_Astral_TimeAtElevation_SunRising(): - a = Astral() - l = a['London'] - - d = datetime.date(2016, 1, 4) - dt = a.time_at_elevation_utc(6, SUN_RISING, d, l.latitude, l.longitude) - cdt = datetime.datetime(2016, 1, 4, 9, 5, 0, tzinfo=pytz.UTC) - # Use error of 5 minutes as website has a rather coarse accuracy - assert datetime_almost_equal(dt, cdt, 300) - - -def test_Astral_TimeAtElevation_SunSetting(): - a = Astral() - l = a['London'] - - d = datetime.date(2016, 1, 4) - dt = a.time_at_elevation_utc(14, SUN_SETTING, d, l.latitude, l.longitude) - cdt = datetime.datetime(2016, 1, 4, 13, 20, 0, tzinfo=pytz.UTC) - assert datetime_almost_equal(dt, cdt, 300) - - -def test_Astral_TimeAtElevation_GreaterThan90(): - a = Astral() - l = a['London'] - - d = datetime.date(2016, 1, 4) - dt = a.time_at_elevation_utc(166, SUN_RISING, d, l.latitude, l.longitude) - cdt = datetime.datetime(2016, 1, 4, 13, 20, 0, tzinfo=pytz.UTC) - assert datetime_almost_equal(dt, cdt, 300) - - -def test_Astral_TimeAtElevation_GreaterThan180(): - a = Astral() - l = a['London'] - - d = datetime.date(2015, 12, 1) - dt = a.time_at_elevation_utc(186, SUN_RISING, d, l.latitude, l.longitude) - cdt = datetime.datetime(2015, 12, 1, 16, 34, tzinfo=pytz.UTC) - assert datetime_almost_equal(dt, cdt, 300) - - -def test_Astral_TimeAtElevation_SunRisingBelowHorizon(): - a = Astral() - l = a['London'] - - d = datetime.date(2016, 1, 4) - dt = a.time_at_elevation_utc(-18, SUN_RISING, d, l.latitude, l.longitude) - cdt = datetime.datetime(2016, 1, 4, 6, 0, 0, tzinfo=pytz.UTC) - assert datetime_almost_equal(dt, cdt, 300) - - -def test_Astral_TimeAtElevation_BadElevation(): - a = Astral() - l = a['London'] - - d = datetime.date(2016, 1, 4) - with pytest.raises(AstralError): - a.time_at_elevation_utc(20, SUN_RISING, d, l.latitude, l.longitude) - - -def test_Astral_Daylight(): - a = Astral() - l = a['London'] - - d = datetime.date(2016, 1, 6) - start, end = a.daylight_utc(d, l.latitude, l.longitude) - cstart = datetime.datetime(2016, 1, 6, 8, 5, 0, tzinfo=pytz.UTC) - cend = datetime.datetime(2016, 1, 6, 16, 7, 0, tzinfo=pytz.UTC) - assert datetime_almost_equal(start, cstart, 300) - assert datetime_almost_equal(end, cend, 300) - - -def test_Astral_Nighttime(): - a = Astral() - l = a['London'] - - d = datetime.date(2016, 1, 6) - start, end = a.night_utc(d, l.latitude, l.longitude) - cstart = datetime.datetime(2016, 1, 6, 18, 10, 0, tzinfo=pytz.UTC) - cend = datetime.datetime(2016, 1, 7, 6, 2, 0, tzinfo=pytz.UTC) - assert datetime_almost_equal(start, cstart, 300) - assert datetime_almost_equal(end, cend, 300) +# -*- coding: utf-8 -*- +# Test data taken from http://www.timeanddate.com/sun/uk/london + +import pytest +import pytz +import datetime +from astral import Astral, AstralError, SUN_RISING, SUN_SETTING + + +def float_almost_equal(value1, value2, diff=0.5): + return abs(value1 - value2) <= diff + + +def datetime_almost_equal(datetime1, datetime2, seconds=60): + dd = datetime1 - datetime2 + sd = (dd.days * 24 * 60 * 60) + dd.seconds + return abs(sd) <= seconds + + +def test_Astral_Dawn_Civil(): + a = Astral() + l = a['London'] + + test_data = { + datetime.date(2015, 12, 1): datetime.datetime(2015, 12, 1, 7, 4), + datetime.date(2015, 12, 2): datetime.datetime(2015, 12, 2, 7, 6), + datetime.date(2015, 12, 3): datetime.datetime(2015, 12, 3, 7, 7), + datetime.date(2015, 12, 12): datetime.datetime(2015, 12, 12, 7, 17), + datetime.date(2015, 12, 25): datetime.datetime(2015, 12, 25, 7, 25), + } + + for day, dawn in test_data.items(): + dawn = pytz.UTC.localize(dawn) + dawn_utc = a.dawn_utc(day, l.latitude, l.longitude) + assert datetime_almost_equal(dawn, dawn_utc) + + +def test_Astral_Dawn_Nautical(): + a = Astral() + a.solar_depression = 'nautical' + l = a['London'] + + test_data = { + datetime.date(2015, 12, 1): datetime.datetime(2015, 12, 1, 6, 22), + datetime.date(2015, 12, 2): datetime.datetime(2015, 12, 2, 6, 23), + datetime.date(2015, 12, 3): datetime.datetime(2015, 12, 3, 6, 24), + datetime.date(2015, 12, 12): datetime.datetime(2015, 12, 12, 6, 34), + datetime.date(2015, 12, 25): datetime.datetime(2015, 12, 25, 6, 42), + } + + for day, dawn in test_data.items(): + dawn = pytz.UTC.localize(dawn) + dawn_utc = a.dawn_utc(day, l.latitude, l.longitude) + assert datetime_almost_equal(dawn, dawn_utc) + + +def test_Astral_Sunrise(): + a = Astral() + l = a['London'] + + test_data = { + datetime.date(2015, 1, 1): datetime.datetime(2015, 1, 1, 8, 6), + datetime.date(2015, 12, 1): datetime.datetime(2015, 12, 1, 7, 43), + datetime.date(2015, 12, 2): datetime.datetime(2015, 12, 2, 7, 45), + datetime.date(2015, 12, 3): datetime.datetime(2015, 12, 3, 7, 46), + datetime.date(2015, 12, 12): datetime.datetime(2015, 12, 12, 7, 57), + datetime.date(2015, 12, 25): datetime.datetime(2015, 12, 25, 8, 5), + } + + for day, sunrise in test_data.items(): + sunrise = pytz.UTC.localize(sunrise) + sunrise_utc = a.sunrise_utc(day, l.latitude, l.longitude) + assert datetime_almost_equal(sunrise, sunrise_utc) + + +def test_Astral_Sunset(): + a = Astral() + l = a['London'] + + test_data = { + datetime.date(2015, 1, 1): datetime.datetime(2015, 1, 1, 16, 2), + datetime.date(2015, 12, 1): datetime.datetime(2015, 12, 1, 15, 55), + datetime.date(2015, 12, 2): datetime.datetime(2015, 12, 2, 15, 55), + datetime.date(2015, 12, 3): datetime.datetime(2015, 12, 3, 15, 54), + datetime.date(2015, 12, 12): datetime.datetime(2015, 12, 12, 15, 51), + datetime.date(2015, 12, 25): datetime.datetime(2015, 12, 25, 15, 56), + } + + for day, sunset in test_data.items(): + sunset = pytz.UTC.localize(sunset) + sunset_utc = a.sunset_utc(day, l.latitude, l.longitude) + assert datetime_almost_equal(sunset, sunset_utc) + + +def test_Astral_Dusk_Civil(): + a = Astral() + l = a['London'] + + test_data = { + datetime.date(2015, 12, 1): datetime.datetime(2015, 12, 1, 16, 34), + datetime.date(2015, 12, 2): datetime.datetime(2015, 12, 2, 16, 34), + datetime.date(2015, 12, 3): datetime.datetime(2015, 12, 3, 16, 33), + datetime.date(2015, 12, 12): datetime.datetime(2015, 12, 12, 16, 31), + datetime.date(2015, 12, 25): datetime.datetime(2015, 12, 25, 16, 36), + } + + for day, dusk in test_data.items(): + dusk = pytz.UTC.localize(dusk) + dusk_utc = a.dusk_utc(day, l.latitude, l.longitude) + assert datetime_almost_equal(dusk, dusk_utc) + + +def test_Astral_Dusk_Nautical(): + a = Astral() + a.solar_depression = 'nautical' + l = a['London'] + + test_data = { + datetime.date(2015, 12, 1): datetime.datetime(2015, 12, 1, 17, 16), + datetime.date(2015, 12, 2): datetime.datetime(2015, 12, 2, 17, 16), + datetime.date(2015, 12, 3): datetime.datetime(2015, 12, 3, 17, 16), + datetime.date(2015, 12, 12): datetime.datetime(2015, 12, 12, 17, 14), + datetime.date(2015, 12, 25): datetime.datetime(2015, 12, 25, 17, 19), + } + + for day, dusk in test_data.items(): + dusk = pytz.UTC.localize(dusk) + dusk_utc = a.dusk_utc(day, l.latitude, l.longitude) + assert datetime_almost_equal(dusk, dusk_utc) + + +def test_Astral_SolarNoon(): + a = Astral() + l = a['London'] + + test_data = { + datetime.date(2015, 12, 1): datetime.datetime(2015, 12, 1, 11, 49), + datetime.date(2015, 12, 2): datetime.datetime(2015, 12, 2, 11, 50), + datetime.date(2015, 12, 3): datetime.datetime(2015, 12, 3, 11, 50), + datetime.date(2015, 12, 12): datetime.datetime(2015, 12, 12, 11, 54), + datetime.date(2015, 12, 25): datetime.datetime(2015, 12, 25, 12, 00), + } + + for day, solar_noon in test_data.items(): + solar_noon = pytz.UTC.localize(solar_noon) + solar_noon_utc = a.solar_noon_utc(day, l.longitude) + assert datetime_almost_equal(solar_noon, solar_noon_utc) + + +def test_Astral_SolarMidnight(): + a = Astral() + l = a['London'] + + test_data = { + datetime.date(2016, 2, 18): datetime.datetime(2016, 2, 18, 0, 14), + datetime.date(2016, 10, 26): datetime.datetime(2016, 10, 25, 23, 44), + } + + for day, solar_midnight in test_data.items(): + solar_midnight = pytz.UTC.localize(solar_midnight) + solar_midnight_utc = a.solar_midnight_utc(day, l.longitude) + assert datetime_almost_equal(solar_midnight, solar_midnight_utc) + + +# Test data from http://www.astroloka.com/rahukaal.aspx?City=Delhi +def test_Astral_Rahukaalam(): + a = Astral() + l = a['New Delhi'] + + test_data = { + datetime.date(2015, 12, 1): (datetime.datetime(2015, 12, 1, 9, 17), datetime.datetime(2015, 12, 1, 10, 35)), + datetime.date(2015, 12, 2): (datetime.datetime(2015, 12, 2, 6, 40), datetime.datetime(2015, 12, 2, 7, 58)), + } + + for day, (start, end) in test_data.items(): + start = pytz.UTC.localize(start) + end = pytz.UTC.localize(end) + + info = a.rahukaalam_utc(day, l.latitude, l.longitude) + start_utc = info[0] + end_utc = info[1] + assert datetime_almost_equal(start, start_utc) + assert datetime_almost_equal(end, end_utc) + + +def test_Astral_SolarElevation(): + a = Astral() + l = a['London'] + + test_data = { + datetime.datetime(2015, 12, 14, 11, 0, 0): 14, + datetime.datetime(2015, 12, 14, 20, 1, 0): -37, + } + + for dt, angle1 in test_data.items(): + angle2 = a.solar_elevation(dt, l.latitude, l.longitude) + assert float_almost_equal(angle1, angle2) + + +def test_Astral_SolarAzimuth(): + a = Astral() + l = a['London'] + + test_data = { + datetime.datetime(2015, 12, 14, 11, 0, 0, tzinfo=pytz.UTC): 167, + datetime.datetime(2015, 12, 14, 20, 1, 0): 279, + } + + for dt, angle1 in test_data.items(): + angle2 = a.solar_azimuth(dt, l.latitude, l.longitude) + assert float_almost_equal(angle1, angle2) + + +def test_Astral_TimeAtElevation_SunRising(): + a = Astral() + l = a['London'] + + d = datetime.date(2016, 1, 4) + dt = a.time_at_elevation_utc(6, SUN_RISING, d, l.latitude, l.longitude) + cdt = datetime.datetime(2016, 1, 4, 9, 5, 0, tzinfo=pytz.UTC) + # Use error of 5 minutes as website has a rather coarse accuracy + assert datetime_almost_equal(dt, cdt, 300) + + +def test_Astral_TimeAtElevation_SunSetting(): + a = Astral() + l = a['London'] + + d = datetime.date(2016, 1, 4) + dt = a.time_at_elevation_utc(14, SUN_SETTING, d, l.latitude, l.longitude) + cdt = datetime.datetime(2016, 1, 4, 13, 20, 0, tzinfo=pytz.UTC) + assert datetime_almost_equal(dt, cdt, 300) + + +def test_Astral_TimeAtElevation_GreaterThan90(): + a = Astral() + l = a['London'] + + d = datetime.date(2016, 1, 4) + dt = a.time_at_elevation_utc(166, SUN_RISING, d, l.latitude, l.longitude) + cdt = datetime.datetime(2016, 1, 4, 13, 20, 0, tzinfo=pytz.UTC) + assert datetime_almost_equal(dt, cdt, 300) + + +def test_Astral_TimeAtElevation_GreaterThan180(): + a = Astral() + l = a['London'] + + d = datetime.date(2015, 12, 1) + dt = a.time_at_elevation_utc(186, SUN_RISING, d, l.latitude, l.longitude) + cdt = datetime.datetime(2015, 12, 1, 16, 34, tzinfo=pytz.UTC) + assert datetime_almost_equal(dt, cdt, 300) + + +def test_Astral_TimeAtElevation_SunRisingBelowHorizon(): + a = Astral() + l = a['London'] + + d = datetime.date(2016, 1, 4) + dt = a.time_at_elevation_utc(-18, SUN_RISING, d, l.latitude, l.longitude) + cdt = datetime.datetime(2016, 1, 4, 6, 0, 0, tzinfo=pytz.UTC) + assert datetime_almost_equal(dt, cdt, 300) + + +def test_Astral_TimeAtElevation_BadElevation(): + a = Astral() + l = a['London'] + + d = datetime.date(2016, 1, 4) + with pytest.raises(AstralError): + a.time_at_elevation_utc(20, SUN_RISING, d, l.latitude, l.longitude) + + +def test_Astral_Daylight(): + a = Astral() + l = a['London'] + + d = datetime.date(2016, 1, 6) + start, end = a.daylight_utc(d, l.latitude, l.longitude) + cstart = datetime.datetime(2016, 1, 6, 8, 5, 0, tzinfo=pytz.UTC) + cend = datetime.datetime(2016, 1, 6, 16, 7, 0, tzinfo=pytz.UTC) + assert datetime_almost_equal(start, cstart, 300) + assert datetime_almost_equal(end, cend, 300) + + +def test_Astral_Nighttime(): + a = Astral() + l = a['London'] + + d = datetime.date(2016, 1, 6) + start, end = a.night_utc(d, l.latitude, l.longitude) + cstart = datetime.datetime(2016, 1, 6, 18, 10, 0, tzinfo=pytz.UTC) + cend = datetime.datetime(2016, 1, 7, 6, 2, 0, tzinfo=pytz.UTC) + assert datetime_almost_equal(start, cstart, 300) + assert datetime_almost_equal(end, cend, 300) diff -Nru astral-1.4/src/test/test_UnicodeLiteral.py astral-1.6.1/src/test/test_UnicodeLiteral.py --- astral-1.4/src/test/test_UnicodeLiteral.py 2017-01-13 09:21:56.000000000 +0000 +++ astral-1.6.1/src/test/test_UnicodeLiteral.py 2018-02-23 12:33:58.000000000 +0000 @@ -1,36 +1,30 @@ -# -*- coding: utf-8 -*- - -# Import unicode_literals as that caused problems with the code -# See bug https://bugs.launchpad.net/astral/+bug/1588198 -from __future__ import unicode_literals -import os -import sys -sys.path.insert(0, - os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) - -import pytest -import astral - -@pytest.mark.py2only -def test_Location_WithUnicodeLiteral(): - a = astral.Astral() - _l = a['London'] - - -@pytest.mark.py2only -def test_Latitude_WithUnicodeLiteral(): - l = astral.Location(('a place', 'a region', 1, 1)) - l.latitude = "24°28'N" - - -@pytest.mark.py2only -def test_Longitude_WithUnicodeLiteral(): - l = astral.Location(('a place', 'a region', 1, 1)) - l.longitude = "54°22'E" - - -@pytest.mark.py2only -def test_Depression_WithUnicodeLiteral(): - l = astral.Location(('a place', 'a region', 1, 1)) - l.solar_depression = 'civil' - \ No newline at end of file +# -*- coding: utf-8 -*- + +# Import unicode_literals as that caused problems with the code +# See bug https://bugs.launchpad.net/astral/+bug/1588198 +from __future__ import unicode_literals +import pytest +import astral + +@pytest.mark.py2only +def test_Location_WithUnicodeLiteral(): + a = astral.Astral() + _l = a['London'] + + +@pytest.mark.py2only +def test_Latitude_WithUnicodeLiteral(): + l = astral.Location(('a place', 'a region', 1, 1)) + l.latitude = "24°28'N" + + +@pytest.mark.py2only +def test_Longitude_WithUnicodeLiteral(): + l = astral.Location(('a place', 'a region', 1, 1)) + l.longitude = "54°22'E" + + +@pytest.mark.py2only +def test_Depression_WithUnicodeLiteral(): + l = astral.Location(('a place', 'a region', 1, 1)) + l.solar_depression = 'civil'