From f79384e2b1efb30fb718158647be3b0f6660fce2 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Mon, 14 Sep 2026 14:07:20 +0400 Subject: [PATCH] =?UTF-8?q?merge=5Fresults:=20=D1=83=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D1=82=D1=8C=20I/O=20=D0=B8=D0=B7=20merge=5Fdocuments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Функция принимает уже прочитанные json-объекты и сама ничего не открывает — транспорт (локальная ФС, SMB-шара, S3, поток) остаётся за вызывающим. Локальное чтение переехало в CLI как одного из них. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01JvCSGmwgL8juhZdXr32qXM --- src/core/merge_results.py | 52 +++++++++++++++++++----------------- tests/test_merge_results.py | 53 ++++++++++++++++++++++--------------- 2 files changed, 59 insertions(+), 46 deletions(-) diff --git a/src/core/merge_results.py b/src/core/merge_results.py index b5668f7..2f6c58d 100644 --- a/src/core/merge_results.py +++ b/src/core/merge_results.py @@ -8,8 +8,12 @@ Содержимое документов не обрабатывается и не валидируется — кладётся как есть. -Модуль намеренно не зависит от config/S3/NATS, чтобы его можно было -использовать и как отдельный скрипт, и из PollWorker. +merge_documents() принимает уже прочитанные объекты и сама ничего не открывает. +Откуда они взялись — локальная ФС, SMB-шара, S3, http — решает вызывающий: + + merge_documents([json.load(f) for f in handles], file_id, legal_entity_id) + +Поэтому модуль не тянет ни config, ни S3, ни NATS, ни smb-клиент. """ import argparse @@ -18,34 +22,31 @@ import sys from pathlib import Path -def load_document(path) -> object: - """Читает один json-документ процессора.""" - with open(path, 'r', encoding='utf-8') as f: - return json.load(f) +def merge_documents(documents, file_id, legal_entity_id) -> dict: + """Собирает распознанные документы в один результат. - -def merge_documents(paths, file_id, legal_entity_id) -> dict: - """Собирает документы в один результат. - - Порядок элементов в 'files' — порядок переданных путей. + documents — любой iterable уже разобранных json-объектов. + Порядок элементов в 'files' сохраняется. """ - paths = list(paths) - if not paths: + documents = list(documents) + if not documents: raise ValueError('нечего мержить: не передано ни одного документа') return { 'file_id': file_id, 'legal_entity_id': legal_entity_id, - 'files': [load_document(path) for path in paths], + 'files': documents, } -def merge_directory(job_dir, file_id, legal_entity_id) -> dict: - """То же, но берёт все *.json из директории, отсортированные по имени.""" - paths = sorted(Path(job_dir).glob('*.json')) - if not paths: - raise ValueError(f'в директории нет json-файлов: {job_dir}') - return merge_documents(paths, file_id, legal_entity_id) +# --- CLI: один из вызывающих, читает локальную ФС --------------------------- + +def _read_local(paths) -> list: + result = [] + for path in paths: + with open(path, 'r', encoding='utf-8') as f: + result.append(json.load(f)) + return result def main(argv=None) -> int: @@ -72,11 +73,14 @@ def main(argv=None) -> int: if bool(args.documents) == bool(args.dir): parser.error('укажи либо пути к документам, либо --dir, но не оба сразу') + paths = sorted(Path(args.dir).glob('*.json')) if args.dir else args.documents + if not paths: + print(f'ошибка: в директории нет json-файлов: {args.dir}', file=sys.stderr) + return 1 + try: - if args.dir: - result = merge_directory(args.dir, args.file_id, args.legal_entity_id) - else: - result = merge_documents(args.documents, args.file_id, args.legal_entity_id) + documents = _read_local(paths) + result = merge_documents(documents, args.file_id, args.legal_entity_id) except (OSError, ValueError, json.JSONDecodeError) as e: print(f'ошибка: {e}', file=sys.stderr) return 1 diff --git a/tests/test_merge_results.py b/tests/test_merge_results.py index 5396ff4..6c648d8 100644 --- a/tests/test_merge_results.py +++ b/tests/test_merge_results.py @@ -19,16 +19,8 @@ DOC_B = { } -def write_doc(directory: Path, name: str, payload: dict) -> Path: - path = directory / name - path.write_text(json.dumps(payload, ensure_ascii=False), encoding='utf-8') - return path - - -def test_merges_two_documents_into_one_result(tmp_path): - paths = [write_doc(tmp_path, 'doc_1.json', DOC_A), write_doc(tmp_path, 'doc_2.json', DOC_B)] - - result = merge_documents(paths, file_id='file-1', legal_entity_id='le-1') +def test_merges_two_documents_into_one_result(): + result = merge_documents([DOC_A, DOC_B], file_id='file-1', legal_entity_id='le-1') assert result == { 'file_id': 'file-1', @@ -37,34 +29,51 @@ def test_merges_two_documents_into_one_result(tmp_path): } -def test_keeps_document_content_untouched(tmp_path): +def test_does_no_io_at_all(monkeypatch): + # функция не должна ничего открывать — чтение целиком на вызывающем + def explode(*args, **kwargs): + raise AssertionError('merge_documents не должна трогать файловую систему') + + monkeypatch.setattr('builtins.open', explode) + + result = merge_documents([DOC_A], file_id='f', legal_entity_id='l') + + assert result['files'] == [DOC_A] + + +def test_keeps_document_content_untouched(): # содержимое кладётся как есть: незнакомые поля и вложенность не теряются weird = {'file_uid': 'uid-c', 'unexpected': [1, {'deep': None}], 'кириллица': 'значение'} - paths = [write_doc(tmp_path, 'doc_1.json', weird)] - result = merge_documents(paths, file_id='file-1', legal_entity_id='le-1') + result = merge_documents([weird], file_id='file-1', legal_entity_id='le-1') assert result['files'] == [weird] -def test_preserves_given_order(tmp_path): - # порядок в files[] — порядок переданных путей, а не порядок записи на диск - second = write_doc(tmp_path, 'z_doc.json', DOC_B) - first = write_doc(tmp_path, 'a_doc.json', DOC_A) - - result = merge_documents([second, first], file_id='f', legal_entity_id='l') +def test_preserves_given_order(): + result = merge_documents([DOC_B, DOC_A], file_id='f', legal_entity_id='l') assert [doc['file_uid'] for doc in result['files']] == ['uid-b', 'uid-a'] +def test_accepts_any_iterable(): + # вызывающий может отдавать генератор, например потоковое чтение с шары + result = merge_documents((d for d in (DOC_A, DOC_B)), file_id='f', legal_entity_id='l') + + assert len(result['files']) == 2 + + def test_rejects_empty_input(): with pytest.raises(ValueError): merge_documents([], file_id='f', legal_entity_id='l') -def test_cli_writes_merged_file(tmp_path): - a = write_doc(tmp_path, 'doc_1.json', DOC_A) - b = write_doc(tmp_path, 'doc_2.json', DOC_B) +def test_cli_reads_local_files_and_writes_merged(tmp_path): + # CLI — один из вызывающих: локальное чтение живёт здесь, а не в merge_documents + a = tmp_path / 'doc_1.json' + a.write_text(json.dumps(DOC_A, ensure_ascii=False), encoding='utf-8') + b = tmp_path / 'doc_2.json' + b.write_text(json.dumps(DOC_B, ensure_ascii=False), encoding='utf-8') out = tmp_path / 'merged.json' completed = subprocess.run(