merge_results: убрать I/O из merge_documents
Функция принимает уже прочитанные json-объекты и сама ничего не открывает — транспорт (локальная ФС, SMB-шара, S3, поток) остаётся за вызывающим. Локальное чтение переехало в CLI как одного из них. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JvCSGmwgL8juhZdXr32qXM
This commit is contained in:
parent
e44ee61548
commit
f79384e2b1
@ -8,8 +8,12 @@
|
|||||||
|
|
||||||
Содержимое документов не обрабатывается и не валидируется — кладётся как есть.
|
Содержимое документов не обрабатывается и не валидируется — кладётся как есть.
|
||||||
|
|
||||||
Модуль намеренно не зависит от config/S3/NATS, чтобы его можно было
|
merge_documents() принимает уже прочитанные объекты и сама ничего не открывает.
|
||||||
использовать и как отдельный скрипт, и из PollWorker.
|
Откуда они взялись — локальная ФС, SMB-шара, S3, http — решает вызывающий:
|
||||||
|
|
||||||
|
merge_documents([json.load(f) for f in handles], file_id, legal_entity_id)
|
||||||
|
|
||||||
|
Поэтому модуль не тянет ни config, ни S3, ни NATS, ни smb-клиент.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@ -18,34 +22,31 @@ import sys
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
def load_document(path) -> object:
|
def merge_documents(documents, file_id, legal_entity_id) -> dict:
|
||||||
"""Читает один json-документ процессора."""
|
"""Собирает распознанные документы в один результат.
|
||||||
with open(path, 'r', encoding='utf-8') as f:
|
|
||||||
return json.load(f)
|
|
||||||
|
|
||||||
|
documents — любой iterable уже разобранных json-объектов.
|
||||||
def merge_documents(paths, file_id, legal_entity_id) -> dict:
|
Порядок элементов в 'files' сохраняется.
|
||||||
"""Собирает документы в один результат.
|
|
||||||
|
|
||||||
Порядок элементов в 'files' — порядок переданных путей.
|
|
||||||
"""
|
"""
|
||||||
paths = list(paths)
|
documents = list(documents)
|
||||||
if not paths:
|
if not documents:
|
||||||
raise ValueError('нечего мержить: не передано ни одного документа')
|
raise ValueError('нечего мержить: не передано ни одного документа')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'file_id': file_id,
|
'file_id': file_id,
|
||||||
'legal_entity_id': legal_entity_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:
|
# --- CLI: один из вызывающих, читает локальную ФС ---------------------------
|
||||||
"""То же, но берёт все *.json из директории, отсортированные по имени."""
|
|
||||||
paths = sorted(Path(job_dir).glob('*.json'))
|
def _read_local(paths) -> list:
|
||||||
if not paths:
|
result = []
|
||||||
raise ValueError(f'в директории нет json-файлов: {job_dir}')
|
for path in paths:
|
||||||
return merge_documents(paths, file_id, legal_entity_id)
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
|
result.append(json.load(f))
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def main(argv=None) -> int:
|
def main(argv=None) -> int:
|
||||||
@ -72,11 +73,14 @@ def main(argv=None) -> int:
|
|||||||
if bool(args.documents) == bool(args.dir):
|
if bool(args.documents) == bool(args.dir):
|
||||||
parser.error('укажи либо пути к документам, либо --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:
|
try:
|
||||||
if args.dir:
|
documents = _read_local(paths)
|
||||||
result = merge_directory(args.dir, args.file_id, args.legal_entity_id)
|
result = merge_documents(documents, args.file_id, args.legal_entity_id)
|
||||||
else:
|
|
||||||
result = merge_documents(args.documents, args.file_id, args.legal_entity_id)
|
|
||||||
except (OSError, ValueError, json.JSONDecodeError) as e:
|
except (OSError, ValueError, json.JSONDecodeError) as e:
|
||||||
print(f'ошибка: {e}', file=sys.stderr)
|
print(f'ошибка: {e}', file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
@ -19,16 +19,8 @@ DOC_B = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def write_doc(directory: Path, name: str, payload: dict) -> Path:
|
def test_merges_two_documents_into_one_result():
|
||||||
path = directory / name
|
result = merge_documents([DOC_A, DOC_B], file_id='file-1', legal_entity_id='le-1')
|
||||||
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')
|
|
||||||
|
|
||||||
assert result == {
|
assert result == {
|
||||||
'file_id': 'file-1',
|
'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}], 'кириллица': 'значение'}
|
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]
|
assert result['files'] == [weird]
|
||||||
|
|
||||||
|
|
||||||
def test_preserves_given_order(tmp_path):
|
def test_preserves_given_order():
|
||||||
# порядок в files[] — порядок переданных путей, а не порядок записи на диск
|
result = merge_documents([DOC_B, DOC_A], file_id='f', legal_entity_id='l')
|
||||||
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')
|
|
||||||
|
|
||||||
assert [doc['file_uid'] for doc in result['files']] == ['uid-b', 'uid-a']
|
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():
|
def test_rejects_empty_input():
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
merge_documents([], file_id='f', legal_entity_id='l')
|
merge_documents([], file_id='f', legal_entity_id='l')
|
||||||
|
|
||||||
|
|
||||||
def test_cli_writes_merged_file(tmp_path):
|
def test_cli_reads_local_files_and_writes_merged(tmp_path):
|
||||||
a = write_doc(tmp_path, 'doc_1.json', DOC_A)
|
# CLI — один из вызывающих: локальное чтение живёт здесь, а не в merge_documents
|
||||||
b = write_doc(tmp_path, 'doc_2.json', DOC_B)
|
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'
|
out = tmp_path / 'merged.json'
|
||||||
|
|
||||||
completed = subprocess.run(
|
completed = subprocess.run(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user