From 486b1db88d93667398d4a8975c3ab6e4c5b558f4 Mon Sep 17 00:00:00 2001 From: JeffreyChen Date: Sat, 20 Jun 2026 12:16:42 +0800 Subject: [PATCH] Add locale-aware number/currency/date parsing (optional babel) --- README.md | 7 ++ README/README_zh-CN.md | 7 ++ README/README_zh-TW.md | 7 ++ .../Eng/doc/new_features/v43_features_doc.rst | 55 +++++++++++++++ docs/source/Eng/eng_index.rst | 1 + .../Zh/doc/new_features/v43_features_doc.rst | 52 ++++++++++++++ docs/source/Zh/zh_index.rst | 1 + je_auto_control/__init__.py | 6 ++ .../gui/script_builder/command_schema.py | 48 +++++++++++++ .../utils/executor/action_executor.py | 37 ++++++++++ .../utils/locale_parse/__init__.py | 9 +++ .../utils/locale_parse/locale_parse.py | 59 ++++++++++++++++ .../utils/mcp_server/tools/_factories.py | 54 +++++++++++++++ .../utils/mcp_server/tools/_handlers.py | 25 +++++++ pyproject.toml | 1 + .../headless/test_locale_parse_batch.py | 68 +++++++++++++++++++ 16 files changed, 437 insertions(+) create mode 100644 docs/source/Eng/doc/new_features/v43_features_doc.rst create mode 100644 docs/source/Zh/doc/new_features/v43_features_doc.rst create mode 100644 je_auto_control/utils/locale_parse/__init__.py create mode 100644 je_auto_control/utils/locale_parse/locale_parse.py create mode 100644 test/unit_test/headless/test_locale_parse_batch.py diff --git a/README.md b/README.md index 5370920e..b6918560 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ ## Table of Contents +- [What's new (2026-06-20) — Locale-Aware Number, Currency & Date Parsing](#whats-new-2026-06-20--locale-aware-number-currency--date-parsing) - [What's new (2026-06-20) — Perceptual-Hash Image Dedupe](#whats-new-2026-06-20--perceptual-hash-image-dedupe) - [What's new (2026-06-20) — S3-Compatible Artifact Store](#whats-new-2026-06-20--s3-compatible-artifact-store) - [What's new (2026-06-20) — Fuzzy String Matching & Dedupe](#whats-new-2026-06-20--fuzzy-string-matching--dedupe) @@ -95,6 +96,12 @@ --- +## What's new (2026-06-20) — Locale-Aware Number, Currency & Date Parsing + +Parse localized numbers/currency/dates. Full reference: [`docs/source/Eng/doc/new_features/v43_features_doc.rst`](docs/source/Eng/doc/new_features/v43_features_doc.rst). + +- **`parse_decimal` / `parse_number` / `format_decimal` / `format_currency` / `format_date`** (`AC_parse_decimal` / `AC_parse_number` / `AC_format_decimal` / `AC_format_currency` / `AC_format_date`, `ac_*`): OCR/UI text like `"1.234,56"` (de_DE) parses correctly to `1234.56` via **Babel**'s CLDR data, and values format back per-locale. `babel` is an optional `[locale]` extra, imported lazily; functional tests run under `importorskip` (wiring/facade always verified). + ## What's new (2026-06-20) — Perceptual-Hash Image Dedupe Collapse near-identical screenshots. Full reference: [`docs/source/Eng/doc/new_features/v42_features_doc.rst`](docs/source/Eng/doc/new_features/v42_features_doc.rst). diff --git a/README/README_zh-CN.md b/README/README_zh-CN.md index cafe7306..ef8bec1e 100644 --- a/README/README_zh-CN.md +++ b/README/README_zh-CN.md @@ -12,6 +12,7 @@ ## 目录 +- [本次更新 (2026-06-20) — 区域设置感知的数字、货币与日期解析](#本次更新-2026-06-20--区域设置感知的数字货币与日期解析) - [本次更新 (2026-06-20) — 感知哈希图像去重](#本次更新-2026-06-20--感知哈希图像去重) - [本次更新 (2026-06-20) — S3 兼容成品存储](#本次更新-2026-06-20--s3-兼容成品存储) - [本次更新 (2026-06-20) — 模糊字符串匹配与去重](#本次更新-2026-06-20--模糊字符串匹配与去重) @@ -94,6 +95,12 @@ --- +## 本次更新 (2026-06-20) — 区域设置感知的数字、货币与日期解析 + +解析本地化的数字/货币/日期。完整参考:[`docs/source/Zh/doc/new_features/v43_features_doc.rst`](../docs/source/Zh/doc/new_features/v43_features_doc.rst)。 + +- **`parse_decimal` / `parse_number` / `format_decimal` / `format_currency` / `format_date`**(`AC_parse_decimal` / `AC_parse_number` / `AC_format_decimal` / `AC_format_currency` / `AC_format_date`、`ac_*`):像 `"1.234,56"`(de_DE)这样的 OCR/UI 文本会通过 **Babel** 的 CLDR 数据正确解析为 `1234.56`,值也能依区域设置格式化回去。`babel` 为可选 `[locale]` extra,采延迟导入;功能测试以 `importorskip` 运行(wiring/facade 一律验证)。 + ## 本次更新 (2026-06-20) — 感知哈希图像去重 收合近乎相同的屏幕截图。完整参考:[`docs/source/Zh/doc/new_features/v42_features_doc.rst`](../docs/source/Zh/doc/new_features/v42_features_doc.rst)。 diff --git a/README/README_zh-TW.md b/README/README_zh-TW.md index 8f798923..c24c907b 100644 --- a/README/README_zh-TW.md +++ b/README/README_zh-TW.md @@ -12,6 +12,7 @@ ## 目錄 +- [本次更新 (2026-06-20) — 區域設定感知的數字、貨幣與日期解析](#本次更新-2026-06-20--區域設定感知的數字貨幣與日期解析) - [本次更新 (2026-06-20) — 感知雜湊影像去重](#本次更新-2026-06-20--感知雜湊影像去重) - [本次更新 (2026-06-20) — S3 相容成品儲存](#本次更新-2026-06-20--s3-相容成品儲存) - [本次更新 (2026-06-20) — 模糊字串比對與去重](#本次更新-2026-06-20--模糊字串比對與去重) @@ -94,6 +95,12 @@ --- +## 本次更新 (2026-06-20) — 區域設定感知的數字、貨幣與日期解析 + +解析在地化的數字/貨幣/日期。完整參考:[`docs/source/Zh/doc/new_features/v43_features_doc.rst`](../docs/source/Zh/doc/new_features/v43_features_doc.rst)。 + +- **`parse_decimal` / `parse_number` / `format_decimal` / `format_currency` / `format_date`**(`AC_parse_decimal` / `AC_parse_number` / `AC_format_decimal` / `AC_format_currency` / `AC_format_date`、`ac_*`):像 `"1.234,56"`(de_DE)這樣的 OCR/UI 文字會透過 **Babel** 的 CLDR 資料正確解析為 `1234.56`,值也能依區域設定格式化回去。`babel` 為選用 `[locale]` extra,採延遲匯入;功能測試以 `importorskip` 執行(wiring/facade 一律驗證)。 + ## 本次更新 (2026-06-20) — 感知雜湊影像去重 收合近乎相同的螢幕截圖。完整參考:[`docs/source/Zh/doc/new_features/v42_features_doc.rst`](../docs/source/Zh/doc/new_features/v42_features_doc.rst)。 diff --git a/docs/source/Eng/doc/new_features/v43_features_doc.rst b/docs/source/Eng/doc/new_features/v43_features_doc.rst new file mode 100644 index 00000000..c68e16b2 --- /dev/null +++ b/docs/source/Eng/doc/new_features/v43_features_doc.rst @@ -0,0 +1,55 @@ +Locale-Aware Number, Currency & Date Parsing +============================================ + +Text scraped from a localized UI or OCR rarely matches Python's ``float()``: +``"1.234,56"`` is twelve-hundred in ``de_DE`` but malformed to ``float``. These +helpers parse such strings — and format values back — using **Babel**'s CLDR +data, so flows can read and assert on numbers, currency, and dates across +locales. + +``babel`` is an **optional** dependency (``pip install je_auto_control[locale]``) +imported lazily, so the package stays importable without it; the functions raise +a clear error only when called without Babel. Imports no ``PySide6``. + +Headless API +------------ + +.. code-block:: python + + from je_auto_control import ( + parse_decimal, parse_number, format_decimal, format_currency, + format_date) + + parse_decimal("1.234,56", locale="de_DE") # -> 1234.56 + parse_number("1,234", locale="en_US") # -> 1234 + + format_decimal(1234.5, locale="en_US") # -> "1,234.5" + format_currency(1234.5, "USD", locale="en_US") # -> "$1,234.50" + format_date("2026-06-20", locale="de_DE", fmt="short") # -> "20.06.26" + +``format_date`` accepts an ISO ``YYYY-MM-DD`` string or a ``date`` object and a +``fmt`` of ``short`` / ``medium`` / ``long`` / ``full``. Parse + format +round-trip within a locale. + +.. note:: + + The functional path requires Babel; CI runs these tests under + ``importorskip`` so they execute wherever Babel is installed and are skipped + otherwise. The wiring/facade are always verified. + +Executor commands +----------------- + +================================ =================================================== +Command Effect +================================ =================================================== +``AC_parse_decimal`` ``{value}`` float from a locale decimal string. +``AC_parse_number`` ``{value}`` int from a locale integer string. +``AC_format_decimal`` ``{text}`` number formatted for a locale. +``AC_format_currency`` ``{text}`` currency (ISO 4217) for a locale. +``AC_format_date`` ``{text}`` ISO date formatted for a locale. +================================ =================================================== + +The same operations are exposed as MCP tools (``ac_parse_decimal`` / +``ac_parse_number`` / ``ac_format_decimal`` / ``ac_format_currency`` / +``ac_format_date``) and as Script Builder commands under **Data**. diff --git a/docs/source/Eng/eng_index.rst b/docs/source/Eng/eng_index.rst index 38c0a32c..76c1e7a5 100644 --- a/docs/source/Eng/eng_index.rst +++ b/docs/source/Eng/eng_index.rst @@ -65,6 +65,7 @@ Comprehensive guides for all AutoControl features. doc/new_features/v40_features_doc doc/new_features/v41_features_doc doc/new_features/v42_features_doc + doc/new_features/v43_features_doc doc/ocr_backends/ocr_backends_doc doc/observability/observability_doc doc/operations_layer/operations_layer_doc diff --git a/docs/source/Zh/doc/new_features/v43_features_doc.rst b/docs/source/Zh/doc/new_features/v43_features_doc.rst new file mode 100644 index 00000000..6555f907 --- /dev/null +++ b/docs/source/Zh/doc/new_features/v43_features_doc.rst @@ -0,0 +1,52 @@ +區域設定感知的數字、貨幣與日期解析 +================================== + +從在地化 UI 或 OCR 擷取的文字,鮮少能直接通過 Python 的 ``float()``:``"1.234,56"`` +在 ``de_DE`` 是一千二百多,但對 ``float`` 卻是格式錯誤。這些輔助函式以 **Babel** 的 +CLDR 資料解析這類字串(並可反向格式化值),讓流程能跨區域設定讀取並斷言數字、貨幣與日 +期。 + +``babel`` 為**選用**相依(``pip install je_auto_control[locale]``),採延遲匯入,因此套 +件在沒有它時仍可匯入;函式僅在未安裝 Babel 而被呼叫時才拋出明確錯誤。不匯入 +``PySide6``。 + +無頭 API +-------- + +.. code-block:: python + + from je_auto_control import ( + parse_decimal, parse_number, format_decimal, format_currency, + format_date) + + parse_decimal("1.234,56", locale="de_DE") # -> 1234.56 + parse_number("1,234", locale="en_US") # -> 1234 + + format_decimal(1234.5, locale="en_US") # -> "1,234.5" + format_currency(1234.5, "USD", locale="en_US") # -> "$1,234.50" + format_date("2026-06-20", locale="de_DE", fmt="short") # -> "20.06.26" + +``format_date`` 接受 ISO ``YYYY-MM-DD`` 字串或 ``date`` 物件,``fmt`` 可為 ``short`` / +``medium`` / ``long`` / ``full``。同一區域設定內解析 + 格式化可往返一致。 + +.. note:: + + 功能路徑需要 Babel;CI 以 ``importorskip`` 執行這些測試,因此在有安裝 Babel 處執 + 行、否則跳過。wiring/facade 則一律驗證。 + +執行器指令 +---------- + +================================ =================================================== +指令 效果 +================================ =================================================== +``AC_parse_decimal`` 由區域設定小數字串得到 ``{value}`` float。 +``AC_parse_number`` 由區域設定整數字串得到 ``{value}`` int。 +``AC_format_decimal`` 依區域設定格式化數字的 ``{text}``。 +``AC_format_currency`` 依區域設定的貨幣(ISO 4217)``{text}``。 +``AC_format_date`` 依區域設定格式化 ISO 日期的 ``{text}``。 +================================ =================================================== + +相同操作亦提供為 MCP 工具(``ac_parse_decimal`` / ``ac_parse_number`` / +``ac_format_decimal`` / ``ac_format_currency`` / ``ac_format_date``),以及 Script +Builder 中 **Data** 分類下的指令。 diff --git a/docs/source/Zh/zh_index.rst b/docs/source/Zh/zh_index.rst index b2eba636..c3844372 100644 --- a/docs/source/Zh/zh_index.rst +++ b/docs/source/Zh/zh_index.rst @@ -65,6 +65,7 @@ AutoControl 所有功能的完整使用指南。 doc/new_features/v40_features_doc doc/new_features/v41_features_doc doc/new_features/v42_features_doc + doc/new_features/v43_features_doc doc/ocr_backends/ocr_backends_doc doc/observability/observability_doc doc/operations_layer/operations_layer_doc diff --git a/je_auto_control/__init__.py b/je_auto_control/__init__.py index 3f29373a..592aef6a 100644 --- a/je_auto_control/__init__.py +++ b/je_auto_control/__init__.py @@ -243,6 +243,10 @@ from je_auto_control.utils.image_dedup import ( average_hash, dedupe_images, dhash, hamming_distance, images_similar, ) +# Locale-aware number/currency/date parsing & formatting (optional babel) +from je_auto_control.utils.locale_parse import ( + format_currency, format_date, format_decimal, parse_decimal, parse_number, +) # Background popup/interrupt watchdog (unattended automation) from je_auto_control.utils.watchdog import ( PopupWatchdog, WatchdogRule, default_popup_watchdog, @@ -694,6 +698,8 @@ def start_autocontrol_gui(*args, **kwargs): "set_default_store", "average_hash", "dedupe_images", "dhash", "hamming_distance", "images_similar", + "format_currency", "format_date", "format_decimal", "parse_decimal", + "parse_number", # MCP server "AuditLogger", "HttpMCPServer", "MCPContent", "MCPPrompt", "MCPPromptArgument", "MCPResource", "MCPServer", "MCPTool", diff --git a/je_auto_control/gui/script_builder/command_schema.py b/je_auto_control/gui/script_builder/command_schema.py index debd7718..bdf01b67 100644 --- a/je_auto_control/gui/script_builder/command_schema.py +++ b/je_auto_control/gui/script_builder/command_schema.py @@ -946,6 +946,54 @@ def _add_misc_specs(specs: List[CommandSpec]) -> None: ), description="Collapse near-duplicate images by perceptual hash.", )) + specs.append(CommandSpec( + "AC_parse_decimal", "Data", "Locale: Parse Decimal", + fields=( + FieldSpec("text", FieldType.STRING, placeholder="1.234,56"), + FieldSpec("locale", FieldType.STRING, optional=True, + default="en_US"), + ), + description="Parse a locale-formatted decimal string to a float.", + )) + specs.append(CommandSpec( + "AC_parse_number", "Data", "Locale: Parse Number", + fields=( + FieldSpec("text", FieldType.STRING, placeholder="1,234"), + FieldSpec("locale", FieldType.STRING, optional=True, + default="en_US"), + ), + description="Parse a locale-formatted integer string to an int.", + )) + specs.append(CommandSpec( + "AC_format_decimal", "Data", "Locale: Format Decimal", + fields=( + FieldSpec("value", FieldType.FLOAT), + FieldSpec("locale", FieldType.STRING, optional=True, + default="en_US"), + ), + description="Format a number for a locale.", + )) + specs.append(CommandSpec( + "AC_format_currency", "Data", "Locale: Format Currency", + fields=( + FieldSpec("value", FieldType.FLOAT), + FieldSpec("currency", FieldType.STRING, placeholder="USD"), + FieldSpec("locale", FieldType.STRING, optional=True, + default="en_US"), + ), + description="Format a value as currency (ISO 4217) for a locale.", + )) + specs.append(CommandSpec( + "AC_format_date", "Data", "Locale: Format Date", + fields=( + FieldSpec("value", FieldType.STRING, placeholder="2026-06-20"), + FieldSpec("locale", FieldType.STRING, optional=True, + default="en_US"), + FieldSpec("fmt", FieldType.ENUM, optional=True, default="medium", + choices=("short", "medium", "long", "full")), + ), + description="Format an ISO date string for a locale.", + )) specs.append(CommandSpec( "AC_generate_sop", "Report", "Generate SOP Document", fields=( diff --git a/je_auto_control/utils/executor/action_executor.py b/je_auto_control/utils/executor/action_executor.py index 1892d716..c76e9d89 100644 --- a/je_auto_control/utils/executor/action_executor.py +++ b/je_auto_control/utils/executor/action_executor.py @@ -3136,6 +3136,38 @@ def _dedupe_images(paths: Any, max_distance: int = 5) -> Dict[str, Any]: max_distance=max_distance)} +def _parse_decimal(text: str, locale: str = "en_US") -> Dict[str, Any]: + """Adapter: parse a locale-formatted decimal string to a float.""" + from je_auto_control.utils.locale_parse import parse_decimal + return {"value": parse_decimal(text, locale)} + + +def _parse_number(text: str, locale: str = "en_US") -> Dict[str, Any]: + """Adapter: parse a locale-formatted integer string to an int.""" + from je_auto_control.utils.locale_parse import parse_number + return {"value": parse_number(text, locale)} + + +def _format_decimal(value: float, locale: str = "en_US") -> Dict[str, Any]: + """Adapter: format a number for a locale.""" + from je_auto_control.utils.locale_parse import format_decimal + return {"text": format_decimal(value, locale)} + + +def _format_currency(value: float, currency: str, + locale: str = "en_US") -> Dict[str, Any]: + """Adapter: format a value as currency for a locale.""" + from je_auto_control.utils.locale_parse import format_currency + return {"text": format_currency(value, currency, locale)} + + +def _format_date(value: str, locale: str = "en_US", + fmt: str = "medium") -> Dict[str, Any]: + """Adapter: format an ISO date string for a locale.""" + from je_auto_control.utils.locale_parse import format_date + return {"text": format_date(value, locale, fmt)} + + class Executor: """ Executor @@ -3397,6 +3429,11 @@ def __init__(self): "AC_s3_delete": _s3_delete, "AC_image_hash": _image_hash, "AC_dedupe_images": _dedupe_images, + "AC_parse_decimal": _parse_decimal, + "AC_parse_number": _parse_number, + "AC_format_decimal": _format_decimal, + "AC_format_currency": _format_currency, + "AC_format_date": _format_date, "AC_a11y_record_start": _a11y_record_start, "AC_a11y_record_stop": _a11y_record_stop, "AC_a11y_record_events": _a11y_record_events, diff --git a/je_auto_control/utils/locale_parse/__init__.py b/je_auto_control/utils/locale_parse/__init__.py new file mode 100644 index 00000000..fa68d181 --- /dev/null +++ b/je_auto_control/utils/locale_parse/__init__.py @@ -0,0 +1,9 @@ +"""Locale-aware number/currency/date parsing & formatting (optional babel).""" +from je_auto_control.utils.locale_parse.locale_parse import ( + format_currency, format_date, format_decimal, parse_decimal, parse_number, +) + +__all__ = [ + "format_currency", "format_date", "format_decimal", "parse_decimal", + "parse_number", +] diff --git a/je_auto_control/utils/locale_parse/locale_parse.py b/je_auto_control/utils/locale_parse/locale_parse.py new file mode 100644 index 00000000..edcd69d2 --- /dev/null +++ b/je_auto_control/utils/locale_parse/locale_parse.py @@ -0,0 +1,59 @@ +"""Parse and format numbers, currency, and dates the way a locale writes them. + +Text scraped from a localized UI or OCR rarely matches Python's ``float()``: +``"1.234,56"`` is twelve-hundred in ``de_DE`` but malformed to ``float``. These +helpers parse such strings (and format values back) using **Babel**'s CLDR data, +so flows can read and assert on numbers/currency/dates across locales. + +``babel`` is an **optional** dependency (``pip install je_auto_control[locale]``); +it is imported lazily, so the package stays importable without it and the +functions raise a clear error only when actually called without Babel installed. +Imports no ``PySide6``. +""" +import datetime +from typing import Any, Union + + +def _numbers() -> Any: + try: + from babel import numbers + except ImportError as error: # pragma: no cover - exercised without babel + raise RuntimeError( + "locale parsing requires Babel: pip install " + "je_auto_control[locale]") from error + return numbers + + +def parse_decimal(text: str, locale: str = "en_US") -> float: + """Parse a locale-formatted decimal string into a ``float``.""" + return float(_numbers().parse_decimal(text, locale=locale)) + + +def parse_number(text: str, locale: str = "en_US") -> int: + """Parse a locale-formatted integer string into an ``int``.""" + return int(_numbers().parse_decimal(text, locale=locale)) + + +def format_decimal(value: Union[int, float], locale: str = "en_US") -> str: + """Format a number the way ``locale`` writes decimals.""" + return _numbers().format_decimal(value, locale=locale) + + +def format_currency(value: Union[int, float], currency: str, + locale: str = "en_US") -> str: + """Format ``value`` as ``currency`` (ISO 4217) for ``locale``.""" + return _numbers().format_currency(value, currency, locale=locale) + + +def format_date(value: Union[str, datetime.date], locale: str = "en_US", + fmt: str = "medium") -> str: + """Format a date (or ISO ``YYYY-MM-DD`` string) for ``locale``.""" + try: + from babel.dates import format_date as _format_date + except ImportError as error: # pragma: no cover - exercised without babel + raise RuntimeError( + "locale formatting requires Babel: pip install " + "je_auto_control[locale]") from error + date_value = (datetime.date.fromisoformat(value) + if isinstance(value, str) else value) + return _format_date(date_value, format=fmt, locale=locale) diff --git a/je_auto_control/utils/mcp_server/tools/_factories.py b/je_auto_control/utils/mcp_server/tools/_factories.py index 727a0495..40b37c6f 100644 --- a/je_auto_control/utils/mcp_server/tools/_factories.py +++ b/je_auto_control/utils/mcp_server/tools/_factories.py @@ -2955,6 +2955,59 @@ def image_dedup_tools() -> List[MCPTool]: ] +def locale_tools() -> List[MCPTool]: + _LOC = {"type": "string"} + return [ + MCPTool( + name="ac_parse_decimal", + description=("Parse a locale-formatted decimal string (e.g. " + "'1.234,56' in de_DE) to a float. Returns {value}."), + input_schema=schema( + {"text": {"type": "string"}, "locale": _LOC}, ["text"]), + handler=h.parse_decimal, + annotations=READ_ONLY, + ), + MCPTool( + name="ac_parse_number", + description=("Parse a locale-formatted integer string to an int. " + "Returns {value}."), + input_schema=schema( + {"text": {"type": "string"}, "locale": _LOC}, ["text"]), + handler=h.parse_number, + annotations=READ_ONLY, + ), + MCPTool( + name="ac_format_decimal", + description="Format a number the way a locale writes decimals. " + "Returns {text}.", + input_schema=schema( + {"value": {"type": "number"}, "locale": _LOC}, ["value"]), + handler=h.format_decimal, + annotations=READ_ONLY, + ), + MCPTool( + name="ac_format_currency", + description=("Format a value as a currency (ISO 4217) for a " + "locale. Returns {text}."), + input_schema=schema( + {"value": {"type": "number"}, "currency": {"type": "string"}, + "locale": _LOC}, ["value", "currency"]), + handler=h.format_currency, + annotations=READ_ONLY, + ), + MCPTool( + name="ac_format_date", + description=("Format an ISO (YYYY-MM-DD) date for a locale. 'fmt' " + "is short/medium/long/full. Returns {text}."), + input_schema=schema( + {"value": {"type": "string"}, "locale": _LOC, + "fmt": {"type": "string"}}, ["value"]), + handler=h.format_date, + annotations=READ_ONLY, + ), + ] + + def unattended_tools() -> List[MCPTool]: return [ MCPTool( @@ -4016,6 +4069,7 @@ def media_assert_tools() -> List[MCPTool]: credential_lease_tools, egress_tools, approval_testing_tools, trajectory_eval_tools, compliance_tools, agent_trace_tools, video_report_tools, fuzzy_tools, artifact_store_tools, image_dedup_tools, + locale_tools, screen_record_tools, process_and_shell_tools, remote_desktop_tools, gamepad_tools, usb_passthrough_tools, assertion_tools, data_source_tools, diff --git a/je_auto_control/utils/mcp_server/tools/_handlers.py b/je_auto_control/utils/mcp_server/tools/_handlers.py index 63f9486c..51b502d1 100644 --- a/je_auto_control/utils/mcp_server/tools/_handlers.py +++ b/je_auto_control/utils/mcp_server/tools/_handlers.py @@ -1423,6 +1423,31 @@ def dedupe_images(paths, max_distance=5): return {"unique": _dedupe(paths, max_distance=max_distance)} +def parse_decimal(text, locale="en_US"): + from je_auto_control.utils.locale_parse import parse_decimal as _parse + return {"value": _parse(text, locale)} + + +def parse_number(text, locale="en_US"): + from je_auto_control.utils.locale_parse import parse_number as _parse + return {"value": _parse(text, locale)} + + +def format_decimal(value, locale="en_US"): + from je_auto_control.utils.locale_parse import format_decimal as _fmt + return {"text": _fmt(value, locale)} + + +def format_currency(value, currency, locale="en_US"): + from je_auto_control.utils.locale_parse import format_currency as _fmt + return {"text": _fmt(value, currency, locale)} + + +def format_date(value, locale="en_US", fmt="medium"): + from je_auto_control.utils.locale_parse import format_date as _fmt + return {"text": _fmt(value, locale, fmt)} + + def vlm_locate(description: str, screen_region: Optional[List[int]] = None, model: Optional[str] = None) -> Optional[List[int]]: diff --git a/pyproject.toml b/pyproject.toml index 9c232205..e8d754d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,6 +74,7 @@ pdf = ["pypdf>=4.0"] office = ["openpyxl>=3.1", "python-docx>=1.1", "python-pptx>=0.6"] fuzzy = ["rapidfuzz>=3.0"] s3 = ["boto3>=1.34"] +locale = ["babel>=2.12"] [tool.bandit] exclude_dirs = [ diff --git a/test/unit_test/headless/test_locale_parse_batch.py b/test/unit_test/headless/test_locale_parse_batch.py new file mode 100644 index 00000000..9b209ebc --- /dev/null +++ b/test/unit_test/headless/test_locale_parse_batch.py @@ -0,0 +1,68 @@ +"""Headless tests for locale-aware parsing/formatting. Functional tests need +Babel (importorskip); wiring/facade tests always run. Pure stdlib, no Qt.""" +import pytest + +import je_auto_control as ac +from je_auto_control.utils.locale_parse import ( + format_currency, format_date, format_decimal, parse_decimal, parse_number) + + +def test_parse_decimal_across_locales(): + pytest.importorskip("babel") + assert parse_decimal("1.234,56", locale="de_DE") == pytest.approx(1234.56) + assert parse_decimal("1,234.56", locale="en_US") == pytest.approx(1234.56) + + +def test_parse_number(): + pytest.importorskip("babel") + assert parse_number("1,234", locale="en_US") == 1234 + + +def test_format_decimal_and_currency(): + pytest.importorskip("babel") + assert format_decimal(1234.5, locale="en_US") == "1,234.5" + assert format_currency(1234.5, "USD", locale="en_US") == "$1,234.50" + + +def test_format_date_from_iso(): + pytest.importorskip("babel") + assert format_date("2026-06-20", locale="de_DE", fmt="short") == "20.06.26" + + +def test_round_trip_de_de(): + pytest.importorskip("babel") + text = format_decimal(1234.56, locale="de_DE") + assert parse_decimal(text, locale="de_DE") == pytest.approx(1234.56) + + +# --- wiring (always runs) ------------------------------------------------- + +def test_executor_round_trip(): + pytest.importorskip("babel") + rec = ac.execute_action([ + ["AC_parse_decimal", {"text": "1.234,56", "locale": "de_DE"}], + ]) + value = next(v for v in rec.values() if isinstance(v, dict))["value"] + assert value == pytest.approx(1234.56) + + +def test_wiring(): + known = ac.executor.known_commands() + assert {"AC_parse_decimal", "AC_parse_number", "AC_format_decimal", + "AC_format_currency", "AC_format_date"} <= known + from je_auto_control.utils.mcp_server.tools import ( + build_default_tool_registry) + names = {t.name for t in build_default_tool_registry()} + assert {"ac_parse_decimal", "ac_parse_number", "ac_format_decimal", + "ac_format_currency", "ac_format_date"} <= names + from je_auto_control.gui.script_builder.command_schema import _build_specs + cmds = {s.command for s in _build_specs()} + assert {"AC_parse_decimal", "AC_parse_number", "AC_format_decimal", + "AC_format_currency", "AC_format_date"} <= cmds + + +def test_facade_exports(): + for attr in ("parse_decimal", "parse_number", "format_decimal", + "format_currency", "format_date"): + assert hasattr(ac, attr) + assert attr in ac.__all__