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