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
132 class TwitterCall(object):
135 self
, auth
, format
, domain
, callable_cls
, uri
="",
136 uriparts
=None, secure
=True, timeout
=None, gzip
=False):
140 self
.callable_cls
= callable_cls
142 self
.uriparts
= uriparts
144 self
.timeout
= timeout
147 def __getattr__(self
, k
):
149 return object.__getattr
__(self
, k
)
150 except AttributeError:
151 def extend_call(arg
):
152 return self
.callable_cls(
153 auth
=self
.auth
, format
=self
.format
, domain
=self
.domain
,
154 callable_cls
=self
.callable_cls
, timeout
=self
.timeout
,
155 secure
=self
.secure
, gzip
=self
.gzip
,
156 uriparts
=self
.uriparts
+ (arg
,))
160 return extend_call(k
)
162 def __call__(self
, **kwargs
):
165 for uripart
in self
.uriparts
:
166 # If this part matches a keyword argument, use the
167 # supplied value otherwise, just use the part.
168 uriparts
.append(str(kwargs
.pop(uripart
, uripart
)))
169 uri
= '/'.join(uriparts
)
171 method
= kwargs
.pop('_method', None)
174 for action
in POST_ACTIONS
:
175 if re
.search("%s(/\d+)?$" % action
, uri
):
179 # If an id kwarg is present and there is no id to fill in in
180 # the list of uriparts, assume the id goes at the end.
181 id = kwargs
.pop('id', None)
185 # If an _id kwarg is present, this is treated as id as a CGI
187 _id
= kwargs
.pop('_id', None)
191 # If an _timeout is specified in kwargs, use it
192 _timeout
= kwargs
.pop('_timeout', None)
200 uriBase
= "http%s://%s/%s%s%s" % (
201 secure_str
, self
.domain
, uri
, dot
, self
.format
)
203 # Catch media arguments to handle oauth query differently for multipart
205 for arg
in ['media[]', 'banner', 'image']:
207 media
= kwargs
.pop(arg
)
211 headers
= {'Accept-Encoding': 'gzip'}
if self
.gzip
else dict()
215 headers
.update(self
.auth
.generate_headers())
216 # Use urlencoded oauth args with no params when sending media
217 # via multipart and send it directly via uri even for post
218 arg_data
= self
.auth
.encode_params(
219 uriBase
, method
, {} if media
else kwargs
)
220 if method
== 'GET' or media
:
221 uriBase
+= '?' + arg_data
223 body
= arg_data
.encode('utf8')
225 # Handle query as multipart when sending media
227 BOUNDARY
= "###Python-Twitter###"
229 bod
.append('--' + BOUNDARY
)
231 'Content-Disposition: form-data; name="%s"' % mediafield
)
234 for k
, v
in kwargs
.items():
235 bod
.append('--' + BOUNDARY
)
236 bod
.append('Content-Disposition: form-data; name="%s"' % k
)
239 bod
.append('--' + BOUNDARY
+ '--')
240 body
= '\r\n'.join(bod
)
241 headers
['Content-Type'] = \
242 'multipart/form-data; boundary=%s' % BOUNDARY
244 req
= urllib_request
.Request(uriBase
, body
, headers
)
245 return self
._handle
_response
(req
, uri
, arg_data
, _timeout
)
247 def _handle_response(self
, req
, uri
, arg_data
, _timeout
=None):
250 kwargs
['timeout'] = _timeout
252 handle
= urllib_request
.urlopen(req
, **kwargs
)
253 if handle
.headers
['Content-Type'] in ['image/jpeg', 'image/png']:
257 except http_client
.IncompleteRead
as e
:
258 # Even if we don't get all the bytes we should have there
259 # may be a complete response in e.partial
261 if handle
.info().get('Content-Encoding') == 'gzip':
262 # Handle gzip decompression
264 f
= gzip
.GzipFile(fileobj
=buf
)
266 if "json" == self
.format
:
267 res
= json
.loads(data
.decode('utf8'))
268 return wrap_response(res
, handle
.headers
)
270 return wrap_response(
271 data
.decode('utf8'), handle
.headers
)
272 except urllib_error
.HTTPError
as e
:
276 raise TwitterHTTPError(e
, uri
, self
.format
, arg_data
)
279 class Twitter(TwitterCall
):
281 The minimalist yet fully featured Twitter API class.
283 Get RESTful data by accessing members of this class. The result
284 is decoded python objects (lists and dicts).
286 The Twitter API is documented at:
288 http://dev.twitter.com/doc
294 auth=OAuth(token, token_key, con_secret, con_secret_key)))
296 # Get your "home" timeline
297 t.statuses.home_timeline()
299 # Get a particular friend's tweets
300 t.statuses.user_timeline(user_id="billybob")
304 status="Using @sixohsix's sweet Python Twitter Tools.")
306 # Send a direct message
307 t.direct_messages.new(
309 text="I think yer swell!")
311 # Get the members of tamtar's list "Things That Are Rad"
312 t._("tamtar")._("things-that-are-rad").members()
314 # Note how the magic `_` method can be used to insert data
315 # into the middle of a call. You can also use replacement:
316 t.user.list.members(user="tamtar", list="things-that-are-rad")
318 # An *optional* `_timeout` parameter can also be used for API
319 # calls which take much more time than normal or twitter stops
320 # responding for some reasone
322 screen_name=','.join(A_LIST_OF_100_SCREEN_NAMES), \
329 # Search for the latest tweets about #pycon
330 t.search.tweets(q="#pycon")
333 Using the data returned
334 -----------------------
336 Twitter API calls return decoded JSON. This is converted into
337 a bunch of Python lists, dicts, ints, and strings. For example::
339 x = twitter.statuses.home_timeline()
341 # The first 'tweet' in the timeline
344 # The screen name of the user who wrote the first 'tweet'
345 x[0]['user']['screen_name']
351 If you prefer to get your Twitter data in XML format, pass
352 format="xml" to the Twitter object when you instantiate it::
354 twitter = Twitter(format="xml")
356 The output will not be parsed in any way. It will be a raw string
362 domain
="api.twitter.com", secure
=True, auth
=None,
363 api_version
=_DEFAULT
):
365 Create a new twitter API connector.
367 Pass an `auth` parameter to use the credentials of a specific
368 user. Generally you'll want to pass an `OAuth`
371 twitter = Twitter(auth=OAuth(
372 token, token_secret, consumer_key, consumer_secret))
375 `domain` lets you change the domain you are connecting. By
376 default it's `api.twitter.com`.
378 If `secure` is False you will connect with HTTP instead of
381 `api_version` is used to set the base uri. By default it's
387 if (format
not in ("json", "xml", "")):
388 raise ValueError("Unknown data format '%s'" % (format
))
390 if api_version
is _DEFAULT
:
395 uriparts
+= (str(api_version
),)
397 TwitterCall
.__init
__(
398 self
, auth
=auth
, format
=format
, domain
=domain
,
399 callable_cls
=TwitterCall
,
400 secure
=secure
, uriparts
=uriparts
)
403 __all__
= ["Twitter", "TwitterError", "TwitterHTTPError", "TwitterResponse"]