df2tables 0.2.2__tar.gz → 0.2.4__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.
- {df2tables-0.2.2 → df2tables-0.2.4}/PKG-INFO +2 -2
- {df2tables-0.2.2 → df2tables-0.2.4}/df2tables/__init__.py +1 -1
- df2tables-0.2.4/df2tables/comnt.py +262 -0
- {df2tables-0.2.2 → df2tables-0.2.4}/df2tables/datatable_templ.html +209 -199
- {df2tables-0.2.2 → df2tables-0.2.4}/df2tables/df2tables.py +138 -76
- {df2tables-0.2.2 → df2tables-0.2.4}/df2tables/tablepl.py +1 -1
- {df2tables-0.2.2 → df2tables-0.2.4}/pyproject.toml +6 -11
- df2tables-0.2.2/df2tables/comnt.py +0 -224
- {df2tables-0.2.2 → df2tables-0.2.4}/LICENSE +0 -0
- {df2tables-0.2.2 → df2tables-0.2.4}/README.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: df2tables
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: df2tables: Pandas and Polars DataFrames to Interactive DataTables
|
|
5
5
|
Project-URL: Homepage, https://github.com/ts-kontakt/df2tables
|
|
6
6
|
Project-URL: Issues, https://github.com/ts-kontakt/df2tables/issues
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
#!/usr/bin/python
|
|
2
|
+
# coding=utf8
|
|
3
|
+
"""
|
|
4
|
+
Comnt - template content using block-annotated comments
|
|
5
|
+
|
|
6
|
+
A dead-simple way to manage template regions using standard comment
|
|
7
|
+
syntax -- no preprocessing needed to view the file in a browser.
|
|
8
|
+
|
|
9
|
+
You can inject real JavaScript-ready values from Python (not just
|
|
10
|
+
quoted strings), so there's no JSON.parse(...) round-trip on the
|
|
11
|
+
frontend like the usual Jinja2 approach requires.
|
|
12
|
+
|
|
13
|
+
Supported tag/comment formats:
|
|
14
|
+
HTML/XML: <!--[tag_id--> ... <!--tag_id]-->
|
|
15
|
+
JS/CSS: /*[tag_id*/ ... /*tag_id]*/
|
|
16
|
+
|
|
17
|
+
Rules:
|
|
18
|
+
- Opening/closing tag ids must match exactly.
|
|
19
|
+
- A tag id must use ONE comment style consistently (mixing html
|
|
20
|
+
and js comments for the same id is an error).
|
|
21
|
+
- A tag id MAY repeat (same style): render() broadcasts its value
|
|
22
|
+
into every occurrence; get_tag_content(..., occurrence=N) reads
|
|
23
|
+
one specific occurrence (default: the first).
|
|
24
|
+
- A value of None in repldict means "leave this tag untouched".
|
|
25
|
+
|
|
26
|
+
Deliberately small: plain string.find() scanning, no regex, no file
|
|
27
|
+
cache, no overlap/nesting detection. Built for templates with a
|
|
28
|
+
handful of tags. If you outgrow that, you want a real template
|
|
29
|
+
engine, not comnt.
|
|
30
|
+
|
|
31
|
+
Author: Tomasz Slugocki
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
import os
|
|
35
|
+
import tempfile
|
|
36
|
+
|
|
37
|
+
__all__ = ['render', 'get_tag_content', 'write_from_template',
|
|
38
|
+
'simple_example', 'example', 'NotFoundError']
|
|
39
|
+
|
|
40
|
+
__version__ = "0.0.2"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class NotFoundError(ValueError):
|
|
44
|
+
"""A tag's markers are missing, one-sided, mixed between comment
|
|
45
|
+
styles, or don't pair up cleanly. Subclasses ValueError so old
|
|
46
|
+
`except ValueError` call sites keep working unchanged."""
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _find_spans(text, start_tag, end_tag):
|
|
51
|
+
"""All non-overlapping (block_start, body_start, body_end, block_end)
|
|
52
|
+
spans for one already-known start/end tag pair, left to right."""
|
|
53
|
+
spans = []
|
|
54
|
+
pos = 0
|
|
55
|
+
while True:
|
|
56
|
+
s = text.find(start_tag, pos)
|
|
57
|
+
if s == -1:
|
|
58
|
+
break
|
|
59
|
+
body_start = s + len(start_tag)
|
|
60
|
+
e = text.find(end_tag, body_start)
|
|
61
|
+
if e == -1:
|
|
62
|
+
break
|
|
63
|
+
spans.append((s, body_start, e, e + len(end_tag)))
|
|
64
|
+
pos = e + len(end_tag)
|
|
65
|
+
return spans
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _scan(text, tag_id):
|
|
69
|
+
"""Locate every complete pair for tag_id. Returns (style, spans).
|
|
70
|
+
Raises NotFoundError if the tag is absent, mixed between comment
|
|
71
|
+
styles, or its opening/closing markers don't pair up cleanly."""
|
|
72
|
+
found = {}
|
|
73
|
+
for style, cstart, cend in (("html", "<!--", "-->"), ("js", "/*", "*/")):
|
|
74
|
+
start_tag = f"{cstart}[{tag_id}{cend}"
|
|
75
|
+
end_tag = f"{cstart}{tag_id}]{cend}"
|
|
76
|
+
start_cnt, end_cnt = text.count(start_tag), text.count(end_tag)
|
|
77
|
+
if start_cnt or end_cnt:
|
|
78
|
+
found[style] = (start_tag, end_tag, start_cnt, end_cnt)
|
|
79
|
+
|
|
80
|
+
if not found:
|
|
81
|
+
raise NotFoundError(
|
|
82
|
+
f"tag '{tag_id}' not found (checked both html and js comment forms)"
|
|
83
|
+
)
|
|
84
|
+
if len(found) > 1:
|
|
85
|
+
raise NotFoundError(
|
|
86
|
+
f"tag '{tag_id}' used in both html and js comments -- "
|
|
87
|
+
f"not allowed, use two distinct ids"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
style = next(iter(found))
|
|
91
|
+
start_tag, end_tag, start_cnt, end_cnt = found[style]
|
|
92
|
+
spans = _find_spans(text, start_tag, end_tag)
|
|
93
|
+
if start_cnt != end_cnt or len(spans) != start_cnt:
|
|
94
|
+
raise NotFoundError(
|
|
95
|
+
f"tag '{tag_id}' has unbalanced or malformed markers: "
|
|
96
|
+
f"{start_cnt}x '{start_tag}' vs {end_cnt}x '{end_tag}' "
|
|
97
|
+
f"({len(spans)} complete pair(s) found)"
|
|
98
|
+
)
|
|
99
|
+
return style, spans
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def get_tag_content(tag_id, instr, occurrence=0):
|
|
103
|
+
"""Return the current content of tag_id without modifying anything.
|
|
104
|
+
occurrence (0-based) picks which copy to read if the tag repeats."""
|
|
105
|
+
if not isinstance(tag_id, str) or not tag_id:
|
|
106
|
+
raise TypeError("'tag_id' must be a non-empty str")
|
|
107
|
+
if not isinstance(instr, str) or not instr:
|
|
108
|
+
raise TypeError("'instr' must be a non-empty str")
|
|
109
|
+
_, spans = _scan(instr, tag_id)
|
|
110
|
+
if not (0 <= occurrence < len(spans)):
|
|
111
|
+
raise IndexError(
|
|
112
|
+
f"tag '{tag_id}' has {len(spans)} occurrence(s); "
|
|
113
|
+
f"requested occurrence index {occurrence}"
|
|
114
|
+
)
|
|
115
|
+
_, body_start, body_end, _ = spans[occurrence]
|
|
116
|
+
return instr[body_start:body_end]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def render(instr, repldict, strict=False):
|
|
120
|
+
"""Replace the body of each tag named in repldict with its value.
|
|
121
|
+
|
|
122
|
+
- val is None -> tag left untouched (does not error).
|
|
123
|
+
- Missing / mixed-style / unbalanced tag ids: printed and skipped,
|
|
124
|
+
or raised immediately if strict=True.
|
|
125
|
+
- A repeated tag id gets its value broadcast into every occurrence.
|
|
126
|
+
"""
|
|
127
|
+
if not isinstance(repldict, dict):
|
|
128
|
+
raise TypeError("repldict must be a dict")
|
|
129
|
+
if not isinstance(instr, str):
|
|
130
|
+
raise TypeError(f"instr must be a str, got {type(instr).__name__}")
|
|
131
|
+
|
|
132
|
+
for tag_id, val in repldict.items():
|
|
133
|
+
if val is None:
|
|
134
|
+
continue
|
|
135
|
+
if not isinstance(val, str):
|
|
136
|
+
raise TypeError(
|
|
137
|
+
f"replacement for '{tag_id}' must be a str or None, "
|
|
138
|
+
f"got {type(val).__name__}"
|
|
139
|
+
)
|
|
140
|
+
try:
|
|
141
|
+
_, spans = _scan(instr, tag_id)
|
|
142
|
+
except NotFoundError as exc:
|
|
143
|
+
if strict:
|
|
144
|
+
raise
|
|
145
|
+
print(f"comnt: {exc}")
|
|
146
|
+
continue
|
|
147
|
+
# right-to-left so earlier offsets already computed for `instr`
|
|
148
|
+
# stay valid as we rewrite it
|
|
149
|
+
for _, body_start, body_end, _ in reversed(spans):
|
|
150
|
+
instr = instr[:body_start] + val + instr[body_end:]
|
|
151
|
+
|
|
152
|
+
return instr
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def write_from_template(template, newfile, repldict, strict=False):
|
|
156
|
+
"""Render `template` (a path) with repldict and write the result to
|
|
157
|
+
`newfile`. Written atomically (temp file + os.replace) so a crash
|
|
158
|
+
mid-write can never leave a corrupted/half-written file. newline=""
|
|
159
|
+
on both read and write preserves the file's original line endings
|
|
160
|
+
(e.g. CRLF) instead of silently normalizing them."""
|
|
161
|
+
if not isinstance(repldict, dict):
|
|
162
|
+
raise TypeError("repldict must be a dict")
|
|
163
|
+
if os.path.normcase(os.path.realpath(template)) \
|
|
164
|
+
== os.path.normcase(os.path.realpath(newfile)):
|
|
165
|
+
raise ValueError("template and newfile must be different paths")
|
|
166
|
+
|
|
167
|
+
with open(template, encoding="utf-8", newline="") as op_file:
|
|
168
|
+
instr = op_file.read()
|
|
169
|
+
replaced = render(instr, repldict, strict=strict)
|
|
170
|
+
|
|
171
|
+
tmp_dir = os.path.dirname(newfile) or "."
|
|
172
|
+
fd, tmp_path = tempfile.mkstemp(
|
|
173
|
+
prefix=os.path.basename(newfile) + ".", suffix=".tmp", dir=tmp_dir
|
|
174
|
+
)
|
|
175
|
+
try:
|
|
176
|
+
with os.fdopen(fd, "w", encoding="utf-8", newline="") as outfile:
|
|
177
|
+
outfile.write(replaced)
|
|
178
|
+
os.replace(tmp_path, newfile)
|
|
179
|
+
except BaseException:
|
|
180
|
+
try:
|
|
181
|
+
os.unlink(tmp_path)
|
|
182
|
+
except OSError:
|
|
183
|
+
pass
|
|
184
|
+
raise
|
|
185
|
+
return True
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _open_file(filename):
|
|
189
|
+
import subprocess
|
|
190
|
+
import sys
|
|
191
|
+
if sys.platform.startswith("win"):
|
|
192
|
+
os.startfile(filename)
|
|
193
|
+
else:
|
|
194
|
+
opener = "open" if sys.platform == "darwin" else "xdg-open"
|
|
195
|
+
subprocess.call([opener, filename])
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def example():
|
|
199
|
+
content = """
|
|
200
|
+
<!DOCTYPE html>
|
|
201
|
+
<html>
|
|
202
|
+
<head><!--[title1--><title> </title><!--title1]--></head>
|
|
203
|
+
<body>
|
|
204
|
+
<h3><!--[title--> Welcome to our site! <!--title]--></h3>
|
|
205
|
+
<!--[content-->
|
|
206
|
+
<p>This is placeholder content.</p>
|
|
207
|
+
<!--content]-->
|
|
208
|
+
<code id="data-display"></code>
|
|
209
|
+
<script>
|
|
210
|
+
const data = /*[data_arr*/ [0, 1] /*data_arr]*/;
|
|
211
|
+
document.addEventListener('DOMContentLoaded', function () {
|
|
212
|
+
document.getElementById('data-display').textContent =
|
|
213
|
+
JSON.stringify(data, null, 2);
|
|
214
|
+
});
|
|
215
|
+
</script>
|
|
216
|
+
</body>
|
|
217
|
+
</html>
|
|
218
|
+
"""
|
|
219
|
+
outstr = render(
|
|
220
|
+
content,
|
|
221
|
+
{
|
|
222
|
+
# "title" appears twice above -- both get the same value
|
|
223
|
+
"title": "Example rendered python object",
|
|
224
|
+
"content": "<p>Below: a python range rendered as a js array</p>",
|
|
225
|
+
"data_arr": repr(list(range(10))), # repr is JS-valid for a plain list
|
|
226
|
+
},
|
|
227
|
+
)
|
|
228
|
+
file_name = os.path.join(os.getcwd(), "comnt_test.html")
|
|
229
|
+
with open(file_name, "w", encoding="utf8") as outfile:
|
|
230
|
+
outfile.write(outstr)
|
|
231
|
+
_open_file(file_name)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def simple_example():
|
|
235
|
+
import json
|
|
236
|
+
content = """
|
|
237
|
+
<!DOCTYPE html>
|
|
238
|
+
<p id="title"></p>
|
|
239
|
+
<div id="data_arr"></div>
|
|
240
|
+
<script>
|
|
241
|
+
const title = /*[title*/ "Example title" /*title]*/;
|
|
242
|
+
const data = /*[data_arr*/ [0, 1] /*data_arr]*/;
|
|
243
|
+
document.getElementById("data_arr").textContent = JSON.stringify(data);
|
|
244
|
+
document.getElementById("title").textContent = title;
|
|
245
|
+
</script>
|
|
246
|
+
"""
|
|
247
|
+
outstr = render(
|
|
248
|
+
content,
|
|
249
|
+
{
|
|
250
|
+
"title": json.dumps("Example rendered python object"),
|
|
251
|
+
"data_arr": json.dumps(list(range(10))),
|
|
252
|
+
},
|
|
253
|
+
)
|
|
254
|
+
file_name = os.path.join(os.getcwd(), "comnt_simple.html")
|
|
255
|
+
with open(file_name, "w", encoding="utf8") as outfile:
|
|
256
|
+
outfile.write(outstr)
|
|
257
|
+
_open_file(file_name)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
if __name__ == "__main__":
|
|
261
|
+
# simple_example()
|
|
262
|
+
example()
|