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, use the
179 # supplied value otherwise, just use the part.
180 uriparts
.append(str(kwargs
.pop(uripart
, uripart
)))
181 uri
= '/'.join(uriparts
)
183 method
= kwargs
.pop('_method', None) or method_for_uri(uri
)
185 # If an id kwarg is present and there is no id to fill in in
186 # the list of uriparts, assume the id goes at the end.
187 id = kwargs
.pop('id', None)
191 # If an _id kwarg is present, this is treated as id as a CGI
193 _id
= kwargs
.pop('_id', None)
197 # If an _timeout is specified in kwargs, use it
198 _timeout
= kwargs
.pop('_timeout', None)
206 uriBase
= "http%s://%s/%s%s%s" % (
207 secure_str
, self
.domain
, uri
, dot
, self
.format
)
209 # Check if argument tells whether img is already base64 encoded
211 if "_base64" in kwargs
:
212 b64_convert
= not kwargs
.pop("_base64")
216 # Catch media arguments to handle oauth query differently for multipart
218 for arg
in ['media[]']:
220 media
= kwargs
.pop(arg
)
222 media
= base64
.b64encode(media
)
223 if sys
.version_info
>= (3, 0):
224 media
= str(media
, 'utf8')
228 # Catch media arguments that are not accepted through multipart
229 # and are not yet base64 encoded
231 for arg
in ['banner', 'image']:
233 kwargs
[arg
] = base64
.b64encode(kwargs
[arg
])
235 headers
= {'Accept-Encoding': 'gzip'}
if self
.gzip
else dict()
239 headers
.update(self
.auth
.generate_headers())
240 # Use urlencoded oauth args with no params when sending media
241 # via multipart and send it directly via uri even for post
242 arg_data
= self
.auth
.encode_params(
243 uriBase
, method
, {} if media
else kwargs
)
244 if method
== 'GET' or media
:
245 uriBase
+= '?' + arg_data
247 body
= arg_data
.encode('utf8')
249 # Handle query as multipart when sending media
251 BOUNDARY
= "###Python-Twitter###"
253 bod
.append('--' + BOUNDARY
)
255 'Content-Disposition: form-data; name="%s"' % mediafield
)
256 bod
.append('Content-Transfer-Encoding: base64')
259 for k
, v
in kwargs
.items():
260 bod
.append('--' + BOUNDARY
)
261 bod
.append('Content-Disposition: form-data; name="%s"' % k
)
263 if sys
.version_info
[:2] <= (2, 7):
265 v
= v
.decode("utf-8")
269 bod
.append('--' + BOUNDARY
+ '--')
270 body
= '\r\n'.join(bod
).encode('utf8')
271 headers
['Content-Type'] = \
272 'multipart/form-data; boundary=%s' % BOUNDARY
274 if sys
.version_info
[:2] <= (2, 7):
275 uriBase
= uriBase
.encode("utf-8")
277 headers
[k
.encode('utf-8')] = headers
.pop(k
)
279 req
= urllib_request
.Request(uriBase
, body
, headers
)
281 return self
._handle
_response
_with
_retry
(req
, uri
, arg_data
, _timeout
)
283 return self
._handle
_response
(req
, uri
, arg_data
, _timeout
)
285 def _handle_response(self
, req
, uri
, arg_data
, _timeout
=None):
288 kwargs
['timeout'] = _timeout
290 handle
= urllib_request
.urlopen(req
, **kwargs
)
291 if handle
.headers
['Content-Type'] in ['image/jpeg', 'image/png']:
295 except http_client
.IncompleteRead
as e
:
296 # Even if we don't get all the bytes we should have there
297 # may be a complete response in e.partial
299 if handle
.info().get('Content-Encoding') == 'gzip':
300 # Handle gzip decompression
302 f
= gzip
.GzipFile(fileobj
=buf
)
305 return wrap_response({}, handle
.headers
)
306 elif "json" == self
.format
:
307 res
= json
.loads(data
.decode('utf8'))
308 return wrap_response(res
, handle
.headers
)
310 return wrap_response(
311 data
.decode('utf8'), handle
.headers
)
312 except urllib_error
.HTTPError
as e
:
316 raise TwitterHTTPError(e
, uri
, self
.format
, arg_data
)
318 def _handle_response_with_retry(self
, req
, uri
, arg_data
, _timeout
=None):
322 return self
._handle
_response
(req
, uri
, arg_data
, _timeout
)
323 except TwitterHTTPError
as e
:
325 # API rate limit reached
326 reset
= int(e
.e
.headers
.get('X-Rate-Limit-Reset', time() + 30))
327 delay
= int(reset
- time() + 2) # add some extra margin
328 print("API rate limit reached; waiting for %ds..." % delay
, file=sys
.stderr
)
329 elif e
.e
.code
in (502, 503, 504):
330 delay
= self
.TWITTER_UNAVAILABLE_WAIT
331 print("Service unavailable; waiting for %ds..." % delay
, file=sys
.stderr
)
334 if isinstance(retry
, int):
341 class Twitter(TwitterCall
):
343 The minimalist yet fully featured Twitter API class.
345 Get RESTful data by accessing members of this class. The result
346 is decoded python objects (lists and dicts).
348 The Twitter API is documented at:
350 http://dev.twitter.com/doc
355 from twitter import *
358 auth=OAuth(token, token_key, con_secret, con_secret_key)))
360 # Get your "home" timeline
361 t.statuses.home_timeline()
363 # Get a particular friend's timeline
364 t.statuses.user_timeline(screen_name="billybob")
366 # to pass in GET/POST parameters, such as `count`
367 t.statuses.home_timeline(count=5)
369 # to pass in the GET/POST parameter `id` you need to use `_id`
370 t.statuses.oembed(_id=1234567890)
374 status="Using @sixohsix's sweet Python Twitter Tools.")
376 # Send a direct message
377 t.direct_messages.new(
379 text="I think yer swell!")
381 # Get the members of tamtar's list "Things That Are Rad"
382 t._("tamtar")._("things-that-are-rad").members()
384 # Note how the magic `_` method can be used to insert data
385 # into the middle of a call. You can also use replacement:
386 t.user.list.members(user="tamtar", list="things-that-are-rad")
388 # An *optional* `_timeout` parameter can also be used for API
389 # calls which take much more time than normal or twitter stops
390 # responding for some reason:
392 screen_name=','.join(A_LIST_OF_100_SCREEN_NAMES), \
395 # Overriding Method: GET/POST
396 # you should not need to use this method as this library properly
397 # detects whether GET or POST should be used, Nevertheless
398 # to force a particular method, use `_method`
399 t.statuses.oembed(_id=1234567890, _method='GET')
401 # Send a tweet with an image included (or set your banner or logo similarily)
402 # by just reading your image from the web or a file in a string:
404 with open("example.png", "rb") as imagefile:
405 params = {"media[]": imagefile.read(), "status": status}
406 t.statuses.update_with_media(**params)
408 # Or by sending a base64 encoded image:
409 params = {"media[]": base64_image, "status": status, "_base64": True}
410 t.statuses.update_with_media(**params)
415 # Search for the latest tweets about #pycon
416 t.search.tweets(q="#pycon")
419 Using the data returned
420 -----------------------
422 Twitter API calls return decoded JSON. This is converted into
423 a bunch of Python lists, dicts, ints, and strings. For example::
425 x = twitter.statuses.home_timeline()
427 # The first 'tweet' in the timeline
430 # The screen name of the user who wrote the first 'tweet'
431 x[0]['user']['screen_name']
437 If you prefer to get your Twitter data in XML format, pass
438 format="xml" to the Twitter object when you instantiate it::
440 twitter = Twitter(format="xml")
442 The output will not be parsed in any way. It will be a raw string
448 domain
="api.twitter.com", secure
=True, auth
=None,
449 api_version
=_DEFAULT
, retry
=False):
451 Create a new twitter API connector.
453 Pass an `auth` parameter to use the credentials of a specific
454 user. Generally you'll want to pass an `OAuth`
457 twitter = Twitter(auth=OAuth(
458 token, token_secret, consumer_key, consumer_secret))
461 `domain` lets you change the domain you are connecting. By
462 default it's `api.twitter.com`.
464 If `secure` is False you will connect with HTTP instead of
467 `api_version` is used to set the base uri. By default it's
470 If `retry` is True, API rate limits will automatically be
471 handled by waiting until the next reset, as indicated by
472 the X-Rate-Limit-Reset HTTP header. If retry is an integer,
473 it defines the number of retries attempted.
478 if (format
not in ("json", "xml", "")):
479 raise ValueError("Unknown data format '%s'" % (format
))
481 if api_version
is _DEFAULT
:
486 uriparts
+= (str(api_version
),)
488 TwitterCall
.__init
__(
489 self
, auth
=auth
, format
=format
, domain
=domain
,
490 callable_cls
=TwitterCall
,
491 secure
=secure
, uriparts
=uriparts
, retry
=retry
)
494 __all__
= ["Twitter", "TwitterError", "TwitterHTTPError", "TwitterResponse"]