Skip to content

Commit

Permalink
Merge pull request #186 from bento-platform/develop
Browse files Browse the repository at this point in the history
Version 1.3.1
  • Loading branch information
zxenia authored Feb 2, 2021
2 parents 7f6c469 + 7c027e8 commit 1efea32
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 3 deletions.
2 changes: 1 addition & 1 deletion chord_metadata_service/package.cfg
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[package]
name = katsu
version = 1.3.0
version = 1.3.1
authors = Ksenia Zaytseva, David Lougheed, Simon Chénard, Romain Grégoire
90 changes: 90 additions & 0 deletions chord_metadata_service/phenopackets/api_views.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from collections import Counter
from rest_framework import viewsets
from rest_framework.settings import api_settings
from rest_framework.decorators import api_view, permission_classes
Expand All @@ -7,6 +8,8 @@

from chord_metadata_service.restapi.api_renderers import PhenopacketsRenderer, FHIRRenderer
from chord_metadata_service.restapi.pagination import LargeResultsSetPagination
from chord_metadata_service.restapi.utils import parse_individual_age
from chord_metadata_service.chord.permissions import OverrideOrSuperUserOnly
from chord_metadata_service.phenopackets.schemas import PHENOPACKET_SCHEMA
from . import models as m, serializers as s, filters as f

