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