]> jfr.im git - z_archive/twitter.git/blob - twitter/api.py
Changed special parameter timeout to _timeout so that it doesn't block any parameter...
[z_archive/twitter.git] / twitter / api.py
1 try:
2 import urllib.request as urllib_request
3 import urllib.error as urllib_error
4 except ImportError:
5 import urllib2 as urllib_request
6 import urllib2 as urllib_error
7
8 try:
9 from cStringIO import StringIO
10 except ImportError:
11 from io import BytesIO as StringIO
12
13 from twitter.twitter_globals import POST_ACTIONS
14 from twitter.auth import NoAuth
15
16 import re
17 import gzip
18
19 try:
20 import json
21 except ImportError:
22 import simplejson as json
23
24 class _DEFAULT(object):
25 pass
26
27 class TwitterError(Exception):
28 """
29 Base Exception thrown by the Twitter object when there is a
30 general error interacting with the API.
31 """
32 pass
33
34 class TwitterHTTPError(TwitterError):
35 """
36 Exception thrown by the Twitter object when there is an
37 HTTP error interacting with twitter.com.
38 """
39 def __init__(self, e, uri, format, uriparts):
40 self.e = e
41 self.uri = uri
42 self.format = format
43 self.uriparts = uriparts
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()
50
51 def __str__(self):
52 fmt = ("." + self.format) if self.format else ""
53 return (
54 "Twitter sent status %i for URL: %s%s using parameters: "
55 "(%s)\ndetails: %s" %(
56 self.e.code, self.uri, fmt, self.uriparts,
57 self.response_data))
58
59 class 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
67 `response.headers.get('h')` to retrieve a header.
68 """
69 def __init__(self, headers):
70 self.headers = headers
71
72 @property
73 def rate_limit_remaining(self):
74 """
75 Remaining requests in the current rate-limit.
76 """
77 return int(self.headers.get('X-Rate-Limit-Remaining', "0"))
78
79 @property
80 def rate_limit_limit(self):
81 """
82 The rate limit ceiling for that given request.
83 """
84 return int(self.headers.get('X-Rate-Limit-Limit', "0"))
85
86 @property
87 def rate_limit_reset(self):
88 """
89 Time in UTC epoch seconds when the rate limit will reset.
90 """
91 return int(self.headers.get('X-Rate-Limit-Reset', "0"))
92
93
94 def wrap_response(response, headers):
95 response_typ = type(response)
96 if response_typ is bool:
97 # HURF DURF MY NAME IS PYTHON AND I CAN'T SUBCLASS bool.
98 response_typ = int
99
100 class WrappedTwitterResponse(response_typ, TwitterResponse):
101 __doc__ = TwitterResponse.__doc__
102
103 def __init__(self, response, headers):
104 response_typ.__init__(self, response)
105 TwitterResponse.__init__(self, headers)
106 def __new__(cls, response, headers):
107 return response_typ.__new__(cls, response)
108
109
110 return WrappedTwitterResponse(response, headers)
111
112
113
114 class TwitterCall(object):
115
116 def __init__(
117 self, auth, format, domain, callable_cls, uri="",
118 uriparts=None, secure=True):
119 self.auth = auth
120 self.format = format
121 self.domain = domain
122 self.callable_cls = callable_cls
123 self.uri = uri
124 self.uriparts = uriparts
125 self.secure = secure
126
127 def __getattr__(self, k):
128 try:
129 return object.__getattr__(self, k)
130 except AttributeError:
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)
141
142 def __call__(self, **kwargs):
143 # Build the uri.
144 uriparts = []
145 for uripart in self.uriparts:
146 # If this part matches a keyword argument, use the
147 # supplied value otherwise, just use the part.
148 uriparts.append(str(kwargs.pop(uripart, uripart)))
149 uri = '/'.join(uriparts)
150
151 method = kwargs.pop('_method', None)
152 if not method:
153 method = "GET"
154 for action in POST_ACTIONS:
155 if re.search("%s(/\d+)?$" % action, uri):
156 method = "POST"
157 break
158
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.
161 id = kwargs.pop('id', None)
162 if id:
163 uri += "/%s" %(id)
164
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
170
171 # If an _timeout is specified in kwargs, use it
172 _timeout = kwargs.pop('_timeout', None)
173
174 secure_str = ''
175 if self.secure:
176 secure_str = 's'
177 dot = ""
178 if self.format:
179 dot = "."
180 uriBase = "http%s://%s/%s%s%s" %(
181 secure_str, self.domain, uri, dot, self.format)
182
183 headers = {'Accept-Encoding': 'gzip'}
184 if self.auth:
185 headers.update(self.auth.generate_headers())
186 arg_data = self.auth.encode_params(uriBase, method, kwargs)
187 if method == 'GET':
188 uriBase += '?' + arg_data
189 body = None
190 else:
191 body = arg_data.encode('utf8')
192
193 req = urllib_request.Request(uriBase, body, headers)
194 return self._handle_response(req, uri, arg_data, _timeout)
195
196 def _handle_response(self, req, uri, arg_data, _timeout=None):
197 kwargs = {}
198 if _timeout:
199 kwargs['timeout'] = _timeout
200 try:
201 handle = urllib_request.urlopen(req, **kwargs)
202 if handle.headers['Content-Type'] in ['image/jpeg', 'image/png']:
203 return handle
204 elif handle.info().get('Content-Encoding') == 'gzip':
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
212 if "json" == self.format:
213 res = json.loads(data.decode('utf8'))
214 return wrap_response(res, handle.headers)
215 else:
216 return wrap_response(
217 data.decode('utf8'), handle.headers)
218 except urllib_error.HTTPError as e:
219 if (e.code == 304):
220 return []
221 else:
222 raise TwitterHTTPError(e, uri, self.format, arg_data)
223
224 class Twitter(TwitterCall):
225 """
226 The minimalist yet fully featured Twitter API class.
227
228 Get RESTful data by accessing members of this class. The result
229 is decoded python objects (lists and dicts).
230
231 The Twitter API is documented at:
232
233 http://dev.twitter.com/doc
234
235
236 Examples::
237
238 t = Twitter(
239 auth=OAuth(token, token_key, con_secret, con_secret_key)))
240
241 # Get the public timeline
242 t.statuses.public_timeline()
243
244 # Get a particular friend's timeline
245 t.statuses.friends_timeline(id="billybob")
246
247 # Also supported (but totally weird)
248 t.statuses.friends_timeline.billybob()
249
250 # Update your status
251 t.statuses.update(
252 status="Using @sixohsix's sweet Python Twitter Tools.")
253
254 # Send a direct message
255 t.direct_messages.new(
256 user="billybob",
257 text="I think yer swell!")
258
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")
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
273
274
275 Searching Twitter::
276
277 twitter_search = Twitter(domain="search.twitter.com")
278
279 # Find the latest search trends
280 twitter_search.trends()
281
282 # Search for the latest News on #gaza
283 twitter_search.search(q="#gaza")
284
285
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::
291
292 x = twitter.statuses.public_timeline()
293
294 # The first 'tweet' in the timeline
295 x[0]
296
297 # The screen name of the user who wrote the first 'tweet'
298 x[0]['user']['screen_name']
299
300
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::
306
307 twitter = Twitter(format="xml")
308
309 The output will not be parsed in any way. It will be a raw string
310 of XML.
311
312 """
313 def __init__(
314 self, format="json",
315 domain="api.twitter.com", secure=True, auth=None,
316 api_version=_DEFAULT):
317 """
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`
322 instance::
323
324 twitter = Twitter(auth=OAuth(
325 token, token_secret, consumer_key, consumer_secret))
326
327
328 `domain` lets you change the domain you are connecting. By
329 default it's `api.twitter.com` but `search.twitter.com` may be
330 useful too.
331
332 If `secure` is False you will connect with HTTP instead of
333 HTTPS.
334
335 `api_version` is used to set the base uri. By default it's
336 '1'. If you are using "search.twitter.com" set this to None.
337 """
338 if not auth:
339 auth = NoAuth()
340
341 if (format not in ("json", "xml", "")):
342 raise ValueError("Unknown data format '%s'" %(format))
343
344 if api_version is _DEFAULT:
345 if domain == 'api.twitter.com':
346 api_version = '1.1'
347 else:
348 api_version = None
349
350 uriparts = ()
351 if api_version:
352 uriparts += (str(api_version),)
353
354 TwitterCall.__init__(
355 self, auth=auth, format=format, domain=domain,
356 callable_cls=TwitterCall,
357 secure=secure, uriparts=uriparts)
358
359
360 __all__ = ["Twitter", "TwitterError", "TwitterHTTPError", "TwitterResponse"]