Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `newsdom_api.dom_builder._html_safe_text` 함수에 early return과 타입 체크를 도입하여 불필요한 `str()` 캐스팅을 제거함으로써 처리 속도를 개선했습니다.

### Added
- [CLI] 주어진 NewsDOM JSON 파일들이 스키마(ParseResponse) 규칙에 부합하는지 검사하는 `tools/validate_dom.py` 도구를 추가했습니다.
- [CLI] NewsDOM JSON 결과물에서 Markdown 등 부가적 포맷팅 없이 순수 텍스트(Plain Text)만을 추출하는 `tools/extract_text.py` 도구를 추가했습니다.
- [CLI] 파싱된 NewsDOM JSON을 Markdown 포맷으로 변환하는 `tools/export_markdown.py` 도구를 추가했습니다.
- [CLI] `tools/batch_parse_pdf.py`에 하위 디렉터리의 PDF를 일괄 처리하고 상대 경로로 JSON을 저장하는 `--recursive` 옵션을 추가했습니다.
- OpenAPI 문서에 contact 및 MIT license metadata를 추가하여 API 소비자가 maintainer와 라이선스 정보를 더 쉽게 확인할 수 있도록 개선
Expand Down
4 changes: 1 addition & 3 deletions src/newsdom_api/synthetic.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,7 @@ def _safe_draw_text(
draw.text(xy, text, fill=fill, font=font)
except UnicodeEncodeError:
# Fallback for ImageFont.load_default() which only supports latin-1
fallback_text = "".join(
c if ord(c) < 256 else "?" for c in text
)
fallback_text = "".join(c if ord(c) < 256 else "?" for c in text)
draw.text(xy, fallback_text, fill=fill, font=font)


Expand Down
8 changes: 2 additions & 6 deletions tests/test_benchmark_ocr.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,7 @@ def test_benchmark_ocr_harness_json(mock_pdf_dir: Path, tmp_path: Path) -> None:
assert results["summary"]["total_runs"] == 6


def test_benchmark_ocr_recursive_and_csv(
mock_pdf_dir: Path, tmp_path: Path
) -> None:
def test_benchmark_ocr_recursive_and_csv(mock_pdf_dir: Path, tmp_path: Path) -> None:
output_path = tmp_path / "results.csv"

mock_engine = MagicMock(return_value={"status": "success", "page_count": 2})
Expand Down Expand Up @@ -118,9 +116,7 @@ def test_benchmark_ocr_no_pdfs(tmp_path: Path) -> None:
)


def test_benchmark_ocr_unknown_engine(
mock_pdf_dir: Path, tmp_path: Path
) -> None:
def test_benchmark_ocr_unknown_engine(mock_pdf_dir: Path, tmp_path: Path) -> None:
"""If an unknown engine is specified, ValueError is raised."""
with pytest.raises(ValueError, match="Unknown engine: fake_engine"):
benchmark_ocr.main(
Expand Down
16 changes: 4 additions & 12 deletions tests/test_derive_private_baseline.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,7 @@ def test_derive_baseline_no_pdfs(tmp_path: Path) -> None:


@patch("tools.derive_private_baseline.parse_pdf_bytes")
def test_derive_baseline_http_exception(
mock_parse_pdf_bytes, tmp_path: Path
) -> None:
def test_derive_baseline_http_exception(mock_parse_pdf_bytes, tmp_path: Path) -> None:
"""HTTPException from parse_pdf_bytes should be wrapped in RuntimeError."""
fixtures_dir = tmp_path / "fixtures"
fixtures_dir.mkdir()
Expand All @@ -153,9 +151,7 @@ def test_main_success(mock_derive_baseline, tmp_path: Path) -> None:
["--private-fixtures-dir", str(fixtures_dir), str(output_path)]
)

mock_derive_baseline.assert_called_once_with(
fixtures_dir, output_path, False, True
)
mock_derive_baseline.assert_called_once_with(fixtures_dir, output_path, False, True)


@patch("tools.derive_private_baseline.derive_baseline")
Expand All @@ -173,9 +169,7 @@ def test_main_success_with_options(mock_derive_baseline, tmp_path: Path) -> None
]
)

mock_derive_baseline.assert_called_once_with(
fixtures_dir, output_path, True, False
)
mock_derive_baseline.assert_called_once_with(fixtures_dir, output_path, True, False)


@patch("tools.derive_private_baseline.derive_baseline")
Expand All @@ -199,9 +193,7 @@ def test_main_runtime_error(mock_derive_baseline, tmp_path: Path, capsys) -> Non


