[
  {
    "path": ".editorconfig",
    "content": "[*]\ncharset = utf-8\nend_of_line = lf\nindent_style = space\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n\n[*.py]\nindent_size = 4\n\n[*.rst]\nindent_size = 3\n\n[*.{json,yml}]\nindent_size = 2\n\n[.api_key]\ninsert_final_newline = false\n"
  },
  {
    "path": ".gitattributes",
    "content": ""
  },
  {
    "path": ".github/workflows/astral-test.yml",
    "content": "name: astral-test\n\non:\n  push:\n    branches: [\"master\", \"develop\"]\n  pull_request:\n    branches: [\"master\"]\n\npermissions:\n  contents: read\n\njobs:\n  tests:\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n        python-version: [\"3.8\", \"3.9\", \"3.10\", \"3.11\", \"3.12\"]\n\n    steps:\n      - uses: actions/checkout@v4\n      - name: Set up Python\n        uses: actions/setup-python@v5\n        with:\n          python-version: ${{ matrix.python-version }}\n      - name: Install dependencies\n        run: |\n          python -m pip install --upgrade pip\n          python -m pip install pdm\n      - name: Install dependencies using pdm\n        run: |\n          pdm install\n      - name: Lint with flake8\n        run: |\n          # stop the build if there are Python syntax errors or undefined names\n          pdm run flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --exclude=.venv\n          # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide\n          pdm run flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics --exclude=.venv\n      - name: Test with pytest\n        run: |\n          pdm run pytest\n"
  },
  {
    "path": ".gitignore",
    "content": ".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\npoetry.lock\npoetry.toml\n.python-version\nartifact\n.mypy_cache\n.pytest_cache\n.pdm-python\n.pdm-build\n.direnv\n.envrc\njust\nJustfile\n\n# Numerous always-ignore extensions\n*.diff\n*.err\n*.orig\n*.log\n*.py[cod]\n*.rej\n*.swo\n*.swp\n*.vi\n*~\n*.sass-cache\n*.egg-info\n\n# OS or Editor folders\n.DS_Store\nThumbs.db\n.cache\n.coverage\n.project\n.pydevproject\n.settings\n.tmproj\n*.esproj\nnbproject\n*.sublime-project\n*.sublime-workspace\n*.iml\n.vscode\n.pytest_cache\n.mypy_cache\n.venv\n\n# Dreamweaver added files\n_notes\ndwsync.xml\n\n# Komodo\n*.komodoproject\n.komodotools\n\n# Folders to ignore\n.bzr\n.hg\n.svn\n.eggs\n.CVS\nintermediate\npublish\n.idea\n__pycache__\nbuild\ndist\nMANIFEST\n\n# build script local files\nbuild/buildinfo.properties\nbuild/config/buildinfo.properties\n.pdm-python\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "repos:\n  - repo: https://github.com/pycqa/flake8\n    rev: 7.0.0\n    hooks:\n      - id: flake8\n        additional_dependencies: [flake8-pyproject]\n  - repo: https://github.com/pycqa/isort\n    rev: 5.13.2\n    hooks:\n      - id: isort\n        args: [\"--profile\", \"black\", \"--filter-files\"]\n  - repo: https://github.com/psf/black\n    rev: 24.2.0\n    hooks:\n      - id: black\n  - repo: https://github.com/pre-commit/pre-commit-hooks\n    rev: v4.5.0\n    hooks:\n      - id: check-yaml\n      - id: end-of-file-fixer\n      - id: trailing-whitespace\n"
  },
  {
    "path": ".readthedocs.yaml",
    "content": "# .readthedocs.yaml\n# Read the Docs configuration file\n# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details\n\nversion: 2\n\n# Set the version of Python and other tools you might need\nbuild:\n  os: ubuntu-20.04\n  tools:\n    python: \"3.9\"\n\n# Build documentation in the docs/ directory with Sphinx\nsphinx:\n   configuration: src/docs/conf.py\n"
  },
  {
    "path": "AUTHORS",
    "content": "Alton Campbell\nMichael Overmeyer\nPascal Bach\nSimon Kennedy\nWojciech Pietruszeski\nZachary Priddy\nMichael Marx\n"
  },
  {
    "path": "ChangeLog.md",
    "content": "# 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- Documentation now hosted on [Github Pages](https://sffjunkie.github.io/astral/)\n\n## 3.1 2022-11-01\n\n### Bug Fix\n\n- Fix for issue [#77](https://github.com/sffjunkie/astral/issues/77)\n\n## 3.0 2022-10-07\n\n### Added\n\n- Added support for moon rise and set times and azimuth / zentih calculations.\n\n- Dropped dependency on  `pytz` and switched to using `zoneinfo` provided as\n  part of Python 3.9 or the `backports.zoneinfo` package.\n\n## 2.2 - 2020-05-20\n\n### Changed\n\n- 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 α).\n- The sun functions can now also be passed the timezone as a string. Previously only a pytz timezone was accepted.\n\n## 2.1 - 2020-02-12\n\n### Bug Fix\n\n- 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.\n\n## 2.0 - 2020-02-11\n\n### Refactor\n\n- 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.\n- Astral, AstralGeocoder & GoogleGeocoder classes removed\n- Requires python 3.6+ due to the use of dataclasses\n- New LocationInfo class to store a location name, region, timezone, latitude & longitude\n- New Observer class to store a latitude, longitude & elevation\n- Geocoder database now returns a LocationInfo instead of a Location\n\n## 1.10.1 - 2019-02-06\n\n### Changed\n\nKeywords arguments to Astral **init** are now passed to the geocoder to allow for passing\nthe `api_key` to GoogleGeocoder.\n\n## 1.10 - 2019-02-04\n\n### Added\n\nAdded method to AstralGeocoder to add locations to the database\n\n## 1.9.2 - 2019-01-31\n\n### Changed\n\nVersion 1.9 broke the sun_utc method. Sun UTC calculation passed incorrect\nparameter to more specific methods e.g. sunrise, sunset etc.\n\n## 1.9.1 - 2019-01-28\n\n### Changed\n\nCorrected version number in module source code.\n\n## 1.9 - 2019-01-28\n\n### Added\n\nSun calculations now take into account the elevation of the location.\n\n## 1.8 - 2018-12-06\n\n### Added\n\nAdded command line interface to return sun information as json.\nAdded support for no timezone in Location methods.\n\n## 1.7.1 - 2018-10-25\n\n### Changed\n\nChanged GoogleGeocoder test to not use raise...from as this is not valid for Python 2\n\n## 1.7 - 2018-10-24\n\n### Changed\n\n- Requests is now only needed when using GoogleGeocoder\n- GoogleGeocoder now requires the `api_key` parameter to be passed to the constructor\n\n## 1.6.1 - 2018-05-02\n\n### Changed\n\n- Updated Travis CI configuration\n\n### Added\n\n- requirements-dev.txt\n\n## 1.6 - 2018-02-22\n\n### Changed\n\n- Added api_key parameter to GoogleGeocoder **init** method. Idea from\n    wpietruszewski <https://github.com/sffjunkie/astral/pull/12>\n\n## 1.5 - 2017-12-07\n\n### Added\n\n- this file\n\n### Changed\n\n- dawn_utc, sunrise_utc, sunset_utc and dusk_utc now only raise AstralError for a math domain\n    exception all other exceptions are passed through.\n- moon_phase now takes another parameter if the type to return either int (the default) or float\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "ReadMe.md",
    "content": "# Astral\n\nThis is 'astral' a Python module which calculates\n\n- Times for various positions of the sun: dawn, sunrise, solar noon,\nsunset, dusk, solar elevation, solar azimuth and rahukaalam.\n- Moon rise, set, azimuth and zenith.\n- The phase of the moon.\n\nFor documentation see https://sffjunkie.github.io/astral/\n\n## Package Status\n\n![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/sffjunkie/astral/astral-test.yml) ![PyPI - Downloads](https://img.shields.io/pypi/dm/astral)\n"
  },
  {
    "path": "flake.nix",
    "content": "{\n  description = \"Astral - Calculations for the sun and moon.\";\n\n  inputs.pyproject-nix.url = \"github:nix-community/pyproject.nix\";\n  inputs.pyproject-nix.inputs.nixpkgs.follows = \"nixpkgs\";\n\n  nixConfig = {\n    bash-prompt = ''\\n\\[\\033[1;34m\\][\\[\\e]0;\\u@\\h: \\w\\a\\]\\u@\\h:\\w]\\\\$\\[\\033[0m\\] '';\n  };\n\n  outputs = {\n    nixpkgs,\n    pyproject-nix,\n    ...\n  }: let\n    inherit (nixpkgs) lib;\n\n    project = pyproject-nix.lib.project.loadPyproject {\n      projectRoot = ./.;\n    };\n\n    forAllSystems = function:\n      nixpkgs.lib.genAttrs [\n        \"aarch64-linux\"\n        \"x86_64-darwin\"\n        \"x86_64-linux\"\n      ] (system: function nixpkgs.legacyPackages.${system});\n  in {\n    devShells = forAllSystems (pkgs: (\n      let\n        python = pkgs.python3;\n        arg = project.renderers.withPackages {inherit python;};\n        pythonEnv = python.withPackages arg;\n      in {\n        default = pkgs.mkShell {\n          packages = [\n            pkgs.pdm\n            pkgs.ruff\n            pythonEnv\n          ];\n          shellHook = ''\n            export PYTHONPATH=${builtins.toString ./src}\n          '';\n        };\n      }\n    ));\n\n    packages = forAllSystems (pkgs: (\n      let\n        python = pkgs.python3;\n        attrs = project.renderers.buildPythonPackage {inherit python;};\n      in {\n        default = python.pkgs.buildPythonPackage attrs;\n      }\n    ));\n  };\n}\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[project]\nname = \"astral\"\nversion = \"3.3\"\ndescription = \"Calculations for the sun and moon.\"\nauthors = [{ name = \"Simon Kennedy\", email = \"sffjunkie+code@gmail.com\" }]\ndependencies = [\"tzdata>=2024.1; sys_platform == 'win32'\"]\nrequires-python = \">=3.10\"\nreadme = \"ReadMe.md\"\nlicense = { text = \"Apache-2.0\" }\n\nclassifiers = [\n    \"Intended Audience :: Developers\",\n    \"Programming Language :: Python\",\n    \"Programming Language :: Python :: 3.7\",\n    \"Programming Language :: Python :: 3.8\",\n    \"Programming Language :: Python :: 3.9\",\n    \"Programming Language :: Python :: 3.10\",\n]\n\n[project.urls]\nHomepage = \"https://github.com/sffjunkie/astral\"\nIssues = \"https://github.com/sffjunkie/astral/issues\"\n\n[build-system]\nrequires = [\"pdm-backend\"]\nbuild-backend = \"pdm.backend\"\n\n[tool.pdm.dev-dependencies]\ndev = [\n    \"freezegun>=1.4.0\",\n    \"pytest>=8.1.1\",\n    \"mypy>=1.9.0\",\n    \"types-freezegun>=1.1.10\",\n    \"tox>=4.14.1\",\n    \"ruff>=0.3.2\",\n    \"pytest-cov>=4.1.0\",\n    \"flake8>=7.0.0\",\n]\ndocs = [\"sphinx-book-theme>=1.1.2\"]\n\n[tool.pdm.scripts]\ntest = \"pytest\"\ntypecheck = \"mypy ./src/astral/\"\n\n[tool.pytest.ini_options]\nmarkers = [\"unit\", \"integration\"]\npythonpath = [\"src\"]\njunit_family = \"xunit2\"\nnorecursedirs = [\n    \".direnv\",\n    \".venv\",\n    \".git\",\n    \".tox\",\n    \".cache\",\n    \".settings\",\n    \"dist\",\n    \"build\",\n    \"docs\",\n]\n"
  },
  {
    "path": "src/astral/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\n# Copyright 2009-2021, Simon Kennedy, sffjunkie+code@gmail.com\n\n#   Licensed under the Apache License, Version 2.0 (the \"License\");\n#   you may not use this file except in compliance with the License.\n#   You may obtain a copy of the License at\n#\n#       http://www.apache.org/licenses/LICENSE-2.0\n#\n#   Unless required by applicable law or agreed to in writing, software\n#   distributed under the License is distributed on an \"AS IS\" BASIS,\n#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n#   See the License for the specific language governing permissions and\n#   limitations under the License.\n\n\"\"\"Calculations for the position of the sun and moon.\n\nThe :mod:`astral` package provides the means to calculate the following times of the sun\n\n* dawn\n* sunrise\n* noon\n* midnight\n* sunset\n* dusk\n* daylight\n* night\n* twilight\n* blue hour\n* golden hour\n* rahukaalam\n* moon rise, set, azimuth and zenith\n\nplus solar azimuth and elevation at a specific latitude/longitude.\nIt can also calculate the moon phase for a specific date.\n\nThe package also provides a self contained geocoder to turn a small set of\nlocation names into timezone, latitude and longitude. The lookups\ncan be perfomed using the :func:`~astral.geocoder.lookup` function defined in\n:mod:`astral.geocoder`\n\"\"\"\n\nimport datetime\nimport re\nfrom dataclasses import dataclass, field\nfrom enum import Enum\nfrom math import radians, tan\nfrom typing import Optional, Tuple, Union\n\ntry:\n    import zoneinfo\nexcept ImportError:\n    from backports import zoneinfo  # type: ignore\n\n\n__all__ = [\n    \"Depression\",\n    \"SunDirection\",\n    \"Observer\",\n    \"LocationInfo\",\n    \"AstralBodyPosition\",\n    \"now\",\n    \"today\",\n    \"dms_to_float\",\n    \"refraction_at_zenith\",\n]\n\n__version__ = \"3.2\"\n__author__ = \"Simon Kennedy <sffjunkie+code@gmail.com>\"\n\n\nTimePeriod = Tuple[datetime.datetime, datetime.datetime]\nElevation = Union[float, Tuple[float, float]]\nDegrees = float\nRadians = float\nMinutes = float\n\n\ndef now(tz: Optional[datetime.tzinfo] = None) -> datetime.datetime:\n    \"\"\"Returns the current time in the specified time zone\"\"\"\n    now_utc = datetime.datetime.now(datetime.timezone.utc)\n    if tz is None:\n        return now_utc\n\n    return now_utc.astimezone(tz)\n\n\ndef today(tz: Optional[datetime.tzinfo] = None) -> datetime.date:\n    \"\"\"Returns the current date in the specified time zone\"\"\"\n    return now(tz).date()\n\n\ndef dms_to_float(\n    dms: Union[str, float, Elevation], limit: Optional[float] = None\n) -> float:\n    \"\"\"Converts as string of the form `degrees°minutes'seconds\"[N|S|E|W]`,\n    or a float encoded as a string, to a float\n\n    N and E return positive values\n    S and W return negative values\n\n    Args:\n        dms: string to convert\n        limit: Limit the value between ± `limit`\n\n    Returns:\n        The number of degrees as a float\n    \"\"\"\n\n    try:\n        res = float(dms)  # type: ignore\n    except (ValueError, TypeError) as exc:\n        _dms_re = r\"(?P<deg>\\d{1,3})[°]((?P<min>\\d{1,2})[′'])?((?P<sec>\\d{1,2})[″\\\"])?(?P<dir>[NSEW])?\"  # noqa\n        dms_match = re.match(_dms_re, str(dms), flags=re.IGNORECASE)\n        if dms_match:\n            deg = dms_match.group(\"deg\") or 0.0\n            min_ = dms_match.group(\"min\") or 0.0\n            sec = dms_match.group(\"sec\") or 0.0\n            dir_ = dms_match.group(\"dir\") or \"E\"\n\n            res = float(deg)\n            if min_:\n                res += float(min_) / 60\n            if sec:\n                res += float(sec) / 3600\n\n            if dir_.upper() in [\"S\", \"W\"]:\n                res = -res\n        else:\n            raise ValueError(\n                \"Unable to convert degrees/minutes/seconds to float\"\n            ) from exc\n\n    if limit is not None:\n        if res > limit:\n            res = limit\n        elif res < -limit:\n            res = -limit\n\n    return res\n\n\ndef hours_to_time(value: float) -> datetime.time:\n    \"\"\"Convert a floating point number of hours to a datetime.time\"\"\"\n\n    hour = int(value)\n    value -= hour\n    value *= 60\n    minute = int(value)\n    value -= minute\n    value *= 60\n    second = int(value)\n    value -= second\n    microsecond = int(value * 1000000)\n\n    return datetime.time(hour, minute, second, microsecond)\n\n\ndef time_to_hours(value: datetime.time) -> float:\n    \"\"\"Convert a datetime.time to a floating point number of hours\"\"\"\n\n    hours = 0.0\n    hours += value.hour\n    hours += value.minute / 60\n    hours += value.second / 3600\n    hours += value.microsecond / 1000000\n\n    return hours\n\n\ndef time_to_seconds(value: datetime.time) -> float:\n    \"\"\"Convert a datetime.time to a floating point number of seconds\"\"\"\n\n    hours = time_to_hours(value)\n    return hours * 3600\n\n\ndef refraction_at_zenith(zenith: float) -> float:\n    \"\"\"Calculate the degrees of refraction of the sun due to the sun's elevation.\"\"\"\n\n    elevation = 90 - zenith\n    if elevation >= 85.0:\n        return 0\n\n    refraction_correction = 0.0\n    te = tan(radians(elevation))\n    if elevation > 5.0:\n        refraction_correction = (\n            58.1 / te - 0.07 / (te * te * te) + 0.000086 / (te * te * te * te * te)\n        )\n    elif elevation > -0.575:\n        step1 = -12.79 + elevation * 0.711\n        step2 = 103.4 + elevation * step1\n        step3 = -518.2 + elevation * step2\n        refraction_correction = 1735.0 + elevation * step3\n    else:\n        refraction_correction = -20.774 / te\n\n    refraction_correction = refraction_correction / 3600.0\n\n    return refraction_correction\n\n\nclass Depression(Enum):\n    \"\"\"The depression angle in degrees for the dawn/dusk calculations\"\"\"\n\n    CIVIL = 6\n    NAUTICAL = 12\n    ASTRONOMICAL = 18\n\n\nclass SunDirection(Enum):\n    \"\"\"Direction of the sun either RISING or SETTING\"\"\"\n\n    RISING = 1\n    SETTING = -1\n\n\n@dataclass\nclass AstralBodyPosition:\n    \"\"\"The position of an astral body as seen from earth\"\"\"\n\n    right_ascension: Radians = field(default_factory=float)\n    declination: Radians = field(default_factory=float)\n    distance: Radians = field(default_factory=float)\n\n\n@dataclass\nclass Observer:\n    \"\"\"Defines the location of an observer on Earth.\n\n    Latitude and longitude can be set either as a float or as a string.\n    For strings they must be of the form\n\n        degrees°minutes'seconds\"[N|S|E|W] e.g. 51°31'N\n\n    `minutes’` & `seconds”` are optional.\n\n    Elevations are either\n\n    * A float that is the elevation in metres above a location, if the nearest\n      obscuring feature is the horizon\n    * or a tuple of the elevation in metres and the distance in metres to the\n      nearest obscuring feature.\n\n    Args:\n        latitude:   Latitude - Northern latitudes should be positive\n        longitude:  Longitude - Eastern longitudes should be positive\n        elevation:  Elevation and/or distance to nearest obscuring feature\n                    in metres above/below the location.\n    \"\"\"\n\n    latitude: Degrees = 51.4733\n    longitude: Degrees = -0.0008333\n    elevation: Elevation = 0.0\n\n    def __setattr__(self, name: str, value: Union[str, float, Elevation]):\n        if name == \"latitude\":\n            value = dms_to_float(value, 90.0)\n        elif name == \"longitude\":\n            value = dms_to_float(value, 180.0)\n        elif name == \"elevation\":\n            if isinstance(value, tuple):\n                value = (float(value[0]), float(value[1]))\n            else:\n                value = float(value)\n        super().__setattr__(name, value)\n\n\n@dataclass\nclass LocationInfo:\n    \"\"\"Defines a location on Earth.\n\n    Latitude and longitude can be set either as a float or as a string.\n    For strings they must be of the form\n\n        degrees°minutes'seconds\"[N|S|E|W] e.g. 51°31'N\n\n    `minutes’` & `seconds”` are optional.\n\n    Args:\n        name:       Location name (can be any string)\n        region:     Region location is in (can be any string)\n        timezone:   The location's time zone (a list of time zone names can be\n                    obtained from `zoneinfo.available_timezones`)\n        latitude:   Latitude - Northern latitudes should be positive\n        longitude:  Longitude - Eastern longitudes should be positive\n    \"\"\"\n\n    name: str = \"Greenwich\"\n    region: str = \"England\"\n    timezone: str = \"Europe/London\"\n    latitude: Degrees = 51.4733\n    longitude: Degrees = -0.0008333\n\n    def __setattr__(self, name: str, value: Union[Degrees, str]):\n        if name == \"latitude\":\n            value = dms_to_float(value, 90.0)\n        elif name == \"longitude\":\n            value = dms_to_float(value, 180.0)\n        super().__setattr__(name, value)\n\n    @property\n    def observer(self):\n        \"\"\"Return an Observer at this location\"\"\"\n        return Observer(self.latitude, self.longitude, 0.0)\n\n    @property\n    def tzinfo(self):  # type: ignore\n        \"\"\"Return a zoneinfo.ZoneInfo for this location\"\"\"\n        return zoneinfo.ZoneInfo(self.timezone)  # type: ignore\n\n    @property\n    def timezone_group(self):\n        \"\"\"Return the group a timezone is in\"\"\"\n        return self.timezone.split(\"/\", maxsplit=1)[0]\n"
  },
  {
    "path": "src/astral/__main__.py",
    "content": "import argparse\nimport datetime\nimport json\nfrom typing import Any, Dict\n\nfrom astral import LocationInfo, Observer, sun\n\ntry:\n    import zoneinfo\nexcept ImportError:\n    from backports import zoneinfo  # type: ignore\n\noptions = argparse.ArgumentParser()\noptions.add_argument(\n    \"-n\",\n    \"--name\",\n    dest=\"name\",\n    default=\"Somewhere\",\n    help=\"Location name (free-form text)\",\n)\noptions.add_argument(\n    \"-r\", \"--region\", dest=\"region\", default=\"On Earth\", help=\"Region (free-form text)\"\n)\noptions.add_argument(\n    \"-d\", \"--date\", dest=\"date\", help=\"Date to compute times for (yyyy-mm-dd)\"\n)\noptions.add_argument(\"-t\", \"--tzname\", help=\"Timezone name\")\noptions.add_argument(\"latitude\", type=float, help=\"Location latitude (float)\")\noptions.add_argument(\"longitude\", type=float, help=\"Location longitude (float)\")\noptions.add_argument(\n    \"elevation\", nargs=\"?\", type=float, default=0.0, help=\"Elevation in metres (float)\"\n)\nargs = options.parse_args()\n\nloc = LocationInfo(\n    args.name,\n    args.region,\n    args.tzname,\n    args.latitude,\n    args.longitude,\n)\n\nobs = Observer(args.latitude, args.longitude, args.elevation)\n\nkwargs: Dict[str, Any] = {}\nkwargs[\"observer\"] = obs\n\nif args.date is not None:\n    try:\n        kwargs[\"date\"] = datetime.datetime.strptime(args.date, \"%Y-%m-%d\").date()\n    except:  # noqa: E722\n        kwargs[\"date\"] = datetime.date.today()\n\nsun_as_str = {}\nformat_str = \"%Y-%m-%dT%H:%M:%S\"\nif args.tzname is None:\n    kwargs[\"tzinfo\"] = datetime.timezone.utc\n    format_str += \"Z\"\nelse:\n    kwargs[\"tzinfo\"] = zoneinfo.ZoneInfo(loc.timezone)\n    format_str += \"%z\"\n\n\ns = sun.sun(**kwargs)\n\nfor key, value in s.items():\n    sun_as_str[key] = s[key].strftime(format_str)\n\nsun_as_str[\"timezone\"] = kwargs[\"tzinfo\"].tzname\nsun_as_str[\"location\"] = f\"{loc.name}, {loc.region}\"\n\nprint(json.dumps(sun_as_str))\n"
  },
  {
    "path": "src/astral/geocoder.py",
    "content": "\"\"\"Astral geocoder is a database of locations stored within the package.\n\nTo get the :class:`~astral.LocationInfo` for a location use the\n:func:`~astral.geocoder.lookup` function e.g. ::\n\n    from astral.geocoder import lookup, database\n    l = lookup(\"London\", database())\n\nAll locations stored in the database can be accessed using the `all_locations`\ngenerator ::\n\n    from astral.geocoder import all_locations\n    for location in all_locations:\n        print(location)\n\"\"\"\n\nfrom typing import Dict, Generator, List, Optional, Tuple, Union\n\nfrom astral import LocationInfo, dms_to_float\n\n__all__ = [\"lookup\", \"database\", \"add_locations\", \"all_locations\"]\n\n\n# region Location Info\n# name,region,timezone,latitude,longitude,elevation\n_LOCATION_INFO = \"\"\"Abu Dhabi,UAE,Asia/Dubai,24°28'N,54°22'E\nAbu Dhabi,United Arab Emirates,Asia/Dubai,24°28'N,54°22'E\nAbuja,Nigeria,Africa/Lagos,09°05'N,07°32'E\nAccra,Ghana,Africa/Accra,05°35'N,00°06'W\nAddis Ababa,Ethiopia,Africa/Addis_Ababa,09°02'N,38°42'E\nAdelaide,Australia,Australia/Adelaide,34°56'S,138°36'E\nAl Jubail,Saudi Arabia,Asia/Riyadh,25°24'N,49°39'W\nAlgiers,Algeria,Africa/Algiers,36°42'N,03°08'E\nAmman,Jordan,Asia/Amman,31°57'N,35°52'E\nAmsterdam,Netherlands,Europe/Amsterdam,52°23'N,04°54'E\nAndorra la Vella,Andorra,Europe/Andorra,42°31'N,01°32'E\nAnkara,Turkey,Europe/Istanbul,39°57'N,32°54'E\nAntananarivo,Madagascar,Indian/Antananarivo,18°55'S,47°31'E\nApia,Samoa,Pacific/Apia,13°50'S,171°50'W\nAshgabat,Turkmenistan,Asia/Ashgabat,38°00'N,57°50'E\nAsmara,Eritrea,Africa/Asmara,15°19'N,38°55'E\nAstana,Kazakhstan,Asia/Qyzylorda,51°10'N,71°30'E\nAsuncion,Paraguay,America/Asuncion,25°10'S,57°30'W\nAthens,Greece,Europe/Athens,37°58'N,23°46'E\nAvarua,Cook Islands,Etc/GMT-10,21°12'N,159°46'W\nBaghdad,Iraq,Asia/Baghdad,33°20'N,44°30'E\nBaku,Azerbaijan,Asia/Baku,40°29'N,49°56'E\nBamako,Mali,Africa/Bamako,12°34'N,07°55'W\nBandar Seri Begawan,Brunei Darussalam,Asia/Brunei,04°52'N,115°00'E\nBangkok,Thailand,Asia/Bangkok,13°45'N,100°35'E\nBangui,Central African Republic,Africa/Bangui,04°23'N,18°35'E\nBanjul,Gambia,Africa/Banjul,13°28'N,16°40'W\nBasse-Terre,Guadeloupe,America/Guadeloupe,16°00'N,61°44'W\nBasseterre,Saint Kitts and Nevis,America/St_Kitts,17°17'N,62°43'W\nBeijing,China,Asia/Harbin,39°55'N,116°20'E\nBeirut,Lebanon,Asia/Beirut,33°53'N,35°31'E\nBelfast,Northern Ireland,Europe/Belfast,54°36'N,5°56'W\nBelgrade,Yugoslavia,Europe/Belgrade,44°50'N,20°37'E\nBelmopan,Belize,America/Belize,17°18'N,88°30'W\nBerlin,Germany,Europe/Berlin,52°30'N,13°25'E\nBern,Switzerland,Europe/Zurich,46°57'N,07°28'E\nBishkek,Kyrgyzstan,Asia/Bishkek,42°54'N,74°46'E\nBissau,Guinea-Bissau,Africa/Bissau,11°45'N,15°45'W\nBloemfontein,South Africa,Africa/Johannesburg,29°12'S,26°07'E\nBogota,Colombia,America/Bogota,04°34'N,74°00'W\nBrasilia,Brazil,Brazil/East,15°47'S,47°55'W\nBratislava,Slovakia,Europe/Bratislava,48°10'N,17°07'E\nBrazzaville,Congo,Africa/Brazzaville,04°09'S,15°12'E\nBridgetown,Barbados,America/Barbados,13°05'N,59°30'W\nBrisbane,Australia,Australia/Brisbane,27°30'S,153°01'E\nBrussels,Belgium,Europe/Brussels,50°51'N,04°21'E\nBucharest,Romania,Europe/Bucharest,44°27'N,26°10'E\nBucuresti,Romania,Europe/Bucharest,44°27'N,26°10'E\nBudapest,Hungary,Europe/Budapest,47°29'N,19°05'E\nBuenos Aires,Argentina,America/Buenos_Aires,34°62'S,58°44'W\nBujumbura,Burundi,Africa/Bujumbura,03°16'S,29°18'E\nCairo,Egypt,Africa/Cairo,30°01'N,31°14'E\nCanberra,Australia,Australia/Canberra,35°15'S,149°08'E\nCape Town,South Africa,Africa/Johannesburg,33°55'S,18°22'E\nCaracas,Venezuela,America/Caracas,10°30'N,66°55'W\nCastries,Saint Lucia,America/St_Lucia,14°02'N,60°58'W\nCayenne,French Guiana,America/Cayenne,05°05'N,52°18'W\nCharlotte Amalie,United States of Virgin Islands,America/Virgin,18°21'N,64°56'W\nChisinau,Moldova,Europe/Chisinau,47°02'N,28°50'E\nConakry,Guinea,Africa/Conakry,09°29'N,13°49'W\nCopenhagen,Denmark,Europe/Copenhagen,55°41'N,12°34'E\nCotonou,Benin,Africa/Porto-Novo,06°23'N,02°42'E\nDakar,Senegal,Africa/Dakar,14°34'N,17°29'W\nDamascus,Syrian Arab Republic,Asia/Damascus,33°30'N,36°18'E\nDammam,Saudi Arabia,Asia/Riyadh,26°30'N,50°12'E\nDarwin,Australia,Australia/Darwin,12°26'S,130°50'E\nDhaka,Bangladesh,Asia/Dhaka,23°43'N,90°26'E\nDili,East Timor,Asia/Dili,08°29'S,125°34'E\nDjibouti,Djibouti,Africa/Djibouti,11°08'N,42°20'E\nDodoma,United Republic of Tanzania,Africa/Dar_es_Salaam,06°08'S,35°45'E\nDoha,Qatar,Asia/Qatar,25°15'N,51°35'E\nDouglas,Isle Of Man,Europe/London,54°9'N,4°29'W\nDublin,Ireland,Europe/Dublin,53°21'N,06°15'W\nDushanbe,Tajikistan,Asia/Dushanbe,38°33'N,68°48'E\nEl Aaiun,Morocco,UTC,27°9'N,13°12'W\nFort-de-France,Martinique,America/Martinique,14°36'N,61°02'W\nFreetown,Sierra Leone,Africa/Freetown,08°30'N,13°17'W\nFunafuti,Tuvalu,Pacific/Funafuti,08°31'S,179°13'E\nGaborone,Botswana,Africa/Gaborone,24°45'S,25°57'E\nGeorge Town,Cayman Islands,America/Cayman,19°20'N,81°24'W\nGeorgetown,Guyana,America/Guyana,06°50'N,58°12'W\nGibraltar,Gibraltar,Europe/Gibraltar,36°9'N,5°21'W\nGuatemala,Guatemala,America/Guatemala,14°40'N,90°22'W\nHanoi,Viet Nam,Asia/Saigon,21°05'N,105°55'E\nHarare,Zimbabwe,Africa/Harare,17°43'S,31°02'E\nHavana,Cuba,America/Havana,23°08'N,82°22'W\nHelsinki,Finland,Europe/Helsinki,60°15'N,25°03'E\nHobart,Tasmania,Australia/Hobart,42°53'S,147°19'E\nHong Kong,China,Asia/Hong_Kong,22°16'N,114°09'E\nHoniara,Solomon Islands,Pacific/Guadalcanal,09°27'S,159°57'E\nIslamabad,Pakistan,Asia/Karachi,33°40'N,73°10'E\nJakarta,Indonesia,Asia/Jakarta,06°09'S,106°49'E\nJerusalem,Israel,Asia/Jerusalem,31°47'N,35°12'E\nJuba,South Sudan,Africa/Juba,4°51'N,31°36'E\nJubail,Saudi Arabia,Asia/Riyadh,27°02'N,49°39'E\nKabul,Afghanistan,Asia/Kabul,34°28'N,69°11'E\nKampala,Uganda,Africa/Kampala,00°20'N,32°30'E\nKathmandu,Nepal,Asia/Kathmandu,27°45'N,85°20'E\nKhartoum,Sudan,Africa/Khartoum,15°31'N,32°35'E\nKiev,Ukraine,Europe/Kiev,50°30'N,30°28'E\nKigali,Rwanda,Africa/Kigali,01°59'S,30°04'E\nKingston,Jamaica,America/Jamaica,18°00'N,76°50'W\nKingston,Norfolk Island,Pacific/Norfolk,45°20'S,168°43'E\nKingstown,Saint Vincent and the Grenadines,America/St_Vincent,13°10'N,61°10'W\nKinshasa,Democratic Republic of the Congo,Africa/Kinshasa,04°20'S,15°15'E\nKoror,Palau,Pacific/Palau,07°20'N,134°28'E\nKuala Lumpur,Malaysia,Asia/Kuala_Lumpur,03°09'N,101°41'E\nKuwait,Kuwait,Asia/Kuwait,29°30'N,48°00'E\nLa Paz,Bolivia,America/La_Paz,16°20'S,68°10'W\nLibreville,Gabon,Africa/Libreville,00°25'N,09°26'E\nLilongwe,Malawi,Africa/Blantyre,14°00'S,33°48'E\nLima,Peru,America/Lima,12°00'S,77°00'W\nLisbon,Portugal,Europe/Lisbon,38°42'N,09°10'W\nLjubljana,Slovenia,Europe/Ljubljana,46°04'N,14°33'E\nLome,Togo,Africa/Lome,06°09'N,01°20'E\nLondon,England,Europe/London,51°28'24\"N,00°00'3\"W\nLuanda,Angola,Africa/Luanda,08°50'S,13°15'E\nLusaka,Zambia,Africa/Lusaka,15°28'S,28°16'E\nLuxembourg,Luxembourg,Europe/Luxembourg,49°37'N,06°09'E\nMacau,Macao,Asia/Macau,22°12'N,113°33'E\nMadinah,Saudi Arabia,Asia/Riyadh,24°28'N,39°36'E\nMadrid,Spain,Europe/Madrid,40°25'N,03°45'W\nMajuro,Marshall Islands,Pacific/Majuro,7°4'N,171°16'E\nMakkah,Saudi Arabia,Asia/Riyadh,21°26'N,39°49'E\nMalabo,Equatorial Guinea,Africa/Malabo,03°45'N,08°50'E\nMale,Maldives,Indian/Maldives,04°00'N,73°28'E\nMamoudzou,Mayotte,Indian/Mayotte,12°48'S,45°14'E\nManagua,Nicaragua,America/Managua,12°06'N,86°20'W\nManama,Bahrain,Asia/Bahrain,26°10'N,50°30'E\nManila,Philippines,Asia/Manila,14°40'N,121°03'E\nMaputo,Mozambique,Africa/Maputo,25°58'S,32°32'E\nMaseru,Lesotho,Africa/Maseru,29°18'S,27°30'E\nMasqat,Oman,Asia/Muscat,23°37'N,58°36'E\nMbabane,Swaziland,Africa/Mbabane,26°18'S,31°06'E\nMecca,Saudi Arabia,Asia/Riyadh,21°26'N,39°49'E\nMedina,Saudi Arabia,Asia/Riyadh,24°28'N,39°36'E\nMelbourne,Australia,Australia/Melbourne,37°48'S,144°57'E\nMexico,Mexico,America/Mexico_City,19°20'N,99°10'W\nMinsk,Belarus,Europe/Minsk,53°52'N,27°30'E\nMogadishu,Somalia,Africa/Mogadishu,02°02'N,45°25'E\nMonaco,Priciplality Of Monaco,Europe/Monaco,43°43'N,7°25'E\nMonrovia,Liberia,Africa/Monrovia,06°18'N,10°47'W\nMontevideo,Uruguay,America/Montevideo,34°50'S,56°11'W\nMoroni,Comoros,Indian/Comoro,11°40'S,43°16'E\nMoscow,Russian Federation,Europe/Moscow,55°45'N,37°35'E\nMoskva,Russian Federation,Europe/Moscow,55°45'N,37°35'E\nMumbai,India,Asia/Kolkata,18°58'N,72°49'E\nMuscat,Oman,Asia/Muscat,23°37'N,58°32'E\nN'Djamena,Chad,Africa/Ndjamena,12°10'N,14°59'E\nNairobi,Kenya,Africa/Nairobi,01°17'S,36°48'E\nNassau,Bahamas,America/Nassau,25°05'N,77°20'W\nNaypyidaw,Myanmar,Asia/Rangoon,19°45'N,96°6'E\nNew Delhi,India,Asia/Kolkata,28°37'N,77°13'E\nNgerulmud,Palau,Pacific/Palau,7°30'N,134°37'E\nNiamey,Niger,Africa/Niamey,13°27'N,02°06'E\nNicosia,Cyprus,Asia/Nicosia,35°10'N,33°25'E\nNouakchott,Mauritania,Africa/Nouakchott,20°10'S,57°30'E\nNoumea,New Caledonia,Pacific/Noumea,22°17'S,166°30'E\nNuku'alofa,Tonga,Pacific/Tongatapu,21°10'S,174°00'W\nNuuk,Greenland,America/Godthab,64°10'N,51°35'W\nOranjestad,Aruba,America/Aruba,12°32'N,70°02'W\nOslo,Norway,Europe/Oslo,59°55'N,10°45'E\nOttawa,Canada,US/Eastern,45°27'N,75°42'W\nOuagadougou,Burkina Faso,Africa/Ouagadougou,12°15'N,01°30'W\nP'yongyang,Democratic People's Republic of Korea,Asia/Pyongyang,39°09'N,125°30'E\nPago Pago,American Samoa,Pacific/Pago_Pago,14°16'S,170°43'W\nPalikir,Micronesia,Pacific/Ponape,06°55'N,158°09'E\nPanama,Panama,America/Panama,09°00'N,79°25'W\nPapeete,French Polynesia,Pacific/Tahiti,17°32'S,149°34'W\nParamaribo,Suriname,America/Paramaribo,05°50'N,55°10'W\nParis,France,Europe/Paris,48°50'N,02°20'E\nPerth,Australia,Australia/Perth,31°56'S,115°50'E\nPhnom Penh,Cambodia,Asia/Phnom_Penh,11°33'N,104°55'E\nPodgorica,Montenegro,Europe/Podgorica,42°28'N,19°16'E\nPort Louis,Mauritius,Indian/Mauritius,20°9'S,57°30'E\nPort Moresby,Papua New Guinea,Pacific/Port_Moresby,09°24'S,147°08'E\nPort-Vila,Vanuatu,Pacific/Efate,17°45'S,168°18'E\nPort-au-Prince,Haiti,America/Port-au-Prince,18°40'N,72°20'W\nPort of Spain,Trinidad and Tobago,America/Port_of_Spain,10°40'N,61°31'W\nPorto-Novo,Benin,Africa/Porto-Novo,06°23'N,02°42'E\nPrague,Czech Republic,Europe/Prague,50°05'N,14°22'E\nPraia,Cape Verde,Atlantic/Cape_Verde,15°02'N,23°34'W\nPretoria,South Africa,Africa/Johannesburg,25°44'S,28°12'E\nPristina,Albania,Europe/Tirane,42°40'N,21°10'E\nQuito,Ecuador,America/Guayaquil,00°15'S,78°35'W\nRabat,Morocco,Africa/Casablanca,34°1'N,6°50'W\nReykjavik,Iceland,Atlantic/Reykjavik,64°10'N,21°57'W\nRiga,Latvia,Europe/Riga,56°53'N,24°08'E\nRiyadh,Saudi Arabia,Asia/Riyadh,24°41'N,46°42'E\nRoad Town,British Virgin Islands,America/Virgin,18°27'N,64°37'W\nRome,Italy,Europe/Rome,41°54'N,12°29'E\nRoseau,Dominica,America/Dominica,15°20'N,61°24'W\nSaint Helier,Jersey,Etc/GMT,49°11'N,2°6'W\nSaint Pierre,Saint Pierre and Miquelon,America/Miquelon,46°46'N,56°12'W\nSaipan,Northern Mariana Islands,Pacific/Saipan,15°12'N,145°45'E\nSana,Yemen,Asia/Aden,15°20'N,44°12'W\nSana'a,Yemen,Asia/Aden,15°20'N,44°12'W\nSan Jose,Costa Rica,America/Costa_Rica,09°55'N,84°02'W\nSan Juan,Puerto Rico,America/Puerto_Rico,18°28'N,66°07'W\nSan Marino,San Marino,Europe/San_Marino,43°55'N,12°30'E\nSan Salvador,El Salvador,America/El_Salvador,13°40'N,89°10'W\nSantiago,Chile,America/Santiago,33°24'S,70°40'W\nSanto Domingo,Dominica Republic,America/Santo_Domingo,18°30'N,69°59'W\nSao Tome,Sao Tome and Principe,Africa/Sao_Tome,00°10'N,06°39'E\nSarajevo,Bosnia and Herzegovina,Europe/Sarajevo,43°52'N,18°26'E\nSeoul,Republic of Korea,Asia/Seoul,37°31'N,126°58'E\nSingapore,Republic of Singapore,Asia/Singapore,1°18'N,103°48'E\nSkopje,The Former Yugoslav Republic of Macedonia,Europe/Skopje,42°01'N,21°26'E\nSofia,Bulgaria,Europe/Sofia,42°45'N,23°20'E\nSri Jayawardenapura Kotte,Sri Lanka,Asia/Colombo,6°54'N,79°53'E\nSt. George's,Grenada,America/Grenada,32°22'N,64°40'W\nSt. John's,Antigua and Barbuda,America/Antigua,17°7'N,61°51'W\nSt. Peter Port,Guernsey,Europe/Guernsey,49°26'N,02°33'W\nStanley,Falkland Islands,Atlantic/Stanley,51°40'S,59°51'W\nStockholm,Sweden,Europe/Stockholm,59°20'N,18°05'E\nSucre,Bolivia,America/La_Paz,16°20'S,68°10'W\nSuva,Fiji,Pacific/Fiji,18°06'S,178°30'E\nSydney,Australia,Australia/Sydney,33°53'S,151°13'E\nTaipei,Republic of China (Taiwan),Asia/Taipei,25°02'N,121°38'E\nT'bilisi,Georgia,Asia/Tbilisi,41°43'N,44°50'E\nTbilisi,Georgia,Asia/Tbilisi,41°43'N,44°50'E\nTallinn,Estonia,Europe/Tallinn,59°22'N,24°48'E\nTarawa,Kiribati,Pacific/Tarawa,01°30'N,173°00'E\nTashkent,Uzbekistan,Asia/Tashkent,41°20'N,69°10'E\nTegucigalpa,Honduras,America/Tegucigalpa,14°05'N,87°14'W\nTehran,Iran,Asia/Tehran,35°44'N,51°30'E\nThimphu,Bhutan,Asia/Thimphu,27°31'N,89°45'E\nTirana,Albania,Europe/Tirane,41°18'N,19°49'E\nTirane,Albania,Europe/Tirane,41°18'N,19°49'E\nTorshavn,Faroe Islands,Atlantic/Faroe,62°05'N,06°56'W\nTokyo,Japan,Asia/Tokyo,35°41'N,139°41'E\nTripoli,Libyan Arab Jamahiriya,Africa/Tripoli,32°49'N,13°07'E\nTunis,Tunisia,Africa/Tunis,36°50'N,10°11'E\nUlan Bator,Mongolia,Asia/Ulaanbaatar,47°55'N,106°55'E\nUlaanbaatar,Mongolia,Asia/Ulaanbaatar,47°55'N,106°55'E\nVaduz,Liechtenstein,Europe/Vaduz,47°08'N,09°31'E\nValletta,Malta,Europe/Malta,35°54'N,14°31'E\nVienna,Austria,Europe/Vienna,48°12'N,16°22'E\nVientiane,Lao People's Democratic Republic,Asia/Vientiane,17°58'N,102°36'E\nVilnius,Lithuania,Europe/Vilnius,54°38'N,25°19'E\nW. Indies,Antigua and Barbuda,America/Antigua,17°20'N,61°48'W\nWarsaw,Poland,Europe/Warsaw,52°13'N,21°00'E\nWashington DC,USA,US/Eastern,39°91'N,77°02'W\nWellington,New Zealand,Pacific/Auckland,41°19'S,174°46'E\nWillemstad,Netherlands Antilles,America/Curacao,12°05'N,69°00'W\nWindhoek,Namibia,Africa/Windhoek,22°35'S,17°04'E\nYamoussoukro,Cote d'Ivoire,Africa/Abidjan,06°49'N,05°17'W\nYangon,Myanmar,Asia/Rangoon,16°45'N,96°20'E\nYaounde,Cameroon,Africa/Douala,03°50'N,11°35'E\nYaren,Nauru,Pacific/Nauru,0°32'S,166°55'E\nYerevan,Armenia,Asia/Yerevan,40°10'N,44°31'E\nZagreb,Croatia,Europe/Zagreb,45°50'N,15°58'E\nZurich,Switzerland,Europe/Zurich,47°22'N,08°33'E\n\n# UK Cities\nAberdeen,Scotland,Europe/London,57°08'N,02°06'W\nBirmingham,England,Europe/London,52°30'N,01°50'W\nBolton,England,Europe/London,53°35'N,02°15'W\nBradford,England,Europe/London,53°47'N,01°45'W\nBristol,England,Europe/London,51°28'N,02°35'W\nCardiff,Wales,Europe/London,51°29'N,03°13'W\nCrawley,England,Europe/London,51°8'N,00°10'W\nEdinburgh,Scotland,Europe/London,55°57'N,03°13'W\nGlasgow,Scotland,Europe/London,55°50'N,04°15'W\nGreenwich,England,Europe/London,51°28'N,00°00'W\nLeeds,England,Europe/London,53°48'N,01°35'W\nLeicester,England,Europe/London,52°38'N,01°08'W\nLiverpool,England,Europe/London,53°25'N,03°00'W\nManchester,England,Europe/London,53°30'N,02°15'W\nNewcastle Upon Tyne,England,Europe/London,54°59'N,01°36'W\nNewcastle,England,Europe/London,54°59'N,01°36'W\nNorwich,England,Europe/London,52°38'N,01°18'E\nOxford,England,Europe/London,51°45'N,01°15'W\nPlymouth,England,Europe/London,50°25'N,04°15'W\nPortsmouth,England,Europe/London,50°48'N,01°05'W\nReading,England,Europe/London,51°27'N,0°58'W\nSheffield,England,Europe/London,53°23'N,01°28'W\nSouthampton,England,Europe/London,50°55'N,01°25'W\nSwansea,England,Europe/London,51°37'N,03°57'W\nSwindon,England,Europe/London,51°34'N,01°47'W\nWolverhampton,England,Europe/London,52°35'N,2°08'W\nBarrow-In-Furness,England,Europe/London,54°06'N,3°13'W\n\n# US State Capitals\nMontgomery,USA,US/Central,32°21'N,86°16'W\nJuneau,USA,US/Alaska,58°23'N,134°11'W\nPhoenix,USA,America/Phoenix,33°26'N,112°04'W\nLittle Rock,USA,US/Central,34°44'N,92°19'W\nSacramento,USA,US/Pacific,38°33'N,121°28'W\nDenver,USA,US/Mountain,39°44'N,104°59'W\nHartford,USA,US/Eastern,41°45'N,72°41'W\nDover,USA,US/Eastern,39°09'N,75°31'W\nTallahassee,USA,US/Eastern,30°27'N,84°16'W\nAtlanta,USA,US/Eastern,33°45'N,84°23'W\nHonolulu,USA,US/Hawaii,21°18'N,157°49'W\nBoise,USA,US/Mountain,43°36'N,116°12'W\nSpringfield,USA,US/Central,39°47'N,89°39'W\nIndianapolis,USA,US/Eastern,39°46'N,86°9'W\nDes Moines,USA,US/Central,41°35'N,93°37'W\nTopeka,USA,US/Central,39°03'N,95°41'W\nFrankfort,USA,US/Eastern,38°11'N,84°51'W\nBaton Rouge,USA,US/Central,30°27'N,91°8'W\nAugusta,USA,US/Eastern,44°18'N,69°46'W\nAnnapolis,USA,US/Eastern,38°58'N,76°30'W\nBoston,USA,US/Eastern,42°21'N,71°03'W\nLansing,USA,US/Eastern,42°44'N,84°32'W\nSaint Paul,USA,US/Central,44°56'N,93°05'W\nJackson,USA,US/Central,32°17'N,90°11'W\nJefferson City,USA,US/Central,38°34'N,92°10'W\nHelena,USA,US/Mountain,46°35'N,112°1'W\nLincoln,USA,US/Central,40°48'N,96°40'W\nCarson City,USA,US/Pacific,39°9'N,119°45'W\nConcord,USA,US/Eastern,43°12'N,71°32'W\nTrenton,USA,US/Eastern,40°13'N,74°45'W\nSanta Fe,USA,US/Mountain,35°40'N,105°57'W\nAlbany,USA,US/Eastern,42°39'N,73°46'W\nRaleigh,USA,US/Eastern,35°49'N,78°38'W\nBismarck,USA,US/Central,46°48'N,100°46'W\nColumbus,USA,US/Eastern,39°59'N,82°59'W\nOklahoma City,USA,US/Central,35°28'N,97°32'W\nSalem,USA,US/Pacific,44°55'N,123°1'W\nHarrisburg,USA,US/Eastern,40°16'N,76°52'W\nProvidence,USA,US/Eastern,41°49'N,71°25'W\nColumbia,USA,US/Eastern,34°00'N,81°02'W\nPierre,USA,US/Central,44°22'N,100°20'W\nNashville,USA,US/Central,36°10'N,86°47'W\nAustin,USA,US/Central,30°16'N,97°45'W\nSalt Lake City,USA,US/Mountain,40°45'N,111°53'W\nMontpelier,USA,US/Eastern,44°15'N,72°34'W\nRichmond,USA,US/Eastern,37°32'N,77°25'W\nOlympia,USA,US/Pacific,47°2'N,122°53'W\nCharleston,USA,US/Eastern,38°20'N,81°38'W\nMadison,USA,US/Central,43°4'N,89°24'W\nCheyenne,USA,US/Mountain,41°8'N,104°48'W\n\n# Major US Cities\nBirmingham,USA,US/Central,33°39'N,86°48'W\nAnchorage,USA,US/Alaska,61°13'N,149°53'W\nLos Angeles,USA,US/Pacific,34°03'N,118°15'W\nSan Francisco,USA,US/Pacific,37°46'N,122°25'W\nBridgeport,USA,US/Eastern,41°11'N,73°11'W\nWilmington,USA,US/Eastern,39°44'N,75°32'W\nJacksonville,USA,US/Eastern,30°19'N,81°39'W\nMiami,USA,US/Eastern,26°8'N,80°12'W\nChicago,USA,US/Central,41°50'N,87°41'W\nWichita,USA,US/Central,37°41'N,97°20'W\nLouisville,USA,US/Eastern,38°15'N,85°45'W\nNew Orleans,USA,US/Central,29°57'N,90°4'W\nPortland,USA,US/Eastern,43°39'N,70°16'W\nBaltimore,USA,US/Eastern,39°17'N,76°37'W\nDetroit,USA,US/Eastern,42°19'N,83°2'W\nMinneapolis,USA,US/Central,44°58'N,93°15'W\nKansas City,USA,US/Central,39°06'N,94°35'W\nBillings,USA,US/Mountain,45°47'N,108°32'W\nOmaha,USA,US/Central,41°15'N,96°0'W\nLas Vegas,USA,US/Pacific,36°10'N,115°08'W\nManchester,USA,US/Eastern,42°59'N,71°27'W\nNewark,USA,US/Eastern,40°44'N,74°11'W\nAlbuquerque,USA,US/Mountain,35°06'N,106°36'W\nNew York,USA,US/Eastern,40°43'N,74°0'W\nCharlotte,USA,US/Eastern,35°13'N,80°50'W\nFargo,USA,US/Central,46°52'N,96°47'W\nCleveland,USA,US/Eastern,41°28'N,81°40'W\nPhiladelphia,USA,US/Eastern,39°57'N,75°10'W\nSioux Falls,USA,US/Central,43°32'N,96°43'W\nMemphis,USA,US/Central,35°07'N,89°58'W\nHouston,USA,US/Central,29°45'N,95°22'W\nDallas,USA,US/Central,32°47'N,96°48'W\nBurlington,USA,US/Eastern,44°28'N,73°9'W\nVirginia Beach,USA,US/Eastern,36°50'N,76°05'W\nSeattle,USA,US/Pacific,47°36'N,122°19'W\nMilwaukee,USA,US/Central,43°03'N,87°57'W\nSan Diego,USA,US/Pacific,32°42'N,117°09'W\nOrlando,USA,US/Eastern,28°32'N,81°22'W\nBuffalo,USA,US/Eastern,42°54'N,78°50'W\nToledo,USA,US/Eastern,41°39'N,83°34'W\n\n# Canadian cities\nVancouver,Canada,America/Vancouver,49°15'N,123°6'W\nCalgary,Canada,America/Edmonton,51°2'N,114°3'W\nEdmonton,Canada,America/Edmonton,53°32'N,113°29'W\nSaskatoon,Canada,America/Regina,52°8'N,106°40'W\nRegina,Canada,America/Regina,50°27'N,104°36'W\nWinnipeg,Canada,America/Winnipeg,49°53'N,97°8'W\nToronto,Canada,America/Toronto,43°39'N,79°22'W\nMontreal,Canada,America/Montreal,45°30'N,73°33'W\nQuebec,Canada,America/Toronto,46°48'N,71°14'W\nFredericton,Canada,America/Halifax,45°57'N,66°38'W\nHalifax,Canada,America/Halifax,44°38'N,63°34'W\nCharlottetown,Canada,America/Halifax,46°14'N,63°7'W\nSt. John's,Canada,America/Halifax,47°33'N,52°42'W\nWhitehorse,Canada,America/Whitehorse,60°43'N,135°3'W\nYellowknife,Canada,America/Yellowknife,62°27'N,114°22'W\nIqaluit,Canada,America/Iqaluit,63°44'N,68°31'W\n\"\"\"\n# endregion\n\nGroupName = str\nLocationName = str\nGroupInfo = Dict[LocationName, List[LocationInfo]]\nLocationDatabase = Dict[GroupName, GroupInfo]\n\n\ndef database() -> LocationDatabase:\n    \"\"\"Returns a database populated with the inital set of locations stored\n    in this module\n    \"\"\"\n    db: LocationDatabase = {}\n    _add_locations_from_str(_LOCATION_INFO, db)\n    return db\n\n\ndef _sanitize_key(key: str) -> str:\n    \"\"\"Sanitize the location or group key to look up\n\n    Args:\n        key: The key to sanitize\n    \"\"\"\n    return str(key).lower().replace(\" \", \"_\")\n\n\ndef _get_group(name: str, db: LocationDatabase) -> Optional[GroupInfo]:\n    return db.get(name, None)\n\n\ndef _add_location_to_db(location: LocationInfo, db: LocationDatabase) -> None:\n    \"\"\"Add a single location to a database\"\"\"\n    key = _sanitize_key(location.timezone_group)\n    group = _get_group(key, db)\n    if not group:\n        group = {}\n        db[key] = group\n\n    location_key = _sanitize_key(location.name)\n    if location_key not in group:\n        group[location_key] = [location]\n    else:\n        group[location_key].append(location)\n\n\ndef _locationinfo_from_str(info: str) -> LocationInfo:\n    idxable = info.split(\",\")\n    return LocationInfo(\n        name=idxable[0],\n        region=idxable[1],\n        timezone=idxable[2],\n        latitude=dms_to_float(idxable[3], 90.0),\n        longitude=dms_to_float(idxable[4], 180.0),\n    )\n\n\ndef _locationinfo_from_indexable(\n    idxable: Union[Tuple[str, ...], List[str]]\n) -> LocationInfo:\n    return LocationInfo(\n        name=idxable[0],\n        region=idxable[1],\n        timezone=idxable[2],\n        latitude=dms_to_float(idxable[3], 90.0),\n        longitude=dms_to_float(idxable[4], 180.0),\n    )\n\n\ndef _add_locations_from_str(location_string: str, db: LocationDatabase) -> None:\n    \"\"\"Add locations from a string.\"\"\"\n\n    for line in location_string.split(\"\\n\"):\n        line = line.strip()\n        if line != \"\" and line[0] != \"#\":\n            location = _locationinfo_from_str(line)\n            _add_location_to_db(location, db)\n\n\ndef _add_locations_from_list(\n    location_list: Union[List[str], List[List[str]], List[Tuple[str, ...]]],\n    db: LocationDatabase,\n) -> None:\n    \"\"\"Add locations from a list of either strings or lists of strings\n    or tuples of strings.\n    \"\"\"\n    for info in location_list:\n        if isinstance(info, str):\n            _add_locations_from_str(info, db)\n        else:\n            location = _locationinfo_from_indexable(info)\n            _add_location_to_db(location, db)\n\n\ndef add_locations(\n    locations: Union[str, List[str], List[List[str]], List[Tuple[str, ...]]],\n    db: LocationDatabase,\n) -> None:\n    \"\"\"Add locations to the database.\n\n    Locations can be added by passing either a string with one line per location or\n    by passing a list containing strings, lists or tuples (lists and tuples are\n    passed directly to the LocationInfo constructor).\"\"\"\n    if isinstance(locations, str):\n        _add_locations_from_str(locations, db)\n    else:\n        _add_locations_from_list(locations, db)\n\n\ndef group(region: str, db: LocationDatabase) -> GroupInfo:\n    \"\"\"Access to each timezone group. For example London is in timezone\n    group Europe.\n\n    Lookups are case insensitive\n\n    Args:\n        region: the name to look up\n\n    Raises:\n        KeyError: if the location is not found\n    \"\"\"\n    key = _sanitize_key(region)\n    for name, value in db.items():\n        if name == key:\n            return value\n\n    raise KeyError(f\"Unrecognised Group - {region}\")\n\n\ndef lookup_in_group(\n    location: str, group: Dict[str, List[LocationInfo]]\n) -> LocationInfo:\n    \"\"\"Looks up the location within a group dictionary\n\n    You can supply an optional region name by adding a comma\n    followed by the region name. Where multiple locations have the\n    same name you may need to supply the region name otherwise\n    the first result will be returned which may not be the one\n    you're looking for::\n\n        location = group['Abu Dhabi,United Arab Emirates']\n\n    Lookups are case insensitive.\n\n    Args:\n        location: The location to look up\n        group: The location group to look in\n\n    Raises:\n        KeyError: if the location is not found\n    \"\"\"\n    key = _sanitize_key(location)\n\n    try:\n        lookup_name, lookup_region = key.split(\",\", 1)\n    except ValueError:\n        lookup_name = key\n        lookup_region = \"\"\n\n    lookup_name = lookup_name.strip(\"\\\"'\")\n    lookup_region = lookup_region.strip(\"\\\"'\")\n\n    for (location_name, location_list) in group.items():\n        if location_name == lookup_name:\n            if lookup_region == \"\":\n                return location_list[0]\n\n            for loc in location_list:\n                if _sanitize_key(loc.region) == lookup_region:\n                    return loc\n\n    raise KeyError(f\"Unrecognised location name - {key}\")\n\n\ndef lookup(name: str, db: LocationDatabase) -> Union[GroupInfo, LocationInfo]:\n    \"\"\"Look up a name in a database.\n\n    If a group with the name specified is a group name then that will\n    be returned. If no group is found a location with the name will be\n    looked up.\n\n    Args:\n        name: The group/location name to look up\n        db:   The location database to look in\n\n    Raises:\n        KeyError: if the name is not found\n    \"\"\"\n\n    key = _sanitize_key(name)\n    for group_key, group in db.items():\n        if group_key == key:\n            return group\n\n        try:\n            return lookup_in_group(name, group)\n        except KeyError:\n            pass\n\n    raise KeyError(f\"Unrecognised name - {name}\")\n\n\ndef all_locations(db: LocationDatabase) -> Generator[LocationInfo, None, None]:\n    \"\"\"A generator that returns all the :class:`~astral.LocationInfo`\\\\s\n    contained in the database\n    \"\"\"\n    for group_info in db.values():\n        for location_list in group_info.values():\n            for location in location_list:\n                yield location\n"
  },
  {
    "path": "src/astral/julian.py",
    "content": "import datetime\nfrom enum import Enum\nfrom typing import Union\n\n\nclass Calendar(Enum):\n    GREGORIAN = 1\n    JULIAN = 2\n\n\ndef day_fraction_to_time(fraction: float) -> datetime.time:\n    s = fraction * (24 * 60 * 60)\n    h = int(s / (60 * 60))\n    s -= h * 60 * 60\n    m = int(s / 60)\n    s -= m * 60\n    s = int(s)\n    return datetime.time(h, m, s)\n\n\ndef julianday(\n    at: Union[datetime.datetime, datetime.date], calendar: Calendar = Calendar.GREGORIAN\n) -> float:\n    \"\"\"Calculate the Julian Day (number) for the specified date/time\n\n    julian day numbers for dates are calculated for the start of the day\n    \"\"\"\n\n    def _time_to_seconds(t: datetime.time) -> int:\n        return int(t.hour * 3600 + t.minute * 60 + t.second)\n\n    year = at.year\n    month = at.month\n    day = at.day\n    day_fraction = 0.0\n    if isinstance(at, datetime.datetime):\n        t = _time_to_seconds(at.time())\n        day_fraction = t / (24 * 60 * 60)\n    else:\n        day_fraction = 0.0\n\n    if month <= 2:\n        year -= 1\n        month += 12\n\n    a = int(year / 100)\n    if calendar == Calendar.GREGORIAN:\n        b = 2 - a + int(a / 4)\n    else:\n        b = 0\n    jd = (\n        int(365.25 * (year + 4716))\n        + int(30.6001 * (month + 1))\n        + day\n        + day_fraction\n        + b\n        - 1524.5\n    )\n\n    return jd\n\n\ndef julianday_modified(at: datetime.datetime) -> float:\n    \"\"\"Calculate the Modified Julian Date number\"\"\"\n\n    year = at.year\n    month = at.month\n    day = at.day\n\n    a = 10000 * year + 100 * month + day\n\n    if year < 0:\n        year += 1\n\n    if month <= 2:\n        month += 12\n        year -= 1\n\n    if a <= 15821004.1:\n        b = -2 + (year + 4716) / 4 - 1179\n    else:\n        b = (year / 400) - (year / 100) + (year / 4)\n\n    a = 365 * year - 679004\n    mjd = a + b + int(30.6001 * (month + 1)) + day + at.hour / 24\n    return mjd\n\n\ndef julianday_to_datetime(jd: float) -> datetime.datetime:\n    \"\"\"Convert a Julian Day number to a datetime\"\"\"\n    jd += 0.5\n    z = int(jd)\n    f = jd - z\n    if z < 2299161:\n        a = z\n    else:\n        alpha = int((z - 1867216.25) / 36524.25)\n        a = z + 1 + alpha + int(alpha / 4.0)\n\n    b = a + 1524\n    c = int((b - 122.1) / 365.25)\n    d = int(365.25 * c)\n    e = int((b - d) / 30.6001)\n\n    d = b - d - int(30.6001 * e) + f  # type: ignore\n    day = int(d)\n    t = d - day\n    total_seconds = t * (24 * 60 * 60)\n    hour = int(total_seconds / 3600)\n    total_seconds -= hour * 3600\n    minute = int(total_seconds / 60)\n    total_seconds -= minute * 60\n    seconds = int(total_seconds)\n\n    if e < 14:\n        month = e - 1\n    else:\n        month = e - 13\n\n    if month > 2:\n        year = c - 4716\n    else:\n        year = c - 4715\n\n    return datetime.datetime(year, month, day, hour, minute, seconds)\n\n\ndef julianday_to_juliancentury(julianday: float) -> float:\n    \"\"\"Convert a Julian Day number to a Julian Century\"\"\"\n    return (julianday - 2451545.0) / 36525.0\n\n\ndef juliancentury_to_julianday(juliancentury: float) -> float:\n    \"\"\"Convert a Julian Century number to a Julian Day\"\"\"\n    return (juliancentury * 36525.0) + 2451545.0\n\n\ndef julianday_2000(at: Union[datetime.datetime, datetime.date]) -> float:\n    \"\"\"Calculate the numer of Julian Days since Jan 1.5, 2000\"\"\"\n    return julianday(at) - 2451545.0\n"
  },
  {
    "path": "src/astral/location.py",
    "content": "import dataclasses\nimport datetime\n\ntry:\n    import zoneinfo\nexcept ImportError:\n    from backports import zoneinfo  # type: ignore\n\nfrom typing import Any, Dict, Optional, Tuple, Union\n\nimport astral.moon\nimport astral.sun\nfrom astral import (\n    Depression,\n    Elevation,\n    LocationInfo,\n    Observer,\n    SunDirection,\n    dms_to_float,\n    today,\n)\n\n\nclass Location:\n    \"\"\"Provides access to information for single location.\"\"\"\n\n    def __init__(self, info: Optional[LocationInfo] = None):\n        \"\"\"Initializes the Location with a LocationInfo object.\n\n        The tuple should contain items in the following order\n\n        ================ =============\n        Field            Default\n        ================ =============\n        name             Greenwich\n        region           England\n        time zone name   Europe/London\n        latitude         51.4733\n        longitude        -0.0008333\n        ================ =============\n\n        See the :attr:`timezone` property for a method of obtaining time zone\n        names\n        \"\"\"\n\n        self._location_info: LocationInfo\n        self._solar_depression: float = Depression.CIVIL.value\n\n        if not info:\n            self._location_info = LocationInfo(\n                \"Greenwich\", \"England\", \"Europe/London\", 51.4733, -0.0008333\n            )\n        else:\n            self._location_info = info\n\n    def __eq__(self, other: object) -> bool:\n        if type(other) is Location:\n            return self._location_info == other._location_info  # type: ignore\n        return NotImplemented\n\n    def __repr__(self) -> str:\n        if self.region:\n            _repr = \"%s/%s\" % (self.name, self.region)\n        else:\n            _repr = self.name\n        return (\n            f\"{_repr}, tz={self.timezone}, \"\n            f\"lat={self.latitude:0.02f}, \"\n            f\"lon={self.longitude:0.02f}\"\n        )\n\n    @property\n    def info(self) -> LocationInfo:\n        return LocationInfo(\n            self.name,\n            self.region,\n            self.timezone,\n            self.latitude,\n            self.longitude,\n        )\n\n    @property\n    def observer(self) -> Observer:\n        return Observer(self.latitude, self.longitude, 0.0)\n\n    @property\n    def name(self) -> str:\n        return self._location_info.name\n\n    @name.setter\n    def name(self, name: str) -> None:\n        self._location_info = dataclasses.replace(self._location_info, name=name)\n\n    @property\n    def region(self) -> str:\n        return self._location_info.region\n\n    @region.setter\n    def region(self, region: str) -> None:\n        self._location_info = dataclasses.replace(self._location_info, region=region)\n\n    @property\n    def latitude(self) -> float:\n        \"\"\"The location's latitude\n\n        ``latitude`` can be set either as a string or as a number\n\n        For strings they must be of the form\n\n            degrees°minutes'[N|S] e.g. 51°31'N\n\n        For numbers, positive numbers signify latitudes to the North.\n        \"\"\"\n\n        return self._location_info.latitude\n\n    @latitude.setter\n    def latitude(self, latitude: Union[float, str]) -> None:\n        self._location_info = dataclasses.replace(\n            self._location_info, latitude=dms_to_float(latitude, 90.0)\n        )\n\n    @property\n    def longitude(self) -> float:\n        \"\"\"The location's longitude.\n\n        ``longitude`` can be set either as a string or as a number\n\n        For strings they must be of the form\n\n            degrees°minutes'[E|W] e.g. 51°31'W\n\n        For numbers, positive numbers signify longitudes to the East.\n        \"\"\"\n\n        return self._location_info.longitude\n\n    @longitude.setter\n    def longitude(self, longitude: Union[float, str]) -> None:\n        self._location_info = dataclasses.replace(\n            self._location_info, longitude=dms_to_float(longitude, 180.0)\n        )\n\n    @property\n    def timezone(self) -> str:\n        \"\"\"The name of the time zone for the location.\n\n        A list of time zone names can be obtained from the zoneinfo module.\n        For example.\n\n        >>> import zoneinfo\n        >>> assert \"CET\" in zoneinfo.available_timezones()\n        \"\"\"\n\n        return self._location_info.timezone\n\n    @timezone.setter\n    def timezone(self, name: str) -> None:\n        if name not in zoneinfo.available_timezones():  # type: ignore\n            raise ValueError(\"Timezone '%s' not recognized\" % name)\n\n        self._location_info = dataclasses.replace(self._location_info, timezone=name)\n\n    @property\n    def tzinfo(self) -> zoneinfo.ZoneInfo:  # type: ignore\n        \"\"\"Time zone information.\"\"\"\n\n        try:\n            tz = zoneinfo.ZoneInfo(self._location_info.timezone)  # type: ignore\n            return tz  # type: ignore\n        except zoneinfo.ZoneInfoNotFoundError as exc:  # type: ignore\n            raise ValueError(\n                \"Unknown timezone '%s'\" % self._location_info.timezone\n            ) from exc\n\n    tz = tzinfo\n\n    @property\n    def solar_depression(self) -> float:\n        \"\"\"The number of degrees the sun must be below the horizon for the\n        dawn/dusk calculation.\n\n        Can either be set as a number of degrees below the horizon or as\n        one of the following strings\n\n        ============= =======\n        String        Degrees\n        ============= =======\n        civil            6.0\n        nautical        12.0\n        astronomical    18.0\n        ============= =======\n        \"\"\"\n\n        return self._solar_depression\n\n    @solar_depression.setter\n    def solar_depression(self, depression: Union[float, str, Depression]) -> None:\n        if isinstance(depression, str):\n            try:\n                self._solar_depression = {\n                    \"civil\": 6.0,\n                    \"nautical\": 12.0,\n                    \"astronomical\": 18.0,\n                }[depression]\n            except KeyError:\n                raise KeyError(\n                    (\n                        \"solar_depression must be either a number \"\n                        \"or one of 'civil', 'nautical' or \"\n                        \"'astronomical'\"\n                    )\n                )\n        elif isinstance(depression, Depression):\n            self._solar_depression = depression.value\n        else:\n            self._solar_depression = float(depression)\n\n    def today(self, local: bool = True) -> datetime.date:\n        if local:\n            return today(self.tzinfo)\n        else:\n            return today()\n\n    def sun(\n        self,\n        date: Optional[datetime.date] = None,\n        local: bool = True,\n        observer_elevation: Elevation = 0.0,\n    ) -> Dict[str, Any]:\n        \"\"\"Returns dawn, sunrise, noon, sunset and dusk as a dictionary.\n\n        :param date: The date for which to calculate the times.\n                     If no date is specified then the current date will be used.\n\n        :param local: True  = Time to be returned in location's time zone;\n                      False = Time to be returned in UTC.\n                      If not specified then the time will be returned in local time\n\n        :param observer_elevation: Elevation of the observer in metres above\n                                   the location.\n\n        :returns: Dictionary with keys ``dawn``, ``sunrise``, ``noon``,\n            ``sunset`` and ``dusk`` whose values are the results of the\n            corresponding methods.\n        \"\"\"\n\n        if local and self.timezone is None:\n            raise ValueError(\"Local time requested but Location has no timezone set.\")\n\n        if date is None:\n            date = self.today(local)\n\n        observer = Observer(self.latitude, self.longitude, observer_elevation)\n\n        if local:\n            return astral.sun.sun(observer, date, self.solar_depression, self.tzinfo)\n        else:\n            return astral.sun.sun(observer, date, self.solar_depression)\n\n    def dawn(\n        self,\n        date: Optional[datetime.date] = None,\n        local: bool = True,\n        observer_elevation: Elevation = 0.0,\n    ) -> datetime.datetime:\n        \"\"\"Calculates the time in the morning when the sun is a certain number\n        of degrees below the horizon. By default this is 6 degrees but can be\n        changed by setting the :attr:`Astral.solar_depression` property.\n\n        :param date: The date for which to calculate the dawn time.\n                     If no date is specified then the current date will be used.\n\n        :param local: True  = Time to be returned in location's time zone;\n                      False = Time to be returned in UTC.\n                      If not specified then the time will be returned in local time\n\n        :param observer_elevation: Elevation of the observer in metres above\n                                   the location.\n\n        :returns: The date and time at which dawn occurs.\n        \"\"\"\n\n        if local and self.timezone is None:\n            raise ValueError(\"Local time requested but Location has no timezone set.\")\n\n        if date is None:\n            date = self.today(local)\n\n        observer = Observer(self.latitude, self.longitude, observer_elevation)\n\n        if local:\n            return astral.sun.dawn(observer, date, self.solar_depression, self.tzinfo)\n        else:\n            return astral.sun.dawn(observer, date, self.solar_depression)\n\n    def sunrise(\n        self,\n        date: Optional[datetime.date] = None,\n        local: bool = True,\n        observer_elevation: Elevation = 0.0,\n    ) -> datetime.datetime:\n        \"\"\"Return sunrise time.\n\n        Calculates the time in the morning when the sun is a 0.833 degrees\n        below the horizon. This is to account for refraction.\n\n        :param date: The date for which to calculate the sunrise time.\n                     If no date is specified then the current date will be used.\n\n        :param local: True  = Time to be returned in location's time zone;\n                      False = Time to be returned in UTC.\n                      If not specified then the time will be returned in local time\n\n        :param observer_elevation: Elevation of the observer in metres above\n                                   the location.\n\n        :returns: The date and time at which sunrise occurs.\n        \"\"\"\n\n        if local and self.timezone is None:\n            raise ValueError(\"Local time requested but Location has no timezone set.\")\n\n        if date is None:\n            date = self.today(local)\n\n        observer = Observer(self.latitude, self.longitude, observer_elevation)\n\n        if local:\n            return astral.sun.sunrise(observer, date, self.tzinfo)\n        else:\n            return astral.sun.sunrise(observer, date)\n\n    def noon(\n        self, date: Optional[datetime.date] = None, local: bool = True\n    ) -> datetime.datetime:\n        \"\"\"Calculates the solar noon (the time when the sun is at its highest\n        point.)\n\n        :param date: The date for which to calculate the noon time.\n                     If no date is specified then the current date will be used.\n\n        :param local: True  = Time to be returned in location's time zone;\n                      False = Time to be returned in UTC.\n                      If not specified then the time will be returned in local time\n\n        :returns: The date and time at which the solar noon occurs.\n        \"\"\"\n\n        if local and self.timezone is None:\n            raise ValueError(\"Local time requested but Location has no timezone set.\")\n\n        if date is None:\n            date = self.today(local)\n\n        observer = Observer(self.latitude, self.longitude)\n        if local:\n            return astral.sun.noon(observer, date, self.tzinfo)\n        else:\n            return astral.sun.noon(observer, date)\n\n    def sunset(\n        self,\n        date: Optional[datetime.date] = None,\n        local: bool = True,\n        observer_elevation: Elevation = 0.0,\n    ) -> datetime.datetime:\n        \"\"\"Calculates sunset time (the time in the evening when the sun is a\n        0.833 degrees below the horizon. This is to account for refraction.)\n\n        :param date: The date for which to calculate the sunset time.\n                     If no date is specified then the current date will be used.\n\n        :param local: True  = Time to be returned in location's time zone;\n                      False = Time to be returned in UTC.\n                      If not specified then the time will be returned in local time\n\n        :param observer_elevation: Elevation of the observer in metres above\n                                   the location.\n\n        :returns: The date and time at which sunset occurs.\n        \"\"\"\n\n        if local and self.timezone is None:\n            raise ValueError(\"Local time requested but Location has no timezone set.\")\n\n        if date is None:\n            date = self.today(local)\n\n        observer = Observer(self.latitude, self.longitude, observer_elevation)\n\n        if local:\n            return astral.sun.sunset(observer, date, self.tzinfo)\n        else:\n            return astral.sun.sunset(observer, date)\n\n    def dusk(\n        self,\n        date: Optional[datetime.date] = None,\n        local: bool = True,\n        observer_elevation: Elevation = 0.0,\n    ) -> datetime.datetime:\n        \"\"\"Calculates the dusk time (the time in the evening when the sun is a\n        certain number of degrees below the horizon. By default this is 6\n        degrees but can be changed by setting the\n        :attr:`solar_depression` property.)\n\n        :param date: The date for which to calculate the dusk time.\n                     If no date is specified then the current date will be used.\n\n        :param local: True  = Time to be returned in location's time zone;\n                      False = Time to be returned in UTC.\n                      If not specified then the time will be returned in local time\n\n        :param observer_elevation: Elevation of the observer in metres above\n                                   the location.\n\n        :returns: The date and time at which dusk occurs.\n        \"\"\"\n\n        if local and self.timezone is None:\n            raise ValueError(\"Local time requested but Location has no timezone set.\")\n\n        if date is None:\n            date = self.today(local)\n\n        observer = Observer(self.latitude, self.longitude, observer_elevation)\n\n        if local:\n            return astral.sun.dusk(observer, date, self.solar_depression, self.tzinfo)\n        else:\n            return astral.sun.dusk(observer, date, self.solar_depression)\n\n    def midnight(\n        self, date: Optional[datetime.date] = None, local: bool = True\n    ) -> datetime.datetime:\n        \"\"\"Calculates the solar midnight (the time when the sun is at its lowest\n        point.)\n\n        :param date: The date for which to calculate the midnight time.\n                     If no date is specified then the current date will be used.\n\n        :param local: True  = Time to be returned in location's time zone;\n                      False = Time to be returned in UTC.\n                      If not specified then the time will be returned in local time\n\n        :returns: The date and time at which the solar midnight occurs.\n        \"\"\"\n\n        if local and self.timezone is None:\n            raise ValueError(\"Local time requested but Location has no timezone set.\")\n\n        if date is None:\n            date = self.today(local)\n\n        observer = Observer(self.latitude, self.longitude)\n\n        if local:\n            return astral.sun.midnight(observer, date, self.tzinfo)\n        else:\n            return astral.sun.midnight(observer, date)\n\n    def daylight(\n        self,\n        date: Optional[datetime.date] = None,\n        local: bool = True,\n        observer_elevation: Elevation = 0.0,\n    ) -> Tuple[datetime.datetime, datetime.datetime]:\n        \"\"\"Calculates the daylight time (the time between sunrise and sunset)\n\n        :param date: The date for which to calculate daylight.\n                     If no date is specified then the current date will be used.\n\n        :param local: True  = Time to be returned in location's time zone;\n                      False = Time to be returned in UTC.\n                      If not specified then the time will be returned in local time\n\n        :param observer_elevation: Elevation of the observer in metres above\n                                   the location.\n\n        :returns: A tuple containing the start and end times\n        \"\"\"\n\n        if local and self.timezone is None:\n            raise ValueError(\"Local time requested but Location has no timezone set.\")\n\n        if date is None:\n            date = self.today(local)\n\n        observer = Observer(self.latitude, self.longitude, observer_elevation)\n\n        if local:\n            return astral.sun.daylight(observer, date, self.tzinfo)\n        else:\n            return astral.sun.daylight(observer, date)\n\n    def night(\n        self,\n        date: Optional[datetime.date] = None,\n        local: bool = True,\n        observer_elevation: Elevation = 0.0,\n    ) -> Tuple[datetime.datetime, datetime.datetime]:\n        \"\"\"Calculates the night time (the time between astronomical dusk and\n        astronomical dawn of the next day)\n\n        :param date: The date for which to calculate the start of the night time.\n                     If no date is specified then the current date will be used.\n\n        :param local: True  = Time to be returned in location's time zone;\n                      False = Time to be returned in UTC.\n                      If not specified then the time will be returned in local time\n\n        :param observer_elevation: Elevation of the observer in metres above\n                                   the location.\n\n        :returns: A tuple containing the start and end times\n        \"\"\"\n\n        if local and self.timezone is None:\n            raise ValueError(\"Local time requested but Location has no timezone set.\")\n\n        if date is None:\n            date = self.today(local)\n\n        observer = Observer(self.latitude, self.longitude, observer_elevation)\n\n        if local:\n            return astral.sun.night(observer, date, self.tzinfo)\n        else:\n            return astral.sun.night(observer, date)\n\n    def twilight(\n        self,\n        date: Optional[datetime.date] = None,\n        direction: SunDirection = SunDirection.RISING,\n        local: bool = True,\n        observer_elevation: Elevation = 0.0,\n    ):\n        \"\"\"Returns the start and end times of Twilight in the UTC timezone when\n        the sun is traversing in the specified direction.\n\n        This method defines twilight as being between the time\n        when the sun is at -6 degrees and sunrise/sunset.\n\n        :param direction:  Determines whether the time is for the sun rising or setting.\n                           Use ``astral.SUN_RISING`` or ``astral.SunDirection.SETTING``.\n\n        :param date: The date for which to calculate the times.\n\n        :param local: True  = Time to be returned in location's time zone;\n                      False = Time to be returned in UTC.\n                      If not specified then the time will be returned in local time\n\n        :param observer_elevation: Elevation of the observer in metres above\n                                   the location.\n\n        :return: A tuple of the UTC date and time at which twilight starts and ends.\n        \"\"\"\n\n        if local and self.timezone is None:\n            raise ValueError(\"Local time requested but Location has no timezone set.\")\n\n        if date is None:\n            date = self.today(local)\n\n        observer = Observer(self.latitude, self.longitude, observer_elevation)\n\n        if local:\n            return astral.sun.twilight(observer, date, direction, self.tzinfo)\n        else:\n            return astral.sun.twilight(observer, date, direction)\n\n    def moonrise(\n        self,\n        date: Optional[datetime.date] = None,\n        local: bool = True,\n    ) -> Optional[datetime.datetime]:\n        \"\"\"Calculates the time when the moon rises.\n\n        :param date: The date for which to calculate the moonrise time.\n                     If no date is specified then the current date will be used.\n\n        :param local: True  = Time to be returned in location's time zone;\n                      False = Time to be returned in UTC.\n                      If not specified then the time will be returned in local time\n\n        :returns: The date and time at which moonrise occurs.\n        \"\"\"\n\n        if local and self.timezone is None:\n            raise ValueError(\"Local time requested but Location has no timezone set.\")\n\n        if date is None:\n            date = self.today(local)\n\n        observer = Observer(self.latitude, self.longitude, 0)\n\n        if local:\n            return astral.moon.moonrise(observer, date, self.tzinfo)\n        else:\n            return astral.moon.moonrise(observer, date)\n\n    def moonset(\n        self,\n        date: Optional[datetime.date] = None,\n        local: bool = True,\n    ) -> Optional[datetime.datetime]:\n        \"\"\"Calculates the time when the moon sets.\n\n        :param date: The date for which to calculate the moonset time.\n                     If no date is specified then the current date will be used.\n\n        :param local: True  = Time to be returned in location's time zone;\n                      False = Time to be returned in UTC.\n                      If not specified then the time will be returned in local time\n\n        :returns: The date and time at which moonset occurs.\n        \"\"\"\n\n        if local and self.timezone is None:\n            raise ValueError(\"Local time requested but Location has no timezone set.\")\n\n        if date is None:\n            date = self.today(local)\n\n        observer = Observer(self.latitude, self.longitude, 0)\n\n        if local:\n            return astral.moon.moonset(observer, date, self.tzinfo)\n        else:\n            return astral.moon.moonset(observer, date)\n\n    def time_at_elevation(\n        self,\n        elevation: float,\n        date: Optional[datetime.date] = None,\n        direction: SunDirection = SunDirection.RISING,\n        local: bool = True,\n    ) -> datetime.datetime:\n        \"\"\"Calculate the time when the sun is at the specified elevation.\n\n        Note:\n            This method uses positive elevations for those above the horizon.\n\n            Elevations greater than 90 degrees are converted to a setting sun\n            i.e. an elevation of 110 will calculate a setting sun at 70 degrees.\n\n        :param elevation:  Elevation in degrees above the horizon to calculate for.\n\n        :param date: The date for which to calculate the elevation time.\n                     If no date is specified then the current date will be used.\n\n        :param direction:  Determines whether the time is for the sun rising or setting.\n                           Use ``SunDirection.RISING`` or ``SunDirection.SETTING``.\n                           Default is rising.\n\n        :param local: True  = Time to be returned in location's time zone;\n                      False = Time to be returned in UTC.\n                      If not specified then the time will be returned in local time\n\n        :returns: The date and time at which dusk occurs.\n        \"\"\"\n\n        if local and self.timezone is None:\n            raise ValueError(\"Local time requested but Location has no timezone set.\")\n\n        if date is None:\n            date = self.today(local)\n\n        if elevation > 90.0:\n            elevation = 180.0 - elevation\n            direction = SunDirection.SETTING\n\n        observer = Observer(self.latitude, self.longitude, 0.0)\n\n        if local:\n            return astral.sun.time_at_elevation(\n                observer, elevation, date, direction, self.tzinfo\n            )\n        else:\n            return astral.sun.time_at_elevation(observer, elevation, date, direction)\n\n    def rahukaalam(\n        self,\n        date: Optional[datetime.date] = None,\n        local: bool = True,\n        observer_elevation: Elevation = 0.0,\n    ) -> Tuple[datetime.datetime, datetime.datetime]:\n        \"\"\"Calculates the period of rahukaalam.\n\n        :param date: The date for which to calculate the rahukaalam period.\n                     A value of ``None`` uses the current date.\n\n        :param local: True  = Time to be returned in location's time zone;\n                      False = Time to be returned in UTC.\n\n        :param observer_elevation: Elevation of the observer in metres above\n                                   the location.\n\n        :return: Tuple containing the start and end times for Rahukaalam.\n        \"\"\"\n\n        if local and self.timezone is None:\n            raise ValueError(\"Local time requested but Location has no timezone set.\")\n\n        if date is None:\n            date = self.today(local)\n\n        observer = Observer(self.latitude, self.longitude, observer_elevation)\n\n        if local:\n            return astral.sun.rahukaalam(observer, date, tzinfo=self.tzinfo)\n        else:\n            return astral.sun.rahukaalam(observer, date)\n\n    def golden_hour(\n        self,\n        direction: SunDirection = SunDirection.RISING,\n        date: Optional[datetime.date] = None,\n        local: bool = True,\n        observer_elevation: Elevation = 0.0,\n    ) -> Tuple[datetime.datetime, datetime.datetime]:\n        \"\"\"Returns the start and end times of the Golden Hour when the sun is traversing\n        in the specified direction.\n\n        This method uses the definition from PhotoPills i.e. the\n        golden hour is when the sun is between 4 degrees below the horizon\n        and 6 degrees above.\n\n        :param direction:  Determines whether the time is for the sun rising or setting.\n                           Use ``SunDirection.RISING`` or ``SunDirection.SETTING``.\n                           Default is rising.\n\n        :param date: The date for which to calculate the times.\n\n        :param local: True  = Times to be returned in location's time zone;\n                      False = Times to be returned in UTC.\n                      If not specified then the time will be returned in local time\n\n        :param observer_elevation: Elevation of the observer in metres above\n                                   the location.\n\n        :return: A tuple of the date and time at which the Golden Hour starts and ends.\n        \"\"\"\n\n        if local and self.timezone is None:\n            raise ValueError(\"Local time requested but Location has no timezone set.\")\n\n        if date is None:\n            date = self.today(local)\n\n        observer = Observer(self.latitude, self.longitude, observer_elevation)\n\n        if local:\n            return astral.sun.golden_hour(observer, date, direction, self.tzinfo)\n        else:\n            return astral.sun.golden_hour(observer, date, direction)\n\n    def blue_hour(\n        self,\n        direction: SunDirection = SunDirection.RISING,\n        date: Optional[datetime.date] = None,\n        local: bool = True,\n        observer_elevation: Elevation = 0.0,\n    ) -> Tuple[datetime.datetime, datetime.datetime]:\n        \"\"\"Returns the start and end times of the Blue Hour when the sun is traversing\n        in the specified direction.\n\n        This method uses the definition from PhotoPills i.e. the\n        blue hour is when the sun is between 6 and 4 degrees below the horizon.\n\n        :param direction:  Determines whether the time is for the sun rising or setting.\n                           Use ``SunDirection.RISING`` or ``SunDirection.SETTING``.\n                           Default is rising.\n\n        :param date: The date for which to calculate the times.\n                     If no date is specified then the current date will be used.\n\n        :param local: True  = Times to be returned in location's time zone;\n                      False = Times to be returned in UTC.\n                      If not specified then the time will be returned in local time\n\n        :param observer_elevation: Elevation of the observer in metres above\n                                   the location.\n\n        :return: A tuple of the date and time at which the Blue Hour starts and ends.\n        \"\"\"\n\n        if local and self.timezone is None:\n            raise ValueError(\"Local time requested but Location has no timezone set.\")\n\n        if date is None:\n            date = self.today(local)\n\n        observer = Observer(self.latitude, self.longitude, observer_elevation)\n\n        if local:\n            return astral.sun.blue_hour(observer, date, direction, self.tzinfo)\n        else:\n            return astral.sun.blue_hour(observer, date, direction)\n\n    def solar_azimuth(\n        self,\n        dateandtime: Optional[datetime.datetime] = None,\n        observer_elevation: Elevation = 0.0,\n    ) -> float:\n        \"\"\"Calculates the solar azimuth angle for a specific date/time.\n\n        :param dateandtime: The date and time for which to calculate the angle.\n        :returns: The azimuth angle in degrees clockwise from North.\n        \"\"\"\n\n        if dateandtime is None:\n            dateandtime = astral.sun.now(self.tzinfo)\n        elif not dateandtime.tzinfo:\n            dateandtime = dateandtime.replace(tzinfo=self.tzinfo)\n\n        observer = Observer(self.latitude, self.longitude, observer_elevation)\n\n        dateandtime = dateandtime.astimezone(datetime.timezone.utc)  # type: ignore\n        return astral.sun.azimuth(observer, dateandtime)\n\n    def solar_elevation(\n        self,\n        dateandtime: Optional[datetime.datetime] = None,\n        observer_elevation: Elevation = 0.0,\n    ) -> float:\n        \"\"\"Calculates the solar elevation angle for a specific time.\n\n        :param dateandtime: The date and time for which to calculate the angle.\n\n        :returns: The elevation angle in degrees above the horizon.\n        \"\"\"\n\n        if dateandtime is None:\n            dateandtime = astral.sun.now(self.tzinfo)\n        elif not dateandtime.tzinfo:\n            dateandtime = dateandtime.replace(tzinfo=self.tzinfo)\n\n        observer = Observer(self.latitude, self.longitude, observer_elevation)\n\n        dateandtime = dateandtime.astimezone(datetime.timezone.utc)  # type: ignore\n        return astral.sun.elevation(observer, dateandtime)\n\n    def solar_zenith(\n        self,\n        dateandtime: Optional[datetime.datetime] = None,\n        observer_elevation: Elevation = 0.0,\n    ) -> float:\n        \"\"\"Calculates the solar zenith angle for a specific time.\n\n        :param dateandtime: The date and time for which to calculate the angle.\n        :returns: The zenith angle in degrees from vertical.\n        \"\"\"\n\n        return 90.0 - self.solar_elevation(dateandtime, observer_elevation)\n\n    def moon_phase(self, date: Optional[datetime.date] = None, local: bool = True):\n        \"\"\"Calculates the moon phase for a specific date.\n\n        :param date:    The date to calculate the phase for. If ommitted the\n                        current date is used.\n\n        :returns:\n            A number designating the phase\n\n            ============  ==============\n            0 .. 6.99     New moon\n            7 .. 13.99    First quarter\n            14 .. 20.99   Full moon\n            21 .. 27.99   Last quarter\n            ============  ==============\n        \"\"\"\n\n        if date is None:\n            date = self.today(local)\n\n        return astral.moon.phase(date)\n"
  },
  {
    "path": "src/astral/moon.py",
    "content": "\"\"\"Moon phase, rise and set times\n\nRight ascension, declination and distance of moon calcaulation\nfrom\n\nLOW-PRECISION FORMULAE FOR PLANETARY POSITIONS\nhttp://articles.adsabs.harvard.edu/pdf/1979ApJS...41..391V\n\"\"\"\n\nimport datetime\nfrom dataclasses import dataclass, field, replace\nfrom math import asin, atan2, cos, degrees, fabs, pi, radians, sin, sqrt\nfrom typing import Callable, List, Optional, Union\n\ntry:\n    import zoneinfo\nexcept ImportError:\n    from backports import zoneinfo  # type: ignore\n\nfrom astral import AstralBodyPosition, Observer, now, today\nfrom astral.julian import julianday, julianday_2000\nfrom astral.sidereal import lmst\nfrom astral.table4 import Table4Row, table4_u, table4_v, table4_w\n\n__all__ = [\"moonrise\", \"moonset\", \"phase\"]\n\n# Using 1896 arc seconds as moon's apparent diameter\nMOON_APPARENT_RADIUS = 1896.0 / (60.0 * 60.0)\n\nDegrees = float\nRadians = float\nRevolutions = float\nArgumentFunc = Optional[Callable[[float], float]]\n\n\n@dataclass\nclass NoTransit:\n    parallax: float = field(default_factory=float)\n\n\n@dataclass\nclass TransitEvent:\n    event: str\n    when: datetime.time = field(default_factory=datetime.time)\n    azimuth: float = field(default_factory=float)\n    distance: float = field(default_factory=float)\n\n\ndef interpolate(f0: float, f1: float, f2: float, p: float) -> float:\n    \"\"\"3-point interpolation\"\"\"\n    a = f1 - f0\n    b = f2 - f1 - a\n    f = f0 + p * (2 * a + b * (2 * p - 1))\n    return f\n\n\ndef sgn(value1: Union[float, datetime.timedelta]) -> int:\n    \"\"\"Test whether value1 and value2 have the same sign\"\"\"\n    if isinstance(value1, datetime.timedelta):\n        value1 = value1.total_seconds()\n\n    if value1 < 0:\n        return -1\n    elif value1 > 0:\n        return 1\n    else:\n        return 0\n\n\ndef moon_mean_longitude(jd2000: float) -> Revolutions:\n    _mean_longitude = 0.606434 + 0.03660110129 * jd2000\n    _mean_longitude = _mean_longitude - int(_mean_longitude)\n    return _mean_longitude\n\n\ndef moon_mean_anomoly(jd2000: float) -> Revolutions:\n    _mean_anomoly = 0.374897 + 0.03629164709 * jd2000\n    _mean_anomoly = _mean_anomoly - int(_mean_anomoly)\n    return _mean_anomoly\n\n\ndef moon_argument_of_latitude(jd2000: float) -> Revolutions:\n    _argument_of_latitude = 0.259091 + 0.03674819520 * jd2000\n    _argument_of_latitude = _argument_of_latitude - int(_argument_of_latitude)\n    return _argument_of_latitude\n\n\ndef moon_mean_elongation_from_sun(jd2000: float) -> Revolutions:\n    _mean_elongation_from_sun = 0.827362 + 0.03386319198 * jd2000\n    _mean_elongation_from_sun = _mean_elongation_from_sun - int(\n        _mean_elongation_from_sun\n    )\n    return _mean_elongation_from_sun\n\n\ndef longitude_lunar_ascending_node(jd2000: float) -> Revolutions:\n    _longitude_lunar_ascending_node = moon_mean_longitude(\n        jd2000\n    ) - moon_argument_of_latitude(jd2000)\n    return _longitude_lunar_ascending_node\n\n\ndef sun_mean_longitude(jd2000: float) -> Revolutions:\n    _sun_mean_longitude = 0.779072 + 0.00273790931 * jd2000\n    _sun_mean_longitude = _sun_mean_longitude - int(_sun_mean_longitude)\n    return _sun_mean_longitude\n\n\ndef sun_mean_anomoly(jd2000: float) -> Revolutions:\n    _sun_mean_anomoly = 0.993126 + 0.00273777850 * jd2000\n    _sun_mean_anomoly = _sun_mean_anomoly - int(_sun_mean_anomoly)\n    return _sun_mean_anomoly\n\n\ndef venus_mean_longitude(jd2000: float) -> Revolutions:\n    _venus_mean_longitude = 0.505498 + 0.00445046867 * jd2000\n    _venus_mean_longitude = _venus_mean_longitude - int(_venus_mean_longitude)\n    return _venus_mean_longitude\n\n\ndef moon_position(jd2000: float) -> AstralBodyPosition:\n    \"\"\"Calculate right ascension, declination and geocentric distance for the moon\"\"\"\n\n    argument_values: List[Union[float, None]] = [\n        moon_mean_longitude(jd2000),  # 1 = Lm\n        moon_mean_anomoly(jd2000),  # 2 = Gm\n        moon_argument_of_latitude(jd2000),  # 3 = Fm\n        moon_mean_elongation_from_sun(jd2000),  # 4 = D\n        longitude_lunar_ascending_node(jd2000),  # 5 = Om\n        None,  # 6\n        sun_mean_longitude(jd2000),  # 7 = Ls\n        sun_mean_anomoly(jd2000),  # 8 = Gs\n        None,  # 9\n        None,  # 10\n        None,  # 11\n        venus_mean_longitude(jd2000),  # 12 = L2\n    ]\n\n    T = jd2000 / 36525 + 1\n\n    def _calc_value(table: List[Table4Row]) -> float:\n        result = 0.0\n        for row in table:\n            revolutions: float = 0.0\n            for arg_number, multiplier in row.argument_multiplers.items():\n                if multiplier != 0:\n                    arg_value = argument_values[arg_number - 1]\n                    if arg_value:\n                        value = arg_value * multiplier\n                        revolutions += value\n                    else:\n                        raise ValueError\n\n            t_multipler = T if row.t else 1\n            result += row.coefficient * t_multipler * row.sincos(revolutions * 2 * pi)\n\n        return result\n\n    v = _calc_value(table4_v)\n    u = _calc_value(table4_u)\n    w = _calc_value(table4_w)\n\n    s = w / sqrt(u - v * v)\n    right_ascension = asin(s) + (argument_values[0] or 0) * 2 * pi  # In radians\n\n    s = v / sqrt(u)\n    declination = asin(s)  # In radians\n\n    distance = 60.40974 * sqrt(u)  # In Earth radii (≈6378km)\n\n    return AstralBodyPosition(right_ascension, declination, distance)\n\n\ndef moon_transit_event(\n    hour: float,\n    lmst: Degrees,\n    latitude: Degrees,\n    distance: float,\n    window: List[AstralBodyPosition],\n) -> Union[TransitEvent, NoTransit]:\n    \"\"\"Check if the moon transits the horizon within the window.\n\n    Args:\n        hour: Hour of the day\n        lmst: Local mean sidereal time in degrees\n        latitude: Observer latitude\n        distance: Distance to the moon\n        window: Sliding window of moon positions that covers a part of the day\n    \"\"\"\n    mst = radians(lmst)\n    hour_angle = [0.0, 0.0, 0.0]\n\n    k1 = radians(15 * 1.0027379097096138907193594760917)\n\n    if window[2].right_ascension < window[0].right_ascension:\n        window[2].right_ascension = window[2].right_ascension + 2 * pi\n\n    hour_angle[0] = mst - window[0].right_ascension + (hour * k1)\n    hour_angle[2] = mst - window[2].right_ascension + (hour * k1) + k1\n    hour_angle[1] = (hour_angle[2] + hour_angle[0]) / 2\n\n    window[1].declination = (window[2].declination + window[0].declination) / 2\n\n    sl = sin(radians(latitude))\n    cl = cos(radians(latitude))\n\n    # moon apparent radius + parallax correction\n    z = cos(radians(90 + MOON_APPARENT_RADIUS - (41.685 / distance)))\n\n    if hour == 0:\n        window[0].distance = (\n            sl * sin(window[0].declination)\n            + cl * cos(window[0].declination) * cos(hour_angle[0])\n            - z\n        )\n\n    window[2].distance = (\n        sl * sin(window[2].declination)\n        + cl * cos(window[2].declination) * cos(hour_angle[2])\n        - z\n    )\n\n    if sgn(window[0].distance) == sgn(window[2].distance):\n        return NoTransit(window[2].distance)\n\n    window[1].distance = (\n        sl * sin(window[1].declination)\n        + cl * cos(window[1].declination) * cos(hour_angle[1])\n        - z\n    )\n\n    a = 2 * window[2].distance - 4 * window[1].distance + 2 * window[0].distance\n    b = 4 * window[1].distance - 3 * window[0].distance - window[2].distance\n    discriminant = b * b - 4 * a * window[0].distance\n\n    if discriminant < 0:\n        return NoTransit(window[2].distance)\n\n    discriminant = sqrt(discriminant)\n    e = (-b + discriminant) / (2 * a)\n    if e > 1 or e < 0:\n        e = (-b - discriminant) / (2 * a)\n\n    time = hour + e + 1 / 120\n\n    h = int(time)\n    m = int((time - h) * 60)\n\n    sd = sin(window[1].declination)\n    cd = cos(window[1].declination)\n\n    hour_angle_crossing = hour_angle[0] + e * (hour_angle[2] - hour_angle[0])\n    sh = sin(hour_angle_crossing)\n    ch = cos(hour_angle_crossing)\n\n    x = cl * sd - sl * cd * ch\n    y = -cd * sh\n\n    az = degrees(atan2(y, x))\n    if az < 0:\n        az += 360\n    if az > 360:\n        az -= 360\n\n    event_time = datetime.time(h, m, 0)\n    if window[0].distance < 0 and window[2].distance > 0:\n        return TransitEvent(\"rise\", event_time, az, window[2].distance)\n\n    if window[0].distance > 0 and window[2].distance < 0:\n        return TransitEvent(\"set\", event_time, az, window[2].distance)\n\n    return NoTransit(window[2].distance)\n\n\ndef riseset(\n    on: datetime.date,\n    observer: Observer,\n):\n    \"\"\"Calculate rise and set times\"\"\"\n    jd2000 = julianday_2000(on)\n    t0 = lmst(\n        on,\n        observer.longitude,\n    )\n\n    m: List[AstralBodyPosition] = []\n    for interval in range(3):\n        pos = moon_position(jd2000 + (interval * 0.5))\n        m.append(pos)\n\n    for interval in range(1, 3):\n        if m[interval].right_ascension <= m[interval - 1].right_ascension:\n            m[interval].right_ascension = m[interval].right_ascension + 2 * pi\n\n    moon_position_window: List[AstralBodyPosition] = [\n        replace(m[0]),  # copy m[0]\n        AstralBodyPosition(),\n        AstralBodyPosition(),\n    ]\n\n    rise_time = None\n    set_time = None\n\n    # events = []\n    for hour in range(24):\n        ph = (hour + 1) / 24\n        moon_position_window[2].right_ascension = interpolate(\n            m[0].right_ascension,\n            m[1].right_ascension,\n            m[2].right_ascension,\n            ph,\n        )\n        moon_position_window[2].declination = interpolate(\n            m[0].declination,\n            m[1].declination,\n            m[2].declination,\n            ph,\n        )\n\n        transit_info = moon_transit_event(\n            hour, t0, observer.latitude, m[1].distance, moon_position_window\n        )\n        if isinstance(transit_info, NoTransit):\n            moon_position_window[2].distance = transit_info.parallax\n        else:\n            query_time = datetime.datetime(\n                on.year, on.month, on.day, hour, 0, 0, tzinfo=datetime.timezone.utc\n            )\n\n            if transit_info.event == \"rise\":\n                event_time = transit_info.when\n                event = datetime.datetime(\n                    on.year,\n                    on.month,\n                    on.day,\n                    event_time.hour,\n                    event_time.minute,\n                    0,\n                    tzinfo=datetime.timezone.utc,\n                )\n                if rise_time is None:\n                    rise_time = event\n                else:\n                    rq_diff = (rise_time - query_time).total_seconds()\n                    eq_diff = (event - query_time).total_seconds()\n                    if set_time is not None:\n                        sq_diff = (set_time - query_time).total_seconds()\n                    else:\n                        sq_diff = 0\n\n                    update_rise_time = sgn(rq_diff) == sgn(eq_diff) and fabs(\n                        rq_diff\n                    ) > fabs(eq_diff)\n                    update_rise_time |= sgn(rq_diff) != sgn(eq_diff) and (\n                        set_time is not None and sgn(rq_diff) == sgn(sq_diff)\n                    )\n\n                    if update_rise_time:\n                        rise_time = event\n            elif transit_info.event == \"set\":\n                event_time = transit_info.when\n                event = datetime.datetime(\n                    on.year,\n                    on.month,\n                    on.day,\n                    event_time.hour,\n                    event_time.minute,\n                    0,\n                    tzinfo=datetime.timezone.utc,\n                )\n                if set_time is None:\n                    set_time = event\n                else:\n                    sq_diff = (set_time - query_time).total_seconds()\n                    eq_diff = (event - query_time).total_seconds()\n                    if rise_time is not None:\n                        rq_diff = (rise_time - query_time).total_seconds()\n                    else:\n                        rq_diff = 0\n\n                    update_set_time = sgn(sq_diff) == sgn(eq_diff) and fabs(\n                        sq_diff\n                    ) > fabs(eq_diff)\n                    update_set_time |= sgn(sq_diff) != sgn(eq_diff) and (\n                        rise_time is not None and sgn(rq_diff) == sgn(sq_diff)\n                    )\n\n                    if update_set_time:\n                        set_time = event\n\n        moon_position_window[0].right_ascension = moon_position_window[\n            2\n        ].right_ascension\n        moon_position_window[0].declination = moon_position_window[2].declination\n        moon_position_window[0].distance = moon_position_window[2].distance\n\n    return rise_time, set_time\n\n\ndef moonrise(\n    observer: Observer,\n    date: Optional[datetime.date] = None,\n    tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc,\n) -> Optional[datetime.datetime]:\n    \"\"\"Calculate the moon rise time\n\n    Args:\n        observer: Observer to calculate moonrise for\n        date:     Date to calculate for. Default is today's date in the\n                  timezone `tzinfo`.\n        tzinfo:   Timezone to return times in. Default is UTC.\n\n    Returns:\n        Date and time at which moonrise occurs.\n    \"\"\"\n    if isinstance(tzinfo, str):\n        tzinfo = zoneinfo.ZoneInfo(tzinfo)  # type: ignore\n\n    if date is None:\n        date = today(tzinfo)  # type: ignore\n    elif isinstance(date, datetime.datetime):\n        date = date.date()\n\n    info = riseset(date, observer)\n    if info[0]:\n        rise = info[0].astimezone(tzinfo)  # type: ignore\n        rd = rise.date()\n        if rd != date:\n            if rd > date:\n                delta = datetime.timedelta(days=-1)\n            else:\n                delta = datetime.timedelta(days=1)\n            new_date = date + delta\n            info = riseset(new_date, observer)\n            if info[0]:\n                rise = info[0].astimezone(tzinfo)  # type: ignore\n                rd = rise.date()\n                if rd != date:\n                    rise = None\n        return rise\n    else:\n        raise ValueError(\"Moon never rises on this date, at this location\")\n\n\ndef moonset(\n    observer: Observer,\n    date: Optional[datetime.date] = None,\n    tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc,\n) -> Optional[datetime.datetime]:\n    \"\"\"Calculate the moon set time\n\n    Args:\n        observer: Observer to calculate moonset for\n        date:     Date to calculate for. Default is today's date in the\n                  timezone `tzinfo`.\n        tzinfo:   Timezone to return times in. Default is UTC.\n\n    Returns:\n        Date and time at which moonset occurs.\n    \"\"\"\n    if isinstance(tzinfo, str):\n        tzinfo = zoneinfo.ZoneInfo(tzinfo)  # type: ignore\n\n    if date is None:\n        date = today(tzinfo)  # type: ignore\n    elif isinstance(date, datetime.datetime):\n        date = date.date()\n\n    info = riseset(date, observer)\n    if info[1]:\n        set = info[1].astimezone(tzinfo)  # type: ignore\n        sd = set.date()\n        if sd != date:\n            if sd > date:\n                delta = datetime.timedelta(days=-1)\n            else:\n                delta = datetime.timedelta(days=1)\n            new_date = date + delta\n            info = riseset(new_date, observer)\n            if info[1]:\n                set = info[1].astimezone(tzinfo)  # type: ignore\n                sd = set.date()\n                if sd != date:\n                    set = None\n        return set\n    else:\n        raise ValueError(\"Moon never sets on this date, at this location\")\n\n\ndef azimuth(\n    observer: Observer,\n    at: Optional[datetime.datetime] = None,\n) -> Degrees:\n    if at is None:\n        at = now()\n\n    jd2000 = julianday_2000(at)\n    position = moon_position(jd2000)\n    lst0: Radians = radians(lmst(at, observer.longitude))\n    hourangle: Radians = lst0 - position.right_ascension\n\n    sh = sin(hourangle)\n    ch = cos(hourangle)\n    sd = sin(position.declination)\n    cd = cos(position.declination)\n    sl = sin(radians(observer.latitude))\n    cl = cos(radians(observer.latitude))\n\n    x = -ch * cd * sl + sd * cl\n    y = -sh * cd\n    azimuth = degrees(atan2(y, x)) % 360\n    return azimuth\n\n\ndef elevation(\n    observer: Observer,\n    at: Optional[datetime.datetime] = None,\n):\n    if at is None:\n        at = now()\n\n    jd2000 = julianday_2000(at)\n    position = moon_position(jd2000)\n    lst0: Radians = radians(lmst(at, observer.longitude))\n    hourangle: Radians = lst0 - position.right_ascension\n\n    sh = sin(hourangle)\n    ch = cos(hourangle)\n    sd = sin(position.declination)\n    cd = cos(position.declination)\n    sl = sin(radians(observer.latitude))\n    cl = cos(radians(observer.latitude))\n\n    x = -ch * cd * sl + sd * cl\n    y = -sh * cd\n\n    z = ch * cd * cl + sd * sl\n    r = sqrt(x * x + y * y)\n    elevation = degrees(atan2(z, r))\n\n    return elevation\n\n\ndef zenith(\n    observer: Observer,\n    at: Optional[datetime.datetime] = None,\n):\n    return 90 - elevation(observer, at)\n\n\ndef _phase_asfloat(date: datetime.date) -> float:\n    jd = julianday(date)\n    dt = pow((jd - 2382148), 2) / (41048480 * 86400)\n    t = (jd + dt - 2451545.0) / 36525\n    t2 = pow(t, 2)\n    t3 = pow(t, 3)\n\n    d = 297.85 + (445267.1115 * t) - (0.0016300 * t2) + (t3 / 545868)\n    d = radians(d % 360.0)\n\n    m = 357.53 + (35999.0503 * t)\n    m = radians(m % 360.0)\n\n    m1 = 134.96 + (477198.8676 * t) + (0.0089970 * t2) + (t3 / 69699)\n    m1 = radians(m1 % 360.0)\n\n    elong = degrees(d) + 6.29 * sin(m1)\n    elong -= 2.10 * sin(m)\n    elong += 1.27 * sin(2 * d - m1)\n    elong += 0.66 * sin(2 * d)\n    elong = elong % 360.0\n    elong = int(elong)\n    moon = ((elong + 6.43) / 360) * 28\n    return moon\n\n\ndef phase(date: Optional[datetime.date] = None) -> float:\n    \"\"\"Calculates the phase of the moon on the specified date.\n\n    Args:\n        date: The date to calculate the phase for. Dates are always in the UTC timezone.\n              If not specified then today's date is used.\n\n    Returns:\n        A number designating the phase.\n\n        ============  ==============\n        0 .. 6.99     New moon\n        7 .. 13.99    First quarter\n        14 .. 20.99   Full moon\n        21 .. 27.99   Last quarter\n        ============  ==============\n    \"\"\"\n\n    if date is None:\n        date = today()\n\n    moon = _phase_asfloat(date)\n    if moon >= 28.0:\n        moon -= 28.0\n    return moon\n"
  },
  {
    "path": "src/astral/py.typed",
    "content": ""
  },
  {
    "path": "src/astral/sidereal.py",
    "content": "import datetime\nfrom typing import Union\n\nfrom astral.julian import julianday_2000\n\nDegrees = float\n\n\ndef gmst(at: Union[datetime.datetime, datetime.date]) -> Degrees:\n    \"\"\"Calculate Greenwich Mean Sidereal Time in degrees\"\"\"\n    jd2000 = julianday_2000(at)\n\n    t0 = jd2000 / 36525\n    value = (\n        280.46061837\n        + 360.98564736629 * jd2000\n        + 0.000387933 * pow(t0, 2)\n        + pow(t0, 3) / 38710000\n    )\n    return value % 360\n\n\ndef lmst(\n    at: Union[datetime.datetime, datetime.date],\n    longitude: Degrees,\n) -> Degrees:\n    \"\"\"Local Mean Sidereal Time for longitude in degrees\n\n    Args:\n        jd2000: Julian day\n        longitude: Longitude in degrees\n    \"\"\"\n    mst = gmst(at)\n    mst += longitude\n    return mst\n"
  },
  {
    "path": "src/astral/sun.py",
    "content": "import datetime\nfrom math import acos, asin, atan2, cos, degrees, fabs, radians, sin, sqrt, tan\nfrom typing import Dict, Optional, Tuple, Union\n\ntry:\n    import zoneinfo\nexcept ImportError:\n    from backports import zoneinfo  # type: ignore\n\nfrom astral import (\n    Depression,\n    Minutes,\n    Observer,\n    SunDirection,\n    TimePeriod,\n    now,\n    refraction_at_zenith,\n    today,\n)\nfrom astral.julian import julianday, julianday_to_juliancentury\n\n__all__ = [\n    \"sun\",\n    \"dawn\",\n    \"sunrise\",\n    \"noon\",\n    \"midnight\",\n    \"sunset\",\n    \"dusk\",\n    \"daylight\",\n    \"night\",\n    \"twilight\",\n    \"blue_hour\",\n    \"golden_hour\",\n    \"rahukaalam\",\n    \"zenith\",\n    \"azimuth\",\n    \"elevation\",\n    \"time_at_elevation\",\n]\n\n\n# Using 32 arc minutes as sun's apparent diameter\nSUN_APPARENT_RADIUS = 32.0 / (60.0 * 2.0)\n\n\n# region Backend\ndef minutes_to_timedelta(minutes: float) -> datetime.timedelta:\n    \"\"\"Convert a floating point number of minutes to a\n    :class:`~datetime.timedelta`\n    \"\"\"\n    d = int(minutes / 1440)\n    minutes = minutes - (d * 1440)\n    minutes = minutes * 60\n    s = int(minutes)\n    sfrac = minutes - s\n    us = int(sfrac * 1_000_000)\n\n    return datetime.timedelta(days=d, seconds=s, microseconds=us)\n\n\ndef geom_mean_long_sun(juliancentury: float) -> float:\n    \"\"\"Calculate the geometric mean longitude of the sun\"\"\"\n    l0 = 280.46646 + juliancentury * (36000.76983 + 0.0003032 * juliancentury)\n    return l0 % 360.0\n\n\ndef geom_mean_anomaly_sun(juliancentury: float) -> float:\n    \"\"\"Calculate the geometric mean anomaly of the sun\"\"\"\n    return 357.52911 + juliancentury * (35999.05029 - 0.0001537 * juliancentury)\n\n\ndef eccentric_location_earth_orbit(juliancentury: float) -> float:\n    \"\"\"Calculate the eccentricity of Earth's orbit\"\"\"\n    return 0.016708634 - juliancentury * (0.000042037 + 0.0000001267 * juliancentury)\n\n\ndef sun_eq_of_center(juliancentury: float) -> float:\n    \"\"\"Calculate the equation of the center of the sun\"\"\"\n    m = geom_mean_anomaly_sun(juliancentury)\n\n    mrad = radians(m)\n    sinm = sin(mrad)\n    sin2m = sin(mrad + mrad)\n    sin3m = sin(mrad + mrad + mrad)\n\n    c = (\n        sinm * (1.914602 - juliancentury * (0.004817 + 0.000014 * juliancentury))\n        + sin2m * (0.019993 - 0.000101 * juliancentury)\n        + sin3m * 0.000289\n    )\n\n    return c\n\n\ndef sun_true_long(juliancentury: float) -> float:\n    \"\"\"Calculate the sun's true longitude\"\"\"\n    l0 = geom_mean_long_sun(juliancentury)\n    c = sun_eq_of_center(juliancentury)\n\n    return l0 + c\n\n\ndef sun_true_anomoly(juliancentury: float) -> float:\n    \"\"\"Calculate the sun's true anomaly\"\"\"\n    m = geom_mean_anomaly_sun(juliancentury)\n    c = sun_eq_of_center(juliancentury)\n\n    return m + c\n\n\ndef sun_rad_vector(juliancentury: float) -> float:\n    v = sun_true_anomoly(juliancentury)\n    e = eccentric_location_earth_orbit(juliancentury)\n\n    return (1.000001018 * (1 - e * e)) / (1 + e * cos(radians(v)))\n\n\ndef sun_apparent_long(juliancentury: float) -> float:\n    true_long = sun_true_long(juliancentury)\n\n    omega = 125.04 - 1934.136 * juliancentury\n    return true_long - 0.00569 - 0.00478 * sin(radians(omega))\n\n\ndef mean_obliquity_of_ecliptic(juliancentury: float) -> float:\n    seconds = 21.448 - juliancentury * (\n        46.815 + juliancentury * (0.00059 - juliancentury * (0.001813))\n    )\n    return 23.0 + (26.0 + (seconds / 60.0)) / 60.0\n\n\ndef obliquity_correction(juliancentury: float) -> float:\n    e0 = mean_obliquity_of_ecliptic(juliancentury)\n\n    omega = 125.04 - 1934.136 * juliancentury\n    return e0 + 0.00256 * cos(radians(omega))\n\n\ndef sun_rt_ascension(juliancentury: float) -> float:\n    \"\"\"Calculate the sun's right ascension\"\"\"\n    oc = obliquity_correction(juliancentury)\n    al = sun_apparent_long(juliancentury)\n\n    tananum = cos(radians(oc)) * sin(radians(al))\n    tanadenom = cos(radians(al))\n\n    return degrees(atan2(tananum, tanadenom))\n\n\ndef sun_declination(juliancentury: float) -> float:\n    \"\"\"Calculate the sun's declination\"\"\"\n    e = obliquity_correction(juliancentury)\n    lambd = sun_apparent_long(juliancentury)\n\n    sint = sin(radians(e)) * sin(radians(lambd))\n    return degrees(asin(sint))\n\n\ndef var_y(juliancentury: float) -> float:\n    epsilon = obliquity_correction(juliancentury)\n    y = tan(radians(epsilon) / 2.0)\n    return y * y\n\n\ndef eq_of_time(juliancentury: float) -> Minutes:\n    l0 = geom_mean_long_sun(juliancentury)\n    e = eccentric_location_earth_orbit(juliancentury)\n    m = geom_mean_anomaly_sun(juliancentury)\n\n    y = var_y(juliancentury)\n\n    sin2l0 = sin(2.0 * radians(l0))\n    sinm = sin(radians(m))\n    cos2l0 = cos(2.0 * radians(l0))\n    sin4l0 = sin(4.0 * radians(l0))\n    sin2m = sin(2.0 * radians(m))\n\n    Etime = (\n        y * sin2l0\n        - 2.0 * e * sinm\n        + 4.0 * e * y * sinm * cos2l0\n        - 0.5 * y * y * sin4l0\n        - 1.25 * e * e * sin2m\n    )\n\n    return degrees(Etime) * 4.0\n\n\ndef hour_angle(\n    latitude: float, declination: float, zenith: float, direction: SunDirection\n) -> float:\n    \"\"\"Calculate the hour angle of the sun\n\n    See https://en.wikipedia.org/wiki/Hour_angle#Solar_hour_angle\n\n    Args:\n        latitude: The latitude of the obersver\n        declination: The declination of the sun\n        zenith: The zenith angle of the sun\n        direction: The direction of traversal of the sun\n\n    Raises:\n        ValueError\n    \"\"\"\n\n    latitude_rad = radians(latitude)\n    declination_rad = radians(declination)\n    zenith_rad = radians(zenith)\n\n    h = (cos(zenith_rad) - sin(latitude_rad) * sin(declination_rad)) / (\n        cos(latitude_rad) * cos(declination_rad)\n    )\n\n    hour_angle = acos(h)\n    if direction == SunDirection.SETTING:\n        hour_angle = -hour_angle\n    return hour_angle\n\n\ndef adjust_to_horizon(elevation: float) -> float:\n    \"\"\"Calculate the extra degrees of depression that you can see round the earth\n    due to the increase in elevation.\n\n    Args:\n        elevation: Elevation above the earth in metres\n\n    Returns:\n        A number of degrees to add to adjust for the elevation of the observer\n    \"\"\"\n\n    if elevation <= 0:\n        return 0\n\n    r = 6356900  # radius of the earth\n    a1 = r\n    h1 = r + elevation\n    theta1 = acos(a1 / h1)\n    return degrees(theta1)\n\n\ndef adjust_to_obscuring_feature(elevation: Tuple[float, float]) -> float:\n    \"\"\"Calculate the number of degrees to adjust for an obscuring feature\"\"\"\n    if elevation[0] == 0.0:\n        return 0.0\n\n    sign = -1 if elevation[0] < 0.0 else 1\n    return sign * degrees(\n        acos(fabs(elevation[0]) / sqrt(pow(elevation[0], 2) + pow(elevation[1], 2)))\n    )\n\n\ndef time_of_transit(\n    observer: Observer,\n    date: datetime.date,\n    zenith: float,\n    direction: SunDirection,\n    with_refraction: bool = True,\n) -> datetime.datetime:\n    \"\"\"Calculate the time in the UTC timezone when the sun transits the\n    specificed zenith\n\n    Args:\n        observer: An observer viewing the sun at a specific, latitude, longitude\n            and elevation\n        date: The date to calculate for\n        zenith: The zenith angle for which to calculate the transit time\n        direction: The direction that the sun is traversing\n\n    Raises:\n        ValueError if the zenith is not transitted by the sun\n\n    Returns:\n        the time when the sun transits the specificed zenith\n    \"\"\"\n    if observer.latitude > 89.8:\n        latitude = 89.8\n    elif observer.latitude < -89.8:\n        latitude = -89.8\n    else:\n        latitude = observer.latitude\n\n    adjustment_for_elevation = 0.0\n    if isinstance(observer.elevation, float) and observer.elevation > 0.0:\n        adjustment_for_elevation = adjust_to_horizon(observer.elevation)\n    elif isinstance(observer.elevation, tuple):\n        adjustment_for_elevation = adjust_to_obscuring_feature(observer.elevation)\n\n    if with_refraction:\n        adjustment_for_refraction = refraction_at_zenith(\n            zenith + adjustment_for_elevation\n        )\n    else:\n        adjustment_for_refraction = 0.0\n\n    jd = julianday(date)\n    adjustment = 0.0\n    timeUTC = 0.0\n\n    for _ in range(2):\n        jc = julianday_to_juliancentury(jd + adjustment)\n        declination = sun_declination(jc)\n\n        hourangle = hour_angle(\n            latitude,\n            declination,\n            zenith + adjustment_for_elevation + adjustment_for_refraction,\n            direction,\n        )\n\n        delta = -observer.longitude - degrees(hourangle)\n\n        eqtime = eq_of_time(jc)\n        offset = delta * 4.0 - eqtime\n\n        if offset < -720.0:\n            offset += 1440\n\n        timeUTC = 720.0 + offset\n        adjustment = timeUTC / 1440.0\n\n    td = minutes_to_timedelta(timeUTC)\n    dt = datetime.datetime(date.year, date.month, date.day) + td\n    dt = dt.replace(tzinfo=datetime.timezone.utc)  # pylint: disable=E1120\n    return dt\n\n\ndef time_at_elevation(\n    observer: Observer,\n    elevation: float,\n    date: Optional[datetime.date] = None,\n    direction: SunDirection = SunDirection.RISING,\n    tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc,\n    with_refraction: bool = True,\n) -> datetime.datetime:\n    \"\"\"Calculates the time when the sun is at the specified elevation on the\n    specified date.\n\n    Note:\n        This method uses positive elevations for those above the horizon.\n\n        Elevations greater than 90 degrees are converted to a setting sun\n        i.e. an elevation of 110 will calculate a setting sun at 70 degrees.\n\n    Args:\n        elevation: Elevation of the sun in degrees above the horizon to calculate for.\n        observer:  Observer to calculate for\n        date:      Date to calculate for. Default is today's date in the timezone\n                   `tzinfo`.\n        direction: Determines whether the calculated time is for the sun rising\n                   or setting.\n                   Use ``SunDirection.RISING`` or ``SunDirection.SETTING``.\n                   Default is rising.\n        tzinfo:    Timezone to return times in. Default is UTC.\n\n    Returns:\n        Date and time at which the sun is at the specified elevation.\n    \"\"\"\n\n    if elevation > 90.0:\n        elevation = 180.0 - elevation\n        direction = SunDirection.SETTING\n\n    if isinstance(tzinfo, str):\n        tzinfo = zoneinfo.ZoneInfo(tzinfo)  # type: ignore\n\n    if date is None:\n        date = today(tzinfo)  # type: ignore\n\n    zenith = 90 - elevation\n    try:\n        return time_of_transit(\n            observer, date, zenith, direction, with_refraction\n        ).astimezone(\n            tzinfo  # type: ignore\n        )\n    except ValueError as exc:\n        if exc.args[0] == \"math domain error\":\n            raise ValueError(\n                f\"Sun never reaches an elevation of {elevation} degrees \"\n                \"at this location.\"\n            ) from exc\n        else:\n            raise\n\n\ndef noon(\n    observer: Observer,\n    date: Optional[datetime.date] = None,\n    tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc,\n) -> datetime.datetime:\n    \"\"\"Calculate solar noon time when the sun is at its highest point.\n\n    Args:\n        observer: An observer viewing the sun at a specific, latitude, longitude\n                  and elevation\n        date:     Date to calculate for. Default is today for the specified tzinfo.\n        tzinfo:   Timezone to return times in. Default is UTC.\n\n    Returns:\n        Date and time at which noon occurs.\n    \"\"\"\n    if isinstance(tzinfo, str):\n        tzinfo = zoneinfo.ZoneInfo(tzinfo)  # type: ignore\n\n    if date is None:\n        date = today(tzinfo)  # type: ignore\n\n    jc = julianday_to_juliancentury(julianday(date))\n    eqtime = eq_of_time(jc)\n    timeUTC = (720.0 - (4 * observer.longitude) - eqtime) / 60.0\n\n    hour = int(timeUTC)\n    minute = int((timeUTC - hour) * 60)\n    second = int((((timeUTC - hour) * 60) - minute) * 60)\n\n    if second > 59:\n        second -= 60\n        minute += 1\n    elif second < 0:\n        second += 60\n        minute -= 1\n\n    if minute > 59:\n        minute -= 60\n        hour += 1\n    elif minute < 0:\n        minute += 60\n        hour -= 1\n\n    if hour > 23:\n        hour -= 24\n        date += datetime.timedelta(days=1)\n    elif hour < 0:\n        hour += 24\n        date -= datetime.timedelta(days=1)\n\n    noon = datetime.datetime(\n        date.year,\n        date.month,\n        date.day,\n        hour,\n        minute,\n        second,\n        tzinfo=datetime.timezone.utc,\n    )\n    return noon.astimezone(tzinfo)  # type: ignore # pylint: disable=E1120\n\n\ndef midnight(\n    observer: Observer,\n    date: Optional[datetime.date] = None,\n    tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc,\n) -> datetime.datetime:\n    \"\"\"Calculate solar midnight time.\n\n    Note:\n        This calculates the solar midnight that is closest\n        to 00:00:00 of the specified date i.e. it may return a time that is on\n        the previous day.\n\n    Args:\n        observer: An observer viewing the sun at a specific, latitude, longitude\n                  and elevation\n        date:     Date to calculate for. Default is today for the specified tzinfo.\n        tzinfo:   Timezone to return times in. Default is UTC.\n\n    Returns:\n        Date and time at which midnight occurs.\n    \"\"\"\n    if isinstance(tzinfo, str):\n        tzinfo = zoneinfo.ZoneInfo(tzinfo)  # type: ignore\n\n    if date is None:\n        date = today(tzinfo)  # type: ignore\n\n    midday = datetime.time(12, 0, 0)\n    jd = julianday(datetime.datetime.combine(date, midday))\n    newt = julianday_to_juliancentury(jd + 0.5 + -observer.longitude / 360.0)\n\n    eqtime = eq_of_time(newt)\n    timeUTC = (-observer.longitude * 4.0) - eqtime\n\n    timeUTC = timeUTC / 60.0\n    hour = int(timeUTC)\n    minute = int((timeUTC - hour) * 60)\n    second = int((((timeUTC - hour) * 60) - minute) * 60)\n\n    if second > 59:\n        second -= 60\n        minute += 1\n    elif second < 0:\n        second += 60\n        minute -= 1\n\n    if minute > 59:\n        minute -= 60\n        hour += 1\n    elif minute < 0:\n        minute += 60\n        hour -= 1\n\n    if hour < 0:\n        hour += 24\n        date -= datetime.timedelta(days=1)\n\n    midnight = datetime.datetime(\n        date.year,\n        date.month,\n        date.day,\n        hour,\n        minute,\n        second,\n        tzinfo=datetime.timezone.utc,\n    )\n    return midnight.astimezone(tzinfo)  # type: ignore\n\n\ndef zenith_and_azimuth(\n    observer: Observer,\n    dateandtime: datetime.datetime,\n    with_refraction: bool = True,\n) -> Tuple[float, float]:\n    if observer.latitude > 89.8:\n        latitude = 89.8\n    elif observer.latitude < -89.8:\n        latitude = -89.8\n    else:\n        latitude = observer.latitude\n\n    longitude = observer.longitude\n\n    if dateandtime.tzinfo is None:\n        zone = 0.0\n        utc_datetime = dateandtime\n    else:\n        zone = -dateandtime.utcoffset().total_seconds() / 3600.0  # type: ignore\n        utc_datetime = dateandtime.astimezone(datetime.timezone.utc)\n\n    jd = julianday(utc_datetime)\n    t = julianday_to_juliancentury(jd)\n    declination = sun_declination(t)\n    eqtime = eq_of_time(t)\n\n    # 360deg * 4 == 1440 minutes, 60*24 = 1440 minutes == 1 rotation\n    solarTimeFix = eqtime + (4.0 * longitude) + (60 * zone)\n    trueSolarTime = (\n        dateandtime.hour * 60.0\n        + dateandtime.minute\n        + dateandtime.second / 60.0\n        + solarTimeFix\n    )\n    #    in minutes as a float, fractional part is seconds\n\n    while trueSolarTime > 1440:\n        trueSolarTime = trueSolarTime - 1440\n\n    hourangle = trueSolarTime / 4.0 - 180.0\n    #    Thanks to Louis Schwarzmayr for the next line:\n    if hourangle < -180:\n        hourangle = hourangle + 360.0\n\n    ch = cos(radians(hourangle))\n    # sh = sin(radians(hourangle))\n    cl = cos(radians(latitude))\n    sl = sin(radians(latitude))\n    sd = sin(radians(declination))\n    cd = cos(radians(declination))\n\n    csz = cl * cd * ch + sl * sd\n\n    if csz > 1.0:\n        csz = 1.0\n    elif csz < -1.0:\n        csz = -1.0\n\n    zenith = degrees(acos(csz))\n\n    azDenom = cl * sin(radians(zenith))\n\n    if abs(azDenom) > 0.001:\n        azRad = ((sl * cos(radians(zenith))) - sd) / azDenom\n\n        if abs(azRad) > 1.0:\n            if azRad < 0:\n                azRad = -1.0\n            else:\n                azRad = 1.0\n\n        azimuth = 180.0 - degrees(acos(azRad))\n\n        if hourangle > 0.0:\n            azimuth = -azimuth\n    else:\n        if latitude > 0.0:\n            azimuth = 180.0\n        else:\n            azimuth = 0.0\n\n    if azimuth < 0.0:\n        azimuth = azimuth + 360.0\n\n    if with_refraction:\n        zenith -= refraction_at_zenith(zenith)\n        # elevation = 90 - zenith\n\n    return zenith, azimuth\n\n\ndef zenith(\n    observer: Observer,\n    dateandtime: Optional[datetime.datetime] = None,\n    with_refraction: bool = True,\n) -> float:\n    \"\"\"Calculate the zenith angle of the sun.\n\n    Args:\n        observer:    Observer to calculate the solar zenith for\n        dateandtime: The date and time for which to calculate the angle.\n                     If `dateandtime` is None or is a naive Python datetime\n                     then it is assumed to be in the UTC timezone.\n        with_refraction: If True adjust zenith to take refraction into account\n\n    Returns:\n        The zenith angle in degrees.\n    \"\"\"\n\n    if dateandtime is None:\n        dateandtime = now(datetime.timezone.utc)\n\n    return zenith_and_azimuth(observer, dateandtime, with_refraction)[0]\n\n\ndef azimuth(\n    observer: Observer,\n    dateandtime: Optional[datetime.datetime] = None,\n) -> float:\n    \"\"\"Calculate the azimuth angle of the sun.\n\n    Args:\n        observer:    Observer to calculate the solar azimuth for\n        dateandtime: The date and time for which to calculate the angle.\n                     If `dateandtime` is None or is a naive Python datetime\n                     then it is assumed to be in the UTC timezone.\n\n    Returns:\n        The azimuth angle in degrees clockwise from North.\n\n    If `dateandtime` is a naive Python datetime then it is assumed to be\n    in the UTC timezone.\n    \"\"\"\n\n    if dateandtime is None:\n        dateandtime = now(datetime.timezone.utc)\n\n    return zenith_and_azimuth(observer, dateandtime)[1]\n\n\ndef elevation(\n    observer: Observer,\n    dateandtime: Optional[datetime.datetime] = None,\n    with_refraction: bool = True,\n) -> float:\n    \"\"\"Calculate the sun's angle of elevation.\n\n    Args:\n        observer:    Observer to calculate the solar elevation for\n        dateandtime: The date and time for which to calculate the angle.\n                     If `dateandtime` is None or is a naive Python datetime\n                     then it is assumed to be in the UTC timezone.\n        with_refraction: If True adjust elevation to take refraction into account\n\n    Returns:\n        The elevation angle in degrees above the horizon.\n    \"\"\"\n\n    if dateandtime is None:\n        dateandtime = now(datetime.timezone.utc)\n\n    return 90.0 - zenith(observer, dateandtime, with_refraction)\n\n\ndef dawn(\n    observer: Observer,\n    date: Optional[datetime.date] = None,\n    depression: Union[float, Depression] = Depression.CIVIL,\n    tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc,\n) -> datetime.datetime:\n    \"\"\"Calculate dawn time.\n\n    Args:\n        observer:   Observer to calculate dawn for\n        date:       Date to calculate for. Default is today's date in the\n                    timezone `tzinfo`.\n        depression: Number of degrees below the horizon to use to calculate dawn.\n                    Default is for Civil dawn i.e. 6.0\n        tzinfo:     Timezone to return times in. Default is UTC.\n\n    Returns:\n        Date and time at which dawn occurs.\n\n    Raises:\n        ValueError: if dawn does not occur on the specified date\n    \"\"\"\n    if isinstance(tzinfo, str):\n        tzinfo = zoneinfo.ZoneInfo(tzinfo)  # type: ignore\n\n    if date is None:\n        date = today(tzinfo)  # type: ignore\n    elif isinstance(date, datetime.datetime):\n        tzinfo = date.tzinfo or tzinfo\n        date = date.date()\n\n    dep: float = 0.0\n    if isinstance(depression, Depression):\n        dep = depression.value\n    else:\n        dep = depression\n\n    try:\n        tot = time_of_transit(\n            observer, date, 90.0 + dep, SunDirection.RISING\n        ).astimezone(\n            tzinfo  # type: ignore\n        )\n\n        # If the dates don't match search on either the next or previous day.\n        tot_date = tot.date()\n        if tot_date != date:\n            if tot_date < date:\n                delta = datetime.timedelta(days=1)\n            else:\n                delta = datetime.timedelta(days=-1)\n            new_date = date + delta\n\n            tot = time_of_transit(\n                observer,\n                new_date,\n                90.0 + dep,\n                SunDirection.RISING,\n            ).astimezone(\n                tzinfo  # type: ignore\n            )\n            # Still can't get a time then raise the error\n            tot_date = tot.date()\n            if tot_date != date:\n                raise ValueError(\"Unable to find a dawn time on the date specified\")\n        return tot\n    except ValueError as exc:\n        if exc.args[0] == \"math domain error\":\n            raise ValueError(\n                f\"Sun never reaches {dep} degrees below the horizon, at this location.\"\n            ) from exc\n        else:\n            raise\n\n\ndef sunrise(\n    observer: Observer,\n    date: Optional[datetime.date] = None,\n    tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc,\n) -> datetime.datetime:\n    \"\"\"Calculate sunrise time.\n\n    Args:\n        observer: Observer to calculate sunrise for\n        date:     Date to calculate for. Default is today's date in the\n                  timezone `tzinfo`.\n        tzinfo:   Timezone to return times in. Default is UTC.\n\n    Returns:\n        Date and time at which sunrise occurs.\n\n    Raises:\n        ValueError: if the sun does not reach the horizon on the specified date\n    \"\"\"\n    if isinstance(tzinfo, str):\n        tzinfo = zoneinfo.ZoneInfo(tzinfo)  # type: ignore\n\n    if date is None:\n        date = today(tzinfo)  # type: ignore\n    elif isinstance(date, datetime.datetime):\n        tzinfo = date.tzinfo or tzinfo\n        date = date.date()\n\n    try:\n        tot = time_of_transit(\n            observer,\n            date,\n            90.0 + SUN_APPARENT_RADIUS,\n            SunDirection.RISING,\n        ).astimezone(\n            tzinfo  # type: ignore\n        )\n\n        tot_date = tot.date()\n        if tot_date != date:\n            if tot_date < date:\n                delta = datetime.timedelta(days=1)\n            else:\n                delta = datetime.timedelta(days=-1)\n            new_date = date + delta\n\n            tot = time_of_transit(\n                observer,\n                new_date,\n                90.0 + SUN_APPARENT_RADIUS,\n                SunDirection.RISING,\n            ).astimezone(\n                tzinfo  # type: ignore\n            )\n            tot_date = tot.date()\n            if tot_date != date:\n                raise ValueError(\"Unable to find a sunrise time on the date specified\")\n        return tot\n    except ValueError as exc:\n        if exc.args[0] == \"math domain error\":\n            z = zenith(observer, noon(observer, date))\n            if z > 90.0:\n                msg = \"Sun is always below the horizon on this day, at this location.\"\n            else:\n                msg = \"Sun is always above the horizon on this day, at this location.\"\n            raise ValueError(msg) from exc\n        else:\n            raise\n\n\ndef sunset(\n    observer: Observer,\n    date: Optional[datetime.date] = None,\n    tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc,\n) -> datetime.datetime:\n    \"\"\"Calculate sunset time.\n\n    Args:\n        observer: Observer to calculate sunset for\n        date:     Date to calculate for. Default is today's date in the\n                  timezone `tzinfo`.\n        tzinfo:   Timezone to return times in. Default is UTC.\n\n    Returns:\n        Date and time at which sunset occurs.\n\n    Raises:\n        ValueError: if the sun does not reach the horizon\n    \"\"\"\n\n    if isinstance(tzinfo, str):\n        tzinfo = zoneinfo.ZoneInfo(tzinfo)  # type: ignore\n\n    if date is None:\n        date = today(tzinfo)  # type: ignore\n    elif isinstance(date, datetime.datetime):\n        tzinfo = date.tzinfo or tzinfo\n        date = date.date()\n\n    try:\n        tot = time_of_transit(\n            observer,\n            date,\n            90.0 + SUN_APPARENT_RADIUS,\n            SunDirection.SETTING,\n        ).astimezone(\n            tzinfo  # type: ignore\n        )\n\n        tot_date = tot.date()\n        if tot_date != date:\n            if tot_date < date:\n                delta = datetime.timedelta(days=1)\n            else:\n                delta = datetime.timedelta(days=-1)\n            new_date = date + delta\n\n            tot = time_of_transit(\n                observer,\n                new_date,\n                90.0 + SUN_APPARENT_RADIUS,\n                SunDirection.SETTING,\n            ).astimezone(\n                tzinfo  # type: ignore\n            )\n            tot_date = tot.date()\n            if tot_date != date:\n                raise ValueError(\"Unable to find a sunset time on the date specified\")\n        return tot\n    except ValueError as exc:\n        if exc.args[0] == \"math domain error\":\n            z = zenith(observer, noon(observer, date))\n            if z > 90.0:\n                msg = \"Sun is always below the horizon on this day, at this location.\"\n            else:\n                msg = \"Sun is always above the horizon on this day, at this location.\"\n            raise ValueError(msg) from exc\n        else:\n            raise\n\n\ndef dusk(\n    observer: Observer,\n    date: Optional[datetime.date] = None,\n    depression: Union[float, Depression] = Depression.CIVIL,\n    tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc,\n) -> datetime.datetime:\n    \"\"\"Calculate dusk time.\n\n    Args:\n        observer:   Observer to calculate dusk for\n        date:       Date to calculate for. Default is today's date in the\n                    timezone `tzinfo`.\n        depression: Number of degrees below the horizon to use to calculate dusk.\n                    Default is for Civil dusk i.e. 6.0\n        tzinfo:     Timezone to return times in. Default is UTC.\n\n    Returns:\n        Date and time at which dusk occurs.\n\n    Raises:\n        ValueError: if dusk does not occur on the specified date\n    \"\"\"\n\n    if isinstance(tzinfo, str):\n        tzinfo = zoneinfo.ZoneInfo(tzinfo)  # type: ignore\n\n    if date is None:\n        date = today(tzinfo)  # type: ignore\n    elif isinstance(date, datetime.datetime):\n        tzinfo = date.tzinfo or tzinfo\n        date = date.date()\n\n    dep: float = 0.0\n    if isinstance(depression, Depression):\n        dep = depression.value\n    else:\n        dep = depression\n\n    try:\n        tot = time_of_transit(\n            observer, date, 90.0 + dep, SunDirection.SETTING\n        ).astimezone(\n            tzinfo  # type: ignore\n        )\n\n        tot_date = tot.date()\n        if tot_date != date:\n            if tot_date < date:\n                delta = datetime.timedelta(days=1)\n            else:\n                delta = datetime.timedelta(days=-1)\n            new_date = date + delta\n\n            tot = time_of_transit(\n                observer,\n                new_date,\n                90.0 + dep,\n                SunDirection.SETTING,\n            ).astimezone(\n                tzinfo  # type: ignore\n            )\n            tot_date = tot.date()\n            if tot_date != date:\n                raise ValueError(\"Unable to find a dusk time on the date specified\")\n        return tot\n    except ValueError as exc:\n        if exc.args[0] == \"math domain error\":\n            raise ValueError(\n                f\"Sun never reaches {dep} degrees below the horizon, at this location.\"\n            ) from exc\n        else:\n            raise\n\n\ndef daylight(\n    observer: Observer,\n    date: Optional[datetime.date] = None,\n    tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc,\n) -> TimePeriod:\n    \"\"\"Calculate daylight start and end times.\n\n    Args:\n        observer:   Observer to calculate daylight for\n        date:       Date to calculate for. Default is today's date in the\n                    timezone `tzinfo`.\n        tzinfo:     Timezone to return times in. Default is UTC.\n\n    Returns:\n        A tuple of the date and time at which daylight starts and ends.\n\n    Raises:\n        ValueError: if the sun does not rise or does not set\n    \"\"\"\n    if isinstance(tzinfo, str):\n        tzinfo = zoneinfo.ZoneInfo(tzinfo)  # type: ignore\n\n    if date is None:\n        date = today(tzinfo)  # type: ignore\n\n    sr = sunrise(observer, date, tzinfo)\n    ss = sunset(observer, date, tzinfo)\n\n    return sr, ss\n\n\ndef night(\n    observer: Observer,\n    date: Optional[datetime.date] = None,\n    tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc,\n) -> TimePeriod:\n    \"\"\"Calculate night start and end times.\n\n    Night is calculated to be between astronomical dusk on the\n    date specified and astronomical dawn of the next day.\n\n    Args:\n        observer:   Observer to calculate night for\n        date:       Date to calculate for. Default is today's date for the\n                    specified tzinfo.\n        tzinfo:     Timezone to return times in. Default is UTC.\n\n    Returns:\n        A tuple of the date and time at which night starts and ends.\n\n    Raises:\n        ValueError: if dawn does not occur on the specified date or\n                    dusk on the following day\n    \"\"\"\n    if isinstance(tzinfo, str):\n        tzinfo = zoneinfo.ZoneInfo(tzinfo)  # type: ignore\n\n    if date is None:\n        date = today(tzinfo)  # type: ignore\n\n    start = dusk(observer, date, 6, tzinfo)\n    tomorrow = date + datetime.timedelta(days=1)\n    end = dawn(observer, tomorrow, 6, tzinfo)\n\n    return start, end\n\n\ndef twilight(\n    observer: Observer,\n    date: Optional[datetime.date] = None,\n    direction: SunDirection = SunDirection.RISING,\n    tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc,\n) -> TimePeriod:\n    \"\"\"Returns the start and end times of Twilight\n    when the sun is traversing in the specified direction.\n\n    This method defines twilight as being between the time\n    when the sun is at -6 degrees and sunrise/sunset.\n\n    Args:\n        observer:   Observer to calculate twilight for\n        date:       Date for which to calculate the times.\n                    Default is today's date in the timezone `tzinfo`.\n        direction:  Determines whether the time is for the sun rising or setting.\n                    Use ``astral.SunDirection.RISING`` or\n                    ``astral.SunDirection.SETTING``.\n        tzinfo:     Timezone to return times in. Default is UTC.\n\n    Returns:\n        A tuple of the date and time at which twilight starts and ends.\n\n    Raises:\n        ValueError: if the sun does not rise or does not set\n    \"\"\"\n\n    if isinstance(tzinfo, str):\n        tzinfo = zoneinfo.ZoneInfo(tzinfo)  # type: ignore\n\n    if date is None:\n        date = today(tzinfo)  # type: ignore\n\n    start = time_of_transit(observer, date, 90 + 6, direction,).astimezone(\n        tzinfo  # type: ignore\n    )\n    if direction == SunDirection.RISING:\n        end = sunrise(observer, date, tzinfo).astimezone(tzinfo)  # type: ignore\n    else:\n        end = sunset(observer, date, tzinfo).astimezone(tzinfo)  # type: ignore\n\n    if direction == SunDirection.RISING:\n        return start, end\n    else:\n        return end, start\n\n\ndef golden_hour(\n    observer: Observer,\n    date: Optional[datetime.date] = None,\n    direction: SunDirection = SunDirection.RISING,\n    tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc,\n) -> TimePeriod:\n    \"\"\"Returns the start and end times of the Golden Hour\n    when the sun is traversing in the specified direction.\n\n    This method uses the definition from PhotoPills i.e. the\n    golden hour is when the sun is between 4 degrees below the horizon\n    and 6 degrees above.\n\n    Args:\n        observer:   Observer to calculate the golden hour for\n        date:       Date for which to calculate the times.\n                    Default is today's date in the timezone `tzinfo`.\n        direction:  Determines whether the time is for the sun rising or setting.\n                    Use ``SunDirection.RISING`` or ``SunDirection.SETTING``.\n        tzinfo:     Timezone to return times in. Default is UTC.\n\n    Returns:\n        A tuple of the date and time at which the Golden Hour starts and ends.\n\n    Raises:\n        ValueError: if the sun does not transit the elevations -4 & +6 degrees\n    \"\"\"\n\n    if isinstance(tzinfo, str):\n        tzinfo = zoneinfo.ZoneInfo(tzinfo)  # type: ignore\n\n    if date is None:\n        date = today(tzinfo)  # type: ignore\n\n    start = time_of_transit(observer, date, 90 + 4, direction,).astimezone(\n        tzinfo  # type: ignore\n    )\n    end = time_of_transit(observer, date, 90 - 6, direction,).astimezone(\n        tzinfo  # type: ignore\n    )\n\n    if direction == SunDirection.RISING:\n        return start, end\n    else:\n        return end, start\n\n\ndef blue_hour(\n    observer: Observer,\n    date: Optional[datetime.date] = None,\n    direction: SunDirection = SunDirection.RISING,\n    tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc,\n) -> TimePeriod:\n    \"\"\"Returns the start and end times of the Blue Hour\n    when the sun is traversing in the specified direction.\n\n    This method uses the definition from PhotoPills i.e. the\n    blue hour is when the sun is between 6 and 4 degrees below the horizon.\n\n    Args:\n        observer:   Observer to calculate the blue hour for\n        date:       Date for which to calculate the times.\n                    Default is today's date in the timezone `tzinfo`.\n        direction:  Determines whether the time is for the sun rising or setting.\n                    Use ``SunDirection.RISING`` or ``SunDirection.SETTING``.\n        tzinfo:     Timezone to return times in. Default is UTC.\n\n    Returns:\n        A tuple of the date and time at which the Blue Hour starts and ends.\n\n    Raises:\n        ValueError: if the sun does not transit the elevations -4 & -6 degrees\n    \"\"\"\n\n    if isinstance(tzinfo, str):\n        tzinfo = zoneinfo.ZoneInfo(tzinfo)  # type: ignore\n\n    if date is None:\n        date = today(tzinfo)  # type: ignore\n\n    start = time_of_transit(observer, date, 90 + 6, direction,).astimezone(\n        tzinfo  # type: ignore\n    )\n    end = time_of_transit(observer, date, 90 + 4, direction,).astimezone(\n        tzinfo  # type: ignore\n    )\n\n    if direction == SunDirection.RISING:\n        return start, end\n    else:\n        return end, start\n\n\ndef rahukaalam(\n    observer: Observer,\n    date: Optional[datetime.date] = None,\n    daytime: bool = True,\n    tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc,\n) -> TimePeriod:\n    \"\"\"Calculate ruhakaalam times.\n\n    Args:\n        observer:   Observer to calculate rahukaalam for\n        date:       Date to calculate for. Default is today's date in the\n                    timezone `tzinfo`.\n        daytime:    If True calculate for the day time else calculate for the\n                    night time.\n        tzinfo:     Timezone to return times in. Default is UTC.\n\n    Returns:\n        Tuple containing the start and end times for Rahukaalam.\n\n    Raises:\n        ValueError: if the sun does not rise or does not set\n    \"\"\"\n\n    if isinstance(tzinfo, str):\n        tzinfo = zoneinfo.ZoneInfo(tzinfo)  # type: ignore\n\n    if date is None:\n        date = today(tzinfo)  # type: ignore\n\n    if daytime:\n        start = sunrise(observer, date, tzinfo)\n        end = sunset(observer, date, tzinfo)\n    else:\n        start = sunset(observer, date, tzinfo)\n        oneday = datetime.timedelta(days=1)\n        end = sunrise(observer, date + oneday, tzinfo)\n\n    octant_duration = datetime.timedelta(seconds=(end - start).seconds / 8)\n\n    # Mo,Sa,Fr,We,Th,Tu,Su\n    octant_index = [1, 6, 4, 5, 3, 2, 7]\n\n    weekday = date.weekday()\n    octant = octant_index[weekday]\n\n    start = start + (octant_duration * octant)\n    end = start + octant_duration\n\n    return start, end\n\n\ndef sun(\n    observer: Observer,\n    date: Optional[datetime.date] = None,\n    dawn_dusk_depression: Union[float, Depression] = Depression.CIVIL,\n    tzinfo: Union[str, datetime.tzinfo] = datetime.timezone.utc,\n) -> Dict[str, datetime.datetime]:\n    \"\"\"Calculate all the info for the sun at once.\n\n    Args:\n        observer:             Observer for which to calculate the times of the sun\n        date:                 Date to calculate for.\n                              Default is today's date in the timezone `tzinfo`.\n        dawn_dusk_depression: Depression to use to calculate dawn and dusk.\n                              Default is for Civil dusk i.e. 6.0\n        tzinfo:               Timezone to return times in. Default is UTC.\n\n    Returns:\n        Dictionary with keys ``dawn``, ``sunrise``, ``noon``, ``sunset`` and ``dusk``\n        whose values are the results of the corresponding functions.\n\n    Raises:\n        ValueError: if passed through from any of the functions\n    \"\"\"\n\n    if isinstance(tzinfo, str):\n        tzinfo = zoneinfo.ZoneInfo(tzinfo)  # type: ignore\n\n    if date is None:\n        date = today(tzinfo)  # type: ignore\n\n    return {\n        \"dawn\": dawn(observer, date, dawn_dusk_depression, tzinfo),\n        \"sunrise\": sunrise(observer, date, tzinfo),\n        \"noon\": noon(observer, date, tzinfo),\n        \"sunset\": sunset(observer, date, tzinfo),\n        \"dusk\": dusk(observer, date, dawn_dusk_depression, tzinfo),\n    }\n"
  },
  {
    "path": "src/astral/table4.py",
    "content": "from math import cos, sin\nfrom typing import Callable, Dict, List, NamedTuple\n\n\nclass Table4Row(NamedTuple):\n    coefficient: float\n    t: bool\n    sincos: Callable[[float], float]\n    argument_multiplers: Dict[int, int]\n\n\nGm = 2  # Moon mean anomoly\nFm = 3  # Moon argument of latitude\nD = 4  # Moon mean elongation from sun\nOm = 5  # Longitude of the lunar ascending node\nLs = 7  # Sun mean longitude\nGs = 8  # Sun mean anomoly\nL2 = 12  # Venus mean longitude\n\ntable4_v: List[Table4Row] = [\n    Table4Row(0.39558, False, sin, {Gm: 0, Fm: 1, D: 0, Om: 1, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.08200, False, sin, {Gm: 0, Fm: 1, D: 0, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.03257, False, sin, {Gm: 1, Fm: -1, D: 0, Om: -1, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.01092, False, sin, {Gm: 1, Fm: 1, D: 0, Om: 1, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00666, False, sin, {Gm: 1, Fm: -1, D: 0, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00644, False, sin, {Gm: 1, Fm: 1, D: -2, Om: 1, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00331, False, sin, {Gm: 0, Fm: 1, D: -2, Om: 1, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00304, False, sin, {Gm: 0, Fm: 1, D: -2, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(\n        -0.00240, False, sin, {Gm: 1, Fm: -1, D: -2, Om: -1, Ls: 0, Gs: 0, L2: 0}\n    ),\n    Table4Row(0.00226, False, sin, {Gm: 1, Fm: 1, D: 0, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00108, False, sin, {Gm: 1, Fm: 1, D: -2, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00079, False, sin, {Gm: 0, Fm: 1, D: 0, Om: -1, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00078, False, sin, {Gm: 0, Fm: 1, D: 2, Om: 1, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00066, False, sin, {Gm: 0, Fm: 1, D: 0, Om: 1, Ls: 0, Gs: -1, L2: 0}),\n    Table4Row(-0.00062, False, sin, {Gm: 0, Fm: 1, D: 0, Om: 1, Ls: 0, Gs: 1, L2: 0}),\n    Table4Row(-0.00050, False, sin, {Gm: 1, Fm: -1, D: -2, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00045, False, sin, {Gm: 2, Fm: 1, D: 0, Om: 1, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00031, False, sin, {Gm: 2, Fm: 1, D: -2, Om: 1, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00027, False, sin, {Gm: 1, Fm: 1, D: -2, Om: 1, Ls: 0, Gs: 1, L2: 0}),\n    Table4Row(-0.00024, False, sin, {Gm: 0, Fm: 1, D: -2, Om: 1, Ls: 0, Gs: 1, L2: 0}),\n    Table4Row(-0.00021, True, sin, {Gm: 0, Fm: 1, D: 0, Om: 1, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00018, False, sin, {Gm: 0, Fm: 1, D: -1, Om: 1, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00016, False, sin, {Gm: 0, Fm: 1, D: 2, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00016, False, sin, {Gm: 1, Fm: -1, D: 0, Om: -1, Ls: 0, Gs: -1, L2: 0}),\n    Table4Row(-0.00016, False, sin, {Gm: 2, Fm: -1, D: 0, Om: -1, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00015, False, sin, {Gm: 0, Fm: 1, D: -2, Om: 0, Ls: 0, Gs: 1, L2: 0}),\n    Table4Row(\n        -0.00012, False, sin, {Gm: 1, Fm: -1, D: -2, Om: -1, Ls: 0, Gs: 1, L2: 0}\n    ),\n    Table4Row(-0.00011, False, sin, {Gm: 1, Fm: -1, D: 0, Om: -1, Ls: 0, Gs: 1, L2: 0}),\n    Table4Row(0.00009, False, sin, {Gm: 1, Fm: 1, D: 0, Om: 1, Ls: 0, Gs: -1, L2: 0}),\n    Table4Row(0.00009, False, sin, {Gm: 2, Fm: 1, D: 0, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00008, False, sin, {Gm: 2, Fm: -1, D: 0, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00008, False, sin, {Gm: 1, Fm: 1, D: 2, Om: 1, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00008, False, sin, {Gm: 0, Fm: 3, D: -2, Om: 1, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00007, False, sin, {Gm: 1, Fm: -1, D: 2, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(\n        -0.00007, False, sin, {Gm: 2, Fm: -1, D: -2, Om: -1, Ls: 0, Gs: 0, L2: 0}\n    ),\n    Table4Row(-0.00007, False, sin, {Gm: 1, Fm: 1, D: 0, Om: 1, Ls: 0, Gs: 1, L2: 0}),\n    Table4Row(-0.00006, False, sin, {Gm: 0, Fm: 1, D: 1, Om: 1, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00006, False, sin, {Gm: 0, Fm: 1, D: -2, Om: 0, Ls: 0, Gs: -1, L2: 0}),\n    Table4Row(0.00006, False, sin, {Gm: 1, Fm: -1, D: 0, Om: 1, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00006, False, sin, {Gm: 0, Fm: 1, D: 2, Om: 1, Ls: 0, Gs: -1, L2: 0}),\n    Table4Row(-0.00005, False, sin, {Gm: 1, Fm: 1, D: -2, Om: 0, Ls: 0, Gs: 1, L2: 0}),\n    Table4Row(-0.00004, False, sin, {Gm: 2, Fm: 1, D: -2, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00004, False, sin, {Gm: 1, Fm: -3, D: 0, Om: -1, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00004, False, sin, {Gm: 1, Fm: -1, D: 0, Om: 0, Ls: 0, Gs: -1, L2: 0}),\n    Table4Row(-0.00003, False, sin, {Gm: 1, Fm: -1, D: 0, Om: 0, Ls: 0, Gs: 1, L2: 0}),\n    Table4Row(0.00003, False, sin, {Gm: 0, Fm: 1, D: -1, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00003, False, sin, {Gm: 0, Fm: 1, D: -2, Om: 1, Ls: 0, Gs: -1, L2: 0}),\n    Table4Row(-0.00003, False, sin, {Gm: 0, Fm: 1, D: -2, Om: -1, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00003, False, sin, {Gm: 1, Fm: 1, D: -2, Om: 1, Ls: 0, Gs: -1, L2: 0}),\n    Table4Row(0.00003, False, sin, {Gm: 0, Fm: 1, D: 0, Om: 0, Ls: 0, Gs: -1, L2: 0}),\n    Table4Row(-0.00003, False, sin, {Gm: 0, Fm: 1, D: -1, Om: 1, Ls: 0, Gs: -1, L2: 0}),\n    Table4Row(-0.00002, False, sin, {Gm: 1, Fm: -1, D: -2, Om: 0, Ls: 0, Gs: 1, L2: 0}),\n    Table4Row(-0.00002, False, sin, {Gm: 0, Fm: 1, D: 0, Om: 0, Ls: 0, Gs: 1, L2: 0}),\n    Table4Row(0.00002, False, sin, {Gm: 1, Fm: 1, D: -1, Om: 1, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00002, False, sin, {Gm: 1, Fm: 1, D: 0, Om: -1, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00002, False, sin, {Gm: 3, Fm: 1, D: 0, Om: 1, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(\n        -0.00002, False, sin, {Gm: 2, Fm: -1, D: -4, Om: -1, Ls: 0, Gs: 0, L2: 0}\n    ),\n    Table4Row(\n        0.00002, False, sin, {Gm: 1, Fm: -1, D: -2, Om: -1, Ls: 0, Gs: -1, L2: 0}\n    ),\n    Table4Row(-0.00002, True, sin, {Gm: 1, Fm: -1, D: 0, Om: -1, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(\n        -0.00002, False, sin, {Gm: 1, Fm: -1, D: -4, Om: -1, Ls: 0, Gs: 0, L2: 0}\n    ),\n    Table4Row(-0.00002, False, sin, {Gm: 1, Fm: 1, D: -4, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00002, False, sin, {Gm: 2, Fm: -1, D: -2, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00002, False, sin, {Gm: 1, Fm: 1, D: 2, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00002, False, sin, {Gm: 1, Fm: 1, D: 0, Om: 0, Ls: 0, Gs: -1, L2: 0}),\n]\n\ntable4_u: List[Table4Row] = [\n    Table4Row(1, False, cos, {Gm: 0, Fm: 0, D: 0, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.10828, False, cos, {Gm: 1, Fm: 0, D: 0, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.01880, False, cos, {Gm: 1, Fm: 0, D: -2, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.01479, False, cos, {Gm: 0, Fm: 0, D: 2, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00181, False, cos, {Gm: 2, Fm: 0, D: -2, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00147, False, cos, {Gm: 2, Fm: 0, D: 0, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00105, False, cos, {Gm: 0, Fm: 0, D: 2, Om: 0, Ls: 0, Gs: -1, L2: 0}),\n    Table4Row(-0.00075, False, cos, {Gm: 1, Fm: 0, D: -2, Om: 0, Ls: 0, Gs: 1, L2: 0}),\n    Table4Row(-0.00067, False, cos, {Gm: 1, Fm: 0, D: 0, Om: 0, Ls: 0, Gs: -1, L2: 0}),\n    Table4Row(0.00057, False, cos, {Gm: 0, Fm: 0, D: 1, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00055, False, cos, {Gm: 1, Fm: 0, D: 0, Om: 0, Ls: 0, Gs: 1, L2: 0}),\n    Table4Row(-0.00046, False, cos, {Gm: 1, Fm: 0, D: 2, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00041, False, cos, {Gm: 1, Fm: -2, D: 0, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00024, False, cos, {Gm: 0, Fm: 0, D: 0, Om: 0, Ls: 0, Gs: 1, L2: 0}),\n    Table4Row(0.00017, False, cos, {Gm: 0, Fm: 0, D: 2, Om: 0, Ls: 0, Gs: 1, L2: 0}),\n    Table4Row(0.00013, False, cos, {Gm: 1, Fm: 0, D: -2, Om: 0, Ls: 0, Gs: -1, L2: 0}),\n    Table4Row(-0.00010, False, cos, {Gm: 1, Fm: 0, D: -4, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00009, False, cos, {Gm: 0, Fm: 0, D: 1, Om: 0, Ls: 0, Gs: 1, L2: 0}),\n    Table4Row(0.00007, False, cos, {Gm: 2, Fm: 0, D: -2, Om: 0, Ls: 0, Gs: 1, L2: 0}),\n    Table4Row(0.00006, False, cos, {Gm: 3, Fm: 0, D: -2, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00006, False, cos, {Gm: 0, Fm: 2, D: -2, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00005, False, cos, {Gm: 0, Fm: 0, D: 2, Om: 0, Ls: 0, Gs: -2, L2: 0}),\n    Table4Row(-0.00005, False, cos, {Gm: 2, Fm: 0, D: -4, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00005, False, cos, {Gm: 1, Fm: 2, D: -2, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00005, False, cos, {Gm: 1, Fm: 0, D: -1, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00004, False, cos, {Gm: 1, Fm: 0, D: 2, Om: 0, Ls: 0, Gs: -1, L2: 0}),\n    Table4Row(-0.00004, False, cos, {Gm: 3, Fm: 0, D: 0, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00003, False, cos, {Gm: 1, Fm: 0, D: -4, Om: 0, Ls: 0, Gs: 1, L2: 0}),\n    Table4Row(-0.00003, False, cos, {Gm: 2, Fm: -2, D: 0, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00003, False, cos, {Gm: 0, Fm: 2, D: 0, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n]\n\n\ntable4_w: List[Table4Row] = [\n    Table4Row(0.10478, False, sin, {Gm: 1, Fm: 0, D: 0, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.04105, False, sin, {Gm: 0, Fm: 2, D: 0, Om: 2, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.02130, False, sin, {Gm: 1, Fm: 0, D: -2, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.01779, False, sin, {Gm: 0, Fm: 2, D: 0, Om: 1, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.01774, False, sin, {Gm: 0, Fm: 0, D: 0, Om: 1, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00987, False, sin, {Gm: 0, Fm: 0, D: 2, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00338, False, sin, {Gm: 1, Fm: -2, D: 0, Om: -2, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00309, False, sin, {Gm: 0, Fm: 0, D: 0, Om: 0, Ls: 0, Gs: 1, L2: 0}),\n    Table4Row(-0.00190, False, sin, {Gm: 0, Fm: 2, D: 0, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00144, False, sin, {Gm: 1, Fm: 0, D: 0, Om: 1, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00144, False, sin, {Gm: 1, Fm: -2, D: 0, Om: -1, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00113, False, sin, {Gm: 1, Fm: 2, D: 0, Om: 2, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00094, False, sin, {Gm: 1, Fm: 0, D: -2, Om: 0, Ls: 0, Gs: 1, L2: 0}),\n    Table4Row(-0.00092, False, sin, {Gm: 2, Fm: 0, D: -2, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00071, False, sin, {Gm: 0, Fm: 0, D: 2, Om: 0, Ls: 0, Gs: -1, L2: 0}),\n    Table4Row(0.00070, False, sin, {Gm: 2, Fm: 0, D: 0, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00067, False, sin, {Gm: 1, Fm: 2, D: -2, Om: 2, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00066, False, sin, {Gm: 0, Fm: 2, D: -2, Om: 1, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00066, False, sin, {Gm: 0, Fm: 0, D: 2, Om: 1, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00061, False, sin, {Gm: 1, Fm: 0, D: 0, Om: 0, Ls: 0, Gs: -1, L2: 0}),\n    Table4Row(-0.00058, False, sin, {Gm: 0, Fm: 0, D: 1, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00049, False, sin, {Gm: 1, Fm: 2, D: 0, Om: 1, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00049, False, sin, {Gm: 1, Fm: 0, D: 0, Om: -1, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00042, False, sin, {Gm: 1, Fm: 0, D: 0, Om: 0, Ls: 0, Gs: 1, L2: 0}),\n    Table4Row(0.00034, False, sin, {Gm: 0, Fm: 2, D: -2, Om: 2, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00026, False, sin, {Gm: 0, Fm: 2, D: -2, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00025, False, sin, {Gm: 1, Fm: -2, D: -2, Om: -2, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00024, False, sin, {Gm: 1, Fm: -2, D: 0, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00023, False, sin, {Gm: 1, Fm: 2, D: -2, Om: 1, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00023, False, sin, {Gm: 1, Fm: 0, D: -2, Om: -1, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00019, False, sin, {Gm: 1, Fm: 0, D: 2, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00012, False, sin, {Gm: 1, Fm: 0, D: -2, Om: 0, Ls: 0, Gs: -1, L2: 0}),\n    Table4Row(0.00011, False, sin, {Gm: 1, Fm: 0, D: -2, Om: 1, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00011, False, sin, {Gm: 1, Fm: -2, D: -2, Om: -1, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00010, False, sin, {Gm: 0, Fm: 0, D: 2, Om: 0, Ls: 0, Gs: 1, L2: 0}),\n    Table4Row(0.00009, False, sin, {Gm: 1, Fm: 0, D: -1, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00008, False, sin, {Gm: 0, Fm: 0, D: 1, Om: 0, Ls: 0, Gs: 1, L2: 0}),\n    Table4Row(-0.00008, False, sin, {Gm: 0, Fm: 2, D: 2, Om: 2, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00008, False, sin, {Gm: 0, Fm: 0, D: 0, Om: 2, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00007, False, sin, {Gm: 0, Fm: 2, D: 0, Om: 2, Ls: 0, Gs: -1, L2: 0}),\n    Table4Row(0.00006, False, sin, {Gm: 0, Fm: 2, D: 0, Om: 2, Ls: 0, Gs: 1, L2: 0}),\n    Table4Row(-0.00005, False, sin, {Gm: 1, Fm: 2, D: 0, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00005, False, sin, {Gm: 3, Fm: 0, D: 0, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(\n        -0.00005, False, sin, {Gm: 1, Fm: 0, D: 0, Om: 0, Ls: 16, Gs: 0, L2: -18}\n    ),\n    Table4Row(-0.00005, False, sin, {Gm: 2, Fm: 2, D: 0, Om: 2, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00004, True, sin, {Gm: 0, Fm: 2, D: 0, Om: 2, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00004, False, cos, {Gm: 1, Fm: 0, D: 0, Om: 0, Ls: 16, Gs: 0, L2: -18}),\n    Table4Row(-0.00004, False, sin, {Gm: 1, Fm: -2, D: 2, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00004, False, sin, {Gm: 1, Fm: 0, D: -4, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00004, False, sin, {Gm: 3, Fm: 0, D: -2, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00004, False, sin, {Gm: 0, Fm: 2, D: 2, Om: 1, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00004, False, sin, {Gm: 0, Fm: 0, D: 2, Om: -1, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00003, False, sin, {Gm: 0, Fm: 0, D: 0, Om: 0, Ls: 0, Gs: 2, L2: 0}),\n    Table4Row(-0.00003, False, sin, {Gm: 1, Fm: 0, D: -2, Om: 0, Ls: 0, Gs: 2, L2: 0}),\n    Table4Row(0.00003, False, sin, {Gm: 0, Fm: 2, D: -2, Om: 1, Ls: 0, Gs: 1, L2: 0}),\n    Table4Row(-0.00003, False, sin, {Gm: 0, Fm: 0, D: 2, Om: 1, Ls: 0, Gs: -1, L2: 0}),\n    Table4Row(0.00003, False, sin, {Gm: 2, Fm: 2, D: -2, Om: 2, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00003, False, sin, {Gm: 0, Fm: 0, D: 2, Om: 0, Ls: 0, Gs: -2, L2: 0}),\n    Table4Row(-0.00003, False, sin, {Gm: 2, Fm: 0, D: -2, Om: 0, Ls: 0, Gs: 1, L2: 0}),\n    Table4Row(0.00003, False, sin, {Gm: 1, Fm: 2, D: -2, Om: 2, Ls: 0, Gs: 1, L2: 0}),\n    Table4Row(-0.00003, False, sin, {Gm: 2, Fm: 0, D: -4, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00002, False, sin, {Gm: 0, Fm: 2, D: -2, Om: 2, Ls: 0, Gs: 1, L2: 0}),\n    Table4Row(-0.00002, False, sin, {Gm: 2, Fm: 2, D: 0, Om: 1, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00002, False, sin, {Gm: 2, Fm: 0, D: 0, Om: -1, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00002, True, cos, {Gm: 1, Fm: 0, D: 0, Om: 0, Ls: 16, Gs: 0, L2: -18}),\n    Table4Row(0.00002, False, sin, {Gm: 0, Fm: 0, D: 4, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00002, False, sin, {Gm: 0, Fm: 2, D: -1, Om: 2, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00002, False, sin, {Gm: 1, Fm: 2, D: -2, Om: 0, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00002, False, sin, {Gm: 2, Fm: 0, D: 0, Om: 1, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00002, False, sin, {Gm: 2, Fm: -2, D: 0, Om: -1, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(0.00002, False, sin, {Gm: 1, Fm: 0, D: 2, Om: 0, Ls: 0, Gs: -1, L2: 0}),\n    Table4Row(0.00002, False, sin, {Gm: 2, Fm: 0, D: 0, Om: 0, Ls: 0, Gs: -1, L2: 0}),\n    Table4Row(-0.00002, False, sin, {Gm: 1, Fm: 0, D: -4, Om: 0, Ls: 0, Gs: 1, L2: 0}),\n    Table4Row(0.00002, True, sin, {Gm: 1, Fm: 0, D: 0, Om: 0, Ls: 16, Gs: 0, L2: -18}),\n    Table4Row(\n        -0.00002, False, sin, {Gm: 1, Fm: -2, D: 0, Om: -2, Ls: 0, Gs: -1, L2: 0}\n    ),\n    Table4Row(0.00002, False, sin, {Gm: 2, Fm: -2, D: 0, Om: -2, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00002, False, sin, {Gm: 1, Fm: 0, D: 2, Om: 1, Ls: 0, Gs: 0, L2: 0}),\n    Table4Row(-0.00002, False, sin, {Gm: 1, Fm: -2, D: 2, Om: -1, Ls: 0, Gs: 0, L2: 0}),\n]\n"
  },
  {
    "path": "src/docs/Makefile",
    "content": "# Makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS    = -d ../../artifact/doctree\nSPHINXBUILD   = sphinx-build\nPAPER         =\nBUILDDIR      = ../../../gh-pages\n\n# User-friendly check for sphinx-build\nifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)\n$(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/)\nendif\n\n# Internal variables.\nPAPEROPT_a4     = -D latex_paper_size=a4\nPAPEROPT_letter = -D latex_paper_size=letter\nALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .\n# the i18n builder cannot share the environment and doctrees with the others\nI18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source\n\n.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext\n\nhelp:\n\t@echo \"Please use \\`make <target>' where <target> is one of\"\n\t@echo \"  html       to make standalone HTML files\"\n\t@echo \"  dirhtml    to make HTML files named index.html in directories\"\n\t@echo \"  singlehtml to make a single large HTML file\"\n\t@echo \"  pickle     to make pickle files\"\n\t@echo \"  json       to make JSON files\"\n\t@echo \"  htmlhelp   to make HTML files and a HTML help project\"\n\t@echo \"  qthelp     to make HTML files and a qthelp project\"\n\t@echo \"  devhelp    to make HTML files and a Devhelp project\"\n\t@echo \"  epub       to make an epub\"\n\t@echo \"  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter\"\n\t@echo \"  latexpdf   to make LaTeX files and run them through pdflatex\"\n\t@echo \"  latexpdfja to make LaTeX files and run them through platex/dvipdfmx\"\n\t@echo \"  text       to make text files\"\n\t@echo \"  man        to make manual pages\"\n\t@echo \"  texinfo    to make Texinfo files\"\n\t@echo \"  info       to make Texinfo files and run them through makeinfo\"\n\t@echo \"  gettext    to make PO message catalogs\"\n\t@echo \"  changes    to make an overview of all changed/added/deprecated items\"\n\t@echo \"  xml        to make Docutils-native XML files\"\n\t@echo \"  pseudoxml  to make pseudoxml-XML files for display purposes\"\n\t@echo \"  linkcheck  to check all external links for integrity\"\n\t@echo \"  doctest    to run all doctests embedded in the documentation (if enabled)\"\n\nclean:\n\trm -rf $(BUILDDIR)/*\n\nhtml:\n\t$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)\n\ttouch $(BUILDDIR)/.nojekyll\n\t@echo\n\t@echo \"Build finished. The HTML pages are in $(BUILDDIR).\"\n\ndirhtml:\n\t$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml\n\t@echo\n\t@echo \"Build finished. The HTML pages are in $(BUILDDIR)/dirhtml.\"\n\nsinglehtml:\n\t$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml\n\t@echo\n\t@echo \"Build finished. The HTML page is in $(BUILDDIR)/singlehtml.\"\n\npickle:\n\t$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle\n\t@echo\n\t@echo \"Build finished; now you can process the pickle files.\"\n\njson:\n\t$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json\n\t@echo\n\t@echo \"Build finished; now you can process the JSON files.\"\n\nhtmlhelp:\n\t$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp\n\t@echo\n\t@echo \"Build finished; now you can run HTML Help Workshop with the\" \\\n\t      \".hhp project file in $(BUILDDIR)/htmlhelp.\"\n\nqthelp:\n\t$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp\n\t@echo\n\t@echo \"Build finished; now you can run \"qcollectiongenerator\" with the\" \\\n\t      \".qhcp project file in $(BUILDDIR)/qthelp, like this:\"\n\t@echo \"# qcollectiongenerator $(BUILDDIR)/qthelp/yyryr.qhcp\"\n\t@echo \"To view the help file:\"\n\t@echo \"# assistant -collectionFile $(BUILDDIR)/qthelp/yyryr.qhc\"\n\ndevhelp:\n\t$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp\n\t@echo\n\t@echo \"Build finished.\"\n\t@echo \"To view the help file:\"\n\t@echo \"# mkdir -p $$HOME/.local/share/devhelp/yyryr\"\n\t@echo \"# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/yyryr\"\n\t@echo \"# devhelp\"\n\nepub:\n\t$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub\n\t@echo\n\t@echo \"Build finished. The epub file is in $(BUILDDIR)/epub.\"\n\nlatex:\n\t$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex\n\t@echo\n\t@echo \"Build finished; the LaTeX files are in $(BUILDDIR)/latex.\"\n\t@echo \"Run \\`make' in that directory to run these through (pdf)latex\" \\\n\t      \"(use \\`make latexpdf' here to do that automatically).\"\n\nlatexpdf:\n\t$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex\n\t@echo \"Running LaTeX files through pdflatex...\"\n\t$(MAKE) -C $(BUILDDIR)/latex all-pdf\n\t@echo \"pdflatex finished; the PDF files are in $(BUILDDIR)/latex.\"\n\nlatexpdfja:\n\t$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex\n\t@echo \"Running LaTeX files through platex and dvipdfmx...\"\n\t$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja\n\t@echo \"pdflatex finished; the PDF files are in $(BUILDDIR)/latex.\"\n\ntext:\n\t$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text\n\t@echo\n\t@echo \"Build finished. The text files are in $(BUILDDIR)/text.\"\n\nman:\n\t$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man\n\t@echo\n\t@echo \"Build finished. The manual pages are in $(BUILDDIR)/man.\"\n\ntexinfo:\n\t$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo\n\t@echo\n\t@echo \"Build finished. The Texinfo files are in $(BUILDDIR)/texinfo.\"\n\t@echo \"Run \\`make' in that directory to run these through makeinfo\" \\\n\t      \"(use \\`make info' here to do that automatically).\"\n\ninfo:\n\t$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo\n\t@echo \"Running Texinfo files through makeinfo...\"\n\tmake -C $(BUILDDIR)/texinfo info\n\t@echo \"makeinfo finished; the Info files are in $(BUILDDIR)/texinfo.\"\n\ngettext:\n\t$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale\n\t@echo\n\t@echo \"Build finished. The message catalogs are in $(BUILDDIR)/locale.\"\n\nchanges:\n\t$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes\n\t@echo\n\t@echo \"The overview file is in $(BUILDDIR)/changes.\"\n\nlinkcheck:\n\t$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck\n\t@echo\n\t@echo \"Link check complete; look for any errors in the above output \" \\\n\t      \"or in $(BUILDDIR)/linkcheck/output.txt.\"\n\ndoctest:\n\t$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest\n\t@echo \"Testing of doctests in the sources finished, look at the \" \\\n\t      \"results in $(BUILDDIR)/doctest/output.txt.\"\n\nxml:\n\t$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml\n\t@echo\n\t@echo \"Build finished. The XML files are in $(BUILDDIR)/xml.\"\n\npseudoxml:\n\t$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml\n\t@echo\n\t@echo \"Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml.\"\n"
  },
  {
    "path": "src/docs/SConstruct",
    "content": "\"\"\"\nThis is a generic SCons script for running Sphinx (http://sphinx.pocoo.org).\n\nType 'scons -h' for help.  This prints the available build targets on your\nsystem, and the configuration options you can set.\n\nIf you set the 'cache' option, the option settings are cached into a file\ncalled '.sconsrc-sphinx' in the current directory.  When running\nsubsequently, this file is reread.  A file with this name is also read from\nyour home directory, if it exists, so you can put global settings there.\n\nThe script looks into your 'conf.py' file for information about the\nproject.  This is used in various places (e.g., to print the introductory\nmessage, and create package files).\n\nHere's some examples.  To build HTML docs:\n\n   scons html\n\nTo create a package containing HTML and PDF docs, remembering the 'install'\nsetting:\n\n   scons install=html,pdf cache=True package\n\nTo clean up everything:\n\n   scons -c all\n\"\"\"\n\n# Script info.\n__author__  = \"Glenn Hutchings\"\n__email__   = \"zondo42@googlemail.com\"\n__url__     = \"http://bitbucket.org/zondo/sphinx-scons\"\n__license__ = \"BSD\"\n__version__ = \"0.4\"\n\nimport sys, os\nimport runpy\n\n# Build targets.\ntargets = (\n    (\"html\",      \"make standalone HTML files\"),\n    (\"dirhtml\",   \"make HTML files named index.html in directories\"),\n    (\"pickle\",    \"make pickle files\"),\n    (\"json\",      \"make JSON files\"),\n    (\"htmlhelp\",  \"make HTML files and a HTML help project\"),\n    (\"qthelp\",    \"make HTML files and a qthelp project\"),\n    (\"devhelp\",   \"make HTML files and a GNOME DevHelp project\"),\n    (\"epub\",      \"make HTML files and an EPUB file for bookreaders\"),\n    (\"latex\",     \"make LaTeX sources\"),\n    (\"texinfo\",   \"make Texinfo sources\"),\n    (\"text\",      \"make text file for each RST file\"),\n    (\"pdf\",       \"make PDF file from LaTeX sources\"),\n    (\"ps\",        \"make PostScript file from LaTeX sources\"),\n    (\"dvi\",       \"make DVI file from LaTeX sources\"),\n    (\"changes\",   \"make an overview over all changed/added/deprecated items\"),\n    (\"linkcheck\", \"check all external links for integrity\"),\n    (\"doctest\",   \"run all doctests embedded in the documentation if enabled\"),\n    (\"source\",    \"run a command to generate the reStructuredText source\"),\n)\n\n# LaTeX builders.\nlatex_builders = {\"pdf\": \"PDF\", \"ps\": \"PostScript\", \"dvi\": \"DVI\"}\n\n# List of target names.\ntargetnames = [name for name, desc in targets]\n\n# Configuration cache filename.\ncachefile = \".sconsrc-sphinx\"\n\n# User cache file.\nhomedir = os.path.expanduser('~')\nusercache = os.path.join(homedir, cachefile)\n\n# Configuration options.\nconfig = Variables([usercache, cachefile], ARGUMENTS)\n\nconfig.AddVariables(\n    EnumVariable(\"default\", \"default build target\", \"html\", targetnames),\n    PathVariable(\"config\", \"sphinx configuration file\", \"conf.py\"),\n    PathVariable(\"srcdir\", \"source directory\", \".\",\n                 PathVariable.PathIsDir),\n    PathVariable(\"builddir\", \"build directory\", \"build\",\n                 PathVariable.PathIsDirCreate),\n    PathVariable(\"doctrees\", \"place to put doctrees\", None,\n                 PathVariable.PathAccept),\n    EnumVariable(\"paper\", \"LaTeX paper size\", None,\n                 [\"a4\", \"letter\"], ignorecase = False),\n    (\"tags\", \"comma-separated list of 'only' tags\", None),\n    (\"builder\", \"program to run to build things\", \"sphinx-build\"),\n    (\"opts\", \"extra builder options to use\", None),\n    ListVariable(\"install\", \"targets to install\", [\"html\"], targetnames),\n    PathVariable(\"instdir\", \"installation directory\", \"/usr/local/doc\",\n                 PathVariable.PathAccept),\n    EnumVariable(\"pkgtype\", \"package type to build with 'scons package'\",\n                 \"zip\", [\"zip\", \"targz\", \"tarbz2\"], ignorecase = False),\n    BoolVariable(\"cache\", \"whether to cache settings in %s\" % cachefile, False),\n    BoolVariable(\"debug\", \"debugging flag\", False),\n    (\"genrst\", \"Command to regenerate reStructuredText source\", None),\n)\n\n# Create a new environment, inheriting PATH to find builder program.  Also\n# force LaTeX instead of TeX, since the .tex file won't exist at the right\n# time to check which one to use.\nenv = Environment(ENV = {\"PATH\" : os.environ[\"PATH\"]},\n                  TEX = \"latex\", PDFTEX = \"pdflatex\",\n                  tools = ['default', 'packaging'],\n                  variables = config)\nif 'PYTHONPATH' in os.environ:\n    env['ENV']['PYTHONPATH'] = os.environ['PYTHONPATH']\n\n# Get configuration values from environment.\nsphinxconf = env[\"config\"]\nbuilder = env[\"builder\"]\ndefault = env[\"default\"]\n\nsrcdir = env[\"srcdir\"]\nbuilddir = env[\"builddir\"]\ndoctrees = env.get(\"doctrees\", os.path.join(builddir, \"doctrees\"))\n\ncache = env[\"cache\"]\ndebug = env[\"debug\"]\n\noptions = env.get(\"opts\", None)\npaper = env.get(\"paper\", None)\ntags = env.get(\"tags\", None)\ngenrst = env.get(\"genrst\", None)\n\ninstdir = env[\"instdir\"]\ninstall = env[\"install\"]\npkgtype = env[\"pkgtype\"]\n\n# Dump internals if debugging.\nif debug:\n    print \"Environment:\"\n    print env.Dump()\n\n# Get parameters from Sphinx config file.\n#sphinxparams = {}\n#execfile(sphinxconf, sphinxparams)\nsphinxparams = runpy.run_path(sphinxconf)\n\nproject = sphinxparams[\"project\"]\nrelease = sphinxparams[\"release\"]\ncopyright = sphinxparams[\"copyright\"]\n\ntry:\n    texfilename = sphinxparams[\"latex_documents\"][0][1]\nexcept KeyError:\n    texfilename = None\n\nname2tag = lambda name: name.replace(\" \", \"-\").strip(\"()\")\nproject_tag = name2tag(project)\nrelease_tag = name2tag(release)\npackage_tag = project_tag.lower() + \"-\" + release_tag.lower()\n\n# Build project description string.\ndescription = \"%(project)s, release %(release)s, \" \\\n               \"copyright %(copyright)s\" % locals()\n\nHelp(description + \"\\n\\n\")\nhelp_format = \"   %-10s  %s\\n\"\n\n# Print banner if required.\nif not any(map(GetOption, (\"silent\", \"clean\", \"help\"))):\n    print\n    print \"This is\", description\n    print\n\n# Build sphinx command-line options.\nopts = []\n\nif tags:\n    opts.extend([\"-t %s\" % tag for tag in tags.split(\",\")])\n\nif paper:\n    opts.append(\"-D latex_paper_size=%s\" % paper)\n\nif options:\n    opts.append(options)\n\noptions = \" \".join(opts)\n\n# Build Sphinx command template.\nsphinxcmd = \"\"\"\n%(builder)s -b %(name)s -d %(doctrees)s %(options)s %(srcdir)s %(targetdir)s\n\"\"\".strip()\n\n# Set up LaTeX input builder if required.\nif texfilename:\n    latexdir = Dir(\"latex\", builddir)\n    texinput = File(texfilename, latexdir)\n    env.SideEffect(texinput, \"latex\")\n    env.NoClean(texinput)\n\n# Add build targets.\nHelp(\"Build targets:\\n\\n\")\n\nif genrst != None:\n    source = env.Command('source', [], genrst, chdir = True)\n    env.AlwaysBuild(source)\n    env.Depends(srcdir, source)\nelse:\n    source = env.Command(\n        'source', [],\n        '@echo \"No reStructuredText generator (genrst) given.\"')\n\nfor name, desc in targets:\n    target = Dir(name, builddir)\n    targetdir = str(target)\n\n    if name == 'source':\n        pass\n    elif name not in latex_builders:\n        # Standard Sphinx target.\n        targets = env.Command(name, sphinxconf,\n                              sphinxcmd % locals(), chdir = True)\n        env.Depends(targets, source)\n        env.AlwaysBuild(name)\n        env.Alias(target, name)\n    elif texinput:\n        # Target built from LaTeX sources.\n        try:\n            buildfunc = getattr(env, latex_builders[name])\n        except AttributeError:\n            continue\n\n        filename = project_tag + \".\" + name\n        outfile = File(filename, latexdir)\n\n        targets = buildfunc(outfile, texinput)\n        env.Depends(targets, source)\n\n        # Copy built file to separate directory.\n        target = File(filename, target)\n        env.Command(target, outfile, Move(target, outfile), chdir = True)\n\n        env.Alias(name, target)\n    else:\n        continue\n\n    env.Clean(name, [target])\n    env.Clean('all', target)\n\n    if name == default: desc += \" (default)\"\n    Help(help_format % (name, desc))\n\nClean('all', doctrees)\nDefault(default)\n\n# Add installation targets and collect package sources.\nHelp(\"\\nOther targets:\\n\\n\")\n\nHelp(help_format % (\"install\", \"install documentation\"))\nprojectdir = os.path.join(instdir, project_tag)\nsources = []\n\nfor name in install:\n    source = Dir(name, builddir)\n    sources.append(source)\n\n    inst = env.Install(projectdir, source)\n    env.Alias('install', inst)\n\n    for node in env.Glob(os.path.join(str(source), '*')):\n        filename = str(node).replace(builddir + os.path.sep, \"\")\n        dirname = os.path.dirname(filename)\n        dest = os.path.join(projectdir, dirname)\n        inst = env.Install(dest, node)\n        env.Alias('install', inst)\n\n# Add uninstall target.\nenv.Command('uninstall', None, Delete(projectdir), chdir = True)\nHelp(help_format % (\"uninstall\", \"uninstall documentation\"))\n\n# Add package builder.\npackageroot = \"-\".join([project_tag, release_tag])\narchive, package = env.Package(NAME = project_tag, VERSION = release,\n                               PACKAGEROOT = packageroot,\n                               PACKAGETYPE = pkgtype,\n                               source = sources)\n\nenv.AlwaysBuild(archive)\nenv.AddPostAction(archive, Delete(packageroot))\nHelp(help_format % (\"package\", \"build documentation package\"))\n\nenv.Clean('all', archive)\n\n# Add config settings to help.\nHelp(\"\\nConfiguration variables:\")\nfor line in config.GenerateHelpText(env).split(\"\\n\"):\n    Help(\"\\n   \" + line)\n\n# Save local configuration if required.\nif cache:\n    config.Update(env)\n    config.Save(cachefile, env)\n"
  },
  {
    "path": "src/docs/conf.py",
    "content": "# -*- coding: utf-8 -*-\nimport os\nimport sys\n\non_rtd = os.environ.get(\"READTHEDOCS\", None) == \"True\"\n\nproject = \"Astral\"\nauthor = \"Simon Kennedy\"\ncopyright = \"2009-2022, %s\" % author\nversion = \"3.2\"\nrelease = \"3.2\"\n\nextensions = [\n    \"sphinx.ext.autodoc\",\n    \"sphinx.ext.intersphinx\",\n    \"sphinx.ext.napoleon\",\n    \"sphinx.ext.doctest\",\n]\n\nintersphinx_mapping = {\n    \"python\": (\"http://docs.python.org/3\", \"python3_intersphinx.inv\")\n}\n\n# Add parent directory so autodoc can find the source code\nsys.path.insert(0, os.path.join(os.path.abspath(\"..\")))\n\nsource_suffix = \".rst\"\nmaster_doc = \"index\"\n\npygments_style = \"sphinx\"\n\ntemplates_path = [\"templates\"]\n# endregion\n\nif not on_rtd:\n    html_theme = \"sphinx_book_theme\"\nelse:\n    html_theme = \"basic\"\nhtml_logo = os.path.join(\"static\", \"earth_sun.png\")\n\nif not on_rtd:\n    html_favicon = os.path.join(\"static\", \"weather-sunny.png\")\n\nhtml_static_path = [\"static\"]\n\nhtml_css_files = [\n    \"astral.css\",\n]\n\nhtml_domain_indices = False\n"
  },
  {
    "path": "src/docs/index.rst",
    "content": ".. Copyright 2009-2021, Simon Kennedy, sffjunkie+code@gmail.com\n\n.. Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n..   http://www.apache.org/licenses/LICENSE-2.0\n\n.. Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n.. TODO: Spatial reference system - WGS-84?\n\n.. TODO: Add note about accuracy\n\n.. TODO: Add note that 0.833 is half sun's disc + refraction adjustment\n\nAstral v\\ |release|\n===================\n\n| |ghaction_status| |pypi_ver|\n\nAstral is a python package for calculating the times of various aspects of\nthe sun and moon.\n\nIt can calculate the following\n\nDawn\n    The time in the morning when the sun is a specific number of degrees\n    below the horizon.\n\nSunrise\n    The time in the morning when the top of the sun breaks the horizon\n    (assuming a location with no obscuring features.)\n\nNoon\n    The time when the sun is at its highest point directly above the observer.\n\nMidnight\n    The time when the sun is at its lowest point.\n\nSunset\n    The time in the evening when the sun is about to disappear below the\n    horizon (asuming a location with no obscuring features.)\n\nDusk\n    The time in the evening when the sun is a specific number of degrees\n    below the horizon.\n\nDaylight\n   The time when the sun is up i.e. between sunrise and sunset\n\nNight\n   The time between astronomical dusk of one day and astronomical dawn of the\n   next\n\nTwilight\n   The time between dawn and sunrise or between sunset and dusk\n\nThe Golden Hour\n   The time when the sun is between 4 degrees below the horizon and 6 degrees\n   above.\n\nThe Blue Hour\n   The time when the sun is between 6 and 4 degrees below the horizon.\n\nTime At Elevation\n   the time when the sun is at a specific elevation for either a rising or a\n   setting sun.\n\nSolar Azimuth\n    The number of degrees clockwise from North at which the sun can be seen\n\nSolar Zenith\n    The angle of the sun down from directly above the observer\n\nSolar Elevation\n    The number of degrees up from the horizon at which the sun can be seen\n\n`Rahukaalam`_\n    \"Rahukaalam or the period of Rahu is a certain amount of time every day\n    that is considered inauspicious for any new venture according to Indian\n    Vedic astrology\".\n\nMoonrise and Moonset\n   Like the Sun but for the moon\n\nMoon Azimuth and Zenith\n   Also like the Sun but for the moon\n\nMoon Phase\n    The phase of the moon for a specified date.\n\nAstral also comes with a geocoder containing a local database that allows you\nto look up information for a small set of locations (`new locations can be\nadded <additional_locations_>`__).\n\nExamples\n========\n\nThe following examples demonstrates some of the functionality available in the\nmodule\n\nSun\n----\n\n.. testcode::\n\n    from astral import LocationInfo\n    city = LocationInfo(\"London\", \"England\", \"Europe/London\", 51.5, -0.116)\n    print((\n        f\"Information for {city.name}/{city.region}\\n\"\n        f\"Timezone: {city.timezone}\\n\"\n        f\"Latitude: {city.latitude:.02f}; Longitude: {city.longitude:.02f}\\n\"\n    ))\n\n.. testoutput::\n\n    Information for London/England\n    Timezone: Europe/London\n    Latitude: 51.50; Longitude: -0.12\n\n.. testcode::\n\n    import datetime\n    from astral.sun import sun\n    s = sun(city.observer, date=datetime.date(2009, 4, 22))\n    print((\n        f'Dawn:    {s[\"dawn\"]}\\n'\n        f'Sunrise: {s[\"sunrise\"]}\\n'\n        f'Noon:    {s[\"noon\"]}\\n'\n        f'Sunset:  {s[\"sunset\"]}\\n'\n        f'Dusk:    {s[\"dusk\"]}\\n'\n    ))\n\n.. testoutput::\n\n    Dawn:    2009-04-22 04:13:04.997608+00:00\n    Sunrise: 2009-04-22 04:50:17.127004+00:00\n    Noon:    2009-04-22 11:59:02+00:00\n    Sunset:  2009-04-22 19:08:41.711407+00:00\n    Dusk:    2009-04-22 19:46:06.423846+00:00\n\n.. note::\n\n   The example above calculates the times of the sun in the UTC timezone.\n   If you want to return times in a different timezone you can pass the\n   `tzinfo` parameter to the function.\n\n   .. code-block:: python\n\n       >>> city = LocationInfo(\"London\", \"England\", \"Europe/London\", 51.5, -0.116)\n       >>> london = Location(city)\n       >>> s = sun(city.observer, date=datetime.date(2009, 4, 22), tzinfo=london.timezone)\n\n   or\n\n   .. code-block:: python\n\n       >>> timezone = zoneinfo.ZoneInfo(\"Europe/London\")\n       >>> s = sun(city.observer, date=datetime.date(2009, 4, 22), tzinfo=timezone)\n\n\nMoon\n----\n\nThe moon rise/set times can be obtained like the sun's functions\n\n.. testcode::\n\n    from astral import LocationInfo\n    city = LocationInfo(\"London\", \"England\", \"Europe/London\", 51.5, -0.116)\n\n    import datetime\n    from astral.moon import moonrise\n    from astral.location import Location\n    dt = datetime.date(2021, 10, 28)\n    london = Location(city)\n    rise = moonrise(city.observer, dt)  # returns a UTC time\n    print(rise)\n\n.. testoutput::\n\n    2021-10-28 22:02:00+00:00\n\nAnd for a local time\n\n.. testsetup::\n\n    import datetime\n    from astral import LocationInfo\n    from astral.moon import moonrise\n    from astral.location import Location\n\n    city = LocationInfo(\"London\", \"England\", \"Europe/London\", 51.5, -0.116)\n    dt = datetime.date(2021, 10, 28)\n    london = Location(city)\n\n.. testcode::\n\n    rise = moonrise(city.observer, dt, city.tzinfo)\n\n\nPhase\n~~~~~~\n\n.. testcode::\n\n   import datetime\n   from astral import moon\n   phase = moon.phase(datetime.date(2018, 1, 1))\n   print(phase)\n\n.. testoutput::\n\n   13.255666666666668\n\nThe moon phase method returns an number describing the phase, where the value\nis between 0 and 27.99. The following lists the mapping of various values to\nthe description of the phase of the moon.\n\n============  ==============\n0 .. 6.99     New moon\n7 .. 13.99    First quarter\n14 .. 20.99   Full moon\n21 .. 27.99   Last quarter\n============  ==============\n\nIf for example the number returned was 27.99 then the moon would be almost at\nthe New Moon phase, and if it was 24.00 it would be half way between the Last\nQuarter and a New Moon.\n\n.. note ::\n\n   The moon phase does not depend on your location. However what the moon\n   actually looks like to you does depend on your location. If you're in the\n   southern hemisphere it looks different than if you were in the northern\n   hemisphere.\n\n   See http://moongazer.x10.mx/website/astronomy/moon-phases/ for an example.\n\n   For an example of using this library to generate moon phases including the\n   names in various languages and the correct Unicode glyphs see the\n   `project by PanderMusubi <https://github.com/PanderMusubi/lunar-phase-calendar/>`_\n   on Github.\n\nGeocoder\n--------\n\n.. code-block:: python\n\n    >>> from astral.geocoder import database, lookup\n    >>> lookup(\"London\", database())\n    LocationInfo(name='London', region='England', timezone='Europe/London',\n        latitude=51.473333333333336, longitude=-0.0008333333333333334)\n\n.. note::\n\n   Location elevations have been removed from the database. These were added\n   due to a misunderstanding of the affect of elevation on the times of the\n   sun. These are not required for the calculations, only the elevation of the\n   observer above/below the location is needed.\n\n   See `Effect of Elevation`_ below.\n\nCustom Location\n~~~~~~~~~~~~~~~\n\nIf you only need a single location that is not in the database then you can\nconstruct a :class:`~astral.LocationInfo` and fill in the values, either on\ninitialization\n\n.. code-block:: python\n\n    from astral import LocationInfo\n    l = LocationInfo('name', 'region', 'timezone/name', 0.1, 1.2)\n\nor set the attributes after initialization::\n\n    from astral import LocationInfo\n    l = LocationInfo()\n    l.name = 'name'\n    l.region = 'region'\n    l.timezone = 'US/Central'\n    l.latitude = 0.1\n    l.longitude = 1.2\n\n.. note::\n\n   `name` and `region` can be anything you like.\n\n.. _additional_locations:\n\nAdditional Locations\n~~~~~~~~~~~~~~~~~~~~\n\nYou can add to the list of available locations using the\n:func:`~astral.geocoder.add_locations` function and passing either a string\nwith one line per location or by passing a list containing strings, lists or\ntuples (lists and tuples are passed directly to the LocationInfo constructor).\n\n.. code-block:: python\n\n    >>> from astral.geocoder import add_locations, database, lookup\n    >>> db = database()\n    >>> try:\n    ...     lookup(\"Somewhere\", db)\n    ... except KeyError:\n    ...     print(\"Somewhere not found\")\n    ...\n    Somewhere not found\n    >>> add_locations(\"Somewhere,Secret Location,UTC,24°28'N,39°36'E\", db)\n    >>> lookup(\"Somewhere\", db)\n    LocationInfo(name='Somewhere', region='Secret Location', timezone='UTC',\n        latitude=24.466666666666665, longitude=39.6)\n\nTimezone Groups\n~~~~~~~~~~~~~~~\n\nTimezone groups such as Europe can be accessed via the :func:`group` function\nin the :mod:`~astral.geocoder` module\n\n.. testcode::\n\n    from astral.geocoder import group, database\n    db = database()\n    europe = group(\"europe\", db)\n    print(sorted(europe.keys())[:4])\n\n.. testoutput::\n\n    ['aberdeen', 'amsterdam', 'andorra_la_vella', 'ankara']\n\n\nEffect of Elevation\n===================\n\nTimes Of The Sun\n----------------\n\nThe times of the sun that you experience depend on what obscurs your view of\nit. It may either be obscured by the horizon or some other geographical\nfeature (e.g. mountains)\n\n1. If what obscures you at ground level is the horizon and you are at a\n   elevation above ground level then the times of the sun depends on how far\n   further round the earth you can see due to your elevation (the sun rises\n   earlier and sets later).\n\n   The extra angle you can see round the earth is determined by calculating the\n   angle α in the image below based on your elevation above ground level,\n   and adding this to the depression angle for the sun calculations.\n\n   .. image:: static/elevation_horizon.svg\n      :class: adjustment\n\n2. If your view is obscured by some other geographical feature than the\n   horizon, then the adjustment angle is based on how far you are above or\n   below the feature and your distance to it.\n\nFor the first case i.e. obscured by the horizon you need to pass a single float\nto the Observer as its elevation. For the second case pass a tuple of 2\nfloats. The first being the vertical distance to the top of the feature and\nthe second the horizontal distance to the feature.\n\nElevation Of The Sun\n--------------------\n\nEven though an observer's elevation can significantly affect the times of the\nsun the same is not true for the elevation angle from the observer to the sun.\n\nAs an example the diagram below shows the difference in angle between an\nobserver at ground level and one on the ISS orbiting 408 km above the earth.\n\n.. image:: static/elevation_sun.svg\n   :class: adjustment\n\nThe largest difference between the two angles is when the angle at ground\nlevel is 1 degree. The difference then is approximately 0.15 degrees.\n\nAt the summit of mount Everest (8,848 m) the maximum difference is\n0.00338821 degrees.\n\nDue to the very small difference the astral package does not currently adjust\nthe solar elevation for changes in observer elevation.\n\nEffect of Refraction\n====================\n\nWhen viewing the sun the position you see it at is different from its actual\nposition due to the effect of atmospheric `refraction`_ which\nmakes the sun appear to be higher in the sky. The calculations in the\npackage take this refraction into account.\n\nThe :func:`~astral.sun.sunrise` and :func:`~astral.sun.sunset` functions\nuse the refraction at an angle when the sun is half of its apparent diameter\nbelow the horizon. This is between about 30 and 32 arcminutes and for the\nastral package a value of 32\" is used.\n\n.. note::\n\n   The refraction calculation does not take into account\n   temperature and pressure which can affect the angle of refraction.\n\n\nLicense\n=======\n\nThis module is licensed under the terms of the `Apache`_ V2.0 license.\n\nInstallation\n============\n\nTo install Astral you should use the `pip`_ tool::\n\n    pip3 install astral\n\n.. note::\n\n   Now that we are Python 3 only and pip provides a versioned executable on\n   Windows you should use the `pip3` command on all operating systems\n   to ensure you are targetting the right Python version.\n\nCities\n======\n\nThe module includes location and time zone data for the following cities.\nThe list includes all capital cities plus some from the UK. The list also\nincludes the US state capitals and some other US cities.\n\nAberdeen, Abu Dhabi, Abu Dhabi, Abuja, Accra, Addis Ababa, Adelaide, Al Jubail,\nAlbany, Albuquerque, Algiers, Amman, Amsterdam, Anchorage, Andorra la Vella,\nAnkara, Annapolis, Antananarivo, Apia, Ashgabat, Asmara, Astana, Asuncion,\nAthens, Atlanta, Augusta, Austin, Avarua, Baghdad, Baku, Baltimore, Bamako,\nBandar Seri Begawan, Bangkok, Bangui, Banjul, Barrow-In-Furness, Basse-Terre,\nBasseterre, Baton Rouge, Beijing, Beirut, Belfast, Belgrade, Belmopan, Berlin,\nBern, Billings, Birmingham, Birmingham, Bishkek, Bismarck, Bissau,\nBloemfontein, Bogota, Boise, Bolton, Boston, Bradford, Brasilia, Bratislava,\nBrazzaville, Bridgeport, Bridgetown, Brisbane, Bristol, Brussels, Bucharest,\nBucuresti, Budapest, Buenos Aires, Buffalo, Bujumbura, Burlington, Cairo,\nCanberra, Cape Town, Caracas, Cardiff, Carson City, Castries, Cayenne,\nCharleston, Charlotte, Charlotte Amalie, Cheyenne, Chicago, Chisinau,\nCleveland, Columbia, Columbus, Conakry, Concord, Copenhagen, Cotonou, Crawley,\nDakar, Dallas, Damascus, Dammam, Denver, Des Moines, Detroit, Dhaka, Dili,\nDjibouti, Dodoma, Doha, Douglas, Dover, Dublin, Dushanbe, Edinburgh, El Aaiun,\nFargo, Fort-de-France, Frankfort, Freetown, Funafuti, Gaborone, George Town,\nGeorgetown, Gibraltar, Glasgow, Greenwich, Guatemala, Hanoi, Harare,\nHarrisburg, Hartford, Havana, Helena, Helsinki, Hobart, Hong Kong, Honiara,\nHonolulu, Houston, Indianapolis, Islamabad, Jackson, Jacksonville, Jakarta,\nJefferson City, Jerusalem, Juba, Jubail, Juneau, Kabul, Kampala, Kansas City,\nKathmandu, Khartoum, Kiev, Kigali, Kingston, Kingston, Kingstown, Kinshasa,\nKoror, Kuala Lumpur, Kuwait, La Paz, Lansing, Las Vegas, Leeds, Leicester,\nLibreville, Lilongwe, Lima, Lincoln, Lisbon, Little Rock, Liverpool, Ljubljana,\nLome, London, Los Angeles, Louisville, Luanda, Lusaka, Luxembourg, Macau,\nMadinah, Madison, Madrid, Majuro, Makkah, Malabo, Male, Mamoudzou, Managua,\nManama, Manchester, Manchester, Manila, Maputo, Maseru, Masqat, Mbabane, Mecca,\nMedina, Melbourne, Memphis, Mexico, Miami, Milwaukee, Minneapolis, Minsk,\nMogadishu, Monaco, Monrovia, Montevideo, Montgomery, Montpelier, Moroni,\nMoscow, Moskva, Mumbai, Muscat, N'Djamena, Nairobi, Nashville, Nassau,\nNaypyidaw, New Delhi, New Orleans, New York, Newark, Newcastle, Newcastle Upon\nTyne, Ngerulmud, Niamey, Nicosia, Norwich, Nouakchott, Noumea, Nuku'alofa,\nNuuk, Oklahoma City, Olympia, Omaha, Oranjestad, Orlando, Oslo, Ottawa,\nOuagadougou, Oxford, P'yongyang, Pago Pago, Palikir, Panama, Papeete,\nParamaribo, Paris, Perth, Philadelphia, Phnom Penh, Phoenix, Pierre, Plymouth,\nPodgorica, Port Louis, Port Moresby, Port of Spain, Port-Vila, Port-au-Prince,\nPortland, Portland, Porto-Novo, Portsmouth, Prague, Praia, Pretoria, Pristina,\nProvidence, Quito, Rabat, Raleigh, Reading, Reykjavik, Richmond, Riga, Riyadh,\nRoad Town, Rome, Roseau, Sacramento, Saint Helier, Saint Paul, Saint Pierre,\nSaipan, Salem, Salt Lake City, San Diego, San Francisco, San Jose, San Juan,\nSan Marino, San Salvador, Sana, Sana'a, Santa Fe, Santiago, Santo Domingo, Sao\nTome, Sarajevo, Seattle, Seoul, Sheffield, Singapore, Sioux Falls, Skopje,\nSofia, Southampton, Springfield, Sri Jayawardenapura Kotte, St. George's, St.\nJohn's, St. Peter Port, Stanley, Stockholm, Sucre, Suva, Swansea, Swindon,\nSydney, T'bilisi, Taipei, Tallahassee, Tallinn, Tarawa, Tashkent, Tbilisi,\nTegucigalpa, Tehran, Thimphu, Tirana, Tirane, Tokyo, Toledo, Topeka, Torshavn,\nTrenton, Tripoli, Tunis, Ulaanbaatar, Ulan Bator, Vaduz, Valletta, Vienna,\nVientiane, Vilnius, Virginia Beach, W. Indies, Warsaw, Washington DC,\nWellington, Wichita, Willemstad, Wilmington, Windhoek, Wolverhampton,\nYamoussoukro, Yangon, Yaounde, Yaren, Yerevan, Zagreb, Zurich\n\nUS Cities\n---------\n\nAlbany, Albuquerque, Anchorage, Annapolis, Atlanta, Augusta, Austin, Baltimore,\nBaton Rouge, Billings, Birmingham, Bismarck, Boise, Boston, Bridgeport,\nBuffalo, Burlington, Carson City, Charleston, Charlotte, Cheyenne, Chicago,\nCleveland, Columbia, Columbus, Concord, Dallas, Denver, Des Moines, Detroit,\nDover, Fargo, Frankfort, Harrisburg, Hartford, Helena, Honolulu, Houston,\nIndianapolis, Jackson, Jacksonville, Jefferson City, Juneau, Kansas City,\nLansing, Las Vegas, Lincoln, Little Rock, Los Angeles, Louisville, Madison,\nManchester, Memphis, Miami, Milwaukee, Minneapolis, Montgomery, Montpelier,\nNashville, New Orleans, New York, Newark, Oklahoma City, Olympia, Omaha,\nOrlando, Philadelphia, Phoenix, Pierre, Portland, Portland, Providence,\nRaleigh, Richmond, Sacramento, Saint Paul, Salem, Salt Lake City, San Diego,\nSan Francisco, Santa Fe, Seattle, Sioux Falls, Springfield, Tallahassee,\nToledo, Topeka, Trenton, Virginia Beach, Wichita, Wilmington\n\nThanks\n======\n\nThe sun calculations in this module were adapted, for Python, from the\nspreadsheets on the following page.\n\n    | https://www.esrl.noaa.gov/gmd/grad/solcalc/calcdetails.html\n\nRefraction calculation is taken from\n\n    | Sun-Pointing Programs and Their Accuracy\n    | John C. Zimmerman Of Sandia National Laboratones\n    | https://www.osti.gov/servlets/purl/6377969\n\nWhich cites the following as the original source\n\n    | In Solar Energy Vol 20 No.5-C\n    | Robert Walraven Of The University Of California, Davis\n\nMoon position calculations from\n\n   | LOW-PRECISION FORMULAE FOR PLANETARY POSITIONS\n   | T. C. Van Flandern and K. F. Pulkkinen\n\nAnd from\n\n   | Astronomical Algorithms\n   | Jean Meeus\n\nThe moon phase calculation is based on javascript code\nfrom Sky and Telescope magazine\n\n    | Moon-phase calculation\n    | Roger W. Sinnott, Sky & Telescope, June 16, 2006.\n    | https://skyandtelescope.org/observing/the-phase-of-the-moon/\n\nAlso to `Sphinx`_ for making doc generation an easy thing (not that the writing\nof the docs is any easier.)\n\nContact\n=======\n\nSimon Kennedy <sffjunkie+code@gmail.com>\n\nVersion History\n===============\n\n========== ====================================================================\nVersion    Description\n========== ====================================================================\n3.2        Dropped Python 3.6 support as it has reached \"End of Life\"\n\n           Documentation now hosted on\n           `Github Pages <https://sffjunkie.github.io/astral/>`_\n---------- --------------------------------------------------------------------\n3.1        Fix for `issue #77`_\n---------- --------------------------------------------------------------------\n3.0        Added moon rise, set, azimuth and zenith functions.https://github.com/sffjunkie/astral/issues/77\n\n           Switched from pytz to `zoneinfo` provided as part of Python >= 3.9 or\n           `backports.zoneinfo` for older versions.\n\n           In some circumstances the result of the calculation of rise and\n           set times would return information for a different date. This\n           has now been fixed.\n---------- --------------------------------------------------------------------\n2.2        Fix for `bug #48`_ - As per the bug report the angle to adjust for\n           the effect of elevation should have been θ (not α).\n\n           The sun functions can now also be passed a timezone name as a\n           string. Previously only a pytz timezone was accepted.\n---------- --------------------------------------------------------------------\n2.1        Fix for bug #44 - Incorrectly raised exception when UTC sun times\n           were on the day previous to the day asked for. Only manifested for\n           timezones with a large positive offset.\n---------- --------------------------------------------------------------------\n2.0        This is a code refactor as well as an update so it is highly likely\n           that you will need to adapt your code to suit.\n\n           Astral, AstralGeocoder & GoogleGeocoder classes removed\n\n           Now only compatible with Python 3.6 and greater due to the\n           use of data classes\n\n           New :class:`~astral.Observer` data class to store a latitude,\n           longitude & elevation\n\n           New :class:`~astral.LocationInfo` data class to store a location\n           name, region, timezone, latitude & longitude\n\n           Geocoder functions return a :class:`~astral.LocationInfo` instead\n           of a :class:`~astral.location.Location`\n\n           All calculations now automatically adjust for refraction.\n           For elevation you can return the true angle by setting the\n           `with_refraction` parameter to False.\n\n           The solar_noon and solar_midnight functions have been renamed to\n           :func:`~astral.sun.noon` and :func:`~astral.sun.midnight`\n           respectively.\n\n           Rahukaalam can now be calculated for night times.\n---------- --------------------------------------------------------------------\n1.10.1     Keyword args are now passed to the geocoder class from Astral\n           __init__ in order to allow the Google Maps API key to be passed to\n           the GoogleGeocoder.\n---------- --------------------------------------------------------------------\n1.10       Added support to AstralGeocoder to add\n           `additional locations <additional_locations_>`__\n           to the database.\n---------- --------------------------------------------------------------------\n1.9.2      1.9 broke the sun_utc method. Sun UTC calculation passed incorrect\n           parameter to more specific methods e.g. sunrise, sunset etc.\n---------- --------------------------------------------------------------------\n1.9.1      Correct version number in astral.py\n---------- --------------------------------------------------------------------\n1.9        Now takes elevation into account.\n---------- --------------------------------------------------------------------\n1.8        Location methods now allow the timezone to be None which returns all\n           times as UTC.\n\n           Added command line interface to return 'sun' values\n---------- --------------------------------------------------------------------\n1.7.1      Changed GoogleGeocoder test to not use raise...from as this is not\n           valid for Python 2\n---------- --------------------------------------------------------------------\n1.7        Requests is now only needed when using GoogleGeocoder\n\n           GoogleGeocoder now requires the `api_key` parameter to be passed to\n           the constructor as Google now require it for their API calls.\n---------- --------------------------------------------------------------------\n1.6.1      Updates for Travis CI integration / Github signed release.\n---------- --------------------------------------------------------------------\n1.6        Added api_key parameter to the GoogleGeocoder :meth:`__init__`\n           method\n---------- --------------------------------------------------------------------\n1.5        Added parameter `rtype` to :meth:`moon_phase` to determine the\n           return type of the method.\n\n           Added example for calculating the phase of the moon.\n---------- --------------------------------------------------------------------\n1.4.1      Using versioneer to manage version numbers\n---------- --------------------------------------------------------------------\n1.4        Changed to use calculations from NOAA spreadsheets\n\n           Changed some exception error messages for when sun does not reach\n           a requested elevation.\n\n           Added more tests\n---------- --------------------------------------------------------------------\n1.3.4      Changes to project configuration files. No user facing changes.\n---------- --------------------------------------------------------------------\n1.3.3      Fixed call to twilight_utc as date and direction parameters\n           were reversed.\n---------- --------------------------------------------------------------------\n1.3.2      Updated URL to point to gitgub.com\n\n           Added Apache 2.0 boilerplate to source file\n---------- --------------------------------------------------------------------\n1.3.1      Added LICENSE file to sdist\n---------- --------------------------------------------------------------------\n1.3        Corrected solar zenith to return the angle from the vertical.\n\n           Added solar midnight calculation.\n---------- --------------------------------------------------------------------\n1.2        Added handling for when unicode literals are used. This may possibly\n           affect your code if you're using Python 2 (there are tests for this\n           but they may not catch all uses.) (Bug `1588198`_\\)\n\n           Changed timezone for Phoenix, AZ to America/Phoenix\n           (Bug `1561258`_\\)\n---------- --------------------------------------------------------------------\n1.1        Added methods to calculate Twilight, the Golden Hour and the Blue\n           Hour.\n---------- --------------------------------------------------------------------\n1.0        It's time for a version 1.0\n\n           Added examples where the location you want is not in the Astral\n           geocoder.\n---------- --------------------------------------------------------------------\n0.9        Added a method to calculate the date and time when the sun is at a\n           specific elevation, for either a rising or a setting sun.\n\n           Added daylight and night methods to Location and Astral classes.\n\n           Rahukaalam methods now return a tuple.\n---------- --------------------------------------------------------------------\n0.8.2      Fix for moon phase calcualtions which were off by 1.\n\n           Use pytz.timezone().localize method instead of passing tzinfo\n           parameter to datetime.datetime. See the `pytz docs`_ for info\n---------- --------------------------------------------------------------------\n0.8.1      Fix for bug `1417641`_\\: :meth:`~astral.Astral.solar_elevation` and\n           :meth:`~astral.Astral.solar_azimuth` fail when a naive\n           :class:`~datetime.datetime` object is used.\n\n           Added :meth:`solar_zenith` methods to :class:`~astral.Astral`\n           and :class:`~astral.Location` as an\n           alias for :meth:`solar_elevation`\n\n           Added `tzinfo` as an alias for `tz`\n---------- --------------------------------------------------------------------\n0.8        Fix for bug `1407773`_\\: Moon phase calculation changed to remove\n           time zone parameter (tz) as it is not required for the calculation.\n---------- --------------------------------------------------------------------\n0.7.5      Fix for bug `1402103`_\\: Buenos Aires incorrect timezone\n---------- --------------------------------------------------------------------\n0.7.4      Added Canadian cities from Yip Shing Ho\n---------- --------------------------------------------------------------------\n0.7.3      Fix for bug `1239387`_ submitted by Torbjörn Lönnemark\n---------- --------------------------------------------------------------------\n0.7.2      Minor bug fix in :class:`~astral.GoogleGeocoder`. location name and\n           region are now stripped of whitespace\n---------- --------------------------------------------------------------------\n0.7.1      Bug fix. Missed a vital return statement in the\n           :class:`~astral.GoogleGeocoder`\n---------- --------------------------------------------------------------------\n0.7        Added ability to lookup location information from\n           Google's mapping APIs (see :class:`~astral.GoogleGeocoder`)\n\n           Renamed :class:`City` class to :class:`~astral.Location`\n\n           Renamed :class:`CityDB` to :class:`~astral.AstralGeocoder`\n\n           Added elevations of cities to database and property to\n           obtain elevation from :class:`~astral.Location` class\n---------- --------------------------------------------------------------------\n0.6.2      Added various cities to database as per\n           https://bugs.launchpad.net/astral/+bug/1040936\n---------- --------------------------------------------------------------------\n0.6.1      Docstrings were not updated to match changes to code.\n\n           Other minor docstring changes made\n---------- --------------------------------------------------------------------\n0.6        Fix for bug `884716`_ submitted by Martin Heemskerk\n           regarding moon phase calculations\n\n           Fixes for bug report `944754`_ submitted by Hajo Werder\n\n           - Changed co-ordinate system so that eastern longitudes\n             are now positive\n           - Added solar_depression property to City class\n---------- --------------------------------------------------------------------\n0.5        Changed :class:`City` to accept unicode name and country.\n\n           Moved city information into a database class :class:`CityDB`\n\n           Added attribute access to database for timezone groups\n---------- --------------------------------------------------------------------\n0.4        Duplicate city names could not be accessed.\n\n           Sun calculations for some cities failed with times\n           outside valid ranges.\n\n           Fixes for city data.\n\n           Added calculation for moon phase.\n---------- --------------------------------------------------------------------\n0.3        Changed to `Apache`_ V2.0 license.\n\n           Fix for bug `555508`_ submitted by me.\n\n           US state capitals and other cities added.\n---------- --------------------------------------------------------------------\n0.2        Fix for bug `554041`_ submitted by Derek\\_ / John Dimatos\n---------- --------------------------------------------------------------------\n0.1        First release\n========== ====================================================================\n\n.. _Rahukaalam: http://en.wikipedia.org/wiki/Rahukaalam\n.. _Sourceforge: http://pytz.sourceforge.net/\n.. _easy_install: http://peak.telecommunity.com/DevCenter/EasyInstall\n.. _Apache: http://www.opensource.org/licenses/apache2.0.php\n.. _Sphinx: https://www.sphinx-doc.org/\n.. _554041: https://bugs.launchpad.net/astral/+bug/554041\n.. _555508: https://bugs.launchpad.net/astral/+bug/555508\n.. _884716: https://bugs.launchpad.net/astral/+bug/884716\n.. _944754: https://bugs.launchpad.net/astral/+bug/944754\n.. _1239387: https://bugs.launchpad.net/astral/+bug/1239387\n.. _1402103: https://bugs.launchpad.net/astral/+bug/1402103\n.. _1407773: https://bugs.launchpad.net/astral/+bug/1407773\n.. _1417641: https://bugs.launchpad.net/astral/+bug/1417641\n.. _1561258: https://bugs.launchpad.net/astral/+bug/1561258\n.. _1588198: https://bugs.launchpad.net/astral/+bug/1588198\n.. _pytz docs: http://pytz.sourceforge.net/#localized-times-and-date-arithmetic\n.. _issue: https://github.com/sffjunkie/astral/issues\n.. _refraction: https://en.wikipedia.org/wiki/Refraction\n.. _pip: https://pip.pypa.io/en/stable/\n.. _bug #48: https://github.com/sffjunkie/astral/issues/48\n.. _issue #77: https://github.com/sffjunkie/astral/issues/77\n\n.. |ghaction_status| image:: https://img.shields.io/github/actions/workflow/status/sffjunkie/astral/astral-test.yml\n\n.. |pypi_ver| image:: https://img.shields.io/pypi/v/astral.svg\n    :target: https://pypi.org/project/astral/\n\n.. toctree::\n   :maxdepth: 2\n   :hidden:\n\n   package\n"
  },
  {
    "path": "src/docs/make.bat",
    "content": "@ECHO OFF\n\nREM Command file for Sphinx documentation\n\nif \"%SPHINXBUILD%\" == \"\" (\n\tset SPHINXBUILD=sphinx-build\n)\nset BUILDDIR=..\\..\\build\\sphinx\nset SOURCEDIR=.\n\nset ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% %SOURCEDIR%\nset I18NSPHINXOPTS=%SPHINXOPTS% %SOURCEDIR%\nif NOT \"%PAPER%\" == \"\" (\n\tset ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%\n\tset I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%\n)\n\nif \"%1\" == \"\" goto help\n\nif \"%1\" == \"help\" (\n\t:help\n\techo.Please use `make ^<target^>` where ^<target^> is one of\n\techo.  html       to make standalone HTML files\n\techo.  dirhtml    to make HTML files named index.html in directories\n\techo.  singlehtml to make a single large HTML file\n\techo.  pickle     to make pickle files\n\techo.  json       to make JSON files\n\techo.  htmlhelp   to make HTML files and a HTML help project\n\techo.  qthelp     to make HTML files and a qthelp project\n\techo.  devhelp    to make HTML files and a Devhelp project\n\techo.  epub       to make an epub\n\techo.  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter\n\techo.  text       to make text files\n\techo.  man        to make manual pages\n\techo.  texinfo    to make Texinfo files\n\techo.  gettext    to make PO message catalogs\n\techo.  changes    to make an overview over all changed/added/deprecated items\n\techo.  xml        to make Docutils-native XML files\n\techo.  pseudoxml  to make pseudoxml-XML files for display purposes\n\techo.  linkcheck  to check all external links for integrity\n\techo.  doctest    to run all doctests embedded in the documentation if enabled\n\tgoto end\n)\n\nif \"%1\" == \"clean\" (\n\tfor /d %%i in (%BUILDDIR%\\*) do rmdir /q /s %%i\n\tdel /q /s %BUILDDIR%\\*\n\tgoto end\n)\n\n\n%SPHINXBUILD% 2> nul\nif errorlevel 9009 (\n\techo.\n\techo.The 'sphinx-build' command was not found. Make sure you have Sphinx\n\techo.installed, then set the SPHINXBUILD environment variable to point\n\techo.to the full path of the 'sphinx-build' executable. Alternatively you\n\techo.may add the Sphinx directory to PATH.\n\techo.\n\techo.If you don't have Sphinx installed, grab it from\n\techo.http://sphinx-doc.org/\n\texit /b 1\n)\n\nif \"%1\" == \"html\" (\n\t%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished. The HTML pages are in %BUILDDIR%\\html.\n\tgoto end\n)\n\nif \"%1\" == \"dirhtml\" (\n\t%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished. The HTML pages are in %BUILDDIR%\\dirhtml.\n\tgoto end\n)\n\nif \"%1\" == \"singlehtml\" (\n\t%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished. The HTML pages are in %BUILDDIR%\\singlehtml.\n\tgoto end\n)\n\nif \"%1\" == \"pickle\" (\n\t%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished; now you can process the pickle files.\n\tgoto end\n)\n\nif \"%1\" == \"json\" (\n\t%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished; now you can process the JSON files.\n\tgoto end\n)\n\nif \"%1\" == \"htmlhelp\" (\n\t%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished; now you can run HTML Help Workshop with the ^\n.hhp project file in %BUILDDIR%\\htmlhelp.\n\tgoto end\n)\n\nif \"%1\" == \"qthelp\" (\n\t%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished; now you can run \"qcollectiongenerator\" with the ^\n.qhcp project file in %BUILDDIR%\\qthelp, like this:\n\techo.^> qcollectiongenerator %BUILDDIR%\\qthelp\\yyryr.qhcp\n\techo.To view the help file:\n\techo.^> assistant -collectionFile %BUILDDIR%\\qthelp\\yyryr.ghc\n\tgoto end\n)\n\nif \"%1\" == \"devhelp\" (\n\t%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished.\n\tgoto end\n)\n\nif \"%1\" == \"epub\" (\n\t%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished. The epub file is in %BUILDDIR%\\epub.\n\tgoto end\n)\n\nif \"%1\" == \"latex\" (\n\t%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished; the LaTeX files are in %BUILDDIR%\\latex.\n\tgoto end\n)\n\nif \"%1\" == \"latexpdf\" (\n\t%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex\n\tcd %BUILDDIR%/latex\n\tmake all-pdf\n\tcd %BUILDDIR%/..\n\techo.\n\techo.Build finished; the PDF files are in %BUILDDIR%\\latex.\n\tgoto end\n)\n\nif \"%1\" == \"latexpdfja\" (\n\t%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex\n\tcd %BUILDDIR%/latex\n\tmake all-pdf-ja\n\tcd %BUILDDIR%/..\n\techo.\n\techo.Build finished; the PDF files are in %BUILDDIR%\\latex.\n\tgoto end\n)\n\nif \"%1\" == \"text\" (\n\t%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished. The text files are in %BUILDDIR%\\text.\n\tgoto end\n)\n\nif \"%1\" == \"man\" (\n\t%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished. The manual pages are in %BUILDDIR%\\man.\n\tgoto end\n)\n\nif \"%1\" == \"texinfo\" (\n\t%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished. The Texinfo files are in %BUILDDIR%\\texinfo.\n\tgoto end\n)\n\nif \"%1\" == \"gettext\" (\n\t%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished. The message catalogs are in %BUILDDIR%\\locale.\n\tgoto end\n)\n\nif \"%1\" == \"changes\" (\n\t%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.The overview file is in %BUILDDIR%\\changes.\n\tgoto end\n)\n\nif \"%1\" == \"linkcheck\" (\n\t%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Link check complete; look for any errors in the above output ^\nor in %BUILDDIR%\\linkcheck\\output.txt.\n\tgoto end\n)\n\nif \"%1\" == \"doctest\" (\n\t%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Testing of doctests in the sources finished, look at the ^\nresults in %BUILDDIR%\\doctest\\output.txt.\n\tgoto end\n)\n\nif \"%1\" == \"xml\" (\n\t%SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished. The XML files are in %BUILDDIR%\\xml.\n\tgoto end\n)\n\nif \"%1\" == \"pseudoxml\" (\n\t%SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished. The pseudo-XML files are in %BUILDDIR%\\pseudoxml.\n\tgoto end\n)\n\n:end\n"
  },
  {
    "path": "src/docs/package.rst",
    "content": ".. Copyright 2009-2021, Simon Kennedy, sffjunkie+code@gmail.com\n\n.. Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n..   http://www.apache.org/licenses/LICENSE-2.0\n\n.. Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\nThe :mod:`astral` Package\n=========================\n\n.. automodule:: astral\n   :members:\n\nastral.sun\n~~~~~~~~~~~\n\n.. automodule:: astral.sun\n   :members:\n\nastral.moon\n~~~~~~~~~~~\n\n.. automodule:: astral.moon\n   :members:\n\nastral.geocoder\n~~~~~~~~~~~~~~~\n\n.. automodule:: astral.geocoder\n   :members:\n\nastral.location\n~~~~~~~~~~~~~~~\n\n.. autoclass:: astral.location.Location\n   :members:\n"
  },
  {
    "path": "src/docs/static/astral.css",
    "content": "img.adjustment {\r\n    margin: 1rem 1.5rem;\r\n}\r\n"
  },
  {
    "path": "src/test/almost_equal.py",
    "content": "import datetime\n\n\ndef datetime_almost_equal(\n    datetime1: datetime.datetime, datetime2: datetime.datetime, seconds: int = 60\n):\n    if not (datetime1.tzinfo):\n        datetime1 = datetime1.replace(tzinfo=datetime.timezone.utc)\n    else:\n        datetime1 = datetime1.astimezone(datetime.timezone.utc)\n\n    if not (datetime2.tzinfo):\n        datetime2 = datetime2.replace(tzinfo=datetime.timezone.utc)\n    else:\n        datetime2 = datetime2.astimezone(datetime.timezone.utc)\n\n    dd = datetime1 - datetime2\n    sd = (dd.days * 24 * 60 * 60) + dd.seconds\n    return abs(sd) <= seconds\n"
  },
  {
    "path": "src/test/conftest.py",
    "content": "import pytest  # type: ignore\n\nfrom astral import LocationInfo\nfrom astral.geocoder import LocationDatabase, database\nfrom astral.location import Location\n\n\n@pytest.fixture\ndef test_database() -> LocationDatabase:\n    return database()\n\n\n@pytest.fixture\ndef london_info() -> LocationInfo:\n    # return LocationInfo(\"London\", \"England\", \"Europe/London\", 51.50853, -0.12574)\n    return LocationInfo(\"London\", \"England\", \"Europe/London\", 51.5, -0.1333333)\n\n\n@pytest.fixture\ndef london(london_info: LocationInfo) -> Location:\n    return Location(london_info)\n\n\n@pytest.fixture\ndef new_delhi_info() -> LocationInfo:\n    return LocationInfo(\"New Delhi\", \"India\", \"Asia/Kolkata\", 28.61, 77.22)\n\n\n@pytest.fixture\ndef new_delhi(new_delhi_info: LocationInfo) -> Location:\n    return Location(new_delhi_info)\n\n\n@pytest.fixture\ndef riyadh_info() -> LocationInfo:\n    return LocationInfo(\"Riyadh\", \"Saudi Arabia\", \"Asia/Riyadh\", 24.71355, 46.67530)\n\n\n@pytest.fixture\ndef riyadh(riyadh_info: LocationInfo) -> Location:\n    return Location(riyadh_info)\n\n\n@pytest.fixture\ndef wellington_info() -> LocationInfo:\n    return LocationInfo(\n        \"Wellington\", \"New Zealand\", \"Pacific/Auckland\", -41.33, 174.766666\n    )\n\n\n@pytest.fixture\ndef wellington(wellington_info: LocationInfo) -> Location:\n    return Location(wellington_info)\n\n\n@pytest.fixture\ndef tromso_info() -> LocationInfo:\n    return LocationInfo(\"Tromso\", \"Norway\", \"CET\", 69.6, 18.95)\n\n\n@pytest.fixture\ndef tromso(tromso_info: LocationInfo) -> Location:\n    return Location(tromso_info)\n"
  },
  {
    "path": "src/test/moon/test_moon.py",
    "content": "# -*- coding: utf-8 -*-\nimport datetime\n\nimport pytest  # type: ignore\nfrom almost_equal import datetime_almost_equal\n\nfrom astral import moon\nfrom astral.location import Location\n\n\n@pytest.mark.parametrize(\n    \"date_,phase\",\n    [\n        (datetime.date(2015, 12, 1), 19.477889),\n        (datetime.date(2015, 12, 2), 20.333444),\n        (datetime.date(2015, 12, 3), 21.189000),\n        (datetime.date(2014, 12, 1), 9.0556666),\n        (datetime.date(2014, 12, 2), 10.066777),\n        (datetime.date(2014, 1, 1), 27.955666),\n    ],\n)\ndef test_moon_phase(date_: datetime.date, phase: float):\n    \"\"\"Test moon phase calculation\"\"\"\n    assert moon.phase(date_) == pytest.approx(phase, abs=0.001)  # type: ignore\n\n\n@pytest.mark.parametrize(\n    \"date_,risetime\",\n    [\n        (datetime.date(2022, 11, 30), datetime.datetime(2022, 11, 30, 13, 17, 0)),\n        (datetime.date(2022, 1, 1), datetime.datetime(2022, 1, 1, 6, 55, 0)),\n        (datetime.date(2022, 2, 1), datetime.datetime(2022, 2, 1, 8, 24, 0)),\n    ],\n)\ndef test_moonrise_utc(\n    date_: datetime.date, risetime: datetime.datetime, london: Location\n):\n    risetime = risetime.replace(tzinfo=london.tzinfo)\n    calc_time = moon.moonrise(london.observer, date_)\n    assert calc_time is not None\n    assert datetime_almost_equal(calc_time, risetime, seconds=300)\n\n\n@pytest.mark.parametrize(\n    \"date_,settime\",\n    [\n        (datetime.date(2021, 10, 28), datetime.datetime(2021, 10, 28, 14, 11, 0)),\n        (datetime.date(2021, 11, 6), datetime.datetime(2021, 11, 6, 17, 21, 0)),\n        (datetime.date(2022, 2, 1), datetime.datetime(2022, 2, 1, 16, 57, 0)),\n    ],\n)\ndef test_moonset_utc(\n    date_: datetime.date, settime: datetime.datetime, london: Location\n):\n    settime = settime.replace(tzinfo=datetime.timezone.utc)\n    calc_time = moon.moonset(london.observer, date_)\n    assert calc_time is not None\n    assert datetime_almost_equal(calc_time, settime, seconds=180)\n\n\n@pytest.mark.parametrize(\n    \"date_,risetime\",\n    [\n        (datetime.date(2022, 5, 1), datetime.datetime(2022, 5, 1, 2, 34, 0)),\n        (datetime.date(2022, 5, 24), datetime.datetime(2022, 5, 24, 22, 59, 0)),\n    ],\n)\ndef test_moonrise_riyadh_utc(\n    date_: datetime.date, risetime: datetime.datetime, riyadh: Location\n):\n    risetime = risetime.replace(tzinfo=datetime.timezone.utc)\n    calc_time = moon.moonrise(riyadh.observer, date_)\n    assert calc_time is not None\n    assert datetime_almost_equal(calc_time, risetime, seconds=180)\n\n\n@pytest.mark.parametrize(\n    \"date_,settime\",\n    [\n        (datetime.date(2021, 10, 28), datetime.datetime(2021, 10, 28, 9, 26, 0)),\n        (datetime.date(2021, 11, 6), datetime.datetime(2021, 11, 6, 15, 33, 0)),\n        (datetime.date(2022, 2, 1), datetime.datetime(2022, 2, 1, 14, 54, 0)),\n    ],\n)\ndef test_moonset_riyadh_utc(\n    date_: datetime.date, settime: datetime.datetime, riyadh: Location\n):\n    settime = settime.replace(tzinfo=datetime.timezone.utc)\n    calc_time = moon.moonset(riyadh.observer, date_)\n    assert calc_time is not None\n    assert datetime_almost_equal(calc_time, settime, seconds=180)\n\n\n@pytest.mark.parametrize(\n    \"date_,risetime\",\n    [\n        (datetime.date(2021, 10, 28), datetime.datetime(2021, 10, 28, 2, 6, 0)),\n        (datetime.date(2021, 11, 6), datetime.datetime(2021, 11, 6, 6, 45, 0)),\n    ],\n)\ndef test_moonrise_wellington(\n    date_: datetime.date, risetime: datetime.datetime, wellington: Location\n):\n    risetime = risetime.replace(tzinfo=wellington.tzinfo)\n    calc_time = moon.moonrise(wellington.observer, date_, tzinfo=wellington.tzinfo)\n    assert calc_time is not None\n    calc_time = calc_time.astimezone(wellington.tzinfo)\n    assert datetime_almost_equal(calc_time, risetime, seconds=120)\n\n\n@pytest.mark.parametrize(\n    \"date_,settime\",\n    [\n        (datetime.date(2021, 8, 18), datetime.datetime(2021, 8, 18, 3, 31, 0)),\n        (datetime.date(2021, 7, 8), datetime.datetime(2021, 7, 8, 15, 16, 0)),\n    ],\n)\ndef test_moonset_wellington(\n    date_: datetime.date, settime: datetime.datetime, wellington: Location\n):\n    settime = settime.replace(tzinfo=wellington.tzinfo)\n    calc_time = moon.moonset(wellington.observer, date_, wellington.tzinfo)\n    assert calc_time is not None\n    calc_time = calc_time.astimezone(wellington.tzinfo)\n    assert datetime_almost_equal(calc_time, settime, seconds=120)\n\n\n# @pytest.mark.parametrize(\n#     \"longitude,jd\",\n#     [\n#         (datetime.date(2021, 10, 28), datetime.datetime(2021, 10, 28, 13, 48, 0)),\n#         (datetime.date(2021, 11, 6), datetime.datetime(2021, 11, 6, 7, 27, 0)),\n#         # (datetime.date(2022, 2, 1), datetime.datetime(2022, 2, 1, 8, 24, 0)),\n#     ],\n# )\n# def test_moon_local_sidereal_time(longitude: float, jd: float):\n#     moon.local_sidereal_time(longitude, jd)\n"
  },
  {
    "path": "src/test/moon/test_moon_azimuth.py",
    "content": "import datetime\n\nimport pytest  # type: ignore\n\nfrom astral import Observer\nfrom astral.location import Location\nfrom astral.moon import azimuth\n\n\n@pytest.mark.parametrize(\n    \"dt,value\",\n    [\n        (datetime.datetime(2022, 10, 6, 1, 10, 0), 240.0),\n        (datetime.datetime(2022, 10, 6, 16, 45, 0), 115.0),\n        (datetime.datetime(2022, 10, 10, 6, 43, 0), 281.0),\n        (datetime.datetime(2022, 10, 10, 3, 0, 0), 235.0),\n    ],\n)\ndef test_moon_azimuth(dt: datetime.datetime, value: float, london: Location):\n    az = azimuth(london.observer, dt)\n    assert pytest.approx(az, abs=1) == value  # type: ignore\n\n\ndef print_moon_azimuth():\n    o = Observer(51.5, -0.13)\n    for hour in range(24):\n        d = datetime.datetime(2022, 10, 10, hour, 0, 0)\n        print(hour, \" 0\", azimuth(o, d))\n        d = datetime.datetime(2022, 10, 10, hour, 30, 0)\n        print(hour, \"30\", azimuth(o, d))\n\n\nif __name__ == \"__main__\":\n    print_moon_azimuth()\n"
  },
  {
    "path": "src/test/moon/test_moon_position.py",
    "content": "from datetime import date\n\nfrom astral.moon import julianday, moon_position\n\n\ndef test_moon_position():\n    d = date(1969, 6, 28)\n    jd = julianday(d)\n    jd2000 = jd - 2451545  # Julian day relative to Jan 1.5, 2000\n    moon_position(jd2000)\n\n    d = date(1992, 4, 12)\n    jd = julianday(d)\n    jd2000 = jd - 2451545  # Julian day relative to Jan 1.5, 2000\n    moon_position(jd2000)\n    pass\n\n\nif __name__ == \"__main__\":\n    test_moon_position()\n"
  },
  {
    "path": "src/test/moon/test_moon_rise.py",
    "content": "def test_moon_rise():\n    ...\n"
  },
  {
    "path": "src/test/moon/test_sidereal_time.py",
    "content": "import datetime\n\nfrom astral import hours_to_time\nfrom astral.sidereal import gmst, lmst\n\n\ndef test_gmst():\n    dt = datetime.datetime(1987, 4, 10, 0, 0, 0)\n    mean_sidereal_time = gmst(dt)\n\n    t = hours_to_time(mean_sidereal_time / 15)\n    assert t.hour == 13\n    assert t.minute == 10\n    assert t.second == 46\n    assert t.microsecond == 366821\n\n\ndef test_gmst_with_time():\n    dt = datetime.datetime(1987, 4, 10, 19, 21, 0)\n    mean_sidereal_time = gmst(dt)\n    t = hours_to_time(mean_sidereal_time / 15)\n    assert t.hour == 8\n    assert t.minute == 34\n    assert t.second == 57\n    assert t.microsecond == 89578\n\n\ndef test_local_mean_sidereal_time():\n    dt = datetime.datetime(1987, 4, 10, 0, 0, 0)\n    mean_sidereal_time = lmst(dt, -0.13)\n    assert mean_sidereal_time == 197.693195090862 - 0.13\n"
  },
  {
    "path": "src/test/test_Location.py",
    "content": "# -*- coding: utf-8 -*-\nimport dataclasses\nimport datetime\n\ntry:\n    import zoneinfo\nexcept ImportError:\n    from backports import zoneinfo  # type: ignore\n\nimport freezegun\nimport pytest  # type: ignore\nfrom almost_equal import datetime_almost_equal\n\nfrom astral import LocationInfo\nfrom astral.location import Location\n\n\nclass TestLocation:\n    \"\"\"Tests for the Location class\"\"\"\n\n    def test_Name(self):\n        \"\"\"Test the default name and that the name is changeable\"\"\"\n        c = Location()\n        assert c.name == \"Greenwich\"\n        c.name = \"Köln\"\n        assert c.name == \"Köln\"\n\n    def test_Region(self):\n        \"\"\"Test the default region and that the region is changeable\"\"\"\n        c = Location()\n        assert c.region == \"England\"\n        c.region = \"Australia\"\n        assert c.region == \"Australia\"\n\n    def test_TimezoneName(self):\n        \"\"\"Test the default timezone and that the timezone is changeable\"\"\"\n        c = Location()\n        assert c.timezone == \"Europe/London\"\n        c.name = \"Asia/Riyadh\"\n        assert c.name == \"Asia/Riyadh\"\n\n    def test_TimezoneNameBad(self):\n        \"\"\"Test that an exception is raised if an invalid timezone is specified\"\"\"\n        c = Location()\n        with pytest.raises(ValueError):\n            c.timezone = \"bad/timezone\"\n\n    def test_TimezoneLookup(self):\n        \"\"\"Test that tz refers to a timezone object\"\"\"\n        c = Location()\n        assert c.tz == zoneinfo.ZoneInfo(\"Europe/London\")  # type: ignore\n        c.timezone = \"Europe/Stockholm\"\n        assert c.tz == zoneinfo.ZoneInfo(\"Europe/Stockholm\")  # type: ignore\n\n    def test_Info(self, london: Location, london_info: LocationInfo):\n        assert london_info == london.info\n\n    def test_Sun(self, london: Location):\n        \"\"\"Test Location's version of the sun calculation\"\"\"\n        ldt = datetime.datetime(2015, 8, 1, 5, 23, 20, tzinfo=london.tzinfo)\n        sunrise = london.sun(datetime.date(2015, 8, 1))[\"sunrise\"]\n        assert datetime_almost_equal(sunrise, ldt)\n\n    def test_Dawn(self, london: Location):\n        \"\"\"Test Location returns dawn times in the local timezone\"\"\"\n        ldt = datetime.datetime(2015, 8, 1, 4, 41, 44, tzinfo=london.tzinfo)\n        dawn = london.dawn(datetime.date(2015, 8, 1))\n        assert datetime_almost_equal(dawn, ldt)\n        # assert dawn.tzinfo.zone == london.tzinfo.zone\n\n    def test_DawnUTC(self, london: Location):\n        \"\"\"Test Location returns dawn times in the UTC timezone\"\"\"\n        udt = datetime.datetime(2015, 8, 1, 3, 41, 44, tzinfo=datetime.timezone.utc)\n        dawn = london.dawn(datetime.date(2015, 8, 1), local=False)\n        assert datetime_almost_equal(dawn, udt)\n        # assert dawn.tzinfo.zone == datetime.timezone.utc.zone\n\n    def test_Sunrise(self, london: Location):\n        ldt = datetime.datetime(2015, 8, 1, 5, 23, 20, tzinfo=london.tzinfo)\n        sunrise = london.sunrise(datetime.date(2015, 8, 1))\n        assert datetime_almost_equal(sunrise, ldt)\n        # assert sunrise.tzinfo.zone == london.tzinfo.zone\n\n    def test_SunriseUTC(self, london: Location):\n        udt = datetime.datetime(2015, 8, 1, 4, 23, 20, tzinfo=datetime.timezone.utc)\n        sunrise = london.sunrise(datetime.date(2015, 8, 1), local=False)\n        assert datetime_almost_equal(sunrise, udt)\n        # assert sunrise.tzinfo.zone == datetime.timezone.utc.zone\n\n    def test_SolarNoon(self, london: Location):\n        ldt = datetime.datetime(2015, 8, 1, 13, 6, 53, tzinfo=london.tzinfo)\n        noon = london.noon(datetime.date(2015, 8, 1))\n        assert datetime_almost_equal(noon, ldt)\n        # assert noon.tzinfo.zone == london.tzinfo.zone\n\n    def test_SolarNoonUTC(self, london: Location):\n        udt = datetime.datetime(2015, 8, 1, 12, 6, 53, tzinfo=datetime.timezone.utc)\n        noon = london.noon(datetime.date(2015, 8, 1), local=False)\n        assert datetime_almost_equal(noon, udt)\n        # assert noon.tzinfo.zone == datetime.timezone.utc.zone\n\n    def test_Dusk(self, london: Location):\n        ldt = datetime.datetime(2015, 12, 1, 16, 35, 11, tzinfo=london.tzinfo)\n        dusk = london.dusk(datetime.date(2015, 12, 1))\n        assert datetime_almost_equal(dusk, ldt)\n        # assert dusk.tzinfo.zone == london.tzinfo.zone\n\n    def test_DuskUTC(self, london: Location):\n        udt = datetime.datetime(2015, 12, 1, 16, 35, 11, tzinfo=datetime.timezone.utc)\n        dusk = london.dusk(datetime.date(2015, 12, 1), local=False)\n        assert datetime_almost_equal(dusk, udt)\n        # assert dusk.tzinfo.zone == datetime.timezone.utc.zone\n\n    def test_Sunset(self, london: Location):\n        ldt = datetime.datetime(2015, 12, 1, 15, 55, 29, tzinfo=london.tzinfo)\n        sunset = london.sunset(datetime.date(2015, 12, 1))\n        assert datetime_almost_equal(sunset, ldt)\n        # assert sunset.tzinfo.zone == london.tzinfo.zone\n\n    def test_SunsetUTC(self, london: Location):\n        udt = datetime.datetime(2015, 12, 1, 15, 55, 29, tzinfo=datetime.timezone.utc)\n        sunset = london.sunset(datetime.date(2015, 12, 1), local=False)\n        assert datetime_almost_equal(sunset, udt)\n        # assert sunset.tzinfo.zone == datetime.timezone.utc.zone\n\n    def test_SolarElevation(self, riyadh: Location):\n        dt = datetime.datetime(2015, 12, 14, 8, 0, 0, tzinfo=riyadh.tzinfo)\n        elevation = riyadh.solar_elevation(dt)\n        assert abs(elevation - 17) < 0.5\n\n    def test_SolarAzimuth(self, riyadh: Location):\n        dt = datetime.datetime(2015, 12, 14, 8, 0, 0, tzinfo=riyadh.tzinfo)\n        azimuth = riyadh.solar_azimuth(dt)\n        assert abs(azimuth - 126) < 0.5\n\n    def test_TimeAtAltitude(self, new_delhi: Location):\n        test_data = {datetime.date(2016, 1, 5): datetime.datetime(2016, 1, 5, 10, 0)}\n\n        for day, cdt in test_data.items():\n            cdt = cdt.replace(tzinfo=new_delhi.tzinfo)\n            dt = new_delhi.time_at_elevation(28, day)\n            assert datetime_almost_equal(dt, cdt, seconds=600)\n\n    def test_SolarDepression(self):\n        c = Location(\n            LocationInfo(\"Heidelberg\", \"Germany\", \"Europe/Berlin\", 49.412, -8.71)\n        )\n        c.solar_depression = \"nautical\"\n        assert c.solar_depression == 12\n\n        c.solar_depression = 18\n        assert c.solar_depression == 18\n\n    def test_BadSolarDepression(self):\n        loc = Location()\n        with pytest.raises(KeyError):\n            loc.solar_depression = \"uncivil\"\n\n    def test_Moon(self):\n        d = datetime.date(2017, 12, 1)\n        c = Location()\n        assert c.moon_phase(date=d) == pytest.approx(11.62, abs=0.01)  # type: ignore\n\n    @freezegun.freeze_time(\"2015-12-01\")\n    def test_MoonNoDate(self):\n        c = Location()\n        assert c.moon_phase() == pytest.approx(19.47, abs=0.01)  # type: ignore\n\n    def test_TzError(self):\n        with pytest.raises(AttributeError):\n            c = Location()\n            c.tz = 1  # type: ignore\n\n    def test_Equality(self):\n        c1 = Location()\n        c2 = Location()\n        assert c1 == c2\n\n    def test_LocationEquality_NotEqual(self, london_info: LocationInfo):\n        location1 = Location(london_info)\n        location2 = Location(london_info)\n        location2.latitude = 23.0\n\n        assert location2 != location1\n\n    def test_LocationEquality_NotALocation(self, london_info: LocationInfo):\n        location = Location(london_info)\n\n        class NotALocation:\n            _location_info = london_info\n\n        assert NotALocation() != location\n\n    def test_SetLatitudeFloat(self):\n        loc = Location()\n        loc.latitude = 34.0\n        assert loc.latitude == 34.0\n\n    def test_SetLatitudeString(self):\n        loc = Location()\n        loc.latitude = \"24°28'N\"\n\n        assert loc.latitude == pytest.approx(24.46666666666666)  # type: ignore\n\n    def test_SetLongitudeFloat(self):\n        loc = Location()\n        loc.longitude = 24.0\n        assert loc.longitude == 24.0\n\n    def test_SetLongitudeString(self):\n        loc = Location()\n        loc.longitude = \"24°28'S\"\n\n        assert loc.longitude == pytest.approx(-24.46666666666666)  # type: ignore\n\n    def test_SetBadLongitudeString(self):\n        loc = Location()\n        with pytest.raises(ValueError):\n            loc.longitude = \"wibble\"\n\n    def test_BadTzinfo(self):\n        loc = Location()\n        loc._location_info = dataclasses.replace(  # type: ignore\n            loc._location_info, timezone=\"Bad/Timezone\"  # type: ignore\n        )\n\n        with pytest.raises(ValueError):\n            loc.tzinfo\n"
  },
  {
    "path": "src/test/test_Repr.py",
    "content": "# -*- coding: utf-8 -*-\nfrom astral import LocationInfo\nfrom astral.location import Location\n\n\nclass TestLocationRepr:\n    def test_default(self):\n        location = Location()\n        assert (\n            location.__repr__()\n            == \"Greenwich/England, tz=Europe/London, lat=51.47, lon=-0.00\"\n        )\n\n    def test_full(self):\n        location = Location(\n            LocationInfo(\"London\", \"England\", \"Europe/London\", 51.68, -0.05)\n        )\n        assert (\n            location.__repr__()\n            == \"London/England, tz=Europe/London, lat=51.68, lon=-0.05\"\n        )\n\n    def test_no_region(self):\n        location = Location(\n            LocationInfo(\n                \"London\",\n                None,\n                \"Europe/London\",\n                51.68,\n                -0.05,\n            )\n        )\n        assert location.__repr__() == (\"London, tz=Europe/London, lat=51.68, lon=-0.05\")\n"
  },
  {
    "path": "src/test/test_all.py",
    "content": "from astral.geocoder import LocationDatabase, all_locations\r\nfrom astral.sun import noon\r\n\r\n\r\ndef test_AllLocations(test_database: LocationDatabase):\r\n    for location in all_locations(test_database):\r\n        noon(location.observer)\r\n"
  },
  {
    "path": "src/test/test_almost_equal.py",
    "content": "import datetime\n\nfrom almost_equal import datetime_almost_equal\n\n\nclass TestDateTimeAlmostEqual:\n    \"\"\"Test the datetime comparison function\"\"\"\n\n    def test_equal(self):\n        d1 = datetime.datetime(2019, 1, 1)\n        d2 = datetime.datetime(2019, 1, 1)\n\n        assert datetime_almost_equal(d1, d2)\n\n    def test_not_equal(self):\n        d1 = datetime.datetime(2019, 1, 1)\n        d2 = datetime.datetime(2019, 1, 1, 12, 2, 0)\n\n        assert not datetime_almost_equal(d1, d2)\n\n    def test_equal_with_delta(self):\n        d1 = datetime.datetime(2019, 1, 1, 12, 0, 0)\n        d2 = datetime.datetime(2019, 1, 1, 12, 2, 0)\n\n        assert datetime_almost_equal(d1, d2, 121)\n"
  },
  {
    "path": "src/test/test_buenos_aries.py",
    "content": "# -*- coding: utf-8 -*-\nfrom astral.geocoder import LocationDatabase, lookup\nfrom astral.location import LocationInfo\n\n\ndef test_BuenosAries(test_database: LocationDatabase):\n    b = lookup(\"Buenos Aires\", test_database)\n    assert isinstance(b, LocationInfo)\n    assert b.timezone == \"America/Buenos_Aires\"\n"
  },
  {
    "path": "src/test/test_depression_not_reached.py",
    "content": "# -*- coding: utf-8 -*-\n\nimport datetime\n\nimport pytest  # type: ignore\n\nfrom astral import LocationInfo\nfrom astral.location import Location\n\n\ndef test_Dawn_NeverReachesDepression():\n    d = datetime.date(2016, 5, 29)\n    with pytest.raises(ValueError):\n        loc = Location(\n            LocationInfo(\n                \"Ghent\",\n                \"Belgium\",\n                \"Europe/Brussels\",\n                \"51°3'N\",\n                \"3°44'W\",\n            )  # type: ignore\n        )\n        loc.solar_depression = 18\n        loc.dawn(date=d, local=True)\n"
  },
  {
    "path": "src/test/test_geocoder.py",
    "content": "# -*- coding: utf-8 -*-\nfrom functools import reduce\nfrom typing import List\n\ntry:\n    import zoneinfo\nexcept ImportError:\n    from backports import zoneinfo  # type: ignore\n\nfrom pytest import approx, raises  # type: ignore\n\nimport astral.geocoder\nfrom astral import LocationInfo\nfrom astral.geocoder import LocationDatabase\n\n\ndef location_count(name: str, locations: List[LocationInfo]):\n    return len(list(filter(lambda item: item.name == name, locations)))\n\n\ndef db_location_count(db: LocationDatabase) -> int:  # type: ignore\n    \"\"\"Returns the count of the locations currently in the database\"\"\"\n    return reduce(lambda count, group: count + len(group), db.values(), 0)\n\n\nclass TestDatabase:\n    \"\"\"Test database access functions\"\"\"\n\n    def test_all_locations(self, test_database: astral.geocoder.LocationDatabase):\n        for loc in astral.geocoder.all_locations(test_database):\n            assert loc.name\n\n        location_list = astral.geocoder.all_locations(test_database)\n        all_locations = list(location_list)\n        assert location_count(\"London\", all_locations) == 1\n        assert location_count(\"Abu Dhabi\", all_locations) == 2\n\n    def test_lookup(self, test_database: astral.geocoder.LocationDatabase):\n        loc = astral.geocoder.lookup(\"London\", test_database)\n        assert isinstance(loc, LocationInfo)\n        assert loc.name == \"London\"\n        assert loc.region == \"England\"\n        assert loc.latitude == approx(51.4733, abs=0.001)\n        assert loc.longitude == approx(-0.0008333, abs=0.000001)\n        tz = zoneinfo.ZoneInfo(\"Europe/London\")  # type: ignore\n        tzl = zoneinfo.ZoneInfo(loc.timezone)  # type: ignore\n        assert tz == tzl\n\n    def test_city_in_db(self, test_database: astral.geocoder.LocationDatabase):\n        astral.geocoder.lookup(\"london\", test_database)\n\n    def test_group_in_db(self, test_database: astral.geocoder.LocationDatabase):\n        astral.geocoder.lookup(\"africa\", test_database)\n\n    def test_location_not_in_db(self, test_database: astral.geocoder.LocationDatabase):\n        with raises(KeyError):\n            astral.geocoder.lookup(\"Nowhere\", test_database)\n\n    def test_group_not_in_db(self, test_database: astral.geocoder.LocationDatabase):\n        with raises(KeyError):\n            astral.geocoder.group(\"wallyland\", test_database)\n\n    def test_lookup_city_and_region(\n        self, test_database: astral.geocoder.LocationDatabase\n    ):\n        city_name = \"Birmingham,England\"\n\n        city = astral.geocoder.lookup(city_name, test_database)\n        assert isinstance(city, LocationInfo)\n        assert city.name == \"Birmingham\"\n        assert city.region == \"England\"\n\n    def test_country_with_multiple_entries_no_country(\n        self, test_database: astral.geocoder.LocationDatabase\n    ):\n        city = astral.geocoder.lookup(\"Abu Dhabi\", test_database)\n        assert isinstance(city, LocationInfo)\n        assert city.name == \"Abu Dhabi\"\n\n    def test_country_with_multiple_entries_with_country(\n        self, test_database: astral.geocoder.LocationDatabase\n    ):\n        \"\"\"Test for fix made due to bug report from Klaus Alexander Seistrup\"\"\"\n\n        city = astral.geocoder.lookup(\"Abu Dhabi,United Arab Emirates\", test_database)\n        assert isinstance(city, LocationInfo)\n        assert city.name == \"Abu Dhabi\"\n\n        city = astral.geocoder.lookup(\"Abu Dhabi,UAE\", test_database)\n        assert isinstance(city, LocationInfo)\n        assert city.name == \"Abu Dhabi\"\n\n\nclass TestBugReports:\n    \"\"\"Test for bug report fixes\"\"\"\n\n    def test_Adelaide(self, test_database: astral.geocoder.LocationDatabase):\n        \"\"\"Test for fix made due to bug report from Klaus Alexander Seistrup\"\"\"\n\n        astral.geocoder.lookup(\"Adelaide\", test_database)\n\n    def test_CandianCities(self, test_database: astral.geocoder.LocationDatabase):\n        astral.geocoder.lookup(\"Fredericton\", test_database)\n\n\nclass TestDatabaseAddLocations:\n    \"\"\"Test adding locations to database\"\"\"\n\n    def test_newline_at_end(self, test_database: astral.geocoder.LocationDatabase):\n        count = db_location_count(test_database)\n        astral.geocoder.add_locations(\n            \"A Place,A Region,Asia/Nicosia,35°10'N,33°25'E,162.0\\n\", test_database\n        )\n        assert db_location_count(test_database) == count + 1\n\n    def test_from_list_of_strings(\n        self, test_database: astral.geocoder.LocationDatabase\n    ):\n        count = db_location_count(test_database)\n        astral.geocoder.add_locations(\n            [\n                \"A Place,A Region,Asia/Nicosia,35°10'N,33°25'E,162.0\",\n                \"Another Place,Somewhere else,Asia/Nicosia,35°10'N,33°25'E,162.0\",\n            ],\n            test_database,\n        )\n        assert db_location_count(test_database) == count + 2\n\n    def test_from_list_of_lists(self, test_database: astral.geocoder.LocationDatabase):\n        count = db_location_count(test_database)\n        astral.geocoder.add_locations(\n            [\n                [\"A Place\", \"A Region\", \"Asia/Nicosia\", \"35°10'N\", \"33°25'E\", \"162.0\"],\n                [\n                    \"Another Place\",\n                    \"Somewhere else\",\n                    \"Asia/Nicosia\",\n                    \"35°10'N\",\n                    \"33°25'E\",\n                    \"162.0\",\n                ],\n            ],\n            test_database,\n        )\n        assert db_location_count(test_database) == count + 2\n\n\ndef test_SanitizeKey():\n    assert astral.geocoder._sanitize_key(\"Los Angeles\") == \"los_angeles\"  # type: ignore\n"
  },
  {
    "path": "src/test/test_julian.py",
    "content": "# type: ignore\nimport datetime\nfrom typing import Union\n\nimport pytest\nfrom almost_equal import datetime_almost_equal\n\nfrom astral.julian import (\n    Calendar,\n    juliancentury_to_julianday,\n    julianday,\n    julianday_to_datetime,\n    julianday_to_juliancentury,\n)\n\n\n@pytest.mark.parametrize(\n    \"day,jd\",\n    [\n        (datetime.datetime(1957, 10, 4, 19, 26, 24), 2436116.31),\n        (datetime.date(2000, 1, 1), 2451544.5),\n        (datetime.date(2012, 1, 1), 2455927.5),\n        (datetime.date(2013, 1, 1), 2456293.5),\n        (datetime.date(2013, 6, 1), 2456444.5),\n        (datetime.date(1867, 2, 1), 2402998.5),\n        (datetime.date(3200, 11, 14), 2890153.5),\n        (datetime.datetime(2000, 1, 1, 12, 0, 0), 2451545.0),\n        (datetime.datetime(1999, 1, 1, 0, 0, 0), 2451179.5),\n        (datetime.datetime(1987, 1, 27, 0, 0, 0), 2446_822.5),\n        (datetime.date(1987, 6, 19), 2446_965.5),\n        (datetime.datetime(1987, 6, 19, 12, 0, 0), 2446_966.0),\n        (datetime.datetime(1988, 1, 27, 0, 0, 0), 2447_187.5),\n        (datetime.date(1988, 6, 19), 2447_331.5),\n        (datetime.datetime(1988, 6, 19, 12, 0, 0), 2447_332.0),\n        (datetime.datetime(1900, 1, 1, 0, 0, 0), 2415_020.5),\n        (datetime.datetime(1600, 1, 1, 0, 0, 0), 2305_447.5),\n        (datetime.datetime(1600, 12, 31, 0, 0, 0), 2305_812.5),\n        (datetime.datetime(2012, 1, 1, 12), 2455928.0),\n        (datetime.date(2013, 1, 1), 2456293.5),\n        (datetime.date(2013, 6, 1), 2456444.5),\n        (datetime.date(1867, 2, 1), 2402998.5),\n        (datetime.date(3200, 11, 14), 2890153.5),\n    ],\n)\ndef test_JulianDay(day: Union[datetime.date, datetime.datetime], jd: float):\n    assert julianday(day) == jd\n\n\n@pytest.mark.parametrize(\n    \"day,jd\",\n    [\n        (datetime.datetime(837, 4, 10, 7, 12, 0), 2026_871.8),\n        (datetime.datetime(333, 1, 27, 12, 0, 0), 1842_713.0),\n    ],\n)\ndef test_JulianDay_JulianCalendar(\n    day: Union[datetime.date, datetime.datetime], jd: float\n):\n    assert julianday(day, Calendar.JULIAN) == jd\n\n\n@pytest.mark.parametrize(\n    \"jd,dt\",\n    [\n        (2026_871.8, datetime.datetime(837, 4, 10, 7, 12, 0)),\n        (1842_713.0, datetime.datetime(333, 1, 27, 12, 0, 0)),\n    ],\n)\ndef test_JulianDay_ToDateTime(jd: float, dt: datetime.datetime):\n    assert datetime_almost_equal(julianday_to_datetime(jd), dt)\n\n\n@pytest.mark.parametrize(\n    \"jd,jc\",\n    [\n        (2455927.5, 0.119986311),\n        (2456293.5, 0.130006845),\n        (2456444.5, 0.134140999),\n        (2402998.5, -1.329130732),\n        (2890153.5, 12.00844627),\n    ],\n)\ndef test_JulianCentury(jd: float, jc: float):\n    assert julianday_to_juliancentury(jd) == pytest.approx(jc)\n\n\n@pytest.mark.parametrize(\n    \"jc,jd\",\n    [\n        (0.119986311, 2455927.5),\n        (0.130006845, 2456293.5),\n        (0.134140999, 2456444.5),\n        (-1.329130732, 2402998.5),\n        (12.00844627, 2890153.5),\n    ],\n)\ndef test_JulianCenturyToJulianDay(jc: float, jd: float):\n    assert juliancentury_to_julianday(jc) == pytest.approx(jd)\n"
  },
  {
    "path": "src/test/test_location_info.py",
    "content": "# type: ignore\nimport pytest\n\nfrom astral import LocationInfo\n\ntry:\n    import zoneinfo\nexcept ImportError:\n    from backports import zoneinfo  # type: ignore\n\n\nclass TestLocationInfo:\n    def test_Default(self):\n        loc = LocationInfo()\n        assert loc.name == \"Greenwich\"\n        assert loc.region == \"England\"\n        assert loc.timezone == \"Europe/London\"\n        assert loc.latitude == pytest.approx(51.4733, abs=0.001)\n        assert loc.longitude == pytest.approx(-0.0008333, abs=0.000001)\n\n    def test_bad_latitude(self):\n        with pytest.raises(ValueError):\n            LocationInfo(\"A place\", \"Somewhere\", \"Europe/London\", \"i\", 2)\n\n    def test_bad_longitude(self):\n        with pytest.raises(ValueError):\n            LocationInfo(\"A place\", \"Somewhere\", \"Europe/London\", 2, \"i\")\n\n    def test_timezone_group(self):\n        li = LocationInfo()\n        assert li.timezone_group == \"Europe\"\n\n    def test_tzinfo(self, new_delhi_info: LocationInfo):\n        assert new_delhi_info.tzinfo == zoneinfo.ZoneInfo(\"Asia/Kolkata\")\n"
  },
  {
    "path": "src/test/test_misc.py",
    "content": "# type: ignore\nfrom datetime import timedelta\n\ntry:\n    import zoneinfo\nexcept ImportError:\n    from backports import zoneinfo  # type: ignore\n\nimport freezegun\nfrom pytest import approx, raises\n\nfrom astral import dms_to_float, now, today\nfrom astral.sun import minutes_to_timedelta\n\n\ndef test_MinutesToTimedelta():\n    assert minutes_to_timedelta(720) == timedelta(seconds=720 * 60)\n    assert minutes_to_timedelta(722) == timedelta(seconds=722 * 60)\n    assert minutes_to_timedelta(722.2) == timedelta(seconds=722.2 * 60)\n    assert minutes_to_timedelta(722.5) == timedelta(seconds=722.5 * 60)\n\n\nclass TestDMS:\n    \"\"\"Test degrees/minutes/seconds conversion functions\"\"\"\n\n    def test_north(self):\n        assert dms_to_float(\"24°28'N\", 90) == approx(24.466666)\n\n    def test_whole_number_of_degrees(self):\n        assert dms_to_float(\"24°\", 90.0) == 24.0\n\n    def test_east(self):\n        assert dms_to_float(\"54°22'E\", 180.0) == approx(54.366666, abs=0.00001)\n\n    def test_south(self):\n        assert dms_to_float(\"37°58'S\", 90.0) == approx(-37.966666, abs=0.00001)\n\n    def test_west(self):\n        assert dms_to_float(\"171°50'W\", 180.0) == approx(-171.8333333, abs=0.00001)\n\n    def test_west_lowercase(self):\n        assert dms_to_float(\"171°50'w\", 180.0) == approx(-171.8333333, abs=0.00001)\n\n    def test_float(self):\n        assert dms_to_float(\"0.2\", 90.0) == 0.2\n\n    def test_not_a_float(self):\n        with raises(ValueError):\n            dms_to_float(\"x\", 90.0)\n\n    def test_latlng_outside_limit(self):\n        assert dms_to_float(\"180°50'w\", 180.0) == -180\n\n\nclass TestToday:\n    @freezegun.freeze_time(\"2020-01-01 14:00:00\")\n    def test_default_timezone(self):\n        td = today()\n        assert td.year == 2020\n        assert td.month == 1\n        assert td.day == 1\n\n    @freezegun.freeze_time(\"2020-01-01 14:00:00\")\n    def test_australia(self):\n        assert today(zoneinfo.ZoneInfo(\"Australia/Melbourne\")).day == 2\n\n    @freezegun.freeze_time(\"2020-01-02 05:00:00\")\n    def test_adak(self):\n        assert today(zoneinfo.ZoneInfo(\"America/Adak\")).day == 1\n\n\nclass TestNow:\n    @freezegun.freeze_time(\"2020-01-01 14:10:20\")\n    def test_default_timezone(self):\n        td = now()\n        assert td.hour == 14\n        assert td.minute == 10\n        assert td.second == 20\n\n    @freezegun.freeze_time(\"2020-01-01 14:20:00\")\n    def test_australia(self):\n        td = now(zoneinfo.ZoneInfo(\"Australia/Melbourne\"))\n        assert td.hour == 1\n        assert td.minute == 20\n"
  },
  {
    "path": "src/test/test_norway.py",
    "content": "from datetime import datetime, timedelta, timezone\n\nimport pytest  # type: ignore\n\nimport astral\nfrom astral import sun\nfrom astral.location import Location\n\n\ndef _next_event(obs: astral.Observer, dt: datetime, event: str):\n    for offset in range(0, 365):\n        newdate = dt + timedelta(days=offset)\n        try:\n            t = getattr(sun, event)(date=newdate, observer=obs)\n            return t\n        except ValueError:\n            pass\n    assert False, \"Should be unreachable\"  # pragma: no cover\n\n\ndef test_NorwaySunUp(tromso: Location):\n    \"\"\"Test location in Norway where the sun doesn't set in summer.\"\"\"\n    june = datetime(2019, 6, 5, tzinfo=timezone.utc)\n\n    with pytest.raises(ValueError):\n        sun.sunrise(tromso.observer, june)\n    with pytest.raises(ValueError):\n        sun.sunset(tromso.observer, june)\n\n    # Find the next sunset and sunrise:\n    next_sunrise = _next_event(tromso.observer, june, \"sunrise\")\n    next_sunset = _next_event(tromso.observer, june, \"sunset\")\n\n    assert next_sunset < next_sunrise\n"
  },
  {
    "path": "src/test/test_observer.py",
    "content": "# type: ignore\nimport pytest\n\nfrom astral import Observer\n\n\nclass TestObserver:\n    def test_default(self):\n        obs = Observer()\n        assert obs.latitude == 51.4733\n        assert obs.longitude == -0.0008333\n        assert obs.elevation == 0.0\n\n    def test_from_float(self):\n        obs = Observer(1, 1, 1)\n        assert obs.latitude == 1.0\n        assert obs.longitude == 1.0\n        assert obs.elevation == 1.0\n\n    def test_from_string(self):\n        obs = Observer(\"1\", \"2\", \"3\")\n        assert obs.latitude == 1.0\n        assert obs.longitude == 2.0\n        assert obs.elevation == 3.0\n\n    def test_from_dms(self):\n        obs = Observer(\"24°N\", \"22°30'S\", \"3\")\n        assert obs.latitude == 24.0\n        assert obs.longitude == -22.5\n        assert obs.elevation == 3.0\n\n    def test_bad_latitude(self):\n        with pytest.raises(ValueError):\n            Observer(\"o\", 1, 1)\n\n    def test_bad_longitude(self):\n        with pytest.raises(ValueError):\n            Observer(1, \"o\", 1)\n\n    def test_bad_elevation(self):\n        with pytest.raises(ValueError):\n            Observer(1, 1, \"o\")\n\n    def test_latitude_outside_limits(self):\n        obs = Observer(90.1, 0, 0)\n        assert obs.latitude == 90.0\n        obs = Observer(-90.1, 0, 0)\n        assert obs.latitude == -90.0\n\n    def test_longitude_outside_limits(self):\n        obs = Observer(0, 180.1, 0)\n        assert obs.longitude == 180.0\n        obs = Observer(0, -180.1, 0)\n        assert obs.longitude == -180.0\n"
  },
  {
    "path": "src/test/test_sun_calc.py",
    "content": "import datetime\n\nimport freezegun\nimport pytest  # type: ignore\n\nfrom astral import Observer, sun, today\nfrom astral.location import Location\n\n\n@pytest.mark.parametrize(\n    \"jc,gmls\",\n    [\n        (-1.329130732, 310.7374254),\n        (12.00844627, 233.8203529),\n        (0.184134155, 69.43779106),\n    ],\n)\ndef test_GeomMeanLongSun(jc: float, gmls: float):\n    assert sun.geom_mean_long_sun(jc) == pytest.approx(gmls)  # type: ignore\n\n\n@pytest.mark.parametrize(\n    \"jc,gmas\",\n    [\n        (0.119986311, 4676.922342),\n        (12.00844627, 432650.1681),\n        (0.184134155, 6986.1838),\n    ],\n)\ndef test_GeomAnomolyLongSun(jc: float, gmas: float):\n    assert sun.geom_mean_anomaly_sun(jc) == pytest.approx(gmas)  # type: ignore\n\n\n@pytest.mark.parametrize(\n    \"jc,eeo\",\n    [\n        (0.119986311, 0.016703588),\n        (12.00844627, 0.016185564),\n        (0.184134155, 0.016700889),\n    ],\n)\ndef test_EccentricityEarthOrbit(jc: float, eeo: float):\n    assert sun.eccentric_location_earth_orbit(jc) == pytest.approx(\n        eeo, abs=1e-6\n    )  # type: ignore\n\n\n@pytest.mark.parametrize(\n    \"jc,eos\",\n    [\n        (0.119986311, -0.104951648),\n        (12.00844627, -1.753028843),\n        (0.184134155, 1.046852316),\n    ],\n)\ndef test_SunEqOfCenter(jc: float, eos: float):\n    assert sun.sun_eq_of_center(jc) == pytest.approx(eos, abs=1e-6)  # type: ignore\n\n\n@pytest.mark.parametrize(\n    \"jc,stl\",\n    [\n        (0.119986311, 279.9610686),\n        (12.00844627, 232.0673358),\n        (0.184134155, 70.48465428),\n    ],\n)\ndef test_SunTrueLong(jc: float, stl: float):\n    assert sun.sun_true_long(jc) == pytest.approx(stl, abs=0.001)  # type: ignore\n\n\n@pytest.mark.parametrize(\n    \"jc,sta\",\n    [\n        (0.119986311, 4676.817391),\n        (12.00844627, 432648.4151),\n        (0.184134155, 6987.230663),\n    ],\n)\ndef test_SunTrueAnomaly(jc: float, sta: float):\n    assert sun.sun_true_anomoly(jc) == pytest.approx(sta, abs=0.001)  # type: ignore\n\n\n@pytest.mark.parametrize(\n    \"jc,srv\",\n    [\n        (0.119986311, 0.983322329),\n        (12.00844627, 0.994653382),\n        (0.184134155, 1.013961204),\n    ],\n)\ndef test_SunRadVector(jc: float, srv: float):\n    assert sun.sun_rad_vector(jc) == pytest.approx(srv, abs=0.001)  # type: ignore\n\n\n@pytest.mark.parametrize(\n    \"jc,sal\",\n    [\n        (0.119986311, 279.95995849827),\n        (12.00844627, 232.065823531804),\n        (0.184134155, 70.475244256027),\n    ],\n)\ndef test_SunApparentLong(jc: float, sal: float):\n    assert sun.sun_apparent_long(jc) == pytest.approx(sal)  # type: ignore\n\n\n@pytest.mark.parametrize(\n    \"jc,mooe\",\n    [\n        (0.119986311, 23.4377307876356),\n        (12.00844627, 23.2839797200388),\n        (0.184134155, 23.4368965974579),\n    ],\n)\ndef test_MeanObliquityOfEcliptic(jc: float, mooe: float):\n    assert sun.mean_obliquity_of_ecliptic(jc) == pytest.approx(mooe)  # type: ignore\n\n\n@pytest.mark.parametrize(\n    \"jc,oc\",\n    [\n        (0.119986311, 23.4369810410121),\n        (12.00844627, 23.2852236361575),\n        (0.184134155, 23.4352890293474),\n    ],\n)\ndef test_ObliquityCorrection(jc: float, oc: float):\n    assert sun.obliquity_correction(jc) == pytest.approx(oc, abs=0.001)  # type: ignore\n\n\n@pytest.mark.parametrize(\n    \"jc,sra\",\n    [\n        (0.119986311, -79.16480352),\n        (12.00844627, -130.3163904),\n        (0.184134155, 68.86915896),\n    ],\n)\ndef test_SunRtAscension(jc: float, sra: float):\n    assert sun.sun_rt_ascension(jc) == pytest.approx(sra, abs=0.001)  # type: ignore\n\n\n@pytest.mark.parametrize(\n    \"jc,sd\",\n    [\n        (0.119986311, -23.06317068),\n        (12.00844627, -18.16694394),\n        (0.184134155, 22.01463552),\n    ],\n)\ndef test_SunDeclination(jc: float, sd: float):\n    assert sun.sun_declination(jc) == pytest.approx(sd, abs=0.001)  # type: ignore\n\n\n@pytest.mark.parametrize(\n    \"jc,eot\",\n    [\n        (0.119986311, -3.078194825),\n        (12.00844627, 16.58348133),\n        (0.184134155, 2.232039737),\n    ],\n)\ndef test_EquationOfTime(jc: float, eot: float):\n    assert sun.eq_of_time(jc) == pytest.approx(eot)  # type: ignore\n\n\n@pytest.mark.parametrize(\n    \"d,ha\",\n    [\n        (datetime.date(2012, 1, 1), 1.03555238),\n        (datetime.date(3200, 11, 14), 1.172253118),\n        (datetime.date(2018, 6, 1), 2.133712555),\n    ],\n)\ndef test_HourAngle(d: datetime.date, ha: float, london: Location):\n    midday = datetime.time(12, 0, 0)\n    jd = sun.julianday(datetime.datetime.combine(d, midday))\n    jc = sun.julianday_to_juliancentury(jd)\n    decl = sun.sun_declination(jc)\n\n    assert sun.hour_angle(\n        london.latitude, decl, 90.8333, sun.SunDirection.RISING\n    ) == pytest.approx(  # type: ignore\n        ha, abs=0.1\n    )\n\n\ndef test_Azimuth(new_delhi: Location):\n    d = datetime.datetime(2001, 6, 21, 13, 11, 0)\n    assert sun.azimuth(new_delhi.observer, d) == pytest.approx(\n        292.76, abs=0.1\n    )  # type: ignore\n\n\ndef test_Elevation(new_delhi: Location):\n    d = datetime.datetime(2001, 6, 21, 13, 11, 0)\n    assert sun.elevation(new_delhi.observer, d) == pytest.approx(\n        7.41, abs=0.1\n    )  # type: ignore\n\n\ndef test_Elevation_NonNaive(new_delhi: Location):\n    d = datetime.datetime(2001, 6, 21, 18, 41, 0, tzinfo=new_delhi.tzinfo)\n    assert sun.elevation(new_delhi.observer, d) == pytest.approx(\n        7.41, abs=0.1\n    )  # type: ignore\n\n\ndef test_Elevation_WithoutRefraction(new_delhi: Location):\n    d = datetime.datetime(2001, 6, 21, 13, 11, 0)\n    assert sun.elevation(\n        new_delhi.observer, d, with_refraction=False\n    ) == pytest.approx(  # type: ignore\n        7.29, abs=0.1\n    )\n\n\ndef test_Azimuth_Above85Degrees():\n    d = datetime.datetime(2001, 6, 21, 13, 11, 0)\n    assert sun.azimuth(Observer(86, 77.2), d) == pytest.approx(\n        276.21, abs=0.1\n    )  # type: ignore\n\n\ndef test_Elevation_Above85Degrees():\n    d = datetime.datetime(2001, 6, 21, 13, 11, 0)\n    assert sun.elevation(Observer(86, 77.2), d) == pytest.approx(  # type: ignore\n        23.102501151619506, abs=0.001\n    )\n\n\n@pytest.mark.parametrize(\"elevation\", range(1, 20))\n@freezegun.freeze_time(\"2020-02-06\")\ndef test_ElevationEqualsTimeAtElevation(elevation: float, london: Location):\n    o = london.observer\n    td = today()\n    et = sun.time_at_elevation(o, elevation, td, with_refraction=True)\n    sun_elevation = sun.elevation(o, et, with_refraction=True)\n    assert sun_elevation == pytest.approx(elevation, abs=0.1)  # type: ignore\n"
  },
  {
    "path": "src/test/test_sun_elevation_adjustment.py",
    "content": "# -*- coding: utf-8 -*-\r\nimport pytest  # type: ignore\r\n\r\nfrom astral.sun import adjust_to_horizon, adjust_to_obscuring_feature\r\n\r\n\r\nclass TestElevationAdjustment:\r\n    def test_Float_Positive(self):\r\n        adjustment = adjust_to_horizon(12000)\r\n        assert adjustment == pytest.approx(3.517744168209966)\r\n\r\n    def test_Float_Negative(self):\r\n        adjustment = adjust_to_horizon(-1)\r\n        assert adjustment == pytest.approx(0)\r\n\r\n    def test_Tuple_0(self):\r\n        adjustment = adjust_to_obscuring_feature((0.0, 100.0))\r\n        assert adjustment == 0.0\r\n\r\n    def test_Tuple_45deg(self):\r\n        adjustment = adjust_to_obscuring_feature((10.0, 10.0))\r\n        assert adjustment == pytest.approx(45.0)\r\n\r\n    def test_Tuple_30deg(self):\r\n        adjustment = adjust_to_obscuring_feature((3.0, 4.0))\r\n        assert adjustment == pytest.approx(53.130102354156)\r\n\r\n    def test_Tuple_neg45deg(self):\r\n        adjustment = adjust_to_obscuring_feature((-10.0, 10.0))\r\n        assert adjustment == pytest.approx(-45.0)\r\n"
  },
  {
    "path": "src/test/test_sun_golden_blue.py",
    "content": "# -*- coding: utf-8 -*-\n# Test data taken from http://www.timeanddate.com/sun/uk/london\n\nimport datetime\n\nimport freezegun\nimport pytest  # type: ignore\nfrom almost_equal import datetime_almost_equal\n\nfrom astral import TimePeriod, sun\nfrom astral.location import Location\nfrom astral.sun import SunDirection\n\n\nclass TestGoldenHour:\n    \"\"\"Tests for the golden_hour function\"\"\"\n\n    @pytest.mark.parametrize(\n        \"day,golden_hour\",\n        [\n            (\n                datetime.date(2015, 12, 1),\n                (\n                    datetime.datetime(2015, 12, 1, 1, 10, 10),\n                    datetime.datetime(2015, 12, 1, 2, 0, 43),\n                ),\n            ),\n            (\n                datetime.date(2016, 1, 1),\n                (\n                    datetime.datetime(2016, 1, 1, 1, 27, 46),\n                    datetime.datetime(2016, 1, 1, 2, 19, 1),\n                ),\n            ),\n        ],\n    )\n    def test_morning(\n        self, day: datetime.date, golden_hour: TimePeriod, new_delhi: Location\n    ):\n        start1 = golden_hour[0].replace(tzinfo=datetime.timezone.utc)\n        end1 = golden_hour[1].replace(tzinfo=datetime.timezone.utc)\n\n        start2, end2 = sun.golden_hour(\n            new_delhi.observer,\n            day,\n            SunDirection.RISING,\n        )\n        assert datetime_almost_equal(end1, end2, seconds=90)\n        assert datetime_almost_equal(start1, start2, seconds=90)\n\n    def test_evening(self, london: Location):\n        test_data = {\n            datetime.date(2016, 5, 18): (\n                datetime.datetime(2016, 5, 18, 19, 2),\n                datetime.datetime(2016, 5, 18, 20, 17),\n            )\n        }\n\n        for day, golden_hour in test_data.items():\n            start1 = golden_hour[0].replace(tzinfo=datetime.timezone.utc)\n            end1 = golden_hour[1].replace(tzinfo=datetime.timezone.utc)\n\n            start2, end2 = sun.golden_hour(\n                london.observer,\n                day,\n                SunDirection.SETTING,\n            )\n            assert datetime_almost_equal(end1, end2, seconds=60)\n            assert datetime_almost_equal(start1, start2, seconds=60)\n\n    @freezegun.freeze_time(\"2015-12-1\")\n    def test_no_date(self, new_delhi: Location):\n        start = datetime.datetime(2015, 12, 1, 1, 10, 10, tzinfo=datetime.timezone.utc)\n        end = datetime.datetime(2015, 12, 1, 2, 0, 43, tzinfo=datetime.timezone.utc)\n        ans = sun.golden_hour(new_delhi.observer)\n        assert datetime_almost_equal(ans[0], start, 90)\n        assert datetime_almost_equal(ans[1], end, 90)\n\n\nclass TestBlueHour:\n    \"\"\"Tests for the blue_hour function\"\"\"\n\n    def test_morning(self, london: Location):\n        test_data = {\n            datetime.date(2016, 5, 19): (\n                datetime.datetime(2016, 5, 19, 3, 19),\n                datetime.datetime(2016, 5, 19, 3, 36),\n            )\n        }\n\n        for day, blue_hour in test_data.items():\n            start1 = blue_hour[0].replace(tzinfo=datetime.timezone.utc)\n            end1 = blue_hour[1].replace(tzinfo=datetime.timezone.utc)\n\n            start2, end2 = sun.blue_hour(london.observer, day, SunDirection.RISING)\n            assert datetime_almost_equal(end1, end2, seconds=90)\n            assert datetime_almost_equal(start1, start2, seconds=90)\n\n    def test_evening(self, london: Location):\n        test_data = {\n            datetime.date(2016, 5, 19): (\n                datetime.datetime(2016, 5, 19, 20, 18),\n                datetime.datetime(2016, 5, 19, 20, 35),\n            )\n        }\n\n        for day, blue_hour in test_data.items():\n            start1 = blue_hour[0].replace(tzinfo=datetime.timezone.utc)\n            end1 = blue_hour[1].replace(tzinfo=datetime.timezone.utc)\n\n            start2, end2 = sun.blue_hour(london.observer, day, SunDirection.SETTING)\n            assert datetime_almost_equal(end1, end2, seconds=90)\n            assert datetime_almost_equal(start1, start2, seconds=90)\n\n    @freezegun.freeze_time(\"2016-5-19\")\n    def test_no_date(self, london: Location):\n        start = datetime.datetime(2016, 5, 19, 20, 18, tzinfo=datetime.timezone.utc)\n        end = datetime.datetime(2016, 5, 19, 20, 35, tzinfo=datetime.timezone.utc)\n        ans = sun.blue_hour(london.observer, direction=SunDirection.SETTING)\n        assert datetime_almost_equal(ans[0], start, 90)\n        assert datetime_almost_equal(ans[1], end, 90)\n"
  },
  {
    "path": "src/test/test_sun_local.py",
    "content": "import datetime\n\nimport pytest  # type: ignore\nfrom almost_equal import datetime_almost_equal\n\nfrom astral import sun\nfrom astral.location import Location\n\n\n@pytest.mark.parametrize(\n    \"day,dawn\",\n    [\n        (datetime.date(2015, 12, 1), datetime.datetime(2015, 12, 1, 6, 30)),\n        (datetime.date(2015, 12, 2), datetime.datetime(2015, 12, 2, 6, 31)),\n        (datetime.date(2015, 12, 3), datetime.datetime(2015, 12, 3, 6, 31)),\n        (datetime.date(2015, 12, 12), datetime.datetime(2015, 12, 12, 6, 38)),\n        (datetime.date(2015, 12, 25), datetime.datetime(2015, 12, 25, 6, 45)),\n    ],\n)\ndef test_Sun_Local_tzinfo(\n    day: datetime.date, dawn: datetime.datetime, new_delhi: Location\n):\n    dawn = dawn.replace(tzinfo=new_delhi.tzinfo)\n    dawn_calc = sun.sun(new_delhi.observer, day, 6.0, new_delhi.tzinfo)[\"dawn\"]\n    assert datetime_almost_equal(dawn, dawn_calc)\n\n\n@pytest.mark.parametrize(\n    \"day,dawn\",\n    [\n        (datetime.date(2015, 12, 1), datetime.datetime(2015, 12, 1, 6, 30)),\n        (datetime.date(2015, 12, 2), datetime.datetime(2015, 12, 2, 6, 31)),\n        (datetime.date(2015, 12, 3), datetime.datetime(2015, 12, 3, 6, 31)),\n        (datetime.date(2015, 12, 12), datetime.datetime(2015, 12, 12, 6, 38)),\n        (datetime.date(2015, 12, 25), datetime.datetime(2015, 12, 25, 6, 45)),\n    ],\n)\ndef test_Sun_Local_str(\n    day: datetime.date, dawn: datetime.datetime, new_delhi: Location\n):\n    dawn = dawn.replace(tzinfo=new_delhi.tzinfo)\n    dawn_calc = sun.sun(new_delhi.observer, day, 6.0, \"Asia/Kolkata\")[\"dawn\"]\n    assert datetime_almost_equal(dawn, dawn_calc)\n"
  },
  {
    "path": "src/test/test_sun_utc.py",
    "content": "# type: ignore\n# Test data taken from http://www.timeanddate.com/sun/uk/london\n\nimport datetime\nfrom typing import Tuple\n\nimport freezegun\nimport pytest\nfrom almost_equal import datetime_almost_equal\n\nfrom astral import LocationInfo, TimePeriod, sun\nfrom astral.sun import Depression, SunDirection\n\n\n@pytest.mark.parametrize(\n    \"day,dawn\",\n    [\n        (datetime.date(2015, 12, 1), datetime.datetime(2015, 12, 1, 7, 4)),\n        (datetime.date(2015, 12, 2), datetime.datetime(2015, 12, 2, 7, 5)),\n        (datetime.date(2015, 12, 3), datetime.datetime(2015, 12, 3, 7, 6)),\n        (datetime.date(2015, 12, 12), datetime.datetime(2015, 12, 12, 7, 16)),\n        (datetime.date(2015, 12, 25), datetime.datetime(2015, 12, 25, 7, 25)),\n    ],\n)\ndef test_Sun(day: datetime.date, dawn: datetime.datetime, london: LocationInfo):\n    dawn = dawn.replace(tzinfo=datetime.timezone.utc)\n    dawn_utc = sun.sun(london.observer, day)[\"dawn\"]\n    assert datetime_almost_equal(dawn, dawn_utc)\n\n\n@freezegun.freeze_time(\"2015-12-01\")\ndef test_Sun_NoDate(london: LocationInfo):\n    ans = datetime.datetime(2015, 12, 1, 7, 4, tzinfo=datetime.timezone.utc)\n    assert datetime_almost_equal(sun.sun(london.observer)[\"dawn\"], ans)\n\n\n@pytest.mark.parametrize(\n    \"day,dawn\",\n    [\n        (datetime.date(2015, 12, 1), datetime.datetime(2015, 12, 1, 7, 4)),\n        (datetime.date(2015, 12, 2), datetime.datetime(2015, 12, 2, 7, 5)),\n        (datetime.date(2015, 12, 3), datetime.datetime(2015, 12, 3, 7, 6)),\n        (datetime.date(2015, 12, 12), datetime.datetime(2015, 12, 12, 7, 16)),\n        (datetime.date(2015, 12, 25), datetime.datetime(2015, 12, 25, 7, 25)),\n    ],\n)\ndef test_Dawn_Civil(day: datetime.date, dawn: datetime.datetime, london: LocationInfo):\n    dawn = dawn.replace(tzinfo=datetime.timezone.utc)\n    dawn_utc = sun.dawn(london.observer, day, Depression.CIVIL)\n    assert datetime_almost_equal(dawn, dawn_utc)\n\n\n@freezegun.freeze_time(\"2015-12-01\")\ndef test_Dawn_NoDate(london: LocationInfo):\n    ans = datetime.datetime(2015, 12, 1, 7, 4, tzinfo=datetime.timezone.utc)\n    assert datetime_almost_equal(sun.dawn(london.observer), ans)\n\n\n@pytest.mark.parametrize(\n    \"day,dawn\",\n    [\n        (datetime.date(2015, 12, 1), datetime.datetime(2015, 12, 1, 6, 22)),\n        (datetime.date(2015, 12, 2), datetime.datetime(2015, 12, 2, 6, 23)),\n        (datetime.date(2015, 12, 3), datetime.datetime(2015, 12, 3, 6, 24)),\n        (datetime.date(2015, 12, 12), datetime.datetime(2015, 12, 12, 6, 33)),\n        (datetime.date(2015, 12, 25), datetime.datetime(2015, 12, 25, 6, 41)),\n    ],\n)\ndef test_Dawn_Nautical(\n    day: datetime.date, dawn: datetime.datetime, london: LocationInfo\n):\n    dawn = dawn.replace(tzinfo=datetime.timezone.utc)\n    dawn_utc = sun.dawn(london.observer, day, 12)\n    assert datetime_almost_equal(dawn, dawn_utc)\n\n\n@pytest.mark.parametrize(\n    \"day,dawn\",\n    [\n        (datetime.date(2015, 12, 1), datetime.datetime(2015, 12, 1, 5, 41)),\n        (datetime.date(2015, 12, 2), datetime.datetime(2015, 12, 2, 5, 42)),\n        (datetime.date(2015, 12, 3), datetime.datetime(2015, 12, 3, 5, 44)),\n        (datetime.date(2015, 12, 12), datetime.datetime(2015, 12, 12, 5, 52)),\n        (datetime.date(2015, 12, 25), datetime.datetime(2015, 12, 25, 6, 1)),\n    ],\n)\ndef test_Dawn_Astronomical(\n    day: datetime.date, dawn: datetime.datetime, london: LocationInfo\n):\n    dawn = dawn.replace(tzinfo=datetime.timezone.utc)\n    dawn_utc = sun.dawn(london.observer, day, 18)\n    assert datetime_almost_equal(dawn, dawn_utc)\n\n\n@pytest.mark.parametrize(\n    \"day,sunrise\",\n    [\n        (datetime.date(2015, 1, 1), datetime.datetime(2015, 1, 1, 8, 6)),\n        (datetime.date(2015, 12, 1), datetime.datetime(2015, 12, 1, 7, 43)),\n        (datetime.date(2015, 12, 2), datetime.datetime(2015, 12, 2, 7, 45)),\n        (datetime.date(2015, 12, 3), datetime.datetime(2015, 12, 3, 7, 46)),\n        (datetime.date(2015, 12, 12), datetime.datetime(2015, 12, 12, 7, 56)),\n        (datetime.date(2015, 12, 25), datetime.datetime(2015, 12, 25, 8, 5)),\n    ],\n)\ndef test_Sunrise(day: datetime.date, sunrise: datetime.datetime, london: LocationInfo):\n    sunrise = sunrise.replace(tzinfo=datetime.timezone.utc)\n    sunrise_utc = sun.sunrise(london.observer, day)\n    assert datetime_almost_equal(sunrise, sunrise_utc)\n\n\n@freezegun.freeze_time(\"2015-12-01\")\ndef test_Sunrise_NoDate(london: LocationInfo):\n    ans = datetime.datetime(2015, 12, 1, 7, 43, tzinfo=datetime.timezone.utc)\n    sunrise = sun.sunrise(london.observer)\n    assert datetime_almost_equal(sunrise, ans)\n\n\n@pytest.mark.parametrize(\n    \"day,sunset\",\n    [\n        (datetime.date(2015, 1, 1), datetime.datetime(2015, 1, 1, 16, 1)),\n        (datetime.date(2015, 12, 1), datetime.datetime(2015, 12, 1, 15, 55)),\n        (datetime.date(2015, 12, 2), datetime.datetime(2015, 12, 2, 15, 54)),\n        (datetime.date(2015, 12, 3), datetime.datetime(2015, 12, 3, 15, 54)),\n        (datetime.date(2015, 12, 12), datetime.datetime(2015, 12, 12, 15, 51)),\n        (datetime.date(2015, 12, 25), datetime.datetime(2015, 12, 25, 15, 55)),\n    ],\n)\ndef test_Sunset(day: datetime.date, sunset: datetime.datetime, london: LocationInfo):\n    sunset = sunset.replace(tzinfo=datetime.timezone.utc)\n    sunset_utc = sun.sunset(london.observer, day)\n    assert datetime_almost_equal(sunset, sunset_utc)\n\n\n@freezegun.freeze_time(\"2015-12-01\")\ndef test_Sunset_NoDate(london: LocationInfo):\n    ans = datetime.datetime(2015, 12, 1, 15, 55, tzinfo=datetime.timezone.utc)\n    sunset = sun.sunset(london.observer)\n    assert datetime_almost_equal(sunset, ans)\n\n\n@pytest.mark.parametrize(\n    \"day,dusk\",\n    [\n        (datetime.date(2015, 12, 1), datetime.datetime(2015, 12, 1, 16, 34)),\n        (datetime.date(2015, 12, 2), datetime.datetime(2015, 12, 2, 16, 34)),\n        (datetime.date(2015, 12, 3), datetime.datetime(2015, 12, 3, 16, 33)),\n        (datetime.date(2015, 12, 12), datetime.datetime(2015, 12, 12, 16, 31)),\n        (datetime.date(2015, 12, 25), datetime.datetime(2015, 12, 25, 16, 36)),\n    ],\n)\ndef test_Dusk_Civil(day: datetime.date, dusk: datetime.datetime, london: LocationInfo):\n    dusk = dusk.replace(tzinfo=datetime.timezone.utc)\n    dusk_utc = sun.dusk(london.observer, day)\n    assert datetime_almost_equal(dusk, dusk_utc)\n\n\n@freezegun.freeze_time(\"2015-12-01\")\ndef test_Dusk_NoDate(london: LocationInfo):\n    ans = datetime.datetime(2015, 12, 1, 16, 34, tzinfo=datetime.timezone.utc)\n    dusk = sun.dusk(london.observer)\n    assert datetime_almost_equal(dusk, ans)\n\n\n@pytest.mark.parametrize(\n    \"day,dusk\",\n    [\n        (datetime.date(2015, 12, 1), datetime.datetime(2015, 12, 1, 17, 16)),\n        (datetime.date(2015, 12, 2), datetime.datetime(2015, 12, 2, 17, 16)),\n        (datetime.date(2015, 12, 3), datetime.datetime(2015, 12, 3, 17, 16)),\n        (datetime.date(2015, 12, 12), datetime.datetime(2015, 12, 12, 17, 14)),\n        (datetime.date(2015, 12, 25), datetime.datetime(2015, 12, 25, 17, 19)),\n    ],\n)\ndef test_Dusk_Nautical(\n    day: datetime.date, dusk: datetime.datetime, london: LocationInfo\n):\n    dusk = dusk.replace(tzinfo=datetime.timezone.utc)\n    dusk_utc = sun.dusk(london.observer, day, 12)\n    assert datetime_almost_equal(dusk, dusk_utc)\n\n\n@pytest.mark.parametrize(\n    \"day,noon\",\n    [\n        (datetime.date(2015, 12, 1), datetime.datetime(2015, 12, 1, 11, 49)),\n        (datetime.date(2015, 12, 2), datetime.datetime(2015, 12, 2, 11, 49)),\n        (datetime.date(2015, 12, 3), datetime.datetime(2015, 12, 3, 11, 50)),\n        (datetime.date(2015, 12, 12), datetime.datetime(2015, 12, 12, 11, 54)),\n        (datetime.date(2015, 12, 25), datetime.datetime(2015, 12, 25, 12, 00)),\n    ],\n)\ndef test_SolarNoon(day: datetime.date, noon: datetime.datetime, london: LocationInfo):\n    noon = noon.replace(tzinfo=datetime.timezone.utc)\n    noon_utc = sun.noon(london.observer, day)\n    assert datetime_almost_equal(noon, noon_utc)\n\n\n@freezegun.freeze_time(\"2015-12-01\")\ndef test_SolarNoon_NoDate(london: LocationInfo):\n    ans = datetime.datetime(2015, 12, 1, 11, 49, tzinfo=datetime.timezone.utc)\n    noon = sun.noon(london.observer)\n    assert datetime_almost_equal(noon, ans)\n\n\n@pytest.mark.parametrize(\n    \"day,midnight\",\n    [\n        (datetime.date(2016, 2, 18), datetime.datetime(2016, 2, 18, 0, 14)),\n        (datetime.date(2016, 10, 26), datetime.datetime(2016, 10, 25, 23, 44)),\n    ],\n)\ndef test_SolarMidnight(\n    day: datetime.date, midnight: datetime.datetime, london: LocationInfo\n):\n    solar_midnight = midnight.replace(tzinfo=datetime.timezone.utc)\n    solar_midnight_utc = sun.midnight(london.observer, day)\n    assert datetime_almost_equal(solar_midnight, solar_midnight_utc)\n\n\n@freezegun.freeze_time(\"2016-2-18\")\ndef test_SolarMidnight_NoDate(london: LocationInfo):\n    ans = datetime.datetime(2016, 2, 18, 0, 14, tzinfo=datetime.timezone.utc)\n    midnight = sun.midnight(london.observer)\n    assert datetime_almost_equal(midnight, ans)\n\n\n@pytest.mark.parametrize(\n    \"day,twilight\",\n    [\n        (\n            datetime.date(2019, 8, 29),\n            (\n                datetime.datetime(2019, 8, 29, 4, 32),\n                datetime.datetime(2019, 8, 29, 5, 8),\n            ),\n        ),\n    ],\n)\ndef test_Twilight_SunRising(\n    day: datetime.date,\n    twilight: Tuple[datetime.datetime, datetime.datetime],\n    london: LocationInfo,\n):\n    start, end = twilight\n    start = start.replace(tzinfo=datetime.timezone.utc)\n    end = end.replace(tzinfo=datetime.timezone.utc)\n\n    info = sun.twilight(london.observer, day)\n    start_utc = info[0]\n    end_utc = info[1]\n    assert datetime_almost_equal(start, start_utc)\n    assert datetime_almost_equal(end, end_utc)\n\n\n@pytest.mark.parametrize(\n    \"day,twilight\",\n    [\n        (\n            datetime.date(2019, 8, 29),\n            (\n                datetime.datetime(2019, 8, 29, 18, 54),\n                datetime.datetime(2019, 8, 29, 19, 30),\n            ),\n        )\n    ],\n)\ndef test_Twilight_SunSetting(\n    day: datetime.date, twilight: TimePeriod, london: LocationInfo\n):\n    start, end = twilight\n    start = start.replace(tzinfo=datetime.timezone.utc)\n    end = end.replace(tzinfo=datetime.timezone.utc)\n\n    info = sun.twilight(london.observer, day, direction=SunDirection.SETTING)\n    start_utc = info[0]\n    end_utc = info[1]\n    assert datetime_almost_equal(start, start_utc)\n    assert datetime_almost_equal(end, end_utc)\n\n\n@freezegun.freeze_time(\"2019-8-29\")\ndef test_Twilight_NoDate(london: LocationInfo):\n    start = datetime.datetime(2019, 8, 29, 18, 54, tzinfo=datetime.timezone.utc)\n    end = datetime.datetime(2019, 8, 29, 19, 30, tzinfo=datetime.timezone.utc)\n    ans = sun.twilight(london.observer, direction=SunDirection.SETTING)\n    assert datetime_almost_equal(ans[0], start)\n    assert datetime_almost_equal(ans[1], end)\n\n\n# Test data from http://www.astroloka.com/rahukaal.aspx?City=Delhi\n@pytest.mark.parametrize(\n    \"day,rahu\",\n    [\n        (\n            datetime.date(2015, 12, 1),\n            (\n                datetime.datetime(2015, 12, 1, 9, 17),\n                datetime.datetime(2015, 12, 1, 10, 35),\n            ),\n        ),\n        (\n            datetime.date(2015, 12, 2),\n            (\n                datetime.datetime(2015, 12, 2, 6, 40),\n                datetime.datetime(2015, 12, 2, 7, 58),\n            ),\n        ),\n    ],\n)\ndef test_Rahukaalam(day: datetime.date, rahu: TimePeriod, new_delhi: LocationInfo):\n    start, end = rahu\n    start = start.replace(tzinfo=datetime.timezone.utc)\n    end = end.replace(tzinfo=datetime.timezone.utc)\n\n    info = sun.rahukaalam(new_delhi.observer, day)\n    start_utc = info[0]\n    end_utc = info[1]\n    assert datetime_almost_equal(start, start_utc)\n    assert datetime_almost_equal(end, end_utc)\n\n\n@freezegun.freeze_time(\"2015-12-01\")\ndef test_Rahukaalam_NoDate(new_delhi: LocationInfo):\n    start = datetime.datetime(2015, 12, 1, 9, 17, tzinfo=datetime.timezone.utc)\n    end = datetime.datetime(2015, 12, 1, 10, 35, tzinfo=datetime.timezone.utc)\n    ans = sun.rahukaalam(new_delhi.observer)\n    assert datetime_almost_equal(ans[0], start)\n    assert datetime_almost_equal(ans[1], end)\n\n\n@pytest.mark.parametrize(\n    \"dt,angle\",\n    [\n        (datetime.datetime(2015, 12, 14, 11, 0, 0), 14.381311),\n        (datetime.datetime(2015, 12, 14, 20, 1, 0), -37.3710156),\n    ],\n)\ndef test_SolarAltitude(dt: datetime.datetime, angle: float, london: LocationInfo):\n    elevation = sun.elevation(london.observer, dt)\n    assert elevation == pytest.approx(angle, abs=0.5)  # type: ignore\n\n\n@freezegun.freeze_time(\"2015-12-14 11:00:00\", tz_offset=0)\ndef test_SolarAltitude_NoDate(london: LocationInfo):\n    elevation = sun.elevation(london.observer)\n    assert elevation == pytest.approx(14.381311, abs=0.5)  # type: ignore\n\n\n@pytest.mark.parametrize(\n    \"dt,angle\",\n    [\n        (datetime.datetime(2015, 12, 14, 11, 0, 0), 166.9676),\n        (datetime.datetime(2015, 12, 14, 20, 1, 0), 279.39927311745),\n    ],\n)\ndef test_SolarAzimuth(dt: datetime.datetime, angle: float, london: LocationInfo):\n    azimuth = sun.azimuth(london.observer, dt)\n    assert azimuth == pytest.approx(angle, abs=0.5)  # type: ignore\n\n\n@freezegun.freeze_time(\"2015-12-14 11:00:00\", tz_offset=0)\ndef test_SolarAzimuth_NoDate(london: LocationInfo):\n    assert sun.azimuth(london.observer) == pytest.approx(\n        166.9676, abs=0.5\n    )  # type: ignore\n\n\n@pytest.mark.parametrize(\n    \"dt,angle\",\n    [\n        (datetime.datetime(2021, 10, 10, 6, 0, 0), 102.6),\n        (datetime.datetime(2021, 10, 10, 7, 0, 0), 93.3),\n        (datetime.datetime(2021, 10, 10, 18, 0, 0), 87.8),\n        (datetime.datetime(2019, 8, 29, 14, 34, 0), 46),\n        (datetime.datetime(2020, 2, 3, 10, 37, 0), 71),\n    ],\n)\ndef test_SolarZenith_London(dt: datetime.datetime, angle: float, london: LocationInfo):\n    dt = dt.replace(tzinfo=london.tzinfo)  # type: ignore\n    zenith = sun.zenith(london.observer, dt)\n    assert zenith == pytest.approx(angle, abs=0.5)  # type: ignore\n\n\n@pytest.mark.parametrize(\n    \"dt,angle\",\n    [\n        (datetime.datetime(2022, 5, 1, 14, 0, 0), 32),\n        (datetime.datetime(2022, 5, 1, 21, 20, 0), 126),\n    ],\n)\ndef test_SolarZenith_Riyadh(dt: datetime.datetime, angle: float, riyadh: LocationInfo):\n    dt = dt.replace(tzinfo=riyadh.tzinfo)  # type: ignore\n    zenith = sun.zenith(riyadh.observer, dt)\n    assert zenith == pytest.approx(angle, abs=0.5)  # type: ignore\n\n\n@freezegun.freeze_time(\"2019-08-29 14:34:00\")\ndef test_SolarZenith_NoDate(london: LocationInfo):\n    zenith = sun.zenith(london.observer)\n    assert zenith == pytest.approx(52.41, abs=0.5)  # type: ignore\n\n\ndef test_TimeAtElevation_SunRising(london: LocationInfo):\n    d = datetime.date(2016, 1, 4)\n    dt = sun.time_at_elevation(london.observer, 6, d, SunDirection.RISING)\n    cdt = datetime.datetime(2016, 1, 4, 9, 5, 0, tzinfo=datetime.timezone.utc)\n    # Use error of 5 minutes as website has a rather coarse accuracy\n    assert datetime_almost_equal(dt, cdt, seconds=300)\n\n\n@freezegun.freeze_time(\"2016-1-4\")\ndef test_TimeAtElevation_NoDate(london: LocationInfo):\n    dt = sun.time_at_elevation(london.observer, 6, direction=SunDirection.RISING)\n    cdt = datetime.datetime(2016, 1, 4, 9, 5, 0, tzinfo=datetime.timezone.utc)\n    # Use error of 5 minutes as website has a rather coarse accuracy\n    assert datetime_almost_equal(dt, cdt, seconds=300)\n\n\ndef test_TimeAtElevation_SunSetting(london: LocationInfo):\n    d = datetime.date(2016, 1, 4)\n    dt = sun.time_at_elevation(london.observer, 14, d, SunDirection.SETTING)\n    cdt = datetime.datetime(2016, 1, 4, 13, 20, 0, tzinfo=datetime.timezone.utc)\n    assert datetime_almost_equal(dt, cdt, seconds=300)\n\n\ndef test_TimeAtElevation_GreaterThan90(london: LocationInfo):\n    d = datetime.date(2016, 1, 4)\n    dt = sun.time_at_elevation(london.observer, 166, d, SunDirection.RISING)\n    cdt = datetime.datetime(2016, 1, 4, 13, 20, 0, tzinfo=datetime.timezone.utc)\n    assert datetime_almost_equal(dt, cdt, seconds=300)\n\n\ndef test_TimeAtElevation_GreaterThan180(london: LocationInfo):\n    d = datetime.date(2015, 12, 1)\n    dt = sun.time_at_elevation(london.observer, 186, d, SunDirection.RISING)\n    cdt = datetime.datetime(2015, 12, 1, 16, 34, tzinfo=datetime.timezone.utc)\n    assert datetime_almost_equal(dt, cdt, seconds=300)\n\n\ndef test_TimeAtElevation_SunRisingBelowHorizon(london: LocationInfo):\n    d = datetime.date(2016, 1, 4)\n    dt = sun.time_at_elevation(london.observer, -18, d, SunDirection.RISING)\n    cdt = datetime.datetime(2016, 1, 4, 6, 0, 0, tzinfo=datetime.timezone.utc)\n    assert datetime_almost_equal(dt, cdt, seconds=300)\n\n\ndef test_TimeAtElevation_BadElevation(london: LocationInfo):\n    d = datetime.date(2016, 1, 4)\n    with pytest.raises(ValueError):\n        sun.time_at_elevation(london.observer, 20, d, SunDirection.RISING)\n\n\ndef test_Daylight(london: LocationInfo):\n    d = datetime.date(2016, 1, 6)\n    start, end = sun.daylight(london.observer, d)\n    cstart = datetime.datetime(2016, 1, 6, 8, 5, 0, tzinfo=datetime.timezone.utc)\n    cend = datetime.datetime(2016, 1, 6, 16, 7, 0, tzinfo=datetime.timezone.utc)\n    assert datetime_almost_equal(start, cstart, 120)\n    assert datetime_almost_equal(end, cend, 120)\n\n\n@freezegun.freeze_time(\"2016-1-06\")\ndef test_Daylight_NoDate(london: LocationInfo):\n    ans = sun.daylight(london.observer)\n\n    start = datetime.datetime(2016, 1, 6, 8, 5, 0, tzinfo=datetime.timezone.utc)\n    end = datetime.datetime(2016, 1, 6, 16, 7, 0, tzinfo=datetime.timezone.utc)\n    assert datetime_almost_equal(ans[0], start, 120)\n    assert datetime_almost_equal(ans[1], end, 120)\n\n\ndef test_Nighttime(london: LocationInfo):\n    d = datetime.date(2016, 1, 6)\n    start, end = sun.night(london.observer, d)\n    cstart = datetime.datetime(2016, 1, 6, 16, 46, tzinfo=datetime.timezone.utc)\n    cend = datetime.datetime(2016, 1, 7, 7, 25, tzinfo=datetime.timezone.utc)\n    assert datetime_almost_equal(start, cstart, 120)\n    assert datetime_almost_equal(end, cend, 120)\n\n\n@freezegun.freeze_time(\"2016-1-06\")\ndef test_Nighttime_NoDate(london: LocationInfo):\n    start = datetime.datetime(2016, 1, 6, 16, 46, tzinfo=datetime.timezone.utc)\n    end = datetime.datetime(2016, 1, 7, 7, 25, tzinfo=datetime.timezone.utc)\n    ans = sun.night(london.observer)\n    assert datetime_almost_equal(ans[0], start, seconds=300)\n    assert datetime_almost_equal(ans[1], end, seconds=300)\n"
  },
  {
    "path": "src/test/test_value_error_bug.py",
    "content": "import datetime\n\nimport astral\nimport astral.sun\n\n\ndef test_value_error_bug():\n    loc = astral.LocationInfo(\n        name=\"Barwani\",\n        region=\"India\",\n        timezone=\"Asia/Kolkata\",\n        latitude=23.518507,\n        longitude=74.952246,\n    )\n    ob = loc.observer\n    sun = astral.sun.sun(ob, date=datetime.date(2022, 7, 20))\n    sun[\"dawn\"]\n\n\nif __name__ == \"__main__\":\n    test_value_error_bug()\n"
  },
  {
    "path": "src/test/test_wellington.py",
    "content": "import datetime\r\n\r\nfrom almost_equal import datetime_almost_equal\r\n\r\nfrom astral.location import Location\r\nfrom astral.sun import sun\r\n\r\n\r\ndef test_Wellington(wellington: Location):\r\n    dt = datetime.date(2020, 2, 11)\r\n    s = sun(wellington.observer, dt, tzinfo=wellington.tzinfo)\r\n    assert datetime_almost_equal(\r\n        s[\"sunrise\"],\r\n        datetime.datetime(2020, 2, 11, 6, 38, 42, tzinfo=wellington.tzinfo),\r\n    )\r\n    assert datetime_almost_equal(\r\n        s[\"sunset\"],\r\n        datetime.datetime(2020, 2, 11, 20, 31, 00, tzinfo=wellington.tzinfo),\r\n    )\r\n"
  },
  {
    "path": "tox.ini",
    "content": "[tox]\nproject = astral\nenvlist = py3\nisolated_build = True\n\n[testenv]\ndeps =\n    freezegun\n    pytest\n    pytest-runner\ncommands =\n    pytest\n\n[testenv:doc]\nchangedir = src/docs\ndeps =\n    sphinx\n    sphinx_press_theme\ncommands =\n    sphinx-build -W -b html . ./html\n\n[flake8]\nmax-line-length=88\n"
  }
]