binder.py 6.93 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
9

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

14
15
re_path_template = re.compile('{\w+}')

Josh Roesslein's avatar
Josh Roesslein committed
16

17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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)

        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)
            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()
51

52
            if api.secure:
53
                self.scheme = 'https://'
54
            else:
55
56
57
58
                self.scheme = 'http://'

            if self.search_api:
                self.host = api.search_host
59
            else:
60
61
                self.host = api.host

62
63
64
65
66
67
            # 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.
            # See Issue http://github.com/joshthecoder/tweepy/issues/#issue/12
            self.headers['Host'] = self.host

68
69
70
        def build_parameters(self, args, kargs):
            self.parameters = {}
            for idx, arg in enumerate(args):
71
72
                if arg is None:
                    continue
73
74

                try:
75
                    self.parameters[self.allowed_param[idx]] = convert_to_utf8_str(arg)
76
77
78
79
80
81
82
83
84
                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)

85
                self.parameters[k] = convert_to_utf8_str(arg)
86
87
88
89
90

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

91
92
93
94
95
96
97
98
99
                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]
100

101
102
                self.path = self.path.replace(variable, value)

103
104
105
106
107
108
109
110
111
        def execute(self):
            # 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.
            if self.api.cache and self.method == 'GET':
112
                cache_result = self.api.cache.get(url)
113
114
115
116
117
                # 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:
118
119
                            if isinstance(result, Model):
                                result._api = self.api
120
                    else:
121
122
                        if isinstance(cache_result, Model):
                            cache_result._api = self.api
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
                    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:
                # Open connection
                # FIXME: add timeout
                if self.api.secure:
                    conn = httplib.HTTPSConnection(self.host)
                else:
                    conn = httplib.HTTPConnection(self.host)

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

                # Execute request
                try:
                    conn.request(self.method, url, headers=self.headers, body=self.post_data)
                    resp = conn.getresponse()
                except Exception, e:
148
                    raise TweepError('Failed to send request: %s' % e)
149
150
151
152
153
154

                # Exit request loop if non-retry error code
                if self.retry_errors:
                    if resp.status not in self.retry_errors: break
                else:
                    if resp.status == 200: break
155

156
157
158
                # Sleep before retrying request again
                time.sleep(self.retry_delay)
                retries_performed += 1
Josh Roesslein's avatar
Josh Roesslein committed
159

160
161
162
163
            # If an error was returned, throw an exception
            self.api.last_response = resp
            if resp.status != 200:
                try:
Ivo Wetzel's avatar
Ivo Wetzel committed
164
                    error_msg = self.api.parser.parse_error(resp.read())
165
166
                except Exception:
                    error_msg = "Twitter error response: status code = %s" % resp.status
167
                raise TweepError(error_msg, resp)
168
169

            # Parse the response payload
170
            result = self.api.parser.parse(self, resp.read())
171
172

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

174
175
176
            # Store result into cache if one is available.
            if self.api.cache and self.method == 'GET' and result:
                self.api.cache.store(url, result)
177

178
            return result
Josh Roesslein's avatar
Josh Roesslein committed
179
180


181
182
183
184
    def _call(api, *args, **kargs):

        method = APIMethod(api, args, kargs)
        return method.execute()
Josh Roesslein's avatar
Josh Roesslein committed
185

186
187

    # Set pagination mode
188
    if 'cursor' in APIMethod.allowed_param:
189
        _call.pagination_mode = 'cursor'
190
    elif 'page' in APIMethod.allowed_param:
191
        _call.pagination_mode = 'page'
192

Josh Roesslein's avatar
Josh Roesslein committed
193
194
    return _call