Statistics
| Revision:

gvsig-scripting / org.gvsig.scripting / trunk / org.gvsig.scripting / org.gvsig.scripting.app / org.gvsig.scripting.app.mainplugin / src / main / resources-plugin / scripting / lib / cartodb / cartodb.py @ 564

History | View | Annotate | Download (11.5 KB)

1
"""
2
  ** CartoDBClient **
3

4
    A simple CartoDB client to perform requests against the CartoDB API.
5
    Internally it uses OAuth
6

7
  * Requirements:
8

9
     python 2.6
10
     pip install -r requirements.txt
11

12
  * Example use:
13
        user =  'your@mail.com'
14
        password =  'XXXX'
15
        CONSUMER_KEY='XXXXXXXXXXXXXXXXXX'
16
        CONSUMER_SECRET='YYYYYYYYYYYYYYYYYYYYYYYYYY'
17
        cartodb_domain = 'vitorino'
18
        cl = CartoDBOAuth(CONSUMER_KEY, CONSUMER_SECRET, user, password, cartodb_domain)
19
        print cl.sql('select * from a')
20

21
"""
22
import httplib2
23
import warnings
24
import oauth2 as oauth
25
import requests
26
from requests_oauthlib import OAuth1Session
27

    
28
try:
29
    from urllib.parse import urlparse, parse_qsl, urlencode
30
except ImportError:
31
    # fall back to Python 2.x
32
    from urlparse import urlparse, parse_qsl
33
    from urllib import urlencode
34

    
35
try:
36
    import json
37
except ImportError:
38
    import simplejson as json
39

    
40
ACCESS_TOKEN_URL = '%(protocol)s://%(user)s.%(domain)s/oauth/access_token'
41
RESOURCE_URL = '%(protocol)s://%(user)s.%(domain)s/api/%(api_version)s/sql'
42
IMPORTS_URL = '%(protocol)s://%(user)s.%(domain)s/api/%(api_version)s/imports'
43

    
44

    
45
def proxyinfo2proxies(proxy_info):
46
    """
47
    Converts ProxyInfo object into a proxies dict
48
    :param proxy_info: ProxyInfo object
49
    :return: requests' proxies dict
50
    """
51
    proxies = {}
52

    
53
    if proxy_info.proxy_user and proxy_info.proxy_pass:
54
        credentials = "{user}:{password}@".format(user=proxy_info.proxy_user, password=proxy_info.proxy_pass)
55
    elif proxy_info.proxy_user:
56
        credentials = "{user}@".format(user=proxy_info.proxy_user)
57
    else:
58
        credentials = ''
59
    port = ":{port}".format(port=proxy_info.proxy_port) if proxy_info.proxy_port else ''
60

    
61
    proxy_url = "http://{credentials}{host}{port}".format(credentials=credentials, host=proxy_info.proxy_host, port=port)
62

    
63
    if proxy_info.applies_to("http"):
64
        proxies["http"] = proxy_url
65
    if proxy_info.applies_to("https"):
66
        proxies["https"] = proxy_url
67

    
68
    return proxies
69

    
70

    
71
def proxies2proxyinfo(proxies):
72
    """
73
    Converts proxies dict into a ProxyInfo object
74
    :param proxies: requests' proxies dict
75
    :return: ProxyInfo object
76
    """
77
    url_components = urlparse(proxies["http"]) if "http" in proxies else urlparse(proxies["https"])
78

    
79
    return httplib2.ProxyInfo(httplib2.socks.PROXY_TYPE_HTTP_NO_TUNNEL, url_components.hostname, url_components.port,
80
                              proxy_user=url_components.username, proxy_pass=url_components.password)
81

    
82

    
83
class CartoDBException(Exception):
84
    pass
85

    
86

    
87
class CartoDBBase(object):
88
    """ basic client to access cartodb api """
89
    MAX_GET_QUERY_LEN = 2048
90

    
91
    def __init__(self, cartodb_domain, host='cartodb.com', protocol='https', api_version=None, proxy_info=None, sql_api_version='v2', import_api_version='v1'):
