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:
Tom Christie 2016-07-04 16:38:17 +01:00 committed by GitHub
parent 1d2fba906e
commit 6ff9840bde
20 changed files with 1449 additions and 34 deletions

383
docs/api-guide/schemas.md Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -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
View 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

View File

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

View 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

View File

@ -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'

View File

@ -2,3 +2,4 @@
markdown==2.6.4
django-guardian==1.4.3
django-filter==0.13.0
coreapi==1.21.1

View File

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

View File

@ -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):
"""

View File

@ -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]

View File

@ -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)

View File

@ -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
View 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

View File

@ -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)

View File

@ -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):

View File

@ -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
View File

View 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
View 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)