diff --git a/README.md b/README.md index 5e4e9a6..0b7b288 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,15 @@ Please refer to our [**official documentation**](https://neurobagel.org/overview ## Launching the API ### 1. Set the Neurobagel nodes to federate over -Create an `.env` file with the variable `NB_NODES` set to the URLs of the nodes to be federated over. -The URLs should be stored as a **space-separated, unquoted** string. +Create a `fed.env` file with the variable `LOCAL_NB_NODES` containing the URLs and (arbitrary) names of the nodes to be federated over. +Each node should be wrapped in brackets `()`, with the URL and name of the node (in that order) separated by a comma. +The variable must be an **unquoted** string. + +This repo contains a [template `fed.env`](/fed.env) file that you can edit. e.g., ```bash -NB_NODES=https://myfirstnode.org/ https://mysecondnode.org/ +LOCAL_NB_NODES=(https://myfirstnode.org/,First Node)(https://mysecondnode.org/,Second Node) ``` ### 2. Run the Docker container @@ -23,6 +26,6 @@ NB_NODES=https://myfirstnode.org/ https://mysecondnode.org/ docker pull neurobagel/federation_api # Make sure to run the next command in the same directory where your .env file is -docker run -d --name=federation -p 8080:8000 --env-file=.env neurobagel/federation_api +docker run -d --name=federation -p 8080:8000 --env-file=fed.env neurobagel/federation_api ``` NOTE: You can replace the port number `8080` for the `-p` flag with any port on the host you wish to use for the API. diff --git a/app/api/crud.py b/app/api/crud.py index d147d98..c926018 100644 --- a/app/api/crud.py +++ b/app/api/crud.py @@ -12,6 +12,7 @@ async def get( min_num_sessions: int, assessment: str, image_modal: str, + node_urls: list[str], ): """ Makes GET requests to one or more Neurobagel node APIs using send_get_request utility function where the parameters are Neurobagel query parameters. @@ -34,6 +35,8 @@ async def get( Non-imaging assessment completed by subjects. image_modal : str Imaging modality of subject scans. + node_urls : list[str] + List of Neurobagel nodes to send the query to. Returns ------- @@ -42,6 +45,10 @@ async def get( """ cross_node_results = [] + + node_urls = util.validate_query_node_url_list(node_urls) + + # Node API query parameters params = {} if min_age: params["min_age"] = min_age @@ -60,8 +67,8 @@ async def get( if image_modal: params["image_modal"] = image_modal - nodes_dict = util.parse_nodes_as_dict(util.NEUROBAGEL_NODES) - for node_url, node_name in nodes_dict.items(): + for node_url in node_urls: + node_name = util.FEDERATION_NODES[node_url] response = util.send_get_request(node_url + "query/", params) for result in response: @@ -89,7 +96,7 @@ async def get_terms(data_element_URI: str): cross_node_results = [] params = {data_element_URI: data_element_URI} - for node_url in util.parse_nodes_as_dict(util.NEUROBAGEL_NODES).keys(): + for node_url in util.FEDERATION_NODES: response = util.send_get_request( node_url + "attributes/" + data_element_URI, params ) diff --git a/app/api/models.py b/app/api/models.py index d701843..b086776 100644 --- a/app/api/models.py +++ b/app/api/models.py @@ -1,8 +1,8 @@ """Data models.""" - from typing import Optional, Union -from pydantic import BaseModel +from fastapi import Query +from pydantic import BaseModel, Field CONTROLLED_TERM_REGEX = r"^[a-zA-Z]+[:]\S+$" @@ -18,6 +18,9 @@ class QueryModel(BaseModel): min_num_sessions: int = None assessment: str = None image_modal: str = None + # TODO: Replace default value with union of local and public nodes once https://github.com/neurobagel/federation-api/issues/28 is merged + # syntax from https://github.com/tiangolo/fastapi/issues/4445#issuecomment-1117632409 + node_url: list[str] | None = Field(Query(default=[])) class CohortQueryResponse(BaseModel): diff --git a/app/api/routers/nodes.py b/app/api/routers/nodes.py index 910aa9e..15febb8 100644 --- a/app/api/routers/nodes.py +++ b/app/api/routers/nodes.py @@ -9,6 +9,5 @@ async def get_nodes(): """Returns a dict of all available nodes apis where key is node URL and value is node name.""" return [ - {"NodeName": v, "ApiURL": k} - for k, v in util.parse_nodes_as_dict(util.NEUROBAGEL_NODES).items() + {"NodeName": v, "ApiURL": k} for k, v in util.FEDERATION_NODES.items() ] diff --git a/app/api/routers/query.py b/app/api/routers/query.py index db3b6b3..cd53245 100644 --- a/app/api/routers/query.py +++ b/app/api/routers/query.py @@ -22,6 +22,7 @@ async def get_query(query: QueryModel = Depends(QueryModel)): query.min_num_sessions, query.assessment, query.image_modal, + query.node_url, ) return response diff --git a/app/api/utility.py b/app/api/utility.py index c6c5698..18a2ecc 100644 --- a/app/api/utility.py +++ b/app/api/utility.py @@ -1,31 +1,107 @@ -"""Constants for federation.""" +"""Constants and utility functions for federation.""" import os import re +import warnings import httpx from fastapi import HTTPException -# Neurobagel nodes -NEUROBAGEL_NODES = os.environ.get( +# Neurobagel nodes - TODO: remove default value? +LOCAL_NODES = os.environ.get( "LOCAL_NB_NODES", "(https://api.neurobagel.org/, OpenNeuro)" ) +FEDERATION_NODES = {} + + +def add_trailing_slash(url: str) -> str: + """Add trailing slash to a URL if it does not already have one.""" + if not url.endswith("/"): + url += "/" + return url def parse_nodes_as_dict(nodes: str) -> list: - """Returns user-defined Neurobagel nodes as a dict. + """ + Transforms a string of user-defined Neurobagel nodes (from an environment variable) to a dict where the keys are the node URLs, and the values are the node names. It uses a regular expression to match the url, name pairs. - Makes sure node URLs end with a slash.""" - pattern = re.compile(r"\((?Phttps?://[^\s]+), (?P