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
)
71 elif "json" == self
.format
:
72 data
= json
.loads(data
.decode('utf8'))
74 data
= data
.decode('utf8')
75 self
.response_data
= data
76 super(TwitterHTTPError
, self
).__init
__(str(self
))
79 fmt
= ("." + self
.format
) if self
.format
else ""
81 "Twitter sent status %i for URL: %s%s using parameters: "
82 "(%s)\ndetails: %s" % (
83 self
.e
.code
, self
.uri
, fmt
, self
.uriparts
,
87 class TwitterResponse(object):
89 Response from a twitter request. Behaves like a list or a string
90 (depending on requested format) but it has a few other interesting
93 `headers` gives you access to the response headers as an
94 httplib.HTTPHeaders instance. You can do
95 `response.headers.get('h')` to retrieve a header.
99 def rate_limit_remaining(self
):
101 Remaining requests in the current rate-limit.
103 return int(self
.headers
.get('X-Rate-Limit-Remaining', "0"))
106 def rate_limit_limit(self
):
108 The rate limit ceiling for that given request.
110 return int(self
.headers
.get('X-Rate-Limit-Limit', "0"))
113 def rate_limit_reset(self
):
115 Time in UTC epoch seconds when the rate limit will reset.
117 return int(self
.headers
.get('X-Rate-Limit-Reset', "0"))
120 class TwitterDictResponse(dict, TwitterResponse
):
124 class TwitterListResponse(list, TwitterResponse
):
128 def wrap_response(response
, headers
):
129 response_typ
= type(response
)
130 if response_typ
is dict:
131 res
= TwitterDictResponse(response
)
132 res
.headers
= headers
133 elif response_typ
is list:
134 res
= TwitterListResponse(response
)
135 res
.headers
= headers
141 POST_ACTIONS_RE
= re
.compile('(' + '|'.join(POST_ACTIONS
) + r
')(/\d+)?$')
143 def method_for_uri(uri
):
144 if POST_ACTIONS_RE
.search(uri
):
149 def build_uri(orig_uriparts
, kwargs
):
151 Build the URI from the original uriparts and kwargs. Modifies kwargs.
154 for uripart
in orig_uriparts
:
155 # If this part matches a keyword argument (starting with _), use
156 # the supplied value. Otherwise, just use the part.
157 if uripart
.startswith("_"):
158 part
= (str(kwargs
.pop(uripart
, uripart
)))
161 uriparts
.append(part
)
162 uri
= '/'.join(uriparts
)
164 # If an id kwarg is present and there is no id to fill in in
165 # the list of uriparts, assume the id goes at the end.
166 id = kwargs
.pop('id', None)
173 class TwitterCall(object):
175 TWITTER_UNAVAILABLE_WAIT
= 30 # delay after HTTP codes 502, 503 or 504
178 self
, auth
, format
, domain
, callable_cls
, uri
="",
179 uriparts
=None, secure
=True, timeout
=None, gzip
=False, retry
=False):
183 self
.callable_cls
= callable_cls
185 self
.uriparts
= uriparts
187 self
.timeout
= timeout
191 def __getattr__(self
, k
):
193 return object.__getattr
__(self
, k
)
194 except AttributeError:
195 def extend_call(arg
):
196 return self
.callable_cls(
197 auth
=self
.auth
, format
=self
.format
, domain
=self
.domain
,
198 callable_cls
=self
.callable_cls
, timeout
=self
.timeout
,
199 secure
=self
.secure
, gzip
=self
.gzip
, retry
=self
.retry
,
200 uriparts
=self
.uriparts
+ (arg
,))
204 return extend_call(k
)
206 def __call__(self
, **kwargs
):
207 kwargs
= dict(kwargs
)
208 uri
= build_uri(self
.uriparts
, kwargs
)
209 method
= kwargs
.pop('_method', None) or method_for_uri(uri
)
212 # If an _id kwarg is present, this is treated as id as a CGI
214 _id
= kwargs
.pop('_id', None)
218 # If an _timeout is specified in kwargs, use it
219 _timeout
= kwargs
.pop('_timeout', None)
227 url_base
= "http%s://%s/%s%s%s" % (
228 secure_str
, domain
, uri
, dot
, self
.format
)
230 # Check if argument tells whether img is already base64 encoded
231 b64_convert
= not kwargs
.pop("_base64", False)
235 # Catch media arguments to handle oauth query differently for multipart
237 if 'media' in kwargs
:
239 media
= kwargs
.pop('media')
241 elif 'media[]' in kwargs
:
242 mediafield
= 'media[]'
243 media
= kwargs
.pop('media[]')
245 media
= base64
.b64encode(media
)
248 # Catch media arguments that are not accepted through multipart
249 # and are not yet base64 encoded
251 for arg
in ['banner', 'image']:
253 kwargs
[arg
] = base64
.b64encode(kwargs
[arg
])
255 headers
= {'Accept-Encoding': 'gzip'}
if self
.gzip
else dict()
259 headers
.update(self
.auth
.generate_headers())
260 # Use urlencoded oauth args with no params when sending media
261 # via multipart and send it directly via uri even for post
262 arg_data
= self
.auth
.encode_params(
263 url_base
, method
, {} if media
else kwargs
)
264 if method
== 'GET' or media
:
265 url_base
+= '?' + arg_data
267 body
= arg_data
.encode('utf-8')
269 # Handle query as multipart when sending media
271 BOUNDARY
= b
"###Python-Twitter###"
273 bod
.append(b
'--' + BOUNDARY
)
275 b
'Content-Disposition: form-data; name="'
276 + actually_bytes(mediafield
)
278 bod
.append(b
'Content-Type: application/octet-stream')
280 bod
.append(b
'Content-Transfer-Encoding: base64')
282 bod
.append(actually_bytes(media
))
283 for k
, v
in kwargs
.items():
284 k
= actually_bytes(k
)
285 v
= actually_bytes(v
)
286 bod
.append(b
'--' + BOUNDARY
)
287 bod
.append(b
'Content-Disposition: form-data; name="' + k
+ b
'"')
288 bod
.append(b
'Content-Type: text/plain;charset=utf-8')
291 bod
.append(b
'--' + BOUNDARY
+ b
'--')
294 body
= b
'\r\n'.join(bod
)
295 # print(body.decode('utf-8', errors='ignore'))
296 headers
['Content-Type'] = \
297 b
'multipart/form-data; boundary=' + BOUNDARY
299 if not PY_3_OR_HIGHER
:
300 url_base
= url_base
.encode("utf-8")
302 headers
[actually_bytes(k
)] = actually_bytes(headers
.pop(k
))
304 req
= urllib_request
.Request(url_base
, data
=body
, headers
=headers
)
306 return self
._handle
_response
_with
_retry
(req
, uri
, arg_data
, _timeout
)
308 return self
._handle
_response
(req
, uri
, arg_data
, _timeout
)
310 def _handle_response(self
, req
, uri
, arg_data
, _timeout
=None):
313 kwargs
['timeout'] = _timeout
315 handle
= urllib_request
.urlopen(req
, **kwargs
)
316 if handle
.headers
['Content-Type'] in ['image/jpeg', 'image/png']:
320 except http_client
.IncompleteRead
as e
:
321 # Even if we don't get all the bytes we should have there
322 # may be a complete response in e.partial
324 if handle
.info().get('Content-Encoding') == 'gzip':
325 # Handle gzip decompression
327 f
= gzip
.GzipFile(fileobj
=buf
)
330 return wrap_response({}, handle
.headers
)
331 elif "json" == self
.format
:
332 res
= json
.loads(data
.decode('utf8'))
333 return wrap_response(res
, handle
.headers
)
335 return wrap_response(
336 data
.decode('utf8'), handle
.headers
)
337 except urllib_error
.HTTPError
as e
:
341 raise TwitterHTTPError(e
, uri
, self
.format
, arg_data
)
343 def _handle_response_with_retry(self
, req
, uri
, arg_data
, _timeout
=None):
347 return self
._handle
_response
(req
, uri
, arg_data
, _timeout
)
348 except TwitterHTTPError
as e
:
350 # API rate limit reached
351 reset
= int(e
.e
.headers
.get('X-Rate-Limit-Reset', time() + 30))
352 delay
= int(reset
- time() + 2) # add some extra margin
353 print("API rate limit reached; waiting for %ds..." % delay
, file=sys
.stderr
)
354 elif e
.e
.code
in (502, 503, 504):
355 delay
= self
.TWITTER_UNAVAILABLE_WAIT
356 print("Service unavailable; waiting for %ds..." % delay
, file=sys
.stderr
)
359 if isinstance(retry
, int) and not isinstance(retry
, bool):
366 class Twitter(TwitterCall
):
368 The minimalist yet fully featured Twitter API class.
370 Get RESTful data by accessing members of this class. The result
371 is decoded python objects (lists and dicts).
373 The Twitter API is documented at:
375 http://dev.twitter.com/doc
380 from twitter import *
383 auth=OAuth(token, token_key, con_secret, con_secret_key))
385 # Get your "home" timeline
386 t.statuses.home_timeline()
388 # Get a particular friend's timeline
389 t.statuses.user_timeline(screen_name="billybob")
391 # to pass in GET/POST parameters, such as `count`
392 t.statuses.home_timeline(count=5)
394 # to pass in the GET/POST parameter `id` you need to use `_id`
395 t.statuses.oembed(_id=1234567890)
399 status="Using @sixohsix's sweet Python Twitter Tools.")
401 # Send a direct message
402 t.direct_messages.new(
404 text="I think yer swell!")
406 # Get the members of tamtar's list "Things That Are Rad"
407 t.lists.members(owner_screen_name="tamtar", slug="things-that-are-rad")
409 # An *optional* `_timeout` parameter can also be used for API
410 # calls which take much more time than normal or twitter stops
411 # responding for some reason:
413 screen_name=','.join(A_LIST_OF_100_SCREEN_NAMES), \
416 # Overriding Method: GET/POST
417 # you should not need to use this method as this library properly
418 # detects whether GET or POST should be used, Nevertheless
419 # to force a particular method, use `_method`
420 t.statuses.oembed(_id=1234567890, _method='GET')
422 # Send a tweet with an image included (or set your banner or logo similarily)
423 # by just reading your image from the web or a file in a string:
425 with open("example.png", "rb") as imagefile:
426 params = {"media[]": imagefile.read(), "status": status}
427 t.statuses.update_with_media(**params)
429 # Or by sending a base64 encoded image:
430 params = {"media[]": base64_image, "status": status, "_base64": True}
431 t.statuses.update_with_media(**params)
436 # Search for the latest tweets about #pycon
437 t.search.tweets(q="#pycon")
440 Using the data returned
441 -----------------------
443 Twitter API calls return decoded JSON. This is converted into
444 a bunch of Python lists, dicts, ints, and strings. For example::
446 x = twitter.statuses.home_timeline()
448 # The first 'tweet' in the timeline
451 # The screen name of the user who wrote the first 'tweet'
452 x[0]['user']['screen_name']
458 If you prefer to get your Twitter data in XML format, pass
459 format="xml" to the Twitter object when you instantiate it::
461 twitter = Twitter(format="xml")
463 The output will not be parsed in any way. It will be a raw string
469 domain
="api.twitter.com", secure
=True, auth
=None,
470 api_version
=_DEFAULT
, retry
=False):
472 Create a new twitter API connector.
474 Pass an `auth` parameter to use the credentials of a specific
475 user. Generally you'll want to pass an `OAuth`
478 twitter = Twitter(auth=OAuth(
479 token, token_secret, consumer_key, consumer_secret))
482 `domain` lets you change the domain you are connecting. By
483 default it's `api.twitter.com`.
485 If `secure` is False you will connect with HTTP instead of
488 `api_version` is used to set the base uri. By default it's
491 If `retry` is True, API rate limits will automatically be
492 handled by waiting until the next reset, as indicated by
493 the X-Rate-Limit-Reset HTTP header. If retry is an integer,
494 it defines the number of retries attempted.
499 if (format
not in ("json", "xml", "")):
500 raise ValueError("Unknown data format '%s'" % (format
))
502 if api_version
is _DEFAULT
:
507 uriparts
+= (str(api_version
),)
509 TwitterCall
.__init
__(
510 self
, auth
=auth
, format
=format
, domain
=domain
,
511 callable_cls
=TwitterCall
,
512 secure
=secure
, uriparts
=uriparts
, retry
=retry
)
515 __all__
= ["Twitter", "TwitterError", "TwitterHTTPError", "TwitterResponse"]