Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
bc836aa
Initial pass at schema support
lovelydinosaur Jun 8, 2016
b64340b
Add coreapi to optional requirements.
lovelydinosaur Jun 9, 2016
c890ad4
Clean up test failures
lovelydinosaur Jun 9, 2016
2d28390
Add missing newline
lovelydinosaur Jun 9, 2016
744dba4
Minor docs update
lovelydinosaur Jun 9, 2016
56ece73
Version bump for coreapi in requirements
lovelydinosaur Jun 9, 2016
99adbf1
Catch SyntaxError when importing coreapi with python 3.2
lovelydinosaur Jun 9, 2016
80c595e
Add --diff to isort
lovelydinosaur Jun 9, 2016
47c7765
Import coreapi from compat
lovelydinosaur Jun 9, 2016
29e228d
Fail gracefully if attempting to use schemas without coreapi being in…
lovelydinosaur Jun 9, 2016
eeffca4
Tutorial updates
lovelydinosaur Jun 9, 2016
6c60f58
Docs update
lovelydinosaur Jun 9, 2016
b7fcdd2
Initial schema generation & first tutorial 7 draft
lovelydinosaur Jun 10, 2016
2e60f41
Spelling
lovelydinosaur Jun 10, 2016
2ffa145
Remove unused variable
lovelydinosaur Jun 10, 2016
b709dd4
Docs tweak
lovelydinosaur Jun 10, 2016
474a23e
Merge branch 'master' into schema-support
lovelydinosaur Jun 14, 2016
4822896
Added SchemaGenerator class
lovelydinosaur Jun 15, 2016
1f76cca
Fail gracefully if coreapi is not installed and SchemaGenerator is used
lovelydinosaur Jun 15, 2016
cad24b1
Schema docs, pagination controls, filter controls
lovelydinosaur Jun 21, 2016
8fb2602
Resolve NameError
lovelydinosaur Jun 21, 2016
b438281
Add 'view' argument to 'get_fields()'
lovelydinosaur Jun 21, 2016
8519b4e
Remove extranous blank line
lovelydinosaur Jun 21, 2016
2f5c974
Add integration tests for schema generation
lovelydinosaur Jun 22, 2016
e78753d
Only set 'encoding' if a 'form' or 'body' field exists
lovelydinosaur Jun 22, 2016
84bb5ea
Do not use schmea in tests if coreapi is not installed
lovelydinosaur Jun 24, 2016
63e8467
Inital pass at API client docs
lovelydinosaur Jun 29, 2016
bdbcb33
Inital pass at API client docs
lovelydinosaur Jun 29, 2016
7236af3
More work towards client documentation
lovelydinosaur Jun 30, 2016
89540ab
Add coreapi to optional packages list
lovelydinosaur Jul 4, 2016
e3ced75
Clean up API clients docs
lovelydinosaur Jul 4, 2016
12be5b3
Resolve typo
lovelydinosaur Jul 4, 2016
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added docs/img/corejson-format.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,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
Expand Down
26 changes: 3 additions & 23 deletions docs/tutorial/6-viewsets-and-routers.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,27 +130,7 @@ Using viewsets can be a really useful abstraction. It helps ensure that URL con

That doesn't mean it's always the right approach to take. There's a similar set of trade-offs to consider as when using class-based views instead of function based views. Using viewsets is less explicit than building your views individually.

## Reviewing our work
In [part 7][tut-7] of the tutorial we'll look at how we can add an API schema,
and interact with our API using a client library or command line tool.

With an incredibly small amount of code, we've now got a complete pastebin Web API, which is fully web browsable, and comes complete with authentication, per-object permissions, and multiple renderer formats.

We've walked through each step of the design process, and seen how if we need to customize anything we can gradually work our way down to simply using regular Django views.

You can review the final [tutorial code][repo] on GitHub, or try out a live example in [the sandbox][sandbox].

## Onwards and upwards

We've reached the end of our tutorial. If you want to get more involved in the REST framework project, here are a few places you can start:

* Contribute on [GitHub][github] by reviewing and submitting issues, and making pull requests.
* Join the [REST framework discussion group][group], and help build the community.
* Follow [the author][twitter] on Twitter and say hi.

**Now go build awesome things.**


