Example #1
0
class WeatherManager:
    """
    A manager objects that provides a full interface to OWM Weather API.

    :param API_key: the OWM AirPollution API key
    :type API_key: str
    :param config: the configuration dictionary
    :type config: dict
    :returns: a *WeatherManager* instance
    :raises: *AssertionError* when no API Key is provided

    """
    def __init__(self, API_key, config):
        assert isinstance(API_key, str), 'You must provide a valid API Key'
        self.API_key = API_key
        assert isinstance(config, dict)
        self.http_client = HttpClient(API_key, config, ROOT_WEATHER_API)

    def weather_api_version(self):
        return WEATHER_API_VERSION

    def weather_at_place(self, name):
        """
        Queries the OWM Weather API for the currently observed weather at the
        specified toponym (eg: "London,uk")

        :param name: the location's toponym
        :type name: str
        :returns: an *Observation* instance or ``None`` if no weather data is
            available
        :raises: *ParseResponseException* when OWM Weather API responses' data
            cannot be parsed or *APICallException* when OWM Weather API can not be
            reached
        """

        assert isinstance(name, str), "Value must be a string"
        params = {'q': name}
        _, json_data = self.http_client.get_json(OBSERVATION_URI,
                                                 params=params)
        return observation.Observation.from_dict(json_data)

    def weather_at_coords(self, lat, lon):
        """
        Queries the OWM Weather API for the currently observed weather at the
        specified geographic (eg: 51.503614, -0.107331).

        :param lat: the location's latitude, must be between -90.0 and 90.0
        :type lat: int/float
        :param lon: the location's longitude, must be between -180.0 and 180.0
        :type lon: int/float
        :returns: an *Observation* instance or ``None`` if no weather data is
            available
        :raises: *ParseResponseException* when OWM Weather API responses' data
            cannot be parsed or *APICallException* when OWM Weather API can not be
            reached
        """
        geo.assert_is_lon(lon)
        geo.assert_is_lat(lat)
        params = {'lon': lon, 'lat': lat}
        _, json_data = self.http_client.get_json(OBSERVATION_URI,
                                                 params=params)
        return observation.Observation.from_dict(json_data)

    def weather_at_zip_code(self, zipcode, country):
        """
        Queries the OWM Weather API for the currently observed weather at the
        specified zip code and country code (eg: 2037, au).

        :param zip: the location's zip or postcode
        :type zip: string
        :param country: the location's country code
        :type country: string
        :returns: an *Observation* instance or ``None`` if no weather data is
            available
        :raises: *ParseResponseException* when OWM Weather API responses' data
            cannot be parsed or *APICallException* when OWM Weather API can not be
            reached
        """
        assert isinstance(zipcode, str), "Value must be a string"
        assert isinstance(country, str), "Value must be a string"
        zip_param = zipcode + ',' + country
        params = {'zip': zip_param}
        _, json_data = self.http_client.get_json(OBSERVATION_URI,
                                                 params=params)
        return observation.Observation.from_dict(json_data)

    def weather_at_id(self, id):
        """
        Queries the OWM Weather API for the currently observed weather at the
        specified city ID (eg: 5128581)

        :param id: the location's city ID
        :type id: int
        :returns: an *Observation* instance or ``None`` if no weather data is
            available
        :raises: *ParseResponseException* when OWM Weather API responses' data
            cannot be parsed or *APICallException* when OWM Weather API can not be
            reached
        """
        assert type(id) is int, "'id' must be an int"
        if id < 0:
            raise ValueError("'id' value must be greater than 0")
        params = {'id': id}
        _, json_data = self.http_client.get_json(OBSERVATION_URI,
                                                 params=params)
        return observation.Observation.from_dict(json_data)

    def weather_at_ids(self, ids_list):
        """
        Queries the OWM Weather API for the currently observed weathers at the
        specified city IDs (eg: [5128581,87182])

        :param ids_list: the list of city IDs
        :type ids_list: list of int
        :returns: a list of *Observation* instances or an empty list if no
            weather data is available
        :raises: *ParseResponseException* when OWM Weather API responses' data
            cannot be parsed or *APICallException* when OWM Weather API can not be
            reached
        """
        assert type(ids_list) is list, "'ids_list' must be a list of integers"
        for id in ids_list:
            assert type(id) is int, "'ids_list' must be a list of integers"
            if id < 0:
                raise ValueError("id values in 'ids_list' must be greater "
                                 "than 0")
        params = {'id': ','.join(list(map(str, ids_list)))}
        _, json_data = self.http_client.get_json(GROUP_OBSERVATIONS_URI,
                                                 params=params)
        return observation.Observation.from_dict_of_lists(json_data)

    def weather_at_places(self, pattern, searchtype, limit=None):
        """
        Queries the OWM Weather API for the currently observed weather in all the
        locations whose name is matching the specified text search parameters.
        A twofold search can be issued: *'accurate'* (exact matching) and
        *'like'* (matches names that are similar to the supplied pattern).

        :param pattern: the string pattern (not a regex) to be searched for the
            toponym
        :type pattern: str
        :param searchtype: the search mode to be used, must be *'accurate'* for
          an exact matching or *'like'* for a likelihood matching
        :type: searchtype: str
        :param limit: the maximum number of *Observation* items in the returned
            list (default is ``None``, which stands for any number of items)
        :param limit: int or ``None``
        :returns: a list of *Observation* objects or ``None`` if no weather
            data is available
        :raises: *ParseResponseException* when OWM Weather API responses' data
            cannot be parsed, *APICallException* when OWM Weather API can not be
            reached, *ValueError* when bad value is supplied for the search
            type or the maximum number of items retrieved
        """
        assert isinstance(pattern, str), "'pattern' must be a str"
        assert isinstance(searchtype, str), "'searchtype' must be a str"
        if searchtype != "accurate" and searchtype != "like":
            raise ValueError("'searchtype' value must be 'accurate' or 'like'")
        if limit is not None:
            assert isinstance(limit, int), "'limit' must be an int or None"
            if limit < 1:
                raise ValueError("'limit' must be None or greater than zero")
        params = {'q': pattern, 'type': searchtype}
        if limit is not None:
            # fix for OWM 2.5 API bug!
            params['cnt'] = limit - 1
        _, json_data = self.http_client.get_json(FIND_OBSERVATIONS_URI,
                                                 params=params)
        return observation.Observation.from_dict_of_lists(json_data)

    def weather_at_places_in_bbox(self,
                                  lon_left,
                                  lat_bottom,
                                  lon_right,
                                  lat_top,
                                  zoom=10,
                                  cluster=False):
        """
        Queries the OWM Weather API for the weather currently observed by
        meteostations inside the bounding box of latitude/longitude coords.

        :param lat_top: latitude for top margin of bounding box, must be
            between -90.0 and 90.0
        :type lat_top: int/float
        :param lon_left: longitude for left margin of bounding box
            must be between -180.0 and 180.0
        :type lon_left: int/float
        :param lat_bottom: latitude for the bottom margin of bounding box, must
            be between -90.0 and 90.0
        :type lat_bottom: int/float
        :param lon_right: longitude for the right margin of bounding box,
            must be between -180.0 and 180.0
        :type lon_right: int/float
        :param zoom: zoom level (defaults to: 10)
        :type zoom: int
        :param cluster: use server clustering of points
        :type cluster: bool
        :returns: a list of *Observation* objects or ``None`` if no weather
            data is available
        :raises: *ParseResponseException* when OWM Weather API responses' data
            cannot be parsed, *APICallException* when OWM Weather API can not be
            reached, *ValueError* when coordinates values are out of bounds or
            negative values are provided for limit
        """
        geo.assert_is_lon(lon_left)
        geo.assert_is_lon(lon_right)
        geo.assert_is_lat(lat_bottom)
        geo.assert_is_lat(lat_top)
        assert type(zoom) is int, "'zoom' must be an int"
        if zoom <= 0:
            raise ValueError("'zoom' must greater than zero")
        assert type(cluster) is bool, "'cluster' must be a bool"
        params = {
            'bbox':
            ','.join([
                str(lon_left),
                str(lat_bottom),
                str(lon_right),
                str(lat_top),
                str(zoom)
            ]),
            'cluster':
            'yes' if cluster else 'no'
        }
        _, json_data = self.http_client.get_json(BBOX_CITY_URI, params=params)
        return observation.Observation.from_dict_of_lists(json_data)

    def weather_around_coords(self, lat, lon, limit=None):
        """
        Queries the OWM Weather API for the currently observed weather in all the
        locations in the proximity of the specified coordinates.

        :param lat: location's latitude, must be between -90.0 and 90.0
        :type lat: int/float
        :param lon: location's longitude, must be between -180.0 and 180.0
        :type lon: int/float
        :param limit: the maximum number of *Observation* items in the returned
            list (default is ``None``, which stands for any number of items)
        :param limit: int or ``None``
        :returns: a list of *Observation* objects or ``None`` if no weather
            data is available
        :raises: *ParseResponseException* when OWM Weather API responses' data
            cannot be parsed, *APICallException* when OWM Weather API can not be
            reached, *ValueError* when coordinates values are out of bounds or
            negative values are provided for limit
        """
        geo.assert_is_lon(lon)
        geo.assert_is_lat(lat)
        params = {'lon': lon, 'lat': lat}
        if limit is not None:
            assert isinstance(limit, int), "'limit' must be an int or None"
            if limit < 1:
                raise ValueError("'limit' must be None or greater than zero")
            params['cnt'] = limit
        _, json_data = self.http_client.get_json(FIND_OBSERVATIONS_URI,
                                                 params=params)
        return observation.Observation.from_dict_of_lists(json_data)

    def forecast_at_place(self, name, interval, limit=None):
        """
        Queries the OWM Weather API for weather forecast for the
        specified location (eg: "London,uk") with the given time granularity.
        A *Forecaster* object is returned, containing a *Forecast*: this instance
        encapsulates *Weather* objects corresponding to the provided granularity.

        :param name: the location's toponym
        :type name: str
        :param interval: the granularity of the forecast, among `3h` and 'daily'
        :type interval: str among `3h` and 'daily'
        :param limit: the maximum number of *Weather* items to be retrieved
            (default is ``None``, which stands for any number of items)
        :type limit: int or ``None``
        :returns: a *Forecaster* instance or ``None`` if forecast data is not
            available for the specified location
        :raises: *ParseResponseException* when OWM Weather API responses' data
            cannot be parsed, *APICallException* when OWM Weather API can not be
            reached
        """
        assert isinstance(name, str), "Value must be a string"
        assert isinstance(interval, str), "Interval must be a string"
        if limit is not None:
            assert isinstance(limit, int), "'limit' must be an int or None"
            if limit < 1:
                raise ValueError("'limit' must be None or greater than zero")
        params = {'q': name}
        if limit is not None:
            params['cnt'] = limit
        if interval == '3h':
            uri = THREE_HOURS_FORECAST_URI
        elif interval == 'daily':
            uri = DAILY_FORECAST_URI
        else:
            raise ValueError("Unsupported time interval for forecast")
        _, json_data = self.http_client.get_json(uri, params=params)
        fc = forecast.Forecast.from_dict(json_data)
        if fc is not None:
            fc.interval = interval
            return forecaster.Forecaster(fc)
        else:
            return None

    def forecast_at_coords(self, lat, lon, interval, limit=None):
        """
        Queries the OWM Weather API for weather forecast for the
        specified geographic coordinates with the given time granularity.
        A *Forecaster* object is returned, containing a *Forecast*: this instance
        encapsulates *Weather* objects corresponding to the provided granularity.

        :param lat: location's latitude, must be between -90.0 and 90.0
        :type lat: int/float
        :param lon: location's longitude, must be between -180.0 and 180.0
        :type lon: int/float
        :param interval: the granularity of the forecast, among `3h` and 'daily'
        :type interval: str among `3h` and 'daily'
        :param limit: the maximum number of *Weather* items to be retrieved
            (default is ``None``, which stands for any number of items)
        :type limit: int or ``None``
        :returns: a *Forecaster* instance or ``None`` if forecast data is not
            available for the specified location
        :raises: *ParseResponseException* when OWM Weather API responses' data
            cannot be parsed, *APICallException* when OWM Weather API can not be
            reached
        """
        geo.assert_is_lon(lon)
        geo.assert_is_lat(lat)
        assert isinstance(interval, str), "Interval must be a string"
        if limit is not None:
            assert isinstance(limit, int), "'limit' must be an int or None"
            if limit < 1:
                raise ValueError("'limit' must be None or greater than zero")
        params = {'lon': lon, 'lat': lat}
        if limit is not None:
            params['cnt'] = limit
        if interval == '3h':
            uri = THREE_HOURS_FORECAST_URI
        elif interval == 'daily':
            uri = DAILY_FORECAST_URI
        else:
            raise ValueError("Unsupported time interval for forecast")
        _, json_data = self.http_client.get_json(uri, params=params)
        fc = forecast.Forecast.from_dict(json_data)
        if fc is not None:
            fc.interval = interval
            return forecaster.Forecaster(fc)
        else:
            return None

    def forecast_at_id(self, id, interval, limit=None):
        """
        Queries the OWM Weather API for weather forecast for the
        specified city ID (eg: 5128581) with the given time granularity.
        A *Forecaster* object is returned, containing a *Forecast*: this instance
        encapsulates *Weather* objects corresponding to the provided granularity.

        :param id: the location's city ID
        :type id: int
        :param interval: the granularity of the forecast, among `3h` and 'daily'
        :type interval: str among `3h` and 'daily'
        :param limit: the maximum number of *Weather* items to be retrieved
            (default is ``None``, which stands for any number of items)
        :type limit: int or ``None``
        :returns: a *Forecaster* instance or ``None`` if forecast data is not
            available for the specified location
        :raises: *ParseResponseException* when OWM Weather API responses' data
            cannot be parsed, *APICallException* when OWM Weather API can not be
            reached
        """
        assert type(id) is int, "'id' must be an int"
        if id < 0:
            raise ValueError("'id' value must be greater than 0")
        assert isinstance(interval, str), "Interval must be a string"
        if limit is not None:
            assert isinstance(limit, int), "'limit' must be an int or None"
            if limit < 1:
                raise ValueError("'limit' must be None or greater than zero")
        params = {'id': id}
        if limit is not None:
            params['cnt'] = limit
        if interval == '3h':
            uri = THREE_HOURS_FORECAST_URI
        elif interval == 'daily':
            uri = DAILY_FORECAST_URI
        else:
            raise ValueError("Unsupported time interval for forecast")
        _, json_data = self.http_client.get_json(uri, params=params)
        fc = forecast.Forecast.from_dict(json_data)
        if fc is not None:
            fc.interval = interval
            return forecaster.Forecaster(fc)
        else:
            return None

    def station_tick_history(self, station_ID, limit=None):
        """
        Queries the OWM Weather API for historic weather data measurements for the
        specified meteostation (eg: 2865), sampled once a minute (tick).
        A *StationHistory* object instance is returned, encapsulating the
        measurements: the total number of data points can be limited using the
        appropriate parameter

        :param station_ID: the numeric ID of the meteostation
        :type station_ID: int
        :param limit: the maximum number of data points the result shall
            contain (default is ``None``, which stands for any number of data
            points)
        :type limit: int or ``None``
        :returns: a *StationHistory* instance or ``None`` if data is not
            available for the specified meteostation
        :raises: *ParseResponseException* when OWM Weather API responses' data
            cannot be parsed, *APICallException* when OWM Weather API can not be
            reached, *ValueError* if the limit value is negative

        """
        assert isinstance(station_ID, int), "'station_ID' must be int"
        if limit is not None:
            assert isinstance(limit, int), "'limit' must be an int or None"
            if limit < 1:
                raise ValueError("'limit' must be None or greater than zero")
        station_history = self._retrieve_station_history(
            station_ID, limit, "tick")
        if station_history is not None:
            return historian.Historian(station_history)
        else:
            return None

    def station_hour_history(self, station_ID, limit=None):
        """
        Queries the OWM Weather API for historic weather data measurements for the
        specified meteostation (eg: 2865), sampled once a hour.
        A *Historian* object instance is returned, encapsulating a
        *StationHistory* objects which contains the measurements. The total
        number of retrieved data points can be limited using the appropriate
        parameter

        :param station_ID: the numeric ID of the meteostation
        :type station_ID: int
        :param limit: the maximum number of data points the result shall
            contain (default is ``None``, which stands for any number of data
            points)
        :type limit: int or ``None``
        :returns: a *Historian* instance or ``None`` if data is not
            available for the specified meteostation
        :raises: *ParseResponseException* when OWM Weather API responses' data
            cannot be parsed, *APICallException* when OWM Weather API can not be
            reached, *ValueError* if the limit value is negative

        """
        assert isinstance(station_ID, int), "'station_ID' must be int"
        if limit is not None:
            assert isinstance(limit, int), "'limit' must be an int or None"
            if limit < 1:
                raise ValueError("'limit' must be None or greater than zero")
        station_history = self._retrieve_station_history(
            station_ID, limit, "hour")
        if station_history is not None:
            return historian.Historian(station_history)
        else:
            return None

    def station_day_history(self, station_ID, limit=None):
        """
        Queries the OWM Weather API for historic weather data measurements for the
        specified meteostation (eg: 2865), sampled once a day.
        A *Historian* object instance is returned, encapsulating a
        *StationHistory* objects which contains the measurements. The total
        number of retrieved data points can be limited using the appropriate
        parameter

        :param station_ID: the numeric ID of the meteostation
        :type station_ID: int
        :param limit: the maximum number of data points the result shall
            contain (default is ``None``, which stands for any number of data
            points)
        :type limit: int or ``None``
        :returns: a *Historian* instance or ``None`` if data is not
            available for the specified meteostation
        :raises: *ParseResponseException* when OWM Weather API responses' data
            cannot be parsed, *APICallException* when OWM Weather API can not be
            reached, *ValueError* if the limit value is negative

        """
        assert isinstance(station_ID, int), "'station_ID' must be int"
        if limit is not None:
            assert isinstance(limit, int), "'limit' must be an int or None"
            if limit < 1:
                raise ValueError("'limit' must be None or greater than zero")
        station_history = self._retrieve_station_history(
            station_ID, limit, "day")
        if station_history is not None:
            return historian.Historian(station_history)
        else:
            return None

    def _retrieve_station_history(self, station_ID, limit, interval):
        """
        Helper method for station_X_history functions.
        """
        params = {'id': station_ID, 'type': interval}
        if limit is not None:
            params['cnt'] = limit
        _, json_data = self.http_client.get_json(STATION_WEATHER_HISTORY_URI,
                                                 params=params)
        sh = stationhistory.StationHistory.from_dict(json_data)
        if sh is not None:
            sh.station_id = station_ID
            sh.interval = interval
        return sh

    def one_call(self, lat: Union[int, float],
                 lon: Union[int, float]) -> one_call.OneCall:
        """
        Queries the OWM Weather API with one call for current weather information and forecast for the
        specified geographic coordinates.
        One Call API provides the following weather data for any geographical coordinate:
        - Current weather
        - Hourly forecast for 48 hours
        - Daily forecast for 7 days

        A *OneCall* object is returned with the current data and the two forecasts.

        :param lat: location's latitude, must be between -90.0 and 90.0
        :type lat: int/float
        :param lon: location's longitude, must be between -180.0 and 180.0
        :type lon: int/float
        :returns: a *OneCall* instance or ``None`` if the data is not
            available for the specified location
        :raises: *ParseResponseException* when OWM Weather API responses' data
            cannot be parsed, *APICallException* when OWM Weather API can not be
            reached
        """
        geo.assert_is_lon(lon)
        geo.assert_is_lat(lat)
        params = {'lon': lon, 'lat': lat}

        _, json_data = self.http_client.get_json(ONE_CALL_URI, params=params)
        return one_call.OneCall.from_dict(json_data)

    def one_call_history(self,
                         lat: Union[int, float],
                         lon: Union[int, float],
                         dt: int = None):
        """
        Queries the OWM Weather API with one call for historical weather information for the
        specified geographic coordinates.

        A *OneCall* object is returned with the current data and the two forecasts.

        :param lat: location's latitude, must be between -90.0 and 90.0
        :type lat: int/float
        :param lon: location's longitude, must be between -180.0 and 180.0
        :type lon: int/float
        :param dt: timestamp from when the historical data starts. Cannot be less then now - 5 days.
                    Default = None means now - 5 days
        :type dt: int
        :returns: a *OneCall* instance or ``None`` if the data is not
            available for the specified location
        :raises: *ParseResponseException* when OWM Weather API responses' data
            cannot be parsed, *APICallException* when OWM Weather API can not be
            reached
        """
        geo.assert_is_lon(lon)
        geo.assert_is_lat(lat)
        if dt is None:
            dt = int(
                (datetime.now() -
                 timedelta(days=5)).replace(tzinfo=timezone.utc).timestamp())
        else:
            if not isinstance(dt, int):
                raise ValueError("dt must be of type int")
            if dt < 0:
                raise ValueError("dt must be positive")

        params = {'lon': lon, 'lat': lat, 'dt': dt}

        _, json_data = self.http_client.get_json(ONE_CALL_HISTORICAL_URI,
                                                 params=params)
        return one_call.OneCall.from_dict(json_data)

    def __repr__(self):
        return '<%s.%s>' % (__name__, self.__class__.__name__)
