2 from __future__
import unicode_literals
5 import urllib
.request
as urllib_request
6 import urllib
.error
as urllib_error
8 import urllib2
as urllib_request
9 import urllib2
as urllib_error
12 from cStringIO
import StringIO
14 from io
import BytesIO
as StringIO
16 from .twitter_globals
import POST_ACTIONS
17 from .auth
import NoAuth
24 import http
.client
as http_client
26 import httplib
as http_client
31 import simplejson
as json
34 class _DEFAULT(object):
38 class TwitterError(Exception):
40 Base Exception thrown by the Twitter object when there is a
41 general error interacting with the API.
46 class TwitterHTTPError(TwitterError
):
48 Exception thrown by the Twitter object when there is an
49 HTTP error interacting with twitter.com.
51 def __init__(self
, e
, uri
, format
, uriparts
):
55 self
.uriparts
= uriparts
57 data
= self
.e
.fp
.read()
58 except http_client
.IncompleteRead
as e
:
59 # can't read the error text
60 # let's try some of it
62 if self
.e
.headers
.get('Content-Encoding') == 'gzip':
64 f
= gzip
.GzipFile(fileobj
=buf
)
65 self
.response_data
= f
.read()
67 self
.response_data
= data
68 super(TwitterHTTPError
, self
).__init
__(str(self
))
71 fmt
= ("." + self
.format
) if self
.format
else ""
73 "Twitter sent status %i for URL: %s%s using parameters: "
74 "(%s)\ndetails: %s" % (
75 self
.e
.code
, self
.uri
, fmt
, self
.uriparts
,
79 class TwitterResponse(object):
81 Response from a twitter request. Behaves like a list or a string
82 (depending on requested format) but it has a few other interesting
85 `headers` gives you access to the response headers as an
86 httplib.HTTPHeaders instance. You can do
87 `response.headers.get('h')` to retrieve a header.
91 def rate_limit_remaining(self
):
93 Remaining requests in the current rate-limit.
95 return int(self
.headers
.get('X-Rate-Limit-Remaining', "0"))
98 def rate_limit_limit(self
):
100 The rate limit ceiling for that given request.
102 return int(self
.headers
.get('X-Rate-Limit-Limit', "0"))
105 def rate_limit_reset(self
):
107 Time in UTC epoch seconds when the rate limit will reset.
109 return int(self
.headers
.get('X-Rate-Limit-Reset', "0"))
112 class TwitterDictResponse(dict, TwitterResponse
):
116 class TwitterListResponse(list, TwitterResponse
):
120 def wrap_response(response
, headers
):
121 response_typ
= type(response
)
122 if response_typ
is dict:
123 res
= TwitterDictResponse(response
)
124 res
.headers
= headers
125 elif response_typ
is list:
126 res
= TwitterListResponse(response
)
127 res
.headers
= headers
133 POST_ACTIONS_RE
= re
.compile('(' + '|'.join(POST_ACTIONS
) + r
')(/\d+)?$')
135 def method_for_uri(uri
):
136 if POST_ACTIONS_RE
.search(uri
):
140 class TwitterCall(object):
143 self
, auth
, format
, domain
, callable_cls
, uri
="",
144 uriparts
=None, secure
=True, timeout
=None, gzip
=False):
148 self
.callable_cls
= callable_cls
150 self
.uriparts
= uriparts
152 self
.timeout
= timeout
155 def __getattr__(self
, k
):
157 return object.__getattr
__(self
, k
)
158 except AttributeError:
159 def extend_call(arg
):
160 return self
.callable_cls(
161 auth
=self
.auth
, format
=self
.format
, domain
=self
.domain
,
162 callable_cls
=self
.callable_cls
, timeout
=self
.timeout
,
163 secure
=self
.secure
, gzip
=self
.gzip
,
164 uriparts
=self
.uriparts
+ (arg
,))
168 return extend_call(k
)
170 def __call__(self
, **kwargs
):
173 for uripart
in self
.uriparts
:
174 # If this part matches a keyword argument, use the
175 # supplied value otherwise, just use the part.
176 uriparts
.append(str(kwargs
.pop(uripart
, uripart
)))
177 uri
= '/'.join(uriparts
)
179 method
= kwargs
.pop('_method', None) or method_for_uri(uri
)
181 # If an id kwarg is present and there is no id to fill in in
182 # the list of uriparts, assume the id goes at the end.
183 id = kwargs
.pop('id', None)
187 # If an _id kwarg is present, this is treated as id as a CGI
189 _id
= kwargs
.pop('_id', None)
193 # If an _timeout is specified in kwargs, use it
194 _timeout
= kwargs
.pop('_timeout', None)
202 uriBase
= "http%s://%s/%s%s%s" % (
203 secure_str
, self
.domain
, uri
, dot
, self
.format
)
205 # Check if argument tells whether img is already base64 encoded
207 if "_base64" in kwargs
:
208 b64_convert
= not kwargs
.pop("_base64")
212 # Catch media arguments to handle oauth query differently for multipart
214 for arg
in ['media[]']:
216 media
= kwargs
.pop(arg
)
218 media
= base64
.b64encode(media
)
219 if sys
.version_info
>= (3, 0):
220 media
= str(media
, 'utf8')
224 # Catch media arguments that are not accepted through multipart
225 # and are not yet base64 encoded
227 for arg
in ['banner', 'image']:
229 kwargs
[arg
] = base64
.b64encode(kwargs
[arg
])
231 headers
= {'Accept-Encoding': 'gzip'}
if self
.gzip
else dict()
235 headers
.update(self
.auth
.generate_headers())
236 # Use urlencoded oauth args with no params when sending media
237 # via multipart and send it directly via uri even for post
238 arg_data
= self
.auth
.encode_params(
239 uriBase
, method
, {} if media
else kwargs
)
240 if method
== 'GET' or media
:
241 uriBase
+= '?' + arg_data
243 body
= arg_data
.encode('utf8')
245 # Handle query as multipart when sending media
247 BOUNDARY
= "###Python-Twitter###"
249 bod
.append('--' + BOUNDARY
)
251 'Content-Disposition: form-data; name="%s"' % mediafield
)
252 bod
.append('Content-Transfer-Encoding: base64')
255 for k
, v
in kwargs
.items():
256 bod
.append('--' + BOUNDARY
)
257 bod
.append('Content-Disposition: form-data; name="%s"' % k
)
259 if sys
.version_info
[:2] <= (2, 7):
261 v
= v
.decode("utf-8")
265 bod
.append('--' + BOUNDARY
+ '--')
266 body
= '\r\n'.join(bod
).encode('utf8')
267 headers
['Content-Type'] = \
268 'multipart/form-data; boundary=%s' % BOUNDARY
270 if sys
.version_info
[:2] <= (2, 7):
271 uriBase
= uriBase
.encode("utf-8")
273 headers
[k
.encode('utf-8')] = headers
.pop(k
)
275 req
= urllib_request
.Request(uriBase
, body
, headers
)
276 return self
._handle
_response
(req
, uri
, arg_data
, _timeout
)
278 def _handle_response(self
, req
, uri
, arg_data
, _timeout
=None):
281 kwargs
['timeout'] = _timeout
283 handle
= urllib_request
.urlopen(req
, **kwargs
)
284 if handle
.headers
['Content-Type'] in ['image/jpeg', 'image/png']:
288 except http_client
.IncompleteRead
as e
:
289 # Even if we don't get all the bytes we should have there
290 # may be a complete response in e.partial
292 if handle
.info().get('Content-Encoding') == 'gzip':
293 # Handle gzip decompression
295 f
= gzip
.GzipFile(fileobj
=buf
)
298 return wrap_response({}, handle
.headers
)
299 elif "json" == self
.format
:
300 res
= json
.loads(data
.decode('utf8'))
301 return wrap_response(res
, handle
.headers
)
303 return wrap_response(
304 data
.decode('utf8'), handle
.headers
)
305 except urllib_error
.HTTPError
as e
:
309 raise TwitterHTTPError(e
, uri
, self
.format
, arg_data
)
312 class Twitter(TwitterCall
):
314 The minimalist yet fully featured Twitter API class.
316 Get RESTful data by accessing members of this class. The result
317 is decoded python objects (lists and dicts).
319 The Twitter API is documented at:
321 http://dev.twitter.com/doc
326 from twitter import *
329 auth=OAuth(token, token_key, con_secret, con_secret_key)))
331 # Get your "home" timeline
332 t.statuses.home_timeline()
334 # Get a particular friend's timeline
335 t.statuses.user_timeline(screen_name="billybob")
337 # to pass in GET/POST parameters, such as `count`
338 t.statuses.home_timeline(count=5)
340 # to pass in the GET/POST parameter `id` you need to use `_id`
341 t.statuses.oembed(_id=1234567890)
345 status="Using @sixohsix's sweet Python Twitter Tools.")
347 # Send a direct message
348 t.direct_messages.new(
350 text="I think yer swell!")
352 # Get the members of tamtar's list "Things That Are Rad"
353 t._("tamtar")._("things-that-are-rad").members()
355 # Note how the magic `_` method can be used to insert data
356 # into the middle of a call. You can also use replacement:
357 t.user.list.members(user="tamtar", list="things-that-are-rad")
359 # An *optional* `_timeout` parameter can also be used for API
360 # calls which take much more time than normal or twitter stops
361 # responding for some reason:
363 screen_name=','.join(A_LIST_OF_100_SCREEN_NAMES), \
366 # Overriding Method: GET/POST
367 # you should not need to use this method as this library properly
368 # detects whether GET or POST should be used, Nevertheless
369 # to force a particular method, use `_method`
370 t.statuses.oembed(_id=1234567890, _method='GET')
372 # Send a tweet with an image included (or set your banner or logo similarily)
373 # by just reading your image from the web or a file in a string:
375 with open("example.png", "rb") as imagefile:
376 params = {"media[]": imagefile.read(), "status": status}
377 t.statuses.update_with_media(**params)
379 # Or by sending a base64 encoded image:
380 params = {"media[]": base64_image, "status": status, "_base64": True}
381 t.statuses.update_with_media(**params)
386 # Search for the latest tweets about #pycon
387 t.search.tweets(q="#pycon")
390 Using the data returned
391 -----------------------
393 Twitter API calls return decoded JSON. This is converted into
394 a bunch of Python lists, dicts, ints, and strings. For example::
396 x = twitter.statuses.home_timeline()
398 # The first 'tweet' in the timeline
401 # The screen name of the user who wrote the first 'tweet'
402 x[0]['user']['screen_name']
408 If you prefer to get your Twitter data in XML format, pass
409 format="xml" to the Twitter object when you instantiate it::
411 twitter = Twitter(format="xml")
413 The output will not be parsed in any way. It will be a raw string
419 domain
="api.twitter.com", secure
=True, auth
=None,
420 api_version
=_DEFAULT
):
422 Create a new twitter API connector.
424 Pass an `auth` parameter to use the credentials of a specific
425 user. Generally you'll want to pass an `OAuth`
428 twitter = Twitter(auth=OAuth(
429 token, token_secret, consumer_key, consumer_secret))
432 `domain` lets you change the domain you are connecting. By
433 default it's `api.twitter.com`.
435 If `secure` is False you will connect with HTTP instead of
438 `api_version` is used to set the base uri. By default it's
444 if (format
not in ("json", "xml", "")):
445 raise ValueError("Unknown data format '%s'" % (format
))
447 if api_version
is _DEFAULT
:
452 uriparts
+= (str(api_version
),)
454 TwitterCall
.__init
__(
455 self
, auth
=auth
, format
=format
, domain
=domain
,
456 callable_cls
=TwitterCall
,
457 secure
=secure
, uriparts
=uriparts
)
460 __all__
= ["Twitter", "TwitterError", "TwitterHTTPError", "TwitterResponse"]