python-hwpx-automation 6.0.3__py3-none-any.whl
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.
- hwpx_automation/__init__.py +61 -0
- hwpx_automation/__init__.pyi +27 -0
- hwpx_automation/__main__.py +8 -0
- hwpx_automation/agent_document.py +392 -0
- hwpx_automation/api.py +136 -0
- hwpx_automation/blind_eval.py +407 -0
- hwpx_automation/capabilities.py +110 -0
- hwpx_automation/compat.py +48 -0
- hwpx_automation/configuration.py +60 -0
- hwpx_automation/core/__init__.py +2 -0
- hwpx_automation/core/content.py +762 -0
- hwpx_automation/core/context.py +111 -0
- hwpx_automation/core/diff.py +53 -0
- hwpx_automation/core/document.py +37 -0
- hwpx_automation/core/formatting.py +513 -0
- hwpx_automation/core/handles.py +24 -0
- hwpx_automation/core/locations.py +205 -0
- hwpx_automation/core/locator.py +162 -0
- hwpx_automation/core/plan.py +680 -0
- hwpx_automation/core/resources.py +42 -0
- hwpx_automation/core/search.py +296 -0
- hwpx_automation/core/transactions.py +434 -0
- hwpx_automation/core/txn.py +48 -0
- hwpx_automation/document_state.py +95 -0
- hwpx_automation/errors.py +174 -0
- hwpx_automation/execution_lock.py +15 -0
- hwpx_automation/fastmcp_adapter.py +672 -0
- hwpx_automation/form_fill.py +1177 -0
- hwpx_automation/form_output_models.py +223 -0
- hwpx_automation/handlers/__init__.py +2 -0
- hwpx_automation/handlers/_shared.py +377 -0
- hwpx_automation/handlers/agent_document.py +257 -0
- hwpx_automation/handlers/authoring.py +750 -0
- hwpx_automation/handlers/content_edit.py +1078 -0
- hwpx_automation/handlers/form_fill.py +607 -0
- hwpx_automation/handlers/layout_style.py +660 -0
- hwpx_automation/handlers/quality_render.py +566 -0
- hwpx_automation/handlers/read_export.py +1295 -0
- hwpx_automation/handlers/specialized.py +624 -0
- hwpx_automation/handlers/tracked_changes.py +589 -0
- hwpx_automation/handlers/workflow.py +105 -0
- hwpx_automation/hwp_converter.py +227 -0
- hwpx_automation/hwp_support.py +94 -0
- hwpx_automation/hwpx_ops.py +1439 -0
- hwpx_automation/identity.json +263 -0
- hwpx_automation/identity.py +18 -0
- hwpx_automation/ingest_adapters.py +85 -0
- hwpx_automation/markdown_plan.py +216 -0
- hwpx_automation/mcp_cli.py +29 -0
- hwpx_automation/metadata/tools_meta.py +40 -0
- hwpx_automation/mixed_form.py +3007 -0
- hwpx_automation/mutation_models.py +401 -0
- hwpx_automation/network_policy.py +232 -0
- hwpx_automation/office/__init__.py +14 -0
- hwpx_automation/office/agent/__init__.py +125 -0
- hwpx_automation/office/agent/_batch_verification.py +383 -0
- hwpx_automation/office/agent/blueprint/__init__.py +58 -0
- hwpx_automation/office/agent/blueprint/bundle.py +282 -0
- hwpx_automation/office/agent/blueprint/catalog.py +136 -0
- hwpx_automation/office/agent/blueprint/dump.py +520 -0
- hwpx_automation/office/agent/blueprint/mapping.py +312 -0
- hwpx_automation/office/agent/blueprint/model.py +722 -0
- hwpx_automation/office/agent/blueprint/native.py +621 -0
- hwpx_automation/office/agent/blueprint/replay.py +622 -0
- hwpx_automation/office/agent/catalog.py +252 -0
- hwpx_automation/office/agent/cli.py +647 -0
- hwpx_automation/office/agent/commands.py +1383 -0
- hwpx_automation/office/agent/document.py +801 -0
- hwpx_automation/office/agent/form_plan.py +1760 -0
- hwpx_automation/office/agent/model.py +808 -0
- hwpx_automation/office/agent/path.py +155 -0
- hwpx_automation/office/agent/query.py +230 -0
- hwpx_automation/office/agent/story.py +207 -0
- hwpx_automation/office/authoring/__init__.py +3542 -0
- hwpx_automation/office/authoring/advanced_generators.py +154 -0
- hwpx_automation/office/authoring/builder/__init__.py +52 -0
- hwpx_automation/office/authoring/builder/core.py +996 -0
- hwpx_automation/office/authoring/builder/report.py +195 -0
- hwpx_automation/office/authoring/design/__init__.py +30 -0
- hwpx_automation/office/authoring/design/_support.py +144 -0
- hwpx_automation/office/authoring/design/composer.py +282 -0
- hwpx_automation/office/authoring/design/harvest.py +305 -0
- hwpx_automation/office/authoring/design/plan.py +69 -0
- hwpx_automation/office/authoring/design/profile.py +88 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/info_table.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/application_form/profile.json +25 -0
- hwpx_automation/office/authoring/design/profiles/application_form/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/profile.json +24 -0
- hwpx_automation/office/authoring/design/profiles/home_notice/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/info_table.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/profile.json +25 -0
- hwpx_automation/office/authoring/design/profiles/official_notice/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/body.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/heading.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/info_table.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/fragments/title.xml +1 -0
- hwpx_automation/office/authoring/design/profiles/report/profile.json +25 -0
- hwpx_automation/office/authoring/design/profiles/report/template.hwpx +0 -0
- hwpx_automation/office/authoring/design/validator.py +107 -0
- hwpx_automation/office/authoring/presets/__init__.py +22 -0
- hwpx_automation/office/authoring/presets/proposal.py +538 -0
- hwpx_automation/office/authoring/report_parser.py +141 -0
- hwpx_automation/office/authoring/style_profile.py +437 -0
- hwpx_automation/office/authoring/template_analyzer.py +657 -0
- hwpx_automation/office/compliance/__init__.py +38 -0
- hwpx_automation/office/compliance/official_lint.py +478 -0
- hwpx_automation/office/compliance/pii.py +388 -0
- hwpx_automation/office/document_ops/__init__.py +13 -0
- hwpx_automation/office/document_ops/comparison.py +62 -0
- hwpx_automation/office/document_ops/mail_merge.py +73 -0
- hwpx_automation/office/document_ops/redline.py +35 -0
- hwpx_automation/office/evalplan/__init__.py +36 -0
- hwpx_automation/office/evalplan/runtime.py +2762 -0
- hwpx_automation/office/exam/__init__.py +44 -0
- hwpx_automation/office/exam/compose.py +282 -0
- hwpx_automation/office/exam/ir.py +44 -0
- hwpx_automation/office/exam/measure.py +163 -0
- hwpx_automation/office/exam/parser.py +150 -0
- hwpx_automation/office/exam/profile.py +123 -0
- hwpx_automation/office/form_fill/__init__.py +66 -0
- hwpx_automation/office/form_fill/classification.py +108 -0
- hwpx_automation/office/form_fill/fill_residue.py +242 -0
- hwpx_automation/office/form_fill/fit/__init__.py +36 -0
- hwpx_automation/office/form_fill/fit/apply.py +24 -0
- hwpx_automation/office/form_fill/fit/engine.py +24 -0
- hwpx_automation/office/form_fill/fit/measure.py +50 -0
- hwpx_automation/office/form_fill/fit/policy.py +28 -0
- hwpx_automation/office/form_fill/fit/report.py +28 -0
- hwpx_automation/office/form_fill/fit/seal.py +457 -0
- hwpx_automation/office/form_fill/fit/wordbox.py +1343 -0
- hwpx_automation/office/form_fill/guidance.py +704 -0
- hwpx_automation/office/form_fill/quality.py +961 -0
- hwpx_automation/office/form_fill/split_run.py +333 -0
- hwpx_automation/office/form_fill/template_formfit.py +656 -0
- hwpx_automation/office/house_style/__init__.py +196 -0
- hwpx_automation/office/house_style/composition.py +68 -0
- hwpx_automation/office/house_style/data/bank.json +625 -0
- hwpx_automation/office/house_style/data/genres.json +43 -0
- hwpx_automation/office/quality/__init__.py +14 -0
- hwpx_automation/office/quality/page_guard.py +277 -0
- hwpx_automation/office/rendering/__init__.py +145 -0
- hwpx_automation/office/rendering/_hancom_open_rate.ps1 +374 -0
- hwpx_automation/office/rendering/_refresh_hwpx_mac.applescript +162 -0
- hwpx_automation/office/rendering/_render_hwpx.ps1 +72 -0
- hwpx_automation/office/rendering/_render_hwpx_mac.applescript +249 -0
- hwpx_automation/office/rendering/block_splits.py +76 -0
- hwpx_automation/office/rendering/detectors.py +151 -0
- hwpx_automation/office/rendering/diff.py +153 -0
- hwpx_automation/office/rendering/fixture_corpus.py +215 -0
- hwpx_automation/office/rendering/oracle.py +909 -0
- hwpx_automation/office/rendering/page_qa.py +245 -0
- hwpx_automation/office/rendering/qa_contracts.py +293 -0
- hwpx_automation/office/rendering/qa_metrics.py +241 -0
- hwpx_automation/office/rendering/worker.py +290 -0
- hwpx_automation/office/utilities/__init__.py +12 -0
- hwpx_automation/office/utilities/table_compute.py +477 -0
- hwpx_automation/ops_services/__init__.py +1 -0
- hwpx_automation/ops_services/_border_fill.py +283 -0
- hwpx_automation/ops_services/composition.py +55 -0
- hwpx_automation/ops_services/content_layout.py +322 -0
- hwpx_automation/ops_services/context.py +213 -0
- hwpx_automation/ops_services/form_fields.py +557 -0
- hwpx_automation/ops_services/media.py +178 -0
- hwpx_automation/ops_services/memo_style.py +477 -0
- hwpx_automation/ops_services/package_validation.py +166 -0
- hwpx_automation/ops_services/planning.py +201 -0
- hwpx_automation/ops_services/preview_export.py +585 -0
- hwpx_automation/ops_services/read_query.py +601 -0
- hwpx_automation/ops_services/save_policy.py +604 -0
- hwpx_automation/ops_services/tables.py +539 -0
- hwpx_automation/ops_services/transactions.py +616 -0
- hwpx_automation/preview_output_models.py +69 -0
- hwpx_automation/public-modules.json +206 -0
- hwpx_automation/py.typed +1 -0
- hwpx_automation/quality.py +351 -0
- hwpx_automation/quality_generation.py +725 -0
- hwpx_automation/runtime.py +321 -0
- hwpx_automation/runtime_services.py +100 -0
- hwpx_automation/server.py +259 -0
- hwpx_automation/storage.py +747 -0
- hwpx_automation/tool_bindings.py +170 -0
- hwpx_automation/tool_contract.py +982 -0
- hwpx_automation/upstream.py +755 -0
- hwpx_automation/utils/__init__.py +2 -0
- hwpx_automation/utils/helpers.py +29 -0
- hwpx_automation/visual_qa.py +667 -0
- hwpx_automation/workflow/__init__.py +55 -0
- hwpx_automation/workflow/adapters.py +482 -0
- hwpx_automation/workflow/dispatcher.py +213 -0
- hwpx_automation/workflow/models.py +243 -0
- hwpx_automation/workflow/policy.py +198 -0
- hwpx_automation/workflow/render_contracts.py +173 -0
- hwpx_automation/workflow/render_metrics.py +196 -0
- hwpx_automation/workflow/render_queue.py +482 -0
- hwpx_automation/workflow/render_security.py +172 -0
- hwpx_automation/workflow/render_transport.py +369 -0
- hwpx_automation/workflow/rendering.py +206 -0
- hwpx_automation/workflow/service.py +758 -0
- hwpx_automation/workflow/state_machine.py +65 -0
- hwpx_automation/workflow/store.py +747 -0
- hwpx_automation/workspace.py +1694 -0
- python_hwpx_automation-6.0.3.dist-info/METADATA +279 -0
- python_hwpx_automation-6.0.3.dist-info/RECORD +217 -0
- python_hwpx_automation-6.0.3.dist-info/WHEEL +5 -0
- python_hwpx_automation-6.0.3.dist-info/entry_points.txt +3 -0
- python_hwpx_automation-6.0.3.dist-info/licenses/LICENSE +178 -0
- python_hwpx_automation-6.0.3.dist-info/licenses/NOTICE +14 -0
- python_hwpx_automation-6.0.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Helpers for generating stable handles for logical nodes."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import hashlib
|
|
7
|
+
from typing import Iterable, Tuple
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _normalize_parts(parts: Iterable[str | int]) -> Tuple[str, ...]:
|
|
11
|
+
normalized: list[str] = []
|
|
12
|
+
for part in parts:
|
|
13
|
+
text = str(part)
|
|
14
|
+
normalized.append(text)
|
|
15
|
+
return tuple(normalized)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def stable_node_id(parts: Iterable[str | int]) -> str:
|
|
19
|
+
"""Return a deterministic identifier for the provided structural path."""
|
|
20
|
+
|
|
21
|
+
normalized = _normalize_parts(parts)
|
|
22
|
+
joined = "::".join(normalized)
|
|
23
|
+
digest = hashlib.sha256(joined.encode("utf-8")).hexdigest()[:16]
|
|
24
|
+
return f"n_{digest}"
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any, Iterator
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class ResolvedParagraph:
|
|
10
|
+
paragraph: Any
|
|
11
|
+
location: dict[str, Any]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def body_paragraph_location(paragraph_index: int) -> dict[str, Any]:
|
|
15
|
+
return {"kind": "body_paragraph", "paragraph_index": paragraph_index}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def table_cell_paragraph_location(
|
|
19
|
+
table_index: int,
|
|
20
|
+
row: int,
|
|
21
|
+
col: int,
|
|
22
|
+
cell_paragraph_index: int,
|
|
23
|
+
) -> dict[str, Any]:
|
|
24
|
+
return {
|
|
25
|
+
"kind": "table_cell_paragraph",
|
|
26
|
+
"table_index": table_index,
|
|
27
|
+
"row": row,
|
|
28
|
+
"col": col,
|
|
29
|
+
"cell_paragraph_index": cell_paragraph_index,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def location_anchor(location: dict[str, Any], position: int | None = None) -> dict[str, Any]:
|
|
34
|
+
anchor_id = _anchor_id(location, position)
|
|
35
|
+
payload: dict[str, Any] = {"anchor_id": anchor_id, "location": dict(location)}
|
|
36
|
+
if position is not None:
|
|
37
|
+
payload["position"] = position
|
|
38
|
+
return payload
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def location_from_anchor(anchor: dict[str, Any] | str) -> dict[str, Any]:
|
|
42
|
+
if isinstance(anchor, dict):
|
|
43
|
+
if isinstance(anchor.get("location"), dict):
|
|
44
|
+
return dict(anchor["location"])
|
|
45
|
+
if "kind" in anchor:
|
|
46
|
+
return dict(anchor)
|
|
47
|
+
if not isinstance(anchor, str):
|
|
48
|
+
raise ValueError("anchor must be an object with location or an anchor_id string")
|
|
49
|
+
|
|
50
|
+
head = anchor.split("@", 1)[0]
|
|
51
|
+
parts = head.split(":")
|
|
52
|
+
if parts[0] == "body" and len(parts) == 2:
|
|
53
|
+
return body_paragraph_location(_int(parts[1], "paragraph_index"))
|
|
54
|
+
if parts[0] == "table" and len(parts) == 5:
|
|
55
|
+
return table_cell_paragraph_location(
|
|
56
|
+
_int(parts[1], "table_index"),
|
|
57
|
+
_int(parts[2], "row"),
|
|
58
|
+
_int(parts[3], "col"),
|
|
59
|
+
_int(parts[4], "cell_paragraph_index"),
|
|
60
|
+
)
|
|
61
|
+
raise ValueError(f"unsupported anchor_id: {anchor}")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def iter_indexed_tables(doc: Any) -> Iterator[tuple[int, Any]]:
|
|
65
|
+
table_index = 0
|
|
66
|
+
for paragraph in getattr(doc, "paragraphs", []):
|
|
67
|
+
for table in _iter_tables_from_paragraph(paragraph):
|
|
68
|
+
yield table_index, table
|
|
69
|
+
table_index += 1
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def iter_paragraph_locations(doc: Any) -> Iterator[ResolvedParagraph]:
|
|
73
|
+
for paragraph_index, paragraph in enumerate(getattr(doc, "paragraphs", [])):
|
|
74
|
+
yield ResolvedParagraph(paragraph, body_paragraph_location(paragraph_index))
|
|
75
|
+
|
|
76
|
+
for table_index, table in iter_indexed_tables(doc):
|
|
77
|
+
for row, col, cell in _iter_anchor_cells(table):
|
|
78
|
+
for cell_paragraph_index, paragraph in enumerate(getattr(cell, "paragraphs", []) or []):
|
|
79
|
+
yield ResolvedParagraph(
|
|
80
|
+
paragraph,
|
|
81
|
+
table_cell_paragraph_location(table_index, row, col, cell_paragraph_index),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def resolve_paragraph_reference(
|
|
86
|
+
doc: Any,
|
|
87
|
+
*,
|
|
88
|
+
paragraph_index: int | None = None,
|
|
89
|
+
location: dict[str, Any] | None = None,
|
|
90
|
+
create: bool = False,
|
|
91
|
+
) -> ResolvedParagraph:
|
|
92
|
+
if location is None:
|
|
93
|
+
if paragraph_index is None:
|
|
94
|
+
raise ValueError("paragraph_index 또는 location 중 하나를 제공해야 합니다.")
|
|
95
|
+
return _resolve_body_paragraph(doc, paragraph_index)
|
|
96
|
+
|
|
97
|
+
kind = str(location.get("kind") or "").strip()
|
|
98
|
+
if kind in {"body", "paragraph", "body_paragraph"}:
|
|
99
|
+
index_value = location.get("paragraph_index", paragraph_index)
|
|
100
|
+
if index_value is None:
|
|
101
|
+
raise ValueError("body_paragraph location에는 paragraph_index가 필요합니다.")
|
|
102
|
+
return _resolve_body_paragraph(doc, int(index_value))
|
|
103
|
+
|
|
104
|
+
if kind in {"table_cell_paragraph", "cell_paragraph"}:
|
|
105
|
+
return _resolve_table_cell_paragraph(doc, location, create=create)
|
|
106
|
+
|
|
107
|
+
raise ValueError(f"지원하지 않는 location kind: {kind or '<missing>'}")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _resolve_body_paragraph(doc: Any, paragraph_index: int) -> ResolvedParagraph:
|
|
111
|
+
paragraphs = list(getattr(doc, "paragraphs", []))
|
|
112
|
+
if paragraph_index < 0 or paragraph_index >= len(paragraphs):
|
|
113
|
+
raise ValueError(f"유효하지 않은 paragraph_index: {paragraph_index}")
|
|
114
|
+
return ResolvedParagraph(paragraphs[paragraph_index], body_paragraph_location(paragraph_index))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _resolve_table_cell_paragraph(
|
|
118
|
+
doc: Any,
|
|
119
|
+
location: dict[str, Any],
|
|
120
|
+
*,
|
|
121
|
+
create: bool,
|
|
122
|
+
) -> ResolvedParagraph:
|
|
123
|
+
table_index = _int(location.get("table_index"), "table_index")
|
|
124
|
+
row = _int(location.get("row"), "row")
|
|
125
|
+
col = _int(location.get("col"), "col")
|
|
126
|
+
cell_paragraph_index = _int(
|
|
127
|
+
location.get("cell_paragraph_index", location.get("p", 0)),
|
|
128
|
+
"cell_paragraph_index",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
tables = [table for _, table in iter_indexed_tables(doc)]
|
|
132
|
+
if table_index < 0 or table_index >= len(tables):
|
|
133
|
+
raise ValueError(f"유효하지 않은 table_index: {table_index}")
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
cell = tables[table_index].cell(row, col)
|
|
137
|
+
except (IndexError, ValueError) as exc:
|
|
138
|
+
raise ValueError(f"유효하지 않은 table cell 좌표: table={table_index}, row={row}, col={col}") from exc
|
|
139
|
+
|
|
140
|
+
paragraphs = list(getattr(cell, "paragraphs", []) or [])
|
|
141
|
+
if create and not paragraphs and hasattr(cell, "add_paragraph"):
|
|
142
|
+
paragraphs = [cell.add_paragraph("")]
|
|
143
|
+
|
|
144
|
+
if cell_paragraph_index < 0 or cell_paragraph_index >= len(paragraphs):
|
|
145
|
+
raise ValueError(
|
|
146
|
+
"유효하지 않은 cell_paragraph_index: "
|
|
147
|
+
f"{cell_paragraph_index} (table={table_index}, row={row}, col={col})"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
normalized = table_cell_paragraph_location(table_index, row, col, cell_paragraph_index)
|
|
151
|
+
return ResolvedParagraph(paragraphs[cell_paragraph_index], normalized)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _iter_tables_from_paragraph(paragraph: Any) -> Iterator[Any]:
|
|
155
|
+
for table in getattr(paragraph, "tables", []) or []:
|
|
156
|
+
yield table
|
|
157
|
+
yield from _iter_nested_tables(table)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _iter_nested_tables(table: Any) -> Iterator[Any]:
|
|
161
|
+
for row in getattr(table, "rows", []) or []:
|
|
162
|
+
for cell in getattr(row, "cells", []) or []:
|
|
163
|
+
for paragraph in getattr(cell, "paragraphs", []) or []:
|
|
164
|
+
yield from _iter_tables_from_paragraph(paragraph)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _iter_anchor_cells(table: Any) -> Iterator[tuple[int, int, Any]]:
|
|
168
|
+
if hasattr(table, "iter_grid"):
|
|
169
|
+
seen: set[int] = set()
|
|
170
|
+
for position in table.iter_grid():
|
|
171
|
+
if not getattr(position, "is_anchor", False):
|
|
172
|
+
continue
|
|
173
|
+
cell = position.cell
|
|
174
|
+
marker = id(getattr(cell, "element", cell))
|
|
175
|
+
if marker in seen:
|
|
176
|
+
continue
|
|
177
|
+
seen.add(marker)
|
|
178
|
+
row, col = getattr(position, "anchor", (position.row, position.column))
|
|
179
|
+
yield int(row), int(col), cell
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
for row_index, row in enumerate(getattr(table, "rows", []) or []):
|
|
183
|
+
for col_index, cell in enumerate(getattr(row, "cells", []) or []):
|
|
184
|
+
yield row_index, col_index, cell
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _int(value: Any, name: str) -> int:
|
|
188
|
+
try:
|
|
189
|
+
return int(value)
|
|
190
|
+
except (TypeError, ValueError) as exc:
|
|
191
|
+
raise ValueError(f"{name} must be an integer") from exc
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _anchor_id(location: dict[str, Any], position: int | None) -> str:
|
|
195
|
+
suffix = "" if position is None else f"@{position}"
|
|
196
|
+
kind = location.get("kind")
|
|
197
|
+
if kind == "body_paragraph":
|
|
198
|
+
return f"body:{location['paragraph_index']}{suffix}"
|
|
199
|
+
if kind == "table_cell_paragraph":
|
|
200
|
+
return (
|
|
201
|
+
"table:"
|
|
202
|
+
f"{location['table_index']}:{location['row']}:{location['col']}:"
|
|
203
|
+
f"{location['cell_paragraph_index']}{suffix}"
|
|
204
|
+
)
|
|
205
|
+
return f"{kind or 'unknown'}{suffix}"
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Document locator models used across tool schemas."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Annotated, Dict, Optional
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
9
|
+
from typing_extensions import Literal
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
_LOCATOR_KEYS = {"type", "path", "uri", "backend", "handleId"}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class _LocatorModel(BaseModel):
|
|
16
|
+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PathLocator(_LocatorModel):
|
|
20
|
+
type: Literal["path"] = Field("path", alias="type")
|
|
21
|
+
path: str
|
|
22
|
+
backend: Optional[str] = None
|
|
23
|
+
|
|
24
|
+
@model_validator(mode="before")
|
|
25
|
+
@classmethod
|
|
26
|
+
def _default_type(cls, data: object) -> object:
|
|
27
|
+
if isinstance(data, dict) and "type" not in data and "path" in data:
|
|
28
|
+
enriched = dict(data)
|
|
29
|
+
enriched.setdefault("type", "path")
|
|
30
|
+
return enriched
|
|
31
|
+
return data
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class UriLocator(_LocatorModel):
|
|
35
|
+
type: Literal["uri"] = Field("uri", alias="type")
|
|
36
|
+
uri: str
|
|
37
|
+
backend: Optional[str] = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class HandleLocator(_LocatorModel):
|
|
41
|
+
type: Literal["handle"] = Field("handle", alias="type")
|
|
42
|
+
handle_id: str = Field(alias="handleId")
|
|
43
|
+
backend: Optional[str] = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class RegisteredHandle(_LocatorModel):
|
|
47
|
+
"""서버가 관리하는 등록 핸들의 읽기 전용 직렬화 모델."""
|
|
48
|
+
|
|
49
|
+
type: Literal["handle"] = Field("handle", alias="type")
|
|
50
|
+
handle_id: str = Field(alias="handleId")
|
|
51
|
+
path: str
|
|
52
|
+
backend: Optional[str] = None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
DocumentLocator = Annotated[
|
|
56
|
+
PathLocator | UriLocator | HandleLocator,
|
|
57
|
+
Field(discriminator="type"),
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def normalize_locator_payload(data: Dict[str, object], *, field_name: str = "document") -> Dict[str, object]:
|
|
62
|
+
"""Return *data* with locator keys grouped under *field_name*.
|
|
63
|
+
|
|
64
|
+
The function accepts legacy payloads that specify ``path`` at the top level
|
|
65
|
+
while also supporting fully qualified discriminated union inputs.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
if field_name in data:
|
|
69
|
+
return data
|
|
70
|
+
|
|
71
|
+
locator: Dict[str, object] = {}
|
|
72
|
+
remainder: Dict[str, object] = {}
|
|
73
|
+
for key, value in data.items():
|
|
74
|
+
if key in _LOCATOR_KEYS:
|
|
75
|
+
locator[key] = value
|
|
76
|
+
else:
|
|
77
|
+
remainder[key] = value
|
|
78
|
+
|
|
79
|
+
if not locator:
|
|
80
|
+
raise ValueError("document locator must include path, uri, or handleId")
|
|
81
|
+
|
|
82
|
+
if "type" not in locator:
|
|
83
|
+
if "path" in locator:
|
|
84
|
+
locator["type"] = "path"
|
|
85
|
+
elif "uri" in locator:
|
|
86
|
+
locator["type"] = "uri"
|
|
87
|
+
elif "handleId" in locator:
|
|
88
|
+
locator["type"] = "handle"
|
|
89
|
+
else: # pragma: no cover - defensive guard
|
|
90
|
+
raise ValueError("document locator requires a discriminator")
|
|
91
|
+
|
|
92
|
+
remainder[field_name] = locator
|
|
93
|
+
return remainder
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def locator_identifier(locator: DocumentLocator) -> str:
|
|
97
|
+
"""Return a stable identifier for *locator*."""
|
|
98
|
+
|
|
99
|
+
if isinstance(locator, PathLocator):
|
|
100
|
+
return locator.path
|
|
101
|
+
if isinstance(locator, UriLocator):
|
|
102
|
+
return locator.uri
|
|
103
|
+
if isinstance(locator, HandleLocator):
|
|
104
|
+
return locator.handle_id
|
|
105
|
+
raise TypeError(f"Unsupported locator type: {type(locator)!r}")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def locator_path(locator: DocumentLocator) -> Optional[str]:
|
|
109
|
+
"""Return the preferred path-like value for *locator* if available."""
|
|
110
|
+
|
|
111
|
+
if isinstance(locator, PathLocator):
|
|
112
|
+
return locator.path
|
|
113
|
+
if isinstance(locator, UriLocator):
|
|
114
|
+
return locator.uri
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def locator_backend(locator: DocumentLocator) -> Optional[str]:
|
|
119
|
+
"""Return the backend hint provided by *locator*, if any."""
|
|
120
|
+
|
|
121
|
+
return getattr(locator, "backend", None)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def document_locator_schema() -> Dict[str, object]:
|
|
125
|
+
"""Return a sanitized JSON schema fragment for document locators."""
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
"type": "object",
|
|
129
|
+
"description": (
|
|
130
|
+
"Discriminated locator supporting legacy paths, HTTP URIs, or opaque handles. "
|
|
131
|
+
"Top-level path/uri/handleId shorthands remain supported for backwards compatibility."
|
|
132
|
+
),
|
|
133
|
+
"properties": {
|
|
134
|
+
"type": {
|
|
135
|
+
"type": "string",
|
|
136
|
+
"enum": ["path", "uri", "handle"],
|
|
137
|
+
"description": "Locator variant; defaults to 'path' when omitted.",
|
|
138
|
+
},
|
|
139
|
+
"path": {
|
|
140
|
+
"type": "string",
|
|
141
|
+
"minLength": 1,
|
|
142
|
+
"description": "Filesystem-relative path resolved by the configured storage backend.",
|
|
143
|
+
},
|
|
144
|
+
"uri": {
|
|
145
|
+
"type": "string",
|
|
146
|
+
"minLength": 1,
|
|
147
|
+
"description": "HTTP or backend-specific URI when using remote storage.",
|
|
148
|
+
},
|
|
149
|
+
"handleId": {
|
|
150
|
+
"type": "string",
|
|
151
|
+
"minLength": 1,
|
|
152
|
+
"description": "Opaque identifier for a previously registered document handle.",
|
|
153
|
+
},
|
|
154
|
+
"backend": {
|
|
155
|
+
"type": "string",
|
|
156
|
+
"minLength": 1,
|
|
157
|
+
"description": "Optional hint describing the storage backend to use (e.g. 'http').",
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
"required": ["type"],
|
|
161
|
+
"additionalProperties": False,
|
|
162
|
+
}
|