Skip to content

Commit 72a36f4

Browse files
authored
Merge pull request #341 from VariantEffect/jstone-dev/273/target-gene-autocompletion
User-specific target gene search
2 parents e4ec609 + f219284 commit 72a36f4

File tree

3 files changed

+198
-19
lines changed

3 files changed

+198
-19
lines changed

src/mavedb/lib/target_genes.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import logging
2+
from typing import Optional
3+
4+
from sqlalchemy import func, or_
5+
from sqlalchemy.orm import Session
6+
7+
from mavedb.lib.logging.context import logging_context, save_to_logging_context
8+
from mavedb.models.contributor import Contributor
9+
from mavedb.models.score_set import ScoreSet
10+
from mavedb.models.target_gene import TargetGene
11+
from mavedb.models.user import User
12+
from mavedb.view_models.search import TextSearch
13+
14+
logger = logging.getLogger(__name__)
15+
16+
17+
def search_target_genes(
18+
db: Session,
19+
owner_or_contributor: Optional[User],
20+
search: TextSearch,
21+
limit: Optional[int],
22+
) -> list[TargetGene]:
23+
save_to_logging_context({"target_gene_search_criteria": search.dict()})
24+
25+
query = db.query(TargetGene)
26+
27+
if search.text and len(search.text.strip()) > 0:
28+
lower_search_text = search.text.strip().lower()
29+
query = query.filter(func.lower(TargetGene.name).contains(lower_search_text))
30+
if owner_or_contributor is not None:
31+
query = query.filter(
32+
TargetGene.score_set.has(
33+
or_(
34+
ScoreSet.created_by_id == owner_or_contributor.id,
35+
ScoreSet.contributors.any(
36+
Contributor.orcid_id == owner_or_contributor.username
37+
),
38+
)
39+
)
40+
)
41+
42+
query = query.order_by(TargetGene.name)
43+
if limit is not None:
44+
query = query.limit(limit)
45+
46+
target_genes = query.all()
47+
if not target_genes:
48+
target_genes = []
49+
50+
save_to_logging_context({"matching_resources": len(target_genes)})
51+
logger.debug(
52+
msg=f"Target gene search yielded {len(target_genes)} matching resources.",
53+
extra=logging_context(),
54+
)
55+
56+
return target_genes

src/mavedb/routers/target_genes.py

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,35 @@
11
from typing import Any, List
22

33
from fastapi import APIRouter, Depends, HTTPException
4-
from sqlalchemy import func
54
from sqlalchemy.orm import Session
65

76
from mavedb import deps
7+
from mavedb.lib.authentication import UserData
8+
from mavedb.lib.authorization import require_current_user
9+
from mavedb.lib.target_genes import (
10+
search_target_genes as _search_target_genes,
11+
)
812
from mavedb.models.target_gene import TargetGene
913
from mavedb.view_models import target_gene
1014
from mavedb.view_models.search import TextSearch
1115

12-
router = APIRouter(prefix="/api/v1/target-genes", tags=["target-genes"], responses={404: {"description": "Not found"}})
16+
router = APIRouter(prefix="/api/v1", tags=["target-genes"], responses={404: {"description": "Not found"}})
1317

1418

