Skip to content

Commit

Permalink
Merge pull request #584 from jennydaman/fix-spectacular-warnings
Browse files Browse the repository at this point in the history
Fix all spectacular warnings and add OpenAPI spec to release draft
  • Loading branch information
jennydaman authored Oct 2, 2024
2 parents 646df44 + b2fc1aa commit f32519a
Show file tree
Hide file tree
Showing 26 changed files with 277 additions and 124 deletions.
67 changes: 66 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ jobs:
command: test-${{ matrix.test-kind }}

build:
name: Build
needs: [ test ]
if: github.event_name == 'push'
runs-on: ubuntu-24.04
Expand Down Expand Up @@ -63,7 +64,7 @@ jobs:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push
uses: docker/build-push-action@v6
with:
Expand All @@ -84,3 +85,67 @@ jobs:
short-description: ChRIS backend
readme-filepath: ./README.md
repository: fnndsc/cube

openapi:
name: Generate OpenAPI Schema
runs-on: ubuntu-24.04
steps:
- uses: taiki-e/install-action@just
- uses: actions/checkout@v4
- run: just prefer docker
- run: just build
- name: Generate OpenAPI schema (correct)
run: just openapi 2> spectacular.log > schema.yaml || (cat spectacular.log; exit 1)
- name: Generate OpenAPI schema (split)
run: just openapi-split 2> spectacular_split.log > schema_split.yaml || (cat spectacular_split.log; exit 1)
- name: Upload schema YAML files
uses: actions/upload-artifact@v4
with:
name: openapi_schema
path: schema*.yaml
if-no-files-found: error
- name: Assert no drf-spectacular errors nor warnings
run: |
for f in spectacular*.log; do
if grep -qi '(warning|error)' $f; then
cat $f
exit 1
fi
done
- name: Stop services
run: just down

