Changeset View
Changeset View
Standalone View
Standalone View
swh/web/api/apiurls.py
# Copyright (C) 2017-2019 The Software Heritage developers | # Copyright (C) 2017-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 functools | import functools | ||||
from typing import Dict | from typing import Dict, List, Optional | ||||
from django.http import HttpResponse | |||||
from rest_framework.decorators import api_view | from rest_framework.decorators import api_view | ||||
from swh.web.api import throttling | from swh.web.api import throttling | ||||
from swh.web.api.apiresponse import make_api_response | |||||
from swh.web.common.urlsindex import UrlsIndex | from swh.web.common.urlsindex import UrlsIndex | ||||
class APIUrls(UrlsIndex): | class APIUrls(UrlsIndex): | ||||
""" | """ | ||||
Class to manage API documentation URLs. | Class to manage API documentation URLs. | ||||
- Indexes all routes documented using apidoc's decorators. | - Indexes all routes documented using apidoc's decorators. | ||||
- Tracks endpoint/request processing method relationships for use in | - Tracks endpoint/request processing method relationships for use in | ||||
generating related urls in API documentation | generating related urls in API documentation | ||||
""" | """ | ||||
_apidoc_routes = {} # type: Dict[str, Dict[str, str]] | _apidoc_routes = {} # type: Dict[str, Dict[str, str]] | ||||
scope = "api" | scope = "api" | ||||
@classmethod | @classmethod | ||||
def get_app_endpoints(cls): | def get_app_endpoints(cls) -> Dict[str, Dict[str, str]]: | ||||
return cls._apidoc_routes | return cls._apidoc_routes | ||||
@classmethod | @classmethod | ||||
def add_doc_route(cls, route, docstring, noargs=False, api_version="1", **kwargs): | def add_doc_route( | ||||
cls, | |||||
route: str, | |||||
docstring: str, | |||||
noargs: bool = False, | |||||
api_version: str = "1", | |||||
**kwargs, | |||||
) -> None: | |||||
""" | """ | ||||
Add a route to the self-documenting API reference | Add a route to the self-documenting API reference | ||||
""" | """ | ||||
route_name = route[1:-1].replace("/", "-") | route_name = route[1:-1].replace("/", "-") | ||||
if not noargs: | if not noargs: | ||||
route_name = "%s-doc" % route_name | route_name = "%s-doc" % route_name | ||||
route_view_name = "api-%s-%s" % (api_version, route_name) | route_view_name = "api-%s-%s" % (api_version, route_name) | ||||
if route not in cls._apidoc_routes: | if route not in cls._apidoc_routes: | ||||
d = { | d = { | ||||
"docstring": docstring, | "docstring": docstring, | ||||
"route": "/api/%s%s" % (api_version, route), | "route": "/api/%s%s" % (api_version, route), | ||||
"route_view_name": route_view_name, | "route_view_name": route_view_name, | ||||
} | } | ||||
for k, v in kwargs.items(): | for k, v in kwargs.items(): | ||||
d[k] = v | d[k] = v | ||||
cls._apidoc_routes[route] = d | cls._apidoc_routes[route] = d | ||||
def api_route( | def api_route( | ||||
url_pattern=None, | url_pattern: str, | ||||
view_name=None, | view_name: Optional[str] = None, | ||||
methods=["GET", "HEAD", "OPTIONS"], | methods: List[str] = ["GET", "HEAD", "OPTIONS"], | ||||
throttle_scope="swh_api", | throttle_scope: str = "swh_api", | ||||
api_version="1", | api_version: str = "1", | ||||
checksum_args=None, | checksum_args: Optional[List[str]] = None, | ||||
): | ): | ||||
""" | """ | ||||
Decorator to ease the registration of an API endpoint | Decorator to ease the registration of an API endpoint | ||||
using the Django REST Framework. | using the Django REST Framework. | ||||
Args: | Args: | ||||
url_pattern: the url pattern used by DRF to identify the API route | url_pattern: the url pattern used by DRF to identify the API route | ||||
view_name: the name of the API view associated to the route used to | view_name: the name of the API view associated to the route used to | ||||
reverse the url | reverse the url | ||||
methods: array of HTTP methods supported by the API route | methods: array of HTTP methods supported by the API route | ||||
throttle_scope: Named scope for rate limiting | |||||
api_version: web API version | |||||
checksum_args: list of view argument names holding checksum values | |||||
""" | """ | ||||
url_pattern = "^" + api_version + url_pattern + "$" | url_pattern = "^" + api_version + url_pattern + "$" | ||||
def decorator(f): | def decorator(f): | ||||
# create a DRF view from the wrapped function | # create a DRF view from the wrapped function | ||||
@api_view(methods) | @api_view(methods) | ||||
@throttling.throttle_scope(throttle_scope) | @throttling.throttle_scope(throttle_scope) | ||||
@functools.wraps(f) | @functools.wraps(f) | ||||
def api_view_f(*args, **kwargs): | def api_view_f(request, **kwargs): | ||||
return f(*args, **kwargs) | response = f(request, **kwargs) | ||||
doc_data = None | |||||
# check if response has been forwarded by api_doc decorator | |||||
if isinstance(response, dict) and "doc_data" in response: | |||||
doc_data = response["doc_data"] | |||||
response = response["data"] | |||||
# check if HTTP response needs to be created | |||||
if not isinstance(response, HttpResponse): | |||||
return make_api_response(request, data=response, doc_data=doc_data) | |||||
else: | |||||
return response | |||||
# small hacks for correctly generating API endpoints index doc | # small hacks for correctly generating API endpoints index doc | ||||
api_view_f.__name__ = f.__name__ | api_view_f.__name__ = f.__name__ | ||||
api_view_f.http_method_names = methods | api_view_f.http_method_names = methods | ||||
# register the route and its view in the endpoints index | # register the route and its view in the endpoints index | ||||
APIUrls.add_url_pattern(url_pattern, api_view_f, view_name) | APIUrls.add_url_pattern(url_pattern, api_view_f, view_name) | ||||
if checksum_args: | if checksum_args: | ||||
APIUrls.add_redirect_for_checksum_args( | APIUrls.add_redirect_for_checksum_args( | ||||
view_name, [url_pattern], checksum_args | view_name, [url_pattern], checksum_args | ||||
) | ) | ||||
return f | return f | ||||
return decorator | return decorator |