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