Skip to content

Commit

Permalink
issue #221: break azure storage conn string into individual fields
Browse files Browse the repository at this point in the history
  • Loading branch information
k-allagbe committed Jan 6, 2025
1 parent f5c3bd0 commit ee1c5f1
Show file tree
Hide file tree
Showing 7 changed files with 221 additions and 273 deletions.
36 changes: 31 additions & 5 deletions app/config.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from contextlib import asynccontextmanager
from http import HTTPStatus

from dotenv import load_dotenv
from fastapi import FastAPI
from fastapi import APIRouter, FastAPI, Request
from fastapi.logger import logger
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from opentelemetry import trace
from opentelemetry._logs import set_logger_provider
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
Expand All @@ -15,9 +17,11 @@
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from pipeline import GPT, OCR
from psycopg_pool import ConnectionPool
from pydantic import Field, PostgresDsn
from pydantic import Field, PostgresDsn, computed_field
from pydantic_settings import BaseSettings

from app.exceptions import log_error

load_dotenv("secrets.env")
load_dotenv("config.env")

Expand All @@ -28,7 +32,10 @@ class Settings(BaseSettings):
base_path: str = Field("", alias="api_base_path")
fertiscan_db_url: PostgresDsn
fertiscan_schema: str
fertiscan_storage_url: str
azure_storage_account_name: str
azure_storage_account_key: str
azure_storage_default_endpoint_protocol: str
azure_storage_endpoint_suffix: str
openai_api_deployment: str = Field(alias="azure_openai_deployment")
openai_api_endpoint: str = Field(alias="azure_openai_endpoint")
openai_api_key: str = Field(alias="azure_openai_key")
Expand All @@ -38,10 +45,20 @@ class Settings(BaseSettings):
allowed_origins: list[str]
otel_exporter_otlp_endpoint: str = Field(alias="otel_exporter_otlp_endpoint")

@computed_field
@property
def azure_storage_connection_string(self) -> str:
return (
f"DefaultEndpointsProtocol={self.azure_storage_default_endpoint_protocol};"
f"AccountName={self.azure_storage_account_name};"
f"AccountKey={self.azure_storage_account_key};"
f"EndpointSuffix={self.azure_storage_endpoint_suffix}"
)


