Repository navigation
feat: implement variant match search (issue #114) #125
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
Open
jennifer-bowser
wants to merge
19
commits into
issue-112-authenticate-incoming-requests
from
issue-114-implement-match-search
Open
Changes from all commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
bdc4da1
first pass at implementing match search - WIP
jennifer-bowser e333fc4
Merge branch 'issue-112-authenticate-incoming-requests' into issue-11…
jennifer-bowser 0e9e25a
first working version of match search
jennifer-bowser fbfb395
make the node selectable
jennifer-bowser 93b6f75
refactor to create selection helpers + add support for choosing an as…
jennifer-bowser fce991a
add support for chromsome name + start position selection
jennifer-bowser 16d0fd0
add support for selecting ref/alt basees
jennifer-bowser 4f4ffa7
switch to using typeadapter for chromosome entry + QOL tweaks to outp…
jennifer-bowser 4364e7a
add printout of query params to output
jennifer-bowser 536b76c
add documentation to funcs
jennifer-bowser c5418cd
update 'match_search'/'match_query' to append 'variant' for clairity.…
jennifer-bowser 8d0c92d
add comment to pyproject.toml explaining ruff ignore
jennifer-bowser 8331d8e
add documentation re: how to run the script
jennifer-bowser 183ed37
filter out nodes w/o a 'match_url' configured
jennifer-bowser b980a97
Merge branch 'issue-112-authenticate-incoming-requests' into issue-11…
jennifer-bowser b348078
whoops actually use the ref/alt bases the users selects instead of th…
jennifer-bowser 1e53810
move temp script wrapper into its own module
jennifer-bowser 6b91435
add more robust error handling
jennifer-bowser 5ffc376
Merge branch 'issue-112-authenticate-incoming-requests' into issue-11…
jennifer-bowser File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| """Search for a matching variant in other VLM Nodes, and retrieve their CAF data for this variant""" | ||
|
|
||
| from http import HTTPMethod | ||
|
|
||
| from requests.models import Response | ||
|
|
||
| from anyvlm.auth.token_provider import TokenProvider | ||
| from anyvlm.outbound.vlm_client import VLMClient | ||
| from anyvlm.utils.types import ( | ||
| ChromosomeName, | ||
| GrcAssemblyId, | ||
| Nucleotide, | ||
| ) | ||
|
|
||
|
|
||
| def variant_match_search( | ||
| query_url: str, | ||
| assembly_id: GrcAssemblyId, | ||
| reference_name: ChromosomeName, | ||
| start: int, | ||
| reference_bases: Nucleotide, | ||
| alternate_bases: Nucleotide, | ||
| ) -> Response: | ||
| """Search for a matching variant in other VLM Nodes, and retrieve their CAF data for this variant. | ||
|
|
||
| :param query_url: The URL for the node you are querying | ||
| :assembly_id: The assembly for variant you're matching | ||
| :reference_name: The name of the chromosome the variant you're matching is on | ||
| :start: The start position of the variant you're matching | ||
| :reference_bases: The reference base for the variant you're matching | ||
| :alternate_bases: The alternate base for hte variant you're matching | ||
| :return: The Response object from the node you're querying. Will match | ||
| """ | ||
| query_url += f"?assemblyId={assembly_id}&referenceName={reference_name}&start={start}&referenceBases={reference_bases}&alternateBases={alternate_bases}" | ||
| vlm_client: VLMClient = VLMClient(token_provider=TokenProvider()) | ||
| return vlm_client.request(method_type=HTTPMethod.GET, url=query_url) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| """Utility scripts""" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,267 @@ | ||
| """Prompt a user to query an external VLM Network node | ||
| Run this script with the following command: `python -m src.anyvlm.scripts.run_variant_match` | ||
| """ | ||
|
|
||
| import sys | ||
| from http import HTTPStatus | ||
| from pprint import pprint | ||
| from typing import TypeVar | ||
|
|
||
| import requests | ||
| from dotenv import load_dotenv | ||
| from pydantic import TypeAdapter, ValidationError | ||
| from requests.models import Response | ||
|
|
||
| from anyvlm.auth.token_provider import TokenProvider | ||
| from anyvlm.outbound.node_registry import NodeRegistry, VLMNode | ||
| from anyvlm.outbound.variant_match_search import variant_match_search | ||
| from anyvlm.outbound.vlm_client import VLMClient | ||
| from anyvlm.utils.types import ( | ||
| ChromosomeName, | ||
| GrcAssemblyId, | ||
| Nucleotide, | ||
| chromosome_name_adapter, | ||
| nucleotide_adapter, | ||
| ) | ||
|
|
||
| load_dotenv() # required for 'TokenProvider' instantiation | ||
|
|
||
| T = TypeVar(name="T") | ||
|
|
||
|
|
||
| def _exit_early() -> None: | ||
| """Print an 'exiting' message and exit""" | ||
| print("🛑 exiting...") | ||
| sys.exit() | ||
|
|
||
|
|
||
| def _validate_selected_int( | ||
| selection: str, max_value: int, min_value: int = 0 | ||
| ) -> int | None: | ||
| """Ensure an inputted number is an int, and that it is within the specified range | ||
|
|
||
| :param selection: The selected int to be validated | ||
| :param max_value: The maximum allowable value for the int | ||
| :param min_value: [Optional] The minimum allowable value for the int. Defaults to `0` if not set. | ||
| :returns: The value as an `int` if valid, else `None` | ||
| """ | ||
| try: | ||
| selection_number: int = int(selection) | ||
| except ValueError: | ||
| return None | ||
|
|
||
| if selection_number <= min_value or selection_number > max_value: | ||
| return None | ||
|
|
||
| return selection_number | ||
|
|
||
|
|
||
| def _select_option_helper( | ||
| options_list: list[T], | ||
| prompt: str, | ||
| no_results_message: str, | ||
| option_attribute: str | None = None, | ||
| ) -> T: | ||
| """A helper function to prompt a user to select one choice from an options list repeatedly until they choose a valid selection | ||
|
|
||
| :param options_list: The list of options from which the user should choose | ||
| :param prompt: The message to display to the user directing them to make a selection | ||
| :param no_results_message: The message to display to the user if the 'options_list' is empty | ||
| :param option_attribute: [Optional] If the options in your 'options_list' are objects, specify the attribute of those objects that should be displayed for the options list | ||
| :returns: A result of the type specified by your options list | ||
| """ | ||
| print("\n" + prompt) | ||
|
|
||
| num_options: int = len(options_list) | ||
| if num_options == 0: | ||
| print(no_results_message) | ||
| _exit_early() | ||
|
|
||
| for i, option in enumerate(options_list): | ||
| selection_value = ( | ||
| getattr(option, option_attribute) if option_attribute else option | ||
| ) | ||
| print(f" ({i + 1}) {selection_value}") | ||
|
|
||
| while True: | ||
| selection: str = input("> Enter your selection's number: ") | ||
| selection_number: int | None = _validate_selected_int( | ||
| selection=selection, max_value=num_options | ||
| ) | ||
| if not selection_number: | ||
| print(f"❌ Please enter a number between 1-{num_options}") | ||
| else: | ||
| return options_list[int(selection_number) - 1] | ||
|
|
||
|
|
||
| def _validate_input_with_type_adapter( | ||
| type_adapter: TypeAdapter[T], prompt: str, error_message: str | None | ||
| ) -> T: | ||
| """Validates that user inputs matches the constraints defined by the provided TypeAdapter | ||
|
|
||
| :param type_adapter: The TypeAdapter to use for validation | ||
| :parma prompt: The message to display to the user directing them to make a selection | ||
| :param error_message: [Optional] A message to display to the user if the validation fails, prompting them to try again. If omitted, the function will fallback to using the error message raised during the failed validation. | ||
| """ | ||
| print() | ||
| while True: | ||
| raw_input: str = input("> " + prompt) | ||
| try: | ||
| transformed_input: T = type_adapter.validate_python(raw_input) | ||
| except ValidationError as e: | ||
| print( | ||
| "❌ " | ||
| + ( | ||
| error_message | ||
| if error_message is not None | ||
| else f"{e.errors()[0]['msg']}" | ||
| ) | ||
| ) | ||
| else: | ||
| return transformed_input | ||
|
|
||
|
|
||
| def _select_node(node_registry: NodeRegistry) -> VLMNode: | ||
| """Prompts the user to select a node to query | ||
|
|
||
| :param node_registry: The list of nodes to choose from | ||
| :returns: A VLMNode of the user's choice | ||
| """ | ||
| return _select_option_helper( | ||
| options_list=[node for node in node_registry.get_nodes() if node.match_url], | ||
| prompt="Select a node to query from the list below:", | ||
| no_results_message="Sorry, no nodes are currently registered on the network", | ||
| option_attribute="client_name", | ||
| ) | ||
|
|
||
|
|
||
| def _select_assembly() -> GrcAssemblyId: | ||
| """Prompts the user to select an assembly for the variant they want to match on | ||
|
|
||
| :returns: The user's selected assembly | ||
| """ | ||
| return _select_option_helper( | ||
| options_list=list[GrcAssemblyId](GrcAssemblyId), | ||
| prompt="Select the assembly for the variant you're matching on:", | ||
| no_results_message="Whoops, there was an error retrieving assembly options", | ||
| ) | ||
|
|
||
|
|
||
| def _select_reference_name() -> ChromosomeName: | ||
| """Prompts the user to select a chromosome for the variant they want to match on | ||
|
|
||
| :returns: The user's selected chromosome | ||
| """ | ||
| return _validate_input_with_type_adapter( | ||
| type_adapter=chromosome_name_adapter, | ||
| prompt="Enter the chromosome for the variant you're matching on: ", | ||
| error_message=None, | ||
| ) | ||
|
|
||
|
|
||
| def _select_start_position() -> int: | ||
| """Prompts the user to select a start position for the variant they want to match on | ||
|
|
||
| :returns: The user's selected start position | ||
| """ | ||
| print() | ||
| max_start_position = 250000000 # heuristic cap; longest chromosome on either assembly is just under this length | ||
| while True: | ||
| start_position_string: str = input( | ||
| "Enter the start position for the variant you're matching on: " | ||
| ) | ||
| start_position_number: int | None = _validate_selected_int( | ||
| selection=start_position_string, max_value=max_start_position | ||
| ) | ||
| if not start_position_number: | ||
| print(f"❌ Please enter a number between 1-{max_start_position}") | ||
| else: | ||
| return start_position_number | ||
|
|
||
|
|
||
| def _select_base(base_type: str) -> Nucleotide: | ||
| """Prompts the user to select a nucleotide base of the specified type | ||
|
|
||
| :param base_type: The type of base to prompt the user for | ||
| :returns: The user's selected nucleotide base | ||
| """ | ||
| return _validate_input_with_type_adapter( | ||
| type_adapter=nucleotide_adapter, | ||
| prompt=f"Enter the {base_type} base for the variant you're matching on: ", | ||
| error_message="Please enter a valid nucleotide. (Note: only single bases are supported by the VLM Network at this time)", | ||
| ) | ||
|
|
||
|
|
||
| def print_args(**kwargs) -> None: # pyright: ignore[reportUnknownParameterType, reportMissingParameterType] | ||
| """Prints out the provided arguments to the console""" | ||
| for name, value in kwargs.items(): | ||
| print(f"\t{name} = {value!r}") | ||
|
|
||
|
|
||
| def run_variant_match_query() -> None: | ||
| """Script to prompt a user to query an external VLM Network node""" | ||
| print("VARIANT MATCH QUERY") | ||
| vlm_client: VLMClient = VLMClient(token_provider=TokenProvider()) | ||
| node_registry: NodeRegistry = NodeRegistry(vlm_client) | ||
| selected_node: VLMNode = _select_node(node_registry) | ||
|
|
||
| match_url: str | None = selected_node.match_url | ||
| if not match_url: | ||
| print("Sorry, your selected node does not support match queries at this time") | ||
| _exit_early() | ||
|
|
||
| print("\n⬇️ Tell us more about the variant you're matching against:") | ||
| assembly: GrcAssemblyId = _select_assembly() | ||
| reference_name: ChromosomeName = _select_reference_name() | ||
| start_position: int = _select_start_position() | ||
| reference_base: Nucleotide = _select_base(base_type="reference") | ||
| alternate_base: Nucleotide = _select_base(base_type="alternate") | ||
|
|
||
| print("\n---\n") | ||
| print( | ||
| f"Querying '{selected_node.client_name}' at '{match_url}' with the following parameters:" | ||
| ) | ||
| print_args( | ||
| assembly=assembly.value, | ||
| reference_name=reference_name, | ||
| start_position=start_position, | ||
| reference_base=reference_base, | ||
| alternate_base=alternate_base, | ||
| ) | ||
|
|
||
| print("\nRESULT:") | ||
| response: Response | None = None | ||
| try: | ||
| response = variant_match_search( | ||
| query_url=match_url, # pyright: ignore[reportArgumentType] | ||
| assembly_id=assembly, | ||
| reference_name=reference_name, | ||
| start=start_position, | ||
| reference_bases=reference_base, | ||
| alternate_bases=alternate_base, | ||
| ) | ||
| response.raise_for_status() | ||
| except requests.exceptions.Timeout: | ||
| print("❌ ERROR: Request timed out") | ||
| raise | ||
| except requests.exceptions.ConnectionError: | ||
| print("❌ ERROR: Failed to connect to server") | ||
| raise | ||
| except requests.exceptions.HTTPError as e: | ||
| print(f"❌ ERROR: HTTP error: {e.response.status_code}") | ||
| raise | ||
| except requests.exceptions.RequestException as e: | ||
| print(f"❌ ERROR: Request failed: {e}") | ||
| raise | ||
|
|
||
| if response and response.status_code == HTTPStatus.OK: | ||
| data = response.json() | ||
| pprint(object=data) | ||
| else: | ||
| print( | ||
| f"Sorry, there was an error querying this node. We received a response status code of: {response.status_code}" | ||
| ) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| run_variant_match_query() |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.