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 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.
37 class TwitterHTTPError(TwitterError
):
39 Exception thrown by the Twitter object when there is an
40 HTTP error interacting with twitter.com.
42 def __init__(self
, e
, uri
, format
, uriparts
):
46 self
.uriparts
= uriparts
48 data
= self
.e
.fp
.read()
49 except http_client
.IncompleteRead
as e
:
50 # can't read the error text
51 # let's try some of it
53 if self
.e
.headers
.get('Content-Encoding') == 'gzip':
55 f
= gzip
.GzipFile(fileobj
=buf
)
56 self
.response_data
= f
.read()
58 self
.response_data
= data
61 fmt
= ("." + self
.format
) if self
.format
else ""
63 "Twitter sent status %i for URL: %s%s using parameters: "
64 "(%s)\ndetails: %s" %(
65 self
.e
.code
, self
.uri
, fmt
, self
.uriparts
,
68 class TwitterResponse(object):
70 Response from a twitter request. Behaves like a list or a string
71 (depending on requested format) but it has a few other interesting
74 `headers` gives you access to the response headers as an
75 httplib.HTTPHeaders instance. You can do
76 `response.headers.get('h')` to retrieve a header.
78 def __init__(self
, headers
):
79 self
.headers
= headers
82 def rate_limit_remaining(self
):
84 Remaining requests in the current rate-limit.
86 return int(self
.headers
.get('X-Rate-Limit-Remaining', "0"))
89 def rate_limit_limit(self
):
91 The rate limit ceiling for that given request.
93 return int(self
.headers
.get('X-Rate-Limit-Limit', "0"))
96 def rate_limit_reset(self
):
98 Time in UTC epoch seconds when the rate limit will reset.
100 return int(self
.headers
.get('X-Rate-Limit-Reset', "0"))
103 def wrap_response(response
, headers
):
104 response_typ
= type(response
)
105 if response_typ
is bool:
106 # HURF DURF MY NAME IS PYTHON AND I CAN'T SUBCLASS bool.
108 elif response_typ
is str:
111 class WrappedTwitterResponse(response_typ
, TwitterResponse
):
112 __doc__
= TwitterResponse
.__doc
__
114 def __init__(self
, response
, headers
):
115 response_typ
.__init
__(self
, response
)
116 TwitterResponse
.__init
__(self
, headers
)
117 def __new__(cls
, response
, headers
):
118 return response_typ
.__new
__(cls
, response
)
120 return WrappedTwitterResponse(response
, headers
)
124 class TwitterCall(object):
127 self
, auth
, format
, domain
, callable_cls
, uri
="",
128 uriparts
=None, secure
=True, timeout
=None):
132 self
.callable_cls
= callable_cls
134 self
.uriparts
= uriparts
136 self
.timeout
= timeout
138 def __getattr__(self
, k
):
140 return object.__getattr
__(self
, k
)
141 except AttributeError:
142 def extend_call(arg
):
143 return self
.callable_cls(
144 auth
=self
.auth
, format
=self
.format
, domain
=self
.domain
,
145 callable_cls
=self
.callable_cls
, timeout
=self
.timeout
, uriparts
=self
.uriparts \
151 return extend_call(k
)
153 def __call__(self
, **kwargs
):
156 for uripart
in self
.uriparts
:
157 # If this part matches a keyword argument, use the
158 # supplied value otherwise, just use the part.
159 uriparts
.append(str(kwargs
.pop(uripart
, uripart
)))
160 uri
= '/'.join(uriparts
)
162 method
= kwargs
.pop('_method', None)
165 for action
in POST_ACTIONS
:
166 if re
.search("%s(/\d+)?$" % action
, uri
):
170 # If an id kwarg is present and there is no id to fill in in
171 # the list of uriparts, assume the id goes at the end.
172 id = kwargs
.pop('id', None)
176 # If an _id kwarg is present, this is treated as id as a CGI
178 _id
= kwargs
.pop('_id', None)
182 # If an _timeout is specified in kwargs, use it
183 _timeout
= kwargs
.pop('_timeout', None)
191 uriBase
= "http%s://%s/%s%s%s" %(
192 secure_str
, self
.domain
, uri
, dot
, self
.format
)
194 headers
= {'Accept-Encoding': 'gzip'}
196 headers
.update(self
.auth
.generate_headers())
197 arg_data
= self
.auth
.encode_params(uriBase
, method
, kwargs
)
199 uriBase
+= '?' + arg_data
202 body
= arg_data
.encode('utf8')
204 req
= urllib_request
.Request(uriBase
, body
, headers
)
205 return self
._handle
_response
(req
, uri
, arg_data
, _timeout
)
207 def _handle_response(self
, req
, uri
, arg_data
, _timeout
=None):
210 kwargs
['timeout'] = _timeout
212 handle
= urllib_request
.urlopen(req
, **kwargs
)
213 if handle
.headers
['Content-Type'] in ['image/jpeg', 'image/png']:
217 except http_client
.IncompleteRead
as e
:
218 # Even if we don't get all the bytes we should have there
219 # may be a complete response in e.partial
221 if handle
.info().get('Content-Encoding') == 'gzip':
222 # Handle gzip decompression
224 f
= gzip
.GzipFile(fileobj
=buf
)
226 if "json" == self
.format
:
227 res
= json
.loads(data
.decode('utf8'))
228 return wrap_response(res
, handle
.headers
)
230 return wrap_response(
231 data
.decode('utf8'), handle
.headers
)
232 except urllib_error
.HTTPError
as e
:
236 raise TwitterHTTPError(e
, uri
, self
.format
, arg_data
)
238 class Twitter(TwitterCall
):
240 The minimalist yet fully featured Twitter API class.
242 Get RESTful data by accessing members of this class. The result
243 is decoded python objects (lists and dicts).
245 The Twitter API is documented at:
247 http://dev.twitter.com/doc
253 auth=OAuth(token, token_key, con_secret, con_secret_key)))
255 # Get your "home" timeline
256 t.statuses.home_timeline()
258 # Get a particular friend's timeline
259 t.statuses.friends_timeline(id="billybob")
261 # Also supported (but totally weird)
262 t.statuses.friends_timeline.billybob()
266 status="Using @sixohsix's sweet Python Twitter Tools.")
268 # Send a direct message
269 t.direct_messages.new(
271 text="I think yer swell!")
273 # Get the members of tamtar's list "Things That Are Rad"
274 t._("tamtar")._("things-that-are-rad").members()
276 # Note how the magic `_` method can be used to insert data
277 # into the middle of a call. You can also use replacement:
278 t.user.list.members(user="tamtar", list="things-that-are-rad")
280 # An *optional* `_timeout` parameter can also be used for API
281 # calls which take much more time than normal or twitter stops
282 # responding for some reasone
284 screen_name=','.join(A_LIST_OF_100_SCREEN_NAMES), \
291 # Search for the latest tweets about #pycon
292 t.search.tweets(q="#pycon")
295 Using the data returned
296 -----------------------
298 Twitter API calls return decoded JSON. This is converted into
299 a bunch of Python lists, dicts, ints, and strings. For example::
301 x = twitter.statuses.home_timeline()
303 # The first 'tweet' in the timeline
306 # The screen name of the user who wrote the first 'tweet'
307 x[0]['user']['screen_name']
313 If you prefer to get your Twitter data in XML format, pass
314 format="xml" to the Twitter object when you instantiate it::
316 twitter = Twitter(format="xml")
318 The output will not be parsed in any way. It will be a raw string
324 domain
="api.twitter.com", secure
=True, auth
=None,
325 api_version
=_DEFAULT
):
327 Create a new twitter API connector.
329 Pass an `auth` parameter to use the credentials of a specific
330 user. Generally you'll want to pass an `OAuth`
333 twitter = Twitter(auth=OAuth(
334 token, token_secret, consumer_key, consumer_secret))
337 `domain` lets you change the domain you are connecting. By
338 default it's `api.twitter.com` but `search.twitter.com` may be
341 If `secure` is False you will connect with HTTP instead of
344 `api_version` is used to set the base uri. By default it's
345 '1'. If you are using "search.twitter.com" set this to None.
350 if (format
not in ("json", "xml", "")):
351 raise ValueError("Unknown data format '%s'" %(format))
353 if api_version
is _DEFAULT
:
358 uriparts
+= (str(api_version
),)
360 TwitterCall
.__init
__(
361 self
, auth
=auth
, format
=format
, domain
=domain
,
362 callable_cls
=TwitterCall
,
363 secure
=secure
, uriparts
=uriparts
)
366 __all__
= ["Twitter", "TwitterError", "TwitterHTTPError", "TwitterResponse"]