tested-auth-refactoring
Some checks failed
Deploy on push / deploy (push) Failing after 5s

This commit is contained in:
2025-07-25 01:04:15 +03:00
parent 867232e48f
commit b60a314ddd
28 changed files with 975 additions and 523 deletions

100
orm/base.py Normal file
View File

@@ -0,0 +1,100 @@
import builtins
import logging
from typing import Any, Callable, ClassVar, Type, Union
import orjson
from sqlalchemy import JSON, Column, Integer
from sqlalchemy.orm import declarative_base, declared_attr
logger = logging.getLogger(__name__)
# Глобальный реестр моделей
REGISTRY: dict[str, Type[Any]] = {}
# Список полей для фильтрации при сериализации
FILTERED_FIELDS: list[str] = []
# Создаем базовый класс для декларативных моделей
_Base = declarative_base()
class SafeColumnMixin:
"""
Миксин для безопасного присваивания значений столбцам с автоматическим преобразованием типов
"""
@declared_attr
def __safe_setattr__(self) -> Callable:
def safe_setattr(self, key: str, value: Any) -> None:
"""
Безопасно устанавливает атрибут для экземпляра.
Args:
key (str): Имя атрибута.
value (Any): Значение атрибута.
"""
setattr(self, key, value)
return safe_setattr
def __setattr__(self, key: str, value: Any) -> None:
"""
Переопределяем __setattr__ для использования безопасного присваивания
"""
safe_method = getattr(self, "__safe_setattr__", object.__setattr__)
safe_method(self, key, value)
class BaseModel(_Base, SafeColumnMixin): # type: ignore[valid-type,misc]
__abstract__ = True
__allow_unmapped__ = True
__table_args__: ClassVar[Union[dict[str, Any], tuple]] = {"extend_existing": True}
id = Column(Integer, primary_key=True)
def __init_subclass__(cls, **kwargs: Any) -> None:
REGISTRY[cls.__name__] = cls
super().__init_subclass__(**kwargs)
def dict(self, access: bool = False) -> builtins.dict[str, Any]:
"""
Конвертирует ORM объект в словарь.
Пропускает атрибуты, которые отсутствуют в объекте, но присутствуют в колонках таблицы.
Преобразует JSON поля в словари.
Returns:
Dict[str, Any]: Словарь с атрибутами объекта
"""
column_names = filter(lambda x: x not in FILTERED_FIELDS, self.__table__.columns.keys())
data: builtins.dict[str, Any] = {}
try:
for column_name in column_names:
try:
# Проверяем, существует ли атрибут в объекте
if hasattr(self, column_name):
value = getattr(self, column_name)
# Проверяем, является ли значение JSON и декодируем его при необходимости
if isinstance(value, (str, bytes)) and isinstance(
self.__table__.columns[column_name].type, JSON
):
try:
data[column_name] = orjson.loads(value)
except (TypeError, orjson.JSONDecodeError) as e:
logger.exception(f"Error decoding JSON for column '{column_name}': {e}")
data[column_name] = value
else:
data[column_name] = value
else:
# Пропускаем атрибут, если его нет в объекте (может быть добавлен после миграции)
logger.debug(f"Skipping missing attribute '{column_name}' for {self.__class__.__name__}")
except AttributeError as e:
logger.warning(f"Attribute error for column '{column_name}': {e}")
except Exception as e:
logger.exception(f"Error occurred while converting object to dictionary: {e}")
return data
def update(self, values: builtins.dict[str, Any]) -> None:
for key, value in values.items():
if hasattr(self, key):
setattr(self, key, value)

View File

@@ -3,7 +3,7 @@ import time
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.orm import relationship
from services.db import BaseModel as Base
from orm.base import BaseModel as Base
class ShoutCollection(Base):

View File

