diff --git a/backend/poetry.lock b/backend/poetry.lock index ff4a322e..3aebca8f 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "airium" @@ -4754,39 +4754,20 @@ wsproto = ">=0.14" [[package]] name = "typer" -version = "0.7.0" +version = "0.12.5" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." optional = false -python-versions = ">=3.6" -files = [ - {file = "typer-0.7.0-py3-none-any.whl", hash = "sha256:b5e704f4e48ec263de1c0b3a2387cd405a13767d2f907f44c1a08cbad96f606d"}, - {file = "typer-0.7.0.tar.gz", hash = "sha256:ff797846578a9f2a201b53442aedeb543319466870fbe1c701eab66dd7681165"}, -] - -[package.dependencies] -click = ">=7.1.1,<9.0.0" - -[package.extras] -all = ["colorama (>=0.4.3,<0.5.0)", "rich (>=10.11.0,<13.0.0)", "shellingham (>=1.3.0,<2.0.0)"] -dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2.17.0,<3.0.0)"] -doc = ["cairosvg (>=2.5.2,<3.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pillow (>=9.3.0,<10.0.0)"] -test = ["black (>=22.3.0,<23.0.0)", "coverage (>=6.2,<7.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "pytest (>=4.4.0,<8.0.0)", "pytest-cov (>=2.10.0,<5.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<4.0.0)", "rich (>=10.11.0,<13.0.0)", "shellingham (>=1.3.0,<2.0.0)"] - -[[package]] -name = "typer-cli" -version = "0.0.13" -description = "Run Typer scripts with completion, without having to create a package, using Typer CLI." -optional = false python-versions = ">=3.7" files = [ - {file = "typer_cli-0.0.13-py3-none-any.whl", hash = "sha256:5ae0f99dce8f8f9669137a2c98eb42485cd4412e0ec225c8eb29ce8ac3378731"}, - {file = "typer_cli-0.0.13.tar.gz", hash = "sha256:f5b85764e56fb3fe835ed008ad5bc7db4961f7bcce1f1c1698ac46b6c5d9b86f"}, + {file = "typer-0.12.5-py3-none-any.whl", hash = "sha256:62fe4e471711b147e3365034133904df3e235698399bc4de2b36c8579298d52b"}, + {file = "typer-0.12.5.tar.gz", hash = "sha256:f592f089bedcc8ec1b974125d64851029c3b1af145f04aca64d69410f0c9b722"}, ] [package.dependencies] -colorama = ">=0.4.3,<=0.5.0" -shellingham = ">=1.3.2,<=1.4.0" -typer = ">=0.4.0,<=0.7.0" +click = ">=8.0.0" +rich = ">=10.11.0" +shellingham = ">=1.3.0" +typing-extensions = ">=3.7.4.3" [[package]] name = "types-python-dateutil" @@ -5338,4 +5319,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "77d53dfcd9d6085bd4c8a247b59f56ea73eb6ba04a9eebd4601edbb6410a8ac8" +content-hash = "2cce2c4f6dc36c8b23aa4a62bc72f03554f3fb69d0e219b388150642bd78575e" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 1b70049a..94f842b4 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -29,8 +29,7 @@ loguru = "*" pystow = ">=0.5.4" requests = "^2.31.0" rich = "*" -typer = "^0.7.0" -typer-cli = "^0.0.13" +typer = "^0.12.0" beautifulsoup4 = "^4.12.3" spacy = "^3.7.6" diff --git a/backend/src/monarch_py/cli.py b/backend/src/monarch_py/cli.py index 3f215019..fb0b3755 100644 --- a/backend/src/monarch_py/cli.py +++ b/backend/src/monarch_py/cli.py @@ -1,22 +1,21 @@ import importlib +import importlib.util from pathlib import Path -from typing import List, Optional -from typing_extensions import Annotated +from typing import Annotated, Optional import typer from monarch_py import solr_cli, sql_cli from monarch_py.api.config import semsimian from monarch_py.api.additional_models import SemsimMetric -from monarch_py.datamodels.category_enums import ( - AssociationCategory, - AssociationPredicate, - EntityCategory, - MappingPredicate, -) from monarch_py.utils.solr_cli_utils import check_for_docker -from monarch_py.utils.utils import set_log_level, get_release_metadata, get_release_versions +from monarch_py.utils.utils import ( + set_log_level, + get_release_metadata, + get_release_versions, +) from monarch_py.utils.format_utils import format_output +from monarch_py.utils import cli_fields as fields app = typer.Typer() @@ -28,11 +27,31 @@ def callback( ctx: typer.Context, version: Annotated[ - Optional[bool], typer.Option("--version", "-v", help="Show the currently installed version", is_eager=True) + Optional[bool], + typer.Option( + "--version", + "-v", + help="Show the currently installed version", + is_eager=True, + ), ] = None, + quiet: Annotated[ + bool, + typer.Option( + "--quiet", + "-q", + help="Set log level to warning", + ), + ] = False, + debug: Annotated[ + bool, + typer.Option( + "--debug", + "-d", + help="Set log level to debug", + ), + ] = False, # verbose: Annotated[int, typer.Option("--verbose", "-v", count=True)] = 0, - quiet: Annotated[bool, typer.Option("--quiet", "-q", help="Set log level to warning")] = False, - debug: Annotated[bool, typer.Option("--debug", "-d", help="Set log level to debug")] = False, ): if version and ctx.invoked_subcommand is None: from monarch_py import __version__ @@ -63,8 +82,15 @@ def schema(): Print the linkml schema for the data model """ schema_name = "model" - schema_dir = Path(importlib.util.find_spec(f"monarch_py.datamodels.{schema_name}").origin).parent - schema_path = schema_dir / Path(schema_name + ".yaml") + import_path = f"monarch_py.datamodels.{schema_name}" + schema_module_spec = importlib.util.find_spec( + f"monarch_py.datamodels.{schema_name}" + ) + if schema_module_spec is None or schema_module_spec.origin is None: + print(f"No python module found at {import_path}") + raise typer.Exit(code=1) + schema_dir = Path(schema_module_spec.origin).parent + schema_path = schema_dir / f"{schema_name}.yaml" with open(schema_path, "r") as schema_file: print(schema_file.read()) raise typer.Exit() @@ -73,247 +99,15 @@ def schema(): ### "Aliases" for Solr CLI ### -@app.command("entity") -def entity( - id: str = typer.Argument(None, help="The identifier of the entity to be retrieved"), - extra: bool = typer.Option( - False, - "--extra", - "-e", - help="Include extra fields in the output (association_counts and node_hierarchy)", - ), - 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"), -): - """ - Retrieve an entity by ID - - Args: - id: The identifier of the entity to be retrieved - fmt: The format of the output (json, yaml, tsv, table) - output: The path to the output file (stdout if not specified) - - """ - solr_cli.entity(**locals()) - - -@app.command("associations") -def associations( - category: List[AssociationCategory] = 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[AssociationPredicate] = 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", - "-d", - help="Whether to exclude associations with subject/object as ancestors", - ), - compact: bool = typer.Option( - False, - "--compact", - "-C", - help="Whether to return a compact representation of the associations", - ), - limit: int = typer.Option(20, "--limit", "-l", help="The number of associations to return"), - offset: int = typer.Option(0, "--offset", help="The offset of the first association to be retrieved"), - 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"), -): - """ - 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 - 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) - """ - solr_cli.associations(**locals()) - - -@app.command("multi-entity-associations") -def multi_entity_associations( - entity: List[str] = typer.Option(None, "--entity", "-e", help="Comma-separated list of entities"), - counterpart_category: List[str] = typer.Option(None, "--counterpart-category", "-c"), - limit: int = typer.Option(20, "--limit", "-l"), - offset: int = typer.Option(0, "--offset"), - 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"), -): - """ - Paginate through associations for multiple entities - - Args: - entity: A comma-separated list of entities - counterpart_category: A comma-separated list of counterpart categories - limit: The number of associations to return - 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) - """ - solr_cli.multi_entity_associations(**locals()) - - -@app.command("search") -def search( - q: str = typer.Option(None, "--query", "-q"), - category: List[EntityCategory] = typer.Option(None, "--category", "-c"), - in_taxon_label: str = typer.Option(None, "--in-taxon-label", "-t"), - facet_fields: List[str] = typer.Option(None, "--facet-fields", "-ff"), - facet_queries: List[str] = typer.Option(None, "--facet-queries"), - limit: int = typer.Option(20, "--limit", "-l"), - offset: int = typer.Option(0, "--offset"), - 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"), - # sort: str = typer.Option(None, "--sort", "-s"), -): - """ - Search for entities - - Args: - q: The query string to search for - category: The category of the entity - in_taxon_label: The taxon label to filter by - limit: The number of entities to return - offset: The offset of the first entity to be retrieved - fmt: The format of the output (json, yaml, tsv, table) - output: The path to the output file (stdout if not specified) - """ - solr_cli.search(**locals()) - - -@app.command("autocomplete") -def autocomplete( - q: str = typer.Argument(None, help="Query string to autocomplete against"), - 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"), -): - """ - Return entity autcomplete matches for a query string - - Args: - q: The query string to autocomplete against - fmt: The format of the output (json, yaml, tsv, table) - output: The path to the output file (stdout if not specified) - - """ - solr_cli.autocomplete(**locals()) - - -@app.command("histopheno") -def histopheno( - subject: str = typer.Argument(None, help="The subject of the association"), - 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"), -): - """ - Retrieve the histopheno data for an entity by ID - - Args: - subject: The subject of the association - - Optional Args: - fmt (str): The format of the output (json, yaml, tsv, table). Default JSON - output (str): The path to the output file. Default stdout - """ - solr_cli.histopheno(**locals()) - - -@app.command("association-counts") -def association_counts( - entity: str = typer.Argument(None, help="The entity to get association counts for"), - 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"), -): - """ - Retrieve association counts for an entity by ID - - Args: - entity: The entity to get association counts for - fmt: The format of the output (json, yaml, tsv, table). Default JSON - output: The path to the output file. Default stdout - - Returns: - A list of association counts for the given entity containing association type, label and count - """ - solr_cli.association_counts(**locals()) - - -@app.command("association-table") -def association_table( - entity: str = typer.Argument(..., help="The entity to get associations for"), - category: AssociationCategory = typer.Argument( - ..., - help="The association category to get associations for, ex. biolink:GeneToPhenotypicFeatureAssociation", - ), - q: str = typer.Option(None, "--query", "-q"), - traverse_orthologs: bool = typer.Option( - False, - "--traverse-orthologs", - "-t", - help="Whether to traverse orthologs when getting associations", - ), - sort: List[str] = typer.Option(None, "--sort", "-s"), - limit: int = typer.Option(5, "--limit", "-l"), - offset: int = typer.Option(0, "--offset"), - 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.association_table(**locals()) +app.command("entity")(solr_cli.entity) +app.command("associations")(solr_cli.associations) +app.command("multi-entity-associations")(solr_cli.multi_entity_associations) +app.command("search")(solr_cli.search) +app.command("autocomplete")(solr_cli.autocomplete) +app.command("histopheno")(solr_cli.histopheno) +app.command("association-counts")(solr_cli.association_counts) +app.command("association-table")(solr_cli.association_table) +app.command("mappings")(solr_cli.mappings) ### CLI Commands for Semsimian ### @@ -321,88 +115,64 @@ def association_table( @app.command("compare") def compare( - subjects: str = typer.Argument(..., help="Comma separated list of subjects to compare"), - objects: str = typer.Argument(..., help="Comma separated list of objects to compare"), - metric: SemsimMetric = typer.Option( - SemsimMetric.ANCESTOR_INFORMATION_CONTENT, - "--metric", - "-m", - help="The metric to use for comparison", - ), - 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"), + subjects: Annotated[ + str, + typer.Argument( + help="Comma separated list of subjects to compare", + ), + ], + objects: Annotated[ + str, + typer.Argument( + help="Comma separated list of objects to compare", + ), + ], + metric: Annotated[ + SemsimMetric, + typer.Option( + "--metric", + "-m", + help="The metric to use for comparison", + ), + ] = SemsimMetric.ANCESTOR_INFORMATION_CONTENT, + fmt: fields.FormatOption = fields.OutputFormat.json, + output: fields.OutputOption = None, ): """Compare two sets of phenotypes using semantic similarity via SemSimian""" - subjects = subjects.split(",") - objects = objects.split(",") - response = semsimian().compare(subjects, objects, metric) + subjects_list = subjects.split(",") + objects_list = objects.split(",") + response = semsimian().compare(subjects_list, objects_list, metric) format_output(fmt, response, output) -### Misc CLI Commands ### - - -@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[MappingPredicate] = 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()) - - ### CLI Commands for Release Info ### @app.command("releases") def releases( - dev: bool = typer.Option(False, "--dev", help="Get dev releases of the KG (default is False)"), - limit: int = typer.Option(0, "--limit", "-l", help="The number of releases to return"), + dev: Annotated[ + bool, + typer.Option( + "--dev", + help="Get dev releases of the KG (default is False)", + ), + ] = False, + limit: fields.LimitOption = 0, ): """ List all available releases of the Monarch Knowledge Graph """ - get_release_versions(**locals(), print_info=True) + get_release_versions(dev, limit, print_info=True) @app.command("release") def release( - release_ver: str = typer.Argument(None, help="The release version to get metadata for"), - 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"), + release_ver: str = typer.Argument(help="The release version to get metadata for"), + fmt: fields.FormatOption = fields.OutputFormat.json, + output: fields.OutputOption = None, ): """ Retrieve metadata for a specific release - - Args: - release_ver: The release version to get metadata for - fmt: The format of the output (json, yaml, tsv, table) - output: The path to the output file (stdout if not specified) """ release_info = get_release_metadata(release_ver) format_output(fmt, release_info, output) diff --git a/backend/src/monarch_py/solr_cli.py b/backend/src/monarch_py/solr_cli.py index 10ffeed9..7701722c 100644 --- a/backend/src/monarch_py/solr_cli.py +++ b/backend/src/monarch_py/solr_cli.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Optional from typing_extensions import Annotated import pystow @@ -10,9 +10,16 @@ EntityCategory, MappingPredicate, ) -from monarch_py.utils.solr_cli_utils import ensure_solr, get_solr, solr_status, start_solr, stop_solr +from monarch_py.utils.solr_cli_utils import ( + ensure_solr, + get_solr, + solr_status, + start_solr, + stop_solr, +) from monarch_py.utils.utils import console, set_log_level from monarch_py.utils.format_utils import format_output +from monarch_py.utils import cli_fields as fields solr_app = typer.Typer() monarchstow = pystow.module("monarch") @@ -21,8 +28,12 @@ @solr_app.callback(invoke_without_command=True) def callback( ctx: typer.Context, - quiet: Annotated[bool, typer.Option("--quiet", "-q", help="Set log level to warning")] = False, - debug: Annotated[bool, typer.Option("--debug", "-d", help="Set log level to debug")] = False, + quiet: Annotated[ + bool, typer.Option("--quiet", "-q", help="Set log level to warning") + ] = False, + debug: Annotated[ + bool, typer.Option("--debug", "-d", help="Set log level to debug") + ] = False, ): if ctx.invoked_subcommand is None: typer.secho( @@ -64,16 +75,13 @@ def status(): def download( version: Annotated[ str, - typer.Option( - "latest", - "--version", + typer.Argument( help="The version of the Solr KG to download (latest, dev, or a specific version)", ), ] = "latest", overwrite: Annotated[ bool, typer.Option( - False, "--overwrite", help="Overwrite the existing Solr KG if it exists", ), @@ -91,37 +99,27 @@ def download( @solr_app.command("entity") def entity( - id: str = typer.Argument(None, help="The identifier of the entity to be retrieved"), - extra: bool = typer.Option( - False, - "--extra", - "-e", - help="Include extra fields in the output (association_counts and node_hierarchy)", - ), - 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"), + entity_id: Annotated[ + str, + typer.Argument(help="The identifier of the entity to be retrieved"), + ], + extra: Annotated[ + bool, + typer.Option( + "--extra", + "-e", + help="Include extra fields in the output (association_counts and node_hierarchy)", + ), + ] = False, + fmt: fields.FormatOption = fields.OutputFormat.json, + output: fields.OutputOption = None, ): """ Retrieve an entity by ID - - Args: - id (str): The identifier of the entity to be retrieved - - Optional Args: - fmt (str): The format of the output (json, yaml, tsv, table). Default JSON - output (str): The path to the output file. Default stdout """ - if not id: - console.print("\n[bold red]Entity ID required.[/]\n") - raise typer.Exit(1) solr = get_solr(update=False) - response = solr.get_entity(id, extra) + response = solr.get_entity(entity_id, extra) if response is not None: format_output(fmt, response, output) else: @@ -131,160 +129,159 @@ def entity( @solr_app.command("associations") def associations( - category: List[AssociationCategory] = 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[AssociationPredicate] = 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", - "-d", - help="Whether to exclude associations with subject/object as ancestors", - ), - compact: bool = typer.Option( - False, - "--compact", - "-C", - help="Whether to return a compact representation of the associations", - ), - limit: int = typer.Option(20, "--limit", "-l", help="The number of associations to return"), - offset: int = typer.Option(0, "--offset", help="The offset of the first association to be retrieved"), - 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"), + category: Annotated[ + Optional[List[AssociationCategory]], + typer.Option( + "--category", + "-c", + help="Category to get associations for", + ), + ] = None, + subject: Annotated[ + Optional[List[str]], + typer.Option( + "--subject", + "-s", + help="Subject ID to get associations for", + ), + ] = None, + predicate: Annotated[ + Optional[List[AssociationPredicate]], + typer.Option( + "--predicate", + "-p", + help="Predicate ID to get associations for", + ), + ] = None, + object: Annotated[ + Optional[List[str]], + typer.Option( + "--object", + "-o", + help="Object ID to get associations for", + ), + ] = None, + entity: Annotated[ + Optional[List[str]], + typer.Option( + "--entity", + "-e", + help="Entity (subject or object) ID to get associations for", + ), + ] = None, + direct: Annotated[ + bool, + typer.Option( + "--direct", + "-d", + help="Whether to exclude associations with subject/object as ancestors", + ), + ] = False, + compact: Annotated[ + bool, + typer.Option( + "--compact", + "-C", + help="Whether to return a compact representation of the associations", + ), + ] = False, + limit: fields.LimitOption = 20, + offset: fields.OffsetOption = 0, + fmt: fields.FormatOption = fields.OutputFormat.json, + output: fields.OutputOption = None, ): """ Paginate through associations - - Args: - 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) (default json) - output: The path to the output file (stdout if not specified) (default None) """ - args = locals() - args.pop("fmt", None) - args.pop("output", None) + kwargs = locals() + kwargs.pop("fmt", None) + kwargs.pop("output", None) solr = get_solr(update=False) - response = solr.get_associations(**args) + response = solr.get_associations(**kwargs) format_output(fmt, response, output) @solr_app.command("multi-entity-associations") def multi_entity_associations( - entity: List[str] = typer.Option(None, "--entity", "-e", help="Entity ID to get associations for"), - counterpart_category: List[str] = typer.Option( - None, "--counterpart-category", "-c", help="Counterpart category to get associations for" - ), - limit: int = typer.Option(20, "--limit", "-l"), - offset: int = typer.Option(0, "--offset"), - 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"), + entity: Annotated[ + Optional[List[str]], + typer.Option( + "--entity", + "-e", + help="Comma-separated list of entities", + ), + ] = None, + counterpart_category: Annotated[ + Optional[List[str]], + typer.Option( + "--counterpart-category", + "-c", + ), + ] = None, + limit: fields.LimitOption = 20, + offset: fields.OffsetOption = 0, + fmt: fields.FormatOption = fields.OutputFormat.json, + output: fields.OutputOption = None, ): """ Paginate through associations for multiple entities - - Args: - entity: A comma-separated list of entities - counterpart_category: A comma-separated list of counterpart categories - limit: The number of associations to return - 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) """ - args = locals() - args.pop("fmt", None) - args.pop("output", None) - args["limit_per_group"] = args.pop("limit") + kwargs = locals() + kwargs.pop("fmt", None) + kwargs.pop("output", None) + kwargs["limit_per_group"] = kwargs.pop("limit") solr = get_solr(update=False) - response = solr.get_multi_entity_associations(**args) + response = solr.get_multi_entity_associations(**kwargs) format_output(fmt, response, output) @solr_app.command("search") def search( - q: str = typer.Option("*:*", "--query", "-q"), - category: List[EntityCategory] = typer.Option(None, "--category", "-c"), - in_taxon_label: str = typer.Option(None, "--in-taxon-label", "-t"), - facet_fields: List[str] = typer.Option(None, "--facet-fields", "-ff"), - facet_queries: List[str] = typer.Option(None, "--facet-queries"), - limit: int = typer.Option(20, "--limit", "-l"), - offset: int = typer.Option(0, "--offset"), - 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"), + q: fields.QueryOption = ":*", + category: Annotated[ + Optional[List[EntityCategory]], + typer.Option("--category", "-c"), + ] = None, + in_taxon_label: Annotated[ + Optional[str], + typer.Option("--in-taxon-label", "-t"), + ] = None, + facet_fields: Annotated[ + Optional[List[str]], + typer.Option("--facet-fields", "-ff"), + ] = None, + facet_queries: Annotated[ + Optional[List[str]], + typer.Option("--facet-queries", "-fq"), + ] = None, + limit: fields.LimitOption = 20, + offset: fields.OffsetOption = 0, + fmt: fields.FormatOption = fields.OutputFormat.json, + output: fields.OutputOption = None, # sort: str = typer.Option(None, "--sort", "-s"), ): """ Search for entities - - Optional Args: - q: The query string to search for - category: The category of the entity - in_taxon_label: The taxon label to filter on - facet_fields: The fields to facet on - facet_queries: The queries to facet on - limit: The number of entities to return - offset: The offset of the first entity to be retrieved - fmt (str): The format of the output (json, yaml, tsv, table). Default JSON - output (str): The path to the output file. Default stdout """ - params = locals() - params.pop("fmt", None) - params.pop("output", None) + kwargs = locals() + kwargs.pop("fmt", None) + kwargs.pop("output", None) solr = get_solr(update=False) - response = solr.search(**params) + response = solr.search(**kwargs) format_output(fmt, response, output) @solr_app.command("autocomplete") def autocomplete( - q: str = typer.Argument(None, help="Query string to autocomplete against"), - 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"), + q: Annotated[str, typer.Argument(help="Query string to autocomplete against")], + fmt: fields.FormatOption = fields.OutputFormat.json, + output: fields.OutputOption = None, ): """ Return entity autcomplete matches for a query string - - Args: - q: The query string to autocomplete against - fmt: The format of the output (json, yaml, tsv, table) - output: The path to the output file (stdout if not specified) - """ solr = get_solr(update=False) response = solr.autocomplete(q) @@ -293,24 +290,12 @@ def autocomplete( @solr_app.command("histopheno") def histopheno( - subject: str = typer.Argument(None, help="The subject of the association"), - 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"), + subject: Annotated[str, typer.Argument(help="The subject of the association")], + fmt: fields.FormatOption = fields.OutputFormat.json, + output: fields.OutputOption = None, ): """ Retrieve the histopheno associations for a given subject - - Args: - subject (str): The subject of the association - - Optional Args: - fmt (str): The format of the output (json, yaml, tsv, table). Default JSON - output (str): The path to the output file. Default stdout """ if not subject: @@ -324,24 +309,14 @@ def histopheno( @solr_app.command("association-counts") def association_counts( - entity: str = typer.Argument(None, help="The entity to get association counts for"), - 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"), + entity: Annotated[ + str, typer.Argument(help="The entity to get association counts for") + ], + fmt: fields.FormatOption = fields.OutputFormat.json, + output: fields.OutputOption = None, ): """ Retrieve the association counts for a given entity - - Args: - entity (str): The entity to get association counts for - - Optional Args: - fmt (str): The format of the output (json, yaml, tsv, table). Default JSON - output (str): The path to the output file. Default stdout """ if not entity: console.print("\n[bold red]Entity ID required.[/]\n") @@ -353,28 +328,38 @@ def association_counts( @solr_app.command("association-table") def association_table( - entity: str = typer.Argument(..., help="The entity to get associations for"), - category: AssociationCategory = typer.Argument( - None, - help="The association category to get associations for, ex. biolink:GeneToPhenotypicFeatureAssociation", - ), - traverse_orthologs: bool = typer.Option( - False, - "--traverse-orthologs", - "-t", - help="Whether to traverse orthologs when getting associations", - ), - q: str = typer.Option(None, "--query", "-q"), - sort: List[str] = typer.Option(None, "--sort", "-s"), - limit: int = typer.Option(5, "--limit", "-l"), - offset: int = typer.Option(0, "--offset"), - 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"), + entity: Annotated[ + str, + typer.Argument( + help="The entity to get associations for", + ), + ], + category: Annotated[ + AssociationCategory, + typer.Argument( + help="The association category to get associations for, ex. biolink:GeneToPhenotypicFeatureAssociation", + ), + ], + q: Annotated[ + Optional[str], + typer.Option("--query", "-q"), + ] = None, + traverse_orthologs: Annotated[ + bool, + typer.Option( + "--traverse-orthologs", + "-t", + help="Whether to traverse orthologs when getting associations", + ), + ] = False, + sort: Annotated[ + Optional[List[str]], + typer.Option("--sort", "-s"), + ] = None, + limit: fields.LimitOption = 20, + offset: fields.OffsetOption = 0, + fmt: fields.FormatOption = fields.OutputFormat.json, + output: fields.OutputOption = None, ): solr = get_solr(update=False) response = solr.get_association_table( @@ -391,24 +376,34 @@ def association_table( @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[MappingPredicate] = 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"), + entity_id: Annotated[ + Optional[List[str]], + typer.Option("--entity-id", "-e", help="entity ID to get mappings for"), + ] = None, + subject_id: Annotated[ + Optional[List[str]], + typer.Option("--subject-id", "-s", help="subject ID to get mappings for"), + ] = None, + predicate_id: Annotated[ + Optional[List[MappingPredicate]], + typer.Option("--predicate-id", "-p", help="predicate ID to get mappings for"), + ] = None, + object_id: Annotated[ + Optional[List[str]], + typer.Option("--object-id", "-o", help="object ID to get mappings for"), + ] = None, + mapping_justification: Annotated[ + Optional[List[str]], + typer.Option( + "--mapping-justification", + "-m", + help="mapping justification to get mappings for", + ), + ] = None, + limit: fields.LimitOption = 20, + offset: fields.OffsetOption = 0, + fmt: fields.FormatOption = fields.OutputFormat.json, + output: fields.OutputOption = None, ): args = locals() args.pop("fmt", None) diff --git a/backend/src/monarch_py/sql_cli.py b/backend/src/monarch_py/sql_cli.py index c10a1b6a..80383176 100644 --- a/backend/src/monarch_py/sql_cli.py +++ b/backend/src/monarch_py/sql_cli.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Optional from typing_extensions import Annotated import typer @@ -10,6 +10,7 @@ from monarch_py.implementations.sql.sql_implementation import SQLImplementation from monarch_py.utils.utils import console, set_log_level from monarch_py.utils.format_utils import format_output +from monarch_py.utils import cli_fields as fields sql_app = typer.Typer() app_state = {"log_level": "WARNING"} @@ -18,8 +19,12 @@ @sql_app.callback(invoke_without_command=True) def callback( ctx: typer.Context, - quiet: Annotated[bool, typer.Option("--quiet", "-q", help="Set log level to warning")] = False, - debug: Annotated[bool, typer.Option("--debug", "-d", help="Set log level to debug")] = False, + quiet: Annotated[ + bool, typer.Option("--quiet", "-q", help="Set log level to warning") + ] = False, + debug: Annotated[ + bool, typer.Option("--debug", "-d", help="Set log level to debug") + ] = False, ): if ctx.invoked_subcommand is None: typer.secho( @@ -33,36 +38,32 @@ def callback( @sql_app.command() def entity( - id: str = typer.Argument(None, help="The identifier of the entity to be retrieved"), - extra: bool = typer.Option( - False, - "--extra", - "-e", - help="Include extra fields in the output (association_counts and node_hierarchy)", - ), - update: bool = typer.Option(False, "--update", "-u", help="Whether to re-download the Monarch KG"), - 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"), + entity_id: Annotated[ + str, + typer.Argument(help="The identifier of the entity to be retrieved"), + ], + extra: Annotated[ + bool, + typer.Option( + "--extra", + "-e", + help="Include extra fields in the output (association_counts and node_hierarchy)", + ), + ], + update: Annotated[ + bool, + typer.Option( + "--update", + "-u", + help="Whether to re-download the Monarch KG", + ), + ], + fmt: fields.FormatOption = fields.OutputFormat.json, + output: fields.OutputOption = None, ): - """Retrieve an entity by ID - - Args: - id (str): The identifier of the entity to be retrieved - update (bool): = Whether to re-download the Monarch KG. Default False - fmt (str): The format of the output (json, yaml, tsv, table). Default JSON - output (str): The path to the output file. Default stdout - """ - if not id: - console.print("\n[bold red]Entity ID required.[/]\n") - raise typer.Exit(1) - + """Retrieve an entity by ID""" data = SQLImplementation() - response = data.get_entity(id, update, extra) + response = data.get_entity(entity_id, update, extra) if not response: console.print(f"\nEntity '{id}' not found.\n") @@ -72,53 +73,69 @@ def entity( @sql_app.command() def associations( - category: List[AssociationCategory] = 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[AssociationPredicate] = 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", - "-d", - help="Whether to exclude associations with subject/object as ancestors", - ), - compact: bool = typer.Option( - False, - "--compact", - "-C", - help="Whether to return a compact representation of the associations", - ), - limit: int = typer.Option(20, "--limit", "-l", help="The number of associations to return"), - offset: int = typer.Option(0, "--offset", help="The offset of the first association to be retrieved"), - 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"), + category: Annotated[ + Optional[List[AssociationCategory]], + typer.Option( + "--category", + "-c", + help="Category to get associations for", + ), + ] = None, + subject: Annotated[ + Optional[List[str]], + typer.Option( + "--subject", + "-s", + help="Subject ID to get associations for", + ), + ] = None, + predicate: Annotated[ + Optional[List[AssociationPredicate]], + typer.Option( + "--predicate", + "-p", + help="Predicate ID to get associations for", + ), + ] = None, + object: Annotated[ + Optional[List[str]], + typer.Option( + "--object", + "-o", + help="Object ID to get associations for", + ), + ] = None, + entity: Annotated[ + Optional[List[str]], + typer.Option( + "--entity", + "-e", + help="Entity (subject or object) ID to get associations for", + ), + ] = None, + direct: Annotated[ + bool, + typer.Option( + "--direct", + "-d", + help="Whether to exclude associations with subject/object as ancestors", + ), + ] = False, + compact: Annotated[ + bool, + typer.Option( + "--compact", + "-C", + help="Whether to return a compact representation of the associations", + ), + ] = False, + limit: fields.LimitOption = 20, + offset: fields.OffsetOption = 0, + fmt: fields.FormatOption = fields.OutputFormat.json, + output: fields.OutputOption = None, ): - """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 - direct: Whether to exclude associations with subject/object as ancestors - compact: Whether to return a compact representation of the associations - limit: The number of associations to return - 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) + """ + Paginate through associations """ args = locals() args.pop("fmt", None) diff --git a/backend/src/monarch_py/utils/cli_fields.py b/backend/src/monarch_py/utils/cli_fields.py new file mode 100644 index 00000000..709caabc --- /dev/null +++ b/backend/src/monarch_py/utils/cli_fields.py @@ -0,0 +1,55 @@ +from enum import Enum +from typing import Annotated, Optional + +import typer + +# Common options + +QueryOption = Annotated[ + str, + typer.Option( + "--query", + "-q", + ) +] + +LimitOption = Annotated[ + int, + typer.Option( + "--limit", + "-l", + help="The number of results to return", + ), +] + +OffsetOption = Annotated[ + int, + typer.Option( + "--offset", + help="The offset of the first result to be retrieved", + ), +] + +class OutputFormat(str, Enum): + json = "json" + yaml = "yaml" + tsv = "tsv" + table = "table" + +FormatOption = Annotated[ + OutputFormat, + typer.Option( + "--format", + "-f", + help="The format of the output (json, yaml, tsv, table)", + ), +] + +OutputOption = Annotated[ + Optional[str], + typer.Option( + "--output", + "-o", + help="The path to write command output (stdout if not specified)", + ), +]