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
45 if self
.e
.headers
['Content-Encoding'] == 'gzip':
46 buf
= StringIO(self
.e
.fp
.read())
47 f
= gzip
.GzipFile(fileobj
=buf
)
48 self
.response_data
= f
.read()
50 self
.response_data
= self
.e
.fp
.read()
53 fmt
= ("." + self
.format
) if self
.format
else ""
55 "Twitter sent status %i for URL: %s%s using parameters: "
56 "(%s)\ndetails: %s" %(
57 self
.e
.code
, self
.uri
, fmt
, self
.uriparts
,
60 class TwitterResponse(object):
62 Response from a twitter request. Behaves like a list or a string
63 (depending on requested format) but it has a few other interesting
66 `headers` gives you access to the response headers as an
67 httplib.HTTPHeaders instance. You can do
68 `response.headers.get('h')` to retrieve a header.
70 def __init__(self
, headers
):
71 self
.headers
= headers
74 def rate_limit_remaining(self
):
76 Remaining requests in the current rate-limit.
78 return int(self
.headers
.get('X-Rate-Limit-Remaining', "0"))
81 def rate_limit_limit(self
):
83 The rate limit ceiling for that given request.
85 return int(self
.headers
.get('X-Rate-Limit-Limit', "0"))
88 def rate_limit_reset(self
):
90 Time in UTC epoch seconds when the rate limit will reset.
92 return int(self
.headers
.get('X-Rate-Limit-Reset', "0"))
95 def wrap_response(response
, headers
):
96 response_typ
= type(response
)
97 if response_typ
is bool:
98 # HURF DURF MY NAME IS PYTHON AND I CAN'T SUBCLASS bool.
101 class WrappedTwitterResponse(response_typ
, TwitterResponse
):
102 __doc__
= TwitterResponse
.__doc
__
104 def __init__(self
, response
, headers
):
105 response_typ
.__init
__(self
, response
)
106 TwitterResponse
.__init
__(self
, headers
)
107 def __new__(cls
, response
, headers
):
108 return response_typ
.__new
__(cls
, response
)
111 return WrappedTwitterResponse(response
, headers
)
115 class TwitterCall(object):
118 self
, auth
, format
, domain
, callable_cls
, uri
="",
119 uriparts
=None, secure
=True):
123 self
.callable_cls
= callable_cls
125 self
.uriparts
= uriparts
128 def __getattr__(self
, k
):
130 return object.__getattr
__(self
, k
)
131 except AttributeError:
132 def extend_call(arg
):
133 return self
.callable_cls(
134 auth
=self
.auth
, format
=self
.format
, domain
=self
.domain
,
135 callable_cls
=self
.callable_cls
, uriparts
=self
.uriparts \
141 return extend_call(k
)
143 def __call__(self
, **kwargs
):
146 for uripart
in self
.uriparts
:
147 # If this part matches a keyword argument, use the
148 # supplied value otherwise, just use the part.
149 uriparts
.append(str(kwargs
.pop(uripart
, uripart
)))
150 uri
= '/'.join(uriparts
)
152 method
= kwargs
.pop('_method', None)
155 for action
in POST_ACTIONS
:
156 if re
.search("%s(/\d+)?$" % action
, uri
):
160 # If an id kwarg is present and there is no id to fill in in
161 # the list of uriparts, assume the id goes at the end.
162 id = kwargs
.pop('id', None)
166 # If an _id kwarg is present, this is treated as id as a CGI
168 _id
= kwargs
.pop('_id', None)
172 # If an _timeout is specified in kwargs, use it
173 _timeout
= kwargs
.pop('_timeout', None)
181 uriBase
= "http%s://%s/%s%s%s" %(
182 secure_str
, self
.domain
, uri
, dot
, self
.format
)
184 headers
= {'Accept-Encoding': 'gzip'}
186 headers
.update(self
.auth
.generate_headers())
187 arg_data
= self
.auth
.encode_params(uriBase
, method
, kwargs
)
189 uriBase
+= '?' + arg_data
192 body
= arg_data
.encode('utf8')
194 req
= urllib_request
.Request(uriBase
, body
, headers
)
195 return self
._handle
_response
(req
, uri
, arg_data
, _timeout
)
197 def _handle_response(self
, req
, uri
, arg_data
, _timeout
=None):
200 kwargs
['timeout'] = _timeout
202 handle
= urllib_request
.urlopen(req
, **kwargs
)
203 if handle
.headers
['Content-Type'] in ['image/jpeg', 'image/png']:
207 except httplib
.IncompleteRead
, e
:
208 # Even if we don't get all the bytes we should have there
209 # may be a complete response in e.partial
211 if handle
.info().get('Content-Encoding') == 'gzip':
212 # Handle gzip decompression
214 f
= gzip
.GzipFile(fileobj
=buf
)
216 if "json" == self
.format
:
217 res
= json
.loads(data
.decode('utf8'))
218 return wrap_response(res
, handle
.headers
)
220 return wrap_response(
221 data
.decode('utf8'), handle
.headers
)
222 except urllib_error
.HTTPError
as e
:
226 raise TwitterHTTPError(e
, uri
, self
.format
, arg_data
)
228 class Twitter(TwitterCall
):
230 The minimalist yet fully featured Twitter API class.
232 Get RESTful data by accessing members of this class. The result
233 is decoded python objects (lists and dicts).
235 The Twitter API is documented at:
237 http://dev.twitter.com/doc
243 auth=OAuth(token, token_key, con_secret, con_secret_key)))
245 # Get your "home" timeline
246 t.statuses.home_timeline()
248 # Get a particular friend's timeline
249 t.statuses.friends_timeline(id="billybob")
251 # Also supported (but totally weird)
252 t.statuses.friends_timeline.billybob()
256 status="Using @sixohsix's sweet Python Twitter Tools.")
258 # Send a direct message
259 t.direct_messages.new(
261 text="I think yer swell!")
263 # Get the members of tamtar's list "Things That Are Rad"
264 t._("tamtar")._("things-that-are-rad").members()
266 # Note how the magic `_` method can be used to insert data
267 # into the middle of a call. You can also use replacement:
268 t.user.list.members(user="tamtar", list="things-that-are-rad")
270 # An *optional* `_timeout` parameter can also be used for API
271 # calls which take much more time than normal or twitter stops
272 # responding for some reasone
274 screen_name=','.join(A_LIST_OF_100_SCREEN_NAMES), \
281 # Search for the latest tweets about #pycon
282 t.search.tweets(q="#pycon")
285 Using the data returned
286 -----------------------
288 Twitter API calls return decoded JSON. This is converted into
289 a bunch of Python lists, dicts, ints, and strings. For example::
291 x = twitter.statuses.home_timeline()
293 # The first 'tweet' in the timeline
296 # The screen name of the user who wrote the first 'tweet'
297 x[0]['user']['screen_name']
303 If you prefer to get your Twitter data in XML format, pass
304 format="xml" to the Twitter object when you instantiate it::
306 twitter = Twitter(format="xml")
308 The output will not be parsed in any way. It will be a raw string
314 domain
="api.twitter.com", secure
=True, auth
=None,
315 api_version
=_DEFAULT
):
317 Create a new twitter API connector.
319 Pass an `auth` parameter to use the credentials of a specific
320 user. Generally you'll want to pass an `OAuth`
323 twitter = Twitter(auth=OAuth(
324 token, token_secret, consumer_key, consumer_secret))
327 `domain` lets you change the domain you are connecting. By
328 default it's `api.twitter.com` but `search.twitter.com` may be
331 If `secure` is False you will connect with HTTP instead of
334 `api_version` is used to set the base uri. By default it's
335 '1'. If you are using "search.twitter.com" set this to None.
340 if (format
not in ("json", "xml", "")):
341 raise ValueError("Unknown data format '%s'" %(format))
343 if api_version
is _DEFAULT
:
344 if domain
== 'api.twitter.com':
351 uriparts
+= (str(api_version
),)
353 TwitterCall
.__init
__(
354 self
, auth
=auth
, format
=format
, domain
=domain
,
355 callable_cls
=TwitterCall
,
356 secure
=secure
, uriparts
=uriparts
)
359 __all__
= ["Twitter", "TwitterError", "TwitterHTTPError", "TwitterResponse"]