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..7a50366962 --- /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 for the Real Python NiceGUI tutorial. Each NiceGUI file is a complete app. `generate_password_cli.py` is a plain CLI helper used by the password UI example. + +## Setup + +These examples need **Python 3.11 or later**. 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/). `hello_nicegui_native.py` uses pywebview to open a desktop window. On Linux, install a GTK backend as in the tutorial (`pywebview[gtk]` plus the system libraries from the [pywebview installation guide](https://pywebview.flowrl.com/guide/installation.html)). + +## Examples + +| Script | What it shows | +| --- | --- | +| `hello_nicegui.py` | Minimal label and button in the browser | +| `hello_nicegui_native.py` | Same app in a native desktop window (`native=True`) | +| `element_catalog.py` | Common inputs and a summary label | +| `generate_password_cli.py` | CLI password generator | +| `generate_password_ui.py` | Browser UI that calls the CLI function | +| `estimate_card.py` | Layout, styling, and a custom color palette | +| `request_chart.py` | Built-in ECharts bar chart and table | +| `plotly_chart.py` | Plotly bar chart from a pandas DataFrame | + +Run a NiceGUI example from this folder: + +```console +$ python hello_nicegui.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 hello_nicegui_native.py +``` + +The password CLI works without starting a server: + +```console +$ python generate_password_cli.py -l 24 --symbols +``` 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..51a40ecf68 --- /dev/null +++ b/nicegui-a-browser-frontend-for-your-python-code/element_catalog.py @@ -0,0 +1,27 @@ +from nicegui import ui + + +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) + + +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") +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..a7dd8ba198 --- /dev/null +++ b/nicegui-a-browser-frontend-for-your-python-code/estimate_card.py @@ -0,0 +1,43 @@ +from nicegui import ui + +ui.colors( + primary="#0F766E", + secondary="#334155", + accent="#C2410C", +) + +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") + ui.space() + ui.badge("Draft").props("color=secondary") + +with ui.card().classes("w-80 shadow-2"): + ui.label("Line item").classes("text-subtitle2 text-weight-medium") + quantity = ui.number("Quantity", value=2, min=1).classes("w-full") + unit_price = ui.number( + "Unit price", value=40.0, min=0, format="%.2f" + ).classes("w-full") + taxed = ui.checkbox("Add 20% tax") + ui.separator() + total = ui.label("Total: —").classes("text-h6 text-weight-bold") + ui.label("Tax is off by default.").classes("text-caption text-grey-7") + + 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").classes( + "w-full q-mt-md" + ) + +ui.run() diff --git a/nicegui-a-browser-frontend-for-your-python-code/generate_password_cli.py b/nicegui-a-browser-frontend-for-your-python-code/generate_password_cli.py new file mode 100644 index 0000000000..62b798d626 --- /dev/null +++ b/nicegui-a-browser-frontend-for-your-python-code/generate_password_cli.py @@ -0,0 +1,60 @@ +import argparse +import secrets +import string + +MIN_LENGTH = 8 +MAX_LENGTH = 64 +DEFAULT_LENGTH = 16 +SYMBOLS = "!@#$%^&*()-_=+" + + +def generate_password( + length: int, + digits: bool = True, + symbols: bool = False, +) -> str: + if not MIN_LENGTH <= length <= MAX_LENGTH: + raise ValueError(f"length must be from {MIN_LENGTH} to {MAX_LENGTH}") + + alphabet = string.ascii_letters + if digits: + alphabet += string.digits + if symbols: + alphabet += SYMBOLS + return "".join(secrets.choice(alphabet) for _ in range(length)) + + +def main() -> None: + parser = argparse.ArgumentParser(description="Generate a random password.") + parser.add_argument( + "-l", + "--length", + type=int, + default=DEFAULT_LENGTH, + help=( + f"number of characters, {MIN_LENGTH} to {MAX_LENGTH} " + f"(default: {DEFAULT_LENGTH})" + ), + ) + parser.add_argument( + "--digits", + action=argparse.BooleanOptionalAction, + default=True, + help="allow digits (default: on)", + ) + parser.add_argument( + "--symbols", + action=argparse.BooleanOptionalAction, + default=False, + help="allow symbols (default: off)", + ) + args = parser.parse_args() + + try: + print(generate_password(args.length, args.digits, args.symbols)) + except ValueError as error: + parser.error(str(error)) + + +if __name__ == "__main__": + main() diff --git a/nicegui-a-browser-frontend-for-your-python-code/generate_password_ui.py b/nicegui-a-browser-frontend-for-your-python-code/generate_password_ui.py new file mode 100644 index 0000000000..af44e016f7 --- /dev/null +++ b/nicegui-a-browser-frontend-for-your-python-code/generate_password_ui.py @@ -0,0 +1,48 @@ +from nicegui import ui + +from generate_password_cli import ( + DEFAULT_LENGTH, + MAX_LENGTH, + MIN_LENGTH, + generate_password, +) + + +def generate() -> None: + size = length.value + if size is None: + show_error("Enter a length.") + return + if not float(size).is_integer(): + show_error("Enter a whole number for the length.") + return + + try: + result = generate_password(int(size), digits.value, symbols.value) + except ValueError as error: + show_error(f"{str(error).capitalize()}.") + return + + password.set_value(result) + status.set_text(f"Generated a new password with {int(size)} characters.") + + +def show_error(message: str) -> None: + password.set_value("") + status.set_text(message) + ui.notify(message, type="warning") + + +length = ui.number( + "Length", + value=DEFAULT_LENGTH, + min=MIN_LENGTH, + max=MAX_LENGTH, +) +digits = ui.checkbox("Allow digits", value=True) +symbols = ui.checkbox("Allow symbols") +password = ui.input("Password").props("readonly").classes("w-full") +status = ui.label("Click Generate for a new password.") +ui.button("Generate", on_click=generate) + +ui.run() diff --git a/nicegui-a-browser-frontend-for-your-python-code/hello_nicegui.py b/nicegui-a-browser-frontend-for-your-python-code/hello_nicegui.py new file mode 100644 index 0000000000..f835bbb52d --- /dev/null +++ b/nicegui-a-browser-frontend-for-your-python-code/hello_nicegui.py @@ -0,0 +1,6 @@ +from nicegui import ui + +ui.label("Hello, NiceGUI!") +ui.button("Click me", on_click=lambda: ui.notify("It works!")) + +ui.run() diff --git a/nicegui-a-browser-frontend-for-your-python-code/hello_nicegui_native.py b/nicegui-a-browser-frontend-for-your-python-code/hello_nicegui_native.py new file mode 100644 index 0000000000..daa2c56fc8 --- /dev/null +++ b/nicegui-a-browser-frontend-for-your-python-code/hello_nicegui_native.py @@ -0,0 +1,5 @@ +from nicegui import ui + +ui.label("Hello, NiceGUI!") +ui.button("Click me", on_click=lambda: ui.notify("It works!")) +ui.run(native=True) 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