-
Notifications
You must be signed in to change notification settings - Fork 110
API Development Documentation #673
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,277 @@ | ||
Developing new APIs | ||
=================== | ||
|
||
Freeseer uses a REST API framework to remotely control a headless Freeseer | ||
host. This article will give a primer on how APIs are designed, and how they | ||
are implemented with `Flask <http://flask.pocoo.org>`_. | ||
|
||
RESTful API Primer | ||
------------------ | ||
|
||
Freeseer's API strives to be RESTful, so you should learn the basics of REST | ||
theory to ensure that your endpoint is in fact RESTful. What follows is by no | ||
means a replacement for figuring out how RESTful APIs and endpoints are | ||
designed, but it is useful for understanding some of the design choices for our | ||
framework. | ||
|
||
A REST API is a resource based interface for interacting with software. The | ||
most common use case for a REST API, is to allow us to interact with software | ||
across a network, without requiring any knowledge of the software's internals. | ||
Any parts of your software you wish to control externally are abstracted into | ||
resources and signified by URIs called endpoints. So in other words to design | ||
and develop a RESTful API is to design and develop endpoints that will allow | ||
us to interact with parts of your software from the outside. If some set of | ||
endpoints is logically related, they are grouped into a set called an API | ||
(For example, the Recording API, or the Configuration API). | ||
|
||
So, to keep the somewhat vague terminology straight: generally when people talk | ||
about a “RESTful API”, they are referring to the entire RESTful API and also | ||
perhaps the framework on which the endpoints and APIs are built, but when they | ||
refer to “an API”, “the <insert group name here> API”, or some variation, they | ||
are referring to some specific API or set of APIs. | ||
|
||
Designing Endpoints | ||
------------------- | ||
|
||
In the most simplistic and crude sense, a RESTful endpoint will be the plural | ||
of the type of resource that you want perform a ``GET``, ``POST``, ``DELETE``, | ||
or ``PATCH`` on. For example if you want to have RESTful endpoint for handling | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we also want to include PUT in that list? |
||
a server's users, you could have an endpoint named: | ||
|
||
``/users`` | ||
|
||
You could either get a list of all users with | ||
|
||
``GET /users`` | ||
|
||
Or post a new user with | ||
|
||
``POST /users`` | ||
|
||
Or if you want to get specific instances of that resource, you use some | ||
identifier. | ||
|
||
``GET /users/1`` - to get user with id “1” | ||
|
||
``DELETE /users/1`` - to delete user with id “1” | ||
|
||
You also want to have some parameters for your endpoint. For example for | ||
creating a user, you will want some set of parameters like, username, email, | ||
etc. | ||
|
||
**From Endpoints to an API** | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I find this paragraph adds little value. Thoughts on removing it to keep the content short and focused? |
||
|
||
In the Freeseer RESTful API there is a logical organization to our endpoints, | ||
which we refer to as APIs. For example, one set of RESTful endpoints is grouped | ||
under the Recording API, where each resource is related to creating, deleting, | ||
accessing, or performing an action on some recording. Very rarely is an endpoint | ||
designed in isolation, we consider what API needs to be developed first and then | ||
think of what endpoints would fall under that API. So make sure that your | ||
endpoint either falls under and existing API or create a new API for that | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. s/and/an/ |
||
endpoint to fall under. | ||
|
||
Developing an API | ||
----------------- | ||
|
||
**Note:** From this point forward we assume that you are creating a new API | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sphinx has markup for creating notes and other stuff. See http://sphinx-doc.org/markup/para.html#directive-note |
||
(a new logically grouped set of RESTful API endpoints). If you simply need to | ||
add new endpoint to an existing API, you can skip ahead to *Developing Endpoints* | ||
|
||
**Creating an API module** | ||
|
||
In the ``freeseer/frontend/controller/`` folder, create a new module | ||
<api_name>.py (replacing <api_name> with the name of your api. You will need to | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing a closing |
||
import the ``Blueprint`` and ``request`` modules from flask with: | ||
|
||
.. code-block:: python | ||
|
||
from flask import Blueprint | ||
from flask import request | ||
|
||
You will also need to import the following: | ||
|
||
.. code-block:: python | ||
|
||
from freeseer.frontend.controller import app | ||
|
||
``app`` is the server's ``Flask`` app. | ||
|
||
**Using Blueprints** | ||
|
||
To organize our endpoints into separate APIs, we make use of Flask's | ||
``Blueprint`` module. All of our endpoints and API specific code and data will | ||
exist in an instance of the ``Blueprint`` module, which simply extends the | ||
existing Flask server app. | ||
|
||
To instantiate our API, we add the following code to our API module. | ||
|
||
.. code-block:: python | ||
|
||
<api_name> = Blueprint('<api_name>', __name__) | ||
|
||
Then we need to associate our Blueprint with the Flask app, to do this we add | ||
code to ``freeseer/frontend/controller/__init__.py``. | ||
|
||
.. code-block:: python | ||
|
||
from freeseer.frontend.controller.<api_name> import <api_name> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it'd be nicer if we use an example name (e.g. |
||
|
||
app.register(<api_name>) | ||
|
||
**API-specific Functions and Data** | ||
|
||
Outside of the endpoints, there are a number of functions an API may need to | ||
function properly. For example, the ``recording`` api needs to instantiate the | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. s/api/API/. Also occurs in a few other places. |
||
multimedia backend for any of its endpoints to work. The ``Blueprint`` can | ||
provide us with a number of decorators to wrap any functions that would be | ||
necessary for the functioning of our api. Furthermore, any api specific data can | ||
be saved to the ``Blueprint`` object. | ||
|
||
One of the most useful for developers will be the | ||
``@<name_of_api>.before_first_request`` decorator. Any code that needs to be run | ||
so that the endpoints can function should be decorated by this decorator so it | ||
can run before the first request is made to the REST framework. For example, in | ||
the recording api, we have a function called ``configure_recording()`` that | ||
loads references to existing videos from disk so our endpoints will work. By | ||
wrapping it with ``@recording.before_app_first_request``, that code will fire | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Earlier it says |
||
when the first call to the REST API is made. | ||
|
||
|
||
Developing Endpoints | ||
-------------------- | ||
|
||
**Route decorator** | ||
|
||
Every endpoint is wrapped with a ``@<name_of_api>.route()`` decorator. | ||
|
||
**Decorator Parameters** | ||
|
||
:rule: the first parameter of the ``route()`` function. The path of the | ||
endpoint with any path parameters declared. Ex. route('/users') will establish | ||
a route at to http://<host_info>/users | ||
|
||
:methods: a list of all methods (GET, POST, etc.) this route accepts. Example: | ||
``route('users/<int:id>', methods=['GET'])`` means this function will only fire | ||
if a GET request is sent to the corresponding path. | ||
|
||
More information about route registration can be found in the `Flask | ||
documentation <http://flask.pocoo.org/docs/0.10/api/#url-route-registration>`_ | ||
|
||
**Path parameters** | ||
|
||
Any path parameters are specified with angular brackets. Ex. | ||
``route('/users/<username>')`` means any text entered after ``/users/`` will be | ||
saved as a string under the variable ``username``. | ||
|
||
If you want your parameter to be coerced to a certain type, you use the format | ||
<type:name> | ||
|
||
Available types include int, and float. | ||
|
||
**Request Body** | ||
|
||
In Flask, for an endpoint to accept a parameters from a request body, we don't | ||
need to explicitly declare body parameters in our function definition or route | ||
decorator. A function can examine the body of data sent by some client by | ||
reading the data found in 'request.form' where our body would be contained. | ||
|
||
|
||
**Request validation** | ||
|
||
Obviously we want some way to ensure our endpoint gets the right kind of data | ||
(in our case, ``JSON`` formatted), and gets the data the endpoint expects. So we | ||
have added a module called ``validate`` that ensures the body data is the | ||
correct format, and contains the data the endpoint needs to function. | ||
|
||
The validate module validates request data through | ||
``validate_form(to_validate, schema)`` | ||
|
||
**Function Parameters:** | ||
|
||
:to_validate: the body data of our request. In most cases this will be | ||
'request.form'. | ||
:schema: a `jsonschema <http://json-schema.org>`_ formatted schema to describe | ||
what our request data should look like. | ||
|
||
If the validation fails, ``validate_form()`` throws an ``HTTPError`` which will | ||
be sent to the client as a response. | ||
|
||
**Validation Schemas** | ||
|
||
Depending on the nature of your API, your validation schema may be | ||
automatically generated. | ||
|
||
.. todo:: Fill in information about how validation schemas are automatically | ||
generated | ||
|
||
If your schema is not auto-generated, you may have to include any relevant | ||
schemas in the Blueprint object. (In the case of Recording API, we store the | ||
schemas in a dictionary called form_schema.) | ||
|
||
We use the library jsonschema to validate our json objects. The json-schema | ||
`documentation <http://json-schema.org>`_ will have any information you need for | ||
creating json schemas to validate data against. | ||
|
||
**Returning a response** | ||
|
||
For your function to return information back to the client, the endpoint | ||
function needs to return a ``dict`` which represents the JSON object that will | ||
be the body of the response returned by the server. | ||
|
||
By wrapping our endpoint function with ``@http_response(status_code)`` | ||
(status_code being the HTTP status code that indicates success), the ``dict`` | ||
and ``status_code`` become the basis for our response to the client. The | ||
decorator should go between the route decorator and the endpoint function. | ||
|
||
**Error handling** | ||
|
||
Our endpoints needs some way of handling requests that would cause our endpoint | ||
functions to fail, and alert the client that their request was faulty. We do | ||
this by catching the error as it happens, or pre-empting it via some validation, | ||
and sending a response back to the client that includes error information for | ||
why the request failed. For example, an endpoint receiving a request for a | ||
non-existent resources like a non-existing user. | ||
|
||
When we do run into one of these errors, we need to send a response with an | ||
appropriate status code, and error information in our responses body. In the | ||
case of a non-existent recording, we alert the user with a 404 status code, and | ||
our response body will be a JSON object that includes a useful message such as | ||
'No recording with id <id> was found.' | ||
|
||
**HTTPError** | ||
|
||
If we encounter some error, we always raise an ``HTTPError`` in our endpoint | ||
function if that error is to be returned to the client. | ||
|
||
**HTTPError Parameters** | ||
|
||
:status_code: | ||
the HTTP Error code that corresponds to our error. The error codes supported | ||
at present are below (more can be added as needed) | ||
:: | ||
|
||
400: 'Bad Request: Request could not be understood due to malformed syntax.' | ||
401: 'Unauthorized: Authentication was not provided or has failed.' | ||
404: 'Not Found: Requested resource is not available.' | ||
409: 'Conflict: Request could not be processed because of server conflict.' | ||
422: 'Unprocessable Entity: Request could not be processed due to semantic errors.' | ||
|
||
|
||
:description: | ||
a string containing human readable information that a client user would find | ||
informative, and rectify the issue. If we don't supply a description method, | ||
the user will only read a generic message corresponding to the status code. | ||
|
||
**Errors Handled by the Framework** | ||
|
||
In some situations the framework or another module already handles these errors | ||
for us, so we do not need to worry about them. (The following list may not be | ||
exhaustive, feel free to add more) | ||
|
||
**Faulty Path Parameters:** If path parameters cannot be coerced to the type | ||
specified by the route's rule parameter, it will send the client a response | ||
with error information. | ||
|
||
**Validation Errors:** when we call the validate_form method from the validate | ||
module, the validate module will always raise an HTTPError and supply | ||
appropriate information. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"plural form"
"want to"