presentation layer

This commit is contained in:
bokho 2025-12-14 22:57:54 +03:00
parent 4a043f8e70
commit 036741c0bf
59 changed files with 3226 additions and 1865 deletions

View File

@ -0,0 +1,4 @@
"""
Application services
"""

View File

@ -0,0 +1,72 @@
"""
Сервис парсинга документов
"""
from typing import BinaryIO
from src.infrastructure.external.yandex_ocr import YandexOCRService, YandexOCRError
class DocumentParserService:
"""Сервис для парсинга документов"""
def __init__(self, ocr_service: YandexOCRService):
self.ocr_service = ocr_service
async def parse_pdf(self, file: BinaryIO, filename: str) -> tuple[str, str]:
"""
Парсинг PDF файла
Args:
file: Файловый объект
filename: Имя файла
Returns:
Кортеж (title, content)
Raises:
YandexOCRError: При ошибке распознавания
"""
try:
content = await self.ocr_service.parse_pdf(file)
title = filename.rsplit(".", 1)[0] if "." in filename else filename
if not content or not content.strip() or content.startswith("Ошибка распознавания:"):
if not content or content.startswith("Ошибка распознавания:"):
pass
else:
content = f"Документ {filename} загружен, но текст не был распознан."
return title, content
except YandexOCRError as e:
title = filename.rsplit(".", 1)[0] if "." in filename else filename
content = f" Ошибка распознавания документа: {str(e)}"
return title, content
except Exception as e:
title = filename.rsplit(".", 1)[0] if "." in filename else filename
content = f" Ошибка при парсинге документа: {str(e)}"
return title, content
async def parse_image(self, file: BinaryIO, filename: str) -> tuple[str, str]:
"""
Парсинг изображения
Args:
file: Файловый объект изображения
filename: Имя файла
Returns:
Кортеж (title, content)
"""
try:
content = await self.ocr_service.parse_image(file)
title = filename.rsplit(".", 1)[0] if "." in filename else filename
if not content or not content.strip():
content = f"Изображение {filename} загружено, но текст не был распознан."
return title, content
except YandexOCRError:
raise
except Exception as e:
raise YandexOCRError(f"Ошибка при парсинге изображения: {str(e)}") from e

View File

@ -0,0 +1,4 @@
"""
Application use cases
"""

View File

@ -0,0 +1,141 @@
"""
Use cases для работы с коллекциями
"""
from uuid import UUID
from typing import Optional
from src.domain.entities.collection import Collection
from src.domain.entities.collection_access import CollectionAccess
from src.domain.repositories.collection_repository import ICollectionRepository
from src.domain.repositories.collection_access_repository import ICollectionAccessRepository
from src.domain.repositories.user_repository import IUserRepository
from src.shared.exceptions import NotFoundError, ForbiddenError
class CollectionUseCases:
"""Use cases для коллекций"""
def __init__(
self,
collection_repository: ICollectionRepository,
access_repository: ICollectionAccessRepository,
user_repository: IUserRepository
):
self.collection_repository = collection_repository
self.access_repository = access_repository
self.user_repository = user_repository
async def create_collection(
self,
name: str,
owner_id: UUID,
description: str = "",
is_public: bool = False
) -> Collection:
"""Создать коллекцию"""
owner = await self.user_repository.get_by_id(owner_id)
if not owner:
raise NotFoundError(f"Пользователь {owner_id} не найден")
collection = Collection(
name=name,
owner_id=owner_id,
description=description,
is_public=is_public
)
return await self.collection_repository.create(collection)
async def get_collection(self, collection_id: UUID) -> Collection:
"""Получить коллекцию по ID"""
collection = await self.collection_repository.get_by_id(collection_id)
if not collection:
raise NotFoundError(f"Коллекция {collection_id} не найдена")
return collection
async def update_collection(
self,
collection_id: UUID,
user_id: UUID,
name: str | None = None,
description: str | None = None,
is_public: bool | None = None
) -> Collection:
"""Обновить коллекцию"""
collection = await self.get_collection(collection_id)
if collection.owner_id != user_id:
raise ForbiddenError("Только владелец может изменять коллекцию")
if name is not None:
collection.name = name
if description is not None:
collection.description = description
if is_public is not None:
collection.is_public = is_public
return await self.collection_repository.update(collection)
async def delete_collection(self, collection_id: UUID, user_id: UUID) -> bool:
"""Удалить коллекцию"""
collection = await self.get_collection(collection_id)
if collection.owner_id != user_id:
raise ForbiddenError("Только владелец может удалять коллекцию")
return await self.collection_repository.delete(collection_id)
async def grant_access(self, collection_id: UUID, user_id: UUID, owner_id: UUID) -> CollectionAccess:
"""Предоставить доступ пользователю к коллекции"""
collection = await self.get_collection(collection_id)
if collection.owner_id != owner_id:
raise ForbiddenError("Только владелец может предоставлять доступ")
user = await self.user_repository.get_by_id(user_id)
if not user:
raise NotFoundError(f"Пользователь {user_id} не найден")
existing_access = await self.access_repository.get_by_user_and_collection(user_id, collection_id)
if existing_access:
return existing_access
access = CollectionAccess(user_id=user_id, collection_id=collection_id)
return await self.access_repository.create(access)
async def revoke_access(self, collection_id: UUID, user_id: UUID, owner_id: UUID) -> bool:
"""Отозвать доступ пользователя к коллекции"""
collection = await self.get_collection(collection_id)
if collection.owner_id != owner_id:
raise ForbiddenError("Только владелец может отзывать доступ")
return await self.access_repository.delete_by_user_and_collection(user_id, collection_id)
async def check_access(self, collection_id: UUID, user_id: UUID) -> bool:
"""Проверить доступ пользователя к коллекции"""
collection = await self.get_collection(collection_id)
if collection.owner_id == user_id:
return True
if collection.is_public:
return True
access = await self.access_repository.get_by_user_and_collection(user_id, collection_id)
return access is not None
async def list_user_collections(self, user_id: UUID, skip: int = 0, limit: int = 100) -> list[Collection]:
"""Получить коллекции, доступные пользователю"""
owned = await self.collection_repository.list_by_owner(user_id, skip=skip, limit=limit)
public = await self.collection_repository.list_public(skip=skip, limit=limit)
accesses = await self.access_repository.list_by_user(user_id)
accessed_collections = []
for access in accesses:
collection = await self.collection_repository.get_by_id(access.collection_id)
if collection:
accessed_collections.append(collection)
all_collections = {c.collection_id: c for c in owned + public + accessed_collections}
return list(all_collections.values())[skip:skip+limit]

