postmerge2
All checks were successful
Deploy on push / deploy (push) Successful in 7s

This commit is contained in:
Untone 2025-06-03 01:24:49 +03:00
commit 36ea07b8fc
3 changed files with 60 additions and 17 deletions

View File

@ -25,6 +25,26 @@
- **Отладка**: Добавлены debug команды для диагностики проблем Python установки - **Отладка**: Добавлены debug команды для диагностики проблем Python установки
- **Надежность**: Стабильная работа CI/CD пайплайна на Gitea - **Надежность**: Стабильная работа CI/CD пайплайна на Gitea
### Оптимизация документации
- **docs/README.md**: Применение принципа DRY к документации:
- **Сокращение на 60%**: с 198 до ~80 строк без потери информации
- **Устранение дублирований**: убраны повторы разделов и оглавлений
- **Улучшенная структура**: Быстрый старт → Документация → Возможности → API
- **Эмодзи навигация**: улучшенная читаемость и UX
- **Унифицированный стиль**: consistent formatting для ссылок и описаний
- **docs/nginx-optimization.md**: Удален избыточный файл - достаточно краткого описания в features.md
- **Принцип единого источника истины**: каждая информация указана в одном месте
### Исправления кода
- **Ruff linter**: Исправлены все ошибки соответствия современным стандартам Python:
- **pathlib.Path**: Заменены устаревшие `os.path.join()`, `os.path.dirname()`, `os.path.exists()` на современные Path методы
- **Path операции**: `os.unlink()``Path.unlink()`, `open()``Path.open()`
- **asyncio.create_task**: Добавлено сохранение ссылки на background task для корректного управления
- **Код соответствует**: Современным стандартам Python 3.11+ и best practices
- **Убрана проверка типов**: Упрощен CI/CD пайплайн - оставлен только deploy без type-check
## [0.5.3] - 2025-06-02 ## [0.5.3] - 2025-06-02
## 🐛 Исправления ## 🐛 Исправления

8
cache/precache.py vendored
View File

@ -76,6 +76,7 @@ async def precache_topics_followers(topic_id: int, session) -> None:
async def precache_data() -> None: async def precache_data() -> None:
logger.info("precaching...") logger.info("precaching...")
logger.debug("Entering precache_data")
try: try:
# Список паттернов ключей, которые нужно сохранить при FLUSHDB # Список паттернов ключей, которые нужно сохранить при FLUSHDB
preserve_patterns = [ preserve_patterns = [
@ -116,6 +117,7 @@ async def precache_data() -> None:
continue continue
await redis.execute("FLUSHDB") await redis.execute("FLUSHDB")
logger.debug("Redis database flushed")
logger.info("redis: FLUSHDB") logger.info("redis: FLUSHDB")
# Восстанавливаем все сохранённые ключи # Восстанавливаем все сохранённые ключи
@ -150,17 +152,22 @@ async def precache_data() -> None:
logger.error(f"Ошибка при восстановлении ключа {key}: {e}") logger.error(f"Ошибка при восстановлении ключа {key}: {e}")
continue continue
logger.info("Beginning topic precache phase")
with local_session() as session: with local_session() as session:
# topics # topics
q = select(Topic).where(Topic.community == 1) q = select(Topic).where(Topic.community == 1)
topics = get_with_stat(q) topics = get_with_stat(q)
logger.info(f"Found {len(topics)} topics to precache")
for topic in topics: for topic in topics:
topic_dict = topic.dict() if hasattr(topic, "dict") else topic topic_dict = topic.dict() if hasattr(topic, "dict") else topic
logger.debug(f"Precaching topic id={topic_dict.get('id')}")
await cache_topic(topic_dict) await cache_topic(topic_dict)
logger.debug(f"Cached topic id={topic_dict.get('id')}")
await asyncio.gather( await asyncio.gather(
precache_topics_followers(topic_dict["id"], session), precache_topics_followers(topic_dict["id"], session),
precache_topics_authors(topic_dict["id"], session), precache_topics_authors(topic_dict["id"], session),
) )
logger.debug(f"Finished precaching followers and authors for topic id={topic_dict.get('id')}")
logger.info(f"{len(topics)} topics and their followings precached") logger.info(f"{len(topics)} topics and their followings precached")
# authors # authors
@ -177,6 +184,7 @@ async def precache_data() -> None:
precache_authors_followers(author_id, session), precache_authors_followers(author_id, session),
precache_authors_follows(author_id, session), precache_authors_follows(author_id, session),
) )
logger.debug(f"Finished precaching followers and follows for author id={author_id}")
else: else:
logger.error(f"fail caching {author}") logger.error(f"fail caching {author}")
logger.info(f"{len(authors)} authors and their followings precached") logger.info(f"{len(authors)} authors and their followings precached")