@asynccontextmanager
async def lifespan(app: FastAPI):
settings = app.state.settings
settings: Settings = app.state.settings
app.pool.open()
resource = Resource.create(
{
Expand Down Expand Up @@ -77,7 +94,7 @@ async def lifespan(app: FastAPI):
tracer_provider.shutdown()


def create_app(settings: Settings):
def create_app(settings: Settings, router: APIRouter, lifespan=None):
app = FastAPI(
lifespan=lifespan, docs_url=settings.swagger_path, root_path=settings.base_path
)
Expand Down Expand Up @@ -109,4 +126,13 @@ def create_app(settings: Settings):
)
app.gpt = gpt

app.include_router(router)

@app.exception_handler(Exception)
async def global_exception_handler(_: Request, e: Exception):
log_error(e)
return JSONResponse(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, content=str(e)
)

return app
155 changes: 4 additions & 151 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -1,152 +1,5 @@
from http import HTTPStatus
from typing import Annotated
from uuid import UUID
from app.config import create_app, lifespan
from app.dependencies import get_settings
from app.routes import router

from fastapi import Depends, Form, HTTPException, Request, UploadFile
from fastapi.responses import JSONResponse, RedirectResponse
from pipeline import GPT, OCR
from psycopg_pool import ConnectionPool

from app.config import Settings, create_app
from app.controllers.data_extraction import extract_data
from app.controllers.inspections import (
create_inspection,
delete_inspection,
read_all_inspections,
read_inspection,
update_inspection,
)
from app.controllers.users import sign_up
from app.dependencies import (
authenticate_user,
fetch_user,
get_connection_pool,
get_gpt,
get_ocr,
get_settings,
validate_files,
)
from app.exceptions import InspectionNotFoundError, UserConflictError, log_error
from app.models.inspections import (
DeletedInspection,
Inspection,
InspectionData,
InspectionUpdate,
)
from app.models.label_data import LabelData
from app.models.monitoring import HealthStatus
from app.models.users import User
from app.sanitization import custom_secure_filename

app = create_app(get_settings())


@app.exception_handler(Exception)
async def global_exception_handler(_: Request, e: Exception):
log_error(e)
return JSONResponse(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, content=str(e))


@app.get("/", tags=["Home"])
async def home():
return RedirectResponse(url=app.docs_url)


@app.get("/health", tags=["Monitoring"], response_model=HealthStatus)
async def health_check():
return HealthStatus()


@app.post("/analyze", response_model=LabelData, tags=["Pipeline"])
async def analyze_document(
ocr: Annotated[OCR, Depends(get_ocr)],
gpt: Annotated[GPT, Depends(get_gpt)],
settings: Annotated[Settings, Depends(get_settings)],
files: Annotated[list[UploadFile], Depends(validate_files)],
):
file_dict = {custom_secure_filename(f.filename): f.file for f in files}
return extract_data(file_dict, ocr, gpt, settings.upload_folder)


@app.post("/signup", tags=["Users"], status_code=201, response_model=User)
async def signup(
cp: Annotated[ConnectionPool, Depends(get_connection_pool)],
user: Annotated[User, Depends(authenticate_user)],
settings: Annotated[Settings, Depends(get_settings)],
):
try:
return await sign_up(cp, user, settings.fertiscan_storage_url)
except UserConflictError:
raise HTTPException(status_code=HTTPStatus.CONFLICT, detail="User exists!")


@app.post("/login", tags=["Users"], status_code=200, response_model=User)
async def login(user: User = Depends(fetch_user)):
return user


@app.get("/inspections", tags=["Inspections"], response_model=list[InspectionData])
async def get_inspections(
cp: Annotated[ConnectionPool, Depends(get_connection_pool)],
user: User = Depends(fetch_user),
):
return await read_all_inspections(cp, user)


@app.get("/inspections/{id}", tags=["Inspections"], response_model=Inspection)
async def get_inspection(
cp: Annotated[ConnectionPool, Depends(get_connection_pool)],
user: Annotated[User, Depends(fetch_user)],
id: UUID,
):
try:
return await read_inspection(cp, user, id)
except InspectionNotFoundError:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Inspection not found"
)


@app.post("/inspections", tags=["Inspections"], response_model=Inspection)
async def post_inspection(
cp: Annotated[ConnectionPool, Depends(get_connection_pool)],
user: Annotated[User, Depends(fetch_user)],
settings: Annotated[Settings, Depends(get_settings)],
label_data: Annotated[LabelData, Form(...)],
files: Annotated[list[UploadFile], Depends(validate_files)],
):
# Note: later on, we might handle label images as their own domain
label_images = [await f.read() for f in files]
conn_string = settings.fertiscan_storage_url
return await create_inspection(cp, user, label_data, label_images, conn_string)


@app.put("/inspections/{id}", tags=["Inspections"], response_model=Inspection)
async def put_inspection(
cp: Annotated[ConnectionPool, Depends(get_connection_pool)],
user: Annotated[User, Depends(fetch_user)],
id: UUID,
inspection: InspectionUpdate,
):
try:
return await update_inspection(cp, user, id, inspection)
except InspectionNotFoundError:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Inspection not found"
)


@app.delete("/inspections/{id}", tags=["Inspections"], response_model=DeletedInspection)
async def delete_inspection_(
cp: Annotated[ConnectionPool, Depends(get_connection_pool)],
user: Annotated[User, Depends(fetch_user)],
settings: Annotated[Settings, Depends(get_settings)],
id: UUID,
):
try:
conn_string = settings.fertiscan_storage_url
return await delete_inspection(cp, user, id, conn_string)
except InspectionNotFoundError:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Inspection not found"
)
app = create_app(get_settings(), router, lifespan)
147 changes: 147 additions & 0 deletions app/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
from http import HTTPStatus
from typing import Annotated
from uuid import UUID

from fastapi import APIRouter, Depends, Form, HTTPException, Request, UploadFile
from fastapi.responses import RedirectResponse
from pipeline import GPT, OCR
from psycopg_pool import ConnectionPool

