예제 #1
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__)
예제 #2
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__)
예제 #3
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'})
예제 #4
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'})
예제 #5
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'})