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

@ -1,13 +1,13 @@
fastapi==0.104.1
uvicorn[standard]==0.24.0
sqlalchemy[asyncio]==2.0.23
asyncpg==0.29.0
alembic==1.12.1
pydantic==2.5.0
pydantic-settings==2.1.0
python-multipart==0.0.6
httpx==0.25.2
PyMuPDF==1.23.8
Pillow==10.2.0
dishka==0.7.0
fastapi==0.104.1
uvicorn[standard]==0.24.0
sqlalchemy[asyncio]==2.0.23
asyncpg==0.29.0
alembic==1.12.1
pydantic==2.5.0
pydantic-settings==2.1.0
python-multipart==0.0.6
httpx==0.25.2
PyMuPDF==1.23.8
Pillow==10.2.0
dishka==0.7.0

View File

@ -1,4 +1,4 @@
"""
ИИ-юрист система
"""
"""
ИИ-юрист система
"""

View File

@ -1,4 +1,4 @@
"""
Application layer
"""
"""
Application layer
"""

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

@ -1,4 +1,4 @@
"""
Domain layer
"""
"""
Domain layer
"""

View File

@ -1,4 +1,4 @@
"""
Domain entities
"""
"""
Domain entities
"""

View File

@ -1,26 +1,26 @@
"""
Доменная сущность Collection
"""
from datetime import datetime
from uuid import UUID, uuid4
class Collection:
"""Каталог документов"""
def __init__(
self,
name: str,
owner_id: UUID,
description: str = "",
is_public: bool = False,
collection_id: UUID | None = None,
created_at: datetime | None = None
):
self.collection_id = collection_id or uuid4()
self.name = name
self.description = description
self.owner_id = owner_id
self.is_public = is_public
self.created_at = created_at or datetime.utcnow()
"""
Доменная сущность Collection
"""
from datetime import datetime
from uuid import UUID, uuid4
class Collection:
"""Каталог документов"""
def __init__(
self,
name: str,
owner_id: UUID,
description: str = "",
is_public: bool = False,
collection_id: UUID | None = None,
created_at: datetime | None = None
):
self.collection_id = collection_id or uuid4()
self.name = name
self.description = description
self.owner_id = owner_id
self.is_public = is_public
self.created_at = created_at or datetime.utcnow()

View File

@ -1,22 +1,22 @@
"""
Доменная сущность CollectionAccess
"""
from datetime import datetime
from uuid import UUID, uuid4
class CollectionAccess:
"""Доступ пользователя к коллекции"""
def __init__(
self,
user_id: UUID,
collection_id: UUID,
access_id: UUID | None = None,
created_at: datetime | None = None
):
self.access_id = access_id or uuid4()
self.user_id = user_id
self.collection_id = collection_id
self.created_at = created_at or datetime.utcnow()
"""
Доменная сущность CollectionAccess
"""
from datetime import datetime
from uuid import UUID, uuid4
class CollectionAccess:
"""Доступ пользователя к коллекции"""
def __init__(
self,
user_id: UUID,
collection_id: UUID,
access_id: UUID | None = None,
created_at: datetime | None = None
):
self.access_id = access_id or uuid4()
self.user_id = user_id
self.collection_id = collection_id
self.created_at = created_at or datetime.utcnow()

View File

@ -1,28 +1,28 @@
"""
Доменная сущность Conversation
"""
from datetime import datetime
from uuid import UUID, uuid4
class Conversation:
"""Беседа пользователя с ИИ"""
def __init__(
self,
user_id: UUID,
collection_id: UUID,
conversation_id: UUID | None = None,
created_at: datetime | None = None,
updated_at: datetime | None = None
):
self.conversation_id = conversation_id or uuid4()
self.user_id = user_id
self.collection_id = collection_id
self.created_at = created_at or datetime.utcnow()
self.updated_at = updated_at or datetime.utcnow()
def update_timestamp(self):
"""Обновить время последнего изменения"""
self.updated_at = datetime.utcnow()
"""
Доменная сущность Conversation
"""
from datetime import datetime
from uuid import UUID, uuid4
class Conversation:
"""Беседа пользователя с ИИ"""
def __init__(
self,
user_id: UUID,
collection_id: UUID,
conversation_id: UUID | None = None,
created_at: datetime | None = None,
updated_at: datetime | None = None
):
self.conversation_id = conversation_id or uuid4()
self.user_id = user_id
self.collection_id = collection_id
self.created_at = created_at or datetime.utcnow()
self.updated_at = updated_at or datetime.utcnow()
def update_timestamp(self):
"""Обновить время последнего изменения"""
self.updated_at = datetime.utcnow()

View File

@ -1,27 +1,27 @@
"""
Доменная сущность Document
"""
from datetime import datetime
from uuid import UUID, uuid4
from typing import Any
class Document:
"""Документ в коллекции"""
def __init__(
self,
collection_id: UUID,
title: str,
content: str,
metadata: dict[str, Any] | None = None,
document_id: UUID | None = None,
created_at: datetime | None = None
):
self.document_id = document_id or uuid4()
self.collection_id = collection_id
self.title = title
self.content = content
self.metadata = metadata or {}
self.created_at = created_at or datetime.utcnow()
"""
Доменная сущность Document
"""
from datetime import datetime
from uuid import UUID, uuid4
from typing import Any
class Document:
"""Документ в коллекции"""
def __init__(
self,
collection_id: UUID,
title: str,
content: str,
metadata: dict[str, Any] | None = None,
document_id: UUID | None = None,
created_at: datetime | None = None
):
self.document_id = document_id or uuid4()
self.collection_id = collection_id
self.title = title
self.content = content
self.metadata = metadata or {}
self.created_at = created_at or datetime.utcnow()

View File

@ -1,25 +1,25 @@
"""
Доменная сущность Embedding
"""
from datetime import datetime
from uuid import UUID, uuid4
from typing import Any
class Embedding:
"""Эмбеддинг документа"""
def __init__(
self,
document_id: UUID,
embedding: list[float] | None = None,
model_version: str = "",
embedding_id: UUID | None = None,
created_at: datetime | None = None
):
self.embedding_id = embedding_id or uuid4()
self.document_id = document_id
self.embedding = embedding or []
self.model_version = model_version
self.created_at = created_at or datetime.utcnow()
"""
Доменная сущность Embedding
"""
from datetime import datetime
from uuid import UUID, uuid4
from typing import Any
class Embedding:
"""Эмбеддинг документа"""
def __init__(
self,
document_id: UUID,
embedding: list[float] | None = None,
model_version: str = "",
embedding_id: UUID | None = None,
created_at: datetime | None = None
):
self.embedding_id = embedding_id or uuid4()
self.document_id = document_id
self.embedding = embedding or []
self.model_version = model_version
self.created_at = created_at or datetime.utcnow()

View File

@ -1,35 +1,35 @@
"""
Доменная сущность Message
"""
from datetime import datetime
from uuid import UUID, uuid4
from typing import Any
from enum import Enum
class MessageRole(str, Enum):
"""Роли сообщений"""
USER = "user"
ASSISTANT = "assistant"
SYSTEM = "system"
class Message:
"""Сообщение в беседе"""
def __init__(
self,
conversation_id: UUID,
content: str,
role: MessageRole,
sources: dict[str, Any] | None = None,
message_id: UUID | None = None,
created_at: datetime | None = None
):
self.message_id = message_id or uuid4()
self.conversation_id = conversation_id
self.content = content
self.role = role
self.sources = sources or {}
self.created_at = created_at or datetime.utcnow()
"""
Доменная сущность Message
"""
from datetime import datetime
from uuid import UUID, uuid4
from typing import Any
from enum import Enum
class MessageRole(str, Enum):
"""Роли сообщений"""
USER = "user"
ASSISTANT = "assistant"
SYSTEM = "system"
class Message:
"""Сообщение в беседе"""
def __init__(
self,
conversation_id: UUID,
content: str,
role: MessageRole,
sources: dict[str, Any] | None = None,
message_id: UUID | None = None,
created_at: datetime | None = None
):
self.message_id = message_id or uuid4()
self.conversation_id = conversation_id
self.content = content
self.role = role
self.sources = sources or {}
self.created_at = created_at or datetime.utcnow()

View File

@ -1,33 +1,33 @@
"""
Доменная сущность User
"""
from datetime import datetime
from uuid import UUID, uuid4
from enum import Enum
class UserRole(str, Enum):
"""Роли пользователей"""
USER = "user"
ADMIN = "admin"
class User:
"""Пользователь системы"""
def __init__(
self,
telegram_id: str,
role: UserRole = UserRole.USER,
user_id: UUID | None = None,
created_at: datetime | None = None
):
self.user_id = user_id or uuid4()
self.telegram_id = telegram_id
self.role = role
self.created_at = created_at or datetime.utcnow()
def is_admin(self) -> bool:
"""проверка, является ли пользователь администратором"""
return self.role == UserRole.ADMIN
"""
Доменная сущность User
"""
from datetime import datetime
from uuid import UUID, uuid4
from enum import Enum
class UserRole(str, Enum):
"""Роли пользователей"""
USER = "user"
ADMIN = "admin"
class User:
"""Пользователь системы"""
def __init__(
self,
telegram_id: str,
role: UserRole = UserRole.USER,
user_id: UUID | None = None,
created_at: datetime | None = None
):
self.user_id = user_id or uuid4()
self.telegram_id = telegram_id
self.role = role
self.created_at = created_at or datetime.utcnow()
def is_admin(self) -> bool:
"""проверка, является ли пользователь администратором"""
return self.role == UserRole.ADMIN

View File

@ -1,4 +1,4 @@
"""
Domain repositories interfaces
"""
"""
Domain repositories interfaces
"""

View File

@ -1,47 +1,47 @@
"""
Интерфейс репозитория для CollectionAccess
"""
from abc import ABC, abstractmethod
from uuid import UUID
from typing import Optional
from src.domain.entities.collection_access import CollectionAccess
class ICollectionAccessRepository(ABC):
"""Интерфейс репозитория доступа к коллекциям"""
@abstractmethod
async def create(self, access: CollectionAccess) -> CollectionAccess:
"""Создать доступ"""
pass
@abstractmethod
async def get_by_id(self, access_id: UUID) -> Optional[CollectionAccess]:
"""Получить доступ по ID"""
pass
@abstractmethod
async def delete(self, access_id: UUID) -> bool:
"""Удалить доступ"""
pass
@abstractmethod
async def delete_by_user_and_collection(self, user_id: UUID, collection_id: UUID) -> bool:
"""Удалить доступ пользователя к коллекции"""
pass
@abstractmethod
async def get_by_user_and_collection(self, user_id: UUID, collection_id: UUID) -> Optional[CollectionAccess]:
"""Получить доступ пользователя к коллекции"""
pass
@abstractmethod
async def list_by_user(self, user_id: UUID) -> list[CollectionAccess]:
"""Получить доступы пользователя"""
pass
@abstractmethod
async def list_by_collection(self, collection_id: UUID) -> list[CollectionAccess]:
"""Получить доступы к коллекции"""
pass
"""
Интерфейс репозитория для CollectionAccess
"""
from abc import ABC, abstractmethod
from uuid import UUID
from typing import Optional
from src.domain.entities.collection_access import CollectionAccess
class ICollectionAccessRepository(ABC):
"""Интерфейс репозитория доступа к коллекциям"""
@abstractmethod
async def create(self, access: CollectionAccess) -> CollectionAccess:
"""Создать доступ"""
pass
@abstractmethod
async def get_by_id(self, access_id: UUID) -> Optional[CollectionAccess]:
"""Получить доступ по ID"""
pass
@abstractmethod
async def delete(self, access_id: UUID) -> bool:
"""Удалить доступ"""
pass
@abstractmethod
async def delete_by_user_and_collection(self, user_id: UUID, collection_id: UUID) -> bool:
"""Удалить доступ пользователя к коллекции"""
pass
@abstractmethod
async def get_by_user_and_collection(self, user_id: UUID, collection_id: UUID) -> Optional[CollectionAccess]:
"""Получить доступ пользователя к коллекции"""
pass
@abstractmethod
async def list_by_user(self, user_id: UUID) -> list[CollectionAccess]:
"""Получить доступы пользователя"""
pass
@abstractmethod
async def list_by_collection(self, collection_id: UUID) -> list[CollectionAccess]:
"""Получить доступы к коллекции"""
pass

