]> jfr.im git - z_archive/twitter.git/blame - twitter/api.py
make inversion optional
[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
612ececa 8from twitter.twitter_globals import POST_ACTIONS
aec68959 9from twitter.auth import NoAuth
4e9d6343 10
2ee65672
MV
11import re
12
4b12a3a0 13try:
f1a8ed67 14 import json
4b12a3a0 15except ImportError:
f1a8ed67 16 import simplejson as json
17
652c5402
MV
18class _DEFAULT(object):
19 pass
20
5251ea48 21class TwitterError(Exception):
21e3bd23 22 """
64a8d213
B
23 Base Exception thrown by the Twitter object when there is a
24 general error interacting with the API.
21e3bd23 25 """
5251ea48 26 pass
27
64a8d213
B
28class TwitterHTTPError(TwitterError):
29 """
30 Exception thrown by the Twitter object when there is an
31 HTTP error interacting with twitter.com.
32 """
1be4ce71 33 def __init__(self, e, uri, format, uriparts):
4b12a3a0
MV
34 self.e = e
35 self.uri = uri
36 self.format = format
37 self.uriparts = uriparts
c7dd86d1 38 self.response_data = self.e.fp.read()
64a8d213
B
39
40 def __str__(self):
57b54437 41 fmt = ("." + self.format) if self.format else ""
68b3e2ee 42 return (
57b54437 43 "Twitter sent status %i for URL: %s%s using parameters: "
68b3e2ee 44 "(%s)\ndetails: %s" %(
57b54437 45 self.e.code, self.uri, fmt, self.uriparts,
c7dd86d1 46 self.response_data))
64a8d213 47
84d0a294
MV
48class TwitterResponse(object):
49 """
50 Response from a twitter request. Behaves like a list or a string
51 (depending on requested format) but it has a few other interesting
52 attributes.
53
54 `headers` gives you access to the response headers as an
55 httplib.HTTPHeaders instance. You can do
56 `response.headers.getheader('h')` to retrieve a header.
57 """
aef72b31 58 def __init__(self, headers):
84d0a294
MV
59 self.headers = headers
60
84d0a294
MV
61 @property
62 def rate_limit_remaining(self):
63 """
64 Remaining requests in the current rate-limit.
65 """
66 return int(self.headers.getheader('X-RateLimit-Remaining'))
67
68 @property
69 def rate_limit_reset(self):
70 """
71 Time in UTC epoch seconds when the rate limit will reset.
72 """
73 return int(self.headers.getheader('X-RateLimit-Reset'))
74
75
abddd419
MV
76def wrap_response(response, headers):
77 response_typ = type(response)
ce92ec77
MV
78 if response_typ is bool:
79 # HURF DURF MY NAME IS PYTHON AND I CAN'T SUBCLASS bool.
80 response_typ = int
12bba6ac
MV
81
82 class WrappedTwitterResponse(response_typ, TwitterResponse):
abddd419
MV
83 __doc__ = TwitterResponse.__doc__
84
c77b5e4b
SK
85 def __init__(self, response, headers):
86 response_typ.__init__(self, response)
87 TwitterResponse.__init__(self, headers)
94803fc9 88 def __new__(cls, response, headers):
89 return response_typ.__new__(cls, response)
90
c77b5e4b
SK
91
92 return WrappedTwitterResponse(response, headers)
abddd419 93
0d6c0646
MV
94
95
7364ea65 96class TwitterCall(object):
dd648a25 97
c8d451e8 98 def __init__(
dd648a25 99 self, auth, format, domain, callable_cls, uri="",
7e43e2ed 100 uriparts=None, secure=True):
568331a9 101 self.auth = auth
a55e6a11 102 self.format = format
153dee29 103 self.domain = domain
dd648a25 104 self.callable_cls = callable_cls
7364ea65 105 self.uri = uri
b0dedfc0 106 self.uriparts = uriparts
9a148ed1 107 self.secure = secure
fd2bc885 108
7364ea65 109 def __getattr__(self, k):
110 try:
111 return object.__getattr__(self, k)
112 except AttributeError:
e748eed8 113 def extend_call(arg):
114 return self.callable_cls(
115 auth=self.auth, format=self.format, domain=self.domain,
116 callable_cls=self.callable_cls, uriparts=self.uriparts \
117 + (arg,),
118 secure=self.secure)
119 if k == "_":
120 return extend_call
121 else:
122 return extend_call(k)
fd2bc885 123
7364ea65 124 def __call__(self, **kwargs):
aec68959 125 # Build the uri.
1be4ce71 126 uriparts = []
b0dedfc0 127 for uripart in self.uriparts:
aec68959
MV
128 # If this part matches a keyword argument, use the
129 # supplied value otherwise, just use the part.
f7e63802
MV
130 uriparts.append(str(kwargs.pop(uripart, uripart)))
131 uri = '/'.join(uriparts)
1be4ce71 132
57b54437 133 method = kwargs.pop('_method', None)
134 if not method:
135 method = "GET"
136 for action in POST_ACTIONS:
2ee65672 137 if re.search("%s(/\d+)?$" % action, uri):
57b54437 138 method = "POST"
139 break
612ececa 140
aec68959
MV
141 # If an id kwarg is present and there is no id to fill in in
142 # the list of uriparts, assume the id goes at the end.
da45d039
MV
143 id = kwargs.pop('id', None)
144 if id:
145 uri += "/%s" %(id)
4e9d6343 146
568331a9
MH
147 secure_str = ''
148 if self.secure:
149 secure_str = 's'
6c527e72 150 dot = ""
1be4ce71 151 if self.format:
6c527e72
MV
152 dot = "."
153 uriBase = "http%s://%s/%s%s%s" %(
154 secure_str, self.domain, uri, dot, self.format)
568331a9 155
5b8b1ead 156 headers = {}
1be4ce71 157 if self.auth:
568331a9 158 headers.update(self.auth.generate_headers())
1be4ce71
MV
159 arg_data = self.auth.encode_params(uriBase, method, kwargs)
160 if method == 'GET':
161 uriBase += '?' + arg_data
162 body = None
163 else:
8eb73aab 164 body = arg_data.encode('utf8')
1be4ce71 165
3930cc7b 166 req = urllib_request.Request(uriBase, body, headers)
dd648a25 167 return self._handle_response(req, uri, arg_data)
102acdb1 168
dd648a25 169 def _handle_response(self, req, uri, arg_data):
7364ea65 170 try:
3930cc7b 171 handle = urllib_request.urlopen(req)
de072195 172 if "json" == self.format:
456ec92b 173 res = json.loads(handle.read().decode('utf8'))
abddd419 174 return wrap_response(res, handle.headers)
de072195 175 else:
456ec92b
MV
176 return wrap_response(
177 handle.read().decode('utf8'), handle.headers)
3930cc7b 178 except urllib_error.HTTPError as e:
de072195 179 if (e.code == 304):
7364ea65 180 return []
de072195 181 else:
aec68959 182 raise TwitterHTTPError(e, uri, self.format, arg_data)
102acdb1 183
7364ea65 184class Twitter(TwitterCall):
185 """
186 The minimalist yet fully featured Twitter API class.
4e9d6343 187
7364ea65 188 Get RESTful data by accessing members of this class. The result
189 is decoded python objects (lists and dicts).
190
191 The Twitter API is documented here:
153dee29 192
aec68959
MV
193 http://dev.twitter.com/doc
194
4e9d6343 195
7364ea65 196 Examples::
4e9d6343 197
69e1f98e
MV
198 twitter = Twitter(
199 auth=OAuth(token, token_key, con_secret, con_secret_key)))
4e9d6343 200
7364ea65 201 # Get the public timeline
202 twitter.statuses.public_timeline()
4e9d6343 203
7364ea65 204 # Get a particular friend's timeline
205 twitter.statuses.friends_timeline(id="billybob")
4e9d6343 206
7364ea65 207 # Also supported (but totally weird)
208 twitter.statuses.friends_timeline.billybob()
4e9d6343 209
7364ea65 210 # Send a direct message
211 twitter.direct_messages.new(
212 user="billybob",
213 text="I think yer swell!")
214
b0dedfc0
MV
215 # Get the members of a particular list of a particular friend
216 twitter.user.listname.members(user="billybob", listname="billysbuds")
217
69e1f98e 218
153dee29 219 Searching Twitter::
4e9d6343 220
0b486eda 221 twitter_search = Twitter(domain="search.twitter.com")
153dee29 222
0b486eda
HN
223 # Find the latest search trends
224 twitter_search.trends()
153dee29 225
0b486eda
HN
226 # Search for the latest News on #gaza
227 twitter_search.search(q="#gaza")
153dee29 228
7364ea65 229
68b3e2ee
MV
230 Using the data returned
231 -----------------------
232
233 Twitter API calls return decoded JSON. This is converted into
234 a bunch of Python lists, dicts, ints, and strings. For example::
7364ea65 235
236 x = twitter.statuses.public_timeline()
237
238 # The first 'tweet' in the timeline
239 x[0]
240
241 # The screen name of the user who wrote the first 'tweet'
242 x[0]['user']['screen_name']
4e9d6343 243
4e9d6343 244
68b3e2ee
MV
245 Getting raw XML data
246 --------------------
247
248 If you prefer to get your Twitter data in XML format, pass
249 format="xml" to the Twitter object when you instantiate it::
4e9d6343 250
a55e6a11 251 twitter = Twitter(format="xml")
4e9d6343 252
a55e6a11 253 The output will not be parsed in any way. It will be a raw string
254 of XML.
68b3e2ee 255
7364ea65 256 """
45688301 257 def __init__(
aec68959 258 self, format="json",
87ad04c3 259 domain="api.twitter.com", secure=True, auth=None,
652c5402 260 api_version=_DEFAULT):
7364ea65 261 """
68b3e2ee
MV
262 Create a new twitter API connector.
263
264 Pass an `auth` parameter to use the credentials of a specific
265 user. Generally you'll want to pass an `OAuth`
69e1f98e
MV
266 instance::
267
268 twitter = Twitter(auth=OAuth(
269 token, token_secret, consumer_key, consumer_secret))
270
271
68b3e2ee 272 `domain` lets you change the domain you are connecting. By
87ad04c3 273 default it's `api.twitter.com` but `search.twitter.com` may be
68b3e2ee
MV
274 useful too.
275
276 If `secure` is False you will connect with HTTP instead of
277 HTTPS.
278
1cc9ab0b 279 `api_version` is used to set the base uri. By default it's
652c5402 280 '1'. If you are using "search.twitter.com" set this to None.
7364ea65 281 """
d20da7f3
MV
282 if not auth:
283 auth = NoAuth()
284
6c527e72 285 if (format not in ("json", "xml", "")):
68b3e2ee
MV
286 raise ValueError("Unknown data format '%s'" %(format))
287
652c5402
MV
288 if api_version is _DEFAULT:
289 if domain == 'api.twitter.com':
290 api_version = '1'
291 else:
292 api_version = None
293
1be4ce71 294 uriparts = ()
68b3e2ee 295 if api_version:
1be4ce71 296 uriparts += (str(api_version),)
68b3e2ee 297
9a148ed1 298 TwitterCall.__init__(
aec68959 299 self, auth=auth, format=format, domain=domain,
dd648a25 300 callable_cls=TwitterCall,
1be4ce71 301 secure=secure, uriparts=uriparts)
7e43e2ed 302
7364ea65 303
abddd419 304__all__ = ["Twitter", "TwitterError", "TwitterHTTPError", "TwitterResponse"]