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