initial commit

This commit is contained in:
Jonas Weinz 2022-07-24 11:58:11 +02:00
parent d02c23e9fb
commit 4a0dae531e
8 changed files with 7006 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
__version__ = "0.1.0"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,646 @@
import datetime as dt
import uuid
from bootstrap_HTML import *
from js import document, FileReader, btoa # type: ignore
from pyodide import create_proxy # type: ignore
import io
import base64
class InputLabel(HTML.Label, BootstrapContainer):
_default_class_name: str = "form-label"
class InputLabelCheckbox(InputLabel):
_default_class_name: str = "form-check-label"
class InputHelp(BootstrapContainer):
_default_class_name: str = "form-text"
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"
class InputFormControlNumber(InputFormControl):
_default_class_name: str = "form-control"
@property
def min(self):
return self.element.getAttribute("min")
@min.setter
def min(self, value):
self.element.setAttribute("min", str(value))
@property
def max(self):
return self.element.getAttribute("max")
@max.setter
def max(self, value):
self.element.setAttribute("max", str(value))
class InputFormControlRange(InputFormControl):
_default_class_name: str = "form-range"
class InputFormControlSelect(InputFormControl):
_default_class_name: str = "form-select"
_tag_type: str = "select"
class InputFormControlCheckbox(InputFormControl):
_default_class_name: str = "form-check-input"
class InputElement(BootstrapContainer):
_default_input_type = "text"
_default_input_class = InputFormControl
_default_label_class = InputLabel
def __init__(self,
label_text: str = None,
help_text: str = None,
floating_label: bool = False,
placeholder: str = None,
input_type=None,
id=None,
parent=None):
super().__init__(parent=parent, id=id)
if floating_label and placeholder is None:
placeholder = " " # create a dummy placeholder
self._input = self.__class__._default_input_class(
parent=None,
id=self.id+"_input",
type=input_type if input_type is not None else self._default_input_type,
placeholder=placeholder
)
self._label = self.__class__._default_label_class(
parent=None, id=self.id+"_label", inner_html=label_text)
# if floating_label is True, add the input first, then the label
if floating_label:
self.append_child(self._input)
self.append_child(self._label)
else:
self.append_child(self._label)
self.append_child(self._input)
self._help_text = None
if help_text is not None:
self._help_text = InputHelp(
parent=self, id=self.id+"_help_text", inner_html=help_text)
self.floating_label = floating_label
@property
def is_small(self) -> bool:
return self._input.has_class("form-control-sm")
@is_small.setter
def is_small(self, value: bool):
self._input.set_class_name("form-control-sm", value)
@property
def is_large(self) -> bool:
return self._input.has_class("form-control-lg")
@is_large.setter
def is_large(self, value: bool):
self._input.set_class_name("form-control-lg", value)
@property
def readonly(self) -> bool:
return self._input.has_attribute("readonly")
@readonly.setter
def readonly(self, value: bool):
self._input.set_attribute("readonly", value, is_boolean_attribute=True)
@property
def value(self):
return self._input.value
@value.setter
def value(self, value):
self._input.value = value
@property
def onchange(self):
return self._input.onchange
@onchange.setter
def onchange(self, value):
self._input.onchange = value
@property
def floating_label(self) -> bool:
return self.has_class("form-floating")
@floating_label.setter
def floating_label(self, value: bool):
self.set_class("form-floating", value)
class InputEmail(InputElement):
_default_input_type = "email"
class InputPassword(InputElement):
_default_input_type = "password"
class InputText(InputElement):
_default_input_type = "text"
class InputFloat(InputElement):
_default_input_type = "number"
_default_number_class = float
def __init__(self,
label_text: str = None,
help_text: str = None,
floating_label: bool = False,
placeholder: str = None,
min: _default_number_class = None,
max: _default_number_class = None,
step: _default_number_class = None,
input_type=None,
id=None,
parent=None):
super().__init__(label_text=label_text,
help_text=help_text,
floating_label=floating_label,
placeholder=placeholder,
input_type=input_type,
id=id,
parent=parent)
if min is not None:
self.min = min
if max is not None:
self.max = max
if step is not None:
self.step = step
@property
def value(self) -> _default_number_class:
val = self._input.value
if val == "":
return None
return self.__class__._default_number_class(self._input.value)
@value.setter
def value(self, value: _default_number_class):
self._input.value = str(value) if value is not None else ""
@property
def min(self) -> _default_number_class:
return self.__class__._default_number_class(self._input.get_attribute("min"))
@min.setter
def min(self, value: _default_number_class):
self._input.set_attribute("min", str(value))
@property
def max(self) -> _default_number_class:
return self.__class__._default_number_class(self._input.get_attribute("max"))
@max.setter
def max(self, value: _default_number_class):
self._input.set_attribute("max", str(value))
@property
def step(self) -> _default_number_class:
return self.__class__._default_number_class(self._input.get_attribute("step"))
@step.setter
def step(self, value: _default_number_class):
self._input.set_attribute("step", str(value))
class InputRangeFloat(InputFloat):
_default_input_type = "range"
_default_input_class = InputFormControlRange
class InputInt(InputFloat):
_default_number_class = int
class InputRangeInt(InputInt):
_default_input_type = "range"
_default_input_class = InputFormControlRange
class InputDate(InputElement):
_default_input_type = "date"
@property
def value(self) -> dt.datetime:
if self._input.value == "":
return None
return dt.datetime.strptime(self._input.value, "%Y-%m-%d").date()
@value.setter
def value(self, value: Union[dt.datetime, dt.date, str, None]):
if isinstance(value, str):
self._input.value = value
self._input.value = value.strftime(
"%Y-%m-%d") if value is not None else ""
class InputTime(InputElement):
_default_input_type = "time"
@property
def value(self) -> dt.datetime:
if self._input.value == "":
return None
return dt.datetime.strptime(self._input.value, "%H:%M").time()
@value.setter
def value(self, value: Union[dt.datetime, dt.time, str, None]):
if isinstance(value, str):
self._input.value = value
self._input.value = value.strftime(
"%H:%M") if value is not None else ""
class InputRange(InputElement):
_default_input_type = "range"
class InputFile(InputElement):
_default_input_type = "file"
def __init__(self,
label_text: str = None,
help_text: str = None,
floating_label: bool = False,
placeholder: str = None,
input_type=None,
id=None,
parent=None):
super().__init__(label_text=label_text,
help_text=help_text,
floating_label=floating_label,
placeholder=placeholder,
input_type=input_type,
id=id,
parent=parent)
self._files = {}
self._input.onchange = self._load_file
self._on_file_change = None
def _load_file(self, *args):
def read_file(event, name):
buffer = io.BytesIO()
# FIXME: way too much decoding and encoding between formats here
# but bytearray conversation between js and python drove me crazy!!!
f = btoa(event.target.result)
buffer.write(base64.decodebytes(bytes(f, "utf-8")))
print(btoa(event.target.result))
self._files[name] = buffer
buffer.seek(0)
buffer.name = name # not standardized, but works here
if self._on_file_change is not None:
self._on_file_change(buffer)
fileList = self._input.element.files
self._files = {}
for f in fileList:
# reader is a pyodide.JsProxy
reader = FileReader.new()
# Create a Python proxy for the callback function
onload_event = create_proxy(lambda event, name=f.name: read_file(event, name))
reader.onload = onload_event
reader.readAsBinaryString(f)
return
@property
def value(self):
return self._files
@property
def onchange(self):
return self._on_file_change
@onchange.setter
def onchange(self, value):
self._on_file_change = value
class InputMultiFile(InputFile):
def __init__(self,
label_text: str = None,
help_text: str = None,
floating_label: bool = False,
placeholder: str = None,
input_type=None,
id=None,
parent=None):
super().__init__(label_text=label_text,
help_text=help_text,
floating_label=floating_label,
placeholder=placeholder,
input_type=input_type,
id=id,
parent=parent)
self._input.set_attribute("multiple", True, is_boolean_attribute=True)
@property
def value(self):
files = []
for i in range(self._input.element.files.length):
files.append(self._input.element.files.item(i).name)
return files
class InputColor(InputElement):
_default_input_type = "color"
_default_input_class = InputFormControlColor
class InputDatalist(InputElement):
def __init__(self,
options: List[str],
label_text: str = None,
help_text: str = None,
floating_label: bool = False,
placeholder: str = None,
input_type=None,
id=None,
parent=None):
super().__init__(label_text=label_text,
help_text=help_text,
floating_label=floating_label,
placeholder=placeholder,
input_type=input_type,
id=id,
parent=parent)
self._input.type = None
self._options = options
self._datalist = HTML.DataList(parent=self, id=self.id+"_datalist")
for option in options:
HTML.Option(inner_html=option, parent=self._datalist)
self._input.set_attribute("list", self._datalist.id)
@property
def options(self):
return self._options
@options.setter
def options(self, value):
self._options = value
for c in self._datalist.children:
c.destroy()
for option in self._options:
HTML.Option(inner_html=option, parent=self._datalist)
class InputSelect(InputElement):
_default_input_class = InputFormControlSelect
def __init__(self,
options: List[str],
label_text: str = None,
help_text: str = None,
floating_label: bool = False,
placeholder: str = None,
multiple: bool = False,
input_type=None,
id=None,
parent=None):
super().__init__(label_text=label_text,
help_text=help_text,
floating_label=floating_label,
placeholder=placeholder,
input_type=input_type,
id=id,
parent=parent)
self._input.type = None
self._options = options
self.multiple = multiple
for option in options:
HTML.Option(inner_html=option, parent=self._input)
@property
def options(self):
return self._options
@options.setter
def options(self, value):
self._options = value
for c in self._input.children:
c.destroy()
for option in self._options:
HTML.Option(inner_html=option, parent=self._input)
@property
def multiple(self):
return self._input.has_attribute("multiple")
@multiple.setter
def multiple(self, value):
self._input.set_attribute("multiple", value, is_boolean_attribute=True)
class InputCheckboxSingle(InputElement):
_default_class_name: str = "form-check"
_default_input_type = "checkbox"
_default_input_class = InputFormControlCheckbox
_default_label_class = InputLabelCheckbox
def __init__(self,
label_text: str = None,
help_text: str = None,
floating_label: bool = False,
placeholder: str = None,
name: str = None,
input_type=None,
id=None,
parent=None):
super().__init__(label_text=label_text,
help_text=help_text,
floating_label=floating_label,
placeholder=placeholder,
input_type=input_type,
id=id,
parent=parent)
if name is not None:
self._input.set_attribute("name", name)
@property
def checked(self) -> bool:
return self._input.element.checked
@checked.setter
def checked(self, value: bool):
self._input.element.checked = value
@property
def value(self) -> bool:
return self.checked
@value.setter
def value(self, value: bool):
self.checked = value
@property
def inline(self) -> bool:
return self.has_class("inline")
@inline.setter
def inline(self, value: bool):
self.set_class("inline", value)
class InputRadioSingle(InputCheckboxSingle):
_default_input_type = "radio"
class InputSwitchSingle(InputCheckboxSingle):
_default_class_name: str = "form-check form-switch"
class InputCheckboxGroup(BootstrapContainer):
_default_input_class = InputCheckboxSingle
def __init__(self,
options: List[str],
inline: bool = False,
id: str = None,
label_text: Union[str, HTML.Element] = None,
group_name: str = None,
class_name: str = None,
parent: HTML.Element = None,
inner_html: str = None,) -> None:
super().__init__(inner_html=inner_html,
id=id,
class_name=class_name,
parent=parent)
if group_name is None:
group_name = "id-" + str(uuid.uuid4())
if label_text is not None:
if isinstance(label_text, str):
label_text = HTML.Label(inner_html=label_text, parent=self)
else:
self.append_child(label_text)
self._options = options
self._option_checkboxes = {}
self._group_name = group_name
for option in options:
cb = self.__class__._default_input_class(
option, name=group_name, parent=self)
cb.inline = inline
self._option_checkboxes[option] = cb
@property
def options(self):
return self._options
@options.setter
def options(self, value):
self._options = value
for c in self._option_checkboxes.values():
c.destroy()
self._option_checkboxes = {}
for option in self._options:
cb = self.__class__._default_input_class(
option, name=self._group_name, parent=self)
self._option_checkboxes[option] = cb
@property
def value(self) -> List[str]:
"""
returns a list of checked options
"""
values = []
for option in self._options:
if self._option_checkboxes[option].checked:
values.append(option)
return values
@value.setter
def value(self, value: List[str]):
for option in self._options:
self._option_checkboxes[option].checked = option in value
@property
def option_checkboxes(self) -> Dict[str, InputCheckboxSingle]:
"""
returns the form elements for each option as dictionary
"""
return self._option_checkboxes
class InputRadioGroup(InputCheckboxGroup):
_default_input_class = InputRadioSingle
class InputSwitchGroup(InputCheckboxGroup):
_default_input_class = InputSwitchSingle
class Form(HTML.Form, BootstrapContainer):
pass
# TODO: implement more Form and form validation methods

