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