Skip to content
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

Refactor database session handling to async with retry logic, remove middleware #152

Merged
merged 66 commits into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
c035ad5
handle missing genotypes
ahdamin Sep 19, 2024
e244f67
add pool_recycle argument
ahdamin Sep 20, 2024
a408848
revert changes
ahdamin Sep 23, 2024
4aef046
rollback before closing active session
ahdamin Sep 24, 2024
eb7aa28
rollback mw active session before closing
ahdamin Sep 24, 2024
5d3d731
use GenotypeDBError
ahdamin Sep 26, 2024
687f302
fix finally block
ahdamin Sep 26, 2024
ba58eae
check session status when closing
ahdamin Sep 26, 2024
3f0e23d
Handle null genotypes
ahdamin Sep 26, 2024
30ff742
Add error handling middleware
ahdamin Sep 26, 2024
dba577c
revert
ahdamin Sep 26, 2024
62141a9
return response for dirty sessions
ahdamin Sep 27, 2024
648b94e
Add detailed logging and session checks in DBSessionMiddleware
ahdamin Sep 27, 2024
07e66cf
rollback in case of PendingRollbackError
ahdamin Sep 30, 2024
a83e8a9
add pool_recycle argument
ahdamin Sep 30, 2024
c87fc99
replace deprecated on_event with lifespan
ahdamin Oct 2, 2024
cafb128
remove deprecated openapi_prefix
ahdamin Oct 2, 2024
cd3cdf6
add packages: aiomysql and pytest-asyncio
ahdamin Oct 7, 2024
56cc256
Refactor DB to async MySQL with retries
ahdamin Oct 7, 2024
bbd7b0a
remove deprecated: openapi_prefix
ahdamin Oct 7, 2024
485daee
Add async support to endpoints
ahdamin Oct 7, 2024
ea24e2f
Refactor BaseHandler for async queries
ahdamin Oct 7, 2024
5e642f3
Refactor crud handlers for async queries
ahdamin Oct 7, 2024
1efeb54
Add async support to services
ahdamin Oct 7, 2024
a2d96d4
Refactor test fixtures, helpers, and tests for async support
ahdamin Oct 7, 2024
5df584c
Add async handling to user authentication
ahdamin Oct 7, 2024
f246d49
Consolidate SQLAlchemy model imports
ahdamin Oct 7, 2024
27a792a
remove duplicated functions
ahdamin Oct 7, 2024
422737c
Delete this unreachable code
ahdamin Oct 7, 2024
ad653de
Merge branch 'main' into fix-lost-db-connection
ahdamin Oct 7, 2024
f34c650
Fix poetry conflict
ahdamin Oct 7, 2024
3bb85ef
Reformat
ahdamin Oct 7, 2024
34476ff
Reformat
ahdamin Oct 7, 2024
8066632
Add eager loading for user plates in get_user_by_id
ahdamin Oct 7, 2024
3d09f8e
Add exception handler for OperationalError
ahdamin Oct 8, 2024
4149b3b
Remove comment
ahdamin Oct 8, 2024
9348379
Add retry logic with tenacity
ahdamin Oct 8, 2024
42fcf3e
Remove commented-out code
ahdamin Oct 8, 2024
993ff00
Update to pydantic v2 field validators
ahdamin Oct 8, 2024
6866fa3
revert pydantic updates
ahdamin Oct 8, 2024
9433cd4
Add join condition for sample and analysis
ahdamin Oct 8, 2024
28948bc
Organize formats
ahdamin Oct 8, 2024
677f18a
Remove commented out lines
ahdamin Oct 8, 2024
67b70d4
Add token validation checks
ahdamin Oct 9, 2024
0f66501
Remove redundant line
ahdamin Oct 9, 2024
6fa777f
Remove unused local variable and imports
ahdamin Oct 9, 2024
18db6ea
use AsyncGenerator for store instance
ahdamin Oct 9, 2024
09f3d36
Remove redundant line
ahdamin Oct 9, 2024
dcb8078
Clean up comments
ahdamin Oct 9, 2024
3a230f4
Simplify async query execution in ReadHandler
ahdamin Oct 9, 2024
4573b29
Add async utility methods for row and scalar query results
ahdamin Oct 10, 2024
2b58ea8
Use fetch_one_or_none from CreateHandler
ahdamin Oct 10, 2024
2e1ab2c
use select and fetch
ahdamin Oct 10, 2024
84d1633
Use fetch_one_or_none
ahdamin Oct 10, 2024
f376ba5
Add retry logic and session health checks in database session
ahdamin Oct 10, 2024
96abb8f
Remove retry logic from Store session creation
ahdamin Oct 10, 2024
530147e
use select and fetch_all_rows
ahdamin Oct 10, 2024
96fea87
Use select and fetch_all_rows
ahdamin Oct 10, 2024
b4c1715
Use select and fetch_all_rows
ahdamin Oct 10, 2024
b334139
Use select and fetch_all_rows
ahdamin Oct 10, 2024
7a6f809
Add query generation methods and use BaseHandler async fetch methods
ahdamin Oct 10, 2024
aad7b0d
Add: _get_samples_with_analyses
ahdamin Oct 11, 2024
8fd9f21
Use: select & async fetch functions from BaseHandler
ahdamin Oct 11, 2024
aebc195
Remove unnecessary filter_functions
ahdamin Oct 11, 2024
e356387
fix: missing parentheses in query
ahdamin Oct 11, 2024
43d8681
Make delete awaitable
ahdamin Oct 11, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 30 additions & 19 deletions genotype_api/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,46 @@

