Full Code of sffjunkie/astral for AI

master ac23ab5c0c69 cached
57 files
282.3 KB
86.8k tokens
353 symbols
1 requests
Download .txt
Showing preview only (300K chars total). Download the full file or copy to clipboard to get everything.
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 <https://github.com/sffjunkie/astral/pull/12>

## 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 <sffjunkie+code@gmail.com>"


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<deg>\d{1,3})[°]((?P<min>\d{1,2})[′'])?((?P<sec>\d{1,2})[″\"])?(?P<dir>[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 <target>' where <target> 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 <additional_locations_>`__).

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 <https://github.com/PanderMusubi/lunar-phase-calendar/>`_
   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
Download .txt
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
Download .txt
SYMBOL INDEX (353 symbols across 34 files)

FILE: src/astral/__init__.py
  function now (line 80) | def now(tz: Optional[datetime.tzinfo] = None) -> datetime.datetime:
  function today (line 89) | def today(tz: Optional[datetime.tzinfo] = None) -> datetime.date:
  function dms_to_float (line 94) | def dms_to_float(
  function hours_to_time (line 144) | def hours_to_time(value: float) -> datetime.time:
  function time_to_hours (line 160) | def time_to_hours(value: datetime.time) -> float:
  function time_to_seconds (line 172) | def time_to_seconds(value: datetime.time) -> float:
  function refraction_at_zenith (line 179) | def refraction_at_zenith(zenith: float) -> float:
  class Depression (line 205) | class Depression(Enum):
  class SunDirection (line 213) | class SunDirection(Enum):
  class AstralBodyPosition (line 221) | class AstralBodyPosition:
  class Observer (line 230) | class Observer:
    method __setattr__ (line 258) | def __setattr__(self, name: str, value: Union[str, float, Elevation]):
  class LocationInfo (line 272) | class LocationInfo:
    method __setattr__ (line 297) | def __setattr__(self, name: str, value: Union[Degrees, str]):
    method observer (line 305) | def observer(self):
    method tzinfo (line 310) | def tzinfo(self):  # type: ignore
    method timezone_group (line 315) | def timezone_group(self):

FILE: src/astral/geocoder.py
  function database (line 429) | def database() -> LocationDatabase:
  function _sanitize_key (line 438) | def _sanitize_key(key: str) -> str:
  function _get_group (line 447) | def _get_group(name: str, db: LocationDatabase) -> Optional[GroupInfo]:
  function _add_location_to_db (line 451) | def _add_location_to_db(location: LocationInfo, db: LocationDatabase) ->...
  function _locationinfo_from_str (line 466) | def _locationinfo_from_str(info: str) -> LocationInfo:
  function _locationinfo_from_indexable (line 477) | def _locationinfo_from_indexable(
  function _add_locations_from_str (line 489) | def _add_locations_from_str(location_string: str, db: LocationDatabase) ...
  function _add_locations_from_list (line 499) | def _add_locations_from_list(
  function add_locations (line 514) | def add_locations(
  function group (line 529) | def group(region: str, db: LocationDatabase) -> GroupInfo:
  function lookup_in_group (line 549) | def lookup_in_group(
  function lookup (line 594) | def lookup(name: str, db: LocationDatabase) -> Union[GroupInfo, Location...
  function all_locations (line 622) | def all_locations(db: LocationDatabase) -> Generator[LocationInfo, None,...

FILE: src/astral/julian.py
  class Calendar (line 6) | class Calendar(Enum):
  function day_fraction_to_time (line 11) | def day_fraction_to_time(fraction: float) -> datetime.time:
  function julianday (line 21) | def julianday(
  function julianday_modified (line 63) | def julianday_modified(at: datetime.datetime) -> float:
  function julianday_to_datetime (line 89) | def julianday_to_datetime(jd: float) -> datetime.datetime:
  function julianday_to_juliancentury (line 128) | def julianday_to_juliancentury(julianday: float) -> float:
  function juliancentury_to_julianday (line 133) | def juliancentury_to_julianday(juliancentury: float) -> float:
  function julianday_2000 (line 138) | def julianday_2000(at: Union[datetime.datetime, datetime.date]) -> float:

FILE: src/astral/location.py
  class Location (line 24) | class Location:
    method __init__ (line 27) | def __init__(self, info: Optional[LocationInfo] = None):
    method __eq__ (line 56) | def __eq__(self, other: object) -> bool:
    method __repr__ (line 61) | def __repr__(self) -> str:
    method info (line 73) | def info(self) -> LocationInfo:
    method observer (line 83) | def observer(self) -> Observer:
    method name (line 87) | def name(self) -> str:
    method name (line 91) | def name(self, name: str) -> None:
    method region (line 95) | def region(self) -> str:
    method region (line 99) | def region(self, region: str) -> None:
    method latitude (line 103) | def latitude(self) -> float:
    method latitude (line 118) | def latitude(self, latitude: Union[float, str]) -> None:
    method longitude (line 124) | def longitude(self) -> float:
    method longitude (line 139) | def longitude(self, longitude: Union[float, str]) -> None:
    method timezone (line 145) | def timezone(self) -> str:
    method timezone (line 158) | def timezone(self, name: str) -> None:
    method tzinfo (line 165) | def tzinfo(self) -> zoneinfo.ZoneInfo:  # type: ignore
    method solar_depression (line 179) | def solar_depression(self) -> float:
    method solar_depression (line 198) | def solar_depression(self, depression: Union[float, str, Depression]) ...
    method today (line 219) | def today(self, local: bool = True) -> datetime.date:
    method sun (line 225) | def sun(
    method dawn (line 261) | def dawn(
    method sunrise (line 297) | def sunrise(
    method noon (line 334) | def noon(
    method sunset (line 362) | def sunset(
    method dusk (line 397) | def dusk(
    method midnight (line 434) | def midnight(
    method daylight (line 463) | def daylight(
    method night (line 497) | def night(
    method twilight (line 532) | def twilight(
    method moonrise (line 573) | def moonrise(
    method moonset (line 603) | def moonset(
    method time_at_elevation (line 633) | def time_at_elevation(
    method rahukaalam (line 683) | def rahukaalam(
    method golden_hour (line 716) | def golden_hour(
    method blue_hour (line 759) | def blue_hour(
    method solar_azimuth (line 802) | def solar_azimuth(
    method solar_elevation (line 823) | def solar_elevation(
    method solar_zenith (line 845) | def solar_zenith(
    method moon_phase (line 858) | def moon_phase(self, date: Optional[datetime.date] = None, local: bool...

FILE: src/astral/moon.py
  class NoTransit (line 37) | class NoTransit:
  class TransitEvent (line 42) | class TransitEvent:
  function interpolate (line 49) | def interpolate(f0: float, f1: float, f2: float, p: float) -> float:
  function sgn (line 57) | def sgn(value1: Union[float, datetime.timedelta]) -> int:
  function moon_mean_longitude (line 70) | def moon_mean_longitude(jd2000: float) -> Revolutions:
  function moon_mean_anomoly (line 76) | def moon_mean_anomoly(jd2000: float) -> Revolutions:
  function moon_argument_of_latitude (line 82) | def moon_argument_of_latitude(jd2000: float) -> Revolutions:
  function moon_mean_elongation_from_sun (line 88) | def moon_mean_elongation_from_sun(jd2000: float) -> Revolutions:
  function longitude_lunar_ascending_node (line 96) | def longitude_lunar_ascending_node(jd2000: float) -> Revolutions:
  function sun_mean_longitude (line 103) | def sun_mean_longitude(jd2000: float) -> Revolutions:
  function sun_mean_anomoly (line 109) | def sun_mean_anomoly(jd2000: float) -> Revolutions:
  function venus_mean_longitude (line 115) | def venus_mean_longitude(jd2000: float) -> Revolutions:
  function moon_position (line 121) | def moon_position(jd2000: float) -> AstralBodyPosition:
  function moon_transit_event (line 174) | def moon_transit_event(
  function riseset (line 275) | def riseset(
  function moonrise (line 400) | def moonrise(
  function moonset (line 445) | def moonset(
  function azimuth (line 490) | def azimuth(
  function elevation (line 515) | def elevation(
  function zenith (line 544) | def zenith(
  function _phase_asfloat (line 551) | def _phase_asfloat(date: datetime.date) -> float:
  function phase (line 577) | def phase(date: Optional[datetime.date] = None) -> float:

FILE: src/astral/sidereal.py
  function gmst (line 9) | def gmst(at: Union[datetime.datetime, datetime.date]) -> Degrees:
  function lmst (line 23) | def lmst(

FILE: src/astral/sun.py
  function minutes_to_timedelta (line 48) | def minutes_to_timedelta(minutes: float) -> datetime.timedelta:
  function geom_mean_long_sun (line 62) | def geom_mean_long_sun(juliancentury: float) -> float:
  function geom_mean_anomaly_sun (line 68) | def geom_mean_anomaly_sun(juliancentury: float) -> float:
  function eccentric_location_earth_orbit (line 73) | def eccentric_location_earth_orbit(juliancentury: float) -> float:
  function sun_eq_of_center (line 78) | def sun_eq_of_center(juliancentury: float) -> float:
  function sun_true_long (line 96) | def sun_true_long(juliancentury: float) -> float:
  function sun_true_anomoly (line 104) | def sun_true_anomoly(juliancentury: float) -> float:
  function sun_rad_vector (line 112) | def sun_rad_vector(juliancentury: float) -> float:
  function sun_apparent_long (line 119) | def sun_apparent_long(juliancentury: float) -> float:
  function mean_obliquity_of_ecliptic (line 126) | def mean_obliquity_of_ecliptic(juliancentury: float) -> float:
  function obliquity_correction (line 133) | def obliquity_correction(juliancentury: float) -> float:
  function sun_rt_ascension (line 140) | def sun_rt_ascension(juliancentury: float) -> float:
  function sun_declination (line 151) | def sun_declination(juliancentury: float) -> float:
  function var_y (line 160) | def var_y(juliancentury: float) -> float:
  function eq_of_time (line 166) | def eq_of_time(juliancentury: float) -> Minutes:
  function hour_angle (line 190) | def hour_angle(
  function adjust_to_horizon (line 221) | def adjust_to_horizon(elevation: float) -> float:
  function adjust_to_obscuring_feature (line 242) | def adjust_to_obscuring_feature(elevation: Tuple[float, float]) -> float:
  function time_of_transit (line 253) | def time_of_transit(
  function time_at_elevation (line 328) | def time_at_elevation(
  function noon (line 387) | def noon(
  function midnight (line 450) | def midnight(
  function zenith_and_azimuth (line 519) | def zenith_and_azimuth(
  function zenith (line 610) | def zenith(
  function azimuth (line 634) | def azimuth(
  function elevation (line 659) | def elevation(
  function dawn (line 683) | def dawn(
  function sunrise (line 758) | def sunrise(
  function sunset (line 828) | def sunset(
  function dusk (line 899) | def dusk(
  function daylight (line 973) | def daylight(
  function night (line 1004) | def night(
  function twilight (line 1040) | def twilight(
  function golden_hour (line 1088) | def golden_hour(
  function blue_hour (line 1135) | def blue_hour(
  function rahukaalam (line 1181) | def rahukaalam(
  function sun (line 1232) | def sun(

FILE: src/astral/table4.py
  class Table4Row (line 5) | class Table4Row(NamedTuple):

FILE: src/test/almost_equal.py
  function datetime_almost_equal (line 4) | def datetime_almost_equal(

FILE: src/test/conftest.py
  function test_database (line 9) | def test_database() -> LocationDatabase:
  function london_info (line 14) | def london_info() -> LocationInfo:
  function london (line 20) | def london(london_info: LocationInfo) -> Location:
  function new_delhi_info (line 25) | def new_delhi_info() -> LocationInfo:
  function new_delhi (line 30) | def new_delhi(new_delhi_info: LocationInfo) -> Location:
  function riyadh_info (line 35) | def riyadh_info() -> LocationInfo:
  function riyadh (line 40) | def riyadh(riyadh_info: LocationInfo) -> Location:
  function wellington_info (line 45) | def wellington_info() -> LocationInfo:
  function wellington (line 52) | def wellington(wellington_info: LocationInfo) -> Location:
  function tromso_info (line 57) | def tromso_info() -> LocationInfo:
  function tromso (line 62) | def tromso(tromso_info: LocationInfo) -> Location:

FILE: src/test/moon/test_moon.py
  function test_moon_phase (line 22) | def test_moon_phase(date_: datetime.date, phase: float):
  function test_moonrise_utc (line 35) | def test_moonrise_utc(
  function test_moonset_utc (line 52) | def test_moonset_utc(
  function test_moonrise_riyadh_utc (line 68) | def test_moonrise_riyadh_utc(
  function test_moonset_riyadh_utc (line 85) | def test_moonset_riyadh_utc(
  function test_moonrise_wellington (line 101) | def test_moonrise_wellington(
  function test_moonset_wellington (line 118) | def test_moonset_wellington(

FILE: src/test/moon/test_moon_azimuth.py
  function test_moon_azimuth (line 19) | def test_moon_azimuth(dt: datetime.datetime, value: float, london: Locat...
  function print_moon_azimuth (line 24) | def print_moon_azimuth():

FILE: src/test/moon/test_moon_position.py
  function test_moon_position (line 6) | def test_moon_position():

FILE: src/test/moon/test_moon_rise.py
  function test_moon_rise (line 1) | def test_moon_rise():

FILE: src/test/moon/test_sidereal_time.py
  function test_gmst (line 7) | def test_gmst():
  function test_gmst_with_time (line 18) | def test_gmst_with_time():
  function test_local_mean_sidereal_time (line 28) | def test_local_mean_sidereal_time():

FILE: src/test/test_Location.py
  class TestLocation (line 18) | class TestLocation:
    method test_Name (line 21) | def test_Name(self):
    method test_Region (line 28) | def test_Region(self):
    method test_TimezoneName (line 35) | def test_TimezoneName(self):
    method test_TimezoneNameBad (line 42) | def test_TimezoneNameBad(self):
    method test_TimezoneLookup (line 48) | def test_TimezoneLookup(self):
    method test_Info (line 55) | def test_Info(self, london: Location, london_info: LocationInfo):
    method test_Sun (line 58) | def test_Sun(self, london: Location):
    method test_Dawn (line 64) | def test_Dawn(self, london: Location):
    method test_DawnUTC (line 71) | def test_DawnUTC(self, london: Location):
    method test_Sunrise (line 78) | def test_Sunrise(self, london: Location):
    method test_SunriseUTC (line 84) | def test_SunriseUTC(self, london: Location):
    method test_SolarNoon (line 90) | def test_SolarNoon(self, london: Location):
    method test_SolarNoonUTC (line 96) | def test_SolarNoonUTC(self, london: Location):
    method test_Dusk (line 102) | def test_Dusk(self, london: Location):
    method test_DuskUTC (line 108) | def test_DuskUTC(self, london: Location):
    method test_Sunset (line 114) | def test_Sunset(self, london: Location):
    method test_SunsetUTC (line 120) | def test_SunsetUTC(self, london: Location):
    method test_SolarElevation (line 126) | def test_SolarElevation(self, riyadh: Location):
    method test_SolarAzimuth (line 131) | def test_SolarAzimuth(self, riyadh: Location):
    method test_TimeAtAltitude (line 136) | def test_TimeAtAltitude(self, new_delhi: Location):
    method test_SolarDepression (line 144) | def test_SolarDepression(self):
    method test_BadSolarDepression (line 154) | def test_BadSolarDepression(self):
    method test_Moon (line 159) | def test_Moon(self):
    method test_MoonNoDate (line 165) | def test_MoonNoDate(self):
    method test_TzError (line 169) | def test_TzError(self):
    method test_Equality (line 174) | def test_Equality(self):
    method test_LocationEquality_NotEqual (line 179) | def test_LocationEquality_NotEqual(self, london_info: LocationInfo):
    method test_LocationEquality_NotALocation (line 186) | def test_LocationEquality_NotALocation(self, london_info: LocationInfo):
    method test_SetLatitudeFloat (line 194) | def test_SetLatitudeFloat(self):
    method test_SetLatitudeString (line 199) | def test_SetLatitudeString(self):
    method test_SetLongitudeFloat (line 205) | def test_SetLongitudeFloat(self):
    method test_SetLongitudeString (line 210) | def test_SetLongitudeString(self):
    method test_SetBadLongitudeString (line 216) | def test_SetBadLongitudeString(self):
    method test_BadTzinfo (line 221) | def test_BadTzinfo(self):

FILE: src/test/test_Repr.py
  class TestLocationRepr (line 6) | class TestLocationRepr:
    method test_default (line 7) | def test_default(self):
    method test_full (line 14) | def test_full(self):
    method test_no_region (line 23) | def test_no_region(self):

FILE: src/test/test_all.py
  function test_AllLocations (line 5) | def test_AllLocations(test_database: LocationDatabase):

FILE: src/test/test_almost_equal.py
  class TestDateTimeAlmostEqual (line 6) | class TestDateTimeAlmostEqual:
    method test_equal (line 9) | def test_equal(self):
    method test_not_equal (line 15) | def test_not_equal(self):
    method test_equal_with_delta (line 21) | def test_equal_with_delta(self):

FILE: src/test/test_buenos_aries.py
  function test_BuenosAries (line 6) | def test_BuenosAries(test_database: LocationDatabase):

FILE: src/test/test_depression_not_reached.py
  function test_Dawn_NeverReachesDepression (line 11) | def test_Dawn_NeverReachesDepression():

FILE: src/test/test_geocoder.py
  function location_count (line 17) | def location_count(name: str, locations: List[LocationInfo]):
  function db_location_count (line 21) | def db_location_count(db: LocationDatabase) -> int:  # type: ignore
  class TestDatabase (line 26) | class TestDatabase:
    method test_all_locations (line 29) | def test_all_locations(self, test_database: astral.geocoder.LocationDa...
    method test_lookup (line 38) | def test_lookup(self, test_database: astral.geocoder.LocationDatabase):
    method test_city_in_db (line 49) | def test_city_in_db(self, test_database: astral.geocoder.LocationDatab...
    method test_group_in_db (line 52) | def test_group_in_db(self, test_database: astral.geocoder.LocationData...
    method test_location_not_in_db (line 55) | def test_location_not_in_db(self, test_database: astral.geocoder.Locat...
    method test_group_not_in_db (line 59) | def test_group_not_in_db(self, test_database: astral.geocoder.Location...
    method test_lookup_city_and_region (line 63) | def test_lookup_city_and_region(
    method test_country_with_multiple_entries_no_country (line 73) | def test_country_with_multiple_entries_no_country(
    method test_country_with_multiple_entries_with_country (line 80) | def test_country_with_multiple_entries_with_country(
  class TestBugReports (line 94) | class TestBugReports:
    method test_Adelaide (line 97) | def test_Adelaide(self, test_database: astral.geocoder.LocationDatabase):
    method test_CandianCities (line 102) | def test_CandianCities(self, test_database: astral.geocoder.LocationDa...
  class TestDatabaseAddLocations (line 106) | class TestDatabaseAddLocations:
    method test_newline_at_end (line 109) | def test_newline_at_end(self, test_database: astral.geocoder.LocationD...
    method test_from_list_of_strings (line 116) | def test_from_list_of_strings(
    method test_from_list_of_lists (line 129) | def test_from_list_of_lists(self, test_database: astral.geocoder.Locat...
  function test_SanitizeKey (line 148) | def test_SanitizeKey():

FILE: src/test/test_julian.py
  function test_JulianDay (line 45) | def test_JulianDay(day: Union[datetime.date, datetime.datetime], jd: flo...
  function test_JulianDay_JulianCalendar (line 56) | def test_JulianDay_JulianCalendar(
  function test_JulianDay_ToDateTime (line 69) | def test_JulianDay_ToDateTime(jd: float, dt: datetime.datetime):
  function test_JulianCentury (line 83) | def test_JulianCentury(jd: float, jc: float):
  function test_JulianCenturyToJulianDay (line 97) | def test_JulianCenturyToJulianDay(jc: float, jd: float):

FILE: src/test/test_location_info.py
  class TestLocationInfo (line 12) | class TestLocationInfo:
    method test_Default (line 13) | def test_Default(self):
    method test_bad_latitude (line 21) | def test_bad_latitude(self):
    method test_bad_longitude (line 25) | def test_bad_longitude(self):
    method test_timezone_group (line 29) | def test_timezone_group(self):
    method test_tzinfo (line 33) | def test_tzinfo(self, new_delhi_info: LocationInfo):

FILE: src/test/test_misc.py
  function test_MinutesToTimedelta (line 16) | def test_MinutesToTimedelta():
  class TestDMS (line 23) | class TestDMS:
    method test_north (line 26) | def test_north(self):
    method test_whole_number_of_degrees (line 29) | def test_whole_number_of_degrees(self):
    method test_east (line 32) | def test_east(self):
    method test_south (line 35) | def test_south(self):
    method test_west (line 38) | def test_west(self):
    method test_west_lowercase (line 41) | def test_west_lowercase(self):
    method test_float (line 44) | def test_float(self):
    method test_not_a_float (line 47) | def test_not_a_float(self):
    method test_latlng_outside_limit (line 51) | def test_latlng_outside_limit(self):
  class TestToday (line 55) | class TestToday:
    method test_default_timezone (line 57) | def test_default_timezone(self):
    method test_australia (line 64) | def test_australia(self):
    method test_adak (line 68) | def test_adak(self):
  class TestNow (line 72) | class TestNow:
    method test_default_timezone (line 74) | def test_default_timezone(self):
    method test_australia (line 81) | def test_australia(self):

FILE: src/test/test_norway.py
  function _next_event (line 10) | def _next_event(obs: astral.Observer, dt: datetime, event: str):
  function test_NorwaySunUp (line 21) | def test_NorwaySunUp(tromso: Location):

FILE: src/test/test_observer.py
  class TestObserver (line 7) | class TestObserver:
    method test_default (line 8) | def test_default(self):
    method test_from_float (line 14) | def test_from_float(self):
    method test_from_string (line 20) | def test_from_string(self):
    method test_from_dms (line 26) | def test_from_dms(self):
    method test_bad_latitude (line 32) | def test_bad_latitude(self):
    method test_bad_longitude (line 36) | def test_bad_longitude(self):
    method test_bad_elevation (line 40) | def test_bad_elevation(self):
    method test_latitude_outside_limits (line 44) | def test_latitude_outside_limits(self):
    method test_longitude_outside_limits (line 50) | def test_longitude_outside_limits(self):

FILE: src/test/test_sun_calc.py
  function test_GeomMeanLongSun (line 18) | def test_GeomMeanLongSun(jc: float, gmls: float):
  function test_GeomAnomolyLongSun (line 30) | def test_GeomAnomolyLongSun(jc: float, gmas: float):
  function test_EccentricityEarthOrbit (line 42) | def test_EccentricityEarthOrbit(jc: float, eeo: float):
  function test_SunEqOfCenter (line 56) | def test_SunEqOfCenter(jc: float, eos: float):
  function test_SunTrueLong (line 68) | def test_SunTrueLong(jc: float, stl: float):
  function test_SunTrueAnomaly (line 80) | def test_SunTrueAnomaly(jc: float, sta: float):
  function test_SunRadVector (line 92) | def test_SunRadVector(jc: float, srv: float):
  function test_SunApparentLong (line 104) | def test_SunApparentLong(jc: float, sal: float):
  function test_MeanObliquityOfEcliptic (line 116) | def test_MeanObliquityOfEcliptic(jc: float, mooe: float):
  function test_ObliquityCorrection (line 128) | def test_ObliquityCorrection(jc: float, oc: float):
  function test_SunRtAscension (line 140) | def test_SunRtAscension(jc: float, sra: float):
  function test_SunDeclination (line 152) | def test_SunDeclination(jc: float, sd: float):
  function test_EquationOfTime (line 164) | def test_EquationOfTime(jc: float, eot: float):
  function test_HourAngle (line 176) | def test_HourAngle(d: datetime.date, ha: float, london: Location):
  function test_Azimuth (line 189) | def test_Azimuth(new_delhi: Location):
  function test_Elevation (line 196) | def test_Elevation(new_delhi: Location):
  function test_Elevation_NonNaive (line 203) | def test_Elevation_NonNaive(new_delhi: Location):
  function test_Elevation_WithoutRefraction (line 210) | def test_Elevation_WithoutRefraction(new_delhi: Location):
  function test_Azimuth_Above85Degrees (line 219) | def test_Azimuth_Above85Degrees():
  function test_Elevation_Above85Degrees (line 226) | def test_Elevation_Above85Degrees():
  function test_ElevationEqualsTimeAtElevation (line 235) | def test_ElevationEqualsTimeAtElevation(elevation: float, london: Locati...

FILE: src/test/test_sun_elevation_adjustment.py
  class TestElevationAdjustment (line 7) | class TestElevationAdjustment:
    method test_Float_Positive (line 8) | def test_Float_Positive(self):
    method test_Float_Negative (line 12) | def test_Float_Negative(self):
    method test_Tuple_0 (line 16) | def test_Tuple_0(self):
    method test_Tuple_45deg (line 20) | def test_Tuple_45deg(self):
    method test_Tuple_30deg (line 24) | def test_Tuple_30deg(self):
    method test_Tuple_neg45deg (line 28) | def test_Tuple_neg45deg(self):

FILE: src/test/test_sun_golden_blue.py
  class TestGoldenHour (line 15) | class TestGoldenHour:
    method test_morning (line 37) | def test_morning(
    method test_evening (line 51) | def test_evening(self, london: Location):
    method test_no_date (line 72) | def test_no_date(self, new_delhi: Location):
  class TestBlueHour (line 80) | class TestBlueHour:
    method test_morning (line 83) | def test_morning(self, london: Location):
    method test_evening (line 99) | def test_evening(self, london: Location):
    method test_no_date (line 116) | def test_no_date(self, london: Location):

FILE: src/test/test_sun_local.py
  function test_Sun_Local_tzinfo (line 20) | def test_Sun_Local_tzinfo(
  function test_Sun_Local_str (line 38) | def test_Sun_Local_str(

FILE: src/test/test_sun_utc.py
  function test_Sun (line 25) | def test_Sun(day: datetime.date, dawn: datetime.datetime, london: Locati...
  function test_Sun_NoDate (line 32) | def test_Sun_NoDate(london: LocationInfo):
  function test_Dawn_Civil (line 47) | def test_Dawn_Civil(day: datetime.date, dawn: datetime.datetime, london:...
  function test_Dawn_NoDate (line 54) | def test_Dawn_NoDate(london: LocationInfo):
  function test_Dawn_Nautical (line 69) | def test_Dawn_Nautical(
  function test_Dawn_Astronomical (line 87) | def test_Dawn_Astronomical(
  function test_Sunrise (line 106) | def test_Sunrise(day: datetime.date, sunrise: datetime.datetime, london:...
  function test_Sunrise_NoDate (line 113) | def test_Sunrise_NoDate(london: LocationInfo):
  function test_Sunset (line 130) | def test_Sunset(day: datetime.date, sunset: datetime.datetime, london: L...
  function test_Sunset_NoDate (line 137) | def test_Sunset_NoDate(london: LocationInfo):
  function test_Dusk_Civil (line 153) | def test_Dusk_Civil(day: datetime.date, dusk: datetime.datetime, london:...
  function test_Dusk_NoDate (line 160) | def test_Dusk_NoDate(london: LocationInfo):
  function test_Dusk_Nautical (line 176) | def test_Dusk_Nautical(
  function test_SolarNoon (line 194) | def test_SolarNoon(day: datetime.date, noon: datetime.datetime, london: ...
  function test_SolarNoon_NoDate (line 201) | def test_SolarNoon_NoDate(london: LocationInfo):
  function test_SolarMidnight (line 214) | def test_SolarMidnight(
  function test_SolarMidnight_NoDate (line 223) | def test_SolarMidnight_NoDate(london: LocationInfo):
  function test_Twilight_SunRising (line 241) | def test_Twilight_SunRising(
  function test_Twilight_SunSetting (line 269) | def test_Twilight_SunSetting(
  function test_Twilight_NoDate (line 284) | def test_Twilight_NoDate(london: LocationInfo):
  function test_Rahukaalam (line 312) | def test_Rahukaalam(day: datetime.date, rahu: TimePeriod, new_delhi: Loc...
  function test_Rahukaalam_NoDate (line 325) | def test_Rahukaalam_NoDate(new_delhi: LocationInfo):
  function test_SolarAltitude (line 340) | def test_SolarAltitude(dt: datetime.datetime, angle: float, london: Loca...
  function test_SolarAltitude_NoDate (line 346) | def test_SolarAltitude_NoDate(london: LocationInfo):
  function test_SolarAzimuth (line 358) | def test_SolarAzimuth(dt: datetime.datetime, angle: float, london: Locat...
  function test_SolarAzimuth_NoDate (line 364) | def test_SolarAzimuth_NoDate(london: LocationInfo):
  function test_SolarZenith_London (line 380) | def test_SolarZenith_London(dt: datetime.datetime, angle: float, london:...
  function test_SolarZenith_Riyadh (line 393) | def test_SolarZenith_Riyadh(dt: datetime.datetime, angle: float, riyadh:...
  function test_SolarZenith_NoDate (line 400) | def test_SolarZenith_NoDate(london: LocationInfo):
  function test_TimeAtElevation_SunRising (line 405) | def test_TimeAtElevation_SunRising(london: LocationInfo):
  function test_TimeAtElevation_NoDate (line 414) | def test_TimeAtElevation_NoDate(london: LocationInfo):
  function test_TimeAtElevation_SunSetting (line 421) | def test_TimeAtElevation_SunSetting(london: LocationInfo):
  function test_TimeAtElevation_GreaterThan90 (line 428) | def test_TimeAtElevation_GreaterThan90(london: LocationInfo):
  function test_TimeAtElevation_GreaterThan180 (line 435) | def test_TimeAtElevation_GreaterThan180(london: LocationInfo):
  function test_TimeAtElevation_SunRisingBelowHorizon (line 442) | def test_TimeAtElevation_SunRisingBelowHorizon(london: LocationInfo):
  function test_TimeAtElevation_BadElevation (line 449) | def test_TimeAtElevation_BadElevation(london: LocationInfo):
  function test_Daylight (line 455) | def test_Daylight(london: LocationInfo):
  function test_Daylight_NoDate (line 465) | def test_Daylight_NoDate(london: LocationInfo):
  function test_Nighttime (line 474) | def test_Nighttime(london: LocationInfo):
  function test_Nighttime_NoDate (line 484) | def test_Nighttime_NoDate(london: LocationInfo):

FILE: src/test/test_value_error_bug.py
  function test_value_error_bug (line 7) | def test_value_error_bug():

FILE: src/test/test_wellington.py
  function test_Wellington (line 9) | def test_Wellington(wellington: Location):
Condensed preview — 57 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (304K chars).
[
  {
    "path": ".editorconfig",
    "chars": 240,
    "preview": "[*]\ncharset = utf-8\nend_of_line = lf\nindent_style = space\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n\n["
  },
  {
    "path": ".gitattributes",
    "chars": 0,
    "preview": ""
  },
  {
    "path": ".github/workflows/astral-test.yml",
    "chars": 1174,
    "preview": "name: astral-test\n\non:\n  push:\n    branches: [\"master\", \"develop\"]\n  pull_request:\n    branches: [\"master\"]\n\npermissions"
  },
  {
    "path": ".gitignore",
    "chars": 861,
    "preview": ".api_key\n.venv\nsetup.cfg\nsetup-dev.cfg\n.tox\n_version.py\n.coveragerc\n.coveralls.yml\n.atom-build.yaml\n.atom-build.yml\npoet"
  },
  {
    "path": ".pre-commit-config.yaml",
    "chars": 540,
    "preview": "repos:\n  - repo: https://github.com/pycqa/flake8\n    rev: 7.0.0\n    hooks:\n      - id: flake8\n        additional_depende"
  },
  {
    "path": ".readthedocs.yaml",
    "chars": 357,
    "preview": "# .readthedocs.yaml\n# Read the Docs configuration file\n# See https://docs.readthedocs.io/en/stable/config-file/v2.html f"
  },
  {
    "path": "AUTHORS",
    "chars": 109,
    "preview": "Alton Campbell\nMichael Overmeyer\nPascal Bach\nSimon Kennedy\nWojciech Pietruszeski\nZachary Priddy\nMichael Marx\n"
  },
  {
    "path": "ChangeLog.md",
    "chars": 3216,
    "preview": "# CHANGELOG\n\n## 3.2 2022-11-05\n\n### Changed\n\n- Removed support for Python 3.6 as it has reached \"End of Life\"\n\n- Documen"
  },
  {
    "path": "LICENSE",
    "chars": 11357,
    "preview": "                                 Apache License\n                           Version 2.0, January 2004\n                   "
  },
  {
    "path": "ReadMe.md",
    "chars": 506,
    "preview": "# Astral\n\nThis is 'astral' a Python module which calculates\n\n- Times for various positions of the sun: dawn, sunrise, so"
  },
  {
    "path": "flake.nix",
    "chars": 1377,
    "preview": "{\n  description = \"Astral - Calculations for the sun and moon.\";\n\n  inputs.pyproject-nix.url = \"github:nix-community/pyp"
  },
  {
    "path": "pyproject.toml",
    "chars": 1347,
    "preview": "[project]\nname = \"astral\"\nversion = \"3.3\"\ndescription = \"Calculations for the sun and moon.\"\nauthors = [{ name = \"Simon "
  },
  {
    "path": "src/astral/__init__.py",
    "chars": 9060,
    "preview": "# -*- coding: utf-8 -*-\n\n# Copyright 2009-2021, Simon Kennedy, sffjunkie+code@gmail.com\n\n#   Licensed under the Apache L"
  },
  {
    "path": "src/astral/__main__.py",
    "chars": 1846,
    "preview": "import argparse\nimport datetime\nimport json\nfrom typing import Any, Dict\n\nfrom astral import LocationInfo, Observer, sun"
  },
  {
    "path": "src/astral/geocoder.py",
    "chars": 25360,
    "preview": "\"\"\"Astral geocoder is a database of locations stored within the package.\n\nTo get the :class:`~astral.LocationInfo` for a"
  },
  {
    "path": "src/astral/julian.py",
    "chars": 3300,
    "preview": "import datetime\nfrom enum import Enum\nfrom typing import Union\n\n\nclass Calendar(Enum):\n    GREGORIAN = 1\n    JULIAN = 2\n"
  },
  {
    "path": "src/astral/location.py",
    "chars": 31301,
    "preview": "import dataclasses\nimport datetime\n\ntry:\n    import zoneinfo\nexcept ImportError:\n    from backports import zoneinfo  # t"
  },
  {
    "path": "src/astral/moon.py",
    "chars": 18324,
    "preview": "\"\"\"Moon phase, rise and set times\n\nRight ascension, declination and distance of moon calcaulation\nfrom\n\nLOW-PRECISION FO"
  },
  {
    "path": "src/astral/py.typed",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "src/astral/sidereal.py",
    "chars": 748,
    "preview": "import datetime\nfrom typing import Union\n\nfrom astral.julian import julianday_2000\n\nDegrees = float\n\n\ndef gmst(at: Union"
  },
  {
    "path": "src/astral/sun.py",
    "chars": 37709,
    "preview": "import datetime\nfrom math import acos, asin, atan2, cos, degrees, fabs, radians, sin, sqrt, tan\nfrom typing import Dict,"
  },
  {
    "path": "src/astral/table4.py",
    "chars": 15715,
    "preview": "from math import cos, sin\nfrom typing import Callable, Dict, List, NamedTuple\n\n\nclass Table4Row(NamedTuple):\n    coeffic"
  },
  {
    "path": "src/docs/Makefile",
    "chars": 6819,
    "preview": "# Makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS    = -d ../../art"
  },
  {
    "path": "src/docs/SConstruct",
    "chars": 9432,
    "preview": "\"\"\"\nThis is a generic SCons script for running Sphinx (http://sphinx.pocoo.org).\n\nType 'scons -h' for help.  This prints"
  },
  {
    "path": "src/docs/conf.py",
    "chars": 986,
    "preview": "# -*- coding: utf-8 -*-\nimport os\nimport sys\n\non_rtd = os.environ.get(\"READTHEDOCS\", None) == \"True\"\n\nproject = \"Astral\""
  },
  {
    "path": "src/docs/index.rst",
    "chars": 31774,
    "preview": ".. Copyright 2009-2021, Simon Kennedy, sffjunkie+code@gmail.com\n\n.. Licensed under the Apache License, Version 2.0 (the "
  },
  {
    "path": "src/docs/make.bat",
    "chars": 6506,
    "preview": "@ECHO OFF\n\nREM Command file for Sphinx documentation\n\nif \"%SPHINXBUILD%\" == \"\" (\n\tset SPHINXBUILD=sphinx-build\n)\nset BUI"
  },
  {
    "path": "src/docs/package.rst",
    "chars": 1002,
    "preview": ".. Copyright 2009-2021, Simon Kennedy, sffjunkie+code@gmail.com\n\n.. Licensed under the Apache License, Version 2.0 (the "
  },
  {
    "path": "src/docs/static/astral.css",
    "chars": 47,
    "preview": "img.adjustment {\r\n    margin: 1rem 1.5rem;\r\n}\r\n"
  },
  {
    "path": "src/test/almost_equal.py",
    "chars": 586,
    "preview": "import datetime\n\n\ndef datetime_almost_equal(\n    datetime1: datetime.datetime, datetime2: datetime.datetime, seconds: in"
  },
  {
    "path": "src/test/conftest.py",
    "chars": 1535,
    "preview": "import pytest  # type: ignore\n\nfrom astral import LocationInfo\nfrom astral.geocoder import LocationDatabase, database\nfr"
  },
  {
    "path": "src/test/moon/test_moon.py",
    "chars": 4785,
    "preview": "# -*- coding: utf-8 -*-\nimport datetime\n\nimport pytest  # type: ignore\nfrom almost_equal import datetime_almost_equal\n\nf"
  },
  {
    "path": "src/test/moon/test_moon_azimuth.py",
    "chars": 953,
    "preview": "import datetime\n\nimport pytest  # type: ignore\n\nfrom astral import Observer\nfrom astral.location import Location\nfrom as"
  },
  {
    "path": "src/test/moon/test_moon_position.py",
    "chars": 448,
    "preview": "from datetime import date\n\nfrom astral.moon import julianday, moon_position\n\n\ndef test_moon_position():\n    d = date(196"
  },
  {
    "path": "src/test/moon/test_moon_rise.py",
    "chars": 30,
    "preview": "def test_moon_rise():\n    ...\n"
  },
  {
    "path": "src/test/moon/test_sidereal_time.py",
    "chars": 806,
    "preview": "import datetime\n\nfrom astral import hours_to_time\nfrom astral.sidereal import gmst, lmst\n\n\ndef test_gmst():\n    dt = dat"
  },
  {
    "path": "src/test/test_Location.py",
    "chars": 8490,
    "preview": "# -*- coding: utf-8 -*-\nimport dataclasses\nimport datetime\n\ntry:\n    import zoneinfo\nexcept ImportError:\n    from backpo"
  },
  {
    "path": "src/test/test_Repr.py",
    "chars": 910,
    "preview": "# -*- coding: utf-8 -*-\nfrom astral import LocationInfo\nfrom astral.location import Location\n\n\nclass TestLocationRepr:\n "
  },
  {
    "path": "src/test/test_all.py",
    "chars": 235,
    "preview": "from astral.geocoder import LocationDatabase, all_locations\r\nfrom astral.sun import noon\r\n\r\n\r\ndef test_AllLocations(test"
  },
  {
    "path": "src/test/test_almost_equal.py",
    "chars": 676,
    "preview": "import datetime\n\nfrom almost_equal import datetime_almost_equal\n\n\nclass TestDateTimeAlmostEqual:\n    \"\"\"Test the datetim"
  },
  {
    "path": "src/test/test_buenos_aries.py",
    "chars": 308,
    "preview": "# -*- coding: utf-8 -*-\nfrom astral.geocoder import LocationDatabase, lookup\nfrom astral.location import LocationInfo\n\n\n"
  },
  {
    "path": "src/test/test_depression_not_reached.py",
    "chars": 555,
    "preview": "# -*- coding: utf-8 -*-\n\nimport datetime\n\nimport pytest  # type: ignore\n\nfrom astral import LocationInfo\nfrom astral.loc"
  },
  {
    "path": "src/test/test_geocoder.py",
    "chars": 5526,
    "preview": "# -*- coding: utf-8 -*-\nfrom functools import reduce\nfrom typing import List\n\ntry:\n    import zoneinfo\nexcept ImportErro"
  },
  {
    "path": "src/test/test_julian.py",
    "chars": 3030,
    "preview": "# type: ignore\nimport datetime\nfrom typing import Union\n\nimport pytest\nfrom almost_equal import datetime_almost_equal\n\nf"
  },
  {
    "path": "src/test/test_location_info.py",
    "chars": 1042,
    "preview": "# type: ignore\nimport pytest\n\nfrom astral import LocationInfo\n\ntry:\n    import zoneinfo\nexcept ImportError:\n    from bac"
  },
  {
    "path": "src/test/test_misc.py",
    "chars": 2497,
    "preview": "# type: ignore\nfrom datetime import timedelta\n\ntry:\n    import zoneinfo\nexcept ImportError:\n    from backports import zo"
  },
  {
    "path": "src/test/test_norway.py",
    "chars": 1039,
    "preview": "from datetime import datetime, timedelta, timezone\n\nimport pytest  # type: ignore\n\nimport astral\nfrom astral import sun\n"
  },
  {
    "path": "src/test/test_observer.py",
    "chars": 1492,
    "preview": "# type: ignore\nimport pytest\n\nfrom astral import Observer\n\n\nclass TestObserver:\n    def test_default(self):\n        obs "
  },
  {
    "path": "src/test/test_sun_calc.py",
    "chars": 6411,
    "preview": "import datetime\n\nimport freezegun\nimport pytest  # type: ignore\n\nfrom astral import Observer, sun, today\nfrom astral.loc"
  },
  {
    "path": "src/test/test_sun_elevation_adjustment.py",
    "chars": 1030,
    "preview": "# -*- coding: utf-8 -*-\r\nimport pytest  # type: ignore\r\n\r\nfrom astral.sun import adjust_to_horizon, adjust_to_obscuring_"
  },
  {
    "path": "src/test/test_sun_golden_blue.py",
    "chars": 4409,
    "preview": "# -*- coding: utf-8 -*-\n# Test data taken from http://www.timeanddate.com/sun/uk/london\n\nimport datetime\n\nimport freezeg"
  },
  {
    "path": "src/test/test_sun_local.py",
    "chars": 1602,
    "preview": "import datetime\n\nimport pytest  # type: ignore\nfrom almost_equal import datetime_almost_equal\n\nfrom astral import sun\nfr"
  },
  {
    "path": "src/test/test_sun_utc.py",
    "chars": 18390,
    "preview": "# type: ignore\n# Test data taken from http://www.timeanddate.com/sun/uk/london\n\nimport datetime\nfrom typing import Tuple"
  },
  {
    "path": "src/test/test_value_error_bug.py",
    "chars": 410,
    "preview": "import datetime\n\nimport astral\nimport astral.sun\n\n\ndef test_value_error_bug():\n    loc = astral.LocationInfo(\n        na"
  },
  {
    "path": "src/test/test_wellington.py",
    "chars": 570,
    "preview": "import datetime\r\n\r\nfrom almost_equal import datetime_almost_equal\r\n\r\nfrom astral.location import Location\r\nfrom astral.s"
  },
  {
    "path": "tox.ini",
    "chars": 296,
    "preview": "[tox]\nproject = astral\nenvlist = py3\nisolated_build = True\n\n[testenv]\ndeps =\n    freezegun\n    pytest\n    pytest-runner\n"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the sffjunkie/astral GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 57 files (282.3 KB), approximately 86.8k tokens, and a symbol index with 353 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!