Skip to content

Commit

Permalink
Woohoo! Autogenerator is working. Added html for QR Wifi generator
Browse files Browse the repository at this point in the history
  • Loading branch information
piotrgredowski committed Aug 27, 2023
1 parent b5d4efc commit 24c8391
Show file tree
Hide file tree
Showing 10 changed files with 346 additions and 2 deletions.
9 changes: 9 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@
"--security",
"wpa"
]
},
// config for running all tests
{
"name": "Run all tests",
"type": "python",
"request": "launch",
"module": "pytest",
"justMyCode": true,
"args": ["", "tests"]
}
]
}
169 changes: 169 additions & 0 deletions bucket_of_utils/panel_autogenerator/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
"""
WIP.
This is a module that generates a `panel` app for a function.
It's in some places specific for `typer` and needs a lot of work to be more generic.
For now - it allows me to generate some `panel` code for functions.
"""

import enum
import typing
from collections.abc import Callable
from typing import Optional
from typing import TypedDict

import panel as pn
import PIL


def _get_all_args_and_kwargs_names(function: Callable) -> tuple:
args = function.__code__.co_varnames[: function.__code__.co_argcount]
kw_start_index = function.__code__.co_argcount
kw_end_index = kw_start_index + function.__code__.co_kwonlyargcount
kwargs = function.__code__.co_varnames[function.__code__.co_argcount : kw_end_index]
return args, kwargs


def get_type_of_arg(function: Callable, arg_name: str) -> str:
try:
type_ = function.__annotations__.get(arg_name).__args__
return type_[0]
except (AttributeError, IndexError):
return function.__annotations__.get(arg_name, typing.Any)


class WidgetDict(TypedDict):
widget: pn.widgets.Widget
args_factory: Optional[Callable]


PANEL_ELEMENTS_MAP: dict[type, WidgetDict] = {
int: WidgetDict(widget=pn.widgets.IntSlider, args_factory=None),
bool: WidgetDict(widget=pn.widgets.Checkbox, args_factory=None),
str: WidgetDict(widget=pn.widgets.TextInput, args_factory=None),
float: WidgetDict(widget=pn.widgets.FloatSlider, args_factory=None),
enum.Enum: WidgetDict(widget=pn.widgets.Select, args_factory=lambda enum_: {"options": [e.value for e in enum_]}),
}

_PANEL_RESULTS_MAP = {
str: pn.pane.Str,
PIL.Image.Image: pn.pane.PNG,
}


def get_help_for_arg(function: Callable, arg_name: str) -> str | None:
arg = function.__annotations__.get(arg_name, None)
try:
return arg.__metadata__[0].help
except AttributeError:
return None


def generate_panel_code( # noqa: C901,PLR0915
function: Callable,
*,
default_args_values: Optional[dict[str, typing.Any]] = None,
args_to_skip: Optional[list[str]] = None,
additional_widget_map: Optional[dict[type, pn.widgets.Widget]] = None,
) -> str:
if default_args_values is None:
default_args_values = {}
if args_to_skip is None:
args_to_skip = []
if additional_widget_map is None:
additional_widget_map = {}
args, kwargs = _get_all_args_and_kwargs_names(function)

args_types = {arg_name: get_type_of_arg(function, arg_name) for arg_name in args}
kwargs_types = {kwarg_name: get_type_of_arg(function, kwarg_name) for kwarg_name in kwargs}

widgets = {}

elements_map = {**PANEL_ELEMENTS_MAP, **additional_widget_map}

for arg_name, arg_type in [*args_types.items(), *kwargs_types.items()]:
if arg_name in args_to_skip:
continue
widget_dict = elements_map.get(arg_type, None)
if widget_dict is None:
continue
args_factory = widget_dict.get("args_factory", None)

def widget_factory_wrapper(*, widget_dict, args_factory, arg_type):
def widget_factory(*args, **kwargs):
additional_args = {}

if args_factory is not None:
additional_args = args_factory(arg_type)

return widget_dict["widget"](*args, **kwargs, **additional_args)

return widget_factory

widgets[arg_name] = widget_factory_wrapper(
widget_dict=widget_dict,
args_factory=args_factory,
arg_type=arg_type,
)

