diff --git a/nicegui-a-browser-frontend-for-your-python-code/README.md b/nicegui-a-browser-frontend-for-your-python-code/README.md new file mode 100644 index 0000000000..fef53992d7 --- /dev/null +++ b/nicegui-a-browser-frontend-for-your-python-code/README.md @@ -0,0 +1,50 @@ +# NiceGUI: A Browser Frontend for Your Python Code + +Sample scripts that build small user interfaces with [NiceGUI](https://nicegui.io/). Each NiceGUI file is a complete app. `csv_to_markdown.py` is a plain CLI helper used by the UI example. + +## Setup + +These examples need Python 3.10 or newer. Create a virtual environment and install the dependencies: + +```console +$ python -m venv venv +$ source venv/bin/activate +$ python -m pip install -r requirements.txt +``` + +On Windows, activate the environment with `venv\Scripts\activate`. + +`requirements.txt` installs NiceGUI 3.17.0 with the Plotly extra, along with pinned versions of Plotly, pandas, and [pywebview](https://pywebview.flowrl.com/). `password_generator_native.py` uses pywebview to open a desktop window. On Linux, pywebview also needs a system WebKit or Qt backend. The [pywebview installation guide](https://pywebview.flowrl.com/guide/installation.html) lists the packages for each platform. + +## Examples + +| Script | What it shows | +| --- | --- | +| `element_catalog.py` | Common inputs (text, number, select, checkbox, radio, slider, switch) and a summary label | +| `estimate_card.py` | A card with quantity, unit price, optional tax, and a calculated total | +| `password_generator.py` | A password generator in the browser | +| `password_generator_native.py` | The same password generator in a native desktop window | +| `csv_to_markdown.py` | CLI that converts a CSV file to a Markdown table | +| `csv_to_markdown_ui.py` | Browser UI for the converter, with progress and `run.io_bound` | +| `request_chart.py` | An ECharts bar chart and a table of request counts | +| `plotly_chart.py` | A Plotly bar chart from a pandas DataFrame | + +Run a NiceGUI example from this folder: + +```console +$ python password_generator.py +``` + +NiceGUI prints a local URL, usually `http://127.0.0.1:8080`. Open that address in a browser. Stop the app with Ctrl+C. + +The native example opens its own window: + +```console +$ python password_generator_native.py +``` + +The CLI converter works without starting a server: + +```console +$ python csv_to_markdown.py people.csv +``` diff --git a/nicegui-a-browser-frontend-for-your-python-code/csv_to_markdown.py b/nicegui-a-browser-frontend-for-your-python-code/csv_to_markdown.py new file mode 100644 index 0000000000..12c5edaf56 --- /dev/null +++ b/nicegui-a-browser-frontend-for-your-python-code/csv_to_markdown.py @@ -0,0 +1,68 @@ +import argparse +import csv +from io import StringIO +from pathlib import Path + + +def csv_to_markdown( + text: str, + *, + delimiter: str = ",", + header: bool = True, +) -> str: + raw = text.strip() + if not raw: + raise ValueError("CSV text is empty") + + rows = list(csv.reader(StringIO(raw), delimiter=delimiter)) + if not rows: + raise ValueError("CSV text has no rows") + if any(len(row) != len(rows[0]) for row in rows): + raise ValueError("CSV rows do not all have the same number of columns") + + if header: + headings = [cell.strip() or " " for cell in rows[0]] + body = rows[1:] + else: + headings = [f"Col {index}" for index in range(1, len(rows[0]) + 1)] + body = rows + + lines = [ + "| " + " | ".join(headings) + " |", + "| " + " | ".join("---" for _ in headings) + " |", + ] + for row in body: + lines.append("| " + " | ".join(cell.strip() for cell in row) + " |") + return "\n".join(lines) + "\n" + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Convert a CSV file to a Markdown table." + ) + parser.add_argument("csv_file", type=Path, help="path to a CSV file") + parser.add_argument( + "-d", + "--delimiter", + default=",", + help="field delimiter (default: comma)", + ) + parser.add_argument( + "--no-header", + action="store_true", + help="treat the first row as data", + ) + args = parser.parse_args() + text = args.csv_file.read_text(encoding="utf-8") + print( + csv_to_markdown( + text, + delimiter=args.delimiter, + header=not args.no_header, + ), + end="", + ) + + +if __name__ == "__main__": + main() diff --git a/nicegui-a-browser-frontend-for-your-python-code/csv_to_markdown_ui.py b/nicegui-a-browser-frontend-for-your-python-code/csv_to_markdown_ui.py new file mode 100644 index 0000000000..0faeef366c --- /dev/null +++ b/nicegui-a-browser-frontend-for-your-python-code/csv_to_markdown_ui.py @@ -0,0 +1,50 @@ +from nicegui import run, ui + +from csv_to_markdown import csv_to_markdown + +SAMPLE = "name,role\nAda,Engineer\nGrace,Scientist\n" + +source = ui.textarea("CSV", value=SAMPLE).classes("w-full").props("rows=8") +delimiter = ui.select( + {",": "Comma", ";": "Semicolon", "\t": "Tab"}, + value=",", + label="Delimiter", +) +header = ui.checkbox("First row is the header", value=True) +status = ui.label("Paste CSV, then click Convert.") +progress = ui.linear_progress(show_value=False, size="8px").props( + "indeterminate" +) +progress.set_visibility(False) +markdown = ui.textarea("Markdown").classes("w-full").props("rows=8") + + +async def convert() -> None: + raw = source.value or "" + convert_button.disable() + progress.set_visibility(True) + status.set_text("Converting") + try: + result = await run.io_bound( + csv_to_markdown, + raw, + delimiter=delimiter.value, + header=header.value, + ) + except ValueError as exc: + markdown.set_value("") + status.set_text(str(exc)) + ui.notify(str(exc), type="negative") + return + finally: + progress.set_visibility(False) + convert_button.enable() + + markdown.set_value(result) + status.set_text("Conversion complete.") + ui.notify("Markdown table ready", type="positive") + + +convert_button = ui.button("Convert", on_click=convert) + +ui.run() diff --git a/nicegui-a-browser-frontend-for-your-python-code/element_catalog.py b/nicegui-a-browser-frontend-for-your-python-code/element_catalog.py new file mode 100644 index 0000000000..e6d83709f1 --- /dev/null +++ b/nicegui-a-browser-frontend-for-your-python-code/element_catalog.py @@ -0,0 +1,28 @@ +from nicegui import ui + +name = ui.input("Project", value="notes") +notes = ui.textarea("Notes", value="Ship the export") +copies = ui.number("Copies", value=2, min=1, max=10) +kind = ui.select(["Draft", "Final"], value="Draft", label="Kind") +urgent = ui.checkbox("Urgent") +ui.label("Extension") +extension = ui.radio(["txt", "csv"], value="txt") +ui.label("Priority") +priority = ui.slider(min=0, max=10, value=3) +archive = ui.switch("Archive after") +summary = ui.label("No choices yet") + + +def show() -> None: + summary.set_text( + f"{name.value}: {kind.value}.{extension.value}, " + f"copies={copies.value}, urgent={urgent.value}, " + f"priority={priority.value}, archive={archive.value}" + ) + if notes.value: + ui.notify(notes.value) + + +ui.button("Show choices", on_click=show) + +ui.run() diff --git a/nicegui-a-browser-frontend-for-your-python-code/estimate_card.py b/nicegui-a-browser-frontend-for-your-python-code/estimate_card.py new file mode 100644 index 0000000000..b2ea707e12 --- /dev/null +++ b/nicegui-a-browser-frontend-for-your-python-code/estimate_card.py @@ -0,0 +1,28 @@ +from nicegui import ui + +with ui.row().classes("w-full items-center gap-4 p-4"): + ui.icon("request_quote", size="md").props("color=primary") + ui.label("Estimate").classes("text-h6 text-weight-bold") + +with ui.card().classes("w-80"): + quantity = ui.number("Quantity", value=2, min=1) + unit_price = ui.number("Unit price", value=40.0, min=0, format="%.2f") + taxed = ui.checkbox("Add 20% tax") + total = ui.label("Total: —").classes("text-subtitle1") + + def calculate() -> None: + qty = quantity.value + price = unit_price.value + if qty is None or price is None or qty < 1 or price < 0: + total.set_text("Total: —") + ui.notify("Enter a quantity and a unit price", type="warning") + return + + amount = float(qty) * float(price) + if taxed.value: + amount *= 1.2 + total.set_text(f"Total: {amount:.2f}") + + ui.button("Calculate", on_click=calculate).props("color=primary") + +ui.run() diff --git a/nicegui-a-browser-frontend-for-your-python-code/password_generator.py b/nicegui-a-browser-frontend-for-your-python-code/password_generator.py new file mode 100644 index 0000000000..789a67d685 --- /dev/null +++ b/nicegui-a-browser-frontend-for-your-python-code/password_generator.py @@ -0,0 +1,35 @@ +import secrets +import string + +from nicegui import ui + +length = ui.number("Length", value=16, min=8, max=64) +digits = ui.checkbox("Include digits", value=True) +symbols = ui.checkbox("Include symbols") +password = ui.input("Password").props("readonly") +status = ui.label("Click Generate for a new password.") + + +def generate() -> None: + size = length.value + if size is None or size != int(size) or size < 8 or size > 64: + password.set_value("") + status.set_text("Choose a whole length from 8 to 64.") + ui.notify("Length must be a whole number from 8 to 64", type="warning") + return + + alphabet = string.ascii_letters + if digits.value: + alphabet += string.digits + if symbols.value: + alphabet += "!@#$%^&*()-_=+" + + size = int(size) + result = "".join(secrets.choice(alphabet) for _ in range(size)) + password.set_value(result) + status.set_text(f"Generated a {size}-character password.") + + +ui.button("Generate", on_click=generate) + +ui.run() diff --git a/nicegui-a-browser-frontend-for-your-python-code/password_generator_native.py b/nicegui-a-browser-frontend-for-your-python-code/password_generator_native.py new file mode 100644 index 0000000000..bef186958f --- /dev/null +++ b/nicegui-a-browser-frontend-for-your-python-code/password_generator_native.py @@ -0,0 +1,35 @@ +import secrets +import string + +from nicegui import ui + +length = ui.number("Length", value=16, min=8, max=64) +digits = ui.checkbox("Include digits", value=True) +symbols = ui.checkbox("Include symbols") +password = ui.input("Password").props("readonly") +status = ui.label("Click Generate for a new password.") + + +def generate() -> None: + size = length.value + if size is None or size != int(size) or size < 8 or size > 64: + password.set_value("") + status.set_text("Choose a whole length from 8 to 64.") + ui.notify("Length must be a whole number from 8 to 64", type="warning") + return + + alphabet = string.ascii_letters + if digits.value: + alphabet += string.digits + if symbols.value: + alphabet += "!@#$%^&*()-_=+" + + size = int(size) + result = "".join(secrets.choice(alphabet) for _ in range(size)) + password.set_value(result) + status.set_text(f"Generated a {size}-character password.") + + +ui.button("Generate", on_click=generate) + +ui.run(native=True) diff --git a/nicegui-a-browser-frontend-for-your-python-code/people.csv b/nicegui-a-browser-frontend-for-your-python-code/people.csv new file mode 100644 index 0000000000..78ed284e8a --- /dev/null +++ b/nicegui-a-browser-frontend-for-your-python-code/people.csv @@ -0,0 +1,3 @@ +name,role +Ada,Engineer +Grace,Scientist diff --git a/nicegui-a-browser-frontend-for-your-python-code/plotly_chart.py b/nicegui-a-browser-frontend-for-your-python-code/plotly_chart.py new file mode 100644 index 0000000000..b904160b83 --- /dev/null +++ b/nicegui-a-browser-frontend-for-your-python-code/plotly_chart.py @@ -0,0 +1,26 @@ +import pandas as pd +import plotly.express as px +from nicegui import ui + +traffic = pd.DataFrame( + { + "Endpoint": [ + "/login", + "/api/items", + "/api/search", + "/checkout", + "/health", + "/admin", + ], + "Requests": [1280, 2540, 1875, 940, 4120, 210], + } +) +fig = px.bar( + traffic, + x="Endpoint", + y="Requests", + title="Requests by Endpoint", +) +ui.plotly(fig).classes("w-full") + +ui.run() diff --git a/nicegui-a-browser-frontend-for-your-python-code/request_chart.py b/nicegui-a-browser-frontend-for-your-python-code/request_chart.py new file mode 100644 index 0000000000..7f4327ebc4 --- /dev/null +++ b/nicegui-a-browser-frontend-for-your-python-code/request_chart.py @@ -0,0 +1,28 @@ +from nicegui import ui + +traffic = [ + {"endpoint": "/login", "requests": 1280}, + {"endpoint": "/api/items", "requests": 2540}, + {"endpoint": "/api/search", "requests": 1875}, + {"endpoint": "/checkout", "requests": 940}, + {"endpoint": "/health", "requests": 4120}, + {"endpoint": "/admin", "requests": 210}, +] + +ui.echart( + { + "title": {"text": "Requests by Endpoint"}, + "xAxis": { + "type": "category", + "data": [row["endpoint"] for row in traffic], + }, + "yAxis": {"type": "value"}, + "series": [ + {"type": "bar", "data": [row["requests"] for row in traffic]} + ], + } +).classes("w-full") + +ui.table(rows=traffic) + +ui.run() diff --git a/nicegui-a-browser-frontend-for-your-python-code/requirements.txt b/nicegui-a-browser-frontend-for-your-python-code/requirements.txt new file mode 100644 index 0000000000..3fc4963672 --- /dev/null +++ b/nicegui-a-browser-frontend-for-your-python-code/requirements.txt @@ -0,0 +1,4 @@ +nicegui[plotly]==3.17.0 +plotly==6.9.0 +pandas==3.0.5 +pywebview==6.2.1