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

Add mapping endpoint to serve mapping rows #466

Merged
merged 9 commits into from
Nov 10, 2023
Merged
Show file tree
Hide file tree
Changes from 6 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
298 changes: 149 additions & 149 deletions backend/poetry.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions backend/src/monarch_py/api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ async def initialize_app():
CurieService()


app.include_router(entity.router, prefix=f"{PREFIX}/entity")
app.include_router(association.router, prefix=f"{PREFIX}/association")
app.include_router(search.router, prefix=PREFIX)
app.include_router(entity.router, prefix=f"{PREFIX}/entity")
app.include_router(histopheno.router, prefix=f"{PREFIX}/histopheno")
app.include_router(search.router, prefix=PREFIX)
app.include_router(semsim.router, prefix=f"{PREFIX}/semsim")

# Allow CORS
Expand Down
21 changes: 21 additions & 0 deletions backend/src/monarch_py/api/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,24 @@ async def autocomplete(
"""
response = solr().autocomplete(q=q)
return response


@router.get("/mappings")
async def mappings(
entity_id: Union[List[str], None] = Query(default=None),
subject_id: Union[List[str], None] = Query(default=None),
predicate_id: Union[List[str], None] = Query(default=None),
object_id: Union[List[str], None] = Query(default=None),
mapping_justification: Union[List[str], None] = Query(default=None),
pagination: PaginationParams = Depends(),
):
response = solr().get_mappings(
entity_id=entity_id,
subject_id=subject_id,
predicate_id=predicate_id,
object_id=object_id,
mapping_justification=mapping_justification,
offset=pagination.offset,
limit=pagination.limit,
)
return response
22 changes: 22 additions & 0 deletions backend/src/monarch_py/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,5 +307,27 @@ def multi_entity_associations(
solr_cli.multi_entity_associations(**locals())


@app.command("mappings")
def mappings(
entity_id: List[str] = typer.Option(None, "--entity-id", "-e", help="entity ID to get mappings for"),
subject_id: List[str] = typer.Option(None, "--subject-id", "-s", help="subject ID to get mappings for"),
predicate_id: List[str] = typer.Option(None, "--predicate-id", "-p", help="predicate ID to get mappings for"),
object_id: List[str] = typer.Option(None, "--object-id", "-o", help="object ID to get mappings for"),
mapping_justification: List[str] = typer.Option(
None, "--mapping-justification", "-m", help="mapping justification to get mappings for"
),
offset: int = typer.Option(0, "--offset", help="The offset of the first mapping to be retrieved"),
limit: int = typer.Option(20, "--limit", "-l", help="The number of mappings to return"),
fmt: str = typer.Option(
"json",
"--format",
"-f",
help="The format of the output (json, yaml, tsv, table)",
),
output: str = typer.Option(None, "--output", "-O", help="The path to the output file"),
):
solr_cli.mappings(**locals())


if __name__ == "__main__":
app()
509 changes: 145 additions & 364 deletions backend/src/monarch_py/datamodels/model.py

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions backend/src/monarch_py/datamodels/model.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,15 @@ classes:
- object_id
- object_label
- mapping_justification
- id
MappingResults:
description: SSSOM Mappings returned as a results collection
is_a: Results
slots:
- items
slot_usage:
items:
range: Mapping
MultiEntityAssociationResults:
is_a: Results
slots:
Expand Down
1 change: 1 addition & 0 deletions backend/src/monarch_py/datamodels/solr.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
class core(Enum):
ENTITY = "entity"
ASSOCIATION = "association"
SSSOM = "sssom"


class HistoPhenoKeys(Enum):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
CategoryGroupedAssociationResults,
Entity,
HistoPheno,
MappingResults,
MultiEntityAssociationResults,
Node,
NodeHierarchy,
Expand All @@ -26,6 +27,7 @@
parse_autocomplete,
parse_entity,
parse_histopheno,
parse_mappings,
parse_search,
)
from monarch_py.implementations.solr.solr_query_utils import (
Expand All @@ -34,6 +36,7 @@
build_association_table_query,
build_autocomplete_query,
build_histopheno_query,
build_mapping_query,
build_multi_entity_association_query,
build_search_query,
)
Expand Down Expand Up @@ -240,7 +243,7 @@ def get_associations(
limit=limit,
)
query_result = solr.query(query)
associations = parse_associations(query_result)
associations = parse_associations(query_result, offset, limit)
return associations

def get_histopheno(self, subject_closure: str = None) -> HistoPheno:
Expand Down Expand Up @@ -418,3 +421,29 @@ def get_association_table(
solr = SolrService(base_url=self.base_url, core=core.ASSOCIATION)
query_result = solr.query(query)
return parse_association_table(query_result, entity, offset, limit)

def get_mappings(
self,
entity_id: List[str] = None,
subject_id: List[str] = None,
predicate_id: List[str] = None,
object_id: List[str] = None,
mapping_justification: List[str] = None,
offset: int = 0,
limit: int = 20,
) -> MappingResults:
solr = SolrService(base_url=self.base_url, core=core.SSSOM)
query = build_mapping_query(
entity_id=[entity_id] if isinstance(entity_id, str) else entity_id,
subject_id=[subject_id] if isinstance(subject_id, str) else subject_id,
predicate_id=[predicate_id] if isinstance(predicate_id, str) else predicate_id,
object_id=[object_id] if isinstance(object_id, str) else object_id,
mapping_justification=[mapping_justification]
if isinstance(mapping_justification, str)
else mapping_justification,
offset=offset,
limit=limit,
)
query_result = solr.query(query)
mappings = parse_mappings(query_result, offset, limit)
return mappings
15 changes: 15 additions & 0 deletions backend/src/monarch_py/implementations/solr/solr_parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
FacetValue,
HistoBin,
HistoPheno,
Mapping,
MappingResults,
SearchResult,
SearchResults,
)
Expand Down Expand Up @@ -168,6 +170,19 @@ def parse_autocomplete(query_result: SolrQueryResult) -> SearchResults:
return SearchResults(limit=10, offset=0, total=total, items=items)


def parse_mappings(query_result: SolrQueryResult, offset: int = 0, limit: int = 20) -> MappingResults:
total = query_result.response.num_found
items = []
for doc in query_result.response.docs:
try:
result = Mapping(**doc)
items.append(result)
except ValidationError:
logger.error(f"Validation error for {doc}")
raise
return MappingResults(limit=limit, offset=offset, total=total, items=items)


##################
# Parser Helpers #
##################
Expand Down
23 changes: 23 additions & 0 deletions backend/src/monarch_py/implementations/solr/solr_query_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,29 @@ def build_autocomplete_query(q: str) -> SolrQuery:
return query


def build_mapping_query(
entity_id: List[str] = None,
subject_id: List[str] = None,
predicate_id: List[str] = None,
object_id: List[str] = None,
mapping_justification: List[str] = None,
offset: int = 0,
limit: int = 20,
) -> SolrQuery:
query = SolrQuery(start=offset, rows=limit)
if entity_id:
query.add_filter_query(" OR ".join([f'subject_id:"{escape(e)}" OR object_id:"{escape(e)}"' for e in entity_id]))
if subject_id:
query.add_filter_query(" OR ".join([f'subject_id:"{escape(e)}"' for e in subject_id]))
if predicate_id:
query.add_filter_query(" OR ".join([f'predicate_id:"{escape(e)}"' for e in predicate_id]))
if object_id:
query.add_filter_query(" OR ".join([f'object_id:"{escape(e)}"' for e in object_id]))
if mapping_justification:
query.add_filter_query(" OR ".join([f'mapping_justification:"{escape(e)}"' for e in mapping_justification]))
return query


### Search helper functions ###


Expand Down
26 changes: 26 additions & 0 deletions backend/src/monarch_py/interfaces/mapping_interface.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from abc import ABC
from typing import List

from monarch_py.datamodels.model import MappingResults


class MappingInterface(ABC):
def get_mappings(
self,
entity_id: List[str] = None,
subject_id: List[str] = None,
predicate_id: List[str] = None,
object_id: List[str] = None,
mapping_justification: List[str] = None,
) -> MappingResults:
"""
Get SSSOM Mappings based on the provided constraints

Args:
entity_id: Filter to only mappings matching the specified entity IDs. Defaults to None.
subject_id: Filter to only mappings matching the specified subject IDs. Defaults to None.
predicate_id: Filter to only mappings matching the specified predicate IDs. Defaults to None.
object_id: Filter to only mappings matching the specified object IDs. Defaults to None.
mapping_justification: Filter to only mappings matching the specified mapping justifications. Defaults to None.
"""
raise NotImplementedError
74 changes: 52 additions & 22 deletions backend/src/monarch_py/solr_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def entity(
"-f",
help="The format of the output (json, yaml, tsv, table)",
),
output: str = typer.Option(None, "--output", "-o", help="The path to the output file"),
output: str = typer.Option(None, "--output", "-O", help="The path to the output file"),
):
"""
Retrieve an entity by ID
Expand All @@ -123,11 +123,13 @@ def entity(

@solr_app.command("associations")
def associations(
category: List[str] = typer.Option(None, "--category", "-c", help="Comma-separated list of categories"),
subject: List[str] = typer.Option(None, "--subject", "-s", help="Comma-separated list of subjects"),
predicate: List[str] = typer.Option(None, "--predicate", "-p", help="Comma-separated list of predicates"),
object: List[str] = typer.Option(None, "--object", "-o", help="Comma-separated list of objects"),
entity: List[str] = typer.Option(None, "--entity", "-e", help="Comma-separated list of entities"),
category: List[str] = typer.Option(None, "--category", "-c", help="Category to get associations for"),
subject: List[str] = typer.Option(None, "--subject", "-s", help="Subject ID to get associations for"),
predicate: List[str] = typer.Option(None, "--predicate", "-p", help="Predicate ID to get associations for"),
object: List[str] = typer.Option(None, "--object", "-o", help="Object ID to get associations for"),
entity: List[str] = typer.Option(
None, "--entity", "-e", help="Entity (subject or object) ID to get associations for"
),
direct: bool = typer.Option(
False,
"--direct",
Expand All @@ -142,22 +144,22 @@ def associations(
"-f",
help="The format of the output (json, yaml, tsv, table)",
),
output: str = typer.Option(None, "--output", "-o", help="The path to the output file"),
output: str = typer.Option(None, "--output", "-O", help="The path to the output file"),
):
"""
Paginate through associations

Args:
category: A comma-separated list of categories
subject: A comma-separated list of subjects
predicate: A comma-separated list of predicates
object: A comma-separated list of objects
entity: A comma-separated list of entities
limit: The number of associations to return
direct: Whether to exclude associations with subject/object as ancestors
category: The category of the association (multi-valued)
subject: The subject of the association (multi-valued)
predicate: The predicate of the association (multi-valued)
object: The object of the association (multi-valued)
entity: The entity (subject or object) of the association (multi-valued)
limit: The number of associations to return (default 20)
direct: Whether to exclude associations with subject/object as ancestors (default False)
offset: The offset of the first association to be retrieved
fmt: The format of the output (json, yaml, tsv, table)
output: The path to the output file (stdout if not specified)
fmt: The format of the output (json, yaml, tsv, table) (default json)
output: The path to the output file (stdout if not specified) (default None)
"""
args = locals()
args.pop("fmt", None)
Expand All @@ -182,7 +184,7 @@ def multi_entity_associations(
"-f",
help="The format of the output (json, yaml, tsv, table)",
),
output: str = typer.Option(None, "--output", "-o", help="The path to the output file"),
output: str = typer.Option(None, "--output", "-O", help="The path to the output file"),
):
"""
Paginate through associations for multiple entities
Expand Down Expand Up @@ -222,7 +224,7 @@ def search(
"-f",
help="The format of the output (json, yaml, tsv, table)",
),
output: str = typer.Option(None, "--output", "-o", help="The path to the output file"),
output: str = typer.Option(None, "--output", "-O", help="The path to the output file"),
# sort: str = typer.Option(None, "--sort", "-s"),
):
"""
Expand Down Expand Up @@ -257,7 +259,7 @@ def autocomplete(
"-f",
help="The format of the output (json, yaml, tsv, table)",
),
output: str = typer.Option(None, "--output", "-o", help="The path to the output file"),
output: str = typer.Option(None, "--output", "-O", help="The path to the output file"),
):
"""
Return entity autcomplete matches for a query string
Expand All @@ -282,7 +284,7 @@ def histopheno(
"-f",
help="The format of the output (json, yaml, tsv, table)",
),
output: str = typer.Option(None, "--output", "-o", help="The path to the output file"),
output: str = typer.Option(None, "--output", "-O", help="The path to the output file"),
):
"""
Retrieve the histopheno associations for a given subject
Expand Down Expand Up @@ -313,7 +315,7 @@ def association_counts(
"-f",
help="The format of the output (json, yaml, tsv, table)",
),
output: str = typer.Option(None, "--output", "-o", help="The path to the output file"),
output: str = typer.Option(None, "--output", "-O", help="The path to the output file"),
):
"""
Retrieve the association counts for a given entity
Expand Down Expand Up @@ -349,8 +351,36 @@ def association_table(
"-f",
help="The format of the output (json, yaml, tsv, table)",
),
output: str = typer.Option(None, "--output", "-o", help="The path to the output file"),
output: str = typer.Option(None, "--output", "-O", help="The path to the output file"),
):
solr = get_solr(update=False)
response = solr.get_association_table(entity=entity, category=category, q=q, limit=limit, offset=offset)
format_output(fmt, response, output)


@solr_app.command("mappings")
def mappings(
entity_id: List[str] = typer.Option(None, "--entity-id", "-e", help="entity ID to get mappings for"),
subject_id: List[str] = typer.Option(None, "--subject-id", "-s", help="subject ID to get mappings for"),
predicate_id: List[str] = typer.Option(None, "--predicate-id", "-p", help="predicate ID to get mappings for"),
object_id: List[str] = typer.Option(None, "--object-id", "-o", help="object ID to get mappings for"),
mapping_justification: List[str] = typer.Option(
None, "--mapping-justification", "-m", help="mapping justification to get mappings for"
),
offset: int = typer.Option(0, "--offset", help="The offset of the first mapping to be retrieved"),
limit: int = typer.Option(20, "--limit", "-l", help="The number of mappings to return"),
fmt: str = typer.Option(
"json",
"--format",
"-f",
help="The format of the output (json, yaml, tsv, table)",
),
output: str = typer.Option(None, "--output", "-O", help="The path to the output file"),
):
args = locals()
args.pop("fmt", None)
args.pop("output", None)

solr = get_solr(update=False)
response = solr.get_mappings(**args)
format_output(fmt, response, output)
Loading