Repository: sffjunkie/astral Branch: master Commit: ac23ab5c0c69 Files: 57 Total size: 282.3 KB Directory structure: gitextract_ffhfea7t/ ├── .editorconfig ├── .gitattributes ├── .github/ │ └── workflows/ │ └── astral-test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── AUTHORS ├── ChangeLog.md ├── LICENSE ├── ReadMe.md ├── flake.nix ├── pyproject.toml ├── src/ │ ├── astral/ │ │ ├── __init__.py │ │ ├── __main__.py │ │ ├── geocoder.py │ │ ├── julian.py │ │ ├── location.py │ │ ├── moon.py │ │ ├── py.typed │ │ ├── sidereal.py │ │ ├── sun.py │ │ └── table4.py │ ├── docs/ │ │ ├── Makefile │ │ ├── SConstruct │ │ ├── conf.py │ │ ├── index.rst │ │ ├── make.bat │ │ ├── package.rst │ │ ├── python3_intersphinx.inv │ │ └── static/ │ │ └── astral.css │ └── test/ │ ├── almost_equal.py │ ├── conftest.py │ ├── moon/ │ │ ├── test_moon.py │ │ ├── test_moon_azimuth.py │ │ ├── test_moon_position.py │ │ ├── test_moon_rise.py │ │ └── test_sidereal_time.py │ ├── test_Location.py │ ├── test_Repr.py │ ├── test_all.py │ ├── test_almost_equal.py │ ├── test_buenos_aries.py │ ├── test_depression_not_reached.py │ ├── test_geocoder.py │ ├── test_julian.py │ ├── test_location_info.py │ ├── test_misc.py │ ├── test_norway.py │ ├── test_observer.py │ ├── test_sun_calc.py │ ├── test_sun_elevation_adjustment.py │ ├── test_sun_golden_blue.py │ ├── test_sun_local.py │ ├── test_sun_utc.py │ ├── test_value_error_bug.py │ └── test_wellington.py └── tox.ini ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ [*] charset = utf-8 end_of_line = lf indent_style = space insert_final_newline = true trim_trailing_whitespace = true [*.py] indent_size = 4 [*.rst] indent_size = 3 [*.{json,yml}] indent_size = 2 [.api_key] insert_final_newline = false ================================================ FILE: .gitattributes ================================================ ================================================ FILE: .github/workflows/astral-test.yml ================================================ name: astral-test on: push: branches: ["master", "develop"] pull_request: branches: ["master"] permissions: contents: read jobs: tests: runs-on: ubuntu-latest strategy: matrix: python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install pdm - name: Install dependencies using pdm run: | pdm install - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names pdm run flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --exclude=.venv # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide pdm run flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics --exclude=.venv - name: Test with pytest run: | pdm run pytest ================================================ FILE: .gitignore ================================================ .api_key .venv setup.cfg setup-dev.cfg .tox _version.py .coveragerc .coveralls.yml .atom-build.yaml .atom-build.yml poetry.lock poetry.toml .python-version artifact .mypy_cache .pytest_cache .pdm-python .pdm-build .direnv .envrc just Justfile # Numerous always-ignore extensions *.diff *.err *.orig *.log *.py[cod] *.rej *.swo *.swp *.vi *~ *.sass-cache *.egg-info # OS or Editor folders .DS_Store Thumbs.db .cache .coverage .project .pydevproject .settings .tmproj *.esproj nbproject *.sublime-project *.sublime-workspace *.iml .vscode .pytest_cache .mypy_cache .venv # Dreamweaver added files _notes dwsync.xml # Komodo *.komodoproject .komodotools # Folders to ignore .bzr .hg .svn .eggs .CVS intermediate publish .idea __pycache__ build dist MANIFEST # build script local files build/buildinfo.properties build/config/buildinfo.properties .pdm-python ================================================ FILE: .pre-commit-config.yaml ================================================ repos: - repo: https://github.com/pycqa/flake8 rev: 7.0.0 hooks: - id: flake8 additional_dependencies: [flake8-pyproject] - repo: https://github.com/pycqa/isort rev: 5.13.2 hooks: - id: isort args: ["--profile", "black", "--filter-files"] - repo: https://github.com/psf/black rev: 24.2.0 hooks: - id: black - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 hooks: - id: check-yaml - id: end-of-file-fixer - id: trailing-whitespace ================================================ FILE: .readthedocs.yaml ================================================ # .readthedocs.yaml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details version: 2 # Set the version of Python and other tools you might need build: os: ubuntu-20.04 tools: python: "3.9" # Build documentation in the docs/ directory with Sphinx sphinx: configuration: src/docs/conf.py ================================================ FILE: AUTHORS ================================================ Alton Campbell Michael Overmeyer Pascal Bach Simon Kennedy Wojciech Pietruszeski Zachary Priddy Michael Marx ================================================ FILE: ChangeLog.md ================================================ # CHANGELOG ## 3.2 2022-11-05 ### Changed - Removed support for Python 3.6 as it has reached "End of Life" - Documentation now hosted on [Github Pages](https://sffjunkie.github.io/astral/) ## 3.1 2022-11-01 ### Bug Fix - Fix for issue [#77](https://github.com/sffjunkie/astral/issues/77) ## 3.0 2022-10-07 ### Added - Added support for moon rise and set times and azimuth / zentih calculations. - Dropped dependency on `pytz` and switched to using `zoneinfo` provided as part of Python 3.9 or the `backports.zoneinfo` package. ## 2.2 - 2020-05-20 ### Changed - Fix for [bug #48](https://github.com/sffjunkie/astral/issues/48). As per the bug report the angle to adjust for the effect of elevation should have been θ (not α). - The sun functions can now also be passed the timezone as a string. Previously only a pytz timezone was accepted. ## 2.1 - 2020-02-12 ### Bug Fix - Fix for bug #44 - Incorrectly raised exception when UTC sun times were on the day previous to the day asked for. This only manifested itself for timezones with a large positive offset. ## 2.0 - 2020-02-11 ### Refactor - This is a code refactor as well as an update so it is highly likely that you will need to adapt your code to suit. - Astral, AstralGeocoder & GoogleGeocoder classes removed - Requires python 3.6+ due to the use of dataclasses - New LocationInfo class to store a location name, region, timezone, latitude & longitude - New Observer class to store a latitude, longitude & elevation - Geocoder database now returns a LocationInfo instead of a Location ## 1.10.1 - 2019-02-06 ### Changed Keywords arguments to Astral **init** are now passed to the geocoder to allow for passing the `api_key` to GoogleGeocoder. ## 1.10 - 2019-02-04 ### Added Added method to AstralGeocoder to add locations to the database ## 1.9.2 - 2019-01-31 ### Changed Version 1.9 broke the sun_utc method. Sun UTC calculation passed incorrect parameter to more specific methods e.g. sunrise, sunset etc. ## 1.9.1 - 2019-01-28 ### Changed Corrected version number in module source code. ## 1.9 - 2019-01-28 ### Added Sun calculations now take into account the elevation of the location. ## 1.8 - 2018-12-06 ### Added Added command line interface to return sun information as json. Added support for no timezone in Location methods. ## 1.7.1 - 2018-10-25 ### Changed Changed GoogleGeocoder test to not use raise...from as this is not valid for Python 2 ## 1.7 - 2018-10-24 ### Changed - Requests is now only needed when using GoogleGeocoder - GoogleGeocoder now requires the `api_key` parameter to be passed to the constructor ## 1.6.1 - 2018-05-02 ### Changed - Updated Travis CI configuration ### Added - requirements-dev.txt ## 1.6 - 2018-02-22 ### Changed - Added api_key parameter to GoogleGeocoder **init** method. Idea from wpietruszewski ## 1.5 - 2017-12-07 ### Added - this file ### Changed - dawn_utc, sunrise_utc, sunset_utc and dusk_utc now only raise AstralError for a math domain exception all other exceptions are passed through. - moon_phase now takes another parameter if the type to return either int (the default) or float ================================================ FILE: LICENSE ================================================ 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. ================================================ FILE: ReadMe.md ================================================ # 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/actions/workflow/status/sffjunkie/astral/astral-test.yml) ![PyPI - Downloads](https://img.shields.io/pypi/dm/astral) ================================================ FILE: flake.nix ================================================ { description = "Astral - Calculations for the sun and moon."; inputs.pyproject-nix.url = "github:nix-community/pyproject.nix"; inputs.pyproject-nix.inputs.nixpkgs.follows = "nixpkgs"; nixConfig = { bash-prompt = ''\n\[\033[1;34m\][\[\e]0;\u@\h: \w\a\]\u@\h:\w]\\$\[\033[0m\] ''; }; outputs = { nixpkgs, pyproject-nix, ... }: let inherit (nixpkgs) lib; project = pyproject-nix.lib.project.loadPyproject { projectRoot = ./.; }; forAllSystems = function: nixpkgs.lib.genAttrs [ "aarch64-linux" "x86_64-darwin" "x86_64-linux" ] (system: function nixpkgs.legacyPackages.${system}); in { devShells = forAllSystems (pkgs: ( let python = pkgs.python3; arg = project.renderers.withPackages {inherit python;}; pythonEnv = python.withPackages arg; in { default = pkgs.mkShell { packages = [ pkgs.pdm pkgs.ruff pythonEnv ]; shellHook = '' export PYTHONPATH=${builtins.toString ./src} ''; }; } )); packages = forAllSystems (pkgs: ( let python = pkgs.python3; attrs = project.renderers.buildPythonPackage {inherit python;}; in { default = python.pkgs.buildPythonPackage attrs; } )); }; } ================================================ FILE: pyproject.toml ================================================ [project] name = "astral" version = "3.3" description = "Calculations for the sun and moon." authors = [{ name = "Simon Kennedy", email = "sffjunkie+code@gmail.com" }] dependencies = ["tzdata>=2024.1; sys_platform == 'win32'"] requires-python = ">=3.10" readme = "ReadMe.md" license = { text = "Apache-2.0" } 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", ] [project.urls] Homepage = "https://github.com/sffjunkie/astral" Issues = "https://github.com/sffjunkie/astral/issues" [build-system] requires = ["pdm-backend"] build-backend = "pdm.backend" [tool.pdm.dev-dependencies] dev = [ "freezegun>=1.4.0", "pytest>=8.1.1", "mypy>=1.9.0", "types-freezegun>=1.1.10", "tox>=4.14.1", "ruff>=0.3.2", "pytest-cov>=4.1.0", "flake8>=7.0.0", ] docs = ["sphinx-book-theme>=1.1.2"] [tool.pdm.scripts] test = "pytest" typecheck = "mypy ./src/astral/" [tool.pytest.ini_options] markers = ["unit", "integration"] pythonpath = ["src"] junit_family = "xunit2" norecursedirs = [ ".direnv", ".venv", ".git", ".tox", ".cache", ".settings", "dist", "build", "docs", ] ================================================ FILE: src/astral/__init__.py ================================================ # -*- 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 # type: ignore __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] ================================================ FILE: src/astral/__main__.py ================================================ import 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 # type: ignore 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: kwargs["tzinfo"] = datetime.timezone.utc format_str += "Z" else: kwargs["tzinfo"] = zoneinfo.ZoneInfo(loc.timezone) format_str += "%z" s = sun.sun(**kwargs) for key, value in s.items(): sun_as_str[key] = s[key].strftime(format_str) sun_as_str["timezone"] = kwargs["tzinfo"].tzname sun_as_str["location"] = f"{loc.name}, {loc.region}" print(json.dumps(sun_as_str)) ================================================ FILE: src/astral/geocoder.py ================================================ """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 ================================================ FILE: src/astral/julian.py ================================================ import 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.0 if isinstance(at, datetime.datetime): t = _time_to_seconds(at.time()) day_fraction = t / (24 * 60 * 60) else: day_fraction = 0.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 # type: ignore 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 ================================================ FILE: src/astral/location.py ================================================ import dataclasses import datetime try: import zoneinfo except ImportError: from backports import zoneinfo # type: ignore 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) ================================================ FILE: src/astral/moon.py ================================================ """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 # type: ignore 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 ================================================ FILE: src/astral/py.typed ================================================ ================================================ FILE: src/astral/sidereal.py ================================================ import 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 ================================================ FILE: src/astral/sun.py ================================================ import 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 # type: ignore 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), } ================================================ FILE: src/astral/table4.py ================================================ from 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}), ] ================================================ FILE: src/docs/Makefile ================================================ # Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = -d ../../artifact/doctree SPHINXBUILD = sphinx-build PAPER = BUILDDIR = ../../../gh-pages # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR) touch $(BUILDDIR)/.nojekyll @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/yyryr.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/yyryr.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/yyryr" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/yyryr" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." ================================================ FILE: src/docs/SConstruct ================================================ """ This is a generic SCons script for running Sphinx (http://sphinx.pocoo.org). Type 'scons -h' for help. This prints the available build targets on your system, and the configuration options you can set. If you set the 'cache' option, the option settings are cached into a file called '.sconsrc-sphinx' in the current directory. When running subsequently, this file is reread. A file with this name is also read from your home directory, if it exists, so you can put global settings there. The script looks into your 'conf.py' file for information about the project. This is used in various places (e.g., to print the introductory message, and create package files). Here's some examples. To build HTML docs: scons html To create a package containing HTML and PDF docs, remembering the 'install' setting: scons install=html,pdf cache=True package To clean up everything: scons -c all """ # Script info. __author__ = "Glenn Hutchings" __email__ = "zondo42@googlemail.com" __url__ = "http://bitbucket.org/zondo/sphinx-scons" __license__ = "BSD" __version__ = "0.4" import sys, os import runpy # Build targets. targets = ( ("html", "make standalone HTML files"), ("dirhtml", "make HTML files named index.html in directories"), ("pickle", "make pickle files"), ("json", "make JSON files"), ("htmlhelp", "make HTML files and a HTML help project"), ("qthelp", "make HTML files and a qthelp project"), ("devhelp", "make HTML files and a GNOME DevHelp project"), ("epub", "make HTML files and an EPUB file for bookreaders"), ("latex", "make LaTeX sources"), ("texinfo", "make Texinfo sources"), ("text", "make text file for each RST file"), ("pdf", "make PDF file from LaTeX sources"), ("ps", "make PostScript file from LaTeX sources"), ("dvi", "make DVI file from LaTeX sources"), ("changes", "make an overview over all changed/added/deprecated items"), ("linkcheck", "check all external links for integrity"), ("doctest", "run all doctests embedded in the documentation if enabled"), ("source", "run a command to generate the reStructuredText source"), ) # LaTeX builders. latex_builders = {"pdf": "PDF", "ps": "PostScript", "dvi": "DVI"} # List of target names. targetnames = [name for name, desc in targets] # Configuration cache filename. cachefile = ".sconsrc-sphinx" # User cache file. homedir = os.path.expanduser('~') usercache = os.path.join(homedir, cachefile) # Configuration options. config = Variables([usercache, cachefile], ARGUMENTS) config.AddVariables( EnumVariable("default", "default build target", "html", targetnames), PathVariable("config", "sphinx configuration file", "conf.py"), PathVariable("srcdir", "source directory", ".", PathVariable.PathIsDir), PathVariable("builddir", "build directory", "build", PathVariable.PathIsDirCreate), PathVariable("doctrees", "place to put doctrees", None, PathVariable.PathAccept), EnumVariable("paper", "LaTeX paper size", None, ["a4", "letter"], ignorecase = False), ("tags", "comma-separated list of 'only' tags", None), ("builder", "program to run to build things", "sphinx-build"), ("opts", "extra builder options to use", None), ListVariable("install", "targets to install", ["html"], targetnames), PathVariable("instdir", "installation directory", "/usr/local/doc", PathVariable.PathAccept), EnumVariable("pkgtype", "package type to build with 'scons package'", "zip", ["zip", "targz", "tarbz2"], ignorecase = False), BoolVariable("cache", "whether to cache settings in %s" % cachefile, False), BoolVariable("debug", "debugging flag", False), ("genrst", "Command to regenerate reStructuredText source", None), ) # Create a new environment, inheriting PATH to find builder program. Also # force LaTeX instead of TeX, since the .tex file won't exist at the right # time to check which one to use. env = Environment(ENV = {"PATH" : os.environ["PATH"]}, TEX = "latex", PDFTEX = "pdflatex", tools = ['default', 'packaging'], variables = config) if 'PYTHONPATH' in os.environ: env['ENV']['PYTHONPATH'] = os.environ['PYTHONPATH'] # Get configuration values from environment. sphinxconf = env["config"] builder = env["builder"] default = env["default"] srcdir = env["srcdir"] builddir = env["builddir"] doctrees = env.get("doctrees", os.path.join(builddir, "doctrees")) cache = env["cache"] debug = env["debug"] options = env.get("opts", None) paper = env.get("paper", None) tags = env.get("tags", None) genrst = env.get("genrst", None) instdir = env["instdir"] install = env["install"] pkgtype = env["pkgtype"] # Dump internals if debugging. if debug: print "Environment:" print env.Dump() # Get parameters from Sphinx config file. #sphinxparams = {} #execfile(sphinxconf, sphinxparams) sphinxparams = runpy.run_path(sphinxconf) project = sphinxparams["project"] release = sphinxparams["release"] copyright = sphinxparams["copyright"] try: texfilename = sphinxparams["latex_documents"][0][1] except KeyError: texfilename = None name2tag = lambda name: name.replace(" ", "-").strip("()") project_tag = name2tag(project) release_tag = name2tag(release) package_tag = project_tag.lower() + "-" + release_tag.lower() # Build project description string. description = "%(project)s, release %(release)s, " \ "copyright %(copyright)s" % locals() Help(description + "\n\n") help_format = " %-10s %s\n" # Print banner if required. if not any(map(GetOption, ("silent", "clean", "help"))): print print "This is", description print # Build sphinx command-line options. opts = [] if tags: opts.extend(["-t %s" % tag for tag in tags.split(",")]) if paper: opts.append("-D latex_paper_size=%s" % paper) if options: opts.append(options) options = " ".join(opts) # Build Sphinx command template. sphinxcmd = """ %(builder)s -b %(name)s -d %(doctrees)s %(options)s %(srcdir)s %(targetdir)s """.strip() # Set up LaTeX input builder if required. if texfilename: latexdir = Dir("latex", builddir) texinput = File(texfilename, latexdir) env.SideEffect(texinput, "latex") env.NoClean(texinput) # Add build targets. Help("Build targets:\n\n") if genrst != None: source = env.Command('source', [], genrst, chdir = True) env.AlwaysBuild(source) env.Depends(srcdir, source) else: source = env.Command( 'source', [], '@echo "No reStructuredText generator (genrst) given."') for name, desc in targets: target = Dir(name, builddir) targetdir = str(target) if name == 'source': pass elif name not in latex_builders: # Standard Sphinx target. targets = env.Command(name, sphinxconf, sphinxcmd % locals(), chdir = True) env.Depends(targets, source) env.AlwaysBuild(name) env.Alias(target, name) elif texinput: # Target built from LaTeX sources. try: buildfunc = getattr(env, latex_builders[name]) except AttributeError: continue filename = project_tag + "." + name outfile = File(filename, latexdir) targets = buildfunc(outfile, texinput) env.Depends(targets, source) # Copy built file to separate directory. target = File(filename, target) env.Command(target, outfile, Move(target, outfile), chdir = True) env.Alias(name, target) else: continue env.Clean(name, [target]) env.Clean('all', target) if name == default: desc += " (default)" Help(help_format % (name, desc)) Clean('all', doctrees) Default(default) # Add installation targets and collect package sources. Help("\nOther targets:\n\n") Help(help_format % ("install", "install documentation")) projectdir = os.path.join(instdir, project_tag) sources = [] for name in install: source = Dir(name, builddir) sources.append(source) inst = env.Install(projectdir, source) env.Alias('install', inst) for node in env.Glob(os.path.join(str(source), '*')): filename = str(node).replace(builddir + os.path.sep, "") dirname = os.path.dirname(filename) dest = os.path.join(projectdir, dirname) inst = env.Install(dest, node) env.Alias('install', inst) # Add uninstall target. env.Command('uninstall', None, Delete(projectdir), chdir = True) Help(help_format % ("uninstall", "uninstall documentation")) # Add package builder. packageroot = "-".join([project_tag, release_tag]) archive, package = env.Package(NAME = project_tag, VERSION = release, PACKAGEROOT = packageroot, PACKAGETYPE = pkgtype, source = sources) env.AlwaysBuild(archive) env.AddPostAction(archive, Delete(packageroot)) Help(help_format % ("package", "build documentation package")) env.Clean('all', archive) # Add config settings to help. Help("\nConfiguration variables:") for line in config.GenerateHelpText(env).split("\n"): Help("\n " + line) # Save local configuration if required. if cache: config.Update(env) config.Save(cachefile, env) ================================================ FILE: src/docs/conf.py ================================================ # -*- coding: utf-8 -*- import os import sys on_rtd = os.environ.get("READTHEDOCS", None) == "True" project = "Astral" author = "Simon Kennedy" copyright = "2009-2022, %s" % author version = "3.2" release = "3.2" extensions = [ "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.napoleon", "sphinx.ext.doctest", ] intersphinx_mapping = { "python": ("http://docs.python.org/3", "python3_intersphinx.inv") } # Add parent directory so autodoc can find the source code sys.path.insert(0, os.path.join(os.path.abspath(".."))) source_suffix = ".rst" master_doc = "index" pygments_style = "sphinx" templates_path = ["templates"] # endregion if not on_rtd: html_theme = "sphinx_book_theme" else: html_theme = "basic" html_logo = os.path.join("static", "earth_sun.png") if not on_rtd: html_favicon = os.path.join("static", "weather-sunny.png") html_static_path = ["static"] html_css_files = [ "astral.css", ] html_domain_indices = False ================================================ FILE: src/docs/index.rst ================================================ .. 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. .. TODO: Spatial reference system - WGS-84? .. TODO: Add note about accuracy .. TODO: Add note that 0.833 is half sun's disc + refraction adjustment Astral v\ |release| =================== | |ghaction_status| |pypi_ver| Astral is a python package for calculating the times of various aspects of the sun and moon. It can calculate the following Dawn The time in the morning when the sun is a specific number of degrees below the horizon. Sunrise The time in the morning when the top of the sun breaks the horizon (assuming a location with no obscuring features.) Noon The time when the sun is at its highest point directly above the observer. Midnight The time when the sun is at its lowest point. Sunset The time in the evening when the sun is about to disappear below the horizon (asuming a location with no obscuring features.) Dusk The time in the evening when the sun is a specific number of degrees below the horizon. Daylight The time when the sun is up i.e. between sunrise and sunset Night The time between astronomical dusk of one day and astronomical dawn of the next Twilight The time between dawn and sunrise or between sunset and dusk The Golden Hour The time when the sun is between 4 degrees below the horizon and 6 degrees above. The Blue Hour The time when the sun is between 6 and 4 degrees below the horizon. Time At Elevation the time when the sun is at a specific elevation for either a rising or a setting sun. Solar Azimuth The number of degrees clockwise from North at which the sun can be seen Solar Zenith The angle of the sun down from directly above the observer Solar Elevation The number of degrees up from the horizon at which the sun can be seen `Rahukaalam`_ "Rahukaalam or the period of Rahu is a certain amount of time every day that is considered inauspicious for any new venture according to Indian Vedic astrology". Moonrise and Moonset Like the Sun but for the moon Moon Azimuth and Zenith Also like the Sun but for the moon Moon Phase The phase of the moon for a specified date. Astral also comes with a geocoder containing a local database that allows you to look up information for a small set of locations (`new locations can be added `__). Examples ======== The following examples demonstrates some of the functionality available in the module Sun ---- .. testcode:: from astral import LocationInfo city = LocationInfo("London", "England", "Europe/London", 51.5, -0.116) print(( f"Information for {city.name}/{city.region}\n" f"Timezone: {city.timezone}\n" f"Latitude: {city.latitude:.02f}; Longitude: {city.longitude:.02f}\n" )) .. testoutput:: Information for London/England Timezone: Europe/London Latitude: 51.50; Longitude: -0.12 .. testcode:: import datetime from astral.sun import sun s = sun(city.observer, date=datetime.date(2009, 4, 22)) print(( f'Dawn: {s["dawn"]}\n' f'Sunrise: {s["sunrise"]}\n' f'Noon: {s["noon"]}\n' f'Sunset: {s["sunset"]}\n' f'Dusk: {s["dusk"]}\n' )) .. testoutput:: Dawn: 2009-04-22 04:13:04.997608+00:00 Sunrise: 2009-04-22 04:50:17.127004+00:00 Noon: 2009-04-22 11:59:02+00:00 Sunset: 2009-04-22 19:08:41.711407+00:00 Dusk: 2009-04-22 19:46:06.423846+00:00 .. note:: The example above calculates the times of the sun in the UTC timezone. If you want to return times in a different timezone you can pass the `tzinfo` parameter to the function. .. code-block:: python >>> city = LocationInfo("London", "England", "Europe/London", 51.5, -0.116) >>> london = Location(city) >>> s = sun(city.observer, date=datetime.date(2009, 4, 22), tzinfo=london.timezone) or .. code-block:: python >>> timezone = zoneinfo.ZoneInfo("Europe/London") >>> s = sun(city.observer, date=datetime.date(2009, 4, 22), tzinfo=timezone) Moon ---- The moon rise/set times can be obtained like the sun's functions .. testcode:: from astral import LocationInfo city = LocationInfo("London", "England", "Europe/London", 51.5, -0.116) import datetime from astral.moon import moonrise from astral.location import Location dt = datetime.date(2021, 10, 28) london = Location(city) rise = moonrise(city.observer, dt) # returns a UTC time print(rise) .. testoutput:: 2021-10-28 22:02:00+00:00 And for a local time .. testsetup:: import datetime from astral import LocationInfo from astral.moon import moonrise from astral.location import Location city = LocationInfo("London", "England", "Europe/London", 51.5, -0.116) dt = datetime.date(2021, 10, 28) london = Location(city) .. testcode:: rise = moonrise(city.observer, dt, city.tzinfo) Phase ~~~~~~ .. testcode:: import datetime from astral import moon phase = moon.phase(datetime.date(2018, 1, 1)) print(phase) .. testoutput:: 13.255666666666668 The moon phase method returns an number describing the phase, where the value is between 0 and 27.99. The following lists the mapping of various values to the description of the phase of the moon. ============ ============== 0 .. 6.99 New moon 7 .. 13.99 First quarter 14 .. 20.99 Full moon 21 .. 27.99 Last quarter ============ ============== If for example the number returned was 27.99 then the moon would be almost at the New Moon phase, and if it was 24.00 it would be half way between the Last Quarter and a New Moon. .. note :: The moon phase does not depend on your location. However what the moon actually looks like to you does depend on your location. If you're in the southern hemisphere it looks different than if you were in the northern hemisphere. See http://moongazer.x10.mx/website/astronomy/moon-phases/ for an example. For an example of using this library to generate moon phases including the names in various languages and the correct Unicode glyphs see the `project by PanderMusubi `_ on Github. Geocoder -------- .. code-block:: python >>> from astral.geocoder import database, lookup >>> lookup("London", database()) LocationInfo(name='London', region='England', timezone='Europe/London', latitude=51.473333333333336, longitude=-0.0008333333333333334) .. note:: Location elevations have been removed from the database. These were added due to a misunderstanding of the affect of elevation on the times of the sun. These are not required for the calculations, only the elevation of the observer above/below the location is needed. See `Effect of Elevation`_ below. Custom Location ~~~~~~~~~~~~~~~ If you only need a single location that is not in the database then you can construct a :class:`~astral.LocationInfo` and fill in the values, either on initialization .. code-block:: python from astral import LocationInfo l = LocationInfo('name', 'region', 'timezone/name', 0.1, 1.2) or set the attributes after initialization:: from astral import LocationInfo l = LocationInfo() l.name = 'name' l.region = 'region' l.timezone = 'US/Central' l.latitude = 0.1 l.longitude = 1.2 .. note:: `name` and `region` can be anything you like. .. _additional_locations: Additional Locations ~~~~~~~~~~~~~~~~~~~~ You can add to the list of available locations using the :func:`~astral.geocoder.add_locations` function and 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). .. code-block:: python >>> from astral.geocoder import add_locations, database, lookup >>> db = database() >>> try: ... lookup("Somewhere", db) ... except KeyError: ... print("Somewhere not found") ... Somewhere not found >>> add_locations("Somewhere,Secret Location,UTC,24°28'N,39°36'E", db) >>> lookup("Somewhere", db) LocationInfo(name='Somewhere', region='Secret Location', timezone='UTC', latitude=24.466666666666665, longitude=39.6) Timezone Groups ~~~~~~~~~~~~~~~ Timezone groups such as Europe can be accessed via the :func:`group` function in the :mod:`~astral.geocoder` module .. testcode:: from astral.geocoder import group, database db = database() europe = group("europe", db) print(sorted(europe.keys())[:4]) .. testoutput:: ['aberdeen', 'amsterdam', 'andorra_la_vella', 'ankara'] Effect of Elevation =================== Times Of The Sun ---------------- The times of the sun that you experience depend on what obscurs your view of it. It may either be obscured by the horizon or some other geographical feature (e.g. mountains) 1. If what obscures you at ground level is the horizon and you are at a elevation above ground level then the times of the sun depends on how far further round the earth you can see due to your elevation (the sun rises earlier and sets later). The extra angle you can see round the earth is determined by calculating the angle α in the image below based on your elevation above ground level, and adding this to the depression angle for the sun calculations. .. image:: static/elevation_horizon.svg :class: adjustment 2. If your view is obscured by some other geographical feature than the horizon, then the adjustment angle is based on how far you are above or below the feature and your distance to it. For the first case i.e. obscured by the horizon you need to pass a single float to the Observer as its elevation. For the second case pass a tuple of 2 floats. The first being the vertical distance to the top of the feature and the second the horizontal distance to the feature. Elevation Of The Sun -------------------- Even though an observer's elevation can significantly affect the times of the sun the same is not true for the elevation angle from the observer to the sun. As an example the diagram below shows the difference in angle between an observer at ground level and one on the ISS orbiting 408 km above the earth. .. image:: static/elevation_sun.svg :class: adjustment The largest difference between the two angles is when the angle at ground level is 1 degree. The difference then is approximately 0.15 degrees. At the summit of mount Everest (8,848 m) the maximum difference is 0.00338821 degrees. Due to the very small difference the astral package does not currently adjust the solar elevation for changes in observer elevation. Effect of Refraction ==================== When viewing the sun the position you see it at is different from its actual position due to the effect of atmospheric `refraction`_ which makes the sun appear to be higher in the sky. The calculations in the package take this refraction into account. The :func:`~astral.sun.sunrise` and :func:`~astral.sun.sunset` functions use the refraction at an angle when the sun is half of its apparent diameter below the horizon. This is between about 30 and 32 arcminutes and for the astral package a value of 32" is used. .. note:: The refraction calculation does not take into account temperature and pressure which can affect the angle of refraction. License ======= This module is licensed under the terms of the `Apache`_ V2.0 license. Installation ============ To install Astral you should use the `pip`_ tool:: pip3 install astral .. note:: Now that we are Python 3 only and pip provides a versioned executable on Windows you should use the `pip3` command on all operating systems to ensure you are targetting the right Python version. Cities ====== The module includes location and time zone data for the following cities. The list includes all capital cities plus some from the UK. The list also includes the US state capitals and some other US cities. Aberdeen, Abu Dhabi, Abu Dhabi, Abuja, Accra, Addis Ababa, Adelaide, Al Jubail, Albany, Albuquerque, Algiers, Amman, Amsterdam, Anchorage, Andorra la Vella, Ankara, Annapolis, Antananarivo, Apia, Ashgabat, Asmara, Astana, Asuncion, Athens, Atlanta, Augusta, Austin, Avarua, Baghdad, Baku, Baltimore, Bamako, Bandar Seri Begawan, Bangkok, Bangui, Banjul, Barrow-In-Furness, Basse-Terre, Basseterre, Baton Rouge, Beijing, Beirut, Belfast, Belgrade, Belmopan, Berlin, Bern, Billings, Birmingham, Birmingham, Bishkek, Bismarck, Bissau, Bloemfontein, Bogota, Boise, Bolton, Boston, Bradford, Brasilia, Bratislava, Brazzaville, Bridgeport, Bridgetown, Brisbane, Bristol, Brussels, Bucharest, Bucuresti, Budapest, Buenos Aires, Buffalo, Bujumbura, Burlington, Cairo, Canberra, Cape Town, Caracas, Cardiff, Carson City, Castries, Cayenne, Charleston, Charlotte, Charlotte Amalie, Cheyenne, Chicago, Chisinau, Cleveland, Columbia, Columbus, Conakry, Concord, Copenhagen, Cotonou, Crawley, Dakar, Dallas, Damascus, Dammam, Denver, Des Moines, Detroit, Dhaka, Dili, Djibouti, Dodoma, Doha, Douglas, Dover, Dublin, Dushanbe, Edinburgh, El Aaiun, Fargo, Fort-de-France, Frankfort, Freetown, Funafuti, Gaborone, George Town, Georgetown, Gibraltar, Glasgow, Greenwich, Guatemala, Hanoi, Harare, Harrisburg, Hartford, Havana, Helena, Helsinki, Hobart, Hong Kong, Honiara, Honolulu, Houston, Indianapolis, Islamabad, Jackson, Jacksonville, Jakarta, Jefferson City, Jerusalem, Juba, Jubail, Juneau, Kabul, Kampala, Kansas City, Kathmandu, Khartoum, Kiev, Kigali, Kingston, Kingston, Kingstown, Kinshasa, Koror, Kuala Lumpur, Kuwait, La Paz, Lansing, Las Vegas, Leeds, Leicester, Libreville, Lilongwe, Lima, Lincoln, Lisbon, Little Rock, Liverpool, Ljubljana, Lome, London, Los Angeles, Louisville, Luanda, Lusaka, Luxembourg, Macau, Madinah, Madison, Madrid, Majuro, Makkah, Malabo, Male, Mamoudzou, Managua, Manama, Manchester, Manchester, Manila, Maputo, Maseru, Masqat, Mbabane, Mecca, Medina, Melbourne, Memphis, Mexico, Miami, Milwaukee, Minneapolis, Minsk, Mogadishu, Monaco, Monrovia, Montevideo, Montgomery, Montpelier, Moroni, Moscow, Moskva, Mumbai, Muscat, N'Djamena, Nairobi, Nashville, Nassau, Naypyidaw, New Delhi, New Orleans, New York, Newark, Newcastle, Newcastle Upon Tyne, Ngerulmud, Niamey, Nicosia, Norwich, Nouakchott, Noumea, Nuku'alofa, Nuuk, Oklahoma City, Olympia, Omaha, Oranjestad, Orlando, Oslo, Ottawa, Ouagadougou, Oxford, P'yongyang, Pago Pago, Palikir, Panama, Papeete, Paramaribo, Paris, Perth, Philadelphia, Phnom Penh, Phoenix, Pierre, Plymouth, Podgorica, Port Louis, Port Moresby, Port of Spain, Port-Vila, Port-au-Prince, Portland, Portland, Porto-Novo, Portsmouth, Prague, Praia, Pretoria, Pristina, Providence, Quito, Rabat, Raleigh, Reading, Reykjavik, Richmond, Riga, Riyadh, Road Town, Rome, Roseau, Sacramento, Saint Helier, Saint Paul, Saint Pierre, Saipan, Salem, Salt Lake City, San Diego, San Francisco, San Jose, San Juan, San Marino, San Salvador, Sana, Sana'a, Santa Fe, Santiago, Santo Domingo, Sao Tome, Sarajevo, Seattle, Seoul, Sheffield, Singapore, Sioux Falls, Skopje, Sofia, Southampton, Springfield, Sri Jayawardenapura Kotte, St. George's, St. John's, St. Peter Port, Stanley, Stockholm, Sucre, Suva, Swansea, Swindon, Sydney, T'bilisi, Taipei, Tallahassee, Tallinn, Tarawa, Tashkent, Tbilisi, Tegucigalpa, Tehran, Thimphu, Tirana, Tirane, Tokyo, Toledo, Topeka, Torshavn, Trenton, Tripoli, Tunis, Ulaanbaatar, Ulan Bator, Vaduz, Valletta, Vienna, Vientiane, Vilnius, Virginia Beach, W. Indies, Warsaw, Washington DC, Wellington, Wichita, Willemstad, Wilmington, Windhoek, Wolverhampton, Yamoussoukro, Yangon, Yaounde, Yaren, Yerevan, Zagreb, Zurich US Cities --------- Albany, Albuquerque, Anchorage, Annapolis, Atlanta, Augusta, Austin, Baltimore, Baton Rouge, Billings, Birmingham, Bismarck, Boise, Boston, Bridgeport, Buffalo, Burlington, Carson City, Charleston, Charlotte, Cheyenne, Chicago, Cleveland, Columbia, Columbus, Concord, Dallas, Denver, Des Moines, Detroit, Dover, Fargo, Frankfort, Harrisburg, Hartford, Helena, Honolulu, Houston, Indianapolis, Jackson, Jacksonville, Jefferson City, Juneau, Kansas City, Lansing, Las Vegas, Lincoln, Little Rock, Los Angeles, Louisville, Madison, Manchester, Memphis, Miami, Milwaukee, Minneapolis, Montgomery, Montpelier, Nashville, New Orleans, New York, Newark, Oklahoma City, Olympia, Omaha, Orlando, Philadelphia, Phoenix, Pierre, Portland, Portland, Providence, Raleigh, Richmond, Sacramento, Saint Paul, Salem, Salt Lake City, San Diego, San Francisco, Santa Fe, Seattle, Sioux Falls, Springfield, Tallahassee, Toledo, Topeka, Trenton, Virginia Beach, Wichita, Wilmington Thanks ====== The sun calculations in this module were adapted, for Python, from the spreadsheets on the following page. | https://www.esrl.noaa.gov/gmd/grad/solcalc/calcdetails.html Refraction calculation is taken from | Sun-Pointing Programs and Their Accuracy | John C. Zimmerman Of Sandia National Laboratones | https://www.osti.gov/servlets/purl/6377969 Which cites the following as the original source | In Solar Energy Vol 20 No.5-C | Robert Walraven Of The University Of California, Davis Moon position calculations from | LOW-PRECISION FORMULAE FOR PLANETARY POSITIONS | T. C. Van Flandern and K. F. Pulkkinen And from | Astronomical Algorithms | Jean Meeus The moon phase calculation is based on javascript code from Sky and Telescope magazine | Moon-phase calculation | Roger W. Sinnott, Sky & Telescope, June 16, 2006. | https://skyandtelescope.org/observing/the-phase-of-the-moon/ Also to `Sphinx`_ for making doc generation an easy thing (not that the writing of the docs is any easier.) Contact ======= Simon Kennedy Version History =============== ========== ==================================================================== Version Description ========== ==================================================================== 3.2 Dropped Python 3.6 support as it has reached "End of Life" Documentation now hosted on `Github Pages `_ ---------- -------------------------------------------------------------------- 3.1 Fix for `issue #77`_ ---------- -------------------------------------------------------------------- 3.0 Added moon rise, set, azimuth and zenith functions.https://github.com/sffjunkie/astral/issues/77 Switched from pytz to `zoneinfo` provided as part of Python >= 3.9 or `backports.zoneinfo` for older versions. In some circumstances the result of the calculation of rise and set times would return information for a different date. This has now been fixed. ---------- -------------------------------------------------------------------- 2.2 Fix for `bug #48`_ - As per the bug report the angle to adjust for the effect of elevation should have been θ (not α). The sun functions can now also be passed a timezone name as a string. Previously only a pytz timezone was accepted. ---------- -------------------------------------------------------------------- 2.1 Fix for bug #44 - Incorrectly raised exception when UTC sun times were on the day previous to the day asked for. Only manifested for timezones with a large positive offset. ---------- -------------------------------------------------------------------- 2.0 This is a code refactor as well as an update so it is highly likely that you will need to adapt your code to suit. Astral, AstralGeocoder & GoogleGeocoder classes removed Now only compatible with Python 3.6 and greater due to the use of data classes New :class:`~astral.Observer` data class to store a latitude, longitude & elevation New :class:`~astral.LocationInfo` data class to store a location name, region, timezone, latitude & longitude Geocoder functions return a :class:`~astral.LocationInfo` instead of a :class:`~astral.location.Location` All calculations now automatically adjust for refraction. For elevation you can return the true angle by setting the `with_refraction` parameter to False. The solar_noon and solar_midnight functions have been renamed to :func:`~astral.sun.noon` and :func:`~astral.sun.midnight` respectively. Rahukaalam can now be calculated for night times. ---------- -------------------------------------------------------------------- 1.10.1 Keyword args are now passed to the geocoder class from Astral __init__ in order to allow the Google Maps API key to be passed to the GoogleGeocoder. ---------- -------------------------------------------------------------------- 1.10 Added support to AstralGeocoder to add `additional locations `__ to the database. ---------- -------------------------------------------------------------------- 1.9.2 1.9 broke the sun_utc method. Sun UTC calculation passed incorrect parameter to more specific methods e.g. sunrise, sunset etc. ---------- -------------------------------------------------------------------- 1.9.1 Correct version number in astral.py ---------- -------------------------------------------------------------------- 1.9 Now takes elevation into account. ---------- -------------------------------------------------------------------- 1.8 Location methods now allow the timezone to be None which returns all times as UTC. Added command line interface to return 'sun' values ---------- -------------------------------------------------------------------- 1.7.1 Changed GoogleGeocoder test to not use raise...from as this is not valid for Python 2 ---------- -------------------------------------------------------------------- 1.7 Requests is now only needed when using GoogleGeocoder GoogleGeocoder now requires the `api_key` parameter to be passed to the constructor as Google now require it for their API calls. ---------- -------------------------------------------------------------------- 1.6.1 Updates for Travis CI integration / Github signed release. ---------- -------------------------------------------------------------------- 1.6 Added api_key parameter to the GoogleGeocoder :meth:`__init__` method ---------- -------------------------------------------------------------------- 1.5 Added parameter `rtype` to :meth:`moon_phase` to determine the return type of the method. Added example for calculating the phase of the moon. ---------- -------------------------------------------------------------------- 1.4.1 Using versioneer to manage version numbers ---------- -------------------------------------------------------------------- 1.4 Changed to use calculations from NOAA spreadsheets Changed some exception error messages for when sun does not reach a requested elevation. Added more tests ---------- -------------------------------------------------------------------- 1.3.4 Changes to project configuration files. No user facing changes. ---------- -------------------------------------------------------------------- 1.3.3 Fixed call to twilight_utc as date and direction parameters were reversed. ---------- -------------------------------------------------------------------- 1.3.2 Updated URL to point to gitgub.com Added Apache 2.0 boilerplate to source file ---------- -------------------------------------------------------------------- 1.3.1 Added LICENSE file to sdist ---------- -------------------------------------------------------------------- 1.3 Corrected solar zenith to return the angle from the vertical. Added solar midnight calculation. ---------- -------------------------------------------------------------------- 1.2 Added handling for when unicode literals are used. This may possibly affect your code if you're using Python 2 (there are tests for this but they may not catch all uses.) (Bug `1588198`_\) Changed timezone for Phoenix, AZ to America/Phoenix (Bug `1561258`_\) ---------- -------------------------------------------------------------------- 1.1 Added methods to calculate Twilight, the Golden Hour and the Blue Hour. ---------- -------------------------------------------------------------------- 1.0 It's time for a version 1.0 Added examples where the location you want is not in the Astral geocoder. ---------- -------------------------------------------------------------------- 0.9 Added a method to calculate the date and time when the sun is at a specific elevation, for either a rising or a setting sun. Added daylight and night methods to Location and Astral classes. Rahukaalam methods now return a tuple. ---------- -------------------------------------------------------------------- 0.8.2 Fix for moon phase calcualtions which were off by 1. Use pytz.timezone().localize method instead of passing tzinfo parameter to datetime.datetime. See the `pytz docs`_ for info ---------- -------------------------------------------------------------------- 0.8.1 Fix for bug `1417641`_\: :meth:`~astral.Astral.solar_elevation` and :meth:`~astral.Astral.solar_azimuth` fail when a naive :class:`~datetime.datetime` object is used. Added :meth:`solar_zenith` methods to :class:`~astral.Astral` and :class:`~astral.Location` as an alias for :meth:`solar_elevation` Added `tzinfo` as an alias for `tz` ---------- -------------------------------------------------------------------- 0.8 Fix for bug `1407773`_\: Moon phase calculation changed to remove time zone parameter (tz) as it is not required for the calculation. ---------- -------------------------------------------------------------------- 0.7.5 Fix for bug `1402103`_\: Buenos Aires incorrect timezone ---------- -------------------------------------------------------------------- 0.7.4 Added Canadian cities from Yip Shing Ho ---------- -------------------------------------------------------------------- 0.7.3 Fix for bug `1239387`_ submitted by Torbjörn Lönnemark ---------- -------------------------------------------------------------------- 0.7.2 Minor bug fix in :class:`~astral.GoogleGeocoder`. location name and region are now stripped of whitespace ---------- -------------------------------------------------------------------- 0.7.1 Bug fix. Missed a vital return statement in the :class:`~astral.GoogleGeocoder` ---------- -------------------------------------------------------------------- 0.7 Added ability to lookup location information from Google's mapping APIs (see :class:`~astral.GoogleGeocoder`) Renamed :class:`City` class to :class:`~astral.Location` Renamed :class:`CityDB` to :class:`~astral.AstralGeocoder` Added elevations of cities to database and property to obtain elevation from :class:`~astral.Location` class ---------- -------------------------------------------------------------------- 0.6.2 Added various cities to database as per https://bugs.launchpad.net/astral/+bug/1040936 ---------- -------------------------------------------------------------------- 0.6.1 Docstrings were not updated to match changes to code. Other minor docstring changes made ---------- -------------------------------------------------------------------- 0.6 Fix for bug `884716`_ submitted by Martin Heemskerk regarding moon phase calculations Fixes for bug report `944754`_ submitted by Hajo Werder - Changed co-ordinate system so that eastern longitudes are now positive - Added solar_depression property to City class ---------- -------------------------------------------------------------------- 0.5 Changed :class:`City` to accept unicode name and country. Moved city information into a database class :class:`CityDB` Added attribute access to database for timezone groups ---------- -------------------------------------------------------------------- 0.4 Duplicate city names could not be accessed. Sun calculations for some cities failed with times outside valid ranges. Fixes for city data. Added calculation for moon phase. ---------- -------------------------------------------------------------------- 0.3 Changed to `Apache`_ V2.0 license. Fix for bug `555508`_ submitted by me. US state capitals and other cities added. ---------- -------------------------------------------------------------------- 0.2 Fix for bug `554041`_ submitted by Derek\_ / John Dimatos ---------- -------------------------------------------------------------------- 0.1 First release ========== ==================================================================== .. _Rahukaalam: http://en.wikipedia.org/wiki/Rahukaalam .. _Sourceforge: http://pytz.sourceforge.net/ .. _easy_install: http://peak.telecommunity.com/DevCenter/EasyInstall .. _Apache: http://www.opensource.org/licenses/apache2.0.php .. _Sphinx: https://www.sphinx-doc.org/ .. _554041: https://bugs.launchpad.net/astral/+bug/554041 .. _555508: https://bugs.launchpad.net/astral/+bug/555508 .. _884716: https://bugs.launchpad.net/astral/+bug/884716 .. _944754: https://bugs.launchpad.net/astral/+bug/944754 .. _1239387: https://bugs.launchpad.net/astral/+bug/1239387 .. _1402103: https://bugs.launchpad.net/astral/+bug/1402103 .. _1407773: https://bugs.launchpad.net/astral/+bug/1407773 .. _1417641: https://bugs.launchpad.net/astral/+bug/1417641 .. _1561258: https://bugs.launchpad.net/astral/+bug/1561258 .. _1588198: https://bugs.launchpad.net/astral/+bug/1588198 .. _pytz docs: http://pytz.sourceforge.net/#localized-times-and-date-arithmetic .. _issue: https://github.com/sffjunkie/astral/issues .. _refraction: https://en.wikipedia.org/wiki/Refraction .. _pip: https://pip.pypa.io/en/stable/ .. _bug #48: https://github.com/sffjunkie/astral/issues/48 .. _issue #77: https://github.com/sffjunkie/astral/issues/77 .. |ghaction_status| image:: https://img.shields.io/github/actions/workflow/status/sffjunkie/astral/astral-test.yml .. |pypi_ver| image:: https://img.shields.io/pypi/v/astral.svg :target: https://pypi.org/project/astral/ .. toctree:: :maxdepth: 2 :hidden: package ================================================ FILE: src/docs/make.bat ================================================ @ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=..\..\build\sphinx set SOURCEDIR=. set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% %SOURCEDIR% set I18NSPHINXOPTS=%SPHINXOPTS% %SOURCEDIR% if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%\html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%\dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%\singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%\htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%\qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\yyryr.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\yyryr.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%\epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%\latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%\latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%\latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%\text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%\man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%\texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%\locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%\changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%\linkcheck\output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%\doctest\output.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%\xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%\pseudoxml. goto end ) :end ================================================ FILE: src/docs/package.rst ================================================ .. 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. The :mod:`astral` Package ========================= .. automodule:: astral :members: astral.sun ~~~~~~~~~~~ .. automodule:: astral.sun :members: astral.moon ~~~~~~~~~~~ .. automodule:: astral.moon :members: astral.geocoder ~~~~~~~~~~~~~~~ .. automodule:: astral.geocoder :members: astral.location ~~~~~~~~~~~~~~~ .. autoclass:: astral.location.Location :members: ================================================ FILE: src/docs/static/astral.css ================================================ img.adjustment { margin: 1rem 1.5rem; } ================================================ FILE: src/test/almost_equal.py ================================================ import 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 ================================================ FILE: src/test/conftest.py ================================================ import pytest # type: ignore 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) ================================================ FILE: src/test/moon/test_moon.py ================================================ # -*- coding: utf-8 -*- import datetime import pytest # type: ignore 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.tzinfo) calc_time = moon.moonrise(wellington.observer, date_, tzinfo=wellington.tzinfo) 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) ================================================ FILE: src/test/moon/test_moon_azimuth.py ================================================ import datetime import pytest # type: ignore 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() ================================================ FILE: src/test/moon/test_moon_position.py ================================================ from 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 moon_position(jd2000) d = date(1992, 4, 12) jd = julianday(d) jd2000 = jd - 2451545 # Julian day relative to Jan 1.5, 2000 moon_position(jd2000) pass if __name__ == "__main__": test_moon_position() ================================================ FILE: src/test/moon/test_moon_rise.py ================================================ def test_moon_rise(): ... ================================================ FILE: src/test/moon/test_sidereal_time.py ================================================ import 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 ================================================ FILE: src/test/test_Location.py ================================================ # -*- coding: utf-8 -*- import dataclasses import datetime try: import zoneinfo except ImportError: from backports import zoneinfo # type: ignore import freezegun import pytest # type: ignore 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.tzinfo) 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.tzinfo) 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.tzinfo) 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.tzinfo) 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.tzinfo) 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.tzinfo) 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.tzinfo) 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.tzinfo) 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.tzinfo) 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 ================================================ FILE: src/test/test_Repr.py ================================================ # -*- coding: utf-8 -*- from astral import LocationInfo from astral.location import Location class TestLocationRepr: def test_default(self): location = Location() assert ( location.__repr__() == "Greenwich/England, tz=Europe/London, lat=51.47, lon=-0.00" ) def test_full(self): location = Location( LocationInfo("London", "England", "Europe/London", 51.68, -0.05) ) assert ( location.__repr__() == "London/England, tz=Europe/London, lat=51.68, lon=-0.05" ) def test_no_region(self): location = Location( LocationInfo( "London", None, "Europe/London", 51.68, -0.05, ) ) assert location.__repr__() == ("London, tz=Europe/London, lat=51.68, lon=-0.05") ================================================ FILE: src/test/test_all.py ================================================ from 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) ================================================ FILE: src/test/test_almost_equal.py ================================================ import 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) ================================================ FILE: src/test/test_buenos_aries.py ================================================ # -*- 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" ================================================ FILE: src/test/test_depression_not_reached.py ================================================ # -*- coding: utf-8 -*- import datetime import pytest # type: ignore 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) ================================================ FILE: src/test/test_geocoder.py ================================================ # -*- coding: utf-8 -*- from functools import reduce from typing import List try: import zoneinfo except ImportError: from backports import zoneinfo # type: ignore 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 ================================================ FILE: src/test/test_julian.py ================================================ # type: ignore import 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) ================================================ FILE: src/test/test_location_info.py ================================================ # type: ignore import pytest from astral import LocationInfo try: import zoneinfo except ImportError: from backports import zoneinfo # type: ignore 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") ================================================ FILE: src/test/test_misc.py ================================================ # type: ignore from datetime import timedelta try: import zoneinfo except ImportError: from backports import zoneinfo # type: ignore 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 ================================================ FILE: src/test/test_norway.py ================================================ from datetime import datetime, timedelta, timezone import pytest # type: ignore 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 ================================================ FILE: src/test/test_observer.py ================================================ # type: ignore import 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 ================================================ FILE: src/test/test_sun_calc.py ================================================ import datetime import freezegun import pytest # type: ignore 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.tzinfo) 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 ================================================ FILE: src/test/test_sun_elevation_adjustment.py ================================================ # -*- coding: utf-8 -*- import pytest # type: ignore from astral.sun import adjust_to_horizon, adjust_to_obscuring_feature 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) ================================================ FILE: src/test/test_sun_golden_blue.py ================================================ # -*- coding: utf-8 -*- # Test data taken from http://www.timeanddate.com/sun/uk/london import datetime import freezegun import pytest # type: ignore 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) ================================================ FILE: src/test/test_sun_local.py ================================================ import datetime import pytest # type: ignore 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) ================================================ FILE: src/test/test_sun_utc.py ================================================ # type: ignore # 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) ================================================ FILE: src/test/test_value_error_bug.py ================================================ import 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)) sun["dawn"] if __name__ == "__main__": test_value_error_bug() ================================================ FILE: src/test/test_wellington.py ================================================ import datetime 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), ) ================================================ FILE: tox.ini ================================================ [tox] project = astral envlist = py3 isolated_build = True [testenv] deps = freezegun pytest pytest-runner commands = pytest [testenv:doc] changedir = src/docs deps = sphinx sphinx_press_theme commands = sphinx-build -W -b html . ./html [flake8] max-line-length=88