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