authors-sort-fix3
All checks were successful
Deploy on push / deploy (push) Successful in 6s

This commit is contained in:
Untone 2025-06-26 17:19:42 +03:00
parent b5aa7032eb
commit 599a6c9f59
4 changed files with 127 additions and 46 deletions

View File

@ -1,5 +1,20 @@
# Changelog
## [0.5.6] - 2025-06-26
### Исправления API
- **Исправлена сортировка авторов**: Решена проблема с неправильной обработкой параметра сортировки в `load_authors_by`:
- **Проблема**: При запросе авторов с параметром сортировки `order="shouts"` всегда применялась сортировка по `followers`
- **Исправления**:
- Создан специальный тип `AuthorsBy` на основе схемы GraphQL для строгой типизации параметра сортировки
- Улучшена обработка параметра `by` в функции `load_authors_by` для поддержки всех полей из схемы GraphQL
- Исправлена логика определения поля сортировки `stats_sort_field` для корректного применения сортировки
- Добавлен флаг `default_sort_applied` для предотвращения конфликтов между разными типами сортировки
- Улучшено кеширование с учетом параметра сортировки в ключе кеша
- Добавлено подробное логирование для отладки SQL запросов и результатов сортировки
- **Результат**: API корректно возвращает авторов, отсортированных по указанному параметру, включая сортировку по количеству публикаций (`shouts`) и подписчиков (`followers`)
## [0.5.5] - 2025-06-19
### Улучшения документации

View File

@ -59,6 +59,7 @@ python dev.py
- **Автоматическая очистка** истекших токенов
- **Connection pooling** и keepalive
- **Type-safe codebase** (mypy clean)
- **Оптимизированная сортировка авторов** с кешированием по параметрам
## 🔧 Конфигурация

View File

@ -98,14 +98,16 @@ ignore = [
"FBT002", # boolean default arguments - иногда удобно для API совместимости
"PERF203", # try-except in loop - иногда нужно для обработки отдельных элементов
# Игнорируем некоторые строгие правила для удобства разработки
"ANN003", # Missing type annotation for `*args` - иногда нужно
"ANN401", # Dynamically typed expressions (Any) - иногда нужно
"S101", # assert statements - нужно в тестах
"T201", # print statements - нужно для отладки
"TRY003", # Avoid specifying long messages outside the exception class - иногда допустимо
"PLR2004", # Magic values - иногда допустимо
"RUF001", # ambiguous unicode characters - для кириллицы
"RUF002", # ambiguous unicode characters in docstrings - для кириллицы
"RUF003", # ambiguous unicode characters in comments - для кириллицы
"RUF002", #
"RUF003", #
"RUF006", #
"TD002", # TODO без автора - не критично
"TD003", # TODO без ссылки на issue - не критично
]

View File

