2424from utils .notification import SlackNewAPIMessage , SlackNewTranslatorAPIMessage
2525from utils .metakg .parser import MetaKGParser
2626from utils .metakg .metakg_errors import MetadataRetrievalError
27+ from utils .decoder import to_dict
2728
2829logger = logging .getLogger ("smartAPI" )
2930
@@ -44,7 +45,6 @@ def _(self, *args, **kwargs):
4445class BaseHandler (BioThingsAuthnMixin , BaseAPIHandler ):
4546 pass
4647
47-
4848class 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
386436class 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 )
0 commit comments