Example #2
0
class AgroManager:
    """
    A manager objects that provides a full interface to OWM Agro API.

    :param API_key: the OWM Weather API key
    :type API_key: str
    :param config: the configuration dictionary
    :type config: dict
    :returns: an `AgroManager` instance
    :raises: `AssertionError` when no API Key is provided

    """
    def __init__(self, API_key, config):
        assert isinstance(API_key, str), 'You must provide a valid API Key'
        self.API_key = API_key
        assert isinstance(config, dict)
        self.http_client = HttpClient(API_key, config, ROOT_AGRO_API)
        self.geotiff_downloader_http_client = HttpClient(
            self.API_key, config, ROOT_DOWNLOAD_GEOTIFF_API)
        self.png_downloader_http_client = HttpClient(self.API_key, config,
                                                     ROOT_DOWNLOAD_PNG_API)

    def agro_api_version(self):
        return AGRO_API_VERSION

    # POLYGON API subset methods

    def create_polygon(self, geopolygon, name=None):
        """
        Create a new polygon on the Agro API with the given parameters

        :param geopolygon: the geopolygon representing the new polygon
        :type geopolygon: `pyowm.utils.geo.Polygon` instance
        :param name: optional mnemonic name for the new polygon
        :type name: str
        :return: a `pyowm.agro10.polygon.Polygon` instance
        """
        assert geopolygon is not None
        assert isinstance(geopolygon, GeoPolygon)
        data = {
            'geo_json': {
                "type": "Feature",
                "properties": {},
                "geometry": geopolygon.to_dict(),
            }
        }

        if name is not None:
            data['name'] = name
        status, payload = self.http_client.post(
            POLYGONS_URI,
            params={'appid': self.API_key},
            data=data,
            headers={'Content-Type': 'application/json'})
        return Polygon.from_dict(payload)

    def get_polygons(self):
        """
        Retrieves all of the user's polygons registered on the Agro API.

        :returns: list of `pyowm.agro10.polygon.Polygon` objects

        """

        status, data = self.http_client.get_json(
            POLYGONS_URI,
            params={'appid': self.API_key},
            headers={'Content-Type': 'application/json'})
        return [Polygon.from_dict(item) for item in data]

    def get_polygon(self, polygon_id):
        """
        Retrieves a named polygon registered on the Agro API.

        :param id: the ID of the polygon
        :type id: str
        :returns: a `pyowm.agro10.polygon.Polygon` object

        """
        status, data = self.http_client.get_json(
            NAMED_POLYGON_URI % str(polygon_id),
            params={'appid': self.API_key},
            headers={'Content-Type': 'application/json'})
        return Polygon.from_dict(data)

    def update_polygon(self, polygon):
        """
        Updates on the Agro API the Polygon identified by the ID of the provided polygon object.
        Currently this only changes the mnemonic name of the remote polygon

        :param polygon: the `pyowm.agro10.polygon.Polygon` object to be updated
        :type polygon: `pyowm.agro10.polygon.Polygon` instance
        :returns: `None` if update is successful, an exception otherwise
        """
        assert polygon.id is not None
        status, _ = self.http_client.put(
            NAMED_POLYGON_URI % str(polygon.id),
            params={'appid': self.API_key},
            data=dict(name=polygon.name),
            headers={'Content-Type': 'application/json'})

    def delete_polygon(self, polygon):
        """
        Deletes on the Agro API the Polygon identified by the ID of the provided polygon object.

        :param polygon: the `pyowm.agro10.polygon.Polygon` object to be deleted
        :type polygon: `pyowm.agro10.polygon.Polygon` instance
        :returns: `None` if deletion is successful, an exception otherwise
        """
        assert polygon.id is not None
        status, _ = self.http_client.delete(
            NAMED_POLYGON_URI % str(polygon.id),
            params={'appid': self.API_key},
            headers={'Content-Type': 'application/json'})

    # SOIL API subset methods

    def soil_data(self, polygon):
        """
        Retrieves the latest soil data on the specified polygon

        :param polygon: the reference polygon you want soil data for
        :type polygon: `pyowm.agro10.polygon.Polygon` instance
        :returns: a `pyowm.agro10.soil.Soil` instance

        """
        assert polygon is not None
        assert isinstance(polygon, Polygon)
        polyd = polygon.id
        status, data = self.http_client.get_json(
            SOIL_URI,
            params={
                'appid': self.API_key,
                'polyid': polyd
            },
            headers={'Content-Type': 'application/json'})
        the_dict = {
            'reference_time': data['dt'],
            'surface_temp': data['t0'],
            'ten_cm_temp': data['t10'],
            'moisture': data['moisture'],
            'polygon_id': polyd,
        }

        return Soil.from_dict(the_dict)

    # Satellite Imagery subset methods

    def search_satellite_imagery(self,
                                 polygon_id,
                                 acquired_from,
                                 acquired_to,
                                 img_type=None,
                                 preset=None,
                                 min_resolution=None,
                                 max_resolution=None,
                                 acquired_by=None,
                                 min_cloud_coverage=None,
                                 max_cloud_coverage=None,
                                 min_valid_data_coverage=None,
                                 max_valid_data_coverage=None):
        """
        Searches on the Agro API the metadata for all available satellite images that contain the specified polygon and
        acquired during the specified time interval; and optionally matching the specified set of filters:
        - image type (eg. GeoTIF)
        - image preset (eg. false color, NDVI, ...)
        - min/max acquisition resolution
        - acquiring satellite
        - min/max cloud coverage on acquired scene
        - min/max valid data coverage on acquired scene

        :param polygon_id: the ID of the reference polygon
        :type polygon_id: str
        :param acquired_from: lower edge of acquisition interval, UNIX timestamp
        :type acquired_from: int
        :param acquired_to: upper edge of acquisition interval, UNIX timestamp
        :type acquired_to: int
        :param img_type: the desired file format type of the images. Allowed values are given by `pyowm.commons.enums.ImageTypeEnum`
        :type img_type: `pyowm.commons.databoxes.ImageType`
        :param preset: the desired preset of the images. Allowed values are given by `pyowm.agroapi10.enums.PresetEnum`
        :type preset: str
        :param min_resolution: minimum resolution for images, px/meters
        :type min_resolution: int
        :param max_resolution: maximum resolution for images, px/meters
        :type max_resolution: int
        :param acquired_by: short symbol of the satellite that acquired the image (eg. "l8")
        :type acquired_by: str
        :param min_cloud_coverage: minimum cloud coverage percentage on acquired images
        :type min_cloud_coverage: int
        :param max_cloud_coverage: maximum cloud coverage percentage on acquired images
        :type max_cloud_coverage: int
        :param min_valid_data_coverage: minimum valid data coverage percentage on acquired images
        :type min_valid_data_coverage: int
        :param max_valid_data_coverage: maximum valid data coverage percentage on acquired images
        :type max_valid_data_coverage: int
        :return: a list of `pyowm.agro10.imagery.MetaImage` subtypes instances
        """
        assert polygon_id is not None
        assert acquired_from is not None
        assert acquired_to is not None
        assert acquired_from <= acquired_to, 'Start timestamp of acquisition window must come before its end'
        if min_resolution is not None:
            assert min_resolution > 0, 'Minimum resolution must be positive'
        if max_resolution is not None:
            assert max_resolution > 0, 'Maximum resolution must be positive'
        if min_resolution is not None and max_resolution is not None:
            assert min_resolution <= max_resolution, 'Mininum resolution must be lower than maximum resolution'
        if min_cloud_coverage is not None:
            assert min_cloud_coverage >= 0, 'Minimum cloud coverage must be non negative'
        if max_cloud_coverage is not None:
            assert max_cloud_coverage >= 0, 'Maximum cloud coverage must be non negative'
        if min_cloud_coverage is not None and max_cloud_coverage is not None:
            assert min_cloud_coverage <= max_cloud_coverage, 'Minimum cloud coverage must be lower than maximum cloud coverage'
        if min_valid_data_coverage is not None:
            assert min_valid_data_coverage >= 0, 'Minimum valid data coverage must be non negative'
        if max_valid_data_coverage is not None:
            assert max_valid_data_coverage >= 0, 'Maximum valid data coverage must be non negative'
        if min_valid_data_coverage is not None and max_valid_data_coverage is not None:
            assert min_valid_data_coverage <= max_valid_data_coverage, 'Minimum valid data coverage must be lower than maximum valid data coverage'

        # prepare params
        params = dict(appid=self.API_key,
                      polyid=polygon_id,
                      start=acquired_from,
                      end=acquired_to)
        if min_resolution is not None:
            params['resolution_min'] = min_resolution
        if max_resolution is not None:
            params['resolution_max'] = max_resolution
        if acquired_by is not None:
            params['type'] = acquired_by
        if min_cloud_coverage is not None:
            params['clouds_min'] = min_cloud_coverage
        if max_cloud_coverage is not None:
            params['clouds_max'] = max_cloud_coverage
        if min_valid_data_coverage is not None:
            params['coverage_min'] = min_valid_data_coverage
        if max_valid_data_coverage is not None:
            params['coverage_max'] = max_valid_data_coverage

        # call API
        status, data = self.http_client.get_json(SATELLITE_IMAGERY_SEARCH_URI,
                                                 params=params)

        result_set = SatelliteImagerySearchResultSet(
            polygon_id, data, timestamps.now(timeformat='unix'))

        # further filter by img_type and/or preset (if specified)
        if img_type is not None and preset is not None:
            return result_set.with_img_type_and_preset(img_type, preset)
        elif img_type is not None:
            return result_set.with_img_type(img_type)
        elif preset is not None:
            return result_set.with_preset(preset)
        else:
            return result_set.all()

    def download_satellite_image(self,
                                 metaimage,
                                 x=None,
                                 y=None,
                                 zoom=None,
                                 palette=None):
        """
        Downloads the satellite image described by the provided metadata. In case the satellite image is a tile, then
        tile coordinates and zoom must be provided. An optional palette ID can be provided, if supported by the
        downloaded preset (currently only NDVI is supported)

        :param metaimage: the satellite image's metadata, in the form of a `MetaImage` subtype instance
        :type metaimage: a `pyowm.agroapi10.imagery.MetaImage` subtype
        :param x: x tile coordinate (only needed in case you are downloading a tile image)
        :type x: int or `None`
        :param y: y tile coordinate (only needed in case you are downloading a tile image)
        :type y: int or `None`
        :param zoom: zoom level (only needed in case you are downloading a tile image)
        :type zoom: int or `None`
        :param palette: ID of the color palette of the downloaded images. Values are provided by `pyowm.agroapi10.enums.PaletteEnum`
        :type palette: str or `None`
        :return: a `pyowm.agroapi10.imagery.SatelliteImage` instance containing both image's metadata and data
        """
        if palette is not None:
            assert isinstance(palette, str)
            params = dict(paletteid=palette)
        else:
            palette = PaletteEnum.GREEN
            params = {}
        # polygon PNG
        if isinstance(metaimage, MetaPNGImage):
            prepared_url = metaimage.url
            status, data = self.png_downloader_http_client.get_png(
                prepared_url, params=params)
            img = Image(data, metaimage.image_type)
            return SatelliteImage(
                metaimage,
                img,
                downloaded_on=timestamps.now(timeformat='unix'),
                palette=palette)
        # GeoTIF
        elif isinstance(metaimage, MetaGeoTiffImage):
            prepared_url = metaimage.url
            status, data = self.geotiff_downloader_http_client.get_geotiff(
                prepared_url, params=params)
            img = Image(data, metaimage.image_type)
            return SatelliteImage(
                metaimage,
                img,
                downloaded_on=timestamps.now(timeformat='unix'),
                palette=palette)
        # tile PNG
        elif isinstance(metaimage, MetaTile):
            assert x is not None
            assert y is not None
            assert zoom is not None
            prepared_url = self._fill_url(metaimage.url, x, y, zoom)
            status, data = self.http_client.get_png(prepared_url,
                                                    params=params)
            img = Image(data, metaimage.image_type)
            tile = Tile(x, y, zoom, None, img)
            return SatelliteImage(
                metaimage,
                tile,
                downloaded_on=timestamps.now(timeformat='unix'),
                palette=palette)
        else:
            raise ValueError("Cannot download: unsupported MetaImage subtype")

    def stats_for_satellite_image(self, metaimage):
        """
        Retrieves statistics for the satellite image described by the provided metadata.
        This is currently only supported 'EVI' and 'NDVI' presets

        :param metaimage: the satellite image's metadata, in the form of a `MetaImage` subtype instance
        :type metaimage: a `pyowm.agroapi10.imagery.MetaImage` subtype
        :return: dict
        """
        if metaimage.preset not in [PresetEnum.EVI, PresetEnum.NDVI]:
            raise ValueError("Unsupported image preset: should be EVI or NDVI")
        if metaimage.stats_url is None:
            raise ValueError("URL for image statistics is not defined")
        status, data = self.http_client.get_json(metaimage.stats_url,
                                                 params={})
        return data

    # Utilities
    def _fill_url(self, url_template, x, y, zoom):
        return url_template.replace('{x}',
                                    str(x)).replace('{y}', str(y)).replace(
                                        '{z}', str(zoom))

    def __repr__(self):
        return '<%s.%s>' % (__name__, self.__class__.__name__)
