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