]> jfr.im git - z_archive/twitter.git/blame - twitter/api.py
API defaults to version 1.1
[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
2ee65672 18
4b12a3a0 19try:
f1a8ed67 20 import json
4b12a3a0 21except ImportError:
f1a8ed67 22 import simplejson as json
23
652c5402
MV
24class _DEFAULT(object):
25 pass
26
5251ea48 27class TwitterError(Exception):
21e3bd23 28 """
64a8d213
B
29 Base Exception thrown by the Twitter object when there is a
30 general error interacting with the API.
21e3bd23 31 """
5251ea48 32 pass
33
64a8d213
B
34class TwitterHTTPError(TwitterError):
35 """
36 Exception thrown by the Twitter object when there is an
37 HTTP error interacting with twitter.com.
38 """
1be4ce71 39 def __init__(self, e, uri, format, uriparts):
4b12a3a0
MV
40 self.e = e
41 self.uri = uri
42 self.format = format
43 self.uriparts = uriparts
84d2da3d 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()
64a8d213
B
50
51 def __str__(self):
57b54437 52 fmt = ("." + self.format) if self.format else ""
68b3e2ee 53 return (
57b54437 54 "Twitter sent status %i for URL: %s%s using parameters: "
68b3e2ee 55 "(%s)\ndetails: %s" %(
57b54437 56 self.e.code, self.uri, fmt, self.uriparts,
c7dd86d1 57 self.response_data))
64a8d213 58
84d0a294
MV
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
ba02331e 67 `response.headers.get('h')` to retrieve a header.
84d0a294 68 """
aef72b31 69 def __init__(self, headers):
84d0a294
MV
70 self.headers = headers
71
84d0a294
MV
72 @property
73 def rate_limit_remaining(self):
74 """
75 Remaining requests in the current rate-limit.
76 """
ba02331e 77 return int(self.headers.get('X-RateLimit-Remaining', "0"))
84d0a294
MV
78
79 @property
80 def rate_limit_reset(self):
81 """
82 Time in UTC epoch seconds when the rate limit will reset.
83 """
ba02331e 84 return int(self.headers.get('X-RateLimit-Reset', "0"))
84d0a294
MV
85
86
abddd419
MV
87def wrap_response(response, headers):
88 response_typ = type(response)
ce92ec77
MV
89 if response_typ is bool:
90 # HURF DURF MY NAME IS PYTHON AND I CAN'T SUBCLASS bool.
91 response_typ = int
12bba6ac
MV
92
93 class WrappedTwitterResponse(response_typ, TwitterResponse):
abddd419
MV
94 __doc__ = TwitterResponse.__doc__
95
c77b5e4b
SK
96 def __init__(self, response, headers):
97 response_typ.__init__(self, response)
98 TwitterResponse.__init__(self, headers)
94803fc9 99 def __new__(cls, response, headers):
100 return response_typ.__new__(cls, response)
101
c77b5e4b
SK
102
103 return WrappedTwitterResponse(response, headers)
abddd419 104
0d6c0646
MV
105
106
7364ea65 107class TwitterCall(object):
dd648a25 108
c8d451e8 109 def __init__(
dd648a25 110 self, auth, format, domain, callable_cls, uri="",
7e43e2ed 111 uriparts=None, secure=True):
568331a9 112 self.auth = auth
a55e6a11 113 self.format = format
153dee29 114 self.domain = domain
dd648a25 115 self.callable_cls = callable_cls
7364ea65 116 self.uri = uri
b0dedfc0 117 self.uriparts = uriparts
9a148ed1 118 self.secure = secure
fd2bc885 119
7364ea65 120 def __getattr__(self, k):
121 try:
122 return object.__getattr__(self, k)
123 except AttributeError:
e748eed8 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)
fd2bc885 134
7364ea65 135 def __call__(self, **kwargs):
aec68959 136 # Build the uri.
1be4ce71 137 uriparts = []
b0dedfc0 138 for uripart in self.uriparts:
aec68959
MV
139 # If this part matches a keyword argument, use the
140 # supplied value otherwise, just use the part.
f7e63802
MV
141 uriparts.append(str(kwargs.pop(uripart, uripart)))
142 uri = '/'.join(uriparts)
1be4ce71 143
57b54437 144 method = kwargs.pop('_method', None)
145 if not method:
146 method = "GET"
147 for action in POST_ACTIONS:
2ee65672 148 if re.search("%s(/\d+)?$" % action, uri):
57b54437 149 method = "POST"
150 break
612ececa 151
aec68959
MV
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.
da45d039
MV
154 id = kwargs.pop('id', None)
155 if id:
156 uri += "/%s" %(id)
4e9d6343 157
568331a9
MH
158 secure_str = ''
159 if self.secure:
160 secure_str = 's'
6c527e72 161 dot = ""
1be4ce71 162 if self.format:
6c527e72
MV
163 dot = "."
164 uriBase = "http%s://%s/%s%s%s" %(
165 secure_str, self.domain, uri, dot, self.format)
568331a9 166
a403f1b3 167 headers = {'Accept-Encoding': 'gzip'}
1be4ce71 168 if self.auth:
568331a9 169 headers.update(self.auth.generate_headers())
1be4ce71
MV
170 arg_data = self.auth.encode_params(uriBase, method, kwargs)
171 if method == 'GET':
172 uriBase += '?' + arg_data
173 body = None
174 else:
8eb73aab 175 body = arg_data.encode('utf8')
1be4ce71 176
3930cc7b 177 req = urllib_request.Request(uriBase, body, headers)
dd648a25 178 return self._handle_response(req, uri, arg_data)
102acdb1 179
dd648a25 180 def _handle_response(self, req, uri, arg_data):
7364ea65 181 try:
3930cc7b 182 handle = urllib_request.urlopen(req)
918b8b48
GC
183 if handle.headers['Content-Type'] in ['image/jpeg', 'image/png']:
184 return handle
309620de 185 elif handle.info().get('Content-Encoding') == 'gzip':
a403f1b3
JL
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
de072195 193 if "json" == self.format:
a403f1b3 194 res = json.loads(data.decode('utf8'))
abddd419 195 return wrap_response(res, handle.headers)
de072195 196 else:
456ec92b 197 return wrap_response(
a403f1b3 198 data.decode('utf8'), handle.headers)
3930cc7b 199 except urllib_error.HTTPError as e:
de072195 200 if (e.code == 304):
7364ea65 201 return []
de072195 202 else:
aec68959 203 raise TwitterHTTPError(e, uri, self.format, arg_data)
102acdb1 204
7364ea65 205class Twitter(TwitterCall):
206 """
207 The minimalist yet fully featured Twitter API class.
4e9d6343 208
7364ea65 209 Get RESTful data by accessing members of this class. The result
210 is decoded python objects (lists and dicts).
211
51e0b8f1 212 The Twitter API is documented at:
153dee29 213
aec68959
MV
214 http://dev.twitter.com/doc
215
4e9d6343 216
7364ea65 217 Examples::
4e9d6343 218
d09c0dd3 219 t = Twitter(
51e0b8f1 220 auth=OAuth(token, token_key, con_secret, con_secret_key)))
4e9d6343 221
51e0b8f1 222 # Get the public timeline
d09c0dd3 223 t.statuses.public_timeline()
4e9d6343 224
51e0b8f1 225 # Get a particular friend's timeline
d09c0dd3 226 t.statuses.friends_timeline(id="billybob")
4e9d6343 227
51e0b8f1 228 # Also supported (but totally weird)
d09c0dd3
MV
229 t.statuses.friends_timeline.billybob()
230
231 # Update your status
232 t.statuses.update(
233 status="Using @sixohsix's sweet Python Twitter Tools.")
4e9d6343 234
51e0b8f1 235 # Send a direct message
d09c0dd3 236 t.direct_messages.new(
51e0b8f1
MV
237 user="billybob",
238 text="I think yer swell!")
7364ea65 239
d09c0dd3
MV
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")
b0dedfc0 246
69e1f98e 247
153dee29 248 Searching Twitter::
4e9d6343 249
51e0b8f1 250 twitter_search = Twitter(domain="search.twitter.com")
153dee29 251
51e0b8f1
MV
252 # Find the latest search trends
253 twitter_search.trends()
153dee29 254
51e0b8f1
MV
255 # Search for the latest News on #gaza
256 twitter_search.search(q="#gaza")
153dee29 257
7364ea65 258
68b3e2ee
MV
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::
7364ea65 264
51e0b8f1 265 x = twitter.statuses.public_timeline()
7364ea65 266
51e0b8f1
MV
267 # The first 'tweet' in the timeline
268 x[0]
7364ea65 269
51e0b8f1
MV
270 # The screen name of the user who wrote the first 'tweet'
271 x[0]['user']['screen_name']
4e9d6343 272
4e9d6343 273
68b3e2ee
MV
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::
4e9d6343 279
51e0b8f1 280 twitter = Twitter(format="xml")
4e9d6343 281
51e0b8f1
MV
282 The output will not be parsed in any way. It will be a raw string
283 of XML.
68b3e2ee 284
7364ea65 285 """
45688301 286 def __init__(
aec68959 287 self, format="json",
87ad04c3 288 domain="api.twitter.com", secure=True, auth=None,
652c5402 289 api_version=_DEFAULT):
7364ea65 290 """
68b3e2ee
MV
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`
69e1f98e
MV
295 instance::
296
297 twitter = Twitter(auth=OAuth(
298 token, token_secret, consumer_key, consumer_secret))
299
300
68b3e2ee 301 `domain` lets you change the domain you are connecting. By
87ad04c3 302 default it's `api.twitter.com` but `search.twitter.com` may be
68b3e2ee
MV
303 useful too.
304
305 If `secure` is False you will connect with HTTP instead of
306 HTTPS.
307
1cc9ab0b 308 `api_version` is used to set the base uri. By default it's
652c5402 309 '1'. If you are using "search.twitter.com" set this to None.
7364ea65 310 """
d20da7f3
MV
311 if not auth:
312 auth = NoAuth()
313
6c527e72 314 if (format not in ("json", "xml", "")):
68b3e2ee
MV
315 raise ValueError("Unknown data format '%s'" %(format))
316
652c5402
MV
317 if api_version is _DEFAULT:
318 if domain == 'api.twitter.com':
4d4dd2cc 319 api_version = '1.1'
652c5402
MV
320 else:
321 api_version = None
322
1be4ce71 323 uriparts = ()
68b3e2ee 324 if api_version:
1be4ce71 325 uriparts += (str(api_version),)
68b3e2ee 326
9a148ed1 327 TwitterCall.__init__(
aec68959 328 self, auth=auth, format=format, domain=domain,
dd648a25 329 callable_cls=TwitterCall,
1be4ce71 330 secure=secure, uriparts=uriparts)
7e43e2ed 331
7364ea65 332
abddd419 333__all__ = ["Twitter", "TwitterError", "TwitterHTTPError", "TwitterResponse"]