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