]> jfr.im git - z_archive/twitter.git/blame - twitter/api.py
Clean up documentation and README
[z_archive/twitter.git] / twitter / api.py
CommitLineData
f0603331
MV
1# encoding: utf-8
2from __future__ import unicode_literals
3
3930cc7b
MV
4try:
5 import urllib.request as urllib_request
6 import urllib.error as urllib_error
7except ImportError:
8 import urllib2 as urllib_request
9 import urllib2 as urllib_error
7364ea65 10
a403f1b3
JL
11try:
12 from cStringIO import StringIO
13except ImportError:
6204d0da 14 from io import BytesIO as StringIO
a403f1b3 15
14fc6b25
MG
16from .twitter_globals import POST_ACTIONS
17from .auth import NoAuth
4e9d6343 18
2ee65672 19import re
a403f1b3 20import gzip
23dcf621 21
22try:
dea9a3e7 23 import http.client as http_client
23dcf621 24except ImportError:
dea9a3e7 25 import httplib as http_client
2ee65672 26
4b12a3a0 27try:
f1a8ed67 28 import json
4b12a3a0 29except ImportError:
f1a8ed67 30 import simplejson as json
31
fcf08b18 32
652c5402
MV
33class _DEFAULT(object):
34 pass
35
fcf08b18 36
5251ea48 37class TwitterError(Exception):
21e3bd23 38 """
64a8d213
B
39 Base Exception thrown by the Twitter object when there is a
40 general error interacting with the API.
21e3bd23 41 """
5251ea48 42 pass
43
fcf08b18 44
64a8d213
B
45class TwitterHTTPError(TwitterError):
46 """
47 Exception thrown by the Twitter object when there is an
48 HTTP error interacting with twitter.com.
49 """
1be4ce71 50 def __init__(self, e, uri, format, uriparts):
4b12a3a0
MV
51 self.e = e
52 self.uri = uri
53 self.format = format
54 self.uriparts = uriparts
7fe9aab6
HN
55 try:
56 data = self.e.fp.read()
dea9a3e7 57 except http_client.IncompleteRead as e:
7fe9aab6
HN
58 # can't read the error text
59 # let's try some of it
60 data = e.partial
e9fc8d86 61 if self.e.headers.get('Content-Encoding') == 'gzip':
7fe9aab6 62 buf = StringIO(data)
84d2da3d 63 f = gzip.GzipFile(fileobj=buf)
64 self.response_data = f.read()
65 else:
7fe9aab6 66 self.response_data = data
9f04d75d 67 super(TwitterHTTPError, self).__init__(str(self))
64a8d213
B
68
69 def __str__(self):
57b54437 70 fmt = ("." + self.format) if self.format else ""
68b3e2ee 71 return (
57b54437 72 "Twitter sent status %i for URL: %s%s using parameters: "
fcf08b18 73 "(%s)\ndetails: %s" % (
57b54437 74 self.e.code, self.uri, fmt, self.uriparts,
c7dd86d1 75 self.response_data))
64a8d213 76
fcf08b18 77
84d0a294
MV
78class 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
ba02331e 86 `response.headers.get('h')` to retrieve a header.
84d0a294 87 """
84d0a294 88
84d0a294
MV
89 @property
90 def rate_limit_remaining(self):
91 """
92 Remaining requests in the current rate-limit.
93 """
eeec9b00
IA
94 return int(self.headers.get('X-Rate-Limit-Remaining', "0"))
95
96 @property
97 def rate_limit_limit(self):
98 """
c53558ad 99 The rate limit ceiling for that given request.
eeec9b00
IA
100 """
101 return int(self.headers.get('X-Rate-Limit-Limit', "0"))
84d0a294
MV
102
103 @property
104 def rate_limit_reset(self):
105 """
106 Time in UTC epoch seconds when the rate limit will reset.
107 """
eeec9b00 108 return int(self.headers.get('X-Rate-Limit-Reset', "0"))
84d0a294
MV
109
110
ed23f46c
MV
111class TwitterDictResponse(dict, TwitterResponse):
112 pass
12bba6ac 113
abddd419 114
ed23f46c
MV
115class TwitterListResponse(list, TwitterResponse):
116 pass
fcf08b18 117
94803fc9 118
ed23f46c
MV
119def 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
abddd419 130
0d6c0646 131
7364ea65 132class TwitterCall(object):
dd648a25 133
c8d451e8 134 def __init__(
fcf08b18 135 self, auth, format, domain, callable_cls, uri="",
1ff50236 136 uriparts=None, secure=True, timeout=None, gzip=False):
568331a9 137 self.auth = auth
a55e6a11 138 self.format = format
153dee29 139 self.domain = domain
dd648a25 140 self.callable_cls = callable_cls
7364ea65 141 self.uri = uri
b0dedfc0 142 self.uriparts = uriparts
9a148ed1 143 self.secure = secure
effd06bb 144 self.timeout = timeout
86318060 145 self.gzip = gzip
fd2bc885 146
7364ea65 147 def __getattr__(self, k):
148 try:
149 return object.__getattr__(self, k)
150 except AttributeError:
e748eed8 151 def extend_call(arg):
152 return self.callable_cls(
153 auth=self.auth, format=self.format, domain=self.domain,
ff3ca197 154 callable_cls=self.callable_cls, timeout=self.timeout,
86318060 155 secure=self.secure, gzip=self.gzip,
ff3ca197 156 uriparts=self.uriparts + (arg,))
e748eed8 157 if k == "_":
158 return extend_call
159 else:
160 return extend_call(k)
fd2bc885 161
7364ea65 162 def __call__(self, **kwargs):
aec68959 163 # Build the uri.
1be4ce71 164 uriparts = []
b0dedfc0 165 for uripart in self.uriparts:
aec68959
MV
166 # If this part matches a keyword argument, use the
167 # supplied value otherwise, just use the part.
f7e63802
MV
168 uriparts.append(str(kwargs.pop(uripart, uripart)))
169 uri = '/'.join(uriparts)
1be4ce71 170
57b54437 171 method = kwargs.pop('_method', None)
172 if not method:
173 method = "GET"
174 for action in POST_ACTIONS:
2ee65672 175 if re.search("%s(/\d+)?$" % action, uri):
57b54437 176 method = "POST"
177 break
612ececa 178
aec68959
MV
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.
da45d039
MV
181 id = kwargs.pop('id', None)
182 if id:
fcf08b18 183 uri += "/%s" % (id)
4e9d6343 184
920528cd
MV
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
be5f32da 190
8fd7289d
IA
191 # If an _timeout is specified in kwargs, use it
192 _timeout = kwargs.pop('_timeout', None)
920528cd 193
568331a9
MH
194 secure_str = ''
195 if self.secure:
196 secure_str = 's'
6c527e72 197 dot = ""
1be4ce71 198 if self.format:
6c527e72 199 dot = "."
fcf08b18 200 uriBase = "http%s://%s/%s%s%s" % (
201 secure_str, self.domain, uri, dot, self.format)
568331a9 202
94fb8fab
R
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)
525c9c31
R
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)
94fb8fab
R
215 mediafield = arg
216 break
217
86318060 218 headers = {'Accept-Encoding': 'gzip'} if self.gzip else dict()
1ff50236 219 body = None
220 arg_data = None
1be4ce71 221 if self.auth:
568331a9 222 headers.update(self.auth.generate_headers())
94fb8fab
R
223 # Use urlencoded oauth args with no params when sending media
224 # via multipart and send it directly via uri even for post
1ff50236 225 arg_data = self.auth.encode_params(
226 uriBase, method, {} if media else kwargs)
94fb8fab 227 if method == 'GET' or media:
1be4ce71 228 uriBase += '?' + arg_data
1be4ce71 229 else:
8eb73aab 230 body = arg_data.encode('utf8')
c53558ad 231
94fb8fab
R
232 # Handle query as multipart when sending media
233 if media:
234 BOUNDARY = "###Python-Twitter###"
235 bod = []
236 bod.append('--' + BOUNDARY)
1ff50236 237 bod.append(
238 'Content-Disposition: form-data; name="%s"' % mediafield)
525c9c31 239 bod.append('Content-Transfer-Encoding: base64')
94fb8fab
R
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)
1ff50236 249 headers['Content-Type'] = \
250 'multipart/form-data; boundary=%s' % BOUNDARY
94fb8fab 251
3930cc7b 252 req = urllib_request.Request(uriBase, body, headers)
8fd7289d 253 return self._handle_response(req, uri, arg_data, _timeout)
102acdb1 254
8fd7289d 255 def _handle_response(self, req, uri, arg_data, _timeout=None):
a5aab114 256 kwargs = {}
8fd7289d
IA
257 if _timeout:
258 kwargs['timeout'] = _timeout
7364ea65 259 try:
a5aab114 260 handle = urllib_request.urlopen(req, **kwargs)
918b8b48
GC
261 if handle.headers['Content-Type'] in ['image/jpeg', 'image/png']:
262 return handle
0fdfdc3d
DM
263 try:
264 data = handle.read()
dea9a3e7 265 except http_client.IncompleteRead as e:
0fdfdc3d
DM
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':
a403f1b3 270 # Handle gzip decompression
0fdfdc3d 271 buf = StringIO(data)
a403f1b3
JL
272 f = gzip.GzipFile(fileobj=buf)
273 data = f.read()
de072195 274 if "json" == self.format:
a403f1b3 275 res = json.loads(data.decode('utf8'))
abddd419 276 return wrap_response(res, handle.headers)
de072195 277 else:
456ec92b 278 return wrap_response(
a403f1b3 279 data.decode('utf8'), handle.headers)
3930cc7b 280 except urllib_error.HTTPError as e:
de072195 281 if (e.code == 304):
7364ea65 282 return []
de072195 283 else:
aec68959 284 raise TwitterHTTPError(e, uri, self.format, arg_data)
102acdb1 285
fcf08b18 286
7364ea65 287class Twitter(TwitterCall):
288 """
289 The minimalist yet fully featured Twitter API class.
4e9d6343 290
7364ea65 291 Get RESTful data by accessing members of this class. The result
292 is decoded python objects (lists and dicts).
293
51e0b8f1 294 The Twitter API is documented at:
153dee29 295
aec68959
MV
296 http://dev.twitter.com/doc
297
4e9d6343 298
7364ea65 299 Examples::
4e9d6343 300
d4f3123e
MV
301 from twitter import *
302
d09c0dd3 303 t = Twitter(
51e0b8f1 304 auth=OAuth(token, token_key, con_secret, con_secret_key)))
4e9d6343 305
58ccea4e
MV
306 # Get your "home" timeline
307 t.statuses.home_timeline()
4e9d6343 308
d4f3123e
MV
309 # Get a particular friend's timeline
310 t.statuses.user_timeline(screen_name="billybob")
311
312 # to pass in GET/POST parameters, such as `count`
313 t.statuses.home_timeline(count=5)
314
315 # to pass in the GET/POST parameter `id` you need to use `_id`
316 t.statuses.oembed(_id=1234567890)
d09c0dd3
MV
317
318 # Update your status
319 t.statuses.update(
320 status="Using @sixohsix's sweet Python Twitter Tools.")
4e9d6343 321
51e0b8f1 322 # Send a direct message
d09c0dd3 323 t.direct_messages.new(
51e0b8f1
MV
324 user="billybob",
325 text="I think yer swell!")
7364ea65 326
d09c0dd3
MV
327 # Get the members of tamtar's list "Things That Are Rad"
328 t._("tamtar")._("things-that-are-rad").members()
329
330 # Note how the magic `_` method can be used to insert data
331 # into the middle of a call. You can also use replacement:
332 t.user.list.members(user="tamtar", list="things-that-are-rad")
be5f32da 333
8fd7289d 334 # An *optional* `_timeout` parameter can also be used for API
a5aab114 335 # calls which take much more time than normal or twitter stops
d4f3123e 336 # responding for some reason:
a5aab114
IA
337 t.users.lookup(
338 screen_name=','.join(A_LIST_OF_100_SCREEN_NAMES), \
8fd7289d 339 _timeout=1)
a5aab114 340
5a412b39
R
341 # Overriding Method: GET/POST
342 # you should not need to use this method as this library properly
343 # detects whether GET or POST should be used, Nevertheless
344 # to force a particular method, use `_method`
345 t.statuses.oembed(_id=1234567890, _method='GET')
346
347 # Send a tweet with an image included (or set your banner or logo similarily)
d4f3123e 348 # by just reading your image from the web or a file in a string:
5a412b39
R
349 with open("example.png", "rb") as imagefile:
350 params = {"media[]": imagefile.read(), "status": "PTT"}
351 t.statuses.update_with_media(**params)
d4f3123e 352
5a412b39
R
353 # Or by sending a base64 encoded image:
354 params = {"media[]": base64_image, "status": "PTT", "_base64": True}
355 t.statuses.update_with_media(**params)
356
b0dedfc0 357
153dee29 358 Searching Twitter::
4e9d6343 359
58ccea4e
MV
360 # Search for the latest tweets about #pycon
361 t.search.tweets(q="#pycon")
153dee29 362
7364ea65 363
68b3e2ee
MV
364 Using the data returned
365 -----------------------
366
367 Twitter API calls return decoded JSON. This is converted into
368 a bunch of Python lists, dicts, ints, and strings. For example::
7364ea65 369
58ccea4e 370 x = twitter.statuses.home_timeline()
7364ea65 371
51e0b8f1
MV
372 # The first 'tweet' in the timeline
373 x[0]
7364ea65 374
51e0b8f1
MV
375 # The screen name of the user who wrote the first 'tweet'
376 x[0]['user']['screen_name']
4e9d6343 377
4e9d6343 378
68b3e2ee
MV
379 Getting raw XML data
380 --------------------
381
382 If you prefer to get your Twitter data in XML format, pass
383 format="xml" to the Twitter object when you instantiate it::
4e9d6343 384
51e0b8f1 385 twitter = Twitter(format="xml")
4e9d6343 386
51e0b8f1
MV
387 The output will not be parsed in any way. It will be a raw string
388 of XML.
68b3e2ee 389
7364ea65 390 """
45688301 391 def __init__(
fcf08b18 392 self, format="json",
393 domain="api.twitter.com", secure=True, auth=None,
394 api_version=_DEFAULT):
7364ea65 395 """
68b3e2ee
MV
396 Create a new twitter API connector.
397
398 Pass an `auth` parameter to use the credentials of a specific
399 user. Generally you'll want to pass an `OAuth`
69e1f98e
MV
400 instance::
401
402 twitter = Twitter(auth=OAuth(
403 token, token_secret, consumer_key, consumer_secret))
404
405
68b3e2ee 406 `domain` lets you change the domain you are connecting. By
fcf08b18 407 default it's `api.twitter.com`.
68b3e2ee
MV
408
409 If `secure` is False you will connect with HTTP instead of
410 HTTPS.
411
1cc9ab0b 412 `api_version` is used to set the base uri. By default it's
fcf08b18 413 '1.1'.
7364ea65 414 """
d20da7f3
MV
415 if not auth:
416 auth = NoAuth()
417
6c527e72 418 if (format not in ("json", "xml", "")):
fcf08b18 419 raise ValueError("Unknown data format '%s'" % (format))
68b3e2ee 420
652c5402 421 if api_version is _DEFAULT:
82a93c03 422 api_version = '1.1'
652c5402 423
1be4ce71 424 uriparts = ()
68b3e2ee 425 if api_version:
1be4ce71 426 uriparts += (str(api_version),)
68b3e2ee 427
9a148ed1 428 TwitterCall.__init__(
aec68959 429 self, auth=auth, format=format, domain=domain,
dd648a25 430 callable_cls=TwitterCall,
1be4ce71 431 secure=secure, uriparts=uriparts)
7e43e2ed 432
7364ea65 433
abddd419 434__all__ = ["Twitter", "TwitterError", "TwitterHTTPError", "TwitterResponse"]