Package: restnavigator
REST Navigator==============|Build Status| |Coverage Status| |Pypi Status| |Documentation Status|REST Navigator is a python library for interacting with hypermedia apis(`REST level3 <http://martinfowler.com/articles/richardsonMaturityModel.html#level3>`__).Right now, it only supports`HAL+JSON <http://tools.ietf.org/html/draft-kelly-json-hal-05>`__ but itshould be general enough to extend to other formats eventually. Itsfirst goal is to make interacting with HAL hypermedia apis as painlessas possible, while discouraging REST anti-patterns.To install it, simply use pip:.. code:: bash$ pip install restnavigatorContents---------  `How to use it <#how-to-use-it>`__-  `Links <#links>`__-  `GET requests <#get-requests>`__-  `Link relation docs <#link-relation-docs>`__-  `POST requests <#post-requests>`__-  `Errors <#errors>`__-  `Templated links <#templated-links>`__-  `Authentication <#authentication>`__-  `Additional Topics <#additional-topics>`__-  `Identity Map <#identity-map>`__-  `Iterating over a Navigator <#iterating-over-a-navigator>`__-  `Headers (Request vs. Response) <#headers-request-vs-response>`__-  `Bracket mini-language <#bracket-minilanguage>`__-  `Finding the right link <#finding-the-right-link>`__-  `Default curie <#default-curie>`__-  `Specifying an api name <#specifying-an-api-name>`__-  `Embedded documents <#embedded-documents>`__-  `Development <#development>`__-  `Testing <#testing>`__-  `Planned for the future <#planned-for-the-future>`__.. raw:: html<!-- end toc -->How to use it-------------To begin interacting with a HAL api, you've got to create a HALNavigatorthat points to the api root. Ideally, in a hypermedia API, the root URLis the only URL that needs to be hardcoded in your application. Allother URLs are obtained from the api responses themselves (think of yourapi client as 'clicking on links', rather than having the urlshardcoded).As an example, we'll connect to the haltalk api... code:: python>>> from restnavigator import Navigator>>> N = Navigator.hal('http://haltalk.herokuapp.com/', default_curie="ht")>>> NHALNavigator(Haltalk)Links~~~~~Usually, with the index (normally at the api root), you're mostinterested in the links. Let's look at those:.. code:: python>>> N.links(){u'ht:users': HALNavigator(Haltalk.users),u'ht:signup': HALNavigator(Haltalk.signup),u'ht:me': TemplatedThunk(Haltalk.users.{name}),u'ht:latest-posts': HALNavigator(Haltalk.posts.latest)}(This may take a moment because asking for the links causes theHALNavigator to actually request the resource from the server).Here we can see that the links are organized by their relation type (thekey), and each key corresponds to a new HALNavigator that representssome other resource. Relation types are extremely important in restfulapis: we need them to be able to determine what a link means in relationto the current resource, in a way that is automatable.GET requests~~~~~~~~~~~~In addition, the root has some state associated with it which you canget in two different ways:.. code:: python>>> N() # cached state of resource (obtained when we looked at N.links){u'hint_1': u'You need an account to post stuff..',u'hint_2': u'Create one by POSTing via the ht:signup link..',u'hint_3': u'Click the orange buttons on the right to make POST requests..',u'hint_4': u'Click the green button to follow a link with a GET request..',u'hint_5': u'Click the book icon to read docs for the link relation.',u'welcome': u'Welcome to a haltalk server.'}>>> N.fetch() # will refetch the resource from the server{u'hint_1': u'You need an account to post stuff..',u'hint_2': u'Create one by POSTing via the ht:signup link..',u'hint_3': u'Click the orange buttons on the right to make POST requests..',u'hint_4': u'Click the green button to follow a link with a GET request..',u'hint_5': u'Click the book icon to read docs for the link relation.',u'welcome': u'Welcome to a haltalk server.'}Calling a HALNavigator will execute a GET request against the resourceand returns its value (which it will cache).Link relation docs~~~~~~~~~~~~~~~~~~Let's register a hal talk account. Unfortunately, we don't really knowhow to do that, so let's look at the documentation. The ``ht:signup``link looks promising, let's check that:.. code:: python>>> N.docsfor('ht:signup')A browser will open to http://haltalk.herokuapp.com/rels/signup.What? Popping up a browser from a library call? Yes, that's howrest\_navigator rolls. The way we see it: docs are for humans, and whilecustom rel-types are URIs, they shouldn't automatically be dereferencedby a program that interacts with the api. So popping up a browser servestwo purposes:1. It allows easy access to the documentation at the time when you mostneed it: when you're mucking about in the command line trying tofigure out how to interact with the api.2. It reminds you not to try to automatically dereference the reldocumentation and parse it in your application.If you need a more robust way to browse the api and the documentation,`HAL Browser <https://github.com/mikekelly/hal-browser>`__ is probablyyour best bet.POST requests~~~~~~~~~~~~~The docs for ``ht:signup`` explain the format of the POST request tosign up. So let's actually sign up. Since we've set ``"ht"`` as ourdefault curie, we can skip typing the curie for convenience. (Note:haltalk is a toy api for example purposes, don't ever send plaintextpasswords over an unencrypted connection in a real app!):.. code:: python>>> fred23 = N['signup'].create(... {'username': 'fred23',...  'password': 'hunter2',...  'real_name': 'Fred 23'}... )>>> fred23HALNavigator(Haltalk.users.fred23)Errors~~~~~~If the user name had already been in use, a 400 would have been returnedfrom the haltalk api. rest\_navigator follows the Zen of Pythonguideline "Errors should never pass silently". An exception would havebeen raised on a 400 or 500 status code. You can squelch this exceptionand just have the post call return a ``HALNavigator`` with a 400/500status code if you want:.. code:: python>>> dup_signup = N['ht:signup'].create({...    'username': 'fred23',...    'password': 'hunter2',...    'real_name': 'Fred Wilson'... }, raise_exc=False)>>> dup_signupOrphanHALNavigator(Haltalk.signup)  # 400!>>> dup_signup.status(400, 'Bad Request')>>> dup_signup.state{u"errors": {u"username": [u"is already taken"]}}Templated links~~~~~~~~~~~~~~~Now that we've signed up, lets take a look at our profile. The link fora user's profile is a templated link, which restnavigator represents asa ``PartialNavigator``. Similar to python's`functools.partial <https://docs.python.org/2/library/functools.html#functools.partial>`__,a ``PartialNavigator`` is an object that needs a few more arguments togive you a full navigator back. Despite its name, it can't talk to thenetwork by itself. Its job is to to generate new navigators for you. Youcan see what variables it has by looking at its ``.variables`` attribute(its ``__repr__`` hints at this as well):.. code:: python>>> N.links().keys()['ht:latest-posts', 'ht:me', 'ht:users', 'ht:signup']>>> N['ht:me']PartialNavigator(Haltalk.users.{name})>>> N['ht:me'].variablesset(['name'])The documentation for the ``ht:me`` rel type should tell us how the nameparameter is supposed to work, but in this case it's fairly obvious(plug in the username). Two provide the template parameters, just callit with keyword args:.. code:: python>>> partial_me = N['ht:me']>>> partial_me.template_uri'http://haltalk.herokuapp.com/users/{name}'>>> Fred = partial_me(name='fred23')>>> FredHALNavigator('haltalk.users.fred23')Now that we have a real navigator, we can fetch the resource:.. code:: python>>> Fred(){u'bio': None, u'real_name': u'Fred Wilson', u'username': u'fred23'}Authentication~~~~~~~~~~~~~~In order to post something to haltalk, we need to authenticate with ournewly created account. HALNavigator allows any `authentication methodthat requestssupports <http://www.python-requests.org/en/latest/user/advanced/#custom-authentication>`__(so OAuth etc). For basic auth (which haltalk uses), we can just pass atuple... code:: python>>> N.authenticate(('fred23', 'hunter2'))  # All subsequent calls are authenticatedThis doesn't send anything to the server, it just sets theauthentication details that we'll use on the next request. Otherauthentication methods may contact the server immediately.Now we can put it all together to create a new post:.. code:: python>>> N_post = N['me'](name='fred23')['posts'].create({'content': 'My first post'})>>> N_postHALNavigator(Haltalk.posts.523670eff0e6370002000001)>>> N_post(){'content': 'My first post', 'created_at': '2015-06-13T19:38:59+00:00'}It is also possible to specify a custom requests Session object when creatinga new navigator.For example, if you want to talk to a OAuth2 protected api, simply passan OAuth2 Session object that will be used for all requestsdone by HALNavigator:.. code:: python>>> from requests_oauthlib import OAuth2Session>>> oauth2_session = OAuth2Session(r'client_id', token='token')>>> N = Navigator.hal('https://api.example.com', session=oauth2_session)Additional Topics-----------------Identity Map~~~~~~~~~~~~You don't need to worry about inadvertently having two differentnavigators pointing to the same resource. rest\_navigator will reuse theexisting navigator instead of creating a new oneIterating over a Navigator~~~~~~~~~~~~~~~~~~~~~~~~~~If a resource has a link with the rel "next", the navigator for thatresource can be used as a python iterator. It will automatically raise aStopIteration exception if a resource in the chain does not have a nextlink. This makes moving through paged resources really simple andpythonic:.. code:: pythonpost_navigator = fred['ht:posts']for post in post_navigator:# the first post will be post_navigator itselfprint(post.state)Headers (Request vs. Response)~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~HTTP response headers are available in ``N.response.headers``Headers that will be sent on each request can be obtained through thesession:.. code:: python>>> N.session.headers# Cookies, etcBracket mini-language~~~~~~~~~~~~~~~~~~~~~The bracket (``[]``) operator on Navigators has a lot of power. As wesaw earlier, the main use is to get a new Navigator from a linkrelation:.. code:: python>>> N2 = N['curie:link_rel']But, it can also go more than one link deep, which is equivalent tousing multiple brackets in a row:.. code:: python>>> N3 = N['curie:first_link', 'curie:second_link']# equivalent to:N3 = N['curie:first_link']['curie:second_link']And of course, if you set a default curie, you can omit it:.. code:: python>>> N3 = N['first_link', 'second_link']Internally, this is completely equivalent to repeatedly applying thebracket operator, so you can even use it to jump over intermediateobjects that aren't Navigators themselves:.. code:: python>>> N['some-link', 3, 'another-link']This would use the ``some-link`` link relation, select the third linkfrom the list, and then follow ``another-link`` from that resource.Finding the right link~~~~~~~~~~~~~~~~~~~~~~Normally, you can chain together brackets to jump from one resource toanother in one go:.. code:: python>>> N['ht:widget']['ht:gadget']This will return a Navigator for the ``ht:widget`` link relation andthen immediately fetch the resource and return a Navigator for the``ht:gadget`` link relation. This works great if you have only one linkper relation, but HAL allows multiple links per relation. Say forinstance we have some links like the following:.. code:: javascript{"ht:some_rel: [{"href": "/api/widget/1","name": "widget1","profile": "widget"},{"href": "/api/widget/2","name": "widget2","profile": "widget"},{"href": "/api/gadget/1","name": "gadget1","profile": "gadget"}]When we go to get the ``ht:some_rel``, we'll get multiple results:.. code:: python>>> N['ht:some_rel'][HALNavigator(api.widget[1]),HALNavigator(api.widget[2]),HALNavigator(api.gadget[1])]How do we know which one is the one we want? The `HALspec <https://tools.ietf.org/html/draft-kelly-json-hal-06#section-5.5>`__says links with the same rel can be disambiguated by the ``name`` linkproperty:.. code:: python>>> N.links['ht:some_rel'].get_by('name', 'gadget1')HALNavigator(api.gadget[1])>>> N.links['ht:some_rel'].named('gadget1')  # same as previousHALNavigator(api.gadget[1])We could also use other properties to slice and dice the list:.. code:: python>>> N.links['ht:some_rel'].get_by('profile', 'gadget')HALNavigator(api.gadget[1])>>> N.links['ht:some_rel'].getall_by('profile', 'widget')[HALNavigator(api.widget[1]), HALNavigator(api.widget[2])]This works for any property on links, not just the standard HALproperties.Default curie~~~~~~~~~~~~~You may specify a default curie when creating your Navigator:.. code:: python>>> N = HALNavigator('http://haltalk.herokuapp.com', curie='ht')Now, when you follow links, you may leave off the default curie if youwant:.. code:: python>>> N.links{'ht:users': [HALNavigator(Haltalk.users)],'ht:signup': [HALNavigator(Haltalk.signup)],'ht:me': [HALNavigator(Haltalk.users.{name})],'ht:latest-posts': [HALNavigator(Haltalk.posts.latest)]}>>> N['ht:users']HALNavigator(Haltalk.users)>>> N['users']HALNavigator(Haltalk.users)The only exception is where the key being supplied is a `IANA registeredlinkrelation <http://www.iana.org/assignments/link-relations/link-relations.xhtml>`__,and there is a conflict (hint: this should be quite rare):.. code:: python>>> N.links{'ht:next': HALNavigator(Haltalk.unregistered),'next': HALNavigator(Haltalk.registered)}>>> N['next']HALNavigator(Haltalk.registered)Specifying an api name~~~~~~~~~~~~~~~~~~~~~~Sometimes the automatic api naming guesses poorly. If you'd like tooverride the default name, you can specify it when creating thenavigator:.. code:: python>>> N = Navigator.hal('http://api.example.com', apiname='MySpecialAPI')HALNavigator(MySpecialAPI)Embedded documents~~~~~~~~~~~~~~~~~~In rest\_navigator, embedded documents are treated transparently. Thismeans that in many cases you don't need to worry about whether adocument is embedded or whether it's just linked.As an example, assume we have a resource like the following:.. code:: json{"_links": {..."xx:yams": {"href": "/yams"}...},"_embedded": {"xx:pickles": {"_links": {"self": {"href": "/pickles"}},"state": "A pickle"}}...}From here, you would access both the ``yams`` and the ``pickles``resource with normal bracket syntax:.. code:: python>>> Yams = N['xx:yams']>>> Pickles = N['xx:pickles']The only difference here is that ``Yams`` hasn't been fetched yet, while``Pickles`` is considered "resolved" already because we got it as anembedded document.::>>> Yams.resolvedFalse>>> Yams.state # None>>> Pickles.resolvedTrue>>> Pickles.state{'state': 'A pickle'}If an embedded document has a self link, you can treat it just like youwould any other resource. So if you want to refresh the resource, it'sas easy as:.. code:: python>>> Pickles.fetch()This will fetch the current state of the resource from the uri in itsself link, even if you've never directly requested that uri before. Ifan embedded resource doesn't have a self link, it will be an``OrphanNavigator`` with the parent set to the resource it was embeddedin.Of course, if you need to directly distinguish between linked resourcesand embedded resources, there is an out:.. code:: python>>> N.embedded(){'xx:pickles': HALNavigator(api.pickles)>>> N.links(){'xx:yams': HALNavigator(api.yams)However, when using the ``in`` operator, it will look in both for a keyyou're interested in:.. code:: python>>> 'yams' in N  # default curie is taken into account!True>>> 'xx:yams in NTrue>>> 'xx:pickles' in NTrueDevelopment-----------Testing~~~~~~~To run tests, first install the `pytestframework <http://pytest.org/latest/getting-started.html>`__:::$ pip install -U pytestTo run tests, execute following from the root of the source directory:::$ py.testPlanned for the future~~~~~~~~~~~~~~~~~~~~~~-  Ability to add hooks for different types, rels and profiles. If alink has one of these properties, it will call your hook when doing aserver call.-  Since HAL doesn't specify what content type POSTs, PUTs, and PATCHesneed to have, you can specify the hooks based on what the server willaccept. This can trigger off either the rel type of the link, or restnavigator can do content negotiation over HTTP with the serverdirectly to see what content types that resource will accept.Contributors~~~~~~~~~~~~Thanks very much to rest navigator's contributors:-  `dudycooly <http://github.com/dudycooly>`__-  `bubenkoff <http://github.com/bubenkoff>`__-  `bbsgfalconer <http://github.com/bbsgfalconer>`__.. |Build Status| image:: https://img.shields.io/travis/deontologician/restnavigator/next.svg:target: https://travis-ci.org/deontologician/restnavigator.. |Coverage Status| image:: https://img.shields.io/coveralls/deontologician/rest_navigator/next.svg:target: https://coveralls.io/r/deontologician/rest_navigator?branch=next.. |Documentation Status| image:: https://readthedocs.org/projects/rest-navigator/badge/?version=latest:target: https://readthedocs.org/projects/rest-navigator/?badge=latest:alt: Documentation Status.. |Pypi Status| image:: https://pypip.in/v/restnavigator/badge.png:target: https://crate.io/packages/restnavigator/Changelog=========Unreleased----------- TBD1.0---- Embedded support- Ability to specify default curies- Resources with no URL are now represented by a special Navigator type called OrphanNavigators- IP addresses can be used in the url (@dudycooly)- All tests pass in python 2.6 -> 3.4 (@bubenkoff), and travis now runs tox to ensure they stay that way- Support the DELETE, and PATCH methods- posts allow an empty body (@bbsgfalconer)- Much improved content negotiation (@bbsgfalconer)- There was also a major refactoring that changed how Navigators are created and internally cleaned up alot of really messy code.