2 from __future__
import unicode_literals
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
23 import http
.client
as http_client
25 import httplib
as http_client
30 import simplejson
as json
33 class _DEFAULT(object):
37 class TwitterError(Exception):
39 Base Exception thrown by the Twitter object when there is a
40 general error interacting with the API.
45 class TwitterHTTPError(TwitterError
):
47 Exception thrown by the Twitter object when there is an
48 HTTP error interacting with twitter.com.
50 def __init__(self
, e
, uri
, format
, uriparts
):
54 self
.uriparts
= uriparts
56 data
= self
.e
.fp
.read()
57 except http_client
.IncompleteRead
as e
:
58 # can't read the error text
59 # let's try some of it
61 if self
.e
.headers
.get('Content-Encoding') == 'gzip':
63 f
= gzip
.GzipFile(fileobj
=buf
)
64 self
.response_data
= f
.read()
66 self
.response_data
= data
67 super(TwitterHTTPError
, self
).__init
__(str(self
))
70 fmt
= ("." + self
.format
) if self
.format
else ""
72 "Twitter sent status %i for URL: %s%s using parameters: "
73 "(%s)\ndetails: %s" % (
74 self
.e
.code
, self
.uri
, fmt
, self
.uriparts
,
78 class TwitterResponse(object):
80 Response from a twitter request. Behaves like a list or a string
81 (depending on requested format) but it has a few other interesting
84 `headers` gives you access to the response headers as an
85 httplib.HTTPHeaders instance. You can do
86 `response.headers.get('h')` to retrieve a header.
90 def rate_limit_remaining(self
):
92 Remaining requests in the current rate-limit.
94 return int(self
.headers
.get('X-Rate-Limit-Remaining', "0"))
97 def rate_limit_limit(self
):
99 The rate limit ceiling for that given request.
101 return int(self
.headers
.get('X-Rate-Limit-Limit', "0"))
104 def rate_limit_reset(self
):
106 Time in UTC epoch seconds when the rate limit will reset.
108 return int(self
.headers
.get('X-Rate-Limit-Reset', "0"))
111 class TwitterDictResponse(dict, TwitterResponse
):
115 class TwitterListResponse(list, TwitterResponse
):
119 def wrap_response(response
, headers
):
120 response_typ
= type(response
)
121 if response_typ
is dict:
122 res
= TwitterDictResponse(response
)
123 res
.headers
= headers
124 elif response_typ
is list:
125 res
= TwitterListResponse(response
)
126 res
.headers
= headers
131 def method_for_uri(uri
):
133 for action
in POST_ACTIONS
:
134 if re
.search("%s(/\d+)?$" % action
, uri
):
139 class TwitterCall(object):
142 self
, auth
, format
, domain
, callable_cls
, uri
="",
143 uriparts
=None, secure
=True, timeout
=None, gzip
=False):
147 self
.callable_cls
= callable_cls
149 self
.uriparts
= uriparts
151 self
.timeout
= timeout
154 def __getattr__(self
, k
):
156 return object.__getattr
__(self
, k
)
157 except AttributeError:
158 def extend_call(arg
):
159 return self
.callable_cls(
160 auth
=self
.auth
, format
=self
.format
, domain
=self
.domain
,
161 callable_cls
=self
.callable_cls
, timeout
=self
.timeout
,
162 secure
=self
.secure
, gzip
=self
.gzip
,
163 uriparts
=self
.uriparts
+ (arg
,))
167 return extend_call(k
)
169 def __call__(self
, **kwargs
):
172 for uripart
in self
.uriparts
:
173 # If this part matches a keyword argument, use the
174 # supplied value otherwise, just use the part.
175 uriparts
.append(str(kwargs
.pop(uripart
, uripart
)))
176 uri
= '/'.join(uriparts
)
178 method
= kwargs
.pop('_method', None) or method_for_uri(uri
)
180 # If an id kwarg is present and there is no id to fill in in
181 # the list of uriparts, assume the id goes at the end.
182 id = kwargs
.pop('id', None)
186 # If an _id kwarg is present, this is treated as id as a CGI
188 _id
= kwargs
.pop('_id', None)
192 # If an _timeout is specified in kwargs, use it
193 _timeout
= kwargs
.pop('_timeout', None)
201 uriBase
= "http%s://%s/%s%s%s" % (
202 secure_str
, self
.domain
, uri
, dot
, self
.format
)
204 # Check if argument tells whether img is already base64 encoded
206 if "_base64" in kwargs
:
207 b64_convert
= not kwargs
.pop("_base64")
211 # Catch media arguments to handle oauth query differently for multipart
213 for arg
in ['media[]']:
215 media
= kwargs
.pop(arg
)
217 media
= base64
.b64encode(media
)
221 # Catch media arguments that are not accepted through multipart
222 # and are not yet base64 encoded
224 for arg
in ['banner', 'image']:
226 kwargs
[arg
] = base64
.b64encode(kwargs
[arg
])
228 headers
= {'Accept-Encoding': 'gzip'}
if self
.gzip
else dict()
232 headers
.update(self
.auth
.generate_headers())
233 # Use urlencoded oauth args with no params when sending media
234 # via multipart and send it directly via uri even for post
235 arg_data
= self
.auth
.encode_params(
236 uriBase
, method
, {} if media
else kwargs
)
237 if method
== 'GET' or media
:
238 uriBase
+= '?' + arg_data
240 body
= arg_data
.encode('utf8')
242 # Handle query as multipart when sending media
244 BOUNDARY
= "###Python-Twitter###"
246 bod
.append('--' + BOUNDARY
)
248 'Content-Disposition: form-data; name="%s"' % mediafield
)
249 bod
.append('Content-Transfer-Encoding: base64')
252 for k
, v
in kwargs
.items():
253 bod
.append('--' + BOUNDARY
)
254 bod
.append('Content-Disposition: form-data; name="%s"' % k
)
257 bod
.append('--' + BOUNDARY
+ '--')
258 body
= '\r\n'.join(bod
).encode('utf8')
259 headers
['Content-Type'] = \
260 'multipart/form-data; boundary=%s' % BOUNDARY
262 req
= urllib_request
.Request(uriBase
, body
, headers
)
263 return self
._handle
_response
(req
, uri
, arg_data
, _timeout
)
265 def _handle_response(self
, req
, uri
, arg_data
, _timeout
=None):
268 kwargs
['timeout'] = _timeout
270 handle
= urllib_request
.urlopen(req
, **kwargs
)
271 if handle
.headers
['Content-Type'] in ['image/jpeg', 'image/png']:
275 except http_client
.IncompleteRead
as e
:
276 # Even if we don't get all the bytes we should have there
277 # may be a complete response in e.partial
279 if handle
.info().get('Content-Encoding') == 'gzip':
280 # Handle gzip decompression
282 f
= gzip
.GzipFile(fileobj
=buf
)
285 return wrap_response({}, handle
.headers
)
286 elif "json" == self
.format
:
287 res
= json
.loads(data
.decode('utf8'))
288 return wrap_response(res
, handle
.headers
)
290 return wrap_response(
291 data
.decode('utf8'), handle
.headers
)
292 except urllib_error
.HTTPError
as e
:
296 raise TwitterHTTPError(e
, uri
, self
.format
, arg_data
)
299 class Twitter(TwitterCall
):
301 The minimalist yet fully featured Twitter API class.
303 Get RESTful data by accessing members of this class. The result
304 is decoded python objects (lists and dicts).
306 The Twitter API is documented at:
308 http://dev.twitter.com/doc
313 from twitter import *
316 auth=OAuth(token, token_key, con_secret, con_secret_key)))
318 # Get your "home" timeline
319 t.statuses.home_timeline()
321 # Get a particular friend's timeline
322 t.statuses.user_timeline(screen_name="billybob")
324 # to pass in GET/POST parameters, such as `count`
325 t.statuses.home_timeline(count=5)
327 # to pass in the GET/POST parameter `id` you need to use `_id`
328 t.statuses.oembed(_id=1234567890)
332 status="Using @sixohsix's sweet Python Twitter Tools.")
334 # Send a direct message
335 t.direct_messages.new(
337 text="I think yer swell!")
339 # Get the members of tamtar's list "Things That Are Rad"
340 t._("tamtar")._("things-that-are-rad").members()
342 # Note how the magic `_` method can be used to insert data
343 # into the middle of a call. You can also use replacement:
344 t.user.list.members(user="tamtar", list="things-that-are-rad")
346 # An *optional* `_timeout` parameter can also be used for API
347 # calls which take much more time than normal or twitter stops
348 # responding for some reason:
350 screen_name=','.join(A_LIST_OF_100_SCREEN_NAMES), \
353 # Overriding Method: GET/POST
354 # you should not need to use this method as this library properly
355 # detects whether GET or POST should be used, Nevertheless
356 # to force a particular method, use `_method`
357 t.statuses.oembed(_id=1234567890, _method='GET')
359 # Send a tweet with an image included (or set your banner or logo similarily)
360 # by just reading your image from the web or a file in a string:
361 with open("example.png", "rb") as imagefile:
362 params = {"media[]": imagefile.read(), "status": "PTT"}
363 t.statuses.update_with_media(**params)
365 # Or by sending a base64 encoded image:
366 params = {"media[]": base64_image, "status": "PTT", "_base64": True}
367 t.statuses.update_with_media(**params)
372 # Search for the latest tweets about #pycon
373 t.search.tweets(q="#pycon")
376 Using the data returned
377 -----------------------
379 Twitter API calls return decoded JSON. This is converted into
380 a bunch of Python lists, dicts, ints, and strings. For example::
382 x = twitter.statuses.home_timeline()
384 # The first 'tweet' in the timeline
387 # The screen name of the user who wrote the first 'tweet'
388 x[0]['user']['screen_name']
394 If you prefer to get your Twitter data in XML format, pass
395 format="xml" to the Twitter object when you instantiate it::
397 twitter = Twitter(format="xml")
399 The output will not be parsed in any way. It will be a raw string
405 domain
="api.twitter.com", secure
=True, auth
=None,
406 api_version
=_DEFAULT
):
408 Create a new twitter API connector.
410 Pass an `auth` parameter to use the credentials of a specific
411 user. Generally you'll want to pass an `OAuth`
414 twitter = Twitter(auth=OAuth(
415 token, token_secret, consumer_key, consumer_secret))
418 `domain` lets you change the domain you are connecting. By
419 default it's `api.twitter.com`.
421 If `secure` is False you will connect with HTTP instead of
424 `api_version` is used to set the base uri. By default it's
430 if (format
not in ("json", "xml", "")):
431 raise ValueError("Unknown data format '%s'" % (format
))
433 if api_version
is _DEFAULT
:
438 uriparts
+= (str(api_version
),)
440 TwitterCall
.__init
__(
441 self
, auth
=auth
, format
=format
, domain
=domain
,
442 callable_cls
=TwitterCall
,
443 secure
=secure
, uriparts
=uriparts
)
446 __all__
= ["Twitter", "TwitterError", "TwitterHTTPError", "TwitterResponse"]