Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- `tools/export_jsonl.py` 도구를 추가하여 NewsDOM JSON에서 기사 단위 JSONL 포맷으로 내보내는 기능 지원.

> **Planned 0.3.0 deployment migration:** parser authentication changes from
> **default-open** to **default-required**. Production must configure
> `NEWSDOM_AUTH_MODE=required`, `NEWSDOM_RUNTIME_PROFILE=production`, and
Expand Down
117 changes: 117 additions & 0 deletions tests/test_tools_export_jsonl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
from __future__ import annotations

import json
from pathlib import Path

import pytest

from tools.export_jsonl import export_jsonl, main

VALID_JSON_DATA = {
"document_id": "test_doc",
"pages": [
{
"page_number": 1,
"articles": [
{
"article_id": "art_1",
"headline": "Test Headline 1",
"body_blocks": ["Block 1", "Block 2"],
},
{
"article_id": "art_2",
"headline": "Test Headline 2",
"body_blocks": [],
},
],
},
"not_a_dict_page",
{
"page_number": 2,
"articles": [
"not_a_dict_article",
{
"article_id": "art_3",
"headline": "Test Headline 3",
"body_blocks": ["Block 3"],
},
],
},
],
}


def test_export_jsonl_success(tmp_path: Path) -> None:
input_file = tmp_path / "input.json"
input_file.write_text(json.dumps(VALID_JSON_DATA), encoding="utf-8")
output_file = tmp_path / "output.jsonl"

export_jsonl(input_file, output_file)

assert output_file.exists()

lines = output_file.read_text(encoding="utf-8").strip().split("\n")
assert len(lines) == 3

art1 = json.loads(lines[0])
assert art1["document_id"] == "test_doc"
assert art1["page_number"] == 1
assert art1["article_id"] == "art_1"
assert art1["headline"] == "Test Headline 1"
assert art1["body_blocks"] == ["Block 1", "Block 2"]

art2 = json.loads(lines[1])
assert art2["article_id"] == "art_2"
assert art2["headline"] == "Test Headline 2"
assert art2["body_blocks"] == []

art3 = json.loads(lines[2])
assert art3["page_number"] == 2
assert art3["article_id"] == "art_3"
assert art3["headline"] == "Test Headline 3"
assert art3["body_blocks"] == ["Block 3"]


def test_export_jsonl_invalid_file(tmp_path: Path) -> None:
output_file = tmp_path / "output.jsonl"

non_existent = tmp_path / "not_exist.json"
with pytest.raises(FileNotFoundError, match="File not found"):
export_jsonl(non_existent, output_file)

not_json = tmp_path / "input.txt"
not_json.write_text("plain text", encoding="utf-8")
with pytest.raises(ValueError, match="must be a .json file"):
export_jsonl(not_json, output_file)

invalid_json = tmp_path / "invalid.json"
invalid_json.write_text("{invalid_json:", encoding="utf-8")
with pytest.raises(ValueError, match="Invalid JSON file"):
export_jsonl(invalid_json, output_file)


def test_export_jsonl_cli_success(tmp_path: Path, capsys: pytest.CaptureFixture) -> None:
input_file = tmp_path / "input.json"
input_file.write_text(json.dumps(VALID_JSON_DATA), encoding="utf-8")
output_file = tmp_path / "output.jsonl"

main([str(input_file), str(output_file)])

assert output_file.exists()
captured = capsys.readouterr()
assert "JSONL successfully written" in captured.out


def test_export_jsonl_cli_invalid_file(
tmp_path: Path, capsys: pytest.CaptureFixture
) -> None:
not_json = tmp_path / "input.txt"
not_json.write_text("plain text", encoding="utf-8")
output_file = tmp_path / "output.jsonl"

with pytest.raises(SystemExit) as exc_info:
main([str(not_json), str(output_file)])

assert exc_info.value.code == 1
captured = capsys.readouterr()
assert "Error exporting JSONL:" in captured.err
64 changes: 64 additions & 0 deletions tools/export_jsonl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path


def export_jsonl(json_path: Path, output_path: Path) -> None:
"""Export NewsDOM JSON to a JSONL file where each line is an article."""
if not json_path.is_file():
raise FileNotFoundError(f"File not found or is not a file: {json_path}")
if json_path.suffix.lower() != ".json":
raise ValueError("Input file must be a .json file.")

try:
data = json.loads(json_path.read_text(encoding="utf-8"))
except json.JSONDecodeError as exc:
raise ValueError(f"Invalid JSON file: {exc}") from exc

pages = data.get("pages", [])

with output_path.open("w", encoding="utf-8") as jsonlfile:
document_id = data.get("document_id", "Unknown Document")

for page in pages:
if not isinstance(page, dict):
continue
page_number = page.get("page_number", "Unknown")

articles = page.get("articles", [])
for article in articles:
if not isinstance(article, dict):
continue

article_data = {
"document_id": document_id,
"page_number": page_number,
"article_id": article.get("article_id", "Unknown Article ID"),
"headline": article.get("headline", ""),
"body_blocks": article.get("body_blocks", []),
}

jsonlfile.write(json.dumps(article_data, ensure_ascii=False) + "\n")


def main(argv: list[str] | None = None) -> None:
"""Run the JSON-to-JSONL export CLI."""
parser = argparse.ArgumentParser(description="Export a NewsDOM JSON file to JSONL.")
parser.add_argument("input", type=Path, help="Path to the input JSON file.")
parser.add_argument("output", type=Path, help="Path to write the JSONL output file.")

args = parser.parse_args(argv)

try:
export_jsonl(args.input, args.output)
print(f"JSONL successfully written to {args.output}")
except Exception as exc:
print(f"Error exporting JSONL: {exc}", file=sys.stderr)
sys.exit(1)


if __name__ == "__main__": # pragma: no cover
main()
8 changes: 4 additions & 4 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading