bootstrap-python 0.1.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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 bootstrap-python authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,226 @@
1
+ Metadata-Version: 2.4
2
+ Name: bootstrap-python
3
+ Version: 0.1.0
4
+ Summary: 用 Pydantic 模型描述 Bootstrap 5 界面,并渲染成 HTML
5
+ Author: bootstrap-python contributors
6
+ License-Expression: MIT
7
+ Keywords: bootstrap,bootstrap5,pydantic,html,ui
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Framework :: Pydantic
10
+ Classifier: Topic :: Text Processing :: Markup :: HTML
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: pydantic>=2.5
15
+ Requires-Dist: jinja2>=3.1
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest>=7; extra == "dev"
18
+ Dynamic: license-file
19
+
20
+ # bootstrap-python
21
+
22
+ 用 **Pydantic 模型**描述 [Bootstrap 5](https://getbootstrap.com/) 界面,并直接渲染成 HTML。
23
+
24
+ 每个组件都是类型安全的 pydantic 模型:字段写错会立刻抛 `ValidationError`,文本默认转义,
25
+ 组合与复用靠 Python 的列表和嵌套完成。交互(弹窗、抽屉、标签页、折叠面板、提示气泡)全部由
26
+ Bootstrap 的 `data-bs-*` 属性驱动,**不需要 Vue/React,也不需要前端构建**,
27
+ 适合服务端渲染、后台页面、报表和邮件页面。
28
+
29
+ ```python
30
+ from bootstrap_ui import Page, Card, Table, Badge
31
+
32
+ page = Page(title="用户", body=[
33
+ Card(title="用户列表", body=[
34
+ Table(columns=["ID", "姓名"], rows=[[1, "张三"], [2, "李四"]]),
35
+ ]),
36
+ ])
37
+ print(page.render()) # 完整 HTML 文档
38
+ ```
39
+
40
+ 打开 [demo.html](demo.html) 可以看到一个把所有组件组合起来的后台概览页
41
+ (导航栏、统计卡片、标签页表格、列表组、折叠面板、模态框、抽屉、分页、Toast)。
42
+
43
+ ![demo](screenshot-top.png)
44
+
45
+ 弹窗与抽屉同样由组件描述,点击按钮即可打开:
46
+
47
+ ![modal](screenshot-modal.png)
48
+
49
+ ---
50
+
51
+ ## 为什么用组件而不是拼 HTML
52
+
53
+ | | 手写 HTML / 模板字符串 | 本库的组件模型 |
54
+ |---|---|---|
55
+ | 转义 | 容易漏,XSS 靠自觉 | 文本默认转义,可信片段需显式 `raw()` |
56
+ | 结构 | 拼接字符串,缩进和闭合靠人肉 | 列表嵌套,标签自动闭合 |
57
+ | 字段校验 | 写错属性名浏览器静默忽略 | pydantic `extra="forbid"`,构造时直接报错 |
58
+ | 复用 | 复制粘贴片段 | 函数/类/列表推导,组合自由 |
59
+ | 改样式 | 全局搜替换 | 改一处 `class_name` 或组件默认值 |
60
+
61
+ 同时要明确它**不是**什么:这不是前端框架,没有响应式状态、没有数据绑定、不做前端请求。
62
+ 分页、筛选、行内编辑这类交互需要后端配合刷新页面,或自己补少量 JS;
63
+ 如果页面需要复杂的前端交互,请直接用 React/Vue 或配套的前端方案,不要硬套本库。
64
+
65
+ ---
66
+
67
+ ## 安装
68
+
69
+ ```bash
70
+ pip install bootstrap-python # 从 PyPI 安装
71
+ pip install -e . # 本地开发
72
+ ```
73
+
74
+ 要求 Python ≥ 3.9、pydantic ≥ 2.5、jinja2 ≥ 3.1。
75
+
76
+ > 发行名是 `bootstrap-python`,导入名是 `bootstrap_ui`:`pip install bootstrap-python` 之后用
77
+ > `from bootstrap_ui import Page`。
78
+
79
+ ## 30 秒上手
80
+
81
+ ```python
82
+ from bootstrap_ui import Page, Card, Row, Col, Table, Badge, Color
83
+
84
+ page = Page(
85
+ title="运营看板",
86
+ navbar=None,
87
+ body=[
88
+ Row(gutter=3, children=[
89
+ Col(md=6, body=[
90
+ Card(title="今日订单", body=["1,284"]),
91
+ ]),
92
+ Col(md=6, body=[
93
+ Card(title="状态", body=[Badge(text="正常", color=Color.success, pill=True)]),
94
+ ]),
95
+ ]),
96
+ Table(
97
+ columns=["订单号", "客户", "金额"],
98
+ rows=[["#A-1042", "张伟", "¥ 1,280.00"]],
99
+ striped=True,
100
+ ),
101
+ ],
102
+ )
103
+
104
+ page.save("dashboard.html")
105
+ ```
106
+
107
+ 生成结果:
108
+
109
+ ```html
110
+ <!DOCTYPE html>
111
+ <html lang="zh-CN">
112
+ <head>
113
+ ...
114
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
115
+ </head>
116
+ <body>
117
+ <div class="container py-4">
118
+ <div class="row g-3">
119
+ <div class="col-md-6"><div class="card h-100">...
120
+ ```
121
+
122
+ ## 三种输出
123
+
124
+ ```python
125
+ node.render() # HTML 字符串(唯一真正用于交付的)
126
+ node.to_dict() # 结构化描述:字段名、Bootstrap 类、子节点,可 JSON 序列化
127
+ node.to_json() # 上面结果的 JSON 文本
128
+ ```
129
+
130
+ `to_dict()` 保留子类的全部字段(借 pydantic 的 `serialize_as_any`),并自动去掉空的 `attrs`/`style`:
131
+
132
+ ```python
133
+ >>> Card(title="卡片", body=[Badge(text="x")]).to_dict()
134
+ {'body': [{'text': 'x', 'color': 'primary', 'pill': False}],
135
+ 'title': '卡片', 'actions': [], 'image_position': 'top'}
136
+ ```
137
+
138
+ ## 组件清单
139
+
140
+ | 分类 | 组件 |
141
+ |---|---|
142
+ | 页面/布局 | `Page` `Container` `Row` `Col` `Stack` `Box` `Fragment` `Hr` |
143
+ | 文本/内容 | `Heading` `Paragraph` `Span` `Strong` `Small` `Link` `Image` `Icon` |
144
+ | 卡片/列表 | `Card` `ListGroup` `ListItem` `Badge` `Alert` |
145
+ | 按钮 | `Button` `ButtonGroup` `CloseButton` |
146
+ | 导航 | `Navbar` `Nav` `NavItem` `Dropdown` `DropdownItem` `Breadcrumb` `Pagination` |
147
+ | 数据展示 | `Table` `TableColumn` `Progress` `Spinner` `Tabs` `TabItem` `Accordion` `AccordionItem` `Carousel` |
148
+ | 浮层 | `Modal` `Offcanvas` `Toast` `ToastContainer` `Tooltip` |
149
+ | 表单 | `Form` `FormGroup` `field()` `Label` `Input` `Textarea` `Select` `Checkbox` `RadioGroup` `InputGroup` `Fieldset` `Hidden` |
150
+ | 常量 | `Color` `Size` `Alignment` `Justify` `Placement` `Breakpoint` |
151
+ | 内核 | `Node` `RawHtml` `raw()` `render_attrs()` `render_children()` |
152
+
153
+ ### 常用写法
154
+
155
+ ```python
156
+ # 表单:控件 + 标签 + 帮助文本
157
+ Form(action="/users", method="post", submit_label="保存", body=[
158
+ field("姓名", Input(name="name", required=True), required=True),
159
+ field("邮箱", Input(name="email", type="email"), help="用于接收通知"),
160
+ field("角色", Select(name="role", placeholder="请选择",
161
+ options=[("管理员", "admin"), ("普通用户", "user")])),
162
+ Checkbox(name="notify", label="保存后发送邮件", switch=True),
163
+ ])
164
+
165
+ # 表格:字典行 + 组件单元格
166
+ Table(
167
+ columns=[TableColumn(label="状态", key="status", align="center")],
168
+ rows=[{"status": Badge(text="正常", color=Color.success)}],
169
+ )
170
+
171
+ # 弹窗:按钮和模态框用 id 关联
172
+ Button(text="新增", toggle_modal="user-modal")
173
+ Modal(id="user-modal", title="新增用户", body=[...], footer=[Button(text="取消", outline=True)])
174
+
175
+ # 抽屉
176
+ Button(text="筛选", toggle_offcanvas="filter-panel")
177
+ Offcanvas(id="filter-panel", title="筛选", placement="end", body=[...])
178
+
179
+ # 不产生包裹标签的组合,或插入可信 HTML
180
+ Span(body=[Icon(name="star-fill"), " 收藏"])
181
+ raw("<div class='ratio ratio-16x9'>…</div>")
182
+ ```
183
+
184
+ ### 字段约定
185
+
186
+ - `class_name` → HTML 的 `class`(序列化为 `"class"`),用于追加任意 Bootstrap 工具类;
187
+ - `style` 支持字符串或字典(`{"min-width": "10rem"}`,键里的 `_` 会转成 `-`);
188
+ - `attrs` 放任意原生属性,例如 `attrs={"data-bs-toggle": "tooltip", "aria-label": "提示"}`;
189
+ - 文本默认转义;只有 `RawHtml` / `raw()` 会原样输出;
190
+ - 组件是 pydantic 模型(`extra="forbid"`),**写错字段名会直接报 `ValidationError`**,不会静默失效。
191
+
192
+ ## 项目结构
193
+
194
+ ```
195
+ bootstrap_ui/
196
+ types.py # Node 基类:属性渲染、转义、to_dict/to_json
197
+ constants.py # Color / Size / Alignment ...
198
+ layout.py # Page / Container / Row / Col / Stack / Box / Fragment / Hr
199
+ content.py # Heading / Span / Card / Alert / ListGroup / Button ...
200
+ nav.py # Navbar / Nav / Dropdown / Pagination / Breadcrumb
201
+ data.py # Table / Tabs / Accordion / Progress / Carousel / Spinner
202
+ overlays.py # Modal / Offcanvas / Toast / Tooltip
203
+ forms.py # Form / FormGroup / Input / Select / Checkbox / RadioGroup ...
204
+ templates/
205
+ page.jinja2 # HTML 外壳(CDN、主题、自动初始化 Toast 与 Tooltip)
206
+ demo.py # 演示脚本:生成 demo.html
207
+ tests/ # 24 个单元测试(含 HTML 标签闭合检查)
208
+ ```
209
+
210
+ ## 运行
211
+
212
+ ```bash
213
+ python demo.py # 生成 demo.html
214
+ python -m unittest discover -s tests -v # 运行测试(24 项)
215
+ ```
216
+
217
+ ## 已知限制
218
+
219
+ 1. **交互能力有限**:分页、筛选、行内编辑都是静态 HTML,需要后端配合或自己写少量 JS;
220
+ 2. **不校验类名**:`class_name` 里写错 Bootstrap 类不会报错(Bootstrap 本身也不校验);
221
+ 3. **组件按需扩展**:目前覆盖常用组件,`Toast` 自动弹出、`Tooltip` 初始化由模板里的脚本统一处理;
222
+ 4. **CDN 依赖**:默认从 jsdelivr 加载 Bootstrap,离线场景请把资源下载到本地并改 `Page(cdn=...)` / `Page(css=...)`。
223
+
224
+ ## License
225
+
226
+ MIT
@@ -0,0 +1,207 @@
1
+ # bootstrap-python
2
+
3
+ 用 **Pydantic 模型**描述 [Bootstrap 5](https://getbootstrap.com/) 界面,并直接渲染成 HTML。
4
+
5
+ 每个组件都是类型安全的 pydantic 模型:字段写错会立刻抛 `ValidationError`,文本默认转义,
6
+ 组合与复用靠 Python 的列表和嵌套完成。交互(弹窗、抽屉、标签页、折叠面板、提示气泡)全部由
7
+ Bootstrap 的 `data-bs-*` 属性驱动,**不需要 Vue/React,也不需要前端构建**,
8
+ 适合服务端渲染、后台页面、报表和邮件页面。
9
+
10
+ ```python
11
+ from bootstrap_ui import Page, Card, Table, Badge
12
+
13
+ page = Page(title="用户", body=[
14
+ Card(title="用户列表", body=[
15
+ Table(columns=["ID", "姓名"], rows=[[1, "张三"], [2, "李四"]]),
16
+ ]),
17
+ ])
18
+ print(page.render()) # 完整 HTML 文档
19
+ ```
20
+
21
+ 打开 [demo.html](demo.html) 可以看到一个把所有组件组合起来的后台概览页
22
+ (导航栏、统计卡片、标签页表格、列表组、折叠面板、模态框、抽屉、分页、Toast)。
23
+
24
+ ![demo](screenshot-top.png)
25
+
26
+ 弹窗与抽屉同样由组件描述,点击按钮即可打开:
27
+
28
+ ![modal](screenshot-modal.png)
29
+
30
+ ---
31
+
32
+ ## 为什么用组件而不是拼 HTML
33
+
34
+ | | 手写 HTML / 模板字符串 | 本库的组件模型 |
35
+ |---|---|---|
36
+ | 转义 | 容易漏,XSS 靠自觉 | 文本默认转义,可信片段需显式 `raw()` |
37
+ | 结构 | 拼接字符串,缩进和闭合靠人肉 | 列表嵌套,标签自动闭合 |
38
+ | 字段校验 | 写错属性名浏览器静默忽略 | pydantic `extra="forbid"`,构造时直接报错 |
39
+ | 复用 | 复制粘贴片段 | 函数/类/列表推导,组合自由 |
40
+ | 改样式 | 全局搜替换 | 改一处 `class_name` 或组件默认值 |
41
+
42
+ 同时要明确它**不是**什么:这不是前端框架,没有响应式状态、没有数据绑定、不做前端请求。
43
+ 分页、筛选、行内编辑这类交互需要后端配合刷新页面,或自己补少量 JS;
44
+ 如果页面需要复杂的前端交互,请直接用 React/Vue 或配套的前端方案,不要硬套本库。
45
+
46
+ ---
47
+
48
+ ## 安装
49
+
50
+ ```bash
51
+ pip install bootstrap-python # 从 PyPI 安装
52
+ pip install -e . # 本地开发
53
+ ```
54
+
55
+ 要求 Python ≥ 3.9、pydantic ≥ 2.5、jinja2 ≥ 3.1。
56
+
57
+ > 发行名是 `bootstrap-python`,导入名是 `bootstrap_ui`:`pip install bootstrap-python` 之后用
58
+ > `from bootstrap_ui import Page`。
59
+
60
+ ## 30 秒上手
61
+
62
+ ```python
63
+ from bootstrap_ui import Page, Card, Row, Col, Table, Badge, Color
64
+
65
+ page = Page(
66
+ title="运营看板",
67
+ navbar=None,
68
+ body=[
69
+ Row(gutter=3, children=[
70
+ Col(md=6, body=[
71
+ Card(title="今日订单", body=["1,284"]),
72
+ ]),
73
+ Col(md=6, body=[
74
+ Card(title="状态", body=[Badge(text="正常", color=Color.success, pill=True)]),
75
+ ]),
76
+ ]),
77
+ Table(
78
+ columns=["订单号", "客户", "金额"],
79
+ rows=[["#A-1042", "张伟", "¥ 1,280.00"]],
80
+ striped=True,
81
+ ),
82
+ ],
83
+ )
84
+
85
+ page.save("dashboard.html")
86
+ ```
87
+
88
+ 生成结果:
89
+
90
+ ```html
91
+ <!DOCTYPE html>
92
+ <html lang="zh-CN">
93
+ <head>
94
+ ...
95
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
96
+ </head>
97
+ <body>
98
+ <div class="container py-4">
99
+ <div class="row g-3">
100
+ <div class="col-md-6"><div class="card h-100">...
101
+ ```
102
+
103
+ ## 三种输出
104
+
105
+ ```python
106
+ node.render() # HTML 字符串(唯一真正用于交付的)
107
+ node.to_dict() # 结构化描述:字段名、Bootstrap 类、子节点,可 JSON 序列化
108
+ node.to_json() # 上面结果的 JSON 文本
109
+ ```
110
+
111
+ `to_dict()` 保留子类的全部字段(借 pydantic 的 `serialize_as_any`),并自动去掉空的 `attrs`/`style`:
112
+
113
+ ```python
114
+ >>> Card(title="卡片", body=[Badge(text="x")]).to_dict()
115
+ {'body': [{'text': 'x', 'color': 'primary', 'pill': False}],
116
+ 'title': '卡片', 'actions': [], 'image_position': 'top'}
117
+ ```
118
+
119
+ ## 组件清单
120
+
121
+ | 分类 | 组件 |
122
+ |---|---|
123
+ | 页面/布局 | `Page` `Container` `Row` `Col` `Stack` `Box` `Fragment` `Hr` |
124
+ | 文本/内容 | `Heading` `Paragraph` `Span` `Strong` `Small` `Link` `Image` `Icon` |
125
+ | 卡片/列表 | `Card` `ListGroup` `ListItem` `Badge` `Alert` |
126
+ | 按钮 | `Button` `ButtonGroup` `CloseButton` |
127
+ | 导航 | `Navbar` `Nav` `NavItem` `Dropdown` `DropdownItem` `Breadcrumb` `Pagination` |
128
+ | 数据展示 | `Table` `TableColumn` `Progress` `Spinner` `Tabs` `TabItem` `Accordion` `AccordionItem` `Carousel` |
129
+ | 浮层 | `Modal` `Offcanvas` `Toast` `ToastContainer` `Tooltip` |
130
+ | 表单 | `Form` `FormGroup` `field()` `Label` `Input` `Textarea` `Select` `Checkbox` `RadioGroup` `InputGroup` `Fieldset` `Hidden` |
131
+ | 常量 | `Color` `Size` `Alignment` `Justify` `Placement` `Breakpoint` |
132
+ | 内核 | `Node` `RawHtml` `raw()` `render_attrs()` `render_children()` |
133
+
134
+ ### 常用写法
135
+
136
+ ```python
137
+ # 表单:控件 + 标签 + 帮助文本
138
+ Form(action="/users", method="post", submit_label="保存", body=[
139
+ field("姓名", Input(name="name", required=True), required=True),
140
+ field("邮箱", Input(name="email", type="email"), help="用于接收通知"),
141
+ field("角色", Select(name="role", placeholder="请选择",
142
+ options=[("管理员", "admin"), ("普通用户", "user")])),
143
+ Checkbox(name="notify", label="保存后发送邮件", switch=True),
144
+ ])
145
+
146
+ # 表格:字典行 + 组件单元格
147
+ Table(
148
+ columns=[TableColumn(label="状态", key="status", align="center")],
149
+ rows=[{"status": Badge(text="正常", color=Color.success)}],
150
+ )
151
+
152
+ # 弹窗:按钮和模态框用 id 关联
153
+ Button(text="新增", toggle_modal="user-modal")
154
+ Modal(id="user-modal", title="新增用户", body=[...], footer=[Button(text="取消", outline=True)])
155
+
156
+ # 抽屉
157
+ Button(text="筛选", toggle_offcanvas="filter-panel")
158
+ Offcanvas(id="filter-panel", title="筛选", placement="end", body=[...])
159
+
160
+ # 不产生包裹标签的组合,或插入可信 HTML
161
+ Span(body=[Icon(name="star-fill"), " 收藏"])
162
+ raw("<div class='ratio ratio-16x9'>…</div>")
163
+ ```
164
+
165
+ ### 字段约定
166
+
167
+ - `class_name` → HTML 的 `class`(序列化为 `"class"`),用于追加任意 Bootstrap 工具类;
168
+ - `style` 支持字符串或字典(`{"min-width": "10rem"}`,键里的 `_` 会转成 `-`);
169
+ - `attrs` 放任意原生属性,例如 `attrs={"data-bs-toggle": "tooltip", "aria-label": "提示"}`;
170
+ - 文本默认转义;只有 `RawHtml` / `raw()` 会原样输出;
171
+ - 组件是 pydantic 模型(`extra="forbid"`),**写错字段名会直接报 `ValidationError`**,不会静默失效。
172
+
173
+ ## 项目结构
174
+
175
+ ```
176
+ bootstrap_ui/
177
+ types.py # Node 基类:属性渲染、转义、to_dict/to_json
178
+ constants.py # Color / Size / Alignment ...
179
+ layout.py # Page / Container / Row / Col / Stack / Box / Fragment / Hr
180
+ content.py # Heading / Span / Card / Alert / ListGroup / Button ...
181
+ nav.py # Navbar / Nav / Dropdown / Pagination / Breadcrumb
182
+ data.py # Table / Tabs / Accordion / Progress / Carousel / Spinner
183
+ overlays.py # Modal / Offcanvas / Toast / Tooltip
184
+ forms.py # Form / FormGroup / Input / Select / Checkbox / RadioGroup ...
185
+ templates/
186
+ page.jinja2 # HTML 外壳(CDN、主题、自动初始化 Toast 与 Tooltip)
187
+ demo.py # 演示脚本:生成 demo.html
188
+ tests/ # 24 个单元测试(含 HTML 标签闭合检查)
189
+ ```
190
+
191
+ ## 运行
192
+
193
+ ```bash
194
+ python demo.py # 生成 demo.html
195
+ python -m unittest discover -s tests -v # 运行测试(24 项)
196
+ ```
197
+
198
+ ## 已知限制
199
+
200
+ 1. **交互能力有限**:分页、筛选、行内编辑都是静态 HTML,需要后端配合或自己写少量 JS;
201
+ 2. **不校验类名**:`class_name` 里写错 Bootstrap 类不会报错(Bootstrap 本身也不校验);
202
+ 3. **组件按需扩展**:目前覆盖常用组件,`Toast` 自动弹出、`Tooltip` 初始化由模板里的脚本统一处理;
203
+ 4. **CDN 依赖**:默认从 jsdelivr 加载 Bootstrap,离线场景请把资源下载到本地并改 `Page(cdn=...)` / `Page(css=...)`。
204
+
205
+ ## License
206
+
207
+ MIT
@@ -0,0 +1,226 @@
1
+ Metadata-Version: 2.4
2
+ Name: bootstrap-python
3
+ Version: 0.1.0
4
+ Summary: 用 Pydantic 模型描述 Bootstrap 5 界面,并渲染成 HTML
5
+ Author: bootstrap-python contributors
6
+ License-Expression: MIT
7
+ Keywords: bootstrap,bootstrap5,pydantic,html,ui
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Framework :: Pydantic
10
+ Classifier: Topic :: Text Processing :: Markup :: HTML
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: pydantic>=2.5
15
+ Requires-Dist: jinja2>=3.1
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest>=7; extra == "dev"
18
+ Dynamic: license-file
19
+
20
+ # bootstrap-python
21
+
22
+ 用 **Pydantic 模型**描述 [Bootstrap 5](https://getbootstrap.com/) 界面,并直接渲染成 HTML。
23
+
24
+ 每个组件都是类型安全的 pydantic 模型:字段写错会立刻抛 `ValidationError`,文本默认转义,
25
+ 组合与复用靠 Python 的列表和嵌套完成。交互(弹窗、抽屉、标签页、折叠面板、提示气泡)全部由
26
+ Bootstrap 的 `data-bs-*` 属性驱动,**不需要 Vue/React,也不需要前端构建**,
27
+ 适合服务端渲染、后台页面、报表和邮件页面。
28
+
29
+ ```python
30
+ from bootstrap_ui import Page, Card, Table, Badge
31
+
32
+ page = Page(title="用户", body=[
33
+ Card(title="用户列表", body=[
34
+ Table(columns=["ID", "姓名"], rows=[[1, "张三"], [2, "李四"]]),
35
+ ]),
36
+ ])
37
+ print(page.render()) # 完整 HTML 文档
38
+ ```
39
+
40
+ 打开 [demo.html](demo.html) 可以看到一个把所有组件组合起来的后台概览页
41
+ (导航栏、统计卡片、标签页表格、列表组、折叠面板、模态框、抽屉、分页、Toast)。
42
+
43
+ ![demo](screenshot-top.png)
44
+
45
+ 弹窗与抽屉同样由组件描述,点击按钮即可打开:
46
+
47
+ ![modal](screenshot-modal.png)
48
+
49
+ ---
50
+
51
+ ## 为什么用组件而不是拼 HTML
52
+
53
+ | | 手写 HTML / 模板字符串 | 本库的组件模型 |
54
+ |---|---|---|
55
+ | 转义 | 容易漏,XSS 靠自觉 | 文本默认转义,可信片段需显式 `raw()` |
56
+ | 结构 | 拼接字符串,缩进和闭合靠人肉 | 列表嵌套,标签自动闭合 |
57
+ | 字段校验 | 写错属性名浏览器静默忽略 | pydantic `extra="forbid"`,构造时直接报错 |
58
+ | 复用 | 复制粘贴片段 | 函数/类/列表推导,组合自由 |
59
+ | 改样式 | 全局搜替换 | 改一处 `class_name` 或组件默认值 |
60
+
61
+ 同时要明确它**不是**什么:这不是前端框架,没有响应式状态、没有数据绑定、不做前端请求。
62
+ 分页、筛选、行内编辑这类交互需要后端配合刷新页面,或自己补少量 JS;
63
+ 如果页面需要复杂的前端交互,请直接用 React/Vue 或配套的前端方案,不要硬套本库。
64
+
65
+ ---
66
+
67
+ ## 安装
68
+
69
+ ```bash
70
+ pip install bootstrap-python # 从 PyPI 安装
71
+ pip install -e . # 本地开发
72
+ ```
73
+
74
+ 要求 Python ≥ 3.9、pydantic ≥ 2.5、jinja2 ≥ 3.1。
75
+
76
+ > 发行名是 `bootstrap-python`,导入名是 `bootstrap_ui`:`pip install bootstrap-python` 之后用
77
+ > `from bootstrap_ui import Page`。
78
+
79
+ ## 30 秒上手
80
+
81
+ ```python
82
+ from bootstrap_ui import Page, Card, Row, Col, Table, Badge, Color
83
+
84
+ page = Page(
85
+ title="运营看板",
86
+ navbar=None,
87
+ body=[
88
+ Row(gutter=3, children=[
89
+ Col(md=6, body=[
90
+ Card(title="今日订单", body=["1,284"]),
91
+ ]),
92
+ Col(md=6, body=[
93
+ Card(title="状态", body=[Badge(text="正常", color=Color.success, pill=True)]),
94
+ ]),
95
+ ]),
96
+ Table(
97
+ columns=["订单号", "客户", "金额"],
98
+ rows=[["#A-1042", "张伟", "¥ 1,280.00"]],
99
+ striped=True,
100
+ ),
101
+ ],
102
+ )
103
+
104
+ page.save("dashboard.html")
105
+ ```
106
+
107
+ 生成结果:
108
+
109
+ ```html
110
+ <!DOCTYPE html>
111
+ <html lang="zh-CN">
112
+ <head>
113
+ ...
114
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
115
+ </head>
116
+ <body>
117
+ <div class="container py-4">
118
+ <div class="row g-3">
119
+ <div class="col-md-6"><div class="card h-100">...
120
+ ```
121
+
122
+ ## 三种输出
123
+
124
+ ```python
125
+ node.render() # HTML 字符串(唯一真正用于交付的)
126
+ node.to_dict() # 结构化描述:字段名、Bootstrap 类、子节点,可 JSON 序列化
127
+ node.to_json() # 上面结果的 JSON 文本
128
+ ```
129
+
130
+ `to_dict()` 保留子类的全部字段(借 pydantic 的 `serialize_as_any`),并自动去掉空的 `attrs`/`style`:
131
+
132
+ ```python
133
+ >>> Card(title="卡片", body=[Badge(text="x")]).to_dict()
134
+ {'body': [{'text': 'x', 'color': 'primary', 'pill': False}],
135
+ 'title': '卡片', 'actions': [], 'image_position': 'top'}
136
+ ```
137
+
138
+ ## 组件清单
139
+
140
+ | 分类 | 组件 |
141
+ |---|---|
142
+ | 页面/布局 | `Page` `Container` `Row` `Col` `Stack` `Box` `Fragment` `Hr` |
143
+ | 文本/内容 | `Heading` `Paragraph` `Span` `Strong` `Small` `Link` `Image` `Icon` |
144
+ | 卡片/列表 | `Card` `ListGroup` `ListItem` `Badge` `Alert` |
145
+ | 按钮 | `Button` `ButtonGroup` `CloseButton` |
146
+ | 导航 | `Navbar` `Nav` `NavItem` `Dropdown` `DropdownItem` `Breadcrumb` `Pagination` |
147
+ | 数据展示 | `Table` `TableColumn` `Progress` `Spinner` `Tabs` `TabItem` `Accordion` `AccordionItem` `Carousel` |
148
+ | 浮层 | `Modal` `Offcanvas` `Toast` `ToastContainer` `Tooltip` |
149
+ | 表单 | `Form` `FormGroup` `field()` `Label` `Input` `Textarea` `Select` `Checkbox` `RadioGroup` `InputGroup` `Fieldset` `Hidden` |
150
+ | 常量 | `Color` `Size` `Alignment` `Justify` `Placement` `Breakpoint` |
151
+ | 内核 | `Node` `RawHtml` `raw()` `render_attrs()` `render_children()` |
152
+
153
+ ### 常用写法
154
+
155
+ ```python
156
+ # 表单:控件 + 标签 + 帮助文本
157
+ Form(action="/users", method="post", submit_label="保存", body=[
158
+ field("姓名", Input(name="name", required=True), required=True),
159
+ field("邮箱", Input(name="email", type="email"), help="用于接收通知"),
160
+ field("角色", Select(name="role", placeholder="请选择",
161
+ options=[("管理员", "admin"), ("普通用户", "user")])),
162
+ Checkbox(name="notify", label="保存后发送邮件", switch=True),
163
+ ])
164
+
165
+ # 表格:字典行 + 组件单元格
166
+ Table(
167
+ columns=[TableColumn(label="状态", key="status", align="center")],
168
+ rows=[{"status": Badge(text="正常", color=Color.success)}],
169
+ )
170
+
171
+ # 弹窗:按钮和模态框用 id 关联
172
+ Button(text="新增", toggle_modal="user-modal")
173
+ Modal(id="user-modal", title="新增用户", body=[...], footer=[Button(text="取消", outline=True)])
174
+
175
+ # 抽屉
176
+ Button(text="筛选", toggle_offcanvas="filter-panel")
177
+ Offcanvas(id="filter-panel", title="筛选", placement="end", body=[...])
178
+
179
+ # 不产生包裹标签的组合,或插入可信 HTML
180
+ Span(body=[Icon(name="star-fill"), " 收藏"])
181
+ raw("<div class='ratio ratio-16x9'>…</div>")
182
+ ```
183
+
184
+ ### 字段约定
185
+
186
+ - `class_name` → HTML 的 `class`(序列化为 `"class"`),用于追加任意 Bootstrap 工具类;
187
+ - `style` 支持字符串或字典(`{"min-width": "10rem"}`,键里的 `_` 会转成 `-`);
188
+ - `attrs` 放任意原生属性,例如 `attrs={"data-bs-toggle": "tooltip", "aria-label": "提示"}`;
189
+ - 文本默认转义;只有 `RawHtml` / `raw()` 会原样输出;
190
+ - 组件是 pydantic 模型(`extra="forbid"`),**写错字段名会直接报 `ValidationError`**,不会静默失效。
191
+
192
+ ## 项目结构
193
+
194
+ ```
195
+ bootstrap_ui/
196
+ types.py # Node 基类:属性渲染、转义、to_dict/to_json
197
+ constants.py # Color / Size / Alignment ...
198
+ layout.py # Page / Container / Row / Col / Stack / Box / Fragment / Hr
199
+ content.py # Heading / Span / Card / Alert / ListGroup / Button ...
200
+ nav.py # Navbar / Nav / Dropdown / Pagination / Breadcrumb
201
+ data.py # Table / Tabs / Accordion / Progress / Carousel / Spinner
202
+ overlays.py # Modal / Offcanvas / Toast / Tooltip
203
+ forms.py # Form / FormGroup / Input / Select / Checkbox / RadioGroup ...
204
+ templates/
205
+ page.jinja2 # HTML 外壳(CDN、主题、自动初始化 Toast 与 Tooltip)
206
+ demo.py # 演示脚本:生成 demo.html
207
+ tests/ # 24 个单元测试(含 HTML 标签闭合检查)
208
+ ```
209
+
210
+ ## 运行
211
+
212
+ ```bash
213
+ python demo.py # 生成 demo.html
214
+ python -m unittest discover -s tests -v # 运行测试(24 项)
215
+ ```
216
+
217
+ ## 已知限制
218
+
219
+ 1. **交互能力有限**:分页、筛选、行内编辑都是静态 HTML,需要后端配合或自己写少量 JS;
220
+ 2. **不校验类名**:`class_name` 里写错 Bootstrap 类不会报错(Bootstrap 本身也不校验);
221
+ 3. **组件按需扩展**:目前覆盖常用组件,`Toast` 自动弹出、`Tooltip` 初始化由模板里的脚本统一处理;
222
+ 4. **CDN 依赖**:默认从 jsdelivr 加载 Bootstrap,离线场景请把资源下载到本地并改 `Page(cdn=...)` / `Page(css=...)`。
223
+
224
+ ## License
225
+
226
+ MIT