diff --git a/config/mcp_servers.json b/config/mcp_servers.json index e79f2593..b16263e7 100644 --- a/config/mcp_servers.json +++ b/config/mcp_servers.json @@ -5,9 +5,19 @@ "enabled": true, "description": "Biomedical research: PubMed papers, bioRxiv, ClinicalTrials.gov, NCI, variants, OncoKB", "transport": "stdio", - "command": "biomcp", - "args": ["run", "--mode", "stdio"], + "command": "biomcp-cli", + "args": ["mcp"], "env": {} + }, + "modulector": { + "enabled": true, + "description": "miRNA target interactions, aliases, methylation sites, disease and drug associations", + "transport": "stdio", + "command": "modulector-mcp", + "args": [], + "env": { + "MODULECTOR_API_BASE_URL": "https://modulector.multiomix.org" + } } } } diff --git a/config/requirements.txt b/config/requirements.txt index 0cc208d0..ae5c988c 100644 --- a/config/requirements.txt +++ b/config/requirements.txt @@ -38,4 +38,6 @@ langchain-mcp-adapters==0.2.2 pgvector==0.4.2 sentence-transformers==3.0.0 openai==2.38.0 -biomcp-cli==0.8.22 \ No newline at end of file +biomcp-cli==0.8.22 +modulector-sdk==2.6.1 +bioapi-sdk==0.1.0 \ No newline at end of file diff --git a/src/api_service/mongo_service.py b/src/api_service/mongo_service.py index 02cc2b13..3bba4a5e 100644 --- a/src/api_service/mongo_service.py +++ b/src/api_service/mongo_service.py @@ -214,20 +214,11 @@ def __get_standard_ids(file_type: FileType, molecules: List[str]) -> Optional[Di elif file_type == FileType.MIRNA: logging.warning('Retrieving data from Modulector') + data: Optional[Dict] if is_for_bioapi: - data = global_mrna_service.get_bioapi_service_content( - 'gene-symbols', - request_params={'gene_ids': molecules}, - is_paginated=False, - method='post' - ) + data = global_mrna_service.get_gene_symbols(gene_ids=molecules) elif file_type == FileType.MIRNA: - data = global_mrna_service.get_modulector_service_content( - 'mirna-codes', - request_params={'mirna_codes': molecules}, - is_paginated=False, - method='post' - ) + data = global_mrna_service.get_mirna_codes(mirna_codes=molecules) else: # In case of methylation, cBioPortal don't manage the methylation sites, so we don't need to use Modulector. # Generates a dummy dict with the same keys and values as the molecules list diff --git a/src/api_service/mrna_service.py b/src/api_service/mrna_service.py index d431f53e..97690814 100644 --- a/src/api_service/mrna_service.py +++ b/src/api_service/mrna_service.py @@ -1,146 +1,313 @@ import logging -from json.decoder import JSONDecodeError -from typing import Any, Dict, Optional, Literal, Union -import requests +from typing import Any, cast, Dict, List, Optional from django.conf import settings -from django.http import QueryDict -from requests.exceptions import ConnectionError + +import bioapi_sdk +from bioapi_sdk import ( + CorrectionMethod, + GeneTermRelationType, + GeneTermsFilterType, + OntologyType, + PathwaySource, + TermRelationType, +) +import modulector_sdk as modulector +from modulector_sdk import PaginatedResponse class MRNAService(object): - url_modulector_prefix: str url_bioapi_prefix: str + _modulector_base_url: str def __init__(self): - modulector_settings = settings.MODULECTOR_SETTINGS - self.url_modulector_prefix = self.__build_url(modulector_settings) - bioapi_settings = settings.BIOAPI_SETTINGS self.url_bioapi_prefix = self.__build_url(bioapi_settings) + self._modulector_base_url = settings.MODULECTOR_BASE_URL @staticmethod - def __build_url(settings: Dict[str, Any]) -> str: + def __build_url(svc_settings: Dict[str, Any]) -> str: """ Constructs the URL based on the settings provided. If the port is the default for the protocol (80 for http, 443 for https), it is omitted. Otherwise, the port is included in the URL. - @param settings: Dictionary containing protocol, host, and port information. + @param svc_settings: Dictionary containing protocol, host, and port information. @return: Constructed URL as a string. """ - protocol = settings['protocol'] - host = settings['host'] - port = settings['port'] + protocol = svc_settings['protocol'] + host = svc_settings['host'] + port = svc_settings['port'] if (protocol == 'http' and port == 80) or (protocol == 'https' and port == 443): return f"{protocol}://{host}" else: return f"{protocol}://{host}:{port}" - @staticmethod - def __generate_rest_query_params(get_request: QueryDict) -> str: - """ - Generates a string with all the query params from GET request. - @param get_request: GET request with query params to send to DRF backend - @return: String to send to Modulector/BioAPI APIs - """ - return '&'.join([f'{key}={value}' for (key, value) in get_request.items()]) + # ------------------------------------------------------------------ # + # Modulector SDK wrappers # + # ------------------------------------------------------------------ # + + def _paginated_to_dict(self, result: Optional[PaginatedResponse]) -> Dict: + """Converts a SDK PaginatedResponse to the dict format expected by the frontend.""" + if result is None: + return {'count': 0, 'next': '', 'previous': '', 'results': []} + return { + 'count': result.count, + 'next': result.next or '', + 'previous': result.previous or '', + 'results': list(result.results or []), + } + + def get_mirna_details(self, mirna: str) -> Optional[Dict]: + """Get miRNA details from Modulector SDK.""" + try: + return modulector.get_mirna_details(mirna=mirna, base_url=self._modulector_base_url) + except Exception as ex: + logging.exception(ex) + return None + + def get_methylation_details(self, methylation_site: str) -> Optional[Dict]: + """Get methylation site details from Modulector SDK.""" + try: + return modulector.get_methylation_details( + methylation_site=methylation_site, + base_url=self._modulector_base_url + ) + except Exception as ex: + logging.exception(ex) + return None - def __get_service_content( + def get_mirna_target_interactions( self, - service_name: str, - request_params: QueryDict, - is_paginated: bool, - url_prefix: str, - method: Literal['get', 'post'], - append_slash: bool - ) -> Optional[Union[Dict, str]]: - """ - Generic function to make a request to a Modulector/BioAPI service - @param service_name: Modulector/BioAPI service to consume - @param request_params: GET/POST request with query params to send to DRF backend - @param is_paginated: True if the expected response is paginated to generate a default response in case of error - @param url_prefix: URL of the Modulector or BioAPI service - @param method: Request method (GET or POST) - @param append_slash: If True appends a slash to prevent issues with Django. - @return: JSON data retrieved from the Modulector service. None if response has 404 status code - """ - url = f'{url_prefix}/{service_name}' - - data = None # Prevents Mypy warning - try: - if method == 'get': - params = self.__generate_rest_query_params(request_params) - if params: - url += f'/?{params}' - data = requests.get(url) - else: - # Prevents issues with Django APPEND_SLASH option - if append_slash and not url.endswith('/'): - url += '/' - - data = requests.post(url, json=request_params) - - if data.status_code != 200: - logging.warning(f'{method.upper()} to {url} returned status_code {data.status_code} and ' - f'message: {data.content}') - return None - - content_type = data.headers.get('Content-Type', '') - if 'application/json' in content_type: - return data.json() - elif 'text/plain' in content_type or 'text/html' in content_type: - return data.text - else: - return None - - except (ConnectionError, JSONDecodeError) as ex: - logging.error(f'Received data from Modulector/BioAPI: {data}') - logging.exception(ex) - - if is_paginated: - return { - 'count': 0, - 'next': '', - 'previous': '', - 'results': [] - } + mirna: Optional[str] = None, + gene: Optional[str] = None, + score: Optional[str] = None, + include_pubmeds: bool = False, + page: Optional[int] = None, + page_size: Optional[int] = None, + ) -> Dict: + """Get miRNA-target interactions from Modulector SDK.""" + try: + result = modulector.get_mirna_target_interactions( + mirna=mirna, + gene=gene, + score=float(score) if score else None, + include_pubmeds=include_pubmeds, + page=page, + page_size=page_size, + base_url=self._modulector_base_url, + ) + return self._paginated_to_dict(result) + except Exception as ex: + logging.exception(ex) + return self._paginated_to_dict(None) + + def get_diseases(self, mirna: Optional[str] = None, page: Optional[int] = None, + page_size: Optional[int] = None) -> Dict: + """Get miRNA disease associations from Modulector SDK.""" + try: + result = modulector.get_diseases( + mirna=mirna, + page=page, + page_size=page_size, + base_url=self._modulector_base_url, + ) + return self._paginated_to_dict(result) + except Exception as ex: + logging.exception(ex) + return self._paginated_to_dict(None) + + def get_drugs(self, mirna: Optional[str] = None, page: Optional[int] = None, + page_size: Optional[int] = None) -> Dict: + """Get drug/miRNA associations from Modulector SDK.""" + try: + result = modulector.get_drugs( + mirna=mirna, + page=page, + page_size=page_size, + base_url=self._modulector_base_url, + ) + return self._paginated_to_dict(result) + except Exception as ex: + logging.exception(ex) + return self._paginated_to_dict(None) + + def get_mirna_codes(self, mirna_codes: List[str]) -> Optional[Dict[str, Optional[str]]]: + """Resolve miRNA identifiers to standard codes via Modulector SDK.""" + try: + return modulector.get_mirna_codes( + mirna_codes=mirna_codes, + base_url=self._modulector_base_url, + ) + except Exception as ex: + logging.exception(ex) + return None + + def find_mirna_codes(self, query: str, limit: Optional[int] = None) -> List[str]: + """Search miRNA identifiers via Modulector SDK.""" + try: + return modulector.find_mirna_codes(query=query, limit=limit, base_url=self._modulector_base_url) + except Exception as ex: + logging.exception(ex) + return [] + + def get_methylation_sites(self, methylation_sites: List[str]) -> Optional[Dict[str, List[str]]]: + """Resolve methylation site identifiers to EPIC 2.0 names via Modulector SDK.""" + try: + return modulector.get_methylation_sites( + methylation_sites=methylation_sites, + base_url=self._modulector_base_url, + ) + except Exception as ex: + logging.exception(ex) return None - def get_modulector_service_content( + def find_methylation_sites(self, query: str, limit: Optional[int] = None) -> List[str]: + """Search methylation site identifiers via Modulector SDK.""" + try: + return modulector.find_methylation_sites(query=query, limit=limit, base_url=self._modulector_base_url) + except Exception as ex: + logging.exception(ex) + return [] + + # ------------------------------------------------------------------ # + # BioAPI SDK wrappers # + # ------------------------------------------------------------------ # + + def get_gene_symbols(self, gene_ids: List[str]) -> Optional[Dict[str, List[str]]]: + """Validate gene identifiers and get their HGNC-approved symbols via BioAPI SDK.""" + try: + return bioapi_sdk.gene_symbols(gene_ids=gene_ids, base_url=self.url_bioapi_prefix) + except Exception as ex: + logging.exception(ex) + return None + + def find_gene_symbols(self, query: str, limit: int = 50) -> List[str]: + """Search gene symbols via BioAPI SDK.""" + try: + return bioapi_sdk.gene_symbols_finder(query=query, limit=limit, base_url=self.url_bioapi_prefix) + except Exception as ex: + logging.exception(ex) + return [] + + def get_gene_information(self, gene_ids: List[str]) -> Optional[Dict]: + """Get genomic and database information for genes via BioAPI SDK.""" + try: + return bioapi_sdk.information_of_genes(gene_ids=gene_ids, base_url=self.url_bioapi_prefix) + except Exception as ex: + logging.exception(ex) + return None + + def get_genes_of_its_group(self, gene_id: str) -> Optional[Dict]: + """Get the HGNC gene group information for a gene via BioAPI SDK.""" + try: + return bioapi_sdk.genes_of_its_group(gene_id=gene_id, base_url=self.url_bioapi_prefix) + except Exception as ex: + logging.exception(ex) + return None + + def get_pathways_in_common(self, gene_ids: List[str]) -> Optional[Dict]: + """Get metabolic pathways common to a list of genes via BioAPI SDK.""" + try: + return bioapi_sdk.pathways_in_common(gene_ids=gene_ids, base_url=self.url_bioapi_prefix) + except Exception as ex: + logging.exception(ex) + return None + + def get_pathway_genes(self, source: str, external_id: str) -> Optional[Dict]: + """Get genes involved in a metabolic pathway via BioAPI SDK.""" + try: + return bioapi_sdk.pathway_genes( + source=cast(PathwaySource, source), + external_id=external_id, + base_url=self.url_bioapi_prefix, + ) + except Exception as ex: + logging.exception(ex) + return None + + def get_genes_to_terms( self, - service_name: str, - request_params: QueryDict, - is_paginated: bool, - method: Literal['get', 'post'] = 'get' - ) -> Optional[Dict]: - """ - Makes a request to a Modulector service. - @param service_name: Modulector service to consume - @param request_params: GET/POST params with query params to send to DRF backend - @param is_paginated: True if the expected response is paginated to generate a default response in case of error - @param method: Request method (GET or POST) - @return: JSON data retrieved from the Modulector service. None if response has 404 status code - """ - return self.__get_service_content(service_name, request_params, is_paginated, self.url_modulector_prefix, - method, append_slash=True) + gene_ids: List[str], + filter_type: str = 'intersection', + relation_type: Optional[List[str]] = None, + ontology_type: Optional[List[str]] = None, + p_value_threshold: Optional[float] = None, + correction_method: Optional[str] = None, + ) -> Optional[List[Dict]]: + """Get Gene Ontology terms related to a list of genes via BioAPI SDK.""" + try: + return bioapi_sdk.genes_to_terms( + gene_ids=gene_ids, + filter_type=cast(GeneTermsFilterType, filter_type), + relation_type=cast(Optional[List[GeneTermRelationType]], relation_type), + ontology_type=cast(Optional[List[OntologyType]], ontology_type), + p_value_threshold=p_value_threshold, + correction_method=cast(Optional[CorrectionMethod], correction_method), + base_url=self.url_bioapi_prefix, + ) + except Exception as ex: + logging.exception(ex) + return None - def get_bioapi_service_content( + def get_related_terms( self, - service_name: str, - request_params: QueryDict, - is_paginated: bool, - method: Literal['get', 'post'] = 'get' - ) -> Optional[Any]: - """ - Makes a request to a BioAPI service. - @param service_name: BioAPI service to consume - @param request_params: GET/POST params with query params to send to DRF backend - @param is_paginated: True if the expected response is paginated to generate a default response in case of error - @param method: Request method (GET or POST) - @return: JSON data retrieved from the Modulector service. None if response has 404 status code - """ - return self.__get_service_content(service_name, request_params, is_paginated, self.url_bioapi_prefix, method, - append_slash=False) + term_id: str, + relations: Optional[List[str]] = None, + ontology_type: Optional[List[str]] = None, + general_depth: Optional[int] = None, + hierarchical_depth_to_children: Optional[int] = None, + to_root: Optional[bool] = None, + ) -> Optional[List[Dict]]: + """Get Gene Ontology terms related to a term via BioAPI SDK.""" + try: + return bioapi_sdk.related_terms( + term_id=term_id, + relations=cast(Optional[List[TermRelationType]], relations), + ontology_type=cast(Optional[List[OntologyType]], ontology_type), + general_depth=general_depth, + hierarchical_depth_to_children=hierarchical_depth_to_children, + to_root=to_root, + base_url=self.url_bioapi_prefix, + ) + except Exception as ex: + logging.exception(ex) + return None + + def get_oncokb_information(self, gene_ids: List[str], query: Optional[str] = None) -> Optional[Dict]: + """Get OncoKB cancer evidence and precision therapy data for genes via BioAPI SDK.""" + try: + return bioapi_sdk.information_of_oncokb(gene_ids=gene_ids, query=query, base_url=self.url_bioapi_prefix) + except Exception as ex: + logging.exception(ex) + return None + + def get_drugs_pharm_gkb(self, gene_ids: List[str]) -> Optional[Dict]: + """Get PharmGKB cancer-related drug labels for genes via BioAPI SDK.""" + try: + return bioapi_sdk.drugs_pharm_gkb(gene_ids=gene_ids, base_url=self.url_bioapi_prefix) + except Exception as ex: + logging.exception(ex) + return None + + def get_string_relations(self, gene_id: str, min_combined_score: Optional[int] = None) -> Optional[List[Dict]]: + """Get STRING functional association relations for a gene via BioAPI SDK.""" + try: + return bioapi_sdk.string_relations( + gene_id=gene_id, + min_combined_score=min_combined_score, + base_url=self.url_bioapi_prefix, + ) + except Exception as ex: + logging.exception(ex) + return None + + def get_drugs_regulating_gene(self, gene_id: str) -> Optional[Dict]: + """Get a DrugBank link listing drugs that regulate a gene via BioAPI SDK.""" + try: + return bioapi_sdk.drugs_regulating_gene(gene_id=gene_id, base_url=self.url_bioapi_prefix) + except Exception as ex: + logging.exception(ex) + return None -global_mrna_service = MRNAService() +global_mrna_service = MRNAService() \ No newline at end of file diff --git a/src/api_service/views.py b/src/api_service/views.py index 901da96b..0f0a3c6b 100644 --- a/src/api_service/views.py +++ b/src/api_service/views.py @@ -914,21 +914,23 @@ def get_number_samples_in_common_action_one_front(request): @login_required def mirna_data_action(request): """Gets miRNA data from Modulector""" - data = global_mrna_service.get_modulector_service_content('mirna', request.GET, is_paginated=False) + data = global_mrna_service.get_mirna_details(mirna=request.GET.get('mirna', '')) return generate_json_response_or_404(data) @login_required def methylation_data_action(request): """Gets Methylation site data from Modulector""" - data = global_mrna_service.get_modulector_service_content('methylation', request.GET, is_paginated=False) + data = global_mrna_service.get_methylation_details( + methylation_site=request.GET.get('methylation_site', '') + ) return generate_json_response_or_404(data) @login_required def get_mirna_target_interaction_action(request): """ - Searches in papers an specific miRNA interaction. + Searches in papers an specific miRNA interaction. Examples: http://127.0.0.1:8000/api-service/mirna-target-interaction?gene=BRCA1&mirna=hsa-miR-132-3p&include_pubmeds=true @@ -939,48 +941,40 @@ def get_mirna_target_interaction_action(request): """ gene = request.GET.get('gene') mirna = request.GET.get('mirna') - include_pubmeds = request.GET.get('include_pubmeds') if not gene and not mirna: return JsonResponse(data={"error": "Param 'mirna' or 'gene' are mandatory"}, status=400) - if gene and not mirna: - params = {'gene': gene} - elif not gene and mirna: - params = {'mirna': mirna} - else: - params = {'mirna': mirna, 'gene': gene} - - score = request.GET.get('score') - if score: - params['score'] = score - - if include_pubmeds: - if include_pubmeds.lower() == "true": - params['include_pubmeds'] = "true" - - # Gets include_pubmeds flag - include_pubmeds = request.GET.get('include_pubmeds') == 'true' - if include_pubmeds: - params['include_pubmeds'] = 'true' - data = global_mrna_service.get_modulector_service_content('mirna-target-interactions', - request_params=params, - is_paginated=True, - method='get') + data = global_mrna_service.get_mirna_target_interactions( + mirna=mirna, + gene=gene, + score=request.GET.get('score'), + include_pubmeds=request.GET.get('include_pubmeds', '').lower() == 'true', + page=request.GET.get('page'), + page_size=request.GET.get('page_size'), + ) return JsonResponse(data) @login_required def get_mirna_diseases_action(request): """Searches in papers miRNA associations with diseases""" - data = global_mrna_service.get_modulector_service_content('diseases', request.GET, is_paginated=True) + data = global_mrna_service.get_diseases( + mirna=request.GET.get('mirna'), + page=request.GET.get('page'), + page_size=request.GET.get('page_size'), + ) return generate_json_response_or_404(data) @login_required def get_mirna_drugs_action(request): """Searches in papers miRNA associations with drugs""" - data = global_mrna_service.get_modulector_service_content('drugs', request.GET, is_paginated=True) + data = global_mrna_service.get_drugs( + mirna=request.GET.get('mirna'), + page=request.GET.get('page'), + page_size=request.GET.get('page_size'), + ) return generate_json_response_or_404(data) diff --git a/src/assistant/services/tools.py b/src/assistant/services/tools.py index be3331d3..fd0dae8b 100644 --- a/src/assistant/services/tools.py +++ b/src/assistant/services/tools.py @@ -542,15 +542,9 @@ def get_mirna_modulators(gene_name: str, min_score: float = 0.0, limit: int = 20 """ from api_service.mrna_service import global_mrna_service - params: dict = {'gene': gene_name} - if min_score > 0: - params['score'] = str(min_score) - - data = global_mrna_service.get_mdulector_service_content( - 'mirna-target-interactions', - request_params=params, - is_paginated=True, - method='get' + data = global_mrna_service.get_mirna_target_interactions( + gene=gene_name, + score=str(min_score) if min_score > 0 else None, ) if not data or data.get('count', 0) == 0: @@ -578,12 +572,7 @@ def get_gene_annotations(gene_name: str) -> str: """ from api_service.mrna_service import global_mrna_service - data = global_mrna_service.get_bioapi_service_content( - 'information-of-genes', - request_params={'gene_ids': [gene_name]}, - is_paginated=False, - method='post' - ) + data = global_mrna_service.get_gene_information(gene_ids=[gene_name]) if not data: return json.dumps({'error': f'No annotation data found for gene "{gene_name}" in BioAPI.'}) @@ -600,12 +589,7 @@ def get_drugs_regulating_gene(gene_name: str) -> str: """ from api_service.mrna_service import global_mrna_service - data = global_mrna_service.get_bioapi_service_content( - f'/drugs-regulating-gene/{gene_name}', - request_params={}, - is_paginated=False, - method='get' - ) + data = global_mrna_service.get_drugs_regulating_gene(gene_id=gene_name) if not data or 'link' not in data: return json.dumps({ diff --git a/src/biomarkers/views.py b/src/biomarkers/views.py index c67d9324..1eabb64a 100644 --- a/src/biomarkers/views.py +++ b/src/biomarkers/views.py @@ -153,12 +153,7 @@ def biomarkers_action(request): def get_gene_aliases(genes_ids: List[str]) -> Optional[Dict]: """Get the aliases for a list of genes from BioAPI""" - return global_mrna_service.get_bioapi_service_content( - 'gene-symbols', - request_params={'gene_ids': genes_ids}, - is_paginated=False, - method='post' - ) + return global_mrna_service.get_gene_symbols(gene_ids=genes_ids) def find_genes_from_request(request: Request) -> List[Dict]: @@ -166,8 +161,13 @@ def find_genes_from_request(request: Request) -> List[Dict]: Generates the structure for the frontend for a list of genes. The needed structure is a list of dicts with the following keys: molecule, standard """ - genes_found = global_mrna_service.get_bioapi_service_content('gene-symbols-finder', - request.GET, is_paginated=False) + limit = request.GET.get('limit') + genes_found = global_mrna_service.find_gene_symbols( + query=request.GET.get('query', ''), + limit=int(limit) if limit else 50, + ) + if not genes_found: + return [] aliases = get_gene_aliases(genes_found) return [{'molecule': gene, 'standard': aliases.get(gene, [None])[0]} for gene in genes_found] @@ -197,17 +197,11 @@ class MiRNACodes(APIView): @staticmethod def __get_mirna_aliases(mirna_codes: List[str]) -> Optional[Dict]: """Get the aliases for a list of miRNAs through Modulector""" - return global_mrna_service.get_modulector_service_content( - 'mirna-codes', - request_params={'mirna_codes': mirna_codes}, - is_paginated=False, - method='post' - ) + return global_mrna_service.get_mirna_codes(mirna_codes=mirna_codes) def get(self, request): """Generates a query to search miRNAs through Modulector""" - mirnas_found = global_mrna_service.get_modulector_service_content('mirna-codes-finder', - request.GET, is_paginated=False) + mirnas_found = global_mrna_service.find_mirna_codes(query=request.GET.get('query', '')) # Generates the structure for the frontend aliases = self.__get_mirna_aliases(mirnas_found) @@ -235,23 +229,12 @@ class MethylationSites(APIView): @staticmethod def __get_methylation_sites_aliases(methylation_sites: List[str]) -> Optional[Dict]: """Get the aliases for a list of Methylation sites through Modulector""" - return global_mrna_service.get_modulector_service_content( - 'methylation-sites', - request_params={'methylation_sites': methylation_sites}, - is_paginated=False, - method='post' - ) + return global_mrna_service.get_methylation_sites(methylation_sites=methylation_sites) def get(self, request: Request): """Generates a query to search Methylation sites through Modulector""" - # methylation_sites = request.GET.get('methylation_sites', '') - # Gets the Methylation sites - sites_found = global_mrna_service.get_modulector_service_content( - 'methylation-sites-finder', - request_params=request.GET, - is_paginated=False - ) + sites_found = global_mrna_service.find_methylation_sites(query=request.GET.get('query', '')) # Generates the structure for the frontend aliases = self.__get_methylation_sites_aliases(sites_found) diff --git a/src/molecules_details/views.py b/src/molecules_details/views.py index 1d4c9c13..4f499ea5 100644 --- a/src/molecules_details/views.py +++ b/src/molecules_details/views.py @@ -20,19 +20,11 @@ class GeneInformation(APIView): def get(request: HttpRequest): gene = request.GET.get('gene', '').strip() - if gene: - gene = gene.split(',') - else: + if not gene: return Response(status=400, data={"error": "Param 'gene' is mandatory"}) + gene_ids = gene.split(',') - data = global_mrna_service.get_bioapi_service_content( - 'information-of-genes', - request_params={ - 'gene_ids': gene - }, - is_paginated=False, - method='post' - ) + data = global_mrna_service.get_gene_information(gene_ids=gene_ids) return Response({ 'data': data if data else None @@ -56,12 +48,7 @@ def get(request: HttpRequest): if not gene: return Response(status=400, data={"error": "Param 'gene' is mandatory"}) - data = global_mrna_service.get_bioapi_service_content( - f'/genes-of-its-group/{gene}', - request_params={}, # No params needed - is_paginated=False, - method='get' - ) + data = global_mrna_service.get_genes_of_its_group(gene_id=gene) return Response({ 'data': data if data else None }) @@ -83,19 +70,11 @@ class PathwaysInformation(APIView): def get(request: HttpRequest): gene = request.GET.get('gene', '').strip() - if gene: - gene = gene.split(',') - else: + if not gene: return Response(status=400, data={"error": "Param 'gene' is mandatory"}) + gene_ids = gene.split(',') - data = global_mrna_service.get_bioapi_service_content( - 'pathways-in-common', - request_params={ - 'gene_ids': gene - }, - is_paginated=False, - method='post' - ) + data = global_mrna_service.get_pathways_in_common(gene_ids=gene_ids) return Response({ 'data': data['pathways'] if data and 'pathways' in data else None @@ -124,12 +103,7 @@ def get(request: HttpRequest): if not pathway_id: return Response(status=400, data={"error": "Param 'id' is mandatory"}) - data = global_mrna_service.get_bioapi_service_content( - f'/pathway-genes/{source}/{pathway_id}', - request_params={}, # No params needed - is_paginated=False, - method='get' - ) + data = global_mrna_service.get_pathway_genes(source=source, external_id=pathway_id) return Response({ 'data': data['genes'] if data and 'genes' in data else None }) @@ -150,17 +124,17 @@ def get(request: HttpRequest): filter_type = request.GET.get('filter_type', '').strip() p_value_threshold = request.GET.get('p_value_threshold', '').strip() correction_method = request.GET.get('correction_method', '').strip() - relation_type = request.GET.get('relation_type', '').strip() - ontology_type = request.GET.get('ontology_type', '').strip() + relation_type_param = request.GET.get('relation_type', '').strip() + ontology_type_param = request.GET.get('ontology_type', '').strip() - if gene: - gene = gene.split(',') - else: + if not gene: return Response(status=400, data={"error": "Param 'gene' is mandatory"}) + gene_ids = gene.split(',') if not filter_type: filter_type = 'intersection' + relation_types: list[str] if filter_type not in ["intersection", "union", "enrichment"]: return Response(status=400, data={"error": "The 'filter_type' parameter must be one of the following " "options: 'intersection', 'union' or 'enrichment'"}) @@ -172,56 +146,42 @@ def get(request: HttpRequest): if not correction_method: return Response(status=400, data={"error": "The 'correction_method' parameter is mandatory if " "'filter_type' is 'enrichment'"}) + relation_types = [] else: - if not relation_type: - relation_type = ["enables", "involved_in", - "part_of", "located_in"] + if not relation_type_param: + relation_types = ["enables", "involved_in", "part_of", "located_in"] else: - relation_type = relation_type.split(',') - for relation in relation_type: + relation_types = relation_type_param.split(',') + for relation in relation_types: if relation not in ["enables", "involved_in", "part_of", "located_in"]: return Response(status=400, data={"error": "The 'relation_type' parameter must be a " "combination of the following options: " "'enables', 'involved_in', 'part_of' and " "'located_in'"}) - if not ontology_type: - ontology_type = ["biological_process", - "molecular_function", "cellular_component"] + if not ontology_type_param: + ontology_types = ["biological_process", "molecular_function", "cellular_component"] else: - ontology_type = ontology_type.split(',') - for type_elem in ontology_type: + ontology_types = ontology_type_param.split(',') + for type_elem in ontology_types: if type_elem not in ["biological_process", "molecular_function", "cellular_component"]: return Response(status=400, data={"error": "The 'ontology_type' parameter must be a combination of " "the following options: 'biological_process', " "'molecular_function' and 'cellular_component'"}) if filter_type in ["intersection", "union"]: - data = global_mrna_service.get_bioapi_service_content( - 'genes-to-terms', - # request_params={ - # 'gene_ids': [gene], - # 'filter_type': filter_type, - # 'ontology_type': ontology_type, - # 'relation_type': relation_type - # }, - # TODO: remove this and uncomment the above code when the bioapi service is fixed - request_params={ - "gene_ids": ["TMCO4"], "relation_type": ["enables"], "ontology_type": ["molecular_function"] - }, - is_paginated=False, - method='post' + # TODO: use gene_ids=gene_ids, filter_type=filter_type, ontology_type=ontology_types, + # relation_type=relation_types when the bioapi service is fixed + data = global_mrna_service.get_genes_to_terms( + gene_ids=["TMCO4"], + relation_type=["enables"], + ontology_type=["molecular_function"], ) else: # filter_type == "enrichment" - data = global_mrna_service.get_bioapi_service_content( - 'genes-to-terms', - request_params={ - 'gene_ids': gene, - 'filter_type': filter_type, - 'p_value_threshold': p_value_threshold, - 'correction_method': correction_method, - 'ontology_type': ontology_type - }, - is_paginated=False, - method='post' + data = global_mrna_service.get_genes_to_terms( + gene_ids=gene_ids, + filter_type=filter_type, + p_value_threshold=float(p_value_threshold), + correction_method=correction_method, + ontology_type=ontology_types, ) return Response({ @@ -239,7 +199,7 @@ class GeneOntologyTermsOfTerm(APIView): permission_classes = [permissions.IsAuthenticated] @staticmethod - def __process_go_data(go_terms: dict) -> dict: + def __process_go_data(go_terms: list[dict] | None) -> dict: """ Generates a structure with the GO terms data in a format ready-to-use for cytoscape frontend lib. @param go_terms: BioAPI GO data. @@ -286,61 +246,57 @@ def get(self, request: HttpRequest): if not general_depth.isnumeric(): return Response(status=400, data={"error": "Param 'general_depth' must be a numeric value"}) - hierarchical_depth_to_children = request.GET.get('hierarchical_depth_to_children', '0').strip() - if hierarchical_depth_to_children: - if not hierarchical_depth_to_children.isnumeric(): + hierarchical_depth_to_children_param = request.GET.get('hierarchical_depth_to_children', '0').strip() + hierarchical_depth_to_children = 0 + if hierarchical_depth_to_children_param: + if not hierarchical_depth_to_children_param.isnumeric(): return Response(status=400, data={"error": "Param 'hierarchical_depth_to_children' must be a " "numeric value"}) - hierarchical_depth_to_children = int(hierarchical_depth_to_children) + hierarchical_depth_to_children = int(hierarchical_depth_to_children_param) - to_root = request.GET.get('to_root', '1').strip() - if to_root: - if to_root not in ["0", "1"]: + to_root_param = request.GET.get('to_root', '1').strip() + to_root = True + if to_root_param: + if to_root_param not in ["0", "1"]: return Response(status=400, data={"error": "Param 'to_root' must be '0' or '1'"}) - to_root = int(to_root) + to_root = bool(int(to_root_param)) - relations = request.GET.get('relations', '').strip() - if not relations: + relations_param = request.GET.get('relations', '').strip() + if not relations_param: relations = ["part_of", "regulates", "has_part"] else: - relations = relations.split(',') + relations = relations_param.split(',') for relation in relations: if relation not in ["part_of", "regulates", "has_part"]: return Response(status=400, data={"error": "The 'relations' parameter must be a combination of " "the following options: 'part_of', 'regulates' and " "'has_part'"}) - ontology_type = request.GET.get('ontology_type', '').strip() - if not ontology_type: - ontology_type = ["biological_process", - "molecular_function", "cellular_component"] + ontology_type_param = request.GET.get('ontology_type', '').strip() + if not ontology_type_param: + ontology_types = ["biological_process", "molecular_function", "cellular_component"] else: - ontology_type = ontology_type.split(',') - for type_elem in ontology_type: + ontology_types = ontology_type_param.split(',') + for type_elem in ontology_types: if type_elem not in ["biological_process", "molecular_function", "cellular_component"]: return Response(status=400, data={"error": "The 'ontology_type' parameter must be a combination " "of the following options: 'biological_process', " "'molecular_function' and 'cellular_component'"}) - data = global_mrna_service.get_bioapi_service_content( - 'related-terms', - request_params={ - 'term_id': term_id, - 'relations': relations, - 'ontology_type': ontology_type, - 'general_depth': int(general_depth), - 'hierarchical_depth_to_children': hierarchical_depth_to_children, - 'to_root': to_root - }, - is_paginated=False, - method='post' + data = global_mrna_service.get_related_terms( + term_id=term_id, + relations=relations, + ontology_type=ontology_types, + general_depth=int(general_depth), + hierarchical_depth_to_children=hierarchical_depth_to_children, + to_root=to_root, ) # Generates structure for cytoscape in frontend - data = self.__process_go_data(data) + go_terms_data = self.__process_go_data(data) return Response({ - 'go_terms': data + 'go_terms': go_terms_data }) @@ -363,21 +319,12 @@ def get(request: HttpRequest): if not gene: return Response(status=400, data={"error": "Param 'gene' is mandatory"}) - else: - gene = gene.split(',') + gene_ids = gene.split(',') if not query: query = "" - data = global_mrna_service.get_bioapi_service_content( - 'information-of-oncokb', - request_params={ - 'gene_ids': gene, - 'query': query - }, - is_paginated=False, - method='post' - ) + data = global_mrna_service.get_oncokb_information(gene_ids=gene_ids, query=query) return Response({ 'data': data if data else None @@ -398,17 +345,9 @@ def get(request: HttpRequest): gene = request.GET.get('gene', '').strip() if not gene: return Response(status=400, data={"error": "Param 'gene' is mandatory"}) - else: - gene = gene.split(',') + gene_ids = gene.split(',') - data = global_mrna_service.get_bioapi_service_content( - 'drugs-pharm-gkb', - request_params={ - 'gene_ids': gene - }, - is_paginated=False, - method='post' - ) + data = global_mrna_service.get_drugs_pharm_gkb(gene_ids=gene_ids) return Response({ 'data': data if data else None @@ -489,7 +428,7 @@ def __generate_edge(association: dict[str, Any], source_key: str, target_key: st 'grabbable': True, } - def __process_associations_data(self, associations: dict) -> list[dict]: + def __process_associations_data(self, associations: list[dict] | None) -> list[dict]: """ Generates a structure with the genes associations data in a format ready-to-use for cytoscape frontend lib. @param associations: BioAPI Gene data. @@ -536,20 +475,15 @@ def get(self, request: HttpRequest): else: return Response(status=400, data={"error": "Param 'min_combined_score' must be a numeric value"}) - data = global_mrna_service.get_bioapi_service_content( - 'string-relations', - request_params={ - 'gene_id': gene_id, - 'min_combined_score': max(900, int(min_combined_score)) - }, - is_paginated=False, - method='post' + data = global_mrna_service.get_string_relations( + gene_id=gene_id, + min_combined_score=max(900, int(min_combined_score)), ) # Generates structure for cytoscape in frontend - data = self.__process_associations_data(data) + associations_data = self.__process_associations_data(data) - return Response({'data': data}) + return Response({'data': associations_data}) class DrugsRegulatingGene(APIView): @@ -568,12 +502,7 @@ def get(request: HttpRequest): if not gene: return Response(status=400, data={"error": "Param 'gene' is mandatory"}) - data = global_mrna_service.get_bioapi_service_content( - f'/drugs-regulating-gene/{gene}', - request_params={}, # No params needed - is_paginated=False, - method='get' - ) + data = global_mrna_service.get_drugs_regulating_gene(gene_id=gene) return Response({ 'data': data["link"] if data and "link" in data else None }) @@ -594,14 +523,7 @@ def get(request: HttpRequest): if not methylation_site: return Response(status=400, data={"error": "Param 'methylation_site' is mandatory"}) - data = global_mrna_service.get_modulector_service_content( - 'methylation', - request_params={ - 'methylation_site': methylation_site - }, - is_paginated=False, - method='get' - ) + data = global_mrna_service.get_methylation_details(methylation_site=methylation_site) return Response({ 'data': data if data else None diff --git a/src/multiomics_intermediate/settings.py b/src/multiomics_intermediate/settings.py index 52f39191..f558fdd8 100644 --- a/src/multiomics_intermediate/settings.py +++ b/src/multiomics_intermediate/settings.py @@ -304,11 +304,15 @@ EMAIL_PAGE_DOMAIN = 'https://multiomix.org' # Modulector settings -MODULECTOR_SETTINGS = { - 'host': os.getenv('MODULECTOR_HOST', 'modulector.multiomix.org'), - 'port': os.getenv('MODULECTOR_PORT', 443), - 'protocol': os.getenv('BIOAPI_PROTOCOL', 'https') -} +_modulector_protocol: str = os.getenv('MODULECTOR_PROTOCOL', os.getenv('BIOAPI_PROTOCOL', 'https')) +_modulector_host: str = os.getenv('MODULECTOR_HOST', 'modulector.multiomix.org') +_modulector_port: int = int(os.getenv('MODULECTOR_PORT', '443')) +# MODULECTOR_API_BASE_URL is also read directly by the modulector-sdk as env var +MODULECTOR_BASE_URL = os.getenv( + 'MODULECTOR_API_BASE_URL', + f"{_modulector_protocol}://{_modulector_host}" if _modulector_port in (80, 443) + else f"{_modulector_protocol}://{_modulector_host}:{_modulector_port}" +) # BioAPI settings BIOAPI_SETTINGS = {