2024-08-07 09:51:09 +03:00
|
|
|
import asyncio
|
2025-07-31 18:55:59 +03:00
|
|
|
import traceback
|
2024-08-12 11:00:01 +03:00
|
|
|
|
2024-05-30 14:25:35 +03:00
|
|
|
from sqlalchemy import and_, join, select
|
2024-08-12 11:00:01 +03:00
|
|
|
|
2025-08-17 16:33:54 +03:00
|
|
|
# Импорт Author, AuthorFollower отложен для избежания циклических импортов
|
2025-05-29 12:37:39 +03:00
|
|
|
from cache.cache import cache_author, cache_topic
|
[0.9.7] - 2025-08-18
### 🔄 Изменения
- **SQLAlchemy KeyError** - исправление ошибки `KeyError: Reaction` при инициализации
- **Исправлена ошибка SQLAlchemy**: Устранена проблема `InvalidRequestError: When initializing mapper Mapper[Shout(shout)], expression Reaction failed to locate a name (Reaction)`
### 🧪 Тестирование
- **Исправление тестов** - адаптация к новой структуре моделей
- **RBAC инициализация** - добавление `rbac.initialize_rbac()` в `conftest.py`
- **Создан тест для getSession**: Добавлен комплексный тест `test_getSession_cookies.py` с проверкой всех сценариев
- **Покрытие edge cases**: Тесты проверяют работу с валидными/невалидными токенами, отсутствующими пользователями
- **Мокирование зависимостей**: Использование unittest.mock для изоляции тестируемого кода
### 🔧 Рефакторинг
- **Упрощена архитектура**: Убраны сложные конструкции с отложенными импортами, заменены на чистую архитектуру
- **Перемещение моделей** - `Author` и связанные модели перенесены в `orm/author.py`: Вынесены базовые модели пользователей (`Author`, `AuthorFollower`, `AuthorBookmark`, `AuthorRating`) из `orm.author` в отдельный модуль
- **Устранены циклические импорты**: Разорван цикл между `auth.core` → `orm.community` → `orm.author` через реструктуризацию архитектуры
- **Создан модуль `utils/password.py`**: Класс `Password` вынесен в utils для избежания циклических зависимостей
- **Оптимизированы импорты моделей**: Убран прямой импорт `Shout` из `orm/community.py`, заменен на строковые ссылки
### 🔧 Авторизация с cookies
- **getSession теперь работает с cookies**: Мутация `getSession` теперь может получать токен из httpOnly cookies даже без заголовка Authorization
- **Убрано требование авторизации**: `getSession` больше не требует декоратор `@login_required`, работает автономно
- **Поддержка dual-авторизации**: Токен может быть получен как из заголовка Authorization, так и из cookie `session_token`
- **Автоматическая установка cookies**: Middleware автоматически устанавливает httpOnly cookies при успешном `getSession`
- **Обновлена GraphQL схема**: `SessionInfo` теперь содержит поля `success`, `error` и опциональные `token`, `author`
- **Единообразная обработка токенов**: Все модули теперь используют централизованные функции для работы с токенами
- **Улучшена обработка ошибок**: Добавлена детальная валидация токенов и пользователей в `getSession`
- **Логирование операций**: Добавлены подробные логи для отслеживания процесса авторизации
### 📝 Документация
- **Обновлена схема GraphQL**: `SessionInfo` тип теперь соответствует новому формату ответа
- Обновлена документация RBAC
- Обновлена документация авторизации с cookies
2025-08-18 14:25:25 +03:00
|
|
|
from orm.author import Author, AuthorFollower
|
2024-08-12 11:00:01 +03:00
|
|
|
from orm.shout import Shout, ShoutAuthor, ShoutReactionsFollower, ShoutTopic
|
2024-05-30 14:25:35 +03:00
|
|
|
from orm.topic import Topic, TopicFollower
|
2024-05-30 14:29:00 +03:00
|
|
|
from resolvers.stat import get_with_stat
|
2025-08-17 17:56:31 +03:00
|
|
|
from storage.db import local_session
|
|
|
|
|
from storage.redis import redis
|
2025-06-02 02:56:11 +03:00
|
|
|
from utils.encoders import fast_json_dumps
|
2024-08-07 08:57:56 +03:00
|
|
|
from utils.logger import root_logger as logger
|
2024-05-30 14:25:35 +03:00
|
|
|
|
|
|
|
|
|
2024-08-07 09:51:09 +03:00
|
|
|
# Предварительное кеширование подписчиков автора
|
2025-06-02 02:56:11 +03:00
|
|
|
async def precache_authors_followers(author_id, session) -> None:
|
|
|
|
|
authors_followers: set[int] = set()
|
[0.9.7] - 2025-08-18
### 🔄 Изменения
- **SQLAlchemy KeyError** - исправление ошибки `KeyError: Reaction` при инициализации
- **Исправлена ошибка SQLAlchemy**: Устранена проблема `InvalidRequestError: When initializing mapper Mapper[Shout(shout)], expression Reaction failed to locate a name (Reaction)`
### 🧪 Тестирование
- **Исправление тестов** - адаптация к новой структуре моделей
- **RBAC инициализация** - добавление `rbac.initialize_rbac()` в `conftest.py`
- **Создан тест для getSession**: Добавлен комплексный тест `test_getSession_cookies.py` с проверкой всех сценариев
- **Покрытие edge cases**: Тесты проверяют работу с валидными/невалидными токенами, отсутствующими пользователями
- **Мокирование зависимостей**: Использование unittest.mock для изоляции тестируемого кода
### 🔧 Рефакторинг
- **Упрощена архитектура**: Убраны сложные конструкции с отложенными импортами, заменены на чистую архитектуру
- **Перемещение моделей** - `Author` и связанные модели перенесены в `orm/author.py`: Вынесены базовые модели пользователей (`Author`, `AuthorFollower`, `AuthorBookmark`, `AuthorRating`) из `orm.author` в отдельный модуль
- **Устранены циклические импорты**: Разорван цикл между `auth.core` → `orm.community` → `orm.author` через реструктуризацию архитектуры
- **Создан модуль `utils/password.py`**: Класс `Password` вынесен в utils для избежания циклических зависимостей
- **Оптимизированы импорты моделей**: Убран прямой импорт `Shout` из `orm/community.py`, заменен на строковые ссылки
### 🔧 Авторизация с cookies
- **getSession теперь работает с cookies**: Мутация `getSession` теперь может получать токен из httpOnly cookies даже без заголовка Authorization
- **Убрано требование авторизации**: `getSession` больше не требует декоратор `@login_required`, работает автономно
- **Поддержка dual-авторизации**: Токен может быть получен как из заголовка Authorization, так и из cookie `session_token`
- **Автоматическая установка cookies**: Middleware автоматически устанавливает httpOnly cookies при успешном `getSession`
- **Обновлена GraphQL схема**: `SessionInfo` теперь содержит поля `success`, `error` и опциональные `token`, `author`
- **Единообразная обработка токенов**: Все модули теперь используют централизованные функции для работы с токенами
- **Улучшена обработка ошибок**: Добавлена детальная валидация токенов и пользователей в `getSession`
- **Логирование операций**: Добавлены подробные логи для отслеживания процесса авторизации
### 📝 Документация
- **Обновлена схема GraphQL**: `SessionInfo` тип теперь соответствует новому формату ответа
- Обновлена документация RBAC
- Обновлена документация авторизации с cookies
2025-08-18 14:25:25 +03:00
|
|
|
followers_query = select(AuthorFollower.follower).where(AuthorFollower.following == author_id)
|
2024-06-04 09:07:46 +03:00
|
|
|
result = session.execute(followers_query)
|
2024-08-07 09:51:09 +03:00
|
|
|
authors_followers.update(row[0] for row in result if row[0])
|
2024-06-04 09:07:46 +03:00
|
|
|
|
2025-06-02 02:56:11 +03:00
|
|
|
followers_payload = fast_json_dumps(list(authors_followers))
|
2024-06-04 09:07:46 +03:00
|
|
|
await redis.execute("SET", f"author:followers:{author_id}", followers_payload)
|
|
|
|
|
|
|
|
|
|
|
2024-08-07 09:51:09 +03:00
|
|
|
# Предварительное кеширование подписок автора
|
2025-06-02 02:56:11 +03:00
|
|
|
async def precache_authors_follows(author_id, session) -> None:
|
2024-06-04 09:07:46 +03:00
|
|
|
follows_topics_query = select(TopicFollower.topic).where(TopicFollower.follower == author_id)
|
[0.9.7] - 2025-08-18
### 🔄 Изменения
- **SQLAlchemy KeyError** - исправление ошибки `KeyError: Reaction` при инициализации
- **Исправлена ошибка SQLAlchemy**: Устранена проблема `InvalidRequestError: When initializing mapper Mapper[Shout(shout)], expression Reaction failed to locate a name (Reaction)`
### 🧪 Тестирование
- **Исправление тестов** - адаптация к новой структуре моделей
- **RBAC инициализация** - добавление `rbac.initialize_rbac()` в `conftest.py`
- **Создан тест для getSession**: Добавлен комплексный тест `test_getSession_cookies.py` с проверкой всех сценариев
- **Покрытие edge cases**: Тесты проверяют работу с валидными/невалидными токенами, отсутствующими пользователями
- **Мокирование зависимостей**: Использование unittest.mock для изоляции тестируемого кода
### 🔧 Рефакторинг
- **Упрощена архитектура**: Убраны сложные конструкции с отложенными импортами, заменены на чистую архитектуру
- **Перемещение моделей** - `Author` и связанные модели перенесены в `orm/author.py`: Вынесены базовые модели пользователей (`Author`, `AuthorFollower`, `AuthorBookmark`, `AuthorRating`) из `orm.author` в отдельный модуль
- **Устранены циклические импорты**: Разорван цикл между `auth.core` → `orm.community` → `orm.author` через реструктуризацию архитектуры
- **Создан модуль `utils/password.py`**: Класс `Password` вынесен в utils для избежания циклических зависимостей
- **Оптимизированы импорты моделей**: Убран прямой импорт `Shout` из `orm/community.py`, заменен на строковые ссылки
### 🔧 Авторизация с cookies
- **getSession теперь работает с cookies**: Мутация `getSession` теперь может получать токен из httpOnly cookies даже без заголовка Authorization
- **Убрано требование авторизации**: `getSession` больше не требует декоратор `@login_required`, работает автономно
- **Поддержка dual-авторизации**: Токен может быть получен как из заголовка Authorization, так и из cookie `session_token`
- **Автоматическая установка cookies**: Middleware автоматически устанавливает httpOnly cookies при успешном `getSession`
- **Обновлена GraphQL схема**: `SessionInfo` теперь содержит поля `success`, `error` и опциональные `token`, `author`
- **Единообразная обработка токенов**: Все модули теперь используют централизованные функции для работы с токенами
- **Улучшена обработка ошибок**: Добавлена детальная валидация токенов и пользователей в `getSession`
- **Логирование операций**: Добавлены подробные логи для отслеживания процесса авторизации
### 📝 Документация
- **Обновлена схема GraphQL**: `SessionInfo` тип теперь соответствует новому формату ответа
- Обновлена документация RBAC
- Обновлена документация авторизации с cookies
2025-08-18 14:25:25 +03:00
|
|
|
follows_authors_query = select(AuthorFollower.following).where(AuthorFollower.follower == author_id)
|
2025-05-29 12:37:39 +03:00
|
|
|
follows_shouts_query = select(ShoutReactionsFollower.shout).where(ShoutReactionsFollower.follower == author_id)
|
2024-06-04 09:07:46 +03:00
|
|
|
|
2024-08-07 09:51:09 +03:00
|
|
|
follows_topics = {row[0] for row in session.execute(follows_topics_query) if row[0]}
|
|
|
|
|
follows_authors = {row[0] for row in session.execute(follows_authors_query) if row[0]}
|
|
|
|
|
follows_shouts = {row[0] for row in session.execute(follows_shouts_query) if row[0]}
|
2024-06-04 09:07:46 +03:00
|
|
|
|
2025-06-02 02:56:11 +03:00
|
|
|
topics_payload = fast_json_dumps(list(follows_topics))
|
|
|
|
|
authors_payload = fast_json_dumps(list(follows_authors))
|
|
|
|
|
shouts_payload = fast_json_dumps(list(follows_shouts))
|
2024-08-07 09:51:09 +03:00
|
|
|
|
|
|
|
|
await asyncio.gather(
|
|
|
|
|
redis.execute("SET", f"author:follows-topics:{author_id}", topics_payload),
|
|
|
|
|
redis.execute("SET", f"author:follows-authors:{author_id}", authors_payload),
|
|
|
|
|
redis.execute("SET", f"author:follows-shouts:{author_id}", shouts_payload),
|
|
|
|
|
)
|
2024-06-04 09:07:46 +03:00
|
|
|
|
|
|
|
|
|
2024-08-07 09:51:09 +03:00
|
|
|
# Предварительное кеширование авторов тем
|
2025-06-02 02:56:11 +03:00
|
|
|
async def precache_topics_authors(topic_id: int, session) -> None:
|
2024-06-04 09:07:46 +03:00
|
|
|
topic_authors_query = (
|
|
|
|
|
select(ShoutAuthor.author)
|
|
|
|
|
.select_from(join(ShoutTopic, Shout, ShoutTopic.shout == Shout.id))
|
|
|
|
|
.join(ShoutAuthor, ShoutAuthor.shout == Shout.id)
|
2025-07-31 18:55:59 +03:00
|
|
|
.where(
|
2024-06-04 09:07:46 +03:00
|
|
|
and_(
|
|
|
|
|
ShoutTopic.topic == topic_id,
|
|
|
|
|
Shout.published_at.is_not(None),
|
|
|
|
|
Shout.deleted_at.is_(None),
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
)
|
2024-08-07 09:51:09 +03:00
|
|
|
topic_authors = {row[0] for row in session.execute(topic_authors_query) if row[0]}
|
2024-06-04 09:07:46 +03:00
|
|
|
|
2025-06-02 02:56:11 +03:00
|
|
|
authors_payload = fast_json_dumps(list(topic_authors))
|
2024-06-04 09:07:46 +03:00
|
|
|
await redis.execute("SET", f"topic:authors:{topic_id}", authors_payload)
|
|
|
|
|
|
|
|
|
|
|
2024-08-07 09:51:09 +03:00
|
|
|
# Предварительное кеширование подписчиков тем
|
2025-06-02 02:56:11 +03:00
|
|
|
async def precache_topics_followers(topic_id: int, session) -> None:
|
2024-06-04 09:07:46 +03:00
|
|
|
followers_query = select(TopicFollower.follower).where(TopicFollower.topic == topic_id)
|
2024-08-07 09:51:09 +03:00
|
|
|
topic_followers = {row[0] for row in session.execute(followers_query) if row[0]}
|
2024-06-04 09:07:46 +03:00
|
|
|
|
2025-06-02 02:56:11 +03:00
|
|
|
followers_payload = fast_json_dumps(list(topic_followers))
|
2024-06-04 09:07:46 +03:00
|
|
|
await redis.execute("SET", f"topic:followers:{topic_id}", followers_payload)
|
|
|
|
|
|
|
|
|
|
|
2025-06-02 02:56:11 +03:00
|
|
|
async def precache_data() -> None:
|
2024-10-14 13:08:43 +03:00
|
|
|
logger.info("precaching...")
|
2025-05-20 11:23:00 -03:00
|
|
|
logger.debug("Entering precache_data")
|
2024-06-05 21:40:32 +03:00
|
|
|
try:
|
2025-06-02 02:56:11 +03:00
|
|
|
# Список паттернов ключей, которые нужно сохранить при FLUSHDB
|
|
|
|
|
preserve_patterns = [
|
|
|
|
|
"migrated_views_*", # Данные миграции просмотров
|
|
|
|
|
"session:*", # Сессии пользователей
|
|
|
|
|
"env_vars:*", # Переменные окружения
|
|
|
|
|
"oauth_*", # OAuth токены
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
# Сохраняем все важные ключи перед очисткой
|
|
|
|
|
all_keys_to_preserve = []
|
|
|
|
|
preserved_data = {}
|
|
|
|
|
|
|
|
|
|
for pattern in preserve_patterns:
|
|
|
|
|
keys = await redis.execute("KEYS", pattern)
|
|
|
|
|
if keys:
|
|
|
|
|
all_keys_to_preserve.extend(keys)
|
|
|
|
|
logger.info(f"Найдено {len(keys)} ключей по паттерну '{pattern}'")
|
|
|
|
|
|
|
|
|
|
if all_keys_to_preserve:
|
|
|
|
|
logger.info(f"Сохраняем {len(all_keys_to_preserve)} важных ключей перед FLUSHDB")
|
|
|
|
|
for key in all_keys_to_preserve:
|
|
|
|
|
try:
|
|
|
|
|
# Определяем тип ключа и сохраняем данные
|
|
|
|
|
key_type = await redis.execute("TYPE", key)
|
|
|
|
|
if key_type == "hash":
|
|
|
|
|
preserved_data[key] = await redis.execute("HGETALL", key)
|
|
|
|
|
elif key_type == "string":
|
|
|
|
|
preserved_data[key] = await redis.execute("GET", key)
|
|
|
|
|
elif key_type == "set":
|
|
|
|
|
preserved_data[key] = await redis.execute("SMEMBERS", key)
|
|
|
|
|
elif key_type == "list":
|
|
|
|
|
preserved_data[key] = await redis.execute("LRANGE", key, 0, -1)
|
|
|
|
|
elif key_type == "zset":
|
|
|
|
|
preserved_data[key] = await redis.execute("ZRANGE", key, 0, -1, "WITHSCORES")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Ошибка при сохранении ключа {key}: {e}")
|
|
|
|
|
continue
|
|
|
|
|
|
2024-06-05 21:40:32 +03:00
|
|
|
await redis.execute("FLUSHDB")
|
2025-05-20 11:23:00 -03:00
|
|
|
logger.debug("Redis database flushed")
|
2024-10-14 12:31:55 +03:00
|
|
|
logger.info("redis: FLUSHDB")
|
2024-06-05 21:40:32 +03:00
|
|
|
|
2025-06-02 02:56:11 +03:00
|
|
|
# Восстанавливаем все сохранённые ключи
|
|
|
|
|
if preserved_data:
|
|
|
|
|
logger.info(f"Восстанавливаем {len(preserved_data)} сохранённых ключей")
|
|
|
|
|
for key, data in preserved_data.items():
|
|
|
|
|
try:
|
|
|
|
|
if isinstance(data, dict) and data:
|
|
|
|
|
# Hash
|
|
|
|
|
for field, val in data.items():
|
2025-07-31 19:31:51 +03:00
|
|
|
await redis.execute("HSET", key, field, val)
|
2025-06-02 02:56:11 +03:00
|
|
|
elif isinstance(data, str) and data:
|
|
|
|
|
# String
|
|
|
|
|
await redis.execute("SET", key, data)
|
|
|
|
|
elif isinstance(data, list) and data:
|
|
|
|
|
# List или ZSet
|
2025-08-17 16:33:54 +03:00
|
|
|
if any(isinstance(item, list | tuple) and len(item) == 2 for item in data):
|
2025-06-02 02:56:11 +03:00
|
|
|
# ZSet with scores
|
|
|
|
|
for item in data:
|
2025-08-17 16:33:54 +03:00
|
|
|
if isinstance(item, list | tuple) and len(item) == 2:
|
2025-06-02 02:56:11 +03:00
|
|
|
await redis.execute("ZADD", key, item[1], item[0])
|
|
|
|
|
else:
|
|
|
|
|
# Regular list
|
|
|
|
|
await redis.execute("LPUSH", key, *data)
|
|
|
|
|
elif isinstance(data, set) and data:
|
|
|
|
|
# Set
|
|
|
|
|
await redis.execute("SADD", key, *data)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Ошибка при восстановлении ключа {key}: {e}")
|
|
|
|
|
continue
|
2024-10-24 00:01:09 +03:00
|
|
|
|
2025-05-20 11:23:00 -03:00
|
|
|
logger.info("Beginning topic precache phase")
|
2024-06-05 21:40:32 +03:00
|
|
|
with local_session() as session:
|
2024-08-07 09:51:09 +03:00
|
|
|
# topics
|
2024-08-07 11:52:07 +03:00
|
|
|
q = select(Topic).where(Topic.community == 1)
|
|
|
|
|
topics = get_with_stat(q)
|
2025-05-20 11:23:00 -03:00
|
|
|
logger.info(f"Found {len(topics)} topics to precache")
|
2024-08-07 09:51:09 +03:00
|
|
|
for topic in topics:
|
2024-08-07 11:52:16 +03:00
|
|
|
topic_dict = topic.dict() if hasattr(topic, "dict") else topic
|
2025-06-16 20:20:23 +03:00
|
|
|
# logger.debug(f"Precaching topic id={topic_dict.get('id')}")
|
2024-08-07 11:38:34 +03:00
|
|
|
await cache_topic(topic_dict)
|
2025-06-16 20:20:23 +03:00
|
|
|
# logger.debug(f"Cached topic id={topic_dict.get('id')}")
|
2024-08-07 09:51:09 +03:00
|
|
|
await asyncio.gather(
|
2024-08-07 12:18:29 +03:00
|
|
|
precache_topics_followers(topic_dict["id"], session),
|
|
|
|
|
precache_topics_authors(topic_dict["id"], session),
|
2024-08-07 09:51:09 +03:00
|
|
|
)
|
2025-06-16 20:20:23 +03:00
|
|
|
# logger.debug(f"Finished precaching followers and authors for topic id={topic_dict.get('id')}")
|
2024-08-07 09:51:09 +03:00
|
|
|
logger.info(f"{len(topics)} topics and their followings precached")
|
|
|
|
|
|
|
|
|
|
# authors
|
2025-05-16 09:23:48 +03:00
|
|
|
authors = get_with_stat(select(Author))
|
|
|
|
|
# logger.info(f"{len(authors)} authors found in database")
|
2024-08-07 09:51:09 +03:00
|
|
|
for author in authors:
|
|
|
|
|
if isinstance(author, Author):
|
|
|
|
|
profile = author.dict()
|
|
|
|
|
author_id = profile.get("id")
|
2025-05-16 09:23:48 +03:00
|
|
|
# user_id = profile.get("user", "").strip()
|
|
|
|
|
if author_id: # and user_id:
|
2024-08-07 09:51:09 +03:00
|
|
|
await cache_author(profile)
|
|
|
|
|
await asyncio.gather(
|
2025-05-16 09:23:48 +03:00
|
|
|
precache_authors_followers(author_id, session),
|
|
|
|
|
precache_authors_follows(author_id, session),
|
2024-08-07 09:51:09 +03:00
|
|
|
)
|
2025-06-16 20:20:23 +03:00
|
|
|
# logger.debug(f"Finished precaching followers and follows for author id={author_id}")
|
2024-08-07 09:51:09 +03:00
|
|
|
else:
|
|
|
|
|
logger.error(f"fail caching {author}")
|
|
|
|
|
logger.info(f"{len(authors)} authors and their followings precached")
|
2024-06-05 21:40:32 +03:00
|
|
|
except Exception as exc:
|
2024-08-07 11:53:31 +03:00
|
|
|
traceback.print_exc()
|
2024-08-07 09:51:09 +03:00
|
|
|
logger.error(f"Error in precache_data: {exc}")
|