2 import urllib
.request
as urllib_request
3 import urllib
.error
as urllib_error
5 import urllib2
as urllib_request
6 import urllib2
as urllib_error
9 from cStringIO
import StringIO
11 from io
import BytesIO
as StringIO
13 from .twitter_globals
import POST_ACTIONS
14 from .auth
import NoAuth
20 import http
.client
as http_client
22 import httplib
as http_client
27 import simplejson
as json
30 class _DEFAULT(object):
33 class TwitterError(Exception):
35 Base Exception thrown by the Twitter object when there is a
36 general error interacting with the API.
40 class TwitterHTTPError(TwitterError
):
42 Exception thrown by the Twitter object when there is an
43 HTTP error interacting with twitter.com.
45 def __init__(self
, e
, uri
, format
, uriparts
):
49 self
.uriparts
= uriparts
51 data
= self
.e
.fp
.read()
52 except http_client
.IncompleteRead
as e
:
53 # can't read the error text
54 # let's try some of it
56 if self
.e
.headers
.get('Content-Encoding') == 'gzip':
58 f
= gzip
.GzipFile(fileobj
=buf
)
59 self
.response_data
= f
.read()
61 self
.response_data
= data
62 super(TwitterHTTPError
, self
).__init
__(str(self
))
65 fmt
= ("." + self
.format
) if self
.format
else ""
67 "Twitter sent status %i for URL: %s%s using parameters: "
68 "(%s)\ndetails: %s" %(
69 self
.e
.code
, self
.uri
, fmt
, self
.uriparts
,
72 class TwitterResponse(object):
74 Response from a twitter request. Behaves like a list or a string
75 (depending on requested format) but it has a few other interesting
78 `headers` gives you access to the response headers as an
79 httplib.HTTPHeaders instance. You can do
80 `response.headers.get('h')` to retrieve a header.
82 def __init__(self
, headers
):
83 self
.headers
= headers
86 def rate_limit_remaining(self
):
88 Remaining requests in the current rate-limit.
90 return int(self
.headers
.get('X-Rate-Limit-Remaining', "0"))
93 def rate_limit_limit(self
):
95 The rate limit ceiling for that given request.
97 return int(self
.headers
.get('X-Rate-Limit-Limit', "0"))
100 def rate_limit_reset(self
):
102 Time in UTC epoch seconds when the rate limit will reset.
104 return int(self
.headers
.get('X-Rate-Limit-Reset', "0"))
107 def wrap_response(response
, headers
):
108 response_typ
= type(response
)
109 if response_typ
is bool:
110 # HURF DURF MY NAME IS PYTHON AND I CAN'T SUBCLASS bool.
112 elif response_typ
is str:
115 class WrappedTwitterResponse(response_typ
, TwitterResponse
):
116 __doc__
= TwitterResponse
.__doc
__
118 def __init__(self
, response
, headers
):
119 response_typ
.__init
__(self
, response
)
120 TwitterResponse
.__init
__(self
, headers
)
121 def __new__(cls
, response
, headers
):
122 return response_typ
.__new
__(cls
, response
)
124 return WrappedTwitterResponse(response
, headers
)
128 class TwitterCall(object):
131 self
, auth
, format
, domain
, callable_cls
, uri
="",
132 uriparts
=None, secure
=True, timeout
=None, gzip
=False):
136 self
.callable_cls
= callable_cls
138 self
.uriparts
= uriparts
140 self
.timeout
= timeout
143 def __getattr__(self
, k
):
145 return object.__getattr
__(self
, k
)
146 except AttributeError:
147 def extend_call(arg
):
148 return self
.callable_cls(
149 auth
=self
.auth
, format
=self
.format
, domain
=self
.domain
,
150 callable_cls
=self
.callable_cls
, timeout
=self
.timeout
,
151 secure
=self
.secure
, gzip
=self
.gzip
,
152 uriparts
=self
.uriparts
+ (arg
,))
156 return extend_call(k
)
158 def __call__(self
, **kwargs
):
161 for uripart
in self
.uriparts
:
162 # If this part matches a keyword argument, use the
163 # supplied value otherwise, just use the part.
164 uriparts
.append(str(kwargs
.pop(uripart
, uripart
)))
165 uri
= '/'.join(uriparts
)
167 method
= kwargs
.pop('_method', None)
170 for action
in POST_ACTIONS
:
171 if re
.search("%s(/\d+)?$" % action
, uri
):
175 # If an id kwarg is present and there is no id to fill in in
176 # the list of uriparts, assume the id goes at the end.
177 id = kwargs
.pop('id', None)
181 # If an _id kwarg is present, this is treated as id as a CGI
183 _id
= kwargs
.pop('_id', None)
187 # If an _timeout is specified in kwargs, use it
188 _timeout
= kwargs
.pop('_timeout', None)
196 uriBase
= "http%s://%s/%s%s%s" %(
197 secure_str
, self
.domain
, uri
, dot
, self
.format
)
199 headers
= {'Accept-Encoding': 'gzip'}
if self
.gzip
else dict()
200 body
= None; arg_data
= None
202 headers
.update(self
.auth
.generate_headers())
203 arg_data
= self
.auth
.encode_params(uriBase
, method
, kwargs
)
205 uriBase
+= '?' + arg_data
207 body
= arg_data
.encode('utf8')
209 req
= urllib_request
.Request(uriBase
, body
, headers
)
210 return self
._handle
_response
(req
, uri
, arg_data
, _timeout
)
212 def _handle_response(self
, req
, uri
, arg_data
, _timeout
=None):
215 kwargs
['timeout'] = _timeout
217 handle
= urllib_request
.urlopen(req
, **kwargs
)
218 if handle
.headers
['Content-Type'] in ['image/jpeg', 'image/png']:
222 except http_client
.IncompleteRead
as e
:
223 # Even if we don't get all the bytes we should have there
224 # may be a complete response in e.partial
226 if handle
.info().get('Content-Encoding') == 'gzip':
227 # Handle gzip decompression
229 f
= gzip
.GzipFile(fileobj
=buf
)
231 if "json" == self
.format
:
232 res
= json
.loads(data
.decode('utf8'))
233 return wrap_response(res
, handle
.headers
)
235 return wrap_response(
236 data
.decode('utf8'), handle
.headers
)
237 except urllib_error
.HTTPError
as e
:
241 raise TwitterHTTPError(e
, uri
, self
.format
, arg_data
)
243 class Twitter(TwitterCall
):
245 The minimalist yet fully featured Twitter API class.
247 Get RESTful data by accessing members of this class. The result
248 is decoded python objects (lists and dicts).
250 The Twitter API is documented at:
252 http://dev.twitter.com/doc
258 auth=OAuth(token, token_key, con_secret, con_secret_key)))
260 # Get your "home" timeline
261 t.statuses.home_timeline()
263 # Get a particular friend's timeline
264 t.statuses.friends_timeline(id="billybob")
266 # Also supported (but totally weird)
267 t.statuses.friends_timeline.billybob()
271 status="Using @sixohsix's sweet Python Twitter Tools.")
273 # Send a direct message
274 t.direct_messages.new(
276 text="I think yer swell!")
278 # Get the members of tamtar's list "Things That Are Rad"
279 t._("tamtar")._("things-that-are-rad").members()
281 # Note how the magic `_` method can be used to insert data
282 # into the middle of a call. You can also use replacement:
283 t.user.list.members(user="tamtar", list="things-that-are-rad")
285 # An *optional* `_timeout` parameter can also be used for API
286 # calls which take much more time than normal or twitter stops
287 # responding for some reasone
289 screen_name=','.join(A_LIST_OF_100_SCREEN_NAMES), \
296 # Search for the latest tweets about #pycon
297 t.search.tweets(q="#pycon")
300 Using the data returned
301 -----------------------
303 Twitter API calls return decoded JSON. This is converted into
304 a bunch of Python lists, dicts, ints, and strings. For example::
306 x = twitter.statuses.home_timeline()
308 # The first 'tweet' in the timeline
311 # The screen name of the user who wrote the first 'tweet'
312 x[0]['user']['screen_name']
318 If you prefer to get your Twitter data in XML format, pass
319 format="xml" to the Twitter object when you instantiate it::
321 twitter = Twitter(format="xml")
323 The output will not be parsed in any way. It will be a raw string
329 domain
="api.twitter.com", secure
=True, auth
=None,
330 api_version
=_DEFAULT
):
332 Create a new twitter API connector.
334 Pass an `auth` parameter to use the credentials of a specific
335 user. Generally you'll want to pass an `OAuth`
338 twitter = Twitter(auth=OAuth(
339 token, token_secret, consumer_key, consumer_secret))
342 `domain` lets you change the domain you are connecting. By
343 default it's `api.twitter.com` but `search.twitter.com` may be
346 If `secure` is False you will connect with HTTP instead of
349 `api_version` is used to set the base uri. By default it's
350 '1'. If you are using "search.twitter.com" set this to None.
355 if (format
not in ("json", "xml", "")):
356 raise ValueError("Unknown data format '%s'" %(format))
358 if api_version
is _DEFAULT
:
363 uriparts
+= (str(api_version
),)
365 TwitterCall
.__init
__(
366 self
, auth
=auth
, format
=format
, domain
=domain
,
367 callable_cls
=TwitterCall
,
368 secure
=secure
, uriparts
=uriparts
)
371 __all__
= ["Twitter", "TwitterError", "TwitterHTTPError", "TwitterResponse"]