binder.py 9.55 KB
Newer Older
1
# Tweepy
2
3
# Copyright 2009-2010 Joshua Roesslein
# See LICENSE for details.
4

5
import httplib
Josh Roesslein's avatar
Josh Roesslein committed
6
import urllib
7
import time
8
import re
Mike's avatar
Mike committed
9
10
from StringIO import StringIO
import gzip
11

Josh Roesslein's avatar
Josh Roesslein committed
12
from tweepy.error import TweepError
13
from tweepy.utils import convert_to_utf8_str
14
from tweepy.models import Model
15

16
17
re_path_template = re.compile('{\w+}')

Josh Roesslein's avatar
Josh Roesslein committed
18

19
20
21
22
23
24
25
26
27
28
29
def bind_api(**config):

    class APIMethod(object):

        path = config['path']
        payload_type = config.get('payload_type', None)
        payload_list = config.get('payload_list', False)
        allowed_param = config.get('allowed_param', [])
        method = config.get('method', 'GET')
        require_auth = config.get('require_auth', False)
        search_api = config.get('search_api', False)
30
        use_cache = config.get('use_cache', True)
31
32
33
34
35
36
37
38
39
40
41
42

        def __init__(self, api, args, kargs):
            # If authentication is required and no credentials
            # are provided, throw an error.
            if self.require_auth and not api.auth:
                raise TweepError('Authentication required!')

            self.api = api
            self.post_data = kargs.pop('post_data', None)
            self.retry_count = kargs.pop('retry_count', api.retry_count)
            self.retry_delay = kargs.pop('retry_delay', api.retry_delay)
            self.retry_errors = kargs.pop('retry_errors', api.retry_errors)
43
            self.wait_on_rate_limit = kargs.pop('wait_on_rate_limit', api.wait_on_rate_limit)
44
            self.parser = kargs.pop('parser', api.parser)
45
46
47
48
49
50
51
52
53
54
55
            self.headers = kargs.pop('headers', {})
            self.build_parameters(args, kargs)

            # Pick correct URL root to use
            if self.search_api:
                self.api_root = api.search_root
            else:
                self.api_root = api.api_root

            # Perform any path variable substitution
            self.build_path()
56

57
            if api.secure:
58
                self.scheme = 'https://'
59
            else:
60
61
62
63
                self.scheme = 'http://'

            if self.search_api:
                self.host = api.search_host
64
            else:
65
66
                self.host = api.host

67
68
69
            # Manually set Host header to fix an issue in python 2.5
            # or older where Host is set including the 443 port.
            # This causes Twitter to issue 301 redirect.
Pablo Castellano's avatar
Pablo Castellano committed
70
            # See Issue https://github.com/tweepy/tweepy/issues/12
71
            self.headers['Host'] = self.host
72
73
74
            # Monitoring rate limits
            self._remaining_calls = None
            self._reset_time = None
75

76
77
78
        def build_parameters(self, args, kargs):
            self.parameters = {}
            for idx, arg in enumerate(args):
79
80
                if arg is None:
                    continue
81
                try:
82
                    self.parameters[self.allowed_param[idx]] = convert_to_utf8_str(arg)
83
84
85
86
87
88
89
90
91
                except IndexError:
                    raise TweepError('Too many parameters supplied!')

            for k, arg in kargs.items():
                if arg is None:
                    continue
                if k in self.parameters:
                    raise TweepError('Multiple values for parameter %s supplied!' % k)

92
                self.parameters[k] = convert_to_utf8_str(arg)
93
94
95
96
97

        def build_path(self):
            for variable in re_path_template.findall(self.path):
                name = variable.strip('{}')

98
99
100
101
102
103
104
105
106
                if name == 'user' and 'user' not in self.parameters and self.api.auth:
                    # No 'user' parameter provided, fetch it from Auth instead.
                    value = self.api.auth.get_username()
                else:
                    try:
                        value = urllib.quote(self.parameters[name])
                    except KeyError:
                        raise TweepError('No parameter value found for path variable: %s' % name)
                    del self.parameters[name]
107

108
109
                self.path = self.path.replace(variable, value)

110
        def execute(self):
Joshua Roesslein's avatar
Joshua Roesslein committed
111
112
            self.api.cached_result = False

113
114
115
116
117
118
119
            # Build the request URL
            url = self.api_root + self.path
            if len(self.parameters):
                url = '%s?%s' % (url, urllib.urlencode(self.parameters))

            # Query the cache if one is available
            # and this request uses a GET method.
120
            if self.use_cache and self.api.cache and self.method == 'GET':
121
                cache_result = self.api.cache.get(url)
122
123
124
125
126
                # if cache result found and not expired, return it
                if cache_result:
                    # must restore api reference
                    if isinstance(cache_result, list):
                        for result in cache_result:
127
128
                            if isinstance(result, Model):
                                result._api = self.api
129
                    else:
130
131
                        if isinstance(cache_result, Model):
                            cache_result._api = self.api
