Skip to content
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
9 changes: 9 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
; Flake8 does not natively support configuration via pyproject.toml.
; See https://github.com/microsoft/vscode-flake8/issues/135

[flake8]
extend-ignore = E501, E203
extend-select = B950
exclude =
build,
dist,
9 changes: 2 additions & 7 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,8 @@ updates:
- "_bot"
- "dependencies"

# In repo settings we have configured dependabot to open PRs for security updates.
# Here we configure custom labels to be applied to security update PRs,
# while still preventing regular version update PRs.
# See https://docs.github.com/en/code-security/dependabot/dependabot-security-updates/configuring-dependabot-security-updates#overriding-the-default-behavior-with-a-configuration-file
# Note: this configuration only has an effect in repositories that have
# a requirements.txt file / use python / pip.
- package-ecosystem: "pip"
# This is a temporary fix until we update the global sync workflow
- package-ecosystem: "uv"
directory: "/"
schedule:
interval: "weekly"
Expand Down
17 changes: 5 additions & 12 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,23 +34,16 @@ jobs:
with:
python-version: "3.10"

- name: Install uv
uses: astral-sh/setup-uv@v5

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

- 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

- name: Test with pytest
run: |
coverage run -m pytest -m "integration or not integration"
coverage lcov -o ./coverage/lcov.info
uv run pytest -m "integration or not integration" --cov --cov-branch --cov-report=xml

- name: Upload results to Codecov
uses: codecov/codecov-action@v5
Expand Down
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -326,4 +326,10 @@ cython_debug/
docs/_book

# TODO: where does this rule come from?
test/
test/

# Allow user to locally specify python version
.python-version

# files generated by hatchling vcs
_version.py
27 changes: 18 additions & 9 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,22 @@ repos:
hooks:
- id: black
args:
- --line-length=79
- --safe
- --config=pyproject.toml

- repo: https://github.com/PyCQA/flake8
rev: 7.3.0
hooks:
- id: flake8
language_version: python3
args:
# See https://black.readthedocs.io/en/stable/guides/using_black_with_other_tools.html#e203
- --extend-ignore=E501,E203
- --extend-select=B950
- --per-file-ignores=./app/api/models.py:F722
# TODO: replace this with ruff to bring all config into pyproject.toml
- --config=.flake8

- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: check-yaml
- id: check-toml
- id: pretty-format-json
args:
- "--autofix"
Expand All @@ -36,16 +34,27 @@ repos:
hooks:
- id: isort
args:
- "--profile=black"
- "--filter-files"
- "--line-length=79"
- --settings-path=pyproject.toml

- repo: https://github.com/codespell-project/codespell
rev: v2.4.1
hooks:
- id: codespell
additional_dependencies:
- tomli
args:
- --toml=pyproject.toml

- repo: https://github.com/iamthefij/docker-pre-commit
rev: v3.0.1
hooks:
- id: docker-compose-check

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.19.1
hooks:
- id: mypy
additional_dependencies:
- pydantic
args:
- --config-file=pyproject.toml
17 changes: 13 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
FROM python:3.10
# We are using a two-stage build here because we use uv for dependency management
# But decide to keep it out of the production image.
# The Pre-build stage generates a pip compatible lockfile and then installs with pip
FROM ghcr.io/astral-sh/uv:latest AS prebuild

WORKDIR /usr/src/
WORKDIR /build

COPY ./requirements.txt /usr/src/app/requirements.txt
COPY pyproject.toml uv.lock ./
RUN uv export --frozen --no-dev --no-hashes -o requirements.txt

RUN pip install --no-cache-dir --upgrade -r /usr/src/app/requirements.txt
# Build stage
FROM python:3.10

WORKDIR /usr/src/
COPY --from=prebuild /build/requirements.txt /usr/src/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /usr/src/requirements.txt

COPY ./app /usr/src/app

