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