Compare commits

17 Commits
devel ... main

Author SHA1 Message Date
75efac61b9 updated readmy 2023-06-11 18:49:04 +02:00
dbea51a2b0 update examples 2023-06-11 18:43:29 +02:00
1eecb7c0a3 fix readme 2023-06-05 11:50:47 +02:00
f188cff5b4 change default package path to github 2023-06-04 20:23:11 +02:00
4257a75557 Update README.md 2023-06-04 20:09:14 +02:00
91c6272f9d add example links 2023-06-04 20:07:31 +02:00
97d8e85523 add some documentation 2023-06-04 19:18:57 +02:00
cf144e5b28 many small improvements 2023-06-04 18:51:06 +02:00
95c4319340 new advanced example, small fixes 2023-06-03 12:21:28 +02:00
c51c1ba64a new example, fix input bug 2022-10-16 18:41:50 +02:00
c236959c01 fix deprecated method call in new project 2022-10-16 17:12:02 +02:00
60e9678b8b forgot to add setup.py 2022-10-16 17:04:00 +02:00
4ca2cdff70 improved pwa functionality 2022-10-16 12:54:39 +02:00
3dc051aba1 initial pwa support 2022-10-16 10:23:01 +02:00
b3d1804a2e small bottstrap template improvements 2022-09-24 17:49:06 +02:00
01eec041b5 some minor tweaks 2022-09-13 19:18:24 +02:00
c0bb89c752 adding simple toasts 2022-09-13 16:51:43 +02:00
75 changed files with 119593 additions and 157 deletions

1
MANIFEST.IN Normal file
View File

@ -0,0 +1 @@
include pyscript_bootstrap_templates/data/*.png

View File

@ -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.
![image-20230604191240295](img/demo.png)
* 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
```

View File

@ -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.

View File

@ -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"
}

View File

@ -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>

View File

@ -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

View 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"}]}

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View 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

View 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);
}
}));
})
);
});

View File

@ -0,0 +1,2 @@

View File

@ -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"
}

View File

@ -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>

View File

@ -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

View 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"}]}

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View 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

View 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);
}
}));
})
);
});

View File

@ -0,0 +1,2 @@

View 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"
}

View 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>

View 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

View 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"}]}

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.

After

Width:  |  Height:  |  Size: 18 KiB

View 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))
})
}

View 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;
}

File diff suppressed because one or more lines are too long

View File

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

View File

@ -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);
}
}));
})
);
});

View File

@ -0,0 +1,2 @@

View 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"
}

View 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>

View 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

View 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"}]}

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.

After

Width:  |  Height:  |  Size: 18 KiB

View 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))
})
}

View 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;
}

File diff suppressed because one or more lines are too long

View File

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

View File

@ -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);
}
}));
})
);
});

View File

@ -0,0 +1,2 @@

BIN
img/demo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 503 KiB

View File

@ -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
@ -209,6 +210,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:

View File

@ -1 +1 @@
__version__ = "0.1.0"
__version__ = "0.2.0"

View File

@ -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):

View File

@ -314,6 +314,15 @@ class BootstrapContainer(HTML.Div):
def text_align(self, value: Union[TextAlign, None]):
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]:
@ -558,6 +567,15 @@ class BootstrapContainer(HTML.Div):
def display_property(self, value: Union[DisplayProperty, None]):
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]:
@ -1112,6 +1130,17 @@ class BootstrapContainer(HTML.Div):
@p.setter
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]:
@ -1178,6 +1207,17 @@ class BootstrapContainer(HTML.Div):
@px.setter
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]:
"""
@ -1376,6 +1438,17 @@ class BootstrapContainer(HTML.Div):
@pb.setter
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]:
@ -1442,6 +1515,17 @@ class BootstrapContainer(HTML.Div):
@ps.setter
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]:
@ -1508,6 +1592,17 @@ class BootstrapContainer(HTML.Div):
@pe.setter
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]:
@ -1576,6 +1671,17 @@ class BootstrapContainer(HTML.Div):
@m.setter
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]:
@ -1642,6 +1748,17 @@ class BootstrapContainer(HTML.Div):
@mx.setter
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]:
@ -1708,6 +1825,17 @@ class BootstrapContainer(HTML.Div):
@my.setter
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]:
@ -1774,6 +1902,17 @@ class BootstrapContainer(HTML.Div):
@mt.setter
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]:
@ -1840,6 +1979,17 @@ class BootstrapContainer(HTML.Div):
@mb.setter
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]:
"""
@ -1972,6 +2133,17 @@ class BootstrapContainer(HTML.Div):
@me.setter
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]:
"""
@ -2240,6 +2445,17 @@ class BootstrapContainer(HTML.Div):
@col.setter
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]:

View File

@ -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

View File

@ -6,40 +6,149 @@ 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")
@ -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_lg=9, col_xl=9)
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.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

View File

@ -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)
@ -107,6 +238,8 @@ btn.onclick = lambda _: bHTML.AlertSuccess(
# only create if not existing:
if not main_py.exists():
main_py.write_text(py, encoding="utf-8")
def create_project(**kwargs):
@ -118,13 +251,16 @@ def create_project(**kwargs):
root_folder.mkdir(parents=True, exist_ok=True)
# 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":

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,2 @@
[console_scripts]
create_pyscript_bootstrap_app = create:main

View File

@ -0,0 +1,2 @@
pillow
parse

View File

@ -36,8 +36,8 @@ setup(
keywords="sample, setuptools, development",
packages=["pyscript_bootstrap_templates"],
include_package_data=True,
package_data={'pyscript_bootstrap_templates': ['data', 'data/icon.png']},
python_requires=">=3.7, <4",
install_requires=[