]> jfr.im git - z_archive/twitter.git/blob - twitter/api.py
add doc to send tweets with images in README + consolidate api.py/README
[z_archive/twitter.git] / twitter / api.py
1 # encoding: utf-8
2 from __future__ import unicode_literals
3
4 try:
5 import urllib.request as urllib_request
6 import urllib.error as urllib_error
7 except ImportError:
8 import urllib2 as urllib_request
9 import urllib2 as urllib_error
10
11 try:
12 from cStringIO import StringIO
13 except ImportError:
14 from io import BytesIO as StringIO
15
16 from .twitter_globals import POST_ACTIONS
17 from .auth import NoAuth
18
19 import re
20 import gzip
21
22 try:
23 import http.client as http_client
24 except ImportError:
25 import httplib as http_client
26
27 try:
28 import json
29 except ImportError:
30 import simplejson as json
31
32
33 class _DEFAULT(object):
34 pass
35
36
37 class TwitterError(Exception):
38 """
39 Base Exception thrown by the Twitter object when there is a
40 general error interacting with the API.
41 """
42 pass
43
44
45 class TwitterHTTPError(TwitterError):
46 """
47 Exception thrown by the Twitter object when there is an
48 HTTP error interacting with twitter.com.
49 """
50 def __init__(self, e, uri, format, uriparts):
51 self.e = e
52 self.uri = uri
53 self.format = format
54 self.uriparts = uriparts
55 try:
56 data = self.e.fp.read()
57 except http_client.IncompleteRead as e:
58 # can't read the error text
59 # let's try some of it
60 data = e.partial
61 if self.e.headers.get('Content-Encoding') == 'gzip':
62 buf = StringIO(data)
63 f = gzip.GzipFile(fileobj=buf)
64 self.response_data = f.read()
65 else:
66 self.response_data = data
67 super(TwitterHTTPError, self).__init__(str(self))
68
69 def __str__(self):
70 fmt = ("." + self.format) if self.format else ""
71 return (
72 "Twitter sent status %i for URL: %s%s using parameters: "
73 "(%s)\ndetails: %s" % (
74 self.e.code, self.uri, fmt, self.uriparts,
75 self.response_data))
76
77
78 class TwitterResponse(object):
79 """
80 Response from a twitter request. Behaves like a list or a string
81 (depending on requested format) but it has a few other interesting
82 attributes.
83
84 `headers` gives you access to the response headers as an
85 httplib.HTTPHeaders instance. You can do
86 `response.headers.get('h')` to retrieve a header.
87 """
88
89 @property
90 def rate_limit_remaining(self):
91 """
92 Remaining requests in the current rate-limit.
93 """
94 return int(self.headers.get('X-Rate-Limit-Remaining', "0"))
95
96 @property
97 def rate_limit_limit(self):
98 """
99 The rate limit ceiling for that given request.
100 """
101 return int(self.headers.get('X-Rate-Limit-Limit', "0"))
102
103 @property
104 def rate_limit_reset(self):
105 """
106 Time in UTC epoch seconds when the rate limit will reset.
107 """
108 return int(self.headers.get('X-Rate-Limit-Reset', "0"))
109
110
111 class TwitterDictResponse(dict, TwitterResponse):
112 pass
113
114
115 class TwitterListResponse(list, TwitterResponse):
116 pass
117
118
119 def wrap_response(response, headers):
120 response_typ = type(response)
121 if response_typ is dict:
122 res = TwitterDictResponse(response)
123 res.headers = headers
124 elif response_typ is list:
125 res = TwitterListResponse(response)
126 res.headers = headers
127 else:
128 res = response
129 return res
130
131
132 class TwitterCall(object):
133
134 def __init__(
135 self, auth, format, domain, callable_cls, uri="",
136 uriparts=None, secure=True, timeout=None, gzip=False):
137 self.auth = auth
138 self.format = format
139 self.domain = domain
140 self.callable_cls = callable_cls
141 self.uri = uri
142 self.uriparts = uriparts
143 self.secure = secure
144 self.timeout = timeout
145 self.gzip = gzip
146
147 def __getattr__(self, k):
148 try:
149 return object.__getattr__(self, k)
150 except AttributeError:
151 def extend_call(arg):
152 return self.callable_cls(
153 auth=self.auth, format=self.format, domain=self.domain,
154 callable_cls=self.callable_cls, timeout=self.timeout,
155 secure=self.secure, gzip=self.gzip,
156 uriparts=self.uriparts + (arg,))
157 if k == "_":
158 return extend_call
159 else:
160 return extend_call(k)
161
162 def __call__(self, **kwargs):
163 # Build the uri.
164 uriparts = []
165 for uripart in self.uriparts:
166 # If this part matches a keyword argument, use the
167 # supplied value otherwise, just use the part.
168 uriparts.append(str(kwargs.pop(uripart, uripart)))
169 uri = '/'.join(uriparts)
170
171 method = kwargs.pop('_method', None)
172 if not method:
173 method = "GET"
174 for action in POST_ACTIONS:
175 if re.search("%s(/\d+)?$" % action, uri):
176 method = "POST"
177 break
178
179 # If an id kwarg is present and there is no id to fill in in
180 # the list of uriparts, assume the id goes at the end.
181 id = kwargs.pop('id', None)
182 if id:
183 uri += "/%s" % (id)
184
185 # If an _id kwarg is present, this is treated as id as a CGI
186 # param.
187 _id = kwargs.pop('_id', None)
188 if _id:
189 kwargs['id'] = _id
190
191 # If an _timeout is specified in kwargs, use it
192 _timeout = kwargs.pop('_timeout', None)
193
194 secure_str = ''
195 if self.secure:
196 secure_str = 's'
197 dot = ""
198 if self.format:
199 dot = "."
200 uriBase = "http%s://%s/%s%s%s" % (
201 secure_str, self.domain, uri, dot, self.format)
202
203 # Catch media arguments to handle oauth query differently for multipart
204 media = None
205 for arg in ['media[]', 'banner', 'image']:
206 if arg in kwargs:
207 media = kwargs.pop(arg)
208 # Check if argument tells whether img is already base64 encoded
209 b64_convert = True
210 if "_base64" in kwargs:
211 b64_convert = not kwargs.pop("_base64")
212 if b64_convert:
213 import base64
214 media = base64.b64encode(media)
215 mediafield = arg
216 break
217
218 headers = {'Accept-Encoding': 'gzip'} if self.gzip else dict()
219 body = None
220 arg_data = None
221 if self.auth:
222 headers.update(self.auth.generate_headers())
223 # Use urlencoded oauth args with no params when sending media
224 # via multipart and send it directly via uri even for post
225 arg_data = self.auth.encode_params(
226 uriBase, method, {} if media else kwargs)
227 if method == 'GET' or media:
228 uriBase += '?' + arg_data
229 else:
230 body = arg_data.encode('utf8')
231
232 # Handle query as multipart when sending media
233 if media:
234 BOUNDARY = "###Python-Twitter###"
235 bod = []
236 bod.append('--' + BOUNDARY)
237 bod.append(
238 'Content-Disposition: form-data; name="%s"' % mediafield)
239 bod.append('Content-Transfer-Encoding: base64')
240 bod.append('')
241 bod.append(media)
242 for k, v in kwargs.items():
243 bod.append('--' + BOUNDARY)
244 bod.append('Content-Disposition: form-data; name="%s"' % k)
245 bod.append('')
246 bod.append(v)
247 bod.append('--' + BOUNDARY + '--')
248 body = '\r\n'.join(bod)
249 headers['Content-Type'] = \
250 'multipart/form-data; boundary=%s' % BOUNDARY
251
252 req = urllib_request.Request(uriBase, body, headers)
253 return self._handle_response(req, uri, arg_data, _timeout)
254
255 def _handle_response(self, req, uri, arg_data, _timeout=None):
256 kwargs = {}
257 if _timeout:
258 kwargs['timeout'] = _timeout
259 try:
260 handle = urllib_request.urlopen(req, **kwargs)
261 if handle.headers['Content-Type'] in ['image/jpeg', 'image/png']:
262 return handle
263 try:
264 data = handle.read()
265 except http_client.IncompleteRead as e:
266 # Even if we don't get all the bytes we should have there
267 # may be a complete response in e.partial
268 data = e.partial
269 if handle.info().get('Content-Encoding') == 'gzip':
270 # Handle gzip decompression
271 buf = StringIO(data)
272 f = gzip.GzipFile(fileobj=buf)
273 data = f.read()
274 if "json" == self.format:
275 res = json.loads(data.decode('utf8'))
276 return wrap_response(res, handle.headers)
277 else:
278 return wrap_response(
279 data.decode('utf8'), handle.headers)
280 except urllib_error.HTTPError as e:
281 if (e.code == 304):
282 return []
283 else:
284 raise TwitterHTTPError(e, uri, self.format, arg_data)
285
286
287 class Twitter(TwitterCall):
288 """
289 The minimalist yet fully featured Twitter API class.
290
291 Get RESTful data by accessing members of this class. The result
292 is decoded python objects (lists and dicts).
293
294 The Twitter API is documented at:
295
296 http://dev.twitter.com/doc
297
298
299 Examples::
300
301 t = Twitter(
302 auth=OAuth(token, token_key, con_secret, con_secret_key)))
303
304 # Get your "home" timeline
305 t.statuses.home_timeline()
306
307 # Get a particular friend's tweets
308 t.statuses.user_timeline(user_id="billybob")
309
310 # Update your status
311 t.statuses.update(
312 status="Using @sixohsix's sweet Python Twitter Tools.")
313
314 # Send a direct message
315 t.direct_messages.new(
316 user="billybob",
317 text="I think yer swell!")
318
319 # Get the members of tamtar's list "Things That Are Rad"
320 t._("tamtar")._("things-that-are-rad").members()
321
322 # Note how the magic `_` method can be used to insert data
323 # into the middle of a call. You can also use replacement:
324 t.user.list.members(user="tamtar", list="things-that-are-rad")
325
326 # An *optional* `_timeout` parameter can also be used for API
327 # calls which take much more time than normal or twitter stops
328 # responding for some reasone
329 t.users.lookup(
330 screen_name=','.join(A_LIST_OF_100_SCREEN_NAMES), \
331 _timeout=1)
332
333 # Overriding Method: GET/POST
334 # you should not need to use this method as this library properly
335 # detects whether GET or POST should be used, Nevertheless
336 # to force a particular method, use `_method`
337 t.statuses.oembed(_id=1234567890, _method='GET')
338
339 # Send a tweet with an image included (or set your banner or logo similarily)
340 # By just reading your image from the web or a file in a string:
341 with open("example.png", "rb") as imagefile:
342 params = {"media[]": imagefile.read(), "status": "PTT"}
343 t.statuses.update_with_media(**params)
344 # Or by sending a base64 encoded image:
345 params = {"media[]": base64_image, "status": "PTT", "_base64": True}
346 t.statuses.update_with_media(**params)
347
348
349
350 Searching Twitter::
351
352 # Search for the latest tweets about #pycon
353 t.search.tweets(q="#pycon")
354
355
356 Using the data returned
357 -----------------------
358
359 Twitter API calls return decoded JSON. This is converted into
360 a bunch of Python lists, dicts, ints, and strings. For example::
361
362 x = twitter.statuses.home_timeline()
363
364 # The first 'tweet' in the timeline
365 x[0]
366
367 # The screen name of the user who wrote the first 'tweet'
368 x[0]['user']['screen_name']
369
370
371 Getting raw XML data
372 --------------------
373
374 If you prefer to get your Twitter data in XML format, pass
375 format="xml" to the Twitter object when you instantiate it::
376
377 twitter = Twitter(format="xml")
378
379 The output will not be parsed in any way. It will be a raw string
380 of XML.
381
382 """
383 def __init__(
384 self, format="json",
385 domain="api.twitter.com", secure=True, auth=None,
386 api_version=_DEFAULT):
387 """
388 Create a new twitter API connector.
389
390 Pass an `auth` parameter to use the credentials of a specific
391 user. Generally you'll want to pass an `OAuth`
392 instance::
393
394 twitter = Twitter(auth=OAuth(
395 token, token_secret, consumer_key, consumer_secret))
396
397
398 `domain` lets you change the domain you are connecting. By
399 default it's `api.twitter.com`.
400
401 If `secure` is False you will connect with HTTP instead of
402 HTTPS.
403
404 `api_version` is used to set the base uri. By default it's
405 '1.1'.
406 """
407 if not auth:
408 auth = NoAuth()
409
410 if (format not in ("json", "xml", "")):
411 raise ValueError("Unknown data format '%s'" % (format))
412
413 if api_version is _DEFAULT:
414 api_version = '1.1'
415
416 uriparts = ()
417 if api_version:
418 uriparts += (str(api_version),)
419
420 TwitterCall.__init__(
421 self, auth=auth, format=format, domain=domain,
422 callable_cls=TwitterCall,
423 secure=secure, uriparts=uriparts)
424
425
426 __all__ = ["Twitter", "TwitterError", "TwitterHTTPError", "TwitterResponse"]