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

G3 226 add publication endpoint #62

Merged
merged 5 commits into from
May 2, 2024
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
344 changes: 170 additions & 174 deletions poetry.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "geneweaver-api"
version = "0.4.0"
version = "0.5.0a0"
description = "The Geneweaver API"
authors = [
"Alexander Berger <[email protected]>",
Expand All @@ -21,7 +21,7 @@ python = "^3.9"
geneweaver-core = "^0.9.1"
fastapi = {extras = ["all"], version = "^0.99.1"}
uvicorn = {extras = ["standard"], version = "^0.24.0"}
geneweaver-db = "^0.3.0"
geneweaver-db = "^0.3.3"
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍

psycopg-pool = "^3.1.7"
requests = "^2.31.0"
python-jose = {extras = ["cryptography"], version = "^3.3.0"}
Expand Down
2 changes: 2 additions & 0 deletions src/geneweaver/api/controller/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
GENE_IDENTIFIER_TYPE_VALUE_ERROR = "Invalid gene identifier type"
RECORD_NOT_FOUND_ERROR = "Record not found"
INVALID_PUBMED_ID_ERROR = "Invalid pubmed id"
RECORD_EXISTS = "Record already in the system"
PUBMED_RETRIEVING_ERROR = "Error retrieving publication info from PubMed API"

##FORM field descriptions
GENE_REFERENCE = "The reference id to search for"
Expand Down
32 changes: 31 additions & 1 deletion src/geneweaver/api/controller/publications.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

from typing import Optional

from fastapi import APIRouter, Depends, HTTPException, Path
from fastapi import APIRouter, Depends, HTTPException, Path, Security
from geneweaver.api import dependencies as deps
from geneweaver.api.schemas.apimodels import NewPubmedRecord
from geneweaver.api.schemas.auth import UserInternal
from geneweaver.api.services import publications as publication_service
from geneweaver.core.schema.publication import Publication
from typing_extensions import Annotated
Expand Down Expand Up @@ -33,3 +35,31 @@ def get_publication_by_id(
raise HTTPException(status_code=404, detail=api_message.RECORD_NOT_FOUND_ERROR)

return response


@router.put("/{publication_id}")
def add_publication(
publication_id: Annotated[
int, Path(format="int64", minimum=0, maxiumum=9223372036854775807)
],
user: UserInternal = Security(deps.full_user),
cursor: Optional[deps.Cursor] = Depends(deps.cursor),
) -> NewPubmedRecord:
"""Add pubmed publication endpoint."""
response = publication_service.add_pubmed_record(
cursor=cursor, user=user, pubmed_id=publication_id
)

if "error" in response:
if response.get("message") == api_message.ACCESS_FORBIDDEN:
raise HTTPException(status_code=403, detail=api_message.ACCESS_FORBIDDEN)
elif response.get("message") == api_message.RECORD_EXISTS:
raise HTTPException(status_code=412, detail=api_message.RECORD_EXISTS)
elif response.get("message") == api_message.PUBMED_RETRIEVING_ERROR:
raise HTTPException(
status_code=422, detail=api_message.PUBMED_RETRIEVING_ERROR
)
else:
raise HTTPException(status_code=500, detail=api_message.UNEXPECTED_ERROR)

return response
6 changes: 2 additions & 4 deletions src/geneweaver/api/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,8 @@ def cursor(request: Request) -> Generator:
with conn.cursor(row_factory=dict_row) as cur:
yield cur

def _get_user_details(
cursor: Cursor,
user: UserInternal
) -> UserInternal:

def _get_user_details(cursor: Cursor, user: UserInternal) -> UserInternal:
"""Get the user details.

:param cursor: The database cursor.
Expand Down
7 changes: 7 additions & 0 deletions src/geneweaver/api/schemas/apimodels.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,10 @@ class GeneValueReturn(BaseModel):
"""Model for geneset values endpoint return."""

data: List[GeneValueSchema]


class NewPubmedRecord(BaseModel):
"""Model returned for adding new pubmed info into DB."""

pub_id: int
pubmed_id: int
46 changes: 43 additions & 3 deletions src/geneweaver/api/services/publications.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
"""Service functions for publications."""

from fastapi.logger import logger
from geneweaver.api.controller import message
from geneweaver.api.schemas.auth import User
from geneweaver.core.exc import ExternalAPIError
from geneweaver.core.publication import pubmed
from geneweaver.db import publication as db_publication
from psycopg import Cursor

Expand All @@ -22,17 +26,53 @@ def get_publication(cursor: Cursor, pub_id: int) -> dict:
return pub


def get_publication_by_pubmed_id(cursor: Cursor, pub_med_id: str) -> dict:
def get_publication_by_pubmed_id(cursor: Cursor, pubmed_id: str) -> dict:
"""Get a publication by Pubmed Id from the DB.

@param cursor: DB cursor
@param pub_med_id: pub med identifier
@param pubmed_id: pub med identifier
@return: dictionary response (publication).
"""
try:
pub = db_publication.by_pubmed_id(cursor, pub_med_id)
pub = db_publication.by_pubmed_id(cursor, pubmed_id)
return pub

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


def add_pubmed_record(cursor: Cursor, user: User, pubmed_id: str) -> dict:
"""Add pubmed publication to the DB given pub-med id.

@param cursor: DB cursor
@param user: logged-in user
@param pubmed_id: pubmed id
@return:
"""
try:
# check logged-in user
if user is None or user.id is None:
return {"error": True, "message": message.ACCESS_FORBIDDEN}
# check publication is not already in the DB
pub = db_publication.by_pubmed_id(cursor, pubmed_id)
if pub:
return {"pubmed_id": pubmed_id, "pub_id": pub.get("id")}

# retrieve publication info to be stored
pub_record = pubmed.get_publication(pubmed_id=pubmed_id)
# persist publication in DB
results = db_publication.add(cursor, pub_record)

if results is None:
return {"error": True, "message": message.UNEXPECTED_ERROR}

return {"pubmed_id": pubmed_id, "pub_id": results.get("pub_id")}

except ExternalAPIError as err:
logger.error(err)
return {"error": True, "message": message.PUBMED_RETRIEVING_ERROR}

except Exception as err:
logger.error(err)
raise err
45 changes: 45 additions & 0 deletions tests/controllers/test_publications.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

publication_by_id_resp = test_publication_data.get("publication_by_id")
publication_by_pubmed_id_resp = test_publication_data.get("publication_by_pubmed_id")
add_pubmed_info = test_publication_data.get("add_pubmed_info")
add_pubmed_resp = test_publication_data.get("add_pubmed_resp")


@patch("geneweaver.api.services.publications.get_publication")
Expand Down Expand Up @@ -65,3 +67,46 @@ def test_invalid_pub_id_type(mock_pub_service_call, client):

response = client.get(url="/api/publications/werte123")
assert response.status_code == 422


@patch("geneweaver.api.services.publications.add_pubmed_record")
def test_add_pubmed_valid_url_req(mock_pub_service_call, client):
"""Test valid url request to add pubmed record."""
mock_pub_service_call.return_value = add_pubmed_resp

response = client.put(url="/api/publications/1234")

assert response.status_code == 200
assert response.json() == add_pubmed_resp


@patch("geneweaver.api.services.publications.add_pubmed_record")
def test_add_pubmed_valid_errors(mock_pub_service_call, client):
"""Test error codes adding pubmed record."""
mock_pub_service_call.return_value = {
"error": True,
"message": message.ACCESS_FORBIDDEN,
}
response = client.put(url="/api/publications/1234")
assert response.status_code == 403

mock_pub_service_call.return_value = {
"error": True,
"message": message.RECORD_EXISTS,
}
response = client.put(url="/api/publications/1234")
assert response.status_code == 412

mock_pub_service_call.return_value = {
"error": True,
"message": message.PUBMED_RETRIEVING_ERROR,
}
response = client.put(url="/api/publications/1234")
assert response.status_code == 422

mock_pub_service_call.return_value = {
"error": True,
"message": message.UNEXPECTED_ERROR,
}
response = client.put(url="/api/publications/1234")
assert response.status_code == 500
2 changes: 2 additions & 0 deletions tests/data/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@
"publication_by_pubmed_id": json.loads(publications_json).get(
"publication_by_pubmed_id"
),
"add_pubmed_info": json.loads(publications_json).get("add_pubmed_info"),
"add_pubmed_resp": json.loads(publications_json).get("add_pubmed_resp"),
}

# Json web token keys data
Expand Down
15 changes: 15 additions & 0 deletions tests/data/publications.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,20 @@
"month": "Jan",
"year": 2008,
"id": 1234
},
"add_pubmed_info": {
"pubmed_id": 1234,
"authors": "D Nadeau, C Marchand",
"title": "Change in the kinetics of sulphacetamide tissue distribution in Walker tumor-bearing rats.",
"abstract": "The effect of Walker tumor on sulphacetamide distribution was studied in rats 21 days after tumor implantation in a hind leg. After oral administration of sulphacetamide (5 and 20 min), the concentration of the drug was found to be lower in the plasma and liver of tumor-bearing rats when compared with that of control group. However, 90 min after sulphacetamide administration, the concentration of the drug in these same tissues was found to be higher in tumor-bearing rats than in control animals. Whereas the tumor had no apparent effect on sulphacetamide concentration in the brain, drug concentrations in the fat tissue of tumor-bearing rats were constantly higher than those of control animals. These changes in sulphacentamide disposition kinetics could be explained in part by delay in gastrointestinal absorption of the drug. Contrary to what was observed after oral administration, constantly higher drug concentrations were found in the plasma of tumor-bearing rats after iv injection of sulphacetamide. Furthermore, the half-life of sulphacetamide in these same animals was much higher than in control animals. It is concluded that, in Walker tumor-bearing rats, there are changes in the kinetics of sulphacetamide which are functions of the route of administration of the drug.",
"journal": "Drug metabolism and disposition: the biological fate of chemicals",
"volume": "3",
"pages": "565-76",
"month": null,
"year": 1975
},
"add_pubmed_resp": {
"pubmed_id": 1234,
"pub_id": 79325
}
}
90 changes: 90 additions & 0 deletions tests/services/test_publications.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,20 @@

from unittest.mock import patch

import pytest
from geneweaver.api.controller import message
from geneweaver.api.schemas.auth import User
from geneweaver.api.services import publications as pub_service
from geneweaver.core.exc import ExternalAPIError

from tests.data import test_publication_data

publication_by_id_resp = test_publication_data.get("publication_by_id")
publication_by_pubmed_id_resp = test_publication_data.get("publication_by_pubmed_id")
add_pubmed_info = test_publication_data.get("add_pubmed_info")
add_pubmed_resp = test_publication_data.get("add_pubmed_resp")
mock_user = User()
mock_user.id = 1


@patch("geneweaver.api.services.publications.db_publication")
Expand All @@ -28,3 +36,85 @@ def test_get_publication_by_pubmed_id(mock_db_publication):
response = pub_service.get_publication_by_pubmed_id(None, "17931734")

assert response == publication_by_id_resp


@patch("geneweaver.api.services.publications.db_publication")
def test_get_publication_errors(mock_db_publication):
"""Test get publication by ID data response structure."""
# unexpected error
mock_db_publication.by_pubmed_id.side_effect = Exception()
with pytest.raises(expected_exception=Exception):
pub_service.get_publication_by_pubmed_id(cursor=None, pubmed_id=1234)

# unexpected error
mock_db_publication.by_id.side_effect = Exception()
with pytest.raises(expected_exception=Exception):
pub_service.get_publication(cursor=None, pub_id=123)


@patch("geneweaver.api.services.publications.pubmed")
@patch("geneweaver.api.services.publications.db_publication")
def test_add_pubmed_publication(mock_db_publication, mock_pubmed):
"""Test persisting non-existing pubmed publication."""
mock_pubmed.get_publication.return_value = add_pubmed_info
mock_db_publication.by_pubmed_id.return_value = None
mock_db_publication.add.return_value = {"pub_id": add_pubmed_resp.get("pub_id")}

# adding pubmed info
response = pub_service.add_pubmed_record(
cursor=None, user=mock_user, pubmed_id=1234
)
assert response == add_pubmed_resp

# pubmed found in DB
mock_db_publication.by_pubmed_id.return_value = publication_by_pubmed_id_resp
pubmed_svc_rsp = {
"pub_id": publication_by_pubmed_id_resp.get("id"),
"pubmed_id": publication_by_pubmed_id_resp.get("pubmed_id"),
}
response = pub_service.add_pubmed_record(
cursor=None, user=mock_user, pubmed_id=17931734
)
assert response.get("error") is None
assert response == pubmed_svc_rsp


@patch("geneweaver.api.services.publications.pubmed")
@patch("geneweaver.api.services.publications.db_publication")
def test_add_pubmed_publication_errors(mock_db_publication, mock_pubmed):
"""Test errors adding pubmed info."""
mock_pubmed.get_publication.return_value = add_pubmed_info
mock_db_publication.by_pubmed_id.return_value = None
mock_db_publication.add.return_value = {"pub_id": add_pubmed_resp.get("pub_id")}

# user is not logged-in
response = pub_service.add_pubmed_record(cursor=None, user=None, pubmed_id=1234)
assert response.get("error") is True
assert response.get("message") == message.ACCESS_FORBIDDEN

# Error retrieving pubmed info
mock_db_publication.by_pubmed_id.return_value = None
mock_pubmed.get_publication.side_effect = ExternalAPIError()
response = pub_service.add_pubmed_record(
cursor=None, user=mock_user, pubmed_id=1234
)
assert response.get("error") is True
assert response.get("message") == message.PUBMED_RETRIEVING_ERROR

# No record returned by DB call
mock_pubmed.get_publication.side_effect = None
mock_pubmed.get_publication.return_value = add_pubmed_info
mock_db_publication.by_pubmed_id.return_value = None
mock_db_publication.add.return_value = None
response = pub_service.add_pubmed_record(
cursor=None, user=mock_user, pubmed_id=1234
)
assert response.get("error") is True
assert response.get("message") == message.UNEXPECTED_ERROR

# unexpected error
mock_db_publication.add.side_effect = Exception()
with pytest.raises(expected_exception=Exception):
response = pub_service.add_pubmed_record(
cursor=None, user=mock_user, pubmed_id=1234
)
Loading