├── AUTHORS ├── wellrested ├── __init__.py └── connections │ └── __init__.py ├── .gitignore ├── setup.py ├── LICENSE └── README.rst /AUTHORS: -------------------------------------------------------------------------------- 1 | Nowell Strite 2 | Shawn Rider 3 | -------------------------------------------------------------------------------- /wellrested/__init__.py: -------------------------------------------------------------------------------- 1 | from wellrested.connections import RestClient, JsonRestClient 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .DS_Store 3 | \#*# 4 | *.pyc 5 | .project 6 | .pydevproject 7 | pip-log.txt 8 | ve/ 9 | src/pbs-plc/ 10 | dist/solr/logs/ 11 | dist/solr/solr/data/ 12 | media/deployed/ 13 | media/uploads/ 14 | media/compressed/ 15 | media/testing/ 16 | docs/_build/ 17 | coverage/ 18 | docs/modules/ -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | version = '0.1.1' 4 | 5 | setup( 6 | name='python-wellrested', 7 | version=version, 8 | description='A REST client providing a convenience wrapper over httplib2', 9 | author='Nowell Strite', 10 | author_email='nowell@strite.org', 11 | url='http://github.com/nowells/python-wellrested/', 12 | packages=['wellrested', 'wellrested.connections'], 13 | install_requires=['httplib2'], 14 | classifiers=[ 15 | 'Development Status :: 4 - Beta', 16 | 'Environment :: Web Environment', 17 | 'Intended Audience :: Developers', 18 | 'Operating System :: OS Independent', 19 | 'Programming Language :: Python', 20 | 'Topic :: Internet :: WWW/HTTP', 21 | 'Topic :: Software Development :: Libraries :: Python Modules', 22 | ] 23 | ) 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2009 Nowell Strite 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ########################################## 2 | python-wellrested A Simple RESTful client 3 | ########################################## 4 | 5 | Introduction 6 | ============ 7 | 8 | The python-wellrested library is a lean client for performing RESTful API calls. 9 | 10 | Basic Usage 11 | =========== 12 | 13 | Basic usage of the python-wellrested library is simple. 14 | 15 | To work with a RESTful API you first instantiate the client: 16 | 17 | :: 18 | 19 | >>> j = JsonRestClient('http://example.com/api/', username='user', pass='password') 20 | 21 | Then you can read data using the ``get`` method: 22 | 23 | :: 24 | 25 | >>> j.get('resource.json') 26 | 27 | A response object will be returned with data similar to the following 28 | 29 | :: 30 | 31 | 32 | 33 | Obviously the exact structure of the data depends on the API that is being called. 34 | 35 | To post data to a RESTful API, the ``post`` method is used: 36 | 37 | :: 38 | 39 | >>> mydata = {'foo':'bar',} 40 | >>> j.post('resource.json', data=mydata) 41 | 42 | This will return a response similar to the one above, containing whatever confirmation the remote API delivers upon posting data. This is normally used for creating objects using the remote API. 43 | 44 | To put data to a RESTful API, the ``put`` method is used: 45 | 46 | :: 47 | 48 | >>> mydata = {'foo':'bar_edited',} 49 | >>> j.put('resource.json', data=mydata) 50 | 51 | And, finally, to delete data using a RESTful API, the ``delete`` method is used: 52 | 53 | :: 54 | 55 | >>> j.delete('resource.json) 56 | 57 | .. note:: 58 | 59 | Please remember that the workings of each RESTful API will vary, and this client only handles the connection between your application and a RESTful resource. The above examples represent the basic usage concepts of python-wellrested, but are not exhaustive nor are they suitable for direct use as doctests. 60 | 61 | Comparisons to Other REST Clients 62 | ================================= 63 | 64 | There are a few different REST clients written in Python that work just fine. It is not the most ambitious type of library to create. At the same time, it is completely possible to make value judgements, and python-wellrested probably lies somewhere in the middle of the quality spectrum. 65 | 66 | In creating this client, we first checked out several others that had some obvious usage issues. Some were buggy, others were incomplete. Nowell wrote the base of python-wellrested quickly one night after hacking away with a less good client from somebody else. 67 | 68 | The goal, as always, is to keep the client simple to use, adherent to relevant standards, and minimal in coding style. 69 | 70 | Shortly after writing the first draft of python-wellrested, Nowell discovered py-restclient http://py-restclient.e-engura.org/, which seems to be a somewhat more mature client and the obvious other choice. There are some subtle api differences between the two clients, but they generally take the same basic approach to handling the RESTful API calls. 71 | 72 | Please feel free to use whatever client best suits your needs. And send a pull request on github if you make any improvements to this code. 73 | 74 | -------------------------------------------------------------------------------- /wellrested/connections/__init__.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import httplib2 3 | import logging 4 | import mimetypes 5 | import mimetools 6 | import urllib 7 | import urlparse 8 | 9 | HTTP_STATUS_OK = '200' 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | class RestClient(object): 15 | content_type = None 16 | 17 | def __init__(self, base_url, username=None, password=None, 18 | connection_class=None, **kwargs): 19 | if connection_class is None: 20 | connection_class = Connection 21 | self._connection = connection_class(base_url, username, password, 22 | **kwargs) 23 | 24 | def get(self, resource, args=None, data=None, headers=None): 25 | return self._request(resource, 'get', args=args, data=data, 26 | headers=headers) 27 | 28 | def put(self, resource, args=None, data=None, headers=None): 29 | return self._request(resource, 'put', args=args, data=data, 30 | headers=headers) 31 | 32 | def delete(self, resource, args=None, data=None, headers=None): 33 | return self._request(resource, 'delete', args=args, data=data, 34 | headers=headers) 35 | 36 | def post(self, resource, args=None, data=None, headers=None): 37 | return self._request(resource, 'post', args=args, data=data, 38 | headers=headers) 39 | 40 | def _request(self, resource, method, args=None, data=None, headers=None): 41 | response_data = None 42 | request_body = self._serialize(data) 43 | response_headers, response_content = \ 44 | self._connection.request(resource, method, args=args, 45 | body=request_body, headers=headers, 46 | content_type=self.content_type) 47 | if response_headers.get('status') == HTTP_STATUS_OK: 48 | response_data = self._deserialize(response_content) 49 | return Response(response_headers, response_content, response_data) 50 | 51 | def _serialize(self, data): 52 | return unicode(data) 53 | 54 | def _deserialize(self, data): 55 | return unicode(data) 56 | 57 | 58 | class JsonRestClient(RestClient): 59 | content_type = 'application/json' 60 | 61 | def _serialize(self, data): 62 | if data: 63 | try: 64 | import simplejson as json 65 | except ImportError: 66 | try: 67 | import json 68 | except ImportError: 69 | raise RuntimeError('simplejson not installed') 70 | 71 | return json.dumps(data) 72 | return None 73 | 74 | def _deserialize(self, data): 75 | if data: 76 | try: 77 | import simplejson as json 78 | except ImportError: 79 | try: 80 | import json 81 | except ImportError: 82 | raise RuntimeError('simplejson not installed') 83 | 84 | return json.loads(data) 85 | return None 86 | 87 | 88 | class XmlRestClient(RestClient): 89 | content_type = 'text/xml' 90 | 91 | 92 | class Response(object): 93 | def __init__(self, headers, content, data): 94 | self.headers = headers 95 | self.content = content 96 | self.data = data 97 | self.status_code = int(headers.get('status', '500')) 98 | 99 | def __repr__(self): 100 | return '' % (self.status_code, self.__dict__) 101 | 102 | 103 | class BaseConnection(object): 104 | def __init__(self, base_url, username=None, password=None): 105 | self.base_url = base_url 106 | self.username = username 107 | self.password = password 108 | self.url = urlparse.urlparse(base_url) 109 | (scheme, netloc, path, query, fragment) = urlparse.urlsplit(base_url) 110 | self.scheme = scheme 111 | self.host = netloc 112 | self.path = path 113 | 114 | def _get_content_type(self, filename): 115 | return mimetypes.guess_type(filename)[0] or 'application/octet-stream' 116 | 117 | def request(self, resource, method="get", args=None, body=None, 118 | headers=None, content_type=None): 119 | raise NotImplementedError 120 | 121 | 122 | class Connection(BaseConnection): 123 | def __init__(self, *args, **kwargs): 124 | cache = kwargs.pop('cache', None) 125 | timeout = kwargs.pop('cache', None) 126 | proxy_info = kwargs.pop('proxy_info', None) 127 | 128 | super(Connection, self).__init__(*args, **kwargs) 129 | 130 | self._conn = httplib2.Http(cache=cache, timeout=timeout, 131 | proxy_info=proxy_info) 132 | self._conn.follow_all_redirects = True 133 | 134 | if self.username and self.password: 135 | self._conn.add_credentials(self.username, self.password) 136 | 137 | def request(self, resource, method, args=None, body=None, headers=None, 138 | content_type=None): 139 | if headers is None: 140 | headers = {} 141 | 142 | params = None 143 | path = resource 144 | headers['User-Agent'] = 'Basic Agent' 145 | 146 | BOUNDARY = mimetools.choose_boundary() 147 | CRLF = u'\r\n' 148 | 149 | if body: 150 | if not headers.get('Content-Type', None): 151 | headers['Content-Type'] = content_type or 'text/plain' 152 | headers['Content-Length'] = str(len(body)) 153 | else: 154 | if 'Content-Length' in headers: 155 | del headers['Content-Length'] 156 | 157 | headers['Content-Type'] = 'text/plain' 158 | 159 | if args: 160 | if method == "get": 161 | path += u"?" + urllib.urlencode(args) 162 | elif method == "put" or method == "post": 163 | headers['Content-Type'] = \ 164 | 'application/x-www-form-urlencoded' 165 | body = urllib.urlencode(args) 166 | 167 | request_path = [] 168 | # Normalise the / in the url path 169 | if self.path != "/": 170 | if self.path.endswith('/'): 171 | request_path.append(self.path[:-1]) 172 | else: 173 | request_path.append(self.path) 174 | if path.startswith('/'): 175 | request_path.append(path[1:]) 176 | else: 177 | request_path.append(path) 178 | 179 | response_headers, response_content = \ 180 | self._conn.request(u"%s://%s%s" % (self.scheme, self.host, 181 | u'/'.join(request_path)), method.upper(), 182 | body=body, headers=headers) 183 | return response_headers, response_content 184 | --------------------------------------------------------------------------------