358 lines
13 KiB
Python
358 lines
13 KiB
Python
|
|
import pathlib
|
|
from typing import List
|
|
import argparse
|
|
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():
|
|
shutil.copy2(url, str(path))
|
|
else:
|
|
response = requests.get(url)
|
|
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/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://github.com/antielektron/pyscript_bootstrap_templates/raw/main/dist/pyscript_bootstrap_templates-0.2.0-py3-none-any.whl",
|
|
**_):
|
|
|
|
if paths is None:
|
|
paths = []
|
|
|
|
pyconfig = {
|
|
"splashscreen":{
|
|
"autoclose": True,
|
|
},
|
|
"packages": [
|
|
f"./resources/{pyscript_bootstrap_templates_wheel_url.split('/')[-1]}",
|
|
*packages
|
|
],
|
|
"paths": [
|
|
*paths
|
|
]
|
|
}
|
|
|
|
html = f"""
|
|
<!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>{title}</title>
|
|
</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>
|
|
|
|
<script src="./resources/bootstrap.js"></script>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
py = f"""
|
|
from pyscript_bootstrap_templates import bootstrap_templates
|
|
from pyscript_bootstrap_templates import bootstrap_HTML as bHTML
|
|
from pyscript_bootstrap_templates import HTML as HTML
|
|
|
|
app = bootstrap_templates.PyScriptBootstrapDashboard(
|
|
parent_element="pyscript_app", brand_name="{title}")
|
|
div = bHTML.BootstrapContainer("This is a sidebar", parent=app.sidebar)
|
|
div.font_size = 4
|
|
|
|
btn = bHTML.ButtonPrimary("Click me", parent=app.sidebar)
|
|
btn.w = 100
|
|
btn.onclick = lambda _: bHTML.AlertSuccess(
|
|
"You clicked me!", parent=app.main)
|
|
"""
|
|
|
|
root_folder=pathlib.Path(root_folder)
|
|
|
|
root_folder.mkdir(parents=True, exist_ok=True)
|
|
(root_folder / "resources").mkdir(parents=True, exist_ok=True)
|
|
|
|
# Download files:
|
|
download_file(pyscript_css_url, root_folder / "resources" / "pyscript.css")
|
|
download_file(pyscript_js_url, root_folder / "resources" / "pyscript.js")
|
|
download_file(pyscript_py_url, root_folder / "resources" / "pyscript.py")
|
|
download_file(bootstrap_css_url, root_folder /
|
|
"resources" / "bootstrap.css")
|
|
download_file(bootstrap_js_url, root_folder / "resources" / "bootstrap.js")
|
|
download_file(pyscript_bootstrap_templates_wheel_url,
|
|
root_folder / "resources" / pyscript_bootstrap_templates_wheel_url.split("/")[-1])
|
|
|
|
index_html = (root_folder / "index.html")
|
|
main_py = (root_folder / "main.py")
|
|
|
|
index_html.write_text(html, encoding="utf-8")
|
|
|
|
# only create if not existing:
|
|
if not main_py.exists():
|
|
main_py.write_text(py, encoding="utf-8")
|
|
|
|
|
|
|
|
def create_project(**kwargs):
|
|
|
|
root_folder: pathlib.Path = kwargs['root_folder']
|
|
|
|
if root_folder.exists():
|
|
raise ValueError(f"cannot create project. Folder {str(root_folder)} already exists")
|
|
|
|
root_folder.mkdir(parents=True, exist_ok=True)
|
|
|
|
# create initial config.json
|
|
|
|
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)
|
|
|
|
|
|
def update_project(**kwargs):
|
|
|
|
root_folder: pathlib.Path = kwargs['root_folder']
|
|
if not root_folder.exists():
|
|
raise ValueError(f"cannot update project in {str(root_folder)}. Path does not exist")
|
|
|
|
# load config file
|
|
config_json = root_folder / "config.json"
|
|
if not config_json.exists():
|
|
raise ValueError(f"cannot update project in {str(root_folder)}. Found no config.json inside give path")
|
|
|
|
with open(config_json, "r") as f:
|
|
config = json.load(f)
|
|
|
|
# override config values that are not None or empty lists
|
|
for arg, val in kwargs.items():
|
|
if val is not None:
|
|
if not isinstance(val, list) or len(val) > 0:
|
|
config[arg] = val
|
|
|
|
generate_project_files(**config)
|
|
create_pwa_manifest(**kwargs)
|
|
create_pwa_service_worker(**kwargs)
|
|
|
|
config['root_folder'] = str(root_folder)
|
|
|
|
with open(root_folder / "config.json", 'w') as f:
|
|
json.dump(config, f, indent=4)
|
|
|
|
def main():
|
|
|
|
argument_parser = argparse.ArgumentParser(
|
|
description="create a new pyscript project")
|
|
|
|
argument_parser.add_argument("command", choices=["create","update"])
|
|
|
|
argument_parser.add_argument(
|
|
"root_folder", type=pathlib.Path, help="the root folder of the new project")
|
|
argument_parser.add_argument(
|
|
"title", type=str, help="the title of the new project")
|
|
argument_parser.add_argument(
|
|
"--packages", type=str, nargs="+", help="the packages to include in the new project")
|
|
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/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,
|
|
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://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()
|
|
|
|
config = {
|
|
'root_folder': args.root_folder,
|
|
'title': args.title,
|
|
'packages': args.packages if args.packages is not None else [],
|
|
'paths': args.paths if args.paths is not None else [],
|
|
'pyscript_css_url': args.pyscript_css_url,
|
|
'pyscript_js_url': args.pyscript_js_url,
|
|
'pyscript_py_url': args.pyscript_py_url,
|
|
'bootstrap_css_url': args.bootstrap_css_url,
|
|
'bootstrap_js_url': args.bootstrap_js_url,
|
|
'pyscript_bootstrap_templates_wheel_url': args.pyscript_bootstrap_templates_wheel_url,
|
|
'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":
|
|
update_project(**config)
|
|
else:
|
|
# should never happen and be catched by argparse#
|
|
raise ValueError("unknown command", args.command)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
exit(0)
|