diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml deleted file mode 100644 index 1fbd27c..0000000 --- a/.github/workflows/tests.yaml +++ /dev/null @@ -1,73 +0,0 @@ ---- -name: Testing - -# yamllint disable-line rule:truthy -on: - push: - pull_request: - workflow_dispatch: - -env: - DEFAULT_PYTHON: "3.11" - -jobs: - pytest: - name: Python ${{ matrix.python }} - runs-on: ubuntu-latest - strategy: - matrix: - python: ["3.11", "3.12"] - steps: - - name: โคต๏ธ Check out code from GitHub - uses: actions/checkout@v4.1.6 - - name: ๐Ÿ— Set up Poetry - run: pipx install poetry - - name: ๐Ÿ— Set up Python ${{ matrix.python }} - id: python - uses: actions/setup-python@v5.1.0 - with: - python-version: ${{ matrix.python }} - cache: "poetry" - - name: ๐Ÿ— Install workflow dependencies - run: | - poetry config virtualenvs.create true - poetry config virtualenvs.in-project true - - name: ๐Ÿ— Install dependencies - run: poetry install --no-interaction - - name: ๐Ÿš€ Run pytest - run: poetry run pytest -v --cov-report xml:coverage.xml --cov src tests - - name: โฌ†๏ธ Upload coverage artifact - uses: actions/upload-artifact@v4.3.3 - with: - name: coverage-${{ matrix.python }} - path: coverage.xml - - coverage: - runs-on: ubuntu-latest - needs: pytest - steps: - - name: โคต๏ธ Check out code from GitHub - uses: actions/checkout@v4.1.6 - with: - fetch-depth: 0 - - name: โฌ‡๏ธ Download coverage data - uses: actions/download-artifact@v4.1.7 - - name: ๐Ÿ— Set up Poetry - run: pipx install poetry - - name: ๐Ÿ— Set up Python ${{ env.DEFAULT_PYTHON }} - id: python - uses: actions/setup-python@v5.1.0 - with: - python-version: ${{ env.DEFAULT_PYTHON }} - cache: 'poetry' - - name: ๐Ÿ— Install workflow dependencies - run: | - poetry config virtualenvs.create true - poetry config virtualenvs.in-project true - - name: ๐Ÿ— Install dependencies - run: poetry install --no-interaction - - name: ๐Ÿš€ Upload coverage report - uses: codecov/codecov-action@v4.4.0 - with: - token: ${{ secrets.CODECOV_TOKEN }} - fail_ci_if_error: true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 80e21a2..d57bc7f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,5 +25,5 @@ Even better: You could submit a pull request with a fix / new feature! developers, or if you do not have permission to do that, you may request the second reviewer to merge it for you. -[github]: https://github.com/klaasnicolaas/forecast_solar/issues -[prs]: https://github.com/klaasnicolaas/forecast_solar/pulls \ No newline at end of file +[github]: https://github.com/rany2/py-solar-forecast/issues +[prs]: https://github.com/rany2/py-solar-forecast/pulls \ No newline at end of file diff --git a/LICENSE b/LICENSE index 20a7080..57af04c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ # MIT License Copyright (c) 2021-2024 Klaas Schoute +Copyright (c) 2024 Rany Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 5a79452..4b6b550 100644 --- a/README.md +++ b/README.md @@ -4,26 +4,16 @@ --> ## Python API fetching Solarpanels forecast information. - -![Project Maintenance][maintenance-shield] -[![License][license-shield]](LICENSE) - -[![GitHub Activity][commits-shield]][commits] -[![GitHub Last Commit][last-commit-shield]][commits] -[![Contributors][contributors-shield]][contributors-url] - -[![Forks][forks-shield]][forks-url] -[![Stargazers][stars-shield]][stars-url] -[![Issues][issues-shield]][issues-url] ## About -With this python library you can request data from [forecast.solar](https://forecast.solar) and see what your solar panels may produce in the coming days. +With this python library you can request data from (Open-Meteo)[https://open-meteo.com/] +and see what your solar panels may produce in the coming days. ## Installation ```bash -pip install forecast-solar +pip install open-meteo-solar-forecast ``` ## Data @@ -64,22 +54,17 @@ This library returns a lot of different data, based on the API: ```python import asyncio -from forecast_solar import ForecastSolar +from open_meteo_solar_forecast import OpenMeteoSolarForecast async def main() -> None: """Show example on how to use the library.""" - async with ForecastSolar( - api_key="YOUR_API_KEY", + async with OpenMeteoSolarForecast( latitude=52.16, longitude=4.47, declination=20, azimuth=10, kwp=2.160, - damping=0, - damping_morning=0.5, - damping_evening=0.5, - horizon="0,0,0,10,10,20,20,30,30", ) as forecast: estimate = await forecast.estimate() print(estimate) @@ -91,15 +76,11 @@ if __name__ == "__main__": | Parameter | value type | Description | | --------- | ---------- | ----------- | -| `api_key` | `str` | Your API key from [forecast.solar](https://forecast.solar) (optional) | +| `base_url` | `str` | The base URL of the API (optional) | +| `api_key` | `str` | Your API key (optional) | | `declination` | `int` | The tilt of the solar panels (required) | | `azimuth` | `int` | The direction the solar panels are facing (required) | | `kwp` | `float` | The size of the solar panels in kWp (required) | -| `damping` | `float` | The damping of the solar panels, [read this][forecast-damping] for more information (optional) | -| `damping_morning` | `float` | The damping of the solar panels in the morning (optional) | -| `damping_evening` | `float` | The damping of the solar panels in the evening (optional) | -| `inverter` | `float` | The maximum power of your inverter in kilo watts (optional) | -| `horizon` | `str` | A list of **comma separated** degrees values, [read this][forecast-horizon] for more information (optional) | ## Contributing @@ -131,17 +112,12 @@ poetry shell exit ``` -To run just the Python tests: - -```bash -poetry run pytest -``` - ## License MIT License Copyright (c) 2021-2024 Klaas Schoute +Copyright (c) 2024 Rany Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -161,24 +137,5 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -[forecast-horizon]: https://doc.forecast.solar/doku.php?id=api#horizon -[forecast-damping]: https://doc.forecast.solar/doku.php?id=damping - - -[maintenance-shield]: https://img.shields.io/maintenance/yes/2024.svg?style=for-the-badge -[contributors-shield]: https://img.shields.io/github/contributors/home-assistant-libs/forecast_solar.svg?style=for-the-badge -[contributors-url]: https://github.com/home-assistant-libs/forecast_solar/graphs/contributors -[forks-shield]: https://img.shields.io/github/forks/home-assistant-libs/forecast_solar.svg?style=for-the-badge -[forks-url]: https://github.com/home-assistant-libs/forecast_solar/network/members -[stars-shield]: https://img.shields.io/github/stars/home-assistant-libs/forecast_solar.svg?style=for-the-badge -[stars-url]: https://github.com/home-assistant-libs/forecast_solar/stargazers -[issues-shield]: https://img.shields.io/github/issues/home-assistant-libs/forecast_solar.svg?style=for-the-badge -[issues-url]: https://github.com/home-assistant-libs/forecast_solar/issues -[license-shield]: https://img.shields.io/github/license/home-assistant-libs/forecast_solar.svg?style=for-the-badge -[commits-shield]: https://img.shields.io/github/commit-activity/y/home-assistant-libs/forecast_solar.svg?style=for-the-badge -[commits]: https://github.com/home-assistant-libs/forecast_solar/commits/master -[last-commit-shield]: https://img.shields.io/github/last-commit/home-assistant-libs/forecast_solar.svg?style=for-the-badge - [poetry-install]: https://python-poetry.org/docs/#installation [poetry]: https://python-poetry.org \ No newline at end of file diff --git a/codecov.yml b/codecov.yml deleted file mode 100644 index a8b6b1f..0000000 --- a/codecov.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -comment: false -coverage: - status: - project: - default: - target: 90 - threshold: 10% diff --git a/examples/estimate.py b/examples/estimate.py index 72a3243..8bba2b3 100644 --- a/examples/estimate.py +++ b/examples/estimate.py @@ -1,33 +1,24 @@ """Example of how to get an estimate from the Forecast.Solar API.""" import asyncio -from datetime import UTC, datetime, timedelta -from pprint import pprint # noqa: F401 +import dataclasses # noqa +from datetime import timedelta +from pprint import pprint # noqa -from forecast_solar import ForecastSolar, ForecastSolarRatelimitError +from open_meteo_solar_forecast import OpenMeteoSolarForecast async def main() -> None: """Get an estimate from the Forecast.Solar API.""" - async with ForecastSolar( + async with OpenMeteoSolarForecast( latitude=52.16, longitude=4.47, declination=20, azimuth=10, kwp=2.160, - damping=0, - horizon="0,0,0,10,10,20,20,30,30", + efficiency_factor=0.9, ) as forecast: - try: - estimate = await forecast.estimate() - except ForecastSolarRatelimitError as err: - print("Ratelimit reached") - print(f"Rate limit resets at {err.reset_at}") - reset_period = err.reset_at - datetime.now(UTC) - # Strip microseconds as they are not informative - reset_period -= timedelta(microseconds=reset_period.microseconds) - print(f"That's in {reset_period}") - return + estimate = await forecast.estimate() # Uncomment this if you want to see what's in the estimate arrays # pprint(dataclasses.asdict(estimate)) @@ -70,8 +61,6 @@ async def main() -> None: print(f"energy_production next 12 hours: {estimate.sum_energy_production(12)}") print(f"energy_production next 24 hours: {estimate.sum_energy_production(24)}") print(f"timezone: {estimate.timezone}") - print(f"account_type: {estimate.account_type}") - print(forecast.ratelimit) if __name__ == "__main__": diff --git a/poetry.lock b/poetry.lock index ea4138f..410e8d1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,18 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. - -[[package]] -name = "aiodns" -version = "3.2.0" -description = "Simple DNS resolver for asyncio" -optional = false -python-versions = "*" -files = [ - {file = "aiodns-3.2.0-py3-none-any.whl", hash = "sha256:e443c0c27b07da3174a109fd9e736d69058d808f144d3c9d56dbd1776964c5f5"}, - {file = "aiodns-3.2.0.tar.gz", hash = "sha256:62869b23409349c21b072883ec8998316b234c9a9e36675756e8e317e8768f72"}, -] - -[package.dependencies] -pycares = ">=4.0.0" +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "aiohttp" @@ -160,70 +146,6 @@ tests = ["attrs[tests-no-zope]", "zope-interface"] tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] -[[package]] -name = "cffi" -version = "1.16.0" -description = "Foreign Function Interface for Python calling C code." -optional = false -python-versions = ">=3.8" -files = [ - {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, - {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, - {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, - {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, - {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, - {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, - {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, - {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, - {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, - {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, - {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, - {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, - {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, - {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, - {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, -] - -[package.dependencies] -pycparser = "*" - [[package]] name = "colorama" version = "0.4.6" @@ -251,82 +173,68 @@ coverage = ">=6.0.2" [[package]] name = "coverage" -version = "7.5.0" +version = "7.5.1" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:432949a32c3e3f820af808db1833d6d1631664d53dd3ce487aa25d574e18ad1c"}, - {file = "coverage-7.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2bd7065249703cbeb6d4ce679c734bef0ee69baa7bff9724361ada04a15b7e3b"}, - {file = "coverage-7.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbfe6389c5522b99768a93d89aca52ef92310a96b99782973b9d11e80511f932"}, - {file = "coverage-7.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39793731182c4be939b4be0cdecde074b833f6171313cf53481f869937129ed3"}, - {file = "coverage-7.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85a5dbe1ba1bf38d6c63b6d2c42132d45cbee6d9f0c51b52c59aa4afba057517"}, - {file = "coverage-7.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:357754dcdfd811462a725e7501a9b4556388e8ecf66e79df6f4b988fa3d0b39a"}, - {file = "coverage-7.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a81eb64feded34f40c8986869a2f764f0fe2db58c0530d3a4afbcde50f314880"}, - {file = "coverage-7.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:51431d0abbed3a868e967f8257c5faf283d41ec882f58413cf295a389bb22e58"}, - {file = "coverage-7.5.0-cp310-cp310-win32.whl", hash = "sha256:f609ebcb0242d84b7adeee2b06c11a2ddaec5464d21888b2c8255f5fd6a98ae4"}, - {file = "coverage-7.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:6782cd6216fab5a83216cc39f13ebe30adfac2fa72688c5a4d8d180cd52e8f6a"}, - {file = "coverage-7.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e768d870801f68c74c2b669fc909839660180c366501d4cc4b87efd6b0eee375"}, - {file = "coverage-7.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:84921b10aeb2dd453247fd10de22907984eaf80901b578a5cf0bb1e279a587cb"}, - {file = "coverage-7.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:710c62b6e35a9a766b99b15cdc56d5aeda0914edae8bb467e9c355f75d14ee95"}, - {file = "coverage-7.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c379cdd3efc0658e652a14112d51a7668f6bfca7445c5a10dee7eabecabba19d"}, - {file = "coverage-7.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fea9d3ca80bcf17edb2c08a4704259dadac196fe5e9274067e7a20511fad1743"}, - {file = "coverage-7.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:41327143c5b1d715f5f98a397608f90ab9ebba606ae4e6f3389c2145410c52b1"}, - {file = "coverage-7.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:565b2e82d0968c977e0b0f7cbf25fd06d78d4856289abc79694c8edcce6eb2de"}, - {file = "coverage-7.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cf3539007202ebfe03923128fedfdd245db5860a36810136ad95a564a2fdffff"}, - {file = "coverage-7.5.0-cp311-cp311-win32.whl", hash = "sha256:bf0b4b8d9caa8d64df838e0f8dcf68fb570c5733b726d1494b87f3da85db3a2d"}, - {file = "coverage-7.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c6384cc90e37cfb60435bbbe0488444e54b98700f727f16f64d8bfda0b84656"}, - {file = "coverage-7.5.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fed7a72d54bd52f4aeb6c6e951f363903bd7d70bc1cad64dd1f087980d309ab9"}, - {file = "coverage-7.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cbe6581fcff7c8e262eb574244f81f5faaea539e712a058e6707a9d272fe5b64"}, - {file = "coverage-7.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad97ec0da94b378e593ef532b980c15e377df9b9608c7c6da3506953182398af"}, - {file = "coverage-7.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd4bacd62aa2f1a1627352fe68885d6ee694bdaebb16038b6e680f2924a9b2cc"}, - {file = "coverage-7.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:adf032b6c105881f9d77fa17d9eebe0ad1f9bfb2ad25777811f97c5362aa07f2"}, - {file = "coverage-7.5.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ba01d9ba112b55bfa4b24808ec431197bb34f09f66f7cb4fd0258ff9d3711b1"}, - {file = "coverage-7.5.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f0bfe42523893c188e9616d853c47685e1c575fe25f737adf473d0405dcfa7eb"}, - {file = "coverage-7.5.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a9a7ef30a1b02547c1b23fa9a5564f03c9982fc71eb2ecb7f98c96d7a0db5cf2"}, - {file = "coverage-7.5.0-cp312-cp312-win32.whl", hash = "sha256:3c2b77f295edb9fcdb6a250f83e6481c679335ca7e6e4a955e4290350f2d22a4"}, - {file = "coverage-7.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:427e1e627b0963ac02d7c8730ca6d935df10280d230508c0ba059505e9233475"}, - {file = "coverage-7.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9dd88fce54abbdbf4c42fb1fea0e498973d07816f24c0e27a1ecaf91883ce69e"}, - {file = "coverage-7.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a898c11dca8f8c97b467138004a30133974aacd572818c383596f8d5b2eb04a9"}, - {file = "coverage-7.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07dfdd492d645eea1bd70fb1d6febdcf47db178b0d99161d8e4eed18e7f62fe7"}, - {file = "coverage-7.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3d117890b6eee85887b1eed41eefe2e598ad6e40523d9f94c4c4b213258e4a4"}, - {file = "coverage-7.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6afd2e84e7da40fe23ca588379f815fb6dbbb1b757c883935ed11647205111cb"}, - {file = "coverage-7.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a9960dd1891b2ddf13a7fe45339cd59ecee3abb6b8326d8b932d0c5da208104f"}, - {file = "coverage-7.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ced268e82af993d7801a9db2dbc1d2322e786c5dc76295d8e89473d46c6b84d4"}, - {file = "coverage-7.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e7c211f25777746d468d76f11719e64acb40eed410d81c26cefac641975beb88"}, - {file = "coverage-7.5.0-cp38-cp38-win32.whl", hash = "sha256:262fffc1f6c1a26125d5d573e1ec379285a3723363f3bd9c83923c9593a2ac25"}, - {file = "coverage-7.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:eed462b4541c540d63ab57b3fc69e7d8c84d5957668854ee4e408b50e92ce26a"}, - {file = "coverage-7.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d0194d654e360b3e6cc9b774e83235bae6b9b2cac3be09040880bb0e8a88f4a1"}, - {file = "coverage-7.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:33c020d3322662e74bc507fb11488773a96894aa82a622c35a5a28673c0c26f5"}, - {file = "coverage-7.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbdf2cae14a06827bec50bd58e49249452d211d9caddd8bd80e35b53cb04631"}, - {file = "coverage-7.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3235d7c781232e525b0761730e052388a01548bd7f67d0067a253887c6e8df46"}, - {file = "coverage-7.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2de4e546f0ec4b2787d625e0b16b78e99c3e21bc1722b4977c0dddf11ca84e"}, - {file = "coverage-7.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4d0e206259b73af35c4ec1319fd04003776e11e859936658cb6ceffdeba0f5be"}, - {file = "coverage-7.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2055c4fb9a6ff624253d432aa471a37202cd8f458c033d6d989be4499aed037b"}, - {file = "coverage-7.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:075299460948cd12722a970c7eae43d25d37989da682997687b34ae6b87c0ef0"}, - {file = "coverage-7.5.0-cp39-cp39-win32.whl", hash = "sha256:280132aada3bc2f0fac939a5771db4fbb84f245cb35b94fae4994d4c1f80dae7"}, - {file = "coverage-7.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:c58536f6892559e030e6924896a44098bc1290663ea12532c78cef71d0df8493"}, - {file = "coverage-7.5.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:2b57780b51084d5223eee7b59f0d4911c31c16ee5aa12737c7a02455829ff067"}, - {file = "coverage-7.5.0.tar.gz", hash = "sha256:cf62d17310f34084c59c01e027259076479128d11e4661bb6c9acb38c5e19bb8"}, + {file = "coverage-7.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0884920835a033b78d1c73b6d3bbcda8161a900f38a488829a83982925f6c2e"}, + {file = "coverage-7.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:39afcd3d4339329c5f58de48a52f6e4e50f6578dd6099961cf22228feb25f38f"}, + {file = "coverage-7.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7b0ceee8147444347da6a66be737c9d78f3353b0681715b668b72e79203e4a"}, + {file = "coverage-7.5.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a9ca3f2fae0088c3c71d743d85404cec8df9be818a005ea065495bedc33da35"}, + {file = "coverage-7.5.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd215c0c7d7aab005221608a3c2b46f58c0285a819565887ee0b718c052aa4e"}, + {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4bf0655ab60d754491004a5efd7f9cccefcc1081a74c9ef2da4735d6ee4a6223"}, + {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:61c4bf1ba021817de12b813338c9be9f0ad5b1e781b9b340a6d29fc13e7c1b5e"}, + {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:db66fc317a046556a96b453a58eced5024af4582a8dbdc0c23ca4dbc0d5b3146"}, + {file = "coverage-7.5.1-cp310-cp310-win32.whl", hash = "sha256:b016ea6b959d3b9556cb401c55a37547135a587db0115635a443b2ce8f1c7228"}, + {file = "coverage-7.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:df4e745a81c110e7446b1cc8131bf986157770fa405fe90e15e850aaf7619bc8"}, + {file = "coverage-7.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:796a79f63eca8814ca3317a1ea443645c9ff0d18b188de470ed7ccd45ae79428"}, + {file = "coverage-7.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fc84a37bfd98db31beae3c2748811a3fa72bf2007ff7902f68746d9757f3746"}, + {file = "coverage-7.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6175d1a0559986c6ee3f7fccfc4a90ecd12ba0a383dcc2da30c2b9918d67d8a3"}, + {file = "coverage-7.5.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fc81d5878cd6274ce971e0a3a18a8803c3fe25457165314271cf78e3aae3aa2"}, + {file = "coverage-7.5.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:556cf1a7cbc8028cb60e1ff0be806be2eded2daf8129b8811c63e2b9a6c43bca"}, + {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9981706d300c18d8b220995ad22627647be11a4276721c10911e0e9fa44c83e8"}, + {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d7fed867ee50edf1a0b4a11e8e5d0895150e572af1cd6d315d557758bfa9c057"}, + {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef48e2707fb320c8f139424a596f5b69955a85b178f15af261bab871873bb987"}, + {file = "coverage-7.5.1-cp311-cp311-win32.whl", hash = "sha256:9314d5678dcc665330df5b69c1e726a0e49b27df0461c08ca12674bcc19ef136"}, + {file = "coverage-7.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:5fa567e99765fe98f4e7d7394ce623e794d7cabb170f2ca2ac5a4174437e90dd"}, + {file = "coverage-7.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b6cf3764c030e5338e7f61f95bd21147963cf6aa16e09d2f74f1fa52013c1206"}, + {file = "coverage-7.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ec92012fefebee89a6b9c79bc39051a6cb3891d562b9270ab10ecfdadbc0c34"}, + {file = "coverage-7.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16db7f26000a07efcf6aea00316f6ac57e7d9a96501e990a36f40c965ec7a95d"}, + {file = "coverage-7.5.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:beccf7b8a10b09c4ae543582c1319c6df47d78fd732f854ac68d518ee1fb97fa"}, + {file = "coverage-7.5.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8748731ad392d736cc9ccac03c9845b13bb07d020a33423fa5b3a36521ac6e4e"}, + {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7352b9161b33fd0b643ccd1f21f3a3908daaddf414f1c6cb9d3a2fd618bf2572"}, + {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:7a588d39e0925f6a2bff87154752481273cdb1736270642aeb3635cb9b4cad07"}, + {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:68f962d9b72ce69ea8621f57551b2fa9c70509af757ee3b8105d4f51b92b41a7"}, + {file = "coverage-7.5.1-cp312-cp312-win32.whl", hash = "sha256:f152cbf5b88aaeb836127d920dd0f5e7edff5a66f10c079157306c4343d86c19"}, + {file = "coverage-7.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:5a5740d1fb60ddf268a3811bcd353de34eb56dc24e8f52a7f05ee513b2d4f596"}, + {file = "coverage-7.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e2213def81a50519d7cc56ed643c9e93e0247f5bbe0d1247d15fa520814a7cd7"}, + {file = "coverage-7.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5037f8fcc2a95b1f0e80585bd9d1ec31068a9bcb157d9750a172836e98bc7a90"}, + {file = "coverage-7.5.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3721c2c9e4c4953a41a26c14f4cef64330392a6d2d675c8b1db3b645e31f0e"}, + {file = "coverage-7.5.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca498687ca46a62ae590253fba634a1fe9836bc56f626852fb2720f334c9e4e5"}, + {file = "coverage-7.5.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cdcbc320b14c3e5877ee79e649677cb7d89ef588852e9583e6b24c2e5072661"}, + {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:57e0204b5b745594e5bc14b9b50006da722827f0b8c776949f1135677e88d0b8"}, + {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fe7502616b67b234482c3ce276ff26f39ffe88adca2acf0261df4b8454668b4"}, + {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9e78295f4144f9dacfed4f92935fbe1780021247c2fabf73a819b17f0ccfff8d"}, + {file = "coverage-7.5.1-cp38-cp38-win32.whl", hash = "sha256:1434e088b41594baa71188a17533083eabf5609e8e72f16ce8c186001e6b8c41"}, + {file = "coverage-7.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:0646599e9b139988b63704d704af8e8df7fa4cbc4a1f33df69d97f36cb0a38de"}, + {file = "coverage-7.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4cc37def103a2725bc672f84bd939a6fe4522310503207aae4d56351644682f1"}, + {file = "coverage-7.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc0b4d8bfeabd25ea75e94632f5b6e047eef8adaed0c2161ada1e922e7f7cece"}, + {file = "coverage-7.5.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d0a0f5e06881ecedfe6f3dd2f56dcb057b6dbeb3327fd32d4b12854df36bf26"}, + {file = "coverage-7.5.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9735317685ba6ec7e3754798c8871c2f49aa5e687cc794a0b1d284b2389d1bd5"}, + {file = "coverage-7.5.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d21918e9ef11edf36764b93101e2ae8cc82aa5efdc7c5a4e9c6c35a48496d601"}, + {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c3e757949f268364b96ca894b4c342b41dc6f8f8b66c37878aacef5930db61be"}, + {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:79afb6197e2f7f60c4824dd4b2d4c2ec5801ceb6ba9ce5d2c3080e5660d51a4f"}, + {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d1d0d98d95dd18fe29dc66808e1accf59f037d5716f86a501fc0256455219668"}, + {file = "coverage-7.5.1-cp39-cp39-win32.whl", hash = "sha256:1cc0fe9b0b3a8364093c53b0b4c0c2dd4bb23acbec4c9240b5f284095ccf7981"}, + {file = "coverage-7.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:dde0070c40ea8bb3641e811c1cfbf18e265d024deff6de52c5950677a8fb1e0f"}, + {file = "coverage-7.5.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:6537e7c10cc47c595828b8a8be04c72144725c383c4702703ff4e42e44577312"}, + {file = "coverage-7.5.1.tar.gz", hash = "sha256:54de9ef3a9da981f7af93eafde4ede199e0846cd819eb27c88e2b712aae9708c"}, ] [package.extras] toml = ["tomli"] -[[package]] -name = "freezegun" -version = "1.5.0" -description = "Let your Python tests travel through time" -optional = false -python-versions = ">=3.7" -files = [ - {file = "freezegun-1.5.0-py3-none-any.whl", hash = "sha256:ec3f4ba030e34eb6cf7e1e257308aee2c60c3d038ff35996d7475760c9ff3719"}, - {file = "freezegun-1.5.0.tar.gz", hash = "sha256:200a64359b363aa3653d8aac289584078386c7c3da77339d257e46a01fb5c77c"}, -] - -[package.dependencies] -python-dateutil = ">=2.7" - [[package]] name = "frozenlist" version = "1.4.1" @@ -628,112 +536,35 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] -[[package]] -name = "pycares" -version = "4.4.0" -description = "Python interface for c-ares" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pycares-4.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:24da119850841d16996713d9c3374ca28a21deee056d609fbbed29065d17e1f6"}, - {file = "pycares-4.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8f64cb58729689d4d0e78f0bfb4c25ce2f851d0274c0273ac751795c04b8798a"}, - {file = "pycares-4.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d33e2a1120887e89075f7f814ec144f66a6ce06a54f5722ccefc62fbeda83cff"}, - {file = "pycares-4.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c680fef1b502ee680f8f0b95a41af4ec2c234e50e16c0af5bbda31999d3584bd"}, - {file = "pycares-4.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fff16b09042ba077f7b8aa5868d1d22456f0002574d0ba43462b10a009331677"}, - {file = "pycares-4.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:229a1675eb33bc9afb1fc463e73ee334950ccc485bc83a43f6ae5839fb4d5fa3"}, - {file = "pycares-4.4.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3aebc73e5ad70464f998f77f2da2063aa617cbd8d3e8174dd7c5b4518f967153"}, - {file = "pycares-4.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6ef64649eba56448f65e26546d85c860709844d2fc22ef14d324fe0b27f761a9"}, - {file = "pycares-4.4.0-cp310-cp310-win32.whl", hash = "sha256:4afc2644423f4eef97857a9fd61be9758ce5e336b4b0bd3d591238bb4b8b03e0"}, - {file = "pycares-4.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:5ed4e04af4012f875b78219d34434a6d08a67175150ac1b79eb70ab585d4ba8c"}, - {file = "pycares-4.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bce8db2fc6f3174bd39b81405210b9b88d7b607d33e56a970c34a0c190da0490"}, - {file = "pycares-4.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9a0303428d013ccf5c51de59c83f9127aba6200adb7fd4be57eddb432a1edd2a"}, - {file = "pycares-4.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afb91792f1556f97be7f7acb57dc7756d89c5a87bd8b90363a77dbf9ea653817"}, - {file = "pycares-4.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b61579cecf1f4d616e5ea31a6e423a16680ab0d3a24a2ffe7bb1d4ee162477ff"}, - {file = "pycares-4.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7af06968cbf6851566e806bf3e72825b0e6671832a2cbe840be1d2d65350710"}, - {file = "pycares-4.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ceb12974367b0a68a05d52f4162b29f575d241bd53de155efe632bf2c943c7f6"}, - {file = "pycares-4.4.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:2eeec144bcf6a7b6f2d74d6e70cbba7886a84dd373c886f06cb137a07de4954c"}, - {file = "pycares-4.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e3a6f7cfdfd11eb5493d6d632e582408c8f3b429f295f8799c584c108b28db6f"}, - {file = "pycares-4.4.0-cp311-cp311-win32.whl", hash = "sha256:34736a2ffaa9c08ca9c707011a2d7b69074bbf82d645d8138bba771479b2362f"}, - {file = "pycares-4.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:eb66c30eb11e877976b7ead13632082a8621df648c408b8e15cdb91a452dd502"}, - {file = "pycares-4.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:fd644505a8cfd7f6584d33a9066d4e3d47700f050ef1490230c962de5dfb28c6"}, - {file = "pycares-4.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52084961262232ec04bd75f5043aed7e5d8d9695e542ff691dfef0110209f2d4"}, - {file = "pycares-4.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0c5368206057884cde18602580083aeaad9b860e2eac14fd253543158ce1e93"}, - {file = "pycares-4.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:112a4979c695b1c86f6782163d7dec58d57a3b9510536dcf4826550f9053dd9a"}, - {file = "pycares-4.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d186dafccdaa3409194c0f94db93c1a5d191145a275f19da6591f9499b8e7b8"}, - {file = "pycares-4.4.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:64965dc19c578a683ea73487a215a8897276224e004d50eeb21f0bc7a0b63c88"}, - {file = "pycares-4.4.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ed2a38e34bec6f2586435f6ff0bc5fe11d14bebd7ed492cf739a424e81681540"}, - {file = "pycares-4.4.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:94d6962db81541eb0396d2f0dfcbb18cdb8c8b251d165efc2d974ae652c547d4"}, - {file = "pycares-4.4.0-cp312-cp312-win32.whl", hash = "sha256:1168a48a834813aa80f412be2df4abaf630528a58d15c704857448b20b1675c0"}, - {file = "pycares-4.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:db24c4e7fea4a052c6e869cbf387dd85d53b9736cfe1ef5d8d568d1ca925e977"}, - {file = "pycares-4.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:21a5a0468861ec7df7befa69050f952da13db5427ae41ffe4713bc96291d1d95"}, - {file = "pycares-4.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:22c00bf659a9fa44d7b405cf1cd69b68b9d37537899898d8cbe5dffa4016b273"}, - {file = "pycares-4.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23aa3993a352491a47fcf17867f61472f32f874df4adcbb486294bd9fbe8abee"}, - {file = "pycares-4.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:813d661cbe2e37d87da2d16b7110a6860e93ddb11735c6919c8a3545c7b9c8d8"}, - {file = "pycares-4.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:77cf5a2fd5583c670de41a7f4a7b46e5cbabe7180d8029f728571f4d2e864084"}, - {file = "pycares-4.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3eaa6681c0a3e3f3868c77aca14b7760fed35fdfda2fe587e15c701950e7bc69"}, - {file = "pycares-4.4.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ad58e284a658a8a6a84af2e0b62f2f961f303cedfe551854d7bd40c3cbb61912"}, - {file = "pycares-4.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bfb89ca9e3d0a9b5332deeb666b2ede9d3469107742158f4aeda5ce032d003f4"}, - {file = "pycares-4.4.0-cp38-cp38-win32.whl", hash = "sha256:f36bdc1562142e3695555d2f4ac0cb69af165eddcefa98efc1c79495b533481f"}, - {file = "pycares-4.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:902461a92b6a80fd5041a2ec5235680c7cc35e43615639ec2a40e63fca2dfb51"}, - {file = "pycares-4.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7bddc6adba8f699728f7fc1c9ce8cef359817ad78e2ed52b9502cb5f8dc7f741"}, - {file = "pycares-4.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cb49d5805cd347c404f928c5ae7c35e86ba0c58ffa701dbe905365e77ce7d641"}, - {file = "pycares-4.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56cf3349fa3a2e67ed387a7974c11d233734636fe19facfcda261b411af14d80"}, - {file = "pycares-4.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bf2eaa83a5987e48fa63302f0fe7ce3275cfda87b34d40fef9ce703fb3ac002"}, - {file = "pycares-4.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82bba2ab77eb5addbf9758d514d9bdef3c1bfe7d1649a47bd9a0d55a23ef478b"}, - {file = "pycares-4.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c6a8bde63106f162fca736e842a916853cad3c8d9d137e11c9ffa37efa818b02"}, - {file = "pycares-4.4.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f5f646eec041db6ffdbcaf3e0756fb92018f7af3266138c756bb09d2b5baadec"}, - {file = "pycares-4.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9dc04c54c6ea615210c1b9e803d0e2d2255f87a3d5d119b6482c8f0dfa15b26b"}, - {file = "pycares-4.4.0-cp39-cp39-win32.whl", hash = "sha256:97892cced5794d721fb4ff8765764aa4ea48fe8b2c3820677505b96b83d4ef47"}, - {file = "pycares-4.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:917f08f0b5d9324e9a34211e68d27447c552b50ab967044776bbab7e42a553a2"}, - {file = "pycares-4.4.0.tar.gz", hash = "sha256:f47579d508f2f56eddd16ce72045782ad3b1b3b678098699e2b6a1b30733e1c2"}, -] - -[package.dependencies] -cffi = ">=1.5.0" - -[package.extras] -idna = ["idna (>=2.1)"] - -[[package]] -name = "pycparser" -version = "2.22" -description = "C parser in Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, - {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, -] - [[package]] name = "pytest" -version = "8.1.1" +version = "8.2.1" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"}, - {file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"}, + {file = "pytest-8.2.1-py3-none-any.whl", hash = "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1"}, + {file = "pytest-8.2.1.tar.gz", hash = "sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd"}, ] [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} iniconfig = "*" packaging = "*" -pluggy = ">=1.4,<2.0" +pluggy = ">=1.5,<2.0" [package.extras] -testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-asyncio" -version = "0.23.6" +version = "0.23.7" description = "Pytest support for asyncio" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-asyncio-0.23.6.tar.gz", hash = "sha256:ffe523a89c1c222598c76856e76852b787504ddb72dd5d9b6617ffa8aa2cde5f"}, - {file = "pytest_asyncio-0.23.6-py3-none-any.whl", hash = "sha256:68516fdd1018ac57b846c9846b954f0393b26f094764a28c955eabb0536a4e8a"}, + {file = "pytest_asyncio-0.23.7-py3-none-any.whl", hash = "sha256:009b48127fbe44518a547bddd25611551b0e43ccdbf1e67d12479f569832c20b"}, + {file = "pytest_asyncio-0.23.7.tar.gz", hash = "sha256:5f5c72948f4c49e7db4f29f2521d4031f1c27f86e57b046126654083d4770268"}, ] [package.dependencies] @@ -743,53 +574,6 @@ pytest = ">=7.0.0,<9" docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] -[[package]] -name = "pytest-cov" -version = "5.0.0" -description = "Pytest plugin for measuring coverage." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, - {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, -] - -[package.dependencies] -coverage = {version = ">=5.2.1", extras = ["toml"]} -pytest = ">=4.6" - -[package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] - -[[package]] -name = "pytest-freezer" -version = "0.4.8" -description = "Pytest plugin providing a fixture interface for spulec/freezegun" -optional = false -python-versions = ">= 3.6" -files = [ - {file = "pytest_freezer-0.4.8-py3-none-any.whl", hash = "sha256:644ce7ddb8ba52b92a1df0a80a699bad2b93514c55cf92e9f2517b68ebe74814"}, - {file = "pytest_freezer-0.4.8.tar.gz", hash = "sha256:8ee2f724b3ff3540523fa355958a22e6f4c1c819928b78a7a183ae4248ce6ee6"}, -] - -[package.dependencies] -freezegun = ">=1.0" -pytest = ">=3.6" - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -description = "Extensions to the standard Python datetime module" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -files = [ - {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, - {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, -] - -[package.dependencies] -six = ">=1.5" - [[package]] name = "pyyaml" version = "6.0.1" @@ -802,6 +586,7 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -809,8 +594,16 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -827,6 +620,7 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -834,6 +628,7 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -865,17 +660,6 @@ files = [ {file = "ruff-0.4.2.tar.gz", hash = "sha256:33bcc160aee2520664bc0859cfeaebc84bb7323becff3f303b8f1f2d81cb4edc"}, ] -[[package]] -name = "six" -version = "1.16.0" -description = "Python 2 and 3 compatibility utilities" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] - [[package]] name = "syrupy" version = "4.6.1" @@ -892,13 +676,13 @@ pytest = ">=7.0.0,<9.0.0" [[package]] name = "typing-extensions" -version = "4.11.0" +version = "4.12.0" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, - {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, + {file = "typing_extensions-4.12.0-py3-none-any.whl", hash = "sha256:b349c66bea9016ac22978d800cfff206d5f9816951f12a7d0ec5578b0a819594"}, + {file = "typing_extensions-4.12.0.tar.gz", hash = "sha256:8cbcdc8606ebcb0d95453ad7dc5065e6237b6aa230a31e81d0f440c30fed5fd8"}, ] [[package]] @@ -1025,4 +809,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "035a7507a5a433a4bd15b1bceebccc0d7bc4b38626bca0c41daa0fa005fb0d41" +content-hash = "9297e1e33f562a6f3eb950227e60763c2d21ddb67559c1c85a25c71015f1a97d" diff --git a/pyproject.toml b/pyproject.toml index e6335b3..d8d5a7a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,14 +1,14 @@ [tool.poetry] -name = "forecast-solar" -version = "0.0.0" +name = "open-meteo-solar-forecast" +version = "0.0.6" description = "Asynchronous Python client for getting forecast solar information" -authors = ["Klaas Schoute "] -maintainers = ["Klaas Schoute "] +authors = ["Klaas Schoute ", "Rany "] +maintainers = ["Rany "] license = "MIT" readme = "README.md" -homepage = "https://github.com/home-assistant-libs/forecast_solar" -repository = "https://github.com/home-assistant-libs/forecast_solar" -documentation = "https://github.com/home-assistant-libs/forecast_solar" +homepage = "https://github.com/rany2/open-meteo-solar-forecast" +repository = "https://github.com/rany2/open-meteo-solar-forecast" +documentation = "https://github.com/rany2/open-meteo-solar-forecast" keywords = ["forecast", "solar", "power", "energy", "api", "async", "client"] classifiers = [ "Framework :: AsyncIO", @@ -21,39 +21,25 @@ classifiers = [ "Topic :: Software Development :: Libraries :: Python Modules", ] packages = [ - { include = "forecast_solar", from = "src" }, + { include = "open_meteo_solar_forecast", from = "src" }, ] [tool.poetry.dependencies] aiohttp = ">=3.0.0" -aiodns = ">=3.0.0" python = "^3.11" -yarl = ">=1.6.0" [tool.poetry.urls] -"Bug Tracker" = "https://github.com/home-assistant-libs/forecast_solar/issues" -Changelog = "https://github.com/home-assistant-libs/forecast_solar/releases" +"Bug Tracker" = "https://github.com/rany2/open-meteo-solar-forecast/issues" +Changelog = "https://github.com/rany2/open-meteo-solar-forecast/releases" [tool.poetry.group.dev.dependencies] aresponses = "3.0.0" covdefaults = "2.3.0" mypy = "1.10.0" -pytest = "8.1.1" -pytest-asyncio = "0.23.6" -pytest-cov = "5.0.0" -pytest-freezer = "0.4.8" ruff = "0.4.2" syrupy = "4.6.1" yamllint = "1.35.1" -[tool.coverage.run] -plugins = ["covdefaults"] -source = ["forecast_solar"] - -[tool.coverage.report] -fail_under = 90 -show_missing = true - [tool.mypy] # Specify the target platform details in config, so your developers are # free to run mypy on Windows, Linux, or macOS and get consistent @@ -88,10 +74,6 @@ warn_return_any = true warn_unused_configs = true warn_unused_ignores = true -[tool.pytest.ini_options] -addopts = "--cov" -asyncio_mode = "auto" - [tool.ruff] target-version = "py311" lint.select = ["ALL"] @@ -112,12 +94,8 @@ lint.ignore = [ "ISC001", ] -[tool.ruff.lint.flake8-pytest-style] -mark-parentheses = false -fixture-parentheses = false - [tool.ruff.lint.isort] -known-first-party = ["forecast_solar"] +known-first-party = ["open_meteo_solar_forecast"] [tool.ruff.lint.mccabe] max-complexity = 25 diff --git a/src/forecast_solar/__init__.py b/src/forecast_solar/__init__.py deleted file mode 100644 index c4f56cb..0000000 --- a/src/forecast_solar/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Asynchronous Python client for the Forecast.Solar API.""" - -from .exceptions import ( - ForecastSolarAuthenticationError, - ForecastSolarConfigError, - ForecastSolarConnectionError, - ForecastSolarError, - ForecastSolarRatelimitError, - ForecastSolarRequestError, -) -from .forecast_solar import ForecastSolar -from .models import AccountType, Estimate, Ratelimit - -__all__ = [ - "AccountType", - "Estimate", - "ForecastSolar", - "ForecastSolarAuthenticationError", - "ForecastSolarConfigError", - "ForecastSolarConnectionError", - "ForecastSolarError", - "ForecastSolarRatelimitError", - "ForecastSolarRequestError", - "Ratelimit", -] diff --git a/src/forecast_solar/exceptions.py b/src/forecast_solar/exceptions.py deleted file mode 100644 index 56dbddf..0000000 --- a/src/forecast_solar/exceptions.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Exceptions for Forecast.Solar.""" - -from datetime import datetime -from typing import Any - - -class ForecastSolarError(Exception): - """Generic Forecast.Solar exception.""" - - -class ForecastSolarConnectionError(ForecastSolarError): - """Forecast.Solar API connection exception.""" - - -class ForecastSolarConfigError(ForecastSolarError): - """Forecast.Solar API configuration exception.""" - - def __init__(self, data: dict[str, str]) -> None: - """Init a solar config error.""" - super().__init__(f'{data["text"]} (error 422)') - - -class ForecastSolarAuthenticationError(ForecastSolarError): - """Forecast.Solar API authentication exception.""" - - def __init__(self, data: dict[str, str]) -> None: - """Init a solar auth error. - - https://doc.forecast.solar/doku.php?id=api#invalid_request - """ - # seems that code is missing in response in some endpoints (i.e /info) - code = data.get("code") - super().__init__(f'{data["text"]} (error {code})') - self.code = code - - -class ForecastSolarRequestError(ForecastSolarError): - """Forecast.Solar wrong request input variables.""" - - def __init__(self, data: dict[str, str]) -> None: - """Init a solar request error. - - https://doc.forecast.solar/doku.php?id=api#invalid_request - """ - super().__init__(f'{data["text"]} (error {data["code"]})') - self.code = data["code"] - - -class ForecastSolarRatelimitError(ForecastSolarRequestError): - """Forecast.Solar maximum number of requests reached exception.""" - - def __init__(self, data: dict[str, Any]) -> None: - """Init a rate limit error.""" - super().__init__(data) - - self.reset_at = datetime.fromisoformat(data["ratelimit"]["retry-at"]) diff --git a/src/forecast_solar/forecast_solar.py b/src/forecast_solar/forecast_solar.py deleted file mode 100644 index 2878bdf..0000000 --- a/src/forecast_solar/forecast_solar.py +++ /dev/null @@ -1,231 +0,0 @@ -"""Asynchronous Python client for the Forecast.Solar API.""" - -from __future__ import annotations - -from dataclasses import dataclass -from typing import Any, Self - -from aiodns import DNSResolver -from aiodns.error import DNSError -from aiohttp import ClientSession -from yarl import URL - -from .exceptions import ( - ForecastSolarAuthenticationError, - ForecastSolarConfigError, - ForecastSolarConnectionError, - ForecastSolarError, - ForecastSolarRatelimitError, - ForecastSolarRequestError, -) -from .models import Estimate, Ratelimit - - -@dataclass -class ForecastSolar: - """Main class for handling connections with the Forecast.Solar API.""" - - azimuth: float - declination: float - kwp: float - latitude: float - longitude: float - - api_key: str | None = None - damping: float = 0 - damping_morning: float | None = None - damping_evening: float | None = None - horizon: str | None = None - - session: ClientSession | None = None - ratelimit: Ratelimit | None = None - inverter: float | None = None - _close_session: bool = False - - async def _request( - self, - uri: str, - *, - rate_limit: bool = True, - authenticate: bool = True, - params: dict[str, Any] | None = None, - ) -> Any: - """Handle a request to the Forecast.Solar API. - - A generic method for sending/handling HTTP requests done against - the Forecast.Solar API. - - Args: - ---- - uri: Request URI, for example, 'estimate' - rate_limit: Parse rate limit from response. Set to False for - endpoints that are missing rate limiting headers in response. - authenticate: Prefix request with api_key. Set to False for - endpoints that do not provide authentication. - - Returns: - ------- - A Python dictionary (JSON decoded) with the response from - the Forecast.Solar API. - - Raises: - ------ - ForecastSolarAuthenticationError: If the API key is invalid. - ForecastSolarConnectionError: An error occurred while communicating - with the Forecast.Solar API. - ForecastSolarError: Received an unexpected response from the - Forecast.Solar API. - ForecastSolarRequestError: There is something wrong with the - variables used in the request. - ForecastSolarRatelimitError: The number of requests has exceeded - the rate limit of the Forecast.Solar API. - - """ - # Forecast.Solar is currently experiencing IPv6 issues. - # However, their DNS does return an non-working IPv6 address. - # This ensures we use the IPv4 address. - dns = DNSResolver() - try: - result = await dns.query("api.forecast.solar", "A") - except DNSError as err: - raise ForecastSolarConnectionError( - "Error while resolving Forecast.Solar API address" - ) from err - - if not result: - raise ForecastSolarConnectionError( - "Could not resolve Forecast.Solar API address" - ) - - # Connect as normal - url = URL.build(scheme="https", host=result[0].host) - - # Add API key if one is provided - if authenticate and self.api_key is not None: - url = url.with_path(f"{self.api_key}/") - - url = url.join(URL(uri)) - - if self.session is None: - self.session = ClientSession() - self._close_session = True - - response = await self.session.request( - "GET", - url, - params=params, - headers={"Host": "api.forecast.solar"}, - ssl=False, - ) - - if response.status in (502, 503): - raise ForecastSolarConnectionError("The Forecast.Solar API is unreachable") - - if response.status == 400: - data = await response.json() - raise ForecastSolarRequestError(data["message"]) - - if response.status in (401, 403): - data = await response.json() - raise ForecastSolarAuthenticationError(data["message"]) - - if response.status == 422: - data = await response.json() - raise ForecastSolarConfigError(data["message"]) - - if response.status == 429: - data = await response.json() - raise ForecastSolarRatelimitError(data["message"]) - - if rate_limit and response.status < 500: - self.ratelimit = Ratelimit.from_response(response) - - response.raise_for_status() - - content_type = response.headers.get("Content-Type", "") - if "application/json" not in content_type: - text = await response.text() - raise ForecastSolarError( - "Unexpected response from the Forecast.Solar API", - {"Content-Type": content_type, "response": text}, - ) - - return await response.json() - - async def validate_plane(self) -> bool: - """Validate plane by calling the Forecast.Solar API. - - Returns - ------- - True, if plane is valid. - - """ - await self._request( - f"check/{self.latitude}/{self.longitude}" - f"/{self.declination}/{self.azimuth}/{self.kwp}", - rate_limit=False, - authenticate=False, - ) - - return True - - async def validate_api_key(self) -> bool: - """Validate api key by calling the Forecast.Solar API. - - Returns - ------- - True, if api key is valid - - """ - await self._request("info", rate_limit=False) - - return True - - async def estimate(self) -> Estimate: - """Get solar production estimations from the Forecast.Solar API. - - Returns - ------- - A Estimate object, with a estimated production forecast. - - """ - params = {"time": "iso8601", "damping": str(self.damping)} - if self.inverter is not None: - params["inverter"] = str(self.inverter) - if self.horizon is not None: - params["horizon"] = str(self.horizon) - if self.damping_morning is not None and self.damping_evening is not None: - params["damping_morning"] = str(self.damping_morning) - params["damping_evening"] = str(self.damping_evening) - data = await self._request( - f"estimate/{self.latitude}/{self.longitude}" - f"/{self.declination}/{self.azimuth}/{self.kwp}", - params=params, - ) - - return Estimate.from_dict(data) - - async def close(self) -> None: - """Close open client session.""" - if self.session and self._close_session: - await self.session.close() - - async def __aenter__(self) -> Self: - """Async enter. - - Returns - ------- - The ForecastSolar object. - - """ - return self - - async def __aexit__(self, *_exc_info: object) -> None: - """Async exit. - - Args: - ---- - _exc_info: Exec type. - - """ - await self.close() diff --git a/src/forecast_solar/models.py b/src/forecast_solar/models.py deleted file mode 100644 index 347ea18..0000000 --- a/src/forecast_solar/models.py +++ /dev/null @@ -1,221 +0,0 @@ -"""Data models for the Forecast.Solar API.""" - -from __future__ import annotations - -from dataclasses import dataclass -from datetime import date, datetime, timedelta -from enum import Enum -from typing import TYPE_CHECKING, Any -from zoneinfo import ZoneInfo - -if TYPE_CHECKING: - from aiohttp import ClientResponse - - -def _timed_value(at: datetime, data: dict[datetime, int]) -> int | None: - """Return the value for a specific time.""" - value = None - for timestamp, cur_value in data.items(): - if timestamp > at: - return value - value = cur_value - - return None - - -def _interval_value_sum( - interval_begin: datetime, interval_end: datetime, data: dict[datetime, int] -) -> int: - """Return the sum of values in interval.""" - total = 0 - - for timestamp, wh in data.items(): - # Skip all until this hour - if timestamp < interval_begin: - continue - - if timestamp >= interval_end: - break - - total += wh - - return total - - -class AccountType(str, Enum): - """Enumeration representing the Forecast.Solar account type.""" - - PUBLIC = "public" - PERSONAL = "personal" - PROFESSIONAL = "professional" - - -@dataclass -class Estimate: - """Object holding estimate forecast results from Forecast.Solar. - - Attributes - ---------- - watts: Estimated solar power output per time period. - wh_period: Estimated solar energy production differences per hour. - wh_days: Estimated solar energy production per day. - - """ - - watts: dict[datetime, int] - wh_period: dict[datetime, int] - wh_days: dict[datetime, int] - api_rate_limit: int - api_timezone: str - - @property - def timezone(self) -> str: - """Return API timezone information.""" - return self.api_timezone - - @property - def account_type(self) -> AccountType: - """Return API account_type information.""" - if self.api_rate_limit == 60: - return AccountType.PERSONAL - if self.api_rate_limit == 5: - return AccountType.PROFESSIONAL - return AccountType.PUBLIC - - @property - def energy_production_today(self) -> int: - """Return estimated energy produced today.""" - return self.day_production(self.now().date()) - - @property - def energy_production_tomorrow(self) -> int: - """Return estimated energy produced today.""" - return self.day_production(self.now().date() + timedelta(days=1)) - - @property - def energy_production_today_remaining(self) -> int: - """Return estimated energy produced in rest of today.""" - return _interval_value_sum( - self.now(), - self.now().replace(hour=0, minute=0, second=0, microsecond=0) - + timedelta(days=1), - self.wh_period, - ) - - @property - def power_production_now(self) -> int: - """Return estimated power production right now.""" - return self.power_production_at_time(self.now()) - - @property - def power_highest_peak_time_today(self) -> datetime: - """Return datetime with highest power production moment today.""" - return self.peak_production_time(self.now().date()) - - @property - def power_highest_peak_time_tomorrow(self) -> datetime: - """Return datetime with highest power production moment tomorrow.""" - return self.peak_production_time(self.now().date() + timedelta(days=1)) - - @property - def energy_current_hour(self) -> int: - """Return the estimated energy production for the current hour.""" - return _interval_value_sum( - self.now().replace(minute=0, second=0, microsecond=0), - self.now().replace(minute=0, second=0, microsecond=0) + timedelta(hours=1), - self.wh_period, - ) - - def day_production(self, specific_date: date) -> int: - """Return the day production.""" - for timestamp, production in self.wh_days.items(): - if timestamp.date() == specific_date: - return production - - return 0 - - def now(self) -> datetime: - """Return the current timestamp in the API timezone.""" - return datetime.now(tz=ZoneInfo(self.api_timezone)) - - def peak_production_time(self, specific_date: date) -> datetime: - """Return the peak time on a specific date.""" - value = max( - (watt for date, watt in self.watts.items() if date.date() == specific_date), - default=None, - ) - for timestamp, watt in self.watts.items(): - if watt == value: - return timestamp - raise RuntimeError("No peak production time found") - - def power_production_at_time(self, time: datetime) -> int: - """Return estimated power production at a specific time.""" - return _timed_value(time, self.watts) or 0 - - def sum_energy_production(self, period_hours: int) -> int: - """Return the sum of the energy production.""" - now = self.now().replace(minute=59, second=59, microsecond=999) - until = now + timedelta(hours=period_hours) - - return _interval_value_sum(now, until, self.wh_period) - - @classmethod - def from_dict(cls: type[Estimate], data: dict[str, Any]) -> Estimate: - """Return a Estimate object from a Forecast.Solar API response. - - Converts a dictionary, obtained from the Forecast.Solar API into - a Estimate object. - - Args: - ---- - data: The estimate response from the Forecast.Solar API. - - Returns: - ------- - An Estimate object. - - """ - return cls( - watts={ - datetime.fromisoformat(d): w for d, w in data["result"]["watts"].items() - }, - wh_period={ - datetime.fromisoformat(d): e - for d, e in data["result"]["watt_hours_period"].items() - }, - wh_days={ - datetime.fromisoformat(d): e - for d, e in data["result"]["watt_hours_day"].items() - }, - api_rate_limit=data["message"]["ratelimit"]["limit"], - api_timezone=data["message"]["info"]["timezone"], - ) - - -@dataclass -class Ratelimit: - """Information about the current rate limit.""" - - call_limit: int - remaining_calls: int - period: int - retry_at: datetime | None - - @classmethod - def from_response(cls: type[Ratelimit], response: ClientResponse) -> Ratelimit: - """Initialize rate limit object from response.""" - # The documented headers do not match the returned headers - # https://doc.forecast.solar/doku.php?id=api#headers - limit = int(response.headers["X-Ratelimit-Limit"]) - period = int(response.headers["X-Ratelimit-Period"]) - - # Remaining is not there if we exceeded limit - remaining = int(response.headers.get("X-Ratelimit-Remaining", 0)) - - if "X-Ratelimit-Retry-At" in response.headers: - retry_at = datetime.fromisoformat(response.headers["X-Ratelimit-Retry-At"]) - else: - retry_at = None - - return cls(limit, remaining, period, retry_at) diff --git a/src/open_meteo_solar_forecast/__init__.py b/src/open_meteo_solar_forecast/__init__.py new file mode 100644 index 0000000..b989934 --- /dev/null +++ b/src/open_meteo_solar_forecast/__init__.py @@ -0,0 +1,23 @@ +"""Asynchronous Python client to get PV solar power estimates from Open-Meteo API.""" + +from .exceptions import ( + OpenMeteoSolarForecastAuthenticationError, + OpenMeteoSolarForecastConfigError, + OpenMeteoSolarForecastConnectionError, + OpenMeteoSolarForecastError, + OpenMeteoSolarForecastRatelimitError, + OpenMeteoSolarForecastRequestError, +) +from .models import Estimate +from .open_meteo_solar_forecast import OpenMeteoSolarForecast + +__all__ = [ + "Estimate", + "OpenMeteoSolarForecast", + "OpenMeteoSolarForecastAuthenticationError", + "OpenMeteoSolarForecastConfigError", + "OpenMeteoSolarForecastConnectionError", + "OpenMeteoSolarForecastError", + "OpenMeteoSolarForecastRatelimitError", + "OpenMeteoSolarForecastRequestError", +] diff --git a/src/open_meteo_solar_forecast/constants.py b/src/open_meteo_solar_forecast/constants.py new file mode 100644 index 0000000..0821d7f --- /dev/null +++ b/src/open_meteo_solar_forecast/constants.py @@ -0,0 +1,13 @@ +"""Constants for the solar forecast module.""" + +# Most solar PV modules have a temperature coefficient of around -0.3% / ยฐC to -0.5% / ยฐC. +# +# STC is an industry-wide standard to indicate the performance of PV modules and specifies +# a cell temperature of 25ยฐC and an irradiance of 1000 W/m2 with an air mass 1.5 (AM1. 5) spectrum. +# +# Source: +# - https://www.eco-greenenergy.com/temperature-coefficient-of-solar-pv-module/ +# - https://sinovoltaics.com/learning-center/quality/standard-test-conditions-stc-definition-and-problems/ +ALPHA_TEMP = -0.05 # ยฐC-1 (temperature coefficient) +G_STD = 1000.0 # W/m2 (standard irradiance) +TEMP_STD = 25.0 # ยฐC (standard cell temperature) diff --git a/src/open_meteo_solar_forecast/exceptions.py b/src/open_meteo_solar_forecast/exceptions.py new file mode 100644 index 0000000..0bda5b4 --- /dev/null +++ b/src/open_meteo_solar_forecast/exceptions.py @@ -0,0 +1,25 @@ +"""Exceptions for the OpenMeteoSolarForecast API client.""" + + +class OpenMeteoSolarForecastError(Exception): + """Generic OpenMeteoSolarForecast exception.""" + + +class OpenMeteoSolarForecastConnectionError(OpenMeteoSolarForecastError): + """OpenMeteoSolarForecast connection exception.""" + + +class OpenMeteoSolarForecastConfigError(OpenMeteoSolarForecastError): + """OpenMeteoSolarForecast configuration exception.""" + + +class OpenMeteoSolarForecastAuthenticationError(OpenMeteoSolarForecastError): + """OpenMeteoSolarForecast authentication exception.""" + + +class OpenMeteoSolarForecastRequestError(OpenMeteoSolarForecastError): + """OpenMeteoSolarForecast request exception.""" + + +class OpenMeteoSolarForecastRatelimitError(OpenMeteoSolarForecastRequestError): + """OpenMeteoSolarForecast rate limit exception.""" diff --git a/src/open_meteo_solar_forecast/models.py b/src/open_meteo_solar_forecast/models.py new file mode 100644 index 0000000..285eae5 --- /dev/null +++ b/src/open_meteo_solar_forecast/models.py @@ -0,0 +1,137 @@ +"""Data models for the Forecast.Solar API.""" + +from __future__ import annotations + +import datetime as dt +from dataclasses import dataclass + + +def _timed_value(at: dt.datetime, data: dict[dt.datetime, int]) -> int | None: + """Return the value for a specific time.""" + value = None + for timestamp, cur_value in data.items(): + if timestamp > at: + return value + value = cur_value + + return None + + +def _interval_value_sum( + interval_begin: dt.datetime, interval_end: dt.datetime, data: dict[dt.datetime, int] +) -> int: + """Return the sum of values in interval.""" + total = 0 + + for timestamp, wh in data.items(): + # Skip all until this hour + if timestamp < interval_begin: + continue + + if timestamp >= interval_end: + break + + total += wh + + return total + + +@dataclass +class Estimate: + """Object holding estimate forecast results from Forecast.Solar. + + Attributes + ---------- + watts: Estimated solar power output per time period. + wh_period: Estimated solar energy production differences per hour. + wh_days: Estimated solar energy production per day. + + """ + + watts: dict[dt.datetime, int] + wh_period: dict[dt.datetime, int] + wh_days: dict[dt.datetime, int] + api_timezone: dt.timezone + + @property + def timezone(self) -> timezone: + """Return API timezone information.""" + return self.api_timezone + + @property + def energy_production_today(self) -> int: + """Return estimated energy produced today.""" + return self.day_production(self.now().date()) + + @property + def energy_production_tomorrow(self) -> int: + """Return estimated energy produced today.""" + return self.day_production(self.now().date() + dt.timedelta(days=1)) + + @property + def energy_production_today_remaining(self) -> int: + """Return estimated energy produced in rest of today.""" + return _interval_value_sum( + self.now(), + self.now().replace(hour=0, minute=0, second=0, microsecond=0) + + dt.timedelta(days=1), + self.wh_period, + ) + + @property + def power_production_now(self) -> int: + """Return estimated power production right now.""" + return self.power_production_at_time(self.now()) + + @property + def power_highest_peak_time_today(self) -> dt.datetime: + """Return datetime with highest power production moment today.""" + return self.peak_production_time(self.now().date()) + + @property + def power_highest_peak_time_tomorrow(self) -> dt.datetime: + """Return datetime with highest power production moment tomorrow.""" + return self.peak_production_time(self.now().date() + dt.timedelta(days=1)) + + @property + def energy_current_hour(self) -> int: + """Return the estimated energy production for the current hour.""" + return _interval_value_sum( + self.now().replace(minute=0, second=0, microsecond=0), + self.now().replace(minute=0, second=0, microsecond=0) + dt.timedelta(hours=1), + self.wh_period, + ) + + def day_production(self, specific_date: dt.date) -> int: + """Return the day production.""" + for date, production in self.wh_days.items(): + if date == specific_date: + return production + + return 0 + + def now(self) -> dt.datetime: + """Return the current timestamp in the API timezone.""" + return dt.datetime.now(tz=self.api_timezone) + + def peak_production_time(self, specific_date: dt.date) -> dt.datetime: + """Return the peak time on a specific date.""" + value = max( + (watt for date, watt in self.watts.items() if date.date() == specific_date), + default=None, + ) + for timestamp, watt in self.watts.items(): + if watt == value and timestamp.date() == specific_date: + return timestamp + raise RuntimeError("No peak production time found") + + def power_production_at_time(self, time: dt.datetime) -> int: + """Return estimated power production at a specific time.""" + return _timed_value(time, self.watts) or 0 + + def sum_energy_production(self, period_hours: int) -> int: + """Return the sum of the energy production.""" + now = self.now().replace(minute=59, second=59, microsecond=999) + until = now + dt.timedelta(hours=period_hours) + + return _interval_value_sum(now, until, self.wh_period) diff --git a/src/open_meteo_solar_forecast/open_meteo_solar_forecast.py b/src/open_meteo_solar_forecast/open_meteo_solar_forecast.py new file mode 100644 index 0000000..ce0cb3c --- /dev/null +++ b/src/open_meteo_solar_forecast/open_meteo_solar_forecast.py @@ -0,0 +1,237 @@ +"""Asynchronous Python client for the API.""" + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime as dt, timedelta, timezone +from time import mktime, strptime +from typing import Any, Self + +from aiohttp import BasicAuth, ClientSession + +from .constants import ALPHA_TEMP, G_STD, TEMP_STD +from .exceptions import ( + OpenMeteoSolarForecastAuthenticationError, + OpenMeteoSolarForecastConfigError, + OpenMeteoSolarForecastConnectionError, + OpenMeteoSolarForecastError, + OpenMeteoSolarForecastRatelimitError, + OpenMeteoSolarForecastRequestError, +) +from .models import Estimate + + +@dataclass +class OpenMeteoSolarForecast: + """Main class for handling connections with the API.""" + + azimuth: float + declination: float + kwp: float + latitude: float + longitude: float + + base_url: str | None = None + api_key: str | None = None + efficiency_factor: float = 1.0 + + session: ClientSession | None = None + _close_session: bool = False + + def __post_init__(self) -> None: + """Initialize the OpenMeteoSolarForecast object.""" + if self.base_url is None: + self.base_url = "https://api.open-meteo.com" + + async def _request( + self, + uri: str, + *, + params: dict[str, Any] | None = None, + ) -> Any: + """Handle a request to the API. + + A generic method for sending/handling HTTP requests done against the API. + + Args: + ---- + uri: Request URI, for example, '/v1/forecast'. + + Returns: + ------- + A Python dictionary (JSON decoded) with the response from the API. + + Raises: + ------ + OpenMeteoSolarForecastAuthenticationError: If the API key is invalid. + OpenMeteoSolarForecastConnectionError: An error occurred while communicating + with the API. + OpenMeteoSolarForecastError: Received an unexpected response from the API. + OpenMeteoSolarForecastRequestError: There is something wrong with the + variables used in the request. + OpenMeteoSolarForecastRatelimitError: The number of requests has exceeded + the rate limit of the API. + + """ + # Connect as normal + if self.session is None: + self.session = ClientSession() + self._close_session = True + + # Add the API key to the request + if self.api_key: + params = params or {} + params["apikey"] = self.api_key + + # Get response from the API + response = await self.session.request( + "GET", + self.base_url + uri, + params=params, + ) + + if response.status in (502, 503): + raise OpenMeteoSolarForecastConnectionError("The API is unreachable") + + if response.status == 400: + raise OpenMeteoSolarForecastRequestError("Bad request") + + if response.status in (401, 403): + raise OpenMeteoSolarForecastAuthenticationError("Invalid API key") + + if response.status == 422: + raise OpenMeteoSolarForecastConfigError("Invalid configuration") + + if response.status == 429: + raise OpenMeteoSolarForecastRatelimitError("Rate limit exceeded") + + response.raise_for_status() + + content_type = response.headers.get("Content-Type", "") + if "application/json" not in content_type: + text = await response.text() + raise OpenMeteoSolarForecastError( + "Unexpected response from the API", + {"Content-Type": content_type, "response": text}, + ) + + return await response.json() + + async def estimate(self) -> Estimate: + """Get solar production estimations from the API. + + Returns + ------- + A Estimate object, with a estimated production forecast. + + """ + params = { + "latitude": str(self.latitude), + "longitude": str(self.longitude), + "azimuth": str(self.azimuth), + "tilt": str(self.declination), + "minutely_15": "temperature_2m,global_tilted_irradiance,global_tilted_irradiance_instant", + "forecast_days": "16", + "past_days": "92", + "timezone": "auto", + } + data = await self._request( + "/v1/forecast", + params=params, + ) + gti_avg_arr = data["minutely_15"]["global_tilted_irradiance"] + gti_instant_arr = data["minutely_15"]["global_tilted_irradiance_instant"] + temp_arr = data["minutely_15"]["temperature_2m"] + utc_offset = data["utc_offset_seconds"] + time_arr = [ + dt.strptime(time, "%Y-%m-%dT%H:%M").replace( + tzinfo=timezone(timedelta(seconds=utc_offset)) + ) + for time in data["minutely_15"]["time"] + ] + + peak_power = self.kwp * 1000 # Convert kW to W + + power_avg: dict[dt, float] = {} + watts_instant: dict[dt, float] = {} + wh_days: dict[dt, float] = {} + + def gen_power(gti: float, temp: float) -> float: + """Calculate the power generated by a solar panel. + + Formula: + PPV(t) = Ppeak(G/Gstandard) - ฮฑT (Tc - Tstandard) + Gstandard = 1000 W/m2, ฮฑT = 0.005ยฐC-1, Tstandard = 25ยฐC + Ppeak = max power output of PV, Tc = ambient temperature, G = radiation + """ + power = peak_power * (gti / G_STD) - ALPHA_TEMP * (temp - TEMP_STD) + power *= self.efficiency_factor + return power + + for i, time in enumerate(time_arr): + # If any of the values are missing, skip the iteration + if None in (gti_avg_arr[i], gti_instant_arr[i], temp_arr[i]): + continue + + # Total radiation received on a tilted pane + gti_avg = gti_avg_arr[i] + gti_instant = gti_instant_arr[i] + + # Get the temperature + temp_instant = temp_arr[i] + temp_avg = (temp_arr[i] + temp_arr[i - 1]) / 2 if i > 0 else temp_arr[i] + + # For minutely data, the time_start is 15 minutes before the current time + time_start = time - timedelta(minutes=15) + + # Calculate and store the power generated + power_avg[time_start] = gen_power(gti_avg, temp_avg) + watts_instant[time_start] = gen_power(gti_instant, temp_instant) + + # Calculate the average power generated per hour + wh_period: dict[dt, float] = {} + wh_period_count: dict[dt, int] = {} + for time, power in power_avg.items(): + hour = time.replace(minute=0, second=0, microsecond=0) + wh_period[hour] = wh_period.get(hour, 0) + power + wh_period_count[hour] = wh_period_count.get(hour, 0) + 1 + for time in wh_period: + wh_period[time] /= wh_period_count[time] + + # Calculate the total energy produced per day + for time, power in wh_period.items(): + day = time.date() + wh_days[day] = wh_days.get(day, 0) + power + + # Return the estimate object + return Estimate( + watts=watts_instant, + wh_period=wh_period, + wh_days=wh_days, + api_timezone=timezone(timedelta(seconds=utc_offset)), + ) + + async def close(self) -> None: + """Close open client session.""" + if self.session and self._close_session: + await self.session.close() + + async def __aenter__(self) -> Self: + """Async enter. + + Returns + ------- + The OpenMeteoSolarForecast object. + + """ + return self + + async def __aexit__(self, *_exc_info: object) -> None: + """Async exit. + + Args: + ---- + _exc_info: Exec type. + + """ + await self.close() diff --git a/src/forecast_solar/py.typed b/src/open_meteo_solar_forecast/py.typed similarity index 100% rename from src/forecast_solar/py.typed rename to src/open_meteo_solar_forecast/py.typed diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index acc564f..0000000 --- a/tests/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Asynchronous Python client for the Forecast.Solar API.""" - -from pathlib import Path - - -def load_fixtures(filename: str) -> str: - """Load a fixture.""" - path = Path(__file__).parent / "fixtures" / filename - return path.read_text() diff --git a/tests/__snapshots__/test_models.ambr b/tests/__snapshots__/test_models.ambr deleted file mode 100644 index 1d9b6b4..0000000 --- a/tests/__snapshots__/test_models.ambr +++ /dev/null @@ -1,7 +0,0 @@ -# serializer version: 1 -# name: test_estimated_forecast - Estimate(watts={FakeDatetime(2024, 4, 26, 6, 20, 17, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 0, FakeDatetime(2024, 4, 26, 7, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 270, FakeDatetime(2024, 4, 26, 8, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 481, FakeDatetime(2024, 4, 26, 9, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 594, FakeDatetime(2024, 4, 26, 10, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 781, FakeDatetime(2024, 4, 26, 11, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 869, FakeDatetime(2024, 4, 26, 12, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 773, FakeDatetime(2024, 4, 26, 13, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 710, FakeDatetime(2024, 4, 26, 14, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 620, FakeDatetime(2024, 4, 26, 15, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 517, FakeDatetime(2024, 4, 26, 16, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 430, FakeDatetime(2024, 4, 26, 17, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 325, FakeDatetime(2024, 4, 26, 18, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 204, FakeDatetime(2024, 4, 26, 19, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 89, FakeDatetime(2024, 4, 26, 20, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 38, FakeDatetime(2024, 4, 26, 20, 59, 23, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 0, FakeDatetime(2024, 4, 27, 6, 18, 17, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 0, FakeDatetime(2024, 4, 27, 7, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 194, FakeDatetime(2024, 4, 27, 8, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 325, FakeDatetime(2024, 4, 27, 9, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 432, FakeDatetime(2024, 4, 27, 10, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 514, FakeDatetime(2024, 4, 27, 11, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 572, FakeDatetime(2024, 4, 27, 12, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 604, FakeDatetime(2024, 4, 27, 13, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 602, FakeDatetime(2024, 4, 27, 14, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 563, FakeDatetime(2024, 4, 27, 15, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 502, FakeDatetime(2024, 4, 27, 16, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 422, FakeDatetime(2024, 4, 27, 17, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 320, FakeDatetime(2024, 4, 27, 18, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 201, FakeDatetime(2024, 4, 27, 19, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 87, FakeDatetime(2024, 4, 27, 20, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 22, FakeDatetime(2024, 4, 27, 21, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 9, FakeDatetime(2024, 4, 27, 21, 1, 5, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 0}, wh_period={FakeDatetime(2024, 4, 26, 6, 20, 17, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 0, FakeDatetime(2024, 4, 26, 7, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 89, FakeDatetime(2024, 4, 26, 8, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 376, FakeDatetime(2024, 4, 26, 9, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 538, FakeDatetime(2024, 4, 26, 10, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 688, FakeDatetime(2024, 4, 26, 11, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 825, FakeDatetime(2024, 4, 26, 12, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 821, FakeDatetime(2024, 4, 26, 13, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 742, FakeDatetime(2024, 4, 26, 14, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 665, FakeDatetime(2024, 4, 26, 15, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 569, FakeDatetime(2024, 4, 26, 16, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 474, FakeDatetime(2024, 4, 26, 17, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 378, FakeDatetime(2024, 4, 26, 18, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 265, FakeDatetime(2024, 4, 26, 19, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 147, FakeDatetime(2024, 4, 26, 20, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 64, FakeDatetime(2024, 4, 26, 20, 59, 23, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 19, FakeDatetime(2024, 4, 27, 6, 18, 17, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 0, FakeDatetime(2024, 4, 27, 7, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 67, FakeDatetime(2024, 4, 27, 8, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 260, FakeDatetime(2024, 4, 27, 9, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 379, FakeDatetime(2024, 4, 27, 10, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 473, FakeDatetime(2024, 4, 27, 11, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 543, FakeDatetime(2024, 4, 27, 12, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 588, FakeDatetime(2024, 4, 27, 13, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 603, FakeDatetime(2024, 4, 27, 14, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 583, FakeDatetime(2024, 4, 27, 15, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 533, FakeDatetime(2024, 4, 27, 16, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 462, FakeDatetime(2024, 4, 27, 17, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 371, FakeDatetime(2024, 4, 27, 18, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 261, FakeDatetime(2024, 4, 27, 19, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 144, FakeDatetime(2024, 4, 27, 20, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 55, FakeDatetime(2024, 4, 27, 21, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 16, FakeDatetime(2024, 4, 27, 21, 1, 5, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 0}, wh_days={FakeDatetime(2024, 4, 26, 0, 0): 6660, FakeDatetime(2024, 4, 27, 0, 0): 5338}, api_rate_limit=12, api_timezone='Europe/Amsterdam') -# --- -# name: test_estimated_forecast_with_subscription - Estimate(watts={FakeDatetime(2024, 4, 27, 6, 18, 15, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 0, FakeDatetime(2024, 4, 27, 6, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 47, FakeDatetime(2024, 4, 27, 7, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 92, FakeDatetime(2024, 4, 27, 7, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 153, FakeDatetime(2024, 4, 27, 8, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 215, FakeDatetime(2024, 4, 27, 8, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 279, FakeDatetime(2024, 4, 27, 9, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 342, FakeDatetime(2024, 4, 27, 9, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 402, FakeDatetime(2024, 4, 27, 10, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 457, FakeDatetime(2024, 4, 27, 10, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 501, FakeDatetime(2024, 4, 27, 11, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 545, FakeDatetime(2024, 4, 27, 11, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 587, FakeDatetime(2024, 4, 27, 12, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 616, FakeDatetime(2024, 4, 27, 12, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 625, FakeDatetime(2024, 4, 27, 13, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 635, FakeDatetime(2024, 4, 27, 13, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 639, FakeDatetime(2024, 4, 27, 14, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 635, FakeDatetime(2024, 4, 27, 14, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 619, FakeDatetime(2024, 4, 27, 15, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 600, FakeDatetime(2024, 4, 27, 15, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 570, FakeDatetime(2024, 4, 27, 16, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 536, FakeDatetime(2024, 4, 27, 16, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 500, FakeDatetime(2024, 4, 27, 17, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 458, FakeDatetime(2024, 4, 27, 17, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 403, FakeDatetime(2024, 4, 27, 18, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 344, FakeDatetime(2024, 4, 27, 18, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 281, FakeDatetime(2024, 4, 27, 19, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 215, FakeDatetime(2024, 4, 27, 19, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 147, FakeDatetime(2024, 4, 27, 20, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 85, FakeDatetime(2024, 4, 27, 20, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 42, FakeDatetime(2024, 4, 27, 21, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 18, FakeDatetime(2024, 4, 27, 21, 1, 7, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 0, FakeDatetime(2024, 4, 28, 6, 16, 16, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 0, FakeDatetime(2024, 4, 28, 6, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 45, FakeDatetime(2024, 4, 28, 7, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 88, FakeDatetime(2024, 4, 28, 7, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 147, FakeDatetime(2024, 4, 28, 8, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 208, FakeDatetime(2024, 4, 28, 8, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 270, FakeDatetime(2024, 4, 28, 9, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 332, FakeDatetime(2024, 4, 28, 9, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 396, FakeDatetime(2024, 4, 28, 10, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 487, FakeDatetime(2024, 4, 28, 10, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 636, FakeDatetime(2024, 4, 28, 11, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 778, FakeDatetime(2024, 4, 28, 11, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 822, FakeDatetime(2024, 4, 28, 12, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 801, FakeDatetime(2024, 4, 28, 12, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 751, FakeDatetime(2024, 4, 28, 13, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 746, FakeDatetime(2024, 4, 28, 13, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 787, FakeDatetime(2024, 4, 28, 14, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 837, FakeDatetime(2024, 4, 28, 14, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 865, FakeDatetime(2024, 4, 28, 15, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 855, FakeDatetime(2024, 4, 28, 15, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 804, FakeDatetime(2024, 4, 28, 16, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 765, FakeDatetime(2024, 4, 28, 16, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 739, FakeDatetime(2024, 4, 28, 17, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 687, FakeDatetime(2024, 4, 28, 17, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 594, FakeDatetime(2024, 4, 28, 18, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 500, FakeDatetime(2024, 4, 28, 18, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 408, FakeDatetime(2024, 4, 28, 19, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 309, FakeDatetime(2024, 4, 28, 19, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 201, FakeDatetime(2024, 4, 28, 20, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 103, FakeDatetime(2024, 4, 28, 20, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 47, FakeDatetime(2024, 4, 28, 21, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 19, FakeDatetime(2024, 4, 28, 21, 2, 49, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 0, FakeDatetime(2024, 4, 29, 6, 14, 18, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 0, FakeDatetime(2024, 4, 29, 6, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 41, FakeDatetime(2024, 4, 29, 7, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 93, FakeDatetime(2024, 4, 29, 7, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 184, FakeDatetime(2024, 4, 29, 8, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 291, FakeDatetime(2024, 4, 29, 8, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 426, FakeDatetime(2024, 4, 29, 9, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 569, FakeDatetime(2024, 4, 29, 9, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 695, FakeDatetime(2024, 4, 29, 10, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 735, FakeDatetime(2024, 4, 29, 10, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 691, FakeDatetime(2024, 4, 29, 11, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 707, FakeDatetime(2024, 4, 29, 11, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 815, FakeDatetime(2024, 4, 29, 12, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 910, FakeDatetime(2024, 4, 29, 12, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 935, FakeDatetime(2024, 4, 29, 13, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 919, FakeDatetime(2024, 4, 29, 13, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 854, FakeDatetime(2024, 4, 29, 14, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 768, FakeDatetime(2024, 4, 29, 14, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 683, FakeDatetime(2024, 4, 29, 15, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 628, FakeDatetime(2024, 4, 29, 15, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 585, FakeDatetime(2024, 4, 29, 16, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 544, FakeDatetime(2024, 4, 29, 16, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 499, FakeDatetime(2024, 4, 29, 17, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 452, FakeDatetime(2024, 4, 29, 17, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 398, FakeDatetime(2024, 4, 29, 18, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 339, FakeDatetime(2024, 4, 29, 18, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 275, FakeDatetime(2024, 4, 29, 19, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 208, FakeDatetime(2024, 4, 29, 19, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 140, FakeDatetime(2024, 4, 29, 20, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 79, FakeDatetime(2024, 4, 29, 20, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 39, FakeDatetime(2024, 4, 29, 21, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 16, FakeDatetime(2024, 4, 29, 21, 4, 31, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 0, FakeDatetime(2024, 4, 30, 6, 12, 21, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 0, FakeDatetime(2024, 4, 30, 6, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 41, FakeDatetime(2024, 4, 30, 7, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 82, FakeDatetime(2024, 4, 30, 7, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 141, FakeDatetime(2024, 4, 30, 8, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 203, FakeDatetime(2024, 4, 30, 8, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 266, FakeDatetime(2024, 4, 30, 9, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 329, FakeDatetime(2024, 4, 30, 9, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 390, FakeDatetime(2024, 4, 30, 10, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 445, FakeDatetime(2024, 4, 30, 10, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 490, FakeDatetime(2024, 4, 30, 11, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 531, FakeDatetime(2024, 4, 30, 11, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 566, FakeDatetime(2024, 4, 30, 12, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 594, FakeDatetime(2024, 4, 30, 12, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 611, FakeDatetime(2024, 4, 30, 13, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 623, FakeDatetime(2024, 4, 30, 13, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 625, FakeDatetime(2024, 4, 30, 14, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 623, FakeDatetime(2024, 4, 30, 14, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 615, FakeDatetime(2024, 4, 30, 15, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 599, FakeDatetime(2024, 4, 30, 15, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 570, FakeDatetime(2024, 4, 30, 16, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 535, FakeDatetime(2024, 4, 30, 16, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 496, FakeDatetime(2024, 4, 30, 17, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 452, FakeDatetime(2024, 4, 30, 17, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 401, FakeDatetime(2024, 4, 30, 18, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 343, FakeDatetime(2024, 4, 30, 18, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 278, FakeDatetime(2024, 4, 30, 19, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 209, FakeDatetime(2024, 4, 30, 19, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 138, FakeDatetime(2024, 4, 30, 20, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 76, FakeDatetime(2024, 4, 30, 20, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 37, FakeDatetime(2024, 4, 30, 21, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 15, FakeDatetime(2024, 4, 30, 21, 6, 12, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 0}, wh_period={FakeDatetime(2024, 4, 27, 6, 18, 15, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 0, FakeDatetime(2024, 4, 27, 6, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 5, FakeDatetime(2024, 4, 27, 7, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 35, FakeDatetime(2024, 4, 27, 7, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 61, FakeDatetime(2024, 4, 27, 8, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 92, FakeDatetime(2024, 4, 27, 8, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 124, FakeDatetime(2024, 4, 27, 9, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 155, FakeDatetime(2024, 4, 27, 9, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 186, FakeDatetime(2024, 4, 27, 10, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 215, FakeDatetime(2024, 4, 27, 10, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 240, FakeDatetime(2024, 4, 27, 11, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 262, FakeDatetime(2024, 4, 27, 11, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 283, FakeDatetime(2024, 4, 27, 12, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 301, FakeDatetime(2024, 4, 27, 12, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 310, FakeDatetime(2024, 4, 27, 13, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 315, FakeDatetime(2024, 4, 27, 13, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 319, FakeDatetime(2024, 4, 27, 14, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 319, FakeDatetime(2024, 4, 27, 14, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 314, FakeDatetime(2024, 4, 27, 15, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 305, FakeDatetime(2024, 4, 27, 15, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 293, FakeDatetime(2024, 4, 27, 16, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 277, FakeDatetime(2024, 4, 27, 16, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 259, FakeDatetime(2024, 4, 27, 17, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 240, FakeDatetime(2024, 4, 27, 17, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 215, FakeDatetime(2024, 4, 27, 18, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 187, FakeDatetime(2024, 4, 27, 18, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 156, FakeDatetime(2024, 4, 27, 19, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 124, FakeDatetime(2024, 4, 27, 19, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 91, FakeDatetime(2024, 4, 27, 20, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 58, FakeDatetime(2024, 4, 27, 20, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 32, FakeDatetime(2024, 4, 27, 21, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 15, FakeDatetime(2024, 4, 27, 21, 1, 7, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 0, FakeDatetime(2024, 4, 28, 6, 16, 16, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 0, FakeDatetime(2024, 4, 28, 6, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 5, FakeDatetime(2024, 4, 28, 7, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 33, FakeDatetime(2024, 4, 28, 7, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 59, FakeDatetime(2024, 4, 28, 8, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 89, FakeDatetime(2024, 4, 28, 8, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 120, FakeDatetime(2024, 4, 28, 9, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 151, FakeDatetime(2024, 4, 28, 9, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 182, FakeDatetime(2024, 4, 28, 10, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 221, FakeDatetime(2024, 4, 28, 10, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 281, FakeDatetime(2024, 4, 28, 11, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 354, FakeDatetime(2024, 4, 28, 11, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 400, FakeDatetime(2024, 4, 28, 12, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 406, FakeDatetime(2024, 4, 28, 12, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 388, FakeDatetime(2024, 4, 28, 13, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 374, FakeDatetime(2024, 4, 28, 13, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 383, FakeDatetime(2024, 4, 28, 14, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 406, FakeDatetime(2024, 4, 28, 14, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 426, FakeDatetime(2024, 4, 28, 15, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 430, FakeDatetime(2024, 4, 28, 15, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 415, FakeDatetime(2024, 4, 28, 16, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 392, FakeDatetime(2024, 4, 28, 16, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 376, FakeDatetime(2024, 4, 28, 17, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 357, FakeDatetime(2024, 4, 28, 17, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 320, FakeDatetime(2024, 4, 28, 18, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 274, FakeDatetime(2024, 4, 28, 18, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 227, FakeDatetime(2024, 4, 28, 19, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 179, FakeDatetime(2024, 4, 28, 19, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 128, FakeDatetime(2024, 4, 28, 20, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 76, FakeDatetime(2024, 4, 28, 20, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 38, FakeDatetime(2024, 4, 28, 21, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 17, FakeDatetime(2024, 4, 28, 21, 2, 49, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 0, FakeDatetime(2024, 4, 29, 6, 14, 18, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 0, FakeDatetime(2024, 4, 29, 6, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 5, FakeDatetime(2024, 4, 29, 7, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 34, FakeDatetime(2024, 4, 29, 7, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 69, FakeDatetime(2024, 4, 29, 8, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 119, FakeDatetime(2024, 4, 29, 8, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 179, FakeDatetime(2024, 4, 29, 9, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 249, FakeDatetime(2024, 4, 29, 9, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 316, FakeDatetime(2024, 4, 29, 10, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 358, FakeDatetime(2024, 4, 29, 10, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 357, FakeDatetime(2024, 4, 29, 11, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 350, FakeDatetime(2024, 4, 29, 11, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 381, FakeDatetime(2024, 4, 29, 12, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 431, FakeDatetime(2024, 4, 29, 12, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 461, FakeDatetime(2024, 4, 29, 13, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 464, FakeDatetime(2024, 4, 29, 13, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 443, FakeDatetime(2024, 4, 29, 14, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 406, FakeDatetime(2024, 4, 29, 14, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 363, FakeDatetime(2024, 4, 29, 15, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 328, FakeDatetime(2024, 4, 29, 15, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 303, FakeDatetime(2024, 4, 29, 16, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 282, FakeDatetime(2024, 4, 29, 16, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 261, FakeDatetime(2024, 4, 29, 17, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 238, FakeDatetime(2024, 4, 29, 17, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 213, FakeDatetime(2024, 4, 29, 18, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 184, FakeDatetime(2024, 4, 29, 18, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 154, FakeDatetime(2024, 4, 29, 19, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 121, FakeDatetime(2024, 4, 29, 19, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 87, FakeDatetime(2024, 4, 29, 20, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 55, FakeDatetime(2024, 4, 29, 20, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 30, FakeDatetime(2024, 4, 29, 21, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 14, FakeDatetime(2024, 4, 29, 21, 4, 31, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 1, FakeDatetime(2024, 4, 30, 6, 12, 21, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 0, FakeDatetime(2024, 4, 30, 6, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 6, FakeDatetime(2024, 4, 30, 7, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 31, FakeDatetime(2024, 4, 30, 7, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 56, FakeDatetime(2024, 4, 30, 8, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 86, FakeDatetime(2024, 4, 30, 8, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 117, FakeDatetime(2024, 4, 30, 9, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 149, FakeDatetime(2024, 4, 30, 9, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 180, FakeDatetime(2024, 4, 30, 10, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 209, FakeDatetime(2024, 4, 30, 10, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 234, FakeDatetime(2024, 4, 30, 11, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 255, FakeDatetime(2024, 4, 30, 11, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 274, FakeDatetime(2024, 4, 30, 12, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 290, FakeDatetime(2024, 4, 30, 12, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 301, FakeDatetime(2024, 4, 30, 13, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 309, FakeDatetime(2024, 4, 30, 13, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 312, FakeDatetime(2024, 4, 30, 14, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 312, FakeDatetime(2024, 4, 30, 14, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 310, FakeDatetime(2024, 4, 30, 15, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 304, FakeDatetime(2024, 4, 30, 15, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 292, FakeDatetime(2024, 4, 30, 16, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 276, FakeDatetime(2024, 4, 30, 16, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 258, FakeDatetime(2024, 4, 30, 17, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 237, FakeDatetime(2024, 4, 30, 17, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 213, FakeDatetime(2024, 4, 30, 18, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 186, FakeDatetime(2024, 4, 30, 18, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 155, FakeDatetime(2024, 4, 30, 19, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 122, FakeDatetime(2024, 4, 30, 19, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 87, FakeDatetime(2024, 4, 30, 20, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 54, FakeDatetime(2024, 4, 30, 20, 30, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 28, FakeDatetime(2024, 4, 30, 21, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 13, FakeDatetime(2024, 4, 30, 21, 6, 12, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))): 1}, wh_days={FakeDatetime(2024, 4, 27, 0, 0): 5788, FakeDatetime(2024, 4, 28, 0, 0): 7507, FakeDatetime(2024, 4, 29, 0, 0): 7256, FakeDatetime(2024, 4, 30, 0, 0): 5657}, api_rate_limit=60, api_timezone='Europe/Amsterdam') -# --- diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index 589a68f..0000000 --- a/tests/conftest.py +++ /dev/null @@ -1,49 +0,0 @@ -"""Fixtures for the Forecast.Solar tests.""" - -from collections.abc import AsyncGenerator - -import pytest -from aiohttp import ClientSession - -from forecast_solar import ForecastSolar - - -@pytest.fixture(name="forecast_client") -async def client() -> AsyncGenerator[ForecastSolar, None]: - """Return a Forecast.Solar client.""" - async with ( - ClientSession() as session, - ForecastSolar( - latitude=52.16, - longitude=4.47, - declination=20, - azimuth=10, - kwp=2.160, - damping=0, - horizon="0,0,0,10,10,20,20,30,30", - session=session, - ) as forecast_client, - ): - yield forecast_client - - -@pytest.fixture(name="forecast_key_client") -async def client_api_key() -> AsyncGenerator[ForecastSolar, None]: - """Return a Forecast.Solar client.""" - async with ( - ClientSession() as session, - ForecastSolar( - api_key="myapikey", - latitude=52.16, - longitude=4.47, - declination=20, - azimuth=10, - kwp=2.160, - damping_morning=0, - damping_evening=0, - horizon="0,0,0,10,10,20,20,30,30", - inverter=1.300, - session=session, - ) as forecast_key_client, - ): - yield forecast_key_client diff --git a/tests/fixtures/forecast.json b/tests/fixtures/forecast.json deleted file mode 100644 index a3d069e..0000000 --- a/tests/fixtures/forecast.json +++ /dev/null @@ -1,134 +0,0 @@ -{ - "result": { - "watts": { - "2024-04-26T06:20:17+02:00": 0, - "2024-04-26T07:00:00+02:00": 270, - "2024-04-26T08:00:00+02:00": 481, - "2024-04-26T09:00:00+02:00": 594, - "2024-04-26T10:00:00+02:00": 781, - "2024-04-26T11:00:00+02:00": 869, - "2024-04-26T12:00:00+02:00": 773, - "2024-04-26T13:00:00+02:00": 710, - "2024-04-26T14:00:00+02:00": 620, - "2024-04-26T15:00:00+02:00": 517, - "2024-04-26T16:00:00+02:00": 430, - "2024-04-26T17:00:00+02:00": 325, - "2024-04-26T18:00:00+02:00": 204, - "2024-04-26T19:00:00+02:00": 89, - "2024-04-26T20:00:00+02:00": 38, - "2024-04-26T20:59:23+02:00": 0, - "2024-04-27T06:18:17+02:00": 0, - "2024-04-27T07:00:00+02:00": 194, - "2024-04-27T08:00:00+02:00": 325, - "2024-04-27T09:00:00+02:00": 432, - "2024-04-27T10:00:00+02:00": 514, - "2024-04-27T11:00:00+02:00": 572, - "2024-04-27T12:00:00+02:00": 604, - "2024-04-27T13:00:00+02:00": 602, - "2024-04-27T14:00:00+02:00": 563, - "2024-04-27T15:00:00+02:00": 502, - "2024-04-27T16:00:00+02:00": 422, - "2024-04-27T17:00:00+02:00": 320, - "2024-04-27T18:00:00+02:00": 201, - "2024-04-27T19:00:00+02:00": 87, - "2024-04-27T20:00:00+02:00": 22, - "2024-04-27T21:00:00+02:00": 9, - "2024-04-27T21:01:05+02:00": 0 - }, - "watt_hours_period": { - "2024-04-26T06:20:17+02:00": 0, - "2024-04-26T07:00:00+02:00": 89, - "2024-04-26T08:00:00+02:00": 376, - "2024-04-26T09:00:00+02:00": 538, - "2024-04-26T10:00:00+02:00": 688, - "2024-04-26T11:00:00+02:00": 825, - "2024-04-26T12:00:00+02:00": 821, - "2024-04-26T13:00:00+02:00": 742, - "2024-04-26T14:00:00+02:00": 665, - "2024-04-26T15:00:00+02:00": 569, - "2024-04-26T16:00:00+02:00": 474, - "2024-04-26T17:00:00+02:00": 378, - "2024-04-26T18:00:00+02:00": 265, - "2024-04-26T19:00:00+02:00": 147, - "2024-04-26T20:00:00+02:00": 64, - "2024-04-26T20:59:23+02:00": 19, - "2024-04-27T06:18:17+02:00": 0, - "2024-04-27T07:00:00+02:00": 67, - "2024-04-27T08:00:00+02:00": 260, - "2024-04-27T09:00:00+02:00": 379, - "2024-04-27T10:00:00+02:00": 473, - "2024-04-27T11:00:00+02:00": 543, - "2024-04-27T12:00:00+02:00": 588, - "2024-04-27T13:00:00+02:00": 603, - "2024-04-27T14:00:00+02:00": 583, - "2024-04-27T15:00:00+02:00": 533, - "2024-04-27T16:00:00+02:00": 462, - "2024-04-27T17:00:00+02:00": 371, - "2024-04-27T18:00:00+02:00": 261, - "2024-04-27T19:00:00+02:00": 144, - "2024-04-27T20:00:00+02:00": 55, - "2024-04-27T21:00:00+02:00": 16, - "2024-04-27T21:01:05+02:00": 0 - }, - "watt_hours": { - "2024-04-26T06:20:17+02:00": 0, - "2024-04-26T07:00:00+02:00": 89, - "2024-04-26T08:00:00+02:00": 465, - "2024-04-26T09:00:00+02:00": 1003, - "2024-04-26T10:00:00+02:00": 1691, - "2024-04-26T11:00:00+02:00": 2516, - "2024-04-26T12:00:00+02:00": 3337, - "2024-04-26T13:00:00+02:00": 4079, - "2024-04-26T14:00:00+02:00": 4744, - "2024-04-26T15:00:00+02:00": 5313, - "2024-04-26T16:00:00+02:00": 5787, - "2024-04-26T17:00:00+02:00": 6165, - "2024-04-26T18:00:00+02:00": 6430, - "2024-04-26T19:00:00+02:00": 6577, - "2024-04-26T20:00:00+02:00": 6641, - "2024-04-26T20:59:23+02:00": 6660, - "2024-04-27T06:18:17+02:00": 0, - "2024-04-27T07:00:00+02:00": 67, - "2024-04-27T08:00:00+02:00": 327, - "2024-04-27T09:00:00+02:00": 706, - "2024-04-27T10:00:00+02:00": 1179, - "2024-04-27T11:00:00+02:00": 1722, - "2024-04-27T12:00:00+02:00": 2310, - "2024-04-27T13:00:00+02:00": 2913, - "2024-04-27T14:00:00+02:00": 3496, - "2024-04-27T15:00:00+02:00": 4029, - "2024-04-27T16:00:00+02:00": 4491, - "2024-04-27T17:00:00+02:00": 4862, - "2024-04-27T18:00:00+02:00": 5123, - "2024-04-27T19:00:00+02:00": 5267, - "2024-04-27T20:00:00+02:00": 5322, - "2024-04-27T21:00:00+02:00": 5338, - "2024-04-27T21:01:05+02:00": 5338 - }, - "watt_hours_day": { - "2024-04-26": 6660, - "2024-04-27": 5338 - } - }, - "message": { - "code": 0, - "type": "success", - "text": "", - "pid": "D8aH09A5", - "info": { - "latitude": 52.16, - "longitude": 4.47, - "distance": 0, - "place": "34, Vondellaan, Lage Mors, Leiden, Zuid-Holland, Nederland, 2332 AE, Nederland", - "timezone": "Europe/Amsterdam", - "time": "2024-04-26T18:56:01+02:00", - "time_utc": "2024-04-26T16:56:01+00:00" - }, - "ratelimit": { - "zone": "IP ADDRESS", - "period": 3600, - "limit": 12, - "remaining": 11 - } - } -} diff --git a/tests/fixtures/forecast_personal.json b/tests/fixtures/forecast_personal.json deleted file mode 100644 index 1e67e67..0000000 --- a/tests/fixtures/forecast_personal.json +++ /dev/null @@ -1,421 +0,0 @@ -{ - "result": { - "watts": { - "2024-04-27T06:18:15+02:00": 0, - "2024-04-27T06:30:00+02:00": 47, - "2024-04-27T07:00:00+02:00": 92, - "2024-04-27T07:30:00+02:00": 153, - "2024-04-27T08:00:00+02:00": 215, - "2024-04-27T08:30:00+02:00": 279, - "2024-04-27T09:00:00+02:00": 342, - "2024-04-27T09:30:00+02:00": 402, - "2024-04-27T10:00:00+02:00": 457, - "2024-04-27T10:30:00+02:00": 501, - "2024-04-27T11:00:00+02:00": 545, - "2024-04-27T11:30:00+02:00": 587, - "2024-04-27T12:00:00+02:00": 616, - "2024-04-27T12:30:00+02:00": 625, - "2024-04-27T13:00:00+02:00": 635, - "2024-04-27T13:30:00+02:00": 639, - "2024-04-27T14:00:00+02:00": 635, - "2024-04-27T14:30:00+02:00": 619, - "2024-04-27T15:00:00+02:00": 600, - "2024-04-27T15:30:00+02:00": 570, - "2024-04-27T16:00:00+02:00": 536, - "2024-04-27T16:30:00+02:00": 500, - "2024-04-27T17:00:00+02:00": 458, - "2024-04-27T17:30:00+02:00": 403, - "2024-04-27T18:00:00+02:00": 344, - "2024-04-27T18:30:00+02:00": 281, - "2024-04-27T19:00:00+02:00": 215, - "2024-04-27T19:30:00+02:00": 147, - "2024-04-27T20:00:00+02:00": 85, - "2024-04-27T20:30:00+02:00": 42, - "2024-04-27T21:00:00+02:00": 18, - "2024-04-27T21:01:07+02:00": 0, - "2024-04-28T06:16:16+02:00": 0, - "2024-04-28T06:30:00+02:00": 45, - "2024-04-28T07:00:00+02:00": 88, - "2024-04-28T07:30:00+02:00": 147, - "2024-04-28T08:00:00+02:00": 208, - "2024-04-28T08:30:00+02:00": 270, - "2024-04-28T09:00:00+02:00": 332, - "2024-04-28T09:30:00+02:00": 396, - "2024-04-28T10:00:00+02:00": 487, - "2024-04-28T10:30:00+02:00": 636, - "2024-04-28T11:00:00+02:00": 778, - "2024-04-28T11:30:00+02:00": 822, - "2024-04-28T12:00:00+02:00": 801, - "2024-04-28T12:30:00+02:00": 751, - "2024-04-28T13:00:00+02:00": 746, - "2024-04-28T13:30:00+02:00": 787, - "2024-04-28T14:00:00+02:00": 837, - "2024-04-28T14:30:00+02:00": 865, - "2024-04-28T15:00:00+02:00": 855, - "2024-04-28T15:30:00+02:00": 804, - "2024-04-28T16:00:00+02:00": 765, - "2024-04-28T16:30:00+02:00": 739, - "2024-04-28T17:00:00+02:00": 687, - "2024-04-28T17:30:00+02:00": 594, - "2024-04-28T18:00:00+02:00": 500, - "2024-04-28T18:30:00+02:00": 408, - "2024-04-28T19:00:00+02:00": 309, - "2024-04-28T19:30:00+02:00": 201, - "2024-04-28T20:00:00+02:00": 103, - "2024-04-28T20:30:00+02:00": 47, - "2024-04-28T21:00:00+02:00": 19, - "2024-04-28T21:02:49+02:00": 0, - "2024-04-29T06:14:18+02:00": 0, - "2024-04-29T06:30:00+02:00": 41, - "2024-04-29T07:00:00+02:00": 93, - "2024-04-29T07:30:00+02:00": 184, - "2024-04-29T08:00:00+02:00": 291, - "2024-04-29T08:30:00+02:00": 426, - "2024-04-29T09:00:00+02:00": 569, - "2024-04-29T09:30:00+02:00": 695, - "2024-04-29T10:00:00+02:00": 735, - "2024-04-29T10:30:00+02:00": 691, - "2024-04-29T11:00:00+02:00": 707, - "2024-04-29T11:30:00+02:00": 815, - "2024-04-29T12:00:00+02:00": 910, - "2024-04-29T12:30:00+02:00": 935, - "2024-04-29T13:00:00+02:00": 919, - "2024-04-29T13:30:00+02:00": 854, - "2024-04-29T14:00:00+02:00": 768, - "2024-04-29T14:30:00+02:00": 683, - "2024-04-29T15:00:00+02:00": 628, - "2024-04-29T15:30:00+02:00": 585, - "2024-04-29T16:00:00+02:00": 544, - "2024-04-29T16:30:00+02:00": 499, - "2024-04-29T17:00:00+02:00": 452, - "2024-04-29T17:30:00+02:00": 398, - "2024-04-29T18:00:00+02:00": 339, - "2024-04-29T18:30:00+02:00": 275, - "2024-04-29T19:00:00+02:00": 208, - "2024-04-29T19:30:00+02:00": 140, - "2024-04-29T20:00:00+02:00": 79, - "2024-04-29T20:30:00+02:00": 39, - "2024-04-29T21:00:00+02:00": 16, - "2024-04-29T21:04:31+02:00": 0, - "2024-04-30T06:12:21+02:00": 0, - "2024-04-30T06:30:00+02:00": 41, - "2024-04-30T07:00:00+02:00": 82, - "2024-04-30T07:30:00+02:00": 141, - "2024-04-30T08:00:00+02:00": 203, - "2024-04-30T08:30:00+02:00": 266, - "2024-04-30T09:00:00+02:00": 329, - "2024-04-30T09:30:00+02:00": 390, - "2024-04-30T10:00:00+02:00": 445, - "2024-04-30T10:30:00+02:00": 490, - "2024-04-30T11:00:00+02:00": 531, - "2024-04-30T11:30:00+02:00": 566, - "2024-04-30T12:00:00+02:00": 594, - "2024-04-30T12:30:00+02:00": 611, - "2024-04-30T13:00:00+02:00": 623, - "2024-04-30T13:30:00+02:00": 625, - "2024-04-30T14:00:00+02:00": 623, - "2024-04-30T14:30:00+02:00": 615, - "2024-04-30T15:00:00+02:00": 599, - "2024-04-30T15:30:00+02:00": 570, - "2024-04-30T16:00:00+02:00": 535, - "2024-04-30T16:30:00+02:00": 496, - "2024-04-30T17:00:00+02:00": 452, - "2024-04-30T17:30:00+02:00": 401, - "2024-04-30T18:00:00+02:00": 343, - "2024-04-30T18:30:00+02:00": 278, - "2024-04-30T19:00:00+02:00": 209, - "2024-04-30T19:30:00+02:00": 138, - "2024-04-30T20:00:00+02:00": 76, - "2024-04-30T20:30:00+02:00": 37, - "2024-04-30T21:00:00+02:00": 15, - "2024-04-30T21:06:12+02:00": 0 - }, - "watt_hours_period": { - "2024-04-27T06:18:15+02:00": 0, - "2024-04-27T06:30:00+02:00": 5, - "2024-04-27T07:00:00+02:00": 35, - "2024-04-27T07:30:00+02:00": 61, - "2024-04-27T08:00:00+02:00": 92, - "2024-04-27T08:30:00+02:00": 124, - "2024-04-27T09:00:00+02:00": 155, - "2024-04-27T09:30:00+02:00": 186, - "2024-04-27T10:00:00+02:00": 215, - "2024-04-27T10:30:00+02:00": 240, - "2024-04-27T11:00:00+02:00": 262, - "2024-04-27T11:30:00+02:00": 283, - "2024-04-27T12:00:00+02:00": 301, - "2024-04-27T12:30:00+02:00": 310, - "2024-04-27T13:00:00+02:00": 315, - "2024-04-27T13:30:00+02:00": 319, - "2024-04-27T14:00:00+02:00": 319, - "2024-04-27T14:30:00+02:00": 314, - "2024-04-27T15:00:00+02:00": 305, - "2024-04-27T15:30:00+02:00": 293, - "2024-04-27T16:00:00+02:00": 277, - "2024-04-27T16:30:00+02:00": 259, - "2024-04-27T17:00:00+02:00": 240, - "2024-04-27T17:30:00+02:00": 215, - "2024-04-27T18:00:00+02:00": 187, - "2024-04-27T18:30:00+02:00": 156, - "2024-04-27T19:00:00+02:00": 124, - "2024-04-27T19:30:00+02:00": 91, - "2024-04-27T20:00:00+02:00": 58, - "2024-04-27T20:30:00+02:00": 32, - "2024-04-27T21:00:00+02:00": 15, - "2024-04-27T21:01:07+02:00": 0, - "2024-04-28T06:16:16+02:00": 0, - "2024-04-28T06:30:00+02:00": 5, - "2024-04-28T07:00:00+02:00": 33, - "2024-04-28T07:30:00+02:00": 59, - "2024-04-28T08:00:00+02:00": 89, - "2024-04-28T08:30:00+02:00": 120, - "2024-04-28T09:00:00+02:00": 151, - "2024-04-28T09:30:00+02:00": 182, - "2024-04-28T10:00:00+02:00": 221, - "2024-04-28T10:30:00+02:00": 281, - "2024-04-28T11:00:00+02:00": 354, - "2024-04-28T11:30:00+02:00": 400, - "2024-04-28T12:00:00+02:00": 406, - "2024-04-28T12:30:00+02:00": 388, - "2024-04-28T13:00:00+02:00": 374, - "2024-04-28T13:30:00+02:00": 383, - "2024-04-28T14:00:00+02:00": 406, - "2024-04-28T14:30:00+02:00": 426, - "2024-04-28T15:00:00+02:00": 430, - "2024-04-28T15:30:00+02:00": 415, - "2024-04-28T16:00:00+02:00": 392, - "2024-04-28T16:30:00+02:00": 376, - "2024-04-28T17:00:00+02:00": 357, - "2024-04-28T17:30:00+02:00": 320, - "2024-04-28T18:00:00+02:00": 274, - "2024-04-28T18:30:00+02:00": 227, - "2024-04-28T19:00:00+02:00": 179, - "2024-04-28T19:30:00+02:00": 128, - "2024-04-28T20:00:00+02:00": 76, - "2024-04-28T20:30:00+02:00": 38, - "2024-04-28T21:00:00+02:00": 17, - "2024-04-28T21:02:49+02:00": 0, - "2024-04-29T06:14:18+02:00": 0, - "2024-04-29T06:30:00+02:00": 5, - "2024-04-29T07:00:00+02:00": 34, - "2024-04-29T07:30:00+02:00": 69, - "2024-04-29T08:00:00+02:00": 119, - "2024-04-29T08:30:00+02:00": 179, - "2024-04-29T09:00:00+02:00": 249, - "2024-04-29T09:30:00+02:00": 316, - "2024-04-29T10:00:00+02:00": 358, - "2024-04-29T10:30:00+02:00": 357, - "2024-04-29T11:00:00+02:00": 350, - "2024-04-29T11:30:00+02:00": 381, - "2024-04-29T12:00:00+02:00": 431, - "2024-04-29T12:30:00+02:00": 461, - "2024-04-29T13:00:00+02:00": 464, - "2024-04-29T13:30:00+02:00": 443, - "2024-04-29T14:00:00+02:00": 406, - "2024-04-29T14:30:00+02:00": 363, - "2024-04-29T15:00:00+02:00": 328, - "2024-04-29T15:30:00+02:00": 303, - "2024-04-29T16:00:00+02:00": 282, - "2024-04-29T16:30:00+02:00": 261, - "2024-04-29T17:00:00+02:00": 238, - "2024-04-29T17:30:00+02:00": 213, - "2024-04-29T18:00:00+02:00": 184, - "2024-04-29T18:30:00+02:00": 154, - "2024-04-29T19:00:00+02:00": 121, - "2024-04-29T19:30:00+02:00": 87, - "2024-04-29T20:00:00+02:00": 55, - "2024-04-29T20:30:00+02:00": 30, - "2024-04-29T21:00:00+02:00": 14, - "2024-04-29T21:04:31+02:00": 1, - "2024-04-30T06:12:21+02:00": 0, - "2024-04-30T06:30:00+02:00": 6, - "2024-04-30T07:00:00+02:00": 31, - "2024-04-30T07:30:00+02:00": 56, - "2024-04-30T08:00:00+02:00": 86, - "2024-04-30T08:30:00+02:00": 117, - "2024-04-30T09:00:00+02:00": 149, - "2024-04-30T09:30:00+02:00": 180, - "2024-04-30T10:00:00+02:00": 209, - "2024-04-30T10:30:00+02:00": 234, - "2024-04-30T11:00:00+02:00": 255, - "2024-04-30T11:30:00+02:00": 274, - "2024-04-30T12:00:00+02:00": 290, - "2024-04-30T12:30:00+02:00": 301, - "2024-04-30T13:00:00+02:00": 309, - "2024-04-30T13:30:00+02:00": 312, - "2024-04-30T14:00:00+02:00": 312, - "2024-04-30T14:30:00+02:00": 310, - "2024-04-30T15:00:00+02:00": 304, - "2024-04-30T15:30:00+02:00": 292, - "2024-04-30T16:00:00+02:00": 276, - "2024-04-30T16:30:00+02:00": 258, - "2024-04-30T17:00:00+02:00": 237, - "2024-04-30T17:30:00+02:00": 213, - "2024-04-30T18:00:00+02:00": 186, - "2024-04-30T18:30:00+02:00": 155, - "2024-04-30T19:00:00+02:00": 122, - "2024-04-30T19:30:00+02:00": 87, - "2024-04-30T20:00:00+02:00": 54, - "2024-04-30T20:30:00+02:00": 28, - "2024-04-30T21:00:00+02:00": 13, - "2024-04-30T21:06:12+02:00": 1 - }, - "watt_hours": { - "2024-04-27T06:18:15+02:00": 0, - "2024-04-27T06:30:00+02:00": 5, - "2024-04-27T07:00:00+02:00": 40, - "2024-04-27T07:30:00+02:00": 101, - "2024-04-27T08:00:00+02:00": 193, - "2024-04-27T08:30:00+02:00": 317, - "2024-04-27T09:00:00+02:00": 472, - "2024-04-27T09:30:00+02:00": 658, - "2024-04-27T10:00:00+02:00": 873, - "2024-04-27T10:30:00+02:00": 1113, - "2024-04-27T11:00:00+02:00": 1375, - "2024-04-27T11:30:00+02:00": 1658, - "2024-04-27T12:00:00+02:00": 1959, - "2024-04-27T12:30:00+02:00": 2269, - "2024-04-27T13:00:00+02:00": 2584, - "2024-04-27T13:30:00+02:00": 2903, - "2024-04-27T14:00:00+02:00": 3222, - "2024-04-27T14:30:00+02:00": 3536, - "2024-04-27T15:00:00+02:00": 3841, - "2024-04-27T15:30:00+02:00": 4134, - "2024-04-27T16:00:00+02:00": 4411, - "2024-04-27T16:30:00+02:00": 4670, - "2024-04-27T17:00:00+02:00": 4910, - "2024-04-27T17:30:00+02:00": 5125, - "2024-04-27T18:00:00+02:00": 5312, - "2024-04-27T18:30:00+02:00": 5468, - "2024-04-27T19:00:00+02:00": 5592, - "2024-04-27T19:30:00+02:00": 5683, - "2024-04-27T20:00:00+02:00": 5741, - "2024-04-27T20:30:00+02:00": 5773, - "2024-04-27T21:00:00+02:00": 5788, - "2024-04-27T21:01:07+02:00": 5788, - "2024-04-28T06:16:16+02:00": 0, - "2024-04-28T06:30:00+02:00": 5, - "2024-04-28T07:00:00+02:00": 38, - "2024-04-28T07:30:00+02:00": 97, - "2024-04-28T08:00:00+02:00": 186, - "2024-04-28T08:30:00+02:00": 306, - "2024-04-28T09:00:00+02:00": 457, - "2024-04-28T09:30:00+02:00": 639, - "2024-04-28T10:00:00+02:00": 860, - "2024-04-28T10:30:00+02:00": 1141, - "2024-04-28T11:00:00+02:00": 1495, - "2024-04-28T11:30:00+02:00": 1895, - "2024-04-28T12:00:00+02:00": 2301, - "2024-04-28T12:30:00+02:00": 2689, - "2024-04-28T13:00:00+02:00": 3063, - "2024-04-28T13:30:00+02:00": 3446, - "2024-04-28T14:00:00+02:00": 3852, - "2024-04-28T14:30:00+02:00": 4278, - "2024-04-28T15:00:00+02:00": 4708, - "2024-04-28T15:30:00+02:00": 5123, - "2024-04-28T16:00:00+02:00": 5515, - "2024-04-28T16:30:00+02:00": 5891, - "2024-04-28T17:00:00+02:00": 6248, - "2024-04-28T17:30:00+02:00": 6568, - "2024-04-28T18:00:00+02:00": 6842, - "2024-04-28T18:30:00+02:00": 7069, - "2024-04-28T19:00:00+02:00": 7248, - "2024-04-28T19:30:00+02:00": 7376, - "2024-04-28T20:00:00+02:00": 7452, - "2024-04-28T20:30:00+02:00": 7490, - "2024-04-28T21:00:00+02:00": 7507, - "2024-04-28T21:02:49+02:00": 7507, - "2024-04-29T06:14:18+02:00": 0, - "2024-04-29T06:30:00+02:00": 5, - "2024-04-29T07:00:00+02:00": 39, - "2024-04-29T07:30:00+02:00": 108, - "2024-04-29T08:00:00+02:00": 227, - "2024-04-29T08:30:00+02:00": 406, - "2024-04-29T09:00:00+02:00": 655, - "2024-04-29T09:30:00+02:00": 971, - "2024-04-29T10:00:00+02:00": 1329, - "2024-04-29T10:30:00+02:00": 1686, - "2024-04-29T11:00:00+02:00": 2036, - "2024-04-29T11:30:00+02:00": 2417, - "2024-04-29T12:00:00+02:00": 2848, - "2024-04-29T12:30:00+02:00": 3309, - "2024-04-29T13:00:00+02:00": 3773, - "2024-04-29T13:30:00+02:00": 4216, - "2024-04-29T14:00:00+02:00": 4622, - "2024-04-29T14:30:00+02:00": 4985, - "2024-04-29T15:00:00+02:00": 5313, - "2024-04-29T15:30:00+02:00": 5616, - "2024-04-29T16:00:00+02:00": 5898, - "2024-04-29T16:30:00+02:00": 6159, - "2024-04-29T17:00:00+02:00": 6397, - "2024-04-29T17:30:00+02:00": 6610, - "2024-04-29T18:00:00+02:00": 6794, - "2024-04-29T18:30:00+02:00": 6948, - "2024-04-29T19:00:00+02:00": 7069, - "2024-04-29T19:30:00+02:00": 7156, - "2024-04-29T20:00:00+02:00": 7211, - "2024-04-29T20:30:00+02:00": 7241, - "2024-04-29T21:00:00+02:00": 7255, - "2024-04-29T21:04:31+02:00": 7256, - "2024-04-30T06:12:21+02:00": 0, - "2024-04-30T06:30:00+02:00": 6, - "2024-04-30T07:00:00+02:00": 37, - "2024-04-30T07:30:00+02:00": 93, - "2024-04-30T08:00:00+02:00": 179, - "2024-04-30T08:30:00+02:00": 296, - "2024-04-30T09:00:00+02:00": 445, - "2024-04-30T09:30:00+02:00": 625, - "2024-04-30T10:00:00+02:00": 834, - "2024-04-30T10:30:00+02:00": 1068, - "2024-04-30T11:00:00+02:00": 1323, - "2024-04-30T11:30:00+02:00": 1597, - "2024-04-30T12:00:00+02:00": 1887, - "2024-04-30T12:30:00+02:00": 2188, - "2024-04-30T13:00:00+02:00": 2497, - "2024-04-30T13:30:00+02:00": 2809, - "2024-04-30T14:00:00+02:00": 3121, - "2024-04-30T14:30:00+02:00": 3431, - "2024-04-30T15:00:00+02:00": 3735, - "2024-04-30T15:30:00+02:00": 4027, - "2024-04-30T16:00:00+02:00": 4303, - "2024-04-30T16:30:00+02:00": 4561, - "2024-04-30T17:00:00+02:00": 4798, - "2024-04-30T17:30:00+02:00": 5011, - "2024-04-30T18:00:00+02:00": 5197, - "2024-04-30T18:30:00+02:00": 5352, - "2024-04-30T19:00:00+02:00": 5474, - "2024-04-30T19:30:00+02:00": 5561, - "2024-04-30T20:00:00+02:00": 5615, - "2024-04-30T20:30:00+02:00": 5643, - "2024-04-30T21:00:00+02:00": 5656, - "2024-04-30T21:06:12+02:00": 5657 - }, - "watt_hours_day": { - "2024-04-27": 5788, - "2024-04-28": 7507, - "2024-04-29": 7256, - "2024-04-30": 5657 - } - }, - "message": { - "code": 0, - "type": "success", - "text": "", - "pid": "wXG62901", - "info": { - "latitude": 52.17, - "longitude": 4.47, - "distance": 0, - "place": "Pedologisch Instituut De Brug, Pomonapad, 2333 VE Leiden, Netherlands", - "timezone": "Europe/Amsterdam", - "time": "2024-04-27T04:48:01+02:00", - "time_utc": "2024-04-27T02:48:01+00:00" - }, - "ratelimit": { - "zone": "API key", - "period": 3600, - "limit": 60, - "remaining": 57 - } - } -} diff --git a/tests/fixtures/ratelimit.json b/tests/fixtures/ratelimit.json deleted file mode 100644 index da415d5..0000000 --- a/tests/fixtures/ratelimit.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "result": "Rate limit for API calls reached.", - "message": { - "code": 429, - "type": "error", - "text": "Rate limit for API calls reached.", - "pid": "0F5m6R53", - "ratelimit": { - "zone": "YOUR IP ADDRESS", - "period": 3600, - "limit": 12, - "retry-at": "2024-04-27T02:48:53+02:00" - } - } -} diff --git a/tests/fixtures/validate_key.json b/tests/fixtures/validate_key.json deleted file mode 100644 index 9f2f9bf..0000000 --- a/tests/fixtures/validate_key.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "result": { - "paypal": "paypalid", - "email": "email@example.com", - "name": "John Doe", - "subscription": "subscriptionid", - "level": 1, - "account": "Personal", - "until": "2024-07-11", - "created": "2021-06-27 15:14:45" - }, - "message": { - "code": 0, - "type": "success", - "text": "", - "pid": "m0UZ756I", - "ratelimit": { - "zone": "API key myapikey", - "period": 3600, - "limit": 60, - "remaining": 57 - } - } -} diff --git a/tests/fixtures/validate_plane.json b/tests/fixtures/validate_plane.json deleted file mode 100644 index b7c7602..0000000 --- a/tests/fixtures/validate_plane.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "result": { - "latitude": "52ยฐ 09' 36\" N", - "longitude": "04ยฐ 28' 12\" E", - "declination": "20ยฐ", - "azimuth": "10ยฐ", - "power": "2.160 kWp", - "place": "34, Vondellaan, Lage Mors, Leiden, Zuid-Holland, Nederland, 2332 AE, Nederland", - "timezone": "Europe/Amsterdam" - }, - "message": { - "code": 0, - "type": "success", - "text": "", - "pid": "w40kJP6l", - "info": { - "latitude": 52.16, - "longitude": 4.47, - "distance": 0, - "place": "34, Vondellaan, Lage Mors, Leiden, Zuid-Holland, Nederland, 2332 AE, Nederland", - "timezone": "Europe/Amsterdam", - "time": "2024-04-27T01:48:53+02:00", - "time_utc": "2024-04-26T23:48:53+00:00" - } - } -} diff --git a/tests/ruff.toml b/tests/ruff.toml deleted file mode 100644 index 1005d3e..0000000 --- a/tests/ruff.toml +++ /dev/null @@ -1,12 +0,0 @@ -# This extend our general Ruff rules specifically for tests -extend = "../pyproject.toml" - -lint.extend-select = [ - "PT", # Use @pytest.fixture without parentheses -] - -lint.extend-ignore = [ - "S101", # Use of assert detected. As these are tests... - "SLF001", # Tests will access private/protected members... - "TCH002", # pytest doesn't like this one... -] diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py deleted file mode 100644 index 728ecd8..0000000 --- a/tests/test_exceptions.py +++ /dev/null @@ -1,130 +0,0 @@ -"""Text exceptions raised by the Forecast.Solar API client.""" - -import pytest -from aresponses import ResponsesMockServer - -from forecast_solar import ( - ForecastSolar, - ForecastSolarAuthenticationError, - ForecastSolarConfigError, - ForecastSolarConnectionError, - ForecastSolarRatelimitError, - ForecastSolarRequestError, -) - -from . import load_fixtures - - -async def test_status_400( - aresponses: ResponsesMockServer, - forecast_client: ForecastSolar, -) -> None: - """Test response status 400.""" - aresponses.add( - "api.forecast.solar", - "/test", - "GET", - aresponses.Response( - status=400, - headers={ - "Content-Type": "application/json", - "X-Ratelimit-Limit": "10", - "X-Ratelimit-Period": "1", - }, - text=load_fixtures("forecast.json"), - ), - ) - with pytest.raises(ForecastSolarRequestError): - assert await forecast_client._request("test") - - -async def test_status_401( - aresponses: ResponsesMockServer, - forecast_client: ForecastSolar, -) -> None: - """Test response status 401 or 403.""" - aresponses.add( - "api.forecast.solar", - "/test", - "GET", - aresponses.Response( - status=401, - headers={ - "Content-Type": "application/json", - "X-Ratelimit-Limit": "10", - "X-Ratelimit-Period": "1", - }, - text=load_fixtures("forecast.json"), - ), - ) - with pytest.raises(ForecastSolarAuthenticationError): - assert await forecast_client._request("test") - - -async def test_status_422( - aresponses: ResponsesMockServer, - forecast_client: ForecastSolar, -) -> None: - """Test response status 422.""" - aresponses.add( - "api.forecast.solar", - "/test", - "GET", - aresponses.Response( - status=422, - headers={ - "Content-Type": "application/json", - "X-Ratelimit-Limit": "10", - "X-Ratelimit-Period": "1", - }, - text=load_fixtures("forecast.json"), - ), - ) - with pytest.raises(ForecastSolarConfigError): - assert await forecast_client._request("test") - - -async def test_status_429( - aresponses: ResponsesMockServer, - forecast_client: ForecastSolar, -) -> None: - """Test response status 429.""" - aresponses.add( - "api.forecast.solar", - "/test", - "GET", - aresponses.Response( - status=429, - headers={ - "Content-Type": "application/json", - "X-Ratelimit-Limit": "10", - "X-Ratelimit-Period": "1", - }, - text=load_fixtures("ratelimit.json"), - ), - ) - with pytest.raises(ForecastSolarRatelimitError): - assert await forecast_client._request("test") - - -async def test_status_502( - aresponses: ResponsesMockServer, - forecast_client: ForecastSolar, -) -> None: - """Test response status 502 or 503.""" - aresponses.add( - "api.forecast.solar", - "/test", - "GET", - aresponses.Response( - status=502, - headers={ - "Content-Type": "application/json", - "X-Ratelimit-Limit": "10", - "X-Ratelimit-Period": "1", - }, - text=load_fixtures("forecast.json"), - ), - ) - with pytest.raises(ForecastSolarConnectionError): - assert await forecast_client._request("test") diff --git a/tests/test_forecast.py b/tests/test_forecast.py deleted file mode 100644 index 8913679..0000000 --- a/tests/test_forecast.py +++ /dev/null @@ -1,87 +0,0 @@ -"""Tests for Forecast.Solar.""" - -# pylint: disable=protected-access - -import pytest -from aresponses import ResponsesMockServer - -from forecast_solar import ( - ForecastSolar, - ForecastSolarError, -) - -from . import load_fixtures - - -async def test_json_request( - aresponses: ResponsesMockServer, - forecast_client: ForecastSolar, -) -> None: - """Test JSON response is handled correctly.""" - aresponses.add( - "api.forecast.solar", - "/test", - "GET", - aresponses.Response( - status=200, - headers={ - "Content-Type": "application/json", - "X-Ratelimit-Limit": "10", - "X-Ratelimit-Period": "1", - }, - text=load_fixtures("forecast.json"), - ), - ) - response = await forecast_client._request("test") - assert response is not None - await forecast_client.close() - - -async def test_internal_session(aresponses: ResponsesMockServer) -> None: - """Test internal session is handled correctly.""" - aresponses.add( - "api.forecast.solar", - "/test", - "GET", - aresponses.Response( - status=200, - headers={ - "Content-Type": "application/json", - "X-Ratelimit-Limit": "10", - "X-Ratelimit-Period": "1", - }, - text=load_fixtures("forecast.json"), - ), - ) - async with ForecastSolar( - latitude=52.16, - longitude=4.47, - declination=20, - azimuth=10, - kwp=2.160, - damping=0, - horizon="0,0,0,10,10,20,20,30,30", - ) as client: - await client._request("test") - - -async def test_content_type( - aresponses: ResponsesMockServer, - forecast_client: ForecastSolar, -) -> None: - """Test content type error handling.""" - aresponses.add( - "api.forecast.solar", - "/test", - "GET", - aresponses.Response( - status=200, - headers={ - "Content-Type": "blabla/blabla", - "X-Ratelimit-Limit": "10", - "X-Ratelimit-Period": "1", - }, - ), - ) - with pytest.raises(ForecastSolarError): - assert await forecast_client._request("test") diff --git a/tests/test_models.py b/tests/test_models.py deleted file mode 100644 index 7eb837b..0000000 --- a/tests/test_models.py +++ /dev/null @@ -1,147 +0,0 @@ -"""Test the models.""" - -from datetime import datetime - -import pytest -from aresponses import ResponsesMockServer -from syrupy.assertion import SnapshotAssertion - -from forecast_solar import AccountType, Estimate, ForecastSolar - -from . import load_fixtures - - -@pytest.mark.freeze_time("2024-04-26T12:00:00+02:00") -async def test_estimated_forecast( - aresponses: ResponsesMockServer, - snapshot: SnapshotAssertion, - forecast_client: ForecastSolar, -) -> None: - """Test estimated forecast.""" - aresponses.add( - "api.forecast.solar", - "/estimate/52.16/4.47/20/10/2.16", - "GET", - aresponses.Response( - status=200, - headers={ - "Content-Type": "application/json", - "X-Ratelimit-Limit": "10", - "X-Ratelimit-Period": "1", - }, - text=load_fixtures("forecast.json"), - ), - ) - forecast: Estimate = await forecast_client.estimate() - assert forecast == snapshot - assert forecast.timezone == "Europe/Amsterdam" - assert forecast.account_type == AccountType.PUBLIC - - assert forecast.energy_production_today == 6660 - assert forecast.energy_production_tomorrow == 5338 - - assert forecast.power_production_now == 773 - assert forecast.energy_production_today_remaining == 4144 - assert forecast.energy_current_hour == 821 - - assert forecast.power_highest_peak_time_today == datetime.fromisoformat( - "2024-04-26T11:00:00+02:00" - ) - assert forecast.power_highest_peak_time_tomorrow == datetime.fromisoformat( - "2024-04-27T12:00:00+02:00" - ) - - assert forecast.sum_energy_production(1) == 742 - assert forecast.sum_energy_production(6) == 3093 - assert forecast.sum_energy_production(12) == 3323 - assert forecast.sum_energy_production(24) == 5633 - - -@pytest.mark.freeze_time("2024-04-27T07:00:00+02:00") -async def test_estimated_forecast_with_subscription( - aresponses: ResponsesMockServer, - snapshot: SnapshotAssertion, - forecast_key_client: ForecastSolar, -) -> None: - """Test estimated forecast.""" - aresponses.add( - "api.forecast.solar", - "/myapikey/estimate/52.16/4.47/20/10/2.16", - "GET", - aresponses.Response( - status=200, - headers={ - "Content-Type": "application/json", - "X-Ratelimit-Limit": "60", - "X-Ratelimit-Period": "3600", - }, - text=load_fixtures("forecast_personal.json"), - ), - ) - forecast: Estimate = await forecast_key_client.estimate() - assert forecast == snapshot - assert forecast.timezone == "Europe/Amsterdam" - assert forecast.account_type == AccountType.PERSONAL - - assert forecast.energy_production_today == 5788 - assert forecast.energy_production_tomorrow == 7507 - - assert forecast.power_production_now == 92 - assert forecast.energy_production_today_remaining == 5783 - assert forecast.energy_current_hour == 96 - - assert forecast.power_highest_peak_time_today == datetime.fromisoformat( - "2024-04-27T13:30:00+02:00" - ) - assert forecast.power_highest_peak_time_tomorrow == datetime.fromisoformat( - "2024-04-28T14:30:00+02:00" - ) - - assert forecast.sum_energy_production(1) == 216 - assert forecast.sum_energy_production(6) == 2802 - assert forecast.sum_energy_production(12) == 5582 - assert forecast.sum_energy_production(24) == 5784 - - -async def test_api_key_validation( - aresponses: ResponsesMockServer, - forecast_key_client: ForecastSolar, -) -> None: - """Test API key validation.""" - aresponses.add( - "api.forecast.solar", - "/myapikey/info", - "GET", - aresponses.Response( - status=200, - headers={ - "Content-Type": "application/json", - "X-Ratelimit-Limit": "10", - "X-Ratelimit-Period": "1", - }, - text=load_fixtures("validate_key.json"), - ), - ) - assert await forecast_key_client.validate_api_key() is True - - -async def test_plane_validation( - aresponses: ResponsesMockServer, - forecast_client: ForecastSolar, -) -> None: - """Test plane validation.""" - aresponses.add( - "api.forecast.solar", - "/check/52.16/4.47/20/10/2.16", - "GET", - aresponses.Response( - status=200, - headers={ - "Content-Type": "application/json", - "X-Ratelimit-Limit": "10", - "X-Ratelimit-Period": "1", - }, - text=load_fixtures("validate_plane.json"), - ), - ) - assert await forecast_client.validate_plane() is True