Permalink
Browse files

- Prefer json over simplejson

- Install requires httplib2
- Only decode json if content-type is right
- Attribute requests returns clones so that you may re-use references for repeated requests
- Attribute access and with_* functions return clone
- Explicit use of with_body for form encoding or with_json for json encoded bodies
- get_url() can be passed extra path parts or url parameters
- Added with_headers()
- Added add_basic_auth helper
  • Loading branch information...
1 parent 409bf45 commit 61a47d709a2d3de7fcdda36b833e8666c805a2c0 @six8 six8 committed Nov 4, 2011
Showing with 423 additions and 80 deletions.
  1. +1 −0 .gitignore
  2. +45 −2 README.markdown
  3. +206 −30 dolt/__init__.py
  4. +22 −0 dolt/helpers.py
  5. +1 −0 setup.py
  6. +148 −48 tests/unit_tests.py
View
@@ -2,3 +2,4 @@
MANIFEST
dist/*
build/*
+*.egg-info
View
@@ -47,7 +47,7 @@ API:
Now the Dolt version:
twitter = Twitter()
- twitter.statuses.update.POST(status="Hello from Dolt!")
+ twitter.statuses.update.with_body(status="Hello from Dolt!").POST()
Notice that all you need to add to it is the method you want to call. If
you're feeling very Pythonic and want to be explicit in every call, you can add
@@ -59,10 +59,26 @@ be in all uppercase. For example, this works just the same as the previous
code:
twitter = Twitter()
- twitter.POST.statuses.update(status="Hello from Dolt!")
+ twitter.POST.with_body(status="Hello from Dolt!").statuses.update()
This works for other HTTP methods as well, such as `PUT`, `DELETE`, and `HEAD`.
+JSON Handling
+-------------
+Dolt will automatically decode JSON if the response uses one of the JSON
+content-types and return a dict.
+
+Dolt can also send JSON:
+
+ twitter = Twitter()
+ twitter.statuses.update.with_json(status="Hello from Dolt!").POST()
+
+Sending Headers
+---------------
+Dolt can send headers with the request:
+
+ api = Dolt()
+ api.foo.with_headers(Accept='text/html').GET()
Handling authentication
-----------------------
@@ -75,6 +91,11 @@ which allows you to pass in an Http object with credentials. For example:
http.add_credentials("some_user", "secret")
some_api = Dolt(http=http)
+You can also use the `add_basic_auth` helper:
+
+ from dolt.helpers import add_basic_auth
+ some_api = Dolt()
+ some_api = add_basic_auth(some_api, username, password)
Using dictionary-style lookups
------------------------------
@@ -90,6 +111,28 @@ That is equivalent to:
couch._design.posts._list.all()
+Re-using requests
+-----------------
+When chainging attributes or using `with_*` functions, Dolt returns a clone of
+the current state. This allows you to safely re-use Dolt requests for batch
+processing.
+
+ http = Http()
+ http.add_credentials("some_user", "secret")
+ some_api = Dolt(http=http)
+
+ update = some_api.collection.with_params(api_key=API_KEY).PUT
+ update.with_json(name='Foo')(id=123)
+ update.with_json(name='Bar')(id=345)
+
+The `Http` connection is re-used for each request along with the path parts
+and params.
+
+ item = some_api.admin.item.with_params(api_key=API_KEY)
+ item[uid1].DELETE()
+ # DELETE /admin/item/<uid1>?api_key=<API_KEY>
+ item[uid2].comments.DELETE()
+ # DELETE /admin/item/<uid2>/comments?api_key=<API_KEY>
Included APIs
-------------
View
@@ -1,83 +1,259 @@
import httplib2
+import urllib
+
try:
- import json as simplejson
@kmike

kmike Nov 18, 2011

Contributor

Why prefer json over simplejson? json in python 2.6's json module doesn't have C extension so there is no way to speed up json parsing for python 2.6 after this change.

@tswicegood

tswicegood Nov 18, 2011

Owner

json is available without having to install or compile anything else. You can always sub-class Dolt and add a custom _handle_response method that uses simplejson if you need to.

@kmike

kmike Nov 18, 2011

Contributor

With the following:

try:
    import simplejson as json
except ImportError:
    import json

faster alternative would be used if it is possible and there is no need to install or compile anything if python is 2.7 or speed is not a concern.

@tswicegood

tswicegood Nov 18, 2011

Owner

I'm ok with that change. I'll merge a PR in if you'll put it together.

@six8

six8 Nov 20, 2011

Contributor

I didn't have a strong preference for json vs simplejson I just thought it odd to import json as simplejson. I wasn't aware Python's 2.6 json module wasn't a C extension. It seems reasonable to me to prefer simplejson if it it's compiled as a C extension instead of the built in json as long as it's import simplejson as json :)

@kmike

kmike Nov 20, 2011

Contributor

This is not so odd given 2.6 python's json module is an older version of simplejson with some 2.4/2.5 support code removed :) But I also like import simplejson as json more.

@tswicegood

tswicegood Nov 22, 2011

Owner

It's a legacy thing -- the original code used simplejson, so the quick fix was to do import json as simplejson and you're set. :-) Like I said, I'm cool by making the change if anyone wants to tackle it.

+ import json
except ImportError:
- import simplejson
-import urllib
+ import simplejson as json
+
+try:
+ from decorator import decorator
+except ImportError:
+ # No decorator package available. Create a no-op "decorator".
+ def decorator(f):
+ def decorate(_func):
+ def inner(*args, **kwargs):
+ return f(_func, *args, **kwargs)
+ return inner
+ return decorate
+
+
+@decorator
+def _makes_clone(_func, *args, **kw):
+ """
+ A decorator that returns a clone of the current object so that
+ we can re-use the object for similar requests.
+ """
+ self = args[0]._clone()
+ _func(self, *args[1:], **kw)
+ return self
class Dolt(object):
def __init__(self, http=None):
- self._supported_methods = ("GET", "POST", "PUT", "HEAD", "DELETE",)
+ self._supported_methods = ("GET", "POST", "PUT", "HEAD", "DELETE", "OPTIONS")
self._attribute_stack = []
self._method = "GET"
- self._posts = []
+ self._body = None
self._http = http or httplib2.Http()
self._params = {}
+ self._headers = {}
self._api_url = ""
self._url_template = '%(domain)s/%(generated_url)s'
self._stack_collapser = "/".join
self._params_template = '?%s'
def __call__(self, *args, **kwargs):
- self._attribute_stack += [str(a) for a in args]
- self._params = kwargs
- try:
- body = self._generate_body()
- response, data = self._http.request(self.get_url(), self._method, body=body)
- return self._handle_response(response, data)
- finally:
- self._attribute_stack = []
+ url = self.get_url(*[str(a) for a in args], **kwargs)
+
+ response, data = self._http.request(url, self._method, body=self._body, headers=self._headers)
+ return self._handle_response(response, data)
def _generate_params(self, params):
return self._params_template % urllib.urlencode(params)
- def _generate_body(self):
- if self._method == 'POST':
- internal_params = self._params.copy()
- if 'GET' in internal_params:
- del internal_params['GET']
- return self._generate_params(internal_params)[1:]
-
def _handle_response(self, response, data):
- return simplejson.loads(data)
+ """
+ Deserializes JSON if the content-type matches, otherwise returns the response
+ body as is.
+ """
+ if data and response.get('content-type') in (
+ 'application/json',
+ 'application/x-javascript',
+ 'text/javascript',
+ 'text/x-javascript',
+ 'text/x-json'
+ ):
+ return json.loads(data)
+ else:
+ return data
+ @_makes_clone
def __getitem__(self, name):
+ """
+ Adds `name` to the URL path.
+ """
self._attribute_stack.append(name)
return self
+ @_makes_clone
def __getattr__(self, name):
+ """
+ Sets the HTTP method for the request or adds `name` to the URL path.
+
+ ::
+
+ >>> dolt.GET._method == 'GET'
+ True
+ >>> dolt.foo.bar.get_url()
+ '/foo/bar'
+
+ """
if name in self._supported_methods:
self._method = name
elif not name.endswith(')'):
self._attribute_stack.append(name)
return self
- def get_url(self):
+ @_makes_clone
+ def with_params(self, **params):
+ """
+ Add URL query parameters to the request.
+ """
+ self._params.update(params)
+ return self
+
+ @_makes_clone
+ def with_body(self, body=None, **params):
+ """
+ Add a body to the request.
+
+ When `body` is a:
+ - string, it will be used as is.
+ - dict or list of (key, value) pairs, it will be form encoded
+ - None, remove request body
+ - anything else, a TypeError will be raised
+
+ If `body` is a dict or None you can also pass in keyword
+ arguments to add to the body.
+
+ ::
+ >>> dolt.with_body(dict(key='val'), foo='bar')._body
+ 'foo=bar&key=val'
+ """
+
+ if isinstance(body, (tuple, list)):
+ body = dict(body)
+
+ if params:
+ # Body must be None or able to be a dict
+ if isinstance(body, dict):
+ body.update(params)
+ elif body is None:
+ body = params
+ else:
+ raise ValueError('Body must be None or a dict if used with params, got: %r' % body)
+
+ if isinstance(body, basestring):
+ self._body = body
+ elif isinstance(body, dict):
+ self._body = urllib.urlencode(body)
+ elif body is None:
+ self._body = None
+ else:
+ raise TypeError('Invalid body type %r' % body)
+
+
+ return self
+
+ def with_json(self, data=None, **params):
+ """
+ Add a json body to the request.
+
+ :param data: A json string, a dict, or a list of key, value pairs
+ :param params: A dict of key value pairs to JSON encode
+ """
+ if isinstance(data, (tuple, list)):
+ data = dict(data)
+
+ if params:
+ # data must be None or able to be a dict
+ if isinstance(data, dict):
+ data.update(params)
+ elif data is None:
+ data = params
+ else:
+ raise ValueError('Data must be None or a dict if used with params, got: %r' % data)
+
+ req = self.with_headers({'Content-Type': 'application/json', 'Accept': 'application/json'})
+ if isinstance(data, basestring):
+ # Looks like it's already been encoded
+ return req.with_body(data)
+ else:
+ return req.with_body(json.dumps(data))
+
+ @_makes_clone
+ def with_headers(self, headers=None, **params):
+ """
+ Add headers to the request.
+
+ :param headers: A dict, or a list of key, value pairs
+ :param params: A dict of key value pairs
+ """
+ if isinstance(headers, (tuple, list)):
+ headers = dict(headers)
+
+ if params:
+ if isinstance(headers, dict):
+ headers.update(params)
+ elif headers is None:
+ headers = params
+
+ self._headers.update(headers)
+ return self
+
+ def get_url(self, *paths, **params):
+ """
+ Returns the URL for this request.
+
+ :param paths: Additional URL path parts to add to the request
+ :param params: Additional query parameters to add to the request
+ """
+ path_stack = self._attribute_stack[:]
+ if paths:
+ path_stack.extend(paths)
+
+ u = self._stack_collapser(path_stack)
url = self._url_template % {
"domain": self._api_url,
- "generated_url" : self._stack_collapser(self._attribute_stack),
+ "generated_url" : u,
}
- if len(self._params):
+
+ if self._params or params:
internal_params = self._params.copy()
- if self._method == 'POST':
- if "GET" not in internal_params:
- return url
- internal_params = internal_params['GET']
+ internal_params.update(params)
url += self._generate_params(internal_params)
return url
+ def _clone(self):
+ """
+ Clones the state of the current operation.
+
+ The state is cloned so that you can freeze the state at a certain point for re-use.
+
+ ::
+
+ >>> cat = dolt.cat
+ >>> cat.get_url()
+ '/cat'
+ >>> o = cat.foo
+ >>> o.get_url()
+ '/cat/foo'
+ >>> cat.get_url()
+ '/cat'
+
+ """
+ cls = self.__class__
+ q = cls.__new__(cls)
+ q.__dict__ = self.__dict__.copy()
+ q._params = self._params.copy()
+ q._headers = self._headers.copy()
+ q._attribute_stack = self._attribute_stack[:]
+
+ return q
+
try:
__IPYTHON__
def __dir__(self):
return [
'_supported_methods',
'_attribute_stack',
'_method',
- '_posts',
+ '_body',
'_http',
'_params',
+ '_headers',
'_api_url',
'_url_template',
'_stack_collapser',
@@ -91,4 +267,4 @@ def __dir__(self):
]
_getAttributeNames = trait_names = __dir__
except NameError:
- pass
+ pass
Oops, something went wrong.

0 comments on commit 61a47d7

Please sign in to comment.