diff --git a/.coveragerc b/.coveragerc index 34bb1ef..beeaca5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -11,4 +11,4 @@ exclude_lines = if __name__ == .__main__.: ignore_errors = True omit = - tests/* \ No newline at end of file + tests/* diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..150fee0 --- /dev/null +++ b/.flake8 @@ -0,0 +1,6 @@ +[flake8] +max-complexity = 10 +max-line-length = 88 +extend-select = B950 +extend-ignore = E203,E501,E701 +exclude = .git,__pycache__,build,dist diff --git a/.github/workflows/publish-to-test-pypi.yml b/.github/workflows/publish-to-test-pypi.yml new file mode 100644 index 0000000..d32b33e --- /dev/null +++ b/.github/workflows/publish-to-test-pypi.yml @@ -0,0 +1,44 @@ +name: Publish Python 🐍 distribution 📦 to PyPI and TestPyPI +on: push +jobs: + build: + name: Build distribution 📦 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install hatch + uses: pypa/hatch@install + - name: Build a binary wheel and a source tarball + run: hatch build + - name: Store the distribution packages + uses: actions/upload-artifact@v4 + with: + name: python-package-distributions + path: dist/ + publish-to-testpypi: + name: Publish Python 🐍 distribution 📦 to TestPyPI + needs: + - build + runs-on: ubuntu-latest + environment: + name: testpypi + url: https://test.pypi.org/p/datapoint + permissions: + id-token: write # IMPORTANT: mandatory for trusted publishing + steps: + - name: Download all the dists + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + - name: Publish distribution 📦 to TestPyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..e12942f --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,19 @@ +name: Run tests +on: pull_request +jobs: + run-tests: + name: Run tests on python ${{ matrix.python-version }} + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install requirements + run: pip install -r requirements-dev.txt + - name: Run tests + run: python -m pytest tests diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..e6832b1 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,27 @@ +# Read the Docs configuration file for Sphinx projects +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the OS, Python version and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.12" + +# Build documentation in the "docs/" directory with Sphinx +sphinx: + configuration: "docs/conf.py" + # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs + # builder: "dirhtml" + # Fail on all warnings to avoid broken references + # fail_on_warning: true + + +# Optional but recommended, declare the Python requirements required +# to build your documentation +# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - requirements: "docs/requirements.txt" diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 4d7b72b..0000000 --- a/.travis.yml +++ /dev/null @@ -1,27 +0,0 @@ -language: python -sudo: false -dist: xenial -python: - - '3.8' - - '3.9' - - '3.10' - - '3.11' - - '3.12' -install: -- pip install codecov -- pip install coverage -- pip install . -- pip install -r requirements.txt -script: -- coverage run -m unittest discover -s tests/unit && codecov -F 'unit' -- if [[ "$API_KEY" != "" ]]; then coverage run -m unittest discover -s tests/integration; fi && codecov -F 'integration' -deploy: - provider: pypi - user: EJEP - skip_cleanup: true - password: "$PYPI_PASSWORD" - on: - tags: true - all_branches: true - repo: EJEP/datapoint-python - python: '3.8' diff --git a/CHANGELOG.md b/CHANGELOG.md index 227f66b..cdfb6d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [0.9.8] - 2020-07-03 + Remove f-string in test - + ## [0.9.7] - 2020-07-03 + Bugfix for `get_observation_sites` diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index e490274..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,2 +0,0 @@ -include versioneer.py -include datapoint/_version.py diff --git a/README.md b/README.md index 25e83d8..2290fb1 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # _DataPoint for Python_ [![PyPi version](https://img.shields.io/pypi/v/datapoint.svg)](https://pypi.python.org/pypi/datapoint/) [![Supported Python versions](https://img.shields.io/pypi/pyversions/datapoint.svg)](https://pypi.python.org/pypi/datapoint/) -[![Build Status](http://img.shields.io/travis/ejep/datapoint-python.svg?style=flat)](https://travis-ci.org/ejep/datapoint-python) [![Documentation Status](https://readthedocs.org/projects/datapoint-python/badge/?version=latest)](https://readthedocs.org/projects/datapoint-python/) diff --git a/datapoint/Day.py b/datapoint/Day.py deleted file mode 100644 index c15c2a9..0000000 --- a/datapoint/Day.py +++ /dev/null @@ -1,21 +0,0 @@ -class Day(): - def __init__(self): - self.date = None - self.timesteps = [] - - def __str__(self): - day_str = '' - - date_part = 'Date: ' + str(self.date) + '\n\n' - day_str += date_part - - day_str += 'Timesteps: \n\n' - try: - for timestep in self.timesteps: - day_str += str(timestep) - day_str += '\n' - - except TypeError: - day_str += 'No timesteps' - - return day_str diff --git a/datapoint/Element.py b/datapoint/Element.py deleted file mode 100644 index 71ec77b..0000000 --- a/datapoint/Element.py +++ /dev/null @@ -1,12 +0,0 @@ -class Element(): - def __init__(self, field_code=None, value=None, units=None): - - self.field_code = field_code - self.value = value - self.units = units - - # For elements which can also have a text value - self.text = None - - def __str__(self): - return str(self.value) + ' ' + str(self.units) diff --git a/datapoint/Forecast.py b/datapoint/Forecast.py deleted file mode 100644 index 8e4ef8f..0000000 --- a/datapoint/Forecast.py +++ /dev/null @@ -1,105 +0,0 @@ -import datetime -from datapoint.exceptions import APIException - - -class Forecast(): - def __init__(self, frequency=""): - self.frequency = frequency - self.data_date = None - self.continent = None - self.country = None - self.name = None - self.longitude = None - self.latitude = None - self.location_id = None - self.elevation = None - self.days = [] - - def timedelta_total_seconds(self, timedelta): - return ( - timedelta.microseconds + 0.0 + - (timedelta.seconds + timedelta.days * 24 * 3600) * 10 ** 6) / 10 ** 6 - - def at_datetime(self, target): - """ Return the timestep closest to the target datetime""" - - # Convert target to offset aware datetime - if target.tzinfo is None: - target = datetime.datetime.combine(target.date(), target.time(), self.days[0].date.tzinfo) - - num_timesteps = len(self.days[1].timesteps) - # First check that the target is at most 1.5 hours before the first timestep - if target < self.days[0].timesteps[0].date - datetime.timedelta(hours=1, minutes=30) and num_timesteps == 8: - err_str = 'There is no forecast available for the requested time. ' + \ - 'The requested time is more than 1.5 hours before the first available forecast' - raise APIException(err_str) - - elif target < self.days[0].timesteps[0].date - datetime.timedelta(hours=6) and num_timesteps == 2: - - err_str = 'There is no forecast available for the requested time. ' + \ - 'The requested time is more than 6 hours before the first available forecast' - - raise APIException(err_str) - - # Ensure that the target is less than 1 hour 30 minutes after the final - # timestep. - # Logic is correct - # If there are 8 timesteps per day, then the target must be within 1.5 - # hours of the last timestep - if target > (self.days[-1].timesteps[-1].date + datetime.timedelta(hours=1, minutes=30)) and num_timesteps == 8: - - err_str = 'There is no forecast available for the requested time. The requested time is more than 1.5 hours after the first available forecast' - - raise APIException(err_str) - - # If there are 2 timesteps per day, then the target must be within 6 - # hours of the last timestep - if target > (self.days[-1].timesteps[-1].date + datetime.timedelta(hours=6)) and num_timesteps == 2: - - err_str = 'There is no forecast available for the requested time. The requested time is more than 6 hours after the first available forecast' - - raise APIException(err_str) - - # Loop over all timesteps - # Calculate the first time difference - prev_td = target - self.days[0].timesteps[0].date - prev_ts = self.days[0].timesteps[0] - - for day in self.days: - for timestep in day.timesteps: - # Calculate the difference between the target time and the - # timestep. - td = target - timestep.date - - # Find the timestep which is further from the target than the - # previous one. Return the previous timestep - if abs(td.total_seconds()) > abs(prev_td.total_seconds()): - # We are further from the target - return prev_ts - if abs(td.total_seconds()) < 5400 and num_timesteps == 8: - # if we are past the final timestep, and it is a 3 hourly - # forecast, check that we are within 90 minutes of it - return timestep - if abs(td.total_seconds()) < 21600 and num_timesteps == 2: - # if we are past the final timestep, and it is a daily - # forecast, check that we are within 6 hours of it - return timestep - - prev_ts = timestep - prev_td = td - - def now(self): - """Function to return the closest timestep to the current time - """ - - d = datetime.datetime.now(tz=self.days[0].date.tzinfo) - return self.at_datetime(d) - - def future(self, in_days=0, in_hours=0, in_minutes=0, in_seconds=0): - """Return the closest timestep to a date in a given amount of time""" - - d = datetime.datetime.now(tz=self.days[0].date.tzinfo) - target = d + datetime.timedelta(days=in_days, hours=in_hours, - minutes=in_minutes, seconds=in_seconds) - - return self.at_datetime(target) diff --git a/datapoint/Manager.py b/datapoint/Manager.py deleted file mode 100644 index 18bcc64..0000000 --- a/datapoint/Manager.py +++ /dev/null @@ -1,685 +0,0 @@ -""" -Datapoint python module -""" - -from datetime import datetime -from datetime import timedelta -from time import time -from math import radians, cos, sin, asin, sqrt -import pytz -from warnings import warn - -import requests -from requests.adapters import HTTPAdapter -from requests.packages.urllib3.util.retry import Retry - -from datapoint.exceptions import APIException -from datapoint.Site import Site -from datapoint.Forecast import Forecast -from datapoint.Observation import Observation -from datapoint.Day import Day -from datapoint.Timestep import Timestep -from datapoint.Element import Element -from datapoint.regions.RegionManager import RegionManager - - -FORECAST_URL = "http://datapoint.metoffice.gov.uk/public/data/val/wxfcs/all/json" -OBSERVATION_URL = "http://datapoint.metoffice.gov.uk/public/data/val/wxobs/all/json" -DATE_FORMAT = "%Y-%m-%dZ" -DATA_DATE_FORMAT = "%Y-%m-%dT%XZ" -FORECAST_DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ" - -# See: -# https://www.metoffice.gov.uk/binaries/content/assets/mohippo/pdf/3/0/datapoint_api_reference.pdf -# pages 8 onwards for a description of the response anatomy, and the elements - -ELEMENTS = { - "Day": - {"U":"U", "V":"V", "W":"W", "T":"Dm", "S":"S", "Pp":"PPd", - "H":"Hn", "G":"Gn", "F":"FDm", "D":"D"}, - "Night": - {"V":"V", "W":"W", "T":"Nm", "S":"S", "Pp":"PPn", - "H":"Hm", "G":"Gm", "F":"FNm", "D":"D"}, - "Default": - {"V":"V", "W":"W", "T":"T", "S":"S", "Pp":"Pp", - "H":"H", "G":"G", "F":"F", "D":"D", "U":"U"}, - "Observation": - {"T":"T", "V":"V", "D":"D", "S":"S", - "W":"W", "P":"P", "Pt":"Pt", "Dp":"Dp", "H":"H"} -} - -WEATHER_CODES = { - "0":"Clear night", - "1":"Sunny day", - "2":"Partly cloudy", - "3":"Partly cloudy", - "4":"Not used", - "5":"Mist", - "6":"Fog", - "7":"Cloudy", - "8":"Overcast", - "9":"Light rain shower", - "10":"Light rain shower", - "11":"Drizzle", - "12":"Light rain", - "13":"Heavy rain shower", - "14":"Heavy rain shower", - "15":"Heavy rain", - "16":"Sleet shower", - "17":"Sleet shower", - "18":"Sleet", - "19":"Hail shower", - "20":"Hail shower", - "21":"Hail", - "22":"Light snow shower", - "23":"Light snow shower", - "24":"Light snow", - "25":"Heavy snow shower", - "26":"Heavy snow shower", - "27":"Heavy snow", - "28":"Thunder shower", - "29":"Thunder shower", - "30":"Thunder" -} - - -class Manager(): - """ - Datapoint Manager object - """ - - def __init__(self, api_key=""): - self.api_key = api_key - self.call_response = None - - # The list of sites changes infrequently so limit to requesting it - # every hour. - self.forecast_sites_last_update = 0 - self.forecast_sites_last_request = None - self.forecast_sites_update_time = 3600 - - self.observation_sites_last_update = 0 - self.observation_sites_last_request = None - self.observation_sites_update_time = 3600 - - self.regions = RegionManager(self.api_key) - - def __retry_session(self, retries=10, backoff_factor=0.3, - status_forcelist=(500, 502, 504), - session=None): - """ - Retry the connection using requests if it fails. Use this as a wrapper - to request from datapoint - """ - - # requests.Session allows finer control, which is needed to use the - # retrying code - the_session = session or requests.Session() - - # The Retry object manages the actual retrying - retry = Retry(total=retries, read=retries, connect=retries, - backoff_factor=backoff_factor, - status_forcelist=status_forcelist) - - adapter = HTTPAdapter(max_retries=retry) - - the_session.mount('http://', adapter) - the_session.mount('https://', adapter) - - return the_session - - def __call_api(self, path, params=None, api_url=FORECAST_URL): - """ - Call the datapoint api using the requests module - - """ - if not params: - params = dict() - payload = {'key': self.api_key} - payload.update(params) - url = "%s/%s" % (api_url, path) - - # Add a timeout to the request. - # The value of 1 second is based on attempting 100 connections to - # datapoint and taking ten times the mean connection time (rounded up). - # Could expose to users in the functions which need to call the api. - #req = requests.get(url, params=payload, timeout=1) - # The wrapper function __retry_session returns a requests.Session - # object. This has a .get() function like requests.get(), so the use - # doesn't change here. - - sess = self.__retry_session() - req = sess.get(url, params=payload, timeout=1) - - try: - data = req.json() - except ValueError: - raise APIException("DataPoint has not returned any data, this could be due to an incorrect API key") - self.call_response = data - if req.status_code != 200: - msg = [data[m] for m in ("message", "error_message", "status") \ - if m in data][0] - raise Exception(msg) - return data - - def _distance_between_coords(self, lon1, lat1, lon2, lat2): - """ - Calculate the great circle distance between two points - on the earth (specified in decimal degrees). - Haversine formula states that: - - d = 2 * r * arcsin(sqrt(sin^2((lat1 - lat2) / 2 + - cos(lat1)cos(lat2)sin^2((lon1 - lon2) / 2)))) - - where r is the radius of the sphere. This assumes the earth is spherical. - """ - - # Convert the coordinates of the points to radians. - lon1, lat1, lon2, lat2 = map(radians, [lon1, lat1, lon2, lat2]) - r = 6371 - - d_hav = 2 * r * asin(sqrt((sin((lat1 - lat2) / 2))**2 + \ - cos(lat1) * cos(lat2) * (sin((lon1 - lon2) / 2)**2 ))) - - return d_hav - - def _get_wx_units(self, params, name): - """ - Give the Wx array returned from datapoint and an element - name and return the units for that element. - """ - units = "" - for param in params: - if str(name) == str(param['name']): - units = param['units'] - return units - - def _weather_to_text(self, code): - if not isinstance(code, int): - raise ValueError("Weather code must be an integer not", type(code)) - if code < 0 or code > 30: - raise ValueError("Weather code outof bounds, should be 0-30") - text = WEATHER_CODES[str(code)] - return text - - def _visibility_to_text(self, distance): - """ - Convert observed visibility in metres to text used in forecast - """ - - if not isinstance(distance, int): - raise ValueError("Distance must be an integer not", type(distance)) - if distance < 0: - raise ValueError("Distance out of bounds, should be 0 or greater") - - if 0 <= distance < 1000: - return 'VP' - elif 1000 <= distance < 4000: - return 'PO' - elif 4000 <= distance < 10000: - return 'MO' - elif 10000 <= distance < 20000: - return 'GO' - elif 20000 <= distance < 40000: - return 'VG' - else: - return 'EX' - - def get_all_sites(self): - """ - Deprecated. This function returns a list of Site object. - """ - warning_message = 'This function is deprecated. Use get_forecast_sites() instead' - warn(warning_message, DeprecationWarning, stacklevel=2) - - return self.get_forecast_sites() - - def get_forecast_sites(self): - """ - This function returns a list of Site object. - """ - - time_now = time() - if (time_now - self.forecast_sites_last_update) > self.forecast_sites_update_time or self.forecast_sites_last_request is None: - - data = self.__call_api("sitelist/") - sites = list() - for jsoned in data['Locations']['Location']: - site = Site() - site.name = jsoned['name'] - site.location_id = jsoned['id'] - site.latitude = jsoned['latitude'] - site.longitude = jsoned['longitude'] - - if 'region' in jsoned: - site.region = jsoned['region'] - - if 'elevation' in jsoned: - site.elevation = jsoned['elevation'] - - if 'unitaryAuthArea' in jsoned: - site.unitaryAuthArea = jsoned['unitaryAuthArea'] - - if 'nationalPark' in jsoned: - site.nationalPark = jsoned['nationalPark'] - - site.api_key = self.api_key - - sites.append(site) - self.forecast_sites_last_request = sites - # Only set self.sites_last_update once self.sites_last_request has - # been set - self.forecast_sites_last_update = time_now - else: - sites = self.forecast_sites_last_request - - return sites - - def get_nearest_site(self, latitude=None, longitude=None): - """ - Deprecated. This function returns nearest Site object to the specified - coordinates. - """ - warning_message = 'This function is deprecated. Use get_nearest_forecast_site() instead' - warn(warning_message, DeprecationWarning, stacklevel=2) - - return self.get_nearest_forecast_site(latitude, longitude) - - def get_nearest_forecast_site(self, latitude, longitude): - """ - This function returns the nearest Site object to the specified - coordinates. - """ - - nearest = False - distance = None - sites = self.get_forecast_sites() - # Sometimes there is a TypeError exception here: sites is None - # So, sometimes self.get_all_sites() has returned None. - for site in sites: - new_distance = \ - self._distance_between_coords( - float(site.longitude), - float(site.latitude), - float(longitude), - float(latitude)) - - if ((distance is None) or (new_distance < distance)): - distance = new_distance - nearest = site - - # If the nearest site is more than 30km away, raise an error - - if distance > 30: - raise APIException("There is no site within 30km.") - - return nearest - - def get_forecast_for_site(self, site_id, frequency="daily"): - """ - Get a forecast for the provided site - - A frequency of "daily" will return two timesteps: - "Day" and "Night". - - A frequency of "3hourly" will return 8 timesteps: - 0, 180, 360 ... 1260 (minutes since midnight UTC) - """ - data = self.__call_api(site_id, {"res": frequency}) - params = data['SiteRep']['Wx']['Param'] - forecast = Forecast(frequency=frequency) - - # If the 'Location' key is missing, there is no data for the site, - # raise an error. - if 'Location' not in data['SiteRep']['DV']: - err_string = ('DataPoint has not returned any data for the' - 'requested site.') - raise APIException(err_string) - - # Check if the other keys we need are in the data returned from the - # datapoint API. If they are not, the data elements in the python class - # are left as None. It is currently the responsibility of the program - # using them to cope with this. - if 'dataDate' in data['SiteRep']['DV']: - forecast.data_date = datetime.strptime(data['SiteRep']['DV']['dataDate'], DATA_DATE_FORMAT).replace(tzinfo=pytz.UTC) - - if 'continent' in data['SiteRep']['DV']['Location']: - forecast.continent = data['SiteRep']['DV']['Location']['continent'] - - if 'country' in data['SiteRep']['DV']['Location']: - forecast.country = data['SiteRep']['DV']['Location']['country'] - - if 'name' in data['SiteRep']['DV']['Location']: - forecast.name = data['SiteRep']['DV']['Location']['name'] - - if 'lon' in data['SiteRep']['DV']['Location']: - forecast.longitude = data['SiteRep']['DV']['Location']['lon'] - - if 'lat' in data['SiteRep']['DV']['Location']: - forecast.latitude = data['SiteRep']['DV']['Location']['lat'] - - if 'i' in data['SiteRep']['DV']['Location']: - forecast.location_id = data['SiteRep']['DV']['Location']['i'] - - if 'elevation' in data['SiteRep']['DV']['Location']: - forecast.elevation = data['SiteRep']['DV']['Location']['elevation'] - - for day in data['SiteRep']['DV']['Location']['Period']: - new_day = Day() - new_day.date = datetime.strptime(day['value'], DATE_FORMAT).replace(tzinfo=pytz.UTC) - - for timestep in day['Rep']: - new_timestep = Timestep() - - # According to the datapoint documentation - # (https://www.metoffice.gov.uk/datapoint/product/uk-daily-site-specific-forecast), - # the data provided are for noon (local time) and midnight - # (local time). This implies that midnight is 00:00 and noon is - # 12:00. - - if timestep['$'] == "Day": - cur_elements = ELEMENTS['Day'] - new_timestep.date = datetime.strptime(day['value'], DATE_FORMAT).replace(tzinfo=pytz.UTC) \ - + timedelta(hours=12) - elif timestep['$'] == "Night": - cur_elements = ELEMENTS['Night'] - new_timestep.date = datetime.strptime(day['value'], DATE_FORMAT).replace(tzinfo=pytz.UTC) - else: - cur_elements = ELEMENTS['Default'] - new_timestep.date = datetime.strptime(day['value'], DATE_FORMAT).replace(tzinfo=pytz.UTC) \ - + timedelta(minutes=int(timestep['$'])) - - if frequency == 'daily': - new_timestep.name = timestep['$'] - elif frequency == '3hourly': - new_timestep.name = int(timestep['$']) - - new_timestep.weather = \ - Element(cur_elements['W'], - timestep[cur_elements['W']], - self._get_wx_units(params, cur_elements['W'])) - new_timestep.weather.text = self._weather_to_text(int(timestep[cur_elements['W']])) - - new_timestep.temperature = \ - Element(cur_elements['T'], - int(timestep[cur_elements['T']]), - self._get_wx_units(params, cur_elements['T'])) - - new_timestep.feels_like_temperature = \ - Element(cur_elements['F'], - int(timestep[cur_elements['F']]), - self._get_wx_units(params, cur_elements['F'])) - - new_timestep.wind_speed = \ - Element(cur_elements['S'], - int(timestep[cur_elements['S']]), - self._get_wx_units(params, cur_elements['S'])) - - new_timestep.wind_direction = \ - Element(cur_elements['D'], - timestep[cur_elements['D']], - self._get_wx_units(params, cur_elements['D'])) - - - new_timestep.wind_gust = \ - Element(cur_elements['G'], - int(timestep[cur_elements['G']]), - self._get_wx_units(params, cur_elements['G'])) - - new_timestep.visibility = \ - Element(cur_elements['V'], - timestep[cur_elements['V']], - self._get_wx_units(params, cur_elements['V'])) - - new_timestep.precipitation = \ - Element(cur_elements['Pp'], - int(timestep[cur_elements['Pp']]), - self._get_wx_units(params, cur_elements['Pp'])) - - new_timestep.humidity = \ - Element(cur_elements['H'], - int(timestep[cur_elements['H']]), - self._get_wx_units(params, cur_elements['H'])) - - if 'U' in cur_elements and cur_elements['U'] in timestep: - new_timestep.uv = \ - Element(cur_elements['U'], - timestep[cur_elements['U']], - self._get_wx_units(params, cur_elements['U'])) - - new_day.timesteps.append(new_timestep) - - # The daily timesteps are not sorted by time. Sort them - if frequency == 'daily': - new_day.timesteps.sort(key=lambda r: r.date) - forecast.days.append(new_day) - - return forecast - - def get_observation_sites(self): - """ - This function returns a list of Site objects for which observations are available. - """ - if (time() - self.observation_sites_last_update) > self.observation_sites_update_time: - self.observation_sites_last_update = time() - data = self.__call_api("sitelist/", None, OBSERVATION_URL) - sites = list() - for jsoned in data['Locations']['Location']: - site = Site() - site.name = jsoned['name'] - site.location_id = jsoned['id'] - site.latitude = jsoned['latitude'] - site.longitude = jsoned['longitude'] - - if 'region' in jsoned: - site.region = jsoned['region'] - - if 'elevation' in jsoned: - site.elevation = jsoned['elevation'] - - if 'unitaryAuthArea' in jsoned: - site.unitaryAuthArea = jsoned['unitaryAuthArea'] - - if 'nationalPark' in jsoned: - site.nationalPark = jsoned['nationalPark'] - - site.api_key = self.api_key - - sites.append(site) - self.observation_sites_last_request = sites - else: - sites = self.observation_sites_last_request - - return sites - - def get_nearest_observation_site(self, latitude, longitude): - """ - This function returns the nearest Site to the specified - coordinates that supports observations - """ - - nearest = False - distance = None - sites = self.get_observation_sites() - for site in sites: - new_distance = \ - self._distance_between_coords( - float(site.longitude), - float(site.latitude), - float(longitude), - float(latitude)) - - if ((distance == None) or (new_distance < distance)): - distance = new_distance - nearest = site - - # If the nearest site is more than 20km away, raise an error - if distance > 20: - raise APIException("There is no site within 30km.") - - return nearest - - - def get_observations_for_site(self, site_id, frequency='hourly'): - """ - Get observations for the provided site - - Returns hourly observations for the previous 24 hours - """ - - data = self.__call_api(site_id,{"res": frequency}, OBSERVATION_URL) - - params = data['SiteRep']['Wx']['Param'] - observation = Observation() - - # Check if keys are in data returned before using them. - if 'dataDate' in data['SiteRep']['DV']: - observation.data_date = datetime.strptime(data['SiteRep']['DV']['dataDate'], DATA_DATE_FORMAT).replace(tzinfo=pytz.UTC) - - if 'Location' in data['SiteRep']['DV']: - if 'continent' in data['SiteRep']['DV']['Location']: - observation.continent = data['SiteRep']['DV']['Location']['continent'] - - if 'country' in data['SiteRep']['DV']['Location']: - observation.country = data['SiteRep']['DV']['Location']['country'] - - if 'name' in data['SiteRep']['DV']['Location']: - observation.name = data['SiteRep']['DV']['Location']['name'] - - if 'lon' in data['SiteRep']['DV']['Location']: - observation.longitude = data['SiteRep']['DV']['Location']['lon'] - - if 'lat' in data['SiteRep']['DV']['Location']: - observation.latitude = data['SiteRep']['DV']['Location']['lat'] - - if 'i' in data['SiteRep']['DV']['Location']: - observation.location_id = data['SiteRep']['DV']['Location']['i'] - - if 'elevation' in data['SiteRep']['DV']['Location']: - observation.elevation = data['SiteRep']['DV']['Location']['elevation'] - - for day in data['SiteRep']['DV']['Location']['Period']: - new_day = Day() - new_day.date = datetime.strptime(day['value'], DATE_FORMAT).replace(tzinfo=pytz.UTC) - - # If the day only has 1 timestep, put it into a list by itself - # so it can be treated the same as a day with multiple timesteps - if type(day['Rep']) is not list: - day['Rep'] = [day['Rep']] - - for timestep in day['Rep']: - # As stated in - # https://www.metoffice.gov.uk/datapoint/product/uk-hourly-site-specific-observations, - # some sites do not have all parameters available for - # observations. The documentation does not state which - # fields may be absent. If the parameter is not available, - # nothing is returned from the API. If this happens the - # value of the element is set to 'Not reported'. This may - # change to the element not being assigned to the timestep. - - new_timestep = Timestep() - # Assume the '$' field is always present. - new_timestep.name = int(timestep['$']) - - cur_elements = ELEMENTS['Observation'] - - new_timestep.date = datetime.strptime(day['value'], DATE_FORMAT).replace(tzinfo=pytz.UTC) + timedelta(minutes=int(timestep['$'])) - - if cur_elements['W'] in timestep: - new_timestep.weather = \ - Element(cur_elements['W'], - timestep[cur_elements['W']], - self._get_wx_units(params, cur_elements['W'])) - new_timestep.weather.text = \ - self._weather_to_text(int(timestep[cur_elements['W']])) - else: - new_timestep.weather = \ - Element(cur_elements['W'], - 'Not reported') - - if cur_elements['T'] in timestep: - new_timestep.temperature = \ - Element(cur_elements['T'], - float(timestep[cur_elements['T']]), - self._get_wx_units(params, cur_elements['T'])) - else: - new_timestep.temperature = \ - Element(cur_elements['T'], - 'Not reported') - - if 'S' in timestep: - new_timestep.wind_speed = \ - Element(cur_elements['S'], - int(timestep[cur_elements['S']]), - self._get_wx_units(params, cur_elements['S'])) - else: - new_timestep.wind_speed = \ - Element(cur_elements['S'], - 'Not reported') - - if 'D' in timestep: - new_timestep.wind_direction = \ - Element(cur_elements['D'], - timestep[cur_elements['D']], - self._get_wx_units(params, cur_elements['D'])) - else: - new_timestep.wind_direction = \ - Element(cur_elements['D'], - 'Not reported') - - if cur_elements['V'] in timestep: - new_timestep.visibility = \ - Element(cur_elements['V'], - int(timestep[cur_elements['V']]), - self._get_wx_units(params, cur_elements['V'])) - new_timestep.visibility.text = self._visibility_to_text(int(timestep[cur_elements['V']])) - else: - new_timestep.visibility = \ - Element(cur_elements['V'], - 'Not reported') - - if cur_elements['H'] in timestep: - new_timestep.humidity = \ - Element(cur_elements['H'], - float(timestep[cur_elements['H']]), - self._get_wx_units(params, cur_elements['H'])) - else: - new_timestep.humidity = \ - Element(cur_elements['H'], - 'Not reported') - - if cur_elements['Dp'] in timestep: - new_timestep.dew_point = \ - Element(cur_elements['Dp'], - float(timestep[cur_elements['Dp']]), - self._get_wx_units(params, - cur_elements['Dp'])) - else: - new_timestep.dew_point = \ - Element(cur_elements['Dp'], - 'Not reported') - - if cur_elements['P'] in timestep: - new_timestep.pressure = \ - Element(cur_elements['P'], - float(timestep[cur_elements['P']]), - self._get_wx_units(params, cur_elements['P'])) - else: - new_timestep.pressure = \ - Element(cur_elements['P'], - 'Not reported') - - if cur_elements['Pt'] in timestep: - new_timestep.pressure_tendency = \ - Element(cur_elements['Pt'], - timestep[cur_elements['Pt']], - self._get_wx_units(params, cur_elements['Pt'])) - else: - new_timestep.pressure_tendency = \ - Element(cur_elements['Pt'], - 'Not reported') - - new_day.timesteps.append(new_timestep) - observation.days.append(new_day) - - return observation diff --git a/datapoint/Observation.py b/datapoint/Observation.py deleted file mode 100644 index 93e12a1..0000000 --- a/datapoint/Observation.py +++ /dev/null @@ -1,20 +0,0 @@ -class Observation(): - def __init__(self): - self.data_date = None - self.continent = None - self.country = None - self.name = None - self.longitude = None - self.latitude = None - self.location_id = None - self.elevation = None - # Stores a list of observations in days - self.days = [] - - def now(self): - """ - Return the final timestep available. This is the most recent - observation. - """ - - return self.days[-1].timesteps[-1] diff --git a/datapoint/Site.py b/datapoint/Site.py deleted file mode 100644 index 331c402..0000000 --- a/datapoint/Site.py +++ /dev/null @@ -1,18 +0,0 @@ -class Site(): - def __init__(self): - self.name = None - self.location_id = None - self.elevation = None - self.latitude = None - self.longitude = None - self.nationalPark = None - self.region = None - self.unitaryAuthArea = None - - def __str__(self): - site_string = '' - for attr, value in self.__dict__.items(): - to_append = attr + ': ' + str(value) + '\n' - site_string += to_append - - return site_string diff --git a/datapoint/Timestep.py b/datapoint/Timestep.py deleted file mode 100644 index 2cb415b..0000000 --- a/datapoint/Timestep.py +++ /dev/null @@ -1,36 +0,0 @@ -from .Element import Element - -class Timestep(): - def __init__(self): - self.name = None - self.date = None - self.weather = None - self.temperature = None - self.feels_like_temperature = None - self.wind_speed = None - self.wind_direction = None - self.wind_gust = None - self.visibility = None - self.uv = None - self.precipitation = None - self.humidity = None - self.pressure = None - self.pressure_tendency = None - self.dew_point = None - - def __iter__(self): - for attr, value in self.__dict__.items(): - yield attr, value - - def elements(self): - """Return a list of the Elements which are not None""" - elements = [el[1] for el in self.__dict__.items() if isinstance(el[1], Element)] - - return elements - - def __str__(self): - timestep_string = '' - for attr, value in self.__dict__.items(): - to_append = attr + ': ' + str(value) + '\n' - timestep_string += to_append - return timestep_string diff --git a/datapoint/__init__.py b/datapoint/__init__.py deleted file mode 100644 index a6181c8..0000000 --- a/datapoint/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Datapoint API to retrieve Met Office data""" - -import os.path - -from datapoint.Manager import Manager -import datapoint.profile - - -def connection(profile_name='default', api_key=None): - """Connect to DataPoint with the given API key profile name.""" - if api_key is None: - profile_fname = datapoint.profile.API_profile_fname(profile_name) - if not os.path.exists(profile_fname): - raise ValueError('Profile not found in {}. Please install your API \n' - 'key with datapoint.profile.install_API_key(' - '"")'.format(profile_fname)) - with open(profile_fname) as fh: - api_key = fh.readlines() - return Manager(api_key=api_key) - -from . import _version -__version__ = _version.get_versions()['version'] diff --git a/datapoint/_version.py b/datapoint/_version.py deleted file mode 100644 index dfcebde..0000000 --- a/datapoint/_version.py +++ /dev/null @@ -1,683 +0,0 @@ - -# This file helps to compute a version number in source trees obtained from -# git-archive tarball (such as those provided by githubs download-from-tag -# feature). Distribution tarballs (built by setup.py sdist) and build -# directories (produced by setup.py build) will contain a much shorter file -# that just contains the computed version number. - -# This file is released into the public domain. -# Generated by versioneer-0.29 -# https://github.com/python-versioneer/python-versioneer - -"""Git implementation of _version.py.""" - -import errno -import os -import re -import subprocess -import sys -from typing import Any, Callable, Dict, List, Optional, Tuple -import functools - - -def get_keywords() -> Dict[str, str]: - """Get the keywords needed to look up the version information.""" - # these strings will be replaced by git during git-archive. - # setup.py/versioneer.py will grep for the variable names, so they must - # each be defined on a line of their own. _version.py will just call - # get_keywords(). - git_refnames = "$Format:%d$" - git_full = "$Format:%H$" - git_date = "$Format:%ci$" - keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} - return keywords - - -class VersioneerConfig: - """Container for Versioneer configuration parameters.""" - - VCS: str - style: str - tag_prefix: str - parentdir_prefix: str - versionfile_source: str - verbose: bool - - -def get_config() -> VersioneerConfig: - """Create, populate and return the VersioneerConfig() object.""" - # these strings are filled in when 'setup.py versioneer' creates - # _version.py - cfg = VersioneerConfig() - cfg.VCS = "git" - cfg.style = "" - cfg.tag_prefix = "" - cfg.parentdir_prefix = "None" - cfg.versionfile_source = "datapoint/_version.py" - cfg.verbose = False - return cfg - - -class NotThisMethod(Exception): - """Exception raised if a method is not valid for the current scenario.""" - - -LONG_VERSION_PY: Dict[str, str] = {} -HANDLERS: Dict[str, Dict[str, Callable]] = {} - - -def register_vcs_handler(vcs: str, method: str) -> Callable: # decorator - """Create decorator to mark a method as the handler of a VCS.""" - def decorate(f: Callable) -> Callable: - """Store f in HANDLERS[vcs][method].""" - if vcs not in HANDLERS: - HANDLERS[vcs] = {} - HANDLERS[vcs][method] = f - return f - return decorate - - -def run_command( - commands: List[str], - args: List[str], - cwd: Optional[str] = None, - verbose: bool = False, - hide_stderr: bool = False, - env: Optional[Dict[str, str]] = None, -) -> Tuple[Optional[str], Optional[int]]: - """Call the given command(s).""" - assert isinstance(commands, list) - process = None - - popen_kwargs: Dict[str, Any] = {} - if sys.platform == "win32": - # This hides the console window if pythonw.exe is used - startupinfo = subprocess.STARTUPINFO() - startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW - popen_kwargs["startupinfo"] = startupinfo - - for command in commands: - try: - dispcmd = str([command] + args) - # remember shell=False, so use git.cmd on windows, not just git - process = subprocess.Popen([command] + args, cwd=cwd, env=env, - stdout=subprocess.PIPE, - stderr=(subprocess.PIPE if hide_stderr - else None), **popen_kwargs) - break - except OSError as e: - if e.errno == errno.ENOENT: - continue - if verbose: - print("unable to run %s" % dispcmd) - print(e) - return None, None - else: - if verbose: - print("unable to find command, tried %s" % (commands,)) - return None, None - stdout = process.communicate()[0].strip().decode() - if process.returncode != 0: - if verbose: - print("unable to run %s (error)" % dispcmd) - print("stdout was %s" % stdout) - return None, process.returncode - return stdout, process.returncode - - -def versions_from_parentdir( - parentdir_prefix: str, - root: str, - verbose: bool, -) -> Dict[str, Any]: - """Try to determine the version from the parent directory name. - - Source tarballs conventionally unpack into a directory that includes both - the project name and a version string. We will also support searching up - two directory levels for an appropriately named parent directory - """ - rootdirs = [] - - for _ in range(3): - dirname = os.path.basename(root) - if dirname.startswith(parentdir_prefix): - return {"version": dirname[len(parentdir_prefix):], - "full-revisionid": None, - "dirty": False, "error": None, "date": None} - rootdirs.append(root) - root = os.path.dirname(root) # up a level - - if verbose: - print("Tried directories %s but none started with prefix %s" % - (str(rootdirs), parentdir_prefix)) - raise NotThisMethod("rootdir doesn't start with parentdir_prefix") - - -@register_vcs_handler("git", "get_keywords") -def git_get_keywords(versionfile_abs: str) -> Dict[str, str]: - """Extract version information from the given file.""" - # the code embedded in _version.py can just fetch the value of these - # keywords. When used from setup.py, we don't want to import _version.py, - # so we do it with a regexp instead. This function is not used from - # _version.py. - keywords: Dict[str, str] = {} - try: - with open(versionfile_abs, "r") as fobj: - for line in fobj: - if line.strip().startswith("git_refnames ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["refnames"] = mo.group(1) - if line.strip().startswith("git_full ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["full"] = mo.group(1) - if line.strip().startswith("git_date ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["date"] = mo.group(1) - except OSError: - pass - return keywords - - -@register_vcs_handler("git", "keywords") -def git_versions_from_keywords( - keywords: Dict[str, str], - tag_prefix: str, - verbose: bool, -) -> Dict[str, Any]: - """Get version information from git keywords.""" - if "refnames" not in keywords: - raise NotThisMethod("Short version file found") - date = keywords.get("date") - if date is not None: - # Use only the last line. Previous lines may contain GPG signature - # information. - date = date.splitlines()[-1] - - # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant - # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 - # -like" string, which we must then edit to make compliant), because - # it's been around since git-1.5.3, and it's too difficult to - # discover which version we're using, or to work around using an - # older one. - date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) - refnames = keywords["refnames"].strip() - if refnames.startswith("$Format"): - if verbose: - print("keywords are unexpanded, not using") - raise NotThisMethod("unexpanded keywords, not a git-archive tarball") - refs = {r.strip() for r in refnames.strip("()").split(",")} - # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of - # just "foo-1.0". If we see a "tag: " prefix, prefer those. - TAG = "tag: " - tags = {r[len(TAG):] for r in refs if r.startswith(TAG)} - if not tags: - # Either we're using git < 1.8.3, or there really are no tags. We use - # a heuristic: assume all version tags have a digit. The old git %d - # expansion behaves like git log --decorate=short and strips out the - # refs/heads/ and refs/tags/ prefixes that would let us distinguish - # between branches and tags. By ignoring refnames without digits, we - # filter out many common branch names like "release" and - # "stabilization", as well as "HEAD" and "master". - tags = {r for r in refs if re.search(r'\d', r)} - if verbose: - print("discarding '%s', no digits" % ",".join(refs - tags)) - if verbose: - print("likely tags: %s" % ",".join(sorted(tags))) - for ref in sorted(tags): - # sorting will prefer e.g. "2.0" over "2.0rc1" - if ref.startswith(tag_prefix): - r = ref[len(tag_prefix):] - # Filter out refs that exactly match prefix or that don't start - # with a number once the prefix is stripped (mostly a concern - # when prefix is '') - if not re.match(r'\d', r): - continue - if verbose: - print("picking %s" % r) - return {"version": r, - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": None, - "date": date} - # no suitable tags, so version is "0+unknown", but full hex is still there - if verbose: - print("no suitable tags, using unknown + full revision id") - return {"version": "0+unknown", - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": "no suitable tags", "date": None} - - -@register_vcs_handler("git", "pieces_from_vcs") -def git_pieces_from_vcs( - tag_prefix: str, - root: str, - verbose: bool, - runner: Callable = run_command -) -> Dict[str, Any]: - """Get version from 'git describe' in the root of the source tree. - - This only gets called if the git-archive 'subst' keywords were *not* - expanded, and _version.py hasn't already been rewritten with a short - version string, meaning we're inside a checked out source tree. - """ - GITS = ["git"] - if sys.platform == "win32": - GITS = ["git.cmd", "git.exe"] - - # GIT_DIR can interfere with correct operation of Versioneer. - # It may be intended to be passed to the Versioneer-versioned project, - # but that should not change where we get our version from. - env = os.environ.copy() - env.pop("GIT_DIR", None) - runner = functools.partial(runner, env=env) - - _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, - hide_stderr=not verbose) - if rc != 0: - if verbose: - print("Directory %s not under git control" % root) - raise NotThisMethod("'git rev-parse --git-dir' returned error") - - # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] - # if there isn't one, this yields HEX[-dirty] (no NUM) - describe_out, rc = runner(GITS, [ - "describe", "--tags", "--dirty", "--always", "--long", - "--match", f"{tag_prefix}[[:digit:]]*" - ], cwd=root) - # --long was added in git-1.5.5 - if describe_out is None: - raise NotThisMethod("'git describe' failed") - describe_out = describe_out.strip() - full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root) - if full_out is None: - raise NotThisMethod("'git rev-parse' failed") - full_out = full_out.strip() - - pieces: Dict[str, Any] = {} - pieces["long"] = full_out - pieces["short"] = full_out[:7] # maybe improved later - pieces["error"] = None - - branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], - cwd=root) - # --abbrev-ref was added in git-1.6.3 - if rc != 0 or branch_name is None: - raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") - branch_name = branch_name.strip() - - if branch_name == "HEAD": - # If we aren't exactly on a branch, pick a branch which represents - # the current commit. If all else fails, we are on a branchless - # commit. - branches, rc = runner(GITS, ["branch", "--contains"], cwd=root) - # --contains was added in git-1.5.4 - if rc != 0 or branches is None: - raise NotThisMethod("'git branch --contains' returned error") - branches = branches.split("\n") - - # Remove the first line if we're running detached - if "(" in branches[0]: - branches.pop(0) - - # Strip off the leading "* " from the list of branches. - branches = [branch[2:] for branch in branches] - if "master" in branches: - branch_name = "master" - elif not branches: - branch_name = None - else: - # Pick the first branch that is returned. Good or bad. - branch_name = branches[0] - - pieces["branch"] = branch_name - - # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] - # TAG might have hyphens. - git_describe = describe_out - - # look for -dirty suffix - dirty = git_describe.endswith("-dirty") - pieces["dirty"] = dirty - if dirty: - git_describe = git_describe[:git_describe.rindex("-dirty")] - - # now we have TAG-NUM-gHEX or HEX - - if "-" in git_describe: - # TAG-NUM-gHEX - mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) - if not mo: - # unparsable. Maybe git-describe is misbehaving? - pieces["error"] = ("unable to parse git-describe output: '%s'" - % describe_out) - return pieces - - # tag - full_tag = mo.group(1) - if not full_tag.startswith(tag_prefix): - if verbose: - fmt = "tag '%s' doesn't start with prefix '%s'" - print(fmt % (full_tag, tag_prefix)) - pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" - % (full_tag, tag_prefix)) - return pieces - pieces["closest-tag"] = full_tag[len(tag_prefix):] - - # distance: number of commits since tag - pieces["distance"] = int(mo.group(2)) - - # commit: short hex revision ID - pieces["short"] = mo.group(3) - - else: - # HEX: no tags - pieces["closest-tag"] = None - out, rc = runner(GITS, ["rev-list", "HEAD", "--left-right"], cwd=root) - pieces["distance"] = len(out.split()) # total number of commits - - # commit date: see ISO-8601 comment in git_versions_from_keywords() - date = runner(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip() - # Use only the last line. Previous lines may contain GPG signature - # information. - date = date.splitlines()[-1] - pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) - - return pieces - - -def plus_or_dot(pieces: Dict[str, Any]) -> str: - """Return a + if we don't already have one, else return a .""" - if "+" in pieces.get("closest-tag", ""): - return "." - return "+" - - -def render_pep440(pieces: Dict[str, Any]) -> str: - """Build up version string, with post-release "local version identifier". - - Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you - get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty - - Exceptions: - 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += plus_or_dot(pieces) - rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - else: - # exception #1 - rendered = "0+untagged.%d.g%s" % (pieces["distance"], - pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - return rendered - - -def render_pep440_branch(pieces: Dict[str, Any]) -> str: - """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . - - The ".dev0" means not master branch. Note that .dev0 sorts backwards - (a feature branch will appear "older" than the master branch). - - Exceptions: - 1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - if pieces["branch"] != "master": - rendered += ".dev0" - rendered += plus_or_dot(pieces) - rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - else: - # exception #1 - rendered = "0" - if pieces["branch"] != "master": - rendered += ".dev0" - rendered += "+untagged.%d.g%s" % (pieces["distance"], - pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - return rendered - - -def pep440_split_post(ver: str) -> Tuple[str, Optional[int]]: - """Split pep440 version string at the post-release segment. - - Returns the release segments before the post-release and the - post-release version number (or -1 if no post-release segment is present). - """ - vc = str.split(ver, ".post") - return vc[0], int(vc[1] or 0) if len(vc) == 2 else None - - -def render_pep440_pre(pieces: Dict[str, Any]) -> str: - """TAG[.postN.devDISTANCE] -- No -dirty. - - Exceptions: - 1: no tags. 0.post0.devDISTANCE - """ - if pieces["closest-tag"]: - if pieces["distance"]: - # update the post release segment - tag_version, post_version = pep440_split_post(pieces["closest-tag"]) - rendered = tag_version - if post_version is not None: - rendered += ".post%d.dev%d" % (post_version + 1, pieces["distance"]) - else: - rendered += ".post0.dev%d" % (pieces["distance"]) - else: - # no commits, use the tag as the version - rendered = pieces["closest-tag"] - else: - # exception #1 - rendered = "0.post0.dev%d" % pieces["distance"] - return rendered - - -def render_pep440_post(pieces: Dict[str, Any]) -> str: - """TAG[.postDISTANCE[.dev0]+gHEX] . - - The ".dev0" means dirty. Note that .dev0 sorts backwards - (a dirty tree will appear "older" than the corresponding clean one), - but you shouldn't be releasing software with -dirty anyways. - - Exceptions: - 1: no tags. 0.postDISTANCE[.dev0] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - rendered += plus_or_dot(pieces) - rendered += "g%s" % pieces["short"] - else: - # exception #1 - rendered = "0.post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - rendered += "+g%s" % pieces["short"] - return rendered - - -def render_pep440_post_branch(pieces: Dict[str, Any]) -> str: - """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . - - The ".dev0" means not master branch. - - Exceptions: - 1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%d" % pieces["distance"] - if pieces["branch"] != "master": - rendered += ".dev0" - rendered += plus_or_dot(pieces) - rendered += "g%s" % pieces["short"] - if pieces["dirty"]: - rendered += ".dirty" - else: - # exception #1 - rendered = "0.post%d" % pieces["distance"] - if pieces["branch"] != "master": - rendered += ".dev0" - rendered += "+g%s" % pieces["short"] - if pieces["dirty"]: - rendered += ".dirty" - return rendered - - -def render_pep440_old(pieces: Dict[str, Any]) -> str: - """TAG[.postDISTANCE[.dev0]] . - - The ".dev0" means dirty. - - Exceptions: - 1: no tags. 0.postDISTANCE[.dev0] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - else: - # exception #1 - rendered = "0.post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - return rendered - - -def render_git_describe(pieces: Dict[str, Any]) -> str: - """TAG[-DISTANCE-gHEX][-dirty]. - - Like 'git describe --tags --dirty --always'. - - Exceptions: - 1: no tags. HEX[-dirty] (note: no 'g' prefix) - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"]: - rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) - else: - # exception #1 - rendered = pieces["short"] - if pieces["dirty"]: - rendered += "-dirty" - return rendered - - -def render_git_describe_long(pieces: Dict[str, Any]) -> str: - """TAG-DISTANCE-gHEX[-dirty]. - - Like 'git describe --tags --dirty --always -long'. - The distance/hash is unconditional. - - Exceptions: - 1: no tags. HEX[-dirty] (note: no 'g' prefix) - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) - else: - # exception #1 - rendered = pieces["short"] - if pieces["dirty"]: - rendered += "-dirty" - return rendered - - -def render(pieces: Dict[str, Any], style: str) -> Dict[str, Any]: - """Render the given version pieces into the requested style.""" - if pieces["error"]: - return {"version": "unknown", - "full-revisionid": pieces.get("long"), - "dirty": None, - "error": pieces["error"], - "date": None} - - if not style or style == "default": - style = "pep440" # the default - - if style == "pep440": - rendered = render_pep440(pieces) - elif style == "pep440-branch": - rendered = render_pep440_branch(pieces) - elif style == "pep440-pre": - rendered = render_pep440_pre(pieces) - elif style == "pep440-post": - rendered = render_pep440_post(pieces) - elif style == "pep440-post-branch": - rendered = render_pep440_post_branch(pieces) - elif style == "pep440-old": - rendered = render_pep440_old(pieces) - elif style == "git-describe": - rendered = render_git_describe(pieces) - elif style == "git-describe-long": - rendered = render_git_describe_long(pieces) - else: - raise ValueError("unknown style '%s'" % style) - - return {"version": rendered, "full-revisionid": pieces["long"], - "dirty": pieces["dirty"], "error": None, - "date": pieces.get("date")} - - -def get_versions() -> Dict[str, Any]: - """Get version information or return default if unable to do so.""" - # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have - # __file__, we can work backwards from there to the root. Some - # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which - # case we can only use expanded keywords. - - cfg = get_config() - verbose = cfg.verbose - - try: - return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, - verbose) - except NotThisMethod: - pass - - try: - root = os.path.realpath(__file__) - # versionfile_source is the relative path from the top of the source - # tree (where the .git directory might live) to this file. Invert - # this to find the root from __file__. - for _ in cfg.versionfile_source.split('/'): - root = os.path.dirname(root) - except NameError: - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, - "error": "unable to find root of source tree", - "date": None} - - try: - pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) - return render(pieces, cfg.style) - except NotThisMethod: - pass - - try: - if cfg.parentdir_prefix: - return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) - except NotThisMethod: - pass - - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, - "error": "unable to compute version", "date": None} diff --git a/datapoint/profile.py b/datapoint/profile.py deleted file mode 100644 index 54803b4..0000000 --- a/datapoint/profile.py +++ /dev/null @@ -1,18 +0,0 @@ -import os - -import appdirs - - -def API_profile_fname(profile_name='default'): - """Get the API key profile filename.""" - return os.path.join(appdirs.user_data_dir('DataPoint'), - profile_name + '.key') - - -def install_API_key(api_key, profile_name='default'): - """Put the given API key into the given profile name.""" - fname = API_profile_fname(profile_name) - if not os.path.isdir(os.path.dirname(fname)): - os.makedirs(os.path.dirname(fname)) - with open(fname, 'w') as fh: - fh.write(api_key) diff --git a/datapoint/regions/RegionManager.py b/datapoint/regions/RegionManager.py deleted file mode 100644 index 54cffc4..0000000 --- a/datapoint/regions/RegionManager.py +++ /dev/null @@ -1,66 +0,0 @@ -import json -from time import time - -import requests - -from datapoint.Site import Site -from datapoint.regions.region_names import REGION_NAMES -REGIONS_BASE_URL = 'http://datapoint.metoffice.gov.uk/public/data/txt/wxfcs/regionalforecast/json' - - -class RegionManager(): - ''' - Datapoint Manager for national and regional text forecasts - ''' - def __init__(self, api_key, base_url=None): - self.api_key = api_key - self.all_regions_path = '/sitelist' - if not base_url: - self.base_url = REGIONS_BASE_URL - - # The list of regions changes infrequently so limit to requesting it - # every hour. - self.regions_last_update = 0 - self.regions_last_request = None - self.regions_update_time = 3600 - - def call_api(self, path, **kwargs): - ''' - Call datapoint api - ''' - if 'key' not in kwargs: - kwargs['key'] = self.api_key - req = requests.get('{0}{1}'.format(self.base_url, path), params=kwargs) - - if req.status_code != requests.codes.ok: - req.raise_for_status() - - return req.json() - - def get_all_regions(self): - ''' - Request a list of regions from Datapoint. Returns each Region - as a Site object. Regions rarely change, so we cache the response - for one hour to minimise requests to API. - ''' - if (time() - self.regions_last_update) < self.regions_update_time: - return self.regions_last_request - - response = self.call_api(self.all_regions_path) - regions = [] - for location in response['Locations']['Location']: - region = Site() - region.location_id = location['@id'] - region.region = location['@name'] - region.name = REGION_NAMES[location['@name']] - regions.append(region) - - self.regions_last_update = time() - self.regions_last_request = regions - return regions - - def get_raw_forecast(self, region_id): - ''' - Request unformatted forecast for a specific region_id. - ''' - return self.call_api('/{0}'.format(region_id)) diff --git a/datapoint/regions/region_names.py b/datapoint/regions/region_names.py deleted file mode 100644 index 8dd5d45..0000000 --- a/datapoint/regions/region_names.py +++ /dev/null @@ -1,19 +0,0 @@ -REGION_NAMES = { - 'os': 'Orkney & Shetland', - 'he': 'Highland & Eilean Siar', - 'gr': 'Grampian', - 'ta': 'Tayside', - 'st': 'Strathclyde', - 'dg': 'Dumfries, Galloway, Lothian', - 'ni': 'Northern Ireland', - 'yh': 'Yorkshire & the Humber', - 'ne': 'Northeast England', - 'em': 'East Midlands', - 'ee': 'East of England', - 'se': 'London & Southeast England', - 'nw': 'Northwest England', - 'wm': 'West Midlands', - 'sw': 'Southwest England', - 'wl': 'Wales', - 'uk': 'UK', -} \ No newline at end of file diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d0c3cbf --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index 60dccb2..0000000 --- a/docs/conf.py +++ /dev/null @@ -1,175 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Configuration file for the Sphinx documentation builder. -# -# This file does only contain a selection of the most common options. For a -# full list see the documentation: -# http://www.sphinx-doc.org/en/master/config - -# -- Path setup -------------------------------------------------------------- - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -import os -import sys -sys.path.insert(0, os.path.abspath('.')) -# Need to change the place we put in path to work with readthedocs -sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir)) -import datapoint - -# -- Project information ----------------------------------------------------- - -project = 'datapoint-python' -copyright = '2014, Jacon Tomlinson' -author = 'Jacob Tomlinson, Emlyn Price' - -# The full version, including alpha/beta/rc tags -release = datapoint.__version__ -# The short X.Y version -version = '.'.join(release.split('.')[:2]) - - -# -- General configuration --------------------------------------------------- - -# If your documentation needs a minimal Sphinx version, state it here. -# -# needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ -] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# -# source_suffix = ['.rst', '.md'] -source_suffix = '.rst' - -# The master toctree document. -master_doc = 'index' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = None - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = None - - -# -- Options for HTML output ------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -html_theme = 'alabaster' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# -# html_theme_options = {} - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# Custom sidebar templates, must be a dictionary that maps document names -# to template names. -# -# The default sidebars (for documents that don't match any pattern) are -# defined by theme itself. Builtin themes are using these templates by -# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', -# 'searchbox.html']``. -# -# html_sidebars = {} - - -# -- Options for HTMLHelp output --------------------------------------------- - -# Output file base name for HTML help builder. -htmlhelp_basename = 'datapoint-python-doc' - - -# -- Options for LaTeX output ------------------------------------------------ - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, 'datapoint-python.tex', 'datapoint-python Documentation', - 'Jacob Tomlinson, Emlyn Price', 'manual'), -] - - -# -- Options for manual page output ------------------------------------------ - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'datapoint-python', 'datapoint-python Documentation', - [author], 1) -] - - -# -- Options for Texinfo output ---------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - (master_doc, 'datapoint-python', 'datapoint-python Documentation', - author, 'datapoint-python', 'One line description of project.', - 'Miscellaneous'), -] - - -# -- Options for Epub output ------------------------------------------------- - -# Bibliographic Dublin Core info. -epub_title = project - -# The unique identifier of the text. This can be a ISBN number -# or the project homepage. -# -# epub_identifier = '' - -# A unique identification for the text. -# -# epub_uid = '' - -# A list of files that should not be packed into the epub file. -epub_exclude_files = ['search.html'] diff --git a/docs/forecast_sites_map.png b/docs/forecast_sites_map.png deleted file mode 100644 index 406489b..0000000 Binary files a/docs/forecast_sites_map.png and /dev/null differ diff --git a/docs/getting-started.rst b/docs/getting-started.rst deleted file mode 100644 index c20f7bd..0000000 --- a/docs/getting-started.rst +++ /dev/null @@ -1,86 +0,0 @@ -Getting started -=============== - -Getting started with DataPoint for Python is simple and you can write a -simple script which prints out data in just 6 lines of Python. - -API Key -------- - -To access DataPoint you need to -`register `__ with the Met -Office and get yourself an API key. The process is simple and just -ensures that you don’t abuse the service. - -Connecting to DataPoint ------------------------ - -Now that we have an API key we can import the module: - -:: - - import datapoint - -And create a connection to DataPoint: - -:: - - conn = datapoint.connection(api_key="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee") - -This creates a :ref:`manager` object which manages our connection and interacts -with DataPoint for us, we’ll discuss Manager Objects in depth later but for now -you just need to know that it looks after your API key and has a load of methods -to return data from DataPoint. - -Getting data from DataPoint ---------------------------- - -So now that we have our Manager Object with a connection to DataPoint we can -request some data. Our goal is to request some forecast data but first we need -to know the site ID for the location we want data for. Luckily the Manager -Object has a method to return a :ref:`site` object, which contains the ID among -other things, from a specified latitude and longitude. - -We can simply request a Site Object like so: - -:: - - site = conn.get_nearest_forecast_site(51.500728, -0.124626) - -For now we’re just going to use this object to get us our forecast but -you’ll find more information about what the Site Object contains later. - -Let’s call another of the Manager Object’s methods to give us a -:ref:`forecast` object for our site: - -:: - - forecast = conn.get_forecast_for_site(site.location_id, "3hourly") - -We’ve given this method two parameters, the site ID for the forecast we want and -also a forecast type of “3hourly”. We’ll discuss the forecast types later on. - -This Forecast Object which has been returned to us contains lots of information -which we will cover in a later section, right now we’re just going to get the -:ref:`timestep` object which represents right this minute: - -:: - - current_timestep = forecast.now() - -This Timestep Object contains many different details about the weather -but for now we’ll just print out the weather text. - -:: - - print current_timestep.weather.text - -And there you have it. If you followed all the steps you should have -printed out the current weather for your chosen location. - -Further Examples ----------------- - -For more code examples please have a look in the `examples -folder `__ -in the GitHub project. diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index c575334..0000000 --- a/docs/index.rst +++ /dev/null @@ -1,10 +0,0 @@ -datapoint-python -================ - -.. toctree:: - :maxdepth: 2 - - install - getting-started - locations - objects diff --git a/docs/locations.rst b/docs/locations.rst deleted file mode 100644 index d595449..0000000 --- a/docs/locations.rst +++ /dev/null @@ -1,30 +0,0 @@ -Locations -========= - -There are around 6000 sites available for forecasts, and around 150 sites available for observations. - -Forecast sites --------------- - -The locations of the sites available for which forecasts are shown on the map below. Forecasts are restricted to locations within 30 km of the nearest site. This includes all of the United Kingdom. Most of the UK is within 10 km of the nearest site. - -.. figure:: forecast_sites_map.png - :alt: Map showing the locations of the sites for which forecasts are available. - :name: forecast_map - - The locations of the sites for which forecasts are available. Produced using cartopy_. - - -Observation sites ------------------ -The locations of the sites available for which observations are shown on the map below. - -.. At present there is no restriction on the - -.. figure:: observation_sites_map.png - :alt: Map showing the locations of the sites for which observations are available. - :name: observation_map - - The locations of the sites for which observations are available. Produced using cartopy_. - -.. _cartopy: https://github.com/SciTools/cartopy diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..747ffb7 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/objects.rst b/docs/objects.rst deleted file mode 100644 index 12ce2a0..0000000 --- a/docs/objects.rst +++ /dev/null @@ -1,287 +0,0 @@ -Objects -======= - -DataPoint for Python makes use of objects for almost everything. There -are 6 different objects which will be returned by the module. - -The diagram below shows the main classes used by the library, and how to -move between them. - -.. figure:: https://user-images.githubusercontent.com/22224469/51768591-a54fb580-20d8-11e9-851a-cbc3dc434cca.png - :alt: classes - - classes - - -.. _manager: - -Manager -------- - -The object which stores your API key and has methods to access the API. - -.. _manager_attributes: - -Attributes -^^^^^^^^^^ - -============================== ==================== -attribute type ------------------------------- -------------------- -api_key string -call_response dict -forecast_sites_last_update float -forecast_sites_last_request list of Site objects -forecast_sites_update_time int -observation_sites_last_update float -observation_sites_last_request list of Site objects -observation_sites_update_time int -============================== ==================== - -.. _manager_methods: - -Methods -^^^^^^^ - -get_forecast_sites() -'''''''''''''''''''' - -Returns a list of available forecast sites. - -- returns: list of Site objects - -get_nearest_forecast_site(latitude=False, longitude=False) -'''''''''''''''''''''''''''''''''''''''''''''''''''''''''' - -Returns the nearest Site object to the specified coordinates which can provide a forecast. - -- param latitude: int or float -- param longitude: int or float - -- returns: Site - -get_forecast_for_site(site_id, frequency=“daily”) -'''''''''''''''''''''''''''''''''''''''''''''''''' - -Get a forecast for the provided site. A frequency of “daily” will return -two timesteps: “Day” and “Night”. A frequency of “3hourly” will return 8 -timesteps: 0, 180, 360 … 1260 (minutes since midnight UTC) - -- param site_id: string or unicode -- param frequency: string (“daily” or “3hourly”) - -- returns: Forecast - -get_observation_sites() -''''''''''''''''''''''' - -Returns a list of sites for which observations are available. - -- returns: list of Site objects - -get_nearest_observation_site(longitude=False, latitude=False) -''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' - -Returns the nearest Site object to the specified coordinates. - -- param longitude: int or float -- param latitude: int or float - -- returns: Site - -get_observations_for_site(site_id, frequency='hourly') -'''''''''''''''''''''''''''''''''''''''''''''''''''''' - -Get the observations for the provided site. -Only hourly observations are available, and provide the last 24 hours of data. - -- param site_id: string or unicode -- param frequency: string ("daily" or "3hourly") - -- returns: Observation - -.. _site: - -Site ----- - -An object containing details about a specific forecast site. - -.. _site_attributes: - -Attributes -^^^^^^^^^^ - -=============== ======= -attribute type ---------------- ------- -api_key string -name unicode -id unicode -elevation unicode -latitude unicode -longitude unicode -nationalPark unicode -region unicode -unitaryAuthArea unicode -=============== ======= - -.. _forecast: - -Forecast --------- - -An object with properties of a single forecast and a list of Day -objects. - -.. _forecast_attributes: - -Attributes -^^^^^^^^^^ - -========== =================== -attribute type ----------- ------------------- -api_key string -data_date datetime -continent unicode -country unicode -name unicode -longitude unicode -latitude unicode -id unicode -elevation unicode -days list of Day objects -========== =================== - -.. _forecast_methods: - -Methods -^^^^^^^ - -now() -''''' - -Get the current timestep from this forecast - -- returns: Timestep - -Observation ------------ - -An object with the properties of a single observation and a list of Day objects. - -.. _observation_attributes: - -Attributes -^^^^^^^^^^ - -========== =================== -attribute type ----------- ------------------- -api_key string -data_date datetime -continent unicode -country unicode -name unicode -longitude unicode -latitude unicode -id unicode -elevation unicode -days list of Day objects -========== =================== - - -.. _observation_methods: - -Methods -^^^^^^^ - -now() -''''' - -Get the current timestep from this observation - -- returns: Timestep - - -Day ---- - -An object with properties of a single day and a list of Timestep -objects. - -.. _day_attributes: - -Attributes -^^^^^^^^^^ - -========= ======================== -attribute type ---------- ------------------------ -api_key string -date datetime -timesteps list of Timestep objects -========= ======================== - -.. _timestep: - -Timestep --------- - -An object with each forecast property (wind, temp, etc) for a specific -time, in the form of Element objects. - -.. _timestep_attributes: - -Attributes -^^^^^^^^^^ - -====================== ======================== -attribute type ----------------------- ------------------------ -api_key string -name string -date datetime -weather Element -temperature Element -feels_like_temperature Element -wind_speed Element -wind_direction Element -wind_gust Element -visibility Element -uv Element -precipitation Element -humidity Element -====================== ======================== - -Methods -^^^^^^^ - -elements() -'''''''''' - -Get a list of element objects in the Timestep. - -- returns: List of element objects - - - -Element -------- - -An object with properties about a specific weather element. - -.. _element_attributes: - -Attributes -^^^^^^^^^^ - -========= ==================== -attribute type ---------- -------------------- -id string -value int, float or string -units unicode -text string or None -========= ==================== diff --git a/docs/observation_sites_map.png b/docs/observation_sites_map.png deleted file mode 100644 index fa4cd5b..0000000 Binary files a/docs/observation_sites_map.png and /dev/null differ diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..3ad934d --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,2 @@ +sphinx +-e . diff --git a/docs/source/api-reference.rst b/docs/source/api-reference.rst new file mode 100644 index 0000000..da16937 --- /dev/null +++ b/docs/source/api-reference.rst @@ -0,0 +1,8 @@ +API reference +============= + +.. automodule:: datapoint.Manager + :members: + +.. automodule:: datapoint.Forecast + :members: diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..ce3dae7 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,32 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information +import importlib + +project = "datapoint-python" +copyright = "2024, Emily Price, Jacob Tomlinson" +author = "Emily Price, Jacob Tomlinson" + +release = importlib.metadata.version("datapoint") +version = importlib.metadata.version("datapoint") + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [ + "sphinx.ext.autodoc", +] + +templates_path = ["_templates"] +exclude_patterns = [] + + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = "alabaster" +html_static_path = ["_static"] diff --git a/docs/source/getting-started.rst b/docs/source/getting-started.rst new file mode 100644 index 0000000..fe64f2a --- /dev/null +++ b/docs/source/getting-started.rst @@ -0,0 +1,70 @@ +Getting started +=============== + +Getting started with DataPoint for Python is simple and you can write a +simple script which prints out data in just 6 lines of Python. + +API Key +------- + +To access DataPoint you need to `register `__ +with the Met Office and get yourself an API key. The process is simple and just +ensures that you don’t abuse the service. You will need access to the +Site-Specific forecast API. + +Connecting to DataHub +----------------------- + +Now that you have an API key you can import the module: + +:: + + import datapoint + +And create a connection to DataHub: + +:: + + manager = datapoint.Manager(api_key="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee") + +This creates a `manager` object which manages the connection and interacts +with DataHub. + +Getting data from DataHub +--------------------------- + +So now that you have a Manager object with a connection to DataHub you can +request some data. To do this, use the `manager` object: + +:: + + forecast = manager.get_forecast(51, 0, "hourly") + +This takes three parameters: the latitude and longitude of the location you want +a forecast for and also a forecast type of “hourly”. We’ll discuss the forecast +types later on. + +This Forecast Object which has been returned to us contains lots of information +which we will cover in a later section, right now we’re just going to get the +data for the current time: + +:: + + current_weather = forecast.now() + +This is a dict which contains many different details about the weather +but for now we’ll just print out one field. + +:: + + print(current_weather["feelsLikeTemperature"]) + +And there you have it. If you followed all the steps you should have +printed out the current weather for your chosen location. + +Further Examples +---------------- + +For more code examples please have a look in the `examples +folder `__ +in the GitHub project. diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..9e5288a --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,16 @@ +.. datapoint-python documentation master file, created by + sphinx-quickstart on Tue Nov 12 17:56:23 2024. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +datapoint-python documentation +============================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + install + getting-started + migration + api-reference diff --git a/docs/install.rst b/docs/source/install.rst similarity index 50% rename from docs/install.rst rename to docs/source/install.rst index 2961a7c..7bbf6d0 100644 --- a/docs/install.rst +++ b/docs/source/install.rst @@ -29,19 +29,6 @@ and to upgrade it in the future: pip install git+git://github.com/ejep/datapoint-python.git@master --upgrade -Easy Install ------------- - -Or if you really feel the need then you can use -`easy_install `__. - -:: - - easy_install datapoint - -But you `probably -shouldn’t `__. - Source ------ @@ -66,26 +53,3 @@ Then run the setup :: python setup.py install - -Windows -------- - -- Install `python `__ - you can see - supported versions in the - `readme `__ -- Install the appropriate - `setuptools `__ - python extension for your machine -- Install the appropriate - `pip `__ python - extension for your machine -- Add pip to your environment variables: - - - Run **Start** > **Edit the environment variables for your - account** - - Create a new variable: - - **name** pip - - **value** the path to **pip.exe** (this should be something like - :code:`C:\Python27\Scripts`) - -- From the command line run **pip install datapoint** diff --git a/docs/source/migration.rst b/docs/source/migration.rst new file mode 100644 index 0000000..37501c1 --- /dev/null +++ b/docs/source/migration.rst @@ -0,0 +1,118 @@ +Migration from DataPoint +======================== + +The new APIs the Met Office provide via DataHub are very different in behaviour +to the old APIs which were provided via DataPoint. As such this library has +changed greatly. + +The main changes are below. + +No concept of 'sites' +--------------------- + +There is no concept of retrieving a site id for a location before requesting a +forecast. Now a latitude and longitude are provided to the library directly. + +No observations +--------------- + +The new API does not provide 'observations' like DataPoint. However, the current +state of the weather is returned as part of the forecast responses. As such, +this library no longer provides separate 'observations'. + +Simplified object hierarchy +--------------------------- + +Python dicts are used instead of classes to allow more flexibility with handling +data returned from the MetOffice API, and because new MetOffice API provides +data with a more convenient structure. The concept of 'Days' has also been +removed from the library and instead all time steps are provided in one list. +The data structure for a single time step is:: + + { + 'time': datetime.datetime(2024, 2, 19, 13, 0, tzinfo=datetime.timezone.utc), + 'screenTemperature': { + 'value': 10.09, + 'description': 'Screen Air Temperature', + 'unit_name': 'degrees Celsius', + 'unit_symbol': 'Cel' + }, + 'screenDewPointTemperature': { + 'value': 8.08, + 'description': 'Screen Dew Point Temperature', + 'unit_name': 'degrees Celsius', + 'unit_symbol': 'Cel' + }, + 'feelsLikeTemperature': { + 'value': 6.85, + 'description': 'Feels Like Temperature', + 'unit_name': 'degrees Celsius', + 'unit_symbol': 'Cel' + }, + 'windSpeed10m': { + 'value': 7.57, + 'description': '10m Wind Speed', + 'unit_name': 'metres per second', + 'unit_symbol': 'm/s' + }, + 'windDirectionFrom10m': { + 'value': 263, + 'description': '10m Wind From Direction', + 'unit_name': 'degrees', + 'unit_symbol': 'deg' + }, + 'windGustSpeed10m': { + 'value': 12.31, + 'description': '10m Wind Gust Speed', + 'unit_name': 'metres per second', + 'unit_symbol': 'm/s' + }, + 'visibility': { + 'value': 21201, + 'description': 'Visibility', + 'unit_name': 'metres', + 'unit_symbol': 'm' + }, + 'screenRelativeHumidity': { + 'value': 87.81, + 'description': 'Screen Relative Humidity', + 'unit_name': 'percentage', + 'unit_symbol': '%' + }, + 'mslp': { + 'value': 103080, + 'description': 'Mean Sea Level Pressure', + 'unit_name': 'pascals', + 'unit_symbol': 'Pa' + }, + 'uvIndex': { + 'value': 1, + 'description': 'UV Index', + 'unit_name': 'dimensionless', + 'unit_symbol': '1' + }, + 'significantWeatherCode': { + 'value': 'Cloudy', + 'description': 'Significant Weather Code', + 'unit_name': 'dimensionless', + 'unit_symbol': '1' + }, + 'precipitationRate': { + 'value': 0.0, + 'description': 'Precipitation Rate', + 'unit_name': 'millimetres per hour', + 'unit_symbol': 'mm/h' + }, + 'probOfPrecipitation': { + 'value': 21, + 'description': 'Probability of Precipitation', + 'unit_name': 'percentage', + 'unit_symbol': '%' + } + } + +Different data provided +----------------------- + +There are some differences in what data are provided in each weather forecast +compared to the old DataPoint API, and in the names of the features. diff --git a/examples/current_weather/current_weather.py b/examples/current_weather/current_weather.py index 6de1117..b00db6e 100644 --- a/examples/current_weather/current_weather.py +++ b/examples/current_weather/current_weather.py @@ -7,19 +7,12 @@ import datapoint # Create datapoint connection -conn = datapoint.Manager(api_key="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee") +manager = datapoint.Manager(api_key="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee") -# Get nearest site and print out its name - -site = conn.get_nearest_forecast_site(51.500728, -0.124626) -print(site.name) - -# Get a forecast for the nearest site -forecast = conn.get_forecast_for_site(site.location_id, "3hourly") +# Get a forecast for the nearest location +forecast = manager.get_forecast(51.500728, -0.124626, "hourly") # Get the current timestep using now() and print out some info now = forecast.now() -print(now.weather.text) -print("%s%s%s" % (now.temperature.value, - '\xb0', #Unicode character for degree symbol - now.temperature.units)) +print(now["significantWeatherCode"]) +print(f"{now['screenTemperature']['value']} {now['screenTemperature']['unit_symbol']}") diff --git a/examples/postcodes_to_lat_lng/postcodes_to_lat_lng.py b/examples/postcodes_to_lat_lng/postcodes_to_lat_lng.py index 6decab7..a17f81b 100644 --- a/examples/postcodes_to_lat_lng/postcodes_to_lat_lng.py +++ b/examples/postcodes_to_lat_lng/postcodes_to_lat_lng.py @@ -3,29 +3,24 @@ A variation on current_weather.py which uses postcodes rather than lon lat. """ -import datapoint import postcodes_io_api +import datapoint + # Create datapoint connection -conn = datapoint.Manager(api_key="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee") +manager = datapoint.Manager(api_key="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee") # Get longitude and latitude from postcode postcodes_conn = postcodes_io_api.Api() -postcode = postcodes_conn.get_postcode('SW1A 2AA') -latitude = postcode['result']['latitude'] -longitude = postcode['result']['longitude'] - -# Get nearest site and print out its name -site = conn.get_nearest_forecast_site(latitude, longitude) -print(site.name) +postcode = postcodes_conn.get_postcode("SW1A 2AA") +latitude = postcode["result"]["latitude"] +longitude = postcode["result"]["longitude"] # Get a forecast for the nearest site -forecast = conn.get_forecast_for_site(site.location_id, "3hourly") +forecast = manager.get_forecast(longitude, latitude, "hourly") # Get the current timestep using now() and print out some info now = forecast.now() -print(now.weather.text) -print("%s%s%s" % (now.temperature.value, - '\xb0', #Unicode character for degree symbol - now.temperature.units)) +print(now["significantWeatherCode"]) +print(f"{now['screenTemperature']['value']} {now['screenTemperature']['unit_symbol']}") diff --git a/examples/simple_forecast/simple_forecast.py b/examples/simple_forecast/simple_forecast.py index 83daf2f..4982b0b 100755 --- a/examples/simple_forecast/simple_forecast.py +++ b/examples/simple_forecast/simple_forecast.py @@ -4,27 +4,27 @@ It will allow us to explore the day, timestep and element objects. """ +import datetime + import datapoint # Create datapoint connection -conn = datapoint.Manager(api_key="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee") +manager = datapoint.Manager(api_key="api key goes here") -# Get nearest site and print out its name -site = conn.get_nearest_forecast_site(51.500728, -0.124626) -print(site.name) +forecast = manager.get_forecast(51.500728, -0.124626, frequency="hourly") -# Get a forecast for the nearest site -forecast = conn.get_forecast_for_site(site.location_id, "3hourly") +# Loop through timesteps and print information +for timestep in forecast.timesteps: + print(timestep["time"]) + print(timestep["significantWeatherCode"]["value"]) + print( + "{temp} {temp_units}".format( + temp=timestep["screenTemperature"]["value"], + temp_units=timestep["screenTemperature"]["unit_symbol"], + ) + ) -# Loop through days and print date -for day in forecast.days: - print("\n%s" % day.date) +print(forecast.now()) - # Loop through time steps and print out info - for timestep in day.timesteps: - print(timestep.date) - print(timestep.weather.text) - print("%s%s%s" % (timestep.temperature.value, - '\xb0', #Unicode character for degree symbol - timestep.temperature.units)) +print(forecast.at_datetime(datetime.datetime(2024, 2, 11, 14, 0))) diff --git a/examples/text_forecast/text_forecast.py b/examples/text_forecast/text_forecast.py deleted file mode 100644 index c7becb3..0000000 --- a/examples/text_forecast/text_forecast.py +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env python -""" -This example will print out the 30 day text forecast for a region of the UK. -""" - -import datapoint - -# Create datapoint connection -conn = datapoint.Manager(api_key="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee") - -# Get all regions and print out their details -regions = conn.regions.get_all_regions() -for region in regions: - print((region.name, region.location_id, region.region)) - -# Get all forecasts for a specific region -my_region = regions[0] -forecast = conn.regions.get_raw_forecast(my_region.location_id)['RegionalFcst'] - -# Print the forecast details -print('Forecast for {} (issued at {}):'.format(my_region.name, forecast['issuedAt'])) - -sections = forecast['FcstPeriods']['Period'] -for section in forecast['FcstPeriods']['Period']: - paragraph = [] - content = section['Paragraph'] - - # Some paragraphs have multiple sections - if isinstance(content, dict): - paragraph.append(content) - else: - paragraph = content - - for line in paragraph: - print('{}\n{}\n'.format(line['title'], line['$'])) diff --git a/examples/tube_bike/tube_bike.py b/examples/tube_bike/tube_bike.py index 15d5d54..bdfde48 100644 --- a/examples/tube_bike/tube_bike.py +++ b/examples/tube_bike/tube_bike.py @@ -5,19 +5,16 @@ cycling or catching the tube. """ -import datapoint import tubestatus -# Create datapoint connection -conn = datapoint.Manager(api_key="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee") +import datapoint -# Get nearest site to my house and to work -my_house = conn.get_nearest_forecast_site(51.5016730, 0.0057500) -work = conn.get_nearest_forecast_site(51.5031650, -0.1123050) +# Create datapoint connection +manager = datapoint.Manager(api_key="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee") # Get a forecast for my house and work -my_house_forecast = conn.get_forecast_for_site(my_house.location_id, "3hourly") -work_forecast = conn.get_forecast_for_site(work.location_id, "3hourly") +my_house_forecast = manager.get_forecast(51.5016730, 0.0057500, "hourly") +work_forecast = manager.get_forecast(51.5031650, -0.1123050, "hourly") # Get the current timestep for both locations my_house_now = my_house_forecast.now() @@ -27,29 +24,34 @@ current_status = tubestatus.Status() # Get the status of the Waterloo and City line -waterloo_status = current_status.get_status('Waterloo and City') +waterloo_status = current_status.get_status("Waterloo and City") # Check whether there are any problems with rain or the tube -if (my_house_now.precipitation.value < 40 and \ - work_now.precipitation.value < 40 and \ - waterloo_status.description == "Good Service"): - +if ( + my_house_now["probOfPrecipitation"]["value"] < 40 + and work_now["probOfPrecipitation"]["value"] < 40 + and waterloo_status.description == "Good Service" +): print("Rain is unlikely and tube service is good, the decision is yours.") # If it is going to rain then suggest the tube -elif ((my_house_now.precipitation.value >= 40 or \ - work_now.precipitation.value >= 40) and \ - waterloo_status.description == "Good Service"): - +elif ( + my_house_now["probOfPrecipitation"]["value"] >= 40 + or work_now["probOfPrecipitation"]["value"] >= 40 +) and waterloo_status.description == "Good Service": print("Looks like rain, better get the tube") # If the tube isn't running then suggest cycling -elif (my_house_now.precipitation.value < 40 and \ - work_now.precipitation.value < 40 and \ - waterloo_status.description != "Good Service"): - +elif ( + my_house_now["probOfPrecipitation"]["value"] < 40 + and work_now["probOfPrecipitation"]["value"] < 40 + and waterloo_status.description != "Good Service" +): print("Bad service on the tube, cycling it is!") # Else if both are bad then suggest cycling in the rain else: - print("The tube has poor service so you'll have to cycle, but it's raining so take your waterproofs.") + print( + "The tube has poor service so you'll have to cycle," + " but it's raining so take your waterproofs." + ) diff --git a/examples/umbrella/umbrella.py b/examples/umbrella/umbrella.py index 19c0fbe..55cabbe 100755 --- a/examples/umbrella/umbrella.py +++ b/examples/umbrella/umbrella.py @@ -4,29 +4,30 @@ today and then decides if we need to take an umbrella. """ +import datetime + import datapoint # Create umbrella variable to use later umbrella = False # Create datapoint connection -conn = datapoint.Manager(api_key="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee") - -# Get nearest site and print out its name -site = conn.get_nearest_forecast_site(51.500728, -0.124626) -print(site.name) +manager = datapoint.Manager(api_key="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee") # Get a forecast for the nearest site -forecast = conn.get_forecast_for_site(site.location_id, "3hourly") +forecast = manager.get_forecast(51.500728, -0.124626, "hourly") # Loop through all the timesteps in day 0 (today) -for timestep in forecast.days[0].timesteps: +for timestep in forecast.timesteps: # Check to see if the chance of rain is more than 20% at any point - if timestep.precipitation.value > 20: + if ( + timestep["probOfPrecipitation"]["value"] > 20 + and timestep["time"].date == datetime.date.now() + ): umbrella = True # Print out the results -if umbrella == True: +if umbrella is True: print("Looks like rain! Better take an umbrella.") else: print("Don't worry you don't need an umbrella today.") diff --git a/examples/washing/washing.py b/examples/washing/washing.py index 88e1cef..c4df6ed 100644 --- a/examples/washing/washing.py +++ b/examples/washing/washing.py @@ -9,53 +9,63 @@ """ import datapoint -from datetime import datetime # Set thresholds -MAX_WIND = 31 # in mph. We don't want the washing to blow away -MAX_PRECIPITATION = 20 # Max chance of rain we will accept +MAX_WIND = 31 # in mph. We don't want the washing to blow away +MAX_PRECIPITATION = 20 # Max chance of rain we will accept # Variables for later -best_day = None -best_day_score = 0 # For simplicity the score will be temperature + wind speed +best_time = None +best_score = 0 # For simplicity the score will be temperature + wind speed # Create datapoint connection -conn = datapoint.Manager(api_key="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee") - -# Get nearest site and print out its name -site = conn.get_nearest_forecast_site(51.500728, -0.124626) -print(site.name) - +manager = datapoint.Manager(api_key="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee") # Get a forecast for the nearest site -forecast = conn.get_forecast_for_site(site.location_id, "daily") +forecast = manager.get_forecast(51.500728, -0.124626, "daily") # Loop through days for day in forecast.days: - # Get the 'Day' timestep - if day.timesteps[0].name == 'Day': + if day.timesteps[0].name == "Day": timestep = day.timesteps[0] # If precipitation, wind speed and gust are less than their threshold - if timestep.precipitation.value < MAX_PRECIPITATION and \ - timestep.wind_speed.value < MAX_WIND and \ - timestep.wind_gust.value < MAX_WIND: - + if ( + timestep.precipitation.value < MAX_PRECIPITATION + and timestep.wind_speed.value < MAX_WIND + and timestep.wind_gust.value < MAX_WIND + ): # Calculate the score for this timestep timestep_score = timestep.wind_speed.value + timestep.temperature.value # If this timestep scores better than the current best replace it - if timestep_score > best_day_score: - best_day_score = timestep_score + if timestep_score > best_score: + best_score = timestep_score best_day = day.date +for timestep in forecast.timesteps: + # If precipitation, wind speed and gust are less than their threshold + if ( + timestep.precipitation.value < MAX_PRECIPITATION + and timestep.wind_speed.value < MAX_WIND + and timestep.wind_gust.value < MAX_WIND + ): + # Calculate the score for this timestep + timestep_score = ( + timestep["windSpeed10m"]["value"] + timestep["screenTemperature"]["value"] + ) + + # If this timestep scores better than the current best replace it + if timestep_score > best_score: + best_score = timestep_score + best_time = timestep["time"] + + # If best_day is still None then there are no good days -if best_day is None: +if best_time is None: print("Better use the tumble dryer") # Otherwise print out the day else: - # Get the day of the week from the datetime object - best_day_formatted = datetime.strftime(best_day, "%A") - print("%s is the best day with a score of %s" % (best_day_formatted, best_day_score)) + print(f"{best_time} is the best day with a score of {best_score}") diff --git a/pyproject.toml b/pyproject.toml index e1692a4..1e1c812 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,4 +1,61 @@ [build-system] -requires = ["setuptools", -"versioneer"] -build-backend = "setuptools.build_meta" \ No newline at end of file +requires = ["hatchling", "versioningit"] +build-backend = "hatchling.build" + +[project] +name = "datapoint" +dynamic = ["version"] +authors = [ + {name="Emily Price", email="emily.j.price.nth@gmail.com"}, + { name="Jacob Tomlinson"}, +] +description = "Python interface to the Met Office's Datapoint API" +readme = "README.md" +requires-python = ">=3.8" +classifiers=[ + "Development Status :: 3 - Alpha", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", +] +dependencies = [ + "requests >= 2.20.0,<3", + "appdirs >=1,<2", + "geojson >= 3.0.0,<4", +] +license = {file = "LICENSE"} +keywords = ["weather", "weather forecast", "Met Office", "DataHub"] + +[project.urls] +Homepage = "https://github.com/ejep/datapoint-python" +Documentation = "http://datapoint-python.readthedocs.org/en/latest" + +[tool.hatch.build.targets.sdist] +exclude = [ + "tests/", + "examples/", +] + +[tool.hatch.version] +source = "versioningit" + +[tool.isort] +profile = "black" +src_paths = ["src", "tests"] + +[tool.versioningit.format] + +distance = "{base_version}+post{distance}{vcs}{rev}" +distance-dirty = "{base_version}+post{distance}{vcs}{rev}.d{build_date:%Y%m%d}" + +[tool.versioningit.vcs] +default-tag = "0.0.1" + +[tool.pytest.ini_options] +addopts = [ + "--import-mode=importlib", +] diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..3061c53 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,7 @@ +black==24.* +isort==5.* +flake8==7.* +flake8-bugbear==24.* +flake8-pytest-style==1.* +pytest==8.* +. diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index af8a0b0..0000000 --- a/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -requests==2.24.0 -appdirs==1.4.4 -pytz==2020.1 -requests-mock==1.8.0 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 3a35110..0000000 --- a/setup.cfg +++ /dev/null @@ -1,5 +0,0 @@ -[versioneer] -VCS = git -versionfile_source = datapoint/_version.py -versionfile_build = datapoint/_version.py -tag_prefix = '' \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index 29ce7ac..0000000 --- a/setup.py +++ /dev/null @@ -1,87 +0,0 @@ -#!/usr/bin/env python - -from setuptools import setup -import versioneer - -setup(name='datapoint', - version='0.9.9', - #cmdclass=versioneer.get_cmdclass(), - install_requires=[ - "requests >= 2.20.0,<3", - "appdirs >=1,<2", - "pytz", - ], - description='Python interface to the Met Office\'s Datapoint API', - long_description_content_type='text/x-rst', - long_description=''' -Datapoint for Python --------------------- - -*A Python module for accessing weather data via the Met -Office's open data API known as -`Datapoint`.* - -**Disclaimer: This module is in no way part of the datapoint -project/service. This module is intended to simplify the use of -Datapoint for small Python projects (e.g school projects). No support -for this module is provided by the Met Office and may break as the -Datapoint service grows/evolves. The author will make reasonable efforts -to keep it up to date and fully featured.** - -Changelog ---------- - -+ Explicitly state the use of semantic versioning in `README.md`. -+ Add `elements()` function to `Timestep`. -+ Remove night/day indication from weather codes which have them. -+ Change the logic used to calculate the closest timestep to a datetime. The closest timestep to the datetime is now used. Add a new function, `Forecast.at_datetime(target)` to do this. `Forecast.now()` has been changed to use this new function. The old behaviour is deprecated and available using `Forecast.now_old()`. `Forecast.future()` has been changed to use this new function. The old behaviour is deprecated and available using `Forecast.future_old()`. -+ Check if keys are returned from datapoint api in `Manager.py`. Do not attempt to read the values from the dict if they are not there. - - -Installation ------------- - -.. code:: bash - - $ pip install datapoint - -You will also require a `Datapoint API` -key from http://www.metoffice.gov.uk/datapoint/API. - -Features --------- - -- List forecast sites -- Get nearest forecast site from latitiude and longitude -- Get the following 5 day forecast types for any site -- Daily (Two timesteps, midday and midnight UTC) -- 3 hourly (Eight timesteps, every 3 hours starting at midnight UTC) -- Get observation sites -- Get observations for any site - -Contributing changes --------------------- - -Please feel free to submit issues and pull requests. - -License -------- - -GPLv3. -''', - author='Jacob Tomlinson, Emlyn Price', - author_email='emlyn.je.price@gmail.com', - maintainer='Emlyn Price', - maintainer_email='emlyn.je.price@gmail.com', - url='https://github.com/ejep/datapoint-python', - license='GPLv3', - packages=['datapoint', 'datapoint.regions'], - classifiers=[ - 'Development Status :: 3 - Alpha', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', - ] - ) diff --git a/src/datapoint/Forecast.py b/src/datapoint/Forecast.py new file mode 100644 index 0000000..e38594b --- /dev/null +++ b/src/datapoint/Forecast.py @@ -0,0 +1,435 @@ +import datetime + +from datapoint.exceptions import APIException +from datapoint.weather_codes import WEATHER_CODES + + +class Forecast: + """Forecast data returned from DataHub + + Provides access to forecasts as far ahead as provided by DataHub. See the + DataHub documentation for the latest limits on the forecast range. The + values of data from DataHub are combined with the unit information and + description and returned as a dict. + + Basic Usage:: + + >>> import datapoint + >>> m = datapoint.Manager.Manager(api_key = "blah") + >>> f = m.get_forecast(latitude=50, longitude=0, frequency="hourly") + >>> f.now() + { + 'time': datetime.datetime(2024, 2, 19, 13, 0, tzinfo=datetime.timezone.utc), + 'screenTemperature': { + 'value': 10.09, + 'description': 'Screen Air Temperature', + 'unit_name': 'degrees Celsius', + 'unit_symbol': 'Cel' + }, + 'screenDewPointTemperature': { + 'value': 8.08, + 'description': 'Screen Dew Point Temperature', + 'unit_name': 'degrees Celsius', + 'unit_symbol': 'Cel' + }, + 'feelsLikeTemperature': { + 'value': 6.85, + 'description': 'Feels Like Temperature', + 'unit_name': 'degrees Celsius', + 'unit_symbol': 'Cel' + }, + 'windSpeed10m': { + 'value': 7.57, + 'description': '10m Wind Speed', + 'unit_name': 'metres per second', + 'unit_symbol': 'm/s' + }, + 'windDirectionFrom10m': { + 'value': 263, + 'description': '10m Wind From Direction', + 'unit_name': 'degrees', + 'unit_symbol': 'deg' + }, + 'windGustSpeed10m': { + 'value': 12.31, + 'description': '10m Wind Gust Speed', + 'unit_name': 'metres per second', + 'unit_symbol': 'm/s' + }, + 'visibility': { + 'value': 21201, + 'description': 'Visibility', + 'unit_name': 'metres', + 'unit_symbol': 'm' + }, + 'screenRelativeHumidity': { + 'value': 87.81, + 'description': 'Screen Relative Humidity', + 'unit_name': 'percentage', + 'unit_symbol': '%' + }, + 'mslp': { + 'value': 103080, + 'description': 'Mean Sea Level Pressure', + 'unit_name': 'pascals', + 'unit_symbol': 'Pa' + }, + 'uvIndex': { + 'value': 1, + 'description': 'UV Index', + 'unit_name': 'dimensionless', + 'unit_symbol': '1' + }, + 'significantWeatherCode': { + 'value': 'Cloudy', + 'description': 'Significant Weather Code', + 'unit_name': 'dimensionless', + 'unit_symbol': '1' + }, + 'precipitationRate': { + 'value': 0.0, + 'description': 'Precipitation Rate', + 'unit_name': 'millimetres per hour', + 'unit_symbol': 'mm/h' + }, + 'probOfPrecipitation': { + 'value': 21, + 'description': 'Probability of Precipitation', + 'unit_name': 'percentage', + 'unit_symbol': '%' + } + } + """ + + def __init__(self, frequency, api_data): + """ + :param frequency: Frequency of forecast: 'hourly', 'three-hourly' or 'daily' + :param api_data: Data returned from API call + :type frequency: string + :type api_data: dict + """ + self.frequency = frequency + # Need to parse format like 2024-02-17T15:00Z. This can only be + # done with datetime.datetime.fromisoformat from python 3.11 + # onwards. + self.data_date = datetime.datetime.strptime( + api_data["features"][0]["properties"]["modelRunDate"], + "%Y-%m-%dT%H:%M%z", + ) #: The date the provided forecast was generated. + + self.forecast_longitude = api_data["features"][0]["geometry"]["coordinates"][ + 0 + ] #: The longitude of the provided forecast. + self.forecast_latitude = api_data["features"][0]["geometry"]["coordinates"][ + 1 + ] #: The latitude of the provided forecast. + self.distance_from_requested_location = api_data["features"][0]["properties"][ + "requestPointDistance" + ] #: The distance of the location of the provided forecast from the requested location + self.name = api_data["features"][0]["properties"]["location"][ + "name" + ] #: The name of the location of the provided forecast + + # N.B. Elevation is in metres above or below the WGS 84 reference + # ellipsoid as per GeoJSON spec. + self.elevation = api_data["features"][0]["geometry"]["coordinates"][ + 2 + ] #: The elevation of the location of the provided forecast + + forecasts = api_data["features"][0]["properties"]["timeSeries"] + parameters = api_data["parameters"][0] + if frequency == "daily": + self.timesteps = self._build_timesteps_from_daily(forecasts, parameters) + else: + self.timesteps = [] + for forecast in forecasts: + self.timesteps.append(self._build_timestep(forecast, parameters)) + + def _build_timesteps_from_daily(self, forecasts, parameters): + """Build individual timesteps from forecasts and metadata + + Take the forecast data from DataHub and combine with unit information + in each timestep. Break each day into day and night steps. ASSUME that + each step has data for the night referred to in the timestamp and the + following dawn-dusk period. + + :parameter forecasts: Forecast data from DataHub + :parameter parameters: Unit information from DataHub + :type forecasts: list + :type parameters: dict + + :return: List of timesteps + :rtype: list + """ + + timesteps = [] + for forecast in forecasts: + # Need to parse format like 2024-02-17T15:00Z. This can only be + # done with datetime.datetime.fromisoformat from python 3.11 + # onwards. + night_step = { + "time": datetime.datetime.strptime(forecast["time"], "%Y-%m-%dT%H:%M%z") + } + day_step = { + "time": datetime.datetime.strptime(forecast["time"], "%Y-%m-%dT%H:%M%z") + + datetime.timedelta(hours=12) + } + + for element, value in forecast.items(): + if element.startswith("midday"): + day_step[element.replace("midday", "")] = { + "value": value, + "description": parameters[element]["description"], + "unit_name": parameters[element]["unit"]["label"], + "unit_symbol": parameters[element]["unit"]["symbol"]["type"], + } + elif element.startswith("midnight"): + night_step[element.replace("midnight", "")] = { + "value": value, + "description": parameters[element]["description"], + "unit_name": parameters[element]["unit"]["label"], + "unit_symbol": parameters[element]["unit"]["symbol"]["type"], + } + elif element.startswith("day"): + day_step[element.replace("day", "")] = { + "value": value, + "description": parameters[element]["description"], + "unit_name": parameters[element]["unit"]["label"], + "unit_symbol": parameters[element]["unit"]["symbol"]["type"], + } + elif element.startswith("night"): + night_step[element.replace("night", "")] = { + "value": value, + "description": parameters[element]["description"], + "unit_name": parameters[element]["unit"]["label"], + "unit_symbol": parameters[element]["unit"]["symbol"]["type"], + } + elif element == "maxUvIndex": + day_step[element] = { + "value": value, + "description": parameters[element]["description"], + "unit_name": parameters[element]["unit"]["label"], + "unit_symbol": parameters[element]["unit"]["symbol"]["type"], + } + + timesteps.append(night_step) + timesteps.append(day_step) + + timesteps = sorted(timesteps, key=lambda t: t["time"]) + return timesteps + + def _build_timestep(self, forecast, parameters): + """Build individual timestep from forecast and metadata + + Take the forecast data from DataHub for a single time and combine with + unit information in each timestep. + + :parameter forecast: Forecast data from DataHub + :parameter parameters: Unit information from DataHub + :type forecast: dict + :type parameters:dict + + :return: Individual forecast timestep + :rtype: dict + + """ + + timestep = {} + for element, value in forecast.items(): + if element == "time": + # Need to parse format like 2024-02-17T15:00Z. This can only be + # done with datetime.datetime.fromisoformat from python 3.11 + # onwards. + timestep["time"] = datetime.datetime.strptime( + forecast["time"], "%Y-%m-%dT%H:%M%z" + ) + + elif element == "significantWeatherCode": + timestep[element] = { + "value": WEATHER_CODES[str(value)], + "description": parameters[element]["description"], + "unit_name": parameters[element]["unit"]["label"], + "unit_symbol": parameters[element]["unit"]["symbol"]["type"], + } + else: + timestep[element] = { + "value": value, + "description": parameters[element]["description"], + "unit_name": parameters[element]["unit"]["label"], + "unit_symbol": parameters[element]["unit"]["symbol"]["type"], + } + + return timestep + + def _check_requested_time(self, target): + """Check that a forecast for the requested time can be provided + + :parameter target: The requested time for the forecast + :type target: datetime + """ + # Check that there is a forecast for the requested time. + # If we have an hourly forecast, check that the requested time is at + # most 30 minutes before the first datetime we have a forecast for. + if self.frequency == "hourly" and target < self.timesteps[0][ + "time" + ] - datetime.timedelta(hours=0, minutes=30): + err_str = ( + "There is no forecast available for the requested time. " + "The requested time is more than 30 minutes before the " + "first available forecast." + ) + raise APIException(err_str) + + # If we have a three-hourly forecast, check that the requested time is at + # most 1.5 hours before the first datetime we have a forecast for. + if self.frequency == "three-hourly" and target < self.timesteps[0][ + "time" + ] - datetime.timedelta(hours=1, minutes=30): + err_str = ( + "There is no forecast available for the requested time. " + "The requested time is more than 1 hour and 30 minutes " + "before the first available forecast." + ) + raise APIException(err_str) + + # If we have a daily forecast, check that the requested time is at + # most 6 hours before the first datetime we have a forecast for. + if self.frequency == "daily" and target < self.timesteps[0][ + "time" + ] - datetime.timedelta(hours=6): + err_str = ( + "There is no forecast available for the requested time. " + "The requested time is more than 6 hours before the first " + "available forecast." + ) + + raise APIException(err_str) + + # If we have an hourly forecast, check that the requested time is at + # most 30 minutes after the final datetime we have a forecast for + if self.frequency == "hourly" and target > ( + self.timesteps[-1]["time"] + datetime.timedelta(hours=0, minutes=30) + ): + err_str = ( + "There is no forecast available for the requested time. The " + "requested time is more than 30 minutes after the first " + "available forecast" + ) + + raise APIException(err_str) + + # If we have a three-hourly forecast, then the target must be within 1.5 + # hours of the last timestep + if self.frequency == "three-hourly" and target > ( + self.timesteps[-1]["time"] + datetime.timedelta(hours=1, minutes=30) + ): + err_str = ( + "There is no forecast available for the requested time. The " + "requested time is more than 1.5 hours after the first " + "available forecast." + ) + + raise APIException(err_str) + + # If we have a daily forecast, then the target must be within 6 hours + # of the last timestep + if self.frequency == "daily" and target > ( + self.timesteps[-1]["time"] + datetime.timedelta(hours=6) + ): + err_str = ( + "There is no forecast available for the requested time. The " + "requested time is more than 6 hours after the first available " + "forecast." + ) + + raise APIException(err_str) + + def at_datetime(self, target): + """Return the timestep closest to the target datetime + + :parameter target: Time to get the forecast for + :type target: datetime + + :return: Individual forecast timestep + :rtype: dict + + """ + + # Convert target to offset aware datetime + if target.tzinfo is None: + target = datetime.datetime.combine( + target.date(), target.time(), self.timesteps[0]["time"].tzinfo + ) + + self._check_requested_time(target) + + # Loop over all timesteps + # Calculate the first time difference + prev_td = target - self.timesteps[0]["time"] + prev_ts = self.timesteps[0] + to_return = None + + for i, timestep in enumerate(self.timesteps, start=1): + # Calculate the difference between the target time and the + # timestep. + td = target - timestep["time"] + + # Find the timestep which is further from the target than the + # previous one. Return the previous timestep + if abs(td.total_seconds()) > abs(prev_td.total_seconds()): + # We are further from the target + to_return = prev_ts + break + if i == len(self.timesteps): + to_return = timestep + + prev_ts = timestep + prev_td = td + return to_return + + def now(self): + """Return the closest timestep to the current time + + :return: Individual forecast timestep + :rtype: dict + """ + + d = datetime.datetime.now(tz=self.timesteps[0]["time"].tzinfo) + return self.at_datetime(d) + + def future(self, days=0, hours=0, minutes=0): + """Return the closest timestep to a date in a given amount of time + + Either provide components of the time to the forecast or the total + hours or minutes + + Providing components:: + + >>> import datapoint + >>> m = datapoint.Manager(api_key = "blah") + >>> f = m.get_forecast(latitude=50, longitude=0, frequency="hourly") + >>> f.future(days=1, hours=2) + + Providing total hours:: + + >>> import datapoint + >>> m = datapoint.Manager(api_key = "blah") + >>> f = m.get_forecast(latitude=50, longitude=0, frequency="hourly") + >>> f.future(hours=26) + + + :parameter days: How many days ahead + :parameter hours: How many hours ahead + :parameter minutes: How many minutes ahead + :type days: int + :type hours: int + :type minutes: int + + :return: Individual forecast timestep + :rtype: dict + """ + + d = datetime.datetime.now(tz=self.timesteps[0]["time"].tzinfo) + target = d + datetime.timedelta(days=days, hours=hours, minutes=minutes) + + return self.at_datetime(target) diff --git a/src/datapoint/Manager.py b/src/datapoint/Manager.py new file mode 100644 index 0000000..b2f01de --- /dev/null +++ b/src/datapoint/Manager.py @@ -0,0 +1,225 @@ +import geojson +import requests +from requests.adapters import HTTPAdapter +from requests.packages.urllib3.util.retry import Retry + +from datapoint.exceptions import APIException +from datapoint.Forecast import Forecast + +API_URL = "https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/" + + +class Manager: + """Manager for DataHub connection. + + Wraps calls to DataHub API, and provides Forecast objects. Basic Usage: + + :: + + >>> import datapoint + >>> m = datapoint.Manager.Manager(api_key = "blah") + >>> f = m.get_forecast(latitude=50, longitude=0, frequency="hourly") + >>> f.now() + { + 'time': datetime.datetime(2024, 2, 19, 13, 0, tzinfo=datetime.timezone.utc), + 'screenTemperature': { + 'value': 10.09, + 'description': 'Screen Air Temperature', + 'unit_name': 'degrees Celsius', + 'unit_symbol': 'Cel' + }, + 'screenDewPointTemperature': { + 'value': 8.08, + 'description': 'Screen Dew Point Temperature', + 'unit_name': 'degrees Celsius', + 'unit_symbol': 'Cel' + }, + 'feelsLikeTemperature': { + 'value': 6.85, + 'description': 'Feels Like Temperature', + 'unit_name': 'degrees Celsius', + 'unit_symbol': 'Cel' + }, + 'windSpeed10m': { + 'value': 7.57, + 'description': '10m Wind Speed', + 'unit_name': 'metres per second', + 'unit_symbol': 'm/s' + }, + 'windDirectionFrom10m': { + 'value': 263, + 'description': '10m Wind From Direction', + 'unit_name': 'degrees', + 'unit_symbol': 'deg' + }, + 'windGustSpeed10m': { + 'value': 12.31, + 'description': '10m Wind Gust Speed', + 'unit_name': 'metres per second', + 'unit_symbol': 'm/s' + }, + 'visibility': { + 'value': 21201, + 'description': 'Visibility', + 'unit_name': 'metres', + 'unit_symbol': 'm' + }, + 'screenRelativeHumidity': { + 'value': 87.81, + 'description': 'Screen Relative Humidity', + 'unit_name': 'percentage', + 'unit_symbol': '%' + }, + 'mslp': { + 'value': 103080, + 'description': 'Mean Sea Level Pressure', + 'unit_name': 'pascals', + 'unit_symbol': 'Pa' + }, + 'uvIndex': { + 'value': 1, + 'description': 'UV Index', + 'unit_name': 'dimensionless', + 'unit_symbol': '1' + }, + 'significantWeatherCode': { + 'value': 'Cloudy', + 'description': 'Significant Weather Code', + 'unit_name': 'dimensionless', + 'unit_symbol': '1' + }, + 'precipitationRate': { + 'value': 0.0, + 'description': 'Precipitation Rate', + 'unit_name': 'millimetres per hour', + 'unit_symbol': 'mm/h' + }, + 'probOfPrecipitation': { + 'value': 21, + 'description': 'Probability of Precipitation', + 'unit_name': 'percentage', + 'unit_symbol': '%' + } + } + + """ + + def __init__(self, api_key=""): + self.api_key = api_key + + def __get_retry_session( + self, + retries=10, + backoff_factor=0.3, + status_forcelist=(500, 502, 504), + session=None, + ): + """ + Retry the connection using requests if it fails. Use this as a wrapper + to request from datapoint. See + https://requests.readthedocs.io/en/latest/user/advanced/?highlight=retry#example-automatic-retries + for more details. + + :parameter retries: How many times to retry + :parameter backoff_factor: Backoff between attempts after second try + :parameter status_forcelist: Codes to force a retry on + :parameter session: Existing session to use + + :return: Session object + :rtype: + """ + + # requests.Session allows finer control, which is needed to use the + # retrying code + the_session = session or requests.Session() + + # The Retry object manages the actual retrying + retry = Retry( + total=retries, + read=retries, + connect=retries, + backoff_factor=backoff_factor, + status_forcelist=status_forcelist, + ) + + adapter = HTTPAdapter(max_retries=retry) + + the_session.mount("http://", adapter) + the_session.mount("https://", adapter) + + return the_session + + def __call_api(self, latitude, longitude, frequency): + """ + Call the datapoint api using the requests module + + :parameter latitude: Latitude of forecast location + :parameter longitude: Longitude of forecast location + :parameter frequency: Forecast frequency. One of 'hourly', 'three-hourly, 'daily' + :type latitude: float + :type longitude: float + :type frequency: string + + :return: Data from DataPoint + :rtype: dict + """ + params = { + "latitude": latitude, + "longitude": longitude, + "includeLocationName": True, + "excludeParameterMetadata": False, + } + headers = { + "accept": "application/json", + "apikey": self.api_key, + } + request_url = API_URL + frequency + + # Add a timeout to the request. + # The value of 1 second is based on attempting 100 connections to + # datapoint and taking ten times the mean connection time (rounded up). + # Could expose to users in the functions which need to call the api. + # req = requests.get(url, params=payload, timeout=1) + # The wrapper function __retry_session returns a requests.Session + # object. This has a .get() function like requests.get(), so the use + # doesn't change here. + + sess = self.__get_retry_session() + req = sess.get( + request_url, + params=params, + headers=headers, + timeout=1, + ) + + req.raise_for_status() + + try: + data = geojson.loads(req.text) + except ValueError as exc: + raise APIException("DataPoint has not returned valid JSON") from exc + + return data + + def get_forecast(self, latitude, longitude, frequency="daily"): + """ + Get a forecast for the provided site + + :parameter latitude: Latitude of forecast location + :parameter longitude: Longitude of forecast location + :parameter frequency: Forecast frequency. One of 'hourly', 'three-hourly, 'daily' + :type latitude: float + :type longitude: float + :type frequency: string + + :return: :class: `Forecast ` object + :rtype: datapoint.Forecast + """ + if frequency not in ["hourly", "three-hourly", "daily"]: + raise ValueError( + "frequency must be set to one of 'hourly', 'three-hourly', 'daily'" + ) + data = self.__call_api(latitude, longitude, frequency) + forecast = Forecast(frequency=frequency, api_data=data) + + return forecast diff --git a/datapoint/regions/__init__.py b/src/datapoint/__init__.py similarity index 100% rename from datapoint/regions/__init__.py rename to src/datapoint/__init__.py diff --git a/datapoint/exceptions.py b/src/datapoint/exceptions.py similarity index 100% rename from datapoint/exceptions.py rename to src/datapoint/exceptions.py diff --git a/src/datapoint/weather_codes.py b/src/datapoint/weather_codes.py new file mode 100644 index 0000000..d54cce4 --- /dev/null +++ b/src/datapoint/weather_codes.py @@ -0,0 +1,35 @@ +# See https://www.metoffice.gov.uk/services/data/datapoint/code-definitions for definitions +WEATHER_CODES = { + "-1": "Trace rain", + "0": "Clear night", + "1": "Sunny day", + "2": "Partly cloudy", + "3": "Partly cloudy", + "4": "Not used", + "5": "Mist", + "6": "Fog", + "7": "Cloudy", + "8": "Overcast", + "9": "Light rain shower", + "10": "Light rain shower", + "11": "Drizzle", + "12": "Light rain", + "13": "Heavy rain shower", + "14": "Heavy rain shower", + "15": "Heavy rain", + "16": "Sleet shower", + "17": "Sleet shower", + "18": "Sleet", + "19": "Hail shower", + "20": "Hail shower", + "21": "Hail", + "22": "Light snow shower", + "23": "Light snow shower", + "24": "Light snow", + "25": "Heavy snow shower", + "26": "Heavy snow shower", + "27": "Heavy snow", + "28": "Thunder shower", + "29": "Thunder shower", + "30": "Thunder", +} diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/datapoint.json b/tests/integration/datapoint.json deleted file mode 100644 index c2b8707..0000000 --- a/tests/integration/datapoint.json +++ /dev/null @@ -1,1499 +0,0 @@ -{ - "all_sites": { - "Locations": { - "Location": [ - { - "elevation": "47.0", - "id": "354107", - "latitude": "53.3986", - "longitude": "-2.9256", - "name": "Wavertree", - "region": "nw", - "unitaryAuthArea": "Merseyside" - }, - { - "elevation": "5.0", - "id": "322380", - "latitude": "52.7561", - "longitude": "0.4019", - "name": "King's Lynn", - "region": "ee", - "unitaryAuthArea": "Norfolk" - } - ] - } - }, - "wavertree_hourly": { - "SiteRep": { - "Wx": { - "Param": [ - { - "name": "F", - "units": "C", - "$": "Feels Like Temperature" - }, - { - "name": "G", - "units": "mph", - "$": "Wind Gust" - }, - { - "name": "H", - "units": "%", - "$": "Screen Relative Humidity" - }, - { - "name": "T", - "units": "C", - "$": "Temperature" - }, - { - "name": "V", - "units": "", - "$": "Visibility" - }, - { - "name": "D", - "units": "compass", - "$": "Wind Direction" - }, - { - "name": "S", - "units": "mph", - "$": "Wind Speed" - }, - { - "name": "U", - "units": "", - "$": "Max UV Index" - }, - { - "name": "W", - "units": "", - "$": "Weather Type" - }, - { - "name": "Pp", - "units": "%", - "$": "Precipitation Probability" - } - ] - }, - "DV": { - "dataDate": "2020-04-25T08:00:00Z", - "type": "Forecast", - "Location": { - "i": "354107", - "lat": "53.3986", - "lon": "-2.9256", - "name": "WAVERTREE", - "country": "ENGLAND", - "continent": "EUROPE", - "elevation": "47.0", - "Period": [ - { - "type": "Day", - "value": "2020-04-25Z", - "Rep": [ - { - "D": "SE", - "F": "7", - "G": "25", - "H": "63", - "Pp": "0", - "S": "9", - "T": "9", - "V": "VG", - "W": "0", - "U": "0", - "$": "180" - }, - { - "D": "ESE", - "F": "4", - "G": "22", - "H": "76", - "Pp": "0", - "S": "11", - "T": "7", - "V": "GO", - "W": "1", - "U": "1", - "$": "360" - }, - { - "D": "SSE", - "F": "8", - "G": "18", - "H": "70", - "Pp": "0", - "S": "9", - "T": "10", - "V": "MO", - "W": "1", - "U": "3", - "$": "540" - }, - { - "D": "SSE", - "F": "14", - "G": "16", - "H": "50", - "Pp": "0", - "S": "9", - "T": "17", - "V": "GO", - "W": "1", - "U": "5", - "$": "720" - }, - { - "D": "S", - "F": "17", - "G": "9", - "H": "43", - "Pp": "1", - "S": "4", - "T": "19", - "V": "GO", - "W": "1", - "U": "2", - "$": "900" - }, - { - "D": "WNW", - "F": "15", - "G": "13", - "H": "55", - "Pp": "2", - "S": "7", - "T": "17", - "V": "GO", - "W": "3", - "U": "1", - "$": "1080" - }, - { - "D": "NW", - "F": "14", - "G": "7", - "H": "64", - "Pp": "1", - "S": "2", - "T": "14", - "V": "GO", - "W": "2", - "U": "0", - "$": "1260" - } - ] - }, - { - "type": "Day", - "value": "2020-04-26Z", - "Rep": [ - { - "D": "WSW", - "F": "13", - "G": "4", - "H": "73", - "Pp": "1", - "S": "2", - "T": "13", - "V": "GO", - "W": "2", - "U": "0", - "$": "0" - }, - { - "D": "WNW", - "F": "12", - "G": "9", - "H": "77", - "Pp": "2", - "S": "4", - "T": "12", - "V": "GO", - "W": "2", - "U": "0", - "$": "180" - }, - { - "D": "NW", - "F": "10", - "G": "9", - "H": "82", - "Pp": "5", - "S": "4", - "T": "11", - "V": "MO", - "W": "7", - "U": "1", - "$": "360" - }, - { - "D": "WNW", - "F": "11", - "G": "7", - "H": "79", - "Pp": "5", - "S": "4", - "T": "12", - "V": "MO", - "W": "7", - "U": "3", - "$": "540" - }, - { - "D": "WNW", - "F": "10", - "G": "18", - "H": "78", - "Pp": "6", - "S": "9", - "T": "12", - "V": "MO", - "W": "7", - "U": "4", - "$": "720" - }, - { - "D": "NW", - "F": "10", - "G": "18", - "H": "71", - "Pp": "5", - "S": "9", - "T": "12", - "V": "GO", - "W": "7", - "U": "2", - "$": "900" - }, - { - "D": "NW", - "F": "9", - "G": "16", - "H": "68", - "Pp": "9", - "S": "9", - "T": "11", - "V": "VG", - "W": "7", - "U": "1", - "$": "1080" - }, - { - "D": "NW", - "F": "8", - "G": "11", - "H": "68", - "Pp": "9", - "S": "7", - "T": "10", - "V": "VG", - "W": "8", - "U": "0", - "$": "1260" - } - ] - }, - { - "type": "Day", - "value": "2020-04-27Z", - "Rep": [ - { - "D": "WNW", - "F": "8", - "G": "9", - "H": "72", - "Pp": "11", - "S": "4", - "T": "9", - "V": "VG", - "W": "8", - "U": "0", - "$": "0" - }, - { - "D": "WNW", - "F": "7", - "G": "11", - "H": "77", - "Pp": "12", - "S": "7", - "T": "8", - "V": "VG", - "W": "7", - "U": "0", - "$": "180" - }, - { - "D": "NW", - "F": "7", - "G": "9", - "H": "80", - "Pp": "14", - "S": "4", - "T": "8", - "V": "GO", - "W": "7", - "U": "1", - "$": "360" - }, - { - "D": "NW", - "F": "7", - "G": "18", - "H": "73", - "Pp": "6", - "S": "9", - "T": "9", - "V": "VG", - "W": "3", - "U": "2", - "$": "540" - }, - { - "D": "NW", - "F": "8", - "G": "20", - "H": "59", - "Pp": "4", - "S": "9", - "T": "10", - "V": "VG", - "W": "3", - "U": "3", - "$": "720" - }, - { - "D": "NW", - "F": "8", - "G": "20", - "H": "58", - "Pp": "1", - "S": "9", - "T": "10", - "V": "VG", - "W": "1", - "U": "2", - "$": "900" - }, - { - "D": "NW", - "F": "8", - "G": "16", - "H": "57", - "Pp": "1", - "S": "7", - "T": "10", - "V": "VG", - "W": "1", - "U": "1", - "$": "1080" - }, - { - "D": "NW", - "F": "8", - "G": "11", - "H": "67", - "Pp": "1", - "S": "4", - "T": "9", - "V": "VG", - "W": "0", - "U": "0", - "$": "1260" - } - ] - }, - { - "type": "Day", - "value": "2020-04-28Z", - "Rep": [ - { - "D": "NNW", - "F": "7", - "G": "7", - "H": "80", - "Pp": "2", - "S": "4", - "T": "8", - "V": "VG", - "W": "0", - "U": "0", - "$": "0" - }, - { - "D": "W", - "F": "6", - "G": "7", - "H": "86", - "Pp": "3", - "S": "4", - "T": "7", - "V": "GO", - "W": "0", - "U": "0", - "$": "180" - }, - { - "D": "S", - "F": "5", - "G": "9", - "H": "86", - "Pp": "5", - "S": "4", - "T": "6", - "V": "GO", - "W": "1", - "U": "1", - "$": "360" - }, - { - "D": "ENE", - "F": "7", - "G": "13", - "H": "72", - "Pp": "6", - "S": "7", - "T": "9", - "V": "GO", - "W": "3", - "U": "3", - "$": "540" - }, - { - "D": "ENE", - "F": "10", - "G": "16", - "H": "57", - "Pp": "10", - "S": "7", - "T": "11", - "V": "GO", - "W": "7", - "U": "4", - "$": "720" - }, - { - "D": "N", - "F": "11", - "G": "16", - "H": "58", - "Pp": "10", - "S": "7", - "T": "12", - "V": "GO", - "W": "7", - "U": "2", - "$": "900" - }, - { - "D": "N", - "F": "10", - "G": "16", - "H": "63", - "Pp": "10", - "S": "7", - "T": "11", - "V": "VG", - "W": "7", - "U": "1", - "$": "1080" - }, - { - "D": "NNE", - "F": "9", - "G": "11", - "H": "72", - "Pp": "9", - "S": "4", - "T": "10", - "V": "VG", - "W": "7", - "U": "0", - "$": "1260" - } - ] - }, - { - "type": "Day", - "value": "2020-04-29Z", - "Rep": [ - { - "D": "E", - "F": "8", - "G": "9", - "H": "79", - "Pp": "6", - "S": "4", - "T": "9", - "V": "VG", - "W": "7", - "U": "0", - "$": "0" - }, - { - "D": "SSE", - "F": "7", - "G": "11", - "H": "81", - "Pp": "3", - "S": "7", - "T": "8", - "V": "GO", - "W": "2", - "U": "0", - "$": "180" - }, - { - "D": "SE", - "F": "5", - "G": "16", - "H": "86", - "Pp": "9", - "S": "9", - "T": "8", - "V": "GO", - "W": "7", - "U": "1", - "$": "360" - }, - { - "D": "SE", - "F": "8", - "G": "22", - "H": "74", - "Pp": "12", - "S": "11", - "T": "10", - "V": "GO", - "W": "7", - "U": "3", - "$": "540" - }, - { - "D": "SE", - "F": "10", - "G": "27", - "H": "72", - "Pp": "47", - "S": "13", - "T": "12", - "V": "GO", - "W": "12", - "U": "3", - "$": "720" - }, - { - "D": "SSE", - "F": "10", - "G": "29", - "H": "73", - "Pp": "59", - "S": "13", - "T": "13", - "V": "GO", - "W": "14", - "U": "2", - "$": "900" - }, - { - "D": "SSE", - "F": "10", - "G": "20", - "H": "69", - "Pp": "39", - "S": "11", - "T": "12", - "V": "VG", - "W": "10", - "U": "1", - "$": "1080" - }, - { - "D": "SSE", - "F": "9", - "G": "22", - "H": "79", - "Pp": "19", - "S": "13", - "T": "11", - "V": "GO", - "W": "7", - "U": "0", - "$": "1260" - } - ] - } - ] - } - } - } - }, - "wavertree_daily": { - "SiteRep": { - "Wx": { - "Param": [ - { - "name": "FDm", - "units": "C", - "$": "Feels Like Day Maximum Temperature" - }, - { - "name": "FNm", - "units": "C", - "$": "Feels Like Night Minimum Temperature" - }, - { - "name": "Dm", - "units": "C", - "$": "Day Maximum Temperature" - }, - { - "name": "Nm", - "units": "C", - "$": "Night Minimum Temperature" - }, - { - "name": "Gn", - "units": "mph", - "$": "Wind Gust Noon" - }, - { - "name": "Gm", - "units": "mph", - "$": "Wind Gust Midnight" - }, - { - "name": "Hn", - "units": "%", - "$": "Screen Relative Humidity Noon" - }, - { - "name": "Hm", - "units": "%", - "$": "Screen Relative Humidity Midnight" - }, - { - "name": "V", - "units": "", - "$": "Visibility" - }, - { - "name": "D", - "units": "compass", - "$": "Wind Direction" - }, - { - "name": "S", - "units": "mph", - "$": "Wind Speed" - }, - { - "name": "U", - "units": "", - "$": "Max UV Index" - }, - { - "name": "W", - "units": "", - "$": "Weather Type" - }, - { - "name": "PPd", - "units": "%", - "$": "Precipitation Probability Day" - }, - { - "name": "PPn", - "units": "%", - "$": "Precipitation Probability Night" - } - ] - }, - "DV": { - "dataDate": "2020-04-25T08:00:00Z", - "type": "Forecast", - "Location": { - "i": "354107", - "lat": "53.3986", - "lon": "-2.9256", - "name": "WAVERTREE", - "country": "ENGLAND", - "continent": "EUROPE", - "elevation": "47.0", - "Period": [ - { - "type": "Day", - "value": "2020-04-25Z", - "Rep": [ - { - "D": "SSE", - "Gn": "16", - "Hn": "50", - "PPd": "2", - "S": "9", - "V": "GO", - "Dm": "19", - "FDm": "18", - "W": "1", - "U": "5", - "$": "Day" - }, - { - "D": "WSW", - "Gm": "4", - "Hm": "73", - "PPn": "2", - "S": "2", - "V": "GO", - "Nm": "11", - "FNm": "11", - "W": "2", - "$": "Night" - } - ] - }, - { - "type": "Day", - "value": "2020-04-26Z", - "Rep": [ - { - "D": "WNW", - "Gn": "18", - "Hn": "78", - "PPd": "9", - "S": "9", - "V": "MO", - "Dm": "13", - "FDm": "11", - "W": "7", - "U": "4", - "$": "Day" - }, - { - "D": "WNW", - "Gm": "9", - "Hm": "72", - "PPn": "12", - "S": "4", - "V": "VG", - "Nm": "8", - "FNm": "7", - "W": "8", - "$": "Night" - } - ] - }, - { - "type": "Day", - "value": "2020-04-27Z", - "Rep": [ - { - "D": "NW", - "Gn": "20", - "Hn": "59", - "PPd": "14", - "S": "9", - "V": "VG", - "Dm": "11", - "FDm": "8", - "W": "3", - "U": "3", - "$": "Day" - }, - { - "D": "NNW", - "Gm": "7", - "Hm": "80", - "PPn": "3", - "S": "4", - "V": "VG", - "Nm": "6", - "FNm": "5", - "W": "0", - "$": "Night" - } - ] - }, - { - "type": "Day", - "value": "2020-04-28Z", - "Rep": [ - { - "D": "ENE", - "Gn": "16", - "Hn": "57", - "PPd": "10", - "S": "7", - "V": "GO", - "Dm": "12", - "FDm": "11", - "W": "7", - "U": "4", - "$": "Day" - }, - { - "D": "E", - "Gm": "9", - "Hm": "79", - "PPn": "9", - "S": "4", - "V": "VG", - "Nm": "7", - "FNm": "6", - "W": "7", - "$": "Night" - } - ] - }, - { - "type": "Day", - "value": "2020-04-29Z", - "Rep": [ - { - "D": "SE", - "Gn": "27", - "Hn": "72", - "PPd": "59", - "S": "13", - "V": "GO", - "Dm": "13", - "FDm": "10", - "W": "12", - "U": "3", - "$": "Day" - }, - { - "D": "SSE", - "Gm": "18", - "Hm": "85", - "PPn": "19", - "S": "11", - "V": "VG", - "Nm": "8", - "FNm": "6", - "W": "7", - "$": "Night" - } - ] - } - ] - } - } - } - }, - "kingslynn_hourly": { - "SiteRep": { - "Wx": { - "Param": [ - { - "name": "F", - "units": "C", - "$": "Feels Like Temperature" - }, - { - "name": "G", - "units": "mph", - "$": "Wind Gust" - }, - { - "name": "H", - "units": "%", - "$": "Screen Relative Humidity" - }, - { - "name": "T", - "units": "C", - "$": "Temperature" - }, - { - "name": "V", - "units": "", - "$": "Visibility" - }, - { - "name": "D", - "units": "compass", - "$": "Wind Direction" - }, - { - "name": "S", - "units": "mph", - "$": "Wind Speed" - }, - { - "name": "U", - "units": "", - "$": "Max UV Index" - }, - { - "name": "W", - "units": "", - "$": "Weather Type" - }, - { - "name": "Pp", - "units": "%", - "$": "Precipitation Probability" - } - ] - }, - "DV": { - "dataDate": "2020-04-25T08:00:00Z", - "type": "Forecast", - "Location": { - "i": "322380", - "lat": "52.7561", - "lon": "0.4019", - "name": "KING'S LYNN", - "country": "ENGLAND", - "continent": "EUROPE", - "elevation": "5.0", - "Period": [ - { - "type": "Day", - "value": "2020-04-25Z", - "Rep": [ - { - "D": "SSE", - "F": "4", - "G": "9", - "H": "88", - "Pp": "7", - "S": "9", - "T": "7", - "V": "GO", - "W": "8", - "U": "0", - "$": "180" - }, - { - "D": "ESE", - "F": "5", - "G": "7", - "H": "86", - "Pp": "9", - "S": "4", - "T": "7", - "V": "GO", - "W": "8", - "U": "1", - "$": "360" - }, - { - "D": "ESE", - "F": "8", - "G": "4", - "H": "75", - "Pp": "9", - "S": "4", - "T": "9", - "V": "VG", - "W": "8", - "U": "3", - "$": "540" - }, - { - "D": "E", - "F": "13", - "G": "7", - "H": "60", - "Pp": "0", - "S": "2", - "T": "14", - "V": "VG", - "W": "1", - "U": "6", - "$": "720" - }, - { - "D": "NNW", - "F": "14", - "G": "9", - "H": "57", - "Pp": "0", - "S": "4", - "T": "15", - "V": "VG", - "W": "1", - "U": "3", - "$": "900" - }, - { - "D": "ENE", - "F": "14", - "G": "9", - "H": "58", - "Pp": "0", - "S": "4", - "T": "14", - "V": "VG", - "W": "1", - "U": "1", - "$": "1080" - }, - { - "D": "SE", - "F": "8", - "G": "18", - "H": "76", - "Pp": "0", - "S": "9", - "T": "10", - "V": "VG", - "W": "0", - "U": "0", - "$": "1260" - } - ] - }, - { - "type": "Day", - "value": "2020-04-26Z", - "Rep": [ - { - "D": "SSE", - "F": "5", - "G": "16", - "H": "84", - "Pp": "0", - "S": "7", - "T": "7", - "V": "VG", - "W": "0", - "U": "0", - "$": "0" - }, - { - "D": "S", - "F": "4", - "G": "16", - "H": "89", - "Pp": "0", - "S": "7", - "T": "6", - "V": "GO", - "W": "0", - "U": "0", - "$": "180" - }, - { - "D": "S", - "F": "4", - "G": "16", - "H": "87", - "Pp": "0", - "S": "7", - "T": "7", - "V": "GO", - "W": "1", - "U": "1", - "$": "360" - }, - { - "D": "SSW", - "F": "11", - "G": "13", - "H": "69", - "Pp": "0", - "S": "9", - "T": "13", - "V": "VG", - "W": "1", - "U": "4", - "$": "540" - }, - { - "D": "SW", - "F": "15", - "G": "18", - "H": "50", - "Pp": "8", - "S": "9", - "T": "17", - "V": "VG", - "W": "1", - "U": "5", - "$": "720" - }, - { - "D": "SW", - "F": "16", - "G": "16", - "H": "47", - "Pp": "8", - "S": "7", - "T": "18", - "V": "VG", - "W": "7", - "U": "2", - "$": "900" - }, - { - "D": "SW", - "F": "15", - "G": "13", - "H": "56", - "Pp": "3", - "S": "7", - "T": "17", - "V": "VG", - "W": "3", - "U": "1", - "$": "1080" - }, - { - "D": "SW", - "F": "13", - "G": "11", - "H": "76", - "Pp": "4", - "S": "4", - "T": "13", - "V": "VG", - "W": "7", - "U": "0", - "$": "1260" - } - ] - }, - { - "type": "Day", - "value": "2020-04-27Z", - "Rep": [ - { - "D": "SSW", - "F": "10", - "G": "13", - "H": "75", - "Pp": "5", - "S": "7", - "T": "11", - "V": "GO", - "W": "7", - "U": "0", - "$": "0" - }, - { - "D": "W", - "F": "9", - "G": "13", - "H": "84", - "Pp": "9", - "S": "7", - "T": "10", - "V": "GO", - "W": "7", - "U": "0", - "$": "180" - }, - { - "D": "NW", - "F": "7", - "G": "16", - "H": "85", - "Pp": "50", - "S": "9", - "T": "9", - "V": "GO", - "W": "12", - "U": "1", - "$": "360" - }, - { - "D": "NW", - "F": "9", - "G": "11", - "H": "78", - "Pp": "36", - "S": "4", - "T": "10", - "V": "VG", - "W": "7", - "U": "3", - "$": "540" - }, - { - "D": "WNW", - "F": "11", - "G": "11", - "H": "66", - "Pp": "9", - "S": "4", - "T": "12", - "V": "VG", - "W": "7", - "U": "4", - "$": "720" - }, - { - "D": "W", - "F": "11", - "G": "13", - "H": "62", - "Pp": "9", - "S": "7", - "T": "13", - "V": "VG", - "W": "7", - "U": "2", - "$": "900" - }, - { - "D": "E", - "F": "11", - "G": "11", - "H": "64", - "Pp": "10", - "S": "7", - "T": "12", - "V": "VG", - "W": "7", - "U": "1", - "$": "1080" - }, - { - "D": "SE", - "F": "9", - "G": "13", - "H": "78", - "Pp": "9", - "S": "7", - "T": "10", - "V": "VG", - "W": "7", - "U": "0", - "$": "1260" - } - ] - }, - { - "type": "Day", - "value": "2020-04-28Z", - "Rep": [ - { - "D": "SE", - "F": "7", - "G": "13", - "H": "85", - "Pp": "9", - "S": "7", - "T": "9", - "V": "VG", - "W": "7", - "U": "0", - "$": "0" - }, - { - "D": "E", - "F": "7", - "G": "9", - "H": "91", - "Pp": "11", - "S": "4", - "T": "8", - "V": "GO", - "W": "7", - "U": "0", - "$": "180" - }, - { - "D": "ESE", - "F": "7", - "G": "9", - "H": "92", - "Pp": "12", - "S": "4", - "T": "8", - "V": "GO", - "W": "7", - "U": "1", - "$": "360" - }, - { - "D": "ESE", - "F": "9", - "G": "13", - "H": "77", - "Pp": "14", - "S": "7", - "T": "11", - "V": "GO", - "W": "7", - "U": "3", - "$": "540" - }, - { - "D": "ESE", - "F": "12", - "G": "16", - "H": "64", - "Pp": "14", - "S": "7", - "T": "13", - "V": "GO", - "W": "7", - "U": "3", - "$": "720" - }, - { - "D": "ESE", - "F": "12", - "G": "18", - "H": "66", - "Pp": "15", - "S": "9", - "T": "13", - "V": "GO", - "W": "7", - "U": "2", - "$": "900" - }, - { - "D": "SSE", - "F": "11", - "G": "13", - "H": "73", - "Pp": "15", - "S": "7", - "T": "12", - "V": "GO", - "W": "7", - "U": "1", - "$": "1080" - }, - { - "D": "SE", - "F": "9", - "G": "13", - "H": "81", - "Pp": "13", - "S": "7", - "T": "10", - "V": "GO", - "W": "7", - "U": "0", - "$": "1260" - } - ] - }, - { - "type": "Day", - "value": "2020-04-29Z", - "Rep": [ - { - "D": "SSE", - "F": "7", - "G": "13", - "H": "87", - "Pp": "11", - "S": "7", - "T": "9", - "V": "GO", - "W": "7", - "U": "0", - "$": "0" - }, - { - "D": "SSE", - "F": "7", - "G": "13", - "H": "91", - "Pp": "15", - "S": "7", - "T": "9", - "V": "GO", - "W": "8", - "U": "0", - "$": "180" - }, - { - "D": "ESE", - "F": "7", - "G": "13", - "H": "89", - "Pp": "8", - "S": "7", - "T": "9", - "V": "GO", - "W": "7", - "U": "1", - "$": "360" - }, - { - "D": "SSE", - "F": "10", - "G": "20", - "H": "75", - "Pp": "8", - "S": "11", - "T": "12", - "V": "VG", - "W": "7", - "U": "3", - "$": "540" - }, - { - "D": "S", - "F": "12", - "G": "22", - "H": "68", - "Pp": "11", - "S": "11", - "T": "14", - "V": "GO", - "W": "7", - "U": "3", - "$": "720" - }, - { - "D": "S", - "F": "12", - "G": "27", - "H": "68", - "Pp": "55", - "S": "13", - "T": "14", - "V": "GO", - "W": "12", - "U": "1", - "$": "900" - }, - { - "D": "SSE", - "F": "11", - "G": "22", - "H": "76", - "Pp": "34", - "S": "11", - "T": "13", - "V": "VG", - "W": "10", - "U": "1", - "$": "1080" - }, - { - "D": "SSE", - "F": "9", - "G": "20", - "H": "86", - "Pp": "20", - "S": "11", - "T": "11", - "V": "VG", - "W": "7", - "U": "0", - "$": "1260" - } - ] - } - ] - } - } - } - } -} \ No newline at end of file diff --git a/tests/integration/test_datapoint.py b/tests/integration/test_datapoint.py deleted file mode 100644 index d305621..0000000 --- a/tests/integration/test_datapoint.py +++ /dev/null @@ -1,163 +0,0 @@ -from datetime import datetime, date -import json -import pathlib -import requests -from requests_mock import Mocker -import unittest -from unittest.mock import patch - -import datapoint - -DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S%z" - - -class MockDateTime(datetime): - """Replacement for datetime that can be mocked for testing.""" - def __new__(cls, *args, **kwargs): - return datetime.__new__(datetime, *args, **kwargs) - - -class TestDataPoint(unittest.TestCase): - @Mocker() - def setUp(self, mock_request): - with open("{}/datapoint.json".format(pathlib.Path(__file__).parent.absolute())) as f: - mock_json = json.load(f) - - self.all_sites = json.dumps(mock_json['all_sites']) - self.wavertree_hourly = json.dumps(mock_json['wavertree_hourly']) - self.wavertree_daily = json.dumps(mock_json['wavertree_daily']) - self.kingslynn_hourly = json.dumps(mock_json['kingslynn_hourly']) - - self.conn = datapoint.connection(api_key="abcdefgh-acbd-abcd-abcd-abcdefghijkl") - - mock_request.get('/public/data/val/wxfcs/all/json/sitelist/', text=self.all_sites) - self.wavertree = self.conn.get_nearest_forecast_site(53.38374, -2.90929) - self.kingslynn = self.conn.get_nearest_forecast_site(52.75556, 0.44231) - - - @Mocker() - @patch('datetime.datetime', MockDateTime) - def test_wavertree_hourly(self, mock_request): - from datetime import datetime, timezone - MockDateTime.now = classmethod(lambda cls, **kwargs: datetime(2020, 4, 25, 12, tzinfo=timezone.utc)) - mock_request.get('/public/data/val/wxfcs/all/json/354107?res=3hourly', text=self.wavertree_hourly) - - forecast = self.conn.get_forecast_for_site(self.wavertree.location_id, "3hourly") - now = forecast.now() - - self.assertEqual(self.wavertree.location_id, '354107') - self.assertEqual(self.wavertree.name, 'Wavertree') - - self.assertEqual(now.date.strftime(DATETIME_FORMAT), '2020-04-25 12:00:00+0000') - self.assertEqual(now.weather.value, '1') - self.assertEqual(now.temperature.value, 17) - self.assertEqual(now.feels_like_temperature.value, 14) - self.assertEqual(now.wind_speed.value, 9) - self.assertEqual(now.wind_direction.value, 'SSE') - self.assertEqual(now.wind_gust.value, 16) - self.assertEqual(now.visibility.value, 'GO') - self.assertEqual(now.uv.value, '5') - self.assertEqual(now.precipitation.value, 0) - self.assertEqual(now.humidity.value, 50) - - self.assertEqual(len(forecast.days), 5) - self.assertEqual(len(forecast.days[0].timesteps), 7) - self.assertEqual(len(forecast.days[3].timesteps), 8) - - self.assertEqual(forecast.days[3].timesteps[7].date.strftime(DATETIME_FORMAT), '2020-04-28 21:00:00+0000') - self.assertEqual(forecast.days[3].timesteps[7].weather.value, '7') - self.assertEqual(forecast.days[3].timesteps[7].temperature.value, 10) - self.assertEqual(forecast.days[3].timesteps[7].feels_like_temperature.value, 9) - self.assertEqual(forecast.days[3].timesteps[7].wind_speed.value, 4) - self.assertEqual(forecast.days[3].timesteps[7].wind_direction.value, 'NNE') - self.assertEqual(forecast.days[3].timesteps[7].wind_gust.value, 11) - self.assertEqual(forecast.days[3].timesteps[7].visibility.value, 'VG') - self.assertEqual(forecast.days[3].timesteps[7].uv.value, '0') - self.assertEqual(forecast.days[3].timesteps[7].precipitation.value, 9) - self.assertEqual(forecast.days[3].timesteps[7].humidity.value, 72) - - - @Mocker() - @patch('datetime.datetime', MockDateTime) - def test_wavertree_daily(self, mock_request): - from datetime import datetime, timezone - MockDateTime.now = classmethod(lambda cls, **kwargs: datetime(2020, 4, 25, 12, tzinfo=timezone.utc)) - mock_request.get('/public/data/val/wxfcs/all/json/354107?res=daily', text=self.wavertree_daily) - - forecast = self.conn.get_forecast_for_site(self.wavertree.location_id, "daily") - now = forecast.now() - - self.assertEqual(self.wavertree.location_id, '354107') - self.assertEqual(self.wavertree.name, 'Wavertree') - - self.assertEqual(now.date.strftime(DATETIME_FORMAT), '2020-04-25 12:00:00+0000') - self.assertEqual(now.weather.value, '1') - self.assertEqual(now.temperature.value, 19) - self.assertEqual(now.feels_like_temperature.value, 18) - self.assertEqual(now.wind_speed.value, 9) - self.assertEqual(now.wind_direction.value, 'SSE') - self.assertEqual(now.wind_gust.value, 16) - self.assertEqual(now.visibility.value, 'GO') - self.assertEqual(now.uv.value, '5') - self.assertEqual(now.precipitation.value, 2) - self.assertEqual(now.humidity.value, 50) - - self.assertEqual(len(forecast.days), 5) - self.assertEqual(len(forecast.days[0].timesteps), 2) - self.assertEqual(len(forecast.days[4].timesteps), 2) - - self.assertEqual(forecast.days[4].timesteps[1].date.strftime(DATETIME_FORMAT), '2020-04-29 12:00:00+0000') - self.assertEqual(forecast.days[4].timesteps[1].weather.value, '12') - self.assertEqual(forecast.days[4].timesteps[1].temperature.value, 13) - self.assertEqual(forecast.days[4].timesteps[1].feels_like_temperature.value, 10) - self.assertEqual(forecast.days[4].timesteps[1].wind_speed.value, 13) - self.assertEqual(forecast.days[4].timesteps[1].wind_direction.value, 'SE') - self.assertEqual(forecast.days[4].timesteps[1].wind_gust.value, 27) - self.assertEqual(forecast.days[4].timesteps[1].visibility.value, 'GO') - self.assertEqual(forecast.days[4].timesteps[1].uv.value, '3') - self.assertEqual(forecast.days[4].timesteps[1].precipitation.value, 59) - self.assertEqual(forecast.days[4].timesteps[1].humidity.value, 72) - - @Mocker() - @patch('datetime.datetime', MockDateTime) - def test_kingslynn_hourly(self, mock_request): - from datetime import datetime, timezone - MockDateTime.now = classmethod(lambda cls, **kwargs: datetime(2020, 4, 25, 12, tzinfo=timezone.utc)) - mock_request.get('/public/data/val/wxfcs/all/json/322380?res=3hourly', text=self.kingslynn_hourly) - - forecast = self.conn.get_forecast_for_site(self.kingslynn.location_id, "3hourly") - now = forecast.now() - - self.assertEqual(self.kingslynn.location_id, '322380') - self.assertEqual(self.kingslynn.name, "King's Lynn") - - self.assertEqual(now.date.strftime(DATETIME_FORMAT), '2020-04-25 12:00:00+0000') - self.assertEqual(now.weather.value, '1') - self.assertEqual(now.temperature.value, 14) - self.assertEqual(now.feels_like_temperature.value, 13) - self.assertEqual(now.wind_speed.value, 2) - self.assertEqual(now.wind_direction.value, 'E') - self.assertEqual(now.wind_gust.value, 7) - self.assertEqual(now.visibility.value, 'VG') - self.assertEqual(now.uv.value, '6') - self.assertEqual(now.precipitation.value, 0) - self.assertEqual(now.humidity.value, 60) - - self.assertEqual(len(forecast.days), 5) - self.assertEqual(len(forecast.days[0].timesteps), 7) - self.assertEqual(len(forecast.days[4].timesteps), 8) - - self.assertEqual(forecast.days[4].timesteps[5].date.strftime(DATETIME_FORMAT), '2020-04-29 15:00:00+0000') - self.assertEqual(forecast.days[4].timesteps[5].weather.value, '12') - self.assertEqual(forecast.days[4].timesteps[5].temperature.value, 14) - self.assertEqual(forecast.days[4].timesteps[5].feels_like_temperature.value, 12) - self.assertEqual(forecast.days[4].timesteps[5].wind_speed.value, 13) - self.assertEqual(forecast.days[4].timesteps[5].wind_direction.value, 'S') - self.assertEqual(forecast.days[4].timesteps[5].wind_gust.value, 27) - self.assertEqual(forecast.days[4].timesteps[5].visibility.value, 'GO') - self.assertEqual(forecast.days[4].timesteps[5].uv.value, '1') - self.assertEqual(forecast.days[4].timesteps[5].precipitation.value, 55) - self.assertEqual(forecast.days[4].timesteps[5].humidity.value, 68) - -if __name__ == '__main__': - unittest.main() diff --git a/tests/integration/test_manager.py b/tests/integration/test_manager.py index d258ea9..38d83a5 100644 --- a/tests/integration/test_manager.py +++ b/tests/integration/test_manager.py @@ -1,263 +1,178 @@ -import datetime -import os -import unittest -import datapoint - -class ManagerIntegrationTestCase(unittest.TestCase): - - def setUp(self): - self.manager = datapoint.Manager(api_key=os.environ['API_KEY']) - - def test_site(self): - site = self.manager.get_nearest_forecast_site(latitude=51.500728, longitude=-0.124626) - self.assertEqual(site.name.upper(), 'HORSEGUARDS PARADE') - - def test_get_forecast_sites(self): - sites = self.manager.get_forecast_sites() - self.assertIsInstance(sites, list) - # What is this assert testing - assert sites - - def test_get_daily_forecast(self): - site = self.manager.get_nearest_forecast_site(latitude=51.500728, longitude=-0.124626) - forecast = self.manager.get_forecast_for_site(site.location_id, 'daily') - self.assertIsInstance(forecast, datapoint.Forecast.Forecast) - self.assertEqual(forecast.continent.upper(), 'EUROPE') - self.assertEqual(forecast.country.upper(), 'ENGLAND') - self.assertEqual(forecast.name.upper(),'HORSEGUARDS PARADE') - self.assertLess(abs(float(forecast.latitude) - 51.500728), 0.1) - self.assertLess(abs(float(forecast.longitude) - (-0.124626)), 0.1) - # Forecast should have been made within last 3 hours - tz = forecast.data_date.tzinfo - self.assertLess(forecast.data_date - datetime.datetime.now(tz=tz), - datetime.timedelta(hours=3)) - # First forecast should be less than 12 hours away - tz = forecast.days[0].timesteps[0].date.tzinfo - self.assertLess(forecast.days[0].timesteps[0].date - - datetime.datetime.now(tz=tz), - datetime.timedelta(hours=12)) - for day in forecast.days: - for timestep in day.timesteps: - self.assertIn(timestep.name, ['Day', 'Night']) - self.assertEqual(self.manager._weather_to_text(int(timestep.weather.value)), - timestep.weather.text) - - self.assertGreater(timestep.temperature.value, -100) - self.assertLess(timestep.temperature.value, 100) - self.assertEqual(timestep.temperature.units, 'C') - - self.assertGreater(timestep.feels_like_temperature.value , -100) - self.assertLess(timestep.feels_like_temperature.value , 100) - self.assertEqual(timestep.feels_like_temperature.units, 'C') - - self.assertGreaterEqual(timestep.wind_speed.value, 0) - self.assertLess(timestep.wind_speed.value, 300) - self.assertEqual(timestep.wind_speed.units,'mph') - - for char in timestep.wind_direction.value: - self.assertIn(char, ['N', 'E', 'S', 'W']) - self.assertEqual(timestep.wind_direction.units, 'compass') - - self.assertGreaterEqual(timestep.wind_gust.value, 0) - self.assertLess(timestep.wind_gust.value, 300) - self.assertEqual(timestep.wind_gust.units, 'mph') - - self.assertIn(timestep.visibility.value, - ['UN', 'VP', 'PO', 'MO', 'GO', 'VG', 'EX']) - - self.assertGreaterEqual(timestep.precipitation.value, 0) - self.assertLessEqual(timestep.precipitation.value, 100) - self.assertEqual(timestep.precipitation.units, '%') - - self.assertGreaterEqual(timestep.humidity.value, 0) - self.assertLessEqual(timestep.humidity.value, 100) - self.assertEqual(timestep.humidity.units, '%') - - if hasattr(timestep.uv, 'value'): - self.assertGreaterEqual(int(timestep.uv.value), 0) - self.assertLess(int(timestep.uv.value), 30) - - - def test_get_3hour_forecast(self): - site = self.manager.get_nearest_forecast_site(latitude=51.500728, longitude=-0.124626) - forecast = self.manager.get_forecast_for_site(site.location_id, '3hourly') - self.assertIsInstance(forecast, datapoint.Forecast.Forecast) - self.assertEqual(forecast.continent.upper(), 'EUROPE') - self.assertEqual(forecast.country.upper(), 'ENGLAND') - self.assertEqual(forecast.name.upper(),'HORSEGUARDS PARADE') - self.assertLess(abs(float(forecast.latitude) - 51.500728), 0.1) - self.assertLess(abs(float(forecast.longitude) - (-0.124626)), 0.1) - # Forecast should have been made within last 3 hours - tz = forecast.data_date.tzinfo - self.assertLess(forecast.data_date - datetime.datetime.now(tz=tz), - datetime.timedelta(hours=3)) - # First forecast should be less than 12 hours away - tz = forecast.days[0].timesteps[0].date.tzinfo - self.assertLess(forecast.days[0].timesteps[0].date - - datetime.datetime.now(tz=tz), - datetime.timedelta(hours=12)) - for day in forecast.days: - for timestep in day.timesteps: - self.assertIsInstance(timestep.name, int) - self.assertEqual(self.manager._weather_to_text( - int(timestep.weather.value)), timestep.weather.text) - - self.assertGreater(timestep.temperature.value, -100) - self.assertLess(timestep.temperature.value, 100) - self.assertEqual(timestep.temperature.units, 'C') - - self.assertGreater(timestep.feels_like_temperature.value , -100) - self.assertLess(timestep.feels_like_temperature.value , 100) - self.assertEqual(timestep.feels_like_temperature.units, 'C') - - self.assertGreaterEqual(timestep.wind_speed.value, 0) - self.assertLess(timestep.wind_speed.value, 300) - self.assertEqual(timestep.wind_speed.units,'mph') - - for char in timestep.wind_direction.value: - self.assertIn(char, ['N', 'E', 'S', 'W']) - self.assertEqual(timestep.wind_direction.units, 'compass') - - self.assertGreaterEqual(timestep.wind_gust.value, 0) - self.assertLess(timestep.wind_gust.value, 300) - self.assertEqual(timestep.wind_gust.units, 'mph') - - self.assertIn(timestep.visibility.value, - ['UN', 'VP', 'PO', 'MO', 'GO', 'VG', 'EX']) - - self.assertGreaterEqual(timestep.precipitation.value, 0) - self.assertLessEqual(timestep.precipitation.value, 100) - self.assertEqual(timestep.precipitation.units, '%') - - self.assertGreaterEqual(timestep.humidity.value, 0) - self.assertLessEqual(timestep.humidity.value, 100) - self.assertEqual(timestep.humidity.units, '%') - - if hasattr(timestep.uv, 'value'): - self.assertGreaterEqual(int(timestep.uv.value), 0) - self.assertLess(int(timestep.uv.value), 30) - - def test_get_nearest_observation_site(self): - site = self.manager.get_nearest_observation_site(longitude=-0.1025, latitude=51.3263) - self.assertEqual(site.name.upper(), 'KENLEY') - - def test_get_observation_sites(self): - sites = self.manager.get_observation_sites() - self.assertIsInstance(sites, list) - # Mystery assert - assert sites - - def test_get_observation_with_wind_data(self): - observation = self.manager.get_observations_for_site(3840) - self.assertIsInstance(observation, datapoint.Observation.Observation) - self.assertEqual(observation.continent.upper(), 'EUROPE') - self.assertEqual(observation.country.upper(), 'ENGLAND') - self.assertEqual(observation.name.upper(), 'DUNKESWELL AERODROME') - - # Observation should be from within the last hour - tz = observation.data_date.tzinfo - self.assertLess(observation.data_date - datetime.datetime.now(tz=tz), - datetime.timedelta(hours=1)) - - # First observation should be between 24 and 25 hours old - tz = observation.days[0].timesteps[0].date.tzinfo - self.assertGreater(datetime.datetime.now(tz=tz) - - observation.days[0].timesteps[0].date, - datetime.timedelta(hours=24)) - self.assertLess(datetime.datetime.now(tz=tz) - - observation.days[0].timesteps[0].date, - datetime.timedelta(hours=25)) - - # Should have total 25 observations across all days - number_of_timesteps = 0 - for day in observation.days: - number_of_timesteps += len(day.timesteps) - self.assertEqual(number_of_timesteps, 25) - - for day in observation.days: - for timestep in day.timesteps: - self.assertIsInstance(timestep.name, int) - if timestep.weather.value != 'Not reported': - self.assertEqual(self.manager._weather_to_text(int(timestep.weather.value)), - timestep.weather.text) - self.assertGreater(timestep.temperature.value, -100) - self.assertLess(timestep.temperature.value, 100) - self.assertEqual(timestep.temperature.units, 'C') - - if timestep.wind_speed.value != 'Not reported': - self.assertGreaterEqual(timestep.wind_speed.value, 0) - self.assertLess(timestep.wind_speed.value, 300) - self.assertEqual(timestep.wind_speed.units,'mph') - - if timestep.wind_direction.value != 'Not reported': - for char in timestep.wind_direction.value: - self.assertIn(char, ['N', 'E', 'S', 'W']) - self.assertEqual(timestep.wind_direction.units, 'compass') - - self.assertGreaterEqual(timestep.visibility.value, 0) - self.assertIn(timestep.visibility.text, - ['UN', 'VP', 'PO', 'MO', 'GO', 'VG', 'EX']) - - self.assertGreaterEqual(timestep.humidity.value, 0) - self.assertLessEqual(timestep.humidity.value, 100) - self.assertEqual(timestep.humidity.units, '%') - - self.assertGreaterEqual(timestep.dew_point.value, 0) - self.assertLessEqual(timestep.dew_point.value, 100) - self.assertEqual(timestep.dew_point.units, 'C') - - if timestep.pressure.value != 'Not reported': - self.assertGreaterEqual(timestep.pressure.value, 900) - self.assertLessEqual(timestep.pressure.value, 1100) - self.assertEqual(timestep.pressure.units, 'hpa') - - if timestep.pressure_tendency.value != 'Not reported': - self.assertIn(timestep.pressure_tendency.value, ['R','F','S']) - self.assertEqual(timestep.pressure_tendency.units, 'Pa/s') - - - # def test_get_observation_without_wind_data(self): - # observation = self.manager.get_observations_for_site(3220) - # assert isinstance(observation, datapoint.Observation.Observation) - # assert observation.continent.upper() == 'EUROPE' - # assert observation.country.upper() == 'ENGLAND' - # assert observation.name.upper() == 'CARLISLE' - - # # Observation should be from within the last hour - # tz = observation.data_date.tzinfo - # assert (observation.data_date - # - datetime.datetime.now(tz=tz) < datetime.timedelta(hours=1)) - - # # First observation should be between 24 and 25 hours old - # tz = observation.days[0].timesteps[0].date.tzinfo - # assert (datetime.datetime.now(tz=tz) - observation.days[0].timesteps[0].date > datetime.timedelta(hours=24)) - # assert (datetime.datetime.now(tz=tz) - observation.days[0].timesteps[0].date < datetime.timedelta(hours=25)) - - # # Should have total 25 observations across all days - # number_of_timesteps = 0 - # for day in observation.days: - # number_of_timesteps += len(day.timesteps) - # assert number_of_timesteps == 25 - - # for day in observation.days: - # for timestep in day.timesteps: - # assert isinstance(timestep.name, int) - # if timestep.weather.value != 'Not reported': - # assert self.manager._weather_to_text( - # int(timestep.weather.value)) == timestep.weather.text - # assert -100 < timestep.temperature.value < 100 - # assert timestep.temperature.units == 'C' - # assert timestep.wind_speed is None - # assert timestep.wind_gust is None - # assert timestep.wind_direction is None - # assert 0 <= timestep.visibility.value - # assert (timestep.visibility.text in - # ['UN', 'VP', 'PO', 'MO', 'GO', 'VG', 'EX']) - # assert 0 <= timestep.humidity.value <= 100 - # assert timestep.humidity.units == '%' - # assert -100 < timestep.dew_point.value < 100 - # assert timestep.dew_point.units == 'C' - # assert 900 < timestep.pressure.value < 1100 - # assert timestep.pressure.units == 'hpa' - # assert timestep.pressure_tendency.value in ('R','F','S') - # assert timestep.pressure_tendency.units == 'Pa/s' +import pytest +import requests + +import tests.reference_data.reference_data_test_forecast as reference_data_test_forecast +from datapoint.Manager import Manager + + +class MockResponseHourly: + def __init__(self): + with open("./tests/reference_data/hourly_api_data.json") as f: + my_json = f.read() + + self.text = my_json + + @staticmethod + def raise_for_status(): + pass + + +@pytest.fixture +def mock_response_hourly(monkeypatch): + def mock_get(*args, **kwargs): + return MockResponseHourly() + + monkeypatch.setattr(requests.Session, "get", mock_get) + + +@pytest.fixture +def hourly_forecast(mock_response_hourly): + m = Manager(api_key="aaaaaaaaaaaaaaaaaaaaaaaaa") + f = m.get_forecast(50.9992, 0.0154, frequency="hourly") + return f + + +@pytest.fixture +def expected_first_hourly_timestep(): + return reference_data_test_forecast.EXPECTED_FIRST_HOURLY_TIMESTEP + + +class MockResponseThreeHourly: + def __init__(self): + with open("./tests/reference_data/three_hourly_api_data.json") as f: + my_json = f.read() + + self.text = my_json + + @staticmethod + def raise_for_status(): + pass + + +@pytest.fixture +def mock_response_three_hourly(monkeypatch): + def mock_get(*args, **kwargs): + return MockResponseThreeHourly() + + monkeypatch.setattr(requests.Session, "get", mock_get) + + +@pytest.fixture +def three_hourly_forecast(mock_response_three_hourly): + m = Manager(api_key="aaaaaaaaaaaaaaaaaaaaaaaaa") + f = m.get_forecast(50.9992, 0.0154, frequency="three-hourly") + return f + + +@pytest.fixture +def expected_first_three_hourly_timestep(): + return reference_data_test_forecast.EXPECTED_FIRST_THREE_HOURLY_TIMESTEP + + +class MockResponseDaily: + def __init__(self): + with open("./tests/reference_data/daily_api_data.json") as f: + my_json = f.read() + + self.text = my_json + + @staticmethod + def raise_for_status(): + pass + + +@pytest.fixture +def mock_response_daily(monkeypatch): + def mock_get(*args, **kwargs): + return MockResponseDaily() + + monkeypatch.setattr(requests.Session, "get", mock_get) + + +@pytest.fixture +def daily_forecast(mock_response_daily): + m = Manager(api_key="aaaaaaaaaaaaaaaaaaaaaaaaa") + f = m.get_forecast(50.9992, 0.0154, frequency="daily") + return f + + +@pytest.fixture +def expected_first_daily_timestep(): + return reference_data_test_forecast.EXPECTED_FIRST_DAILY_TIMESTEP + + +class TestHourly: + def test_location_name(self, hourly_forecast): + assert hourly_forecast.name == "Sheffield Park" + + def test_forecast_frequency(self, hourly_forecast): + assert hourly_forecast.frequency == "hourly" + + def test_forecast_location_latitude(self, hourly_forecast): + assert hourly_forecast.forecast_latitude == 50.9992 + + def test_forecast_location_longitude(self, hourly_forecast): + assert hourly_forecast.forecast_longitude == 0.0154 + + def test_forecast_distance_from_request(self, hourly_forecast): + assert hourly_forecast.distance_from_requested_location == 1081.5349 + + def test_forecast_elevation(self, hourly_forecast): + assert hourly_forecast.elevation == 37.0 + + def test_forecast_first_timestep( + self, hourly_forecast, expected_first_hourly_timestep + ): + assert hourly_forecast.timesteps[0] == expected_first_hourly_timestep + + +class TestThreeHourly: + def test_forecast_frequency(self, three_hourly_forecast): + assert three_hourly_forecast.frequency == "three-hourly" + + def test_forecast_location_name(self, three_hourly_forecast): + assert three_hourly_forecast.name == "Sheffield Park" + + def test_forecast_location_latitude(self, three_hourly_forecast): + assert three_hourly_forecast.forecast_latitude == 50.9992 + + def test_forecast_location_longitude(self, three_hourly_forecast): + assert three_hourly_forecast.forecast_longitude == 0.0154 + + def test_forecast_distance_from_request(self, three_hourly_forecast): + assert three_hourly_forecast.distance_from_requested_location == 1081.5349 + + def test_forecast_elevation(self, three_hourly_forecast): + assert three_hourly_forecast.elevation == 37.0 + + def test_forecast_first_timestep( + self, three_hourly_forecast, expected_first_three_hourly_timestep + ): + assert ( + three_hourly_forecast.timesteps[0] == expected_first_three_hourly_timestep + ) + + +class TestDaily: + def test_forecast_frequency(self, daily_forecast): + assert daily_forecast.frequency == "daily" + + def test_forecast_location_name(self, daily_forecast): + assert daily_forecast.name == "Sheffield Park" + + def test_forecast_location_latitude(self, daily_forecast): + assert daily_forecast.forecast_latitude == 50.9992 + + def test_forecast_location_longitude(self, daily_forecast): + assert daily_forecast.forecast_longitude == 0.0154 + + def test_forecast_distance_from_request(self, daily_forecast): + assert daily_forecast.distance_from_requested_location == 1081.5349 + + def test_forecast_elevation(self, daily_forecast): + assert daily_forecast.elevation == 37.0 + + def test_forecast_first_timestep( + self, daily_forecast, expected_first_daily_timestep + ): + assert daily_forecast.timesteps[0] == expected_first_daily_timestep diff --git a/tests/integration/test_regions.py b/tests/integration/test_regions.py deleted file mode 100644 index ee52a4e..0000000 --- a/tests/integration/test_regions.py +++ /dev/null @@ -1,41 +0,0 @@ -import os -from requests import HTTPError -import unittest -import datapoint - - -class RegionsIntegrationTestCase(unittest.TestCase): - def setUp(self): - self.manager = datapoint.Manager(api_key=os.environ['API_KEY']) - self.regions = self.manager.regions - - def test_key(self): - self.assertEqual(self.regions.api_key, os.environ['API_KEY']) - - def test_call_api(self): - self.assertIn ( - u'RegionalFcst', self.regions.call_api('/500')) - self.assertRaises( - HTTPError, self.regions.call_api, '/fake_path') - self.assertRaises( - HTTPError, self.regions.call_api, '/500', key='fake_key') - - def test_get_all_regions(self): - all_regions = self.regions.get_all_regions() - sample_region = next( - region for region in all_regions - if region.location_id == '515') - self.assertEqual(sample_region.name, 'UK') - self.assertEqual(sample_region.region, 'uk') - - def test_get_raw_forecast(self): - sample_region = self.regions.get_all_regions()[0] - response = self.regions.get_raw_forecast( - sample_region.location_id)['RegionalFcst'] - self.assertEqual(response['regionId'], sample_region.region) - - # Based on what Datapoint serves at time of writing... - forecast_periods = response['FcstPeriods']['Period'] - forecast_ids = [period['id'] for period in forecast_periods] - expected_ids = ['day1to2', 'day3to5', 'day6to15', 'day16to30'] - self.assertEqual(forecast_ids, expected_ids) diff --git a/tests/reference_data/__init__.py b/tests/reference_data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/reference_data/daily_api_data.json b/tests/reference_data/daily_api_data.json new file mode 100644 index 0000000..5cab156 --- /dev/null +++ b/tests/reference_data/daily_api_data.json @@ -0,0 +1,805 @@ +{ + "type": "FeatureCollection", + "parameters": [{ + "daySignificantWeatherCode": { + "type": "Parameter", + "description": "Day Significant Weather Code", + "unit": { + "label": "dimensionless", + "symbol": { + "value": "https://metoffice.apiconnect.ibmcloud.com/metoffice/production/", + "type": "1" + } + } + }, + "midnightRelativeHumidity": { + "type": "Parameter", + "description": "Relative Humidity at Local Midnight", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "nightProbabilityOfHeavyRain": { + "type": "Parameter", + "description": "Probability of Heavy Rain During The Night", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "midnight10MWindSpeed": { + "type": "Parameter", + "description": "10m Wind Speed at Local Midnight", + "unit": { + "label": "metres per second", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m/s" + } + } + }, + "nightUpperBoundMinFeelsLikeTemp": { + "type": "Parameter", + "description": "Upper Bound on Night Minimum Feels Like Air Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "nightUpperBoundMinTemp": { + "type": "Parameter", + "description": "Upper Bound on Night Minimum Screen Air Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "midnightVisibility": { + "type": "Parameter", + "description": "Visibility at Local Midnight", + "unit": { + "label": "metres", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m" + } + } + }, + "dayUpperBoundMaxFeelsLikeTemp": { + "type": "Parameter", + "description": "Upper Bound on Day Maximum Feels Like Air Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "nightProbabilityOfRain": { + "type": "Parameter", + "description": "Probability of Rain During The Night", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "midday10MWindDirection": { + "type": "Parameter", + "description": "10m Wind Direction at Local Midday", + "unit": { + "label": "degrees", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "deg" + } + } + }, + "nightLowerBoundMinFeelsLikeTemp": { + "type": "Parameter", + "description": "Lower Bound on Night Minimum Feels Like Air Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "nightProbabilityOfHail": { + "type": "Parameter", + "description": "Probability of Hail During The Night", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "middayMslp": { + "type": "Parameter", + "description": "Mean Sea Level Pressure at Local Midday", + "unit": { + "label": "pascals", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Pa" + } + } + }, + "dayProbabilityOfHeavySnow": { + "type": "Parameter", + "description": "Probability of Heavy Snow During The Day", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "nightProbabilityOfPrecipitation": { + "type": "Parameter", + "description": "Probability of Precipitation During The Night", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "dayProbabilityOfHail": { + "type": "Parameter", + "description": "Probability of Hail During The Day", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "dayProbabilityOfRain": { + "type": "Parameter", + "description": "Probability of Rain During The Day", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "midday10MWindSpeed": { + "type": "Parameter", + "description": "10m Wind Speed at Local Midday", + "unit": { + "label": "metres per second", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m/s" + } + } + }, + "midday10MWindGust": { + "type": "Parameter", + "description": "10m Wind Gust Speed at Local Midday", + "unit": { + "label": "metres per second", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m/s" + } + } + }, + "middayVisibility": { + "type": "Parameter", + "description": "Visibility at Local Midday", + "unit": { + "label": "metres", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m" + } + } + }, + "midnight10MWindGust": { + "type": "Parameter", + "description": "10m Wind Gust Speed at Local Midnight", + "unit": { + "label": "metres per second", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m/s" + } + } + }, + "midnightMslp": { + "type": "Parameter", + "description": "Mean Sea Level Pressure at Local Midnight", + "unit": { + "label": "pascals", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Pa" + } + } + }, + "dayProbabilityOfSferics": { + "type": "Parameter", + "description": "Probability of Sferics During The Day", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "nightSignificantWeatherCode": { + "type": "Parameter", + "description": "Night Significant Weather Code", + "unit": { + "label": "dimensionless", + "symbol": { + "value": "https://metoffice.apiconnect.ibmcloud.com/metoffice/production/", + "type": "1" + } + } + }, + "dayProbabilityOfPrecipitation": { + "type": "Parameter", + "description": "Probability of Precipitation During The Day", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "dayProbabilityOfHeavyRain": { + "type": "Parameter", + "description": "Probability of Heavy Rain During The Day", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "dayMaxScreenTemperature": { + "type": "Parameter", + "description": "Day Maximum Screen Air Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "nightMinScreenTemperature": { + "type": "Parameter", + "description": "Night Minimum Screen Air Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "midnight10MWindDirection": { + "type": "Parameter", + "description": "10m Wind Direction at Local Midnight", + "unit": { + "label": "degrees", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "deg" + } + } + }, + "maxUvIndex": { + "type": "Parameter", + "description": "Day Maximum UV Index", + "unit": { + "label": "dimensionless", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "1" + } + } + }, + "dayProbabilityOfSnow": { + "type": "Parameter", + "description": "Probability of Snow During The Day", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "nightProbabilityOfSnow": { + "type": "Parameter", + "description": "Probability of Snow During The Night", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "dayLowerBoundMaxTemp": { + "type": "Parameter", + "description": "Lower Bound on Day Maximum Screen Air Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "nightProbabilityOfHeavySnow": { + "type": "Parameter", + "description": "Probability of Heavy Snow During The Night", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "dayLowerBoundMaxFeelsLikeTemp": { + "type": "Parameter", + "description": "Lower Bound on Day Maximum Feels Like Air Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "dayUpperBoundMaxTemp": { + "type": "Parameter", + "description": "Upper Bound on Day Maximum Screen Air Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "dayMaxFeelsLikeTemp": { + "type": "Parameter", + "description": "Day Maximum Feels Like Air Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "middayRelativeHumidity": { + "type": "Parameter", + "description": "Relative Humidity at Local Midday", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "nightLowerBoundMinTemp": { + "type": "Parameter", + "description": "Lower Bound on Night Minimum Screen Air Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "nightMinFeelsLikeTemp": { + "type": "Parameter", + "description": "Night Minimum Feels Like Air Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "nightProbabilityOfSferics": { + "type": "Parameter", + "description": "Probability of Sferics During The Night", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + } + }], + "features": [{ + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [0.0154, 50.9992, 37.0] + }, + "properties": { + "location": { + "name": "Sheffield Park" + }, + "requestPointDistance": 1081.5349, + "modelRunDate": "2024-02-17T14:00Z", + "timeSeries": [{ + "time": "2024-02-16T00:00Z", + "midday10MWindSpeed": 5.04, + "midnight10MWindSpeed": 1.39, + "midday10MWindDirection": 273, + "midnight10MWindDirection": 243, + "midday10MWindGust": 8.75, + "midnight10MWindGust": 7.2, + "middayVisibility": 28772, + "midnightVisibility": 27712, + "middayRelativeHumidity": 75.21, + "midnightRelativeHumidity": 80.91, + "middayMslp": 101680, + "midnightMslp": 102640, + "nightSignificantWeatherCode": 7, + "dayMaxScreenTemperature": 12.82, + "nightMinScreenTemperature": 5.32, + "dayUpperBoundMaxTemp": 14.1, + "nightUpperBoundMinTemp": 9.17, + "dayLowerBoundMaxTemp": 11.97, + "nightLowerBoundMinTemp": 3.56, + "nightMinFeelsLikeTemp": 6.27, + "dayUpperBoundMaxFeelsLikeTemp": 12.47, + "nightUpperBoundMinFeelsLikeTemp": 8.74, + "dayLowerBoundMaxFeelsLikeTemp": 10.01, + "nightLowerBoundMinFeelsLikeTemp": 2.75, + "nightProbabilityOfPrecipitation": 11, + "nightProbabilityOfSnow": 0, + "nightProbabilityOfHeavySnow": 0, + "nightProbabilityOfRain": 10, + "nightProbabilityOfHeavyRain": 0, + "nightProbabilityOfHail": 0, + "nightProbabilityOfSferics": 0 + }, { + "time": "2024-02-17T00:00Z", + "midday10MWindSpeed": 4.32, + "midnight10MWindSpeed": 6.1, + "midday10MWindDirection": 230, + "midnight10MWindDirection": 218, + "midday10MWindGust": 8.75, + "midnight10MWindGust": 12.98, + "middayVisibility": 4158, + "midnightVisibility": 5915, + "middayRelativeHumidity": 97.38, + "midnightRelativeHumidity": 93.62, + "middayMslp": 103140, + "midnightMslp": 102800, + "maxUvIndex": 1, + "daySignificantWeatherCode": 8, + "nightSignificantWeatherCode": 15, + "dayMaxScreenTemperature": 12.0, + "nightMinScreenTemperature": 9.96, + "dayUpperBoundMaxTemp": 13.71, + "nightUpperBoundMinTemp": 10.71, + "dayLowerBoundMaxTemp": 10.23, + "nightLowerBoundMinTemp": 9.04, + "dayMaxFeelsLikeTemp": 10.6, + "nightMinFeelsLikeTemp": 7.76, + "dayUpperBoundMaxFeelsLikeTemp": 11.49, + "nightUpperBoundMinFeelsLikeTemp": 8.28, + "dayLowerBoundMaxFeelsLikeTemp": 8.38, + "nightLowerBoundMinFeelsLikeTemp": 7.04, + "dayProbabilityOfPrecipitation": 18, + "nightProbabilityOfPrecipitation": 97, + "dayProbabilityOfSnow": 0, + "nightProbabilityOfSnow": 0, + "dayProbabilityOfHeavySnow": 0, + "nightProbabilityOfHeavySnow": 0, + "dayProbabilityOfRain": 18, + "nightProbabilityOfRain": 97, + "dayProbabilityOfHeavyRain": 5, + "nightProbabilityOfHeavyRain": 96, + "dayProbabilityOfHail": 0, + "nightProbabilityOfHail": 20, + "dayProbabilityOfSferics": 0, + "nightProbabilityOfSferics": 10 + }, { + "time": "2024-02-18T00:00Z", + "midday10MWindSpeed": 4.07, + "midnight10MWindSpeed": 2.84, + "midday10MWindDirection": 292, + "midnight10MWindDirection": 281, + "midday10MWindGust": 7.55, + "midnight10MWindGust": 6.36, + "middayVisibility": 25425, + "midnightVisibility": 17183, + "middayRelativeHumidity": 86.19, + "midnightRelativeHumidity": 94.68, + "middayMslp": 102625, + "midnightMslp": 103041, + "maxUvIndex": 1, + "daySignificantWeatherCode": 12, + "nightSignificantWeatherCode": 7, + "dayMaxScreenTemperature": 13.89, + "nightMinScreenTemperature": 7.15, + "dayUpperBoundMaxTemp": 14.73, + "nightUpperBoundMinTemp": 9.16, + "dayLowerBoundMaxTemp": 12.4, + "nightLowerBoundMinTemp": 5.31, + "dayMaxFeelsLikeTemp": 11.75, + "nightMinFeelsLikeTemp": 5.34, + "dayUpperBoundMaxFeelsLikeTemp": 13.47, + "nightUpperBoundMinFeelsLikeTemp": 7.12, + "dayLowerBoundMaxFeelsLikeTemp": 10.8, + "nightLowerBoundMinFeelsLikeTemp": 5.06, + "dayProbabilityOfPrecipitation": 55, + "nightProbabilityOfPrecipitation": 9, + "dayProbabilityOfSnow": 0, + "nightProbabilityOfSnow": 0, + "dayProbabilityOfHeavySnow": 0, + "nightProbabilityOfHeavySnow": 0, + "dayProbabilityOfRain": 55, + "nightProbabilityOfRain": 9, + "dayProbabilityOfHeavyRain": 36, + "nightProbabilityOfHeavyRain": 2, + "dayProbabilityOfHail": 4, + "nightProbabilityOfHail": 0, + "dayProbabilityOfSferics": 3, + "nightProbabilityOfSferics": 0 + }, { + "time": "2024-02-19T00:00Z", + "midday10MWindSpeed": 5.47, + "midnight10MWindSpeed": 2.59, + "midday10MWindDirection": 276, + "midnight10MWindDirection": 296, + "midday10MWindGust": 10.47, + "midnight10MWindGust": 4.49, + "middayVisibility": 22511, + "midnightVisibility": 23913, + "middayRelativeHumidity": 82.45, + "midnightRelativeHumidity": 89.64, + "middayMslp": 102910, + "midnightMslp": 103121, + "maxUvIndex": 1, + "daySignificantWeatherCode": 3, + "nightSignificantWeatherCode": 7, + "dayMaxScreenTemperature": 12.12, + "nightMinScreenTemperature": 4.02, + "dayUpperBoundMaxTemp": 13.27, + "nightUpperBoundMinTemp": 8.61, + "dayLowerBoundMaxTemp": 10.18, + "nightLowerBoundMinTemp": 1.52, + "dayMaxFeelsLikeTemp": 9.55, + "nightMinFeelsLikeTemp": 2.36, + "dayUpperBoundMaxFeelsLikeTemp": 11.33, + "nightUpperBoundMinFeelsLikeTemp": 6.08, + "dayLowerBoundMaxFeelsLikeTemp": 7.8, + "nightLowerBoundMinFeelsLikeTemp": 1.06, + "dayProbabilityOfPrecipitation": 49, + "nightProbabilityOfPrecipitation": 5, + "dayProbabilityOfSnow": 0, + "nightProbabilityOfSnow": 1, + "dayProbabilityOfHeavySnow": 0, + "nightProbabilityOfHeavySnow": 0, + "dayProbabilityOfRain": 49, + "nightProbabilityOfRain": 5, + "dayProbabilityOfHeavyRain": 30, + "nightProbabilityOfHeavyRain": 0, + "dayProbabilityOfHail": 3, + "nightProbabilityOfHail": 0, + "dayProbabilityOfSferics": 4, + "nightProbabilityOfSferics": 0 + }, { + "time": "2024-02-20T00:00Z", + "midday10MWindSpeed": 7.16, + "midnight10MWindSpeed": 6.4, + "midday10MWindDirection": 230, + "midnight10MWindDirection": 232, + "midday10MWindGust": 13.73, + "midnight10MWindGust": 11.87, + "middayVisibility": 24484, + "midnightVisibility": 15050, + "middayRelativeHumidity": 81.96, + "midnightRelativeHumidity": 92.53, + "middayMslp": 102756, + "midnightMslp": 102018, + "maxUvIndex": 1, + "daySignificantWeatherCode": 7, + "nightSignificantWeatherCode": 12, + "dayMaxScreenTemperature": 10.71, + "nightMinScreenTemperature": 8.75, + "dayUpperBoundMaxTemp": 12.07, + "nightUpperBoundMinTemp": 10.16, + "dayLowerBoundMaxTemp": 9.37, + "nightLowerBoundMinTemp": 5.87, + "dayMaxFeelsLikeTemp": 7.33, + "nightMinFeelsLikeTemp": 6.1, + "dayUpperBoundMaxFeelsLikeTemp": 8.37, + "nightUpperBoundMinFeelsLikeTemp": 7.54, + "dayLowerBoundMaxFeelsLikeTemp": 6.38, + "nightLowerBoundMinFeelsLikeTemp": 4.52, + "dayProbabilityOfPrecipitation": 13, + "nightProbabilityOfPrecipitation": 84, + "dayProbabilityOfSnow": 0, + "nightProbabilityOfSnow": 0, + "dayProbabilityOfHeavySnow": 0, + "nightProbabilityOfHeavySnow": 0, + "dayProbabilityOfRain": 13, + "nightProbabilityOfRain": 84, + "dayProbabilityOfHeavyRain": 3, + "nightProbabilityOfHeavyRain": 79, + "dayProbabilityOfHail": 0, + "nightProbabilityOfHail": 15, + "dayProbabilityOfSferics": 0, + "nightProbabilityOfSferics": 8 + }, { + "time": "2024-02-21T00:00Z", + "midday10MWindSpeed": 8.38, + "midnight10MWindSpeed": 6.06, + "midday10MWindDirection": 206, + "midnight10MWindDirection": 228, + "midday10MWindGust": 15.83, + "midnight10MWindGust": 11.11, + "middayVisibility": 7351, + "midnightVisibility": 11329, + "middayRelativeHumidity": 90.56, + "midnightRelativeHumidity": 93.31, + "middayMslp": 100933, + "midnightMslp": 99967, + "maxUvIndex": 1, + "daySignificantWeatherCode": 15, + "nightSignificantWeatherCode": 12, + "dayMaxScreenTemperature": 10.79, + "nightMinScreenTemperature": 8.58, + "dayUpperBoundMaxTemp": 13.6, + "nightUpperBoundMinTemp": 11.01, + "dayLowerBoundMaxTemp": 9.05, + "nightLowerBoundMinTemp": 4.97, + "dayMaxFeelsLikeTemp": 6.95, + "nightMinFeelsLikeTemp": 6.72, + "dayUpperBoundMaxFeelsLikeTemp": 10.54, + "nightUpperBoundMinFeelsLikeTemp": 7.88, + "dayLowerBoundMaxFeelsLikeTemp": 5.69, + "nightLowerBoundMinFeelsLikeTemp": 1.79, + "dayProbabilityOfPrecipitation": 86, + "nightProbabilityOfPrecipitation": 78, + "dayProbabilityOfSnow": 0, + "nightProbabilityOfSnow": 0, + "dayProbabilityOfHeavySnow": 0, + "nightProbabilityOfHeavySnow": 0, + "dayProbabilityOfRain": 86, + "nightProbabilityOfRain": 78, + "dayProbabilityOfHeavyRain": 82, + "nightProbabilityOfHeavyRain": 73, + "dayProbabilityOfHail": 16, + "nightProbabilityOfHail": 14, + "dayProbabilityOfSferics": 8, + "nightProbabilityOfSferics": 7 + }, { + "time": "2024-02-22T00:00Z", + "midday10MWindSpeed": 8.32, + "midnight10MWindSpeed": 6.61, + "midday10MWindDirection": 215, + "midnight10MWindDirection": 245, + "midday10MWindGust": 15.92, + "midnight10MWindGust": 11.58, + "middayVisibility": 13595, + "midnightVisibility": 25279, + "middayRelativeHumidity": 87.24, + "midnightRelativeHumidity": 77.46, + "middayMslp": 98762, + "midnightMslp": 98742, + "maxUvIndex": 1, + "daySignificantWeatherCode": 14, + "nightSignificantWeatherCode": 2, + "dayMaxScreenTemperature": 10.99, + "nightMinScreenTemperature": 4.47, + "dayUpperBoundMaxTemp": 12.73, + "nightUpperBoundMinTemp": 6.6, + "dayLowerBoundMaxTemp": 8.4, + "nightLowerBoundMinTemp": 2.22, + "dayMaxFeelsLikeTemp": 6.86, + "nightMinFeelsLikeTemp": 1.2, + "dayUpperBoundMaxFeelsLikeTemp": 9.46, + "nightUpperBoundMinFeelsLikeTemp": 2.54, + "dayLowerBoundMaxFeelsLikeTemp": 5.2, + "nightLowerBoundMinFeelsLikeTemp": -0.95, + "dayProbabilityOfPrecipitation": 71, + "nightProbabilityOfPrecipitation": 44, + "dayProbabilityOfSnow": 0, + "nightProbabilityOfSnow": 1, + "dayProbabilityOfHeavySnow": 0, + "nightProbabilityOfHeavySnow": 0, + "dayProbabilityOfRain": 71, + "nightProbabilityOfRain": 44, + "dayProbabilityOfHeavyRain": 67, + "nightProbabilityOfHeavyRain": 32, + "dayProbabilityOfHail": 13, + "nightProbabilityOfHail": 4, + "dayProbabilityOfSferics": 12, + "nightProbabilityOfSferics": 6 + }, { + "time": "2024-02-23T00:00Z", + "midday10MWindSpeed": 7.11, + "midnight10MWindSpeed": 4.2, + "midday10MWindDirection": 231, + "midnight10MWindDirection": 232, + "midday10MWindGust": 13.38, + "midnight10MWindGust": 7.16, + "middayVisibility": 23049, + "midnightVisibility": 22325, + "middayRelativeHumidity": 73.25, + "midnightRelativeHumidity": 85.83, + "middayMslp": 98974, + "midnightMslp": 99364, + "maxUvIndex": 2, + "daySignificantWeatherCode": 10, + "nightSignificantWeatherCode": 2, + "dayMaxScreenTemperature": 8.57, + "nightMinScreenTemperature": 3.1, + "dayUpperBoundMaxTemp": 10.67, + "nightUpperBoundMinTemp": 6.84, + "dayLowerBoundMaxTemp": 6.67, + "nightLowerBoundMinTemp": -0.72, + "dayMaxFeelsLikeTemp": 4.42, + "nightMinFeelsLikeTemp": 0.74, + "dayUpperBoundMaxFeelsLikeTemp": 7.38, + "nightUpperBoundMinFeelsLikeTemp": 4.01, + "dayLowerBoundMaxFeelsLikeTemp": 3.89, + "nightLowerBoundMinFeelsLikeTemp": -2.25, + "dayProbabilityOfPrecipitation": 52, + "nightProbabilityOfPrecipitation": 9, + "dayProbabilityOfSnow": 0, + "nightProbabilityOfSnow": 1, + "dayProbabilityOfHeavySnow": 0, + "nightProbabilityOfHeavySnow": 1, + "dayProbabilityOfRain": 52, + "nightProbabilityOfRain": 9, + "dayProbabilityOfHeavyRain": 48, + "nightProbabilityOfHeavyRain": 4, + "dayProbabilityOfHail": 10, + "nightProbabilityOfHail": 1, + "dayProbabilityOfSferics": 11, + "nightProbabilityOfSferics": 1 + }] + } + }] +} diff --git a/tests/reference_data/hourly_api_data.json b/tests/reference_data/hourly_api_data.json new file mode 100644 index 0000000..2aab58b --- /dev/null +++ b/tests/reference_data/hourly_api_data.json @@ -0,0 +1,1188 @@ +{ + "type": "FeatureCollection", + "parameters": [{ + "totalSnowAmount": { + "type": "Parameter", + "description": "Total Snow Amount Over Previous Hour", + "unit": { + "label": "millimetres", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "mm" + } + } + }, + "screenTemperature": { + "type": "Parameter", + "description": "Screen Air Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "visibility": { + "type": "Parameter", + "description": "Visibility", + "unit": { + "label": "metres", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m" + } + } + }, + "windDirectionFrom10m": { + "type": "Parameter", + "description": "10m Wind From Direction", + "unit": { + "label": "degrees", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "deg" + } + } + }, + "precipitationRate": { + "type": "Parameter", + "description": "Precipitation Rate", + "unit": { + "label": "millimetres per hour", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "mm/h" + } + } + }, + "maxScreenAirTemp": { + "type": "Parameter", + "description": "Maximum Screen Air Temperature Over Previous Hour", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "feelsLikeTemperature": { + "type": "Parameter", + "description": "Feels Like Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "screenDewPointTemperature": { + "type": "Parameter", + "description": "Screen Dew Point Temperature", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "screenRelativeHumidity": { + "type": "Parameter", + "description": "Screen Relative Humidity", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "windSpeed10m": { + "type": "Parameter", + "description": "10m Wind Speed", + "unit": { + "label": "metres per second", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m/s" + } + } + }, + "probOfPrecipitation": { + "type": "Parameter", + "description": "Probability of Precipitation", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "max10mWindGust": { + "type": "Parameter", + "description": "Maximum 10m Wind Gust Speed Over Previous Hour", + "unit": { + "label": "metres per second", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m/s" + } + } + }, + "significantWeatherCode": { + "type": "Parameter", + "description": "Significant Weather Code", + "unit": { + "label": "dimensionless", + "symbol": { + "value": "https://metoffice.apiconnect.ibmcloud.com/metoffice/production/", + "type": "1" + } + } + }, + "minScreenAirTemp": { + "type": "Parameter", + "description": "Minimum Screen Air Temperature Over Previous Hour", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "totalPrecipAmount": { + "type": "Parameter", + "description": "Total Precipitation Amount Over Previous Hour", + "unit": { + "label": "millimetres", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "mm" + } + } + }, + "mslp": { + "type": "Parameter", + "description": "Mean Sea Level Pressure", + "unit": { + "label": "pascals", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Pa" + } + } + }, + "windGustSpeed10m": { + "type": "Parameter", + "description": "10m Wind Gust Speed", + "unit": { + "label": "metres per second", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m/s" + } + } + }, + "uvIndex": { + "type": "Parameter", + "description": "UV Index", + "unit": { + "label": "dimensionless", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "1" + } + } + } + }], + "features": [{ + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [0.0154, 50.9992, 37.0] + }, + "properties": { + "location": { + "name": "Sheffield Park" + }, + "requestPointDistance": 1081.5349, + "modelRunDate": "2024-02-15T19:00Z", + "timeSeries": [{ + "time": "2024-02-15T19:00Z", + "screenTemperature": 11.0, + "maxScreenAirTemp": 11.55, + "minScreenAirTemp": 10.98, + "screenDewPointTemperature": 8.94, + "feelsLikeTemperature": 10.87, + "windSpeed10m": 1.18, + "windDirectionFrom10m": 180, + "windGustSpeed10m": 6.69, + "max10mWindGust": 8.92, + "visibility": 19174, + "screenRelativeHumidity": 86.99, + "mslp": 100660, + "uvIndex": 0, + "significantWeatherCode": 2, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 4 + }, { + "time": "2024-02-15T20:00Z", + "screenTemperature": 11.38, + "maxScreenAirTemp": 11.38, + "minScreenAirTemp": 11.0, + "screenDewPointTemperature": 9.55, + "feelsLikeTemperature": 10.96, + "windSpeed10m": 1.68, + "windDirectionFrom10m": 96, + "windGustSpeed10m": 3.55, + "max10mWindGust": 5.64, + "visibility": 17279, + "screenRelativeHumidity": 88.39, + "mslp": 100653, + "uvIndex": 0, + "significantWeatherCode": 2, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 1 + }, { + "time": "2024-02-15T21:00Z", + "screenTemperature": 11.49, + "maxScreenAirTemp": 11.5, + "minScreenAirTemp": 11.38, + "screenDewPointTemperature": 9.97, + "feelsLikeTemperature": 11.21, + "windSpeed10m": 1.52, + "windDirectionFrom10m": 79, + "windGustSpeed10m": 3.34, + "max10mWindGust": 5.42, + "visibility": 14995, + "screenRelativeHumidity": 90.31, + "mslp": 100714, + "uvIndex": 0, + "significantWeatherCode": 7, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 4 + }, { + "time": "2024-02-15T22:00Z", + "screenTemperature": 11.18, + "maxScreenAirTemp": 11.49, + "minScreenAirTemp": 11.13, + "screenDewPointTemperature": 9.93, + "feelsLikeTemperature": 10.47, + "windSpeed10m": 2.05, + "windDirectionFrom10m": 112, + "windGustSpeed10m": 4.34, + "max10mWindGust": 5.18, + "visibility": 13525, + "screenRelativeHumidity": 91.97, + "mslp": 100668, + "uvIndex": 0, + "significantWeatherCode": 2, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 0 + }, { + "time": "2024-02-15T23:00Z", + "screenTemperature": 10.84, + "maxScreenAirTemp": 11.18, + "minScreenAirTemp": 10.78, + "screenDewPointTemperature": 9.7, + "feelsLikeTemperature": 10.15, + "windSpeed10m": 1.96, + "windDirectionFrom10m": 108, + "windGustSpeed10m": 4.74, + "max10mWindGust": 5.33, + "visibility": 12925, + "screenRelativeHumidity": 92.64, + "mslp": 100680, + "uvIndex": 0, + "significantWeatherCode": 2, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 1 + }, { + "time": "2024-02-16T00:00Z", + "screenTemperature": 10.53, + "maxScreenAirTemp": 10.84, + "minScreenAirTemp": 10.52, + "screenDewPointTemperature": 9.55, + "feelsLikeTemperature": 10.1, + "windSpeed10m": 1.47, + "windDirectionFrom10m": 108, + "windGustSpeed10m": 5.09, + "max10mWindGust": 5.43, + "visibility": 12220, + "screenRelativeHumidity": 93.66, + "mslp": 100650, + "uvIndex": 0, + "significantWeatherCode": 2, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 1 + }, { + "time": "2024-02-16T01:00Z", + "screenTemperature": 10.72, + "maxScreenAirTemp": 10.78, + "minScreenAirTemp": 10.53, + "screenDewPointTemperature": 9.62, + "feelsLikeTemperature": 10.31, + "windSpeed10m": 1.48, + "windDirectionFrom10m": 135, + "windGustSpeed10m": 5.05, + "max10mWindGust": 5.65, + "visibility": 14094, + "screenRelativeHumidity": 92.91, + "mslp": 100660, + "uvIndex": 0, + "significantWeatherCode": 7, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 5 + }, { + "time": "2024-02-16T02:00Z", + "screenTemperature": 10.73, + "maxScreenAirTemp": 10.82, + "minScreenAirTemp": 10.65, + "screenDewPointTemperature": 9.75, + "feelsLikeTemperature": 10.51, + "windSpeed10m": 1.17, + "windDirectionFrom10m": 185, + "windGustSpeed10m": 5.33, + "max10mWindGust": 5.73, + "visibility": 13709, + "screenRelativeHumidity": 93.64, + "mslp": 100688, + "uvIndex": 0, + "significantWeatherCode": 2, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 2 + }, { + "time": "2024-02-16T03:00Z", + "screenTemperature": 10.93, + "maxScreenAirTemp": 10.94, + "minScreenAirTemp": 10.73, + "screenDewPointTemperature": 9.92, + "feelsLikeTemperature": 10.38, + "windSpeed10m": 1.61, + "windDirectionFrom10m": 240, + "windGustSpeed10m": 5.97, + "max10mWindGust": 6.16, + "visibility": 13864, + "screenRelativeHumidity": 93.55, + "mslp": 100744, + "uvIndex": 0, + "significantWeatherCode": 7, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 6 + }, { + "time": "2024-02-16T04:00Z", + "screenTemperature": 11.03, + "maxScreenAirTemp": 11.05, + "minScreenAirTemp": 10.93, + "screenDewPointTemperature": 10.21, + "feelsLikeTemperature": 9.95, + "windSpeed10m": 2.59, + "windDirectionFrom10m": 279, + "windGustSpeed10m": 7.23, + "max10mWindGust": 7.35, + "visibility": 11033, + "screenRelativeHumidity": 94.71, + "mslp": 100806, + "uvIndex": 0, + "significantWeatherCode": 8, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 13 + }, { + "time": "2024-02-16T05:00Z", + "screenTemperature": 11.27, + "maxScreenAirTemp": 11.28, + "minScreenAirTemp": 11.03, + "screenDewPointTemperature": 10.54, + "feelsLikeTemperature": 9.74, + "windSpeed10m": 3.47, + "windDirectionFrom10m": 288, + "windGustSpeed10m": 8.23, + "max10mWindGust": 8.45, + "visibility": 14201, + "screenRelativeHumidity": 95.25, + "mslp": 100909, + "uvIndex": 0, + "significantWeatherCode": 12, + "precipitationRate": 0.23, + "totalPrecipAmount": 0.08, + "totalSnowAmount": 0, + "probOfPrecipitation": 43 + }, { + "time": "2024-02-16T06:00Z", + "screenTemperature": 10.92, + "maxScreenAirTemp": 11.13, + "minScreenAirTemp": 10.9, + "screenDewPointTemperature": 10.04, + "feelsLikeTemperature": 9.49, + "windSpeed10m": 3.18, + "windDirectionFrom10m": 283, + "windGustSpeed10m": 7.71, + "max10mWindGust": 8.27, + "visibility": 25090, + "screenRelativeHumidity": 94.41, + "mslp": 101042, + "uvIndex": 0, + "significantWeatherCode": 7, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 7 + }, { + "time": "2024-02-16T07:00Z", + "screenTemperature": 10.62, + "maxScreenAirTemp": 10.92, + "minScreenAirTemp": 10.61, + "screenDewPointTemperature": 9.43, + "feelsLikeTemperature": 8.87, + "windSpeed10m": 3.68, + "windDirectionFrom10m": 279, + "windGustSpeed10m": 8.1, + "max10mWindGust": 8.55, + "visibility": 21863, + "screenRelativeHumidity": 92.4, + "mslp": 101186, + "uvIndex": 0, + "significantWeatherCode": 7, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 7 + }, { + "time": "2024-02-16T08:00Z", + "screenTemperature": 10.3, + "maxScreenAirTemp": 10.62, + "minScreenAirTemp": 10.27, + "screenDewPointTemperature": 8.77, + "feelsLikeTemperature": 8.27, + "windSpeed10m": 4.15, + "windDirectionFrom10m": 278, + "windGustSpeed10m": 8.77, + "max10mWindGust": 8.86, + "visibility": 17499, + "screenRelativeHumidity": 90.34, + "mslp": 101326, + "uvIndex": 1, + "significantWeatherCode": 3, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 1 + }, { + "time": "2024-02-16T09:00Z", + "screenTemperature": 10.46, + "maxScreenAirTemp": 10.47, + "minScreenAirTemp": 10.3, + "screenDewPointTemperature": 8.47, + "feelsLikeTemperature": 8.26, + "windSpeed10m": 4.59, + "windDirectionFrom10m": 279, + "windGustSpeed10m": 8.75, + "max10mWindGust": 8.75, + "visibility": 16833, + "screenRelativeHumidity": 87.56, + "mslp": 101456, + "uvIndex": 1, + "significantWeatherCode": 3, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 0 + }, { + "time": "2024-02-16T10:00Z", + "screenTemperature": 11.07, + "maxScreenAirTemp": 11.09, + "minScreenAirTemp": 10.46, + "screenDewPointTemperature": 8.27, + "feelsLikeTemperature": 8.92, + "windSpeed10m": 4.68, + "windDirectionFrom10m": 276, + "windGustSpeed10m": 8.7, + "max10mWindGust": 8.7, + "visibility": 20678, + "screenRelativeHumidity": 82.98, + "mslp": 101557, + "uvIndex": 1, + "significantWeatherCode": 3, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 0 + }, { + "time": "2024-02-16T11:00Z", + "screenTemperature": 11.71, + "maxScreenAirTemp": 11.74, + "minScreenAirTemp": 11.07, + "screenDewPointTemperature": 7.9, + "feelsLikeTemperature": 9.42, + "windSpeed10m": 5.16, + "windDirectionFrom10m": 273, + "windGustSpeed10m": 9.42, + "max10mWindGust": 9.42, + "visibility": 30259, + "screenRelativeHumidity": 77.53, + "mslp": 101647, + "uvIndex": 1, + "significantWeatherCode": 1, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 0 + }, { + "time": "2024-02-16T12:00Z", + "screenTemperature": 12.37, + "maxScreenAirTemp": 12.39, + "minScreenAirTemp": 11.71, + "screenDewPointTemperature": 7.67, + "feelsLikeTemperature": 10.03, + "windSpeed10m": 5.39, + "windDirectionFrom10m": 275, + "windGustSpeed10m": 9.94, + "max10mWindGust": 9.94, + "visibility": 34462, + "screenRelativeHumidity": 73.05, + "mslp": 101700, + "uvIndex": 1, + "significantWeatherCode": 3, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 0 + }, { + "time": "2024-02-16T13:00Z", + "screenTemperature": 12.91, + "maxScreenAirTemp": 12.94, + "minScreenAirTemp": 12.37, + "screenDewPointTemperature": 7.75, + "feelsLikeTemperature": 10.76, + "windSpeed10m": 4.97, + "windDirectionFrom10m": 271, + "windGustSpeed10m": 9.39, + "max10mWindGust": 9.65, + "visibility": 37584, + "screenRelativeHumidity": 70.9, + "mslp": 101750, + "uvIndex": 1, + "significantWeatherCode": 3, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 0 + }, { + "time": "2024-02-16T14:00Z", + "screenTemperature": 12.9, + "maxScreenAirTemp": 13.04, + "minScreenAirTemp": 12.69, + "screenDewPointTemperature": 7.06, + "feelsLikeTemperature": 10.54, + "windSpeed10m": 5.39, + "windDirectionFrom10m": 270, + "windGustSpeed10m": 10.16, + "max10mWindGust": 10.16, + "visibility": 38254, + "screenRelativeHumidity": 67.76, + "mslp": 101813, + "uvIndex": 1, + "significantWeatherCode": 3, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 1 + }, { + "time": "2024-02-16T15:00Z", + "screenTemperature": 12.79, + "maxScreenAirTemp": 12.9, + "minScreenAirTemp": 12.72, + "screenDewPointTemperature": 6.92, + "feelsLikeTemperature": 10.69, + "windSpeed10m": 4.78, + "windDirectionFrom10m": 267, + "windGustSpeed10m": 9.36, + "max10mWindGust": 9.96, + "visibility": 38521, + "screenRelativeHumidity": 67.6, + "mslp": 101887, + "uvIndex": 1, + "significantWeatherCode": 3, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 1 + }, { + "time": "2024-02-16T16:00Z", + "screenTemperature": 12.06, + "maxScreenAirTemp": 12.79, + "minScreenAirTemp": 12.04, + "screenDewPointTemperature": 6.8, + "feelsLikeTemperature": 10.23, + "windSpeed10m": 4.07, + "windDirectionFrom10m": 266, + "windGustSpeed10m": 8.66, + "max10mWindGust": 9.27, + "visibility": 37284, + "screenRelativeHumidity": 70.37, + "mslp": 101980, + "uvIndex": 1, + "significantWeatherCode": 1, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 0 + }, { + "time": "2024-02-16T17:00Z", + "screenTemperature": 10.8, + "maxScreenAirTemp": 12.06, + "minScreenAirTemp": 10.77, + "screenDewPointTemperature": 6.94, + "feelsLikeTemperature": 9.36, + "windSpeed10m": 3.16, + "windDirectionFrom10m": 261, + "windGustSpeed10m": 8.0, + "max10mWindGust": 8.43, + "visibility": 33668, + "screenRelativeHumidity": 77.2, + "mslp": 102066, + "uvIndex": 1, + "significantWeatherCode": 1, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 0 + }, { + "time": "2024-02-16T18:00Z", + "screenTemperature": 9.6, + "maxScreenAirTemp": 10.8, + "minScreenAirTemp": 9.58, + "screenDewPointTemperature": 6.7, + "feelsLikeTemperature": 8.37, + "windSpeed10m": 2.58, + "windDirectionFrom10m": 257, + "windGustSpeed10m": 7.4, + "max10mWindGust": 8.52, + "visibility": 29126, + "screenRelativeHumidity": 82.34, + "mslp": 102166, + "uvIndex": 0, + "significantWeatherCode": 0, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 0 + }, { + "time": "2024-02-16T19:00Z", + "screenTemperature": 8.94, + "maxScreenAirTemp": 9.6, + "minScreenAirTemp": 8.9, + "screenDewPointTemperature": 6.8, + "feelsLikeTemperature": 7.68, + "windSpeed10m": 2.46, + "windDirectionFrom10m": 255, + "windGustSpeed10m": 7.35, + "max10mWindGust": 8.02, + "visibility": 22767, + "screenRelativeHumidity": 86.81, + "mslp": 102246, + "uvIndex": 0, + "significantWeatherCode": 0, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 0 + }, { + "time": "2024-02-16T20:00Z", + "screenTemperature": 8.42, + "maxScreenAirTemp": 8.94, + "minScreenAirTemp": 8.41, + "screenDewPointTemperature": 6.3, + "feelsLikeTemperature": 7.06, + "windSpeed10m": 2.45, + "windDirectionFrom10m": 263, + "windGustSpeed10m": 7.47, + "max10mWindGust": 7.99, + "visibility": 21802, + "screenRelativeHumidity": 86.73, + "mslp": 102329, + "uvIndex": 0, + "significantWeatherCode": 0, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 0 + }, { + "time": "2024-02-16T21:00Z", + "screenTemperature": 7.87, + "maxScreenAirTemp": 8.42, + "minScreenAirTemp": 7.86, + "screenDewPointTemperature": 6.11, + "feelsLikeTemperature": 6.51, + "windSpeed10m": 2.33, + "windDirectionFrom10m": 267, + "windGustSpeed10m": 7.0, + "max10mWindGust": 7.86, + "visibility": 21303, + "screenRelativeHumidity": 88.8, + "mslp": 102406, + "uvIndex": 0, + "significantWeatherCode": 0, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 0 + }, { + "time": "2024-02-16T22:00Z", + "screenTemperature": 7.48, + "maxScreenAirTemp": 7.87, + "minScreenAirTemp": 7.46, + "screenDewPointTemperature": 5.89, + "feelsLikeTemperature": 6.06, + "windSpeed10m": 2.32, + "windDirectionFrom10m": 274, + "windGustSpeed10m": 6.96, + "max10mWindGust": 7.77, + "visibility": 20523, + "screenRelativeHumidity": 89.86, + "mslp": 102479, + "uvIndex": 0, + "significantWeatherCode": 0, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 1 + }, { + "time": "2024-02-16T23:00Z", + "screenTemperature": 7.04, + "maxScreenAirTemp": 7.48, + "minScreenAirTemp": 7.01, + "screenDewPointTemperature": 5.66, + "feelsLikeTemperature": 5.68, + "windSpeed10m": 2.18, + "windDirectionFrom10m": 280, + "windGustSpeed10m": 6.69, + "max10mWindGust": 7.53, + "visibility": 20867, + "screenRelativeHumidity": 91.09, + "mslp": 102545, + "uvIndex": 0, + "significantWeatherCode": 0, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 1 + }, { + "time": "2024-02-17T00:00Z", + "screenTemperature": 6.7, + "maxScreenAirTemp": 7.04, + "minScreenAirTemp": 6.67, + "screenDewPointTemperature": 5.59, + "feelsLikeTemperature": 5.32, + "windSpeed10m": 2.11, + "windDirectionFrom10m": 281, + "windGustSpeed10m": 6.34, + "max10mWindGust": 7.01, + "visibility": 20045, + "screenRelativeHumidity": 92.83, + "mslp": 102614, + "uvIndex": 0, + "significantWeatherCode": 0, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 1 + }, { + "time": "2024-02-17T01:00Z", + "screenTemperature": 6.26, + "maxScreenAirTemp": 6.7, + "minScreenAirTemp": 6.21, + "screenDewPointTemperature": 5.34, + "feelsLikeTemperature": 4.85, + "windSpeed10m": 2.05, + "windDirectionFrom10m": 283, + "windGustSpeed10m": 6.13, + "max10mWindGust": 6.88, + "visibility": 18378, + "screenRelativeHumidity": 94.11, + "mslp": 102657, + "uvIndex": 0, + "significantWeatherCode": 0, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 1 + }, { + "time": "2024-02-17T02:00Z", + "screenTemperature": 5.72, + "maxScreenAirTemp": 6.26, + "minScreenAirTemp": 5.66, + "screenDewPointTemperature": 5.02, + "feelsLikeTemperature": 4.45, + "windSpeed10m": 1.73, + "windDirectionFrom10m": 282, + "windGustSpeed10m": 5.73, + "max10mWindGust": 6.69, + "visibility": 14463, + "screenRelativeHumidity": 95.57, + "mslp": 102697, + "uvIndex": 0, + "significantWeatherCode": 0, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 2 + }, { + "time": "2024-02-17T03:00Z", + "screenTemperature": 5.22, + "maxScreenAirTemp": 5.72, + "minScreenAirTemp": 5.17, + "screenDewPointTemperature": 4.68, + "feelsLikeTemperature": 4.03, + "windSpeed10m": 1.37, + "windDirectionFrom10m": 267, + "windGustSpeed10m": 5.08, + "max10mWindGust": 6.2, + "visibility": 12881, + "screenRelativeHumidity": 96.48, + "mslp": 102729, + "uvIndex": 0, + "significantWeatherCode": 0, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 3 + }, { + "time": "2024-02-17T04:00Z", + "screenTemperature": 5.44, + "maxScreenAirTemp": 5.46, + "minScreenAirTemp": 5.22, + "screenDewPointTemperature": 4.9, + "feelsLikeTemperature": 4.28, + "windSpeed10m": 1.22, + "windDirectionFrom10m": 234, + "windGustSpeed10m": 4.72, + "max10mWindGust": 5.38, + "visibility": 13816, + "screenRelativeHumidity": 96.53, + "mslp": 102767, + "uvIndex": 0, + "significantWeatherCode": 2, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 2 + }, { + "time": "2024-02-17T05:00Z", + "screenTemperature": 5.67, + "maxScreenAirTemp": 5.73, + "minScreenAirTemp": 5.44, + "screenDewPointTemperature": 5.3, + "feelsLikeTemperature": 4.55, + "windSpeed10m": 1.28, + "windDirectionFrom10m": 182, + "windGustSpeed10m": 3.99, + "max10mWindGust": 4.95, + "visibility": 4000, + "screenRelativeHumidity": 97.73, + "mslp": 102819, + "uvIndex": 0, + "significantWeatherCode": 7, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 5 + }, { + "time": "2024-02-17T06:00Z", + "screenTemperature": 6.01, + "maxScreenAirTemp": 6.24, + "minScreenAirTemp": 5.67, + "screenDewPointTemperature": 5.66, + "feelsLikeTemperature": 4.95, + "windSpeed10m": 1.51, + "windDirectionFrom10m": 156, + "windGustSpeed10m": 3.65, + "max10mWindGust": 3.87, + "visibility": 999, + "screenRelativeHumidity": 97.85, + "mslp": 102866, + "uvIndex": 0, + "significantWeatherCode": 6, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 17 + }, { + "time": "2024-02-17T07:00Z", + "screenTemperature": 6.74, + "maxScreenAirTemp": 6.75, + "minScreenAirTemp": 6.01, + "screenDewPointTemperature": 6.32, + "feelsLikeTemperature": 5.73, + "windSpeed10m": 1.63, + "windDirectionFrom10m": 161, + "windGustSpeed10m": 4.12, + "max10mWindGust": 4.12, + "visibility": 4213, + "screenRelativeHumidity": 97.43, + "mslp": 102918, + "uvIndex": 0, + "significantWeatherCode": 7, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 8 + }, { + "time": "2024-02-17T08:00Z", + "screenTemperature": 7.5, + "maxScreenAirTemp": 7.54, + "minScreenAirTemp": 6.74, + "screenDewPointTemperature": 7.09, + "feelsLikeTemperature": 6.54, + "windSpeed10m": 1.8, + "windDirectionFrom10m": 171, + "windGustSpeed10m": 4.56, + "max10mWindGust": 4.56, + "visibility": 5736, + "screenRelativeHumidity": 97.44, + "mslp": 102968, + "uvIndex": 1, + "significantWeatherCode": 7, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 14 + }, { + "time": "2024-02-17T09:00Z", + "screenTemperature": 8.46, + "maxScreenAirTemp": 8.48, + "minScreenAirTemp": 7.5, + "screenDewPointTemperature": 7.89, + "feelsLikeTemperature": 7.54, + "windSpeed10m": 1.9, + "windDirectionFrom10m": 176, + "windGustSpeed10m": 5.23, + "max10mWindGust": 5.23, + "visibility": 5476, + "screenRelativeHumidity": 96.48, + "mslp": 103015, + "uvIndex": 1, + "significantWeatherCode": 7, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 15 + }, { + "time": "2024-02-17T10:00Z", + "screenTemperature": 9.31, + "maxScreenAirTemp": 9.32, + "minScreenAirTemp": 8.46, + "screenDewPointTemperature": 8.71, + "feelsLikeTemperature": 8.16, + "windSpeed10m": 2.49, + "windDirectionFrom10m": 194, + "windGustSpeed10m": 5.87, + "max10mWindGust": 6.19, + "visibility": 5671, + "screenRelativeHumidity": 96.3, + "mslp": 103039, + "uvIndex": 1, + "significantWeatherCode": 8, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 13 + }, { + "time": "2024-02-17T11:00Z", + "screenTemperature": 10.23, + "maxScreenAirTemp": 10.23, + "minScreenAirTemp": 9.31, + "screenDewPointTemperature": 9.34, + "feelsLikeTemperature": 8.78, + "windSpeed10m": 3.08, + "windDirectionFrom10m": 202, + "windGustSpeed10m": 6.31, + "max10mWindGust": 6.31, + "visibility": 11241, + "screenRelativeHumidity": 94.6, + "mslp": 103069, + "uvIndex": 1, + "significantWeatherCode": 8, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 13 + }, { + "time": "2024-02-17T12:00Z", + "screenTemperature": 10.79, + "maxScreenAirTemp": 10.82, + "minScreenAirTemp": 10.23, + "screenDewPointTemperature": 9.48, + "feelsLikeTemperature": 9.18, + "windSpeed10m": 3.5, + "windDirectionFrom10m": 203, + "windGustSpeed10m": 6.77, + "max10mWindGust": 6.77, + "visibility": 13088, + "screenRelativeHumidity": 92.01, + "mslp": 103068, + "uvIndex": 1, + "significantWeatherCode": 8, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 13 + }, { + "time": "2024-02-17T13:00Z", + "screenTemperature": 10.84, + "maxScreenAirTemp": 10.84, + "minScreenAirTemp": 10.79, + "screenDewPointTemperature": 9.5, + "feelsLikeTemperature": 9.17, + "windSpeed10m": 3.63, + "windDirectionFrom10m": 202, + "windGustSpeed10m": 7.09, + "max10mWindGust": 7.09, + "visibility": 13756, + "screenRelativeHumidity": 91.77, + "mslp": 103050, + "uvIndex": 1, + "significantWeatherCode": 8, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 14 + }, { + "time": "2024-02-17T14:00Z", + "screenTemperature": 10.63, + "maxScreenAirTemp": 10.84, + "minScreenAirTemp": 10.63, + "screenDewPointTemperature": 9.58, + "feelsLikeTemperature": 8.92, + "windSpeed10m": 3.62, + "windDirectionFrom10m": 201, + "windGustSpeed10m": 7.07, + "max10mWindGust": 7.07, + "visibility": 12109, + "screenRelativeHumidity": 93.68, + "mslp": 103021, + "uvIndex": 1, + "significantWeatherCode": 8, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 14 + }, { + "time": "2024-02-17T15:00Z", + "screenTemperature": 10.62, + "maxScreenAirTemp": 10.73, + "minScreenAirTemp": 10.6, + "screenDewPointTemperature": 9.53, + "feelsLikeTemperature": 8.92, + "windSpeed10m": 3.61, + "windDirectionFrom10m": 200, + "windGustSpeed10m": 7.22, + "max10mWindGust": 7.22, + "visibility": 12463, + "screenRelativeHumidity": 93.39, + "mslp": 103003, + "uvIndex": 1, + "significantWeatherCode": 8, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 12 + }, { + "time": "2024-02-17T16:00Z", + "screenTemperature": 10.57, + "maxScreenAirTemp": 10.62, + "minScreenAirTemp": 10.56, + "screenDewPointTemperature": 9.47, + "feelsLikeTemperature": 8.88, + "windSpeed10m": 3.65, + "windDirectionFrom10m": 197, + "windGustSpeed10m": 7.38, + "max10mWindGust": 7.38, + "visibility": 12932, + "screenRelativeHumidity": 93.29, + "mslp": 102986, + "uvIndex": 1, + "significantWeatherCode": 8, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 12 + }, { + "time": "2024-02-17T17:00Z", + "screenTemperature": 10.45, + "maxScreenAirTemp": 10.57, + "minScreenAirTemp": 10.44, + "screenDewPointTemperature": 9.39, + "feelsLikeTemperature": 8.75, + "windSpeed10m": 3.67, + "windDirectionFrom10m": 191, + "windGustSpeed10m": 7.54, + "max10mWindGust": 7.54, + "visibility": 11295, + "screenRelativeHumidity": 93.5, + "mslp": 102968, + "uvIndex": 1, + "significantWeatherCode": 8, + "precipitationRate": 0.0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "probOfPrecipitation": 14 + }, { + "time": "2024-02-17T18:00Z", + "screenTemperature": 10.25, + "screenDewPointTemperature": 9.3, + "feelsLikeTemperature": 8.46, + "windSpeed10m": 3.77, + "windDirectionFrom10m": 182, + "windGustSpeed10m": 7.94, + "visibility": 10383, + "screenRelativeHumidity": 94.28, + "mslp": 102949, + "uvIndex": 0, + "significantWeatherCode": 7, + "precipitationRate": 0.0, + "probOfPrecipitation": 11 + }, { + "time": "2024-02-17T19:00Z", + "screenTemperature": 10.34, + "screenDewPointTemperature": 9.37, + "feelsLikeTemperature": 8.34, + "windSpeed10m": 4.29, + "windDirectionFrom10m": 187, + "windGustSpeed10m": 8.68, + "visibility": 10128, + "screenRelativeHumidity": 94.17, + "mslp": 102910, + "uvIndex": 0, + "significantWeatherCode": 8, + "precipitationRate": 0.0, + "probOfPrecipitation": 16 + }] + } + }] +} diff --git a/tests/reference_data/reference_data_test_forecast.py b/tests/reference_data/reference_data_test_forecast.py new file mode 100644 index 0000000..0ed1c7f --- /dev/null +++ b/tests/reference_data/reference_data_test_forecast.py @@ -0,0 +1,1073 @@ +import datetime + +EXPECTED_FIRST_HOURLY_TIMESTEP = { + "time": datetime.datetime(2024, 2, 15, 19, 0, tzinfo=datetime.timezone.utc), + "screenTemperature": { + "value": 11.0, + "description": "Screen Air Temperature", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "maxScreenAirTemp": { + "value": 11.55, + "description": "Maximum Screen Air Temperature Over Previous Hour", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "minScreenAirTemp": { + "value": 10.98, + "description": "Minimum Screen Air Temperature Over Previous Hour", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "screenDewPointTemperature": { + "value": 8.94, + "description": "Screen Dew Point Temperature", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "feelsLikeTemperature": { + "value": 10.87, + "description": "Feels Like Temperature", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "windSpeed10m": { + "value": 1.18, + "description": "10m Wind Speed", + "unit_name": "metres per second", + "unit_symbol": "m/s", + }, + "windDirectionFrom10m": { + "value": 180, + "description": "10m Wind From Direction", + "unit_name": "degrees", + "unit_symbol": "deg", + }, + "windGustSpeed10m": { + "value": 6.69, + "description": "10m Wind Gust Speed", + "unit_name": "metres per second", + "unit_symbol": "m/s", + }, + "max10mWindGust": { + "value": 8.92, + "description": "Maximum 10m Wind Gust Speed Over Previous Hour", + "unit_name": "metres per second", + "unit_symbol": "m/s", + }, + "visibility": { + "value": 19174, + "description": "Visibility", + "unit_name": "metres", + "unit_symbol": "m", + }, + "screenRelativeHumidity": { + "value": 86.99, + "description": "Screen Relative Humidity", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "mslp": { + "value": 100660, + "description": "Mean Sea Level Pressure", + "unit_name": "pascals", + "unit_symbol": "Pa", + }, + "uvIndex": { + "value": 0, + "description": "UV Index", + "unit_name": "dimensionless", + "unit_symbol": "1", + }, + "significantWeatherCode": { + "value": "Partly cloudy", + "description": "Significant Weather Code", + "unit_name": "dimensionless", + "unit_symbol": "1", + }, + "precipitationRate": { + "value": 0.0, + "description": "Precipitation Rate", + "unit_name": "millimetres per hour", + "unit_symbol": "mm/h", + }, + "totalPrecipAmount": { + "value": 0.0, + "description": "Total Precipitation Amount Over Previous Hour", + "unit_name": "millimetres", + "unit_symbol": "mm", + }, + "totalSnowAmount": { + "value": 0, + "description": "Total Snow Amount Over Previous Hour", + "unit_name": "millimetres", + "unit_symbol": "mm", + }, + "probOfPrecipitation": { + "value": 4, + "description": "Probability of Precipitation", + "unit_name": "percentage", + "unit_symbol": "%", + }, +} + +EXPECTED_AT_DATETIME_HOURLY_TIMESTEP = { + "time": datetime.datetime(2024, 2, 16, 19, 0, tzinfo=datetime.timezone.utc), + "screenTemperature": { + "value": 8.94, + "description": "Screen Air Temperature", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "maxScreenAirTemp": { + "value": 9.6, + "description": "Maximum Screen Air Temperature Over Previous Hour", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "minScreenAirTemp": { + "value": 8.9, + "description": "Minimum Screen Air Temperature Over Previous Hour", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "screenDewPointTemperature": { + "value": 6.8, + "description": "Screen Dew Point Temperature", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "feelsLikeTemperature": { + "value": 7.68, + "description": "Feels Like Temperature", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "windSpeed10m": { + "value": 2.46, + "description": "10m Wind Speed", + "unit_name": "metres per second", + "unit_symbol": "m/s", + }, + "windDirectionFrom10m": { + "value": 255, + "description": "10m Wind From Direction", + "unit_name": "degrees", + "unit_symbol": "deg", + }, + "windGustSpeed10m": { + "value": 7.35, + "description": "10m Wind Gust Speed", + "unit_name": "metres per second", + "unit_symbol": "m/s", + }, + "max10mWindGust": { + "value": 8.02, + "description": "Maximum 10m Wind Gust Speed Over Previous Hour", + "unit_name": "metres per second", + "unit_symbol": "m/s", + }, + "visibility": { + "value": 22767, + "description": "Visibility", + "unit_name": "metres", + "unit_symbol": "m", + }, + "screenRelativeHumidity": { + "value": 86.81, + "description": "Screen Relative Humidity", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "mslp": { + "value": 102246, + "description": "Mean Sea Level Pressure", + "unit_name": "pascals", + "unit_symbol": "Pa", + }, + "uvIndex": { + "value": 0, + "description": "UV Index", + "unit_name": "dimensionless", + "unit_symbol": "1", + }, + "significantWeatherCode": { + "value": "Clear night", + "description": "Significant Weather Code", + "unit_name": "dimensionless", + "unit_symbol": "1", + }, + "precipitationRate": { + "value": 0.0, + "description": "Precipitation Rate", + "unit_name": "millimetres per hour", + "unit_symbol": "mm/h", + }, + "totalPrecipAmount": { + "value": 0.0, + "description": "Total Precipitation Amount Over Previous Hour", + "unit_name": "millimetres", + "unit_symbol": "mm", + }, + "totalSnowAmount": { + "value": 0, + "description": "Total Snow Amount Over Previous Hour", + "unit_name": "millimetres", + "unit_symbol": "mm", + }, + "probOfPrecipitation": { + "value": 0, + "description": "Probability of Precipitation", + "unit_name": "percentage", + "unit_symbol": "%", + }, +} + +EXPECTED_AT_DATETIME_HOURLY_FINAL_TIMESTEP = { + "time": datetime.datetime(2024, 2, 17, 19, 0, tzinfo=datetime.timezone.utc), + "screenTemperature": { + "value": 10.34, + "description": "Screen Air Temperature", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "screenDewPointTemperature": { + "value": 9.37, + "description": "Screen Dew Point Temperature", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "feelsLikeTemperature": { + "value": 8.34, + "description": "Feels Like Temperature", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "windSpeed10m": { + "value": 4.29, + "description": "10m Wind Speed", + "unit_name": "metres per second", + "unit_symbol": "m/s", + }, + "windDirectionFrom10m": { + "value": 187, + "description": "10m Wind From Direction", + "unit_name": "degrees", + "unit_symbol": "deg", + }, + "windGustSpeed10m": { + "value": 8.68, + "description": "10m Wind Gust Speed", + "unit_name": "metres per second", + "unit_symbol": "m/s", + }, + "visibility": { + "value": 10128, + "description": "Visibility", + "unit_name": "metres", + "unit_symbol": "m", + }, + "screenRelativeHumidity": { + "value": 94.17, + "description": "Screen Relative Humidity", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "mslp": { + "value": 102910, + "description": "Mean Sea Level Pressure", + "unit_name": "pascals", + "unit_symbol": "Pa", + }, + "uvIndex": { + "value": 0, + "description": "UV Index", + "unit_name": "dimensionless", + "unit_symbol": "1", + }, + "significantWeatherCode": { + "value": "Overcast", + "description": "Significant Weather Code", + "unit_name": "dimensionless", + "unit_symbol": "1", + }, + "precipitationRate": { + "value": 0.0, + "description": "Precipitation Rate", + "unit_name": "millimetres per hour", + "unit_symbol": "mm/h", + }, + "probOfPrecipitation": { + "value": 16, + "description": "Probability of Precipitation", + "unit_name": "percentage", + "unit_symbol": "%", + }, +} +EXPECTED_FIRST_DAILY_TIMESTEP = { + "time": datetime.datetime(2024, 2, 16, 0, 0, tzinfo=datetime.timezone.utc), + "10MWindSpeed": { + "value": 1.39, + "description": "10m Wind Speed at Local Midnight", + "unit_name": "metres per second", + "unit_symbol": "m/s", + }, + "10MWindDirection": { + "value": 243, + "description": "10m Wind Direction at Local Midnight", + "unit_name": "degrees", + "unit_symbol": "deg", + }, + "10MWindGust": { + "value": 7.2, + "description": "10m Wind Gust Speed at Local Midnight", + "unit_name": "metres per second", + "unit_symbol": "m/s", + }, + "Visibility": { + "value": 27712, + "description": "Visibility at Local Midnight", + "unit_name": "metres", + "unit_symbol": "m", + }, + "RelativeHumidity": { + "value": 80.91, + "description": "Relative Humidity at Local Midnight", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "Mslp": { + "value": 102640, + "description": "Mean Sea Level Pressure at Local Midnight", + "unit_name": "pascals", + "unit_symbol": "Pa", + }, + "SignificantWeatherCode": { + "value": 7, + "description": "Night Significant Weather Code", + "unit_name": "dimensionless", + "unit_symbol": "1", + }, + "MinScreenTemperature": { + "value": 5.32, + "description": "Night Minimum Screen Air Temperature", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "UpperBoundMinTemp": { + "value": 9.17, + "description": "Upper Bound on Night Minimum Screen Air Temperature", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "LowerBoundMinTemp": { + "value": 3.56, + "description": "Lower Bound on Night Minimum Screen Air Temperature", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "MinFeelsLikeTemp": { + "value": 6.27, + "description": "Night Minimum Feels Like Air Temperature", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "UpperBoundMinFeelsLikeTemp": { + "value": 8.74, + "description": "Upper Bound on Night Minimum Feels Like Air Temperature", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "LowerBoundMinFeelsLikeTemp": { + "value": 2.75, + "description": "Lower Bound on Night Minimum Feels Like Air Temperature", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "ProbabilityOfPrecipitation": { + "value": 11, + "description": "Probability of Precipitation During The Night", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "ProbabilityOfSnow": { + "value": 0, + "description": "Probability of Snow During The Night", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "ProbabilityOfHeavySnow": { + "value": 0, + "description": "Probability of Heavy Snow During The Night", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "ProbabilityOfRain": { + "value": 10, + "description": "Probability of Rain During The Night", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "ProbabilityOfHeavyRain": { + "value": 0, + "description": "Probability of Heavy Rain During The Night", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "ProbabilityOfHail": { + "value": 0, + "description": "Probability of Hail During The Night", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "ProbabilityOfSferics": { + "value": 0, + "description": "Probability of Sferics During The Night", + "unit_name": "percentage", + "unit_symbol": "%", + }, +} + +EXPECTED_AT_DATETIME_DAILY_TIMESTEP = { + "time": datetime.datetime(2024, 2, 17, 0, 0, tzinfo=datetime.timezone.utc), + "10MWindSpeed": { + "value": 6.1, + "description": "10m Wind Speed at Local Midnight", + "unit_name": "metres per second", + "unit_symbol": "m/s", + }, + "10MWindDirection": { + "value": 218, + "description": "10m Wind Direction at Local Midnight", + "unit_name": "degrees", + "unit_symbol": "deg", + }, + "10MWindGust": { + "value": 12.98, + "description": "10m Wind Gust Speed at Local Midnight", + "unit_name": "metres per second", + "unit_symbol": "m/s", + }, + "Visibility": { + "value": 5915, + "description": "Visibility at Local Midnight", + "unit_name": "metres", + "unit_symbol": "m", + }, + "RelativeHumidity": { + "value": 93.62, + "description": "Relative Humidity at Local Midnight", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "Mslp": { + "value": 102800, + "description": "Mean Sea Level Pressure at Local Midnight", + "unit_name": "pascals", + "unit_symbol": "Pa", + }, + "SignificantWeatherCode": { + "value": 15, + "description": "Night Significant Weather Code", + "unit_name": "dimensionless", + "unit_symbol": "1", + }, + "MinScreenTemperature": { + "value": 9.96, + "description": "Night Minimum Screen Air Temperature", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "UpperBoundMinTemp": { + "value": 10.71, + "description": "Upper Bound on Night Minimum Screen Air Temperature", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "LowerBoundMinTemp": { + "value": 9.04, + "description": "Lower Bound on Night Minimum Screen Air Temperature", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "MinFeelsLikeTemp": { + "value": 7.76, + "description": "Night Minimum Feels Like Air Temperature", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "UpperBoundMinFeelsLikeTemp": { + "value": 8.28, + "description": "Upper Bound on Night Minimum Feels Like Air Temperature", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "LowerBoundMinFeelsLikeTemp": { + "value": 7.04, + "description": "Lower Bound on Night Minimum Feels Like Air Temperature", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "ProbabilityOfPrecipitation": { + "value": 97, + "description": "Probability of Precipitation During The Night", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "ProbabilityOfSnow": { + "value": 0, + "description": "Probability of Snow During The Night", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "ProbabilityOfHeavySnow": { + "value": 0, + "description": "Probability of Heavy Snow During The Night", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "ProbabilityOfRain": { + "value": 97, + "description": "Probability of Rain During The Night", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "ProbabilityOfHeavyRain": { + "value": 96, + "description": "Probability of Heavy Rain During The Night", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "ProbabilityOfHail": { + "value": 20, + "description": "Probability of Hail During The Night", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "ProbabilityOfSferics": { + "value": 10, + "description": "Probability of Sferics During The Night", + "unit_name": "percentage", + "unit_symbol": "%", + }, +} +EXPECTED_AT_DATETIME_DAILY_FINAL_TIMESTEP = { + "time": datetime.datetime(2024, 2, 23, 12, 0, tzinfo=datetime.timezone.utc), + "10MWindSpeed": { + "value": 7.11, + "description": "10m Wind Speed at Local Midday", + "unit_name": "metres per second", + "unit_symbol": "m/s", + }, + "10MWindDirection": { + "value": 231, + "description": "10m Wind Direction at Local Midday", + "unit_name": "degrees", + "unit_symbol": "deg", + }, + "10MWindGust": { + "value": 13.38, + "description": "10m Wind Gust Speed at Local Midday", + "unit_name": "metres per second", + "unit_symbol": "m/s", + }, + "Visibility": { + "value": 23049, + "description": "Visibility at Local Midday", + "unit_name": "metres", + "unit_symbol": "m", + }, + "RelativeHumidity": { + "value": 73.25, + "description": "Relative Humidity at Local Midday", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "Mslp": { + "value": 98974, + "description": "Mean Sea Level Pressure at Local Midday", + "unit_name": "pascals", + "unit_symbol": "Pa", + }, + "maxUvIndex": { + "value": 2, + "description": "Day Maximum UV Index", + "unit_name": "dimensionless", + "unit_symbol": "1", + }, + "SignificantWeatherCode": { + "value": 10, + "description": "Day Significant Weather Code", + "unit_name": "dimensionless", + "unit_symbol": "1", + }, + "MaxScreenTemperature": { + "value": 8.57, + "description": "Day Maximum Screen Air Temperature", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "UpperBoundMaxTemp": { + "value": 10.67, + "description": "Upper Bound on Day Maximum Screen Air Temperature", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "LowerBoundMaxTemp": { + "value": 6.67, + "description": "Lower Bound on Day Maximum Screen Air Temperature", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "MaxFeelsLikeTemp": { + "value": 4.42, + "description": "Day Maximum Feels Like Air Temperature", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "UpperBoundMaxFeelsLikeTemp": { + "value": 7.38, + "description": "Upper Bound on Day Maximum Feels Like Air Temperature", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "LowerBoundMaxFeelsLikeTemp": { + "value": 3.89, + "description": "Lower Bound on Day Maximum Feels Like Air Temperature", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "ProbabilityOfPrecipitation": { + "value": 52, + "description": "Probability of Precipitation During The Day", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "ProbabilityOfSnow": { + "value": 0, + "description": "Probability of Snow During The Day", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "ProbabilityOfHeavySnow": { + "value": 0, + "description": "Probability of Heavy Snow During The Day", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "ProbabilityOfRain": { + "value": 52, + "description": "Probability of Rain During The Day", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "ProbabilityOfHeavyRain": { + "value": 48, + "description": "Probability of Heavy Rain During The Day", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "ProbabilityOfHail": { + "value": 10, + "description": "Probability of Hail During The Day", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "ProbabilityOfSferics": { + "value": 11, + "description": "Probability of Sferics During The Day", + "unit_name": "percentage", + "unit_symbol": "%", + }, +} + +EXPECTED_FIRST_THREE_HOURLY_TIMESTEP = { + "time": datetime.datetime(2024, 2, 17, 15, 0, tzinfo=datetime.timezone.utc), + "maxScreenAirTemp": { + "value": 12.0, + "description": "Maximum Screen Air Temperature Over Previous Three Hours", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "minScreenAirTemp": { + "value": 10.73, + "description": "Minimum Screen Air Temperature Over Previous Three Hours", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "max10mWindGust": { + "value": 8.87, + "description": "Maximum 10m Wind Gust Speed Over Previous Three Hours", + "unit_name": "metres per second", + "unit_symbol": "m/s", + }, + "significantWeatherCode": { + "value": "Overcast", + "description": "Three Hour Significant Weather Code", + "unit_name": "dimensionless", + "unit_symbol": "1", + }, + "totalPrecipAmount": { + "value": 0.0, + "description": "Total Precipitation Amount Over Previous Three Hours", + "unit_name": "millimetres", + "unit_symbol": "mm", + }, + "totalSnowAmount": { + "value": 0, + "description": "Total Snow Amount Over Previous Three Hours", + "unit_name": "millimetres", + "unit_symbol": "mm", + }, + "windSpeed10m": { + "value": 5.04, + "description": "10m Wind Speed Over Previous Three Hours", + "unit_name": "metres per second", + "unit_symbol": "m/s", + }, + "windDirectionFrom10m": { + "value": 225, + "description": "10m Wind From Direction Over Previous Three Hours", + "unit_name": "degrees", + "unit_symbol": "deg", + }, + "windGustSpeed10m": { + "value": 8.75, + "description": "10m Wind Gust Speed Over Previous Three Hours", + "unit_name": "metres per second", + "unit_symbol": "m/s", + }, + "visibility": { + "value": 9118, + "description": "Visibility Over Previous Three Hours", + "unit_name": "metres", + "unit_symbol": "m", + }, + "mslp": { + "value": 103090, + "description": "Mean Sea Level Pressure Over Previous Three Hours", + "unit_name": "pascals", + "unit_symbol": "Pa", + }, + "screenRelativeHumidity": { + "value": 96.84, + "description": "Screen Relative Humidity Over Previous Three Hours", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "feelsLikeTemp": { + "value": 8.53, + "description": "Feels Like Temperature Over Previous Three Hours", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "uvIndex": { + "value": 1, + "description": "Maximum UV Index Over Previous Three Hours", + "unit_name": "dimensionless", + "unit_symbol": "1", + }, + "probOfPrecipitation": { + "value": 10, + "description": "Three Hour Probability of Precipitation", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "probOfSnow": { + "value": 0, + "description": "Three Hour Probability of Snow", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "probOfHeavySnow": { + "value": 0, + "description": "Three Hour Probability of Heavy Snow", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "probOfRain": { + "value": 10, + "description": "Three Hour Probability of Rain", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "probOfHeavyRain": { + "value": 0, + "description": "Three Hour Probability of Heavy Rain", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "probOfHail": { + "value": 0, + "description": "Three Hour Probability of Hail", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "probOfSferics": { + "value": 0, + "description": "Three Hour Probability of Sferics", + "unit_name": "percentage", + "unit_symbol": "%", + }, +} + +EXPECTED_AT_DATETIME_THREE_HOURLY_TIMESTEP = { + "time": datetime.datetime(2024, 2, 22, 18, 0, tzinfo=datetime.timezone.utc), + "maxScreenAirTemp": { + "value": 9.82, + "description": "Maximum Screen Air Temperature Over Previous Three Hours", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "minScreenAirTemp": { + "value": 7.61, + "description": "Minimum Screen Air Temperature Over Previous Three Hours", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "max10mWindGust": { + "value": 19.28, + "description": "Maximum 10m Wind Gust Speed Over Previous Three Hours", + "unit_name": "metres per second", + "unit_symbol": "m/s", + }, + "significantWeatherCode": { + "value": "Light rain shower", + "description": "Three Hour Significant Weather Code", + "unit_name": "dimensionless", + "unit_symbol": "1", + }, + "totalPrecipAmount": { + "value": 1.11, + "description": "Total Precipitation Amount Over Previous Three Hours", + "unit_name": "millimetres", + "unit_symbol": "mm", + }, + "totalSnowAmount": { + "value": 0, + "description": "Total Snow Amount Over Previous Three Hours", + "unit_name": "millimetres", + "unit_symbol": "mm", + }, + "windSpeed10m": { + "value": 7.91, + "description": "10m Wind Speed Over Previous Three Hours", + "unit_name": "metres per second", + "unit_symbol": "m/s", + }, + "windDirectionFrom10m": { + "value": 239, + "description": "10m Wind From Direction Over Previous Three Hours", + "unit_name": "degrees", + "unit_symbol": "deg", + }, + "windGustSpeed10m": { + "value": 14.42, + "description": "10m Wind Gust Speed Over Previous Three Hours", + "unit_name": "metres per second", + "unit_symbol": "m/s", + }, + "visibility": { + "value": 20148, + "description": "Visibility Over Previous Three Hours", + "unit_name": "metres", + "unit_symbol": "m", + }, + "mslp": { + "value": 98434, + "description": "Mean Sea Level Pressure Over Previous Three Hours", + "unit_name": "pascals", + "unit_symbol": "Pa", + }, + "screenRelativeHumidity": { + "value": 79.36, + "description": "Screen Relative Humidity Over Previous Three Hours", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "feelsLikeTemp": { + "value": 3.74, + "description": "Feels Like Temperature Over Previous Three Hours", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "uvIndex": { + "value": 0, + "description": "Maximum UV Index Over Previous Three Hours", + "unit_name": "dimensionless", + "unit_symbol": "1", + }, + "probOfPrecipitation": { + "value": 44, + "description": "Three Hour Probability of Precipitation", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "probOfSnow": { + "value": 0, + "description": "Three Hour Probability of Snow", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "probOfHeavySnow": { + "value": 0, + "description": "Three Hour Probability of Heavy Snow", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "probOfRain": { + "value": 44, + "description": "Three Hour Probability of Rain", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "probOfHeavyRain": { + "value": 32, + "description": "Three Hour Probability of Heavy Rain", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "probOfHail": { + "value": 4, + "description": "Three Hour Probability of Hail", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "probOfSferics": { + "value": 6, + "description": "Three Hour Probability of Sferics", + "unit_name": "percentage", + "unit_symbol": "%", + }, +} + +EXPECTED_AT_DATETIME_THREE_HOURLY_FINAL_TIMESTEP = { + "time": datetime.datetime(2024, 2, 24, 15, 0, tzinfo=datetime.timezone.utc), + "maxScreenAirTemp": { + "value": 8.25, + "description": "Maximum Screen Air Temperature Over Previous Three Hours", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "minScreenAirTemp": { + "value": 7.96, + "description": "Minimum Screen Air Temperature Over Previous Three Hours", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "max10mWindGust": { + "value": 12.77, + "description": "Maximum 10m Wind Gust Speed Over Previous Three Hours", + "unit_name": "metres per second", + "unit_symbol": "m/s", + }, + "significantWeatherCode": { + "value": "Light rain shower", + "description": "Three Hour Significant Weather Code", + "unit_name": "dimensionless", + "unit_symbol": "1", + }, + "totalPrecipAmount": { + "value": 0.58, + "description": "Total Precipitation Amount Over Previous Three Hours", + "unit_name": "millimetres", + "unit_symbol": "mm", + }, + "totalSnowAmount": { + "value": 0, + "description": "Total Snow Amount Over Previous Three Hours", + "unit_name": "millimetres", + "unit_symbol": "mm", + }, + "windSpeed10m": { + "value": 5.98, + "description": "10m Wind Speed Over Previous Three Hours", + "unit_name": "metres per second", + "unit_symbol": "m/s", + }, + "windDirectionFrom10m": { + "value": 228, + "description": "10m Wind From Direction Over Previous Three Hours", + "unit_name": "degrees", + "unit_symbol": "deg", + }, + "windGustSpeed10m": { + "value": 11.48, + "description": "10m Wind Gust Speed Over Previous Three Hours", + "unit_name": "metres per second", + "unit_symbol": "m/s", + }, + "visibility": { + "value": 22483, + "description": "Visibility Over Previous Three Hours", + "unit_name": "metres", + "unit_symbol": "m", + }, + "mslp": { + "value": 99725, + "description": "Mean Sea Level Pressure Over Previous Three Hours", + "unit_name": "pascals", + "unit_symbol": "Pa", + }, + "screenRelativeHumidity": { + "value": 72.01, + "description": "Screen Relative Humidity Over Previous Three Hours", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "feelsLikeTemp": { + "value": 4.97, + "description": "Feels Like Temperature Over Previous Three Hours", + "unit_name": "degrees Celsius", + "unit_symbol": "Cel", + }, + "uvIndex": { + "value": 1, + "description": "Maximum UV Index Over Previous Three Hours", + "unit_name": "dimensionless", + "unit_symbol": "1", + }, + "probOfPrecipitation": { + "value": 32, + "description": "Three Hour Probability of Precipitation", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "probOfSnow": { + "value": 0, + "description": "Three Hour Probability of Snow", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "probOfHeavySnow": { + "value": 0, + "description": "Three Hour Probability of Heavy Snow", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "probOfRain": { + "value": 32, + "description": "Three Hour Probability of Rain", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "probOfHeavyRain": { + "value": 17, + "description": "Three Hour Probability of Heavy Rain", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "probOfHail": { + "value": 1, + "description": "Three Hour Probability of Hail", + "unit_name": "percentage", + "unit_symbol": "%", + }, + "probOfSferics": { + "value": 3, + "description": "Three Hour Probability of Sferics", + "unit_name": "percentage", + "unit_symbol": "%", + }, +} diff --git a/tests/reference_data/three_hourly_api_data.json b/tests/reference_data/three_hourly_api_data.json new file mode 100644 index 0000000..5329345 --- /dev/null +++ b/tests/reference_data/three_hourly_api_data.json @@ -0,0 +1,1562 @@ +{ + "type": "FeatureCollection", + "parameters": [{ + "totalSnowAmount": { + "type": "Parameter", + "description": "Total Snow Amount Over Previous Three Hours", + "unit": { + "label": "millimetres", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "mm" + } + } + }, + "visibility": { + "type": "Parameter", + "description": "Visibility Over Previous Three Hours", + "unit": { + "label": "metres", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m" + } + } + }, + "probOfHail": { + "type": "Parameter", + "description": "Three Hour Probability of Hail", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "windDirectionFrom10m": { + "type": "Parameter", + "description": "10m Wind From Direction Over Previous Three Hours", + "unit": { + "label": "degrees", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "deg" + } + } + }, + "probOfHeavyRain": { + "type": "Parameter", + "description": "Three Hour Probability of Heavy Rain", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "maxScreenAirTemp": { + "type": "Parameter", + "description": "Maximum Screen Air Temperature Over Previous Three Hours", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "feelsLikeTemp": { + "type": "Parameter", + "description": "Feels Like Temperature Over Previous Three Hours", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "probOfSferics": { + "type": "Parameter", + "description": "Three Hour Probability of Sferics", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "screenRelativeHumidity": { + "type": "Parameter", + "description": "Screen Relative Humidity Over Previous Three Hours", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "windSpeed10m": { + "type": "Parameter", + "description": "10m Wind Speed Over Previous Three Hours", + "unit": { + "label": "metres per second", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m/s" + } + } + }, + "probOfPrecipitation": { + "type": "Parameter", + "description": "Three Hour Probability of Precipitation", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "probOfRain": { + "type": "Parameter", + "description": "Three Hour Probability of Rain", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "max10mWindGust": { + "type": "Parameter", + "description": "Maximum 10m Wind Gust Speed Over Previous Three Hours", + "unit": { + "label": "metres per second", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m/s" + } + } + }, + "significantWeatherCode": { + "type": "Parameter", + "description": "Three Hour Significant Weather Code", + "unit": { + "label": "dimensionless", + "symbol": { + "value": "https://metoffice.apiconnect.ibmcloud.com/metoffice/production/", + "type": "1" + } + } + }, + "probOfHeavySnow": { + "type": "Parameter", + "description": "Three Hour Probability of Heavy Snow", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "minScreenAirTemp": { + "type": "Parameter", + "description": "Minimum Screen Air Temperature Over Previous Three Hours", + "unit": { + "label": "degrees Celsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "totalPrecipAmount": { + "type": "Parameter", + "description": "Total Precipitation Amount Over Previous Three Hours", + "unit": { + "label": "millimetres", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "mm" + } + } + }, + "mslp": { + "type": "Parameter", + "description": "Mean Sea Level Pressure Over Previous Three Hours", + "unit": { + "label": "pascals", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Pa" + } + } + }, + "windGustSpeed10m": { + "type": "Parameter", + "description": "10m Wind Gust Speed Over Previous Three Hours", + "unit": { + "label": "metres per second", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m/s" + } + } + }, + "uvIndex": { + "type": "Parameter", + "description": "Maximum UV Index Over Previous Three Hours", + "unit": { + "label": "dimensionless", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "1" + } + } + }, + "probOfSnow": { + "type": "Parameter", + "description": "Three Hour Probability of Snow", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + } + }], + "features": [{ + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [0.0154, 50.9992, 37.0] + }, + "properties": { + "location": { + "name": "Sheffield Park" + }, + "requestPointDistance": 1081.5349, + "modelRunDate": "2024-02-17T15:00Z", + "timeSeries": [{ + "time": "2024-02-17T15:00Z", + "maxScreenAirTemp": 12.0, + "minScreenAirTemp": 10.73, + "max10mWindGust": 8.87, + "significantWeatherCode": 8, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "windSpeed10m": 5.04, + "windDirectionFrom10m": 225, + "windGustSpeed10m": 8.75, + "visibility": 9118, + "mslp": 103090, + "screenRelativeHumidity": 96.84, + "feelsLikeTemp": 8.53, + "uvIndex": 1, + "probOfPrecipitation": 10, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 10, + "probOfHeavyRain": 0, + "probOfHail": 0, + "probOfSferics": 0 + }, { + "time": "2024-02-17T18:00Z", + "maxScreenAirTemp": 10.92, + "minScreenAirTemp": 10.48, + "max10mWindGust": 10.01, + "significantWeatherCode": 8, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "windSpeed10m": 4.22, + "windDirectionFrom10m": 191, + "windGustSpeed10m": 9.9, + "visibility": 9579, + "mslp": 103021, + "screenRelativeHumidity": 90.49, + "feelsLikeTemp": 8.57, + "uvIndex": 0, + "probOfPrecipitation": 13, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 13, + "probOfHeavyRain": 3, + "probOfHail": 0, + "probOfSferics": 0 + }, { + "time": "2024-02-17T21:00Z", + "maxScreenAirTemp": 11.21, + "minScreenAirTemp": 10.58, + "max10mWindGust": 12.91, + "significantWeatherCode": 15, + "totalPrecipAmount": 0.11, + "totalSnowAmount": 0, + "windSpeed10m": 5.81, + "windDirectionFrom10m": 198, + "windGustSpeed10m": 12.82, + "visibility": 11035, + "mslp": 102910, + "screenRelativeHumidity": 88.83, + "feelsLikeTemp": 8.55, + "uvIndex": 0, + "probOfPrecipitation": 94, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 94, + "probOfHeavyRain": 93, + "probOfHail": 19, + "probOfSferics": 9 + }, { + "time": "2024-02-18T00:00Z", + "maxScreenAirTemp": 11.1, + "minScreenAirTemp": 10.47, + "max10mWindGust": 12.89, + "significantWeatherCode": 15, + "totalPrecipAmount": 5.75, + "totalSnowAmount": 0, + "windSpeed10m": 5.83, + "windDirectionFrom10m": 217, + "windGustSpeed10m": 12.74, + "visibility": 4164, + "mslp": 102782, + "screenRelativeHumidity": 94.21, + "feelsLikeTemp": 7.83, + "uvIndex": 0, + "probOfPrecipitation": 97, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 97, + "probOfHeavyRain": 96, + "probOfHail": 19, + "probOfSferics": 10 + }, { + "time": "2024-02-18T03:00Z", + "maxScreenAirTemp": 10.53, + "minScreenAirTemp": 10.11, + "max10mWindGust": 12.75, + "significantWeatherCode": 15, + "totalPrecipAmount": 6.38, + "totalSnowAmount": 0, + "windSpeed10m": 4.95, + "windDirectionFrom10m": 229, + "windGustSpeed10m": 10.92, + "visibility": 5768, + "mslp": 102618, + "screenRelativeHumidity": 95.9, + "feelsLikeTemp": 7.79, + "uvIndex": 0, + "probOfPrecipitation": 97, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 97, + "probOfHeavyRain": 97, + "probOfHail": 20, + "probOfSferics": 10 + }, { + "time": "2024-02-18T06:00Z", + "maxScreenAirTemp": 10.16, + "minScreenAirTemp": 10.06, + "max10mWindGust": 10.65, + "significantWeatherCode": 15, + "totalPrecipAmount": 3.37, + "totalSnowAmount": 0, + "windSpeed10m": 3.51, + "windDirectionFrom10m": 243, + "windGustSpeed10m": 7.9, + "visibility": 19831, + "mslp": 102519, + "screenRelativeHumidity": 96.87, + "feelsLikeTemp": 8.48, + "uvIndex": 0, + "probOfPrecipitation": 90, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 90, + "probOfHeavyRain": 88, + "probOfHail": 18, + "probOfSferics": 12 + }, { + "time": "2024-02-18T09:00Z", + "maxScreenAirTemp": 10.69, + "minScreenAirTemp": 10.14, + "max10mWindGust": 7.51, + "significantWeatherCode": 12, + "totalPrecipAmount": 1.05, + "totalSnowAmount": 0, + "windSpeed10m": 2.34, + "windDirectionFrom10m": 250, + "windGustSpeed10m": 5.75, + "visibility": 9319, + "mslp": 102513, + "screenRelativeHumidity": 98.27, + "feelsLikeTemp": 9.86, + "uvIndex": 1, + "probOfPrecipitation": 52, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 52, + "probOfHeavyRain": 32, + "probOfHail": 3, + "probOfSferics": 1 + }, { + "time": "2024-02-18T12:00Z", + "maxScreenAirTemp": 13.18, + "minScreenAirTemp": 10.69, + "max10mWindGust": 7.44, + "significantWeatherCode": 7, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "windSpeed10m": 3.99, + "windDirectionFrom10m": 297, + "windGustSpeed10m": 7.44, + "visibility": 25301, + "mslp": 102620, + "screenRelativeHumidity": 86.89, + "feelsLikeTemp": 11.57, + "uvIndex": 1, + "probOfPrecipitation": 11, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 11, + "probOfHeavyRain": 6, + "probOfHail": 1, + "probOfSferics": 1 + }, { + "time": "2024-02-18T15:00Z", + "maxScreenAirTemp": 13.65, + "minScreenAirTemp": 13.12, + "max10mWindGust": 7.92, + "significantWeatherCode": 3, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "windSpeed10m": 3.93, + "windDirectionFrom10m": 297, + "windGustSpeed10m": 7.45, + "visibility": 29088, + "mslp": 102627, + "screenRelativeHumidity": 75.69, + "feelsLikeTemp": 11.95, + "uvIndex": 1, + "probOfPrecipitation": 3, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 3, + "probOfHeavyRain": 2, + "probOfHail": 0, + "probOfSferics": 0 + }, { + "time": "2024-02-18T18:00Z", + "maxScreenAirTemp": 13.52, + "minScreenAirTemp": 10.8, + "max10mWindGust": 7.29, + "significantWeatherCode": 2, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "windSpeed10m": 2.14, + "windDirectionFrom10m": 287, + "windGustSpeed10m": 6.16, + "visibility": 21358, + "mslp": 102800, + "screenRelativeHumidity": 89.42, + "feelsLikeTemp": 10.28, + "uvIndex": 0, + "probOfPrecipitation": 2, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 2, + "probOfHeavyRain": 2, + "probOfHail": 0, + "probOfSferics": 0 + }, { + "time": "2024-02-18T21:00Z", + "maxScreenAirTemp": 11.01, + "minScreenAirTemp": 9.88, + "max10mWindGust": 7.24, + "significantWeatherCode": 2, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "windSpeed10m": 2.59, + "windDirectionFrom10m": 282, + "windGustSpeed10m": 6.65, + "visibility": 19012, + "mslp": 102967, + "screenRelativeHumidity": 92.62, + "feelsLikeTemp": 8.71, + "uvIndex": 0, + "probOfPrecipitation": 1, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 1, + "probOfHeavyRain": 0, + "probOfHail": 0, + "probOfSferics": 0 + }, { + "time": "2024-02-19T00:00Z", + "maxScreenAirTemp": 9.91, + "minScreenAirTemp": 9.04, + "max10mWindGust": 7.07, + "significantWeatherCode": 7, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "windSpeed10m": 2.81, + "windDirectionFrom10m": 282, + "windGustSpeed10m": 6.48, + "visibility": 17411, + "mslp": 103035, + "screenRelativeHumidity": 94.29, + "feelsLikeTemp": 7.51, + "uvIndex": 0, + "probOfPrecipitation": 4, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 4, + "probOfHeavyRain": 0, + "probOfHail": 0, + "probOfSferics": 0 + }, { + "time": "2024-02-19T03:00Z", + "maxScreenAirTemp": 9.05, + "minScreenAirTemp": 8.24, + "max10mWindGust": 7.07, + "significantWeatherCode": 7, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "windSpeed10m": 2.33, + "windDirectionFrom10m": 273, + "windGustSpeed10m": 5.44, + "visibility": 16944, + "mslp": 103012, + "screenRelativeHumidity": 95.46, + "feelsLikeTemp": 6.97, + "uvIndex": 0, + "probOfPrecipitation": 4, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 4, + "probOfHeavyRain": 0, + "probOfHail": 0, + "probOfSferics": 0 + }, { + "time": "2024-02-19T06:00Z", + "maxScreenAirTemp": 8.27, + "minScreenAirTemp": 7.56, + "max10mWindGust": 7.47, + "significantWeatherCode": 8, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "windSpeed10m": 3.33, + "windDirectionFrom10m": 258, + "windGustSpeed10m": 6.86, + "visibility": 20004, + "mslp": 102970, + "screenRelativeHumidity": 94.27, + "feelsLikeTemp": 5.54, + "uvIndex": 0, + "probOfPrecipitation": 10, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 10, + "probOfHeavyRain": 1, + "probOfHail": 0, + "probOfSferics": 0 + }, { + "time": "2024-02-19T09:00Z", + "maxScreenAirTemp": 8.94, + "minScreenAirTemp": 7.65, + "max10mWindGust": 9.55, + "significantWeatherCode": 7, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "windSpeed10m": 4.71, + "windDirectionFrom10m": 264, + "windGustSpeed10m": 8.92, + "visibility": 12932, + "mslp": 102970, + "screenRelativeHumidity": 88.85, + "feelsLikeTemp": 6.39, + "uvIndex": 1, + "probOfPrecipitation": 52, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 52, + "probOfHeavyRain": 30, + "probOfHail": 2, + "probOfSferics": 1 + }, { + "time": "2024-02-19T12:00Z", + "maxScreenAirTemp": 10.71, + "minScreenAirTemp": 8.93, + "max10mWindGust": 10.18, + "significantWeatherCode": 7, + "totalPrecipAmount": 0.09, + "totalSnowAmount": 0, + "windSpeed10m": 5.29, + "windDirectionFrom10m": 278, + "windGustSpeed10m": 10.18, + "visibility": 24236, + "mslp": 102917, + "screenRelativeHumidity": 82.57, + "feelsLikeTemp": 8.2, + "uvIndex": 1, + "probOfPrecipitation": 13, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 13, + "probOfHeavyRain": 5, + "probOfHail": 0, + "probOfSferics": 1 + }, { + "time": "2024-02-19T15:00Z", + "maxScreenAirTemp": 12.13, + "minScreenAirTemp": 10.64, + "max10mWindGust": 11.16, + "significantWeatherCode": 3, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "windSpeed10m": 5.06, + "windDirectionFrom10m": 309, + "windGustSpeed10m": 9.66, + "visibility": 34168, + "mslp": 102889, + "screenRelativeHumidity": 65.14, + "feelsLikeTemp": 9.69, + "uvIndex": 1, + "probOfPrecipitation": 4, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 4, + "probOfHeavyRain": 2, + "probOfHail": 0, + "probOfSferics": 0 + }, { + "time": "2024-02-19T18:00Z", + "maxScreenAirTemp": 11.98, + "minScreenAirTemp": 8.91, + "max10mWindGust": 10.5, + "significantWeatherCode": 0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "windSpeed10m": 3.27, + "windDirectionFrom10m": 299, + "windGustSpeed10m": 6.36, + "visibility": 29205, + "mslp": 103024, + "screenRelativeHumidity": 77.5, + "feelsLikeTemp": 7.11, + "uvIndex": 0, + "probOfPrecipitation": 1, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 1, + "probOfHeavyRain": 0, + "probOfHail": 0, + "probOfSferics": 0 + }, { + "time": "2024-02-19T21:00Z", + "maxScreenAirTemp": 9.04, + "minScreenAirTemp": 6.96, + "max10mWindGust": 7.08, + "significantWeatherCode": 7, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "windSpeed10m": 3.06, + "windDirectionFrom10m": 298, + "windGustSpeed10m": 5.3, + "visibility": 26260, + "mslp": 103122, + "screenRelativeHumidity": 85.18, + "feelsLikeTemp": 4.87, + "uvIndex": 0, + "probOfPrecipitation": 4, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 4, + "probOfHeavyRain": 0, + "probOfHail": 0, + "probOfSferics": 0 + }, { + "time": "2024-02-20T00:00Z", + "maxScreenAirTemp": 7.06, + "minScreenAirTemp": 5.8, + "max10mWindGust": 6.14, + "significantWeatherCode": 7, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "windSpeed10m": 2.59, + "windDirectionFrom10m": 296, + "windGustSpeed10m": 4.49, + "visibility": 23913, + "mslp": 103122, + "screenRelativeHumidity": 89.64, + "feelsLikeTemp": 3.88, + "uvIndex": 0, + "probOfPrecipitation": 4, + "probOfSnow": 1, + "probOfHeavySnow": 0, + "probOfRain": 4, + "probOfHeavyRain": 0, + "probOfHail": 0, + "probOfSferics": 0 + }, { + "time": "2024-02-20T03:00Z", + "maxScreenAirTemp": 5.85, + "minScreenAirTemp": 4.32, + "max10mWindGust": 5.69, + "significantWeatherCode": 7, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "windSpeed10m": 2.47, + "windDirectionFrom10m": 266, + "windGustSpeed10m": 3.93, + "visibility": 20523, + "mslp": 103057, + "screenRelativeHumidity": 94.0, + "feelsLikeTemp": 2.36, + "uvIndex": 0, + "probOfPrecipitation": 5, + "probOfSnow": 1, + "probOfHeavySnow": 0, + "probOfRain": 5, + "probOfHeavyRain": 0, + "probOfHail": 0, + "probOfSferics": 0 + }, { + "time": "2024-02-20T06:00Z", + "maxScreenAirTemp": 5.65, + "minScreenAirTemp": 4.45, + "max10mWindGust": 5.62, + "significantWeatherCode": 7, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "windSpeed10m": 2.92, + "windDirectionFrom10m": 233, + "windGustSpeed10m": 5.24, + "visibility": 17975, + "mslp": 102979, + "screenRelativeHumidity": 94.91, + "feelsLikeTemp": 2.75, + "uvIndex": 0, + "probOfPrecipitation": 5, + "probOfSnow": 1, + "probOfHeavySnow": 0, + "probOfRain": 5, + "probOfHeavyRain": 0, + "probOfHail": 0, + "probOfSferics": 0 + }, { + "time": "2024-02-20T09:00Z", + "maxScreenAirTemp": 8.11, + "minScreenAirTemp": 5.04, + "max10mWindGust": 9.74, + "significantWeatherCode": 7, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "windSpeed10m": 4.63, + "windDirectionFrom10m": 225, + "windGustSpeed10m": 8.8, + "visibility": 20465, + "mslp": 102896, + "screenRelativeHumidity": 91.17, + "feelsLikeTemp": 5.48, + "uvIndex": 1, + "probOfPrecipitation": 6, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 6, + "probOfHeavyRain": 0, + "probOfHail": 0, + "probOfSferics": 0 + }, { + "time": "2024-02-20T12:00Z", + "maxScreenAirTemp": 10.42, + "minScreenAirTemp": 8.11, + "max10mWindGust": 13.83, + "significantWeatherCode": 7, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "windSpeed10m": 7.16, + "windDirectionFrom10m": 230, + "windGustSpeed10m": 13.73, + "visibility": 24484, + "mslp": 102757, + "screenRelativeHumidity": 81.96, + "feelsLikeTemp": 7.33, + "uvIndex": 1, + "probOfPrecipitation": 6, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 6, + "probOfHeavyRain": 0, + "probOfHail": 0, + "probOfSferics": 0 + }, { + "time": "2024-02-20T15:00Z", + "maxScreenAirTemp": 10.41, + "minScreenAirTemp": 10.17, + "max10mWindGust": 16.04, + "significantWeatherCode": 7, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "windSpeed10m": 7.58, + "windDirectionFrom10m": 229, + "windGustSpeed10m": 14.32, + "visibility": 21127, + "mslp": 102515, + "screenRelativeHumidity": 85.35, + "feelsLikeTemp": 7.05, + "uvIndex": 1, + "probOfPrecipitation": 6, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 6, + "probOfHeavyRain": 1, + "probOfHail": 0, + "probOfSferics": 0 + }, { + "time": "2024-02-20T18:00Z", + "maxScreenAirTemp": 10.27, + "minScreenAirTemp": 9.66, + "max10mWindGust": 15.03, + "significantWeatherCode": 8, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "windSpeed10m": 7.12, + "windDirectionFrom10m": 228, + "windGustSpeed10m": 13.37, + "visibility": 17884, + "mslp": 102364, + "screenRelativeHumidity": 89.82, + "feelsLikeTemp": 6.52, + "uvIndex": 0, + "probOfPrecipitation": 13, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 13, + "probOfHeavyRain": 3, + "probOfHail": 0, + "probOfSferics": 0 + }, { + "time": "2024-02-20T21:00Z", + "maxScreenAirTemp": 9.74, + "minScreenAirTemp": 9.68, + "max10mWindGust": 15.84, + "significantWeatherCode": 8, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "windSpeed10m": 6.94, + "windDirectionFrom10m": 231, + "windGustSpeed10m": 12.99, + "visibility": 17806, + "mslp": 102220, + "screenRelativeHumidity": 90.71, + "feelsLikeTemp": 6.58, + "uvIndex": 0, + "probOfPrecipitation": 19, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 19, + "probOfHeavyRain": 8, + "probOfHail": 1, + "probOfSferics": 1 + }, { + "time": "2024-02-21T00:00Z", + "maxScreenAirTemp": 9.8, + "minScreenAirTemp": 9.54, + "max10mWindGust": 13.74, + "significantWeatherCode": 8, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "windSpeed10m": 6.4, + "windDirectionFrom10m": 232, + "windGustSpeed10m": 11.87, + "visibility": 15050, + "mslp": 102019, + "screenRelativeHumidity": 92.53, + "feelsLikeTemp": 6.67, + "uvIndex": 0, + "probOfPrecipitation": 26, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 26, + "probOfHeavyRain": 15, + "probOfHail": 2, + "probOfSferics": 1 + }, { + "time": "2024-02-21T03:00Z", + "maxScreenAirTemp": 9.62, + "minScreenAirTemp": 9.31, + "max10mWindGust": 13.58, + "significantWeatherCode": 12, + "totalPrecipAmount": 0.47, + "totalSnowAmount": 0, + "windSpeed10m": 5.9, + "windDirectionFrom10m": 228, + "windGustSpeed10m": 10.84, + "visibility": 10455, + "mslp": 101768, + "screenRelativeHumidity": 93.57, + "feelsLikeTemp": 6.52, + "uvIndex": 0, + "probOfPrecipitation": 55, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 55, + "probOfHeavyRain": 32, + "probOfHail": 2, + "probOfSferics": 1 + }, { + "time": "2024-02-21T06:00Z", + "maxScreenAirTemp": 9.34, + "minScreenAirTemp": 8.97, + "max10mWindGust": 12.06, + "significantWeatherCode": 15, + "totalPrecipAmount": 1.04, + "totalSnowAmount": 0, + "windSpeed10m": 6.04, + "windDirectionFrom10m": 213, + "windGustSpeed10m": 11.11, + "visibility": 7726, + "mslp": 101529, + "screenRelativeHumidity": 93.56, + "feelsLikeTemp": 6.11, + "uvIndex": 0, + "probOfPrecipitation": 84, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 84, + "probOfHeavyRain": 79, + "probOfHail": 15, + "probOfSferics": 8 + }, { + "time": "2024-02-21T09:00Z", + "maxScreenAirTemp": 9.34, + "minScreenAirTemp": 9.02, + "max10mWindGust": 13.95, + "significantWeatherCode": 15, + "totalPrecipAmount": 2.02, + "totalSnowAmount": 0, + "windSpeed10m": 7.31, + "windDirectionFrom10m": 207, + "windGustSpeed10m": 13.69, + "visibility": 6769, + "mslp": 101264, + "screenRelativeHumidity": 92.54, + "feelsLikeTemp": 6.09, + "uvIndex": 1, + "probOfPrecipitation": 85, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 85, + "probOfHeavyRain": 82, + "probOfHail": 16, + "probOfSferics": 8 + }, { + "time": "2024-02-21T12:00Z", + "maxScreenAirTemp": 9.96, + "minScreenAirTemp": 9.32, + "max10mWindGust": 16.28, + "significantWeatherCode": 15, + "totalPrecipAmount": 2.06, + "totalSnowAmount": 0, + "windSpeed10m": 8.38, + "windDirectionFrom10m": 206, + "windGustSpeed10m": 15.83, + "visibility": 7351, + "mslp": 100933, + "screenRelativeHumidity": 90.56, + "feelsLikeTemp": 6.52, + "uvIndex": 1, + "probOfPrecipitation": 86, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 86, + "probOfHeavyRain": 82, + "probOfHail": 16, + "probOfSferics": 8 + }, { + "time": "2024-02-21T15:00Z", + "maxScreenAirTemp": 10.39, + "minScreenAirTemp": 9.93, + "max10mWindGust": 17.58, + "significantWeatherCode": 15, + "totalPrecipAmount": 1.85, + "totalSnowAmount": 0, + "windSpeed10m": 8.41, + "windDirectionFrom10m": 211, + "windGustSpeed10m": 15.81, + "visibility": 8521, + "mslp": 100550, + "screenRelativeHumidity": 90.45, + "feelsLikeTemp": 6.89, + "uvIndex": 1, + "probOfPrecipitation": 83, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 83, + "probOfHeavyRain": 78, + "probOfHail": 15, + "probOfSferics": 8 + }, { + "time": "2024-02-21T18:00Z", + "maxScreenAirTemp": 10.33, + "minScreenAirTemp": 10.08, + "max10mWindGust": 17.02, + "significantWeatherCode": 15, + "totalPrecipAmount": 1.55, + "totalSnowAmount": 0, + "windSpeed10m": 7.78, + "windDirectionFrom10m": 215, + "windGustSpeed10m": 14.46, + "visibility": 9682, + "mslp": 100328, + "screenRelativeHumidity": 92.32, + "feelsLikeTemp": 6.98, + "uvIndex": 0, + "probOfPrecipitation": 78, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 78, + "probOfHeavyRain": 73, + "probOfHail": 14, + "probOfSferics": 7 + }, { + "time": "2024-02-21T21:00Z", + "maxScreenAirTemp": 10.24, + "minScreenAirTemp": 10.14, + "max10mWindGust": 16.36, + "significantWeatherCode": 12, + "totalPrecipAmount": 0.96, + "totalSnowAmount": 0, + "windSpeed10m": 6.97, + "windDirectionFrom10m": 226, + "windGustSpeed10m": 12.85, + "visibility": 12699, + "mslp": 100164, + "screenRelativeHumidity": 93.13, + "feelsLikeTemp": 7.16, + "uvIndex": 0, + "probOfPrecipitation": 52, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 52, + "probOfHeavyRain": 33, + "probOfHail": 3, + "probOfSferics": 2 + }, { + "time": "2024-02-22T00:00Z", + "maxScreenAirTemp": 10.18, + "minScreenAirTemp": 9.7, + "max10mWindGust": 14.35, + "significantWeatherCode": 8, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "windSpeed10m": 6.06, + "windDirectionFrom10m": 228, + "windGustSpeed10m": 11.11, + "visibility": 11329, + "mslp": 99968, + "screenRelativeHumidity": 93.31, + "feelsLikeTemp": 6.98, + "uvIndex": 0, + "probOfPrecipitation": 23, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 23, + "probOfHeavyRain": 12, + "probOfHail": 2, + "probOfSferics": 1 + }, { + "time": "2024-02-22T03:00Z", + "maxScreenAirTemp": 9.77, + "minScreenAirTemp": 9.47, + "max10mWindGust": 13.64, + "significantWeatherCode": 12, + "totalPrecipAmount": 0.59, + "totalSnowAmount": 0, + "windSpeed10m": 5.82, + "windDirectionFrom10m": 222, + "windGustSpeed10m": 10.68, + "visibility": 11002, + "mslp": 99689, + "screenRelativeHumidity": 93.29, + "feelsLikeTemp": 6.78, + "uvIndex": 0, + "probOfPrecipitation": 46, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 46, + "probOfHeavyRain": 25, + "probOfHail": 1, + "probOfSferics": 1 + }, { + "time": "2024-02-22T06:00Z", + "maxScreenAirTemp": 9.69, + "minScreenAirTemp": 9.45, + "max10mWindGust": 12.65, + "significantWeatherCode": 8, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "windSpeed10m": 6.02, + "windDirectionFrom10m": 211, + "windGustSpeed10m": 11.2, + "visibility": 10456, + "mslp": 99392, + "screenRelativeHumidity": 93.57, + "feelsLikeTemp": 6.72, + "uvIndex": 0, + "probOfPrecipitation": 24, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 24, + "probOfHeavyRain": 14, + "probOfHail": 2, + "probOfSferics": 1 + }, { + "time": "2024-02-22T09:00Z", + "maxScreenAirTemp": 9.84, + "minScreenAirTemp": 9.53, + "max10mWindGust": 14.46, + "significantWeatherCode": 15, + "totalPrecipAmount": 1.11, + "totalSnowAmount": 0, + "windSpeed10m": 6.77, + "windDirectionFrom10m": 207, + "windGustSpeed10m": 12.65, + "visibility": 10933, + "mslp": 99076, + "screenRelativeHumidity": 92.04, + "feelsLikeTemp": 6.78, + "uvIndex": 1, + "probOfPrecipitation": 71, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 71, + "probOfHeavyRain": 67, + "probOfHail": 13, + "probOfSferics": 7 + }, { + "time": "2024-02-22T12:00Z", + "maxScreenAirTemp": 10.49, + "minScreenAirTemp": 9.8, + "max10mWindGust": 16.89, + "significantWeatherCode": 14, + "totalPrecipAmount": 1.44, + "totalSnowAmount": 0, + "windSpeed10m": 8.32, + "windDirectionFrom10m": 215, + "windGustSpeed10m": 15.92, + "visibility": 13595, + "mslp": 98762, + "screenRelativeHumidity": 87.24, + "feelsLikeTemp": 6.86, + "uvIndex": 1, + "probOfPrecipitation": 62, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 61, + "probOfHeavyRain": 57, + "probOfHail": 12, + "probOfSferics": 11 + }, { + "time": "2024-02-22T15:00Z", + "maxScreenAirTemp": 10.31, + "minScreenAirTemp": 9.72, + "max10mWindGust": 19.52, + "significantWeatherCode": 14, + "totalPrecipAmount": 1.48, + "totalSnowAmount": 0, + "windSpeed10m": 9.04, + "windDirectionFrom10m": 223, + "windGustSpeed10m": 17.18, + "visibility": 14585, + "mslp": 98434, + "screenRelativeHumidity": 82.96, + "feelsLikeTemp": 6.07, + "uvIndex": 1, + "probOfPrecipitation": 62, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 61, + "probOfHeavyRain": 57, + "probOfHail": 12, + "probOfSferics": 12 + }, { + "time": "2024-02-22T18:00Z", + "maxScreenAirTemp": 9.82, + "minScreenAirTemp": 7.61, + "max10mWindGust": 19.28, + "significantWeatherCode": 9, + "totalPrecipAmount": 1.11, + "totalSnowAmount": 0, + "windSpeed10m": 7.91, + "windDirectionFrom10m": 239, + "windGustSpeed10m": 14.42, + "visibility": 20148, + "mslp": 98434, + "screenRelativeHumidity": 79.36, + "feelsLikeTemp": 3.74, + "uvIndex": 0, + "probOfPrecipitation": 44, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 44, + "probOfHeavyRain": 32, + "probOfHail": 4, + "probOfSferics": 6 + }, { + "time": "2024-02-22T21:00Z", + "maxScreenAirTemp": 7.69, + "minScreenAirTemp": 6.05, + "max10mWindGust": 16.16, + "significantWeatherCode": 0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "windSpeed10m": 7.38, + "windDirectionFrom10m": 249, + "windGustSpeed10m": 13.14, + "visibility": 23784, + "mslp": 98597, + "screenRelativeHumidity": 77.6, + "feelsLikeTemp": 1.88, + "uvIndex": 0, + "probOfPrecipitation": 12, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 12, + "probOfHeavyRain": 10, + "probOfHail": 2, + "probOfSferics": 1 + }, { + "time": "2024-02-23T00:00Z", + "maxScreenAirTemp": 6.1, + "minScreenAirTemp": 5.22, + "max10mWindGust": 14.6, + "significantWeatherCode": 0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "windSpeed10m": 6.61, + "windDirectionFrom10m": 245, + "windGustSpeed10m": 11.58, + "visibility": 25279, + "mslp": 98742, + "screenRelativeHumidity": 77.46, + "feelsLikeTemp": 1.2, + "uvIndex": 0, + "probOfPrecipitation": 8, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 8, + "probOfHeavyRain": 5, + "probOfHail": 1, + "probOfSferics": 1 + }, { + "time": "2024-02-23T03:00Z", + "maxScreenAirTemp": 5.42, + "minScreenAirTemp": 5.18, + "max10mWindGust": 13.07, + "significantWeatherCode": 0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "windSpeed10m": 6.22, + "windDirectionFrom10m": 237, + "windGustSpeed10m": 10.91, + "visibility": 24397, + "mslp": 98771, + "screenRelativeHumidity": 79.47, + "feelsLikeTemp": 1.25, + "uvIndex": 0, + "probOfPrecipitation": 5, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 5, + "probOfHeavyRain": 4, + "probOfHail": 1, + "probOfSferics": 1 + }, { + "time": "2024-02-23T06:00Z", + "maxScreenAirTemp": 5.39, + "minScreenAirTemp": 5.13, + "max10mWindGust": 12.51, + "significantWeatherCode": 7, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "windSpeed10m": 5.96, + "windDirectionFrom10m": 232, + "windGustSpeed10m": 10.43, + "visibility": 23385, + "mslp": 98811, + "screenRelativeHumidity": 81.65, + "feelsLikeTemp": 1.36, + "uvIndex": 0, + "probOfPrecipitation": 10, + "probOfSnow": 1, + "probOfHeavySnow": 0, + "probOfRain": 10, + "probOfHeavyRain": 5, + "probOfHail": 1, + "probOfSferics": 1 + }, { + "time": "2024-02-23T09:00Z", + "maxScreenAirTemp": 6.69, + "minScreenAirTemp": 5.26, + "max10mWindGust": 12.62, + "significantWeatherCode": 1, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "windSpeed10m": 6.33, + "windDirectionFrom10m": 228, + "windGustSpeed10m": 11.6, + "visibility": 23165, + "mslp": 98890, + "screenRelativeHumidity": 78.08, + "feelsLikeTemp": 2.96, + "uvIndex": 1, + "probOfPrecipitation": 10, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 10, + "probOfHeavyRain": 6, + "probOfHail": 1, + "probOfSferics": 1 + }, { + "time": "2024-02-23T12:00Z", + "maxScreenAirTemp": 7.99, + "minScreenAirTemp": 6.64, + "max10mWindGust": 14.79, + "significantWeatherCode": 14, + "totalPrecipAmount": 0.95, + "totalSnowAmount": 0, + "windSpeed10m": 7.11, + "windDirectionFrom10m": 231, + "windGustSpeed10m": 13.38, + "visibility": 23049, + "mslp": 98974, + "screenRelativeHumidity": 73.25, + "feelsLikeTemp": 3.88, + "uvIndex": 2, + "probOfPrecipitation": 52, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 52, + "probOfHeavyRain": 48, + "probOfHail": 10, + "probOfSferics": 11 + }, { + "time": "2024-02-23T15:00Z", + "maxScreenAirTemp": 8.11, + "minScreenAirTemp": 7.61, + "max10mWindGust": 14.99, + "significantWeatherCode": 10, + "totalPrecipAmount": 0.67, + "totalSnowAmount": 0, + "windSpeed10m": 6.59, + "windDirectionFrom10m": 230, + "windGustSpeed10m": 12.43, + "visibility": 23824, + "mslp": 99018, + "screenRelativeHumidity": 71.06, + "feelsLikeTemp": 4.42, + "uvIndex": 1, + "probOfPrecipitation": 34, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 34, + "probOfHeavyRain": 20, + "probOfHail": 1, + "probOfSferics": 4 + }, { + "time": "2024-02-23T18:00Z", + "maxScreenAirTemp": 7.88, + "minScreenAirTemp": 6.36, + "max10mWindGust": 13.18, + "significantWeatherCode": 0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "windSpeed10m": 5.1, + "windDirectionFrom10m": 228, + "windGustSpeed10m": 8.88, + "visibility": 24021, + "mslp": 99142, + "screenRelativeHumidity": 78.49, + "feelsLikeTemp": 3.13, + "uvIndex": 0, + "probOfPrecipitation": 7, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 7, + "probOfHeavyRain": 4, + "probOfHail": 0, + "probOfSferics": 1 + }, { + "time": "2024-02-23T21:00Z", + "maxScreenAirTemp": 6.4, + "minScreenAirTemp": 5.49, + "max10mWindGust": 9.53, + "significantWeatherCode": 7, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "windSpeed10m": 4.55, + "windDirectionFrom10m": 233, + "windGustSpeed10m": 7.87, + "visibility": 23349, + "mslp": 99270, + "screenRelativeHumidity": 82.54, + "feelsLikeTemp": 2.42, + "uvIndex": 0, + "probOfPrecipitation": 9, + "probOfSnow": 1, + "probOfHeavySnow": 0, + "probOfRain": 9, + "probOfHeavyRain": 4, + "probOfHail": 0, + "probOfSferics": 1 + }, { + "time": "2024-02-24T00:00Z", + "maxScreenAirTemp": 5.59, + "minScreenAirTemp": 4.77, + "max10mWindGust": 8.74, + "significantWeatherCode": 0, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "windSpeed10m": 4.2, + "windDirectionFrom10m": 232, + "windGustSpeed10m": 7.16, + "visibility": 22325, + "mslp": 99365, + "screenRelativeHumidity": 85.83, + "feelsLikeTemp": 1.8, + "uvIndex": 0, + "probOfPrecipitation": 7, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 6, + "probOfHeavyRain": 4, + "probOfHail": 0, + "probOfSferics": 1 + }, { + "time": "2024-02-24T03:00Z", + "maxScreenAirTemp": 4.92, + "minScreenAirTemp": 4.42, + "max10mWindGust": 8.5, + "significantWeatherCode": 7, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "windSpeed10m": 4.23, + "windDirectionFrom10m": 238, + "windGustSpeed10m": 7.16, + "visibility": 20772, + "mslp": 99430, + "screenRelativeHumidity": 87.91, + "feelsLikeTemp": 1.28, + "uvIndex": 0, + "probOfPrecipitation": 8, + "probOfSnow": 1, + "probOfHeavySnow": 1, + "probOfRain": 8, + "probOfHeavyRain": 4, + "probOfHail": 1, + "probOfSferics": 1 + }, { + "time": "2024-02-24T06:00Z", + "maxScreenAirTemp": 4.49, + "minScreenAirTemp": 3.86, + "max10mWindGust": 8.07, + "significantWeatherCode": 2, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "windSpeed10m": 4.09, + "windDirectionFrom10m": 235, + "windGustSpeed10m": 6.85, + "visibility": 20467, + "mslp": 99535, + "screenRelativeHumidity": 89.35, + "feelsLikeTemp": 0.74, + "uvIndex": 0, + "probOfPrecipitation": 7, + "probOfSnow": 1, + "probOfHeavySnow": 1, + "probOfRain": 6, + "probOfHeavyRain": 3, + "probOfHail": 1, + "probOfSferics": 1 + }, { + "time": "2024-02-24T09:00Z", + "maxScreenAirTemp": 6.32, + "minScreenAirTemp": 3.95, + "max10mWindGust": 8.97, + "significantWeatherCode": 7, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "windSpeed10m": 4.86, + "windDirectionFrom10m": 236, + "windGustSpeed10m": 8.91, + "visibility": 23436, + "mslp": 99655, + "screenRelativeHumidity": 81.66, + "feelsLikeTemp": 3.25, + "uvIndex": 1, + "probOfPrecipitation": 9, + "probOfSnow": 1, + "probOfHeavySnow": 1, + "probOfRain": 9, + "probOfHeavyRain": 4, + "probOfHail": 0, + "probOfSferics": 1 + }, { + "time": "2024-02-24T12:00Z", + "maxScreenAirTemp": 8.36, + "minScreenAirTemp": 6.32, + "max10mWindGust": 11.98, + "significantWeatherCode": 7, + "totalPrecipAmount": 0.0, + "totalSnowAmount": 0, + "windSpeed10m": 5.91, + "windDirectionFrom10m": 234, + "windGustSpeed10m": 11.62, + "visibility": 22370, + "mslp": 99735, + "screenRelativeHumidity": 70.47, + "feelsLikeTemp": 5.17, + "uvIndex": 2, + "probOfPrecipitation": 13, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 13, + "probOfHeavyRain": 6, + "probOfHail": 1, + "probOfSferics": 1 + }, { + "time": "2024-02-24T15:00Z", + "maxScreenAirTemp": 8.25, + "minScreenAirTemp": 7.96, + "max10mWindGust": 12.77, + "significantWeatherCode": 10, + "totalPrecipAmount": 0.58, + "totalSnowAmount": 0, + "windSpeed10m": 5.98, + "windDirectionFrom10m": 228, + "windGustSpeed10m": 11.48, + "visibility": 22483, + "mslp": 99725, + "screenRelativeHumidity": 72.01, + "feelsLikeTemp": 4.97, + "uvIndex": 1, + "probOfPrecipitation": 32, + "probOfSnow": 0, + "probOfHeavySnow": 0, + "probOfRain": 32, + "probOfHeavyRain": 17, + "probOfHail": 1, + "probOfSferics": 3 + }] + } + }] +} diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/test_forecast.py b/tests/unit/test_forecast.py index d84a09c..2a498b1 100644 --- a/tests/unit/test_forecast.py +++ b/tests/unit/test_forecast.py @@ -1,112 +1,273 @@ import datetime -import datapoint -import unittest +import geojson +import pytest -class TestForecast(unittest.TestCase): +import tests.reference_data.reference_data_test_forecast as reference_data_test_forecast +from datapoint import Forecast +from datapoint.exceptions import APIException - def setUp(self): - self.forecast_3hrly = datapoint.Forecast.Forecast(frequency='3hourly') +# TODO look into pytest-cases. Should reduce the amount of stored data structures - test_day_0 = datapoint.Day.Day() - test_day_0.date = datetime.datetime(2020, 3, 3, tzinfo=datetime.timezone.utc) - for i in range(5): - ts = datapoint.Timestep.Timestep() - ts.name = 9+(3*i) - ts.date = datetime.datetime(2020, 3, 3, 9+(3*i), tzinfo=datetime.timezone.utc) - test_day_0.timesteps.append(ts) +@pytest.fixture +def load_hourly_json(): + with open("./tests/reference_data/hourly_api_data.json") as f: + my_json = geojson.load(f) + return my_json - test_day_1 = datapoint.Day.Day() - test_day_1.date = datetime.datetime(2020, 3, 4, tzinfo=datetime.timezone.utc) - for i in range(8): - ts = datapoint.Timestep.Timestep() - ts.name = 3*i - ts.date = datetime.datetime(2020, 3, 4, 3*i, tzinfo=datetime.timezone.utc) - test_day_1.timesteps.append(ts) +@pytest.fixture +def load_daily_json(): + with open("./tests/reference_data/daily_api_data.json") as f: + my_json = geojson.load(f) + return my_json - self.forecast_3hrly.days.append(test_day_0) - self.forecast_3hrly.days.append(test_day_1) - test_day_0 = datapoint.Day.Day() - test_day_0.date = datetime.datetime(2020, 3, 3, tzinfo=datetime.timezone.utc) +@pytest.fixture +def load_three_hourly_json(): + with open("./tests/reference_data/three_hourly_api_data.json") as f: + my_json = geojson.load(f) + return my_json - for i in range(2): - ts = datapoint.Timestep.Timestep() - ts.name = 2*i - ts.date = datetime.datetime(2020, 3, 3, 2*i, tzinfo=datetime.timezone.utc) - test_day_0.timesteps.append(ts) - test_day_1 = datapoint.Day.Day() - test_day_1.date = datetime.datetime(2020, 3, 4, tzinfo=datetime.timezone.utc) +@pytest.fixture +def daily_forecast(load_daily_json): + return Forecast.Forecast("daily", load_daily_json) - for i in range(2): - ts = datapoint.Timestep.Timestep() - ts.name = 2*i - ts.date = datetime.datetime(2020, 3, 4, 2*i, tzinfo=datetime.timezone.utc) - test_day_1.timesteps.append(ts) - self.forecast_daily = datapoint.Forecast.Forecast(frequency='daily') +@pytest.fixture +def hourly_forecast(load_hourly_json): + return Forecast.Forecast("hourly", load_hourly_json) - self.forecast_daily.days.append(test_day_0) - self.forecast_daily.days.append(test_day_1) - def test_at_datetime_1_5_hours_before_after(self): +@pytest.fixture +def three_hourly_forecast(load_three_hourly_json): + return Forecast.Forecast("three-hourly", load_three_hourly_json) - target_before = datetime.datetime(2020, 3, 3, 7, 0, - tzinfo=datetime.timezone.utc) - target_after = datetime.datetime(2020, 3, 4, 23, 0, - tzinfo=datetime.timezone.utc) +@pytest.fixture +def hourly_first_forecast_and_parameters(load_hourly_json): + parameters = load_hourly_json["parameters"][0] + forecast = load_hourly_json["features"][0]["properties"]["timeSeries"][0] + return (forecast, parameters) - self.assertRaises(datapoint.exceptions.APIException, - self.forecast_3hrly.at_datetime, target_before) - self.assertRaises(datapoint.exceptions.APIException, - self.forecast_3hrly.at_datetime, target_after) +@pytest.fixture +def three_hourly_first_forecast_and_parameters(load_three_hourly_json): + parameters = load_three_hourly_json["parameters"][0] + forecast = load_three_hourly_json["features"][0]["properties"]["timeSeries"][0] + return (forecast, parameters) - def test_at_datetime_6_hours_before_after(self): - # Generate 2 timesteps These are set at 00:00 and 12:00 +@pytest.fixture +def expected_first_hourly_timestep(): + return reference_data_test_forecast.EXPECTED_FIRST_HOURLY_TIMESTEP - target_before = datetime.datetime(2020, 3, 2, 15, 0, - tzinfo=datetime.timezone.utc) - target_after = datetime.datetime(2020, 3, 6, 7, 0, - tzinfo=datetime.timezone.utc) +@pytest.fixture +def expected_at_datetime_hourly_timestep(): + return reference_data_test_forecast.EXPECTED_AT_DATETIME_HOURLY_TIMESTEP - self.assertRaises(datapoint.exceptions.APIException, - self.forecast_daily.at_datetime, target_before) - self.assertRaises(datapoint.exceptions.APIException, - self.forecast_daily.at_datetime, target_after) +@pytest.fixture +def expected_at_datetime_hourly_final_timestep(): + return reference_data_test_forecast.EXPECTED_AT_DATETIME_HOURLY_FINAL_TIMESTEP - def test_normal_time(self): - target = datetime.datetime(2020, 3, 3, 10, 0, - tzinfo=datetime.timezone.utc) - nearest = self.forecast_3hrly.at_datetime(target) - expected = datetime.datetime(2020, 3, 3, 9, - tzinfo=datetime.timezone.utc) - self.assertEqual(nearest.date, expected) +@pytest.fixture +def expected_first_daily_timestep(): + return reference_data_test_forecast.EXPECTED_FIRST_DAILY_TIMESTEP - target = datetime.datetime(2020, 3, 3, 11, 0, - tzinfo=datetime.timezone.utc) - nearest = self.forecast_3hrly.at_datetime(target) - expected = datetime.datetime(2020, 3, 3, 12, - tzinfo=datetime.timezone.utc) - self.assertEqual(nearest.date, expected) +@pytest.fixture +def expected_at_datetime_daily_timestep(): + return reference_data_test_forecast.EXPECTED_AT_DATETIME_DAILY_TIMESTEP - def test_forecase_midnight(self): - target = datetime.datetime(2020, 3, 4, 0, 15, - tzinfo=datetime.timezone.utc) +@pytest.fixture +def expected_at_datetime_daily_final_timestep(): + return reference_data_test_forecast.EXPECTED_AT_DATETIME_DAILY_FINAL_TIMESTEP - nearest = self.forecast_3hrly.at_datetime(target) - expected = datetime.datetime(2020, 3, 4, 0, - tzinfo=datetime.timezone.utc) - self.assertEqual(nearest.date, expected) +@pytest.fixture +def expected_first_three_hourly_timestep(): + return reference_data_test_forecast.EXPECTED_FIRST_THREE_HOURLY_TIMESTEP + +@pytest.fixture +def expected_at_datetime_three_hourly_timestep(): + return reference_data_test_forecast.EXPECTED_AT_DATETIME_THREE_HOURLY_TIMESTEP + + +@pytest.fixture +def expected_at_datetime_three_hourly_final_timestep(): + return reference_data_test_forecast.EXPECTED_AT_DATETIME_THREE_HOURLY_FINAL_TIMESTEP + + +class TestHourlyForecast: + def test_forecast_frequency(self, hourly_forecast): + assert hourly_forecast.frequency == "hourly" + + def test_forecast_location_name(self, hourly_forecast): + assert hourly_forecast.name == "Sheffield Park" + + def test_forecast_location_latitude(self, hourly_forecast): + assert hourly_forecast.forecast_latitude == 50.9992 + + def test_forecast_location_longitude(self, hourly_forecast): + assert hourly_forecast.forecast_longitude == 0.0154 + + def test_forecast_distance_from_request(self, hourly_forecast): + assert hourly_forecast.distance_from_requested_location == 1081.5349 + + def test_forecast_elevation(self, hourly_forecast): + assert hourly_forecast.elevation == 37.0 + + def test_forecast_first_timestep( + self, hourly_forecast, expected_first_hourly_timestep + ): + assert hourly_forecast.timesteps[0] == expected_first_hourly_timestep + + def test_build_timestep( + self, + hourly_forecast, + hourly_first_forecast_and_parameters, + expected_first_hourly_timestep, + ): + built_timestep = hourly_forecast._build_timestep( + hourly_first_forecast_and_parameters[0], + hourly_first_forecast_and_parameters[1], + ) + + assert built_timestep == expected_first_hourly_timestep + + def test_at_datetime(self, hourly_forecast, expected_at_datetime_hourly_timestep): + ts = hourly_forecast.at_datetime(datetime.datetime(2024, 2, 16, 19, 15)) + assert ts == expected_at_datetime_hourly_timestep + + def test_at_datetime_final_timestamp( + self, hourly_forecast, expected_at_datetime_hourly_final_timestep + ): + ts = hourly_forecast.at_datetime(datetime.datetime(2024, 2, 17, 19, 20)) + assert ts == expected_at_datetime_hourly_final_timestep + + def test_requested_time_too_early(self, hourly_forecast): + with pytest.raises(APIException): + hourly_forecast.at_datetime(datetime.datetime(2024, 2, 15, 18, 25)) + + def test_requested_time_too_late(self, hourly_forecast): + with pytest.raises(APIException): + hourly_forecast.at_datetime(datetime.datetime(2024, 2, 17, 19, 35)) + + +class TestDailyForecast: + def test_forecast_frequency(self, daily_forecast): + assert daily_forecast.frequency == "daily" + + def test_forecast_location_name(self, daily_forecast): + assert daily_forecast.name == "Sheffield Park" + + def test_forecast_location_latitude(self, daily_forecast): + assert daily_forecast.forecast_latitude == 50.9992 + + def test_forecast_location_longitude(self, daily_forecast): + assert daily_forecast.forecast_longitude == 0.0154 + + def test_forecast_distance_from_request(self, daily_forecast): + assert daily_forecast.distance_from_requested_location == 1081.5349 + + def test_forecast_elevation(self, daily_forecast): + assert daily_forecast.elevation == 37.0 + + def test_forecast_first_timestep( + self, daily_forecast, expected_first_daily_timestep + ): + assert daily_forecast.timesteps[0] == expected_first_daily_timestep + + def test_build_timesteps_from_daily( + self, daily_forecast, load_daily_json, expected_first_daily_timestep + ): + timesteps = daily_forecast._build_timesteps_from_daily( + load_daily_json["features"][0]["properties"]["timeSeries"], + load_daily_json["parameters"][0], + ) + assert timesteps[0] == expected_first_daily_timestep + + def test_at_datetime(self, daily_forecast, expected_at_datetime_daily_timestep): + ts = daily_forecast.at_datetime(datetime.datetime(2024, 2, 16, 19, 15)) + assert ts == expected_at_datetime_daily_timestep + + def test_at_datetime_final_timestamp( + self, daily_forecast, expected_at_datetime_daily_final_timestep + ): + ts = daily_forecast.at_datetime(datetime.datetime(2024, 2, 23, 17)) + assert ts == expected_at_datetime_daily_final_timestep + + def test_requested_time_too_early(self, daily_forecast): + with pytest.raises(APIException): + daily_forecast.at_datetime(datetime.datetime(2024, 2, 15, 17)) + + def test_requested_time_too_late(self, daily_forecast): + with pytest.raises(APIException): + daily_forecast.at_datetime(datetime.datetime(2024, 2, 23, 19)) + + +class TestThreeHourlyForecast: + def test_forecast_frequency(self, three_hourly_forecast): + assert three_hourly_forecast.frequency == "three-hourly" + + def test_forecast_location_name(self, three_hourly_forecast): + assert three_hourly_forecast.name == "Sheffield Park" + + def test_forecast_location_latitude(self, three_hourly_forecast): + assert three_hourly_forecast.forecast_latitude == 50.9992 + + def test_forecast_location_longitude(self, three_hourly_forecast): + assert three_hourly_forecast.forecast_longitude == 0.0154 + + def test_forecast_distance_from_request(self, three_hourly_forecast): + assert three_hourly_forecast.distance_from_requested_location == 1081.5349 + + def test_forecast_elevation(self, three_hourly_forecast): + assert three_hourly_forecast.elevation == 37.0 + + def test_forecast_first_timestep( + self, three_hourly_forecast, expected_first_three_hourly_timestep + ): + assert ( + three_hourly_forecast.timesteps[0] == expected_first_three_hourly_timestep + ) + + def test_build_timestep( + self, + three_hourly_forecast, + three_hourly_first_forecast_and_parameters, + expected_first_three_hourly_timestep, + ): + built_timestep = three_hourly_forecast._build_timestep( + three_hourly_first_forecast_and_parameters[0], + three_hourly_first_forecast_and_parameters[1], + ) + + assert built_timestep == expected_first_three_hourly_timestep + + def test_at_datetime( + self, three_hourly_forecast, expected_at_datetime_three_hourly_timestep + ): + ts = three_hourly_forecast.at_datetime(datetime.datetime(2024, 2, 22, 19, 15)) + assert ts == expected_at_datetime_three_hourly_timestep + + def test_at_datetime_final_timestamp( + self, three_hourly_forecast, expected_at_datetime_three_hourly_final_timestep + ): + ts = three_hourly_forecast.at_datetime(datetime.datetime(2024, 2, 24, 16)) + assert ts == expected_at_datetime_three_hourly_final_timestep + + def test_requested_time_too_early(self, three_hourly_forecast): + with pytest.raises(APIException): + three_hourly_forecast.at_datetime(datetime.datetime(2024, 2, 17, 13, 20)) + + def test_requested_time_too_late(self, three_hourly_forecast): + with pytest.raises(APIException): + three_hourly_forecast.at_datetime(datetime.datetime(2024, 2, 24, 17)) diff --git a/tests/unit/test_manager.py b/tests/unit/test_manager.py deleted file mode 100644 index bad677a..0000000 --- a/tests/unit/test_manager.py +++ /dev/null @@ -1,29 +0,0 @@ -import unittest -import datapoint - -class ManagerTestCase(unittest.TestCase): - - def setUp(self): - self.manager = datapoint.Manager(api_key="") - - def test_weather_to_text_is_string(self): - weather_text = self.manager._weather_to_text(0) - self.assertIsInstance(weather_text, type("")) - - def test_weather_to_text_invalid_input_None(self): - self.assertRaises(ValueError, self.manager._weather_to_text, None) - - def test_weather_to_text_invalid_input_out_of_bounds(self): - self.assertRaises(ValueError, self.manager._weather_to_text, 31) - - def test_weather_to_text_invalid_input_String(self): - self.assertRaises(ValueError, self.manager._weather_to_text, '1') - - def test_visbility_to_text_invalid_input_None(self): - self.assertRaises(ValueError, self.manager._weather_to_text, None) - - def test_visibility_to_text_invalid_input_out_of_bounds(self): - self.assertRaises(ValueError, self.manager._weather_to_text, -1) - - def test_visibility_to_text_invalid_input_String(self): - self.assertRaises(ValueError, self.manager._weather_to_text, '1') diff --git a/tests/unit/test_observation.py b/tests/unit/test_observation.py deleted file mode 100644 index a5d0251..0000000 --- a/tests/unit/test_observation.py +++ /dev/null @@ -1,23 +0,0 @@ -import unittest -import datetime -import datapoint - -class ObservationTestCase(unittest.TestCase): - - def setUp(self): - self.observation = datapoint.Observation.Observation() - - def test_observation(self): - # Just copy the forecast test - test_day = datapoint.Day.Day() - test_day.date = datetime.datetime.utcnow() - - test_timestep = datapoint.Timestep.Timestep() - test_timestep.name = 1 - - test_day.timesteps.append(test_timestep) - - self.observation.days.append(test_day) - - # What is this asserting? - assert self.observation.now() diff --git a/versioneer.py b/versioneer.py deleted file mode 100644 index 1e3753e..0000000 --- a/versioneer.py +++ /dev/null @@ -1,2277 +0,0 @@ - -# Version: 0.29 - -"""The Versioneer - like a rocketeer, but for versions. - -The Versioneer -============== - -* like a rocketeer, but for versions! -* https://github.com/python-versioneer/python-versioneer -* Brian Warner -* License: Public Domain (Unlicense) -* Compatible with: Python 3.7, 3.8, 3.9, 3.10, 3.11 and pypy3 -* [![Latest Version][pypi-image]][pypi-url] -* [![Build Status][travis-image]][travis-url] - -This is a tool for managing a recorded version number in setuptools-based -python projects. The goal is to remove the tedious and error-prone "update -the embedded version string" step from your release process. Making a new -release should be as easy as recording a new tag in your version-control -system, and maybe making new tarballs. - - -## Quick Install - -Versioneer provides two installation modes. The "classic" vendored mode installs -a copy of versioneer into your repository. The experimental build-time dependency mode -is intended to allow you to skip this step and simplify the process of upgrading. - -### Vendored mode - -* `pip install versioneer` to somewhere in your $PATH - * A [conda-forge recipe](https://github.com/conda-forge/versioneer-feedstock) is - available, so you can also use `conda install -c conda-forge versioneer` -* add a `[tool.versioneer]` section to your `pyproject.toml` or a - `[versioneer]` section to your `setup.cfg` (see [Install](INSTALL.md)) - * Note that you will need to add `tomli; python_version < "3.11"` to your - build-time dependencies if you use `pyproject.toml` -* run `versioneer install --vendor` in your source tree, commit the results -* verify version information with `python setup.py version` - -### Build-time dependency mode - -* `pip install versioneer` to somewhere in your $PATH - * A [conda-forge recipe](https://github.com/conda-forge/versioneer-feedstock) is - available, so you can also use `conda install -c conda-forge versioneer` -* add a `[tool.versioneer]` section to your `pyproject.toml` or a - `[versioneer]` section to your `setup.cfg` (see [Install](INSTALL.md)) -* add `versioneer` (with `[toml]` extra, if configuring in `pyproject.toml`) - to the `requires` key of the `build-system` table in `pyproject.toml`: - ```toml - [build-system] - requires = ["setuptools", "versioneer[toml]"] - build-backend = "setuptools.build_meta" - ``` -* run `versioneer install --no-vendor` in your source tree, commit the results -* verify version information with `python setup.py version` - -## Version Identifiers - -Source trees come from a variety of places: - -* a version-control system checkout (mostly used by developers) -* a nightly tarball, produced by build automation -* a snapshot tarball, produced by a web-based VCS browser, like github's - "tarball from tag" feature -* a release tarball, produced by "setup.py sdist", distributed through PyPI - -Within each source tree, the version identifier (either a string or a number, -this tool is format-agnostic) can come from a variety of places: - -* ask the VCS tool itself, e.g. "git describe" (for checkouts), which knows - about recent "tags" and an absolute revision-id -* the name of the directory into which the tarball was unpacked -* an expanded VCS keyword ($Id$, etc) -* a `_version.py` created by some earlier build step - -For released software, the version identifier is closely related to a VCS -tag. Some projects use tag names that include more than just the version -string (e.g. "myproject-1.2" instead of just "1.2"), in which case the tool -needs to strip the tag prefix to extract the version identifier. For -unreleased software (between tags), the version identifier should provide -enough information to help developers recreate the same tree, while also -giving them an idea of roughly how old the tree is (after version 1.2, before -version 1.3). Many VCS systems can report a description that captures this, -for example `git describe --tags --dirty --always` reports things like -"0.7-1-g574ab98-dirty" to indicate that the checkout is one revision past the -0.7 tag, has a unique revision id of "574ab98", and is "dirty" (it has -uncommitted changes). - -The version identifier is used for multiple purposes: - -* to allow the module to self-identify its version: `myproject.__version__` -* to choose a name and prefix for a 'setup.py sdist' tarball - -## Theory of Operation - -Versioneer works by adding a special `_version.py` file into your source -tree, where your `__init__.py` can import it. This `_version.py` knows how to -dynamically ask the VCS tool for version information at import time. - -`_version.py` also contains `$Revision$` markers, and the installation -process marks `_version.py` to have this marker rewritten with a tag name -during the `git archive` command. As a result, generated tarballs will -contain enough information to get the proper version. - -To allow `setup.py` to compute a version too, a `versioneer.py` is added to -the top level of your source tree, next to `setup.py` and the `setup.cfg` -that configures it. This overrides several distutils/setuptools commands to -compute the version when invoked, and changes `setup.py build` and `setup.py -sdist` to replace `_version.py` with a small static file that contains just -the generated version data. - -## Installation - -See [INSTALL.md](./INSTALL.md) for detailed installation instructions. - -## Version-String Flavors - -Code which uses Versioneer can learn about its version string at runtime by -importing `_version` from your main `__init__.py` file and running the -`get_versions()` function. From the "outside" (e.g. in `setup.py`), you can -import the top-level `versioneer.py` and run `get_versions()`. - -Both functions return a dictionary with different flavors of version -information: - -* `['version']`: A condensed version string, rendered using the selected - style. This is the most commonly used value for the project's version - string. The default "pep440" style yields strings like `0.11`, - `0.11+2.g1076c97`, or `0.11+2.g1076c97.dirty`. See the "Styles" section - below for alternative styles. - -* `['full-revisionid']`: detailed revision identifier. For Git, this is the - full SHA1 commit id, e.g. "1076c978a8d3cfc70f408fe5974aa6c092c949ac". - -* `['date']`: Date and time of the latest `HEAD` commit. For Git, it is the - commit date in ISO 8601 format. This will be None if the date is not - available. - -* `['dirty']`: a boolean, True if the tree has uncommitted changes. Note that - this is only accurate if run in a VCS checkout, otherwise it is likely to - be False or None - -* `['error']`: if the version string could not be computed, this will be set - to a string describing the problem, otherwise it will be None. It may be - useful to throw an exception in setup.py if this is set, to avoid e.g. - creating tarballs with a version string of "unknown". - -Some variants are more useful than others. Including `full-revisionid` in a -bug report should allow developers to reconstruct the exact code being tested -(or indicate the presence of local changes that should be shared with the -developers). `version` is suitable for display in an "about" box or a CLI -`--version` output: it can be easily compared against release notes and lists -of bugs fixed in various releases. - -The installer adds the following text to your `__init__.py` to place a basic -version in `YOURPROJECT.__version__`: - - from ._version import get_versions - __version__ = get_versions()['version'] - del get_versions - -## Styles - -The setup.cfg `style=` configuration controls how the VCS information is -rendered into a version string. - -The default style, "pep440", produces a PEP440-compliant string, equal to the -un-prefixed tag name for actual releases, and containing an additional "local -version" section with more detail for in-between builds. For Git, this is -TAG[+DISTANCE.gHEX[.dirty]] , using information from `git describe --tags ---dirty --always`. For example "0.11+2.g1076c97.dirty" indicates that the -tree is like the "1076c97" commit but has uncommitted changes (".dirty"), and -that this commit is two revisions ("+2") beyond the "0.11" tag. For released -software (exactly equal to a known tag), the identifier will only contain the -stripped tag, e.g. "0.11". - -Other styles are available. See [details.md](details.md) in the Versioneer -source tree for descriptions. - -## Debugging - -Versioneer tries to avoid fatal errors: if something goes wrong, it will tend -to return a version of "0+unknown". To investigate the problem, run `setup.py -version`, which will run the version-lookup code in a verbose mode, and will -display the full contents of `get_versions()` (including the `error` string, -which may help identify what went wrong). - -## Known Limitations - -Some situations are known to cause problems for Versioneer. This details the -most significant ones. More can be found on Github -[issues page](https://github.com/python-versioneer/python-versioneer/issues). - -### Subprojects - -Versioneer has limited support for source trees in which `setup.py` is not in -the root directory (e.g. `setup.py` and `.git/` are *not* siblings). The are -two common reasons why `setup.py` might not be in the root: - -* Source trees which contain multiple subprojects, such as - [Buildbot](https://github.com/buildbot/buildbot), which contains both - "master" and "slave" subprojects, each with their own `setup.py`, - `setup.cfg`, and `tox.ini`. Projects like these produce multiple PyPI - distributions (and upload multiple independently-installable tarballs). -* Source trees whose main purpose is to contain a C library, but which also - provide bindings to Python (and perhaps other languages) in subdirectories. - -Versioneer will look for `.git` in parent directories, and most operations -should get the right version string. However `pip` and `setuptools` have bugs -and implementation details which frequently cause `pip install .` from a -subproject directory to fail to find a correct version string (so it usually -defaults to `0+unknown`). - -`pip install --editable .` should work correctly. `setup.py install` might -work too. - -Pip-8.1.1 is known to have this problem, but hopefully it will get fixed in -some later version. - -[Bug #38](https://github.com/python-versioneer/python-versioneer/issues/38) is tracking -this issue. The discussion in -[PR #61](https://github.com/python-versioneer/python-versioneer/pull/61) describes the -issue from the Versioneer side in more detail. -[pip PR#3176](https://github.com/pypa/pip/pull/3176) and -[pip PR#3615](https://github.com/pypa/pip/pull/3615) contain work to improve -pip to let Versioneer work correctly. - -Versioneer-0.16 and earlier only looked for a `.git` directory next to the -`setup.cfg`, so subprojects were completely unsupported with those releases. - -### Editable installs with setuptools <= 18.5 - -`setup.py develop` and `pip install --editable .` allow you to install a -project into a virtualenv once, then continue editing the source code (and -test) without re-installing after every change. - -"Entry-point scripts" (`setup(entry_points={"console_scripts": ..})`) are a -convenient way to specify executable scripts that should be installed along -with the python package. - -These both work as expected when using modern setuptools. When using -setuptools-18.5 or earlier, however, certain operations will cause -`pkg_resources.DistributionNotFound` errors when running the entrypoint -script, which must be resolved by re-installing the package. This happens -when the install happens with one version, then the egg_info data is -regenerated while a different version is checked out. Many setup.py commands -cause egg_info to be rebuilt (including `sdist`, `wheel`, and installing into -a different virtualenv), so this can be surprising. - -[Bug #83](https://github.com/python-versioneer/python-versioneer/issues/83) describes -this one, but upgrading to a newer version of setuptools should probably -resolve it. - - -## Updating Versioneer - -To upgrade your project to a new release of Versioneer, do the following: - -* install the new Versioneer (`pip install -U versioneer` or equivalent) -* edit `setup.cfg` and `pyproject.toml`, if necessary, - to include any new configuration settings indicated by the release notes. - See [UPGRADING](./UPGRADING.md) for details. -* re-run `versioneer install --[no-]vendor` in your source tree, to replace - `SRC/_version.py` -* commit any changed files - -## Future Directions - -This tool is designed to make it easily extended to other version-control -systems: all VCS-specific components are in separate directories like -src/git/ . The top-level `versioneer.py` script is assembled from these -components by running make-versioneer.py . In the future, make-versioneer.py -will take a VCS name as an argument, and will construct a version of -`versioneer.py` that is specific to the given VCS. It might also take the -configuration arguments that are currently provided manually during -installation by editing setup.py . Alternatively, it might go the other -direction and include code from all supported VCS systems, reducing the -number of intermediate scripts. - -## Similar projects - -* [setuptools_scm](https://github.com/pypa/setuptools_scm/) - a non-vendored build-time - dependency -* [minver](https://github.com/jbweston/miniver) - a lightweight reimplementation of - versioneer -* [versioningit](https://github.com/jwodder/versioningit) - a PEP 518-based setuptools - plugin - -## License - -To make Versioneer easier to embed, all its code is dedicated to the public -domain. The `_version.py` that it creates is also in the public domain. -Specifically, both are released under the "Unlicense", as described in -https://unlicense.org/. - -[pypi-image]: https://img.shields.io/pypi/v/versioneer.svg -[pypi-url]: https://pypi.python.org/pypi/versioneer/ -[travis-image]: -https://img.shields.io/travis/com/python-versioneer/python-versioneer.svg -[travis-url]: https://travis-ci.com/github/python-versioneer/python-versioneer - -""" -# pylint:disable=invalid-name,import-outside-toplevel,missing-function-docstring -# pylint:disable=missing-class-docstring,too-many-branches,too-many-statements -# pylint:disable=raise-missing-from,too-many-lines,too-many-locals,import-error -# pylint:disable=too-few-public-methods,redefined-outer-name,consider-using-with -# pylint:disable=attribute-defined-outside-init,too-many-arguments - -import configparser -import errno -import json -import os -import re -import subprocess -import sys -from pathlib import Path -from typing import Any, Callable, cast, Dict, List, Optional, Tuple, Union -from typing import NoReturn -import functools - -have_tomllib = True -if sys.version_info >= (3, 11): - import tomllib -else: - try: - import tomli as tomllib - except ImportError: - have_tomllib = False - - -class VersioneerConfig: - """Container for Versioneer configuration parameters.""" - - VCS: str - style: str - tag_prefix: str - versionfile_source: str - versionfile_build: Optional[str] - parentdir_prefix: Optional[str] - verbose: Optional[bool] - - -def get_root() -> str: - """Get the project root directory. - - We require that all commands are run from the project root, i.e. the - directory that contains setup.py, setup.cfg, and versioneer.py . - """ - root = os.path.realpath(os.path.abspath(os.getcwd())) - setup_py = os.path.join(root, "setup.py") - pyproject_toml = os.path.join(root, "pyproject.toml") - versioneer_py = os.path.join(root, "versioneer.py") - if not ( - os.path.exists(setup_py) - or os.path.exists(pyproject_toml) - or os.path.exists(versioneer_py) - ): - # allow 'python path/to/setup.py COMMAND' - root = os.path.dirname(os.path.realpath(os.path.abspath(sys.argv[0]))) - setup_py = os.path.join(root, "setup.py") - pyproject_toml = os.path.join(root, "pyproject.toml") - versioneer_py = os.path.join(root, "versioneer.py") - if not ( - os.path.exists(setup_py) - or os.path.exists(pyproject_toml) - or os.path.exists(versioneer_py) - ): - err = ("Versioneer was unable to run the project root directory. " - "Versioneer requires setup.py to be executed from " - "its immediate directory (like 'python setup.py COMMAND'), " - "or in a way that lets it use sys.argv[0] to find the root " - "(like 'python path/to/setup.py COMMAND').") - raise VersioneerBadRootError(err) - try: - # Certain runtime workflows (setup.py install/develop in a setuptools - # tree) execute all dependencies in a single python process, so - # "versioneer" may be imported multiple times, and python's shared - # module-import table will cache the first one. So we can't use - # os.path.dirname(__file__), as that will find whichever - # versioneer.py was first imported, even in later projects. - my_path = os.path.realpath(os.path.abspath(__file__)) - me_dir = os.path.normcase(os.path.splitext(my_path)[0]) - vsr_dir = os.path.normcase(os.path.splitext(versioneer_py)[0]) - if me_dir != vsr_dir and "VERSIONEER_PEP518" not in globals(): - print("Warning: build in %s is using versioneer.py from %s" - % (os.path.dirname(my_path), versioneer_py)) - except NameError: - pass - return root - - -def get_config_from_root(root: str) -> VersioneerConfig: - """Read the project setup.cfg file to determine Versioneer config.""" - # This might raise OSError (if setup.cfg is missing), or - # configparser.NoSectionError (if it lacks a [versioneer] section), or - # configparser.NoOptionError (if it lacks "VCS="). See the docstring at - # the top of versioneer.py for instructions on writing your setup.cfg . - root_pth = Path(root) - pyproject_toml = root_pth / "pyproject.toml" - setup_cfg = root_pth / "setup.cfg" - section: Union[Dict[str, Any], configparser.SectionProxy, None] = None - if pyproject_toml.exists() and have_tomllib: - try: - with open(pyproject_toml, 'rb') as fobj: - pp = tomllib.load(fobj) - section = pp['tool']['versioneer'] - except (tomllib.TOMLDecodeError, KeyError) as e: - print(f"Failed to load config from {pyproject_toml}: {e}") - print("Try to load it from setup.cfg") - if not section: - parser = configparser.ConfigParser() - with open(setup_cfg) as cfg_file: - parser.read_file(cfg_file) - parser.get("versioneer", "VCS") # raise error if missing - - section = parser["versioneer"] - - # `cast`` really shouldn't be used, but its simplest for the - # common VersioneerConfig users at the moment. We verify against - # `None` values elsewhere where it matters - - cfg = VersioneerConfig() - cfg.VCS = section['VCS'] - cfg.style = section.get("style", "") - cfg.versionfile_source = cast(str, section.get("versionfile_source")) - cfg.versionfile_build = section.get("versionfile_build") - cfg.tag_prefix = cast(str, section.get("tag_prefix")) - if cfg.tag_prefix in ("''", '""', None): - cfg.tag_prefix = "" - cfg.parentdir_prefix = section.get("parentdir_prefix") - if isinstance(section, configparser.SectionProxy): - # Make sure configparser translates to bool - cfg.verbose = section.getboolean("verbose") - else: - cfg.verbose = section.get("verbose") - - return cfg - - -class NotThisMethod(Exception): - """Exception raised if a method is not valid for the current scenario.""" - - -# these dictionaries contain VCS-specific tools -LONG_VERSION_PY: Dict[str, str] = {} -HANDLERS: Dict[str, Dict[str, Callable]] = {} - - -def register_vcs_handler(vcs: str, method: str) -> Callable: # decorator - """Create decorator to mark a method as the handler of a VCS.""" - def decorate(f: Callable) -> Callable: - """Store f in HANDLERS[vcs][method].""" - HANDLERS.setdefault(vcs, {})[method] = f - return f - return decorate - - -def run_command( - commands: List[str], - args: List[str], - cwd: Optional[str] = None, - verbose: bool = False, - hide_stderr: bool = False, - env: Optional[Dict[str, str]] = None, -) -> Tuple[Optional[str], Optional[int]]: - """Call the given command(s).""" - assert isinstance(commands, list) - process = None - - popen_kwargs: Dict[str, Any] = {} - if sys.platform == "win32": - # This hides the console window if pythonw.exe is used - startupinfo = subprocess.STARTUPINFO() - startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW - popen_kwargs["startupinfo"] = startupinfo - - for command in commands: - try: - dispcmd = str([command] + args) - # remember shell=False, so use git.cmd on windows, not just git - process = subprocess.Popen([command] + args, cwd=cwd, env=env, - stdout=subprocess.PIPE, - stderr=(subprocess.PIPE if hide_stderr - else None), **popen_kwargs) - break - except OSError as e: - if e.errno == errno.ENOENT: - continue - if verbose: - print("unable to run %s" % dispcmd) - print(e) - return None, None - else: - if verbose: - print("unable to find command, tried %s" % (commands,)) - return None, None - stdout = process.communicate()[0].strip().decode() - if process.returncode != 0: - if verbose: - print("unable to run %s (error)" % dispcmd) - print("stdout was %s" % stdout) - return None, process.returncode - return stdout, process.returncode - - -LONG_VERSION_PY['git'] = r''' -# This file helps to compute a version number in source trees obtained from -# git-archive tarball (such as those provided by githubs download-from-tag -# feature). Distribution tarballs (built by setup.py sdist) and build -# directories (produced by setup.py build) will contain a much shorter file -# that just contains the computed version number. - -# This file is released into the public domain. -# Generated by versioneer-0.29 -# https://github.com/python-versioneer/python-versioneer - -"""Git implementation of _version.py.""" - -import errno -import os -import re -import subprocess -import sys -from typing import Any, Callable, Dict, List, Optional, Tuple -import functools - - -def get_keywords() -> Dict[str, str]: - """Get the keywords needed to look up the version information.""" - # these strings will be replaced by git during git-archive. - # setup.py/versioneer.py will grep for the variable names, so they must - # each be defined on a line of their own. _version.py will just call - # get_keywords(). - git_refnames = "%(DOLLAR)sFormat:%%d%(DOLLAR)s" - git_full = "%(DOLLAR)sFormat:%%H%(DOLLAR)s" - git_date = "%(DOLLAR)sFormat:%%ci%(DOLLAR)s" - keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} - return keywords - - -class VersioneerConfig: - """Container for Versioneer configuration parameters.""" - - VCS: str - style: str - tag_prefix: str - parentdir_prefix: str - versionfile_source: str - verbose: bool - - -def get_config() -> VersioneerConfig: - """Create, populate and return the VersioneerConfig() object.""" - # these strings are filled in when 'setup.py versioneer' creates - # _version.py - cfg = VersioneerConfig() - cfg.VCS = "git" - cfg.style = "%(STYLE)s" - cfg.tag_prefix = "%(TAG_PREFIX)s" - cfg.parentdir_prefix = "%(PARENTDIR_PREFIX)s" - cfg.versionfile_source = "%(VERSIONFILE_SOURCE)s" - cfg.verbose = False - return cfg - - -class NotThisMethod(Exception): - """Exception raised if a method is not valid for the current scenario.""" - - -LONG_VERSION_PY: Dict[str, str] = {} -HANDLERS: Dict[str, Dict[str, Callable]] = {} - - -def register_vcs_handler(vcs: str, method: str) -> Callable: # decorator - """Create decorator to mark a method as the handler of a VCS.""" - def decorate(f: Callable) -> Callable: - """Store f in HANDLERS[vcs][method].""" - if vcs not in HANDLERS: - HANDLERS[vcs] = {} - HANDLERS[vcs][method] = f - return f - return decorate - - -def run_command( - commands: List[str], - args: List[str], - cwd: Optional[str] = None, - verbose: bool = False, - hide_stderr: bool = False, - env: Optional[Dict[str, str]] = None, -) -> Tuple[Optional[str], Optional[int]]: - """Call the given command(s).""" - assert isinstance(commands, list) - process = None - - popen_kwargs: Dict[str, Any] = {} - if sys.platform == "win32": - # This hides the console window if pythonw.exe is used - startupinfo = subprocess.STARTUPINFO() - startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW - popen_kwargs["startupinfo"] = startupinfo - - for command in commands: - try: - dispcmd = str([command] + args) - # remember shell=False, so use git.cmd on windows, not just git - process = subprocess.Popen([command] + args, cwd=cwd, env=env, - stdout=subprocess.PIPE, - stderr=(subprocess.PIPE if hide_stderr - else None), **popen_kwargs) - break - except OSError as e: - if e.errno == errno.ENOENT: - continue - if verbose: - print("unable to run %%s" %% dispcmd) - print(e) - return None, None - else: - if verbose: - print("unable to find command, tried %%s" %% (commands,)) - return None, None - stdout = process.communicate()[0].strip().decode() - if process.returncode != 0: - if verbose: - print("unable to run %%s (error)" %% dispcmd) - print("stdout was %%s" %% stdout) - return None, process.returncode - return stdout, process.returncode - - -def versions_from_parentdir( - parentdir_prefix: str, - root: str, - verbose: bool, -) -> Dict[str, Any]: - """Try to determine the version from the parent directory name. - - Source tarballs conventionally unpack into a directory that includes both - the project name and a version string. We will also support searching up - two directory levels for an appropriately named parent directory - """ - rootdirs = [] - - for _ in range(3): - dirname = os.path.basename(root) - if dirname.startswith(parentdir_prefix): - return {"version": dirname[len(parentdir_prefix):], - "full-revisionid": None, - "dirty": False, "error": None, "date": None} - rootdirs.append(root) - root = os.path.dirname(root) # up a level - - if verbose: - print("Tried directories %%s but none started with prefix %%s" %% - (str(rootdirs), parentdir_prefix)) - raise NotThisMethod("rootdir doesn't start with parentdir_prefix") - - -@register_vcs_handler("git", "get_keywords") -def git_get_keywords(versionfile_abs: str) -> Dict[str, str]: - """Extract version information from the given file.""" - # the code embedded in _version.py can just fetch the value of these - # keywords. When used from setup.py, we don't want to import _version.py, - # so we do it with a regexp instead. This function is not used from - # _version.py. - keywords: Dict[str, str] = {} - try: - with open(versionfile_abs, "r") as fobj: - for line in fobj: - if line.strip().startswith("git_refnames ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["refnames"] = mo.group(1) - if line.strip().startswith("git_full ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["full"] = mo.group(1) - if line.strip().startswith("git_date ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["date"] = mo.group(1) - except OSError: - pass - return keywords - - -@register_vcs_handler("git", "keywords") -def git_versions_from_keywords( - keywords: Dict[str, str], - tag_prefix: str, - verbose: bool, -) -> Dict[str, Any]: - """Get version information from git keywords.""" - if "refnames" not in keywords: - raise NotThisMethod("Short version file found") - date = keywords.get("date") - if date is not None: - # Use only the last line. Previous lines may contain GPG signature - # information. - date = date.splitlines()[-1] - - # git-2.2.0 added "%%cI", which expands to an ISO-8601 -compliant - # datestamp. However we prefer "%%ci" (which expands to an "ISO-8601 - # -like" string, which we must then edit to make compliant), because - # it's been around since git-1.5.3, and it's too difficult to - # discover which version we're using, or to work around using an - # older one. - date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) - refnames = keywords["refnames"].strip() - if refnames.startswith("$Format"): - if verbose: - print("keywords are unexpanded, not using") - raise NotThisMethod("unexpanded keywords, not a git-archive tarball") - refs = {r.strip() for r in refnames.strip("()").split(",")} - # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of - # just "foo-1.0". If we see a "tag: " prefix, prefer those. - TAG = "tag: " - tags = {r[len(TAG):] for r in refs if r.startswith(TAG)} - if not tags: - # Either we're using git < 1.8.3, or there really are no tags. We use - # a heuristic: assume all version tags have a digit. The old git %%d - # expansion behaves like git log --decorate=short and strips out the - # refs/heads/ and refs/tags/ prefixes that would let us distinguish - # between branches and tags. By ignoring refnames without digits, we - # filter out many common branch names like "release" and - # "stabilization", as well as "HEAD" and "master". - tags = {r for r in refs if re.search(r'\d', r)} - if verbose: - print("discarding '%%s', no digits" %% ",".join(refs - tags)) - if verbose: - print("likely tags: %%s" %% ",".join(sorted(tags))) - for ref in sorted(tags): - # sorting will prefer e.g. "2.0" over "2.0rc1" - if ref.startswith(tag_prefix): - r = ref[len(tag_prefix):] - # Filter out refs that exactly match prefix or that don't start - # with a number once the prefix is stripped (mostly a concern - # when prefix is '') - if not re.match(r'\d', r): - continue - if verbose: - print("picking %%s" %% r) - return {"version": r, - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": None, - "date": date} - # no suitable tags, so version is "0+unknown", but full hex is still there - if verbose: - print("no suitable tags, using unknown + full revision id") - return {"version": "0+unknown", - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": "no suitable tags", "date": None} - - -@register_vcs_handler("git", "pieces_from_vcs") -def git_pieces_from_vcs( - tag_prefix: str, - root: str, - verbose: bool, - runner: Callable = run_command -) -> Dict[str, Any]: - """Get version from 'git describe' in the root of the source tree. - - This only gets called if the git-archive 'subst' keywords were *not* - expanded, and _version.py hasn't already been rewritten with a short - version string, meaning we're inside a checked out source tree. - """ - GITS = ["git"] - if sys.platform == "win32": - GITS = ["git.cmd", "git.exe"] - - # GIT_DIR can interfere with correct operation of Versioneer. - # It may be intended to be passed to the Versioneer-versioned project, - # but that should not change where we get our version from. - env = os.environ.copy() - env.pop("GIT_DIR", None) - runner = functools.partial(runner, env=env) - - _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, - hide_stderr=not verbose) - if rc != 0: - if verbose: - print("Directory %%s not under git control" %% root) - raise NotThisMethod("'git rev-parse --git-dir' returned error") - - # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] - # if there isn't one, this yields HEX[-dirty] (no NUM) - describe_out, rc = runner(GITS, [ - "describe", "--tags", "--dirty", "--always", "--long", - "--match", f"{tag_prefix}[[:digit:]]*" - ], cwd=root) - # --long was added in git-1.5.5 - if describe_out is None: - raise NotThisMethod("'git describe' failed") - describe_out = describe_out.strip() - full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root) - if full_out is None: - raise NotThisMethod("'git rev-parse' failed") - full_out = full_out.strip() - - pieces: Dict[str, Any] = {} - pieces["long"] = full_out - pieces["short"] = full_out[:7] # maybe improved later - pieces["error"] = None - - branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], - cwd=root) - # --abbrev-ref was added in git-1.6.3 - if rc != 0 or branch_name is None: - raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") - branch_name = branch_name.strip() - - if branch_name == "HEAD": - # If we aren't exactly on a branch, pick a branch which represents - # the current commit. If all else fails, we are on a branchless - # commit. - branches, rc = runner(GITS, ["branch", "--contains"], cwd=root) - # --contains was added in git-1.5.4 - if rc != 0 or branches is None: - raise NotThisMethod("'git branch --contains' returned error") - branches = branches.split("\n") - - # Remove the first line if we're running detached - if "(" in branches[0]: - branches.pop(0) - - # Strip off the leading "* " from the list of branches. - branches = [branch[2:] for branch in branches] - if "master" in branches: - branch_name = "master" - elif not branches: - branch_name = None - else: - # Pick the first branch that is returned. Good or bad. - branch_name = branches[0] - - pieces["branch"] = branch_name - - # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] - # TAG might have hyphens. - git_describe = describe_out - - # look for -dirty suffix - dirty = git_describe.endswith("-dirty") - pieces["dirty"] = dirty - if dirty: - git_describe = git_describe[:git_describe.rindex("-dirty")] - - # now we have TAG-NUM-gHEX or HEX - - if "-" in git_describe: - # TAG-NUM-gHEX - mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) - if not mo: - # unparsable. Maybe git-describe is misbehaving? - pieces["error"] = ("unable to parse git-describe output: '%%s'" - %% describe_out) - return pieces - - # tag - full_tag = mo.group(1) - if not full_tag.startswith(tag_prefix): - if verbose: - fmt = "tag '%%s' doesn't start with prefix '%%s'" - print(fmt %% (full_tag, tag_prefix)) - pieces["error"] = ("tag '%%s' doesn't start with prefix '%%s'" - %% (full_tag, tag_prefix)) - return pieces - pieces["closest-tag"] = full_tag[len(tag_prefix):] - - # distance: number of commits since tag - pieces["distance"] = int(mo.group(2)) - - # commit: short hex revision ID - pieces["short"] = mo.group(3) - - else: - # HEX: no tags - pieces["closest-tag"] = None - out, rc = runner(GITS, ["rev-list", "HEAD", "--left-right"], cwd=root) - pieces["distance"] = len(out.split()) # total number of commits - - # commit date: see ISO-8601 comment in git_versions_from_keywords() - date = runner(GITS, ["show", "-s", "--format=%%ci", "HEAD"], cwd=root)[0].strip() - # Use only the last line. Previous lines may contain GPG signature - # information. - date = date.splitlines()[-1] - pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) - - return pieces - - -def plus_or_dot(pieces: Dict[str, Any]) -> str: - """Return a + if we don't already have one, else return a .""" - if "+" in pieces.get("closest-tag", ""): - return "." - return "+" - - -def render_pep440(pieces: Dict[str, Any]) -> str: - """Build up version string, with post-release "local version identifier". - - Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you - get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty - - Exceptions: - 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += plus_or_dot(pieces) - rendered += "%%d.g%%s" %% (pieces["distance"], pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - else: - # exception #1 - rendered = "0+untagged.%%d.g%%s" %% (pieces["distance"], - pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - return rendered - - -def render_pep440_branch(pieces: Dict[str, Any]) -> str: - """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . - - The ".dev0" means not master branch. Note that .dev0 sorts backwards - (a feature branch will appear "older" than the master branch). - - Exceptions: - 1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - if pieces["branch"] != "master": - rendered += ".dev0" - rendered += plus_or_dot(pieces) - rendered += "%%d.g%%s" %% (pieces["distance"], pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - else: - # exception #1 - rendered = "0" - if pieces["branch"] != "master": - rendered += ".dev0" - rendered += "+untagged.%%d.g%%s" %% (pieces["distance"], - pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - return rendered - - -def pep440_split_post(ver: str) -> Tuple[str, Optional[int]]: - """Split pep440 version string at the post-release segment. - - Returns the release segments before the post-release and the - post-release version number (or -1 if no post-release segment is present). - """ - vc = str.split(ver, ".post") - return vc[0], int(vc[1] or 0) if len(vc) == 2 else None - - -def render_pep440_pre(pieces: Dict[str, Any]) -> str: - """TAG[.postN.devDISTANCE] -- No -dirty. - - Exceptions: - 1: no tags. 0.post0.devDISTANCE - """ - if pieces["closest-tag"]: - if pieces["distance"]: - # update the post release segment - tag_version, post_version = pep440_split_post(pieces["closest-tag"]) - rendered = tag_version - if post_version is not None: - rendered += ".post%%d.dev%%d" %% (post_version + 1, pieces["distance"]) - else: - rendered += ".post0.dev%%d" %% (pieces["distance"]) - else: - # no commits, use the tag as the version - rendered = pieces["closest-tag"] - else: - # exception #1 - rendered = "0.post0.dev%%d" %% pieces["distance"] - return rendered - - -def render_pep440_post(pieces: Dict[str, Any]) -> str: - """TAG[.postDISTANCE[.dev0]+gHEX] . - - The ".dev0" means dirty. Note that .dev0 sorts backwards - (a dirty tree will appear "older" than the corresponding clean one), - but you shouldn't be releasing software with -dirty anyways. - - Exceptions: - 1: no tags. 0.postDISTANCE[.dev0] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%%d" %% pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - rendered += plus_or_dot(pieces) - rendered += "g%%s" %% pieces["short"] - else: - # exception #1 - rendered = "0.post%%d" %% pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - rendered += "+g%%s" %% pieces["short"] - return rendered - - -def render_pep440_post_branch(pieces: Dict[str, Any]) -> str: - """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . - - The ".dev0" means not master branch. - - Exceptions: - 1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%%d" %% pieces["distance"] - if pieces["branch"] != "master": - rendered += ".dev0" - rendered += plus_or_dot(pieces) - rendered += "g%%s" %% pieces["short"] - if pieces["dirty"]: - rendered += ".dirty" - else: - # exception #1 - rendered = "0.post%%d" %% pieces["distance"] - if pieces["branch"] != "master": - rendered += ".dev0" - rendered += "+g%%s" %% pieces["short"] - if pieces["dirty"]: - rendered += ".dirty" - return rendered - - -def render_pep440_old(pieces: Dict[str, Any]) -> str: - """TAG[.postDISTANCE[.dev0]] . - - The ".dev0" means dirty. - - Exceptions: - 1: no tags. 0.postDISTANCE[.dev0] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%%d" %% pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - else: - # exception #1 - rendered = "0.post%%d" %% pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - return rendered - - -def render_git_describe(pieces: Dict[str, Any]) -> str: - """TAG[-DISTANCE-gHEX][-dirty]. - - Like 'git describe --tags --dirty --always'. - - Exceptions: - 1: no tags. HEX[-dirty] (note: no 'g' prefix) - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"]: - rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) - else: - # exception #1 - rendered = pieces["short"] - if pieces["dirty"]: - rendered += "-dirty" - return rendered - - -def render_git_describe_long(pieces: Dict[str, Any]) -> str: - """TAG-DISTANCE-gHEX[-dirty]. - - Like 'git describe --tags --dirty --always -long'. - The distance/hash is unconditional. - - Exceptions: - 1: no tags. HEX[-dirty] (note: no 'g' prefix) - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) - else: - # exception #1 - rendered = pieces["short"] - if pieces["dirty"]: - rendered += "-dirty" - return rendered - - -def render(pieces: Dict[str, Any], style: str) -> Dict[str, Any]: - """Render the given version pieces into the requested style.""" - if pieces["error"]: - return {"version": "unknown", - "full-revisionid": pieces.get("long"), - "dirty": None, - "error": pieces["error"], - "date": None} - - if not style or style == "default": - style = "pep440" # the default - - if style == "pep440": - rendered = render_pep440(pieces) - elif style == "pep440-branch": - rendered = render_pep440_branch(pieces) - elif style == "pep440-pre": - rendered = render_pep440_pre(pieces) - elif style == "pep440-post": - rendered = render_pep440_post(pieces) - elif style == "pep440-post-branch": - rendered = render_pep440_post_branch(pieces) - elif style == "pep440-old": - rendered = render_pep440_old(pieces) - elif style == "git-describe": - rendered = render_git_describe(pieces) - elif style == "git-describe-long": - rendered = render_git_describe_long(pieces) - else: - raise ValueError("unknown style '%%s'" %% style) - - return {"version": rendered, "full-revisionid": pieces["long"], - "dirty": pieces["dirty"], "error": None, - "date": pieces.get("date")} - - -def get_versions() -> Dict[str, Any]: - """Get version information or return default if unable to do so.""" - # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have - # __file__, we can work backwards from there to the root. Some - # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which - # case we can only use expanded keywords. - - cfg = get_config() - verbose = cfg.verbose - - try: - return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, - verbose) - except NotThisMethod: - pass - - try: - root = os.path.realpath(__file__) - # versionfile_source is the relative path from the top of the source - # tree (where the .git directory might live) to this file. Invert - # this to find the root from __file__. - for _ in cfg.versionfile_source.split('/'): - root = os.path.dirname(root) - except NameError: - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, - "error": "unable to find root of source tree", - "date": None} - - try: - pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) - return render(pieces, cfg.style) - except NotThisMethod: - pass - - try: - if cfg.parentdir_prefix: - return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) - except NotThisMethod: - pass - - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, - "error": "unable to compute version", "date": None} -''' - - -@register_vcs_handler("git", "get_keywords") -def git_get_keywords(versionfile_abs: str) -> Dict[str, str]: - """Extract version information from the given file.""" - # the code embedded in _version.py can just fetch the value of these - # keywords. When used from setup.py, we don't want to import _version.py, - # so we do it with a regexp instead. This function is not used from - # _version.py. - keywords: Dict[str, str] = {} - try: - with open(versionfile_abs, "r") as fobj: - for line in fobj: - if line.strip().startswith("git_refnames ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["refnames"] = mo.group(1) - if line.strip().startswith("git_full ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["full"] = mo.group(1) - if line.strip().startswith("git_date ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["date"] = mo.group(1) - except OSError: - pass - return keywords - - -@register_vcs_handler("git", "keywords") -def git_versions_from_keywords( - keywords: Dict[str, str], - tag_prefix: str, - verbose: bool, -) -> Dict[str, Any]: - """Get version information from git keywords.""" - if "refnames" not in keywords: - raise NotThisMethod("Short version file found") - date = keywords.get("date") - if date is not None: - # Use only the last line. Previous lines may contain GPG signature - # information. - date = date.splitlines()[-1] - - # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant - # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 - # -like" string, which we must then edit to make compliant), because - # it's been around since git-1.5.3, and it's too difficult to - # discover which version we're using, or to work around using an - # older one. - date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) - refnames = keywords["refnames"].strip() - if refnames.startswith("$Format"): - if verbose: - print("keywords are unexpanded, not using") - raise NotThisMethod("unexpanded keywords, not a git-archive tarball") - refs = {r.strip() for r in refnames.strip("()").split(",")} - # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of - # just "foo-1.0". If we see a "tag: " prefix, prefer those. - TAG = "tag: " - tags = {r[len(TAG):] for r in refs if r.startswith(TAG)} - if not tags: - # Either we're using git < 1.8.3, or there really are no tags. We use - # a heuristic: assume all version tags have a digit. The old git %d - # expansion behaves like git log --decorate=short and strips out the - # refs/heads/ and refs/tags/ prefixes that would let us distinguish - # between branches and tags. By ignoring refnames without digits, we - # filter out many common branch names like "release" and - # "stabilization", as well as "HEAD" and "master". - tags = {r for r in refs if re.search(r'\d', r)} - if verbose: - print("discarding '%s', no digits" % ",".join(refs - tags)) - if verbose: - print("likely tags: %s" % ",".join(sorted(tags))) - for ref in sorted(tags): - # sorting will prefer e.g. "2.0" over "2.0rc1" - if ref.startswith(tag_prefix): - r = ref[len(tag_prefix):] - # Filter out refs that exactly match prefix or that don't start - # with a number once the prefix is stripped (mostly a concern - # when prefix is '') - if not re.match(r'\d', r): - continue - if verbose: - print("picking %s" % r) - return {"version": r, - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": None, - "date": date} - # no suitable tags, so version is "0+unknown", but full hex is still there - if verbose: - print("no suitable tags, using unknown + full revision id") - return {"version": "0+unknown", - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": "no suitable tags", "date": None} - - -@register_vcs_handler("git", "pieces_from_vcs") -def git_pieces_from_vcs( - tag_prefix: str, - root: str, - verbose: bool, - runner: Callable = run_command -) -> Dict[str, Any]: - """Get version from 'git describe' in the root of the source tree. - - This only gets called if the git-archive 'subst' keywords were *not* - expanded, and _version.py hasn't already been rewritten with a short - version string, meaning we're inside a checked out source tree. - """ - GITS = ["git"] - if sys.platform == "win32": - GITS = ["git.cmd", "git.exe"] - - # GIT_DIR can interfere with correct operation of Versioneer. - # It may be intended to be passed to the Versioneer-versioned project, - # but that should not change where we get our version from. - env = os.environ.copy() - env.pop("GIT_DIR", None) - runner = functools.partial(runner, env=env) - - _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, - hide_stderr=not verbose) - if rc != 0: - if verbose: - print("Directory %s not under git control" % root) - raise NotThisMethod("'git rev-parse --git-dir' returned error") - - # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] - # if there isn't one, this yields HEX[-dirty] (no NUM) - describe_out, rc = runner(GITS, [ - "describe", "--tags", "--dirty", "--always", "--long", - "--match", f"{tag_prefix}[[:digit:]]*" - ], cwd=root) - # --long was added in git-1.5.5 - if describe_out is None: - raise NotThisMethod("'git describe' failed") - describe_out = describe_out.strip() - full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root) - if full_out is None: - raise NotThisMethod("'git rev-parse' failed") - full_out = full_out.strip() - - pieces: Dict[str, Any] = {} - pieces["long"] = full_out - pieces["short"] = full_out[:7] # maybe improved later - pieces["error"] = None - - branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], - cwd=root) - # --abbrev-ref was added in git-1.6.3 - if rc != 0 or branch_name is None: - raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") - branch_name = branch_name.strip() - - if branch_name == "HEAD": - # If we aren't exactly on a branch, pick a branch which represents - # the current commit. If all else fails, we are on a branchless - # commit. - branches, rc = runner(GITS, ["branch", "--contains"], cwd=root) - # --contains was added in git-1.5.4 - if rc != 0 or branches is None: - raise NotThisMethod("'git branch --contains' returned error") - branches = branches.split("\n") - - # Remove the first line if we're running detached - if "(" in branches[0]: - branches.pop(0) - - # Strip off the leading "* " from the list of branches. - branches = [branch[2:] for branch in branches] - if "master" in branches: - branch_name = "master" - elif not branches: - branch_name = None - else: - # Pick the first branch that is returned. Good or bad. - branch_name = branches[0] - - pieces["branch"] = branch_name - - # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] - # TAG might have hyphens. - git_describe = describe_out - - # look for -dirty suffix - dirty = git_describe.endswith("-dirty") - pieces["dirty"] = dirty - if dirty: - git_describe = git_describe[:git_describe.rindex("-dirty")] - - # now we have TAG-NUM-gHEX or HEX - - if "-" in git_describe: - # TAG-NUM-gHEX - mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) - if not mo: - # unparsable. Maybe git-describe is misbehaving? - pieces["error"] = ("unable to parse git-describe output: '%s'" - % describe_out) - return pieces - - # tag - full_tag = mo.group(1) - if not full_tag.startswith(tag_prefix): - if verbose: - fmt = "tag '%s' doesn't start with prefix '%s'" - print(fmt % (full_tag, tag_prefix)) - pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" - % (full_tag, tag_prefix)) - return pieces - pieces["closest-tag"] = full_tag[len(tag_prefix):] - - # distance: number of commits since tag - pieces["distance"] = int(mo.group(2)) - - # commit: short hex revision ID - pieces["short"] = mo.group(3) - - else: - # HEX: no tags - pieces["closest-tag"] = None - out, rc = runner(GITS, ["rev-list", "HEAD", "--left-right"], cwd=root) - pieces["distance"] = len(out.split()) # total number of commits - - # commit date: see ISO-8601 comment in git_versions_from_keywords() - date = runner(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip() - # Use only the last line. Previous lines may contain GPG signature - # information. - date = date.splitlines()[-1] - pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) - - return pieces - - -def do_vcs_install(versionfile_source: str, ipy: Optional[str]) -> None: - """Git-specific installation logic for Versioneer. - - For Git, this means creating/changing .gitattributes to mark _version.py - for export-subst keyword substitution. - """ - GITS = ["git"] - if sys.platform == "win32": - GITS = ["git.cmd", "git.exe"] - files = [versionfile_source] - if ipy: - files.append(ipy) - if "VERSIONEER_PEP518" not in globals(): - try: - my_path = __file__ - if my_path.endswith((".pyc", ".pyo")): - my_path = os.path.splitext(my_path)[0] + ".py" - versioneer_file = os.path.relpath(my_path) - except NameError: - versioneer_file = "versioneer.py" - files.append(versioneer_file) - present = False - try: - with open(".gitattributes", "r") as fobj: - for line in fobj: - if line.strip().startswith(versionfile_source): - if "export-subst" in line.strip().split()[1:]: - present = True - break - except OSError: - pass - if not present: - with open(".gitattributes", "a+") as fobj: - fobj.write(f"{versionfile_source} export-subst\n") - files.append(".gitattributes") - run_command(GITS, ["add", "--"] + files) - - -def versions_from_parentdir( - parentdir_prefix: str, - root: str, - verbose: bool, -) -> Dict[str, Any]: - """Try to determine the version from the parent directory name. - - Source tarballs conventionally unpack into a directory that includes both - the project name and a version string. We will also support searching up - two directory levels for an appropriately named parent directory - """ - rootdirs = [] - - for _ in range(3): - dirname = os.path.basename(root) - if dirname.startswith(parentdir_prefix): - return {"version": dirname[len(parentdir_prefix):], - "full-revisionid": None, - "dirty": False, "error": None, "date": None} - rootdirs.append(root) - root = os.path.dirname(root) # up a level - - if verbose: - print("Tried directories %s but none started with prefix %s" % - (str(rootdirs), parentdir_prefix)) - raise NotThisMethod("rootdir doesn't start with parentdir_prefix") - - -SHORT_VERSION_PY = """ -# This file was generated by 'versioneer.py' (0.29) from -# revision-control system data, or from the parent directory name of an -# unpacked source archive. Distribution tarballs contain a pre-generated copy -# of this file. - -import json - -version_json = ''' -%s -''' # END VERSION_JSON - - -def get_versions(): - return json.loads(version_json) -""" - - -def versions_from_file(filename: str) -> Dict[str, Any]: - """Try to determine the version from _version.py if present.""" - try: - with open(filename) as f: - contents = f.read() - except OSError: - raise NotThisMethod("unable to read _version.py") - mo = re.search(r"version_json = '''\n(.*)''' # END VERSION_JSON", - contents, re.M | re.S) - if not mo: - mo = re.search(r"version_json = '''\r\n(.*)''' # END VERSION_JSON", - contents, re.M | re.S) - if not mo: - raise NotThisMethod("no version_json in _version.py") - return json.loads(mo.group(1)) - - -def write_to_version_file(filename: str, versions: Dict[str, Any]) -> None: - """Write the given version number to the given _version.py file.""" - contents = json.dumps(versions, sort_keys=True, - indent=1, separators=(",", ": ")) - with open(filename, "w") as f: - f.write(SHORT_VERSION_PY % contents) - - print("set %s to '%s'" % (filename, versions["version"])) - - -def plus_or_dot(pieces: Dict[str, Any]) -> str: - """Return a + if we don't already have one, else return a .""" - if "+" in pieces.get("closest-tag", ""): - return "." - return "+" - - -def render_pep440(pieces: Dict[str, Any]) -> str: - """Build up version string, with post-release "local version identifier". - - Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you - get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty - - Exceptions: - 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += plus_or_dot(pieces) - rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - else: - # exception #1 - rendered = "0+untagged.%d.g%s" % (pieces["distance"], - pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - return rendered - - -def render_pep440_branch(pieces: Dict[str, Any]) -> str: - """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . - - The ".dev0" means not master branch. Note that .dev0 sorts backwards - (a feature branch will appear "older" than the master branch). - - Exceptions: - 1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - if pieces["branch"] != "master": - rendered += ".dev0" - rendered += plus_or_dot(pieces) - rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - else: - # exception #1 - rendered = "0" - if pieces["branch"] != "master": - rendered += ".dev0" - rendered += "+untagged.%d.g%s" % (pieces["distance"], - pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - return rendered - - -def pep440_split_post(ver: str) -> Tuple[str, Optional[int]]: - """Split pep440 version string at the post-release segment. - - Returns the release segments before the post-release and the - post-release version number (or -1 if no post-release segment is present). - """ - vc = str.split(ver, ".post") - return vc[0], int(vc[1] or 0) if len(vc) == 2 else None - - -def render_pep440_pre(pieces: Dict[str, Any]) -> str: - """TAG[.postN.devDISTANCE] -- No -dirty. - - Exceptions: - 1: no tags. 0.post0.devDISTANCE - """ - if pieces["closest-tag"]: - if pieces["distance"]: - # update the post release segment - tag_version, post_version = pep440_split_post(pieces["closest-tag"]) - rendered = tag_version - if post_version is not None: - rendered += ".post%d.dev%d" % (post_version + 1, pieces["distance"]) - else: - rendered += ".post0.dev%d" % (pieces["distance"]) - else: - # no commits, use the tag as the version - rendered = pieces["closest-tag"] - else: - # exception #1 - rendered = "0.post0.dev%d" % pieces["distance"] - return rendered - - -def render_pep440_post(pieces: Dict[str, Any]) -> str: - """TAG[.postDISTANCE[.dev0]+gHEX] . - - The ".dev0" means dirty. Note that .dev0 sorts backwards - (a dirty tree will appear "older" than the corresponding clean one), - but you shouldn't be releasing software with -dirty anyways. - - Exceptions: - 1: no tags. 0.postDISTANCE[.dev0] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - rendered += plus_or_dot(pieces) - rendered += "g%s" % pieces["short"] - else: - # exception #1 - rendered = "0.post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - rendered += "+g%s" % pieces["short"] - return rendered - - -def render_pep440_post_branch(pieces: Dict[str, Any]) -> str: - """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . - - The ".dev0" means not master branch. - - Exceptions: - 1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%d" % pieces["distance"] - if pieces["branch"] != "master": - rendered += ".dev0" - rendered += plus_or_dot(pieces) - rendered += "g%s" % pieces["short"] - if pieces["dirty"]: - rendered += ".dirty" - else: - # exception #1 - rendered = "0.post%d" % pieces["distance"] - if pieces["branch"] != "master": - rendered += ".dev0" - rendered += "+g%s" % pieces["short"] - if pieces["dirty"]: - rendered += ".dirty" - return rendered - - -def render_pep440_old(pieces: Dict[str, Any]) -> str: - """TAG[.postDISTANCE[.dev0]] . - - The ".dev0" means dirty. - - Exceptions: - 1: no tags. 0.postDISTANCE[.dev0] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - else: - # exception #1 - rendered = "0.post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - return rendered - - -def render_git_describe(pieces: Dict[str, Any]) -> str: - """TAG[-DISTANCE-gHEX][-dirty]. - - Like 'git describe --tags --dirty --always'. - - Exceptions: - 1: no tags. HEX[-dirty] (note: no 'g' prefix) - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"]: - rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) - else: - # exception #1 - rendered = pieces["short"] - if pieces["dirty"]: - rendered += "-dirty" - return rendered - - -def render_git_describe_long(pieces: Dict[str, Any]) -> str: - """TAG-DISTANCE-gHEX[-dirty]. - - Like 'git describe --tags --dirty --always -long'. - The distance/hash is unconditional. - - Exceptions: - 1: no tags. HEX[-dirty] (note: no 'g' prefix) - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) - else: - # exception #1 - rendered = pieces["short"] - if pieces["dirty"]: - rendered += "-dirty" - return rendered - - -def render(pieces: Dict[str, Any], style: str) -> Dict[str, Any]: - """Render the given version pieces into the requested style.""" - if pieces["error"]: - return {"version": "unknown", - "full-revisionid": pieces.get("long"), - "dirty": None, - "error": pieces["error"], - "date": None} - - if not style or style == "default": - style = "pep440" # the default - - if style == "pep440": - rendered = render_pep440(pieces) - elif style == "pep440-branch": - rendered = render_pep440_branch(pieces) - elif style == "pep440-pre": - rendered = render_pep440_pre(pieces) - elif style == "pep440-post": - rendered = render_pep440_post(pieces) - elif style == "pep440-post-branch": - rendered = render_pep440_post_branch(pieces) - elif style == "pep440-old": - rendered = render_pep440_old(pieces) - elif style == "git-describe": - rendered = render_git_describe(pieces) - elif style == "git-describe-long": - rendered = render_git_describe_long(pieces) - else: - raise ValueError("unknown style '%s'" % style) - - return {"version": rendered, "full-revisionid": pieces["long"], - "dirty": pieces["dirty"], "error": None, - "date": pieces.get("date")} - - -class VersioneerBadRootError(Exception): - """The project root directory is unknown or missing key files.""" - - -def get_versions(verbose: bool = False) -> Dict[str, Any]: - """Get the project version from whatever source is available. - - Returns dict with two keys: 'version' and 'full'. - """ - if "versioneer" in sys.modules: - # see the discussion in cmdclass.py:get_cmdclass() - del sys.modules["versioneer"] - - root = get_root() - cfg = get_config_from_root(root) - - assert cfg.VCS is not None, "please set [versioneer]VCS= in setup.cfg" - handlers = HANDLERS.get(cfg.VCS) - assert handlers, "unrecognized VCS '%s'" % cfg.VCS - verbose = verbose or bool(cfg.verbose) # `bool()` used to avoid `None` - assert cfg.versionfile_source is not None, \ - "please set versioneer.versionfile_source" - assert cfg.tag_prefix is not None, "please set versioneer.tag_prefix" - - versionfile_abs = os.path.join(root, cfg.versionfile_source) - - # extract version from first of: _version.py, VCS command (e.g. 'git - # describe'), parentdir. This is meant to work for developers using a - # source checkout, for users of a tarball created by 'setup.py sdist', - # and for users of a tarball/zipball created by 'git archive' or github's - # download-from-tag feature or the equivalent in other VCSes. - - get_keywords_f = handlers.get("get_keywords") - from_keywords_f = handlers.get("keywords") - if get_keywords_f and from_keywords_f: - try: - keywords = get_keywords_f(versionfile_abs) - ver = from_keywords_f(keywords, cfg.tag_prefix, verbose) - if verbose: - print("got version from expanded keyword %s" % ver) - return ver - except NotThisMethod: - pass - - try: - ver = versions_from_file(versionfile_abs) - if verbose: - print("got version from file %s %s" % (versionfile_abs, ver)) - return ver - except NotThisMethod: - pass - - from_vcs_f = handlers.get("pieces_from_vcs") - if from_vcs_f: - try: - pieces = from_vcs_f(cfg.tag_prefix, root, verbose) - ver = render(pieces, cfg.style) - if verbose: - print("got version from VCS %s" % ver) - return ver - except NotThisMethod: - pass - - try: - if cfg.parentdir_prefix: - ver = versions_from_parentdir(cfg.parentdir_prefix, root, verbose) - if verbose: - print("got version from parentdir %s" % ver) - return ver - except NotThisMethod: - pass - - if verbose: - print("unable to compute version") - - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, "error": "unable to compute version", - "date": None} - - -def get_version() -> str: - """Get the short version string for this project.""" - return get_versions()["version"] - - -def get_cmdclass(cmdclass: Optional[Dict[str, Any]] = None): - """Get the custom setuptools subclasses used by Versioneer. - - If the package uses a different cmdclass (e.g. one from numpy), it - should be provide as an argument. - """ - if "versioneer" in sys.modules: - del sys.modules["versioneer"] - # this fixes the "python setup.py develop" case (also 'install' and - # 'easy_install .'), in which subdependencies of the main project are - # built (using setup.py bdist_egg) in the same python process. Assume - # a main project A and a dependency B, which use different versions - # of Versioneer. A's setup.py imports A's Versioneer, leaving it in - # sys.modules by the time B's setup.py is executed, causing B to run - # with the wrong versioneer. Setuptools wraps the sub-dep builds in a - # sandbox that restores sys.modules to it's pre-build state, so the - # parent is protected against the child's "import versioneer". By - # removing ourselves from sys.modules here, before the child build - # happens, we protect the child from the parent's versioneer too. - # Also see https://github.com/python-versioneer/python-versioneer/issues/52 - - cmds = {} if cmdclass is None else cmdclass.copy() - - # we add "version" to setuptools - from setuptools import Command - - class cmd_version(Command): - description = "report generated version string" - user_options: List[Tuple[str, str, str]] = [] - boolean_options: List[str] = [] - - def initialize_options(self) -> None: - pass - - def finalize_options(self) -> None: - pass - - def run(self) -> None: - vers = get_versions(verbose=True) - print("Version: %s" % vers["version"]) - print(" full-revisionid: %s" % vers.get("full-revisionid")) - print(" dirty: %s" % vers.get("dirty")) - print(" date: %s" % vers.get("date")) - if vers["error"]: - print(" error: %s" % vers["error"]) - cmds["version"] = cmd_version - - # we override "build_py" in setuptools - # - # most invocation pathways end up running build_py: - # distutils/build -> build_py - # distutils/install -> distutils/build ->.. - # setuptools/bdist_wheel -> distutils/install ->.. - # setuptools/bdist_egg -> distutils/install_lib -> build_py - # setuptools/install -> bdist_egg ->.. - # setuptools/develop -> ? - # pip install: - # copies source tree to a tempdir before running egg_info/etc - # if .git isn't copied too, 'git describe' will fail - # then does setup.py bdist_wheel, or sometimes setup.py install - # setup.py egg_info -> ? - - # pip install -e . and setuptool/editable_wheel will invoke build_py - # but the build_py command is not expected to copy any files. - - # we override different "build_py" commands for both environments - if 'build_py' in cmds: - _build_py: Any = cmds['build_py'] - else: - from setuptools.command.build_py import build_py as _build_py - - class cmd_build_py(_build_py): - def run(self) -> None: - root = get_root() - cfg = get_config_from_root(root) - versions = get_versions() - _build_py.run(self) - if getattr(self, "editable_mode", False): - # During editable installs `.py` and data files are - # not copied to build_lib - return - # now locate _version.py in the new build/ directory and replace - # it with an updated value - if cfg.versionfile_build: - target_versionfile = os.path.join(self.build_lib, - cfg.versionfile_build) - print("UPDATING %s" % target_versionfile) - write_to_version_file(target_versionfile, versions) - cmds["build_py"] = cmd_build_py - - if 'build_ext' in cmds: - _build_ext: Any = cmds['build_ext'] - else: - from setuptools.command.build_ext import build_ext as _build_ext - - class cmd_build_ext(_build_ext): - def run(self) -> None: - root = get_root() - cfg = get_config_from_root(root) - versions = get_versions() - _build_ext.run(self) - if self.inplace: - # build_ext --inplace will only build extensions in - # build/lib<..> dir with no _version.py to write to. - # As in place builds will already have a _version.py - # in the module dir, we do not need to write one. - return - # now locate _version.py in the new build/ directory and replace - # it with an updated value - if not cfg.versionfile_build: - return - target_versionfile = os.path.join(self.build_lib, - cfg.versionfile_build) - if not os.path.exists(target_versionfile): - print(f"Warning: {target_versionfile} does not exist, skipping " - "version update. This can happen if you are running build_ext " - "without first running build_py.") - return - print("UPDATING %s" % target_versionfile) - write_to_version_file(target_versionfile, versions) - cmds["build_ext"] = cmd_build_ext - - if "cx_Freeze" in sys.modules: # cx_freeze enabled? - from cx_Freeze.dist import build_exe as _build_exe # type: ignore - # nczeczulin reports that py2exe won't like the pep440-style string - # as FILEVERSION, but it can be used for PRODUCTVERSION, e.g. - # setup(console=[{ - # "version": versioneer.get_version().split("+", 1)[0], # FILEVERSION - # "product_version": versioneer.get_version(), - # ... - - class cmd_build_exe(_build_exe): - def run(self) -> None: - root = get_root() - cfg = get_config_from_root(root) - versions = get_versions() - target_versionfile = cfg.versionfile_source - print("UPDATING %s" % target_versionfile) - write_to_version_file(target_versionfile, versions) - - _build_exe.run(self) - os.unlink(target_versionfile) - with open(cfg.versionfile_source, "w") as f: - LONG = LONG_VERSION_PY[cfg.VCS] - f.write(LONG % - {"DOLLAR": "$", - "STYLE": cfg.style, - "TAG_PREFIX": cfg.tag_prefix, - "PARENTDIR_PREFIX": cfg.parentdir_prefix, - "VERSIONFILE_SOURCE": cfg.versionfile_source, - }) - cmds["build_exe"] = cmd_build_exe - del cmds["build_py"] - - if 'py2exe' in sys.modules: # py2exe enabled? - try: - from py2exe.setuptools_buildexe import py2exe as _py2exe # type: ignore - except ImportError: - from py2exe.distutils_buildexe import py2exe as _py2exe # type: ignore - - class cmd_py2exe(_py2exe): - def run(self) -> None: - root = get_root() - cfg = get_config_from_root(root) - versions = get_versions() - target_versionfile = cfg.versionfile_source - print("UPDATING %s" % target_versionfile) - write_to_version_file(target_versionfile, versions) - - _py2exe.run(self) - os.unlink(target_versionfile) - with open(cfg.versionfile_source, "w") as f: - LONG = LONG_VERSION_PY[cfg.VCS] - f.write(LONG % - {"DOLLAR": "$", - "STYLE": cfg.style, - "TAG_PREFIX": cfg.tag_prefix, - "PARENTDIR_PREFIX": cfg.parentdir_prefix, - "VERSIONFILE_SOURCE": cfg.versionfile_source, - }) - cmds["py2exe"] = cmd_py2exe - - # sdist farms its file list building out to egg_info - if 'egg_info' in cmds: - _egg_info: Any = cmds['egg_info'] - else: - from setuptools.command.egg_info import egg_info as _egg_info - - class cmd_egg_info(_egg_info): - def find_sources(self) -> None: - # egg_info.find_sources builds the manifest list and writes it - # in one shot - super().find_sources() - - # Modify the filelist and normalize it - root = get_root() - cfg = get_config_from_root(root) - self.filelist.append('versioneer.py') - if cfg.versionfile_source: - # There are rare cases where versionfile_source might not be - # included by default, so we must be explicit - self.filelist.append(cfg.versionfile_source) - self.filelist.sort() - self.filelist.remove_duplicates() - - # The write method is hidden in the manifest_maker instance that - # generated the filelist and was thrown away - # We will instead replicate their final normalization (to unicode, - # and POSIX-style paths) - from setuptools import unicode_utils - normalized = [unicode_utils.filesys_decode(f).replace(os.sep, '/') - for f in self.filelist.files] - - manifest_filename = os.path.join(self.egg_info, 'SOURCES.txt') - with open(manifest_filename, 'w') as fobj: - fobj.write('\n'.join(normalized)) - - cmds['egg_info'] = cmd_egg_info - - # we override different "sdist" commands for both environments - if 'sdist' in cmds: - _sdist: Any = cmds['sdist'] - else: - from setuptools.command.sdist import sdist as _sdist - - class cmd_sdist(_sdist): - def run(self) -> None: - versions = get_versions() - self._versioneer_generated_versions = versions - # unless we update this, the command will keep using the old - # version - self.distribution.metadata.version = versions["version"] - return _sdist.run(self) - - def make_release_tree(self, base_dir: str, files: List[str]) -> None: - root = get_root() - cfg = get_config_from_root(root) - _sdist.make_release_tree(self, base_dir, files) - # now locate _version.py in the new base_dir directory - # (remembering that it may be a hardlink) and replace it with an - # updated value - target_versionfile = os.path.join(base_dir, cfg.versionfile_source) - print("UPDATING %s" % target_versionfile) - write_to_version_file(target_versionfile, - self._versioneer_generated_versions) - cmds["sdist"] = cmd_sdist - - return cmds - - -CONFIG_ERROR = """ -setup.cfg is missing the necessary Versioneer configuration. You need -a section like: - - [versioneer] - VCS = git - style = pep440 - versionfile_source = src/myproject/_version.py - versionfile_build = myproject/_version.py - tag_prefix = - parentdir_prefix = myproject- - -You will also need to edit your setup.py to use the results: - - import versioneer - setup(version=versioneer.get_version(), - cmdclass=versioneer.get_cmdclass(), ...) - -Please read the docstring in ./versioneer.py for configuration instructions, -edit setup.cfg, and re-run the installer or 'python versioneer.py setup'. -""" - -SAMPLE_CONFIG = """ -# See the docstring in versioneer.py for instructions. Note that you must -# re-run 'versioneer.py setup' after changing this section, and commit the -# resulting files. - -[versioneer] -#VCS = git -#style = pep440 -#versionfile_source = -#versionfile_build = -#tag_prefix = -#parentdir_prefix = - -""" - -OLD_SNIPPET = """ -from ._version import get_versions -__version__ = get_versions()['version'] -del get_versions -""" - -INIT_PY_SNIPPET = """ -from . import {0} -__version__ = {0}.get_versions()['version'] -""" - - -def do_setup() -> int: - """Do main VCS-independent setup function for installing Versioneer.""" - root = get_root() - try: - cfg = get_config_from_root(root) - except (OSError, configparser.NoSectionError, - configparser.NoOptionError) as e: - if isinstance(e, (OSError, configparser.NoSectionError)): - print("Adding sample versioneer config to setup.cfg", - file=sys.stderr) - with open(os.path.join(root, "setup.cfg"), "a") as f: - f.write(SAMPLE_CONFIG) - print(CONFIG_ERROR, file=sys.stderr) - return 1 - - print(" creating %s" % cfg.versionfile_source) - with open(cfg.versionfile_source, "w") as f: - LONG = LONG_VERSION_PY[cfg.VCS] - f.write(LONG % {"DOLLAR": "$", - "STYLE": cfg.style, - "TAG_PREFIX": cfg.tag_prefix, - "PARENTDIR_PREFIX": cfg.parentdir_prefix, - "VERSIONFILE_SOURCE": cfg.versionfile_source, - }) - - ipy = os.path.join(os.path.dirname(cfg.versionfile_source), - "__init__.py") - maybe_ipy: Optional[str] = ipy - if os.path.exists(ipy): - try: - with open(ipy, "r") as f: - old = f.read() - except OSError: - old = "" - module = os.path.splitext(os.path.basename(cfg.versionfile_source))[0] - snippet = INIT_PY_SNIPPET.format(module) - if OLD_SNIPPET in old: - print(" replacing boilerplate in %s" % ipy) - with open(ipy, "w") as f: - f.write(old.replace(OLD_SNIPPET, snippet)) - elif snippet not in old: - print(" appending to %s" % ipy) - with open(ipy, "a") as f: - f.write(snippet) - else: - print(" %s unmodified" % ipy) - else: - print(" %s doesn't exist, ok" % ipy) - maybe_ipy = None - - # Make VCS-specific changes. For git, this means creating/changing - # .gitattributes to mark _version.py for export-subst keyword - # substitution. - do_vcs_install(cfg.versionfile_source, maybe_ipy) - return 0 - - -def scan_setup_py() -> int: - """Validate the contents of setup.py against Versioneer's expectations.""" - found = set() - setters = False - errors = 0 - with open("setup.py", "r") as f: - for line in f.readlines(): - if "import versioneer" in line: - found.add("import") - if "versioneer.get_cmdclass()" in line: - found.add("cmdclass") - if "versioneer.get_version()" in line: - found.add("get_version") - if "versioneer.VCS" in line: - setters = True - if "versioneer.versionfile_source" in line: - setters = True - if len(found) != 3: - print("") - print("Your setup.py appears to be missing some important items") - print("(but I might be wrong). Please make sure it has something") - print("roughly like the following:") - print("") - print(" import versioneer") - print(" setup( version=versioneer.get_version(),") - print(" cmdclass=versioneer.get_cmdclass(), ...)") - print("") - errors += 1 - if setters: - print("You should remove lines like 'versioneer.VCS = ' and") - print("'versioneer.versionfile_source = ' . This configuration") - print("now lives in setup.cfg, and should be removed from setup.py") - print("") - errors += 1 - return errors - - -def setup_command() -> NoReturn: - """Set up Versioneer and exit with appropriate error code.""" - errors = do_setup() - errors += scan_setup_py() - sys.exit(1 if errors else 0) - - -if __name__ == "__main__": - cmd = sys.argv[1] - if cmd == "setup": - setup_command()