├── restclient ├── test │ ├── __init__.py │ └── test_everything.py └── __init__.py ├── .gitignore ├── .travis.yml ├── setup.py └── README.markdown /restclient/test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | build 3 | dist 4 | .coverage 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.6" 4 | - "2.7" 5 | install: 6 | - pip install nose 7 | - pip install httplib2 8 | - pip install HTTPretty 9 | script: 10 | - python setup.py nosetests 11 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2007, Columbia Center For New Media Teaching And Learning (CCNMTL) 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # * Redistributions of source code must retain the above copyright 7 | # notice, this list of conditions and the following disclaimer. 8 | # * Redistributions in binary form must reproduce the above copyright 9 | # notice, this list of conditions and the following disclaimer in the 10 | # documentation and/or other materials provided with the distribution. 11 | # * Neither the name of the CCNMTL nor the 12 | # names of its contributors may be used to endorse or promote products 13 | # derived from this software without specific prior written permission. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY CCNMTL ``AS IS'' AND ANY 16 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | # DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 19 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | from setuptools import setup, find_packages 27 | 28 | setup( 29 | name="restclient", 30 | version="0.11.0", 31 | author="Anders Pearson", 32 | author_email="anders@columbia.edu", 33 | url="http://github.com/thraxil/restclient/", 34 | description="convenient library for writing REST clients", 35 | long_description="makes it easy to invoke REST services properly", 36 | install_requires = ["httplib2"], 37 | scripts = [], 38 | license = "BSD", 39 | platforms = ["any"], 40 | zip_safe=False, 41 | packages=find_packages(), 42 | test_suite='nose.collector', 43 | ) 44 | -------------------------------------------------------------------------------- /restclient/test/test_everything.py: -------------------------------------------------------------------------------- 1 | # Copyright 2007, Columbia Center For New Media Teaching And Learning (CCNMTL) 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without 5 | # modification, are permitted provided that the following conditions are met: 6 | # * Redistributions of source code must retain the above copyright 7 | # notice, this list of conditions and the following disclaimer. 8 | # * Redistributions in binary form must reproduce the above copyright 9 | # notice, this list of conditions and the following disclaimer in the 10 | # documentation and/or other materials provided with the distribution. 11 | # * Neither the name of the CCNMTL nor the 12 | # names of its contributors may be used to endorse or promote products 13 | # derived from this software without specific prior written permission. 14 | # 15 | # THIS SOFTWARE IS PROVIDED BY CCNMTL ``AS IS'' AND ANY 16 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | # DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 19 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | """ Basic Test Suite for restclient 27 | 28 | Requires nose and HTTPretty to run 29 | """ 30 | 31 | from restclient import GET, POST 32 | from httpretty import HTTPretty, httprettified 33 | 34 | test_url = "http://example.com/" 35 | default_body = "Simple response" 36 | 37 | 38 | @httprettified 39 | def test_get(): 40 | HTTPretty.register_uri( 41 | HTTPretty.GET, 42 | test_url, 43 | body=default_body, 44 | content_type="text/html" 45 | ) 46 | r = GET(test_url) 47 | assert r == default_body 48 | assert HTTPretty.last_request.method == "GET" 49 | 50 | 51 | @httprettified 52 | def test_post(): 53 | HTTPretty.register_uri( 54 | HTTPretty.POST, 55 | test_url, 56 | body=default_body, 57 | content_type="text/html" 58 | ) 59 | r = POST( 60 | test_url, 61 | params={'value': 'store this'}, 62 | accept=["text/plain", "text/html"], 63 | async=False) 64 | assert r == default_body 65 | assert HTTPretty.last_request.method == "POST" 66 | assert HTTPretty.last_request.headers['accept'] == "text/plain,text/html" 67 | 68 | 69 | if __name__ == "__main__": 70 | import nose 71 | nose.main() 72 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | Restclient 2 | ========== 3 | 4 | Status Note 5 | ----------- 6 | 7 | restclient is still maintained and in production use by the 8 | author. However, I would not recommend starting new projects with 9 | it. restclient was written years ago when the only options available 10 | were raw httplib2/urllib type libraries and made my (and others') life 11 | much easier. Now though, the `requests` library is available, better 12 | documented, better tested, and more widely understood. `requests` does 13 | everything restclient does so if you should use it instead. 14 | 15 | I'm happy to take patches and bugfixes on restclient and will try to 16 | keep it alive and available for anyone who is already using and 17 | doesn't feel the need to switch. But there will probably not be any 18 | new development on restclient. 19 | 20 | Introduction 21 | ------------ 22 | 23 | A helper library to make writing REST clients in python extremely 24 | simple. Specifically, while httplib2 and similar libraries are very 25 | efficient and powerful, doing something very simple like making a POST 26 | request to a url with a couple parameters can involve quite a bit of 27 | code. Restclient tries to make these common tasks simple. It does not 28 | try to do everything though. If you need to construct a request in a 29 | very particular way, are expecting a large result, or need better 30 | error handling, nothing is stopping you from using one of the lower 31 | level libraries directly for that. 32 | 33 | Installation 34 | ============ 35 | 36 | If you have setuptools installed, just do "easy_install restclient" 37 | 38 | 39 | Documentation 40 | ============= 41 | 42 | Restclient is very simple so the main documentation is in docstrings in the code itself. this page just serves as a quick starter. 43 | 44 | 45 | from restclient import GET, POST, PUT, DELETE 46 | r = GET("http://www.example.com/") # makes a GET request to the url. returns a string 47 | POST("http://www.example.com/") # makes a POST request 48 | PUT("http://www.example.com/") # makes a PUT request 49 | DELETE("http://www.example.com/") # makes a DELETE request 50 | POST("http://www.example.com/",params={'foo' : 'bar'}) # passes params along 51 | POST("http://www.example.com/",headers={'foo' : 'bar'}) # sends additional HTTP headers with the request 52 | POST("http://www.example.com/",accept=['application/xml','text/plain']) # specify HTTP Accept: headers 53 | POST("http://www.example.com/",credentials=('username','password')) # HTTP Auth 54 | 55 | Restclient also handles multipart file uploads nicely: 56 | 57 | f = open("foo.txt").read() 58 | POST("http://www.example.com/",files={'file1' : {'file' : f, 'filename' : 'foo.txt'}}) 59 | 60 | 61 | by default, POST(), PUT(), and DELETE() make their requests asynchronously. IE, they spawn a new thread to do the request and return immediately. GET() is synchronous. You can change this behavior with the 'async' parameter: 62 | 63 | POST("http://www.example.com/",async=False) # will wait for the request to complete before returning 64 | 65 | Doing an asynchronous GET would be silly and pointless so I won't give an example of that but I'm sure you could figure it out. 66 | 67 | With any of those, if you add a return_resp=True argument, restclient 68 | will make the request like normal and then return the raw httplib2 69 | response object. You'll have to extract the response body yourself, 70 | but you'll also have access to the HTTP response codes, etc. 71 | 72 | For finer grained control over the httplib2 library use the keyword 'httplib_params' 73 | to supply a dict of key/value pairs. This will be passed unadulterated as 74 | parameters to httplib2.Http(). In addition, you can now include a debuglevel as 75 | a key to 'httplib_params' to see the debug output from httplib2. The value 76 | is an integer for the amount of debug output desired. The debug level is set 77 | for the one REST call and then reset to the previous value which allows 78 | general debugging to be enabled and then override with detailed debugging 79 | for specific calls. 80 | 81 | The handling of JSON data in POST and PUT requests is now handled by setting 82 | the 'params' option to the data structure and adding a Content-Type header 83 | set to 'application/json'. For example: 84 | 85 | POST("http://www.example.com", params={'name':'Some User', 'action':'create'}, headers={'Content-Type': 'application/json'}) 86 | 87 | 88 | Credits 89 | ======= 90 | 91 | written by Anders Pearson at the [Columbia Center For New Media Teaching And Learning](http://ccnmtl.columbia.edu/). 92 | 93 | httplib2 debugging and JSON support added by Gerard Hickey (ghickey@ebay.com) 94 | -------------------------------------------------------------------------------- /restclient/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Copyright (c) 2007 4 | # Columbia Center For New Media Teaching And Learning (CCNMTL) 5 | # All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without 8 | # modification, are permitted provided that the following conditions are met: 9 | # * Redistributions of source code must retain the above copyright 10 | # notice, this list of conditions and the following disclaimer. 11 | # * Redistributions in binary form must reproduce the above copyright 12 | # notice, this list of conditions and the following disclaimer in the 13 | # documentation and/or other materials provided with the distribution. 14 | # * Neither the name of the CCNMTL nor the 15 | # names of its contributors may be used to endorse or promote products 16 | # derived from this software without specific prior written permission. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY CCNMTL ``AS IS'' AND ANY 19 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | # DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 22 | # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 25 | # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | 30 | """ 31 | REST client convenience library 32 | 33 | This module contains everything that's needed for a nice, simple REST client. 34 | 35 | the main function it provides is rest_invoke(), which will make an HTTP 36 | request to a REST server. it allows for all kinds of nice things like: 37 | 38 | * alternative verbs: POST, PUT, DELETE, etc. 39 | * parameters 40 | * file uploads (multipart/form-data) 41 | * proper unicode handling 42 | * Accept: headers 43 | * ability to specify other headers 44 | 45 | this library is mostly a wrapper around the standard urllib and 46 | httplib2 functionality, but also includes file upload support via a 47 | python cookbook recipe 48 | (http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/146306) and 49 | has had additional work to make sure high unicode characters in the 50 | parameters or headers don't cause any UnicodeEncodeError problems. 51 | 52 | Joe Gregario's httplib2 library is required. It can be easy_installed, 53 | or downloaded nose is required to run the unit tests. 54 | 55 | CHANGESET: 56 | * 2012-11-17 - Anders - flake8 cleanup, version bump 57 | * 2012-11-16 - hickey - added support for sending JSON data 58 | * 2012-11-16 - hickey - added debuglevel to httplib_params 59 | * 2012-04-16 - alexmock - added httplib_params for fine-grained control of 60 | httplib2 61 | * 2010-10-11 - Anders - added 'credentials' parameter to support HTTP Auth 62 | * 2010-07-25 - Anders - merged Greg Baker's patch for 63 | https urls 64 | * 2007-06-13 - Anders - added experimental, partial support for HTTPCallback 65 | * 2007-03-28 - Anders - merged Christopher Hesse's patches for fix_params and 66 | to eliminate mutable default args 67 | * 2007-03-14 - Anders - quieted BaseHTTPServer in the test suite 68 | * 2007-03-06 - Anders - merged Christopher Hesse's bugfix and self-contained 69 | test suite 70 | * 2006-12-01 - Anders - switched to httplib2. Improved handling of parameters 71 | and made it stricter about unicode in headers 72 | (only ASCII is allowed). Added resp option. More 73 | docstrings. 74 | * 2006-03-23 - Anders - working around cherrypy bug properly now by being 75 | more careful about sending the right 76 | * 2006-03-17 - Anders - fixed my broken refactoring :) also added async 77 | support and we now use post_multipart for everything 78 | since it works around a cherrypy bug. 79 | * 2006-03-10 - Anders - refactored and added GET, POST, PUT, and DELETE 80 | convenience functions 81 | * 2006-02-22 - Anders - handles ints in params + headers correctly now 82 | 83 | """ 84 | 85 | import httplib2 86 | import mimetypes 87 | import thread 88 | import types 89 | import urllib 90 | import urllib2 91 | try: 92 | import json 93 | except ImportError: 94 | import simplejson as json 95 | 96 | 97 | __version__ = "0.11.0" 98 | 99 | 100 | def post_multipart(host, selector, method, fields, files, headers=None, 101 | return_resp=False, scheme="http", credentials=None, 102 | httplib_params=None): 103 | """ 104 | Post fields and files to an http host as multipart/form-data. 105 | fields is a sequence of (name, value) elements for regular form 106 | fields. files is a sequence of (name, filename, value) elements 107 | for data to be uploaded as files Return the server's response 108 | page. 109 | """ 110 | if headers is None: 111 | headers = {} 112 | if httplib_params is None: 113 | httplib_params = {} 114 | content_type, body = encode_multipart_formdata(fields, files) 115 | 116 | # Check for debuglevel in httplib_params 117 | orig_debuglevel = httplib2.debuglevel 118 | if 'debuglevel' in httplib_params: 119 | httplib2.debuglevel = httplib_params['debuglevel'] 120 | del httplib_params['debuglevel'] 121 | h = httplib2.Http(**httplib_params) 122 | if credentials: 123 | h.add_credentials(*credentials) 124 | headers['Content-Length'] = str(len(body)) 125 | headers['Content-Type'] = content_type 126 | resp, content = h.request("%s://%s%s" % (scheme, host, selector), 127 | method, body, headers) 128 | # reset httplib2 debuglevel to original value 129 | httplib2.debuglevel = orig_debuglevel 130 | # if the content-type is JSON, then convert back to objects. 131 | if resp['content-type'].startswith('application/json'): 132 | content = json.loads(content) 133 | elif method == 'GET' and content.startswith('{') and content.endswith('}'): 134 | content = json.loads(content) 135 | elif method == 'GET' and content.startswith('[') and content.endswith(']'): 136 | content = json.loads(content) 137 | if return_resp: 138 | return resp, content 139 | else: 140 | return content 141 | 142 | 143 | def encode_multipart_formdata(fields, files): 144 | """ 145 | fields is a sequence of (name, value) elements for regular form 146 | fields. files is a sequence of (name, filename, value) elements 147 | for data to be uploaded as files Return (content_type, body) ready 148 | for httplib.HTTP instance 149 | """ 150 | BOUNDARY = '----------ThIs_Is_tHe_bouNdaRY_$' 151 | CRLF = '\r\n' 152 | L = [] 153 | for (key, value) in fields: 154 | L.append('--' + BOUNDARY) 155 | L.append('Content-Disposition: form-data; name="%s"' % key) 156 | L.append('') 157 | L.append(str(value)) 158 | for (key, filename, value) in files: 159 | L.append('--' + BOUNDARY) 160 | L.append('Content-Disposition: form-data; name="%s"; filename="%s"' 161 | % (key, filename)) 162 | L.append('Content-Type: %s' % get_content_type(filename)) 163 | L.append('') 164 | L.append(str(value)) 165 | L.append('--' + BOUNDARY + '--') 166 | L.append('') 167 | L = [str(l) for l in L] 168 | 169 | body = CRLF.join(L) 170 | content_type = 'multipart/form-data; boundary=%s' % BOUNDARY 171 | return content_type, body 172 | 173 | 174 | def get_content_type(filename): 175 | return mimetypes.guess_type(filename)[0] or 'application/octet-stream' 176 | 177 | 178 | def GET(url, params=None, files=None, accept=[], headers=None, async=False, 179 | resp=False, credentials=None, httplib_params=None): 180 | """ make an HTTP GET request. 181 | 182 | performs a GET request to the specified URL and returns the body 183 | of the response. 184 | 185 | in addition, parameters and headers can be specified (as dicts). a 186 | list of mimetypes to accept may be specified. 187 | 188 | if async=True is passed in, it will perform the request in a new 189 | thread and immediately return nothing. 190 | 191 | if resp=True is passed in, it will return a tuple of an httplib2 192 | response object and the content instead of just the content. 193 | """ 194 | return rest_invoke(url=url, method=u"GET", params=params, 195 | files=files, accept=accept, headers=headers, 196 | async=async, resp=resp, credentials=credentials, 197 | httplib_params=httplib_params) 198 | 199 | 200 | def POST(url, params=None, files=None, accept=[], headers=None, 201 | async=True, resp=False, credentials=None, httplib_params=None): 202 | """ make an HTTP POST request. 203 | 204 | performs a POST request to the specified URL. 205 | 206 | in addition, parameters and headers can be specified (as dicts). a 207 | list of mimetypes to accept may be specified. 208 | 209 | files to upload may be specified. the data structure for them is: 210 | 211 | param : {'file' : file object, 'filename' : filename} 212 | 213 | and immediately return nothing. 214 | 215 | by default POST() performs the request in a new thread and returns 216 | (nothing) immediately. 217 | 218 | To wait for the response and have it return the body of the 219 | response, specify async=False. 220 | 221 | if resp=True is passed in, it will return a tuple of an httplib2 222 | response object and the content instead of just the content. 223 | """ 224 | return rest_invoke(url=url, method=u"POST", params=params, 225 | files=files, accept=accept, headers=headers, 226 | async=async, resp=resp, credentials=credentials, 227 | httplib_params=httplib_params) 228 | 229 | 230 | def PUT(url, params=None, files=None, accept=[], headers=None, 231 | async=True, resp=False, credentials=None, httplib_params=None): 232 | """ make an HTTP PUT request. 233 | 234 | performs a PUT request to the specified URL. 235 | 236 | in addition, parameters and headers can be specified (as dicts). a 237 | list of mimetypes to accept may be specified. 238 | 239 | files to upload may be specified. the data structure for them is: 240 | 241 | param : {'file' : file object, 'filename' : filename} 242 | 243 | and immediately return nothing. 244 | 245 | by default PUT() performs the request in a new thread and returns 246 | (nothing) immediately. 247 | 248 | To wait for the response and have it return the body of the 249 | response, specify async=False. 250 | 251 | if resp=True is passed in, it will return a tuple of an httplib2 252 | response object and the content instead of just the content. 253 | """ 254 | 255 | return rest_invoke(url=url, method=u"PUT", params=params, 256 | files=files, accept=accept, headers=headers, 257 | async=async, resp=resp, credentials=credentials, 258 | httplib_params=httplib_params) 259 | 260 | 261 | def DELETE(url, params=None, files=None, accept=[], headers=None, 262 | async=True, resp=False, credentials=None, 263 | httplib_params=None): 264 | """ make an HTTP DELETE request. 265 | 266 | performs a DELETE request to the specified URL. 267 | 268 | in addition, parameters and headers can be specified (as dicts). a 269 | list of mimetypes to accept may be specified. 270 | 271 | by default DELETE() performs the request in a new thread and 272 | returns (nothing) immediately. 273 | 274 | To wait for the response and have it return the body of the 275 | response, specify async=False. 276 | 277 | if resp=True is passed in, it will return a tuple of an httplib2 278 | response object and the content instead of just the content. 279 | """ 280 | 281 | return rest_invoke(url=url, method=u"DELETE", params=params, 282 | files=files, accept=accept, headers=headers, 283 | async=async, resp=resp, credentials=credentials, 284 | httplib_params=httplib_params) 285 | 286 | 287 | def rest_invoke(url, method=u"GET", params=None, files=None, 288 | accept=[], headers=None, async=False, resp=False, 289 | httpcallback=None, credentials=None, 290 | httplib_params=None): 291 | """ make an HTTP request with all the trimmings. 292 | 293 | rest_invoke() will make an HTTP request and can handle all the 294 | advanced things that are necessary for a proper REST client to handle: 295 | 296 | * alternative verbs: POST, PUT, DELETE, etc. 297 | * parameters 298 | * file uploads (multipart/form-data) 299 | * proper unicode handling 300 | * Accept: headers 301 | * ability to specify other headers 302 | 303 | rest_invoke() returns the body of the response that it gets from 304 | the server. 305 | 306 | rest_invoke() does not try to do any fancy error handling. if the 307 | server is down or gives an error, it will propagate up to the 308 | caller. 309 | 310 | this function expects to receive unicode strings. passing in byte 311 | strings risks double encoding. 312 | 313 | parameters: 314 | 315 | url: the full url you are making the request to 316 | method: HTTP verb to use. defaults to GET 317 | params: dictionary of params to include in the request 318 | files: dictionary of files to upload. the structure is 319 | 320 | param : {'file' : file object, 'filename' : filename} 321 | 322 | accept: list of mimetypes to accept in order of 323 | preference. defaults to '*/*' 324 | headers: dictionary of additional headers to send to the server 325 | async: Boolean. if true, does request in new thread and nothing is 326 | returned 327 | resp: Boolean. if true, returns a tuple of response, 328 | content. otherwise returns just content 329 | httpcallback: None. an HTTPCallback object (see 330 | http://microapps.org/HTTP_Callback). If specified, 331 | it will override the other params. 332 | httplib_params: dict of parameters supplied to httplib2 - for 333 | example ca_certs='/etc/ssl/certs/ca-certificates.crt' 334 | """ 335 | if async: 336 | thread.start_new_thread(_rest_invoke, 337 | (url, method, params, files, accept, 338 | headers, resp, httpcallback, credentials, 339 | httplib_params)) 340 | else: 341 | return _rest_invoke(url, method, params, files, accept, headers, 342 | resp, httpcallback, credentials, httplib_params) 343 | 344 | 345 | def _rest_invoke(url, method=u"GET", params=None, files=None, accept=None, 346 | headers=None, resp=False, httpcallback=None, 347 | credentials=None, httplib_params=None): 348 | if params is None: 349 | params = {} 350 | if files is None: 351 | files = {} 352 | if accept is None: 353 | accept = [] 354 | if headers is None: 355 | headers = {} 356 | 357 | if httpcallback is not None: 358 | method = httpcallback.method 359 | url = httpcallback.url 360 | if httpcallback.queryString != "": 361 | if "?" not in url: 362 | url += "?" + httpcallback.queryString 363 | else: 364 | url += "&" * httpcallback.queryString 365 | ps = httpcallback.params 366 | for (k, v) in ps: 367 | params[k] = v 368 | hs = httpcallback.headers 369 | for (k, v) in hs: 370 | headers[k] = v 371 | 372 | if httpcallback.username or httpcallback.password: 373 | print "warning: restclient can't handle HTTP auth yet" 374 | if httpcallback.redirections != 5: 375 | print ("warning: restclient doesn't support " 376 | "HTTPCallback's restrictions yet") 377 | if httpcallback.follow_all_redirects: 378 | print ("warning: restclient doesn't support " 379 | "HTTPCallback's follow_all_redirects_yet") 380 | if httpcallback.body != "": 381 | print "warning: restclient doesn't support HTTPCallback's body yet" 382 | 383 | headers = add_accepts(accept, headers) 384 | if method in ['POST', 'PUT'] and 'Content-Type' not in headers: 385 | headers['Content-Type'] = 'application/x-www-form-urlencoded' 386 | params = urllib.urlencode(fix_params(params)) 387 | elif (method in ['POST', 'PUT'] and 388 | headers['Content-Type'] == 'application/json'): 389 | params = json.dumps(params) 390 | else: 391 | # GET and DELETE requests 392 | params = urllib.urlencode(fix_params(params)) 393 | 394 | if files: 395 | return post_multipart(extract_host(url), extract_path(url), 396 | method, 397 | unpack_params(params), 398 | unpack_files(fix_files(files)), 399 | fix_headers(headers), 400 | resp, scheme=extract_scheme(url), 401 | credentials=credentials, 402 | httplib_params=httplib_params) 403 | else: 404 | return non_multipart(params, extract_host(url), 405 | method, extract_path(url), 406 | fix_headers(headers), resp, 407 | scheme=extract_scheme(url), 408 | credentials=credentials, 409 | httplib_params=httplib_params) 410 | 411 | 412 | def non_multipart(params, host, method, path, headers, return_resp, 413 | scheme="http", credentials=None, httplib_params=None): 414 | if httplib_params is None: 415 | httplib_params = {} 416 | if method == "GET": 417 | headers['Content-Length'] = '0' 418 | if params: 419 | # put the params into the url instead of the body 420 | if "?" not in path: 421 | path += "?" + params 422 | else: 423 | if path.endswith('?'): 424 | path += params 425 | else: 426 | path += "&" + params 427 | params = "" 428 | else: 429 | headers['Content-Length'] = str(len(params)) 430 | 431 | # Check for debuglevel in httplib_params 432 | orig_debuglevel = httplib2.debuglevel 433 | if 'debuglevel' in httplib_params: 434 | httplib2.debuglevel = httplib_params['debuglevel'] 435 | del httplib_params['debuglevel'] 436 | h = httplib2.Http(**httplib_params) 437 | if credentials: 438 | h.add_credentials(*credentials) 439 | url = "%s://%s%s" % (scheme, host, path) 440 | resp, content = h.request(url, method.encode('utf-8'), 441 | params.encode('utf-8'), headers) 442 | # reset httplib2 debuglevel to original value 443 | httplib2.debuglevel = orig_debuglevel 444 | # if the content-type is JSON, then convert back to objects. 445 | if resp['content-type'].startswith('application/json'): 446 | content = json.loads(content) 447 | elif method == 'GET' and content.startswith('{') and content.endswith('}'): 448 | content = json.loads(content) 449 | elif method == 'GET' and content.startswith('[') and content.endswith(']'): 450 | content = json.loads(content) 451 | if return_resp: 452 | return resp, content 453 | else: 454 | return content 455 | 456 | 457 | def extract_host(url): 458 | return my_urlparse(url)[1] 459 | 460 | 461 | def extract_scheme(url): 462 | return my_urlparse(url)[0] 463 | 464 | 465 | def extract_path(url): 466 | return my_urlparse(url)[2] 467 | 468 | 469 | def my_urlparse(url): 470 | (scheme, host, path, ps, query, fragment) = urllib2.urlparse.urlparse(url) 471 | if ps: 472 | path += ";" + ps 473 | if query: 474 | path += "?" + query 475 | 476 | return (scheme, host, path) 477 | 478 | 479 | def unpack_params(params): 480 | return [(k, params[k]) for k in params.keys()] 481 | 482 | 483 | def unpack_files(files): 484 | return [(k, files[k]['filename'], files[k]['file']) for k in files.keys()] 485 | 486 | 487 | def add_accepts(accept=None, headers=None): 488 | if accept is None: 489 | accept = [] 490 | if headers is None: 491 | headers = {} 492 | if accept: 493 | headers['Accept'] = ','.join(accept) 494 | else: 495 | headers['Accept'] = '*/*' 496 | return headers 497 | 498 | 499 | def fix_params(params=None): 500 | if params is None: 501 | params = {} 502 | for k in params.keys(): 503 | if type(k) not in types.StringTypes: 504 | new_k = str(k) 505 | params[new_k] = params[k] 506 | del params[k] 507 | else: 508 | try: 509 | k = k.encode('ascii') 510 | except UnicodeEncodeError: 511 | new_k = k.encode('utf8') 512 | params[new_k] = params[k] 513 | del params[k] 514 | except UnicodeDecodeError: 515 | pass 516 | 517 | for k in params.keys(): 518 | if type(params[k]) not in types.StringTypes: 519 | params[k] = str(params[k]) 520 | try: 521 | params[k].encode('ascii') 522 | except UnicodeEncodeError: 523 | new_v = params[k].encode('utf8') 524 | params[k] = new_v 525 | except UnicodeDecodeError: 526 | pass 527 | 528 | return params 529 | 530 | 531 | def fix_headers(headers=None): 532 | if headers is None: 533 | headers = {} 534 | for k in headers.keys(): 535 | if type(k) not in types.StringTypes: 536 | new_k = str(k) 537 | headers[new_k] = headers[k] 538 | del headers[k] 539 | if type(headers[k]) not in types.StringTypes: 540 | headers[k] = str(headers[k]) 541 | try: 542 | headers[k].encode('ascii') 543 | k = k.encode('ascii') 544 | except UnicodeEncodeError: 545 | new_k = k.encode('ascii', 'ignore') 546 | new_v = headers[k].encode('ascii', 'ignore') 547 | headers[new_k] = new_v 548 | del headers[k] 549 | return headers 550 | 551 | 552 | def fix_files(files=None): 553 | if files is None: 554 | files = {} 555 | # fix keys in files 556 | for k in files.keys(): 557 | if type(k) not in types.StringTypes: 558 | new_k = str(k) 559 | files[new_k] = files[k] 560 | del files[k] 561 | try: 562 | k = k.encode('ascii') 563 | except UnicodeEncodeError: 564 | new_k = k.encode('utf8') 565 | files[new_k] = files[k] 566 | del files[k] 567 | # second pass to fix filenames 568 | for k in files.keys(): 569 | try: 570 | files[k]['filename'].encode('ascii') 571 | except UnicodeEncodeError: 572 | files[k]['filename'] = files[k]['filename'].encode('utf8') 573 | return files 574 | 575 | 576 | if __name__ == "__main__": 577 | print rest_invoke("http://localhost:9090/", 578 | method="POST", params={'value': 'store this'}, 579 | accept=["text/plain", "text/html"], async=False) 580 | image = open('sample.jpg').read() 581 | r = rest_invoke("http://resizer.ccnmtl.columbia.edu/resize", 582 | method="POST", 583 | files={'image': {'file': image, 584 | 'filename': 'sample.jpg'}}, 585 | async=False) 586 | out = open("thumb.jpg", "w") 587 | out.write(r) 588 | out.close() 589 | GET("http://resizer.ccnmtl.columbia.edu/") 590 | r = POST("http://resizer.ccnmtl.columbia.edu/resize", 591 | files={'image': {'file': image, 592 | 'filename': 'sample.jpg'}}, 593 | async=False) 594 | # evil unicode tests 595 | print rest_invoke(u"http://localhost:9090/foo/", 596 | params={u'foo\u2012': u'\u2012'}, 597 | headers={u"foo\u2012": u"foo\u2012"}) 598 | 599 | r = rest_invoke(u"http://localhost:9090/resize", method="POST", 600 | files={u'image\u2012': {'file': image, 601 | 'filename': u'samp\u2012le.jpg'}}, 602 | async=False) 603 | --------------------------------------------------------------------------------