Expand Down Expand Up @@ -232,3 +235,90 @@ def get_chord_phenopacket_schema(_request):
Chord phenopacket schema that can be shared with data providers.
"""
return Response(PHENOPACKET_SCHEMA)


@api_view(["GET"])
@permission_classes([OverrideOrSuperUserOnly])
def phenopackets_overview(_request):
"""
get:
Overview of all Phenopackets in the database
"""
phenopackets = m.Phenopacket.objects.all()

diseases_counter = Counter()
phenotypic_features_counter = Counter()

biosamples_set = set()
individuals_set = set()

biosamples_taxonomy = Counter()
biosamples_sampled_tissue = Counter()

individuals_sex = Counter()
individuals_k_sex = Counter()
individuals_taxonomy = Counter()
individuals_age = Counter()

def count_individual(ind):
individuals_set.add(ind.id)
individuals_sex.update((ind.sex,))
individuals_k_sex.update((ind.karyotypic_sex,))
if ind.age is not None:
individuals_age.update((parse_individual_age(ind.age),))
if ind.taxonomy is not None:
individuals_taxonomy.update((ind.taxonomy["label"],))

for p in phenopackets.prefetch_related("biosamples"):
for b in p.biosamples.all():
biosamples_set.add(b.id)
biosamples_sampled_tissue.update((b.sampled_tissue["label"],))

if b.taxonomy is not None:
biosamples_taxonomy.update((b.taxonomy["label"],))

# TODO decide what to do with nested Phenotypic features and Subject in Biosample
# This might serve future use cases that Biosample as a have main focus of study
# for pf in b.phenotypic_features.all():
# phenotypic_features_counter.update((pf.pftype["label"],))

# according to Phenopackets standard
# phenotypic features also can be linked to a Biosample
# but we count them here because all our use cases current have them linked to Phenopacket not biosample
for d in p.diseases.all():
diseases_counter.update((d.term["label"],))

for pf in p.phenotypic_features.all():
phenotypic_features_counter.update((pf.pftype["label"],))

# Currently, phenopacket subject is required so we can assume it's not None
count_individual(p.subject)

return Response({
"phenopackets": phenopackets.count(),
"data_type_specific": {
"biosamples": {
"count": len(biosamples_set),
"taxonomy": dict(biosamples_taxonomy),
"sampled_tissue": dict(biosamples_sampled_tissue),
},
"diseases": {
# count is a number of unique disease terms (not all diseases in the database)
"count": len(diseases_counter.keys()),
"term": dict(diseases_counter)
},
"individuals": {
"count": len(individuals_set),
"sex": {k: individuals_sex[k] for k in (s[0] for s in m.Individual.SEX)},
"karyotypic_sex": {k: individuals_k_sex[k] for k in (s[0] for s in m.Individual.KARYOTYPIC_SEX)},
"taxonomy": dict(individuals_taxonomy),
"age": dict(individuals_age),
# TODO: how to count age: it can be represented by three different schemas
},
"phenotypic_features": {
# count is a number of unique phenotypic feature types (not all pfs in the database)
"count": len(phenotypic_features_counter.keys()),
"type": dict(phenotypic_features_counter)
},
}
})
38 changes: 38 additions & 0 deletions chord_metadata_service/phenopackets/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,3 +282,41 @@ def test_interpretation(self):
response = get_response('interpretation-list',
self.interpretation)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)


class OverviewTest(APITestCase):

def setUp(self) -> None:
# create 2 phenopackets for 2 individuals; each individual has 1 biosample;
# one of phenopackets has 1 phenotypic feature and 1 disease
self.individual_1 = m.Individual.objects.create(**c.VALID_INDIVIDUAL_1)
self.individual_2 = m.Individual.objects.create(**c.VALID_INDIVIDUAL_2)
self.metadata_1 = m.MetaData.objects.create(**c.VALID_META_DATA_1)
self.metadata_2 = m.MetaData.objects.create(**c.VALID_META_DATA_2)
self.phenopacket_1 = m.Phenopacket.objects.create(
**c.valid_phenopacket(subject=self.individual_1, meta_data=self.metadata_1)
)
self.phenopacket_2 = m.Phenopacket.objects.create(
id='phenopacket:2', subject=self.individual_2, meta_data=self.metadata_2
)
self.disease = m.Disease.objects.create(**c.VALID_DISEASE_1)
self.procedure = m.Procedure.objects.create(**c.VALID_PROCEDURE_1)
self.biosample_1 = m.Biosample.objects.create(**c.valid_biosample_1(self.individual_1, self.procedure))
self.biosample_2 = m.Biosample.objects.create(**c.valid_biosample_2(self.individual_2, self.procedure))
self.phenotypic_feature = m.PhenotypicFeature.objects.create(
**c.valid_phenotypic_feature(self.biosample_1, self.phenopacket_1)
)
self.phenopacket_1.biosamples.set([self.biosample_1])
self.phenopacket_2.biosamples.set([self.biosample_2])
self.phenopacket_1.diseases.set([self.disease])

def test_overview(self):
response = self.client.get('/api/overview')
response_obj = response.json()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIsInstance(response_obj, dict)
self.assertEqual(response_obj['phenopackets'], 2)
self.assertEqual(response_obj['data_type_specific']['individuals']['count'], 2)
self.assertEqual(response_obj['data_type_specific']['biosamples']['count'], 2)
self.assertEqual(response_obj['data_type_specific']['phenotypic_features']['count'], 1)
self.assertEqual(response_obj['data_type_specific']['diseases']['count'], 1)
5 changes: 3 additions & 2 deletions chord_metadata_service/restapi/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@
from chord_metadata_service.phenopackets import api_views as phenopacket_views
from chord_metadata_service.resources import api_views as resources_views


__all__ = ["router", "urlpatterns"]


router = routers.DefaultRouter(trailing_slash=False)

# CHORD app urls
Expand Down Expand Up @@ -64,4 +62,7 @@
name="experiment-schema"),
path('mcode_schema', mcode_views.get_mcode_schema,
name="mcode-schema"),
# overview
path('overview', phenopacket_views.phenopackets_overview,
name="overview"),
]
20 changes: 20 additions & 0 deletions chord_metadata_service/restapi/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,23 @@ def parse_onset(onset):
return f"{onset['start']['age']} - {onset['end']['age']}"
else:
return None


def parse_duration(string):
""" Returns years integer. """
string = string.split('P')[-1]
return int(float(string.split('Y')[0]))


def parse_individual_age(age_obj):
""" Parses two possible age representations and returns average age or age as integer. """
if 'start' in age_obj:
start_age = parse_duration(age_obj['start']['age'])
end_age = parse_duration(age_obj['end']['age'])
# for the duration calculate the average age
age = (start_age + end_age) // 2
elif isinstance(age_obj, str):
age = parse_duration(age_obj)
else:
raise ValueError(f"Error: {age_obj} format not supported")
return age

0 comments on commit 1efea32

Please sign in to comment.