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

feat: incorporate api commons exception handlers #175

Merged
merged 5 commits into from
Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 5 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/sbl_filing_api/entities/repos/submission_repo.py
Expand Up @@ -41,7 +41,7 @@ async def get_submissions(session: AsyncSession, lei: str = None, filing_period:
return await query_helper(session, SubmissionDAO, filing=filing_id)


async def get_latest_submission(session: AsyncSession, lei: str, filing_period: str) -> List[SubmissionDAO]:
async def get_latest_submission(session: AsyncSession, lei: str, filing_period: str) -> SubmissionDAO | None:
filing = await get_filing(session, lei=lei, filing_period=filing_period)
stmt = select(SubmissionDAO).filter_by(filing=filing.id).order_by(desc(SubmissionDAO.submission_time)).limit(1)
return await session.scalar(stmt)
Expand Down
15 changes: 15 additions & 0 deletions src/sbl_filing_api/main.py
Expand Up @@ -6,10 +6,19 @@
from fastapi import FastAPI
from fastapi.security import OAuth2AuthorizationCodeBearer
from fastapi.middleware.cors import CORSMiddleware
from fastapi.exceptions import RequestValidationError
from starlette.middleware.authentication import AuthenticationMiddleware
from starlette.exceptions import HTTPException

from regtech_api_commons.oauth2.oauth2_backend import BearerTokenAuthBackend
from regtech_api_commons.oauth2.oauth2_admin import OAuth2Admin
from regtech_api_commons.api.exceptions import RegTechHttpException
from regtech_api_commons.api.exception_handlers import (
regtech_http_exception_handler,
request_validation_error_handler,
http_exception_handler,
general_exception_handler,
)

from sbl_filing_api.routers.filing import router as filing_router

Expand Down Expand Up @@ -42,6 +51,12 @@ def run_migrations():
app = FastAPI(lifespan=lifespan)


app.add_exception_handler(RegTechHttpException, regtech_http_exception_handler) # type: ignore[type-arg] # noqa: E501
app.add_exception_handler(RequestValidationError, request_validation_error_handler) # type: ignore[type-arg] # noqa: E501
app.add_exception_handler(HTTPException, http_exception_handler) # type: ignore[type-arg] # noqa: E501
app.add_exception_handler(Exception, general_exception_handler) # type: ignore[type-arg] # noqa: E501


token_bearer = OAuth2AuthorizationCodeBearer(
authorizationUrl=kc_settings.auth_url.unicode_string(), tokenUrl=kc_settings.token_url.unicode_string()
)
Expand Down
13 changes: 9 additions & 4 deletions src/sbl_filing_api/routers/dependencies.py
Expand Up @@ -3,21 +3,26 @@

from sbl_filing_api.config import settings
from http import HTTPStatus
from fastapi import Request, HTTPException
from fastapi import Request
from regtech_api_commons.api.exceptions import RegTechHttpException


def verify_lei(request: Request, lei: str) -> None:
res = httpx.get(settings.user_fi_api_url + lei, headers={"authorization": request.headers["authorization"]})
lei_obj = res.json()
if not lei_obj["is_active"]:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail=f"LEI {lei} is in an inactive state.")
raise RegTechHttpException(
status_code=HTTPStatus.FORBIDDEN, name="Request Forbidden", detail=f"LEI {lei} is in an inactive state."
)


def verify_user_lei_relation(request: Request, lei: str = None) -> None:
if os.getenv("ENV", "LOCAL") != "LOCAL" and lei:
if request.user.is_authenticated:
institutions = request.user.institutions
if lei not in institutions:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN, detail=f"LEI {lei} is not associated with the user."
raise RegTechHttpException(
status_code=HTTPStatus.FORBIDDEN,
name="Request Forbidden",
detail=f"LEI {lei} is not associated with the user.",
)
84 changes: 45 additions & 39 deletions src/sbl_filing_api/routers/filing.py
@@ -1,8 +1,7 @@
import logging

