document-adapter 0.2.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. {document_adapter-0.2.0 → document_adapter-0.3.0}/PKG-INFO +63 -20
  2. {document_adapter-0.2.0 → document_adapter-0.3.0}/README.md +62 -19
  3. {document_adapter-0.2.0 → document_adapter-0.3.0}/document_adapter/base.py +64 -1
  4. document_adapter-0.3.0/document_adapter/docx_adapter.py +352 -0
  5. document_adapter-0.3.0/document_adapter/hwpx_adapter.py +389 -0
  6. document_adapter-0.3.0/document_adapter/pptx_adapter.py +354 -0
  7. {document_adapter-0.2.0 → document_adapter-0.3.0}/document_adapter/tools.py +106 -17
  8. {document_adapter-0.2.0 → document_adapter-0.3.0}/document_adapter.egg-info/PKG-INFO +63 -20
  9. {document_adapter-0.2.0 → document_adapter-0.3.0}/pyproject.toml +1 -1
  10. {document_adapter-0.2.0 → document_adapter-0.3.0}/tests/test_smoke.py +325 -0
  11. document_adapter-0.2.0/document_adapter/docx_adapter.py +0 -133
  12. document_adapter-0.2.0/document_adapter/hwpx_adapter.py +0 -246
  13. document_adapter-0.2.0/document_adapter/pptx_adapter.py +0 -178
  14. {document_adapter-0.2.0 → document_adapter-0.3.0}/LICENSE +0 -0
  15. {document_adapter-0.2.0 → document_adapter-0.3.0}/document_adapter/__init__.py +0 -0
  16. {document_adapter-0.2.0 → document_adapter-0.3.0}/document_adapter/mcp_server.py +0 -0
  17. {document_adapter-0.2.0 → document_adapter-0.3.0}/document_adapter.egg-info/SOURCES.txt +0 -0
  18. {document_adapter-0.2.0 → document_adapter-0.3.0}/document_adapter.egg-info/dependency_links.txt +0 -0
  19. {document_adapter-0.2.0 → document_adapter-0.3.0}/document_adapter.egg-info/entry_points.txt +0 -0
  20. {document_adapter-0.2.0 → document_adapter-0.3.0}/document_adapter.egg-info/requires.txt +0 -0
  21. {document_adapter-0.2.0 → document_adapter-0.3.0}/document_adapter.egg-info/top_level.txt +0 -0
  22. {document_adapter-0.2.0 → document_adapter-0.3.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: document-adapter
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: LLM-friendly document template editing (DOCX/PPTX/HWPX) with MCP server and Claude API tool-use support
5
5
  Author-email: Son Seongjun <sonsj97@plateer.com>
6
6
  License: MIT
@@ -53,14 +53,15 @@ Dynamic: license-file
53
53
 
54
54
  ## 지원 포맷
55
55
 
56
- | 포맷 | 백엔드 | 템플릿 렌더 | 표 읽기 | 셀 수정 | 행 추가 |
57
- |---|---|---|---|---|---|
58
- | `.docx` | `docxtpl` + `python-docx` | Jinja2 (`{%tr%}` loop 포함) | ✅ | ✅ | ✅ |
59
- | `.pptx` | `python-pptx` | `{{key}}` 치환 | ✅ (슬라이드 위치 포함) | ✅ | ❌ (미지원) |
60
- | `.hwpx` | `python-hwpx` (Pure Python) | `{{key}}` 치환 | ✅ | ✅ | ❌ (미지원) |
56
+ | 포맷 | 백엔드 | 템플릿 렌더 | 표 읽기 | 병합 셀 인지 | 중첩 테이블 | 셀 수정 | 행 추가 |
57
+ |---|---|---|---|---|---|---|---|
58
+ | `.docx` | `docxtpl` + `python-docx` | Jinja2 (`{%tr%}` loop 포함) | ✅ | ✅ | ✅ | ✅ | ✅ |
59
+ | `.pptx` | `python-pptx` | `{{key}}` 치환 | ✅ (슬라이드 위치 포함) | ✅ | — (포맷 미지원) | ✅ | ❌ (미지원) |
60
+ | `.hwpx` | `python-hwpx` (Pure Python) | `{{key}}` 치환 | ✅ | ✅ | ✅ | ✅ | ✅ (v0.3+) |
61
61
 
62
62
  - HWPX는 한컴오피스 설치가 **불필요**합니다 (macOS/Linux 서버에서 그대로 동작).
63
63
  - 구버전 `.hwp`(바이너리 포맷)는 지원하지 않습니다 — `.hwpx`로 변환 후 사용하세요.
64
+ - 병합 셀: 3개 포맷 모두 preview에 `null` 슬롯 + `merges` 메타로 구조 노출. non-anchor 좌표에 쓰기는 `MergedCellWriteError`로 거부.
64
65
 
65
66
  ## 설치
66
67
 
@@ -106,14 +107,50 @@ doc.save("report_filled.docx")
106
107
 
107
108
  # 3. 기존 양식 파일의 표 셀 수정
108
109
  doc = load("checklist.docx")
110
+
111
+ # 빈 셀 값 교체
109
112
  old = doc.set_cell(table_index=1, row=1, col=1, value="○○전자")
110
- doc.append_row(1, ["새 항목", "값"]) # DOCX만 지원
113
+
114
+ # 라벨이 있는 셀 ("성 명")에 값 추가 → "성 명 홍길동"
115
+ doc.append_to_cell(table_index=2, row=0, col=0, value="홍길동")
116
+
117
+ # 셀 전체 텍스트 + 병합 메타 조회 (preview의 40자 잘림 없이)
118
+ cell = doc.get_cell(table_index=1, row=3, col=2)
119
+ print(cell.text, cell.is_anchor, cell.span, cell.nested_table_indices)
120
+
121
+ # DOCX/HWPX는 행 추가 지원
122
+ doc.append_row(1, ["새 항목", "값"])
123
+
111
124
  doc.save("checklist_filled.docx")
112
125
  doc.close()
113
126
  ```
114
127
 
115
128
  확장자로 자동 분기되므로 `.pptx` / `.hwpx`도 동일한 API를 사용합니다.
116
129
 
130
+ ### 병합 셀 인지 동작 (v0.2+)
131
+
132
+ ```python
133
+ schema = doc.get_schema()
134
+ t = schema.tables[0]
135
+
136
+ # preview는 logical grid. 병합된 non-anchor 슬롯은 None.
137
+ # [['HEADER', None, None], ['A1', 'A2', 'A3']]
138
+ print(t.preview)
139
+
140
+ # merges는 span>1x1인 anchor 목록
141
+ # [MergeInfo(anchor=(0,0), span=(1,3))]
142
+ print(t.merges)
143
+
144
+ # non-anchor에 쓰기 시도하면 MergedCellWriteError (ValueError 서브클래스)
145
+ try:
146
+ doc.set_cell(0, 0, 2, "X")
147
+ except ValueError as e:
148
+ print(e) # "cell (0,2) is part of a merged region anchored at (0,0)..."
149
+
150
+ # 의도적으로 앵커로 리디렉트하고 싶다면
151
+ doc.set_cell(0, 0, 2, "X", allow_merge_redirect=True) # 경고 + 실제로 (0,0) 수정
152
+ ```
153
+
117
154
  ## MCP 서버로 사용 — Claude Desktop / Claude Code
118
155
 
119
156
  ### 실행
@@ -177,39 +214,45 @@ resp = client.messages.create(
177
214
 
178
215
  전체 agent loop 예시는 [`examples/claude_api_example.py`](examples/claude_api_example.py) 참고.
179
216
 
180
- ## 노출되는 4개 도구
217
+ ## 노출되는 도구
181
218
 
182
219
  | 도구 | 설명 |
183
220
  |---|---|
184
221
  | `inspect_document` | 문서 구조(placeholders, tables)를 JSON으로 반환. **항상 첫 호출로 사용** |
185
222
  | `render_template` | `{{key}}`를 context dict 값으로 치환해 새 파일 저장 |
186
- | `set_cell` | 특정 표의 `(row, col)` 셀 값 교체 |
187
- | `append_row` | 표 끝에 새 행 추가 (DOCX 전용) |
223
+ | `get_cell` | 셀 전체 텍스트 + 병합/중첩 메타 반환 (preview의 40자 잘림 없이) |
224
+ | `set_cell` | 특정 표의 `(row, col)` 셀 값 교체 (병합 anchor만) |
225
+ | `append_to_cell` | 기존 텍스트 뒤에 값 덧붙임 (라벨 유지용, 예: `"성 명"` → `"성 명 홍길동"`) |
226
+ | `append_row` | 표 끝에 새 행 추가 (DOCX/HWPX 지원) |
188
227
 
189
- ### `inspect_document` 반환 예시
228
+ ### `inspect_document` 반환 예시 (v0.2+)
190
229
 
191
230
  ```json
192
231
  {
193
- "format": "docx",
194
- "source": "/path/to/checklist.docx",
232
+ "format": "hwpx",
233
+ "source": "/path/to/form.hwpx",
195
234
  "placeholders": [],
196
235
  "tables": [
197
236
  {
198
- "index": 1,
199
- "rows": 7,
200
- "cols": 2,
237
+ "index": 0,
238
+ "rows": 28,
239
+ "cols": 16,
201
240
  "location": null,
241
+ "parent_path": null,
202
242
  "preview": [
203
- {"row": 0, "cells": ["항목", "기입 내용"]},
204
- {"row": 1, "cells": ["고객사 / 조직", ""]},
205
- {"row": 2, "cells": ["현업 담당부서 / 책임자", ""]}
243
+ ["포상금 지급신청서", null, null, null, null, null, null, null, null, null, null, null, null, null, null, null],
244
+ ["접수번호", null, null, "", "접수일자", null, null, "", ...]
245
+ ],
246
+ "merges": [
247
+ {"anchor": [0, 0], "span": [1, 16]},
248
+ {"anchor": [1, 0], "span": [1, 3]}
206
249
  ]
207
250
  }
208
251
  ]
209
252
  }
210
253
  ```
211
254
 
212
- LLM은 이 preview를 보고 **"빈 셀이 어디 있는지 / 어떤 값을 넣어야 하는지"** 를 판단하여 `set_cell`을 호출합니다.
255
+ LLM은 이 preview를 보고 **"빈 셀이 어디 있는지 / 어떤 값을 넣어야 하는지"** 를 판단하여 `set_cell` / `append_to_cell`을 호출합니다. `null` 슬롯은 병합된 영역이며 `merges`의 anchor 좌표로만 쓸 수 있습니다.
213
256
 
214
257
  ## 템플릿 작성 규칙
215
258
 
@@ -14,14 +14,15 @@
14
14
 
15
15
  ## 지원 포맷
16
16
 
17
- | 포맷 | 백엔드 | 템플릿 렌더 | 표 읽기 | 셀 수정 | 행 추가 |
18
- |---|---|---|---|---|---|
19
- | `.docx` | `docxtpl` + `python-docx` | Jinja2 (`{%tr%}` loop 포함) | ✅ | ✅ | ✅ |
20
- | `.pptx` | `python-pptx` | `{{key}}` 치환 | ✅ (슬라이드 위치 포함) | ✅ | ❌ (미지원) |
21
- | `.hwpx` | `python-hwpx` (Pure Python) | `{{key}}` 치환 | ✅ | ✅ | ❌ (미지원) |
17
+ | 포맷 | 백엔드 | 템플릿 렌더 | 표 읽기 | 병합 셀 인지 | 중첩 테이블 | 셀 수정 | 행 추가 |
18
+ |---|---|---|---|---|---|---|---|
19
+ | `.docx` | `docxtpl` + `python-docx` | Jinja2 (`{%tr%}` loop 포함) | ✅ | ✅ | ✅ | ✅ | ✅ |
20
+ | `.pptx` | `python-pptx` | `{{key}}` 치환 | ✅ (슬라이드 위치 포함) | ✅ | — (포맷 미지원) | ✅ | ❌ (미지원) |
21
+ | `.hwpx` | `python-hwpx` (Pure Python) | `{{key}}` 치환 | ✅ | ✅ | ✅ | ✅ | ✅ (v0.3+) |
22
22
 
23
23
  - HWPX는 한컴오피스 설치가 **불필요**합니다 (macOS/Linux 서버에서 그대로 동작).
24
24
  - 구버전 `.hwp`(바이너리 포맷)는 지원하지 않습니다 — `.hwpx`로 변환 후 사용하세요.
25
+ - 병합 셀: 3개 포맷 모두 preview에 `null` 슬롯 + `merges` 메타로 구조 노출. non-anchor 좌표에 쓰기는 `MergedCellWriteError`로 거부.
25
26
 
26
27
  ## 설치
27
28
 
@@ -67,14 +68,50 @@ doc.save("report_filled.docx")
67
68
 
68
69
  # 3. 기존 양식 파일의 표 셀 수정
69
70
  doc = load("checklist.docx")
71
+
72
+ # 빈 셀 값 교체
70
73
  old = doc.set_cell(table_index=1, row=1, col=1, value="○○전자")
71
- doc.append_row(1, ["새 항목", "값"]) # DOCX만 지원
74
+
75
+ # 라벨이 있는 셀 ("성 명")에 값 추가 → "성 명 홍길동"
76
+ doc.append_to_cell(table_index=2, row=0, col=0, value="홍길동")
77
+
78
+ # 셀 전체 텍스트 + 병합 메타 조회 (preview의 40자 잘림 없이)
79
+ cell = doc.get_cell(table_index=1, row=3, col=2)
80
+ print(cell.text, cell.is_anchor, cell.span, cell.nested_table_indices)
81
+
82
+ # DOCX/HWPX는 행 추가 지원
83
+ doc.append_row(1, ["새 항목", "값"])
84
+
72
85
  doc.save("checklist_filled.docx")
73
86
  doc.close()
74
87
  ```
75
88
 
76
89
  확장자로 자동 분기되므로 `.pptx` / `.hwpx`도 동일한 API를 사용합니다.
77
90
 
91
+ ### 병합 셀 인지 동작 (v0.2+)
92
+
93
+ ```python
94
+ schema = doc.get_schema()
95
+ t = schema.tables[0]
96
+
97
+ # preview는 logical grid. 병합된 non-anchor 슬롯은 None.
98
+ # [['HEADER', None, None], ['A1', 'A2', 'A3']]
99
+ print(t.preview)
100
+
101
+ # merges는 span>1x1인 anchor 목록
102
+ # [MergeInfo(anchor=(0,0), span=(1,3))]
103
+ print(t.merges)
104
+
105
+ # non-anchor에 쓰기 시도하면 MergedCellWriteError (ValueError 서브클래스)
106
+ try:
107
+ doc.set_cell(0, 0, 2, "X")
108
+ except ValueError as e:
109
+ print(e) # "cell (0,2) is part of a merged region anchored at (0,0)..."
110
+
111
+ # 의도적으로 앵커로 리디렉트하고 싶다면
112
+ doc.set_cell(0, 0, 2, "X", allow_merge_redirect=True) # 경고 + 실제로 (0,0) 수정
113
+ ```
114
+
78
115
  ## MCP 서버로 사용 — Claude Desktop / Claude Code
79
116
 
80
117
  ### 실행
@@ -138,39 +175,45 @@ resp = client.messages.create(
138
175
 
139
176
  전체 agent loop 예시는 [`examples/claude_api_example.py`](examples/claude_api_example.py) 참고.
140
177
 
141
- ## 노출되는 4개 도구
178
+ ## 노출되는 도구
142
179
 
143
180
  | 도구 | 설명 |
144
181
  |---|---|
145
182
  | `inspect_document` | 문서 구조(placeholders, tables)를 JSON으로 반환. **항상 첫 호출로 사용** |
146
183
  | `render_template` | `{{key}}`를 context dict 값으로 치환해 새 파일 저장 |
147
- | `set_cell` | 특정 표의 `(row, col)` 셀 값 교체 |
148
- | `append_row` | 표 끝에 새 행 추가 (DOCX 전용) |
184
+ | `get_cell` | 셀 전체 텍스트 + 병합/중첩 메타 반환 (preview의 40자 잘림 없이) |
185
+ | `set_cell` | 특정 표의 `(row, col)` 셀 값 교체 (병합 anchor만) |
186
+ | `append_to_cell` | 기존 텍스트 뒤에 값 덧붙임 (라벨 유지용, 예: `"성 명"` → `"성 명 홍길동"`) |
187
+ | `append_row` | 표 끝에 새 행 추가 (DOCX/HWPX 지원) |
149
188
 
150
- ### `inspect_document` 반환 예시
189
+ ### `inspect_document` 반환 예시 (v0.2+)
151
190
 
152
191
  ```json
153
192
  {
154
- "format": "docx",
155
- "source": "/path/to/checklist.docx",
193
+ "format": "hwpx",
194
+ "source": "/path/to/form.hwpx",
156
195
  "placeholders": [],
157
196
  "tables": [
158
197
  {
159
- "index": 1,
160
- "rows": 7,
161
- "cols": 2,
198
+ "index": 0,
199
+ "rows": 28,
200
+ "cols": 16,
162
201
  "location": null,
202
+ "parent_path": null,
163
203
  "preview": [
164
- {"row": 0, "cells": ["항목", "기입 내용"]},
165
- {"row": 1, "cells": ["고객사 / 조직", ""]},
166
- {"row": 2, "cells": ["현업 담당부서 / 책임자", ""]}
204
+ ["포상금 지급신청서", null, null, null, null, null, null, null, null, null, null, null, null, null, null, null],
205
+ ["접수번호", null, null, "", "접수일자", null, null, "", ...]
206
+ ],
207
+ "merges": [
208
+ {"anchor": [0, 0], "span": [1, 16]},
209
+ {"anchor": [1, 0], "span": [1, 3]}
167
210
  ]
168
211
  }
169
212
  ]
170
213
  }
171
214
  ```
172
215
 
173
- LLM은 이 preview를 보고 **"빈 셀이 어디 있는지 / 어떤 값을 넣어야 하는지"** 를 판단하여 `set_cell`을 호출합니다.
216
+ LLM은 이 preview를 보고 **"빈 셀이 어디 있는지 / 어떤 값을 넣어야 하는지"** 를 판단하여 `set_cell` / `append_to_cell`을 호출합니다. `null` 슬롯은 병합된 영역이며 `merges`의 anchor 좌표로만 쓸 수 있습니다.
174
217
 
175
218
  ## 템플릿 작성 규칙
176
219
 
@@ -3,7 +3,7 @@
3
3
  세 포맷(DOCX/PPTX/HWPX)의 공통 작업을 추상화:
4
4
  - 템플릿 렌더링 ({{key}} 치환)
5
5
  - 표 스키마 추출 (LLM 입력용)
6
- - 셀 수정 / 행 추가
6
+ - 셀 수정 / 값 추가 / 행 추가
7
7
  """
8
8
  from __future__ import annotations
9
9
 
@@ -13,6 +13,28 @@ from pathlib import Path
13
13
  from typing import Any
14
14
 
15
15
 
16
+ # ---- custom exceptions -----------------------------------------------------
17
+ # 표준 예외를 상속해 기존 ``except ValueError/IndexError`` 흐름과 호환.
18
+
19
+ class MergedCellWriteError(ValueError):
20
+ """병합 영역의 non-anchor 좌표에 쓰기를 시도했을 때."""
21
+
22
+
23
+ class CellOutOfBoundsError(IndexError):
24
+ """(row, col)이 표 경계를 벗어남."""
25
+
26
+
27
+ class TableIndexError(IndexError):
28
+ """table_index로 표를 찾지 못함."""
29
+
30
+
31
+ class NotImplementedForFormat(NotImplementedError):
32
+ """특정 포맷이 지원하지 않는 연산."""
33
+
34
+
35
+ # ---- dataclasses -----------------------------------------------------------
36
+
37
+
16
38
  @dataclass
17
39
  class MergeInfo:
18
40
  """병합 셀 정보. anchor=(row,col)에서 span=(rows,cols)만큼 병합."""
@@ -51,6 +73,34 @@ class TableSchema:
51
73
  }
52
74
 
53
75
 
76
+ @dataclass
77
+ class CellContent:
78
+ """단일 셀의 전체 내용 + 병합/중첩 메타.
79
+
80
+ 프리뷰가 max_cell_len으로 잘리는 것과 달리 ``text``는 전체 본문을 보유.
81
+ """
82
+ row: int
83
+ col: int
84
+ text: str # 셀 전체 평문 (자르지 않음)
85
+ paragraphs: list[str] # paragraph 단위 분리
86
+ is_anchor: bool # (row,col)이 병합 앵커인지
87
+ anchor: tuple[int, int] # 이 logical 위치의 앵커 좌표
88
+ span: tuple[int, int] # (row_span, col_span)
89
+ nested_table_indices: list[int] = field(default_factory=list)
90
+
91
+ def to_dict(self) -> dict[str, Any]:
92
+ return {
93
+ "row": self.row,
94
+ "col": self.col,
95
+ "text": self.text,
96
+ "paragraphs": self.paragraphs,
97
+ "is_anchor": self.is_anchor,
98
+ "anchor": list(self.anchor),
99
+ "span": list(self.span),
100
+ "nested_table_indices": self.nested_table_indices,
101
+ }
102
+
103
+
54
104
  @dataclass
55
105
  class DocumentSchema:
56
106
  """문서 전체 스키마."""
@@ -98,6 +148,10 @@ class DocumentAdapter(ABC):
98
148
  preview_rows: int = 4, max_cell_len: int = 40) -> list[TableSchema]:
99
149
  """필터 조건을 만족하는 표 스키마 목록."""
100
150
 
151
+ @abstractmethod
152
+ def get_cell(self, table_index: int, row: int, col: int) -> CellContent:
153
+ """셀 단건 조회. preview로 잘린 전체 텍스트와 병합/중첩 메타 반환."""
154
+
101
155
  def get_schema(self) -> DocumentSchema:
102
156
  return DocumentSchema(
103
157
  format=self.format,
@@ -115,6 +169,15 @@ class DocumentAdapter(ABC):
115
169
  def set_cell(self, table_index: int, row: int, col: int, value: str) -> str:
116
170
  """셀 값 교체. 원래 값 반환."""
117
171
 
172
+ @abstractmethod
173
+ def append_to_cell(self, table_index: int, row: int, col: int, value: str,
174
+ separator: str = " ") -> str:
175
+ """기존 셀 텍스트 뒤에 ``separator + value``를 덧붙임.
176
+
177
+ 라벨(예: "성 명")을 유지한 채 사용자 입력을 추가하는 용도.
178
+ 빈 셀이면 separator 없이 value만 기록. 원래 값 반환.
179
+ """
180
+
118
181
  @abstractmethod
119
182
  def append_row(self, table_index: int, values: list[str]) -> None:
120
183
  """표 끝에 새 행 추가."""