1
- """Python wrapper for the Wordnik API.
1
+ """Python wrapper for the Wordnik API.
2
2
3
3
This API implements all the methods described at http://developer.wordnik.com/docs
4
4
5
5
maintainer: Robin Walsh (robin@wordnik.com)
6
6
"""
7
7
8
+ from __future__ import print_function
9
+
8
10
import helpers
9
- import httplib
11
+ import collections
10
12
try :
11
13
import simplejson as json
12
14
except ImportError :
13
15
import json
14
16
import os
15
- import urllib
16
17
import urllib2
18
+ import urlparse
17
19
from optparse import OptionParser
18
20
from xml .etree import ElementTree
19
21
from pprint import pprint
23
25
DEFAULT_URI = "/v4"
24
26
DEFAULT_URL = "http://" + DEFAULT_HOST + DEFAULT_URI
25
27
FORMAT_JSON = "json"
26
- FORMAT_XML = "xml"
28
+ FORMAT_XML = "xml"
27
29
DEFAULT_FORMAT = FORMAT_JSON
28
30
29
31
class RestfulError (Exception ):
@@ -37,129 +39,129 @@ class NoAPIKey(Exception):
37
39
38
40
class MissingParameters (Exception ):
39
41
"""Raised if we try to call an API method with required parameters missing"""
40
-
42
+
41
43
class Wordnik (object ):
42
-
44
+
43
45
"""
44
46
A generic Wordnik object. Use me to interact with the Wordnik API.
45
-
47
+
46
48
All of my methods can be called in multiple ways. All positional
47
49
arguments passed into one of my methods (with the exception of "format")
48
50
will be substituted for the correponding path parameter, if possible.
49
51
For example, consider the "get_word_examples" method. The URI path is:
50
-
52
+
51
53
/word.{format}/{word}/examples
52
-
54
+
53
55
So we can skip format (default format is JSON) and infer that the first
54
56
positional argument is the word we want examples for. Hence:
55
-
57
+
56
58
Wordnik.word_get_examples('cat')
57
-
59
+
58
60
All other (non-path) arguments are keyword arguments. The "format"
59
61
paramater can be passed in this way as well. Hence:
60
-
62
+
61
63
Wordnik.word_get_examples('cat', format='xml', limit=500)
62
-
64
+
63
65
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")
66
68
"""
67
-
68
-
69
+
70
+
69
71
def __init__ (self , api_key = None , username = None , password = None , beta = False ):
70
72
"""
71
73
Initialize a Wordnik object. You must pass in an API key when
72
74
you make a new Wordnik. We don't validate the API key until the
73
75
first call against the API is made, at which point you'll find
74
76
out if it's good.
75
-
77
+
76
78
If you also pass in a username and password, we will try to get an
77
79
auth token so you can use the Wordnik authenticated methods.
78
80
Alternatively, you can call Wordnik.authenticate(user, pass)
79
81
"""
80
-
82
+
81
83
if api_key is None :
82
84
raise NoAPIKey ("No API key passed to our constructor" )
83
-
85
+
84
86
self ._api_key = api_key
85
87
self .username = username
86
88
self .password = password
87
89
self .token = None
88
90
self .beta = beta
89
-
91
+
90
92
if username and password :
91
93
try :
92
94
j = json .loads (self .account_get_authenticate (username , password = password ))
93
95
self .token = j ['token' ]
94
96
except :
95
97
raise RestfulError ("Could not authenticate with the given username and password" )
96
-
97
-
98
+
99
+
98
100
@classmethod
99
101
def _populate_methods (klass ):
100
102
"""This will create all the methods we need to interact with
101
103
the Wordnik API"""
102
-
104
+
103
105
## there is a directory called "endpoints"
104
106
basedir = os .path .dirname (__file__ )
105
107
for filename in os .listdir ('{0}/endpoints' .format (basedir )):
106
108
j = json .load (open ('{0}/endpoints/{1}' .format (basedir , filename )))
107
109
Wordnik ._create_methods (j )
108
-
110
+
109
111
@classmethod
110
112
def _create_methods (klass , jsn ):
111
113
"""A helper method that will populate this module's namespace
112
114
with methods (parsed directlly from the Wordnik API's output)
113
115
"""
114
116
endpoints = jsn ['endPoints' ]
115
-
117
+
116
118
for method in endpoints :
117
119
path = method ['path' ]
118
120
for op in method ['operations' ]:
119
121
summary = op ['summary' ]
120
122
httpmethod = op ['httpMethod' ]
121
123
params = op ['parameters' ]
122
124
response = op ['response' ]
123
-
125
+
124
126
## a path like: /user.{format}/{username}/wordOfTheDayList/{permalink} (GET)
125
127
## will get translated into method: user_get_word_of_the_day_list
126
128
methodName = helpers .normalize (path , httpmethod .lower ())
127
129
docs = helpers .generate_docs (params , response , summary , path )
128
130
method = helpers .create_method (methodName , docs , params , path , httpmethod .upper ())
129
-
131
+
130
132
setattr ( Wordnik , methodName , method )
131
-
133
+
132
134
def _run_command (self , command_name , * args , ** kwargs ):
133
135
if 'api_key' not in kwargs :
134
136
kwargs .update ( {"api_key" : self ._api_key } )
135
137
if self .token :
136
138
kwargs .update ( {"auth_token" : self .token } )
137
-
139
+
138
140
command = getattr (self , command_name )
139
141
(path , headers , body ) = helpers .process_args (command ._path , command ._params , args , kwargs )
140
142
httpmethod = command ._http
141
-
143
+
142
144
return self ._do_http (path , headers , body , httpmethod , beta = self .beta )
143
-
145
+
144
146
def multi (self , calls , ** kwargs ):
145
147
"""Multiple calls, batched. This is a "special case" method
146
148
in that it's not automatically generated from the API documentation.
147
149
That's because, well, it's undocumented. Here's how you use it:
148
-
150
+
149
151
Wordnik.multi( [call1, call2, call3 ], **kwargs)
150
-
152
+
151
153
where each "call" is (word, resource, {param1: value1, ...} )
152
154
So we could form a batch call like so:
153
-
155
+
154
156
calls = [("dog","examples"),("cat","definitions",{"limit":500})]
155
-
157
+
156
158
Wordnik.multi(calls, format="xml")
157
-
159
+
158
160
"""
159
-
161
+
160
162
path = "/word.%s?multi=true" % (kwargs .get ('format' ) or DEFAULT_FORMAT )
161
-
162
-
163
+
164
+
163
165
for calls_made , call in enumerate (calls ):
164
166
word = call [0 ]
165
167
resource = call [1 ]
@@ -172,13 +174,13 @@ def multi(self, calls, **kwargs):
172
174
for key ,val in otherParams .items ():
173
175
## Add potential extra params to the URL
174
176
path += "&{0}.{1}={2}" .format (key , calls_made , val )
175
-
177
+
176
178
headers = { "api_key" : self ._api_key }
177
179
if self .token :
178
180
headers .update ( {"auth_token" : self .token } )
179
-
181
+
180
182
return self ._do_http (path , headers , beta = self .beta )
181
-
183
+
182
184
## Some convenience methods to help with api keys and tokens
183
185
def get_key (self ):
184
186
"""Returns the API key we're currently using"""
@@ -199,37 +201,66 @@ def set_auth_token(self, token):
199
201
## A convenience method to wrap the retrieval and storage of an auth
200
202
## token in case we don't initialize with a username and password.
201
203
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
203
205
not instantiated with a username and a password.
204
206
"""
205
207
206
208
try :
207
- resp = self .account_get_authenticate (username , password = password ,
209
+ resp = self .account_get_authenticate (username , password = password ,
208
210
format = 'json' )
209
211
except :
210
212
raise RestfulError ("Could not authenticate with the given username and password" )
211
213
else :
212
214
self .token = resp ['token' ]
213
215
return True
214
-
216
+
215
217
@staticmethod
216
218
def _do_http (uri , headers , body = None , method = "GET" , beta = False ):
217
219
"""This wraps the HTTP call. This may get factored out in the future."""
218
220
if body :
219
- headers .update ( {"Content-Type" : "application/json" })
221
+ headers .update ({"Content-Type" : "application/json" })
220
222
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