]> jfr.im git - z_archive/twitter.git/blame - twitter/api.py
avoid any border effect
[z_archive/twitter.git] / twitter / api.py
CommitLineData
f0603331
MV
1# encoding: utf-8
2from __future__ import unicode_literals
3
3930cc7b
MV
4try:
5 import urllib.request as urllib_request
6 import urllib.error as urllib_error
7except ImportError:
8 import urllib2 as urllib_request
9 import urllib2 as urllib_error
7364ea65 10
a403f1b3
JL
11try:
12 from cStringIO import StringIO
13except ImportError:
6204d0da 14 from io import BytesIO as StringIO
a403f1b3 15
14fc6b25
MG
16from .twitter_globals import POST_ACTIONS
17from .auth import NoAuth
4e9d6343 18
2ee65672 19import re
745061c1 20import sys
a403f1b3 21import gzip
23dcf621 22
23try:
dea9a3e7 24 import http.client as http_client
23dcf621 25except ImportError:
dea9a3e7 26 import httplib as http_client
2ee65672 27
4b12a3a0 28try:
f1a8ed67 29 import json
4b12a3a0 30except ImportError:
f1a8ed67 31 import simplejson as json
32
fcf08b18 33
652c5402
MV
34class _DEFAULT(object):
35 pass
36
fcf08b18 37
5251ea48 38class TwitterError(Exception):
21e3bd23 39 """
64a8d213
B
40 Base Exception thrown by the Twitter object when there is a
41 general error interacting with the API.
21e3bd23 42 """
5251ea48 43 pass
44
fcf08b18 45
64a8d213
B
46class TwitterHTTPError(TwitterError):
47 """
48 Exception thrown by the Twitter object when there is an
49 HTTP error interacting with twitter.com.
50 """
1be4ce71 51 def __init__(self, e, uri, format, uriparts):
4b12a3a0
MV
52 self.e = e
53 self.uri = uri
54 self.format = format
55 self.uriparts = uriparts
7fe9aab6
HN
56 try:
57 data = self.e.fp.read()
dea9a3e7 58 except http_client.IncompleteRead as e:
7fe9aab6
HN
59 # can't read the error text
60 # let's try some of it
61 data = e.partial
e9fc8d86 62 if self.e.headers.get('Content-Encoding') == 'gzip':
7fe9aab6 63 buf = StringIO(data)
84d2da3d 64 f = gzip.GzipFile(fileobj=buf)
65 self.response_data = f.read()
66 else:
7fe9aab6 67 self.response_data = data
9f04d75d 68 super(TwitterHTTPError, self).__init__(str(self))
64a8d213
B
69
70 def __str__(self):
57b54437 71 fmt = ("." + self.format) if self.format else ""
68b3e2ee 72 return (
57b54437 73 "Twitter sent status %i for URL: %s%s using parameters: "
fcf08b18 74 "(%s)\ndetails: %s" % (
57b54437 75 self.e.code, self.uri, fmt, self.uriparts,
c7dd86d1 76 self.response_data))
64a8d213 77
fcf08b18 78
84d0a294
MV
79class TwitterResponse(object):
80 """
81 Response from a twitter request. Behaves like a list or a string
82 (depending on requested format) but it has a few other interesting
83 attributes.
84
85 `headers` gives you access to the response headers as an
86 httplib.HTTPHeaders instance. You can do
ba02331e 87 `response.headers.get('h')` to retrieve a header.
84d0a294 88 """
84d0a294 89
84d0a294
MV
90 @property
91 def rate_limit_remaining(self):
92 """
93 Remaining requests in the current rate-limit.
94 """
eeec9b00
IA
95 return int(self.headers.get('X-Rate-Limit-Remaining', "0"))
96
97 @property
98 def rate_limit_limit(self):
99 """
c53558ad 100 The rate limit ceiling for that given request.
eeec9b00
IA
101 """
102 return int(self.headers.get('X-Rate-Limit-Limit', "0"))
84d0a294
MV
103
104 @property
105 def rate_limit_reset(self):
106 """
107 Time in UTC epoch seconds when the rate limit will reset.
108 """
eeec9b00 109 return int(self.headers.get('X-Rate-Limit-Reset', "0"))
84d0a294
MV
110
111
ed23f46c
MV
112class TwitterDictResponse(dict, TwitterResponse):
113 pass
12bba6ac 114
abddd419 115
ed23f46c
MV
116class TwitterListResponse(list, TwitterResponse):
117 pass
fcf08b18 118
94803fc9 119
ed23f46c
MV
120def wrap_response(response, headers):
121 response_typ = type(response)
122 if response_typ is dict:
123 res = TwitterDictResponse(response)
124 res.headers = headers
125 elif response_typ is list:
126 res = TwitterListResponse(response)
127 res.headers = headers
128 else:
129 res = response
130 return res
abddd419 131
4e5c4880
MV
132def method_for_uri(uri):
133 method = "GET"
134 for action in POST_ACTIONS:
135 if re.search("%s(/\d+)?$" % action, uri):
136 method = "POST"
137 break
138 return method
0d6c0646 139
7364ea65 140class TwitterCall(object):
dd648a25 141
c8d451e8 142 def __init__(
fcf08b18 143 self, auth, format, domain, callable_cls, uri="",
1ff50236 144 uriparts=None, secure=True, timeout=None, gzip=False):
568331a9 145 self.auth = auth
a55e6a11 146 self.format = format
153dee29 147 self.domain = domain
dd648a25 148 self.callable_cls = callable_cls
7364ea65 149 self.uri = uri
b0dedfc0 150 self.uriparts = uriparts
9a148ed1 151 self.secure = secure
effd06bb 152 self.timeout = timeout
86318060 153 self.gzip = gzip
fd2bc885 154
7364ea65 155 def __getattr__(self, k):
156 try:
157 return object.__getattr__(self, k)
158 except AttributeError:
e748eed8 159 def extend_call(arg):
160 return self.callable_cls(
161 auth=self.auth, format=self.format, domain=self.domain,
ff3ca197 162 callable_cls=self.callable_cls, timeout=self.timeout,
86318060 163 secure=self.secure, gzip=self.gzip,
ff3ca197 164 uriparts=self.uriparts + (arg,))
e748eed8 165 if k == "_":
166 return extend_call
167 else:
168 return extend_call(k)
fd2bc885 169
7364ea65 170 def __call__(self, **kwargs):
aec68959 171 # Build the uri.
1be4ce71 172 uriparts = []
b0dedfc0 173 for uripart in self.uriparts:
aec68959
MV
174 # If this part matches a keyword argument, use the
175 # supplied value otherwise, just use the part.
f7e63802
MV
176 uriparts.append(str(kwargs.pop(uripart, uripart)))
177 uri = '/'.join(uriparts)
1be4ce71 178
4e5c4880 179 method = kwargs.pop('_method', None) or method_for_uri(uri)
612ececa 180
aec68959
MV
181 # If an id kwarg is present and there is no id to fill in in
182 # the list of uriparts, assume the id goes at the end.
da45d039
MV
183 id = kwargs.pop('id', None)
184 if id:
fcf08b18 185 uri += "/%s" % (id)
4e9d6343 186
920528cd
MV
187 # If an _id kwarg is present, this is treated as id as a CGI
188 # param.
189 _id = kwargs.pop('_id', None)
190 if _id:
191 kwargs['id'] = _id
be5f32da 192
8fd7289d
IA
193 # If an _timeout is specified in kwargs, use it
194 _timeout = kwargs.pop('_timeout', None)
920528cd 195
568331a9
MH
196 secure_str = ''
197 if self.secure:
198 secure_str = 's'
6c527e72 199 dot = ""
1be4ce71 200 if self.format:
6c527e72 201 dot = "."
fcf08b18 202 uriBase = "http%s://%s/%s%s%s" % (
203 secure_str, self.domain, uri, dot, self.format)
568331a9 204
c1d973eb
A
205 # Check if argument tells whether img is already base64 encoded
206 b64_convert = True
207 if "_base64" in kwargs:
208 b64_convert = not kwargs.pop("_base64")
209 if b64_convert:
210 import base64
211
94fb8fab
R
212 # Catch media arguments to handle oauth query differently for multipart
213 media = None
0e197382 214 for arg in ['media[]']:
94fb8fab
R
215 if arg in kwargs:
216 media = kwargs.pop(arg)
525c9c31 217 if b64_convert:
525c9c31 218 media = base64.b64encode(media)
745061c1
R
219 if sys.version_info >= (3, 0):
220 media = str(media, 'utf8')
94fb8fab
R
221 mediafield = arg
222 break
223
c1d973eb
A
224 # Catch media arguments that are not accepted through multipart
225 # and are not yet base64 encoded
226 if b64_convert:
0e197382 227 for arg in ['banner', 'image']:
c1d973eb
A
228 if arg in kwargs:
229 kwargs[arg] = base64.b64encode(kwargs[arg])
230
86318060 231 headers = {'Accept-Encoding': 'gzip'} if self.gzip else dict()
1ff50236 232 body = None
233 arg_data = None
1be4ce71 234 if self.auth:
568331a9 235 headers.update(self.auth.generate_headers())
94fb8fab
R
236 # Use urlencoded oauth args with no params when sending media
237 # via multipart and send it directly via uri even for post
1ff50236 238 arg_data = self.auth.encode_params(
239 uriBase, method, {} if media else kwargs)
94fb8fab 240 if method == 'GET' or media:
1be4ce71 241 uriBase += '?' + arg_data
1be4ce71 242 else:
8eb73aab 243 body = arg_data.encode('utf8')
c53558ad 244
94fb8fab
R
245 # Handle query as multipart when sending media
246 if media:
247 BOUNDARY = "###Python-Twitter###"
248 bod = []
249 bod.append('--' + BOUNDARY)
1ff50236 250 bod.append(
251 'Content-Disposition: form-data; name="%s"' % mediafield)
525c9c31 252 bod.append('Content-Transfer-Encoding: base64')
94fb8fab
R
253 bod.append('')
254 bod.append(media)
255 for k, v in kwargs.items():
256 bod.append('--' + BOUNDARY)
257 bod.append('Content-Disposition: form-data; name="%s"' % k)
258 bod.append('')
259 bod.append(v)
260 bod.append('--' + BOUNDARY + '--')
c1d973eb 261 body = '\r\n'.join(bod).encode('utf8')
1ff50236 262 headers['Content-Type'] = \
263 'multipart/form-data; boundary=%s' % BOUNDARY
94fb8fab 264
238c4c2a
R
265 if sys.version_info[:2] == (2, 7):
266 uriBase = uriBase.encode("utf-8")
267 for k in headers:
268 headers[k.encode('utf-8')] = headers.pop(k)
f5ca09ad 269
3930cc7b 270 req = urllib_request.Request(uriBase, body, headers)
8fd7289d 271 return self._handle_response(req, uri, arg_data, _timeout)
102acdb1 272
8fd7289d 273 def _handle_response(self, req, uri, arg_data, _timeout=None):
a5aab114 274 kwargs = {}
8fd7289d
IA
275 if _timeout:
276 kwargs['timeout'] = _timeout
7364ea65 277 try:
a5aab114 278 handle = urllib_request.urlopen(req, **kwargs)
918b8b48
GC
279 if handle.headers['Content-Type'] in ['image/jpeg', 'image/png']:
280 return handle
0fdfdc3d
DM
281 try:
282 data = handle.read()
dea9a3e7 283 except http_client.IncompleteRead as e:
0fdfdc3d
DM
284 # Even if we don't get all the bytes we should have there
285 # may be a complete response in e.partial
286 data = e.partial
287 if handle.info().get('Content-Encoding') == 'gzip':
a403f1b3 288 # Handle gzip decompression
0fdfdc3d 289 buf = StringIO(data)
a403f1b3
JL
290 f = gzip.GzipFile(fileobj=buf)
291 data = f.read()
c1d973eb
A
292 if len(data) == 0:
293 return wrap_response({}, handle.headers)
294 elif "json" == self.format:
a403f1b3 295 res = json.loads(data.decode('utf8'))
abddd419 296 return wrap_response(res, handle.headers)
de072195 297 else:
456ec92b 298 return wrap_response(
a403f1b3 299 data.decode('utf8'), handle.headers)
3930cc7b 300 except urllib_error.HTTPError as e:
de072195 301 if (e.code == 304):
7364ea65 302 return []
de072195 303 else:
aec68959 304 raise TwitterHTTPError(e, uri, self.format, arg_data)
102acdb1 305
fcf08b18 306
7364ea65 307class Twitter(TwitterCall):
308 """
309 The minimalist yet fully featured Twitter API class.
4e9d6343 310
7364ea65 311 Get RESTful data by accessing members of this class. The result
312 is decoded python objects (lists and dicts).
313
51e0b8f1 314 The Twitter API is documented at:
153dee29 315
aec68959
MV
316 http://dev.twitter.com/doc
317
4e9d6343 318
7364ea65 319 Examples::
4e9d6343 320
d4f3123e
MV
321 from twitter import *
322
d09c0dd3 323 t = Twitter(
51e0b8f1 324 auth=OAuth(token, token_key, con_secret, con_secret_key)))
4e9d6343 325
58ccea4e
MV
326 # Get your "home" timeline
327 t.statuses.home_timeline()
4e9d6343 328
d4f3123e
MV
329 # Get a particular friend's timeline
330 t.statuses.user_timeline(screen_name="billybob")
331
332 # to pass in GET/POST parameters, such as `count`
333 t.statuses.home_timeline(count=5)
334
335 # to pass in the GET/POST parameter `id` you need to use `_id`
336 t.statuses.oembed(_id=1234567890)
d09c0dd3
MV
337
338 # Update your status
339 t.statuses.update(
340 status="Using @sixohsix's sweet Python Twitter Tools.")
4e9d6343 341
51e0b8f1 342 # Send a direct message
d09c0dd3 343 t.direct_messages.new(
51e0b8f1
MV
344 user="billybob",
345 text="I think yer swell!")
7364ea65 346
d09c0dd3
MV
347 # Get the members of tamtar's list "Things That Are Rad"
348 t._("tamtar")._("things-that-are-rad").members()
349
350 # Note how the magic `_` method can be used to insert data
351 # into the middle of a call. You can also use replacement:
352 t.user.list.members(user="tamtar", list="things-that-are-rad")
be5f32da 353
8fd7289d 354 # An *optional* `_timeout` parameter can also be used for API
a5aab114 355 # calls which take much more time than normal or twitter stops
d4f3123e 356 # responding for some reason:
a5aab114
IA
357 t.users.lookup(
358 screen_name=','.join(A_LIST_OF_100_SCREEN_NAMES), \
8fd7289d 359 _timeout=1)
a5aab114 360
5a412b39
R
361 # Overriding Method: GET/POST
362 # you should not need to use this method as this library properly
363 # detects whether GET or POST should be used, Nevertheless
364 # to force a particular method, use `_method`
365 t.statuses.oembed(_id=1234567890, _method='GET')
366
367 # Send a tweet with an image included (or set your banner or logo similarily)
d4f3123e 368 # by just reading your image from the web or a file in a string:
880418b2
R
369 # Note that the text sent as status along with the picture must be unicode.
370 status = u"PTT ★" # or with python 3: status = "PTT ★"
5a412b39 371 with open("example.png", "rb") as imagefile:
880418b2 372 params = {"media[]": imagefile.read(), "status": status}
5a412b39 373 t.statuses.update_with_media(**params)
d4f3123e 374
5a412b39 375 # Or by sending a base64 encoded image:
880418b2 376 params = {"media[]": base64_image, "status": status, "_base64": True}
5a412b39
R
377 t.statuses.update_with_media(**params)
378
b0dedfc0 379
153dee29 380 Searching Twitter::
4e9d6343 381
58ccea4e
MV
382 # Search for the latest tweets about #pycon
383 t.search.tweets(q="#pycon")
153dee29 384
7364ea65 385
68b3e2ee
MV
386 Using the data returned
387 -----------------------
388
389 Twitter API calls return decoded JSON. This is converted into
390 a bunch of Python lists, dicts, ints, and strings. For example::
7364ea65 391
58ccea4e 392 x = twitter.statuses.home_timeline()
7364ea65 393
51e0b8f1
MV
394 # The first 'tweet' in the timeline
395 x[0]
7364ea65 396
51e0b8f1
MV
397 # The screen name of the user who wrote the first 'tweet'
398 x[0]['user']['screen_name']
4e9d6343 399
4e9d6343 400
68b3e2ee
MV
401 Getting raw XML data
402 --------------------
403
404 If you prefer to get your Twitter data in XML format, pass
405 format="xml" to the Twitter object when you instantiate it::
4e9d6343 406
51e0b8f1 407 twitter = Twitter(format="xml")
4e9d6343 408
51e0b8f1
MV
409 The output will not be parsed in any way. It will be a raw string
410 of XML.
68b3e2ee 411
7364ea65 412 """
45688301 413 def __init__(
fcf08b18 414 self, format="json",
415 domain="api.twitter.com", secure=True, auth=None,
416 api_version=_DEFAULT):
7364ea65 417 """
68b3e2ee
MV
418 Create a new twitter API connector.
419
420 Pass an `auth` parameter to use the credentials of a specific
421 user. Generally you'll want to pass an `OAuth`
69e1f98e
MV
422 instance::
423
424 twitter = Twitter(auth=OAuth(
425 token, token_secret, consumer_key, consumer_secret))
426
427
68b3e2ee 428 `domain` lets you change the domain you are connecting. By
fcf08b18 429 default it's `api.twitter.com`.
68b3e2ee
MV
430
431 If `secure` is False you will connect with HTTP instead of
432 HTTPS.
433
1cc9ab0b 434 `api_version` is used to set the base uri. By default it's
fcf08b18 435 '1.1'.
7364ea65 436 """
d20da7f3
MV
437 if not auth:
438 auth = NoAuth()
439
6c527e72 440 if (format not in ("json", "xml", "")):
fcf08b18 441 raise ValueError("Unknown data format '%s'" % (format))
68b3e2ee 442
652c5402 443 if api_version is _DEFAULT:
82a93c03 444 api_version = '1.1'
652c5402 445
1be4ce71 446 uriparts = ()
68b3e2ee 447 if api_version:
1be4ce71 448 uriparts += (str(api_version),)
68b3e2ee 449
9a148ed1 450 TwitterCall.__init__(
aec68959 451 self, auth=auth, format=format, domain=domain,
dd648a25 452 callable_cls=TwitterCall,
1be4ce71 453 secure=secure, uriparts=uriparts)
7e43e2ed 454
7364ea65 455
abddd419 456__all__ = ["Twitter", "TwitterError", "TwitterHTTPError", "TwitterResponse"]