Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ENH] Set up federation prototype and CI workflows #9

Merged
merged 7 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/dependabot.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Documentation
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
interval: 'weekly'
labels:
- "_bot"
- "maint:dependency"
- "type:maintenance"
32 changes: 32 additions & 0 deletions .github/workflows/build_docker.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: build docker

on:
push:
branches:
- main
workflow_dispatch:

jobs:
build docker:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v4
-
name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/federation_api:latest
19 changes: 19 additions & 0 deletions .github/workflows/codespell.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
name: Codespell

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
codespell:
name: Check for spelling errors
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4
- name: Codespell
uses: codespell-project/actions-codespell@v2
35 changes: 35 additions & 0 deletions .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: lint

on:
push:
branches:
- main
pull_request:
workflow_dispatch:

jobs:
lint:

runs-on: ubuntu-latest

steps:

- uses: actions/checkout@v4

- name: Set up Python 3.10
uses: actions/setup-python@v4
with:
python-version: "3.10"

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi

- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --per-file-ignores=./app/api/models.py:F722
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
14 changes: 14 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
FROM python:3.10

WORKDIR /usr/src/

COPY ./requirements.txt /usr/src/app/requirements.txt

RUN pip install --no-cache-dir --upgrade -r /usr/src/app/requirements.txt

COPY ./app /usr/src/app

# NB_API_PORT, representing the port on which the API will be exposed,
# is an environment variable that will always have a default value of 8000 when building the image
# but can be overridden when running the container.
ENTRYPOINT uvicorn app.main:app --proxy-headers --host 0.0.0.0 --port ${NB_API_PORT:-8000}
Empty file added app/__init__.py
Empty file.
Empty file added app/api/__init__.py
Empty file.
81 changes: 81 additions & 0 deletions app/api/crud.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""CRUD functions called by path operations."""

import httpx
from fastapi import HTTPException

from . import utility as util

async def get(
min_age: float,
max_age: float,
sex: str,
diagnosis: str,
is_control: bool,
min_num_sessions: int,
assessment: str,
image_modal: str,
):
"""
Makes GET requests to one or more Neurobagel node APIs using httpx where the parameters are Neurobagel query parameters.

Parameters
----------
min_age : float
Minimum age of subject.
max_age : float
Maximum age of subject.
sex : str
Sex of subject.
diagnosis : str
Subject diagnosis.
is_control : bool
Whether or not subject is a control.
min_num_sessions : int
Subject minimum number of imaging sessions.
assessment : str
Non-imaging assessment completed by subjects.
image_modal : str
Imaging modality of subject scans.

Returns
-------
httpx.response
Response of the POST request.

"""
cross_node_results = []
params = {}
if min_age:
params["min_age"] = min_age
if max_age:
params["max_age"] = max_age
if sex:
params["sex"] = sex
if diagnosis:
params["diagnosis"] = diagnosis
if is_control:
params["is_control"] = is_control
if min_num_sessions:
params["min_num_sessions"] = min_num_sessions
if assessment:
params["assessment"] = assessment
if image_modal:
params["image_modal"] = image_modal

for node_url in util.NEUROBAGEL_NODES:
response = httpx.get(
url=node_url,
params=params,
# TODO: Revisit timeout value when query performance is improved
timeout=30.0,
)

if not response.is_success:
raise HTTPException(
status_code=response.status_code,
detail=f"{response.reason_phrase}: {response.text}",
)

cross_node_results += response.json()

return cross_node_results
15 changes: 15 additions & 0 deletions app/api/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""Data models."""
from pydantic import BaseModel


class QueryModel(BaseModel):
"""Data model and dependency for API that stores the query parameters to be accepted and validated."""

min_age: float = None
max_age: float = None
sex: str = None
diagnosis: str = None
is_control: bool = None
min_num_sessions: int = None
assessment: str = None
image_modal: str = None
Empty file added app/api/routers/__init__.py
Empty file.
25 changes: 25 additions & 0 deletions app/api/routers/query.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""Router for query path operations."""

from fastapi import APIRouter, Depends

from .. import crud
from ..models import QueryModel

router = APIRouter(prefix="/query", tags=["query"])


@router.get("/")
async def get_query(query: QueryModel = Depends(QueryModel)):
"""When a GET request is sent, return list of dicts corresponding to subject-level metadata aggregated by dataset."""
response = await crud.get(
query.min_age,
query.max_age,
query.sex,
query.diagnosis,
query.is_control,
query.min_num_sessions,
query.assessment,
query.image_modal,
)

return response
6 changes: 6 additions & 0 deletions app/api/utility.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Constants for federation."""

import os

# Neurobagel nodes
NEUROBAGEL_NODES = os.environ.get("NB_NODES", ["http://206.12.99.17:8000/query/"])
25 changes: 25 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""Main app."""

import uvicorn
from fastapi import FastAPI
from fastapi.responses import ORJSONResponse
from fastapi.middleware.cors import CORSMiddleware

from .api.routers import query

app = FastAPI(default_response_class=ORJSONResponse)

app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)


app.include_router(query.router)

# Automatically start uvicorn server on execution of main.py
if __name__ == "__main__":
uvicorn.run("app.main:app", port=8000, reload=True)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does reload get ignored in prod? or should we remove that here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It gets ignored because of the Dockerfile entrypoint! This is the same as what we have for the node APIs.