from fastapi import Depends, Request, UploadFile, BackgroundTasks, status, HTTPException
from fastapi import Depends, Request, UploadFile, BackgroundTasks, status
from fastapi.responses import JSONResponse, FileResponse
from regtech_api_commons.api.router_wrapper import Router
from regtech_api_commons.api.exceptions import RegTechHttpException
from sbl_filing_api.entities.models.model_enums import UserActionType
from sbl_filing_api.services import submission_processor
from typing import Annotated, List
Expand Down Expand Up @@ -33,7 +32,6 @@ async def set_db(request: Request, session: Annotated[AsyncSession, Depends(get_


router = Router(dependencies=[Depends(set_db), Depends(verify_user_lei_relation)])
logger = logging.getLogger(__name__)


@router.get("/periods", response_model=List[FilingPeriodDTO])
Expand Down Expand Up @@ -76,15 +74,17 @@ async def post_filing(request: Request, lei: str, period_code: str):
try:
return await repo.create_new_filing(request.state.db_session, lei, period_code, creator_id=creator.id)
except IntegrityError:
raise HTTPException(
raise RegTechHttpException(
status_code=status.HTTP_409_CONFLICT,
name="Filing Creation Conflict",
detail=f"Filing already exists for Filing Period {period_code} and LEI {lei}",
)

else:
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content=f"The period ({period_code}) does not exist, therefore a Filing can not be created for this period.",
raise RegTechHttpException(
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe more of a stylistic thing, but throwing an exception for non-200 responses, and let exception handlers deal with the rest looks better in my eyes; thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've seen both ways, and I don't have a preference but sticking with one way is ideal. I know there are a couple of places where we're raising and HttpException but others we're returning JSON responses. Philosophically non-200 responses are errors/exceptions/deviations from happy-path so raising versus returning makes a lot of sense. I say we go with that as a standard approach across the board.

status_code=status.HTTP_404_NOT_FOUND,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@billhimmelsbach @shindigira @meissadia not sure how far along front end is with the status code; is it ok to switch most of the 422s in the filing API which are filing doesn't exist, submission doesn't exist, or filing period doesn't exist to 404s? It makes more sense in my mind; since in this example a filing period doesn't exist, therefore a not found error.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am going to take your updates here and build a wiki page with response codes based on when and we can discuss those during devchat?

name="Filing Period Not Found",
detail=f"The period ({period_code}) does not exist, therefore a Filing can not be created for this period.",
)


Expand All @@ -93,20 +93,23 @@ async def post_filing(request: Request, lei: str, period_code: str):
async def sign_filing(request: Request, lei: str, period_code: str):
filing = await repo.get_filing(request.state.db_session, lei, period_code)
if not filing:
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content=f"There is no Filing for LEI {lei} in period {period_code}, unable to sign a non-existent Filing.",
raise RegTechHttpException(
status_code=status.HTTP_404_NOT_FOUND,
name="Filing Not Found",
detail=f"There is no Filing for LEI {lei} in period {period_code}, unable to sign a non-existent Filing.",
)
latest_sub = await repo.get_latest_submission(request.state.db_session, lei, period_code)
if not latest_sub or latest_sub.state != SubmissionState.SUBMISSION_ACCEPTED:
return JSONResponse(
raise RegTechHttpException(
status_code=status.HTTP_403_FORBIDDEN,
content=f"Cannot sign filing. Filing for {lei} for period {period_code} does not have a latest submission the SUBMISSION_ACCEPTED state.",
name="Filing Action Forbidden",
detail=f"Cannot sign filing. Filing for {lei} for period {period_code} does not have a latest submission the SUBMISSION_ACCEPTED state.",
)
if not filing.contact_info:
return JSONResponse(
raise RegTechHttpException(
status_code=status.HTTP_403_FORBIDDEN,
content=f"Cannot sign filing. Filing for {lei} for period {period_code} does not have contact info defined.",
name="Filing Action Forbidden",
detail=f"Cannot sign filing. Filing for {lei} for period {period_code} does not have contact info defined.",
)
"""
if not filing.institution_snapshot_id:
Expand Down Expand Up @@ -138,9 +141,10 @@ async def upload_file(

filing = await repo.get_filing(request.state.db_session, lei, period_code)
if not filing:
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content=f"There is no Filing for LEI {lei} in period {period_code}, unable to submit file.",
raise RegTechHttpException(
status_code=status.HTTP_404_NOT_FOUND,
name="Filing Not Found",
detail=f"There is no Filing for LEI {lei} in period {period_code}, unable to submit file.",
)
try:
submitter = await repo.add_user_action(
Expand All @@ -159,25 +163,23 @@ async def upload_file(
submission.state = SubmissionState.SUBMISSION_UPLOADED
submission = await repo.update_submission(submission)
except Exception as e:
logger.error(
f"Error while trying to process Submission {submission.id}", e, exec_info=True, stack_info=True
)
submission.state = SubmissionState.UPLOAD_FAILED
submission = await repo.update_submission(submission)
return JSONResponse(
raise RegTechHttpException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content=f"{e}",
)
name="Submission Unprocessable",
detail=f"Error while trying to process Submission {submission.id}",
) from e
background_tasks.add_task(submission_processor.validation_monitor, period_code, lei, submission, content)

return submission

except Exception as e:
logger.error("Error while trying to process SUBMIT User Action", e, exec_info=True, stack_info=True)
return JSONResponse(
raise RegTechHttpException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content=f"{e}",
)
name="Submission Unprocessable",
detail="Error while trying to process SUBMIT User Action",
) from e


@router.get("/institutions/{lei}/filings/{period_code}/submissions", response_model=List[SubmissionDTO])
Expand Down Expand Up @@ -209,17 +211,19 @@ async def get_submission(request: Request, id: int):
async def accept_submission(request: Request, id: int, lei: str, period_code: str):
submission = await repo.get_submission(request.state.db_session, id)
if not submission:
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content=f"Submission ID {id} does not exist, cannot accept a non-existing submission.",
raise RegTechHttpException(
status_code=status.HTTP_404_NOT_FOUND,
name="Submission Not Found",
detail=f"Submission ID {id} does not exist, cannot accept a non-existing submission.",
)
if (
submission.state != SubmissionState.VALIDATION_SUCCESSFUL
and submission.state != SubmissionState.VALIDATION_WITH_WARNINGS
):
return JSONResponse(
raise RegTechHttpException(
status_code=status.HTTP_403_FORBIDDEN,
content=f"Submission {id} for LEI {lei} in filing period {period_code} is not in an acceptable state. Submissions must be validated successfully or with only warnings to be accepted.",
name="Submission Action Forbidden",
detail=f"Submission {id} for LEI {lei} in filing period {period_code} is not in an acceptable state. Submissions must be validated successfully or with only warnings to be accepted.",
)

accepter = await repo.add_user_action(
Expand All @@ -243,9 +247,10 @@ async def put_institution_snapshot(request: Request, lei: str, period_code: str,
if result:
result.institution_snapshot_id = update_value.institution_snapshot_id
return await repo.upsert_filing(request.state.db_session, result)
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content=f"A Filing for the LEI ({lei}) and period ({period_code}) that was attempted to be updated does not exist.",
raise RegTechHttpException(
status_code=status.HTTP_404_NOT_FOUND,
name="Filing Not Found",
detail=f"A Filing for the LEI ({lei}) and period ({period_code}) that was attempted to be updated does not exist.",
)


Expand All @@ -270,9 +275,10 @@ async def put_contact_info(request: Request, lei: str, period_code: str, contact
result = await repo.get_filing(request.state.db_session, lei, period_code)
if result:
return await repo.update_contact_info(request.state.db_session, lei, period_code, contact_info)
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content=f"A Filing for the LEI ({lei}) and period ({period_code}) that was attempted to be updated does not exist.",
raise RegTechHttpException(
status_code=status.HTTP_404_NOT_FOUND,
name="Filing Not Found",
detail=f"A Filing for the LEI ({lei}) and period ({period_code}) that was attempted to be updated does not exist.",
)


Expand Down
18 changes: 11 additions & 7 deletions src/sbl_filing_api/services/submission_processor.py
Expand Up @@ -11,10 +11,10 @@
from sbl_filing_api.entities.models.dao import SubmissionDAO, SubmissionState
from sbl_filing_api.entities.repos.submission_repo import update_submission
from http import HTTPStatus
from fastapi import HTTPException
import logging
from fsspec import AbstractFileSystem, filesystem
from sbl_filing_api.config import settings
from regtech_api_commons.api.exceptions import RegTechHttpException

log = logging.getLogger(__name__)

Expand All @@ -41,16 +41,18 @@ async def validation_monitor(period_code: str, lei: str, submission: SubmissionD
def validate_file_processable(file: UploadFile) -> None:
extension = file.filename.split(".")[-1].lower()
if file.content_type != settings.submission_file_type or extension != settings.submission_file_extension:
raise HTTPException(
raise RegTechHttpException(
status_code=HTTPStatus.UNSUPPORTED_MEDIA_TYPE,
name="Unsupported File Type",
detail=(
f"Only {settings.submission_file_type} file type with extension {settings.submission_file_extension} is supported; "
f'submitted file is "{file.content_type}" with "{extension}" extension',
),
)
if file.size > settings.submission_file_size:
raise HTTPException(
raise RegTechHttpException(
status_code=HTTPStatus.REQUEST_ENTITY_TOO_LARGE,
name="File Too Large",
detail=f"Uploaded file size of {file.size} bytes exceeds the limit of {settings.submission_file_size} bytes.",
)

Expand All @@ -65,8 +67,9 @@ async def upload_to_storage(period_code: str, lei: str, file_identifier: str, co
) as f:
f.write(content)
except Exception as e:
log.error("Failed to upload file", e, exc_info=True, stack_info=True)
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Failed to upload file")
raise RegTechHttpException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, name="Upload Failure", detail="Failed to upload file"
) from e


async def get_from_storage(period_code: str, lei: str, file_identifier: str, extension: str = "csv"):
Expand All @@ -76,8 +79,9 @@ async def get_from_storage(period_code: str, lei: str, file_identifier: str, ext
with fs.open(file_path, "r") as f:
return f.name
except Exception as e:
log.error(f"Failed to read file {file_path}:", e, exc_info=True, stack_info=True)
raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Failed to read file.")
raise RegTechHttpException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, name="Download Failure", detail="Failed to read file."
) from e


async def validate_and_update_submission(period_code: str, lei: str, submission: SubmissionDAO, content: bytes):
Expand Down