Example #3
0
class GeocodingManager:
    """
    A manager objects that provides a full interface to OWM Geocoding API.

    :param API_key: the OWM API key
    :type API_key: str
    :param config: the configuration dictionary
    :type config: dict
    :returns: an *GeocodingManager* instance
    :raises: *AssertionError* when no API Key is provided

    """
    def __init__(self, API_key, config):
        assert API_key is not None, 'You must provide a valid API Key'
        self.API_key = API_key
        assert isinstance(config, dict)
        self.http_client = HttpClient(API_key, config, ROOT_GEOCODING_API_URL)

    def geocoding_api_version(self):
        return GEOCODING_API_VERSION

    def geocode(self, toponym, country=None, state_code=None, limit=None):
        """
        Invokes the direct geocoding API endpoint

        :param toponym: the name of the location
        :type toponym: `str`
        :param country: the 2-chars ISO symbol of the country
        :type country: `str` or `None`
        :param state_code: the 2-chars ISO symbol of state (only useful in case the country is US)
        :type state_code: `str` or `None`
        :param limit: the max number of results to be returned in case of multiple matchings (no limits by default)
        :type limit: `int` or `None`
        :returns: a list of *Location* instances
        :raises: *AssertionError*, *ValueError*, *APIRequestError*

        """
        assert toponym, 'Toponym must be specified'
        if country is not None and len(country) != 2:
            raise ValueError("Country must be a 2-char string")
        if state_code is not None and len(state_code) != 2:
            raise ValueError("State Code must be a 2-char string")
        if limit is not None:
            assert isinstance(limit, int)
            assert limit > 0

        query = toponym
        if state_code is not None:
            query += ',' + state_code
        if country is not None:
            query += ',' + country

        params = {'q': query}

        if limit is not None:
            params['limit'] = limit

        _, json_data = self.http_client.get_json(DIRECT_GEOCODING_URI,
                                                 params=params)
        return [Location.from_dict(item) for item in json_data]

    def reverse_geocode(self, lat, lon, limit=None):
        geo.assert_is_lon(lon)
        geo.assert_is_lat(lat)
        if limit is not None:
            assert isinstance(limit, int)
            assert limit > 0

        params = {'lat': lat, 'lon': lon}
        if limit is not None:
            params['limit'] = limit

        _, json_data = self.http_client.get_json(REVERSE_GEOCODING_URI,
                                                 params=params)
        return [Location.from_dict(item) for item in json_data]

    def __repr__(self):
        return '<%s.%s>' % (__name__, self.__class__.__name__)
