Files
anti-plagiarism/services/worker-gost/app/formatters/gost_7_1.py
jze9 2daaa8c8a4 chore(lint): ruff-гейт в CI + фиксы (0 находок) — блокирует кривой деплой
Второй CI-гейт после тестов: ruff как статический анализатор всего Python-кода
(services + scripts). Раньше ни линта, ни проверки типов в CI не было вовсе.

Конфиг ruff.toml: правила E/F/W/I/UP/B/SIM/C4, line-length 100. Осознанно
выключены E501 (длину держит форматтер; длинные RU-комментарии — норма),
B008 (Depends()/Query() в дефолтах — идиома FastAPI, не баг) и UP042
((str, Enum)→StrEnum меняет __str__/сериализацию — не трогаем).

Починено под ноль находок:
- B904 (11): raise ... from exc / from None — читаемые цепочки исключений в
  Celery-ретраях и HTTPException, ошибки обработки не маскируют исходные.
- SIM105 (5): try/except/pass → contextlib.suppress (faiss remove_ids, lsh.remove,
  сброс кэша, ws-disconnect, парс года).
- C416/SIM108/B905/F841/UP035/UP017/F401/I001: dict(rows), тернарник, zip strict,
  мёртвая переменная, устаревшие импорты, timezone.utc→UTC, чистка/сортировка.

Обвязка: scripts/run_lint.sh (ruff в изолированном python:3.11-slim), шаг «Линт»
в job test перед юнит-тестами (падаем раньше). make lint / make lint-fix.
Все 41 юнит-тест по-прежнему зелёные, изменённые файлы компилируются.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 17:20:24 +05:00

