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

@@ -19,6 +19,12 @@ from services.env import EnvVariable, env_manager
from settings import ADMIN_EMAILS as ADMIN_EMAILS_LIST
from utils.logger import root_logger as logger
# Отложенный импорт Author для избежания циклических импортов
def get_author_model():
"""Возвращает модель Author для использования в admin"""
from auth.orm import Author
return Author
class AdminService:
"""Сервис для админ-панели с бизнес-логикой"""
@@ -53,6 +59,7 @@ class AdminService:
"slug": "system",
}
Author = get_author_model()
author = session.query(Author).where(Author.id == author_id).first()
if author:
return {
@@ -69,7 +76,7 @@ class AdminService:
}
@staticmethod
def get_user_roles(user: Author, community_id: int = 1) -> list[str]:
def get_user_roles(user: Any, community_id: int = 1) -> list[str]:
"""Получает роли пользователя в сообществе"""
admin_emails = ADMIN_EMAILS_LIST.split(",") if ADMIN_EMAILS_LIST else []

View File

@@ -7,7 +7,7 @@ import json
import secrets
import time
from functools import wraps
from typing import Any, Callable, Optional
from typing import Any, Callable
from graphql.error import GraphQLError
from starlette.requests import Request
@@ -21,6 +21,7 @@ from auth.orm import Author
from auth.password import Password
from auth.tokens.storage import TokenStorage
from auth.tokens.verification import VerificationTokenManager
from cache.cache import get_cached_author_by_id
from orm.community import (
Community,
CommunityAuthor,
@@ -38,6 +39,11 @@ from settings import (
from utils.generate_slug import generate_unique_slug
from utils.logger import root_logger as logger
# Author уже импортирован в начале файла
def get_author_model():
"""Возвращает модель Author для использования в auth"""
return Author
# Список разрешенных заголовков
ALLOWED_HEADERS = ["Authorization", "Content-Type"]
@@ -107,6 +113,7 @@ class AuthService:
# Проверяем админские права через email если нет роли админа
if not is_admin:
with local_session() as session:
Author = get_author_model()
author = session.query(Author).where(Author.id == user_id_int).first()
if author and author.email in ADMIN_EMAILS.split(","):
is_admin = True
@@ -120,7 +127,7 @@ class AuthService:
return user_id, user_roles, is_admin
async def add_user_role(self, user_id: str, roles: Optional[list[str]] = None) -> Optional[str]:
async def add_user_role(self, user_id: str, roles: list[str] | None = None) -> str | None:
"""
Добавление ролей пользователю в локальной БД через CommunityAuthor.
"""
@@ -160,6 +167,7 @@ class AuthService:
# Проверяем уникальность email
with local_session() as session:
Author = get_author_model()
existing_user = session.query(Author).where(Author.email == user_dict["email"]).first()
if existing_user:
# Если пользователь с таким email уже существует, возвращаем его
@@ -172,6 +180,7 @@ class AuthService:
# Проверяем уникальность slug
with local_session() as session:
# Добавляем суффикс, если slug уже существует
Author = get_author_model()
counter = 1
unique_slug = base_slug
while session.query(Author).where(Author.slug == unique_slug).first():
@@ -227,9 +236,6 @@ class AuthService:
async def get_session(self, token: str) -> dict[str, Any]:
"""Получает информацию о текущей сессии по токену"""
# Поздний импорт для избежания циклических зависимостей
from cache.cache import get_cached_author_by_id
try:
# Проверяем токен
payload = JWTCodec.decode(token)
@@ -261,6 +267,7 @@ class AuthService:
logger.info(f"Попытка регистрации для {email}")
with local_session() as session:
Author = get_author_model()
user = session.query(Author).where(Author.email == email).first()
if user:
logger.warning(f"Пользователь {email} уже существует")
@@ -300,6 +307,7 @@ class AuthService:
"""Отправляет ссылку подтверждения на email"""
email = email.lower()
with local_session() as session:
Author = get_author_model()
user = session.query(Author).where(Author.email == email).first()
if not user:
raise ObjectNotExistError("User not found")
@@ -337,6 +345,7 @@ class AuthService:
username = payload.get("username")
with local_session() as session:
Author = get_author_model()
user = session.query(Author).where(Author.id == user_id).first()
if not user:
logger.warning(f"Пользователь с ID {user_id} не найден")
@@ -371,6 +380,7 @@ class AuthService:
try:
with local_session() as session:
Author = get_author_model()
author = session.query(Author).where(Author.email == email).first()
if not author:
logger.warning(f"Пользователь {email} не найден")
@@ -779,7 +789,6 @@ class AuthService:
info.context["is_admin"] = is_admin
# Автор будет получен в резолвере при необходимости
pass
else:
logger.debug("login_accepted: Пользователь не авторизован")
info.context["roles"] = None

View File

@@ -3,7 +3,7 @@ from typing import Any
from graphql.error import GraphQLError
from auth.orm import Author
# Импорт Author отложен для избежания циклических импортов
from orm.community import Community
from orm.draft import Draft
from orm.reaction import Reaction
@@ -11,6 +11,12 @@ from orm.shout import Shout
from orm.topic import Topic
from utils.logger import root_logger as logger
# Отложенный импорт Author для избежания циклических импортов
def get_author_model():
"""Возвращает модель Author для использования в common_result"""
from auth.orm import Author
return Author
def handle_error(operation: str, error: Exception) -> GraphQLError:
"""Обрабатывает ошибки в резолверах"""
@@ -28,8 +34,8 @@ class CommonResult:
slugs: list[str] | None = None
shout: Shout | None = None
shouts: list[Shout] | None = None
author: Author | None = None
authors: list[Author] | None = None
author: Any | None = None # Author type resolved at runtime
authors: list[Any] | None = None # Author type resolved at runtime
reaction: Reaction | None = None
reactions: list[Reaction] | None = None
topic: Topic | None = None

View File

@@ -153,9 +153,8 @@ def create_table_if_not_exists(
logger.info(f"Created table: {model_cls.__tablename__}")
finally:
# Close connection only if we created it
if should_close:
if hasattr(connection, "close"):
connection.close() # type: ignore[attr-defined]
if should_close and hasattr(connection, "close"):
connection.close() # type: ignore[attr-defined]
def get_column_names_without_virtual(model_cls: Type[DeclarativeBase]) -> list[str]:

View File

@@ -1,6 +1,6 @@
import os
from dataclasses import dataclass
from typing import ClassVar, Optional
from typing import ClassVar
from services.redis import redis
from utils.logger import root_logger as logger
@@ -292,7 +292,7 @@ class EnvService:
logger.error(f"Ошибка при удалении переменной {key}: {e}")
return False
async def get_variable(self, key: str) -> Optional[str]:
async def get_variable(self, key: str) -> str | None:
"""Получает значение конкретной переменной"""
# Сначала проверяем Redis

View File

@@ -1,5 +1,5 @@
from collections.abc import Collection
from typing import Any, Union
from typing import Any
import orjson
@@ -11,12 +11,12 @@ from services.redis import redis
from utils.logger import root_logger as logger
def save_notification(action: str, entity: str, payload: Union[dict[Any, Any], str, int, None]) -> None:
def save_notification(action: str, entity: str, payload: dict[Any, Any] | str | int | None) -> None:
"""Save notification with proper payload handling"""
if payload is None:
return
if isinstance(payload, (Reaction, Shout)):
if isinstance(payload, Reaction | Shout):
# Convert ORM objects to dict representation
payload = {"id": payload.id}
@@ -26,7 +26,7 @@ def save_notification(action: str, entity: str, payload: Union[dict[Any, Any], s
session.commit()
async def notify_reaction(reaction: Union[Reaction, int], action: str = "create") -> None:
async def notify_reaction(reaction: Reaction | int, action: str = "create") -> None:
channel_name = "reaction"
# Преобразуем объект Reaction в словарь для сериализации
@@ -56,7 +56,7 @@ async def notify_shout(shout: dict[str, Any], action: str = "update") -> None:
data = {"payload": shout, "action": action}
try:
payload = data.get("payload")
if isinstance(payload, Collection) and not isinstance(payload, (str, bytes, dict)):
if isinstance(payload, Collection) and not isinstance(payload, str | bytes | dict):
payload = str(payload)
save_notification(action, channel_name, payload)
await redis.publish(channel_name, orjson.dumps(data))
@@ -72,7 +72,7 @@ async def notify_follower(follower: dict[str, Any], author_id: int, action: str
data = {"payload": simplified_follower, "action": action}
# save in channel
payload = data.get("payload")
if isinstance(payload, Collection) and not isinstance(payload, (str, bytes, dict)):
if isinstance(payload, Collection) and not isinstance(payload, str | bytes | dict):
payload = str(payload)
save_notification(action, channel_name, payload)
@@ -144,7 +144,7 @@ async def notify_draft(draft_data: dict[str, Any], action: str = "publish") -> N
# Сохраняем уведомление
payload = data.get("payload")
if isinstance(payload, Collection) and not isinstance(payload, (str, bytes, dict)):
if isinstance(payload, Collection) and not isinstance(payload, str | bytes | dict):
payload = str(payload)
save_notification(action, channel_name, payload)

View File

@@ -9,27 +9,15 @@ RBAC: динамическая система прав для ролей и со
"""
import asyncio
import json
from functools import wraps
from pathlib import Path
from typing import Callable
from typing import Any, Callable
from auth.orm import Author
from auth.rbac_interface import get_community_queries, get_rbac_operations
from services.db import local_session
from services.redis import redis
from settings import ADMIN_EMAILS
from utils.logger import root_logger as logger
# --- Загрузка каталога сущностей и дефолтных прав ---
with Path("services/permissions_catalog.json").open() as f:
PERMISSIONS_CATALOG = json.load(f)
with Path("services/default_role_permissions.json").open() as f:
DEFAULT_ROLE_PERMISSIONS = json.load(f)
role_names = list(DEFAULT_ROLE_PERMISSIONS.keys())
async def initialize_community_permissions(community_id: int) -> None:
"""
@@ -38,117 +26,8 @@ async def initialize_community_permissions(community_id: int) -> None:
Args:
community_id: ID сообщества
"""
key = f"community:roles:{community_id}"
# Проверяем, не инициализировано ли уже
existing = await redis.execute("GET", key)
if existing:
logger.debug(f"Права для сообщества {community_id} уже инициализированы")
return
# Создаем полные списки разрешений с учетом иерархии
expanded_permissions = {}
def get_role_permissions(role: str, processed_roles: set[str] | None = None) -> set[str]:
"""
Рекурсивно получает все разрешения для роли, включая наследованные
Args:
role: Название роли
processed_roles: Список уже обработанных ролей для предотвращения зацикливания
Returns:
Множество разрешений
"""
if processed_roles is None:
processed_roles = set()
if role in processed_roles:
return set()
processed_roles.add(role)
# Получаем прямые разрешения роли
direct_permissions = set(DEFAULT_ROLE_PERMISSIONS.get(role, []))
# Проверяем, есть ли наследование роли
for perm in list(direct_permissions):
if perm in role_names:
# Если пермишен - это название роли, добавляем все её разрешения
direct_permissions.remove(perm)
direct_permissions.update(get_role_permissions(perm, processed_roles))
return direct_permissions
# Формируем расширенные разрешения для каждой роли
for role in role_names:
expanded_permissions[role] = list(get_role_permissions(role))
# Сохраняем в Redis уже развернутые списки с учетом иерархии
await redis.execute("SET", key, json.dumps(expanded_permissions))
logger.info(f"Инициализированы права с иерархией для сообщества {community_id}")
async def get_role_permissions_for_community(community_id: int) -> dict:
"""
Получает права ролей для конкретного сообщества.
Если права не настроены, автоматически инициализирует их дефолтными.
Args:
community_id: ID сообщества
Returns:
Словарь прав ролей для сообщества
"""
key = f"community:roles:{community_id}"
data = await redis.execute("GET", key)
if data:
return json.loads(data)
# Автоматически инициализируем, если не найдено
await initialize_community_permissions(community_id)
# Получаем инициализированные разрешения
data = await redis.execute("GET", key)
if data:
return json.loads(data)
# Fallback на дефолтные разрешения если что-то пошло не так
return DEFAULT_ROLE_PERMISSIONS
async def set_role_permissions_for_community(community_id: int, role_permissions: dict) -> None:
"""
Устанавливает кастомные права ролей для сообщества.
Args:
community_id: ID сообщества
role_permissions: Словарь прав ролей
"""
key = f"community:roles:{community_id}"
await redis.execute("SET", key, json.dumps(role_permissions))
logger.info(f"Обновлены права ролей для сообщества {community_id}")
async def update_all_communities_permissions() -> None:
"""
Обновляет права для всех существующих сообществ с новыми дефолтными настройками.
"""
from orm.community import Community
with local_session() as session:
communities = session.query(Community).all()
for community in communities:
# Удаляем старые права
key = f"community:roles:{community.id}"
await redis.execute("DEL", key)
# Инициализируем новые права
await initialize_community_permissions(community.id)
logger.info(f"Обновлены права для {len(communities)} сообществ")
rbac_ops = get_rbac_operations()
await rbac_ops.initialize_community_permissions(community_id)
async def get_permissions_for_role(role: str, community_id: int) -> list[str]:
@@ -163,42 +42,54 @@ async def get_permissions_for_role(role: str, community_id: int) -> list[str]:
Returns:
Список разрешений для роли
"""
role_perms = await get_role_permissions_for_community(community_id)
return role_perms.get(role, [])
rbac_ops = get_rbac_operations()
return await rbac_ops.get_permissions_for_role(role, community_id)
async def update_all_communities_permissions() -> None:
"""
Обновляет права для всех существующих сообществ на основе актуальных дефолтных настроек.
Используется в админ-панели для применения изменений в правах на все сообщества.
"""
rbac_ops = get_rbac_operations()
# Поздний импорт для избежания циклических зависимостей
from orm.community import Community
try:
with local_session() as session:
# Получаем все сообщества
communities = session.query(Community).all()
for community in communities:
# Сбрасываем кеш прав для каждого сообщества
from services.redis import redis
key = f"community:roles:{community.id}"
await redis.execute("DEL", key)
# Переинициализируем права с актуальными дефолтными настройками
await rbac_ops.initialize_community_permissions(community.id)
logger.info(f"Обновлены права для {len(communities)} сообществ")
except Exception as e:
logger.error(f"Ошибка при обновлении прав всех сообществ: {e}", exc_info=True)
raise
# --- Получение ролей пользователя ---
def get_user_roles_in_community(author_id: int, community_id: int = 1, session=None) -> list[str]:
def get_user_roles_in_community(author_id: int, community_id: int = 1, session: Any = None) -> list[str]:
"""
Получает роли пользователя в сообществе через новую систему CommunityAuthor
"""
# Поздний импорт для избежания циклических зависимостей
from orm.community import CommunityAuthor
try:
if session:
ca = (
session.query(CommunityAuthor)
.where(CommunityAuthor.author_id == author_id, CommunityAuthor.community_id == community_id)
.first()
)
return ca.role_list if ca else []
# Используем local_session для продакшена
with local_session() as db_session:
ca = (
db_session.query(CommunityAuthor)
.where(CommunityAuthor.author_id == author_id, CommunityAuthor.community_id == community_id)
.first()
)
return ca.role_list if ca else []
except Exception as e:
logger.error(f"[get_user_roles_in_community] Ошибка при получении ролей: {e}")
return []
community_queries = get_community_queries()
return community_queries.get_user_roles_in_community(author_id, community_id, session)
async def user_has_permission(author_id: int, permission: str, community_id: int, session=None) -> bool:
async def user_has_permission(author_id: int, permission: str, community_id: int, session: Any = None) -> bool:
"""
Проверяет, есть ли у пользователя конкретное разрешение в сообществе.
@@ -211,8 +102,8 @@ async def user_has_permission(author_id: int, permission: str, community_id: int
Returns:
True если разрешение есть, False если нет
"""
user_roles = get_user_roles_in_community(author_id, community_id, session)
return await roles_have_permission(user_roles, permission, community_id)
rbac_ops = get_rbac_operations()
return await rbac_ops.user_has_permission(author_id, permission, community_id, session)
# --- Проверка прав ---
@@ -228,8 +119,8 @@ async def roles_have_permission(role_slugs: list[str], permission: str, communit
Returns:
True если хотя бы одна роль имеет разрешение
"""
role_perms = await get_role_permissions_for_community(community_id)
return any(permission in role_perms.get(role, []) for role in role_slugs)
rbac_ops = get_rbac_operations()
return await rbac_ops._roles_have_permission(role_slugs, permission, community_id)
# --- Декораторы ---
@@ -352,8 +243,7 @@ def get_community_id_from_context(info) -> int:
if "slug" in variables:
slug = variables["slug"]
try:
from orm.community import Community
from services.db import local_session
from orm.community import Community # Поздний импорт
with local_session() as session:
community = session.query(Community).filter_by(slug=slug).first()

205
services/rbac_impl.py Normal file
View File

@@ -0,0 +1,205 @@
"""
Реализация RBAC операций для использования через интерфейс.
Этот модуль предоставляет конкретную реализацию RBAC операций,
не импортирует ORM модели напрямую, используя dependency injection.
"""
import asyncio
import json
from pathlib import Path
from typing import Any
from auth.orm import Author
from auth.rbac_interface import CommunityAuthorQueries, RBACOperations, get_community_queries
from services.db import local_session
from services.redis import redis
from settings import ADMIN_EMAILS
from utils.logger import root_logger as logger
# --- Загрузка каталога сущностей и дефолтных прав ---
with Path("services/permissions_catalog.json").open() as f:
PERMISSIONS_CATALOG = json.load(f)
with Path("services/default_role_permissions.json").open() as f:
DEFAULT_ROLE_PERMISSIONS = json.load(f)
role_names = list(DEFAULT_ROLE_PERMISSIONS.keys())
class RBACOperationsImpl(RBACOperations):
"""Конкретная реализация RBAC операций"""
async def get_permissions_for_role(self, role: str, community_id: int) -> list[str]:
"""
Получает список разрешений для конкретной роли в сообществе.
Иерархия уже применена при инициализации сообщества.
Args:
role: Название роли
community_id: ID сообщества
Returns:
Список разрешений для роли
"""
role_perms = await self._get_role_permissions_for_community(community_id)
return role_perms.get(role, [])
async def initialize_community_permissions(self, community_id: int) -> None:
"""
Инициализирует права для нового сообщества на основе дефолтных настроек с учетом иерархии.
Args:
community_id: ID сообщества
"""
key = f"community:roles:{community_id}"
# Проверяем, не инициализировано ли уже
existing = await redis.execute("GET", key)
if existing:
logger.debug(f"Права для сообщества {community_id} уже инициализированы")
return
# Создаем полные списки разрешений с учетом иерархии
expanded_permissions = {}
def get_role_permissions(role: str, processed_roles: set[str] | None = None) -> set[str]:
"""
Рекурсивно получает все разрешения для роли, включая наследованные
Args:
role: Название роли
processed_roles: Список уже обработанных ролей для предотвращения зацикливания
Returns:
Множество разрешений
"""
if processed_roles is None:
processed_roles = set()
if role in processed_roles:
return set()
processed_roles.add(role)
# Получаем прямые разрешения роли
direct_permissions = set(DEFAULT_ROLE_PERMISSIONS.get(role, []))
# Проверяем, есть ли наследование роли
for perm in list(direct_permissions):
if perm in role_names:
# Если пермишен - это название роли, добавляем все её разрешения
direct_permissions.remove(perm)
direct_permissions.update(get_role_permissions(perm, processed_roles))
return direct_permissions
# Формируем расширенные разрешения для каждой роли
for role in role_names:
expanded_permissions[role] = list(get_role_permissions(role))
# Сохраняем в Redis уже развернутые списки с учетом иерархии
await redis.execute("SET", key, json.dumps(expanded_permissions))
logger.info(f"Инициализированы права с иерархией для сообщества {community_id}")
async def user_has_permission(
self, author_id: int, permission: str, community_id: int, session: Any = None
) -> bool:
"""
Проверяет, есть ли у пользователя конкретное разрешение в сообществе.
Args:
author_id: ID автора
permission: Разрешение для проверки
community_id: ID сообщества
session: Опциональная сессия БД (для тестов)
Returns:
True если разрешение есть, False если нет
"""
community_queries = get_community_queries()
user_roles = community_queries.get_user_roles_in_community(author_id, community_id, session)
return await self._roles_have_permission(user_roles, permission, community_id)
async def _get_role_permissions_for_community(self, community_id: int) -> dict:
"""
Получает права ролей для конкретного сообщества.
Если права не настроены, автоматически инициализирует их дефолтными.
Args:
community_id: ID сообщества
Returns:
Словарь прав ролей для сообщества
"""
key = f"community:roles:{community_id}"
data = await redis.execute("GET", key)
if data:
return json.loads(data)
# Автоматически инициализируем, если не найдено
await self.initialize_community_permissions(community_id)
# Получаем инициализированные разрешения
data = await redis.execute("GET", key)
if data:
return json.loads(data)
# Fallback на дефолтные разрешения если что-то пошло не так
return DEFAULT_ROLE_PERMISSIONS
async def _roles_have_permission(self, role_slugs: list[str], permission: str, community_id: int) -> bool:
"""
Проверяет, есть ли у набора ролей конкретное разрешение в сообществе.
Args:
role_slugs: Список ролей для проверки
permission: Разрешение для проверки
community_id: ID сообщества
Returns:
True если хотя бы одна роль имеет разрешение
"""
role_perms = await self._get_role_permissions_for_community(community_id)
return any(permission in role_perms.get(role, []) for role in role_slugs)
class CommunityAuthorQueriesImpl(CommunityAuthorQueries):
"""Конкретная реализация запросов CommunityAuthor через поздний импорт"""
def get_user_roles_in_community(
self, author_id: int, community_id: int = 1, session: Any = None
) -> list[str]:
"""
Получает роли пользователя в сообществе через новую систему CommunityAuthor
"""
# Поздний импорт для избежания циклических зависимостей
from orm.community import CommunityAuthor
try:
if session:
ca = (
session.query(CommunityAuthor)
.where(CommunityAuthor.author_id == author_id, CommunityAuthor.community_id == community_id)
.first()
)
return ca.role_list if ca else []
# Используем local_session для продакшена
with local_session() as db_session:
ca = (
db_session.query(CommunityAuthor)
.where(CommunityAuthor.author_id == author_id, CommunityAuthor.community_id == community_id)
.first()
)
return ca.role_list if ca else []
except Exception as e:
logger.error(f"[get_user_roles_in_community] Ошибка при получении ролей: {e}")
return []
# Создаем экземпляры реализаций
rbac_operations = RBACOperationsImpl()
community_queries = CommunityAuthorQueriesImpl()

24
services/rbac_init.py Normal file
View File

@@ -0,0 +1,24 @@
"""
Модуль инициализации RBAC системы.
Настраивает dependency injection для разрешения циклических зависимостей.
Должен вызываться при старте приложения.
"""
from auth.rbac_interface import set_community_queries, set_rbac_operations
from utils.logger import root_logger as logger
def initialize_rbac() -> None:
"""
Инициализирует RBAC систему с dependency injection.
Должна быть вызвана один раз при старте приложения после импорта всех модулей.
"""
from services.rbac_impl import community_queries, rbac_operations
# Устанавливаем реализации
set_rbac_operations(rbac_operations)
set_community_queries(community_queries)
logger.info("🧿 RBAC система инициализирована с dependency injection")

View File

@@ -1,6 +1,6 @@
import json
import logging
from typing import Any, Optional, Set, Union
from typing import Any, Set
import redis.asyncio as aioredis
@@ -20,7 +20,7 @@ class RedisService:
"""
def __init__(self, redis_url: str = REDIS_URL) -> None:
self._client: Optional[aioredis.Redis] = None
self._client: aioredis.Redis | None = None
self._redis_url = redis_url # Исправлено на _redis_url
self._is_available = aioredis is not None
@@ -126,11 +126,11 @@ class RedisService:
logger.exception("Redis command failed")
return None
async def get(self, key: str) -> Optional[Union[str, bytes]]:
async def get(self, key: str) -> str | bytes | None:
"""Get value by key"""
return await self.execute("get", key)
async def set(self, key: str, value: Any, ex: Optional[int] = None) -> bool:
async def set(self, key: str, value: Any, ex: int | None = None) -> bool:
"""Set key-value pair with optional expiration"""
if ex is not None:
result = await self.execute("setex", key, ex, value)
@@ -167,7 +167,7 @@ class RedisService:
"""Set hash field"""
await self.execute("hset", key, field, value)
async def hget(self, key: str, field: str) -> Optional[Union[str, bytes]]:
async def hget(self, key: str, field: str) -> str | bytes | None:
"""Get hash field"""
return await self.execute("hget", key, field)
@@ -213,10 +213,10 @@ class RedisService:
result = await self.execute("expire", key, seconds)
return bool(result)
async def serialize_and_set(self, key: str, data: Any, ex: Optional[int] = None) -> bool:
async def serialize_and_set(self, key: str, data: Any, ex: int | None = None) -> bool:
"""Serialize data to JSON and store in Redis"""
try:
if isinstance(data, (str, bytes)):
if isinstance(data, str | bytes):
serialized_data: bytes = data.encode("utf-8") if isinstance(data, str) else data
else:
serialized_data = json.dumps(data).encode("utf-8")

View File

@@ -9,9 +9,10 @@ from ariadne import (
load_schema_from_path,
)
from auth.orm import Author, AuthorBookmark, AuthorFollower, AuthorRating
# Импорт Author, AuthorBookmark, AuthorFollower, AuthorRating отложен для избежания циклических импортов
from orm import collection, community, draft, invite, notification, reaction, shout, topic
from services.db import create_table_if_not_exists, local_session
from auth.orm import Author, AuthorBookmark, AuthorFollower, AuthorRating
# Создаем основные типы
query = QueryType()

View File

@@ -4,7 +4,7 @@ import logging
import os
import secrets
import time
from typing import Any, Optional, cast
from typing import Any, cast
from httpx import AsyncClient, Response
@@ -80,7 +80,7 @@ class SearchCache:
logger.info(f"Cached {len(results)} search results for query '{query}' in memory")
return True
async def get(self, query: str, limit: int = 10, offset: int = 0) -> Optional[list]:
async def get(self, query: str, limit: int = 10, offset: int = 0) -> list | None:
"""Get paginated results for a query"""
normalized_query = self._normalize_query(query)
all_results = None

View File

@@ -1,9 +1,9 @@
import asyncio
import os
import time
from datetime import datetime, timedelta, timezone
from datetime import UTC, datetime, timedelta
from pathlib import Path
from typing import ClassVar, Optional
from typing import ClassVar
# ga
from google.analytics.data_v1beta import BetaAnalyticsDataClient
@@ -38,13 +38,13 @@ class ViewedStorage:
shouts_by_author: ClassVar[dict] = {}
views = None
period = 60 * 60 # каждый час
analytics_client: Optional[BetaAnalyticsDataClient] = None
analytics_client: BetaAnalyticsDataClient | None = None
auth_result = None
running = False
redis_views_key = None
last_update_timestamp = 0
start_date = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
_background_task: Optional[asyncio.Task] = None
start_date = datetime.now(tz=UTC).strftime("%Y-%m-%d")
_background_task: asyncio.Task | None = None
@staticmethod
async def init() -> None:
@@ -120,11 +120,11 @@ class ViewedStorage:
timestamp = await redis.execute("HGET", latest_key, "_timestamp")
if timestamp:
self.last_update_timestamp = int(timestamp)
timestamp_dt = datetime.fromtimestamp(int(timestamp), tz=timezone.utc)
timestamp_dt = datetime.fromtimestamp(int(timestamp), tz=UTC)
self.start_date = timestamp_dt.strftime("%Y-%m-%d")
# Если данные сегодняшние, считаем их актуальными
now_date = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
now_date = datetime.now(tz=UTC).strftime("%Y-%m-%d")
if now_date == self.start_date:
logger.info(" * Views data is up to date!")
else:
@@ -291,7 +291,7 @@ class ViewedStorage:
self.running = False
break
if failed == 0:
when = datetime.now(timezone.utc) + timedelta(seconds=self.period)
when = datetime.now(UTC) + timedelta(seconds=self.period)
t = format(when.astimezone().isoformat())
logger.info(" ⎩ next update: %s", t.split("T")[0] + " " + t.split("T")[1].split(".")[0])
await asyncio.sleep(self.period)