from app.config import Settings
from app.controllers.data_extraction import extract_data
from app.controllers.inspections import (
create_inspection,
delete_inspection,
read_all_inspections,
read_inspection,
update_inspection,
)
from app.controllers.users import sign_up
from app.dependencies import (
authenticate_user,
fetch_user,
get_connection_pool,
get_gpt,
get_ocr,
get_settings,
validate_files,
)
from app.exceptions import InspectionNotFoundError, UserConflictError
from app.models.inspections import (
DeletedInspection,
Inspection,
InspectionData,
InspectionUpdate,
)
from app.models.label_data import LabelData
from app.models.monitoring import HealthStatus
from app.models.users import User
from app.sanitization import custom_secure_filename

router = APIRouter()


@router.get("/", tags=["Home"])
async def home(request: Request):
return RedirectResponse(url=request.app.docs_url)


@router.get("/health", tags=["Monitoring"], response_model=HealthStatus)
async def health_check():
return HealthStatus()


@router.post("/analyze", response_model=LabelData, tags=["Pipeline"])
async def analyze_document(
ocr: Annotated[OCR, Depends(get_ocr)],
gpt: Annotated[GPT, Depends(get_gpt)],
settings: Annotated[Settings, Depends(get_settings)],
files: Annotated[list[UploadFile], Depends(validate_files)],
):
file_dict = {custom_secure_filename(f.filename): f.file for f in files}
return extract_data(file_dict, ocr, gpt, settings.upload_folder)


@router.post("/signup", tags=["Users"], status_code=201, response_model=User)
async def signup(
cp: Annotated[ConnectionPool, Depends(get_connection_pool)],
user: Annotated[User, Depends(authenticate_user)],
settings: Annotated[Settings, Depends(get_settings)],
):
try:
return await sign_up(cp, user, settings.azure_storage_connection_string)
except UserConflictError:
raise HTTPException(status_code=HTTPStatus.CONFLICT, detail="User exists!")


@router.post("/login", tags=["Users"], status_code=200, response_model=User)
async def login(user: User = Depends(fetch_user)):
return user


@router.get("/inspections", tags=["Inspections"], response_model=list[InspectionData])
async def get_inspections(
cp: Annotated[ConnectionPool, Depends(get_connection_pool)],
user: User = Depends(fetch_user),
):
return await read_all_inspections(cp, user)


@router.get("/inspections/{id}", tags=["Inspections"], response_model=Inspection)
async def get_inspection(
cp: Annotated[ConnectionPool, Depends(get_connection_pool)],
user: Annotated[User, Depends(fetch_user)],
id: UUID,
):
try:
return await read_inspection(cp, user, id)
except InspectionNotFoundError:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Inspection not found"
)


@router.post("/inspections", tags=["Inspections"], response_model=Inspection)
async def post_inspection(
cp: Annotated[ConnectionPool, Depends(get_connection_pool)],
user: Annotated[User, Depends(fetch_user)],
settings: Annotated[Settings, Depends(get_settings)],
label_data: Annotated[LabelData, Form(...)],
files: Annotated[list[UploadFile], Depends(validate_files)],
):
label_images = [await f.read() for f in files]
conn_string = settings.azure_storage_connection_string
return await create_inspection(cp, user, label_data, label_images, conn_string)


@router.put("/inspections/{id}", tags=["Inspections"], response_model=Inspection)
async def put_inspection(
cp: Annotated[ConnectionPool, Depends(get_connection_pool)],
user: Annotated[User, Depends(fetch_user)],
id: UUID,
inspection: InspectionUpdate,
):
try:
return await update_inspection(cp, user, id, inspection)
except InspectionNotFoundError:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Inspection not found"
)


@router.delete(
"/inspections/{id}", tags=["Inspections"], response_model=DeletedInspection
)
async def delete_inspection_(
cp: Annotated[ConnectionPool, Depends(get_connection_pool)],
user: Annotated[User, Depends(fetch_user)],
settings: Annotated[Settings, Depends(get_settings)],
id: UUID,
):
try:
conn_string = settings.azure_storage_connection_string
return await delete_inspection(cp, user, id, conn_string)
except InspectionNotFoundError:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Inspection not found"
)
2 changes: 1 addition & 1 deletion config.env
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ AZURE_OPENAI_DEPLOYMENT=gpt-4o
ALLOWED_ORIGINS=["*"]

# API base path for deployment
API_BASE_PATH=/swagger
API_BASE_PATH=
SWAGGER_PATH=/docs

# DB
Expand Down
Loading

0 comments on commit ee1c5f1

Please sign in to comment.