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

#20 Add v3 Notification Route Handler #31

Merged
merged 17 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
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
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* @department-of-veterans-affairs/va-notify-write
6 changes: 4 additions & 2 deletions .github/workflows/test_suite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ on:
env:
PYTHONDONTWRITEBYTECODE: 1
PORT: 6011
POETRY_ARGS: "--with static_tools"
POETRY_ARGS: "--with test,static_tools"
POETRY_HOME: "/opt/poetry"
POETRY_VIRTUALENVS_IN_PROJECT: 1
POETRY_NO_INTERACTION: 1
Expand Down Expand Up @@ -51,4 +51,6 @@ jobs:

- name: Run Tests
run: |
echo "This is where the tests would be if we had any!"
# Set path to poetry venv
export PATH="$PWD/.venv/bin:$PATH"
pytest
23 changes: 23 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
repos:
- repo: https://github.com/thoughtworks/talisman
rev: "v1.32.0"
hooks:
- id: talisman-commit
entry: cmd --githook pre-commit
- repo: https://github.com/PyCQA/bandit
rev: "1.7.9"
hooks:
- id: bandit
args: [-c, pyproject.toml, -r, -l]
additional_dependencies: ["bandit[toml]"]
exclude: 'tests/*'
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: "v0.6.4"
hooks:
# Run the linter.
- id: ruff
args: [--fix]
exclude: 'tests/*'
# Run the formatter.
- id: ruff-format
4 changes: 4 additions & 0 deletions .talismanrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
fileignoreconfig:
- filename: poetry.lock
checksum: 377e5f825a843fa6b6b6fb4f1448b2f306d2ccee1ae3db42e72a82af9ddd1bf8
version: ""
1 change: 1 addition & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""."""
7 changes: 5 additions & 2 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

from fastapi import FastAPI

from app.v3.notifications.rest import notification_router

app = FastAPI()
app.include_router(notification_router)


@app.get("/")
@app.get('/')
def simple_route() -> dict[str, str]:
"""Return a hello world.

Expand All @@ -14,4 +17,4 @@ def simple_route() -> dict[str, str]:
dict[str, str]: Hello World

"""
return {"Hello": "World"}
return {'Hello': 'World'}
1 change: 1 addition & 0 deletions app/v3/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""."""
1 change: 1 addition & 0 deletions app/v3/notifications/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""."""
102 changes: 102 additions & 0 deletions app/v3/notifications/rest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"""All endpoints for the v3/notifications route."""

import logging
from datetime import datetime, timezone
from time import monotonic
from typing import Any, Callable, Coroutine
from uuid import uuid4

from fastapi import APIRouter, HTTPException, Request, status
from fastapi.exceptions import RequestValidationError
from fastapi.responses import Response
from fastapi.routing import APIRoute
from pydantic import UUID4

from app.v3.notifications.route_schema import NotificationSingleRequest, NotificationSingleResponse

RESPONSE_400 = 'Request body failed validation'
RESPONSE_404 = 'Not found'
RESPONSE_500 = 'Unhandled VA Notify exception'

logger = logging.getLogger('uvicorn.default')


class NotificationRoute(APIRoute):
"""Exception and logging handling."""

def get_route_handler(self) -> Callable[[Request], Coroutine[Any, Any, Response]]:
"""Override default handler.

Returns
-------
Callable: the route handler

"""
original_route_handler = super().get_route_handler()

async def custom_route_handler(request: Request) -> Response:
status_code = None
try:
start = monotonic()
resp = await original_route_handler(request)
status_code = resp.status_code
return resp
except RequestValidationError as exc:
status_code = 400
logger.warning('Request: %s failed validation: %s', request, exc.errors())
raise HTTPException(400, f'{RESPONSE_400} - {exc}')
except Exception as exc:
status_code = 500
logger.exception('%s: %s', RESPONSE_500, type(exc).__name__)
raise HTTPException(status_code, RESPONSE_500)
finally:
logger.info('%s %s %s %ss', request.method, request.url, status_code, f'{(monotonic() - start):6f}')

return custom_route_handler


# https://fastapi.tiangolo.com/reference/apirouter/
notification_router = APIRouter(
prefix='/v3/notifications',
tags=['v3 Notification Endpoints'],
responses={404: {'description': RESPONSE_404}},
route_class=NotificationRoute,
)


@notification_router.get('/{notification_id}', status_code=status.HTTP_200_OK)
async def get_notification(notification_id: UUID4) -> UUID4:
"""Get a notification.

Args:
----
notification_id (UUID4): The notification to get

Returns:
-------
UUID4: The notification object

"""
return notification_id


@notification_router.post('/', status_code=status.HTTP_202_ACCEPTED)
async def create_notification(request: NotificationSingleRequest) -> NotificationSingleResponse:
"""Return app status.

Args:
----
request (NotificationSingleRequest): Data for the request

Returns:
-------
UUID4: The notification object

"""
response = NotificationSingleResponse(
id=uuid4(),
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc),
to=request.to,
)
return response
35 changes: 35 additions & 0 deletions app/v3/notifications/route_schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Request and Response bodies for v3/notifications."""

from typing import Optional, Union

from pydantic import UUID4, AwareDatetime, BaseModel


class NotificationSingleRequest(BaseModel):
"""Request model for notification endpoint."""

to: str
personalization: Optional[dict[str, str]] = None
template: UUID4

def serialize(self) -> dict[str, Optional[Union[str, dict[str, str]]]]:
"""Serialize the pydantic model.

Returns
-------
dict[str, Optional[Union[str, dict[str, str]]]]: Serialized version of the model

"""
serialized = self.model_dump()
serialized['template'] = str(serialized['template'])
return serialized


class NotificationSingleResponse(BaseModel):
"""Response for notification endpoint."""

id: UUID4
created_at: AwareDatetime
updated_at: AwareDatetime
sent_at: Optional[AwareDatetime] = None
to: str
Loading