View File

@ -0,0 +1,68 @@
"""
Use cases для работы с беседами
"""
from uuid import UUID
from src.domain.entities.conversation import Conversation
from src.domain.repositories.conversation_repository import IConversationRepository
from src.domain.repositories.collection_repository import ICollectionRepository
from src.domain.repositories.collection_access_repository import ICollectionAccessRepository
from src.shared.exceptions import NotFoundError, ForbiddenError
class ConversationUseCases:
"""Use cases для бесед"""
def __init__(
self,
conversation_repository: IConversationRepository,
collection_repository: ICollectionRepository,
access_repository: ICollectionAccessRepository
):
self.conversation_repository = conversation_repository
self.collection_repository = collection_repository
self.access_repository = access_repository
async def create_conversation(self, user_id: UUID, collection_id: UUID) -> Conversation:
"""Создать беседу"""
collection = await self.collection_repository.get_by_id(collection_id)
if not collection:
raise NotFoundError(f"Коллекция {collection_id} не найдена")
has_access = await self._check_collection_access(user_id, collection)
if not has_access:
raise ForbiddenError("Нет доступа к коллекции")
conversation = Conversation(user_id=user_id, collection_id=collection_id)
return await self.conversation_repository.create(conversation)
async def get_conversation(self, conversation_id: UUID, user_id: UUID) -> Conversation:
"""Получить беседу по ID"""
conversation = await self.conversation_repository.get_by_id(conversation_id)
if not conversation:
raise NotFoundError(f"Беседа {conversation_id} не найдена")
if conversation.user_id != user_id:
raise ForbiddenError("Нет доступа к этой беседе")
return conversation
async def delete_conversation(self, conversation_id: UUID, user_id: UUID) -> bool:
"""Удалить беседу"""
conversation = await self.get_conversation(conversation_id, user_id)
return await self.conversation_repository.delete(conversation_id)
async def list_user_conversations(self, user_id: UUID, skip: int = 0, limit: int = 100) -> list[Conversation]:
"""Получить беседы пользователя"""
return await self.conversation_repository.list_by_user(user_id, skip=skip, limit=limit)
async def _check_collection_access(self, user_id: UUID, collection) -> bool:
"""Проверить доступ пользователя к коллекции"""
if collection.owner_id == user_id:
return True
if collection.is_public:
return True
access = await self.access_repository.get_by_user_and_collection(user_id, collection.collection_id)
return access is not None

View File

