pax_global_header00006660000000000000000000000064146561760150014525gustar00rootroot0000000000000052 comment=617420643532a70ff7258b0fe47c2ab813526f0c pyairnow-1.2.2/000077500000000000000000000000001465617601500133775ustar00rootroot00000000000000pyairnow-1.2.2/.github/000077500000000000000000000000001465617601500147375ustar00rootroot00000000000000pyairnow-1.2.2/.github/workflows/000077500000000000000000000000001465617601500167745ustar00rootroot00000000000000pyairnow-1.2.2/.github/workflows/ci.yaml000066400000000000000000000031531465617601500202550ustar00rootroot00000000000000--- name: CI on: pull_request: branches: - master push: branches: - master jobs: test: name: Tests runs-on: ubuntu-latest strategy: matrix: python-version: - "3.8" - "3.9" - "3.10" - "3.11" 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.11" 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.11" 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.2.2/.github/workflows/publish.yaml000066400000000000000000000022101465617601500213210ustar00rootroot00000000000000# 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.11' - name: Install Poetry run: | curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python - 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.2.2/.gitignore000066400000000000000000000034631465617601500153750ustar00rootroot00000000000000# 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.2.2/AUTHORS.md000066400000000000000000000001331465617601500150430ustar00rootroot00000000000000# Contributions to `pyairnow` ## Owners - Jonathan Krauss (https://github.com/asymworks) pyairnow-1.2.2/CHANGELOG.md000066400000000000000000000023411465617601500152100ustar00rootroot00000000000000# 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.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.2.2/LICENSE000066400000000000000000000020571465617601500144100ustar00rootroot00000000000000MIT 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.2.2/Makefile000066400000000000000000000012361465617601500150410ustar00rootroot00000000000000 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.2.2/README.md000066400000000000000000000113241465617601500146570ustar00rootroot00000000000000# 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.8 * Python 3.9 * Python 3.10 * Python 3.11 # 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://www.airnow.gov/sites/default/files/2020-05/aqi-technical-assistance-document-sept2018.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.2.2/examples/000077500000000000000000000000001465617601500152155ustar00rootroot00000000000000pyairnow-1.2.2/examples/__init__.py000066400000000000000000000000751465617601500173300ustar00rootroot00000000000000'''pyAirNow Examples (run with python -m examples.module)''' pyairnow-1.2.2/examples/cli.py000066400000000000000000000074601465617601500163450ustar00rootroot00000000000000'''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.2.2/pyairnow/000077500000000000000000000000001465617601500152475ustar00rootroot00000000000000pyairnow-1.2.2/pyairnow/__init__.py000066400000000000000000000000751465617601500173620ustar00rootroot00000000000000'''Public Objects''' from .api import WebServiceAPI # noqa pyairnow-1.2.2/pyairnow/api.py000066400000000000000000000061231465617601500163740ustar00rootroot00000000000000'''Client to interact with AirNow Air Quality API''' from json.decoder import JSONDecodeError from typing import Optional 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: 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 = 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.2.2/pyairnow/conv.py000066400000000000000000000111021465617601500165610ustar00rootroot00000000000000'''Convert AQI to and from Pollutant Concentrations''' EPA_TABLE = [ { 'iL': 0, 'iH': 50, 'breakpoints': { 'O3': { 'bL': 0.000, 'bH': 0.054 }, 'PM2.5': { 'bL': 0.0, 'bH': 12.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': 12.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': 150.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': 150.5, 'bH': 250.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': 400, 'breakpoints': { # Ozone here changes from 8h to 1h reporting 'O3': { 'bL': 0.405, 'bH': 0.504 }, 'PM2.5': { 'bL': 250.5, 'bH': 350.4 }, 'PM10': { 'bL': 425, 'bH': 504 }, 'CO': { 'bL': 30.5, 'bH': 40.4 }, 'SO2': { 'bL': 605, 'bH': 804 }, 'NO2': { 'bL': 1250, 'bH': 1649 }, }, }, { 'iL': 401, 'iH': 500, 'breakpoints': { 'O3': { 'bL': 0.505, 'bH': 0.604 }, 'PM2.5': { 'bL': 350.5, 'bH': 500.4 }, 'PM10': { 'bL': 505, 'bH': 604 }, 'CO': { 'bL': 40.5, 'bH': 50.4 }, 'SO2': { 'bL': 805, 'bH': 1004 }, 'NO2': { 'bL': 1650, '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, pollutant): '''Convert AQI (0-500) to Pollutant Concentration''' if aqi < 0 or aqi > 500: raise ValueError('AQI must be between 0 and 500') for row in EPA_TABLE: iL = row['iL'] iH = row['iH'] if iL <= aqi and iH >= 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, pollutant): '''Convert Pollutant Concentration to AQI''' if conc < 0: raise ValueError('Concentration must be positive') rounded = EPA_ROUND_FNS[pollutant](conc) for row in EPA_TABLE: bL = row['breakpoints'][pollutant]['bL'] bH = row['breakpoints'][pollutant]['bH'] if bL <= rounded and bH >= rounded: 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.2.2/pyairnow/errors.py000066400000000000000000000005261465617601500171400ustar00rootroot00000000000000'''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.2.2/pyairnow/forecast.py000066400000000000000000000041321465617601500174270ustar00rootroot00000000000000'''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.2.2/pyairnow/observation.py000066400000000000000000000024541465617601500201610ustar00rootroot00000000000000'''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, ) -> None: '''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.2.2/pyairnow/py.typed000066400000000000000000000000001465617601500167340ustar00rootroot00000000000000pyairnow-1.2.2/pyproject.toml000066400000000000000000000024041465617601500163130ustar00rootroot00000000000000[build-system] requires = ["poetry_core>=1.0.0"] build-backend = "poetry.core.masonry.api" [tool.poetry] name = "pyairnow" version = "1.2.2" 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.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "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.8.3" python = "^3.8" [tool.poetry.dev-dependencies] aioresponses = "^0.7.0" coverage = "^5.2.1" flake8 = "^3.8.3" pytest = "^6.0.2" pytest-asyncio = "^0.14.0" pytest-cov = "^2.10.1" [tool.setuptools.package-data] "pyairnow" = ["py.typed"] [tool.setuptools.packages.find] where = ["pyairnow"] pyairnow-1.2.2/requirements-dev.txt000066400000000000000000001241331465617601500174430ustar00rootroot00000000000000aiohttp==3.8.4 ; python_version >= "3.8" and python_version < "4.0" \ --hash=sha256:03543dcf98a6619254b409be2d22b51f21ec66272be4ebda7b04e6412e4b2e14 \ --hash=sha256:03baa76b730e4e15a45f81dfe29a8d910314143414e528737f8589ec60cf7391 \ --hash=sha256:0a63f03189a6fa7c900226e3ef5ba4d3bd047e18f445e69adbd65af433add5a2 \ --hash=sha256:10c8cefcff98fd9168cdd86c4da8b84baaa90bf2da2269c6161984e6737bf23e \ --hash=sha256:147ae376f14b55f4f3c2b118b95be50a369b89b38a971e80a17c3fd623f280c9 \ --hash=sha256:176a64b24c0935869d5bbc4c96e82f89f643bcdf08ec947701b9dbb3c956b7dd \ --hash=sha256:17b79c2963db82086229012cff93ea55196ed31f6493bb1ccd2c62f1724324e4 \ --hash=sha256:1a45865451439eb320784918617ba54b7a377e3501fb70402ab84d38c2cd891b \ --hash=sha256:1b3ea7edd2d24538959c1c1abf97c744d879d4e541d38305f9bd7d9b10c9ec41 \ --hash=sha256:22f6eab15b6db242499a16de87939a342f5a950ad0abaf1532038e2ce7d31567 \ --hash=sha256:3032dcb1c35bc330134a5b8a5d4f68c1a87252dfc6e1262c65a7e30e62298275 \ --hash=sha256:33587f26dcee66efb2fff3c177547bd0449ab7edf1b73a7f5dea1e38609a0c54 \ --hash=sha256:34ce9f93a4a68d1272d26030655dd1b58ff727b3ed2a33d80ec433561b03d67a \ --hash=sha256:3a80464982d41b1fbfe3154e440ba4904b71c1a53e9cd584098cd41efdb188ef \ --hash=sha256:3b90467ebc3d9fa5b0f9b6489dfb2c304a1db7b9946fa92aa76a831b9d587e99 \ --hash=sha256:3d89efa095ca7d442a6d0cbc755f9e08190ba40069b235c9886a8763b03785da \ --hash=sha256:3d8ef1a630519a26d6760bc695842579cb09e373c5f227a21b67dc3eb16cfea4 \ --hash=sha256:3f43255086fe25e36fd5ed8f2ee47477408a73ef00e804cb2b5cba4bf2ac7f5e \ --hash=sha256:40653609b3bf50611356e6b6554e3a331f6879fa7116f3959b20e3528783e699 \ --hash=sha256:41a86a69bb63bb2fc3dc9ad5ea9f10f1c9c8e282b471931be0268ddd09430b04 \ --hash=sha256:493f5bc2f8307286b7799c6d899d388bbaa7dfa6c4caf4f97ef7521b9cb13719 \ --hash=sha256:4a6cadebe132e90cefa77e45f2d2f1a4b2ce5c6b1bfc1656c1ddafcfe4ba8131 \ --hash=sha256:4c745b109057e7e5f1848c689ee4fb3a016c8d4d92da52b312f8a509f83aa05e \ --hash=sha256:4d347a172f866cd1d93126d9b239fcbe682acb39b48ee0873c73c933dd23bd0f \ --hash=sha256:4dac314662f4e2aa5009977b652d9b8db7121b46c38f2073bfeed9f4049732cd \ --hash=sha256:4ddaae3f3d32fc2cb4c53fab020b69a05c8ab1f02e0e59665c6f7a0d3a5be54f \ --hash=sha256:5393fb786a9e23e4799fec788e7e735de18052f83682ce2dfcabaf1c00c2c08e \ --hash=sha256:59f029a5f6e2d679296db7bee982bb3d20c088e52a2977e3175faf31d6fb75d1 \ --hash=sha256:5a7bdf9e57126dc345b683c3632e8ba317c31d2a41acd5800c10640387d193ed \ --hash=sha256:5b3f2e06a512e94722886c0827bee9807c86a9f698fac6b3aee841fab49bbfb4 \ --hash=sha256:5ce45967538fb747370308d3145aa68a074bdecb4f3a300869590f725ced69c1 \ --hash=sha256:5e14f25765a578a0a634d5f0cd1e2c3f53964553a00347998dfdf96b8137f777 \ --hash=sha256:618c901dd3aad4ace71dfa0f5e82e88b46ef57e3239fc7027773cb6d4ed53531 \ --hash=sha256:652b1bff4f15f6287550b4670546a2947f2a4575b6c6dff7760eafb22eacbf0b \ --hash=sha256:6c08e8ed6fa3d477e501ec9db169bfac8140e830aa372d77e4a43084d8dd91ab \ --hash=sha256:6ddb2a2026c3f6a68c3998a6c47ab6795e4127315d2e35a09997da21865757f8 \ --hash=sha256:6e601588f2b502c93c30cd5a45bfc665faaf37bbe835b7cfd461753068232074 \ --hash=sha256:6e74dd54f7239fcffe07913ff8b964e28b712f09846e20de78676ce2a3dc0bfc \ --hash=sha256:7235604476a76ef249bd64cb8274ed24ccf6995c4a8b51a237005ee7a57e8643 \ --hash=sha256:7ab43061a0c81198d88f39aaf90dae9a7744620978f7ef3e3708339b8ed2ef01 \ --hash=sha256:7c7837fe8037e96b6dd5cfcf47263c1620a9d332a87ec06a6ca4564e56bd0f36 \ --hash=sha256:80575ba9377c5171407a06d0196b2310b679dc752d02a1fcaa2bc20b235dbf24 \ --hash=sha256:80a37fe8f7c1e6ce8f2d9c411676e4bc633a8462844e38f46156d07a7d401654 \ --hash=sha256:8189c56eb0ddbb95bfadb8f60ea1b22fcfa659396ea36f6adcc521213cd7b44d \ --hash=sha256:854f422ac44af92bfe172d8e73229c270dc09b96535e8a548f99c84f82dde241 \ --hash=sha256:880e15bb6dad90549b43f796b391cfffd7af373f4646784795e20d92606b7a51 \ --hash=sha256:8b631e26df63e52f7cce0cce6507b7a7f1bc9b0c501fcde69742130b32e8782f \ --hash=sha256:8c29c77cc57e40f84acef9bfb904373a4e89a4e8b74e71aa8075c021ec9078c2 \ --hash=sha256:91f6d540163f90bbaef9387e65f18f73ffd7c79f5225ac3d3f61df7b0d01ad15 \ --hash=sha256:92c0cea74a2a81c4c76b62ea1cac163ecb20fb3ba3a75c909b9fa71b4ad493cf \ --hash=sha256:9bcb89336efa095ea21b30f9e686763f2be4478f1b0a616969551982c4ee4c3b \ --hash=sha256:a1f4689c9a1462f3df0a1f7e797791cd6b124ddbee2b570d34e7f38ade0e2c71 \ --hash=sha256:a3fec6a4cb5551721cdd70473eb009d90935b4063acc5f40905d40ecfea23e05 \ --hash=sha256:a5d794d1ae64e7753e405ba58e08fcfa73e3fad93ef9b7e31112ef3c9a0efb52 \ --hash=sha256:a86d42d7cba1cec432d47ab13b6637bee393a10f664c425ea7b305d1301ca1a3 \ --hash=sha256:adfbc22e87365a6e564c804c58fc44ff7727deea782d175c33602737b7feadb6 \ --hash=sha256:aeb29c84bb53a84b1a81c6c09d24cf33bb8432cc5c39979021cc0f98c1292a1a \ --hash=sha256:aede4df4eeb926c8fa70de46c340a1bc2c6079e1c40ccf7b0eae1313ffd33519 \ --hash=sha256:b744c33b6f14ca26b7544e8d8aadff6b765a80ad6164fb1a430bbadd593dfb1a \ --hash=sha256:b7a00a9ed8d6e725b55ef98b1b35c88013245f35f68b1b12c5cd4100dddac333 \ --hash=sha256:bb96fa6b56bb536c42d6a4a87dfca570ff8e52de2d63cabebfd6fb67049c34b6 \ --hash=sha256:bbcf1a76cf6f6dacf2c7f4d2ebd411438c275faa1dc0c68e46eb84eebd05dd7d \ --hash=sha256:bca5f24726e2919de94f047739d0a4fc01372801a3672708260546aa2601bf57 \ --hash=sha256:bf2e1a9162c1e441bf805a1fd166e249d574ca04e03b34f97e2928769e91ab5c \ --hash=sha256:c4eb3b82ca349cf6fadcdc7abcc8b3a50ab74a62e9113ab7a8ebc268aad35bb9 \ --hash=sha256:c6cc15d58053c76eacac5fa9152d7d84b8d67b3fde92709195cb984cfb3475ea \ --hash=sha256:c6cd05ea06daca6ad6a4ca3ba7fe7dc5b5de063ff4daec6170ec0f9979f6c332 \ --hash=sha256:c844fd628851c0bc309f3c801b3a3d58ce430b2ce5b359cd918a5a76d0b20cb5 \ --hash=sha256:c9cb1565a7ad52e096a6988e2ee0397f72fe056dadf75d17fa6b5aebaea05622 \ --hash=sha256:cab9401de3ea52b4b4c6971db5fb5c999bd4260898af972bf23de1c6b5dd9d71 \ --hash=sha256:cd468460eefef601ece4428d3cf4562459157c0f6523db89365202c31b6daebb \ --hash=sha256:d1e6a862b76f34395a985b3cd39a0d949ca80a70b6ebdea37d3ab39ceea6698a \ --hash=sha256:d1f9282c5f2b5e241034a009779e7b2a1aa045f667ff521e7948ea9b56e0c5ff \ --hash=sha256:d265f09a75a79a788237d7f9054f929ced2e69eb0bb79de3798c468d8a90f945 \ --hash=sha256:db3fc6120bce9f446d13b1b834ea5b15341ca9ff3f335e4a951a6ead31105480 \ --hash=sha256:dbf3a08a06b3f433013c143ebd72c15cac33d2914b8ea4bea7ac2c23578815d6 \ --hash=sha256:de04b491d0e5007ee1b63a309956eaed959a49f5bb4e84b26c8f5d49de140fa9 \ --hash=sha256:e4b09863aae0dc965c3ef36500d891a3ff495a2ea9ae9171e4519963c12ceefd \ --hash=sha256:e595432ac259af2d4630008bf638873d69346372d38255774c0e286951e8b79f \ --hash=sha256:e75b89ac3bd27d2d043b234aa7b734c38ba1b0e43f07787130a0ecac1e12228a \ --hash=sha256:ea9eb976ffdd79d0e893869cfe179a8f60f152d42cb64622fca418cd9b18dc2a \ --hash=sha256:eafb3e874816ebe2a92f5e155f17260034c8c341dad1df25672fb710627c6949 \ --hash=sha256:ee3c36df21b5714d49fc4580247947aa64bcbe2939d1b77b4c8dcb8f6c9faecc \ --hash=sha256:f352b62b45dff37b55ddd7b9c0c8672c4dd2eb9c0f9c11d395075a84e2c40f75 \ --hash=sha256:fabb87dd8850ef0f7fe2b366d44b77d7e6fa2ea87861ab3844da99291e81e60f \ --hash=sha256:fe11310ae1e4cd560035598c3f29d86cef39a83d244c7466f95c27ae04850f10 \ --hash=sha256:fe7ba4a51f33ab275515f66b0a236bcde4fb5561498fe8f898d4e549b2e4509f aioresponses==0.7.4 ; python_version >= "3.8" and python_version < "4.0" \ --hash=sha256:1160486b5ea96fcae6170cf2bdef029b9d3a283b7dbeabb3d7f1182769bfb6b7 \ --hash=sha256:9b8c108b36354c04633bad0ea752b55d956a7602fe3e3234b939fc44af96f1d8 aiosignal==1.3.1 ; python_version >= "3.8" and python_version < "4.0" \ --hash=sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc \ --hash=sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17 async-timeout==4.0.2 ; python_version >= "3.8" and python_version < "4.0" \ --hash=sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15 \ --hash=sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c atomicwrites==1.4.1 ; python_version >= "3.8" and python_version < "4.0" and sys_platform == "win32" \ --hash=sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11 attrs==22.2.0 ; python_version >= "3.8" and python_version < "4.0" \ --hash=sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836 \ --hash=sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99 charset-normalizer==3.0.1 ; python_version >= "3.8" and python_version < "4.0" \ --hash=sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b \ --hash=sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42 \ --hash=sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d \ --hash=sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b \ --hash=sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a \ --hash=sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59 \ --hash=sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154 \ --hash=sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1 \ --hash=sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c \ --hash=sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a \ --hash=sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d \ --hash=sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6 \ --hash=sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b \ --hash=sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b \ --hash=sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783 \ --hash=sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5 \ --hash=sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918 \ --hash=sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555 \ --hash=sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639 \ --hash=sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786 \ --hash=sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e \ --hash=sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed \ --hash=sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820 \ --hash=sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8 \ --hash=sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3 \ --hash=sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541 \ --hash=sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14 \ --hash=sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be \ --hash=sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e \ --hash=sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76 \ --hash=sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b \ --hash=sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c \ --hash=sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b \ --hash=sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3 \ --hash=sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc \ --hash=sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6 \ --hash=sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59 \ --hash=sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4 \ --hash=sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d \ --hash=sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d \ --hash=sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3 \ --hash=sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a \ --hash=sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea \ --hash=sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6 \ --hash=sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e \ --hash=sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603 \ --hash=sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24 \ --hash=sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a \ --hash=sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58 \ --hash=sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678 \ --hash=sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a \ --hash=sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c \ --hash=sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6 \ --hash=sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18 \ --hash=sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174 \ --hash=sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317 \ --hash=sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f \ --hash=sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc \ --hash=sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837 \ --hash=sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41 \ --hash=sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c \ --hash=sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579 \ --hash=sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753 \ --hash=sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8 \ --hash=sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291 \ --hash=sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087 \ --hash=sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866 \ --hash=sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3 \ --hash=sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d \ --hash=sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1 \ --hash=sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca \ --hash=sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e \ --hash=sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db \ --hash=sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72 \ --hash=sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d \ --hash=sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc \ --hash=sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539 \ --hash=sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d \ --hash=sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af \ --hash=sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b \ --hash=sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602 \ --hash=sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f \ --hash=sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478 \ --hash=sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c \ --hash=sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e \ --hash=sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479 \ --hash=sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7 \ --hash=sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8 colorama==0.4.6 ; python_version >= "3.8" and python_version < "4.0" and sys_platform == "win32" \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 coverage==5.5 ; python_version >= "3.8" and python_version < "4" \ --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==3.9.2 ; python_version >= "3.8" and python_version < "4.0" \ --hash=sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b \ --hash=sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907 frozenlist==1.3.3 ; python_version >= "3.8" and python_version < "4.0" \ --hash=sha256:008a054b75d77c995ea26629ab3a0c0d7281341f2fa7e1e85fa6153ae29ae99c \ --hash=sha256:02c9ac843e3390826a265e331105efeab489ffaf4dd86384595ee8ce6d35ae7f \ --hash=sha256:034a5c08d36649591be1cbb10e09da9f531034acfe29275fc5454a3b101ce41a \ --hash=sha256:05cdb16d09a0832eedf770cb7bd1fe57d8cf4eaf5aced29c4e41e3f20b30a784 \ --hash=sha256:0693c609e9742c66ba4870bcee1ad5ff35462d5ffec18710b4ac89337ff16e27 \ --hash=sha256:0771aed7f596c7d73444c847a1c16288937ef988dc04fb9f7be4b2aa91db609d \ --hash=sha256:0af2e7c87d35b38732e810befb9d797a99279cbb85374d42ea61c1e9d23094b3 \ --hash=sha256:14143ae966a6229350021384870458e4777d1eae4c28d1a7aa47f24d030e6678 \ --hash=sha256:180c00c66bde6146a860cbb81b54ee0df350d2daf13ca85b275123bbf85de18a \ --hash=sha256:1841e200fdafc3d51f974d9d377c079a0694a8f06de2e67b48150328d66d5483 \ --hash=sha256:23d16d9f477bb55b6154654e0e74557040575d9d19fe78a161bd33d7d76808e8 \ --hash=sha256:2b07ae0c1edaa0a36339ec6cce700f51b14a3fc6545fdd32930d2c83917332cf \ --hash=sha256:2c926450857408e42f0bbc295e84395722ce74bae69a3b2aa2a65fe22cb14b99 \ --hash=sha256:2e24900aa13212e75e5b366cb9065e78bbf3893d4baab6052d1aca10d46d944c \ --hash=sha256:303e04d422e9b911a09ad499b0368dc551e8c3cd15293c99160c7f1f07b59a48 \ --hash=sha256:352bd4c8c72d508778cf05ab491f6ef36149f4d0cb3c56b1b4302852255d05d5 \ --hash=sha256:3843f84a6c465a36559161e6c59dce2f2ac10943040c2fd021cfb70d58c4ad56 \ --hash=sha256:394c9c242113bfb4b9aa36e2b80a05ffa163a30691c7b5a29eba82e937895d5e \ --hash=sha256:3bbdf44855ed8f0fbcd102ef05ec3012d6a4fd7c7562403f76ce6a52aeffb2b1 \ --hash=sha256:40de71985e9042ca00b7953c4f41eabc3dc514a2d1ff534027f091bc74416401 \ --hash=sha256:41fe21dc74ad3a779c3d73a2786bdf622ea81234bdd4faf90b8b03cad0c2c0b4 \ --hash=sha256:47df36a9fe24054b950bbc2db630d508cca3aa27ed0566c0baf661225e52c18e \ --hash=sha256:4ea42116ceb6bb16dbb7d526e242cb6747b08b7710d9782aa3d6732bd8d27649 \ --hash=sha256:58bcc55721e8a90b88332d6cd441261ebb22342e238296bb330968952fbb3a6a \ --hash=sha256:5c11e43016b9024240212d2a65043b70ed8dfd3b52678a1271972702d990ac6d \ --hash=sha256:5cf820485f1b4c91e0417ea0afd41ce5cf5965011b3c22c400f6d144296ccbc0 \ --hash=sha256:5d8860749e813a6f65bad8285a0520607c9500caa23fea6ee407e63debcdbef6 \ --hash=sha256:6327eb8e419f7d9c38f333cde41b9ae348bec26d840927332f17e887a8dcb70d \ --hash=sha256:65a5e4d3aa679610ac6e3569e865425b23b372277f89b5ef06cf2cdaf1ebf22b \ --hash=sha256:66080ec69883597e4d026f2f71a231a1ee9887835902dbe6b6467d5a89216cf6 \ --hash=sha256:783263a4eaad7c49983fe4b2e7b53fa9770c136c270d2d4bbb6d2192bf4d9caf \ --hash=sha256:7f44e24fa70f6fbc74aeec3e971f60a14dde85da364aa87f15d1be94ae75aeef \ --hash=sha256:7fdfc24dcfce5b48109867c13b4cb15e4660e7bd7661741a391f821f23dfdca7 \ --hash=sha256:810860bb4bdce7557bc0febb84bbd88198b9dbc2022d8eebe5b3590b2ad6c842 \ --hash=sha256:841ea19b43d438a80b4de62ac6ab21cfe6827bb8a9dc62b896acc88eaf9cecba \ --hash=sha256:84610c1502b2461255b4c9b7d5e9c48052601a8957cd0aea6ec7a7a1e1fb9420 \ --hash=sha256:899c5e1928eec13fd6f6d8dc51be23f0d09c5281e40d9cf4273d188d9feeaf9b \ --hash=sha256:8bae29d60768bfa8fb92244b74502b18fae55a80eac13c88eb0b496d4268fd2d \ --hash=sha256:8df3de3a9ab8325f94f646609a66cbeeede263910c5c0de0101079ad541af332 \ --hash=sha256:8fa3c6e3305aa1146b59a09b32b2e04074945ffcfb2f0931836d103a2c38f936 \ --hash=sha256:924620eef691990dfb56dc4709f280f40baee568c794b5c1885800c3ecc69816 \ --hash=sha256:9309869032abb23d196cb4e4db574232abe8b8be1339026f489eeb34a4acfd91 \ --hash=sha256:9545a33965d0d377b0bc823dcabf26980e77f1b6a7caa368a365a9497fb09420 \ --hash=sha256:9ac5995f2b408017b0be26d4a1d7c61bce106ff3d9e3324374d66b5964325448 \ --hash=sha256:9bbbcedd75acdfecf2159663b87f1bb5cfc80e7cd99f7ddd9d66eb98b14a8411 \ --hash=sha256:a4ae8135b11652b08a8baf07631d3ebfe65a4c87909dbef5fa0cdde440444ee4 \ --hash=sha256:a6394d7dadd3cfe3f4b3b186e54d5d8504d44f2d58dcc89d693698e8b7132b32 \ --hash=sha256:a97b4fe50b5890d36300820abd305694cb865ddb7885049587a5678215782a6b \ --hash=sha256:ae4dc05c465a08a866b7a1baf360747078b362e6a6dbeb0c57f234db0ef88ae0 \ --hash=sha256:b1c63e8d377d039ac769cd0926558bb7068a1f7abb0f003e3717ee003ad85530 \ --hash=sha256:b1e2c1185858d7e10ff045c496bbf90ae752c28b365fef2c09cf0fa309291669 \ --hash=sha256:b4395e2f8d83fbe0c627b2b696acce67868793d7d9750e90e39592b3626691b7 \ --hash=sha256:b756072364347cb6aa5b60f9bc18e94b2f79632de3b0190253ad770c5df17db1 \ --hash=sha256:ba64dc2b3b7b158c6660d49cdb1d872d1d0bf4e42043ad8d5006099479a194e5 \ --hash=sha256:bed331fe18f58d844d39ceb398b77d6ac0b010d571cba8267c2e7165806b00ce \ --hash=sha256:c188512b43542b1e91cadc3c6c915a82a5eb95929134faf7fd109f14f9892ce4 \ --hash=sha256:c21b9aa40e08e4f63a2f92ff3748e6b6c84d717d033c7b3438dd3123ee18f70e \ --hash=sha256:ca713d4af15bae6e5d79b15c10c8522859a9a89d3b361a50b817c98c2fb402a2 \ --hash=sha256:cd4210baef299717db0a600d7a3cac81d46ef0e007f88c9335db79f8979c0d3d \ --hash=sha256:cfe33efc9cb900a4c46f91a5ceba26d6df370ffddd9ca386eb1d4f0ad97b9ea9 \ --hash=sha256:d5cd3ab21acbdb414bb6c31958d7b06b85eeb40f66463c264a9b343a4e238642 \ --hash=sha256:dfbac4c2dfcc082fcf8d942d1e49b6aa0766c19d3358bd86e2000bf0fa4a9cf0 \ --hash=sha256:e235688f42b36be2b6b06fc37ac2126a73b75fb8d6bc66dd632aa35286238703 \ --hash=sha256:eb82dbba47a8318e75f679690190c10a5e1f447fbf9df41cbc4c3afd726d88cb \ --hash=sha256:ebb86518203e12e96af765ee89034a1dbb0c3c65052d1b0c19bbbd6af8a145e1 \ --hash=sha256:ee78feb9d293c323b59a6f2dd441b63339a30edf35abcb51187d2fc26e696d13 \ --hash=sha256:eedab4c310c0299961ac285591acd53dc6723a1ebd90a57207c71f6e0c2153ab \ --hash=sha256:efa568b885bca461f7c7b9e032655c0c143d305bf01c30caf6db2854a4532b38 \ --hash=sha256:efce6ae830831ab6a22b9b4091d411698145cb9b8fc869e1397ccf4b4b6455cb \ --hash=sha256:f163d2fd041c630fed01bc48d28c3ed4a3b003c00acd396900e11ee5316b56bb \ --hash=sha256:f20380df709d91525e4bee04746ba612a4df0972c1b8f8e1e8af997e678c7b81 \ --hash=sha256:f30f1928162e189091cf4d9da2eac617bfe78ef907a761614ff577ef4edfb3c8 \ --hash=sha256:f470c92737afa7d4c3aacc001e335062d582053d4dbe73cda126f2d7031068dd \ --hash=sha256:ff8bf625fe85e119553b5383ba0fb6aa3d0ec2ae980295aaefa552374926b3f4 idna==3.4 ; python_version >= "3.8" and python_version < "4.0" \ --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \ --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2 iniconfig==2.0.0 ; python_version >= "3.8" and python_version < "4.0" \ --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 mccabe==0.6.1 ; python_version >= "3.8" and python_version < "4.0" \ --hash=sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42 \ --hash=sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f multidict==6.0.4 ; python_version >= "3.8" and python_version < "4.0" \ --hash=sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9 \ --hash=sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8 \ --hash=sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03 \ --hash=sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710 \ --hash=sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161 \ --hash=sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664 \ --hash=sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569 \ --hash=sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067 \ --hash=sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313 \ --hash=sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706 \ --hash=sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2 \ --hash=sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636 \ --hash=sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49 \ --hash=sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93 \ --hash=sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603 \ --hash=sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0 \ --hash=sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60 \ --hash=sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4 \ --hash=sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e \ --hash=sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1 \ --hash=sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60 \ --hash=sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951 \ --hash=sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc \ --hash=sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe \ --hash=sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95 \ --hash=sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d \ --hash=sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8 \ --hash=sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed \ --hash=sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2 \ --hash=sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775 \ --hash=sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87 \ --hash=sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c \ --hash=sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2 \ --hash=sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98 \ --hash=sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3 \ --hash=sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe \ --hash=sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78 \ --hash=sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660 \ --hash=sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176 \ --hash=sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e \ --hash=sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988 \ --hash=sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c \ --hash=sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c \ --hash=sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0 \ --hash=sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449 \ --hash=sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f \ --hash=sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde \ --hash=sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5 \ --hash=sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d \ --hash=sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac \ --hash=sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a \ --hash=sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9 \ --hash=sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca \ --hash=sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11 \ --hash=sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35 \ --hash=sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063 \ --hash=sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b \ --hash=sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982 \ --hash=sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258 \ --hash=sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1 \ --hash=sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52 \ --hash=sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480 \ --hash=sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7 \ --hash=sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461 \ --hash=sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d \ --hash=sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc \ --hash=sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779 \ --hash=sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a \ --hash=sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547 \ --hash=sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0 \ --hash=sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171 \ --hash=sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf \ --hash=sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d \ --hash=sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba packaging==23.0 ; python_version >= "3.8" and python_version < "4.0" \ --hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2 \ --hash=sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97 pluggy==1.0.0 ; python_version >= "3.8" and python_version < "4.0" \ --hash=sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159 \ --hash=sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3 py==1.11.0 ; python_version >= "3.8" and python_version < "4.0" \ --hash=sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719 \ --hash=sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378 pycodestyle==2.7.0 ; python_version >= "3.8" and python_version < "4.0" \ --hash=sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068 \ --hash=sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef pyflakes==2.3.1 ; python_version >= "3.8" and python_version < "4.0" \ --hash=sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3 \ --hash=sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db pytest-asyncio==0.14.0 ; python_version >= "3.8" and python_version < "4.0" \ --hash=sha256:2eae1e34f6c68fc0a9dc12d4bea190483843ff4708d24277c41568d6b6044f1d \ --hash=sha256:9882c0c6b24429449f5f969a5158b528f39bde47dc32e85b9f0403965017e700 pytest-cov==2.12.1 ; python_version >= "3.8" and python_version < "4.0" \ --hash=sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a \ --hash=sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7 pytest==6.2.5 ; python_version >= "3.8" and python_version < "4.0" \ --hash=sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89 \ --hash=sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134 toml==0.10.2 ; python_version >= "3.8" and python_version < "4.0" \ --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \ --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f yarl==1.8.2 ; python_version >= "3.8" and python_version < "4.0" \ --hash=sha256:009a028127e0a1755c38b03244c0bea9d5565630db9c4cf9572496e947137a87 \ --hash=sha256:0414fd91ce0b763d4eadb4456795b307a71524dbacd015c657bb2a39db2eab89 \ --hash=sha256:0978f29222e649c351b173da2b9b4665ad1feb8d1daa9d971eb90df08702668a \ --hash=sha256:0ef8fb25e52663a1c85d608f6dd72e19bd390e2ecaf29c17fb08f730226e3a08 \ --hash=sha256:10b08293cda921157f1e7c2790999d903b3fd28cd5c208cf8826b3b508026996 \ --hash=sha256:1684a9bd9077e922300ecd48003ddae7a7474e0412bea38d4631443a91d61077 \ --hash=sha256:1b372aad2b5f81db66ee7ec085cbad72c4da660d994e8e590c997e9b01e44901 \ --hash=sha256:1e21fb44e1eff06dd6ef971d4bdc611807d6bd3691223d9c01a18cec3677939e \ --hash=sha256:2305517e332a862ef75be8fad3606ea10108662bc6fe08509d5ca99503ac2aee \ --hash=sha256:24ad1d10c9db1953291f56b5fe76203977f1ed05f82d09ec97acb623a7976574 \ --hash=sha256:272b4f1599f1b621bf2aabe4e5b54f39a933971f4e7c9aa311d6d7dc06965165 \ --hash=sha256:2a1fca9588f360036242f379bfea2b8b44cae2721859b1c56d033adfd5893634 \ --hash=sha256:2b4fa2606adf392051d990c3b3877d768771adc3faf2e117b9de7eb977741229 \ --hash=sha256:3150078118f62371375e1e69b13b48288e44f6691c1069340081c3fd12c94d5b \ --hash=sha256:326dd1d3caf910cd26a26ccbfb84c03b608ba32499b5d6eeb09252c920bcbe4f \ --hash=sha256:34c09b43bd538bf6c4b891ecce94b6fa4f1f10663a8d4ca589a079a5018f6ed7 \ --hash=sha256:388a45dc77198b2460eac0aca1efd6a7c09e976ee768b0d5109173e521a19daf \ --hash=sha256:3adeef150d528ded2a8e734ebf9ae2e658f4c49bf413f5f157a470e17a4a2e89 \ --hash=sha256:3edac5d74bb3209c418805bda77f973117836e1de7c000e9755e572c1f7850d0 \ --hash=sha256:3f6b4aca43b602ba0f1459de647af954769919c4714706be36af670a5f44c9c1 \ --hash=sha256:3fc056e35fa6fba63248d93ff6e672c096f95f7836938241ebc8260e062832fe \ --hash=sha256:418857f837347e8aaef682679f41e36c24250097f9e2f315d39bae3a99a34cbf \ --hash=sha256:42430ff511571940d51e75cf42f1e4dbdded477e71c1b7a17f4da76c1da8ea76 \ --hash=sha256:44ceac0450e648de86da8e42674f9b7077d763ea80c8ceb9d1c3e41f0f0a9951 \ --hash=sha256:47d49ac96156f0928f002e2424299b2c91d9db73e08c4cd6742923a086f1c863 \ --hash=sha256:48dd18adcf98ea9cd721a25313aef49d70d413a999d7d89df44f469edfb38a06 \ --hash=sha256:49d43402c6e3013ad0978602bf6bf5328535c48d192304b91b97a3c6790b1562 \ --hash=sha256:4d04acba75c72e6eb90745447d69f84e6c9056390f7a9724605ca9c56b4afcc6 \ --hash=sha256:57a7c87927a468e5a1dc60c17caf9597161d66457a34273ab1760219953f7f4c \ --hash=sha256:58a3c13d1c3005dbbac5c9f0d3210b60220a65a999b1833aa46bd6677c69b08e \ --hash=sha256:5df5e3d04101c1e5c3b1d69710b0574171cc02fddc4b23d1b2813e75f35a30b1 \ --hash=sha256:63243b21c6e28ec2375f932a10ce7eda65139b5b854c0f6b82ed945ba526bff3 \ --hash=sha256:64dd68a92cab699a233641f5929a40f02a4ede8c009068ca8aa1fe87b8c20ae3 \ --hash=sha256:6604711362f2dbf7160df21c416f81fac0de6dbcf0b5445a2ef25478ecc4c778 \ --hash=sha256:6c4fcfa71e2c6a3cb568cf81aadc12768b9995323186a10827beccf5fa23d4f8 \ --hash=sha256:6d88056a04860a98341a0cf53e950e3ac9f4e51d1b6f61a53b0609df342cc8b2 \ --hash=sha256:705227dccbe96ab02c7cb2c43e1228e2826e7ead880bb19ec94ef279e9555b5b \ --hash=sha256:728be34f70a190566d20aa13dc1f01dc44b6aa74580e10a3fb159691bc76909d \ --hash=sha256:74dece2bfc60f0f70907c34b857ee98f2c6dd0f75185db133770cd67300d505f \ --hash=sha256:75c16b2a900b3536dfc7014905a128a2bea8fb01f9ee26d2d7d8db0a08e7cb2c \ --hash=sha256:77e913b846a6b9c5f767b14dc1e759e5aff05502fe73079f6f4176359d832581 \ --hash=sha256:7a66c506ec67eb3159eea5096acd05f5e788ceec7b96087d30c7d2865a243918 \ --hash=sha256:8c46d3d89902c393a1d1e243ac847e0442d0196bbd81aecc94fcebbc2fd5857c \ --hash=sha256:93202666046d9edadfe9f2e7bf5e0782ea0d497b6d63da322e541665d65a044e \ --hash=sha256:97209cc91189b48e7cfe777237c04af8e7cc51eb369004e061809bcdf4e55220 \ --hash=sha256:a48f4f7fea9a51098b02209d90297ac324241bf37ff6be6d2b0149ab2bd51b37 \ --hash=sha256:a783cd344113cb88c5ff7ca32f1f16532a6f2142185147822187913eb989f739 \ --hash=sha256:ae0eec05ab49e91a78700761777f284c2df119376e391db42c38ab46fd662b77 \ --hash=sha256:ae4d7ff1049f36accde9e1ef7301912a751e5bae0a9d142459646114c70ecba6 \ --hash=sha256:b05df9ea7496df11b710081bd90ecc3a3db6adb4fee36f6a411e7bc91a18aa42 \ --hash=sha256:baf211dcad448a87a0d9047dc8282d7de59473ade7d7fdf22150b1d23859f946 \ --hash=sha256:bb81f753c815f6b8e2ddd2eef3c855cf7da193b82396ac013c661aaa6cc6b0a5 \ --hash=sha256:bcd7bb1e5c45274af9a1dd7494d3c52b2be5e6bd8d7e49c612705fd45420b12d \ --hash=sha256:bf071f797aec5b96abfc735ab97da9fd8f8768b43ce2abd85356a3127909d146 \ --hash=sha256:c15163b6125db87c8f53c98baa5e785782078fbd2dbeaa04c6141935eb6dab7a \ --hash=sha256:cb6d48d80a41f68de41212f3dfd1a9d9898d7841c8f7ce6696cf2fd9cb57ef83 \ --hash=sha256:ceff9722e0df2e0a9e8a79c610842004fa54e5b309fe6d218e47cd52f791d7ef \ --hash=sha256:cfa2bbca929aa742b5084fd4663dd4b87c191c844326fcb21c3afd2d11497f80 \ --hash=sha256:d617c241c8c3ad5c4e78a08429fa49e4b04bedfc507b34b4d8dceb83b4af3588 \ --hash=sha256:d881d152ae0007809c2c02e22aa534e702f12071e6b285e90945aa3c376463c5 \ --hash=sha256:da65c3f263729e47351261351b8679c6429151ef9649bba08ef2528ff2c423b2 \ --hash=sha256:de986979bbd87272fe557e0a8fcb66fd40ae2ddfe28a8b1ce4eae22681728fef \ --hash=sha256:df60a94d332158b444301c7f569659c926168e4d4aad2cfbf4bce0e8fb8be826 \ --hash=sha256:dfef7350ee369197106805e193d420b75467b6cceac646ea5ed3049fcc950a05 \ --hash=sha256:e59399dda559688461762800d7fb34d9e8a6a7444fd76ec33220a926c8be1516 \ --hash=sha256:e6f3515aafe0209dd17fb9bdd3b4e892963370b3de781f53e1746a521fb39fc0 \ --hash=sha256:e7fd20d6576c10306dea2d6a5765f46f0ac5d6f53436217913e952d19237efc4 \ --hash=sha256:ebb78745273e51b9832ef90c0898501006670d6e059f2cdb0e999494eb1450c2 \ --hash=sha256:efff27bd8cbe1f9bd127e7894942ccc20c857aa8b5a0327874f30201e5ce83d0 \ --hash=sha256:f37db05c6051eff17bc832914fe46869f8849de5b92dc4a3466cd63095d23dfd \ --hash=sha256:f8ca8ad414c85bbc50f49c0a106f951613dfa5f948ab69c10ce9b128d368baf8 \ --hash=sha256:fb742dcdd5eec9f26b61224c23baea46c9055cf16f62475e11b9b15dfd5c117b \ --hash=sha256:fc77086ce244453e074e445104f0ecb27530d6fd3a46698e33f6c38951d5a0f1 \ --hash=sha256:ff205b58dc2929191f68162633d5e10e8044398d7a45265f90a0f1d51f85f72c pyairnow-1.2.2/tests/000077500000000000000000000000001465617601500145415ustar00rootroot00000000000000pyairnow-1.2.2/tests/__init__.py000066400000000000000000000000001465617601500166400ustar00rootroot00000000000000pyairnow-1.2.2/tests/conftest.py000066400000000000000000000006311465617601500167400ustar00rootroot00000000000000'''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.2.2/tests/mock_api.py000066400000000000000000000052061465617601500167000ustar00rootroot00000000000000'''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.2.2/tests/test_10_api.py000066400000000000000000000017721465617601500172320ustar00rootroot00000000000000import 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.2.2/tests/test_11_errors.py000066400000000000000000000041351465617601500177720ustar00rootroot00000000000000import 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.2.2/tests/test_12_forecast.py000066400000000000000000000141111465617601500202600ustar00rootroot00000000000000import 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.2.2/tests/test_13_observations.py000066400000000000000000000045141465617601500211770ustar00rootroot00000000000000import 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.2.2/tests/test_14_conversions.py000066400000000000000000000022241465617601500210260ustar00rootroot00000000000000import 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') == 6 assert aqi_to_concentration(50, 'PM2.5') == 12 assert aqi_to_concentration(51, 'PM2.5') == 12.1 assert aqi_to_concentration(86, 'PM2.5') == 28.7 assert aqi_to_concentration(144, 'PM2.5') == 53.0 assert concentration_to_aqi(0, 'PM2.5') == 0 assert concentration_to_aqi(12.0, 'PM2.5') == 50 assert concentration_to_aqi(12.1, 'PM2.5') == 51 assert concentration_to_aqi(28.66, 'PM2.5') == 86 assert concentration_to_aqi(53.0, 'PM2.5') == 144 def test_convert_invalid_negative_aqi(): with pytest.raises(ValueError): aqi_to_concentration(-1, 'PM2.5') def test_convert_invalid_overrange_aqi(): with pytest.raises(ValueError): aqi_to_concentration(501, 'PM2.5') def test_convert_invalid_negative_o3(): with pytest.raises(ValueError): concentration_to_aqi(-0.0008, 'O3') def test_convert_invalid_overrange_o3(): with pytest.raises(ValueError): concentration_to_aqi(0.605, 'O3') pyairnow-1.2.2/tox.ini000066400000000000000000000001111465617601500147030ustar00rootroot00000000000000[flake8] exclude = .venv extend-ignore = E201,E202,E272 pyairnow/conv.py