Migration to drf_spectacular (#4210)

main
Maria Khrustaleva 4 years ago committed by GitHub
parent dd4a78d8c9
commit d098e42c45
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -40,6 +40,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Different resources (tasks, projects) are not visible anymore for all CVAT instance users by default (<https://github.com/openvinotoolkit/cvat/pull/3788>)
- API versioning scheme: using accept header versioning instead of namespace versioning (<https://github.com/openvinotoolkit/cvat/pull/4239>)
- Replaced 'django_sendfile' with 'django_sendfile2' (<https://github.com/openvinotoolkit/cvat/pull/4267>)
- Use drf-spectacular instead of drf-yasg for swagger documentation (<https://github.com/openvinotoolkit/cvat/pull/4210>)
### Deprecated
- Job field "status" is not used in UI anymore, but it has not been removed from the database yet (<https://github.com/openvinotoolkit/cvat/pull/3788>)

@ -43,6 +43,10 @@ class StatusChoice(str, Enum):
def choices(cls):
return tuple((x.value, x.name) for x in cls)
@classmethod
def list(cls):
return list(map(lambda x: x.value, cls))
def __str__(self):
return self.value

@ -17,6 +17,8 @@ from cvat.apps.engine.cloud_provider import get_cloud_storage_instance, Credenti
from cvat.apps.engine.log import slogger
from cvat.apps.engine.utils import parse_specific_attributes
from drf_spectacular.utils import OpenApiExample, extend_schema_serializer
class BasicUserSerializer(serializers.ModelSerializer):
def validate(self, data):
if hasattr(self, 'initial_data'):
@ -849,7 +851,7 @@ class ManifestSerializer(serializers.ModelSerializer):
def to_representation(self, instance):
return instance.filename if instance else instance
class BaseCloudStorageSerializer(serializers.ModelSerializer):
class CloudStorageReadSerializer(serializers.ModelSerializer):
owner = BasicUserSerializer(required=False)
manifests = ManifestSerializer(many=True, default=[])
class Meta:
@ -857,7 +859,70 @@ class BaseCloudStorageSerializer(serializers.ModelSerializer):
exclude = ['credentials']
read_only_fields = ('created_date', 'updated_date', 'owner', 'organization')
class CloudStorageSerializer(serializers.ModelSerializer):
@extend_schema_serializer(
examples=[
OpenApiExample(
'Create AWS S3 cloud storage with credentials',
description='',
value={
'provider_type': models.CloudProviderChoice.AWS_S3,
'resource': 'somebucket',
'display_name': 'Bucket',
'credentials_type': models.CredentialsTypeChoice.KEY_SECRET_KEY_PAIR,
'specific_attributes': 'region=eu-central-1',
'description': 'Some description',
'manifests': [
'manifest.jsonl'
],
},
request_only=True,
),
OpenApiExample(
'Create AWS S3 cloud storage without credentials',
value={
'provider_type': models.CloudProviderChoice.AWS_S3,
'resource': 'somebucket',
'display_name': 'Bucket',
'credentials_type': models.CredentialsTypeChoice.ANONYMOUS_ACCESS,
'manifests': [
'manifest.jsonl'
],
},
request_only=True,
),
OpenApiExample(
'Create Azure cloud storage',
value={
'provider_type': models.CloudProviderChoice.AZURE_CONTAINER,
'resource': 'sonecontainer',
'display_name': 'Container',
'credentials_type': models.CredentialsTypeChoice.ACCOUNT_NAME_TOKEN_PAIR,
'account_name': 'someaccount',
'session_token': 'xxx',
'manifests': [
'manifest.jsonl'
],
},
request_only=True,
),
OpenApiExample(
'Create GCS',
value={
'provider_type': models.CloudProviderChoice.GOOGLE_CLOUD_STORAGE,
'resource': 'somebucket',
'display_name': 'Bucket',
'credentials_type': models.CredentialsTypeChoice.KEY_FILE_PATH,
'key_file': 'file',
'manifests': [
'manifest.jsonl'
],
},
request_only=True,
)
]
)
class CloudStorageWriteSerializer(serializers.ModelSerializer):
owner = BasicUserSerializer(required=False)
session_token = serializers.CharField(max_length=440, allow_blank=True, required=False)
key = serializers.CharField(max_length=20, allow_blank=True, required=False)

@ -6,42 +6,12 @@
from django.urls import path, include
from . import views
from rest_framework import routers
from rest_framework import permissions
from drf_yasg.views import get_schema_view
from drf_yasg import openapi
from django.views.generic import RedirectView
from django.conf import settings
from cvat.apps.restrictions.views import RestrictionsViewSet
from cvat.apps.iam.decorators import login_required
schema_view = get_schema_view(
openapi.Info(
title="CVAT REST API",
default_version='v1',
description="REST API for Computer Vision Annotation Tool (CVAT)",
terms_of_service="https://www.google.com/policies/terms/",
contact=openapi.Contact(email="nikita.manovich@intel.com"),
license=openapi.License(name="MIT License"),
),
public=True,
permission_classes=(permissions.IsAuthenticated,),
)
# drf-yasg component doesn't handle correctly URL_FORMAT_OVERRIDE and
# send requests with ?format=openapi suffix instead of ?scheme=openapi.
# We map the required parameter explicitly and add it into query arguments
# on the server side.
def wrap_swagger(view):
@login_required
def _map_format_to_schema(request, scheme=None):
if 'format' in request.GET:
request.GET = request.GET.copy()
format_alias = settings.REST_FRAMEWORK['URL_FORMAT_OVERRIDE']
request.GET[format_alias] = request.GET['format']
return view(request, format=scheme)
return _map_format_to_schema
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
router = routers.DefaultRouter(trailing_slash=False)
router.register('projects', views.ProjectViewSet)
@ -60,12 +30,9 @@ urlpatterns = [
query_string=True)),
# documentation for API
path('api/swagger<str:scheme>', wrap_swagger(
schema_view.without_ui(cache_timeout=0)), name='schema-json'),
path('api/swagger/', wrap_swagger(
schema_view.with_ui('swagger', cache_timeout=0)), name='schema-swagger-ui'),
path('api/docs/', wrap_swagger(
schema_view.with_ui('redoc', cache_timeout=0)), name='schema-redoc'),
path('api/schema/', SpectacularAPIView.as_view(api_version='2.0'), name='schema'),
path('api/swagger/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger'),
path('api/docs/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
# entry point for API
path('api/', include('cvat.apps.iam.urls')),

File diff suppressed because it is too large Load Diff

@ -2,3 +2,4 @@
#
# SPDX-License-Identifier: MIT
from .schema import *

@ -6,12 +6,18 @@ import coreapi
from rest_framework.filters import BaseFilterBackend
class OrganizationFilterBackend(BaseFilterBackend):
organization_slug = 'org'
organization_slug_description = 'Organization unique slug'
organization_id = 'org_id'
organization_id_description = 'Organization identifier'
def get_schema_fields(self, view):
return [
coreapi.Field(name='org', location='query', required=False,
type='string', description='Organization unique slug'),
coreapi.Field(name='org_id', location='query', required=False,
type='string', description='Organization identifier'),
# NOTE: in coreapi.Field 'type', 'description' and 'example' are now deprecated, in favor of 'schema'.
coreapi.Field(name=self.organization_slug, location='query', required=False,
type='string', description=self.organization_slug_description),
coreapi.Field(name=self.organization_id, location='query', required=False,
type='string', description=self.organization_id_description),
]
def filter_queryset(self, request, queryset, view):

@ -0,0 +1,44 @@
# Copyright (C) 2022 Intel Corporation
#
# SPDX-License-Identifier: MIT
from drf_spectacular.extensions import OpenApiFilterExtension, OpenApiAuthenticationExtension
from drf_spectacular.plumbing import build_parameter_type
from drf_spectacular.utils import OpenApiParameter
# https://drf-spectacular.readthedocs.io/en/latest/customization.html?highlight=OpenApiFilterExtension#step-5-extensions
class OrganizationFilterExtension(OpenApiFilterExtension):
"""Describe OrganizationFilterBackend filter"""
target_class = 'cvat.apps.iam.filters.OrganizationFilterBackend'
priority = 1
def get_schema_operation_parameters(self, auto_schema, *args, **kwargs):
"""Describe query parameters"""
return [
build_parameter_type(
name=self.target.organization_slug,
required=False,
location=OpenApiParameter.QUERY,
description=self.target.organization_slug_description,
schema={'type': 'string'},
),
build_parameter_type(
name=self.target.organization_id,
required=False,
location=OpenApiParameter.QUERY,
description=self.target.organization_id_description,
schema={'type': 'string'},
)
]
class SignatureAuthenticationScheme(OpenApiAuthenticationExtension):
target_class = 'cvat.apps.iam.authentication.SignatureAuthentication'
name = 'SignatureAuthentication' # name used in the schema
def get_security_definition(self, auto_schema):
return {
'type': 'apiKey',
'in': 'query',
'name': 'sign',
}

@ -4,7 +4,7 @@
from django.core.exceptions import BadRequest
from django.utils.functional import SimpleLazyObject
from rest_framework import views
from rest_framework import views, serializers
from rest_framework.exceptions import ValidationError
from django.conf import settings
from rest_framework.response import Response
@ -12,9 +12,9 @@ from rest_auth.registration.views import RegisterView
from allauth.account import app_settings as allauth_settings
from furl import furl
from django.utils.decorators import method_decorator
from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer, extend_schema_view
from .authentication import Signer
@ -79,25 +79,19 @@ class ContextMiddleware:
return self.get_response(request)
@method_decorator(name='post', decorator=swagger_auto_schema(
request_body=openapi.Schema(
type=openapi.TYPE_OBJECT,
required=[
'url'
],
properties={
'url': openapi.Schema(type=openapi.TYPE_STRING)
@extend_schema_view(post=extend_schema(
summary='This method signs URL for access to the server',
description='Signed URL contains a token which authenticates a user on the server.'
'Signed URL is valid during 30 seconds since signing.',
request=inline_serializer(
name='Signing',
fields={
'url': serializers.CharField(),
}
),
responses={'200': openapi.Response(description='text URL')}
))
responses={'200': OpenApiResponse(response=OpenApiTypes.STR, description='text URL')}, tags=['auth'], versions=['2.0']))
class SigningView(views.APIView):
"""
This method signs URL for access to the server.
Signed URL contains a token which authenticates a user on the server.
Signed URL is valid during 30 seconds since signing.
"""
def post(self, request):
url = request.data.get('url')
if not url:

@ -25,5 +25,5 @@ router.register('requests', views.RequestViewSet, basename='request')
# GET /api/lambda/requests/<int:rid> - get status of the request
# DEL /api/lambda/requests/<int:rid> - cancel a request (don't delete)
urlpatterns = [
path('api/lambda/', include((router.urls, 'cvat'), namespace='v1'))
path('api/lambda/', include(router.urls))
]

@ -17,6 +17,9 @@ from cvat.apps.engine.models import Task as TaskModel
from cvat.apps.engine.serializers import LabeledDataSerializer
from cvat.apps.engine.models import ShapeType, SourceType
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse, OpenApiParameter
from drf_spectacular.types import OpenApiTypes
class LambdaType(Enum):
DETECTOR = "detector"
INTERACTOR = "interactor"
@ -228,7 +231,6 @@ class LambdaFunction:
return base64.b64encode(image[0].getvalue()).decode('utf-8')
class LambdaQueue:
def _get_queue(self):
QUEUE_NAME = "low"
@ -286,7 +288,6 @@ class LambdaQueue:
return LambdaJob(job)
class LambdaJob:
def __init__(self, job):
self.job = job
@ -552,10 +553,19 @@ def return_response(success_code=status.HTTP_200_OK):
return func_wrapper
return wrap_response
@extend_schema_view(retrieve=extend_schema(
summary='Method returns the information about the function',
responses={
'200': OpenApiResponse(response=OpenApiTypes.OBJECT, description='Information about the function'),
},
tags=['lambda'], versions=['2.0']))
@extend_schema_view(list=extend_schema(
summary='Method returns a list of functions', tags=['lambda'], versions=['2.0']))
class FunctionViewSet(viewsets.ViewSet):
lookup_value_regex = '[a-zA-Z0-9_.-]+'
lookup_field = 'func_id'
iam_organization_field = None
serializer_class = None
@return_response()
def list(self, request):
@ -585,8 +595,24 @@ class FunctionViewSet(viewsets.ViewSet):
return lambda_func.invoke(db_task, request.data)
@extend_schema_view(retrieve=extend_schema(
summary='Method returns the status of the request',
parameters=[
# specify correct type
OpenApiParameter('id', location=OpenApiParameter.PATH, type=OpenApiTypes.INT,
description='Request id'),
],
tags=['lambda'], versions=['2.0']))
@extend_schema_view(list=extend_schema(
summary='Method returns a list of requests', tags=['lambda'], versions=['2.0']))
#TODO
@extend_schema_view(create=extend_schema(
summary='Method calls the function', tags=['lambda'], versions=['2.0']))
@extend_schema_view(delete=extend_schema(
summary='Method cancels the request', tags=['lambda'], versions=['2.0']))
class RequestViewSet(viewsets.ViewSet):
iam_organization_field = None
serializer_class = None
@return_response()
def list(self, request):

@ -10,4 +10,4 @@ router.register('organizations', OrganizationViewSet)
router.register('invitations', InvitationViewSet)
router.register('memberships', MembershipViewSet)
urlpatterns = router.urls
urlpatterns = router.urls

@ -7,6 +7,8 @@ from rest_framework.permissions import SAFE_METHODS
from django.utils.crypto import get_random_string
from django_filters import rest_framework as filters
from drf_spectacular.utils import OpenApiResponse, extend_schema, extend_schema_view
from cvat.apps.iam.permissions import (
InvitationPermission, MembershipPermission, OrganizationPermission)
from .models import Invitation, Membership, Organization
@ -16,6 +18,37 @@ from .serializers import (
MembershipReadSerializer, MembershipWriteSerializer,
OrganizationReadSerializer, OrganizationWriteSerializer)
@extend_schema_view(retrieve=extend_schema(
summary='Method returns details of an organization',
responses={
'200': OrganizationReadSerializer,
}, tags=['organizations'], versions=['2.0']))
@extend_schema_view(list=extend_schema(
summary='Method returns a paginated list of organizatins according to query parameters',
responses={
'200': OrganizationReadSerializer(many=True),
}, tags=['organizations'], versions=['2.0']))
@extend_schema_view(update=extend_schema(
summary='Method updates an organization by id',
responses={
'200': OrganizationWriteSerializer,
}, tags=['organizations'], versions=['2.0']))
@extend_schema_view(partial_update=extend_schema(
summary='Methods does a partial update of chosen fields in an organization',
responses={
'200': OrganizationWriteSerializer,
}, tags=['organizations'], versions=['2.0']))
@extend_schema_view(create=extend_schema(
summary='Method creates an organization',
responses={
'201': OrganizationWriteSerializer,
}, tags=['organizations'], versions=['2.0']))
@extend_schema_view(destroy=extend_schema(
summary='Method deletes an organization',
responses={
'204': OpenApiResponse(description='The organization has been deleted'),
}, tags=['organizations'], versions=['2.0']))
class OrganizationViewSet(viewsets.ModelViewSet):
queryset = Organization.objects.all()
ordering = ['-id']
@ -46,7 +79,31 @@ class MembershipFilter(filters.FilterSet):
class Meta:
model = Membership
fields = ("user", )
@extend_schema_view(retrieve=extend_schema(
summary='Method returns details of a membership',
responses={
'200': MembershipReadSerializer,
}, tags=['memberships'], versions=['2.0']))
@extend_schema_view(list=extend_schema(
summary='Method returns a paginated list of memberships according to query parameters',
responses={
'200': MembershipReadSerializer(many=True),
}, tags=['memberships'], versions=['2.0']))
@extend_schema_view(update=extend_schema(
summary='Method updates a membership by id',
responses={
'200': MembershipWriteSerializer,
}, tags=['memberships'], versions=['2.0']))
@extend_schema_view(partial_update=extend_schema(
summary='Methods does a partial update of chosen fields in a membership',
responses={
'200': MembershipWriteSerializer,
}, tags=['memberships'], versions=['2.0']))
@extend_schema_view(destroy=extend_schema(
summary='Method deletes a membership',
responses={
'204': OpenApiResponse(description='The membership has been deleted'),
}, tags=['memberships'], versions=['2.0']))
class MembershipViewSet(mixins.RetrieveModelMixin, mixins.DestroyModelMixin,
mixins.ListModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet):
queryset = Membership.objects.all()
@ -66,6 +123,37 @@ class MembershipViewSet(mixins.RetrieveModelMixin, mixins.DestroyModelMixin,
permission = MembershipPermission(self.request, self)
return permission.filter(queryset)
# TODO
@extend_schema_view(retrieve=extend_schema(
summary='Method returns details of an invitation',
responses={
'200': InvitationReadSerializer,
}, tags=['invitations'], versions=['2.0']))
@extend_schema_view(list=extend_schema(
summary='Method returns a paginated list of invitations according to query parameters',
responses={
'200': InvitationReadSerializer(many=True),
}, tags=['invitations'], versions=['2.0']))
@extend_schema_view(update=extend_schema(
summary='Method updates an invitation by id',
responses={
'200': InvitationWriteSerializer,
}, tags=['invitations'], versions=['2.0']))
@extend_schema_view(partial_update=extend_schema(
summary='Methods does a partial update of chosen fields in an invitation',
responses={
'200': InvitationWriteSerializer,
}, tags=['invitations'], versions=['2.0']))
@extend_schema_view(create=extend_schema(
summary='Method creates an invitation',
responses={
'201': InvitationWriteSerializer,
}, tags=['invitations'], versions=['2.0']))
@extend_schema_view(destroy=extend_schema(
summary='Method deletes an invitation',
responses={
'204': OpenApiResponse(description='The invitation has been deleted'),
}, tags=['invitations'], versions=['2.0']))
class InvitationViewSet(viewsets.ModelViewSet):
queryset = Invitation.objects.all()
ordering = ['-created_date']

@ -8,7 +8,8 @@ from rest_framework.decorators import action
from rest_framework import viewsets
from rest_framework.permissions import AllowAny
from rest_framework.renderers import TemplateHTMLRenderer
from drf_yasg.utils import swagger_auto_schema
from drf_spectacular.utils import OpenApiResponse, extend_schema
from cvat.apps.restrictions.serializers import UserAgreementSerializer
@ -24,10 +25,9 @@ class RestrictionsViewSet(viewsets.ViewSet):
pass
@staticmethod
@swagger_auto_schema(
method='get',
operation_summary='Method provides user agreements that the user must accept to register',
responses={'200': UserAgreementSerializer})
@extend_schema(summary='Method provides user agreements that the user must accept to register',
responses={'200': UserAgreementSerializer},
tags=['restrictions'], versions=['2.0'])
@action(detail=False, methods=['GET'], serializer_class=UserAgreementSerializer, url_path='user-agreements')
def user_agreements(request):
user_agreements = settings.RESTRICTIONS['user_agreements']
@ -36,6 +36,9 @@ class RestrictionsViewSet(viewsets.ViewSet):
return Response(data=serializer.data)
@staticmethod
@extend_schema(summary='Method provides CVAT terms of use',
responses={'200': OpenApiResponse(description='CVAT terms of use')},
tags=['restrictions'], versions=['2.0'])
@action(detail=False, methods=['GET'], renderer_classes=(TemplateHTMLRenderer,),
url_path='terms-of-use')
def terms_of_use(request):

@ -28,7 +28,7 @@ django-filter==2.4.0
Markdown==3.2.2
djangorestframework==3.12.4
Pygments==2.7.4
drf-yasg==1.20.0
drf-spectacular==0.21.2
Shapely==1.7.1
pdf2image==1.14.0
django-rest-auth[with_social]==0.9.5

@ -112,7 +112,7 @@ INSTALLED_APPS = [
'rest_framework',
'rest_framework.authtoken',
'django_filters',
'drf_yasg',
'drf_spectacular',
'rest_auth',
'django.contrib.sites',
'allauth',
@ -177,6 +177,7 @@ REST_FRAMEWORK = {
'anon': '100/minute',
},
'DEFAULT_METADATA_CLASS': 'rest_framework.metadata.SimpleMetadata',
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}
REST_AUTH_REGISTER_SERIALIZERS = {
@ -492,7 +493,44 @@ TUS_DEFAULT_CHUNK_SIZE = 104857600 # 100 mb
# How django uses X-Forwarded-Proto - https://docs.djangoproject.com/en/2.2/ref/settings/#secure-proxy-ssl-header
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# Django-sendfile requires to set SENDFILE_ROOT
# https://github.com/moggers87/django-sendfile2
SENDFILE_ROOT = BASE_DIR
SPECTACULAR_SETTINGS = {
'TITLE': 'CVAT REST API',
'DESCRIPTION': 'REST API for Computer Vision Annotation Tool (CVAT)',
# Statically set schema version. May also be an empty string. When used together with
# view versioning, will become '0.0.0 (v2)' for 'v2' versioned requests.
# Set VERSION to None if only the request version should be rendered.
'VERSION': None,
'CONTACT': {
'name': 'Nikita Manovich',
'url': 'https://github.com/nmanovic',
'email': 'nikita.manovich@intel.com',
},
'LICENSE': {
'name': 'MIT License',
'url': 'https://en.wikipedia.org/wiki/MIT_License',
},
'SERVE_PUBLIC': True,
'SCHEMA_COERCE_PATH_PK_SUFFIX': True,
'SCHEMA_PATH_PREFIX': '/api',
'SCHEMA_PATH_PREFIX_TRIM': False,
'SERVE_PERMISSIONS': ['rest_framework.permissions.IsAuthenticated'],
# https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/
'SWAGGER_UI_SETTINGS': {
'deepLinking': True,
'displayOperationId': True,
'displayRequestDuration': True,
'filter': True,
'showExtensions': True,
},
'TOS': 'https://www.google.com/policies/terms/',
'EXTERNAL_DOCS': {
'description': 'CVAT documentation',
'url': 'https://openvinotoolkit.github.io/cvat/docs/',
},
# OTHER SETTINGS
# https://drf-spectacular.readthedocs.io/en/latest/settings.html
}

Loading…
Cancel
Save