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.
@@ -0,0 +1,3 @@
1
+ """DTree Explorer: encode a directed tree with DTree's projections Q1 and Q2 and watch it, step by step."""
2
+
3
+ __version__ = "1.0.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())
@@ -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
+ })