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