forked from balisujohn/localwriter
-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathunits.py
More file actions
272 lines (222 loc) · 9.07 KB
/
Copy pathunits.py
File metadata and controls
272 lines (222 loc) · 9.07 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# WriterAgent - AI Writing Assistant for LibreOffice
# Copyright (c) 2026 KeithCu (modifications and relicensing)
#
# SPDX-License-Identifier: GPL-3.0-or-later
"""Units helper templates, host RPC, and document egress (LO host).
Compute is lazy-loaded from ``plugin.scripting.venv.units`` via ``__getattr__``.
"""
from __future__ import annotations
from typing import Any
from plugin.doc.document_helpers import is_calc, is_writer
from plugin.framework.errors import ToolExecutionError
from plugin.framework.i18n import _
from plugin.scripting._lazy_venv import make_getattr
from plugin.scripting.client import run_units as client_run_units
from plugin.scripting.helper_domain import (
HelperScriptMeta,
header_prefix,
)
from plugin.scripting.calc_functions_common import UNITS_HELPER_NAMES as HELPER_NAMES
UNITS_HEADER_PREFIX = header_prefix("units")
_SHIPPED_TEMPLATES = frozenset({"convert_quantity", "parse_quantity", "check_dimensionality"})
_DEFAULT_PARAMS: dict[str, dict[str, Any]] = {
"convert_quantity": {"value": 10, "from": "m/s", "to": "km/h"},
"parse_quantity": {"quantity": "10 m/s"},
"check_dimensionality": {"quantity_a": "10 m/s", "quantity_b": "5 km/h"},
}
_HELPER_DESCRIPTIONS: dict[str, str] = {
"convert_quantity": "Convert a numeric value between units.",
"parse_quantity": "Parse a quantity string (e.g. '10 m/s') into magnitude and units.",
"format_quantity": "Format a magnitude and unit string for display.",
"check_dimensionality": "Check whether two quantities or units are dimensionally compatible.",
}
_UNITS_VENV_EXPORTS = frozenset(
{
"check_dimensionality",
"convert_quantity",
"format_quantity",
"parse_quantity",
"run_units",
}
)
__getattr__ = make_getattr("units", _UNITS_VENV_EXPORTS)
OUTPUT_STYLES = frozenset({"formatted", "detailed"})
_FORMATTED_DEFAULT_HELPERS = frozenset({"convert_quantity", "parse_quantity"})
_EGRESS_PARAM_KEYS = frozenset({"output_style"})
# --- Templates ---
from plugin.scripting.helper_domain import DomainFacadeConfig, make_template_api
UnitsScriptMeta = HelperScriptMeta
_API = make_template_api(
DomainFacadeConfig(
tag="units",
helper_names=HELPER_NAMES,
default_params=_DEFAULT_PARAMS,
descriptions=_HELPER_DESCRIPTIONS,
import_module="writeragent.scripting.units",
run_name="run_units",
shipped_templates=_SHIPPED_TEMPLATES,
data_expr="None",
positional_args={
"convert_quantity": ("value", "from", "to"),
"parse_quantity": ("quantity",),
"check_dimensionality": ("quantity_a", "quantity_b"),
},
)
)
_template_body = _API.template_body
get_units_script_templates = _API.get_templates
parse_units_script_header = _API.parse_header
# --- Runner ---
def supports_units_manual(doc: Any) -> bool:
"""True when Run Python Script should expose Units Helpers for *doc*."""
if doc is None:
return False
try:
return is_writer(doc) or is_calc(doc)
except Exception:
return False
def run_trusted_units(
uno_ctx: Any,
doc: Any,
*,
helper: str,
params: dict[str, Any] | None = None,
task_hint: str | None = None,
) -> dict[str, Any]:
"""Run a trusted units helper in the user venv."""
name = str(helper or "").strip()
if not name:
raise ToolExecutionError("helper is required", code="UNITS_ERROR")
if name not in HELPER_NAMES:
raise ToolExecutionError(f"Unknown helper {name!r}", code="UNITS_ERROR")
if not is_calc(doc) and not is_writer(doc):
raise ToolExecutionError("Units helpers require a Writer or Calc document.", code="UNITS_ERROR")
spec: dict[str, Any] = {"helper": name}
clean_params, _output_style = split_helper_params(params if isinstance(params, dict) else None)
if clean_params:
spec["params"] = clean_params
context: dict[str, Any] = {}
if task_hint:
context["task_hint"] = str(task_hint)
return client_run_units(uno_ctx, spec, None, context=context or None)
# --- Egress ---
def resolve_output_style(helper: str, output_style: str | None) -> str:
"""Resolve Calc egress layout: formatted (single cell) or detailed (key-value grid)."""
if output_style in OUTPUT_STYLES:
return output_style
if helper in _FORMATTED_DEFAULT_HELPERS:
return "formatted"
return "detailed"
def split_helper_params(params: dict[str, Any] | None) -> tuple[dict[str, Any], str | None]:
"""Strip egress-only keys before dispatching to Pint helpers."""
if not isinstance(params, dict):
return {}, None
clean = dict(params)
raw_style = clean.pop("output_style", None)
output_style = str(raw_style).strip() if raw_style is not None else None
if output_style == "":
output_style = None
return clean, output_style
def is_units_result(value: Any) -> bool:
"""True when *value* matches the compact units helper result contract."""
if not isinstance(value, dict):
return False
if "status" not in value:
return False
helper = value.get("helper")
if isinstance(helper, str) and helper in HELPER_NAMES:
return True
return "formatted" in value and "magnitude" in value
def format_units_for_calc(result: dict[str, Any], *, output_style: str | None = None) -> list[list[Any]]:
"""Turn a units helper result into a row-major grid for sheet egress."""
if result.get("status") == "error":
code = str(result.get("code") or "ERROR")
message = str(result.get("message") or "Units helper failed.")
return [[f"Units error ({code})"], [message]]
helper = str(result.get("helper") or "units")
formatted = str(result.get("formatted") or result.get("text") or "").strip()
style = resolve_output_style(helper, output_style)
if style == "formatted":
return [[formatted]] if formatted else [[helper]]
rows: list[list[Any]] = []
if formatted:
rows.append([formatted])
else:
rows.append([helper])
magnitude = result.get("magnitude")
if magnitude is not None:
rows.append(["Magnitude", magnitude])
units = str(result.get("units") or "").strip()
if units:
rows.append(["Units", units])
compatible = result.get("compatible")
if compatible is not None:
rows.append(["Compatible", compatible])
dimensionality_a = result.get("dimensionality_a")
dimensionality_b = result.get("dimensionality_b")
if dimensionality_a is not None:
rows.append(["Dimensionality A", dimensionality_a])
if dimensionality_b is not None:
rows.append(["Dimensionality B", dimensionality_b])
return rows
def insert_units_result_into_writer(ctx: Any, doc: Any, result: dict[str, Any]) -> None:
"""Insert formatted units text at the Writer selection."""
if result.get("status") == "error":
code = str(result.get("code") or "UNITS_ERROR")
message = str(result.get("message") or _("Units helper failed."))
raise ToolExecutionError(message, code=code, details={"units_result": result})
text = str(result.get("formatted") or result.get("text") or "").strip()
if not text:
raise ToolExecutionError(
_("Units helper returned no formatted text."),
code="UNITS_ERROR",
details={"units_result": result},
)
from plugin.writer.format import insert_content_at_position
insert_content_at_position(doc, ctx, text, "selection")
def insert_units_result_into_calc(
doc: Any,
ctx: Any,
result: dict[str, Any],
*,
output_style: str | None = None,
) -> int:
"""Write units result rows on the active Calc sheet."""
from plugin.calc.analysis_egress import calc_anchor_from_selection
from plugin.calc.address_utils import index_to_column
from plugin.calc.bridge import CalcBridge
from plugin.calc.manipulator import CellManipulator
helper = str(result.get("helper") or "")
grid = format_units_for_calc(result, output_style=resolve_output_style(helper, output_style))
col, row = calc_anchor_from_selection(doc)
bridge = CalcBridge(doc)
manipulator = CellManipulator(bridge)
addr = f"{index_to_column(col)}{row + 1}"
manipulator.write_formula_range(addr, grid)
return len(grid)
def insert_units_result_into_doc(
ctx: Any,
doc: Any,
result: dict[str, Any],
*,
output_style: str | None = None,
) -> int:
"""Insert a units helper result into Writer or Calc."""
if is_writer(doc):
insert_units_result_into_writer(ctx, doc, result)
return 1
if is_calc(doc):
return insert_units_result_into_calc(doc, ctx, result, output_style=output_style)
raise ToolExecutionError(_("Unsupported document type for units insertion."), code="UNITS_ERROR")
def try_insert_units_result(
ctx: Any,
doc: Any,
result_data: Any,
*,
output_style: str | None = None,
) -> bool:
"""Insert units results when present. Returns True if insertion ran."""
if not is_units_result(result_data):
return False
insert_units_result_into_doc(ctx, doc, result_data, output_style=output_style)
return True