forked from HSE_team/BetterCallPraskovia
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