]> jfr.im git - z_archive/twitter.git/blob - twitter/api.py
Fix a test.
[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 fmt = ("." + self.format) if self.format else ""
40 return (
41 "Twitter sent status %i for URL: %s%s using parameters: "
42 "(%s)\ndetails: %s" %(
43 self.e.code, self.uri, fmt, self.uriparts,
44 self.response_data))
45
46 class 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 """
56 def __init__(self, headers):
57 self.headers = headers
58
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
74 def wrap_response(response, headers):
75 response_typ = type(response)
76 if response_typ is bool:
77 # HURF DURF MY NAME IS PYTHON AND I CAN'T SUBCLASS bool.
78 response_typ = int
79
80 class WrappedTwitterResponse(response_typ, TwitterResponse):
81 __doc__ = TwitterResponse.__doc__
82
83 def __init__(self, response, headers):
84 response_typ.__init__(self, response)
85 TwitterResponse.__init__(self, headers)
86 def __new__(cls, response, headers):
87 return response_typ.__new__(cls, response)
88
89
90 return WrappedTwitterResponse(response, headers)
91
92
93
94 class TwitterCall(object):
95
96 def __init__(
97 self, auth, format, domain, callable_cls, uri="",
98 uriparts=None, secure=True):
99 self.auth = auth
100 self.format = format
101 self.domain = domain
102 self.callable_cls = callable_cls
103 self.uri = uri
104 self.uriparts = uriparts
105 self.secure = secure
106
107 def __getattr__(self, k):
108 try:
109 return object.__getattr__(self, k)
110 except AttributeError:
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)
121
122 def __call__(self, **kwargs):
123 # Build the uri.
124 uriparts = []
125 for uripart in self.uriparts:
126 # If this part matches a keyword argument, use the
127 # supplied value otherwise, just use the part.
128 uriparts.append(str(kwargs.pop(uripart, uripart)))
129 uri = '/'.join(uriparts)
130
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
138
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.
141 id = kwargs.pop('id', None)
142 if id:
143 uri += "/%s" %(id)
144
145 secure_str = ''
146 if self.secure:
147 secure_str = 's'
148 dot = ""
149 if self.format:
150 dot = "."
151 uriBase = "http%s://%s/%s%s%s" %(
152 secure_str, self.domain, uri, dot, self.format)
153
154 headers = {}
155 if self.auth:
156 headers.update(self.auth.generate_headers())
157 arg_data = self.auth.encode_params(uriBase, method, kwargs)
158 if method == 'GET':
159 uriBase += '?' + arg_data
160 body = None
161 else:
162 body = arg_data.encode('utf8')
163
164 req = urllib_request.Request(uriBase, body, headers)
165 return self._handle_response(req, uri, arg_data)
166
167 def _handle_response(self, req, uri, arg_data):
168 try:
169 handle = urllib_request.urlopen(req)
170 if "json" == self.format:
171 res = json.loads(handle.read().decode('utf8'))
172 return wrap_response(res, handle.headers)
173 else:
174 return wrap_response(
175 handle.read().decode('utf8'), handle.headers)
176 except urllib_error.HTTPError as e:
177 if (e.code == 304):
178 return []
179 else:
180 raise TwitterHTTPError(e, uri, self.format, arg_data)
181
182 class Twitter(TwitterCall):
183 """
184 The minimalist yet fully featured Twitter API class.
185
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:
190
191 http://dev.twitter.com/doc
192
193
194 Examples::
195
196 twitter = Twitter(
197 auth=OAuth(token, token_key, con_secret, con_secret_key)))
198
199 # Get the public timeline
200 twitter.statuses.public_timeline()
201
202 # Get a particular friend's timeline
203 twitter.statuses.friends_timeline(id="billybob")
204
205 # Also supported (but totally weird)
206 twitter.statuses.friends_timeline.billybob()
207
208 # Send a direct message
209 twitter.direct_messages.new(
210 user="billybob",
211 text="I think yer swell!")
212
213 # Get the members of a particular list of a particular friend
214 twitter.user.listname.members(user="billybob", listname="billysbuds")
215
216
217 Searching Twitter::
218
219 twitter_search = Twitter(domain="search.twitter.com")
220
221 # Find the latest search trends
222 twitter_search.trends()
223
224 # Search for the latest News on #gaza
225 twitter_search.search(q="#gaza")
226
227
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::
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']
241
242
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::
248
249 twitter = Twitter(format="xml")
250
251 The output will not be parsed in any way. It will be a raw string
252 of XML.
253
254 """
255 def __init__(
256 self, format="json",
257 domain="api.twitter.com", secure=True, auth=None,
258 api_version=_DEFAULT):
259 """
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`
264 instance::
265
266 twitter = Twitter(auth=OAuth(
267 token, token_secret, consumer_key, consumer_secret))
268
269
270 `domain` lets you change the domain you are connecting. By
271 default it's `api.twitter.com` but `search.twitter.com` may be
272 useful too.
273
274 If `secure` is False you will connect with HTTP instead of
275 HTTPS.
276
277 `api_version` is used to set the base uri. By default it's
278 '1'. If you are using "search.twitter.com" set this to None.
279 """
280 if not auth:
281 auth = NoAuth()
282
283 if (format not in ("json", "xml", "")):
284 raise ValueError("Unknown data format '%s'" %(format))
285
286 if api_version is _DEFAULT:
287 if domain == 'api.twitter.com':
288 api_version = '1'
289 else:
290 api_version = None
291
292 uriparts = ()
293 if api_version:
294 uriparts += (str(api_version),)
295
296 TwitterCall.__init__(
297 self, auth=auth, format=format, domain=domain,
298 callable_cls=TwitterCall,
299 secure=secure, uriparts=uriparts)
300
301
302 __all__ = ["Twitter", "TwitterError", "TwitterHTTPError", "TwitterResponse"]