Repository: Clinical-Genomics/patientMatcher

Issue:
MatchMaker updated requirements n.3 --> metrics!
New point of the updated MME Service Requirements go into effect March 1:

**Implement MME Metrics API (https://github.com/ga4gh/mme-apis/blob/master/metrics-api.md) and make metrics publicly available**

I guess this will be done by creating a new endpoint. It should be relatively easy.

Discussion:

Patch:
diff --git a/README.md b/README.md
index 0e0caba..b2eed1c 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@ Table of Contents:
 5. [ Server endpoints ](#endpoints)
     - [ Add patient to server (/patient/add) ](#add)
     - [ Delete patient from server (/patient/delete/<patient_id>) ](#delete)
-    - [ Get a list of patients on server (/patient/view) ](#view)
+    - [ Get server stats (/metrics) ](#metrics)
     - [ Get the list of connected nodes (/nodes) ](#node_list)
     - [ Send a match request to server (/match) ](#match)
     - [ Send a match request to external nodes (/match/external/<patient_id>) ](#match_external)
@@ -179,13 +179,15 @@ Matching results where the removed patient is instead listed among the matching
 
 &nbsp;&nbsp;
 
-<a name="view"></a>
-- **/patient/view**.
-&nbsp;Use this endpoint to **get** a list of all patients in the database. Example:
+<a name="metrics"></a>
+- **/metrics**.
+&nbsp;Use this endpoint to **get** database metrics.<br>
+Stats which could be retrieved by a MME service are described [here](https://github.com/ga4gh/mme-apis/blob/master/metrics-api.md)<br>
+Example:
 ```bash
 curl -X GET \
   -H 'X-Auth-Token: custom_token' \
-  localhost:9020/patient/view
+  localhost:9020/metrics
 ```
 &nbsp;&nbsp;
 
diff --git a/instance/config.py b/instance/config.py
index 0317b3f..bebb335 100644
--- a/instance/config.py
+++ b/instance/config.py
@@ -16,6 +16,10 @@ MAX_PHENO_SCORE = 0.5
 # Max results matches returned by server.
 MAX_RESULTS = 5
 
+# Disclaimer. This text is returned along with match results or server metrics
+DISCLAIMER = 'patientMatcher provides data in good faith as a research tool. patientMatcher makes no warranty nor assumes any legal responsibility for any purpose for which the data are used. Users should not attempt in any case to identify patients whose data is returned by the service. Users who intend to publish paper using this software should acknowldge patientMatcher and its developers (https://www.scilifelab.se/facilities/clinical-genomics-stockholm/).'
+
+
 # Email notification params.
 # Required only if you want to send match notifications to patients contacts
 #MAIL_SERVER = smtp_server
diff --git a/patientMatcher/match/handler.py b/patientMatcher/match/handler.py
index 22324be..a58c180 100644
--- a/patientMatcher/match/handler.py
+++ b/patientMatcher/match/handler.py
@@ -55,22 +55,24 @@ def internal_matcher(database, patient_obj, max_pheno_score, max_geno_score, max
     """
     json_pat = json_patient(patient_obj)
     pheno_matches = []
+    pheno_m_keys = []
     geno_matches = []
+    geno_m_keys = []
     matches = []
 
     # phenotype score can be obtained if patient has an associated phenotype (HPO or OMIM terms)
-    if len(patient_obj['features']) or len(patient_obj['disorders']) > 0:
+    if patient_obj.get('features') or patient_obj.get('disorders'):
         LOG.info('Matching phenotypes against database patients..')
         pheno_matches = phenomatch(database, max_pheno_score, patient_obj.get('features',[]), patient_obj.get('disorders',[]))
+        pheno_m_keys = list(pheno_matches.keys())
 
     # genomic score can be obtained if patient has at least one genomic feature
     if len(patient_obj['genomicFeatures']) > 0:
         LOG.info('Matching variants/genes against database patients..')
         geno_matches = genomatch(database, patient_obj['genomicFeatures'], max_geno_score)
+        geno_m_keys = list(geno_matches.keys())
 
     # obtain unique list of all patient IDs returned by the 2 algorithms:
-    pheno_m_keys = list(pheno_matches.keys())
-    geno_m_keys = list(geno_matches.keys())
     unique_patients = list(set(pheno_m_keys + geno_m_keys))
 
     # create matching result objects with combined score from the 2 algorithms
diff --git a/patientMatcher/parse/patient.py b/patientMatcher/parse/patient.py
index 2e2a127..f49b93c 100644
--- a/patientMatcher/parse/patient.py
+++ b/patientMatcher/parse/patient.py
@@ -27,7 +27,7 @@ def mme_patient(json_patient, compute_phenotypes=False):
         'label' : json_patient.get('label'),
         'sex' : json_patient.get('sex'),
         'contact' : json_patient['contact'],
-        'features' : json_patient['features'],
+        'features' : json_patient.get('features'),
         'genomicFeatures' : json_patient.get('genomicFeatures'),
         'disorders' : json_patient.get('disorders'),
         'species' : json_patient.get('species'),
@@ -35,7 +35,7 @@ def mme_patient(json_patient, compute_phenotypes=False):
         'inheritanceMode' : json_patient.get('inheritanceMode')
     }
 
-    if compute_phenotypes: # build Monarch phenotypes from patients' HPO terms
+    if mme_patient['features'] and compute_phenotypes: # build Monarch phenotypes from patients' HPO terms
         hpo_terms = features_to_hpo(json_patient['features'])
         computed_phenotypes = monarch_phenotypes(hpo_terms)
         mme_patient['monarch_phenotypes'] = computed_phenotypes
diff --git a/patientMatcher/server/controllers.py b/patientMatcher/server/controllers.py
index 6f412ab..6390234 100644
--- a/patientMatcher/server/controllers.py
+++ b/patientMatcher/server/controllers.py
@@ -3,19 +3,19 @@ import logging
 from flask import jsonify
 from jsonschema import ValidationError
 from patientMatcher.constants import STATUS_CODES
+from patientMatcher.utils.stats import general_metrics
+from patientMatcher.utils.delete import delete_by_query
 from patientMatcher.utils.patient import patients
 from patientMatcher.parse.patient import json_patient, validate_api, mme_patient
 from patientMatcher.auth.auth import authorize
-from patientMatcher.utils.delete import delete_by_query
 from patientMatcher.match.handler import external_matcher
 
 LOG = logging.getLogger(__name__)
 
-def get_patients(database, patient_ids=None):
-    """return all patients in response to client"""
-    mme_patients = list(patients(database, patient_ids))
-    json_like_patients = [json_patient(mmep) for mmep in mme_patients]
-    return json_like_patients
+def metrics(database):
+    """return database metrics"""
+    db_metrics = general_metrics(database)
+    return db_metrics
 
 
 def get_nodes(database):
diff --git a/patientMatcher/server/views.py b/patientMatcher/server/views.py
index 2c65c1e..40c5f7e 100644
--- a/patientMatcher/server/views.py
+++ b/patientMatcher/server/views.py
@@ -68,14 +68,14 @@ def delete(patient_id):
     return resp
 
 
-@blueprint.route('/patient/view', methods=['GET'])
-def view():
-    """Get all patients in database"""
+@blueprint.route('/metrics', methods=['GET'])
+def metrics():
+    """Get database metrics"""
     resp = None
     if authorize(current_app.db, request):
-        LOG.info('Authorized client requests all patients..')
-        results = controllers.get_patients(database=current_app.db)
-        resp = jsonify(results)
+        LOG.info('Authorized client requests metrics..')
+        results = controllers.metrics(database=current_app.db)
+        resp = jsonify({'metrics' : results, 'disclaimer' : current_app.config.get('DISCLAIMER')})
         resp.status_code = 200
 
     else: # not authorized, return a 401 status code
diff --git a/patientMatcher/utils/stats.py b/patientMatcher/utils/stats.py
new file mode 100644
index 0000000..e6c30e1
--- /dev/null
+++ b/patientMatcher/utils/stats.py
@@ -0,0 +1,95 @@
+# -*- coding: utf-8 -*-
+import logging
+from datetime import date
+
+LOG = logging.getLogger(__name__)
+
+def general_metrics(db):
+    """Create an object with database metrics
+
+    Args:
+        db(pymongo.database.Database)
+
+    Returns:
+        metrics(dict): According to the MME API it should be a dictionary like this:
+            {
+                "metrics": {
+                    "numberOfCases": 0,
+                    "numberOfSubmitters": 0,
+                    "numberOfGenes": 0,
+                    "numberOfUniqueGenes": 0,
+                    "numberOfVariants": 0,
+                    "numberOfUniqueVariants": 0,
+                    "numberOfFeatures": 0,
+                    "numberOfUniqueFeatures": 0,
+                    "numberOfFeatureSets": 0, # endpoint is not returning this, at the moment
+                    "numberOfUniqueGenesMatched": 0,
+                    "numberOfCasesWithDiagnosis": 0,
+                    "numberOfRequestsReceived": 0,
+                    "numberOfPotentialMatchesSent": 0,
+                    "dateGenerated": "2017-08-24",
+
+                },
+                "disclaimer": "Disclaimer text...",
+                "terms": "Terms text..."
+            }
+    """
+    # get gene/occurrence for all genes in db
+    n_genes = 0
+    gene_occurrs = item_occurrence(db, 'genomicFeatures', 'genomicFeatures.gene', 'genomicFeatures.gene.id')
+    for gene_count in gene_occurrs:
+        n_genes += gene_count['count']
+
+    # get numberOfUniqueVariants/occurrence for all variants in db
+    variant_occurr = item_occurrence(db, 'genomicFeatures', 'genomicFeatures.variant', 'genomicFeatures.variant')
+    n_vars = 0
+    for var in variant_occurr:
+        n_vars += var.get('count')
+
+    # get feature/occurrence for all features in db
+    n_feat = 0
+    feat_occurr = item_occurrence(db, 'features', 'features.id')
+    for feat in feat_occurr:
+        n_feat += feat.get('count')
+
+    # include in unique_gene_matches only matches actively returned by the server (internal)
+    match_type = {'match_type':'internal'}
+    unique_gene_matches = db.matches.distinct('results.patients.patient.genomicFeatures.gene', match_type)
+
+    metrics = {
+        'numberOfCases' : db.patients.find().count(),
+        'numberOfSubmitters' : len(db.patients.distinct('contact.href')),
+        'numberOfGenes' : n_genes,
+        'numberOfUniqueGenes': len(db.patients.distinct('genomicFeatures.gene')),
+        'numberOfVariants' : n_vars,
+        'numberOfUniqueVariants' : len(db.patients.distinct('genomicFeatures.variant')),
+        'numberOfFeatures' : n_feat,
+        'numberOfUniqueFeatures' : len(db.patients.distinct('features.id')),
+        'numberOfUniqueGenesMatched' : len(unique_gene_matches),
+        'numberOfCasesWithDiagnosis' : db.patients.find({'disorders': {'$exists': True, '$ne' : []} }).count(),
+        'numberOfRequestsReceived' : db.matches.find({'match_type':'internal'}).count(),
+        'numberOfPotentialMatchesSent' : db.matches.find({'match_type':'internal', 'has_matches': True}).count(),
+        'dateGenerated' : str(date.today())
+    }
+    return metrics
+
+
+def item_occurrence(db, unw1, group, unw2=None):
+    """Get a list of item/occurrence in patient collection
+
+    Args:
+        db(pymongo.database.Database)
+        unw1(string): first nested unwind item
+        group(string): item to group results by
+        unw2(string): second nested unwind item # none if nested level is missing
+
+    Returns:
+        item_occurr(list) example: [{'id':'item_obj', 'count': item_occurrence}, ..]
+    """
+    # create query pipeline
+    pipeline = [{"$unwind": ''.join(['$',unw1])}]
+    if unw2:
+        pipeline.append({"$unwind": ''.join(['$',unw2])})
+    pipeline.append({"$group": {"_id": ''.join(['$',group]), "count": {"$sum": 1}}})
+    item_occurr = list(db.patients.aggregate(pipeline))
+    return item_occurr


Test patch:
diff --git a/tests/server/test_server_responses.py b/tests/server/test_server_responses.py
index c711ea0..a01cbab 100644
--- a/tests/server/test_server_responses.py
+++ b/tests/server/test_server_responses.py
@@ -73,12 +73,12 @@ def test_add_patient(database, json_patients, test_client, test_node):
     assert database['matches'].find().count()==2
 
 
-def test_patient_view(database, test_client):
+def test_metrics(database, test_client, demo_data_path, match_objs):
     """Testing viewing the list of patients on server for authorized users"""
 
     app.db = database
     # send a get request without being authorized
-    response = app.test_client().get('patient/view')
+    response = app.test_client().get('metrics')
     assert response.status_code == 401
 
     # add an authorized client to database
@@ -90,10 +90,32 @@ def test_patient_view(database, test_client):
     clients = app.db['clients'].find({'auth_token' : ok_token }).count()
     assert clients > 0
 
-    # if a valid token is provided the server should return a status code 200 (success)
-    auth_response = app.test_client().get('patient/view', headers = auth_headers(ok_token))
+    # load demo data of 50 test patients
+    inserted_ids = load_demo(demo_data_path, database)
+    assert len(inserted_ids) == 50 # 50 test cases should be loaded
+
+    # load mock matches into database
+    database.matches.insert_many(match_objs)
+    assert database.matches.find().count() == 3
+
+    # if a valid token is provided the server should return metrics with patient data and matching results
+    auth_response = app.test_client().get('metrics', headers = auth_headers(ok_token))
     assert auth_response.status_code == 200
 
+    data = json.loads(auth_response.data)
+    assert data['disclaimer'] # disclaimer should be returned
+    metrics = data['metrics']
+
+    assert metrics['numberOfCases'] == 50
+    assert metrics['numberOfSubmitters'] > 0
+    assert metrics['numberOfGenes'] > metrics['numberOfUniqueGenes']
+    assert metrics['numberOfVariants'] > metrics['numberOfUniqueVariants']
+    assert metrics['numberOfFeatures'] > metrics['numberOfUniqueFeatures']
+    assert metrics['numberOfCasesWithDiagnosis'] >0
+    assert metrics['numberOfUniqueGenesMatched'] == 0 # no gene was provided in match_obj results
+    assert metrics['numberOfRequestsReceived'] == 2 # Sent 2 requests
+    assert metrics['numberOfPotentialMatchesSent'] == 1 # Just one has returned results
+
 
 def test_nodes_view(database, test_node, test_client):
     """testing viewing the list of connected nodes as an authenticated client"""