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