././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1667670964.116485 astral-3.2/LICENSE0000644000000000000000000002613514331521664010613 0ustar00 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1667670964.116485 astral-3.2/ReadMe.md0000644000000000000000000000075614331521664011266 0ustar00# 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. - Moon rise, set, azimuth and zenith. - The phase of the moon. For documentation see https://sffjunkie.github.io/astral/ ## Package Status ![GitHub Workflow Status](https://img.shields.io/github/workflow/status/sffjunkie/astral/astral-test) ![PyPI - Downloads](https://img.shields.io/pypi/dm/astral) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1667670964.116485 astral-3.2/pyproject.toml0000644000000000000000000000305114331521664012512 0ustar00[tool.poetry] name = "astral" version = "3.2" description = "Calculations for the position of the sun and moon." keywords = ["sun", "moon", "sunrise", "sunset", "dawn", "dusk"] authors = ["Simon Kennedy "] license = "Apache-2.0" readme = "ReadMe.md" documentation = "https://sffjunkie.github.io/astral" repository = "https://github.com/sffjunkie/astral" classifiers = [ "Intended Audience :: Developers", "Programming Language :: Python", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", ] packages = [{ include = "astral", from = "src" }] include = ["src/doc", "src/test"] [tool.poetry.dependencies] python = "^3.7" "backports.zoneinfo" = { version = "*", markers = "python_version < '3.9'" } tzdata = { version = "*", markers = "sys_platform == 'win32'" } [tool.poetry.dev-dependencies] freezegun = "*" pytest = "*" tox = "*" pylint = "^2.4.4" flake8 = "*" [tool.poetry.group.docs.dependencies] sphinx-book-theme = "^0.3.3" [tool.poetry.group.dev.dependencies] pre-commit = "^2.20.0" [tool.pytest.ini_options] pythonpath = ["src"] junit_family = "xunit2" cache_dir = "~/.cache/pytest" norecursedirs = [ "__pycache__", ".cache", ".mypy_cache", ".pytest_cache", ".eggs", ".git", ".settings", ".venv", ".vscode", "dist", "build", ] [tool.mypy] cache_dir = "~/.cache/mypy" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1667313041.2108524 astral-3.2/src/astral/__init__.py0000644000000000000000000002154714330226621013770 0ustar00# -*- coding: utf-8 -*- # Copyright 2009-2021, 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` package provides the means to calculate the following times of the sun * dawn * sunrise * noon * midnight * sunset * dusk * daylight * night * twilight * blue hour * golden hour * rahukaalam * moon rise, set, azimuth and zenith plus solar azimuth and elevation at a specific latitude/longitude. It can also calculate the moon phase for a specific date. The package also provides a self contained geocoder to turn a small set of location names into timezone, latitude and longitude. The lookups can be perfomed using the :func:`~astral.geocoder.lookup` function defined in :mod:`astral.geocoder` """ import datetime import re from dataclasses import dataclass, field from enum import Enum from math import radians, tan from typing import Optional, Tuple, Union try: import zoneinfo except ImportError: from backports import zoneinfo __all__ = [ "Depression", "SunDirection", "Observer", "LocationInfo", "AstralBodyPosition", "now", "today", "dms_to_float", "refraction_at_zenith", ] __version__ = "3.2" __author__ = "Simon Kennedy " TimePeriod = Tuple[datetime.datetime, datetime.datetime] Elevation = Union[float, Tuple[float, float]] Degrees = float Radians = float Minutes = float def now(tz: Optional[datetime.tzinfo] = None) -> datetime.datetime: """Returns the current time in the specified time zone""" now_utc = datetime.datetime.now(datetime.timezone.utc) if tz is None: return now_utc return now_utc.astimezone(tz) def today(tz: Optional[datetime.tzinfo] = None) -> datetime.date: """Returns the current date in the specified time zone""" return now(tz).date() def dms_to_float( dms: Union[str, float, Elevation], limit: Optional[float] = None ) -> float: """Converts as string of the form `degrees°minutes'seconds"[N|S|E|W]`, or a float encoded as a string, to a float N and E return positive values S and W return negative values Args: dms: string to convert limit: Limit the value between ± `limit` Returns: The number of degrees as a float """ try: res = float(dms) # type: ignore except (ValueError, TypeError) as exc: _dms_re = r"(?P\d{1,3})[°]((?P\d{1,2})[′'])?((?P\d{1,2})[″\"])?(?P[NSEW])?" # noqa dms_match = re.match(_dms_re, str(dms), flags=re.IGNORECASE) if dms_match: deg = dms_match.group("deg") or 0.0 min_ = dms_match.group("min") or 0.0 sec = dms_match.group("sec") or 0.0 dir_ = dms_match.group("dir") or "E" res = float(deg) if min_: res += float(min_) / 60 if sec: res += float(sec) / 3600 if dir_.upper() in ["S", "W"]: res = -res else: raise ValueError( "Unable to convert degrees/minutes/seconds to float" ) from exc if limit is not None: if res > limit: res = limit elif res < -limit: res = -limit return res def hours_to_time(value: float) -> datetime.time: """Convert a floating point number of hours to a datetime.time""" hour = int(value) value -= hour value *= 60 minute = int(value) value -= minute value *= 60 second = int(value) value -= second microsecond = int(value * 1000000) return datetime.time(hour, minute, second, microsecond) def time_to_hours(value: datetime.time) -> float: """Convert a datetime.time to a floating point number of hours""" hours = 0.0 hours += value.hour hours += value.minute / 60 hours += value.second / 3600 hours += value.microsecond / 1000000 return hours def time_to_seconds(value: datetime.time) -> float: """Convert a datetime.time to a floating point number of seconds""" hours = time_to_hours(value) return hours * 3600 def refraction_at_zenith(zenith: float) -> float: """Calculate the degrees of refraction of the sun due to the sun's elevation.""" elevation = 90 - zenith if elevation >= 85.0: return 0 refraction_correction = 0.0 te = tan(radians(elevation)) if elevation > 5.0: refraction_correction = ( 58.1 / te - 0.07 / (te * te * te) + 0.000086 / (te * te * te * te * te) ) elif elevation > -0.575: step1 = -12.79 + elevation * 0.711 step2 = 103.4 + elevation * step1 step3 = -518.2 + elevation * step2 refraction_correction = 1735.0 + elevation * step3 else: refraction_correction = -20.774 / te refraction_correction = refraction_correction / 3600.0 return refraction_correction class Depression(Enum): """The depression angle in degrees for the dawn/dusk calculations""" CIVIL = 6 NAUTICAL = 12 ASTRONOMICAL = 18 class SunDirection(Enum): """Direction of the sun either RISING or SETTING""" RISING = 1 SETTING = -1 @dataclass class AstralBodyPosition: """The position of an astral body as seen from earth""" right_ascension: Radians = field(default_factory=float) declination: Radians = field(default_factory=float) distance: Radians = field(default_factory=float) @dataclass class Observer: """Defines the location of an observer on Earth. Latitude and longitude can be set either as a float or as a string. For strings they must be of the form degrees°minutes'seconds"[N|S|E|W] e.g. 51°31'N `minutes’` & `seconds”` are optional. Elevations are either * A float that is the elevation in metres above a location, if the nearest obscuring feature is the horizon * or a tuple of the elevation in metres and the distance in metres to the nearest obscuring feature. Args: latitude: Latitude - Northern latitudes should be positive longitude: Longitude - Eastern longitudes should be positive elevation: Elevation and/or distance to nearest obscuring feature in metres above/below the location. """ latitude: Degrees = 51.4733 longitude: Degrees = -0.0008333 elevation: Elevation = 0.0 def __setattr__(self, name: str, value: Union[str, float, Elevation]): if name == "latitude": value = dms_to_float(value, 90.0) elif name == "longitude": value = dms_to_float(value, 180.0) elif name == "elevation": if isinstance(value, tuple): value = (float(value[0]), float(value[1])) else: value = float(value) super().__setattr__(name, value) @dataclass class LocationInfo: """Defines a location on Earth. Latitude and longitude can be set either as a float or as a string. For strings they must be of the form degrees°minutes'seconds"[N|S|E|W] e.g. 51°31'N `minutes’` & `seconds”` are optional. Args: name: Location name (can be any string) region: Region location is in (can be any string) timezone: The location's time zone (a list of time zone names can be obtained from `zoneinfo.available_timezones`) latitude: Latitude - Northern latitudes should be positive longitude: Longitude - Eastern longitudes should be positive """ name: str = "Greenwich" region: str = "England" timezone: str = "Europe/London" latitude: Degrees = 51.4733 longitude: Degrees = -0.0008333 def __setattr__(self, name: str, value: Union[Degrees, str]): if name == "latitude": value = dms_to_float(value, 90.0) elif name == "longitude": value = dms_to_float(value, 180.0) super().__setattr__(name, value) @property def observer(self): """Return an Observer at this location""" return Observer(self.latitude, self.longitude, 0.0) @property def tzinfo(self): # type: ignore """Return a zoneinfo.ZoneInfo for this location""" return zoneinfo.ZoneInfo(self.timezone) # type: ignore @property def timezone_group(self): """Return the group a timezone is in""" return self.timezone.split("/", maxsplit=1)[0] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1666076747.2556775 astral-3.2/src/astral/__main__.py0000644000000000000000000000344214323450113013740 0ustar00import argparse import datetime import json from typing import Any, Dict from astral import LocationInfo, Observer, sun try: import zoneinfo except ImportError: from backports import zoneinfo options = argparse.ArgumentParser() options.add_argument( "-n", "--name", dest="name", default="Somewhere", help="Location name (free-form text)", ) options.add_argument( "-r", "--region", dest="region", default="On Earth", help="Region (free-form text)" ) options.add_argument( "-d", "--date", dest="date", help="Date to compute times for (yyyy-mm-dd)" ) options.add_argument("-t", "--tzname", help="Timezone name") options.add_argument("latitude", type=float, help="Location latitude (float)") options.add_argument("longitude", type=float, help="Location longitude (float)") options.add_argument( "elevation", nargs="?", type=float, default=0.0, help="Elevation in metres (float)" ) args = options.parse_args() loc = LocationInfo( args.name, args.region, args.tzname, args.latitude, args.longitude, ) obs = Observer(args.latitude, args.longitude, args.elevation) kwargs: Dict[str, Any] = {} kwargs["observer"] = obs if args.date is not None: try: kwargs["date"] = datetime.datetime.strptime(args.date, "%Y-%m-%d").date() except: # noqa: E722 kwargs["date"] = datetime.date.today() sun_as_str = {} format_str = "%Y-%m-%dT%H:%M:%S" if args.tzname is None: tzinfo = datetime.timezone.utc format_str += "Z" else: tzinfo = zoneinfo.ZoneInfo(loc.timezone) format_str += "%z" kwargs["tzinfo"] = tzinfo s = sun.sun(**kwargs) for key, value in s.items(): sun_as_str[key] = s[key].strftime(format_str) sun_as_str["timezone"] = tzinfo.tzname sun_as_str["location"] = f"{loc.name}, {loc.region}" print(json.dumps(sun_as_str)) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1666076747.2556775 astral-3.2/src/astral/geocoder.py0000644000000000000000000006302414323450113014011 0ustar00"""Astral geocoder is a database of locations stored within the package. To get the :class:`~astral.LocationInfo` for a location use the :func:`~astral.geocoder.lookup` function e.g. :: from astral.geocoder import lookup, database l = lookup("London", database()) All locations stored in the database can be accessed using the `all_locations` generator :: from astral.geocoder import all_locations for location in all_locations: print(location) """ from typing import Dict, Generator, List, Optional, Tuple, Union from astral import LocationInfo, dms_to_float __all__ = ["lookup", "database", "add_locations", "all_locations"] # region Location Info # name,region,timezone,latitude,longitude,elevation _LOCATION_INFO = """Abu Dhabi,UAE,Asia/Dubai,24°28'N,54°22'E Abu Dhabi,United Arab Emirates,Asia/Dubai,24°28'N,54°22'E Abuja,Nigeria,Africa/Lagos,09°05'N,07°32'E Accra,Ghana,Africa/Accra,05°35'N,00°06'W Addis Ababa,Ethiopia,Africa/Addis_Ababa,09°02'N,38°42'E Adelaide,Australia,Australia/Adelaide,34°56'S,138°36'E Al Jubail,Saudi Arabia,Asia/Riyadh,25°24'N,49°39'W Algiers,Algeria,Africa/Algiers,36°42'N,03°08'E Amman,Jordan,Asia/Amman,31°57'N,35°52'E Amsterdam,Netherlands,Europe/Amsterdam,52°23'N,04°54'E Andorra la Vella,Andorra,Europe/Andorra,42°31'N,01°32'E Ankara,Turkey,Europe/Istanbul,39°57'N,32°54'E Antananarivo,Madagascar,Indian/Antananarivo,18°55'S,47°31'E Apia,Samoa,Pacific/Apia,13°50'S,171°50'W Ashgabat,Turkmenistan,Asia/Ashgabat,38°00'N,57°50'E Asmara,Eritrea,Africa/Asmara,15°19'N,38°55'E Astana,Kazakhstan,Asia/Qyzylorda,51°10'N,71°30'E Asuncion,Paraguay,America/Asuncion,25°10'S,57°30'W Athens,Greece,Europe/Athens,37°58'N,23°46'E Avarua,Cook Islands,Etc/GMT-10,21°12'N,159°46'W Baghdad,Iraq,Asia/Baghdad,33°20'N,44°30'E Baku,Azerbaijan,Asia/Baku,40°29'N,49°56'E Bamako,Mali,Africa/Bamako,12°34'N,07°55'W Bandar Seri Begawan,Brunei Darussalam,Asia/Brunei,04°52'N,115°00'E Bangkok,Thailand,Asia/Bangkok,13°45'N,100°35'E Bangui,Central African Republic,Africa/Bangui,04°23'N,18°35'E Banjul,Gambia,Africa/Banjul,13°28'N,16°40'W Basse-Terre,Guadeloupe,America/Guadeloupe,16°00'N,61°44'W Basseterre,Saint Kitts and Nevis,America/St_Kitts,17°17'N,62°43'W Beijing,China,Asia/Harbin,39°55'N,116°20'E Beirut,Lebanon,Asia/Beirut,33°53'N,35°31'E Belfast,Northern Ireland,Europe/Belfast,54°36'N,5°56'W Belgrade,Yugoslavia,Europe/Belgrade,44°50'N,20°37'E Belmopan,Belize,America/Belize,17°18'N,88°30'W Berlin,Germany,Europe/Berlin,52°30'N,13°25'E Bern,Switzerland,Europe/Zurich,46°57'N,07°28'E Bishkek,Kyrgyzstan,Asia/Bishkek,42°54'N,74°46'E Bissau,Guinea-Bissau,Africa/Bissau,11°45'N,15°45'W Bloemfontein,South Africa,Africa/Johannesburg,29°12'S,26°07'E Bogota,Colombia,America/Bogota,04°34'N,74°00'W Brasilia,Brazil,Brazil/East,15°47'S,47°55'W Bratislava,Slovakia,Europe/Bratislava,48°10'N,17°07'E Brazzaville,Congo,Africa/Brazzaville,04°09'S,15°12'E Bridgetown,Barbados,America/Barbados,13°05'N,59°30'W Brisbane,Australia,Australia/Brisbane,27°30'S,153°01'E Brussels,Belgium,Europe/Brussels,50°51'N,04°21'E Bucharest,Romania,Europe/Bucharest,44°27'N,26°10'E Bucuresti,Romania,Europe/Bucharest,44°27'N,26°10'E Budapest,Hungary,Europe/Budapest,47°29'N,19°05'E Buenos Aires,Argentina,America/Buenos_Aires,34°62'S,58°44'W Bujumbura,Burundi,Africa/Bujumbura,03°16'S,29°18'E Cairo,Egypt,Africa/Cairo,30°01'N,31°14'E Canberra,Australia,Australia/Canberra,35°15'S,149°08'E Cape Town,South Africa,Africa/Johannesburg,33°55'S,18°22'E Caracas,Venezuela,America/Caracas,10°30'N,66°55'W Castries,Saint Lucia,America/St_Lucia,14°02'N,60°58'W Cayenne,French Guiana,America/Cayenne,05°05'N,52°18'W Charlotte Amalie,United States of Virgin Islands,America/Virgin,18°21'N,64°56'W Chisinau,Moldova,Europe/Chisinau,47°02'N,28°50'E Conakry,Guinea,Africa/Conakry,09°29'N,13°49'W Copenhagen,Denmark,Europe/Copenhagen,55°41'N,12°34'E Cotonou,Benin,Africa/Porto-Novo,06°23'N,02°42'E Dakar,Senegal,Africa/Dakar,14°34'N,17°29'W Damascus,Syrian Arab Republic,Asia/Damascus,33°30'N,36°18'E Dammam,Saudi Arabia,Asia/Riyadh,26°30'N,50°12'E Darwin,Australia,Australia/Darwin,12°26'S,130°50'E Dhaka,Bangladesh,Asia/Dhaka,23°43'N,90°26'E Dili,East Timor,Asia/Dili,08°29'S,125°34'E Djibouti,Djibouti,Africa/Djibouti,11°08'N,42°20'E Dodoma,United Republic of Tanzania,Africa/Dar_es_Salaam,06°08'S,35°45'E Doha,Qatar,Asia/Qatar,25°15'N,51°35'E Douglas,Isle Of Man,Europe/London,54°9'N,4°29'W Dublin,Ireland,Europe/Dublin,53°21'N,06°15'W Dushanbe,Tajikistan,Asia/Dushanbe,38°33'N,68°48'E El Aaiun,Morocco,UTC,27°9'N,13°12'W Fort-de-France,Martinique,America/Martinique,14°36'N,61°02'W Freetown,Sierra Leone,Africa/Freetown,08°30'N,13°17'W Funafuti,Tuvalu,Pacific/Funafuti,08°31'S,179°13'E Gaborone,Botswana,Africa/Gaborone,24°45'S,25°57'E George Town,Cayman Islands,America/Cayman,19°20'N,81°24'W Georgetown,Guyana,America/Guyana,06°50'N,58°12'W Gibraltar,Gibraltar,Europe/Gibraltar,36°9'N,5°21'W Guatemala,Guatemala,America/Guatemala,14°40'N,90°22'W Hanoi,Viet Nam,Asia/Saigon,21°05'N,105°55'E Harare,Zimbabwe,Africa/Harare,17°43'S,31°02'E Havana,Cuba,America/Havana,23°08'N,82°22'W Helsinki,Finland,Europe/Helsinki,60°15'N,25°03'E Hobart,Tasmania,Australia/Hobart,42°53'S,147°19'E Hong Kong,China,Asia/Hong_Kong,22°16'N,114°09'E Honiara,Solomon Islands,Pacific/Guadalcanal,09°27'S,159°57'E Islamabad,Pakistan,Asia/Karachi,33°40'N,73°10'E Jakarta,Indonesia,Asia/Jakarta,06°09'S,106°49'E Jerusalem,Israel,Asia/Jerusalem,31°47'N,35°12'E Juba,South Sudan,Africa/Juba,4°51'N,31°36'E Jubail,Saudi Arabia,Asia/Riyadh,27°02'N,49°39'E Kabul,Afghanistan,Asia/Kabul,34°28'N,69°11'E Kampala,Uganda,Africa/Kampala,00°20'N,32°30'E Kathmandu,Nepal,Asia/Kathmandu,27°45'N,85°20'E Khartoum,Sudan,Africa/Khartoum,15°31'N,32°35'E Kiev,Ukraine,Europe/Kiev,50°30'N,30°28'E Kigali,Rwanda,Africa/Kigali,01°59'S,30°04'E Kingston,Jamaica,America/Jamaica,18°00'N,76°50'W Kingston,Norfolk Island,Pacific/Norfolk,45°20'S,168°43'E Kingstown,Saint Vincent and the Grenadines,America/St_Vincent,13°10'N,61°10'W Kinshasa,Democratic Republic of the Congo,Africa/Kinshasa,04°20'S,15°15'E Koror,Palau,Pacific/Palau,07°20'N,134°28'E Kuala Lumpur,Malaysia,Asia/Kuala_Lumpur,03°09'N,101°41'E Kuwait,Kuwait,Asia/Kuwait,29°30'N,48°00'E La Paz,Bolivia,America/La_Paz,16°20'S,68°10'W Libreville,Gabon,Africa/Libreville,00°25'N,09°26'E Lilongwe,Malawi,Africa/Blantyre,14°00'S,33°48'E Lima,Peru,America/Lima,12°00'S,77°00'W Lisbon,Portugal,Europe/Lisbon,38°42'N,09°10'W Ljubljana,Slovenia,Europe/Ljubljana,46°04'N,14°33'E Lome,Togo,Africa/Lome,06°09'N,01°20'E London,England,Europe/London,51°28'24"N,00°00'3"W Luanda,Angola,Africa/Luanda,08°50'S,13°15'E Lusaka,Zambia,Africa/Lusaka,15°28'S,28°16'E Luxembourg,Luxembourg,Europe/Luxembourg,49°37'N,06°09'E Macau,Macao,Asia/Macau,22°12'N,113°33'E Madinah,Saudi Arabia,Asia/Riyadh,24°28'N,39°36'E Madrid,Spain,Europe/Madrid,40°25'N,03°45'W Majuro,Marshall Islands,Pacific/Majuro,7°4'N,171°16'E Makkah,Saudi Arabia,Asia/Riyadh,21°26'N,39°49'E Malabo,Equatorial Guinea,Africa/Malabo,03°45'N,08°50'E Male,Maldives,Indian/Maldives,04°00'N,73°28'E Mamoudzou,Mayotte,Indian/Mayotte,12°48'S,45°14'E Managua,Nicaragua,America/Managua,12°06'N,86°20'W Manama,Bahrain,Asia/Bahrain,26°10'N,50°30'E Manila,Philippines,Asia/Manila,14°40'N,121°03'E Maputo,Mozambique,Africa/Maputo,25°58'S,32°32'E Maseru,Lesotho,Africa/Maseru,29°18'S,27°30'E Masqat,Oman,Asia/Muscat,23°37'N,58°36'E Mbabane,Swaziland,Africa/Mbabane,26°18'S,31°06'E Mecca,Saudi Arabia,Asia/Riyadh,21°26'N,39°49'E Medina,Saudi Arabia,Asia/Riyadh,24°28'N,39°36'E Melbourne,Australia,Australia/Melbourne,37°48'S,144°57'E Mexico,Mexico,America/Mexico_City,19°20'N,99°10'W Minsk,Belarus,Europe/Minsk,53°52'N,27°30'E Mogadishu,Somalia,Africa/Mogadishu,02°02'N,45°25'E Monaco,Priciplality Of Monaco,Europe/Monaco,43°43'N,7°25'E Monrovia,Liberia,Africa/Monrovia,06°18'N,10°47'W Montevideo,Uruguay,America/Montevideo,34°50'S,56°11'W Moroni,Comoros,Indian/Comoro,11°40'S,43°16'E Moscow,Russian Federation,Europe/Moscow,55°45'N,37°35'E Moskva,Russian Federation,Europe/Moscow,55°45'N,37°35'E Mumbai,India,Asia/Kolkata,18°58'N,72°49'E Muscat,Oman,Asia/Muscat,23°37'N,58°32'E N'Djamena,Chad,Africa/Ndjamena,12°10'N,14°59'E Nairobi,Kenya,Africa/Nairobi,01°17'S,36°48'E Nassau,Bahamas,America/Nassau,25°05'N,77°20'W Naypyidaw,Myanmar,Asia/Rangoon,19°45'N,96°6'E New Delhi,India,Asia/Kolkata,28°37'N,77°13'E Ngerulmud,Palau,Pacific/Palau,7°30'N,134°37'E Niamey,Niger,Africa/Niamey,13°27'N,02°06'E Nicosia,Cyprus,Asia/Nicosia,35°10'N,33°25'E Nouakchott,Mauritania,Africa/Nouakchott,20°10'S,57°30'E Noumea,New Caledonia,Pacific/Noumea,22°17'S,166°30'E Nuku'alofa,Tonga,Pacific/Tongatapu,21°10'S,174°00'W Nuuk,Greenland,America/Godthab,64°10'N,51°35'W Oranjestad,Aruba,America/Aruba,12°32'N,70°02'W Oslo,Norway,Europe/Oslo,59°55'N,10°45'E Ottawa,Canada,US/Eastern,45°27'N,75°42'W Ouagadougou,Burkina Faso,Africa/Ouagadougou,12°15'N,01°30'W P'yongyang,Democratic People's Republic of Korea,Asia/Pyongyang,39°09'N,125°30'E Pago Pago,American Samoa,Pacific/Pago_Pago,14°16'S,170°43'W Palikir,Micronesia,Pacific/Ponape,06°55'N,158°09'E Panama,Panama,America/Panama,09°00'N,79°25'W Papeete,French Polynesia,Pacific/Tahiti,17°32'S,149°34'W Paramaribo,Suriname,America/Paramaribo,05°50'N,55°10'W Paris,France,Europe/Paris,48°50'N,02°20'E Perth,Australia,Australia/Perth,31°56'S,115°50'E Phnom Penh,Cambodia,Asia/Phnom_Penh,11°33'N,104°55'E Podgorica,Montenegro,Europe/Podgorica,42°28'N,19°16'E Port Louis,Mauritius,Indian/Mauritius,20°9'S,57°30'E Port Moresby,Papua New Guinea,Pacific/Port_Moresby,09°24'S,147°08'E Port-Vila,Vanuatu,Pacific/Efate,17°45'S,168°18'E Port-au-Prince,Haiti,America/Port-au-Prince,18°40'N,72°20'W Port of Spain,Trinidad and Tobago,America/Port_of_Spain,10°40'N,61°31'W Porto-Novo,Benin,Africa/Porto-Novo,06°23'N,02°42'E Prague,Czech Republic,Europe/Prague,50°05'N,14°22'E Praia,Cape Verde,Atlantic/Cape_Verde,15°02'N,23°34'W Pretoria,South Africa,Africa/Johannesburg,25°44'S,28°12'E Pristina,Albania,Europe/Tirane,42°40'N,21°10'E Quito,Ecuador,America/Guayaquil,00°15'S,78°35'W Rabat,Morocco,Africa/Casablanca,34°1'N,6°50'W Reykjavik,Iceland,Atlantic/Reykjavik,64°10'N,21°57'W Riga,Latvia,Europe/Riga,56°53'N,24°08'E Riyadh,Saudi Arabia,Asia/Riyadh,24°41'N,46°42'E Road Town,British Virgin Islands,America/Virgin,18°27'N,64°37'W Rome,Italy,Europe/Rome,41°54'N,12°29'E Roseau,Dominica,America/Dominica,15°20'N,61°24'W Saint Helier,Jersey,Etc/GMT,49°11'N,2°6'W Saint Pierre,Saint Pierre and Miquelon,America/Miquelon,46°46'N,56°12'W Saipan,Northern Mariana Islands,Pacific/Saipan,15°12'N,145°45'E Sana,Yemen,Asia/Aden,15°20'N,44°12'W Sana'a,Yemen,Asia/Aden,15°20'N,44°12'W San Jose,Costa Rica,America/Costa_Rica,09°55'N,84°02'W San Juan,Puerto Rico,America/Puerto_Rico,18°28'N,66°07'W San Marino,San Marino,Europe/San_Marino,43°55'N,12°30'E San Salvador,El Salvador,America/El_Salvador,13°40'N,89°10'W Santiago,Chile,America/Santiago,33°24'S,70°40'W Santo Domingo,Dominica Republic,America/Santo_Domingo,18°30'N,69°59'W Sao Tome,Sao Tome and Principe,Africa/Sao_Tome,00°10'N,06°39'E Sarajevo,Bosnia and Herzegovina,Europe/Sarajevo,43°52'N,18°26'E Seoul,Republic of Korea,Asia/Seoul,37°31'N,126°58'E Singapore,Republic of Singapore,Asia/Singapore,1°18'N,103°48'E Skopje,The Former Yugoslav Republic of Macedonia,Europe/Skopje,42°01'N,21°26'E Sofia,Bulgaria,Europe/Sofia,42°45'N,23°20'E Sri Jayawardenapura Kotte,Sri Lanka,Asia/Colombo,6°54'N,79°53'E St. George's,Grenada,America/Grenada,32°22'N,64°40'W St. John's,Antigua and Barbuda,America/Antigua,17°7'N,61°51'W St. Peter Port,Guernsey,Europe/Guernsey,49°26'N,02°33'W Stanley,Falkland Islands,Atlantic/Stanley,51°40'S,59°51'W Stockholm,Sweden,Europe/Stockholm,59°20'N,18°05'E Sucre,Bolivia,America/La_Paz,16°20'S,68°10'W Suva,Fiji,Pacific/Fiji,18°06'S,178°30'E Sydney,Australia,Australia/Sydney,33°53'S,151°13'E Taipei,Republic of China (Taiwan),Asia/Taipei,25°02'N,121°38'E T'bilisi,Georgia,Asia/Tbilisi,41°43'N,44°50'E Tbilisi,Georgia,Asia/Tbilisi,41°43'N,44°50'E Tallinn,Estonia,Europe/Tallinn,59°22'N,24°48'E Tarawa,Kiribati,Pacific/Tarawa,01°30'N,173°00'E Tashkent,Uzbekistan,Asia/Tashkent,41°20'N,69°10'E Tegucigalpa,Honduras,America/Tegucigalpa,14°05'N,87°14'W Tehran,Iran,Asia/Tehran,35°44'N,51°30'E Thimphu,Bhutan,Asia/Thimphu,27°31'N,89°45'E Tirana,Albania,Europe/Tirane,41°18'N,19°49'E Tirane,Albania,Europe/Tirane,41°18'N,19°49'E Torshavn,Faroe Islands,Atlantic/Faroe,62°05'N,06°56'W Tokyo,Japan,Asia/Tokyo,35°41'N,139°41'E Tripoli,Libyan Arab Jamahiriya,Africa/Tripoli,32°49'N,13°07'E Tunis,Tunisia,Africa/Tunis,36°50'N,10°11'E Ulan Bator,Mongolia,Asia/Ulaanbaatar,47°55'N,106°55'E Ulaanbaatar,Mongolia,Asia/Ulaanbaatar,47°55'N,106°55'E Vaduz,Liechtenstein,Europe/Vaduz,47°08'N,09°31'E Valletta,Malta,Europe/Malta,35°54'N,14°31'E Vienna,Austria,Europe/Vienna,48°12'N,16°22'E Vientiane,Lao People's Democratic Republic,Asia/Vientiane,17°58'N,102°36'E Vilnius,Lithuania,Europe/Vilnius,54°38'N,25°19'E W. Indies,Antigua and Barbuda,America/Antigua,17°20'N,61°48'W Warsaw,Poland,Europe/Warsaw,52°13'N,21°00'E Washington DC,USA,US/Eastern,39°91'N,77°02'W Wellington,New Zealand,Pacific/Auckland,41°19'S,174°46'E Willemstad,Netherlands Antilles,America/Curacao,12°05'N,69°00'W Windhoek,Namibia,Africa/Windhoek,22°35'S,17°04'E Yamoussoukro,Cote d'Ivoire,Africa/Abidjan,06°49'N,05°17'W Yangon,Myanmar,Asia/Rangoon,16°45'N,96°20'E Yaounde,Cameroon,Africa/Douala,03°50'N,11°35'E Yaren,Nauru,Pacific/Nauru,0°32'S,166°55'E Yerevan,Armenia,Asia/Yerevan,40°10'N,44°31'E Zagreb,Croatia,Europe/Zagreb,45°50'N,15°58'E Zurich,Switzerland,Europe/Zurich,47°22'N,08°33'E # UK Cities Aberdeen,Scotland,Europe/London,57°08'N,02°06'W Birmingham,England,Europe/London,52°30'N,01°50'W Bolton,England,Europe/London,53°35'N,02°15'W Bradford,England,Europe/London,53°47'N,01°45'W Bristol,England,Europe/London,51°28'N,02°35'W Cardiff,Wales,Europe/London,51°29'N,03°13'W Crawley,England,Europe/London,51°8'N,00°10'W Edinburgh,Scotland,Europe/London,55°57'N,03°13'W Glasgow,Scotland,Europe/London,55°50'N,04°15'W Greenwich,England,Europe/London,51°28'N,00°00'W Leeds,England,Europe/London,53°48'N,01°35'W Leicester,England,Europe/London,52°38'N,01°08'W Liverpool,England,Europe/London,53°25'N,03°00'W Manchester,England,Europe/London,53°30'N,02°15'W Newcastle Upon Tyne,England,Europe/London,54°59'N,01°36'W Newcastle,England,Europe/London,54°59'N,01°36'W Norwich,England,Europe/London,52°38'N,01°18'E Oxford,England,Europe/London,51°45'N,01°15'W Plymouth,England,Europe/London,50°25'N,04°15'W Portsmouth,England,Europe/London,50°48'N,01°05'W Reading,England,Europe/London,51°27'N,0°58'W Sheffield,England,Europe/London,53°23'N,01°28'W Southampton,England,Europe/London,50°55'N,01°25'W Swansea,England,Europe/London,51°37'N,03°57'W Swindon,England,Europe/London,51°34'N,01°47'W Wolverhampton,England,Europe/London,52°35'N,2°08'W Barrow-In-Furness,England,Europe/London,54°06'N,3°13'W # US State Capitals Montgomery,USA,US/Central,32°21'N,86°16'W Juneau,USA,US/Alaska,58°23'N,134°11'W Phoenix,USA,America/Phoenix,33°26'N,112°04'W Little Rock,USA,US/Central,34°44'N,92°19'W Sacramento,USA,US/Pacific,38°33'N,121°28'W Denver,USA,US/Mountain,39°44'N,104°59'W Hartford,USA,US/Eastern,41°45'N,72°41'W Dover,USA,US/Eastern,39°09'N,75°31'W Tallahassee,USA,US/Eastern,30°27'N,84°16'W Atlanta,USA,US/Eastern,33°45'N,84°23'W Honolulu,USA,US/Hawaii,21°18'N,157°49'W Boise,USA,US/Mountain,43°36'N,116°12'W Springfield,USA,US/Central,39°47'N,89°39'W Indianapolis,USA,US/Eastern,39°46'N,86°9'W Des Moines,USA,US/Central,41°35'N,93°37'W Topeka,USA,US/Central,39°03'N,95°41'W Frankfort,USA,US/Eastern,38°11'N,84°51'W Baton Rouge,USA,US/Central,30°27'N,91°8'W Augusta,USA,US/Eastern,44°18'N,69°46'W Annapolis,USA,US/Eastern,38°58'N,76°30'W Boston,USA,US/Eastern,42°21'N,71°03'W Lansing,USA,US/Eastern,42°44'N,84°32'W Saint Paul,USA,US/Central,44°56'N,93°05'W Jackson,USA,US/Central,32°17'N,90°11'W Jefferson City,USA,US/Central,38°34'N,92°10'W Helena,USA,US/Mountain,46°35'N,112°1'W Lincoln,USA,US/Central,40°48'N,96°40'W Carson City,USA,US/Pacific,39°9'N,119°45'W Concord,USA,US/Eastern,43°12'N,71°32'W Trenton,USA,US/Eastern,40°13'N,74°45'W Santa Fe,USA,US/Mountain,35°40'N,105°57'W Albany,USA,US/Eastern,42°39'N,73°46'W Raleigh,USA,US/Eastern,35°49'N,78°38'W Bismarck,USA,US/Central,46°48'N,100°46'W Columbus,USA,US/Eastern,39°59'N,82°59'W Oklahoma City,USA,US/Central,35°28'N,97°32'W Salem,USA,US/Pacific,44°55'N,123°1'W Harrisburg,USA,US/Eastern,40°16'N,76°52'W Providence,USA,US/Eastern,41°49'N,71°25'W Columbia,USA,US/Eastern,34°00'N,81°02'W Pierre,USA,US/Central,44°22'N,100°20'W Nashville,USA,US/Central,36°10'N,86°47'W Austin,USA,US/Central,30°16'N,97°45'W Salt Lake City,USA,US/Mountain,40°45'N,111°53'W Montpelier,USA,US/Eastern,44°15'N,72°34'W Richmond,USA,US/Eastern,37°32'N,77°25'W Olympia,USA,US/Pacific,47°2'N,122°53'W Charleston,USA,US/Eastern,38°20'N,81°38'W Madison,USA,US/Central,43°4'N,89°24'W Cheyenne,USA,US/Mountain,41°8'N,104°48'W # Major US Cities Birmingham,USA,US/Central,33°39'N,86°48'W Anchorage,USA,US/Alaska,61°13'N,149°53'W Los Angeles,USA,US/Pacific,34°03'N,118°15'W San Francisco,USA,US/Pacific,37°46'N,122°25'W Bridgeport,USA,US/Eastern,41°11'N,73°11'W Wilmington,USA,US/Eastern,39°44'N,75°32'W Jacksonville,USA,US/Eastern,30°19'N,81°39'W Miami,USA,US/Eastern,26°8'N,80°12'W Chicago,USA,US/Central,41°50'N,87°41'W Wichita,USA,US/Central,37°41'N,97°20'W Louisville,USA,US/Eastern,38°15'N,85°45'W New Orleans,USA,US/Central,29°57'N,90°4'W Portland,USA,US/Eastern,43°39'N,70°16'W Baltimore,USA,US/Eastern,39°17'N,76°37'W Detroit,USA,US/Eastern,42°19'N,83°2'W Minneapolis,USA,US/Central,44°58'N,93°15'W Kansas City,USA,US/Central,39°06'N,94°35'W Billings,USA,US/Mountain,45°47'N,108°32'W Omaha,USA,US/Central,41°15'N,96°0'W Las Vegas,USA,US/Pacific,36°10'N,115°08'W Manchester,USA,US/Eastern,42°59'N,71°27'W Newark,USA,US/Eastern,40°44'N,74°11'W Albuquerque,USA,US/Mountain,35°06'N,106°36'W New York,USA,US/Eastern,40°43'N,74°0'W Charlotte,USA,US/Eastern,35°13'N,80°50'W Fargo,USA,US/Central,46°52'N,96°47'W Cleveland,USA,US/Eastern,41°28'N,81°40'W Philadelphia,USA,US/Eastern,39°57'N,75°10'W Sioux Falls,USA,US/Central,43°32'N,96°43'W Memphis,USA,US/Central,35°07'N,89°58'W Houston,USA,US/Central,29°45'N,95°22'W Dallas,USA,US/Central,32°47'N,96°48'W Burlington,USA,US/Eastern,44°28'N,73°9'W Virginia Beach,USA,US/Eastern,36°50'N,76°05'W Seattle,USA,US/Pacific,47°36'N,122°19'W Milwaukee,USA,US/Central,43°03'N,87°57'W San Diego,USA,US/Pacific,32°42'N,117°09'W Orlando,USA,US/Eastern,28°32'N,81°22'W Buffalo,USA,US/Eastern,42°54'N,78°50'W Toledo,USA,US/Eastern,41°39'N,83°34'W # Canadian cities Vancouver,Canada,America/Vancouver,49°15'N,123°6'W Calgary,Canada,America/Edmonton,51°2'N,114°3'W Edmonton,Canada,America/Edmonton,53°32'N,113°29'W Saskatoon,Canada,America/Regina,52°8'N,106°40'W Regina,Canada,America/Regina,50°27'N,104°36'W Winnipeg,Canada,America/Winnipeg,49°53'N,97°8'W Toronto,Canada,America/Toronto,43°39'N,79°22'W Montreal,Canada,America/Montreal,45°30'N,73°33'W Quebec,Canada,America/Toronto,46°48'N,71°14'W Fredericton,Canada,America/Halifax,45°57'N,66°38'W Halifax,Canada,America/Halifax,44°38'N,63°34'W Charlottetown,Canada,America/Halifax,46°14'N,63°7'W St. John's,Canada,America/Halifax,47°33'N,52°42'W Whitehorse,Canada,America/Whitehorse,60°43'N,135°3'W Yellowknife,Canada,America/Yellowknife,62°27'N,114°22'W Iqaluit,Canada,America/Iqaluit,63°44'N,68°31'W """ # endregion GroupName = str LocationName = str GroupInfo = Dict[LocationName, List[LocationInfo]] LocationDatabase = Dict[GroupName, GroupInfo] def database() -> LocationDatabase: """Returns a database populated with the inital set of locations stored in this module """ db: LocationDatabase = {} _add_locations_from_str(_LOCATION_INFO, db) return db def _sanitize_key(key: str) -> str: """Sanitize the location or group key to look up Args: key: The key to sanitize """ return str(key).lower().replace(" ", "_") def _get_group(name: str, db: LocationDatabase) -> Optional[GroupInfo]: return db.get(name, None) def _add_location_to_db(location: LocationInfo, db: LocationDatabase) -> None: """Add a single location to a database""" key = _sanitize_key(location.timezone_group) group = _get_group(key, db) if not group: group = {} db[key] = group location_key = _sanitize_key(location.name) if location_key not in group: group[location_key] = [location] else: group[location_key].append(location) def _locationinfo_from_str(info: str) -> LocationInfo: idxable = info.split(",") return LocationInfo( name=idxable[0], region=idxable[1], timezone=idxable[2], latitude=dms_to_float(idxable[3], 90.0), longitude=dms_to_float(idxable[4], 180.0), ) def _locationinfo_from_indexable( idxable: Union[Tuple[str, ...], List[str]] ) -> LocationInfo: return LocationInfo( name=idxable[0], region=idxable[1], timezone=idxable[2], latitude=dms_to_float(idxable[3], 90.0), longitude=dms_to_float(idxable[4], 180.0), ) def _add_locations_from_str(location_string: str, db: LocationDatabase) -> None: """Add locations from a string.""" for line in location_string.split("\n"): line = line.strip() if line != "" and line[0] != "#": location = _locationinfo_from_str(line) _add_location_to_db(location, db) def _add_locations_from_list( location_list: Union[List[str], List[List[str]], List[Tuple[str, ...]]], db: LocationDatabase, ) -> None: """Add locations from a list of either strings or lists of strings or tuples of strings. """ for info in location_list: if isinstance(info, str): _add_locations_from_str(info, db) else: location = _locationinfo_from_indexable(info) _add_location_to_db(location, db) def add_locations( locations: Union[str, List[str], List[List[str]], List[Tuple[str, ...]]], db: LocationDatabase, ) -> None: """Add locations to the database. Locations can be added by passing either a string with one line per location or by passing a list containing strings, lists or tuples (lists and tuples are passed directly to the LocationInfo constructor).""" if isinstance(locations, str): _add_locations_from_str(locations, db) else: _add_locations_from_list(locations, db) def group(region: str, db: LocationDatabase) -> GroupInfo: """Access to each timezone group. For example London is in timezone group Europe. Lookups are case insensitive Args: region: the name to look up Raises: KeyError: if the location is not found """ key = _sanitize_key(region) for name, value in db.items(): if name == key: return value raise KeyError(f"Unrecognised Group - {region}") def lookup_in_group( location: str, group: Dict[str, List[LocationInfo]] ) -> LocationInfo: """Looks up the location within a group dictionary 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'] Lookups are case insensitive. Args: location: The location to look up group: The location group to look in Raises: KeyError: if the location is not found """ key = _sanitize_key(location) 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 group.items(): if location_name == lookup_name: if lookup_region == "": return location_list[0] for loc in location_list: if _sanitize_key(loc.region) == lookup_region: return loc raise KeyError(f"Unrecognised location name - {key}") def lookup(name: str, db: LocationDatabase) -> Union[GroupInfo, LocationInfo]: """Look up a name in a database. If a group with the name specified is a group name then that will be returned. If no group is found a location with the name will be looked up. Args: name: The group/location name to look up db: The location database to look in Raises: KeyError: if the name is not found """ key = _sanitize_key(name) for group_key, group in db.items(): if group_key == key: return group try: return lookup_in_group(name, group) except KeyError: pass raise KeyError(f"Unrecognised name - {name}") def all_locations(db: LocationDatabase) -> Generator[LocationInfo, None, None]: """A generator that returns all the :class:`~astral.LocationInfo`\\s contained in the database """ for group_info in db.values(): for location_list in group_info.values(): for location in location_list: yield location ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1666076747.2556775 astral-3.2/src/astral/julian.py0000644000000000000000000000632014323450113013500 0ustar00import datetime from enum import Enum from typing import Union class Calendar(Enum): GREGORIAN = 1 JULIAN = 2 def day_fraction_to_time(fraction: float) -> datetime.time: s = fraction * (24 * 60 * 60) h = int(s / (60 * 60)) s -= h * 60 * 60 m = int(s / 60) s -= m * 60 s = int(s) return datetime.time(h, m, s) def julianday( at: Union[datetime.datetime, datetime.date], calendar: Calendar = Calendar.GREGORIAN ) -> float: """Calculate the Julian Day (number) for the specified date/time julian day numbers for dates are calculated for the start of the day """ def _time_to_seconds(t: datetime.time) -> int: return int(t.hour * 3600 + t.minute * 60 + t.second) year = at.year month = at.month day = at.day day_fraction = 0 if isinstance(at, datetime.datetime): t = _time_to_seconds(at.time()) day_fraction = t / (24 * 60 * 60) else: day_fraction = 0 if month <= 2: year -= 1 month += 12 a = int(year / 100) if calendar == Calendar.GREGORIAN: b = 2 - a + int(a / 4) else: b = 0 jd = ( int(365.25 * (year + 4716)) + int(30.6001 * (month + 1)) + day + day_fraction + b - 1524.5 ) return jd def julianday_modified(at: datetime.datetime) -> float: """Calculate the Modified Julian Date number""" year = at.year month = at.month day = at.day a = 10000 * year + 100 * month + day if year < 0: year += 1 if month <= 2: month += 12 year -= 1 if a <= 15821004.1: b = -2 + (year + 4716) / 4 - 1179 else: b = (year / 400) - (year / 100) + (year / 4) a = 365 * year - 679004 mjd = a + b + int(30.6001 * (month + 1)) + day + at.hour / 24 return mjd def julianday_to_datetime(jd: float) -> datetime.datetime: """Convert a Julian Day number to a datetime""" jd += 0.5 z = int(jd) f = jd - z if z < 2299161: a = z else: alpha = int((z - 1867216.25) / 36524.25) a = z + 1 + alpha + int(alpha / 4.0) b = a + 1524 c = int((b - 122.1) / 365.25) d = int(365.25 * c) e = int((b - d) / 30.6001) d = b - d - int(30.6001 * e) + f day = int(d) t = d - day total_seconds = t * (24 * 60 * 60) hour = int(total_seconds / 3600) total_seconds -= hour * 3600 minute = int(total_seconds / 60) total_seconds -= minute * 60 seconds = int(total_seconds) if e < 14: month = e - 1 else: month = e - 13 if month > 2: year = c - 4716 else: year = c - 4715 return datetime.datetime(year, month, day, hour, minute, seconds) def julianday_to_juliancentury(julianday: float) -> float: """Convert a Julian Day number to a Julian Century""" return (julianday - 2451545.0) / 36525.0 def juliancentury_to_julianday(juliancentury: float) -> float: """Convert a Julian Century number to a Julian Day""" return (juliancentury * 36525.0) + 2451545.0 def julianday_2000(at: Union[datetime.datetime, datetime.date]) -> float: """Calculate the numer of Julian Days since Jan 1.5, 2000""" return julianday(at) - 2451545.0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1666076747.2556775 astral-3.2/src/astral/location.py0000644000000000000000000007507114323450113014037 0ustar00import dataclasses import datetime try: import zoneinfo except ImportError: from backports import zoneinfo from typing import Any, Dict, Optional, Tuple, Union import astral.moon import astral.sun from astral import ( Depression, Elevation, LocationInfo, Observer, SunDirection, dms_to_float, today, ) class Location: """Provides access to information for single location.""" def __init__(self, info: Optional[LocationInfo] = None): """Initializes the Location with a LocationInfo object. The tuple should contain items in the following order ================ ============= Field Default ================ ============= name Greenwich region England time zone name Europe/London latitude 51.4733 longitude -0.0008333 ================ ============= See the :attr:`timezone` property for a method of obtaining time zone names """ self._location_info: LocationInfo self._solar_depression: float = Depression.CIVIL.value if not info: self._location_info = LocationInfo( "Greenwich", "England", "Europe/London", 51.4733, -0.0008333 ) else: self._location_info = info def __eq__(self, other: object) -> bool: if type(other) is Location: return self._location_info == other._location_info # type: ignore return NotImplemented def __repr__(self) -> str: if self.region: _repr = "%s/%s" % (self.name, self.region) else: _repr = self.name return ( f"{_repr}, tz={self.timezone}, " f"lat={self.latitude:0.02f}, " f"lon={self.longitude:0.02f}" ) @property def info(self) -> LocationInfo: return LocationInfo( self.name, self.region, self.timezone, self.latitude, self.longitude, ) @property def observer(self) -> Observer: return Observer(self.latitude, self.longitude, 0.0) @property def name(self) -> str: return self._location_info.name @name.setter def name(self, name: str) -> None: self._location_info = dataclasses.replace(self._location_info, name=name) @property def region(self) -> str: return self._location_info.region @region.setter def region(self, region: str) -> None: self._location_info = dataclasses.replace(self._location_info, region=region) @property def latitude(self) -> float: """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._location_info.latitude @latitude.setter def latitude(self, latitude: Union[float, str]) -> None: self._location_info = dataclasses.replace( self._location_info, latitude=dms_to_float(latitude, 90.0) ) @property def longitude(self) -> float: """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._location_info.longitude @longitude.setter def longitude(self, longitude: Union[float, str]) -> None: self._location_info = dataclasses.replace( self._location_info, longitude=dms_to_float(longitude, 180.0) ) @property def timezone(self) -> str: """The name of the time zone for the location. A list of time zone names can be obtained from the zoneinfo module. For example. >>> import zoneinfo >>> assert "CET" in zoneinfo.available_timezones() """ return self._location_info.timezone @timezone.setter def timezone(self, name: str) -> None: if name not in zoneinfo.available_timezones(): # type: ignore raise ValueError("Timezone '%s' not recognized" % name) self._location_info = dataclasses.replace(self._location_info, timezone=name) @property def tzinfo(self) -> zoneinfo.ZoneInfo: # type: ignore """Time zone information.""" try: tz = zoneinfo.ZoneInfo(self._location_info.timezone) # type: ignore return tz # type: ignore except zoneinfo.ZoneInfoNotFoundError as exc: # type: ignore raise ValueError( "Unknown timezone '%s'" % self._location_info.timezone ) from exc tz = tzinfo @property def solar_depression(self) -> float: """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._solar_depression @solar_depression.setter def solar_depression(self, depression: Union[float, str, Depression]) -> None: if isinstance(depression, str): try: self._solar_depression = { "civil": 6.0, "nautical": 12.0, "astronomical": 18.0, }[depression] except KeyError: raise KeyError( ( "solar_depression must be either a number " "or one of 'civil', 'nautical' or " "'astronomical'" ) ) elif isinstance(depression, Depression): self._solar_depression = depression.value else: self._solar_depression = float(depression) def today(self, local: bool = True) -> datetime.date: if local: return today(self.tzinfo) else: return today() def sun( self, date: Optional[datetime.date] = None, local: bool = True, observer_elevation: Elevation = 0.0, ) -> Dict[str, Any]: """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 :param observer_elevation: Elevation of the observer in metres above the location. :returns: Dictionary with keys ``dawn``, ``sunrise``, ``noon``, ``sunset`` and ``dusk`` whose values are the results of the corresponding methods. """ if local and self.timezone is None: raise ValueError("Local time requested but Location has no timezone set.") if date is None: date = self.today(local) observer = Observer(self.latitude, self.longitude, observer_elevation) if local: return astral.sun.sun(observer, date, self.solar_depression, self.tzinfo) else: return astral.sun.sun(observer, date, self.solar_depression) def dawn( self, date: Optional[datetime.date] = None, local: bool = True, observer_elevation: Elevation = 0.0, ) -> datetime.datetime: """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 :param observer_elevation: Elevation of the observer in metres above the location. :returns: The date and time at which dawn occurs. """ if local and self.timezone is None: raise ValueError("Local time requested but Location has no timezone set.") if date is None: date = self.today(local) observer = Observer(self.latitude, self.longitude, observer_elevation) if local: return astral.sun.dawn(observer, date, self.solar_depression, self.tzinfo) else: return astral.sun.dawn(observer, date, self.solar_depression) def sunrise( self, date: Optional[datetime.date] = None, local: bool = True, observer_elevation: Elevation = 0.0, ) -> datetime.datetime: """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 :param observer_elevation: Elevation of the observer in metres above the location. :returns: The date and time at which sunrise occurs. """ if local and self.timezone is None: raise ValueError("Local time requested but Location has no timezone set.") if date is None: date = self.today(local) observer = Observer(self.latitude, self.longitude, observer_elevation) if local: return astral.sun.sunrise(observer, date, self.tzinfo) else: return astral.sun.sunrise(observer, date) def noon( self, date: Optional[datetime.date] = None, local: bool = True ) -> datetime.datetime: """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. """ if local and self.timezone is None: raise ValueError("Local time requested but Location has no timezone set.") if date is None: date = self.today(local) observer = Observer(self.latitude, self.longitude) if local: return astral.sun.noon(observer, date, self.tzinfo) else: return astral.sun.noon(observer, date) def sunset( self, date: Optional[datetime.date] = None, local: bool = True, observer_elevation: Elevation = 0.0, ) -> datetime.datetime: """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 :param observer_elevation: Elevation of the observer in metres above the location. :returns: The date and time at which sunset occurs. """ if local and self.timezone is None: raise ValueError("Local time requested but Location has no timezone set.") if date is None: date = self.today(local) observer = Observer(self.latitude, self.longitude, observer_elevation) if local: return astral.sun.sunset(observer, date, self.tzinfo) else: return astral.sun.sunset(observer, date) def dusk( self, date: Optional[datetime.date] = None, local: bool = True, observer_elevation: Elevation = 0.0, ) -> datetime.datetime: """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 :param observer_elevation: Elevation of the observer in metres above the location. :returns: The date and time at which dusk occurs. """ if local and self.timezone is None: raise ValueError("Local time requested but Location has no timezone set.") if date is None: date = self.today(local) observer = Observer(self.latitude, self.longitude, observer_elevation) if local: return astral.sun.dusk(observer, date, self.solar_depression, self.tzinfo) else: return astral.sun.dusk(observer, date, self.solar_depression) def midnight( self, date: Optional[datetime.date] = None, local: bool = True ) -> datetime.datetime: """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. """ if local and self.timezone is None: raise ValueError("Local time requested but Location has no timezone set.") if date is None: date = self.today(local) observer = Observer(self.latitude, self.longitude) if local: return astral.sun.midnight(observer, date, self.tzinfo) else: return astral.sun.midnight(observer, date) def daylight( self, date: Optional[datetime.date] = None, local: bool = True, observer_elevation: Elevation = 0.0, ) -> Tuple[datetime.datetime, datetime.datetime]: """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 :param observer_elevation: Elevation of the observer in metres above the location. :returns: A tuple containing the start and end times """ if local and self.timezone is None: raise ValueError("Local time requested but Location has no timezone set.") if date is None: date = self.today(local) observer = Observer(self.latitude, self.longitude, observer_elevation) if local: return astral.sun.daylight(observer, date, self.tzinfo) else: return astral.sun.daylight(observer, date) def night( self, date: Optional[datetime.date] = None, local: bool = True, observer_elevation: Elevation = 0.0, ) -> Tuple[datetime.datetime, datetime.datetime]: """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 :param observer_elevation: Elevation of the observer in metres above the location. :returns: A tuple containing the start and end times """ if local and self.timezone is None: raise ValueError("Local time requested but Location has no timezone set.") if date is None: date = self.today(local) observer = Observer(self.latitude, self.longitude, observer_elevation) if local: return astral.sun.night(observer, date, self.tzinfo) else: return astral.sun.night(observer, date) def twilight( self, date: Optional[datetime.date] = None, direction: SunDirection = SunDirection.RISING, local: bool = True, observer_elevation: Elevation = 0.0, ): """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.SunDirection.SETTING``. :param date: The date for which to calculate the times. :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 :param observer_elevation: Elevation of the observer in metres above the location. :return: A tuple of the UTC date and time at which twilight starts and ends. """ if local and self.timezone is None: raise ValueError("Local time requested but Location has no timezone set.") if date is None: date = self.today(local) observer = Observer(self.latitude, self.longitude, observer_elevation) if local: return astral.sun.twilight(observer, date, direction, self.tzinfo) else: return astral.sun.twilight(observer, date, direction) def moonrise( self, date: Optional[datetime.date] = None, local: bool = True, ) -> Optional[datetime.datetime]: """Calculates the time when the moon rises. :param date: The date for which to calculate the moonrise 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 moonrise occurs. """ if local and self.timezone is None: raise ValueError("Local time requested but Location has no timezone set.") if date is None: date = self.today(local) observer = Observer(self.latitude, self.longitude, 0) if local: return astral.moon.moonrise(observer, date, self.tzinfo) else: return astral.moon.moonrise(observer, date) def moonset( self, date: Optional[datetime.date] = None, local: bool = True, ) -> Optional[datetime.datetime]: """Calculates the time when the moon sets. :param date: The date for which to calculate the moonset 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 moonset occurs. """ if local and self.timezone is None: raise ValueError("Local time requested but Location has no timezone set.") if date is None: date = self.today(local) observer = Observer(self.latitude, self.longitude, 0) if local: return astral.moon.moonset(observer, date, self.tzinfo) else: return astral.moon.moonset(observer, date) def time_at_elevation( self, elevation: float, date: Optional[datetime.date] = None, direction: SunDirection = SunDirection.RISING, local: bool = True, ) -> datetime.datetime: """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. :param date: The date for which to calculate the elevation time. If no date is specified then the current date will be used. :param direction: Determines whether the time is for the sun rising or setting. Use ``SunDirection.RISING`` or ``SunDirection.SETTING``. Default is rising. :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. """ if local and self.timezone is None: raise ValueError("Local time requested but Location has no timezone set.") if date is None: date = self.today(local) if elevation > 90.0: elevation = 180.0 - elevation direction = SunDirection.SETTING observer = Observer(self.latitude, self.longitude, 0.0) if local: return astral.sun.time_at_elevation( observer, elevation, date, direction, self.tzinfo ) else: return astral.sun.time_at_elevation(observer, elevation, date, direction) def rahukaalam( self, date: Optional[datetime.date] = None, local: bool = True, observer_elevation: Elevation = 0.0, ) -> Tuple[datetime.datetime, datetime.datetime]: """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. :param observer_elevation: Elevation of the observer in metres above the location. :return: Tuple containing the start and end times for Rahukaalam. """ if local and self.timezone is None: raise ValueError("Local time requested but Location has no timezone set.") if date is None: date = self.today(local) observer = Observer(self.latitude, self.longitude, observer_elevation) if local: return astral.sun.rahukaalam(observer, date, tzinfo=self.tzinfo) else: return astral.sun.rahukaalam(observer, date) def golden_hour( self, direction: SunDirection = SunDirection.RISING, date: Optional[datetime.date] = None, local: bool = True, observer_elevation: Elevation = 0.0, ) -> Tuple[datetime.datetime, datetime.datetime]: """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 ``SunDirection.RISING`` or ``SunDirection.SETTING``. Default is rising. :param date: The date for which to calculate the times. :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 :param observer_elevation: Elevation of the observer in metres above the location. :return: A tuple of the date and time at which the Golden Hour starts and ends. """ if local and self.timezone is None: raise ValueError("Local time requested but Location has no timezone set.") if date is None: date = self.today(local) observer = Observer(self.latitude, self.longitude, observer_elevation) if local: return astral.sun.golden_hour(observer, date, direction, self.tzinfo) else: return astral.sun.golden_hour(observer, date, direction) def blue_hour( self, direction: SunDirection = SunDirection.RISING, date: Optional[datetime.date] = None, local: bool = True, observer_elevation: Elevation = 0.0, ) -> Tuple[datetime.datetime, datetime.datetime]: """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 ``SunDirection.RISING`` or ``SunDirection.SETTING``. Default is rising. :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 :param observer_elevation: Elevation of the observer in metres above the location. :return: A tuple of the date and time at which the Blue Hour starts and ends. """ if local and self.timezone is None: raise ValueError("Local time requested but Location has no timezone set.") if date is None: date = self.today(local) observer = Observer(self.latitude, self.longitude, observer_elevation) if local: return astral.sun.blue_hour(observer, date, direction, self.tzinfo) else: return astral.sun.blue_hour(observer, date, direction) def solar_azimuth( self, dateandtime: Optional[datetime.datetime] = None, observer_elevation: Elevation = 0.0, ) -> float: """Calculates the solar azimuth angle for a specific date/time. :param dateandtime: The date and time for which to calculate the angle. :returns: The azimuth angle in degrees clockwise from North. """ if dateandtime is None: dateandtime = astral.sun.now(self.tzinfo) elif not dateandtime.tzinfo: dateandtime = dateandtime.replace(tzinfo=self.tzinfo) observer = Observer(self.latitude, self.longitude, observer_elevation) dateandtime = dateandtime.astimezone(datetime.timezone.utc) # type: ignore return astral.sun.azimuth(observer, dateandtime) def solar_elevation( self, dateandtime: Optional[datetime.datetime] = None, observer_elevation: Elevation = 0.0, ) -> float: """Calculates the solar elevation angle for a specific time. :param dateandtime: The date and time for which to calculate the angle. :returns: The elevation angle in degrees above the horizon. """ if dateandtime is None: dateandtime = astral.sun.now(self.tzinfo) elif not dateandtime.tzinfo: dateandtime = dateandtime.replace(tzinfo=self.tzinfo) observer = Observer(self.latitude, self.longitude, observer_elevation) dateandtime = dateandtime.astimezone(datetime.timezone.utc) # type: ignore return astral.sun.elevation(observer, dateandtime) def solar_zenith( self, dateandtime: Optional[datetime.datetime] = None, observer_elevation: Elevation = 0.0, ) -> float: """Calculates the solar zenith angle for a specific time. :param dateandtime: The date and time for which to calculate the angle. :returns: The zenith angle in degrees from vertical. """ return 90.0 - self.solar_elevation(dateandtime, observer_elevation) def moon_phase(self, date: Optional[datetime.date] = None, local: bool = True): """Calculates the moon phase for a specific date. :param date: The date to calculate the phase for. If ommitted the current date is used. :returns: A number designating the phase ============ ============== 0 .. 6.99 New moon 7 .. 13.99 First quarter 14 .. 20.99 Full moon 21 .. 27.99 Last quarter ============ ============== """ if date is None: date = self.today(local) return astral.moon.phase(date) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1667296111.1233768 astral-3.2/src/astral/moon.py0000644000000000000000000004360614330165557013213 0ustar00"""Moon phase, rise and set times Right ascension, declination and distance of moon calcaulation from LOW-PRECISION FORMULAE FOR PLANETARY POSITIONS http://articles.adsabs.harvard.edu/pdf/1979ApJS...41..391V """ import datetime from dataclasses import dataclass, field, replace from math import asin, atan2, cos, degrees, fabs, pi, radians, sin, sqrt from typing import Callable, List, Optional, Union try: import zoneinfo except ImportError: from backports import zoneinfo from astral import AstralBodyPosition, Observer, now, today from astral.julian import julianday, julianday_2000 from astral.sidereal import lmst from astral.table4 import Table4Row, table4_u, table4_v, table4_w __all__ = ["moonrise", "moonset", "phase"] # Using 1896 arc seconds as moon's apparent diameter MOON_APPARENT_RADIUS = 1896.0 / (60.0 * 60.0) Degrees = float Radians = float Revolutions = float ArgumentFunc = Optional[Callable[[float], float]] @dataclass class NoTransit: parallax: float = field(default_factory=float) @dataclass class TransitEvent: event: str when: datetime.time = field(default_factory=datetime.time) azimuth: float = field(default_factory=float) distance: float = field(default_factory=float) def interpolate(f0: float, f1: float, f2: float, p: float) -> float: """3-point interpolation""" a = f1 - f0 b = f2 - f1 - a f = f0 + p * (2 * a + b * (2 * p - 1)) return f def sgn(value1: Union[float, datetime.timedelta]) -> int: """Test whether value1 and value2 have the same sign""" if isinstance(value1, datetime.timedelta): value1 = value1.total_seconds() if value1 < 0: return -1 elif value1 > 0: return 1 else: return 0 def moon_mean_longitude(jd2000: float) -> Revolutions: _mean_longitude = 0.606434 + 0.03660110129 * jd2000 _mean_longitude = _mean_longitude - int(_mean_longitude) return _mean_longitude def moon_mean_anomoly(jd2000: float) -> Revolutions: _mean_anomoly = 0.374897 + 0.03629164709 * jd2000 _mean_anomoly = _mean_anomoly - int(_mean_anomoly) return _mean_anomoly def moon_argument_of_latitude(jd2000: float) -> Revolutions: _argument_of_latitude = 0.259091 + 0.03674819520 * jd2000 _argument_of_latitude = _argument_of_latitude - int(_argument_of_latitude) return _argument_of_latitude def moon_mean_elongation_from_sun(jd2000: float) -> Revolutions: _mean_elongation_from_sun = 0.827362 + 0.03386319198 * jd2000 _mean_elongation_from_sun = _mean_elongation_from_sun - int( _mean_elongation_from_sun ) return _mean_elongation_from_sun def longitude_lunar_ascending_node(jd2000: float) -> Revolutions: _longitude_lunar_ascending_node = moon_mean_longitude( jd2000 ) - moon_argument_of_latitude(jd2000) return _longitude_lunar_ascending_node def sun_mean_longitude(jd2000: float) -> Revolutions: _sun_mean_longitude = 0.779072 + 0.00273790931 * jd2000 _sun_mean_longitude = _sun_mean_longitude - int(_sun_mean_longitude) return _sun_mean_longitude def sun_mean_anomoly(jd2000: float) -> Revolutions: _sun_mean_anomoly = 0.993126 + 0.00273777850 * jd2000 _sun_mean_anomoly = _sun_mean_anomoly - int(_sun_mean_anomoly) return _sun_mean_anomoly def venus_mean_longitude(jd2000: float) -> Revolutions: _venus_mean_longitude = 0.505498 + 0.00445046867 * jd2000 _venus_mean_longitude = _venus_mean_longitude - int(_venus_mean_longitude) return _venus_mean_longitude def moon_position(jd2000: float) -> AstralBodyPosition: """Calculate right ascension, declination and geocentric distance for the moon""" argument_values: List[Union[float, None]] = [ moon_mean_longitude(jd2000), # 1 = Lm moon_mean_anomoly(jd2000), # 2 = Gm moon_argument_of_latitude(jd2000), # 3 = Fm moon_mean_elongation_from_sun(jd2000), # 4 = D longitude_lunar_ascending_node(jd2000), # 5 = Om None, # 6 sun_mean_longitude(jd2000), # 7 = Ls sun_mean_anomoly(jd2000), # 8 = Gs None, # 9 None, # 10 None, # 11 venus_mean_longitude(jd2000), # 12 = L2 ] T = jd2000 / 36525 + 1 def _calc_value(table: List[Table4Row]) -> float: result = 0.0 for row in table: revolutions: float = 0.0 for arg_number, multiplier in row.argument_multiplers.items(): if multiplier != 0: arg_value = argument_values[arg_number - 1] if arg_value: value = arg_value * multiplier revolutions += value else: raise ValueError t_multipler = T if row.t else 1 result += row.coefficient * t_multipler * row.sincos(revolutions * 2 * pi) return result v = _calc_value(table4_v) u = _calc_value(table4_u) w = _calc_value(table4_w) s = w / sqrt(u - v * v) right_ascension = asin(s) + (argument_values[0] or 0) * 2 * pi # In radians s = v / sqrt(u) declination = asin(s) # In radians distance = 60.40974 * sqrt(u) # In Earth radii (≈6378km) return AstralBodyPosition(right_ascension, declination, distance) def moon_transit_event( hour: float, lmst: Degrees, latitude: Degrees, distance: float, window: List[AstralBodyPosition], ) -> Union[TransitEvent, NoTransit]: """Check if the moon transits the horizon within the window. Args: hour: Hour of the day lmst: Local mean sidereal time in degrees latitude: Observer latitude distance: Distance to the moon window: Sliding window of moon positions that covers a part of the day """ mst = radians(lmst) hour_angle = [0.0, 0.0, 0.0] k1 = radians(15 * 1.0027379097096138907193594760917) if window[2].right_ascension < window[0].right_ascension: window[2].right_ascension = window[2].right_ascension + 2 * pi hour_angle[0] = mst - window[0].right_ascension + (hour * k1) hour_angle[2] = mst - window[2].right_ascension + (hour * k1) + k1 hour_angle[1] = (hour_angle[2] + hour_angle[0]) / 2 window[1].declination = (window[2].declination + window[0].declination) / 2 sl = sin(radians(latitude)) cl = cos(radians(latitude)) # moon apparent radius + parallax correction z = cos(radians(90 + MOON_APPARENT_RADIUS - (41.685 / distance))) if hour == 0: window[0].distance = ( sl * sin(window[0].declination) + cl * cos(window[0].declination) * cos(hour_angle[0]) - z ) window[2].distance = ( sl * sin(window[2].declination) + cl * cos(window[2].declination) * cos(hour_angle[2]) - z ) if sgn(window[0].distance) == sgn(window[2].distance): return NoTransit(window[2].distance) window[1].distance = ( sl * sin(window[1].declination) + cl * cos(window[1].declination) * cos(hour_angle[1]) - z ) a = 2 * window[2].distance - 4 * window[1].distance + 2 * window[0].distance b = 4 * window[1].distance - 3 * window[0].distance - window[2].distance discriminant = b * b - 4 * a * window[0].distance if discriminant < 0: return NoTransit(window[2].distance) discriminant = sqrt(discriminant) e = (-b + discriminant) / (2 * a) if e > 1 or e < 0: e = (-b - discriminant) / (2 * a) time = hour + e + 1 / 120 h = int(time) m = int((time - h) * 60) sd = sin(window[1].declination) cd = cos(window[1].declination) hour_angle_crossing = hour_angle[0] + e * (hour_angle[2] - hour_angle[0]) sh = sin(hour_angle_crossing) ch = cos(hour_angle_crossing) x = cl * sd - sl * cd * ch y = -cd * sh az = degrees(atan2(y, x)) if az < 0: az += 360 if az > 360: az -= 360 event_time = datetime.time(h, m, 0) if window[0].distance < 0 and window[2].distance > 0: return TransitEvent("rise", event_time, az, window[2].distance) if window[0].distance > 0 and window[2].distance < 0: return TransitEvent("set", event_time, az, window[2].distance) return NoTransit(window[2].distance) def riseset( on: datetime.date, observer: Observer, ): """Calculate rise and set times""" jd2000 = julianday_2000(on) t0 = lmst( on, observer.longitude, ) m: List[AstralBodyPosition] = [] for interval in range(3): pos = moon_position(jd2000 + (interval * 0.5)) m.append(pos) for interval in range(1, 3): if m[interval].right_ascension <= m[interval - 1].right_ascension: m[interval].right_ascension = m[interval].right_ascension + 2 * pi moon_position_window: List[AstralBodyPosition] = [ replace(m[0]), # copy m[0] AstralBodyPosition(), AstralBodyPosition(), ] rise_time = None set_time = None # events = [] for hour in range(24): ph = (hour + 1) / 24 moon_position_window[2].right_ascension = interpolate( m[0].right_ascension, m[1].right_ascension, m[2].right_ascension, ph, ) moon_position_window[2].declination = interpolate( m[0].declination, m[1].declination, m[2].declination, ph, ) transit_info = moon_transit_event( hour, t0, observer.latitude, m[1].distance, moon_position_window ) if isinstance(transit_info, NoTransit): moon_position_window[2].distance = transit_info.parallax else: query_time = datetime.datetime( on.year, on.month, on.day, hour, 0, 0, tzinfo=datetime.timezone.utc ) if transit_info.event == "rise": event_time = transit_info.when event = datetime.datetime( on.year, on.month, on.day, event_time.hour, event_time.minute, 0, tzinfo=datetime.timezone.utc, ) if rise_time is None: rise_time = event else: rq_diff = (rise_time - query_time).total_seconds() eq_diff = (event - query_time).total_seconds() if set_time is not None: sq_diff = (set_time - query_time).total_seconds() else: sq_diff = 0 update_rise_time = sgn(rq_diff) == sgn(eq_diff) and fabs( rq_diff ) > fabs(eq_diff) update_rise_time |= sgn(rq_diff) != sgn(eq_diff) and ( set_time is not None and sgn(rq_diff) == sgn(sq_diff) ) if update_rise_time: rise_time = event elif transit_info.event == "set": event_time = transit_info.when event = datetime.datetime( on.year, on.month, on.day, event_time.hour, event_time.minute, 0, tzinfo=datetime.timezone.utc, ) if set_time is None: set_time = event else: sq_diff = (set_time - query_time).total_seconds() eq_diff = (event - query_time).total_seconds() if rise_time is not None: rq_diff = (rise_time - query_time).total_seconds() else: rq_diff = 0 update_set_time = sgn(sq_diff) == sgn(eq_diff) and fabs( sq_diff ) > fabs(eq_diff) update_set_time |= sgn(sq_diff) != sgn(eq_diff) and ( rise_time is not None and sgn(rq_diff) == sgn(sq_diff) ) if update_set_time: set_time = event moon_position_window[0].right_ascension = moon_position_window[ 2 ].right_ascension moon_position_window[0].declination = moon_position_window[2].declination moon_position_window[0].distance = moon_position_window[2].distance return rise_time, set_time def moonrise( observer: Observer, date: Optional[datetime.date] = None, tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc, ) -> Optional[datetime.datetime]: """Calculate the moon rise time Args: observer: Observer to calculate moonrise for date: Date to calculate for. Default is today's date in the timezone `tzinfo`. tzinfo: Timezone to return times in. Default is UTC. Returns: Date and time at which moonrise occurs. """ if isinstance(tzinfo, str): tzinfo = zoneinfo.ZoneInfo(tzinfo) # type: ignore if date is None: date = today(tzinfo) # type: ignore elif isinstance(date, datetime.datetime): date = date.date() info = riseset(date, observer) if info[0]: rise = info[0].astimezone(tzinfo) # type: ignore rd = rise.date() if rd != date: if rd > date: delta = datetime.timedelta(days=-1) else: delta = datetime.timedelta(days=1) new_date = date + delta info = riseset(new_date, observer) if info[0]: rise = info[0].astimezone(tzinfo) # type: ignore rd = rise.date() if rd != date: rise = None return rise else: raise ValueError("Moon never rises on this date, at this location") def moonset( observer: Observer, date: Optional[datetime.date] = None, tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc, ) -> Optional[datetime.datetime]: """Calculate the moon set time Args: observer: Observer to calculate moonset for date: Date to calculate for. Default is today's date in the timezone `tzinfo`. tzinfo: Timezone to return times in. Default is UTC. Returns: Date and time at which moonset occurs. """ if isinstance(tzinfo, str): tzinfo = zoneinfo.ZoneInfo(tzinfo) # type: ignore if date is None: date = today(tzinfo) # type: ignore elif isinstance(date, datetime.datetime): date = date.date() info = riseset(date, observer) if info[1]: set = info[1].astimezone(tzinfo) # type: ignore sd = set.date() if sd != date: if sd > date: delta = datetime.timedelta(days=-1) else: delta = datetime.timedelta(days=1) new_date = date + delta info = riseset(new_date, observer) if info[1]: set = info[1].astimezone(tzinfo) # type: ignore sd = set.date() if sd != date: set = None return set else: raise ValueError("Moon never sets on this date, at this location") def azimuth( observer: Observer, at: Optional[datetime.datetime] = None, ) -> Degrees: if at is None: at = now() jd2000 = julianday_2000(at) position = moon_position(jd2000) lst0: Radians = radians(lmst(at, observer.longitude)) hourangle: Radians = lst0 - position.right_ascension sh = sin(hourangle) ch = cos(hourangle) sd = sin(position.declination) cd = cos(position.declination) sl = sin(radians(observer.latitude)) cl = cos(radians(observer.latitude)) x = -ch * cd * sl + sd * cl y = -sh * cd azimuth = degrees(atan2(y, x)) % 360 return azimuth def elevation( observer: Observer, at: Optional[datetime.datetime] = None, ): if at is None: at = now() jd2000 = julianday_2000(at) position = moon_position(jd2000) lst0: Radians = radians(lmst(at, observer.longitude)) hourangle: Radians = lst0 - position.right_ascension sh = sin(hourangle) ch = cos(hourangle) sd = sin(position.declination) cd = cos(position.declination) sl = sin(radians(observer.latitude)) cl = cos(radians(observer.latitude)) x = -ch * cd * sl + sd * cl y = -sh * cd z = ch * cd * cl + sd * sl r = sqrt(x * x + y * y) elevation = degrees(atan2(z, r)) return elevation def zenith( observer: Observer, at: Optional[datetime.datetime] = None, ): return 90 - elevation(observer, at) def _phase_asfloat(date: datetime.date) -> float: jd = 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(d % 360.0) m = 357.53 + (35999.0503 * t) m = radians(m % 360.0) m1 = 134.96 + (477198.8676 * t) + (0.0089970 * t2) + (t3 / 69699) m1 = radians(m1 % 360.0) 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 = elong % 360.0 elong = int(elong) moon = ((elong + 6.43) / 360) * 28 return moon def phase(date: Optional[datetime.date] = None) -> float: """Calculates the phase of the moon on the specified date. Args: date: The date to calculate the phase for. Dates are always in the UTC timezone. If not specified then today's date is used. Returns: A number designating the phase. ============ ============== 0 .. 6.99 New moon 7 .. 13.99 First quarter 14 .. 20.99 Full moon 21 .. 27.99 Last quarter ============ ============== """ if date is None: date = today() moon = _phase_asfloat(date) if moon >= 28.0: moon -= 28.0 return moon ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1667670964.116485 astral-3.2/src/astral/py.typed0000644000000000000000000000000014331521664013341 0ustar00././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1666076747.2566774 astral-3.2/src/astral/sidereal.py0000644000000000000000000000135414323450113014010 0ustar00import datetime from typing import Union from astral.julian import julianday_2000 Degrees = float def gmst(at: Union[datetime.datetime, datetime.date]) -> Degrees: """Calculate Greenwich Mean Sidereal Time in degrees""" jd2000 = julianday_2000(at) t0 = jd2000 / 36525 value = ( 280.46061837 + 360.98564736629 * jd2000 + 0.000387933 * pow(t0, 2) + pow(t0, 3) / 38710000 ) return value % 360 def lmst( at: Union[datetime.datetime, datetime.date], longitude: Degrees, ) -> Degrees: """Local Mean Sidereal Time for longitude in degrees Args: jd2000: Julian day longitude: Longitude in degrees """ mst = gmst(at) mst += longitude return mst ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1667670964.116485 astral-3.2/src/astral/sun.py0000644000000000000000000011147314331521664013042 0ustar00import datetime from math import acos, asin, atan2, cos, degrees, fabs, radians, sin, sqrt, tan from typing import Dict, Optional, Tuple, Union try: import zoneinfo except ImportError: from backports import zoneinfo from astral import ( Depression, Minutes, Observer, SunDirection, TimePeriod, now, refraction_at_zenith, today, ) from astral.julian import julianday, julianday_to_juliancentury __all__ = [ "sun", "dawn", "sunrise", "noon", "midnight", "sunset", "dusk", "daylight", "night", "twilight", "blue_hour", "golden_hour", "rahukaalam", "zenith", "azimuth", "elevation", "time_at_elevation", ] # Using 32 arc minutes as sun's apparent diameter SUN_APPARENT_RADIUS = 32.0 / (60.0 * 2.0) # region Backend def minutes_to_timedelta(minutes: float) -> datetime.timedelta: """Convert a floating point number of minutes to a :class:`~datetime.timedelta` """ d = int(minutes / 1440) minutes = minutes - (d * 1440) minutes = minutes * 60 s = int(minutes) sfrac = minutes - s us = int(sfrac * 1_000_000) return datetime.timedelta(days=d, seconds=s, microseconds=us) def geom_mean_long_sun(juliancentury: float) -> float: """Calculate the geometric mean longitude of the sun""" l0 = 280.46646 + juliancentury * (36000.76983 + 0.0003032 * juliancentury) return l0 % 360.0 def geom_mean_anomaly_sun(juliancentury: float) -> float: """Calculate the geometric mean anomaly of the sun""" return 357.52911 + juliancentury * (35999.05029 - 0.0001537 * juliancentury) def eccentric_location_earth_orbit(juliancentury: float) -> float: """Calculate the eccentricity of Earth's orbit""" return 0.016708634 - juliancentury * (0.000042037 + 0.0000001267 * juliancentury) def sun_eq_of_center(juliancentury: float) -> float: """Calculate the equation of the center of the sun""" m = 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(juliancentury: float) -> float: """Calculate the sun's true longitude""" l0 = geom_mean_long_sun(juliancentury) c = sun_eq_of_center(juliancentury) return l0 + c def sun_true_anomoly(juliancentury: float) -> float: """Calculate the sun's true anomaly""" m = geom_mean_anomaly_sun(juliancentury) c = sun_eq_of_center(juliancentury) return m + c def sun_rad_vector(juliancentury: float) -> float: v = sun_true_anomoly(juliancentury) e = eccentric_location_earth_orbit(juliancentury) return (1.000001018 * (1 - e * e)) / (1 + e * cos(radians(v))) def sun_apparent_long(juliancentury: float) -> float: true_long = 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(juliancentury: float) -> float: 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(juliancentury: float) -> float: e0 = mean_obliquity_of_ecliptic(juliancentury) omega = 125.04 - 1934.136 * juliancentury return e0 + 0.00256 * cos(radians(omega)) def sun_rt_ascension(juliancentury: float) -> float: """Calculate the sun's right ascension""" oc = obliquity_correction(juliancentury) al = sun_apparent_long(juliancentury) tananum = cos(radians(oc)) * sin(radians(al)) tanadenom = cos(radians(al)) return degrees(atan2(tananum, tanadenom)) def sun_declination(juliancentury: float) -> float: """Calculate the sun's declination""" e = obliquity_correction(juliancentury) lambd = sun_apparent_long(juliancentury) sint = sin(radians(e)) * sin(radians(lambd)) return degrees(asin(sint)) def var_y(juliancentury: float) -> float: epsilon = obliquity_correction(juliancentury) y = tan(radians(epsilon) / 2.0) return y * y def eq_of_time(juliancentury: float) -> Minutes: l0 = geom_mean_long_sun(juliancentury) e = eccentric_location_earth_orbit(juliancentury) m = geom_mean_anomaly_sun(juliancentury) y = 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( latitude: float, declination: float, zenith: float, direction: SunDirection ) -> float: """Calculate the hour angle of the sun See https://en.wikipedia.org/wiki/Hour_angle#Solar_hour_angle Args: latitude: The latitude of the obersver declination: The declination of the sun zenith: The zenith angle of the sun direction: The direction of traversal of the sun Raises: ValueError """ latitude_rad = radians(latitude) declination_rad = radians(declination) zenith_rad = radians(zenith) h = (cos(zenith_rad) - sin(latitude_rad) * sin(declination_rad)) / ( cos(latitude_rad) * cos(declination_rad) ) hour_angle = acos(h) if direction == SunDirection.SETTING: hour_angle = -hour_angle return hour_angle def adjust_to_horizon(elevation: float) -> float: """Calculate the extra degrees of depression that you can see round the earth due to the increase in elevation. Args: elevation: Elevation above the earth in metres Returns: A number of degrees to add to adjust for the elevation of the observer """ if elevation <= 0: return 0 r = 6356900 # radius of the earth a1 = r h1 = r + elevation theta1 = acos(a1 / h1) return degrees(theta1) def adjust_to_obscuring_feature(elevation: Tuple[float, float]) -> float: """Calculate the number of degrees to adjust for an obscuring feature""" if elevation[0] == 0.0: return 0.0 sign = -1 if elevation[0] < 0.0 else 1 return sign * degrees( acos(fabs(elevation[0]) / sqrt(pow(elevation[0], 2) + pow(elevation[1], 2))) ) def time_of_transit( observer: Observer, date: datetime.date, zenith: float, direction: SunDirection, with_refraction: bool = True, ) -> datetime.datetime: """Calculate the time in the UTC timezone when the sun transits the specificed zenith Args: observer: An observer viewing the sun at a specific, latitude, longitude and elevation date: The date to calculate for zenith: The zenith angle for which to calculate the transit time direction: The direction that the sun is traversing Raises: ValueError if the zenith is not transitted by the sun Returns: the time when the sun transits the specificed zenith """ if observer.latitude > 89.8: latitude = 89.8 elif observer.latitude < -89.8: latitude = -89.8 else: latitude = observer.latitude adjustment_for_elevation = 0.0 if isinstance(observer.elevation, float) and observer.elevation > 0.0: adjustment_for_elevation = adjust_to_horizon(observer.elevation) elif isinstance(observer.elevation, tuple): adjustment_for_elevation = adjust_to_obscuring_feature(observer.elevation) if with_refraction: adjustment_for_refraction = refraction_at_zenith( zenith + adjustment_for_elevation ) else: adjustment_for_refraction = 0.0 jd = julianday(date) adjustment = 0.0 timeUTC = 0.0 for _ in range(2): jc = julianday_to_juliancentury(jd + adjustment) declination = sun_declination(jc) hourangle = hour_angle( latitude, declination, zenith + adjustment_for_elevation + adjustment_for_refraction, direction, ) delta = -observer.longitude - degrees(hourangle) eqtime = eq_of_time(jc) offset = delta * 4.0 - eqtime if offset < -720.0: offset += 1440 timeUTC = 720.0 + offset adjustment = timeUTC / 1440.0 td = minutes_to_timedelta(timeUTC) dt = datetime.datetime(date.year, date.month, date.day) + td dt = dt.replace(tzinfo=datetime.timezone.utc) # pylint: disable=E1120 return dt def time_at_elevation( observer: Observer, elevation: float, date: Optional[datetime.date] = None, direction: SunDirection = SunDirection.RISING, tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc, with_refraction: bool = True, ) -> datetime.datetime: """Calculates the time when the sun is at the specified elevation on the specified date. 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. Args: elevation: Elevation of the sun in degrees above the horizon to calculate for. observer: Observer to calculate for date: Date to calculate for. Default is today's date in the timezone `tzinfo`. direction: Determines whether the calculated time is for the sun rising or setting. Use ``SunDirection.RISING`` or ``SunDirection.SETTING``. Default is rising. tzinfo: Timezone to return times in. Default is UTC. Returns: Date and time at which the sun is at the specified elevation. """ if elevation > 90.0: elevation = 180.0 - elevation direction = SunDirection.SETTING if isinstance(tzinfo, str): tzinfo = zoneinfo.ZoneInfo(tzinfo) # type: ignore if date is None: date = today(tzinfo) # type: ignore zenith = 90 - elevation try: return time_of_transit( observer, date, zenith, direction, with_refraction ).astimezone( tzinfo # type: ignore ) except ValueError as exc: if exc.args[0] == "math domain error": raise ValueError( f"Sun never reaches an elevation of {elevation} degrees " "at this location." ) from exc else: raise def noon( observer: Observer, date: Optional[datetime.date] = None, tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc, ) -> datetime.datetime: """Calculate solar noon time when the sun is at its highest point. Args: observer: An observer viewing the sun at a specific, latitude, longitude and elevation date: Date to calculate for. Default is today for the specified tzinfo. tzinfo: Timezone to return times in. Default is UTC. Returns: Date and time at which noon occurs. """ if isinstance(tzinfo, str): tzinfo = zoneinfo.ZoneInfo(tzinfo) # type: ignore if date is None: date = today(tzinfo) # type: ignore jc = julianday_to_juliancentury(julianday(date)) eqtime = eq_of_time(jc) timeUTC = (720.0 - (4 * observer.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, tzinfo=datetime.timezone.utc, ) return noon.astimezone(tzinfo) # type: ignore pylint: disable=E1120 def midnight( observer: Observer, date: Optional[datetime.date] = None, tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc, ) -> datetime.datetime: """Calculate solar midnight time. Note: This calculates the solar midnight that is closest to 00:00:00 of the specified date i.e. it may return a time that is on the previous day. Args: observer: An observer viewing the sun at a specific, latitude, longitude and elevation date: Date to calculate for. Default is today for the specified tzinfo. tzinfo: Timezone to return times in. Default is UTC. Returns: Date and time at which midnight occurs. """ if isinstance(tzinfo, str): tzinfo = zoneinfo.ZoneInfo(tzinfo) # type: ignore if date is None: date = today(tzinfo) # type: ignore midday = datetime.time(12, 0, 0) jd = julianday(datetime.datetime.combine(date, midday)) newt = julianday_to_juliancentury(jd + 0.5 + -observer.longitude / 360.0) eqtime = eq_of_time(newt) timeUTC = (-observer.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, tzinfo=datetime.timezone.utc, ) return midnight.astimezone(tzinfo) # type: ignore def zenith_and_azimuth( observer: Observer, dateandtime: datetime.datetime, with_refraction: bool = True, ) -> Tuple[float, float]: if observer.latitude > 89.8: latitude = 89.8 elif observer.latitude < -89.8: latitude = -89.8 else: latitude = observer.latitude longitude = observer.longitude if dateandtime.tzinfo is None: zone = 0.0 utc_datetime = dateandtime else: zone = -dateandtime.utcoffset().total_seconds() / 3600.0 # type: ignore utc_datetime = dateandtime.astimezone(datetime.timezone.utc) jd = julianday(utc_datetime) t = julianday_to_juliancentury(jd) declination = sun_declination(t) eqtime = eq_of_time(t) # 360deg * 4 == 1440 minutes, 60*24 = 1440 minutes == 1 rotation solarTimeFix = eqtime + (4.0 * longitude) + (60 * zone) trueSolarTime = ( dateandtime.hour * 60.0 + dateandtime.minute + dateandtime.second / 60.0 + solarTimeFix ) # in minutes as a float, fractional part is seconds 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 ch = cos(radians(hourangle)) # sh = sin(radians(hourangle)) cl = cos(radians(latitude)) sl = sin(radians(latitude)) sd = sin(radians(declination)) cd = cos(radians(declination)) csz = cl * cd * ch + sl * sd if csz > 1.0: csz = 1.0 elif csz < -1.0: csz = -1.0 zenith = degrees(acos(csz)) azDenom = cl * sin(radians(zenith)) if abs(azDenom) > 0.001: azRad = ((sl * cos(radians(zenith))) - sd) / 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 if with_refraction: zenith -= refraction_at_zenith(zenith) # elevation = 90 - zenith return zenith, azimuth def zenith( observer: Observer, dateandtime: Optional[datetime.datetime] = None, with_refraction: bool = True, ) -> float: """Calculate the zenith angle of the sun. Args: observer: Observer to calculate the solar zenith for dateandtime: The date and time for which to calculate the angle. If `dateandtime` is None or is a naive Python datetime then it is assumed to be in the UTC timezone. with_refraction: If True adjust zenith to take refraction into account Returns: The zenith angle in degrees. """ if dateandtime is None: dateandtime = now(datetime.timezone.utc) return zenith_and_azimuth(observer, dateandtime, with_refraction)[0] def azimuth( observer: Observer, dateandtime: Optional[datetime.datetime] = None, ) -> float: """Calculate the azimuth angle of the sun. Args: observer: Observer to calculate the solar azimuth for dateandtime: The date and time for which to calculate the angle. If `dateandtime` is None or is a naive Python datetime then it is assumed to be in the UTC timezone. Returns: The azimuth angle in degrees clockwise from North. If `dateandtime` is a naive Python datetime then it is assumed to be in the UTC timezone. """ if dateandtime is None: dateandtime = now(datetime.timezone.utc) return zenith_and_azimuth(observer, dateandtime)[1] def elevation( observer: Observer, dateandtime: Optional[datetime.datetime] = None, with_refraction: bool = True, ) -> float: """Calculate the sun's angle of elevation. Args: observer: Observer to calculate the solar elevation for dateandtime: The date and time for which to calculate the angle. If `dateandtime` is None or is a naive Python datetime then it is assumed to be in the UTC timezone. with_refraction: If True adjust elevation to take refraction into account Returns: The elevation angle in degrees above the horizon. """ if dateandtime is None: dateandtime = now(datetime.timezone.utc) return 90.0 - zenith(observer, dateandtime, with_refraction) def dawn( observer: Observer, date: Optional[datetime.date] = None, depression: Union[float, Depression] = Depression.CIVIL, tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc, ) -> datetime.datetime: """Calculate dawn time. Args: observer: Observer to calculate dawn for date: Date to calculate for. Default is today's date in the timezone `tzinfo`. depression: Number of degrees below the horizon to use to calculate dawn. Default is for Civil dawn i.e. 6.0 tzinfo: Timezone to return times in. Default is UTC. Returns: Date and time at which dawn occurs. Raises: ValueError: if dawn does not occur on the specified date """ if isinstance(tzinfo, str): tzinfo = zoneinfo.ZoneInfo(tzinfo) # type: ignore if date is None: date = today(tzinfo) # type: ignore elif isinstance(date, datetime.datetime): tzinfo = date.tzinfo or tzinfo date = date.date() dep: float = 0.0 if isinstance(depression, Depression): dep = depression.value else: dep = depression try: tot = time_of_transit( observer, date, 90.0 + dep, SunDirection.RISING ).astimezone( tzinfo # type: ignore ) # If the dates don't match search on either the next or previous day. tot_date = tot.date() if tot_date != date: if tot_date < date: delta = datetime.timedelta(days=1) else: delta = datetime.timedelta(days=-1) new_date = date + delta tot = time_of_transit( observer, new_date, 90.0 + dep, SunDirection.RISING, ).astimezone( tzinfo # type: ignore ) # Still can't get a time then raise the error tot_date = tot.date() if tot_date != date: raise ValueError("Unable to find a dawn time on the date specified") return tot except ValueError as exc: if exc.args[0] == "math domain error": raise ValueError( f"Sun never reaches {dep} degrees below the horizon, at this location." ) from exc else: raise def sunrise( observer: Observer, date: Optional[datetime.date] = None, tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc, ) -> datetime.datetime: """Calculate sunrise time. Args: observer: Observer to calculate sunrise for date: Date to calculate for. Default is today's date in the timezone `tzinfo`. tzinfo: Timezone to return times in. Default is UTC. Returns: Date and time at which sunrise occurs. Raises: ValueError: if the sun does not reach the horizon on the specified date """ if isinstance(tzinfo, str): tzinfo = zoneinfo.ZoneInfo(tzinfo) # type: ignore if date is None: date = today(tzinfo) # type: ignore elif isinstance(date, datetime.datetime): tzinfo = date.tzinfo or tzinfo date = date.date() try: tot = time_of_transit( observer, date, 90.0 + SUN_APPARENT_RADIUS, SunDirection.RISING, ).astimezone( tzinfo # type: ignore ) tot_date = tot.date() if tot_date != date: if tot_date < date: delta = datetime.timedelta(days=1) else: delta = datetime.timedelta(days=-1) new_date = date + delta tot = time_of_transit( observer, new_date, 90.0 + SUN_APPARENT_RADIUS, SunDirection.RISING, ).astimezone( tzinfo # type: ignore ) tot_date = tot.date() if tot_date != date: raise ValueError("Unable to find a sunrise time on the date specified") return tot except ValueError as exc: if exc.args[0] == "math domain error": z = zenith(observer, noon(observer, date)) if z > 90.0: msg = "Sun is always below the horizon on this day, at this location." else: msg = "Sun is always above the horizon on this day, at this location." raise ValueError(msg) from exc else: raise def sunset( observer: Observer, date: Optional[datetime.date] = None, tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc, ) -> datetime.datetime: """Calculate sunset time. Args: observer: Observer to calculate sunset for date: Date to calculate for. Default is today's date in the timezone `tzinfo`. tzinfo: Timezone to return times in. Default is UTC. Returns: Date and time at which sunset occurs. Raises: ValueError: if the sun does not reach the horizon """ if isinstance(tzinfo, str): tzinfo = zoneinfo.ZoneInfo(tzinfo) # type: ignore if date is None: date = today(tzinfo) # type: ignore elif isinstance(date, datetime.datetime): tzinfo = date.tzinfo or tzinfo date = date.date() try: tot = time_of_transit( observer, date, 90.0 + SUN_APPARENT_RADIUS, SunDirection.SETTING, ).astimezone( tzinfo # type: ignore ) tot_date = tot.date() if tot_date != date: if tot_date < date: delta = datetime.timedelta(days=1) else: delta = datetime.timedelta(days=-1) new_date = date + delta tot = time_of_transit( observer, new_date, 90.0 + SUN_APPARENT_RADIUS, SunDirection.SETTING, ).astimezone( tzinfo # type: ignore ) tot_date = tot.date() if tot_date != date: raise ValueError("Unable to find a sunset time on the date specified") return tot except ValueError as exc: if exc.args[0] == "math domain error": z = zenith(observer, noon(observer, date)) if z > 90.0: msg = "Sun is always below the horizon on this day, at this location." else: msg = "Sun is always above the horizon on this day, at this location." raise ValueError(msg) from exc else: raise def dusk( observer: Observer, date: Optional[datetime.date] = None, depression: Union[float, Depression] = Depression.CIVIL, tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc, ) -> datetime.datetime: """Calculate dusk time. Args: observer: Observer to calculate dusk for date: Date to calculate for. Default is today's date in the timezone `tzinfo`. depression: Number of degrees below the horizon to use to calculate dusk. Default is for Civil dusk i.e. 6.0 tzinfo: Timezone to return times in. Default is UTC. Returns: Date and time at which dusk occurs. Raises: ValueError: if dusk does not occur on the specified date """ if isinstance(tzinfo, str): tzinfo = zoneinfo.ZoneInfo(tzinfo) # type: ignore if date is None: date = today(tzinfo) # type: ignore elif isinstance(date, datetime.datetime): tzinfo = date.tzinfo or tzinfo date = date.date() dep: float = 0.0 if isinstance(depression, Depression): dep = depression.value else: dep = depression try: tot = time_of_transit( observer, date, 90.0 + dep, SunDirection.SETTING ).astimezone( tzinfo # type: ignore ) tot_date = tot.date() if tot_date != date: if tot_date < date: delta = datetime.timedelta(days=1) else: delta = datetime.timedelta(days=-1) new_date = date + delta tot = time_of_transit( observer, new_date, 90.0 + dep, SunDirection.SETTING, ).astimezone( tzinfo # type: ignore ) tot_date = tot.date() if tot_date != date: raise ValueError("Unable to find a dusk time on the date specified") return tot except ValueError as exc: if exc.args[0] == "math domain error": raise ValueError( f"Sun never reaches {dep} degrees below the horizon, at this location." ) from exc else: raise def daylight( observer: Observer, date: Optional[datetime.date] = None, tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc, ) -> TimePeriod: """Calculate daylight start and end times. Args: observer: Observer to calculate daylight for date: Date to calculate for. Default is today's date in the timezone `tzinfo`. tzinfo: Timezone to return times in. Default is UTC. Returns: A tuple of the date and time at which daylight starts and ends. Raises: ValueError: if the sun does not rise or does not set """ if isinstance(tzinfo, str): tzinfo = zoneinfo.ZoneInfo(tzinfo) # type: ignore if date is None: date = today(tzinfo) # type: ignore sr = sunrise(observer, date, tzinfo) ss = sunset(observer, date, tzinfo) return sr, ss def night( observer: Observer, date: Optional[datetime.date] = None, tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc, ) -> TimePeriod: """Calculate night start and end times. Night is calculated to be between astronomical dusk on the date specified and astronomical dawn of the next day. Args: observer: Observer to calculate night for date: Date to calculate for. Default is today's date for the specified tzinfo. tzinfo: Timezone to return times in. Default is UTC. Returns: A tuple of the date and time at which night starts and ends. Raises: ValueError: if dawn does not occur on the specified date or dusk on the following day """ if isinstance(tzinfo, str): tzinfo = zoneinfo.ZoneInfo(tzinfo) # type: ignore if date is None: date = today(tzinfo) # type: ignore start = dusk(observer, date, 6, tzinfo) tomorrow = date + datetime.timedelta(days=1) end = dawn(observer, tomorrow, 6, tzinfo) return start, end def twilight( observer: Observer, date: Optional[datetime.date] = None, direction: SunDirection = SunDirection.RISING, tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc, ) -> TimePeriod: """Returns the start and end times of Twilight 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. Args: observer: Observer to calculate twilight for date: Date for which to calculate the times. Default is today's date in the timezone `tzinfo`. direction: Determines whether the time is for the sun rising or setting. Use ``astral.SunDirection.RISING`` or ``astral.SunDirection.SETTING``. tzinfo: Timezone to return times in. Default is UTC. Returns: A tuple of the date and time at which twilight starts and ends. Raises: ValueError: if the sun does not rise or does not set """ if isinstance(tzinfo, str): tzinfo = zoneinfo.ZoneInfo(tzinfo) # type: ignore if date is None: date = today(tzinfo) # type: ignore start = time_of_transit(observer, date, 90 + 6, direction,).astimezone( tzinfo # type: ignore ) if direction == SunDirection.RISING: end = sunrise(observer, date, tzinfo).astimezone(tzinfo) # type: ignore else: end = sunset(observer, date, tzinfo).astimezone(tzinfo) # type: ignore if direction == SunDirection.RISING: return start, end else: return end, start def golden_hour( observer: Observer, date: Optional[datetime.date] = None, direction: SunDirection = SunDirection.RISING, tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc, ) -> TimePeriod: """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. Args: observer: Observer to calculate the golden hour for date: Date for which to calculate the times. Default is today's date in the timezone `tzinfo`. direction: Determines whether the time is for the sun rising or setting. Use ``SunDirection.RISING`` or ``SunDirection.SETTING``. tzinfo: Timezone to return times in. Default is UTC. Returns: A tuple of the date and time at which the Golden Hour starts and ends. Raises: ValueError: if the sun does not transit the elevations -4 & +6 degrees """ if isinstance(tzinfo, str): tzinfo = zoneinfo.ZoneInfo(tzinfo) # type: ignore if date is None: date = today(tzinfo) # type: ignore start = time_of_transit(observer, date, 90 + 4, direction,).astimezone( tzinfo # type: ignore ) end = time_of_transit(observer, date, 90 - 6, direction,).astimezone( tzinfo # type: ignore ) if direction == SunDirection.RISING: return start, end else: return end, start def blue_hour( observer: Observer, date: Optional[datetime.date] = None, direction: SunDirection = SunDirection.RISING, tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc, ) -> TimePeriod: """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. Args: observer: Observer to calculate the blue hour for date: Date for which to calculate the times. Default is today's date in the timezone `tzinfo`. direction: Determines whether the time is for the sun rising or setting. Use ``SunDirection.RISING`` or ``SunDirection.SETTING``. tzinfo: Timezone to return times in. Default is UTC. Returns: A tuple of the date and time at which the Blue Hour starts and ends. Raises: ValueError: if the sun does not transit the elevations -4 & -6 degrees """ if isinstance(tzinfo, str): tzinfo = zoneinfo.ZoneInfo(tzinfo) # type: ignore if date is None: date = today(tzinfo) # type: ignore start = time_of_transit(observer, date, 90 + 6, direction,).astimezone( tzinfo # type: ignore ) end = time_of_transit(observer, date, 90 + 4, direction,).astimezone( tzinfo # type: ignore ) if direction == SunDirection.RISING: return start, end else: return end, start def rahukaalam( observer: Observer, date: Optional[datetime.date] = None, daytime: bool = True, tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc, ) -> TimePeriod: """Calculate ruhakaalam times. Args: observer: Observer to calculate rahukaalam for date: Date to calculate for. Default is today's date in the timezone `tzinfo`. daytime: If True calculate for the day time else calculate for the night time. tzinfo: Timezone to return times in. Default is UTC. Returns: Tuple containing the start and end times for Rahukaalam. Raises: ValueError: if the sun does not rise or does not set """ if isinstance(tzinfo, str): tzinfo = zoneinfo.ZoneInfo(tzinfo) # type: ignore if date is None: date = today(tzinfo) # type: ignore if daytime: start = sunrise(observer, date, tzinfo) end = sunset(observer, date, tzinfo) else: start = sunset(observer, date, tzinfo) oneday = datetime.timedelta(days=1) end = sunrise(observer, date + oneday, tzinfo) octant_duration = datetime.timedelta(seconds=(end - start).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 = start + (octant_duration * octant) end = start + octant_duration return start, end def sun( observer: Observer, date: Optional[datetime.date] = None, dawn_dusk_depression: Union[float, Depression] = Depression.CIVIL, tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc, ) -> Dict[str, datetime.datetime]: """Calculate all the info for the sun at once. Args: observer: Observer for which to calculate the times of the sun date: Date to calculate for. Default is today's date in the timezone `tzinfo`. dawn_dusk_depression: Depression to use to calculate dawn and dusk. Default is for Civil dusk i.e. 6.0 tzinfo: Timezone to return times in. Default is UTC. Returns: Dictionary with keys ``dawn``, ``sunrise``, ``noon``, ``sunset`` and ``dusk`` whose values are the results of the corresponding functions. Raises: ValueError: if passed through from any of the functions """ if isinstance(tzinfo, str): tzinfo = zoneinfo.ZoneInfo(tzinfo) # type: ignore if date is None: date = today(tzinfo) # type: ignore return { "dawn": dawn(observer, date, dawn_dusk_depression, tzinfo), "sunrise": sunrise(observer, date, tzinfo), "noon": noon(observer, date, tzinfo), "sunset": sunset(observer, date, tzinfo), "dusk": dusk(observer, date, dawn_dusk_depression, tzinfo), } ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1667670964.116485 astral-3.2/src/astral/table4.py0000644000000000000000000003654314331521664013414 0ustar00from math import cos, sin from typing import Callable, Dict, List, NamedTuple class Table4Row(NamedTuple): coefficient: float t: bool sincos: Callable[[float], float] argument_multiplers: Dict[int, int] Gm = 2 # Moon mean anomoly Fm = 3 # Moon argument of latitude D = 4 # Moon mean elongation from sun Om = 5 # Longitude of the lunar ascending node Ls = 7 # Sun mean longitude Gs = 8 # Sun mean anomoly L2 = 12 # Venus mean longitude table4_v: List[Table4Row] = [ Table4Row(0.39558, False, sin, {Gm: 0, Fm: 1, D: 0, Om: 1, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.08200, False, sin, {Gm: 0, Fm: 1, D: 0, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.03257, False, sin, {Gm: 1, Fm: -1, D: 0, Om: -1, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.01092, False, sin, {Gm: 1, Fm: 1, D: 0, Om: 1, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00666, False, sin, {Gm: 1, Fm: -1, D: 0, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00644, False, sin, {Gm: 1, Fm: 1, D: -2, Om: 1, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00331, False, sin, {Gm: 0, Fm: 1, D: -2, Om: 1, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00304, False, sin, {Gm: 0, Fm: 1, D: -2, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row( -0.00240, False, sin, {Gm: 1, Fm: -1, D: -2, Om: -1, Ls: 0, Gs: 0, L2: 0} ), Table4Row(0.00226, False, sin, {Gm: 1, Fm: 1, D: 0, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00108, False, sin, {Gm: 1, Fm: 1, D: -2, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00079, False, sin, {Gm: 0, Fm: 1, D: 0, Om: -1, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00078, False, sin, {Gm: 0, Fm: 1, D: 2, Om: 1, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00066, False, sin, {Gm: 0, Fm: 1, D: 0, Om: 1, Ls: 0, Gs: -1, L2: 0}), Table4Row(-0.00062, False, sin, {Gm: 0, Fm: 1, D: 0, Om: 1, Ls: 0, Gs: 1, L2: 0}), Table4Row(-0.00050, False, sin, {Gm: 1, Fm: -1, D: -2, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00045, False, sin, {Gm: 2, Fm: 1, D: 0, Om: 1, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00031, False, sin, {Gm: 2, Fm: 1, D: -2, Om: 1, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00027, False, sin, {Gm: 1, Fm: 1, D: -2, Om: 1, Ls: 0, Gs: 1, L2: 0}), Table4Row(-0.00024, False, sin, {Gm: 0, Fm: 1, D: -2, Om: 1, Ls: 0, Gs: 1, L2: 0}), Table4Row(-0.00021, True, sin, {Gm: 0, Fm: 1, D: 0, Om: 1, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00018, False, sin, {Gm: 0, Fm: 1, D: -1, Om: 1, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00016, False, sin, {Gm: 0, Fm: 1, D: 2, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00016, False, sin, {Gm: 1, Fm: -1, D: 0, Om: -1, Ls: 0, Gs: -1, L2: 0}), Table4Row(-0.00016, False, sin, {Gm: 2, Fm: -1, D: 0, Om: -1, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00015, False, sin, {Gm: 0, Fm: 1, D: -2, Om: 0, Ls: 0, Gs: 1, L2: 0}), Table4Row( -0.00012, False, sin, {Gm: 1, Fm: -1, D: -2, Om: -1, Ls: 0, Gs: 1, L2: 0} ), Table4Row(-0.00011, False, sin, {Gm: 1, Fm: -1, D: 0, Om: -1, Ls: 0, Gs: 1, L2: 0}), Table4Row(0.00009, False, sin, {Gm: 1, Fm: 1, D: 0, Om: 1, Ls: 0, Gs: -1, L2: 0}), Table4Row(0.00009, False, sin, {Gm: 2, Fm: 1, D: 0, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00008, False, sin, {Gm: 2, Fm: -1, D: 0, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00008, False, sin, {Gm: 1, Fm: 1, D: 2, Om: 1, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00008, False, sin, {Gm: 0, Fm: 3, D: -2, Om: 1, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00007, False, sin, {Gm: 1, Fm: -1, D: 2, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row( -0.00007, False, sin, {Gm: 2, Fm: -1, D: -2, Om: -1, Ls: 0, Gs: 0, L2: 0} ), Table4Row(-0.00007, False, sin, {Gm: 1, Fm: 1, D: 0, Om: 1, Ls: 0, Gs: 1, L2: 0}), Table4Row(-0.00006, False, sin, {Gm: 0, Fm: 1, D: 1, Om: 1, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00006, False, sin, {Gm: 0, Fm: 1, D: -2, Om: 0, Ls: 0, Gs: -1, L2: 0}), Table4Row(0.00006, False, sin, {Gm: 1, Fm: -1, D: 0, Om: 1, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00006, False, sin, {Gm: 0, Fm: 1, D: 2, Om: 1, Ls: 0, Gs: -1, L2: 0}), Table4Row(-0.00005, False, sin, {Gm: 1, Fm: 1, D: -2, Om: 0, Ls: 0, Gs: 1, L2: 0}), Table4Row(-0.00004, False, sin, {Gm: 2, Fm: 1, D: -2, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00004, False, sin, {Gm: 1, Fm: -3, D: 0, Om: -1, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00004, False, sin, {Gm: 1, Fm: -1, D: 0, Om: 0, Ls: 0, Gs: -1, L2: 0}), Table4Row(-0.00003, False, sin, {Gm: 1, Fm: -1, D: 0, Om: 0, Ls: 0, Gs: 1, L2: 0}), Table4Row(0.00003, False, sin, {Gm: 0, Fm: 1, D: -1, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00003, False, sin, {Gm: 0, Fm: 1, D: -2, Om: 1, Ls: 0, Gs: -1, L2: 0}), Table4Row(-0.00003, False, sin, {Gm: 0, Fm: 1, D: -2, Om: -1, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00003, False, sin, {Gm: 1, Fm: 1, D: -2, Om: 1, Ls: 0, Gs: -1, L2: 0}), Table4Row(0.00003, False, sin, {Gm: 0, Fm: 1, D: 0, Om: 0, Ls: 0, Gs: -1, L2: 0}), Table4Row(-0.00003, False, sin, {Gm: 0, Fm: 1, D: -1, Om: 1, Ls: 0, Gs: -1, L2: 0}), Table4Row(-0.00002, False, sin, {Gm: 1, Fm: -1, D: -2, Om: 0, Ls: 0, Gs: 1, L2: 0}), Table4Row(-0.00002, False, sin, {Gm: 0, Fm: 1, D: 0, Om: 0, Ls: 0, Gs: 1, L2: 0}), Table4Row(0.00002, False, sin, {Gm: 1, Fm: 1, D: -1, Om: 1, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00002, False, sin, {Gm: 1, Fm: 1, D: 0, Om: -1, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00002, False, sin, {Gm: 3, Fm: 1, D: 0, Om: 1, Ls: 0, Gs: 0, L2: 0}), Table4Row( -0.00002, False, sin, {Gm: 2, Fm: -1, D: -4, Om: -1, Ls: 0, Gs: 0, L2: 0} ), Table4Row( 0.00002, False, sin, {Gm: 1, Fm: -1, D: -2, Om: -1, Ls: 0, Gs: -1, L2: 0} ), Table4Row(-0.00002, True, sin, {Gm: 1, Fm: -1, D: 0, Om: -1, Ls: 0, Gs: 0, L2: 0}), Table4Row( -0.00002, False, sin, {Gm: 1, Fm: -1, D: -4, Om: -1, Ls: 0, Gs: 0, L2: 0} ), Table4Row(-0.00002, False, sin, {Gm: 1, Fm: 1, D: -4, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00002, False, sin, {Gm: 2, Fm: -1, D: -2, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00002, False, sin, {Gm: 1, Fm: 1, D: 2, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00002, False, sin, {Gm: 1, Fm: 1, D: 0, Om: 0, Ls: 0, Gs: -1, L2: 0}), ] table4_u: List[Table4Row] = [ Table4Row(1, False, cos, {Gm: 0, Fm: 0, D: 0, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.10828, False, cos, {Gm: 1, Fm: 0, D: 0, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.01880, False, cos, {Gm: 1, Fm: 0, D: -2, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.01479, False, cos, {Gm: 0, Fm: 0, D: 2, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00181, False, cos, {Gm: 2, Fm: 0, D: -2, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00147, False, cos, {Gm: 2, Fm: 0, D: 0, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00105, False, cos, {Gm: 0, Fm: 0, D: 2, Om: 0, Ls: 0, Gs: -1, L2: 0}), Table4Row(-0.00075, False, cos, {Gm: 1, Fm: 0, D: -2, Om: 0, Ls: 0, Gs: 1, L2: 0}), Table4Row(-0.00067, False, cos, {Gm: 1, Fm: 0, D: 0, Om: 0, Ls: 0, Gs: -1, L2: 0}), Table4Row(0.00057, False, cos, {Gm: 0, Fm: 0, D: 1, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00055, False, cos, {Gm: 1, Fm: 0, D: 0, Om: 0, Ls: 0, Gs: 1, L2: 0}), Table4Row(-0.00046, False, cos, {Gm: 1, Fm: 0, D: 2, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00041, False, cos, {Gm: 1, Fm: -2, D: 0, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00024, False, cos, {Gm: 0, Fm: 0, D: 0, Om: 0, Ls: 0, Gs: 1, L2: 0}), Table4Row(0.00017, False, cos, {Gm: 0, Fm: 0, D: 2, Om: 0, Ls: 0, Gs: 1, L2: 0}), Table4Row(0.00013, False, cos, {Gm: 1, Fm: 0, D: -2, Om: 0, Ls: 0, Gs: -1, L2: 0}), Table4Row(-0.00010, False, cos, {Gm: 1, Fm: 0, D: -4, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00009, False, cos, {Gm: 0, Fm: 0, D: 1, Om: 0, Ls: 0, Gs: 1, L2: 0}), Table4Row(0.00007, False, cos, {Gm: 2, Fm: 0, D: -2, Om: 0, Ls: 0, Gs: 1, L2: 0}), Table4Row(0.00006, False, cos, {Gm: 3, Fm: 0, D: -2, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00006, False, cos, {Gm: 0, Fm: 2, D: -2, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00005, False, cos, {Gm: 0, Fm: 0, D: 2, Om: 0, Ls: 0, Gs: -2, L2: 0}), Table4Row(-0.00005, False, cos, {Gm: 2, Fm: 0, D: -4, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00005, False, cos, {Gm: 1, Fm: 2, D: -2, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00005, False, cos, {Gm: 1, Fm: 0, D: -1, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00004, False, cos, {Gm: 1, Fm: 0, D: 2, Om: 0, Ls: 0, Gs: -1, L2: 0}), Table4Row(-0.00004, False, cos, {Gm: 3, Fm: 0, D: 0, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00003, False, cos, {Gm: 1, Fm: 0, D: -4, Om: 0, Ls: 0, Gs: 1, L2: 0}), Table4Row(-0.00003, False, cos, {Gm: 2, Fm: -2, D: 0, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00003, False, cos, {Gm: 0, Fm: 2, D: 0, Om: 0, Ls: 0, Gs: 0, L2: 0}), ] table4_w: List[Table4Row] = [ Table4Row(0.10478, False, sin, {Gm: 1, Fm: 0, D: 0, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.04105, False, sin, {Gm: 0, Fm: 2, D: 0, Om: 2, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.02130, False, sin, {Gm: 1, Fm: 0, D: -2, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.01779, False, sin, {Gm: 0, Fm: 2, D: 0, Om: 1, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.01774, False, sin, {Gm: 0, Fm: 0, D: 0, Om: 1, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00987, False, sin, {Gm: 0, Fm: 0, D: 2, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00338, False, sin, {Gm: 1, Fm: -2, D: 0, Om: -2, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00309, False, sin, {Gm: 0, Fm: 0, D: 0, Om: 0, Ls: 0, Gs: 1, L2: 0}), Table4Row(-0.00190, False, sin, {Gm: 0, Fm: 2, D: 0, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00144, False, sin, {Gm: 1, Fm: 0, D: 0, Om: 1, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00144, False, sin, {Gm: 1, Fm: -2, D: 0, Om: -1, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00113, False, sin, {Gm: 1, Fm: 2, D: 0, Om: 2, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00094, False, sin, {Gm: 1, Fm: 0, D: -2, Om: 0, Ls: 0, Gs: 1, L2: 0}), Table4Row(-0.00092, False, sin, {Gm: 2, Fm: 0, D: -2, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00071, False, sin, {Gm: 0, Fm: 0, D: 2, Om: 0, Ls: 0, Gs: -1, L2: 0}), Table4Row(0.00070, False, sin, {Gm: 2, Fm: 0, D: 0, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00067, False, sin, {Gm: 1, Fm: 2, D: -2, Om: 2, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00066, False, sin, {Gm: 0, Fm: 2, D: -2, Om: 1, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00066, False, sin, {Gm: 0, Fm: 0, D: 2, Om: 1, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00061, False, sin, {Gm: 1, Fm: 0, D: 0, Om: 0, Ls: 0, Gs: -1, L2: 0}), Table4Row(-0.00058, False, sin, {Gm: 0, Fm: 0, D: 1, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00049, False, sin, {Gm: 1, Fm: 2, D: 0, Om: 1, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00049, False, sin, {Gm: 1, Fm: 0, D: 0, Om: -1, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00042, False, sin, {Gm: 1, Fm: 0, D: 0, Om: 0, Ls: 0, Gs: 1, L2: 0}), Table4Row(0.00034, False, sin, {Gm: 0, Fm: 2, D: -2, Om: 2, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00026, False, sin, {Gm: 0, Fm: 2, D: -2, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00025, False, sin, {Gm: 1, Fm: -2, D: -2, Om: -2, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00024, False, sin, {Gm: 1, Fm: -2, D: 0, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00023, False, sin, {Gm: 1, Fm: 2, D: -2, Om: 1, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00023, False, sin, {Gm: 1, Fm: 0, D: -2, Om: -1, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00019, False, sin, {Gm: 1, Fm: 0, D: 2, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00012, False, sin, {Gm: 1, Fm: 0, D: -2, Om: 0, Ls: 0, Gs: -1, L2: 0}), Table4Row(0.00011, False, sin, {Gm: 1, Fm: 0, D: -2, Om: 1, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00011, False, sin, {Gm: 1, Fm: -2, D: -2, Om: -1, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00010, False, sin, {Gm: 0, Fm: 0, D: 2, Om: 0, Ls: 0, Gs: 1, L2: 0}), Table4Row(0.00009, False, sin, {Gm: 1, Fm: 0, D: -1, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00008, False, sin, {Gm: 0, Fm: 0, D: 1, Om: 0, Ls: 0, Gs: 1, L2: 0}), Table4Row(-0.00008, False, sin, {Gm: 0, Fm: 2, D: 2, Om: 2, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00008, False, sin, {Gm: 0, Fm: 0, D: 0, Om: 2, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00007, False, sin, {Gm: 0, Fm: 2, D: 0, Om: 2, Ls: 0, Gs: -1, L2: 0}), Table4Row(0.00006, False, sin, {Gm: 0, Fm: 2, D: 0, Om: 2, Ls: 0, Gs: 1, L2: 0}), Table4Row(-0.00005, False, sin, {Gm: 1, Fm: 2, D: 0, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00005, False, sin, {Gm: 3, Fm: 0, D: 0, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row( -0.00005, False, sin, {Gm: 1, Fm: 0, D: 0, Om: 0, Ls: 16, Gs: 0, L2: -18} ), Table4Row(-0.00005, False, sin, {Gm: 2, Fm: 2, D: 0, Om: 2, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00004, True, sin, {Gm: 0, Fm: 2, D: 0, Om: 2, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00004, False, cos, {Gm: 1, Fm: 0, D: 0, Om: 0, Ls: 16, Gs: 0, L2: -18}), Table4Row(-0.00004, False, sin, {Gm: 1, Fm: -2, D: 2, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00004, False, sin, {Gm: 1, Fm: 0, D: -4, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00004, False, sin, {Gm: 3, Fm: 0, D: -2, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00004, False, sin, {Gm: 0, Fm: 2, D: 2, Om: 1, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00004, False, sin, {Gm: 0, Fm: 0, D: 2, Om: -1, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00003, False, sin, {Gm: 0, Fm: 0, D: 0, Om: 0, Ls: 0, Gs: 2, L2: 0}), Table4Row(-0.00003, False, sin, {Gm: 1, Fm: 0, D: -2, Om: 0, Ls: 0, Gs: 2, L2: 0}), Table4Row(0.00003, False, sin, {Gm: 0, Fm: 2, D: -2, Om: 1, Ls: 0, Gs: 1, L2: 0}), Table4Row(-0.00003, False, sin, {Gm: 0, Fm: 0, D: 2, Om: 1, Ls: 0, Gs: -1, L2: 0}), Table4Row(0.00003, False, sin, {Gm: 2, Fm: 2, D: -2, Om: 2, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00003, False, sin, {Gm: 0, Fm: 0, D: 2, Om: 0, Ls: 0, Gs: -2, L2: 0}), Table4Row(-0.00003, False, sin, {Gm: 2, Fm: 0, D: -2, Om: 0, Ls: 0, Gs: 1, L2: 0}), Table4Row(0.00003, False, sin, {Gm: 1, Fm: 2, D: -2, Om: 2, Ls: 0, Gs: 1, L2: 0}), Table4Row(-0.00003, False, sin, {Gm: 2, Fm: 0, D: -4, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00002, False, sin, {Gm: 0, Fm: 2, D: -2, Om: 2, Ls: 0, Gs: 1, L2: 0}), Table4Row(-0.00002, False, sin, {Gm: 2, Fm: 2, D: 0, Om: 1, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00002, False, sin, {Gm: 2, Fm: 0, D: 0, Om: -1, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00002, True, cos, {Gm: 1, Fm: 0, D: 0, Om: 0, Ls: 16, Gs: 0, L2: -18}), Table4Row(0.00002, False, sin, {Gm: 0, Fm: 0, D: 4, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00002, False, sin, {Gm: 0, Fm: 2, D: -1, Om: 2, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00002, False, sin, {Gm: 1, Fm: 2, D: -2, Om: 0, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00002, False, sin, {Gm: 2, Fm: 0, D: 0, Om: 1, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00002, False, sin, {Gm: 2, Fm: -2, D: 0, Om: -1, Ls: 0, Gs: 0, L2: 0}), Table4Row(0.00002, False, sin, {Gm: 1, Fm: 0, D: 2, Om: 0, Ls: 0, Gs: -1, L2: 0}), Table4Row(0.00002, False, sin, {Gm: 2, Fm: 0, D: 0, Om: 0, Ls: 0, Gs: -1, L2: 0}), Table4Row(-0.00002, False, sin, {Gm: 1, Fm: 0, D: -4, Om: 0, Ls: 0, Gs: 1, L2: 0}), Table4Row(0.00002, True, sin, {Gm: 1, Fm: 0, D: 0, Om: 0, Ls: 16, Gs: 0, L2: -18}), Table4Row( -0.00002, False, sin, {Gm: 1, Fm: -2, D: 0, Om: -2, Ls: 0, Gs: -1, L2: 0} ), Table4Row(0.00002, False, sin, {Gm: 2, Fm: -2, D: 0, Om: -2, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00002, False, sin, {Gm: 1, Fm: 0, D: 2, Om: 1, Ls: 0, Gs: 0, L2: 0}), Table4Row(-0.00002, False, sin, {Gm: 1, Fm: -2, D: 2, Om: -1, Ls: 0, Gs: 0, L2: 0}), ] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1666076747.2566774 astral-3.2/src/test/almost_equal.py0000644000000000000000000000111214323450113014367 0ustar00import datetime def datetime_almost_equal( datetime1: datetime.datetime, datetime2: datetime.datetime, seconds: int = 60 ): if not (datetime1.tzinfo): datetime1 = datetime1.replace(tzinfo=datetime.timezone.utc) else: datetime1 = datetime1.astimezone(datetime.timezone.utc) if not (datetime2.tzinfo): datetime2 = datetime2.replace(tzinfo=datetime.timezone.utc) else: datetime2 = datetime2.astimezone(datetime.timezone.utc) dd = datetime1 - datetime2 sd = (dd.days * 24 * 60 * 60) + dd.seconds return abs(sd) <= seconds ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1667670964.1174853 astral-3.2/src/test/conftest.py0000644000000000000000000000275714331521664013557 0ustar00import pytest from astral import LocationInfo from astral.geocoder import LocationDatabase, database from astral.location import Location @pytest.fixture def test_database() -> LocationDatabase: return database() @pytest.fixture def london_info() -> LocationInfo: # return LocationInfo("London", "England", "Europe/London", 51.50853, -0.12574) return LocationInfo("London", "England", "Europe/London", 51.5, -0.1333333) @pytest.fixture def london(london_info: LocationInfo) -> Location: return Location(london_info) @pytest.fixture def new_delhi_info() -> LocationInfo: return LocationInfo("New Delhi", "India", "Asia/Kolkata", 28.61, 77.22) @pytest.fixture def new_delhi(new_delhi_info: LocationInfo) -> Location: return Location(new_delhi_info) @pytest.fixture def riyadh_info() -> LocationInfo: return LocationInfo("Riyadh", "Saudi Arabia", "Asia/Riyadh", 24.71355, 46.67530) @pytest.fixture def riyadh(riyadh_info: LocationInfo) -> Location: return Location(riyadh_info) @pytest.fixture def wellington_info() -> LocationInfo: return LocationInfo( "Wellington", "New Zealand", "Pacific/Auckland", -41.33, 174.766666 ) @pytest.fixture def wellington(wellington_info: LocationInfo) -> Location: return Location(wellington_info) @pytest.fixture def tromso_info() -> LocationInfo: return LocationInfo("Tromso", "Norway", "CET", 69.6, 18.95) @pytest.fixture def tromso(tromso_info: LocationInfo) -> Location: return Location(tromso_info) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1667670964.1174853 astral-3.2/src/test/moon/test_moon.py0000644000000000000000000001135314331521664014701 0ustar00# -*- coding: utf-8 -*- import datetime import pytest try: import zoneinfo except ImportError: from backports import zoneinfo from almost_equal import datetime_almost_equal from astral import moon from astral.location import Location @pytest.mark.parametrize( "date_,phase", [ (datetime.date(2015, 12, 1), 19.477889), (datetime.date(2015, 12, 2), 20.333444), (datetime.date(2015, 12, 3), 21.189000), (datetime.date(2014, 12, 1), 9.0556666), (datetime.date(2014, 12, 2), 10.066777), (datetime.date(2014, 1, 1), 27.955666), ], ) def test_moon_phase(date_: datetime.date, phase: float): """Test moon phase calculation""" assert moon.phase(date_) == pytest.approx(phase, abs=0.001) # type: ignore @pytest.mark.parametrize( "date_,risetime", [ (datetime.date(2022, 11, 30), datetime.datetime(2022, 11, 30, 13, 17, 0)), (datetime.date(2022, 1, 1), datetime.datetime(2022, 1, 1, 6, 55, 0)), (datetime.date(2022, 2, 1), datetime.datetime(2022, 2, 1, 8, 24, 0)), ], ) def test_moonrise_utc( date_: datetime.date, risetime: datetime.datetime, london: Location ): risetime = risetime.replace(tzinfo=london.tzinfo) calc_time = moon.moonrise(london.observer, date_) assert calc_time is not None assert datetime_almost_equal(calc_time, risetime, seconds=300) @pytest.mark.parametrize( "date_,settime", [ (datetime.date(2021, 10, 28), datetime.datetime(2021, 10, 28, 14, 11, 0)), (datetime.date(2021, 11, 6), datetime.datetime(2021, 11, 6, 17, 21, 0)), (datetime.date(2022, 2, 1), datetime.datetime(2022, 2, 1, 16, 57, 0)), ], ) def test_moonset_utc( date_: datetime.date, settime: datetime.datetime, london: Location ): settime = settime.replace(tzinfo=datetime.timezone.utc) calc_time = moon.moonset(london.observer, date_) assert calc_time is not None assert datetime_almost_equal(calc_time, settime, seconds=180) @pytest.mark.parametrize( "date_,risetime", [ (datetime.date(2022, 5, 1), datetime.datetime(2022, 5, 1, 2, 34, 0)), (datetime.date(2022, 5, 24), datetime.datetime(2022, 5, 24, 22, 59, 0)), ], ) def test_moonrise_riyadh_utc( date_: datetime.date, risetime: datetime.datetime, riyadh: Location ): risetime = risetime.replace(tzinfo=datetime.timezone.utc) calc_time = moon.moonrise(riyadh.observer, date_) assert calc_time is not None assert datetime_almost_equal(calc_time, risetime, seconds=180) @pytest.mark.parametrize( "date_,settime", [ (datetime.date(2021, 10, 28), datetime.datetime(2021, 10, 28, 9, 26, 0)), (datetime.date(2021, 11, 6), datetime.datetime(2021, 11, 6, 15, 33, 0)), (datetime.date(2022, 2, 1), datetime.datetime(2022, 2, 1, 14, 54, 0)), ], ) def test_moonset_riyadh_utc( date_: datetime.date, settime: datetime.datetime, riyadh: Location ): settime = settime.replace(tzinfo=datetime.timezone.utc) calc_time = moon.moonset(riyadh.observer, date_) assert calc_time is not None assert datetime_almost_equal(calc_time, settime, seconds=180) @pytest.mark.parametrize( "date_,risetime", [ (datetime.date(2021, 10, 28), datetime.datetime(2021, 10, 28, 2, 6, 0)), (datetime.date(2021, 11, 6), datetime.datetime(2021, 11, 6, 6, 45, 0)), ], ) def test_moonrise_wellington( date_: datetime.date, risetime: datetime.datetime, wellington: Location ): risetime = risetime.replace(tzinfo=wellington.tz) calc_time = moon.moonrise(wellington.observer, date_, tzinfo=wellington.tz) assert calc_time is not None calc_time = calc_time.astimezone(wellington.tzinfo) assert datetime_almost_equal(calc_time, risetime, seconds=120) @pytest.mark.parametrize( "date_,settime", [ (datetime.date(2021, 8, 18), datetime.datetime(2021, 8, 18, 3, 31, 0)), (datetime.date(2021, 7, 8), datetime.datetime(2021, 7, 8, 15, 16, 0)), ], ) def test_moonset_wellington( date_: datetime.date, settime: datetime.datetime, wellington: Location ): settime = settime.replace(tzinfo=wellington.tzinfo) calc_time = moon.moonset(wellington.observer, date_, wellington.tzinfo) assert calc_time is not None calc_time = calc_time.astimezone(wellington.tzinfo) assert datetime_almost_equal(calc_time, settime, seconds=120) # @pytest.mark.parametrize( # "longitude,jd", # [ # (datetime.date(2021, 10, 28), datetime.datetime(2021, 10, 28, 13, 48, 0)), # (datetime.date(2021, 11, 6), datetime.datetime(2021, 11, 6, 7, 27, 0)), # # (datetime.date(2022, 2, 1), datetime.datetime(2022, 2, 1, 8, 24, 0)), # ], # ) # def test_moon_local_sidereal_time(longitude: float, jd: float): # moon.local_sidereal_time(longitude, jd) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1667670964.1174853 astral-3.2/src/test/moon/test_moon_azimuth.py0000644000000000000000000000165114331521664016442 0ustar00import datetime import pytest from astral import Observer from astral.location import Location from astral.moon import azimuth @pytest.mark.parametrize( "dt,value", [ (datetime.datetime(2022, 10, 6, 1, 10, 0), 240.0), (datetime.datetime(2022, 10, 6, 16, 45, 0), 115.0), (datetime.datetime(2022, 10, 10, 6, 43, 0), 281.0), (datetime.datetime(2022, 10, 10, 3, 0, 0), 235.0), ], ) def test_moon_azimuth(dt: datetime.datetime, value: float, london: Location): az = azimuth(london.observer, dt) assert pytest.approx(az, abs=1) == value # type: ignore def print_moon_azimuth(): o = Observer(51.5, -0.13) for hour in range(24): d = datetime.datetime(2022, 10, 10, hour, 0, 0) print(hour, " 0", azimuth(o, d)) d = datetime.datetime(2022, 10, 10, hour, 30, 0) print(hour, "30", azimuth(o, d)) if __name__ == "__main__": print_moon_azimuth() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1667670964.1174853 astral-3.2/src/test/moon/test_moon_position.py0000644000000000000000000000072014331521664016621 0ustar00from datetime import date from astral.moon import julianday, moon_position def test_moon_position(): d = date(1969, 6, 28) jd = julianday(d) jd2000 = jd - 2451545 # Julian day relative to Jan 1.5, 2000 _pos1 = moon_position(jd2000) d = date(1992, 4, 12) jd = julianday(d) jd2000 = jd - 2451545 # Julian day relative to Jan 1.5, 2000 _pos2 = moon_position(jd2000) pass if __name__ == "__main__": test_moon_position() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1666076747.2576776 astral-3.2/src/test/moon/test_moon_rise.py0000644000000000000000000000003614323450113015706 0ustar00def test_moon_rise(): ... ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1666076747.2576776 astral-3.2/src/test/moon/test_sidereal_time.py0000644000000000000000000000144614323450113016530 0ustar00import datetime from astral import hours_to_time from astral.sidereal import gmst, lmst def test_gmst(): dt = datetime.datetime(1987, 4, 10, 0, 0, 0) mean_sidereal_time = gmst(dt) t = hours_to_time(mean_sidereal_time / 15) assert t.hour == 13 assert t.minute == 10 assert t.second == 46 assert t.microsecond == 366821 def test_gmst_with_time(): dt = datetime.datetime(1987, 4, 10, 19, 21, 0) mean_sidereal_time = gmst(dt) t = hours_to_time(mean_sidereal_time / 15) assert t.hour == 8 assert t.minute == 34 assert t.second == 57 assert t.microsecond == 89578 def test_local_mean_sidereal_time(): dt = datetime.datetime(1987, 4, 10, 0, 0, 0) mean_sidereal_time = lmst(dt, -0.13) assert mean_sidereal_time == 197.693195090862 - 0.13 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1667670964.1174853 astral-3.2/src/test/test_Location.py0000644000000000000000000002035214331521664014530 0ustar00# -*- coding: utf-8 -*- import dataclasses import datetime try: import zoneinfo except ImportError: from backports import zoneinfo import freezegun import pytest from almost_equal import datetime_almost_equal from astral import LocationInfo from astral.location import Location class TestLocation: """Tests for the Location class""" def test_Name(self): """Test the default name and that the name is changeable""" c = Location() assert c.name == "Greenwich" c.name = "Köln" assert c.name == "Köln" def test_Region(self): """Test the default region and that the region is changeable""" c = Location() assert c.region == "England" c.region = "Australia" assert c.region == "Australia" def test_TimezoneName(self): """Test the default timezone and that the timezone is changeable""" c = Location() assert c.timezone == "Europe/London" c.name = "Asia/Riyadh" assert c.name == "Asia/Riyadh" def test_TimezoneNameBad(self): """Test that an exception is raised if an invalid timezone is specified""" c = Location() with pytest.raises(ValueError): c.timezone = "bad/timezone" def test_TimezoneLookup(self): """Test that tz refers to a timezone object""" c = Location() assert c.tz == zoneinfo.ZoneInfo("Europe/London") # type: ignore c.timezone = "Europe/Stockholm" assert c.tz == zoneinfo.ZoneInfo("Europe/Stockholm") # type: ignore def test_Info(self, london: Location, london_info: LocationInfo): assert london_info == london.info def test_Sun(self, london: Location): """Test Location's version of the sun calculation""" ldt = datetime.datetime(2015, 8, 1, 5, 23, 20, tzinfo=london.tz) sunrise = london.sun(datetime.date(2015, 8, 1))["sunrise"] assert datetime_almost_equal(sunrise, ldt) def test_Dawn(self, london: Location): """Test Location returns dawn times in the local timezone""" ldt = datetime.datetime(2015, 8, 1, 4, 41, 44, tzinfo=london.tz) dawn = london.dawn(datetime.date(2015, 8, 1)) assert datetime_almost_equal(dawn, ldt) # assert dawn.tzinfo.zone == london.tzinfo.zone def test_DawnUTC(self, london: Location): """Test Location returns dawn times in the UTC timezone""" udt = datetime.datetime(2015, 8, 1, 3, 41, 44, tzinfo=datetime.timezone.utc) dawn = london.dawn(datetime.date(2015, 8, 1), local=False) assert datetime_almost_equal(dawn, udt) # assert dawn.tzinfo.zone == datetime.timezone.utc.zone def test_Sunrise(self, london: Location): ldt = datetime.datetime(2015, 8, 1, 5, 23, 20, tzinfo=london.tz) sunrise = london.sunrise(datetime.date(2015, 8, 1)) assert datetime_almost_equal(sunrise, ldt) # assert sunrise.tzinfo.zone == london.tzinfo.zone def test_SunriseUTC(self, london: Location): udt = datetime.datetime(2015, 8, 1, 4, 23, 20, tzinfo=datetime.timezone.utc) sunrise = london.sunrise(datetime.date(2015, 8, 1), local=False) assert datetime_almost_equal(sunrise, udt) # assert sunrise.tzinfo.zone == datetime.timezone.utc.zone def test_SolarNoon(self, london: Location): ldt = datetime.datetime(2015, 8, 1, 13, 6, 53, tzinfo=london.tz) noon = london.noon(datetime.date(2015, 8, 1)) assert datetime_almost_equal(noon, ldt) # assert noon.tzinfo.zone == london.tzinfo.zone def test_SolarNoonUTC(self, london: Location): udt = datetime.datetime(2015, 8, 1, 12, 6, 53, tzinfo=datetime.timezone.utc) noon = london.noon(datetime.date(2015, 8, 1), local=False) assert datetime_almost_equal(noon, udt) # assert noon.tzinfo.zone == datetime.timezone.utc.zone def test_Dusk(self, london: Location): ldt = datetime.datetime(2015, 12, 1, 16, 35, 11, tzinfo=london.tz) dusk = london.dusk(datetime.date(2015, 12, 1)) assert datetime_almost_equal(dusk, ldt) # assert dusk.tzinfo.zone == london.tzinfo.zone def test_DuskUTC(self, london: Location): udt = datetime.datetime(2015, 12, 1, 16, 35, 11, tzinfo=datetime.timezone.utc) dusk = london.dusk(datetime.date(2015, 12, 1), local=False) assert datetime_almost_equal(dusk, udt) # assert dusk.tzinfo.zone == datetime.timezone.utc.zone def test_Sunset(self, london: Location): ldt = datetime.datetime(2015, 12, 1, 15, 55, 29, tzinfo=london.tz) sunset = london.sunset(datetime.date(2015, 12, 1)) assert datetime_almost_equal(sunset, ldt) # assert sunset.tzinfo.zone == london.tzinfo.zone def test_SunsetUTC(self, london: Location): udt = datetime.datetime(2015, 12, 1, 15, 55, 29, tzinfo=datetime.timezone.utc) sunset = london.sunset(datetime.date(2015, 12, 1), local=False) assert datetime_almost_equal(sunset, udt) # assert sunset.tzinfo.zone == datetime.timezone.utc.zone def test_SolarElevation(self, riyadh: Location): dt = datetime.datetime(2015, 12, 14, 8, 0, 0, tzinfo=riyadh.tz) elevation = riyadh.solar_elevation(dt) assert abs(elevation - 17) < 0.5 def test_SolarAzimuth(self, riyadh: Location): dt = datetime.datetime(2015, 12, 14, 8, 0, 0, tzinfo=riyadh.tz) azimuth = riyadh.solar_azimuth(dt) assert abs(azimuth - 126) < 0.5 def test_TimeAtAltitude(self, new_delhi: Location): test_data = {datetime.date(2016, 1, 5): datetime.datetime(2016, 1, 5, 10, 0)} for day, cdt in test_data.items(): cdt = cdt.replace(tzinfo=new_delhi.tz) dt = new_delhi.time_at_elevation(28, day) assert datetime_almost_equal(dt, cdt, seconds=600) def test_SolarDepression(self): c = Location( LocationInfo("Heidelberg", "Germany", "Europe/Berlin", 49.412, -8.71) ) c.solar_depression = "nautical" assert c.solar_depression == 12 c.solar_depression = 18 assert c.solar_depression == 18 def test_BadSolarDepression(self): loc = Location() with pytest.raises(KeyError): loc.solar_depression = "uncivil" def test_Moon(self): d = datetime.date(2017, 12, 1) c = Location() assert c.moon_phase(date=d) == pytest.approx(11.62, abs=0.01) # type: ignore @freezegun.freeze_time("2015-12-01") def test_MoonNoDate(self): c = Location() assert c.moon_phase() == pytest.approx(19.47, abs=0.01) # type: ignore def test_TzError(self): with pytest.raises(AttributeError): c = Location() c.tz = 1 # type: ignore def test_Equality(self): c1 = Location() c2 = Location() assert c1 == c2 def test_LocationEquality_NotEqual(self, london_info: LocationInfo): location1 = Location(london_info) location2 = Location(london_info) location2.latitude = 23.0 assert location2 != location1 def test_LocationEquality_NotALocation(self, london_info: LocationInfo): location = Location(london_info) class NotALocation: _location_info = london_info assert NotALocation() != location def test_SetLatitudeFloat(self): loc = Location() loc.latitude = 34.0 assert loc.latitude == 34.0 def test_SetLatitudeString(self): loc = Location() loc.latitude = "24°28'N" assert loc.latitude == pytest.approx(24.46666666666666) # type: ignore def test_SetLongitudeFloat(self): loc = Location() loc.longitude = 24.0 assert loc.longitude == 24.0 def test_SetLongitudeString(self): loc = Location() loc.longitude = "24°28'S" assert loc.longitude == pytest.approx(-24.46666666666666) # type: ignore def test_SetBadLongitudeString(self): loc = Location() with pytest.raises(ValueError): loc.longitude = "wibble" def test_BadTzinfo(self): loc = Location() loc._location_info = dataclasses.replace( # type: ignore loc._location_info, timezone="Bad/Timezone" # type: ignore ) with pytest.raises(ValueError): loc.tzinfo ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1666076747.2576776 astral-3.2/src/test/test_Repr.py0000644000000000000000000000124714323450113013661 0ustar00# -*- coding: utf-8 -*- from astral import LocationInfo from astral.location import Location class TestLocationRepr: def test_default(self): l = Location() assert ( l.__repr__() == "Greenwich/England, tz=Europe/London, lat=51.47, lon=-0.00" ) def test_full(self): l = Location(LocationInfo("London", "England", "Europe/London", 51.68, -0.05)) assert l.__repr__() == "London/England, tz=Europe/London, lat=51.68, lon=-0.05" def test_no_region(self): l = Location(LocationInfo("London", None, "Europe/London", 51.68, -0.05)) assert l.__repr__() == "London, tz=Europe/London, lat=51.68, lon=-0.05" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1667670964.1174853 astral-3.2/src/test/test_all.py0000644000000000000000000000035314331521664013527 0ustar00from astral.geocoder import LocationDatabase, all_locations from astral.sun import noon def test_AllLocations(test_database: LocationDatabase): for location in all_locations(test_database): noon(location.observer) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1666076747.2576776 astral-3.2/src/test/test_almost_equal.py0000644000000000000000000000124414323450113015434 0ustar00import datetime from almost_equal import datetime_almost_equal class TestDateTimeAlmostEqual: """Test the datetime comparison function""" def test_equal(self): d1 = datetime.datetime(2019, 1, 1) d2 = datetime.datetime(2019, 1, 1) assert datetime_almost_equal(d1, d2) def test_not_equal(self): d1 = datetime.datetime(2019, 1, 1) d2 = datetime.datetime(2019, 1, 1, 12, 2, 0) assert not datetime_almost_equal(d1, d2) def test_equal_with_delta(self): d1 = datetime.datetime(2019, 1, 1, 12, 0, 0) d2 = datetime.datetime(2019, 1, 1, 12, 2, 0) assert datetime_almost_equal(d1, d2, 121) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1667670964.1174853 astral-3.2/src/test/test_buenos_aries.py0000644000000000000000000000046414331521664015440 0ustar00# -*- coding: utf-8 -*- from astral.geocoder import LocationDatabase, lookup from astral.location import LocationInfo def test_BuenosAries(test_database: LocationDatabase): b = lookup("Buenos Aires", test_database) assert isinstance(b, LocationInfo) assert b.timezone == "America/Buenos_Aires" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1667670964.1174853 astral-3.2/src/test/test_depression_not_reached.py0000644000000000000000000000103514331521664017463 0ustar00# -*- coding: utf-8 -*- import datetime import pytest from astral import LocationInfo from astral.location import Location def test_Dawn_NeverReachesDepression(): d = datetime.date(2016, 5, 29) with pytest.raises(ValueError): loc = Location( LocationInfo( "Ghent", "Belgium", "Europe/Brussels", "51°3'N", "3°44'W", ) # type: ignore ) loc.solar_depression = 18 loc.dawn(date=d, local=True) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1667670964.1174853 astral-3.2/src/test/test_geocoder.py0000644000000000000000000001262014331521664014546 0ustar00# -*- coding: utf-8 -*- from functools import reduce from typing import List try: import zoneinfo except ImportError: from backports import zoneinfo from pytest import approx, raises # type: ignore import astral.geocoder from astral import LocationInfo from astral.geocoder import LocationDatabase def location_count(name: str, locations: List[LocationInfo]): return len(list(filter(lambda item: item.name == name, locations))) def db_location_count(db: LocationDatabase) -> int: # type: ignore """Returns the count of the locations currently in the database""" return reduce(lambda count, group: count + len(group), db.values(), 0) class TestDatabase: """Test database access functions""" def test_all_locations(self, test_database: astral.geocoder.LocationDatabase): for loc in astral.geocoder.all_locations(test_database): assert loc.name location_list = astral.geocoder.all_locations(test_database) all_locations = list(location_list) assert location_count("London", all_locations) == 1 assert location_count("Abu Dhabi", all_locations) == 2 def test_lookup(self, test_database: astral.geocoder.LocationDatabase): loc = astral.geocoder.lookup("London", test_database) assert isinstance(loc, LocationInfo) assert loc.name == "London" assert loc.region == "England" assert loc.latitude == approx(51.4733, abs=0.001) assert loc.longitude == approx(-0.0008333, abs=0.000001) tz = zoneinfo.ZoneInfo("Europe/London") # type: ignore tzl = zoneinfo.ZoneInfo(loc.timezone) # type: ignore assert tz == tzl def test_city_in_db(self, test_database: astral.geocoder.LocationDatabase): astral.geocoder.lookup("london", test_database) def test_group_in_db(self, test_database: astral.geocoder.LocationDatabase): astral.geocoder.lookup("africa", test_database) def test_location_not_in_db(self, test_database: astral.geocoder.LocationDatabase): with raises(KeyError): astral.geocoder.lookup("Nowhere", test_database) def test_group_not_in_db(self, test_database: astral.geocoder.LocationDatabase): with raises(KeyError): astral.geocoder.group("wallyland", test_database) def test_lookup_city_and_region( self, test_database: astral.geocoder.LocationDatabase ): city_name = "Birmingham,England" city = astral.geocoder.lookup(city_name, test_database) assert isinstance(city, LocationInfo) assert city.name == "Birmingham" assert city.region == "England" def test_country_with_multiple_entries_no_country( self, test_database: astral.geocoder.LocationDatabase ): city = astral.geocoder.lookup("Abu Dhabi", test_database) assert isinstance(city, LocationInfo) assert city.name == "Abu Dhabi" def test_country_with_multiple_entries_with_country( self, test_database: astral.geocoder.LocationDatabase ): """Test for fix made due to bug report from Klaus Alexander Seistrup""" city = astral.geocoder.lookup("Abu Dhabi,United Arab Emirates", test_database) assert isinstance(city, LocationInfo) assert city.name == "Abu Dhabi" city = astral.geocoder.lookup("Abu Dhabi,UAE", test_database) assert isinstance(city, LocationInfo) assert city.name == "Abu Dhabi" class TestBugReports: """Test for bug report fixes""" def test_Adelaide(self, test_database: astral.geocoder.LocationDatabase): """Test for fix made due to bug report from Klaus Alexander Seistrup""" astral.geocoder.lookup("Adelaide", test_database) def test_CandianCities(self, test_database: astral.geocoder.LocationDatabase): astral.geocoder.lookup("Fredericton", test_database) class TestDatabaseAddLocations: """Test adding locations to database""" def test_newline_at_end(self, test_database: astral.geocoder.LocationDatabase): count = db_location_count(test_database) astral.geocoder.add_locations( "A Place,A Region,Asia/Nicosia,35°10'N,33°25'E,162.0\n", test_database ) assert db_location_count(test_database) == count + 1 def test_from_list_of_strings( self, test_database: astral.geocoder.LocationDatabase ): count = db_location_count(test_database) astral.geocoder.add_locations( [ "A Place,A Region,Asia/Nicosia,35°10'N,33°25'E,162.0", "Another Place,Somewhere else,Asia/Nicosia,35°10'N,33°25'E,162.0", ], test_database, ) assert db_location_count(test_database) == count + 2 def test_from_list_of_lists(self, test_database: astral.geocoder.LocationDatabase): count = db_location_count(test_database) astral.geocoder.add_locations( [ ["A Place", "A Region", "Asia/Nicosia", "35°10'N", "33°25'E", "162.0"], [ "Another Place", "Somewhere else", "Asia/Nicosia", "35°10'N", "33°25'E", "162.0", ], ], test_database, ) assert db_location_count(test_database) == count + 2 def test_SanitizeKey(): assert astral.geocoder._sanitize_key("Los Angeles") == "los_angeles" # type: ignore ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1667670964.1174853 astral-3.2/src/test/test_julian.py0000644000000000000000000000570714331521664014251 0ustar00import datetime from typing import Union import pytest from almost_equal import datetime_almost_equal from astral.julian import ( Calendar, juliancentury_to_julianday, julianday, julianday_to_datetime, julianday_to_juliancentury, ) @pytest.mark.parametrize( "day,jd", [ (datetime.datetime(1957, 10, 4, 19, 26, 24), 2436116.31), (datetime.date(2000, 1, 1), 2451544.5), (datetime.date(2012, 1, 1), 2455927.5), (datetime.date(2013, 1, 1), 2456293.5), (datetime.date(2013, 6, 1), 2456444.5), (datetime.date(1867, 2, 1), 2402998.5), (datetime.date(3200, 11, 14), 2890153.5), (datetime.datetime(2000, 1, 1, 12, 0, 0), 2451545.0), (datetime.datetime(1999, 1, 1, 0, 0, 0), 2451179.5), (datetime.datetime(1987, 1, 27, 0, 0, 0), 2446_822.5), (datetime.date(1987, 6, 19), 2446_965.5), (datetime.datetime(1987, 6, 19, 12, 0, 0), 2446_966.0), (datetime.datetime(1988, 1, 27, 0, 0, 0), 2447_187.5), (datetime.date(1988, 6, 19), 2447_331.5), (datetime.datetime(1988, 6, 19, 12, 0, 0), 2447_332.0), (datetime.datetime(1900, 1, 1, 0, 0, 0), 2415_020.5), (datetime.datetime(1600, 1, 1, 0, 0, 0), 2305_447.5), (datetime.datetime(1600, 12, 31, 0, 0, 0), 2305_812.5), (datetime.datetime(2012, 1, 1, 12), 2455928.0), (datetime.date(2013, 1, 1), 2456293.5), (datetime.date(2013, 6, 1), 2456444.5), (datetime.date(1867, 2, 1), 2402998.5), (datetime.date(3200, 11, 14), 2890153.5), ], ) def test_JulianDay(day: Union[datetime.date, datetime.datetime], jd: float): assert julianday(day) == jd @pytest.mark.parametrize( "day,jd", [ (datetime.datetime(837, 4, 10, 7, 12, 0), 2026_871.8), (datetime.datetime(333, 1, 27, 12, 0, 0), 1842_713.0), ], ) def test_JulianDay_JulianCalendar( day: Union[datetime.date, datetime.datetime], jd: float ): assert julianday(day, Calendar.JULIAN) == jd @pytest.mark.parametrize( "jd,dt", [ (2026_871.8, datetime.datetime(837, 4, 10, 7, 12, 0)), (1842_713.0, datetime.datetime(333, 1, 27, 12, 0, 0)), ], ) def test_JulianDay_ToDateTime(jd: float, dt: datetime.datetime): assert datetime_almost_equal(julianday_to_datetime(jd), dt) @pytest.mark.parametrize( "jd,jc", [ (2455927.5, 0.119986311), (2456293.5, 0.130006845), (2456444.5, 0.134140999), (2402998.5, -1.329130732), (2890153.5, 12.00844627), ], ) def test_JulianCentury(jd: float, jc: float): assert julianday_to_juliancentury(jd) == pytest.approx(jc) @pytest.mark.parametrize( "jc,jd", [ (0.119986311, 2455927.5), (0.130006845, 2456293.5), (0.134140999, 2456444.5), (-1.329130732, 2402998.5), (12.00844627, 2890153.5), ], ) def test_JulianCenturyToJulianDay(jc: float, jd: float): assert juliancentury_to_julianday(jc) == pytest.approx(jd) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1667670964.1184852 astral-3.2/src/test/test_location_info.py0000644000000000000000000000176314331521664015610 0ustar00import pytest from astral import LocationInfo try: import zoneinfo except ImportError: from backports import zoneinfo class TestLocationInfo: def test_Default(self): loc = LocationInfo() assert loc.name == "Greenwich" assert loc.region == "England" assert loc.timezone == "Europe/London" assert loc.latitude == pytest.approx(51.4733, abs=0.001) assert loc.longitude == pytest.approx(-0.0008333, abs=0.000001) def test_bad_latitude(self): with pytest.raises(ValueError): LocationInfo("A place", "Somewhere", "Europe/London", "i", 2) def test_bad_longitude(self): with pytest.raises(ValueError): LocationInfo("A place", "Somewhere", "Europe/London", 2, "i") def test_timezone_group(self): li = LocationInfo() assert li.timezone_group == "Europe" def test_tzinfo(self, new_delhi_info: LocationInfo): assert new_delhi_info.tzinfo == zoneinfo.ZoneInfo("Asia/Kolkata") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1667670964.1184852 astral-3.2/src/test/test_misc.py0000644000000000000000000000465114331521664013717 0ustar00from datetime import timedelta try: import zoneinfo except ImportError: from backports import zoneinfo import freezegun from pytest import approx, raises from astral import dms_to_float, now, today from astral.sun import minutes_to_timedelta def test_MinutesToTimedelta(): assert minutes_to_timedelta(720) == timedelta(seconds=720 * 60) assert minutes_to_timedelta(722) == timedelta(seconds=722 * 60) assert minutes_to_timedelta(722.2) == timedelta(seconds=722.2 * 60) assert minutes_to_timedelta(722.5) == timedelta(seconds=722.5 * 60) class TestDMS: """Test degrees/minutes/seconds conversion functions""" def test_north(self): assert dms_to_float("24°28'N", 90) == approx(24.466666) def test_whole_number_of_degrees(self): assert dms_to_float("24°", 90.0) == 24.0 def test_east(self): assert dms_to_float("54°22'E", 180.0) == approx(54.366666, abs=0.00001) def test_south(self): assert dms_to_float("37°58'S", 90.0) == approx(-37.966666, abs=0.00001) def test_west(self): assert dms_to_float("171°50'W", 180.0) == approx(-171.8333333, abs=0.00001) def test_west_lowercase(self): assert dms_to_float("171°50'w", 180.0) == approx(-171.8333333, abs=0.00001) def test_float(self): assert dms_to_float("0.2", 90.0) == 0.2 def test_not_a_float(self): with raises(ValueError): dms_to_float("x", 90.0) def test_latlng_outside_limit(self): assert dms_to_float("180°50'w", 180.0) == -180 class TestToday: @freezegun.freeze_time("2020-01-01 14:00:00") def test_default_timezone(self): td = today() assert td.year == 2020 assert td.month == 1 assert td.day == 1 @freezegun.freeze_time("2020-01-01 14:00:00") def test_australia(self): assert today(zoneinfo.ZoneInfo("Australia/Melbourne")).day == 2 @freezegun.freeze_time("2020-01-02 05:00:00") def test_adak(self): assert today(zoneinfo.ZoneInfo("America/Adak")).day == 1 class TestNow: @freezegun.freeze_time("2020-01-01 14:10:20") def test_default_timezone(self): td = now() assert td.hour == 14 assert td.minute == 10 assert td.second == 20 @freezegun.freeze_time("2020-01-01 14:20:00") def test_australia(self): td = now(zoneinfo.ZoneInfo("Australia/Melbourne")) assert td.hour == 1 assert td.minute == 20 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1667670964.1184852 astral-3.2/src/test/test_norway.py0000644000000000000000000000177714331521664014311 0ustar00from datetime import datetime, timedelta, timezone import pytest import astral from astral import sun from astral.location import Location def _next_event(obs: astral.Observer, dt: datetime, event: str): for offset in range(0, 365): newdate = dt + timedelta(days=offset) try: t = getattr(sun, event)(date=newdate, observer=obs) return t except ValueError: pass assert False, "Should be unreachable" # pragma: no cover def test_NorwaySunUp(tromso: Location): """Test location in Norway where the sun doesn't set in summer.""" june = datetime(2019, 6, 5, tzinfo=timezone.utc) with pytest.raises(ValueError): sun.sunrise(tromso.observer, june) with pytest.raises(ValueError): sun.sunset(tromso.observer, june) # Find the next sunset and sunrise: next_sunrise = _next_event(tromso.observer, june, "sunrise") next_sunset = _next_event(tromso.observer, june, "sunset") assert next_sunset < next_sunrise ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1666076747.2576776 astral-3.2/src/test/test_observer.py0000644000000000000000000000270714323450113014602 0ustar00import pytest from astral import Observer class TestObserver: def test_default(self): obs = Observer() assert obs.latitude == 51.4733 assert obs.longitude == -0.0008333 assert obs.elevation == 0.0 def test_from_float(self): obs = Observer(1, 1, 1) assert obs.latitude == 1.0 assert obs.longitude == 1.0 assert obs.elevation == 1.0 def test_from_string(self): obs = Observer("1", "2", "3") assert obs.latitude == 1.0 assert obs.longitude == 2.0 assert obs.elevation == 3.0 def test_from_dms(self): obs = Observer("24°N", "22°30'S", "3") assert obs.latitude == 24.0 assert obs.longitude == -22.5 assert obs.elevation == 3.0 def test_bad_latitude(self): with pytest.raises(ValueError): Observer("o", 1, 1) def test_bad_longitude(self): with pytest.raises(ValueError): Observer(1, "o", 1) def test_bad_elevation(self): with pytest.raises(ValueError): Observer(1, 1, "o") def test_latitude_outside_limits(self): obs = Observer(90.1, 0, 0) assert obs.latitude == 90.0 obs = Observer(-90.1, 0, 0) assert obs.latitude == -90.0 def test_longitude_outside_limits(self): obs = Observer(0, 180.1, 0) assert obs.longitude == 180.0 obs = Observer(0, -180.1, 0) assert obs.longitude == -180.0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1667670964.1184852 astral-3.2/src/test/test_sun_calc.py0000644000000000000000000001424314331521664014551 0ustar00import datetime import freezegun import pytest from astral import Observer, sun, today from astral.location import Location @pytest.mark.parametrize( "jc,gmls", [ (-1.329130732, 310.7374254), (12.00844627, 233.8203529), (0.184134155, 69.43779106), ], ) def test_GeomMeanLongSun(jc: float, gmls: float): assert sun.geom_mean_long_sun(jc) == pytest.approx(gmls) # type: ignore @pytest.mark.parametrize( "jc,gmas", [ (0.119986311, 4676.922342), (12.00844627, 432650.1681), (0.184134155, 6986.1838), ], ) def test_GeomAnomolyLongSun(jc: float, gmas: float): assert sun.geom_mean_anomaly_sun(jc) == pytest.approx(gmas) # type: ignore @pytest.mark.parametrize( "jc,eeo", [ (0.119986311, 0.016703588), (12.00844627, 0.016185564), (0.184134155, 0.016700889), ], ) def test_EccentricityEarthOrbit(jc: float, eeo: float): assert sun.eccentric_location_earth_orbit(jc) == pytest.approx(eeo, abs=1e-6) # type: ignore @pytest.mark.parametrize( "jc,eos", [ (0.119986311, -0.104951648), (12.00844627, -1.753028843), (0.184134155, 1.046852316), ], ) def test_SunEqOfCenter(jc: float, eos: float): assert sun.sun_eq_of_center(jc) == pytest.approx(eos, abs=1e-6) # type: ignore @pytest.mark.parametrize( "jc,stl", [ (0.119986311, 279.9610686), (12.00844627, 232.0673358), (0.184134155, 70.48465428), ], ) def test_SunTrueLong(jc: float, stl: float): assert sun.sun_true_long(jc) == pytest.approx(stl, abs=0.001) # type: ignore @pytest.mark.parametrize( "jc,sta", [ (0.119986311, 4676.817391), (12.00844627, 432648.4151), (0.184134155, 6987.230663), ], ) def test_SunTrueAnomaly(jc: float, sta: float): assert sun.sun_true_anomoly(jc) == pytest.approx(sta, abs=0.001) # type: ignore @pytest.mark.parametrize( "jc,srv", [ (0.119986311, 0.983322329), (12.00844627, 0.994653382), (0.184134155, 1.013961204), ], ) def test_SunRadVector(jc: float, srv: float): assert sun.sun_rad_vector(jc) == pytest.approx(srv, abs=0.001) # type: ignore @pytest.mark.parametrize( "jc,sal", [ (0.119986311, 279.95995849827), (12.00844627, 232.065823531804), (0.184134155, 70.475244256027), ], ) def test_SunApparentLong(jc: float, sal: float): assert sun.sun_apparent_long(jc) == pytest.approx(sal) # type: ignore @pytest.mark.parametrize( "jc,mooe", [ (0.119986311, 23.4377307876356), (12.00844627, 23.2839797200388), (0.184134155, 23.4368965974579), ], ) def test_MeanObliquityOfEcliptic(jc: float, mooe: float): assert sun.mean_obliquity_of_ecliptic(jc) == pytest.approx(mooe) # type: ignore @pytest.mark.parametrize( "jc,oc", [ (0.119986311, 23.4369810410121), (12.00844627, 23.2852236361575), (0.184134155, 23.4352890293474), ], ) def test_ObliquityCorrection(jc: float, oc: float): assert sun.obliquity_correction(jc) == pytest.approx(oc, abs=0.001) # type: ignore @pytest.mark.parametrize( "jc,sra", [ (0.119986311, -79.16480352), (12.00844627, -130.3163904), (0.184134155, 68.86915896), ], ) def test_SunRtAscension(jc: float, sra: float): assert sun.sun_rt_ascension(jc) == pytest.approx(sra, abs=0.001) # type: ignore @pytest.mark.parametrize( "jc,sd", [ (0.119986311, -23.06317068), (12.00844627, -18.16694394), (0.184134155, 22.01463552), ], ) def test_SunDeclination(jc: float, sd: float): assert sun.sun_declination(jc) == pytest.approx(sd, abs=0.001) # type: ignore @pytest.mark.parametrize( "jc,eot", [ (0.119986311, -3.078194825), (12.00844627, 16.58348133), (0.184134155, 2.232039737), ], ) def test_EquationOfTime(jc: float, eot: float): assert sun.eq_of_time(jc) == pytest.approx(eot) # type: ignore @pytest.mark.parametrize( "d,ha", [ (datetime.date(2012, 1, 1), 1.03555238), (datetime.date(3200, 11, 14), 1.172253118), (datetime.date(2018, 6, 1), 2.133712555), ], ) def test_HourAngle(d: datetime.date, ha: float, london: Location): midday = datetime.time(12, 0, 0) jd = sun.julianday(datetime.datetime.combine(d, midday)) jc = sun.julianday_to_juliancentury(jd) decl = sun.sun_declination(jc) assert sun.hour_angle( london.latitude, decl, 90.8333, sun.SunDirection.RISING ) == pytest.approx( # type: ignore ha, abs=0.1 ) def test_Azimuth(new_delhi: Location): d = datetime.datetime(2001, 6, 21, 13, 11, 0) assert sun.azimuth(new_delhi.observer, d) == pytest.approx(292.76, abs=0.1) # type: ignore def test_Elevation(new_delhi: Location): d = datetime.datetime(2001, 6, 21, 13, 11, 0) assert sun.elevation(new_delhi.observer, d) == pytest.approx(7.41, abs=0.1) # type: ignore def test_Elevation_NonNaive(new_delhi: Location): d = datetime.datetime(2001, 6, 21, 18, 41, 0, tzinfo=new_delhi.tz) assert sun.elevation(new_delhi.observer, d) == pytest.approx(7.41, abs=0.1) # type: ignore def test_Elevation_WithoutRefraction(new_delhi: Location): d = datetime.datetime(2001, 6, 21, 13, 11, 0) assert sun.elevation(new_delhi.observer, d, with_refraction=False) == pytest.approx( # type: ignore 7.29, abs=0.1 ) def test_Azimuth_Above85Degrees(): d = datetime.datetime(2001, 6, 21, 13, 11, 0) assert sun.azimuth(Observer(86, 77.2), d) == pytest.approx(276.21, abs=0.1) # type: ignore def test_Elevation_Above85Degrees(): d = datetime.datetime(2001, 6, 21, 13, 11, 0) assert sun.elevation(Observer(86, 77.2), d) == pytest.approx( # type: ignore 23.102501151619506, abs=0.001 ) @pytest.mark.parametrize("elevation", range(1, 20)) @freezegun.freeze_time("2020-02-06") def test_ElevationEqualsTimeAtElevation(elevation: float, london: Location): o = london.observer td = today() et = sun.time_at_elevation(o, elevation, td, with_refraction=True) sun_elevation = sun.elevation(o, et, with_refraction=True) assert sun_elevation == pytest.approx(elevation, abs=0.1) # type: ignore ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1667670964.1184852 astral-3.2/src/test/test_sun_elevation_adjustment.py0000644000000000000000000000226614331521664020075 0ustar00# -*- coding: utf-8 -*- from datetime import datetime import freezegun import pytest from almost_equal import datetime_almost_equal from astral.geocoder import database, lookup from astral.location import Location from astral.sun import adjust_to_horizon, adjust_to_obscuring_feature, sunrise class TestElevationAdjustment: def test_Float_Positive(self): adjustment = adjust_to_horizon(12000) assert adjustment == pytest.approx(3.517744168209966) def test_Float_Negative(self): adjustment = adjust_to_horizon(-1) assert adjustment == pytest.approx(0) def test_Tuple_0(self): adjustment = adjust_to_obscuring_feature((0.0, 100.0)) assert adjustment == 0.0 def test_Tuple_45deg(self): adjustment = adjust_to_obscuring_feature((10.0, 10.0)) assert adjustment == pytest.approx(45.0) def test_Tuple_30deg(self): adjustment = adjust_to_obscuring_feature((3.0, 4.0)) assert adjustment == pytest.approx(53.130102354156) def test_Tuple_neg45deg(self): adjustment = adjust_to_obscuring_feature((-10.0, 10.0)) assert adjustment == pytest.approx(-45.0) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1667670964.1184852 astral-3.2/src/test/test_sun_golden_blue.py0000644000000000000000000001027314331521664016125 0ustar00# -*- coding: utf-8 -*- # Test data taken from http://www.timeanddate.com/sun/uk/london import datetime import freezegun import pytest from almost_equal import datetime_almost_equal from astral import TimePeriod, sun from astral.location import Location from astral.sun import SunDirection class TestGoldenHour: """Tests for the golden_hour function""" @pytest.mark.parametrize( "day,golden_hour", [ ( datetime.date(2015, 12, 1), ( datetime.datetime(2015, 12, 1, 1, 10, 10), datetime.datetime(2015, 12, 1, 2, 0, 43), ), ), ( datetime.date(2016, 1, 1), ( datetime.datetime(2016, 1, 1, 1, 27, 46), datetime.datetime(2016, 1, 1, 2, 19, 1), ), ), ], ) def test_morning( self, day: datetime.date, golden_hour: TimePeriod, new_delhi: Location ): start1 = golden_hour[0].replace(tzinfo=datetime.timezone.utc) end1 = golden_hour[1].replace(tzinfo=datetime.timezone.utc) start2, end2 = sun.golden_hour(new_delhi.observer, day, SunDirection.RISING) assert datetime_almost_equal(end1, end2, seconds=90) assert datetime_almost_equal(start1, start2, seconds=90) def test_evening(self, london: Location): test_data = { datetime.date(2016, 5, 18): ( datetime.datetime(2016, 5, 18, 19, 2), datetime.datetime(2016, 5, 18, 20, 17), ) } for day, golden_hour in test_data.items(): start1 = golden_hour[0].replace(tzinfo=datetime.timezone.utc) end1 = golden_hour[1].replace(tzinfo=datetime.timezone.utc) start2, end2 = sun.golden_hour(london.observer, day, SunDirection.SETTING) assert datetime_almost_equal(end1, end2, seconds=60) assert datetime_almost_equal(start1, start2, seconds=60) @freezegun.freeze_time("2015-12-1") def test_no_date(self, new_delhi: Location): start = datetime.datetime(2015, 12, 1, 1, 10, 10, tzinfo=datetime.timezone.utc) end = datetime.datetime(2015, 12, 1, 2, 0, 43, tzinfo=datetime.timezone.utc) ans = sun.golden_hour(new_delhi.observer) assert datetime_almost_equal(ans[0], start, 90) assert datetime_almost_equal(ans[1], end, 90) class TestBlueHour: """Tests for the blue_hour function""" def test_morning(self, london: Location): 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 = blue_hour[0].replace(tzinfo=datetime.timezone.utc) end1 = blue_hour[1].replace(tzinfo=datetime.timezone.utc) start2, end2 = sun.blue_hour(london.observer, day, SunDirection.RISING) assert datetime_almost_equal(end1, end2, seconds=90) assert datetime_almost_equal(start1, start2, seconds=90) def test_evening(self, london: Location): test_data = { datetime.date(2016, 5, 19): ( datetime.datetime(2016, 5, 19, 20, 18), datetime.datetime(2016, 5, 19, 20, 35), ) } for day, blue_hour in test_data.items(): start1 = blue_hour[0].replace(tzinfo=datetime.timezone.utc) end1 = blue_hour[1].replace(tzinfo=datetime.timezone.utc) start2, end2 = sun.blue_hour(london.observer, day, SunDirection.SETTING) assert datetime_almost_equal(end1, end2, seconds=90) assert datetime_almost_equal(start1, start2, seconds=90) @freezegun.freeze_time("2016-5-19") def test_no_date(self, london: Location): start = datetime.datetime(2016, 5, 19, 20, 18, tzinfo=datetime.timezone.utc) end = datetime.datetime(2016, 5, 19, 20, 35, tzinfo=datetime.timezone.utc) ans = sun.blue_hour(london.observer, direction=SunDirection.SETTING) assert datetime_almost_equal(ans[0], start, 90) assert datetime_almost_equal(ans[1], end, 90) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1667670964.1184852 astral-3.2/src/test/test_sun_local.py0000644000000000000000000000306214331521664014736 0ustar00import datetime import pytest from almost_equal import datetime_almost_equal from astral import sun from astral.location import Location @pytest.mark.parametrize( "day,dawn", [ (datetime.date(2015, 12, 1), datetime.datetime(2015, 12, 1, 6, 30)), (datetime.date(2015, 12, 2), datetime.datetime(2015, 12, 2, 6, 31)), (datetime.date(2015, 12, 3), datetime.datetime(2015, 12, 3, 6, 31)), (datetime.date(2015, 12, 12), datetime.datetime(2015, 12, 12, 6, 38)), (datetime.date(2015, 12, 25), datetime.datetime(2015, 12, 25, 6, 45)), ], ) def test_Sun_Local_tzinfo( day: datetime.date, dawn: datetime.datetime, new_delhi: Location ): dawn = dawn.replace(tzinfo=new_delhi.tzinfo) dawn_calc = sun.sun(new_delhi.observer, day, 6.0, new_delhi.tzinfo)["dawn"] assert datetime_almost_equal(dawn, dawn_calc) @pytest.mark.parametrize( "day,dawn", [ (datetime.date(2015, 12, 1), datetime.datetime(2015, 12, 1, 6, 30)), (datetime.date(2015, 12, 2), datetime.datetime(2015, 12, 2, 6, 31)), (datetime.date(2015, 12, 3), datetime.datetime(2015, 12, 3, 6, 31)), (datetime.date(2015, 12, 12), datetime.datetime(2015, 12, 12, 6, 38)), (datetime.date(2015, 12, 25), datetime.datetime(2015, 12, 25, 6, 45)), ], ) def test_Sun_Local_str( day: datetime.date, dawn: datetime.datetime, new_delhi: Location ): dawn = dawn.replace(tzinfo=new_delhi.tzinfo) dawn_calc = sun.sun(new_delhi.observer, day, 6.0, "Asia/Kolkata")["dawn"] assert datetime_almost_equal(dawn, dawn_calc) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1667670964.1184852 astral-3.2/src/test/test_sun_utc.py0000644000000000000000000004367114331521664014451 0ustar00# Test data taken from http://www.timeanddate.com/sun/uk/london import datetime from typing import Tuple import freezegun import pytest from almost_equal import datetime_almost_equal from astral import LocationInfo, TimePeriod, sun from astral.sun import Depression, SunDirection @pytest.mark.parametrize( "day,dawn", [ (datetime.date(2015, 12, 1), datetime.datetime(2015, 12, 1, 7, 4)), (datetime.date(2015, 12, 2), datetime.datetime(2015, 12, 2, 7, 5)), (datetime.date(2015, 12, 3), datetime.datetime(2015, 12, 3, 7, 6)), (datetime.date(2015, 12, 12), datetime.datetime(2015, 12, 12, 7, 16)), (datetime.date(2015, 12, 25), datetime.datetime(2015, 12, 25, 7, 25)), ], ) def test_Sun(day: datetime.date, dawn: datetime.datetime, london: LocationInfo): dawn = dawn.replace(tzinfo=datetime.timezone.utc) dawn_utc = sun.sun(london.observer, day)["dawn"] assert datetime_almost_equal(dawn, dawn_utc) @freezegun.freeze_time("2015-12-01") def test_Sun_NoDate(london: LocationInfo): ans = datetime.datetime(2015, 12, 1, 7, 4, tzinfo=datetime.timezone.utc) assert datetime_almost_equal(sun.sun(london.observer)["dawn"], ans) @pytest.mark.parametrize( "day,dawn", [ (datetime.date(2015, 12, 1), datetime.datetime(2015, 12, 1, 7, 4)), (datetime.date(2015, 12, 2), datetime.datetime(2015, 12, 2, 7, 5)), (datetime.date(2015, 12, 3), datetime.datetime(2015, 12, 3, 7, 6)), (datetime.date(2015, 12, 12), datetime.datetime(2015, 12, 12, 7, 16)), (datetime.date(2015, 12, 25), datetime.datetime(2015, 12, 25, 7, 25)), ], ) def test_Dawn_Civil(day: datetime.date, dawn: datetime.datetime, london: LocationInfo): dawn = dawn.replace(tzinfo=datetime.timezone.utc) dawn_utc = sun.dawn(london.observer, day, Depression.CIVIL) assert datetime_almost_equal(dawn, dawn_utc) @freezegun.freeze_time("2015-12-01") def test_Dawn_NoDate(london: LocationInfo): ans = datetime.datetime(2015, 12, 1, 7, 4, tzinfo=datetime.timezone.utc) assert datetime_almost_equal(sun.dawn(london.observer), ans) @pytest.mark.parametrize( "day,dawn", [ (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, 33)), (datetime.date(2015, 12, 25), datetime.datetime(2015, 12, 25, 6, 41)), ], ) def test_Dawn_Nautical( day: datetime.date, dawn: datetime.datetime, london: LocationInfo ): dawn = dawn.replace(tzinfo=datetime.timezone.utc) dawn_utc = sun.dawn(london.observer, day, 12) assert datetime_almost_equal(dawn, dawn_utc) @pytest.mark.parametrize( "day,dawn", [ (datetime.date(2015, 12, 1), datetime.datetime(2015, 12, 1, 5, 41)), (datetime.date(2015, 12, 2), datetime.datetime(2015, 12, 2, 5, 42)), (datetime.date(2015, 12, 3), datetime.datetime(2015, 12, 3, 5, 44)), (datetime.date(2015, 12, 12), datetime.datetime(2015, 12, 12, 5, 52)), (datetime.date(2015, 12, 25), datetime.datetime(2015, 12, 25, 6, 1)), ], ) def test_Dawn_Astronomical( day: datetime.date, dawn: datetime.datetime, london: LocationInfo ): dawn = dawn.replace(tzinfo=datetime.timezone.utc) dawn_utc = sun.dawn(london.observer, day, 18) assert datetime_almost_equal(dawn, dawn_utc) @pytest.mark.parametrize( "day,sunrise", [ (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, 56)), (datetime.date(2015, 12, 25), datetime.datetime(2015, 12, 25, 8, 5)), ], ) def test_Sunrise(day: datetime.date, sunrise: datetime.datetime, london: LocationInfo): sunrise = sunrise.replace(tzinfo=datetime.timezone.utc) sunrise_utc = sun.sunrise(london.observer, day) assert datetime_almost_equal(sunrise, sunrise_utc) @freezegun.freeze_time("2015-12-01") def test_Sunrise_NoDate(london: LocationInfo): ans = datetime.datetime(2015, 12, 1, 7, 43, tzinfo=datetime.timezone.utc) sunrise = sun.sunrise(london.observer) assert datetime_almost_equal(sunrise, ans) @pytest.mark.parametrize( "day,sunset", [ (datetime.date(2015, 1, 1), datetime.datetime(2015, 1, 1, 16, 1)), (datetime.date(2015, 12, 1), datetime.datetime(2015, 12, 1, 15, 55)), (datetime.date(2015, 12, 2), datetime.datetime(2015, 12, 2, 15, 54)), (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, 55)), ], ) def test_Sunset(day: datetime.date, sunset: datetime.datetime, london: LocationInfo): sunset = sunset.replace(tzinfo=datetime.timezone.utc) sunset_utc = sun.sunset(london.observer, day) assert datetime_almost_equal(sunset, sunset_utc) @freezegun.freeze_time("2015-12-01") def test_Sunset_NoDate(london: LocationInfo): ans = datetime.datetime(2015, 12, 1, 15, 55, tzinfo=datetime.timezone.utc) sunset = sun.sunset(london.observer) assert datetime_almost_equal(sunset, ans) @pytest.mark.parametrize( "day,dusk", [ (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)), ], ) def test_Dusk_Civil(day: datetime.date, dusk: datetime.datetime, london: LocationInfo): dusk = dusk.replace(tzinfo=datetime.timezone.utc) dusk_utc = sun.dusk(london.observer, day) assert datetime_almost_equal(dusk, dusk_utc) @freezegun.freeze_time("2015-12-01") def test_Dusk_NoDate(london: LocationInfo): ans = datetime.datetime(2015, 12, 1, 16, 34, tzinfo=datetime.timezone.utc) dusk = sun.dusk(london.observer) assert datetime_almost_equal(dusk, ans) @pytest.mark.parametrize( "day,dusk", [ (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)), ], ) def test_Dusk_Nautical( day: datetime.date, dusk: datetime.datetime, london: LocationInfo ): dusk = dusk.replace(tzinfo=datetime.timezone.utc) dusk_utc = sun.dusk(london.observer, day, 12) assert datetime_almost_equal(dusk, dusk_utc) @pytest.mark.parametrize( "day,noon", [ (datetime.date(2015, 12, 1), datetime.datetime(2015, 12, 1, 11, 49)), (datetime.date(2015, 12, 2), datetime.datetime(2015, 12, 2, 11, 49)), (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)), ], ) def test_SolarNoon(day: datetime.date, noon: datetime.datetime, london: LocationInfo): noon = noon.replace(tzinfo=datetime.timezone.utc) noon_utc = sun.noon(london.observer, day) assert datetime_almost_equal(noon, noon_utc) @freezegun.freeze_time("2015-12-01") def test_SolarNoon_NoDate(london: LocationInfo): ans = datetime.datetime(2015, 12, 1, 11, 49, tzinfo=datetime.timezone.utc) noon = sun.noon(london.observer) assert datetime_almost_equal(noon, ans) @pytest.mark.parametrize( "day,midnight", [ (datetime.date(2016, 2, 18), datetime.datetime(2016, 2, 18, 0, 14)), (datetime.date(2016, 10, 26), datetime.datetime(2016, 10, 25, 23, 44)), ], ) def test_SolarMidnight( day: datetime.date, midnight: datetime.datetime, london: LocationInfo ): solar_midnight = midnight.replace(tzinfo=datetime.timezone.utc) solar_midnight_utc = sun.midnight(london.observer, day) assert datetime_almost_equal(solar_midnight, solar_midnight_utc) @freezegun.freeze_time("2016-2-18") def test_SolarMidnight_NoDate(london: LocationInfo): ans = datetime.datetime(2016, 2, 18, 0, 14, tzinfo=datetime.timezone.utc) midnight = sun.midnight(london.observer) assert datetime_almost_equal(midnight, ans) @pytest.mark.parametrize( "day,twilight", [ ( datetime.date(2019, 8, 29), ( datetime.datetime(2019, 8, 29, 4, 32), datetime.datetime(2019, 8, 29, 5, 8), ), ), ], ) def test_Twilight_SunRising( day: datetime.date, twilight: Tuple[datetime.datetime, datetime.datetime], london: LocationInfo, ): start, end = twilight start = start.replace(tzinfo=datetime.timezone.utc) end = end.replace(tzinfo=datetime.timezone.utc) info = sun.twilight(london.observer, day) start_utc = info[0] end_utc = info[1] assert datetime_almost_equal(start, start_utc) assert datetime_almost_equal(end, end_utc) @pytest.mark.parametrize( "day,twilight", [ ( datetime.date(2019, 8, 29), ( datetime.datetime(2019, 8, 29, 18, 54), datetime.datetime(2019, 8, 29, 19, 30), ), ) ], ) def test_Twilight_SunSetting( day: datetime.date, twilight: TimePeriod, london: LocationInfo ): start, end = twilight start = start.replace(tzinfo=datetime.timezone.utc) end = end.replace(tzinfo=datetime.timezone.utc) info = sun.twilight(london.observer, day, direction=SunDirection.SETTING) start_utc = info[0] end_utc = info[1] assert datetime_almost_equal(start, start_utc) assert datetime_almost_equal(end, end_utc) @freezegun.freeze_time("2019-8-29") def test_Twilight_NoDate(london: LocationInfo): start = datetime.datetime(2019, 8, 29, 18, 54, tzinfo=datetime.timezone.utc) end = datetime.datetime(2019, 8, 29, 19, 30, tzinfo=datetime.timezone.utc) ans = sun.twilight(london.observer, direction=SunDirection.SETTING) assert datetime_almost_equal(ans[0], start) assert datetime_almost_equal(ans[1], end) # Test data from http://www.astroloka.com/rahukaal.aspx?City=Delhi @pytest.mark.parametrize( "day,rahu", [ ( 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), ), ), ], ) def test_Rahukaalam(day: datetime.date, rahu: TimePeriod, new_delhi: LocationInfo): start, end = rahu start = start.replace(tzinfo=datetime.timezone.utc) end = end.replace(tzinfo=datetime.timezone.utc) info = sun.rahukaalam(new_delhi.observer, day) start_utc = info[0] end_utc = info[1] assert datetime_almost_equal(start, start_utc) assert datetime_almost_equal(end, end_utc) @freezegun.freeze_time("2015-12-01") def test_Rahukaalam_NoDate(new_delhi: LocationInfo): start = datetime.datetime(2015, 12, 1, 9, 17, tzinfo=datetime.timezone.utc) end = datetime.datetime(2015, 12, 1, 10, 35, tzinfo=datetime.timezone.utc) ans = sun.rahukaalam(new_delhi.observer) assert datetime_almost_equal(ans[0], start) assert datetime_almost_equal(ans[1], end) @pytest.mark.parametrize( "dt,angle", [ (datetime.datetime(2015, 12, 14, 11, 0, 0), 14.381311), (datetime.datetime(2015, 12, 14, 20, 1, 0), -37.3710156), ], ) def test_SolarAltitude(dt: datetime.datetime, angle: float, london: LocationInfo): elevation = sun.elevation(london.observer, dt) assert elevation == pytest.approx(angle, abs=0.5) # type: ignore @freezegun.freeze_time("2015-12-14 11:00:00", tz_offset=0) def test_SolarAltitude_NoDate(london: LocationInfo): elevation = sun.elevation(london.observer) assert elevation == pytest.approx(14.381311, abs=0.5) # type: ignore @pytest.mark.parametrize( "dt,angle", [ (datetime.datetime(2015, 12, 14, 11, 0, 0), 166.9676), (datetime.datetime(2015, 12, 14, 20, 1, 0), 279.39927311745), ], ) def test_SolarAzimuth(dt: datetime.datetime, angle: float, london: LocationInfo): azimuth = sun.azimuth(london.observer, dt) assert azimuth == pytest.approx(angle, abs=0.5) # type: ignore @freezegun.freeze_time("2015-12-14 11:00:00", tz_offset=0) def test_SolarAzimuth_NoDate(london: LocationInfo): assert sun.azimuth(london.observer) == pytest.approx(166.9676, abs=0.5) # type: ignore @pytest.mark.parametrize( "dt,angle", [ (datetime.datetime(2021, 10, 10, 6, 0, 0), 102.6), (datetime.datetime(2021, 10, 10, 7, 0, 0), 93.3), (datetime.datetime(2021, 10, 10, 18, 0, 0), 87.8), (datetime.datetime(2019, 8, 29, 14, 34, 0), 46), (datetime.datetime(2020, 2, 3, 10, 37, 0), 71), ], ) def test_SolarZenith_London(dt: datetime.datetime, angle: float, london: LocationInfo): dt = dt.replace(tzinfo=london.tzinfo) # type: ignore zenith = sun.zenith(london.observer, dt) assert zenith == pytest.approx(angle, abs=0.5) # type: ignore @pytest.mark.parametrize( "dt,angle", [ (datetime.datetime(2022, 5, 1, 14, 0, 0), 32), (datetime.datetime(2022, 5, 1, 21, 20, 0), 126), ], ) def test_SolarZenith_Riyadh(dt: datetime.datetime, angle: float, riyadh: LocationInfo): dt = dt.replace(tzinfo=riyadh.tzinfo) # type: ignore zenith = sun.zenith(riyadh.observer, dt) assert zenith == pytest.approx(angle, abs=0.5) # type: ignore @freezegun.freeze_time("2019-08-29 14:34:00") def test_SolarZenith_NoDate(london: LocationInfo): zenith = sun.zenith(london.observer) assert zenith == pytest.approx(52.41, abs=0.5) # type: ignore def test_TimeAtElevation_SunRising(london: LocationInfo): d = datetime.date(2016, 1, 4) dt = sun.time_at_elevation(london.observer, 6, d, SunDirection.RISING) cdt = datetime.datetime(2016, 1, 4, 9, 5, 0, tzinfo=datetime.timezone.utc) # Use error of 5 minutes as website has a rather coarse accuracy assert datetime_almost_equal(dt, cdt, seconds=300) @freezegun.freeze_time("2016-1-4") def test_TimeAtElevation_NoDate(london: LocationInfo): dt = sun.time_at_elevation(london.observer, 6, direction=SunDirection.RISING) cdt = datetime.datetime(2016, 1, 4, 9, 5, 0, tzinfo=datetime.timezone.utc) # Use error of 5 minutes as website has a rather coarse accuracy assert datetime_almost_equal(dt, cdt, seconds=300) def test_TimeAtElevation_SunSetting(london: LocationInfo): d = datetime.date(2016, 1, 4) dt = sun.time_at_elevation(london.observer, 14, d, SunDirection.SETTING) cdt = datetime.datetime(2016, 1, 4, 13, 20, 0, tzinfo=datetime.timezone.utc) assert datetime_almost_equal(dt, cdt, seconds=300) def test_TimeAtElevation_GreaterThan90(london: LocationInfo): d = datetime.date(2016, 1, 4) dt = sun.time_at_elevation(london.observer, 166, d, SunDirection.RISING) cdt = datetime.datetime(2016, 1, 4, 13, 20, 0, tzinfo=datetime.timezone.utc) assert datetime_almost_equal(dt, cdt, seconds=300) def test_TimeAtElevation_GreaterThan180(london: LocationInfo): d = datetime.date(2015, 12, 1) dt = sun.time_at_elevation(london.observer, 186, d, SunDirection.RISING) cdt = datetime.datetime(2015, 12, 1, 16, 34, tzinfo=datetime.timezone.utc) assert datetime_almost_equal(dt, cdt, seconds=300) def test_TimeAtElevation_SunRisingBelowHorizon(london: LocationInfo): d = datetime.date(2016, 1, 4) dt = sun.time_at_elevation(london.observer, -18, d, SunDirection.RISING) cdt = datetime.datetime(2016, 1, 4, 6, 0, 0, tzinfo=datetime.timezone.utc) assert datetime_almost_equal(dt, cdt, seconds=300) def test_TimeAtElevation_BadElevation(london: LocationInfo): d = datetime.date(2016, 1, 4) with pytest.raises(ValueError): sun.time_at_elevation(london.observer, 20, d, SunDirection.RISING) def test_Daylight(london: LocationInfo): d = datetime.date(2016, 1, 6) start, end = sun.daylight(london.observer, d) cstart = datetime.datetime(2016, 1, 6, 8, 5, 0, tzinfo=datetime.timezone.utc) cend = datetime.datetime(2016, 1, 6, 16, 7, 0, tzinfo=datetime.timezone.utc) assert datetime_almost_equal(start, cstart, 120) assert datetime_almost_equal(end, cend, 120) @freezegun.freeze_time("2016-1-06") def test_Daylight_NoDate(london: LocationInfo): ans = sun.daylight(london.observer) start = datetime.datetime(2016, 1, 6, 8, 5, 0, tzinfo=datetime.timezone.utc) end = datetime.datetime(2016, 1, 6, 16, 7, 0, tzinfo=datetime.timezone.utc) assert datetime_almost_equal(ans[0], start, 120) assert datetime_almost_equal(ans[1], end, 120) def test_Nighttime(london: LocationInfo): d = datetime.date(2016, 1, 6) start, end = sun.night(london.observer, d) cstart = datetime.datetime(2016, 1, 6, 16, 46, tzinfo=datetime.timezone.utc) cend = datetime.datetime(2016, 1, 7, 7, 25, tzinfo=datetime.timezone.utc) assert datetime_almost_equal(start, cstart, 120) assert datetime_almost_equal(end, cend, 120) @freezegun.freeze_time("2016-1-06") def test_Nighttime_NoDate(london: LocationInfo): start = datetime.datetime(2016, 1, 6, 16, 46, tzinfo=datetime.timezone.utc) end = datetime.datetime(2016, 1, 7, 7, 25, tzinfo=datetime.timezone.utc) ans = sun.night(london.observer) assert datetime_almost_equal(ans[0], start, seconds=300) assert datetime_almost_equal(ans[1], end, seconds=300) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1667670964.1184852 astral-3.2/src/test/test_value_error_bug.py0000644000000000000000000000064114331521664016141 0ustar00import datetime import astral import astral.sun def test_value_error_bug(): loc = astral.LocationInfo( name="Barwani", region="India", timezone="Asia/Kolkata", latitude=23.518507, longitude=74.952246, ) ob = loc.observer sun = astral.sun.sun(ob, date=datetime.date(2022, 7, 20)) dawn = sun["dawn"] if __name__ == "__main__": test_value_error_bug() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1667670964.1184852 astral-3.2/src/test/test_wellington.py0000644000000000000000000000112514331521664015137 0ustar00import datetime from pprint import pprint from almost_equal import datetime_almost_equal from astral.location import Location from astral.sun import sun def test_Wellington(wellington: Location): dt = datetime.date(2020, 2, 11) s = sun(wellington.observer, dt, tzinfo=wellington.tzinfo) assert datetime_almost_equal( s["sunrise"], datetime.datetime(2020, 2, 11, 6, 38, 42, tzinfo=wellington.tzinfo), ) assert datetime_almost_equal( s["sunset"], datetime.datetime(2020, 2, 11, 20, 31, 00, tzinfo=wellington.tzinfo), ) astral-3.2/setup.py0000644000000000000000000000242500000000000011250 0ustar00# -*- coding: utf-8 -*- from setuptools import setup package_dir = \ {'': 'src'} packages = \ ['astral'] package_data = \ {'': ['*']} extras_require = \ {':python_version < "3.9"': ['backports.zoneinfo'], ':sys_platform == "win32"': ['tzdata']} setup_kwargs = { 'name': 'astral', 'version': '3.2', 'description': 'Calculations for the position of the sun and moon.', 'long_description': "# Astral\n\nThis is 'astral' a Python module which calculates\n\n- Times for various positions of the sun: dawn, sunrise, solar noon,\nsunset, dusk, solar elevation, solar azimuth and rahukaalam.\n- Moon rise, set, azimuth and zenith.\n- The phase of the moon.\n\nFor documentation see https://sffjunkie.github.io/astral/\n\n## Package Status\n\n![GitHub Workflow Status](https://img.shields.io/github/workflow/status/sffjunkie/astral/astral-test) ![PyPI - Downloads](https://img.shields.io/pypi/dm/astral)\n", 'author': 'Simon Kennedy', 'author_email': 'sffjunkie+code@gmail.com', 'maintainer': 'None', 'maintainer_email': 'None', 'url': 'https://github.com/sffjunkie/astral', 'package_dir': package_dir, 'packages': packages, 'package_data': package_data, 'extras_require': extras_require, 'python_requires': '>=3.7,<4.0', } setup(**setup_kwargs) astral-3.2/PKG-INFO0000644000000000000000000000321100000000000010625 0ustar00Metadata-Version: 2.1 Name: astral Version: 3.2 Summary: Calculations for the position of the sun and moon. Home-page: https://github.com/sffjunkie/astral License: Apache-2.0 Keywords: sun,moon,sunrise,sunset,dawn,dusk Author: Simon Kennedy Author-email: sffjunkie+code@gmail.com Requires-Python: >=3.7,<4.0 Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Requires-Dist: backports.zoneinfo; python_version < "3.9" Requires-Dist: tzdata; sys_platform == "win32" Project-URL: Documentation, https://sffjunkie.github.io/astral Project-URL: Repository, https://github.com/sffjunkie/astral Description-Content-Type: text/markdown # 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. - Moon rise, set, azimuth and zenith. - The phase of the moon. For documentation see https://sffjunkie.github.io/astral/ ## Package Status ![GitHub Workflow Status](https://img.shields.io/github/workflow/status/sffjunkie/astral/astral-test) ![PyPI - Downloads](https://img.shields.io/pypi/dm/astral)