Compare commits
17 Commits
Author | SHA1 | Date | |
---|---|---|---|
75efac61b9 | |||
dbea51a2b0 | |||
1eecb7c0a3 | |||
f188cff5b4 | |||
4257a75557 | |||
91c6272f9d | |||
97d8e85523 | |||
cf144e5b28 | |||
95c4319340 | |||
c51c1ba64a | |||
c236959c01 | |||
60e9678b8b | |||
4ca2cdff70 | |||
3dc051aba1 | |||
b3d1804a2e | |||
01eec041b5 | |||
c0bb89c752 |
1
MANIFEST.IN
Normal file
1
MANIFEST.IN
Normal file
@ -0,0 +1 @@
|
||||
include pyscript_bootstrap_templates/data/*.png
|
94
README.md
94
README.md
@ -1,3 +1,95 @@
|
||||
# pyscript-bootstrap-templates
|
||||
|
||||
templates and pyscript wrappers for various bootstrap properties
|
||||
|
||||
This package provides tools to write single page Dashboard apps with python that run entirely client-side in the Browser.
|
||||
To do this, it uses [pyscript](https://pyscript.net/) and builds python wrapper classes for various [bootstrap](https://getbootstrap.com/) elements.
|
||||
|
||||

|
||||
|
||||
* examples (Code is in [examples](./examples/)):
|
||||
* [Hello World](https://antielektron.github.io/pyscript_bootstrap_templates/examples/01_hello_world)
|
||||
* [opencv image filter](https://antielektron.github.io/pyscript_bootstrap_templates/examples/02_image_filter)
|
||||
* [numpy grid demo](https://antielektron.github.io/pyscript_bootstrap_templates/examples/03_numpy_grid_demo)
|
||||
* [cell colony detector using opencv](https://antielektron.github.io/pyscript_bootstrap_templates/examples/04_cell_detector)
|
||||
|
||||
## installation
|
||||
|
||||
```bash
|
||||
pip install git+https://github.com/antielektron/pyscript_bootstrap_templates.git
|
||||
```
|
||||
|
||||
## usage
|
||||
|
||||
|
||||
### create a project
|
||||
to create a new project, run
|
||||
|
||||
```bash
|
||||
pyscript_bootstrap_app create <project_name> <title> --packages PACKAGES [PACKAGES ...]
|
||||
```
|
||||
|
||||
* this will create a new folder containing a PWA app skeleton to start with.
|
||||
The main entry point should be in main.py
|
||||
|
||||
* this will also create all necessary files such that this app can operate as an standalone PWA application.
|
||||
|
||||
* the `packages` parameter is optional, you can specify on which PyPi packages your code depends
|
||||
* **NOTE**: since pyscript's python implementation depends on WASM and it's limited in what it can do, not pure python packages may not work. Here is a list of supported packages: https://pyodide.org/en/stable/usage/packages-in-pyodide.html
|
||||
|
||||
### update a project
|
||||
Since it's a pwa, browsers will cache all content of the app for offline usage. To bump the version of the PWA and trigger a redownload after the project has changed,
|
||||
simply run
|
||||
|
||||
```bash
|
||||
pyscript_bootstrap_app update <project_name> <title>
|
||||
```
|
||||
|
||||
### serving a project for development
|
||||
|
||||
you can use python's builtin webserver to serve the files locally for testing. Just run
|
||||
|
||||
```bash
|
||||
python -m http.server 1111
|
||||
```
|
||||
|
||||
inside your project folder and navigate to http://localhost:1111 in your browser
|
||||
|
||||
### advanced usage
|
||||
There are a few more options you can pass to `pyscript_bootstrap_app` (e.g. the pyscript version that is used). Here is the full list:
|
||||
|
||||
```bash
|
||||
usage: pyscript_bootstrap_app [-h] [--packages PACKAGES [PACKAGES ...]] [--paths PATHS [PATHS ...]] [--pyscript-css-url PYSCRIPT_CSS_URL] [--pyscript-js-url PYSCRIPT_JS_URL]
|
||||
[--pyscript-py-url PYSCRIPT_PY_URL] [--bootstrap-css-url BOOTSTRAP_CSS_URL] [--bootstrap-js-url BOOTSTRAP_JS_URL]
|
||||
[--pyscript-bootstrap-templates-wheel-url PYSCRIPT_BOOTSTRAP_TEMPLATES_WHEEL_URL] [--pwa-bg-color PWA_BG_COLOR] [--pwa-theme-color PWA_THEME_COLOR]
|
||||
{create,update} root_folder title
|
||||
|
||||
create a new pyscript project
|
||||
|
||||
positional arguments:
|
||||
{create,update}
|
||||
root_folder the root folder of the new project
|
||||
title the title of the new project
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--packages PACKAGES [PACKAGES ...]
|
||||
the packages to include in the new project
|
||||
--paths PATHS [PATHS ...]
|
||||
additional local python files to include in the new project
|
||||
--pyscript-css-url PYSCRIPT_CSS_URL
|
||||
the url of the pyscript css file
|
||||
--pyscript-js-url PYSCRIPT_JS_URL
|
||||
the url of the pyscript js file
|
||||
--pyscript-py-url PYSCRIPT_PY_URL
|
||||
the url of the pyscript py file
|
||||
--bootstrap-css-url BOOTSTRAP_CSS_URL
|
||||
the url of the bootstrap css file
|
||||
--bootstrap-js-url BOOTSTRAP_JS_URL
|
||||
the url of the bootstrap js file
|
||||
--pyscript-bootstrap-templates-wheel-url PYSCRIPT_BOOTSTRAP_TEMPLATES_WHEEL_URL
|
||||
the url of the pyscript bootstrap templates wheel file
|
||||
--pwa-bg-color PWA_BG_COLOR
|
||||
background color for pwa configuration
|
||||
--pwa-theme-color PWA_THEME_COLOR
|
||||
theme color for pwa configuration
|
||||
```
|
||||
|
4
build.sh
4
build.sh
@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
rm -rf ./dist/
|
||||
#rm -rf ./dist/
|
||||
python setup.py bdist_wheel
|
||||
|
||||
wheel_files=( dist/*whl )
|
||||
@ -9,5 +9,5 @@ wheel_files=( dist/*whl )
|
||||
cd ./examples/
|
||||
|
||||
for example in *;
|
||||
do pyscript_bootstrap_app update $example $example --pyscript_bootstrap_templates_wheel_url ../${wheel_files[0]}
|
||||
do pyscript_bootstrap_app update $example $example --pyscript-bootstrap-templates-wheel-url ../${wheel_files[0]}
|
||||
done
|
||||
|
Binary file not shown.
BIN
dist/pyscript_bootstrap_templates-0.2.0-py3-none-any.whl
vendored
Normal file
BIN
dist/pyscript_bootstrap_templates-0.2.0-py3-none-any.whl
vendored
Normal file
Binary file not shown.
@ -3,10 +3,12 @@
|
||||
"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",
|
||||
"pyscript_css_url": "https://pyscript.net/latest/pyscript.css",
|
||||
"pyscript_js_url": "https://pyscript.net/latest/pyscript.js",
|
||||
"pyscript_py_url": "https://pyscript.net/latest/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"
|
||||
"pyscript_bootstrap_templates_wheel_url": "../dist/pyscript_bootstrap_templates-0.2.0-py3-none-any.whl",
|
||||
"pwa_bg_color": "#000000",
|
||||
"pwa_theme_color": "#ffffff"
|
||||
}
|
@ -5,21 +5,29 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
|
||||
<script src="./resources/pwa.js"></script>
|
||||
|
||||
<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%">
|
||||
<py-config type="json">
|
||||
{
|
||||
"splashscreen": {
|
||||
"autoclose": true
|
||||
},
|
||||
"packages": [
|
||||
"./resources/pyscript_bootstrap_templates-0.2.0-py3-none-any.whl"
|
||||
],
|
||||
"paths": []
|
||||
}
|
||||
</py-config>
|
||||
<div id="pyscript_app" style="height: 100%; min-height: 100%"></div>
|
||||
<py-script src="./main.py"></py-script>
|
||||
|
||||
|
@ -5,11 +5,27 @@ 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 = bHTML.Div("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)
|
||||
|
||||
toast_container = bHTML.ToastContainer(parent=app.main)
|
||||
toast_container.position_bottom = 0
|
||||
toast_container.position_end = 0
|
||||
|
||||
|
||||
|
||||
def onclick(_):
|
||||
# NOTE: you can also use app.toast(...) and app.alert_success(...) to show toasts
|
||||
# and alerts at the default bottom right position
|
||||
alert = bHTML.AlertSuccess("You clicked me!", parent=app.main)
|
||||
alert.w = 25
|
||||
toast = bHTML.Toast("You clicked me!", parent=toast_container)
|
||||
toast.animation = True
|
||||
toast.show()
|
||||
|
||||
btn.onclick = onclick
|
||||
|
1
examples/01_hello_world/manifest.json
Normal file
1
examples/01_hello_world/manifest.json
Normal file
@ -0,0 +1 @@
|
||||
{"name": "01_hello_world", "short_name": "01_hello_world", "start_url": "./index.html", "scope": ".", "display": "standalone", "background_color": "#000000", "theme_color": "#ffffff", "icons": [{"src": "resources/icon-512x512.png", "type": "image/png", "sizes": "512x512"}]}
|
BIN
examples/01_hello_world/resources/icon-512x512.png
Normal file
BIN
examples/01_hello_world/resources/icon-512x512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
10
examples/01_hello_world/resources/pwa.js
Normal file
10
examples/01_hello_world/resources/pwa.js
Normal file
@ -0,0 +1,10 @@
|
||||
|
||||
if ("serviceWorker" in navigator) {
|
||||
window.addEventListener("load", function () {
|
||||
navigator.serviceWorker
|
||||
.register("./serviceWorker.js")
|
||||
.then(res => console.log("service worker registered", res))
|
||||
.catch(err => console.log("service worker not registered", err))
|
||||
})
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
62
examples/01_hello_world/serviceWorker.js
Normal file
62
examples/01_hello_world/serviceWorker.js
Normal file
@ -0,0 +1,62 @@
|
||||
|
||||
const pwa_version = "01_hello_world_202306111641"
|
||||
const assets = ["./index.html",
|
||||
"./main.py",
|
||||
"./resources/bootstrap.css",
|
||||
"./resources/bootstrap.js",
|
||||
"./resources/pyscript.css",
|
||||
"./resources/pyscript.js",
|
||||
"./resources/pyscript.py",
|
||||
"./resources/pyscript_bootstrap_templates-0.2.0-py3-none-any.whl",
|
||||
"./resources/pwa.js",
|
||||
"./site.js",
|
||||
]
|
||||
self.addEventListener("install", installEvent => {
|
||||
installEvent.waitUntil(
|
||||
caches.open(pwa_version).then(cache => {
|
||||
cache.addAll(assets).then(r => {
|
||||
console.log("Cache assets downloaded");
|
||||
}).catch(err => console.log("Error caching item", err))
|
||||
console.log(`Cache ${pwa_version} opened.`);
|
||||
}).catch(err => console.log("Error opening cache", err))
|
||||
)
|
||||
})
|
||||
|
||||
self.addEventListener('activate', e => {
|
||||
console.log('Service Worker: Activated');
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', event => {
|
||||
if (event.request.method != 'GET')
|
||||
return;
|
||||
event.respondWith((async () => {
|
||||
const cachedResponse = await caches.match(event.request);
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
const response = await fetch(event.request);
|
||||
|
||||
if (!response || response.status !== 200 || response.type !== 'basic') {
|
||||
return response;
|
||||
}
|
||||
|
||||
const responseToCache = response.clone();
|
||||
const cache = await caches.open(pwa_version)
|
||||
await cache.put(event.request, response.clone());
|
||||
|
||||
return response;
|
||||
})());
|
||||
});
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys().then((keyList) => {
|
||||
return Promise.all(keyList.map((key) => {
|
||||
if(key !== pwa_version) {
|
||||
return caches.delete(key);
|
||||
}
|
||||
}));
|
||||
})
|
||||
);
|
||||
});
|
||||
|
2
examples/01_hello_world/site.js
Normal file
2
examples/01_hello_world/site.js
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
|
@ -3,13 +3,15 @@
|
||||
"title": "02_image_filter",
|
||||
"packages": [
|
||||
"numpy",
|
||||
"scikit-image"
|
||||
"opencv-python"
|
||||
],
|
||||
"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",
|
||||
"pyscript_css_url": "https://pyscript.net/latest/pyscript.css",
|
||||
"pyscript_js_url": "https://pyscript.net/latest/pyscript.js",
|
||||
"pyscript_py_url": "https://pyscript.net/latest/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"
|
||||
"pyscript_bootstrap_templates_wheel_url": "../dist/pyscript_bootstrap_templates-0.2.0-py3-none-any.whl",
|
||||
"pwa_bg_color": "#000000",
|
||||
"pwa_theme_color": "#ffffff"
|
||||
}
|
@ -5,22 +5,31 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
|
||||
<script src="./resources/pwa.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="./resources/pyscript.css" />
|
||||
<script defer src="./resources/pyscript.js"></script>
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="./resources/bootstrap.css" rel="stylesheet">
|
||||
|
||||
<title>02_image_filter</title>
|
||||
|
||||
<py-env>
|
||||
- ./resources/pyscript_bootstrap_templates-0.1.0-py3-none-any.whl
|
||||
- numpy
|
||||
- scikit-image
|
||||
|
||||
</py-env>
|
||||
</head>
|
||||
<body style="width: 100%; height: 100%">
|
||||
<py-config type="json">
|
||||
{
|
||||
"splashscreen": {
|
||||
"autoclose": true
|
||||
},
|
||||
"packages": [
|
||||
"./resources/pyscript_bootstrap_templates-0.2.0-py3-none-any.whl",
|
||||
"numpy",
|
||||
"opencv-python"
|
||||
],
|
||||
"paths": []
|
||||
}
|
||||
</py-config>
|
||||
<div id="pyscript_app" style="height: 100%; min-height: 100%"></div>
|
||||
<py-script src="./main.py"></py-script>
|
||||
|
||||
|
@ -7,32 +7,88 @@ from pyscript_bootstrap_templates import HTML as HTML
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
from io import BytesIO
|
||||
from skimage import filters
|
||||
import cv2
|
||||
|
||||
# create a sobel image with skimage
|
||||
# create a sobel image with OpenCV
|
||||
def sobel_image(img):
|
||||
if len(img.shape) == 3:
|
||||
img = img[:,:,0] + img[:,:,1] + img[:,:,2]
|
||||
img = img / 3
|
||||
img_sobel = filters.sobel(img)
|
||||
return ((255 * img_sobel) / img_sobel.max()).astype(np.uint8)
|
||||
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
||||
grad_x = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize=3)
|
||||
grad_y = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize=3)
|
||||
img_sobel = cv2.sqrt(cv2.addWeighted(cv2.pow(grad_x, 2.0), 0.5, cv2.pow(grad_y, 2.0), 0.5, 0))
|
||||
img_sobel = cv2.normalize(img_sobel, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U)
|
||||
return img_sobel
|
||||
|
||||
# create a gaussian blur image with OpenCV
|
||||
def gaussian_image(img):
|
||||
return cv2.GaussianBlur(img, (51, 51), 0)
|
||||
|
||||
# create a canny edge image with OpenCV
|
||||
def canny_image(img):
|
||||
return cv2.Canny(img, 50, 100)
|
||||
|
||||
# convert to grayscale
|
||||
def grayscale_image(img):
|
||||
return cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
||||
|
||||
|
||||
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.set_attribute("accept", "image/*")
|
||||
filter_map = {
|
||||
"sobel": sobel_image,
|
||||
"gaussian": gaussian_image,
|
||||
"canny": canny_image,
|
||||
"grayscale": grayscale_image
|
||||
}
|
||||
|
||||
filter_selection = bInputs.InputSelect(label_text="filter", parent=app.sidebar, options=list(filter_map.keys()) )
|
||||
|
||||
image_input = bInputs.InputFile(label_text="choose image file", parent=app.sidebar)
|
||||
image_input._input.set_attribute("accept", "image/*")
|
||||
|
||||
process_button = bInputs.ButtonPrimary("process", parent=app.sidebar)
|
||||
process_button.width = "100%"
|
||||
process_button.m = 2
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def on_process(*args, **kwargs):
|
||||
|
||||
for child in app.main.children.copy():
|
||||
app.main.remove_child(child)
|
||||
child.destroy()
|
||||
|
||||
file_dictionary = image_input.value
|
||||
if len(file_dictionary) == 0:
|
||||
app.alert_danger(message="no image selected")
|
||||
return
|
||||
|
||||
# get first file
|
||||
name, f = list(file_dictionary.items())[0]
|
||||
|
||||
def on_image_change(f, *args):
|
||||
print("image changed")
|
||||
print(f)
|
||||
f.seek(0)
|
||||
img = Image.open(f)
|
||||
img = sobel_image(np.array(img))
|
||||
img = np.array(img)
|
||||
|
||||
output = bHTML.Image.from_numpy_array(img, parent=app.main_area)
|
||||
filter_name = filter_selection.value
|
||||
|
||||
img = filter_map[filter_name](img)
|
||||
|
||||
bHTML.HTML.H3(f"{name} ({img.shape[0]}x{img.shape[1]}):", parent=app.main)
|
||||
|
||||
output = bHTML.Image.from_numpy_array(img, parent=app.main)
|
||||
output.rounded = True
|
||||
output.rounded_size = 10
|
||||
output.width = "100%"
|
||||
output.shadow = bHTML.Shadow.LARGE
|
||||
|
||||
image_input.onchange = on_image_change
|
||||
|
||||
app.toast(message="processing done", title="Info")
|
||||
|
||||
|
||||
process_button.onclick = on_process
|
||||
|
||||
# alternatively, you can register the callback function with the file input element:
|
||||
#image_input.onchange = on_image_change
|
||||
|
1
examples/02_image_filter/manifest.json
Normal file
1
examples/02_image_filter/manifest.json
Normal file
@ -0,0 +1 @@
|
||||
{"name": "02_image_filter", "short_name": "02_image_filter", "start_url": "./index.html", "scope": ".", "display": "standalone", "background_color": "#000000", "theme_color": "#ffffff", "icons": [{"src": "resources/icon-512x512.png", "type": "image/png", "sizes": "512x512"}]}
|
BIN
examples/02_image_filter/resources/icon-512x512.png
Normal file
BIN
examples/02_image_filter/resources/icon-512x512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
10
examples/02_image_filter/resources/pwa.js
Normal file
10
examples/02_image_filter/resources/pwa.js
Normal file
@ -0,0 +1,10 @@
|
||||
|
||||
if ("serviceWorker" in navigator) {
|
||||
window.addEventListener("load", function () {
|
||||
navigator.serviceWorker
|
||||
.register("./serviceWorker.js")
|
||||
.then(res => console.log("service worker registered", res))
|
||||
.catch(err => console.log("service worker not registered", err))
|
||||
})
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
62
examples/02_image_filter/serviceWorker.js
Normal file
62
examples/02_image_filter/serviceWorker.js
Normal file
@ -0,0 +1,62 @@
|
||||
|
||||
const pwa_version = "02_image_filter_202306111641"
|
||||
const assets = ["./index.html",
|
||||
"./main.py",
|
||||
"./resources/bootstrap.css",
|
||||
"./resources/bootstrap.js",
|
||||
"./resources/pyscript.css",
|
||||
"./resources/pyscript.js",
|
||||
"./resources/pyscript.py",
|
||||
"./resources/pyscript_bootstrap_templates-0.2.0-py3-none-any.whl",
|
||||
"./resources/pwa.js",
|
||||
"./site.js",
|
||||
]
|
||||
self.addEventListener("install", installEvent => {
|
||||
installEvent.waitUntil(
|
||||
caches.open(pwa_version).then(cache => {
|
||||
cache.addAll(assets).then(r => {
|
||||
console.log("Cache assets downloaded");
|
||||
}).catch(err => console.log("Error caching item", err))
|
||||
console.log(`Cache ${pwa_version} opened.`);
|
||||
}).catch(err => console.log("Error opening cache", err))
|
||||
)
|
||||
})
|
||||
|
||||
self.addEventListener('activate', e => {
|
||||
console.log('Service Worker: Activated');
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', event => {
|
||||
if (event.request.method != 'GET')
|
||||
return;
|
||||
event.respondWith((async () => {
|
||||
const cachedResponse = await caches.match(event.request);
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
const response = await fetch(event.request);
|
||||
|
||||
if (!response || response.status !== 200 || response.type !== 'basic') {
|
||||
return response;
|
||||
}
|
||||
|
||||
const responseToCache = response.clone();
|
||||
const cache = await caches.open(pwa_version)
|
||||
await cache.put(event.request, response.clone());
|
||||
|
||||
return response;
|
||||
})());
|
||||
});
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys().then((keyList) => {
|
||||
return Promise.all(keyList.map((key) => {
|
||||
if(key !== pwa_version) {
|
||||
return caches.delete(key);
|
||||
}
|
||||
}));
|
||||
})
|
||||
);
|
||||
});
|
||||
|
2
examples/02_image_filter/site.js
Normal file
2
examples/02_image_filter/site.js
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
|
16
examples/03_numpy_grid_demo/config.json
Normal file
16
examples/03_numpy_grid_demo/config.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"root_folder": "03_numpy_grid_demo",
|
||||
"title": "03_numpy_grid_demo",
|
||||
"packages": [
|
||||
"numpy"
|
||||
],
|
||||
"paths": [],
|
||||
"pyscript_css_url": "https://pyscript.net/latest/pyscript.css",
|
||||
"pyscript_js_url": "https://pyscript.net/latest/pyscript.js",
|
||||
"pyscript_py_url": "https://pyscript.net/latest/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.2.0-py3-none-any.whl",
|
||||
"pwa_bg_color": "#000000",
|
||||
"pwa_theme_color": "#ffffff"
|
||||
}
|
38
examples/03_numpy_grid_demo/index.html
Normal file
38
examples/03_numpy_grid_demo/index.html
Normal file
@ -0,0 +1,38 @@
|
||||
|
||||
<!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="manifest" href="manifest.json" />
|
||||
|
||||
<script src="./resources/pwa.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="./resources/pyscript.css" />
|
||||
<script defer src="./resources/pyscript.js"></script>
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="./resources/bootstrap.css" rel="stylesheet">
|
||||
|
||||
<title>03_numpy_grid_demo</title>
|
||||
</head>
|
||||
<body style="width: 100%; height: 100%">
|
||||
<py-config type="json">
|
||||
{
|
||||
"splashscreen": {
|
||||
"autoclose": true
|
||||
},
|
||||
"packages": [
|
||||
"./resources/pyscript_bootstrap_templates-0.2.0-py3-none-any.whl",
|
||||
"numpy"
|
||||
],
|
||||
"paths": []
|
||||
}
|
||||
</py-config>
|
||||
<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>
|
||||
|
114
examples/03_numpy_grid_demo/main.py
Normal file
114
examples/03_numpy_grid_demo/main.py
Normal file
@ -0,0 +1,114 @@
|
||||
|
||||
from pyscript_bootstrap_templates import bootstrap_templates
|
||||
from pyscript_bootstrap_templates import bootstrap_HTML as bHTML
|
||||
from pyscript_bootstrap_templates import HTML as HTML
|
||||
from pyscript_bootstrap_templates import bootstrap_inputs as bInputs
|
||||
|
||||
import numpy as np
|
||||
|
||||
# wrapper class to create editable numpy matrices:
|
||||
class NumpyGrid2D(HTML.Table, bHTML.BootstrapContainer):
|
||||
def __init__(self, numpy_grid: np.ndarray, parent: HTML.Element = None):
|
||||
|
||||
|
||||
# store numpy grid
|
||||
self._numpy_grid = numpy_grid.copy()
|
||||
m,n = self._numpy_grid.shape
|
||||
|
||||
super().__init__(parent=parent)
|
||||
|
||||
self.m = 0
|
||||
self.p = 0
|
||||
|
||||
# create a grid of inputs
|
||||
self._inputs = []
|
||||
|
||||
for i in range(m):
|
||||
row = []
|
||||
# to keep it simple: use plain html table elements
|
||||
tr = HTML.Tr(parent=self)
|
||||
for j in range(n):
|
||||
th = HTML.Th(parent=tr)
|
||||
cell = bInputs.InputFloat(parent=th)
|
||||
cell.p = 0
|
||||
cell.m = 0
|
||||
cell.value = self._numpy_grid[i,j]
|
||||
row.append(cell)
|
||||
self._inputs.append(row)
|
||||
|
||||
def _update_ndarray(self):
|
||||
for i, row in enumerate(self._inputs):
|
||||
for j, cell in enumerate(row):
|
||||
self._numpy_grid[i,j] = float(cell.value)
|
||||
|
||||
|
||||
@property
|
||||
def numpy_grid(self) -> np.ndarray:
|
||||
self._update_ndarray()
|
||||
return self._numpy_grid.copy()
|
||||
|
||||
@numpy_grid.setter
|
||||
def numpy_grid(self, value:np.ndarray):
|
||||
assert value.shape == self._numpy_grid.shape
|
||||
self._numpy_grid = value.copy()
|
||||
# update input grid
|
||||
|
||||
for i, row in enumerate(self._inputs):
|
||||
for j, cell in enumerate(row):
|
||||
cell.value = self._numpy_grid[i,j]
|
||||
|
||||
|
||||
# create default values
|
||||
matrix_a = np.array([[1,2,3],[4,5,6],[7,8,9]])
|
||||
matrix_b = np.array([[1,2,3],[4,5,6],[7,8,9]])
|
||||
|
||||
matrix_result = np.zeros((3,3))
|
||||
|
||||
app = bootstrap_templates.PyScriptBootstrapDashboard(
|
||||
parent_element="pyscript_app", brand_name="03_numpy_grid_demo")
|
||||
div = bHTML.BootstrapContainer("Matrix operations", parent=app.sidebar)
|
||||
div.font_size = 4
|
||||
|
||||
matrix_row = bHTML.Row(parent=app.main)
|
||||
matrix_row.p = 1
|
||||
matrix_row.m = 1
|
||||
matrix_row.display_property = bHTML.DisplayProperty.INLINE_FLEX
|
||||
|
||||
# create row of three matrices
|
||||
mat_widget_a = NumpyGrid2D(matrix_a)
|
||||
mat_widget_b = NumpyGrid2D(matrix_b)
|
||||
mat_widget_result = NumpyGrid2D(matrix_result)
|
||||
|
||||
# just for fanciness: wrap the matrices in bootstrap cards
|
||||
col_a = bHTML.Col(parent=matrix_row)
|
||||
col_b = bHTML.Col(parent=matrix_row)
|
||||
col_result = bHTML.Col(parent=matrix_row)
|
||||
|
||||
card_a = bHTML.Card(mat_widget_a, card_header=bHTML.Div("Matrix A"), parent=col_a)
|
||||
card_b = bHTML.Card(mat_widget_b, card_header=bHTML.Div("Matrix B"), parent=col_b)
|
||||
card_result = bHTML.Card(mat_widget_result, card_header=bHTML.Div("Result"), parent=col_result)
|
||||
|
||||
card_a.shadow = bHTML.Shadow.MEDIUM
|
||||
card_b.shadow = bHTML.Shadow.MEDIUM
|
||||
card_result.shadow = bHTML.Shadow.MEDIUM
|
||||
|
||||
operations = {
|
||||
'+': np.add,
|
||||
'-': np.subtract,
|
||||
'*': np.multiply,
|
||||
'/': np.divide,
|
||||
'dot product': np.dot
|
||||
}
|
||||
|
||||
for op in operations:
|
||||
btn = bHTML.ButtonPrimary(op, parent=app.sidebar)
|
||||
btn.w = 100
|
||||
btn.m = 1
|
||||
def onclick(event, numpy_func=operations[op]):
|
||||
mat_widget_result.numpy_grid = numpy_func(mat_widget_a.numpy_grid, mat_widget_b.numpy_grid)
|
||||
btn.onclick = onclick
|
||||
|
||||
|
||||
|
||||
|
||||
|
1
examples/03_numpy_grid_demo/manifest.json
Normal file
1
examples/03_numpy_grid_demo/manifest.json
Normal file
@ -0,0 +1 @@
|
||||
{"name": "03_numpy_grid_demo", "short_name": "03_numpy_grid_demo", "start_url": "./index.html", "scope": ".", "display": "standalone", "background_color": "#000000", "theme_color": "#ffffff", "icons": [{"src": "resources/icon-512x512.png", "type": "image/png", "sizes": "512x512"}]}
|
7
examples/03_numpy_grid_demo/resources/bootstrap.css
vendored
Normal file
7
examples/03_numpy_grid_demo/resources/bootstrap.css
vendored
Normal file
File diff suppressed because one or more lines are too long
7
examples/03_numpy_grid_demo/resources/bootstrap.js
vendored
Normal file
7
examples/03_numpy_grid_demo/resources/bootstrap.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
examples/03_numpy_grid_demo/resources/icon-512x512.png
Normal file
BIN
examples/03_numpy_grid_demo/resources/icon-512x512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
10
examples/03_numpy_grid_demo/resources/pwa.js
Normal file
10
examples/03_numpy_grid_demo/resources/pwa.js
Normal file
@ -0,0 +1,10 @@
|
||||
|
||||
if ("serviceWorker" in navigator) {
|
||||
window.addEventListener("load", function () {
|
||||
navigator.serviceWorker
|
||||
.register("./serviceWorker.js")
|
||||
.then(res => console.log("service worker registered", res))
|
||||
.catch(err => console.log("service worker not registered", err))
|
||||
})
|
||||
}
|
||||
|
332
examples/03_numpy_grid_demo/resources/pyscript.css
Normal file
332
examples/03_numpy_grid_demo/resources/pyscript.css
Normal file
@ -0,0 +1,332 @@
|
||||
/* py-config - not a component */
|
||||
py-config {
|
||||
display: none;
|
||||
}
|
||||
/* py-{el} - components not defined */
|
||||
py-script:not(:defined) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
py-repl:not(:defined) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
py-title:not(:defined) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
py-inputbox:not(:defined) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
py-button:not(:defined) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
py-box:not(:defined) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue',
|
||||
Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.spinner::after {
|
||||
content: '';
|
||||
box-sizing: border-box;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
position: absolute;
|
||||
top: calc(40% - 20px);
|
||||
left: calc(50% - 20px);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.spinner.smooth::after {
|
||||
border-top: 4px solid rgba(255, 255, 255, 1);
|
||||
border-left: 4px solid rgba(255, 255, 255, 1);
|
||||
border-right: 4px solid rgba(255, 255, 255, 0);
|
||||
animation: spinner 0.6s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spinner {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
display: block;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 0.8rem;
|
||||
margin-top: 6rem;
|
||||
}
|
||||
|
||||
/* Pop-up second layer begin */
|
||||
|
||||
.py-overlay {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: white;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
transition: opacity 500ms;
|
||||
visibility: hidden;
|
||||
color: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.py-overlay {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.py-pop-up {
|
||||
text-align: center;
|
||||
width: 600px;
|
||||
}
|
||||
|
||||
.py-pop-up p {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.py-pop-up a {
|
||||
position: absolute;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
font-size: 200%;
|
||||
top: 3.5%;
|
||||
right: 5%;
|
||||
}
|
||||
|
||||
/* Pop-up second layer end */
|
||||
.alert-banner {
|
||||
position: relative;
|
||||
padding: 0.5rem 1.5rem 0.5rem 0.5rem;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.alert-banner p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.py-error {
|
||||
background-color: #ffe9e8;
|
||||
border: solid;
|
||||
border-color: #f0625f;
|
||||
color: #9d041c;
|
||||
}
|
||||
|
||||
.py-warning {
|
||||
background-color: rgb(255, 244, 229);
|
||||
border: solid;
|
||||
border-color: #ffa016;
|
||||
color: #794700;
|
||||
}
|
||||
|
||||
.alert-banner.py-error > #alert-close-button {
|
||||
color: #9d041c;
|
||||
}
|
||||
|
||||
.alert-banner.py-warning > #alert-close-button {
|
||||
color: #794700;
|
||||
}
|
||||
|
||||
#alert-close-button {
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
top: 0.5rem;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.py-box {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.py-box div.py-box-child * {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.py-repl-box {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.py-repl-editor {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgba(209, 213, 219, var(--tw-border-opacity));
|
||||
border-width: 1px;
|
||||
position: relative;
|
||||
--tw-ring-inset: var(--tw-empty, /*!*/ /*!*/);
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-color: rgba(59, 130, 246, 0.5);
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-ring-shadow: 0 0 #0000;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
position: relative;
|
||||
|
||||
box-sizing: border-box;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: rgb(209, 213, 219);
|
||||
}
|
||||
|
||||
.editor-box:hover button {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.py-repl-run-button {
|
||||
opacity: 0;
|
||||
bottom: 0.25rem;
|
||||
right: 0.25rem;
|
||||
position: absolute;
|
||||
padding: 0;
|
||||
line-height: inherit;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
background-image: none;
|
||||
-webkit-appearance: button;
|
||||
text-transform: none;
|
||||
font-family: inherit;
|
||||
font-size: 100%;
|
||||
margin: 0;
|
||||
text-rendering: auto;
|
||||
letter-spacing: normal;
|
||||
word-spacing: normal;
|
||||
line-height: normal;
|
||||
text-transform: none;
|
||||
text-indent: 0px;
|
||||
text-shadow: none;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
align-items: flex-start;
|
||||
cursor: default;
|
||||
box-sizing: border-box;
|
||||
background-color: -internal-light-dark(rgb(239, 239, 239), rgb(59, 59, 59));
|
||||
margin: 0em;
|
||||
padding: 1px 6px;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.py-repl-run-button:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.py-title {
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.py-title h1 {
|
||||
font-weight: 700;
|
||||
font-size: 1.875rem;
|
||||
}
|
||||
|
||||
.py-input {
|
||||
padding: 0.5rem;
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgba(209, 213, 219, var(--tw-border-opacity));
|
||||
border-width: 1px;
|
||||
border-radius: 0.25rem;
|
||||
margin-right: 0.75rem;
|
||||
border-style: solid;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.py-box input.py-input {
|
||||
width: -webkit-fill-available;
|
||||
}
|
||||
|
||||
.central-content {
|
||||
max-width: 20rem;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
input {
|
||||
text-rendering: auto;
|
||||
color: -internal-light-dark(black, white);
|
||||
letter-spacing: normal;
|
||||
word-spacing: normal;
|
||||
line-height: normal;
|
||||
text-transform: none;
|
||||
text-indent: 0px;
|
||||
text-shadow: none;
|
||||
display: inline-block;
|
||||
text-align: start;
|
||||
appearance: auto;
|
||||
-webkit-rtl-ordering: logical;
|
||||
background-color: -internal-light-dark(rgb(255, 255, 255), rgb(59, 59, 59));
|
||||
margin: 0em;
|
||||
padding: 1px 2px;
|
||||
border-width: 2px;
|
||||
border-style: inset;
|
||||
border-color: -internal-light-dark(rgb(118, 118, 118), rgb(133, 133, 133));
|
||||
border-image: initial;
|
||||
}
|
||||
|
||||
.py-button {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgba(255, 255, 255, var(--tw-text-opacity));
|
||||
padding: 0.5rem;
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgba(37, 99, 235, var(--tw-bg-opacity));
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgba(37, 99, 235, var(--tw-border-opacity));
|
||||
border-width: 1px;
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.py-li-element p {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.py-li-element p {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
font-size: 100%;
|
||||
line-height: 1.15;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.line-through {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
/* ===== py-terminal plugin ===== */
|
||||
/* XXX: it would be nice if these rules were stored in e.g. pyterminal.css and
|
||||
bundled together at build time (by rollup?) */
|
||||
|
||||
.py-terminal {
|
||||
min-height: 10em;
|
||||
background-color: black;
|
||||
color: white;
|
||||
padding: 0.5rem;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.py-terminal-hidden {
|
||||
display: none;
|
||||
}
|
28907
examples/03_numpy_grid_demo/resources/pyscript.js
Normal file
28907
examples/03_numpy_grid_demo/resources/pyscript.js
Normal file
File diff suppressed because one or more lines are too long
424
examples/03_numpy_grid_demo/resources/pyscript.py
Normal file
424
examples/03_numpy_grid_demo/resources/pyscript.py
Normal 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()
|
Binary file not shown.
Binary file not shown.
62
examples/03_numpy_grid_demo/serviceWorker.js
Normal file
62
examples/03_numpy_grid_demo/serviceWorker.js
Normal file
@ -0,0 +1,62 @@
|
||||
|
||||
const pwa_version = "03_numpy_grid_demo_202306111641"
|
||||
const assets = ["./index.html",
|
||||
"./main.py",
|
||||
"./resources/bootstrap.css",
|
||||
"./resources/bootstrap.js",
|
||||
"./resources/pyscript.css",
|
||||
"./resources/pyscript.js",
|
||||
"./resources/pyscript.py",
|
||||
"./resources/pyscript_bootstrap_templates-0.2.0-py3-none-any.whl",
|
||||
"./resources/pwa.js",
|
||||
"./site.js",
|
||||
]
|
||||
self.addEventListener("install", installEvent => {
|
||||
installEvent.waitUntil(
|
||||
caches.open(pwa_version).then(cache => {
|
||||
cache.addAll(assets).then(r => {
|
||||
console.log("Cache assets downloaded");
|
||||
}).catch(err => console.log("Error caching item", err))
|
||||
console.log(`Cache ${pwa_version} opened.`);
|
||||
}).catch(err => console.log("Error opening cache", err))
|
||||
)
|
||||
})
|
||||
|
||||
self.addEventListener('activate', e => {
|
||||
console.log('Service Worker: Activated');
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', event => {
|
||||
if (event.request.method != 'GET')
|
||||
return;
|
||||
event.respondWith((async () => {
|
||||
const cachedResponse = await caches.match(event.request);
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
const response = await fetch(event.request);
|
||||
|
||||
if (!response || response.status !== 200 || response.type !== 'basic') {
|
||||
return response;
|
||||
}
|
||||
|
||||
const responseToCache = response.clone();
|
||||
const cache = await caches.open(pwa_version)
|
||||
await cache.put(event.request, response.clone());
|
||||
|
||||
return response;
|
||||
})());
|
||||
});
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys().then((keyList) => {
|
||||
return Promise.all(keyList.map((key) => {
|
||||
if(key !== pwa_version) {
|
||||
return caches.delete(key);
|
||||
}
|
||||
}));
|
||||
})
|
||||
);
|
||||
});
|
||||
|
2
examples/03_numpy_grid_demo/site.js
Normal file
2
examples/03_numpy_grid_demo/site.js
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
|
18
examples/04_cell_detector/config.json
Normal file
18
examples/04_cell_detector/config.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"root_folder": "04_cell_detector",
|
||||
"title": "04_cell_detector",
|
||||
"packages": [
|
||||
"numpy",
|
||||
"Pillow",
|
||||
"opencv-python"
|
||||
],
|
||||
"paths": [],
|
||||
"pyscript_css_url": "https://pyscript.net/latest/pyscript.css",
|
||||
"pyscript_js_url": "https://pyscript.net/latest/pyscript.js",
|
||||
"pyscript_py_url": "https://pyscript.net/latest/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.2.0-py3-none-any.whl",
|
||||
"pwa_bg_color": "#000000",
|
||||
"pwa_theme_color": "#ffffff"
|
||||
}
|
40
examples/04_cell_detector/index.html
Normal file
40
examples/04_cell_detector/index.html
Normal file
@ -0,0 +1,40 @@
|
||||
|
||||
<!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="manifest" href="manifest.json" />
|
||||
|
||||
<script src="./resources/pwa.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="./resources/pyscript.css" />
|
||||
<script defer src="./resources/pyscript.js"></script>
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="./resources/bootstrap.css" rel="stylesheet">
|
||||
|
||||
<title>04_cell_detector</title>
|
||||
</head>
|
||||
<body style="width: 100%; height: 100%">
|
||||
<py-config type="json">
|
||||
{
|
||||
"splashscreen": {
|
||||
"autoclose": true
|
||||
},
|
||||
"packages": [
|
||||
"./resources/pyscript_bootstrap_templates-0.2.0-py3-none-any.whl",
|
||||
"numpy",
|
||||
"Pillow",
|
||||
"opencv-python"
|
||||
],
|
||||
"paths": []
|
||||
}
|
||||
</py-config>
|
||||
<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>
|
||||
|
309
examples/04_cell_detector/main.py
Normal file
309
examples/04_cell_detector/main.py
Normal file
@ -0,0 +1,309 @@
|
||||
|
||||
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
|
||||
from pyscript_bootstrap_templates import HTML as HTML
|
||||
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
import cv2
|
||||
|
||||
loaded_img:np.ndarray = None
|
||||
|
||||
app = bootstrap_templates.PyScriptBootstrapDashboard(parent_element="pyscript_app", brand_name="Pyscript Cell Colony Detector")
|
||||
|
||||
main_div = bHTML.BootstrapContainer(parent=app.main)
|
||||
main_div.w = 100
|
||||
result_div = bHTML.BootstrapContainer (parent=main_div)
|
||||
|
||||
|
||||
def process_image(image,
|
||||
hough_min_dist = 500,
|
||||
hough_param1 = 80,
|
||||
hough_param2 = 500,
|
||||
minRadius = 100,
|
||||
maxRadius=500,
|
||||
inner_hough_param1 = 25,
|
||||
inner_hough_param2 = 50,
|
||||
inner_hough_circles=True,
|
||||
cell_colony_color_channel:int = None):
|
||||
|
||||
for child in result_div.children:
|
||||
child.destroy()
|
||||
|
||||
# Convert the image to grayscale
|
||||
if cell_colony_color_channel is None:
|
||||
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
||||
else:
|
||||
gray = image.astype(float)[...,cell_colony_color_channel]
|
||||
gray = (255 * gray / gray.max()).astype(np.uint8)
|
||||
|
||||
# Use median blur to reduce noise
|
||||
gray = cv2.medianBlur(gray, 5)
|
||||
|
||||
# Apply Hough transform on the image to find circles
|
||||
circles = cv2.HoughCircles(gray,
|
||||
cv2.HOUGH_GRADIENT,
|
||||
2,
|
||||
minDist=hough_min_dist,
|
||||
param1=hough_param1,
|
||||
param2=hough_param2,
|
||||
minRadius=minRadius,
|
||||
maxRadius=maxRadius)
|
||||
|
||||
if circles is None:
|
||||
app.alert_danger("No cell areas detected in image")
|
||||
return False
|
||||
|
||||
# Convert to integers
|
||||
circles = np.uint16(np.around(circles))
|
||||
|
||||
# Loop over all detected circles and add them to the image
|
||||
#for i in circles[0,:]:
|
||||
# # Draw outer circle
|
||||
# cv2.circle(image,(i[0],i[1]),i[2],(0,255,0),2)
|
||||
# # Draw center of circle
|
||||
# cv2.circle(image,(i[0],i[1]),2,(0,0,255),3)
|
||||
|
||||
first_wells = []
|
||||
first_wells_gray = []
|
||||
|
||||
|
||||
for i in circles[0,:]:
|
||||
|
||||
center = (i[0], i[1])
|
||||
radius = i[2]
|
||||
|
||||
|
||||
# Cut out the well
|
||||
well = image[center[1]-radius:center[1]+radius, center[0]-radius:center[0]+radius]
|
||||
well_gray = gray[center[1]-radius:center[1]+radius, center[0]-radius:center[0]+radius]
|
||||
|
||||
|
||||
first_wells.append(well)
|
||||
first_wells_gray.append(well_gray)
|
||||
|
||||
if inner_hough_circles:
|
||||
|
||||
second_wells = []
|
||||
second_wells_gray = []
|
||||
|
||||
for well, well_gray in zip(first_wells, first_wells_gray):
|
||||
|
||||
circles = cv2.HoughCircles(well_gray,
|
||||
cv2.HOUGH_GRADIENT,
|
||||
1,
|
||||
minDist=hough_min_dist,
|
||||
param1=inner_hough_param1,
|
||||
param2=inner_hough_param2,
|
||||
minRadius=int((min(well.shape[0],well.shape[1]) // 2)*0.79),
|
||||
maxRadius=int((min(well.shape[0],well.shape[1]) // 2)*0.95))
|
||||
if circles is not None:
|
||||
circles = np.uint16(np.around(circles))
|
||||
i = circles[0,:][0]
|
||||
center = (i[0], i[1])
|
||||
radius = i[2] - 1
|
||||
|
||||
min_y = max(center[1]-radius, 0)
|
||||
max_y = center[1]+radius
|
||||
|
||||
min_x = max(center[0]-radius, 0)
|
||||
max_x = center[0]+radius
|
||||
|
||||
|
||||
|
||||
second_wells.append(well[min_y:max_y, min_x:max_x])
|
||||
second_wells_gray.append(well_gray[min_y:max_y, min_x:max_x])
|
||||
else:
|
||||
second_wells.append(well)
|
||||
second_wells_gray.append(well_gray)
|
||||
|
||||
else:
|
||||
second_wells = first_wells
|
||||
second_wells_gray = first_wells_gray
|
||||
|
||||
tabs = {}
|
||||
|
||||
for well in second_wells:
|
||||
|
||||
well_gray = cv2.cvtColor(well, cv2.COLOR_BGR2GRAY)
|
||||
_, binary = cv2.threshold(255 - well_gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
|
||||
|
||||
|
||||
# Perform some morphological operations to remove small noise - you can change the kernel size
|
||||
kernel = np.ones((3,3),np.uint8)
|
||||
cleaned = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel)
|
||||
|
||||
|
||||
radius = min(well.shape[0], well.shape[1]) // 2 -1
|
||||
# Now clear all pixels outside the circular well.
|
||||
# We do this by creating a mask for the well and applying it to the image.
|
||||
well_mask = np.zeros_like(cleaned, dtype=np.uint8)
|
||||
cv2.circle(well_mask, (well.shape[1] // 2, well.shape[0] // 2), radius, 1, thickness=-1)
|
||||
cleaned = cleaned * well_mask
|
||||
|
||||
circle = well_mask > 0
|
||||
|
||||
# now create an image overlay to display
|
||||
ratio = np.sum((cleaned > 0).astype(int)) / np.sum(circle.astype(int))
|
||||
div_result = bHTML.BootstrapContainer(f"ratio: {ratio * 100}%")
|
||||
div_result.shadow = bHTML.Shadow.LARGE
|
||||
div_result.rounded = True
|
||||
div_result.w = 75
|
||||
div_result.h = 75
|
||||
div_result.rounded_size = 50
|
||||
div_result.p = 3
|
||||
|
||||
well = well.copy()
|
||||
well[cleaned > 0,0] = 255
|
||||
final_img = bHTML.Image.from_numpy_array(well, parent=div_result)
|
||||
final_img.rounded = True
|
||||
final_img.rounded_size = 50
|
||||
final_img.shadow = bHTML.Shadow.LARGE
|
||||
final_img.w = 100
|
||||
|
||||
tabs[f"Well #{len(tabs) + 1}"] = div_result
|
||||
|
||||
tabs = bHTML.Tabs(tabs, parent=result_div)
|
||||
tabs.w = 100
|
||||
|
||||
|
||||
div = bHTML.BootstrapContainer("Controls", parent=app.sidebar)
|
||||
div.font_size = 4
|
||||
|
||||
image_input = bInputs.InputFile(label_text="choose image file", parent=app.sidebar)
|
||||
|
||||
btn = bHTML.ButtonPrimary("Process", parent=app.sidebar)
|
||||
btn.w = 100
|
||||
btn.mt = 3
|
||||
btn.mb = 3
|
||||
btn.ml = 4
|
||||
btn.mr = 4
|
||||
|
||||
i_hough_min_dist = bInputs.InputInt("hough min distance [px]",
|
||||
parent=app.sidebar,
|
||||
help_text="minimal distance between wells in pixels")
|
||||
i_hough_min_dist.value = 500
|
||||
|
||||
i_hough_param1 = bInputs.InputInt("hough param1",
|
||||
parent=app.sidebar,
|
||||
help_text="parameter for canny edge detector")
|
||||
i_hough_param1.value = 80
|
||||
|
||||
i_hough_param2 = bInputs.InputInt("hough param2",
|
||||
parent=app.sidebar,
|
||||
help_text="increase this value to prevent false circle detection")
|
||||
i_hough_param2.value = 500
|
||||
|
||||
i_hough_min_radius = bInputs.InputInt("hough min radius [px]",
|
||||
parent=app.sidebar,
|
||||
help_text="min radius for circle detection")
|
||||
i_hough_min_radius.value = 100
|
||||
|
||||
i_hough_max_radius = bInputs.InputInt("hough max radius [px]",
|
||||
parent=app.sidebar,
|
||||
help_text="max radius for circle detection")
|
||||
i_hough_max_radius.value = 500
|
||||
|
||||
i_inner_hough = bInputs.InputCheckboxSingle("nested hough transform?",
|
||||
parent=app.sidebar,
|
||||
help_text="if set, circle detection will be applied twice on detected wells")
|
||||
i_inner_hough.value = True
|
||||
i_inner_hough.m = 3
|
||||
|
||||
i_inner_hough_param1 = bInputs.InputInt("inner hough param1", parent=app.sidebar)
|
||||
i_inner_hough_param1.value = 25
|
||||
|
||||
i_inner_hough_param2 = bInputs.InputInt("inner hough param2", parent=app.sidebar)
|
||||
i_inner_hough_param2.value = 50
|
||||
|
||||
i_use_color_channel = bInputs.InputSelect(["all",
|
||||
"red",
|
||||
"green",
|
||||
"blue"],
|
||||
label_text="cell colony color channel",
|
||||
parent=app.sidebar,
|
||||
help_text="if cells are more present in a specific channel, select it here")
|
||||
i_use_color_channel.value = "blue"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def on_image_change(f, *args):
|
||||
|
||||
try:
|
||||
global loaded_img
|
||||
f.seek(0)
|
||||
img = np.array(Image.open(f))
|
||||
for child in result_div.children:
|
||||
child.destroy()
|
||||
|
||||
bHTML.BootstrapContainer("input image:", parent=result_div)
|
||||
output = bHTML.Image.from_numpy_array(img, parent=result_div)
|
||||
output.rounded = True
|
||||
output.rounded_size = 10
|
||||
output.shadow = bHTML.Shadow.LARGE
|
||||
output.width = "100%"
|
||||
output.height = "100%"
|
||||
|
||||
loaded_img = img
|
||||
|
||||
app.toast("successfully loaded image", "info")
|
||||
#toast = bHTML.Toast("successfully loaded image", title="info", parent=toast_container)
|
||||
#toast.animation = True
|
||||
#toast.show()
|
||||
except Exception as e:
|
||||
for child in result_div.children:
|
||||
child.destroy()
|
||||
bHTML.AlertDanger(f"error while loading image: {str(e)}", parent=alert_container)
|
||||
|
||||
|
||||
image_input.onchange = on_image_change
|
||||
|
||||
def on_click(*args, **kwargs):
|
||||
if loaded_img is None:
|
||||
app.alert_danger("No image loaded")
|
||||
return
|
||||
for child in result_div.children:
|
||||
child.destroy()
|
||||
|
||||
try:
|
||||
h_min_dist = int(i_hough_min_dist.value)
|
||||
h_param1 = int(i_hough_param1.value)
|
||||
h_param2 = int(i_hough_param2.value)
|
||||
h_inner_param1 = int(i_inner_hough_param1.value)
|
||||
h_inner_param2 = int(i_inner_hough_param2.value)
|
||||
h_min_radius = int(i_hough_min_radius.value)
|
||||
h_max_radius = int(i_hough_max_radius.value)
|
||||
inner_hough = bool(i_inner_hough.value)
|
||||
color_channel = {
|
||||
"all": None,
|
||||
"red": 0,
|
||||
"green": 1,
|
||||
"blue": 2
|
||||
}[i_use_color_channel.value]
|
||||
|
||||
|
||||
|
||||
process_image(loaded_img,
|
||||
hough_min_dist=h_min_dist,
|
||||
hough_param1=h_param1,
|
||||
hough_param2=h_param2,
|
||||
minRadius=h_min_radius,
|
||||
maxRadius=h_max_radius,
|
||||
inner_hough_param1=h_inner_param1,
|
||||
inner_hough_param2=h_inner_param2,
|
||||
inner_hough_circles=inner_hough,
|
||||
cell_colony_color_channel=color_channel)
|
||||
app.toast("successfully processed image", title="info")
|
||||
|
||||
except Exception as e:
|
||||
for child in result_div.children:
|
||||
child.destroy()
|
||||
app.alert_danger(f"error while processing image: {str(e)}")
|
||||
|
||||
btn.onclick = on_click
|
||||
|
||||
|
1
examples/04_cell_detector/manifest.json
Normal file
1
examples/04_cell_detector/manifest.json
Normal file
@ -0,0 +1 @@
|
||||
{"name": "04_cell_detector", "short_name": "04_cell_detector", "start_url": "./index.html", "scope": ".", "display": "standalone", "background_color": "#000000", "theme_color": "#ffffff", "icons": [{"src": "resources/icon-512x512.png", "type": "image/png", "sizes": "512x512"}]}
|
7
examples/04_cell_detector/resources/bootstrap.css
vendored
Normal file
7
examples/04_cell_detector/resources/bootstrap.css
vendored
Normal file
File diff suppressed because one or more lines are too long
7
examples/04_cell_detector/resources/bootstrap.js
vendored
Normal file
7
examples/04_cell_detector/resources/bootstrap.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
examples/04_cell_detector/resources/icon-512x512.png
Normal file
BIN
examples/04_cell_detector/resources/icon-512x512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
10
examples/04_cell_detector/resources/pwa.js
Normal file
10
examples/04_cell_detector/resources/pwa.js
Normal file
@ -0,0 +1,10 @@
|
||||
|
||||
if ("serviceWorker" in navigator) {
|
||||
window.addEventListener("load", function () {
|
||||
navigator.serviceWorker
|
||||
.register("./serviceWorker.js")
|
||||
.then(res => console.log("service worker registered", res))
|
||||
.catch(err => console.log("service worker not registered", err))
|
||||
})
|
||||
}
|
||||
|
332
examples/04_cell_detector/resources/pyscript.css
Normal file
332
examples/04_cell_detector/resources/pyscript.css
Normal file
@ -0,0 +1,332 @@
|
||||
/* py-config - not a component */
|
||||
py-config {
|
||||
display: none;
|
||||
}
|
||||
/* py-{el} - components not defined */
|
||||
py-script:not(:defined) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
py-repl:not(:defined) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
py-title:not(:defined) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
py-inputbox:not(:defined) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
py-button:not(:defined) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
py-box:not(:defined) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue',
|
||||
Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.spinner::after {
|
||||
content: '';
|
||||
box-sizing: border-box;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
position: absolute;
|
||||
top: calc(40% - 20px);
|
||||
left: calc(50% - 20px);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.spinner.smooth::after {
|
||||
border-top: 4px solid rgba(255, 255, 255, 1);
|
||||
border-left: 4px solid rgba(255, 255, 255, 1);
|
||||
border-right: 4px solid rgba(255, 255, 255, 0);
|
||||
animation: spinner 0.6s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spinner {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
display: block;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 0.8rem;
|
||||
margin-top: 6rem;
|
||||
}
|
||||
|
||||
/* Pop-up second layer begin */
|
||||
|
||||
.py-overlay {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: white;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
transition: opacity 500ms;
|
||||
visibility: hidden;
|
||||
color: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.py-overlay {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.py-pop-up {
|
||||
text-align: center;
|
||||
width: 600px;
|
||||
}
|
||||
|
||||
.py-pop-up p {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.py-pop-up a {
|
||||
position: absolute;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
font-size: 200%;
|
||||
top: 3.5%;
|
||||
right: 5%;
|
||||
}
|
||||
|
||||
/* Pop-up second layer end */
|
||||
.alert-banner {
|
||||
position: relative;
|
||||
padding: 0.5rem 1.5rem 0.5rem 0.5rem;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.alert-banner p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.py-error {
|
||||
background-color: #ffe9e8;
|
||||
border: solid;
|
||||
border-color: #f0625f;
|
||||
color: #9d041c;
|
||||
}
|
||||
|
||||
.py-warning {
|
||||
background-color: rgb(255, 244, 229);
|
||||
border: solid;
|
||||
border-color: #ffa016;
|
||||
color: #794700;
|
||||
}
|
||||
|
||||
.alert-banner.py-error > #alert-close-button {
|
||||
color: #9d041c;
|
||||
}
|
||||
|
||||
.alert-banner.py-warning > #alert-close-button {
|
||||
color: #794700;
|
||||
}
|
||||
|
||||
#alert-close-button {
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
top: 0.5rem;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.py-box {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.py-box div.py-box-child * {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.py-repl-box {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.py-repl-editor {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgba(209, 213, 219, var(--tw-border-opacity));
|
||||
border-width: 1px;
|
||||
position: relative;
|
||||
--tw-ring-inset: var(--tw-empty, /*!*/ /*!*/);
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-color: rgba(59, 130, 246, 0.5);
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-ring-shadow: 0 0 #0000;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
position: relative;
|
||||
|
||||
box-sizing: border-box;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: rgb(209, 213, 219);
|
||||
}
|
||||
|
||||
.editor-box:hover button {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.py-repl-run-button {
|
||||
opacity: 0;
|
||||
bottom: 0.25rem;
|
||||
right: 0.25rem;
|
||||
position: absolute;
|
||||
padding: 0;
|
||||
line-height: inherit;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
background-image: none;
|
||||
-webkit-appearance: button;
|
||||
text-transform: none;
|
||||
font-family: inherit;
|
||||
font-size: 100%;
|
||||
margin: 0;
|
||||
text-rendering: auto;
|
||||
letter-spacing: normal;
|
||||
word-spacing: normal;
|
||||
line-height: normal;
|
||||
text-transform: none;
|
||||
text-indent: 0px;
|
||||
text-shadow: none;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
align-items: flex-start;
|
||||
cursor: default;
|
||||
box-sizing: border-box;
|
||||
background-color: -internal-light-dark(rgb(239, 239, 239), rgb(59, 59, 59));
|
||||
margin: 0em;
|
||||
padding: 1px 6px;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.py-repl-run-button:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.py-title {
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.py-title h1 {
|
||||
font-weight: 700;
|
||||
font-size: 1.875rem;
|
||||
}
|
||||
|
||||
.py-input {
|
||||
padding: 0.5rem;
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgba(209, 213, 219, var(--tw-border-opacity));
|
||||
border-width: 1px;
|
||||
border-radius: 0.25rem;
|
||||
margin-right: 0.75rem;
|
||||
border-style: solid;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.py-box input.py-input {
|
||||
width: -webkit-fill-available;
|
||||
}
|
||||
|
||||
.central-content {
|
||||
max-width: 20rem;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
input {
|
||||
text-rendering: auto;
|
||||
color: -internal-light-dark(black, white);
|
||||
letter-spacing: normal;
|
||||
word-spacing: normal;
|
||||
line-height: normal;
|
||||
text-transform: none;
|
||||
text-indent: 0px;
|
||||
text-shadow: none;
|
||||
display: inline-block;
|
||||
text-align: start;
|
||||
appearance: auto;
|
||||
-webkit-rtl-ordering: logical;
|
||||
background-color: -internal-light-dark(rgb(255, 255, 255), rgb(59, 59, 59));
|
||||
margin: 0em;
|
||||
padding: 1px 2px;
|
||||
border-width: 2px;
|
||||
border-style: inset;
|
||||
border-color: -internal-light-dark(rgb(118, 118, 118), rgb(133, 133, 133));
|
||||
border-image: initial;
|
||||
}
|
||||
|
||||
.py-button {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgba(255, 255, 255, var(--tw-text-opacity));
|
||||
padding: 0.5rem;
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgba(37, 99, 235, var(--tw-bg-opacity));
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgba(37, 99, 235, var(--tw-border-opacity));
|
||||
border-width: 1px;
|
||||
border-radius: 0.25rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.py-li-element p {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.py-li-element p {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
font-size: 100%;
|
||||
line-height: 1.15;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.line-through {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
/* ===== py-terminal plugin ===== */
|
||||
/* XXX: it would be nice if these rules were stored in e.g. pyterminal.css and
|
||||
bundled together at build time (by rollup?) */
|
||||
|
||||
.py-terminal {
|
||||
min-height: 10em;
|
||||
background-color: black;
|
||||
color: white;
|
||||
padding: 0.5rem;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.py-terminal-hidden {
|
||||
display: none;
|
||||
}
|
28907
examples/04_cell_detector/resources/pyscript.js
Normal file
28907
examples/04_cell_detector/resources/pyscript.js
Normal file
File diff suppressed because one or more lines are too long
424
examples/04_cell_detector/resources/pyscript.py
Normal file
424
examples/04_cell_detector/resources/pyscript.py
Normal 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()
|
Binary file not shown.
Binary file not shown.
62
examples/04_cell_detector/serviceWorker.js
Normal file
62
examples/04_cell_detector/serviceWorker.js
Normal file
@ -0,0 +1,62 @@
|
||||
|
||||
const pwa_version = "04_cell_detector_202306111641"
|
||||
const assets = ["./index.html",
|
||||
"./main.py",
|
||||
"./resources/bootstrap.css",
|
||||
"./resources/bootstrap.js",
|
||||
"./resources/pyscript.css",
|
||||
"./resources/pyscript.js",
|
||||
"./resources/pyscript.py",
|
||||
"./resources/pyscript_bootstrap_templates-0.2.0-py3-none-any.whl",
|
||||
"./resources/pwa.js",
|
||||
"./site.js",
|
||||
]
|
||||
self.addEventListener("install", installEvent => {
|
||||
installEvent.waitUntil(
|
||||
caches.open(pwa_version).then(cache => {
|
||||
cache.addAll(assets).then(r => {
|
||||
console.log("Cache assets downloaded");
|
||||
}).catch(err => console.log("Error caching item", err))
|
||||
console.log(`Cache ${pwa_version} opened.`);
|
||||
}).catch(err => console.log("Error opening cache", err))
|
||||
)
|
||||
})
|
||||
|
||||
self.addEventListener('activate', e => {
|
||||
console.log('Service Worker: Activated');
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', event => {
|
||||
if (event.request.method != 'GET')
|
||||
return;
|
||||
event.respondWith((async () => {
|
||||
const cachedResponse = await caches.match(event.request);
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
const response = await fetch(event.request);
|
||||
|
||||
if (!response || response.status !== 200 || response.type !== 'basic') {
|
||||
return response;
|
||||
}
|
||||
|
||||
const responseToCache = response.clone();
|
||||
const cache = await caches.open(pwa_version)
|
||||
await cache.put(event.request, response.clone());
|
||||
|
||||
return response;
|
||||
})());
|
||||
});
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys().then((keyList) => {
|
||||
return Promise.all(keyList.map((key) => {
|
||||
if(key !== pwa_version) {
|
||||
return caches.delete(key);
|
||||
}
|
||||
}));
|
||||
})
|
||||
);
|
||||
});
|
||||
|
2
examples/04_cell_detector/site.js
Normal file
2
examples/04_cell_detector/site.js
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
|
BIN
img/demo.png
Normal file
BIN
img/demo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 503 KiB |
@ -2,7 +2,7 @@
|
||||
from typing import Callable, Iterable, Union
|
||||
import uuid
|
||||
from js import document, CanvasRenderingContext2D # type: ignore
|
||||
from pyodide import create_proxy # type: ignore
|
||||
from pyodide.ffi import create_proxy # type: ignore
|
||||
from parse import *
|
||||
import base64
|
||||
from PIL import Image as PILImage
|
||||
@ -68,6 +68,7 @@ class Element(object):
|
||||
self._display = self.element.style.display
|
||||
self.element.style.display = "none"
|
||||
|
||||
|
||||
def show(self) -> None:
|
||||
if self._display is None:
|
||||
return
|
||||
@ -210,6 +211,27 @@ class Element(object):
|
||||
def write(self, object):
|
||||
self.element.write(self.id, object) # type: ignore
|
||||
|
||||
def set_style(self, property_name: str, value: str) -> None:
|
||||
"""Set a CSS style property on this element."""
|
||||
setattr(self._element.style, property_name, value)
|
||||
|
||||
def get_style(self, property_name: str) -> str:
|
||||
"""Get the value of a CSS style property on this element."""
|
||||
try:
|
||||
return getattr(self._element.style, property_name)
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
def remove_style(self, property_name: str) -> None:
|
||||
"""Remove a CSS style property from this element."""
|
||||
setattr(self._element.style, property_name, None)
|
||||
|
||||
def set_styles(self, **styles) -> None:
|
||||
"""Set multiple CSS style properties on this element."""
|
||||
for property_name, value in styles.items():
|
||||
self.set_style(property_name, value)
|
||||
|
||||
|
||||
|
||||
class A(Element):
|
||||
|
||||
@ -1282,11 +1304,11 @@ class Input(Element):
|
||||
|
||||
@property
|
||||
def value(self) -> str:
|
||||
return self.get_attribute("value")
|
||||
return self._element.value
|
||||
|
||||
@value.setter
|
||||
def value(self, value: str) -> None:
|
||||
self.set_attribute("value", value)
|
||||
self._element.value = value
|
||||
|
||||
@property
|
||||
def placeholder(self) -> str:
|
||||
|
@ -1 +1 @@
|
||||
__version__ = "0.1.0"
|
||||
__version__ = "0.2.0"
|
||||
|
@ -6,7 +6,8 @@ from . import HTML
|
||||
from .bootstrap_HTML_container import *
|
||||
from js import bootstrap # type: ignore
|
||||
|
||||
|
||||
class Div(BootstrapContainer):
|
||||
pass
|
||||
class Button(HTML.Button, BootstrapContainer):
|
||||
_default_class_name = "btn"
|
||||
|
||||
@ -515,9 +516,11 @@ class Card(BootstrapContainer):
|
||||
self._title_image = title_image
|
||||
self._card_header = card_header
|
||||
|
||||
self.append_child(card_body)
|
||||
self.append_child(title_image)
|
||||
self.append_child(card_header)
|
||||
self.append_child(title_image)
|
||||
self.append_child(card_body)
|
||||
|
||||
|
||||
|
||||
@property
|
||||
def card_body(self) -> HTML.Div:
|
||||
@ -793,16 +796,22 @@ class Col(BootstrapContainer):
|
||||
class_name: str = None,
|
||||
parent: HTML.Element = None,
|
||||
col: int = None,
|
||||
col_xs: int = None,
|
||||
col_sm: int = None,
|
||||
col_md: int = None,
|
||||
col_lg: int = None,
|
||||
col_xl: int = None,
|
||||
col_xxl: int = None) -> None:
|
||||
|
||||
|
||||
super().__init__(inner_html, id, class_name, parent)
|
||||
|
||||
|
||||
if col is not None:
|
||||
self.col = col
|
||||
|
||||
super().__init__(inner_html, id, class_name, parent)
|
||||
if col_xs is not None:
|
||||
self.col_xs = col_xs
|
||||
|
||||
if col_sm is not None:
|
||||
self.col_sm = col_sm
|
||||
@ -1107,6 +1116,9 @@ class NavbarBrand(HTML.A, BootstrapContainer):
|
||||
parent=parent)
|
||||
|
||||
self.set_attribute("href", href)
|
||||
self.ms = 3
|
||||
self.me = 3
|
||||
|
||||
|
||||
|
||||
class Navbar(BootstrapContainer):
|
||||
@ -1316,6 +1328,96 @@ class TabsDark(Tabs):
|
||||
|
||||
_default_navbar_tabs_class = NavbarTabsDark
|
||||
|
||||
class ToastHeader(BootstrapContainer):
|
||||
|
||||
_default_class_name = "toast-header"
|
||||
|
||||
def __init__(self, title:str,
|
||||
inner_html: str = None,
|
||||
id: str = None,
|
||||
class_name: str = None,
|
||||
parent: "Element" = None) -> None:
|
||||
super().__init__(inner_html=inner_html, id=id, class_name=class_name, parent=parent)
|
||||
|
||||
HTML.Strong(title, class_name="me-auto", parent=self)
|
||||
close_button = HTML.Button(class_name="btn-close", parent=self)
|
||||
close_button.set_attribute("data-bs-dismiss", "toast")
|
||||
close_button.set_attribute("aria-label", "Close")
|
||||
|
||||
class ToastBody(BootstrapContainer):
|
||||
_default_class_name: str = "toast-body"
|
||||
|
||||
class Toast(BootstrapContainer):
|
||||
|
||||
_default_class_name: str = "toast"
|
||||
|
||||
def __init__(self, inner_html: str = None,
|
||||
title: str = "Toast",
|
||||
id: str = None,
|
||||
class_name: str = None,
|
||||
parent: "Element" = None) -> None:
|
||||
|
||||
super().__init__(id=id, class_name=class_name, parent=parent)
|
||||
|
||||
self.set_attribute("role", "alert")
|
||||
self.set_attribute("aria-live", "assertlive")
|
||||
self.set_attribute("aria-atomic", True)
|
||||
self.p = 2
|
||||
|
||||
ToastHeader(title, parent=self)
|
||||
ToastBody(inner_html, parent=self)
|
||||
|
||||
self._js_toast = bootstrap.Toast.new(self.element)
|
||||
|
||||
def show(self):
|
||||
self._js_toast.show()
|
||||
|
||||
def hide(self):
|
||||
self._js_toast.hide()
|
||||
|
||||
def dispose(self):
|
||||
self._js_toast.dispose()
|
||||
|
||||
@property
|
||||
def animation(self) -> bool:
|
||||
return self.get_attribute("data-bs-animation", is_boolean_attribute=True)
|
||||
|
||||
@animation.setter
|
||||
def animation(self, value:bool):
|
||||
self.set_attribute("data-bs-animation", attribute_value=value ,is_boolean_attribute=True)
|
||||
|
||||
@property
|
||||
def autohide(self) -> bool:
|
||||
return self.get_attribute("data-bs-autohide", is_boolean_attribute=True)
|
||||
|
||||
@autohide.setter
|
||||
def autohide(self, value:bool):
|
||||
self.set_attribute("data-bs-autohide", attribute_value=value ,is_boolean_attribute=True)
|
||||
|
||||
@property
|
||||
def delay(self) -> bool:
|
||||
return self.get_attribute("data-bs-delay", is_boolean_attribute=True)
|
||||
|
||||
@delay.setter
|
||||
def delay(self, value:bool):
|
||||
self.set_attribute("data-bs-delay", attribute_value=value ,is_boolean_attribute=True)
|
||||
|
||||
class ToastContainer(BootstrapContainer):
|
||||
_default_class_name: str = "toast-container"
|
||||
|
||||
def __init__(self, inner_html: str = None,
|
||||
id: str = None,
|
||||
class_name: str = None,
|
||||
parent: "Element" = None) -> None:
|
||||
|
||||
super().__init__(inner_html=inner_html, id=id, class_name=class_name, parent=parent)
|
||||
|
||||
self.position = Position.ABSOLUTE
|
||||
self.p = 3
|
||||
|
||||
def show_toast(self, toast: Toast):
|
||||
self.append_child(toast)
|
||||
toast.show()
|
||||
|
||||
class OffcanvasTitle(HTML.H5, BootstrapContainer):
|
||||
|
||||
|
@ -315,6 +315,15 @@ class BootstrapContainer(HTML.Div):
|
||||
self._set_enum_property(
|
||||
value=value, prefix="text-", enum_class=TextAlign, enum_values=_TEXT_ALIGNS)
|
||||
|
||||
@property
|
||||
def text_align_xs(self) -> Union[TextAlign, None]:
|
||||
return self._get_enum_property(prefix="text-", enum_class=TextAlign, enum_values=_TEXT_ALIGNS, breakpoint="xs")
|
||||
|
||||
@text_align_xs.setter
|
||||
def text_align_xs(self, value: Union[TextAlign, None]):
|
||||
self._set_enum_property(value=value, prefix="text-",
|
||||
enum_class=TextAlign, enum_values=_TEXT_ALIGNS, breakpoint="xs")
|
||||
|
||||
@property
|
||||
def text_align_sm(self) -> Union[TextAlign, None]:
|
||||
return self._get_enum_property(prefix="text-", enum_class=TextAlign, enum_values=_TEXT_ALIGNS, breakpoint="sm")
|
||||
@ -559,6 +568,15 @@ class BootstrapContainer(HTML.Div):
|
||||
self._set_enum_property(
|
||||
value=value, prefix="d-", enum_class=DisplayProperty, enum_values=_DISPLAY_PROPERTIES)
|
||||
|
||||
@property
|
||||
def display_property_xs(self) -> Union[DisplayProperty, None]:
|
||||
return self._get_enum_property(prefix="d-", enum_class=DisplayProperty, enum_values=_DISPLAY_PROPERTIES, breakpoint="xs")
|
||||
|
||||
@display_property_xs.setter
|
||||
def display_property_xs(self, value: Union[DisplayProperty, None]):
|
||||
self._set_enum_property(value=value, prefix="d-", enum_class=DisplayProperty,
|
||||
enum_values=_DISPLAY_PROPERTIES, breakpoint="xs")
|
||||
|
||||
@property
|
||||
def display_property_sm(self) -> Union[DisplayProperty, None]:
|
||||
return self._get_enum_property(prefix="d-", enum_class=DisplayProperty, enum_values=_DISPLAY_PROPERTIES, breakpoint="sm")
|
||||
@ -1113,6 +1131,17 @@ class BootstrapContainer(HTML.Div):
|
||||
def p(self, value: Union[int, str, None]):
|
||||
self._set_css_param_number("p-", value)
|
||||
|
||||
@property
|
||||
def p_xs(self) -> Union[int, str, None]:
|
||||
"""
|
||||
element's padding for small screens. By default, bootstrap defines the values 0,1,2,3,4,5,auto
|
||||
"""
|
||||
return self._get_css_param_number("p-", "xs")
|
||||
|
||||
@p_xs.setter
|
||||
def p_xs(self, value: Union[int, str, None]):
|
||||
self._set_css_param_number("p-", value, "xs")
|
||||
|
||||
@property
|
||||
def p_sm(self) -> Union[int, str, None]:
|
||||
"""
|
||||
@ -1179,6 +1208,17 @@ class BootstrapContainer(HTML.Div):
|
||||
def px(self, value: Union[int, str, None]):
|
||||
self._set_css_param_number("px-", value)
|
||||
|
||||
@property
|
||||
def px_xs(self) -> Union[int, str, None]:
|
||||
"""
|
||||
element's horizontal padding for small screens. By default, bootstrap defines the values 0,1,2,3,4,5,auto
|
||||
"""
|
||||
return self._get_css_param_number("px-", "xs")
|
||||
|
||||
@px_xs.setter
|
||||
def px_xs(self, value: Union[int, str, None]):
|
||||
self._set_css_param_number("px-", value, "xs")
|
||||
|
||||
@property
|
||||
def px_sm(self) -> Union[int, str, None]:
|
||||
"""
|
||||
@ -1245,6 +1285,17 @@ class BootstrapContainer(HTML.Div):
|
||||
def py(self, value: Union[int, str, None]):
|
||||
self._set_css_param_number("py-", value)
|
||||
|
||||
@property
|
||||
def py_xs(self) -> Union[int, str, None]:
|
||||
"""
|
||||
element's vertical padding for small screens. By default, bootstrap defines the values 0,1,2,3,4,5,auto
|
||||
"""
|
||||
return self._get_css_param_number("py-", "xs")
|
||||
|
||||
@py_xs.setter
|
||||
def py_xs(self, value: Union[int, str, None]):
|
||||
self._set_css_param_number("py-", value, "xs")
|
||||
|
||||
@property
|
||||
def py_sm(self) -> Union[int, str, None]:
|
||||
"""
|
||||
@ -1311,6 +1362,17 @@ class BootstrapContainer(HTML.Div):
|
||||
def pt(self, value: Union[int, str, None]):
|
||||
self._set_css_param_number("pt-", value)
|
||||
|
||||
@property
|
||||
def pt_xs(self) -> Union[int, str, None]:
|
||||
"""
|
||||
element's top padding for small screens. By default, bootstrap defines the values 0,1,2,3,4,5,auto
|
||||
"""
|
||||
return self._get_css_param_number("pt-", "xs")
|
||||
|
||||
@pt_xs.setter
|
||||
def pt_xs(self, value: Union[int, str, None]):
|
||||
self._set_css_param_number("pt-", value, "xs")
|
||||
|
||||
@property
|
||||
def pt_sm(self) -> Union[int, str, None]:
|
||||
"""
|
||||
@ -1377,6 +1439,17 @@ class BootstrapContainer(HTML.Div):
|
||||
def pb(self, value: Union[int, str, None]):
|
||||
self._set_css_param_number("pb-", value)
|
||||
|
||||
@property
|
||||
def pb_xs(self) -> Union[int, str, None]:
|
||||
"""
|
||||
element's bottom padding for small screens. By default, bootstrap defines the values 0,1,2,3,4,5,auto
|
||||
"""
|
||||
return self._get_css_param_number("pb-", "xs")
|
||||
|
||||
@pb_xs.setter
|
||||
def pb_xs(self, value: Union[int, str, None]):
|
||||
self._set_css_param_number("pb-", value, "xs")
|
||||
|
||||
@property
|
||||
def pb_sm(self) -> Union[int, str, None]:
|
||||
"""
|
||||
@ -1443,6 +1516,17 @@ class BootstrapContainer(HTML.Div):
|
||||
def ps(self, value: Union[int, str, None]):
|
||||
self._set_css_param_number("ps-", value)
|
||||
|
||||
@property
|
||||
def ps_xs(self) -> Union[int, str, None]:
|
||||
"""
|
||||
element's start padding (left for LTR languages) for small screens. By default, bootstrap defines the values 0,1,2,3,4,5,auto
|
||||
"""
|
||||
return self._get_css_param_number("ps-", "xs")
|
||||
|
||||
@ps_xs.setter
|
||||
def ps_xs(self, value: Union[int, str, None]):
|
||||
self._set_css_param_number("ps-", value, "xs")
|
||||
|
||||
@property
|
||||
def ps_sm(self) -> Union[int, str, None]:
|
||||
"""
|
||||
@ -1509,6 +1593,17 @@ class BootstrapContainer(HTML.Div):
|
||||
def pe(self, value: Union[int, str, None]):
|
||||
self._set_css_param_number("pe-", value)
|
||||
|
||||
@property
|
||||
def pe_xs(self) -> Union[int, str, None]:
|
||||
"""
|
||||
element's end padding (right for LTR languages) for small screens. By default, bootstrap defines the values 0,1,2,3,4,5,auto
|
||||
"""
|
||||
return self._get_css_param_number("pe-", "xs")
|
||||
|
||||
@pe_xs.setter
|
||||
def pe_xs(self, value: Union[int, str, None]):
|
||||
self._set_css_param_number("pe-", value, "xs")
|
||||
|
||||
@property
|
||||
def pe_sm(self) -> Union[int, str, None]:
|
||||
"""
|
||||
@ -1577,6 +1672,17 @@ class BootstrapContainer(HTML.Div):
|
||||
def m(self, value: Union[int, str, None]):
|
||||
self._set_css_param_number("m-", value)
|
||||
|
||||
@property
|
||||
def m_xs(self) -> Union[int, str, None]:
|
||||
"""
|
||||
element's margin for small screens. By default, bootstrap defines the values 0,1,2,3,4,5,auto
|
||||
"""
|
||||
return self._get_css_param_number("m-", "xs")
|
||||
|
||||
@m_xs.setter
|
||||
def m_xs(self, value: Union[int, str, None]):
|
||||
self._set_css_param_number("m-", value, "xs")
|
||||
|
||||
@property
|
||||
def m_sm(self) -> Union[int, str, None]:
|
||||
"""
|
||||
@ -1643,6 +1749,17 @@ class BootstrapContainer(HTML.Div):
|
||||
def mx(self, value: Union[int, str, None]):
|
||||
self._set_css_param_number("mx-", value)
|
||||
|
||||
@property
|
||||
def mx_xs(self) -> Union[int, str, None]:
|
||||
"""
|
||||
element's margin-left and margin-right for small screens. By default, bootstrap defines the values 0,1,2,3,4,5,auto
|
||||
"""
|
||||
return self._get_css_param_number("mx-", "xs")
|
||||
|
||||
@mx_xs.setter
|
||||
def mx_xs(self, value: Union[int, str, None]):
|
||||
self._set_css_param_number("mx-", value, "xs")
|
||||
|
||||
@property
|
||||
def mx_sm(self) -> Union[int, str, None]:
|
||||
"""
|
||||
@ -1709,6 +1826,17 @@ class BootstrapContainer(HTML.Div):
|
||||
def my(self, value: Union[int, str, None]):
|
||||
self._set_css_param_number("my-", value)
|
||||
|
||||
@property
|
||||
def my_xs(self) -> Union[int, str, None]:
|
||||
"""
|
||||
element's margin-top and margin-bottom for small screens. By default, bootstrap defines the values 0,1,2,3,4,5,auto
|
||||
"""
|
||||
return self._get_css_param_number("my-", "xs")
|
||||
|
||||
@my_xs.setter
|
||||
def my_xs(self, value: Union[int, str, None]):
|
||||
self._set_css_param_number("my-", value, "xs")
|
||||
|
||||
@property
|
||||
def my_sm(self) -> Union[int, str, None]:
|
||||
"""
|
||||
@ -1775,6 +1903,17 @@ class BootstrapContainer(HTML.Div):
|
||||
def mt(self, value: Union[int, str, None]):
|
||||
self._set_css_param_number("mt-", value)
|
||||
|
||||
@property
|
||||
def mt_xs(self) -> Union[int, str, None]:
|
||||
"""
|
||||
element's margin-top for small screens. By default, bootstrap defines the values 0,1,2,3,4,5,auto
|
||||
"""
|
||||
return self._get_css_param_number("mt-", "xs")
|
||||
|
||||
@mt_xs.setter
|
||||
def mt_xs(self, value: Union[int, str, None]):
|
||||
self._set_css_param_number("mt-", value, "xs")
|
||||
|
||||
@property
|
||||
def mt_sm(self) -> Union[int, str, None]:
|
||||
"""
|
||||
@ -1841,6 +1980,17 @@ class BootstrapContainer(HTML.Div):
|
||||
def mb(self, value: Union[int, str, None]):
|
||||
self._set_css_param_number("mb-", value)
|
||||
|
||||
@property
|
||||
def mb_xs(self) -> Union[int, str, None]:
|
||||
"""
|
||||
element's margin-bottom for small screens. By default, bootstrap defines the values 0,1,2,3,4,5,auto
|
||||
"""
|
||||
return self._get_css_param_number("mb-", "xs")
|
||||
|
||||
@mb_xs.setter
|
||||
def mb_xs(self, value: Union[int, str, None]):
|
||||
self._set_css_param_number("mb-", value, "xs")
|
||||
|
||||
@property
|
||||
def mb_sm(self) -> Union[int, str, None]:
|
||||
"""
|
||||
@ -1907,6 +2057,17 @@ class BootstrapContainer(HTML.Div):
|
||||
def ms(self, value: Union[int, str, None]):
|
||||
self._set_css_param_number("ms-", value)
|
||||
|
||||
@property
|
||||
def ms_xs(self) -> Union[int, str, None]:
|
||||
"""
|
||||
element's start margin (left for LTR languages) for small screens. By default, bootstrap defines the values 0,1,2,3,4,5,auto
|
||||
"""
|
||||
return self._get_css_param_number("ms-", "xs")
|
||||
|
||||
@ms_xs.setter
|
||||
def ms_xs(self, value: Union[int, str, None]):
|
||||
self._set_css_param_number("ms-", value, "xs")
|
||||
|
||||
@property
|
||||
def ms_sm(self) -> Union[int, str, None]:
|
||||
"""
|
||||
@ -1973,6 +2134,17 @@ class BootstrapContainer(HTML.Div):
|
||||
def me(self, value: Union[int, str, None]):
|
||||
self._set_css_param_number("me-", value)
|
||||
|
||||
@property
|
||||
def me_xs(self) -> Union[int, str, None]:
|
||||
"""
|
||||
element's end margin (right for LTR languages) for small screens. By default, bootstrap defines the values 0,1,2,3,4,5,auto
|
||||
"""
|
||||
return self._get_css_param_number("me-", "xs")
|
||||
|
||||
@me_xs.setter
|
||||
def me_xs(self, value: Union[int, str, None]):
|
||||
self._set_css_param_number("me-", value, "xs")
|
||||
|
||||
@property
|
||||
def me_sm(self) -> Union[int, str, None]:
|
||||
"""
|
||||
@ -2041,6 +2213,17 @@ class BootstrapContainer(HTML.Div):
|
||||
def g(self, value: Union[int, str, None]):
|
||||
self._set_css_param_number("g-", value)
|
||||
|
||||
@property
|
||||
def g_xs(self) -> Union[int, str, None]:
|
||||
"""
|
||||
gutter (gutters are the space between the element and its content) for small screens. By default, bootstrap defines the values 0,1,2,3,4,5
|
||||
"""
|
||||
return self._get_css_param_number("g-", "xs")
|
||||
|
||||
@g_xs.setter
|
||||
def g_xs(self, value: Union[int, str, None]):
|
||||
self._set_css_param_number("g-", value, "xs")
|
||||
|
||||
@property
|
||||
def g_sm(self) -> Union[int, str, None]:
|
||||
"""
|
||||
@ -2107,6 +2290,17 @@ class BootstrapContainer(HTML.Div):
|
||||
def gx(self, value: Union[int, str, None]):
|
||||
self._set_css_param_number("gx-", value)
|
||||
|
||||
@property
|
||||
def gx_xs(self) -> Union[int, str, None]:
|
||||
"""
|
||||
horizontal gutter (gutters are the space between the element and its content) for small screens. By default, bootstrap defines the values 0,1,2,3,4,5
|
||||
"""
|
||||
return self._get_css_param_number("gx-", "xs")
|
||||
|
||||
@gx_xs.setter
|
||||
def gx_xs(self, value: Union[int, str, None]):
|
||||
self._set_css_param_number("gx-", value, "xs")
|
||||
|
||||
@property
|
||||
def gx_sm(self) -> Union[int, str, None]:
|
||||
"""
|
||||
@ -2173,6 +2367,17 @@ class BootstrapContainer(HTML.Div):
|
||||
def gy(self, value: Union[int, str, None]):
|
||||
self._set_css_param_number("gy-", value)
|
||||
|
||||
@property
|
||||
def gy_xs(self) -> Union[int, str, None]:
|
||||
"""
|
||||
vertical gutter (gutters are the space between the element and its content) for small screens. By default, bootstrap defines the values 0,1,2,3,4,5
|
||||
"""
|
||||
return self._get_css_param_number("gy-", "xs")
|
||||
|
||||
@gy_xs.setter
|
||||
def gy_xs(self, value: Union[int, str, None]):
|
||||
self._set_css_param_number("gy-", value, "xs")
|
||||
|
||||
@property
|
||||
def gy_sm(self) -> Union[int, str, None]:
|
||||
"""
|
||||
@ -2241,6 +2446,17 @@ class BootstrapContainer(HTML.Div):
|
||||
def col(self, value: Union[int, str, None]):
|
||||
self._set_css_param_number("col-", value)
|
||||
|
||||
@property
|
||||
def col_xs(self) -> Union[int, str, None]:
|
||||
"""
|
||||
column class for very small screens. By default, bootstrap defines the values 1,2,3,4,5,6,7,8,9,10,11,12
|
||||
"""
|
||||
return self._get_css_param_number("col-", "xs")
|
||||
|
||||
@col_xs.setter
|
||||
def col_xs(self, value: Union[int, str, None]):
|
||||
self._set_css_param_number("col-", value, "xs")
|
||||
|
||||
@property
|
||||
def col_sm(self) -> Union[int, str, None]:
|
||||
"""
|
||||
|
@ -3,7 +3,7 @@ import uuid
|
||||
|
||||
from .bootstrap_HTML import *
|
||||
from js import document, FileReader, btoa, Uint8Array # type: ignore
|
||||
from pyodide import create_proxy # type: ignore
|
||||
from pyodide.ffi import create_proxy # type: ignore
|
||||
import io
|
||||
import base64
|
||||
|
||||
@ -23,14 +23,6 @@ class InputHelp(BootstrapContainer):
|
||||
class InputFormControl(HTML.Input, BootstrapContainer):
|
||||
_default_class_name: str = "form-control"
|
||||
|
||||
@property
|
||||
def value(self) -> str:
|
||||
return self._parse_value()
|
||||
|
||||
@value.setter
|
||||
def value(self, value: str):
|
||||
self._set_value(value)
|
||||
|
||||
|
||||
class InputFormControlColor(InputFormControl):
|
||||
_default_class_name: str = "form-control form-control-color"
|
||||
@ -331,8 +323,6 @@ class InputFile(InputElement):
|
||||
|
||||
name = f.name
|
||||
|
||||
print("loaded ", name)
|
||||
|
||||
uint8_array = Uint8Array.new(await f.arrayBuffer())
|
||||
buffer = io.BytesIO(bytearray(uint8_array))
|
||||
self._files[name] = buffer
|
||||
|
@ -6,41 +6,150 @@ class PyScriptBootstrapApp(object):
|
||||
def __init__(self, parent_element:str = "pyscript_app"):
|
||||
self._main_div = bHTML.ContainerFluid(id="main")
|
||||
|
||||
self.main_div.w = 100
|
||||
self.main_div.h = 100
|
||||
self._main_div.w = 100
|
||||
self._main_div.vh_100 = True
|
||||
|
||||
self._main_div.p = 0
|
||||
self._main_div.m = 0
|
||||
|
||||
self._main_div.col_xs = 12
|
||||
self._main_div.col = 12
|
||||
|
||||
|
||||
|
||||
self._parent_element = document.getElementById(parent_element)
|
||||
self._parent_element.appendChild(self.main_div.element)
|
||||
self._parent_element.appendChild(self._main_div.element)
|
||||
|
||||
self._toast_container = bHTML.ToastContainer(parent=self._main_div)
|
||||
self._toast_container.position_end = 0
|
||||
self._toast_container.position_bottom = 0
|
||||
|
||||
self._alert_container = bHTML.BootstrapContainer(parent=self._main_div)
|
||||
self._alert_container.position_end = 0
|
||||
self._alert_container.position_bottom = 0
|
||||
self._alert_container.position = bHTML.Position.ABSOLUTE
|
||||
|
||||
def _alert(self, message: str, alert_class: type):
|
||||
return alert_class(message, parent=self._alert_container)
|
||||
bHTML.Al
|
||||
|
||||
@property
|
||||
def main_div(self) -> HTML.Div:
|
||||
def main(self) -> HTML.Div:
|
||||
return self._main_div
|
||||
|
||||
@property
|
||||
def toast_container(self) -> bHTML.ToastContainer:
|
||||
return self._toast_container
|
||||
|
||||
@property
|
||||
def alert_container(self) -> bHTML.BootstrapContainer:
|
||||
return self._alert_container
|
||||
|
||||
def toast(self, message: str, title: str, animation: bool = True, show: bool = True) -> bHTML.Toast:
|
||||
"""
|
||||
show a toast on the default toast location (bottom right)
|
||||
"""
|
||||
toast = bHTML.Toast(inner_html=message, title=title, parent=self._toast_container)
|
||||
toast.animation = animation
|
||||
if show:
|
||||
toast.show()
|
||||
return toast
|
||||
|
||||
def alert(self, message:str) -> bHTML.Alert:
|
||||
"""
|
||||
show an alert on the default alert location (bottom right)
|
||||
"""
|
||||
return self._alert(message, bHTML.Alert)
|
||||
|
||||
def alert_success(self, message:str) -> bHTML.AlertSuccess:
|
||||
"""
|
||||
show an alert on the default alert location (bottom right)
|
||||
"""
|
||||
return self._alert(message, bHTML.AlertSuccess)
|
||||
|
||||
def alert_info(self, message:str) -> bHTML.AlertInfo:
|
||||
"""
|
||||
show an alert on the default alert location (bottom right)
|
||||
"""
|
||||
return self._alert(message, bHTML.AlertInfo)
|
||||
|
||||
def alert_warning(self, message:str) -> bHTML.AlertWarning:
|
||||
"""
|
||||
show an alert on the default alert location (bottom right)
|
||||
"""
|
||||
return self._alert(message, bHTML.AlertWarning)
|
||||
|
||||
def alert_danger(self, message:str) -> bHTML.AlertDanger:
|
||||
"""
|
||||
show an alert on the default alert location (bottom right)
|
||||
"""
|
||||
return self._alert(message, bHTML.AlertDanger)
|
||||
|
||||
def alert_primary(self, message:str) -> bHTML.AlertPrimary:
|
||||
"""
|
||||
show an alert on the default alert location (bottom right)
|
||||
"""
|
||||
return self._alert(message, bHTML.AlertPrimary)
|
||||
|
||||
def alert_secondary(self, message:str) -> bHTML.AlertSecondary:
|
||||
"""
|
||||
show an alert on the default alert location (bottom right)
|
||||
"""
|
||||
return self._alert(message, bHTML.AlertSecondary)
|
||||
|
||||
def alert_light(self, message:str) -> bHTML.AlertLight:
|
||||
"""
|
||||
show an alert on the default alert location (bottom right)
|
||||
"""
|
||||
return self._alert(message, bHTML.AlertLight)
|
||||
|
||||
def alert_dark(self, message:str) -> bHTML.AlertDark:
|
||||
"""
|
||||
show an alert on the default alert location (bottom right)
|
||||
"""
|
||||
return self._alert(message, bHTML.AlertDark)
|
||||
|
||||
class PyScriptBootstrapDashboard(PyScriptBootstrapApp):
|
||||
def __init__(self, parent_element:str = "pyscript_app", brand_name = "Dashboard"):
|
||||
|
||||
super().__init__(parent_element)
|
||||
|
||||
row = bHTML.Row(parent=self.main_div)
|
||||
row.height = "100%"
|
||||
row.width = "100%"
|
||||
row.mt = 5
|
||||
row.display_property = bHTML.DisplayProperty.INLINE_FLEX
|
||||
#row.set_attribute("style", "flex-shrink: 0;")
|
||||
|
||||
self._sidebar = bHTML.Col(id="sidebar", col_sm=5, col_md=4, col_lg=3, col_xl=3, parent=row)
|
||||
self._sidebar = bHTML.Col(id="sidebar", col=12, col_sm=12, col_md=4, col_lg=3, col_xl=3)
|
||||
self._sidebar.style = {"height": "100vh", "overflow": "auto"}
|
||||
|
||||
self._navbar = bHTML.NavbarDark(
|
||||
parent=self.main_div,
|
||||
parent=self._main_div,
|
||||
brand = brand_name,
|
||||
nav_fill=True,
|
||||
toggle_button_for_target=self.sidebar
|
||||
)
|
||||
|
||||
self._navbar.position = bHTML.Position.FIXED_TOP
|
||||
self._navbar.p = 2
|
||||
self._navbar.height = "4em"
|
||||
self._navbar.ps = 5
|
||||
self._navbar.pe = 5
|
||||
self._navbar.col = 12
|
||||
|
||||
|
||||
top_margin_dummy = bHTML.BootstrapContainer(id="top_margin_dummy", parent=self._main_div)
|
||||
top_margin_dummy.height = "3.5em" # have it slightly less high than navbar
|
||||
top_margin_dummy.width = "100vw"
|
||||
top_margin_dummy.m = 0
|
||||
top_margin_dummy.p = 0
|
||||
|
||||
|
||||
row = bHTML.Row(parent=self._main_div)
|
||||
row.w = 100
|
||||
|
||||
row.height = "calc(100vh - 4em)"
|
||||
row.p = 0
|
||||
row.m = 0
|
||||
row.col_xs = 12
|
||||
row.col = 12
|
||||
|
||||
|
||||
row.display_property = bHTML.DisplayProperty.INLINE_FLEX
|
||||
|
||||
row.append_child(self.sidebar)
|
||||
self._sidebar.add_classes("sidebar")
|
||||
self._sidebar.background_color = bHTML.BackgroundColor.LIGHT
|
||||
@ -48,19 +157,20 @@ class PyScriptBootstrapDashboard(PyScriptBootstrapApp):
|
||||
self._sidebar.g = 2
|
||||
self._sidebar.position = bHTML.Position.STATIC
|
||||
self._sidebar.collapsable = True
|
||||
self._sidebar.height = "100%"
|
||||
self._sidebar.mw = 100
|
||||
self._sidebar.height = "calc(100vh - 4em)"
|
||||
self._sidebar.shadow = bHTML.Shadow.LARGE
|
||||
self._sidebar.overflow = bHTML.Overflow.SCROLL
|
||||
|
||||
|
||||
|
||||
self._modal = bHTML.Modal(parent=self.main_div, title="Modal")
|
||||
self._modal = bHTML.Modal(parent=self._main_div, title="Modal")
|
||||
|
||||
self._main_area = bHTML.Col(id="main_area", parent=row, col=12, col_sm=12, col_md=8, col_lg=9, col_xl=9)
|
||||
|
||||
self._main_area = bHTML.Col(id="main_area", parent=row, col_lg=9, col_xl=9)
|
||||
self._main_area.p = 4
|
||||
self._main_area.overflow = bHTML.Overflow.SCROLL
|
||||
self._main_area.height = "100%"
|
||||
self._main_area.height = "calc(100vh - 4em)"
|
||||
self._main_area.mw = 100
|
||||
|
||||
def show_modal(self):
|
||||
@ -86,5 +196,5 @@ class PyScriptBootstrapDashboard(PyScriptBootstrapApp):
|
||||
return self._modal
|
||||
|
||||
@property
|
||||
def main_area(self) -> HTML.Div:
|
||||
def main(self) -> HTML.Div:
|
||||
return self._main_area
|
||||
|
@ -1,14 +1,12 @@
|
||||
|
||||
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
|
||||
|
||||
import datetime as dt
|
||||
import pkg_resources
|
||||
|
||||
def download_file(url: str, path: pathlib.Path):
|
||||
if pathlib.Path(url).exists():
|
||||
@ -18,27 +16,158 @@ def download_file(url: str, path: pathlib.Path):
|
||||
path.write_bytes(response.content)
|
||||
|
||||
|
||||
def create_pwa_manifest(root_folder: pathlib.Path, **kwargs):
|
||||
"""
|
||||
creating the PWA's manifest file
|
||||
"""
|
||||
|
||||
#TODO: make icon configurable
|
||||
|
||||
manifest_dict = {
|
||||
"name": kwargs['title'],
|
||||
"short_name": kwargs['title'],
|
||||
"start_url": "./index.html",
|
||||
"scope": ".",
|
||||
"display": "standalone",
|
||||
"background_color": kwargs['pwa_bg_color'],
|
||||
"theme_color": kwargs['pwa_theme_color'],
|
||||
"icons": [
|
||||
{
|
||||
"src": "resources/icon-512x512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
with open(root_folder / "manifest.json", "w") as f:
|
||||
json.dump(manifest_dict, f)
|
||||
|
||||
# create dummy icon TODO: making this configurable
|
||||
|
||||
default_icon_path = pkg_resources.resource_filename('pyscript_bootstrap_templates', 'data/icon.png')
|
||||
download_file(str(default_icon_path), root_folder / "resources" / "icon-512x512.png")
|
||||
|
||||
def create_pwa_service_worker(root_folder: pathlib.Path, **kwargs):
|
||||
version = f"{kwargs['title']}_{dt.datetime.utcnow().strftime('%Y%m%d%H%M')}"
|
||||
bslash = "\\"
|
||||
assets = [
|
||||
*[f'"./{p}",' for p in kwargs['paths']],
|
||||
'"./index.html",',
|
||||
'"./main.py",',
|
||||
'"./resources/bootstrap.css",',
|
||||
'"./resources/bootstrap.js",',
|
||||
'"./resources/pyscript.css",',
|
||||
'"./resources/pyscript.js",',
|
||||
'"./resources/pyscript.py",',
|
||||
f'"./resources/{kwargs["pyscript_bootstrap_templates_wheel_url"].replace(bslash,"/").split("/")[-1]}",', # FIXME: not completely os independent
|
||||
'"./resources/pwa.js",',
|
||||
'"./site.js",'
|
||||
]
|
||||
|
||||
assets_str = "[" + "\n ".join(assets) + "\n]"
|
||||
|
||||
service_worker_js = f"""
|
||||
const pwa_version = "{version}"
|
||||
const assets = {assets_str}
|
||||
self.addEventListener("install", installEvent => {{
|
||||
installEvent.waitUntil(
|
||||
caches.open(pwa_version).then(cache => {{
|
||||
cache.addAll(assets).then(r => {{
|
||||
console.log("Cache assets downloaded");
|
||||
}}).catch(err => console.log("Error caching item", err))
|
||||
console.log(`Cache ${{pwa_version}} opened.`);
|
||||
}}).catch(err => console.log("Error opening cache", err))
|
||||
)
|
||||
}})
|
||||
|
||||
self.addEventListener('activate', e => {{
|
||||
console.log('Service Worker: Activated');
|
||||
}});
|
||||
|
||||
self.addEventListener('fetch', event => {{
|
||||
if (event.request.method != 'GET')
|
||||
return;
|
||||
event.respondWith((async () => {{
|
||||
const cachedResponse = await caches.match(event.request);
|
||||
if (cachedResponse) {{
|
||||
return cachedResponse;
|
||||
}}
|
||||
|
||||
const response = await fetch(event.request);
|
||||
|
||||
if (!response || response.status !== 200 || response.type !== 'basic') {{
|
||||
return response;
|
||||
}}
|
||||
|
||||
const responseToCache = response.clone();
|
||||
const cache = await caches.open(pwa_version)
|
||||
await cache.put(event.request, response.clone());
|
||||
|
||||
return response;
|
||||
}})());
|
||||
}});
|
||||
self.addEventListener('activate', (event) => {{
|
||||
event.waitUntil(
|
||||
caches.keys().then((keyList) => {{
|
||||
return Promise.all(keyList.map((key) => {{
|
||||
if(key !== pwa_version) {{
|
||||
return caches.delete(key);
|
||||
}}
|
||||
}}));
|
||||
}})
|
||||
);
|
||||
}});
|
||||
"""
|
||||
|
||||
pwa_js = """
|
||||
if ("serviceWorker" in navigator) {
|
||||
window.addEventListener("load", function () {
|
||||
navigator.serviceWorker
|
||||
.register("./serviceWorker.js")
|
||||
.then(res => console.log("service worker registered", res))
|
||||
.catch(err => console.log("service worker not registered", err))
|
||||
})
|
||||
}
|
||||
"""
|
||||
|
||||
site_js = """
|
||||
"""
|
||||
|
||||
# write js files:
|
||||
(root_folder / "serviceWorker.js").write_text(service_worker_js, encoding="utf-8")
|
||||
(root_folder / "resources" / "pwa.js").write_text(pwa_js, encoding="utf-8")
|
||||
(root_folder / "site.js").write_text(site_js, encoding="utf-8")
|
||||
|
||||
|
||||
|
||||
def generate_project_files(root_folder: pathlib.Path,
|
||||
title: str,
|
||||
packages: List[str],
|
||||
paths: List[str] = None,
|
||||
pyscript_css_url: str = "https://pyscript.net/alpha/pyscript.css",
|
||||
pyscript_js_url: str = "https://pyscript.net/alpha/pyscript.min.js",
|
||||
pyscript_py_url: str = "https://pyscript.net/alpha/pyscript.py",
|
||||
pyscript_css_url: str = "https://pyscript.net/latest/pyscript.css",
|
||||
pyscript_js_url: str = "https://pyscript.net/latest/pyscript.min.js",
|
||||
pyscript_py_url: str = "https://pyscript.net/latest/pyscript.py",
|
||||
bootstrap_css_url: str = "https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css",
|
||||
bootstrap_js_url: str = "https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js",
|
||||
pyscript_bootstrap_templates_wheel_url: str = "https://the-cake-is-a-lie.net/gogs/jonas/pyscript-bootstrap-templates/raw/branch/main/dist/pyscript_bootstrap_templates-0.1.0-py3-none-any.whl"):
|
||||
pyscript_bootstrap_templates_wheel_url: str = "https://github.com/antielektron/pyscript_bootstrap_templates/raw/main/dist/pyscript_bootstrap_templates-0.2.0-py3-none-any.whl",
|
||||
**_):
|
||||
|
||||
pyenv = f"- ./resources/{pyscript_bootstrap_templates_wheel_url.split('/')[-1]}"
|
||||
for package in packages:
|
||||
pyenv += f"\n - {package}"
|
||||
if paths is None:
|
||||
paths = []
|
||||
|
||||
if paths is not None:
|
||||
pyenv += "\n - paths:"
|
||||
for path in paths:
|
||||
pyenv += f"\n - {path}"
|
||||
|
||||
pyenv += "\n"
|
||||
pyconfig = {
|
||||
"splashscreen":{
|
||||
"autoclose": True,
|
||||
},
|
||||
"packages": [
|
||||
f"./resources/{pyscript_bootstrap_templates_wheel_url.split('/')[-1]}",
|
||||
*packages
|
||||
],
|
||||
"paths": [
|
||||
*paths
|
||||
]
|
||||
}
|
||||
|
||||
html = f"""
|
||||
<!DOCTYPE html>
|
||||
@ -47,19 +176,21 @@ def generate_project_files(root_folder: pathlib.Path,
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
|
||||
<script src="./resources/pwa.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="./resources/pyscript.css" />
|
||||
<script defer src="./resources/pyscript.js"></script>
|
||||
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="./resources/bootstrap.css" rel="stylesheet">
|
||||
|
||||
<title>{title}</title>
|
||||
|
||||
<py-env>
|
||||
{pyenv}
|
||||
</py-env>
|
||||
</head>
|
||||
<body style="width: 100%; height: 100%">
|
||||
<py-config type="json">
|
||||
{json.dumps(pyconfig, indent=4)}
|
||||
</py-config>
|
||||
<div id="pyscript_app" style="height: 100%; min-height: 100%"></div>
|
||||
<py-script src="./main.py"></py-script>
|
||||
|
||||
@ -81,7 +212,7 @@ 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)
|
||||
"You clicked me!", parent=app.main)
|
||||
"""
|
||||
|
||||
root_folder=pathlib.Path(root_folder)
|
||||
@ -108,6 +239,8 @@ btn.onclick = lambda _: bHTML.AlertSuccess(
|
||||
if not main_py.exists():
|
||||
main_py.write_text(py, encoding="utf-8")
|
||||
|
||||
|
||||
|
||||
def create_project(**kwargs):
|
||||
|
||||
root_folder: pathlib.Path = kwargs['root_folder']
|
||||
@ -119,12 +252,15 @@ def create_project(**kwargs):
|
||||
|
||||
# create initial config.json
|
||||
|
||||
generate_project_files(**kwargs)
|
||||
create_pwa_manifest(**kwargs)
|
||||
create_pwa_service_worker(**kwargs)
|
||||
|
||||
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):
|
||||
|
||||
@ -147,6 +283,8 @@ def update_project(**kwargs):
|
||||
config[arg] = val
|
||||
|
||||
generate_project_files(**config)
|
||||
create_pwa_manifest(**kwargs)
|
||||
create_pwa_service_worker(**kwargs)
|
||||
|
||||
config['root_folder'] = str(root_folder)
|
||||
|
||||
@ -168,18 +306,22 @@ def main():
|
||||
"--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,
|
||||
help="the url of the pyscript css file", default="https://pyscript.net/alpha/pyscript.css")
|
||||
argument_parser.add_argument("--pyscript_js_url", type=str,
|
||||
help="the url of the pyscript js file", default="https://pyscript.net/alpha/pyscript.min.js")
|
||||
argument_parser.add_argument("--pyscript_py_url", type=str,
|
||||
help="the url of the pyscript py file", default="https://pyscript.net/alpha/pyscript.py")
|
||||
argument_parser.add_argument("--bootstrap_css_url", type=str,
|
||||
argument_parser.add_argument("--pyscript-css-url", type=str,
|
||||
help="the url of the pyscript css file", default="https://pyscript.net/latest/pyscript.css")
|
||||
argument_parser.add_argument("--pyscript-js-url", type=str,
|
||||
help="the url of the pyscript js file", default="https://pyscript.net/latest/pyscript.js")
|
||||
argument_parser.add_argument("--pyscript-py-url", type=str,
|
||||
help="the url of the pyscript py file", default="https://pyscript.net/latest/pyscript.py")
|
||||
argument_parser.add_argument("--bootstrap-css-url", type=str,
|
||||
help="the url of the bootstrap css file", default="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css")
|
||||
argument_parser.add_argument("--bootstrap_js_url", type=str,
|
||||
argument_parser.add_argument("--bootstrap-js-url", type=str,
|
||||
help="the url of the bootstrap js file", default="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js")
|
||||
argument_parser.add_argument("--pyscript_bootstrap_templates_wheel_url", type=str,
|
||||
help="the url of the pyscript bootstrap templates wheel file", default="https://the-cake-is-a-lie.net/gogs/jonas/pyscript-bootstrap-templates/raw/branch/main/dist/pyscript_bootstrap_templates-0.1.0-py3-none-any.whl")
|
||||
argument_parser.add_argument("--pyscript-bootstrap-templates-wheel-url", type=str,
|
||||
help="the url of the pyscript bootstrap templates wheel file", default="https://github.com/antielektron/pyscript_bootstrap_templates/raw/main/dist/pyscript_bootstrap_templates-0.2.0-py3-none-any.whl")
|
||||
argument_parser.add_argument("--pwa-bg-color", type=str, help="background color for pwa configuration", default="#000000")
|
||||
argument_parser.add_argument("--pwa-theme-color", type=str, help="theme color for pwa configuration", default="#ffffff")
|
||||
|
||||
|
||||
|
||||
args = argument_parser.parse_args()
|
||||
|
||||
@ -193,10 +335,14 @@ def main():
|
||||
'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
|
||||
'pyscript_bootstrap_templates_wheel_url': args.pyscript_bootstrap_templates_wheel_url,
|
||||
'pwa_bg_color': args.pwa_bg_color,
|
||||
'pwa_theme_color': args.pwa_theme_color
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
if args.command == "create":
|
||||
create_project(**config)
|
||||
elif args.command == "update":
|
||||
|
BIN
pyscript_bootstrap_templates/data/icon.png
Normal file
BIN
pyscript_bootstrap_templates/data/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
@ -0,0 +1,18 @@
|
||||
Metadata-Version: 2.1
|
||||
Name: pyscript-bootstrap-templates
|
||||
Version: 0.1.0
|
||||
Summary: templates and basic python/pyscript wrappers for bootstrap 5
|
||||
Home-page: https://github.com/pypa/sampleproject
|
||||
Author: Jonas Weinz
|
||||
Author-email: author@example.com
|
||||
License: UNKNOWN
|
||||
Keywords: sample,setuptools,development
|
||||
Platform: UNKNOWN
|
||||
Requires-Python: >=3.7, <4
|
||||
Description-Content-Type: text/markdown
|
||||
License-File: LICENSE
|
||||
|
||||
# pyscript-bootstrap-templates
|
||||
|
||||
templates and pyscript wrappers for various bootstrap properties
|
||||
|
@ -0,0 +1,9 @@
|
||||
LICENSE
|
||||
README.md
|
||||
setup.py
|
||||
pyscript-bootstrap-templates/pyscript_bootstrap_templates.egg-info/PKG-INFO
|
||||
pyscript-bootstrap-templates/pyscript_bootstrap_templates.egg-info/SOURCES.txt
|
||||
pyscript-bootstrap-templates/pyscript_bootstrap_templates.egg-info/dependency_links.txt
|
||||
pyscript-bootstrap-templates/pyscript_bootstrap_templates.egg-info/entry_points.txt
|
||||
pyscript-bootstrap-templates/pyscript_bootstrap_templates.egg-info/requires.txt
|
||||
pyscript-bootstrap-templates/pyscript_bootstrap_templates.egg-info/top_level.txt
|
@ -0,0 +1 @@
|
||||
|
@ -0,0 +1,2 @@
|
||||
[console_scripts]
|
||||
create_pyscript_bootstrap_app = create:main
|
@ -0,0 +1,2 @@
|
||||
pillow
|
||||
parse
|
@ -0,0 +1 @@
|
||||
|
Reference in New Issue
Block a user