presentation layer

This commit is contained in:
2025-12-14 22:57:54 +03:00
parent 4a043f8e70
commit 036741c0bf
59 changed files with 3226 additions and 1865 deletions
+4 -4
View File
@@ -1,4 +1,4 @@
"""
Infrastructure layer
"""
"""
Infrastructure layer
"""
@@ -1,4 +1,4 @@
"""
Database infrastructure
"""
"""
Database infrastructure
"""
+32 -32
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()
+109 -109
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": "Уникальный доступ пользователя к коллекции"},
)
+4 -4
View File
@@ -1,4 +1,4 @@
"""
External services
"""
"""
External services
"""
+223 -223
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
+35 -35
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
}
+280 -280
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
@@ -1,4 +1,4 @@
"""
PostgreSQL repository implementations
"""
"""
PostgreSQL repository implementations
"""
@@ -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
)
@@ -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
)
@@ -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
)
@@ -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
)
@@ -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
)
@@ -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
)