mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-22 09:36:49 +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:
|
The following packages are optional:
|
||||||
|
|
||||||
|
* [coreapi][coreapi] (1.21.0+) - Schema generation support.
|
||||||
* [Markdown][markdown] (2.1.0+) - Markdown support for the browsable API.
|
* [Markdown][markdown] (2.1.0+) - Markdown support for the browsable API.
|
||||||
* [django-filter][django-filter] (0.9.2+) - Filtering support.
|
* [django-filter][django-filter] (0.9.2+) - Filtering support.
|
||||||
* [django-crispy-forms][django-crispy-forms] - Improved HTML display for filtering.
|
* [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]
|
* [Versioning][versioning]
|
||||||
* [Content negotiation][contentnegotiation]
|
* [Content negotiation][contentnegotiation]
|
||||||
* [Metadata][metadata]
|
* [Metadata][metadata]
|
||||||
|
* [Schemas][schemas]
|
||||||
* [Format suffixes][formatsuffixes]
|
* [Format suffixes][formatsuffixes]
|
||||||
* [Returning URLs][reverse]
|
* [Returning URLs][reverse]
|
||||||
* [Exceptions][exceptions]
|
* [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.
|
General guides to using REST framework.
|
||||||
|
|
||||||
* [Documenting your API][documenting-your-api]
|
* [Documenting your API][documenting-your-api]
|
||||||
|
* [API Clients][api-clients]
|
||||||
* [Internationalization][internationalization]
|
* [Internationalization][internationalization]
|
||||||
* [AJAX, CSRF & CORS][ajax-csrf-cors]
|
* [AJAX, CSRF & CORS][ajax-csrf-cors]
|
||||||
* [HTML & Forms][html-and-forms]
|
* [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/
|
[redhat]: https://www.redhat.com/
|
||||||
[heroku]: https://www.heroku.com/
|
[heroku]: https://www.heroku.com/
|
||||||
[eventbrite]: https://www.eventbrite.co.uk/about/
|
[eventbrite]: https://www.eventbrite.co.uk/about/
|
||||||
|
[coreapi]: http://pypi.python.org/pypi/coreapi/
|
||||||
[markdown]: http://pypi.python.org/pypi/Markdown/
|
[markdown]: http://pypi.python.org/pypi/Markdown/
|
||||||
[django-filter]: http://pypi.python.org/pypi/django-filter
|
[django-filter]: http://pypi.python.org/pypi/django-filter
|
||||||
[django-crispy-forms]: https://github.com/maraujop/django-crispy-forms
|
[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-4]: tutorial/4-authentication-and-permissions.md
|
||||||
[tut-5]: tutorial/5-relationships-and-hyperlinked-apis.md
|
[tut-5]: tutorial/5-relationships-and-hyperlinked-apis.md
|
||||||
[tut-6]: tutorial/6-viewsets-and-routers.md
|
[tut-6]: tutorial/6-viewsets-and-routers.md
|
||||||
|
[tut-7]: tutorial/7-schemas-and-client-libraries.md
|
||||||
|
|
||||||
[request]: api-guide/requests.md
|
[request]: api-guide/requests.md
|
||||||
[response]: api-guide/responses.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
|
[versioning]: api-guide/versioning.md
|
||||||
[contentnegotiation]: api-guide/content-negotiation.md
|
[contentnegotiation]: api-guide/content-negotiation.md
|
||||||
[metadata]: api-guide/metadata.md
|
[metadata]: api-guide/metadata.md
|
||||||
|
[schemas]: 'api-guide/schemas.md'
|
||||||
[formatsuffixes]: api-guide/format-suffixes.md
|
[formatsuffixes]: api-guide/format-suffixes.md
|
||||||
[reverse]: api-guide/reverse.md
|
[reverse]: api-guide/reverse.md
|
||||||
[exceptions]: api-guide/exceptions.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
|
[settings]: api-guide/settings.md
|
||||||
|
|
||||||
[documenting-your-api]: topics/documenting-your-api.md
|
[documenting-your-api]: topics/documenting-your-api.md
|
||||||
|
[api-clients]: topics/api-clients.md
|
||||||
[internationalization]: topics/internationalization.md
|
[internationalization]: topics/internationalization.md
|
||||||
[ajax-csrf-cors]: topics/ajax-csrf-cors.md
|
[ajax-csrf-cors]: topics/ajax-csrf-cors.md
|
||||||
[html-and-forms]: topics/html-and-forms.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.
|
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.
|
[tut-7]: 7-schemas-and-client-libraries.md
|
||||||
|
|
||||||
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
|
|
||||||
|
|
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'
|
- '4 - Authentication and permissions': 'tutorial/4-authentication-and-permissions.md'
|
||||||
- '5 - Relationships and hyperlinked APIs': 'tutorial/5-relationships-and-hyperlinked-apis.md'
|
- '5 - Relationships and hyperlinked APIs': 'tutorial/5-relationships-and-hyperlinked-apis.md'
|
||||||
- '6 - Viewsets and routers': 'tutorial/6-viewsets-and-routers.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:
|
- API Guide:
|
||||||
- 'Requests': 'api-guide/requests.md'
|
- 'Requests': 'api-guide/requests.md'
|
||||||
- 'Responses': 'api-guide/responses.md'
|
- 'Responses': 'api-guide/responses.md'
|
||||||
|
@ -41,6 +42,7 @@ pages:
|
||||||
- 'Versioning': 'api-guide/versioning.md'
|
- 'Versioning': 'api-guide/versioning.md'
|
||||||
- 'Content negotiation': 'api-guide/content-negotiation.md'
|
- 'Content negotiation': 'api-guide/content-negotiation.md'
|
||||||
- 'Metadata': 'api-guide/metadata.md'
|
- 'Metadata': 'api-guide/metadata.md'
|
||||||
|
- 'Schemas': 'api-guide/schemas.md'
|
||||||
- 'Format suffixes': 'api-guide/format-suffixes.md'
|
- 'Format suffixes': 'api-guide/format-suffixes.md'
|
||||||
- 'Returning URLs': 'api-guide/reverse.md'
|
- 'Returning URLs': 'api-guide/reverse.md'
|
||||||
- 'Exceptions': 'api-guide/exceptions.md'
|
- 'Exceptions': 'api-guide/exceptions.md'
|
||||||
|
@ -49,6 +51,7 @@ pages:
|
||||||
- 'Settings': 'api-guide/settings.md'
|
- 'Settings': 'api-guide/settings.md'
|
||||||
- Topics:
|
- Topics:
|
||||||
- 'Documenting your API': 'topics/documenting-your-api.md'
|
- 'Documenting your API': 'topics/documenting-your-api.md'
|
||||||
|
- 'API Clients': 'topics/api-clients.md'
|
||||||
- 'Internationalization': 'topics/internationalization.md'
|
- 'Internationalization': 'topics/internationalization.md'
|
||||||
- 'AJAX, CSRF & CORS': 'topics/ajax-csrf-cors.md'
|
- 'AJAX, CSRF & CORS': 'topics/ajax-csrf-cors.md'
|
||||||
- 'HTML & Forms': 'topics/html-and-forms.md'
|
- 'HTML & Forms': 'topics/html-and-forms.md'
|
||||||
|
|
|
@ -2,3 +2,4 @@
|
||||||
markdown==2.6.4
|
markdown==2.6.4
|
||||||
django-guardian==1.4.3
|
django-guardian==1.4.3
|
||||||
django-filter==0.13.0
|
django-filter==0.13.0
|
||||||
|
coreapi==1.21.1
|
||||||
|
|
|
@ -156,6 +156,16 @@ except ImportError:
|
||||||
crispy_forms = None
|
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
|
# Django-guardian is optional. Import only if guardian is in INSTALLED_APPS
|
||||||
# Fixes (#1712). We keep the try/except for the test suite.
|
# Fixes (#1712). We keep the try/except for the test suite.
|
||||||
guardian = None
|
guardian = None
|
||||||
|
|
|
@ -72,6 +72,9 @@ class BaseFilterBackend(object):
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError(".filter_queryset() must be overridden.")
|
raise NotImplementedError(".filter_queryset() must be overridden.")
|
||||||
|
|
||||||
|
def get_fields(self, view):
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
class DjangoFilterBackend(BaseFilterBackend):
|
class DjangoFilterBackend(BaseFilterBackend):
|
||||||
"""
|
"""
|
||||||
|
@ -128,6 +131,17 @@ class DjangoFilterBackend(BaseFilterBackend):
|
||||||
template = loader.get_template(self.template)
|
template = loader.get_template(self.template)
|
||||||
return template_render(template, context)
|
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):
|
class SearchFilter(BaseFilterBackend):
|
||||||
# The URL query parameter used for the search.
|
# The URL query parameter used for the search.
|
||||||
|
@ -217,6 +231,9 @@ class SearchFilter(BaseFilterBackend):
|
||||||
template = loader.get_template(self.template)
|
template = loader.get_template(self.template)
|
||||||
return template_render(template, context)
|
return template_render(template, context)
|
||||||
|
|
||||||
|
def get_fields(self, view):
|
||||||
|
return [self.search_param]
|
||||||
|
|
||||||
|
|
||||||
class OrderingFilter(BaseFilterBackend):
|
class OrderingFilter(BaseFilterBackend):
|
||||||
# The URL query parameter used for the ordering.
|
# The URL query parameter used for the ordering.
|
||||||
|
@ -330,6 +347,9 @@ class OrderingFilter(BaseFilterBackend):
|
||||||
context = self.get_template_context(request, queryset, view)
|
context = self.get_template_context(request, queryset, view)
|
||||||
return template_render(template, context)
|
return template_render(template, context)
|
||||||
|
|
||||||
|
def get_fields(self, view):
|
||||||
|
return [self.ordering_param]
|
||||||
|
|
||||||
|
|
||||||
class DjangoObjectPermissionsFilter(BaseFilterBackend):
|
class DjangoObjectPermissionsFilter(BaseFilterBackend):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -157,6 +157,9 @@ class BasePagination(object):
|
||||||
def get_results(self, data):
|
def get_results(self, data):
|
||||||
return data['results']
|
return data['results']
|
||||||
|
|
||||||
|
def get_fields(self, view):
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
class PageNumberPagination(BasePagination):
|
class PageNumberPagination(BasePagination):
|
||||||
"""
|
"""
|
||||||
|
@ -280,6 +283,11 @@ class PageNumberPagination(BasePagination):
|
||||||
context = self.get_html_context()
|
context = self.get_html_context()
|
||||||
return template_render(template, 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):
|
class LimitOffsetPagination(BasePagination):
|
||||||
"""
|
"""
|
||||||
|
@ -404,6 +412,9 @@ class LimitOffsetPagination(BasePagination):
|
||||||
context = self.get_html_context()
|
context = self.get_html_context()
|
||||||
return template_render(template, context)
|
return template_render(template, context)
|
||||||
|
|
||||||
|
def get_fields(self, view):
|
||||||
|
return [self.limit_query_param, self.offset_query_param]
|
||||||
|
|
||||||
|
|
||||||
class CursorPagination(BasePagination):
|
class CursorPagination(BasePagination):
|
||||||
"""
|
"""
|
||||||
|
@ -706,3 +717,6 @@ class CursorPagination(BasePagination):
|
||||||
template = loader.get_template(self.template)
|
template = loader.get_template(self.template)
|
||||||
context = self.get_html_context()
|
context = self.get_html_context()
|
||||||
return template_render(template, 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 import VERSION, exceptions, serializers, status
|
||||||
from rest_framework.compat import (
|
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.exceptions import ParseError
|
||||||
from rest_framework.request import is_form_media_type, override_method
|
from rest_framework.request import is_form_media_type, override_method
|
||||||
|
@ -790,3 +791,17 @@ class MultiPartRenderer(BaseRenderer):
|
||||||
"test case." % key
|
"test case." % key
|
||||||
)
|
)
|
||||||
return encode_multipart(self.BOUNDARY, data)
|
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.exceptions import ImproperlyConfigured
|
||||||
from django.core.urlresolvers import NoReverseMatch
|
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.response import Response
|
||||||
from rest_framework.reverse import reverse
|
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
|
from rest_framework.urlpatterns import format_suffix_patterns
|
||||||
|
|
||||||
Route = namedtuple('Route', ['url', 'mapping', 'name', 'initkwargs'])
|
Route = namedtuple('Route', ['url', 'mapping', 'name', 'initkwargs'])
|
||||||
|
@ -255,6 +257,7 @@ class SimpleRouter(BaseRouter):
|
||||||
lookup=lookup,
|
lookup=lookup,
|
||||||
trailing_slash=self.trailing_slash
|
trailing_slash=self.trailing_slash
|
||||||
)
|
)
|
||||||
|
|
||||||
view = viewset.as_view(mapping, **route.initkwargs)
|
view = viewset.as_view(mapping, **route.initkwargs)
|
||||||
name = route.name.format(basename=basename)
|
name = route.name.format(basename=basename)
|
||||||
ret.append(url(regex, view, name=name))
|
ret.append(url(regex, view, name=name))
|
||||||
|
@ -270,8 +273,13 @@ class DefaultRouter(SimpleRouter):
|
||||||
include_root_view = True
|
include_root_view = True
|
||||||
include_format_suffixes = True
|
include_format_suffixes = True
|
||||||
root_view_name = 'api-root'
|
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.
|
Return a view to use as the API root.
|
||||||
"""
|
"""
|
||||||
|
@ -280,10 +288,33 @@ class DefaultRouter(SimpleRouter):
|
||||||
for prefix, viewset, basename in self.registry:
|
for prefix, viewset, basename in self.registry:
|
||||||
api_root_dict[prefix] = list_name.format(basename=basename)
|
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):
|
class APIRoot(views.APIView):
|
||||||
_ignore_model_permissions = True
|
_ignore_model_permissions = True
|
||||||
|
renderer_classes = view_renderers
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
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()
|
ret = OrderedDict()
|
||||||
namespace = request.resolver_match.namespace
|
namespace = request.resolver_match.namespace
|
||||||
for key, url_name in api_root_dict.items():
|
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
|
Generate the list of URL patterns, including a default root view
|
||||||
for the API, and appending `.json` style format suffixes.
|
for the API, and appending `.json` style format suffixes.
|
||||||
"""
|
"""
|
||||||
urls = []
|
urls = super(DefaultRouter, self).get_urls()
|
||||||
|
|
||||||
if self.include_root_view:
|
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)
|
urls.append(root_url)
|
||||||
|
|
||||||
default_urls = super(DefaultRouter, self).get_urls()
|
|
||||||
urls.extend(default_urls)
|
|
||||||
|
|
||||||
if self.include_format_suffixes:
|
if self.include_format_suffixes:
|
||||||
urls = format_suffix_patterns(urls)
|
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.encoding import force_text
|
||||||
from django.utils.functional import Promise
|
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):
|
class JSONEncoder(json.JSONEncoder):
|
||||||
|
@ -64,4 +64,9 @@ class JSONEncoder(json.JSONEncoder):
|
||||||
pass
|
pass
|
||||||
elif hasattr(obj, '__iter__'):
|
elif hasattr(obj, '__iter__'):
|
||||||
return tuple(item for item in obj)
|
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)
|
return super(JSONEncoder, self).default(obj)
|
||||||
|
|
|
@ -98,6 +98,7 @@ class ViewSetMixin(object):
|
||||||
# resolved URL.
|
# resolved URL.
|
||||||
view.cls = cls
|
view.cls = cls
|
||||||
view.suffix = initkwargs.get('suffix', None)
|
view.suffix = initkwargs.get('suffix', None)
|
||||||
|
view.actions = actions
|
||||||
return csrf_exempt(view)
|
return csrf_exempt(view)
|
||||||
|
|
||||||
def initialize_request(self, request, *args, **kwargs):
|
def initialize_request(self, request, *args, **kwargs):
|
||||||
|
|
|
@ -14,7 +14,7 @@ PYTEST_ARGS = {
|
||||||
|
|
||||||
FLAKE8_ARGS = ['rest_framework', 'tests', '--ignore=E501']
|
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__))
|
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):
|
def test_router_has_custom_name(self):
|
||||||
expected = 'nameable-root'
|
expected = 'nameable-root'
|
||||||
self.assertEqual(expected, self.urls[0].name)
|
self.assertEqual(expected, self.urls[-1].name)
|
||||||
|
|
||||||
|
|
||||||
class TestActionKeywordArgs(TestCase):
|
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