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