""" Базовый абстрактный класс для всех сущностей с поддержкой триплетного подхода. """ import uuid from abc import abstractmethod from dataclasses import dataclass, field, fields from uuid import UUID @dataclass class LinkerEntity: """ Общий класс для всех сущностей в системе извлечения и сборки. Поддерживает триплетный подход, где каждая сущность может опционально связывать две другие сущности. Attributes: id (UUID): Уникальный идентификатор сущности. name (str): Название сущности. text (str): Текстое представление сущности. in_search_text (str | None): Текст для поиска. Если задан, используется в __str__, иначе используется обычное представление. metadata (dict): Метаданные сущности. source_id (UUID | None): Опциональный идентификатор исходной сущности. Если указан, эта сущность является связью. target_id (UUID | None): Опциональный идентификатор целевой сущности. Если указан, эта сущность является связью. number_in_relation (int | None): Используется в случае связей один-ко-многим, указывает номер целевой сущности в списке. type (str): Тип сущности. """ id: UUID name: str text: str metadata: dict # JSON с метаданными in_search_text: str | None = None source_id: UUID | None = None target_id: UUID | None = None number_in_relation: int | None = None type: str = field(default_factory=lambda: "Entity") def __post_init__(self): if self.id is None: self.id = uuid.uuid4() # Проверяем корректность полей связи if (self.source_id is not None and self.target_id is None) or \ (self.source_id is None and self.target_id is not None): raise ValueError("source_id и target_id должны быть либо оба указаны, либо оба None") def is_link(self) -> bool: """ Проверяет, является ли сущность связью (имеет и source_id, и target_id). Returns: bool: True, если сущность является связью, иначе False """ return self.source_id is not None and self.target_id is not None def __str__(self) -> str: """ Возвращает строковое представление сущности. Если задан in_search_text, возвращает его, иначе возвращает стандартное представление. """ if self.in_search_text is not None: return self.in_search_text return f"{self.name}: {self.text}" def __eq__(self, other: 'LinkerEntity') -> bool: """ Сравнивает текущую сущность с другой. Args: other: Другая сущность для сравнения Returns: bool: True если сущности совпадают, иначе False """ if not isinstance(other, self.__class__): return False basic_equality = ( self.id == other.id and self.name == other.name and self.text == other.text and self.type == other.type ) # Если мы имеем дело со связями, также проверяем поля связи if self.is_link() or other.is_link(): return ( basic_equality and self.source_id == other.source_id and self.target_id == other.target_id ) return basic_equality def serialize(self) -> 'LinkerEntity': """ Сериализует сущность в простейшую форму сущности, передавая все дополнительные поля в метаданные. """ # Получаем список полей базового класса known_fields = {field.name for field in fields(LinkerEntity)} # Получаем все атрибуты текущего объекта dict_entity = {} for attr_name in dir(self): # Пропускаем служебные атрибуты, методы и уже известные поля if ( attr_name.startswith('_') or attr_name in known_fields or callable(getattr(self, attr_name)) ): continue # Добавляем дополнительные поля в словарь dict_entity[attr_name] = getattr(self, attr_name) # Преобразуем имена дополнительных полей, добавляя префикс "_" dict_entity = {f'_{name}': value for name, value in dict_entity.items()} # Объединяем с существующими метаданными dict_entity = {**dict_entity, **self.metadata} result_type = self.type if result_type == "Entity": result_type = self.__class__.__name__ # Создаем базовый объект LinkerEntity с новыми метаданными return LinkerEntity( id=self.id, name=self.name, text=self.text, in_search_text=self.in_search_text, metadata=dict_entity, source_id=self.source_id, target_id=self.target_id, number_in_relation=self.number_in_relation, type=result_type, ) @classmethod @abstractmethod def deserialize(cls, data: 'LinkerEntity') -> 'Self': """ Десериализует сущность из простейшей формы сущности, учитывая все дополнительные поля в метаданных. """ raise NotImplementedError( f"Метод deserialize для класса {cls.__class__.__name__} не реализован" ) # Реестр для хранения всех наследников LinkerEntity _entity_classes = {} @classmethod def register_entity_class(cls, entity_class): """ Регистрирует класс-наследник в реестре. Args: entity_class: Класс для регистрации """ entity_type = entity_class.__name__ cls._entity_classes[entity_type] = entity_class # Также регистрируем по типу, если он отличается от имени класса if hasattr(entity_class, 'type') and isinstance(entity_class.type, str): cls._entity_classes[entity_class.type] = entity_class @classmethod def deserialize(cls, data: 'LinkerEntity') -> 'LinkerEntity': """ Десериализует сущность в нужный тип на основе поля type. Args: data: Сериализованная сущность типа LinkerEntity Returns: Десериализованная сущность правильного типа """ # Получаем тип сущности entity_type = data.type # Проверяем реестр классов if entity_type in cls._entity_classes: try: return cls._entity_classes[entity_type].deserialize(data) except (AttributeError, NotImplementedError) as e: # Если метод не реализован, возвращаем исходную сущность return data # Если тип не найден в реестре, просто возвращаем исходную сущность # Больше не используем опасное сканирование sys.modules return data # Декоратор для регистрации производных классов def register_entity(cls): """ Декоратор для регистрации классов-наследников LinkerEntity. Пример использования: @register_entity class MyEntity(LinkerEntity): type = "my_entity" Args: cls: Класс, который нужно зарегистрировать Returns: Исходный класс (без изменений) """ # Регистрируем класс в реестр, используя его имя или указанный тип entity_type = getattr(cls, 'type', cls.__name__) LinkerEntity._entity_classes[entity_type] = cls return cls