2 from __future__
import unicode_literals
, print_function
4 from .util
import PY_3_OR_HIGHER
, actually_bytes
7 import urllib
.request
as urllib_request
8 import urllib
.error
as urllib_error
10 import urllib2
as urllib_request
11 import urllib2
as urllib_error
14 from cStringIO
import StringIO
16 from io
import BytesIO
as StringIO
18 from .twitter_globals
import POST_ACTIONS
19 from .auth
import NoAuth
24 from time
import sleep
, time
27 import http
.client
as http_client
29 import httplib
as http_client
34 import simplejson
as json
37 class _DEFAULT(object):
41 class TwitterError(Exception):
43 Base Exception thrown by the Twitter object when there is a
44 general error interacting with the API.
49 class TwitterHTTPError(TwitterError
):
51 Exception thrown by the Twitter object when there is an
52 HTTP error interacting with twitter.com.
54 def __init__(self
, e
, uri
, format
, uriparts
):
58 self
.uriparts
= uriparts
60 data
= self
.e
.fp
.read()
61 except http_client
.IncompleteRead
as e
:
62 # can't read the error text
63 # let's try some of it
65 if self
.e
.headers
.get('Content-Encoding') == 'gzip':
67 f
= gzip
.GzipFile(fileobj
=buf
)
72 data
= data
.decode('utf8')
73 if "json" == self
.format
:
75 data
= json
.loads(data
)
77 # We try to load the response as json as a nicety; if it fails, carry on.
79 self
.response_data
= data
80 super(TwitterHTTPError
, self
).__init
__(str(self
))
83 fmt
= ("." + self
.format
) if self
.format
else ""
85 "Twitter sent status %i for URL: %s%s using parameters: "
86 "(%s)\ndetails: %s" % (
87 self
.e
.code
, self
.uri
, fmt
, self
.uriparts
,
91 class TwitterResponse(object):
93 Response from a twitter request. Behaves like a list or a string
94 (depending on requested format) but it has a few other interesting
97 `headers` gives you access to the response headers as an
98 httplib.HTTPHeaders instance. You can do
99 `response.headers.get('h')` to retrieve a header.
103 def rate_limit_remaining(self
):
105 Remaining requests in the current rate-limit.
107 return int(self
.headers
.get('X-Rate-Limit-Remaining', "0"))
110 def rate_limit_limit(self
):
112 The rate limit ceiling for that given request.
114 return int(self
.headers
.get('X-Rate-Limit-Limit', "0"))
117 def rate_limit_reset(self
):
119 Time in UTC epoch seconds when the rate limit will reset.
121 return int(self
.headers
.get('X-Rate-Limit-Reset', "0"))
124 class TwitterDictResponse(dict, TwitterResponse
):
128 class TwitterListResponse(list, TwitterResponse
):
132 def wrap_response(response
, headers
):
133 response_typ
= type(response
)
134 if response_typ
is dict:
135 res
= TwitterDictResponse(response
)
136 res
.headers
= headers
137 elif response_typ
is list:
138 res
= TwitterListResponse(response
)
139 res
.headers
= headers
145 POST_ACTIONS_RE
= re
.compile('(' + '|'.join(POST_ACTIONS
) + r
')(/\d+)?$')
147 def method_for_uri(uri
):
148 if POST_ACTIONS_RE
.search(uri
):
153 def build_uri(orig_uriparts
, kwargs
):
155 Build the URI from the original uriparts and kwargs. Modifies kwargs.
158 for uripart
in orig_uriparts
:
159 # If this part matches a keyword argument (starting with _), use
160 # the supplied value. Otherwise, just use the part.
161 if uripart
.startswith("_"):
162 part
= (str(kwargs
.pop(uripart
, uripart
)))
165 uriparts
.append(part
)
166 uri
= '/'.join(uriparts
)
168 # If an id kwarg is present and there is no id to fill in in
169 # the list of uriparts, assume the id goes at the end.
170 id = kwargs
.pop('id', None)
177 class TwitterCall(object):
179 TWITTER_UNAVAILABLE_WAIT
= 30 # delay after HTTP codes 502, 503 or 504
182 self
, auth
, format
, domain
, callable_cls
, uri
="",
183 uriparts
=None, secure
=True, timeout
=None, gzip
=False, retry
=False):
187 self
.callable_cls
= callable_cls
189 self
.uriparts
= uriparts
191 self
.timeout
= timeout
195 def __getattr__(self
, k
):
197 return object.__getattr
__(self
, k
)
198 except AttributeError:
199 def extend_call(arg
):
200 return self
.callable_cls(
201 auth
=self
.auth
, format
=self
.format
, domain
=self
.domain
,
202 callable_cls
=self
.callable_cls
, timeout
=self
.timeout
,
203 secure
=self
.secure
, gzip
=self
.gzip
, retry
=self
.retry
,
204 uriparts
=self
.uriparts
+ (arg
,))
208 return extend_call(k
)
210 def __call__(self
, **kwargs
):
211 kwargs
= dict(kwargs
)
212 uri
= build_uri(self
.uriparts
, kwargs
)
213 method
= kwargs
.pop('_method', None) or method_for_uri(uri
)
216 # If an _id kwarg is present, this is treated as id as a CGI
218 _id
= kwargs
.pop('_id', None)
222 # If an _timeout is specified in kwargs, use it
223 _timeout
= kwargs
.pop('_timeout', None)
231 url_base
= "http%s://%s/%s%s%s" % (
232 secure_str
, domain
, uri
, dot
, self
.format
)
234 # Check if argument tells whether img is already base64 encoded
235 b64_convert
= not kwargs
.pop("_base64", False)
239 # Catch media arguments to handle oauth query differently for multipart
241 if 'media' in kwargs
:
243 media
= kwargs
.pop('media')
245 elif 'media[]' in kwargs
:
246 mediafield
= 'media[]'
247 media
= kwargs
.pop('media[]')
249 media
= base64
.b64encode(media
)
252 # Catch media arguments that are not accepted through multipart
253 # and are not yet base64 encoded
255 for arg
in ['banner', 'image']:
257 kwargs
[arg
] = base64
.b64encode(kwargs
[arg
])
259 headers
= {'Accept-Encoding': 'gzip'}
if self
.gzip
else dict()
263 headers
.update(self
.auth
.generate_headers())
264 # Use urlencoded oauth args with no params when sending media
265 # via multipart and send it directly via uri even for post
266 arg_data
= self
.auth
.encode_params(
267 url_base
, method
, {} if media
else kwargs
)
268 if method
== 'GET' or media
:
269 url_base
+= '?' + arg_data
271 body
= arg_data
.encode('utf-8')
273 # Handle query as multipart when sending media
275 BOUNDARY
= b
"###Python-Twitter###"
277 bod
.append(b
'--' + BOUNDARY
)
279 b
'Content-Disposition: form-data; name="'
280 + actually_bytes(mediafield
)
282 bod
.append(b
'Content-Type: application/octet-stream')
284 bod
.append(b
'Content-Transfer-Encoding: base64')
286 bod
.append(actually_bytes(media
))
287 for k
, v
in kwargs
.items():
288 k
= actually_bytes(k
)
289 v
= actually_bytes(v
)
290 bod
.append(b
'--' + BOUNDARY
)
291 bod
.append(b
'Content-Disposition: form-data; name="' + k
+ b
'"')
292 bod
.append(b
'Content-Type: text/plain;charset=utf-8')
295 bod
.append(b
'--' + BOUNDARY
+ b
'--')
298 body
= b
'\r\n'.join(bod
)
299 # print(body.decode('utf-8', errors='ignore'))
300 headers
['Content-Type'] = \
301 b
'multipart/form-data; boundary=' + BOUNDARY
303 if not PY_3_OR_HIGHER
:
304 url_base
= url_base
.encode("utf-8")
306 headers
[actually_bytes(k
)] = actually_bytes(headers
.pop(k
))
308 req
= urllib_request
.Request(url_base
, data
=body
, headers
=headers
)
310 return self
._handle
_response
_with
_retry
(req
, uri
, arg_data
, _timeout
)
312 return self
._handle
_response
(req
, uri
, arg_data
, _timeout
)
314 def _handle_response(self
, req
, uri
, arg_data
, _timeout
=None):
317 kwargs
['timeout'] = _timeout
319 handle
= urllib_request
.urlopen(req
, **kwargs
)
320 if handle
.headers
['Content-Type'] in ['image/jpeg', 'image/png']:
324 except http_client
.IncompleteRead
as e
:
325 # Even if we don't get all the bytes we should have there
326 # may be a complete response in e.partial
328 if handle
.info().get('Content-Encoding') == 'gzip':
329 # Handle gzip decompression
331 f
= gzip
.GzipFile(fileobj
=buf
)
334 return wrap_response({}, handle
.headers
)
335 elif "json" == self
.format
:
336 res
= json
.loads(data
.decode('utf8'))
337 return wrap_response(res
, handle
.headers
)
339 return wrap_response(
340 data
.decode('utf8'), handle
.headers
)
341 except urllib_error
.HTTPError
as e
:
345 raise TwitterHTTPError(e
, uri
, self
.format
, arg_data
)
347 def _handle_response_with_retry(self
, req
, uri
, arg_data
, _timeout
=None):
351 return self
._handle
_response
(req
, uri
, arg_data
, _timeout
)
352 except TwitterHTTPError
as e
:
354 # API rate limit reached
355 reset
= int(e
.e
.headers
.get('X-Rate-Limit-Reset', time() + 30))
356 delay
= int(reset
- time() + 2) # add some extra margin
357 print("API rate limit reached; waiting for %ds..." % delay
, file=sys
.stderr
)
358 elif e
.e
.code
in (502, 503, 504):
359 delay
= self
.TWITTER_UNAVAILABLE_WAIT
360 print("Service unavailable; waiting for %ds..." % delay
, file=sys
.stderr
)
363 if isinstance(retry
, int) and not isinstance(retry
, bool):
370 class Twitter(TwitterCall
):
372 The minimalist yet fully featured Twitter API class.
374 Get RESTful data by accessing members of this class. The result
375 is decoded python objects (lists and dicts).
377 The Twitter API is documented at:
379 http://dev.twitter.com/doc
384 from twitter import *
387 auth=OAuth(token, token_key, con_secret, con_secret_key))
389 # Get your "home" timeline
390 t.statuses.home_timeline()
392 # Get a particular friend's timeline
393 t.statuses.user_timeline(screen_name="billybob")
395 # to pass in GET/POST parameters, such as `count`
396 t.statuses.home_timeline(count=5)
398 # to pass in the GET/POST parameter `id` you need to use `_id`
399 t.statuses.oembed(_id=1234567890)
403 status="Using @sixohsix's sweet Python Twitter Tools.")
405 # Send a direct message
406 t.direct_messages.new(
408 text="I think yer swell!")
410 # Get the members of tamtar's list "Things That Are Rad"
411 t.lists.members(owner_screen_name="tamtar", slug="things-that-are-rad")
413 # An *optional* `_timeout` parameter can also be used for API
414 # calls which take much more time than normal or twitter stops
415 # responding for some reason:
417 screen_name=','.join(A_LIST_OF_100_SCREEN_NAMES), \
420 # Overriding Method: GET/POST
421 # you should not need to use this method as this library properly
422 # detects whether GET or POST should be used, Nevertheless
423 # to force a particular method, use `_method`
424 t.statuses.oembed(_id=1234567890, _method='GET')
426 # Send images along with your tweets:
427 # - first just read images from the web or from files the regular way:
428 with open("example.png", "rb") as imagefile:
429 imagedata = imagefile.read()
430 # - then upload medias one by one on Twitter's dedicated server
431 # and collect each one's id:
432 t_up = Twitter(domain='upload.twitter.com',
433 auth=OAuth(token, token_key, con_secret, con_secret_key))
434 id_img1 = t_up.media.upload(media=imagedata)["media_id_string"]
435 id_img2 = t_up.media.upload(media=imagedata)["media_id_string"]
437 # - finally send your tweet with the list of media ids:
438 t.statuses.update(status="PTT ★", media_ids=",".join([id_img1, id_img2]))
440 # Or send a tweet with an image (or set a logo/banner similarily)
441 # using the old deprecated method that will probably disappear some day
442 params = {"media[]": imagedata, "status": "PTT ★"}
443 # Or for an image encoded as base64:
444 params = {"media[]": base64_image, "status": "PTT ★", "_base64": True}
445 t.statuses.update_with_media(**params)
451 # Search for the latest tweets about #pycon
452 t.search.tweets(q="#pycon")
455 Using the data returned
456 -----------------------
458 Twitter API calls return decoded JSON. This is converted into
459 a bunch of Python lists, dicts, ints, and strings. For example::
461 x = twitter.statuses.home_timeline()
463 # The first 'tweet' in the timeline
466 # The screen name of the user who wrote the first 'tweet'
467 x[0]['user']['screen_name']
473 If you prefer to get your Twitter data in XML format, pass
474 format="xml" to the Twitter object when you instantiate it::
476 twitter = Twitter(format="xml")
478 The output will not be parsed in any way. It will be a raw string
484 domain
="api.twitter.com", secure
=True, auth
=None,
485 api_version
=_DEFAULT
, retry
=False):
487 Create a new twitter API connector.
489 Pass an `auth` parameter to use the credentials of a specific
490 user. Generally you'll want to pass an `OAuth`
493 twitter = Twitter(auth=OAuth(
494 token, token_secret, consumer_key, consumer_secret))
497 `domain` lets you change the domain you are connecting. By
498 default it's `api.twitter.com`.
500 If `secure` is False you will connect with HTTP instead of
503 `api_version` is used to set the base uri. By default it's
506 If `retry` is True, API rate limits will automatically be
507 handled by waiting until the next reset, as indicated by
508 the X-Rate-Limit-Reset HTTP header. If retry is an integer,
509 it defines the number of retries attempted.
514 if (format
not in ("json", "xml", "")):
515 raise ValueError("Unknown data format '%s'" % (format
))
517 if api_version
is _DEFAULT
:
522 uriparts
+= (str(api_version
),)
524 TwitterCall
.__init
__(
525 self
, auth
=auth
, format
=format
, domain
=domain
,
526 callable_cls
=TwitterCall
,
527 secure
=secure
, uriparts
=uriparts
, retry
=retry
)
530 __all__
= ["Twitter", "TwitterError", "TwitterHTTPError", "TwitterResponse"]