"""

from fastapi import FastAPI, status, Request
from fastapi.responses import JSONResponse
import logging
from contextlib import asynccontextmanager

from fastapi import FastAPI, Request, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from sqlalchemy.exc import NoResultFound, OperationalError

from genotype_api.api.middleware import DBSessionMiddleware
from genotype_api.config import security_settings, settings
from genotype_api.database.database import create_all_tables, initialise_database
from genotype_api.api.endpoints import samples, snps, users, plates, analyses
from sqlalchemy.exc import NoResultFound
from genotype_api.api.endpoints import analyses, plates, samples, snps, users
from genotype_api.config import security_settings

app = FastAPI(
root_path=security_settings.api_root_path,
root_path_in_servers=True,
openapi_prefix=security_settings.api_root_path,
)
LOG = logging.getLogger(__name__)


@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup actions, like connecting to the database
LOG.debug("Starting up...")
yield # This is important, it must yield control
# Shutdown actions, like closing the database connection
LOG.debug("Shutting down...")


app = FastAPI(lifespan=lifespan, root_path=security_settings.api_root_path)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.add_middleware(DBSessionMiddleware)


@app.exception_handler(OperationalError)
async def db_connection_exception_handler(request: Request, exc: OperationalError):
LOG.error(f"Database connection error: {exc}")
return JSONResponse(
content={"detail": "Database connection error. Please try again later."},
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
)


@app.exception_handler(NoResultFound)
Expand Down Expand Up @@ -72,9 +89,3 @@ def welcome():
tags=["analyses"],
responses={status.HTTP_404_NOT_FOUND: {"description": "Not found"}},
)


@app.on_event("startup")
def on_startup():
initialise_database(settings.db_uri)
create_all_tables()
24 changes: 10 additions & 14 deletions genotype_api/api/endpoints/analyses.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,15 @@

from http import HTTPStatus

from fastapi import APIRouter, Depends, File, Query, UploadFile, status, HTTPException
from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile, status
from fastapi.responses import JSONResponse

from genotype_api.database.store import Store, get_store
from genotype_api.dto.analysis import AnalysisResponse
from genotype_api.dto.user import CurrentUser

from genotype_api.exceptions import AnalysisNotFoundError
from genotype_api.security import get_active_user
from genotype_api.services.endpoint_services.analysis_service import (
AnalysisService,
)

from genotype_api.services.endpoint_services.analysis_service import AnalysisService

router = APIRouter()

Expand All @@ -24,14 +20,14 @@ def get_analysis_service(store: Store = Depends(get_store)) -> AnalysisService:


@router.get("/{analysis_id}", response_model=AnalysisResponse)
def read_analysis(
async def read_analysis(
analysis_id: int,
analysis_service: AnalysisService = Depends(get_analysis_service),
current_user: CurrentUser = Depends(get_active_user),
):
"""Return analysis."""
try:
return analysis_service.get_analysis(analysis_id)
return await analysis_service.get_analysis(analysis_id)
except AnalysisNotFoundError:
raise HTTPException(
detail=f"Could not find analysis with id: {analysis_id}",
Expand All @@ -40,15 +36,15 @@ def read_analysis(


@router.get("/", response_model=list[AnalysisResponse], response_model_exclude={"genotypes"})
def read_analyses(
async def read_analyses(
skip: int = 0,
limit: int = Query(default=100, lte=100),
analysis_service: AnalysisService = Depends(get_analysis_service),
current_user: CurrentUser = Depends(get_active_user),
):
"""Return all analyses."""
try:
return analysis_service.get_analyses(skip=skip, limit=limit)
return await analysis_service.get_analyses(skip=skip, limit=limit)
except AnalysisNotFoundError:
raise HTTPException(
detail="Could not fetch analyses from backend.",
Expand All @@ -57,14 +53,14 @@ def read_analyses(


@router.delete("/{analysis_id}")
def delete_analysis(
async def delete_analysis(
analysis_id: int,
analysis_service: AnalysisService = Depends(get_analysis_service),
current_user: CurrentUser = Depends(get_active_user),
):
"""Delete analysis based on analysis id."""
try:
analysis_service.delete_analysis(analysis_id)
await analysis_service.delete_analysis(analysis_id)
except AnalysisNotFoundError:
raise HTTPException(
detail=f"Could not find analysis with id: {analysis_id}",
Expand All @@ -76,12 +72,12 @@ def delete_analysis(
@router.post(
"/sequence", response_model=list[AnalysisResponse], response_model_exclude={"genotypes"}
)
def upload_sequence_analysis(
async def upload_sequence_analysis(
file: UploadFile = File(...),
analysis_service: AnalysisService = Depends(get_analysis_service),
current_user: CurrentUser = Depends(get_active_user),
):
"""Reading VCF file, creating and uploading sequence analyses and sample objects to the database."""

analyses: list[AnalysisResponse] = analysis_service.get_upload_sequence_analyses(file)
analyses: list[AnalysisResponse] = await analysis_service.get_upload_sequence_analyses(file)
return analyses
27 changes: 13 additions & 14 deletions genotype_api/api/endpoints/plates.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,18 @@

from http import HTTPStatus
from typing import Literal
from fastapi import APIRouter, Depends, File, Query, UploadFile, status, HTTPException
from fastapi.responses import JSONResponse
from genotype_api.database.filter_models.plate_models import PlateOrderParams

from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile, status
from fastapi.responses import JSONResponse

from genotype_api.database.filter_models.plate_models import PlateOrderParams
from genotype_api.database.store import Store, get_store
from genotype_api.dto.plate import PlateResponse
from genotype_api.dto.user import CurrentUser
from genotype_api.exceptions import PlateNotFoundError, PlateExistsError
from genotype_api.exceptions import PlateExistsError, PlateNotFoundError
from genotype_api.security import get_active_user
from genotype_api.services.endpoint_services.plate_service import PlateService


router = APIRouter()


Expand All @@ -25,14 +24,14 @@ def get_plate_service(store: Store = Depends(get_store)) -> PlateService:
@router.post(
"/plate",
)
def upload_plate(
async def upload_plate(
file: UploadFile = File(...),
plate_service: PlateService = Depends(get_plate_service),
current_user: CurrentUser = Depends(get_active_user),
):

try:
plate_service.upload_plate(file)
await plate_service.upload_plate(file)
except PlateExistsError:
raise HTTPException(
detail="Plate already exists in the database.", status_code=HTTPStatus.BAD_REQUEST
Expand All @@ -45,7 +44,7 @@ def upload_plate(
response_model=PlateResponse,
response_model_exclude={"analyses", "user", "plate_status_counts"},
)
def sign_off_plate(
async def sign_off_plate(
plate_id: int,
method_document: str = Query(...),
method_version: str = Query(...),
Expand All @@ -57,7 +56,7 @@ def sign_off_plate(
Add Depends with current user
"""

