Source code for openaq

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import json
import requests
import math

from pkg_resources import get_distribution
from .exceptions import ApiError

from .decorators import pandasize

__all__ = ['OpenAQ']

__version__ = "1.1.0"

class API(object):
    """Generic API wrapper object.
    """
    def __init__(self, **kwargs):
        self._key       = kwargs.pop('key', '')
        self._pswd      = kwargs.pop('pswd', '')
        self._version   = kwargs.pop('version', None)
        self._baseurl   = kwargs.pop('baseurl', None)
        self._headers   = {'content-type': 'application/json'}

    def _make_url(self, endpoint, **kwargs):
        """Internal method to create a url from an endpoint.
        :param endpoint: Endpoint for an API call
        :type endpoint: string
        :returns: url
        """
        endpoint = "{}/{}/{}".format(self._baseurl, self._version, endpoint)

        extra = []
        for key, value in kwargs.items():
            if isinstance(value, list) or isinstance(value, tuple):
                #value = ','.join(value)
                for v in value:
                    extra.append("{}={}".format(key, v))
            else:
                extra.append("{}={}".format(key, value))

        if len(extra) > 0:
            endpoint = '?'.join([endpoint, '&'.join(extra)])

        return endpoint

    def _send(self, endpoint, method='GET', **kwargs):
        """Make an API call of any method

        :param endpoint: API endpoint
        :param method: API call type. Options are PUT, POST, GET, DELETE

        :type endpoint: string
        :type method: string

        :returns: (status_code, json_response)

        :raises ApiError: raises an exception
        """
        auth = (self._key, self._pswd)
        url  = self._make_url(endpoint, **kwargs)

        if method == 'GET':
            resp = requests.get(url, auth=auth, headers=self._headers)
        else:
            raise ApiError("Invalid Method")

        if resp.status_code != 200:
            raise ApiError("A bad request was made: {}".format(resp.status_code))

        res = resp.json()

        # Add a 'pages' attribute to the meta data
        try:
            res['meta']['pages'] = math.ceil(res['meta']['found'] / res['meta']['limit'])
        except:
            pass

        return resp.status_code, res

    def _get(self, url, **kwargs):
        return self._send(url, 'GET', **kwargs)

