Исходный код EJIO.user.model

## /user/model.py

import hashlib
from dataclasses import dataclass, asdict
from typing import Any, Self
from .flags import UserRole
from .err import IncorrectLoginError, SaltNotFoundError, IncorrectPasswordError, ServiceNotInitializedError

__all__ = [
    'User',
]


[документация] @dataclass(slots=True) class User: """ Модель пользователя. Сравнивается по регистронезависимому логину. """ id: str = "" login: str = "" name: str = "" role: UserRole = UserRole.USER crypto_key: str = "" # Системные поля метаданных avatar_id: str | None = None password_hash: str = "" salt: str = "" def __post_init__(self) -> None: """Нормализация текстовых полей при сборке.""" self.login = self.login.strip() if not self.name and self.login: self.name = self.login
[документация] @classmethod def create_bare(cls, login: str) -> Self: """ Внутренний фабричный метод создания «голой» заготовки пользователя. Имя по умолчанию выставляется равным логину. """ lg = login.strip() if not lg: raise IncorrectLoginError("User login must be of a valid format.") return cls(login=lg, name=lg)
@property def password(self) -> str: """Возвращает текущий сохраненный хэш пароля.""" return self.password_hash @password.setter def password(self, raw_password: str) -> None: """ Автоматически солит и хэширует входящий текстовый пароль. Использует соль, которая должна быть сгенерирована при создании пользователя. """ if not self.salt: raise SaltNotFoundError("Unable to hash the password: a salt has not yet been assigned to the user.") if not raw_password or len(raw_password.strip()) < 4: raise IncorrectPasswordError("The password cannot be empty or shorter than 4 characters.") salted_payload = raw_password + self.salt self.password_hash = hashlib.sha256(salted_payload.encode('utf-8')).hexdigest() @property def avatar(self) -> str | None:#todo: проверка инициализации """Возвращает абсолютный путь к физическому файлу изображения в ОС.""" if not self.avatar_id: return None from .facade import UserFacade if not UserFacade.avatar_service: raise ServiceNotInitializedError("Service not initialized yet.") return str(UserFacade.avatar_service.get_absolute_path(self.avatar_id)) @avatar.setter def avatar(self, source_path: str | None) -> None: """Принимает путь к внешнему файлу, автоматически копирует его через медиа-фасад.""" from .facade import UserFacade if not UserFacade.avatar_service: raise ServiceNotInitializedError("Service not initialized yet.") if source_path is None: if self.avatar_id: UserFacade.avatar_service.delete_file(self.avatar_id) self.avatar_id = None return # Копируем файл через выделенный сервис, получая UUID-идентификатор new_id = UserFacade.avatar_service.store_file(source_path) if self.avatar_id: UserFacade.avatar_service.delete_file(self.avatar_id) self.avatar_id = new_id
[документация] def to_dict(self) -> dict[str, Any]: """Автоматическая сериализация свойств на базе dataclasses.asdict.""" data = asdict(self) data["role"] = int(self.role) # Переводим Enum в число для сохранения return data
[документация] @classmethod def from_dict(cls, data: dict[str, Any]) -> 'User': """Восстанавливает объект из словаря, сохраняя все оригинальные ID с диска.""" if "role" in data: data["role"] = UserRole(data["role"]) return cls(**data)