]> jfr.im git - z_archive/twitter.git/blame - twitter/api.py
fixes for python 3
[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
c7dd86d1 44 self.response_data = self.e.fp.read()
64a8d213
B
45
46 def __str__(self):
57b54437 47 fmt = ("." + self.format) if self.format else ""
68b3e2ee 48 return (
57b54437 49 "Twitter sent status %i for URL: %s%s using parameters: "
68b3e2ee 50 "(%s)\ndetails: %s" %(
57b54437 51 self.e.code, self.uri, fmt, self.uriparts,
c7dd86d1 52 self.response_data))
64a8d213 53
84d0a294
MV
54class 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 """
aef72b31 64 def __init__(self, headers):
84d0a294
MV
65 self.headers = headers
66
84d0a294
MV
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
abddd419
MV
82def wrap_response(response, headers):
83 response_typ = type(response)
ce92ec77
MV
84 if response_typ is bool:
85 # HURF DURF MY NAME IS PYTHON AND I CAN'T SUBCLASS bool.
86 response_typ = int
12bba6ac
MV
87
88 class WrappedTwitterResponse(response_typ, TwitterResponse):
abddd419
MV
89 __doc__ = TwitterResponse.__doc__
90
c77b5e4b
SK
91 def __init__(self, response, headers):
92 response_typ.__init__(self, response)
93 TwitterResponse.__init__(self, headers)
94803fc9 94 def __new__(cls, response, headers):
95 return response_typ.__new__(cls, response)
96
c77b5e4b
SK
97
98 return WrappedTwitterResponse(response, headers)
abddd419 99
0d6c0646
MV
100
101
7364ea65 102class TwitterCall(object):
dd648a25 103
c8d451e8 104 def __init__(
dd648a25 105 self, auth, format, domain, callable_cls, uri="",
7e43e2ed 106 uriparts=None, secure=True):
568331a9 107 self.auth = auth
a55e6a11 108 self.format = format
153dee29 109 self.domain = domain
dd648a25 110 self.callable_cls = callable_cls
7364ea65 111 self.uri = uri
b0dedfc0 112 self.uriparts = uriparts
9a148ed1 113 self.secure = secure
fd2bc885 114
7364ea65 115 def __getattr__(self, k):
116 try:
117 return object.__getattr__(self, k)
118 except AttributeError:
e748eed8 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)
fd2bc885 129
7364ea65 130 def __call__(self, **kwargs):
aec68959 131 # Build the uri.
1be4ce71 132 uriparts = []
b0dedfc0 133 for uripart in self.uriparts:
aec68959
MV
134 # If this part matches a keyword argument, use the
135 # supplied value otherwise, just use the part.
f7e63802
MV
136 uriparts.append(str(kwargs.pop(uripart, uripart)))
137 uri = '/'.join(uriparts)
1be4ce71 138
57b54437 139 method = kwargs.pop('_method', None)
140 if not method:
141 method = "GET"
142 for action in POST_ACTIONS:
2ee65672 143 if re.search("%s(/\d+)?$" % action, uri):
57b54437 144 method = "POST"
145 break
612ececa 146
aec68959
MV
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.
da45d039
MV
149 id = kwargs.pop('id', None)
150 if id:
151 uri += "/%s" %(id)
4e9d6343 152
568331a9
MH
153 secure_str = ''
154 if self.secure:
155 secure_str = 's'
6c527e72 156 dot = ""
1be4ce71 157 if self.format:
6c527e72
MV
158 dot = "."
159 uriBase = "http%s://%s/%s%s%s" %(
160 secure_str, self.domain, uri, dot, self.format)
568331a9 161
a403f1b3 162 headers = {'Accept-Encoding': 'gzip'}
1be4ce71 163 if self.auth:
568331a9 164 headers.update(self.auth.generate_headers())
1be4ce71
MV
165 arg_data = self.auth.encode_params(uriBase, method, kwargs)
166 if method == 'GET':
167 uriBase += '?' + arg_data
168 body = None
169 else:
8eb73aab 170 body = arg_data.encode('utf8')
1be4ce71 171
3930cc7b 172 req = urllib_request.Request(uriBase, body, headers)
dd648a25 173 return self._handle_response(req, uri, arg_data)
102acdb1 174
dd648a25 175 def _handle_response(self, req, uri, arg_data):
7364ea65 176 try:
3930cc7b 177 handle = urllib_request.urlopen(req)
918b8b48
GC
178 if handle.headers['Content-Type'] in ['image/jpeg', 'image/png']:
179 return handle
309620de 180 elif handle.info().get('Content-Encoding') == 'gzip':
a403f1b3
JL
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
de072195 188 if "json" == self.format:
a403f1b3 189 res = json.loads(data.decode('utf8'))
abddd419 190 return wrap_response(res, handle.headers)
de072195 191 else:
456ec92b 192 return wrap_response(
a403f1b3 193 data.decode('utf8'), handle.headers)
3930cc7b 194 except urllib_error.HTTPError as e:
de072195 195 if (e.code == 304):
7364ea65 196 return []
de072195 197 else:
aec68959 198 raise TwitterHTTPError(e, uri, self.format, arg_data)
102acdb1 199
7364ea65 200class Twitter(TwitterCall):
201 """
202 The minimalist yet fully featured Twitter API class.
4e9d6343 203
7364ea65 204 Get RESTful data by accessing members of this class. The result
205 is decoded python objects (lists and dicts).
206
51e0b8f1 207 The Twitter API is documented at:
153dee29 208
aec68959
MV
209 http://dev.twitter.com/doc
210
4e9d6343 211
7364ea65 212 Examples::
4e9d6343 213
d09c0dd3 214 t = Twitter(
51e0b8f1 215 auth=OAuth(token, token_key, con_secret, con_secret_key)))
4e9d6343 216
51e0b8f1 217 # Get the public timeline
d09c0dd3 218 t.statuses.public_timeline()
4e9d6343 219
51e0b8f1 220 # Get a particular friend's timeline
d09c0dd3 221 t.statuses.friends_timeline(id="billybob")
4e9d6343 222
51e0b8f1 223 # Also supported (but totally weird)
d09c0dd3
MV
224 t.statuses.friends_timeline.billybob()
225
226 # Update your status
227 t.statuses.update(
228 status="Using @sixohsix's sweet Python Twitter Tools.")
4e9d6343 229
51e0b8f1 230 # Send a direct message
d09c0dd3 231 t.direct_messages.new(
51e0b8f1
MV
232 user="billybob",
233 text="I think yer swell!")
7364ea65 234
d09c0dd3
MV
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")
b0dedfc0 241
69e1f98e 242
153dee29 243 Searching Twitter::
4e9d6343 244
51e0b8f1 245 twitter_search = Twitter(domain="search.twitter.com")
153dee29 246
51e0b8f1
MV
247 # Find the latest search trends
248 twitter_search.trends()
153dee29 249
51e0b8f1
MV
250 # Search for the latest News on #gaza
251 twitter_search.search(q="#gaza")
153dee29 252
7364ea65 253
68b3e2ee
MV
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::
7364ea65 259
51e0b8f1 260 x = twitter.statuses.public_timeline()
7364ea65 261
51e0b8f1
MV
262 # The first 'tweet' in the timeline
263 x[0]
7364ea65 264
51e0b8f1
MV
265 # The screen name of the user who wrote the first 'tweet'
266 x[0]['user']['screen_name']
4e9d6343 267
4e9d6343 268
68b3e2ee
MV
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::
4e9d6343 274
51e0b8f1 275 twitter = Twitter(format="xml")
4e9d6343 276
51e0b8f1
MV
277 The output will not be parsed in any way. It will be a raw string
278 of XML.
68b3e2ee 279
7364ea65 280 """
45688301 281 def __init__(
aec68959 282 self, format="json",
87ad04c3 283 domain="api.twitter.com", secure=True, auth=None,
652c5402 284 api_version=_DEFAULT):
7364ea65 285 """
68b3e2ee
MV
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`
69e1f98e
MV
290 instance::
291
292 twitter = Twitter(auth=OAuth(
293 token, token_secret, consumer_key, consumer_secret))
294
295
68b3e2ee 296 `domain` lets you change the domain you are connecting. By
87ad04c3 297 default it's `api.twitter.com` but `search.twitter.com` may be
68b3e2ee
MV
298 useful too.
299
300 If `secure` is False you will connect with HTTP instead of
301 HTTPS.
302
1cc9ab0b 303 `api_version` is used to set the base uri. By default it's
652c5402 304 '1'. If you are using "search.twitter.com" set this to None.
7364ea65 305 """
d20da7f3
MV
306 if not auth:
307 auth = NoAuth()
308
6c527e72 309 if (format not in ("json", "xml", "")):
68b3e2ee
MV
310 raise ValueError("Unknown data format '%s'" %(format))
311
652c5402
MV
312 if api_version is _DEFAULT:
313 if domain == 'api.twitter.com':
314 api_version = '1'
315 else:
316 api_version = None
317
1be4ce71 318 uriparts = ()
68b3e2ee 319 if api_version:
1be4ce71 320 uriparts += (str(api_version),)
68b3e2ee 321
9a148ed1 322 TwitterCall.__init__(
aec68959 323 self, auth=auth, format=format, domain=domain,
dd648a25 324 callable_cls=TwitterCall,
1be4ce71 325 secure=secure, uriparts=uriparts)
7e43e2ed 326
7364ea65 327
abddd419 328__all__ = ["Twitter", "TwitterError", "TwitterHTTPError", "TwitterResponse"]