dfpretty 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.
dfpretty-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 YOUR_NAME
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,148 @@
1
+ Metadata-Version: 2.4
2
+ Name: dfpretty
3
+ Version: 0.1.0
4
+ Summary: Pretty-print pandas DataFrames as styled interactive HTML tables in your browser
5
+ License: MIT License
6
+
7
+ Copyright (c) 2025 YOUR_NAME
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all
17
+ copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ SOFTWARE.
26
+
27
+ Project-URL: Homepage, https://github.com/YOUR_USERNAME/dfpretty
28
+ Project-URL: Repository, https://github.com/YOUR_USERNAME/dfpretty
29
+ Project-URL: Bug Tracker, https://github.com/YOUR_USERNAME/dfpretty/issues
30
+ Keywords: pandas,dataframe,visualization,html,jupyter
31
+ Classifier: Development Status :: 3 - Alpha
32
+ Classifier: Intended Audience :: Developers
33
+ Classifier: Intended Audience :: Science/Research
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Programming Language :: Python :: 3
36
+ Classifier: Programming Language :: Python :: 3.9
37
+ Classifier: Programming Language :: Python :: 3.10
38
+ Classifier: Programming Language :: Python :: 3.11
39
+ Classifier: Programming Language :: Python :: 3.12
40
+ Classifier: Topic :: Scientific/Engineering :: Visualization
41
+ Requires-Python: >=3.9
42
+ Description-Content-Type: text/markdown
43
+ License-File: LICENSE
44
+ Requires-Dist: pandas>=1.5
45
+ Provides-Extra: dev
46
+ Requires-Dist: pytest>=7; extra == "dev"
47
+ Requires-Dist: pytest-cov; extra == "dev"
48
+ Requires-Dist: ruff; extra == "dev"
49
+ Requires-Dist: build; extra == "dev"
50
+ Requires-Dist: twine; extra == "dev"
51
+ Dynamic: license-file
52
+
53
+ # dfpretty
54
+
55
+ > Pretty-print pandas DataFrames as styled interactive HTML tables — with theme switcher and Excel-like column filters.
56
+
57
+ Opens a standalone browser window (no Jupyter required).
58
+
59
+ ```python
60
+ from dfpretty import pretty
61
+ pretty(df, theme="tableau", title="Sales Q1")
62
+ ```
63
+
64
+ ![themes preview](https://raw.githubusercontent.com/YOUR_USERNAME/dfpretty/main/docs/preview.png)
65
+
66
+ ---
67
+
68
+ ## Installation
69
+
70
+ ```bash
71
+ # pip
72
+ pip install dfpretty
73
+
74
+ # conda (once on conda-forge)
75
+ conda install -c conda-forge dfpretty
76
+ ```
77
+
78
+ ---
79
+
80
+ ## Usage
81
+
82
+ ```python
83
+ import pandas as pd
84
+ from dfpretty import pretty
85
+
86
+ df = pd.read_csv("data.csv")
87
+
88
+ pretty(df) # dark theme, opens browser
89
+ pretty(df, theme="tableau", title="My Table") # Tableau style
90
+ pretty(df, theme="terminal") # green-on-black
91
+ pretty(df, locale="de-DE") # German number formatting
92
+ pretty(df, save="report.html", open_browser=False) # save without opening
93
+ ```
94
+
95
+ ### Parameters
96
+
97
+ | Parameter | Type | Default | Description |
98
+ |---|---|---|---|
99
+ | `df` | `pd.DataFrame` | — | DataFrame to display |
100
+ | `title` | `str` | `"DataFrame"` | Title in the top bar |
101
+ | `theme` | `str` | `"dark"` | Initial colour theme |
102
+ | `locale` | `str` | `"en-US"` | BCP-47 locale for number formatting |
103
+ | `save` | `str \| Path \| None` | `None` | Save HTML to this path |
104
+ | `open_browser` | `bool` | `True` | Open browser automatically |
105
+
106
+ **Returns:** `Path` — path to the generated HTML file.
107
+
108
+ ---
109
+
110
+ ## Themes
111
+
112
+ Themes can be switched live in the browser via the buttons in the top bar.
113
+
114
+ | Name | Style |
115
+ |---|---|
116
+ | `dark` | Deep blue-slate, blue accents |
117
+ | `tableau` | Cream background, charcoal header, orange accent — Tableau-inspired |
118
+ | `light` | Clean white, indigo accents |
119
+ | `terminal` | Black, green-on-black Matrix style |
120
+ | `notion` | Soft white, editorial typography |
121
+
122
+ ---
123
+
124
+ ## Features
125
+
126
+ - **Column filters** — click ▾ on any column header to filter by value (Excel-style)
127
+ - **Global search** — filter across all columns at once
128
+ - **Sort** — click any column name to sort ↑ ↓
129
+ - **Number formatting** — integers and floats formatted with locale-aware separators
130
+ - **Theme switcher** — switch themes live without reopening
131
+ - **Save to file** — export a standalone HTML report
132
+
133
+ ---
134
+
135
+ ## Development
136
+
137
+ ```bash
138
+ git clone https://github.com/YOUR_USERNAME/dfpretty
139
+ cd dfpretty
140
+ pip install -e ".[dev]"
141
+ pytest
142
+ ```
143
+
144
+ ---
145
+
146
+ ## License
147
+
148
+ MIT
@@ -0,0 +1,96 @@
1
+ # dfpretty
2
+
3
+ > Pretty-print pandas DataFrames as styled interactive HTML tables — with theme switcher and Excel-like column filters.
4
+
5
+ Opens a standalone browser window (no Jupyter required).
6
+
7
+ ```python
8
+ from dfpretty import pretty
9
+ pretty(df, theme="tableau", title="Sales Q1")
10
+ ```
11
+
12
+ ![themes preview](https://raw.githubusercontent.com/YOUR_USERNAME/dfpretty/main/docs/preview.png)
13
+
14
+ ---
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ # pip
20
+ pip install dfpretty
21
+
22
+ # conda (once on conda-forge)
23
+ conda install -c conda-forge dfpretty
24
+ ```
25
+
26
+ ---
27
+
28
+ ## Usage
29
+
30
+ ```python
31
+ import pandas as pd
32
+ from dfpretty import pretty
33
+
34
+ df = pd.read_csv("data.csv")
35
+
36
+ pretty(df) # dark theme, opens browser
37
+ pretty(df, theme="tableau", title="My Table") # Tableau style
38
+ pretty(df, theme="terminal") # green-on-black
39
+ pretty(df, locale="de-DE") # German number formatting
40
+ pretty(df, save="report.html", open_browser=False) # save without opening
41
+ ```
42
+
43
+ ### Parameters
44
+
45
+ | Parameter | Type | Default | Description |
46
+ |---|---|---|---|
47
+ | `df` | `pd.DataFrame` | — | DataFrame to display |
48
+ | `title` | `str` | `"DataFrame"` | Title in the top bar |
49
+ | `theme` | `str` | `"dark"` | Initial colour theme |
50
+ | `locale` | `str` | `"en-US"` | BCP-47 locale for number formatting |
51
+ | `save` | `str \| Path \| None` | `None` | Save HTML to this path |
52
+ | `open_browser` | `bool` | `True` | Open browser automatically |
53
+
54
+ **Returns:** `Path` — path to the generated HTML file.
55
+
56
+ ---
57
+
58
+ ## Themes
59
+
60
+ Themes can be switched live in the browser via the buttons in the top bar.
61
+
62
+ | Name | Style |
63
+ |---|---|
64
+ | `dark` | Deep blue-slate, blue accents |
65
+ | `tableau` | Cream background, charcoal header, orange accent — Tableau-inspired |
66
+ | `light` | Clean white, indigo accents |
67
+ | `terminal` | Black, green-on-black Matrix style |
68
+ | `notion` | Soft white, editorial typography |
69
+
70
+ ---
71
+
72
+ ## Features
73
+
74
+ - **Column filters** — click ▾ on any column header to filter by value (Excel-style)
75
+ - **Global search** — filter across all columns at once
76
+ - **Sort** — click any column name to sort ↑ ↓
77
+ - **Number formatting** — integers and floats formatted with locale-aware separators
78
+ - **Theme switcher** — switch themes live without reopening
79
+ - **Save to file** — export a standalone HTML report
80
+
81
+ ---
82
+
83
+ ## Development
84
+
85
+ ```bash
86
+ git clone https://github.com/YOUR_USERNAME/dfpretty
87
+ cd dfpretty
88
+ pip install -e ".[dev]"
89
+ pytest
90
+ ```
91
+
92
+ ---
93
+
94
+ ## License
95
+
96
+ MIT
@@ -0,0 +1,25 @@
1
+ """
2
+ dfpretty
3
+ ~~~~~~~~
4
+ Pretty-print pandas DataFrames as styled interactive HTML tables
5
+ that open in your browser — with theme switcher and Excel-like filters.
6
+
7
+ Basic usage::
8
+
9
+ import pandas as pd
10
+ from dfpretty import pretty
11
+
12
+ df = pd.DataFrame(...)
13
+ pretty(df) # dark theme (default)
14
+ pretty(df, theme="tableau")
15
+ pretty(df, theme="terminal", title="My Results")
16
+ pretty(df, save="report.html", open_browser=False)
17
+
18
+ Available themes: dark · tableau · light · terminal · notion
19
+ """
20
+
21
+ from .core import pretty
22
+ from .themes import AVAILABLE_THEMES
23
+
24
+ __all__ = ["pretty", "AVAILABLE_THEMES"]
25
+ __version__ = "0.1.0"
@@ -0,0 +1,523 @@
1
+ """
2
+ dfpretty._html
3
+ ~~~~~~~~~~~~~~
4
+ Builds the self-contained HTML string rendered in the browser.
5
+ """
6
+
7
+ from __future__ import annotations
8
+ import json
9
+ from .themes import AVAILABLE_THEMES, build_css_vars
10
+
11
+
12
+ _GOOGLE_FONTS = (
13
+ "https://fonts.googleapis.com/css2?"
14
+ "family=IBM+Plex+Mono:wght@400;600"
15
+ "&family=IBM+Plex+Sans:wght@400;500;600"
16
+ "&family=Geist+Mono:wght@400;600"
17
+ "&family=Merriweather+Sans:wght@400;600"
18
+ "&display=swap"
19
+ )
20
+
21
+ _BASE_CSS = """
22
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
23
+
24
+ body {
25
+ background: var(--bg);
26
+ font-family: var(--font-body);
27
+ color: var(--text);
28
+ min-height: 100vh;
29
+ display: flex;
30
+ flex-direction: column;
31
+ transition: background 0.25s, color 0.25s;
32
+ }
33
+
34
+ /* Topbar */
35
+ .topbar {
36
+ background: var(--surface);
37
+ border-bottom: 1px solid var(--border);
38
+ padding: 12px 20px;
39
+ display: flex;
40
+ align-items: center;
41
+ justify-content: space-between;
42
+ position: sticky;
43
+ top: 0;
44
+ z-index: 100;
45
+ gap: 16px;
46
+ flex-wrap: wrap;
47
+ }
48
+ .topbar-title {
49
+ font-family: var(--font-mono);
50
+ font-size: 13px;
51
+ font-weight: 600;
52
+ color: var(--text-bright);
53
+ letter-spacing: 0.04em;
54
+ white-space: nowrap;
55
+ }
56
+ .topbar-meta {
57
+ font-family: var(--font-mono);
58
+ font-size: 11px;
59
+ color: var(--text-dim);
60
+ white-space: nowrap;
61
+ }
62
+ .topbar-right {
63
+ display: flex;
64
+ align-items: center;
65
+ gap: 6px;
66
+ flex-wrap: wrap;
67
+ }
68
+
69
+ /* Theme switcher */
70
+ .theme-label {
71
+ font-family: var(--font-mono);
72
+ font-size: 10px;
73
+ color: var(--text-dim);
74
+ text-transform: uppercase;
75
+ letter-spacing: 0.08em;
76
+ margin-right: 2px;
77
+ }
78
+ .theme-btn {
79
+ padding: 4px 10px;
80
+ border-radius: 6px;
81
+ font-size: 11px;
82
+ font-family: var(--font-mono);
83
+ cursor: pointer;
84
+ border: 1px solid var(--border);
85
+ background: var(--bg2);
86
+ color: var(--text-muted);
87
+ transition: all 0.15s;
88
+ white-space: nowrap;
89
+ }
90
+ .theme-btn:hover { background: var(--surface2); color: var(--text-bright); }
91
+ .theme-btn.active {
92
+ background: var(--accent);
93
+ color: #fff;
94
+ border-color: var(--accent);
95
+ }
96
+
97
+ /* Searchbar */
98
+ .searchbar {
99
+ padding: 10px 20px;
100
+ background: var(--bg);
101
+ border-bottom: 1px solid var(--border2);
102
+ display: flex;
103
+ align-items: center;
104
+ gap: 8px;
105
+ }
106
+ .searchbar input {
107
+ background: var(--surface);
108
+ border: 1px solid var(--border);
109
+ border-radius: 8px;
110
+ color: var(--text-bright);
111
+ font-family: var(--font-body);
112
+ font-size: 13px;
113
+ padding: 7px 12px;
114
+ outline: none;
115
+ width: 300px;
116
+ transition: border-color 0.2s;
117
+ }
118
+ .searchbar input:focus { border-color: var(--accent); }
119
+ .searchbar input::placeholder { color: var(--text-dim); }
120
+ .clear-btn {
121
+ background: var(--surface);
122
+ border: 1px solid var(--border);
123
+ border-radius: 8px;
124
+ color: var(--text-muted);
125
+ font-size: 12px;
126
+ padding: 7px 12px;
127
+ cursor: pointer;
128
+ font-family: var(--font-body);
129
+ transition: all 0.2s;
130
+ }
131
+ .clear-btn:hover { background: var(--surface2); color: var(--text-bright); }
132
+
133
+ /* Table */
134
+ .table-wrap {
135
+ flex: 1;
136
+ overflow: auto;
137
+ padding: 16px 20px 60px;
138
+ }
139
+ table {
140
+ border-collapse: collapse;
141
+ width: 100%;
142
+ font-size: 13px;
143
+ min-width: 400px;
144
+ }
145
+ thead th {
146
+ background: var(--header-bg);
147
+ color: var(--header-text);
148
+ font-family: var(--font-mono);
149
+ font-size: 11px;
150
+ font-weight: 600;
151
+ text-transform: uppercase;
152
+ letter-spacing: 0.07em;
153
+ padding: 0;
154
+ border-bottom: 2px solid var(--th-border);
155
+ border-right: 1px solid var(--border);
156
+ position: sticky;
157
+ top: 0;
158
+ z-index: 10;
159
+ white-space: nowrap;
160
+ transition: background 0.25s;
161
+ }
162
+ thead th:last-child { border-right: none; }
163
+
164
+ .th-inner { display: flex; align-items: stretch; }
165
+ .th-label {
166
+ flex: 1;
167
+ padding: 11px 13px;
168
+ cursor: pointer;
169
+ user-select: none;
170
+ display: flex;
171
+ align-items: center;
172
+ gap: 5px;
173
+ transition: color 0.15s;
174
+ }
175
+ .th-label:hover { color: var(--text-bright); }
176
+ .sort-icon { font-size: 10px; opacity: 0.3; transition: opacity 0.2s; }
177
+ .th-label:hover .sort-icon { opacity: 0.7; }
178
+ th.sorted .sort-icon { opacity: 1; color: var(--accent); }
179
+
180
+ /* Filter */
181
+ .filter-btn {
182
+ padding: 0 9px;
183
+ cursor: pointer;
184
+ color: var(--text-dim);
185
+ background: transparent;
186
+ border: none;
187
+ border-left: 1px solid var(--border);
188
+ font-size: 12px;
189
+ transition: all 0.15s;
190
+ display: flex;
191
+ align-items: center;
192
+ }
193
+ .filter-btn:hover { color: var(--text-bright); background: var(--surface2); }
194
+ .filter-btn.active { color: var(--accent); }
195
+
196
+ .filter-dropdown {
197
+ display: none;
198
+ position: absolute;
199
+ top: 100%;
200
+ left: 0;
201
+ z-index: 200;
202
+ background: var(--surface);
203
+ border: 1px solid var(--border);
204
+ border-radius: 10px;
205
+ padding: 10px;
206
+ min-width: 210px;
207
+ box-shadow: 0 12px 40px rgba(0,0,0,0.18);
208
+ }
209
+ .filter-dropdown.open { display: block; }
210
+
211
+ .filter-search {
212
+ width: 100%;
213
+ background: var(--bg);
214
+ border: 1px solid var(--border);
215
+ border-radius: 6px;
216
+ color: var(--text-bright);
217
+ font-family: var(--font-body);
218
+ font-size: 12px;
219
+ padding: 6px 10px;
220
+ outline: none;
221
+ margin-bottom: 8px;
222
+ }
223
+ .filter-search:focus { border-color: var(--accent); }
224
+
225
+ .filter-options {
226
+ max-height: 210px;
227
+ overflow-y: auto;
228
+ scrollbar-width: thin;
229
+ scrollbar-color: var(--surface2) transparent;
230
+ }
231
+ .filter-option {
232
+ display: flex;
233
+ align-items: center;
234
+ gap: 8px;
235
+ padding: 5px 6px;
236
+ border-radius: 5px;
237
+ cursor: pointer;
238
+ font-size: 12px;
239
+ color: var(--text-muted);
240
+ transition: background 0.1s;
241
+ }
242
+ .filter-option:hover { background: var(--surface2); color: var(--text-bright); }
243
+ .filter-option input[type=checkbox] {
244
+ accent-color: var(--accent);
245
+ width: 13px; height: 13px;
246
+ cursor: pointer;
247
+ }
248
+
249
+ .filter-actions {
250
+ display: flex;
251
+ gap: 6px;
252
+ margin-top: 8px;
253
+ padding-top: 8px;
254
+ border-top: 1px solid var(--border);
255
+ }
256
+ .filter-actions button {
257
+ flex: 1;
258
+ padding: 6px;
259
+ border-radius: 6px;
260
+ font-size: 11px;
261
+ cursor: pointer;
262
+ font-family: var(--font-body);
263
+ font-weight: 600;
264
+ transition: all 0.15s;
265
+ }
266
+ .btn-apply { background: var(--accent); color: #fff; border: 1px solid var(--accent); }
267
+ .btn-apply:hover { background: var(--accent2); }
268
+ .btn-clear { background: transparent; color: var(--text-muted); border: 1px solid var(--border); }
269
+ .btn-clear:hover { background: var(--surface2); color: var(--text-bright); }
270
+
271
+ /* Rows */
272
+ tbody tr { transition: background 0.08s; }
273
+ tbody tr:nth-child(odd) td { background: var(--row-odd); }
274
+ tbody tr:nth-child(even) td { background: var(--row-even); }
275
+ tbody tr:hover td { background: var(--row-hover) !important; color: var(--text-bright); }
276
+
277
+ td {
278
+ padding: 9px 13px;
279
+ border-bottom: 1px solid var(--border2);
280
+ border-right: 1px solid var(--border2);
281
+ white-space: nowrap;
282
+ transition: background 0.08s, color 0.08s;
283
+ }
284
+ td:last-child { border-right: none; }
285
+ td.num {
286
+ font-family: var(--font-mono);
287
+ text-align: right;
288
+ color: var(--num-color);
289
+ }
290
+
291
+ /* Footer */
292
+ .footer {
293
+ position: fixed;
294
+ bottom: 0; left: 0; right: 0;
295
+ background: var(--surface);
296
+ border-top: 1px solid var(--border);
297
+ padding: 7px 20px;
298
+ font-family: var(--font-mono);
299
+ font-size: 11px;
300
+ color: var(--text-dim);
301
+ display: flex;
302
+ justify-content: space-between;
303
+ z-index: 100;
304
+ transition: background 0.25s;
305
+ }
306
+ """
307
+
308
+
309
+ def build_html(
310
+ data_json: str,
311
+ cols_json: str,
312
+ title: str,
313
+ theme: str,
314
+ locale: str,
315
+ ) -> str:
316
+ """Return the full self-contained HTML string."""
317
+
318
+ theme_buttons = "\n ".join(
319
+ f'<button class="theme-btn" onclick="setTheme(\'{t}\')">'
320
+ f'{t.capitalize()}</button>'
321
+ for t in AVAILABLE_THEMES
322
+ )
323
+
324
+ css_vars = build_css_vars(theme)
325
+
326
+ return f"""<!DOCTYPE html>
327
+ <html lang="es" data-theme="{theme}">
328
+ <head>
329
+ <meta charset="UTF-8">
330
+ <title>{title}</title>
331
+ <link rel="stylesheet" href="{_GOOGLE_FONTS}">
332
+ <style>
333
+ {css_vars}
334
+ {_BASE_CSS}
335
+ </style>
336
+ </head>
337
+ <body>
338
+
339
+ <div class="topbar">
340
+ <div class="topbar-title">⬡ {title}</div>
341
+ <div class="topbar-right">
342
+ <span class="theme-label">theme</span>
343
+ {theme_buttons}
344
+ </div>
345
+ <div class="topbar-meta" id="meta"></div>
346
+ </div>
347
+
348
+ <div class="searchbar">
349
+ <input type="text" id="globalSearch" placeholder="🔍 Search…" oninput="applyAll()">
350
+ <button class="clear-btn" onclick="clearAll()">✕ Clear filters</button>
351
+ </div>
352
+
353
+ <div class="table-wrap">
354
+ <table id="tbl">
355
+ <thead id="thead"></thead>
356
+ <tbody id="tbody"></tbody>
357
+ </table>
358
+ </div>
359
+
360
+ <div class="footer">
361
+ <span>dfpretty</span>
362
+ <span id="footerRight"></span>
363
+ </div>
364
+
365
+ <script>
366
+ const RAW = {data_json};
367
+ const COLS = {cols_json};
368
+ const LOCALE = "{locale}";
369
+
370
+ let sortCol = null, sortDir = 1;
371
+ const activeFilters = {{}};
372
+ let openDropdown = null;
373
+
374
+ const numCols = new Set(
375
+ COLS.filter(c => RAW.every(r => r[c] === null || r[c] === undefined || typeof r[c] === "number"))
376
+ );
377
+
378
+ /* ── Theme ── */
379
+ function setTheme(t) {{
380
+ document.documentElement.setAttribute("data-theme", t);
381
+ document.querySelectorAll(".theme-btn").forEach(b =>
382
+ b.classList.toggle("active", b.textContent.toLowerCase() === t)
383
+ );
384
+ }}
385
+ (function() {{
386
+ const cur = document.documentElement.getAttribute("data-theme");
387
+ document.querySelectorAll(".theme-btn").forEach(b =>
388
+ b.classList.toggle("active", b.textContent.toLowerCase() === cur)
389
+ );
390
+ }})();
391
+
392
+ /* ── Format ── */
393
+ function fmt(v) {{
394
+ if (v === null || v === undefined) return "—";
395
+ if (typeof v === "number")
396
+ return Number.isInteger(v)
397
+ ? v.toLocaleString(LOCALE)
398
+ : v.toLocaleString(LOCALE, {{minimumFractionDigits:2, maximumFractionDigits:2}});
399
+ return v;
400
+ }}
401
+
402
+ /* ── Header ── */
403
+ function buildHeader() {{
404
+ const tr = document.createElement("tr");
405
+ COLS.forEach(col => {{
406
+ const th = document.createElement("th");
407
+ th.dataset.col = col;
408
+ th.style.position = "relative";
409
+ th.innerHTML = `
410
+ <div class="th-inner">
411
+ <div class="th-label" onclick="sortBy('${{col}}')">
412
+ ${{col}}<span class="sort-icon" id="si-${{col}}">⇅</span>
413
+ </div>
414
+ <button class="filter-btn" id="fb-${{col}}" onclick="toggleDropdown(event,'${{col}}')">▾</button>
415
+ <div class="filter-dropdown" id="fd-${{col}}">
416
+ <input class="filter-search" placeholder="Search…" oninput="renderOptions('${{col}}',this.value)">
417
+ <div class="filter-options" id="fo-${{col}}"></div>
418
+ <div class="filter-actions">
419
+ <button class="btn-clear" onclick="clearFilter('${{col}}')">Clear</button>
420
+ <button class="btn-apply" onclick="closeDropdown()">Apply</button>
421
+ </div>
422
+ </div>
423
+ </div>`;
424
+ tr.appendChild(th);
425
+ }});
426
+ document.getElementById("thead").appendChild(tr);
427
+ }}
428
+
429
+ function uniqueVals(col) {{
430
+ return [...new Set(RAW.map(r => r[col]))].sort((a,b) => {{
431
+ if (a === null) return 1; if (b === null) return -1;
432
+ return a > b ? 1 : -1;
433
+ }});
434
+ }}
435
+
436
+ function renderOptions(col, search="") {{
437
+ const container = document.getElementById(`fo-${{col}}`);
438
+ const vals = uniqueVals(col).filter(v => String(v).toLowerCase().includes(search.toLowerCase()));
439
+ const sel = activeFilters[col] || new Set(uniqueVals(col).map(String));
440
+ container.innerHTML = vals.map(v => `
441
+ <label class="filter-option">
442
+ <input type="checkbox" value="${{v}}" ${{sel.has(String(v)) ? "checked" : ""}}
443
+ onchange="toggleVal('${{col}}',this.value,this.checked)">
444
+ ${{fmt(v)}}
445
+ </label>`).join("");
446
+ }}
447
+
448
+ function toggleVal(col, val, checked) {{
449
+ if (!activeFilters[col]) activeFilters[col] = new Set(uniqueVals(col).map(String));
450
+ checked ? activeFilters[col].add(val) : activeFilters[col].delete(val);
451
+ document.getElementById(`fb-${{col}}`).classList.toggle(
452
+ "active", activeFilters[col].size < uniqueVals(col).length
453
+ );
454
+ applyAll();
455
+ }}
456
+
457
+ function clearFilter(col) {{
458
+ delete activeFilters[col];
459
+ document.getElementById(`fb-${{col}}`).classList.remove("active");
460
+ renderOptions(col);
461
+ applyAll();
462
+ }}
463
+
464
+ function clearAll() {{
465
+ COLS.forEach(c => {{ delete activeFilters[c]; document.getElementById(`fb-${{c}}`).classList.remove("active"); }});
466
+ document.getElementById("globalSearch").value = "";
467
+ applyAll();
468
+ }}
469
+
470
+ function toggleDropdown(e, col) {{
471
+ e.stopPropagation();
472
+ const fd = document.getElementById(`fd-${{col}}`);
473
+ if (openDropdown && openDropdown !== fd) openDropdown.classList.remove("open");
474
+ const isOpen = fd.classList.toggle("open");
475
+ openDropdown = isOpen ? fd : null;
476
+ if (isOpen) renderOptions(col);
477
+ }}
478
+
479
+ function closeDropdown() {{
480
+ if (openDropdown) {{ openDropdown.classList.remove("open"); openDropdown = null; }}
481
+ }}
482
+
483
+ document.addEventListener("click", e => {{
484
+ if (!e.target.closest(".filter-dropdown") && !e.target.closest(".filter-btn")) closeDropdown();
485
+ }});
486
+
487
+ function sortBy(col) {{
488
+ if (sortCol === col) sortDir *= -1; else {{ sortCol = col; sortDir = 1; }}
489
+ COLS.forEach(c => {{
490
+ document.getElementById(`si-${{c}}`).textContent = "⇅";
491
+ document.querySelector(`[data-col="${{c}}"]`).classList.remove("sorted");
492
+ }});
493
+ document.getElementById(`si-${{col}}`).textContent = sortDir === 1 ? "↑" : "↓";
494
+ document.querySelector(`[data-col="${{col}}"]`).classList.add("sorted");
495
+ applyAll();
496
+ }}
497
+
498
+ function applyAll() {{
499
+ const q = document.getElementById("globalSearch").value.toLowerCase();
500
+ let rows = [...RAW];
501
+ COLS.forEach(col => {{
502
+ if (activeFilters[col]) rows = rows.filter(r => activeFilters[col].has(String(r[col])));
503
+ }});
504
+ if (q) rows = rows.filter(r => COLS.some(c => String(r[c]).toLowerCase().includes(q)));
505
+ if (sortCol) rows.sort((a,b) => {{
506
+ const va = a[sortCol], vb = b[sortCol];
507
+ if (va === null) return 1; if (vb === null) return -1;
508
+ return (va > vb ? 1 : va < vb ? -1 : 0) * sortDir;
509
+ }});
510
+
511
+ document.getElementById("tbody").innerHTML = rows.map(r =>
512
+ `<tr>${{COLS.map(c=>`<td class="${{numCols.has(c)?"num":""}}">${{fmt(r[c])}}</td>`).join("")}}</tr>`
513
+ ).join("");
514
+
515
+ document.getElementById("meta").textContent = `${{rows.length}} / ${{RAW.length}} rows`;
516
+ document.getElementById("footerRight").textContent = `${{rows.length}} rows · ${{COLS.length}} cols`;
517
+ }}
518
+
519
+ buildHeader();
520
+ applyAll();
521
+ </script>
522
+ </body>
523
+ </html>"""
@@ -0,0 +1,99 @@
1
+ """
2
+ dfpretty.core
3
+ ~~~~~~~~~~~~~
4
+ Main entry point: the pretty() function.
5
+ """
6
+
7
+ from __future__ import annotations
8
+ import json
9
+ import tempfile
10
+ import webbrowser
11
+ from pathlib import Path
12
+
13
+ import pandas as pd
14
+
15
+ from .themes import AVAILABLE_THEMES
16
+ from ._html import build_html
17
+
18
+
19
+ def pretty(
20
+ df: pd.DataFrame,
21
+ title: str = "DataFrame",
22
+ theme: str = "dark",
23
+ locale: str = "en-US",
24
+ save: str | Path | None = None,
25
+ open_browser: bool = True,
26
+ ) -> Path:
27
+ """
28
+ Render a DataFrame as a styled interactive table in the browser.
29
+
30
+ Parameters
31
+ ----------
32
+ df : pd.DataFrame
33
+ The DataFrame to display.
34
+ title : str
35
+ Window / page title shown in the top bar.
36
+ theme : str
37
+ Initial colour theme. One of: 'dark', 'tableau', 'light',
38
+ 'terminal', 'notion'. Can be changed live in the browser.
39
+ locale : str
40
+ BCP-47 locale string used by Intl.NumberFormat for number
41
+ formatting (e.g. 'en-US', 'es-ES', 'de-DE').
42
+ save : str | Path | None
43
+ If given, save the HTML file at this path instead of a
44
+ temporary file. The file persists after the browser closes.
45
+ open_browser : bool
46
+ If False, build the file but don't launch the browser.
47
+ Useful for testing or headless environments.
48
+
49
+ Returns
50
+ -------
51
+ Path
52
+ Path to the generated HTML file.
53
+
54
+ Examples
55
+ --------
56
+ >>> import pandas as pd
57
+ >>> from dfpretty import pretty
58
+ >>> df = pd.DataFrame({"x": [1, 2], "y": ["a", "b"]})
59
+ >>> pretty(df)
60
+ >>> pretty(df, theme="tableau", title="My Data")
61
+ >>> pretty(df, save="output.html", open_browser=False)
62
+ """
63
+ if theme not in AVAILABLE_THEMES:
64
+ raise ValueError(
65
+ f"Unknown theme '{theme}'. "
66
+ f"Choose one of: {', '.join(AVAILABLE_THEMES)}"
67
+ )
68
+
69
+ data_json = df.to_json(orient="records")
70
+ cols_json = json.dumps(list(df.columns))
71
+
72
+ html = build_html(
73
+ data_json=data_json,
74
+ cols_json=cols_json,
75
+ title=title,
76
+ theme=theme,
77
+ locale=locale,
78
+ )
79
+
80
+ if save is not None:
81
+ out_path = Path(save)
82
+ out_path.write_text(html, encoding="utf-8")
83
+ else:
84
+ tmp = tempfile.NamedTemporaryFile(
85
+ mode="w",
86
+ suffix=".html",
87
+ delete=False,
88
+ encoding="utf-8",
89
+ prefix="dfpretty_",
90
+ )
91
+ tmp.write(html)
92
+ tmp.close()
93
+ out_path = Path(tmp.name)
94
+
95
+ if open_browser:
96
+ webbrowser.open(out_path.as_uri())
97
+ print(f"✓ dfpretty [{theme}] → {out_path}")
98
+
99
+ return out_path
@@ -0,0 +1,142 @@
1
+ """
2
+ dfpretty.themes
3
+ ~~~~~~~~~~~~~~~
4
+ CSS variable definitions for each built-in theme.
5
+ Adding a custom theme: add a new key to THEMES with the required variables.
6
+ """
7
+
8
+ THEMES: dict[str, dict[str, str]] = {
9
+ "dark": {
10
+ "bg": "#0f172a",
11
+ "bg2": "#111827",
12
+ "surface": "#1e293b",
13
+ "surface2": "#334155",
14
+ "border": "#334155",
15
+ "border2": "#1e293b",
16
+ "text": "#cbd5e1",
17
+ "text_dim": "#475569",
18
+ "text_muted": "#94a3b8",
19
+ "text_bright": "#f1f5f9",
20
+ "accent": "#3b82f6",
21
+ "accent2": "#2563eb",
22
+ "num_color": "#93c5fd",
23
+ "row_odd": "#111827",
24
+ "row_even": "#0f172a",
25
+ "row_hover": "#1e293b",
26
+ "header_bg": "#1e293b",
27
+ "header_text": "#94a3b8",
28
+ "th_border": "#3b82f6",
29
+ "font_body": "'IBM Plex Sans', sans-serif",
30
+ "font_mono": "'IBM Plex Mono', monospace",
31
+ },
32
+ "tableau": {
33
+ "bg": "#f5f5f2",
34
+ "bg2": "#eeede9",
35
+ "surface": "#ffffff",
36
+ "surface2": "#e8e7e3",
37
+ "border": "#d1cfc9",
38
+ "border2": "#e8e7e3",
39
+ "text": "#3b3935",
40
+ "text_dim": "#8a8780",
41
+ "text_muted": "#6b6966",
42
+ "text_bright": "#1a1917",
43
+ "accent": "#1f6bb0",
44
+ "accent2": "#174f82",
45
+ "num_color": "#1f6bb0",
46
+ "row_odd": "#ffffff",
47
+ "row_even": "#f5f5f2",
48
+ "row_hover": "#eaf2fb",
49
+ "header_bg": "#3b3935",
50
+ "header_text": "#e8e7e3",
51
+ "th_border": "#e8702a",
52
+ "font_body": "'Merriweather Sans', sans-serif",
53
+ "font_mono": "'IBM Plex Mono', monospace",
54
+ },
55
+ "light": {
56
+ "bg": "#f8fafc",
57
+ "bg2": "#f1f5f9",
58
+ "surface": "#ffffff",
59
+ "surface2": "#e2e8f0",
60
+ "border": "#e2e8f0",
61
+ "border2": "#f1f5f9",
62
+ "text": "#334155",
63
+ "text_dim": "#94a3b8",
64
+ "text_muted": "#64748b",
65
+ "text_bright": "#0f172a",
66
+ "accent": "#6366f1",
67
+ "accent2": "#4f46e5",
68
+ "num_color": "#6366f1",
69
+ "row_odd": "#ffffff",
70
+ "row_even": "#f8fafc",
71
+ "row_hover": "#eef2ff",
72
+ "header_bg": "#f1f5f9",
73
+ "header_text": "#475569",
74
+ "th_border": "#6366f1",
75
+ "font_body": "'IBM Plex Sans', sans-serif",
76
+ "font_mono": "'IBM Plex Mono', monospace",
77
+ },
78
+ "terminal": {
79
+ "bg": "#0d0d0d",
80
+ "bg2": "#111111",
81
+ "surface": "#1a1a1a",
82
+ "surface2": "#2a2a2a",
83
+ "border": "#2a2a2a",
84
+ "border2": "#1a1a1a",
85
+ "text": "#a3e635",
86
+ "text_dim": "#4d7c0f",
87
+ "text_muted": "#65a30d",
88
+ "text_bright": "#d9f99d",
89
+ "accent": "#4ade80",
90
+ "accent2": "#22c55e",
91
+ "num_color": "#86efac",
92
+ "row_odd": "#111111",
93
+ "row_even": "#0d0d0d",
94
+ "row_hover": "#1a2e1a",
95
+ "header_bg": "#1a1a1a",
96
+ "header_text": "#4ade80",
97
+ "th_border": "#4ade80",
98
+ "font_body": "'Geist Mono', monospace",
99
+ "font_mono": "'Geist Mono', monospace",
100
+ },
101
+ "notion": {
102
+ "bg": "#ffffff",
103
+ "bg2": "#f7f6f3",
104
+ "surface": "#ffffff",
105
+ "surface2": "#f1f1ef",
106
+ "border": "#e9e9e7",
107
+ "border2": "#f1f1ef",
108
+ "text": "#37352f",
109
+ "text_dim": "#9b9a97",
110
+ "text_muted": "#6b6a68",
111
+ "text_bright": "#1a1a1a",
112
+ "accent": "#2eaadc",
113
+ "accent2": "#0e8fc4",
114
+ "num_color": "#0e8fc4",
115
+ "row_odd": "#ffffff",
116
+ "row_even": "#f7f6f3",
117
+ "row_hover": "#f1f1ef",
118
+ "header_bg": "#f7f6f3",
119
+ "header_text": "#9b9a97",
120
+ "th_border": "#e9e9e7",
121
+ "font_body": "'IBM Plex Sans', sans-serif",
122
+ "font_mono": "'IBM Plex Mono', monospace",
123
+ },
124
+ }
125
+
126
+ AVAILABLE_THEMES = list(THEMES.keys())
127
+
128
+
129
+ def build_css_vars(theme_name: str) -> str:
130
+ """Return a CSS :root block with the variables for *theme_name*."""
131
+ if theme_name not in THEMES:
132
+ raise ValueError(
133
+ f"Unknown theme '{theme_name}'. "
134
+ f"Available: {', '.join(AVAILABLE_THEMES)}"
135
+ )
136
+ t = THEMES[theme_name]
137
+ lines = [" :root {"]
138
+ for k, v in t.items():
139
+ css_key = "--" + k.replace("_", "-")
140
+ lines.append(f" {css_key}: {v};")
141
+ lines.append(" }")
142
+ return "\n".join(lines)
@@ -0,0 +1,148 @@
1
+ Metadata-Version: 2.4
2
+ Name: dfpretty
3
+ Version: 0.1.0
4
+ Summary: Pretty-print pandas DataFrames as styled interactive HTML tables in your browser
5
+ License: MIT License
6
+
7
+ Copyright (c) 2025 YOUR_NAME
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all
17
+ copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ SOFTWARE.
26
+
27
+ Project-URL: Homepage, https://github.com/YOUR_USERNAME/dfpretty
28
+ Project-URL: Repository, https://github.com/YOUR_USERNAME/dfpretty
29
+ Project-URL: Bug Tracker, https://github.com/YOUR_USERNAME/dfpretty/issues
30
+ Keywords: pandas,dataframe,visualization,html,jupyter
31
+ Classifier: Development Status :: 3 - Alpha
32
+ Classifier: Intended Audience :: Developers
33
+ Classifier: Intended Audience :: Science/Research
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Programming Language :: Python :: 3
36
+ Classifier: Programming Language :: Python :: 3.9
37
+ Classifier: Programming Language :: Python :: 3.10
38
+ Classifier: Programming Language :: Python :: 3.11
39
+ Classifier: Programming Language :: Python :: 3.12
40
+ Classifier: Topic :: Scientific/Engineering :: Visualization
41
+ Requires-Python: >=3.9
42
+ Description-Content-Type: text/markdown
43
+ License-File: LICENSE
44
+ Requires-Dist: pandas>=1.5
45
+ Provides-Extra: dev
46
+ Requires-Dist: pytest>=7; extra == "dev"
47
+ Requires-Dist: pytest-cov; extra == "dev"
48
+ Requires-Dist: ruff; extra == "dev"
49
+ Requires-Dist: build; extra == "dev"
50
+ Requires-Dist: twine; extra == "dev"
51
+ Dynamic: license-file
52
+
53
+ # dfpretty
54
+
55
+ > Pretty-print pandas DataFrames as styled interactive HTML tables — with theme switcher and Excel-like column filters.
56
+
57
+ Opens a standalone browser window (no Jupyter required).
58
+
59
+ ```python
60
+ from dfpretty import pretty
61
+ pretty(df, theme="tableau", title="Sales Q1")
62
+ ```
63
+
64
+ ![themes preview](https://raw.githubusercontent.com/YOUR_USERNAME/dfpretty/main/docs/preview.png)
65
+
66
+ ---
67
+
68
+ ## Installation
69
+
70
+ ```bash
71
+ # pip
72
+ pip install dfpretty
73
+
74
+ # conda (once on conda-forge)
75
+ conda install -c conda-forge dfpretty
76
+ ```
77
+
78
+ ---
79
+
80
+ ## Usage
81
+
82
+ ```python
83
+ import pandas as pd
84
+ from dfpretty import pretty
85
+
86
+ df = pd.read_csv("data.csv")
87
+
88
+ pretty(df) # dark theme, opens browser
89
+ pretty(df, theme="tableau", title="My Table") # Tableau style
90
+ pretty(df, theme="terminal") # green-on-black
91
+ pretty(df, locale="de-DE") # German number formatting
92
+ pretty(df, save="report.html", open_browser=False) # save without opening
93
+ ```
94
+
95
+ ### Parameters
96
+
97
+ | Parameter | Type | Default | Description |
98
+ |---|---|---|---|
99
+ | `df` | `pd.DataFrame` | — | DataFrame to display |
100
+ | `title` | `str` | `"DataFrame"` | Title in the top bar |
101
+ | `theme` | `str` | `"dark"` | Initial colour theme |
102
+ | `locale` | `str` | `"en-US"` | BCP-47 locale for number formatting |
103
+ | `save` | `str \| Path \| None` | `None` | Save HTML to this path |
104
+ | `open_browser` | `bool` | `True` | Open browser automatically |
105
+
106
+ **Returns:** `Path` — path to the generated HTML file.
107
+
108
+ ---
109
+
110
+ ## Themes
111
+
112
+ Themes can be switched live in the browser via the buttons in the top bar.
113
+
114
+ | Name | Style |
115
+ |---|---|
116
+ | `dark` | Deep blue-slate, blue accents |
117
+ | `tableau` | Cream background, charcoal header, orange accent — Tableau-inspired |
118
+ | `light` | Clean white, indigo accents |
119
+ | `terminal` | Black, green-on-black Matrix style |
120
+ | `notion` | Soft white, editorial typography |
121
+
122
+ ---
123
+
124
+ ## Features
125
+
126
+ - **Column filters** — click ▾ on any column header to filter by value (Excel-style)
127
+ - **Global search** — filter across all columns at once
128
+ - **Sort** — click any column name to sort ↑ ↓
129
+ - **Number formatting** — integers and floats formatted with locale-aware separators
130
+ - **Theme switcher** — switch themes live without reopening
131
+ - **Save to file** — export a standalone HTML report
132
+
133
+ ---
134
+
135
+ ## Development
136
+
137
+ ```bash
138
+ git clone https://github.com/YOUR_USERNAME/dfpretty
139
+ cd dfpretty
140
+ pip install -e ".[dev]"
141
+ pytest
142
+ ```
143
+
144
+ ---
145
+
146
+ ## License
147
+
148
+ MIT
@@ -0,0 +1,13 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ dfpretty/__init__.py
5
+ dfpretty/_html.py
6
+ dfpretty/core.py
7
+ dfpretty/themes.py
8
+ dfpretty.egg-info/PKG-INFO
9
+ dfpretty.egg-info/SOURCES.txt
10
+ dfpretty.egg-info/dependency_links.txt
11
+ dfpretty.egg-info/requires.txt
12
+ dfpretty.egg-info/top_level.txt
13
+ tests/test_pretty.py
@@ -0,0 +1,8 @@
1
+ pandas>=1.5
2
+
3
+ [dev]
4
+ pytest>=7
5
+ pytest-cov
6
+ ruff
7
+ build
8
+ twine
@@ -0,0 +1 @@
1
+ dfpretty
@@ -0,0 +1,49 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "dfpretty"
7
+ version = "0.1.0"
8
+ description = "Pretty-print pandas DataFrames as styled interactive HTML tables in your browser"
9
+ readme = "README.md"
10
+ license = { file = "LICENSE" }
11
+ requires-python = ">=3.9"
12
+ keywords = ["pandas", "dataframe", "visualization", "html", "jupyter"]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Intended Audience :: Developers",
16
+ "Intended Audience :: Science/Research",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.9",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Topic :: Scientific/Engineering :: Visualization",
24
+ ]
25
+ dependencies = [
26
+ "pandas>=1.5",
27
+ ]
28
+
29
+ [project.optional-dependencies]
30
+ dev = [
31
+ "pytest>=7",
32
+ "pytest-cov",
33
+ "ruff",
34
+ "build",
35
+ "twine",
36
+ ]
37
+
38
+ [project.urls]
39
+ Homepage = "https://github.com/YOUR_USERNAME/dfpretty"
40
+ Repository = "https://github.com/YOUR_USERNAME/dfpretty"
41
+ "Bug Tracker" = "https://github.com/YOUR_USERNAME/dfpretty/issues"
42
+
43
+ [tool.setuptools.packages.find]
44
+ where = ["."]
45
+ include = ["dfpretty*"]
46
+
47
+ [tool.ruff]
48
+ line-length = 88
49
+ target-version = "py39"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,52 @@
1
+ """Basic tests for dfpretty."""
2
+ import pandas as pd
3
+ import pytest
4
+ from dfpretty import pretty, AVAILABLE_THEMES
5
+
6
+
7
+ @pytest.fixture
8
+ def sample_df():
9
+ return pd.DataFrame({
10
+ "name": ["Alice", "Bob", "Carol"],
11
+ "score": [95.5, 82.0, 78.3],
12
+ "rank": [1, 2, 3],
13
+ "active": [True, False, True],
14
+ })
15
+
16
+
17
+ def test_returns_path(tmp_path, sample_df):
18
+ out = pretty(sample_df, save=tmp_path / "out.html", open_browser=False)
19
+ assert out.exists()
20
+ assert out.suffix == ".html"
21
+
22
+
23
+ def test_html_contains_title(tmp_path, sample_df):
24
+ out = pretty(sample_df, title="My Test", save=tmp_path / "out.html", open_browser=False)
25
+ content = out.read_text()
26
+ assert "My Test" in content
27
+
28
+
29
+ def test_html_contains_data(tmp_path, sample_df):
30
+ out = pretty(sample_df, save=tmp_path / "out.html", open_browser=False)
31
+ content = out.read_text()
32
+ assert "Alice" in content
33
+ assert "95.5" in content
34
+
35
+
36
+ def test_all_themes(tmp_path, sample_df):
37
+ for theme in AVAILABLE_THEMES:
38
+ out = pretty(sample_df, theme=theme, save=tmp_path / f"{theme}.html", open_browser=False)
39
+ assert out.exists()
40
+ content = out.read_text()
41
+ assert f'data-theme="{theme}"' in content
42
+
43
+
44
+ def test_invalid_theme(sample_df):
45
+ with pytest.raises(ValueError, match="Unknown theme"):
46
+ pretty(sample_df, theme="neon_pink", open_browser=False)
47
+
48
+
49
+ def test_available_themes_list():
50
+ assert "dark" in AVAILABLE_THEMES
51
+ assert "tableau" in AVAILABLE_THEMES
52
+ assert len(AVAILABLE_THEMES) == 5