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