@patch("tools.derive_private_baseline.derive_baseline")
def test_main_unexpected_error(
mock_derive_baseline, tmp_path: Path, capsys
) -> None:
def test_main_unexpected_error(mock_derive_baseline, tmp_path: Path, capsys) -> None:
"""main() should exit(1) and print an unexpected error."""
fixtures_dir = tmp_path / "fixtures"
output_path = tmp_path / "baseline.json"
Expand Down
5 changes: 4 additions & 1 deletion tests/test_errors.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from newsdom_api.errors import MineruIncompleteOutputError, MineruRuntimeUnavailableError
from newsdom_api.errors import (
MineruIncompleteOutputError,
MineruRuntimeUnavailableError,
)


def test_mineru_runtime_unavailable_error_initialization():
Expand Down
5 changes: 4 additions & 1 deletion tests/test_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,8 @@ def test_page_node_openapi_schema_descriptions():
schema = PageNode.model_json_schema()
properties = schema["properties"]

assert properties["page_number"]["description"] == "One-based page number from the parsed PDF."
assert (
properties["page_number"]["description"]
== "One-based page number from the parsed PDF."
)
assert properties["articles"]["description"] == "Articles extracted from this page."
4 changes: 3 additions & 1 deletion tests/test_tools_batch_parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ def test_batch_parse_success(mock_parse, mock_pdf_dir, tmp_path, capsys):


@patch("tools.batch_parse_pdf.parse_pdf_bytes")
def test_batch_parse_recursive_preserves_relative_paths(mock_parse, mock_pdf_dir, tmp_path):
def test_batch_parse_recursive_preserves_relative_paths(
mock_parse, mock_pdf_dir, tmp_path
):
nested_dir = mock_pdf_dir / "section"
nested_dir.mkdir()
(nested_dir / "doc3.pdf").write_bytes(b"content3")
Expand Down
130 changes: 130 additions & 0 deletions tests/test_tools_extract_text.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
from __future__ import annotations

import json

import pytest
from tools import extract_text


@pytest.fixture
def mock_json_data() -> dict:
return {
"document_id": "test_doc",
"pages": [
{
"page_number": 1,
"headers": ["Header 1", "Header 2"],
"articles": [
{
"article_id": "art_1",
"headline": "Headline 1",
"body_blocks": ["Body paragraph 1.", "Body paragraph 2."],
"images": [
{
"path": "img1.jpg",
"captions": [{"text": "Image Caption 1"}],
"footnotes": [{"text": "Image Footnote 1"}],
}
],
"captions": [{"text": "Article Caption 1"}],
"footnotes": [{"text": "Article Footnote 1"}],
}
],
"ads": ["Ad text 1"],
"footers": ["Footer 1"],
},
{
"page_number": 2,
"articles": [{"headline": "", "body_blocks": ["", "Only this block."]}],
},
"invalid_page_type",
],
}


def test_extract_plain_text(mock_json_data):
result = extract_text.extract_plain_text(mock_json_data)

assert "Header 1" in result
assert "Header 2" in result
assert "Headline 1" in result
assert "Body paragraph 1." in result
assert "Body paragraph 2." in result
assert "Image Caption 1" in result
assert "Image Footnote 1" in result
assert "Article Caption 1" in result
assert "Article Footnote 1" in result
assert "Ad text 1" in result
assert "Footer 1" in result
assert "Only this block." in result
assert "invalid_page_type" not in result

# Check that double newlines exist
assert "Header 1\n\nHeader 2" in result


def test_extract_plain_text_empty():
assert extract_text.extract_plain_text({}) == ""


def test_main_stdout(tmp_path, mock_json_data, capsys):
json_path = tmp_path / "test.json"
json_path.write_text(json.dumps(mock_json_data), encoding="utf-8")

extract_text.main([str(json_path)])
out = capsys.readouterr().out
assert "Headline 1" in out
assert "Body paragraph 1." in out


def test_main_file_output(tmp_path, mock_json_data, capsys):
json_path = tmp_path / "test.json"
json_path.write_text(json.dumps(mock_json_data), encoding="utf-8")
out_path = tmp_path / "out.txt"

extract_text.main([str(json_path), "-o", str(out_path)])

assert out_path.exists()
content = out_path.read_text(encoding="utf-8")
assert "Headline 1" in content
assert "Body paragraph 1." in content

out = capsys.readouterr().out
assert "Text extracted and written to" in out


def test_main_error(tmp_path, capsys):
missing_path = tmp_path / "missing.json"
with pytest.raises(SystemExit) as e:
extract_text.main([str(missing_path)])

assert e.value.code == 1
assert "Error extracting text:" in capsys.readouterr().err


def test_extract_plain_text_branches():
from tools.extract_text import extract_plain_text

# Test dictionary bypass branches inside loops
data = {
"pages": [
"not a dict",
{
"articles": [
"not a dict",
{
"images": ["not a dict", {"captions": [{}], "footnotes": [{}]}],
"captions": [{}],
"footnotes": [{}],
},
]
},
]
}
assert extract_plain_text(data) == ""


def test_caption_text_string():
from tools.extract_text import _caption_text

assert _caption_text("string caption") == "string caption"
Loading
Loading