@ -1,6 +1,6 @@
import asyncio
import time
from typing import Any, Optional
from typing import Any, Optional, TypedDict
from graphql import GraphQLResolveInfo
from sqlalchemy import select, text
@ -26,6 +26,32 @@ from utils.logger import root_logger as logger
DEFAULT_COMMUNITIES = [1]
# Определение типа AuthorsBy на основе схемы GraphQL
class AuthorsBy(TypedDict, total=False):
"""
Тип для параметра сортировки авторов, соответствующий схеме GraphQL.
Поля:
last_seen: Временная метка последнего посещения
created_at: Временная метка создания
slug: Уникальный идентификатор автора
name: Имя автора
topic: Тема, связанная с автором
order: Поле для сортировки (shouts, followers, rating, comments, name)
after: Временная метка для фильтрации "после"
stat: Поле статистики
"""
last_seen: Optional[int]
created_at: Optional[int]
slug: Optional[str]
name: Optional[str]
topic: Optional[str]
order: Optional[str]
after: Optional[int]
stat: Optional[str]
# Вспомогательная функция для получения всех авторов без статистики
async def get_all_authors(current_user_id: Optional[int] = None) -> list[Any]:
"""
@ -62,7 +88,7 @@ async def get_all_authors(current_user_id: Optional[int] = None) -> list[Any]:
# Вспомогательная функция для получения авторов со статистикой с пагинацией
async def get_authors_with_stats(
limit: int = 10, offset: int = 0, by: Optional[str] = None, current_user_id: Optional[int] = None
limit: int = 10, offset: int = 0, by: Optional[AuthorsBy] = None, current_user_id: Optional[int] = None
):
"""
Получает авторов со статистикой с пагинацией.
@ -70,13 +96,14 @@ async def get_authors_with_stats(
Args:
limit: Максимальное количество возвращаемых авторов
offset: Смещение для пагинации
by: Опциональный параметр сортировки (new/active)
by: Опциональный параметр сортировки (AuthorsBy)
current_user_id: ID текущего пользователя
Returns:
list: Список авторов с их статистикой
"""
# Формируем ключ кеша с помощью универсальной функции
cache_key = f"authors:stats:limit={limit}:offset={offset}"
order_value = by.get("order", "default") if by else "default"
cache_key = f"authors:stats:limit={limit}:offset={offset}:order={order_value}"
# Функция для получения авторов из БД
async def fetch_authors_with_stats() -> list[Any]:
@ -96,32 +123,33 @@ async def get_authors_with_stats(
# Базовый запрос для получения авторов
base_query = select(Author).where(Author.deleted_at.is_(None))
# Применяем сортировку
# vars for statistics sorting
stats_sort_field = None
default_sort_applied = False
if by:
if isinstance(by, dict):
logger.debug(f"Processing dict-based sorting: {by}")
# Обработка словаря параметров сортировки
# Checking for order field in the dictionary
if "order" in by:
order_value = by["order"]
logger.debug(f"Found order field with value: {order_value}")
if order_value in ["shouts", "followers", "rating", "comments"]:
stats_sort_field = order_value
logger.debug(f"Applying statistics-based sorting by: {stats_sort_field}")
# Не применяем другую сортировку, так как будем использовать stats_sort_field
default_sort_applied = True
elif order_value == "name":
# Sorting by name in ascending order
base_query = base_query.order_by(asc(Author.name))
logger.debug("Applying alphabetical sorting by name")
default_sort_applied = True
else:
# If order is not a stats field, treat it as a regular field
column = getattr(Author, order_value, None)
if column:
base_query = base_query.order_by(sql_desc(column))
logger.debug(f"Applying sorting by column: {order_value}")
default_sort_applied = True
else:
logger.warning(f"Unknown order field: {order_value}")
else:
# Regular sorting by fields
for field, direction in by.items():
@ -131,19 +159,20 @@ async def get_authors_with_stats(
base_query = base_query.order_by(sql_desc(column))
else:
base_query = base_query.order_by(column)
elif by == "new":
base_query = base_query.order_by(sql_desc(Author.created_at))
elif by == "active":
base_query = base_query.order_by(sql_desc(Author.last_seen))
else:
# По умолчанию сортируем по времени создания
base_query = base_query.order_by(sql_desc(Author.created_at))
logger.debug(f"Applying sorting by field: {field}, direction: {direction}")
default_sort_applied = True
else:
logger.warning(f"Unknown field: {field}")
# Если сортировка еще не применена, используем сортировку по умолчанию
if not default_sort_applied and not stats_sort_field:
base_query = base_query.order_by(sql_desc(Author.created_at))
logger.debug("Applying default sorting by created_at (no by parameter)")
# If sorting by statistics, modify the query
if stats_sort_field == "shouts":
# Sorting by the number of shouts
logger.debug("Building subquery for shouts sorting")
subquery = (
select(ShoutAuthor.author, func.count(func.distinct(Shout.id)).label("shouts_count"))
.select_from(ShoutAuthor)
@ -153,11 +182,22 @@ async def get_authors_with_stats(
.subquery()
)
# Сбрасываем предыдущую сортировку и применяем новую
base_query = base_query.outerjoin(subquery, Author.id == subquery.c.author).order_by(
sql_desc(func.coalesce(subquery.c.shouts_count, 0))
)
logger.debug("Applied sorting by shouts count")
# Логирование для отладки сортировки
try:
# Получаем SQL запрос для проверки
sql_query = str(base_query.compile(compile_kwargs={"literal_binds": True}))
logger.debug(f"Generated SQL query for shouts sorting: {sql_query}")
except Exception as e:
logger.error(f"Error generating SQL query: {e}")
elif stats_sort_field == "followers":
# Sorting by the number of followers
logger.debug("Building subquery for followers sorting")
subquery = (
select(
AuthorFollower.author,
@ -168,9 +208,19 @@ async def get_authors_with_stats(
.subquery()
)
# Сбрасываем предыдущую сортировку и применяем новую
base_query = base_query.outerjoin(subquery, Author.id == subquery.c.author).order_by(
sql_desc(func.coalesce(subquery.c.followers_count, 0))
)
logger.debug("Applied sorting by followers count")
# Логирование для отладки сортировки
try:
# Получаем SQL запрос для проверки
sql_query = str(base_query.compile(compile_kwargs={"literal_binds": True}))
logger.debug(f"Generated SQL query for followers sorting: {sql_query}")
except Exception as e:
logger.error(f"Error generating SQL query: {e}")
# Применяем лимит и смещение
base_query = base_query.limit(limit).offset(offset)
@ -182,6 +232,10 @@ async def get_authors_with_stats(
if not author_ids:
return []
# Логирование результатов для отладки сортировки
if stats_sort_field:
logger.debug(f"Query returned {len(authors)} authors with sorting by {stats_sort_field}")
# Оптимизированный запрос для получения статистики по публикациям для авторов
placeholders = ", ".join([f":id{i}" for i in range(len(author_ids))])
shouts_stats_query = f"""
@ -292,7 +346,7 @@ async def update_author(_: None, info: GraphQLResolveInfo, profile: dict[str, An
if isinstance(author_with_stat, Author):
# Кэшируем полную версию для админов
author_dict = author_with_stat.dict(is_admin)
asyncio.create_task(cache_author(author_dict))
_t = asyncio.create_task(cache_author(author_dict))
# Возвращаем обычную полную версию, т.к. это владелец
return CommonResult(error=None, author=author)
@ -354,7 +408,7 @@ async def get_author(
if isinstance(author_with_stat, Author):
# Кэшируем полные данные для админов
original_dict = author_with_stat.dict(True)
asyncio.create_task(cache_author(original_dict))
_t = asyncio.create_task(cache_author(original_dict))
# Возвращаем отфильтрованную версию
author_dict = author_with_stat.dict(is_admin)
@ -371,13 +425,22 @@ async def get_author(
@query.field("load_authors_by")
async def load_authors_by(_: None, info: GraphQLResolveInfo, by: str, limit: int = 10, offset: int = 0) -> list[Any]:
async def load_authors_by(
_: None, info: GraphQLResolveInfo, by: AuthorsBy, limit: int = 10, offset: int = 0
) -> list[Any]:
"""Load authors by different criteria"""
try:
# Получаем ID текущего пользователя и флаг админа из контекста
viewer_id = info.context.get("author", {}).get("id")
info.context.get("is_admin", False)
# Логирование для отладки
logger.debug(f"load_authors_by called with by={by}, limit={limit}, offset={offset}")
# Проверяем наличие параметра order в словаре
if "order" in by:
logger.debug(f"Sorting by order={by['order']}")
# Используем оптимизированную функцию для получения авторов
return await get_authors_with_stats(limit, offset, by, viewer_id)
except Exception as exc: