Coverage for src / idx_api / main.py: 88%

69 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2025-12-28 11:16 -0700

1"""FastAPI application entry point.""" 

2 

3from contextlib import asynccontextmanager 

4from importlib.metadata import version 

5 

6from fastapi import FastAPI 

7from fastapi.middleware.cors import CORSMiddleware 

8from fastapi.staticfiles import StaticFiles 

9from pathlib import Path 

10 

11from idx_api.config import settings 

12from idx_api.database import get_db_session 

13from idx_api.embeddings import create_vector_tables, create_image_tables 

14from idx_api.routers import admin, agents, auth, broker_contacts, brokerage_domains, brokerages, clients, content_sources, data_management, embeddings_admin, leads, newsletter, properties, public, search, suggestions, tours, uploads, vision 

15 

16 

17def get_version() -> str: 

18 """Get package version.""" 

19 try: 

20 return version("idx-api") 

21 except Exception: 

22 return "0.0.0-dev" 

23 

24 

25@asynccontextmanager 

26async def lifespan(app: FastAPI): 

27 """Application lifespan events.""" 

28 # Startup 

29 print(f"🏠 IDX API v{get_version()} starting...") 

30 print(f"📁 Database: {settings.sqlite_db_path}") 

31 print(f"🔮 Ollama: {settings.ollama_base_url}") 

32 

33 # Initialize vector tables 

34 try: 

35 with get_db_session() as db: 

36 create_vector_tables(db) 

37 print("✅ Vector tables initialized") 

38 except Exception as e: 

39 print(f"⚠️ Failed to initialize vector tables: {e}") 

40 

41 # Initialize image tables for multi-modal search 

42 try: 

43 with get_db_session() as db: 

44 create_image_tables(db) 

45 print("✅ Image tables initialized (multi-modal search)") 

46 except Exception as e: 

47 print(f"⚠️ Failed to initialize image tables: {e}") 

48 

49 yield 

50 # Shutdown 

51 print("👋 IDX API shutting down...") 

52 

53 

54app = FastAPI( 

55 title="MLSGrid IDX API", 

56 description="Property search and listing API powered by MLSGrid data", 

57 version=get_version(), 

58 lifespan=lifespan, 

59 docs_url="/docs", 

60 redoc_url="/redoc", 

61) 

62 

63# CORS 

64app.add_middleware( 

65 CORSMiddleware, 

66 allow_origins=settings.cors_origins, 

67 allow_credentials=True, 

68 allow_methods=["*"], 

69 allow_headers=["*"], 

70) 

71 

72# Routers 

73app.include_router(public.router, prefix="/api", tags=["public"]) # Public endpoints (no auth) 

74app.include_router(brokerage_domains.router, prefix="/api", tags=["domains"], include_in_schema=True) # Public domain lookup 

75app.include_router(auth.router, prefix="/api/auth", tags=["auth"]) 

76app.include_router(admin.router, prefix="/api/admin", tags=["admin"]) 

77app.include_router(agents.router, prefix="/api/admin", tags=["agents"]) 

78app.include_router(brokerages.router, prefix="/api/admin", tags=["brokerages"]) 

79app.include_router(broker_contacts.router, prefix="/api/admin", tags=["broker-contacts"]) 

80app.include_router(brokerage_domains.router, prefix="/api/admin", tags=["brokerage-domains"]) 

81app.include_router(suggestions.router, prefix="/api/admin", tags=["suggestions"]) 

82app.include_router(embeddings_admin.router, prefix="/api/admin", tags=["embeddings"]) 

83app.include_router(data_management.router, prefix="/api/admin", tags=["data-management"]) 

84app.include_router(content_sources.router, prefix="/api/admin", tags=["content-sources"]) 

85app.include_router(clients.router, prefix="/api/admin", tags=["clients"]) 

86app.include_router(uploads.router, prefix="/api", tags=["uploads"]) 

87app.include_router(properties.router, prefix="/api/properties", tags=["properties"]) 

88app.include_router(search.router, prefix="/api/search", tags=["search"]) 

89app.include_router(tours.router, prefix="/api/tours", tags=["tours"]) 

90app.include_router(newsletter.router, prefix="/api", tags=["newsletter"]) 

91app.include_router(leads.router, prefix="/api", tags=["leads"]) # Contact form → lead creation 

92app.include_router(vision.router, prefix="/api/admin/vision", tags=["vision"]) 

93 

94# Static files for uploads 

95uploads_dir = Path("/data/uploads") 

96uploads_dir.mkdir(parents=True, exist_ok=True) 

97app.mount("/uploads", StaticFiles(directory=str(uploads_dir)), name="uploads") 

98# Also mount as /media for backward compatibility 

99app.mount("/media", StaticFiles(directory=str(uploads_dir)), name="media") 

100 

101 

102@app.get("/health") 

103async def health_check(): 

104 """Health check endpoint.""" 

105 return {"status": "healthy", "version": get_version()} 

106 

107 

108@app.get("/") 

109async def root(): 

110 """API root with links to documentation.""" 

111 return { 

112 "name": "MLSGrid IDX API", 

113 "version": get_version(), 

114 "docs": "/docs", 

115 "health": "/health", 

116 } 

117 

118 

119def run(): 

120 """Run the server (for script entry point).""" 

121 import uvicorn 

122 

123 uvicorn.run( 

124 "idx_api.main:app", 

125 host=settings.api_host, 

126 port=settings.api_port, 

127 reload=settings.environment == "dev", 

128 ) 

129 

130 

131if __name__ == "__main__": 

132 run()