]> jfr.im git - z_archive/twitter.git/blob - twitter/api.py
API defaults to version 1.1
[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-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
87 def 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
107 class 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 secure_str = ''
159 if self.secure:
160 secure_str = 's'
161 dot = ""
162 if self.format:
163 dot = "."
164 uriBase = "http%s://%s/%s%s%s" %(
165 secure_str, self.domain, uri, dot, self.format)
166
167 headers = {'Accept-Encoding': 'gzip'}
168 if self.auth:
169 headers.update(self.auth.generate_headers())
170 arg_data = self.auth.encode_params(uriBase, method, kwargs)
171 if method == 'GET':
172 uriBase += '?' + arg_data
173 body = None
174 else:
175 body = arg_data.encode('utf8')
176
177 req = urllib_request.Request(uriBase, body, headers)
178 return self._handle_response(req, uri, arg_data)
179
180 def _handle_response(self, req, uri, arg_data):
181 try:
182 handle = urllib_request.urlopen(req)
183 if handle.headers['Content-Type'] in ['image/jpeg', 'image/png']:
184 return handle
185 elif handle.info().get('Content-Encoding') == 'gzip':
186 # Handle gzip decompression
187 buf = StringIO(handle.read())
188 f = gzip.GzipFile(fileobj=buf)
189 data = f.read()
190 else:
191 data = handle.read()
192
193 if "json" == self.format:
194 res = json.loads(data.decode('utf8'))
195 return wrap_response(res, handle.headers)
196 else:
197 return wrap_response(
198 data.decode('utf8'), handle.headers)
199 except urllib_error.HTTPError as e:
200 if (e.code == 304):
201 return []
202 else:
203 raise TwitterHTTPError(e, uri, self.format, arg_data)
204
205 class Twitter(TwitterCall):
206 """
207 The minimalist yet fully featured Twitter API class.
208
209 Get RESTful data by accessing members of this class. The result
210 is decoded python objects (lists and dicts).
211
212 The Twitter API is documented at:
213
214 http://dev.twitter.com/doc
215
216
217 Examples::
218
219 t = Twitter(
220 auth=OAuth(token, token_key, con_secret, con_secret_key)))
221
222 # Get the public timeline
223 t.statuses.public_timeline()
224
225 # Get a particular friend's timeline
226 t.statuses.friends_timeline(id="billybob")
227
228 # Also supported (but totally weird)
229 t.statuses.friends_timeline.billybob()
230
231 # Update your status
232 t.statuses.update(
233 status="Using @sixohsix's sweet Python Twitter Tools.")
234
235 # Send a direct message
236 t.direct_messages.new(
237 user="billybob",
238 text="I think yer swell!")
239
240 # Get the members of tamtar's list "Things That Are Rad"
241 t._("tamtar")._("things-that-are-rad").members()
242
243 # Note how the magic `_` method can be used to insert data
244 # into the middle of a call. You can also use replacement:
245 t.user.list.members(user="tamtar", list="things-that-are-rad")
246
247
248 Searching Twitter::
249
250 twitter_search = Twitter(domain="search.twitter.com")
251
252 # Find the latest search trends
253 twitter_search.trends()
254
255 # Search for the latest News on #gaza
256 twitter_search.search(q="#gaza")
257
258
259 Using the data returned
260 -----------------------
261
262 Twitter API calls return decoded JSON. This is converted into
263 a bunch of Python lists, dicts, ints, and strings. For example::
264
265 x = twitter.statuses.public_timeline()
266
267 # The first 'tweet' in the timeline
268 x[0]
269
270 # The screen name of the user who wrote the first 'tweet'
271 x[0]['user']['screen_name']
272
273
274 Getting raw XML data
275 --------------------
276
277 If you prefer to get your Twitter data in XML format, pass
278 format="xml" to the Twitter object when you instantiate it::
279
280 twitter = Twitter(format="xml")
281
282 The output will not be parsed in any way. It will be a raw string
283 of XML.
284
285 """
286 def __init__(
287 self, format="json",
288 domain="api.twitter.com", secure=True, auth=None,
289 api_version=_DEFAULT):
290 """
291 Create a new twitter API connector.
292
293 Pass an `auth` parameter to use the credentials of a specific
294 user. Generally you'll want to pass an `OAuth`
295 instance::
296
297 twitter = Twitter(auth=OAuth(
298 token, token_secret, consumer_key, consumer_secret))
299
300
301 `domain` lets you change the domain you are connecting. By
302 default it's `api.twitter.com` but `search.twitter.com` may be
303 useful too.
304
305 If `secure` is False you will connect with HTTP instead of
306 HTTPS.
307
308 `api_version` is used to set the base uri. By default it's
309 '1'. If you are using "search.twitter.com" set this to None.
310 """
311 if not auth:
312 auth = NoAuth()
313
314 if (format not in ("json", "xml", "")):
315 raise ValueError("Unknown data format '%s'" %(format))
316
317 if api_version is _DEFAULT:
318 if domain == 'api.twitter.com':
319 api_version = '1.1'
320 else:
321 api_version = None
322
323 uriparts = ()
324 if api_version:
325 uriparts += (str(api_version),)
326
327 TwitterCall.__init__(
328 self, auth=auth, format=format, domain=domain,
329 callable_cls=TwitterCall,
330 secure=secure, uriparts=uriparts)
331
332
333 __all__ = ["Twitter", "TwitterError", "TwitterHTTPError", "TwitterResponse"]