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