Skip to content

Commit a219cec

Browse files
committed
Use urllib2 instead of httplib, as httplib does not have proxy support. Fixes #7.
1 parent dc26716 commit a219cec

File tree

1 file changed

+93
-62
lines changed

1 file changed

+93
-62
lines changed

wordnik/wordnik.py

Lines changed: 93 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
1-
"""Python wrapper for the Wordnik API.
1+
"""Python wrapper for the Wordnik API.
22
33
This API implements all the methods described at http://developer.wordnik.com/docs
44
55
maintainer: Robin Walsh (robin@wordnik.com)
66
"""
77

8+
from __future__ import print_function
9+
810
import helpers
9-
import httplib
11+
import collections
1012
try:
1113
import simplejson as json
1214
except ImportError:
1315
import json
1416
import os
15-
import urllib
1617
import urllib2
18+
import urlparse
1719
from optparse import OptionParser
1820
from xml.etree import ElementTree
1921
from pprint import pprint
@@ -23,7 +25,7 @@
2325
DEFAULT_URI = "/v4"
2426
DEFAULT_URL = "http://" + DEFAULT_HOST + DEFAULT_URI
2527
FORMAT_JSON = "json"
26-
FORMAT_XML = "xml"
28+
FORMAT_XML = "xml"
2729
DEFAULT_FORMAT = FORMAT_JSON
2830

2931
class RestfulError(Exception):
@@ -37,129 +39,129 @@ class NoAPIKey(Exception):
3739

3840
class MissingParameters(Exception):
3941
"""Raised if we try to call an API method with required parameters missing"""
40-
42+
4143
class Wordnik(object):
42-
44+
4345
"""
4446
A generic Wordnik object. Use me to interact with the Wordnik API.
45-
47+
4648
All of my methods can be called in multiple ways. All positional
4749
arguments passed into one of my methods (with the exception of "format")
4850
will be substituted for the correponding path parameter, if possible.
4951
For example, consider the "get_word_examples" method. The URI path is:
50-
52+
5153
/word.{format}/{word}/examples
52-
54+
5355
So we can skip format (default format is JSON) and infer that the first
5456
positional argument is the word we want examples for. Hence:
55-
57+
5658
Wordnik.word_get_examples('cat')
57-
59+
5860
All other (non-path) arguments are keyword arguments. The "format"
5961
paramater can be passed in this way as well. Hence:
60-
62+
6163
Wordnik.word_get_examples('cat', format='xml', limit=500)
62-
64+
6365
In the case where you're making a POST, you will need a "body" keyword:
64-
65-
Wordnik.word_list_put(wordListId=1234, body="Some HTTP body")
66+
67+
Wordnik.word_list_put(wordListId=1234, body="Some HTTP body")
6668
"""
67-
68-
69+
70+
6971
def __init__(self, api_key=None, username=None, password=None, beta=False):
7072
"""
7173
Initialize a Wordnik object. You must pass in an API key when
7274
you make a new Wordnik. We don't validate the API key until the
7375
first call against the API is made, at which point you'll find
7476
out if it's good.
75-
77+
7678
If you also pass in a username and password, we will try to get an
7779
auth token so you can use the Wordnik authenticated methods.
7880
Alternatively, you can call Wordnik.authenticate(user, pass)
7981
"""
80-
82+
8183
if api_key is None:
8284
raise NoAPIKey("No API key passed to our constructor")
83-
85+
8486
self._api_key = api_key
8587
self.username = username
8688
self.password = password
8789
self.token = None
8890
self.beta = beta
89-
91+
9092
if username and password:
9193
try:
9294
j = json.loads(self.account_get_authenticate(username, password=password))
9395
self.token = j['token']
9496
except:
9597
raise RestfulError("Could not authenticate with the given username and password")
96-
97-
98+
99+
98100
@classmethod
99101
def _populate_methods(klass):
100102
"""This will create all the methods we need to interact with
101103
the Wordnik API"""
102-
104+
103105
## there is a directory called "endpoints"
104106
basedir = os.path.dirname(__file__)
105107
for filename in os.listdir('{0}/endpoints'.format(basedir)):
106108
j = json.load(open('{0}/endpoints/{1}'.format(basedir, filename)))
107109
Wordnik._create_methods(j)
108-
110+
109111
@classmethod
110112
def _create_methods(klass, jsn):
111113
"""A helper method that will populate this module's namespace
112114
with methods (parsed directlly from the Wordnik API's output)
113115
"""
114116
endpoints = jsn['endPoints']
115-
117+
116118
for method in endpoints:
117119
path = method['path']
118120
for op in method['operations']:
119121
summary = op['summary']
120122
httpmethod = op['httpMethod']
121123
params = op['parameters']
122124
response = op['response']
123-
125+
124126
## a path like: /user.{format}/{username}/wordOfTheDayList/{permalink} (GET)
125127
## will get translated into method: user_get_word_of_the_day_list
126128
methodName = helpers.normalize(path, httpmethod.lower())
127129
docs = helpers.generate_docs(params, response, summary, path)
128130
method = helpers.create_method(methodName, docs, params, path, httpmethod.upper())
129-
131+
130132
setattr( Wordnik, methodName, method )
131-
133+
132134
def _run_command(self, command_name, *args, **kwargs):
133135
if 'api_key' not in kwargs:
134136
kwargs.update( {"api_key": self._api_key} )
135137
if self.token:
136138
kwargs.update( {"auth_token": self.token} )
137-
139+
138140
command = getattr(self, command_name)
139141
(path, headers, body) = helpers.process_args(command._path, command._params, args, kwargs)
140142
httpmethod = command._http
141-
143+
142144
return self._do_http(path, headers, body, httpmethod, beta=self.beta)
143-
145+
144146
def multi(self, calls, **kwargs):
145147
"""Multiple calls, batched. This is a "special case" method
146148
in that it's not automatically generated from the API documentation.
147149
That's because, well, it's undocumented. Here's how you use it:
148-
150+
149151
Wordnik.multi( [call1, call2, call3 ], **kwargs)
150-
152+
151153
where each "call" is (word, resource, {param1: value1, ...} )
152154
So we could form a batch call like so:
153-
155+
154156
calls = [("dog","examples"),("cat","definitions",{"limit":500})]
155-
157+
156158
Wordnik.multi(calls, format="xml")
157-
159+
158160
"""
159-
161+
160162
path = "/word.%s?multi=true" % (kwargs.get('format') or DEFAULT_FORMAT)
161-
162-
163+
164+
163165
for calls_made, call in enumerate(calls):
164166
word = call[0]
165167
resource = call[1]
@@ -172,13 +174,13 @@ def multi(self, calls, **kwargs):
172174
for key,val in otherParams.items():
173175
## Add potential extra params to the URL
174176
path += "&{0}.{1}={2}".format(key, calls_made, val)
175-
177+
176178
headers = { "api_key": self._api_key }
177179
if self.token:
178180
headers.update( {"auth_token": self.token} )
179-
181+
180182
return self._do_http(path, headers, beta=self.beta)
181-
183+
182184
## Some convenience methods to help with api keys and tokens
183185
def get_key(self):
184186
"""Returns the API key we're currently using"""
@@ -199,37 +201,66 @@ def set_auth_token(self, token):
199201
## A convenience method to wrap the retrieval and storage of an auth
200202
## token in case we don't initialize with a username and password.
201203
def authenticate(self, username, password):
202-
"""A convenience method to get an auth token in case the object was
204+
"""A convenience method to get an auth token in case the object was
203205
not instantiated with a username and a password.
204206
"""
205207

206208
try:
207-
resp = self.account_get_authenticate(username, password=password,
209+
resp = self.account_get_authenticate(username, password=password,
208210
format='json')
209211
except:
210212
raise RestfulError("Could not authenticate with the given username and password")
211213
else:
212214
self.token = resp['token']
213215
return True
214-
216+
215217
@staticmethod
216218
def _do_http(uri, headers, body=None, method="GET", beta=False):
217219
"""This wraps the HTTP call. This may get factored out in the future."""
218220
if body:
219-
headers.update( {"Content-Type": "application/json"})
221+
headers.update({"Content-Type": "application/json"})
220222
full_uri = DEFAULT_URI + uri
221-
conn = httplib.HTTPConnection(DEFAULT_HOST)
222-
if beta:
223-
conn = httplib.HTTPConnection("beta.wordnik.com")
224-
conn.request(method, full_uri, body, headers)
225-
response = conn.getresponse()
226-
if response.status == httplib.OK:
227-
text = response.read()
228-
format_ = headers.get('format', DEFAULT_FORMAT)
229-
if format_ == FORMAT_JSON:
230-
return json.loads(text)
231-
elif format_ == FORMAT_XML:
232-
return ElementTree.XML(text)
233-
else:
234-
print >> stderr, "{0}: {1}".format(response.status, response.reason)
235-
return None
223+
host = DEFAULT_HOST if not beta else 'beta.wordnik.com'
224+
# construct a URL for urllib2
225+
url = urlparse.urlunsplit([
226+
'http', # scheme
227+
host,
228+
full_uri, # path
229+
'', # query
230+
'', # fragment
231+
])
232+
request = MethodRequest(url, data=body, headers=headers,
233+
method=method)
234+
try:
235+
response = urllib2.urlopen(request)
236+
format = response.headers.get('format', DEFAULT_FORMAT)
237+
# return None when format is unknown
238+
unknown_content_handler = lambda source: None
239+
handler_map = collections.defaultdict(
240+
lambda: unknown_content_handler,
241+
{
242+
FORMAT_JSON: json.load,
243+
FORMAT_XML: ElementTree.parse,
244+
}
245+
)
246+
content_handler = handler_map[format]
247+
return content_handler(response)
248+
except urllib2.HTTPError as err:
249+
msg = "HTTP error {err.code}: {err.msg}".format(**vars())
250+
print(msg, file=stderr)
251+
except urllib2.URLError as err:
252+
print("Error: {err.reason}".format(**vars()), file=stderr)
253+
254+
class MethodRequest(urllib2.Request):
255+
def __init__(self, *args, **kwargs):
256+
"""
257+
Construct a MethodRequest. Usage is the same as for
258+
`urllib2.Request` except it also takes an optional `method`
259+
keyword argument. If supplied, `method` will be used instead of
260+
the default.
261+
"""
262+
self.method = kwargs.pop('method', None)
263+
return urllib2.Request.__init__(self, *args, **kwargs)
264+
265+
def get_method(self):
266+
return self.method or urllib2.Request.get_method(self)

0 commit comments

Comments
 (0)