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