Example #4
0
class StationsManager:
    """
    A manager objects that provides a full interface to OWM Stations API. Mainly
    it implements CRUD methods on Station entities and the corresponding
    measured datapoints.

    :param API_key: the OWM Weather API key
    :type API_key: str
    :param config: the configuration dictionary
    :type config: dict
    :returns: a *StationsManager* instance
    :raises: *AssertionError* when no API Key is provided

    """
    def __init__(self, API_key, config):
        assert API_key is not None, 'You must provide a valid API Key'
        self.API_key = API_key
        assert isinstance(config, dict)
        self.http_client = HttpClient(API_key, config, ROOT_STATIONS_API_URL)

    def stations_api_version(self):
        return STATIONS_API_VERSION

    # STATIONS Methods

    def get_stations(self):
        """
        Retrieves all of the user's stations registered on the Stations API.

        :returns: list of *pyowm.stationsapi30.station.Station* objects

        """

        status, data = self.http_client.get_json(
            STATIONS_URI,
            params={'appid': self.API_key},
            headers={'Content-Type': 'application/json'})
        return [Station.from_dict(item) for item in data]

    def get_station(self, id):
        """
        Retrieves a named station registered on the Stations API.

        :param id: the ID of the station
        :type id: str
        :returns: a *pyowm.stationsapi30.station.Station* object

        """
        status, data = self.http_client.get_json(
            NAMED_STATION_URI % str(id),
            params={'appid': self.API_key},
            headers={'Content-Type': 'application/json'})
        return Station.from_dict(data)

    def create_station(self, external_id, name, lat, lon, alt=None):
        """
        Create a new station on the Station API with the given parameters

        :param external_id: the user-given ID of the station
        :type external_id: str
        :param name: the name of the station
        :type name: str
        :param lat: latitude of the station
        :type lat: float
        :param lon: longitude of the station
        :type lon: float
        :param alt: altitude of the station
        :type alt: float
        :returns: the new *pyowm.stationsapi30.station.Station* object
        """
        assert external_id is not None
        assert name is not None
        assert lon is not None
        assert lat is not None
        if lon < -180.0 or lon > 180.0:
            raise ValueError("'lon' value must be between -180 and 180")
        if lat < -90.0 or lat > 90.0:
            raise ValueError("'lat' value must be between -90 and 90")
        if alt is not None and alt < 0.0:
            raise ValueError("'alt' value must not be negative")
        status, payload = self.http_client.post(
            STATIONS_URI,
            params={'appid': self.API_key},
            data=dict(external_id=external_id,
                      name=name,
                      lat=lat,
                      lon=lon,
                      alt=alt),
            headers={'Content-Type': 'application/json'})
        return Station.from_dict(payload)

    def update_station(self, station):
        """
        Updates the Station API record identified by the ID of the provided
        *pyowm.stationsapi30.station.Station* object with all of its fields

        :param station: the *pyowm.stationsapi30.station.Station* object to be updated
        :type station: *pyowm.stationsapi30.station.Station*
        :returns: `None` if update is successful, an exception otherwise
        """
        assert station.id is not None
        status, _ = self.http_client.put(
            NAMED_STATION_URI % str(station.id),
            params={'appid': self.API_key},
            data=dict(external_id=station.external_id,
                      name=station.name,
                      lat=station.lat,
                      lon=station.lon,
                      alt=station.alt),
            headers={'Content-Type': 'application/json'})

    def delete_station(self, station):
        """
        Deletes the Station API record identified by the ID of the provided
        *pyowm.stationsapi30.station.Station*, along with all its related
        measurements

        :param station: the *pyowm.stationsapi30.station.Station* object to be deleted
        :type station: *pyowm.stationsapi30.station.Station*
        :returns: `None` if deletion is successful, an exception otherwise
        """
        assert station.id is not None
        status, _ = self.http_client.delete(
            NAMED_STATION_URI % str(station.id),
            params={'appid': self.API_key},
            headers={'Content-Type': 'application/json'})

    # Measurements-related methods

    def send_measurement(self, measurement):
        """
        Posts the provided Measurement object's data to the Station API.

        :param measurement: the *pyowm.stationsapi30.measurement.Measurement*
          object to be posted
        :type measurement: *pyowm.stationsapi30.measurement.Measurement* instance
        :returns: `None` if creation is successful, an exception otherwise
        """
        assert measurement is not None
        assert measurement.station_id is not None
        status, _ = self.http_client.post(
            MEASUREMENTS_URI,
            params={'appid': self.API_key},
            data=[self._structure_dict(measurement)],
            headers={'Content-Type': 'application/json'})

    def send_measurements(self, list_of_measurements):
        """
        Posts data about the provided list of Measurement objects to the
        Station API. The objects may be related to different station IDs.

        :param list_of_measurements: list of *pyowm.stationsapi30.measurement.Measurement*
          objects to be posted
        :type list_of_measurements: list of *pyowm.stationsapi30.measurement.Measurement*
          instances
        :returns: `None` if creation is successful, an exception otherwise
        """
        assert list_of_measurements is not None
        assert all(m.station_id is not None for m in list_of_measurements)
        msmts = [self._structure_dict(m) for m in list_of_measurements]
        status, _ = self.http_client.post(
            MEASUREMENTS_URI,
            params={'appid': self.API_key},
            data=msmts,
            headers={'Content-Type': 'application/json'})

    def get_measurements(self,
                         station_id,
                         aggregated_on,
                         from_timestamp,
                         to_timestamp,
                         limit=100):
        """
        Reads measurements of a specified station recorded in the specified time
        window and aggregated on minute, hour or day. Optionally, the number of
        resulting measurements can be limited.

        :param station_id: unique station identifier
        :type station_id: str
        :param aggregated_on: aggregation time-frame for this measurement
        :type aggregated_on: string between 'm','h' and 'd'
        :param from_timestamp: Unix timestamp corresponding to the beginning of
          the time window
        :type from_timestamp: int
        :param to_timestamp: Unix timestamp corresponding to the end of the
          time window
        :type to_timestamp: int
        :param limit: max number of items to be returned. Defaults to 100
        :type limit: int
        :returns: list of *pyowm.stationsapi30.measurement.AggregatedMeasurement*
          objects
        """
        assert station_id is not None
        assert aggregated_on is not None
        assert from_timestamp is not None
        assert from_timestamp > 0
        assert to_timestamp is not None
        assert to_timestamp > 0
        if to_timestamp < from_timestamp:
            raise ValueError(
                "End timestamp can't be earlier than begin timestamp")
        assert isinstance(limit, int)
        assert limit >= 0
        query = {
            'appid': self.API_key,
            'station_id': station_id,
            'type': aggregated_on,
            'from': from_timestamp,
            'to': to_timestamp,
            'limit': limit
        }
        status, data = self.http_client.get_json(
            MEASUREMENTS_URI,
            params=query,
            headers={'Content-Type': 'application/json'})
        return [AggregatedMeasurement.from_dict(item) for item in data]

    def send_buffer(self, buffer):
        """
        Posts to the Stations API data about the Measurement objects contained
        into the provided Buffer instance.

        :param buffer: the *pyowm.stationsapi30.buffer.Buffer* instance whose
          measurements are to be posted
        :type buffer: *pyowm.stationsapi30.buffer.Buffer* instance
        :returns: `None` if creation is successful, an exception otherwise
        """
        assert buffer is not None
        msmts = [self._structure_dict(m) for m in buffer.measurements]
        status, _ = self.http_client.post(
            MEASUREMENTS_URI,
            params={'appid': self.API_key},
            data=msmts,
            headers={'Content-Type': 'application/json'})

    def _structure_dict(self, measurement):
        d = measurement.to_dict()
        return {
            'station_id':
            d['station_id'],
            'dt':
            d['timestamp'],
            'temperature':
            d['temperature'],
            'wind_speed':
            d['wind_speed'],
            'wind_gust':
            d['wind_gust'],
            'wind_deg':
            d['wind_deg'],
            'pressure':
            d['pressure'],
            'humidity':
            d['humidity'],
            'rain_1h':
            d['rain_1h'],
            'rain_6h':
            d['rain_6h'],
            'rain_24h':
            d['rain_24h'],
            'snow_1h':
            d['snow_1h'],
            'snow_6h':
            d['snow_6h'],
            'snow_24h':
            d['snow_24h'],
            'dew_point':
            d['dew_point'],
            'humidex':
            d['humidex'],
            'heat_index':
            d['heat_index'],
            'visibility_distance':
            d['visibility_distance'],
            'visibility_prefix':
            d['visibility_prefix'],
            'clouds': [
                dict(distance=d['clouds_distance']),
                dict(condition=d['clouds_condition']),
                dict(cumulus=d['clouds_cumulus']),
            ],
            'weather': [
                dict(precipitation=d['weather_precipitation']),
                dict(descriptor=d['weather_descriptor']),
                dict(intensity=d['weather_intensity']),
                dict(proximity=d['weather_proximity']),
                dict(obscuration=d['weather_obscuration']),
                dict(other=d['weather_other']),
            ],
        }

    def __repr__(self):
        return '<%s.%s>' % (__name__, self.__class__.__name__)
