pax_global_header00006660000000000000000000000064150265237020014514gustar00rootroot0000000000000052 comment=e60e7e5e667596f93df25f6fe95cbb678330e8c2 pyairnow-1.3.1/000077500000000000000000000000001502652370200133665ustar00rootroot00000000000000pyairnow-1.3.1/.github/000077500000000000000000000000001502652370200147265ustar00rootroot00000000000000pyairnow-1.3.1/.github/workflows/000077500000000000000000000000001502652370200167635ustar00rootroot00000000000000pyairnow-1.3.1/.github/workflows/ci.yaml000066400000000000000000000031771502652370200202520ustar00rootroot00000000000000--- name: CI on: pull_request: branches: - master push: branches: - master jobs: test: name: Tests runs-on: ubuntu-latest strategy: matrix: python-version: - "3.9" - "3.10" - "3.11" - "3.12" - "3.13" steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v1 with: python-version: ${{ matrix.python-version }} architecture: x64 - run: | python -m venv .venv .venv/bin/pip install -r requirements-dev.txt .venv/bin/python -m pytest tests/ coverage: name: Coverage runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v1 with: python-version: "3.13" architecture: x64 - run: | python -m venv .venv .venv/bin/pip install -r requirements-dev.txt .venv/bin/python -m pytest --cov-report=xml --cov=pyairnow tests/ - uses: codecov/codecov-action@v1 with: token: ${{ secrets.CODECOV_TOKEN }} lint: name: Linter runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v1 with: python-version: "3.13" architecture: x64 - run: | python -m venv .venv .venv/bin/pip install -r requirements-dev.txt .venv/bin/flake8 --ignore .venv --count --select=E9,F63,F7,F82 --show-source --statistics .venv/bin/flake8 --ignore .venv --count --exit-zero --max-complexity=12 --max-line-length=127 --statistics pyairnow-1.3.1/.github/workflows/publish.yaml000066400000000000000000000021411502652370200213130ustar00rootroot00000000000000# This workflows will upload a Python Package using Twine when a release is created # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries name: Publish Python Package on: release: types: [created] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: '3.13' - name: Install Poetry run: | curl -sSL https://install.python-poetry.org | python3 - - name: Install Dependencies run: $HOME/.poetry/bin/poetry install - name: Build and Publish env: POETRY_PYPI_TOKEN_PYPI: ${{ secrets.POETRY_PYPI_TOKEN_PYPI }} POETRY_HTTP_BASIC_PYPI_USERNAME: ${{ secrets.POETRY_PYPI_USERNAME }} POETRY_HTTP_BASIC_PYPI_PASSWORD: ${{ secrets.POETRY_PYPI_PASSWORD }} run: | $HOME/.poetry/bin/poetry build $HOME/.poetry/bin/poetry publish -u ${{ secrets.POETRY_PYPI_USERNAME }} -p ${{ secrets.POETRY_PYPI_PASSWORD }} pyairnow-1.3.1/.gitignore000066400000000000000000000034631502652370200153640ustar00rootroot00000000000000# Coverage .htmlcov/ # Poetry poetry.lock # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ pyairnow-1.3.1/AUTHORS.md000066400000000000000000000001331502652370200150320ustar00rootroot00000000000000# Contributions to `pyairnow` ## Owners - Jonathan Krauss (https://github.com/asymworks) pyairnow-1.3.1/CHANGELOG.md000066400000000000000000000027001502652370200151760ustar00rootroot00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] None ## [1.3.1] - 2025-06-24 - Improve type hinting (#16, from @natepugh) ## [1.3.0] - 2025-06-22 - Update conv.py to accommodate revised EPA guidance from May 2024. (#11, from @natepugh) - Update supported Python to 3.9-3.13 ## [1.2.2] - 2024-08-11 - Add py.typed marker (PEP-561) (#9) - Remove setuptools, wheel from build-system (#7) ## [1.2.1] - 2023-02-20 ### Changed - Update dependencies to support Python 3.11 to match Home Assistant CI ## [1.2.0] - 2023-02-20 ### Changed - Update supported Python to 3.8-3.10 - Update project to use `poetry-core` (from @fabaff) - Update to force HTTPS since AirNow requires HTTPS as of Feb 3rd 2023 (from @stephenjamieson) ## [1.1.0] - 2020-09-13 ### Added - Added conversion helpers between AQI and Pollutant Concentration ## [1.0.1] - 2020-09-13 ### Changed - Fixed GitHub Publish workflow [unreleased]: https://github.com/asymworks/pyairnow/compare/v1.2.1...HEAD [1.2.1]: https://github.com/asymworks/pyairnow/compare/v1.2.0...v1.2.1 [1.2.0]: https://github.com/asymworks/pyairnow/compare/v1.1.0...v1.2.0 [1.1.0]: https://github.com/asymworks/pyairnow/compare/v1.0.1...v1.1.0 [1.0.1]: https://github.com/asymworks/pyairnow/releases/tag/v1.0.1 pyairnow-1.3.1/LICENSE000066400000000000000000000020571502652370200143770ustar00rootroot00000000000000MIT License Copyright (c) 2020 Asymworks, LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 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. pyairnow-1.3.1/Makefile000066400000000000000000000012361502652370200150300ustar00rootroot00000000000000 build : poetry build clean : $(RM) -rf build dist *.egg-info coverage : poetry run coverage run --source=pyairnow -m pytest tests coverage-html : poetry run coverage html -d .htmlcov && open .htmlcov/index.html coverage-report : poetry run coverage report -m lint : poetry run flake8 publish : poetry publish requirements : poetry export -f requirements.txt -o requirements-dev.txt --with dev test : poetry run python -m pytest tests test-x : poetry run python -m pytest tests -x test-wip : poetry run python -m pytest tests -m wip all: test lint build publish .PHONY: build \ coverage coverage-html coverage-report \ lint test test-wip test-x pyairnow-1.3.1/README.md000066400000000000000000000113461502652370200146520ustar00rootroot00000000000000# pyairnow: a thin Python wrapper for the AirNow API [![CI](https://github.com/asymworks/pyairnow/workflows/CI/badge.svg)](https://github.com/asymworks/pyairnow/actions) [![PyPi](https://img.shields.io/pypi/v/pyairnow.svg)](https://pypi.python.org/pypi/pyairnow) [![Version](https://img.shields.io/pypi/pyversions/pyairnow.svg)](https://pypi.python.org/pypi/pyairnow) [![License](https://img.shields.io/pypi/l/pyairnow.svg)](https://github.com/asymworks/pyairnow/blob/master/LICENSE) [![Code Coverage](https://codecov.io/gh/asymworks/pyairnow/branch/master/graph/badge.svg)](https://codecov.io/gh/asymworks/pyairnow) `pyairnow` is a simple, tested, thin client library for interacting with the [AirNow](https://www.airnow.gov) United States EPA Air Quality Index API. - [Python Versions](#python-versions) - [Installation](#installation) - [API Key](#api-key) - [Usage](#usage) - [Contributing](#contributing) # Python Versions `pyairnow` is currently supported and tested on: * Python 3.9 * Python 3.10 * Python 3.11 * Python 3.12 * Python 3.13 # Installation ```python pip install pyairnow ``` # API Key You can get an AirNow API key from [the AirNow API site](https://docs.airnowapi.org/account/request/). Ensure you read and understand the expectations and limitations of API usage, which can be found at [the AirNow FAQ](https://docs.airnowapi.org/faq). # Usage ```python import asyncio import datetime from pyairnow import WebServiceAPI async def main() -> None: client = WebServiceAPI('your-api-key') # Get current observational data based on a zip code data = await client.observations.zipCode( '90001', # if there are no observation stations in this zip code, optionally # provide a radius to search (in miles) distance=50, ) # Get current observational data based on a latitude and longitude data = await client.observations.latLong( 34.053718, -118.244842, # if there are no observation stations at this location, optionally # provide a radius to search (in miles) distance=50, ) # Get forecast data based on a zip code data = await client.forecast.zipCode( '90001', # to get a forecast for a certain day, provide a date in yyyy-mm-dd, # if not specified the current day will be used date='2020-09-01', # if there are no observation stations in this zip code, optionally # provide a radius to search (in miles) distance=50, ) # Get forecast data based on a latitude and longitude data = await client.forecast.latLong( # Lat/Long may be strings or floats '34.053718', '-118.244842', # forecast dates may also be datetime.date or datetime.datetime objects date=datetime.date(2020, 9, 1), # if there are no observation stations in this zip code, optionally # provide a radius to search (in miles) distance=50, ) asyncio.run(main()) ``` By default, the library creates a new connection to AirNow with each coroutine. If you are calling a large number of coroutines (or merely want to squeeze out every second of runtime savings possible), an [`aiohttp`](https://github.com/aio-libs/aiohttp) `ClientSession` can be used for connection pooling: ```python import asyncio import datetime from aiohttp import ClientSession from pyairnow import WebServiceAPI async def main() -> None: async with ClientSession() as session: client = WebServiceAPI('your-api-key', session=session) # ... asyncio.run(main()) ``` The library provides two convenience functions to convert between AQI and pollutant concentrations. See [this EPA document](https://document.airnow.gov/technical-assistance-document-for-the-reporting-of-daily-air-quailty.pdf) for more details. ```python from pyairnow.conv import aqi_to_concentration, concentration_to_aqi # Supported Pollutants # -------------------- # Ozone ('O3'): ppm # pm2.5 ('PM2.5'): ug/m^3 # pm10 ('PM10'): ug/m^3 # Carbon Monoxide ('CO'): ppm # Sulfur Dioxide ('SO2'): ppm # Nitrogen Dioxide ('NO2'): ppm # Returns AQI = 144 for pm2.5 of 53.0 ug/m^3 aqi_to_concentration(144, 'PM2.5') # Returns Cp = 53.0 ug/m^3 concentration_to_aqi(53.0, 'PM2.5') ``` # Contributing 1. [Check for open features/bugs](https://github.com/asymworks/pyairnow/issues) or [start a discussion on one](https://github.com/asymworks/pyairnow/issues/new). 2. [Fork the repository](https://github.com/asymworks/pyairnow/fork). 3. Install [Poetry](https://python-poetry.org/) and set up the development workspace: `poetry install` 4. Code your new feature or bug fix. 5. Write tests that cover your new functionality. 6. Run tests and ensure 100% code coverage: `make test` 7. Run the linter to ensure 100% code style correctness: `make lint` 8. Update `README.md` with any new documentation. 9. Add yourself to `AUTHORS.md`. 10. Submit a pull request! pyairnow-1.3.1/examples/000077500000000000000000000000001502652370200152045ustar00rootroot00000000000000pyairnow-1.3.1/examples/__init__.py000066400000000000000000000000751502652370200173170ustar00rootroot00000000000000'''pyAirNow Examples (run with python -m examples.module)''' pyairnow-1.3.1/examples/cli.py000066400000000000000000000074601502652370200163340ustar00rootroot00000000000000'''Test Script/CLI Interface to pyairnow''' import argparse import asyncio from pyairnow import WebServiceAPI def optionalArgs(args): '''Load optional keyword arguments (date and dict)''' kwargs = dict() if args.date is not None: kwargs['date'] = args.date if args.distance is not None: kwargs['distance'] = args.distance return kwargs def llArgs(args): '''Load latitude and longitude from the location argument''' try: lat, long = args.split(',') except ValueError: raise ValueError( 'Invalid location string, expected ,' ) return [float(lat.strip()), float(long.strip())] async def get_forecast_zip(args: argparse.Namespace) -> None: '''Print forecast information for a zip code''' client = WebServiceAPI(args.api_key) data = await client.forecast.zipCode(args.zipcode, **optionalArgs(args)) print(data) async def get_forecast_ll(args: argparse.Namespace) -> None: '''Print forecast information for a latitude and longitude''' client = WebServiceAPI(args.api_key) data = await client.forecast.latLong(*llArgs(args), **optionalArgs(args)) print(data) async def get_current_zip(args: argparse.Namespace) -> None: '''Print current observation information for a zip code''' client = WebServiceAPI(args.api_key) data = await client.observations.zipCode( args.zipcode, **optionalArgs(args), ) print(data) async def get_current_ll(args: argparse.Namespace) -> None: '''Print current observation information for a latitude and longitude''' client = WebServiceAPI(args.api_key) data = await client.observations.latLong( *llArgs(args), **optionalArgs(args), ) print(data) if __name__ == '__main__': parser = argparse.ArgumentParser( description='CLI Client for pyAirNow, which obtains current and ' 'forecasted air quality information from AirNow. The ' 'returned JSON data is printed to stdout.', epilog='To obtain an API key, visit https://docs.airnowapi.org/login', ) parser.add_argument( '-k', '--api-key', required=True, help='AirNow API Key (required)', ) location_group = parser.add_mutually_exclusive_group(required=True) location_group.add_argument( '-z', '--zipcode', help='Zip Code for Observations or Forecast. May not be used with ' '--location.', ) location_group.add_argument( '-l', '--location', help='Latitude and Longitude for Observations or Forecast (must be ' 'formatted as "," with an optional space ' 'before the longitude value. May not be used with --zipcode.' ) parser.add_argument( '-r', '--distance', help='If no reporting area is associated with the zip code or ' 'latitude and longitude, return data from a station within this ' 'distance (in miles)' ) parser.add_argument( '-d', '--date', help='Desired forecast date. If omitted, the current day forecast is ' 'returned. Format as yyyy-mm-dd' ) parser.add_argument( 'type', choices=['forecast', 'observations'], help='Look up either the forecast or the current observations for the ' 'given location' ) args = parser.parse_args() loop = asyncio.get_event_loop() if args.type == 'forecast': if args.zipcode is not None: loop.run_until_complete(get_forecast_zip(args)) else: loop.run_until_complete(get_forecast_ll(args)) else: if args.zipcode is not None: loop.run_until_complete(get_current_zip(args)) else: loop.run_until_complete(get_current_ll(args)) pyairnow-1.3.1/pyairnow/000077500000000000000000000000001502652370200152365ustar00rootroot00000000000000pyairnow-1.3.1/pyairnow/__init__.py000066400000000000000000000000751502652370200173510ustar00rootroot00000000000000'''Public Objects''' from .api import WebServiceAPI # noqa pyairnow-1.3.1/pyairnow/api.py000066400000000000000000000061701502652370200163650ustar00rootroot00000000000000'''Client to interact with AirNow Air Quality API''' from json.decoder import JSONDecodeError from typing import Optional, cast from aiohttp import ClientSession, ClientTimeout from .errors import AirNowError, EmptyResponseError, InvalidJsonError, \ InvalidKeyError from .forecast import Forecast from .observation import Observations API_BASE_URL: str = 'https://www.airnowapi.org' API_DEFAULT_TIMEOUT: int = 10 class WebServiceAPI: '''Client to interact with AirNow API''' def __init__( self, api_key: str, *, session: Optional[ClientSession] = None ) -> None: '''Initialize with Client Session and API Key''' self._api_key: Optional[str] = api_key self._session: Optional[ClientSession] = session self.forecast = Forecast(self._get) self.observations = Observations(self._get) async def _get( self, endpoint: str, *, base_url: str = API_BASE_URL, **kwargs ) -> list: '''Run a Request against the API''' kwargs.setdefault('params', {}) kwargs['params']['API_KEY'] = self._api_key kwargs['params']['format'] = 'application/json' use_running_session = self._session and not self._session.closed session: ClientSession if use_running_session: session = cast(ClientSession, self._session) else: session = ClientSession( timeout=ClientTimeout(total=API_DEFAULT_TIMEOUT) ) try: async with session.get( f'{base_url}/{endpoint}', **kwargs ) as resp: data = await resp.json(content_type=None) except JSONDecodeError: # The response can't be parsed as JSON, so we'll use its body text # in an error: response_text = await resp.text() raise InvalidJsonError(response_text) finally: if not use_running_session: await session.close() if isinstance(data, dict) and 'WebServiceError' in data: # Process an Error Message from the API server wsErr = data['WebServiceError'] if isinstance(wsErr, list) and len(wsErr) > 0: if 'Message' in wsErr[0]: if wsErr[0]['Message'] == 'Invalid API key': raise InvalidKeyError(wsErr[0]['Message']) elif wsErr[0]['Message'] == 'Request not authenticated': raise InvalidKeyError(wsErr[0]['Message']) else: raise AirNowError(wsErr[0]['Message']) else: raise AirNowError(str(wsErr[0])) else: raise AirNowError(str(wsErr)) elif not isinstance(data, list): # We should get a list of Observation or Forecast objects raise InvalidJsonError( 'Unexpected response type: %s' % (type(data)) ) elif len(data) == 0: # No objects were returned raise EmptyResponseError('No data was returned') # Return list of objects for further processing return data pyairnow-1.3.1/pyairnow/conv.py000066400000000000000000000117521502652370200165630ustar00rootroot00000000000000'''Convert AQI to and from Pollutant Concentrations''' from typing import TypedDict class ConcBreakpointPair(TypedDict): bL: float bH: float class EPATableRow(TypedDict): iL: int iH: int breakpoints: dict[str, ConcBreakpointPair] EPA_TABLE: list[EPATableRow] = [ { 'iL': 0, 'iH': 50, 'breakpoints': { 'O3': { 'bL': 0.000, 'bH': 0.054 }, 'PM2.5': { 'bL': 0.0, 'bH': 9.0 }, 'PM10': { 'bL': 0, 'bH': 54 }, 'CO': { 'bL': 0.0, 'bH': 4.4 }, 'SO2': { 'bL': 0, 'bH': 35 }, 'NO2': { 'bL': 0, 'bH': 53 }, }, }, { 'iL': 51, 'iH': 100, 'breakpoints': { 'O3': { 'bL': 0.055, 'bH': 0.070 }, 'PM2.5': { 'bL': 9.1, 'bH': 35.4 }, 'PM10': { 'bL': 55, 'bH': 154 }, 'CO': { 'bL': 4.5, 'bH': 9.4 }, 'SO2': { 'bL': 36, 'bH': 75 }, 'NO2': { 'bL': 54, 'bH': 100 }, }, }, { 'iL': 101, 'iH': 150, 'breakpoints': { 'O3': { 'bL': 0.071, 'bH': 0.085 }, 'PM2.5': { 'bL': 35.5, 'bH': 55.4 }, 'PM10': { 'bL': 155, 'bH': 254 }, 'CO': { 'bL': 9.5, 'bH': 12.4 }, 'SO2': { 'bL': 76, 'bH': 185 }, 'NO2': { 'bL': 101, 'bH': 360 }, }, }, { 'iL': 151, 'iH': 200, 'breakpoints': { 'O3': { 'bL': 0.086, 'bH': 0.105 }, 'PM2.5': { 'bL': 55.5, 'bH': 125.4 }, 'PM10': { 'bL': 255, 'bH': 354 }, 'CO': { 'bL': 12.5, 'bH': 15.4 }, 'SO2': { 'bL': 186, 'bH': 304 }, 'NO2': { 'bL': 361, 'bH': 649 }, }, }, { 'iL': 201, 'iH': 300, 'breakpoints': { 'O3': { 'bL': 0.106, 'bH': 0.200 }, 'PM2.5': { 'bL': 125.5, 'bH': 225.4 }, 'PM10': { 'bL': 355, 'bH': 424 }, 'CO': { 'bL': 15.5, 'bH': 30.4 }, 'SO2': { 'bL': 305, 'bH': 604 }, 'NO2': { 'bL': 650, 'bH': 1249 }, }, }, { 'iL': 301, 'iH': 500, 'breakpoints': { # Ozone here changes from 8h to 1h reporting 'O3': { 'bL': 0.201, 'bH': 0.604 }, 'PM2.5': { 'bL': 225.5, 'bH': 325.4 }, 'PM10': { 'bL': 425, 'bH': 604 }, 'CO': { 'bL': 30.5, 'bH': 50.4 }, 'SO2': { 'bL': 605, 'bH': 1004 }, 'NO2': { 'bL': 1250, 'bH': 2049 }, }, }, ] EPA_ROUND_FNS = { 'O3': lambda c: round(c, 3), 'PM2.5': lambda c: round(c, 1), 'PM10': lambda c: int(round(c, 0)), 'CO': lambda c: round(c, 1), 'SO2': lambda c: int(round(c, 0)), 'NO2': lambda c: int(round(c, 0)), } def aqi_to_concentration(aqi: int, pollutant: str) -> float: '''Convert AQI (0-500) to Pollutant Concentration''' if aqi < 0: raise ValueError('AQI must be greater than 0') max_breakpoint_aqi = EPA_TABLE[-1]['iH'] for row in EPA_TABLE: iL = row['iL'] iH = row['iH'] # The min() comparison here selects the last breakpoint row when # AQI > 500. See the comment in `concentration_to_aqi`. if iL <= aqi and iH >= min(aqi, max_breakpoint_aqi): bL = row['breakpoints'][pollutant]['bL'] bH = row['breakpoints'][pollutant]['bH'] cp = float(aqi - iL) * (bH - bL) / (iH - iL) + bL return EPA_ROUND_FNS[pollutant](cp) raise RuntimeError('No matching row found in EPA_TABLE') def concentration_to_aqi(conc: float, pollutant: str) -> int: '''Convert Pollutant Concentration to AQI''' if conc < 0: raise ValueError('Concentration must be positive') rounded = EPA_ROUND_FNS[pollutant](conc) max_breakpoint_pollutant_conc = ( EPA_TABLE[-1]['breakpoints'][pollutant]['bH'] ) for row in EPA_TABLE: bL = row['breakpoints'][pollutant]['bL'] bH = row['breakpoints'][pollutant]['bH'] # As per EPA: # AQI values greater than 500 should be calculated using # [this algorithm] and the concentration specified for the # AQI value of 500. # The min() comparison here selects the last breakpoint row # in those cases. if bL <= rounded and bH >= min(rounded, max_breakpoint_pollutant_conc): iL = row['iL'] iH = row['iH'] return int(round(float(iH - iL) / (bH - bL) * (conc - bL) + iL, 0)) raise ValueError( f'Invalid concentration {rounded} for pollutant {pollutant}' ) pyairnow-1.3.1/pyairnow/errors.py000066400000000000000000000005261502652370200171270ustar00rootroot00000000000000'''AirNow API Error Classes''' class AirNowError(Exception): '''Base AirNow Error Class''' pass class EmptyResponseError(Exception): '''Empty Response Error''' pass class InvalidJsonError(Exception): '''Invalid JSON Data Error''' pass class InvalidKeyError(Exception): '''Invalid API Key Error''' pass pyairnow-1.3.1/pyairnow/forecast.py000066400000000000000000000041321502652370200174160ustar00rootroot00000000000000'''Retrieve Air Quality Forecasts''' from datetime import date as date_, datetime from typing import Callable, Coroutine, Optional, Union class Forecast: ''' Class to retrieve the air quality forecast by zip code or by latitude and longitude. ''' def __init__(self, request: Callable[..., Coroutine]) -> None: self._request = request async def zipCode( self, zipCode: str, *, date: Optional[Union[date_, datetime, str]] = None, distance: Optional[int] = None ) -> list: '''Request current observation for zip code''' params: dict = dict(zipCode=zipCode) if date and isinstance(date, str): y, m, d = date.split('-') params['date'] = date_(int(y), int(m), int(d)).isoformat() elif date and isinstance(date, datetime): params['date'] = date.date().isoformat() elif date and isinstance(date, date_): params['date'] = date.isoformat() if distance: params['distance'] = distance return await self._request( 'aq/forecast/zipCode', params=params ) async def latLong( self, latitude: Optional[Union[float, str]] = None, longitude: Optional[Union[float, str]] = None, *, date: Optional[Union[date_, datetime, str]] = None, distance: Optional[int] = None, ) -> None: '''Request current observation for latitude/longitude''' params: dict = dict( latitude=str(latitude), longitude=str(longitude), ) if date and isinstance(date, str): y, m, d = date.split('-') params['date'] = date_(int(y), int(m), int(d)).isoformat() elif date and isinstance(date, datetime): params['date'] = date.date().isoformat() elif date and isinstance(date, date_): params['date'] = date.isoformat() if distance: params['distance'] = distance return await self._request( 'aq/forecast/latLong', params=params ) pyairnow-1.3.1/pyairnow/observation.py000066400000000000000000000024541502652370200201500ustar00rootroot00000000000000'''Retrieve a list of Current Observations''' from typing import Callable, Coroutine, Optional, Union class Observations: ''' Class to retrieve the current air quality observations by zip code or by latitude and longitude. ''' def __init__(self, request: Callable[..., Coroutine]) -> None: self._request = request async def zipCode( self, zipCode: str, *, distance: Optional[int] = None ) -> list: '''Request current observation for zip code''' params: dict = dict(zipCode=zipCode) if distance: params['distance'] = distance return await self._request( 'aq/observation/zipCode/current', params=params ) async def latLong( self, latitude: Optional[Union[float, str]] = None, longitude: Optional[Union[float, str]] = None, *, distance: Optional[int] = None, ) -> list: '''Request current observation for latitude/longitude''' params: dict = dict( latitude=str(latitude), longitude=str(longitude), ) if distance: params['distance'] = distance return await self._request( 'aq/observation/latLong/current', params=params ) pyairnow-1.3.1/pyairnow/py.typed000066400000000000000000000000001502652370200167230ustar00rootroot00000000000000pyairnow-1.3.1/pyproject.toml000066400000000000000000000025651502652370200163120ustar00rootroot00000000000000[build-system] requires = ["poetry_core>=1.0.0"] build-backend = "poetry.core.masonry.api" [tool.poetry] name = "pyairnow" version = "1.3.1" description = "A lightweight Python wrapper for EPA AirNow Air Quality API" readme = "README.md" authors = ["Jonathan Krauss "] license = "MIT" repository = "https://github.com/asymworks/pyairnow" classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Topic :: Scientific/Engineering :: Atmospheric Science", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed", ] [tool.poetry.dependencies] aiohttp = "^3.10.0" python = "^3.9" [tool.poetry.group.dev.dependencies] aioresponses = "^0.7.8" coverage = "^5.5" flake8 = "^5.0.0" pytest = "^6.2.5" pytest-asyncio = "^0.14.0" pytest-cov = "^2.12.1" [tool.setuptools.package-data] "pyairnow" = ["py.typed"] [tool.setuptools.packages.find] where = ["pyairnow"] [tool.poetry.requires-plugins] poetry-plugin-export = ">=1.8" pyairnow-1.3.1/requirements-dev.txt000066400000000000000000001413341502652370200174340ustar00rootroot00000000000000aiohappyeyeballs==2.4.4 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:5fdd7d87889c63183afc18ce9271f9b0a7d32c2303e394468dd45d514a757745 \ --hash=sha256:a980909d50efcd44795c4afeca523296716d50cd756ddca6af8c65b996e27de8 aiohttp==3.10.11 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:0316e624b754dbbf8c872b62fe6dcb395ef20c70e59890dfa0de9eafccd2849d \ --hash=sha256:099fd126bf960f96d34a760e747a629c27fb3634da5d05c7ef4d35ef4ea519fc \ --hash=sha256:0acafb350cfb2eba70eb5d271f55e08bd4502ec35e964e18ad3e7d34d71f7261 \ --hash=sha256:0c5580f3c51eea91559db3facd45d72e7ec970b04528b4709b1f9c2555bd6d0b \ --hash=sha256:0f449a50cc33f0384f633894d8d3cd020e3ccef81879c6e6245c3c375c448625 \ --hash=sha256:14cdc8c1810bbd4b4b9f142eeee23cda528ae4e57ea0923551a9af4820980e39 \ --hash=sha256:1dc0f4ca54842173d03322793ebcf2c8cc2d34ae91cc762478e295d8e361e03f \ --hash=sha256:1e7b825da878464a252ccff2958838f9caa82f32a8dbc334eb9b34a026e2c636 \ --hash=sha256:20063c7acf1eec550c8eb098deb5ed9e1bb0521613b03bb93644b810986027ac \ --hash=sha256:20b3d9e416774d41813bc02fdc0663379c01817b0874b932b81c7f777f67b217 \ --hash=sha256:22b7c540c55909140f63ab4f54ec2c20d2635c0289cdd8006da46f3327f971b9 \ --hash=sha256:236b28ceb79532da85d59aa9b9bf873b364e27a0acb2ceaba475dc61cffb6f3f \ --hash=sha256:249c8ff8d26a8b41a0f12f9df804e7c685ca35a207e2410adbd3e924217b9006 \ --hash=sha256:25fd5470922091b5a9aeeb7e75be609e16b4fba81cdeaf12981393fb240dd10e \ --hash=sha256:29103f9099b6068bbdf44d6a3d090e0a0b2be6d3c9f16a070dd9d0d910ec08f9 \ --hash=sha256:2b943011b45ee6bf74b22245c6faab736363678e910504dd7531a58c76c9015a \ --hash=sha256:2c8f96e9ee19f04c4914e4e7a42a60861066d3e1abf05c726f38d9d0a466e695 \ --hash=sha256:2dfb612dcbe70fb7cdcf3499e8d483079b89749c857a8f6e80263b021745c730 \ --hash=sha256:2e4e18a0a2d03531edbc06c366954e40a3f8d2a88d2b936bbe78a0c75a3aab3e \ --hash=sha256:2ea224cf7bc2d8856d6971cea73b1d50c9c51d36971faf1abc169a0d5f85a382 \ --hash=sha256:30283f9d0ce420363c24c5c2421e71a738a2155f10adbb1a11a4d4d6d2715cfc \ --hash=sha256:38e3c4f80196b4f6c3a85d134a534a56f52da9cb8d8e7af1b79a32eefee73a00 \ --hash=sha256:3bf6d027d9d1d34e1c2e1645f18a6498c98d634f8e373395221121f1c258ace8 \ --hash=sha256:459f0f32c8356e8125f45eeff0ecf2b1cb6db1551304972702f34cd9e6c44658 \ --hash=sha256:473aebc3b871646e1940c05268d451f2543a1d209f47035b594b9d4e91ce8339 \ --hash=sha256:489cced07a4c11488f47aab1f00d0c572506883f877af100a38f1fedaa884c3a \ --hash=sha256:48bc1d924490f0d0b3658fe5c4b081a4d56ebb58af80a6729d4bd13ea569797a \ --hash=sha256:4996ff1345704ffdd6d75fb06ed175938c133425af616142e7187f28dc75f14e \ --hash=sha256:4e8d8aad9402d3aa02fdc5ca2fe68bcb9fdfe1f77b40b10410a94c7f408b664d \ --hash=sha256:5077b1a5f40ffa3ba1f40d537d3bec4383988ee51fbba6b74aa8fb1bc466599e \ --hash=sha256:5a5f7ab8baf13314e6b2485965cbacb94afff1e93466ac4d06a47a81c50f9cca \ --hash=sha256:5ab2328a61fdc86424ee540d0aeb8b73bbcad7351fb7cf7a6546fc0bcffa0038 \ --hash=sha256:5f0463bf8b0754bc744e1feb61590706823795041e63edf30118a6f0bf577461 \ --hash=sha256:686b03196976e327412a1b094f4120778c7c4b9cff9bce8d2fdfeca386b89829 \ --hash=sha256:6cd3f10b01f0c31481fba8d302b61603a2acb37b9d30e1d14e0f5a58b7b18a31 \ --hash=sha256:6ce66780fa1a20e45bc753cda2a149daa6dbf1561fc1289fa0c308391c7bc0a4 \ --hash=sha256:703938e22434d7d14ec22f9f310559331f455018389222eed132808cd8f44127 \ --hash=sha256:72b191cdf35a518bfc7ca87d770d30941decc5aaf897ec8b484eb5cc8c7706f3 \ --hash=sha256:7400a93d629a0608dc1d6c55f1e3d6e07f7375745aaa8bd7f085571e4d1cee97 \ --hash=sha256:7480519f70e32bfb101d71fb9a1f330fbd291655a4c1c922232a48c458c52710 \ --hash=sha256:74baf1a7d948b3d640badeac333af581a367ab916b37e44cf90a0334157cdfd2 \ --hash=sha256:778cbd01f18ff78b5dd23c77eb82987ee4ba23408cbed233009fd570dda7e674 \ --hash=sha256:7b26b1551e481012575dab8e3727b16fe7dd27eb2711d2e63ced7368756268fb \ --hash=sha256:7ce6a51469bfaacff146e59e7fb61c9c23006495d11cc24c514a455032bcfa03 \ --hash=sha256:80ff08556c7f59a7972b1e8919f62e9c069c33566a6d28586771711e0eea4f07 \ --hash=sha256:82052be3e6d9e0c123499127782a01a2b224b8af8c62ab46b3f6197035ad94e9 \ --hash=sha256:8663f7777ce775f0413324be0d96d9730959b2ca73d9b7e2c2c90539139cbdd6 \ --hash=sha256:878ca6a931ee8c486a8f7b432b65431d095c522cbeb34892bee5be97b3481d0f \ --hash=sha256:8d6a14a4d93b5b3c2891fca94fa9d41b2322a68194422bef0dd5ec1e57d7d298 \ --hash=sha256:9208299251370ee815473270c52cd3f7069ee9ed348d941d574d1457d2c73e8b \ --hash=sha256:968b8fb2a5eee2770eda9c7b5581587ef9b96fbdf8dcabc6b446d35ccc69df01 \ --hash=sha256:971aa438a29701d4b34e4943e91b5e984c3ae6ccbf80dd9efaffb01bd0b243a9 \ --hash=sha256:9a309c5de392dfe0f32ee57fa43ed8fc6ddf9985425e84bd51ed66bb16bce3a7 \ --hash=sha256:9bc50b63648840854e00084c2b43035a62e033cb9b06d8c22b409d56eb098413 \ --hash=sha256:9c6e0ffd52c929f985c7258f83185d17c76d4275ad22e90aa29f38e211aacbec \ --hash=sha256:9dc2b8f3dcab2e39e0fa309c8da50c3b55e6f34ab25f1a71d3288f24924d33a7 \ --hash=sha256:9ec1628180241d906a0840b38f162a3215114b14541f1a8711c368a8739a9be4 \ --hash=sha256:a919c8957695ea4c0e7a3e8d16494e3477b86f33067478f43106921c2fef15bb \ --hash=sha256:aa93063d4af05c49276cf14e419550a3f45258b6b9d1f16403e777f1addf4519 \ --hash=sha256:aad3cd91d484d065ede16f3cf15408254e2469e3f613b241a1db552c5eb7ab7d \ --hash=sha256:b3e70f24e7d0405be2348da9d5a7836936bf3a9b4fd210f8c37e8d48bc32eca6 \ --hash=sha256:b5e29706e6389a2283a91611c91bf24f218962717c8f3b4e528ef529d112ee27 \ --hash=sha256:bbde2ca67230923a42161b1f408c3992ae6e0be782dca0c44cb3206bf330dee1 \ --hash=sha256:bc6f1ab987a27b83c5268a17218463c2ec08dbb754195113867a27b166cd6087 \ --hash=sha256:bcaf2d79104d53d4dcf934f7ce76d3d155302d07dae24dff6c9fffd217568067 \ --hash=sha256:c13ed0c779911c7998a58e7848954bd4d63df3e3575f591e321b19a2aec8df9f \ --hash=sha256:c2f746a6968c54ab2186574e15c3f14f3e7f67aef12b761e043b33b89c5b5f95 \ --hash=sha256:c73c4d3dae0b4644bc21e3de546530531d6cdc88659cdeb6579cd627d3c206aa \ --hash=sha256:c891011e76041e6508cbfc469dd1a8ea09bc24e87e4c204e05f150c4c455a5fa \ --hash=sha256:ca117819d8ad113413016cb29774b3f6d99ad23c220069789fc050267b786c16 \ --hash=sha256:cdc493a2e5d8dc79b2df5bec9558425bcd39aff59fc949810cbd0832e294b106 \ --hash=sha256:d110cabad8360ffa0dec8f6ec60e43286e9d251e77db4763a87dcfe55b4adb92 \ --hash=sha256:d97187de3c276263db3564bb9d9fad9e15b51ea10a371ffa5947a5ba93ad6777 \ --hash=sha256:db9503f79e12d5d80b3efd4d01312853565c05367493379df76d2674af881caa \ --hash=sha256:deef4362af9493d1382ef86732ee2e4cbc0d7c005947bd54ad1a9a16dd59298e \ --hash=sha256:e0099c7d5d7afff4202a0c670e5b723f7718810000b4abcbc96b064129e64bc7 \ --hash=sha256:e12eb3f4b1f72aaaf6acd27d045753b18101524f72ae071ae1c91c1cd44ef115 \ --hash=sha256:e1ffa713d3ea7cdcd4aea9cddccab41edf6882fa9552940344c44e59652e1120 \ --hash=sha256:e5358addc8044ee49143c546d2182c15b4ac3a60be01c3209374ace05af5733d \ --hash=sha256:ea9b3bab329aeaa603ed3bf605f1e2a6f36496ad7e0e1aa42025f368ee2dc07b \ --hash=sha256:f14ebc419a568c2eff3c1ed35f634435c24ead2fe19c07426af41e7adb68713a \ --hash=sha256:f34b97e4b11b8d4eb2c3a4f975be626cc8af99ff479da7de49ac2c6d02d35725 \ --hash=sha256:f4df4b8ca97f658c880fb4b90b1d1ec528315d4030af1ec763247ebfd33d8b9a \ --hash=sha256:f65267266c9aeb2287a6622ee2bb39490292552f9fbf851baabc04c9f84e048d \ --hash=sha256:f6c6dec398ac5a87cb3a407b068e1106b20ef001c344e34154616183fe684288 \ --hash=sha256:f9b615d3da0d60e7d53c62e22b4fd1c70f4ae5993a44687b011ea3a2e49051b8 \ --hash=sha256:f9f92a344c50b9667827da308473005f34767b6a2a60d9acff56ae94f895f385 \ --hash=sha256:fb8601394d537da9221947b5d6e62b064c9a43e88a1ecd7414d21a1a6fba9c24 \ --hash=sha256:fc31820cfc3b2863c6e95e14fcf815dc7afe52480b4dc03393c4873bb5599f71 \ --hash=sha256:fdf6429f0caabfd8a30c4e2eaecb547b3c340e4730ebfe25139779b9815ba138 \ --hash=sha256:ffbfde2443696345e23a3c597049b1dd43049bb65337837574205e7368472177 aioresponses==0.7.8 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:b73bd4400d978855e55004b23a3a84cb0f018183bcf066a85ad392800b5b9a94 \ --hash=sha256:b861cdfe5dc58f3b8afac7b0a6973d5d7b2cb608dd0f6253d16b8ee8eaf6df11 aiosignal==1.3.1 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc \ --hash=sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17 async-timeout==5.0.1 ; python_version >= "3.9" and python_version < "3.11" \ --hash=sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c \ --hash=sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3 atomicwrites==1.4.1 ; python_version >= "3.9" and python_version < "4.0" and sys_platform == "win32" \ --hash=sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11 attrs==25.3.0 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3 \ --hash=sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b colorama==0.4.6 ; python_version >= "3.9" and python_version < "4.0" and sys_platform == "win32" \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 coverage==5.5 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c \ --hash=sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6 \ --hash=sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45 \ --hash=sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a \ --hash=sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03 \ --hash=sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529 \ --hash=sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a \ --hash=sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a \ --hash=sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2 \ --hash=sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6 \ --hash=sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759 \ --hash=sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53 \ --hash=sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a \ --hash=sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4 \ --hash=sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff \ --hash=sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502 \ --hash=sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793 \ --hash=sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb \ --hash=sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905 \ --hash=sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821 \ --hash=sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b \ --hash=sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81 \ --hash=sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0 \ --hash=sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b \ --hash=sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3 \ --hash=sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184 \ --hash=sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701 \ --hash=sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a \ --hash=sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82 \ --hash=sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638 \ --hash=sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5 \ --hash=sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083 \ --hash=sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6 \ --hash=sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90 \ --hash=sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465 \ --hash=sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a \ --hash=sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3 \ --hash=sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e \ --hash=sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066 \ --hash=sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf \ --hash=sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b \ --hash=sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae \ --hash=sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669 \ --hash=sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873 \ --hash=sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b \ --hash=sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6 \ --hash=sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb \ --hash=sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160 \ --hash=sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c \ --hash=sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079 \ --hash=sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d \ --hash=sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6 flake8==5.0.4 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db \ --hash=sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248 frozenlist==1.5.0 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e \ --hash=sha256:03d33c2ddbc1816237a67f66336616416e2bbb6beb306e5f890f2eb22b959cdf \ --hash=sha256:04a5c6babd5e8fb7d3c871dc8b321166b80e41b637c31a995ed844a6139942b6 \ --hash=sha256:0996c66760924da6e88922756d99b47512a71cfd45215f3570bf1e0b694c206a \ --hash=sha256:0cc974cc93d32c42e7b0f6cf242a6bd941c57c61b618e78b6c0a96cb72788c1d \ --hash=sha256:0f253985bb515ecd89629db13cb58d702035ecd8cfbca7d7a7e29a0e6d39af5f \ --hash=sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28 \ --hash=sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b \ --hash=sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9 \ --hash=sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2 \ --hash=sha256:15538c0cbf0e4fa11d1e3a71f823524b0c46299aed6e10ebb4c2089abd8c3bec \ --hash=sha256:15b731db116ab3aedec558573c1a5eec78822b32292fe4f2f0345b7f697745c2 \ --hash=sha256:17dcc32fc7bda7ce5875435003220a457bcfa34ab7924a49a1c19f55b6ee185c \ --hash=sha256:1893f948bf6681733aaccf36c5232c231e3b5166d607c5fa77773611df6dc336 \ --hash=sha256:189f03b53e64144f90990d29a27ec4f7997d91ed3d01b51fa39d2dbe77540fd4 \ --hash=sha256:1a8ea951bbb6cacd492e3948b8da8c502a3f814f5d20935aae74b5df2b19cf3d \ --hash=sha256:1b96af8c582b94d381a1c1f51ffaedeb77c821c690ea5f01da3d70a487dd0a9b \ --hash=sha256:1e76bfbc72353269c44e0bc2cfe171900fbf7f722ad74c9a7b638052afe6a00c \ --hash=sha256:2150cc6305a2c2ab33299453e2968611dacb970d2283a14955923062c8d00b10 \ --hash=sha256:226d72559fa19babe2ccd920273e767c96a49b9d3d38badd7c91a0fdeda8ea08 \ --hash=sha256:237f6b23ee0f44066219dae14c70ae38a63f0440ce6750f868ee08775073f942 \ --hash=sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8 \ --hash=sha256:2b5e23253bb709ef57a8e95e6ae48daa9ac5f265637529e4ce6b003a37b2621f \ --hash=sha256:2d0da8bbec082bf6bf18345b180958775363588678f64998c2b7609e34719b10 \ --hash=sha256:2f3f7a0fbc219fb4455264cae4d9f01ad41ae6ee8524500f381de64ffaa077d5 \ --hash=sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6 \ --hash=sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21 \ --hash=sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c \ --hash=sha256:366d8f93e3edfe5a918c874702f78faac300209a4d5bf38352b2c1bdc07a766d \ --hash=sha256:374ca2dabdccad8e2a76d40b1d037f5bd16824933bf7bcea3e59c891fd4a0923 \ --hash=sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608 \ --hash=sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de \ --hash=sha256:498524025a5b8ba81695761d78c8dd7382ac0b052f34e66939c42df860b8ff17 \ --hash=sha256:50cf5e7ee9b98f22bdecbabf3800ae78ddcc26e4a435515fc72d97903e8488e0 \ --hash=sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f \ --hash=sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641 \ --hash=sha256:5a3ba5f9a0dfed20337d3e966dc359784c9f96503674c2faf015f7fe8e96798c \ --hash=sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a \ --hash=sha256:5c28f4b5dbef8a0d8aad0d4de24d1e9e981728628afaf4ea0792f5d0939372f0 \ --hash=sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9 \ --hash=sha256:6321899477db90bdeb9299ac3627a6a53c7399c8cd58d25da094007402b039ab \ --hash=sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f \ --hash=sha256:666534d15ba8f0fda3f53969117383d5dc021266b3c1a42c9ec4855e4b58b9d3 \ --hash=sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a \ --hash=sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784 \ --hash=sha256:73f2e31ea8dd7df61a359b731716018c2be196e5bb3b74ddba107f694fbd7604 \ --hash=sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d \ --hash=sha256:76e4753701248476e6286f2ef492af900ea67d9706a0155335a40ea21bf3b2f5 \ --hash=sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03 \ --hash=sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e \ --hash=sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953 \ --hash=sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee \ --hash=sha256:7f3c8c1dacd037df16e85227bac13cca58c30da836c6f936ba1df0c05d046d8d \ --hash=sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817 \ --hash=sha256:828afae9f17e6de596825cf4228ff28fbdf6065974e5ac1410cecc22f699d2b3 \ --hash=sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039 \ --hash=sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f \ --hash=sha256:90646abbc7a5d5c7c19461d2e3eeb76eb0b204919e6ece342feb6032c9325ae9 \ --hash=sha256:91d6c171862df0a6c61479d9724f22efb6109111017c87567cfeb7b5d1449fdf \ --hash=sha256:9272fa73ca71266702c4c3e2d4a28553ea03418e591e377a03b8e3659d94fa76 \ --hash=sha256:92b5278ed9d50fe610185ecd23c55d8b307d75ca18e94c0e7de328089ac5dcba \ --hash=sha256:97160e245ea33d8609cd2b8fd997c850b56db147a304a262abc2b3be021a9171 \ --hash=sha256:977701c081c0241d0955c9586ffdd9ce44f7a7795df39b9151cd9a6fd0ce4cfb \ --hash=sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439 \ --hash=sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631 \ --hash=sha256:9bbcdfaf4af7ce002694a4e10a0159d5a8d20056a12b05b45cea944a4953f972 \ --hash=sha256:9c2623347b933fcb9095841f1cc5d4ff0b278addd743e0e966cb3d460278840d \ --hash=sha256:a2fe128eb4edeabe11896cb6af88fca5346059f6c8d807e3b910069f39157869 \ --hash=sha256:a72b7a6e3cd2725eff67cd64c8f13335ee18fc3c7befc05aed043d24c7b9ccb9 \ --hash=sha256:a9fe0f1c29ba24ba6ff6abf688cb0b7cf1efab6b6aa6adc55441773c252f7411 \ --hash=sha256:b97f7b575ab4a8af9b7bc1d2ef7f29d3afee2226bd03ca3875c16451ad5a7723 \ --hash=sha256:bdac3c7d9b705d253b2ce370fde941836a5f8b3c5c2b8fd70940a3ea3af7f4f2 \ --hash=sha256:c03eff4a41bd4e38415cbed054bbaff4a075b093e2394b6915dca34a40d1e38b \ --hash=sha256:c16d2fa63e0800723139137d667e1056bee1a1cf7965153d2d104b62855e9b99 \ --hash=sha256:c1fac3e2ace2eb1052e9f7c7db480818371134410e1f5c55d65e8f3ac6d1407e \ --hash=sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840 \ --hash=sha256:cee6798eaf8b1416ef6909b06f7dc04b60755206bddc599f52232606e18179d3 \ --hash=sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb \ --hash=sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3 \ --hash=sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0 \ --hash=sha256:dd94994fc91a6177bfaafd7d9fd951bc8689b0a98168aa26b5f543868548d3ca \ --hash=sha256:de537c11e4aa01d37db0d403b57bd6f0546e71a82347a97c6a9f0dcc532b3a45 \ --hash=sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e \ --hash=sha256:e66cc454f97053b79c2ab09c17fbe3c825ea6b4de20baf1be28919460dd7877f \ --hash=sha256:e79225373c317ff1e35f210dd5f1344ff31066ba8067c307ab60254cd3a78ad5 \ --hash=sha256:f1577515d35ed5649d52ab4319db757bb881ce3b2b796d7283e6634d99ace307 \ --hash=sha256:f1e6540b7fa044eee0bb5111ada694cf3dc15f2b0347ca125ee9ca984d5e9e6e \ --hash=sha256:f2ac49a9bedb996086057b75bf93538240538c6d9b38e57c82d51f75a73409d2 \ --hash=sha256:f47c9c9028f55a04ac254346e92977bf0f166c483c74b4232bee19a6697e4778 \ --hash=sha256:f5f9da7f5dbc00a604fe74aa02ae7c98bcede8a3b8b9666f9f86fc13993bc71a \ --hash=sha256:fd74520371c3c4175142d02a976aee0b4cb4a7cc912a60586ffd8d5929979b30 \ --hash=sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a idna==3.10 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \ --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3 iniconfig==2.1.0 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7 \ --hash=sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760 mccabe==0.7.0 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325 \ --hash=sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e multidict==6.1.0 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f \ --hash=sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056 \ --hash=sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761 \ --hash=sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3 \ --hash=sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b \ --hash=sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6 \ --hash=sha256:10524ebd769727ac77ef2278390fb0068d83f3acb7773792a5080f2b0abf7748 \ --hash=sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966 \ --hash=sha256:16e5f4bf4e603eb1fdd5d8180f1a25f30056f22e55ce51fb3d6ad4ab29f7d96f \ --hash=sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1 \ --hash=sha256:189f652a87e876098bbc67b4da1049afb5f5dfbaa310dd67c594b01c10388db6 \ --hash=sha256:1ca0083e80e791cffc6efce7660ad24af66c8d4079d2a750b29001b53ff59ada \ --hash=sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305 \ --hash=sha256:2090f6a85cafc5b2db085124d752757c9d251548cedabe9bd31afe6363e0aff2 \ --hash=sha256:20b9b5fbe0b88d0bdef2012ef7dee867f874b72528cf1d08f1d59b0e3850129d \ --hash=sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a \ --hash=sha256:22f3105d4fb15c8f57ff3959a58fcab6ce36814486500cd7485651230ad4d4ef \ --hash=sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c \ --hash=sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb \ --hash=sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60 \ --hash=sha256:3702ea6872c5a2a4eeefa6ffd36b042e9773f05b1f37ae3ef7264b1163c2dcf6 \ --hash=sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4 \ --hash=sha256:3914f5aaa0f36d5d60e8ece6a308ee1c9784cd75ec8151062614657a114c4478 \ --hash=sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81 \ --hash=sha256:3c8b88a2ccf5493b6c8da9076fb151ba106960a2df90c2633f342f120751a9e7 \ --hash=sha256:3e97b5e938051226dc025ec80980c285b053ffb1e25a3db2a3aa3bc046bf7f56 \ --hash=sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3 \ --hash=sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6 \ --hash=sha256:483a6aea59cb89904e1ceabd2b47368b5600fb7de78a6e4a2c2987b2d256cf30 \ --hash=sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb \ --hash=sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506 \ --hash=sha256:4a9cb68166a34117d6646c0023c7b759bf197bee5ad4272f420a0141d7eb03a0 \ --hash=sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925 \ --hash=sha256:4e18b656c5e844539d506a0a06432274d7bd52a7487e6828c63a63d69185626c \ --hash=sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6 \ --hash=sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e \ --hash=sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95 \ --hash=sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2 \ --hash=sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133 \ --hash=sha256:5845c1fd4866bb5dd3125d89b90e57ed3138241540897de748cdf19de8a2fca2 \ --hash=sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa \ --hash=sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3 \ --hash=sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3 \ --hash=sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436 \ --hash=sha256:682b987361e5fd7a139ed565e30d81fd81e9629acc7d925a205366877d8c8657 \ --hash=sha256:6b5d83030255983181005e6cfbac1617ce9746b219bc2aad52201ad121226581 \ --hash=sha256:6bb5992037f7a9eff7991ebe4273ea7f51f1c1c511e6a2ce511d0e7bdb754492 \ --hash=sha256:73eae06aa53af2ea5270cc066dcaf02cc60d2994bbb2c4ef5764949257d10f43 \ --hash=sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2 \ --hash=sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2 \ --hash=sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926 \ --hash=sha256:87701f25a2352e5bf7454caa64757642734da9f6b11384c1f9d1a8e699758057 \ --hash=sha256:9079dfc6a70abe341f521f78405b8949f96db48da98aeb43f9907f342f627cdc \ --hash=sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80 \ --hash=sha256:957cf8e4b6e123a9eea554fa7ebc85674674b713551de587eb318a2df3e00255 \ --hash=sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1 \ --hash=sha256:9f636b730f7e8cb19feb87094949ba54ee5357440b9658b2a32a5ce4bce53972 \ --hash=sha256:a114d03b938376557927ab23f1e950827c3b893ccb94b62fd95d430fd0e5cf53 \ --hash=sha256:a185f876e69897a6f3325c3f19f26a297fa058c5e456bfcff8015e9a27e83ae1 \ --hash=sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423 \ --hash=sha256:aa466da5b15ccea564bdab9c89175c762bc12825f4659c11227f515cee76fa4a \ --hash=sha256:aaed8b0562be4a0876ee3b6946f6869b7bcdb571a5d1496683505944e268b160 \ --hash=sha256:ab7c4ceb38d91570a650dba194e1ca87c2b543488fe9309b4212694174fd539c \ --hash=sha256:ac10f4c2b9e770c4e393876e35a7046879d195cd123b4f116d299d442b335bcd \ --hash=sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa \ --hash=sha256:b1c416351ee6271b2f49b56ad7f308072f6f44b37118d69c2cad94f3fa8a40d5 \ --hash=sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b \ --hash=sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa \ --hash=sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef \ --hash=sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44 \ --hash=sha256:c08be4f460903e5a9d0f76818db3250f12e9c344e79314d1d570fc69d7f4eae4 \ --hash=sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156 \ --hash=sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753 \ --hash=sha256:ce2186a7df133a9c895dea3331ddc5ddad42cdd0d1ea2f0a51e5d161e4762f28 \ --hash=sha256:d093be959277cb7dee84b801eb1af388b6ad3ca6a6b6bf1ed7585895789d027d \ --hash=sha256:d094ddec350a2fb899fec68d8353c78233debde9b7d8b4beeafa70825f1c281a \ --hash=sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304 \ --hash=sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008 \ --hash=sha256:d618649d4e70ac6efcbba75be98b26ef5078faad23592f9b51ca492953012429 \ --hash=sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72 \ --hash=sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399 \ --hash=sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3 \ --hash=sha256:db7457bac39421addd0c8449933ac32d8042aae84a14911a757ae6ca3eef1392 \ --hash=sha256:e27bbb6d14416713a8bd7aaa1313c0fc8d44ee48d74497a0ff4c3a1b6ccb5167 \ --hash=sha256:e617fb6b0b6953fffd762669610c1c4ffd05632c138d61ac7e14ad187870669c \ --hash=sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774 \ --hash=sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351 \ --hash=sha256:f179dee3b863ab1c59580ff60f9d99f632f34ccb38bf67a33ec6b3ecadd0fd76 \ --hash=sha256:f4c035da3f544b1882bac24115f3e2e8760f10a0107614fc9839fd232200b875 \ --hash=sha256:f67f217af4b1ff66c68a87318012de788dd95fcfeb24cc889011f4e1c7454dfd \ --hash=sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28 \ --hash=sha256:ff3827aef427c89a25cc96ded1759271a93603aba9fb977a6d264648ebf989db packaging==25.0 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \ --hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f pluggy==1.5.0 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1 \ --hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 propcache==0.2.0 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:00181262b17e517df2cd85656fcd6b4e70946fe62cd625b9d74ac9977b64d8d9 \ --hash=sha256:0e53cb83fdd61cbd67202735e6a6687a7b491c8742dfc39c9e01e80354956763 \ --hash=sha256:1235c01ddaa80da8235741e80815ce381c5267f96cc49b1477fdcf8c047ef325 \ --hash=sha256:140fbf08ab3588b3468932974a9331aff43c0ab8a2ec2c608b6d7d1756dbb6cb \ --hash=sha256:191db28dc6dcd29d1a3e063c3be0b40688ed76434622c53a284e5427565bbd9b \ --hash=sha256:1e41d67757ff4fbc8ef2af99b338bfb955010444b92929e9e55a6d4dcc3c4f09 \ --hash=sha256:1ec43d76b9677637a89d6ab86e1fef70d739217fefa208c65352ecf0282be957 \ --hash=sha256:20a617c776f520c3875cf4511e0d1db847a076d720714ae35ffe0df3e440be68 \ --hash=sha256:218db2a3c297a3768c11a34812e63b3ac1c3234c3a086def9c0fee50d35add1f \ --hash=sha256:22aa8f2272d81d9317ff5756bb108021a056805ce63dd3630e27d042c8092798 \ --hash=sha256:25a1f88b471b3bc911d18b935ecb7115dff3a192b6fef46f0bfaf71ff4f12418 \ --hash=sha256:25c8d773a62ce0451b020c7b29a35cfbc05de8b291163a7a0f3b7904f27253e6 \ --hash=sha256:2a60ad3e2553a74168d275a0ef35e8c0a965448ffbc3b300ab3a5bb9956c2162 \ --hash=sha256:2a66df3d4992bc1d725b9aa803e8c5a66c010c65c741ad901e260ece77f58d2f \ --hash=sha256:2ccc28197af5313706511fab3a8b66dcd6da067a1331372c82ea1cb74285e036 \ --hash=sha256:2e900bad2a8456d00a113cad8c13343f3b1f327534e3589acc2219729237a2e8 \ --hash=sha256:2ee7606193fb267be4b2e3b32714f2d58cad27217638db98a60f9efb5efeccc2 \ --hash=sha256:33ac8f098df0585c0b53009f039dfd913b38c1d2edafed0cedcc0c32a05aa110 \ --hash=sha256:3444cdba6628accf384e349014084b1cacd866fbb88433cd9d279d90a54e0b23 \ --hash=sha256:363ea8cd3c5cb6679f1c2f5f1f9669587361c062e4899fce56758efa928728f8 \ --hash=sha256:375a12d7556d462dc64d70475a9ee5982465fbb3d2b364f16b86ba9135793638 \ --hash=sha256:388f3217649d6d59292b722d940d4d2e1e6a7003259eb835724092a1cca0203a \ --hash=sha256:3947483a381259c06921612550867b37d22e1df6d6d7e8361264b6d037595f44 \ --hash=sha256:39e104da444a34830751715f45ef9fc537475ba21b7f1f5b0f4d71a3b60d7fe2 \ --hash=sha256:3c997f8c44ec9b9b0bcbf2d422cc00a1d9b9c681f56efa6ca149a941e5560da2 \ --hash=sha256:3dfafb44f7bb35c0c06eda6b2ab4bfd58f02729e7c4045e179f9a861b07c9850 \ --hash=sha256:3ebbcf2a07621f29638799828b8d8668c421bfb94c6cb04269130d8de4fb7136 \ --hash=sha256:3f88a4095e913f98988f5b338c1d4d5d07dbb0b6bad19892fd447484e483ba6b \ --hash=sha256:439e76255daa0f8151d3cb325f6dd4a3e93043e6403e6491813bcaaaa8733887 \ --hash=sha256:4569158070180c3855e9c0791c56be3ceeb192defa2cdf6a3f39e54319e56b89 \ --hash=sha256:466c219deee4536fbc83c08d09115249db301550625c7fef1c5563a584c9bc87 \ --hash=sha256:4a9d9b4d0a9b38d1c391bb4ad24aa65f306c6f01b512e10a8a34a2dc5675d348 \ --hash=sha256:4c7dde9e533c0a49d802b4f3f218fa9ad0a1ce21f2c2eb80d5216565202acab4 \ --hash=sha256:53d1bd3f979ed529f0805dd35ddaca330f80a9a6d90bc0121d2ff398f8ed8861 \ --hash=sha256:55346705687dbd7ef0d77883ab4f6fabc48232f587925bdaf95219bae072491e \ --hash=sha256:56295eb1e5f3aecd516d91b00cfd8bf3a13991de5a479df9e27dd569ea23959c \ --hash=sha256:56bb5c98f058a41bb58eead194b4db8c05b088c93d94d5161728515bd52b052b \ --hash=sha256:5a5b3bb545ead161be780ee85a2b54fdf7092815995661947812dde94a40f6fb \ --hash=sha256:5f2564ec89058ee7c7989a7b719115bdfe2a2fb8e7a4543b8d1c0cc4cf6478c1 \ --hash=sha256:608cce1da6f2672a56b24a015b42db4ac612ee709f3d29f27a00c943d9e851de \ --hash=sha256:63f13bf09cc3336eb04a837490b8f332e0db41da66995c9fd1ba04552e516354 \ --hash=sha256:662dd62358bdeaca0aee5761de8727cfd6861432e3bb828dc2a693aa0471a563 \ --hash=sha256:676135dcf3262c9c5081cc8f19ad55c8a64e3f7282a21266d05544450bffc3a5 \ --hash=sha256:67aeb72e0f482709991aa91345a831d0b707d16b0257e8ef88a2ad246a7280bf \ --hash=sha256:67b69535c870670c9f9b14a75d28baa32221d06f6b6fa6f77a0a13c5a7b0a5b9 \ --hash=sha256:682a7c79a2fbf40f5dbb1eb6bfe2cd865376deeac65acf9beb607505dced9e12 \ --hash=sha256:6994984550eaf25dd7fc7bd1b700ff45c894149341725bb4edc67f0ffa94efa4 \ --hash=sha256:69d3a98eebae99a420d4b28756c8ce6ea5a29291baf2dc9ff9414b42676f61d5 \ --hash=sha256:6e2e54267980349b723cff366d1e29b138b9a60fa376664a157a342689553f71 \ --hash=sha256:73e4b40ea0eda421b115248d7e79b59214411109a5bc47d0d48e4c73e3b8fcf9 \ --hash=sha256:74acd6e291f885678631b7ebc85d2d4aec458dd849b8c841b57ef04047833bed \ --hash=sha256:7665f04d0c7f26ff8bb534e1c65068409bf4687aa2534faf7104d7182debb336 \ --hash=sha256:7735e82e3498c27bcb2d17cb65d62c14f1100b71723b68362872bca7d0913d90 \ --hash=sha256:77a86c261679ea5f3896ec060be9dc8e365788248cc1e049632a1be682442063 \ --hash=sha256:7cf18abf9764746b9c8704774d8b06714bcb0a63641518a3a89c7f85cc02c2ad \ --hash=sha256:83928404adf8fb3d26793665633ea79b7361efa0287dfbd372a7e74311d51ee6 \ --hash=sha256:8e40876731f99b6f3c897b66b803c9e1c07a989b366c6b5b475fafd1f7ba3fb8 \ --hash=sha256:8f188cfcc64fb1266f4684206c9de0e80f54622c3f22a910cbd200478aeae61e \ --hash=sha256:91997d9cb4a325b60d4e3f20967f8eb08dfcb32b22554d5ef78e6fd1dda743a2 \ --hash=sha256:91ee8fc02ca52e24bcb77b234f22afc03288e1dafbb1f88fe24db308910c4ac7 \ --hash=sha256:92fe151145a990c22cbccf9ae15cae8ae9eddabfc949a219c9f667877e40853d \ --hash=sha256:945db8ee295d3af9dbdbb698cce9bbc5c59b5c3fe328bbc4387f59a8a35f998d \ --hash=sha256:9517d5e9e0731957468c29dbfd0f976736a0e55afaea843726e887f36fe017df \ --hash=sha256:952e0d9d07609d9c5be361f33b0d6d650cd2bae393aabb11d9b719364521984b \ --hash=sha256:97a58a28bcf63284e8b4d7b460cbee1edaab24634e82059c7b8c09e65284f178 \ --hash=sha256:97e48e8875e6c13909c800fa344cd54cc4b2b0db1d5f911f840458a500fde2c2 \ --hash=sha256:9e0f07b42d2a50c7dd2d8675d50f7343d998c64008f1da5fef888396b7f84630 \ --hash=sha256:a3dc1a4b165283bd865e8f8cb5f0c64c05001e0718ed06250d8cac9bec115b48 \ --hash=sha256:a3ebe9a75be7ab0b7da2464a77bb27febcb4fab46a34f9288f39d74833db7f61 \ --hash=sha256:a64e32f8bd94c105cc27f42d3b658902b5bcc947ece3c8fe7bc1b05982f60e89 \ --hash=sha256:a6ed8db0a556343d566a5c124ee483ae113acc9a557a807d439bcecc44e7dfbb \ --hash=sha256:ad9c9b99b05f163109466638bd30ada1722abb01bbb85c739c50b6dc11f92dc3 \ --hash=sha256:b33d7a286c0dc1a15f5fc864cc48ae92a846df287ceac2dd499926c3801054a6 \ --hash=sha256:bc092ba439d91df90aea38168e11f75c655880c12782facf5cf9c00f3d42b562 \ --hash=sha256:c436130cc779806bdf5d5fae0d848713105472b8566b75ff70048c47d3961c5b \ --hash=sha256:c5869b8fd70b81835a6f187c5fdbe67917a04d7e52b6e7cc4e5fe39d55c39d58 \ --hash=sha256:c5ecca8f9bab618340c8e848d340baf68bcd8ad90a8ecd7a4524a81c1764b3db \ --hash=sha256:cfac69017ef97db2438efb854edf24f5a29fd09a536ff3a992b75990720cdc99 \ --hash=sha256:d2f0d0f976985f85dfb5f3d685697ef769faa6b71993b46b295cdbbd6be8cc37 \ --hash=sha256:d5bed7f9805cc29c780f3aee05de3262ee7ce1f47083cfe9f77471e9d6777e83 \ --hash=sha256:d6a21ef516d36909931a2967621eecb256018aeb11fc48656e3257e73e2e247a \ --hash=sha256:d9b6ddac6408194e934002a69bcaadbc88c10b5f38fb9307779d1c629181815d \ --hash=sha256:db47514ffdbd91ccdc7e6f8407aac4ee94cc871b15b577c1c324236b013ddd04 \ --hash=sha256:df81779732feb9d01e5d513fad0122efb3d53bbc75f61b2a4f29a020bc985e70 \ --hash=sha256:e4a91d44379f45f5e540971d41e4626dacd7f01004826a18cb048e7da7e96544 \ --hash=sha256:e63e3e1e0271f374ed489ff5ee73d4b6e7c60710e1f76af5f0e1a6117cd26394 \ --hash=sha256:e70fac33e8b4ac63dfc4c956fd7d85a0b1139adcfc0d964ce288b7c527537fea \ --hash=sha256:ecddc221a077a8132cf7c747d5352a15ed763b674c0448d811f408bf803d9ad7 \ --hash=sha256:f45eec587dafd4b2d41ac189c2156461ebd0c1082d2fe7013571598abb8505d1 \ --hash=sha256:f52a68c21363c45297aca15561812d542f8fc683c85201df0bebe209e349f793 \ --hash=sha256:f571aea50ba5623c308aa146eb650eebf7dbe0fd8c5d946e28343cb3b5aad577 \ --hash=sha256:f60f0ac7005b9f5a6091009b09a419ace1610e163fa5deaba5ce3484341840e7 \ --hash=sha256:f6475a1b2ecb310c98c28d271a30df74f9dd436ee46d09236a6b750a7599ce57 \ --hash=sha256:f6d5749fdd33d90e34c2efb174c7e236829147a2713334d708746e94c4bde40d \ --hash=sha256:f902804113e032e2cdf8c71015651c97af6418363bea8d78dc0911d56c335032 \ --hash=sha256:fa1076244f54bb76e65e22cb6910365779d5c3d71d1f18b275f1dfc7b0d71b4d \ --hash=sha256:fc2db02409338bf36590aa985a461b2c96fce91f8e7e0f14c50c5fcc4f229016 \ --hash=sha256:ffcad6c564fe6b9b8916c1aefbb37a362deebf9394bd2974e9d84232e3e08504 py==1.11.0 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719 \ --hash=sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378 pycodestyle==2.9.1 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785 \ --hash=sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b pyflakes==2.5.0 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2 \ --hash=sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3 pytest-asyncio==0.14.0 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:2eae1e34f6c68fc0a9dc12d4bea190483843ff4708d24277c41568d6b6044f1d \ --hash=sha256:9882c0c6b24429449f5f969a5158b528f39bde47dc32e85b9f0403965017e700 pytest-cov==2.12.1 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a \ --hash=sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7 pytest==6.2.5 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89 \ --hash=sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134 toml==0.10.2 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \ --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f typing-extensions==4.13.2 ; python_version >= "3.9" and python_version < "3.11" \ --hash=sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c \ --hash=sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef yarl==1.15.2 ; python_version >= "3.9" and python_version < "4.0" \ --hash=sha256:0545de8c688fbbf3088f9e8b801157923be4bf8e7b03e97c2ecd4dfa39e48e0e \ --hash=sha256:076b1ed2ac819933895b1a000904f62d615fe4533a5cf3e052ff9a1da560575c \ --hash=sha256:0afad2cd484908f472c8fe2e8ef499facee54a0a6978be0e0cff67b1254fd747 \ --hash=sha256:0ccaa1bc98751fbfcf53dc8dfdb90d96e98838010fc254180dd6707a6e8bb179 \ --hash=sha256:0d3105efab7c5c091609abacad33afff33bdff0035bece164c98bcf5a85ef90a \ --hash=sha256:0e1af74a9529a1137c67c887ed9cde62cff53aa4d84a3adbec329f9ec47a3936 \ --hash=sha256:136f9db0f53c0206db38b8cd0c985c78ded5fd596c9a86ce5c0b92afb91c3a19 \ --hash=sha256:156ececdf636143f508770bf8a3a0498de64da5abd890c7dbb42ca9e3b6c05b8 \ --hash=sha256:15c87339490100c63472a76d87fe7097a0835c705eb5ae79fd96e343473629ed \ --hash=sha256:1695497bb2a02a6de60064c9f077a4ae9c25c73624e0d43e3aa9d16d983073c2 \ --hash=sha256:173563f3696124372831007e3d4b9821746964a95968628f7075d9231ac6bb33 \ --hash=sha256:173866d9f7409c0fb514cf6e78952e65816600cb888c68b37b41147349fe0057 \ --hash=sha256:23ec1d3c31882b2a8a69c801ef58ebf7bae2553211ebbddf04235be275a38548 \ --hash=sha256:243fbbbf003754fe41b5bdf10ce1e7f80bcc70732b5b54222c124d6b4c2ab31c \ --hash=sha256:28c6cf1d92edf936ceedc7afa61b07e9d78a27b15244aa46bbcd534c7458ee1b \ --hash=sha256:2aa738e0282be54eede1e3f36b81f1e46aee7ec7602aa563e81e0e8d7b67963f \ --hash=sha256:2cf441c4b6e538ba0d2591574f95d3fdd33f1efafa864faa077d9636ecc0c4e9 \ --hash=sha256:30c3ff305f6e06650a761c4393666f77384f1cc6c5c0251965d6bfa5fbc88f7f \ --hash=sha256:31561a5b4d8dbef1559b3600b045607cf804bae040f64b5f5bca77da38084a8a \ --hash=sha256:32b66be100ac5739065496c74c4b7f3015cef792c3174982809274d7e51b3e04 \ --hash=sha256:3433da95b51a75692dcf6cc8117a31410447c75a9a8187888f02ad45c0a86c50 \ --hash=sha256:34a2d76a1984cac04ff8b1bfc939ec9dc0914821264d4a9c8fd0ed6aa8d4cfd2 \ --hash=sha256:353665775be69bbfc6d54c8d134bfc533e332149faeddd631b0bc79df0897f46 \ --hash=sha256:38d0124fa992dbacd0c48b1b755d3ee0a9f924f427f95b0ef376556a24debf01 \ --hash=sha256:3c56ec1eacd0a5d35b8a29f468659c47f4fe61b2cab948ca756c39b7617f0aa5 \ --hash=sha256:3db817b4e95eb05c362e3b45dafe7144b18603e1211f4a5b36eb9522ecc62bcf \ --hash=sha256:3e52474256a7db9dcf3c5f4ca0b300fdea6c21cca0148c8891d03a025649d935 \ --hash=sha256:416f2e3beaeae81e2f7a45dc711258be5bdc79c940a9a270b266c0bec038fb84 \ --hash=sha256:435aca062444a7f0c884861d2e3ea79883bd1cd19d0a381928b69ae1b85bc51d \ --hash=sha256:4388c72174868884f76affcdd3656544c426407e0043c89b684d22fb265e04a5 \ --hash=sha256:43ebdcc120e2ca679dba01a779333a8ea76b50547b55e812b8b92818d604662c \ --hash=sha256:458c0c65802d816a6b955cf3603186de79e8fdb46d4f19abaec4ef0a906f50a7 \ --hash=sha256:533a28754e7f7439f217550a497bb026c54072dbe16402b183fdbca2431935a9 \ --hash=sha256:553dad9af802a9ad1a6525e7528152a015b85fb8dbf764ebfc755c695f488367 \ --hash=sha256:5838f2b79dc8f96fdc44077c9e4e2e33d7089b10788464609df788eb97d03aad \ --hash=sha256:5b48388ded01f6f2429a8c55012bdbd1c2a0c3735b3e73e221649e524c34a58d \ --hash=sha256:5bc0df728e4def5e15a754521e8882ba5a5121bd6b5a3a0ff7efda5d6558ab3d \ --hash=sha256:63eab904f8630aed5a68f2d0aeab565dcfc595dc1bf0b91b71d9ddd43dea3aea \ --hash=sha256:66f629632220a4e7858b58e4857927dd01a850a4cef2fb4044c8662787165cf7 \ --hash=sha256:670eb11325ed3a6209339974b276811867defe52f4188fe18dc49855774fa9cf \ --hash=sha256:69d5856d526802cbda768d3e6246cd0d77450fa2a4bc2ea0ea14f0d972c2894b \ --hash=sha256:6e840553c9c494a35e449a987ca2c4f8372668ee954a03a9a9685075228e5036 \ --hash=sha256:711bdfae4e699a6d4f371137cbe9e740dc958530cb920eb6f43ff9551e17cfbc \ --hash=sha256:74abb8709ea54cc483c4fb57fb17bb66f8e0f04438cff6ded322074dbd17c7ec \ --hash=sha256:75119badf45f7183e10e348edff5a76a94dc19ba9287d94001ff05e81475967b \ --hash=sha256:766dcc00b943c089349d4060b935c76281f6be225e39994c2ccec3a2a36ad627 \ --hash=sha256:78e6fdc976ec966b99e4daa3812fac0274cc28cd2b24b0d92462e2e5ef90d368 \ --hash=sha256:81dadafb3aa124f86dc267a2168f71bbd2bfb163663661ab0038f6e4b8edb810 \ --hash=sha256:82d5161e8cb8f36ec778fd7ac4d740415d84030f5b9ef8fe4da54784a1f46c94 \ --hash=sha256:833547179c31f9bec39b49601d282d6f0ea1633620701288934c5f66d88c3e50 \ --hash=sha256:856b7f1a7b98a8c31823285786bd566cf06226ac4f38b3ef462f593c608a9bd6 \ --hash=sha256:8657d3f37f781d987037f9cc20bbc8b40425fa14380c87da0cb8dfce7c92d0fb \ --hash=sha256:93bed8a8084544c6efe8856c362af08a23e959340c87a95687fdbe9c9f280c8b \ --hash=sha256:954dde77c404084c2544e572f342aef384240b3e434e06cecc71597e95fd1ce7 \ --hash=sha256:98f68df80ec6ca3015186b2677c208c096d646ef37bbf8b49764ab4a38183931 \ --hash=sha256:99e12d2bf587b44deb74e0d6170fec37adb489964dbca656ec41a7cd8f2ff178 \ --hash=sha256:9a13a07532e8e1c4a5a3afff0ca4553da23409fad65def1b71186fb867eeae8d \ --hash=sha256:9c1e3ff4b89cdd2e1a24c214f141e848b9e0451f08d7d4963cb4108d4d798f1f \ --hash=sha256:9ce2e0f6123a60bd1a7f5ae3b2c49b240c12c132847f17aa990b841a417598a2 \ --hash=sha256:9fcda20b2de7042cc35cf911702fa3d8311bd40055a14446c1e62403684afdc5 \ --hash=sha256:a32d58f4b521bb98b2c0aa9da407f8bd57ca81f34362bcb090e4a79e9924fefc \ --hash=sha256:a39c36f4218a5bb668b4f06874d676d35a035ee668e6e7e3538835c703634b84 \ --hash=sha256:a5cafb02cf097a82d74403f7e0b6b9df3ffbfe8edf9415ea816314711764a27b \ --hash=sha256:a7cf963a357c5f00cb55b1955df8bbe68d2f2f65de065160a1c26b85a1e44172 \ --hash=sha256:a880372e2e5dbb9258a4e8ff43f13888039abb9dd6d515f28611c54361bc5644 \ --hash=sha256:ace4cad790f3bf872c082366c9edd7f8f8f77afe3992b134cfc810332206884f \ --hash=sha256:af8ff8d7dc07ce873f643de6dfbcd45dc3db2c87462e5c387267197f59e6d776 \ --hash=sha256:b47a6000a7e833ebfe5886b56a31cb2ff12120b1efd4578a6fcc38df16cc77bd \ --hash=sha256:b71862a652f50babab4a43a487f157d26b464b1dedbcc0afda02fd64f3809d04 \ --hash=sha256:b7f227ca6db5a9fda0a2b935a2ea34a7267589ffc63c8045f0e4edb8d8dcf956 \ --hash=sha256:bc8936d06cd53fddd4892677d65e98af514c8d78c79864f418bbf78a4a2edde4 \ --hash=sha256:bed1b5dbf90bad3bfc19439258c97873eab453c71d8b6869c136346acfe497e7 \ --hash=sha256:c45817e3e6972109d1a2c65091504a537e257bc3c885b4e78a95baa96df6a3f8 \ --hash=sha256:c68e820879ff39992c7f148113b46efcd6ec765a4865581f2902b3c43a5f4bbb \ --hash=sha256:c77494a2f2282d9bbbbcab7c227a4d1b4bb829875c96251f66fb5f3bae4fb053 \ --hash=sha256:c998d0558805860503bc3a595994895ca0f7835e00668dadc673bbf7f5fbfcbe \ --hash=sha256:ccad2800dfdff34392448c4bf834be124f10a5bc102f254521d931c1c53c455a \ --hash=sha256:cd126498171f752dd85737ab1544329a4520c53eed3997f9b08aefbafb1cc53b \ --hash=sha256:ce44217ad99ffad8027d2fde0269ae368c86db66ea0571c62a000798d69401fb \ --hash=sha256:d1ac2bc069f4a458634c26b101c2341b18da85cb96afe0015990507efec2e417 \ --hash=sha256:d417a4f6943112fae3924bae2af7112562285848d9bcee737fc4ff7cbd450e6c \ --hash=sha256:d538df442c0d9665664ab6dd5fccd0110fa3b364914f9c85b3ef9b7b2e157980 \ --hash=sha256:ded1b1803151dd0f20a8945508786d57c2f97a50289b16f2629f85433e546d47 \ --hash=sha256:e2e93b88ecc8f74074012e18d679fb2e9c746f2a56f79cd5e2b1afcf2a8a786b \ --hash=sha256:e4ca3b9f370f218cc2a0309542cab8d0acdfd66667e7c37d04d617012485f904 \ --hash=sha256:e4ee8b8639070ff246ad3649294336b06db37a94bdea0d09ea491603e0be73b8 \ --hash=sha256:e52f77a0cd246086afde8815039f3e16f8d2be51786c0a39b57104c563c5cbb0 \ --hash=sha256:eaea112aed589131f73d50d570a6864728bd7c0c66ef6c9154ed7b59f24da611 \ --hash=sha256:ed20a4bdc635f36cb19e630bfc644181dd075839b6fc84cac51c0f381ac472e2 \ --hash=sha256:eedc3f247ee7b3808ea07205f3e7d7879bc19ad3e6222195cd5fbf9988853e4d \ --hash=sha256:f0e1844ad47c7bd5d6fa784f1d4accc5f4168b48999303a868fe0f8597bde715 \ --hash=sha256:f4fe99ce44128c71233d0d72152db31ca119711dfc5f2c82385ad611d8d7f897 \ --hash=sha256:f8cfd847e6b9ecf9f2f2531c8427035f291ec286c0a4944b0a9fce58c6446046 \ --hash=sha256:f9ca0e6ce7774dc7830dc0cc4bb6b3eec769db667f230e7c770a628c1aa5681b \ --hash=sha256:fa2bea05ff0a8fb4d8124498e00e02398f06d23cdadd0fe027d84a3f7afde31e \ --hash=sha256:fbbb63bed5fcd70cd3dd23a087cd78e4675fb5a2963b8af53f945cbbca79ae16 \ --hash=sha256:fbda058a9a68bec347962595f50546a8a4a34fd7b0654a7b9697917dc2bf810d \ --hash=sha256:ffd591e22b22f9cb48e472529db6a47203c41c2c5911ff0a52e85723196c0d75 pyairnow-1.3.1/tests/000077500000000000000000000000001502652370200145305ustar00rootroot00000000000000pyairnow-1.3.1/tests/__init__.py000066400000000000000000000000001502652370200166270ustar00rootroot00000000000000pyairnow-1.3.1/tests/conftest.py000066400000000000000000000006311502652370200167270ustar00rootroot00000000000000'''pyAirNow pytest Fixtures''' import pytest import re from aioresponses import aioresponses from .mock_api import mock_airnow_api @pytest.fixture def mock_aioresponse(): with aioresponses() as m: yield m @pytest.fixture def mock_airnowapi(mock_aioresponse): url_pattern = re.compile(r'^https://www\.airnowapi\.org/(.*)$') mock_aioresponse.get(url_pattern, callback=mock_airnow_api) pyairnow-1.3.1/tests/mock_api.py000066400000000000000000000052061502652370200166670ustar00rootroot00000000000000'''Mocked AirNow API''' import re from aioresponses import CallbackResult MOCK_API_KEY = '01234567-89AB-CDEF-0123-456789ABCDEF' RE_ENDPOINTS = re.compile( '^/?aq/(forecast|observation)/(zipCode|latLong)(/(current))?/?$' ) def mock_airnow_api(url, **kwargs): ''' Mock the AirNow API ''' m = RE_ENDPOINTS.match(url.path) if m is None: return CallbackResult(status=404) # Parse URL path ep_type = m.group(1) ep_mode = m.group(2) ep_when = m.group(4) # Check API Key if 'API_KEY' not in url.query: return CallbackResult(status=401, payload=dict( WebServiceError=[ dict( Message='Request not authenticated' ), ], )) if url.query['API_KEY'] != MOCK_API_KEY: return CallbackResult(status=401, payload=dict( WebServiceError=[ dict( Message='Invalid API key' ), ], )) # A zip code with value "empty" will return an empty list if 'zipCode' in url.query and url.query['zipCode'] == 'empty': return CallbackResult(payload=[]) # A zip code with value "bad_json" will return invalid json if 'zipCode' in url.query and url.query['zipCode'] == 'bad_json': return CallbackResult(body='Bad JSON Test') # A zip code with value "error" will return a JSON error message if 'zipCode' in url.query and url.query['zipCode'] == 'error': return CallbackResult(status=400, payload=dict( WebServiceError=[ dict( Message='Client Error' ), ], )) # A zip code with value "error1" will return a JSON error message if 'zipCode' in url.query and url.query['zipCode'] == 'error1': return CallbackResult(status=400, payload=dict( WebServiceError=[ 'Client Error' ], )) # A zip code with value "error2" will return a non-structured error message if 'zipCode' in url.query and url.query['zipCode'] == 'error2': return CallbackResult(status=500, payload=dict( WebServiceError='Internal Server Error' )) # A zip code with value "dict" will return a JSON dictionary instead of # an array if 'zipCode' in url.query and url.query['zipCode'] == 'dict': return CallbackResult(payload=dict(status='OK')) # Return JSON with the endpoint and query information payload = dict( type=ep_type, mode=ep_mode, when=ep_when, query=dict(url.query), ) return CallbackResult(payload=[payload]) pyairnow-1.3.1/tests/test_10_api.py000066400000000000000000000017721502652370200172210ustar00rootroot00000000000000import pytest from aiohttp import ClientSession from pyairnow import WebServiceAPI from .mock_api import MOCK_API_KEY @pytest.mark.asyncio async def test_api(mock_airnowapi): client = WebServiceAPI(MOCK_API_KEY) data = await client.forecast.zipCode('90001') assert isinstance(data, list) assert len(data) == 1 assert data[0]['type'] == 'forecast' assert data[0]['mode'] == 'zipCode' assert data[0]['when'] is None assert 'zipCode' in data[0]['query'] assert data[0]['query']['zipCode'] == '90001' @pytest.mark.asyncio async def test_api_with_session(mock_airnowapi): session = ClientSession() client = WebServiceAPI(MOCK_API_KEY, session=session) data = await client.forecast.zipCode(90001) assert isinstance(data, list) assert len(data) == 1 assert data[0]['type'] == 'forecast' assert data[0]['mode'] == 'zipCode' assert data[0]['when'] is None assert 'zipCode' in data[0]['query'] assert data[0]['query']['zipCode'] == '90001' pyairnow-1.3.1/tests/test_11_errors.py000066400000000000000000000041351502652370200177610ustar00rootroot00000000000000import pytest from pyairnow import WebServiceAPI from pyairnow.errors import AirNowError, EmptyResponseError, \ InvalidJsonError, InvalidKeyError from .mock_api import MOCK_API_KEY @pytest.mark.asyncio async def test_api_not_authenticated(mock_airnowapi): client = WebServiceAPI('') with pytest.raises(InvalidKeyError) as exc_info: await client.forecast.zipCode(90001) assert 'not authenticated' in str(exc_info.value) @pytest.mark.asyncio async def test_api_invalid_key(mock_airnowapi): client = WebServiceAPI('123ABC') with pytest.raises(InvalidKeyError) as exc_info: await client.forecast.zipCode(90001) assert 'Invalid API key' in str(exc_info.value) @pytest.mark.asyncio async def test_api_bad_json(mock_airnowapi): client = WebServiceAPI(MOCK_API_KEY) with pytest.raises(InvalidJsonError): await client.forecast.zipCode('bad_json') @pytest.mark.asyncio async def test_api_bad_json_2(mock_airnowapi): client = WebServiceAPI(MOCK_API_KEY) with pytest.raises(InvalidJsonError): await client.forecast.zipCode('dict') @pytest.mark.asyncio async def test_api_empty_response(mock_airnowapi): client = WebServiceAPI(MOCK_API_KEY) with pytest.raises(EmptyResponseError): await client.forecast.zipCode('empty') @pytest.mark.asyncio async def test_api_unexpected_error(mock_airnowapi): client = WebServiceAPI(MOCK_API_KEY) with pytest.raises(AirNowError) as exc_info: await client.forecast.zipCode('error') assert 'Client Error' in str(exc_info.value) @pytest.mark.asyncio async def test_api_unexpected_error_1(mock_airnowapi): client = WebServiceAPI(MOCK_API_KEY) with pytest.raises(AirNowError) as exc_info: await client.forecast.zipCode('error1') assert 'Internal Server Error' in str(exc_info.value) @pytest.mark.asyncio async def test_api_unexpected_error_2(mock_airnowapi): client = WebServiceAPI(MOCK_API_KEY) with pytest.raises(AirNowError) as exc_info: await client.forecast.zipCode('error2') assert 'Internal Server Error' in str(exc_info.value) pyairnow-1.3.1/tests/test_12_forecast.py000066400000000000000000000141111502652370200202470ustar00rootroot00000000000000import datetime import pytest from pyairnow import WebServiceAPI from .mock_api import MOCK_API_KEY @pytest.mark.asyncio async def test_api_forecast_zipcode(mock_airnowapi): client = WebServiceAPI(MOCK_API_KEY) data = await client.forecast.zipCode(90001) assert isinstance(data, list) assert len(data) == 1 assert data[0]['type'] == 'forecast' assert data[0]['mode'] == 'zipCode' assert data[0]['when'] is None assert 'zipCode' in data[0]['query'] assert data[0]['query']['zipCode'] == '90001' @pytest.mark.asyncio async def test_api_forecast_zipcode_distance(mock_airnowapi): client = WebServiceAPI(MOCK_API_KEY) data = await client.forecast.zipCode(90001, distance=100) assert isinstance(data, list) assert len(data) == 1 assert data[0]['type'] == 'forecast' assert data[0]['mode'] == 'zipCode' assert data[0]['when'] is None assert 'zipCode' in data[0]['query'] assert data[0]['query']['zipCode'] == '90001' assert 'distance' in data[0]['query'] assert data[0]['query']['distance'] == '100' @pytest.mark.asyncio async def test_api_forecast_zipcode_date_str(mock_airnowapi): client = WebServiceAPI(MOCK_API_KEY) data = await client.forecast.zipCode(90001, date='2020-09-01') assert isinstance(data, list) assert len(data) == 1 assert data[0]['type'] == 'forecast' assert data[0]['mode'] == 'zipCode' assert data[0]['when'] is None assert 'zipCode' in data[0]['query'] assert data[0]['query']['zipCode'] == '90001' assert 'date' in data[0]['query'] assert data[0]['query']['date'] == '2020-09-01' @pytest.mark.asyncio async def test_api_forecast_zipcode_date_date(mock_airnowapi): client = WebServiceAPI(MOCK_API_KEY) data = await client.forecast.zipCode( 90001, date=datetime.date(2020, 9, 1) ) assert isinstance(data, list) assert len(data) == 1 assert data[0]['type'] == 'forecast' assert data[0]['mode'] == 'zipCode' assert data[0]['when'] is None assert 'zipCode' in data[0]['query'] assert data[0]['query']['zipCode'] == '90001' assert 'date' in data[0]['query'] assert data[0]['query']['date'] == '2020-09-01' @pytest.mark.asyncio async def test_api_forecast_zipcode_date_datetime(mock_airnowapi): client = WebServiceAPI(MOCK_API_KEY) data = await client.forecast.zipCode( 90001, date=datetime.datetime(2020, 9, 1, 11, 45, 1) ) assert isinstance(data, list) assert len(data) == 1 assert data[0]['type'] == 'forecast' assert data[0]['mode'] == 'zipCode' assert data[0]['when'] is None assert 'zipCode' in data[0]['query'] assert data[0]['query']['zipCode'] == '90001' assert 'date' in data[0]['query'] assert data[0]['query']['date'] == '2020-09-01' @pytest.mark.asyncio async def test_api_forecast_ll(mock_airnowapi): client = WebServiceAPI(MOCK_API_KEY) data = await client.forecast.latLong(34.053718, -118.244842) assert isinstance(data, list) assert len(data) == 1 assert data[0]['type'] == 'forecast' assert data[0]['mode'] == 'latLong' assert data[0]['when'] is None assert 'latitude' in data[0]['query'] assert data[0]['query']['latitude'] == '34.053718' assert 'longitude' in data[0]['query'] assert data[0]['query']['longitude'] == '-118.244842' @pytest.mark.asyncio async def test_api_forecast_ll_distance(mock_airnowapi): client = WebServiceAPI(MOCK_API_KEY) data = await client.forecast.latLong( 34.053718, -118.244842, distance=120 ) assert isinstance(data, list) assert len(data) == 1 assert data[0]['type'] == 'forecast' assert data[0]['mode'] == 'latLong' assert data[0]['when'] is None assert 'latitude' in data[0]['query'] assert data[0]['query']['latitude'] == '34.053718' assert 'longitude' in data[0]['query'] assert data[0]['query']['longitude'] == '-118.244842' assert 'distance' in data[0]['query'] assert data[0]['query']['distance'] == '120' @pytest.mark.asyncio async def test_api_forecast_ll_date_str(mock_airnowapi): client = WebServiceAPI(MOCK_API_KEY) data = await client.forecast.latLong( 34.053718, -118.244842, date='2020-09-01' ) assert isinstance(data, list) assert len(data) == 1 assert data[0]['type'] == 'forecast' assert data[0]['mode'] == 'latLong' assert data[0]['when'] is None assert 'latitude' in data[0]['query'] assert data[0]['query']['latitude'] == '34.053718' assert 'longitude' in data[0]['query'] assert data[0]['query']['longitude'] == '-118.244842' assert 'date' in data[0]['query'] assert data[0]['query']['date'] == '2020-09-01' @pytest.mark.asyncio async def test_api_forecast_ll_date_date(mock_airnowapi): client = WebServiceAPI(MOCK_API_KEY) data = await client.forecast.latLong( 34.053718, -118.244842, date=datetime.date(2020, 9, 1) ) assert isinstance(data, list) assert len(data) == 1 assert data[0]['type'] == 'forecast' assert data[0]['mode'] == 'latLong' assert data[0]['when'] is None assert 'latitude' in data[0]['query'] assert data[0]['query']['latitude'] == '34.053718' assert 'longitude' in data[0]['query'] assert data[0]['query']['longitude'] == '-118.244842' assert 'date' in data[0]['query'] assert data[0]['query']['date'] == '2020-09-01' @pytest.mark.asyncio async def test_api_forecast_ll_date_datetime(mock_airnowapi): client = WebServiceAPI(MOCK_API_KEY) data = await client.forecast.latLong( 34.053718, -118.244842, date=datetime.datetime(2020, 9, 1, 11, 45, 0) ) assert isinstance(data, list) assert len(data) == 1 assert data[0]['type'] == 'forecast' assert data[0]['mode'] == 'latLong' assert data[0]['when'] is None assert 'latitude' in data[0]['query'] assert data[0]['query']['latitude'] == '34.053718' assert 'longitude' in data[0]['query'] assert data[0]['query']['longitude'] == '-118.244842' assert 'date' in data[0]['query'] assert data[0]['query']['date'] == '2020-09-01' pyairnow-1.3.1/tests/test_13_observations.py000066400000000000000000000045141502652370200211660ustar00rootroot00000000000000import pytest from pyairnow import WebServiceAPI from .mock_api import MOCK_API_KEY @pytest.mark.asyncio async def test_api_observations_zipcode(mock_airnowapi): client = WebServiceAPI(MOCK_API_KEY) data = await client.observations.zipCode(90001) assert isinstance(data, list) assert len(data) == 1 assert data[0]['type'] == 'observation' assert data[0]['mode'] == 'zipCode' assert data[0]['when'] == 'current' assert 'zipCode' in data[0]['query'] assert data[0]['query']['zipCode'] == '90001' @pytest.mark.asyncio async def test_api_observations_zipcode_distance(mock_airnowapi): client = WebServiceAPI(MOCK_API_KEY) data = await client.observations.zipCode(90001, distance=100) assert isinstance(data, list) assert len(data) == 1 assert data[0]['type'] == 'observation' assert data[0]['mode'] == 'zipCode' assert data[0]['when'] == 'current' assert 'zipCode' in data[0]['query'] assert data[0]['query']['zipCode'] == '90001' assert 'distance' in data[0]['query'] assert data[0]['query']['distance'] == '100' @pytest.mark.asyncio async def test_api_observations_ll(mock_airnowapi): client = WebServiceAPI(MOCK_API_KEY) data = await client.observations.latLong(34.053718, -118.244842) assert isinstance(data, list) assert len(data) == 1 assert data[0]['type'] == 'observation' assert data[0]['mode'] == 'latLong' assert data[0]['when'] == 'current' assert 'latitude' in data[0]['query'] assert data[0]['query']['latitude'] == '34.053718' assert 'longitude' in data[0]['query'] assert data[0]['query']['longitude'] == '-118.244842' @pytest.mark.asyncio async def test_api_observations_ll_distance(mock_airnowapi): client = WebServiceAPI(MOCK_API_KEY) data = await client.observations.latLong( 34.053718, -118.244842, distance=120 ) assert isinstance(data, list) assert len(data) == 1 assert data[0]['type'] == 'observation' assert data[0]['mode'] == 'latLong' assert data[0]['when'] == 'current' assert 'latitude' in data[0]['query'] assert data[0]['query']['latitude'] == '34.053718' assert 'longitude' in data[0]['query'] assert data[0]['query']['longitude'] == '-118.244842' assert 'distance' in data[0]['query'] assert data[0]['query']['distance'] == '120' pyairnow-1.3.1/tests/test_14_conversions.py000066400000000000000000000021731502652370200210200ustar00rootroot00000000000000import pytest from pyairnow.conv import aqi_to_concentration, concentration_to_aqi def test_convert_pm25(): assert aqi_to_concentration(0, 'PM2.5') == 0 assert aqi_to_concentration(25, 'PM2.5') == 4.5 assert aqi_to_concentration(50, 'PM2.5') == 9.0 assert aqi_to_concentration(51, 'PM2.5') == 9.1 assert aqi_to_concentration(86, 'PM2.5') == 27.9 assert aqi_to_concentration(144, 'PM2.5') == 53.0 assert aqi_to_concentration(500, 'PM2.5') == 325.4 assert aqi_to_concentration(501, 'PM2.5') == 325.9 assert concentration_to_aqi(0, 'PM2.5') == 0 assert concentration_to_aqi(9.0, 'PM2.5') == 50 assert concentration_to_aqi(9.1, 'PM2.5') == 51 assert concentration_to_aqi(27.9, 'PM2.5') == 86 assert concentration_to_aqi(53.0, 'PM2.5') == 144 assert concentration_to_aqi(325.4, 'PM2.5') == 500 assert concentration_to_aqi(325.9, 'PM2.5') == 501 def test_convert_invalid_negative_aqi(): with pytest.raises(ValueError): aqi_to_concentration(-1, 'PM2.5') def test_convert_invalid_negative_o3(): with pytest.raises(ValueError): concentration_to_aqi(-0.0008, 'O3') pyairnow-1.3.1/tox.ini000066400000000000000000000001111502652370200146720ustar00rootroot00000000000000[flake8] exclude = .venv extend-ignore = E201,E202,E272 pyairnow/conv.py