From e47127f0ab4ccac3a5c6157cab72916b3a4b6ebc Mon Sep 17 00:00:00 2001 From: Kori Kuzma Date: Wed, 3 Jul 2024 10:45:17 -0400 Subject: [PATCH 1/3] chore!: remove FastAPI close #208, #198 * no longer needed --- docs/source/usage.rst | 23 ---- pyproject.toml | 5 - src/cool_seq_tool/api.py | 41 ------- src/cool_seq_tool/routers/__init__.py | 17 --- src/cool_seq_tool/routers/default.py | 128 --------------------- src/cool_seq_tool/routers/mane.py | 98 ---------------- src/cool_seq_tool/routers/mappings.py | 155 -------------------------- 7 files changed, 467 deletions(-) delete mode 100644 src/cool_seq_tool/api.py delete mode 100644 src/cool_seq_tool/routers/__init__.py delete mode 100644 src/cool_seq_tool/routers/default.py delete mode 100644 src/cool_seq_tool/routers/mane.py delete mode 100644 src/cool_seq_tool/routers/mappings.py diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 100b3e76..f3463a91 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -58,29 +58,6 @@ Descriptions and examples of functions can be found in the :ref:`API Reference < See the `asyncio module documentation `_ for more information. -REST server ------------ - -Core Cool-Seq-Tool functions can also be performed via a REST HTTP interface, provided via `FastAPI `_. Use the ``uvicorn`` shell command to start a server instance: - -.. code-block:: shell - - uvicorn cool_seq_tool.api:app - -By default, ``uvicorn`` serves to port 8000. Once initialized, go to ``_ in a web browser for OpenAPI docs describing available endpoints. - -REST routes are defined using the FastAPI ``APIRouter`` class, meaning that they can also be mounted to other FastAPI applications: - -.. code-block:: python - - from fastapi import FastAPI - from cool_seq_tool.routers import mane - - app = FastAPI() - app.include_router(mane.router) - -.. _configuration: - Environment configuration ------------------------- diff --git a/pyproject.toml b/pyproject.toml index 8989801c..e8fc5cb9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,6 @@ authors = [ readme = "README.md" classifiers = [ "Development Status :: 3 - Alpha", - "Framework :: FastAPI", "Framework :: Pydantic", "Framework :: Pydantic :: 2", "Intended Audience :: Science/Research", @@ -34,7 +33,6 @@ dependencies = [ "biocommons.seqrepo", "pydantic == 2.*", "uvicorn", - "fastapi", "ga4gh.vrs", "wags-tails ~= 0.1.3" ] @@ -178,8 +176,5 @@ lint.ignore = [ "*__init__.py" = ["F401"] "src/cool_seq_tool/schemas.py" = ["ANN201", "N805", "ANN001"] -[tool.ruff.lint.flake8-bugbear] -extend-immutable-calls = ["fastapi.Query"] - [tool.ruff.format] docstring-code-format = true diff --git a/src/cool_seq_tool/api.py b/src/cool_seq_tool/api.py deleted file mode 100644 index ddcd26f5..00000000 --- a/src/cool_seq_tool/api.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Main application for FastAPI""" - -from fastapi import FastAPI -from fastapi.openapi.utils import get_openapi - -from cool_seq_tool import __version__ -from cool_seq_tool.routers import SERVICE_NAME, default, mane, mappings - -app = FastAPI( - docs_url=f"/{SERVICE_NAME}", - openapi_url=f"/{SERVICE_NAME}/openapi.json", - swagger_ui_parameters={"tryItOutEnabled": True}, -) - - -app.include_router(default.router) -app.include_router(mane.router) -app.include_router(mappings.router) - - -def custom_openapi() -> dict: - """Generate custom fields for OpenAPI response.""" - if app.openapi_schema: - return app.openapi_schema - openapi_schema = get_openapi( - title="The GenomicMedLab Cool-Seq-Tool", - version=__version__, - description="Common Operations On Lots of Sequences Tool.", - routes=app.routes, - ) - - openapi_schema["info"]["contact"] = { - "name": "Alex H. Wagner", - "email": "Alex.Wagner@nationwidechildrens.org", - "url": "https://www.nationwidechildrens.org/specialties/institute-for-genomic-medicine/research-labs/wagner-lab", - } - app.openapi_schema = openapi_schema - return app.openapi_schema - - -app.openapi = custom_openapi diff --git a/src/cool_seq_tool/routers/__init__.py b/src/cool_seq_tool/routers/__init__.py deleted file mode 100644 index 210bbfe4..00000000 --- a/src/cool_seq_tool/routers/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Module for routers""" - -from enum import Enum - -from cool_seq_tool.app import CoolSeqTool - -cool_seq_tool = CoolSeqTool() -SERVICE_NAME = "cool_seq_tool" -RESP_DESCR = "A response to a validly-formed query." -UNHANDLED_EXCEPTION_MSG = "Unhandled exception occurred. Check logs for more details." - - -class Tags(str, Enum): - """Define tags for endpoints""" - - MANE_TRANSCRIPT = "MANE Transcript" - ALIGNMENT_MAPPER = "Alignment Mapper" diff --git a/src/cool_seq_tool/routers/default.py b/src/cool_seq_tool/routers/default.py deleted file mode 100644 index 12cea2fa..00000000 --- a/src/cool_seq_tool/routers/default.py +++ /dev/null @@ -1,128 +0,0 @@ -"""Module containing default routes""" - -import logging -import os -import tempfile -from pathlib import Path - -from fastapi import APIRouter, HTTPException, Query -from fastapi.responses import FileResponse -from starlette.background import BackgroundTasks - -from cool_seq_tool.routers import ( - RESP_DESCR, - SERVICE_NAME, - UNHANDLED_EXCEPTION_MSG, - cool_seq_tool, -) -from cool_seq_tool.schemas import ( - GenomicDataResponse, - GenomicRequestBody, - TranscriptRequestBody, -) -from cool_seq_tool.utils import service_meta - -_logger = logging.getLogger(__name__) - -router = APIRouter(prefix=f"/{SERVICE_NAME}") - - -@router.post( - "/genomic_to_transcript_exon_coordinates", - summary="Get transcript exon data given genomic coordinate data", - response_description=RESP_DESCR, - description="Return transcript exon data", - response_model=GenomicDataResponse, -) -async def genomic_to_transcript_exon_coordinates( - request_body: GenomicRequestBody, -) -> GenomicDataResponse: - """Get transcript exon data given genomic coordinate data - - :param GenomicRequestBody request_body: Request body - - Returns: GenomicDataResponse with data and warnings - """ - request_body = request_body.model_dump() - - response = GenomicDataResponse( - genomic_data=None, warnings=[], service_meta=service_meta() - ) - - try: - response = await cool_seq_tool.ex_g_coords_mapper.genomic_to_transcript_exon_coordinates( - **request_body - ) - except Exception as e: - _logger.error( - "genomic_to_transcript_exon_coordinates unhandled exception %s", e - ) - response.warnings.append(UNHANDLED_EXCEPTION_MSG) - - return response - - -@router.post( - "/transcript_to_genomic_coordinates", - summary="Get genomic coordinate data given transcript exon data", - response_description=RESP_DESCR, - description="Return genomic coordinate data", - response_model=GenomicDataResponse, -) -async def transcript_to_genomic_coordinates( - request_body: TranscriptRequestBody, -) -> GenomicDataResponse: - """Get transcript exon data given genomic coordinate data - - :param TranscriptRequestBody request_body: Request body - - Returns: GenomicDataResponse with data and warnings - """ - request_body = request_body.model_dump() - - response = GenomicDataResponse( - genomic_data=None, warnings=[], service_meta=service_meta() - ) - - try: - response = ( - await cool_seq_tool.ex_g_coords_mapper.transcript_to_genomic_coordinates( - **request_body - ) - ) - except Exception as e: - _logger.error("transcript_to_genomic_coordinates unhandled exception %s", e) - response.warnings.append(UNHANDLED_EXCEPTION_MSG) - - return response - - -@router.get( - "/download_sequence", - summary="Get sequence for ID", - response_description=RESP_DESCR, - description="Given a known accession identifier, retrieve sequence data and return" - "as a FASTA file", - response_class=FileResponse, -) -async def get_sequence( - background_tasks: BackgroundTasks, - sequence_id: str = Query( - ..., description="ID of sequence to retrieve, sans namespace" - ), -) -> FileResponse: - """Get sequence for requested sequence ID. - :param sequence_id: accession ID, sans namespace, eg `NM_152263.3` - :param background_tasks: Starlette background tasks object. Use to clean up - tempfile after get method returns. - :return: FASTA file if successful, or 404 if unable to find matching resource - """ - _, path = tempfile.mkstemp(suffix=".fasta") - try: - cool_seq_tool.seqrepo_access.get_fasta_file(sequence_id, Path(path)) - except KeyError as e: - raise HTTPException( - status_code=404, detail="No sequence available for requested identifier" - ) from e - background_tasks.add_task(lambda p: os.unlink(p), path) # noqa: PTH108 - return FileResponse(path) diff --git a/src/cool_seq_tool/routers/mane.py b/src/cool_seq_tool/routers/mane.py deleted file mode 100644 index 64cda1d6..00000000 --- a/src/cool_seq_tool/routers/mane.py +++ /dev/null @@ -1,98 +0,0 @@ -"""Module containing routes related to MANE data""" - -import logging - -from fastapi import APIRouter, Query - -from cool_seq_tool.routers import ( - RESP_DESCR, - SERVICE_NAME, - UNHANDLED_EXCEPTION_MSG, - Tags, - cool_seq_tool, -) -from cool_seq_tool.schemas import AnnotationLayer, ManeDataService, ResidueMode -from cool_seq_tool.utils import service_meta - -_logger = logging.getLogger(__name__) - -router = APIRouter(prefix=f"/{SERVICE_NAME}/mane") - - -ref_descr = ( - "Reference at position given during input. When this is set, it will " - "ensure that the reference sequences match for the final result." -) -try_longest_compatible_descr = ( - "`True` if should try longest compatible remaining if" - " mane transcript was not compatible. `False` otherwise." -) - - -@router.get( - "/get_mane_data", - summary="Retrieve MANE data in inter-residue coordinates", - response_description=RESP_DESCR, - description="Return MANE Select, MANE Plus Clinical, or Longest Remaining " - "Transcript data in inter-residue coordinates. See our docs for " - "more information on transcript priority.", - response_model=ManeDataService, - tags=[Tags.MANE_TRANSCRIPT], -) -async def get_mane_data( - ac: str = Query(..., description="Accession"), - start_pos: int = Query(..., description="Start position"), - start_annotation_layer: AnnotationLayer = Query( - ..., description="Starting annotation layer for query" - ), - end_pos: int | None = Query( - None, description="End position. If not set, will set to `start_pos`." - ), - gene: str | None = Query(None, description="HGNC gene symbol"), - ref: str | None = Query(None, description=ref_descr), - try_longest_compatible: bool = Query( - True, description=try_longest_compatible_descr - ), - residue_mode: ResidueMode = Query( - ResidueMode.RESIDUE, description="Residue mode for position(s)" - ), -) -> ManeDataService: - """Return MANE or Longest Compatible Remaining Transcript data on inter-residue - coordinates - - :param str ac: Accession - :param int start_pos: Start position - :param AnnotationLayer start_annotation_layer: Starting annotation layer for query - :param Optional[int] end_pos: End position. If `None` assumes - both `start_pos` and `end_pos` have same values. - :param Optional[str] gene: Gene symbol - :param Optional[str] ref: Reference at position given during input - :param bool try_longest_compatible: `True` if should try longest - compatible remaining if mane transcript was not compatible. - `False` otherwise. - :param ResidueMode residue_mode: Starting residue mode for `start_pos` - and `end_pos`. Will always return coordinates in inter-residue - """ - warnings = [] - mane_data = None - try: - mane_data = await cool_seq_tool.mane_transcript.get_mane_transcript( - ac=ac, - start_pos=start_pos, - end_pos=end_pos, - start_annotation_layer=start_annotation_layer, - gene=gene, - ref=ref, - try_longest_compatible=try_longest_compatible, - residue_mode=residue_mode, - ) - - if not mane_data: - warnings.append("Unable to retrieve MANE data") - except Exception as e: - _logger.exception("get_mane_data unhandled exception %s", e) - warnings.append(UNHANDLED_EXCEPTION_MSG) - - return ManeDataService( - mane_data=mane_data, warnings=warnings, service_meta=service_meta() - ) diff --git a/src/cool_seq_tool/routers/mappings.py b/src/cool_seq_tool/routers/mappings.py deleted file mode 100644 index 083624b1..00000000 --- a/src/cool_seq_tool/routers/mappings.py +++ /dev/null @@ -1,155 +0,0 @@ -"""Module containing routes related to alignment mapping""" - -import logging - -from fastapi import APIRouter, Query - -from cool_seq_tool.routers import RESP_DESCR, SERVICE_NAME, Tags, cool_seq_tool -from cool_seq_tool.schemas import Assembly, ResidueMode, ToCdnaService, ToGenomicService -from cool_seq_tool.utils import service_meta - -_logger = logging.getLogger(__name__) - -router = APIRouter(prefix=f"/{SERVICE_NAME}/alignment_mapper") - - -@router.get( - "/p_to_c", - summary="Translate protein representation to cDNA representation", - response_description=RESP_DESCR, - description="Given protein accession and positions, return associated cDNA " - "accession and positions to codon(s)", - response_model=ToCdnaService, - tags=[Tags.ALIGNMENT_MAPPER], -) -async def p_to_c( - p_ac: str = Query(..., description="Protein RefSeq accession"), - p_start_pos: int = Query(..., description="Protein start position"), - p_end_pos: int = Query(..., description="Protein end position"), - residue_mode: ResidueMode = Query( - ResidueMode.RESIDUE, - description="Residue mode for `p_start_pos` and `p_end_pos`", - ), -) -> ToCdnaService: - """Translate protein representation to cDNA representation - - :param str p_ac: Protein RefSeq accession - :param int p_start_pos: Protein start position - :param int p_end_pos: Protein end position - :param ResidueMode residue_mode: Residue mode for `p_start_pos` and `p_end_pos`. - :return: ToCdnaService containing cDNA representation, warnings, and - service meta - """ - try: - c_data, w = await cool_seq_tool.alignment_mapper.p_to_c( - p_ac, p_start_pos, p_end_pos, residue_mode - ) - except Exception as e: - _logger.error("Unhandled exception: %s", str(e)) - w = "Unhandled exception. See logs for more information." - c_data = None - return ToCdnaService( - c_data=c_data, warnings=[w] if w else [], service_meta=service_meta() - ) - - -@router.get( - "/c_to_g", - summary="Translate cDNA representation to genomic representation", - response_description=RESP_DESCR, - description="Given cDNA accession and positions for codon(s), return associated genomic" - " accession and positions for a given target genome assembly", - response_model=ToGenomicService, - tags=[Tags.ALIGNMENT_MAPPER], -) -async def c_to_g( - c_ac: str = Query(..., description="cDNA RefSeq accession"), - c_start_pos: int = Query(..., description="cDNA start position for codon"), - c_end_pos: int = Query(..., description="cDNA end position for codon"), - cds_start: int | None = Query( - None, description="CDS start site. If not provided, this will be computed." - ), - residue_mode: ResidueMode = Query( - ResidueMode.RESIDUE, - description="Residue mode for `c_start_pos` and `c_end_pos`", - ), - target_genome_assembly: Assembly = Query( - Assembly.GRCH38, description="Genomic assembly to map to" - ), -) -> ToGenomicService: - """Translate cDNA representation to genomic representation - - :param str c_ac: cDNA RefSeq accession - :param int c_start_pos: cDNA start position for codon - :param int c_end_pos: cDNA end position for codon - :param Optional[int] cds_start: CDS start site. If not provided, this will be - computed. - :param ResidueMode residue_mode: Residue mode for `c_start_pos` and `c_end_pos`. - :param Assembly target_genome_assembly: Genome assembly to get genomic data for - :return: ToGenomicService containing genomic representation, warnings, and - service meta - """ - try: - g_data, w = await cool_seq_tool.alignment_mapper.c_to_g( - c_ac, - c_start_pos, - c_end_pos, - cds_start=cds_start, - residue_mode=residue_mode, - target_genome_assembly=target_genome_assembly, - ) - except Exception as e: - _logger.error("Unhandled exception: %s", str(e)) - w = "Unhandled exception. See logs for more information." - g_data = None - return ToGenomicService( - g_data=g_data, warnings=[w] if w else [], service_meta=service_meta() - ) - - -@router.get( - "/p_to_g", - summary="Translate protein representation to genomic representation", - response_description=RESP_DESCR, - description="Given protein accession and positions, return associated genomic " - "accession and positions for a given target genome assembly", - response_model=ToGenomicService, - tags=[Tags.ALIGNMENT_MAPPER], -) -async def p_to_g( - p_ac: str = Query(..., description="Protein RefSeq accession"), - p_start_pos: int = Query(..., description="Protein start position"), - p_end_pos: int = Query(..., description="Protein end position"), - residue_mode: ResidueMode = Query( - ResidueMode.RESIDUE, - description="Residue mode for `p_start_pos` and `p_end_pos`", - ), - target_genome_assembly: Assembly = Query( - Assembly.GRCH38, description="Genomic assembly to map to" - ), -) -> ToGenomicService: - """Translate protein representation to genomic representation - - :param str p_ac: Protein RefSeq accession - :param int p_start_pos: Protein start position - :param int p_end_pos: Protein end position - :param ResidueMode residue_mode: Residue mode for `p_start_pos` and `p_end_pos`. - :param Assembly target_genome_assembly: Genome assembly to get genomic data for - :return: ToGenomicService containing genomic representation, warnings, and - service meta - """ - try: - g_data, w = await cool_seq_tool.alignment_mapper.p_to_g( - p_ac, - p_start_pos, - p_end_pos, - residue_mode=residue_mode, - target_genome_assembly=target_genome_assembly, - ) - except Exception as e: - _logger.error("Unhandled exception: %s", str(e)) - w = "Unhandled exception. See logs for more information." - g_data = None - return ToGenomicService( - g_data=g_data, warnings=[w] if w else [], service_meta=service_meta() - ) From f5b88e5978d6f9a3fa3597855b9e292b12b25cfa Mon Sep 17 00:00:00 2001 From: Kori Kuzma Date: Wed, 3 Jul 2024 10:49:33 -0400 Subject: [PATCH 2/3] add back config directive --- docs/source/usage.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/source/usage.rst b/docs/source/usage.rst index f3463a91..f90b0206 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -58,6 +58,8 @@ Descriptions and examples of functions can be found in the :ref:`API Reference < See the `asyncio module documentation `_ for more information. +.. _configuration: + Environment configuration ------------------------- From fe5c7926de08fd6763071caf2b097c49d6fa9bcf Mon Sep 17 00:00:00 2001 From: Kori Kuzma Date: Wed, 3 Jul 2024 11:11:03 -0400 Subject: [PATCH 3/3] build: rm uvicorn --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e8fc5cb9..68b87c40 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,6 @@ dependencies = [ "hgvs", "biocommons.seqrepo", "pydantic == 2.*", - "uvicorn", "ga4gh.vrs", "wags-tails ~= 0.1.3" ]