1
0
Fork 0

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 <cshields@gmail.com>
spacecruft
Corey Shields 2021-01-09 20:39:09 -05:00
parent bec7469dc2
commit 5e03f7c759
13 changed files with 458 additions and 94 deletions

View File

@ -26,11 +26,10 @@ schema:
script: script:
- pip install --no-cache-dir --no-deps -r "requirements.txt" --force-reinstall . - pip install --no-cache-dir --no-deps -r "requirements.txt" --force-reinstall .
- >- - >-
./manage.py generateschema ./manage.py spectacular
--title "SatNOGS DB" --file satnogs-db-api-client/api-schema.yml
--description "SatNOGS DB is a transmitter suggestions and crowd-sourcing app." --validate
--generator_class "db.api.generators.SchemaGenerator" --fail-on-warn
> satnogs-db-api-client/api-schema.yml
artifacts: artifacts:
expire_in: 1 week expire_in: 1 week
when: always when: always

View File

@ -6,3 +6,4 @@ satnogs-db-api-client
build build
docs docs
versioneer.py versioneer.py
db/settings.py

View File

@ -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

View File

@ -2,12 +2,31 @@
# pylint: disable=R0201 # pylint: disable=R0201
import h5py 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 rest_framework import serializers
from db.base.models import TRANSMITTER_STATUS, Artifact, DemodData, LatestTleSet, Mode, \ from db.base.models import TRANSMITTER_STATUS, Artifact, DemodData, LatestTleSet, Mode, \
Satellite, Telemetry, Transmitter 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): class ModeSerializer(serializers.ModelSerializer):
"""SatNOGS DB Mode API Serializer""" """SatNOGS DB Mode API Serializer"""
class Meta: class Meta:
@ -22,6 +41,32 @@ class SatTelemetrySerializer(serializers.ModelSerializer):
fields = ['decoder'] 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): class SatelliteSerializer(serializers.ModelSerializer):
"""SatNOGS DB Satellite API Serializer""" """SatNOGS DB Satellite API Serializer"""
@ -36,15 +81,50 @@ class SatelliteSerializer(serializers.ModelSerializer):
'website', 'operator', 'countries', 'telemetries' 'website', 'operator', 'countries', 'telemetries'
) )
@extend_schema_field(OpenApiTypes.STR)
def get_operator(self, obj): def get_operator(self, obj):
"""Returns operator text""" """Returns operator text"""
return str(obj.operator) return str(obj.operator)
@extend_schema_field(OpenApiTypes.STR)
def get_countries(self, obj): def get_countries(self, obj):
"""Returns countires""" """Returns countires"""
return ','.join(map(str, obj.countries)) 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): class TransmitterSerializer(serializers.ModelSerializer):
"""SatNOGS DB Transmitter API Serializer""" """SatNOGS DB Transmitter API Serializer"""
norad_cat_id = serializers.SerializerMethodField() norad_cat_id = serializers.SerializerMethodField()
@ -64,10 +144,12 @@ class TransmitterSerializer(serializers.ModelSerializer):
) )
# Keeping alive field for compatibility issues # Keeping alive field for compatibility issues
@extend_schema_field(OpenApiTypes.BOOL)
def get_alive(self, obj): def get_alive(self, obj):
"""Returns transmitter status""" """Returns transmitter status"""
return obj.status == TRANSMITTER_STATUS[0] return obj.status == TRANSMITTER_STATUS[0]
@extend_schema_field(OpenApiTypes.INT)
def get_mode_id(self, obj): def get_mode_id(self, obj):
"""Returns downlink mode id""" """Returns downlink mode id"""
try: try:
@ -75,6 +157,7 @@ class TransmitterSerializer(serializers.ModelSerializer):
except AttributeError: # rare chance that this happens in prod except AttributeError: # rare chance that this happens in prod
return None return None
@extend_schema_field(OpenApiTypes.INT)
def get_mode(self, obj): def get_mode(self, obj):
"""Returns downlink mode name""" """Returns downlink mode name"""
try: try:
@ -82,6 +165,7 @@ class TransmitterSerializer(serializers.ModelSerializer):
except AttributeError: except AttributeError:
return None return None
@extend_schema_field(OpenApiTypes.INT)
def get_uplink_mode(self, obj): def get_uplink_mode(self, obj):
"""Returns uplink mode name""" """Returns uplink mode name"""
try: try:
@ -89,6 +173,7 @@ class TransmitterSerializer(serializers.ModelSerializer):
except AttributeError: except AttributeError:
return None return None
@extend_schema_field(OpenApiTypes.INT64)
def get_norad_cat_id(self, obj): def get_norad_cat_id(self, obj):
"""Returns Satellite NORAD ID""" """Returns Satellite NORAD ID"""
try: try:
@ -97,6 +182,23 @@ class TransmitterSerializer(serializers.ModelSerializer):
return None 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): class LatestTleSetSerializer(serializers.ModelSerializer):
"""SatNOGS DB LatestTleSet API Serializer""" """SatNOGS DB LatestTleSet API Serializer"""
@ -111,31 +213,65 @@ class LatestTleSetSerializer(serializers.ModelSerializer):
model = LatestTleSet model = LatestTleSet
fields = ('tle0', 'tle1', 'tle2', 'tle_source', 'norad_cat_id', 'updated') fields = ('tle0', 'tle1', 'tle2', 'tle_source', 'norad_cat_id', 'updated')
@extend_schema_field(OpenApiTypes.INT64)
def get_norad_cat_id(self, obj): def get_norad_cat_id(self, obj):
"""Returns Satellite NORAD ID""" """Returns Satellite NORAD ID"""
return obj.satellite.norad_cat_id return obj.satellite.norad_cat_id
@extend_schema_field(OpenApiTypes.STR)
def get_tle0(self, obj): def get_tle0(self, obj):
"""Returns TLE line 0""" """Returns TLE line 0"""
return obj.tle0 return obj.tle0
@extend_schema_field(OpenApiTypes.STR)
def get_tle1(self, obj): def get_tle1(self, obj):
"""Returns TLE line 1""" """Returns TLE line 1"""
return obj.tle1 return obj.tle1
@extend_schema_field(OpenApiTypes.STR)
def get_tle2(self, obj): def get_tle2(self, obj):
"""Returns TLE line 2""" """Returns TLE line 2"""
return obj.tle2 return obj.tle2
@extend_schema_field(OpenApiTypes.STR)
def get_tle_source(self, obj): def get_tle_source(self, obj):
"""Returns TLE source""" """Returns TLE source"""
return obj.tle_source return obj.tle_source
@extend_schema_field(OpenApiTypes.DATETIME)
def get_updated(self, obj): def get_updated(self, obj):
"""Returns TLE updated datetime""" """Returns TLE updated datetime"""
return obj.updated.strftime('%Y-%m-%dT%H:%M:%S.%f%z') 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): class TelemetrySerializer(serializers.ModelSerializer):
"""SatNOGS DB Telemetry API Serializer""" """SatNOGS DB Telemetry API Serializer"""
norad_cat_id = serializers.SerializerMethodField() norad_cat_id = serializers.SerializerMethodField()
@ -151,10 +287,12 @@ class TelemetrySerializer(serializers.ModelSerializer):
'timestamp' 'timestamp'
) )
@extend_schema_field(OpenApiTypes.INT64)
def get_norad_cat_id(self, obj): def get_norad_cat_id(self, obj):
"""Returns Satellite NORAD ID for this Transmitter""" """Returns Satellite NORAD ID for this Transmitter"""
return obj.satellite.norad_cat_id return obj.satellite.norad_cat_id
@extend_schema_field(OpenApiTypes.UUID)
def get_transmitter(self, obj): def get_transmitter(self, obj):
"""Returns Transmitter UUID""" """Returns Transmitter UUID"""
try: try:
@ -162,6 +300,8 @@ class TelemetrySerializer(serializers.ModelSerializer):
except AttributeError: except AttributeError:
return '' return ''
# deprecated, needs pulled out - cshields
@extend_schema_field(OpenApiTypes.STR)
def get_schema(self, obj): def get_schema(self, obj):
"""Returns Transmitter telemetry schema""" """Returns Transmitter telemetry schema"""
try: try:
@ -169,10 +309,12 @@ class TelemetrySerializer(serializers.ModelSerializer):
except AttributeError: except AttributeError:
return '' return ''
@extend_schema_field(OpenApiTypes.STR)
def get_decoded(self, obj): def get_decoded(self, obj):
"""Returns the payload_decoded field""" """Returns the payload_decoded field"""
return obj.payload_decoded return obj.payload_decoded
@extend_schema_field(OpenApiTypes.STR)
def get_frame(self, obj): def get_frame(self, obj):
"""Returns the payload frame""" """Returns the payload frame"""
return obj.display_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): class ArtifactSerializer(serializers.ModelSerializer):
"""SatNOGS DB Artifacts API Serializer""" """SatNOGS DB Artifacts API Serializer"""
class Meta: class Meta:
@ -195,6 +357,21 @@ class ArtifactSerializer(serializers.ModelSerializer):
fields = ('id', 'network_obs_id', 'artifact_file') 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): class NewArtifactSerializer(serializers.ModelSerializer):
"""SatNOGS Network New Artifact API Serializer""" """SatNOGS Network New Artifact API Serializer"""
def validate(self, attrs): def validate(self, attrs):
@ -206,7 +383,7 @@ class NewArtifactSerializer(serializers.ModelSerializer):
raise serializers.ValidationError( raise serializers.ValidationError(
'Not a valid SatNOGS Artifact.', code='invalid' 'Not a valid SatNOGS Artifact.', code='invalid'
) )
except OSError as error: except (OSError, MultiValueDictKeyError) as error:
raise serializers.ValidationError( raise serializers.ValidationError(
'Not a valid HDF5 file: {}'.format(error), code='invalid' 'Not a valid HDF5 file: {}'.format(error), code='invalid'
) )

