Skip to content

Commit 95f8f84

Browse files
committed
added Mixin function for improved code
1 parent bad1284 commit 95f8f84

File tree

2 files changed

+83
-93
lines changed

2 files changed

+83
-93
lines changed

src/handlers/api.py

Lines changed: 77 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from utils.notification import SlackNewAPIMessage, SlackNewTranslatorAPIMessage
2525
from utils.metakg.parser import MetaKGParser
2626
from utils.metakg.metakg_errors import MetadataRetrievalError
27+
from utils.decoder import to_dict
2728

2829
logger = logging.getLogger("smartAPI")
2930

@@ -44,7 +45,6 @@ def _(self, *args, **kwargs):
4445
class BaseHandler(BioThingsAuthnMixin, BaseAPIHandler):
4546
pass
4647

47-
4848
class AuthHandler(BaseHandler):
4949
def set_cache_header(self, cache_value):
5050
# disabel cache for auth-related handlers
@@ -382,7 +382,57 @@ def post(self):
382382
else:
383383
raise HTTPError(400, reason="Missing required form field: id")
384384

385+
class MetaKGHandlerMixin:
386+
"""
387+
Mixin to provide reusable logic for filtering API information.
388+
"""
389+
def get_filtered_api(self, api_dict):
390+
"""Extract and return filtered API information."""
391+
api_info = api_dict.get("api", api_dict) # Handle both formats
392+
bte = getattr(self.args, "bte", 0)
393+
api_details = getattr(self.args, "api_details", 0)
394+
395+
# Default structure to preserve top-level keys
396+
filtered_dict = {
397+
key: api_dict.get(key)
398+
for key in ["subject", "object", "predicate", "subject_prefix", "object_prefix"]
399+
if key in api_dict
400+
}
401+
402+
# Determine filtered API structure based on `bte` and `api_details`
403+
if bte == 1 and api_details == 0:
404+
filtered_api = {
405+
**({"name": api_info.get("name")} if "name" in api_info else {}),
406+
**(
407+
{"smartapi": {"id": api_info.get("smartapi", {}).get("id", None)}}
408+
if "smartapi" in api_info
409+
else {"smartapi": {"id": None}}
410+
),
411+
"bte": api_info.get("bte", {}),
412+
}
413+
elif api_details == 1:
414+
# Covers both (bte=0, api_details=1) and (bte=1, api_details=1)
415+
filtered_api = api_info.copy()
416+
if bte == 0:
417+
filtered_api.pop("bte", None)
418+
else: # bte == 0 and api_details == 0
419+
filtered_api = {
420+
**({"name": api_info.get("name")} if "name" in api_info else {}),
421+
**(
422+
{"smartapi": {"id": api_info.get("smartapi", {}).get("id", None)}}
423+
if "smartapi" in api_info
424+
else {"smartapi": {"id": None}}
425+
),
426+
}
427+
428+
# Add the filtered 'api' key to the preserved top-level structure
429+
filtered_dict["api"] = filtered_api
430+
431+
# Remove 'bte' from 'api' and move it to the top level
432+
if "bte" in filtered_dict["api"]:
433+
filtered_dict["bte"] = filtered_dict["api"].pop("bte")
385434

