fixed inefficient file loading

This commit is contained in:
Jonas Weinz 2022-09-11 16:33:43 +02:00
parent ea43b2f086
commit 01e083addb
23 changed files with 653 additions and 42 deletions

13
build.sh Executable file
View File

@ -0,0 +1,13 @@
#!/bin/bash
rm -rf ./dist/
python setup.py bdist_wheel
wheel_files=( dist/*whl )
cd ./examples/
for example in *;
do pyscript_bootstrap_app update $example $example --pyscript_bootstrap_templates_wheel_url ../${wheel_files[0]}
done

View File

@ -0,0 +1,12 @@
{
"root_folder": "01_hello_world",
"title": "01_hello_world",
"packages": [],
"paths": [],
"pyscript_css_url": "https://pyscript.net/alpha/pyscript.css",
"pyscript_js_url": "https://pyscript.net/alpha/pyscript.min.js",
"pyscript_py_url": "https://pyscript.net/alpha/pyscript.py",
"bootstrap_css_url": "https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css",
"bootstrap_js_url": "https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js",
"pyscript_bootstrap_templates_wheel_url": "../dist/pyscript_bootstrap_templates-0.1.0-py3-none-any.whl"
}

View File

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en" style="width: 100%;height: 100%">
<head id="head">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="./resources/pyscript.css" />
<script defer src="./resources/pyscript.js"></script>
<!-- Bootstrap CSS -->
<link href="./resources/bootstrap.css" rel="stylesheet">
<title>01_hello_world</title>
<py-env>
- ./resources/pyscript_bootstrap_templates-0.1.0-py3-none-any.whl
- paths:
</py-env>
</head>
<body style="width: 100%; height: 100%">
<div id="pyscript_app" style="height: 100%; min-height: 100%"></div>
<py-script src="./main.py"></py-script>
<script src="./resources/bootstrap.js"></script>
</body>
</html>

View File

@ -0,0 +1,15 @@
from pyscript_bootstrap_templates import bootstrap_templates
from pyscript_bootstrap_templates import bootstrap_HTML as bHTML
from pyscript_bootstrap_templates import HTML as HTML
app = bootstrap_templates.PyScriptBootstrapDashboard(
parent_element="pyscript_app", brand_name="hello_world")
div = bHTML.BootstrapContainer("This is a sidebar", parent=app.sidebar)
div.font_size = 4
btn = bHTML.ButtonPrimary("Click me", parent=app.sidebar)
btn.w = 100
btn.onclick = lambda _: bHTML.AlertSuccess(
"You clicked me!", parent=app.main_area)

View File

@ -0,0 +1,15 @@
{
"root_folder": "02_image_filter",
"title": "02_image_filter",
"packages": [
"numpy",
"scikit-image"
],
"paths": null,
"pyscript_css_url": "https://pyscript.net/alpha/pyscript.css",
"pyscript_js_url": "https://pyscript.net/alpha/pyscript.min.js",
"pyscript_py_url": "https://pyscript.net/alpha/pyscript.py",
"bootstrap_css_url": "https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css",
"bootstrap_js_url": "https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js",
"pyscript_bootstrap_templates_wheel_url": "../dist/pyscript_bootstrap_templates-0.1.0-py3-none-any.whl"
}

View File

@ -11,12 +11,12 @@
<!-- Bootstrap CSS -->
<link href="./resources/bootstrap.css" rel="stylesheet">
<title>sobel_filter</title>
<title>02_image_filter</title>
<py-env>
- ./resources/pyscript_bootstrap_templates-0.1.0-py3-none-any.whl
- numpy
- scikit-image
- ./resources/pyscript_bootstrap_templates-0.1.0-py3-none-any.whl
</py-env>
</head>

View File

@ -1,5 +1,3 @@
from pyscript_bootstrap_templates import bootstrap_templates
from pyscript_bootstrap_templates import bootstrap_HTML as bHTML
from pyscript_bootstrap_templates import bootstrap_inputs as bInputs
@ -20,15 +18,10 @@ def sobel_image(img):
return ((255 * img_sobel) / img_sobel.max()).astype(np.uint8)
app = bootstrap_templates.PyScriptBootstrapDashboard(parent_element="pyscript_app", brand_name="Sobel Filter Example")
HTML.H3("upload an image:", parent=app.sidebar)
HTML.H3("filtered image:", parent=app.main_area)
app = bootstrap_templates.PyScriptBootstrapDashboard(parent_element="pyscript_app", brand_name="Image Filter Example")
image_input = bInputs.InputFile(label_text="choose image file", parent=app.sidebar)
image_input._input.set_attribute("accept", "image/*") # TODO: write wrapper for this
image_input.set_attribute("accept", "image/*")
def on_image_change(f, *args):
print("image changed")
@ -43,4 +36,3 @@ def on_image_change(f, *args):
output.shadow = bHTML.Shadow.LARGE
image_input.onchange = on_image_change

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,424 @@
import asyncio
import base64
import io
import sys
import time
from textwrap import dedent
import micropip # noqa: F401
from js import console, document
loop = asyncio.get_event_loop()
MIME_METHODS = {
"__repr__": "text/plain",
"_repr_html_": "text/html",
"_repr_markdown_": "text/markdown",
"_repr_svg_": "image/svg+xml",
"_repr_png_": "image/png",
"_repr_pdf_": "application/pdf",
"_repr_jpeg_": "image/jpeg",
"_repr_latex": "text/latex",
"_repr_json_": "application/json",
"_repr_javascript_": "application/javascript",
"savefig": "image/png",
}
def render_image(mime, value, meta):
data = f"data:{mime};charset=utf-8;base64,{value}"
attrs = " ".join(['{k}="{v}"' for k, v in meta.items()])
return f'<img src="{data}" {attrs}</img>'
def identity(value, meta):
return value
MIME_RENDERERS = {
"text/plain": identity,
"text/html": identity,
"image/png": lambda value, meta: render_image("image/png", value, meta),
"image/jpeg": lambda value, meta: render_image("image/jpeg", value, meta),
"image/svg+xml": identity,
"application/json": identity,
"application/javascript": lambda value, meta: f"<script>{value}</script>",
}
def eval_formatter(obj, print_method):
"""
Evaluates a formatter method.
"""
if print_method == "__repr__":
return repr(obj)
elif hasattr(obj, print_method):
if print_method == "savefig":
buf = io.BytesIO()
obj.savefig(buf, format="png")
buf.seek(0)
return base64.b64encode(buf.read()).decode("utf-8")
return getattr(obj, print_method)()
elif print_method == "_repr_mimebundle_":
return {}, {}
return None
def format_mime(obj):
"""
Formats object using _repr_x_ methods.
"""
if isinstance(obj, str):
return obj, "text/plain"
mimebundle = eval_formatter(obj, "_repr_mimebundle_")
if isinstance(mimebundle, tuple):
format_dict, _ = mimebundle
else:
format_dict = mimebundle
output, not_available = None, []
for method, mime_type in reversed(MIME_METHODS.items()):
if mime_type in format_dict:
output = format_dict[mime_type]
else:
output = eval_formatter(obj, method)
if output is None:
continue
elif mime_type not in MIME_RENDERERS:
not_available.append(mime_type)
continue
break
if output is None:
if not_available:
console.warning(
f"Rendered object requested unavailable MIME renderers: {not_available}"
)
output = repr(output)
mime_type = "text/plain"
elif isinstance(output, tuple):
output, meta = output
else:
meta = {}
return MIME_RENDERERS[mime_type](output, meta), mime_type
class PyScript:
loop = loop
@staticmethod
def write(element_id, value, append=False, exec_id=0):
"""Writes value to the element with id "element_id"""
console.log(f"APPENDING: {append} ==> {element_id} --> {value}")
if append:
child = document.createElement("div")
element = document.querySelector(f"#{element_id}")
if not element:
return
exec_id = exec_id or element.childElementCount + 1
element_id = child.id = f"{element_id}-{exec_id}"
element.appendChild(child)
element = document.getElementById(element_id)
html, mime_type = format_mime(value)
if mime_type in ("application/javascript", "text/html"):
script_element = document.createRange().createContextualFragment(html)
element.appendChild(script_element)
else:
element.innerHTML = html
@staticmethod
def run_until_complete(f):
_ = loop.run_until_complete(f)
class Element:
def __init__(self, element_id, element=None):
self._id = element_id
self._element = element
@property
def id(self):
return self._id
@property
def element(self):
"""Return the dom element"""
if not self._element:
self._element = document.querySelector(f"#{self._id}")
return self._element
@property
def value(self):
return self.element.value
@property
def innerHtml(self):
return self.element.innerHtml
def write(self, value, append=False):
console.log(f"Element.write: {value} --> {append}")
# TODO: it should be the opposite... pyscript.write should use the Element.write
# so we can consolidate on how we write depending on the element type
pyscript.write(self._id, value, append=append)
def clear(self):
if hasattr(self.element, "value"):
self.element.value = ""
else:
self.write("", append=False)
def select(self, query, from_content=False):
el = self.element
if from_content:
el = el.content
_el = el.querySelector(query)
if _el:
return Element(_el.id, _el)
else:
console.log(f"WARNING: can't find element matching query {query}")
def clone(self, new_id=None, to=None):
if new_id is None:
new_id = self.element.id
clone = self.element.cloneNode(True)
clone.id = new_id
if to:
to.element.appendChild(clone)
# Inject it into the DOM
self.element.after(clone)
return Element(clone.id, clone)
def remove_class(self, classname):
if isinstance(classname, list):
for cl in classname:
self.remove_class(cl)
else:
self.element.classList.remove(classname)
def add_class(self, classname):
self.element.classList.add(classname)
def add_classes(element, class_list):
for klass in class_list.split(" "):
element.classList.add(klass)
def create(what, id_=None, classes=""):
element = document.createElement(what)
if id_:
element.id = id_
add_classes(element, classes)
return Element(id_, element)
class PyWidgetTheme:
def __init__(self, main_style_classes):
self.main_style_classes = main_style_classes
def theme_it(self, widget):
for klass in self.main_style_classes.split(" "):
widget.classList.add(klass)
class PyItemTemplate(Element):
label_fields = None
def __init__(self, data, labels=None, state_key=None, parent=None):
self.data = data
self.register_parent(parent)
if not labels:
labels = list(self.data.keys())
self.labels = labels
self.state_key = state_key
super().__init__(self._id)
def register_parent(self, parent):
self._parent = parent
if parent:
self._id = f"{self._parent._id}-c-{len(self._parent._children)}"
self.data["id"] = self._id
else:
self._id = None
def create(self):
console.log("creating section")
new_child = create("section", self._id, "task bg-white my-1")
console.log("creating values")
console.log("creating innerHtml")
new_child._element.innerHTML = dedent(
f"""
<label for="flex items-center p-2 ">
<input class="mr-2" type="checkbox" class="task-check">
<p class="m-0 inline">{self.render_content()}</p>
</label>
"""
)
console.log("returning")
return new_child
def on_click(self, evt):
pass
def pre_append(self):
pass
def post_append(self):
self.element.click = self.on_click
self.element.onclick = self.on_click
self._post_append()
def _post_append(self):
pass
def strike(self, value, extra=None):
if value:
self.add_class("line-through")
else:
self.remove_class("line-through")
def render_content(self):
return " - ".join([self.data[f] for f in self.labels])
class PyListTemplate:
theme = PyWidgetTheme("flex flex-col-reverse mt-8 mx-8")
item_class = PyItemTemplate
def __init__(self, parent):
self.parent = parent
self._children = []
self._id = self.parent.id
@property
def children(self):
return self._children
@property
def data(self):
return [c.data for c in self._children]
def render_children(self):
binds = {}
for i, c in enumerate(self._children):
txt = c.element.innerHTML
rnd = str(time.time()).replace(".", "")[-5:]
new_id = f"{c.element.id}-{i}-{rnd}"
binds[new_id] = c.element.id
txt = txt.replace(">", f" id='{new_id}'>")
print(txt)
def foo(evt):
console.log(evt)
evtEl = evt.srcElement
srcEl = Element(binds[evtEl.id])
srcEl.element.onclick()
evtEl.classList = srcEl.element.classList
for new_id in binds:
Element(new_id).element.onclick = foo
def connect(self):
self.md = main_div = document.createElement("div")
main_div.id = self._id + "-list-tasks-container"
if self.theme:
self.theme.theme_it(main_div)
self.parent.appendChild(main_div)
def add(self, *args, **kws):
if not isinstance(args[0], self.item_class):
child = self.item_class(*args, **kws)
else:
child = args[0]
child.register_parent(self)
return self._add(child)
def _add(self, child_elem):
console.log("appending child", child_elem.element)
self.pre_child_append(child_elem)
child_elem.pre_append()
self._children.append(child_elem)
self.md.appendChild(child_elem.create().element)
child_elem.post_append()
self.child_appended(child_elem)
return child_elem
def pre_child_append(self, child):
pass
def child_appended(self, child):
"""Overwrite me to define logic"""
pass
class OutputCtxManager:
def __init__(self, out=None, output_to_console=True, append=True):
self._out = out
self._prev = out
self.output_to_console = output_to_console
self._append = append
def change(self, out=None, err=None, output_to_console=True, append=True):
self._prev = self._out
self._out = out
self.output_to_console = output_to_console
self._append = append
console.log("----> changed out to", self._out, self._append)
def revert(self):
console.log("----> reverted")
self._out = self._prev
def write(self, txt):
console.log("writing to", self._out, txt, self._append)
if self._out:
pyscript.write(self._out, txt, append=self._append)
if self.output_to_console:
console.log(self._out, txt)
class OutputManager:
def __init__(self, out=None, err=None, output_to_console=True, append=True):
sys.stdout = self._out_manager = OutputCtxManager(
out, output_to_console, append
)
sys.stderr = self._err_manager = OutputCtxManager(
err, output_to_console, append
)
self.output_to_console = output_to_console
self._append = append
def change(self, out=None, err=None, output_to_console=True, append=True):
self._out_manager.change(out, output_to_console, append)
sys.stdout = self._out_manager
self._err_manager.change(err, output_to_console, append)
sys.stderr = self._err_manager
self.output_to_console = output_to_console
self.append = append
def revert(self):
self._out_manager.revert()
self._err_manager.revert()
sys.stdout = self._out_manager
sys.stderr = self._err_manager
console.log("----> reverted")
pyscript = PyScript()
output_manager = OutputManager()

View File

@ -2,7 +2,7 @@ import datetime as dt
import uuid
from .bootstrap_HTML import *
from js import document, FileReader, btoa # type: ignore
from js import document, FileReader, btoa, Uint8Array # type: ignore
from pyodide import create_proxy # type: ignore
import io
import base64
@ -323,39 +323,39 @@ class InputFile(InputElement):
def _load_file(self, *args):
def read_file(event, name):
buffer = io.BytesIO()
async def read_file(event, f):
# FIXME: way too much decoding and encoding between formats here
# but bytearray conversation between js and python drove me crazy!!!
# FIXME: i think i am doing things too complicated here,
# but my understanding of loading raw bytes from file
# inputs in javascript (and their pyodide interacions) is very much limited
name = f.name
f = btoa(event.target.result)
buffer.write(base64.decodebytes(bytes(f, "utf-8")))
print(btoa(event.target.result))
print("loaded ", name)
uint8_array = Uint8Array.new(await f.arrayBuffer())
buffer = io.BytesIO(bytearray(uint8_array))
self._files[name] = buffer
buffer.seek(0)
buffer.name = name # not standardized, but works here
buffer.name = name # not standardized, but works here # TODO: create base class for that purpose
if self._on_file_change is not None:
self._on_file_change(buffer)
fileList = self._input.element.files
file_list = self._input.element.files.to_py()
self._files = {}
for f in fileList:
for f in file_list:
# reader is a pyodide.JsProxy
reader = FileReader.new()
# Create a Python proxy for the callback function
onload_event = create_proxy(lambda event, name=f.name: read_file(event, name))
onload_event = create_proxy(lambda event, f=f: read_file(event, f))
reader.onload = onload_event
reader.readAsBinaryString(f)
return
@property

View File

@ -1,18 +1,24 @@
from distutils.command.config import config
from multiprocessing.sharedctypes import Value
import pathlib
from typing import List
import argparse
from numpy import isin
import requests
import json
import shutil
def download_file(url: str, path: pathlib.Path):
response = requests.get(url)
path.write_bytes(response.content)
# create a new pyscript project
if pathlib.Path(url).exists():
shutil.copy2(url, str(path))
else:
response = requests.get(url)
path.write_bytes(response.content)
def create_project(root_folder: pathlib.Path,
def generate_project_files(root_folder: pathlib.Path,
title: str,
packages: List[str],
paths: List[str] = None,
@ -78,11 +84,10 @@ btn.onclick = lambda _: bHTML.AlertSuccess(
"You clicked me!", parent=app.main_area)
"""
if root_folder.exists():
raise("Value Error: root_folder already exists. Please choose a different name.")
root_folder=pathlib.Path(root_folder)
root_folder.mkdir(parents=True)
(root_folder / "resources").mkdir(parents=True)
root_folder.mkdir(parents=True, exist_ok=True)
(root_folder / "resources").mkdir(parents=True, exist_ok=True)
# Download files:
download_file(pyscript_css_url, root_folder / "resources" / "pyscript.css")
@ -94,20 +99,73 @@ btn.onclick = lambda _: bHTML.AlertSuccess(
download_file(pyscript_bootstrap_templates_wheel_url,
root_folder / "resources" / pyscript_bootstrap_templates_wheel_url.split("/")[-1])
(root_folder / "index.html").write_text(html, encoding="utf-8")
(root_folder / "main.py").write_text(py, encoding="utf-8")
index_html = (root_folder / "index.html")
main_py = (root_folder / "main.py")
index_html.write_text(html, encoding="utf-8")
# only create if not existing:
if not main_py.exists():
main_py.write_text(py, encoding="utf-8")
def create_project(**kwargs):
root_folder: pathlib.Path = kwargs['root_folder']
if root_folder.exists():
raise ValueError(f"cannot create project. Folder {str(root_folder)} already exists")
root_folder.mkdir(parents=True, exist_ok=True)
# create initial config.json
kwargs['root_folder'] = str(root_folder)
with open(root_folder / "config.json", 'w') as f:
json.dump(kwargs, f, indent=4)
generate_project_files(**kwargs)
def update_project(**kwargs):
root_folder: pathlib.Path = kwargs['root_folder']
if not root_folder.exists():
raise ValueError(f"cannot update project in {str(root_folder)}. Path does not exist")
# load config file
config_json = root_folder / "config.json"
if not config_json.exists():
raise ValueError(f"cannot update project in {str(root_folder)}. Found no config.json inside give path")
with open(config_json, "r") as f:
config = json.load(f)
# override config values that are not None or empty lists
for arg, val in kwargs.items():
if val is not None:
if not isinstance(val, list) or len(val) > 0:
config[arg] = val
generate_project_files(**config)
config['root_folder'] = str(root_folder)
with open(root_folder / "config.json", 'w') as f:
json.dump(config, f, indent=4)
def main():
argument_parser = argparse.ArgumentParser(
description="create a new pyscript project")
argument_parser.add_argument("command", choices=["create","update"])
argument_parser.add_argument(
"root_folder", type=pathlib.Path, help="the root folder of the new project")
argument_parser.add_argument(
"title", type=str, help="the title of the new project")
argument_parser.add_argument(
"packages", type=str, nargs="*", help="the packages to include in the new project")
"--packages", type=str, nargs="+", help="the packages to include in the new project")
argument_parser.add_argument("--paths", type=str, nargs="+",
help="additional local python files to include in the new project")
argument_parser.add_argument("--pyscript_css_url", type=str,
@ -125,7 +183,28 @@ def main():
args = argument_parser.parse_args()
create_project(args.root_folder, args.title, args.packages, args.paths, args.pyscript_css_url, args.pyscript_js_url, args.pyscript_py_url, args.bootstrap_css_url, args.bootstrap_js_url, args.pyscript_bootstrap_templates_wheel_url)
config = {
'root_folder': args.root_folder,
'title': args.title,
'packages': args.packages if args.packages is not None else [],
'paths': args.paths if args.paths is not None else [],
'pyscript_css_url': args.pyscript_css_url,
'pyscript_js_url': args.pyscript_js_url,
'pyscript_py_url': args.pyscript_py_url,
'bootstrap_css_url': args.bootstrap_css_url,
'bootstrap_js_url': args.bootstrap_js_url,
'pyscript_bootstrap_templates_wheel_url': args.pyscript_bootstrap_templates_wheel_url
}
if args.command == "create":
create_project(**config)
elif args.command == "update":
update_project(**config)
else:
# should never happen and be catched by argparse#
raise ValueError("unknown command", args.command)
if __name__ == "__main__":
main()

View File

@ -47,7 +47,7 @@ setup(
entry_points={
"console_scripts": [
"create_pyscript_bootstrap_app=pyscript_bootstrap_templates.create:main",
"pyscript_bootstrap_app=pyscript_bootstrap_templates.cli_interface:main",
],
},