View File

@ -1,42 +1,42 @@
"""
Интерфейс репозитория для Collection
"""
from abc import ABC, abstractmethod
from uuid import UUID
from typing import Optional
from src.domain.entities.collection import Collection
class ICollectionRepository(ABC):
"""Интерфейс репозитория коллекций"""
@abstractmethod
async def create(self, collection: Collection) -> Collection:
"""Создать коллекцию"""
pass
@abstractmethod
async def get_by_id(self, collection_id: UUID) -> Optional[Collection]:
"""Получить коллекцию по ID"""
pass
@abstractmethod
async def update(self, collection: Collection) -> Collection:
"""Обновить коллекцию"""
pass
@abstractmethod
async def delete(self, collection_id: UUID) -> bool:
"""Удалить коллекцию"""
pass
@abstractmethod
async def list_by_owner(self, owner_id: UUID, skip: int = 0, limit: int = 100) -> list[Collection]:
"""Получить коллекции владельца"""
pass
@abstractmethod
async def list_public(self, skip: int = 0, limit: int = 100) -> list[Collection]:
"""Получить публичные коллекции"""
pass
"""
Интерфейс репозитория для Collection
"""
from abc import ABC, abstractmethod
from uuid import UUID
from typing import Optional
from src.domain.entities.collection import Collection
class ICollectionRepository(ABC):
"""Интерфейс репозитория коллекций"""
@abstractmethod
async def create(self, collection: Collection) -> Collection:
"""Создать коллекцию"""
pass
@abstractmethod
async def get_by_id(self, collection_id: UUID) -> Optional[Collection]:
"""Получить коллекцию по ID"""
pass
@abstractmethod
async def update(self, collection: Collection) -> Collection:
"""Обновить коллекцию"""
pass
@abstractmethod
async def delete(self, collection_id: UUID) -> bool:
"""Удалить коллекцию"""
pass
@abstractmethod
async def list_by_owner(self, owner_id: UUID, skip: int = 0, limit: int = 100) -> list[Collection]:
"""Получить коллекции владельца"""
pass
@abstractmethod
async def list_public(self, skip: int = 0, limit: int = 100) -> list[Collection]:
"""Получить публичные коллекции"""
pass

View File

@ -1,42 +1,42 @@
"""
Интерфейс репозитория для Conversation
"""
from abc import ABC, abstractmethod
from uuid import UUID
from typing import Optional
from src.domain.entities.conversation import Conversation
class IConversationRepository(ABC):
"""Интерфейс репозитория бесед"""
@abstractmethod
async def create(self, conversation: Conversation) -> Conversation:
"""Создать беседу"""
pass
@abstractmethod
async def get_by_id(self, conversation_id: UUID) -> Optional[Conversation]:
"""Получить беседу по ID"""
pass
@abstractmethod
async def update(self, conversation: Conversation) -> Conversation:
"""Обновить беседу"""
pass
@abstractmethod
async def delete(self, conversation_id: UUID) -> bool:
"""Удалить беседу"""
pass
@abstractmethod
async def list_by_user(self, user_id: UUID, skip: int = 0, limit: int = 100) -> list[Conversation]:
"""Получить беседы пользователя"""
pass
@abstractmethod
async def list_by_collection(self, collection_id: UUID, skip: int = 0, limit: int = 100) -> list[Conversation]:
"""Получить беседы по коллекции"""
pass
"""
Интерфейс репозитория для Conversation
"""
from abc import ABC, abstractmethod
from uuid import UUID
from typing import Optional
from src.domain.entities.conversation import Conversation
class IConversationRepository(ABC):
"""Интерфейс репозитория бесед"""
@abstractmethod
async def create(self, conversation: Conversation) -> Conversation:
"""Создать беседу"""
pass
@abstractmethod
async def get_by_id(self, conversation_id: UUID) -> Optional[Conversation]:
"""Получить беседу по ID"""
pass
@abstractmethod
async def update(self, conversation: Conversation) -> Conversation:
"""Обновить беседу"""
pass
@abstractmethod
async def delete(self, conversation_id: UUID) -> bool:
"""Удалить беседу"""
pass
@abstractmethod
async def list_by_user(self, user_id: UUID, skip: int = 0, limit: int = 100) -> list[Conversation]:
"""Получить беседы пользователя"""
pass
@abstractmethod
async def list_by_collection(self, collection_id: UUID, skip: int = 0, limit: int = 100) -> list[Conversation]:
"""Получить беседы по коллекции"""
pass

View File

@ -1,37 +1,37 @@
"""
Интерфейс репозитория для Document
"""
from abc import ABC, abstractmethod
from uuid import UUID
from typing import Optional
from src.domain.entities.document import Document
class IDocumentRepository(ABC):
"""Интерфейс репозитория документов"""
@abstractmethod
async def create(self, document: Document) -> Document:
"""Создать документ"""
pass
@abstractmethod
async def get_by_id(self, document_id: UUID) -> Optional[Document]:
"""Получить документ по ID"""
pass
@abstractmethod
async def update(self, document: Document) -> Document:
"""Обновить документ"""
pass
@abstractmethod
async def delete(self, document_id: UUID) -> bool:
"""Удалить документ"""
pass
@abstractmethod
async def list_by_collection(self, collection_id: UUID, skip: int = 0, limit: int = 100) -> list[Document]:
"""Получить документы коллекции"""
pass
"""
Интерфейс репозитория для Document
"""
from abc import ABC, abstractmethod
from uuid import UUID
from typing import Optional
from src.domain.entities.document import Document
class IDocumentRepository(ABC):
"""Интерфейс репозитория документов"""
@abstractmethod
async def create(self, document: Document) -> Document:
"""Создать документ"""
pass
@abstractmethod
async def get_by_id(self, document_id: UUID) -> Optional[Document]:
"""Получить документ по ID"""
pass
@abstractmethod
async def update(self, document: Document) -> Document:
"""Обновить документ"""
pass
@abstractmethod
async def delete(self, document_id: UUID) -> bool:
"""Удалить документ"""
pass
@abstractmethod
async def list_by_collection(self, collection_id: UUID, skip: int = 0, limit: int = 100) -> list[Document]:
"""Получить документы коллекции"""
pass

View File

@ -1,37 +1,37 @@
"""
Интерфейс репозитория для Message
"""
from abc import ABC, abstractmethod
from uuid import UUID
from typing import Optional
from src.domain.entities.message import Message
class IMessageRepository(ABC):
"""Интерфейс репозитория сообщений"""
@abstractmethod
async def create(self, message: Message) -> Message:
"""Создать сообщение"""
pass
@abstractmethod
async def get_by_id(self, message_id: UUID) -> Optional[Message]:
"""Получить сообщение по ID"""
pass
@abstractmethod
async def update(self, message: Message) -> Message:
"""Обновить сообщение"""
pass
@abstractmethod
async def delete(self, message_id: UUID) -> bool:
"""Удалить сообщение"""
pass
@abstractmethod
async def list_by_conversation(self, conversation_id: UUID, skip: int = 0, limit: int = 100) -> list[Message]:
"""Получить сообщения беседы"""
pass
"""
Интерфейс репозитория для Message
"""
from abc import ABC, abstractmethod
from uuid import UUID
from typing import Optional
from src.domain.entities.message import Message
class IMessageRepository(ABC):
"""Интерфейс репозитория сообщений"""
@abstractmethod
async def create(self, message: Message) -> Message:
"""Создать сообщение"""
pass
@abstractmethod
async def get_by_id(self, message_id: UUID) -> Optional[Message]:
"""Получить сообщение по ID"""
pass
@abstractmethod
async def update(self, message: Message) -> Message:
"""Обновить сообщение"""
pass
@abstractmethod
async def delete(self, message_id: UUID) -> bool:
"""Удалить сообщение"""
pass
@abstractmethod
async def list_by_conversation(self, conversation_id: UUID, skip: int = 0, limit: int = 100) -> list[Message]:
"""Получить сообщения беседы"""
pass

View File

@ -1,42 +1,42 @@
"""
Интерфейс репозитория для User
"""
from abc import ABC, abstractmethod
from uuid import UUID
from typing import Optional
from src.domain.entities.user import User
class IUserRepository(ABC):
"""Интерфейс репозитория пользователей"""
@abstractmethod
async def create(self, user: User) -> User:
"""Создать пользователя"""
pass
@abstractmethod
async def get_by_id(self, user_id: UUID) -> Optional[User]:
"""Получить пользователя по ID"""
pass
@abstractmethod
async def get_by_telegram_id(self, telegram_id: str) -> Optional[User]:
"""Получить пользователя по Telegram ID"""
pass
@abstractmethod
async def update(self, user: User) -> User:
"""Обновить пользователя"""
pass
@abstractmethod
async def delete(self, user_id: UUID) -> bool:
"""Удалить пользователя"""
pass
@abstractmethod
async def list_all(self, skip: int = 0, limit: int = 100) -> list[User]:
"""Получить список всех пользователей"""
pass
"""
Интерфейс репозитория для User
"""
from abc import ABC, abstractmethod
from uuid import UUID
from typing import Optional
from src.domain.entities.user import User
class IUserRepository(ABC):
"""Интерфейс репозитория пользователей"""
@abstractmethod
async def create(self, user: User) -> User:
"""Создать пользователя"""
pass
@abstractmethod
async def get_by_id(self, user_id: UUID) -> Optional[User]:
"""Получить пользователя по ID"""
pass
@abstractmethod
async def get_by_telegram_id(self, telegram_id: str) -> Optional[User]:
"""Получить пользователя по Telegram ID"""
pass
@abstractmethod
async def update(self, user: User) -> User:
"""Обновить пользователя"""
pass
@abstractmethod
async def delete(self, user_id: UUID) -> bool:
"""Удалить пользователя"""
pass
@abstractmethod
async def list_all(self, skip: int = 0, limit: int = 100) -> list[User]:
"""Получить список всех пользователей"""
pass

View File

@ -1,4 +1,4 @@
"""
Infrastructure layer
"""
"""
Infrastructure layer
"""

View File

@ -1,4 +1,4 @@
"""
Database infrastructure
"""
"""
Database infrastructure
"""

View File

@ -1,32 +1,32 @@
"""
Базовые настройки базы данных
"""
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from sqlalchemy.orm import declarative_base
from src.shared.config import settings
engine = create_async_engine(
settings.database_url.replace("postgresql://", "postgresql+asyncpg://"),
echo=settings.DEBUG,
future=True
)
AsyncSessionLocal = async_sessionmaker(
engine,
class_=AsyncSession,
expire_on_commit=False,
autocommit=False,
autoflush=False
)
Base = declarative_base()
async def get_db() -> AsyncSession:
"""Dependency для получения сессии БД"""
async with AsyncSessionLocal() as session:
try:
yield session
finally:
await session.close()
"""
Базовые настройки базы данных
"""
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from sqlalchemy.orm import declarative_base
from src.shared.config import settings
engine = create_async_engine(
settings.database_url.replace("postgresql://", "postgresql+asyncpg://"),
echo=settings.DEBUG,
future=True
)
AsyncSessionLocal = async_sessionmaker(
engine,
class_=AsyncSession,
expire_on_commit=False,
autocommit=False,
autoflush=False
)
Base = declarative_base()
async def get_db() -> AsyncSession:
"""Dependency для получения сессии БД"""
async with AsyncSessionLocal() as session:
try:
yield session
finally:
await session.close()

View File

