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