datraxlsx 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,122 @@
1
+ Metadata-Version: 2.4
2
+ Name: datraxlsx
3
+ Version: 0.3.0
4
+ Summary: 把 DataFrame 保存为带有 Excel 格式的 XLSX 文件的可复用样式工具
5
+ Author: SidneyZhang
6
+ Author-email: SidneyZhang <zly@lyzhang.me>
7
+ License-Expression: MIT
8
+ Requires-Dist: openpyxl>=3.1.5
9
+ Requires-Dist: pandas>=3.0.3
10
+ Requires-Dist: polars>=1.42.1
11
+ Requires-Dist: pyarrow>=14.0.0
12
+ Requires-Python: >=3.13
13
+ Description-Content-Type: text/markdown
14
+
15
+ # DaTraXLSX
16
+
17
+ 这是一个把DataFrame保存为带有Excel格式的XLSX文件的工具。
18
+
19
+ ## 安装
20
+
21
+ datraxlsx 需要Python 3.13及以上版本,依赖 openpyxl、pandas、polars。
22
+
23
+ ```bash
24
+ # 本地开发安装
25
+ pip install -e .
26
+
27
+ # 或使用 uv
28
+ uv add datraxlsx
29
+ ```
30
+
31
+ ## 核心概念
32
+
33
+ datraxlsx 由四个可复用的构建块组成:
34
+
35
+ - `CellStyle` — 可复用单元格样式,聚合 `font`/`fill`/`alignment`/`border`/`number_format` 五个字段(均可选),通过 `apply(cell)` 应用到单元格。
36
+ - `RowBorder` — 以"边"描述一行单元格的边框:`top`/`bottom`/`left_edge`/`right_edge`/`inner_vertical`。它会自动区分左右边缘与内部竖线,无需手动处理首尾列。
37
+ - `TableTheme` — 聚合 `title`/`header`/`body`/`summary` 四套 `CellStyle` 及其对应 `RowBorder`,外加 `title_height`/`row_height`/`column_width`。`TableTheme.default()` 开箱即用,`.replace(...)` 定制单个字段。
38
+ - `ExcelWriter` + `Sheet` — 工作簿与工作表写入器。`ExcelWriter` 持有主题与游标起点,通过 `.sheet(name)` 创建工作表;`Sheet` 提供 `use`/`write_title`/`write_data`/`write_summary`/`write_cell`/`set_column_width` 等方法,支持多 sheet 写入。
39
+
40
+ ## 快速开始
41
+
42
+ 零配置即可写出带标题、表头、数据、合计行的格式化报表:
43
+
44
+ ```python
45
+ import pandas as pd
46
+ from datraxlsx import ExcelWriter
47
+
48
+ df = pd.DataFrame({"产品": ["A", "B", "C"], "数量": [10, 20, 30], "金额": [100.5, 200.5, 300.5]})
49
+
50
+ with ExcelWriter("output.xlsx") as wb:
51
+ ws = wb.sheet("销售")
52
+ ws.use(df)
53
+ ws.write_title("销售报表")
54
+ ws.write_data()
55
+ ws.write_summary(agg_cols=["数量", "金额"], merge_cols=["产品"])
56
+ ```
57
+
58
+ polars DataFrame 同样可用,`use()` 会自动转换。
59
+
60
+ ## 复用主题
61
+
62
+ 定义一次主题,跨 sheet、跨文件复用。`TableTheme.default().replace(...)` 只覆盖传入的字段,其余沿用默认值:
63
+
64
+ ```python
65
+ from datraxlsx import ExcelWriter, TableTheme, CellStyle, RowBorder
66
+ from openpyxl.styles import Font, PatternFill, Side
67
+
68
+ theme = TableTheme.default().replace(
69
+ header=CellStyle(
70
+ font=Font(name="微软雅黑", size=12, bold=True, color="FFFFFF"),
71
+ fill=PatternFill(patternType="solid", fgColor="0066CC"),
72
+ ),
73
+ header_border=RowBorder(
74
+ top=Side(border_style="thick"),
75
+ bottom=Side(border_style="double"),
76
+ ),
77
+ )
78
+
79
+ with ExcelWriter("output.xlsx", theme=theme) as wb:
80
+ for name, df in sheets.items():
81
+ ws = wb.sheet(name)
82
+ ws.use(df)
83
+ ws.write_title(name)
84
+ ws.write_data()
85
+ ```
86
+
87
+ ## 按列名指定数字格式
88
+
89
+ `write_data` 与 `write_summary` 的 `number_format` 参数支持字符串(应用到所有列)或字典(按列名指定):
90
+
91
+ ```python
92
+ ws.write_data(number_format={"金额": "#,##0.00", "比例": "0.00%"})
93
+ ```
94
+
95
+ ## 一次性覆盖
96
+
97
+ 每个写入方法都接受 `style`/`border`/`height` 等关键字参数,传入即覆盖该次调用的主题样式,不修改主题本身:
98
+
99
+ ```python
100
+ ws.write_data(body_style=CellStyle(font=Font(italic=True)))
101
+ ```
102
+
103
+ ## API 速览
104
+
105
+ `Sheet` 方法(写入前须先调用 `use(df)`):
106
+
107
+ - `use(df, *, columns=None)` — 绑定 DataFrame(pandas 或 polars),可选重排列或选取列子集。
108
+ - `write_title(text, *, crossline=0, style=None, border=None, height=None)` — 写入跨列合并标题,`crossline` 控制跨行行数。
109
+ - `write_data(*, header_style=None, body_style=None, header_border=None, body_border=None, number_format=None, height=None)` — 写入表头与数据行。
110
+ - `write_summary(*, agg_cols=None, pass_cols=None, agg_fun="sum", row_name="总计", merge_cols=None, pass_seq="--", style=None, border=None, number_format=None, height=None)` — 写入合计行,`agg_cols` 求和、`pass_cols` 填占位符、`merge_cols` 合并前导列。
111
+ - `write_cell(range, value=None, *, style=None, height=None)` — 写入或合并写入任意单元格区域,`range` 支持 `"A1"` 或 `"A1:C3"`。
112
+ - `set_column_width(width, *, columns=None)` — 设置列宽,`columns` 省略则全部列,可传列名或列名列表。
113
+
114
+ `ExcelWriter` 方法:
115
+
116
+ - `ExcelWriter(filename, *, theme=None, start_row=1, start_col=1)` — 构造工作簿,可指定主题与写入起点。
117
+ - `.sheet(name=None, *, show_gridlines=False)` — 创建工作表,默认隐藏网格线。
118
+ - `.save()` — 保存文件;作为上下文管理器使用时退出即保存。
119
+
120
+ ## 工具函数
121
+
122
+ `transColname2Letter(colnames, xlsDataRange)` 将列名映射到 Excel 列字母。传入列名列表与对应的 Excel 区域字符串(如 `"A1:D10"`),返回 `{列名: 列字母}` 字典。列名数量须与区域列数一致,否则抛出 `ValueError`。
@@ -0,0 +1,108 @@
1
+ # DaTraXLSX
2
+
3
+ 这是一个把DataFrame保存为带有Excel格式的XLSX文件的工具。
4
+
5
+ ## 安装
6
+
7
+ datraxlsx 需要Python 3.13及以上版本,依赖 openpyxl、pandas、polars。
8
+
9
+ ```bash
10
+ # 本地开发安装
11
+ pip install -e .
12
+
13
+ # 或使用 uv
14
+ uv add datraxlsx
15
+ ```
16
+
17
+ ## 核心概念
18
+
19
+ datraxlsx 由四个可复用的构建块组成:
20
+
21
+ - `CellStyle` — 可复用单元格样式,聚合 `font`/`fill`/`alignment`/`border`/`number_format` 五个字段(均可选),通过 `apply(cell)` 应用到单元格。
22
+ - `RowBorder` — 以"边"描述一行单元格的边框:`top`/`bottom`/`left_edge`/`right_edge`/`inner_vertical`。它会自动区分左右边缘与内部竖线,无需手动处理首尾列。
23
+ - `TableTheme` — 聚合 `title`/`header`/`body`/`summary` 四套 `CellStyle` 及其对应 `RowBorder`,外加 `title_height`/`row_height`/`column_width`。`TableTheme.default()` 开箱即用,`.replace(...)` 定制单个字段。
24
+ - `ExcelWriter` + `Sheet` — 工作簿与工作表写入器。`ExcelWriter` 持有主题与游标起点,通过 `.sheet(name)` 创建工作表;`Sheet` 提供 `use`/`write_title`/`write_data`/`write_summary`/`write_cell`/`set_column_width` 等方法,支持多 sheet 写入。
25
+
26
+ ## 快速开始
27
+
28
+ 零配置即可写出带标题、表头、数据、合计行的格式化报表:
29
+
30
+ ```python
31
+ import pandas as pd
32
+ from datraxlsx import ExcelWriter
33
+
34
+ df = pd.DataFrame({"产品": ["A", "B", "C"], "数量": [10, 20, 30], "金额": [100.5, 200.5, 300.5]})
35
+
36
+ with ExcelWriter("output.xlsx") as wb:
37
+ ws = wb.sheet("销售")
38
+ ws.use(df)
39
+ ws.write_title("销售报表")
40
+ ws.write_data()
41
+ ws.write_summary(agg_cols=["数量", "金额"], merge_cols=["产品"])
42
+ ```
43
+
44
+ polars DataFrame 同样可用,`use()` 会自动转换。
45
+
46
+ ## 复用主题
47
+
48
+ 定义一次主题,跨 sheet、跨文件复用。`TableTheme.default().replace(...)` 只覆盖传入的字段,其余沿用默认值:
49
+
50
+ ```python
51
+ from datraxlsx import ExcelWriter, TableTheme, CellStyle, RowBorder
52
+ from openpyxl.styles import Font, PatternFill, Side
53
+
54
+ theme = TableTheme.default().replace(
55
+ header=CellStyle(
56
+ font=Font(name="微软雅黑", size=12, bold=True, color="FFFFFF"),
57
+ fill=PatternFill(patternType="solid", fgColor="0066CC"),
58
+ ),
59
+ header_border=RowBorder(
60
+ top=Side(border_style="thick"),
61
+ bottom=Side(border_style="double"),
62
+ ),
63
+ )
64
+
65
+ with ExcelWriter("output.xlsx", theme=theme) as wb:
66
+ for name, df in sheets.items():
67
+ ws = wb.sheet(name)
68
+ ws.use(df)
69
+ ws.write_title(name)
70
+ ws.write_data()
71
+ ```
72
+
73
+ ## 按列名指定数字格式
74
+
75
+ `write_data` 与 `write_summary` 的 `number_format` 参数支持字符串(应用到所有列)或字典(按列名指定):
76
+
77
+ ```python
78
+ ws.write_data(number_format={"金额": "#,##0.00", "比例": "0.00%"})
79
+ ```
80
+
81
+ ## 一次性覆盖
82
+
83
+ 每个写入方法都接受 `style`/`border`/`height` 等关键字参数,传入即覆盖该次调用的主题样式,不修改主题本身:
84
+
85
+ ```python
86
+ ws.write_data(body_style=CellStyle(font=Font(italic=True)))
87
+ ```
88
+
89
+ ## API 速览
90
+
91
+ `Sheet` 方法(写入前须先调用 `use(df)`):
92
+
93
+ - `use(df, *, columns=None)` — 绑定 DataFrame(pandas 或 polars),可选重排列或选取列子集。
94
+ - `write_title(text, *, crossline=0, style=None, border=None, height=None)` — 写入跨列合并标题,`crossline` 控制跨行行数。
95
+ - `write_data(*, header_style=None, body_style=None, header_border=None, body_border=None, number_format=None, height=None)` — 写入表头与数据行。
96
+ - `write_summary(*, agg_cols=None, pass_cols=None, agg_fun="sum", row_name="总计", merge_cols=None, pass_seq="--", style=None, border=None, number_format=None, height=None)` — 写入合计行,`agg_cols` 求和、`pass_cols` 填占位符、`merge_cols` 合并前导列。
97
+ - `write_cell(range, value=None, *, style=None, height=None)` — 写入或合并写入任意单元格区域,`range` 支持 `"A1"` 或 `"A1:C3"`。
98
+ - `set_column_width(width, *, columns=None)` — 设置列宽,`columns` 省略则全部列,可传列名或列名列表。
99
+
100
+ `ExcelWriter` 方法:
101
+
102
+ - `ExcelWriter(filename, *, theme=None, start_row=1, start_col=1)` — 构造工作簿,可指定主题与写入起点。
103
+ - `.sheet(name=None, *, show_gridlines=False)` — 创建工作表,默认隐藏网格线。
104
+ - `.save()` — 保存文件;作为上下文管理器使用时退出即保存。
105
+
106
+ ## 工具函数
107
+
108
+ `transColname2Letter(colnames, xlsDataRange)` 将列名映射到 Excel 列字母。传入列名列表与对应的 Excel 区域字符串(如 `"A1:D10"`),返回 `{列名: 列字母}` 字典。列名数量须与区域列数一致,否则抛出 `ValueError`。
@@ -0,0 +1,20 @@
1
+ [project]
2
+ name = "datraxlsx"
3
+ version = "0.3.0"
4
+ description = "把 DataFrame 保存为带有 Excel 格式的 XLSX 文件的可复用样式工具"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ authors = [
8
+ { name = "SidneyZhang", email = "zly@lyzhang.me" }
9
+ ]
10
+ requires-python = ">=3.13"
11
+ dependencies = [
12
+ "openpyxl>=3.1.5",
13
+ "pandas>=3.0.3",
14
+ "polars>=1.42.1",
15
+ "pyarrow>=14.0.0",
16
+ ]
17
+
18
+ [build-system]
19
+ requires = ["uv_build>=0.11.26,<0.12.0"]
20
+ build-backend = "uv_build"
@@ -0,0 +1,4 @@
1
+ from .styles import CellStyle, RowBorder, TableTheme
2
+ from .writer import ExcelWriter, Sheet, transColname2Letter
3
+
4
+ __all__ = ["CellStyle", "RowBorder", "TableTheme", "ExcelWriter", "Sheet", "transColname2Letter"]
@@ -0,0 +1,82 @@
1
+ """Reusable style objects for Excel writing."""
2
+ from dataclasses import dataclass, replace
3
+
4
+ from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class CellStyle:
9
+ font: Font | None = None
10
+ fill: PatternFill | None = None
11
+ alignment: Alignment | None = None
12
+ border: Border | None = None
13
+ number_format: str | None = None
14
+
15
+ def apply(self, cell) -> None:
16
+ if self.font is not None:
17
+ cell.font = self.font
18
+ if self.fill is not None:
19
+ cell.fill = self.fill
20
+ if self.alignment is not None:
21
+ cell.alignment = self.alignment
22
+ if self.border is not None:
23
+ cell.border = self.border
24
+ if self.number_format is not None:
25
+ cell.number_format = self.number_format
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class RowBorder:
30
+ top: Side | None = None
31
+ bottom: Side | None = None
32
+ left_edge: Side | None = None
33
+ right_edge: Side | None = None
34
+ inner_vertical: Side | None = None
35
+
36
+ def apply_to_range(self, ws, start_col: int, start_row: int, end_col: int, end_row: int) -> None:
37
+ for r in range(start_row, end_row + 1):
38
+ for c in range(start_col, end_col + 1):
39
+ top = self.top if r == start_row else None
40
+ bottom = self.bottom if r == end_row else None
41
+ left = self.left_edge if c == start_col else self.inner_vertical
42
+ right = self.right_edge if c == end_col else self.inner_vertical
43
+ ws.cell(row=r, column=c).border = Border(top=top, bottom=bottom, left=left, right=right)
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class TableTheme:
48
+ title: CellStyle
49
+ header: CellStyle
50
+ body: CellStyle
51
+ summary: CellStyle
52
+ title_border: RowBorder | None = None
53
+ header_border: RowBorder | None = None
54
+ body_border: RowBorder | None = None
55
+ summary_border: RowBorder | None = None
56
+ title_height: int = 75
57
+ row_height: int = 33
58
+ column_width: float = 17.5
59
+
60
+ @classmethod
61
+ def default(cls) -> "TableTheme":
62
+ return cls(
63
+ title=CellStyle(
64
+ font=Font(name="微软雅黑", size=23, bold=True),
65
+ alignment=Alignment(horizontal="center", vertical="center"),
66
+ ),
67
+ header=CellStyle(
68
+ font=Font(name="微软雅黑", size=11.5, bold=True),
69
+ alignment=Alignment(horizontal="center", vertical="center", wrapText=True),
70
+ ),
71
+ body=CellStyle(
72
+ font=Font(name="微软雅黑", size=11.5),
73
+ alignment=Alignment(horizontal="center", vertical="center"),
74
+ ),
75
+ summary=CellStyle(
76
+ font=Font(name="微软雅黑", size=11.5, bold=True),
77
+ alignment=Alignment(horizontal="center", vertical="center"),
78
+ ),
79
+ )
80
+
81
+ def replace(self, **fields) -> "TableTheme":
82
+ return replace(self, **fields)
@@ -0,0 +1,271 @@
1
+ """DataFrame-to-Excel writer with style-based configuration."""
2
+ from pathlib import Path
3
+
4
+ import pandas as pd
5
+ import polars as pl
6
+ from openpyxl import Workbook
7
+ from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
8
+ from openpyxl.utils.cell import cols_from_range, coordinate_from_string, get_column_letter
9
+ from openpyxl.worksheet import cell_range
10
+ from typing import Self
11
+
12
+ from .styles import CellStyle, RowBorder, TableTheme
13
+
14
+ # `range` is shadowed by the `write_cell` parameter; keep a builtin reference.
15
+ _range = range
16
+
17
+
18
+ def transColname2Letter(colnames: list[str], xlsDataRange: str) -> dict[str, str]:
19
+ colLetter = [i[0][0] for i in cols_from_range(xlsDataRange)]
20
+ if len(colnames) != len(colLetter):
21
+ raise ValueError("colnames must be equal to xlsDataRange")
22
+ return dict(zip(colnames, colLetter))
23
+
24
+
25
+ class ExcelWriter:
26
+ def __init__(
27
+ self,
28
+ filename: str | Path,
29
+ *,
30
+ theme: TableTheme | None = None,
31
+ start_row: int = 1,
32
+ start_col: int = 1,
33
+ ) -> None:
34
+ self._filename = filename
35
+ self._wb = Workbook()
36
+ self._theme = theme or TableTheme.default()
37
+ self._start_row = start_row
38
+ self._start_col = start_col
39
+ self._first_sheet_used = False
40
+
41
+ def sheet(self, name: str | None = None, *, show_gridlines: bool = False) -> "Sheet":
42
+ if not self._first_sheet_used:
43
+ ws = self._wb.active
44
+ self._first_sheet_used = True
45
+ else:
46
+ ws = self._wb.create_sheet()
47
+ ws.sheet_view.showGridLines = show_gridlines
48
+ if name:
49
+ ws.title = name
50
+ return Sheet(ws, self._theme, self._start_row, self._start_col)
51
+
52
+ def save(self) -> None:
53
+ self._wb.save(self._filename)
54
+
55
+ def __enter__(self) -> Self:
56
+ return self
57
+
58
+ def __exit__(self, exc_type, exc_value, traceback) -> None:
59
+ if exc_type is None:
60
+ self.save()
61
+
62
+
63
+ class Sheet:
64
+ def __init__(self, ws, theme: TableTheme, start_row: int = 1, start_col: int = 1) -> None:
65
+ self._ws = ws
66
+ self._theme = theme
67
+ self._start_row = start_row
68
+ self._start_col = start_col
69
+ self._cursor = start_row
70
+ self._df = None
71
+ self._columns = None
72
+
73
+ def use(self, df, *, columns: list[str] | None = None) -> None:
74
+ if isinstance(df, pl.DataFrame):
75
+ df = df.to_pandas()
76
+ self._df = df
77
+ self._columns = columns if columns is not None else df.columns.tolist()
78
+
79
+ def _ncols(self) -> int:
80
+ return len(self._columns)
81
+
82
+ def write_title(
83
+ self,
84
+ text: str,
85
+ *,
86
+ crossline: int = 0,
87
+ style: CellStyle | None = None,
88
+ border: RowBorder | None = None,
89
+ height: int | None = None,
90
+ ) -> None:
91
+ if self._df is None:
92
+ raise ValueError("call use(df) before write_title()")
93
+ st = style or self._theme.title
94
+ bd = border if border is not None else self._theme.title_border
95
+ h = height if height is not None else self._theme.title_height
96
+ n = self._ncols()
97
+ start_row = self._cursor
98
+ end_row = start_row + crossline
99
+ start_cell = f"{get_column_letter(self._start_col)}{start_row}"
100
+ end_cell = f"{get_column_letter(self._start_col + n - 1)}{end_row}"
101
+ if end_cell != start_cell:
102
+ self._ws.merge_cells(f"{start_cell}:{end_cell}")
103
+ self._ws[start_cell].value = text
104
+ st.apply(self._ws[start_cell])
105
+ for r in range(start_row, end_row + 1):
106
+ self._ws.row_dimensions[r].height = h
107
+ if bd is not None:
108
+ bd.apply_to_range(self._ws, self._start_col, start_row, self._start_col + n - 1, end_row)
109
+ self._cursor = end_row + 1
110
+
111
+ def write_data(
112
+ self,
113
+ *,
114
+ header_style: CellStyle | None = None,
115
+ body_style: CellStyle | None = None,
116
+ header_border: RowBorder | None = None,
117
+ body_border: RowBorder | None = None,
118
+ number_format: str | dict | None = None,
119
+ height: int | None = None,
120
+ ) -> None:
121
+ if self._df is None:
122
+ raise ValueError("call use(df) before write_data()")
123
+ hs = header_style or self._theme.header
124
+ bs = body_style or self._theme.body
125
+ hb = header_border if header_border is not None else self._theme.header_border
126
+ bb = body_border if body_border is not None else self._theme.body_border
127
+ h = height if height is not None else self._theme.row_height
128
+ n = self._ncols()
129
+ df = self._df[self._columns]
130
+
131
+ hr = self._cursor
132
+ for j, col in enumerate(self._columns):
133
+ cell = self._ws.cell(row=hr, column=self._start_col + j, value=col)
134
+ hs.apply(cell)
135
+ self._ws.row_dimensions[hr].height = h
136
+ if hb is not None:
137
+ hb.apply_to_range(self._ws, self._start_col, hr, self._start_col + n - 1, hr)
138
+ self._cursor = hr + 1
139
+
140
+ for row_values in df.to_numpy().tolist():
141
+ r = self._cursor
142
+ for j, val in enumerate(row_values):
143
+ cell = self._ws.cell(row=r, column=self._start_col + j, value=val)
144
+ bs.apply(cell)
145
+ self._ws.row_dimensions[r].height = h
146
+ if bb is not None:
147
+ bb.apply_to_range(self._ws, self._start_col, r, self._start_col + n - 1, r)
148
+ self._cursor = r + 1
149
+
150
+ if number_format is not None:
151
+ self._apply_number_format(number_format, hr + 1, self._cursor - 1)
152
+
153
+ def write_summary(
154
+ self,
155
+ *,
156
+ agg_cols: list[str] | None = None,
157
+ pass_cols: list[str] | None = None,
158
+ agg_fun: str = "sum",
159
+ row_name: str = "总计",
160
+ merge_cols: list[str] | None = None,
161
+ pass_seq: str = "--",
162
+ style: CellStyle | None = None,
163
+ border: RowBorder | None = None,
164
+ number_format: str | dict | None = None,
165
+ height: int | None = None,
166
+ ) -> None:
167
+ if self._df is None:
168
+ raise ValueError("call use(df) before write_summary()")
169
+ st = style or self._theme.summary
170
+ bd = border if border is not None else self._theme.summary_border
171
+ h = height if height is not None else self._theme.row_height
172
+ n = self._ncols()
173
+ df = self._df[self._columns]
174
+ if merge_cols:
175
+ if merge_cols != self._columns[: len(merge_cols)]:
176
+ raise ValueError("merge_cols must be the leading columns")
177
+ nm = len(merge_cols)
178
+ else:
179
+ nm = 0
180
+
181
+ values = [row_name] * nm
182
+ for col in self._columns[nm:]:
183
+ if agg_cols and col in agg_cols:
184
+ values.append(df[col].agg(agg_fun))
185
+ elif pass_cols and col in pass_cols:
186
+ values.append(pass_seq)
187
+ elif pd.api.types.is_numeric_dtype(df[col].dtype):
188
+ values.append(df[col].agg(agg_fun))
189
+ else:
190
+ values.append(df[col].iloc[0])
191
+
192
+ r = self._cursor
193
+ for j, val in enumerate(values):
194
+ cell = self._ws.cell(row=r, column=self._start_col + j, value=val)
195
+ st.apply(cell)
196
+ if nm > 0:
197
+ sc = f"{get_column_letter(self._start_col)}{r}"
198
+ ec = f"{get_column_letter(self._start_col + nm - 1)}{r}"
199
+ if ec != sc:
200
+ self._ws.merge_cells(f"{sc}:{ec}")
201
+ self._ws[sc].value = row_name
202
+ st.apply(self._ws[sc])
203
+ self._ws.row_dimensions[r].height = h
204
+ if bd is not None:
205
+ bd.apply_to_range(self._ws, self._start_col, r, self._start_col + n - 1, r)
206
+ self._cursor = r + 1
207
+ if number_format is not None:
208
+ self._apply_number_format(number_format, r, r)
209
+
210
+ def write_cell(
211
+ self,
212
+ range: str,
213
+ value=None,
214
+ *,
215
+ style: CellStyle | None = None,
216
+ height: int | None = None,
217
+ ) -> None:
218
+ h = height if height is not None else self._theme.row_height
219
+ if ":" in range:
220
+ self._ws.merge_cells(range)
221
+ top_left = range.split(":")[0]
222
+ cell = self._ws[top_left]
223
+ if value is not None:
224
+ cell.value = value
225
+ if style is not None:
226
+ style.apply(cell)
227
+ bounds = cell_range.CellRange(range_string=range).bounds
228
+ for r in _range(bounds[1], bounds[3] + 1):
229
+ self._ws.row_dimensions[r].height = h
230
+ else:
231
+ cell = self._ws[range]
232
+ if value is not None:
233
+ cell.value = value
234
+ if style is not None:
235
+ style.apply(cell)
236
+ bounds = cell_range.CellRange(range_string=range).bounds
237
+ self._ws.row_dimensions[bounds[1]].height = h
238
+
239
+ def set_column_width(self, width: float, *, columns: str | list[str] | None = None) -> None:
240
+ if self._df is None:
241
+ raise ValueError("call use(df) before set_column_width()")
242
+ if columns is None:
243
+ for j in range(self._ncols()):
244
+ self._ws.column_dimensions[get_column_letter(self._start_col + j)].width = width
245
+ elif isinstance(columns, str):
246
+ if columns in self._columns:
247
+ j = self._columns.index(columns)
248
+ letter = get_column_letter(self._start_col + j)
249
+ else:
250
+ letter = columns
251
+ self._ws.column_dimensions[letter].width = width
252
+ else:
253
+ for c in columns:
254
+ j = self._columns.index(c)
255
+ self._ws.column_dimensions[get_column_letter(self._start_col + j)].width = width
256
+
257
+ def _apply_number_format(self, number_format, first_row: int, last_row: int) -> None:
258
+ if isinstance(number_format, str):
259
+ for r in range(first_row, last_row + 1):
260
+ for j in range(self._ncols()):
261
+ self._ws.cell(row=r, column=self._start_col + j).number_format = number_format
262
+ elif isinstance(number_format, dict):
263
+ col_to_idx = {name: i for i, name in enumerate(self._columns)}
264
+ for name, fmt in number_format.items():
265
+ if name not in col_to_idx:
266
+ raise KeyError(name)
267
+ j = col_to_idx[name]
268
+ for r in range(first_row, last_row + 1):
269
+ self._ws.cell(row=r, column=self._start_col + j).number_format = fmt
270
+ else:
271
+ raise TypeError("number_format must be str or dict")