circular-fix
Some checks failed
Deploy on push / deploy (push) Failing after 17s

This commit is contained in:
2025-08-17 16:33:54 +03:00
parent bc8447a444
commit e78e12eeee
65 changed files with 3304 additions and 1051 deletions

View File

@@ -2,8 +2,9 @@
Админ-резолверы - тонкие GraphQL обёртки над AdminService
"""
import json
import time
from typing import Any, Optional
from typing import Any
from graphql import GraphQLError, GraphQLResolveInfo
from sqlalchemy import and_, case, func, or_
@@ -21,6 +22,7 @@ from resolvers.topic import invalidate_topic_followers_cache, invalidate_topics_
from services.admin import AdminService
from services.common_result import handle_error
from services.db import local_session
from services.rbac import update_all_communities_permissions
from services.redis import redis
from services.schema import mutation, query
from utils.logger import root_logger as logger
@@ -66,7 +68,7 @@ async def admin_get_shouts(
offset: int = 0,
search: str = "",
status: str = "all",
community: Optional[int] = None,
community: int | None = None,
) -> dict[str, Any]:
"""Получает список публикаций"""
try:
@@ -85,7 +87,8 @@ async def admin_update_shout(_: None, info: GraphQLResolveInfo, shout: dict[str,
return {"success": False, "error": "ID публикации не указан"}
shout_input = {k: v for k, v in shout.items() if k != "id"}
result = await update_shout(None, info, shout_id, shout_input)
title = shout_input.get("title")
result = await update_shout(None, info, shout_id, title)
if result.error:
return {"success": False, "error": result.error}
@@ -464,8 +467,6 @@ async def admin_get_roles(_: None, _info: GraphQLResolveInfo, community: int | N
# Если указано сообщество, добавляем кастомные роли из Redis
if community:
import json
custom_roles_data = await redis.execute("HGETALL", f"community:custom_roles:{community}")
for role_id, role_json in custom_roles_data.items():
@@ -841,8 +842,6 @@ async def admin_create_custom_role(_: None, _info: GraphQLResolveInfo, role: dic
}
# Сохраняем роль в Redis
import json
await redis.execute("HSET", f"community:custom_roles:{community_id}", role_id, json.dumps(role_data))
logger.info(f"Создана новая роль {role_id} для сообщества {community_id}")
@@ -887,8 +886,6 @@ async def admin_delete_custom_role(
async def admin_update_permissions(_: None, _info: GraphQLResolveInfo) -> dict[str, Any]:
"""Обновляет права для всех сообществ с новыми дефолтными настройками"""
try:
from services.rbac import update_all_communities_permissions
await update_all_communities_permissions()
logger.info("Права для всех сообществ обновлены")

View File

@@ -2,7 +2,7 @@
Auth резолверы - тонкие GraphQL обёртки над AuthService
"""
from typing import Any, Union
from typing import Any
from graphql import GraphQLResolveInfo
from starlette.responses import JSONResponse
@@ -16,7 +16,7 @@ from utils.logger import root_logger as logger
@type_author.field("roles")
def resolve_roles(obj: Union[dict, Any], info: GraphQLResolveInfo) -> list[str]:
def resolve_roles(obj: dict | Any, info: GraphQLResolveInfo) -> list[str]:
"""Резолвер для поля roles автора"""
try:
if hasattr(obj, "get_roles"):

View File

