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
):
141 class TwitterCall(object):
143 TWITTER_UNAVAILABLE_WAIT
= 30 # delay after HTTP codes 502, 503 or 504
146 self
, auth
, format
, domain
, callable_cls
, uri
="",
147 uriparts
=None, secure
=True, timeout
=None, gzip
=False, retry
=False):
151 self
.callable_cls
= callable_cls
153 self
.uriparts
= uriparts
155 self
.timeout
= timeout
159 def __getattr__(self
, k
):
161 return object.__getattr
__(self
, k
)
162 except AttributeError:
163 def extend_call(arg
):
164 return self
.callable_cls(
165 auth
=self
.auth
, format
=self
.format
, domain
=self
.domain
,
166 callable_cls
=self
.callable_cls
, timeout
=self
.timeout
,
167 secure
=self
.secure
, gzip
=self
.gzip
, retry
=self
.retry
,
168 uriparts
=self
.uriparts
+ (arg
,))
172 return extend_call(k
)
174 def __call__(self
, **kwargs
):
177 for uripart
in self
.uriparts
:
178 # If this part matches a keyword argument (starting with _), use
179 # the supplied value. Otherwise, just use the part.
180 if uripart
.startswith("_"):
181 part
= (str(kwargs
.pop(uripart
, uripart
)))
184 uriparts
.append(part
)
185 uri
= '/'.join(uriparts
)
187 method
= kwargs
.pop('_method', None) or method_for_uri(uri
)
189 # If an id kwarg is present and there is no id to fill in in
190 # the list of uriparts, assume the id goes at the end.
191 id = kwargs
.pop('id', None)
195 # If an _id kwarg is present, this is treated as id as a CGI
197 _id
= kwargs
.pop('_id', None)
201 # If an _timeout is specified in kwargs, use it
202 _timeout
= kwargs
.pop('_timeout', None)
210 uriBase
= "http%s://%s/%s%s%s" % (
211 secure_str
, self
.domain
, uri
, dot
, self
.format
)
213 # Check if argument tells whether img is already base64 encoded
214 b64_convert
= not kwargs
.pop("_base64", False)
218 # Catch media arguments to handle oauth query differently for multipart
220 if 'media' in kwargs
:
222 media
= kwargs
.pop('media')
223 elif 'media[]' in kwargs
:
224 mediafield
= 'media[]'
225 media
= kwargs
.pop('media[]')
227 media
= base64
.b64encode(media
)
228 if sys
.version_info
>= (3, 0):
229 media
= str(media
, 'utf8')
231 # Catch media arguments that are not accepted through multipart
232 # and are not yet base64 encoded
234 for arg
in ['banner', 'image']:
236 kwargs
[arg
] = base64
.b64encode(kwargs
[arg
])
238 headers
= {'Accept-Encoding': 'gzip'}
if self
.gzip
else dict()
242 headers
.update(self
.auth
.generate_headers())
243 # Use urlencoded oauth args with no params when sending media
244 # via multipart and send it directly via uri even for post
245 arg_data
= self
.auth
.encode_params(
246 uriBase
, method
, {} if media
else kwargs
)
247 if method
== 'GET' or media
:
248 uriBase
+= '?' + arg_data
250 body
= arg_data
.encode('utf8')
252 # Handle query as multipart when sending media
254 BOUNDARY
= b
"###Python-Twitter###"
256 bod
.append(b
'--' + BOUNDARY
)
258 b
'Content-Disposition: form-data; name="%s"' % mediafield
.encode('utf-8'))
259 bod
.append(b
'Content-Transfer-Encoding: base64')
262 for k
, v
in kwargs
.items():
263 if sys
.version_info
< (3, 0):
264 k
= k
.encode("utf-8")
265 v
= v
.encode("utf-8")
266 bod
.append(b
'--' + BOUNDARY
)
267 bod
.append(b
'Content-Disposition: form-data; name="%s"' % k
)
270 bod
.append(b
'--' + BOUNDARY
+ b
'--')
271 body
= b
'\r\n'.join(bod
)
272 headers
['Content-Type'] = \
273 'multipart/form-data; boundary=%s' % BOUNDARY
275 if sys
.version_info
< (3, 0):
276 uriBase
= uriBase
.encode("utf-8")
278 headers
[k
.encode('utf-8')] = headers
.pop(k
)
280 req
= urllib_request
.Request(uriBase
, body
, headers
)
282 return self
._handle
_response
_with
_retry
(req
, uri
, arg_data
, _timeout
)
284 return self
._handle
_response
(req
, uri
, arg_data
, _timeout
)
286 def _handle_response(self
, req
, uri
, arg_data
, _timeout
=None):
289 kwargs
['timeout'] = _timeout
291 handle
= urllib_request
.urlopen(req
, **kwargs
)
292 if handle
.headers
['Content-Type'] in ['image/jpeg', 'image/png']:
296 except http_client
.IncompleteRead
as e
:
297 # Even if we don't get all the bytes we should have there
298 # may be a complete response in e.partial
300 if handle
.info().get('Content-Encoding') == 'gzip':
301 # Handle gzip decompression
303 f
= gzip
.GzipFile(fileobj
=buf
)
306 return wrap_response({}, handle
.headers
)
307 elif "json" == self
.format
:
308 res
= json
.loads(data
.decode('utf8'))
309 return wrap_response(res
, handle
.headers
)
311 return wrap_response(
312 data
.decode('utf8'), handle
.headers
)
313 except urllib_error
.HTTPError
as e
:
317 raise TwitterHTTPError(e
, uri
, self
.format
, arg_data
)
319 def _handle_response_with_retry(self
, req
, uri
, arg_data
, _timeout
=None):
323 return self
._handle
_response
(req
, uri
, arg_data
, _timeout
)
324 except TwitterHTTPError
as e
:
326 # API rate limit reached
327 reset
= int(e
.e
.headers
.get('X-Rate-Limit-Reset', time() + 30))
328 delay
= int(reset
- time() + 2) # add some extra margin
329 print("API rate limit reached; waiting for %ds..." % delay
, file=sys
.stderr
)
330 elif e
.e
.code
in (502, 503, 504):
331 delay
= self
.TWITTER_UNAVAILABLE_WAIT
332 print("Service unavailable; waiting for %ds..." % delay
, file=sys
.stderr
)
335 if isinstance(retry
, int):
342 class Twitter(TwitterCall
):
344 The minimalist yet fully featured Twitter API class.
346 Get RESTful data by accessing members of this class. The result
347 is decoded python objects (lists and dicts).
349 The Twitter API is documented at:
351 http://dev.twitter.com/doc
356 from twitter import *
359 auth=OAuth(token, token_key, con_secret, con_secret_key)))
361 # Get your "home" timeline
362 t.statuses.home_timeline()
364 # Get a particular friend's timeline
365 t.statuses.user_timeline(screen_name="billybob")
367 # to pass in GET/POST parameters, such as `count`
368 t.statuses.home_timeline(count=5)
370 # to pass in the GET/POST parameter `id` you need to use `_id`
371 t.statuses.oembed(_id=1234567890)
375 status="Using @sixohsix's sweet Python Twitter Tools.")
377 # Send a direct message
378 t.direct_messages.new(
380 text="I think yer swell!")
382 # Get the members of tamtar's list "Things That Are Rad"
383 t._("tamtar")._("things-that-are-rad").members()
385 # Note how the magic `_` method can be used to insert data
386 # into the middle of a call. You can also use replacement:
387 t.user.list.members(user="tamtar", list="things-that-are-rad")
389 # An *optional* `_timeout` parameter can also be used for API
390 # calls which take much more time than normal or twitter stops
391 # responding for some reason:
393 screen_name=','.join(A_LIST_OF_100_SCREEN_NAMES), \
396 # Overriding Method: GET/POST
397 # you should not need to use this method as this library properly
398 # detects whether GET or POST should be used, Nevertheless
399 # to force a particular method, use `_method`
400 t.statuses.oembed(_id=1234567890, _method='GET')
402 # Send a tweet with an image included (or set your banner or logo similarily)
403 # by just reading your image from the web or a file in a string:
405 with open("example.png", "rb") as imagefile:
406 params = {"media[]": imagefile.read(), "status": status}
407 t.statuses.update_with_media(**params)
409 # Or by sending a base64 encoded image:
410 params = {"media[]": base64_image, "status": status, "_base64": True}
411 t.statuses.update_with_media(**params)
416 # Search for the latest tweets about #pycon
417 t.search.tweets(q="#pycon")
420 Using the data returned
421 -----------------------
423 Twitter API calls return decoded JSON. This is converted into
424 a bunch of Python lists, dicts, ints, and strings. For example::
426 x = twitter.statuses.home_timeline()
428 # The first 'tweet' in the timeline
431 # The screen name of the user who wrote the first 'tweet'
432 x[0]['user']['screen_name']
438 If you prefer to get your Twitter data in XML format, pass
439 format="xml" to the Twitter object when you instantiate it::
441 twitter = Twitter(format="xml")
443 The output will not be parsed in any way. It will be a raw string
449 domain
="api.twitter.com", secure
=True, auth
=None,
450 api_version
=_DEFAULT
, retry
=False):
452 Create a new twitter API connector.
454 Pass an `auth` parameter to use the credentials of a specific
455 user. Generally you'll want to pass an `OAuth`
458 twitter = Twitter(auth=OAuth(
459 token, token_secret, consumer_key, consumer_secret))
462 `domain` lets you change the domain you are connecting. By
463 default it's `api.twitter.com`.
465 If `secure` is False you will connect with HTTP instead of
468 `api_version` is used to set the base uri. By default it's
471 If `retry` is True, API rate limits will automatically be
472 handled by waiting until the next reset, as indicated by
473 the X-Rate-Limit-Reset HTTP header. If retry is an integer,
474 it defines the number of retries attempted.
479 if (format
not in ("json", "xml", "")):
480 raise ValueError("Unknown data format '%s'" % (format
))
482 if api_version
is _DEFAULT
:
487 uriparts
+= (str(api_version
),)
489 TwitterCall
.__init
__(
490 self
, auth
=auth
, format
=format
, domain
=domain
,
491 callable_cls
=TwitterCall
,
492 secure
=secure
, uriparts
=uriparts
, retry
=retry
)
495 __all__
= ["Twitter", "TwitterError", "TwitterHTTPError", "TwitterResponse"]