return plate_service.update_plate_sign_off(
return await plate_service.update_plate_sign_off(
plate_id=plate_id,
user_email=current_user.email,
method_version=method_version,
Expand Down Expand Up @@ -87,14 +86,14 @@ def sign_off_plate(
}
},
)
def read_plate(
async def read_plate(
plate_id: int,
plate_service: PlateService = Depends(get_plate_service),
current_user: CurrentUser = Depends(get_active_user),
):
"""Display information about a plate."""
try:
return plate_service.get_plate(plate_id=plate_id)
return await plate_service.get_plate(plate_id=plate_id)
except PlateNotFoundError:
raise HTTPException(
detail=f"Could not find plate with id: {plate_id}", status_code=HTTPStatus.BAD_REQUEST
Expand All @@ -120,22 +119,22 @@ async def read_plates(
order_by=order_by, skip=skip, limit=limit, sort_order=sort_order
)
try:
return plate_service.get_plates(order_params=order_params)
return await plate_service.get_plates(order_params=order_params)
except PlateNotFoundError:
raise HTTPException(
detail="Could not fetch plates from backend.", status_code=HTTPStatus.BAD_REQUEST
)


@router.delete("/{plate_id}")
def delete_plate(
async def delete_plate(
plate_id: int,
plate_service: PlateService = Depends(get_plate_service),
current_user: CurrentUser = Depends(get_active_user),
):
"""Delete plate."""
try:
analysis_ids = plate_service.delete_plate(plate_id)
analysis_ids = await plate_service.delete_plate(plate_id)
return JSONResponse(
f"Deleted plate: {plate_id} and analyses: {analysis_ids}",
status_code=status.HTTP_200_OK,
Expand Down
Loading
Loading