]> jfr.im git - z_archive/twitter.git/blame - twitter/api.py
Fix issue #90. Use _id to mean 'id' as a CGI param.
[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
920528cd
MV
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
568331a9
MH
164 secure_str = ''
165 if self.secure:
166 secure_str = 's'
6c527e72 167 dot = ""
1be4ce71 168 if self.format:
6c527e72
MV
169 dot = "."
170 uriBase = "http%s://%s/%s%s%s" %(
171 secure_str, self.domain, uri, dot, self.format)
568331a9 172
a403f1b3 173 headers = {'Accept-Encoding': 'gzip'}
1be4ce71 174 if self.auth:
568331a9 175 headers.update(self.auth.generate_headers())
1be4ce71
MV
176 arg_data = self.auth.encode_params(uriBase, method, kwargs)
177 if method == 'GET':
178 uriBase += '?' + arg_data
179 body = None
180 else:
8eb73aab 181 body = arg_data.encode('utf8')
1be4ce71 182
3930cc7b 183 req = urllib_request.Request(uriBase, body, headers)
dd648a25 184 return self._handle_response(req, uri, arg_data)
102acdb1 185
dd648a25 186 def _handle_response(self, req, uri, arg_data):
7364ea65 187 try:
3930cc7b 188 handle = urllib_request.urlopen(req)
918b8b48
GC
189 if handle.headers['Content-Type'] in ['image/jpeg', 'image/png']:
190 return handle
309620de 191 elif handle.info().get('Content-Encoding') == 'gzip':
a403f1b3
JL
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
de072195 199 if "json" == self.format:
a403f1b3 200 res = json.loads(data.decode('utf8'))
abddd419 201 return wrap_response(res, handle.headers)
de072195 202 else:
456ec92b 203 return wrap_response(
a403f1b3 204 data.decode('utf8'), handle.headers)
3930cc7b 205 except urllib_error.HTTPError as e:
de072195 206 if (e.code == 304):
7364ea65 207 return []
de072195 208 else:
aec68959 209 raise TwitterHTTPError(e, uri, self.format, arg_data)
102acdb1 210
7364ea65 211class Twitter(TwitterCall):
212 """
213 The minimalist yet fully featured Twitter API class.
4e9d6343 214
7364ea65 215 Get RESTful data by accessing members of this class. The result
216 is decoded python objects (lists and dicts).
217
51e0b8f1 218 The Twitter API is documented at:
153dee29 219
aec68959
MV
220 http://dev.twitter.com/doc
221
4e9d6343 222
7364ea65 223 Examples::
4e9d6343 224
d09c0dd3 225 t = Twitter(
51e0b8f1 226 auth=OAuth(token, token_key, con_secret, con_secret_key)))
4e9d6343 227
51e0b8f1 228 # Get the public timeline
d09c0dd3 229 t.statuses.public_timeline()
4e9d6343 230
51e0b8f1 231 # Get a particular friend's timeline
d09c0dd3 232 t.statuses.friends_timeline(id="billybob")
4e9d6343 233
51e0b8f1 234 # Also supported (but totally weird)
d09c0dd3
MV
235 t.statuses.friends_timeline.billybob()
236
237 # Update your status
238 t.statuses.update(
239 status="Using @sixohsix's sweet Python Twitter Tools.")
4e9d6343 240
51e0b8f1 241 # Send a direct message
d09c0dd3 242 t.direct_messages.new(
51e0b8f1
MV
243 user="billybob",
244 text="I think yer swell!")
7364ea65 245
d09c0dd3
MV
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")
b0dedfc0 252
69e1f98e 253
153dee29 254 Searching Twitter::
4e9d6343 255
51e0b8f1 256 twitter_search = Twitter(domain="search.twitter.com")
153dee29 257
51e0b8f1
MV
258 # Find the latest search trends
259 twitter_search.trends()
153dee29 260
51e0b8f1
MV
261 # Search for the latest News on #gaza
262 twitter_search.search(q="#gaza")
153dee29 263
7364ea65 264
68b3e2ee
MV
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::
7364ea65 270
51e0b8f1 271 x = twitter.statuses.public_timeline()
7364ea65 272
51e0b8f1
MV
273 # The first 'tweet' in the timeline
274 x[0]
7364ea65 275
51e0b8f1
MV
276 # The screen name of the user who wrote the first 'tweet'
277 x[0]['user']['screen_name']
4e9d6343 278
4e9d6343 279
68b3e2ee
MV
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::
4e9d6343 285
51e0b8f1 286 twitter = Twitter(format="xml")
4e9d6343 287
51e0b8f1
MV
288 The output will not be parsed in any way. It will be a raw string
289 of XML.
68b3e2ee 290
7364ea65 291 """
45688301 292 def __init__(
aec68959 293 self, format="json",
87ad04c3 294 domain="api.twitter.com", secure=True, auth=None,
652c5402 295 api_version=_DEFAULT):
7364ea65 296 """
68b3e2ee
MV
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`
69e1f98e
MV
301 instance::
302
303 twitter = Twitter(auth=OAuth(
304 token, token_secret, consumer_key, consumer_secret))
305
306
68b3e2ee 307 `domain` lets you change the domain you are connecting. By
87ad04c3 308 default it's `api.twitter.com` but `search.twitter.com` may be
68b3e2ee
MV
309 useful too.
310
311 If `secure` is False you will connect with HTTP instead of
312 HTTPS.
313
1cc9ab0b 314 `api_version` is used to set the base uri. By default it's
652c5402 315 '1'. If you are using "search.twitter.com" set this to None.
7364ea65 316 """
d20da7f3
MV
317 if not auth:
318 auth = NoAuth()
319
6c527e72 320 if (format not in ("json", "xml", "")):
68b3e2ee
MV
321 raise ValueError("Unknown data format '%s'" %(format))
322
652c5402
MV
323 if api_version is _DEFAULT:
324 if domain == 'api.twitter.com':
4d4dd2cc 325 api_version = '1.1'
652c5402
MV
326 else:
327 api_version = None
328
1be4ce71 329 uriparts = ()
68b3e2ee 330 if api_version:
1be4ce71 331 uriparts += (str(api_version),)
68b3e2ee 332
9a148ed1 333 TwitterCall.__init__(
aec68959 334 self, auth=auth, format=format, domain=domain,
dd648a25 335 callable_cls=TwitterCall,
1be4ce71 336 secure=secure, uriparts=uriparts)
7e43e2ed 337
7364ea65 338
abddd419 339__all__ = ["Twitter", "TwitterError", "TwitterHTTPError", "TwitterResponse"]