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