finalised backend
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
"""
|
||||
Главный файл FastAPI приложения
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
|
||||
if '/app' not in sys.path:
|
||||
sys.path.insert(0, '/app')
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from contextlib import asynccontextmanager
|
||||
from dishka.integrations.fastapi import setup_dishka
|
||||
from dishka import Container
|
||||
from src.shared.config import settings
|
||||
from src.shared.exceptions import LawyerAIException
|
||||
from src.shared.di_container import create_container
|
||||
from src.presentation.middleware.error_handler import exception_handler
|
||||
from src.presentation.api.v1 import users, collections, documents, conversations, messages
|
||||
from src.infrastructure.database.base import engine, Base
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""Управление жизненным циклом приложения"""
|
||||
container = create_container()
|
||||
setup_dishka(container, app)
|
||||
try:
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
except Exception as e:
|
||||
print(f"Примечание при создании таблиц: {e}")
|
||||
yield
|
||||
await container.close()
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
title=settings.APP_NAME,
|
||||
description="API для системы ИИ-юриста",
|
||||
version="1.0.0",
|
||||
lifespan=lifespan
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.CORS_ORIGINS,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.add_exception_handler(LawyerAIException, exception_handler)
|
||||
|
||||
app.include_router(users.router, prefix="/api/v1")
|
||||
app.include_router(collections.router, prefix="/api/v1")
|
||||
app.include_router(documents.router, prefix="/api/v1")
|
||||
app.include_router(conversations.router, prefix="/api/v1")
|
||||
app.include_router(messages.router, prefix="/api/v1")
|
||||
|
||||
try:
|
||||
from src.presentation.api.v1 import admin
|
||||
app.include_router(admin.router, prefix="/api/v1")
|
||||
except (ImportError, AttributeError) as e:
|
||||
print(f"Админ-панель не загружена: {e}")
|
||||
pass
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
"""Корневой эндпоинт"""
|
||||
return {
|
||||
"message": "Добро пожаловать в API ИИ-юриста",
|
||||
"version": "1.0.0",
|
||||
"docs": "/docs"
|
||||
}
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""Проверка здоровья приложения"""
|
||||
return {"status": "ok"}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
"""
|
||||
Middleware
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
"""
|
||||
Middleware для авторизации через Telegram бота
|
||||
|
||||
Для Telegram бота авторизация простая:
|
||||
1. Бот получает user_id от Telegram в каждом сообщении
|
||||
2. Бот передает user_id на бэкенд в заголовке X-Telegram-ID
|
||||
3. Бэкенд идентифицирует пользователя по telegram_id
|
||||
"""
|
||||
from fastapi import HTTPException, Request
|
||||
from src.domain.entities.user import User
|
||||
from src.domain.repositories.user_repository import IUserRepository
|
||||
from src.domain.entities.user import UserRole
|
||||
|
||||
|
||||
async def get_current_user(
|
||||
request: Request,
|
||||
user_repo: IUserRepository
|
||||
) -> User:
|
||||
"""
|
||||
Получить текущего пользователя по Telegram ID
|
||||
|
||||
Telegram бот передает user_id в заголовке X-Telegram-ID.
|
||||
Бэкенд идентифицирует пользователя по этому ID.
|
||||
|
||||
Args:
|
||||
request: FastAPI Request объект
|
||||
user_repo: Репозиторий пользователей
|
||||
|
||||
Returns:
|
||||
Пользователь
|
||||
|
||||
Raises:
|
||||
HTTPException: Если не указан X-Telegram-ID
|
||||
"""
|
||||
x_telegram_id = request.headers.get("X-Telegram-ID")
|
||||
|
||||
if not x_telegram_id:
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail="Не указан X-Telegram-ID. Бот должен передавать user_id в заголовке."
|
||||
)
|
||||
|
||||
user = await user_repo.get_by_telegram_id(x_telegram_id)
|
||||
|
||||
if not user:
|
||||
user = User(telegram_id=x_telegram_id, role=UserRole.USER)
|
||||
user = await user_repo.create(user)
|
||||
|
||||
return user
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
"""
|
||||
Обработчик ошибок для FastAPI
|
||||
"""
|
||||
from fastapi import Request, status
|
||||
from fastapi.responses import JSONResponse
|
||||
from src.shared.exceptions import (
|
||||
LawyerAIException,
|
||||
NotFoundError,
|
||||
UnauthorizedError,
|
||||
ForbiddenError,
|
||||
ValidationError,
|
||||
DatabaseError
|
||||
)
|
||||
|
||||
|
||||
async def exception_handler(request: Request, exc: LawyerAIException) -> JSONResponse:
|
||||
"""Обработчик кастомных исключений"""
|
||||
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
detail = str(exc)
|
||||
|
||||
if isinstance(exc, NotFoundError):
|
||||
status_code = status.HTTP_404_NOT_FOUND
|
||||
elif isinstance(exc, UnauthorizedError):
|
||||
status_code = status.HTTP_401_UNAUTHORIZED
|
||||
elif isinstance(exc, ForbiddenError):
|
||||
status_code = status.HTTP_403_FORBIDDEN
|
||||
elif isinstance(exc, ValidationError):
|
||||
status_code = status.HTTP_400_BAD_REQUEST
|
||||
elif isinstance(exc, DatabaseError):
|
||||
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
|
||||
return JSONResponse(
|
||||
status_code=status_code,
|
||||
content={"detail": detail, "type": exc.__class__.__name__}
|
||||
)
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
"""
|
||||
DI контейнер на основе dishka
|
||||
"""
|
||||
from dishka import Container, Provider, Scope, provide
|
||||
from fastapi import Request
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from src.infrastructure.database.base import AsyncSessionLocal
|
||||
from src.infrastructure.repositories.postgresql.user_repository import PostgreSQLUserRepository
|
||||
from src.infrastructure.repositories.postgresql.collection_repository import PostgreSQLCollectionRepository
|
||||
from src.infrastructure.repositories.postgresql.document_repository import PostgreSQLDocumentRepository
|
||||
from src.infrastructure.repositories.postgresql.conversation_repository import PostgreSQLConversationRepository
|
||||
from src.infrastructure.repositories.postgresql.message_repository import PostgreSQLMessageRepository
|
||||
from src.infrastructure.repositories.postgresql.collection_access_repository import PostgreSQLCollectionAccessRepository
|
||||
from src.domain.repositories.user_repository import IUserRepository
|
||||
from src.domain.repositories.collection_repository import ICollectionRepository
|
||||
from src.domain.repositories.document_repository import IDocumentRepository
|
||||
from src.domain.repositories.conversation_repository import IConversationRepository
|
||||
from src.domain.repositories.message_repository import IMessageRepository
|
||||
from src.domain.repositories.collection_access_repository import ICollectionAccessRepository
|
||||
from src.infrastructure.external.yandex_ocr import YandexOCRService
|
||||
from src.infrastructure.external.deepseek_client import DeepSeekClient
|
||||
from src.application.services.document_parser_service import DocumentParserService
|
||||
from src.application.use_cases.user_use_cases import UserUseCases
|
||||
from src.application.use_cases.collection_use_cases import CollectionUseCases
|
||||
from src.application.use_cases.document_use_cases import DocumentUseCases
|
||||
from src.application.use_cases.conversation_use_cases import ConversationUseCases
|
||||
from src.application.use_cases.message_use_cases import MessageUseCases
|
||||
from src.domain.entities.user import User
|
||||
|
||||
|
||||
class DatabaseProvider(Provider):
|
||||
@provide(scope=Scope.REQUEST)
|
||||
@asynccontextmanager
|
||||
async def get_db(self) -> AsyncSession:
|
||||
async with AsyncSessionLocal() as session:
|
||||
try:
|
||||
yield session
|
||||
finally:
|
||||
await session.close()
|
||||
|
||||
|
||||
class RepositoryProvider(Provider):
|
||||
@provide(scope=Scope.REQUEST)
|
||||
def get_user_repository(self, session: AsyncSession) -> IUserRepository:
|
||||
return PostgreSQLUserRepository(session)
|
||||
|
||||
@provide(scope=Scope.REQUEST)
|
||||
def get_collection_repository(self, session: AsyncSession) -> ICollectionRepository:
|
||||
return PostgreSQLCollectionRepository(session)
|
||||
|
||||
@provide(scope=Scope.REQUEST)
|
||||
def get_document_repository(self, session: AsyncSession) -> IDocumentRepository:
|
||||
return PostgreSQLDocumentRepository(session)
|
||||
|
||||
@provide(scope=Scope.REQUEST)
|
||||
def get_conversation_repository(self, session: AsyncSession) -> IConversationRepository:
|
||||
return PostgreSQLConversationRepository(session)
|
||||
|
||||
@provide(scope=Scope.REQUEST)
|
||||
def get_message_repository(self, session: AsyncSession) -> IMessageRepository:
|
||||
return PostgreSQLMessageRepository(session)
|
||||
|
||||
@provide(scope=Scope.REQUEST)
|
||||
def get_collection_access_repository(self, session: AsyncSession) -> ICollectionAccessRepository:
|
||||
return PostgreSQLCollectionAccessRepository(session)
|
||||
|
||||
|
||||
class ServiceProvider(Provider):
|
||||
@provide(scope=Scope.APP)
|
||||
def get_ocr_service(self) -> YandexOCRService:
|
||||
return YandexOCRService()
|
||||
|
||||
@provide(scope=Scope.APP)
|
||||
def get_deepseek_client(self) -> DeepSeekClient:
|
||||
return DeepSeekClient()
|
||||
|
||||
@provide(scope=Scope.APP)
|
||||
def get_parser_service(self, ocr_service: YandexOCRService) -> DocumentParserService:
|
||||
return DocumentParserService(ocr_service)
|
||||
|
||||
|
||||
class AuthProvider(Provider):
|
||||
@provide(scope=Scope.REQUEST)
|
||||
async def get_current_user(self, request: Request, user_repo: IUserRepository) -> User:
|
||||
from src.presentation.middleware.auth_middleware import get_current_user
|
||||
return await get_current_user(request, user_repo)
|
||||
|
||||
|
||||
class UseCaseProvider(Provider):
|
||||
@provide(scope=Scope.REQUEST)
|
||||
def get_user_use_cases(
|
||||
self,
|
||||
user_repo: IUserRepository
|
||||
) -> UserUseCases:
|
||||
return UserUseCases(user_repo)
|
||||
|
||||
@provide(scope=Scope.REQUEST)
|
||||
def get_collection_use_cases(
|
||||
self,
|
||||
collection_repo: ICollectionRepository,
|
||||
access_repo: ICollectionAccessRepository,
|
||||
user_repo: IUserRepository
|
||||
) -> CollectionUseCases:
|
||||
return CollectionUseCases(collection_repo, access_repo, user_repo)
|
||||
|
||||
@provide(scope=Scope.REQUEST)
|
||||
def get_document_use_cases(
|
||||
self,
|
||||
document_repo: IDocumentRepository,
|
||||
collection_repo: ICollectionRepository,
|
||||
parser_service: DocumentParserService
|
||||
) -> DocumentUseCases:
|
||||
return DocumentUseCases(document_repo, collection_repo, parser_service)
|
||||
|
||||
@provide(scope=Scope.REQUEST)
|
||||
def get_conversation_use_cases(
|
||||
self,
|
||||
conversation_repo: IConversationRepository,
|
||||
collection_repo: ICollectionRepository,
|
||||
access_repo: ICollectionAccessRepository
|
||||
) -> ConversationUseCases:
|
||||
return ConversationUseCases(conversation_repo, collection_repo, access_repo)
|
||||
|
||||
@provide(scope=Scope.REQUEST)
|
||||
def get_message_use_cases(
|
||||
self,
|
||||
message_repo: IMessageRepository,
|
||||
conversation_repo: IConversationRepository
|
||||
) -> MessageUseCases:
|
||||
return MessageUseCases(message_repo, conversation_repo)
|
||||
|
||||
|
||||
def create_container() -> Container:
|
||||
container = Container()
|
||||
container.add_provider(DatabaseProvider())
|
||||
container.add_provider(RepositoryProvider())
|
||||
container.add_provider(ServiceProvider())
|
||||
container.add_provider(AuthProvider())
|
||||
container.add_provider(UseCaseProvider())
|
||||
return container
|
||||
|
||||
Reference in New Issue
Block a user