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,401 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Discriminated public mutation vocabularies with legacy-payload adapters."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Annotated, Any, Literal, Mapping, TypeAlias
|
|
7
|
+
|
|
8
|
+
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, RootModel, model_validator
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class _ClosedModel(BaseModel):
|
|
12
|
+
model_config = ConfigDict(populate_by_name=True, extra="forbid")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ReplacementRule(_ClosedModel):
|
|
16
|
+
find_text: str = Field(
|
|
17
|
+
validation_alias=AliasChoices("findText", "find_text", "find"),
|
|
18
|
+
serialization_alias="findText",
|
|
19
|
+
)
|
|
20
|
+
replace_text: str = Field(
|
|
21
|
+
default="",
|
|
22
|
+
validation_alias=AliasChoices("replaceText", "replace_text", "replace"),
|
|
23
|
+
serialization_alias="replaceText",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class EditReplaceText(_ClosedModel):
|
|
28
|
+
type: Literal["replace_text"]
|
|
29
|
+
find_text: str = Field(
|
|
30
|
+
validation_alias=AliasChoices("findText", "find_text", "find"),
|
|
31
|
+
serialization_alias="findText",
|
|
32
|
+
)
|
|
33
|
+
replace_text: str = Field(
|
|
34
|
+
default="",
|
|
35
|
+
validation_alias=AliasChoices("replaceText", "replace_text", "replace"),
|
|
36
|
+
serialization_alias="replaceText",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class EditBatchReplace(_ClosedModel):
|
|
41
|
+
type: Literal["batch_replace"]
|
|
42
|
+
replacements: list[ReplacementRule]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class EditAddHeading(_ClosedModel):
|
|
46
|
+
type: Literal["add_heading"]
|
|
47
|
+
text: str = ""
|
|
48
|
+
level: int = Field(default=1, ge=1, le=9)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class EditAddParagraph(_ClosedModel):
|
|
52
|
+
type: Literal["add_paragraph"]
|
|
53
|
+
text: str = ""
|
|
54
|
+
style: str | int | None = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class EditInsertParagraph(_ClosedModel):
|
|
58
|
+
type: Literal["insert_paragraph"]
|
|
59
|
+
paragraph_index: int = Field(
|
|
60
|
+
ge=0,
|
|
61
|
+
validation_alias=AliasChoices("paragraphIndex", "paragraph_index"),
|
|
62
|
+
serialization_alias="paragraphIndex",
|
|
63
|
+
)
|
|
64
|
+
text: str = ""
|
|
65
|
+
style: str | int | None = None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class EditDeleteParagraph(_ClosedModel):
|
|
69
|
+
type: Literal["delete_paragraph"]
|
|
70
|
+
paragraph_index: int = Field(
|
|
71
|
+
ge=0,
|
|
72
|
+
validation_alias=AliasChoices("paragraphIndex", "paragraph_index"),
|
|
73
|
+
serialization_alias="paragraphIndex",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class EditAddTable(_ClosedModel):
|
|
78
|
+
type: Literal["add_table"]
|
|
79
|
+
rows: int = Field(ge=1)
|
|
80
|
+
cols: int = Field(ge=1, validation_alias=AliasChoices("cols", "columns"))
|
|
81
|
+
data: list[list[Any]] | None = None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class EditSetTableCellText(_ClosedModel):
|
|
85
|
+
type: Literal["set_table_cell_text"]
|
|
86
|
+
table_index: int = Field(
|
|
87
|
+
default=0,
|
|
88
|
+
ge=0,
|
|
89
|
+
validation_alias=AliasChoices("tableIndex", "table_index"),
|
|
90
|
+
serialization_alias="tableIndex",
|
|
91
|
+
)
|
|
92
|
+
row: int = Field(ge=0)
|
|
93
|
+
col: int = Field(ge=0, validation_alias=AliasChoices("col", "column"))
|
|
94
|
+
text: str = ""
|
|
95
|
+
preserve_format: bool = Field(
|
|
96
|
+
default=True,
|
|
97
|
+
validation_alias=AliasChoices("preserveFormat", "preserve_format"),
|
|
98
|
+
serialization_alias="preserveFormat",
|
|
99
|
+
)
|
|
100
|
+
split_paragraphs: bool = Field(
|
|
101
|
+
default=False,
|
|
102
|
+
validation_alias=AliasChoices("splitParagraphs", "split_paragraphs"),
|
|
103
|
+
serialization_alias="splitParagraphs",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class EditFillByPath(_ClosedModel):
|
|
108
|
+
type: Literal["fill_by_path"]
|
|
109
|
+
mappings: dict[str, str] = Field(min_length=1)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class EditAddPageBreak(_ClosedModel):
|
|
113
|
+
type: Literal["add_page_break"]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
EditOperationUnion: TypeAlias = Annotated[
|
|
117
|
+
EditReplaceText
|
|
118
|
+
| EditBatchReplace
|
|
119
|
+
| EditAddHeading
|
|
120
|
+
| EditAddParagraph
|
|
121
|
+
| EditInsertParagraph
|
|
122
|
+
| EditDeleteParagraph
|
|
123
|
+
| EditAddTable
|
|
124
|
+
| EditSetTableCellText
|
|
125
|
+
| EditFillByPath
|
|
126
|
+
| EditAddPageBreak,
|
|
127
|
+
Field(discriminator="type"),
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class EditOperation(RootModel[EditOperationUnion]):
|
|
132
|
+
@model_validator(mode="before")
|
|
133
|
+
@classmethod
|
|
134
|
+
def normalize_discriminator(cls, value: Any) -> Any:
|
|
135
|
+
if isinstance(value, Mapping):
|
|
136
|
+
payload = dict(value)
|
|
137
|
+
raw = payload.get("type", payload.get("op", payload.get("operation")))
|
|
138
|
+
if isinstance(raw, str):
|
|
139
|
+
payload["type"] = raw.strip().replace("-", "_")
|
|
140
|
+
payload.pop("op", None)
|
|
141
|
+
payload.pop("operation", None)
|
|
142
|
+
return payload
|
|
143
|
+
return value
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class CellAnchor(_ClosedModel):
|
|
147
|
+
label: str
|
|
148
|
+
direction: Literal["right", "left", "below", "above"] = Field(
|
|
149
|
+
default="right",
|
|
150
|
+
validation_alias=AliasChoices("direction", "dir"),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class _TableAddress(_ClosedModel):
|
|
155
|
+
section_path: str = Field(
|
|
156
|
+
default="Contents/section0.xml",
|
|
157
|
+
validation_alias=AliasChoices("sectionPath", "section_path"),
|
|
158
|
+
serialization_alias="sectionPath",
|
|
159
|
+
)
|
|
160
|
+
table_index: int | None = Field(
|
|
161
|
+
default=None,
|
|
162
|
+
ge=0,
|
|
163
|
+
validation_alias=AliasChoices("tableIndex", "table_index"),
|
|
164
|
+
serialization_alias="tableIndex",
|
|
165
|
+
)
|
|
166
|
+
table_anchor: str | None = Field(
|
|
167
|
+
default=None,
|
|
168
|
+
validation_alias=AliasChoices("tableAnchor", "table_anchor"),
|
|
169
|
+
serialization_alias="tableAnchor",
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
@model_validator(mode="after")
|
|
173
|
+
def require_table_target(self) -> "_TableAddress":
|
|
174
|
+
if self.table_index is None and not self.table_anchor:
|
|
175
|
+
raise ValueError("tableIndex or tableAnchor is required")
|
|
176
|
+
return self
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class TableFillCell(_TableAddress):
|
|
180
|
+
op: Literal["fill_cell"]
|
|
181
|
+
row: int | None = Field(default=None, ge=0)
|
|
182
|
+
col: int | None = Field(default=None, ge=0)
|
|
183
|
+
text: str
|
|
184
|
+
cell_anchor: CellAnchor | None = Field(
|
|
185
|
+
default=None,
|
|
186
|
+
validation_alias=AliasChoices("cellAnchor", "cell_anchor"),
|
|
187
|
+
serialization_alias="cellAnchor",
|
|
188
|
+
)
|
|
189
|
+
max_lines: int | None = Field(
|
|
190
|
+
default=None,
|
|
191
|
+
ge=1,
|
|
192
|
+
validation_alias=AliasChoices("maxLines", "max_lines"),
|
|
193
|
+
serialization_alias="maxLines",
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
@model_validator(mode="after")
|
|
197
|
+
def require_cell_target(self) -> "TableFillCell":
|
|
198
|
+
if (self.row is None or self.col is None) and self.cell_anchor is None:
|
|
199
|
+
raise ValueError("row+col or cellAnchor is required")
|
|
200
|
+
return self
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class TableDeleteColumn(_TableAddress):
|
|
204
|
+
op: Literal["delete_column"]
|
|
205
|
+
col: int | None = Field(default=None, ge=0)
|
|
206
|
+
cols: list[int] | None = None
|
|
207
|
+
|
|
208
|
+
@model_validator(mode="after")
|
|
209
|
+
def require_columns(self) -> "TableDeleteColumn":
|
|
210
|
+
if self.col is None and not self.cols:
|
|
211
|
+
raise ValueError("col or cols is required")
|
|
212
|
+
return self
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class TableDeleteRow(_TableAddress):
|
|
216
|
+
op: Literal["delete_row"]
|
|
217
|
+
row: int | None = Field(default=None, ge=0)
|
|
218
|
+
rows: list[int] | None = None
|
|
219
|
+
|
|
220
|
+
@model_validator(mode="after")
|
|
221
|
+
def require_rows(self) -> "TableDeleteRow":
|
|
222
|
+
if self.row is None and not self.rows:
|
|
223
|
+
raise ValueError("row or rows is required")
|
|
224
|
+
return self
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class TableDeleteTable(_TableAddress):
|
|
228
|
+
op: Literal["delete_table"]
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class TableCloneTable(_TableAddress):
|
|
232
|
+
op: Literal["clone_table"]
|
|
233
|
+
count: int = Field(default=1, ge=1)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class TableInsertRowByClone(_TableAddress):
|
|
237
|
+
op: Literal["insert_row_by_clone"]
|
|
238
|
+
ref_row: int = Field(ge=0)
|
|
239
|
+
count: int = Field(default=1, ge=1)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class TableInsertBlockByClone(_TableAddress):
|
|
243
|
+
op: Literal["insert_block_by_clone"]
|
|
244
|
+
ref_rows: tuple[int, int]
|
|
245
|
+
count: int = Field(default=1, ge=1)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class TableSetColumnWidths(_TableAddress):
|
|
249
|
+
op: Literal["set_column_widths"]
|
|
250
|
+
widths: dict[int, int] | list[int]
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class TableAutofitColumns(_TableAddress):
|
|
254
|
+
op: Literal["autofit_columns"]
|
|
255
|
+
min_frac: float = Field(default=0.06, gt=0, lt=1)
|
|
256
|
+
damp: float = Field(default=0.5, gt=0, le=1)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class TableSetRowHeights(_TableAddress):
|
|
260
|
+
op: Literal["set_row_heights"]
|
|
261
|
+
heights: dict[int, int]
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class TableSplitCellVertical(_TableAddress):
|
|
265
|
+
op: Literal["split_cell_vertical"]
|
|
266
|
+
row: int = Field(ge=0)
|
|
267
|
+
col: int = Field(ge=0)
|
|
268
|
+
sizes: list[int] = Field(min_length=2)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class TableSetCellLineSpacing(_TableAddress):
|
|
272
|
+
op: Literal["set_cell_line_spacing"]
|
|
273
|
+
cells: list[tuple[int, int]] = Field(default_factory=list)
|
|
274
|
+
rows: list[int] = Field(default_factory=list)
|
|
275
|
+
line_spacing: int = Field(
|
|
276
|
+
gt=0,
|
|
277
|
+
validation_alias=AliasChoices("lineSpacing", "line_spacing"),
|
|
278
|
+
serialization_alias="lineSpacing",
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
@model_validator(mode="after")
|
|
282
|
+
def require_cells_or_rows(self) -> "TableSetCellLineSpacing":
|
|
283
|
+
if not self.cells and not self.rows:
|
|
284
|
+
raise ValueError("cells or rows is required")
|
|
285
|
+
return self
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
TableOperationUnion: TypeAlias = Annotated[
|
|
289
|
+
TableFillCell
|
|
290
|
+
| TableDeleteColumn
|
|
291
|
+
| TableDeleteRow
|
|
292
|
+
| TableDeleteTable
|
|
293
|
+
| TableCloneTable
|
|
294
|
+
| TableInsertRowByClone
|
|
295
|
+
| TableInsertBlockByClone
|
|
296
|
+
| TableSetColumnWidths
|
|
297
|
+
| TableAutofitColumns
|
|
298
|
+
| TableSetRowHeights
|
|
299
|
+
| TableSplitCellVertical
|
|
300
|
+
| TableSetCellLineSpacing,
|
|
301
|
+
Field(discriminator="op"),
|
|
302
|
+
]
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
class TableOperation(RootModel[TableOperationUnion]):
|
|
306
|
+
@model_validator(mode="before")
|
|
307
|
+
@classmethod
|
|
308
|
+
def normalize_discriminator(cls, value: Any) -> Any:
|
|
309
|
+
if isinstance(value, Mapping):
|
|
310
|
+
payload = dict(value)
|
|
311
|
+
raw = payload.get("op", payload.get("type"))
|
|
312
|
+
if isinstance(raw, str):
|
|
313
|
+
payload["op"] = raw.strip().replace("-", "_")
|
|
314
|
+
payload.pop("type", None)
|
|
315
|
+
return payload
|
|
316
|
+
return value
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class BodyReplaceText(_ClosedModel):
|
|
320
|
+
op: Literal["replace_text"]
|
|
321
|
+
find: str
|
|
322
|
+
replace: str = ""
|
|
323
|
+
count: int = Field(default=1, ge=1)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class BodyDeleteParagraph(_ClosedModel):
|
|
327
|
+
op: Literal["delete_paragraph"]
|
|
328
|
+
index: int = Field(ge=0)
|
|
329
|
+
allow_tables: bool = False
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
class BodyInsertParagraphByClone(_ClosedModel):
|
|
333
|
+
op: Literal["insert_paragraph_by_clone"]
|
|
334
|
+
ref_index: int = Field(ge=0)
|
|
335
|
+
count: int = Field(default=1, ge=1)
|
|
336
|
+
texts: list[str] | None = None
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
class BodySetParagraphText(_ClosedModel):
|
|
340
|
+
op: Literal["set_paragraph_text"]
|
|
341
|
+
index: int = Field(ge=0)
|
|
342
|
+
text: str
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
class BodyReorderParagraphs(_ClosedModel):
|
|
346
|
+
op: Literal["reorder_paragraphs"]
|
|
347
|
+
start: int = Field(ge=0)
|
|
348
|
+
end: int = Field(ge=0)
|
|
349
|
+
order: list[int]
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
class BodyRestyleText(_ClosedModel):
|
|
353
|
+
op: Literal["restyle_text"]
|
|
354
|
+
find: str
|
|
355
|
+
count: int = Field(default=1, ge=1)
|
|
356
|
+
text_color: str | None = None
|
|
357
|
+
drop_italic: bool = True
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
BodyOperationUnion: TypeAlias = Annotated[
|
|
361
|
+
BodyReplaceText
|
|
362
|
+
| BodyDeleteParagraph
|
|
363
|
+
| BodyInsertParagraphByClone
|
|
364
|
+
| BodySetParagraphText
|
|
365
|
+
| BodyReorderParagraphs
|
|
366
|
+
| BodyRestyleText,
|
|
367
|
+
Field(discriminator="op"),
|
|
368
|
+
]
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
class BodyOperation(RootModel[BodyOperationUnion]):
|
|
372
|
+
@model_validator(mode="before")
|
|
373
|
+
@classmethod
|
|
374
|
+
def normalize_discriminator(cls, value: Any) -> Any:
|
|
375
|
+
if isinstance(value, Mapping):
|
|
376
|
+
payload = dict(value)
|
|
377
|
+
raw = payload.get("op", payload.get("type"))
|
|
378
|
+
if isinstance(raw, str):
|
|
379
|
+
payload["op"] = raw.strip().replace("-", "_")
|
|
380
|
+
payload.pop("type", None)
|
|
381
|
+
return payload
|
|
382
|
+
return value
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def operation_payloads(values: list[Any]) -> list[dict[str, Any]]:
|
|
386
|
+
"""Convert validated models or direct-call legacy dictionaries to core payloads."""
|
|
387
|
+
|
|
388
|
+
payloads: list[dict[str, Any]] = []
|
|
389
|
+
for value in values:
|
|
390
|
+
if isinstance(value, RootModel):
|
|
391
|
+
payloads.append(value.root.model_dump(by_alias=True, exclude_none=True))
|
|
392
|
+
elif isinstance(value, BaseModel):
|
|
393
|
+
payloads.append(value.model_dump(by_alias=True, exclude_none=True))
|
|
394
|
+
elif isinstance(value, Mapping):
|
|
395
|
+
payloads.append(dict(value))
|
|
396
|
+
else:
|
|
397
|
+
raise TypeError("mutation operations must be objects")
|
|
398
|
+
return payloads
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
__all__ = ["BodyOperation", "EditOperation", "TableOperation", "operation_payloads"]
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Default-deny SSRF policy for outbound HWPX HTTP clients."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import ipaddress
|
|
7
|
+
import socket
|
|
8
|
+
import ssl
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from http.client import HTTPConnection, HTTPSConnection
|
|
11
|
+
from urllib.parse import urljoin, urlsplit
|
|
12
|
+
from urllib.request import (
|
|
13
|
+
HTTPHandler,
|
|
14
|
+
HTTPRedirectHandler,
|
|
15
|
+
HTTPSHandler,
|
|
16
|
+
OpenerDirector,
|
|
17
|
+
Request,
|
|
18
|
+
build_opener,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from .configuration import canonical_env_name, env_value, legacy_env_name
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
ALLOW_PRIVATE_NETWORK_ENV = canonical_env_name("ALLOW_PRIVATE_NETWORK")
|
|
25
|
+
LEGACY_ALLOW_PRIVATE_NETWORK_ENV = legacy_env_name("ALLOW_PRIVATE_NETWORK")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class NetworkPolicyError(ValueError):
|
|
29
|
+
code = "NETWORK_DESTINATION_DENIED"
|
|
30
|
+
|
|
31
|
+
def __init__(self, message: str, *, reason: str) -> None:
|
|
32
|
+
super().__init__(message)
|
|
33
|
+
self.reason = reason
|
|
34
|
+
|
|
35
|
+
def safe_details(self) -> dict[str, str]:
|
|
36
|
+
return {"reason": self.reason}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _private_network_enabled() -> bool:
|
|
40
|
+
return (env_value("ALLOW_PRIVATE_NETWORK", "") or "").strip().casefold() in {
|
|
41
|
+
"1",
|
|
42
|
+
"true",
|
|
43
|
+
"yes",
|
|
44
|
+
"on",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True, slots=True)
|
|
49
|
+
class NetworkPolicy:
|
|
50
|
+
allow_private_network: bool = False
|
|
51
|
+
allow_http: bool = False
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def from_environment(cls, *, allow_http: bool = False) -> "NetworkPolicy":
|
|
55
|
+
return cls(
|
|
56
|
+
allow_private_network=_private_network_enabled(),
|
|
57
|
+
allow_http=allow_http,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def _addresses(self, host: str, port: int) -> set[ipaddress.IPv4Address | ipaddress.IPv6Address]:
|
|
61
|
+
try:
|
|
62
|
+
return {ipaddress.ip_address(host)}
|
|
63
|
+
except ValueError:
|
|
64
|
+
pass
|
|
65
|
+
try:
|
|
66
|
+
answers = socket.getaddrinfo(host, port, type=socket.SOCK_STREAM)
|
|
67
|
+
except socket.gaierror as exc:
|
|
68
|
+
raise NetworkPolicyError(
|
|
69
|
+
"network destination could not be resolved",
|
|
70
|
+
reason="dns_resolution_failed",
|
|
71
|
+
) from exc
|
|
72
|
+
addresses = {ipaddress.ip_address(answer[4][0]) for answer in answers}
|
|
73
|
+
if not addresses:
|
|
74
|
+
raise NetworkPolicyError(
|
|
75
|
+
"network destination resolved to no addresses",
|
|
76
|
+
reason="dns_resolution_empty",
|
|
77
|
+
)
|
|
78
|
+
return addresses
|
|
79
|
+
|
|
80
|
+
def _address_allowed(self, address: ipaddress.IPv4Address | ipaddress.IPv6Address) -> bool:
|
|
81
|
+
if address.is_global:
|
|
82
|
+
return True
|
|
83
|
+
if not self.allow_private_network:
|
|
84
|
+
return False
|
|
85
|
+
if address.is_link_local or address.is_multicast or address.is_unspecified:
|
|
86
|
+
return False
|
|
87
|
+
if address.is_loopback:
|
|
88
|
+
return True
|
|
89
|
+
if address.is_reserved:
|
|
90
|
+
return False
|
|
91
|
+
return address.is_private
|
|
92
|
+
|
|
93
|
+
def validate_connected_address(self, value: str) -> None:
|
|
94
|
+
try:
|
|
95
|
+
address = ipaddress.ip_address(value)
|
|
96
|
+
except ValueError as exc:
|
|
97
|
+
raise NetworkPolicyError(
|
|
98
|
+
"connected network peer did not expose a valid IP address",
|
|
99
|
+
reason="peer_address_invalid",
|
|
100
|
+
) from exc
|
|
101
|
+
if not self._address_allowed(address):
|
|
102
|
+
raise NetworkPolicyError(
|
|
103
|
+
"connected network peer is not allowed by the outbound network policy",
|
|
104
|
+
reason="peer_address_denied",
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def validate_url(self, url: str) -> str:
|
|
108
|
+
parsed = urlsplit(url)
|
|
109
|
+
scheme = parsed.scheme.casefold()
|
|
110
|
+
allowed_schemes = {"https"} | ({"http"} if self.allow_http else set())
|
|
111
|
+
if scheme not in allowed_schemes:
|
|
112
|
+
raise NetworkPolicyError(
|
|
113
|
+
"network URL must use https:// unless an explicit test-only HTTP policy is active",
|
|
114
|
+
reason="scheme_denied",
|
|
115
|
+
)
|
|
116
|
+
if not parsed.hostname or parsed.username is not None or parsed.password is not None:
|
|
117
|
+
raise NetworkPolicyError(
|
|
118
|
+
"network URL must contain a host and no embedded credentials",
|
|
119
|
+
reason="authority_invalid",
|
|
120
|
+
)
|
|
121
|
+
try:
|
|
122
|
+
port = parsed.port or (443 if scheme == "https" else 80)
|
|
123
|
+
except ValueError as exc:
|
|
124
|
+
raise NetworkPolicyError(
|
|
125
|
+
"network URL contains an invalid port",
|
|
126
|
+
reason="authority_invalid",
|
|
127
|
+
) from exc
|
|
128
|
+
addresses = self._addresses(parsed.hostname, port)
|
|
129
|
+
if not all(self._address_allowed(address) for address in addresses):
|
|
130
|
+
raise NetworkPolicyError(
|
|
131
|
+
"network destination is private, loopback, link-local, reserved, or otherwise non-public",
|
|
132
|
+
reason="address_class_denied",
|
|
133
|
+
)
|
|
134
|
+
return url
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class PolicyRedirectHandler(HTTPRedirectHandler):
|
|
138
|
+
def __init__(self, policy: NetworkPolicy) -> None:
|
|
139
|
+
super().__init__()
|
|
140
|
+
self.policy = policy
|
|
141
|
+
|
|
142
|
+
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
143
|
+
target = urljoin(req.full_url, newurl)
|
|
144
|
+
self.policy.validate_url(target)
|
|
145
|
+
return super().redirect_request(req, fp, code, msg, headers, target)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class _PolicyHTTPConnection(HTTPConnection):
|
|
149
|
+
def __init__(self, host: str, *, policy: NetworkPolicy, **kwargs) -> None:
|
|
150
|
+
self._network_policy = policy
|
|
151
|
+
super().__init__(host, **kwargs)
|
|
152
|
+
|
|
153
|
+
def connect(self) -> None:
|
|
154
|
+
super().connect()
|
|
155
|
+
assert self.sock is not None
|
|
156
|
+
try:
|
|
157
|
+
self._network_policy.validate_connected_address(self.sock.getpeername()[0])
|
|
158
|
+
except Exception:
|
|
159
|
+
self.close()
|
|
160
|
+
raise
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class _PolicyHTTPSConnection(HTTPSConnection):
|
|
164
|
+
def __init__(self, host: str, *, policy: NetworkPolicy, **kwargs) -> None:
|
|
165
|
+
self._network_policy = policy
|
|
166
|
+
super().__init__(host, **kwargs)
|
|
167
|
+
|
|
168
|
+
def connect(self) -> None:
|
|
169
|
+
super().connect()
|
|
170
|
+
assert self.sock is not None
|
|
171
|
+
try:
|
|
172
|
+
self._network_policy.validate_connected_address(self.sock.getpeername()[0])
|
|
173
|
+
except Exception:
|
|
174
|
+
self.close()
|
|
175
|
+
raise
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class PolicyHTTPHandler(HTTPHandler):
|
|
179
|
+
def __init__(self, policy: NetworkPolicy) -> None:
|
|
180
|
+
super().__init__()
|
|
181
|
+
self.policy = policy
|
|
182
|
+
|
|
183
|
+
def http_open(self, req):
|
|
184
|
+
def connection(host: str, **kwargs):
|
|
185
|
+
return _PolicyHTTPConnection(host, policy=self.policy, **kwargs)
|
|
186
|
+
|
|
187
|
+
return self.do_open(connection, req)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class PolicyHTTPSHandler(HTTPSHandler):
|
|
191
|
+
def __init__(
|
|
192
|
+
self,
|
|
193
|
+
policy: NetworkPolicy,
|
|
194
|
+
*,
|
|
195
|
+
context: ssl.SSLContext | None = None,
|
|
196
|
+
) -> None:
|
|
197
|
+
super().__init__(context=context)
|
|
198
|
+
self.policy = policy
|
|
199
|
+
|
|
200
|
+
def https_open(self, req):
|
|
201
|
+
def connection(host: str, **kwargs):
|
|
202
|
+
return _PolicyHTTPSConnection(host, policy=self.policy, **kwargs)
|
|
203
|
+
|
|
204
|
+
return self.do_open(
|
|
205
|
+
connection,
|
|
206
|
+
req,
|
|
207
|
+
context=self._context,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def build_policy_opener(
|
|
212
|
+
policy: NetworkPolicy,
|
|
213
|
+
*,
|
|
214
|
+
context: ssl.SSLContext | None = None,
|
|
215
|
+
) -> OpenerDirector:
|
|
216
|
+
handlers: list[object] = [
|
|
217
|
+
PolicyRedirectHandler(policy),
|
|
218
|
+
PolicyHTTPHandler(policy),
|
|
219
|
+
PolicyHTTPSHandler(policy, context=context),
|
|
220
|
+
]
|
|
221
|
+
return build_opener(*handlers)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def open_url(
|
|
225
|
+
request: Request,
|
|
226
|
+
*,
|
|
227
|
+
policy: NetworkPolicy,
|
|
228
|
+
timeout: float | None,
|
|
229
|
+
context: ssl.SSLContext | None = None,
|
|
230
|
+
):
|
|
231
|
+
policy.validate_url(request.full_url)
|
|
232
|
+
return build_policy_opener(policy, context=context).open(request, timeout=timeout)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Office-application services layered on top of the python-hwpx core."""
|
|
3
|
+
|
|
4
|
+
from .rendering import (
|
|
5
|
+
HancomRenderBackend,
|
|
6
|
+
bind_document_rendering,
|
|
7
|
+
resolve_hancom_backend,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"HancomRenderBackend",
|
|
12
|
+
"bind_document_rendering",
|
|
13
|
+
"resolve_hancom_backend",
|
|
14
|
+
]
|