@ -1,109 +1,109 @@
"""
SQLAlchemy модели для базы данных
"""
from sqlalchemy import Column, String, Text, Boolean, DateTime, ForeignKey, JSON, Integer
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
from datetime import datetime
import uuid
from src.infrastructure.database.base import Base
class UserModel(Base):
"""Модель пользователя"""
__tablename__ = "users"
user_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
telegram_id = Column(String, unique=True, nullable=False, index=True)
role = Column(String, nullable=False, default="user")
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
collections = relationship("CollectionModel", back_populates="owner", cascade="all, delete-orphan")
conversations = relationship("ConversationModel", back_populates="user", cascade="all, delete-orphan")
collection_accesses = relationship("CollectionAccessModel", back_populates="user", cascade="all, delete-orphan")
class CollectionModel(Base):
"""Модель коллекции"""
__tablename__ = "collections"
collection_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name = Column(String, nullable=False)
description = Column(Text, nullable=True)
owner_id = Column(UUID(as_uuid=True), ForeignKey("users.user_id"), nullable=False)
is_public = Column(Boolean, nullable=False, default=False)
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
owner = relationship("UserModel", back_populates="collections")
documents = relationship("DocumentModel", back_populates="collection", cascade="all, delete-orphan")
conversations = relationship("ConversationModel", back_populates="collection", cascade="all, delete-orphan")
accesses = relationship("CollectionAccessModel", back_populates="collection", cascade="all, delete-orphan")
class DocumentModel(Base):
"""Модель документа"""
__tablename__ = "documents"
document_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
collection_id = Column(UUID(as_uuid=True), ForeignKey("collections.collection_id"), nullable=False)
title = Column(String, nullable=False)
content = Column(Text, nullable=False)
document_metadata = Column("metadata", JSON, nullable=True, default={})
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
collection = relationship("CollectionModel", back_populates="documents")
embeddings = relationship("EmbeddingModel", back_populates="document", cascade="all, delete-orphan")
class EmbeddingModel(Base):
"""Модель эмбеддинга (заглушка)"""
__tablename__ = "embeddings"
embedding_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
document_id = Column(UUID(as_uuid=True), ForeignKey("documents.document_id"), nullable=False)
embedding = Column(JSON, nullable=True)
model_version = Column(String, nullable=True)
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
document = relationship("DocumentModel", back_populates="embeddings")
class ConversationModel(Base):
"""Модель беседы"""
__tablename__ = "conversations"
conversation_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), ForeignKey("users.user_id"), nullable=False)
collection_id = Column(UUID(as_uuid=True), ForeignKey("collections.collection_id"), nullable=False)
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
updated_at = Column(DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow)
user = relationship("UserModel", back_populates="conversations")
collection = relationship("CollectionModel", back_populates="conversations")
messages = relationship("MessageModel", back_populates="conversation", cascade="all, delete-orphan")
class MessageModel(Base):
"""Модель сообщения"""
__tablename__ = "messages"
message_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
conversation_id = Column(UUID(as_uuid=True), ForeignKey("conversations.conversation_id"), nullable=False)
content = Column(Text, nullable=False)
role = Column(String, nullable=False)
sources = Column(JSON, nullable=True, default={})
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
conversation = relationship("ConversationModel", back_populates="messages")
class CollectionAccessModel(Base):
"""Модель доступа к коллекции"""
__tablename__ = "collection_access"
access_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), ForeignKey("users.user_id"), nullable=False)
collection_id = Column(UUID(as_uuid=True), ForeignKey("collections.collection_id"), nullable=False)
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
user = relationship("UserModel", back_populates="collection_accesses")
collection = relationship("CollectionModel", back_populates="accesses")
__table_args__ = (
{"comment": "Уникальный доступ пользователя к коллекции"},
)
"""
SQLAlchemy модели для базы данных
"""
from sqlalchemy import Column, String, Text, Boolean, DateTime, ForeignKey, JSON, Integer
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
from datetime import datetime
import uuid
from src.infrastructure.database.base import Base
class UserModel(Base):
"""Модель пользователя"""
__tablename__ = "users"
user_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
telegram_id = Column(String, unique=True, nullable=False, index=True)
role = Column(String, nullable=False, default="user")
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
collections = relationship("CollectionModel", back_populates="owner", cascade="all, delete-orphan")
conversations = relationship("ConversationModel", back_populates="user", cascade="all, delete-orphan")
collection_accesses = relationship("CollectionAccessModel", back_populates="user", cascade="all, delete-orphan")
class CollectionModel(Base):
"""Модель коллекции"""
__tablename__ = "collections"
collection_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name = Column(String, nullable=False)
description = Column(Text, nullable=True)
owner_id = Column(UUID(as_uuid=True), ForeignKey("users.user_id"), nullable=False)
is_public = Column(Boolean, nullable=False, default=False)
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
owner = relationship("UserModel", back_populates="collections")
documents = relationship("DocumentModel", back_populates="collection", cascade="all, delete-orphan")
conversations = relationship("ConversationModel", back_populates="collection", cascade="all, delete-orphan")
accesses = relationship("CollectionAccessModel", back_populates="collection", cascade="all, delete-orphan")
class DocumentModel(Base):
"""Модель документа"""
__tablename__ = "documents"
document_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
collection_id = Column(UUID(as_uuid=True), ForeignKey("collections.collection_id"), nullable=False)
title = Column(String, nullable=False)
content = Column(Text, nullable=False)
document_metadata = Column("metadata", JSON, nullable=True, default={})
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
collection = relationship("CollectionModel", back_populates="documents")
embeddings = relationship("EmbeddingModel", back_populates="document", cascade="all, delete-orphan")
class EmbeddingModel(Base):
"""Модель эмбеддинга (заглушка)"""
__tablename__ = "embeddings"
embedding_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
document_id = Column(UUID(as_uuid=True), ForeignKey("documents.document_id"), nullable=False)
embedding = Column(JSON, nullable=True)
model_version = Column(String, nullable=True)
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
document = relationship("DocumentModel", back_populates="embeddings")
class ConversationModel(Base):
"""Модель беседы"""
__tablename__ = "conversations"
conversation_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), ForeignKey("users.user_id"), nullable=False)
collection_id = Column(UUID(as_uuid=True), ForeignKey("collections.collection_id"), nullable=False)
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
updated_at = Column(DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow)
user = relationship("UserModel", back_populates="conversations")
collection = relationship("CollectionModel", back_populates="conversations")
messages = relationship("MessageModel", back_populates="conversation", cascade="all, delete-orphan")
class MessageModel(Base):
"""Модель сообщения"""
__tablename__ = "messages"
message_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
conversation_id = Column(UUID(as_uuid=True), ForeignKey("conversations.conversation_id"), nullable=False)
content = Column(Text, nullable=False)
role = Column(String, nullable=False)
sources = Column(JSON, nullable=True, default={})
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
conversation = relationship("ConversationModel", back_populates="messages")
class CollectionAccessModel(Base):
"""Модель доступа к коллекции"""
__tablename__ = "collection_access"
access_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), ForeignKey("users.user_id"), nullable=False)
collection_id = Column(UUID(as_uuid=True), ForeignKey("collections.collection_id"), nullable=False)
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
user = relationship("UserModel", back_populates="collection_accesses")
collection = relationship("CollectionModel", back_populates="accesses")
__table_args__ = (
{"comment": "Уникальный доступ пользователя к коллекции"},
)

View File

@ -1,4 +1,4 @@
"""
External services
"""
"""
External services
"""

View File

