Consolidate duplicate _parse_json helpers from content_editor.py /
design_director.py / kei_client.py (fuller form) and pipeline.py (simple form)
into shared src/json_utils.parse_json (strict superset). All 18 call-sites
preserved via `parse_json as _parse_json` alias import; no behavior change.
- src/json_utils.py (new): shared helper, fenced/plain-fence/bare-brace patterns
+ list-prefix cleanup fallback.
- tests/test_json_utils.py (new): 9 unit tests pinning parser semantics.
- src/content_editor.py / design_director.py: remove local helper +
unused `import json` / `import re`.
- src/kei_client.py / pipeline.py: remove local helper; `json` / `re` retained
(used elsewhere).
Targeted tests 9 passed; full pytest 374 passed (3 pre-existing scripts/
collection errors reproduce on baseline 909bf75, IMP-28 unrelated).
47 lines
1.4 KiB
Python
47 lines
1.4 KiB
Python
"""JSON 추출 공용 유틸리티.
|
|
|
|
Kei / Claude API 응답 텍스트에서 JSON 객체를 추출한다.
|
|
content_editor, design_director, kei_client, pipeline 공통 헬퍼.
|
|
|
|
응답이 마크다운 리스트 접두사("- " / "* ")로 감싸진 경우에도 처리.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
from typing import Any
|
|
|
|
_JSON_PATTERNS: tuple[str, ...] = (
|
|
r"```json\s*(.*?)```",
|
|
r"```\s*(.*?)```",
|
|
r"(\{.*\})",
|
|
)
|
|
|
|
|
|
def parse_json(text: str) -> dict[str, Any] | None:
|
|
"""텍스트에서 JSON을 추출한다.
|
|
|
|
Kei API가 마크다운 리스트 접두사(- )를 붙여 응답하는 경우에도 처리.
|
|
원본 → 리스트 접두사 제거 버전 순서로 fenced JSON / plain fenced / 베어 brace 패턴을
|
|
차례로 시도한다. 모두 실패하면 None.
|
|
"""
|
|
lines = text.split("\n")
|
|
cleaned_lines: list[str] = []
|
|
for line in lines:
|
|
stripped = line.lstrip()
|
|
if stripped.startswith("- ") or stripped.startswith("* "):
|
|
cleaned_lines.append(stripped[2:])
|
|
else:
|
|
cleaned_lines.append(stripped)
|
|
cleaned = "\n".join(cleaned_lines)
|
|
|
|
for target in (text, cleaned):
|
|
for pattern in _JSON_PATTERNS:
|
|
match = re.search(pattern, target, re.DOTALL)
|
|
if match:
|
|
try:
|
|
return json.loads(match.group(1).strip())
|
|
except json.JSONDecodeError:
|
|
continue
|
|
return None
|