[repo]: https://github.com/tomchristie/rest-framework-tutorial
[sandbox]: http://restframework.herokuapp.com/
[github]: https://github.com/tomchristie/django-rest-framework
[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
[twitter]: https://twitter.com/_tomchristie
[tut-7]: 7-schemas-and-client-libraries.md
216 changes: 216 additions & 0 deletions docs/tutorial/7-schemas-and-client-libraries.md
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
1 change: 1 addition & 0 deletions requirements/requirements-optionals.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
markdown==2.6.4
django-guardian==1.4.3
django-filter==0.13.0
coreapi==1.21.1
10 changes: 10 additions & 0 deletions rest_framework/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,16 @@ def value_from_object(field, obj):
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
Expand Down
17 changes: 16 additions & 1 deletion rest_framework/renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@

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
Expand Down Expand Up @@ -790,3 +791,17 @@ def render(self, data, accepted_media_type=None, renderer_context=None):
"test case." % key
)
return encode_multipart(self.BOUNDARY, data)


class CoreJSONRenderer(BaseRenderer):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was a move in 3.1 to remove features which had third-party dependencies from the core and put them into third-party packages that were still highly visible. See django-rest-framework-xml, django-rest-framework-yaml, and django-rest-framework-oauth for examples.

Is that still something we are pushing for?

Copy link
Contributor Author

@lovelydinosaur lovelydinosaur Jun 15, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kinda. I see coreapi as a foundational thing here, so it's a bit different.

The various types of schema and docs that you can use it to generate will be third party, yup.
So eg we can have third party packages for schema formats: Swagger / API Blueprint / JSON Hyperschema, for docs templates driven by a coreapi.Document object, and for various hypermedia styles.

Using coreapi ensures that we're able to provide a common interface for all of those, so I don't have any great issues with pulling it in. If it's in core, then we're making the promise that it's an interface that is available to third-party devs, which should help drive folks making schema renderers / hypermedia renderers and docs renderers. Having said that, we could push for it to be third-party, if we wanted to, so I might be open to discussion.

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)
34 changes: 27 additions & 7 deletions rest_framework/routers.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@
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.compat import coreapi
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'])
Expand Down Expand Up @@ -252,6 +255,7 @@ def get_urls(self):
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))
Expand All @@ -268,7 +272,11 @@ class DefaultRouter(SimpleRouter):
include_format_suffixes = True
root_view_name = 'api-root'

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.
"""
Expand All @@ -277,10 +285,24 @@ def get_api_root_view(self):
for prefix, viewset, basename in self.registry:
api_root_dict[prefix] = list_name.format(basename=basename)

view_renderers = list(api_settings.DEFAULT_RENDERER_CLASSES)

if schema_urls and self.schema_title:
assert coreapi, '`coreapi` must be installed for schema support.'
view_renderers += [renderers.CoreJSONRenderer]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it make more sense to expect users to include the CoreJSONRenderer in their default renderers, and raise an error if it is missing from the defaults?

schema_generator = SchemaGenerator(patterns=schema_urls)

class APIRoot(views.APIView):
_ignore_model_permissions = True
renderer_classes = view_renderers

def get(self, request, *args, **kwargs):
if request.accepted_renderer.format == 'corejson':
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason why this couldn't be brought into the CoreJSONRenderer?

Copy link
Contributor Author

@lovelydinosaur lovelydinosaur Jun 15, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is less what to do with CoreJSONRenderer and more about making sure that we preserve the behavior of that view for anything that isn't a schema renderer.

(Also I'd consider that part still slightly in flux - clearly needs refinement)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

making sure that we preserve the behavior of that view for anything that isn't a schema renderer

Right, my main concern with that line (which I'm sure will probably change another few times before this is merged) is that it's hard-coding corejson as the only renderer with schema support.

Also I'd consider that part still slightly in flux - clearly needs refinement

Definitely understandable.

schema = schema_generator.get_schema(request)
if schema is None:
raise exceptions.PermissionDenied()
return Response(schema)

ret = OrderedDict()
namespace = request.resolver_match.namespace
for key, url_name in api_root_dict.items():
Expand All @@ -307,15 +329,13 @@ def get_urls(self):
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)

Expand Down
Loading