diff --git a/.vscode/launch.json b/.vscode/launch.json index 278af26..5411c6e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -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"] } ] } diff --git a/README.md b/README.md index b384084..6fb7517 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,11 @@ Bunch of utils to be hosted on GH pages using `pyscript`. Look at: [https://gredowski.com/bucket-of-utils/](https://gredowski.com/bucket-of-utils/) -## Wifi QR code generator (not available on GH pages yet) +## Wifi QR code generator + +### Demo: [https://gredowski.com/bucket-of-utils/bucket_of_utils/qr/index.html](https://gredowski.com/bucket-of-utils/bucket_of_utils/qr/index.html) + +### Usage: ```bash generate_wifi_qr_code --ssid my_ssid --password 1234 --security wpa diff --git a/bucket_of_utils/panel_autogenerator/main.py b/bucket_of_utils/panel_autogenerator/main.py new file mode 100644 index 0000000..e4af68c --- /dev/null +++ b/bucket_of_utils/panel_autogenerator/main.py @@ -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) diff --git a/bucket_of_utils/qr/index.html b/bucket_of_utils/qr/index.html new file mode 100644 index 0000000..7e25f75 --- /dev/null +++ b/bucket_of_utils/qr/index.html @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + 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" + + + + +
+
+ + + + + + diff --git a/bucket_of_utils/qr/index.py b/bucket_of_utils/qr/index.py new file mode 100644 index 0000000..4bd1ec7 --- /dev/null +++ b/bucket_of_utils/qr/index.py @@ -0,0 +1,23 @@ +import enum + +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.servable(target="main") diff --git a/bucket_of_utils/qr/wifi.py b/bucket_of_utils/qr/wifi.py index cc3b87a..e9f56c3 100644 --- a/bucket_of_utils/qr/wifi.py +++ b/bucket_of_utils/qr/wifi.py @@ -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): @@ -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( @@ -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) diff --git a/index.html b/index.html index d95772e..04c866d 100644 --- a/index.html +++ b/index.html @@ -14,6 +14,15 @@

Bucket of utils

+
  • + QR + diff --git a/pyproject.toml b/pyproject.toml index ae69429..3ad41d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/requirements-dev.lock b/requirements-dev.lock index 5552e5c..d2be156 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -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 @@ -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 @@ -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 diff --git a/requirements.lock b/requirements.lock index e9fac80..9b6fa77 100644 --- a/requirements.lock +++ b/requirements.lock @@ -8,28 +8,52 @@ -e file:. attrs==23.1.0 +bleach==6.0.0 +bokeh==3.2.2 certifi==2023.7.22 charset-normalizer==3.2.0 click==8.1.6 colorama==0.4.6 +contourpy==1.1.0 fonttools==4.42.0 idna==3.4 +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 +numpy==1.25.2 +packaging==23.1 +pandas==2.0.3 +panel==1.2.1 +param==1.13.0 pdf-annotate==0.12.0 pdfrw==0.4 pillow==10.0.0 pygments==2.16.1 pyodide-py==0.23.4 pypng==0.20220715.0 +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 +webencodings==0.5.1 +xyzservices==2023.7.0 diff --git a/tests/panel_autogenerator/test_main.py b/tests/panel_autogenerator/test_main.py new file mode 100644 index 0000000..2cddd2e --- /dev/null +++ b/tests/panel_autogenerator/test_main.py @@ -0,0 +1,29 @@ +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 + + +def test_it(): + 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")) + + # pn.serve(widget_box)