Commit b0ae5b9e authored by Kenzo-Hugo Hillion's avatar Kenzo-Hugo Hillion
Browse files

Merge branch '43-doc-swagger' into 'dev'

Generate Documentation for the backend API

Closes #43

See merge request !10
parents 70c740a1 ba20e779
Pipeline #13634 passed with stages
in 2 minutes and 11 seconds
...@@ -4,50 +4,52 @@ url = "https://pypi.org/simple" ...@@ -4,50 +4,52 @@ url = "https://pypi.org/simple"
verify_ssl = true verify_ssl = true
[dev-packages] [dev-packages]
atomicwrites = "==1.3.0" atomicwrites = "*"
attrs = "==19.1.0" attrs = "*"
coverage = "==4.5.3" coverage = "*"
entrypoints = "==0.3" entrypoints = "*"
flake8 = "==3.7.7" flake8 = "*"
importlib-metadata = "==0.18" importlib-metadata = "*"
kiwisolver = "==1.1.0" kiwisolver = "*"
mccabe = "==0.6.1" mccabe = "*"
more-itertools = "==7.0.0" more-itertools = "*"
pluggy = "==0.12.0" pluggy = "*"
py = "==1.8.0" py = "*"
pycodestyle = "==2.5.0" pycodestyle = "*"
pyflakes = "==2.1.1" pyflakes = "*"
pyparsing = "==2.4.0" pyparsing = "*"
pytest = "==4.6.3" pytest = "*"
pytest-cov = "==2.7.1" pytest-cov = "*"
pytest-django = "==3.5.0" pytest-django = "*"
wcwidth = "==0.1.7" wcwidth = "*"
zipp = "==0.5.1" zipp = "*"
Cycler = "==0.10.0" Cycler = "*"
jupyter = "*" jupyter = "*"
[packages] [packages]
certifi = "==2019.6.16" certifi = "*"
chardet = "==3.0.4" chardet = "*"
django-cors-headers = "==3.0.2" django-cors-headers = "*"
django-environ = "==0.4.5" django-environ = "*"
django-extensions = "==2.1.7" django-extensions = "*"
django-filter = "==2.1.0" django-filter = "*"
djangorestframework = "==3.9.4" djangorestframework = "*"
djangorestframework-jwt = "==1.11.0" djangorestframework-jwt = "*"
idna = "==2.8" idna = "*"
numpy = "==1.16.4" numpy = "*"
pandas = "==0.24.2" pandas = "*"
psycopg2 = "==2.8.2" psycopg2 = "*"
python-dateutil = "==2.8.0" python-dateutil = "*"
pytz = "==2019.1" pytz = "*"
requests = "==2.22.0" requests = "*"
six = "==1.12.0" six = "*"
sqlparse = "==0.3.0" sqlparse = "*"
urllib3 = "==1.25.3" urllib3 = "*"
Django = "==2.2.1" Django = "*"
PyJWT = "==1.7.1" PyJWT = "*"
metagenedb = {editable = true,path = "."} metagenedb = {editable = true,path = "."}
drf-yasg = "*"
packaging = "*"
[requires] [requires]
python_version = "3.7" python_version = "3.7"
This diff is collapsed.
import pandas as pd import pandas as pd
from rest_framework.viewsets import GenericViewSet from drf_yasg import openapi
from rest_framework import mixins from drf_yasg.utils import swagger_auto_schema
from rest_framework import status from rest_framework import status
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.permissions import IsAdminUser
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from metagenedb.common.utils.df_operations import get_mask from metagenedb.common.utils.df_operations import get_mask
from metagenedb.apps.catalog.models import Gene from metagenedb.apps.catalog.models import Gene
from metagenedb.apps.catalog.serializers import GeneSerializer from metagenedb.apps.catalog.serializers import GeneSerializer
class GeneViewSet(mixins.ListModelMixin, class DocGeneLength(object):
mixins.RetrieveModelMixin, """
GenericViewSet): Define response for API documentation of gene length distribution method
{
"results": {
"counts": [
0,
887,
],
"labels": [
"0-9999",
"10000-19999",
"20000-29999",
]
}
}
"""
window_size_param = openapi.Parameter('window_size', in_=openapi.IN_QUERY, description='Size of the window.',
type=openapi.TYPE_INTEGER, default=10000)
counts = openapi.Schema(type="array", items=openapi.Schema(type="int"),
description="Counts for every window_size")
labels = openapi.Schema(type="array", items=openapi.Schema(type="char"),
description="Corresponding windows")
results = openapi.Schema(type="object", properties={'counts': counts, 'labels': labels},
description="results of your request")
gene_length_schema = openapi.Schema(type="object", properties={'results': results})
gene_length_response = openapi.Response('Get the distribution of gene length for a given window size',
schema=gene_length_schema)
class GeneViewSet(ModelViewSet):
queryset = Gene.objects.all() queryset = Gene.objects.all()
serializer_class = GeneSerializer serializer_class = GeneSerializer
GENE_LENGTH_COL = 'gene_length' GENE_LENGTH_COL = 'length'
def get_permissions(self):
if self.action == 'create':
self.permission_classes = [IsAdminUser, ]
return super(self.__class__, self).get_permissions()
def _count_windows(self, df, window_size=10000, window_col=GENE_LENGTH_COL): def _count_windows(self, df, window_size=10000, window_col=GENE_LENGTH_COL):
""" """
...@@ -36,6 +71,14 @@ class GeneViewSet(mixins.ListModelMixin, ...@@ -36,6 +71,14 @@ class GeneViewSet(mixins.ListModelMixin,
'labels': labels 'labels': labels
} }
@swagger_auto_schema(
manual_parameters=[DocGeneLength.window_size_param],
responses={
'200': DocGeneLength.gene_length_response,
'204': 'No genes on the catalog to build the distribution'
},
operation_id='Gene length distribution',
)
@action(methods=['get'], detail=False) @action(methods=['get'], detail=False)
def gene_length(self, request, window_size=10000): def gene_length(self, request, window_size=10000):
if 'window_size' in request.query_params: if 'window_size' in request.query_params:
......
...@@ -42,7 +42,7 @@ class TestGenes(TestCase): ...@@ -42,7 +42,7 @@ class TestGenes(TestCase):
class TestCountWindows(TestCase): class TestCountWindows(TestCase):
def setUp(self): def setUp(self):
self.window_col = "gene_length" self.window_col = "length"
self.df = pd.DataFrame( self.df = pd.DataFrame(
[22, 29, 35], [22, 29, 35],
columns=[self.window_col] columns=[self.window_col]
......
...@@ -6,7 +6,7 @@ from metagenedb.apps.catalog.models import Gene ...@@ -6,7 +6,7 @@ from metagenedb.apps.catalog.models import Gene
@admin.register(Gene) @admin.register(Gene)
class GeneAdmin(admin.ModelAdmin): class GeneAdmin(admin.ModelAdmin):
list_display = ('gene_id', 'gene_length', 'get_functions', 'get_taxonomy') list_display = ('gene_id', 'length', 'get_functions', 'get_taxonomy')
search_fields = ('gene_id',) search_fields = ('gene_id',)
def get_functions(self, obj): def get_functions(self, obj):
......
# Generated by Django 2.2.1 on 2019-08-07 14:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('catalog', '0006_gene_taxonomy'),
]
operations = [
migrations.AlterField(
model_name='gene',
name='gene_length',
field=models.PositiveIntegerField(),
),
migrations.RenameField(
model_name='gene',
old_name='gene_length',
new_name='length',
),
]
...@@ -5,7 +5,7 @@ from .function import Function ...@@ -5,7 +5,7 @@ from .function import Function
class Gene(models.Model): class Gene(models.Model):
gene_id = models.CharField(max_length=100, unique=True, db_index=True) gene_id = models.CharField(max_length=100, unique=True, db_index=True)
gene_length = models.IntegerField() length = models.PositiveIntegerField()
functions = models.ManyToManyField(Function) functions = models.ManyToManyField(Function)
taxonomy = models.ForeignKey( taxonomy = models.ForeignKey(
'Taxonomy', related_name='genes', 'Taxonomy', related_name='genes',
......
...@@ -4,7 +4,7 @@ from metagenedb.apps.catalog.serializers import FunctionSerializer ...@@ -4,7 +4,7 @@ from metagenedb.apps.catalog.serializers import FunctionSerializer
class GeneSerializer(serializers.ModelSerializer): class GeneSerializer(serializers.ModelSerializer):
functions = FunctionSerializer(many=True, read_only=True) functions = FunctionSerializer(many=True, required=False)
taxonomy = serializers.SlugRelatedField( taxonomy = serializers.SlugRelatedField(
queryset=Taxonomy.objects.all(), queryset=Taxonomy.objects.all(),
slug_field='tax_id', slug_field='tax_id',
...@@ -13,4 +13,4 @@ class GeneSerializer(serializers.ModelSerializer): ...@@ -13,4 +13,4 @@ class GeneSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Gene model = Gene
fields = ('gene_id', 'gene_length', 'functions', 'taxonomy') fields = ('gene_id', 'length', 'functions', 'taxonomy')
...@@ -33,7 +33,7 @@ class IGCLineParser(object): ...@@ -33,7 +33,7 @@ class IGCLineParser(object):
return { return {
'igc_id': gene_info[0], 'igc_id': gene_info[0],
'gene_id': gene_info[1], 'gene_id': gene_info[1],
'gene_length': gene_info[2], 'length': gene_info[2],
'gene_completeness_status': gene_info[3], 'gene_completeness_status': gene_info[3],
'cohort_origin': gene_info[4], 'cohort_origin': gene_info[4],
'taxo_phylum': gene_info[5], 'taxo_phylum': gene_info[5],
......
...@@ -9,7 +9,7 @@ class TestIGCLineParser(TestCase): ...@@ -9,7 +9,7 @@ class TestIGCLineParser(TestCase):
raw_data = [ raw_data = [
'gene_id', 'gene_id',
'gene_name', 'gene_name',
'gene_length', 'length',
'gene_completeness_status', 'gene_completeness_status',
'cohort_origin', 'cohort_origin',
'taxo_phylum', 'taxo_phylum',
...@@ -26,7 +26,7 @@ class TestIGCLineParser(TestCase): ...@@ -26,7 +26,7 @@ class TestIGCLineParser(TestCase):
expected_dict = { expected_dict = {
'igc_id': raw_data[0], 'igc_id': raw_data[0],
'gene_id': raw_data[1], 'gene_id': raw_data[1],
'gene_length': raw_data[2], 'length': raw_data[2],
'gene_completeness_status': raw_data[3], 'gene_completeness_status': raw_data[3],
'cohort_origin': raw_data[4], 'cohort_origin': raw_data[4],
'taxo_phylum': raw_data[5], 'taxo_phylum': raw_data[5],
......
...@@ -19,6 +19,7 @@ INSTALLED_APPS = [ ...@@ -19,6 +19,7 @@ INSTALLED_APPS = [
'rest_framework', 'rest_framework',
'django_extensions', 'django_extensions',
'corsheaders', 'corsheaders',
'drf_yasg',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
...@@ -105,7 +106,7 @@ REST_FRAMEWORK = { ...@@ -105,7 +106,7 @@ REST_FRAMEWORK = {
# 'rest_framework.authentication.BasicAuthentication', # 'rest_framework.authentication.BasicAuthentication',
), ),
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 100 'PAGE_SIZE': 100,
} }
......
...@@ -15,9 +15,29 @@ Including another URLconf ...@@ -15,9 +15,29 @@ Including another URLconf
""" """
from django.contrib import admin from django.contrib import admin
from django.urls import include, path from django.urls import include, path
from django.conf.urls import url
from rest_framework import permissions
from drf_yasg.views import get_schema_view
from drf_yasg import openapi
schema_view = get_schema_view(
openapi.Info(
title="Metagenedb API",
default_version='v1',
description="API documentation for metagenedb",
contact=openapi.Contact(email="kehillio@pasteur.fr"),
license=openapi.License(name="License not defined"),
),
public=True,
permission_classes=(permissions.AllowAny,),
)
urlpatterns = [ urlpatterns = [
path('api/', include(('metagenedb.api.urls', 'api'))), path('api/', include(('metagenedb.api.urls', 'api'))),
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
url(r'^swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'),
url(r'^swagger/$', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
url(r'^redoc/$', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
] ]
...@@ -22,7 +22,7 @@ _LOGGER = logging.getLogger(__name__) ...@@ -22,7 +22,7 @@ _LOGGER = logging.getLogger(__name__)
PHYLUM_COL = 'taxo_phylum' PHYLUM_COL = 'taxo_phylum'
GENUS_COL = 'taxo_genus' GENUS_COL = 'taxo_genus'
SELECTED_KEYS = ['gene_id', 'gene_length', 'kegg_ko', PHYLUM_COL, GENUS_COL] SELECTED_KEYS = ['gene_id', 'length', 'kegg_ko', PHYLUM_COL, GENUS_COL]
def parse_gene(raw_line, selected_keys=SELECTED_KEYS): def parse_gene(raw_line, selected_keys=SELECTED_KEYS):
......
...@@ -13,7 +13,7 @@ class TestParseGene(TestCase): ...@@ -13,7 +13,7 @@ class TestParseGene(TestCase):
raw_data = [ raw_data = [
'gene_id', 'gene_id',
'gene_name', 'gene_name',
'gene_length', 'length',
'gene_completeness_status', 'gene_completeness_status',
'cohort_origin', 'cohort_origin',
'taxo_phylum', 'taxo_phylum',
...@@ -34,7 +34,7 @@ class TestParseGene(TestCase): ...@@ -34,7 +34,7 @@ class TestParseGene(TestCase):
""" """
expected_dict = { expected_dict = {
'gene_id': 'gene_name', 'gene_id': 'gene_name',
'gene_length': 'gene_length', 'length': 'length',
'kegg_ko': 'kegg', 'kegg_ko': 'kegg',
'taxo_phylum': 'taxo_phylum', 'taxo_phylum': 'taxo_phylum',
'taxo_genus': 'taxo_genus', 'taxo_genus': 'taxo_genus',
...@@ -46,10 +46,10 @@ class TestParseGene(TestCase): ...@@ -46,10 +46,10 @@ class TestParseGene(TestCase):
""" """
This test should failed and need to be updated when SELECTED_KEYS are changed This test should failed and need to be updated when SELECTED_KEYS are changed
""" """
selected_keys = ['gene_id', 'gene_length'] selected_keys = ['gene_id', 'length']
expected_dict = { expected_dict = {
'gene_id': 'gene_name', 'gene_id': 'gene_name',
'gene_length': 'gene_length' 'length': 'length'
} }
tested_dict = parse_gene(self.raw_line, selected_keys=selected_keys) tested_dict = parse_gene(self.raw_line, selected_keys=selected_keys)
self.assertDictEqual(tested_dict, expected_dict) self.assertDictEqual(tested_dict, expected_dict)
...@@ -58,10 +58,10 @@ class TestParseGene(TestCase): ...@@ -58,10 +58,10 @@ class TestParseGene(TestCase):
""" """
Unknown key should be ignored Unknown key should be ignored
""" """
selected_keys = ['gene_id', 'gene_length', 'secret_code'] selected_keys = ['gene_id', 'length', 'secret_code']
expected_dict = { expected_dict = {
'gene_id': 'gene_name', 'gene_id': 'gene_name',
'gene_length': 'gene_length' 'length': 'length'
} }
tested_dict = parse_gene(self.raw_line, selected_keys=selected_keys) tested_dict = parse_gene(self.raw_line, selected_keys=selected_keys)
self.assertDictEqual(tested_dict, expected_dict) self.assertDictEqual(tested_dict, expected_dict)
...@@ -72,15 +72,15 @@ class TestUpsertGene(APITestCase): ...@@ -72,15 +72,15 @@ class TestUpsertGene(APITestCase):
def test_insert_valid_gene_no_kegg(self): def test_insert_valid_gene_no_kegg(self):
valid_gene = { valid_gene = {
'gene_id': 'test_gene01', 'gene_id': 'test_gene01',
'gene_length': 3556 'length': 3556
} }
upsert_gene(valid_gene) upsert_gene(valid_gene)
self.assertEqual(Gene.objects.all().count(), 1) self.assertEqual(Gene.objects.all().count(), 1)
def test_insert_invalid_gene_length(self): def test_insert_invalid_length(self):
invalid_gene = { invalid_gene = {
'gene_id': 'test_gene01', 'gene_id': 'test_gene01',
'gene_length': 'wrong_format' 'length': 'wrong_format'
} }
with self.assertRaises(ValidationError) as context: # noqa with self.assertRaises(ValidationError) as context: # noqa
upsert_gene(invalid_gene) upsert_gene(invalid_gene)
...@@ -88,16 +88,16 @@ class TestUpsertGene(APITestCase): ...@@ -88,16 +88,16 @@ class TestUpsertGene(APITestCase):
def test_update_gene(self): def test_update_gene(self):
valid_gene = { valid_gene = {
'gene_id': 'test_gene01', 'gene_id': 'test_gene01',
'gene_length': 3556 'length': 3556
} }
updated_gene = { updated_gene = {
'gene_id': 'test_gene01', 'gene_id': 'test_gene01',
'gene_length': 356 'length': 356
} }
upsert_gene(valid_gene) upsert_gene(valid_gene)
self.assertEqual(Gene.objects.get(gene_id="test_gene01").gene_length, 3556) self.assertEqual(Gene.objects.get(gene_id="test_gene01").length, 3556)
upsert_gene(updated_gene) upsert_gene(updated_gene)
self.assertEqual(Gene.objects.get(gene_id="test_gene01").gene_length, 356) self.assertEqual(Gene.objects.get(gene_id="test_gene01").length, 356)
class TestSelectTaxonomy(TestCase): class TestSelectTaxonomy(TestCase):
...@@ -114,13 +114,13 @@ class TestSelectTaxonomy(TestCase): ...@@ -114,13 +114,13 @@ class TestSelectTaxonomy(TestCase):
def test_both_unknown(self): def test_both_unknown(self):
gene_dict = { gene_dict = {
'gene_id': 'gene', 'gene_id': 'gene',
'gene_length': 135, 'length': 135,
'taxo_phylum': 'unknown', 'taxo_phylum': 'unknown',
'taxo_genus': 'unknown' 'taxo_genus': 'unknown'
} }
expected_dict = { expected_dict = {
'gene_id': 'gene', 'gene_id': 'gene',
'gene_length': 135 'length': 135
} }
tested_dict = select_taxonomy(gene_dict) tested_dict = select_taxonomy(gene_dict)
self.assertDictEqual(tested_dict, expected_dict) self.assertDictEqual(tested_dict, expected_dict)
...@@ -24,6 +24,14 @@ spec: ...@@ -24,6 +24,14 @@ spec:
serviceName: backend serviceName: backend
servicePort: 8000 servicePort: 8000
- path: /api - path: /api
backend:
serviceName: backend
servicePort: 8000
- path: /swagger
backend:
serviceName: backend
servicePort: 8000
- path: /redoc
backend: backend:
serviceName: backend serviceName: backend
servicePort: 8000 servicePort: 8000
\ No newline at end of file
...@@ -48,9 +48,7 @@ services: ...@@ -48,9 +48,7 @@ services:
- NODE_ENV=development - NODE_ENV=development
nginx: nginx:
build: image: nginx:1.13.12-alpine
context: .
dockerfile: nginx/dev/Dockerfile
ports: ports:
- "80:80" - "80:80"
depends_on: depends_on:
......
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
> >
<template v-slot:items="props"> <template v-slot:items="props">
<td>{{ props.item.gene_id }}</td> <td>{{ props.item.gene_id }}</td>