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