*****************
Top-level objects
*****************

Every web service has a top-level "root" object.

    >>> from lazr.restfulclient.tests.example import CookbookWebServiceClient
    >>> service = CookbookWebServiceClient()

The root object provides access to service-wide objects.

    >>> sorted(service.lp_entries)
    ['featured_cookbook']

    >>> sorted(service.lp_collections)
    ['cookbooks', 'dishes', 'recipes']


Top-level entries
=================

You can access a top-level entry through attribute access.

    >>> print service.featured_cookbook.name
    Mastering the Art of French Cooking


Top-level collections
=====================

You can access a top-level collection through attribute access.

    >>> len(service.dishes)
    3

Specific top-level collections may support key-based lookups. For
instance, the recipe collection does lookups by recipe ID. This is
custom code written for this specific web service, and it won't work
in general.

    >>> print service.recipes[1].dish.name
    Roast chicken

Looking up an object in a top-level collection triggers an HTTP
request, and if the object does not exist on the server, a KeyError is
raised.

    >>> import httplib2
    >>> httplib2.debuglevel = 1
    >>> debug_service = CookbookWebServiceClient()
    send: 'GET ...'
    ...

    >>> recipe = debug_service.recipes[4]
    send: 'GET /1.0/recipes/4 ...'
    ...

    >>> recipe = debug_service.recipes[1000]
    Traceback (most recent call last):
    ...
    KeyError: 1000

If you want to look up an object without triggering an HTTP request,
you can use parentheses instead of square brackets.

    >>> recipe = debug_service.recipes(4)
    >>> nonexistent_recipe = debug_service.recipes(1000)

    >>> sorted(recipe.lp_attributes)
    ['http_etag', 'id', 'instructions', ...]

The HTTP request, and any potential error, happens when you try to
access one of the object's properties.

    >>> print recipe.instructions
    send: 'GET /1.0/recipes/4 ...'
    ...
    Preheat oven to...

    >>> print nonexistent_recipe.instructions
    Traceback (most recent call last):
    ...
    NotFound: HTTP Error 404: Not Found
    ...

This is useful if you plan to invoke a named operation on the object
instead of accessing its properties--this can save you an HTTP request
and speed up your applicaiton.

Now, let's imagine that a top-level collection is misconfigured. We
know that the top-level collection of recipes contains objects whose
resource type is 'recipe'. But let's tell lazr.restfulclient that that
collection contains objects of type 'cookbook'.

    >>> from lazr.restfulclient.tests.example import RecipeSet
    >>> print RecipeSet.collection_of
    recipe
    >>> RecipeSet.collection_of = 'cookbook'

Looking up an object will give you something that presents the
interface of a cookbook.

    >>> not_really_a_cookbook = debug_service.recipes(2)
    >>> sorted(not_really_a_cookbook.lp_attributes)
    ['confirmed', 'copyright_date', 'cuisine'...]

But once you try to access one of the object's properties, and the
HTTP request is made...

    >>> print not_really_a_cookbook.resource_type_link
    send: 'GET /1.0/recipes/2 ...'
    ...
    http://cookbooks.dev/1.0/#recipe

...the server serves a recipe, and so the client-side object takes on
the properties of a recipe. You can only fool lazr.restfulclient up to
the point where it has real data to look at.

    >>> sorted(not_really_a_cookbook.lp_attributes)
    ['http_etag', 'id', 'instructions', ...]

    >>> print not_really_a_cookbook.instructions
    Draw, singe, stuff, and truss...

This isn't just a defense mechanism: it's a useful feature when a
top-level collection contains mixed subclasses of some superclass. For
instance, the launchpadlib library defines the 'people' collection as
containing 'team' objects, even though it also contains 'person'
objects, which expose a subset of a team's functionality. All objects
looked up in that collection start out as team objects, but once an
object's data is fetched, if it turns out to actually be a person, it
switches from the "team" interface to the "people" interface. (This
bit of hackery is necessary because WADL doesn't have an inheritance
mechanism.)

If you try to access a property based on a resource type the object
doesn't really implement, you'll get an error.

    >>> not_really_a_cookbook = debug_service.recipes(3)
    >>> sorted(not_really_a_cookbook.lp_attributes)
    ['confirmed', 'copyright_date', 'cuisine'...]
    >>> not_really_a_cookbook.cuisine
    Traceback (most recent call last):
    ...
    AttributeError: http://cookbooks.dev/1.0/recipes/3 object has no attribute 'cuisine'

Cleanup.

    >>> httplib2.debuglevel = 0
    >>> RecipeSet.collection_of = 'recipe'

Versioning
==========

By passing in a 'version' argument to the client constructor, you can
access different versions of the web service.

    >>> print service.recipes[1].self_link
    http://cookbooks.dev/1.0/recipes/1

    >>> devel_service = CookbookWebServiceClient(version="devel")
    >>> print devel_service.recipes[1].self_link
    http://cookbooks.dev/devel/recipes/1

You can also forgo the 'version' argument and pass in a service root
that incorporates a version string.

    >>> devel_service = CookbookWebServiceClient(
    ...     service_root="http://cookbooks.dev/devel/", version=None)
    >>> print devel_service.recipes[1].self_link
    http://cookbooks.dev/devel/recipes/1

Error reporting
===============

If there's an error communicating with the server, lazr.restfulclient
raises HTTPError or an appropriate subclass. The error might be a
client-side error (maybe you tried to access something that doesn't
exist) or a server-side error (maybe the server crashed due to a
bug). The string representation of the error should have enough
information to help you figure out what happened.

This example demonstrates NotFound, the HTTPError subclass used when
the server sends a 404 error For detailed information about the
different HTTPError subclasses, see tests/test_error.py.

    >>> from lazr.restfulclient.errors import HTTPError
    >>> try:
    ...    service.load("http://cookbooks.dev/")
    ... except Exception, e:
    ...     pass

    >>> raise e
    Traceback (most recent call last):
    ...
    NotFound: HTTP Error 404: Not Found
    Response headers:
    ---
    ...
    content-type: text/plain
    ...
    ---
    Response body:
    ---
    ...
    ---

    >>> print isinstance(e, HTTPError)
    True

