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