draft-release:
name: Draft GitHub Release
runs-on: ubuntu-24.04
if: startsWith(github.ref, 'refs/tags/v')
needs:
- build
- openapi
steps:
- id: version
run: echo "version=${GITHUB_REF_NAME:1}" >> "$GITHUB_OUTPUT"
- uses: actions/download-artifact@v4
with:
name: openapi_schema
- uses: softprops/action-gh-release@v2
with:
draft: true
prerelease: ${{ contains( github.ref_name, '-' ) }}
files: openapi_schema/*.yaml
fail_on_unmatched_files: true
name: CUBE - version ${{ steps.version.outputs.version }}
generate_release_notes: true
body: |
## Getting the Image
Using [Podman](https://podman.io):
```shell
podman pull ghcr.io/fnndsc/cube:${{ steps.version.outputs.version }}
```
Using [Docker](https://docker.com):
```shell
docker pull ghcr.io/fnndsc/cube:${{ steps.version.outputs.version }}
```
7 changes: 6 additions & 1 deletion chris_backend/config/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from environs import Env

from __version__ import __version__
from plugins.enums import PLUGIN_TYPE_CHOICES, TYPE_CHOICES

# Environment variables-based secrets
env = Env()
Expand Down Expand Up @@ -196,14 +197,18 @@
'email': '[email protected]'
},
'SERVE_INCLUDE_SCHEMA': True,
'ENUM_NAME_OVERRIDES': {
'PluginType': PLUGIN_TYPE_CHOICES,
'PluginParameterType': TYPE_CHOICES,
},
'COMPONENT_SPLIT_REQUEST': env.bool("SPECTACULAR_SPLIT_REQUEST", False),
'PREPROCESSING_HOOKS': [
'drf_spectacular.hooks.preprocess_exclude_path_format',
],
'POSTPROCESSING_HOOKS': [
'drf_spectacular.hooks.postprocess_schema_enums',
'collectionjson.spectacular_hooks.postprocess_remove_collectionjson',
'plugininstances.spectacular_hooks.additionalproperties_for_plugins_instances_create'
'plugininstances.spectacular_hooks.additionalproperties_for_plugins_instances_create',
],

'SCHEMA_PATH_PREFIX': '/api/v1/',
Expand Down
50 changes: 49 additions & 1 deletion chris_backend/core/serializers.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
from typing import Optional

from rest_framework import serializers
from drf_spectacular.utils import OpenApiTypes, extend_schema_field

from collectionjson.fields import ItemLinkField
from .utils import get_file_resource_link
from .models import ChrisInstance, FileDownloadToken


class ChrisInstanceSerializer(serializers.HyperlinkedModelSerializer):

class Meta:
model = ChrisInstance
fields = ('url', 'id', 'creation_date', 'name', 'uuid', 'job_id_prefix',
Expand All @@ -20,3 +23,48 @@ class FileDownloadTokenSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = FileDownloadToken
fields = ('url', 'id', 'creation_date', 'token', 'owner_username', 'owner')


def file_serializer(cls: Optional[serializers.SerializerMetaclass] = None, required: bool = True):
"""
A class decorator for adding fields to serializers of ``ChrisFile`` views.
:param cls: the serializer to wrap
:param required: whether the ``fname`` field is required
"""
if cls is None:
return lambda cls: _wrap_file_serializer_class(cls, required)
return _wrap_file_serializer_class(cls, required)


def _wrap_file_serializer_class(cls: serializers.SerializerMetaclass, required: bool):
# Implementation notes:
# - Do not use ``fsize = serializers.ReadOnlyField(source="fname.size")``
# See bug: https://github.com/tfranzel/drf-spectacular/issues/1303
# - Mixin pattern does not work, you get " Field name `fsize` is not valid for model `ChrisFile`."
# Decorator pattern is a workaround.
assert type(cls) == serializers.SerializerMetaclass, f'{cls} is not a serializer'

class _FileSerializer(cls):
fname = serializers.FileField(use_url=False, required=required)
fsize = serializers.SerializerMethodField()
file_resource = ItemLinkField('get_file_link')
owner_username = serializers.ReadOnlyField(source='owner.username')
parent_folder = serializers.HyperlinkedRelatedField(view_name='chrisfolder-detail', read_only=True)
owner = serializers.HyperlinkedRelatedField(view_name='user-detail', read_only=True)

def get_fsize(self, obj) -> int:
"""
Get the size of the file in bytes.
"""
return obj.fname.size

@extend_schema_field(OpenApiTypes.URI)
def get_file_link(self, obj):
"""
Custom method to get the hyperlink to the actual file resource.
"""
return get_file_resource_link(self, obj)

_FileSerializer.__name__ = cls.__name__
return _FileSerializer
4 changes: 4 additions & 0 deletions chris_backend/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ def get_queryset(self):
Overriden to return a custom queryset that is only comprised by the file download
tokens owned by the currently authenticated user.
"""
if getattr(self, "swagger_fake_view", False):
return FileDownloadToken.objects.none()
user = self.request.user
# if the user is chris then return all the file download tokens in the system
if user.username == 'chris':
Expand All @@ -98,6 +100,8 @@ def get_queryset(self):
Overriden to return a custom queryset that is only comprised by the file download
tokens owned by the currently authenticated user.
"""
if getattr(self, "swagger_fake_view", False):
return FileDownloadToken.objects.none()
user = self.request.user
# if the user is chris then return all the file download tokens in the system
if user.username == 'chris':
Expand Down
16 changes: 8 additions & 8 deletions chris_backend/feeds/serializers.py
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -161,15 +161,15 @@ def validate_public(self, public):
"superuser 'chris'."])
return public

def get_created_jobs(self, obj):
def get_created_jobs(self, obj) -> int:
"""
Overriden to get the number of plugin instances in 'created' status.
"""
if 'created' not in [status[0] for status in STATUS_CHOICES]:
raise KeyError("Undefined plugin instance execution status: 'created'.")
return obj.get_plugin_instances_status_count('created')

def get_waiting_jobs(self, obj):
def get_waiting_jobs(self, obj) -> int:
"""
Overriden to get the number of plugin instances in 'waiting' status.
"""
Expand All @@ -178,23 +178,23 @@ def get_waiting_jobs(self, obj):
raise KeyError(msg)
return obj.get_plugin_instances_status_count('waiting')

def get_scheduled_jobs(self, obj):
def get_scheduled_jobs(self, obj) -> int:
"""
Overriden to get the number of plugin instances in 'scheduled' status.
"""
if 'scheduled' not in [status[0] for status in STATUS_CHOICES]:
raise KeyError("Undefined plugin instance execution status: 'scheduled'.")
return obj.get_plugin_instances_status_count('scheduled')

def get_started_jobs(self, obj):
def get_started_jobs(self, obj) -> int:
"""
Overriden to get the number of plugin instances in 'started' status.
"""
if 'started' not in [status[0] for status in STATUS_CHOICES]:
raise KeyError("Undefined plugin instance execution status: 'started'.")
return obj.get_plugin_instances_status_count('started')

def get_registering_jobs(self, obj):
def get_registering_jobs(self, obj) -> int:
"""
Overriden to get the number of plugin instances in 'registeringFiles' status.
"""
Expand All @@ -203,7 +203,7 @@ def get_registering_jobs(self, obj):
raise KeyError(msg)
return obj.get_plugin_instances_status_count('registeringFiles')

def get_finished_jobs(self, obj):
def get_finished_jobs(self, obj) -> int:
"""
Overriden to get the number of plugin instances in 'finishedSuccessfully' status.
"""
Expand All @@ -212,7 +212,7 @@ def get_finished_jobs(self, obj):
"'finishedSuccessfully'.")
return obj.get_plugin_instances_status_count('finishedSuccessfully')

def get_errored_jobs(self, obj):
def get_errored_jobs(self, obj) -> int:
"""
Overriden to get the number of plugin instances in 'finishedWithError' status.
"""
Expand All @@ -221,7 +221,7 @@ def get_errored_jobs(self, obj):
"'finishedWithError'.")
return obj.get_plugin_instances_status_count('finishedWithError')

def get_cancelled_jobs(self, obj):
def get_cancelled_jobs(self, obj) -> int:
"""
Overriden to get the number of plugin instances in 'cancelled' status.
"""
Expand Down
16 changes: 16 additions & 0 deletions chris_backend/feeds/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from django.shortcuts import get_object_or_404
from rest_framework import generics, permissions
from rest_framework.reverse import reverse
from drf_spectacular.utils import extend_schema, extend_schema_view

from collectionjson import services
from plugininstances.serializers import PluginInstanceSerializer
Expand Down Expand Up @@ -39,6 +40,9 @@ def retrieve(self, request, *args, **kwargs):
return services.append_collection_template(response, template_data)


@extend_schema_view(
get=extend_schema(operation_id="tags_list")
)
class TagList(generics.ListCreateAPIView):
"""
A view for the collection of tags.
Expand Down Expand Up @@ -98,6 +102,9 @@ def retrieve(self, request, *args, **kwargs):
return services.append_collection_template(response, template_data)


@extend_schema_view(
get=extend_schema(operation_id="feed_tags_list")
)
class FeedTagList(generics.ListAPIView):
"""
A view for a feed-specific collection of user-specific tags.
Expand Down Expand Up @@ -487,6 +494,8 @@ def get_queryset(self):
Overriden to return a custom queryset that is comprised by the feed-specific
group permissions.
"""
if getattr(self, "swagger_fake_view", False):
return FeedGroupPermission.objects.none()
feed = get_object_or_404(Feed, pk=self.kwargs['pk'])
return FeedGroupPermission.objects.filter(feed=feed)

Expand Down Expand Up @@ -582,6 +591,8 @@ def get_queryset(self):
Overriden to return a custom queryset that is comprised by the feed-specific
user permissions.
"""
if getattr(self, "swagger_fake_view", False):
return FeedUserPermission.objects.none()
feed = get_object_or_404(Feed, pk=self.kwargs['pk'])
return FeedUserPermission.objects.filter(feed=feed)

Expand Down Expand Up @@ -678,6 +689,8 @@ def get_queryset(self):
Overriden to return a custom queryset that is comprised by the feed-specific
comments.
"""
if getattr(self, 'swagger_fake_view', False):
return Feed.comments.field.model.objects.none()
feed = get_object_or_404(Feed, pk=self.kwargs['pk'])
return feed.comments.all()

Expand All @@ -702,6 +715,9 @@ def retrieve(self, request, *args, **kwargs):
return services.append_collection_template(response, template_data)


# @extend_schema_view(
# get=extend_schema(operation_id='feed_plugins_instances_list')
# )
class FeedPluginInstanceList(generics.ListAPIView):
"""
A view for the collection of feed-specific plugin instances.
Expand Down
Loading

0 comments on commit f32519a

Please sign in to comment.