From ccc3521e746b55306f1752597ae39cfd0dee161d Mon Sep 17 00:00:00 2001 From: jze9 Date: Tue, 11 Aug 2026 20:20:01 +0500 Subject: [PATCH] =?UTF-8?q?refactor(gost):=20=D0=B2=D1=8B=D0=BD=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D0=B8=20=D1=81=D0=B1=D0=BE=D1=80=D0=BA=D1=83=20?= =?UTF-8?q?=D1=81=D0=BF=D0=B8=D1=81=D0=BA=D0=B0=20=D0=BB=D0=B8=D1=82=D0=B5?= =?UTF-8?q?=D1=80=D0=B0=D1=82=D1=83=D1=80=D1=8B=20=D0=B2=20app.bibliograph?= =?UTF-8?q?y=20+=207=20=D1=82=D0=B5=D1=81=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Сортировка (кириллица→латиница), нумерация и выбор форматтера жили внутри Celery-задачи с БД и не тестировались — хотя это порядок и вид готового списка литературы, который видит студент. Вынес в чистый app.bibliography.build_bibliography: - нумерация сквозная с 1; total = число записей; - сортировка по фамилии первого автора, кириллица раньше латиницы; - стиль 7.1 → полное описание (format_full), иначе 7.0.5 → краткая ссылка; - doc_id сохраняется в каждой записи; пустой список → total 0. ORM→dict конверсия осталась в задаче (она из БД), поведение сохранено 1:1. Добавлен в mypy-гейт. Тестов всего: 73 (indexer 24, gost 24, gpu 25). Co-Authored-By: Claude Opus 4.8 --- scripts/run_lint.sh | 2 +- services/worker-gost/app/bibliography.py | 42 +++++++++++++ services/worker-gost/app/tasks/gost.py | 31 ++-------- .../worker-gost/tests/test_bibliography.py | 59 +++++++++++++++++++ 4 files changed, 106 insertions(+), 28 deletions(-) create mode 100644 services/worker-gost/app/bibliography.py create mode 100644 services/worker-gost/tests/test_bibliography.py diff --git a/scripts/run_lint.sh b/scripts/run_lint.sh index 6e1c753..7d97705 100755 --- a/scripts/run_lint.sh +++ b/scripts/run_lint.sh @@ -26,7 +26,7 @@ docker run --rm \ echo '▶ mypy (чистая логика L1/L2 + ГОСТ + скоринг)' ( cd services/worker-indexer && mypy --config-file /repo/mypy.ini app/algorithms/ app/fragments.py ) - ( cd services/worker-gost && mypy --config-file /repo/mypy.ini app/formatters/ ) + ( cd services/worker-gost && mypy --config-file /repo/mypy.ini app/formatters/ app/bibliography.py ) ( cd services/worker-gpu && mypy --config-file /repo/mypy.ini app/scoring.py ) " echo "✅ Линт (ruff + mypy) пройден" diff --git a/services/worker-gost/app/bibliography.py b/services/worker-gost/app/bibliography.py new file mode 100644 index 0000000..64dc7ed --- /dev/null +++ b/services/worker-gost/app/bibliography.py @@ -0,0 +1,42 @@ +"""Сборка списка литературы по ГОСТ — чистая логика (без Celery/БД). + +Сортирует источники (кириллица → латиница), нумерует и форматирует каждый по +выбранному стилю. Вынесено из Celery-задачи для изолированного тестирования +порядка и нумерации — того, что студент видит в готовом списке литературы. +""" + +from collections.abc import Callable +from typing import Any + +from app.formatters.gost_7_0_5 import GOST705Formatter +from app.formatters.gost_7_1 import GOST71Formatter, _get_sort_key + + +def build_bibliography(docs: list[dict[str, Any]], style: str = "7.1") -> dict[str, Any]: + """Собрать нумерованный список литературы по ГОСТ. + + Args: + docs: словари документов (title/authors/year/...); каждый должен иметь "id" + style: "7.1" (полное описание) или иначе — "7.0.5" (краткая ссылка) + + Returns: + dict с полями: + bibliography: list of {"number", "citation", "doc_id"} + style: применённый стиль + total: число записей + """ + cite: Callable[[dict[str, Any]], str] = ( + GOST71Formatter().format_full if style == "7.1" else GOST705Formatter().format_short + ) + + sorted_docs = sorted(docs, key=_get_sort_key) + bibliography = [ + {"number": number, "citation": cite(doc), "doc_id": doc["id"]} + for number, doc in enumerate(sorted_docs, 1) + ] + + return { + "bibliography": bibliography, + "style": style, + "total": len(bibliography), + } diff --git a/services/worker-gost/app/tasks/gost.py b/services/worker-gost/app/tasks/gost.py index 5ec3be5..99ff3f9 100644 --- a/services/worker-gost/app/tasks/gost.py +++ b/services/worker-gost/app/tasks/gost.py @@ -5,10 +5,9 @@ from typing import Any from celery.utils.log import get_task_logger from sqlalchemy import select +from app.bibliography import build_bibliography from app.celery_app import celery_app from app.db import db_session -from app.formatters.gost_7_0_5 import GOST705Formatter -from app.formatters.gost_7_1 import GOST71Formatter, _get_sort_key logger = get_task_logger(__name__) @@ -59,9 +58,6 @@ def format_bibliography( if not docs: raise ValueError(f"Документы не найдены: {doc_ids}") - # Выбрать форматтер - formatter = GOST71Formatter() if style == "7.1" else GOST705Formatter() - # Преобразовать ORM объекты в словари для форматирования docs_dicts = [] for doc in docs: @@ -80,27 +76,8 @@ def format_bibliography( "source": doc.source, }) - # Сортировать: кириллица (рус. авторы) → латиница (иностр.) - sorted_docs = sorted(docs_dicts, key=lambda d: _get_sort_key(d)) - - bibliography = [] - for i, doc_dict in enumerate(sorted_docs, 1): - if style == "7.1": - citation = formatter.format_full(doc_dict) - else: - citation = formatter.format_short(doc_dict) - - bibliography.append({ - "number": i, - "citation": citation, - "doc_id": doc_dict["id"], - }) - - result = { - "bibliography": bibliography, - "style": style, - "total": len(bibliography), - } + # Сортировка + нумерация + форматирование — чистая логика в app.bibliography + result = build_bibliography(docs_dicts, style) # Сохранить результат task = session.get(Task, task_id) @@ -112,7 +89,7 @@ def format_bibliography( logger.info( f"Библиография сформирована для задачи {task_id!r}: " - f"{len(bibliography)} записей по ГОСТ {style}" + f"{result['total']} записей по ГОСТ {style}" ) # Уведомить пользователя diff --git a/services/worker-gost/tests/test_bibliography.py b/services/worker-gost/tests/test_bibliography.py new file mode 100644 index 0000000..12ee8ab --- /dev/null +++ b/services/worker-gost/tests/test_bibliography.py @@ -0,0 +1,59 @@ +"""Юнит-тесты сборки списка литературы по ГОСТ (порядок и нумерация).""" + +from app.bibliography import build_bibliography + + +def _doc(doc_id: int, last_name: str, year: int = 2020) -> dict: + return { + "id": doc_id, + "title": "Некоторое название работы", + "authors": [{"last_name": last_name}], + "year": year, + "journal": "Вестник", + "lang": "ru", + } + + +def test_empty_list(): + out = build_bibliography([], style="7.1") + assert out == {"bibliography": [], "style": "7.1", "total": 0} + + +def test_numbering_is_sequential_from_one(): + docs = [_doc(10, "Борисов"), _doc(20, "Александров"), _doc(30, "Васильев")] + out = build_bibliography(docs, style="7.1") + assert [b["number"] for b in out["bibliography"]] == [1, 2, 3] + assert out["total"] == 3 + + +def test_sorted_alphabetically_within_cyrillic(): + docs = [_doc(1, "Яковлев"), _doc(2, "Абрамов"), _doc(3, "Миронов")] + out = build_bibliography(docs, style="7.1") + order = [b["doc_id"] for b in out["bibliography"]] + assert order == [2, 3, 1] # Абрамов, Миронов, Яковлев + + +def test_cyrillic_sorted_before_latin(): + docs = [_doc(1, "Adams"), _doc(2, "Яковлев")] + out = build_bibliography(docs, style="7.1") + # русский источник идёт первым, иностранный — после + assert [b["doc_id"] for b in out["bibliography"]] == [2, 1] + + +def test_style_7_1_uses_full_citation(): + out = build_bibliography([_doc(1, "Иванов")], style="7.1") + citation = out["bibliography"][0]["citation"] + assert "//" in citation # полное описание статьи содержит разделитель журнала + assert out["style"] == "7.1" + + +def test_style_7_0_5_uses_short_citation(): + out = build_bibliography([_doc(1, "Иванов", year=2021)], style="7.0.5") + citation = out["bibliography"][0]["citation"] + assert citation == "[Иванов, 2021]" # краткая ссылка + assert out["style"] == "7.0.5" + + +def test_doc_id_preserved_in_each_entry(): + out = build_bibliography([_doc(42, "Иванов"), _doc(7, "Абрамов")], style="7.1") + assert {b["doc_id"] for b in out["bibliography"]} == {42, 7}