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,477 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """General table calculation helpers."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import re
7
+ from collections import OrderedDict
8
+ from collections.abc import Mapping, Sequence
9
+ from dataclasses import dataclass
10
+ from decimal import Decimal, InvalidOperation
11
+ from typing import Any
12
+
13
+ TABLE_COMPUTE_REPORT_VERSION = "table-compute-v1"
14
+
15
+ _NUMERIC_CLEAN_RE = re.compile(r"[,\s원%]")
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class _Column:
20
+ key: str
21
+ label: str
22
+ index: int
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class _NormalizedTable:
27
+ mode: str
28
+ columns: list[_Column]
29
+ rows: list[dict[str, Any]]
30
+ original: Mapping[str, Any] | Sequence[Any]
31
+
32
+
33
+ def table_compute(
34
+ table: Mapping[str, Any] | Sequence[Mapping[str, Any]] | Sequence[Sequence[Any]],
35
+ *,
36
+ value_columns: Sequence[str | int] | None = None,
37
+ operations: Sequence[str] | None = None,
38
+ append: str = "rows",
39
+ group_by: str | int | None = None,
40
+ label_column: str | int | None = None,
41
+ labels: Mapping[str, str] | None = None,
42
+ ) -> dict[str, Any]:
43
+ """Compute sum, average, and subtotal rows or columns for a general table."""
44
+
45
+ normalized = _normalize_table(table)
46
+ operation_names = _normalize_operations(operations)
47
+ append_mode = _normalize_append(append)
48
+ label_map = {
49
+ "sum": "합계",
50
+ "average": "평균",
51
+ "subtotal": "소계",
52
+ **{str(key): str(value) for key, value in (labels or {}).items()},
53
+ }
54
+ label_key = _resolve_column(label_column, normalized.columns) if label_column is not None else normalized.columns[0].key
55
+ group_key = _resolve_column(group_by, normalized.columns) if group_by is not None else None
56
+ numeric_keys = _resolve_value_columns(value_columns, normalized, label_key=label_key, group_key=group_key)
57
+
58
+ rows = [dict(row) for row in normalized.rows]
59
+ evidence: list[dict[str, Any]] = []
60
+ warnings: list[str] = []
61
+
62
+ if append_mode in {"columns", "both"}:
63
+ _append_computed_columns(
64
+ rows,
65
+ columns=normalized.columns,
66
+ numeric_keys=numeric_keys,
67
+ operations=operation_names,
68
+ labels=label_map,
69
+ evidence=evidence,
70
+ warnings=warnings,
71
+ )
72
+
73
+ if append_mode in {"rows", "both"}:
74
+ rows = _append_computed_rows(
75
+ rows,
76
+ columns=normalized.columns,
77
+ numeric_keys=numeric_keys,
78
+ operations=operation_names,
79
+ label_key=label_key,
80
+ group_key=group_key,
81
+ labels=label_map,
82
+ evidence=evidence,
83
+ warnings=warnings,
84
+ )
85
+
86
+ computed = _restore_table(normalized, rows)
87
+ return {
88
+ "report_version": TABLE_COMPUTE_REPORT_VERSION,
89
+ "ok": not warnings,
90
+ "computedTable": computed,
91
+ "operations": operation_names,
92
+ "append": append_mode,
93
+ "valueColumns": numeric_keys,
94
+ "groupBy": group_key,
95
+ "evidence": evidence,
96
+ "warnings": warnings,
97
+ }
98
+
99
+
100
+ def _normalize_operations(operations: Sequence[str] | None) -> list[str]:
101
+ raw = operations or ("sum",)
102
+ normalized: list[str] = []
103
+ aliases = {"avg": "average", "mean": "average", "total": "sum", "sub_total": "subtotal"}
104
+ for item in raw:
105
+ op = aliases.get(str(item).strip().lower().replace("-", "_"), str(item).strip().lower().replace("-", "_"))
106
+ if op not in {"sum", "average", "subtotal"}:
107
+ raise ValueError(f"unsupported table operation: {item!r}")
108
+ if op not in normalized:
109
+ normalized.append(op)
110
+ return normalized
111
+
112
+
113
+ def _normalize_append(value: str) -> str:
114
+ normalized = str(value or "rows").strip().lower()
115
+ aliases = {"row": "rows", "column": "columns", "cols": "columns"}
116
+ normalized = aliases.get(normalized, normalized)
117
+ if normalized not in {"rows", "columns", "both"}:
118
+ raise ValueError("append must be rows, columns, or both")
119
+ return normalized
120
+
121
+
122
+ def _normalize_table(
123
+ table: Mapping[str, Any] | Sequence[Mapping[str, Any]] | Sequence[Sequence[Any]],
124
+ ) -> _NormalizedTable:
125
+ if isinstance(table, Mapping):
126
+ if "columns" in table:
127
+ columns = [
128
+ _Column(
129
+ key=str(column.get("key") or column.get("label") or index),
130
+ label=str(column.get("label") or column.get("key") or index),
131
+ index=index,
132
+ )
133
+ for index, column in enumerate(table.get("columns") or [])
134
+ if isinstance(column, Mapping)
135
+ ]
136
+ rows = [
137
+ {column.key: row.get(column.key, "") for column in columns}
138
+ for row in table.get("rows") or []
139
+ if isinstance(row, Mapping)
140
+ ]
141
+ return _NormalizedTable("plan_columns", columns, rows, table)
142
+ if "header" in table:
143
+ header = [str(item) for item in table.get("header") or []]
144
+ columns = [_Column(key=label or str(index), label=label or str(index), index=index) for index, label in enumerate(header)]
145
+ rows = [_row_sequence_to_mapping(row, columns) for row in table.get("rows") or []]
146
+ return _NormalizedTable("plan_header", columns, rows, table)
147
+ if "rows" in table:
148
+ return _normalize_table(table.get("rows") or [])
149
+ raise ValueError("table mapping must contain columns, header, or rows")
150
+
151
+ rows_list = list(table)
152
+ if not rows_list:
153
+ raise ValueError("table must contain at least one row")
154
+ if all(isinstance(row, Mapping) for row in rows_list):
155
+ keys: OrderedDict[str, None] = OrderedDict()
156
+ for row in rows_list:
157
+ for key in row.keys(): # type: ignore[attr-defined]
158
+ keys.setdefault(str(key), None)
159
+ columns = [_Column(key=key, label=key, index=index) for index, key in enumerate(keys)]
160
+ rows = [
161
+ {column.key: row.get(column.key, "") for column in columns} # type: ignore[attr-defined]
162
+ for row in rows_list
163
+ ]
164
+ return _NormalizedTable("rows_mapping", columns, rows, rows_list)
165
+
166
+ first = rows_list[0]
167
+ if not isinstance(first, Sequence) or isinstance(first, (str, bytes, bytearray)):
168
+ raise TypeError("table rows must be mappings or sequences")
169
+ width = max(len(row) for row in rows_list if isinstance(row, Sequence) and not isinstance(row, (str, bytes, bytearray)))
170
+ columns = [_Column(key=str(index), label=str(index), index=index) for index in range(width)]
171
+ rows = [_row_sequence_to_mapping(row, columns) for row in rows_list] # type: ignore[arg-type]
172
+ return _NormalizedTable("rows_sequence", columns, rows, rows_list)
173
+
174
+
175
+ def _row_sequence_to_mapping(row: Sequence[Any], columns: Sequence[_Column]) -> dict[str, Any]:
176
+ return {
177
+ column.key: row[column.index] if column.index < len(row) else ""
178
+ for column in columns
179
+ }
180
+
181
+
182
+ def _resolve_value_columns(
183
+ value_columns: Sequence[str | int] | None,
184
+ table: _NormalizedTable,
185
+ *,
186
+ label_key: str,
187
+ group_key: str | None,
188
+ ) -> list[str]:
189
+ if value_columns:
190
+ return [_resolve_column(column, table.columns) for column in value_columns]
191
+ skipped = {label_key}
192
+ if group_key:
193
+ skipped.add(group_key)
194
+ detected: list[str] = []
195
+ for column in table.columns:
196
+ if column.key in skipped:
197
+ continue
198
+ if any(_to_decimal(row.get(column.key)) is not None for row in table.rows):
199
+ detected.append(column.key)
200
+ if not detected:
201
+ raise ValueError("no numeric value columns were found")
202
+ return detected
203
+
204
+
205
+ def _resolve_column(value: str | int, columns: Sequence[_Column]) -> str:
206
+ if isinstance(value, int):
207
+ for column in columns:
208
+ if column.index == value:
209
+ return column.key
210
+ raise ValueError(f"column index out of range: {value}")
211
+ text = str(value)
212
+ for column in columns:
213
+ if text in {column.key, column.label, str(column.index)}:
214
+ return column.key
215
+ raise ValueError(f"unknown table column: {value!r}")
216
+
217
+
218
+ def _append_computed_columns(
219
+ rows: list[dict[str, Any]],
220
+ *,
221
+ columns: list[_Column],
222
+ numeric_keys: Sequence[str],
223
+ operations: Sequence[str],
224
+ labels: Mapping[str, str],
225
+ evidence: list[dict[str, Any]],
226
+ warnings: list[str],
227
+ ) -> None:
228
+ for op in operations:
229
+ if op == "subtotal":
230
+ continue
231
+ key = _unique_column_key(labels[op], columns, rows)
232
+ columns.append(_Column(key=key, label=labels[op], index=len(columns)))
233
+ for row_index, row in enumerate(rows):
234
+ values = _numeric_values(row, numeric_keys, row_index=row_index, warnings=warnings)
235
+ result = _calculate(op, values)
236
+ row[key] = _format_decimal(result) if result is not None else ""
237
+ evidence.append(
238
+ {
239
+ "operation": op,
240
+ "axis": "columns",
241
+ "rowIndex": row_index,
242
+ "sourceColumns": list(numeric_keys),
243
+ "resultColumn": key,
244
+ "sourceValueCount": len(values),
245
+ "result": row[key],
246
+ }
247
+ )
248
+
249
+
250
+ def _append_computed_rows(
251
+ source_rows: list[dict[str, Any]],
252
+ *,
253
+ columns: Sequence[_Column],
254
+ numeric_keys: Sequence[str],
255
+ operations: Sequence[str],
256
+ label_key: str,
257
+ group_key: str | None,
258
+ labels: Mapping[str, str],
259
+ evidence: list[dict[str, Any]],
260
+ warnings: list[str],
261
+ ) -> list[dict[str, Any]]:
262
+ rows = list(source_rows)
263
+ if "subtotal" in operations and group_key:
264
+ rows = _insert_subtotal_rows(
265
+ rows,
266
+ columns=columns,
267
+ numeric_keys=numeric_keys,
268
+ label_key=label_key,
269
+ group_key=group_key,
270
+ label=labels["subtotal"],
271
+ evidence=evidence,
272
+ warnings=warnings,
273
+ )
274
+ elif "subtotal" in operations:
275
+ warnings.append("subtotal requested without group_by")
276
+
277
+ summary_rows: list[dict[str, Any]] = []
278
+ for op in operations:
279
+ if op == "subtotal":
280
+ continue
281
+ summary_rows.append(
282
+ _summary_row(
283
+ source_rows,
284
+ columns=columns,
285
+ numeric_keys=numeric_keys,
286
+ operation=op,
287
+ label_key=label_key,
288
+ label=labels[op],
289
+ evidence=evidence,
290
+ warnings=warnings,
291
+ )
292
+ )
293
+ return [*rows, *summary_rows]
294
+
295
+
296
+ def _insert_subtotal_rows(
297
+ rows: list[dict[str, Any]],
298
+ *,
299
+ columns: Sequence[_Column],
300
+ numeric_keys: Sequence[str],
301
+ label_key: str,
302
+ group_key: str,
303
+ label: str,
304
+ evidence: list[dict[str, Any]],
305
+ warnings: list[str],
306
+ ) -> list[dict[str, Any]]:
307
+ result: list[dict[str, Any]] = []
308
+ current_group: Any = object()
309
+ group_rows: list[dict[str, Any]] = []
310
+ group_start = 0
311
+
312
+ def flush(group: Any) -> None:
313
+ if not group_rows:
314
+ return
315
+ subtotal = _blank_row(columns)
316
+ subtotal[label_key] = f"{group} {label}".strip()
317
+ if group_key != label_key:
318
+ subtotal[group_key] = group
319
+ for key in numeric_keys:
320
+ values = _numeric_values(group_rows, [key], row_index=group_start, warnings=warnings)
321
+ value = _calculate("sum", values)
322
+ subtotal[key] = _format_decimal(value) if value is not None else ""
323
+ evidence.append(
324
+ {
325
+ "operation": "subtotal",
326
+ "axis": "rows",
327
+ "groupBy": group_key,
328
+ "group": group,
329
+ "column": key,
330
+ "sourceRowStart": group_start,
331
+ "sourceRowCount": len(group_rows),
332
+ "sourceValueCount": len(values),
333
+ "result": subtotal[key],
334
+ }
335
+ )
336
+ result.append(subtotal)
337
+
338
+ for row_index, row in enumerate(rows):
339
+ group = row.get(group_key, "")
340
+ if group_rows and group != current_group:
341
+ flush(current_group)
342
+ group_rows = []
343
+ group_start = row_index
344
+ if not group_rows:
345
+ current_group = group
346
+ group_start = row_index
347
+ result.append(row)
348
+ group_rows.append(row)
349
+ flush(current_group)
350
+ return result
351
+
352
+
353
+ def _summary_row(
354
+ rows: list[dict[str, Any]],
355
+ *,
356
+ columns: Sequence[_Column],
357
+ numeric_keys: Sequence[str],
358
+ operation: str,
359
+ label_key: str,
360
+ label: str,
361
+ evidence: list[dict[str, Any]],
362
+ warnings: list[str],
363
+ ) -> dict[str, Any]:
364
+ row = _blank_row(columns)
365
+ row[label_key] = label
366
+ for key in numeric_keys:
367
+ values = _numeric_values(rows, [key], row_index=0, warnings=warnings)
368
+ value = _calculate(operation, values)
369
+ row[key] = _format_decimal(value) if value is not None else ""
370
+ evidence.append(
371
+ {
372
+ "operation": operation,
373
+ "axis": "rows",
374
+ "column": key,
375
+ "sourceRowStart": 0,
376
+ "sourceRowCount": len(rows),
377
+ "sourceValueCount": len(values),
378
+ "result": row[key],
379
+ }
380
+ )
381
+ return row
382
+
383
+
384
+ def _blank_row(columns: Sequence[_Column]) -> dict[str, Any]:
385
+ return {column.key: "" for column in columns}
386
+
387
+
388
+ def _numeric_values(
389
+ rows_or_row: list[dict[str, Any]] | dict[str, Any],
390
+ keys: Sequence[str],
391
+ *,
392
+ row_index: int,
393
+ warnings: list[str],
394
+ ) -> list[Decimal]:
395
+ rows = rows_or_row if isinstance(rows_or_row, list) else [rows_or_row]
396
+ values: list[Decimal] = []
397
+ for offset, row in enumerate(rows):
398
+ for key in keys:
399
+ value = _to_decimal(row.get(key))
400
+ if value is None:
401
+ if row.get(key) not in (None, ""):
402
+ warnings.append(f"non-numeric value skipped at row {row_index + offset}, column {key}")
403
+ continue
404
+ values.append(value)
405
+ return values
406
+
407
+
408
+ def _calculate(operation: str, values: Sequence[Decimal]) -> Decimal | None:
409
+ if not values:
410
+ return None
411
+ total = sum(values, Decimal("0"))
412
+ if operation == "sum":
413
+ return total
414
+ if operation == "average":
415
+ return total / Decimal(len(values))
416
+ raise ValueError(f"unsupported calculation: {operation}")
417
+
418
+
419
+ def _to_decimal(value: Any) -> Decimal | None:
420
+ if value is None or value == "":
421
+ return None
422
+ text = _NUMERIC_CLEAN_RE.sub("", str(value))
423
+ if text == "":
424
+ return None
425
+ try:
426
+ return Decimal(text)
427
+ except (InvalidOperation, ValueError):
428
+ return None
429
+
430
+
431
+ def _format_decimal(value: Decimal | None) -> str:
432
+ if value is None:
433
+ return ""
434
+ normalized = value.normalize()
435
+ if normalized == normalized.to_integral():
436
+ return f"{int(normalized):,}"
437
+ text = format(normalized, "f").rstrip("0").rstrip(".")
438
+ integer, fraction = text.split(".", 1)
439
+ return f"{int(integer):,}.{fraction}"
440
+
441
+
442
+ def _unique_column_key(label: str, columns: Sequence[_Column], rows: Sequence[Mapping[str, Any]]) -> str:
443
+ base = label or "computed"
444
+ existing = {column.key for column in columns}
445
+ for row in rows:
446
+ existing.update(str(key) for key in row.keys())
447
+ key = base
448
+ counter = 2
449
+ while key in existing:
450
+ key = f"{base}_{counter}"
451
+ counter += 1
452
+ return key
453
+
454
+
455
+ def _restore_table(normalized: _NormalizedTable, rows: list[dict[str, Any]]) -> Any:
456
+ if normalized.mode == "plan_columns":
457
+ original = dict(normalized.original)
458
+ original["columns"] = [
459
+ {"key": column.key, "label": column.label}
460
+ for column in normalized.columns
461
+ ]
462
+ original["rows"] = rows
463
+ return original
464
+ if normalized.mode == "plan_header":
465
+ original = dict(normalized.original)
466
+ original["header"] = [column.label for column in normalized.columns]
467
+ original["rows"] = [[row.get(column.key, "") for column in normalized.columns] for row in rows]
468
+ return original
469
+ if normalized.mode == "rows_sequence":
470
+ return [[row.get(column.key, "") for column in normalized.columns] for row in rows]
471
+ return rows
472
+
473
+
474
+ __all__ = [
475
+ "TABLE_COMPUTE_REPORT_VERSION",
476
+ "table_compute",
477
+ ]
@@ -0,0 +1 @@
1
+ """Bounded implementation owners behind :class:`hwpx_automation.hwpx_ops.HwpxOps`."""