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