435+
return filtered_dict
386436
class MetaKGQueryHandler(QueryHandler):
387437
"""
388438
Support metakg queries with biolink model's semantic descendants
@@ -462,27 +512,6 @@ async def get(self, *args, **kwargs):
462512

463513
await super().get(*args, **kwargs)
464514

465-
def get_filtered_api(self, api_dict):
466-
"""Extract and return filtered API information."""
467-
api_info = api_dict
468-
if not self.args.bte and not self.args.api_details: # no bte and no api details
469-
filtered_api= {
470-
**({"name": api_info["name"]} if "name" in api_info else {}),
471-
**({"smartapi": {"id": api_info["smartapi"]["id"]}} if "smartapi" in api_info and "id" in api_info["smartapi"] else {})
472-
}
473-
elif self.args.bte and not self.args.api_details : # bte and no api details
474-
filtered_api= {
475-
**({"name": api_info["name"]} if "name" in api_info else {}),
476-
**({"smartapi": {"id": api_info["smartapi"]["id"]}} if "smartapi" in api_info and "id" in api_info["smartapi"] else {}),
477-
'bte': api_info.get('bte', {})
478-
}
479-
elif not self.args.bte and self.args.api_details: # no bte and api details
480-
api_info.pop('bte', None)
481-
filtered_api = api_info
482-
else:
483-
filtered_api = api_info
484-
return filtered_api
485-
486515
def process_apis(self, apis):
487516
"""Process each API dict based on provided args."""
488517
if isinstance(apis, list):
@@ -730,53 +759,6 @@ def initialize(self, *args, **kwargs):
730759
# change the default query pipeline from self.biothings.pipeline
731760
self.pipeline = MetaKGQueryPipeline(ns=self.biothings)
732761

733-
def get_filtered_api(self, api_dict):
734-
"""Extract and return filtered API information."""
735-
api_info = api_dict["api"]
736-
bte = self.args.bte
737-
api_details = self.args.api_details
738-
739-
# Default structure to preserve top-level keys
740-
filtered_dict = {
741-
key: api_dict.get(key)
742-
for key in ["subject", "object", "predicate", "subject_prefix", "object_prefix"]
743-
}
744-
745-
# Determine filtered API structure based on `bte` and `api_details`
746-
if bte == 1 and api_details == 0:
747-
filtered_api = {
748-
**({"name": api_info["name"]} if "name" in api_info else {}),
749-
**(
750-
{"smartapi": {"id": api_info["smartapi"]["id"]}}
751-
if "smartapi" in api_info and "id" in api_info["smartapi"]
752-
else {}
753-
),
754-
"bte": api_info.get("bte", {}),
755-
}
756-
elif api_details == 1:
757-
# Covers both (bte=0, api_details=1) and (bte=1, api_details=1)
758-
filtered_api = api_info.copy()
759-
if bte == 0:
760-
filtered_api.pop("bte", None)
761-
else: # bte == 0 and api_details == 0
762-
filtered_api = {
763-
**({"name": api_info["name"]} if "name" in api_info else {}),
764-
**(
765-
{"smartapi": {"id": api_info["smartapi"]["id"]}}
766-
if "smartapi" in api_info and "id" in api_info["smartapi"]
767-
else {}
768-
),
769-
}
770-
771-
# Add the filtered 'api' key to the preserved top-level structure
772-
filtered_dict["api"] = filtered_api
773-
774-
# Remove 'bte' from 'api' and move it to the top level
775-
if "bte" in filtered_dict["api"]:
776-
filtered_dict["bte"] = filtered_dict["api"].pop("bte")
777-
778-
return filtered_dict
779-
780762
def process_apis(self, apis):
781763
"""Process each API dict based on provided args."""
782764
if isinstance(apis, list):
@@ -831,37 +813,49 @@ async def get(self, *args, **kwargs):
831813
for i, api_dict in enumerate(combined_data):
832814
filtered_api = self.get_filtered_api(api_dict)
833815
combined_data[i] = filtered_api
816+
# parser does not pick up this information, so we add it here
817+
if self.args.api_details == 1:
818+
for data_dict in combined_data:
819+
if "metadata" in data_dict["api"]["smartapi"] and data_dict["api"]["smartapi"]["metadata"] is None:
820+
data_dict["api"]["smartapi"]["metadata"] = self.args.url
834821

835822
response = {
836-
"took": 1,
837823
"total": len(combined_data),
838-
"max_score": 1,
839824
"hits": combined_data,
840825
}
841826

842-
self.set_header("Content-Type", "application/json")
843-
self.write(response)
827+
self.finish(response)
844828

845829
async def post(self, *args, **kwargs):
846830
if not self.request.body:
847831
raise HTTPError(400, reason="Request body cannot be empty.")
832+
content_type = self.request.headers.get("Content-Type", "")
833+
data_body = self.request.body
848834

849-
# Attempt to parse JSON body
850-
try:
851-
data = json.loads(self.request.body)
852-
except json.JSONDecodeError:
853-
raise HTTPError(400, reason=f"Unexcepted value for api_details, {self.get_argument('api_details')}. Please enter integer, 0 or 1.")
854-
855-
# Ensure the parsed data is a dictionary
835+
if content_type == "application/json":
836+
try:
837+
data = to_dict(data_body, ctype="application/json")
838+
except ValueError:
839+
raise HTTPError(400, reason="Invalid data. Please provide a valid JSON object.")
840+
except TypeError:
841+
raise HTTPError(400, reason="Invalid data type. Please provide a valid type.")
842+
if content_type == "application/x-yaml":
843+
try:
844+
data = to_dict(data_body)
845+
except ValueError:
846+
raise HTTPError(400, reason="Invalid input data. Please provide a valid YAML object.")
847+
except TypeError:
848+
raise HTTPError(400, reason="Invalid type data. Please provide a valid type.")
849+
# # Ensure the parsed data is a dictionary
856850
if not isinstance(data, dict):
857-
raise HTTPError(400, reason=f"Unexcepted value for bte, {self.get_argument('bte')}. Please enter integer, 0 or 1.")
851+
raise ValueError("Invalid input data. Please provide a valid JSON/YAML object.")
858852

859853
parser = MetaKGParser()
860854

861855
try:
862856
self.args.api_details = int(self.get_argument("api_details", 0))
863857
except ValueError:
864-
raise HTTPError(400, reason="Invalid query parameter value. 'api_details' and 'bte' must be integers.")
858+
raise HTTPError(400, reason=f"Unexcepted value for api_details, {self.get_argument('api_details')}. Please enter integer, 0 or 1.")
865859

866860
try:
867861
self.args.bte = int(self.get_argument("bte", 0))
@@ -892,11 +886,8 @@ async def post(self, *args, **kwargs):
892886
combined_data[i] = filtered_api
893887

894888
response = {
895-
"took": 1,
896889
"total": len(combined_data),
897-
"max_score": 1,
898890
"hits": combined_data,
899891
}
900892

901-
self.set_header("Content-Type", "application/json")
902-
self.write(response)
893+
self.finish(response)

src/utils/metakg/parser.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import json
22
import logging
33
from copy import copy
4-
from tornado.web import HTTPError
54
from utils.downloader import DownloadError
65
from utils.metakg.metakg_errors import MetadataRetrievalError
76
import requests
@@ -18,7 +17,7 @@ class MetaKGParser:
1817
def get_non_TRAPI_metadatas(self, data=None, extra_data=None, url=None):
1918
"""
2019
Extract MetaKG edges from a SmartAPI document provided as `data` or fetched from a `url`.
21-
Raises an error if no valid input is given.
20+
Raises an error if no valid input is given, or if parser fails to parse the document.
2221
"""
2322
if not data and not url:
2423
raise MetadataRetrievalError(400, "Either data or url value is expected for this request, please provide data or a url.")
@@ -38,8 +37,8 @@ def get_non_TRAPI_metadatas(self, data=None, extra_data=None, url=None):
3837

3938
def get_TRAPI_metadatas(self, data=None, extra_data=None, url=None):
4039
"""
41-
Extract and process TRAPI metadata from a SmartAPI document or URL.
42-
Returns MetaKG edges or propagates errors.
40+
Extract and process TRAPI metadata from a SmartAPI document or URL.
41+
Returns MetaKG edges or propagates errors.
4342
"""
4443
if not data and not url:
4544
raise MetadataRetrievalError(400, "Either data or url value is expected for this request, please provide data or a url.")
@@ -66,9 +65,9 @@ def get_TRAPI_metadatas(self, data=None, extra_data=None, url=None):
6665
return self.extract_metakgedges(ops, extra_data=extra_data)
6766

6867
def get_TRAPI_with_metakg_endpoint(self, data=None, url=None):
69-
"""
70-
Retrieve TRAPI metadata from a SmartAPI document or URL.
71-
Returns metadata if TRAPI endpoints are found, else an empty list.
68+
"""
69+
Retrieve TRAPI metadata from a SmartAPI document or URL.
70+
Returns metadata if TRAPI endpoints are found, else an empty list.
7271
"""
7372
if not data and not url:
7473
raise MetadataRetrievalError(400, "Either data or url value is expected for this request, please provide data or a url.")

0 commit comments

Comments
 (0)