infrastructure + deepseek+ocr
This commit is contained in:
parent
ae252a796c
commit
4a043f8e70
4
backend/src/infrastructure/external/__init__.py
vendored
Normal file
4
backend/src/infrastructure/external/__init__.py
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
"""
|
||||
External services
|
||||
"""
|
||||
|
||||
223
backend/src/infrastructure/external/deepseek_client.py
vendored
Normal file
223
backend/src/infrastructure/external/deepseek_client.py
vendored
Normal file
@ -0,0 +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
|
||||
|
||||
35
backend/src/infrastructure/external/telegram_auth.py
vendored
Normal file
35
backend/src/infrastructure/external/telegram_auth.py
vendored
Normal file
@ -0,0 +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
|
||||
}
|
||||
|
||||
280
backend/src/infrastructure/external/yandex_ocr.py
vendored
Normal file
280
backend/src/infrastructure/external/yandex_ocr.py
vendored
Normal file
@ -0,0 +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
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
"""
|
||||
PostgreSQL repository implementations
|
||||
"""
|
||||
|
||||
@ -0,0 +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
|
||||
)
|
||||
|
||||
@ -0,0 +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
|
||||
)
|
||||
|
||||
@ -0,0 +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
|
||||
)
|
||||
|
||||
@ -0,0 +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
|
||||
)
|
||||
|
||||
@ -0,0 +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
|
||||
)
|
||||
|
||||
@ -0,0 +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
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user