Example #5
0
class AlertManager:
    """
    A manager objects that provides a full interface to OWM Alert API. It implements CRUD methods on Trigger entities
    and read/deletion of related Alert objects

    :param API_key: the OWM web API key
    :type API_key: str
    :returns: an *AlertManager* instance
    :raises: *AssertionError* when no API Key is provided

    """
    def __init__(self, API_key):
        assert API_key is not None, 'You must provide a valid API Key'
        self.API_key = API_key
        self.trigger_parser = TriggerParser()
        self.alert_parser = AlertParser()
        self.http_client = HttpClient()

    def alert_api_version(self):
        return ALERT_API_VERSION

    # TRIGGER methods

    def create_trigger(self,
                       start,
                       end,
                       conditions,
                       area,
                       alert_channels=None):
        """
        Create a new trigger on the Alert API with the given parameters
        :param start: time object representing the time when the trigger begins to be checked
        :type start: int, ``datetime.datetime`` or ISO8601-formatted string
        :param end: time object representing the time when the trigger ends to be checked
        :type end: int, ``datetime.datetime`` or ISO8601-formatted string
        :param conditions: the `Condition` objects representing the set of checks to be done on weather variables
        :type conditions: list of `pyowm.utils.alertapi30.Condition` instances
        :param area: the geographic are over which conditions are checked: it can be composed by multiple geoJSON types
        :type area: list of geoJSON types
        :param alert_channels: the alert channels through which alerts originating from this `Trigger` can be consumed.
        Defaults to OWM API polling
        :type alert_channels: list of `pyowm.utils.alertapi30.AlertChannel` instances
        :returns:  a *Trigger* instance
        :raises: *ValueError* when start or end epochs are `None` or when end precedes start or when conditions or area
        are empty collections
        """
        assert start is not None
        assert end is not None

        # prepare time period
        unix_start = timeformatutils.to_UNIXtime(start)
        unix_end = timeformatutils.to_UNIXtime(end)
        unix_current = timeutils.now(timeformat='unix')
        if unix_start >= unix_end:
            raise ValueError(
                "The start timestamp must precede the end timestamp")
        delta_millis_start = timeutils.millis_offset_between_epochs(
            unix_current, unix_start)
        delta_millis_end = timeutils.millis_offset_between_epochs(
            unix_current, unix_end)
        the_time_period = {
            "start": {
                "expression": "after",
                "amount": delta_millis_start
            },
            "end": {
                "expression": "after",
                "amount": delta_millis_end
            }
        }

        assert conditions is not None
        if len(conditions) == 0:
            raise ValueError(
                'A trigger must contain at least one condition: you provided none'
            )
        the_conditions = [
            dict(name=c.weather_param, expression=c.operator, amount=c.amount)
            for c in conditions
        ]

        assert area is not None
        if len(area) == 0:
            raise ValueError(
                'The area for a trigger must contain at least one geoJSON type: you provided none'
            )
        the_area = [a.as_dict() for a in area]

        # >>> for the moment, no specific handling for alert channels

        status, payload = self.http_client.post(
            TRIGGERS_URI,
            params={'appid': self.API_key},
            data=dict(time_period=the_time_period,
                      conditions=the_conditions,
                      area=the_area),
            headers={'Content-Type': 'application/json'})
        return self.trigger_parser.parse_dict(payload)

    def get_triggers(self):
        """
        Retrieves all of the user's triggers that are set on the Weather Alert API.

        :returns: list of `pyowm.alertapi30.trigger.Trigger` objects

        """
        status, data = self.http_client.get_json(
            TRIGGERS_URI,
            params={'appid': self.API_key},
            headers={'Content-Type': 'application/json'})
        return [self.trigger_parser.parse_dict(item) for item in data]

    def get_trigger(self, trigger_id):
        """
        Retrieves the named trigger from the Weather Alert API.

        :param trigger_id: the ID of the trigger
        :type trigger_id: str
        :return: a `pyowm.alertapi30.trigger.Trigger` instance
        """
        stringutils.assert_is_string_or_unicode(trigger_id)
        status, data = self.http_client.get_json(
            NAMED_TRIGGER_URI % trigger_id,
            params={'appid': self.API_key},
            headers={'Content-Type': 'application/json'})
        return self.trigger_parser.parse_dict(data)

    def update_trigger(self, trigger):
        """
        Updates on the Alert API the trigger record having the ID of the specified Trigger object: the remote record is
        updated with data from the local Trigger object.

        :param trigger: the Trigger with updated data
        :type trigger: `pyowm.alertapi30.trigger.Trigger`
        :return: ``None`` if update is successful, an error otherwise
        """
        assert trigger is not None
        stringutils.assert_is_string_or_unicode(trigger.id)
        the_time_period = {
            "start": {
                "expression": "after",
                "amount": trigger.start_after_millis
            },
            "end": {
                "expression": "after",
                "amount": trigger.end_after_millis
            }
        }
        the_conditions = [
            dict(name=c.weather_param, expression=c.operator, amount=c.amount)
            for c in trigger.conditions
        ]
        the_area = [a.as_dict() for a in trigger.area]

        status, _ = self.http_client.put(
            NAMED_TRIGGER_URI % trigger.id,
            params={'appid': self.API_key},
            data=dict(time_period=the_time_period,
                      conditions=the_conditions,
                      area=the_area),
            headers={'Content-Type': 'application/json'})

    def delete_trigger(self, trigger):
        """
        Deletes from the Alert API the trigger record identified by the ID of the provided
        `pyowm.alertapi30.trigger.Trigger`, along with all related alerts

        :param trigger: the `pyowm.alertapi30.trigger.Trigger` object to be deleted
        :type trigger: `pyowm.alertapi30.trigger.Trigger`
        :returns: `None` if deletion is successful, an exception otherwise
        """
        assert trigger is not None
        stringutils.assert_is_string_or_unicode(trigger.id)
        status, _ = self.http_client.delete(
            NAMED_TRIGGER_URI % trigger.id,
            params={'appid': self.API_key},
            headers={'Content-Type': 'application/json'})

    # ALERTS methods

    def get_alerts_for(self, trigger):
        """
        Retrieves all of the alerts that were fired for the specified Trigger
        :param trigger: the trigger
        :type trigger: `pyowm.alertapi30.trigger.Trigger`
        :return: list of `pyowm.alertapi30.alert.Alert` objects
        """
        assert trigger is not None
        stringutils.assert_is_string_or_unicode(trigger.id)
        status, data = self.http_client.get_json(
            ALERTS_URI % trigger.id,
            params={'appid': self.API_key},
            headers={'Content-Type': 'application/json'})
        return [self.alert_parser.parse_dict(item) for item in data]

    def get_alert(self, alert_id, trigger):
        """
        Retrieves info about the alert record on the Alert API that has the specified ID and belongs to the specified
        parent Trigger object
        :param trigger: the parent trigger
        :type trigger: `pyowm.alertapi30.trigger.Trigger`
        :param alert_id: the ID of the alert
        :type alert_id `pyowm.alertapi30.alert.Alert`
        :return: an `pyowm.alertapi30.alert.Alert` instance
        """
        assert trigger is not None
        assert alert_id is not None
        stringutils.assert_is_string_or_unicode(alert_id)
        stringutils.assert_is_string_or_unicode(trigger.id)
        status, data = self.http_client.get_json(
            NAMED_ALERT_URI % (trigger.id, alert_id),
            params={'appid': self.API_key},
            headers={'Content-Type': 'application/json'})
        return self.alert_parser.parse_dict(data)

    def delete_all_alerts_for(self, trigger):
        """
        Deletes all of the alert that were fired for the specified Trigger
        :param trigger: the trigger whose alerts are to be cleared
        :type trigger: `pyowm.alertapi30.trigger.Trigger`
        :return: `None` if deletion is successful, an exception otherwise
        """
        assert trigger is not None
        stringutils.assert_is_string_or_unicode(trigger.id)
        status, _ = self.http_client.delete(
            ALERTS_URI % trigger.id,
            params={'appid': self.API_key},
            headers={'Content-Type': 'application/json'})

    def delete_alert(self, alert):
        """
        Deletes the specified alert from the Alert API
        :param alert: the alert to be deleted
        :type alert: pyowm.alertapi30.alert.Alert`
        :return: ``None`` if the deletion was successful, an error otherwise
        """
        assert alert is not None
        stringutils.assert_is_string_or_unicode(alert.id)
        stringutils.assert_is_string_or_unicode(alert.trigger_id)
        status, _ = self.http_client.delete(
            NAMED_ALERT_URI % (alert.trigger_id, alert.id),
            params={'appid': self.API_key},
            headers={'Content-Type': 'application/json'})
