Skip to content

Commit

Permalink
Merge pull request #98 from TheJacksonLaboratory/G3-462-add-general-s…
Browse files Browse the repository at this point in the history
…earch-functions-for-geneset-search

G3 462 add general search functions for geneset search
  • Loading branch information
bergsalex authored Sep 27, 2024
2 parents bf866a3 + b7b8ab9 commit bc5d457
Show file tree
Hide file tree
Showing 9 changed files with 403 additions and 308 deletions.
562 changes: 288 additions & 274 deletions poetry.lock

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "geneweaver-api"
version = "0.9.0"
version = "0.10.0a0"
description = "The Geneweaver API"
authors = [
"Alexander Berger <alexander.berger@jax.org>",
Expand All @@ -18,9 +18,9 @@ packages = [
[tool.poetry.dependencies]
python = "^3.9"
geneweaver-core = "^0.10.0a3"
fastapi = {extras = ["all"], version = "^0.111.0"}
fastapi = {extras = ["all"], version = "^0.115.0"}
uvicorn = {extras = ["standard"], version = "^0.30.0"}
geneweaver-db = "0.5.0a17"
geneweaver-db = "0.5.0a21"
psycopg-pool = "^3.1.7"
requests = "^2.32.3"
python-jose = {extras = ["cryptography"], version = "^3.3.0"}
Expand Down
20 changes: 19 additions & 1 deletion src/geneweaver/api/controller/genesets.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@
from fastapi import APIRouter, Depends, HTTPException, Path, Query, Security
from fastapi.responses import FileResponse, StreamingResponse
from geneweaver.api import dependencies as deps
from geneweaver.api.schemas.apimodels import GeneValueReturn
from geneweaver.api.schemas.apimodels import GeneValueReturn, SearchResponse
from geneweaver.api.schemas.auth import UserInternal
from geneweaver.api.schemas.search import GenesetSearch
from geneweaver.api.services import geneset as genset_service
from geneweaver.api.services import publications as publication_service
from geneweaver.core.enum import GeneIdentifier, GenesetTier, Species
from geneweaver.core.schema.score import GenesetScoreType, ScoreType
from geneweaver.db import search as db_search
from typing_extensions import Annotated

from . import message as api_message
Expand Down Expand Up @@ -147,6 +149,22 @@ def get_visible_genesets(
return response


@router.get("/search")
def search(
geneset_search: Annotated[GenesetSearch, Query()],
user: UserInternal = Security(deps.optional_full_user),
cursor: Optional[deps.Cursor] = Depends(deps.cursor),
) -> SearchResponse:
"""Search genesets."""
return SearchResponse(
db_search.genesets(
cursor,
is_readable_by=0 if user is None else user.id,
**geneset_search.model_dump(exclude_none=True),
)
)


@router.get("/{geneset_id}")
def get_geneset(
geneset_id: Annotated[
Expand Down
25 changes: 12 additions & 13 deletions src/geneweaver/api/controller/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

from fastapi import APIRouter, Depends, Query, Security
from geneweaver.api import dependencies as deps
from geneweaver.api.schemas.apimodels import GsPubSearchType, SeachResponse
from geneweaver.api.schemas.apimodels import CombinedSearchResponse, GsPubSearchType
from geneweaver.api.schemas.auth import UserInternal
from geneweaver.api.services import geneset as genset_service
from geneweaver.api.services import publications as publication_service
from geneweaver.db import search as db_search
from typing_extensions import Annotated

from . import message as api_message
Expand Down Expand Up @@ -41,20 +41,19 @@ def search(
description=api_message.OFFSET,
),
] = None,
) -> SeachResponse:
) -> CombinedSearchResponse:
"""Search genesets and publications."""
data = {}
response = SeachResponse(data=data)
search_results = {}
if "genesets" in entities:
geneset_response = genset_service.get_visible_genesets(
cursor=cursor,
user=user,
genesets = db_search.genesets(
cursor,
search_text=search_text,
is_readable_by=0 if user is None else user.id,
limit=limit,
offset=offset,
)

data["geneset"] = geneset_response.get("data")
search_results["genesets"] = genesets

if "publications" in entities:
pub_response = publication_service.get(
Expand All @@ -64,8 +63,8 @@ def search(
offset=offset,
)

data["publications"] = pub_response.get("data")

response.data = data
search_results["publications"] = pub_response.get("data")

return response
return CombinedSearchResponse(
object=search_results,
)
16 changes: 13 additions & 3 deletions src/geneweaver/api/core/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ async def get_auth_header(
auto_error_auth: Optional[bool] = False,
) -> Optional[Dict[str, str]]:
"""Get the auth header from the token."""
user = await self.get_user(security_scopes, creds, auto_error_auth)
user = await self._get_user(security_scopes, creds, auto_error_auth)
return user.auth_header if user else None

async def get_user_strict(
Expand All @@ -140,9 +140,19 @@ async def get_user_strict(
),
) -> UserInternal:
"""Get the user from the token, raise an exception if not found."""
return await self.get_user(security_scopes, creds, True, disallow_public=True)
return await self._get_user(security_scopes, creds, True, disallow_public=True)

async def get_user( # noqa: C901
async def get_user(
self,
security_scopes: SecurityScopes,
creds: Optional[HTTPAuthorizationCredentials] = Depends(
Auth0HTTPBearer(auto_error=False)
),
) -> Optional[UserInternal]:
"""Get the user from the token, don't error if not found."""
return await self._get_user(security_scopes, creds, True, disallow_public=False)

async def _get_user( # noqa: C901
self,
security_scopes: SecurityScopes,
creds: Optional[HTTPAuthorizationCredentials] = Depends(
Expand Down
34 changes: 31 additions & 3 deletions src/geneweaver/api/schemas/apimodels.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
"""Models for API requests."""

# ruff: noqa: ANN002, ANN003

from enum import Enum
from typing import Iterable, List, Optional
from typing import Dict, Generic, Iterable, List, Optional, TypeVar

from geneweaver.core.enum import GeneIdentifier, Species
from geneweaver.core.schema.gene import Gene as GeneSchema
from geneweaver.core.schema.geneset import GeneValue as GeneValueSchema
from geneweaver.core.schema.species import Species as SpeciesSchema
from pydantic import AnyUrl, BaseModel

T = TypeVar("T")


class PagingLinks(BaseModel):
"""Schema for holding paging links."""
Expand Down Expand Up @@ -99,7 +103,31 @@ class GsPubSearchType(str, Enum):
PUBLICATIONS = "publications"


class SeachResponse(CollectionResponse):
class SearchResponse(CollectionResponse, Generic[T]):
"""Model for search response endpoint."""

data: dict
data: List[T]

def __init__(self, *args, **kwargs) -> None:
"""Initialize the search response model.
First argument is assigned to `data`.
"""
if args:
kwargs["data"] = args[0]
super().__init__(**kwargs)


class CombinedSearchResponse(BaseModel, Generic[T]):
"""Model for combined search response endpoint."""

object: Dict[str, List[T]]

def __init__(self, *args, **kwargs) -> None:
"""Initialize the combined search response model.
First argument is assigned to `object`.
"""
if args:
kwargs["object"] = args[0]
super().__init__(**kwargs)
26 changes: 26 additions & 0 deletions src/geneweaver/api/schemas/search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""Schema for search API."""

from datetime import date
from typing import Optional, Set

from geneweaver.core.enum import GenesetTier, ScoreType, Species
from pydantic import BaseModel, Field


class GenesetSearch(BaseModel):
"""Schema for geneset search."""

search_text: str
publication_id: Optional[int] = None
pubmed_id: Optional[int] = None
species: Optional[Set[Species]] = None
curation_tier: Optional[Set[GenesetTier]] = None
score_type: Optional[Set[ScoreType]] = None
lte_count: Optional[int] = None
gte_count: Optional[int] = None
created_before: Optional[date] = None
created_after: Optional[date] = None
updated_before: Optional[date] = None
updated_after: Optional[date] = None
limit: Optional[int] = Field(25, ge=0, le=1000)
offset: Optional[int] = None
12 changes: 6 additions & 6 deletions tests/controllers/test_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,19 @@ def test_pub_search(mock_pub_service_call, client):
)

assert response.status_code == 200
assert response.json().get("data").get("publications") == get_publications.get(
assert response.json().get("object").get("publications") == get_publications.get(
"data"
)


@patch("geneweaver.api.services.geneset.get_visible_genesets")
def test_genesets_search_response(mock_get_visible_genesets, client):
@patch("geneweaver.api.controller.search.db_search.genesets")
def test_genesets_search_response(mock_geneset_search, client):
"""Test search for geneset data response."""
mock_data = geneset_by_id_resp.get("geneset")
mock_get_visible_genesets.return_value = mock_data
mock_data = [geneset_by_id_resp.get("geneset")]
mock_geneset_search.return_value = mock_data

response = client.get(
url="/api/search/", params={"entities": "genesets", "search_text": "gene"}
)
assert response.status_code == 200
assert response.json().get("data").get("genesets") == mock_data.get("data")
assert response.json().get("object").get("genesets") == mock_data
10 changes: 5 additions & 5 deletions tests/core/test_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ async def test_invalid_token_format(mock_requests, mock_security_scope):
creds = HTTPAuthorizationCredentials(scheme="", credentials="token")

with pytest.raises(expected_exception=Auth0UnauthenticatedException):
await auth.get_user(
await auth._get_user(
security_scopes=mock_security_scope,
creds=creds,
auto_error_auth=True,
Expand All @@ -118,7 +118,7 @@ async def test_valid_jwt_token(
token = create_test_token()
creds = HTTPAuthorizationCredentials(credentials=token, scheme="")

user: UserInternal = await auth.get_user(
user: UserInternal = await auth._get_user(
security_scopes=mock_security_scope,
creds=creds,
auto_error_auth=True,
Expand Down Expand Up @@ -228,7 +228,7 @@ async def test_is_user_not_public(
):
"""Test user is not public."""
auth = do_auth()
is_public = await auth.get_user(
is_public = await auth._get_user(
security_scopes=mock_security_scope, creds=None, disallow_public=False
)

Expand Down Expand Up @@ -257,7 +257,7 @@ async def test_invalid_claim(
creds = HTTPAuthorizationCredentials(credentials=token, scheme="")

with pytest.raises(expected_exception=Auth0UnauthenticatedException):
await auth.get_user(
await auth._get_user(
security_scopes=mock_security_scope,
creds=creds,
auto_error_auth=True,
Expand Down Expand Up @@ -289,7 +289,7 @@ async def test_missing_claim_email_error_claim(
creds = HTTPAuthorizationCredentials(credentials=token, scheme="")

with pytest.raises(expected_exception=Auth0UnauthorizedException):
await auth.get_user(
await auth._get_user(
security_scopes=mock_security_scope,
creds=creds,
auto_error_auth=True,
Expand Down

0 comments on commit bc5d457

Please sign in to comment.