[New Feature] Rest API implementation to showcase the OpenDBM features
This commit is contained in:
2
rest_api/app/__init__.py
Normal file
2
rest_api/app/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
__version__ = '0.1.0'
|
||||
__api_version__ = 'v1'
|
||||
0
rest_api/app/config/__init__.py
Normal file
0
rest_api/app/config/__init__.py
Normal file
29
rest_api/app/config/database.py
Normal file
29
rest_api/app/config/database.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
SQLALCHEMY_DATABASE_URL = "sqlite:///./app.db"
|
||||
|
||||
engine = create_engine(
|
||||
SQLALCHEMY_DATABASE_URL,
|
||||
connect_args={"check_same_thread": False},
|
||||
)
|
||||
SessionLocal = sessionmaker(
|
||||
autocommit=False,
|
||||
autoflush=False,
|
||||
bind=engine,
|
||||
)
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
def get_db():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def create_tables():
|
||||
Base.metadata.create_all(bind=engine)
|
||||
18
rest_api/app/config/db.py
Normal file
18
rest_api/app/config/db.py
Normal file
@@ -0,0 +1,18 @@
|
||||
def get_db():
|
||||
fake_users_db = {
|
||||
"aicure": {
|
||||
"username": "aicure",
|
||||
"full_name": "AiCure OpenDBM",
|
||||
"email": "opendbm@aicure.com",
|
||||
"hashed_password": "$2b$12$k4R5SPuHkjFKBsQV5gAHl.e/BlxrX2z1H3vxiB9uGtaDZLFXjggCm",
|
||||
"disabled": False,
|
||||
},
|
||||
"alice": {
|
||||
"username": "alice",
|
||||
"full_name": "Alice Wonderson",
|
||||
"email": "alice@aicure.com",
|
||||
"hashed_password": "fakehashedsecret2",
|
||||
"disabled": True,
|
||||
},
|
||||
}
|
||||
return fake_users_db
|
||||
BIN
rest_api/app/files/$process_movement
Normal file
BIN
rest_api/app/files/$process_movement
Normal file
Binary file not shown.
BIN
rest_api/app/files/facial_speech_verbal_video_test.mp4
Normal file
BIN
rest_api/app/files/facial_speech_verbal_video_test.mp4
Normal file
Binary file not shown.
2
rest_api/app/files/test.csv
Normal file
2
rest_api/app/files/test.csv
Normal file
@@ -0,0 +1,2 @@
|
||||
id,name
|
||||
1,Johannes
|
||||
|
45
rest_api/app/main.py
Normal file
45
rest_api/app/main.py
Normal file
@@ -0,0 +1,45 @@
|
||||
import uvicorn
|
||||
from utils.app_exceptions import AppExceptionCase
|
||||
from fastapi import FastAPI
|
||||
from routers import router
|
||||
from config.database import create_tables
|
||||
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
|
||||
from utils.request_exceptions import (
|
||||
http_exception_handler,
|
||||
request_validation_exception_handler,
|
||||
)
|
||||
from utils.app_exceptions import app_exception_handler
|
||||
|
||||
create_tables()
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.exception_handler(StarletteHTTPException)
|
||||
async def custom_http_exception_handler(request, e):
|
||||
return await http_exception_handler(request, e)
|
||||
|
||||
|
||||
@app.exception_handler(RequestValidationError)
|
||||
async def custom_validation_exception_handler(request, e):
|
||||
return await request_validation_exception_handler(request, e)
|
||||
|
||||
|
||||
@app.exception_handler(AppExceptionCase)
|
||||
async def custom_app_exception_handler(request, e):
|
||||
return await app_exception_handler(request, e)
|
||||
|
||||
app.include_router(router.auth_router)
|
||||
app.include_router(router.router)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {"message": "Hello World"}
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
0
rest_api/app/models/__init__.py
Normal file
0
rest_api/app/models/__init__.py
Normal file
0
rest_api/app/routers/__init__.py
Normal file
0
rest_api/app/routers/__init__.py
Normal file
79
rest_api/app/routers/router.py
Normal file
79
rest_api/app/routers/router.py
Normal file
@@ -0,0 +1,79 @@
|
||||
from config.database import get_db
|
||||
from fastapi import APIRouter, Depends, File, UploadFile
|
||||
from fastapi.responses import FileResponse
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from schemas.biomaker_request import BiomakerRequest
|
||||
from schemas.file_properties import FileProperties
|
||||
from schemas.token import Token
|
||||
from services.auth.auth import get_current_active_user, login
|
||||
from services.biomaker.biomaker import BiomakerService
|
||||
from services.file.file import get_file_service
|
||||
from services.file.i_file import FileService
|
||||
from utils.service_result import handle_result
|
||||
|
||||
db = get_db()
|
||||
api_version = "v1"
|
||||
|
||||
auth_router = APIRouter(
|
||||
prefix="/odbm/" + api_version,
|
||||
tags=["Open DBM Authentication"],
|
||||
responses={404: {"description": "Not found"}},
|
||||
)
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/odbm/" + api_version,
|
||||
tags=["Open DBM APIs"],
|
||||
dependencies=[Depends(get_current_active_user)],
|
||||
responses={404: {"description": "Not found"}},
|
||||
)
|
||||
|
||||
|
||||
@auth_router.post("/login", response_model=Token)
|
||||
async def auth_login(form_data: OAuth2PasswordRequestForm = Depends()):
|
||||
result = login(form_data)
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/upload")
|
||||
async def upload(
|
||||
file_properties: FileProperties = Depends(), file: UploadFile = File(...)
|
||||
):
|
||||
file_service: FileService = get_file_service(file_properties.platform)
|
||||
result = file_service.upload(file_properties, file)
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/video/facial")
|
||||
async def video_facial(biomaker_request: BiomakerRequest = Depends()):
|
||||
result, file_name = BiomakerService().process("facial", biomaker_request)
|
||||
return FileResponse(path=result, filename=f"{file_name}.zip")
|
||||
|
||||
|
||||
@router.post("/video/acoustic")
|
||||
async def video_acoustic(biomaker_request: BiomakerRequest = Depends()):
|
||||
result, file_name = BiomakerService().process("acoustic", biomaker_request)
|
||||
return FileResponse(path=result, filename=f"{file_name}.zip")
|
||||
|
||||
|
||||
@router.post("/video/movement")
|
||||
async def video_movement(biomaker_request: BiomakerRequest = Depends()):
|
||||
result, file_name = BiomakerService().process("movement", biomaker_request)
|
||||
return FileResponse(path=result, filename=f"{file_name}.zip")
|
||||
|
||||
|
||||
@router.post("/video/speech")
|
||||
async def video_speech(biomaker_request: BiomakerRequest = Depends()):
|
||||
result, file_name = BiomakerService().process("speech", biomaker_request)
|
||||
return FileResponse(path=result, filename=f"{file_name}.zip")
|
||||
|
||||
|
||||
@router.post("/audio/acoustic")
|
||||
async def audio_acoustic(biomaker_request: BiomakerRequest = Depends()):
|
||||
result, file_name = BiomakerService().process("acoustic", biomaker_request)
|
||||
return FileResponse(path=result, filename=f"{file_name}.zip")
|
||||
|
||||
|
||||
@router.post("/audio/speech")
|
||||
async def audio_speech(biomaker_request: BiomakerRequest = Depends()):
|
||||
result, file_name = BiomakerService().process("speech", biomaker_request)
|
||||
return FileResponse(path=result, filename=f"{file_name}.zip")
|
||||
0
rest_api/app/schemas/__init__.py
Normal file
0
rest_api/app/schemas/__init__.py
Normal file
6
rest_api/app/schemas/biomaker_request.py
Normal file
6
rest_api/app/schemas/biomaker_request.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
class BiomakerRequest(BaseModel):
|
||||
file_url: str
|
||||
platform: str
|
||||
variables: list = []
|
||||
7
rest_api/app/schemas/file_properties.py
Normal file
7
rest_api/app/schemas/file_properties.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
class FileProperties(BaseModel):
|
||||
file_name: str = None
|
||||
file_extension: str = None
|
||||
platform: str = ''
|
||||
|
||||
9
rest_api/app/schemas/token.py
Normal file
9
rest_api/app/schemas/token.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Union
|
||||
|
||||
class Token(BaseModel):
|
||||
access_token: str
|
||||
token_type: str
|
||||
|
||||
class TokenData(BaseModel):
|
||||
username: Union[str, None] = None
|
||||
11
rest_api/app/schemas/user.py
Normal file
11
rest_api/app/schemas/user.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Union
|
||||
|
||||
class User(BaseModel):
|
||||
username: str
|
||||
email: Union[str, None] = None
|
||||
full_name: Union[str, None] = None
|
||||
disabled: Union[bool, None] = None
|
||||
|
||||
class UserInDB(User):
|
||||
hashed_password: str
|
||||
0
rest_api/app/services/__init__.py
Normal file
0
rest_api/app/services/__init__.py
Normal file
0
rest_api/app/services/auth/__init__.py
Normal file
0
rest_api/app/services/auth/__init__.py
Normal file
94
rest_api/app/services/auth/auth.py
Normal file
94
rest_api/app/services/auth/auth.py
Normal file
@@ -0,0 +1,94 @@
|
||||
from datetime import datetime, timedelta
|
||||
from schemas.user import User, UserInDB
|
||||
from schemas.token import Token, TokenData
|
||||
from services.main import OpenDBMSessionContext
|
||||
from config.db import get_db
|
||||
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from passlib.context import CryptContext
|
||||
from jose import JWTError, jwt
|
||||
from typing import Union
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="odbm/v1/login")
|
||||
SECRET_KEY = os.getenv('JWT_SECRET', 'DUMMY_SECRET')
|
||||
ALGORITHM = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv('TOKEN_EXPIRE', 30))
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
db = get_db()
|
||||
|
||||
def get_user(username: str):
|
||||
if username in db:
|
||||
user_dict = db[username]
|
||||
return UserInDB(**user_dict)
|
||||
|
||||
def verify_password(plain_password, hashed_password):
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
|
||||
def get_password_hash(password):
|
||||
return pwd_context.hash(password)
|
||||
|
||||
def authenticate_user(username: str, password: str):
|
||||
user = get_user(username)
|
||||
if not user:
|
||||
return False
|
||||
if not verify_password(password, user.hashed_password):
|
||||
return False
|
||||
return user
|
||||
|
||||
def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None):
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(minutes=15)
|
||||
to_encode.update({"exp": expire})
|
||||
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
async def get_current_user(token: str = Depends(oauth2_scheme)):
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
try:
|
||||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||
username: str = payload.get("sub")
|
||||
if username is None:
|
||||
raise credentials_exception
|
||||
token_data = TokenData(username=username)
|
||||
except JWTError:
|
||||
raise credentials_exception
|
||||
user = get_user(username=token_data.username)
|
||||
if user is None:
|
||||
raise credentials_exception
|
||||
return user
|
||||
|
||||
async def get_current_active_user(current_user: User = Depends(get_current_user)):
|
||||
if current_user.disabled:
|
||||
raise HTTPException(status_code=400, detail="Inactive user")
|
||||
return current_user
|
||||
|
||||
def login(form_data: OAuth2PasswordRequestForm):
|
||||
hashed_pwd = get_password_hash(form_data.password)
|
||||
user = authenticate_user(form_data.username, form_data.password)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect username or password",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
access_token = create_access_token(
|
||||
data={"sub": user.username}, expires_delta=access_token_expires
|
||||
)
|
||||
token = Token(access_token=access_token, token_type="bearer")
|
||||
return token
|
||||
|
||||
0
rest_api/app/services/biomaker/__init__.py
Normal file
0
rest_api/app/services/biomaker/__init__.py
Normal file
129
rest_api/app/services/biomaker/biomaker.py
Normal file
129
rest_api/app/services/biomaker/biomaker.py
Normal file
@@ -0,0 +1,129 @@
|
||||
import os
|
||||
from ast import For
|
||||
from zipfile import ZipFile
|
||||
|
||||
from schemas.biomaker_request import BiomakerRequest
|
||||
|
||||
from opendbm import FacialActivity, Movement, Speech, VerbalAcoustics
|
||||
|
||||
|
||||
class BiomakerService:
|
||||
def process(self, group: str, biomaker_request: BiomakerRequest):
|
||||
if group == "facial":
|
||||
return self.process_facial(group, biomaker_request)
|
||||
elif group == "acoustic":
|
||||
return self.process_acoustic(group, biomaker_request)
|
||||
elif group == "movement":
|
||||
return self.process_movement(group, biomaker_request)
|
||||
elif group == "speech":
|
||||
return self.process_speech(group, biomaker_request)
|
||||
pass
|
||||
|
||||
def process_facial(self, group, biomaker_request: BiomakerRequest):
|
||||
m = FacialActivity()
|
||||
curWorkingDir = os.getcwd()
|
||||
methodName = "process_facial"
|
||||
testfile = f"{curWorkingDir}/{biomaker_request.file_url}"
|
||||
if os.path.isfile(testfile):
|
||||
print("File exist")
|
||||
else:
|
||||
print("File not exist")
|
||||
m.fit(testfile)
|
||||
zip_filename = f"{curWorkingDir}/files/${methodName}"
|
||||
zipObj = ZipFile(zip_filename, "w")
|
||||
for var in biomaker_request.variables:
|
||||
if var == "landmark":
|
||||
lmk = m.get_landmark()
|
||||
lmk.to_dataframe().to_csv(var + ".csv", index=False)
|
||||
zipObj.write(var + ".csv")
|
||||
if var == "asymmetry":
|
||||
asym = m.get_asymmetry()
|
||||
asym.to_dataframe().to_csv(var + ".csv", index=False)
|
||||
zipObj.write(var + ".csv")
|
||||
if var == "expressivity":
|
||||
expr = m.get_expressivity()
|
||||
expr.to_dataframe().to_csv(var + ".csv", index=False)
|
||||
zipObj.write(var + ".csv")
|
||||
if var == "action_unit":
|
||||
au = m.get_action_unit()
|
||||
au.to_dataframe().to_csv(var + ".csv", index=False)
|
||||
zipObj.write(var + ".csv")
|
||||
zipObj.close()
|
||||
return zip_filename, methodName
|
||||
|
||||
def process_acoustic(self, group, biomaker_request: BiomakerRequest):
|
||||
m = VerbalAcoustics()
|
||||
curWorkingDir = os.getcwd()
|
||||
methodName = "process_acoustic"
|
||||
testfile = f"{curWorkingDir}/{biomaker_request.file_url}"
|
||||
m.fit(testfile)
|
||||
zip_filename = f"{curWorkingDir}/files/${methodName}"
|
||||
zipObj = ZipFile(zip_filename, "w")
|
||||
for var in biomaker_request.variables:
|
||||
if var == "audio_intensity":
|
||||
au = m.get_audio_intensity()
|
||||
au.to_dataframe().to_csv(var + ".csv", index=False)
|
||||
zipObj.write(var + ".csv")
|
||||
if var == "pitch_frequency":
|
||||
vp = m.get_pitch_frequency()
|
||||
vp.to_dataframe().to_csv(var + ".csv", index=False)
|
||||
zipObj.write(var + ".csv")
|
||||
if var == "formant_frequency":
|
||||
ff = m.get_formant_frequency()
|
||||
ff.to_dataframe().to_csv(var + ".csv", index=False)
|
||||
zipObj.write(var + ".csv")
|
||||
if var == "harmonic_noise":
|
||||
hn = m.get_harmonic_noise()
|
||||
hn.to_dataframe().to_csv(var + ".csv", index=False)
|
||||
zipObj.write(var + ".csv")
|
||||
zipObj.close()
|
||||
return zip_filename, methodName
|
||||
|
||||
def process_movement(self, group, biomaker_request: BiomakerRequest):
|
||||
m = Movement()
|
||||
curWorkingDir = os.getcwd()
|
||||
methodName = "process_movement"
|
||||
testfile = f"{curWorkingDir}/{biomaker_request.file_url}"
|
||||
m.fit(testfile)
|
||||
zip_filename = f"{curWorkingDir}/files/${methodName}"
|
||||
zipObj = ZipFile(zip_filename, "w")
|
||||
|
||||
for var in biomaker_request.variables:
|
||||
if var == "head_movement":
|
||||
lmk = m.get_head_movement()
|
||||
lmk.to_dataframe().to_csv(var + ".csv", index=False)
|
||||
zipObj.write(var + ".csv")
|
||||
if var == "eye_blink":
|
||||
asym = m.get_eye_blink()
|
||||
asym.to_dataframe().to_csv(var + ".csv", index=False)
|
||||
zipObj.write(var + ".csv")
|
||||
if var == "facial_tremor":
|
||||
au = m.get_facial_tremor()
|
||||
au.to_dataframe().to_csv(var + ".csv", index=False)
|
||||
zipObj.write(var + ".csv")
|
||||
if var == "vocal_tremor":
|
||||
au = m.get_vocal_tremor()
|
||||
au.to_dataframe().to_csv(var + ".csv", index=False)
|
||||
zipObj.write(var + ".csv")
|
||||
|
||||
return zip_filename, methodName
|
||||
|
||||
def process_speech(self, group, biomaker_request: BiomakerRequest):
|
||||
m = Speech()
|
||||
curWorkingDir = os.getcwd()
|
||||
methodName = "process_speech"
|
||||
testfile = f"{curWorkingDir}/{biomaker_request.file_url}"
|
||||
m.fit(testfile)
|
||||
zip_filename = f"{curWorkingDir}/files/${methodName}"
|
||||
zipObj = ZipFile(zip_filename, "w")
|
||||
for var in biomaker_request.variables:
|
||||
if var == "speech_features":
|
||||
sf = m.get_speech_features()
|
||||
sf.to_dataframe().to_csv(var + ".csv", index=False)
|
||||
zipObj.write(var + ".csv")
|
||||
if var == "transcribe":
|
||||
tr = m.get_transcribe()
|
||||
tr.to_dataframe().to_csv(var + ".csv", index=False)
|
||||
zipObj.write(var + ".csv")
|
||||
zipObj.close()
|
||||
return zip_filename, methodName
|
||||
0
rest_api/app/services/file/__init__.py
Normal file
0
rest_api/app/services/file/__init__.py
Normal file
53
rest_api/app/services/file/file.py
Normal file
53
rest_api/app/services/file/file.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import os
|
||||
import shutil
|
||||
from fastapi import UploadFile
|
||||
import boto3
|
||||
|
||||
from schemas.file_properties import FileProperties
|
||||
from services.file.i_file import FileService
|
||||
|
||||
AWS_ACCESS_KEY = os.getenv('AWS_ACCESS_KEY', 'DUMMY_KEY')
|
||||
AWS_SECRET_KEY = os.getenv('AWS_SECRET_KEY', 'DUMMY_SECRET')
|
||||
S3_BUCKET_NAME = 'odbm-test'
|
||||
|
||||
def get_file_service(platform:str) -> FileService:
|
||||
if platform.lower() == 's3':
|
||||
return S3FileService()
|
||||
else:
|
||||
return MemoryFileService()
|
||||
|
||||
|
||||
client = boto3.client(
|
||||
's3',
|
||||
aws_access_key_id=AWS_ACCESS_KEY,
|
||||
aws_secret_access_key=AWS_SECRET_KEY
|
||||
)
|
||||
|
||||
s3 = boto3.resource(
|
||||
's3',
|
||||
aws_access_key_id=AWS_ACCESS_KEY,
|
||||
aws_secret_access_key=AWS_SECRET_KEY
|
||||
)
|
||||
|
||||
class S3FileService(FileService):
|
||||
def upload(self, file_properties: FileProperties, file: UploadFile):
|
||||
print(AWS_ACCESS_KEY)
|
||||
print(AWS_SECRET_KEY)
|
||||
s3 = boto3.resource("s3")
|
||||
bucket = s3.Bucket(S3_BUCKET_NAME)
|
||||
bucket.upload_fileobj(file.file, file.filename, ExtraArgs={"ACL": "public-read"})
|
||||
uploaded_file_url = f"https://{S3_BUCKET_NAME}.s3.amazonaws.com/{file.filename}"
|
||||
return {"returnUrl": uploaded_file_url}
|
||||
|
||||
def download(file_properties: FileProperties):
|
||||
pass
|
||||
|
||||
class MemoryFileService(FileService):
|
||||
def upload(self, file_properties: FileProperties, file: UploadFile):
|
||||
file_location = f"files/{file.filename}"
|
||||
with open(file_location, "wb+") as file_object:
|
||||
shutil.copyfileobj(file.file, file_object)
|
||||
return {"info": f"file '{file.filename}' saved at '{file_location}'"}
|
||||
|
||||
def download(file_properties: FileProperties):
|
||||
pass
|
||||
13
rest_api/app/services/file/i_file.py
Normal file
13
rest_api/app/services/file/i_file.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from fastapi import UploadFile
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from schemas.file_properties import FileProperties
|
||||
|
||||
|
||||
class FileService:
|
||||
__metaclass__ = ABCMeta
|
||||
|
||||
@abstractmethod
|
||||
def upload(file_properties: FileProperties, file: UploadFile): raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def download(file_properties: FileProperties): raise NotImplementedError
|
||||
20
rest_api/app/services/main.py
Normal file
20
rest_api/app/services/main.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from sqlalchemy.orm import Session
|
||||
from config.db import get_db
|
||||
|
||||
user_db = get_db()
|
||||
|
||||
class DBSessionContext(object):
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
|
||||
|
||||
class AppService(DBSessionContext):
|
||||
pass
|
||||
|
||||
|
||||
class AppCRUD(DBSessionContext):
|
||||
pass
|
||||
|
||||
class OpenDBMSessionContext(object):
|
||||
def __init__(self):
|
||||
self.db = user_db
|
||||
0
rest_api/app/utils/__init__.py
Normal file
0
rest_api/app/utils/__init__.py
Normal file
51
rest_api/app/utils/app_exceptions.py
Normal file
51
rest_api/app/utils/app_exceptions.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from fastapi import Request
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
|
||||
class AppExceptionCase(Exception):
|
||||
def __init__(self, status_code: int, context: dict):
|
||||
self.exception_case = self.__class__.__name__
|
||||
self.status_code = status_code
|
||||
self.context = context
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
f"<AppException {self.exception_case} - "
|
||||
+ f"status_code={self.status_code} - context={self.context}>"
|
||||
)
|
||||
|
||||
|
||||
async def app_exception_handler(request: Request, exc: AppExceptionCase):
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content={
|
||||
"app_exception": exc.exception_case,
|
||||
"context": exc.context,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class AppException(object):
|
||||
class FooCreateItem(AppExceptionCase):
|
||||
def __init__(self, context: dict = None):
|
||||
"""
|
||||
Item creation failed
|
||||
"""
|
||||
status_code = 500
|
||||
AppExceptionCase.__init__(self, status_code, context)
|
||||
|
||||
class FooGetItem(AppExceptionCase):
|
||||
def __init__(self, context: dict = None):
|
||||
"""
|
||||
Item not found
|
||||
"""
|
||||
status_code = 404
|
||||
AppExceptionCase.__init__(self, status_code, context)
|
||||
|
||||
class FooItemRequiresAuth(AppExceptionCase):
|
||||
def __init__(self, context: dict = None):
|
||||
"""
|
||||
Item is not public and requires auth
|
||||
"""
|
||||
status_code = 401
|
||||
AppExceptionCase.__init__(self, status_code, context)
|
||||
@@ -0,0 +1,4 @@
|
||||
from utils.app_exceptions import AppException
|
||||
|
||||
print([e for e in dir(AppException) if "__" not in e])
|
||||
# ['FooCreateItem', 'FooGetItem', 'FooItemRequiresAuth']
|
||||
22
rest_api/app/utils/request_exceptions.py
Normal file
22
rest_api/app/utils/request_exceptions.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
|
||||
from starlette.exceptions import HTTPException
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import JSONResponse
|
||||
from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
|
||||
async def http_exception_handler(
|
||||
request: Request, exc: HTTPException
|
||||
) -> JSONResponse:
|
||||
return JSONResponse({"detail": exc.detail}, status_code=exc.status_code)
|
||||
|
||||
|
||||
async def request_validation_exception_handler(
|
||||
request: Request, exc: RequestValidationError
|
||||
) -> JSONResponse:
|
||||
return JSONResponse(
|
||||
status_code=HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
content={"detail": jsonable_encoder(exc.errors())},
|
||||
)
|
||||
47
rest_api/app/utils/service_result.py
Normal file
47
rest_api/app/utils/service_result.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from loguru import logger
|
||||
import inspect
|
||||
|
||||
from utils.app_exceptions import AppExceptionCase
|
||||
|
||||
|
||||
class ServiceResult(object):
|
||||
def __init__(self, arg):
|
||||
if isinstance(arg, AppExceptionCase):
|
||||
self.success = False
|
||||
self.exception_case = arg.exception_case
|
||||
self.status_code = arg.status_code
|
||||
else:
|
||||
self.success = True
|
||||
self.exception_case = None
|
||||
self.status_code = None
|
||||
self.value = arg
|
||||
|
||||
def __str__(self):
|
||||
if self.success:
|
||||
return "[Success]"
|
||||
return f'[Exception] "{self.exception_case}"'
|
||||
|
||||
def __repr__(self):
|
||||
if self.success:
|
||||
return "<ServiceResult Success>"
|
||||
return f"<ServiceResult AppException {self.exception_case}>"
|
||||
|
||||
def __enter__(self):
|
||||
return self.value
|
||||
|
||||
def __exit__(self, *kwargs):
|
||||
pass
|
||||
|
||||
|
||||
def caller_info() -> str:
|
||||
info = inspect.getframeinfo(inspect.stack()[2][0])
|
||||
return f"{info.filename}:{info.function}:{info.lineno}"
|
||||
|
||||
|
||||
def handle_result(result: ServiceResult):
|
||||
if not result.success:
|
||||
with result as exception:
|
||||
logger.error(f"{exception} | caller={caller_info()}")
|
||||
raise exception
|
||||
with result as result:
|
||||
return result
|
||||
Reference in New Issue
Block a user