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.
Files changed (217) hide show
  1. hwpx_automation/__init__.py +61 -0
  2. hwpx_automation/__init__.pyi +27 -0
  3. hwpx_automation/__main__.py +8 -0
  4. hwpx_automation/agent_document.py +392 -0
  5. hwpx_automation/api.py +136 -0
  6. hwpx_automation/blind_eval.py +407 -0
  7. hwpx_automation/capabilities.py +110 -0
  8. hwpx_automation/compat.py +48 -0
  9. hwpx_automation/configuration.py +60 -0
  10. hwpx_automation/core/__init__.py +2 -0
  11. hwpx_automation/core/content.py +762 -0
  12. hwpx_automation/core/context.py +111 -0
  13. hwpx_automation/core/diff.py +53 -0
  14. hwpx_automation/core/document.py +37 -0
  15. hwpx_automation/core/formatting.py +513 -0
  16. hwpx_automation/core/handles.py +24 -0
  17. hwpx_automation/core/locations.py +205 -0
  18. hwpx_automation/core/locator.py +162 -0
  19. hwpx_automation/core/plan.py +680 -0
  20. hwpx_automation/core/resources.py +42 -0
  21. hwpx_automation/core/search.py +296 -0
  22. hwpx_automation/core/transactions.py +434 -0
  23. hwpx_automation/core/txn.py +48 -0
  24. hwpx_automation/document_state.py +95 -0
  25. hwpx_automation/errors.py +174 -0
  26. hwpx_automation/execution_lock.py +15 -0
  27. hwpx_automation/fastmcp_adapter.py +672 -0
  28. hwpx_automation/form_fill.py +1177 -0
  29. hwpx_automation/form_output_models.py +223 -0
  30. hwpx_automation/handlers/__init__.py +2 -0
  31. hwpx_automation/handlers/_shared.py +377 -0
  32. hwpx_automation/handlers/agent_document.py +257 -0
  33. hwpx_automation/handlers/authoring.py +750 -0
  34. hwpx_automation/handlers/content_edit.py +1078 -0
  35. hwpx_automation/handlers/form_fill.py +607 -0
  36. hwpx_automation/handlers/layout_style.py +660 -0
  37. hwpx_automation/handlers/quality_render.py +566 -0
  38. hwpx_automation/handlers/read_export.py +1295 -0
  39. hwpx_automation/handlers/specialized.py +624 -0
  40. hwpx_automation/handlers/tracked_changes.py +589 -0
  41. hwpx_automation/handlers/workflow.py +105 -0
  42. hwpx_automation/hwp_converter.py +227 -0
  43. hwpx_automation/hwp_support.py +94 -0
  44. hwpx_automation/hwpx_ops.py +1439 -0
  45. hwpx_automation/identity.json +263 -0
  46. hwpx_automation/identity.py +18 -0
  47. hwpx_automation/ingest_adapters.py +85 -0
  48. hwpx_automation/markdown_plan.py +216 -0
  49. hwpx_automation/mcp_cli.py +29 -0
  50. hwpx_automation/metadata/tools_meta.py +40 -0
  51. hwpx_automation/mixed_form.py +3007 -0
  52. hwpx_automation/mutation_models.py +401 -0
  53. hwpx_automation/network_policy.py +232 -0
  54. hwpx_automation/office/__init__.py +14 -0
  55. hwpx_automation/office/agent/__init__.py +125 -0
  56. hwpx_automation/office/agent/_batch_verification.py +383 -0
  57. hwpx_automation/office/agent/blueprint/__init__.py +58 -0
  58. hwpx_automation/office/agent/blueprint/bundle.py +282 -0
  59. hwpx_automation/office/agent/blueprint/catalog.py +136 -0
  60. hwpx_automation/office/agent/blueprint/dump.py +520 -0
  61. hwpx_automation/office/agent/blueprint/mapping.py +312 -0
  62. hwpx_automation/office/agent/blueprint/model.py +722 -0
  63. hwpx_automation/office/agent/blueprint/native.py +621 -0
  64. hwpx_automation/office/agent/blueprint/replay.py +622 -0
  65. hwpx_automation/office/agent/catalog.py +252 -0
  66. hwpx_automation/office/agent/cli.py +647 -0
  67. hwpx_automation/office/agent/commands.py +1383 -0
  68. hwpx_automation/office/agent/document.py +801 -0
  69. hwpx_automation/office/agent/form_plan.py +1760 -0
  70. hwpx_automation/office/agent/model.py +808 -0
  71. hwpx_automation/office/agent/path.py +155 -0
  72. hwpx_automation/office/agent/query.py +230 -0
  73. hwpx_automation/office/agent/story.py +207 -0
  74. hwpx_automation/office/authoring/__init__.py +3542 -0
  75. hwpx_automation/office/authoring/advanced_generators.py +154 -0
  76. hwpx_automation/office/authoring/builder/__init__.py +52 -0
  77. hwpx_automation/office/authoring/builder/core.py +996 -0
  78. hwpx_automation/office/authoring/builder/report.py +195 -0
  79. hwpx_automation/office/authoring/design/__init__.py +30 -0
  80. hwpx_automation/office/authoring/design/_support.py +144 -0
  81. hwpx_automation/office/authoring/design/composer.py +282 -0
  82. hwpx_automation/office/authoring/design/harvest.py +305 -0
  83. hwpx_automation/office/authoring/design/plan.py +69 -0
  84. hwpx_automation/office/authoring/design/profile.py +88 -0
  85. hwpx_automation/office/authoring/design/profiles/application_form/fragments/body.xml +1 -0
  86. hwpx_automation/office/authoring/design/profiles/application_form/fragments/heading.xml +1 -0
  87. hwpx_automation/office/authoring/design/profiles/application_form/fragments/info_table.xml +1 -0
  88. hwpx_automation/office/authoring/design/profiles/application_form/fragments/title.xml +1 -0
  89. hwpx_automation/office/authoring/design/profiles/application_form/profile.json +25 -0
  90. hwpx_automation/office/authoring/design/profiles/application_form/template.hwpx +0 -0
  91. hwpx_automation/office/authoring/design/profiles/home_notice/fragments/body.xml +1 -0
  92. hwpx_automation/office/authoring/design/profiles/home_notice/fragments/heading.xml +1 -0
  93. hwpx_automation/office/authoring/design/profiles/home_notice/fragments/title.xml +1 -0
  94. hwpx_automation/office/authoring/design/profiles/home_notice/profile.json +24 -0
  95. hwpx_automation/office/authoring/design/profiles/home_notice/template.hwpx +0 -0
  96. hwpx_automation/office/authoring/design/profiles/official_notice/fragments/body.xml +1 -0
  97. hwpx_automation/office/authoring/design/profiles/official_notice/fragments/heading.xml +1 -0
  98. hwpx_automation/office/authoring/design/profiles/official_notice/fragments/info_table.xml +1 -0
  99. hwpx_automation/office/authoring/design/profiles/official_notice/fragments/title.xml +1 -0
  100. hwpx_automation/office/authoring/design/profiles/official_notice/profile.json +25 -0
  101. hwpx_automation/office/authoring/design/profiles/official_notice/template.hwpx +0 -0
  102. hwpx_automation/office/authoring/design/profiles/report/fragments/body.xml +1 -0
  103. hwpx_automation/office/authoring/design/profiles/report/fragments/heading.xml +1 -0
  104. hwpx_automation/office/authoring/design/profiles/report/fragments/info_table.xml +1 -0
  105. hwpx_automation/office/authoring/design/profiles/report/fragments/title.xml +1 -0
  106. hwpx_automation/office/authoring/design/profiles/report/profile.json +25 -0
  107. hwpx_automation/office/authoring/design/profiles/report/template.hwpx +0 -0
  108. hwpx_automation/office/authoring/design/validator.py +107 -0
  109. hwpx_automation/office/authoring/presets/__init__.py +22 -0
  110. hwpx_automation/office/authoring/presets/proposal.py +538 -0
  111. hwpx_automation/office/authoring/report_parser.py +141 -0
  112. hwpx_automation/office/authoring/style_profile.py +437 -0
  113. hwpx_automation/office/authoring/template_analyzer.py +657 -0
  114. hwpx_automation/office/compliance/__init__.py +38 -0
  115. hwpx_automation/office/compliance/official_lint.py +478 -0
  116. hwpx_automation/office/compliance/pii.py +388 -0
  117. hwpx_automation/office/document_ops/__init__.py +13 -0
  118. hwpx_automation/office/document_ops/comparison.py +62 -0
  119. hwpx_automation/office/document_ops/mail_merge.py +73 -0
  120. hwpx_automation/office/document_ops/redline.py +35 -0
  121. hwpx_automation/office/evalplan/__init__.py +36 -0
  122. hwpx_automation/office/evalplan/runtime.py +2762 -0
  123. hwpx_automation/office/exam/__init__.py +44 -0
  124. hwpx_automation/office/exam/compose.py +282 -0
  125. hwpx_automation/office/exam/ir.py +44 -0
  126. hwpx_automation/office/exam/measure.py +163 -0
  127. hwpx_automation/office/exam/parser.py +150 -0
  128. hwpx_automation/office/exam/profile.py +123 -0
  129. hwpx_automation/office/form_fill/__init__.py +66 -0
  130. hwpx_automation/office/form_fill/classification.py +108 -0
  131. hwpx_automation/office/form_fill/fill_residue.py +242 -0
  132. hwpx_automation/office/form_fill/fit/__init__.py +36 -0
  133. hwpx_automation/office/form_fill/fit/apply.py +24 -0
  134. hwpx_automation/office/form_fill/fit/engine.py +24 -0
  135. hwpx_automation/office/form_fill/fit/measure.py +50 -0
  136. hwpx_automation/office/form_fill/fit/policy.py +28 -0
  137. hwpx_automation/office/form_fill/fit/report.py +28 -0
  138. hwpx_automation/office/form_fill/fit/seal.py +457 -0
  139. hwpx_automation/office/form_fill/fit/wordbox.py +1343 -0
  140. hwpx_automation/office/form_fill/guidance.py +704 -0
  141. hwpx_automation/office/form_fill/quality.py +961 -0
  142. hwpx_automation/office/form_fill/split_run.py +333 -0
  143. hwpx_automation/office/form_fill/template_formfit.py +656 -0
  144. hwpx_automation/office/house_style/__init__.py +196 -0
  145. hwpx_automation/office/house_style/composition.py +68 -0
  146. hwpx_automation/office/house_style/data/bank.json +625 -0
  147. hwpx_automation/office/house_style/data/genres.json +43 -0
  148. hwpx_automation/office/quality/__init__.py +14 -0
  149. hwpx_automation/office/quality/page_guard.py +277 -0
  150. hwpx_automation/office/rendering/__init__.py +145 -0
  151. hwpx_automation/office/rendering/_hancom_open_rate.ps1 +374 -0
  152. hwpx_automation/office/rendering/_refresh_hwpx_mac.applescript +162 -0
  153. hwpx_automation/office/rendering/_render_hwpx.ps1 +72 -0
  154. hwpx_automation/office/rendering/_render_hwpx_mac.applescript +249 -0
  155. hwpx_automation/office/rendering/block_splits.py +76 -0
  156. hwpx_automation/office/rendering/detectors.py +151 -0
  157. hwpx_automation/office/rendering/diff.py +153 -0
  158. hwpx_automation/office/rendering/fixture_corpus.py +215 -0
  159. hwpx_automation/office/rendering/oracle.py +909 -0
  160. hwpx_automation/office/rendering/page_qa.py +245 -0
  161. hwpx_automation/office/rendering/qa_contracts.py +293 -0
  162. hwpx_automation/office/rendering/qa_metrics.py +241 -0
  163. hwpx_automation/office/rendering/worker.py +290 -0
  164. hwpx_automation/office/utilities/__init__.py +12 -0
  165. hwpx_automation/office/utilities/table_compute.py +477 -0
  166. hwpx_automation/ops_services/__init__.py +1 -0
  167. hwpx_automation/ops_services/_border_fill.py +283 -0
  168. hwpx_automation/ops_services/composition.py +55 -0
  169. hwpx_automation/ops_services/content_layout.py +322 -0
  170. hwpx_automation/ops_services/context.py +213 -0
  171. hwpx_automation/ops_services/form_fields.py +557 -0
  172. hwpx_automation/ops_services/media.py +178 -0
  173. hwpx_automation/ops_services/memo_style.py +477 -0
  174. hwpx_automation/ops_services/package_validation.py +166 -0
  175. hwpx_automation/ops_services/planning.py +201 -0
  176. hwpx_automation/ops_services/preview_export.py +585 -0
  177. hwpx_automation/ops_services/read_query.py +601 -0
  178. hwpx_automation/ops_services/save_policy.py +604 -0
  179. hwpx_automation/ops_services/tables.py +539 -0
  180. hwpx_automation/ops_services/transactions.py +616 -0
  181. hwpx_automation/preview_output_models.py +69 -0
  182. hwpx_automation/public-modules.json +206 -0
  183. hwpx_automation/py.typed +1 -0
  184. hwpx_automation/quality.py +351 -0
  185. hwpx_automation/quality_generation.py +725 -0
  186. hwpx_automation/runtime.py +321 -0
  187. hwpx_automation/runtime_services.py +100 -0
  188. hwpx_automation/server.py +259 -0
  189. hwpx_automation/storage.py +747 -0
  190. hwpx_automation/tool_bindings.py +170 -0
  191. hwpx_automation/tool_contract.py +982 -0
  192. hwpx_automation/upstream.py +755 -0
  193. hwpx_automation/utils/__init__.py +2 -0
  194. hwpx_automation/utils/helpers.py +29 -0
  195. hwpx_automation/visual_qa.py +667 -0
  196. hwpx_automation/workflow/__init__.py +55 -0
  197. hwpx_automation/workflow/adapters.py +482 -0
  198. hwpx_automation/workflow/dispatcher.py +213 -0
  199. hwpx_automation/workflow/models.py +243 -0
  200. hwpx_automation/workflow/policy.py +198 -0
  201. hwpx_automation/workflow/render_contracts.py +173 -0
  202. hwpx_automation/workflow/render_metrics.py +196 -0
  203. hwpx_automation/workflow/render_queue.py +482 -0
  204. hwpx_automation/workflow/render_security.py +172 -0
  205. hwpx_automation/workflow/render_transport.py +369 -0
  206. hwpx_automation/workflow/rendering.py +206 -0
  207. hwpx_automation/workflow/service.py +758 -0
  208. hwpx_automation/workflow/state_machine.py +65 -0
  209. hwpx_automation/workflow/store.py +747 -0
  210. hwpx_automation/workspace.py +1694 -0
  211. python_hwpx_automation-6.0.3.dist-info/METADATA +279 -0
  212. python_hwpx_automation-6.0.3.dist-info/RECORD +217 -0
  213. python_hwpx_automation-6.0.3.dist-info/WHEEL +5 -0
  214. python_hwpx_automation-6.0.3.dist-info/entry_points.txt +3 -0
  215. python_hwpx_automation-6.0.3.dist-info/licenses/LICENSE +178 -0
  216. python_hwpx_automation-6.0.3.dist-info/licenses/NOTICE +14 -0
  217. 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
+ ]