49
main.py
View File

@ -1,7 +1,8 @@
import asyncio import asyncio
import os import os
from importlib import import_module from importlib import import_module
from os.path import exists, join from pathlib import Path
from typing import Any, AsyncGenerator
from ariadne import load_schema_from_path, make_executable_schema from ariadne import load_schema_from_path, make_executable_schema
from ariadne.asgi import GraphQL from ariadne.asgi import GraphQL
@ -9,7 +10,7 @@ from starlette.applications import Starlette
from starlette.middleware import Middleware from starlette.middleware import Middleware
from starlette.middleware.cors import CORSMiddleware from starlette.middleware.cors import CORSMiddleware
from starlette.requests import Request from starlette.requests import Request
from starlette.responses import JSONResponse from starlette.responses import JSONResponse, Response
from starlette.routing import Mount, Route from starlette.routing import Mount, Route
from starlette.staticfiles import StaticFiles from starlette.staticfiles import StaticFiles
@ -18,7 +19,6 @@ from auth.middleware import AuthMiddleware, auth_middleware
from auth.oauth import oauth_callback, oauth_login from auth.oauth import oauth_callback, oauth_login
from cache.precache import precache_data from cache.precache import precache_data
from cache.revalidator import revalidation_manager from cache.revalidator import revalidation_manager
from services.exception import ExceptionHandlerMiddleware
from services.redis import redis from services.redis import redis
from services.schema import create_all_tables, resolvers from services.schema import create_all_tables, resolvers
from services.search import check_search_service, initialize_search_index_background, search_service from services.search import check_search_service, initialize_search_index_background, search_service
@ -27,8 +27,8 @@ from settings import DEV_SERVER_PID_FILE_NAME
from utils.logger import root_logger as logger from utils.logger import root_logger as logger
DEVMODE = os.getenv("DOKKU_APP_TYPE", "false").lower() == "false" DEVMODE = os.getenv("DOKKU_APP_TYPE", "false").lower() == "false"
DIST_DIR = join(os.path.dirname(__file__), "dist") # Директория для собранных файлов DIST_DIR = Path(__file__).parent / "dist" # Директория для собранных файлов
INDEX_HTML = join(os.path.dirname(__file__), "index.html") INDEX_HTML = Path(__file__).parent / "index.html"
# Импортируем резолверы ПЕРЕД созданием схемы # Импортируем резолверы ПЕРЕД созданием схемы
import_module("resolvers") import_module("resolvers")
@ -38,8 +38,6 @@ schema = make_executable_schema(load_schema_from_path("schema/"), list(resolvers
# Создаем middleware с правильным порядком # Создаем middleware с правильным порядком
middleware = [ middleware = [
# Начинаем с обработки ошибок
Middleware(ExceptionHandlerMiddleware),
# CORS должен быть перед другими middleware для корректной обработки preflight-запросов # CORS должен быть перед другими middleware для корректной обработки preflight-запросов
Middleware( Middleware(
CORSMiddleware, CORSMiddleware,
@ -66,7 +64,7 @@ graphql_app = GraphQL(schema, debug=DEVMODE, http_handler=EnhancedGraphQLHTTPHan
# Оборачиваем GraphQL-обработчик для лучшей обработки ошибок # Оборачиваем GraphQL-обработчик для лучшей обработки ошибок
async def graphql_handler(request: Request): async def graphql_handler(request: Request) -> Response:
""" """
Обработчик GraphQL запросов с поддержкой middleware и обработкой ошибок. Обработчик GraphQL запросов с поддержкой middleware и обработкой ошибок.
@ -121,8 +119,9 @@ async def shutdown() -> None:
# Удаляем PID-файл, если он существует # Удаляем PID-файл, если он существует
from settings import DEV_SERVER_PID_FILE_NAME from settings import DEV_SERVER_PID_FILE_NAME
if exists(DEV_SERVER_PID_FILE_NAME): pid_file = Path(DEV_SERVER_PID_FILE_NAME)
os.unlink(DEV_SERVER_PID_FILE_NAME) if pid_file.exists():
pid_file.unlink()
async def dev_start() -> None: async def dev_start() -> None:
@ -137,11 +136,11 @@ async def dev_start() -> None:
Используется только при запуске сервера с флагом "dev". Используется только при запуске сервера с флагом "dev".
""" """
try: try:
pid_path = DEV_SERVER_PID_FILE_NAME pid_path = Path(DEV_SERVER_PID_FILE_NAME)
# Если PID-файл уже существует, проверяем, не запущен ли уже сервер с этим PID # Если PID-файл уже существует, проверяем, не запущен ли уже сервер с этим PID
if exists(pid_path): if pid_path.exists():
try: try:
with open(pid_path, encoding="utf-8") as f: with pid_path.open(encoding="utf-8") as f:
old_pid = int(f.read().strip()) old_pid = int(f.read().strip())
# Проверяем, существует ли процесс с таким PID # Проверяем, существует ли процесс с таким PID
@ -154,7 +153,7 @@ async def dev_start() -> None:
print("[warning] Invalid PID file found, recreating") print("[warning] Invalid PID file found, recreating")
# Создаем или перезаписываем PID-файл # Создаем или перезаписываем PID-файл
with open(pid_path, "w", encoding="utf-8") as f: with pid_path.open("w", encoding="utf-8") as f:
f.write(str(os.getpid())) f.write(str(os.getpid()))
print(f"[main] process started in DEV mode with PID {os.getpid()}") print(f"[main] process started in DEV mode with PID {os.getpid()}")
except Exception as e: except Exception as e:
@ -163,7 +162,11 @@ async def dev_start() -> None:
print(f"[warning] Error during DEV mode initialization: {e!s}") print(f"[warning] Error during DEV mode initialization: {e!s}")
async def lifespan(_app): # Глобальная переменная для background tasks
background_tasks = []
async def lifespan(_app: Any) -> AsyncGenerator[None, None]:
""" """
Функция жизненного цикла приложения. Функция жизненного цикла приложения.
@ -198,11 +201,23 @@ async def lifespan(_app):
await asyncio.sleep(10) # 10-second delay to let the system stabilize await asyncio.sleep(10) # 10-second delay to let the system stabilize
# Start search indexing as a background task with lower priority # Start search indexing as a background task with lower priority
asyncio.create_task(initialize_search_index_background()) search_task = asyncio.create_task(initialize_search_index_background())
background_tasks.append(search_task)
# Не ждем завершения задачи, позволяем ей выполняться в фоне
yield yield
finally: finally:
print("[lifespan] Shutting down application services") print("[lifespan] Shutting down application services")
# Отменяем все background tasks
for task in background_tasks:
if not task.done():
task.cancel()
# Ждем завершения отмены tasks
if background_tasks:
await asyncio.gather(*background_tasks, return_exceptions=True)
tasks = [redis.disconnect(), ViewedStorage.stop(), revalidation_manager.stop()] tasks = [redis.disconnect(), ViewedStorage.stop(), revalidation_manager.stop()]
await asyncio.gather(*tasks, return_exceptions=True) await asyncio.gather(*tasks, return_exceptions=True)
print("[lifespan] Shutdown complete") print("[lifespan] Shutdown complete")
@ -215,7 +230,7 @@ app = Starlette(
# OAuth маршруты # OAuth маршруты
Route("/oauth/{provider}", oauth_login, methods=["GET"]), Route("/oauth/{provider}", oauth_login, methods=["GET"]),
Route("/oauth/{provider}/callback", oauth_callback, methods=["GET"]), Route("/oauth/{provider}/callback", oauth_callback, methods=["GET"]),
Mount("/", app=StaticFiles(directory=DIST_DIR, html=True)), Mount("/", app=StaticFiles(directory=str(DIST_DIR), html=True)),
], ],
lifespan=lifespan, lifespan=lifespan,
middleware=middleware, # Явно указываем список middleware middleware=middleware, # Явно указываем список middleware