hiplot-plus 0.1.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.
hiplot/render.py ADDED
@@ -0,0 +1,141 @@
1
+ # Copyright (c) Facebook, Inc. and its affiliates.
2
+ # This source code is licensed under the MIT license found in the
3
+ # LICENSE file in the root directory of this source tree.
4
+
5
+ import argparse
6
+ import base64
7
+ import codecs
8
+ import json
9
+ import sys
10
+ import uuid
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from . import fetchers
15
+
16
+
17
+ def escapejs(val: Any) -> str:
18
+ return json.dumps(str(val))
19
+
20
+
21
+ def render_jinja_html(template_loc: str, file_name: str) -> str:
22
+ import jinja2
23
+
24
+ return jinja2.Environment(loader=jinja2.FileSystemLoader(template_loc + "/")).get_template(file_name).render()
25
+
26
+
27
+ def html_inlinize(html: str, replace_local: bool = True) -> str:
28
+ """
29
+ Includes external CSS, JS and images directly in the HTML
30
+ (only for files with a relative path)
31
+ """
32
+ from bs4 import BeautifulSoup
33
+
34
+ SUFFIX_TO_TYPE = {
35
+ ".png": "image/png",
36
+ ".svg": "image/svg+xml",
37
+ }
38
+ static_root = str(Path(__file__).parent)
39
+ soup = BeautifulSoup(html, "html.parser")
40
+ for i in soup.find_all("link"):
41
+ href = str(i["href"])
42
+ if href.startswith(("http", "//")):
43
+ continue
44
+ if not replace_local:
45
+ continue
46
+
47
+ if i["rel"][0] == "stylesheet":
48
+ if href.startswith("/"):
49
+ href = href[1:]
50
+ file = Path(static_root, href)
51
+ new_tag = soup.new_tag("style")
52
+ new_tag.string = file.read_text(encoding="utf-8")
53
+ i.replace_with(new_tag)
54
+ elif i["rel"][0] == "icon": # Favicon
55
+ file = Path(static_root, href)
56
+ encoded = base64.b64encode(file.read_bytes()).decode("ascii")
57
+ i["href"] = f"data:{SUFFIX_TO_TYPE[file.suffix]};base64,{encoded}"
58
+ for i in soup.find_all("script"):
59
+ try:
60
+ src = str(i["src"])
61
+ except KeyError:
62
+ continue
63
+ if src.startswith(("http", "//")):
64
+ continue
65
+ if not replace_local:
66
+ continue
67
+
68
+ if src.startswith("/"):
69
+ src = src[1:]
70
+ file = Path(static_root, src)
71
+ new_tag = soup.new_tag("script")
72
+ new_tag.string = file.read_text(encoding="utf-8")
73
+ new_tag["type"] = i["type"]
74
+ i.replace_with(new_tag)
75
+ return str(soup)
76
+
77
+
78
+ #: Where the template receives the JSON data block holding the options.
79
+ DATA_MARKER = "<!--HIPLOT_DATA-->"
80
+
81
+
82
+ def get_index_html_template() -> str:
83
+ return render_jinja_html(str(Path(__file__).parent / "templates"), "index.html")
84
+
85
+
86
+ def json_for_script_element(value: Any) -> str:
87
+ """Strict JSON that is safe as the text of a <script> element.
88
+
89
+ Escaping every "<" keeps "</script>" and "<!--" out of the element; JSON.parse
90
+ reads the escape back as "<".
91
+ """
92
+ return json.dumps(value, allow_nan=False).replace("<", "\\u003c")
93
+
94
+
95
+ def make_experiment_standalone_page(options: dict[str, Any], inline_static: bool = False) -> str:
96
+ """A page rendering HiPlot with `options`, which are embedded as a JSON data block.
97
+
98
+ With `inline_static`, the bundle and assets are inlined (see `html_inlinize`)
99
+ before the options are added, so large experiments are never parsed as HTML.
100
+ """
101
+ hiplot_options = {"dataProviderName": "none"}
102
+ hiplot_options.update(options)
103
+
104
+ page_id = uuid.uuid4().hex
105
+ data_id = f"hiplot_data_{page_id}"
106
+ index_html = get_index_html_template()
107
+ if inline_static:
108
+ index_html = html_inlinize(index_html)
109
+ index_html = index_html.replace("hiplot_element_id", f"hiplot_{page_id}")
110
+ index_html = index_html.replace(
111
+ DATA_MARKER,
112
+ f'<script type="application/json" id="{data_id}">{json_for_script_element(hiplot_options)}</script>',
113
+ )
114
+ return index_html.replace(
115
+ "/*ON_LOAD_SCRIPT_INJECT*/",
116
+ f"""/*ON_LOAD_SCRIPT_INJECT*/
117
+ Object.assign(options, JSON.parse(document.getElementById({escapejs(data_id)}).textContent));
118
+ """,
119
+ )
120
+
121
+
122
+ def hiplot_render_main() -> int:
123
+ parser = argparse.ArgumentParser(
124
+ prog="HiPlot", description="Render an HiPlot experiment to HTML or CSV in the standard output"
125
+ )
126
+ parser.add_argument("experiment_uri", type=str)
127
+ parser.add_argument("--format", default="html", choices=["csv", "html"], help="File format")
128
+ parser.add_argument("fetchers", nargs="*", type=str, help="Additional fetchers to use")
129
+ args = parser.parse_args()
130
+
131
+ exp = fetchers.load_xp_with_fetchers(fetchers.get_fetchers(args.fetchers), args.experiment_uri)
132
+ exp.validate()
133
+ stdout_writer = codecs.getwriter("utf-8")(sys.stdout.buffer)
134
+ match args.format:
135
+ case "csv":
136
+ exp.to_csv(stdout_writer)
137
+ case "html":
138
+ exp.to_html(stdout_writer)
139
+ case unknown:
140
+ raise ValueError(f"Unsupported format: {unknown}")
141
+ return 0
hiplot/server.py ADDED
@@ -0,0 +1,76 @@
1
+ # Copyright (c) Facebook, Inc. and its affiliates.
2
+ # This source code is licensed under the MIT license found in the
3
+ # LICENSE file in the root directory of this source tree.
4
+
5
+ import argparse
6
+ from collections.abc import Iterable
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from . import experiment as exp
10
+ from . import pkginfo
11
+ from .fetchers import NoFetcherFound, get_fetchers, load_xp_with_fetchers
12
+ from .render import get_index_html_template
13
+
14
+ if TYPE_CHECKING:
15
+ from flask import Flask
16
+
17
+
18
+ def create_app(fetchers: Iterable[exp.ExperimentFetcher]) -> "Flask":
19
+ """The HiPlot web app: the interface at `/`, experiments loaded by URI at `/data`.
20
+
21
+ Args:
22
+ fetchers: functions that convert a URI into a :class:`hiplot.Experiment`, tried in order.
23
+ """
24
+ try:
25
+ from flask import Flask, jsonify, request
26
+ from flask_compress import Compress
27
+ except ImportError as e:
28
+ raise ImportError(
29
+ "Flask is required to run the HiPlot server. Install it with: pip install hiplot-plus[server]"
30
+ ) from e
31
+
32
+ fetchers = list(fetchers)
33
+ app = Flask(__name__)
34
+
35
+ @app.route("/")
36
+ def index() -> Any:
37
+ return get_index_html_template()
38
+
39
+ @app.route("/data")
40
+ def data() -> Any:
41
+ uri = request.args.get("uri", type=str)
42
+ if uri is None:
43
+ return jsonify({"error": "Missing the uri query parameter"}), 400
44
+ try:
45
+ xp = load_xp_with_fetchers(fetchers, uri)
46
+ xp.validate()
47
+ return jsonify({"query": uri, "experiment": xp._payload()})
48
+ except NoFetcherFound as e:
49
+ return jsonify({"error": f"No fetcher found for this experiment: {e}"})
50
+
51
+ Compress(app)
52
+ return app
53
+
54
+
55
+ def run_server(
56
+ fetchers: list[exp.ExperimentFetcher], host: str = "127.0.0.1", port: int = 5005, debug: bool = False
57
+ ) -> None:
58
+ """
59
+ Runs the HiPlot server, given a list of ExperimentFetchers:
60
+ functions that convert a URI into a :class:`hiplot.Experiment`
61
+ """
62
+ create_app(fetchers).run(debug=debug, host=host, port=port)
63
+
64
+
65
+ def run_server_main() -> int:
66
+ parser = argparse.ArgumentParser(prog="HiPlot", description="Start HiPlot webserver")
67
+ parser.add_argument("--version", action="version", version=f"{pkginfo.package_name} {pkginfo.__version__}")
68
+ parser.add_argument("--host", type=str, default="127.0.0.1")
69
+ parser.add_argument("--port", type=int, default=5005)
70
+ parser.add_argument(
71
+ "--dev", action="store_true", help="Enable Flask Debug mode (watches for files modifications, etc..)"
72
+ )
73
+ parser.add_argument("fetchers", nargs="*", type=str)
74
+ args = parser.parse_args()
75
+ run_server(fetchers=get_fetchers(args.fetchers), host=args.host, port=args.port, debug=args.dev)
76
+ return 0