@ -0,0 +1,119 @@
"""
Use cases для работы с документами
"""
from uuid import UUID
from typing import BinaryIO, Optional
from src.domain.entities.document import Document
from src.domain.repositories.document_repository import IDocumentRepository
from src.domain.repositories.collection_repository import ICollectionRepository
from src.application.services.document_parser_service import DocumentParserService
from src.shared.exceptions import NotFoundError, ForbiddenError
class DocumentUseCases:
"""Use cases для документов"""
def __init__(
self,
document_repository: IDocumentRepository,
collection_repository: ICollectionRepository,
parser_service: DocumentParserService
):
self.document_repository = document_repository
self.collection_repository = collection_repository
self.parser_service = parser_service
async def create_document(
self,
collection_id: UUID,
title: str,
content: str,
metadata: dict | None = None
) -> Document:
"""Создать документ"""
collection = await self.collection_repository.get_by_id(collection_id)
if not collection:
raise NotFoundError(f"Коллекция {collection_id} не найдена")
document = Document(
collection_id=collection_id,
title=title,
content=content,
metadata=metadata or {}
)
return await self.document_repository.create(document)
async def upload_and_parse_document(
self,
collection_id: UUID,
file: BinaryIO,
filename: str,
user_id: UUID
) -> Document:
"""Загрузить и распарсить документ"""
collection = await self.collection_repository.get_by_id(collection_id)
if not collection:
raise NotFoundError(f"Коллекция {collection_id} не найдена")
if collection.owner_id != user_id:
raise ForbiddenError("Только владелец может добавлять документы")
title, content = await self.parser_service.parse_pdf(file, filename)
document = Document(
collection_id=collection_id,
title=title,
content=content,
metadata={"filename": filename}
)
return await self.document_repository.create(document)
async def get_document(self, document_id: UUID) -> Document:
"""Получить документ по ID"""
document = await self.document_repository.get_by_id(document_id)
if not document:
raise NotFoundError(f"Документ {document_id} не найден")
return document
async def update_document(
self,
document_id: UUID,
user_id: UUID,
title: str | None = None,
content: str | None = None,
metadata: dict | None = None
) -> Document:
"""Обновить документ"""
document = await self.get_document(document_id)
collection = await self.collection_repository.get_by_id(document.collection_id)
if not collection or collection.owner_id != user_id:
raise ForbiddenError("Только владелец коллекции может изменять документы")
if title is not None:
document.title = title
if content is not None:
document.content = content
if metadata is not None:
document.metadata = metadata
return await self.document_repository.update(document)
async def delete_document(self, document_id: UUID, user_id: UUID) -> bool:
"""Удалить документ"""
document = await self.get_document(document_id)
collection = await self.collection_repository.get_by_id(document.collection_id)
if not collection or collection.owner_id != user_id:
raise ForbiddenError("Только владелец коллекции может удалять документы")
return await self.document_repository.delete(document_id)
async def list_collection_documents(self, collection_id: UUID, skip: int = 0, limit: int = 100) -> list[Document]:
"""Получить документы коллекции"""
collection = await self.collection_repository.get_by_id(collection_id)
if not collection:
raise NotFoundError(f"Коллекция {collection_id} не найдена")
return await self.document_repository.list_by_collection(collection_id, skip=skip, limit=limit)

View File

@ -0,0 +1,93 @@
"""
Use cases для работы с сообщениями
"""
from uuid import UUID
from src.domain.entities.message import Message, MessageRole
from src.domain.repositories.message_repository import IMessageRepository
from src.domain.repositories.conversation_repository import IConversationRepository
from src.shared.exceptions import NotFoundError, ForbiddenError
class MessageUseCases:
"""Use cases для сообщений"""
def __init__(
self,
message_repository: IMessageRepository,
conversation_repository: IConversationRepository
):
self.message_repository = message_repository
self.conversation_repository = conversation_repository
async def create_message(
self,
conversation_id: UUID,
content: str,
role: MessageRole,
user_id: UUID,
sources: dict | None = None
) -> Message:
"""Создать сообщение"""
conversation = await self.conversation_repository.get_by_id(conversation_id)
if not conversation:
raise NotFoundError(f"Беседа {conversation_id} не найдена")
if conversation.user_id != user_id:
raise ForbiddenError("Нет доступа к этой беседе")
message = Message(
conversation_id=conversation_id,
content=content,
role=role,
sources=sources or {}
)
conversation.update_timestamp()
await self.conversation_repository.update(conversation)
return await self.message_repository.create(message)
async def get_message(self, message_id: UUID) -> Message:
"""Получить сообщение по ID"""
message = await self.message_repository.get_by_id(message_id)
if not message:
raise NotFoundError(f"Сообщение {message_id} не найдено")
return message
async def update_message(
self,
message_id: UUID,
content: str | None = None,
sources: dict | None = None
) -> Message:
"""Обновить сообщение"""
message = await self.get_message(message_id)
if content is not None:
message.content = content
if sources is not None:
message.sources = sources
return await self.message_repository.update(message)
async def delete_message(self, message_id: UUID) -> bool:
"""Удалить сообщение"""
return await self.message_repository.delete(message_id)
async def list_conversation_messages(
self,
conversation_id: UUID,
user_id: UUID,
skip: int = 0,
limit: int = 100
) -> list[Message]:
"""Получить сообщения беседы"""
conversation = await self.conversation_repository.get_by_id(conversation_id)
if not conversation:
raise NotFoundError(f"Беседа {conversation_id} не найдена")
if conversation.user_id != user_id:
raise ForbiddenError("Нет доступа к этой беседе")
return await self.message_repository.list_by_conversation(conversation_id, skip=skip, limit=limit)

