Positive Incline Mike Burrows (@asplake) moving on up, positively

February 18, 2010

PathTo for Python gets a JSON-capable HTTP client

Filed under: Web Integration — Tags: , , , , , , , , — Mike @ 1:55 pm

…and it works!

>>> import path_to
>>> app = path_to.open_app('http://example.com', format='json')
>>> app.login.post(credentials, expected_status=302)
>>> print app.products.resource_template
products      products     GET, POST http://example.com/products{.format}
  new_product new_product  GET       http://example.com/products/new{.format}
  {product}   product      GET, PUT  http://example.com/products/{product}{.format}
    edit      edit_product GET       http://example.com/products/{product}/edit{.format}
>>> product = app.products['Foo'].get(expected_status=200).parsed
>>> product['description'] = 'Updated!'
>>> app.products['Foo'].put(product, expected_status=302)
>>> app.products['Foo'].get(expected_status=200).parsed['description']
u'Updated!'

OK, so it wasn’t really “example.com”, and the updated product wasn’t called “Foo”, but the rest is for real.

In stages:

0) The Pylons-based server has the JSON-capable @validate (un)decorator of my previous post and DescribedRoutesMiddleware from DescribedRoutes installed in its wsgi stack.

1) Create a client-side proxy to the app. Following link headers published by the app, it finds the ResourceTemplates description, which it retrieves in JSON format.

>>> import path_to
>>> app = path_to.open_app('http://example.com', format='json')

The format='json' is a small red herring here, it’s simply remembered for later.

2) Log in by posting credentials, expecting a 302 status (a failed attempt would return a 200 and the validation errors in the body). The client handles cookies automatically behind the scenes.

>>> app.login.post(credentials, expected_status=302)

3) View a friendly representation of the metadata (or rather the part that relates to products):

>>> print app.products.resource_template
products      products     GET, POST http://example.com/products{.format}
  new_product new_product  GET       http://example.com/products/new{.format}
  {product}   product      GET, PUT  http://example.com/products/{product}{.format}
    edit      edit_product GET       http://example.com/products/{product}/edit{.format}

4) Get a product identified by ‘Foo’ and return the JSON payload parsed into a Python dict.:

>>> product = app.products['Foo'].get(expected_status=200).parsed

Behind the scenes it has expanded the “http://example.com/products/{product}{.format}” template seen previously into “http://example.com/products/Foo.json”, using the remembered format parameter and the supplied ‘Foo’ key which is assumed to correspond to the required product parameter.

5) Update the local product representation and send it back (it gets converted back to JSON along the way):

>>> product['description'] = 'Updated!'
>>> app.products['Foo'].put(product, expected_status=302)

6) Finally, demonstrate that it was successful!

>>> app.products['Foo'].get(expected_status=200).parsed['description']
u'Updated!'

February 13, 2010

Experimental: Pylons validation (un)decorator with JSON support

Filed under: Web Integration,Work — Tags: , — Mike @ 12:24 pm

In this gist (it’s not even a patch, just some stuff I keep in my app’s lib/base.by) is a refactored @validate.

On the surface it’s just the same as the standard pylons.decorators.validate:

    @validate(form='edit')
    def update(self):
        ...

but it has been broken up into a number of controller methods, each of which is usable directly. So if you find the decorator inflexible, you can write your actions in the undecorated form

    def update(self):
        try:
            self._parse(request, schema=MyForm())
        except Invalid as e:
            return self._render_invalid(e, form='edit')
        ...

It’s more than just a stylistic change; in addition to the usual HTML form data it will also accept POST data sent in JSON and send back any validation errors in JSON too if the client so desires. The presence of JSON is recognized by an sent_json() helper that looks to see if request’s route had a format parameter set to ‘json’ (see previous post) or if the Content-Type header is ‘application/json’; responses work similarly via an accepts_json() helper, but checking the Accept header.

Throw in a render_json() helper and here are idioms for actions that will quite happily accept and produce JSON or HTML. First the traditional, decorated look:

    @validate(form='edit')
    def update(self):
        ...
        if accepts_json():
            return render_json(a_json_serialisable_thing)
        else:
            return render('template')

And undecorated:

    def update(self):
        try:
            self._parse(request, schema=MyForm())
            ...
        except Invalid as e:
            return self._render_invalid(e, form='edit')
        ...
        if accepts_json():
            return render_json(a_json_serialisable_thing)
        else:
            return render('template')

The undecorated form is slightly longer — potentially addressed by sharing exception handling controller-wide — but it offers the intriguing opportunity to move some validation to the model &emdash; any Invalid exceptions raised in the try block will be rendered appropriately, regardless of where they came from.

Powered by WordPress