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