***************** 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