dtree-explorer 1.0.0__py3-none-any.whl
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.
- dtree_explorer/__init__.py +3 -0
- dtree_explorer/__main__.py +105 -0
- dtree_explorer/bridge.py +87 -0
- dtree_explorer/explorer.html +1963 -0
- dtree_explorer-1.0.0.dist-info/METADATA +74 -0
- dtree_explorer-1.0.0.dist-info/RECORD +10 -0
- dtree_explorer-1.0.0.dist-info/WHEEL +5 -0
- dtree_explorer-1.0.0.dist-info/entry_points.txt +2 -0
- dtree_explorer-1.0.0.dist-info/licenses/LICENSE.md +21 -0
- dtree_explorer-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Start the DTree Explorer: a local web page that computes with the installed dtree-encoding.
|
|
2
|
+
|
|
3
|
+
dtree-explorer open the Explorer in the browser
|
|
4
|
+
dtree-explorer --port 8765 use a fixed port (default: a free one)
|
|
5
|
+
dtree-explorer --no-browser only start the server and print its address
|
|
6
|
+
dtree-explorer --save FILE write the page as one HTML file and exit
|
|
7
|
+
|
|
8
|
+
The server listens on 127.0.0.1 only and serves the page and one computing endpoint;
|
|
9
|
+
nothing leaves your machine. Stop it with Ctrl+C.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import json
|
|
14
|
+
import platform
|
|
15
|
+
import sys
|
|
16
|
+
import webbrowser
|
|
17
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
18
|
+
from importlib import resources
|
|
19
|
+
|
|
20
|
+
import dtree
|
|
21
|
+
|
|
22
|
+
from . import __version__, bridge
|
|
23
|
+
|
|
24
|
+
MAX_INPUT = 4 * 1024 * 1024 # bytes of tree description per request
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def page() -> bytes:
|
|
28
|
+
return resources.files(__package__).joinpath("explorer.html").read_bytes()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Handler(BaseHTTPRequestHandler):
|
|
32
|
+
server_version = "DTreeExplorer/" + __version__
|
|
33
|
+
|
|
34
|
+
def _local_host(self) -> bool:
|
|
35
|
+
host = (self.headers.get("Host") or "").rsplit(":", 1)[0]
|
|
36
|
+
return host in ("127.0.0.1", "localhost") # refuses requests under other host names
|
|
37
|
+
|
|
38
|
+
def _send(self, code, kind, body: bytes):
|
|
39
|
+
self.send_response(code)
|
|
40
|
+
self.send_header("Content-Type", kind)
|
|
41
|
+
self.send_header("Content-Length", str(len(body)))
|
|
42
|
+
self.send_header("Cache-Control", "no-store")
|
|
43
|
+
self.end_headers()
|
|
44
|
+
self.wfile.write(body)
|
|
45
|
+
|
|
46
|
+
def do_GET(self):
|
|
47
|
+
if not self._local_host():
|
|
48
|
+
return self._send(403, "text/plain", b"forbidden")
|
|
49
|
+
path = self.path.split("?", 1)[0]
|
|
50
|
+
if path in ("/", "/index.html"):
|
|
51
|
+
return self._send(200, "text/html; charset=utf-8", page())
|
|
52
|
+
if path == "/api/info":
|
|
53
|
+
info = {"dtree": dtree.__version__, "python": platform.python_version(), "explorer": __version__}
|
|
54
|
+
return self._send(200, "application/json", json.dumps(info).encode())
|
|
55
|
+
self._send(404, "text/plain", b"not found")
|
|
56
|
+
|
|
57
|
+
def do_POST(self):
|
|
58
|
+
if not self._local_host():
|
|
59
|
+
return self._send(403, "text/plain", b"forbidden")
|
|
60
|
+
if self.path.split("?", 1)[0] != "/api/encode":
|
|
61
|
+
return self._send(404, "text/plain", b"not found")
|
|
62
|
+
n = int(self.headers.get("Content-Length") or 0)
|
|
63
|
+
if n > MAX_INPUT:
|
|
64
|
+
return self._send(413, "text/plain", b"description too large")
|
|
65
|
+
text = self.rfile.read(n).decode("utf-8", "replace")
|
|
66
|
+
self._send(200, "application/json", bridge.encode(text).encode())
|
|
67
|
+
|
|
68
|
+
def log_message(self, *args): # quiet
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def serve(port=0):
|
|
73
|
+
"""Create the server (not yet running); port 0 picks a free port."""
|
|
74
|
+
return ThreadingHTTPServer(("127.0.0.1", port), Handler)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def main(argv=None):
|
|
78
|
+
ap = argparse.ArgumentParser(prog="dtree-explorer", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
79
|
+
ap.add_argument("--port", type=int, default=0, help="port on 127.0.0.1 (default: a free one)")
|
|
80
|
+
ap.add_argument("--no-browser", action="store_true", help="do not open the browser")
|
|
81
|
+
ap.add_argument("--save", metavar="FILE", help="write the page as one HTML file and exit")
|
|
82
|
+
ap.add_argument("--version", action="version", version=f"DTree Explorer {__version__}, dtree-encoding {dtree.__version__}")
|
|
83
|
+
args = ap.parse_args(argv)
|
|
84
|
+
if args.save:
|
|
85
|
+
with open(args.save, "wb") as fh:
|
|
86
|
+
fh.write(page())
|
|
87
|
+
print(f"Wrote {args.save}. Open it in a browser; it loads Python (Pyodide) from the internet.")
|
|
88
|
+
return 0
|
|
89
|
+
httpd = serve(args.port)
|
|
90
|
+
url = f"http://127.0.0.1:{httpd.server_address[1]}/"
|
|
91
|
+
print(f"DTree Explorer {__version__} with dtree-encoding {dtree.__version__} at {url}")
|
|
92
|
+
print("Press Ctrl+C to stop.")
|
|
93
|
+
if not args.no_browser:
|
|
94
|
+
webbrowser.open(url)
|
|
95
|
+
try:
|
|
96
|
+
httpd.serve_forever()
|
|
97
|
+
except KeyboardInterrupt:
|
|
98
|
+
print("\nStopped.")
|
|
99
|
+
finally:
|
|
100
|
+
httpd.server_close()
|
|
101
|
+
return 0
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
if __name__ == "__main__":
|
|
105
|
+
sys.exit(main())
|
dtree_explorer/bridge.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Bridge between the DTree Explorer page and the dtree package.
|
|
2
|
+
|
|
3
|
+
The page runs this module in the browser (Pyodide) with the published dtree-encoding
|
|
4
|
+
package; the same code runs under CPython for the tests and to precompute the example
|
|
5
|
+
that is shown while the Python runtime loads. Everything the page displays about the
|
|
6
|
+
encoding (the order of Q1 and Q2, the positions, the parents) comes from here.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
|
|
11
|
+
import dtree
|
|
12
|
+
from dtree import DTree
|
|
13
|
+
|
|
14
|
+
LIMIT = 300 # insertions recorded step by step; larger trees show the final state only
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _split(line):
|
|
18
|
+
at = min((i for i in (line.find(":"), line.find(">"), line.find("<")) if i >= 0), default=-1)
|
|
19
|
+
if at < 0:
|
|
20
|
+
return None
|
|
21
|
+
return line[at], line[:at].strip(), [c.strip() for c in line[at + 1:].split(";") if c.strip()]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _state(t):
|
|
25
|
+
q1, q2 = t.q1, t.q2
|
|
26
|
+
pos = {v: [str(t.Q1.index(v)), str(t.Q2.index(v))] for v in q1}
|
|
27
|
+
return {"q1": q1, "q2": q2, "pos": pos}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def encode(text, limit=LIMIT):
|
|
31
|
+
"""Build a tree from the description and return the steps as JSON.
|
|
32
|
+
|
|
33
|
+
Each step is one inserted node: the operation, its line, the new node and its parent,
|
|
34
|
+
and (up to ``limit`` insertions) the full state after it: Q1, Q2 and the positions.
|
|
35
|
+
"""
|
|
36
|
+
t = DTree("E")
|
|
37
|
+
steps, errors, parents = [], [], {}
|
|
38
|
+
recording = True
|
|
39
|
+
for no, raw in enumerate(text.splitlines(), 1):
|
|
40
|
+
line = raw.strip().lstrip("")
|
|
41
|
+
if not line or line.startswith(("#", "--", "**")):
|
|
42
|
+
continue
|
|
43
|
+
if line.startswith("@@"):
|
|
44
|
+
break
|
|
45
|
+
parts = _split(line)
|
|
46
|
+
if parts is None:
|
|
47
|
+
errors.append({"line": no, "text": line, "msg": "expected ':', '>' or '<'"})
|
|
48
|
+
continue
|
|
49
|
+
op, anchor, kids = parts
|
|
50
|
+
for child in kids:
|
|
51
|
+
try:
|
|
52
|
+
if op == ":":
|
|
53
|
+
t.insert(anchor, child)
|
|
54
|
+
shown = f"{anchor}:{child}"
|
|
55
|
+
elif op == ">":
|
|
56
|
+
t.insert_after(anchor, child)
|
|
57
|
+
shown = f"{anchor}>{child}"
|
|
58
|
+
anchor = child
|
|
59
|
+
else:
|
|
60
|
+
t.insert_before(anchor, child)
|
|
61
|
+
shown = f"{anchor}<{child}"
|
|
62
|
+
except (KeyError, ValueError) as e:
|
|
63
|
+
msg = e.args[0] if e.args else str(e)
|
|
64
|
+
errors.append({"line": no, "text": line, "msg": f"{child}: {msg}"})
|
|
65
|
+
continue
|
|
66
|
+
parents[child] = t.get_parent(child)
|
|
67
|
+
step = {"line": no, "op": shown, "new": child}
|
|
68
|
+
if recording and len(steps) < limit:
|
|
69
|
+
step.update(_state(t))
|
|
70
|
+
elif recording:
|
|
71
|
+
recording = False
|
|
72
|
+
for s in steps:
|
|
73
|
+
for k in ("q1", "q2", "pos"):
|
|
74
|
+
s.pop(k, None)
|
|
75
|
+
steps.append(step)
|
|
76
|
+
final = _state(t) if t.root is not None else {"q1": [], "q2": [], "pos": {}}
|
|
77
|
+
return json.dumps({
|
|
78
|
+
"version": dtree.__version__,
|
|
79
|
+
"root": t.root,
|
|
80
|
+
"parents": parents,
|
|
81
|
+
"steps": steps,
|
|
82
|
+
"final": final,
|
|
83
|
+
"stepwise": recording,
|
|
84
|
+
"limit": limit,
|
|
85
|
+
"renumber": [t.Q1.renumber_count, t.Q2.renumber_count],
|
|
86
|
+
"errors": errors,
|
|
87
|
+
})
|