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.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: df2tables
3
- Version: 0.2.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
@@ -1,5 +1,5 @@
1
1
  from .df2tables import *
2
- __version__ = "0.2.2"
2
+ __version__ = "0.2.4"
3
3
  __author__ = "Tomasz Slugocki"
4
4
  __license__ = "MIT"
5
5
 
@@ -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()