Skip to content

Commit

Permalink
Add mapping endpoint to serve mapping rows (#466)
Browse files Browse the repository at this point in the history
Closes #427 

This PR is to add an endpoint that serves mappings following the
associations endpoint, and to serve the mappings for an entity with the
entity response (with extras turned on)

- [x] model
- [x] interface
- [x] implementation tests
- [x] implementation
- [x] cli
- [x] api
- [x] api tests

---------

Co-authored-by: glass-ships <[email protected]>
Co-authored-by: Vincent Rubinetti <[email protected]>
  • Loading branch information
3 people authored Nov 10, 2023
1 parent 9e9e7ea commit 7184f54
Show file tree
Hide file tree
Showing 28 changed files with 831 additions and 201 deletions.
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()
22 changes: 21 additions & 1 deletion backend/src/monarch_py/datamodels/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from datetime import datetime, date
from enum import Enum
from typing import List, Dict, Optional, Any, Union
from pydantic import BaseModel as BaseModel, Field
from pydantic import BaseModel as BaseModel, ConfigDict, Field
import sys

if sys.version_info >= (3, 8):
Expand Down Expand Up @@ -435,6 +435,7 @@ class Mapping(ConfiguredBaseModel):
object_id: str = Field(...)
object_label: Optional[str] = Field(None, description="""The name of the object entity""")
mapping_justification: Optional[str] = Field(None)
id: str = Field(...)


class Node(Entity):
Expand All @@ -456,6 +457,10 @@ class Node(Entity):
default_factory=list,
description="""A list of diseases that are known to be causally associated with a gene""",
)
mappings: Optional[List[ExpandedCurie]] = Field(
default_factory=list,
description="""List of ExpandedCuries with id and url for mapped entities""",
)
external_links: Optional[List[ExpandedCurie]] = Field(
default_factory=list, description="""ExpandedCurie with id and url for xrefs"""
)
Expand Down Expand Up @@ -538,6 +543,20 @@ class EntityResults(Results):
total: int = Field(..., description="""total number of items matching a query""")


class MappingResults(Results):
"""
SSSOM Mappings returned as a results collection
"""

items: List[Mapping] = Field(
default_factory=list,
description="""A collection of items, with the type to be overriden by slot_usage""",
)
limit: int = Field(..., description="""number of items to return in a response""")
offset: int = Field(..., description="""offset into the total number of items""")
total: int = Field(..., description="""total number of items matching a query""")


class MultiEntityAssociationResults(Results):

id: str = Field(...)
Expand Down Expand Up @@ -684,6 +703,7 @@ class BestMatch(ConfiguredBaseModel):
AssociationTableResults.update_forward_refs()
CategoryGroupedAssociationResults.update_forward_refs()
EntityResults.update_forward_refs()
MappingResults.update_forward_refs()
MultiEntityAssociationResults.update_forward_refs()
SearchResult.update_forward_refs()
SearchResults.update_forward_refs()
Expand Down
19 changes: 17 additions & 2 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 All @@ -225,6 +234,7 @@ classes:
- inheritance
- causal_gene
- causes_disease
- mappings
- external_links
- provided_by_link
- association_counts
Expand Down Expand Up @@ -517,7 +527,6 @@ slots:
qualifiers_closure_label:
multivalued: true
description: Field containing frequency_qualifier name and the names of all of it's ancestors

frequency_qualifier_label:
is_a: name
description: The name of the frequency_qualifier entity
Expand Down Expand Up @@ -578,7 +587,13 @@ slots:
stage_qualifier_closure_label:
multivalued: true
description: Field containing stage_qualifier name and the names of all of it's ancestors
# sssom slots

# sssom slots
mappings:
description: List of ExpandedCuries with id and url for mapped entities
range: ExpandedCurie
multivalued: true
inlined_as_list: true
subject_id:
range: string
required: true
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,13 +36,15 @@
build_association_table_query,
build_autocomplete_query,
build_histopheno_query,
build_mapping_query,
build_multi_entity_association_query,
build_search_query,
)
from monarch_py.interfaces.association_interface import AssociationInterface
from monarch_py.interfaces.entity_interface import EntityInterface
from monarch_py.interfaces.search_interface import SearchInterface
from monarch_py.service.solr_service import SolrService
from monarch_py.utils.entity_utils import get_expanded_curie
from monarch_py.utils.utils import get_provided_by_link, get_links_for_field


Expand Down Expand Up @@ -113,11 +117,25 @@ def get_entity(self, id: str, extra: bool) -> Optional[Union[Node, Entity]]:
node.association_counts = self.get_association_counts(id).items
node.external_links = get_links_for_field(node.xref) if node.xref else []
node.provided_by_link = get_provided_by_link(node.provided_by)
node.mappings = self._get_mapped_entities(node)

return node

### Entity helpers ###

def _get_mapped_entities(self, this_entity: Entity) -> list:
"""..."""
mapped_entities = []
mappings = self.get_mappings(entity_id=this_entity.id)
for m in mappings.items:
if this_entity.id == m.subject_id:
mapped_entities.append(get_expanded_curie(m.object_id))
elif this_entity.id == m.object_id:
mapped_entities.append(get_expanded_curie(m.subject_id))
else:
pass
return mapped_entities

def _get_associated_entity(self, association: Association, this_entity: Entity) -> Entity:
"""Returns the id, name, and category of the other Entity in an Association given this_entity"""
if this_entity.id == association.subject:
Expand Down Expand Up @@ -240,7 +258,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 +436,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
Loading

0 comments on commit 7184f54

Please sign in to comment.