15-
@router.get("/", status_code=200, response_model=List[target_gene.TargetGene], responses={404: {}})
19+
@router.post("/me/target-genes/search", status_code=200, response_model=List[target_gene.TargetGene])
20+
def search_my_target_genes(
21+
search: TextSearch,
22+
db: Session = Depends(deps.get_db),
23+
user_data: UserData = Depends(require_current_user)
24+
) -> Any:
25+
"""
26+
Search my target genes.
27+
"""
28+
29+
return _search_target_genes(db, user_data.user, search, 50)
30+
31+
32+
@router.get("/target-genes", status_code=200, response_model=List[target_gene.TargetGene], responses={404: {}})
1633
def list_target_genes(
1734
*,
1835
db: Session = Depends(deps.get_db),
@@ -24,7 +41,7 @@ def list_target_genes(
2441
return items
2542

2643

27-
@router.get("/names", status_code=200, response_model=List[str], responses={404: {}})
44+
@router.get("/target-genes/names", status_code=200, response_model=List[str], responses={404: {}})
2845
def list_target_gene_names(
2946
*,
3047
db: Session = Depends(deps.get_db),
@@ -38,7 +55,7 @@ def list_target_gene_names(
3855
return sorted(list(set(names)))
3956

4057

41-
@router.get("/categories", status_code=200, response_model=List[str], responses={404: {}})
58+
@router.get("/target-genes/categories", status_code=200, response_model=List[str], responses={404: {}})
4259
def list_target_gene_categories(
4360
*,
4461
db: Session = Depends(deps.get_db),
@@ -52,7 +69,7 @@ def list_target_gene_categories(
5269
return sorted(list(set(categories)))
5370

5471

55-
@router.get("/{item_id}", status_code=200, response_model=target_gene.TargetGene, responses={404: {}})
72+
@router.get("/target-genes/{item_id}", status_code=200, response_model=target_gene.TargetGene, responses={404: {}})
5673
def fetch_target_gene(
5774
*,
5875
item_id: int,
@@ -67,20 +84,13 @@ def fetch_target_gene(
6784
return item
6885

6986

70-
@router.post("/search", status_code=200, response_model=List[target_gene.TargetGene])
71-
def search_target_genes(search: TextSearch, db: Session = Depends(deps.get_db)) -> Any:
87+
@router.post("/target-genes/search", status_code=200, response_model=List[target_gene.TargetGene])
88+
def search_target_genes(
89+
search: TextSearch,
90+
db: Session = Depends(deps.get_db)
91+
) -> Any:
7292
"""
7393
Search target genes.
7494
"""
7595

76-
query = db.query(TargetGene)
77-
78-
if search.text and len(search.text.strip()) > 0:
79-
lower_search_text = search.text.strip().lower()
80-
query = query.filter(func.lower(TargetGene.name).contains(lower_search_text))
81-
else:
82-
raise HTTPException(status_code=500, detail="Search text is required")
83-
items = query.order_by(TargetGene.name).limit(50).all()
84-
if not items:
85-
items = []
86-
return items
96+
return _search_target_genes(db, None, search, 50)

tests/routers/test_target_gene.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
from mavedb.models.score_set import ScoreSet as ScoreSetDbModel
2+
from tests.helpers.util import (
3+
change_ownership,
4+
create_experiment,
5+
create_seq_score_set_with_variants,
6+
)
7+
8+
9+
def test_search_my_target_genes_no_match(session, data_provider, client, setup_router_db, data_files):
10+
experiment_1 = create_experiment(client, {"title": "Experiment 1"})
11+
create_seq_score_set_with_variants(
12+
client,
13+
session,
14+
data_provider,
15+
experiment_1["urn"],
16+
data_files / "scores.csv",
17+
update={"title": "Test Score Set"},
18+
)
19+
20+
search_payload = {"text": "NONEXISTENT"}
21+
response = client.post("/api/v1/me/target-genes/search", json=search_payload)
22+
assert response.status_code == 200
23+
assert len(response.json()) == 0
24+
25+
26+
def test_search_my_target_genes_no_match_on_other_user(session, data_provider, client, setup_router_db, data_files):
27+
experiment_1 = create_experiment(client, {"title": "Experiment 1"})
28+
score_set = create_seq_score_set_with_variants(
29+
client,
30+
session,
31+
data_provider,
32+
experiment_1["urn"],
33+
data_files / "scores.csv",
34+
update={"title": "Test Score Set"},
35+
)
36+
change_ownership(session, score_set["urn"], ScoreSetDbModel)
37+
38+
search_payload = {"text": "TEST1"}
39+
response = client.post("/api/v1/me/target-genes/search", json=search_payload)
40+
assert response.status_code == 200
41+
assert len(response.json()) == 0
42+
43+
44+
def test_search_my_target_genes_match(session, data_provider, client, setup_router_db, data_files):
45+
experiment_1 = create_experiment(client, {"title": "Experiment 1"})
46+
create_seq_score_set_with_variants(
47+
client,
48+
session,
49+
data_provider,
50+
experiment_1["urn"],
51+
data_files / "scores.csv",
52+
update={"title": "Test Score Set"},
53+
)
54+
55+
search_payload = {"text": "TEST1"}
56+
response = client.post("/api/v1/me/target-genes/search", json=search_payload)
57+
assert response.status_code == 200
58+
assert len(response.json()) == 1
59+
assert response.json()[0]["name"] == "TEST1"
60+
61+
62+
def test_search_target_genes_no_match(session, data_provider, client, setup_router_db, data_files):
63+
experiment_1 = create_experiment(client, {"title": "Experiment 1"})
64+
create_seq_score_set_with_variants(
65+
client,
66+
session,
67+
data_provider,
68+
experiment_1["urn"],
69+
data_files / "scores.csv",
70+
update={"title": "Test Score Set"},
71+
)
72+
73+
search_payload = {"text": "NONEXISTENT"}
74+
response = client.post("/api/v1/target-genes/search", json=search_payload)
75+
assert response.status_code == 200
76+
assert len(response.json()) == 0
77+
78+
79+
def test_search_target_genes_match_on_other_user(session, data_provider, client, setup_router_db, data_files):
80+
experiment_1 = create_experiment(client, {"title": "Experiment 1"})
81+
score_set = create_seq_score_set_with_variants(
82+
client,
83+
session,
84+
data_provider,
85+
experiment_1["urn"],
86+
data_files / "scores.csv",
87+
update={"title": "Test Score Set"},
88+
)
89+
change_ownership(session, score_set["urn"], ScoreSetDbModel)
90+
91+
search_payload = {"text": "TEST1"}
92+
response = client.post("/api/v1/target-genes/search", json=search_payload)
93+
assert response.status_code == 200
94+
assert len(response.json()) == 1
95+
assert response.json()[0]["name"] == "TEST1"
96+
97+
98+
def test_search_target_genes_match(session, data_provider, client, setup_router_db, data_files):
99+
experiment_1 = create_experiment(client, {"title": "Experiment 1"})
100+
create_seq_score_set_with_variants(
101+
client,
102+
session,
103+
data_provider,
104+
experiment_1["urn"],
105+
data_files / "scores.csv",
106+
update={"title": "Test Score Set"},
107+
)
108+
109+
search_payload = {"text": "TEST1"}
110+
response = client.post("/api/v1/target-genes/search", json=search_payload)
111+
assert response.status_code == 200
112+
assert len(response.json()) == 1
113+
assert response.json()[0]["name"] == "TEST1"

0 commit comments

Comments
 (0)