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 f1d31c5
Show file tree
Hide file tree
Showing 8 changed files with 223 additions and 297 deletions.
38 changes: 32 additions & 6 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.settings
app.pool.open()
resource = Resource.create(
{
Expand Down Expand Up @@ -77,11 +94,11 @@ 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
)
app.state.settings = settings
app.settings = settings

app.add_middleware(
CORSMiddleware,
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
25 changes: 2 additions & 23 deletions app/dependencies.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from functools import lru_cache
from http import HTTPStatus

from fastapi import Depends, File, HTTPException, Request, UploadFile
Expand All @@ -7,44 +6,30 @@
from psycopg_pool import ConnectionPool

from app.config import Settings

from app.controllers.users import sign_in
from app.exceptions import UserNotFoundError
from app.models.users import User

auth = HTTPBasic()


@lru_cache
def get_settings():
return Settings()
def get_settings(request: Request) -> Settings:
return request.app.settings


def get_connection_pool(request: Request) -> ConnectionPool:
"""
Returns the app's connection pool.
"""
return request.app.pool


def get_ocr(request: Request) -> OCR:
"""
Returns the app's OCR instance.
"""
return request.app.ocr


def get_gpt(request: Request) -> GPT:
"""
Returns the app's GPT instance.
"""
return request.app.gpt


def authenticate_user(credentials: HTTPBasicCredentials = Depends(auth)):
"""
Authenticates a user.
"""
if not credentials.username:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="Missing email address!"
Expand All @@ -57,9 +42,6 @@ async def fetch_user(
auth_user: User = Depends(authenticate_user),
cp: ConnectionPool = Depends(get_connection_pool),
) -> User:
"""
Fetches the authenticated user's info from db.
"""
try:
return await sign_in(cp, auth_user)
except UserNotFoundError:
Expand All @@ -69,9 +51,6 @@ async def fetch_user(


def validate_files(files: list[UploadFile] = File(..., min_length=1)):
"""
Validates uploaded files.
"""
for f in files:
if f.size == 0:
raise HTTPException(
Expand Down
154 changes: 3 additions & 151 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -1,152 +1,4 @@
from http import HTTPStatus
from typing import Annotated
from uuid import UUID
from app.config import Settings, create_app, lifespan
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(Settings(), router, lifespan)
Loading

0 comments on commit f1d31c5

Please sign in to comment.