refactor(gost): вынести сборку списка литературы в app.bibliography + 7 тестов
Сортировка (кириллица→латиница), нумерация и выбор форматтера жили внутри 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 <noreply@anthropic.com>
This commit is contained in:
@@ -26,7 +26,7 @@ docker run --rm \
|
|||||||
|
|
||||||
echo '▶ mypy (чистая логика L1/L2 + ГОСТ + скоринг)'
|
echo '▶ mypy (чистая логика L1/L2 + ГОСТ + скоринг)'
|
||||||
( cd services/worker-indexer && mypy --config-file /repo/mypy.ini app/algorithms/ app/fragments.py )
|
( 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 )
|
( cd services/worker-gpu && mypy --config-file /repo/mypy.ini app/scoring.py )
|
||||||
"
|
"
|
||||||
echo "✅ Линт (ruff + mypy) пройден"
|
echo "✅ Линт (ruff + mypy) пройден"
|
||||||
|
|||||||
42
services/worker-gost/app/bibliography.py
Normal file
42
services/worker-gost/app/bibliography.py
Normal file
@@ -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),
|
||||||
|
}
|
||||||
@@ -5,10 +5,9 @@ from typing import Any
|
|||||||
from celery.utils.log import get_task_logger
|
from celery.utils.log import get_task_logger
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
from app.bibliography import build_bibliography
|
||||||
from app.celery_app import celery_app
|
from app.celery_app import celery_app
|
||||||
from app.db import db_session
|
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__)
|
logger = get_task_logger(__name__)
|
||||||
|
|
||||||
@@ -59,9 +58,6 @@ def format_bibliography(
|
|||||||
if not docs:
|
if not docs:
|
||||||
raise ValueError(f"Документы не найдены: {doc_ids}")
|
raise ValueError(f"Документы не найдены: {doc_ids}")
|
||||||
|
|
||||||
# Выбрать форматтер
|
|
||||||
formatter = GOST71Formatter() if style == "7.1" else GOST705Formatter()
|
|
||||||
|
|
||||||
# Преобразовать ORM объекты в словари для форматирования
|
# Преобразовать ORM объекты в словари для форматирования
|
||||||
docs_dicts = []
|
docs_dicts = []
|
||||||
for doc in docs:
|
for doc in docs:
|
||||||
@@ -80,27 +76,8 @@ def format_bibliography(
|
|||||||
"source": doc.source,
|
"source": doc.source,
|
||||||
})
|
})
|
||||||
|
|
||||||
# Сортировать: кириллица (рус. авторы) → латиница (иностр.)
|
# Сортировка + нумерация + форматирование — чистая логика в app.bibliography
|
||||||
sorted_docs = sorted(docs_dicts, key=lambda d: _get_sort_key(d))
|
result = build_bibliography(docs_dicts, style)
|
||||||
|
|
||||||
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),
|
|
||||||
}
|
|
||||||
|
|
||||||
# Сохранить результат
|
# Сохранить результат
|
||||||
task = session.get(Task, task_id)
|
task = session.get(Task, task_id)
|
||||||
@@ -112,7 +89,7 @@ def format_bibliography(
|
|||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Библиография сформирована для задачи {task_id!r}: "
|
f"Библиография сформирована для задачи {task_id!r}: "
|
||||||
f"{len(bibliography)} записей по ГОСТ {style}"
|
f"{result['total']} записей по ГОСТ {style}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Уведомить пользователя
|
# Уведомить пользователя
|
||||||
|
|||||||
59
services/worker-gost/tests/test_bibliography.py
Normal file
59
services/worker-gost/tests/test_bibliography.py
Normal file
@@ -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}
|
||||||
Reference in New Issue
Block a user