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