Changeset View
Changeset View
Standalone View
Standalone View
swh/web/api/apidoc.py
# Copyright (C) 2015-2019 The Software Heritage developers | # Copyright (C) 2015-2019 The Software Heritage developers | ||||
# See the AUTHORS file at the top-level directory of this distribution | # See the AUTHORS file at the top-level directory of this distribution | ||||
# License: GNU Affero General Public License version 3, or any later version | # License: GNU Affero General Public License version 3, or any later version | ||||
# See top-level LICENSE file for more information | # See top-level LICENSE file for more information | ||||
import docutils.nodes | |||||
import docutils.parsers.rst | |||||
import docutils.utils | |||||
import functools | import functools | ||||
from functools import wraps | from functools import wraps | ||||
import os | import os | ||||
import re | import re | ||||
import textwrap | import textwrap | ||||
from typing import List | |||||
import docutils.nodes | |||||
import docutils.parsers.rst | |||||
import docutils.utils | |||||
from rest_framework.decorators import api_view | from rest_framework.decorators import api_view | ||||
import sentry_sdk | import sentry_sdk | ||||
from swh.web.common.utils import parse_rst | from swh.web.common.utils import parse_rst | ||||
from swh.web.api.apiurls import APIUrls | from swh.web.api.apiurls import APIUrls | ||||
from swh.web.api.apiresponse import make_api_response, error_response | from swh.web.api.apiresponse import make_api_response, error_response | ||||
▲ Show 20 Lines • Show All 226 Lines • ▼ Show 20 Lines | |||||
class APIDocException(Exception): | class APIDocException(Exception): | ||||
""" | """ | ||||
Custom exception to signal errors in the use of the APIDoc decorators | Custom exception to signal errors in the use of the APIDoc decorators | ||||
""" | """ | ||||
def api_doc(route, noargs=False, need_params=False, tags=[], | def api_doc(route: str, noargs: bool = False, need_params: bool = False, | ||||
handle_response=False, api_version='1'): | tags: List[str] = [], handle_response: bool = False, | ||||
api_version: str = '1', post_only: bool = False): | |||||
""" | """ | ||||
Decorate an API function to register it in the API doc route index | Decorator for an API endpoint implementation used to generate a dedicated | ||||
and create the corresponding DRF route. | view displaying its HTML documentation. | ||||
The documentation will be generated from the endpoint docstring based on | |||||
sphinxcontrib-httpdomain format. | |||||
Args: | Args: | ||||
route (str): documentation page's route | route: documentation page's route | ||||
noargs (boolean): set to True if the route has no arguments, and its | noargs: set to True if the route has no arguments, and its | ||||
result should be displayed anytime its documentation | result should be displayed anytime its documentation | ||||
is requested. Default to False | is requested. Default to False | ||||
need_params (boolean): specify the route requires query parameters | need_params: specify the route requires query parameters | ||||
otherwise errors will occur. It enables to avoid displaying the | otherwise errors will occur. It enables to avoid displaying the | ||||
invalid response in its HTML documentation. Default to False. | invalid response in its HTML documentation. Default to False. | ||||
tags (list): Further information on api endpoints. Two values are | tags: Further information on api endpoints. Two values are | ||||
possibly expected: | possibly expected: | ||||
* hidden: remove the entry points from the listing | * hidden: remove the entry points from the listing | ||||
* upcoming: display the entry point but it is not followable | * upcoming: display the entry point but it is not followable | ||||
handle_response (boolean): indicate if the decorated function takes | handle_response: indicate if the decorated function takes | ||||
care of creating the HTTP response or delegates that task to the | care of creating the HTTP response or delegates that task to the | ||||
apiresponse module | apiresponse module | ||||
api_version (str): api version string | api_version: api version string | ||||
post_only: if the decorated route only accepts POST requests, set this | |||||
parameter to True in order to generate a dedicated doc view | |||||
accepting GET requests | |||||
""" | """ | ||||
urlpattern = '^' + api_version + route + '$' | |||||
tags = set(tags) | tags_set = set(tags) | ||||
# @api_doc() Decorator call | # @api_doc() Decorator call | ||||
def decorator(f): | def decorator(f): | ||||
# If the route is not hidden, add it to the index | # If the route is not hidden, add it to the index | ||||
if 'hidden' not in tags: | if 'hidden' not in tags_set: | ||||
doc_data = get_doc_data(f, route, noargs) | doc_data = get_doc_data(f, route) | ||||
doc_desc = doc_data['description'] | doc_desc = doc_data['description'] | ||||
first_dot_pos = doc_desc.find('.') | first_dot_pos = doc_desc.find('.') | ||||
APIUrls.add_route(route, doc_desc[:first_dot_pos+1], | APIUrls.add_route(route, doc_desc[:first_dot_pos+1], | ||||
tags=tags) | api_version=api_version, | ||||
doc_route=post_only, tags=tags_set) | |||||
# If the decorated route has arguments, we create a specific | # If the decorated route has arguments or if the route does not accept | ||||
# documentation view | # GET requests, we create a specific documentation view | ||||
if not noargs: | if not noargs or post_only: | ||||
@api_view(['GET', 'HEAD']) | @api_view(['GET', 'HEAD']) | ||||
@wraps(f) | @wraps(f) | ||||
def doc_view(request): | def doc_view(request): | ||||
doc_data = get_doc_data(f, route, noargs) | doc_data = get_doc_data(f, route) | ||||
return make_api_response(request, None, doc_data) | return make_api_response(request, None, doc_data) | ||||
view_name = 'api-%s-%s' % \ | route_name = route[1:-1].replace('/', '-') | ||||
(api_version, route[1:-1].replace('/', '-')) | urlpattern = '^' + api_version + route | ||||
# if the decorated route does not accept GET requests, we need to | |||||
# register the doc view with a different url and name | |||||
if post_only: | |||||
route_name += '-doc' | |||||
urlpattern += 'doc/$' | |||||
else: | |||||
urlpattern += '$' | |||||
view_name = 'api-%s-%s' % (api_version, route_name) | |||||
APIUrls.add_url_pattern(urlpattern, doc_view, view_name) | APIUrls.add_url_pattern(urlpattern, doc_view, view_name) | ||||
@wraps(f) | @wraps(f) | ||||
def documented_view(request, **kwargs): | def documented_view(request, **kwargs): | ||||
doc_data = get_doc_data(f, route, noargs) | doc_data = get_doc_data(f, route) | ||||
try: | try: | ||||
response = f(request, **kwargs) | response = f(request, **kwargs) | ||||
except Exception as exc: | except Exception as exc: | ||||
sentry_sdk.capture_exception(exc) | sentry_sdk.capture_exception(exc) | ||||
if request.accepted_media_type == 'text/html' and \ | if request.accepted_media_type == 'text/html' and \ | ||||
need_params and not request.query_params: | need_params and not request.query_params: | ||||
response = None | response = None | ||||
else: | else: | ||||
return error_response(request, exc, doc_data) | return error_response(request, exc, doc_data) | ||||
if handle_response: | if handle_response: | ||||
return response | return response | ||||
else: | else: | ||||
return make_api_response(request, response, doc_data) | return make_api_response(request, response, doc_data) | ||||
return documented_view | return documented_view | ||||
return decorator | return decorator | ||||
@functools.lru_cache(maxsize=32) | @functools.lru_cache(maxsize=32) | ||||
def get_doc_data(f, route, noargs): | def get_doc_data(f, route): | ||||
""" | """ | ||||
Build documentation data for the decorated api endpoint function | Build documentation data for the decorated api endpoint function | ||||
""" | """ | ||||
data = { | data = { | ||||
'description': '', | 'description': '', | ||||
'response_data': None, | 'response_data': None, | ||||
'urls': [], | 'urls': [], | ||||
'args': [], | 'args': [], | ||||
'params': [], | 'params': [], | ||||
'resheaders': [], | 'resheaders': [], | ||||
'reqheaders': [], | 'reqheaders': [], | ||||
'return_type': '', | 'return_type': '', | ||||
'returns': [], | 'returns': [], | ||||
'status_codes': [], | 'status_codes': [], | ||||
'examples': [], | 'examples': [], | ||||
'route': route, | 'route': route, | ||||
'noargs': noargs | |||||
} | } | ||||
if not f.__doc__: | if not f.__doc__: | ||||
raise APIDocException('apidoc: expected a docstring' | raise APIDocException('apidoc: expected a docstring' | ||||
' for function %s' | ' for function %s' | ||||
% (f.__name__,)) | % (f.__name__,)) | ||||
# use raw docstring as endpoint documentation if sphinx | # use raw docstring as endpoint documentation if sphinx | ||||
Show All 40 Lines |