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