@@ -1,7 +1,7 @@
import asyncio
import time
import traceback
from typing import Any, Optional, TypedDict
from typing import Any, TypedDict
from graphql import GraphQLResolveInfo
from sqlalchemy import and_, asc, func, select, text
@@ -46,18 +46,18 @@ class AuthorsBy(TypedDict, total=False):
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]
last_seen: int | None
created_at: int | None
slug: str | None
name: str | None
topic: str | None
order: str | None
after: int | None
stat: str | None
# Вспомогательная функция для получения всех авторов без статистики
async def get_all_authors(current_user_id: Optional[int] = None) -> list[Any]:
async def get_all_authors(current_user_id: int | None = None) -> list[Any]:
"""
Получает всех авторов без статистики.
Используется для случаев, когда нужен полный список авторов без дополнительной информации.
@@ -92,7 +92,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[AuthorsBy] = None, current_user_id: Optional[int] = None
limit: int = 10, offset: int = 0, by: AuthorsBy | None = None, current_user_id: int | None = None
) -> list[dict[str, Any]]:
"""
Получает авторов со статистикой с пагинацией.
@@ -367,7 +367,7 @@ async def get_authors_all(_: None, info: GraphQLResolveInfo) -> list[Any]:
@query.field("get_author")
async def get_author(
_: None, info: GraphQLResolveInfo, slug: Optional[str] = None, author_id: Optional[int] = None
_: None, info: GraphQLResolveInfo, slug: str | None = None, author_id: int | None = None
) -> dict[str, Any] | None:
"""Get specific author by slug or ID"""
# Получаем ID текущего пользователя и флаг админа из контекста
@@ -451,8 +451,8 @@ async def load_authors_search(_: None, info: GraphQLResolveInfo, **kwargs: Any)
def get_author_id_from(
slug: Optional[str] = None, user: Optional[str] = None, author_id: Optional[int] = None
) -> Optional[int]:
slug: str | None = None, user: str | None = None, author_id: int | None = None
) -> int | None:
"""Get author ID from different identifiers"""
try:
if author_id:
@@ -474,7 +474,7 @@ def get_author_id_from(
@query.field("get_author_follows")
async def get_author_follows(
_, info: GraphQLResolveInfo, slug: Optional[str] = None, user: Optional[str] = None, author_id: Optional[int] = None
_, info: GraphQLResolveInfo, slug: str | None = None, user: str | None = None, author_id: int | None = None
) -> dict[str, Any]:
"""Get entities followed by author"""
# Получаем ID текущего пользователя и флаг админа из контекста
@@ -519,9 +519,9 @@ async def get_author_follows(
async def get_author_follows_topics(
_,
_info: GraphQLResolveInfo,
slug: Optional[str] = None,
user: Optional[str] = None,
author_id: Optional[int] = None,
slug: str | None = None,
user: str | None = None,
author_id: int | None = None,
) -> list[Any]:
"""Get topics followed by author"""
logger.debug(f"getting followed topics for @{slug}")
@@ -537,7 +537,7 @@ async def get_author_follows_topics(
@query.field("get_author_follows_authors")
async def get_author_follows_authors(
_, info: GraphQLResolveInfo, slug: Optional[str] = None, user: Optional[str] = None, author_id: Optional[int] = None
_, info: GraphQLResolveInfo, slug: str | None = None, user: str | None = None, author_id: int | None = None
) -> list[Any]:
"""Get authors followed by author"""
# Получаем ID текущего пользователя и флаг админа из контекста

View File

@@ -40,8 +40,7 @@ def load_shouts_bookmarked(_: None, info, options) -> list[Shout]:
)
)
q, limit, offset = apply_options(q, options, author_id)
shouts = get_shouts_with_links(info, q, limit, offset)
return shouts
return get_shouts_with_links(info, q, limit, offset)
@mutation.field("toggle_bookmark_shout")

View File

@@ -1,5 +1,5 @@
import time
from typing import Any
from typing import Any, List
import orjson
from graphql import GraphQLResolveInfo
@@ -8,6 +8,12 @@ from sqlalchemy.orm import joinedload
from sqlalchemy.sql.functions import coalesce
from auth.orm import Author
from cache.cache import (
cache_author,
cache_topic,
invalidate_shout_related_cache,
invalidate_shouts_cache,
)
from orm.shout import Shout, ShoutAuthor, ShoutTopic
from orm.topic import Topic
from resolvers.follower import follow
@@ -383,16 +389,15 @@ def patch_topics(session: Any, shout: Any, topics_input: list[Any]) -> None:
# @mutation.field("update_shout")
# @login_required
async def update_shout(
_: None, info: GraphQLResolveInfo, shout_id: int, shout_input: dict | None = None, *, publish: bool = False
_: None,
info: GraphQLResolveInfo,
shout_id: int,
title: str | None = None,
body: str | None = None,
topics: List[str] | None = None,
collections: List[int] | None = None,
publish: bool = False,
) -> CommonResult:
# Поздние импорты для избежания циклических зависимостей
from cache.cache import (
cache_author,
cache_topic,
invalidate_shout_related_cache,
invalidate_shouts_cache,
)
"""Update an existing shout with optional publishing"""
logger.info(f"update_shout called with shout_id={shout_id}, publish={publish}")
@@ -403,12 +408,9 @@ async def update_shout(
return CommonResult(error="unauthorized", shout=None)
logger.info(f"Starting update_shout with id={shout_id}, publish={publish}")
logger.debug(f"Full shout_input: {shout_input}") # DraftInput
roles = info.context.get("roles", [])
current_time = int(time.time())
shout_input = shout_input or {}
shout_id = shout_id or shout_input.get("id", shout_id)
slug = shout_input.get("slug")
slug = title # Используем title как slug если он передан
try:
with local_session() as session:
@@ -442,17 +444,18 @@ async def update_shout(
c += 1
same_slug_shout.slug = f"{slug}-{c}" # type: ignore[assignment]
same_slug_shout = session.query(Shout).where(Shout.slug == slug).first()
shout_input["slug"] = slug
shout_by_id.slug = slug
logger.info(f"shout#{shout_id} slug patched")
if filter(lambda x: x.id == author_id, list(shout_by_id.authors)) or "editor" in roles:
logger.info(f"Author #{author_id} has permission to edit shout#{shout_id}")
# topics patch
topics_input = shout_input.get("topics")
if topics_input:
logger.info(f"Received topics_input for shout#{shout_id}: {topics_input}")
if topics:
logger.info(f"Received topics for shout#{shout_id}: {topics}")
try:
# Преобразуем topics в формат для patch_topics
topics_input = [{"id": int(t)} for t in topics if t.isdigit()]
patch_topics(session, shout_by_id, topics_input)
logger.info(f"Successfully patched topics for shout#{shout_id}")
@@ -463,17 +466,16 @@ async def update_shout(
logger.error(f"Error patching topics: {e}", exc_info=True)
return CommonResult(error=f"Failed to update topics: {e!s}", shout=None)
del shout_input["topics"]
for tpc in topics_input:
await cache_by_id(Topic, tpc["id"], cache_topic)
else:
logger.warning(f"No topics_input received for shout#{shout_id}")
logger.warning(f"No topics received for shout#{shout_id}")
# main topic
main_topic = shout_input.get("main_topic")
if main_topic:
logger.info(f"Updating main topic for shout#{shout_id} to {main_topic}")
patch_main_topic(session, main_topic, shout_by_id)
# Обновляем title и body если переданы
if title:
shout_by_id.title = title
if body:
shout_by_id.body = body
shout_by_id.updated_at = current_time # type: ignore[assignment]
if publish:
@@ -497,8 +499,8 @@ async def update_shout(
logger.info("Author link already exists")
# Логируем финальное состояние перед сохранением
logger.info(f"Final shout_input for update: {shout_input}")
Shout.update(shout_by_id, shout_input)
logger.info(f"Final shout_input for update: {shout_by_id.dict()}")
Shout.update(shout_by_id, shout_by_id.dict())
session.add(shout_by_id)
try:
@@ -572,11 +574,6 @@ async def update_shout(
# @mutation.field("delete_shout")
# @login_required
async def delete_shout(_: None, info: GraphQLResolveInfo, shout_id: int) -> CommonResult:
# Поздние импорты для избежания циклических зависимостей
from cache.cache import (
invalidate_shout_related_cache,
)
"""Delete a shout (mark as deleted)"""
author_dict = info.context.get("author", {})
if not author_dict:
@@ -667,12 +664,6 @@ async def unpublish_shout(_: None, info: GraphQLResolveInfo, shout_id: int) -> C
"""
Unpublish a shout by setting published_at to NULL
"""
# Поздние импорты для избежания циклических зависимостей
from cache.cache import (
invalidate_shout_related_cache,
invalidate_shouts_cache,
)
author_dict = info.context.get("author", {})
author_id = author_dict.get("id")
roles = info.context.get("roles", [])

View File

@@ -6,6 +6,12 @@ from graphql import GraphQLResolveInfo
from sqlalchemy.sql import and_
from auth.orm import Author, AuthorFollower
from cache.cache import (
cache_author,
cache_topic,
get_cached_follower_authors,
get_cached_follower_topics,
)
from orm.community import Community, CommunityFollower
from orm.shout import Shout, ShoutReactionsFollower
from orm.topic import Topic, TopicFollower
@@ -36,14 +42,6 @@ async def follow(
follower_id = follower_dict.get("id")
logger.debug(f"follower_id: {follower_id}")
# Поздние импорты для избежания циклических зависимостей
from cache.cache import (
cache_author,
cache_topic,
get_cached_follower_authors,
get_cached_follower_topics,
)
entity_classes = {
"AUTHOR": (Author, AuthorFollower, get_cached_follower_authors, cache_author),
"TOPIC": (Topic, TopicFollower, get_cached_follower_topics, cache_topic),
@@ -173,14 +171,6 @@ async def unfollow(
follower_id = follower_dict.get("id")
logger.debug(f"follower_id: {follower_id}")
# Поздние импорты для избежания циклических зависимостей
from cache.cache import (
cache_author,
cache_topic,
get_cached_follower_authors,
get_cached_follower_topics,
)
entity_classes = {
"AUTHOR": (Author, AuthorFollower, get_cached_follower_authors, cache_author),
"TOPIC": (Topic, TopicFollower, get_cached_follower_topics, cache_topic),

View File

@@ -1,4 +1,4 @@
from typing import Any, Optional
from typing import Any
import orjson
from graphql import GraphQLResolveInfo
@@ -400,7 +400,7 @@ def apply_filters(q: Select, filters: dict[str, Any]) -> Select:
@query.field("get_shout")
async def get_shout(_: None, info: GraphQLResolveInfo, slug: str = "", shout_id: int = 0) -> Optional[Shout]:
async def get_shout(_: None, info: GraphQLResolveInfo, slug: str = "", shout_id: int = 0) -> Shout | None:
"""
Получение публикации по slug или id.

View File

@@ -1,13 +1,14 @@
import asyncio
import sys
import traceback
from typing import Any, Optional
from typing import Any
from sqlalchemy import and_, distinct, func, join, select
from sqlalchemy.orm import aliased
from sqlalchemy.sql.expression import Select
from auth.orm import Author, AuthorFollower
from cache.cache import cache_author
from orm.community import Community, CommunityFollower
from orm.reaction import Reaction, ReactionKind
from orm.shout import Shout, ShoutAuthor, ShoutTopic
@@ -362,10 +363,8 @@ def update_author_stat(author_id: int) -> None:
:param author_id: Идентификатор автора.
"""
# Поздний импорт для избежания циклических зависимостей
from cache.cache import cache_author
author_query = select(Author).where(Author.id == author_id)
try:
author_query = select(Author).where(Author.id == author_id)
result = get_with_stat(author_query)
if result:
author_with_stat = result[0]
@@ -436,7 +435,7 @@ def get_following_count(entity_type: str, entity_id: int) -> int:
def get_shouts_count(
author_id: Optional[int] = None, topic_id: Optional[int] = None, community_id: Optional[int] = None
author_id: int | None = None, topic_id: int | None = None, community_id: int | None = None
) -> int:
"""Получает количество публикаций"""
try:
@@ -458,7 +457,7 @@ def get_shouts_count(
return 0
def get_authors_count(community_id: Optional[int] = None) -> int:
def get_authors_count(community_id: int | None = None) -> int:
"""Получает количество авторов"""
try:
with local_session() as session:
@@ -479,7 +478,7 @@ def get_authors_count(community_id: Optional[int] = None) -> int:
return 0
def get_topics_count(author_id: Optional[int] = None) -> int:
def get_topics_count(author_id: int | None = None) -> int:
"""Получает количество топиков"""
try:
with local_session() as session:
@@ -509,7 +508,7 @@ def get_communities_count() -> int:
return 0
def get_reactions_count(shout_id: Optional[int] = None, author_id: Optional[int] = None) -> int:
def get_reactions_count(shout_id: int | None = None, author_id: int | None = None) -> int:
"""Получает количество реакций"""
try:
with local_session() as session:

View File

@@ -1,5 +1,5 @@
from math import ceil
from typing import Any, Optional
from typing import Any
from graphql import GraphQLResolveInfo
from sqlalchemy import desc, func, select, text
@@ -55,7 +55,7 @@ async def get_all_topics() -> list[Any]:
# Вспомогательная функция для получения тем со статистикой с пагинацией
async def get_topics_with_stats(
limit: int = 100, offset: int = 0, community_id: Optional[int] = None, by: Optional[str] = None
limit: int = 100, offset: int = 0, community_id: int | None = None, by: str | None = None
) -> dict[str, Any]:
"""
Получает темы со статистикой с пагинацией.
@@ -292,7 +292,7 @@ async def get_topics_with_stats(
# Функция для инвалидации кеша тем
async def invalidate_topics_cache(topic_id: Optional[int] = None) -> None:
async def invalidate_topics_cache(topic_id: int | None = None) -> None:
"""
Инвалидирует кеши тем при изменении данных.
@@ -350,7 +350,7 @@ async def get_topics_all(_: None, _info: GraphQLResolveInfo) -> list[Any]:
# Запрос на получение тем по сообществу
@query.field("get_topics_by_community")
async def get_topics_by_community(
_: None, _info: GraphQLResolveInfo, community_id: int, limit: int = 100, offset: int = 0, by: Optional[str] = None
_: None, _info: GraphQLResolveInfo, community_id: int, limit: int = 100, offset: int = 0, by: str | None = None
) -> list[Any]:
"""
Получает список тем, принадлежащих указанному сообществу с пагинацией и статистикой.
@@ -386,7 +386,7 @@ async def get_topics_by_author(
# Запрос на получение одной темы по её slug
@query.field("get_topic")
async def get_topic(_: None, _info: GraphQLResolveInfo, slug: str) -> Optional[Any]:
async def get_topic(_: None, _info: GraphQLResolveInfo, slug: str) -> Any | None:
topic = await get_cached_topic_by_slug(slug, get_with_stat)
if topic:
return topic