function_name = " ".join(function.__name__.upper().split("_"))

doc_of_function = function.__doc__ or None
if doc_of_function:
header = pn.pane.Markdown(
f"""
# {function_name}
**{doc_of_function}**
""".strip(),
)

widgets_list = []
for arg_name, widget_type in widgets.items():
name = arg_name
if help_text := get_help_for_arg(function, arg_name):
name = f"{arg_name} ({help_text})"
value = default_args_values.get(arg_name, None)
widget_kwargs = {"name": name}
if value is not None:
widget_kwargs["value"] = value

pass
# widget_kwargs["options"] = [e.name for e in arg_type]
widgets_list.append(widget_type(**widget_kwargs))

button = pn.widgets.Button(name="Run", button_type="primary")
widgets_list.append(button)

args_column = pn.Column(*widgets_list)

results_container = pn.Column(pn.pane.Markdown("# Result"))

results_column = pn.Column()

def _run(column: pn.Column, clicked: pn.widgets.Button):
if not clicked:
return

func_args = []
for widget in column:
for arg_name in [*args, *kwargs]:
if not widget.name.startswith(arg_name):
continue
func_args.append(widget.value)
result = function(*func_args)
results_column.clear()

results_type = type(result)

result_widget = _PANEL_RESULTS_MAP.get(results_type, pn.pane.Str)(result)
results_column.append(result_widget)
results_container.append(results_column)
row.append(results_container)

_runner = pn.bind(_run, args_column, clicked=button)
args_column.append(_runner)
row = pn.Row(pn.Column(header, args_column))

return pn.WidgetBox(row)
47 changes: 47 additions & 0 deletions bucket_of_utils/qr/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<html>
<head>
<link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
<script defer src="https://pyscript.net/latest/pyscript.js"></script>
<script type="text/javascript" src="https://cdn.bokeh.org/bokeh/release/bokeh-3.2.1.js"></script>
<script type="text/javascript" src="https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.2.1.min.js"></script>
<script type="text/javascript" src="https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.2.1.min.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/@holoviz/[email protected]/dist/panel.min.js"></script>

<style>

</style>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-4bw+/aepP/YC94hEpVNVgiZdgIC5+VKNBQNGCHeKRQN+PtmoHDEXuppvnDJzQIu9" crossorigin="anonymous">

</head>
<py-config type="toml">
packages = [
"qrcode",
"pillow",
"typer",
"https://cdn.holoviz.org/panel/1.2.1/dist/wheels/bokeh-3.2.1-py3-none-any.whl",
"https://cdn.holoviz.org/panel/1.2.1/dist/wheels/panel-1.2.1-py3-none-any.whl"
]
[[fetch]]
files = ["./index.py"]
[[fetch]]
from = "../../bucket_of_utils/qr/wifi.py"
to_folder = "bucket_of_utils/qr"
[[fetch]]
from = "../../bucket_of_utils/panel_autogenerator/main.py"
to_folder = "bucket_of_utils/panel_autogenerator"
[[fetch]]
from = "https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/ttf"
files = ["FiraCode-Regular.ttf"]
to_folder = "_static/fonts"

</py-config>

<body>
<main class="container row" id="main">
</main>
<py-script src="./index.py"></py-script>
<py-script> </py-script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-HwwvtgBNo3bZJJLYd8oVXjrBZt8cqVSpeBNS5n7C8IVInixGAoxmnlMuBnhbgrkm" crossorigin="anonymous"></script>
</body>

</html>
27 changes: 27 additions & 0 deletions bucket_of_utils/qr/index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import enum

import panel as pn

from bucket_of_utils.panel_autogenerator.main import PANEL_ELEMENTS_MAP
from bucket_of_utils.panel_autogenerator.main import WidgetDict
from bucket_of_utils.panel_autogenerator.main import generate_panel_code
from bucket_of_utils.qr.wifi import SecurityTypes
from bucket_of_utils.qr.wifi import generate_qr_code

default_args_values = {
"security": "WPA",
"hidden": False,
}
additional_widget_map: dict[type:WidgetDict] = {
SecurityTypes: PANEL_ELEMENTS_MAP[enum.Enum],
}