@@ -1,12 +1,12 @@
import time
from typing import Any, Dict
from sqlalchemy import JSON, Boolean, Column, ForeignKey, Index, Integer, String, Text, UniqueConstraint, distinct, func
from sqlalchemy import JSON, Boolean, Column, ForeignKey, Index, Integer, String, UniqueConstraint, distinct, func
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import relationship
from auth.orm import Author
from services.db import BaseModel
from orm.base import BaseModel
from services.rbac import get_permissions_for_role
# Словарь названий ролей
@@ -372,7 +372,7 @@ class CommunityAuthor(BaseModel):
id = Column(Integer, primary_key=True)
community_id = Column(Integer, ForeignKey("community.id"), nullable=False)
author_id = Column(Integer, ForeignKey("author.id"), nullable=False)
roles = Column(Text, nullable=True, comment="Roles (comma-separated)")
roles = Column(String, nullable=True, comment="Roles (comma-separated)")
joined_at = Column(Integer, nullable=False, default=lambda: int(time.time()))
# Связи
@@ -435,12 +435,16 @@ class CommunityAuthor(BaseModel):
def set_roles(self, roles: list[str]) -> None:
"""
Устанавливает полный список ролей (заменяет текущие)
Устанавливает роли для CommunityAuthor.
Args:
roles: Список ролей для установки
"""
self.role_list = roles
# Фильтруем и очищаем роли
valid_roles = [role.strip() for role in roles if role and role.strip()]
# Если список пустой, устанавливаем None
self.roles = ",".join(valid_roles) if valid_roles else ""
async def get_permissions(self) -> list[str]:
"""

View File

@@ -4,14 +4,14 @@ from sqlalchemy import JSON, Boolean, Column, ForeignKey, Integer, String
from sqlalchemy.orm import relationship
from auth.orm import Author
from orm.base import BaseModel as Base
from orm.topic import Topic
from services.db import BaseModel as Base
class DraftTopic(Base):
__tablename__ = "draft_topic"
id = None # type: ignore
id = None # type: ignore[misc]
shout = Column(ForeignKey("draft.id"), primary_key=True, index=True)
topic = Column(ForeignKey("topic.id"), primary_key=True, index=True)
main = Column(Boolean, nullable=True)
@@ -20,7 +20,7 @@ class DraftTopic(Base):
class DraftAuthor(Base):
__tablename__ = "draft_author"
id = None # type: ignore
id = None # type: ignore[misc]
shout = Column(ForeignKey("draft.id"), primary_key=True, index=True)
author = Column(ForeignKey("author.id"), primary_key=True, index=True)
caption = Column(String, nullable=True, default="")

View File

@@ -3,7 +3,7 @@ import enum
from sqlalchemy import Column, ForeignKey, String
from sqlalchemy.orm import relationship
from services.db import BaseModel as Base
from orm.base import BaseModel as Base
class InviteStatus(enum.Enum):
@@ -12,7 +12,7 @@ class InviteStatus(enum.Enum):
REJECTED = "REJECTED"
@classmethod
def from_string(cls, value):
def from_string(cls, value: str) -> "Invite":
return cls(value)

View File

@@ -1,35 +1,120 @@
import enum
import time
from datetime import datetime
from enum import Enum, auto
from sqlalchemy import JSON, Column, ForeignKey, Integer, String
from sqlalchemy import JSON, Column, DateTime, ForeignKey, Integer, String
from sqlalchemy import Enum as SQLAlchemyEnum
from sqlalchemy.orm import relationship
from auth.orm import Author
from services.db import BaseModel as Base
from orm.author import Author
from orm.base import BaseModel as Base
from services.logger import root_logger as logger
class NotificationStatus(Enum):
"""Статусы уведомлений."""
UNREAD = auto()
READ = auto()
ARCHIVED = auto()
@classmethod
def from_string(cls, value: str) -> "NotificationStatus":
"""
Создает экземпляр статуса уведомления из строки.
Args:
value (str): Строковое представление статуса.
Returns:
NotificationStatus: Экземпляр статуса уведомления.
"""
try:
return cls[value.upper()]
except KeyError:
logger.error(f"Неверный статус уведомления: {value}")
raise ValueError("Неверный статус уведомления") # noqa: B904
class NotificationKind(Enum):
"""Типы уведомлений."""
COMMENT = auto()
MENTION = auto()
REACTION = auto()
FOLLOW = auto()
INVITE = auto()
@classmethod
def from_string(cls, value: str) -> "NotificationKind":
"""
Создает экземпляр типа уведомления из строки.
Args:
value (str): Строковое представление типа.
Returns:
NotificationKind: Экземпляр типа уведомления.
"""
try:
return cls[value.upper()]
except KeyError:
logger.error(f"Неверный тип уведомления: {value}")
raise ValueError("Неверный тип уведомления") # noqa: B904
class NotificationEntity(enum.Enum):
REACTION = "reaction"
"""Сущности, связанные с уведомлениями."""
TOPIC = "topic"
COMMENT = "comment"
SHOUT = "shout"
FOLLOWER = "follower"
AUTHOR = "author"
COMMUNITY = "community"
@classmethod
def from_string(cls, value):
return cls(value)
def from_string(cls, value: str) -> "NotificationEntity":
"""
Создает экземпляр сущности уведомления из строки.
Args:
value (str): Строковое представление сущности.
Returns:
NotificationEntity: Экземпляр сущности уведомления.
"""
try:
return cls(value)
except ValueError:
logger.error(f"Неверная сущность уведомления: {value}")
raise ValueError("Неверная сущность уведомления") # noqa: B904
class NotificationAction(enum.Enum):
"""Действия в уведомлениях."""
CREATE = "create"
UPDATE = "update"
DELETE = "delete"
SEEN = "seen"
FOLLOW = "follow"
UNFOLLOW = "unfollow"
MENTION = "mention"
REACT = "react"
@classmethod
def from_string(cls, value):
return cls(value)
def from_string(cls, value: str) -> "NotificationAction":
"""
Создает экземпляр действия уведомления из строки.
Args:
value (str): Строковое представление действия.
Returns:
NotificationAction: Экземпляр действия уведомления.
"""
try:
return cls(value)
except ValueError:
logger.error(f"Неверное действие уведомления: {value}")
raise ValueError("Неверное действие уведомления") # noqa: B904
class NotificationSeen(Base):
@@ -42,22 +127,31 @@ class NotificationSeen(Base):
class Notification(Base):
__tablename__ = "notification"
id = Column(Integer, primary_key=True, autoincrement=True)
created_at = Column(Integer, server_default=str(int(time.time())))
id = Column(Integer, primary_key=True, index=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
entity = Column(String, nullable=False)
action = Column(String, nullable=False)
payload = Column(JSON, nullable=True)
status = Column(SQLAlchemyEnum(NotificationStatus), default=NotificationStatus.UNREAD)
kind = Column(SQLAlchemyEnum(NotificationKind), nullable=False)
seen = relationship(Author, secondary="notification_seen")
def set_entity(self, entity: NotificationEntity):
self.entity = entity.value # type: ignore[assignment]
"""Устанавливает сущность уведомления."""
self.entity = entity.value
def get_entity(self) -> NotificationEntity:
"""Возвращает сущность уведомления."""
return NotificationEntity.from_string(self.entity)
def set_action(self, action: NotificationAction):
self.action = action.value # type: ignore[assignment]
"""Устанавливает действие уведомления."""
self.action = action.value
def get_action(self) -> NotificationAction:
"""Возвращает действие уведомления."""
return NotificationAction.from_string(self.action)

View File

@@ -3,7 +3,7 @@ from enum import Enum as Enumeration
from sqlalchemy import Column, ForeignKey, Integer, String
from services.db import BaseModel as Base
from orm.base import BaseModel as Base
class ReactionKind(Enumeration):

View File

@@ -4,9 +4,9 @@ from sqlalchemy import JSON, Boolean, Column, ForeignKey, Index, Integer, String
from sqlalchemy.orm import relationship
from auth.orm import Author
from orm.base import BaseModel as Base
from orm.reaction import Reaction
from orm.topic import Topic
from services.db import BaseModel as Base
class ShoutTopic(Base):
@@ -21,7 +21,7 @@ class ShoutTopic(Base):
__tablename__ = "shout_topic"
id = None # type: ignore
id = None # type: ignore[misc]
shout = Column(ForeignKey("shout.id"), primary_key=True, index=True)
topic = Column(ForeignKey("topic.id"), primary_key=True, index=True)
main = Column(Boolean, nullable=True)
@@ -36,7 +36,7 @@ class ShoutTopic(Base):
class ShoutReactionsFollower(Base):
__tablename__ = "shout_reactions_followers"
id = None # type: ignore
id = None # type: ignore[misc]
follower = Column(ForeignKey("author.id"), primary_key=True, index=True)
shout = Column(ForeignKey("shout.id"), primary_key=True, index=True)
auto = Column(Boolean, nullable=False, default=False)
@@ -56,7 +56,7 @@ class ShoutAuthor(Base):
__tablename__ = "shout_author"
id = None # type: ignore
id = None # type: ignore[misc]
shout = Column(ForeignKey("shout.id"), primary_key=True, index=True)
author = Column(ForeignKey("author.id"), primary_key=True, index=True)
caption = Column(String, nullable=True, default="")

View File

@@ -2,7 +2,7 @@ import time
from sqlalchemy import JSON, Boolean, Column, ForeignKey, Index, Integer, String
from services.db import BaseModel as Base
from orm.base import BaseModel as Base
class TopicFollower(Base):
@@ -18,7 +18,7 @@ class TopicFollower(Base):
__tablename__ = "topic_followers"
id = None # type: ignore
id = None # type: ignore[misc]
follower = Column(Integer, ForeignKey("author.id"), primary_key=True)
topic = Column(Integer, ForeignKey("topic.id"), primary_key=True)
created_at = Column(Integer, nullable=False, default=int(time.time()))