From 5e03f7c759983c168331c99396163fed60fe533f Mon Sep 17 00:00:00 2001 From: Corey Shields Date: Sat, 9 Jan 2021 20:39:09 -0500 Subject: [PATCH] Epic API (doc) changes for SatNOGS DB I've decided to change things up in API schema and doc generation. Work is not quite complete but its enough for testing in dev and feedback. Major changes: * Renaming of api.view classes to match ViewSet inheritance (minor annoyance) * Introduce drf-spectacular for schema generation and doc UI via swagger-ui * lots of doc changes for the API to provide a good experience with the above. New schema generation should work seamlessly in gitlab ci, as well as via /api/schema dynamically. The new swagger ui view is available via /api/schema/docs/ Signed-off-by: Corey Shields --- .gitlab-ci.yml | 9 +-- .yapfignore | 1 + db/api/generators.py | 46 ----------- db/api/serializers.py | 179 +++++++++++++++++++++++++++++++++++++++++- db/api/urls.py | 12 +-- db/api/views.py | 133 +++++++++++++++++++++++++++---- db/settings.py | 126 ++++++++++++++++++++++++++++- db/urls.py | 17 ++-- docs/api.rst | 2 +- package-lock.json | 5 ++ package.json | 7 +- requirements.txt | 14 ++-- setup.cfg | 1 + 13 files changed, 458 insertions(+), 94 deletions(-) delete mode 100644 db/api/generators.py diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3348347..6e38cef 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -26,11 +26,10 @@ schema: script: - pip install --no-cache-dir --no-deps -r "requirements.txt" --force-reinstall . - >- - ./manage.py generateschema - --title "SatNOGS DB" - --description "SatNOGS DB is a transmitter suggestions and crowd-sourcing app." - --generator_class "db.api.generators.SchemaGenerator" - > satnogs-db-api-client/api-schema.yml + ./manage.py spectacular + --file satnogs-db-api-client/api-schema.yml + --validate + --fail-on-warn artifacts: expire_in: 1 week when: always diff --git a/.yapfignore b/.yapfignore index da81543..43b9bbc 100644 --- a/.yapfignore +++ b/.yapfignore @@ -6,3 +6,4 @@ satnogs-db-api-client build docs versioneer.py +db/settings.py diff --git a/db/api/generators.py b/db/api/generators.py deleted file mode 100644 index dda14ee..0000000 --- a/db/api/generators.py +++ /dev/null @@ -1,46 +0,0 @@ -""" -NOTE this is a patch to add missing functionality from DRF's openapi implementation -and should be revisited periodically as new functionality is implemented upstream. -refer to https://github.com/encode/django-rest-framework/pull/7516 -""" - -from rest_framework.schemas.openapi import SchemaGenerator as OpenAPISchemaGenerator - - -class SchemaGenerator(OpenAPISchemaGenerator): - """ - Returns an extended schema that includes some missing fields from the - upstream OpenAPI implementation - """ - def get_schema(self, request=None, public=False): - schema = super().get_schema(request, public) - - schema['info']['version'] = '1.0' - - # temporarily add servers until the following is fixed - # https://github.com/encode/django-rest-framework/issues/7631 - schema['servers'] = [ - { - 'url': 'http://localhost:8000', - 'description': 'local dev' - }, { - 'url': 'https://db.satnogs.org', - 'description': 'production' - } - ] - - # temporarily add securitySchemes until implemented upstream - if 'securitySchemes' not in schema['components']: - schema['components']['securitySchemes'] = { - 'ApiKeyAuth': { - 'type': 'apiKey', - 'in': 'header', - 'name': 'Authorization' - } - } - - # temporarily add default security object at top-level - if 'security' not in schema: - schema['security'] = [{'ApiKeyAuth': []}] - - return schema diff --git a/db/api/serializers.py b/db/api/serializers.py index 5067423..24be20b 100644 --- a/db/api/serializers.py +++ b/db/api/serializers.py @@ -2,12 +2,31 @@ # pylint: disable=R0201 import h5py +from django.utils.datastructures import MultiValueDictKeyError +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import OpenApiExample, extend_schema_field, extend_schema_serializer from rest_framework import serializers from db.base.models import TRANSMITTER_STATUS, Artifact, DemodData, LatestTleSet, Mode, \ Satellite, Telemetry, Transmitter +@extend_schema_serializer( + examples=[ + OpenApiExample( + 'Mode Example 1', + summary='Example: list all modes', + description='This is a truncated example response for listing all RF Mode entries', + value=[ + { + 'id': 49, + 'name': 'AFSK' + }, + ], + response_only=True, # signal that example only applies to responses + ), + ] +) class ModeSerializer(serializers.ModelSerializer): """SatNOGS DB Mode API Serializer""" class Meta: @@ -22,6 +41,32 @@ class SatTelemetrySerializer(serializers.ModelSerializer): fields = ['decoder'] +@extend_schema_serializer( + examples=[ + OpenApiExample( + 'Satellite Example 1', + summary='Example: retrieving ISS', + description='This is an example response for retrieving the ISS entry, NORAD ID 25544', + value={ + 'norad_cat_id': 25544, + 'name': 'ISS', + 'names': 'ZARYA', + 'image': 'https://db-satnogs.freetls.fastly.net/media/satellites/ISS.jpg', + 'status': 'alive', + 'decayed': None, + 'launched': '1998-11-20T00:00:00Z', + 'deployed': '1998-11-20T00:00:00Z', + 'website': 'https://www.nasa.gov/mission_pages/station/main/index.html', + 'operator': 'None', + 'countries': 'RU,US', + 'telemetries': [{ + 'decoder': 'iss' + }] + }, + response_only=True, # signal that example only applies to responses + ), + ] +) class SatelliteSerializer(serializers.ModelSerializer): """SatNOGS DB Satellite API Serializer""" @@ -36,15 +81,50 @@ class SatelliteSerializer(serializers.ModelSerializer): 'website', 'operator', 'countries', 'telemetries' ) + @extend_schema_field(OpenApiTypes.STR) def get_operator(self, obj): """Returns operator text""" return str(obj.operator) + @extend_schema_field(OpenApiTypes.STR) def get_countries(self, obj): """Returns countires""" return ','.join(map(str, obj.countries)) +@extend_schema_serializer( + examples=[ + OpenApiExample( + 'Transmitter Example 1', + summary='Example: Transmitter API response', + value={ + 'uuid': 'eozSf5mKyzNxoascs8V4bV', + 'description': 'Mode V/U FM - Voice Repeater', + 'alive': True, + 'type': 'Transceiver', + 'uplink_low': 145990000, + 'uplink_high': None, + 'uplink_drift': None, + 'downlink_low': 437800000, + 'downlink_high': None, + 'downlink_drift': None, + 'mode': 'FM', + 'mode_id': 1, + 'uplink_mode': 'FM', + 'invert': False, + 'baud': None, + 'norad_cat_id': 25544, + 'status': 'active', + 'updated': '2020-09-03T13:14:41.552071Z', + 'citation': 'https://www.ariss.org/press-releases/september-2-2020', + 'service': 'Amateur', + 'coordination': '', + 'coordination_url': '' + }, + response_only=True, # signal that example only applies to responses + ), + ] +) class TransmitterSerializer(serializers.ModelSerializer): """SatNOGS DB Transmitter API Serializer""" norad_cat_id = serializers.SerializerMethodField() @@ -64,10 +144,12 @@ class TransmitterSerializer(serializers.ModelSerializer): ) # Keeping alive field for compatibility issues + @extend_schema_field(OpenApiTypes.BOOL) def get_alive(self, obj): """Returns transmitter status""" return obj.status == TRANSMITTER_STATUS[0] + @extend_schema_field(OpenApiTypes.INT) def get_mode_id(self, obj): """Returns downlink mode id""" try: @@ -75,6 +157,7 @@ class TransmitterSerializer(serializers.ModelSerializer): except AttributeError: # rare chance that this happens in prod return None + @extend_schema_field(OpenApiTypes.INT) def get_mode(self, obj): """Returns downlink mode name""" try: @@ -82,6 +165,7 @@ class TransmitterSerializer(serializers.ModelSerializer): except AttributeError: return None + @extend_schema_field(OpenApiTypes.INT) def get_uplink_mode(self, obj): """Returns uplink mode name""" try: @@ -89,6 +173,7 @@ class TransmitterSerializer(serializers.ModelSerializer): except AttributeError: return None + @extend_schema_field(OpenApiTypes.INT64) def get_norad_cat_id(self, obj): """Returns Satellite NORAD ID""" try: @@ -97,6 +182,23 @@ class TransmitterSerializer(serializers.ModelSerializer): return None +@extend_schema_serializer( + examples=[ + OpenApiExample( + 'TLE Example 1', + summary='Example: TLE API response', + value={ + 'tle0': '0 ISS (ZARYA)', + 'tle1': '1 25544U 98067A 21009.90234038 .00001675 00000-0 38183-4 0 9997', + 'tle2': '2 25544 51.6464 45.6388 0000512 205.3232 213.2158 15.49275327264062', + 'tle_source': 'undisclosed', + 'norad_cat_id': 25544, + 'updated': '2021-01-09T22:46:37.781923+0000' + }, + response_only=True, # signal that example only applies to responses + ), + ] +) class LatestTleSetSerializer(serializers.ModelSerializer): """SatNOGS DB LatestTleSet API Serializer""" @@ -111,31 +213,65 @@ class LatestTleSetSerializer(serializers.ModelSerializer): model = LatestTleSet fields = ('tle0', 'tle1', 'tle2', 'tle_source', 'norad_cat_id', 'updated') + @extend_schema_field(OpenApiTypes.INT64) def get_norad_cat_id(self, obj): """Returns Satellite NORAD ID""" return obj.satellite.norad_cat_id + @extend_schema_field(OpenApiTypes.STR) def get_tle0(self, obj): """Returns TLE line 0""" return obj.tle0 + @extend_schema_field(OpenApiTypes.STR) def get_tle1(self, obj): """Returns TLE line 1""" return obj.tle1 + @extend_schema_field(OpenApiTypes.STR) def get_tle2(self, obj): """Returns TLE line 2""" return obj.tle2 + @extend_schema_field(OpenApiTypes.STR) def get_tle_source(self, obj): """Returns TLE source""" return obj.tle_source + @extend_schema_field(OpenApiTypes.DATETIME) def get_updated(self, obj): """Returns TLE updated datetime""" return obj.updated.strftime('%Y-%m-%dT%H:%M:%S.%f%z') +@extend_schema_serializer( + exclude_fields=('app_source', 'observer', 'timestamp'), + examples=[ + OpenApiExample( + 'Telemetry Example 1', + summary='Example: retrieving a single Telemetry frame', + description='This is an example response for retrieving a single data frame', + value={ + 'norad_cat_id': 40379, + 'transmitter': None, + 'app_source': 'network', + 'schema': None, + 'decoded': 'influxdb', + 'frame': + '968870A6A0A66086A240404040E103F0ABCD0000004203F500B475E215EA5FA0040C000B00090001\ + 0025008E55EE7B64650100000000AE4D07005D660F007673340000C522370067076507FD0C6000270\ + 0FE0CC50E0D00AD0E0B069007BD0E0E00650D21001400FE0C910054007007690D8700FC0CBA00E407\ + 43001C0F140077077807D7078E00120F240068076D07DA0A74003D0F2500830780077A0AC401490F9\ + 60070077207FDFC9F079507950700C03B0015009AFF6900C8FFE0FFA700EBFF3A00F200F3FF02016D\ + 0A590A0D0AE3099B0C830CB50DA70D9D06CC0043009401B8338B334C20001000000000009F0200000\ + 3000000FF723D00BEFFFFFFFF2E89B0151C00', + 'observer': 'KB9JHU-EM69uf', + 'timestamp': '2021-01-05T22:28:09Z' + }, + response_only=True, # signal that example only applies to responses + ), + ] +) class TelemetrySerializer(serializers.ModelSerializer): """SatNOGS DB Telemetry API Serializer""" norad_cat_id = serializers.SerializerMethodField() @@ -151,10 +287,12 @@ class TelemetrySerializer(serializers.ModelSerializer): 'timestamp' ) + @extend_schema_field(OpenApiTypes.INT64) def get_norad_cat_id(self, obj): """Returns Satellite NORAD ID for this Transmitter""" return obj.satellite.norad_cat_id + @extend_schema_field(OpenApiTypes.UUID) def get_transmitter(self, obj): """Returns Transmitter UUID""" try: @@ -162,6 +300,8 @@ class TelemetrySerializer(serializers.ModelSerializer): except AttributeError: return '' + # deprecated, needs pulled out - cshields + @extend_schema_field(OpenApiTypes.STR) def get_schema(self, obj): """Returns Transmitter telemetry schema""" try: @@ -169,10 +309,12 @@ class TelemetrySerializer(serializers.ModelSerializer): except AttributeError: return '' + @extend_schema_field(OpenApiTypes.STR) def get_decoded(self, obj): """Returns the payload_decoded field""" return obj.payload_decoded + @extend_schema_field(OpenApiTypes.STR) def get_frame(self, obj): """Returns the payload frame""" return obj.display_frame() @@ -188,6 +330,26 @@ class SidsSerializer(serializers.ModelSerializer): ) +@extend_schema_serializer( + examples=[ + OpenApiExample( + 'View Artifact Example 1', + summary='Example: retrieving a specific artifact', + description='This is an example response when requesting a specific artifact \ + previously uploaded to DB', + value={ + 'id': + 1337, + 'network_obs_id': + 3376466, + 'artifact_file': + 'http://db-dev.satnogs.org/media/artifacts/bba35b2d-76cc-4a8f-9b8a-4a2ecb09c6df.h5' + }, + status_codes=['200'], + response_only=True, # signal that example only applies to responses + ), + ] +) class ArtifactSerializer(serializers.ModelSerializer): """SatNOGS DB Artifacts API Serializer""" class Meta: @@ -195,6 +357,21 @@ class ArtifactSerializer(serializers.ModelSerializer): fields = ('id', 'network_obs_id', 'artifact_file') +@extend_schema_serializer( + examples=[ + OpenApiExample( + 'New Artifact Example 1', + summary='Example: uploading artifact', + description='This is an example response after successfully uploading an artifact \ + file. The ID of the artifact is returned', + value={ + 'id': 1337, + }, + status_codes=['200', '201'], + response_only=True, # signal that example only applies to responses + ), + ] +) class NewArtifactSerializer(serializers.ModelSerializer): """SatNOGS Network New Artifact API Serializer""" def validate(self, attrs): @@ -206,7 +383,7 @@ class NewArtifactSerializer(serializers.ModelSerializer): raise serializers.ValidationError( 'Not a valid SatNOGS Artifact.', code='invalid' ) - except OSError as error: + except (OSError, MultiValueDictKeyError) as error: raise serializers.ValidationError( 'Not a valid HDF5 file: {}'.format(error), code='invalid' ) diff --git a/db/api/urls.py b/db/api/urls.py index 9523126..60b4b96 100644 --- a/db/api/urls.py +++ b/db/api/urls.py @@ -5,11 +5,11 @@ from db.api import views ROUTER = routers.DefaultRouter() -ROUTER.register(r'artifacts', views.ArtifactView) -ROUTER.register(r'modes', views.ModeView) -ROUTER.register(r'satellites', views.SatelliteView) -ROUTER.register(r'transmitters', views.TransmitterView) -ROUTER.register(r'telemetry', views.TelemetryView) -ROUTER.register(r'tle', views.LatestTleSetView) +ROUTER.register(r'artifacts', views.ArtifactViewSet) +ROUTER.register(r'modes', views.ModeViewSet) +ROUTER.register(r'satellites', views.SatelliteViewSet) +ROUTER.register(r'transmitters', views.TransmitterViewSet) +ROUTER.register(r'telemetry', views.TelemetryViewSet) +ROUTER.register(r'tle', views.LatestTleSetViewSet) API_URLPATTERNS = ROUTER.urls diff --git a/db/api/views.py b/db/api/views.py index 20886d5..afcb156 100644 --- a/db/api/views.py +++ b/db/api/views.py @@ -1,6 +1,8 @@ """SatNOGS DB API django rest framework Views""" from django.core.files.base import ContentFile from django.db.models import F +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import OpenApiParameter, extend_schema, extend_schema_view from rest_framework import mixins, status, viewsets from rest_framework.parsers import FileUploadParser, FormParser, MultiPartParser from rest_framework.permissions import IsAuthenticated @@ -16,8 +18,21 @@ from db.base.models import Artifact, DemodData, LatestTleSet, Mode, Satellite, T from db.base.tasks import update_satellite -class ModeView(viewsets.ReadOnlyModelViewSet): # pylint: disable=R0901 - """View into the transmitter modulation modes in the SatNOGS DB database""" +@extend_schema_view( + retrieve=extend_schema( + description='Retrieve a single RF Mode from SatNOGS DB based on its ID', + ), + list=extend_schema(description='Retrieve a complete list of RF Modes from SatNOGS DB', ) +) +class ModeViewSet(viewsets.ReadOnlyModelViewSet): # pylint: disable=R0901 + """ + Read-only view into the transmitter modulation modes (RF Modes) currently tracked + in the SatNOGS DB database + + For more details on individual RF mode types please [see our wiki][moderef]. + + [moderef]: https://wiki.satnogs.org/Category:RF_Modes + """ renderer_classes = [ JSONRenderer, BrowsableAPIRenderer, JSONLDRenderer, BrowserableJSONLDRenderer ] @@ -25,8 +40,47 @@ class ModeView(viewsets.ReadOnlyModelViewSet): # pylint: disable=R0901 serializer_class = serializers.ModeSerializer -class SatelliteView(viewsets.ReadOnlyModelViewSet): # pylint: disable=R0901 - """View into the Satellite entities in the SatNOGS DB database""" +@extend_schema_view( + list=extend_schema( + description='Retrieve a full or filtered list of satellites in SatNOGS DB', + parameters=[ + # drf-spectacular does not currently recognize the in_orbit filter as a + # bool, forcing it here. See drf-spectacular#234 + OpenApiParameter( + name='in_orbit', + description='Filter by satellites currently in orbit (True) or those that have \ + decayed (False)', + required=False, + type=bool + ), + OpenApiParameter( + name='norad_cat_id', + description='Select a satellite by its NORAD-assigned identifier' + ), + OpenApiParameter( + name='status', + description='Filter satellites by their operational status', + required=False, + type=bool + ), + ], + ), + retrieve=extend_schema( + description='Retrieve details on a single satellite in SatNOGS DB', + parameters=[ + OpenApiParameter( + 'norad_cat_id', + OpenApiTypes.INT64, + OpenApiParameter.PATH, + description='Select a satellite by its NORAD-assigned identifier' + ), + ], + ) +) +class SatelliteViewSet(viewsets.ReadOnlyModelViewSet): # pylint: disable=R0901 + """ + Read-only view into the Satellite entities in the SatNOGS DB database + """ renderer_classes = [ JSONRenderer, BrowsableAPIRenderer, JSONLDRenderer, BrowserableJSONLDRenderer ] @@ -36,9 +90,9 @@ class SatelliteView(viewsets.ReadOnlyModelViewSet): # pylint: disable=R0901 lookup_field = 'norad_cat_id' -class TransmitterView(viewsets.ReadOnlyModelViewSet): # pylint: disable=R0901 +class TransmitterViewSet(viewsets.ReadOnlyModelViewSet): # pylint: disable=R0901 """ - View into the Transmitter entities in the SatNOGS DB database. + Read-only view into the Transmitter entities in the SatNOGS DB database. Transmitters are inclusive of Transceivers and Transponders """ renderer_classes = [ @@ -50,9 +104,10 @@ class TransmitterView(viewsets.ReadOnlyModelViewSet): # pylint: disable=R0901 lookup_field = 'uuid' -class LatestTleSetView(viewsets.ReadOnlyModelViewSet): # pylint: disable=R0901 +class LatestTleSetViewSet(viewsets.ReadOnlyModelViewSet): # pylint: disable=R0901 """ - View into the most recent two-line elements (TLE) in the SatNOGS DB database + Read-only view into the most recent two-line elements (TLE) in the SatNOGS DB + database """ renderer_classes = [JSONRenderer, BrowsableAPIRenderer] queryset = LatestTleSet.objects.all().select_related('satellite').exclude( @@ -84,7 +139,29 @@ class LatestTleSetView(viewsets.ReadOnlyModelViewSet): # pylint: disable=R0901 return self.queryset -class TelemetryView( # pylint: disable=R0901 +@extend_schema_view( + list=extend_schema( + parameters=[ + OpenApiParameter( + name='app_source', + description='The submission source for the telemetry frames: manual (a manual \ + upload/entry), network (SatNOGS Network observations), or sids \ + (legacy API submission)', + ), + OpenApiParameter( + name='observer', + description='(string) name of the observer (submitter) to retrieve telemetry data \ + from' + ), + OpenApiParameter( + name='satellite', + description='NORAD ID of a satellite to filter telemetry data for' + ), + OpenApiParameter(name='transmitter', description='Not currently in use'), + ], + ), +) +class TelemetryViewSet( # pylint: disable=R0901 mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.CreateModelMixin, viewsets.GenericViewSet): """ @@ -102,11 +179,12 @@ class TelemetryView( # pylint: disable=R0901 parser_classes = (FormParser, FileUploadParser) pagination_class = pagination.LinkedHeaderPageNumberPagination + @extend_schema( + responses={'201': None}, # None + ) def create(self, request, *args, **kwargs): """ - Creates an frame of telemetry data from a satellite observation. See - https://www.pe0sat.vgnet.nl/download/Hidden/Dombrovski-SIDS-Simple-Downlink-Share-Convention.pdf - for a description of the original protocol. + Creates an frame of telemetry data from a satellite observation. """ data = {} @@ -156,11 +234,34 @@ class TelemetryView( # pylint: disable=R0901 return Response(status=status.HTTP_201_CREATED, headers=headers) -class ArtifactView( # pylint: disable=R0901 +@extend_schema_view( + list=extend_schema( + parameters=[ + OpenApiParameter( + 'network_obs_id', + OpenApiTypes.INT64, + required=False, + description='Given a SatNOGS Network observation ID, this will return any \ + artifacts files associated with the observation.' + ), + ], + ), + retrieve=extend_schema( + parameters=[ + OpenApiParameter( + 'id', + OpenApiTypes.URI, + OpenApiParameter.PATH, + description='The ID for the requested artifact entry in DB' + ), + ], + ), +) +class ArtifactViewSet( # pylint: disable=R0901 mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.CreateModelMixin, viewsets.GenericViewSet): """ - Artifacts are objects collected in relation to a satellite observation. + Artifacts are file-formatted objects collected from a satellite observation. """ queryset = Artifact.objects.all() filterset_class = filters.ArtifactViewFilter @@ -176,8 +277,10 @@ class ArtifactView( # pylint: disable=R0901 def create(self, request, *args, **kwargs): """ - Creates observation artifact + Creates observation artifact from an [HDF5 formatted file][hdf5ref] * Requires session or key authentication to create an artifact + + [hdf5ref]: https://en.wikipedia.org/wiki/Hierarchical_Data_Format """ serializer = self.get_serializer(data=request.data) try: diff --git a/db/settings.py b/db/settings.py index 95d7b25..68e7631 100644 --- a/db/settings.py +++ b/db/settings.py @@ -35,6 +35,7 @@ THIRD_PARTY_APPS = ( 'bootstrap_modal_forms', 'rest_framework', 'rest_framework.authtoken', + 'drf_spectacular', 'django_countries', 'django_filters', 'fontawesome_5', @@ -278,7 +279,127 @@ REST_FRAMEWORK = { 'rest_framework.authentication.TokenAuthentication', 'rest_framework.authentication.SessionAuthentication' ], - 'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend', ) + 'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend', ), + 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', +} + +SPECTACULAR_SETTINGS = { + # path prefix is used for tagging the discovered operations. + # use '/api/v[0-9]' for tagging apis like '/api/v1/albums' with ['albums'] + 'SCHEMA_PATH_PREFIX': r'/api', + 'DEFAULT_GENERATOR_CLASS': 'drf_spectacular.generators.SchemaGenerator', + + # Configuration for serving the schema with SpectacularAPIView + 'SERVE_URLCONF': None, + + # complete public schema or a subset based on the requesting user + 'SERVE_PUBLIC': True, + + # is the + 'SERVE_INCLUDE_SCHEMA': True, + 'SERVE_PERMISSIONS': ['rest_framework.permissions.AllowAny'], + + # available SwaggerUI configuration parameters + # https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/ + 'SWAGGER_UI_SETTINGS': { + 'deepLinking': True, + 'persistAuthorization': True, + 'displayOperationId': True, + }, + + # available SwaggerUI versions: https://github.com/swagger-api/swagger-ui/releases + 'SWAGGER_UI_DIST': STATIC_URL + 'lib/swagger-ui-dist', + 'SWAGGER_UI_FAVICON_HREF': STATIC_URL + 'favicon.ico', + 'TITLE': 'SatNOGS DB', + 'DESCRIPTION': 'SatNOGS DB is a crowdsourced database of details about orbital \ + satellites and data collected from them.', + 'TOS': None, + + # Optional: MAY contain "name", "url", "email" + 'CONTACT': { + 'name': 'SatNOGS Developer Chat', + 'url': 'https://riot.im/app/#/room/#satnogs-dev:matrix.org' + }, + + # Optional: MUST contain "name", MAY contain URL + 'LICENSE': { + 'name': 'AGPL 3.0', + 'url': 'https://www.gnu.org/licenses/agpl-3.0.html' + }, + 'VERSION': '1.1', + + # Optional list of servers. + # Each entry MUST contain "url", MAY contain "description", "variables" + 'SERVERS': [ + { + 'url': 'https://db-dev.satnogs.org', + 'description': 'Development server' + }, + { + 'url': 'https://db.satnogs.org', + 'description': 'Production server' + } + ], + + # Postprocessing functions that run at the end of schema generation. + # must satisfy interface result = hook(generator, request, public, result) + 'POSTPROCESSING_HOOKS': [ + 'drf_spectacular.hooks.postprocess_schema_enums' + ], + + # Function that returns a mocked request for view processing. For CLI usage + # original_request will be None. + # interface: request = build_mock_request(method, path, view, original_request, **kwargs) + 'GET_MOCK_REQUEST': 'drf_spectacular.plumbing.build_mock_request', + + # Tags defined in the global scope + 'TAGS': [ + { + 'name': 'artifacts', + 'description': 'IN DEVELOPMENT (BETA): Artifacts are file-formatted objects \ + collected from a satellite observation.' + }, + { + 'name': 'modes', + 'description': 'Radio Frequency modulation modes (RF Modes) currently \ + tracked in the SatNOGS DB database', + 'externalDocs': { + 'description': 'RF Modes in SatNOGS Wiki', + 'url': 'https://wiki.satnogs.org/Category:RF_Modes', + } + }, + { + 'name': 'satellites', + 'description': 'Human-made orbital objects, typically with radio frequency \ + transmitters and/or reveivers' + }, + { + 'name': 'telemetry', + 'description': 'Telemetry objects in the SatNOGS DB database are frames of \ + data collected from downlinked observations.' + }, + { + 'name': 'tle', + 'description': 'The most recent two-line elements (TLE) in the SatNOGS DB database', + 'externalDocs': { + 'description': 'TLE Wikipedia doc', + 'url': 'https://en.wikipedia.org/wiki/Two-line_element_set', + } + }, + { + 'name': 'transmitters', + 'description': 'Radio Frequency (RF) transmitter entities in the SatNOGS DB \ + database. Transmitters in this case are inclusive of Transceivers \ + and Transponders' + }, + ], + + # Optional: MUST contain 'url', may contain "description" + 'EXTERNAL_DOCS': { + 'url': 'https://wiki.satnogs.org', + 'description': 'SatNOGS Wiki' + }, + 'COMPONENT_SPLIT_REQUEST': True } # Security @@ -397,3 +518,6 @@ if ENVIRONMENT == 'dev': for backend in TEMPLATES: del backend['OPTIONS']['loaders'] backend['APP_DIRS'] = True + +# for h5 artifact uploads +DATA_UPLOAD_MAX_MEMORY_SIZE = 5242880 diff --git a/db/urls.py b/db/urls.py index 3c58190..39649fd 100644 --- a/db/urls.py +++ b/db/urls.py @@ -5,9 +5,8 @@ from django.conf.urls import include from django.contrib import admin from django.urls import path from django.views.static import serve -from rest_framework.schemas import get_schema_view +from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerSplitView -from db.api.generators import SchemaGenerator from db.api.urls import API_URLPATTERNS from db.base.urls import BASE_URLPATTERNS @@ -22,16 +21,10 @@ urlpatterns = [ path('api/', include(API_URLPATTERNS)), # API Schema - path( - 'api-schema', - get_schema_view( - title='SatNOGS DB', - description='SatNOGS DB is a transmitter suggestions and crowd-sourcing app.', - version='1.0', - generator_class=SchemaGenerator - ), - name='api-schema' - ), + path('api/schema/', SpectacularAPIView.as_view(), name='schema'), + # Swagger UI view of our schema. Note the use of SpectacularSwaggerSplitView + # is to avoid CSP issues without having to open up unsafe-inline. + path('api/schema/docs/', SpectacularSwaggerSplitView.as_view(url_name='schema'), name='docs'), # Admin path('admin/', admin.site.urls), diff --git a/docs/api.rst b/docs/api.rst index b9ff7d5..91a9418 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -23,4 +23,4 @@ This Python client is available in `PyPI `_ contain a full interactive reference of the API. diff --git a/package-lock.json b/package-lock.json index a429090..7dd560a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6909,6 +6909,11 @@ "pdfkit": ">=0.8.1" } }, + "swagger-ui-dist": { + "version": "3.39.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.39.0.tgz", + "integrity": "sha512-mNCdhxMvYH0E96ebDX5LL3Yj8zMqC/HFAN5YDjwYxuetEewZ6onBrBBSJsWcl6vCxbEbtS2qBiy9OtBY+YyndQ==" + }, "sweetalert2": { "version": "9.17.2", "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-9.17.2.tgz", diff --git a/package.json b/package.json index 2a14685..a3f359e 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "d3": "^6.3.0", "flot": "^4.2.1", "gpredict.js": "github:kerel-fs/gpredict.js", - "mapbox-gl": "^2.0.0" + "mapbox-gl": "^2.0.0", + "swagger-ui-dist": "^3.39.0" }, "assets": [ "admin-lte/**/*", @@ -28,6 +29,8 @@ "flot/dist/**/*", "gpredict.js/dist/gpredict.min.js", "mapbox-gl/dist/mapbox-gl.css", - "mapbox-gl/dist/mapbox-gl.js" + "mapbox-gl/dist/mapbox-gl.js", + "swagger-ui-dist/swagger-ui.css", + "swagger-ui-dist/swagger-ui-bundle.js" ] } diff --git a/requirements.txt b/requirements.txt index 03264bc..9384405 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,7 @@ chardet==3.0.4 cryptography==3.3.1 defusedxml==0.7.0rc1 dj-database-url==0.5.0 -Django==3.1.4 +Django==3.1.5 django-allauth==0.44.0 django-appconf==1.0.4 django-avatar==5.0.0 @@ -33,6 +33,7 @@ django-shortuuidfield==0.1.3 django-widget-tweaks==1.4.8 djangorestframework==3.12.2 dnspython==1.16.0 +drf-spectacular==0.12.0 ecdsa==0.14.1 enum34==1.1.10 eventlet==0.29.1 @@ -42,21 +43,24 @@ gunicorn==19.9.0 h5py==3.1.0 idna==2.10 importlib-metadata==1.7.0 +inflection==0.5.1 influxdb==5.3.1 +jsonschema==3.2.0 kaitaistruct==0.9 kombu==4.6.11 Logbook==1.5.3 lxml==4.6.2 Markdown==3.3.3 msgpack==1.0.2 -mysqlclient==2.0.2 -numpy==1.19.4 +mysqlclient==2.0.3 +numpy==1.19.5 oauthlib==3.1.0 -Pillow==8.0.1 +Pillow==8.1.0 pyasn1==0.4.8 pycparser==2.20 PyJWT==2.0.0 PyLD==2.0.3 +pyrsistent==0.17.3 python-dateutil==2.8.1 python-decouple==3.3 python-dotenv==0.15.0 @@ -81,7 +85,7 @@ simplejson==3.17.2 six==1.15.0 social-auth-app-django==4.0.0 social-auth-core==3.3.3 -spacetrack==0.15.0 +spacetrack==0.16.0 sqlparse==0.4.1 Unipath==1.1 uritemplate==3.0.1 diff --git a/setup.cfg b/setup.cfg index 9530009..3b7be11 100644 --- a/setup.cfg +++ b/setup.cfg @@ -53,6 +53,7 @@ install_requires = django_compressor~=2.4.0 # API djangorestframework~=3.12.0 + drf-spectacular~=0.12.0 Markdown~=3.3.0 django-filter~=2.4.0 # Astronomy