@ -1,223 +1,223 @@
"""
Клиент для работы с DeepSeek API
"""
import json
from typing import Optional, AsyncIterator
import httpx
from src.shared.config import settings
class DeepSeekAPIError(Exception):
"""Ошибка при работе с DeepSeek API"""
pass
class DeepSeekClient:
"""Клиент для работы с DeepSeek API"""
def __init__(self, api_key: str | None = None, api_url: str | None = None):
self.api_key = api_key or settings.DEEPSEEK_API_KEY
self.api_url = api_url or settings.DEEPSEEK_API_URL
self.timeout = 60.0
def _get_headers(self) -> dict[str, str]:
"""Получить заголовки для запроса"""
if not self.api_key:
raise DeepSeekAPIError("DEEPSEEK_API_KEY не установлен в настройках")
return {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.api_key}"
}
async def chat_completion(
self,
messages: list[dict[str, str]],
model: str = "deepseek-chat",
temperature: float = 0.7,
max_tokens: Optional[int] = None,
stream: bool = False
) -> dict:
"""
Отправка запроса на генерацию ответа
Args:
messages: Список сообщений в формате [{"role": "user", "content": "..."}]
model: Модель для использования (по умолчанию "deepseek-chat")
temperature: Температура генерации (0.0-2.0)
max_tokens: Максимальное количество токенов в ответе
stream: Использовать ли потоковую генерацию
Returns:
Ответ от API в формате:
{
"content": "текст ответа",
"usage": {
"prompt_tokens": int,
"completion_tokens": int,
"total_tokens": int
}
}
Raises:
DeepSeekAPIError: При ошибке API
"""
if not self.api_key:
return {
"content": " DEEPSEEK_API_KEY не установлен. Установите ключ в настройках для работы с DeepSeek API.",
"usage": {
"prompt_tokens": 0,
"completion_tokens": 0,
"total_tokens": 0
}
}
payload = {
"model": model,
"messages": messages,
"temperature": temperature,
"stream": stream
}
if max_tokens is not None:
payload["max_tokens"] = max_tokens
try:
async with httpx.AsyncClient(timeout=self.timeout) as client:
response = await client.post(
self.api_url,
headers=self._get_headers(),
json=payload
)
response.raise_for_status()
data = response.json()
if "choices" in data and len(data["choices"]) > 0:
content = data["choices"][0]["message"]["content"]
else:
raise DeepSeekAPIError("Неожиданный формат ответа от DeepSeek API")
usage = data.get("usage", {})
return {
"content": content,
"usage": {
"prompt_tokens": usage.get("prompt_tokens", 0),
"completion_tokens": usage.get("completion_tokens", 0),
"total_tokens": usage.get("total_tokens", 0)
}
}
except httpx.HTTPStatusError as e:
error_msg = f"Ошибка DeepSeek API: {e.response.status_code}"
try:
error_data = e.response.json()
if "error" in error_data:
error_msg = f"Ошибка DeepSeek API: {error_data['error'].get('message', error_msg)}"
except:
pass
raise DeepSeekAPIError(error_msg) from e
except httpx.RequestError as e:
raise DeepSeekAPIError(f"Ошибка подключения к DeepSeek API: {str(e)}") from e
except Exception as e:
raise DeepSeekAPIError(f"Неожиданная ошибка при работе с DeepSeek API: {str(e)}") from e
async def stream_chat_completion(
self,
messages: list[dict[str, str]],
model: str = "deepseek-chat",
temperature: float = 0.7,
max_tokens: Optional[int] = None
) -> AsyncIterator[str]:
"""
Потоковая генерация ответа
Args:
messages: Список сообщений в формате [{"role": "user", "content": "..."}]
model: Модель для использования
temperature: Температура генерации
max_tokens: Максимальное количество токенов
Yields:
Части ответа (chunks) по мере генерации
Raises:
DeepSeekAPIError: При ошибке API
"""
if not self.api_key:
yield "⚠️ DEEPSEEK_API_KEY не установлен. Установите ключ в настройках для работы с DeepSeek API."
return
payload = {
"model": model,
"messages": messages,
"temperature": temperature,
"stream": True
}
if max_tokens is not None:
payload["max_tokens"] = max_tokens
try:
async with httpx.AsyncClient(timeout=self.timeout) as client:
async with client.stream(
"POST",
self.api_url,
headers=self._get_headers(),
json=payload
) as response:
response.raise_for_status()
async for line in response.aiter_lines():
if not line.strip():
continue
if line.startswith("data: "):
line = line[6:]
if line.strip() == "[DONE]":
break
try:
data = json.loads(line)
if "choices" in data and len(data["choices"]) > 0:
delta = data["choices"][0].get("delta", {})
content = delta.get("content", "")
if content:
yield content
except json.JSONDecodeError:
continue
except httpx.HTTPStatusError as e:
error_msg = f"Ошибка DeepSeek API: {e.response.status_code}"
try:
error_data = e.response.json()
if "error" in error_data:
error_msg = f"Ошибка DeepSeek API: {error_data['error'].get('message', error_msg)}"
except:
pass
raise DeepSeekAPIError(error_msg) from e
except httpx.RequestError as e:
raise DeepSeekAPIError(f"Ошибка подключения к DeepSeek API: {str(e)}") from e
except Exception as e:
raise DeepSeekAPIError(f"Неожиданная ошибка при потоковой генерации: {str(e)}") from e
async def health_check(self) -> bool:
"""
Проверка доступности API
Returns:
True если API доступен, False иначе
"""
if not self.api_key:
return False
try:
test_messages = [{"role": "user", "content": "test"}]
await self.chat_completion(test_messages, max_tokens=1)
return True
except Exception:
return False
"""
Клиент для работы с DeepSeek API
"""
import json
from typing import Optional, AsyncIterator
import httpx
from src.shared.config import settings
class DeepSeekAPIError(Exception):
"""Ошибка при работе с DeepSeek API"""
pass
class DeepSeekClient:
"""Клиент для работы с DeepSeek API"""
def __init__(self, api_key: str | None = None, api_url: str | None = None):
self.api_key = api_key or settings.DEEPSEEK_API_KEY
self.api_url = api_url or settings.DEEPSEEK_API_URL
self.timeout = 60.0
def _get_headers(self) -> dict[str, str]:
"""Получить заголовки для запроса"""
if not self.api_key:
raise DeepSeekAPIError("DEEPSEEK_API_KEY не установлен в настройках")
return {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.api_key}"
}
async def chat_completion(
self,
messages: list[dict[str, str]],
model: str = "deepseek-chat",
temperature: float = 0.7,
max_tokens: Optional[int] = None,
stream: bool = False
) -> dict:
"""
Отправка запроса на генерацию ответа
Args:
messages: Список сообщений в формате [{"role": "user", "content": "..."}]
model: Модель для использования (по умолчанию "deepseek-chat")
temperature: Температура генерации (0.0-2.0)
max_tokens: Максимальное количество токенов в ответе
stream: Использовать ли потоковую генерацию
Returns:
Ответ от API в формате:
{
"content": "текст ответа",
"usage": {
"prompt_tokens": int,
"completion_tokens": int,
"total_tokens": int
}
}
Raises:
DeepSeekAPIError: При ошибке API
"""
if not self.api_key:
return {
"content": " DEEPSEEK_API_KEY не установлен. Установите ключ в настройках для работы с DeepSeek API.",
"usage": {
"prompt_tokens": 0,
"completion_tokens": 0,
"total_tokens": 0
}
}
payload = {
"model": model,
"messages": messages,
"temperature": temperature,
"stream": stream
}
if max_tokens is not None:
payload["max_tokens"] = max_tokens
try:
async with httpx.AsyncClient(timeout=self.timeout) as client:
response = await client.post(
self.api_url,
headers=self._get_headers(),
json=payload
)
response.raise_for_status()
data = response.json()
if "choices" in data and len(data["choices"]) > 0:
content = data["choices"][0]["message"]["content"]
else:
raise DeepSeekAPIError("Неожиданный формат ответа от DeepSeek API")
usage = data.get("usage", {})
return {
"content": content,
"usage": {
"prompt_tokens": usage.get("prompt_tokens", 0),
"completion_tokens": usage.get("completion_tokens", 0),
"total_tokens": usage.get("total_tokens", 0)
}
}
except httpx.HTTPStatusError as e:
error_msg = f"Ошибка DeepSeek API: {e.response.status_code}"
try:
error_data = e.response.json()
if "error" in error_data:
error_msg = f"Ошибка DeepSeek API: {error_data['error'].get('message', error_msg)}"
except:
pass
raise DeepSeekAPIError(error_msg) from e
except httpx.RequestError as e:
raise DeepSeekAPIError(f"Ошибка подключения к DeepSeek API: {str(e)}") from e
except Exception as e:
raise DeepSeekAPIError(f"Неожиданная ошибка при работе с DeepSeek API: {str(e)}") from e
async def stream_chat_completion(
self,
messages: list[dict[str, str]],
model: str = "deepseek-chat",
temperature: float = 0.7,
max_tokens: Optional[int] = None
) -> AsyncIterator[str]:
"""
Потоковая генерация ответа
Args:
messages: Список сообщений в формате [{"role": "user", "content": "..."}]
model: Модель для использования
temperature: Температура генерации
max_tokens: Максимальное количество токенов
Yields:
Части ответа (chunks) по мере генерации
Raises:
DeepSeekAPIError: При ошибке API
"""
if not self.api_key:
yield "⚠️ DEEPSEEK_API_KEY не установлен. Установите ключ в настройках для работы с DeepSeek API."
return
payload = {
"model": model,
"messages": messages,
"temperature": temperature,
"stream": True
}
if max_tokens is not None:
payload["max_tokens"] = max_tokens
try:
async with httpx.AsyncClient(timeout=self.timeout) as client:
async with client.stream(
"POST",
self.api_url,
headers=self._get_headers(),
json=payload
) as response:
response.raise_for_status()
async for line in response.aiter_lines():
if not line.strip():
continue
if line.startswith("data: "):
line = line[6:]
if line.strip() == "[DONE]":
break
try:
data = json.loads(line)
if "choices" in data and len(data["choices"]) > 0:
delta = data["choices"][0].get("delta", {})
content = delta.get("content", "")
if content:
yield content
except json.JSONDecodeError:
continue
except httpx.HTTPStatusError as e:
error_msg = f"Ошибка DeepSeek API: {e.response.status_code}"
try:
error_data = e.response.json()
if "error" in error_data:
error_msg = f"Ошибка DeepSeek API: {error_data['error'].get('message', error_msg)}"
except:
pass
raise DeepSeekAPIError(error_msg) from e
except httpx.RequestError as e:
raise DeepSeekAPIError(f"Ошибка подключения к DeepSeek API: {str(e)}") from e
except Exception as e:
raise DeepSeekAPIError(f"Неожиданная ошибка при потоковой генерации: {str(e)}") from e
async def health_check(self) -> bool:
"""
Проверка доступности API
Returns:
True если API доступен, False иначе
"""
if not self.api_key:
return False
try:
test_messages = [{"role": "user", "content": "test"}]
await self.chat_completion(test_messages, max_tokens=1)
return True
except Exception:
return False

View File

@ -1,35 +1,35 @@
"""
Сервис для работы с Telegram Bot API
"""
from typing import Optional
from src.shared.config import settings
class TelegramAuthService:
"""
Сервис для работы с Telegram Bot API
"""
def __init__(self, bot_token: str | None = None):
self.bot_token = bot_token or settings.TELEGRAM_BOT_TOKEN
async def get_user_info(self, telegram_id: str) -> Optional[dict]:
"""
Получение информации о пользователе через Telegram Bot API
Args:
telegram_id: ID пользователя в Telegram
Returns:
Информация о пользователе или None
"""
if not self.bot_token:
return None
return {
"id": telegram_id,
"first_name": "User",
"username": None
}
"""
Сервис для работы с Telegram Bot API
"""
from typing import Optional
from src.shared.config import settings
class TelegramAuthService:
"""
Сервис для работы с Telegram Bot API
"""
def __init__(self, bot_token: str | None = None):
self.bot_token = bot_token or settings.TELEGRAM_BOT_TOKEN
async def get_user_info(self, telegram_id: str) -> Optional[dict]:
"""
Получение информации о пользователе через Telegram Bot API
Args:
telegram_id: ID пользователя в Telegram
Returns:
Информация о пользователе или None
"""
if not self.bot_token:
return None
return {
"id": telegram_id,
"first_name": "User",
"username": None
}

View File

