mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-25 11:04:02 +03:00
Schemas & client libraries. (#4179)
* Added schema generation support. * New tutorial section. * API guide on schema generation. * Topic guide on API clients.
This commit is contained in:
parent
1d2fba906e
commit
6ff9840bde
383
docs/api-guide/schemas.md
Normal file
383
docs/api-guide/schemas.md
Normal file
|
@ -0,0 +1,383 @@
|
|||
source: schemas.py
|
||||
|
||||
# Schemas
|
||||
|
||||
> A machine-readable [schema] describes what resources are available via the API, what their URLs are, how they are represented and what operations they support.
|
||||
>
|
||||
> — Heroku, [JSON Schema for the Heroku Platform API][cite]
|
||||
|
||||
API schemas are a useful tool that allow for a range of use cases, including
|
||||
generating reference documentation, or driving dynamic client libraries that
|
||||
can interact with your API.
|
||||
|
||||
## Representing schemas internally
|
||||
|
||||
REST framework uses [Core API][coreapi] in order to model schema information in
|
||||
a format-independent representation. This information can then be rendered
|
||||
into various different schema formats, or used to generate API documentation.
|
||||
|
||||
When using Core API, a schema is represented as a `Document` which is the
|
||||
top-level container object for information about the API. Available API
|
||||
interactions are represented using `Link` objects. Each link includes a URL,
|
||||
HTTP method, and may include a list of `Field` instances, which describe any
|
||||
parameters that may be accepted by the API endpoint. The `Link` and `Field`
|
||||
instances may also include descriptions, that allow an API schema to be
|
||||
rendered into user documentation.
|
||||
|
||||
Here's an example of an API description that includes a single `search`
|
||||
endpoint:
|
||||
|
||||
coreapi.Document(
|
||||
title='Flight Search API',
|
||||
url='https://api.example.org/',
|
||||
content={
|
||||
'search': coreapi.Link(
|
||||
url='/search/',
|
||||
action='get',
|
||||
fields=[
|
||||
coreapi.Field(
|
||||
name='from',
|
||||
required=True,
|
||||
location='query',
|
||||
description='City name or airport code.'
|
||||
),
|
||||
coreapi.Field(
|
||||
name='to',
|
||||
required=True,
|
||||
location='query',
|
||||
description='City name or airport code.'
|
||||
),
|
||||
coreapi.Field(
|
||||
name='date',
|
||||
required=True,
|
||||
location='query',
|
||||
description='Flight date in "YYYY-MM-DD" format.'
|
||||
)
|
||||
],
|
||||
description='Return flight availability and prices.'
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
## Schema output formats
|
||||
|
||||
In order to be presented in an HTTP response, the internal representation
|
||||
has to be rendered into the actual bytes that are used in the response.
|
||||
|
||||
[Core JSON][corejson] is designed as a canonical format for use with Core API.
|
||||
REST framework includes a renderer class for handling this media type, which
|
||||
is available as `renderers.CoreJSONRenderer`.
|
||||
|
||||
Other schema formats such as [Open API][open-api] (Formerly "Swagger"),
|
||||
[JSON HyperSchema][json-hyperschema], or [API Blueprint][api-blueprint] can
|
||||
also be supported by implementing a custom renderer class.
|
||||
|
||||
## Schemas vs Hypermedia
|
||||
|
||||
It's worth pointing out here that Core API can also be used to model hypermedia
|
||||
responses, which present an alternative interaction style to API schemas.
|
||||
|
||||
With an API schema, the entire available interface is presented up-front
|
||||
as a single endpoint. Responses to individual API endpoints are then typically
|
||||
presented as plain data, without any further interactions contained in each
|
||||
response.
|
||||
|
||||
With Hypermedia, the client is instead presented with a document containing
|
||||
both data and available interactions. Each interaction results in a new
|
||||
document, detailing both the current state and the available interactions.
|
||||
|
||||
Further information and support on building Hypermedia APIs with REST framework
|
||||
is planned for a future version.
|
||||
|
||||
---
|
||||
|
||||
# Adding a schema
|
||||
|
||||
You'll need to install the `coreapi` package in order to add schema support
|
||||
for REST framework.
|
||||
|
||||
pip install coreapi
|
||||
|
||||
REST framework includes functionality for auto-generating a schema,
|
||||
or allows you to specify one explicitly. There are a few different ways to
|
||||
add a schema to your API, depending on exactly what you need.
|
||||
|
||||
## Using DefaultRouter
|
||||
|
||||
If you're using `DefaultRouter` then you can include an auto-generated schema,
|
||||
simply by adding a `schema_title` argument to the router.
|
||||
|
||||
router = DefaultRouter(schema_title='Server Monitoring API')
|
||||
|
||||
The schema will be included at the root URL, `/`, and presented to clients
|
||||
that include the Core JSON media type in their `Accept` header.
|
||||
|
||||
$ http http://127.0.0.1:8000/ Accept:application/vnd.coreapi+json
|
||||
HTTP/1.0 200 OK
|
||||
Allow: GET, HEAD, OPTIONS
|
||||
Content-Type: application/vnd.coreapi+json
|
||||
|
||||
{
|
||||
"_meta": {
|
||||
"title": "Server Monitoring API"
|
||||
},
|
||||
"_type": "document",
|
||||
...
|
||||
}
|
||||
|
||||
This is a great zero-configuration option for when you want to get up and
|
||||
running really quickly. If you want a little more flexibility over the
|
||||
schema output then you'll need to consider using `SchemaGenerator` instead.
|
||||
|
||||
## Using SchemaGenerator
|
||||
|
||||
The most common way to add a schema to your API is to use the `SchemaGenerator`
|
||||
class to auto-generate the `Document` instance, and to return that from a view.
|
||||
|
||||
This option gives you the flexibility of setting up the schema endpoint
|
||||
with whatever behaviour you want. For example, you can apply different
|
||||
permission, throttling or authentication policies to the schema endpoint.
|
||||
|
||||
Here's an example of using `SchemaGenerator` together with a view to
|
||||
return the schema.
|
||||
|
||||
**views.py:**
|
||||
|
||||
from rest_framework.decorators import api_view, renderer_classes
|
||||
from rest_framework import renderers, schemas
|
||||
|
||||
generator = schemas.SchemaGenerator(title='Bookings API')
|
||||
|
||||
@api_view()
|
||||
@renderer_classes([renderers.CoreJSONRenderer])
|
||||
def schema_view(request):
|
||||
return generator.get_schema()
|
||||
|
||||
**urls.py:**
|
||||
|
||||
urlpatterns = [
|
||||
url('/', schema_view),
|
||||
...
|
||||
]
|
||||
|
||||
You can also serve different schemas to different users, depending on the
|
||||
permissions they have available. This approach can be used to ensure that
|
||||
unauthenticated requests are presented with a different schema to
|
||||
authenticated requests, or to ensure that different parts of the API are
|
||||
made visible to different users depending on their role.
|
||||
|
||||
In order to present a schema with endpoints filtered by user permissions,
|
||||
you need to pass the `request` argument to the `get_schema()` method, like so:
|
||||
|
||||
@api_view()
|
||||
@renderer_classes([renderers.CoreJSONRenderer])
|
||||
def schema_view(request):
|
||||
return generator.get_schema(request=request)
|
||||
|
||||
## Explicit schema definition
|
||||
|
||||
An alternative to the auto-generated approach is to specify the API schema
|
||||
explicitly, by declaring a `Document` object in your codebase. Doing so is a
|
||||
little more work, but ensures that you have full control over the schema
|
||||
representation.
|
||||
|
||||
import coreapi
|
||||
from rest_framework.decorators import api_view, renderer_classes
|
||||
from rest_framework import renderers
|
||||
|
||||
schema = coreapi.Document(
|
||||
title='Bookings API',
|
||||
content={
|
||||
...
|
||||
}
|
||||
)
|
||||
|
||||
@api_view()
|
||||
@renderer_classes([renderers.CoreJSONRenderer])
|
||||
def schema_view(request):
|
||||
return schema
|
||||
|
||||
## Static schema file
|
||||
|
||||
A final option is to write your API schema as a static file, using one
|
||||
of the available formats, such as Core JSON or Open API.
|
||||
|
||||
You could then either:
|
||||
|
||||
* Write a schema definition as a static file, and [serve the static file directly][static-files].
|
||||
* Write a schema definition that is loaded using `Core API`, and then
|
||||
rendered to one of many available formats, depending on the client request.
|
||||
|
||||
---
|
||||
|
||||
# API Reference
|
||||
|
||||
## SchemaGenerator
|
||||
|
||||
A class that deals with introspecting your API views, which can be used to
|
||||
generate a schema.
|
||||
|
||||
Typically you'll instantiate `SchemaGenerator` with a single argument, like so:
|
||||
|
||||
generator = SchemaGenerator(title='Stock Prices API')
|
||||
|
||||
Arguments:
|
||||
|
||||
* `title` - The name of the API. **required**
|
||||
* `patterns` - A list of URLs to inspect when generating the schema. Defaults to the project's URL conf.
|
||||
* `urlconf` - A URL conf module name to use when generating the schema. Defaults to `settings.ROOT_URLCONF`.
|
||||
|
||||
### get_schema()
|
||||
|
||||
Returns a `coreapi.Document` instance that represents the API schema.
|
||||
|
||||
@api_view
|
||||
@renderer_classes([renderers.CoreJSONRenderer])
|
||||
def schema_view(request):
|
||||
return generator.get_schema()
|
||||
|
||||
Arguments:
|
||||
|
||||
* `request` - The incoming request. Optionally used if you want to apply per-user permissions to the schema-generation.
|
||||
|
||||
---
|
||||
|
||||
## Core API
|
||||
|
||||
This documentation gives a brief overview of the components within the `coreapi`
|
||||
package that are used to represent an API schema.
|
||||
|
||||
Note that these classes are imported from the `coreapi` package, rather than
|
||||
from the `rest_framework` package.
|
||||
|
||||
### Document
|
||||
|
||||
Represents a container for the API schema.
|
||||
|
||||
#### `title`
|
||||
|
||||
A name for the API.
|
||||
|
||||
#### `url`
|
||||
|
||||
A canonical URL for the API.
|
||||
|
||||
#### `content`
|
||||
|
||||
A dictionary, containing the `Link` objects that the schema contains.
|
||||
|
||||
In order to provide more structure to the schema, the `content` dictionary
|
||||
may be nested, typically to a second level. For example:
|
||||
|
||||
content={
|
||||
"bookings": {
|
||||
"list": Link(...),
|
||||
"create": Link(...),
|
||||
...
|
||||
},
|
||||
"venues": {
|
||||
"list": Link(...),
|
||||
...
|
||||
},
|
||||
...
|
||||
}
|
||||
|
||||
### Link
|
||||
|
||||
Represents an individual API endpoint.
|
||||
|
||||
#### `url`
|
||||
|
||||
The URL of the endpoint. May be a URI template, such as `/users/{username}/`.
|
||||
|
||||
#### `action`
|
||||
|
||||
The HTTP method associated with the endpoint. Note that URLs that support
|
||||
more than one HTTP method, should correspond to a single `Link` for each.
|
||||
|
||||
#### `fields`
|
||||
|
||||
A list of `Field` instances, describing the available parameters on the input.
|
||||
|
||||
#### `description`
|
||||
|
||||
A short description of the meaning and intended usage of the endpoint.
|
||||
|
||||
### Field
|
||||
|
||||
Represents a single input parameter on a given API endpoint.
|
||||
|
||||
#### `name`
|
||||
|
||||
A descriptive name for the input.
|
||||
|
||||
#### `required`
|
||||
|
||||
A boolean, indicated if the client is required to included a value, or if
|
||||
the parameter can be omitted.
|
||||
|
||||
#### `location`
|
||||
|
||||
Determines how the information is encoded into the request. Should be one of
|
||||
the following strings:
|
||||
|
||||
**"path"**
|
||||
|
||||
Included in a templated URI. For example a `url` value of `/products/{product_code}/` could be used together with a `"path"` field, to handle API inputs in a URL path such as `/products/slim-fit-jeans/`.
|
||||
|
||||
These fields will normally correspond with [named arguments in the project URL conf][named-arguments].
|
||||
|
||||
**"query"**
|
||||
|
||||
Included as a URL query parameter. For example `?search=sale`. Typically for `GET` requests.
|
||||
|
||||
These fields will normally correspond with pagination and filtering controls on a view.
|
||||
|
||||
**"form"**
|
||||
|
||||
Included in the request body, as a single item of a JSON object or HTML form. For example `{"colour": "blue", ...}`. Typically for `POST`, `PUT` and `PATCH` requests. Multiple `"form"` fields may be included on a single link.
|
||||
|
||||
These fields will normally correspond with serializer fields on a view.
|
||||
|
||||
**"body"**
|
||||
|
||||
Included as the complete request body. Typically for `POST`, `PUT` and `PATCH` requests. No more than one `"body"` field may exist on a link. May not be used together with `"form"` fields.
|
||||
|
||||
These fields will normally correspond with views that use `ListSerializer` to validate the request input, or with file upload views.
|
||||
|
||||
#### `encoding`
|
||||
|
||||
**"application/json"**
|
||||
|
||||
JSON encoded request content. Corresponds to views using `JSONParser`.
|
||||
Valid only if either one or more `location="form"` fields, or a single
|
||||
`location="body"` field is included on the `Link`.
|
||||
|
||||
**"multipart/form-data"**
|
||||
|
||||
Multipart encoded request content. Corresponds to views using `MultiPartParser`.
|
||||
Valid only if one or more `location="form"` fields is included on the `Link`.
|
||||
|
||||
**"application/x-www-form-urlencoded"**
|
||||
|
||||
URL encoded request content. Corresponds to views using `FormParser`. Valid
|
||||
only if one or more `location="form"` fields is included on the `Link`.
|
||||
|
||||
**"application/octet-stream"**
|
||||
|
||||
Binary upload request content. Corresponds to views using `FileUploadParser`.
|
||||
Valid only if a `location="body"` field is included on the `Link`.
|
||||
|
||||
#### `description`
|
||||
|
||||
A short description of the meaning and intended usage of the input field.
|
||||
|
||||
|
||||
[cite]: https://blog.heroku.com/archives/2014/1/8/json_schema_for_heroku_platform_api
|
||||
[coreapi]: http://www.coreapi.org/
|
||||
[corejson]: http://www.coreapi.org/specification/encoding/#core-json-encoding
|
||||
[open-api]: https://openapis.org/
|
||||
[json-hyperschema]: http://json-schema.org/latest/json-schema-hypermedia.html
|
||||
[api-blueprint]: https://apiblueprint.org/
|
||||
[static-files]: https://docs.djangoproject.com/en/dev/howto/static-files/
|
||||
[named-arguments]: https://docs.djangoproject.com/en/dev/topics/http/urls/#named-groups
|
BIN
docs/img/corejson-format.png
Normal file
BIN
docs/img/corejson-format.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
|
@ -91,6 +91,7 @@ REST framework requires the following:
|
|||
|
||||
The following packages are optional:
|
||||
|
||||
* [coreapi][coreapi] (1.21.0+) - Schema generation support.
|
||||
* [Markdown][markdown] (2.1.0+) - Markdown support for the browsable API.
|
||||
* [django-filter][django-filter] (0.9.2+) - Filtering support.
|
||||
* [django-crispy-forms][django-crispy-forms] - Improved HTML display for filtering.
|
||||
|
@ -214,6 +215,7 @@ The API guide is your complete reference manual to all the functionality provide
|
|||
* [Versioning][versioning]
|
||||
* [Content negotiation][contentnegotiation]
|
||||
* [Metadata][metadata]
|
||||
* [Schemas][schemas]
|
||||
* [Format suffixes][formatsuffixes]
|
||||
* [Returning URLs][reverse]
|
||||
* [Exceptions][exceptions]
|
||||
|
@ -226,6 +228,7 @@ The API guide is your complete reference manual to all the functionality provide
|
|||
General guides to using REST framework.
|
||||
|
||||
* [Documenting your API][documenting-your-api]
|
||||
* [API Clients][api-clients]
|
||||
* [Internationalization][internationalization]
|
||||
* [AJAX, CSRF & CORS][ajax-csrf-cors]
|
||||
* [HTML & Forms][html-and-forms]
|
||||
|
@ -296,6 +299,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
[redhat]: https://www.redhat.com/
|
||||
[heroku]: https://www.heroku.com/
|
||||
[eventbrite]: https://www.eventbrite.co.uk/about/
|
||||
[coreapi]: http://pypi.python.org/pypi/coreapi/
|
||||
[markdown]: http://pypi.python.org/pypi/Markdown/
|
||||
[django-filter]: http://pypi.python.org/pypi/django-filter
|
||||
[django-crispy-forms]: https://github.com/maraujop/django-crispy-forms
|
||||
|
@ -318,6 +322,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
[tut-4]: tutorial/4-authentication-and-permissions.md
|
||||
[tut-5]: tutorial/5-relationships-and-hyperlinked-apis.md
|
||||
[tut-6]: tutorial/6-viewsets-and-routers.md
|
||||
[tut-7]: tutorial/7-schemas-and-client-libraries.md
|
||||
|
||||
[request]: api-guide/requests.md
|
||||
[response]: api-guide/responses.md
|
||||
|
@ -339,6 +344,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
[versioning]: api-guide/versioning.md
|
||||
[contentnegotiation]: api-guide/content-negotiation.md
|
||||
[metadata]: api-guide/metadata.md
|
||||
[schemas]: 'api-guide/schemas.md'
|
||||
[formatsuffixes]: api-guide/format-suffixes.md
|
||||
[reverse]: api-guide/reverse.md
|
||||
[exceptions]: api-guide/exceptions.md
|
||||
|
@ -347,6 +353,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
[settings]: api-guide/settings.md
|
||||
|
||||
[documenting-your-api]: topics/documenting-your-api.md
|
||||
[api-clients]: topics/api-clients.md
|
||||
[internationalization]: topics/internationalization.md
|
||||
[ajax-csrf-cors]: topics/ajax-csrf-cors.md
|
||||
[html-and-forms]: topics/html-and-forms.md
|
||||
|
|
294
docs/topics/api-clients.md
Normal file
294
docs/topics/api-clients.md
Normal file
|
@ -0,0 +1,294 @@
|
|||
# API Clients
|
||||
|
||||
An API client handles the underlying details of how network requests are made
|
||||
and how responses are decoded. They present the developer with an application
|
||||
interface to work against, rather than working directly with the network interface.
|
||||
|
||||
The API clients documented here are not restricted to APIs built with Django REST framework.
|
||||
They can be used with any API that exposes a supported schema format.
|
||||
|
||||
For example, [the Heroku platform API][heroku-api] exposes a schema in the JSON
|
||||
Hyperschema format. As a result, the Core API command line client and Python
|
||||
client library can be [used to interact with the Heroku API][heroku-example].
|
||||
|
||||
## Client-side Core API
|
||||
|
||||
[Core API][core-api] is a document specification that can be used to describe APIs. It can
|
||||
be used either server-side, as is done with REST framework's [schema generation][schema-generation],
|
||||
or used client-side, as described here.
|
||||
|
||||
When used client-side, Core API allows for *dynamically driven client libraries*
|
||||
that can interact with any API that exposes a supported schema or hypermedia
|
||||
format.
|
||||
|
||||
Using a dynamically driven client has a number of advantages over interacting
|
||||
with an API by building HTTP requests directly.
|
||||
|
||||
#### More meaningful interaction
|
||||
|
||||
API interactions are presented in a more meaningful way. You're working at
|
||||
the application interface layer, rather than the network interface layer.
|
||||
|
||||
#### Resilience & evolvability
|
||||
|
||||
The client determines what endpoints are available, what parameters exist
|
||||
against each particular endpoint, and how HTTP requests are formed.
|
||||
|
||||
This also allows for a degree of API evolvability. URLs can be modified
|
||||
without breaking existing clients, or more efficient encodings can be used
|
||||
on-the-wire, with clients transparently upgrading.
|
||||
|
||||
#### Self-descriptive APIs
|
||||
|
||||
A dynamically driven client is able to present documentation on the API to the
|
||||
end user. This documentation allows the user to discover the available endpoints
|
||||
and parameters, and better understand the API they are working with.
|
||||
|
||||
Because this documentation is driven by the API schema it will always be fully
|
||||
up to date with the most recently deployed version of the service.
|
||||
|
||||
---
|
||||
|
||||
# Command line client
|
||||
|
||||
The command line client allows you to inspect and interact with any API that
|
||||
exposes a supported schema format.
|
||||
|
||||
## Getting started
|
||||
|
||||
To install the Core API command line client, use `pip`.
|
||||
|
||||
$ pip install coreapi
|
||||
|
||||
To start inspecting and interacting with an API the schema must first be loaded
|
||||
from the network.
|
||||
|
||||
|
||||
$ coreapi get http://api.example.org/
|
||||
<Pastebin API "http://127.0.0.1:8000/">
|
||||
snippets: {
|
||||
create(code, [title], [linenos], [language], [style])
|
||||
destroy(pk)
|
||||
highlight(pk)
|
||||
list([page])
|
||||
partial_update(pk, [title], [code], [linenos], [language], [style])
|
||||
retrieve(pk)
|
||||
update(pk, code, [title], [linenos], [language], [style])
|
||||
}
|
||||
users: {
|
||||
list([page])
|
||||
retrieve(pk)
|
||||
}
|
||||
|
||||
This will then load the schema, displaying the resulting `Document`. This
|
||||
`Document` includes all the available interactions that may be made against the API.
|
||||
|
||||
To interact with the API, use the `action` command. This command requires a list
|
||||
of keys that are used to index into the link.
|
||||
|
||||
$ coreapi action users list
|
||||
[
|
||||
{
|
||||
"url": "http://127.0.0.1:8000/users/2/",
|
||||
"id": 2,
|
||||
"username": "aziz",
|
||||
"snippets": []
|
||||
},
|
||||
...
|
||||
]
|
||||
|
||||
To inspect the underlying HTTP request and response, use the `--debug` flag.
|
||||
|
||||
$ coreapi action users list --debug
|
||||
> GET /users/ HTTP/1.1
|
||||
> Accept: application/vnd.coreapi+json, */*
|
||||
> Authorization: Basic bWF4Om1heA==
|
||||
> Host: 127.0.0.1
|
||||
> User-Agent: coreapi
|
||||
< 200 OK
|
||||
< Allow: GET, HEAD, OPTIONS
|
||||
< Content-Type: application/json
|
||||
< Date: Thu, 30 Jun 2016 10:51:46 GMT
|
||||
< Server: WSGIServer/0.1 Python/2.7.10
|
||||
< Vary: Accept, Cookie
|
||||
<
|
||||
< [{"url":"http://127.0.0.1/users/2/","id":2,"username":"aziz","snippets":[]},{"url":"http://127.0.0.1/users/3/","id":3,"username":"amy","snippets":["http://127.0.0.1/snippets/3/"]},{"url":"http://127.0.0.1/users/4/","id":4,"username":"max","snippets":["http://127.0.0.1/snippets/4/","http://127.0.0.1/snippets/5/","http://127.0.0.1/snippets/6/","http://127.0.0.1/snippets/7/"]},{"url":"http://127.0.0.1/users/5/","id":5,"username":"jose","snippets":[]},{"url":"http://127.0.0.1/users/6/","id":6,"username":"admin","snippets":["http://127.0.0.1/snippets/1/","http://127.0.0.1/snippets/2/"]}]
|
||||
|
||||
[
|
||||
...
|
||||
]
|
||||
|
||||
Some actions may include optional or required parameters.
|
||||
|
||||
$ coreapi action users create --params username example
|
||||
|
||||
## Authentication & headers
|
||||
|
||||
The `credentials` command is used to manage the request `Authentication:` header.
|
||||
Any credentials added are always linked to a particular domain, so as to ensure
|
||||
that credentials are not leaked across differing APIs.
|
||||
|
||||
The format for adding a new credential is:
|
||||
|
||||
coreapi credentials add <domain> <credentials string>
|
||||
|
||||
For instance:
|
||||
|
||||
coreapi credentials add api.example.org "Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b"
|
||||
|
||||
The optional `--auth` flag also allows you to add specific types of authentication,
|
||||
handling the encoding for you. Currently only `"basic"` is supported as an option here.
|
||||
For example:
|
||||
|
||||
coreapi credentials add api.example.org tomchristie:foobar --auth basic
|
||||
|
||||
You can also add specific request headers, using the `headers` command:
|
||||
|
||||
coreapi headers add api.example.org x-api-version 2
|
||||
|
||||
For more information and a listing of the available subcommands use `coreapi
|
||||
credentials --help` or `coreapi headers --help`.
|
||||
|
||||
## Utilities
|
||||
|
||||
The command line client includes functionality for bookmarking API URLs
|
||||
under a memorable name. For example, you can add a bookmark for the
|
||||
existing API, like so...
|
||||
|
||||
coreapi bookmarks add accountmanagement
|
||||
|
||||
There is also functionality for navigating forward or backward through the
|
||||
history of which API URLs have been accessed.
|
||||
|
||||
coreapi history show
|
||||
coreapi history back
|
||||
|
||||
For more information and a listing of the available subcommands use
|
||||
`coreapi bookmarks --help` or `coreapi history --help`.
|
||||
|
||||
## Other commands
|
||||
|
||||
To display the current `Document`:
|
||||
|
||||
coreapi show
|
||||
|
||||
To reload the current `Document` from the network:
|
||||
|
||||
coreapi reload
|
||||
|
||||
To load a schema file from disk:
|
||||
|
||||
coreapi load my-api-schema.json --format corejson
|
||||
|
||||
To remove the current document, along with all currently saved history,
|
||||
credentials, headers and bookmarks:
|
||||
|
||||
coreapi clear
|
||||
|
||||
---
|
||||
|
||||
# Python client library
|
||||
|
||||
The `coreapi` Python package allows you to programatically interact with any
|
||||
API that exposes a supported schema format.
|
||||
|
||||
## Getting started
|
||||
|
||||
You'll need to install the `coreapi` package using `pip` before you can get
|
||||
started. Once you've done so, open up a python terminal.
|
||||
|
||||
In order to start working with an API, we first need a `Client` instance. The
|
||||
client holds any configuration around which codecs and transports are supported
|
||||
when interacting with an API, which allows you to provide for more advanced
|
||||
kinds of behaviour.
|
||||
|
||||
import coreapi
|
||||
client = coreapi.Client()
|
||||
|
||||
Once we have a `Client` instance, we can fetch an API schema from the network.
|
||||
|
||||
schema = client.get('https://api.example.org/')
|
||||
|
||||
The object returned from this call will be a `Document` instance, which is
|
||||
the internal representation of the interface that we are interacting with.
|
||||
|
||||
Now that we have our schema `Document`, we can now start to interact with the API:
|
||||
|
||||
users = client.action(schema, ['users', 'list'])
|
||||
|
||||
Some endpoints may include named parameters, which might be either optional or required:
|
||||
|
||||
new_user = client.action(schema, ['users', 'create'], params={"username": "max"})
|
||||
|
||||
## Codecs
|
||||
|
||||
Codecs are responsible for encoding or decoding Documents.
|
||||
|
||||
The decoding process is used by a client to take a bytestring of an API schema
|
||||
definition, and returning the Core API `Document` that represents that interface.
|
||||
|
||||
A codec should be associated with a particular media type, such as **TODO**.
|
||||
|
||||
This media type is used by the server in the response `Content-Type` header,
|
||||
in order to indicate what kind of data is being returned in the response.
|
||||
|
||||
#### Configuring codecs
|
||||
|
||||
The codecs that are available can be configured when instantiating a client.
|
||||
The keyword argument used here is `decoders`, because in the context of a
|
||||
client the codecs are only for *decoding* responses.
|
||||
|
||||
In the following example we'll configure a client to only accept `Core JSON`
|
||||
and `JSON` responses. This will allow us to receive and decode a Core JSON schema,
|
||||
and subsequently to receive JSON responses made against the API.
|
||||
|
||||
from coreapi import codecs, Client
|
||||
|
||||
decoders = [codecs.CoreJSONCodec(), codecs.JSONCodec()]
|
||||
client = Client(decoders=decoders)
|
||||
|
||||
#### Loading and saving schemas
|
||||
|
||||
You can use a codec directly, in order to load an existing schema definition,
|
||||
and return the resulting `Document`.
|
||||
|
||||
schema_definition = open('my-api-schema.json', 'r').read()
|
||||
codec = codecs.CoreJSONCodec()
|
||||
schema = codec.load(schema_definition)
|
||||
|
||||
You can also use a codec directly to generate a schema definition given a `Document` instance:
|
||||
|
||||
schema_definition = codec.dump(schema)
|
||||
output_file = open('my-api-schema.json', 'r')
|
||||
output_file.write(schema_definition)
|
||||
|
||||
## Transports
|
||||
|
||||
Transports are responsible for making network requests. The set of transports
|
||||
that a client has installed determines which network protocols it is able to
|
||||
support.
|
||||
|
||||
Currently the `coreapi` library only includes an HTTP/HTTPS transport, but
|
||||
other protocols can also be supported.
|
||||
|
||||
#### Configuring transports
|
||||
|
||||
The behaviour of the network layer can be customized by configuring the
|
||||
transports that the client is instantiated with.
|
||||
|
||||
import requests
|
||||
from coreapi import transports, Client
|
||||
|
||||
credentials = {'api.example.org': 'Token 3bd44a009d16ff'}
|
||||
transports = transports.HTTPTransport(credentials=credentials)
|
||||
client = Client(transports=transports)
|
||||
|
||||
More complex customizations can also be achieved, for example modifying the
|
||||
underlying `requests.Session` instance to [attach transport adaptors][transport-adaptors]
|
||||
that modify the outgoing requests.
|
||||
|
||||
[heroku-api]: https://devcenter.heroku.com/categories/platform-api
|
||||
[heroku-example]: http://www.coreapi.org/tools-and-resources/example-services/#heroku-json-hyper-schema
|
||||
[core-api]: http://www.coreapi.org/
|
||||
[schema-generation]: ../api-guide/schemas.md
|
||||
[transport-adaptors]: http://docs.python-requests.org/en/master/user/advanced/#transport-adapters
|
|
@ -130,27 +130,7 @@ Using viewsets can be a really useful abstraction. It helps ensure that URL con
|
|||
|
||||
That doesn't mean it's always the right approach to take. There's a similar set of trade-offs to consider as when using class-based views instead of function based views. Using viewsets is less explicit than building your views individually.
|
||||
|
||||
## Reviewing our work
|
||||
In [part 7][tut-7] of the tutorial we'll look at how we can add an API schema,
|
||||
and interact with our API using a client library or command line tool.
|
||||
|
||||
With an incredibly small amount of code, we've now got a complete pastebin Web API, which is fully web browsable, and comes complete with authentication, per-object permissions, and multiple renderer formats.
|
||||
|
||||
We've walked through each step of the design process, and seen how if we need to customize anything we can gradually work our way down to simply using regular Django views.
|
||||
|
||||
You can review the final [tutorial code][repo] on GitHub, or try out a live example in [the sandbox][sandbox].
|
||||
|
||||
## Onwards and upwards
|
||||
|
||||
We've reached the end of our tutorial. If you want to get more involved in the REST framework project, here are a few places you can start:
|
||||
|
||||
* Contribute on [GitHub][github] by reviewing and submitting issues, and making pull requests.
|
||||
* Join the [REST framework discussion group][group], and help build the community.
|
||||
* Follow [the author][twitter] on Twitter and say hi.
|
||||
|
||||
**Now go build awesome things.**
|
||||
|
||||
|
||||
[repo]: https://github.com/tomchristie/rest-framework-tutorial
|
||||
[sandbox]: http://restframework.herokuapp.com/
|
||||
[github]: https://github.com/tomchristie/django-rest-framework
|
||||
[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
|
||||
[twitter]: https://twitter.com/_tomchristie
|
||||
[tut-7]: 7-schemas-and-client-libraries.md
|
||||
|
|
216
docs/tutorial/7-schemas-and-client-libraries.md
Normal file
216
docs/tutorial/7-schemas-and-client-libraries.md
Normal file
|
@ -0,0 +1,216 @@
|
|||
# Tutorial 7: Schemas & client libraries
|
||||
|
||||
A schema is a machine-readable document that describes the available API
|
||||
endpoints, their URLS, and what operations they support.
|
||||
|
||||
Schemas can be a useful tool for auto-generated documentation, and can also
|
||||
be used to drive dynamic client libraries that can interact with the API.
|
||||
|
||||
## Core API
|
||||
|
||||
In order to provide schema support REST framework uses [Core API][coreapi].
|
||||
|
||||
Core API is a document specification for describing APIs. It is used to provide
|
||||
an internal representation format of the available endpoints and possible
|
||||
interactions that an API exposes. It can either be used server-side, or
|
||||
client-side.
|
||||
|
||||
When used server-side, Core API allows an API to support rendering to a wide
|
||||
range of schema or hypermedia formats.
|
||||
|
||||
When used client-side, Core API allows for dynamically driven client libraries
|
||||
that can interact with any API that exposes a supported schema or hypermedia
|
||||
format.
|
||||
|
||||
## Adding a schema
|
||||
|
||||
REST framework supports either explicitly defined schema views, or
|
||||
automatically generated schemas. Since we're using viewsets and routers,
|
||||
we can simply use the automatic schema generation.
|
||||
|
||||
You'll need to install the `coreapi` python package in order to include an
|
||||
API schema.
|
||||
|
||||
$ pip install coreapi
|
||||
|
||||
We can now include a schema for our API, by adding a `schema_title` argument to
|
||||
the router instantiation.
|
||||
|
||||
router = DefaultRouter(schema_title='Pastebin API')
|
||||
|
||||
If you visit the API root endpoint in a browser you should now see `corejson`
|
||||
representation become available as an option.
|
||||
|
||||
![Schema format](../img/corejson-format.png)
|
||||
|
||||
We can also request the schema from the command line, by specifying the desired
|
||||
content type in the `Accept` header.
|
||||
|
||||
$ http http://127.0.0.1:8000/ Accept:application/vnd.coreapi+json
|
||||
HTTP/1.0 200 OK
|
||||
Allow: GET, HEAD, OPTIONS
|
||||
Content-Type: application/vnd.coreapi+json
|
||||
|
||||
{
|
||||
"_meta": {
|
||||
"title": "Pastebin API"
|
||||
},
|
||||
"_type": "document",
|
||||
...
|
||||
|
||||
The default output style is to use the [Core JSON][corejson] encoding.
|
||||
|
||||
Other schema formats, such as [Open API][openapi] (formerly Swagger) are
|
||||
also supported.
|
||||
|
||||
## Using a command line client
|
||||
|
||||
Now that our API is exposing a schema endpoint, we can use a dynamic client
|
||||
library to interact with the API. To demonstrate this, let's use the
|
||||
Core API command line client. We've already installed the `coreapi` package
|
||||
using `pip`, so the client tool should already be installed. Check that it
|
||||
is available on the command line...
|
||||
|
||||
$ coreapi
|
||||
Usage: coreapi [OPTIONS] COMMAND [ARGS]...
|
||||
|
||||
Command line client for interacting with CoreAPI services.
|
||||
|
||||
Visit http://www.coreapi.org for more information.
|
||||
|
||||
Options:
|
||||
--version Display the package version number.
|
||||
--help Show this message and exit.
|
||||
|
||||
Commands:
|
||||
...
|
||||
|
||||
First we'll load the API schema using the command line client.
|
||||
|
||||
$ coreapi get http://127.0.0.1:8000/
|
||||
<Pastebin API "http://127.0.0.1:8000/">
|
||||
snippets: {
|
||||
highlight(pk)
|
||||
list()
|
||||
retrieve(pk)
|
||||
}
|
||||
users: {
|
||||
list()
|
||||
retrieve(pk)
|
||||
}
|
||||
|
||||
We haven't authenticated yet, so right now we're only able to see the read only
|
||||
endpoints, in line with how we've set up the permissions on the API.
|
||||
|
||||
Let's try listing the existing snippets, using the command line client:
|
||||
|
||||
$ coreapi action snippets list
|
||||
[
|
||||
{
|
||||
"url": "http://127.0.0.1:8000/snippets/1/",
|
||||
"highlight": "http://127.0.0.1:8000/snippets/1/highlight/",
|
||||
"owner": "lucy",
|
||||
"title": "Example",
|
||||
"code": "print('hello, world!')",
|
||||
"linenos": true,
|
||||
"language": "python",
|
||||
"style": "friendly"
|
||||
},
|
||||
...
|
||||
|
||||
Some of the API endpoints require named parameters. For example, to get back
|
||||
the highlight HTML for a particular snippet we need to provide an id.
|
||||
|
||||
$ coreapi action snippets highlight --param pk 1
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>Example</title>
|
||||
...
|
||||
|
||||
## Authenticating our client
|
||||
|
||||
If we want to be able to create, edit and delete snippets, we'll need to
|
||||
authenticate as a valid user. In this case we'll just use basic auth.
|
||||
|
||||
Make sure to replace the `<username>` and `<password>` below with your
|
||||
actual username and password.
|
||||
|
||||
$ coreapi credentials add 127.0.0.1 <username>:<password> --auth basic
|
||||
Added credentials
|
||||
127.0.0.1 "Basic <...>"
|
||||
|
||||
Now if we fetch the schema again, we should be able to see the full
|
||||
set of available interactions.
|
||||
|
||||
$ coreapi reload
|
||||
Pastebin API "http://127.0.0.1:8000/">
|
||||
snippets: {
|
||||
create(code, [title], [linenos], [language], [style])
|
||||
destroy(pk)
|
||||
highlight(pk)
|
||||
list()
|
||||
partial_update(pk, [title], [code], [linenos], [language], [style])
|
||||
retrieve(pk)
|
||||
update(pk, code, [title], [linenos], [language], [style])
|
||||
}
|
||||
users: {
|
||||
list()
|
||||
retrieve(pk)
|
||||
}
|
||||
|
||||
We're now able to interact with these endpoints. For example, to create a new
|
||||
snippet:
|
||||
|
||||
$ coreapi action snippets create --param title "Example" --param code "print('hello, world')"
|
||||
{
|
||||
"url": "http://127.0.0.1:8000/snippets/7/",
|
||||
"id": 7,
|
||||
"highlight": "http://127.0.0.1:8000/snippets/7/highlight/",
|
||||
"owner": "lucy",
|
||||
"title": "Example",
|
||||
"code": "print('hello, world')",
|
||||
"linenos": false,
|
||||
"language": "python",
|
||||
"style": "friendly"
|
||||
}
|
||||
|
||||
And to delete a snippet:
|
||||
|
||||
$ coreapi action snippets destroy --param pk 7
|
||||
|
||||
As well as the command line client, developers can also interact with your
|
||||
API using client libraries. The Python client library is the first of these
|
||||
to be available, and a Javascript client library is planned to be released
|
||||
soon.
|
||||
|
||||
For more details on customizing schema generation and using Core API
|
||||
client libraries you'll need to refer to the full documentation.
|
||||
|
||||
## Reviewing our work
|
||||
|
||||
With an incredibly small amount of code, we've now got a complete pastebin Web API, which is fully web browsable, includes a schema-driven client library, and comes complete with authentication, per-object permissions, and multiple renderer formats.
|
||||
|
||||
We've walked through each step of the design process, and seen how if we need to customize anything we can gradually work our way down to simply using regular Django views.
|
||||
|
||||
You can review the final [tutorial code][repo] on GitHub, or try out a live example in [the sandbox][sandbox].
|
||||
|
||||
## Onwards and upwards
|
||||
|
||||
We've reached the end of our tutorial. If you want to get more involved in the REST framework project, here are a few places you can start:
|
||||
|
||||
* Contribute on [GitHub][github] by reviewing and submitting issues, and making pull requests.
|
||||
* Join the [REST framework discussion group][group], and help build the community.
|
||||
* Follow [the author][twitter] on Twitter and say hi.
|
||||
|
||||
**Now go build awesome things.**
|
||||
|
||||
[coreapi]: http://www.coreapi.org
|
||||
[corejson]: http://www.coreapi.org/specification/encoding/#core-json-encoding
|
||||
[openapi]: https://openapis.org/
|
||||
[repo]: https://github.com/tomchristie/rest-framework-tutorial
|
||||
[sandbox]: http://restframework.herokuapp.com/
|
||||
[github]: https://github.com/tomchristie/django-rest-framework
|
||||
[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
|
||||
[twitter]: https://twitter.com/_tomchristie
|
|
@ -20,6 +20,7 @@ pages:
|
|||
- '4 - Authentication and permissions': 'tutorial/4-authentication-and-permissions.md'
|
||||
- '5 - Relationships and hyperlinked APIs': 'tutorial/5-relationships-and-hyperlinked-apis.md'
|
||||
- '6 - Viewsets and routers': 'tutorial/6-viewsets-and-routers.md'
|
||||
- '7 - Schemas and client libraries': 'tutorial/7-schemas-and-client-libraries.md'
|
||||
- API Guide:
|
||||
- 'Requests': 'api-guide/requests.md'
|
||||
- 'Responses': 'api-guide/responses.md'
|
||||
|
@ -41,6 +42,7 @@ pages:
|
|||
- 'Versioning': 'api-guide/versioning.md'
|
||||
- 'Content negotiation': 'api-guide/content-negotiation.md'
|
||||
- 'Metadata': 'api-guide/metadata.md'
|
||||
- 'Schemas': 'api-guide/schemas.md'
|
||||
- 'Format suffixes': 'api-guide/format-suffixes.md'
|
||||
- 'Returning URLs': 'api-guide/reverse.md'
|
||||
- 'Exceptions': 'api-guide/exceptions.md'
|
||||
|
@ -49,6 +51,7 @@ pages:
|
|||
- 'Settings': 'api-guide/settings.md'
|
||||
- Topics:
|
||||
- 'Documenting your API': 'topics/documenting-your-api.md'
|
||||
- 'API Clients': 'topics/api-clients.md'
|
||||
- 'Internationalization': 'topics/internationalization.md'
|
||||
- 'AJAX, CSRF & CORS': 'topics/ajax-csrf-cors.md'
|
||||
- 'HTML & Forms': 'topics/html-and-forms.md'
|
||||
|
|
|
@ -2,3 +2,4 @@
|
|||
markdown==2.6.4
|
||||
django-guardian==1.4.3
|
||||
django-filter==0.13.0
|
||||
coreapi==1.21.1
|
||||
|
|
|
@ -156,6 +156,16 @@ except ImportError:
|
|||
crispy_forms = None
|
||||
|
||||
|
||||
# coreapi is optional (Note that uritemplate is a dependancy of coreapi)
|
||||
try:
|
||||
import coreapi
|
||||
import uritemplate
|
||||
except (ImportError, SyntaxError):
|
||||
# SyntaxError is possible under python 3.2
|
||||
coreapi = None
|
||||
uritemplate = None
|
||||
|
||||
|
||||
# Django-guardian is optional. Import only if guardian is in INSTALLED_APPS
|
||||
# Fixes (#1712). We keep the try/except for the test suite.
|
||||
guardian = None
|
||||
|
|
|
@ -72,6 +72,9 @@ class BaseFilterBackend(object):
|
|||
"""
|
||||
raise NotImplementedError(".filter_queryset() must be overridden.")
|
||||
|
||||
def get_fields(self, view):
|
||||
return []
|
||||
|
||||
|
||||
class DjangoFilterBackend(BaseFilterBackend):
|
||||
"""
|
||||
|
@ -128,6 +131,17 @@ class DjangoFilterBackend(BaseFilterBackend):
|
|||
template = loader.get_template(self.template)
|
||||
return template_render(template, context)
|
||||
|
||||
def get_fields(self, view):
|
||||
filter_class = getattr(view, 'filter_class', None)
|
||||
if filter_class:
|
||||
return list(filter_class().filters.keys())
|
||||
|
||||
filter_fields = getattr(view, 'filter_fields', None)
|
||||
if filter_fields:
|
||||
return filter_fields
|
||||
|
||||
return []
|
||||
|
||||
|
||||
class SearchFilter(BaseFilterBackend):
|
||||
# The URL query parameter used for the search.
|
||||
|
@ -217,6 +231,9 @@ class SearchFilter(BaseFilterBackend):
|
|||
template = loader.get_template(self.template)
|
||||
return template_render(template, context)
|
||||
|
||||
def get_fields(self, view):
|
||||
return [self.search_param]
|
||||
|
||||
|
||||
class OrderingFilter(BaseFilterBackend):
|
||||
# The URL query parameter used for the ordering.
|
||||
|
@ -330,6 +347,9 @@ class OrderingFilter(BaseFilterBackend):
|
|||
context = self.get_template_context(request, queryset, view)
|
||||
return template_render(template, context)
|
||||
|
||||
def get_fields(self, view):
|
||||
return [self.ordering_param]
|
||||
|
||||
|
||||
class DjangoObjectPermissionsFilter(BaseFilterBackend):
|
||||
"""
|
||||
|
|
|
@ -157,6 +157,9 @@ class BasePagination(object):
|
|||
def get_results(self, data):
|
||||
return data['results']
|
||||
|
||||
def get_fields(self, view):
|
||||
return []
|
||||
|
||||
|
||||
class PageNumberPagination(BasePagination):
|
||||
"""
|
||||
|
@ -280,6 +283,11 @@ class PageNumberPagination(BasePagination):
|
|||
context = self.get_html_context()
|
||||
return template_render(template, context)
|
||||
|
||||
def get_fields(self, view):
|
||||
if self.page_size_query_param is None:
|
||||
return [self.page_query_param]
|
||||
return [self.page_query_param, self.page_size_query_param]
|
||||
|
||||
|
||||
class LimitOffsetPagination(BasePagination):
|
||||
"""
|
||||
|
@ -404,6 +412,9 @@ class LimitOffsetPagination(BasePagination):
|
|||
context = self.get_html_context()
|
||||
return template_render(template, context)
|
||||
|
||||
def get_fields(self, view):
|
||||
return [self.limit_query_param, self.offset_query_param]
|
||||
|
||||
|
||||
class CursorPagination(BasePagination):
|
||||
"""
|
||||
|
@ -706,3 +717,6 @@ class CursorPagination(BasePagination):
|
|||
template = loader.get_template(self.template)
|
||||
context = self.get_html_context()
|
||||
return template_render(template, context)
|
||||
|
||||
def get_fields(self, view):
|
||||
return [self.cursor_query_param]
|
||||
|
|
|
@ -22,7 +22,8 @@ from django.utils import six
|
|||
|
||||
from rest_framework import VERSION, exceptions, serializers, status
|
||||
from rest_framework.compat import (
|
||||
INDENT_SEPARATORS, LONG_SEPARATORS, SHORT_SEPARATORS, template_render
|
||||
INDENT_SEPARATORS, LONG_SEPARATORS, SHORT_SEPARATORS, coreapi,
|
||||
template_render
|
||||
)
|
||||
from rest_framework.exceptions import ParseError
|
||||
from rest_framework.request import is_form_media_type, override_method
|
||||
|
@ -790,3 +791,17 @@ class MultiPartRenderer(BaseRenderer):
|
|||
"test case." % key
|
||||
)
|
||||
return encode_multipart(self.BOUNDARY, data)
|
||||
|
||||
|
||||
class CoreJSONRenderer(BaseRenderer):
|
||||
media_type = 'application/vnd.coreapi+json'
|
||||
charset = None
|
||||
format = 'corejson'
|
||||
|
||||
def __init__(self):
|
||||
assert coreapi, 'Using CoreJSONRenderer, but `coreapi` is not installed.'
|
||||
|
||||
def render(self, data, media_type=None, renderer_context=None):
|
||||
indent = bool(renderer_context.get('indent', 0))
|
||||
codec = coreapi.codecs.CoreJSONCodec()
|
||||
return codec.dump(data, indent=indent)
|
||||
|
|
|
@ -22,9 +22,11 @@ from django.conf.urls import url
|
|||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.core.urlresolvers import NoReverseMatch
|
||||
|
||||
from rest_framework import views
|
||||
from rest_framework import exceptions, renderers, views
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.reverse import reverse
|
||||
from rest_framework.schemas import SchemaGenerator
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.urlpatterns import format_suffix_patterns
|
||||
|
||||
Route = namedtuple('Route', ['url', 'mapping', 'name', 'initkwargs'])
|
||||
|
@ -255,6 +257,7 @@ class SimpleRouter(BaseRouter):
|
|||
lookup=lookup,
|
||||
trailing_slash=self.trailing_slash
|
||||
)
|
||||
|
||||
view = viewset.as_view(mapping, **route.initkwargs)
|
||||
name = route.name.format(basename=basename)
|
||||
ret.append(url(regex, view, name=name))
|
||||
|
@ -270,8 +273,13 @@ class DefaultRouter(SimpleRouter):
|
|||
include_root_view = True
|
||||
include_format_suffixes = True
|
||||
root_view_name = 'api-root'
|
||||
schema_renderers = [renderers.CoreJSONRenderer]
|
||||
|
||||
def get_api_root_view(self):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.schema_title = kwargs.pop('schema_title', None)
|
||||
super(DefaultRouter, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_api_root_view(self, schema_urls=None):
|
||||
"""
|
||||
Return a view to use as the API root.
|
||||
"""
|
||||
|
@ -280,10 +288,33 @@ class DefaultRouter(SimpleRouter):
|
|||
for prefix, viewset, basename in self.registry:
|
||||
api_root_dict[prefix] = list_name.format(basename=basename)
|
||||
|
||||
view_renderers = list(api_settings.DEFAULT_RENDERER_CLASSES)
|
||||
schema_media_types = []
|
||||
|
||||
if schema_urls and self.schema_title:
|
||||
view_renderers += list(self.schema_renderers)
|
||||
schema_generator = SchemaGenerator(
|
||||
title=self.schema_title,
|
||||
patterns=schema_urls
|
||||
)
|
||||
schema_media_types = [
|
||||
renderer.media_type
|
||||
for renderer in self.schema_renderers
|
||||
]
|
||||
|
||||
class APIRoot(views.APIView):
|
||||
_ignore_model_permissions = True
|
||||
renderer_classes = view_renderers
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if request.accepted_renderer.media_type in schema_media_types:
|
||||
# Return a schema response.
|
||||
schema = schema_generator.get_schema(request)
|
||||
if schema is None:
|
||||
raise exceptions.PermissionDenied()
|
||||
return Response(schema)
|
||||
|
||||
# Return a plain {"name": "hyperlink"} response.
|
||||
ret = OrderedDict()
|
||||
namespace = request.resolver_match.namespace
|
||||
for key, url_name in api_root_dict.items():
|
||||
|
@ -310,15 +341,13 @@ class DefaultRouter(SimpleRouter):
|
|||
Generate the list of URL patterns, including a default root view
|
||||
for the API, and appending `.json` style format suffixes.
|
||||
"""
|
||||
urls = []
|
||||
urls = super(DefaultRouter, self).get_urls()
|
||||
|
||||
if self.include_root_view:
|
||||
root_url = url(r'^$', self.get_api_root_view(), name=self.root_view_name)
|
||||
view = self.get_api_root_view(schema_urls=urls)
|
||||
root_url = url(r'^$', view, name=self.root_view_name)
|
||||
urls.append(root_url)
|
||||
|
||||
default_urls = super(DefaultRouter, self).get_urls()
|
||||
urls.extend(default_urls)
|
||||
|
||||
if self.include_format_suffixes:
|
||||
urls = format_suffix_patterns(urls)
|
||||
|
||||
|
|
300
rest_framework/schemas.py
Normal file
300
rest_framework/schemas.py
Normal file
|
@ -0,0 +1,300 @@
|
|||
from importlib import import_module
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.admindocs.views import simplify_regex
|
||||
from django.core.urlresolvers import RegexURLPattern, RegexURLResolver
|
||||
from django.utils import six
|
||||
|
||||
from rest_framework import exceptions, serializers
|
||||
from rest_framework.compat import coreapi, uritemplate
|
||||
from rest_framework.request import clone_request
|
||||
from rest_framework.views import APIView
|
||||
|
||||
|
||||
def as_query_fields(items):
|
||||
"""
|
||||
Take a list of Fields and plain strings.
|
||||
Convert any pain strings into `location='query'` Field instances.
|
||||
"""
|
||||
return [
|
||||
item if isinstance(item, coreapi.Field) else coreapi.Field(name=item, required=False, location='query')
|
||||
for item in items
|
||||
]
|
||||
|
||||
|
||||
def is_api_view(callback):
|
||||
"""
|
||||
Return `True` if the given view callback is a REST framework view/viewset.
|
||||
"""
|
||||
cls = getattr(callback, 'cls', None)
|
||||
return (cls is not None) and issubclass(cls, APIView)
|
||||
|
||||
|
||||
def insert_into(target, keys, item):
|
||||
"""
|
||||
Insert `item` into the nested dictionary `target`.
|
||||
|
||||
For example:
|
||||
|
||||
target = {}
|
||||
insert_into(target, ('users', 'list'), Link(...))
|
||||
insert_into(target, ('users', 'detail'), Link(...))
|
||||
assert target == {'users': {'list': Link(...), 'detail': Link(...)}}
|
||||
"""
|
||||
for key in keys[:1]:
|
||||
if key not in target:
|
||||
target[key] = {}
|
||||
target = target[key]
|
||||
target[keys[-1]] = item
|
||||
|
||||
|
||||
class SchemaGenerator(object):
|
||||
default_mapping = {
|
||||
'get': 'read',
|
||||
'post': 'create',
|
||||
'put': 'update',
|
||||
'patch': 'partial_update',
|
||||
'delete': 'destroy',
|
||||
}
|
||||
|
||||
def __init__(self, title=None, patterns=None, urlconf=None):
|
||||
assert coreapi, '`coreapi` must be installed for schema support.'
|
||||
|
||||
if patterns is None and urlconf is not None:
|
||||
if isinstance(urlconf, six.string_types):
|
||||
urls = import_module(urlconf)
|
||||
else:
|
||||
urls = urlconf
|
||||
patterns = urls.urlpatterns
|
||||
elif patterns is None and urlconf is None:
|
||||
urls = import_module(settings.ROOT_URLCONF)
|
||||
patterns = urls.urlpatterns
|
||||
|
||||
self.title = title
|
||||
self.endpoints = self.get_api_endpoints(patterns)
|
||||
|
||||
def get_schema(self, request=None):
|
||||
if request is None:
|
||||
endpoints = self.endpoints
|
||||
else:
|
||||
# Filter the list of endpoints to only include those that
|
||||
# the user has permission on.
|
||||
endpoints = []
|
||||
for key, link, callback in self.endpoints:
|
||||
method = link.action.upper()
|
||||
view = callback.cls()
|
||||
view.request = clone_request(request, method)
|
||||
try:
|
||||
view.check_permissions(view.request)
|
||||
except exceptions.APIException:
|
||||
pass
|
||||
else:
|
||||
endpoints.append((key, link, callback))
|
||||
|
||||
if not endpoints:
|
||||
return None
|
||||
|
||||
# Generate the schema content structure, from the endpoints.
|
||||
# ('users', 'list'), Link -> {'users': {'list': Link()}}
|
||||
content = {}
|
||||
for key, link, callback in endpoints:
|
||||
insert_into(content, key, link)
|
||||
|
||||
# Return the schema document.
|
||||
return coreapi.Document(title=self.title, content=content)
|
||||
|
||||
def get_api_endpoints(self, patterns, prefix=''):
|
||||
"""
|
||||
Return a list of all available API endpoints by inspecting the URL conf.
|
||||
"""
|
||||
api_endpoints = []
|
||||
|
||||
for pattern in patterns:
|
||||
path_regex = prefix + pattern.regex.pattern
|
||||
|
||||
if isinstance(pattern, RegexURLPattern):
|
||||
path = self.get_path(path_regex)
|
||||
callback = pattern.callback
|
||||
if self.should_include_endpoint(path, callback):
|
||||
for method in self.get_allowed_methods(callback):
|
||||
key = self.get_key(path, method, callback)
|
||||
link = self.get_link(path, method, callback)
|
||||
endpoint = (key, link, callback)
|
||||
api_endpoints.append(endpoint)
|
||||
|
||||
elif isinstance(pattern, RegexURLResolver):
|
||||
nested_endpoints = self.get_api_endpoints(
|
||||
patterns=pattern.url_patterns,
|
||||
prefix=path_regex
|
||||
)
|
||||
api_endpoints.extend(nested_endpoints)
|
||||
|
||||
return api_endpoints
|
||||
|
||||
def get_path(self, path_regex):
|
||||
"""
|
||||
Given a URL conf regex, return a URI template string.
|
||||
"""
|
||||
path = simplify_regex(path_regex)
|
||||
path = path.replace('<', '{').replace('>', '}')
|
||||
return path
|
||||
|
||||
def should_include_endpoint(self, path, callback):
|
||||
"""
|
||||
Return `True` if the given endpoint should be included.
|
||||
"""
|
||||
if not is_api_view(callback):
|
||||
return False # Ignore anything except REST framework views.
|
||||
|
||||
if path.endswith('.{format}') or path.endswith('.{format}/'):
|
||||
return False # Ignore .json style URLs.
|
||||
|
||||
if path == '/':
|
||||
return False # Ignore the root endpoint.
|
||||
|
||||
return True
|
||||
|
||||
def get_allowed_methods(self, callback):
|
||||
"""
|
||||
Return a list of the valid HTTP methods for this endpoint.
|
||||
"""
|
||||
if hasattr(callback, 'actions'):
|
||||
return [method.upper() for method in callback.actions.keys()]
|
||||
|
||||
return [
|
||||
method for method in
|
||||
callback.cls().allowed_methods if method != 'OPTIONS'
|
||||
]
|
||||
|
||||
def get_key(self, path, method, callback):
|
||||
"""
|
||||
Return a tuple of strings, indicating the identity to use for a
|
||||
given endpoint. eg. ('users', 'list').
|
||||
"""
|
||||
category = None
|
||||
for item in path.strip('/').split('/'):
|
||||
if '{' in item:
|
||||
break
|
||||
category = item
|
||||
|
||||
actions = getattr(callback, 'actions', self.default_mapping)
|
||||
action = actions[method.lower()]
|
||||
|
||||
if category:
|
||||
return (category, action)
|
||||
return (action,)
|
||||
|
||||
# Methods for generating each individual `Link` instance...
|
||||
|
||||
def get_link(self, path, method, callback):
|
||||
"""
|
||||
Return a `coreapi.Link` instance for the given endpoint.
|
||||
"""
|
||||
view = callback.cls()
|
||||
|
||||
fields = self.get_path_fields(path, method, callback, view)
|
||||
fields += self.get_serializer_fields(path, method, callback, view)
|
||||
fields += self.get_pagination_fields(path, method, callback, view)
|
||||
fields += self.get_filter_fields(path, method, callback, view)
|
||||
|
||||
if fields and any([field.location in ('form', 'body') for field in fields]):
|
||||
encoding = self.get_encoding(path, method, callback, view)
|
||||
else:
|
||||
encoding = None
|
||||
|
||||
return coreapi.Link(
|
||||
url=path,
|
||||
action=method.lower(),
|
||||
encoding=encoding,
|
||||
fields=fields
|
||||
)
|
||||
|
||||
def get_encoding(self, path, method, callback, view):
|
||||
"""
|
||||
Return the 'encoding' parameter to use for a given endpoint.
|
||||
"""
|
||||
# Core API supports the following request encodings over HTTP...
|
||||
supported_media_types = set((
|
||||
'application/json',
|
||||
'application/x-www-form-urlencoded',
|
||||
'multipart/form-data',
|
||||
))
|
||||
parser_classes = getattr(view, 'parser_classes', [])
|
||||
for parser_class in parser_classes:
|
||||
media_type = getattr(parser_class, 'media_type', None)
|
||||
if media_type in supported_media_types:
|
||||
return media_type
|
||||
# Raw binary uploads are supported with "application/octet-stream"
|
||||
if media_type == '*/*':
|
||||
return 'application/octet-stream'
|
||||
|
||||
return None
|
||||
|
||||
def get_path_fields(self, path, method, callback, view):
|
||||
"""
|
||||
Return a list of `coreapi.Field` instances corresponding to any
|
||||
templated path variables.
|
||||
"""
|
||||
fields = []
|
||||
|
||||
for variable in uritemplate.variables(path):
|
||||
field = coreapi.Field(name=variable, location='path', required=True)
|
||||
fields.append(field)
|
||||
|
||||
return fields
|
||||
|
||||
def get_serializer_fields(self, path, method, callback, view):
|
||||
"""
|
||||
Return a list of `coreapi.Field` instances corresponding to any
|
||||
request body input, as determined by the serializer class.
|
||||
"""
|
||||
if method not in ('PUT', 'PATCH', 'POST'):
|
||||
return []
|
||||
|
||||
fields = []
|
||||
|
||||
serializer_class = view.get_serializer_class()
|
||||
serializer = serializer_class()
|
||||
|
||||
if isinstance(serializer, serializers.ListSerializer):
|
||||
return coreapi.Field(name='data', location='body', required=True)
|
||||
|
||||
if not isinstance(serializer, serializers.Serializer):
|
||||
return []
|
||||
|
||||
for field in serializer.fields.values():
|
||||
if field.read_only:
|
||||
continue
|
||||
required = field.required and method != 'PATCH'
|
||||
field = coreapi.Field(name=field.source, location='form', required=required)
|
||||
fields.append(field)
|
||||
|
||||
return fields
|
||||
|
||||
def get_pagination_fields(self, path, method, callback, view):
|
||||
if method != 'GET':
|
||||
return []
|
||||
|
||||
if hasattr(callback, 'actions') and ('list' not in callback.actions.values()):
|
||||
return []
|
||||
|
||||
if not hasattr(view, 'pagination_class'):
|
||||
return []
|
||||
|
||||
paginator = view.pagination_class()
|
||||
return as_query_fields(paginator.get_fields(view))
|
||||
|
||||
def get_filter_fields(self, path, method, callback, view):
|
||||
if method != 'GET':
|
||||
return []
|
||||
|
||||
if hasattr(callback, 'actions') and ('list' not in callback.actions.values()):
|
||||
return []
|
||||
|
||||
if not hasattr(view, 'filter_backends'):
|
||||
return []
|
||||
|
||||
fields = []
|
||||
for filter_backend in view.filter_backends:
|
||||
fields += as_query_fields(filter_backend().get_fields(view))
|
||||
return fields
|
|
@ -13,7 +13,7 @@ from django.utils import six, timezone
|
|||
from django.utils.encoding import force_text
|
||||
from django.utils.functional import Promise
|
||||
|
||||
from rest_framework.compat import total_seconds
|
||||
from rest_framework.compat import coreapi, total_seconds
|
||||
|
||||
|
||||
class JSONEncoder(json.JSONEncoder):
|
||||
|
@ -64,4 +64,9 @@ class JSONEncoder(json.JSONEncoder):
|
|||
pass
|
||||
elif hasattr(obj, '__iter__'):
|
||||
return tuple(item for item in obj)
|
||||
elif (coreapi is not None) and isinstance(obj, (coreapi.Document, coreapi.Error)):
|
||||
raise RuntimeError(
|
||||
'Cannot return a coreapi object from a JSON view. '
|
||||
'You should be using a schema renderer instead for this view.'
|
||||
)
|
||||
return super(JSONEncoder, self).default(obj)
|
||||
|
|
|
@ -98,6 +98,7 @@ class ViewSetMixin(object):
|
|||
# resolved URL.
|
||||
view.cls = cls
|
||||
view.suffix = initkwargs.get('suffix', None)
|
||||
view.actions = actions
|
||||
return csrf_exempt(view)
|
||||
|
||||
def initialize_request(self, request, *args, **kwargs):
|
||||
|
|
|
@ -14,7 +14,7 @@ PYTEST_ARGS = {
|
|||
|
||||
FLAKE8_ARGS = ['rest_framework', 'tests', '--ignore=E501']
|
||||
|
||||
ISORT_ARGS = ['--recursive', '--check-only', '-p', 'tests', 'rest_framework', 'tests']
|
||||
ISORT_ARGS = ['--recursive', '--check-only', '-o' 'uritemplate', '-p', 'tests', 'rest_framework', 'tests']
|
||||
|
||||
sys.path.append(os.path.dirname(__file__))
|
||||
|
||||
|
|
0
schema-support
Normal file
0
schema-support
Normal file
|
@ -257,7 +257,7 @@ class TestNameableRoot(TestCase):
|
|||
|
||||
def test_router_has_custom_name(self):
|
||||
expected = 'nameable-root'
|
||||
self.assertEqual(expected, self.urls[0].name)
|
||||
self.assertEqual(expected, self.urls[-1].name)
|
||||
|
||||
|
||||
class TestActionKeywordArgs(TestCase):
|
||||
|
|
137
tests/test_schemas.py
Normal file
137
tests/test_schemas.py
Normal file
|
@ -0,0 +1,137 @@
|
|||
import unittest
|
||||
|
||||
from django.conf.urls import include, url
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
from rest_framework import filters, pagination, permissions, serializers
|
||||
from rest_framework.compat import coreapi
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from rest_framework.test import APIClient
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
|
||||
class MockUser(object):
|
||||
def is_authenticated(self):
|
||||
return True
|
||||
|
||||
|
||||
class ExamplePagination(pagination.PageNumberPagination):
|
||||
page_size = 100
|
||||
|
||||
|
||||
class ExampleSerializer(serializers.Serializer):
|
||||
a = serializers.CharField(required=True)
|
||||
b = serializers.CharField(required=False)
|
||||
|
||||
|
||||
class ExampleViewSet(ModelViewSet):
|
||||
pagination_class = ExamplePagination
|
||||
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
|
||||
filter_backends = [filters.OrderingFilter]
|
||||
serializer_class = ExampleSerializer
|
||||
|
||||
|
||||
router = DefaultRouter(schema_title='Example API' if coreapi else None)
|
||||
router.register('example', ExampleViewSet, base_name='example')
|
||||
urlpatterns = [
|
||||
url(r'^', include(router.urls))
|
||||
]
|
||||
|
||||
|
||||
@unittest.skipUnless(coreapi, 'coreapi is not installed')
|
||||
@override_settings(ROOT_URLCONF='tests.test_schemas')
|
||||
class TestRouterGeneratedSchema(TestCase):
|
||||
def test_anonymous_request(self):
|
||||
client = APIClient()
|
||||
response = client.get('/', HTTP_ACCEPT='application/vnd.coreapi+json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
expected = coreapi.Document(
|
||||
url='',
|
||||
title='Example API',
|
||||
content={
|
||||
'example': {
|
||||
'list': coreapi.Link(
|
||||
url='/example/',
|
||||
action='get',
|
||||
fields=[
|
||||
coreapi.Field('page', required=False, location='query'),
|
||||
coreapi.Field('ordering', required=False, location='query')
|
||||
]
|
||||
),
|
||||
'retrieve': coreapi.Link(
|
||||
url='/example/{pk}/',
|
||||
action='get',
|
||||
fields=[
|
||||
coreapi.Field('pk', required=True, location='path')
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
self.assertEqual(response.data, expected)
|
||||
|
||||
def test_authenticated_request(self):
|
||||
client = APIClient()
|
||||
client.force_authenticate(MockUser())
|
||||
response = client.get('/', HTTP_ACCEPT='application/vnd.coreapi+json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
expected = coreapi.Document(
|
||||
url='',
|
||||
title='Example API',
|
||||
content={
|
||||
'example': {
|
||||
'list': coreapi.Link(
|
||||
url='/example/',
|
||||
action='get',
|
||||
fields=[
|
||||
coreapi.Field('page', required=False, location='query'),
|
||||
coreapi.Field('ordering', required=False, location='query')
|
||||
]
|
||||
),
|
||||
'create': coreapi.Link(
|
||||
url='/example/',
|
||||
action='post',
|
||||
encoding='application/json',
|
||||
fields=[
|
||||
coreapi.Field('a', required=True, location='form'),
|
||||
coreapi.Field('b', required=False, location='form')
|
||||
]
|
||||
),
|
||||
'retrieve': coreapi.Link(
|
||||
url='/example/{pk}/',
|
||||
action='get',
|
||||
fields=[
|
||||
coreapi.Field('pk', required=True, location='path')
|
||||
]
|
||||
),
|
||||
'update': coreapi.Link(
|
||||
url='/example/{pk}/',
|
||||
action='put',
|
||||
encoding='application/json',
|
||||
fields=[
|
||||
coreapi.Field('pk', required=True, location='path'),
|
||||
coreapi.Field('a', required=True, location='form'),
|
||||
coreapi.Field('b', required=False, location='form')
|
||||
]
|
||||
),
|
||||
'partial_update': coreapi.Link(
|
||||
url='/example/{pk}/',
|
||||
action='patch',
|
||||
encoding='application/json',
|
||||
fields=[
|
||||
coreapi.Field('pk', required=True, location='path'),
|
||||
coreapi.Field('a', required=False, location='form'),
|
||||
coreapi.Field('b', required=False, location='form')
|
||||
]
|
||||
),
|
||||
'destroy': coreapi.Link(
|
||||
url='/example/{pk}/',
|
||||
action='delete',
|
||||
fields=[
|
||||
coreapi.Field('pk', required=True, location='path')
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
self.assertEqual(response.data, expected)
|
Loading…
Reference in New Issue
Block a user