feat: キュー処理メインループ(run_queue)を追加
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
23e558fe1b
commit
0a0bb057e2
@ -69,6 +69,19 @@ def check_external_tools(tesseract_exe: Path, poppler_exe: Path) -> list[str]:
|
|||||||
return missing
|
return missing
|
||||||
|
|
||||||
|
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from file_ops import copy_with_unique_name, move_with_unique_name, wait_until_stable
|
||||||
|
from logger import append_log
|
||||||
|
|
||||||
|
|
||||||
|
def key_pressed() -> bool:
|
||||||
|
import msvcrt
|
||||||
|
|
||||||
|
return msvcrt.kbhit()
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class FieldBox:
|
class FieldBox:
|
||||||
label: str
|
label: str
|
||||||
@ -160,6 +173,64 @@ def process_pdf(pdf_path: Path, boxes: list[FieldBox], margin: float) -> tuple[s
|
|||||||
return name_text, date_text
|
return name_text, date_text
|
||||||
|
|
||||||
|
|
||||||
|
def run_queue(
|
||||||
|
scan_dir: Path,
|
||||||
|
output_dir: Path,
|
||||||
|
success_dir: Path,
|
||||||
|
failed_dir: Path,
|
||||||
|
log_dir: Path,
|
||||||
|
boxes: list[FieldBox],
|
||||||
|
margin: float,
|
||||||
|
stable_wait_sec: float,
|
||||||
|
stable_retries: int,
|
||||||
|
process_pdf_func=process_pdf,
|
||||||
|
key_check=key_pressed,
|
||||||
|
sleep_func=time.sleep,
|
||||||
|
) -> None:
|
||||||
|
while True:
|
||||||
|
queue = sorted(scan_dir.glob("*.pdf"))
|
||||||
|
if not queue:
|
||||||
|
break
|
||||||
|
|
||||||
|
pdf_path = queue[0]
|
||||||
|
|
||||||
|
if key_check():
|
||||||
|
append_log(
|
||||||
|
log_dir, "エラー", pdf_path.name, None, None,
|
||||||
|
"ユーザー操作により停止しました",
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
|
if not pdf_path.exists():
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not wait_until_stable(pdf_path, stable_wait_sec, stable_retries, sleep_func):
|
||||||
|
append_log(
|
||||||
|
log_dir, "スキップ", pdf_path.name, None, None,
|
||||||
|
"サイズ不安定のためスキップ",
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
name, date = process_pdf_func(pdf_path, boxes, margin)
|
||||||
|
except Exception as e:
|
||||||
|
append_log(
|
||||||
|
log_dir, "エラー", pdf_path.name, None, None,
|
||||||
|
f"{type(e).__name__}: {e}\n{traceback.format_exc()}",
|
||||||
|
)
|
||||||
|
move_with_unique_name(pdf_path, failed_dir, pdf_path.stem, pdf_path.suffix)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not name or not date:
|
||||||
|
append_log(log_dir, "失敗", pdf_path.name, name, date, "")
|
||||||
|
move_with_unique_name(pdf_path, failed_dir, pdf_path.stem, pdf_path.suffix)
|
||||||
|
continue
|
||||||
|
|
||||||
|
copy_with_unique_name(pdf_path, output_dir / name, f"{name}_{date}", ".pdf")
|
||||||
|
move_with_unique_name(pdf_path, success_dir, pdf_path.stem, pdf_path.suffix)
|
||||||
|
append_log(log_dir, "成功", pdf_path.name, name, date, "")
|
||||||
|
|
||||||
|
|
||||||
def safe_move(src: Path, dest_dir: Path, filename: str) -> Path:
|
def safe_move(src: Path, dest_dir: Path, filename: str) -> Path:
|
||||||
dest_dir.mkdir(parents=True, exist_ok=True)
|
dest_dir.mkdir(parents=True, exist_ok=True)
|
||||||
dest_path = dest_dir / filename
|
dest_path = dest_dir / filename
|
||||||
|
|||||||
@ -3,6 +3,7 @@ from pathlib import Path
|
|||||||
from extract_and_rename import (
|
from extract_and_rename import (
|
||||||
check_dependencies,
|
check_dependencies,
|
||||||
check_external_tools,
|
check_external_tools,
|
||||||
|
run_queue,
|
||||||
select_template_file,
|
select_template_file,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -56,3 +57,162 @@ def test_check_external_tools_all_present(tmp_path):
|
|||||||
result = check_external_tools(tesseract, poppler)
|
result = check_external_tools(tesseract, poppler)
|
||||||
|
|
||||||
assert result == []
|
assert result == []
|
||||||
|
|
||||||
|
|
||||||
|
def _make_dirs(tmp_path):
|
||||||
|
dirs = {
|
||||||
|
"scan": tmp_path / "スキャン",
|
||||||
|
"output": tmp_path / "アウトプット",
|
||||||
|
"success": tmp_path / "成功",
|
||||||
|
"failed": tmp_path / "失敗",
|
||||||
|
"log": tmp_path / "ログ",
|
||||||
|
}
|
||||||
|
dirs["scan"].mkdir()
|
||||||
|
return dirs
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_queue_success_moves_and_copies(tmp_path):
|
||||||
|
dirs = _make_dirs(tmp_path)
|
||||||
|
pdf = dirs["scan"] / "scan001.pdf"
|
||||||
|
pdf.write_bytes(b"dummy")
|
||||||
|
|
||||||
|
def fake_process_pdf(path, boxes, margin):
|
||||||
|
return ("氏名A", "20260802")
|
||||||
|
|
||||||
|
run_queue(
|
||||||
|
dirs["scan"], dirs["output"], dirs["success"], dirs["failed"], dirs["log"],
|
||||||
|
boxes=[], margin=0.1, stable_wait_sec=0, stable_retries=1,
|
||||||
|
process_pdf_func=fake_process_pdf,
|
||||||
|
key_check=lambda: False,
|
||||||
|
sleep_func=lambda s: None,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert (dirs["output"] / "氏名A" / "氏名A_20260802.pdf").exists()
|
||||||
|
assert (dirs["success"] / "scan001.pdf").exists()
|
||||||
|
assert not pdf.exists()
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
log_content = (dirs["log"] / f"{datetime.now().strftime('%Y-%m-%d')}.log").read_text(encoding="utf-8-sig")
|
||||||
|
assert "結果=成功" in log_content
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_queue_missing_name_moves_to_failed(tmp_path):
|
||||||
|
dirs = _make_dirs(tmp_path)
|
||||||
|
pdf = dirs["scan"] / "scan002.pdf"
|
||||||
|
pdf.write_bytes(b"dummy")
|
||||||
|
|
||||||
|
def fake_process_pdf(path, boxes, margin):
|
||||||
|
return (None, "20260802")
|
||||||
|
|
||||||
|
run_queue(
|
||||||
|
dirs["scan"], dirs["output"], dirs["success"], dirs["failed"], dirs["log"],
|
||||||
|
boxes=[], margin=0.1, stable_wait_sec=0, stable_retries=1,
|
||||||
|
process_pdf_func=fake_process_pdf,
|
||||||
|
key_check=lambda: False,
|
||||||
|
sleep_func=lambda s: None,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert (dirs["failed"] / "scan002.pdf").exists()
|
||||||
|
assert not pdf.exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_queue_exception_moves_to_failed(tmp_path):
|
||||||
|
dirs = _make_dirs(tmp_path)
|
||||||
|
pdf = dirs["scan"] / "scan003.pdf"
|
||||||
|
pdf.write_bytes(b"dummy")
|
||||||
|
|
||||||
|
def fake_process_pdf(path, boxes, margin):
|
||||||
|
raise RuntimeError("OCRエラー")
|
||||||
|
|
||||||
|
run_queue(
|
||||||
|
dirs["scan"], dirs["output"], dirs["success"], dirs["failed"], dirs["log"],
|
||||||
|
boxes=[], margin=0.1, stable_wait_sec=0, stable_retries=1,
|
||||||
|
process_pdf_func=fake_process_pdf,
|
||||||
|
key_check=lambda: False,
|
||||||
|
sleep_func=lambda s: None,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert (dirs["failed"] / "scan003.pdf").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_queue_stops_on_key_press(tmp_path):
|
||||||
|
dirs = _make_dirs(tmp_path)
|
||||||
|
pdf = dirs["scan"] / "scan004.pdf"
|
||||||
|
pdf.write_bytes(b"dummy")
|
||||||
|
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def fake_process_pdf(path, boxes, margin):
|
||||||
|
calls.append(path)
|
||||||
|
return ("氏名A", "20260802")
|
||||||
|
|
||||||
|
run_queue(
|
||||||
|
dirs["scan"], dirs["output"], dirs["success"], dirs["failed"], dirs["log"],
|
||||||
|
boxes=[], margin=0.1, stable_wait_sec=0, stable_retries=1,
|
||||||
|
process_pdf_func=fake_process_pdf,
|
||||||
|
key_check=lambda: True, # 常に押下扱い
|
||||||
|
sleep_func=lambda s: None,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert calls == []
|
||||||
|
assert pdf.exists() # 処理されず残る
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_queue_skips_file_deleted_before_processing(tmp_path):
|
||||||
|
dirs = _make_dirs(tmp_path)
|
||||||
|
pdf = dirs["scan"] / "scan005.pdf"
|
||||||
|
pdf.write_bytes(b"dummy")
|
||||||
|
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def fake_process_pdf(path, boxes, margin):
|
||||||
|
calls.append(path)
|
||||||
|
return ("氏名A", "20260802")
|
||||||
|
|
||||||
|
def key_check_and_delete():
|
||||||
|
if pdf.exists():
|
||||||
|
pdf.unlink()
|
||||||
|
return False
|
||||||
|
|
||||||
|
run_queue(
|
||||||
|
dirs["scan"], dirs["output"], dirs["success"], dirs["failed"], dirs["log"],
|
||||||
|
boxes=[], margin=0.1, stable_wait_sec=0, stable_retries=1,
|
||||||
|
process_pdf_func=fake_process_pdf,
|
||||||
|
key_check=key_check_and_delete,
|
||||||
|
sleep_func=lambda s: None,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert calls == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_queue_empty_scan_dir_returns_immediately(tmp_path):
|
||||||
|
dirs = _make_dirs(tmp_path)
|
||||||
|
|
||||||
|
run_queue(
|
||||||
|
dirs["scan"], dirs["output"], dirs["success"], dirs["failed"], dirs["log"],
|
||||||
|
boxes=[], margin=0.1, stable_wait_sec=0, stable_retries=1,
|
||||||
|
process_pdf_func=lambda p, b, m: ("氏名A", "20260802"),
|
||||||
|
key_check=lambda: False,
|
||||||
|
sleep_func=lambda s: None,
|
||||||
|
)
|
||||||
|
# 例外が出ずに戻ることのみ確認
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_queue_name_collision_gets_suffix(tmp_path):
|
||||||
|
dirs = _make_dirs(tmp_path)
|
||||||
|
existing_output_dir = dirs["output"] / "氏名A"
|
||||||
|
existing_output_dir.mkdir(parents=True)
|
||||||
|
(existing_output_dir / "氏名A_20260802.pdf").write_bytes(b"old")
|
||||||
|
|
||||||
|
pdf = dirs["scan"] / "scan006.pdf"
|
||||||
|
pdf.write_bytes(b"dummy")
|
||||||
|
|
||||||
|
run_queue(
|
||||||
|
dirs["scan"], dirs["output"], dirs["success"], dirs["failed"], dirs["log"],
|
||||||
|
boxes=[], margin=0.1, stable_wait_sec=0, stable_retries=1,
|
||||||
|
process_pdf_func=lambda p, b, m: ("氏名A", "20260802"),
|
||||||
|
key_check=lambda: False,
|
||||||
|
sleep_func=lambda s: None,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert (existing_output_dir / "氏名A_20260802(2).pdf").exists()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user