Expand Down
63 changes: 50 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[![Main branch check status](https://img.shields.io/github/check-runs/neurobagel/api/main?style=flat-square&logo=github)](https://github.com/neurobagel/api/actions?query=branch:main)
[![Tests Status](https://img.shields.io/github/actions/workflow/status/neurobagel/api/test.yaml?branch=main&style=flat-square&logo=github&label=tests)](https://github.com/neurobagel/api/actions/workflows/test.yaml)
[![Codecov](https://img.shields.io/codecov/c/github/neurobagel/api?token=ZEOGQFFZMJ&style=flat-square&logo=codecov&link=https%3A%2F%2Fcodecov.io%2Fgh%2Fneurobagel%2Fapi)](https://app.codecov.io/gh/neurobagel/api)
[![Python versions static](https://img.shields.io/badge/python-3.10-blue?style=flat-square&logo=python)](https://www.python.org)
[![Python versions static](https://img.shields.io/badge/python-3.10--3.13-blue?style=flat-square&logo=python)](https://www.python.org)
[![License](https://img.shields.io/github/license/neurobagel/api?style=flat-square&color=purple&link=LICENSE)](LICENSE)
[![Docker Image Version (tag)](https://img.shields.io/docker/v/neurobagel/api/latest?style=flat-square&logo=docker&link=https%3A%2F%2Fhub.docker.com%2Fr%2Fneurobagel%2Fapi%2Ftags)](https://hub.docker.com/r/neurobagel/api/tags)
[![Docker Pulls](https://img.shields.io/docker/pulls/neurobagel/api?style=flat-square&logo=docker&link=https%3A%2F%2Fhub.docker.com%2Fr%2Fneurobagel%2Fapi%2Ftags)](https://hub.docker.com/r/neurobagel/api/tags)
Expand All @@ -31,9 +31,9 @@ Please refer to our [**official documentation**](https://neurobagel.org/user_gui
- [Troubleshooting](#troubleshooting)
- [Testing](#testing)
- [The default Neurobagel SPARQL query](#the-default-neurobagel-sparql-query)
- [Updating dependencies](#updating-dependencies)
- [License](#license)


## Quickstart
The API is hosted at https://api.neurobagel.org/ and interfaces with Neurobagel's graph database. Queries of the graph can be run using the `/query` route.

Expand Down Expand Up @@ -113,27 +113,42 @@ curl http://127.0.0.1:8000/query?sex=snomed:248152002
The response should be a list of dictionaries containing info about datasets with participants matching the query.

### Python
#### `uv`
We use `uv` to facilitate dependency management and reproducible environments.
If you are setting up a local development environment for the first time,
install `uv` following the [docs](https://docs.astral.sh/uv/getting-started/installation/).

#### Install dependencies

After cloning the repository, install the dependencies outlined in the requirements.txt file. For convenience, you can use Python's `venv` package to install dependencies in a virtual environment. You can find the instructions on creating and activating a virtual environment in the official [documentation](https://docs.python.org/3.10/library/venv.html). After setting up and activating your environment, you can install the dependencies by running the following command in your terminal:
After cloning the repository, install the package in editable mode with development dependencies using [uv](https://docs.astral.sh/uv/):

```bash
$ pip install -r requirements.txt
uv sync --group dev
```

This will create a virtual environment (if one doesn't exist)
called `.venv` in the repository root and install all project and dev dependencies into that environment.

#### Launch the API

To launch the API make sure you're in repository's main directory and in your environment where the dependencies are installed and environment variables are set.
To launch the API, make sure you're in the repository root and that your [`.env` file has been configured](#set-the-environment-variables).

Export the variables defined in your `.env` file:
```bash
export $(cat .env | xargs)
```

You can then launch the API by running the following command in your terminal:
You can then launch the API using either of these methods:

**Option 1: Using uv run (recommended)**
```bash
uv run python -m app.main
```
This launches the API inside the project environment without requiring you to activate a virtual environment first.
**Option 2: Activate the virtual environment manually**
```bash
$ python -m app.main
source .venv/bin/activate
python -m app.main
```

```bash
Expand All @@ -154,7 +169,7 @@ If you get a 401 response to your API request with an `"Unauthorized: "` error m

Neurobagel API utilizes [Pytest](https://docs.pytest.org/en/7.2.x/) framework for testing.

To run the tests, first ensure you're in the repository's root directory and in the environment where the dependencies are installed.
To run the tests, first ensure you're in the repository's root directory.

Install the submodules used by the tests:
```bash
Expand All @@ -165,7 +180,7 @@ git submodule update
You can then run the tests by executing the following command in your terminal:

```bash
pytest tests
uv run pytest
```

To run the integration tests of SPARQL queries (skipped by default), also launch the test graph store:
Expand All @@ -179,14 +194,14 @@ since docker compose will try to use `.env` by default._

Then, run all tests using:
```bash
pytest -m "integration or not integration"
uv run pytest -m "integration or not integration"
# OR
pytest -m ""
uv run pytest -m ""
```

Or, to run only the integration tests:
```bash
pytest -m "integration"
uv run pytest -m "integration"
```

Once you are done with testing, you can stop and remove the test graph container:
Expand All @@ -202,8 +217,12 @@ This file is mainly intended for reference because in normal operations,
the API will always talk to the graph on behalf of the user.

(For developers)
To regenerate this sample query when the API query template is updated, run the following commands from the repository root in an interactive Python terminal:
To regenerate this sample query when the API query template is updated, first launch an interactive Python terminal from the repository root:

```bash
uv run python
```
Then, run the following commands:
```python
from app.main import fetch_supported_namespaces_for_config
from app.api import env_settings
Expand All @@ -215,6 +234,24 @@ with open("docs/default_neurobagel_query.rq", "w") as file:
file.write(create_query(return_agg=False))
```

#### Updating dependencies

We use `uv` to manage dependencies.
To [add a new dependency](https://docs.astral.sh/uv/concepts/projects/dependencies/#adding-dependencies), use:

```bash
uv add <dependency>
```

`uv` creates a lockfile (`uv.lock`) with exact version pins based on the `pyproject.toml`. We use this lockfile for deterministic builds
and to make sure that production and development environments are the same.
If you modify existing dependencies in the `pyproject.toml`,
you must:

1. [Update the lockfile](https://docs.astral.sh/uv/concepts/projects/sync/#automatic-lock-and-sync):



## License

Neurobagel API is released under the terms of the [MIT License](LICENSE)
13 changes: 8 additions & 5 deletions app/api/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import asyncio
from collections import defaultdict
from typing import Optional

import httpx
import pandas as pd
Expand Down Expand Up @@ -32,7 +33,9 @@
]


async def post_query_to_graph(query: str, timeout: float = None) -> dict:
async def post_query_to_graph(
query: str, timeout: Optional[float] = None
) -> list[dict]:
"""
Makes a post request to the graph API to perform a query, using parameters from the environment.

Expand Down Expand Up @@ -150,7 +153,7 @@ async def query_available_modalities_and_pipelines(
lambda pipeline_versions: list(pipeline_versions.dropna().unique())
)
)
dataset_pipelines = defaultdict(dict)
dataset_pipelines: dict[str, dict] = defaultdict(dict)
for (dataset_uuid, pipeline_name), versions in pipeline_versions.items():
dataset_pipelines[dataset_uuid][pipeline_name] = versions
# Cast back to regular dict to avoid unpredictable defaultdict behavior downstream
Expand Down Expand Up @@ -287,7 +290,7 @@ async def query_records(
}

if settings.return_agg:
subject_data = "protected"
subject_data: str | list = "protected"
else:
dataset_matching_records = dataset_matching_records.drop(
dataset_cols, axis=1
Expand Down Expand Up @@ -361,7 +364,7 @@ async def post_subjects(query: SubjectsQueryModel):
continue

if settings.return_agg:
subject_data = "protected"
subject_data: str | list = "protected"
else:
subject_data = util.construct_matching_sub_results_for_dataset(
dataset_matching_records
Expand Down Expand Up @@ -512,7 +515,7 @@ async def get_terms(
)
# Since the term vocabulary for a standardized variable can contain terms from several namespaces,
# we first have to locate the namespace used in the term we are looking up
namespace_terms = next(
namespace_terms: list = next(
(
namespace["terms"]
for namespace in std_trm_vocab
Expand Down
11 changes: 7 additions & 4 deletions app/api/env_settings.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Configuration environment variables for the API."""

from pathlib import Path
from typing import Any

from pydantic import Field, computed_field
from pydantic_settings import BaseSettings, SettingsConfigDict
Expand All @@ -13,9 +14,11 @@
# see https://www.starlette.io/applications/#storing-state-on-the-app-instance).
# This avoids having to thread the request object through every function that needs access to e.g., the context
# and also makes it easier to mock the configuration during testing.
CONTEXT = {}
ALL_VOCABS = {}
DATASETS_METADATA = {}
# TODO: do something a bit more precise with these type hints.
# For now they are here so mypy is happy.
CONTEXT: dict[str, Any] = {}
ALL_VOCABS: dict[str, Any] = {}
DATASETS_METADATA: dict[str, Any] = {}


class Settings(BaseSettings):
Expand Down Expand Up @@ -54,7 +57,7 @@ class Settings(BaseSettings):
description="The name of the community configuration to use to query the graph data.",
)

@computed_field
@computed_field # type: ignore[prop-decorator]
@property
def query_url(self) -> str:
"""Construct the URL of the graph store to be queried."""
Expand Down
Loading
Loading