# Copyright (C) 2017-2024 The Software Heritage developers
# See the AUTHORS file at the top-level directory of this distribution
# License: GNU Affero General Public License version 3, or any later version
# See top-level LICENSE file for more information
import functools
from typing import Dict, List, Literal, Optional
from django.http.response import HttpResponseBase
from rest_framework.decorators import api_view
from swh.web.api import throttling
from swh.web.api.apiresponse import make_api_response
from swh.web.utils.urlsindex import UrlsIndex
CategoryId = Literal[
"Archive",
"Batch download",
"Metadata",
"Request archival",
"Miscellaneous",
"test",
"External IDentifiers",
"Provenance",
]
[docs]
class APIUrls(UrlsIndex):
"""Class to manage API URLs and endpoint documentation URLs."""
apidoc_routes: Dict[str, Dict[str, str]] = {}
[docs]
def get_app_endpoints(self) -> Dict[str, Dict[str, str]]:
return self.apidoc_routes
[docs]
def add_doc_route(
self,
route: str,
category: CategoryId,
docstring: str,
noargs: bool = False,
api_version: str = "1",
**kwargs,
) -> None:
"""
Add a route to the self-documenting API reference
"""
route_name = route[1:-1].replace("/", "-")
if not noargs:
route_name = "%s-doc" % route_name
route_view_name = "api-%s-%s" % (api_version, route_name)
if route not in self.apidoc_routes:
d = {
"category": category,
"docstring": docstring,
"route": "/api/%s%s" % (api_version, route),
"route_view_name": route_view_name,
}
for k, v in kwargs.items():
d[k] = v
self.apidoc_routes[route] = d
api_urls = APIUrls()
[docs]
def api_route(
url_pattern: str,
view_name: str,
methods: List[str] = ["GET", "HEAD", "OPTIONS"],
throttle_scope: str = "swh_api",
api_version: str = "1",
checksum_args: Optional[List[str]] = None,
never_cache: bool = False,
api_urls: APIUrls = api_urls,
):
"""
Decorator to ease the registration of an API endpoint
using the Django REST Framework.
Args:
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
reverse the url
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
never_cache: define if api response must be cached
"""
url_pattern = "api/" + api_version + url_pattern
def decorator(f):
# create a DRF view from the wrapped function
@api_view(methods)
@throttling.throttle_scope(throttle_scope)
@functools.wraps(f)
def api_view_f(request, **kwargs):
# never_cache will be handled in apiresponse module
request.never_cache = never_cache
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, HttpResponseBase):
api_response = make_api_response(
request, data=response, doc_data=doc_data
)
else:
api_response = response
return api_response
# small hacks for correctly generating API endpoints index doc
api_view_f.__name__ = f.__name__
api_view_f.http_method_names = methods
# register the route and its view in the endpoints index
api_urls.add_url_pattern(url_pattern, api_view_f, view_name)
if checksum_args:
api_urls.add_redirect_for_checksum_args(
view_name, [url_pattern], checksum_args
)
return f
return decorator