View File

@ -0,0 +1,55 @@
"""
Use cases для работы с пользователями
"""
from uuid import UUID
from typing import Optional
from src.domain.entities.user import User, UserRole
from src.domain.repositories.user_repository import IUserRepository
from src.shared.exceptions import NotFoundError, ValidationError
class UserUseCases:
"""Use cases для пользователей"""
def __init__(self, user_repository: IUserRepository):
self.user_repository = user_repository
async def create_user(self, telegram_id: str, role: UserRole = UserRole.USER) -> User:
"""Создать пользователя"""
existing_user = await self.user_repository.get_by_telegram_id(telegram_id)
if existing_user:
raise ValidationError(f"Пользователь с telegram_id {telegram_id} уже существует")
user = User(telegram_id=telegram_id, role=role)
return await self.user_repository.create(user)
async def get_user(self, user_id: UUID) -> User:
"""Получить пользователя по ID"""
user = await self.user_repository.get_by_id(user_id)
if not user:
raise NotFoundError(f"Пользователь {user_id} не найден")
return user
async def get_user_by_telegram_id(self, telegram_id: str) -> Optional[User]:
"""Получить пользователя по Telegram ID"""
return await self.user_repository.get_by_telegram_id(telegram_id)
async def update_user(self, user_id: UUID, telegram_id: str | None = None, role: UserRole | None = None) -> User:
"""Обновить пользователя"""
user = await self.get_user(user_id)
if telegram_id is not None:
user.telegram_id = telegram_id
if role is not None:
user.role = role
return await self.user_repository.update(user)
async def delete_user(self, user_id: UUID) -> bool:
"""Удалить пользователя"""
return await self.user_repository.delete(user_id)
async def list_users(self, skip: int = 0, limit: int = 100) -> list[User]:
"""Получить список пользователей"""
return await self.user_repository.list_all(skip=skip, limit=limit)

View File

@ -0,0 +1,4 @@
"""
API v1 роутеры
"""

View File

@ -0,0 +1,56 @@
"""
Админ-панель - упрощенная версия через API эндпоинты
В будущем можно интегрировать полноценную админ-панель
"""
from fastapi import APIRouter, HTTPException
from typing import List
from uuid import UUID
from dishka.integrations.fastapi import FromDishka
from src.presentation.schemas.user_schemas import UserResponse
from src.presentation.schemas.collection_schemas import CollectionResponse
from src.presentation.schemas.document_schemas import DocumentResponse
from src.presentation.schemas.conversation_schemas import ConversationResponse
from src.presentation.schemas.message_schemas import MessageResponse
from src.domain.entities.user import User, UserRole
from src.application.use_cases.user_use_cases import UserUseCases
from src.application.use_cases.collection_use_cases import CollectionUseCases
router = APIRouter(prefix="/admin", tags=["admin"])
@router.get("/users", response_model=List[UserResponse])
async def admin_list_users(
skip: int = 0,
limit: int = 100,
current_user: FromDishka[User] = FromDishka(),
use_cases: FromDishka[UserUseCases] = FromDishka()
):
"""Получить список всех пользователей (только для админов)"""
if not current_user.is_admin():
raise HTTPException(status_code=403, detail="Требуются права администратора")
users = await use_cases.list_users(skip=skip, limit=limit)
return [UserResponse.from_entity(user) for user in users]
@router.get("/collections", response_model=List[CollectionResponse])
async def admin_list_collections(
skip: int = 0,
limit: int = 100,
current_user: FromDishka[User] = FromDishka(),
use_cases: FromDishka[CollectionUseCases] = FromDishka()
):
"""Получить список всех коллекций (только для админов)"""
from src.infrastructure.database.base import AsyncSessionLocal
from src.infrastructure.repositories.postgresql.collection_repository import PostgreSQLCollectionRepository
from sqlalchemy import select
from src.infrastructure.database.models import CollectionModel
async with AsyncSessionLocal() as session:
repo = PostgreSQLCollectionRepository(session)
result = await session.execute(
select(CollectionModel).offset(skip).limit(limit)
)
db_collections = result.scalars().all()
collections = [repo._to_entity(c) for c in db_collections if c]
return [CollectionResponse.from_entity(c) for c in collections if c]

View File