Example #6
0
class AlertManager:

    """
    A manager objects that provides a full interface to OWM Alert API. It implements CRUD methods on Trigger entities
    and read/deletion of related Alert objects

    :param API_key: the OWM Weather API key
    :type API_key: str
    :returns: an *AlertManager* instance
    :raises: *AssertionError* when no API Key is provided

    """

    def __init__(self, API_key):
        assert API_key is not None, 'You must provide a valid API Key'
        self.API_key = API_key
        self.trigger_parser = TriggerParser()
        self.alert_parser = AlertParser()
        self.http_client = HttpClient()

    def alert_api_version(self):
        return ALERT_API_VERSION

    # TRIGGER methods

    def create_trigger(self,  start, end, conditions, area, alert_channels=None):
        """
        Create a new trigger on the Alert API with the given parameters
        :param start: time object representing the time when the trigger begins to be checked
        :type start: int, ``datetime.datetime`` or ISO8601-formatted string
        :param end: time object representing the time when the trigger ends to be checked
        :type end: int, ``datetime.datetime`` or ISO8601-formatted string
        :param conditions: the `Condition` objects representing the set of checks to be done on weather variables
        :type conditions: list of `pyowm.utils.alertapi30.Condition` instances
        :param area: the geographic are over which conditions are checked: it can be composed by multiple geoJSON types
        :type area: list of geoJSON types
        :param alert_channels: the alert channels through which alerts originating from this `Trigger` can be consumed.
        Defaults to OWM API polling
        :type alert_channels: list of `pyowm.utils.alertapi30.AlertChannel` instances
        :returns:  a *Trigger* instance
        :raises: *ValueError* when start or end epochs are `None` or when end precedes start or when conditions or area
        are empty collections
        """
        assert start is not None
        assert end is not None

        # prepare time period
        unix_start = timeformatutils.to_UNIXtime(start)
        unix_end = timeformatutils.to_UNIXtime(end)
        unix_current = timeutils.now(timeformat='unix')
        if unix_start >= unix_end:
            raise ValueError("The start timestamp must precede the end timestamp")
        delta_millis_start = timeutils.millis_offset_between_epochs(unix_current, unix_start)
        delta_millis_end = timeutils.millis_offset_between_epochs(unix_current, unix_end)
        the_time_period = {
            "start": {
                "expression": "after",
                "amount": delta_millis_start
            },
            "end": {
                "expression": "after",
                "amount": delta_millis_end
            }
        }

        assert conditions is not None
        if len(conditions) == 0:
            raise ValueError('A trigger must contain at least one condition: you provided none')
        the_conditions = [dict(name=c.weather_param, expression=c.operator, amount=c.amount) for c in conditions]

        assert area is not None
        if len(area) == 0:
            raise ValueError('The area for a trigger must contain at least one geoJSON type: you provided none')
        the_area = [a.as_dict() for a in area]

        # >>> for the moment, no specific handling for alert channels

        status, payload = self.http_client.post(
            TRIGGERS_URI,
            params={'appid': self.API_key},
            data=dict(time_period=the_time_period, conditions=the_conditions, area=the_area),
            headers={'Content-Type': 'application/json'})
        return self.trigger_parser.parse_dict(payload)

    def get_triggers(self):
        """
        Retrieves all of the user's triggers that are set on the Weather Alert API.

        :returns: list of `pyowm.alertapi30.trigger.Trigger` objects

        """
        status, data = self.http_client.get_json(
            TRIGGERS_URI,
            params={'appid': self.API_key},
            headers={'Content-Type': 'application/json'})
        return [self.trigger_parser.parse_dict(item) for item in data]

    def get_trigger(self, trigger_id):
        """
        Retrieves the named trigger from the Weather Alert API.

        :param trigger_id: the ID of the trigger
        :type trigger_id: str
        :return: a `pyowm.alertapi30.trigger.Trigger` instance
        """
        assert isinstance(trigger_id, str), "Value must be a string"
        status, data = self.http_client.get_json(
            NAMED_TRIGGER_URI % trigger_id,
            params={'appid': self.API_key},
            headers={'Content-Type': 'application/json'})
        return self.trigger_parser.parse_dict(data)

    def update_trigger(self, trigger):
        """
        Updates on the Alert API the trigger record having the ID of the specified Trigger object: the remote record is
        updated with data from the local Trigger object.

        :param trigger: the Trigger with updated data
        :type trigger: `pyowm.alertapi30.trigger.Trigger`
        :return: ``None`` if update is successful, an error otherwise
        """
        assert trigger is not None
        assert isinstance(trigger.id, str), "Value must be a string"
        the_time_period = {
            "start": {
                "expression": "after",
                "amount": trigger.start_after_millis
            },
            "end": {
                "expression": "after",
                "amount": trigger.end_after_millis
            }
        }
        the_conditions = [dict(name=c.weather_param, expression=c.operator, amount=c.amount) for c in trigger.conditions]
        the_area = [a.as_dict() for a in trigger.area]

        status, _ = self.http_client.put(
            NAMED_TRIGGER_URI % trigger.id,
            params={'appid': self.API_key},
            data=dict(time_period=the_time_period, conditions=the_conditions, area=the_area),
            headers={'Content-Type': 'application/json'})

    def delete_trigger(self, trigger):
        """
        Deletes from the Alert API the trigger record identified by the ID of the provided
        `pyowm.alertapi30.trigger.Trigger`, along with all related alerts

        :param trigger: the `pyowm.alertapi30.trigger.Trigger` object to be deleted
        :type trigger: `pyowm.alertapi30.trigger.Trigger`
        :returns: `None` if deletion is successful, an exception otherwise
        """
        assert trigger is not None
        assert isinstance(trigger.id, str), "Value must be a string"
        status, _ = self.http_client.delete(
            NAMED_TRIGGER_URI % trigger.id,
            params={'appid': self.API_key},
            headers={'Content-Type': 'application/json'})

    # ALERTS methods

    def get_alerts_for(self, trigger):
        """
        Retrieves all of the alerts that were fired for the specified Trigger
        :param trigger: the trigger
        :type trigger: `pyowm.alertapi30.trigger.Trigger`
        :return: list of `pyowm.alertapi30.alert.Alert` objects
        """
        assert trigger is not None
        assert isinstance(trigger.id, str), "Value must be a string"
        status, data = self.http_client.get_json(
            ALERTS_URI % trigger.id,
            params={'appid': self.API_key},
            headers={'Content-Type': 'application/json'})
        return [self.alert_parser.parse_dict(item) for item in data]

    def get_alert(self, alert_id, trigger):
        """
        Retrieves info about the alert record on the Alert API that has the specified ID and belongs to the specified
        parent Trigger object
        :param trigger: the parent trigger
        :type trigger: `pyowm.alertapi30.trigger.Trigger`
        :param alert_id: the ID of the alert
        :type alert_id `pyowm.alertapi30.alert.Alert`
        :return: an `pyowm.alertapi30.alert.Alert` instance
        """
        assert trigger is not None
        assert alert_id is not None
        assert isinstance(alert_id, str), "Value must be a string"
        assert isinstance(trigger.id, str), "Value must be a string"
        status, data = self.http_client.get_json(
            NAMED_ALERT_URI % (trigger.id, alert_id),
            params={'appid': self.API_key},
            headers={'Content-Type': 'application/json'})
        return self.alert_parser.parse_dict(data)

    def delete_all_alerts_for(self, trigger):
        """
        Deletes all of the alert that were fired for the specified Trigger
        :param trigger: the trigger whose alerts are to be cleared
        :type trigger: `pyowm.alertapi30.trigger.Trigger`
        :return: `None` if deletion is successful, an exception otherwise
        """
        assert trigger is not None
        assert isinstance(trigger.id, str), "Value must be a string"
        status, _ = self.http_client.delete(
            ALERTS_URI % trigger.id,
            params={'appid': self.API_key},
            headers={'Content-Type': 'application/json'})

    def delete_alert(self, alert):
        """
        Deletes the specified alert from the Alert API
        :param alert: the alert to be deleted
        :type alert: pyowm.alertapi30.alert.Alert`
        :return: ``None`` if the deletion was successful, an error otherwise
        """
        assert alert is not None
        assert isinstance(alert.id, str), "Value must be a string"
        assert isinstance(alert.trigger_id, str), "Value must be a string"
        status, _ = self.http_client.delete(
            NAMED_ALERT_URI % (alert.trigger_id, alert.id),
            params={'appid': self.API_key},
            headers={'Content-Type': 'application/json'})
