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""" {title} {json.dumps(pyconfig, indent=4)}
""" 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)