2 from __future__
import unicode_literals
, print_function
5 import urllib
.request
as urllib_request
6 import urllib
.error
as urllib_error
8 import urllib2
as urllib_request
9 import urllib2
as urllib_error
12 from cStringIO
import StringIO
14 from io
import BytesIO
as StringIO
16 from .twitter_globals
import POST_ACTIONS
17 from .auth
import NoAuth
22 from time
import sleep
, time
25 import http
.client
as http_client
27 import httplib
as http_client
32 import simplejson
as json
35 class _DEFAULT(object):
39 class TwitterError(Exception):
41 Base Exception thrown by the Twitter object when there is a
42 general error interacting with the API.
47 class TwitterHTTPError(TwitterError
):
49 Exception thrown by the Twitter object when there is an
50 HTTP error interacting with twitter.com.
52 def __init__(self
, e
, uri
, format
, uriparts
):
56 self
.uriparts
= uriparts
58 data
= self
.e
.fp
.read()
59 except http_client
.IncompleteRead
as e
:
60 # can't read the error text
61 # let's try some of it
63 if self
.e
.headers
.get('Content-Encoding') == 'gzip':
65 f
= gzip
.GzipFile(fileobj
=buf
)
66 self
.response_data
= f
.read()
68 self
.response_data
= data
69 super(TwitterHTTPError
, self
).__init
__(str(self
))
72 fmt
= ("." + self
.format
) if self
.format
else ""
74 "Twitter sent status %i for URL: %s%s using parameters: "
75 "(%s)\ndetails: %s" % (
76 self
.e
.code
, self
.uri
, fmt
, self
.uriparts
,
80 class TwitterResponse(object):
82 Response from a twitter request. Behaves like a list or a string
83 (depending on requested format) but it has a few other interesting
86 `headers` gives you access to the response headers as an
87 httplib.HTTPHeaders instance. You can do
88 `response.headers.get('h')` to retrieve a header.
92 def rate_limit_remaining(self
):
94 Remaining requests in the current rate-limit.
96 return int(self
.headers
.get('X-Rate-Limit-Remaining', "0"))
99 def rate_limit_limit(self
):
101 The rate limit ceiling for that given request.
103 return int(self
.headers
.get('X-Rate-Limit-Limit', "0"))
106 def rate_limit_reset(self
):
108 Time in UTC epoch seconds when the rate limit will reset.
110 return int(self
.headers
.get('X-Rate-Limit-Reset', "0"))
113 class TwitterDictResponse(dict, TwitterResponse
):
117 class TwitterListResponse(list, TwitterResponse
):
121 def wrap_response(response
, headers
):
122 response_typ
= type(response
)
123 if response_typ
is dict:
124 res
= TwitterDictResponse(response
)
125 res
.headers
= headers
126 elif response_typ
is list:
127 res
= TwitterListResponse(response
)
128 res
.headers
= headers
134 POST_ACTIONS_RE
= re
.compile('(' + '|'.join(POST_ACTIONS
) + r
')(/\d+)?$')
136 def method_for_uri(uri
):
137 if POST_ACTIONS_RE
.search(uri
):
142 def build_uri(orig_uriparts
, kwargs
):
144 Build the URI from the original uriparts and kwargs. Modifies kwargs.
147 for uripart
in orig_uriparts
:
148 # If this part matches a keyword argument (starting with _), use
149 # the supplied value. Otherwise, just use the part.
150 if uripart
.startswith("_"):
151 part
= (str(kwargs
.pop(uripart
, uripart
)))
154 uriparts
.append(part
)
155 uri
= '/'.join(uriparts
)
157 # If an id kwarg is present and there is no id to fill in in
158 # the list of uriparts, assume the id goes at the end.
159 id = kwargs
.pop('id', None)
166 class TwitterCall(object):
168 TWITTER_UNAVAILABLE_WAIT
= 30 # delay after HTTP codes 502, 503 or 504
171 self
, auth
, format
, domain
, callable_cls
, uri
="",
172 uriparts
=None, secure
=True, timeout
=None, gzip
=False, retry
=False):
176 self
.callable_cls
= callable_cls
178 self
.uriparts
= uriparts
180 self
.timeout
= timeout
184 def __getattr__(self
, k
):
186 return object.__getattr
__(self
, k
)
187 except AttributeError:
188 def extend_call(arg
):
189 return self
.callable_cls(
190 auth
=self
.auth
, format
=self
.format
, domain
=self
.domain
,
191 callable_cls
=self
.callable_cls
, timeout
=self
.timeout
,
192 secure
=self
.secure
, gzip
=self
.gzip
, retry
=self
.retry
,
193 uriparts
=self
.uriparts
+ (arg
,))
197 return extend_call(k
)
199 def __call__(self
, **kwargs
):
200 kwargs
= dict(kwargs
)
201 uri
= build_uri(self
.uriparts
, kwargs
)
202 method
= kwargs
.pop('_method', None) or method_for_uri(uri
)
204 # If an _id kwarg is present, this is treated as id as a CGI
206 _id
= kwargs
.pop('_id', None)
210 # If an _timeout is specified in kwargs, use it
211 _timeout
= kwargs
.pop('_timeout', None)
219 uriBase
= "http%s://%s/%s%s%s" % (
220 secure_str
, self
.domain
, uri
, dot
, self
.format
)
222 # Check if argument tells whether img is already base64 encoded
223 b64_convert
= not kwargs
.pop("_base64", False)
227 # Catch media arguments to handle oauth query differently for multipart
229 if 'media' in kwargs
:
231 media
= kwargs
.pop('media')
232 elif 'media[]' in kwargs
:
233 mediafield
= 'media[]'
234 media
= kwargs
.pop('media[]')
236 media
= base64
.b64encode(media
)
237 if sys
.version_info
>= (3, 0):
238 media
= str(media
, 'utf8')
240 # Catch media arguments that are not accepted through multipart
241 # and are not yet base64 encoded
243 for arg
in ['banner', 'image']:
245 kwargs
[arg
] = base64
.b64encode(kwargs
[arg
])
247 headers
= {'Accept-Encoding': 'gzip'}
if self
.gzip
else dict()
251 headers
.update(self
.auth
.generate_headers())
252 # Use urlencoded oauth args with no params when sending media
253 # via multipart and send it directly via uri even for post
254 arg_data
= self
.auth
.encode_params(
255 uriBase
, method
, {} if media
else kwargs
)
256 if method
== 'GET' or media
:
257 uriBase
+= '?' + arg_data
259 body
= arg_data
.encode('utf8')
261 # Handle query as multipart when sending media
263 BOUNDARY
= b
"###Python-Twitter###"
265 bod
.append(b
'--' + BOUNDARY
)
267 b
'Content-Disposition: form-data; name="%s"' % mediafield
.encode('utf-8'))
268 bod
.append(b
'Content-Transfer-Encoding: base64')
271 for k
, v
in kwargs
.items():
272 if sys
.version_info
< (3, 0):
273 k
= k
.encode("utf-8")
274 v
= v
.encode("utf-8")
275 bod
.append(b
'--' + BOUNDARY
)
276 bod
.append(b
'Content-Disposition: form-data; name="%s"' % k
)
279 bod
.append(b
'--' + BOUNDARY
+ b
'--')
280 body
= b
'\r\n'.join(bod
)
281 headers
['Content-Type'] = \
282 'multipart/form-data; boundary=%s' % BOUNDARY
284 if sys
.version_info
< (3, 0):
285 uriBase
= uriBase
.encode("utf-8")
287 headers
[k
.encode('utf-8')] = headers
.pop(k
)
289 req
= urllib_request
.Request(uriBase
, body
, headers
)
291 return self
._handle
_response
_with
_retry
(req
, uri
, arg_data
, _timeout
)
293 return self
._handle
_response
(req
, uri
, arg_data
, _timeout
)
295 def _handle_response(self
, req
, uri
, arg_data
, _timeout
=None):
298 kwargs
['timeout'] = _timeout
300 handle
= urllib_request
.urlopen(req
, **kwargs
)
301 if handle
.headers
['Content-Type'] in ['image/jpeg', 'image/png']:
305 except http_client
.IncompleteRead
as e
:
306 # Even if we don't get all the bytes we should have there
307 # may be a complete response in e.partial
309 if handle
.info().get('Content-Encoding') == 'gzip':
310 # Handle gzip decompression
312 f
= gzip
.GzipFile(fileobj
=buf
)
315 return wrap_response({}, handle
.headers
)
316 elif "json" == self
.format
:
317 res
= json
.loads(data
.decode('utf8'))
318 return wrap_response(res
, handle
.headers
)
320 return wrap_response(
321 data
.decode('utf8'), handle
.headers
)
322 except urllib_error
.HTTPError
as e
:
326 raise TwitterHTTPError(e
, uri
, self
.format
, arg_data
)
328 def _handle_response_with_retry(self
, req
, uri
, arg_data
, _timeout
=None):
332 return self
._handle
_response
(req
, uri
, arg_data
, _timeout
)
333 except TwitterHTTPError
as e
:
335 # API rate limit reached
336 reset
= int(e
.e
.headers
.get('X-Rate-Limit-Reset', time() + 30))
337 delay
= int(reset
- time() + 2) # add some extra margin
338 print("API rate limit reached; waiting for %ds..." % delay
, file=sys
.stderr
)
339 elif e
.e
.code
in (502, 503, 504):
340 delay
= self
.TWITTER_UNAVAILABLE_WAIT
341 print("Service unavailable; waiting for %ds..." % delay
, file=sys
.stderr
)
344 if isinstance(retry
, int):
351 class Twitter(TwitterCall
):
353 The minimalist yet fully featured Twitter API class.
355 Get RESTful data by accessing members of this class. The result
356 is decoded python objects (lists and dicts).
358 The Twitter API is documented at:
360 http://dev.twitter.com/doc
365 from twitter import *
368 auth=OAuth(token, token_key, con_secret, con_secret_key)))
370 # Get your "home" timeline
371 t.statuses.home_timeline()
373 # Get a particular friend's timeline
374 t.statuses.user_timeline(screen_name="billybob")
376 # to pass in GET/POST parameters, such as `count`
377 t.statuses.home_timeline(count=5)
379 # to pass in the GET/POST parameter `id` you need to use `_id`
380 t.statuses.oembed(_id=1234567890)
384 status="Using @sixohsix's sweet Python Twitter Tools.")
386 # Send a direct message
387 t.direct_messages.new(
389 text="I think yer swell!")
391 # Get the members of tamtar's list "Things That Are Rad"
392 t._("tamtar")._("things-that-are-rad").members()
394 # Note how the magic `_` method can be used to insert data
395 # into the middle of a call. You can also use replacement:
396 t.user.list.members(user="tamtar", list="things-that-are-rad")
398 # An *optional* `_timeout` parameter can also be used for API
399 # calls which take much more time than normal or twitter stops
400 # responding for some reason:
402 screen_name=','.join(A_LIST_OF_100_SCREEN_NAMES), \
405 # Overriding Method: GET/POST
406 # you should not need to use this method as this library properly
407 # detects whether GET or POST should be used, Nevertheless
408 # to force a particular method, use `_method`
409 t.statuses.oembed(_id=1234567890, _method='GET')
411 # Send a tweet with an image included (or set your banner or logo similarily)
412 # by just reading your image from the web or a file in a string:
414 with open("example.png", "rb") as imagefile:
415 params = {"media[]": imagefile.read(), "status": status}
416 t.statuses.update_with_media(**params)
418 # Or by sending a base64 encoded image:
419 params = {"media[]": base64_image, "status": status, "_base64": True}
420 t.statuses.update_with_media(**params)
425 # Search for the latest tweets about #pycon
426 t.search.tweets(q="#pycon")
429 Using the data returned
430 -----------------------
432 Twitter API calls return decoded JSON. This is converted into
433 a bunch of Python lists, dicts, ints, and strings. For example::
435 x = twitter.statuses.home_timeline()
437 # The first 'tweet' in the timeline
440 # The screen name of the user who wrote the first 'tweet'
441 x[0]['user']['screen_name']
447 If you prefer to get your Twitter data in XML format, pass
448 format="xml" to the Twitter object when you instantiate it::
450 twitter = Twitter(format="xml")
452 The output will not be parsed in any way. It will be a raw string
458 domain
="api.twitter.com", secure
=True, auth
=None,
459 api_version
=_DEFAULT
, retry
=False):
461 Create a new twitter API connector.
463 Pass an `auth` parameter to use the credentials of a specific
464 user. Generally you'll want to pass an `OAuth`
467 twitter = Twitter(auth=OAuth(
468 token, token_secret, consumer_key, consumer_secret))
471 `domain` lets you change the domain you are connecting. By
472 default it's `api.twitter.com`.
474 If `secure` is False you will connect with HTTP instead of
477 `api_version` is used to set the base uri. By default it's
480 If `retry` is True, API rate limits will automatically be
481 handled by waiting until the next reset, as indicated by
482 the X-Rate-Limit-Reset HTTP header. If retry is an integer,
483 it defines the number of retries attempted.
488 if (format
not in ("json", "xml", "")):
489 raise ValueError("Unknown data format '%s'" % (format
))
491 if api_version
is _DEFAULT
:
496 uriparts
+= (str(api_version
),)
498 TwitterCall
.__init
__(
499 self
, auth
=auth
, format
=format
, domain
=domain
,
500 callable_cls
=TwitterCall
,
501 secure
=secure
, uriparts
=uriparts
, retry
=retry
)
504 __all__
= ["Twitter", "TwitterError", "TwitterHTTPError", "TwitterResponse"]