View File

@ -0,0 +1,90 @@
import HTML
import bootstrap_HTML as bHTML
from js import document # type: ignore
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._parent_element = document.getElementById(parent_element)
self._parent_element.appendChild(self.main_div.element)
@property
def main_div(self) -> HTML.Div:
return self._main_div
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._navbar = bHTML.NavbarDark(
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
row.append_child(self.sidebar)
self._sidebar.add_classes("sidebar")
self._sidebar.background_color = bHTML.BackgroundColor.LIGHT
self._sidebar.p = 4
self._sidebar.g = 2
self._sidebar.position = bHTML.Position.STATIC
self._sidebar.collapsable = True
self._sidebar.height = "100%"
self._sidebar.mw = 100
self._sidebar.shadow = bHTML.Shadow.LARGE
self._sidebar.overflow = bHTML.Overflow.SCROLL
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.p = 4
self._main_area.overflow = bHTML.Overflow.SCROLL
self._main_area.height = "100%"
self._main_area.mw = 100
def show_modal(self):
self.modal.show()
def hide_modal(self):
self.modal.hide()
def toggle_modal(self):
self.modal.toggle()
@property
def header(self) -> HTML.Header:
return self._header
@property
def sidebar(self) -> HTML.Div:
return self._sidebar
@property
def modal(self) -> HTML.Div:
return self._modal
@property
def main_area(self) -> HTML.Div:
return self._main_area

View File

@ -0,0 +1,95 @@
import pathlib
from typing import List
import argparse
# create a new pyscript project
def create_project(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.js"):
pyenv = "- git+https://the-cake-is-a-lie.net/gogs/jonas/pyscript-bootstrap-templates.git"
for package in packages:
pyenv += f"\n - {package}"
if paths is not None:
pyenv += "\n - paths:"
for path in paths:
pyenv += f"\n - {path}"
pyenv += "\n"
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">
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<link rel="stylesheet" href="{pyscript_css_url}" />
<script defer src="{pyscript_js_url}"></script>
<title>{title}</title>
<py-env>
{pyenv}
</py-env>
</head>
<body style="width: 100%; height: 100%">
<div id="simple_app" style="height: 100%; min-height: 100%"></div>
<py-script src="./main.py"></py-script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
</body>
</html>
"""
py = f"""
"""
if not root_folder.exists():
root_folder.mkdir(parents=True)
(root_folder / "index.html").write_text(html, encoding="utf-8")
(root_folder / "main.py").write_text("", encoding="utf-8")
def main():
argument_parser = argparse.ArgumentParser(
description="create a new pyscript project")
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/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.js")
arguments = argument_parser.parse_args()
create_project(arguments.root_folder,
arguments.title,
arguments.packages,
arguments.paths,
arguments.pyscript_css_url,
arguments.pyscript_js_url)
if __name__ == "__main__":
main()
exit(0)

56
setup.py Normal file
View File

@ -0,0 +1,56 @@
from setuptools import setup, find_packages
import pathlib
here = pathlib.Path(__file__).parent.resolve()
def get_version(rel_path):
for line in (here / rel_path).read_text(encoding="utf-8").splitlines():
if line.startswith('__version__'):
delim = '"' if '"' in line else "'"
return line.split(delim)[1]
else:
raise RuntimeError("Unable to find version string.")
# Get the long description from the README file
long_description = (here / "README.md").read_text(encoding="utf-8")
setup(
name="pyscript-bootstrap-templates",
version=get_version("pyscript-bootstrap-templates/__init__.py"),
description="templates and basic python/pyscript wrappers for bootstrap 5",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/pypa/sampleproject",
author="Jonas Weinz",
author_email="author@example.com",
keywords="sample, setuptools, development",
package_dir={"": "pyscript-bootstrap-templates"},
packages=find_packages(where="pyscript-bootstrap-templates"),
python_requires=">=3.7, <4",
install_requires=[
"pillow",
"parse",
],
entry_points={
"console_scripts": [
"create_pyscript_bootstrap_app=create:main",
],
},
)