View File

@ -5,11 +5,11 @@ from db.api import views
ROUTER = routers.DefaultRouter() ROUTER = routers.DefaultRouter()
ROUTER.register(r'artifacts', views.ArtifactView) ROUTER.register(r'artifacts', views.ArtifactViewSet)
ROUTER.register(r'modes', views.ModeView) ROUTER.register(r'modes', views.ModeViewSet)
ROUTER.register(r'satellites', views.SatelliteView) ROUTER.register(r'satellites', views.SatelliteViewSet)
ROUTER.register(r'transmitters', views.TransmitterView) ROUTER.register(r'transmitters', views.TransmitterViewSet)
ROUTER.register(r'telemetry', views.TelemetryView) ROUTER.register(r'telemetry', views.TelemetryViewSet)
ROUTER.register(r'tle', views.LatestTleSetView) ROUTER.register(r'tle', views.LatestTleSetViewSet)
API_URLPATTERNS = ROUTER.urls API_URLPATTERNS = ROUTER.urls

View File

@ -1,6 +1,8 @@
"""SatNOGS DB API django rest framework Views""" """SatNOGS DB API django rest framework Views"""
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.db.models import F 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 import mixins, status, viewsets
from rest_framework.parsers import FileUploadParser, FormParser, MultiPartParser from rest_framework.parsers import FileUploadParser, FormParser, MultiPartParser
from rest_framework.permissions import IsAuthenticated 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 from db.base.tasks import update_satellite
class ModeView(viewsets.ReadOnlyModelViewSet): # pylint: disable=R0901 @extend_schema_view(
"""View into the transmitter modulation modes in the SatNOGS DB database""" 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 = [ renderer_classes = [
JSONRenderer, BrowsableAPIRenderer, JSONLDRenderer, BrowserableJSONLDRenderer JSONRenderer, BrowsableAPIRenderer, JSONLDRenderer, BrowserableJSONLDRenderer
] ]
@ -25,8 +40,47 @@ class ModeView(viewsets.ReadOnlyModelViewSet): # pylint: disable=R0901
serializer_class = serializers.ModeSerializer serializer_class = serializers.ModeSerializer
class SatelliteView(viewsets.ReadOnlyModelViewSet): # pylint: disable=R0901 @extend_schema_view(
"""View into the Satellite entities in the SatNOGS DB database""" 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 = [ renderer_classes = [
JSONRenderer, BrowsableAPIRenderer, JSONLDRenderer, BrowserableJSONLDRenderer JSONRenderer, BrowsableAPIRenderer, JSONLDRenderer, BrowserableJSONLDRenderer
] ]
@ -36,9 +90,9 @@ class SatelliteView(viewsets.ReadOnlyModelViewSet): # pylint: disable=R0901
lookup_field = 'norad_cat_id' 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 Transmitters are inclusive of Transceivers and Transponders
""" """
renderer_classes = [ renderer_classes = [
@ -50,9 +104,10 @@ class TransmitterView(viewsets.ReadOnlyModelViewSet): # pylint: disable=R0901
lookup_field = 'uuid' 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] renderer_classes = [JSONRenderer, BrowsableAPIRenderer]
queryset = LatestTleSet.objects.all().select_related('satellite').exclude( queryset = LatestTleSet.objects.all().select_related('satellite').exclude(
@ -84,7 +139,29 @@ class LatestTleSetView(viewsets.ReadOnlyModelViewSet): # pylint: disable=R0901
return self.queryset 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, mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.CreateModelMixin,
viewsets.GenericViewSet): viewsets.GenericViewSet):
""" """
@ -102,11 +179,12 @@ class TelemetryView( # pylint: disable=R0901
parser_classes = (FormParser, FileUploadParser) parser_classes = (FormParser, FileUploadParser)
pagination_class = pagination.LinkedHeaderPageNumberPagination pagination_class = pagination.LinkedHeaderPageNumberPagination
@extend_schema(
responses={'201': None}, # None
)
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
""" """
Creates an frame of telemetry data from a satellite observation. See Creates an frame of telemetry data from a satellite observation.
https://www.pe0sat.vgnet.nl/download/Hidden/Dombrovski-SIDS-Simple-Downlink-Share-Convention.pdf
for a description of the original protocol.
""" """
data = {} data = {}
@ -156,11 +234,34 @@ class TelemetryView( # pylint: disable=R0901
return Response(status=status.HTTP_201_CREATED, headers=headers) 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, mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.CreateModelMixin,
viewsets.GenericViewSet): 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() queryset = Artifact.objects.all()
filterset_class = filters.ArtifactViewFilter filterset_class = filters.ArtifactViewFilter
@ -176,8 +277,10 @@ class ArtifactView( # pylint: disable=R0901
def create(self, request, *args, **kwargs): 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 * 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) serializer = self.get_serializer(data=request.data)
try: try:

View File

@ -35,6 +35,7 @@ THIRD_PARTY_APPS = (
'bootstrap_modal_forms', 'bootstrap_modal_forms',
'rest_framework', 'rest_framework',
'rest_framework.authtoken', 'rest_framework.authtoken',
'drf_spectacular',
'django_countries', 'django_countries',
'django_filters', 'django_filters',
'fontawesome_5', 'fontawesome_5',
@ -278,7 +279,127 @@ REST_FRAMEWORK = {
'rest_framework.authentication.TokenAuthentication', 'rest_framework.authentication.TokenAuthentication',
'rest_framework.authentication.SessionAuthentication' '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 # Security
@ -397,3 +518,6 @@ if ENVIRONMENT == 'dev':
for backend in TEMPLATES: for backend in TEMPLATES:
del backend['OPTIONS']['loaders'] del backend['OPTIONS']['loaders']
backend['APP_DIRS'] = True backend['APP_DIRS'] = True
# for h5 artifact uploads
DATA_UPLOAD_MAX_MEMORY_SIZE = 5242880

View File

@ -5,9 +5,8 @@ from django.conf.urls import include
from django.contrib import admin from django.contrib import admin
from django.urls import path from django.urls import path
from django.views.static import serve 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.api.urls import API_URLPATTERNS
from db.base.urls import BASE_URLPATTERNS from db.base.urls import BASE_URLPATTERNS
@ -22,16 +21,10 @@ urlpatterns = [
path('api/', include(API_URLPATTERNS)), path('api/', include(API_URLPATTERNS)),
# API Schema # API Schema
path( path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
'api-schema', # Swagger UI view of our schema. Note the use of SpectacularSwaggerSplitView
get_schema_view( # is to avoid CSP issues without having to open up unsafe-inline.
title='SatNOGS DB', path('api/schema/docs/', SpectacularSwaggerSplitView.as_view(url_name='schema'), name='docs'),
description='SatNOGS DB is a transmitter suggestions and crowd-sourcing app.',
version='1.0',
generator_class=SchemaGenerator
),
name='api-schema'
),
# Admin # Admin
path('admin/', admin.site.urls), path('admin/', admin.site.urls),

View File

@ -23,4 +23,4 @@ This Python client is available in `PyPI <https://pypi.org/project/satnogs-db-ap
API Reference API Reference
------------- -------------
|api_reference_url|_ contains a full reference of the API. `Our live schema docs <https://db.satnogs.org/api/schema/docs/>`_ contain a full interactive reference of the API.

5
package-lock.json generated
View File

@ -6909,6 +6909,11 @@
"pdfkit": ">=0.8.1" "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": { "sweetalert2": {
"version": "9.17.2", "version": "9.17.2",
"resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-9.17.2.tgz", "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-9.17.2.tgz",

View File

@ -20,7 +20,8 @@
"d3": "^6.3.0", "d3": "^6.3.0",
"flot": "^4.2.1", "flot": "^4.2.1",
"gpredict.js": "github:kerel-fs/gpredict.js", "gpredict.js": "github:kerel-fs/gpredict.js",
"mapbox-gl": "^2.0.0" "mapbox-gl": "^2.0.0",
"swagger-ui-dist": "^3.39.0"
}, },
"assets": [ "assets": [
"admin-lte/**/*", "admin-lte/**/*",
@ -28,6 +29,8 @@
"flot/dist/**/*", "flot/dist/**/*",
"gpredict.js/dist/gpredict.min.js", "gpredict.js/dist/gpredict.min.js",
"mapbox-gl/dist/mapbox-gl.css", "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"
] ]
} }

View File

@ -15,7 +15,7 @@ chardet==3.0.4
cryptography==3.3.1 cryptography==3.3.1
defusedxml==0.7.0rc1 defusedxml==0.7.0rc1
dj-database-url==0.5.0 dj-database-url==0.5.0
Django==3.1.4 Django==3.1.5
django-allauth==0.44.0 django-allauth==0.44.0
django-appconf==1.0.4 django-appconf==1.0.4
django-avatar==5.0.0 django-avatar==5.0.0
@ -33,6 +33,7 @@ django-shortuuidfield==0.1.3
django-widget-tweaks==1.4.8 django-widget-tweaks==1.4.8
djangorestframework==3.12.2 djangorestframework==3.12.2
dnspython==1.16.0 dnspython==1.16.0
drf-spectacular==0.12.0
ecdsa==0.14.1 ecdsa==0.14.1
enum34==1.1.10 enum34==1.1.10
eventlet==0.29.1 eventlet==0.29.1
@ -42,21 +43,24 @@ gunicorn==19.9.0
h5py==3.1.0 h5py==3.1.0
idna==2.10 idna==2.10
importlib-metadata==1.7.0 importlib-metadata==1.7.0
inflection==0.5.1
influxdb==5.3.1 influxdb==5.3.1
jsonschema==3.2.0
kaitaistruct==0.9 kaitaistruct==0.9
kombu==4.6.11 kombu==4.6.11
Logbook==1.5.3 Logbook==1.5.3
lxml==4.6.2 lxml==4.6.2
Markdown==3.3.3 Markdown==3.3.3
msgpack==1.0.2 msgpack==1.0.2
mysqlclient==2.0.2 mysqlclient==2.0.3
numpy==1.19.4 numpy==1.19.5
oauthlib==3.1.0 oauthlib==3.1.0
Pillow==8.0.1 Pillow==8.1.0
pyasn1==0.4.8 pyasn1==0.4.8
pycparser==2.20 pycparser==2.20
PyJWT==2.0.0 PyJWT==2.0.0
PyLD==2.0.3 PyLD==2.0.3
pyrsistent==0.17.3
python-dateutil==2.8.1 python-dateutil==2.8.1
python-decouple==3.3 python-decouple==3.3
python-dotenv==0.15.0 python-dotenv==0.15.0
@ -81,7 +85,7 @@ simplejson==3.17.2
six==1.15.0 six==1.15.0
social-auth-app-django==4.0.0 social-auth-app-django==4.0.0
social-auth-core==3.3.3 social-auth-core==3.3.3
spacetrack==0.15.0 spacetrack==0.16.0
sqlparse==0.4.1 sqlparse==0.4.1
Unipath==1.1 Unipath==1.1
uritemplate==3.0.1 uritemplate==3.0.1

View File

@ -53,6 +53,7 @@ install_requires =
django_compressor~=2.4.0 django_compressor~=2.4.0
# API # API
djangorestframework~=3.12.0 djangorestframework~=3.12.0
drf-spectacular~=0.12.0
Markdown~=3.3.0 Markdown~=3.3.0
django-filter~=2.4.0 django-filter~=2.4.0
# Astronomy # Astronomy