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,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`."""
|