From 95b95a7e15ff1014f02f2dc597117b0f64a527da Mon Sep 17 00:00:00 2001 From: James Stevenson Date: Wed, 30 Aug 2023 09:52:22 -0400 Subject: [PATCH] style: use Ruff and Black (#259) --- .github/workflows/backend_checks.yml | 31 +++++++++++++ .gitignore | 1 - .pre-commit-config.yaml | 14 +++--- server/.flake8 | 20 --------- server/curfu/__init__.py | 11 ++--- server/curfu/cli.py | 9 ++-- server/curfu/devtools/__init__.py | 3 +- server/curfu/devtools/build_client_types.py | 3 +- server/curfu/devtools/gene.py | 11 +++-- server/curfu/devtools/interpro.py | 18 ++++---- server/curfu/domain_services.py | 10 +++-- server/curfu/gene_services.py | 15 +++---- server/curfu/main.py | 27 ++++++------ server/curfu/routers/complete.py | 13 +++--- server/curfu/routers/constructors.py | 19 ++++---- server/curfu/routers/demo.py | 39 ++++++++--------- server/curfu/routers/lookup.py | 9 ++-- server/curfu/routers/meta.py | 3 +- server/curfu/routers/nomenclature.py | 13 +++--- server/curfu/routers/utilities.py | 17 ++++---- server/curfu/routers/validate.py | 2 +- server/curfu/schemas.py | 19 ++++---- server/curfu/sequence_services.py | 6 +-- server/curfu/utils.py | 15 +++---- server/pyproject.toml | 43 +++++++++++++++++++ server/setup.cfg | 3 +- server/tests/conftest.py | 4 +- server/tests/integration/test_main.py | 8 ++-- server/tests/integration/test_nomenclature.py | 2 +- server/tests/integration/test_utilities.py | 3 +- server/tests/integration/test_validate.py | 3 +- 31 files changed, 217 insertions(+), 177 deletions(-) create mode 100644 .github/workflows/backend_checks.yml delete mode 100644 server/.flake8 create mode 100644 server/pyproject.toml diff --git a/.github/workflows/backend_checks.yml b/.github/workflows/backend_checks.yml new file mode 100644 index 00000000..dec028d0 --- /dev/null +++ b/.github/workflows/backend_checks.yml @@ -0,0 +1,31 @@ +name: backend_checks +on: [push, pull_request] +jobs: + build: + name: build + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: 3.11 + + - name: Install dependencies + run: pip install server/ + lint: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: black + uses: psf/black@stable + with: + src: "./server" + + - name: ruff + uses: chartboost/ruff-action@v1 + with: + src: "./server" diff --git a/.gitignore b/.gitignore index 1b417e78..9af1cf3d 100644 --- a/.gitignore +++ b/.gitignore @@ -160,7 +160,6 @@ dynamodb_local_latest/* # Build files Pipfile.lock -pyproject.toml # client-side things curation/client/node_modules diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ba775bbf..c1a48b23 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,17 +2,21 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v1.4.0 hooks: - - id: flake8 - additional_dependencies: [flake8-docstrings] - args: ["--config=server/.flake8"] - id: check-added-large-files args: ["--maxkb=2000"] - id: detect-private-key + - id: trailing-whitespace + - id: end-of-file-fixer - repo: https://github.com/psf/black - rev: 22.6.0 + rev: 23.7.0 hooks: - id: black - args: [--diff, --check] + language_version: python3.11 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.0.286 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] - repo: https://github.com/pre-commit/mirrors-eslint rev: "v8.20.0" hooks: diff --git a/server/.flake8 b/server/.flake8 deleted file mode 100644 index 6b13a128..00000000 --- a/server/.flake8 +++ /dev/null @@ -1,20 +0,0 @@ -[flake8] -extend-ignore = D205, D400, I101, ANN101, ANN002, ANN003, W503, D301 -max-line-length = 100 -exclude = - .git - venv - __pycache__ - source - outputs - docs/* - analysis/* -inline-quotes = " -import-order-style = pep8 -application-import-names = - curation - tests -per-file-ignores = - tests/*:ANN001, ANN2 - *__init__.py:F401 -mypy-init-return = true diff --git a/server/curfu/__init__.py b/server/curfu/__init__.py index 523744b0..235e9429 100644 --- a/server/curfu/__init__.py +++ b/server/curfu/__init__.py @@ -1,11 +1,10 @@ """Fusion curation interface.""" -from pathlib import Path -from os import environ import logging +from os import environ +from pathlib import Path from .version import __version__ - # provide consistent paths APP_ROOT = Path(__file__).resolve().parents[0] @@ -51,13 +50,9 @@ SEQREPO_DATA_PATH = environ["SEQREPO_DATA_PATH"] -class ServiceWarning(Exception): +class LookupServiceError(Exception): """Custom Exception to use when lookups fail in curation services.""" - def __init__(self, *args, **kwargs): - """Initialize exception.""" - super().__init__(*args, **kwargs) - # define max acceptable matches for autocomplete suggestions MAX_SUGGESTIONS = 50 diff --git a/server/curfu/cli.py b/server/curfu/cli.py index d5c22595..72b19aca 100644 --- a/server/curfu/cli.py +++ b/server/curfu/cli.py @@ -1,15 +1,15 @@ """Provide command-line interface to application and associated utilities.""" import os -from typing import Optional from pathlib import Path +from typing import Optional import click -from curfu import APP_ROOT +from curfu import APP_ROOT from curfu.devtools import DEFAULT_INTERPRO_TYPES from curfu.devtools.build_client_types import build_client_types -from curfu.devtools.interpro import build_gene_domain_maps from curfu.devtools.gene import GeneSuggestionBuilder +from curfu.devtools.interpro import build_gene_domain_maps @click.command() @@ -27,9 +27,8 @@ def serve(port: int) -> None: @click.group() -def devtools(): +def devtools() -> None: """Provide setup utilities for constructing data for Fusion Curation app.""" - pass types_help = """ diff --git a/server/curfu/devtools/__init__.py b/server/curfu/devtools/__init__.py index d6241a8c..c1ba6c1e 100644 --- a/server/curfu/devtools/__init__.py +++ b/server/curfu/devtools/__init__.py @@ -1,10 +1,11 @@ """Utility functions for application setup.""" import ftplib +from typing import Callable from curfu import logger -def ftp_download(domain: str, path: str, fname: str, callback) -> None: +def ftp_download(domain: str, path: str, fname: str, callback: Callable) -> None: """Acquire file via FTP. :param str domain: domain name for remote file host :param str path: path within host to desired file diff --git a/server/curfu/devtools/build_client_types.py b/server/curfu/devtools/build_client_types.py index a41abbca..8e2a940d 100644 --- a/server/curfu/devtools/build_client_types.py +++ b/server/curfu/devtools/build_client_types.py @@ -1,9 +1,10 @@ """Provide client type generation tooling.""" from pathlib import Path + from pydantic2ts.cli.script import generate_typescript_defs -def build_client_types(): +def build_client_types() -> None: """Construct type definitions for front-end client.""" client_dir = Path(__file__).resolve().parents[3] / "client" generate_typescript_defs( diff --git a/server/curfu/devtools/gene.py b/server/curfu/devtools/gene.py index 94b1e884..ddd2b07a 100644 --- a/server/curfu/devtools/gene.py +++ b/server/curfu/devtools/gene.py @@ -1,15 +1,14 @@ """Provide tools to build backend data relating to gene identification.""" -from typing import Dict, Tuple -from pathlib import Path from datetime import datetime as dt +from pathlib import Path from timeit import default_timer as timer +from typing import Dict, Tuple -from gene.query import QueryHandler import click +from gene.query import QueryHandler from curfu import APP_ROOT, logger - # type stub Map = Dict[str, Tuple[str, str, str]] @@ -27,7 +26,7 @@ class GeneSuggestionBuilder: alias_map = {} assoc_with_map = {} - def __init__(self): + def __init__(self) -> None: """Initialize class. TODO: think about how best to force prod environment @@ -108,7 +107,7 @@ def build_gene_suggest_maps(self, output_dir: Path = APP_ROOT / "data") -> None: break today = dt.strftime(dt.today(), "%Y%m%d") - for (map, name) in ( + for map, name in ( (self.xrefs_map, "xrefs"), (self.symbol_map, "symbols"), (self.label_map, "labels"), diff --git a/server/curfu/devtools/interpro.py b/server/curfu/devtools/interpro.py index 9f88c79b..d07ad739 100644 --- a/server/curfu/devtools/interpro.py +++ b/server/curfu/devtools/interpro.py @@ -1,16 +1,16 @@ """Provide utilities relating to data fetched from InterPro service.""" -import gzip -from typing import Tuple, Dict, Optional, Set -from pathlib import Path import csv -from datetime import datetime -from timeit import default_timer as timer +import gzip import os import shutil -import xml.etree.ElementTree as ET +import xml.etree.ElementTree as ET # noqa: N817 +from datetime import datetime +from pathlib import Path +from timeit import default_timer as timer +from typing import Dict, Optional, Set, Tuple -from gene.query import QueryHandler import click +from gene.query import QueryHandler from curfu import APP_ROOT, logger from curfu.devtools import ftp_download @@ -34,7 +34,7 @@ def download_protein2ipr(output_dir: Path) -> None: gz_file_path = output_dir / "protein2ipr.dat.gz" with open(gz_file_path, "w") as fp: - def writefile(data): + def writefile(data): # noqa fp.write(data) ftp_download( @@ -283,7 +283,7 @@ def build_gene_domain_maps( # get relevant Interpro IDs interpro_data_bin = [] - def get_interpro_data(data): + def get_interpro_data(data): # noqa interpro_data_bin.append(data) ftp_download( diff --git a/server/curfu/domain_services.py b/server/curfu/domain_services.py index 55511b03..ca1b08af 100644 --- a/server/curfu/domain_services.py +++ b/server/curfu/domain_services.py @@ -1,13 +1,15 @@ """Provide lookup services for functional domains. -TODO +Todo: +---- * domains file should be a JSON and pre-pruned to unique pairs * get_possible_domains shouldn't have to force uniqueness + """ -from typing import List, Dict import csv +from typing import Dict, List -from curfu import logger, ServiceWarning +from curfu import LookupServiceError, logger from curfu.utils import get_data_file @@ -56,5 +58,5 @@ def get_possible_domains(self, gene_id: str) -> List[Dict]: domains = self.domains[gene_id.lower()] except KeyError: logger.warning(f"Unable to retrieve associated domains for {gene_id}") - raise ServiceWarning + raise LookupServiceError return domains diff --git a/server/curfu/gene_services.py b/server/curfu/gene_services.py index f70ed5ce..f4c5f985 100644 --- a/server/curfu/gene_services.py +++ b/server/curfu/gene_services.py @@ -1,15 +1,14 @@ """Wrapper for required Gene Normalization services.""" -from typing import List, Tuple, Dict, Union import csv +from typing import Dict, List, Tuple, Union +from ga4gh.vrsatile.pydantic.vrsatile_models import CURIE from gene.query import QueryHandler from gene.schemas import MatchType -from ga4gh.vrsatile.pydantic.vrsatile_models import CURIE -from curfu import logger, ServiceWarning, MAX_SUGGESTIONS +from curfu import MAX_SUGGESTIONS, LookupServiceError, logger from curfu.utils import get_data_file - # term -> (normalized ID, normalized label) Map = Dict[str, Tuple[str, str, str]] @@ -51,13 +50,13 @@ def get_normalized_gene( if not gd or not gd.gene_id: msg = f"Unexpected null property in normalized response for `{term}`" logger.error(msg) - raise ServiceWarning(msg) + raise LookupServiceError(msg) concept_id = gd.gene_id symbol = gd.label if not symbol: msg = f"Unable to retrieve symbol for gene {concept_id}" logger.error(msg) - raise ServiceWarning(msg) + raise LookupServiceError(msg) term_lower = term.lower() term_cased = None if response.match_type == 100: @@ -100,7 +99,7 @@ def get_normalized_gene( else: warn = f"Lookup of gene term {term} failed." logger.warning(warn) - raise ServiceWarning(warn) + raise LookupServiceError(warn) def suggest_genes(self, query: str) -> Dict[str, List[Tuple[str, str, str]]]: """Provide autocomplete suggestions based on submitted term. @@ -153,6 +152,6 @@ def suggest_genes(self, query: str) -> Dict[str, List[Tuple[str, str, str]]]: if n > MAX_SUGGESTIONS: warn = f"Exceeds max matches: Got {n} possible matches for {query} (limit: {MAX_SUGGESTIONS})" # noqa: E501 logger.warning(warn) - raise ServiceWarning(warn) + raise LookupServiceError(warn) else: return suggestions diff --git a/server/curfu/main.py b/server/curfu/main.py index 7f083a98..9f8e811e 100644 --- a/server/curfu/main.py +++ b/server/curfu/main.py @@ -3,24 +3,23 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates -from starlette.templating import _TemplateResponse as TemplateResponse from fusor import FUSOR +from starlette.templating import _TemplateResponse as TemplateResponse from curfu import APP_ROOT -from curfu.version import __version__ as curfu_version -from curfu.gene_services import GeneService from curfu.domain_services import DomainService +from curfu.gene_services import GeneService from curfu.routers import ( - nomenclature, - utilities, - constructors, - lookup, complete, - validate, + constructors, demo, + lookup, meta, + nomenclature, + utilities, + validate, ) - +from curfu.version import __version__ as curfu_version fastapi_app = FastAPI( version=curfu_version, @@ -52,8 +51,7 @@ def serve_react_app(app: FastAPI) -> FastAPI: - """ - Wrap application initialization in Starlette route param converter. + """Wrap application initialization in Starlette route param converter. :param app: FastAPI application instance :return: application with React frontend mounted @@ -67,8 +65,7 @@ def serve_react_app(app: FastAPI) -> FastAPI: @app.get("/{full_path:path}") async def serve_react_app(request: Request, full_path: str) -> TemplateResponse: - """ - Add arbitrary path support to FastAPI service. + """Add arbitrary path support to FastAPI service. React-router provides something akin to client-side routing based out of the Javascript embedded in index.html. However, FastAPI will intercede @@ -118,7 +115,7 @@ def get_domain_services() -> DomainService: @app.on_event("startup") -async def startup(): +async def startup() -> None: """Get FUSOR reference""" app.state.fusor = await start_fusor() app.state.genes = get_gene_services() @@ -126,6 +123,6 @@ async def startup(): @app.on_event("shutdown") -async def shutdown(): +async def shutdown() -> None: """Clean up thread pool.""" await app.state.fusor.cool_seq_tool.uta_db._connection_pool.close() diff --git a/server/curfu/routers/complete.py b/server/curfu/routers/complete.py index b879a5d4..804a8633 100644 --- a/server/curfu/routers/complete.py +++ b/server/curfu/routers/complete.py @@ -1,11 +1,10 @@ """Provide routes for autocomplete/term suggestion methods""" -from typing import Dict, Any +from typing import Any, Dict -from fastapi import Query, APIRouter, Request - -from curfu import ServiceWarning -from curfu.schemas import ResponseDict, AssociatedDomainResponse, SuggestGeneResponse +from fastapi import APIRouter, Query, Request +from curfu import LookupServiceError +from curfu.schemas import AssociatedDomainResponse, ResponseDict, SuggestGeneResponse router = APIRouter() @@ -29,7 +28,7 @@ def suggest_gene(request: Request, term: str = Query("")) -> ResponseDict: try: possible_matches = request.app.state.genes.suggest_genes(term) response.update(possible_matches) - except ServiceWarning as e: + except LookupServiceError as e: response["warnings"] = [str(e)] return response @@ -53,6 +52,6 @@ def suggest_domain(request: Request, gene_id: str = Query("")) -> ResponseDict: try: possible_matches = request.app.state.domains.get_possible_domains(gene_id) response["suggestions"] = possible_matches - except ServiceWarning: + except LookupServiceError: response["warnings"] = [f"No associated domains for {gene_id}"] return response diff --git a/server/curfu/routers/constructors.py b/server/curfu/routers/constructors.py index f461a9f4..88120656 100644 --- a/server/curfu/routers/constructors.py +++ b/server/curfu/routers/constructors.py @@ -1,21 +1,21 @@ """Provide routes for element construction endpoints""" from typing import Optional -from fastapi import Query, Request, APIRouter +from fastapi import APIRouter, Query, Request +from fusor.models import DomainStatus, RegulatoryClass, Strand from pydantic import ValidationError -from fusor.models import RegulatoryClass, Strand, DomainStatus from curfu import logger +from curfu.routers import parse_identifier from curfu.schemas import ( GeneElementResponse, - RegulatoryElementResponse, - TxSegmentElementResponse, - TemplatedSequenceElementResponse, GetDomainResponse, + RegulatoryElementResponse, ResponseDict, + TemplatedSequenceElementResponse, + TxSegmentElementResponse, ) -from curfu.routers import parse_identifier -from curfu.sequence_services import get_strand, InvalidInputException +from curfu.sequence_services import InvalidInputError, get_strand router = APIRouter() @@ -28,6 +28,7 @@ ) def build_gene_element(request: Request, term: str = Query("")) -> GeneElementResponse: """Construct valid gene element given user-provided term. + \f :param request: the HTTP request context, supplied by FastAPI. Use to access FUSOR and UTA-associated tools. @@ -108,7 +109,7 @@ async def build_tx_segment_gct( if strand is not None: try: strand_validated = get_strand(strand) - except InvalidInputException: + except InvalidInputError: warning = f"Received invalid strand value: {strand}" logger.warning(warning) return TxSegmentElementResponse(warnings=[warning], element=None) @@ -155,7 +156,7 @@ async def build_tx_segment_gcg( if strand is not None: try: strand_validated = get_strand(strand) - except InvalidInputException: + except InvalidInputError: warning = f"Received invalid strand value: {strand}" logger.warning(warning) return TxSegmentElementResponse(warnings=[warning], element=None) diff --git a/server/curfu/routers/demo.py b/server/curfu/routers/demo.py index 702a5e76..6feabb8b 100644 --- a/server/curfu/routers/demo.py +++ b/server/curfu/routers/demo.py @@ -1,39 +1,39 @@ """Provide routes for accessing demo objects to client.""" -from uuid import uuid4 from typing import Union +from uuid import uuid4 from fastapi import APIRouter, Request -from fusor import examples, FUSOR +from fusor import FUSOR, examples from fusor.models import ( - RegulatoryElement, - StructuralElementType, - CategoricalFusion, AssayedFusion, + CategoricalFusion, FUSORTypes, + RegulatoryElement, + StructuralElementType, ) from fusor.nomenclature import ( - tx_segment_nomenclature, - templated_seq_nomenclature, gene_nomenclature, reg_element_nomenclature, + templated_seq_nomenclature, + tx_segment_nomenclature, ) from curfu.schemas import ( - DemoResponse, - ClientTranscriptSegmentElement, + ClientAssayedFusion, + ClientCategoricalFusion, + ClientGeneElement, ClientLinkerElement, + ClientMultiplePossibleGenesElement, ClientTemplatedSequenceElement, - ClientGeneElement, + ClientTranscriptSegmentElement, ClientUnknownGeneElement, - ClientMultiplePossibleGenesElement, - TranscriptSegmentElement, + DemoResponse, + GeneElement, LinkerElement, + MultiplePossibleGenesElement, TemplatedSequenceElement, - GeneElement, + TranscriptSegmentElement, UnknownGeneElement, - MultiplePossibleGenesElement, - ClientCategoricalFusion, - ClientAssayedFusion, ) router = APIRouter() @@ -63,8 +63,7 @@ def clientify_structural_element( element: ElementUnion, fusor_instance: FUSOR, ) -> ClientElementUnion: - """ - Add fields required by client to structural element object. + """Add fields required by client to structural element object. \f :param element: a structural element object :param fusor_instance: instantiated FUSOR object, passed down from FastAPI request @@ -112,8 +111,8 @@ def clientify_structural_element( def clientify_fusion(fusion: Fusion, fusor_instance: FUSOR) -> ClientFusion: - """ - Add client-required properties to fusion object. + """Add client-required properties to fusion object. + :param fusion: fusion to append to :param fusor_instance: FUSOR object instance provided by FastAPI request context :return: completed client-ready fusion diff --git a/server/curfu/routers/lookup.py b/server/curfu/routers/lookup.py index 348b96e5..45854b5a 100644 --- a/server/curfu/routers/lookup.py +++ b/server/curfu/routers/lookup.py @@ -1,9 +1,8 @@ """Provide routes for basic data lookup endpoints""" -from fastapi import Query, Request, APIRouter - -from curfu import ServiceWarning -from curfu.schemas import ResponseDict, NormalizeGeneResponse +from fastapi import APIRouter, Query, Request +from curfu import LookupServiceError +from curfu.schemas import NormalizeGeneResponse, ResponseDict router = APIRouter() @@ -30,6 +29,6 @@ def normalize_gene(request: Request, term: str = Query("")) -> ResponseDict: response["concept_id"] = concept_id response["symbol"] = symbol response["cased"] = cased - except ServiceWarning as e: + except LookupServiceError as e: response["warnings"] = [str(e)] return response diff --git a/server/curfu/routers/meta.py b/server/curfu/routers/meta.py index db76b24f..7313440f 100644 --- a/server/curfu/routers/meta.py +++ b/server/curfu/routers/meta.py @@ -1,8 +1,7 @@ """Provide service meta information""" +from cool_seq_tool.version import __version__ as cool_seq_tool_version from fastapi import APIRouter - from fusor import __version__ as fusor_version -from cool_seq_tool.version import __version__ as cool_seq_tool_version from curfu.schemas import ServiceInfoResponse from curfu.version import __version__ as curfu_version diff --git a/server/curfu/routers/nomenclature.py b/server/curfu/routers/nomenclature.py index 1c38c4c2..291954b9 100644 --- a/server/curfu/routers/nomenclature.py +++ b/server/curfu/routers/nomenclature.py @@ -1,7 +1,8 @@ """Provide routes for nomenclature generation.""" from typing import Dict -from fastapi import Request, APIRouter, Body +from fastapi import APIRouter, Body, Request +from fusor.exceptions import FUSORParametersException from fusor.models import ( GeneElement, RegulatoryElement, @@ -14,13 +15,11 @@ templated_seq_nomenclature, tx_segment_nomenclature, ) -from fusor.exceptions import FUSORParametersException from pydantic import ValidationError from curfu import logger from curfu.schemas import NomenclatureResponse, ResponseDict - router = APIRouter() @@ -33,8 +32,8 @@ def generate_regulatory_element_nomenclature( request: Request, regulatory_element: Dict = Body() ) -> ResponseDict: - """ - Build regulatory element nomenclature. + """Build regulatory element nomenclature. + \f :param request: the HTTP request context, supplied by FastAPI. Use to access FUSOR and UTA-associated tools. @@ -72,8 +71,8 @@ def generate_regulatory_element_nomenclature( response_model_exclude_none=True, ) def generate_tx_segment_nomenclature(tx_segment: Dict = Body()) -> ResponseDict: - """ - Build transcript segment element nomenclature. + """Build transcript segment element nomenclature. + \f :param request: the HTTP request context, supplied by FastAPI. Use to access FUSOR and UTA-associated tools. diff --git a/server/curfu/routers/utilities.py b/server/curfu/routers/utilities.py index 89a0f6b0..4ffed21a 100644 --- a/server/curfu/routers/utilities.py +++ b/server/curfu/routers/utilities.py @@ -1,22 +1,21 @@ """Provide routes for app utility endpoints""" -from typing import Dict, Optional, List, Any -import tempfile import os +import tempfile from pathlib import Path +from typing import Any, Dict, List, Optional -from fastapi import APIRouter, Request, Query, HTTPException +from fastapi import APIRouter, HTTPException, Query, Request from fastapi.responses import FileResponse +from gene import schemas as gene_schemas from starlette.background import BackgroundTasks -from gene import schemas as GeneSchemas from curfu import logger from curfu.schemas import ( - GetTranscriptsResponse, CoordsUtilsResponse, + GetTranscriptsResponse, SequenceIDResponse, ) -from curfu.sequence_services import get_strand, InvalidInputException - +from curfu.sequence_services import InvalidInputError, get_strand router = APIRouter() @@ -36,7 +35,7 @@ def get_mane_transcripts(request: Request, term: str) -> Dict: :return: Dict containing transcripts if lookup succeeds, or warnings upon failure """ normalized = request.app.state.fusor.gene_normalizer.normalize(term) - if normalized.match_type == GeneSchemas.MatchType.NO_MATCH: + if normalized.match_type == gene_schemas.MatchType.NO_MATCH: return {"warnings": [f"Normalization error: {term}"]} elif not normalized.gene_descriptor.gene_id.lower().startswith("hgnc"): return {"warnings": [f"No HGNC symbol: {term}"]} @@ -157,7 +156,7 @@ async def get_exon_coords( if strand is not None: try: strand_validated = get_strand(strand) - except InvalidInputException: + except InvalidInputError: warnings.append(f"Received invalid strand value: {strand}") else: strand_validated = strand diff --git a/server/curfu/routers/validate.py b/server/curfu/routers/validate.py index d4425c2e..5150c315 100644 --- a/server/curfu/routers/validate.py +++ b/server/curfu/routers/validate.py @@ -1,7 +1,7 @@ """Provide validation endpoint to confirm correctness of fusion object structure.""" from typing import Dict -from fastapi import Body, Request, APIRouter +from fastapi import APIRouter, Body, Request from fusor.exceptions import FUSORParametersException from curfu.schemas import ResponseDict, ValidateFusionResponse diff --git a/server/curfu/schemas.py b/server/curfu/schemas.py index 15971ec3..e714fc7b 100644 --- a/server/curfu/schemas.py +++ b/server/curfu/schemas.py @@ -1,24 +1,23 @@ """Provide schemas for FastAPI responses.""" -from typing import List, Optional, Tuple, Union, Literal, Dict +from typing import Dict, List, Literal, Optional, Tuple, Union -from pydantic import BaseModel, Field, StrictStr, StrictInt, validator, Extra -from ga4gh.vrsatile.pydantic.vrsatile_models import CURIE +from cool_seq_tool.schemas import GenomicData from fusor.models import ( AssayedFusion, CategoricalFusion, + FunctionalDomain, Fusion, - TranscriptSegmentElement, - LinkerElement, - TemplatedSequenceElement, GeneElement, - UnknownGeneElement, + LinkerElement, MultiplePossibleGenesElement, RegulatoryElement, - FunctionalDomain, Strand, + TemplatedSequenceElement, + TranscriptSegmentElement, + UnknownGeneElement, ) -from cool_seq_tool.schemas import GenomicData - +from ga4gh.vrsatile.pydantic.vrsatile_models import CURIE +from pydantic import BaseModel, Extra, Field, StrictInt, StrictStr, validator ResponseWarnings = Optional[List[StrictStr]] diff --git a/server/curfu/sequence_services.py b/server/curfu/sequence_services.py index 996b8a69..b6535b84 100644 --- a/server/curfu/sequence_services.py +++ b/server/curfu/sequence_services.py @@ -5,11 +5,9 @@ logger.setLevel(logging.DEBUG) -class InvalidInputException(Exception): +class InvalidInputError(Exception): """Provide exception for input validation.""" - pass - def get_strand(input: str) -> int: """Validate strand arguments received from client. @@ -22,4 +20,4 @@ def get_strand(input: str) -> int: elif input == "-": return -1 else: - raise InvalidInputException + raise InvalidInputError diff --git a/server/curfu/utils.py b/server/curfu/utils.py index b2dabaaf..3634bfff 100644 --- a/server/curfu/utils.py +++ b/server/curfu/utils.py @@ -1,15 +1,14 @@ """Miscellaneous helper functions.""" import os from pathlib import Path -from typing import TypeVar, List +from typing import List, TypeVar import boto3 from boto3.exceptions import ResourceLoadException -from botocore.exceptions import ClientError - from botocore.config import Config +from botocore.exceptions import ClientError -from curfu import logger, APP_ROOT +from curfu import APP_ROOT, logger ObjectSummary = TypeVar("ObjectSummary") @@ -58,9 +57,9 @@ def download_s3_file(bucket_object: ObjectSummary) -> Path: def get_latest_data_file(file_prefix: str, local_files: List[Path]) -> Path: - """ - Get path to latest version of given data file. Download from S3 if not + """Get path to latest version of given data file. Download from S3 if not available locally. + :param file_prefix: leading pattern in filename (eg `gene_aliases`) :param local_files: local files matching pattern :return: path to up-to-date file @@ -74,8 +73,8 @@ def get_latest_data_file(file_prefix: str, local_files: List[Path]) -> Path: def get_data_file(filename_prefix: str) -> Path: - """ - Acquire most recent version of static data file. Download from S3 if not available locally. + """Acquire most recent version of static data file. Download from S3 if not available locally. + :param filename_prefix: leading text of filename, eg `gene_aliases_suggest`. Should not include filetype or date information. :return: Path to acquired file. diff --git a/server/pyproject.toml b/server/pyproject.toml new file mode 100644 index 00000000..e50d642d --- /dev/null +++ b/server/pyproject.toml @@ -0,0 +1,43 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta:__legacy__" + +[tool.black] +line-length = 88 + +[tool.ruff] +# pycodestyle (E, W) +# Pyflakes (F) +# flake8-annotations (ANN) +# flake8-quotes (Q) +# pydocstyle (D) +# pep8-naming (N) +# isort (I) +select = ["E", "W", "F", "ANN", "Q", "D", "N", "I"] + +fixable = ["I"] + +# D205 - blank-line-after-summary +# D400 - ends-in-period +# D415 - ends-in-punctuation +# ANN101 - missing-type-self +# ANN003 - missing-type-kwargs +# E501 - line-too-long +ignore = ["D205", "D400", "D415", "ANN101", "ANN003", "E501"] + +[tool.ruff.flake8-quotes] +docstring-quotes = "double" + +[tool.ruff.per-file-ignores] +# ANN001 - missing-type-function-argument +# ANN2 - missing-return-type +# ANN102 - missing-type-cls +# N805 - invalid-first-argument-name-for-method +# F821 - undefined-name +# F401 - unused-import +"tests/*" = ["ANN001", "ANN2", "ANN102"] +"setup.py" = ["F821"] +"*__init__.py" = ["F401"] +"curfu/schemas.py" = ["ANN201", "N805", "ANN001"] +"curfu/routers/*" = ["D301"] +"curfu/cli.py" = ["D301"] diff --git a/server/setup.cfg b/server/setup.cfg index 98138733..18b8c7fe 100644 --- a/server/setup.cfg +++ b/server/setup.cfg @@ -33,8 +33,7 @@ tests = dev = psycopg2-binary - flake8 - flake8-docstrings + ruff black pre-commit pydantic-to-typescript diff --git a/server/tests/conftest.py b/server/tests/conftest.py index c1d4a340..46c481b8 100644 --- a/server/tests/conftest.py +++ b/server/tests/conftest.py @@ -1,11 +1,11 @@ """Provide core fixtures for testing Flask functions.""" +import asyncio from typing import Callable, Dict import pytest -import asyncio from httpx import AsyncClient -from curfu.main import app, start_fusor, get_gene_services, get_domain_services +from curfu.main import app, get_domain_services, get_gene_services, start_fusor @pytest.fixture(scope="session") diff --git a/server/tests/integration/test_main.py b/server/tests/integration/test_main.py index f220fe6b..0a692e2d 100644 --- a/server/tests/integration/test_main.py +++ b/server/tests/integration/test_main.py @@ -15,10 +15,10 @@ async def test_service_info(async_client): assert response.status_code == 200 response_json = response.json() assert response_json["warnings"] == [] - SEMVER_PATTERN = r"^(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$" # noqa: E501 - assert re.match(SEMVER_PATTERN, response_json["curfu_version"]) - assert re.match(SEMVER_PATTERN, response_json["fusor_version"]) - assert re.match(SEMVER_PATTERN, response_json["cool_seq_tool_version"]) + semver_pattern = r"^(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$" # noqa: E501 + assert re.match(semver_pattern, response_json["curfu_version"]) + assert re.match(semver_pattern, response_json["fusor_version"]) + assert re.match(semver_pattern, response_json["cool_seq_tool_version"]) # not sure if I want to include vrs-python # also its current version number isn't legal semver # assert re.match( diff --git a/server/tests/integration/test_nomenclature.py b/server/tests/integration/test_nomenclature.py index 5fc39bfe..ef78fca0 100644 --- a/server/tests/integration/test_nomenclature.py +++ b/server/tests/integration/test_nomenclature.py @@ -2,8 +2,8 @@ from typing import Dict import pytest -from httpx import AsyncClient from fusor.examples import bcr_abl1 +from httpx import AsyncClient @pytest.fixture(scope="module") diff --git a/server/tests/integration/test_utilities.py b/server/tests/integration/test_utilities.py index 3652b4ba..e0d3d0f6 100644 --- a/server/tests/integration/test_utilities.py +++ b/server/tests/integration/test_utilities.py @@ -1,9 +1,8 @@ """Test end-to-end correctness of utility routes.""" -from typing import Dict, Callable +from typing import Callable, Dict import pytest - response_callback_type = Callable[[Dict, Dict], None] diff --git a/server/tests/integration/test_validate.py b/server/tests/integration/test_validate.py index f1f445d7..b15605d0 100644 --- a/server/tests/integration/test_validate.py +++ b/server/tests/integration/test_validate.py @@ -202,7 +202,8 @@ def wrong_type_fusion(): async def check_validated_fusion_response(client, fixture: Dict, case_name: str): """Run basic checks on fusion validation response. - TODO + Todo: + ---- * FUSOR should provide a "fusion equality" utility function -- incorporate it here when that's done """