[docs]class OpenAQ(API): """Create an instance of the OpenAQ API """
[docs] def __init__(self, version='v1', **kwargs): """Initialize the OpenAQ instance. :param version: API version. :param kwargs: API options. :type version: string :type kwargs: dictionary """ self._baseurl = 'https://api.openaq.org' super(OpenAQ, self).__init__(version=version, baseurl=self._baseurl)
[docs] @pandasize() def cities(self, **kwargs): """Returns a listing of cities within the platform. :param country: limit results by a certain country :param limit: limit results in the query. Default is 100. Max is 10000. :param page: paginate through the results. Default is 1. :param order_by: order by one or more fields (ex. order_by=['country', 'locations']). Default value is 'country' :param sort: define the sort order for one or more fields (ex. sort='desc') :param df: convert the output from json to a pandas DataFrame :param index: if returning as a DataFrame, set index to ('utc', 'local', None). The default is 'local' :return: dictionary containing the *city*, *country*, *count*, and number of *locations* :type country: 2-digit ISO code :type limit: number :type order_by: string or list of strings :type sort: string :type page: number :type country: string or array of strings :type df: bool :type index: string :Example: >>> import openaq >>> api = openaq.OpenAQ() >>> status, resp = api.cities() >>> resp['results'] [ { "city": "Amsterdam", "country": "NL", "count": 21301, "locations": 14 }, { "city": "Badhoevedorp", "country": "NL", "count": 2326, "locations": 1 }, ... ] """ return self._get('cities', **kwargs)
[docs] @pandasize() def countries(self, **kwargs): """Returns a listing of all countries within the platform :param order_by: order by one or more fields (ex. order_by=['cities', 'locations']). Default value is 'country' :param sort: define the sort order for one or more fields (ex. sort='desc') :param limit: change the number of results returned. Max is 10000. Default is 100. :param page: paginate through results. Default is 1. :param df: return the results as a pandas DataFrame :param index: if returning as a DataFrame, set index to ('utc', 'local', None). The default is local :type order_by: string or list :type sort: string :type limit: int :type page: int :type df: bool :type index: string :return: dictionary containing the *code*, *name*, *count*, *cities*, and number of *locations*. :Example: >>> import openaq >>> api = openaq.OpenAQ() >>> status, resp = api.countries() >>> resp['results'] [ { "cities": 174, "code": "AT", "count": 121987, "locations": 174, "name": "Austria" }, { "cities": 28, "code": "AU", "count": 1066179, "locations": 28, "name": "Australia", }, ... ] """ return self._get('countries', **kwargs)
[docs] @pandasize() def latest(self, **kwargs): """Provides the latest value of each parameter for each location :param city: limit results by a certain city. Defaults to ``None``. :param country: limit results by a certain country. Should be a 2-digit ISO country code. Defaults to ``None``. :param location: limit results by a city. Defaults to ``None``. :param parameter: limit results by a specific parameter. Options include [ pm25, pm10, so2, co, no2, o3, bc] :param has_geo: filter items that do or do not have geographic information. :param coordinates: center point (`lat`, `long`) used to get measurements within a certain area. (Ex: coordinates=40.23,34.17) :param radius: radius (in meters) used to get measurements. Must be used with coordinates. Default value is 2500. :param limit: change the number of results returned. Max is 10000. Default is 100. :param page: paginate through the results. :param df: return results as a pandas DataFrame :param index: if returning as a DataFrame, set index to ('utc', 'local', None). The default is local :type city: string :type country: string :type location: string :type parameter: string :type has_geo: bool :type coordinates: string :type radius: int :type limit: int :type page: int :type df: bool :type index: string :return: dictionary containing the *location*, *country*, *city*, and number of *measurements* :Example: >>> import openaq >>> api = openaq.OpenAQ() >>> status, resp = api.latest() >>> resp['results'] [ { "location": "Punjabi Bagh", "city": "Delhi", "country": "IN", "measurements": [ { "parameter": "so2", "value": 7.8, "unit": "ug/m3", "lastUpdated": "2015-07-24T11:30:00.000Z" }, { "parameter": "co", "value": 1.3, "unit": "mg/m3", "lastUpdated": "2015-07-24T11:30:00.000Z" }, ... ] ... } ] """ return self._get('latest', **kwargs)
[docs] @pandasize() def locations(self, **kwargs): """Provides metadata about distinct measurement locations :param city: Limit results by one or more cities. Defaults to ``None``. Can define as a single city (ex. city='Delhi'), a list of cities (ex. city=['Delhi', 'Mumbai']), or as a tuple (ex. city=('Delhi', 'Mumbai')). :param country: Limit results by one or more countries. Should be a 2-digit ISO country code as a string, a list, or a tuple. See `city` for details. :param location: Limit results by one or more locations. :param parameter: Limit results by one or more parameters. Options include [ pm25, pm10, so2, co, no2, o3, bc] :param has_geo: Filter items that do or do not have geographic information. :param coordinates: center point (`lat`, `long`) used to get measurements within a certain area. (Ex: coordinates=40.23,34.17) :param nearest: get the X nearest number of locations to `coordinates`. Must be used with coordinates. Wins over `radius` if both are present. Will add the `distance` property to locations. :param radius: radius (in meters) used to get measurements. Must be used with coordinates. Default value is 2500. :param order_by: order by one or more fields (ex. order_by=['country', 'count']). Default value is 'location' :param sort: define the sort order for one or more fields (ex. sort='desc') :param limit: change the number of results returned. Max is 10000. Default is 100. :param page: paginate through the results. :param df: return results as a pandas DataFrame :param index: if returning as a DataFrame, set index to ('utc', 'local', None). The default is local :type city: string, array, or tuple :type country: string, array, or tuple :type location: string, array, or tuple :type parameter: string, array, or tuple :type has_geo: bool :type coordinates: string :type nearest: int :type radius: int :type order_by: string or list :type sort: string :type limit: int :type page: int :type df: bool :type index: string :return: a dictionary containing the *location*, *country*, *city*, *count*, *distance*, *sourceName*, *sourceNames*, *firstUpdated*, *lastUpdated*, *parameters*, and *coordinates* :Example: >>> import openaq >>> api = openaq.OpenAQ() >>> status, resp = api.locations() >>> resp['results'] [ { "count": 4242, "sourceName": "Australia - New South Wales", "firstUpdated": "2015-07-24T11:30:00.000Z", "lastUpdated": "2015-07-24T11:30:00.000Z", "parameters": [ "pm25", "pm10", "so2", "co", "no2", "o3" ], "country": "AU", "city": "Central Coast", "location": "wyong" }, ... ] """ return self._get('locations', **kwargs)
[docs] @pandasize() def measurements(self, **kwargs): """Provides data about individual measurements :param city: Limit results by a certain city. Defaults to ``None``. :param country: Limit results by a certain country. Should be a 2-digit ISO country code. Defaults to ``None``. :param location: Limit results by a city. Defaults to ``None``. :param parameter: Limit results by one or more parameters. Options include [ pm25, pm10, so2, co, no2, o3, bc] :param has_geo: Filter items that do or do not have geographic information. :param coordinates: center point (`lat`, `long`) used to get measurements within a certain area. (Ex: coordinates=40.23,34.17) :param radius: radius (in meters) used to get measurements. Must be used with `coordinates`. Default value is 2500. :param value_from: Show results above a value threshold. Must be used with `parameter`. :param value_to: Show results below a value threshold. Must be used with `parameter`. :param date_from: Show results after a certain date. Format should be ``Y-M-D``. :param date_to: Show results before a certain date. Format should be ``Y-M-D``. :param sort: The sort order (``asc`` or ``desc``). Must be used with `order_by`. :param order_by: Field to sort by. Must be used with **sort**. :param include_fields: Include additional fields in the output. Allowed values are: *attribution*, *averagingPeriod*, and *sourceName*. :param limit: Change the number of results returned. Max is 10000 and default is 100. :param page: Paginate through the results :param df: return the results as a pandas DataFrame :param index: if returning as a DataFrame, set index to ('utc', 'local', None). The default is local :type city: string :type country: string :type location: string :type parameter: string, array, or tuple :type has_geo: bool :type coordinates: string :type radius: int :type value_from: number :type value_to: number :type date_from: date :type date_to: date :type sort: string :type order_by: string :type include_fields: array :type limit: number :type page: number :type df: bool :type index: string :return: a dictionary containing the *date*, *parameter*, *value*, *unit*, *location*, *country*, *city*, *coordinates*, and *sourceName*. :Example: >>> import openaq >>> api = openaq.OpenAQ() >>> status, resp = api.measurements(city = 'Delhi') >>> resp['results'] { "parameter": "Ammonia", "date": { "utc": "2015-07-16T20:30:00.000Z", 'local': "2015-07-16T18:30:00.000-02:00" }, "value": "72.9", "unit": "ug/m3", "location": "Anand Vihar", "country": "IN", "city": "Delhi", "coordinates": { "latitude": 43.34, "longitude": 23.04 }, "attribution": { "name": "SINCA", "url": "http://sinca.mma.gob.cl/" }, { "name": "Ministerio del Medio Ambiente" } ... } """ return self._get('measurements', **kwargs)
[docs] def fetches(self, **kwargs): """Provides data about individual fetch operations that are used to populate data in the platform. :param order_by: order by one or more fields (ex. order_by=['timeEnded', 'count']). Default value is 'country' :param sort: define the sort order for one or more fields (ex. sort='desc') :param limit: change the number of results returned. Max is 10000. Default is 100. :param page: paginate through the results. Default is 1. :type order_by: string or list :type sort: string :type limit: int :type page: int :return: dictionary containing the *timeStarted*, *timeEnded*, *count*, and *results* :Example: >>> import openaq >>> api = openaq.OpenAQ() >>> status, resp = api.fetches() >>> resp { "meta": { "name": "openaq-api", "license": "website": "page": 1, "limit": 100, "found": 3, "pages": 1 }, "results": [ { "count": 0, "results": [ { "message": "New measurements inserted for Mandir Marg: 1", "failures": {}, "count": 0, "duration": 0.153, "sourceName": "Mandir Marg" }, { "message": "New measurements inserted for Sao Paulo: 1898", "failures": {}, "count": 1898, "duration": 16.918, "sourceName": "Sao Paulo" }, ... ], "timeStarted": "2016-02-07T15:25:04.603Z", "timeEnded": "2016-02-07T15:25:04.793Z", } ] } """ return self._get('fetches', **kwargs)
[docs] @pandasize() def parameters(self, **kwargs): """ Provides a simple listing of parameters within the platform. :param order_by: order by one or more fields (ex. order_by=['preferredUnit', 'id']). Default value is 'country' :param sort: define the sort order for one or more fields (ex. sort='desc') :type order_by: string or list :type sort: string :return: a dictionary containing the *id*, *name*, *description*, and *preferredUnit*. :Example: >>> import openaq >>> api = openaq.OpenAQ() >>> status, resp = api.parameters() >>> resp['results'] [ { "id": "pm25", "name": "PM2.5", "description": "Particulate matter less than 2.5 micrometers in diameter", "preferredUnit": "ug/m3" } ... ] """ return self._get('parameters', **kwargs)
[docs] @pandasize() def sources(self, **kwargs): """ Provides a list of data sources. :param order_by: order by one or more fields (ex. order_by=['active', 'country']). Default value is 'country' :param sort: define the sort order for one or more fields (ex. sort='desc') :param limit: Change the number of results returned. :param page: Paginate through the results :param df: return the results as a pandas DataFrame :param index: if returning as a DataFrame, set index to ('utc', 'local', None). The default is local :type limit: number :type page: number :type df: bool :type index: string :type order_by: string or list :type sort: string :return: a dictionary containing the *url*, *adapter*, *name*, *city*, *country*, *description*, *resolution*, *sourceURL*, *contacts*, and *active*. :Example: >>> import openaq >>> api = openaq.OpenAQ() >>> status, resp = api.sources() >>> resp['results'] [ { "url": "http://airquality.environment.nsw.gov.au/aquisnetnswphp/getPage.php?reportid=2", "adapter": "nsw", "name": "Australia - New South Wales", "city": "", "country": "AU", "description": "Measurements from the Office of Environment & Heritage of the New South Wales government.", "resolution": "1 hr", "sourceURL": "http://www.environment.nsw.gov.au/AQMS/hourlydata.htm", "contacts": [ "olaf@developmentseed.org" ] } ... ] """ return self._get('sources', **kwargs)
def __repr__(self): return "OpenAQ API"