[New Feature] Rest API implementation to showcase the OpenDBM features

This commit is contained in:
Rudy Haryanto
2022-10-04 03:07:29 +07:00
parent a574bc6870
commit 92e08860a8
41 changed files with 2576 additions and 0 deletions

2
rest_api/app/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
__version__ = '0.1.0'
__api_version__ = 'v1'

View File

View 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
View 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

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,2 @@
id,name
1,Johannes
1 id name
2 1 Johannes

45
rest_api/app/main.py Normal file
View 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)

View File

View File

View 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")

View File

View File

@@ -0,0 +1,6 @@
from pydantic import BaseModel
class BiomakerRequest(BaseModel):
file_url: str
platform: str
variables: list = []

View File

@@ -0,0 +1,7 @@
from pydantic import BaseModel
class FileProperties(BaseModel):
file_name: str = None
file_extension: str = None
platform: str = ''

View 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

View 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

View File

View File

View 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

View 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

View File

View 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

View 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

View 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

View File

View 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)

View File

@@ -0,0 +1,4 @@
from utils.app_exceptions import AppException
print([e for e in dir(AppException) if "__" not in e])
# ['FooCreateItem', 'FooGetItem', 'FooItemRequiresAuth']

View 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())},
)

View 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