@ -1,280 +1,280 @@
"""
Интеграция с Yandex Vision OCR для парсинга документов
"""
import base64
import io
from typing import BinaryIO
import httpx
import fitz
from PIL import Image
from src.shared.config import settings
class YandexOCRError(Exception):
"""Ошибка при работе с Yandex OCR API"""
pass
class YandexOCRService:
"""Сервис для работы с Yandex Vision OCR"""
def __init__(self, api_key: str | None = None):
self.api_key = api_key or settings.YANDEX_OCR_API_KEY
self.api_url = settings.YANDEX_OCR_API_URL
self.timeout = 120.0
self.max_file_size = 10 * 1024 * 1024
def _get_headers(self) -> dict[str, str]:
"""Получить заголовки для запроса"""
if not self.api_key:
raise YandexOCRError("YANDEX_OCR_API_KEY не установлен в настройках")
return {
"Authorization": f"Api-Key {self.api_key}",
"Content-Type": "application/json"
}
def _validate_file_size(self, file_content: bytes) -> None:
"""Проверка размера файла"""
if len(file_content) > self.max_file_size:
raise YandexOCRError(
f"Файл слишком большой: {len(file_content)} байт. "
f"Максимальный размер: {self.max_file_size} байт (10 МБ)"
)
async def extract_text(
self,
file_content: bytes,
file_type: str = "pdf",
language_codes: list[str] | None = None
) -> str:
"""
Извлечение текста из файла через Yandex Vision OCR
Args:
file_content: Содержимое файла в байтах
file_type: Тип файла (pdf, image)
language_codes: Коды языков для распознавания (по умолчанию ['ru', 'en'])
Returns:
Извлеченный текст
Raises:
YandexOCRError: При ошибке API
"""
if not self.api_key:
return " YANDEX_OCR_API_KEY не установлен. Установите ключ в настройках для распознавания документов."
self._validate_file_size(file_content)
image_data = base64.b64encode(file_content).decode('utf-8')
if language_codes is None:
language_codes = ['ru', 'en']
model = 'page'
payload = {
"analyze_specs": [{
"content": image_data,
"features": [{
"type": "TEXT_DETECTION",
"text_detection_config": {
"model": model,
"language_codes": language_codes
}
}]
}]
}
try:
async with httpx.AsyncClient(timeout=self.timeout) as client:
response = await client.post(
self.api_url,
headers=self._get_headers(),
json=payload
)
response.raise_for_status()
result = response.json()
return self._extract_text_from_response(result)
except httpx.HTTPStatusError as e:
error_msg = f"Ошибка Yandex OCR API: {e.response.status_code}"
try:
error_data = e.response.json()
if "message" in error_data:
error_msg = f"Ошибка Yandex OCR API: {error_data['message']}"
except:
pass
raise YandexOCRError(error_msg) from e
except httpx.RequestError as e:
raise YandexOCRError(f"Ошибка подключения к Yandex OCR API: {str(e)}") from e
except YandexOCRError:
raise
except Exception as e:
import traceback
error_details = traceback.format_exc()
raise YandexOCRError(f"Неожиданная ошибка при работе с Yandex OCR: {str(e)}\n{error_details}") from e
def _extract_text_from_response(self, response: dict) -> str:
"""
Извлечение текста из ответа Yandex Vision API
Args:
response: JSON ответ от API
Returns:
Извлеченный текст
"""
import json
if not self.api_key:
return " YANDEX_OCR_API_KEY не установлен. Установите ключ в настройках для распознавания документов."
text_parts = []
if "results" not in response:
if "error" in response:
error_msg = response.get("error", {}).get("message", "Неизвестная ошибка")
raise YandexOCRError(f"Ошибка Yandex OCR API: {error_msg}")
raise YandexOCRError(f"Неожиданный формат ответа от Yandex OCR API. Структура: {list(response.keys())}")
for result in response["results"]:
if "results" not in result:
continue
for annotation in result["results"]:
if "textDetection" not in annotation:
continue
text_detection = annotation["textDetection"]
if "pages" in text_detection:
for page in text_detection["pages"]:
if "blocks" in page:
for block in page["blocks"]:
if "lines" in block:
for line in block["lines"]:
if "words" in line:
line_text = " ".join([
word.get("text", "")
for word in line["words"]
])
if line_text:
text_parts.append(line_text)
full_text = "\n".join(text_parts)
if not full_text.strip():
return f" Не удалось извлечь текст из документа. Возможно, документ пустой или нечитаемый. Структура ответа: {json.dumps(response, indent=2, ensure_ascii=False)[:500]}"
return full_text
async def parse_pdf(self, file: BinaryIO) -> str:
"""
Парсинг PDF документа через YandexOCR
Yandex Vision API не поддерживает PDF напрямую, поэтому
конвертируем каждую страницу PDF в изображение и распознаем отдельно.
Args:
file: Файловый объект PDF
Returns:
Текст из документа (объединенный текст со всех страниц)
"""
file_content = await self._read_file(file)
images = await self._pdf_to_images(file_content)
if not images:
return " Не удалось конвертировать PDF в изображения. Возможно, файл поврежден."
all_text_parts = []
for i, image_bytes in enumerate(images, 1):
try:
page_text = await self.extract_text(image_bytes, file_type="image")
if page_text and not page_text.startswith("Ошибка распознавания:"):
all_text_parts.append(f"--- Страница {i} ---\n{page_text}")
except YandexOCRError as e:
all_text_parts.append(f"--- Страница {i} ---\n Ошибка распознавания: {str(e)}")
if not all_text_parts:
return " Не удалось распознать текст ни с одной страницы PDF."
return "\n\n".join(all_text_parts)
async def _pdf_to_images(self, pdf_content: bytes) -> list[bytes]:
"""
Конвертация PDF в список изображений (по одной на страницу)
Args:
pdf_content: Содержимое PDF файла в байтах
Returns:
Список изображений в формате PNG (каждое в байтах)
"""
try:
pdf_document = fitz.open(stream=pdf_content, filetype="pdf")
images = []
for page_num in range(len(pdf_document)):
page = pdf_document[page_num]
mat = fitz.Matrix(2.0, 2.0)
pix = page.get_pixmap(matrix=mat)
img_data = pix.tobytes("png")
images.append(img_data)
pdf_document.close()
return images
except Exception as e:
raise YandexOCRError(f"Ошибка при конвертации PDF в изображения: {str(e)}") from e
async def parse_image(self, file: BinaryIO) -> str:
"""
Парсинг изображения через YandexOCR
Args:
file: Файловый объект изображения (PNG, JPEG, etc.)
Returns:
Текст из изображения
"""
file_content = await self._read_file(file)
return await self.extract_text(file_content, file_type="image")
async def _read_file(self, file: BinaryIO) -> bytes:
"""
Чтение содержимого файла в байты
Args:
file: Файловый объект
Returns:
Содержимое файла в байтах
"""
if hasattr(file, 'read'):
content = file.read()
if hasattr(file, 'seek'):
file.seek(0)
return content
else:
raise YandexOCRError("Некорректный файловый объект")
async def health_check(self) -> bool:
"""
Проверка доступности API
Returns:
True если API доступен, False иначе
"""
if not self.api_key:
return False
return True
"""
Интеграция с Yandex Vision OCR для парсинга документов
"""
import base64
import io
from typing import BinaryIO
import httpx
import fitz
from PIL import Image
from src.shared.config import settings
class YandexOCRError(Exception):
"""Ошибка при работе с Yandex OCR API"""
pass
class YandexOCRService:
"""Сервис для работы с Yandex Vision OCR"""
def __init__(self, api_key: str | None = None):
self.api_key = api_key or settings.YANDEX_OCR_API_KEY
self.api_url = settings.YANDEX_OCR_API_URL
self.timeout = 120.0
self.max_file_size = 10 * 1024 * 1024
def _get_headers(self) -> dict[str, str]:
"""Получить заголовки для запроса"""
if not self.api_key:
raise YandexOCRError("YANDEX_OCR_API_KEY не установлен в настройках")
return {
"Authorization": f"Api-Key {self.api_key}",
"Content-Type": "application/json"
}
def _validate_file_size(self, file_content: bytes) -> None:
"""Проверка размера файла"""
if len(file_content) > self.max_file_size:
raise YandexOCRError(
f"Файл слишком большой: {len(file_content)} байт. "
f"Максимальный размер: {self.max_file_size} байт (10 МБ)"
)
async def extract_text(
self,
file_content: bytes,
file_type: str = "pdf",
language_codes: list[str] | None = None
) -> str:
"""
Извлечение текста из файла через Yandex Vision OCR
Args:
file_content: Содержимое файла в байтах
file_type: Тип файла (pdf, image)
language_codes: Коды языков для распознавания (по умолчанию ['ru', 'en'])
Returns:
Извлеченный текст
Raises:
YandexOCRError: При ошибке API
"""
if not self.api_key:
return " YANDEX_OCR_API_KEY не установлен. Установите ключ в настройках для распознавания документов."
self._validate_file_size(file_content)
image_data = base64.b64encode(file_content).decode('utf-8')
if language_codes is None:
language_codes = ['ru', 'en']
model = 'page'
payload = {
"analyze_specs": [{
"content": image_data,
"features": [{
"type": "TEXT_DETECTION",
"text_detection_config": {
"model": model,
"language_codes": language_codes
}
}]
}]
}
try:
async with httpx.AsyncClient(timeout=self.timeout) as client:
response = await client.post(
self.api_url,
headers=self._get_headers(),
json=payload
)
response.raise_for_status()
result = response.json()
return self._extract_text_from_response(result)
except httpx.HTTPStatusError as e:
error_msg = f"Ошибка Yandex OCR API: {e.response.status_code}"
try:
error_data = e.response.json()
if "message" in error_data:
error_msg = f"Ошибка Yandex OCR API: {error_data['message']}"
except:
pass
raise YandexOCRError(error_msg) from e
except httpx.RequestError as e:
raise YandexOCRError(f"Ошибка подключения к Yandex OCR API: {str(e)}") from e
except YandexOCRError:
raise
except Exception as e:
import traceback
error_details = traceback.format_exc()
raise YandexOCRError(f"Неожиданная ошибка при работе с Yandex OCR: {str(e)}\n{error_details}") from e
def _extract_text_from_response(self, response: dict) -> str:
"""
Извлечение текста из ответа Yandex Vision API
Args:
response: JSON ответ от API
Returns:
Извлеченный текст
"""
import json
if not self.api_key:
return " YANDEX_OCR_API_KEY не установлен. Установите ключ в настройках для распознавания документов."
text_parts = []
if "results" not in response:
if "error" in response:
error_msg = response.get("error", {}).get("message", "Неизвестная ошибка")
raise YandexOCRError(f"Ошибка Yandex OCR API: {error_msg}")
raise YandexOCRError(f"Неожиданный формат ответа от Yandex OCR API. Структура: {list(response.keys())}")
for result in response["results"]:
if "results" not in result:
continue
for annotation in result["results"]:
if "textDetection" not in annotation:
continue
text_detection = annotation["textDetection"]
if "pages" in text_detection:
for page in text_detection["pages"]:
if "blocks" in page:
for block in page["blocks"]:
if "lines" in block:
for line in block["lines"]:
if "words" in line:
line_text = " ".join([
word.get("text", "")
for word in line["words"]
])
if line_text:
text_parts.append(line_text)
full_text = "\n".join(text_parts)
if not full_text.strip():
return f" Не удалось извлечь текст из документа. Возможно, документ пустой или нечитаемый. Структура ответа: {json.dumps(response, indent=2, ensure_ascii=False)[:500]}"
return full_text
async def parse_pdf(self, file: BinaryIO) -> str:
"""
Парсинг PDF документа через YandexOCR
Yandex Vision API не поддерживает PDF напрямую, поэтому
конвертируем каждую страницу PDF в изображение и распознаем отдельно.
Args:
file: Файловый объект PDF
Returns:
Текст из документа (объединенный текст со всех страниц)
"""
file_content = await self._read_file(file)
images = await self._pdf_to_images(file_content)
if not images:
return " Не удалось конвертировать PDF в изображения. Возможно, файл поврежден."
all_text_parts = []
for i, image_bytes in enumerate(images, 1):
try:
page_text = await self.extract_text(image_bytes, file_type="image")
if page_text and not page_text.startswith("Ошибка распознавания:"):
all_text_parts.append(f"--- Страница {i} ---\n{page_text}")
except YandexOCRError as e:
all_text_parts.append(f"--- Страница {i} ---\n Ошибка распознавания: {str(e)}")
if not all_text_parts:
return " Не удалось распознать текст ни с одной страницы PDF."
return "\n\n".join(all_text_parts)
async def _pdf_to_images(self, pdf_content: bytes) -> list[bytes]:
"""
Конвертация PDF в список изображений (по одной на страницу)
Args:
pdf_content: Содержимое PDF файла в байтах
Returns:
Список изображений в формате PNG (каждое в байтах)
"""
try:
pdf_document = fitz.open(stream=pdf_content, filetype="pdf")
images = []
for page_num in range(len(pdf_document)):
page = pdf_document[page_num]
mat = fitz.Matrix(2.0, 2.0)
pix = page.get_pixmap(matrix=mat)
img_data = pix.tobytes("png")
images.append(img_data)
pdf_document.close()
return images
except Exception as e:
raise YandexOCRError(f"Ошибка при конвертации PDF в изображения: {str(e)}") from e
async def parse_image(self, file: BinaryIO) -> str:
"""
Парсинг изображения через YandexOCR
Args:
file: Файловый объект изображения (PNG, JPEG, etc.)
Returns:
Текст из изображения
"""
file_content = await self._read_file(file)
return await self.extract_text(file_content, file_type="image")
async def _read_file(self, file: BinaryIO) -> bytes:
"""
Чтение содержимого файла в байты
Args:
file: Файловый объект
Returns:
Содержимое файла в байтах
"""
if hasattr(file, 'read'):
content = file.read()
if hasattr(file, 'seek'):
file.seek(0)
return content
else:
raise YandexOCRError("Некорректный файловый объект")
async def health_check(self) -> bool:
"""
Проверка доступности API
Returns:
True если API доступен, False иначе
"""
if not self.api_key:
return False
return True

View File

@ -1,4 +1,4 @@
"""
PostgreSQL repository implementations
"""
"""
PostgreSQL repository implementations
"""

View File

