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
26 class _DEFAULT(object):
30 class TwitterError(Exception):
32 Base Exception thrown by the Twitter object when there is a
33 general error interacting with the API.
38 class TwitterHTTPError(TwitterError
):
40 Exception thrown by the Twitter object when there is an
41 HTTP error interacting with twitter.com.
43 def __init__(self
, e
, uri
, format
, uriparts
):
47 self
.uriparts
= uriparts
48 if self
.e
.headers
['Content-Encoding'] == 'gzip':
49 buf
= StringIO(self
.e
.fp
.read())
50 f
= gzip
.GzipFile(fileobj
=buf
)
51 self
.response_data
= f
.read()
53 self
.response_data
= self
.e
.fp
.read()
56 fmt
= ("." + self
.format
) if self
.format
else ""
58 "Twitter sent status %i for URL: %s%s using parameters: "
59 "(%s)\ndetails: %s" % (
60 self
.e
.code
, self
.uri
, fmt
, self
.uriparts
,
64 class TwitterResponse(object):
66 Response from a twitter request. Behaves like a list or a string
67 (depending on requested format) but it has a few other interesting
70 `headers` gives you access to the response headers as an
71 httplib.HTTPHeaders instance. You can do
72 `response.headers.get('h')` to retrieve a header.
74 def __init__(self
, headers
):
75 self
.headers
= headers
78 def rate_limit_remaining(self
):
80 Remaining requests in the current rate-limit.
82 return int(self
.headers
.get('X-Rate-Limit-Remaining', "0"))
85 def rate_limit_limit(self
):
87 The rate limit ceiling for that given request.
89 return int(self
.headers
.get('X-Rate-Limit-Limit', "0"))
92 def rate_limit_reset(self
):
94 Time in UTC epoch seconds when the rate limit will reset.
96 return int(self
.headers
.get('X-Rate-Limit-Reset', "0"))
99 def wrap_response(response
, headers
):
100 response_typ
= type(response
)
101 if response_typ
is bool:
102 # HURF DURF MY NAME IS PYTHON AND I CAN'T SUBCLASS bool.
105 class WrappedTwitterResponse(response_typ
, TwitterResponse
):
106 __doc__
= TwitterResponse
.__doc
__
108 def __init__(self
, response
, headers
):
109 response_typ
.__init
__(self
, response
)
110 TwitterResponse
.__init
__(self
, headers
)
112 def __new__(cls
, response
, headers
):
113 return response_typ
.__new
__(cls
, response
)
115 return WrappedTwitterResponse(response
, headers
)
118 class TwitterCall(object):
121 self
, auth
, format
, domain
, callable_cls
, uri
="",
122 uriparts
=None, secure
=True):
126 self
.callable_cls
= callable_cls
128 self
.uriparts
= uriparts
131 def __getattr__(self
, k
):
133 return object.__getattr
__(self
, k
)
134 except AttributeError:
135 def extend_call(arg
):
136 return self
.callable_cls(
137 auth
=self
.auth
, format
=self
.format
, domain
=self
.domain
,
138 callable_cls
=self
.callable_cls
, uriparts
=self
.uriparts
144 return extend_call(k
)
146 def __call__(self
, **kwargs
):
149 for uripart
in self
.uriparts
:
150 # If this part matches a keyword argument, use the
151 # supplied value otherwise, just use the part.
152 uriparts
.append(str(kwargs
.pop(uripart
, uripart
)))
153 uri
= '/'.join(uriparts
)
155 method
= kwargs
.pop('_method', None)
158 for action
in POST_ACTIONS
:
159 if re
.search("%s(/\d+)?$" % action
, uri
):
163 # If an id kwarg is present and there is no id to fill in in
164 # the list of uriparts, assume the id goes at the end.
165 id = kwargs
.pop('id', None)
169 # If an _id kwarg is present, this is treated as id as a CGI
171 _id
= kwargs
.pop('_id', None)
175 # If an _timeout is specified in kwargs, use it
176 _timeout
= kwargs
.pop('_timeout', None)
184 uriBase
= "http%s://%s/%s%s%s" % (
185 secure_str
, self
.domain
, uri
, dot
, self
.format
)
187 headers
= {'Accept-Encoding': 'gzip'}
189 headers
.update(self
.auth
.generate_headers())
190 arg_data
= self
.auth
.encode_params(uriBase
, method
, kwargs
)
192 uriBase
+= '?' + arg_data
195 body
= arg_data
.encode('utf8')
197 req
= urllib_request
.Request(uriBase
, body
, headers
)
198 return self
._handle
_response
(req
, uri
, arg_data
, _timeout
)
200 def _handle_response(self
, req
, uri
, arg_data
, _timeout
=None):
203 kwargs
['timeout'] = _timeout
205 handle
= urllib_request
.urlopen(req
, **kwargs
)
206 if handle
.headers
['Content-Type'] in ['image/jpeg', 'image/png']:
210 except httplib
.IncompleteRead
as e
:
211 # Even if we don't get all the bytes we should have there
212 # may be a complete response in e.partial
214 if handle
.info().get('Content-Encoding') == 'gzip':
215 # Handle gzip decompression
217 f
= gzip
.GzipFile(fileobj
=buf
)
219 if "json" == self
.format
:
220 res
= json
.loads(data
.decode('utf8'))
221 return wrap_response(res
, handle
.headers
)
223 return wrap_response(
224 data
.decode('utf8'), handle
.headers
)
225 except urllib_error
.HTTPError
as e
:
229 raise TwitterHTTPError(e
, uri
, self
.format
, arg_data
)
232 class Twitter(TwitterCall
):
234 The minimalist yet fully featured Twitter API class.
236 Get RESTful data by accessing members of this class. The result
237 is decoded python objects (lists and dicts).
239 The Twitter API is documented at:
241 http://dev.twitter.com/doc
247 auth=OAuth(token, token_key, con_secret, con_secret_key)))
249 # Get your "home" timeline
250 t.statuses.home_timeline()
252 # Get a particular friend's timeline
253 t.statuses.friends_timeline(id="billybob")
255 # Also supported (but totally weird)
256 t.statuses.friends_timeline.billybob()
260 status="Using @sixohsix's sweet Python Twitter Tools.")
262 # Send a direct message
263 t.direct_messages.new(
265 text="I think yer swell!")
267 # Get the members of tamtar's list "Things That Are Rad"
268 t._("tamtar")._("things-that-are-rad").members()
270 # Note how the magic `_` method can be used to insert data
271 # into the middle of a call. You can also use replacement:
272 t.user.list.members(user="tamtar", list="things-that-are-rad")
274 # An *optional* `_timeout` parameter can also be used for API
275 # calls which take much more time than normal or twitter stops
276 # responding for some reasone
278 screen_name=','.join(A_LIST_OF_100_SCREEN_NAMES), \
285 # Search for the latest tweets about #pycon
286 t.search.tweets(q="#pycon")
289 Using the data returned
290 -----------------------
292 Twitter API calls return decoded JSON. This is converted into
293 a bunch of Python lists, dicts, ints, and strings. For example::
295 x = twitter.statuses.home_timeline()
297 # The first 'tweet' in the timeline
300 # The screen name of the user who wrote the first 'tweet'
301 x[0]['user']['screen_name']
307 If you prefer to get your Twitter data in XML format, pass
308 format="xml" to the Twitter object when you instantiate it::
310 twitter = Twitter(format="xml")
312 The output will not be parsed in any way. It will be a raw string
318 domain
="api.twitter.com", secure
=True, auth
=None,
319 api_version
=_DEFAULT
):
321 Create a new twitter API connector.
323 Pass an `auth` parameter to use the credentials of a specific
324 user. Generally you'll want to pass an `OAuth`
327 twitter = Twitter(auth=OAuth(
328 token, token_secret, consumer_key, consumer_secret))
331 `domain` lets you change the domain you are connecting. By
332 default it's `api.twitter.com`.
334 If `secure` is False you will connect with HTTP instead of
337 `api_version` is used to set the base uri. By default it's
343 if (format
not in ("json", "xml", "")):
344 raise ValueError("Unknown data format '%s'" % (format
))
346 if api_version
is _DEFAULT
:
347 if domain
== 'api.twitter.com':
354 uriparts
+= (str(api_version
),)
356 TwitterCall
.__init
__(
357 self
, auth
=auth
, format
=format
, domain
=domain
,
358 callable_cls
=TwitterCall
,
359 secure
=secure
, uriparts
=uriparts
)
362 __all__
= ["Twitter", "TwitterError", "TwitterHTTPError", "TwitterResponse"]