db created

This commit is contained in:
bokho 2025-12-14 17:28:04 +03:00
parent 39bcd1c16e
commit ae252a796c
18 changed files with 596 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +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()

View File

@ -0,0 +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": "Уникальный доступ пользователя к коллекции"},
)