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