92
        """
93
        :param cartodb_domain: Subdomain for API requests. It's called "cartodb_domain", but it's just a subdomain and doesn't have to live under cartodb.com
94
        :param host: Domain for API requests, even though it's called "host"
95
        :param protocol: Just use the default
96
        :param sql_api_version: Use default or 'v1' to avoid caching
97
        :param import_api_version: Only 'v1' is currently supported
98
        :param api_version: SQL API version, kept only for backward compatibility
99
        :param proxy_info: httplib2's ProxyInfo object or requests' proxy dict
100
        :return:
101
        """
102
        if api_version is None:
103
            api_version = sql_api_version
104
        self.resource_url = RESOURCE_URL % {'user': cartodb_domain, 'domain': host, 'protocol': protocol, 'api_version': api_version}
105
        self.imports_url = IMPORTS_URL % {'user': cartodb_domain, 'domain': host, 'protocol': protocol, 'api_version': import_api_version}
106
        self.host = host
107
        self.protocol = protocol
108
        self.api_version = api_version
109
        # For backwards compatibility, we need to support httplib2's ProxyInfo and requests' proxies objects
110
        # And we still need old-style ProxyInfo for the xAuth client
111
        if type(proxy_info) == httplib2.ProxyInfo:
112
            self.proxy_info = proxy_info
113
            self.proxies = proxyinfo2proxies(self.proxy_info)
114
        if type(proxy_info) == dict:
115
            self.proxies = proxy_info
116
            self.proxy_info = proxies2proxyinfo(self.proxies)
117
        else:
118
            self.proxy_info = None
119
            self.proxies = None
120

    
121
    def req(self, url, http_method="GET", http_headers=None, body=None, params=None, files=None):
122
        """
123
        Subclasses must implement this method, that will be used to send API requests with proper auth
124
        :param url: API URL, currently, only SQL API is supported
125
        :param http_method: "GET" or "POST"
126
        :param http_headers: requests' http_headers
127
        :param body: requests' "data"
128
        :param params: requests' "params"
129
        :param files: requests' "files"
130
        :return:
131
        """
132
        raise NotImplementedError('req method must be implemented')
133

    
134
    def get_response_data(self, resp, parse_json=True):
135
        """
136
        Get response data or throw an appropiate exception
137
        :param resp: requests' response object
138
        :param parse_json: if True, response will be parsed as JSON
139
        :return: response data, either as json or as a regular response.content object
140
        """
141
        if resp.status_code == requests.codes.ok:
142
            if parse_json:
143
                return resp.json()
144
            return resp.content
145
        elif resp.status_code == requests.codes.bad_request:
146
            r = resp.json()
147
            raise CartoDBException(r.get('error', False) or r.get('errors', 'Bad Request: ' + resp.text))
148
        elif resp.status_code == requests.codes.not_found:
149
            raise CartoDBException('Not found: ' + resp.url)
150
        elif resp.status_code == requests.codes.internal_server_error:
151
            raise CartoDBException('Internal server error')
152
        elif resp.status_code == requests.codes.unauthorized or resp.status_code == requests.codes.forbidden:
153
            raise CartoDBException('Access denied')
154
        else:
155
            raise CartoDBException('Unknown error occurred')
156

    
157
    def sql(self, sql, parse_json=True, do_post=True, format=None):
158
        """
159
        Executes SQL query in a CartoDB server
160
        :param sql:
161
        :param parse_json: Set it to False if you want raw reponse
162
        :param do_post: Set it to True to force post request
163
        :param format: Any of the data export formats allowed by CartoDB's SQL API
164
        :return: response data, either as json or as a regular response.content object
165
        """
166
        params = {'q': sql}
167
        if format:
168
            params['format'] = format
169
            if format not in ['json', 'geojson']:
170
                parse_json = False
171
        url = self.resource_url
172

    
173
        # depending on query size do a POST or GET
174
        if len(sql) < self.MAX_GET_QUERY_LEN and not do_post:
175
            resp = self.req(url, 'GET', params=params)
176
        else:
177
            resp = self.req(url, 'POST', body=params)
178

    
179
        return self.get_response_data(resp, parse_json)
180

    
181

    
182
class CartoDBOAuth(CartoDBBase):
183
    """
184
    This class provides you with authenticated access to CartoDB's APIs using your XAuth credentials.
185
    You can find your API key in https://USERNAME.cartodb.com/your_apps/oauth.
186
    """
187
    def __init__(self, key, secret, email, password, cartodb_domain, **kwargs):
