Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

## Table of Contents

- [What's new (2026-06-22) — multipart/form-data Build & Parse](#whats-new-2026-06-22--multipartform-data-build--parse)
- [What's new (2026-06-22) — Secret Redaction for Config & Logs](#whats-new-2026-06-22--secret-redaction-for-config--logs)
- [What's new (2026-06-22) — RFC 8288 Link Header & Pagination](#whats-new-2026-06-22--rfc-8288-link-header--pagination)
- [What's new (2026-06-22) — Referential Integrity Checks](#whats-new-2026-06-22--referential-integrity-checks)
Expand Down Expand Up @@ -141,6 +142,12 @@

---

## What's new (2026-06-22) — multipart/form-data Build & Parse

Build file-upload bodies. Full reference: [`docs/source/Eng/doc/new_features/v89_features_doc.rst`](docs/source/Eng/doc/new_features/v89_features_doc.rst).

- **`build_multipart` / `parse_multipart` / `MultipartFile`** (`AC_build_multipart`, `AC_parse_multipart`): `http_request` sent only JSON/raw — there was no file upload, and stdlib `cgi` (which parsed multipart) was removed in 3.13. This assembles a `multipart/form-data` body from text fields and files with an injectable boundary (byte-stable), and parses one back into `{fields, files}`. Pure-stdlib, deterministic.

## What's new (2026-06-22) — Secret Redaction for Config & Logs

Mask secrets before logging or exporting. Full reference: [`docs/source/Eng/doc/new_features/v88_features_doc.rst`](docs/source/Eng/doc/new_features/v88_features_doc.rst).
Expand Down
7 changes: 7 additions & 0 deletions README/README_zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

## 目录

- [本次更新 (2026-06-22) — multipart/form-data 构建与解析](#本次更新-2026-06-22--multipartform-data-构建与解析)
- [本次更新 (2026-06-22) — 配置与日志的机密脱敏](#本次更新-2026-06-22--配置与日志的机密脱敏)
- [本次更新 (2026-06-22) — RFC 8288 Link 标头与分页](#本次更新-2026-06-22--rfc-8288-link-标头与分页)
- [本次更新 (2026-06-22) — 参照完整性检查](#本次更新-2026-06-22--参照完整性检查)
Expand Down Expand Up @@ -140,6 +141,12 @@

---

## 本次更新 (2026-06-22) — multipart/form-data 构建与解析

构建文件上传内文。完整参考:[`docs/source/Zh/doc/new_features/v89_features_doc.rst`](../docs/source/Zh/doc/new_features/v89_features_doc.rst)。

- **`build_multipart` / `parse_multipart` / `MultipartFile`**(`AC_build_multipart`、`AC_parse_multipart`):`http_request` 只发 JSON/原始 —— 没有文件上传,且解析 multipart 的标准库 `cgi` 已在 3.13 移除。本功能以可注入的 boundary(字节稳定)从文本字段与文件组装 `multipart/form-data` 内文,并能解析回 `{fields, files}`。纯标准库、确定。

## 本次更新 (2026-06-22) — 配置与日志的机密脱敏

在记录或导出前脱敏机密。完整参考:[`docs/source/Zh/doc/new_features/v88_features_doc.rst`](../docs/source/Zh/doc/new_features/v88_features_doc.rst)。
Expand Down
7 changes: 7 additions & 0 deletions README/README_zh-TW.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

## 目錄

- [本次更新 (2026-06-22) — multipart/form-data 建立與解析](#本次更新-2026-06-22--multipartform-data-建立與解析)
- [本次更新 (2026-06-22) — 設定與日誌的機密遮蔽](#本次更新-2026-06-22--設定與日誌的機密遮蔽)
- [本次更新 (2026-06-22) — RFC 8288 Link 標頭與分頁](#本次更新-2026-06-22--rfc-8288-link-標頭與分頁)
- [本次更新 (2026-06-22) — 參照完整性檢查](#本次更新-2026-06-22--參照完整性檢查)
Expand Down Expand Up @@ -140,6 +141,12 @@

---

## 本次更新 (2026-06-22) — multipart/form-data 建立與解析

建立檔案上傳內文。完整參考:[`docs/source/Zh/doc/new_features/v89_features_doc.rst`](../docs/source/Zh/doc/new_features/v89_features_doc.rst)。

- **`build_multipart` / `parse_multipart` / `MultipartFile`**(`AC_build_multipart`、`AC_parse_multipart`):`http_request` 只送 JSON/原始 —— 沒有檔案上傳,且解析 multipart 的標準函式庫 `cgi` 已在 3.13 移除。本功能以可注入的 boundary(位元組穩定)從文字欄位與檔案組裝 `multipart/form-data` 內文,並能解析回 `{fields, files}`。純標準函式庫、具決定性。

## 本次更新 (2026-06-22) — 設定與日誌的機密遮蔽

在記錄或匯出前遮蔽機密。完整參考:[`docs/source/Zh/doc/new_features/v88_features_doc.rst`](../docs/source/Zh/doc/new_features/v88_features_doc.rst)。
Expand Down
42 changes: 42 additions & 0 deletions docs/source/Eng/doc/new_features/v89_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
multipart/form-data Build & Parse
=================================

``http_request`` sends only JSON or raw bodies — there was no file upload, and
the stdlib ``cgi`` module (which once parsed multipart) was removed in Python
3.13. This assembles a ``multipart/form-data`` body from text fields and files
with a deterministic boundary, and parses one back.

Pure standard library (``re`` / ``secrets``); imports no ``PySide6``. The
boundary is injectable, so a built body is byte-stable and CI-testable.

Headless API
------------

.. code-block:: python

from je_auto_control import build_multipart, parse_multipart, MultipartFile

content_type, body = build_multipart(
fields={"title": "Q3 report"},
files=[MultipartFile("file", "report.csv", csv_text, "text/csv")],
)
http_request(url, method="POST",
headers={"Content-Type": content_type}, data=body)

parsed = parse_multipart(content_type, body) # {"fields": {...}, "files": [...]}

``build_multipart`` accepts ``fields`` (a dict or ``(name, value)`` list) and
``files`` (``MultipartFile`` instances or ``{name, filename, content,
content_type?}`` dicts), returning ``(content_type, body_bytes)``. Pass an
explicit ``boundary`` for a byte-stable body, or call ``new_boundary`` for a
fresh token. ``parse_multipart`` reads a body back into ``{fields, files}`` (each
file as ``{name, filename, content_type, content}``).

Executor commands
-----------------

``AC_build_multipart`` returns ``{content_type, body_base64}`` for ``fields`` /
``files`` (and an optional ``boundary``); ``AC_parse_multipart`` takes a
``content_type`` and ``body_base64`` and returns ``{fields, files}``. Both are
exposed as MCP tools (``ac_build_multipart`` / ``ac_parse_multipart``) and as
Script Builder commands under **Data**.
1 change: 1 addition & 0 deletions docs/source/Eng/eng_index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ Comprehensive guides for all AutoControl features.
doc/new_features/v86_features_doc
doc/new_features/v87_features_doc
doc/new_features/v88_features_doc
doc/new_features/v89_features_doc
doc/ocr_backends/ocr_backends_doc
doc/observability/observability_doc
doc/operations_layer/operations_layer_doc
Expand Down
38 changes: 38 additions & 0 deletions docs/source/Zh/doc/new_features/v89_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
multipart/form-data 建立與解析
=============================

``http_request`` 只送出 JSON 或原始內文 —— 沒有檔案上傳,而曾經能解析 multipart 的標準函式庫 ``cgi``
模組已在 Python 3.13 移除。本功能以決定性的 boundary 從文字欄位與檔案組裝 ``multipart/form-data``
內文,並能將其解析回來。

純標準函式庫(``re`` / ``secrets``);不匯入 ``PySide6``。boundary 可注入,因此建立出的內文位元組穩定、
可於 CI 測試。

無頭 API
--------

.. code-block:: python

from je_auto_control import build_multipart, parse_multipart, MultipartFile

content_type, body = build_multipart(
fields={"title": "Q3 report"},
files=[MultipartFile("file", "report.csv", csv_text, "text/csv")],
)
http_request(url, method="POST",
headers={"Content-Type": content_type}, data=body)

parsed = parse_multipart(content_type, body) # {"fields": {...}, "files": [...]}

``build_multipart`` 接受 ``fields``(dict 或 ``(name, value)`` 清單)與 ``files``(``MultipartFile``
實例或 ``{name, filename, content, content_type?}`` dict),回傳 ``(content_type, body_bytes)``。傳入明確
的 ``boundary`` 可得位元組穩定的內文,或呼叫 ``new_boundary`` 取得新 token。``parse_multipart`` 把內文讀回
``{fields, files}``(每個檔案為 ``{name, filename, content_type, content}``)。

執行器命令
----------

``AC_build_multipart`` 對 ``fields`` / ``files``(以及選用 ``boundary``)回傳 ``{content_type,
body_base64}``;``AC_parse_multipart`` 接受 ``content_type`` 與 ``body_base64`` 回傳 ``{fields, files}``。
兩者皆以 MCP 工具(``ac_build_multipart`` / ``ac_parse_multipart``)以及 Script Builder 中 **Data** 分類下
的命令提供。
1 change: 1 addition & 0 deletions docs/source/Zh/zh_index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ AutoControl 所有功能的完整使用指南。
doc/new_features/v86_features_doc
doc/new_features/v87_features_doc
doc/new_features/v88_features_doc
doc/new_features/v89_features_doc
doc/ocr_backends/ocr_backends_doc
doc/observability/observability_doc
doc/operations_layer/operations_layer_doc
Expand Down
5 changes: 5 additions & 0 deletions je_auto_control/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,10 @@
from je_auto_control.utils.link_header import (
Link, links_by_rel, next_url, paginate, parse_link_header,
)
# multipart/form-data building + parsing (file upload bodies)
from je_auto_control.utils.multipart import (
MultipartFile, build_multipart, new_boundary, parse_multipart,
)
# W3C Trace Context propagation (traceparent / tracestate)
from je_auto_control.utils.trace_context import (
SpanContext, TraceContextError, child_context, extract_context,
Expand Down Expand Up @@ -943,6 +947,7 @@ def start_autocontrol_gui(*args, **kwargs):
"raise_for_problem",
"SSEEvent", "SSEParser", "parse_event_stream",
"Link", "links_by_rel", "next_url", "paginate", "parse_link_header",
"MultipartFile", "build_multipart", "new_boundary", "parse_multipart",
"SpanContext", "TraceContextError", "child_context", "extract_context",
"format_traceparent", "format_tracestate", "inject_context",
"new_root_context", "new_span_id", "new_trace_id", "parse_traceparent",
Expand Down
22 changes: 22 additions & 0 deletions je_auto_control/gui/script_builder/command_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -1744,6 +1744,28 @@ def _add_resilience_specs(specs: List[CommandSpec]) -> None:
),
description="Return the rel=next URL from a Link header.",
))
specs.append(CommandSpec(
"AC_build_multipart", "Data", "Multipart: Build Body",
fields=(
FieldSpec("fields", FieldType.STRING, optional=True,
placeholder='{"name": "report", "tag": "v1"}'),
FieldSpec("files", FieldType.STRING, optional=True,
placeholder='[{"name": "f", "filename": "a.txt", '
'"content": "hi"}]'),
FieldSpec("boundary", FieldType.STRING, optional=True),
),
description="Build a multipart/form-data body (base64) for upload.",
))
specs.append(CommandSpec(
"AC_parse_multipart", "Data", "Multipart: Parse Body",
fields=(
FieldSpec("content_type", FieldType.STRING,
placeholder="multipart/form-data; boundary=..."),
FieldSpec("body_base64", FieldType.STRING,
placeholder="<base64 body>"),
),
description="Parse a base64 multipart body into fields and files.",
))
specs.append(CommandSpec(
"AC_resolve_config", "Data", "Layered Config: Resolve",
fields=(
Expand Down
25 changes: 25 additions & 0 deletions je_auto_control/utils/executor/action_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3030,6 +3030,29 @@ def _redact_secret_text(text: str, mask: str = "***") -> Dict[str, Any]:
return {"text": redact_secret_text(text, mask=mask)}


def _build_multipart(fields: Any = None, files: Any = None,
boundary: Any = None) -> Dict[str, Any]:
"""Adapter: build a multipart/form-data body (base64-encoded)."""
import base64
import json
from je_auto_control.utils.multipart import build_multipart
if isinstance(fields, str):
fields = json.loads(fields)
if isinstance(files, str):
files = json.loads(files)
content_type, body = build_multipart(fields, files, boundary=boundary)
return {"content_type": content_type,
"body_base64": base64.b64encode(body).decode("ascii")}


def _parse_multipart(content_type: str, body_base64: str) -> Dict[str, Any]:
"""Adapter: parse a base64-encoded multipart body into {fields, files}."""
import base64
from je_auto_control.utils.multipart import parse_multipart
body = base64.b64decode(body_base64)
return parse_multipart(content_type, body)


def _parse_link_header(value: str) -> Dict[str, Any]:
"""Adapter: parse an RFC 8288 Link header into {links}."""
from je_auto_control.utils.link_header import parse_link_header
Expand Down Expand Up @@ -4305,6 +4328,8 @@ def __init__(self):
"AC_redact_secret_text": _redact_secret_text,
"AC_parse_link_header": _parse_link_header,
"AC_next_url": _next_url,
"AC_build_multipart": _build_multipart,
"AC_parse_multipart": _parse_multipart,
"AC_profile_rows": _profile_rows,
"AC_infer_schema": _infer_schema,
"AC_parse_problem": _parse_problem,
Expand Down
31 changes: 30 additions & 1 deletion je_auto_control/utils/mcp_server/tools/_factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -3343,6 +3343,35 @@ def rate_limit_tools() -> List[MCPTool]:
]


def multipart_tools() -> List[MCPTool]:
return [
MCPTool(
name="ac_build_multipart",
description=("Build a multipart/form-data body from 'fields' "
"(object/list) and 'files' (each {name, filename, "
"content, content_type?}); optional 'boundary'. Returns "
"{content_type, body_base64}."),
input_schema=schema(
{"fields": {"type": "object"}, "files": {"type": "array"},
"boundary": {"type": "string"}},
[]),
handler=h.build_multipart,
annotations=READ_ONLY,
),
MCPTool(
name="ac_parse_multipart",
description=("Parse a base64 multipart body ('body_base64') with its "
"'content_type' into {fields, files}."),
input_schema=schema(
{"content_type": {"type": "string"},
"body_base64": {"type": "string"}},
["content_type", "body_base64"]),
handler=h.parse_multipart,
annotations=READ_ONLY,
),
]


def link_header_tools() -> List[MCPTool]:
return [
MCPTool(
Expand Down Expand Up @@ -5216,7 +5245,7 @@ def media_assert_tools() -> List[MCPTool]:
config_redaction_tools,
data_profile_tools, http_problem_tools, dotenv_tools,
sse_client_tools, layered_config_tools, data_drift_tools,
dataset_diff_tools, referential_tools, link_header_tools,
dataset_diff_tools, referential_tools, link_header_tools, multipart_tools,
saga_tools, decision_table_tools, locator_repair_tools,
pii_text_tools, sarif_tools,
screen_record_tools,
Expand Down
10 changes: 10 additions & 0 deletions je_auto_control/utils/mcp_server/tools/_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1756,6 +1756,16 @@ def next_url(value):
return _next_url(value)


def build_multipart(fields=None, files=None, boundary=None):
from je_auto_control.utils.executor.action_executor import _build_multipart
return _build_multipart(fields, files, boundary)


def parse_multipart(content_type, body_base64):
from je_auto_control.utils.executor.action_executor import _parse_multipart
return _parse_multipart(content_type, body_base64)


def redact_config(obj, mask="***"):
from je_auto_control.utils.executor.action_executor import _redact_config
return _redact_config(obj, mask)
Expand Down
8 changes: 8 additions & 0 deletions je_auto_control/utils/multipart/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""multipart/form-data building and parsing for AutoControl."""
from je_auto_control.utils.multipart.multipart import (
MultipartFile, build_multipart, new_boundary, parse_multipart,
)

__all__ = [
"MultipartFile", "build_multipart", "new_boundary", "parse_multipart",
]
Loading
Loading