]> jfr.im git - z_archive/twitter.git/blame_incremental - twitter/api.py
Merge pull request #124 from Adapptor/incompleteread
[z_archive/twitter.git] / twitter / api.py
... / ...
CommitLineData
1try:
2 import urllib.request as urllib_request
3 import urllib.error as urllib_error
4except ImportError:
5 import urllib2 as urllib_request
6 import urllib2 as urllib_error
7
8try:
9 from cStringIO import StringIO
10except ImportError:
11 from io import BytesIO as StringIO
12
13from twitter.twitter_globals import POST_ACTIONS
14from twitter.auth import NoAuth
15
16import re
17import gzip
18import httplib
19
20try:
21 import json
22except ImportError:
23 import simplejson as json
24
25class _DEFAULT(object):
26 pass
27
28class TwitterError(Exception):
29 """
30 Base Exception thrown by the Twitter object when there is a
31 general error interacting with the API.
32 """
33 pass
34
35class TwitterHTTPError(TwitterError):
36 """
37 Exception thrown by the Twitter object when there is an
38 HTTP error interacting with twitter.com.
39 """
40 def __init__(self, e, uri, format, uriparts):
41 self.e = e
42 self.uri = uri
43 self.format = format
44 self.uriparts = uriparts
45 if self.e.headers['Content-Encoding'] == 'gzip':
46 buf = StringIO(self.e.fp.read())
47 f = gzip.GzipFile(fileobj=buf)
48 self.response_data = f.read()
49 else:
50 self.response_data = self.e.fp.read()
51
52 def __str__(self):
53 fmt = ("." + self.format) if self.format else ""
54 return (
55 "Twitter sent status %i for URL: %s%s using parameters: "
56 "(%s)\ndetails: %s" %(
57 self.e.code, self.uri, fmt, self.uriparts,
58 self.response_data))
59
60class TwitterResponse(object):
61 """
62 Response from a twitter request. Behaves like a list or a string
63 (depending on requested format) but it has a few other interesting
64 attributes.
65
66 `headers` gives you access to the response headers as an
67 httplib.HTTPHeaders instance. You can do
68 `response.headers.get('h')` to retrieve a header.
69 """
70 def __init__(self, headers):
71 self.headers = headers
72
73 @property
74 def rate_limit_remaining(self):
75 """
76 Remaining requests in the current rate-limit.
77 """
78 return int(self.headers.get('X-Rate-Limit-Remaining', "0"))
79
80 @property
81 def rate_limit_limit(self):
82 """
83 The rate limit ceiling for that given request.
84 """
85 return int(self.headers.get('X-Rate-Limit-Limit', "0"))
86
87 @property
88 def rate_limit_reset(self):
89 """
90 Time in UTC epoch seconds when the rate limit will reset.
91 """
92 return int(self.headers.get('X-Rate-Limit-Reset', "0"))
93
94
95def wrap_response(response, headers):
96 response_typ = type(response)
97 if response_typ is bool:
98 # HURF DURF MY NAME IS PYTHON AND I CAN'T SUBCLASS bool.
99 response_typ = int
100
101 class WrappedTwitterResponse(response_typ, TwitterResponse):
102 __doc__ = TwitterResponse.__doc__
103
104 def __init__(self, response, headers):
105 response_typ.__init__(self, response)
106 TwitterResponse.__init__(self, headers)
107 def __new__(cls, response, headers):
108 return response_typ.__new__(cls, response)
109
110
111 return WrappedTwitterResponse(response, headers)
112
113
114
115class TwitterCall(object):
116
117 def __init__(
118 self, auth, format, domain, callable_cls, uri="",
119 uriparts=None, secure=True):
120 self.auth = auth
121 self.format = format
122 self.domain = domain
123 self.callable_cls = callable_cls
124 self.uri = uri
125 self.uriparts = uriparts
126 self.secure = secure
127
128 def __getattr__(self, k):
129 try:
130 return object.__getattr__(self, k)
131 except AttributeError:
132 def extend_call(arg):
133 return self.callable_cls(
134 auth=self.auth, format=self.format, domain=self.domain,
135 callable_cls=self.callable_cls, uriparts=self.uriparts \
136 + (arg,),
137 secure=self.secure)
138 if k == "_":
139 return extend_call
140 else:
141 return extend_call(k)
142
143 def __call__(self, **kwargs):
144 # Build the uri.
145 uriparts = []
146 for uripart in self.uriparts:
147 # If this part matches a keyword argument, use the
148 # supplied value otherwise, just use the part.
149 uriparts.append(str(kwargs.pop(uripart, uripart)))
150 uri = '/'.join(uriparts)
151
152 method = kwargs.pop('_method', None)
153 if not method:
154 method = "GET"
155 for action in POST_ACTIONS:
156 if re.search("%s(/\d+)?$" % action, uri):
157 method = "POST"
158 break
159
160 # If an id kwarg is present and there is no id to fill in in
161 # the list of uriparts, assume the id goes at the end.
162 id = kwargs.pop('id', None)
163 if id:
164 uri += "/%s" %(id)
165
166 # If an _id kwarg is present, this is treated as id as a CGI
167 # param.
168 _id = kwargs.pop('_id', None)
169 if _id:
170 kwargs['id'] = _id
171
172 # If an _timeout is specified in kwargs, use it
173 _timeout = kwargs.pop('_timeout', None)
174
175 secure_str = ''
176 if self.secure:
177 secure_str = 's'
178 dot = ""
179 if self.format:
180 dot = "."
181 uriBase = "http%s://%s/%s%s%s" %(
182 secure_str, self.domain, uri, dot, self.format)
183
184 headers = {'Accept-Encoding': 'gzip'}
185 if self.auth:
186 headers.update(self.auth.generate_headers())
187 arg_data = self.auth.encode_params(uriBase, method, kwargs)
188 if method == 'GET':
189 uriBase += '?' + arg_data
190 body = None
191 else:
192 body = arg_data.encode('utf8')
193
194 req = urllib_request.Request(uriBase, body, headers)
195 return self._handle_response(req, uri, arg_data, _timeout)
196
197 def _handle_response(self, req, uri, arg_data, _timeout=None):
198 kwargs = {}
199 if _timeout:
200 kwargs['timeout'] = _timeout
201 try:
202 handle = urllib_request.urlopen(req, **kwargs)
203 if handle.headers['Content-Type'] in ['image/jpeg', 'image/png']:
204 return handle
205 try:
206 data = handle.read()
207 except httplib.IncompleteRead, e:
208 # Even if we don't get all the bytes we should have there
209 # may be a complete response in e.partial
210 data = e.partial
211 if handle.info().get('Content-Encoding') == 'gzip':
212 # Handle gzip decompression
213 buf = StringIO(data)
214 f = gzip.GzipFile(fileobj=buf)
215 data = f.read()
216 if "json" == self.format:
217 res = json.loads(data.decode('utf8'))
218 return wrap_response(res, handle.headers)
219 else:
220 return wrap_response(
221 data.decode('utf8'), handle.headers)
222 except urllib_error.HTTPError as e:
223 if (e.code == 304):
224 return []
225 else:
226 raise TwitterHTTPError(e, uri, self.format, arg_data)
227
228class Twitter(TwitterCall):
229 """
230 The minimalist yet fully featured Twitter API class.
231
232 Get RESTful data by accessing members of this class. The result
233 is decoded python objects (lists and dicts).
234
235 The Twitter API is documented at:
236
237 http://dev.twitter.com/doc
238
239
240 Examples::
241
242 t = Twitter(
243 auth=OAuth(token, token_key, con_secret, con_secret_key)))
244
245 # Get your "home" timeline
246 t.statuses.home_timeline()
247
248 # Get a particular friend's timeline
249 t.statuses.friends_timeline(id="billybob")
250
251 # Also supported (but totally weird)
252 t.statuses.friends_timeline.billybob()
253
254 # Update your status
255 t.statuses.update(
256 status="Using @sixohsix's sweet Python Twitter Tools.")
257
258 # Send a direct message
259 t.direct_messages.new(
260 user="billybob",
261 text="I think yer swell!")
262
263 # Get the members of tamtar's list "Things That Are Rad"
264 t._("tamtar")._("things-that-are-rad").members()
265
266 # Note how the magic `_` method can be used to insert data
267 # into the middle of a call. You can also use replacement:
268 t.user.list.members(user="tamtar", list="things-that-are-rad")
269
270 # An *optional* `_timeout` parameter can also be used for API
271 # calls which take much more time than normal or twitter stops
272 # responding for some reasone
273 t.users.lookup(
274 screen_name=','.join(A_LIST_OF_100_SCREEN_NAMES), \
275 _timeout=1)
276
277
278
279 Searching Twitter::
280
281 # Search for the latest tweets about #pycon
282 t.search.tweets(q="#pycon")
283
284
285 Using the data returned
286 -----------------------
287
288 Twitter API calls return decoded JSON. This is converted into
289 a bunch of Python lists, dicts, ints, and strings. For example::
290
291 x = twitter.statuses.home_timeline()
292
293 # The first 'tweet' in the timeline
294 x[0]
295
296 # The screen name of the user who wrote the first 'tweet'
297 x[0]['user']['screen_name']
298
299
300 Getting raw XML data
301 --------------------
302
303 If you prefer to get your Twitter data in XML format, pass
304 format="xml" to the Twitter object when you instantiate it::
305
306 twitter = Twitter(format="xml")
307
308 The output will not be parsed in any way. It will be a raw string
309 of XML.
310
311 """
312 def __init__(
313 self, format="json",
314 domain="api.twitter.com", secure=True, auth=None,
315 api_version=_DEFAULT):
316 """
317 Create a new twitter API connector.
318
319 Pass an `auth` parameter to use the credentials of a specific
320 user. Generally you'll want to pass an `OAuth`
321 instance::
322
323 twitter = Twitter(auth=OAuth(
324 token, token_secret, consumer_key, consumer_secret))
325
326
327 `domain` lets you change the domain you are connecting. By
328 default it's `api.twitter.com` but `search.twitter.com` may be
329 useful too.
330
331 If `secure` is False you will connect with HTTP instead of
332 HTTPS.
333
334 `api_version` is used to set the base uri. By default it's
335 '1'. If you are using "search.twitter.com" set this to None.
336 """
337 if not auth:
338 auth = NoAuth()
339
340 if (format not in ("json", "xml", "")):
341 raise ValueError("Unknown data format '%s'" %(format))
342
343 if api_version is _DEFAULT:
344 if domain == 'api.twitter.com':
345 api_version = '1.1'
346 else:
347 api_version = None
348
349 uriparts = ()
350 if api_version:
351 uriparts += (str(api_version),)
352
353 TwitterCall.__init__(
354 self, auth=auth, format=format, domain=domain,
355 callable_cls=TwitterCall,
356 secure=secure, uriparts=uriparts)
357
358
359__all__ = ["Twitter", "TwitterError", "TwitterHTTPError", "TwitterResponse"]