@ -0,0 +1,120 @@
"""
API роутеры для работы с коллекциями
"""
from uuid import UUID
from fastapi import APIRouter, status
from fastapi.responses import JSONResponse
from typing import List
from dishka.integrations.fastapi import FromDishka
from src.presentation.schemas.collection_schemas import (
CollectionCreate,
CollectionUpdate,
CollectionResponse,
CollectionAccessGrant,
CollectionAccessResponse
)
from src.application.use_cases.collection_use_cases import CollectionUseCases
from src.domain.entities.user import User
from src.presentation.middleware.auth_middleware import get_current_user
router = APIRouter(prefix="/collections", tags=["collections"])
@router.post("", response_model=CollectionResponse, status_code=status.HTTP_201_CREATED)
async def create_collection(
collection_data: CollectionCreate,
current_user: User = FromDishka(),
use_cases: FromDishka[CollectionUseCases] = FromDishka()
):
"""Создать коллекцию"""
collection = await use_cases.create_collection(
name=collection_data.name,
owner_id=current_user.user_id,
description=collection_data.description,
is_public=collection_data.is_public
)
return CollectionResponse.from_entity(collection)
@router.get("/{collection_id}", response_model=CollectionResponse)
async def get_collection(
collection_id: UUID,
use_cases: FromDishka[CollectionUseCases] = FromDishka()
):
"""Получить коллекцию по ID"""
collection = await use_cases.get_collection(collection_id)
return CollectionResponse.from_entity(collection)
@router.put("/{collection_id}", response_model=CollectionResponse)
async def update_collection(
collection_id: UUID,
collection_data: CollectionUpdate,
current_user: User = FromDishka(),
use_cases: FromDishka[CollectionUseCases] = FromDishka()
):
"""Обновить коллекцию"""
collection = await use_cases.update_collection(
collection_id=collection_id,
user_id=current_user.user_id,
name=collection_data.name,
description=collection_data.description,
is_public=collection_data.is_public
)
return CollectionResponse.from_entity(collection)
@router.delete("/{collection_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_collection(
collection_id: UUID,
current_user: User = FromDishka(),
use_cases: FromDishka[CollectionUseCases] = FromDishka()
):
"""Удалить коллекцию"""
await use_cases.delete_collection(collection_id, current_user.user_id)
return JSONResponse(status_code=status.HTTP_204_NO_CONTENT, content=None)
@router.get("", response_model=List[CollectionResponse])
async def list_collections(
skip: int = 0,
limit: int = 100,
current_user: User = FromDishka(),
use_cases: FromDishka[CollectionUseCases] = FromDishka()
):
"""Получить список коллекций, доступных пользователю"""
collections = await use_cases.list_user_collections(
user_id=current_user.user_id,
skip=skip,
limit=limit
)
return [CollectionResponse.from_entity(c) for c in collections]
@router.post("/{collection_id}/access", response_model=CollectionAccessResponse, status_code=status.HTTP_201_CREATED)
async def grant_access(
collection_id: UUID,
access_data: CollectionAccessGrant,
current_user: User = FromDishka(),
use_cases: FromDishka[CollectionUseCases] = FromDishka()
):
"""Предоставить доступ пользователю к коллекции"""
access = await use_cases.grant_access(
collection_id=collection_id,
user_id=access_data.user_id,
owner_id=current_user.user_id
)
return CollectionAccessResponse.from_entity(access)
@router.delete("/{collection_id}/access/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
async def revoke_access(
collection_id: UUID,
user_id: UUID,
current_user: User = FromDishka(),
use_cases: FromDishka[CollectionUseCases] = FromDishka()
):
"""Отозвать доступ пользователя к коллекции"""
await use_cases.revoke_access(collection_id, user_id, current_user.user_id)
return JSONResponse(status_code=status.HTTP_204_NO_CONTENT, content=None)

View File

@ -0,0 +1,69 @@
"""
API роутеры для работы с беседами
"""
from uuid import UUID
from fastapi import APIRouter, status
from fastapi.responses import JSONResponse
from typing import List
from dishka.integrations.fastapi import FromDishka
from src.presentation.schemas.conversation_schemas import (
ConversationCreate,
ConversationResponse
)
from src.application.use_cases.conversation_use_cases import ConversationUseCases
from src.domain.entities.user import User
router = APIRouter(prefix="/conversations", tags=["conversations"])
@router.post("", response_model=ConversationResponse, status_code=status.HTTP_201_CREATED)
async def create_conversation(
conversation_data: ConversationCreate,
current_user: FromDishka[User] = FromDishka(),
use_cases: FromDishka[ConversationUseCases] = FromDishka()
):
"""Создать беседу"""
conversation = await use_cases.create_conversation(
user_id=current_user.user_id,
collection_id=conversation_data.collection_id
)
return ConversationResponse.from_entity(conversation)
@router.get("/{conversation_id}", response_model=ConversationResponse)
async def get_conversation(
conversation_id: UUID,
current_user: FromDishka[User] = FromDishka(),
use_cases: FromDishka[ConversationUseCases] = FromDishka()
):
"""Получить беседу по ID"""
conversation = await use_cases.get_conversation(conversation_id, current_user.user_id)
return ConversationResponse.from_entity(conversation)
@router.delete("/{conversation_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_conversation(
conversation_id: UUID,
current_user: FromDishka[User] = FromDishka(),
use_cases: FromDishka[ConversationUseCases] = FromDishka()
):
"""Удалить беседу"""
await use_cases.delete_conversation(conversation_id, current_user.user_id)
return JSONResponse(status_code=status.HTTP_204_NO_CONTENT, content=None)
@router.get("", response_model=List[ConversationResponse])
async def list_conversations(
skip: int = 0,
limit: int = 100,
current_user: FromDishka[User] = FromDishka(),
use_cases: FromDishka[ConversationUseCases] = FromDishka()
):
"""Получить список бесед пользователя"""
conversations = await use_cases.list_user_conversations(
user_id=current_user.user_id,
skip=skip,
limit=limit
)
return [ConversationResponse.from_entity(c) for c in conversations]

View File

@ -0,0 +1,121 @@
"""
API роутеры для работы с документами
"""
from uuid import UUID
from fastapi import APIRouter, status, UploadFile, File
from fastapi.responses import JSONResponse
from typing import List
from dishka.integrations.fastapi import FromDishka
from src.presentation.schemas.document_schemas import (
DocumentCreate,
DocumentUpdate,
DocumentResponse
)
from src.application.use_cases.document_use_cases import DocumentUseCases
from src.domain.entities.user import User
router = APIRouter(prefix="/documents", tags=["documents"])
@router.post("", response_model=DocumentResponse, status_code=status.HTTP_201_CREATED)
async def create_document(
document_data: DocumentCreate,
current_user: FromDishka[User] = FromDishka(),
use_cases: FromDishka[DocumentUseCases] = FromDishka()
):
"""Создать документ"""
document = await use_cases.create_document(
collection_id=document_data.collection_id,
title=document_data.title,
content=document_data.content,
metadata=document_data.metadata
)
return DocumentResponse.from_entity(document)
@router.post("/upload", response_model=DocumentResponse, status_code=status.HTTP_201_CREATED)
async def upload_document(
collection_id: UUID,
file: UploadFile = File(...),
current_user: FromDishka[User] = FromDishka(),
use_cases: FromDishka[DocumentUseCases] = FromDishka()
):
"""Загрузить и распарсить PDF документ или изображение"""
if not file.filename:
raise JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={"detail": "Имя файла не указано"}
)
supported_formats = ['.pdf', '.png', '.jpg', '.jpeg', '.tiff', '.bmp']
file_ext = file.filename.lower().rsplit('.', 1)[-1] if '.' in file.filename else ''
if f'.{file_ext}' not in supported_formats:
raise JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={"detail": f"Неподдерживаемый формат файла. Поддерживаются: {', '.join(supported_formats)}"}
)
document = await use_cases.upload_and_parse_document(
collection_id=collection_id,
file=file.file,
filename=file.filename,
user_id=current_user.user_id
)
return DocumentResponse.from_entity(document)
@router.get("/{document_id}", response_model=DocumentResponse)
async def get_document(
document_id: UUID,
use_cases: FromDishka[DocumentUseCases] = FromDishka()
):
"""Получить документ по ID"""
document = await use_cases.get_document(document_id)
return DocumentResponse.from_entity(document)
@router.put("/{document_id}", response_model=DocumentResponse)
async def update_document(
document_id: UUID,
document_data: DocumentUpdate,
current_user: FromDishka[User] = FromDishka(),
use_cases: FromDishka[DocumentUseCases] = FromDishka()
):
"""Обновить документ"""
document = await use_cases.update_document(
document_id=document_id,
user_id=current_user.user_id,
title=document_data.title,
content=document_data.content,
metadata=document_data.metadata
)
return DocumentResponse.from_entity(document)
@router.delete("/{document_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_document(
document_id: UUID,
current_user: FromDishka[User] = FromDishka(),
use_cases: FromDishka[DocumentUseCases] = FromDishka()
):
"""Удалить документ"""
await use_cases.delete_document(document_id, current_user.user_id)
return JSONResponse(status_code=status.HTTP_204_NO_CONTENT, content=None)
@router.get("/collection/{collection_id}", response_model=List[DocumentResponse])
async def list_collection_documents(
collection_id: UUID,
skip: int = 0,
limit: int = 100,
use_cases: FromDishka[DocumentUseCases] = FromDishka()
):
"""Получить документы коллекции"""
documents = await use_cases.list_collection_documents(
collection_id=collection_id,
skip=skip,
limit=limit
)
return [DocumentResponse.from_entity(d) for d in documents]

