Skip to content

Commit b14d6a9

Browse files
authored
Merge pull request #295 from Integration-Automation/feat/link-header-batch
Add RFC 8288 Link header parsing and pagination
2 parents ce24ba1 + 713315e commit b14d6a9

15 files changed

Lines changed: 361 additions & 1 deletion

File tree

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
## Table of Contents
1515

16+
- [What's new (2026-06-22) — RFC 8288 Link Header & Pagination](#whats-new-2026-06-22--rfc-8288-link-header--pagination)
1617
- [What's new (2026-06-22) — Referential Integrity Checks](#whats-new-2026-06-22--referential-integrity-checks)
1718
- [What's new (2026-06-22) — URI-Scheme Value References](#whats-new-2026-06-22--uri-scheme-value-references)
1819
- [What's new (2026-06-21) — W3C Baggage Propagation](#whats-new-2026-06-21--w3c-baggage-propagation)
@@ -139,6 +140,12 @@
139140

140141
---
141142

143+
## What's new (2026-06-22) — RFC 8288 Link Header & Pagination
144+
145+
Parse `Link` headers and follow `rel="next"`. Full reference: [`docs/source/Eng/doc/new_features/v87_features_doc.rst`](docs/source/Eng/doc/new_features/v87_features_doc.rst).
146+
147+
- **`parse_link_header` / `next_url` / `links_by_rel` / `paginate`** (`AC_parse_link_header`, `AC_next_url`): paginated REST APIs return `Link: <...>; rel="next"` but nothing parsed it. This parses the header (quoted values with commas, multiple links), indexes by relation, and `paginate` walks `rel="next"` over an injected `fetch` (transport/cassette) up to `max_pages`. Pure-stdlib, deterministic.
148+
142149
## What's new (2026-06-22) — Referential Integrity Checks
143150

144151
Foreign-key, unique, accepted-values and row-count checks across tables. Full reference: [`docs/source/Eng/doc/new_features/v86_features_doc.rst`](docs/source/Eng/doc/new_features/v86_features_doc.rst).

README/README_zh-CN.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
## 目录
1414

15+
- [本次更新 (2026-06-22) — RFC 8288 Link 标头与分页](#本次更新-2026-06-22--rfc-8288-link-标头与分页)
1516
- [本次更新 (2026-06-22) — 参照完整性检查](#本次更新-2026-06-22--参照完整性检查)
1617
- [本次更新 (2026-06-22) — URI-Scheme 值引用](#本次更新-2026-06-22--uri-scheme-值引用)
1718
- [本次更新 (2026-06-21) — W3C Baggage 传播](#本次更新-2026-06-21--w3c-baggage-传播)
@@ -138,6 +139,12 @@
138139

139140
---
140141

142+
## 本次更新 (2026-06-22) — RFC 8288 Link 标头与分页
143+
144+
解析 `Link` 标头并跟随 `rel="next"`。完整参考:[`docs/source/Zh/doc/new_features/v87_features_doc.rst`](../docs/source/Zh/doc/new_features/v87_features_doc.rst)
145+
146+
- **`parse_link_header` / `next_url` / `links_by_rel` / `paginate`**(`AC_parse_link_header``AC_next_url`):分页的 REST API 返回 `Link: <...>; rel="next"`,但没有东西解析它。本功能解析该标头(含逗号的引号值、多个链接)、按关系索引,`paginate` 通过注入的 `fetch`(传输/卡带)跟随 `rel="next"`,上限为 `max_pages`。纯标准库、确定。
147+
141148
## 本次更新 (2026-06-22) — 参照完整性检查
142149

143150
跨数据表的外键、唯一键、accepted-values 与行数检查。完整参考:[`docs/source/Zh/doc/new_features/v86_features_doc.rst`](../docs/source/Zh/doc/new_features/v86_features_doc.rst)

README/README_zh-TW.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
## 目錄
1414

15+
- [本次更新 (2026-06-22) — RFC 8288 Link 標頭與分頁](#本次更新-2026-06-22--rfc-8288-link-標頭與分頁)
1516
- [本次更新 (2026-06-22) — 參照完整性檢查](#本次更新-2026-06-22--參照完整性檢查)
1617
- [本次更新 (2026-06-22) — URI-Scheme 值參照](#本次更新-2026-06-22--uri-scheme-值參照)
1718
- [本次更新 (2026-06-21) — W3C Baggage 傳播](#本次更新-2026-06-21--w3c-baggage-傳播)
@@ -138,6 +139,12 @@
138139

139140
---
140141

142+
## 本次更新 (2026-06-22) — RFC 8288 Link 標頭與分頁
143+
144+
解析 `Link` 標頭並跟隨 `rel="next"`。完整參考:[`docs/source/Zh/doc/new_features/v87_features_doc.rst`](../docs/source/Zh/doc/new_features/v87_features_doc.rst)
145+
146+
- **`parse_link_header` / `next_url` / `links_by_rel` / `paginate`**(`AC_parse_link_header``AC_next_url`):分頁的 REST API 回傳 `Link: <...>; rel="next"`,但沒有東西解析它。本功能解析該標頭(含逗號的引號值、多個連結)、依關係索引,`paginate` 透過注入的 `fetch`(傳輸/卡帶)跟隨 `rel="next"`,上限為 `max_pages`。純標準函式庫、具決定性。
147+
141148
## 本次更新 (2026-06-22) — 參照完整性檢查
142149

143150
跨資料表的外鍵、唯一鍵、accepted-values 與筆數檢查。完整參考:[`docs/source/Zh/doc/new_features/v86_features_doc.rst`](../docs/source/Zh/doc/new_features/v86_features_doc.rst)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
RFC 8288 Link Header & Pagination
2+
=================================
3+
4+
Paginated REST APIs return ``Link: <...>; rel="next"`` headers, but nothing
5+
parsed them, so multi-page fetches needed manual glue. This parses the header
6+
(handling quoted parameter values and multiple links), indexes links by their
7+
relation, and walks ``rel="next"`` over an injected transport.
8+
9+
Pure standard library (``re``); imports no ``PySide6``. The parser is pure and
10+
``paginate`` takes an injected ``fetch`` callable, so pagination is CI-testable
11+
without a live server.
12+
13+
Headless API
14+
------------
15+
16+
.. code-block:: python
17+
18+
from je_auto_control import parse_link_header, next_url, links_by_rel, paginate
19+
20+
header = '<https://api/x?page=2>; rel="next", <https://api/x?page=9>; rel="last"'
21+
links = parse_link_header(header) # [Link(uri=..., rel="next"), ...]
22+
nxt = next_url(header) # "https://api/x?page=2"
23+
last = links_by_rel(header)["last"].uri
24+
25+
# Walk every page over an injected fetch (transport / cassette):
26+
pages = paginate(start_url, fetch, max_pages=50)
27+
28+
``parse_link_header`` returns a list of ``Link`` (``uri``, ``rel``, and all
29+
``params``), tolerating quoted values that contain commas and multiple links in
30+
one header. ``links_by_rel`` indexes by each (space-separated) relation,
31+
``next_url`` is the ``rel="next"`` convenience, and ``paginate`` fetches a URL
32+
and follows ``next`` links via the supplied ``fetch`` callable up to
33+
``max_pages``.
34+
35+
Executor commands
36+
-----------------
37+
38+
``AC_parse_link_header`` parses a header ``value`` into ``{links}``;
39+
``AC_next_url`` returns ``{url}`` for ``rel="next"``. Both are exposed as MCP
40+
tools (``ac_parse_link_header`` / ``ac_next_url``) and as Script Builder
41+
commands under **Data**.

docs/source/Eng/eng_index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ Comprehensive guides for all AutoControl features.
109109
doc/new_features/v84_features_doc
110110
doc/new_features/v85_features_doc
111111
doc/new_features/v86_features_doc
112+
doc/new_features/v87_features_doc
112113
doc/ocr_backends/ocr_backends_doc
113114
doc/observability/observability_doc
114115
doc/operations_layer/operations_layer_doc
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
RFC 8288 Link 標頭與分頁
2+
=======================
3+
4+
分頁的 REST API 會回傳 ``Link: <...>; rel="next"`` 標頭,但沒有任何東西解析它,因此多頁抓取需要手動黏合。
5+
本功能解析該標頭(處理含引號的參數值與多個連結)、依關係索引連結,並透過注入的傳輸走訪 ``rel="next"``。
6+
7+
純標準函式庫(``re``);不匯入 ``PySide6``。解析器為純函式,``paginate`` 接受注入的 ``fetch`` callable,
8+
因此分頁可在無線上伺服器的情況下於 CI 測試。
9+
10+
無頭 API
11+
--------
12+
13+
.. code-block:: python
14+
15+
from je_auto_control import parse_link_header, next_url, links_by_rel, paginate
16+
17+
header = '<https://api/x?page=2>; rel="next", <https://api/x?page=9>; rel="last"'
18+
links = parse_link_header(header) # [Link(uri=..., rel="next"), ...]
19+
nxt = next_url(header) # "https://api/x?page=2"
20+
last = links_by_rel(header)["last"].uri
21+
22+
# 透過注入的 fetch(傳輸 / 卡帶)走訪每一頁:
23+
pages = paginate(start_url, fetch, max_pages=50)
24+
25+
``parse_link_header`` 回傳 ``Link`` 清單(``uri``、``rel`` 與所有 ``params``),容許含逗號的引號值與單一
26+
標頭中的多個連結。``links_by_rel`` 依每個(以空白分隔的)關係索引,``next_url`` 是 ``rel="next"`` 的便利
27+
函式,``paginate`` 抓取一個 URL 並透過提供的 ``fetch`` callable 跟隨 ``next`` 連結,上限為 ``max_pages``。
28+
29+
執行器命令
30+
----------
31+
32+
``AC_parse_link_header`` 把標頭 ``value`` 解析成 ``{links}``;``AC_next_url`` 回傳 ``rel="next"`` 的
33+
``{url}``。兩者皆以 MCP 工具(``ac_parse_link_header`` / ``ac_next_url``)以及 Script Builder 中 **Data**
34+
分類下的命令提供。

docs/source/Zh/zh_index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ AutoControl 所有功能的完整使用指南。
109109
doc/new_features/v84_features_doc
110110
doc/new_features/v85_features_doc
111111
doc/new_features/v86_features_doc
112+
doc/new_features/v87_features_doc
112113
doc/ocr_backends/ocr_backends_doc
113114
doc/observability/observability_doc
114115
doc/operations_layer/operations_layer_doc

je_auto_control/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,10 @@
412412
from je_auto_control.utils.sse_client import (
413413
SSEEvent, SSEParser, parse_event_stream,
414414
)
415+
# RFC 8288 Link header parsing + rel="next" pagination
416+
from je_auto_control.utils.link_header import (
417+
Link, links_by_rel, next_url, paginate, parse_link_header,
418+
)
415419
# W3C Trace Context propagation (traceparent / tracestate)
416420
from je_auto_control.utils.trace_context import (
417421
SpanContext, TraceContextError, child_context, extract_context,
@@ -933,6 +937,7 @@ def start_autocontrol_gui(*args, **kwargs):
933937
"HttpProblemError", "ProblemDetails", "is_problem", "parse_problem",
934938
"raise_for_problem",
935939
"SSEEvent", "SSEParser", "parse_event_stream",
940+
"Link", "links_by_rel", "next_url", "paginate", "parse_link_header",
936941
"SpanContext", "TraceContextError", "child_context", "extract_context",
937942
"format_traceparent", "format_tracestate", "inject_context",
938943
"new_root_context", "new_span_id", "new_trace_id", "parse_traceparent",

je_auto_control/gui/script_builder/command_schema.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1710,6 +1710,22 @@ def _add_resilience_specs(specs: List[CommandSpec]) -> None:
17101710
),
17111711
description="Parse a text/event-stream blob into events.",
17121712
))
1713+
specs.append(CommandSpec(
1714+
"AC_parse_link_header", "Data", "Link Header: Parse",
1715+
fields=(
1716+
FieldSpec("value", FieldType.STRING,
1717+
placeholder='<https://api/x?page=2>; rel="next"'),
1718+
),
1719+
description="Parse an RFC 8288 Link header into links.",
1720+
))
1721+
specs.append(CommandSpec(
1722+
"AC_next_url", "Data", "Link Header: Next URL",
1723+
fields=(
1724+
FieldSpec("value", FieldType.STRING,
1725+
placeholder='<https://api/x?page=2>; rel="next"'),
1726+
),
1727+
description="Return the rel=next URL from a Link header.",
1728+
))
17131729
specs.append(CommandSpec(
17141730
"AC_resolve_config", "Data", "Layered Config: Resolve",
17151731
fields=(

je_auto_control/utils/executor/action_executor.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3015,6 +3015,18 @@ def _resolve_refs(obj: Any) -> Dict[str, Any]:
30153015
return {"resolved": resolve_refs_in(obj)}
30163016

30173017

3018+
def _parse_link_header(value: str) -> Dict[str, Any]:
3019+
"""Adapter: parse an RFC 8288 Link header into {links}."""
3020+
from je_auto_control.utils.link_header import parse_link_header
3021+
return {"links": [link.to_dict() for link in parse_link_header(value)]}
3022+
3023+
3024+
def _next_url(value: str) -> Dict[str, Any]:
3025+
"""Adapter: return the rel=next URL from a Link header."""
3026+
from je_auto_control.utils.link_header import next_url
3027+
return {"url": next_url(value)}
3028+
3029+
30183030
def _baggage_parse(header: str) -> Dict[str, Any]:
30193031
"""Adapter: parse a W3C baggage header into {items}."""
30203032
from je_auto_control.utils.baggage import parse_baggage
@@ -4274,6 +4286,8 @@ def __init__(self):
42744286
"AC_baggage_format": _baggage_format,
42754287
"AC_resolve_ref": _resolve_ref,
42764288
"AC_resolve_refs": _resolve_refs,
4289+
"AC_parse_link_header": _parse_link_header,
4290+
"AC_next_url": _next_url,
42774291
"AC_profile_rows": _profile_rows,
42784292
"AC_infer_schema": _infer_schema,
42794293
"AC_parse_problem": _parse_problem,

0 commit comments

Comments
 (0)