Example #7
0
class AgroManager(object):

    """
    A manager objects that provides a full interface to OWM Agro API.

    :param API_key: the OWM Weather API key
    :type API_key: str
    :returns: an `AgroManager` instance
    :raises: `AssertionError` when no API Key is provided

    """

    def __init__(self, API_key):
        assert API_key is not None, 'You must provide a valid API Key'
        self.API_key = API_key
        self.http_client = HttpClient()

    def agro_api_version(self):
        return AGRO_API_VERSION

    # POLYGON API subset methods

    def create_polygon(self, geopolygon, name=None):
        """
        Create a new polygon on the Agro API with the given parameters

        :param geopolygon: the geopolygon representing the new polygon
        :type geopolygon: `pyowm.utils.geo.Polygon` instance
        :param name: optional mnemonic name for the new polygon
        :type name: str
        :return: a `pyowm.agro10.polygon.Polygon` instance
        """
        assert geopolygon is not None
        assert isinstance(geopolygon, GeoPolygon)
        data = dict()
        data['geo_json'] = {
            "type": "Feature",
            "properties": {},
            "geometry": geopolygon.as_dict()
        }
        if name is not None:
            data['name'] = name
        status, payload = self.http_client.post(
            POLYGONS_URI,
            params={'appid': self.API_key},
            data=data,
            headers={'Content-Type': 'application/json'})
        return Polygon.from_dict(payload)

    def get_polygons(self):
        """
        Retrieves all of the user's polygons registered on the Agro API.

        :returns: list of `pyowm.agro10.polygon.Polygon` objects

        """

        status, data = self.http_client.get_json(
            POLYGONS_URI,
            params={'appid': self.API_key},
            headers={'Content-Type': 'application/json'})
        return [Polygon.from_dict(item) for item in data]

    def get_polygon(self, polygon_id):
        """
        Retrieves a named polygon registered on the Agro API.

        :param id: the ID of the polygon
        :type id: str
        :returns: a `pyowm.agro10.polygon.Polygon` object

        """
        status, data = self.http_client.get_json(
            NAMED_POLYGON_URI % str(polygon_id),
            params={'appid': self.API_key},
            headers={'Content-Type': 'application/json'})
        return Polygon.from_dict(data)

    def update_polygon(self, polygon):
        """
        Updates on the Agro API the Polygon identified by the ID of the provided polygon object.
        Currently this only changes the mnemonic name of the remote polygon

        :param polygon: the `pyowm.agro10.polygon.Polygon` object to be updated
        :type polygon: `pyowm.agro10.polygon.Polygon` instance
        :returns: `None` if update is successful, an exception otherwise
        """
        assert polygon.id is not None
        status, _ = self.http_client.put(
            NAMED_POLYGON_URI % str(polygon.id),
            params={'appid': self.API_key},
            data=dict(name=polygon.name),
            headers={'Content-Type': 'application/json'})

    def delete_polygon(self, polygon):
        """
        Deletes on the Agro API the Polygon identified by the ID of the provided polygon object.

        :param polygon: the `pyowm.agro10.polygon.Polygon` object to be deleted
        :type polygon: `pyowm.agro10.polygon.Polygon` instance
        :returns: `None` if deletion is successful, an exception otherwise
        """
        assert polygon.id is not None
        status, _ = self.http_client.delete(
            NAMED_POLYGON_URI % str(polygon.id),
            params={'appid': self.API_key},
            headers={'Content-Type': 'application/json'})

    # SOIL API subset methods

    def soil_data(self, polygon):
        """
        Retrieves the latest soil data on the specified polygon

        :param polygon: the reference polygon you want soil data for
        :type polygon: `pyowm.agro10.polygon.Polygon` instance
        :returns: a `pyowm.agro10.soil.Soil` instance

        """
        assert polygon is not None
        assert isinstance(polygon, Polygon)
        polyd = polygon.id
        status, data = self.http_client.get_json(
            SOIL_URI,
            params={'appid': self.API_key,
                    'polyid': polyd},
            headers={'Content-Type': 'application/json'})
        the_dict = dict()
        the_dict['reference_time'] = data['dt']
        the_dict['surface_temp'] = data['t0']
        the_dict['ten_cm_temp'] = data['t10']
        the_dict['moisture'] = data['moisture']
        the_dict['polygon_id'] = polyd
        return Soil.from_dict(the_dict)

    # Satellite Imagery subset methods

    def search_satellite_imagery(self, polygon_id, acquired_from, acquired_to, img_type=None, preset=None,
                                 min_resolution=None, max_resolution=None, acquired_by=None, min_cloud_coverage=None,
                                 max_cloud_coverage=None, min_valid_data_coverage=None, max_valid_data_coverage=None):
        """
        Searches on the Agro API the metadata for all available satellite images that contain the specified polygon and
        acquired during the specified time interval; and optionally matching the specified set of filters:
        - image type (eg. GeoTIF)
        - image preset (eg. false color, NDVI, ...)
        - min/max acquisition resolution
        - acquiring satellite
        - min/max cloud coverage on acquired scene
        - min/max valid data coverage on acquired scene

        :param polygon_id: the ID of the reference polygon
        :type polygon_id: str
        :param acquired_from: lower edge of acquisition interval, UNIX timestamp
        :type acquired_from: int
        :param acquired_to: upper edge of acquisition interval, UNIX timestamp
        :type acquired_to: int
        :param img_type: the desired file format type of the images. Allowed values are given by `pyowm.commons.enums.ImageTypeEnum`
        :type img_type: `pyowm.commons.databoxes.ImageType`
        :param preset: the desired preset of the images. Allowed values are given by `pyowm.agroapi10.enums.PresetEnum`
        :type preset: str
        :param min_resolution: minimum resolution for images, px/meters
        :type min_resolution: int
        :param max_resolution: maximum resolution for images, px/meters
        :type max_resolution: int
        :param acquired_by: short symbol of the satellite that acquired the image (eg. "l8")
        :type acquired_by: str
        :param min_cloud_coverage: minimum cloud coverage percentage on acquired images
        :type min_cloud_coverage: int
        :param max_cloud_coverage: maximum cloud coverage percentage on acquired images
        :type max_cloud_coverage: int
        :param min_valid_data_coverage: minimum valid data coverage percentage on acquired images
        :type min_valid_data_coverage: int
        :param max_valid_data_coverage: maximum valid data coverage percentage on acquired images
        :type max_valid_data_coverage: int
        :return: a list of `pyowm.agro10.imagery.MetaImage` subtypes instances
        """
        assert polygon_id is not None
        assert acquired_from is not None
        assert acquired_to is not None
        assert acquired_from <= acquired_to, 'Start timestamp of acquisition window must come before its end'
        if min_resolution is not None:
            assert min_resolution > 0, 'Minimum resolution must be positive'
        if max_resolution is not None:
            assert max_resolution > 0, 'Maximum resolution must be positive'
        if min_resolution is not None and max_resolution is not None:
            assert min_resolution <= max_resolution, 'Mininum resolution must be lower than maximum resolution'
        if min_cloud_coverage is not None:
            assert min_cloud_coverage >= 0, 'Minimum cloud coverage must be non negative'
        if max_cloud_coverage is not None:
            assert max_cloud_coverage >= 0, 'Maximum cloud coverage must be non negative'
        if min_cloud_coverage is not None and max_cloud_coverage is not None:
            assert min_cloud_coverage <= max_cloud_coverage, 'Minimum cloud coverage must be lower than maximum cloud coverage'
        if min_valid_data_coverage is not None:
            assert min_valid_data_coverage >= 0, 'Minimum valid data coverage must be non negative'
        if max_valid_data_coverage is not None:
            assert max_valid_data_coverage >= 0, 'Maximum valid data coverage must be non negative'
        if min_valid_data_coverage is not None and max_valid_data_coverage is not None:
            assert min_valid_data_coverage <= max_valid_data_coverage, 'Minimum valid data coverage must be lower than maximum valid data coverage'

        # prepare params
        params = dict(appid=self.API_key, polyid=polygon_id, start=acquired_from, end=acquired_to)
        if min_resolution is not None:
            params['resolution_min'] = min_resolution
        if max_resolution is not None:
            params['resolution_max'] = max_resolution
        if acquired_by is not None:
            params['type'] = acquired_by
        if min_cloud_coverage is not None:
            params['clouds_min'] = min_cloud_coverage
        if max_cloud_coverage is not None:
            params['clouds_max'] = max_cloud_coverage
        if min_valid_data_coverage is not None:
            params['coverage_min'] = min_valid_data_coverage
        if max_valid_data_coverage is not None:
            params['coverage_max'] = max_valid_data_coverage

        # call API
        status, data = self.http_client.get_json(SATELLITE_IMAGERY_SEARCH_URI, params=params)

        result_set = SatelliteImagerySearchResultSet(polygon_id, data, timeutils.now(timeformat='unix'))

        # further filter by img_type and/or preset (if specified)
        if img_type is not None and preset is not None:
            return result_set.with_img_type_and_preset(img_type, preset)
        elif img_type is not None:
            return result_set.with_img_type(img_type)
        elif preset is not None:
            return result_set.with_preset(preset)
        else:
            return result_set.all()

    def download_satellite_image(self, metaimage, x=None, y=None, zoom=None, palette=None):
        """
        Downloads the satellite image described by the provided metadata. In case the satellite image is a tile, then
        tile coordinates and zoom must be provided. An optional palette ID can be provided, if supported by the
        downloaded preset (currently only NDVI is supported)

        :param metaimage: the satellite image's metadata, in the form of a `MetaImage` subtype instance
        :type metaimage: a `pyowm.agroapi10.imagery.MetaImage` subtype
        :param x: x tile coordinate (only needed in case you are downloading a tile image)
        :type x: int or `None`
        :param y: y tile coordinate (only needed in case you are downloading a tile image)
        :type y: int or `None`
        :param zoom: zoom level (only needed in case you are downloading a tile image)
        :type zoom: int or `None`
        :param palette: ID of the color palette of the downloaded images. Values are provided by `pyowm.agroapi10.enums.PaletteEnum`
        :type palette: str or `None`
        :return: a `pyowm.agroapi10.imagery.SatelliteImage` instance containing both image's metadata and data
        """
        if palette is not None:
            assert isinstance(palette, str)
            params = dict(paletteid=palette)
        else:
            palette = PaletteEnum.GREEN
            params = dict()
        # polygon PNG
        if isinstance(metaimage, MetaPNGImage):
            prepared_url = metaimage.url
            status, data = self.http_client.get_png(
                prepared_url, params=params)
            img = Image(data, metaimage.image_type)
            return SatelliteImage(metaimage, img, downloaded_on=timeutils.now(timeformat='unix'), palette=palette)
        # GeoTIF
        elif isinstance(metaimage, MetaGeoTiffImage):
            prepared_url = metaimage.url
            status, data = self.http_client.get_geotiff(
                prepared_url, params=params)
            img = Image(data, metaimage.image_type)
            return SatelliteImage(metaimage, img, downloaded_on=timeutils.now(timeformat='unix'), palette=palette)
        # tile PNG
        elif isinstance(metaimage, MetaTile):
            assert x is not None
            assert y is not None
            assert zoom is not None
            prepared_url = self._fill_url(metaimage.url, x, y, zoom)
            status, data = self.http_client.get_png(
                prepared_url, params=params)
            img = Image(data, metaimage.image_type)
            tile = Tile(x, y, zoom, None, img)
            return SatelliteImage(metaimage, tile, downloaded_on=timeutils.now(timeformat='unix'), palette=palette)
        else:
            raise ValueError("Cannot download: unsupported MetaImage subtype")

    def stats_for_satellite_image(self, metaimage):
        """
        Retrieves statistics for the satellite image described by the provided metadata.
        This is currently only supported 'EVI' and 'NDVI' presets

        :param metaimage: the satellite image's metadata, in the form of a `MetaImage` subtype instance
        :type metaimage: a `pyowm.agroapi10.imagery.MetaImage` subtype
        :return: dict
        """
        if metaimage.preset != PresetEnum.EVI and metaimage.preset != PresetEnum.NDVI:
            raise ValueError("Unsupported image preset: should be EVI or NDVI")
        if metaimage.stats_url is None:
            raise ValueError("URL for image statistics is not defined")
        status, data = self.http_client.get_json(metaimage.stats_url, params={})
        return data

    # Utilities
    def _fill_url(self, url_template, x, y, zoom):
        return url_template.replace('{x}', str(x)).replace('{y}', str(y)).replace('{z}', str(zoom))

    def __repr__(self):
        return '<%s.%s>' % (__name__, self.__class__.__name__)