@ -1,107 +1,107 @@
"""
Реализация репозитория доступа к коллекциям для PostgreSQL
"""
from uuid import UUID
from typing import Optional
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from src.domain.entities.collection_access import CollectionAccess
from src.domain.repositories.collection_access_repository import ICollectionAccessRepository
from src.infrastructure.database.models import CollectionAccessModel
from src.shared.exceptions import NotFoundError
class PostgreSQLCollectionAccessRepository(ICollectionAccessRepository):
"""PostgreSQL реализация репозитория доступа к коллекциям"""
def __init__(self, session: AsyncSession):
self.session = session
async def create(self, access: CollectionAccess) -> CollectionAccess:
"""Создать доступ"""
db_access = CollectionAccessModel(
access_id=access.access_id,
user_id=access.user_id,
collection_id=access.collection_id,
created_at=access.created_at
)
self.session.add(db_access)
await self.session.commit()
await self.session.refresh(db_access)
return self._to_entity(db_access)
async def get_by_id(self, access_id: UUID) -> Optional[CollectionAccess]:
"""Получить доступ по ID"""
result = await self.session.execute(
select(CollectionAccessModel).where(CollectionAccessModel.access_id == access_id)
)
db_access = result.scalar_one_or_none()
return self._to_entity(db_access) if db_access else None
async def delete(self, access_id: UUID) -> bool:
"""Удалить доступ"""
result = await self.session.execute(
select(CollectionAccessModel).where(CollectionAccessModel.access_id == access_id)
)
db_access = result.scalar_one_or_none()
if not db_access:
return False
await self.session.delete(db_access)
await self.session.commit()
return True
async def delete_by_user_and_collection(self, user_id: UUID, collection_id: UUID) -> bool:
"""Удалить доступ пользователя к коллекции"""
result = await self.session.execute(
select(CollectionAccessModel).where(
CollectionAccessModel.user_id == user_id,
CollectionAccessModel.collection_id == collection_id
)
)
db_access = result.scalar_one_or_none()
if not db_access:
return False
await self.session.delete(db_access)
await self.session.commit()
return True
async def get_by_user_and_collection(self, user_id: UUID, collection_id: UUID) -> Optional[CollectionAccess]:
"""Получить доступ пользователя к коллекции"""
result = await self.session.execute(
select(CollectionAccessModel).where(
CollectionAccessModel.user_id == user_id,
CollectionAccessModel.collection_id == collection_id
)
)
db_access = result.scalar_one_or_none()
return self._to_entity(db_access) if db_access else None
async def list_by_user(self, user_id: UUID) -> list[CollectionAccess]:
"""Получить доступы пользователя"""
result = await self.session.execute(
select(CollectionAccessModel).where(CollectionAccessModel.user_id == user_id)
)
db_accesses = result.scalars().all()
return [self._to_entity(db_access) for db_access in db_accesses]
async def list_by_collection(self, collection_id: UUID) -> list[CollectionAccess]:
"""Получить доступы к коллекции"""
result = await self.session.execute(
select(CollectionAccessModel).where(CollectionAccessModel.collection_id == collection_id)
)
db_accesses = result.scalars().all()
return [self._to_entity(db_access) for db_access in db_accesses]
def _to_entity(self, db_access: CollectionAccessModel | None) -> CollectionAccess | None:
"""Преобразовать модель БД в доменную сущность"""
if not db_access:
return None
return CollectionAccess(
access_id=db_access.access_id,
user_id=db_access.user_id,
collection_id=db_access.collection_id,
created_at=db_access.created_at
)
"""
Реализация репозитория доступа к коллекциям для PostgreSQL
"""
from uuid import UUID
from typing import Optional
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from src.domain.entities.collection_access import CollectionAccess
from src.domain.repositories.collection_access_repository import ICollectionAccessRepository
from src.infrastructure.database.models import CollectionAccessModel
from src.shared.exceptions import NotFoundError
class PostgreSQLCollectionAccessRepository(ICollectionAccessRepository):
"""PostgreSQL реализация репозитория доступа к коллекциям"""
def __init__(self, session: AsyncSession):
self.session = session
async def create(self, access: CollectionAccess) -> CollectionAccess:
"""Создать доступ"""
db_access = CollectionAccessModel(
access_id=access.access_id,
user_id=access.user_id,
collection_id=access.collection_id,
created_at=access.created_at
)
self.session.add(db_access)
await self.session.commit()
await self.session.refresh(db_access)
return self._to_entity(db_access)
async def get_by_id(self, access_id: UUID) -> Optional[CollectionAccess]:
"""Получить доступ по ID"""
result = await self.session.execute(
select(CollectionAccessModel).where(CollectionAccessModel.access_id == access_id)
)
db_access = result.scalar_one_or_none()
return self._to_entity(db_access) if db_access else None
async def delete(self, access_id: UUID) -> bool:
"""Удалить доступ"""
result = await self.session.execute(
select(CollectionAccessModel).where(CollectionAccessModel.access_id == access_id)
)
db_access = result.scalar_one_or_none()
if not db_access:
return False
await self.session.delete(db_access)
await self.session.commit()
return True
async def delete_by_user_and_collection(self, user_id: UUID, collection_id: UUID) -> bool:
"""Удалить доступ пользователя к коллекции"""
result = await self.session.execute(
select(CollectionAccessModel).where(
CollectionAccessModel.user_id == user_id,
CollectionAccessModel.collection_id == collection_id
)
)
db_access = result.scalar_one_or_none()
if not db_access:
return False
await self.session.delete(db_access)
await self.session.commit()
return True
async def get_by_user_and_collection(self, user_id: UUID, collection_id: UUID) -> Optional[CollectionAccess]:
"""Получить доступ пользователя к коллекции"""
result = await self.session.execute(
select(CollectionAccessModel).where(
CollectionAccessModel.user_id == user_id,
CollectionAccessModel.collection_id == collection_id
)
)
db_access = result.scalar_one_or_none()
return self._to_entity(db_access) if db_access else None
async def list_by_user(self, user_id: UUID) -> list[CollectionAccess]:
"""Получить доступы пользователя"""
result = await self.session.execute(
select(CollectionAccessModel).where(CollectionAccessModel.user_id == user_id)
)
db_accesses = result.scalars().all()
return [self._to_entity(db_access) for db_access in db_accesses]
async def list_by_collection(self, collection_id: UUID) -> list[CollectionAccess]:
"""Получить доступы к коллекции"""
result = await self.session.execute(
select(CollectionAccessModel).where(CollectionAccessModel.collection_id == collection_id)
)
db_accesses = result.scalars().all()
return [self._to_entity(db_access) for db_access in db_accesses]
def _to_entity(self, db_access: CollectionAccessModel | None) -> CollectionAccess | None:
"""Преобразовать модель БД в доменную сущность"""
if not db_access:
return None
return CollectionAccess(
access_id=db_access.access_id,
user_id=db_access.user_id,
collection_id=db_access.collection_id,
created_at=db_access.created_at
)

View File

@ -1,106 +1,106 @@
"""
Реализация репозитория коллекций для PostgreSQL
"""
from uuid import UUID
from typing import Optional
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from src.domain.entities.collection import Collection
from src.domain.repositories.collection_repository import ICollectionRepository
from src.infrastructure.database.models import CollectionModel
from src.shared.exceptions import NotFoundError
class PostgreSQLCollectionRepository(ICollectionRepository):
"""PostgreSQL реализация репозитория коллекций"""
def __init__(self, session: AsyncSession):
self.session = session
async def create(self, collection: Collection) -> Collection:
"""Создать коллекцию"""
db_collection = CollectionModel(
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
)
self.session.add(db_collection)
await self.session.commit()
await self.session.refresh(db_collection)
return self._to_entity(db_collection)
async def get_by_id(self, collection_id: UUID) -> Optional[Collection]:
"""Получить коллекцию по ID"""
result = await self.session.execute(
select(CollectionModel).where(CollectionModel.collection_id == collection_id)
)
db_collection = result.scalar_one_or_none()
return self._to_entity(db_collection) if db_collection else None
async def update(self, collection: Collection) -> Collection:
"""Обновить коллекцию"""
result = await self.session.execute(
select(CollectionModel).where(CollectionModel.collection_id == collection.collection_id)
)
db_collection = result.scalar_one_or_none()
if not db_collection:
raise NotFoundError(f"Коллекция {collection.collection_id} не найдена")
db_collection.name = collection.name
db_collection.description = collection.description
db_collection.is_public = collection.is_public
await self.session.commit()
await self.session.refresh(db_collection)
return self._to_entity(db_collection)
async def delete(self, collection_id: UUID) -> bool:
"""Удалить коллекцию"""
result = await self.session.execute(
select(CollectionModel).where(CollectionModel.collection_id == collection_id)
)
db_collection = result.scalar_one_or_none()
if not db_collection:
return False
await self.session.delete(db_collection)
await self.session.commit()
return True
async def list_by_owner(self, owner_id: UUID, skip: int = 0, limit: int = 100) -> list[Collection]:
"""Получить коллекции владельца"""
result = await self.session.execute(
select(CollectionModel)
.where(CollectionModel.owner_id == owner_id)
.offset(skip)
.limit(limit)
)
db_collections = result.scalars().all()
return [self._to_entity(db_collection) for db_collection in db_collections]
async def list_public(self, skip: int = 0, limit: int = 100) -> list[Collection]:
"""Получить публичные коллекции"""
result = await self.session.execute(
select(CollectionModel)
.where(CollectionModel.is_public == True)
.offset(skip)
.limit(limit)
)
db_collections = result.scalars().all()
return [self._to_entity(db_collection) for db_collection in db_collections]
def _to_entity(self, db_collection: CollectionModel | None) -> Collection | None:
"""Преобразовать модель БД в доменную сущность"""
if not db_collection:
return None
return Collection(
collection_id=db_collection.collection_id,
name=db_collection.name,
description=db_collection.description or "",
owner_id=db_collection.owner_id,
is_public=db_collection.is_public,
created_at=db_collection.created_at
)
"""
Реализация репозитория коллекций для PostgreSQL
"""
from uuid import UUID
from typing import Optional
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from src.domain.entities.collection import Collection
from src.domain.repositories.collection_repository import ICollectionRepository
from src.infrastructure.database.models import CollectionModel
from src.shared.exceptions import NotFoundError
class PostgreSQLCollectionRepository(ICollectionRepository):
"""PostgreSQL реализация репозитория коллекций"""
def __init__(self, session: AsyncSession):
self.session = session
async def create(self, collection: Collection) -> Collection:
"""Создать коллекцию"""
db_collection = CollectionModel(
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
)
self.session.add(db_collection)
await self.session.commit()
await self.session.refresh(db_collection)
return self._to_entity(db_collection)
async def get_by_id(self, collection_id: UUID) -> Optional[Collection]:
"""Получить коллекцию по ID"""
result = await self.session.execute(
select(CollectionModel).where(CollectionModel.collection_id == collection_id)
)
db_collection = result.scalar_one_or_none()
return self._to_entity(db_collection) if db_collection else None
async def update(self, collection: Collection) -> Collection:
"""Обновить коллекцию"""
result = await self.session.execute(
select(CollectionModel).where(CollectionModel.collection_id == collection.collection_id)
)
db_collection = result.scalar_one_or_none()
if not db_collection:
raise NotFoundError(f"Коллекция {collection.collection_id} не найдена")
db_collection.name = collection.name
db_collection.description = collection.description
db_collection.is_public = collection.is_public
await self.session.commit()
await self.session.refresh(db_collection)
return self._to_entity(db_collection)
async def delete(self, collection_id: UUID) -> bool:
"""Удалить коллекцию"""
result = await self.session.execute(
select(CollectionModel).where(CollectionModel.collection_id == collection_id)
)
db_collection = result.scalar_one_or_none()
if not db_collection:
return False
await self.session.delete(db_collection)
await self.session.commit()
return True
async def list_by_owner(self, owner_id: UUID, skip: int = 0, limit: int = 100) -> list[Collection]:
"""Получить коллекции владельца"""
result = await self.session.execute(
select(CollectionModel)
.where(CollectionModel.owner_id == owner_id)
.offset(skip)
.limit(limit)
)
db_collections = result.scalars().all()
return [self._to_entity(db_collection) for db_collection in db_collections]
async def list_public(self, skip: int = 0, limit: int = 100) -> list[Collection]:
"""Получить публичные коллекции"""
result = await self.session.execute(
select(CollectionModel)
.where(CollectionModel.is_public == True)
.offset(skip)
.limit(limit)
)
db_collections = result.scalars().all()
return [self._to_entity(db_collection) for db_collection in db_collections]
def _to_entity(self, db_collection: CollectionModel | None) -> Collection | None:
"""Преобразовать модель БД в доменную сущность"""
if not db_collection:
return None
return Collection(
collection_id=db_collection.collection_id,
name=db_collection.name,
description=db_collection.description or "",
owner_id=db_collection.owner_id,
is_public=db_collection.is_public,
created_at=db_collection.created_at
)

View File

