Mercurial > p > roundup > code
changeset 5660:d8d2b7724292
First attempt at REST-API documentation
Also fix operator of patch to be '@op', not 'op'.
Example for retire/restore using both, DELETE and PATCH with @op=action.
| author | Ralf Schlatterbeck <rsc@runtux.com> |
|---|---|
| date | Fri, 22 Mar 2019 11:23:02 +0100 |
| parents | 1e51a709431c |
| children | b08a308c273b |
| files | doc/rest.txt roundup/rest.py |
| diffstat | 2 files changed, 155 insertions(+), 8 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/doc/rest.txt Fri Mar 22 11:23:02 2019 +0100 @@ -0,0 +1,147 @@ + +==================== +REST API for Roundup +==================== + +.. contents:: + :local: + +Introduction +------------ + +After the last 1.6.0 Release, a REST-API developed in 2015 during a Google +Summer of Code (GSOC) by Chau Nguyen, supervised by Ezio Melotti was +integrated. The code was then updated by John Rouillard and Ralf +Schlatterbeck to fix some shortcomings and provide the necessary +functions for a single page web application, e.g. etag support among +others. + +Enabling the REST API +--------------------- + +The REST API can be disabled in the ``[web]`` section of ``config.ini`` +via the variable ``enable_rest`` which is ``yes`` by default. + +The REST api is reached via the ``/rest/`` endpoint of the tracker URL. + + +Client API +---------- + +The top-level REST url ``/rest/`` will display the current version of +the REST API (Version 1 as of this writing) and some links to relevant +endpoints of the API. In the following the ``/rest`` prefix is ommitted +from relative REST-API links for brevety. + +Summary +======= + +A Summary page can be reached via ``/data/summary`` via the ``GET`` method. +This is currently hard-coded for the standard tracker schema shipped +with roundup and will display a summary of open issues. + +Data +==== + +All the links mentioned in the following support the http method ``GET``. +Results of a ``GET`` request will always return the results as a +dictionary with the entry ``data`` referring to the returned data. + +The ``/data`` link will display a set of classes of the tracker. All +classes can be reached via ``/data/<classname>`` where ``<classname>`` +is replace with the name of the class to query, e.g. ``/data/issue``. +Individual items of a class (e.g. a single issue) can be queried by +giving the issue-id, e.g., ``/data/issue/42``. Individual properties of +an item can be queried by appending the property, e.g., +``/data/issue/42/title``. + +When performing the ``GET`` method on a class (e.g. ``/data/issue``), the +number of items is returned in ``@total_size``. Then a ``collection`` +list follows which contains the id and link to the respective item. + +When performing the ``GET`` method on an item (e.g. ``/data/issue/42``), a +``link`` attribute contains the link to the item, ``id`` contains the +id, type contains the class name (e.g. ``issue`` in the example) and an +``etag`` property can be used to detect modifications since the last +query. The individual properties of the item are returned in an +``attributes`` dictionary. The properties returned depend on the +permissions of the account used for the query. + +A ``GET`` method on a property (e.g. ``/data/issue/42/title``) returns the +link, an ``@etag``, the type of the property (e.g. "<type str>") the id +of the item and the content of the property in ``data``. + +Only class links support the ``POST`` method for creation of new items +of a class, e.g., a new issue via the ``/data/issue`` link. The post +gets a dictionary of keys/values for the new item. It returns the same +parameters as the GET method after successful creation. + +All endpoints support an ``OPTIONS`` method for determining which +methods are allowed on a given endpoint. + +The method ``PUT`` is allowed on individual items, e.g. +``/data/issue/42`` as well as properties, e.g., +``/data/issue/42/title``. On success it returns the same parameters as +the respective ``GET`` method. Note that for ``PUT`` an Etag has to be +supplied, either in the request header or as an @etag parameter. + +The method ``DELETE`` is allowed on items, e.g., ``/data/issue/42`` and +will retire (mark as deleted) the respective item. On success it will +only return a status code. It is also possible to call ``DELETE`` on a +property of an item, e.g., ``/data/issue/42/nosy`` to delete the nosy +list. The same effect can be achieved with a ``PUT`` request and an +empty new value. + +Finally the ``PATCH`` method can be applied to individual items, e.g., +``/data/issue/42`` and to properties, e.g., ``/data/issue/42/title``. +This method gets an operator ``@op=<method>`` where ``<method`` is one +of ``add``, ``replace``, ``remove``, only for an item (not for a +property) an additional operator ``action`` is supported. If no operator +is specified, the default is ``replace``. The first three operators are +self explanatory. For an ``action`` operator an ``@action_name`` and +optional ``@action_argsXXX`` parameters have to be supplied. Currently +there are only two actions without parameters, namely ``retire`` and +``restore``. The ``retire`` action on an item is the same as a +``DELETE`` method, it retires the item. The ``restore`` action is the +inverse of ``retire``, the item is again visible. +On success the returned value is the same as the respective ``GET`` +method. + +sample python client +==================== + +The client uses the python ``requests`` library for easier interaction +with a REST API supporting JSON encoding:: + + + >>> import requests + >>> u = 'http://user:password@tracker.example.com/demo/rest/data/' + >>> s = requests.session() + >>> r = s.get(u + 'issue/42/title') + >>> if r.status_code != 200: + ... print("Failed: %s: %s" % (r.status_code, r.reason)) + ... exit(1) + >>> print (r.json() ['data']['data'] + TEST Title + >>> r = s.post (u + 'issue', data = dict (title = 'TEST Issue')) + >>> if not 200 <= r.status_code <= 201: + ... print("Failed: %s: %s" % (r.status_code, r.reason)) + ... exit(1) + >>> print(r.json()) + +Retire/Restore:: + >>> r = s.delete (u + 'issue/42') + >>> print (r.json()) + >>> r = s.get (u + 'issue/42') + >>> etag = r.headers['ETag'] + >>> print("ETag: %s" % etag) + >>> etag = r.json()['data']['@etag'] + >>> print("@etag: %s" % etag) + >>> h = dict(ETag = etag) + >>> d = {'@op:'action', '@action_name':'retire'} + >>> r = s.patch(u + 'issue/42', data = d, headers = h) + >>> print(r.json()) + >>> d = {'@op:'action', '@action_name':'restore'} + >>> r = s.patch(u + 'issue/42', data = d, headers = h) + >>> print(r.json()) +
--- a/roundup/rest.py Fri Mar 22 09:56:10 2019 +0100 +++ b/roundup/rest.py Fri Mar 22 11:23:02 2019 +0100 @@ -786,7 +786,7 @@ class_name, item_id): raise PreconditionFailed("Etag is missing or does not match." - "Retreive asset and retry modification if valid.") + " Retrieve asset and retry modification if valid.") result = class_obj.set(item_id, **props) self.db.commit() except (TypeError, IndexError, ValueError) as message: @@ -841,7 +841,7 @@ obtain_etags(self.client.request.headers, input), class_name, item_id): raise PreconditionFailed("Etag is missing or does not match." - "Retreive asset and retry modification if valid.") + " Retrieve asset and retry modification if valid.") result = class_obj.set(item_id, **props) self.db.commit() except (TypeError, IndexError, ValueError) as message: @@ -932,7 +932,7 @@ class_name, item_id): raise PreconditionFailed("Etag is missing or does not match." - "Retreive asset and retry modification if valid.") + " Retrieve asset and retry modification if valid.") class_obj.retire (item_id) self.db.commit() @@ -982,7 +982,7 @@ class_name, item_id): raise PreconditionFailed("Etag is missing or does not match." - "Retreive asset and retry modification if valid.") + " Retrieve asset and retry modification if valid.") class_obj.set(item_id, **props) self.db.commit() @@ -1022,7 +1022,7 @@ if class_name not in self.db.classes: raise NotFound('Class %s not found' % class_name) try: - op = input['op'].value.lower() + op = input['@op'].value.lower() except KeyError: op = self.__default_patch_op class_obj = self.db.getclass(class_name) @@ -1032,7 +1032,7 @@ class_name, item_id): raise PreconditionFailed("Etag is missing or does not match." - "Retreive asset and retry modification if valid.") + " Retrieve asset and retry modification if valid.") # if patch operation is action, call the action handler action_args = [class_name + item_id] @@ -1121,7 +1121,7 @@ if class_name not in self.db.classes: raise NotFound('Class %s not found' % class_name) try: - op = input['op'].value.lower() + op = input['@op'].value.lower() except KeyError: op = self.__default_patch_op @@ -1141,7 +1141,7 @@ class_name, item_id): raise PreconditionFailed("Etag is missing or does not match." - "Retreive asset and retry modification if valid.") + " Retrieve asset and retry modification if valid.") props = { prop: self.prop_from_arg(
