def cacheable_get_json(self): cached_data = '{"name": "james bond", "designation": "007"}' other_data = '{"name": "doctor no"}' def monkey_patched_get(uri, params=None, headers=None, timeout=None, verify=False): return MockResponse(200, other_data) requests.get = monkey_patched_get # cache hit cache = MockCache(cached_data) instance = HttpClient(cache=cache) status, data = instance.cacheable_get_json('http://anyurl.com') self.assertEqual(200, status) self.assertEqual(cached_data, data) # cache miss cache = MockCache(None) instance = HttpClient(cache=cache) status, data = instance.cacheable_get_json('http://anyurl.com') self.assertEqual(200, status) self.assertEqual(other_data, data) requests.get = self.requests_original_get
def __init__(self, API_key, map_layer): assert API_key is not None, 'You must provide a valid API Key' self.API_key = API_key assert map_layer is not None, 'You must provide a valid map layer name' assert isinstance(map_layer, str), 'Map layer name must be a string' self.map_layer = map_layer self.http_client = HttpClient()
def test_delete(self): # in case an empty payload is returned def monkey_patched_delete(uri, params=None, headers=None, proxies=None, json=None, timeout=None, verify=False): return MockResponse(204, None) requests.delete = monkey_patched_delete status, data = HttpClient('apikey', DEFAULT_CONFIG, 'anyurl.com').delete('/resource') self.assertIsNone(data) # in case a non-empty payload is returned expected_data = '{"message": "deleted"}' def monkey_patched_delete_returning_payload(uri, params=None, headers=None, proxies=None, json=None, timeout=None, verify=False): return MockResponse(204, expected_data) requests.delete = monkey_patched_delete_returning_payload status, data = HttpClient('apikey', DEFAULT_CONFIG, 'anyurl.com').delete('/resource') self.assertEqual(json.loads(expected_data), data) requests.delete = self.requests_original_delete
def test_escape_subdomain(self): # correct subscription type API_endpoint = 'http://%s.openweathermap.org/data/2.5/ep' api_subscription = 'free' expected = 'http://api.openweathermap.org/data/2.5/ep' result = HttpClient._escape_subdomain(API_endpoint, api_subscription) self.assertEqual(expected, result) # non-existing subscription type API_endpoint = 'http://%s.openweathermap.org/data/2.5/ep' try: HttpClient._escape_subdomain(API_endpoint, 'unexistent') self.fail() except ValueError: pass # no subdomains needed API_endpoint = 'http://test.com' result = HttpClient._escape_subdomain(API_endpoint, api_subscription) self.assertEqual(API_endpoint, result) # subscription type is None API_endpoint = 'http://test.com' result = HttpClient._escape_subdomain(API_endpoint, None) self.assertEqual(API_endpoint, result)
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.ap_client = airpollution_client.AirPollutionHttpClient( API_key, HttpClient(API_key, config, ROOT_POLLUTION_API_URL)) self.new_ap_client = airpollution_client.AirPollutionHttpClient( API_key, HttpClient(API_key, config, NEW_ROOT_POLLUTION_API_URL))
def __init__(self, API_key, map_layer, config): assert API_key is not None, 'You must provide a valid API Key' self.API_key = API_key assert map_layer is not None, 'You must provide a valid map layer name' assert isinstance(map_layer, str), 'Map layer name must be a string' self.map_layer = map_layer assert isinstance(config, dict) self.http_client = HttpClient(API_key, config, ROOT_TILE_URL, admits_subdomains=False)
def test_get_geotiff(self): # https://download.osgeo.org/geotiff/samples/made_up/bogota.tif config = DEFAULT_CONFIG.copy() config['connection']['use_ssl'] = True instance = HttpClient('fakeapikey', config, 'download.osgeo.org', admits_subdomains=False) status, data = instance.get_geotiff( 'geotiff/samples/made_up/bogota.tif') self.assertIsNotNone(data) self.assertIsInstance(data, bytes)
def test_to_url_with_unicode_chars_in_API_key(self): API_key = '£°test££' API_endpoint = 'http://%s.openweathermap.org/data/2.5/ep' api_subscription = 'free' result = HttpClient.to_url(API_endpoint, API_key, api_subscription) expected = 'http://api.openweathermap.org/data/2.5/ep?APPID=%C2%A3%C2%B0test%C2%A3%C2%A3' self.assertEqual(expected, result)
def test_to_url_with_no_API_key(self): API_endpoint = 'http://%s.openweathermap.org/data/2.5/ep' params = {'a': 1, 'b': 2} api_subscription = 'pro' result = HttpClient.to_url(API_endpoint, None, api_subscription) expected = 'http://pro.openweathermap.org/data/2.5/ep' self.assertEqual(expected, result)
def test_to_url(self): API_endpoint = 'http://%s.openweathermap.org/data/2.5/ep' API_key = 'test_API_key' api_subscription = 'free' result = HttpClient.to_url(API_endpoint, API_key, api_subscription) expected = 'http://api.openweathermap.org/data/2.5/ep?APPID=test_API_key' self.assertEqual(expected, expected)
def test_get_json_parse_error(self): def monkey_patched_get(uri, params=None, headers=None, proxies=None, timeout=None, verify=False): return MockResponse(200, 123846237647236) requests.get = monkey_patched_get self.assertRaises(pyowm.commons.exceptions.ParseAPIResponseError, HttpClient('apikey', DEFAULT_CONFIG, 'anyurl.com').get_json, '/resource', params=dict(a=1, b=2)) requests.get = self.requests_original_get
def test_put(self): expected_data = '{"key": "value"}' def monkey_patched_put(uri, params=None, headers=None, proxies=None, json=None, timeout=None, verify=False): return MockResponse(200, expected_data) requests.put = monkey_patched_put status, data = HttpClient('apikey', DEFAULT_CONFIG, 'anyurl.com').put('/resource', data=dict(key=7)) self.assertEqual(json.loads(expected_data), data) requests.put = self.requests_original_put
def test_get_json_parse_error(self): def monkey_patched_get(uri, params=None, headers=None): return MockResponse(200, 123846237647236) requests.get = monkey_patched_get self.assertRaises(parse_response_error.ParseResponseError, HttpClient().get_json, 'http://anyurl.com', params=dict(a=1, b=2)) requests.get = self.requests_original_get
def test_get_json(self): expected_data = '{"name": "james bond", "designation": "007"}' def monkey_patched_get(uri, params=None, headers=None): return MockResponse(200, expected_data) requests.get = monkey_patched_get status, data = HttpClient().get_json('http://anyurl.com') self.assertEquals(json.loads(expected_data), data) requests.get = self.requests_original_get
def test_get_json(self): expected_data = '{"name": "james bond", "designation": "007"}' def monkey_patched_get(uri, params=None, headers=None, proxies=None, timeout=None, verify=False): return MockResponse(200, expected_data) requests.get = monkey_patched_get status, data = HttpClient('apikey', DEFAULT_CONFIG, 'anyurl.com').get_json('/resource') self.assertEqual(json.loads(expected_data), data) requests.get = self.requests_original_get
def test_get_geotiff(self): expected_data = b'II*\x00\x08\x00\x04\x00k{\x84s\x84\x84\x8c\x84\x84\x84k\x84k\x84\x84k{s\x9c\x94k\x84' def monkey_patched_get(uri, stream=True, params=None, headers=None, proxies=None, timeout=None, verify=False): return MockResponse(200, expected_data) requests.get = monkey_patched_get status, data = HttpClient('apikey', DEFAULT_CONFIG, 'anyurl.com').get_geotiff('/resource') self.assertIsInstance(data, bytes) self.assertEqual(expected_data, data) requests.get = self.requests_original_get
def test_get_png(self): expected_data = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x01\x03\x00\x00\x00%\xdbV\xca\x00\x00\x00\x03PLTE\x00p\xff\xa5G\xab\xa1\x00\x00\x00\x01tRNS\xcc\xd24V\xfd\x00\x00\x00\nIDATx\x9ccb\x00\x00\x00\x06\x00\x0367|\xa8\x00\x00\x00\x00IEND\xaeB`\x82' def monkey_patched_get(uri, stream=True, params=None, headers=None, proxies=None, timeout=None, verify=False): return MockResponse(200, expected_data) requests.get = monkey_patched_get status, data = HttpClient('apikey', DEFAULT_CONFIG, 'anyurl.com').get_png('/resource') self.assertIsInstance(data, bytes) self.assertEqual(expected_data, data) requests.get = self.requests_original_get
def test_put(self): expected_data = '{"key": "value"}' def monkey_patched_put(uri, params=None, headers=None, json=None): return MockResponse(200, expected_data) requests.put = monkey_patched_put status, data = HttpClient().put('http://anyurl.com', data=dict(key=7)) self.assertEquals(json.loads(expected_data), data) requests.put = self.requests_original_put
def test_ssl_certs_verification_failure(self): # https://wrong.host.badssl.com does not have a valid SSL cert config = DEFAULT_CONFIG.copy() config['connection']['use_ssl'] = True config['connection']['verify_ssl_certs'] = True instance = HttpClient('fakeapikey', config, 'wrong.host.badssl.com', admits_subdomains=False) self.assertRaises(pyowm.commons.exceptions.InvalidSSLCertificateError, HttpClient.get_json, instance, '')
def test_is_success(self): self.assertTrue(HttpClient.is_success(200)) self.assertTrue(HttpClient.is_success(201)) self.assertTrue(HttpClient.is_success(299)) self.assertFalse(HttpClient.is_success(300)) self.assertFalse(HttpClient.is_success(400)) self.assertFalse(HttpClient.is_success(500))
def test_delete(self): # in case an empty payload is returned def monkey_patched_delete(uri, params=None, headers=None, json=None): return MockResponse(204, None) requests.delete = monkey_patched_delete status, data = HttpClient().delete('http://anyurl.com') self.assertIsNone(data) # in case a non-empty payload is returned expected_data = '{"message": "deleted"}' def monkey_patched_delete_returning_payload(uri, params=None, headers=None, json=None): return MockResponse(204, expected_data) requests.delete = monkey_patched_delete_returning_payload status, data = HttpClient().delete('http://anyurl.com') self.assertEquals(json.loads(expected_data), data) requests.delete = self.requests_original_delete
class TileManager: """ A manager objects that reads OWM map layers tile images . :param API_key: the OWM Weather API key :type API_key: str :param map_layer: the layer for which you want tiles fetched. Allowed map layers are specified by the `pyowm.tiles.enum.MapLayerEnum` enumerator class. :type map_layer: str :param config: the configuration dictionary :type config: dict :returns: a *TileManager* instance :raises: *AssertionError* when no API Key or no map layer is provided, or map layer name is not a string """ def __init__(self, API_key, map_layer, config): assert API_key is not None, 'You must provide a valid API Key' self.API_key = API_key assert map_layer is not None, 'You must provide a valid map layer name' assert isinstance(map_layer, str), 'Map layer name must be a string' self.map_layer = map_layer assert isinstance(config, dict) self.http_client = HttpClient(API_key, config, ROOT_TILE_URL, admits_subdomains=False) def get_tile(self, x, y, zoom): """ Retrieves the tile having the specified coordinates and zoom level :param x: horizontal tile number in OWM tile reference system :type x: int :param y: vertical tile number in OWM tile reference system :type y: int :param zoom: zoom level for the tile :type zoom: int :returns: a `pyowm.tiles.Tile` instance """ status, data = self.http_client.get_png( NAMED_MAP_LAYER_URL % self.map_layer + '/%s/%s/%s.png' % (zoom, x, y), params={'appid': self.API_key}) img = Image(data, ImageTypeEnum.PNG) return Tile(x, y, zoom, self.map_layer, img) def __repr__(self): return "<%s.%s - layer_name=%s>" % (__name__, self.__class__.__name__, self.map_layer)
def test_timeouts(self): timeout = 0.5 def monkey_patched_get_timeouting(uri, params=None, headers=None, proxies=None, timeout=timeout, verify=False): raise requests.exceptions.Timeout() requests.get = monkey_patched_get_timeouting config = DEFAULT_CONFIG.copy() config['connection']['timeout_secs'] = timeout try: status, data = HttpClient('apikey', config, 'anyurl.com').get_json('/resource') self.fail() except pyowm.commons.exceptions.TimeoutError: requests.get = self.requests_original_get
class TestHTTPClient(unittest.TestCase): instance = HttpClient() def test_get_json_against_httpbin_ok(self): # http://httpbin.org/ip status, data = self.instance.get_json('http://httpbin.org/ip') self.assertEqual(200, status) self.assertIsInstance(data, dict) def test_get_json_against_httpbin_status_code_ko(self): # http://httpbin.org/status/400 expected_status = 400 self.assertRaises(api_call_error.APICallError, HttpClient.get_json, self.instance, 'http://httpbin.org/status/' + str(expected_status)) def test_get_json_against_httpbin_parse_error(self): # http://httpbin.org/xml try: status, data = self.instance.get_json('http://httpbin.org/xml') self.fail() except parse_response_error.ParseResponseError: pass def test_put_against_httpbin(self): # http://httpbin.org/put formdata = dict(a=1, b=2, c=3) status, data = self.instance.put('http://httpbin.org/put', data=formdata) self.assertEqual(200, status) self.assertIsInstance(data, dict) self.assertEqual(formdata, data['json']) def test_delete_against_httpbin(self): # http://httpbin.org/delete formdata = dict(a=1, b=2, c=3) status, data = self.instance.delete('http://httpbin.org/delete', data=formdata) self.assertEqual(200, status) self.assertIsInstance(data, dict) self.assertEqual(formdata, data['json']) def test_ssl_certs_verification_failure(self): # https://wrong.host.badssl.com does not have a valid SSL cert client = HttpClient(timeout=4, use_ssl=True, verify_ssl_certs=True) self.assertRaises(api_call_error.APIInvalidSSLCertificateError, HttpClient.get_json, client, 'https://wrong.host.badssl.com')
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 test_timeouts(self): timeout = 0.5 def monkey_patched_get_timeouting(uri, params=None, headers=None, timeout=timeout, verify=False): raise requests.exceptions.Timeout() requests.get = monkey_patched_get_timeouting try: status, data = HttpClient( timeout=timeout).get_json('http://anyurl.com') self.fail() except api_call_error.APICallTimeoutError: requests.get = self.requests_original_get
class TileManager(object): """ A manager objects that reads OWM map layers tile images . :param API_key: the OWM Weather API key :type API_key: str :param map_layer: the layer for which you want tiles fetched. Allowed map layers are specified by the `pyowm.tiles.enum.MapLayerEnum` enumerator class. :type map_layer: str :returns: a *TileManager* instance :raises: *AssertionError* when no API Key or no map layer is provided, or map layer name is not a string """ def __init__(self, API_key, map_layer): assert API_key is not None, 'You must provide a valid API Key' self.API_key = API_key assert map_layer is not None, 'You must provide a valid map layer name' assert isinstance(map_layer, str), 'Map layer name must be a string' self.map_layer = map_layer self.http_client = HttpClient() def get_tile(self, x, y, zoom): """ Retrieves the tile having the specified coordinates and zoom level :param x: horizontal tile number in OWM tile reference system :type x: int :param y: vertical tile number in OWM tile reference system :type y: int :param zoom: zoom level for the tile :type zoom: int :returns: a `pyowm.tiles.Tile` instance """ status, data = self.http_client.get_png( ROOT_TILE_URL % self.map_layer + '/%s/%s/%s.png' % (zoom, x, y), params={'appid': self.API_key}) img = Image(data, ImageTypeEnum.PNG) return Tile(x, y, zoom, self.map_layer, img) def __repr__(self): return "<%s.%s - layer name=%s>" % (__name__, self.__class__.__name__, self.map_layer)
class TestHTTPClient(unittest.TestCase): instance = HttpClient() def test_get_json_against_httpbin_ok(self): # https://httpbin.org/ip status, data = self.instance.get_json('http://httpbin.org/ip') self.assertEqual(200, status) self.assertIsInstance(data, dict) def test_get_json_against_httpbin_status_code_ko(self): # https://httpbin.org/status/400 expected_status = 400 self.assertRaises(api_call_error.APICallError, HttpClient.get_json, self.instance, 'https://httpbin.org/status/' + str(expected_status)) def test_get_json_against_httpbin_parse_error(self): # https://httpbin.org/xml try: status, data = self.instance.get_json('http://httpbin.org/xml') self.fail() except parse_response_error.ParseResponseError: pass def test_put_against_httpbin(self): # https://httpbin.org/put formdata = dict(a=1, b=2, c=3) status, data = self.instance.put('http://httpbin.org/put', data=formdata) self.assertEqual(200, status) self.assertIsInstance(data, dict) self.assertEquals(formdata, data['json']) def test_delete_against_httpbin(self): # https://httpbin.org/delete formdata = dict(a=1, b=2, c=3) status, data = self.instance.delete('http://httpbin.org/delete', data=formdata) self.assertEqual(200, status) self.assertIsInstance(data, dict) self.assertEquals(formdata, data['json'])
class TestOWMHttpUVClient(unittest.TestCase): __test_cache = NullCache() __instance = UltraVioletHttpClient('xyz', HttpClient(cache=__test_cache)) def test_trim_to(self): ts = timeformatutils.to_date(1463041620) # 2016-05-12T08:27:00Z self.assertEqual(self.__instance._trim_to(ts, 'minute'), '2016-05-12T08:27Z') self.assertEqual(self.__instance._trim_to(ts, 'hour'), '2016-05-12T08Z') self.assertEqual(self.__instance._trim_to(ts, 'day'), '2016-05-12Z') self.assertEqual(self.__instance._trim_to(ts, 'month'), '2016-05Z') self.assertEqual(self.__instance._trim_to(ts, 'year'), '2016Z') self.assertRaises(ValueError, self.__instance._trim_to, ts, 'abcdef') def test_get_uvi(self): # case: current UV index params = {'lon': 8.25, 'lat': 43.75} expected = {k: str(v) for k, v in params.items()} def mock_func(uri, params=None, headers=None): return 200, (uri, params) self.__instance._client.cacheable_get_json = mock_func result = self.__instance.get_uvi(params) self.assertEqual('http://api.openweathermap.org/data/2.5/uvi?APPID=xyz', result[0]) self.assertEqual(expected, result[1]) def test_get_uvi_forecast(self): params = {'lon': 8.25, 'lat': 43.75} expected = {k: str(v) for k, v in params.items()} def mock_func(uri, params=None, headers=None): return 200, (uri, params) self.__instance._client.cacheable_get_json = mock_func result = self.__instance.get_uvi_forecast(params) self.assertEqual('http://api.openweathermap.org/data/2.5/uvi/forecast?APPID=xyz', result[0]) self.assertEqual(expected, result[1]) def test_get_uvi_history(self): params = {'lon': 8.25, 'lat': 43.75, 'start': 1498049953, 'end': 1498481991} expected = {k: str(v) for k, v in params.items()} def mock_func(uri, params=None, headers=None): return 200, (uri, params) self.__instance._client.cacheable_get_json = mock_func result = self.__instance.get_uvi_history(params) self.assertEqual('http://api.openweathermap.org/data/2.5/uvi/history?APPID=xyz', result[0]) self.assertEqual(expected, result[1])
def test_fix_schema_to_https(self): API_endpoint = 'http://api.openweathermap.org/data/2.5/ep?APPID=%C2%A3%C2%B0test%C2%A3%C2%A3' expected = 'https://api.openweathermap.org/data/2.5/ep?APPID=%C2%A3%C2%B0test%C2%A3%C2%A3' result = HttpClient._fix_schema(API_endpoint, True) self.assertEqual(expected, result)
def test_check_status_code(self): msg = 'Generic error' HttpClient.check_status_code(200, msg) with self.assertRaises(api_call_error.APICallError): HttpClient.check_status_code(400, msg) with self.assertRaises(api_response_error.UnauthorizedError): HttpClient.check_status_code(401, msg) with self.assertRaises(api_response_error.NotFoundError): HttpClient.check_status_code(404, msg) with self.assertRaises(api_call_error.BadGatewayError): HttpClient.check_status_code(502, msg) with self.assertRaises(api_call_error.APICallError): HttpClient.check_status_code(555, msg)
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__)
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()
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__)
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'})
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 __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 __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)
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'})