From 76bcb0db35cfea5bf6cf564720caba1de629afa5 Mon Sep 17 00:00:00 2001 From: codex Date: Thu, 8 Oct 2026 14:52:12 +0300 Subject: [PATCH] fix: make council source checksum reproducible across platforms --- .gitattributes | 2 ++ README.md | 7 +++++-- app/data/council_articles.json | 3 ++- app/services/council_source.py | 6 ++++-- app/version.py | 6 +++--- pyproject.toml | 2 +- scripts/prepare_council_articles.py | 2 +- tests/test_api.py | 2 +- tests/test_council.py | 2 +- tests/test_council_source.py | 24 +++++++++++++++++++++++- 10 files changed, 43 insertions(+), 13 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..3858263 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +app/data/council_articles.csv text eol=lf +app/data/council_articles.json text eol=lf diff --git a/README.md b/README.md index 003972d..cc2cdfb 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ node --check app/static/admin.js Тесты с данными используют временную SQLite; отдельные API smoke-тесты запускают приложение с его текущей конфигурацией. Браузерная проверка: `pip install playwright`, `python -m playwright install chromium`, затем `python tests/browser_admin.py`. Она проверяет реальный рендеринг страниц, клавиатуру, диалог, фильтры и восстановление прогресса после ошибки. Скриншоты сохраняются во временную папку; путь выводится в конце. -Версия сервиса: `0.15.2`. Админка всегда показывает версии backend и frontend в footer. +Версия сервиса: `0.15.3`. Админка всегда показывает версии backend и frontend в footer. ## Разовый импорт публикаций @@ -219,6 +219,9 @@ python scripts/prepare_council_articles.py app/data/council_articles.csv ``` Нужна только стандартная библиотека Python. Скрипт проверяет структуру CSV и индекс, -сохраняет число записей и SHA-256 источника и заменяет JSON атомарно после проверки. +сохраняет число записей и SHA-256 источника с окончаниями строк LF и заменяет JSON +атомарно после проверки. CRLF нормализуются в LF только при чтении; исходный CSV +не перезаписывается. JSON и хеш одинаковы на Windows и Linux; Git фиксирует эти +файлы с LF. После замены файла перезапустите процессы API, чтобы сбросить их кэш. Обновления публикаций из БД продолжают отображаться без перезапуска. diff --git a/app/data/council_articles.json b/app/data/council_articles.json index 90f770d..20bedb9 100644 --- a/app/data/council_articles.json +++ b/app/data/council_articles.json @@ -1,7 +1,8 @@ { "schema_version": 1, "source_file": "council_articles.csv", - "source_sha256": "7729d06503de8197c25e61f6e653a342390f274f31fd6bed43496c4cc21b8177", + "source_sha256": "f9e6126efcb144042e89bcce284deb3e5b26dd76b1be0741962626d2b140c043", + "source_hash_eol": "LF", "source_rows": 1797, "publications": [ { diff --git a/app/services/council_source.py b/app/services/council_source.py index 80c13ce..dba54bf 100644 --- a/app/services/council_source.py +++ b/app/services/council_source.py @@ -41,7 +41,9 @@ def _journal_names(data: dict) -> list[str]: def prepare_reference(source: Path) -> dict: publications = [] by_author = defaultdict(list) - source_bytes = source.read_bytes() + # Git stores this text export with LF; canonicalize Windows CRLF before + # parsing and hashing so the prepared artifact is identical on both hosts. + source_bytes = source.read_bytes().replace(b"\r\n", b"\n") with StringIO(source_bytes.decode("utf-8-sig"), newline="") as stream: reader = csv.DictReader(stream, delimiter=";") required = {"Название публикации", "Год", "DOI", "Авторы НИУ ВШЭ", "Квартиль scopus", "Квартиль wos"} @@ -65,7 +67,7 @@ def prepare_reference(source: Path) -> dict: for author in sorted({_name(value) for value in authors} - {""}): by_author[author].append(index) return {"schema_version": 1, "source_file": source.name, - "source_sha256": hashlib.sha256(source_bytes).hexdigest(), + "source_sha256": hashlib.sha256(source_bytes).hexdigest(), "source_hash_eol": "LF", "source_rows": len(publications), "publications": publications, "by_author": dict(by_author)} diff --git a/app/version.py b/app/version.py index f5463ef..2d0ed8b 100644 --- a/app/version.py +++ b/app/version.py @@ -1,3 +1,3 @@ -APP_VERSION = "0.15.2" -FRONTEND_VERSION = "0.15.2" -BACKEND_VERSION = "0.15.2" +APP_VERSION = "0.15.3" +FRONTEND_VERSION = "0.15.3" +BACKEND_VERSION = "0.15.3" diff --git a/pyproject.toml b/pyproject.toml index ac014a1..6db66dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "miem-workers" -version = "0.15.2" +version = "0.15.3" description = "MIEM employees parser, admin API, and web admin" requires-python = ">=3.11" dependencies = [ diff --git a/scripts/prepare_council_articles.py b/scripts/prepare_council_articles.py index bce2a46..5336e2a 100644 --- a/scripts/prepare_council_articles.py +++ b/scripts/prepare_council_articles.py @@ -21,7 +21,7 @@ if __name__ == "__main__": parser.error("Output must not overwrite the source CSV") result = prepare_reference(args.source) text = json.dumps(result, ensure_ascii=False, indent=2) + "\n" - with NamedTemporaryFile(mode="w", encoding="utf-8", dir=args.output.parent, + with NamedTemporaryFile(mode="w", encoding="utf-8", newline="\n", dir=args.output.parent, prefix=args.output.name + ".", suffix=".tmp", delete=False) as stream: stream.write(text) temporary = Path(stream.name) diff --git a/tests/test_api.py b/tests/test_api.py index ce0391d..3bbde7e 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -18,7 +18,7 @@ def test_health_returns_versions(): response = TestClient(app).get("/api/health") assert response.status_code == 200 - assert response.json()["backend_version"] == "0.15.2" + assert response.json()["backend_version"] == "0.15.3" def test_api_employees_and_stats_require_admin_session(): diff --git a/tests/test_council.py b/tests/test_council.py index 0f63351..fb583b2 100644 --- a/tests/test_council.py +++ b/tests/test_council.py @@ -215,7 +215,7 @@ def test_public_page_data_minimization_and_safe_links(db_session, export): assert "<script>" in response.text and "