Joshua Roesslein's avatar
Joshua Roesslein committed
132
                    self.api.cached_result = True
133
134
135
136
137
138
                    return cache_result

            # Continue attempting request until successful
            # or maximum number of retries is reached.
            retries_performed = 0
            while retries_performed < self.retry_count + 1:
139
140
141
142
143
                # handle running out of api calls
                if self.wait_on_rate_limit and self._reset_time is not None and \
                   self._remaining_calls is not None and self._remaining_calls < 1:
                    sleep_time = self._reset_time - int(time.time())
                    if sleep_time > 0:
144
145
146
147
                        if self.wait_on_rate_limit_notify:
                            print "Max retries reached. Sleeping for: " + str(sleep_time)
                        time.sleep(sleep_time + 5) # sleep for few extra sec

148
149
                # Open connection
                if self.api.secure:
Joshua Roesslein's avatar
Joshua Roesslein committed
150
                    conn = httplib.HTTPSConnection(self.host, timeout=self.api.timeout)
151
                else:
Joshua Roesslein's avatar
Joshua Roesslein committed
152
                    conn = httplib.HTTPConnection(self.host, timeout=self.api.timeout)
153
154
155
156
157
158
159
160

                # Apply authentication
                if self.api.auth:
                    self.api.auth.apply_auth(
                            self.scheme + self.host + url,
                            self.method, self.headers, self.parameters
                    )

Mike's avatar
Mike committed
161
162
163
164
                # Request compression if configured
                if self.api.compression:
                    self.headers['Accept-encoding'] = 'gzip'

165
166
167
168
                # Execute request
                try:
                    conn.request(self.method, url, headers=self.headers, body=self.post_data)
                    resp = conn.getresponse()
169
                except Exception as e:
170
                    raise TweepError('Failed to send request: %s' % e)
171
172
                rem_calls = resp.getheader('x-rate-limit-remaining')
                if rem_calls is not None:
173
                    self._remaining_calls = int(rem_calls)
174
175
176
177
                elif isinstance(self._remaining_calls, int):
                    self._remaining_calls -= 1
                reset_time = resp.getheader('x-rate-limit-reset')
                if reset_time is not None:
178
                    self._reset_time = int(reset_time)
179
                if self.wait_on_rate_limit and self._remaining_calls == 0 and (resp.status == 429 or resp.status == 420): # if ran out of calls before waiting switching retry last call
180
                    continue
181
                retry_delay = self.retry_delay
182
                # Exit request loop if non-retry error code
183
184
185
186
187
188
                if resp.status == 200:
                    break
                elif (resp.status == 429 or resp.status == 420) and self.wait_on_rate_limit:
                    if 'retry-after' in resp.msg:
                        retry_delay = float(resp.msg['retry-after'])
                elif self.retry_errors and resp.status not in self.retry_errors:
189
                    break
190

191
                # Sleep before retrying request again
192
                time.sleep(retry_delay)
193
                retries_performed += 1
Josh Roesslein's avatar
Josh Roesslein committed
194

195
196
            # If an error was returned, throw an exception
            self.api.last_response = resp
197
            if resp.status and not 200 <= resp.status < 300:
198
                try:
199
                    error_msg = self.parser.parse_error(resp.read())
200
201
                except Exception:
                    error_msg = "Twitter error response: status code = %s" % resp.status
202
                raise TweepError(error_msg, resp)
203
204

            # Parse the response payload
Mike's avatar
Mike committed
205
206
207
208
209
            body = resp.read()
            if resp.getheader('Content-Encoding', '') == 'gzip':
                try:
                    zipper = gzip.GzipFile(fileobj=StringIO(body))
                    body = zipper.read()
210
                except Exception as e:
Mike's avatar
Mike committed
211
                    raise TweepError('Failed to decompress data: %s' % e)
212
213
            
            result = self.parser.parse(self, body)
214
215

            conn.close()
Josh Roesslein's avatar
Josh Roesslein committed
216

217
            # Store result into cache if one is available.
218
            if self.use_cache and self.api.cache and self.method == 'GET' and result:
219
                self.api.cache.store(url, result)
220

221
            return result
Josh Roesslein's avatar
Josh Roesslein committed
222

223
    def _call(api, create=False, *args, **kargs):
224
225

        method = APIMethod(api, args, kargs)
226
227
228
229
        if create:
            return method
        else:
            return method.execute()
Josh Roesslein's avatar
Josh Roesslein committed
230

231
232

    # Set pagination mode
233
    if 'cursor' in APIMethod.allowed_param:
234
        _call.pagination_mode = 'cursor'
235
236
237
    elif 'max_id' in APIMethod.allowed_param and \
         'since_id' in APIMethod.allowed_param:
        _call.pagination_mode = 'id'
238
    elif 'page' in APIMethod.allowed_param:
239
        _call.pagination_mode = 'page'
240

Josh Roesslein's avatar
Josh Roesslein committed
241
    return _call