188
        """
189
        :param key: XAuth consumer key
190
        :param secret: XAuth consumer secret
191
        :param email: User name on CartoDB (yes, no email)
192
        :param password: User password on CartoDB
193
        :param cartodb_domain: Subdomain for API requests. It's called "cartodb_domain", but it's just a subdomain and doesn't have to live under cartodb.com
194
        :param kwargs: Any other params to be sent to the parent class
195
        :return:
196
        """
197
        super(CartoDBOAuth, self).__init__(cartodb_domain, **kwargs)
198

    
199
        # Sadly, we xAuth is not supported by requests or any of its modules, so we need to stick to
200
        # oauth2 when it comes to getting the access token
201
        self.consumer_key = key
202
        self.consumer_secret = secret
203
        consumer = oauth.Consumer(self.consumer_key, self.consumer_secret)
204

    
205
        client = oauth.Client(consumer, proxy_info=self.proxy_info)
206
        client.set_signature_method = oauth.SignatureMethod_HMAC_SHA1()
207

    
208
        params = {}
209
        params["x_auth_username"] = email
210
        params["x_auth_password"] = password
211
        params["x_auth_mode"] = 'client_auth'
212

    
213
        # Get Access Token
214
        access_token_url = ACCESS_TOKEN_URL % {'user': cartodb_domain, 'domain': self.host, 'protocol': self.protocol}
215
        resp, token = client.request(access_token_url, method="POST", body=urlencode(params))
216
        if resp['status'] != '200':
217
            raise CartoDBException("%s: %s" % (resp['status'], token))
218
        access_token = dict(parse_qsl(token.decode()))
219

    
220
        # Prepare client (now this is requests again!)
221
        try:
222
            self.client = OAuth1Session(self.consumer_key, client_secret=self.consumer_secret, resource_owner_key=access_token['oauth_token'],
223
                                        resource_owner_secret=access_token['oauth_token_secret'])
224
        except KeyError:
225
            raise CartoDBException('Access denied')
226

    
227
    def req(self, url, http_method="GET", http_headers=None, body=None, params=None, files=None):
228
        """
229
        Make a XAuth-authorized request
230
        :param url: API URL, currently, only SQL API is supported
231
        :param http_method: "GET" or "POST"
232
        :param http_headers: requests' http_headers
233
        :param body: requests' "data"
234
        :param params: requests' "params"
235
        :param files: requests' "files"
236
        :return: requests' response object
237
        """
238
        return self.client.request(http_method.lower(), url, params=params, data=body, headers=http_headers, proxies=self.proxies, files=files)
239

    
240

    
241
class CartoDBAPIKey(CartoDBBase):
242
    """
243
    This class provides you with authenticated access to CartoDB's APIs using your API key.
244
    You can find your API key in https://USERNAME.cartodb.com/your_apps/api_key.
245
    This method is easier than use the oauth authentification but if less secure, it is recommended to use only using the https endpoint
246
    """
247
    def __init__(self, api_key, cartodb_domain, **kwargs):
248
        """
249
        :param api_key: API key
250
        :param cartodb_domain: Subdomain for API requests. It's called "cartodb_domain", but it's just a subdomain and doesn't have to live under cartodb.com
251
        :param kwargs: Any other params to be sent to the parent class
252
        :return:
253
        """
254
        super(CartoDBAPIKey, self).__init__(cartodb_domain, **kwargs)
255

    
256
        self.api_key = api_key
257
        self.client = requests
258

    
259
        if self.protocol != 'https':
260
            warnings.warn("You are using unencrypted API key authentication!!!")
261

    
262
    def req(self, url, http_method="GET", http_headers=None, body=None, params=None, files=None):
263
        """
264
        Make a API-key-authorized request
265
        :param url: API URL, currently, only SQL API is supported
266
        :param http_method: "GET" or "POST"
267
        :param http_headers: requests' http_headers
268
        :param body: requests' "data"
269
        :param params: requests' "params"
270
        :param files: requests' "files"
271
        :return: requests' response object
272
        """
273
        params = params or {}
274
        params.update({"api_key": self.api_key})
275

    
276
        return self.client.request(http_method.lower(), url, params=params, data=body, headers=http_headers, proxies=self.proxies, files=files)