pyDiffTools 0.1.24__tar.gz → 0.1.25__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.
- {pydifftools-0.1.24/pyDiffTools.egg-info → pydifftools-0.1.25}/PKG-INFO +1 -1
- {pydifftools-0.1.24 → pydifftools-0.1.25/pyDiffTools.egg-info}/PKG-INFO +1 -1
- {pydifftools-0.1.24 → pydifftools-0.1.25}/pydifftools/browser_lifecycle.py +2 -1
- {pydifftools-0.1.24 → pydifftools-0.1.25}/pydifftools/continuous.py +2 -1
- {pydifftools-0.1.24 → pydifftools-0.1.25}/pydifftools/flowchart/graph.py +19 -24
- {pydifftools-0.1.24 → pydifftools-0.1.25}/pydifftools/flowchart/watch_graph.py +240 -38
- {pydifftools-0.1.24 → pydifftools-0.1.25}/pyproject.toml +1 -1
- {pydifftools-0.1.24 → pydifftools-0.1.25}/tests/test_browser_lifecycle.py +1 -12
- {pydifftools-0.1.24 → pydifftools-0.1.25}/LICENSE.md +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/MANIFEST.in +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/README.rst +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/example_notebook/_quarto.yml +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/example_notebook/project1/.jupyter_cache/__version__.txt +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/example_notebook/project1/.jupyter_cache/executed/1a724af72b16f5a9e607e12b1c721645/base.ipynb +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/example_notebook/project1/.jupyter_cache/executed/1b28fc9daac9081847e5161b2c546f8a/base.ipynb +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/example_notebook/project1/.jupyter_cache/executed/231f64eee282fa225d1104935cf80a24/base.ipynb +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/example_notebook/project1/.jupyter_cache/executed/26e56f6b0ff54851a45145157f2f0dc4/base.ipynb +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/example_notebook/project1/.jupyter_cache/executed/311fabd7029ffd050d056e2f316eb50f/base.ipynb +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/example_notebook/project1/.jupyter_cache/executed/57da2021e5b156ac3adf01398201c723/base.ipynb +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/example_notebook/project1/.jupyter_cache/executed/62b24ea7da75011d92b0f8924faa208d/base.ipynb +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/example_notebook/project1/.jupyter_cache/executed/7f1b20d69d889514ab5d1cc92e3cb14f/base.ipynb +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/example_notebook/project1/.jupyter_cache/executed/86f74c8c54a87ff892d9b15dd714e8f0/base.ipynb +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/example_notebook/project1/.jupyter_cache/executed/88893b4234eac2945d9d6cb2e277f186/base.ipynb +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/example_notebook/project1/.jupyter_cache/executed/9a40046ada6f582ee34af00fbdbfb417/base.ipynb +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/example_notebook/project1/.jupyter_cache/executed/a1bf4d270d0641ff41faf1d7cce3439a/base.ipynb +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/example_notebook/project1/.jupyter_cache/executed/a3789f7d9585a781f2a1c60ce95ff10d/base.ipynb +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/example_notebook/project1/.jupyter_cache/executed/be46ecba858d39ad5f0c46902ddf1c02/base.ipynb +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/example_notebook/project1/.jupyter_cache/executed/d0cbc57a12f2ccce710a5afd04cc05e7/base.ipynb +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/example_notebook/project1/.jupyter_cache/executed/d3e12d320b14228f701231ae32ddd7dd/base.ipynb +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/example_notebook/project1/.jupyter_cache/executed/eb434f61555438d020a6970a5dbf9ee8/base.ipynb +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/example_notebook/project1/.jupyter_cache/executed/f6b0e73aa7fa029134665d4dde57e096/base.ipynb +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/example_notebook/project1/.jupyter_cache/executed/f719a6e4ff09873cb0ffb06ec9d232f9/base.ipynb +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/example_notebook/project1/.jupyter_cache/executed/feeee244a7ce3d60e1a227eb604df823/base.ipynb +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/example_notebook/project1/.jupyter_cache/global.db +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/example_notebook/project1/example.qmd +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/example_notebook/project1/example.tex +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/example_notebook/project1/index.qmd +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/example_notebook/project1/subproject1/.jupyter_cache/__version__.txt +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/example_notebook/project1/subproject1/.jupyter_cache/executed/ca90e4df5f4f0583df6554156a68dc7f/base.ipynb +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/example_notebook/project1/subproject1/.jupyter_cache/global.db +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/example_notebook/project1/subproject1/end_vs_he_sketch.jpg +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/example_notebook/project1/subproject1/independent.qmd +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/example_notebook/project1/subproject1/index.qmd +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/example_notebook/project1/subproject1/tasks.qmd +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/example_notebook/project1/subproject1/test_include.qmd +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/example_notebook/project1/subproject1/tryforerror.qmd +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/example_notebook/project1/test_include.qmd +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/pyDiffTools.egg-info/SOURCES.txt +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/pyDiffTools.egg-info/dependency_links.txt +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/pyDiffTools.egg-info/entry_points.txt +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/pyDiffTools.egg-info/requires.txt +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/pyDiffTools.egg-info/top_level.txt +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/pydifftools/__init__.py +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/pydifftools/check_numbers.py +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/pydifftools/command_line.py +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/pydifftools/command_registry.py +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/pydifftools/comment_functions.py +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/pydifftools/comment_tags.lua +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/pydifftools/comment_toggle.js +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/pydifftools/comments.css +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/pydifftools/copy_files.py +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/pydifftools/diff-doc.js +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/pydifftools/doc_contents.py +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/pydifftools/flowchart/__init__.py +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/pydifftools/flowchart/dot_to_yaml.py +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/pydifftools/html_comments.py +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/pydifftools/html_uncomments.py +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/pydifftools/match_spaces.py +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/pydifftools/notebook/__init__.py +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/pydifftools/notebook/fast_build.py +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/pydifftools/notebook/tex_to_qmd.py +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/pydifftools/onewordify.py +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/pydifftools/onewordify_undo.py +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/pydifftools/outline.py +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/pydifftools/rearrange_tex.py +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/pydifftools/searchacro.py +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/pydifftools/separate_comments.py +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/pydifftools/split_conflict.py +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/pydifftools/unseparate_comments.py +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/pydifftools/update_check.py +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/pydifftools/wrap_sentences.py +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/pydifftools/xml2xlsx.vbs +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/setup.cfg +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/tests/test_continuous_shutdown.py +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/tests/test_rrng.py +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/tests/test_tex_to_qmd.py +0 -0
- {pydifftools-0.1.24 → pydifftools-0.1.25}/tests/test_update_check.py +0 -0
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
def browser_window_is_alive(browser):
|
|
2
2
|
# Keep all browser liveness checks in one place so watch commands share
|
|
3
3
|
# the same shutdown behavior when a user closes the browser window.
|
|
4
|
+
# Do not probe with execute_script here: page navigations can briefly
|
|
5
|
+
# interrupt script execution even while the window is still open.
|
|
4
6
|
if browser is None:
|
|
5
7
|
return False
|
|
6
8
|
try:
|
|
7
9
|
handles = browser.window_handles
|
|
8
10
|
if not handles:
|
|
9
11
|
return False
|
|
10
|
-
browser.execute_script("return 1")
|
|
11
12
|
return True
|
|
12
13
|
except Exception:
|
|
13
14
|
return False
|
|
@@ -312,7 +312,8 @@ position
|
|
|
312
312
|
);
|
|
313
313
|
});
|
|
314
314
|
|
|
315
|
-
// When the page has loaded,
|
|
315
|
+
// When the page has loaded,
|
|
316
|
+
// restore hidden comments and scroll position
|
|
316
317
|
window.addEventListener('load', function() {
|
|
317
318
|
var hiddenCommentIndexes = sessionStorage.getItem(
|
|
318
319
|
'commentHiddenBubbleIndexes'
|
|
@@ -378,28 +378,26 @@ def _normalize_graph_dates(data):
|
|
|
378
378
|
def _append_node(
|
|
379
379
|
lines, indent, node_name, data, wrap_width, order_by_date, sort_order
|
|
380
380
|
):
|
|
381
|
-
#
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
381
|
+
# Every rendered DOT node corresponds to a real YAML node, so build the
|
|
382
|
+
# label directly from that node and prepend the task-link marker line.
|
|
383
|
+
label = _node_label(
|
|
384
|
+
_node_text_with_due(data["nodes"][node_name]), wrap_width
|
|
385
|
+
)
|
|
386
|
+
task_link_line = (
|
|
387
|
+
f'<font point-size="7">__WGRPH_TASK_LINK__:{node_name}</font>'
|
|
388
|
+
)
|
|
388
389
|
if label:
|
|
389
|
-
|
|
390
|
-
lines.append(
|
|
391
|
-
f"{indent}{node_name} [label={label},"
|
|
392
|
-
f" sortv={sort_order[node_name]}];"
|
|
393
|
-
)
|
|
394
|
-
else:
|
|
395
|
-
lines.append(f"{indent}{node_name} [label={label}];")
|
|
390
|
+
label = "<" + task_link_line + '<br align="left"/>' + label[1:]
|
|
396
391
|
else:
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
392
|
+
label = "<" + task_link_line + '<br align="left"/>' + ">"
|
|
393
|
+
|
|
394
|
+
if order_by_date:
|
|
395
|
+
lines.append(
|
|
396
|
+
f"{indent}{node_name} [label={label},"
|
|
397
|
+
f" sortv={sort_order[node_name]}];"
|
|
398
|
+
)
|
|
399
|
+
else:
|
|
400
|
+
lines.append(f"{indent}{node_name} [label={label}];")
|
|
403
401
|
|
|
404
402
|
|
|
405
403
|
def yaml_to_dot(data, wrap_width=55, order_by_date=False):
|
|
@@ -648,10 +646,7 @@ def write_dot_from_yaml(
|
|
|
648
646
|
if parent in ancestors:
|
|
649
647
|
continue
|
|
650
648
|
ancestors.add(parent)
|
|
651
|
-
if
|
|
652
|
-
parent in data["nodes"]
|
|
653
|
-
and "parents" in data["nodes"][parent]
|
|
654
|
-
):
|
|
649
|
+
if parent in data["nodes"] and "parents" in data["nodes"][parent]:
|
|
655
650
|
for grandparent in data["nodes"][parent]["parents"]:
|
|
656
651
|
parents_to_check.append(grandparent)
|
|
657
652
|
incomplete_ancestors = set()
|
|
@@ -2,6 +2,9 @@ import subprocess
|
|
|
2
2
|
import time
|
|
3
3
|
import shutil
|
|
4
4
|
import math
|
|
5
|
+
import threading
|
|
6
|
+
import urllib.parse
|
|
7
|
+
import http.server
|
|
5
8
|
import xml.etree.ElementTree as ET
|
|
6
9
|
from pathlib import Path
|
|
7
10
|
|
|
@@ -24,12 +27,19 @@ from pydifftools.browser_lifecycle import (
|
|
|
24
27
|
from .graph import write_dot_from_yaml
|
|
25
28
|
|
|
26
29
|
|
|
27
|
-
def _reload_svg(driver,
|
|
30
|
+
def _reload_svg(driver, svg_src) -> None:
|
|
28
31
|
"""Refresh the embedded SVG while preserving zoom and scroll."""
|
|
29
32
|
zoom = driver.execute_script("return window.visualViewport.scale")
|
|
30
33
|
scroll_x = driver.execute_script("return window.scrollX")
|
|
31
34
|
scroll_y = driver.execute_script("return window.scrollY")
|
|
32
|
-
|
|
35
|
+
if isinstance(svg_src, Path):
|
|
36
|
+
svg_uri = svg_src.resolve().as_uri()
|
|
37
|
+
else:
|
|
38
|
+
svg_uri = str(svg_src)
|
|
39
|
+
if "?" in svg_uri:
|
|
40
|
+
svg_uri = svg_uri + f"&ts={time.time()}"
|
|
41
|
+
else:
|
|
42
|
+
svg_uri = svg_uri + f"?ts={time.time()}"
|
|
33
43
|
driver.execute_async_script(
|
|
34
44
|
"const [src,z,x,y,done]=arguments;const"
|
|
35
45
|
" s=document.getElementById('svg-view');s.onload=function()"
|
|
@@ -42,10 +52,10 @@ def _reload_svg(driver, svg_file: Path) -> None:
|
|
|
42
52
|
)
|
|
43
53
|
|
|
44
54
|
|
|
45
|
-
def start_chrome(webdriver, options,
|
|
46
|
-
# Launch Chrome and display the local SVG preview
|
|
55
|
+
def start_chrome(webdriver, options, preview_url):
|
|
56
|
+
# Launch Chrome and display the local SVG preview page from the server.
|
|
47
57
|
driver = webdriver.Chrome(options=options)
|
|
48
|
-
driver.get(
|
|
58
|
+
driver.get(preview_url)
|
|
49
59
|
return driver
|
|
50
60
|
|
|
51
61
|
|
|
@@ -219,11 +229,23 @@ def _svg_add_canvas_padding(svg_root, padding=8.0):
|
|
|
219
229
|
)
|
|
220
230
|
|
|
221
231
|
|
|
222
|
-
def _watch_html(
|
|
232
|
+
def _watch_html(svg_url, order_by_date):
|
|
233
|
+
# Keep the SVG as the page's main content so browser zoom behavior matches
|
|
234
|
+
# the original watcher experience (the graph scales, not just footer text).
|
|
235
|
+
# The footer link toggles between dependency/date views depending on mode.
|
|
236
|
+
footer_label = "date-ordered"
|
|
237
|
+
footer_url = "/?d=1"
|
|
238
|
+
if order_by_date:
|
|
239
|
+
footer_label = "dependency-ordered"
|
|
240
|
+
footer_url = "/"
|
|
223
241
|
return (
|
|
224
|
-
"<html><body style='margin:0'
|
|
225
|
-
" style='display:block;' type='image/svg+xml'"
|
|
226
|
-
f" src='{
|
|
242
|
+
"<html><body style='margin:0'>"
|
|
243
|
+
"<embed id='svg-view' style='display:block;' type='image/svg+xml'"
|
|
244
|
+
f" src='{svg_url}'/>"
|
|
245
|
+
"<p style='margin:0.4em 0.8em;font-family:sans-serif;font-size:13px;'>"
|
|
246
|
+
f"<a href='{footer_url}'>{footer_label}</a>"
|
|
247
|
+
"</p>"
|
|
248
|
+
"</body></html>"
|
|
227
249
|
)
|
|
228
250
|
|
|
229
251
|
|
|
@@ -263,6 +285,32 @@ def _svg_expanded_outline(
|
|
|
263
285
|
)
|
|
264
286
|
|
|
265
287
|
|
|
288
|
+
def _svg_add_task_links(svg_root, namespace):
|
|
289
|
+
# Replace marker text emitted in DOT labels with clickable links. Graphviz
|
|
290
|
+
# generates one <text> item per line, so the marker occupies its own row.
|
|
291
|
+
xlink_ns = "http://www.w3.org/1999/xlink"
|
|
292
|
+
ET.register_namespace("xlink", xlink_ns)
|
|
293
|
+
link_marker = "__WGRPH_TASK_LINK__:"
|
|
294
|
+
for group in svg_root.iter(f"{namespace}g"):
|
|
295
|
+
if "class" not in group.attrib or group.attrib["class"] != "node":
|
|
296
|
+
continue
|
|
297
|
+
for index, child in enumerate(list(group)):
|
|
298
|
+
if child.tag != f"{namespace}text" or child.text is None:
|
|
299
|
+
continue
|
|
300
|
+
if not child.text.startswith(link_marker):
|
|
301
|
+
continue
|
|
302
|
+
task_name = child.text[len(link_marker) :]
|
|
303
|
+
child.text = task_name
|
|
304
|
+
link = ET.Element(f"{namespace}a")
|
|
305
|
+
link.set(
|
|
306
|
+
f"{{{xlink_ns}}}href", f"/?t={urllib.parse.quote(task_name)}"
|
|
307
|
+
)
|
|
308
|
+
link.set("target", "_top")
|
|
309
|
+
link.append(child)
|
|
310
|
+
group.remove(child)
|
|
311
|
+
group.insert(index, link)
|
|
312
|
+
|
|
313
|
+
|
|
266
314
|
def build_graph(
|
|
267
315
|
yaml_file,
|
|
268
316
|
dot_file,
|
|
@@ -297,6 +345,8 @@ def build_graph(
|
|
|
297
345
|
if svg_root.tag.startswith("{"):
|
|
298
346
|
namespace = svg_root.tag[: svg_root.tag.find("}") + 1]
|
|
299
347
|
|
|
348
|
+
_svg_add_task_links(svg_root, namespace)
|
|
349
|
+
|
|
300
350
|
if not order_by_date:
|
|
301
351
|
# In dependency view mode, each node explicitly tagged with
|
|
302
352
|
# ``style: endpoint`` defines a project color. A project includes the
|
|
@@ -502,27 +552,30 @@ class GraphEventHandler(FileSystemEventHandler):
|
|
|
502
552
|
yaml_file,
|
|
503
553
|
dot_file,
|
|
504
554
|
svg_file,
|
|
505
|
-
|
|
555
|
+
preview_url=None,
|
|
556
|
+
svg_url=None,
|
|
506
557
|
driver=None,
|
|
507
558
|
options=None,
|
|
508
559
|
webdriver=None,
|
|
509
560
|
wrap_width=55,
|
|
510
561
|
data=None,
|
|
511
|
-
|
|
512
|
-
target_task=None,
|
|
562
|
+
state=None,
|
|
513
563
|
debounce=0.25,
|
|
514
564
|
):
|
|
515
565
|
self.yaml_file = Path(yaml_file)
|
|
516
566
|
self.dot_file = Path(dot_file)
|
|
517
567
|
self.svg_file = Path(svg_file)
|
|
518
|
-
self.
|
|
568
|
+
self.preview_url = preview_url
|
|
569
|
+
self.svg_url = svg_url
|
|
519
570
|
self.driver = driver
|
|
520
571
|
self.options = options
|
|
521
572
|
self.webdriver = webdriver
|
|
522
573
|
self.wrap_width = wrap_width
|
|
523
574
|
self.data = data
|
|
524
|
-
|
|
525
|
-
|
|
575
|
+
if state is None:
|
|
576
|
+
self.state = {"order_by_date": False, "target_task": None}
|
|
577
|
+
else:
|
|
578
|
+
self.state = state
|
|
526
579
|
self.debounce = debounce
|
|
527
580
|
self._last_handled = 0.0
|
|
528
581
|
self._last_mtime = None
|
|
@@ -542,9 +595,9 @@ class GraphEventHandler(FileSystemEventHandler):
|
|
|
542
595
|
self.dot_file,
|
|
543
596
|
self.svg_file,
|
|
544
597
|
self.wrap_width,
|
|
545
|
-
self.order_by_date,
|
|
598
|
+
self.state["order_by_date"],
|
|
546
599
|
self.data,
|
|
547
|
-
self.target_task,
|
|
600
|
+
self.state["target_task"],
|
|
548
601
|
)
|
|
549
602
|
except Exception:
|
|
550
603
|
# If the graph fails to build (e.g. invalid date), close the
|
|
@@ -558,21 +611,135 @@ class GraphEventHandler(FileSystemEventHandler):
|
|
|
558
611
|
if (
|
|
559
612
|
self.webdriver is not None
|
|
560
613
|
and self.options is not None
|
|
561
|
-
and self.
|
|
614
|
+
and self.preview_url is not None
|
|
562
615
|
):
|
|
563
616
|
self.driver = start_chrome(
|
|
564
|
-
self.webdriver, self.options, self.
|
|
617
|
+
self.webdriver, self.options, self.preview_url
|
|
565
618
|
)
|
|
566
619
|
else:
|
|
567
|
-
# Allow legacy
|
|
568
|
-
|
|
620
|
+
# Allow test/legacy usage where no browser driver exists.
|
|
621
|
+
if self.svg_url is not None:
|
|
622
|
+
_reload_svg(self.driver, self.svg_url)
|
|
623
|
+
else:
|
|
624
|
+
_reload_svg(self.driver, self.svg_file)
|
|
569
625
|
self._last_mtime = self.yaml_file.stat().st_mtime
|
|
570
626
|
return
|
|
627
|
+
if self.svg_url is not None:
|
|
628
|
+
_reload_svg(self.driver, self.svg_url)
|
|
571
629
|
else:
|
|
572
630
|
_reload_svg(self.driver, self.svg_file)
|
|
573
631
|
self._last_mtime = self.yaml_file.stat().st_mtime
|
|
574
632
|
|
|
575
633
|
|
|
634
|
+
class FlowchartPreviewServer:
|
|
635
|
+
def __init__(self, event_handler, host="127.0.0.1"):
|
|
636
|
+
self.event_handler = event_handler
|
|
637
|
+
self.host = host
|
|
638
|
+
self.httpd = None
|
|
639
|
+
self.server_thread = None
|
|
640
|
+
self.base_url = None
|
|
641
|
+
self.svg_url = None
|
|
642
|
+
|
|
643
|
+
def start(self):
|
|
644
|
+
event_handler = self.event_handler
|
|
645
|
+
|
|
646
|
+
class Handler(http.server.BaseHTTPRequestHandler):
|
|
647
|
+
def do_GET(self):
|
|
648
|
+
parsed = urllib.parse.urlparse(self.path)
|
|
649
|
+
if parsed.path == "/graph.svg":
|
|
650
|
+
svg_bytes = event_handler.svg_file.read_bytes()
|
|
651
|
+
self.send_response(200)
|
|
652
|
+
self.send_header(
|
|
653
|
+
"Content-Type", "image/svg+xml; charset=utf-8"
|
|
654
|
+
)
|
|
655
|
+
self.send_header("Cache-Control", "no-store")
|
|
656
|
+
self.end_headers()
|
|
657
|
+
self.wfile.write(svg_bytes)
|
|
658
|
+
return
|
|
659
|
+
|
|
660
|
+
if parsed.path != "/" and parsed.path != "/index.html":
|
|
661
|
+
self.send_error(404)
|
|
662
|
+
return
|
|
663
|
+
|
|
664
|
+
# Parse query args so GET requests control graph mode.
|
|
665
|
+
params = urllib.parse.parse_qs(
|
|
666
|
+
parsed.query, keep_blank_values=True
|
|
667
|
+
)
|
|
668
|
+
order_by_date = event_handler.state["order_by_date"]
|
|
669
|
+
target_task = event_handler.state["target_task"]
|
|
670
|
+
if "d" in params:
|
|
671
|
+
d_value = params["d"][-1]
|
|
672
|
+
order_by_date = d_value in (
|
|
673
|
+
"1",
|
|
674
|
+
"true",
|
|
675
|
+
"yes",
|
|
676
|
+
"on",
|
|
677
|
+
"",
|
|
678
|
+
)
|
|
679
|
+
if "t" in params:
|
|
680
|
+
t_value = params["t"][-1].strip()
|
|
681
|
+
if t_value:
|
|
682
|
+
target_task = t_value
|
|
683
|
+
order_by_date = False
|
|
684
|
+
else:
|
|
685
|
+
target_task = None
|
|
686
|
+
|
|
687
|
+
if (
|
|
688
|
+
order_by_date != event_handler.state["order_by_date"]
|
|
689
|
+
or target_task != event_handler.state["target_task"]
|
|
690
|
+
):
|
|
691
|
+
event_handler.state["order_by_date"] = order_by_date
|
|
692
|
+
event_handler.state["target_task"] = target_task
|
|
693
|
+
event_handler.data = build_graph(
|
|
694
|
+
event_handler.yaml_file,
|
|
695
|
+
event_handler.dot_file,
|
|
696
|
+
event_handler.svg_file,
|
|
697
|
+
event_handler.wrap_width,
|
|
698
|
+
event_handler.state["order_by_date"],
|
|
699
|
+
event_handler.data,
|
|
700
|
+
event_handler.state["target_task"],
|
|
701
|
+
)
|
|
702
|
+
|
|
703
|
+
body = _watch_html(
|
|
704
|
+
"/graph.svg", event_handler.state["order_by_date"]
|
|
705
|
+
)
|
|
706
|
+
body_bytes = body.encode("utf-8")
|
|
707
|
+
self.send_response(200)
|
|
708
|
+
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
709
|
+
self.send_header("Content-Length", str(len(body_bytes)))
|
|
710
|
+
self.send_header("Cache-Control", "no-store")
|
|
711
|
+
self.end_headers()
|
|
712
|
+
self.wfile.write(body_bytes)
|
|
713
|
+
|
|
714
|
+
def log_message(self, format, *args):
|
|
715
|
+
return
|
|
716
|
+
|
|
717
|
+
self.httpd = http.server.ThreadingHTTPServer((self.host, 0), Handler)
|
|
718
|
+
self.httpd.daemon_threads = True
|
|
719
|
+
port = self.httpd.server_address[1]
|
|
720
|
+
self.base_url = f"http://{self.host}:{port}/"
|
|
721
|
+
self.svg_url = f"http://{self.host}:{port}/graph.svg"
|
|
722
|
+
# Start serving immediately so the first browser navigation does not
|
|
723
|
+
# block waiting for the watcher loop to call handle_request.
|
|
724
|
+
self.server_thread = threading.Thread(
|
|
725
|
+
target=self.httpd.serve_forever,
|
|
726
|
+
daemon=True,
|
|
727
|
+
)
|
|
728
|
+
self.server_thread.start()
|
|
729
|
+
|
|
730
|
+
def serve_pending_request(self):
|
|
731
|
+
# The server runs in a background thread; this method remains for
|
|
732
|
+
# compatibility with the watcher loop call site.
|
|
733
|
+
return
|
|
734
|
+
|
|
735
|
+
def stop(self):
|
|
736
|
+
if self.httpd is not None:
|
|
737
|
+
self.httpd.shutdown()
|
|
738
|
+
self.httpd.server_close()
|
|
739
|
+
if self.server_thread is not None:
|
|
740
|
+
self.server_thread.join(timeout=1.0)
|
|
741
|
+
|
|
742
|
+
|
|
576
743
|
@register_command(
|
|
577
744
|
"Watch a flowchart YAML file, rebuild DOT/SVG output, and open the"
|
|
578
745
|
" preview",
|
|
@@ -580,7 +747,7 @@ class GraphEventHandler(FileSystemEventHandler):
|
|
|
580
747
|
"yaml": "Path to the flowchart YAML file",
|
|
581
748
|
"wrap_width": "Line wrap width used when generating node labels",
|
|
582
749
|
"d": "Render nodes by date without showing connections",
|
|
583
|
-
"t":
|
|
750
|
+
"t": "Task name to focus on (show incomplete ancestor tasks only)",
|
|
584
751
|
},
|
|
585
752
|
)
|
|
586
753
|
def wgrph(yaml, wrap_width=55, d=False, t=None):
|
|
@@ -602,41 +769,76 @@ def wgrph(yaml, wrap_width=55, d=False, t=None):
|
|
|
602
769
|
|
|
603
770
|
dot_file = yaml_file.with_suffix(".dot")
|
|
604
771
|
svg_file = yaml_file.with_suffix(".svg")
|
|
605
|
-
html_file = yaml_file.with_suffix(".html")
|
|
606
772
|
|
|
607
|
-
#
|
|
608
|
-
#
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
773
|
+
# The browser now drives filtering/date-mode by GET query parameters
|
|
774
|
+
# handled by the local preview server. Keep Python state in sync there.
|
|
775
|
+
initial_state = {"order_by_date": False, "target_task": None}
|
|
776
|
+
|
|
777
|
+
# Build the default dependency graph first. Optional -t / -d args are then
|
|
778
|
+
# applied by requesting server URLs with query parameters.
|
|
779
|
+
data = build_graph(
|
|
780
|
+
yaml_file,
|
|
781
|
+
dot_file,
|
|
782
|
+
svg_file,
|
|
783
|
+
wrap_width,
|
|
784
|
+
initial_state["order_by_date"],
|
|
785
|
+
None,
|
|
786
|
+
initial_state["target_task"],
|
|
787
|
+
)
|
|
788
|
+
|
|
612
789
|
options = Options()
|
|
613
|
-
driver = start_chrome(webdriver, options, html_file)
|
|
614
790
|
event_handler = GraphEventHandler(
|
|
615
791
|
yaml_file,
|
|
616
792
|
dot_file,
|
|
617
793
|
svg_file,
|
|
618
|
-
|
|
619
|
-
|
|
794
|
+
None,
|
|
795
|
+
None,
|
|
796
|
+
None,
|
|
620
797
|
options,
|
|
621
798
|
webdriver,
|
|
622
799
|
wrap_width,
|
|
623
800
|
data,
|
|
624
|
-
|
|
625
|
-
t,
|
|
801
|
+
initial_state,
|
|
626
802
|
)
|
|
803
|
+
preview_server = FlowchartPreviewServer(event_handler)
|
|
804
|
+
preview_server.start()
|
|
805
|
+
event_handler.preview_url = preview_server.base_url
|
|
806
|
+
event_handler.svg_url = preview_server.svg_url
|
|
807
|
+
|
|
808
|
+
driver = start_chrome(webdriver, options, preview_server.base_url)
|
|
809
|
+
event_handler.driver = driver
|
|
810
|
+
|
|
811
|
+
if t is not None and str(t).strip():
|
|
812
|
+
driver.get(
|
|
813
|
+
preview_server.base_url
|
|
814
|
+
+ "?t="
|
|
815
|
+
+ urllib.parse.quote(str(t).strip())
|
|
816
|
+
)
|
|
817
|
+
elif d:
|
|
818
|
+
driver.get(preview_server.base_url + "?d=1")
|
|
819
|
+
|
|
627
820
|
observer = Observer()
|
|
628
821
|
observer.schedule(event_handler, yaml_file.parent, recursive=False)
|
|
629
822
|
observer.start()
|
|
823
|
+
dead_since = None
|
|
630
824
|
try:
|
|
631
825
|
while True:
|
|
632
|
-
|
|
633
|
-
#
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
826
|
+
preview_server.serve_pending_request()
|
|
827
|
+
# Exit the watcher when the browser window is really closed, but
|
|
828
|
+
# tolerate short Selenium liveness errors during top-level
|
|
829
|
+
# navigation (for example when opening /?t=... from the links).
|
|
830
|
+
if browser_window_is_alive(event_handler.driver):
|
|
831
|
+
dead_since = None
|
|
832
|
+
else:
|
|
833
|
+
if dead_since is None:
|
|
834
|
+
dead_since = time.time()
|
|
835
|
+
elif time.time() - dead_since >= 1.0:
|
|
836
|
+
break
|
|
837
|
+
time.sleep(0.1)
|
|
637
838
|
except KeyboardInterrupt:
|
|
638
839
|
pass
|
|
639
840
|
finally:
|
|
640
841
|
observer.stop()
|
|
641
842
|
observer.join()
|
|
642
843
|
close_chrome(event_handler.driver)
|
|
844
|
+
preview_server.stop()
|
|
@@ -2,17 +2,11 @@ from pydifftools import browser_lifecycle
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
class FakeBrowser:
|
|
5
|
-
def __init__(self, handles=None,
|
|
5
|
+
def __init__(self, handles=None, quit_error=False):
|
|
6
6
|
self.window_handles = handles if handles is not None else ["main"]
|
|
7
|
-
self.script_error = script_error
|
|
8
7
|
self.quit_error = quit_error
|
|
9
8
|
self.quit_calls = 0
|
|
10
9
|
|
|
11
|
-
def execute_script(self, _code):
|
|
12
|
-
if self.script_error:
|
|
13
|
-
raise RuntimeError("window closed")
|
|
14
|
-
return 1
|
|
15
|
-
|
|
16
10
|
def quit(self):
|
|
17
11
|
self.quit_calls += 1
|
|
18
12
|
if self.quit_error:
|
|
@@ -29,11 +23,6 @@ def test_browser_window_is_alive_false_when_handles_missing():
|
|
|
29
23
|
assert not browser_lifecycle.browser_window_is_alive(browser)
|
|
30
24
|
|
|
31
25
|
|
|
32
|
-
def test_browser_window_is_alive_false_when_script_errors():
|
|
33
|
-
browser = FakeBrowser(script_error=True)
|
|
34
|
-
assert not browser_lifecycle.browser_window_is_alive(browser)
|
|
35
|
-
|
|
36
|
-
|
|
37
26
|
def test_close_browser_window_quits_and_swallows_errors():
|
|
38
27
|
browser = FakeBrowser(quit_error=True)
|
|
39
28
|
browser_lifecycle.close_browser_window(browser)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pydifftools-0.1.24 → pydifftools-0.1.25}/example_notebook/project1/.jupyter_cache/__version__.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pydifftools-0.1.24 → pydifftools-0.1.25}/example_notebook/project1/.jupyter_cache/global.db
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pydifftools-0.1.24 → pydifftools-0.1.25}/example_notebook/project1/subproject1/end_vs_he_sketch.jpg
RENAMED
|
File without changes
|
{pydifftools-0.1.24 → pydifftools-0.1.25}/example_notebook/project1/subproject1/independent.qmd
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pydifftools-0.1.24 → pydifftools-0.1.25}/example_notebook/project1/subproject1/test_include.qmd
RENAMED
|
File without changes
|
{pydifftools-0.1.24 → pydifftools-0.1.25}/example_notebook/project1/subproject1/tryforerror.qmd
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|