@ -1,104 +1,104 @@
"""
Реализация репозитория бесед для PostgreSQL
"""
from uuid import UUID
from typing import Optional
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from src.domain.entities.conversation import Conversation
from src.domain.repositories.conversation_repository import IConversationRepository
from src.infrastructure.database.models import ConversationModel
from src.shared.exceptions import NotFoundError
class PostgreSQLConversationRepository(IConversationRepository):
"""PostgreSQL реализация репозитория бесед"""
def __init__(self, session: AsyncSession):
self.session = session
async def create(self, conversation: Conversation) -> Conversation:
"""Создать беседу"""
db_conversation = ConversationModel(
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
)
self.session.add(db_conversation)
await self.session.commit()
await self.session.refresh(db_conversation)
return self._to_entity(db_conversation)
async def get_by_id(self, conversation_id: UUID) -> Optional[Conversation]:
"""Получить беседу по ID"""
result = await self.session.execute(
select(ConversationModel).where(ConversationModel.conversation_id == conversation_id)
)
db_conversation = result.scalar_one_or_none()
return self._to_entity(db_conversation) if db_conversation else None
async def update(self, conversation: Conversation) -> Conversation:
"""Обновить беседу"""
result = await self.session.execute(
select(ConversationModel).where(ConversationModel.conversation_id == conversation.conversation_id)
)
db_conversation = result.scalar_one_or_none()
if not db_conversation:
raise NotFoundError(f"Беседа {conversation.conversation_id} не найдена")
db_conversation.user_id = conversation.user_id
db_conversation.collection_id = conversation.collection_id
db_conversation.updated_at = conversation.updated_at
await self.session.commit()
await self.session.refresh(db_conversation)
return self._to_entity(db_conversation)
async def delete(self, conversation_id: UUID) -> bool:
"""Удалить беседу"""
result = await self.session.execute(
select(ConversationModel).where(ConversationModel.conversation_id == conversation_id)
)
db_conversation = result.scalar_one_or_none()
if not db_conversation:
return False
await self.session.delete(db_conversation)
await self.session.commit()
return True
async def list_by_user(self, user_id: UUID, skip: int = 0, limit: int = 100) -> list[Conversation]:
"""Получить беседы пользователя"""
result = await self.session.execute(
select(ConversationModel)
.where(ConversationModel.user_id == user_id)
.offset(skip)
.limit(limit)
)
db_conversations = result.scalars().all()
return [self._to_entity(db_conversation) for db_conversation in db_conversations]
async def list_by_collection(self, collection_id: UUID, skip: int = 0, limit: int = 100) -> list[Conversation]:
"""Получить беседы по коллекции"""
result = await self.session.execute(
select(ConversationModel)
.where(ConversationModel.collection_id == collection_id)
.offset(skip)
.limit(limit)
)
db_conversations = result.scalars().all()
return [self._to_entity(db_conversation) for db_conversation in db_conversations]
def _to_entity(self, db_conversation: ConversationModel | None) -> Conversation | None:
"""Преобразовать модель БД в доменную сущность"""
if not db_conversation:
return None
return Conversation(
conversation_id=db_conversation.conversation_id,
user_id=db_conversation.user_id,
collection_id=db_conversation.collection_id,
created_at=db_conversation.created_at,
updated_at=db_conversation.updated_at
)
"""
Реализация репозитория бесед для PostgreSQL
"""
from uuid import UUID
from typing import Optional
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from src.domain.entities.conversation import Conversation
from src.domain.repositories.conversation_repository import IConversationRepository
from src.infrastructure.database.models import ConversationModel
from src.shared.exceptions import NotFoundError
class PostgreSQLConversationRepository(IConversationRepository):
"""PostgreSQL реализация репозитория бесед"""
def __init__(self, session: AsyncSession):
self.session = session
async def create(self, conversation: Conversation) -> Conversation:
"""Создать беседу"""
db_conversation = ConversationModel(
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
)
self.session.add(db_conversation)
await self.session.commit()
await self.session.refresh(db_conversation)
return self._to_entity(db_conversation)
async def get_by_id(self, conversation_id: UUID) -> Optional[Conversation]:
"""Получить беседу по ID"""
result = await self.session.execute(
select(ConversationModel).where(ConversationModel.conversation_id == conversation_id)
)
db_conversation = result.scalar_one_or_none()
return self._to_entity(db_conversation) if db_conversation else None
async def update(self, conversation: Conversation) -> Conversation:
"""Обновить беседу"""
result = await self.session.execute(
select(ConversationModel).where(ConversationModel.conversation_id == conversation.conversation_id)
)
db_conversation = result.scalar_one_or_none()
if not db_conversation:
raise NotFoundError(f"Беседа {conversation.conversation_id} не найдена")
db_conversation.user_id = conversation.user_id
db_conversation.collection_id = conversation.collection_id
db_conversation.updated_at = conversation.updated_at
await self.session.commit()
await self.session.refresh(db_conversation)
return self._to_entity(db_conversation)
async def delete(self, conversation_id: UUID) -> bool:
"""Удалить беседу"""
result = await self.session.execute(
select(ConversationModel).where(ConversationModel.conversation_id == conversation_id)
)
db_conversation = result.scalar_one_or_none()
if not db_conversation:
return False
await self.session.delete(db_conversation)
await self.session.commit()
return True
async def list_by_user(self, user_id: UUID, skip: int = 0, limit: int = 100) -> list[Conversation]:
"""Получить беседы пользователя"""
result = await self.session.execute(
select(ConversationModel)
.where(ConversationModel.user_id == user_id)
.offset(skip)
.limit(limit)
)
db_conversations = result.scalars().all()
return [self._to_entity(db_conversation) for db_conversation in db_conversations]
async def list_by_collection(self, collection_id: UUID, skip: int = 0, limit: int = 100) -> list[Conversation]:
"""Получить беседы по коллекции"""
result = await self.session.execute(
select(ConversationModel)
.where(ConversationModel.collection_id == collection_id)
.offset(skip)
.limit(limit)
)
db_conversations = result.scalars().all()
return [self._to_entity(db_conversation) for db_conversation in db_conversations]
def _to_entity(self, db_conversation: ConversationModel | None) -> Conversation | None:
"""Преобразовать модель БД в доменную сущность"""
if not db_conversation:
return None
return Conversation(
conversation_id=db_conversation.conversation_id,
user_id=db_conversation.user_id,
collection_id=db_conversation.collection_id,
created_at=db_conversation.created_at,
updated_at=db_conversation.updated_at
)

View File

@ -1,95 +1,95 @@
"""
Реализация репозитория документов для PostgreSQL
"""
from uuid import UUID
from typing import Optional
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from src.domain.entities.document import Document
from src.domain.repositories.document_repository import IDocumentRepository
from src.infrastructure.database.models import DocumentModel
from src.shared.exceptions import NotFoundError
class PostgreSQLDocumentRepository(IDocumentRepository):
"""PostgreSQL реализация репозитория документов"""
def __init__(self, session: AsyncSession):
self.session = session
async def create(self, document: Document) -> Document:
"""Создать документ"""
db_document = DocumentModel(
document_id=document.document_id,
collection_id=document.collection_id,
title=document.title,
content=document.content,
document_metadata=document.metadata,
created_at=document.created_at
)
self.session.add(db_document)
await self.session.commit()
await self.session.refresh(db_document)
return self._to_entity(db_document)
async def get_by_id(self, document_id: UUID) -> Optional[Document]:
"""Получить документ по ID"""
result = await self.session.execute(
select(DocumentModel).where(DocumentModel.document_id == document_id)
)
db_document = result.scalar_one_or_none()
return self._to_entity(db_document) if db_document else None
async def update(self, document: Document) -> Document:
"""Обновить документ"""
result = await self.session.execute(
select(DocumentModel).where(DocumentModel.document_id == document.document_id)
)
db_document = result.scalar_one_or_none()
if not db_document:
raise NotFoundError(f"Документ {document.document_id} не найден")
db_document.title = document.title
db_document.content = document.content
db_document.document_metadata = document.metadata
await self.session.commit()
await self.session.refresh(db_document)
return self._to_entity(db_document)
async def delete(self, document_id: UUID) -> bool:
"""Удалить документ"""
result = await self.session.execute(
select(DocumentModel).where(DocumentModel.document_id == document_id)
)
db_document = result.scalar_one_or_none()
if not db_document:
return False
await self.session.delete(db_document)
await self.session.commit()
return True
async def list_by_collection(self, collection_id: UUID, skip: int = 0, limit: int = 100) -> list[Document]:
"""Получить документы коллекции"""
result = await self.session.execute(
select(DocumentModel)
.where(DocumentModel.collection_id == collection_id)
.offset(skip)
.limit(limit)
)
db_documents = result.scalars().all()
return [self._to_entity(db_document) for db_document in db_documents]
def _to_entity(self, db_document: DocumentModel | None) -> Document | None:
"""Преобразовать модель БД в доменную сущность"""
if not db_document:
return None
return Document(
document_id=db_document.document_id,
collection_id=db_document.collection_id,
title=db_document.title,
content=db_document.content,
metadata=db_document.document_metadata or {},
created_at=db_document.created_at
)
"""
Реализация репозитория документов для PostgreSQL
"""
from uuid import UUID
from typing import Optional
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from src.domain.entities.document import Document
from src.domain.repositories.document_repository import IDocumentRepository
from src.infrastructure.database.models import DocumentModel
from src.shared.exceptions import NotFoundError
class PostgreSQLDocumentRepository(IDocumentRepository):
"""PostgreSQL реализация репозитория документов"""
def __init__(self, session: AsyncSession):
self.session = session
async def create(self, document: Document) -> Document:
"""Создать документ"""
db_document = DocumentModel(
document_id=document.document_id,
collection_id=document.collection_id,
title=document.title,
content=document.content,
document_metadata=document.metadata,
created_at=document.created_at
)
self.session.add(db_document)
await self.session.commit()
await self.session.refresh(db_document)
return self._to_entity(db_document)
async def get_by_id(self, document_id: UUID) -> Optional[Document]:
"""Получить документ по ID"""
result = await self.session.execute(
select(DocumentModel).where(DocumentModel.document_id == document_id)
)
db_document = result.scalar_one_or_none()
return self._to_entity(db_document) if db_document else None
async def update(self, document: Document) -> Document:
"""Обновить документ"""
result = await self.session.execute(
select(DocumentModel).where(DocumentModel.document_id == document.document_id)
)
db_document = result.scalar_one_or_none()
if not db_document:
raise NotFoundError(f"Документ {document.document_id} не найден")
db_document.title = document.title
db_document.content = document.content
db_document.document_metadata = document.metadata
await self.session.commit()
await self.session.refresh(db_document)
return self._to_entity(db_document)
async def delete(self, document_id: UUID) -> bool:
"""Удалить документ"""
result = await self.session.execute(
select(DocumentModel).where(DocumentModel.document_id == document_id)
)
db_document = result.scalar_one_or_none()
if not db_document:
return False
await self.session.delete(db_document)
await self.session.commit()
return True
async def list_by_collection(self, collection_id: UUID, skip: int = 0, limit: int = 100) -> list[Document]:
"""Получить документы коллекции"""
result = await self.session.execute(
select(DocumentModel)
.where(DocumentModel.collection_id == collection_id)
.offset(skip)
.limit(limit)
)
db_documents = result.scalars().all()
return [self._to_entity(db_document) for db_document in db_documents]
def _to_entity(self, db_document: DocumentModel | None) -> Document | None:
"""Преобразовать модель БД в доменную сущность"""
if not db_document:
return None
return Document(
document_id=db_document.document_id,
collection_id=db_document.collection_id,
title=db_document.title,
content=db_document.content,
metadata=db_document.document_metadata or {},
created_at=db_document.created_at
)

View File

@ -1,96 +1,96 @@
"""
Реализация репозитория сообщений для PostgreSQL
"""
from uuid import UUID
from typing import Optional
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from src.domain.entities.message import Message, MessageRole
from src.domain.repositories.message_repository import IMessageRepository
from src.infrastructure.database.models import MessageModel
from src.shared.exceptions import NotFoundError
class PostgreSQLMessageRepository(IMessageRepository):
"""PostgreSQL реализация репозитория сообщений"""
def __init__(self, session: AsyncSession):
self.session = session
async def create(self, message: Message) -> Message:
"""Создать сообщение"""
db_message = MessageModel(
message_id=message.message_id,
conversation_id=message.conversation_id,
content=message.content,
role=message.role.value,
sources=message.sources,
created_at=message.created_at
)
self.session.add(db_message)
await self.session.commit()
await self.session.refresh(db_message)
return self._to_entity(db_message)
async def get_by_id(self, message_id: UUID) -> Optional[Message]:
"""Получить сообщение по ID"""
result = await self.session.execute(
select(MessageModel).where(MessageModel.message_id == message_id)
)
db_message = result.scalar_one_or_none()
return self._to_entity(db_message) if db_message else None
async def update(self, message: Message) -> Message:
"""Обновить сообщение"""
result = await self.session.execute(
select(MessageModel).where(MessageModel.message_id == message.message_id)
)
db_message = result.scalar_one_or_none()
if not db_message:
raise NotFoundError(f"Сообщение {message.message_id} не найдено")
db_message.content = message.content
db_message.role = message.role.value
db_message.sources = message.sources
await self.session.commit()
await self.session.refresh(db_message)
return self._to_entity(db_message)
async def delete(self, message_id: UUID) -> bool:
"""Удалить сообщение"""
result = await self.session.execute(
select(MessageModel).where(MessageModel.message_id == message_id)
)
db_message = result.scalar_one_or_none()
if not db_message:
return False
await self.session.delete(db_message)
await self.session.commit()
return True
async def list_by_conversation(self, conversation_id: UUID, skip: int = 0, limit: int = 100) -> list[Message]:
"""Получить сообщения беседы"""
result = await self.session.execute(
select(MessageModel)
.where(MessageModel.conversation_id == conversation_id)
.order_by(MessageModel.created_at)
.offset(skip)
.limit(limit)
)
db_messages = result.scalars().all()
return [self._to_entity(db_message) for db_message in db_messages]
def _to_entity(self, db_message: MessageModel | None) -> Message | None:
"""Преобразовать модель БД в доменную сущность"""
if not db_message:
return None
return Message(
message_id=db_message.message_id,
conversation_id=db_message.conversation_id,
content=db_message.content,
role=MessageRole(db_message.role),
sources=db_message.sources or {},
created_at=db_message.created_at
)
"""
Реализация репозитория сообщений для PostgreSQL
"""
from uuid import UUID
from typing import Optional
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from src.domain.entities.message import Message, MessageRole
from src.domain.repositories.message_repository import IMessageRepository
from src.infrastructure.database.models import MessageModel
from src.shared.exceptions import NotFoundError
class PostgreSQLMessageRepository(IMessageRepository):
"""PostgreSQL реализация репозитория сообщений"""
def __init__(self, session: AsyncSession):
self.session = session
async def create(self, message: Message) -> Message:
"""Создать сообщение"""
db_message = MessageModel(
message_id=message.message_id,
conversation_id=message.conversation_id,
content=message.content,
role=message.role.value,
sources=message.sources,
created_at=message.created_at
)
self.session.add(db_message)
await self.session.commit()
await self.session.refresh(db_message)
return self._to_entity(db_message)
async def get_by_id(self, message_id: UUID) -> Optional[Message]:
"""Получить сообщение по ID"""
result = await self.session.execute(
select(MessageModel).where(MessageModel.message_id == message_id)
)
db_message = result.scalar_one_or_none()
return self._to_entity(db_message) if db_message else None
async def update(self, message: Message) -> Message:
"""Обновить сообщение"""
result = await self.session.execute(
select(MessageModel).where(MessageModel.message_id == message.message_id)
)
db_message = result.scalar_one_or_none()
if not db_message:
raise NotFoundError(f"Сообщение {message.message_id} не найдено")
db_message.content = message.content
db_message.role = message.role.value
db_message.sources = message.sources
await self.session.commit()
await self.session.refresh(db_message)
return self._to_entity(db_message)
async def delete(self, message_id: UUID) -> bool:
"""Удалить сообщение"""
result = await self.session.execute(
select(MessageModel).where(MessageModel.message_id == message_id)
)
db_message = result.scalar_one_or_none()
if not db_message:
return False
await self.session.delete(db_message)
await self.session.commit()
return True
async def list_by_conversation(self, conversation_id: UUID, skip: int = 0, limit: int = 100) -> list[Message]:
"""Получить сообщения беседы"""
result = await self.session.execute(
select(MessageModel)
.where(MessageModel.conversation_id == conversation_id)
.order_by(MessageModel.created_at)
.offset(skip)
.limit(limit)
)
db_messages = result.scalars().all()
return [self._to_entity(db_message) for db_message in db_messages]
def _to_entity(self, db_message: MessageModel | None) -> Message | None:
"""Преобразовать модель БД в доменную сущность"""
if not db_message:
return None
return Message(
message_id=db_message.message_id,
conversation_id=db_message.conversation_id,
content=db_message.content,
role=MessageRole(db_message.role),
sources=db_message.sources or {},
created_at=db_message.created_at
)

