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