Coverage for src/semware/main.py: 90%

41 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-09-09 02:16 -0700

1"""Main FastAPI application for SemWare.""" 

2 

3from datetime import datetime 

4 

5from fastapi import FastAPI, Request, status 

6from fastapi.exceptions import RequestValidationError 

7from fastapi.responses import JSONResponse 

8from loguru import logger 

9 

10from .config import settings 

11from .models.requests import HealthResponse 

12from .models.schemas import ErrorResponse 

13from .utils.logging import setup_logging 

14 

15 

16def create_app() -> FastAPI: 

17 """Create and configure the FastAPI application.""" 

18 

19 # Setup logging 

20 setup_logging( 

21 level=settings.log_level, 

22 log_file=settings.log_file, 

23 ) 

24 

25 # Create data directory if it doesn't exist 

26 settings.db_path.mkdir(parents=True, exist_ok=True) 

27 logger.info(f"Data directory: {settings.db_path}") 

28 

29 # Create FastAPI app 

30 app = FastAPI( 

31 title=settings.app_name, 

32 version=settings.app_version, 

33 description="Semantic search API server using vector databases and ML embeddings", 

34 debug=settings.debug, 

35 docs_url="/docs" if settings.debug else None, 

36 redoc_url="/redoc" if settings.debug else None, 

37 ) 

38 

39 # Add middleware and exception handlers 

40 setup_exception_handlers(app) 

41 

42 # Add routes 

43 setup_routes(app) 

44 

45 logger.info(f"FastAPI app created: {settings.app_name} v{settings.app_version}") 

46 return app 

47 

48 

49def setup_exception_handlers(app: FastAPI) -> None: 

50 """Setup custom exception handlers.""" 

51 

52 @app.exception_handler(RequestValidationError) 

53 async def validation_exception_handler( 

54 request: Request, exc: RequestValidationError 

55 ): 

56 """Handle request validation errors.""" 

57 logger.warning(f"Validation error for {request.url}: {exc}") 

58 return JSONResponse( 

59 status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, 

60 content=ErrorResponse( 

61 error="Validation Error", detail=str(exc), error_code="VALIDATION_ERROR" 

62 ).model_dump(), 

63 ) 

64 

65 @app.exception_handler(Exception) 

66 async def general_exception_handler(request: Request, exc: Exception): 

67 """Handle general exceptions.""" 

68 logger.exception(f"Unhandled exception for {request.url}: {exc}") 

69 return JSONResponse( 

70 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, 

71 content=ErrorResponse( 

72 error="Internal Server Error", 

73 detail="An unexpected error occurred", 

74 error_code="INTERNAL_ERROR", 

75 ).model_dump(), 

76 ) 

77 

78 

79def setup_routes(app: FastAPI) -> None: 

80 """Setup API routes.""" 

81 

82 @app.get("/health", response_model=HealthResponse, tags=["Health"]) 

83 async def health_check(): 

84 """Health check endpoint.""" 

85 return HealthResponse( 

86 status="healthy", 

87 app_name=settings.app_name, 

88 version=settings.app_version, 

89 timestamp=datetime.now().isoformat(), 

90 ) 

91 

92 @app.get("/", tags=["Root"]) 

93 async def root(): 

94 """Root endpoint.""" 

95 return { 

96 "message": f"Welcome to {settings.app_name} v{settings.app_version}", 

97 "docs_url": ( 

98 "/docs" if settings.debug else "Documentation disabled in production" 

99 ), 

100 } 

101 

102 # Import and include routers 

103 from .api.data import router as data_router 

104 from .api.search import router as search_router 

105 from .api.tables import router as tables_router 

106 

107 app.include_router(tables_router, prefix="/tables", tags=["Tables"]) 

108 app.include_router(data_router, prefix="/tables", tags=["Data"]) 

109 app.include_router(search_router, prefix="/tables", tags=["Search"]) 

110 

111 

112# Create the app instance 

113app = create_app() 

114 

115 

116if __name__ == "__main__": 

117 import uvicorn 

118 

119 logger.info(f"Starting SemWare server on {settings.host}:{settings.port}") 

120 uvicorn.run( 

121 "semware.main:app", 

122 host=settings.host, 

123 port=settings.port, 

124 workers=settings.workers, 

125 reload=settings.debug, 

126 log_level=settings.log_level.lower(), 

127 )