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
.twitter_globals
import POST_ACTIONS
14 from twitter
.auth
import NoAuth
23 import simplejson
as json
25 class _DEFAULT(object):
28 class TwitterError(Exception):
30 Base Exception thrown by the Twitter object when there is a
31 general error interacting with the API.
35 class TwitterHTTPError(TwitterError
):
37 Exception thrown by the Twitter object when there is an
38 HTTP error interacting with twitter.com.
40 def __init__(self
, e
, uri
, format
, uriparts
):
44 self
.uriparts
= uriparts
46 data
= self
.e
.fp
.read()
47 except httplib
.IncompleteRead
, e
:
48 # can't read the error text
49 # let's try some of it
51 if self
.e
.headers
['Content-Encoding'] == 'gzip':
53 f
= gzip
.GzipFile(fileobj
=buf
)
54 self
.response_data
= f
.read()
56 self
.response_data
= data
59 fmt
= ("." + self
.format
) if self
.format
else ""
61 "Twitter sent status %i for URL: %s%s using parameters: "
62 "(%s)\ndetails: %s" %(
63 self
.e
.code
, self
.uri
, fmt
, self
.uriparts
,
66 class TwitterResponse(object):
68 Response from a twitter request. Behaves like a list or a string
69 (depending on requested format) but it has a few other interesting
72 `headers` gives you access to the response headers as an
73 httplib.HTTPHeaders instance. You can do
74 `response.headers.get('h')` to retrieve a header.
76 def __init__(self
, headers
):
77 self
.headers
= headers
80 def rate_limit_remaining(self
):
82 Remaining requests in the current rate-limit.
84 return int(self
.headers
.get('X-Rate-Limit-Remaining', "0"))
87 def rate_limit_limit(self
):
89 The rate limit ceiling for that given request.
91 return int(self
.headers
.get('X-Rate-Limit-Limit', "0"))
94 def rate_limit_reset(self
):
96 Time in UTC epoch seconds when the rate limit will reset.
98 return int(self
.headers
.get('X-Rate-Limit-Reset', "0"))
101 def wrap_response(response
, headers
):
102 response_typ
= type(response
)
103 if response_typ
is bool:
104 # HURF DURF MY NAME IS PYTHON AND I CAN'T SUBCLASS bool.
107 class WrappedTwitterResponse(response_typ
, TwitterResponse
):
108 __doc__
= TwitterResponse
.__doc
__
110 def __init__(self
, response
, headers
):
111 response_typ
.__init
__(self
, response
)
112 TwitterResponse
.__init
__(self
, headers
)
113 def __new__(cls
, response
, headers
):
114 return response_typ
.__new
__(cls
, response
)
117 return WrappedTwitterResponse(response
, headers
)
121 class TwitterCall(object):
124 self
, auth
, format
, domain
, callable_cls
, uri
="",
125 uriparts
=None, secure
=True):
129 self
.callable_cls
= callable_cls
131 self
.uriparts
= uriparts
134 def __getattr__(self
, k
):
136 return object.__getattr
__(self
, k
)
137 except AttributeError:
138 def extend_call(arg
):
139 return self
.callable_cls(
140 auth
=self
.auth
, format
=self
.format
, domain
=self
.domain
,
141 callable_cls
=self
.callable_cls
, uriparts
=self
.uriparts \
147 return extend_call(k
)
149 def __call__(self
, **kwargs
):
152 for uripart
in self
.uriparts
:
153 # If this part matches a keyword argument, use the
154 # supplied value otherwise, just use the part.
155 uriparts
.append(str(kwargs
.pop(uripart
, uripart
)))
156 uri
= '/'.join(uriparts
)
158 method
= kwargs
.pop('_method', None)
161 for action
in POST_ACTIONS
:
162 if re
.search("%s(/\d+)?$" % action
, uri
):
166 # If an id kwarg is present and there is no id to fill in in
167 # the list of uriparts, assume the id goes at the end.
168 id = kwargs
.pop('id', None)
172 # If an _id kwarg is present, this is treated as id as a CGI
174 _id
= kwargs
.pop('_id', None)
178 # If an _timeout is specified in kwargs, use it
179 _timeout
= kwargs
.pop('_timeout', None)
187 uriBase
= "http%s://%s/%s%s%s" %(
188 secure_str
, self
.domain
, uri
, dot
, self
.format
)
190 headers
= {'Accept-Encoding': 'gzip'}
192 headers
.update(self
.auth
.generate_headers())
193 arg_data
= self
.auth
.encode_params(uriBase
, method
, kwargs
)
195 uriBase
+= '?' + arg_data
198 body
= arg_data
.encode('utf8')
200 req
= urllib_request
.Request(uriBase
, body
, headers
)
201 return self
._handle
_response
(req
, uri
, arg_data
, _timeout
)
203 def _handle_response(self
, req
, uri
, arg_data
, _timeout
=None):
206 kwargs
['timeout'] = _timeout
208 handle
= urllib_request
.urlopen(req
, **kwargs
)
209 if handle
.headers
['Content-Type'] in ['image/jpeg', 'image/png']:
213 except httplib
.IncompleteRead
, e
:
214 # Even if we don't get all the bytes we should have there
215 # may be a complete response in e.partial
217 if handle
.info().get('Content-Encoding') == 'gzip':
218 # Handle gzip decompression
220 f
= gzip
.GzipFile(fileobj
=buf
)
222 if "json" == self
.format
:
223 res
= json
.loads(data
.decode('utf8'))
224 return wrap_response(res
, handle
.headers
)
226 return wrap_response(
227 data
.decode('utf8'), handle
.headers
)
228 except urllib_error
.HTTPError
as e
:
232 raise TwitterHTTPError(e
, uri
, self
.format
, arg_data
)
234 class Twitter(TwitterCall
):
236 The minimalist yet fully featured Twitter API class.
238 Get RESTful data by accessing members of this class. The result
239 is decoded python objects (lists and dicts).
241 The Twitter API is documented at:
243 http://dev.twitter.com/doc
249 auth=OAuth(token, token_key, con_secret, con_secret_key)))
251 # Get your "home" timeline
252 t.statuses.home_timeline()
254 # Get a particular friend's timeline
255 t.statuses.friends_timeline(id="billybob")
257 # Also supported (but totally weird)
258 t.statuses.friends_timeline.billybob()
262 status="Using @sixohsix's sweet Python Twitter Tools.")
264 # Send a direct message
265 t.direct_messages.new(
267 text="I think yer swell!")
269 # Get the members of tamtar's list "Things That Are Rad"
270 t._("tamtar")._("things-that-are-rad").members()
272 # Note how the magic `_` method can be used to insert data
273 # into the middle of a call. You can also use replacement:
274 t.user.list.members(user="tamtar", list="things-that-are-rad")
276 # An *optional* `_timeout` parameter can also be used for API
277 # calls which take much more time than normal or twitter stops
278 # responding for some reasone
280 screen_name=','.join(A_LIST_OF_100_SCREEN_NAMES), \
287 # Search for the latest tweets about #pycon
288 t.search.tweets(q="#pycon")
291 Using the data returned
292 -----------------------
294 Twitter API calls return decoded JSON. This is converted into
295 a bunch of Python lists, dicts, ints, and strings. For example::
297 x = twitter.statuses.home_timeline()
299 # The first 'tweet' in the timeline
302 # The screen name of the user who wrote the first 'tweet'
303 x[0]['user']['screen_name']
309 If you prefer to get your Twitter data in XML format, pass
310 format="xml" to the Twitter object when you instantiate it::
312 twitter = Twitter(format="xml")
314 The output will not be parsed in any way. It will be a raw string
320 domain
="api.twitter.com", secure
=True, auth
=None,
321 api_version
=_DEFAULT
):
323 Create a new twitter API connector.
325 Pass an `auth` parameter to use the credentials of a specific
326 user. Generally you'll want to pass an `OAuth`
329 twitter = Twitter(auth=OAuth(
330 token, token_secret, consumer_key, consumer_secret))
333 `domain` lets you change the domain you are connecting. By
334 default it's `api.twitter.com` but `search.twitter.com` may be
337 If `secure` is False you will connect with HTTP instead of
340 `api_version` is used to set the base uri. By default it's
341 '1'. If you are using "search.twitter.com" set this to None.
346 if (format
not in ("json", "xml", "")):
347 raise ValueError("Unknown data format '%s'" %(format))
349 if api_version
is _DEFAULT
:
350 if domain
== 'api.twitter.com':
357 uriparts
+= (str(api_version
),)
359 TwitterCall
.__init
__(
360 self
, auth
=auth
, format
=format
, domain
=domain
,
361 callable_cls
=TwitterCall
,
362 secure
=secure
, uriparts
=uriparts
)
365 __all__
= ["Twitter", "TwitterError", "TwitterHTTPError", "TwitterResponse"]