├── tests ├── __init__.py ├── utils.py ├── mokcs.py ├── test_base.py ├── test_forecast.py └── data.py ├── darksky ├── __init__.py ├── types │ ├── __init__.py │ ├── units.py │ ├── weather.py │ └── languages.py ├── exceptions.py ├── utils.py ├── base.py ├── request_manager.py ├── forecast.py └── api.py ├── .coveragerc ├── .flake8 ├── .isort.cfg ├── requirements-ci.txt ├── Makefile ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── setup.py ├── LICENSE ├── .gitignore ├── .circleci └── config.yml ├── README.md └── .pylintrc /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /darksky/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /darksky/types/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source=darksky 3 | omit=venv/* -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = venv/*,tests/data.py -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | line_length=79 3 | skip=venv -------------------------------------------------------------------------------- /darksky/types/units.py: -------------------------------------------------------------------------------- 1 | AUTO = 'auto' 2 | CA = 'ca' 3 | UK2 = 'uk2' 4 | US = 'us' 5 | SI = 'si' 6 | -------------------------------------------------------------------------------- /darksky/types/weather.py: -------------------------------------------------------------------------------- 1 | CURRENTLY = 'currently' 2 | MINUTELY = 'minutely' 3 | HOURLY = 'hourly' 4 | DAILY = 'daily' 5 | ALERTS = 'alerts' 6 | FLAGS = 'flags' 7 | -------------------------------------------------------------------------------- /requirements-ci.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.5.4 2 | requests==2.21.0 3 | pytz==2019.1 4 | pytest-asyncio==0.10.0 5 | aioresponses==0.6.0 6 | mock==3.0.5 7 | pytest==5.0.1 8 | yarl==1.3.0 9 | codecov==2.0.15 10 | pytest-cov==2.8.1 11 | flake8==3.7.9 12 | pylint==2.4.4 13 | isort==4.3.21 14 | pytest-cov==2.8.1 15 | pytest-html==2.0.1 -------------------------------------------------------------------------------- /darksky/exceptions.py: -------------------------------------------------------------------------------- 1 | 2 | class DarkSkyException(Exception): 3 | def __init__(self, code, msg): # pylint: disable=W0231 4 | self.code = code 5 | self.msg = msg 6 | 7 | def __str__(self): 8 | return 'Error[{code}]: {msg}'.format( 9 | code=self.code, 10 | msg=self.msg 11 | ) 12 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | def snake_case_key(key: str) -> str: 2 | assert isinstance(key, str) 3 | new_key = key[0] 4 | for char in key[1:]: 5 | if char.isupper(): 6 | new_key += "_{char}".format(char=char.lower()) 7 | elif char == "-": 8 | new_key += "__" 9 | else: 10 | new_key += char 11 | return new_key 12 | -------------------------------------------------------------------------------- /tests/mokcs.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | from .data import DATA 4 | 5 | 6 | class MockSession: 7 | def __init__(self): 8 | self.headers = {} 9 | self.auth = None 10 | self.mock_json = None 11 | 12 | def get(self, url, params=None): 13 | return self 14 | 15 | def json(self): # pylint: disable=R0201 16 | return copy.deepcopy(DATA) 17 | -------------------------------------------------------------------------------- /darksky/utils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from pytz import UTC, timezone 4 | 5 | 6 | def undo_snake_case_key(key: str) -> str: 7 | assert isinstance(key, str) 8 | new_key = key.split('__') 9 | int_key = '-'.join(new_key) 10 | new_key = int_key.split('_') 11 | return new_key[0] + ''.join([it.title() for it in new_key[1:]]) 12 | 13 | 14 | def get_datetime_from_unix(value: int, tz: timezone = UTC) -> datetime: 15 | if not isinstance(value, int): 16 | return None 17 | return datetime.fromtimestamp(value, tz=UTC).astimezone(tz) 18 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PACKAGES = $$(python -c "from setuptools import find_packages; print(' '.join({p.split('.')[0] + '/' for p in find_packages()}))") 2 | 3 | test: 4 | python -m pytest -s -v --html="artefacts/pytest/pytest.html" --self-contained-html --cov=./ --cov-report html:artefacts/coverage_html . 5 | 6 | style: 7 | isort -y 8 | autopep8 --in-place --aggressive --aggressive --recursive --exclude=venv . 9 | 10 | lint-pylint: 11 | pylint -v --output-format=parseable $(PACKAGES) -j 0 12 | 13 | lint-flake8: 14 | flake8 --config=".flake8" 15 | 16 | lint-isort: 17 | isort --check-only 18 | 19 | lint: lint-pylint lint-flake8 lint-isort 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: Detrous 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: Detrous 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Error Messages/Program Output** 24 | If applicable, add error messages or output to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. Ubuntu 18.04] 28 | - Dark Sky Version [e.g. 1.9.0 (`pip3 freeze | grep darksky-weather`)] 29 | - Python Version: [e.g. 3.7.4 (`python3 --version`)] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import find_packages, setup 4 | 5 | __version__ = "1.9.0" 6 | 7 | 8 | with open(os.path.join( 9 | os.path.abspath(os.path.dirname(__file__)), "README.md") 10 | ) as f: 11 | README = f.read() 12 | 13 | repo_url = "https://github.com/Detrous/darksky" 14 | setup( 15 | version=__version__, 16 | name="darksky_weather", 17 | packages=find_packages(), 18 | install_requires=["requests==2.21.0", "pytz==2019.1", "aiohttp==3.5.4"], 19 | description="The Dark Sky API wrapper", 20 | long_description="View on github", 21 | author="Detrous", 22 | author_email="detrous@protonmail.com", 23 | url=repo_url, 24 | download_url=f"{repo_url}/archive/{__version__}.tar.gz", 25 | license="GPLv3 License", 26 | classifiers=[ 27 | "Environment :: Web Environment", 28 | "Intended Audience :: Developers", 29 | "License :: OSI Approved :: MIT License", 30 | "Programming Language :: Python :: 3.6", 31 | "Topic :: Software Development :: Libraries :: Python Modules", 32 | ], 33 | ) 34 | -------------------------------------------------------------------------------- /darksky/types/languages.py: -------------------------------------------------------------------------------- 1 | ARABIC = 'ar' 2 | AZERBAIJANI = 'az' 3 | BELARUSIAN = 'be' 4 | BULGARIAN = 'bg' 5 | BENGALI = 'bn' 6 | BOSNIAN = 'bs' 7 | CATALAN = 'ca' 8 | CZECH = 'cs' 9 | DANISH = 'da' 10 | GERMAN = 'de' 11 | GREEK = 'el' 12 | ENGLISH = 'en' 13 | ESPERANTO = 'eo' 14 | SPANISH = 'es' 15 | ESTONIAN = 'et' 16 | FINNISH = 'fi' 17 | FRENCH = 'fr' 18 | HEBREW = 'he' 19 | HINDI = 'hi' 20 | CROATIAN = 'hr' 21 | HUNGARIAN = 'hu' 22 | INDONESIAN = 'id' 23 | ICELANDIC = 'is' 24 | ITALIAN = 'it' 25 | JAPANESE = 'ja' 26 | GEORGIAN = 'ka' 27 | KANNADA = 'kn' 28 | KOREAN = 'ko' 29 | CORNISH = 'kw' 30 | LATVIAN = 'lv' 31 | MALAYAM = 'ml' 32 | MARATHI = 'mr' 33 | NORWEGIAN_BOKMAL = 'nb' 34 | DUTCH = 'nl' 35 | NORWEGIAN_BOKMAL_NB = 'no' 36 | PUNJABI = 'pa' 37 | POLISH = 'pl' 38 | PORTUGUESE = 'pt' 39 | ROMANIAN = 'ro' 40 | RUSSIAN = 'ru' 41 | SLOVAK = 'sk' 42 | SLOVENIAN = 'sl' 43 | SERBIAN = 'sr' 44 | SWEDISH = 'sv' 45 | TAMIL = 'ta' 46 | TELUGU = 'te' 47 | TETUM = 'tet' 48 | TURKISH = 'tr' 49 | UKRAINIAN = 'uk' 50 | URDU = 'ur' 51 | IGPAY_ATINLAY = 'x-pig-latin' 52 | SIMPLIFIED_CHINESE = 'zh' 53 | TRADITIONAL_CHINESE = 'zh-tw' 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019-present Artyom (detrous) Slobodyan. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /darksky/base.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | import pytz 4 | 5 | from .utils import get_datetime_from_unix, undo_snake_case_key 6 | 7 | 8 | class BaseWeather: 9 | summary: str 10 | icon: str 11 | data_class: object 12 | 13 | def __init__(self, summary=None, icon=None, data=None, timezone=None): 14 | self.summary = summary 15 | self.icon = icon 16 | 17 | assert self.data_class is not None 18 | self.data = [self.data_class(timezone=timezone, **item) 19 | for item in (data or [])] 20 | 21 | def __repr__(self): 22 | return '%s([%d])' % (self.__class__.__name__, len(self.data)) 23 | 24 | def __iter__(self): 25 | return iter(self.data) 26 | 27 | 28 | class AutoInit: 29 | 30 | def __init__(self, **params): 31 | try: 32 | timezone = pytz.timezone(params.pop('timezone', None)) 33 | except (pytz.UnknownTimeZoneError, AttributeError): 34 | timezone = pytz.UTC 35 | 36 | for field in self.__annotations__: 37 | api_field = undo_snake_case_key(field) 38 | if self.__annotations__[field] == datetime: 39 | params[api_field] = get_datetime_from_unix( 40 | params.get(api_field), 41 | timezone 42 | ) 43 | 44 | if api_field in params: 45 | setattr(self, field, params.get(api_field)) 46 | else: 47 | setattr(self, field, None) 48 | 49 | def __iter__(self): 50 | return iter(self.__dict__.items()) 51 | 52 | def __repr__(self): 53 | return '%s(%s)' % (self.__class__.__name__, getattr(self, 'time', '')) 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | artefacts 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | .vscode 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # celery beat schedule file 82 | celerybeat-schedule 83 | 84 | # SageMath parsed files 85 | *.sage.py 86 | 87 | # Environments 88 | .env 89 | .venv 90 | env/ 91 | venv/ 92 | ENV/ 93 | env.bak/ 94 | venv.bak/ 95 | 96 | # Spyder project settings 97 | .spyderproject 98 | .spyproject 99 | 100 | # Rope project settings 101 | .ropeproject 102 | 103 | # mkdocs documentation 104 | /site 105 | 106 | # mypy 107 | .mypy_cache/ 108 | -------------------------------------------------------------------------------- /darksky/request_manager.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from aiohttp import ClientSession 3 | 4 | from .exceptions import DarkSkyException 5 | 6 | 7 | class BaseRequestManger: 8 | def __init__(self, gzip: bool): 9 | self.headers = {} if not gzip else {"Accept-Encoding": "gzip"} 10 | 11 | def make_request(self, url: str, **params): 12 | raise NotImplementedError 13 | 14 | 15 | class RequestManger(BaseRequestManger): 16 | def __init__(self, gzip: bool): 17 | super().__init__(gzip) 18 | self.session = requests.Session() 19 | self.session.headers = self.headers 20 | 21 | def make_request(self, url: str, **params): 22 | response = self.session.get(url, params=params).json() 23 | if "error" in response: 24 | raise DarkSkyException(response["code"], response["error"]) 25 | response["timezone"] = params.get("timezone") or response["timezone"] 26 | return response 27 | 28 | 29 | class RequestMangerAsync(BaseRequestManger): 30 | async def make_request( 31 | self, 32 | url: str, 33 | session: ClientSession, 34 | **params 35 | ): 36 | assert isinstance(session, ClientSession) 37 | 38 | for key in list(params.keys()): 39 | if params[key] is None: 40 | del params[key] 41 | elif isinstance(params[key], list): 42 | params[key] = ",".join(params[key]) 43 | 44 | async with session.get( 45 | url, params=params, headers=self.headers 46 | ) as resp: 47 | response = await resp.json() 48 | if "error" in response: 49 | raise DarkSkyException(response["code"], response["error"]) 50 | response["timezone"] = params.get("timezone") or response["timezone"] 51 | return response 52 | -------------------------------------------------------------------------------- /tests/test_base.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from typing import List 4 | 5 | import pytest 6 | 7 | from darksky.base import AutoInit, BaseWeather 8 | from darksky.utils import undo_snake_case_key 9 | 10 | sys.path.insert( 11 | 0, 12 | os.path.realpath( 13 | os.path.join(os.path.dirname(__file__), "..") 14 | ) 15 | ) 16 | 17 | 18 | def test_undo_snake_case_key(): 19 | undo_key = undo_snake_case_key("snake_key") 20 | assert undo_key == "snakeKey" 21 | 22 | 23 | def test_undo_snake_case_key_one_item(): 24 | undo_key = undo_snake_case_key("key") 25 | assert undo_key == "key" 26 | 27 | 28 | def test_undo_snake_case_key_bad_value_type(): 29 | with pytest.raises(AssertionError): 30 | undo_snake_case_key(1) 31 | 32 | 33 | def test_base_weather(): 34 | class TestDataBaseWeather: 35 | def __init__(self, test_field, **kwargs): 36 | self.test_field = test_field 37 | 38 | class TestBaseWeather(BaseWeather): 39 | data: List[TestDataBaseWeather] 40 | data_class = TestDataBaseWeather 41 | 42 | test_base_weather_obj = TestBaseWeather( 43 | "summary", "icon", data=[{"test_field": "data"}] 44 | ) 45 | 46 | assert test_base_weather_obj.summary == "summary" 47 | assert test_base_weather_obj.icon == "icon" 48 | assert test_base_weather_obj.data[0].test_field == "data" 49 | 50 | 51 | def test_auto_init(): 52 | class TestAutoInit(AutoInit): 53 | field: str 54 | 55 | test_auto_init_obj = TestAutoInit(field="data") 56 | assert test_auto_init_obj.field == "data" 57 | 58 | 59 | def test_auto_init__field_exists_on_class_but_is_not_given_in_constructor(): 60 | class TestAutoInit(AutoInit): 61 | field: str 62 | other_field: int 63 | 64 | test_auto_init_obj = TestAutoInit(field="data") 65 | assert test_auto_init_obj.field == "data" 66 | assert test_auto_init_obj.other_field is None 67 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | deps: 4 | docker: 5 | - image: circleci/python:3.6 6 | steps: 7 | - checkout 8 | - restore_cache: 9 | keys: 10 | - deps-cache-{{ checksum "requirements-ci.txt" }} 11 | - run: 12 | command: | 13 | python3 -m venv venv 14 | . venv/bin/activate 15 | pip install -r requirements-ci.txt 16 | - save_cache: 17 | key: deps-cache-{{ checksum "requirements-ci.txt" }} 18 | paths: 19 | - venv 20 | linters: 21 | docker: 22 | - image: circleci/python:3.6 23 | steps: 24 | - checkout 25 | - restore_cache: 26 | keys: 27 | - deps-cache-{{ checksum "requirements-ci.txt" }} 28 | - run: 29 | command: | 30 | python3 -m venv venv 31 | - run: 32 | command: | 33 | . venv/bin/activate 34 | export PACKAGES=$(python -c "from setuptools import find_packages; print(' '.join({p.split('.')[0] + '/' for p in find_packages()}))") 35 | isort --check-only 36 | pylint -v --output-format=parseable $PACKAGES -j 0 37 | flake8 --config=".flake8" 38 | tests: 39 | docker: 40 | - image: circleci/python:3.6 41 | steps: 42 | - checkout 43 | - restore_cache: 44 | keys: 45 | - deps-cache-{{ checksum "requirements-ci.txt" }} 46 | - run: 47 | command: | 48 | python3 -m venv venv 49 | - run: 50 | command: | 51 | . venv/bin/activate 52 | pytest --cov=./ -s -v 53 | codecov --token=$CODECOV_TOKEN 54 | isort -y 55 | deploy: 56 | docker: 57 | - image: circleci/python:3.6 58 | steps: 59 | - checkout 60 | - run: 61 | command: | 62 | python3 -m venv venv 63 | - run: 64 | name: init .pypirc 65 | command: | 66 | echo -e "[distutils]" >> ~/.pypirc 67 | echo -e "index-servers =" >> ~/.pypirc 68 | echo -e " pypi" >> ~/.pypirc 69 | echo -e "[pypi]" >> ~/.pypirc 70 | echo -e "repository=https://pypi.python.org/pypi" >> ~/.pypirc 71 | echo -e "username = $PYPI_USERNAME" >> ~/.pypirc 72 | echo -e "password = $PYPI_PASSWORD" >> ~/.pypirc 73 | - run: 74 | name: create packages 75 | command: | 76 | . venv/bin/activate 77 | python setup.py sdist upload -r pypi 78 | workflows: 79 | version: 2 80 | build_and_deploy: 81 | jobs: 82 | - deps 83 | - linters: 84 | requires: 85 | - deps 86 | - tests: 87 | requires: 88 | - linters 89 | - deploy: 90 | requires: 91 | - tests 92 | filters: 93 | branches: 94 | only: master -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | DarkSky 2 | ========== 3 | 4 | [![CircleCI](https://circleci.com/gh/Detrous/darksky/tree/master.svg?style=svg)](https://circleci.com/gh/Detrous/darksky/tree/master) [![CircleCI](https://codecov.io/gh/detrous/darksky/branch/master/graph/badge.svg)](https://codecov.io/gh/detrous/darksky/tree/master) 5 | 6 | # [Dark Sky, the provider on which this repo relies, will no longer accept new signups. The API will continue to function through the end of 2021.](https://blog.darksky.net/dark-sky-has-a-new-home) 7 | 8 | This library for the [Dark Sky 9 | API](https://darksky.net/dev/docs) provides access to detailed 10 | weather information from around the globe. 11 | 12 | * [Installation](#installation) 13 | * [Get started](#get-started) 14 | * [Contact us](#contact-us) 15 | * [License](#license) 16 | 17 | 18 | ### Installation 19 | ``` 20 | pip3 install darksky_weather 21 | ``` 22 | 23 | ### Get started 24 | 25 | Before you start using this library, you need to get your API key 26 | [here](https://darksky.net/dev/register). 27 | 28 | All classes are fully annotated, source code it's your best doc : ) 29 | 30 | ```python 31 | from darksky.api import DarkSky, DarkSkyAsync 32 | from darksky.types import languages, units, weather 33 | 34 | 35 | API_KEY = '0123456789abcdef9876543210fedcba' 36 | 37 | # Synchronous way 38 | darksky = DarkSky(API_KEY) 39 | 40 | latitude = 42.3601 41 | longitude = -71.0589 42 | forecast = darksky.get_forecast( 43 | latitude, longitude, 44 | extend=False, # default `False` 45 | lang=languages.ENGLISH, # default `ENGLISH` 46 | values_units=units.AUTO, # default `auto` 47 | exclude=[weather.MINUTELY, weather.ALERTS], # default `[]`, 48 | timezone='UTC' # default None - will be set by DarkSky API automatically 49 | ) 50 | 51 | # Synchronous way Time Machine 52 | 53 | from datetime import datetime as dt 54 | 55 | darksky = DarkSky(API_KEY) 56 | t = dt(2018, 5, 6, 12) 57 | 58 | latitude = 42.3601 59 | longitude = -71.0589 60 | forecast = darksky.get_time_machine_forecast( 61 | latitude, longitude, 62 | extend=False, # default `False` 63 | lang=languages.ENGLISH, # default `ENGLISH` 64 | values_units=units.AUTO, # default `auto` 65 | exclude=[weather.MINUTELY, weather.ALERTS], # default `[]`, 66 | timezone='UTC', # default None - will be set by DarkSky API automatically 67 | time=t 68 | ) 69 | 70 | # Asynchronous way 71 | # NOTE! On Mac os you will have problem with ssl checking https://github.com/aio-libs/aiohttp/issues/2822 72 | # So you need to create your own session with disabled ssl verify and pass it into the get_forecast 73 | # session = aiohttp.ClientSession(connector=aiohttp.TCPConnector(verify_ssl=False)) 74 | # darksky = DarkSkyAsync(API_KEY) 75 | # forecast = await darksky.get_forecast( 76 | # *arguments*, 77 | # client_session=session 78 | # ) 79 | 80 | darksky = DarkSkyAsync(API_KEY) 81 | 82 | latitude = 42.3601 83 | longitude = -71.0589 84 | forecast = await darksky.get_forecast( 85 | latitude, longitude, 86 | extend=False, # default `False` 87 | lang=languages.ENGLISH, # default `ENGLISH` 88 | values_units=units.AUTO, # default `auto` 89 | exclude=[weather.MINUTELY, weather.ALERTS], # default `[]` 90 | timezone='UTC' # default None - will be set by DarkSky API automatically, 91 | client_session=aiohttp.ClientSession() # default aiohttp.ClientSession() 92 | ) 93 | 94 | # Final wrapper identical for both ways 95 | forecast.latitude # 42.3601 96 | forecast.longitude # -71.0589 97 | forecast.timezone # timezone for coordinates. For exmaple: `America/New_York` 98 | 99 | forecast.currently # CurrentlyForecast. Can be found at darksky/forecast.py 100 | forecast.minutely # MinutelyForecast. Can be found at darksky/forecast.py 101 | forecast.hourly # HourlyForecast. Can be found at darksky/forecast.py 102 | forecast.daily # DailyForecast. Can be found at darksky/forecast.py 103 | forecast.alerts # [Alert]. Can be found at darksky/forecast.py 104 | ``` 105 | 106 | ### Contact us. 107 | 108 | If you have any issues or questions regarding the library, you are welcome to create an issue, or 109 | You can write an Email to `detrous@protonmail.com` 110 | 111 | 112 | ### License. 113 | 114 | Library is released under the [MIT License](./LICENSE). 115 | -------------------------------------------------------------------------------- /darksky/forecast.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import List 3 | 4 | from . import base 5 | 6 | 7 | class CurrentlyForecast(base.AutoInit): 8 | time: datetime 9 | summary: str = None 10 | icon: str 11 | nearest_storm_distance: int 12 | nearest_storm_bearing: int 13 | precip_intensity: float 14 | precip_intensity_error: float 15 | precip_probability: float 16 | precip_type: str 17 | precipAccumulation: float 18 | temperature: float 19 | apparent_temperature: float 20 | dew_point: float 21 | humidity: float 22 | pressure: float 23 | wind_speed: float 24 | wind_gust: float 25 | wind_bearing: int 26 | cloud_cover: float 27 | uv_index: int 28 | visibility: float 29 | ozone: float 30 | 31 | 32 | class MinutelyForecastItem(base.AutoInit): 33 | time: datetime 34 | precip_intensity: float 35 | precip_intensity_error: float 36 | precip_probability: float 37 | precip_type: str 38 | 39 | 40 | class MinutelyForecast(base.BaseWeather): 41 | data: List[MinutelyForecastItem] 42 | data_class = MinutelyForecastItem 43 | 44 | 45 | class HourlyForecastItem(base.AutoInit): 46 | time: datetime 47 | summary: str = None 48 | icon: str 49 | precip_intensity: float 50 | precip_probability: float 51 | precip_type: str 52 | precipAccumulation: float 53 | temperature: float 54 | apparent_temperature: float 55 | dew_point: float 56 | humidity: float 57 | pressure: float 58 | wind_speed: float 59 | wind_gust: float 60 | wind_bearing: int 61 | cloud_cover: float 62 | uv_index: int 63 | visibility: float 64 | ozone: float 65 | 66 | 67 | class HourlyForecast(base.BaseWeather): 68 | data: List[HourlyForecastItem] 69 | data_class = HourlyForecastItem 70 | 71 | 72 | class DailyForecastItem(base.AutoInit): 73 | time: datetime 74 | summary: str = None 75 | icon: str 76 | sunrise_time: datetime 77 | sunset_time: datetime 78 | moon_phase: float 79 | precip_intensity: float 80 | precip_intensity_max: float 81 | precip_intensity_max_time: datetime 82 | precip_probability: float 83 | precip_type: str 84 | precipAccumulation: float 85 | temperature_high: float 86 | temperature_high_time: datetime 87 | temperature_low: float 88 | temperature_low_time: datetime 89 | apparent_temperature_high: float 90 | apparent_temperature_high_time: datetime 91 | apparent_temperature_low: float 92 | apparent_temperature_low_time: datetime 93 | dew_point: float 94 | humidity: float 95 | pressure: float 96 | wind_speed: float 97 | wind_gust: float 98 | wind_gust_time: datetime 99 | wind_bearing: int 100 | cloud_cover: float 101 | uv_index: int 102 | uv_index_time: datetime 103 | visibility: int 104 | ozone: float 105 | temperature_min: float 106 | temperature_min_time: datetime 107 | temperature_max: float 108 | temperature_max_time: datetime 109 | apparent_temperature_min: float 110 | apparent_temperature_min_time: datetime 111 | apparent_temperature_max: float 112 | apparent_temperature_max_time: datetime 113 | 114 | 115 | class DailyForecast(base.BaseWeather): 116 | data: List[DailyForecastItem] 117 | data_class = DailyForecastItem 118 | 119 | 120 | class Alert(base.AutoInit): 121 | title: str 122 | regions: list 123 | severity: str 124 | time: datetime 125 | expires: datetime 126 | description: str 127 | uri: str 128 | 129 | 130 | class Flags(base.AutoInit): 131 | sources: List[str] 132 | sources_class = str 133 | nearest__station: float 134 | darksky__unavailable: bool 135 | units: str 136 | 137 | 138 | class Forecast: 139 | latitude: float 140 | longitude: float 141 | timezone: str 142 | currently: CurrentlyForecast 143 | minutely: MinutelyForecast 144 | hourly: HourlyForecast 145 | daily: DailyForecast 146 | alerts: List[Alert] 147 | flags: Flags 148 | offset: int 149 | 150 | def __init__( 151 | self, 152 | latitude: float, 153 | longitude: float, 154 | timezone: str, 155 | currently: dict = None, 156 | minutely: dict = None, 157 | hourly: dict = None, 158 | daily: dict = None, 159 | alerts: [dict] = None, 160 | flags: dict = None, 161 | offset: int = None, 162 | ): 163 | self.latitude = latitude 164 | self.longitude = longitude 165 | self.timezone = timezone 166 | 167 | self.currently = CurrentlyForecast( 168 | timezone=timezone, **(currently or {})) 169 | self.minutely = MinutelyForecast(timezone=timezone, **(minutely or {})) 170 | self.hourly = HourlyForecast(timezone=timezone, **(hourly or {})) 171 | self.daily = DailyForecast(timezone=timezone, **(daily or {})) 172 | 173 | self.alerts = [Alert(timezone=timezone, **alert) 174 | for alert in (alerts or [])] 175 | self.flags = Flags(timezone=timezone, **(flags or {})) 176 | 177 | self.offset = offset 178 | -------------------------------------------------------------------------------- /darksky/api.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | import aiohttp 4 | 5 | from .forecast import Forecast 6 | from .request_manager import (BaseRequestManger, RequestManger, 7 | RequestMangerAsync) 8 | from .types import languages, units, weather 9 | 10 | 11 | class BaseDarkSky: 12 | HOST = "https://api.darksky.net/forecast" 13 | 14 | def __init__(self, api_key: str): 15 | self.api_key: str = api_key 16 | self.request_manager: BaseRequestManger = None 17 | 18 | def get_forecast( 19 | self, 20 | latitude: float, 21 | longitude: float, 22 | extend: bool = None, 23 | lang=languages.ENGLISH, 24 | values_units=units.AUTO, 25 | exclude: [weather] = None, 26 | timezone: str = None, 27 | ): 28 | raise NotImplementedError 29 | 30 | def get_time_machine_forecast( 31 | self, 32 | latitude: float, 33 | longitude: float, 34 | time: datetime, 35 | extend: bool = False, 36 | lang=languages.ENGLISH, 37 | values_units=units.AUTO, 38 | exclude: [weather] = None, 39 | timezone: str = None, 40 | ): 41 | raise NotImplementedError 42 | 43 | def get_url(self, latitude: float, longitude: float, time=None, **params): 44 | if time is None: 45 | return "{host}/{api_key}/{latitude},{longitude}".format( 46 | api_key=self.api_key, 47 | host=self.HOST, 48 | latitude=latitude, 49 | longitude=longitude, 50 | ) 51 | return "{host}/{api_key}/{latitude},{longitude},{time}".format( 52 | api_key=self.api_key, 53 | host=self.HOST, 54 | latitude=latitude, 55 | longitude=longitude, 56 | time=time, 57 | ) 58 | 59 | 60 | class DarkSky(BaseDarkSky): 61 | def __init__(self, api_key: str, gzip: bool = True): 62 | super().__init__(api_key) 63 | self.request_manager = RequestManger(gzip) 64 | 65 | def get_forecast( 66 | self, 67 | latitude: float, 68 | longitude: float, 69 | extend: bool = None, 70 | lang=languages.ENGLISH, 71 | values_units=units.AUTO, 72 | exclude: [weather] = None, 73 | timezone: str = None, 74 | ) -> Forecast: 75 | url = self.get_url(latitude, longitude) 76 | data = self.request_manager.make_request( 77 | url=url, 78 | extend=weather.HOURLY if extend else None, 79 | lang=lang, 80 | units=values_units, 81 | exclude=exclude, 82 | timezone=timezone, 83 | ) 84 | return Forecast(**data) 85 | 86 | def get_time_machine_forecast( 87 | self, 88 | latitude: float, 89 | longitude: float, 90 | time: datetime, 91 | extend: bool = False, 92 | lang=languages.ENGLISH, 93 | values_units=units.AUTO, 94 | exclude: [weather] = None, 95 | timezone: str = None, 96 | ) -> Forecast: 97 | url = self.get_url(latitude, longitude, int(time.timestamp())) 98 | data = self.request_manager.make_request( 99 | url=url, 100 | extend=weather.HOURLY if extend else None, 101 | lang=lang, 102 | units=values_units, 103 | exclude=exclude, 104 | timezone=timezone, 105 | ) 106 | return Forecast(**data) 107 | 108 | 109 | class DarkSkyAsync(BaseDarkSky): 110 | def __init__( 111 | self, 112 | api_key: str, 113 | gzip: bool = True 114 | ): 115 | super().__init__(api_key) 116 | self.request_manager = RequestMangerAsync( 117 | gzip=gzip 118 | ) 119 | 120 | async def get_forecast( 121 | self, 122 | latitude: float, 123 | longitude: float, 124 | client_session: aiohttp.ClientSession, 125 | extend: bool = None, 126 | lang=languages.ENGLISH, 127 | values_units=units.AUTO, 128 | exclude: [weather] = None, 129 | timezone: str = None, 130 | ) -> Forecast: 131 | url = self.get_url(latitude, longitude) 132 | data = await self.request_manager.make_request( 133 | url=url, 134 | extend=weather.HOURLY if extend else None, 135 | lang=lang, 136 | units=values_units, 137 | exclude=exclude, 138 | timezone=timezone, 139 | session=client_session, 140 | ) 141 | return Forecast(**data) 142 | 143 | async def get_time_machine_forecast( 144 | self, 145 | latitude: float, 146 | longitude: float, 147 | time: datetime, 148 | client_session: aiohttp.ClientSession, 149 | extend: bool = False, 150 | lang=languages.ENGLISH, 151 | values_units=units.AUTO, 152 | exclude: [weather] = None, 153 | timezone: str = None 154 | ) -> Forecast: 155 | url = self.get_url(latitude, longitude, int(time.timestamp())) 156 | data = await self.request_manager.make_request( 157 | url=url, 158 | extend=weather.HOURLY if extend else None, 159 | lang=lang, 160 | units=values_units, 161 | exclude=exclude, 162 | timezone=timezone, 163 | session=client_session, 164 | ) 165 | return Forecast(**data) 166 | -------------------------------------------------------------------------------- /tests/test_forecast.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import copy 3 | import os 4 | import re 5 | import sys 6 | from datetime import datetime 7 | 8 | import aiohttp 9 | import aioresponses 10 | import mock 11 | import pytest 12 | 13 | from darksky.api import DarkSky, DarkSkyAsync 14 | from darksky.forecast import Forecast 15 | from darksky.utils import get_datetime_from_unix 16 | 17 | from . import mokcs, utils 18 | from .data import DATA 19 | 20 | sys.path.insert( 21 | 0, 22 | os.path.realpath( 23 | os.path.join( 24 | os.path.dirname(__file__), 25 | ".." 26 | ) 27 | ) 28 | ) 29 | 30 | 31 | @mock.patch("requests.Session", mokcs.MockSession) 32 | def get_forecast_sync() -> Forecast: 33 | darksky = DarkSky("api_key") 34 | 35 | return darksky.get_forecast(DATA["latitude"], DATA["longitude"]) 36 | 37 | 38 | def get_forecast_async(): 39 | async def get_async_data(): 40 | darksky = DarkSkyAsync("api_key") 41 | with aioresponses.aioresponses() as resp: 42 | resp.get(re.compile(".+"), status=200, payload=copy.deepcopy(DATA)) 43 | 44 | result = await darksky.get_forecast( 45 | DATA["latitude"], 46 | DATA["longitude"], 47 | client_session=aiohttp.ClientSession() 48 | ) 49 | 50 | return result 51 | 52 | loop = asyncio.get_event_loop() 53 | return loop.run_until_complete(get_async_data()) 54 | 55 | 56 | @pytest.mark.parametrize( 57 | "forecast", 58 | [get_forecast_sync(), get_forecast_async()] 59 | ) 60 | def test_forecast_base_fields(forecast): 61 | 62 | assert isinstance(forecast, Forecast) 63 | assert forecast.latitude == DATA["latitude"] 64 | assert forecast.longitude == DATA["longitude"] 65 | assert forecast.timezone == "America/New_York" 66 | 67 | 68 | @pytest.mark.parametrize( 69 | "forecast", 70 | [get_forecast_sync(), get_forecast_async()] 71 | ) 72 | def test_forecast_currently(forecast): 73 | 74 | f_item, d_item = forecast.currently, copy.deepcopy(DATA["currently"]) 75 | for key in d_item: 76 | forecast_key = utils.snake_case_key(key) 77 | if isinstance(getattr(f_item, forecast_key), datetime): 78 | d_item[key] = get_datetime_from_unix(d_item[key]) 79 | assert hasattr(f_item, forecast_key) 80 | assert getattr(f_item, forecast_key) == d_item[key] 81 | 82 | 83 | @pytest.mark.parametrize( 84 | "forecast", 85 | [get_forecast_sync(), get_forecast_async()] 86 | ) 87 | def test_forecast_minutely(forecast): 88 | 89 | assert forecast.minutely.summary == DATA["minutely"]["summary"] 90 | assert forecast.minutely.icon == DATA["minutely"]["icon"] 91 | 92 | for f_item, d_item in zip( 93 | forecast.minutely.data, copy.deepcopy(DATA["minutely"]["data"]) 94 | ): 95 | for key in d_item: 96 | forecast_key = utils.snake_case_key(key) 97 | if isinstance(getattr(f_item, forecast_key), datetime): 98 | d_item[key] = get_datetime_from_unix(d_item[key]) 99 | assert hasattr(f_item, forecast_key) 100 | assert getattr(f_item, forecast_key) == d_item[key] 101 | 102 | 103 | @pytest.mark.parametrize( 104 | "forecast", 105 | [get_forecast_sync(), get_forecast_async()] 106 | ) 107 | def test_forecast_hourly(forecast): 108 | 109 | assert forecast.hourly.summary == DATA["hourly"]["summary"] 110 | assert forecast.hourly.icon == DATA["hourly"]["icon"] 111 | 112 | for f_item, d_item in zip( 113 | forecast.hourly.data, copy.deepcopy(DATA["hourly"]["data"]) 114 | ): 115 | for key in d_item: 116 | forecast_key = utils.snake_case_key(key) 117 | if isinstance(getattr(f_item, forecast_key), datetime): 118 | d_item[key] = get_datetime_from_unix(d_item[key]) 119 | assert hasattr(f_item, forecast_key) 120 | assert getattr(f_item, forecast_key) == d_item[key] 121 | 122 | 123 | @pytest.mark.parametrize( 124 | "forecast", 125 | [get_forecast_sync(), get_forecast_async()] 126 | ) 127 | def test_forecast_daily(forecast): 128 | 129 | assert forecast.daily.summary == DATA["daily"]["summary"] 130 | assert forecast.daily.icon == DATA["daily"]["icon"] 131 | 132 | for f_item, d_item in zip( 133 | forecast.daily.data, copy.deepcopy(DATA["daily"]["data"]) 134 | ): 135 | for key in d_item: 136 | forecast_key = utils.snake_case_key(key) 137 | if isinstance(getattr(f_item, forecast_key), datetime): 138 | d_item[key] = get_datetime_from_unix(d_item[key]) 139 | assert hasattr(f_item, forecast_key) 140 | assert getattr(f_item, forecast_key) == d_item[key] 141 | 142 | 143 | @pytest.mark.parametrize( 144 | "forecast", 145 | [get_forecast_sync(), get_forecast_async()] 146 | ) 147 | def test_forecast_alerts(forecast): 148 | 149 | for f_item, d_item in zip(forecast.alerts, copy.deepcopy(DATA["alerts"])): 150 | for key in d_item: 151 | forecast_key = utils.snake_case_key(key) 152 | if isinstance(getattr(f_item, forecast_key), datetime): 153 | d_item[key] = get_datetime_from_unix(d_item[key]) 154 | assert hasattr(f_item, forecast_key) 155 | assert getattr(f_item, forecast_key) == d_item[key] 156 | 157 | 158 | @pytest.mark.parametrize( 159 | "forecast", 160 | [get_forecast_sync(), get_forecast_async()] 161 | ) 162 | def test_forecast_flags(forecast): 163 | d_item = copy.deepcopy(DATA["flags"]) 164 | f_item = forecast.flags 165 | for key in d_item: 166 | forecast_key = utils.snake_case_key(key) 167 | if isinstance(getattr(f_item, forecast_key), datetime): 168 | d_item[key] = get_datetime_from_unix(d_item[key]) 169 | assert hasattr(f_item, forecast_key) 170 | assert getattr(f_item, forecast_key) == d_item[key] 171 | -------------------------------------------------------------------------------- /tests/data.py: -------------------------------------------------------------------------------- 1 | DATA = {"latitude": 42.3601, 2 | "longitude": -71.0589, 3 | "timezone": "America/New_York", 4 | "currently": {"time": 1509993277, 5 | "summary": "Drizzle", 6 | "icon": "rain", 7 | "nearestStormDistance": 0, 8 | "precipIntensity": 0.0089, 9 | "precipIntensityError": 0.0046, 10 | "precipProbability": 0.9, 11 | "precipType": "rain", 12 | "temperature": 66.1, 13 | "apparentTemperature": 66.31, 14 | "dewPoint": 60.77, 15 | "humidity": 0.83, 16 | "pressure": 1010.34, 17 | "windSpeed": 5.59, 18 | "windGust": 12.03, 19 | "windBearing": 246, 20 | "cloudCover": 0.7, 21 | "uvIndex": 1, 22 | "visibility": 9.84, 23 | "ozone": 267.44, 24 | }, 25 | "minutely": {"summary": "Light rain stopping in 13 min., starting again 30 min. later.", 26 | "icon": "rain", 27 | "data": [{"time": 1509993240, 28 | "precipIntensity": 0.007, 29 | "precipIntensityError": 0.004, 30 | "precipProbability": 0.84, 31 | "precipType": "rain", 32 | }], 33 | }, 34 | "hourly": {"summary": "Rain starting later this afternoon, continuing until this evening.", 35 | "icon": "rain", 36 | "data": [{"time": 1509991200, 37 | "summary": "Mostly Cloudy", 38 | "icon": "partly-cloudy-day", 39 | "precipIntensity": 0.0007, 40 | "precipProbability": 0.1, 41 | "precipType": "rain", 42 | "temperature": 65.76, 43 | "apparentTemperature": 66.01, 44 | "dewPoint": 60.99, 45 | "humidity": 0.85, 46 | "pressure": 1010.57, 47 | "windSpeed": 4.23, 48 | "windGust": 9.52, 49 | "windBearing": 230, 50 | "cloudCover": 0.62, 51 | "uvIndex": 1, 52 | "visibility": 9.32, 53 | "ozone": 268.95, 54 | }], 55 | }, 56 | "daily": {"summary": "Mixed precipitation throughout the week, with temperatures falling to 39°F on Saturday.", 57 | "icon": "rain", 58 | "data": [{"time": 1509944400, 59 | "summary": "Rain starting in the afternoon, continuing until evening.", 60 | "icon": "rain", 61 | "sunriseTime": 1509967519, 62 | "sunsetTime": 1510003982, 63 | "moonPhase": 0.59, 64 | "precipIntensity": 0.0088, 65 | "precipIntensityMax": 0.0725, 66 | "precipIntensityMaxTime": 1510002000, 67 | "precipProbability": 0.73, 68 | "precipType": "rain", 69 | "temperatureHigh": 66.35, 70 | "temperatureHighTime": 1509994800, 71 | "temperatureLow": 41.28, 72 | "temperatureLowTime": 1510056000, 73 | "apparentTemperatureHigh": 66.53, 74 | "apparentTemperatureHighTime": 1509994800, 75 | "apparentTemperatureLow": 35.74, 76 | "apparentTemperatureLowTime": 1510056000, 77 | "dewPoint": 57.66, 78 | "humidity": 0.86, 79 | "pressure": 1012.93, 80 | "windSpeed": 3.22, 81 | "windGust": 26.32, 82 | "windGustTime": 1510023600, 83 | "windBearing": 270, 84 | "cloudCover": 0.8, 85 | "uvIndex": 2, 86 | "uvIndexTime": 1509987600, 87 | "visibility": 10, 88 | "ozone": 269.45, 89 | "temperatureMin": 52.08, 90 | "temperatureMinTime": 1510027200, 91 | "temperatureMax": 66.35, 92 | "temperatureMaxTime": 1509994800, 93 | "apparentTemperatureMin": 52.08, 94 | "apparentTemperatureMinTime": 1510027200, 95 | "apparentTemperatureMax": 66.53, 96 | "apparentTemperatureMaxTime": 1509994800, 97 | }], 98 | }, 99 | "alerts": [{"title": "Flood Watch for Mason, WA", 100 | "time": 1509993360, 101 | "expires": 1510036680, 102 | "description": "...FLOOD WATCH REMAINS IN EFFECT THROUGH LATE MONDAY NIGHT...\nTHE FLOOD WATCH CONTINUES FOR\n* A PORTION OF NORTHWEST WASHINGTON...INCLUDING THE FOLLOWING\nCOUNTY...MASON.\n* THROUGH LATE FRIDAY NIGHT\n* A STRONG WARM FRONT WILL BRING HEAVY RAIN TO THE OLYMPICS\nTONIGHT THROUGH THURSDAY NIGHT. THE HEAVY RAIN WILL PUSH THE\nSKOKOMISH RIVER ABOVE FLOOD STAGE TODAY...AND MAJOR FLOODING IS\nPOSSIBLE.\n* A FLOOD WARNING IS IN EFFECT FOR THE SKOKOMISH RIVER. THE FLOOD\nWATCH REMAINS IN EFFECT FOR MASON COUNTY FOR THE POSSIBILITY OF\nAREAL FLOODING ASSOCIATED WITH A MAJOR FLOOD.\n", 103 | "uri": "http://alerts.weather.gov/cap/wwacapget.php?x=WA1255E4DB8494.FloodWatch.1255E4DCE35CWA.SEWFFASEW.38e78ec64613478bb70fc6ed9c87f6e6", 104 | }], 105 | "flags": {"sources": ["nwspa", 106 | "cmc", 107 | "gfs", 108 | "hrrr", 109 | "icon", 110 | "isd", 111 | "madis", 112 | "nam", 113 | "sref", 114 | "darksky", 115 | "nearest-precip", 116 | ], 117 | "nearest-station": 1.835, 118 | "units": "us", 119 | }, 120 | } 121 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # A comma-separated list of package or module names from where C extensions may 4 | # be loaded. Extensions are loading into the active Python interpreter and may 5 | # run arbitrary code 6 | extension-pkg-whitelist=lxml 7 | 8 | # Add files or directories to the blacklist. They should be base names, not 9 | # paths. 10 | ignore=CVS,venv,data.py 11 | 12 | # Add files or directories matching the regex patterns to the blacklist. The 13 | # regex matches against base names, not paths. 14 | ignore-patterns= 15 | 16 | # Python code to execute, usually for sys.path manipulation such as 17 | # pygtk.require(). 18 | #init-hook= 19 | 20 | # Use multiple processes to speed up Pylint. 21 | jobs=1 22 | 23 | # List of plugins (as comma separated values of python modules names) to load, 24 | # usually to register additional checkers. 25 | load-plugins= 26 | 27 | # Pickle collected data for later comparisons. 28 | persistent=yes 29 | 30 | # Specify a configuration file. 31 | #rcfile= 32 | 33 | # When enabled, pylint would attempt to guess common misconfiguration and emit 34 | # user-friendly hints instead of false-positive error messages 35 | suggestion-mode=yes 36 | 37 | # Allow loading of arbitrary C extensions. Extensions are imported into the 38 | # active Python interpreter and may run arbitrary code. 39 | unsafe-load-any-extension=no 40 | 41 | 42 | [MESSAGES CONTROL] 43 | 44 | # Only show warnings with the listed confidence levels. Leave empty to show 45 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED 46 | confidence= 47 | 48 | # Disable the message, report, category or checker with the given id(s). You 49 | # can either give multiple identifiers separated by comma (,) or put this 50 | # option multiple times (only on the command line, not in the configuration 51 | # file where it should appear only once).You can also use "--disable=all" to 52 | # disable everything first and then reenable specific checks. For example, if 53 | # you want to run only the similarities checker, you can use "--disable=all 54 | # --enable=similarities". If you want to run only the classes checker, but have 55 | # no Warning level messages displayed, use"--disable=all --enable=classes 56 | # --disable=W" 57 | disable=print-statement, 58 | parameter-unpacking, 59 | unpacking-in-except, 60 | old-raise-syntax, 61 | backtick, 62 | long-suffix, 63 | old-ne-operator, 64 | old-octal-literal, 65 | import-star-module-level, 66 | non-ascii-bytes-literal, 67 | invalid-unicode-literal, 68 | raw-checker-failed, 69 | bad-inline-option, 70 | locally-disabled, 71 | locally-enabled, 72 | file-ignored, 73 | suppressed-message, 74 | useless-suppression, 75 | deprecated-pragma, 76 | apply-builtin, 77 | basestring-builtin, 78 | buffer-builtin, 79 | cmp-builtin, 80 | coerce-builtin, 81 | execfile-builtin, 82 | file-builtin, 83 | long-builtin, 84 | raw_input-builtin, 85 | reduce-builtin, 86 | standarderror-builtin, 87 | unicode-builtin, 88 | xrange-builtin, 89 | coerce-method, 90 | delslice-method, 91 | getslice-method, 92 | setslice-method, 93 | no-absolute-import, 94 | old-division, 95 | dict-iter-method, 96 | dict-view-method, 97 | next-method-called, 98 | metaclass-assignment, 99 | indexing-exception, 100 | raising-string, 101 | reload-builtin, 102 | oct-method, 103 | hex-method, 104 | nonzero-method, 105 | cmp-method, 106 | input-builtin, 107 | round-builtin, 108 | intern-builtin, 109 | unichr-builtin, 110 | map-builtin-not-iterating, 111 | zip-builtin-not-iterating, 112 | range-builtin-not-iterating, 113 | filter-builtin-not-iterating, 114 | using-cmp-argument, 115 | eq-without-hash, 116 | div-method, 117 | idiv-method, 118 | rdiv-method, 119 | exception-message-attribute, 120 | invalid-str-codec, 121 | sys-max-int, 122 | bad-python3-import, 123 | deprecated-string-function, 124 | deprecated-str-translate-call, 125 | deprecated-itertools-function, 126 | deprecated-types-field, 127 | next-method-defined, 128 | dict-items-not-iterating, 129 | dict-keys-not-iterating, 130 | dict-values-not-iterating, 131 | deprecated-operator-function, 132 | deprecated-urllib-function, 133 | xreadlines-attribute, 134 | deprecated-sys-function, 135 | exception-escape, 136 | comprehension-escape, 137 | unused-argument, 138 | global-statement, 139 | too-few-public-methods, 140 | too-many-arguments, 141 | too-many-instance-attributes, 142 | bad-continuation, 143 | no-member, 144 | invalid-name, 145 | arguments-differ, 146 | # todo fix 147 | missing-function-docstring, 148 | missing-module-docstring, 149 | missing-class-docstring 150 | 151 | 152 | # Enable the message, report, category or checker with the given id(s). You can 153 | # either give multiple identifier separated by comma (,) or put this option 154 | # multiple time (only on the command line, not in the configuration file where 155 | # it should appear only once). See also the "--disable" option for examples. 156 | enable=c-extension-no-member 157 | 158 | 159 | [REPORTS] 160 | 161 | # Python expression which should return a note less than 10 (10 is the highest 162 | # note). You have access to the variables errors warning, statement which 163 | # respectively contain the number of errors / warnings messages and the total 164 | # number of statements analyzed. This is used by the global evaluation report 165 | # (RP0004). 166 | evaluation=10.0 - ((float(5 error + warning + refactor + convention) / statement) 10) 167 | 168 | # Template used to display messages. This is a python new-style format string 169 | # used to format the message information. See doc for all details 170 | #msg-template= 171 | 172 | # Set the output format. Available formats are text, parseable, colorized, json 173 | # and msvs (visual studio).You can also give a reporter class, eg 174 | # mypackage.mymodule.MyReporterClass. 175 | output-format=text 176 | 177 | # Tells whether to display a full report or only the messages 178 | reports=no 179 | 180 | # Activate the evaluation score. 181 | score=yes 182 | 183 | 184 | [REFACTORING] 185 | 186 | # Maximum number of nested blocks for function / method body 187 | max-nested-blocks=5 188 | 189 | # Complete name of functions that never returns. When checking for 190 | # inconsistent-return-statements if a never returning function is called then 191 | # it will be considered as an explicit return statement and no message will be 192 | # printed. 193 | never-returning-functions=optparse.Values,sys.exit 194 | 195 | 196 | [LOGGING] 197 | 198 | # Logging modules to check that the string format arguments are in logging 199 | # function parameter format 200 | logging-modules=logging 201 | 202 | 203 | [SPELLING] 204 | 205 | # Limits count of emitted suggestions for spelling mistakes 206 | max-spelling-suggestions=4 207 | 208 | # Spelling dictionary name. Available dictionaries: none. To make it working 209 | # install python-enchant package. 210 | spelling-dict= 211 | 212 | # List of comma separated words that should not be checked. 213 | spelling-ignore-words= 214 | 215 | # A path to a file that contains private dictionary; one word per line. 216 | spelling-private-dict-file= 217 | 218 | # Tells whether to store unknown words to indicated private dictionary in 219 | # --spelling-private-dict-file option instead of raising a message. 220 | spelling-store-unknown-words=no 221 | 222 | 223 | [MISCELLANEOUS] 224 | 225 | # List of note tags to take in consideration, separated by a comma. 226 | notes=FIXME, 227 | XXX, 228 | TODO 229 | 230 | 231 | [TYPECHECK] 232 | 233 | # List of decorators that produce context managers, such as 234 | # contextlib.contextmanager. Add to this list to register other decorators that 235 | # produce valid context managers. 236 | contextmanager-decorators=contextlib.contextmanager 237 | 238 | # List of members which are set dynamically and missed by pylint inference 239 | # system, and so shouldn't trigger E1101 when accessed. Python regular 240 | # expressions are accepted. 241 | generated-members= 242 | 243 | # Tells whether missing members accessed in mixin class should be ignored. A 244 | # mixin class is detected if its name ends with "mixin" (case insensitive). 245 | ignore-mixin-members=yes 246 | 247 | # This flag controls whether pylint should warn about no-member and similar 248 | # checks whenever an opaque object is returned when inferring. The inference 249 | # can return multiple potential results while evaluating a Python object, but 250 | # some branches might not be evaluated, which results in partial inference. In 251 | # that case, it might be useful to still emit no-member and other checks for 252 | # the rest of the inferred objects. 253 | ignore-on-opaque-inference=yes 254 | 255 | # List of class names for which member attributes should not be checked (useful 256 | # for classes with dynamically set attributes). This supports the use of 257 | # qualified names. 258 | ignored-classes=optparse.Values,thread._local,_thread._local 259 | 260 | # List of module names for which member attributes should not be checked 261 | # (useful for modules/projects where namespaces are manipulated during runtime 262 | # and thus existing member attributes cannot be deduced by static analysis. It 263 | # supports qualified module names, as well as Unix pattern matching. 264 | ignored-modules= 265 | 266 | # Show a hint with possible names when a member name was not found. The aspect 267 | # of finding the hint is based on edit distance. 268 | missing-member-hint=yes 269 | 270 | # The minimum edit distance a name should have in order to be considered a 271 | # similar match for a missing member name. 272 | missing-member-hint-distance=1 273 | 274 | # The total number of similar names that should be taken in consideration when 275 | # showing a hint for a missing member. 276 | missing-member-max-choices=1 277 | 278 | 279 | [VARIABLES] 280 | 281 | # List of additional names supposed to be defined in builtins. Remember that 282 | # you should avoid to define new builtins when possible. 283 | additional-builtins= 284 | 285 | # Tells whether unused global variables should be treated as a violation. 286 | allow-global-unused-variables=yes 287 | 288 | # List of strings which can identify a callback function by name. A callback 289 | # name must start or end with one of those strings. 290 | callbacks=cb_, 291 | _cb 292 | 293 | # A regular expression matching the name of dummy variables (i.e. expectedly 294 | # not used). 295 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 296 | 297 | # Argument names that match this expression will be ignored. Default to name 298 | # with leading underscore 299 | ignored-argument-names=_.*|^ignored_|^unused_ 300 | 301 | # Tells whether we should check for unused import in _init_ files. 302 | init-import=no 303 | 304 | # List of qualified module names which can have objects that can redefine 305 | # builtins. 306 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,io,builtins 307 | 308 | 309 | [FORMAT] 310 | 311 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 312 | expected-line-ending-format= 313 | 314 | # Regexp for a line that is allowed to be longer than the limit. 315 | ignore-long-lines=^\s*(# )??$ 316 | 317 | # Number of spaces of indent required inside a hanging or continued line. 318 | indent-after-paren=4 319 | 320 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 321 | # tab). 322 | indent-string=' ' 323 | 324 | # Maximum number of characters on a single line. 325 | max-line-length=100 326 | 327 | # Maximum number of lines in a module 328 | max-module-lines=1000 329 | 330 | # List of optional constructs for which whitespace checking is disabled. `dict- 331 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 332 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 333 | # `empty-line` allows space-only lines. 334 | no-space-check=trailing-comma, 335 | dict-separator 336 | 337 | # Allow the body of a class to be on the same line as the declaration if body 338 | # contains single statement. 339 | single-line-class-stmt=no 340 | 341 | # Allow the body of an if to be on the same line as the test if there is no 342 | # else. 343 | single-line-if-stmt=no 344 | 345 | 346 | [SIMILARITIES] 347 | 348 | # Ignore comments when computing similarities. 349 | ignore-comments=yes 350 | 351 | # Ignore docstrings when computing similarities. 352 | ignore-docstrings=yes 353 | 354 | # Ignore imports when computing similarities. 355 | ignore-imports=no 356 | 357 | # Minimum lines number of a similarity. 358 | min-similarity-lines=4 359 | 360 | 361 | [BASIC] 362 | 363 | # Naming style matching correct argument names 364 | argument-naming-style=snake_case 365 | 366 | # Regular expression matching correct argument names. Overrides argument- 367 | # naming-style 368 | #argument-rgx= 369 | 370 | # Naming style matching correct attribute names 371 | attr-naming-style=snake_case 372 | 373 | # Regular expression matching correct attribute names. Overrides attr-naming- 374 | # style 375 | #attr-rgx= 376 | 377 | # Bad variable names which should always be refused, separated by a comma 378 | bad-names=foo, 379 | bar, 380 | baz, 381 | toto, 382 | tutu, 383 | tata 384 | 385 | # Naming style matching correct class attribute names 386 | class-attribute-naming-style=any 387 | 388 | # Regular expression matching correct class attribute names. Overrides class- 389 | # attribute-naming-style 390 | #class-attribute-rgx= 391 | 392 | # Naming style matching correct class names 393 | class-naming-style=PascalCase 394 | 395 | # Regular expression matching correct class names. Overrides class-naming-style 396 | #class-rgx= 397 | 398 | # Naming style matching correct constant names 399 | const-naming-style=UPPER_CASE 400 | 401 | # Regular expression matching correct constant names. Overrides const-naming- 402 | # style 403 | #const-rgx= 404 | 405 | # Minimum line length for functions/classes that require docstrings, shorter 406 | # ones are exempt. 407 | docstring-min-length=-1 408 | 409 | # Naming style matching correct function names 410 | function-naming-style=snake_case 411 | 412 | # Regular expression matching correct function names. Overrides function- 413 | # naming-style 414 | #function-rgx= 415 | 416 | # Good variable names which should always be accepted, separated by a comma 417 | good-names=i, 418 | j, 419 | k, 420 | ex, 421 | Run, 422 | _, 423 | app, 424 | urlpatterns, 425 | logger, 426 | application, 427 | 428 | # Include a hint for the correct naming format with invalid-name 429 | include-naming-hint=no 430 | 431 | # Naming style matching correct inline iteration names 432 | inlinevar-naming-style=any 433 | 434 | # Regular expression matching correct inline iteration names. Overrides 435 | # inlinevar-naming-style 436 | #inlinevar-rgx= 437 | 438 | # Naming style matching correct method names 439 | method-naming-style=snake_case 440 | 441 | # Regular expression matching correct method names. Overrides method-naming- 442 | # style 443 | #method-rgx= 444 | 445 | # Naming style matching correct module names 446 | module-naming-style=snake_case 447 | 448 | # Regular expression matching correct module names. Overrides module-naming- 449 | # style 450 | #module-rgx= 451 | 452 | # Colon-delimited sets of names that determine each other's naming style when 453 | # the name regexes allow several styles. 454 | name-group= 455 | 456 | # Regular expression which should only match function or class names that do 457 | # not require a docstring. 458 | no-docstring-rgx=(__.*__|test) 459 | 460 | # List of decorators that produce properties, such as abc.abstractproperty. Add 461 | # to this list to register other decorators that produce valid properties. 462 | property-classes=abc.abstractproperty 463 | 464 | # Naming style matching correct variable names 465 | variable-naming-style=snake_case 466 | 467 | # Regular expression matching correct variable names. Overrides variable- 468 | # naming-style 469 | #variable-rgx= 470 | 471 | 472 | [IMPORTS] 473 | 474 | # Allow wildcard imports from modules that define _all_. 475 | allow-wildcard-with-all=no 476 | 477 | # Analyse import fallback blocks. This can be used to support both Python 2 and 478 | # 3 compatible code, which means that the block might have code that exists 479 | # only in one or another interpreter, leading to false positives when analysed. 480 | analyse-fallback-blocks=no 481 | 482 | # Deprecated modules which should not be used, separated by a comma 483 | deprecated-modules=optparse,tkinter.tix 484 | 485 | # Create a graph of external dependencies in the given file (report RP0402 must 486 | # not be disabled) 487 | ext-import-graph= 488 | 489 | # Create a graph of every (i.e. internal and external) dependencies in the 490 | # given file (report RP0402 must not be disabled) 491 | import-graph= 492 | 493 | # Create a graph of internal dependencies in the given file (report RP0402 must 494 | # not be disabled) 495 | int-import-graph= 496 | 497 | # Force import order to recognize a module as part of the standard 498 | # compatibility libraries. 499 | known-standard-library= 500 | 501 | # Force import order to recognize a module as part of a third party library. 502 | known-third-party=enchant 503 | 504 | 505 | [CLASSES] 506 | 507 | # List of method names used to declare (i.e. assign) instance attributes. 508 | defining-attr-methods=__init__, 509 | _new_, 510 | setUp 511 | 512 | # List of member names, which should be excluded from the protected access 513 | # warning. 514 | exclude-protected=_asdict, 515 | _fields, 516 | _replace, 517 | _source, 518 | _make 519 | 520 | # List of valid names for the first argument in a class method. 521 | valid-classmethod-first-arg=cls 522 | 523 | # List of valid names for the first argument in a metaclass class method. 524 | valid-metaclass-classmethod-first-arg=mcs 525 | 526 | 527 | [DESIGN] 528 | 529 | # Maximum number of arguments for function / method 530 | max-args=5 531 | 532 | # Maximum number of attributes for a class (see R0902). 533 | max-attributes=7 534 | 535 | # Maximum number of boolean expressions in a if statement 536 | max-bool-expr=5 537 | 538 | # Maximum number of branch for function / method body 539 | max-branches=12 540 | 541 | # Maximum number of locals for function / method body 542 | max-locals=15 543 | 544 | # Maximum number of parents for a class (see R0901). 545 | max-parents=7 546 | 547 | # Maximum number of public methods for a class (see R0904). 548 | max-public-methods=20 549 | 550 | # Maximum number of return / yield for function / method body 551 | max-returns=6 552 | 553 | # Maximum number of statements in function / method body 554 | max-statements=50 555 | 556 | # Minimum number of public methods for a class (see R0903). 557 | min-public-methods=2 558 | 559 | 560 | [EXCEPTIONS] 561 | 562 | # Exceptions that will emit a warning when being caught. Defaults to 563 | # "Exception" 564 | overgeneral-exceptions=Exception --------------------------------------------------------------------------------