View File

@ -1,95 +1,95 @@
"""
Реализация репозитория пользователей для PostgreSQL
"""
from uuid import UUID
from typing import Optional
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from src.domain.entities.user import User, UserRole
from src.domain.repositories.user_repository import IUserRepository
from src.infrastructure.database.models import UserModel
from src.shared.exceptions import NotFoundError
class PostgreSQLUserRepository(IUserRepository):
"""PostgreSQL реализация репозитория пользователей"""
def __init__(self, session: AsyncSession):
self.session = session
async def create(self, user: User) -> User:
"""Создать пользователя"""
db_user = UserModel(
user_id=user.user_id,
telegram_id=user.telegram_id,
role=user.role.value,
created_at=user.created_at
)
self.session.add(db_user)
await self.session.commit()
await self.session.refresh(db_user)
return self._to_entity(db_user)
async def get_by_id(self, user_id: UUID) -> Optional[User]:
"""Получить пользователя по ID"""
result = await self.session.execute(
select(UserModel).where(UserModel.user_id == user_id)
)
db_user = result.scalar_one_or_none()
return self._to_entity(db_user) if db_user else None
async def get_by_telegram_id(self, telegram_id: str) -> Optional[User]:
"""Получить пользователя по Telegram ID"""
result = await self.session.execute(
select(UserModel).where(UserModel.telegram_id == telegram_id)
)
db_user = result.scalar_one_or_none()
return self._to_entity(db_user) if db_user else None
async def update(self, user: User) -> User:
"""Обновить пользователя"""
result = await self.session.execute(
select(UserModel).where(UserModel.user_id == user.user_id)
)
db_user = result.scalar_one_or_none()
if not db_user:
raise NotFoundError(f"Пользователь {user.user_id} не найден")
db_user.telegram_id = user.telegram_id
db_user.role = user.role.value
await self.session.commit()
await self.session.refresh(db_user)
return self._to_entity(db_user)
async def delete(self, user_id: UUID) -> bool:
"""Удалить пользователя"""
result = await self.session.execute(
select(UserModel).where(UserModel.user_id == user_id)
)
db_user = result.scalar_one_or_none()
if not db_user:
return False
await self.session.delete(db_user)
await self.session.commit()
return True
async def list_all(self, skip: int = 0, limit: int = 100) -> list[User]:
"""Получить список всех пользователей"""
result = await self.session.execute(
select(UserModel).offset(skip).limit(limit)
)
db_users = result.scalars().all()
return [self._to_entity(db_user) for db_user in db_users]
def _to_entity(self, db_user: UserModel | None) -> User | None:
"""Преобразовать модель БД в доменную сущность"""
if not db_user:
return None
return User(
user_id=db_user.user_id,
telegram_id=db_user.telegram_id,
role=UserRole(db_user.role),
created_at=db_user.created_at
)
"""
Реализация репозитория пользователей для PostgreSQL
"""
from uuid import UUID
from typing import Optional
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from src.domain.entities.user import User, UserRole
from src.domain.repositories.user_repository import IUserRepository
from src.infrastructure.database.models import UserModel
from src.shared.exceptions import NotFoundError
class PostgreSQLUserRepository(IUserRepository):
"""PostgreSQL реализация репозитория пользователей"""
def __init__(self, session: AsyncSession):
self.session = session
async def create(self, user: User) -> User:
"""Создать пользователя"""
db_user = UserModel(
user_id=user.user_id,
telegram_id=user.telegram_id,
role=user.role.value,
created_at=user.created_at
)
self.session.add(db_user)
await self.session.commit()
await self.session.refresh(db_user)
return self._to_entity(db_user)
async def get_by_id(self, user_id: UUID) -> Optional[User]:
"""Получить пользователя по ID"""
result = await self.session.execute(
select(UserModel).where(UserModel.user_id == user_id)
)
db_user = result.scalar_one_or_none()
return self._to_entity(db_user) if db_user else None
async def get_by_telegram_id(self, telegram_id: str) -> Optional[User]:
"""Получить пользователя по Telegram ID"""
result = await self.session.execute(
select(UserModel).where(UserModel.telegram_id == telegram_id)
)
db_user = result.scalar_one_or_none()
return self._to_entity(db_user) if db_user else None
async def update(self, user: User) -> User:
"""Обновить пользователя"""
result = await self.session.execute(
select(UserModel).where(UserModel.user_id == user.user_id)
)
db_user = result.scalar_one_or_none()
if not db_user:
raise NotFoundError(f"Пользователь {user.user_id} не найден")
db_user.telegram_id = user.telegram_id
db_user.role = user.role.value
await self.session.commit()
await self.session.refresh(db_user)
return self._to_entity(db_user)
async def delete(self, user_id: UUID) -> bool:
"""Удалить пользователя"""
result = await self.session.execute(
select(UserModel).where(UserModel.user_id == user_id)
)
db_user = result.scalar_one_or_none()
if not db_user:
return False
await self.session.delete(db_user)
await self.session.commit()
return True
async def list_all(self, skip: int = 0, limit: int = 100) -> list[User]:
"""Получить список всех пользователей"""
result = await self.session.execute(
select(UserModel).offset(skip).limit(limit)
)
db_users = result.scalars().all()
return [self._to_entity(db_user) for db_user in db_users]
def _to_entity(self, db_user: UserModel | None) -> User | None:
"""Преобразовать модель БД в доменную сущность"""
if not db_user:
return None
return User(
user_id=db_user.user_id,
telegram_id=db_user.telegram_id,
role=UserRole(db_user.role),
created_at=db_user.created_at
)

View File

@ -1,4 +1,4 @@
"""
Presentation layer
"""
"""
Presentation layer
"""

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

View File

@ -1,4 +1,4 @@
"""
Shared utilities
"""
"""
Shared utilities
"""

View File

@ -1,48 +1,48 @@
"""
Конфигурация приложения
"""
from pydantic_settings import BaseSettings
from typing import Optional
class Settings(BaseSettings):
"""Настройки (загружаются из .env автоматически)"""
POSTGRES_HOST: str = "localhost"
POSTGRES_PORT: int = 5432
POSTGRES_USER: str = "postgres"
POSTGRES_PASSWORD: str = "postgres"
POSTGRES_DB: str = "lawyer_ai"
QDRANT_HOST: str = "localhost"
QDRANT_PORT: int = 6333
REDIS_HOST: str = "localhost"
REDIS_PORT: int = 6379
TELEGRAM_BOT_TOKEN: Optional[str] = None
YANDEX_OCR_API_KEY: Optional[str] = None
DEEPSEEK_API_KEY: Optional[str] = None
YANDEX_OCR_API_URL: str = "https://vision.api.cloud.yandex.net/vision/v1/batchAnalyze"
DEEPSEEK_API_URL: str = "https://api.deepseek.com/v1/chat/completions"
APP_NAME: str = "ИИ-юрист"
DEBUG: bool = False
SECRET_KEY: str = "your-secret-key-change-in-production"
CORS_ORIGINS: list[str] = ["*"]
@property
def database_url(self) -> str:
"""Вычисляемый URL подключения"""
return f"postgresql://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}"
class Config:
env_file = ".env"
case_sensitive = True
settings = Settings()
"""
Конфигурация приложения
"""
from pydantic_settings import BaseSettings
from typing import Optional
class Settings(BaseSettings):
"""Настройки (загружаются из .env автоматически)"""
POSTGRES_HOST: str = "localhost"
POSTGRES_PORT: int = 5432
POSTGRES_USER: str = "postgres"
POSTGRES_PASSWORD: str = "postgres"
POSTGRES_DB: str = "lawyer_ai"
QDRANT_HOST: str = "localhost"
QDRANT_PORT: int = 6333
REDIS_HOST: str = "localhost"
REDIS_PORT: int = 6379
TELEGRAM_BOT_TOKEN: Optional[str] = None
YANDEX_OCR_API_KEY: Optional[str] = None
DEEPSEEK_API_KEY: Optional[str] = None
YANDEX_OCR_API_URL: str = "https://vision.api.cloud.yandex.net/vision/v1/batchAnalyze"
DEEPSEEK_API_URL: str = "https://api.deepseek.com/v1/chat/completions"
APP_NAME: str = "ИИ-юрист"
DEBUG: bool = False
SECRET_KEY: str = "your-secret-key-change-in-production"
CORS_ORIGINS: list[str] = ["*"]
@property
def database_url(self) -> str:
"""Вычисляемый URL подключения"""
return f"postgresql://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}@{self.POSTGRES_HOST}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}"
class Config:
env_file = ".env"
case_sensitive = True
settings = Settings()

View File

@ -1,35 +1,35 @@
"""
Кастомные исключения приложения
"""
class LawyerAIException(Exception):
"""Базовое исключение приложения"""
pass
class NotFoundError(LawyerAIException):
"""Ресурс не найден"""
pass
class UnauthorizedError(LawyerAIException):
"""Пользователь не авторизован"""
pass
class ForbiddenError(LawyerAIException):
"""Доступ запрещен"""
pass
class ValidationError(LawyerAIException):
"""Ошибка валидации данных"""
pass
class DatabaseError(LawyerAIException):
"""Ошибка базы данных"""
pass
"""
Кастомные исключения приложения
"""
class LawyerAIException(Exception):
"""Базовое исключение приложения"""
pass
class NotFoundError(LawyerAIException):
"""Ресурс не найден"""
pass
class UnauthorizedError(LawyerAIException):
"""Пользователь не авторизован"""
pass
class ForbiddenError(LawyerAIException):
"""Доступ запрещен"""
pass
class ValidationError(LawyerAIException):
"""Ошибка валидации данных"""
pass
class DatabaseError(LawyerAIException):
"""Ошибка базы данных"""
pass