View File

@ -0,0 +1,88 @@
"""
API роутеры для работы с сообщениями
"""
from uuid import UUID
from fastapi import APIRouter, status
from fastapi.responses import JSONResponse
from typing import List
from dishka.integrations.fastapi import FromDishka
from src.presentation.schemas.message_schemas import (
MessageCreate,
MessageUpdate,
MessageResponse
)
from src.application.use_cases.message_use_cases import MessageUseCases
from src.domain.entities.user import User
router = APIRouter(prefix="/messages", tags=["messages"])
@router.post("", response_model=MessageResponse, status_code=status.HTTP_201_CREATED)
async def create_message(
message_data: MessageCreate,
current_user: FromDishka[User] = FromDishka(),
use_cases: FromDishka[MessageUseCases] = FromDishka()
):
"""Создать сообщение"""
message = await use_cases.create_message(
conversation_id=message_data.conversation_id,
content=message_data.content,
role=message_data.role,
user_id=current_user.user_id,
sources=message_data.sources
)
return MessageResponse.from_entity(message)
@router.get("/{message_id}", response_model=MessageResponse)
async def get_message(
message_id: UUID,
use_cases: FromDishka[MessageUseCases] = FromDishka()
):
"""Получить сообщение по ID"""
message = await use_cases.get_message(message_id)
return MessageResponse.from_entity(message)
@router.put("/{message_id}", response_model=MessageResponse)
async def update_message(
message_id: UUID,
message_data: MessageUpdate,
use_cases: FromDishka[MessageUseCases] = FromDishka()
):
"""Обновить сообщение"""
message = await use_cases.update_message(
message_id=message_id,
content=message_data.content,
sources=message_data.sources
)
return MessageResponse.from_entity(message)
@router.delete("/{message_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_message(
message_id: UUID,
use_cases: FromDishka[MessageUseCases] = FromDishka()
):
"""Удалить сообщение"""
await use_cases.delete_message(message_id)
return JSONResponse(status_code=status.HTTP_204_NO_CONTENT, content=None)
@router.get("/conversation/{conversation_id}", response_model=List[MessageResponse])
async def list_conversation_messages(
conversation_id: UUID,
skip: int = 0,
limit: int = 100,
current_user: FromDishka[User] = FromDishka(),
use_cases: FromDishka[MessageUseCases] = FromDishka()
):
"""Получить сообщения беседы"""
messages = await use_cases.list_conversation_messages(
conversation_id=conversation_id,
user_id=current_user.user_id,
skip=skip,
limit=limit
)
return [MessageResponse.from_entity(m) for m in messages]

