feat: restore source parity and harden agent runtime
This commit is contained in:
@@ -0,0 +1,943 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""父格 + 聚合驱动的数据源扩展(全表拓扑,支持跨行上父格 / 横向父格树)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from online_dev.report_manager.engine.parent_cells import (
|
||||
cell_key,
|
||||
filter_rows_by_parents,
|
||||
resolve_field_path,
|
||||
resolve_parents,
|
||||
_index_data_sources,
|
||||
_get_nested_value,
|
||||
_int_coord,
|
||||
)
|
||||
from online_dev.report_manager.engine.polymerize import BindData, build_bind_list, expand_span
|
||||
|
||||
CellKey = Tuple[str, int, int]
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExpandedSlot:
|
||||
"""单元格一次绑定扩展占用的行/列区间(左闭右开)。"""
|
||||
|
||||
value: Any
|
||||
data_list: List[Dict[str, Any]]
|
||||
start: int
|
||||
end: int
|
||||
|
||||
|
||||
def _expand_direction(custom: Dict[str, Any]) -> str:
|
||||
expand = (custom.get("expand") or custom.get("expandDirection") or "").lower()
|
||||
if expand in ("down", "list", "vertical"):
|
||||
return "down"
|
||||
if expand in ("right", "horizontal", "across"):
|
||||
return "right"
|
||||
fill = (custom.get("fillDirection") or "").lower()
|
||||
if fill in ("portrait", "vertical", "down"):
|
||||
return "down"
|
||||
if fill in ("landscape", "horizontal", "right", "across"):
|
||||
return "right"
|
||||
return "none"
|
||||
|
||||
|
||||
def _dataset_name(cell: Dict[str, Any]) -> str:
|
||||
custom = cell.get("custom") or {}
|
||||
name = str(
|
||||
custom.get("dataSetName") or custom.get("dataSet") or custom.get("alias") or ""
|
||||
)
|
||||
if name:
|
||||
return name
|
||||
field = str(custom.get("field") or custom.get("bindField") or "")
|
||||
if "." in field:
|
||||
return field.split(".", 1)[0]
|
||||
return ""
|
||||
|
||||
|
||||
def _field_name(cell: Dict[str, Any]) -> str:
|
||||
custom = cell.get("custom") or {}
|
||||
return str(custom.get("field") or custom.get("bindField") or "")
|
||||
|
||||
|
||||
def _poly_list(cell: Dict[str, Any]) -> bool:
|
||||
return str((cell.get("custom") or {}).get("polymerizationType") or "1") == "1"
|
||||
|
||||
|
||||
def _poly_summary(cell: Dict[str, Any]) -> bool:
|
||||
return str((cell.get("custom") or {}).get("polymerizationType") or "1") == "3"
|
||||
|
||||
|
||||
def _poly_group(cell: Dict[str, Any]) -> bool:
|
||||
return str((cell.get("custom") or {}).get("polymerizationType") or "1") == "2"
|
||||
|
||||
|
||||
def _should_merge_group(cell: Dict[str, Any]) -> bool:
|
||||
"""分组列是否合并单元格(custom.mergeCell,默认开启)。"""
|
||||
if not _poly_group(cell):
|
||||
return False
|
||||
merge = (cell.get("custom") or {}).get("mergeCell")
|
||||
if merge is None:
|
||||
return True
|
||||
if isinstance(merge, str):
|
||||
return merge.strip().lower() not in ("0", "false", "no", "")
|
||||
return bool(merge)
|
||||
|
||||
|
||||
def _add_vertical_merge_region(
|
||||
sheets: Dict[str, Any],
|
||||
sheet_id: str,
|
||||
start_row: int,
|
||||
end_row: int,
|
||||
col: int,
|
||||
) -> None:
|
||||
"""分组列:同组多行合并为一个单元格(预览/导出)。"""
|
||||
if end_row <= start_row:
|
||||
return
|
||||
sheet = sheets.get(sheet_id)
|
||||
if not sheet:
|
||||
return
|
||||
region = {
|
||||
"startRow": start_row,
|
||||
"endRow": end_row,
|
||||
"startColumn": col,
|
||||
"endColumn": col,
|
||||
}
|
||||
merges = sheet.setdefault("mergeData", [])
|
||||
if region not in merges:
|
||||
merges.append(region)
|
||||
|
||||
|
||||
def _compute_down_band_lengths(
|
||||
down_by_sheet: Dict[str, List[Dict[str, Any]]],
|
||||
datasets: Dict[str, List[Any]],
|
||||
) -> Dict[Tuple[str, int], int]:
|
||||
lengths: Dict[Tuple[str, int], int] = {}
|
||||
for sheet_id, down_cells in down_by_sheet.items():
|
||||
bands: Dict[int, List[Dict[str, Any]]] = {}
|
||||
for cell in down_cells:
|
||||
bands.setdefault(_int_coord(cell.get("row")), []).append(cell)
|
||||
for start_row, band in bands.items():
|
||||
max_len = max(
|
||||
(len(datasets.get(_dataset_name(item)) or []) for item in band),
|
||||
default=0,
|
||||
)
|
||||
lengths[(sheet_id, start_row)] = max_len
|
||||
return lengths
|
||||
|
||||
|
||||
def _shift_sheet_rows_down(
|
||||
sheets: Dict[str, Any],
|
||||
sheet_id: str,
|
||||
from_row: int,
|
||||
delta: int,
|
||||
) -> None:
|
||||
"""将 from_row 及以下的 cellData 整体下移 delta 行(列表扩展前保留汇总/static 行)。"""
|
||||
if delta <= 0:
|
||||
return
|
||||
sheet = sheets.get(sheet_id)
|
||||
if not sheet:
|
||||
return
|
||||
cell_data = sheet.get("cellData") or {}
|
||||
shifted: Dict[str, Any] = {}
|
||||
for row_key, row_obj in cell_data.items():
|
||||
row = _int_coord(row_key, 0)
|
||||
target_key = str(row + delta) if row >= from_row else row_key
|
||||
if target_key in shifted and row >= from_row:
|
||||
existing = shifted[target_key]
|
||||
if isinstance(existing, dict) and isinstance(row_obj, dict):
|
||||
merged = {**existing, **row_obj}
|
||||
shifted[target_key] = merged
|
||||
else:
|
||||
shifted[target_key] = row_obj
|
||||
else:
|
||||
shifted[target_key] = row_obj
|
||||
sheet["cellData"] = shifted
|
||||
|
||||
|
||||
def _summary_filter_rows(
|
||||
cell: Dict[str, Any],
|
||||
rows: List[Dict[str, Any]],
|
||||
by_pos: Dict[CellKey, Dict[str, Any]],
|
||||
by_sheet: Dict[str, List[Dict[str, Any]]],
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""汇总格:仅 custom 父格按切片统计;default/none 对整表数据集聚合(合计行)。"""
|
||||
custom = cell.get("custom") or {}
|
||||
left_pt = custom.get("leftParentCellType") or "default"
|
||||
top_pt = custom.get("topParentCellType") or "default"
|
||||
if left_pt != "custom" and top_pt != "custom":
|
||||
return rows
|
||||
left, top = resolve_parents(cell, by_pos, by_sheet)
|
||||
left_bind = top_bind = None
|
||||
if left:
|
||||
lb = build_bind_list(left, rows)
|
||||
left_bind = lb[0].data_list if lb else None
|
||||
if top:
|
||||
tb = build_bind_list(top, rows)
|
||||
top_bind = tb[0].data_list if tb else None
|
||||
return filter_rows_by_parents(
|
||||
rows,
|
||||
cell,
|
||||
left_parent=left,
|
||||
top_parent=top,
|
||||
left_bind=left_bind,
|
||||
top_bind=top_bind,
|
||||
)
|
||||
|
||||
|
||||
def _summary_output_row(
|
||||
sheet_id: str,
|
||||
template_row: int,
|
||||
band_lengths: Dict[Tuple[str, int], int],
|
||||
) -> int:
|
||||
"""汇总格位于列表模板行下方时,输出到扩展后的末行(对齐 JNPF 合计行)。"""
|
||||
anchor: Optional[Tuple[int, int]] = None
|
||||
for (sid, start_row), max_len in band_lengths.items():
|
||||
if sid != sheet_id or max_len <= 0 or template_row <= start_row:
|
||||
continue
|
||||
if anchor is None or start_row > anchor[0]:
|
||||
anchor = (start_row, max_len)
|
||||
if anchor is None:
|
||||
return template_row
|
||||
start_row, max_len = anchor
|
||||
return template_row + (max_len - 1)
|
||||
|
||||
|
||||
def _template_style_ref(
|
||||
sheets: Dict[str, Any],
|
||||
sheet_id: str,
|
||||
row: int,
|
||||
col: int,
|
||||
) -> Optional[Any]:
|
||||
sheet = sheets.get(sheet_id)
|
||||
if not sheet:
|
||||
return None
|
||||
cell_data = sheet.get("cellData") or {}
|
||||
cell = (cell_data.get(str(row)) or {}).get(str(col))
|
||||
if not isinstance(cell, dict):
|
||||
return None
|
||||
return cell.get("s")
|
||||
|
||||
|
||||
def _set_cell_value(
|
||||
sheets: Dict[str, Any],
|
||||
sheet_id: str,
|
||||
row: int,
|
||||
col: int,
|
||||
value: Any,
|
||||
*,
|
||||
style_template_row: Optional[int] = None,
|
||||
style_template_col: Optional[int] = None,
|
||||
) -> None:
|
||||
sheet = sheets.get(sheet_id)
|
||||
if not sheet:
|
||||
return
|
||||
cell_data = sheet.setdefault("cellData", {})
|
||||
row_data = cell_data.setdefault(str(row), {})
|
||||
cell_obj = row_data.setdefault(str(col), {})
|
||||
style_ref = None
|
||||
if style_template_row is not None:
|
||||
style_ref = _template_style_ref(sheets, sheet_id, style_template_row, col)
|
||||
elif style_template_col is not None:
|
||||
style_ref = _template_style_ref(sheets, sheet_id, row, style_template_col)
|
||||
if style_ref is not None:
|
||||
cell_obj["s"] = style_ref
|
||||
if value is None:
|
||||
cell_obj["v"] = ""
|
||||
elif isinstance(value, (dict, list)):
|
||||
cell_obj["v"] = str(value)
|
||||
else:
|
||||
cell_obj["v"] = value
|
||||
|
||||
|
||||
def _apply_fill_empty(
|
||||
sheets: Dict[str, Any],
|
||||
sheet_id: str,
|
||||
custom: Dict[str, Any],
|
||||
*,
|
||||
direction: str,
|
||||
anchor_row: int,
|
||||
anchor_col: int,
|
||||
data_len: int,
|
||||
) -> None:
|
||||
if not custom.get("fillEmptyRows"):
|
||||
return
|
||||
try:
|
||||
fill_n = int(custom.get("fillEmptyNum") or 1)
|
||||
except (TypeError, ValueError):
|
||||
fill_n = 1
|
||||
if fill_n < 1:
|
||||
return
|
||||
for offset in range(fill_n):
|
||||
if direction == "down":
|
||||
_set_cell_value(
|
||||
sheets, sheet_id, anchor_row + data_len + offset, anchor_col, ""
|
||||
)
|
||||
else:
|
||||
_set_cell_value(
|
||||
sheets, sheet_id, anchor_row, anchor_col + data_len + offset, ""
|
||||
)
|
||||
|
||||
|
||||
def _slot_at_index(slots: List[ExpandedSlot], index: int) -> Optional[ExpandedSlot]:
|
||||
if 0 <= index < len(slots):
|
||||
return slots[index]
|
||||
return None
|
||||
|
||||
|
||||
def _slot_covering(slots: List[ExpandedSlot], pos: int) -> Optional[ExpandedSlot]:
|
||||
for s in slots:
|
||||
if s.start <= pos < s.end:
|
||||
return s
|
||||
return None
|
||||
|
||||
|
||||
def _children_of(
|
||||
parent: Dict[str, Any],
|
||||
candidates: List[Dict[str, Any]],
|
||||
by_pos: Dict[CellKey, Dict[str, Any]],
|
||||
by_sheet: Dict[str, List[Dict[str, Any]]],
|
||||
) -> List[Dict[str, Any]]:
|
||||
pk = cell_key(parent)
|
||||
out: List[Dict[str, Any]] = []
|
||||
for c in candidates:
|
||||
left, top = resolve_parents(c, by_pos, by_sheet)
|
||||
if (left and cell_key(left) == pk) or (top and cell_key(top) == pk):
|
||||
out.append(c)
|
||||
return sorted(out, key=lambda x: (_int_coord(x.get("row")), _int_coord(x.get("col"))))
|
||||
|
||||
|
||||
def _is_down_root(
|
||||
cell: Dict[str, Any],
|
||||
down_keys: set[CellKey],
|
||||
by_pos: Dict[CellKey, Dict[str, Any]],
|
||||
by_sheet: Dict[str, List[Dict[str, Any]]],
|
||||
) -> bool:
|
||||
left, top = resolve_parents(cell, by_pos, by_sheet)
|
||||
if left and cell_key(left) in down_keys:
|
||||
return False
|
||||
if top and cell_key(top) in down_keys:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _rows_for_bind(
|
||||
rows: List[Dict[str, Any]],
|
||||
cell: Dict[str, Any],
|
||||
*,
|
||||
left_parent: Optional[Dict[str, Any]],
|
||||
top_parent: Optional[Dict[str, Any]],
|
||||
left_slot: Optional[ExpandedSlot],
|
||||
top_slot: Optional[ExpandedSlot],
|
||||
) -> List[Dict[str, Any]]:
|
||||
left_bind = left_slot.data_list if left_slot else None
|
||||
top_bind = top_slot.data_list if top_slot else None
|
||||
return filter_rows_by_parents(
|
||||
rows,
|
||||
cell,
|
||||
left_parent=left_parent,
|
||||
top_parent=top_parent,
|
||||
left_bind=left_bind,
|
||||
top_bind=top_bind,
|
||||
)
|
||||
|
||||
|
||||
def apply_pending_group_merges(
|
||||
snapshot: Dict[str, Any],
|
||||
pending: List[Tuple[str, int, int, int]],
|
||||
) -> Dict[str, Any]:
|
||||
sheets = snapshot.get("sheets") or {}
|
||||
for sheet_id, col, start_row, end_row in pending:
|
||||
_add_vertical_merge_region(sheets, sheet_id, start_row, end_row, col)
|
||||
return snapshot
|
||||
|
||||
|
||||
def _visit_down(
|
||||
cell: Dict[str, Any],
|
||||
rows: List[Dict[str, Any]],
|
||||
row_cursor: int,
|
||||
*,
|
||||
sheets: Dict[str, Any],
|
||||
sheet_id: str,
|
||||
down_cells: List[Dict[str, Any]],
|
||||
datasets: Dict[str, List[Any]],
|
||||
by_pos: Dict[CellKey, Dict[str, Any]],
|
||||
by_sheet: Dict[str, List[Dict[str, Any]]],
|
||||
row_registry: Dict[CellKey, List[ExpandedSlot]],
|
||||
bind_index: int = 0,
|
||||
left_slot: Optional[ExpandedSlot] = None,
|
||||
top_slot: Optional[ExpandedSlot] = None,
|
||||
parent_scoped: bool = False,
|
||||
pending_group_merges: Optional[List[Tuple[str, int, int, int]]] = None,
|
||||
) -> int:
|
||||
custom = cell.get("custom") or {}
|
||||
field = resolve_field_path(_field_name(cell), _dataset_name(cell))
|
||||
col = _int_coord(cell.get("col"))
|
||||
anchor_row = _int_coord(cell.get("row"))
|
||||
left_p, top_p = resolve_parents(cell, by_pos, by_sheet)
|
||||
|
||||
if not parent_scoped:
|
||||
if left_slot is None and left_p:
|
||||
left_slot = _slot_at_index(
|
||||
row_registry.get(cell_key(left_p), []), bind_index
|
||||
)
|
||||
if top_slot is None and top_p:
|
||||
top_slot = _slot_at_index(
|
||||
row_registry.get(cell_key(top_p), []), bind_index
|
||||
)
|
||||
filtered = _rows_for_bind(
|
||||
rows,
|
||||
cell,
|
||||
left_parent=left_p,
|
||||
top_parent=top_p,
|
||||
left_slot=left_slot,
|
||||
top_slot=top_slot,
|
||||
)
|
||||
else:
|
||||
filtered = [r if isinstance(r, dict) else {} for r in rows]
|
||||
|
||||
binds = build_bind_list(cell, filtered)
|
||||
children = _children_of(cell, down_cells, by_pos, by_sheet)
|
||||
child_cols = {_int_coord(ch.get("col")) for ch in children}
|
||||
slots: List[ExpandedSlot] = []
|
||||
end_row = row_cursor
|
||||
|
||||
for bi, bind in enumerate(binds):
|
||||
block_start = end_row
|
||||
if children:
|
||||
child_end = block_start
|
||||
for ch in children:
|
||||
child_end = _visit_down(
|
||||
ch,
|
||||
bind.data_list,
|
||||
block_start,
|
||||
sheets=sheets,
|
||||
sheet_id=sheet_id,
|
||||
down_cells=down_cells,
|
||||
datasets=datasets,
|
||||
by_pos=by_pos,
|
||||
by_sheet=by_sheet,
|
||||
row_registry=row_registry,
|
||||
bind_index=bi,
|
||||
parent_scoped=True,
|
||||
pending_group_merges=pending_group_merges,
|
||||
)
|
||||
block_len = max(1, child_end - block_start)
|
||||
else:
|
||||
block_len = expand_span(bind, cell)
|
||||
|
||||
if col not in child_cols:
|
||||
style_row = anchor_row
|
||||
if _poly_list(cell):
|
||||
for i in range(block_len):
|
||||
r = block_start + i
|
||||
val = (
|
||||
_get_nested_value(bind.data_list[i], field)
|
||||
if i < len(bind.data_list)
|
||||
else ""
|
||||
)
|
||||
_set_cell_value(
|
||||
sheets,
|
||||
sheet_id,
|
||||
r,
|
||||
col,
|
||||
val,
|
||||
style_template_row=style_row,
|
||||
)
|
||||
elif _poly_group(cell):
|
||||
for i in range(block_len):
|
||||
r = block_start + i
|
||||
_set_cell_value(
|
||||
sheets,
|
||||
sheet_id,
|
||||
r,
|
||||
col,
|
||||
bind.value,
|
||||
style_template_row=style_row,
|
||||
)
|
||||
if (
|
||||
pending_group_merges is not None
|
||||
and block_len > 1
|
||||
and _should_merge_group(cell)
|
||||
):
|
||||
pending_group_merges.append(
|
||||
(sheet_id, col, block_start, block_start + block_len - 1)
|
||||
)
|
||||
else:
|
||||
for i in range(block_len):
|
||||
r = block_start + i
|
||||
_set_cell_value(
|
||||
sheets,
|
||||
sheet_id,
|
||||
r,
|
||||
col,
|
||||
bind.value,
|
||||
style_template_row=style_row,
|
||||
)
|
||||
|
||||
slots.append(
|
||||
ExpandedSlot(bind.value, bind.data_list, block_start, block_start + block_len)
|
||||
)
|
||||
end_row = block_start + block_len
|
||||
|
||||
key = cell_key(cell)
|
||||
row_registry[key] = row_registry.get(key, []) + slots
|
||||
|
||||
if custom.get("fillEmptyRows"):
|
||||
_apply_fill_empty(
|
||||
sheets,
|
||||
sheet_id,
|
||||
custom,
|
||||
direction="down",
|
||||
anchor_row=anchor_row,
|
||||
anchor_col=col,
|
||||
data_len=end_row - row_cursor,
|
||||
)
|
||||
return end_row
|
||||
|
||||
|
||||
def _expand_sheet_down(
|
||||
sheets: Dict[str, Any],
|
||||
sheet_id: str,
|
||||
down_cells: List[Dict[str, Any]],
|
||||
datasets: Dict[str, List[Any]],
|
||||
by_pos: Dict[CellKey, Dict[str, Any]],
|
||||
by_sheet: Dict[str, List[Dict[str, Any]]],
|
||||
pending_group_merges: Optional[List[Tuple[str, int, int, int]]] = None,
|
||||
) -> None:
|
||||
down_keys = {cell_key(c) for c in down_cells}
|
||||
row_registry: Dict[CellKey, List[ExpandedSlot]] = {}
|
||||
roots = [c for c in down_cells if _is_down_root(c, down_keys, by_pos, by_sheet)]
|
||||
if not roots:
|
||||
roots = down_cells
|
||||
|
||||
bands: Dict[int, List[Dict[str, Any]]] = {}
|
||||
for c in roots:
|
||||
bands.setdefault(_int_coord(c.get("row")), []).append(c)
|
||||
|
||||
for start_row in sorted(bands.keys()):
|
||||
cursor = start_row
|
||||
for root in sorted(bands[start_row], key=lambda x: _int_coord(x.get("col"))):
|
||||
ds = _dataset_name(root)
|
||||
raw = datasets.get(ds) or []
|
||||
rows = [r if isinstance(r, dict) else {} for r in raw]
|
||||
cursor = max(
|
||||
cursor,
|
||||
_visit_down(
|
||||
root,
|
||||
rows,
|
||||
cursor,
|
||||
sheets=sheets,
|
||||
sheet_id=sheet_id,
|
||||
down_cells=down_cells,
|
||||
datasets=datasets,
|
||||
by_pos=by_pos,
|
||||
by_sheet=by_sheet,
|
||||
row_registry=row_registry,
|
||||
pending_group_merges=pending_group_merges,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _legacy_down_band(
|
||||
sheets: Dict[str, Any],
|
||||
sheet_id: str,
|
||||
start_row: int,
|
||||
band: List[Dict[str, Any]],
|
||||
datasets: Dict[str, List[Any]],
|
||||
pending_group_merges: Optional[List[Tuple[str, int, int, int]]] = None,
|
||||
) -> None:
|
||||
max_len = 0
|
||||
for item in band:
|
||||
max_len = max(max_len, len(datasets.get(_dataset_name(item)) or []))
|
||||
for item in sorted(band, key=lambda x: _int_coord(x.get("col"))):
|
||||
ds = _dataset_name(item)
|
||||
field = resolve_field_path(_field_name(item), ds)
|
||||
col = _int_coord(item.get("col"))
|
||||
data_rows = [r if isinstance(r, dict) else {} for r in datasets.get(ds) or []]
|
||||
custom = item.get("custom") or {}
|
||||
if _poly_group(item):
|
||||
binds = build_bind_list(item, data_rows)
|
||||
row_cursor = start_row
|
||||
for bind in binds:
|
||||
span = max(1, len(bind.data_list))
|
||||
for i in range(span):
|
||||
_set_cell_value(
|
||||
sheets,
|
||||
sheet_id,
|
||||
row_cursor + i,
|
||||
col,
|
||||
bind.value,
|
||||
style_template_row=start_row,
|
||||
)
|
||||
if (
|
||||
span > 1
|
||||
and pending_group_merges is not None
|
||||
and _should_merge_group(item)
|
||||
):
|
||||
pending_group_merges.append(
|
||||
(sheet_id, col, row_cursor, row_cursor + span - 1)
|
||||
)
|
||||
row_cursor += span
|
||||
if custom.get("fillEmptyRows"):
|
||||
_apply_fill_empty(
|
||||
sheets,
|
||||
sheet_id,
|
||||
custom,
|
||||
direction="down",
|
||||
anchor_row=start_row,
|
||||
anchor_col=col,
|
||||
data_len=row_cursor - start_row,
|
||||
)
|
||||
continue
|
||||
for i in range(max_len):
|
||||
if i < len(data_rows):
|
||||
val = _get_nested_value(data_rows[i], field)
|
||||
else:
|
||||
val = ""
|
||||
_set_cell_value(
|
||||
sheets,
|
||||
sheet_id,
|
||||
start_row + i,
|
||||
col,
|
||||
val,
|
||||
style_template_row=start_row,
|
||||
)
|
||||
if custom.get("fillEmptyRows"):
|
||||
_apply_fill_empty(
|
||||
sheets,
|
||||
sheet_id,
|
||||
custom,
|
||||
direction="down",
|
||||
anchor_row=start_row,
|
||||
anchor_col=col,
|
||||
data_len=max_len,
|
||||
)
|
||||
|
||||
|
||||
def _has_down_parent_link(
|
||||
band: List[Dict[str, Any]],
|
||||
down_keys: set[CellKey],
|
||||
by_pos: Dict[CellKey, Dict[str, Any]],
|
||||
by_sheet: Dict[str, List[Dict[str, Any]]],
|
||||
) -> bool:
|
||||
for c in band:
|
||||
left, top = resolve_parents(c, by_pos, by_sheet)
|
||||
if left and cell_key(left) in down_keys:
|
||||
return True
|
||||
if top and cell_key(top) in down_keys:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _visit_right(
|
||||
cell: Dict[str, Any],
|
||||
rows: List[Dict[str, Any]],
|
||||
col_cursor: int,
|
||||
*,
|
||||
sheets: Dict[str, Any],
|
||||
sheet_id: str,
|
||||
row: int,
|
||||
right_cells: List[Dict[str, Any]],
|
||||
by_pos: Dict[CellKey, Dict[str, Any]],
|
||||
by_sheet: Dict[str, List[Dict[str, Any]]],
|
||||
col_registry: Dict[CellKey, List[ExpandedSlot]],
|
||||
bind_index: int = 0,
|
||||
left_slot: Optional[ExpandedSlot] = None,
|
||||
top_slot: Optional[ExpandedSlot] = None,
|
||||
parent_scoped: bool = False,
|
||||
) -> int:
|
||||
custom = cell.get("custom") or {}
|
||||
field = resolve_field_path(_field_name(cell), _dataset_name(cell))
|
||||
col = _int_coord(cell.get("col"))
|
||||
anchor_col = col
|
||||
left_p, top_p = resolve_parents(cell, by_pos, by_sheet)
|
||||
|
||||
if not parent_scoped:
|
||||
if left_slot is None and left_p:
|
||||
left_slot = _slot_at_index(
|
||||
col_registry.get(cell_key(left_p), []), bind_index
|
||||
)
|
||||
if top_slot is None and top_p:
|
||||
top_slot = _slot_at_index(
|
||||
col_registry.get(cell_key(top_p), []), bind_index
|
||||
)
|
||||
filtered = _rows_for_bind(
|
||||
rows,
|
||||
cell,
|
||||
left_parent=left_p,
|
||||
top_parent=top_p,
|
||||
left_slot=left_slot,
|
||||
top_slot=top_slot,
|
||||
)
|
||||
else:
|
||||
filtered = [r if isinstance(r, dict) else {} for r in rows]
|
||||
|
||||
binds = build_bind_list(cell, filtered)
|
||||
children = _children_of(cell, right_cells, by_pos, by_sheet)
|
||||
child_cols = {_int_coord(ch.get("col")) for ch in children}
|
||||
slots: List[ExpandedSlot] = []
|
||||
end_col = col_cursor
|
||||
|
||||
for bi, bind in enumerate(binds):
|
||||
block_start = end_col
|
||||
if children:
|
||||
child_end = block_start
|
||||
for ch in children:
|
||||
child_end = _visit_right(
|
||||
ch,
|
||||
bind.data_list,
|
||||
block_start,
|
||||
sheets=sheets,
|
||||
sheet_id=sheet_id,
|
||||
row=row,
|
||||
right_cells=right_cells,
|
||||
by_pos=by_pos,
|
||||
by_sheet=by_sheet,
|
||||
col_registry=col_registry,
|
||||
bind_index=bi,
|
||||
parent_scoped=True,
|
||||
)
|
||||
block_len = max(1, child_end - block_start)
|
||||
else:
|
||||
block_len = expand_span(bind, cell)
|
||||
|
||||
if col not in child_cols:
|
||||
for i in range(block_len):
|
||||
c = block_start + i
|
||||
if _poly_list(cell):
|
||||
val = (
|
||||
_get_nested_value(bind.data_list[i], field)
|
||||
if i < len(bind.data_list)
|
||||
else ""
|
||||
)
|
||||
else:
|
||||
val = bind.value
|
||||
_set_cell_value(
|
||||
sheets,
|
||||
sheet_id,
|
||||
row,
|
||||
c,
|
||||
val,
|
||||
style_template_col=anchor_col,
|
||||
)
|
||||
|
||||
slots.append(
|
||||
ExpandedSlot(bind.value, bind.data_list, block_start, block_start + block_len)
|
||||
)
|
||||
end_col = block_start + block_len
|
||||
|
||||
key = cell_key(cell)
|
||||
col_registry[key] = col_registry.get(key, []) + slots
|
||||
|
||||
if custom.get("fillEmptyRows"):
|
||||
_apply_fill_empty(
|
||||
sheets,
|
||||
sheet_id,
|
||||
custom,
|
||||
direction="right",
|
||||
anchor_row=row,
|
||||
anchor_col=col,
|
||||
data_len=end_col - col_cursor,
|
||||
)
|
||||
return end_col
|
||||
|
||||
|
||||
def _is_right_root(
|
||||
cell: Dict[str, Any],
|
||||
right_keys: set[CellKey],
|
||||
by_pos: Dict[CellKey, Dict[str, Any]],
|
||||
by_sheet: Dict[str, List[Dict[str, Any]]],
|
||||
) -> bool:
|
||||
left, top = resolve_parents(cell, by_pos, by_sheet)
|
||||
if left and cell_key(left) in right_keys:
|
||||
return False
|
||||
if top and cell_key(top) in right_keys:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _expand_sheet_right(
|
||||
sheets: Dict[str, Any],
|
||||
sheet_id: str,
|
||||
row: int,
|
||||
right_cells: List[Dict[str, Any]],
|
||||
datasets: Dict[str, List[Any]],
|
||||
by_pos: Dict[CellKey, Dict[str, Any]],
|
||||
by_sheet: Dict[str, List[Dict[str, Any]]],
|
||||
) -> None:
|
||||
right_keys = {cell_key(c) for c in right_cells}
|
||||
col_registry: Dict[CellKey, List[ExpandedSlot]] = {}
|
||||
roots = [c for c in right_cells if _is_right_root(c, right_keys, by_pos, by_sheet)]
|
||||
if not roots:
|
||||
roots = right_cells
|
||||
|
||||
cursor = min(_int_coord(c.get("col")) for c in roots)
|
||||
for root in sorted(roots, key=lambda x: _int_coord(x.get("col"))):
|
||||
ds = _dataset_name(root)
|
||||
raw = datasets.get(ds) or []
|
||||
rows_data = [r if isinstance(r, dict) else {} for r in raw]
|
||||
cursor = max(
|
||||
cursor,
|
||||
_visit_right(
|
||||
root,
|
||||
rows_data,
|
||||
cursor,
|
||||
sheets=sheets,
|
||||
sheet_id=sheet_id,
|
||||
row=row,
|
||||
right_cells=right_cells,
|
||||
by_pos=by_pos,
|
||||
by_sheet=by_sheet,
|
||||
col_registry=col_registry,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _legacy_right_band(
|
||||
sheets: Dict[str, Any],
|
||||
sheet_id: str,
|
||||
row: int,
|
||||
band: List[Dict[str, Any]],
|
||||
datasets: Dict[str, List[Any]],
|
||||
) -> None:
|
||||
max_len = max((len(datasets.get(_dataset_name(c)) or []) for c in band), default=0)
|
||||
for item in sorted(band, key=lambda x: _int_coord(x.get("col"))):
|
||||
ds = _dataset_name(item)
|
||||
field = resolve_field_path(_field_name(item), ds)
|
||||
start_col = _int_coord(item.get("col"))
|
||||
data_rows = datasets.get(ds) or []
|
||||
for i in range(max_len):
|
||||
if i < len(data_rows) and isinstance(data_rows[i], dict):
|
||||
val = _get_nested_value(data_rows[i], field)
|
||||
else:
|
||||
val = ""
|
||||
_set_cell_value(
|
||||
sheets,
|
||||
sheet_id,
|
||||
row,
|
||||
start_col + i,
|
||||
val,
|
||||
style_template_col=start_col,
|
||||
)
|
||||
|
||||
|
||||
def apply_data_source_cells(
|
||||
snapshot: Dict[str, Any],
|
||||
cells_meta: Dict[str, Any],
|
||||
datasets: Dict[str, List[Any]],
|
||||
) -> Tuple[Dict[str, Any], List[Tuple[str, int, int, int]]]:
|
||||
result = copy.deepcopy(snapshot)
|
||||
sheets = result.get("sheets") or {}
|
||||
pending_group_merges: List[Tuple[str, int, int, int]] = []
|
||||
cell_list = cells_meta.get("cells") or []
|
||||
by_pos, by_sheet = _index_data_sources(cell_list)
|
||||
|
||||
down_by_sheet: Dict[str, List[Dict[str, Any]]] = {}
|
||||
right_by_sheet_row: Dict[Tuple[str, int], List[Dict[str, Any]]] = {}
|
||||
singles: List[dict] = []
|
||||
|
||||
for cell in cell_list:
|
||||
if cell.get("type") != "dataSource":
|
||||
continue
|
||||
custom = cell.get("custom") or {}
|
||||
if not _dataset_name(cell) or not _field_name(cell):
|
||||
continue
|
||||
direction = _expand_direction(custom)
|
||||
sheet_id = str(cell.get("sheet", "sheet1"))
|
||||
|
||||
if direction == "down":
|
||||
down_by_sheet.setdefault(sheet_id, []).append(cell)
|
||||
elif direction == "right":
|
||||
row = _int_coord(cell.get("row"), 0)
|
||||
right_by_sheet_row.setdefault((sheet_id, row), []).append(cell)
|
||||
else:
|
||||
singles.append(cell)
|
||||
|
||||
band_lengths = _compute_down_band_lengths(down_by_sheet, datasets)
|
||||
for (sheet_id, start_row), max_len in sorted(
|
||||
band_lengths.items(), key=lambda x: x[0][1], reverse=True
|
||||
):
|
||||
delta = max(0, max_len - 1)
|
||||
if delta > 0:
|
||||
_shift_sheet_rows_down(sheets, sheet_id, start_row + 1, delta)
|
||||
|
||||
for sheet_id, down_cells in down_by_sheet.items():
|
||||
down_keys = {cell_key(c) for c in down_cells}
|
||||
bands: Dict[int, List[Dict[str, Any]]] = {}
|
||||
for c in down_cells:
|
||||
bands.setdefault(_int_coord(c.get("row")), []).append(c)
|
||||
uses_tree = False
|
||||
for band in bands.values():
|
||||
if _has_down_parent_link(band, down_keys, by_pos, by_sheet):
|
||||
uses_tree = True
|
||||
break
|
||||
if uses_tree:
|
||||
_expand_sheet_down(
|
||||
sheets,
|
||||
sheet_id,
|
||||
down_cells,
|
||||
datasets,
|
||||
by_pos,
|
||||
by_sheet,
|
||||
pending_group_merges,
|
||||
)
|
||||
else:
|
||||
for start_row, band in bands.items():
|
||||
_legacy_down_band(
|
||||
sheets,
|
||||
sheet_id,
|
||||
start_row,
|
||||
band,
|
||||
datasets,
|
||||
pending_group_merges,
|
||||
)
|
||||
|
||||
for (sheet_id, row), band in right_by_sheet_row.items():
|
||||
right_keys = {cell_key(c) for c in band}
|
||||
if _has_down_parent_link(band, right_keys, by_pos, by_sheet):
|
||||
_expand_sheet_right(
|
||||
sheets, sheet_id, row, band, datasets, by_pos, by_sheet
|
||||
)
|
||||
else:
|
||||
_legacy_right_band(sheets, sheet_id, row, band, datasets)
|
||||
|
||||
for cell in singles:
|
||||
sheet_id = str(cell.get("sheet", "sheet1"))
|
||||
template_row = _int_coord(cell.get("row"), 0)
|
||||
col = _int_coord(cell.get("col"), 0)
|
||||
row = (
|
||||
_summary_output_row(sheet_id, template_row, band_lengths)
|
||||
if _poly_summary(cell)
|
||||
else template_row
|
||||
)
|
||||
raw = datasets.get(_dataset_name(cell)) or []
|
||||
rows = [r if isinstance(r, dict) else {} for r in raw]
|
||||
if _poly_summary(cell):
|
||||
filtered = _summary_filter_rows(cell, rows, by_pos, by_sheet)
|
||||
else:
|
||||
left, top = resolve_parents(cell, by_pos, by_sheet)
|
||||
left_bind = top_bind = None
|
||||
if left:
|
||||
lb = build_bind_list(left, rows)
|
||||
left_bind = lb[0].data_list if lb else None
|
||||
if top:
|
||||
tb = build_bind_list(top, rows)
|
||||
top_bind = tb[0].data_list if tb else None
|
||||
filtered = filter_rows_by_parents(
|
||||
rows,
|
||||
cell,
|
||||
left_parent=left,
|
||||
top_parent=top,
|
||||
left_bind=left_bind,
|
||||
top_bind=top_bind,
|
||||
)
|
||||
binds = build_bind_list(cell, filtered)
|
||||
val = binds[0].value if binds else ""
|
||||
_set_cell_value(sheets, sheet_id, row, col, val)
|
||||
|
||||
result["sheets"] = sheets
|
||||
return result, pending_group_merges
|
||||
Reference in New Issue
Block a user