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", convert_weather_code=True ) >>> 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, } if frequency == "twice-daily": request_url = API_URL + "daily" else: 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", convert_weather_code=True ): """ Get a forecast for the provided site. Three frequencies are supported by DataHub: hourly, three-hourly and daily. The 'twice-daily' option is for convenience and splits a daily forecast into two steps, one for day and one for night. :parameter latitude: Latitude of forecast location :parameter longitude: Longitude of forecast location :parameter frequency: Forecast frequency. One of 'hourly', 'three-hourly,'twice-daily', 'daily' :parameter convert_weather_code: Convert numeric weather codes to string description :type latitude: float :type longitude: float :type frequency: string :type convert_weather_code: bool :return: :class: `Forecast ` object :rtype: datapoint.Forecast """ if frequency not in ["hourly", "three-hourly", "twice-daily", "daily"]: raise ValueError( "frequency must be set to one of 'hourly', 'three-hourly', " "'twice-daily', 'daily'" ) data = self.__call_api(latitude, longitude, frequency) forecast = Forecast( frequency=frequency, api_data=data, convert_weather_code=convert_weather_code, ) return forecast