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
22 import simplejson
as json
24 class _DEFAULT(object):
27 class TwitterError(Exception):
29 Base Exception thrown by the Twitter object when there is a
30 general error interacting with the API.
34 class TwitterHTTPError(TwitterError
):
36 Exception thrown by the Twitter object when there is an
37 HTTP error interacting with twitter.com.
39 def __init__(self
, e
, uri
, format
, uriparts
):
43 self
.uriparts
= uriparts
44 if self
.e
.headers
['Content-Encoding'] == 'gzip':
45 buf
= StringIO(self
.e
.fp
.read())
46 f
= gzip
.GzipFile(fileobj
=buf
)
47 self
.response_data
= f
.read()
49 self
.response_data
= self
.e
.fp
.read()
52 fmt
= ("." + self
.format
) if self
.format
else ""
54 "Twitter sent status %i for URL: %s%s using parameters: "
55 "(%s)\ndetails: %s" %(
56 self
.e
.code
, self
.uri
, fmt
, self
.uriparts
,
59 class TwitterResponse(object):
61 Response from a twitter request. Behaves like a list or a string
62 (depending on requested format) but it has a few other interesting
65 `headers` gives you access to the response headers as an
66 httplib.HTTPHeaders instance. You can do
67 `response.headers.get('h')` to retrieve a header.
69 def __init__(self
, headers
):
70 self
.headers
= headers
73 def rate_limit_remaining(self
):
75 Remaining requests in the current rate-limit.
77 return int(self
.headers
.get('X-Rate-Limit-Remaining', "0"))
80 def rate_limit_limit(self
):
82 The rate limit ceiling for that given request.
84 return int(self
.headers
.get('X-Rate-Limit-Limit', "0"))
87 def rate_limit_reset(self
):
89 Time in UTC epoch seconds when the rate limit will reset.
91 return int(self
.headers
.get('X-Rate-Limit-Reset', "0"))
94 def wrap_response(response
, headers
):
95 response_typ
= type(response
)
96 if response_typ
is bool:
97 # HURF DURF MY NAME IS PYTHON AND I CAN'T SUBCLASS bool.
100 class WrappedTwitterResponse(response_typ
, TwitterResponse
):
101 __doc__
= TwitterResponse
.__doc
__
103 def __init__(self
, response
, headers
):
104 response_typ
.__init
__(self
, response
)
105 TwitterResponse
.__init
__(self
, headers
)
106 def __new__(cls
, response
, headers
):
107 return response_typ
.__new
__(cls
, response
)
110 return WrappedTwitterResponse(response
, headers
)
114 class TwitterCall(object):
117 self
, auth
, format
, domain
, callable_cls
, uri
="",
118 uriparts
=None, secure
=True):
122 self
.callable_cls
= callable_cls
124 self
.uriparts
= uriparts
127 def __getattr__(self
, k
):
129 return object.__getattr
__(self
, k
)
130 except AttributeError:
131 def extend_call(arg
):
132 return self
.callable_cls(
133 auth
=self
.auth
, format
=self
.format
, domain
=self
.domain
,
134 callable_cls
=self
.callable_cls
, uriparts
=self
.uriparts \
140 return extend_call(k
)
142 def __call__(self
, **kwargs
):
145 for uripart
in self
.uriparts
:
146 # If this part matches a keyword argument, use the
147 # supplied value otherwise, just use the part.
148 uriparts
.append(str(kwargs
.pop(uripart
, uripart
)))
149 uri
= '/'.join(uriparts
)
151 method
= kwargs
.pop('_method', None)
154 for action
in POST_ACTIONS
:
155 if re
.search("%s(/\d+)?$" % action
, uri
):
159 # If an id kwarg is present and there is no id to fill in in
160 # the list of uriparts, assume the id goes at the end.
161 id = kwargs
.pop('id', None)
165 # If an _id kwarg is present, this is treated as id as a CGI
167 _id
= kwargs
.pop('_id', None)
171 # If an _timeout is specified in kwargs, use it
172 _timeout
= kwargs
.pop('_timeout', None)
180 uriBase
= "http%s://%s/%s%s%s" %(
181 secure_str
, self
.domain
, uri
, dot
, self
.format
)
183 headers
= {'Accept-Encoding': 'gzip'}
185 headers
.update(self
.auth
.generate_headers())
186 arg_data
= self
.auth
.encode_params(uriBase
, method
, kwargs
)
188 uriBase
+= '?' + arg_data
191 body
= arg_data
.encode('utf8')
193 req
= urllib_request
.Request(uriBase
, body
, headers
)
194 return self
._handle
_response
(req
, uri
, arg_data
, _timeout
)
196 def _handle_response(self
, req
, uri
, arg_data
, _timeout
=None):
199 kwargs
['timeout'] = _timeout
201 handle
= urllib_request
.urlopen(req
, **kwargs
)
202 if handle
.headers
['Content-Type'] in ['image/jpeg', 'image/png']:
204 elif handle
.info().get('Content-Encoding') == 'gzip':
205 # Handle gzip decompression
206 buf
= StringIO(handle
.read())
207 f
= gzip
.GzipFile(fileobj
=buf
)
212 if "json" == self
.format
:
213 res
= json
.loads(data
.decode('utf8'))
214 return wrap_response(res
, handle
.headers
)
216 return wrap_response(
217 data
.decode('utf8'), handle
.headers
)
218 except urllib_error
.HTTPError
as e
:
222 raise TwitterHTTPError(e
, uri
, self
.format
, arg_data
)
224 class Twitter(TwitterCall
):
226 The minimalist yet fully featured Twitter API class.
228 Get RESTful data by accessing members of this class. The result
229 is decoded python objects (lists and dicts).
231 The Twitter API is documented at:
233 http://dev.twitter.com/doc
239 auth=OAuth(token, token_key, con_secret, con_secret_key)))
241 # Get your "home" timeline
242 t.statuses.home_timeline()
244 # Get a particular friend's timeline
245 t.statuses.friends_timeline(id="billybob")
247 # Also supported (but totally weird)
248 t.statuses.friends_timeline.billybob()
252 status="Using @sixohsix's sweet Python Twitter Tools.")
254 # Send a direct message
255 t.direct_messages.new(
257 text="I think yer swell!")
259 # Get the members of tamtar's list "Things That Are Rad"
260 t._("tamtar")._("things-that-are-rad").members()
262 # Note how the magic `_` method can be used to insert data
263 # into the middle of a call. You can also use replacement:
264 t.user.list.members(user="tamtar", list="things-that-are-rad")
266 # An *optional* `_timeout` parameter can also be used for API
267 # calls which take much more time than normal or twitter stops
268 # responding for some reasone
270 screen_name=','.join(A_LIST_OF_100_SCREEN_NAMES), \
277 # Search for the latest tweets about #pycon
278 t.search.tweets(q="#pycon")
281 Using the data returned
282 -----------------------
284 Twitter API calls return decoded JSON. This is converted into
285 a bunch of Python lists, dicts, ints, and strings. For example::
287 x = twitter.statuses.home_timeline()
289 # The first 'tweet' in the timeline
292 # The screen name of the user who wrote the first 'tweet'
293 x[0]['user']['screen_name']
299 If you prefer to get your Twitter data in XML format, pass
300 format="xml" to the Twitter object when you instantiate it::
302 twitter = Twitter(format="xml")
304 The output will not be parsed in any way. It will be a raw string
310 domain
="api.twitter.com", secure
=True, auth
=None,
311 api_version
=_DEFAULT
):
313 Create a new twitter API connector.
315 Pass an `auth` parameter to use the credentials of a specific
316 user. Generally you'll want to pass an `OAuth`
319 twitter = Twitter(auth=OAuth(
320 token, token_secret, consumer_key, consumer_secret))
323 `domain` lets you change the domain you are connecting. By
324 default it's `api.twitter.com` but `search.twitter.com` may be
327 If `secure` is False you will connect with HTTP instead of
330 `api_version` is used to set the base uri. By default it's
331 '1'. If you are using "search.twitter.com" set this to None.
336 if (format
not in ("json", "xml", "")):
337 raise ValueError("Unknown data format '%s'" %(format))
339 if api_version
is _DEFAULT
:
340 if domain
== 'api.twitter.com':
347 uriparts
+= (str(api_version
),)
349 TwitterCall
.__init
__(
350 self
, auth
=auth
, format
=format
, domain
=domain
,
351 callable_cls
=TwitterCall
,
352 secure
=secure
, uriparts
=uriparts
)
355 __all__
= ["Twitter", "TwitterError", "TwitterHTTPError", "TwitterResponse"]