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