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/__init__.py +38 -0
- hiplot/__main__.py +10 -0
- hiplot/compress.py +70 -0
- hiplot/csvvalues.py +83 -0
- hiplot/experiment.py +637 -0
- hiplot/fetchers.py +322 -0
- hiplot/fetchers_demo.py +459 -0
- hiplot/ipython.py +217 -0
- hiplot/pkginfo.py +15 -0
- hiplot/py.typed +0 -0
- hiplot/render.py +141 -0
- hiplot/server.py +76 -0
- hiplot/static/built/hiplot.bundle.js +1224 -0
- hiplot/static/built/streamlit_component/hiplot_streamlit.bundle.js +1220 -0
- hiplot/static/built/streamlit_component/index.html +16 -0
- hiplot/static/icon.png +0 -0
- hiplot/streamlit_helpers.py +127 -0
- hiplot/templates/index.html +68 -0
- hiplot_plus-0.1.0.dist-info/METADATA +197 -0
- hiplot_plus-0.1.0.dist-info/RECORD +24 -0
- hiplot_plus-0.1.0.dist-info/WHEEL +4 -0
- hiplot_plus-0.1.0.dist-info/entry_points.txt +4 -0
- hiplot_plus-0.1.0.dist-info/licenses/LICENSE +21 -0
- hiplot_plus-0.1.0.dist-info/licenses/NOTICE +207 -0
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
|