pl2html 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.
- pl2html-0.1.0/PKG-INFO +9 -0
- pl2html-0.1.0/README.md +0 -0
- pl2html-0.1.0/pl2html/__init__.py +168 -0
- pl2html-0.1.0/pyproject.toml +66 -0
pl2html-0.1.0/PKG-INFO
ADDED
pl2html-0.1.0/README.md
ADDED
|
File without changes
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import polars as pl
|
|
2
|
+
|
|
3
|
+
__version__ = '0.1.0'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _escape_polars_string(col_name: str) -> pl.Expr:
|
|
7
|
+
"""
|
|
8
|
+
Escapes a column's string representations for secure HTML compatibility.
|
|
9
|
+
Because standard HTML entities use look-arounds or loops, we can fall back to
|
|
10
|
+
a native character loop or custom mapper if extensive characters are used,
|
|
11
|
+
but basic safety replaces characters sequentially.
|
|
12
|
+
"""
|
|
13
|
+
return (
|
|
14
|
+
pl.col(col_name)
|
|
15
|
+
.cast(pl.String)
|
|
16
|
+
.str.replace_all('&', '&')
|
|
17
|
+
.str.replace_all('<', '<')
|
|
18
|
+
.str.replace_all('>', '>')
|
|
19
|
+
.str.replace_all('"', '"')
|
|
20
|
+
.str.replace_all("'", ''')
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _format_integer(col_name: str) -> pl.Expr:
|
|
25
|
+
"""Adds thousands separators to integers using look-around-free logic."""
|
|
26
|
+
return (
|
|
27
|
+
pl.col(col_name)
|
|
28
|
+
.fill_null(0)
|
|
29
|
+
.cast(pl.String)
|
|
30
|
+
.str.reverse()
|
|
31
|
+
.str.replace_all(r'\d{3}', '$0,')
|
|
32
|
+
.str.reverse()
|
|
33
|
+
.str.replace(r'^,', '')
|
|
34
|
+
.str.replace(r'^-,', '-')
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _format_float(col_name: str) -> pl.Expr:
|
|
39
|
+
"""Formats floats with 3 decimals and thousands separators."""
|
|
40
|
+
rounded_str = pl.col(col_name).round(3).cast(pl.String)
|
|
41
|
+
int_part = rounded_str.str.split_exact('.', 1).struct.field('field_0')
|
|
42
|
+
dec_part = rounded_str.str.split_exact('.', 1).struct.field('field_1')
|
|
43
|
+
|
|
44
|
+
formatted_int = (
|
|
45
|
+
int_part.str.reverse()
|
|
46
|
+
.str.replace_all(r'\d{3}', '$0,')
|
|
47
|
+
.str.reverse()
|
|
48
|
+
.str.replace(r'^,', '')
|
|
49
|
+
.str.replace(r'^-,', '-')
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
pl.when(dec_part.is_not_null() & (dec_part != ''))
|
|
54
|
+
.then(formatted_int + pl.lit('.') + dec_part)
|
|
55
|
+
.otherwise(formatted_int)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _build_cell_expr(
|
|
60
|
+
col_name: str,
|
|
61
|
+
dtype: pl.DataType,
|
|
62
|
+
attrs: dict[str, dict[str, pl.Expr]],
|
|
63
|
+
) -> pl.Expr:
|
|
64
|
+
"""
|
|
65
|
+
Applies secure auto-escaping or data formatting overrides, then dynamically
|
|
66
|
+
compiles HTML attributes into a valid single opening <td> tag block.
|
|
67
|
+
"""
|
|
68
|
+
# 1. Base Data Evaluation (Formatted and HTML Escaped safely)
|
|
69
|
+
if dtype.is_integer():
|
|
70
|
+
fmt_expr = _format_integer(col_name)
|
|
71
|
+
elif dtype.is_float():
|
|
72
|
+
fmt_expr = _format_float(col_name)
|
|
73
|
+
else:
|
|
74
|
+
fmt_expr = _escape_polars_string(col_name)
|
|
75
|
+
|
|
76
|
+
fmt_expr = fmt_expr.fill_null('')
|
|
77
|
+
|
|
78
|
+
# 2. Build Attributes Expression Natively (e.g., style="...", class="...")
|
|
79
|
+
attr_expr_list = []
|
|
80
|
+
if col_name in attrs:
|
|
81
|
+
for attr_name, val_expr in attrs[col_name].items():
|
|
82
|
+
# If the expression returns null or empty for a specific row, skip writing the attribute
|
|
83
|
+
compiled_attr = (
|
|
84
|
+
pl.when(val_expr.is_not_null() & (val_expr != ''))
|
|
85
|
+
.then(
|
|
86
|
+
pl.lit(f' {attr_name}="')
|
|
87
|
+
+ val_expr.cast(pl.String)
|
|
88
|
+
+ pl.lit('"')
|
|
89
|
+
)
|
|
90
|
+
.otherwise(pl.lit(''))
|
|
91
|
+
)
|
|
92
|
+
attr_expr_list.append(compiled_attr)
|
|
93
|
+
|
|
94
|
+
# Combine attribute strings together
|
|
95
|
+
if attr_expr_list:
|
|
96
|
+
opening_td = (
|
|
97
|
+
pl.lit('<td') + pl.concat_str(attr_expr_list) + pl.lit('>')
|
|
98
|
+
)
|
|
99
|
+
else:
|
|
100
|
+
opening_td = pl.lit('<td>')
|
|
101
|
+
|
|
102
|
+
return opening_td + fmt_expr + pl.lit('</td>')
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _build_html_skeleton(
|
|
106
|
+
visible_columns: list[str], add_row_no: bool
|
|
107
|
+
) -> tuple[pl.Expr, pl.Expr]:
|
|
108
|
+
header_parts = ['<table>', ' <thead>\n <tr>']
|
|
109
|
+
if add_row_no:
|
|
110
|
+
header_parts.append(' <th>#</th>')
|
|
111
|
+
for c in visible_columns:
|
|
112
|
+
header_parts.append(f' <th>{c}</th>')
|
|
113
|
+
header_parts.append(' </tr>\n </thead>\n <tbody>')
|
|
114
|
+
|
|
115
|
+
html_header = pl.lit('\n'.join(header_parts) + '\n')
|
|
116
|
+
html_footer = pl.lit('\n </tbody>\n</table>')
|
|
117
|
+
return html_header, html_footer
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def to_html(
|
|
121
|
+
df: pl.DataFrame | pl.LazyFrame,
|
|
122
|
+
*,
|
|
123
|
+
attrs: dict[str, dict[str, pl.Expr]] | None = None,
|
|
124
|
+
exclude_columns: list[str] | None = None,
|
|
125
|
+
add_row_no: bool = True,
|
|
126
|
+
) -> pl.LazyFrame:
|
|
127
|
+
"""
|
|
128
|
+
Compiles a Polars DataFrame safely into an HTML string layout.
|
|
129
|
+
Accepts structural custom attributes mappings to handle layout modifications natively.
|
|
130
|
+
"""
|
|
131
|
+
lf = df.lazy() if isinstance(df, pl.DataFrame) else df
|
|
132
|
+
|
|
133
|
+
exclude_columns = exclude_columns or []
|
|
134
|
+
attrs = attrs or {}
|
|
135
|
+
|
|
136
|
+
schema = lf.collect_schema()
|
|
137
|
+
visible_columns = [c for c in schema.names() if c not in exclude_columns]
|
|
138
|
+
|
|
139
|
+
cell_expressions = []
|
|
140
|
+
|
|
141
|
+
# 1. Row Index Element Processing
|
|
142
|
+
if add_row_no:
|
|
143
|
+
index_expr = (
|
|
144
|
+
pl.lit('<td>')
|
|
145
|
+
+ (pl.arange(1, pl.len() + 1).cast(pl.String))
|
|
146
|
+
+ pl.lit('</td>')
|
|
147
|
+
)
|
|
148
|
+
cell_expressions.append(index_expr)
|
|
149
|
+
|
|
150
|
+
# 2. Map structural cell loops
|
|
151
|
+
for c in visible_columns:
|
|
152
|
+
cell_expressions.append(_build_cell_expr(c, schema[c], attrs))
|
|
153
|
+
|
|
154
|
+
# 3. Concatenate columns horizontally into rows
|
|
155
|
+
row_expr = (
|
|
156
|
+
pl.lit(' <tr>') + pl.concat_str(cell_expressions) + pl.lit('</tr>')
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# 4. Generate wrappers and frame the query graph
|
|
160
|
+
html_header, html_footer = _build_html_skeleton(
|
|
161
|
+
visible_columns, add_row_no
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
return lf.select(row_expr.alias('html_row')).select(
|
|
165
|
+
(html_header + pl.col('html_row').str.join('\n') + html_footer).alias(
|
|
166
|
+
'html_table'
|
|
167
|
+
)
|
|
168
|
+
)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ['uv_build>=0.8.3,<0.9.0']
|
|
3
|
+
build-backend = 'uv_build'
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pl2html"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Add your description here"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.14"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"polars>=1.42.1",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.urls]
|
|
16
|
+
GitHub = "https://github.com/5j9/pl2html"
|
|
17
|
+
|
|
18
|
+
[tool.uv.build-backend]
|
|
19
|
+
module-root = ''
|
|
20
|
+
module-name = "pl2html"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
[tool.ruff]
|
|
24
|
+
line-length = 79
|
|
25
|
+
format.quote-style = 'single'
|
|
26
|
+
lint.isort.combine-as-imports = true
|
|
27
|
+
lint.extend-select = [
|
|
28
|
+
'W605', # invalid-escape-sequence
|
|
29
|
+
'FA', # flake8-future-annotations
|
|
30
|
+
'I', # isort
|
|
31
|
+
'UP', # pyupgrade
|
|
32
|
+
'RUF', # Ruff-specific rules (RUF)
|
|
33
|
+
]
|
|
34
|
+
lint.ignore = [
|
|
35
|
+
'E721', # Do not compare types, use `isinstance()`
|
|
36
|
+
'RUF001', # ambiguous-unicode-character-string
|
|
37
|
+
'RUF002', # ambiguous-unicode-character-docstring
|
|
38
|
+
'RUF003', # ambiguous-unicode-character-comment
|
|
39
|
+
'RUF012', # mutable-class-default
|
|
40
|
+
'RUF059', # Unpacked variable never used
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[tool.pytest]
|
|
44
|
+
addopts = ["--quiet", "--tb=short"]
|
|
45
|
+
truncation_limit_lines = '0'
|
|
46
|
+
truncation_limit_chars = '0'
|
|
47
|
+
|
|
48
|
+
[tool.pyright]
|
|
49
|
+
typeCheckingMode = 'standard'
|
|
50
|
+
reportInvalidStringEscapeSequence = false
|
|
51
|
+
reportConstantRedefinition = 'error'
|
|
52
|
+
reportDeprecated = 'warning'
|
|
53
|
+
reportPropertyTypeMismatch = 'error'
|
|
54
|
+
reportTypeCommentUsage = 'warning'
|
|
55
|
+
reportUnnecessaryCast = 'warning'
|
|
56
|
+
reportUnnecessaryComparison = 'warning'
|
|
57
|
+
reportUnnecessaryContains = 'warning'
|
|
58
|
+
reportUnnecessaryIsInstance = 'warning'
|
|
59
|
+
reportUnnecessaryTypeIgnoreComment = 'warning'
|
|
60
|
+
venvPath = "."
|
|
61
|
+
venv = ".venv"
|
|
62
|
+
|
|
63
|
+
[dependency-groups]
|
|
64
|
+
dev = [
|
|
65
|
+
"pytest>=9.1.1",
|
|
66
|
+
]
|