Skip to content

Commit

Permalink
Merge pull request #7 from TheJacksonLaboratory/G3-9-get-geneset-by-i…
Browse files Browse the repository at this point in the history
…d-endpoint

G3 9 get geneset by id endpoint
  • Loading branch information
francastell authored Jan 4, 2024
2 parents 4dbfbd3 + 6183b22 commit 9ae1739
Show file tree
Hide file tree
Showing 12 changed files with 1,175 additions and 811 deletions.
22 changes: 22 additions & 0 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,28 @@ virtualenv activated, or just run `poetry run uvicorn geneweaver.api.main:app --
This will host the application on `http://127.0.0.1:8000/` which means the swagger docs
page is available at `http://127.0.0.1:8000/docs`.

### Code linters

Ruff rules: (https://docs.astral.sh/ruff/rules/)

From active local environment on command line run:

ruff src/ tests/ --fix

then run black (code formatter: https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html)

black src/ tests/

### Unit test

Create appropriate test per code modifications without dependencies to external resources
such as DB, Webserver, or APIs. Create appropriate mocks for external resources and data as needed.

Tests directory: geneweaver-api/tests

To execute tests, from the command line run:

pytest

### Continuous Integration & Deployment
When a PR is crated in GitHub, it will automatically trigger a workflow that will run
Expand Down
1,677 changes: 872 additions & 805 deletions poetry.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ packages = [

[tool.poetry.dependencies]
python = "^3.9"

geneweaver-core = "^0.8.0a0"
fastapi = {extras = ["all"], version = "^0.99.1"}
uvicorn = {extras = ["standard"], version = "^0.24.0"}
Expand Down
17 changes: 11 additions & 6 deletions src/geneweaver/api/controller/genesets.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
from fastapi import APIRouter, Depends, HTTPException, Security
from geneweaver.api import dependencies as deps
from geneweaver.api.schemas.auth import UserInternal
from geneweaver.api.services import geneset as genset_service
from geneweaver.core.schema.geneset import GenesetUpload
from geneweaver.db import geneset as db_geneset
from geneweaver.db import geneset_value as db_geneset_value
from geneweaver.db.geneset import is_readable

from . import message as api_message

router = APIRouter(prefix="/genesets")

Expand All @@ -29,12 +31,15 @@ def get_geneset(
cursor: Optional[deps.Cursor] = Depends(deps.cursor),
) -> dict:
"""Get a geneset by ID."""
if not is_readable(cursor, user.id, geneset_id):
raise HTTPException(status_code=403, detail="Forbidden")
response = genset_service.get_geneset(cursor, geneset_id, user)

if "error" in response:
if response.get("message") == api_message.ACCESS_FORBIDEN:
raise HTTPException(status_code=403, detail=api_message.ACCESS_FORBIDEN)
else:
raise HTTPException(status_code=500, detail=api_message.UNEXPECTED_ERROR)

geneset = db_geneset.by_id(cursor, geneset_id)
geneset_values = db_geneset_value.by_geneset_id(cursor, geneset_id)
return {"geneset": geneset, "geneset_values": geneset_values}
return response


@router.post("")
Expand Down
6 changes: 6 additions & 0 deletions src/geneweaver/api/controller/message.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Constants key/value messages."""


##Errors
ACCESS_FORBIDEN = "Forbidden"
UNEXPECTED_ERROR = "Unexpected Error"
35 changes: 35 additions & 0 deletions src/geneweaver/api/services/geneset.py
Original file line number Diff line number Diff line change
@@ -1 +1,36 @@
"""Service functions for dealing with genesets."""

from fastapi.logger import logger
from geneweaver.api.controller import message
from geneweaver.api.schemas.auth import User
from geneweaver.db import geneset as db_geneset
from geneweaver.db import geneset_value as db_geneset_value
from geneweaver.db.geneset import is_readable as db_is_readable
from psycopg import Cursor


def get_geneset(cursor: Cursor, geneset_id: int, user: User) -> dict:
"""Get a geneset by ID."""
try:
if not is_geneset_readable_by_user(cursor, geneset_id, user):
return {"error": True, "message": message.ACCESS_FORBIDEN}

geneset = db_geneset.by_id(cursor, geneset_id)
geneset_values = db_geneset_value.by_geneset_id(cursor, geneset_id)
return {"geneset": geneset, "geneset_values": geneset_values}

except Exception as err:
logger.error(err)
raise err


def is_geneset_readable_by_user(cursor: Cursor, geneset_id: int, user: User) -> bool:
"""Check if the user can read the geneset from DB."""
readable: bool = False
try:
readable = db_is_readable(cursor, user.id, geneset_id)
except Exception as err:
logger.error(err)
raise err

return readable
1 change: 1 addition & 0 deletions tests/controllers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Tests for controllers."""
68 changes: 68 additions & 0 deletions tests/controllers/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""Fixtures for the controller tests."""
import importlib.resources
import json
from unittest.mock import Mock

import psycopg
import pytest
from fastapi.testclient import TestClient
from geneweaver.api.core.config_class import GeneweaverAPIConfig

# Load test data
# Opening JSON file
str_json = importlib.resources.read_text("tests.data", "response_geneset_1234.json")
# returns JSON string as a dictionary
test_data = json.loads(str_json)

response_mock = Mock()
response_mock.status_code = 200
response_mock.json.return_value = test_data


# Mock dependencies
def mock_full_user() -> Mock:
"""User auth mock."""
m1 = Mock()
return m1.AsyncMock()


def mock_cursor() -> psycopg.Cursor:
"""DB cursor mock."""
m2 = Mock()
return m2.AsyncMock()


@pytest.fixture()
def mock_settings(monkeypatch) -> GeneweaverAPIConfig:
"""Patch the settings class to return a test settings instance.
returns: A patched settings instance.
"""
test_settings = GeneweaverAPIConfig(
DB_HOST="localhost",
DB_USERNAME="postgres",
DB_PASSWORD="postgres",
DB_NAME="geneweaver",
)

monkeypatch.setattr(
"geneweaver.api.core.config_class.GeneweaverAPIConfig", lambda: test_settings
)

return test_settings


@pytest.fixture()
def client(mock_settings) -> TestClient:
"""Provide a mocked FastAPI application.
returns: A mocked FastAPI application.
"""
from geneweaver.api.dependencies import cursor, full_user
from geneweaver.api.main import app

test_app = TestClient(app)

app.dependency_overrides.update({full_user: mock_full_user, cursor: mock_cursor})

return test_app
37 changes: 37 additions & 0 deletions tests/controllers/test_genesets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Tests for geneset API."""
from unittest.mock import patch

import pytest

from tests.controllers.conftest import test_data


@patch("geneweaver.api.services.geneset.get_geneset")
@patch("geneweaver.api.services.geneset.is_geneset_readable_by_user")
def test_get_geneset_response(mock_genset_is_readable, mock_get_genenset, client):
"""Test get geneset ID data response."""
mock_genset_is_readable.return_value = True
mock_get_genenset.return_value = test_data

response = client.get("/api/genesets/1234")
assert response.status_code == 200
assert response.json() == test_data


@patch("geneweaver.api.services.geneset.db_is_readable")
def test_get_geneset_forbidden(mock_genset_is_readable, client):
"""Test forbidden response."""
mock_genset_is_readable.return_value = False
response = client.get("/api/genesets/1234")

assert response.json() == {"detail": "Forbidden"}
assert response.status_code == 403


@patch("geneweaver.api.services.geneset.db_is_readable")
def test_get_geneset_unexpected_error(mock_genset_is_readable, client):
"""Test unexpected error response."""
mock_genset_is_readable.side_effect = Exception

with pytest.raises(expected_exception=Exception):
client.get("/api/genesets/1234")
1 change: 1 addition & 0 deletions tests/data/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Data package for tests."""
50 changes: 50 additions & 0 deletions tests/data/response_geneset_1234.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"geneset": null,
"geneset_values": [
{
"gs_id": 1234,
"ode_gene_id": 70495,
"gsv_value": 1,
"gsv_hits": 1,
"gsv_source_list": [
"PDE4DIP"
],
"gsv_value_list": [
1
],
"gsv_in_threshold": true,
"gsv_date": "2020-05-05",
"ode_ref_id": "PDE4DIP"
},
{
"gs_id": 1234,
"ode_gene_id": 83819,
"gsv_value": 1,
"gsv_hits": 1,
"gsv_source_list": [
"TMEPAI"
],
"gsv_value_list": [
1
],
"gsv_in_threshold": true,
"gsv_date": "2020-05-05",
"ode_ref_id": "PMEPA1"
},
{
"gs_id": 1234,
"ode_gene_id": 90284,
"gsv_value": 1,
"gsv_hits": 1,
"gsv_source_list": [
"PREPL"
],
"gsv_value_list": [
1
],
"gsv_in_threshold": true,
"gsv_date": "2020-05-05",
"ode_ref_id": "PREPL"
}
]
}
71 changes: 71 additions & 0 deletions tests/services/test_genset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""Tests for geneset Service."""

import importlib.resources
import json
from unittest.mock import patch

import pytest
from geneweaver.api.controller import message
from geneweaver.api.services import geneset

## Load test data
# Opening JSON file
str_json = importlib.resources.read_text("tests.data", "response_geneset_1234.json")
# returns JSON string as a dictionary
test_data = json.loads(str_json)


@patch("geneweaver.api.services.geneset.db_geneset")
@patch("geneweaver.api.services.geneset.db_geneset_value")
@patch("geneweaver.api.services.geneset.is_geneset_readable_by_user")
def test_get_geneset(mock_genset_readable_func, mock_db_geneset, mock_db_genset_value):
"""Test basic get geneset by ID."""
mock_genset_readable_func.return_value = True
mock_db_geneset.by_id.return_value = {}
mock_db_genset_value.by_id.return_value = {}
response = geneset.get_geneset(None, 1234, None)
assert response.get("error") is None


@patch("geneweaver.api.services.geneset.is_geneset_readable_by_user")
def test_get_geneset_no_user_access(mock_genset_readable_func):
"""Test get geneset by ID with no user access."""
mock_genset_readable_func.return_value = False
response = geneset.get_geneset(None, 1234, None)
assert response.get("error") is True
assert response.get("message") == message.ACCESS_FORBIDEN


@patch("geneweaver.api.services.geneset.db_geneset")
@patch("geneweaver.api.services.geneset.db_geneset_value")
@patch("geneweaver.api.services.geneset.is_geneset_readable_by_user")
def test_get_geneset_returned_values(
mock_genset_readable_func, mock_db_genset_value, mock_db_geneset
):
"""Test get geneset by ID data response structure."""
mock_genset_readable_func.return_value = True
mock_db_geneset.by_id.return_value = test_data.get("geneset")
mock_db_genset_value.by_geneset_id.return_value = test_data.get("geneset_values")
response = geneset.get_geneset(None, 1234, None)

assert response.get("genset") == test_data["geneset"]
assert response.get("geneset_values") == test_data["geneset_values"]


@patch("geneweaver.api.services.geneset.db_is_readable")
@patch("geneweaver.api.services.geneset.User")
def test_is_redable_by_user(mock_user, mock_genset_is_readable):
"""Test is geneset ID readable by passed user."""
mock_genset_is_readable.return_value = True
response = geneset.is_geneset_readable_by_user(None, 1234, mock_user)
assert response is True


@patch("geneweaver.db.geneset.is_readable")
@patch("geneweaver.api.services.geneset.User")
def test_is_redable_by_user_error(mock_user, mock_genset_is_readable):
"""Test is geneset ID readable with server error."""
mock_genset_is_readable.sideEffect = Exception

with pytest.raises(expected_exception=Exception):
geneset.is_geneset_readable_by_user(None, 1234, mock_user)

0 comments on commit 9ae1739

Please sign in to comment.