264 lines
8.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""ГОСТ 7.1-2003 — Библиографическая запись. Библиографическое описание.
Полная запись для списка литературы.
Формат для статьи в журнале:
Автор(ы). Название // Журнал. — Год. — Т. X, № X. — С. X–X.
Формат для книги:
Автор(ы). Название. — Город : Издательство, Год. — X с.
Правила по ГОСТ 7.1-2003:
- До 3 авторов: перечислить всех
- 4+ авторов: первые 3 + "и др." (рус.) / "et al." (англ.)
- Разделитель смысловых частей: " — "
- Разделитель авторов: ", "
- Место публикации через "/"
"""
def _format_author(author: dict) -> str:
"""
Форматировать одного автора.
Args:
author: dict с полями last_name, first_name, initials (опционально)
Returns:
Строка вида "Иванов И. И." или "Иванов И."
"""
last_name = author.get("last_name", "").strip()
initials = author.get("initials", "").strip()
first_name = author.get("first_name", "").strip()
if not last_name:
return ""
if initials:
# Нормализовать инициалы: обеспечить точки
if not initials.endswith("."):
initials += "."
return f"{last_name} {initials}"
elif first_name:
# Извлечь инициалы из полного имени
parts = first_name.split()
inits = "".join(p[0].upper() + "." for p in parts if p)
return f"{last_name} {inits}"
else:
return last_name
def _format_authors(authors: list[dict], lang: str = "ru") -> str:
"""
Форматировать список авторов по правилам ГОСТ 7.1-2003.
Args:
authors: Список словарей с авторами
lang: Язык для "и др." / "et al."
Returns:
Строка с отформатированными авторами
"""
if not authors:
return ""
et_al = "и др." if lang == "ru" else "et al."
formatted = [_format_author(a) for a in authors if a.get("last_name")]
formatted = [f for f in formatted if f]
if not formatted:
return ""
if len(formatted) <= 3:
return ", ".join(formatted)
else:
return ", ".join(formatted[:3]) + f", {et_al}"
class GOST71Formatter:
"""Форматтер полных библиографических записей по ГОСТ 7.1-2003."""
def format_full(self, doc: dict) -> str:
"""
Форматировать полную библиографическую запись.
Args:
doc: Словарь с полями документа
Returns:
Отформатированная строка библиографической записи
"""
doc_type = self._detect_type(doc)
if doc_type == "article":
return self._format_article(doc)
elif doc_type == "book":
return self._format_book(doc)
elif doc_type == "web":
return self._format_web(doc)
else:
return self._format_generic(doc)
def _detect_type(self, doc: dict) -> str:
"""Определить тип документа."""
if doc.get("journal"):
return "article"
if doc.get("url") and not doc.get("journal"):
return "web"
return "generic"
def _format_article(self, doc: dict) -> str:
"""
Форматировать статью в журнале.
Формат: Автор(ы). Название // Журнал. — Год. — Т. X, № X. — С. X–X.
"""
lang = doc.get("lang", "ru") or "ru"
authors = doc.get("authors", [])
author_str = _format_authors(authors, lang=lang)
title = (doc.get("title") or "").strip()
journal = (doc.get("journal") or "").strip()
year = doc.get("year", "")
volume = doc.get("volume", "")
issue = doc.get("issue", "")
pages = doc.get("pages", "")
doi = doc.get("doi", "")
parts = []
# Авторы и название
if author_str:
parts.append(f"{author_str}. {title}")
else:
parts.append(title)
# Место публикации
location = f"// {journal}"
if year:
location += f". — {year}"
if volume:
location += f". — Т. {volume}"
if issue:
location += f", № {issue}"
if pages:
location += f". — С. {pages}"
parts.append(location)
result = " ".join(parts)
# DOI
if doi:
result += f". — DOI: {doi}"
return result.strip()
def _format_book(self, doc: dict) -> str:
"""
Форматировать книгу / монографию.
Формат: Автор(ы). Название. — Город : Издательство, Год. — X с.
"""
lang = doc.get("lang", "ru") or "ru"
authors = doc.get("authors", [])
author_str = _format_authors(authors, lang=lang)
title = (doc.get("title") or "").strip()
year = doc.get("year", "б. г.")
pages = doc.get("pages", "")
parts = []
if author_str:
parts.append(f"{author_str}.")
parts.append(f"{title}.")
parts.append(f"— {year}.")
if pages:
# Предполагаем, что pages содержит количество страниц
parts.append(f"— {pages} с.")
return " ".join(parts).strip()
def _format_web(self, doc: dict) -> str:
"""
Форматировать электронный ресурс.
Формат: Автор(ы). Название [Электронный ресурс]. — URL: ... (дата обращения: ...)
"""
lang = doc.get("lang", "ru") or "ru"
authors = doc.get("authors", [])
author_str = _format_authors(authors, lang=lang)
title = (doc.get("title") or "").strip()
url = (doc.get("url") or "").strip()
year = doc.get("year", "")
parts = []
if author_str:
parts.append(f"{author_str}.")
# Для ГОСТ — обозначение электронного ресурса
parts.append(f"{title} [Электронный ресурс].")
if year:
parts.append(f"— {year}.")
if url:
parts.append(f"— URL: {url}")
return " ".join(parts).strip()
def _format_generic(self, doc: dict) -> str:
"""Базовый форматтер для неопределённых типов."""
lang = doc.get("lang", "ru") or "ru"
author_str = _format_authors(doc.get("authors", []), lang=lang)
title = (doc.get("title") or "").strip()
year = doc.get("year", "")
result = ""
if author_str:
result += f"{author_str}. "
result += title
if year:
result += f". — {year}"
return result.strip()
def _get_sort_key(doc: dict) -> str:
"""
Получить ключ сортировки для документа.
Кириллица идёт перед латиницей (для русских традиций):
сначала русскоязычные источники, затем иностранные.
Args:
doc: Словарь с метаданными
Returns:
Строка для сортировки
"""
authors = doc.get("authors", [])
last_name = authors[0].get("last_name", "") if authors else doc.get("title", "")
if not last_name:
return "яяя" # В конец
# Кириллица < латиница (традиция: русские источники первыми)
first_char = last_name[0].lower()
is_latin = ord(first_char) < 256 and first_char.isalpha()
# Префикс для сортировки: 0 для кириллицы, 1 для латиницы
prefix = "1" if is_latin else "0"
return f"{prefix}{last_name.lower()}"
def format_full(doc: dict) -> str:
"""Удобная функция для быстрого форматирования."""
return GOST71Formatter().format_full(doc)