widget_box = generate_panel_code(
generate_qr_code,
args_to_skip=["directory_to_save_qr_code", "overwrite"],
default_args_values=default_args_values,
additional_widget_map=additional_widget_map,
)
widget_box.append(pn.pane.Markdown("Possible security types are: `WEP`, `WPA` and no security - leave blank"))

widget_box.servable(target="main")
11 changes: 9 additions & 2 deletions bucket_of_utils/qr/wifi.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ class SecurityTypes(enum.StrEnum):

def get_font():
path_to_font = "_static/fonts/FiraCode-Regular.ttf"
return ImageFont.truetype(path_to_font, 30)

try:
return ImageFont.truetype(path_to_font, 30)
except OSError:
return ImageFont.load_default()


def add_text_to_image(image: PIL.Image, text: str, y: int, font: ImageFont):
Expand Down Expand Up @@ -116,8 +120,9 @@ def generate_qr_code( # noqa: PLR0913
bool,
typer.Option(help="Whether to overwrite the QR code if it already exists"),
] = False, # noqa: FBT002
):
) -> PIL.Image:
"""Generate a QR code for a given WIFI network"""
security = SecurityTypes(security.lower())
string = get_qr_string(ssid=ssid, password=password, security=security.value, hidden=hidden)

qr = qrcode.QRCode(
Expand Down Expand Up @@ -164,6 +169,8 @@ def generate_qr_code( # noqa: PLR0913

print(f"QR code saved to: {path}")

return image


def main():
typer.run(generate_qr_code)
Expand Down
9 changes: 9 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ <h1>Bucket of utils</h1>
</li>
</ul>
</li>
<li>
QR
<ul>
<li>
<a href="bucket_of_utils/qr/index.html"
>Generate QR code for Wifi network</a
>
</li>
</ul>
</ul>
</body>
</html>
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ dependencies = [
"types-requests>=2.31.0.2",
"unidecode>=1.3.6",
"qrcode[pil]>=7.4.2",
"panel>=1.2.1",
]
readme = "README.md"
requires-python = ">= 3.11"
Expand Down
22 changes: 22 additions & 0 deletions requirements-dev.lock
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@
-e file:.
attrs==23.1.0
black==23.7.0
bleach==6.0.0
bokeh==3.2.2
certifi==2023.7.22
cfgv==3.3.1
charset-normalizer==3.2.0
click==8.1.6
colorama==0.4.6
contourpy==1.1.0
coverage==7.2.7
distlib==0.3.7
filelock==3.12.2
Expand All @@ -22,12 +25,21 @@ identify==2.5.26
idna==3.4
imgcompare==2.0.1
iniconfig==2.0.0
jinja2==3.1.2
linkify-it-py==2.0.2
markdown==3.4.4
markdown-it-py==3.0.0
markupsafe==2.1.3
mdit-py-plugins==0.4.0
mdurl==0.1.2
mypy==1.4.1
mypy-extensions==1.0.0
nodeenv==1.8.0
numpy==1.25.2
packaging==23.1
pandas==2.0.3
panel==1.2.1
param==1.13.0
pathspec==0.11.2
pdf-annotate==0.12.0
pdfrw==0.4
Expand All @@ -41,18 +53,28 @@ pypng==0.20220715.0
pytest==7.4.0
pytest-cov==4.1.0
pytest-mock==3.11.1
python-dateutil==2.8.2
pytz==2023.3
pyviz-comms==3.0.0
pyyaml==6.0.1
qrcode==7.4.2
requests==2.31.0
rich==13.5.2
ruff==0.0.284
shellingham==1.5.3
six==1.16.0
tornado==6.3.3
tqdm==4.66.1
typer==0.9.0
types-requests==2.31.0.2
types-urllib3==1.26.25.14
typing-extensions==4.7.1
tzdata==2023.3
uc-micro-py==1.0.2
unidecode==1.3.6
urllib3==2.0.4
virtualenv==20.24.2
webencodings==0.5.1
xyzservices==2023.7.0
# The following packages are considered to be unsafe in a requirements file:
setuptools==68.0.0
Loading

0 comments on commit 24c8391

Please sign in to comment.