class StationsManager(object):

    """
    A manager objects that provides a full interface to OWM Stations API. Mainly
    it implements CRUD methods on Station entities and the corresponding
    measured datapoints.

    :param API_key: the OWM web API key (defaults to ``None``)
    :type API_key: str
    :returns: a *StationsManager* instance
    :raises: *AssertionError* when no API Key is provided

    """

    def __init__(self, API_key):
        assert API_key is not None, 'You must provide a valid API Key'
        self.API_key = API_key
        self.stations_parser = StationParser()
        self.aggregated_measurements_parser = AggregatedMeasurementParser()
        self.http_client = HttpClient()

    def stations_api_version(self):
        return STATIONS_API_VERSION

    # STATIONS Methods

    def get_stations(self):
        """
        Retrieves all of the user's stations registered on the Stations API.

        :returns: list of *pyowm.stationsapi30.station.Station* objects

        """

        status, data = self.http_client.get_json(
            'http://api.openweathermap.org/data/3.0/stations',
            params={'appid': self.API_key},
            headers={'Content-Type': 'application/json'})
        return [self.stations_parser.parse_dict(item) for item in data]

    def get_station(self, id):
        """
        Retrieves a named station registered on the Stations API.

        :param id: the ID of the station
        :type id: str
        :returns: a *pyowm.stationsapi30.station.Station* object

        """
        status, data = self.http_client.get_json(
            'http://api.openweathermap.org/data/3.0/stations/%s' % str(id),
            params={'appid': self.API_key},
            headers={'Content-Type': 'application/json'})
        return self.stations_parser.parse_dict(data)

    def create_station(self, external_id, name, lat, lon, alt=None):
        """
        Create a new station on the Station API with the given parameters

        :param external_id: the user-given ID of the station
        :type external_id: str
        :param name: the name of the station
        :type name: str
        :param lat: latitude of the station
        :type lat: float
        :param lon: longitude of the station
        :type lon: float
        :param alt: altitude of the station
        :type alt: float
        :returns: the new *pyowm.stationsapi30.station.Station* object
        """
        assert external_id is not None
        assert name is not None
        assert lon is not None
        assert lat is not None
        if lon < -180.0 or lon > 180.0:
            raise ValueError("'lon' value must be between -180 and 180")
        if lat < -90.0 or lat > 90.0:
            raise ValueError("'lat' value must be between -90 and 90")
        if alt is not None:
            if alt < 0.0:
                raise ValueError("'alt' value must not be negative")
        status, payload = self.http_client.post(
            'http://api.openweathermap.org/data/3.0/stations',
            params={'appid': self.API_key},
            data=dict(external_id=external_id, name=name, lat=lat,
                      lon=lon, alt=alt),
            headers={'Content-Type': 'application/json'})
        return self.stations_parser.parse_dict(payload)

    def update_station(self, station):
        """
        Updates the Station API record identified by the ID of the provided
        *pyowm.stationsapi30.station.Station* object with all of its fields

        :param station: the *pyowm.stationsapi30.station.Station* object to be updated
        :type station: *pyowm.stationsapi30.station.Station*
        :returns: `None` if update is successful, an exception otherwise
        """
        assert station.id is not None
        status, _ = self.http_client.put(
            'http://api.openweathermap.org/data/3.0/stations/%s' % str(station.id),
            params={'appid': self.API_key},
            data=dict(external_id=station.external_id, name=station.name,
                      lat=station.lat, lon=station.lon, alt=station.alt),
            headers={'Content-Type': 'application/json'})

    def delete_station(self, station):
        """
        Deletes the Station API record identified by the ID of the provided
        *pyowm.stationsapi30.station.Station*, along with all its related
        measurements

        :param station: the *pyowm.stationsapi30.station.Station* object to be deleted
        :type station: *pyowm.stationsapi30.station.Station*
        :returns: `None` if deletion is successful, an exception otherwise
        """
        assert station.id is not None
        status, _ = self.http_client.delete(
            'http://api.openweathermap.org/data/3.0/stations/%s' % str(station.id),
            params={'appid': self.API_key},
            headers={'Content-Type': 'application/json'})

    # Measurements-related methods

    def send_measurement(self, measurement):
        """
        Posts the provided Measurement object's data to the Station API.

        :param measurement: the *pyowm.stationsapi30.measurement.Measurement*
          object to be posted
        :type measurement: *pyowm.stationsapi30.measurement.Measurement* instance
        :returns: `None` if creation is successful, an exception otherwise
        """
        assert measurement is not None
        assert measurement.station_id is not None
        status, _ = self.http_client.post(
            'http://api.openweathermap.org/data/3.0/measurements',
            params={'appid': self.API_key},
            data=[measurement.to_dict()],
            headers={'Content-Type': 'application/json'})

    def send_measurements(self, list_of_measurements):
        """
        Posts data about the provided list of Measurement objects to the
        Station API. The objects may be related to different station IDs.

        :param list_of_measurements: list of *pyowm.stationsapi30.measurement.Measurement*
          objects to be posted
        :type list_of_measurements: list of *pyowm.stationsapi30.measurement.Measurement*
          instances
        :returns: `None` if creation is successful, an exception otherwise
        """
        assert list_of_measurements is not None
        assert all([m.station_id is not None for m in list_of_measurements])
        msmts = [m.to_dict() for m in list_of_measurements]
        status, _ = self.http_client.post(
            'http://api.openweathermap.org/data/3.0/measurements',
            params={'appid': self.API_key},
            data=msmts,
            headers={'Content-Type': 'application/json'})

    def get_measurements(self, station_id, aggregated_on, from_timestamp,
                         to_timestamp, limit=100):
        """
        Reads measurements of a specified station recorded in the specified time
        window and aggregated on minute, hour or day. Optionally, the number of
        resulting measurements can be limited.

        :param station_id: unique station identifier
        :type station_id: str
        :param aggregated_on: aggregation time-frame for this measurement
        :type aggregated_on: string between 'm','h' and 'd'
        :param from_timestamp: Unix timestamp corresponding to the beginning of
          the time window
        :type from_timestamp: int
        :param to_timestamp: Unix timestamp corresponding to the end of the
          time window
        :type to_timestamp: int
        :param limit: max number of items to be returned. Defaults to 100
        :type limit: int
        :returns: list of *pyowm.stationsapi30.measurement.AggregatedMeasurement*
          objects
        """
        assert station_id is not None
        assert aggregated_on is not None
        assert from_timestamp is not None
        assert from_timestamp > 0
        assert to_timestamp is not None
        assert to_timestamp > 0
        if to_timestamp < from_timestamp:
            raise ValueError("End timestamp can't be earlier than begin timestamp")
        assert isinstance(limit, int)
        assert limit >= 0
        query = {'appid': self.API_key,
                 'station_id': station_id,
                 'type': aggregated_on,
                 'from': from_timestamp,
                 'to': to_timestamp,
                 'limit': limit}
        status, data = self.http_client.get_json(
            'http://api.openweathermap.org/data/3.0/measurements',
            params=query,
            headers={'Content-Type': 'application/json'})
        return [self.aggregated_measurements_parser.parse_dict(item) for item in data]

    def send_buffer(self, buffer):
        """
        Posts to the Stations API data about the Measurement objects contained
        into the provided Buffer instance.

        :param buffer: the *pyowm.stationsapi30.buffer.Buffer* instance whose
          measurements are to be posted
        :type buffer: *pyowm.stationsapi30.buffer.Buffer* instance
        :returns: `None` if creation is successful, an exception otherwise
        """
        assert buffer is not None
        msmts = []
        for x in buffer.measurements:
            m = x.to_dict()
            item = dict()
            item['station_id'] = m['station_id']
            item['dt'] = m['timestamp']
            item['temperature'] = m['temperature']
            item['wind_speed'] = m['wind_speed']
            item['wind_gust'] = m['wind_gust']
            item['wind_deg'] = m['wind_deg']
            item['pressure'] = m['pressure']
            item['humidity'] = m['humidity']
            item['rain_1h'] = m['rain_1h']
            item['rain_6h'] = m['rain_6h']
            item['rain_24h'] = m['rain_24h']
            item['snow_1h'] = m['snow_1h']
            item['snow_6h'] = m['snow_6h']
            item['snow_24h'] = m['snow_24h']
            item['dew_point'] = m['dew_point']
            item['humidex'] = m['humidex']
            item['heat_index'] = m['heat_index']
            item['visibility_distance'] = m['visibility_distance']
            item['visibility_prefix'] = m['visibility_prefix']
            item['clouds'] = [dict(distance=m['clouds_distance']),
                              dict(condition=m['clouds_condition']),
                              dict(cumulus=m['clouds_cumulus'])]
            item['weather'] = [
                dict(precipitation=m['weather_precipitation']),
                dict(descriptor=m['weather_descriptor']),
                dict(intensity=m['weather_intensity']),
                dict(proximity=m['weather_proximity']),
                dict(obscuration=m['weather_obscuration']),
                dict(other=m['weather_other'])]
            msmts.append(item)
        status, _ = self.http_client.post(
            'http://api.openweathermap.org/data/3.0/measurements',
            params={'appid': self.API_key},
            data=msmts,
            headers={'Content-Type': 'application/json'})