View File

@ -0,0 +1,81 @@
"""
API роутеры для работы с пользователями
"""
from uuid import UUID
from fastapi import APIRouter, status
from fastapi.responses import JSONResponse
from typing import List
from dishka.integrations.fastapi import FromDishka
from src.presentation.schemas.user_schemas import UserCreate, UserUpdate, UserResponse
from src.application.use_cases.user_use_cases import UserUseCases
from src.domain.entities.user import User
router = APIRouter(prefix="/users", tags=["users"])
@router.post("", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
async def create_user(
user_data: UserCreate,
use_cases: FromDishka[UserUseCases] = FromDishka()
):
"""Создать пользователя"""
user = await use_cases.create_user(
telegram_id=user_data.telegram_id,
role=user_data.role
)
return UserResponse.from_entity(user)
@router.get("/me", response_model=UserResponse)
async def get_current_user_info(
current_user: FromDishka[User] = FromDishka()
):
"""Получить информацию о текущем пользователе"""
return UserResponse.from_entity(current_user)
@router.get("/{user_id}", response_model=UserResponse)
async def get_user(
user_id: UUID,
use_cases: FromDishka[UserUseCases] = FromDishka()
):
"""Получить пользователя по ID"""
user = await use_cases.get_user(user_id)
return UserResponse.from_entity(user)
@router.put("/{user_id}", response_model=UserResponse)
async def update_user(
user_id: UUID,
user_data: UserUpdate,
use_cases: FromDishka[UserUseCases] = FromDishka()
):
"""Обновить пользователя"""
user = await use_cases.update_user(
user_id=user_id,
telegram_id=user_data.telegram_id,
role=user_data.role
)
return UserResponse.from_entity(user)
@router.delete("/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_user(
user_id: UUID,
use_cases: FromDishka[UserUseCases] = FromDishka()
):
"""Удалить пользователя"""
await use_cases.delete_user(user_id)
return JSONResponse(status_code=status.HTTP_204_NO_CONTENT, content=None)
@router.get("", response_model=List[UserResponse])
async def list_users(
skip: int = 0,
limit: int = 100,
use_cases: FromDishka[UserUseCases] = FromDishka()
):
"""Получить список пользователей"""
users = await use_cases.list_users(skip=skip, limit=limit)
return [UserResponse.from_entity(user) for user in users]

View File

@ -0,0 +1,4 @@
"""
Pydantic schemas
"""

View File

@ -0,0 +1,77 @@
"""
Pydantic схемы для Collection
"""
from uuid import UUID
from datetime import datetime
from pydantic import BaseModel
class CollectionBase(BaseModel):
"""Базовая схема коллекции"""
name: str
description: str = ""
is_public: bool = False
class CollectionCreate(CollectionBase):
"""Схема создания коллекции"""
pass
class CollectionUpdate(BaseModel):
"""Схема обновления коллекции"""
name: str | None = None
description: str | None = None
is_public: bool | None = None
class CollectionResponse(BaseModel):
"""Схема ответа с коллекцией"""
collection_id: UUID
name: str
description: str
owner_id: UUID
is_public: bool
created_at: datetime
@classmethod
def from_entity(cls, collection: "Collection") -> "CollectionResponse":
"""Создать из доменной сущности"""
return cls(
collection_id=collection.collection_id,
name=collection.name,
description=collection.description,
owner_id=collection.owner_id,
is_public=collection.is_public,
created_at=collection.created_at
)
class Config:
from_attributes = True
class CollectionAccessGrant(BaseModel):
"""Схема предоставления доступа"""
user_id: UUID
class CollectionAccessResponse(BaseModel):
"""Схема ответа с доступом"""
access_id: UUID
user_id: UUID
collection_id: UUID
created_at: datetime
@classmethod
def from_entity(cls, access: "CollectionAccess") -> "CollectionAccessResponse":
"""Создать из доменной сущности"""
return cls(
access_id=access.access_id,
user_id=access.user_id,
collection_id=access.collection_id,
created_at=access.created_at
)
class Config:
from_attributes = True

View File

@ -0,0 +1,35 @@
"""
Pydantic схемы для Conversation
"""
from uuid import UUID
from datetime import datetime
from pydantic import BaseModel
class ConversationCreate(BaseModel):
"""Схема создания беседы"""
collection_id: UUID
class ConversationResponse(BaseModel):
"""Схема ответа с беседой"""
conversation_id: UUID
user_id: UUID
collection_id: UUID
created_at: datetime
updated_at: datetime
@classmethod
def from_entity(cls, conversation: "Conversation") -> "ConversationResponse":
"""Создать из доменной сущности"""
return cls(
conversation_id=conversation.conversation_id,
user_id=conversation.user_id,
collection_id=conversation.collection_id,
created_at=conversation.created_at,
updated_at=conversation.updated_at
)
class Config:
from_attributes = True

View File

@ -0,0 +1,52 @@
"""
Pydantic схемы для Document
"""
from uuid import UUID
from datetime import datetime
from typing import Any
from pydantic import BaseModel
class DocumentBase(BaseModel):
"""Базовая схема документа"""
title: str
content: str
metadata: dict[str, Any] = {}
class DocumentCreate(DocumentBase):
"""Схема создания документа"""
collection_id: UUID
class DocumentUpdate(BaseModel):
"""Схема обновления документа"""
title: str | None = None
content: str | None = None
metadata: dict[str, Any] | None = None
class DocumentResponse(BaseModel):
"""Схема ответа с документом"""
document_id: UUID
collection_id: UUID
title: str
content: str
metadata: dict[str, Any]
created_at: datetime
@classmethod
def from_entity(cls, document: "Document") -> "DocumentResponse":
"""Создать из доменной сущности"""
return cls(
document_id=document.document_id,
collection_id=document.collection_id,
title=document.title,
content=document.content,
metadata=document.metadata,
created_at=document.created_at
)
class Config:
from_attributes = True

View File

@ -0,0 +1,52 @@
"""
Pydantic схемы для Message
"""
from uuid import UUID
from datetime import datetime
from typing import Any
from pydantic import BaseModel
from src.domain.entities.message import MessageRole
class MessageBase(BaseModel):
"""Базовая схема сообщения"""
content: str
role: MessageRole
sources: dict[str, Any] = {}
class MessageCreate(MessageBase):
"""Схема создания сообщения"""
conversation_id: UUID
class MessageUpdate(BaseModel):
"""Схема обновления сообщения"""
content: str | None = None
sources: dict[str, Any] | None = None
class MessageResponse(BaseModel):
"""Схема ответа с сообщением"""
message_id: UUID
conversation_id: UUID
content: str
role: MessageRole
sources: dict[str, Any]
created_at: datetime
@classmethod
def from_entity(cls, message: "Message") -> "MessageResponse":
"""Создать из доменной сущности"""
return cls(
message_id=message.message_id,
conversation_id=message.conversation_id,
content=message.content,
role=message.role,
sources=message.sources,
created_at=message.created_at
)
class Config:
from_attributes = True

View File

@ -0,0 +1,46 @@
"""
Pydantic схемы для User
"""
from uuid import UUID
from datetime import datetime
from pydantic import BaseModel
from src.domain.entities.user import UserRole
class UserBase(BaseModel):
"""Базовая схема пользователя"""
telegram_id: str
role: UserRole
class UserCreate(UserBase):
"""Схема создания пользователя"""
pass
class UserUpdate(BaseModel):
"""Схема обновления пользователя"""
telegram_id: str | None = None
role: UserRole | None = None
class UserResponse(BaseModel):
"""Схема ответа с пользователем"""
user_id: UUID
telegram_id: str
role: UserRole
created_at: datetime
@classmethod
def from_entity(cls, user: "User") -> "UserResponse":
"""Создать из доменной сущности"""
return cls(
user_id=user.user_id,
telegram_id=user.telegram_id,
role=user.role,
created_at=user.created_at
)
class Config:
from_attributes = True